feat: cross-repo merge-graphs; fix #527 CLAUDE_CONFIG_DIR; fix #524 graphify-out excluded from source scan

This commit is contained in:
Safi
2026-04-23 19:59:14 +01:00
parent 2c49da24f0
commit 2faeed99a2
3 changed files with 67 additions and 4 deletions
+49 -1
View File
@@ -148,7 +148,12 @@ def install(platform: str = "claude") -> None:
print(f"error: {cfg['skill_file']} not found in package - reinstall graphify", file=sys.stderr)
sys.exit(1)
skill_dst = Path.home() / cfg["skill_dst"]
import os as _os
if platform in ("claude", "windows") and _os.environ.get("CLAUDE_CONFIG_DIR"):
_claude_base = Path(_os.environ["CLAUDE_CONFIG_DIR"])
skill_dst = _claude_base / "skills" / "graphify" / "SKILL.md"
else:
skill_dst = Path.home() / cfg["skill_dst"]
skill_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(skill_src, skill_dst)
(skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
@@ -977,6 +982,8 @@ def main() -> None:
print(" explain \"X\" plain-language explanation of a node and its neighbors")
print(" --graph <path> path to graph.json (default graphify-out/graph.json)")
print(" clone <github-url> clone a GitHub repo locally and print its path for /graphify")
print(" merge-graphs <g1> <g2> merge two or more graph.json files into one cross-repo graph")
print(" --out <path> output path (default: graphify-out/merged-graph.json)")
print(" --branch <branch> checkout a specific branch (default: repo default)")
print(" --out <dir> clone to a custom directory (default: ~/.graphify/repos/<owner>/<repo>)")
print(" add <url> fetch a URL and save it to ./raw, then update the graph")
@@ -1415,6 +1422,47 @@ def main() -> None:
from graphify.watch import check_update
check_update(Path(sys.argv[2]).resolve())
sys.exit(0)
elif cmd == "merge-graphs":
# graphify merge-graphs graph1.json graph2.json ... --out merged.json
args = sys.argv[2:]
graph_paths: list[Path] = []
out_path = Path("graphify-out/merged-graph.json")
i = 0
while i < len(args):
if args[i] == "--out" and i + 1 < len(args):
out_path = Path(args[i + 1]); i += 2
else:
graph_paths.append(Path(args[i])); i += 1
if len(graph_paths) < 2:
print("Usage: graphify merge-graphs <graph1.json> <graph2.json> [...] [--out merged.json]", file=sys.stderr)
sys.exit(1)
import networkx as _nx
from networkx.readwrite import json_graph as _jg
graphs = []
for gp in graph_paths:
if not gp.exists():
print(f"error: not found: {gp}", file=sys.stderr)
sys.exit(1)
data = json.loads(gp.read_text(encoding="utf-8"))
try:
G = _jg.node_link_graph(data, edges="links")
except TypeError:
G = _jg.node_link_graph(data)
# Tag every node with which repo it came from
repo_tag = gp.parent.parent.name # graphify-out/../ → repo dir name
for node in G.nodes:
G.nodes[node].setdefault("repo", repo_tag)
graphs.append(G)
merged = _nx.compose_all(graphs)
try:
out_data = _jg.node_link_data(merged, edges="links")
except TypeError:
out_data = _jg.node_link_data(merged)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(out_data, indent=2), encoding="utf-8")
print(f"Merged {len(graphs)} graphs → {merged.number_of_nodes()} nodes, {merged.number_of_edges()} edges")
print(f"Written to: {out_path}")
elif cmd == "clone":
if len(sys.argv) < 3:
print("Usage: graphify clone <github-url> [--branch <branch>] [--out <dir>]", file=sys.stderr)
+1
View File
@@ -241,6 +241,7 @@ _SKIP_DIRS = {
"site-packages", "lib64",
".pytest_cache", ".mypy_cache", ".ruff_cache",
".tox", ".eggs", "*.egg-info",
"graphify-out", # never treat own output as source input (#524)
}
# Large generated files that are never useful to extract
+17 -3
View File
@@ -15,6 +15,7 @@ Turn any folder of files into a navigable knowledge graph with community detecti
/graphify <path> # full pipeline on specific path
/graphify https://github.com/<owner>/<repo> # clone repo then run full pipeline on it
/graphify https://github.com/<owner>/<repo> --branch <branch> # clone a specific branch
/graphify <url1> <url2> ... # clone multiple repos, build each, merge into one cross-repo graph
/graphify <path> --mode deep # thorough extraction, richer INFERRED edges
/graphify <path> --update # incremental - re-extract only new/changed files
/graphify <path> --directed # build directed graph (preserves edge direction: source→target)
@@ -63,15 +64,28 @@ If the path argument starts with `https://github.com/` or `http://github.com/`,
Follow these steps in order. Do not skip steps.
### Step 0 - Clone GitHub repo (only if a GitHub URL was given)
### Step 0 - Clone GitHub repo(s) (only if a GitHub URL was given)
**Single repo:**
```bash
# Clone the repo (or pull if already cloned) and capture the local path
LOCAL_PATH=$(graphify clone <github-url> [--branch <branch>])
# Use LOCAL_PATH as the target for all subsequent steps
```
Graphify clones into `~/.graphify/repos/<owner>/<repo>` so repeated calls on the same URL reuse the existing clone. Print the resolved path to the user before continuing. If `--branch` was specified, pass it through.
**Multiple repos (cross-repo graph):**
```bash
# Clone each repo, run the full pipeline on each, then merge
graphify clone <url1> # → ~/.graphify/repos/<owner1>/<repo1>
graphify clone <url2> # → ~/.graphify/repos/<owner2>/<repo2>
# Run /graphify on each local path to produce their graph.json files
# Then merge:
graphify merge-graphs \
~/.graphify/repos/<owner1>/<repo1>/graphify-out/graph.json \
~/.graphify/repos/<owner2>/<repo2>/graphify-out/graph.json \
--out graphify-out/cross-repo-graph.json
```
Graphify clones into `~/.graphify/repos/<owner>/<repo>` and reuses existing clones on repeat runs. Each node in the merged graph carries a `repo` attribute so you can filter by origin.
### Step 1 - Ensure graphify is installed