Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu

## 0.8.33 (2026-06-06)

- Feat: FalkorDB export backend — sibling to Neo4j, selected via `graphify export falkordb [--push falkordb://localhost:6379]`. FalkorDB is OpenCypher-compatible, so the MERGE/SET upsert queries match the Neo4j path; auth is optional and the target graph defaults to `graphify`. Install with `uv tool install "graphifyy[falkordb]"` (#1175).
- Feat: install banner — `graphify install` now prints an amber knowledge-graph brain in the terminal (TTY-only, silent in CI/pipes, never raises).
- Fix: Python `from pkg import submod` package-form imports now resolve to a file-level `imports_from` edge to the submodule file when it exists on disk. Previously these imports produced zero edges, leaving test files as disconnected islands in the graph (up to 66% of test nodes in some corpora). The fix lives in the symbol-resolution post-pass which has filesystem access (#1146).
- Fix: builtin type-annotation nodes (`str`, `int`, `bool`, `float`, `bytes`, `MagicMock`, `Mock`, `AsyncMock`, etc.) no longer appear as graph nodes or accumulate edges. They were being created via the annotation walker whenever used as parameter or return types, inflating degree counts ~25% and displacing real abstractions from god-node rankings. A new `_PYTHON_ANNOTATION_NOISE` filter suppresses them at extraction time; `god_nodes` also filters them as a defense for pre-existing graphs (#1147).
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ Install only what you need:
| `video` | Video/audio transcription (faster-whisper + yt-dlp) | `uv tool install "graphifyy[video]"` |
| `mcp` | MCP stdio server | `uv tool install "graphifyy[mcp]"` |
| `neo4j` | Neo4j push support | `uv tool install "graphifyy[neo4j]"` |
| `falkordb` | FalkorDB push support | `uv tool install "graphifyy[falkordb]"` |
| `svg` | SVG graph export | `uv tool install "graphifyy[svg]"` |
| `leiden` | Leiden community detection (Python < 3.13 only) | `uv tool install "graphifyy[leiden]"` |
| `ollama` | Ollama local inference | `uv tool install "graphifyy[ollama]"` |
Expand Down Expand Up @@ -496,6 +497,8 @@ graphify install # overwrites the skill file
/graphify ./raw --graphml # export for Gephi / yEd
/graphify ./raw --neo4j # generate cypher.txt for Neo4j
/graphify ./raw --neo4j-push bolt://localhost:7687
/graphify ./raw --falkordb # generate cypher.txt for FalkorDB
/graphify ./raw --falkordb-push falkordb://localhost:6379
/graphify ./raw --watch # auto-sync as files change
/graphify ./raw --mcp # start MCP stdio server

Expand Down
50 changes: 36 additions & 14 deletions graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3370,7 +3370,7 @@ def _load_graph(p: str):

elif cmd == "export":
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
if subcmd not in ("html", "callflow-html", "obsidian", "wiki", "svg", "graphml", "neo4j"):
if subcmd not in ("html", "callflow-html", "obsidian", "wiki", "svg", "graphml", "neo4j", "falkordb"):
print("Usage: graphify export <format>", file=sys.stderr)
print(" html [--graph PATH] [--labels PATH] [--node-limit N] [--no-viz]", file=sys.stderr)
print(" callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH] [--output HTML]", file=sys.stderr)
Expand All @@ -3381,6 +3381,8 @@ def _load_graph(p: str):
print(" graphml [--graph PATH]", file=sys.stderr)
print(" neo4j [--graph PATH] [--push URI] [--user U] [--password P]", file=sys.stderr)
print(" (or set NEO4J_PASSWORD instead of --password to keep it off argv)", file=sys.stderr)
print(" falkordb [--graph PATH] [--push URI] [--user U] [--password P]", file=sys.stderr)
print(" (or set FALKORDB_PASSWORD instead of --password to keep it off argv)", file=sys.stderr)
sys.exit(1)

# Parse shared args
Expand All @@ -3402,12 +3404,18 @@ def _load_graph(p: str):
node_limit = 5000
no_viz = False
obsidian_dir = Path(_GRAPHIFY_OUT) / "obsidian"
neo4j_uri: str | None = None
neo4j_user = "neo4j"
# F-031: prefer the NEO4J_PASSWORD env var so the password never
# appears on argv (visible in `ps` output / shell history). The
# explicit --password flag still overrides it for compatibility.
neo4j_password: str | None = os.environ.get("NEO4J_PASSWORD") or None
# Shared push-connection settings for the graph-database sinks (neo4j,
# falkordb), parsed from the generic --push/--user/--password flags below.
push_uri: str | None = None
push_user = "neo4j" # Neo4j default user; FalkorDB auth is optional and ignores it
# F-031: prefer an env var so the password never appears on argv (visible
# in `ps` output / shell history). The explicit --password flag still
# overrides it. Each sink reads its own var: FALKORDB_PASSWORD for falkordb,
# NEO4J_PASSWORD otherwise.
push_password: str | None = (
os.environ.get("FALKORDB_PASSWORD") if subcmd == "falkordb"
else os.environ.get("NEO4J_PASSWORD")
) or None
i = 0
while i < len(args):
a = args[i]
Expand Down Expand Up @@ -3458,11 +3466,11 @@ def _load_graph(p: str):
elif a == "--dir" and i + 1 < len(args):
obsidian_dir = Path(args[i + 1]); i += 2
elif a == "--push" and i + 1 < len(args):
neo4j_uri = args[i + 1]; i += 2
push_uri = args[i + 1]; i += 2
elif a == "--user" and i + 1 < len(args):
neo4j_user = args[i + 1]; i += 2
push_user = args[i + 1]; i += 2
elif a == "--password" and i + 1 < len(args):
neo4j_password = args[i + 1]; i += 2
push_password = args[i + 1]; i += 2
elif subcmd == "callflow-html" and not a.startswith("-") and not graph_path_explicit:
candidate = Path(a)
if candidate.name == "graph.json" or candidate.suffix.lower() == ".json":
Expand Down Expand Up @@ -3613,19 +3621,33 @@ def _load_graph(p: str):
print(f"graph.graphml written - open in Gephi, yEd, or any GraphML tool")

elif subcmd == "neo4j":
if neo4j_uri:
if push_uri:
from graphify.export import push_to_neo4j as _push
if neo4j_password is None:
if push_password is None:
print("error: --password required for --push", file=sys.stderr)
sys.exit(1)
result = _push(G, uri=neo4j_uri, user=neo4j_user,
password=neo4j_password, communities=communities)
result = _push(G, uri=push_uri, user=push_user,
password=push_password, communities=communities)
print(f"Pushed to Neo4j: {result['nodes']} nodes, {result['edges']} edges")
else:
from graphify.export import to_cypher as _to_cypher
_to_cypher(G, str(out_dir / "cypher.txt"))
print(f"cypher.txt written - import with: cypher-shell < {out_dir}/cypher.txt")

elif subcmd == "falkordb":
if push_uri:
from graphify.export import push_to_falkordb as _push
result = _push(G, uri=push_uri, user=push_user,
password=push_password, communities=communities)
print(f"Pushed to FalkorDB: {result['nodes']} nodes, {result['edges']} edges")
else:
from graphify.export import to_cypher as _to_cypher
_to_cypher(G, str(out_dir / "cypher.txt"))
print(f"cypher.txt written ({out_dir}/cypher.txt) - statements are OpenCypher. "
f"FalkorDB's GRAPH.QUERY runs one statement at a time (no bulk script "
f"import), so load a graph with: graphify export falkordb --push "
f"falkordb://localhost:6379")

elif cmd == "benchmark":
from graphify.benchmark import run_benchmark, print_benchmark

Expand Down
96 changes: 96 additions & 0 deletions graphify/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -1309,6 +1309,102 @@ def _safe_label(label: str) -> str:
return {"nodes": nodes_pushed, "edges": edges_pushed}


def push_to_falkordb(
G: nx.Graph,
uri: str,
user: str | None = None,
password: str | None = None,
communities: dict[int, list[str]] | None = None,
graph_name: str = "graphify",
) -> dict[str, int]:
"""Push graph directly to a running FalkorDB instance via the Python SDK.

Requires: pip install falkordb

FalkorDB is OpenCypher-compatible, so the MERGE/SET upsert queries are
identical to push_to_neo4j. Differences from the Neo4j path:
- connects with FalkorDB(host, port, username, password) instead of a bolt
driver; only the host/port are read from the URI, so the scheme is
informational - "falkordb://localhost:6379", "redis://localhost:6379"
and a bare "localhost:6379" are all equivalent (default port 6379).
- a named graph is selected via db.select_graph(graph_name) (default
"graphify"); FalkorDB keys each graph by name in the same instance.
- queries run via graph.query(cypher, params) - there is no session object.
- auth is optional (FalkorDB runs without credentials by default), so user
and password may be None.
- no APOC: the Neo4j path does not use APOC either, so nothing to port.

Uses MERGE so re-running is safe - nodes and edges are upserted, not
duplicated. Returns a dict with counts of nodes and edges pushed.
"""
try:
from falkordb import FalkorDB
except ImportError as e:
raise ImportError(
"falkordb SDK not installed. Run: pip install falkordb"
) from e

from urllib.parse import urlparse

node_community = _node_community_map(communities) if communities else {}

def _safe_rel(relation: str) -> str:
return re.sub(r"[^A-Z0-9_]", "_", relation.upper().replace(" ", "_").replace("-", "_")) or "RELATED_TO"

def _safe_label(label: str) -> str:
"""Sanitize a FalkorDB node label to prevent Cypher injection."""
sanitized = re.sub(r"[^A-Za-z0-9_]", "", label)
return sanitized if sanitized else "Entity"

parsed = urlparse(uri if "://" in uri else f"redis://{uri}")
# FalkorDB auth is optional. Only send credentials when a password is
# provided; otherwise connect anonymously and ignore any bolt-style default
# username (e.g. Neo4j's "neo4j"), which FalkorDB rejects as an unknown ACL
# user. Credentials embedded in the URI take precedence over the args.
connect_user = parsed.username or (user if password else None)
connect_password = parsed.password or (password or None)
db = FalkorDB(
host=parsed.hostname or "localhost",
port=parsed.port or 6379,
username=connect_user,
password=connect_password,
)
graph = db.select_graph(graph_name)
nodes_pushed = 0
edges_pushed = 0

for node_id, data in G.nodes(data=True):
props = {
k: v for k, v in data.items()
if isinstance(v, (str, int, float, bool)) and not k.startswith("_")
}
props["id"] = node_id
cid = node_community.get(node_id)
if cid is not None:
props["community"] = cid
ftype = _safe_label(data.get("file_type", "Entity").capitalize())
graph.query(
f"MERGE (n:{ftype} {{id: $id}}) SET n += $props",
{"id": node_id, "props": props},
)
nodes_pushed += 1

for u, v, data in G.edges(data=True):
rel = _safe_rel(data.get("relation", "RELATED_TO"))
props = {
k: v for k, v in data.items()
if isinstance(v, (str, int, float, bool)) and not k.startswith("_")
}
graph.query(
f"MATCH (a {{id: $src}}), (b {{id: $tgt}}) "
f"MERGE (a)-[r:{rel}]->(b) SET r += $props",
{"src": u, "tgt": v, "props": props},
)
edges_pushed += 1

return {"nodes": nodes_pushed, "edges": edges_pushed}


def to_graphml(
G: nx.Graph,
communities: dict[int, list[str]],
Expand Down
6 changes: 4 additions & 2 deletions graphify/skill-amp.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Turn any folder of files into a navigable knowledge graph with community detecti
/graphify <path> --graphml # export graph.graphml (Gephi, yEd)
/graphify <path> --neo4j # generate graphify-out/cypher.txt for Neo4j
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
/graphify <path> --falkordb # generate graphify-out/cypher.txt for FalkorDB
/graphify <path> --falkordb-push falkordb://localhost:6379 # push directly to FalkorDB
/graphify <path> --mcp # start MCP stdio server for agent access
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
/graphify <path> --wiki # build agent-crawlable wiki (index.md + one article per community)
Expand Down Expand Up @@ -475,9 +477,9 @@ graphify export html # auto-aggregates to community view if graph > 5000 nodes
# or: graphify export html --no-viz
```

### Steps 6b-8 - Wiki, Neo4j, SVG, GraphML, MCP, benchmark (only on their flags)
### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags)

These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.
These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--falkordb`/`--falkordb-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.

---

Expand Down
6 changes: 4 additions & 2 deletions graphify/skill-claw.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Turn any folder of files into a navigable knowledge graph with community detecti
/graphify <path> --graphml # export graph.graphml (Gephi, yEd)
/graphify <path> --neo4j # generate graphify-out/cypher.txt for Neo4j
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
/graphify <path> --falkordb # generate graphify-out/cypher.txt for FalkorDB
/graphify <path> --falkordb-push falkordb://localhost:6379 # push directly to FalkorDB
/graphify <path> --mcp # start MCP stdio server for agent access
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
/graphify <path> --wiki # build agent-crawlable wiki (index.md + one article per community)
Expand Down Expand Up @@ -478,9 +480,9 @@ graphify export html # auto-aggregates to community view if graph > 5000 nodes
# or: graphify export html --no-viz
```

### Steps 6b-8 - Wiki, Neo4j, SVG, GraphML, MCP, benchmark (only on their flags)
### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags)

These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.
These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--falkordb`/`--falkordb-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.

---

Expand Down
6 changes: 4 additions & 2 deletions graphify/skill-codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Turn any folder of files into a navigable knowledge graph with community detecti
/graphify <path> --graphml # export graph.graphml (Gephi, yEd)
/graphify <path> --neo4j # generate graphify-out/cypher.txt for Neo4j
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
/graphify <path> --falkordb # generate graphify-out/cypher.txt for FalkorDB
/graphify <path> --falkordb-push falkordb://localhost:6379 # push directly to FalkorDB
/graphify <path> --mcp # start MCP stdio server for agent access
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
/graphify <path> --wiki # build agent-crawlable wiki (index.md + one article per community)
Expand Down Expand Up @@ -475,9 +477,9 @@ graphify export html # auto-aggregates to community view if graph > 5000 nodes
# or: graphify export html --no-viz
```

### Steps 6b-8 - Wiki, Neo4j, SVG, GraphML, MCP, benchmark (only on their flags)
### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags)

These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.
These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--falkordb`/`--falkordb-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.

---

Expand Down
6 changes: 4 additions & 2 deletions graphify/skill-copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Turn any folder of files into a navigable knowledge graph with community detecti
/graphify <path> --graphml # export graph.graphml (Gephi, yEd)
/graphify <path> --neo4j # generate graphify-out/cypher.txt for Neo4j
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
/graphify <path> --falkordb # generate graphify-out/cypher.txt for FalkorDB
/graphify <path> --falkordb-push falkordb://localhost:6379 # push directly to FalkorDB
/graphify <path> --mcp # start MCP stdio server for agent access
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
/graphify <path> --wiki # build agent-crawlable wiki (index.md + one article per community)
Expand Down Expand Up @@ -478,9 +480,9 @@ graphify export html # auto-aggregates to community view if graph > 5000 nodes
# or: graphify export html --no-viz
```

### Steps 6b-8 - Wiki, Neo4j, SVG, GraphML, MCP, benchmark (only on their flags)
### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags)

These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.
These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--falkordb`/`--falkordb-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.

---

Expand Down
Loading