Fix watch edge key, claw path, Blade support, WSL MCP docs (0.4.7)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-04-12 20:49:24 +01:00
co-authored by Claude Sonnet 4.6
parent f8c91a987f
commit 4210de270f
8 changed files with 93 additions and 7 deletions
+7
View File
@@ -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`, `<livewire:>` 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)
+26 -1
View File
@@ -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.
<details>
<summary>Manual install (curl)</summary>
@@ -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
+1 -1
View File
@@ -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": {
+3
View File
@@ -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
+52 -1
View File
@@ -1165,6 +1165,53 @@ def extract_php(path: Path) -> dict:
return _extract_generic(path, _PHP_CONFIG)
def extract_blade(path: Path) -> dict:
"""Extract @include, <livewire:> 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})
# <livewire:component.name /> or <livewire:component.name>
for m in re.finditer(r"<livewire:([\w.\-]+)", src):
tgt_nid = _make_id(m.group(1))
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": "uses_component",
"confidence": "EXTRACTED", "confidence_score": 1.0,
"source_file": str(path), "source_location": None, "weight": 1.0})
# wire:click="methodName"
for m in re.finditer(r'wire:click=["\']([^"\']+)["\']', src):
tgt_nid = _make_id(m.group(1))
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": "binds_method",
"confidence": "EXTRACTED", "confidence_score": 1.0,
"source_file": str(path), "source_location": None, "weight": 1.0})
return {"nodes": nodes, "edges": edges}
def extract_lua(path: Path) -> 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)
+1 -1
View File
@@ -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 = {
+1 -1
View File
@@ -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" }
+2 -2
View File
@@ -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):