mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-26 16:26:42 +00:00
v0.4.15: VS Code Copilot Chat, OpenCode/Gemini Windows fixes, .mjs/.ejs, macOS watch, god_nodes degree rename
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
7ec92ecc6d
commit
429e46a665
@@ -2,6 +2,15 @@
|
||||
|
||||
Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases)
|
||||
|
||||
## 0.4.15 (2026-04-15)
|
||||
|
||||
- Feat: VS Code Copilot Chat support — `graphify vscode install` installs a Python-only skill (works on Windows PowerShell) and writes `.github/copilot-instructions.md` for always-on graph context (#206)
|
||||
- Fix: OpenCode plugin path used backslashes on Windows causing duplicate entries in `opencode.json` — now uses forward slashes via `.as_posix()` (#378)
|
||||
- Fix: Gemini CLI on Windows now installs skill to `~/.agents/skills/` (higher priority) instead of `~/.gemini/skills/` (#368)
|
||||
- Fix: `.mjs` and `.ejs` files now recognised by the AST extractor as JavaScript (#365, #372)
|
||||
- Fix: `god_nodes()` field renamed from `edges` to `degree` for clarity — updated in report, wiki, serve, and all tests (#375)
|
||||
- Fix: macOS `graphify watch` now uses `PollingObserver` by default to avoid missed events with FSEvents (#373)
|
||||
|
||||
## 0.4.14 (2026-04-15)
|
||||
|
||||
- Fix: cross-file call edges now emitted for all languages (Swift, Go, Rust, Java, C#, Kotlin, Scala, Ruby, PHP, and others) — previously only Python had cross-file resolution; unresolved call sites are now saved per file and resolved against a global label map in a post-pass (#348)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[](https://github.com/sponsors/safishamsi)
|
||||
[](https://www.linkedin.com/in/safi-shamsi)
|
||||
|
||||
**An AI coding assistant skill.** Type `/graphify` in Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, or Google Antigravity - it reads your files, builds a knowledge graph, and gives you back structure you didn't know was there. Understand a codebase faster. Find the "why" behind architectural decisions.
|
||||
**An AI coding assistant skill.** Type `/graphify` in Claude Code, Codex, OpenCode, Cursor, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, or Google Antigravity - it reads your files, builds a knowledge graph, and gives you back structure you didn't know was there. Understand a codebase faster. Find the "why" behind architectural decisions.
|
||||
|
||||
Fully multimodal. Drop in code, PDFs, markdown, screenshots, diagrams, whiteboard photos, images in other languages, or video and audio files - graphify extracts concepts and relationships from all of it and connects them into one graph. Videos are transcribed with Whisper using a domain-aware prompt derived from your corpus. 25 languages supported via tree-sitter AST (Python, JS, TS, Go, Rust, Java, C, C++, Ruby, C#, Kotlin, Scala, PHP, Swift, Lua, Zig, PowerShell, Elixir, Objective-C, Julia, Verilog, SystemVerilog, Vue, Svelte, Dart).
|
||||
|
||||
@@ -48,7 +48,7 @@ Every relationship is tagged `EXTRACTED` (found directly in source), `INFERRED`
|
||||
|
||||
## Install
|
||||
|
||||
**Requires:** Python 3.10+ and one of: [Claude Code](https://claude.ai/code), [Codex](https://openai.com/codex), [OpenCode](https://opencode.ai), [Cursor](https://cursor.com), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli), [Aider](https://aider.chat), [OpenClaw](https://openclaw.ai), [Factory Droid](https://factory.ai), [Trae](https://trae.ai), [Kiro](https://kiro.dev), Hermes, or [Google Antigravity](https://antigravity.google)
|
||||
**Requires:** Python 3.10+ and one of: [Claude Code](https://claude.ai/code), [Codex](https://openai.com/codex), [OpenCode](https://opencode.ai), [Cursor](https://cursor.com), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/copilot-cli), [VS Code Copilot Chat](https://code.visualstudio.com/docs/copilot/overview), [Aider](https://aider.chat), [OpenClaw](https://openclaw.ai), [Factory Droid](https://factory.ai), [Trae](https://trae.ai), [Kiro](https://kiro.dev), Hermes, or [Google Antigravity](https://antigravity.google)
|
||||
|
||||
```bash
|
||||
pip install graphifyy && graphify install
|
||||
@@ -65,6 +65,7 @@ pip install graphifyy && graphify install
|
||||
| Codex | `graphify install --platform codex` |
|
||||
| OpenCode | `graphify install --platform opencode` |
|
||||
| GitHub Copilot CLI | `graphify install --platform copilot` |
|
||||
| VS Code Copilot Chat | `graphify vscode install` |
|
||||
| Aider | `graphify install --platform aider` |
|
||||
| OpenClaw | `graphify install --platform claw` |
|
||||
| Factory Droid | `graphify install --platform droid` |
|
||||
@@ -96,6 +97,7 @@ After building a graph, run this once in your project:
|
||||
| Codex | `graphify codex install` |
|
||||
| OpenCode | `graphify opencode install` |
|
||||
| GitHub Copilot CLI | `graphify copilot install` |
|
||||
| VS Code Copilot Chat | `graphify vscode install` |
|
||||
| Aider | `graphify aider install` |
|
||||
| OpenClaw | `graphify claw install` |
|
||||
| Factory Droid | `graphify droid install` |
|
||||
@@ -125,6 +127,8 @@ After building a graph, run this once in your project:
|
||||
|
||||
**GitHub Copilot CLI** copies the skill to `~/.copilot/skills/graphify/SKILL.md`. Run `graphify copilot install` to set it up.
|
||||
|
||||
**VS Code Copilot Chat** installs a Python-only skill (works on Windows PowerShell and macOS/Linux alike) and writes `.github/copilot-instructions.md` in your project root — VS Code reads this automatically every session, making graph context always-on without any hook mechanism. Run `graphify vscode install`. Note: this configures the chat panel in VS Code, not the Copilot CLI terminal tool.
|
||||
|
||||
Uninstall with the matching uninstall command (e.g. `graphify claude uninstall`).
|
||||
|
||||
**Always-on vs explicit trigger — what's the difference?**
|
||||
|
||||
+92
-5
@@ -225,8 +225,12 @@ _GEMINI_HOOK = {
|
||||
def gemini_install(project_dir: Path | None = None) -> None:
|
||||
"""Copy skill file to ~/.gemini/skills/graphify/, write GEMINI.md section, and install BeforeTool hook."""
|
||||
# Copy skill file to ~/.gemini/skills/graphify/SKILL.md
|
||||
# On Windows, Gemini CLI prioritises ~/.agents/skills/ over ~/.gemini/skills/
|
||||
skill_src = Path(__file__).parent / "skill.md"
|
||||
skill_dst = Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
|
||||
if platform.system() == "Windows":
|
||||
skill_dst = Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"
|
||||
else:
|
||||
skill_dst = Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
|
||||
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")
|
||||
@@ -284,8 +288,11 @@ def _uninstall_gemini_hook(project_dir: Path) -> None:
|
||||
|
||||
def gemini_uninstall(project_dir: Path | None = None) -> None:
|
||||
"""Remove the graphify section from GEMINI.md, uninstall hook, and remove skill file."""
|
||||
# Remove skill file
|
||||
skill_dst = Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
|
||||
# Remove skill file (mirror the install path detection)
|
||||
if platform.system() == "Windows":
|
||||
skill_dst = Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"
|
||||
else:
|
||||
skill_dst = Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
|
||||
if skill_dst.exists():
|
||||
skill_dst.unlink()
|
||||
print(f" skill removed -> {skill_dst}")
|
||||
@@ -316,6 +323,75 @@ def gemini_uninstall(project_dir: Path | None = None) -> None:
|
||||
_uninstall_gemini_hook(project_dir or Path("."))
|
||||
|
||||
|
||||
_VSCODE_INSTRUCTIONS_MARKER = "## graphify"
|
||||
_VSCODE_INSTRUCTIONS_SECTION = """\
|
||||
## graphify
|
||||
|
||||
Before answering architecture or codebase questions, read `graphify-out/GRAPH_REPORT.md` if it exists.
|
||||
If `graphify-out/wiki/index.md` exists, navigate it for deep questions.
|
||||
Type `/graphify` in Copilot Chat to build or update the knowledge graph.
|
||||
"""
|
||||
|
||||
|
||||
def vscode_install(project_dir: Path | None = None) -> None:
|
||||
"""Install graphify skill for VS Code Copilot Chat + write .github/copilot-instructions.md."""
|
||||
skill_src = Path(__file__).parent / "skill-vscode.md"
|
||||
if not skill_src.exists():
|
||||
skill_src = Path(__file__).parent / "skill-copilot.md"
|
||||
skill_dst = Path.home() / ".copilot" / "skills" / "graphify" / "SKILL.md"
|
||||
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")
|
||||
print(f" skill installed -> {skill_dst}")
|
||||
|
||||
instructions = (project_dir or Path(".")) / ".github" / "copilot-instructions.md"
|
||||
instructions.parent.mkdir(parents=True, exist_ok=True)
|
||||
if instructions.exists():
|
||||
content = instructions.read_text(encoding="utf-8")
|
||||
if _VSCODE_INSTRUCTIONS_MARKER in content:
|
||||
print(f" {instructions} -> already configured (no change)")
|
||||
else:
|
||||
instructions.write_text(content.rstrip() + "\n\n" + _VSCODE_INSTRUCTIONS_SECTION, encoding="utf-8")
|
||||
print(f" {instructions} -> graphify section added")
|
||||
else:
|
||||
instructions.write_text(_VSCODE_INSTRUCTIONS_SECTION, encoding="utf-8")
|
||||
print(f" {instructions} -> created")
|
||||
|
||||
print()
|
||||
print("VS Code Copilot Chat configured. Type /graphify in the chat panel to build the graph.")
|
||||
print("Note: for GitHub Copilot CLI (terminal), use: graphify copilot install")
|
||||
|
||||
|
||||
def vscode_uninstall(project_dir: Path | None = None) -> None:
|
||||
"""Remove graphify VS Code Copilot Chat skill and .github/copilot-instructions.md section."""
|
||||
skill_dst = Path.home() / ".copilot" / "skills" / "graphify" / "SKILL.md"
|
||||
if skill_dst.exists():
|
||||
skill_dst.unlink()
|
||||
print(f" skill removed -> {skill_dst}")
|
||||
version_file = skill_dst.parent / ".graphify_version"
|
||||
if version_file.exists():
|
||||
version_file.unlink()
|
||||
for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent):
|
||||
try:
|
||||
d.rmdir()
|
||||
except OSError:
|
||||
break
|
||||
|
||||
instructions = (project_dir or Path(".")) / ".github" / "copilot-instructions.md"
|
||||
if not instructions.exists():
|
||||
return
|
||||
content = instructions.read_text(encoding="utf-8")
|
||||
if _VSCODE_INSTRUCTIONS_MARKER not in content:
|
||||
return
|
||||
cleaned = re.sub(r"\n*## graphify\n.*?(?=\n## |\Z)", "", content, flags=re.DOTALL).rstrip()
|
||||
if cleaned:
|
||||
instructions.write_text(cleaned + "\n", encoding="utf-8")
|
||||
print(f" graphify section removed from {instructions}")
|
||||
else:
|
||||
instructions.unlink()
|
||||
print(f" {instructions} -> deleted (was empty after removal)")
|
||||
|
||||
|
||||
_ANTIGRAVITY_RULES_PATH = Path(".agent") / "rules" / "graphify.md"
|
||||
_ANTIGRAVITY_WORKFLOW_PATH = Path(".agent") / "workflows" / "graphify.md"
|
||||
|
||||
@@ -566,7 +642,7 @@ def _install_opencode_plugin(project_dir: Path) -> None:
|
||||
config = {}
|
||||
|
||||
plugins = config.setdefault("plugin", [])
|
||||
entry = str(_OPENCODE_PLUGIN_PATH)
|
||||
entry = _OPENCODE_PLUGIN_PATH.as_posix()
|
||||
if entry not in plugins:
|
||||
plugins.append(entry)
|
||||
config_file.write_text(json.dumps(config, indent=2), encoding="utf-8")
|
||||
@@ -590,7 +666,7 @@ def _uninstall_opencode_plugin(project_dir: Path) -> None:
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
plugins = config.get("plugin", [])
|
||||
entry = str(_OPENCODE_PLUGIN_PATH)
|
||||
entry = _OPENCODE_PLUGIN_PATH.as_posix()
|
||||
if entry in plugins:
|
||||
plugins.remove(entry)
|
||||
if not plugins:
|
||||
@@ -861,6 +937,8 @@ def main() -> None:
|
||||
print(" aider uninstall remove graphify section from AGENTS.md")
|
||||
print(" copilot install copy graphify skill to ~/.copilot/skills (GitHub Copilot CLI)")
|
||||
print(" copilot uninstall remove graphify skill from ~/.copilot/skills")
|
||||
print(" vscode install configure VS Code Copilot Chat (skill + .github/copilot-instructions.md)")
|
||||
print(" vscode uninstall remove VS Code Copilot Chat configuration")
|
||||
print(" claw install write graphify section to AGENTS.md (OpenClaw)")
|
||||
print(" claw uninstall remove graphify section from AGENTS.md")
|
||||
print(" droid install write graphify section to AGENTS.md (Factory Droid)")
|
||||
@@ -922,6 +1000,15 @@ def main() -> None:
|
||||
else:
|
||||
print("Usage: graphify cursor [install|uninstall]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd == "vscode":
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
vscode_install()
|
||||
elif subcmd == "uninstall":
|
||||
vscode_uninstall()
|
||||
else:
|
||||
print("Usage: graphify vscode [install|uninstall]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd == "copilot":
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ def god_nodes(G: nx.Graph, top_n: int = 10) -> list[dict]:
|
||||
result.append({
|
||||
"id": node_id,
|
||||
"label": G.nodes[node_id].get("label", node_id),
|
||||
"edges": deg,
|
||||
"degree": deg,
|
||||
})
|
||||
if len(result) >= top_n:
|
||||
break
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ class FileType(str, Enum):
|
||||
|
||||
_MANIFEST_PATH = "graphify-out/manifest.json"
|
||||
|
||||
CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart', '.v', '.sv'}
|
||||
CODE_EXTENSIONS = {'.py', '.ts', '.js', '.jsx', '.tsx', '.mjs', '.ejs', '.go', '.rs', '.java', '.cpp', '.cc', '.cxx', '.c', '.h', '.hpp', '.rb', '.swift', '.kt', '.kts', '.cs', '.scala', '.php', '.lua', '.toc', '.zig', '.ps1', '.ex', '.exs', '.m', '.mm', '.jl', '.vue', '.svelte', '.dart', '.v', '.sv'}
|
||||
DOC_EXTENSIONS = {'.md', '.txt', '.rst'}
|
||||
PAPER_EXTENSIONS = {'.pdf'}
|
||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'}
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ def generate(
|
||||
"## God Nodes (most connected - your core abstractions)",
|
||||
]
|
||||
for i, node in enumerate(god_node_list, 1):
|
||||
lines.append(f"{i}. `{node['label']}` - {node['edges']} edges")
|
||||
lines.append(f"{i}. `{node['label']}` - {node['degree']} edges")
|
||||
|
||||
lines += ["", "## Surprising Connections (you probably didn't know these)"]
|
||||
if surprise_list:
|
||||
|
||||
+1
-1
@@ -295,7 +295,7 @@ def serve(graph_path: str = "graphify-out/graph.json") -> None:
|
||||
from .analyze import god_nodes as _god_nodes
|
||||
nodes = _god_nodes(G, top_n=int(arguments.get("top_n", 10)))
|
||||
lines = ["God nodes (most connected):"]
|
||||
lines += [f" {i}. {n['label']} - {n['edges']} edges" for i, n in enumerate(nodes, 1)]
|
||||
lines += [f" {i}. {n['label']} - {n['degree']} edges" for i, n in enumerate(nodes, 1)]
|
||||
return "\n".join(lines)
|
||||
|
||||
def _tool_graph_stats(_: dict) -> str:
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
---
|
||||
name: graphify
|
||||
description: any input (code, docs, papers, images) → knowledge graph → clustered communities → HTML + JSON + audit report
|
||||
trigger: /graphify
|
||||
---
|
||||
|
||||
# /graphify
|
||||
|
||||
Turn any folder of files into a navigable knowledge graph with community detection, an honest audit trail, and three outputs: interactive HTML, GraphRAG-ready JSON, and a plain-language GRAPH_REPORT.md.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/graphify # full pipeline on current directory
|
||||
/graphify <path> # full pipeline on specific path
|
||||
/graphify <path> --update # incremental - re-extract only new/changed files
|
||||
/graphify <path> --no-viz # skip visualization, just report + JSON
|
||||
/graphify <path> --wiki # build agent-crawlable wiki
|
||||
/graphify query "<question>" # BFS traversal - broad context
|
||||
```
|
||||
|
||||
## What You Must Do When Invoked
|
||||
|
||||
If no path was given, use `.` (current directory). Do not ask the user for a path.
|
||||
|
||||
Follow these steps in order. Do not skip steps.
|
||||
|
||||
**All commands use `python -c "..."` syntax — no bash heredocs, no shell redirects, no `&&`/`||`. This runs correctly on Windows PowerShell and macOS/Linux alike.**
|
||||
|
||||
### Step 1 - Ensure graphify is installed
|
||||
|
||||
```python
|
||||
python -c "import graphify; import sys; from pathlib import Path; Path('graphify-out').mkdir(exist_ok=True); Path('graphify-out/.graphify_python').write_text(sys.executable)"
|
||||
```
|
||||
|
||||
If the import fails, install first:
|
||||
|
||||
```python
|
||||
python -m pip install graphifyy -q
|
||||
```
|
||||
|
||||
Then re-run the Step 1 command.
|
||||
|
||||
### Step 2 - Detect files
|
||||
|
||||
```python
|
||||
python -c "
|
||||
import json, sys
|
||||
from graphify.detect import detect
|
||||
from pathlib import Path
|
||||
|
||||
result = detect(Path('INPUT_PATH'))
|
||||
Path('graphify-out/.graphify_detect.json').write_text(json.dumps(result, indent=2))
|
||||
total = result.get('total_files', 0)
|
||||
words = result.get('total_words', 0)
|
||||
print(f'Corpus: {total} files, ~{words} words')
|
||||
for ftype, files in result.get('files', {}).items():
|
||||
if files:
|
||||
print(f' {ftype}: {len(files)} files')
|
||||
"
|
||||
```
|
||||
|
||||
Replace `INPUT_PATH` with the actual path. Present a clean summary — do not dump the raw JSON.
|
||||
|
||||
- If `total_files` is 0: stop with "No supported files found in [path]."
|
||||
- If `total_words` > 2,000,000 OR `total_files` > 200: warn the user and ask which subfolder to run on.
|
||||
- Otherwise: proceed to Step 3.
|
||||
|
||||
### Step 3 - Extract entities and relationships
|
||||
|
||||
#### Part A - Structural extraction (AST, free, no API cost)
|
||||
|
||||
```python
|
||||
python -c "
|
||||
import json
|
||||
from graphify.extract import collect_files, extract
|
||||
from pathlib import Path
|
||||
|
||||
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text())
|
||||
code_files = []
|
||||
for f in detect.get('files', {}).get('code', []):
|
||||
p = Path(f)
|
||||
code_files.extend(collect_files(p) if p.is_dir() else [p])
|
||||
|
||||
if code_files:
|
||||
result = extract(code_files)
|
||||
Path('graphify-out/.graphify_ast.json').write_text(json.dumps(result, indent=2))
|
||||
print(f'AST: {len(result[\"nodes\"])} nodes, {len(result[\"edges\"])} edges')
|
||||
else:
|
||||
Path('graphify-out/.graphify_ast.json').write_text(json.dumps({'nodes':[],'edges':[],'input_tokens':0,'output_tokens':0}))
|
||||
print('No code files - skipping AST extraction')
|
||||
"
|
||||
```
|
||||
|
||||
#### Part B - Semantic extraction (AI, costs tokens)
|
||||
|
||||
Skip if corpus is code-only (no docs, papers, or images).
|
||||
|
||||
Check cache first:
|
||||
|
||||
```python
|
||||
python -c "
|
||||
import json
|
||||
from graphify.cache import check_semantic_cache
|
||||
from pathlib import Path
|
||||
|
||||
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text())
|
||||
all_files = [f for files in detect['files'].values() for f in files]
|
||||
cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(all_files)
|
||||
|
||||
if cached_nodes or cached_edges:
|
||||
Path('graphify-out/.graphify_cached.json').write_text(json.dumps({'nodes': cached_nodes, 'edges': cached_edges, 'hyperedges': cached_hyperedges}))
|
||||
Path('graphify-out/.graphify_uncached.txt').write_text('\n'.join(uncached))
|
||||
print(f'Cache: {len(all_files)-len(uncached)} hit, {len(uncached)} need extraction')
|
||||
"
|
||||
```
|
||||
|
||||
For each chunk of uncached files (20-25 files per chunk), dispatch a subagent with this prompt:
|
||||
|
||||
```
|
||||
You are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment.
|
||||
Output ONLY valid JSON: {"nodes": [...], "edges": [...], "hyperedges": [...]}
|
||||
|
||||
Each node: {"id": "unique_id", "label": "Human Name", "file_type": "code|document|paper|image"}
|
||||
Each edge: {"source": "id", "target": "id", "relation": "verb_phrase", "confidence": "EXTRACTED|INFERRED|AMBIGUOUS"}
|
||||
hyperedges: [] unless you find a genuine group relationship
|
||||
|
||||
Files:
|
||||
FILE_LIST
|
||||
```
|
||||
|
||||
Collect all subagent responses and merge them:
|
||||
|
||||
```python
|
||||
python -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Merge: combine AST + cached + all semantic chunk results
|
||||
all_nodes, all_edges, all_hyperedges = [], [], []
|
||||
|
||||
ast = json.loads(Path('graphify-out/.graphify_ast.json').read_text())
|
||||
all_nodes.extend(ast.get('nodes', []))
|
||||
all_edges.extend(ast.get('edges', []))
|
||||
|
||||
cached_path = Path('graphify-out/.graphify_cached.json')
|
||||
if cached_path.exists():
|
||||
cached = json.loads(cached_path.read_text())
|
||||
all_nodes.extend(cached.get('nodes', []))
|
||||
all_edges.extend(cached.get('edges', []))
|
||||
all_hyperedges.extend(cached.get('hyperedges', []))
|
||||
|
||||
# PASTE each subagent response here as chunk_1, chunk_2, etc.
|
||||
for chunk_json in []: # replace [] with your chunk results
|
||||
chunk = json.loads(chunk_json) if isinstance(chunk_json, str) else chunk_json
|
||||
all_nodes.extend(chunk.get('nodes', []))
|
||||
all_edges.extend(chunk.get('edges', []))
|
||||
all_hyperedges.extend(chunk.get('hyperedges', []))
|
||||
|
||||
merged = {'nodes': all_nodes, 'edges': all_edges, 'hyperedges': all_hyperedges, 'input_tokens': 0, 'output_tokens': 0}
|
||||
Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged, indent=2))
|
||||
print(f'Merged: {len(all_nodes)} nodes, {len(all_edges)} edges')
|
||||
"
|
||||
```
|
||||
|
||||
### Step 4 - Build graph and cluster
|
||||
|
||||
```python
|
||||
python -c "
|
||||
import json
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import cluster
|
||||
from graphify.analyze import god_nodes, surprising_connections
|
||||
from pathlib import Path
|
||||
|
||||
extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text())
|
||||
G = build_from_json(extraction)
|
||||
communities = cluster(G)
|
||||
gods = god_nodes(G)
|
||||
surprises = surprising_connections(G, communities)
|
||||
|
||||
import networkx as nx
|
||||
from networkx.readwrite import json_graph
|
||||
graph_data = json_graph.node_link_data(G)
|
||||
Path('graphify-out/graph.json').write_text(json.dumps(graph_data, indent=2))
|
||||
Path('graphify-out/.graphify_analysis.json').write_text(json.dumps({
|
||||
'communities': {str(k): v for k, v in communities.items()},
|
||||
'cohesion': {},
|
||||
'god_nodes': gods,
|
||||
'surprises': surprises,
|
||||
}, indent=2))
|
||||
print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges, {len(communities)} communities')
|
||||
print(f'God nodes: {[g[\"label\"] for g in gods[:5]]}')
|
||||
"
|
||||
```
|
||||
|
||||
### Step 5 - Generate report and visualization
|
||||
|
||||
```python
|
||||
python -c "
|
||||
import json
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import cluster
|
||||
from graphify.analyze import god_nodes, surprising_connections
|
||||
from graphify.report import generate
|
||||
from pathlib import Path
|
||||
|
||||
extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text())
|
||||
analysis = json.loads(Path('graphify-out/.graphify_analysis.json').read_text())
|
||||
|
||||
G = build_from_json(extraction)
|
||||
communities = {int(k): v for k, v in analysis['communities'].items()}
|
||||
gods = god_nodes(G)
|
||||
surprises = surprising_connections(G, communities)
|
||||
|
||||
report = generate(G, communities, {}, {}, gods, surprises, extraction)
|
||||
Path('graphify-out/GRAPH_REPORT.md').write_text(report)
|
||||
print('GRAPH_REPORT.md written')
|
||||
"
|
||||
```
|
||||
|
||||
```python
|
||||
python -c "
|
||||
import json
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import cluster
|
||||
from graphify.export import to_html
|
||||
from pathlib import Path
|
||||
|
||||
extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text())
|
||||
G = build_from_json(extraction)
|
||||
communities = cluster(G)
|
||||
|
||||
try:
|
||||
to_html(G, communities, 'graphify-out/graph.html')
|
||||
print('graph.html written')
|
||||
except ValueError as e:
|
||||
print(f'Visualization skipped: {e}')
|
||||
"
|
||||
```
|
||||
|
||||
### After completing all steps
|
||||
|
||||
Print this summary:
|
||||
|
||||
```
|
||||
graphify complete
|
||||
graph.json — GraphRAG-ready, queryable by MCP or CLI
|
||||
graph.html — interactive visualization (open in browser)
|
||||
GRAPH_REPORT.md — plain-language architecture summary
|
||||
```
|
||||
|
||||
Read `graphify-out/GRAPH_REPORT.md` and share the **God Nodes** and **Surprising Connections** sections directly in the chat — do not ask the user to open the file themselves.
|
||||
+3
-1
@@ -120,6 +120,7 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
||||
"""
|
||||
try:
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.observers.polling import PollingObserver
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
except ImportError as e:
|
||||
raise ImportError("watchdog not installed. Run: pip install watchdog") from e
|
||||
@@ -145,7 +146,8 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None:
|
||||
changed.add(path)
|
||||
|
||||
handler = Handler()
|
||||
observer = Observer()
|
||||
# Use polling observer on macOS — FSEvents can miss rapid saves in some editors
|
||||
observer = PollingObserver() if sys.platform == "darwin" else Observer()
|
||||
observer.schedule(handler, str(watch_path), recursive=True)
|
||||
observer.start()
|
||||
|
||||
|
||||
+1
-1
@@ -154,7 +154,7 @@ def _index_md(
|
||||
if god_nodes_data:
|
||||
lines += ["## God Nodes", "(most connected concepts — the load-bearing abstractions)", ""]
|
||||
for node in god_nodes_data:
|
||||
lines.append(f"- [[{node['label']}]] — {node['edges']} connections")
|
||||
lines.append(f"- [[{node['label']}]] — {node['degree']} connections")
|
||||
lines.append("")
|
||||
|
||||
lines += [
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "graphifyy"
|
||||
version = "0.4.14"
|
||||
version = "0.4.15"
|
||||
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
@@ -60,4 +60,4 @@ where = ["."]
|
||||
include = ["graphify*"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-aider.md", "skill-copilot.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md", "skill-kiro.md"]
|
||||
graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-aider.md", "skill-copilot.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md", "skill-kiro.md", "skill-vscode.md"]
|
||||
|
||||
@@ -23,7 +23,7 @@ def test_god_nodes_returns_list():
|
||||
def test_god_nodes_sorted_by_degree():
|
||||
G = make_graph()
|
||||
result = god_nodes(G, top_n=10)
|
||||
degrees = [r["edges"] for r in result]
|
||||
degrees = [r["degree"] for r in result]
|
||||
assert degrees == sorted(degrees, reverse=True)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ def test_god_nodes_have_required_keys():
|
||||
result = god_nodes(G, top_n=1)
|
||||
assert "id" in result[0]
|
||||
assert "label" in result[0]
|
||||
assert "edges" in result[0]
|
||||
assert "degree" in result[0]
|
||||
|
||||
|
||||
def test_surprising_connections_cross_source_multi_file():
|
||||
|
||||
@@ -166,7 +166,7 @@ def _make_report(G):
|
||||
communities = {0: list(G.nodes())}
|
||||
cohesion = {0: 1.0}
|
||||
labels = {0: "All"}
|
||||
gods = [{"label": "BasicAuth", "edges": 2}]
|
||||
gods = [{"label": "BasicAuth", "degree": 2}]
|
||||
surprises = []
|
||||
return generate(G, communities, cohesion, labels, gods, surprises, SAMPLE_DETECTION, {"input": 10, "output": 5}, ".")
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ def run_pipeline(tmp_path: Path) -> dict:
|
||||
# Step 5: analyze
|
||||
gods = god_nodes(G)
|
||||
assert len(gods) > 0
|
||||
assert all("id" in g and "edges" in g for g in gods)
|
||||
assert all("id" in g and "degree" in g for g in gods)
|
||||
|
||||
surprises = surprising_connections(G, communities)
|
||||
assert isinstance(surprises, list)
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ def _make_graph():
|
||||
COMMUNITIES = {0: ["n1", "n2"], 1: ["n3", "n4"]}
|
||||
LABELS = {0: "Parsing Layer", 1: "Rendering Layer"}
|
||||
COHESION = {0: 0.85, 1: 0.72}
|
||||
GOD_NODES = [{"id": "n1", "label": "parse", "edges": 2}]
|
||||
GOD_NODES = [{"id": "n1", "label": "parse", "degree": 2}]
|
||||
|
||||
|
||||
def test_to_wiki_writes_index(tmp_path):
|
||||
@@ -105,7 +105,7 @@ def test_god_node_article_links_community(tmp_path):
|
||||
def test_to_wiki_skips_missing_god_node_ids(tmp_path):
|
||||
"""God node with bad ID should not crash."""
|
||||
G = _make_graph()
|
||||
bad_gods = [{"id": "nonexistent", "label": "ghost", "edges": 99}]
|
||||
bad_gods = [{"id": "nonexistent", "label": "ghost", "degree": 99}]
|
||||
n = to_wiki(G, COMMUNITIES, tmp_path, community_labels=LABELS, god_nodes_data=bad_gods)
|
||||
# 2 communities + 0 god nodes (nonexistent skipped) = 2
|
||||
assert n == 2
|
||||
|
||||
Reference in New Issue
Block a user