diff --git a/graphify/analyze.py b/graphify/analyze.py index 8ca7ddac..50f85aca 100644 --- a/graphify/analyze.py +++ b/graphify/analyze.py @@ -109,19 +109,16 @@ def _is_concept_node(G: nx.Graph, node_id: str) -> bool: return False -_CODE_EXTENSIONS = {"py", "ts", "tsx", "js", "go", "rs", "java", "rb", "cpp", "c", "h", "cs", "kt", "scala", "php"} -_DOC_EXTENSIONS = {"md", "txt", "rst"} -_PAPER_EXTENSIONS = {"pdf"} -_IMAGE_EXTENSIONS = {"png", "jpg", "jpeg", "webp", "gif", "svg"} +from graphify.detect import CODE_EXTENSIONS, DOC_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS def _file_category(path: str) -> str: - ext = path.rsplit(".", 1)[-1].lower() if "." in path else "" - if ext in _CODE_EXTENSIONS: + ext = ("." + path.rsplit(".", 1)[-1].lower()) if "." in path else "" + if ext in CODE_EXTENSIONS: return "code" - if ext in _PAPER_EXTENSIONS: + if ext in PAPER_EXTENSIONS: return "paper" - if ext in _IMAGE_EXTENSIONS: + if ext in IMAGE_EXTENSIONS: return "image" return "doc" diff --git a/graphify/cluster.py b/graphify/cluster.py index 09d07040..363d555c 100644 --- a/graphify/cluster.py +++ b/graphify/cluster.py @@ -52,23 +52,6 @@ def _partition(G: nx.Graph) -> dict[str, int]: return {node: cid for cid, nodes in enumerate(communities) for node in nodes} -def build_graph(nodes: list[dict], edges: list[dict]) -> nx.Graph: - """Build a NetworkX graph from graphify node/edge dicts. - - Preserves original edge direction as _src/_tgt attributes so that - display functions can show relationships in the correct direction, - even though the graph is undirected for structural analysis. - """ - G = nx.Graph() - for n in nodes: - G.add_node(n["id"], **{k: v for k, v in n.items() if k != "id"}) - for e in edges: - attrs = {k: v for k, v in e.items() if k not in ("source", "target")} - attrs["_src"] = e["source"] - attrs["_tgt"] = e["target"] - G.add_edge(e["source"], e["target"], **attrs) - return G - _MAX_COMMUNITY_FRACTION = 0.25 # communities larger than 25% of graph get split _MIN_SPLIT_SIZE = 10 # only split if community has at least this many nodes diff --git a/graphify/export.py b/graphify/export.py index d7a7349a..300f93ac 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -183,9 +183,28 @@ function focusNode(nodeId) {{ showInfo(nodeId); }} +// Track hovered node — hover detection is more reliable than click params +let hoveredNodeId = null; +network.on('hoverNode', params => {{ + hoveredNodeId = params.node; + container.style.cursor = 'pointer'; +}}); +network.on('blurNode', () => {{ + hoveredNodeId = null; + container.style.cursor = 'default'; +}}); +container.addEventListener('click', () => {{ + if (hoveredNodeId !== null) {{ + showInfo(hoveredNodeId); + network.selectNodes([hoveredNodeId]); + }} +}}); network.on('click', params => {{ - if (params.nodes.length > 0) showInfo(params.nodes[0]); - else document.getElementById('info-content').innerHTML = 'Click a node to inspect it'; + if (params.nodes.length > 0) {{ + showInfo(params.nodes[0]); + }} else if (hoveredNodeId === null) {{ + document.getElementById('info-content').innerHTML = 'Click a node to inspect it'; + }} }}); const searchInput = document.getElementById('search'); diff --git a/graphify/skill-windows.md b/graphify/skill-windows.md index e635c020..e0955c61 100644 --- a/graphify/skill-windows.md +++ b/graphify/skill-windows.md @@ -637,7 +637,7 @@ cost_path.write_text(json.dumps(cost, indent=2)) print(f'This run: {input_tok:,} input tokens, {output_tok:,} output tokens') print(f'All time: {cost[\"total_input_tokens\"]:,} input, {cost[\"total_output_tokens\"]:,} output ({len(cost[\"runs\"])} runs)') " -Remove-Item -ErrorAction SilentlyContinue .graphify_detect.json, .graphify_extract.json, .graphify_ast.json, .graphify_semantic.json, .graphify_analysis.json, .graphify_labels.json, .graphify_python +Remove-Item -ErrorAction SilentlyContinue .graphify_detect.json, .graphify_extract.json, .graphify_ast.json, .graphify_semantic.json, .graphify_analysis.json, .graphify_labels.json Remove-Item -ErrorAction SilentlyContinue graphify-out/.needs_update ``` diff --git a/graphify/skill.md b/graphify/skill.md index ec09523d..e4049e4b 100644 --- a/graphify/skill.md +++ b/graphify/skill.md @@ -71,9 +71,9 @@ else PYTHON="python3" fi "$PYTHON" -c "import graphify" 2>/dev/null || pip install graphifyy -q --break-system-packages 2>&1 | tail -3 -# Write interpreter path for all subsequent steps -"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w').write(sys.executable)" +# Write interpreter path for all subsequent steps (persists across invocations) mkdir -p graphify-out +"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w').write(sys.executable)" ``` If the import succeeds, print nothing and move straight to Step 2. @@ -683,6 +683,24 @@ The graph is the map. Your job after the pipeline is to be the guide. --- +## Interpreter guard for subcommands + +Before running any subcommand below (`--update`, `--cluster-only`, `query`, `path`, `explain`, `add`), check that `.graphify_python` exists. If it's missing (e.g. user deleted `graphify-out/`), re-resolve the interpreter first: + +```bash +if [ ! -f graphify-out/.graphify_python ]; then + GRAPHIFY_BIN=$(which graphify 2>/dev/null) + if [ -n "$GRAPHIFY_BIN" ]; then + PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!') + case "$PYTHON" in *[!a-zA-Z0-9/_.-]*) PYTHON="python3" ;; esac + else + PYTHON="python3" + fi + mkdir -p graphify-out + "$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w').write(sys.executable)" +fi +``` + ## For --update (incremental re-extraction) Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time. diff --git a/graphify/watch.py b/graphify/watch.py index 0d92bd6c..a0262527 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -5,17 +5,10 @@ import time from pathlib import Path -_WATCHED_EXTENSIONS = { - ".py", ".ts", ".js", ".go", ".rs", ".java", ".cpp", ".c", ".rb", ".swift", ".kt", - ".cs", ".scala", ".php", ".cc", ".cxx", ".hpp", ".h", ".kts", - ".md", ".txt", ".rst", ".pdf", - ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", -} +from graphify.detect import CODE_EXTENSIONS, DOC_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS -_CODE_EXTENSIONS = { - ".py", ".ts", ".js", ".go", ".rs", ".java", ".cpp", ".c", ".rb", ".swift", ".kt", - ".cs", ".scala", ".php", ".cc", ".cxx", ".hpp", ".h", ".kts", -} +_WATCHED_EXTENSIONS = CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS +_CODE_EXTENSIONS = CODE_EXTENSIONS def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool: diff --git a/pyproject.toml b/pyproject.toml index 3ba33a5f..9a1dac6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.3.17" +version = "0.3.18" description = "AI coding assistant skill (Claude Code, Codex, OpenCode, OpenClaw) - turn any folder of code, docs, papers, or images into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" } @@ -57,4 +57,4 @@ where = ["."] include = ["graphify*"] [tool.setuptools.package-data] -graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md", "skill-windows.md", "skill-droid.md"] +graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md"] diff --git a/tests/test_analyze.py b/tests/test_analyze.py index 0ae2ede5..2d496139 100644 --- a/tests/test_analyze.py +++ b/tests/test_analyze.py @@ -137,6 +137,15 @@ def test_file_category(): assert _file_category("flash.pdf") == "paper" assert _file_category("diagram.png") == "image" assert _file_category("notes.md") == "doc" + # Languages added in later releases — would misclassify as "doc" without detect.py import + assert _file_category("app.swift") == "code" + assert _file_category("plugin.lua") == "code" + assert _file_category("build.zig") == "code" + assert _file_category("deploy.ps1") == "code" + assert _file_category("server.ex") == "code" + assert _file_category("component.jsx") == "code" + assert _file_category("analysis.jl") == "code" + assert _file_category("view.m") == "code" def test_is_concept_node_empty_source(): diff --git a/tests/test_install.py b/tests/test_install.py index ac8f24b0..7715b4db 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -9,6 +9,10 @@ PLATFORMS = { "codex": (".agents/skills/graphify/SKILL.md",), "opencode": (".config/opencode/skills/graphify/SKILL.md",), "claw": (".claw/skills/graphify/SKILL.md",), + "droid": (".factory/skills/graphify/SKILL.md",), + "trae": (".trae/skills/graphify/SKILL.md",), + "trae-cn": (".trae-cn/skills/graphify/SKILL.md",), + "windows": (".claude/skills/graphify/SKILL.md",), } @@ -38,6 +42,26 @@ def test_install_claw(tmp_path): assert (tmp_path / ".claw" / "skills" / "graphify" / "SKILL.md").exists() +def test_install_droid(tmp_path): + _install(tmp_path, "droid") + assert (tmp_path / ".factory" / "skills" / "graphify" / "SKILL.md").exists() + + +def test_install_trae(tmp_path): + _install(tmp_path, "trae") + assert (tmp_path / ".trae" / "skills" / "graphify" / "SKILL.md").exists() + + +def test_install_trae_cn(tmp_path): + _install(tmp_path, "trae-cn") + assert (tmp_path / ".trae-cn" / "skills" / "graphify" / "SKILL.md").exists() + + +def test_install_windows(tmp_path): + _install(tmp_path, "windows") + assert (tmp_path / ".claude" / "skills" / "graphify" / "SKILL.md").exists() + + def test_install_unknown_platform_exits(tmp_path): with pytest.raises(SystemExit): _install(tmp_path, "unknown") @@ -67,10 +91,10 @@ def test_claw_skill_is_sequential(): def test_all_skill_files_exist_in_package(): - """All four platform skill files must be present in the installed package.""" + """All installable platform skill files must be present in the installed package.""" import graphify pkg = Path(graphify.__file__).parent - for name in ("skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md"): + for name in ("skill.md", "skill-codex.md", "skill-opencode.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md"): assert (pkg / name).exists(), f"Missing: {name}"