From 4210de270f4345d796b90dd33c2b3e9f95e6f157 Mon Sep 17 00:00:00 2001 From: Safi Date: Sun, 12 Apr 2026 20:49:24 +0100 Subject: [PATCH] Fix watch edge key, claw path, Blade support, WSL MCP docs (0.4.7) Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 7 ++++++ README.md | 27 +++++++++++++++++++++- graphify/__main__.py | 2 +- graphify/detect.py | 3 +++ graphify/extract.py | 53 ++++++++++++++++++++++++++++++++++++++++++- graphify/watch.py | 2 +- pyproject.toml | 2 +- tests/test_install.py | 4 ++-- 8 files changed, 93 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee53b3a2f..9b5fd6d1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases) +## 0.4.7 (2026-04-12) + +- Fix: `watch` semantic edge preservation was always empty — `graph.json` uses `links` key but code read `edges` (#269) +- Fix: `graphify claw install` now writes to `.openclaw/` (correct OpenClaw directory) instead of `.claw/` (#208) +- Add: Blade template support — `@include`, `` components, and `wire:click` bindings extracted from `.blade.php` files (#242) +- Docs: WSL/Linux MCP setup note — package name is `graphifyy`, use `.venv/bin/python3` in `.mcp.json` (#250) + ## 0.4.6 (2026-04-12) - Add: Google Antigravity support — `graphify antigravity install` writes `.agent/rules/graphify.md` (always-on rules) and `.agent/workflows/graphify.md` (`/graphify` slash command) (#203, #199, #53) diff --git a/README.md b/README.md index ea1d4356a..0711ad532 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,15 @@ python -m graphify.serve graphify-out/graph.json That gives the assistant structured graph access for repeated queries such as `query_graph`, `get_node`, `get_neighbors`, and `shortest_path`. +> **WSL / Linux note:** Ubuntu ships `python3`, not `python`. Install into a project venv to avoid PEP 668 conflicts, and use the full venv path in your `.mcp.json`: +> ```bash +> python3 -m venv .venv && .venv/bin/pip install "graphifyy[mcp]" +> ``` +> ```json +> { "mcpServers": { "graphify": { "type": "stdio", "command": ".venv/bin/python3", "args": ["-m", "graphify.serve", "graphify-out/graph.json"] } } } +> ``` +> Also note: the PyPI package is `graphifyy` (double-y) — `pip install graphify` installs an unrelated package. +
Manual install (curl) @@ -329,9 +338,25 @@ graphify sends file contents to your AI coding assistant's underlying model API NetworkX + Leiden (graspologic) + tree-sitter + vis.js. Semantic extraction via Claude (Claude Code), GPT-4 (Codex), or whichever model your platform runs. Video transcription via faster-whisper + yt-dlp (optional, `pip install graphifyy[video]`). No Neo4j required, no server, runs entirely locally. +## Built on graphify — Penpax + +[**Penpax**](https://safishamsi.github.io/penpax.ai) is the enterprise layer on top of graphify. Where graphify turns a folder of files into a knowledge graph, Penpax applies the same graph to your entire working life — continuously. + +| | graphify | Penpax | +|---|---|---| +| Input | A folder of files | Browser history, meetings, emails, files, code — everything | +| Runs | On demand | Continuously in the background | +| Scope | A project | Your entire working life | +| Query | CLI / MCP / AI skill | Natural language, always on | +| Privacy | Local by default | Fully on-device, no cloud | + +Built for lawyers, consultants, executives, doctors, researchers — anyone whose work lives across hundreds of conversations and documents they can never fully reconstruct. + +**Free trial launching soon.** [Join the waitlist →](https://safishamsi.github.io/penpax.ai) + ## What we are building next -graphify is the graph layer. We are building [Penpax](https://safishamsi.github.io/penpax.ai) on top of it — an on-device digital twin that connects your meetings, browser history, files, emails, and code into one continuously updating knowledge graph. No cloud, no training on your data. [Join the waitlist.](https://safishamsi.github.io/penpax.ai) +graphify is the graph layer. Penpax is the always-on layer on top of it — an on-device digital twin that connects your meetings, browser history, files, emails, and code into one continuously updating knowledge graph. No cloud, no training on your data. [Join the waitlist.](https://safishamsi.github.io/penpax.ai) ## Star history diff --git a/graphify/__main__.py b/graphify/__main__.py index 89efdc7c9..eaa13c59b 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -74,7 +74,7 @@ _PLATFORM_CONFIG: dict[str, dict] = { }, "claw": { "skill_file": "skill-claw.md", - "skill_dst": Path(".claw") / "skills" / "graphify" / "SKILL.md", + "skill_dst": Path(".openclaw") / "skills" / "graphify" / "SKILL.md", "claude_md": False, }, "droid": { diff --git a/graphify/detect.py b/graphify/detect.py index 53ab095a6..721c0d473 100644 --- a/graphify/detect.py +++ b/graphify/detect.py @@ -80,6 +80,9 @@ _ASSET_DIR_MARKERS = {".imageset", ".xcassets", ".appiconset", ".colorset", ".la def classify_file(path: Path) -> FileType | None: + # Compound extensions must be checked before simple suffix lookup + if path.name.lower().endswith(".blade.php"): + return FileType.CODE ext = path.suffix.lower() if ext in CODE_EXTENSIONS: return FileType.CODE diff --git a/graphify/extract.py b/graphify/extract.py index 065a664aa..24e1001ae 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -1165,6 +1165,53 @@ def extract_php(path: Path) -> dict: return _extract_generic(path, _PHP_CONFIG) +def extract_blade(path: Path) -> dict: + """Extract @include, components, and wire:click bindings from Blade templates.""" + import re + try: + src = path.read_text(encoding="utf-8", errors="replace") + except OSError: + return {"error": f"cannot read {path}"} + + file_nid = _make_id(str(path)) + nodes = [{"id": file_nid, "label": path.name, "file_type": "code", + "source_file": str(path), "source_location": None}] + edges = [] + + # @include('path.to.partial') or @include("path.to.partial") + for m in re.finditer(r"@include\(['\"]([^'\"]+)['\"]", src): + tgt = m.group(1).replace(".", "/") + tgt_nid = _make_id(tgt) + if tgt_nid not in {n["id"] for n in nodes}: + nodes.append({"id": tgt_nid, "label": m.group(1), "file_type": "code", + "source_file": str(path), "source_location": None}) + edges.append({"source": file_nid, "target": tgt_nid, "relation": "includes", + "confidence": "EXTRACTED", "confidence_score": 1.0, + "source_file": str(path), "source_location": None, "weight": 1.0}) + + # or + for m in re.finditer(r" dict: """Extract functions, methods, require() imports, and calls from a .lua file.""" return _extract_generic(path, _LUA_CONFIG) @@ -2655,7 +2702,11 @@ def extract(paths: list[Path]) -> dict: for i, path in enumerate(paths): if total >= _PROGRESS_INTERVAL and i % _PROGRESS_INTERVAL == 0 and i > 0: print(f" AST extraction: {i}/{total} files ({i * 100 // total}%)", flush=True) - extractor = _DISPATCH.get(path.suffix) + # .blade.php must be checked before suffix lookup since Path.suffix returns .php + if path.name.endswith(".blade.php"): + extractor = extract_blade + else: + extractor = _DISPATCH.get(path.suffix) if extractor is None: continue cached = load_cached(path, root) diff --git a/graphify/watch.py b/graphify/watch.py index 09f65d0d2..45d03a9b7 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -43,7 +43,7 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False) -> bool: existing = json.loads(existing_graph.read_text(encoding="utf-8")) code_ids = {n["id"] for n in existing.get("nodes", []) if n.get("file_type") == "code"} sem_nodes = [n for n in existing.get("nodes", []) if n.get("file_type") != "code"] - sem_edges = [e for e in existing.get("edges", []) + sem_edges = [e for e in existing.get("links", existing.get("edges", [])) if e.get("confidence") in ("INFERRED", "AMBIGUOUS") or (e.get("source") not in code_ids and e.get("target") not in code_ids)] result = { diff --git a/pyproject.toml b/pyproject.toml index 18fd76bbb..f3621ac89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "graphifyy" -version = "0.4.6" +version = "0.4.7" description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, OpenClaw, Factory Droid, Trae) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph" readme = "README.md" license = { file = "LICENSE" } diff --git a/tests/test_install.py b/tests/test_install.py index f8353ac4d..3264abb25 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -8,7 +8,7 @@ PLATFORMS = { "claude": (".claude/skills/graphify/SKILL.md",), "codex": (".agents/skills/graphify/SKILL.md",), "opencode": (".config/opencode/skills/graphify/SKILL.md",), - "claw": (".claw/skills/graphify/SKILL.md",), + "claw": (".openclaw/skills/graphify/SKILL.md",), "droid": (".factory/skills/graphify/SKILL.md",), "trae": (".trae/skills/graphify/SKILL.md",), "trae-cn": (".trae-cn/skills/graphify/SKILL.md",), @@ -39,7 +39,7 @@ def test_install_opencode(tmp_path): def test_install_claw(tmp_path): _install(tmp_path, "claw") - assert (tmp_path / ".claw" / "skills" / "graphify" / "SKILL.md").exists() + assert (tmp_path / ".openclaw" / "skills" / "graphify" / "SKILL.md").exists() def test_install_droid(tmp_path):