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:
Safi
2026-04-15 23:08:43 +01:00
co-authored by Claude Sonnet 4.6
parent 7ec92ecc6d
commit 429e46a665
15 changed files with 376 additions and 21 deletions
+9
View File
@@ -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)
+6 -2
View File
@@ -8,7 +8,7 @@
[![Sponsor](https://img.shields.io/badge/sponsor-safishamsi-ea4aaa?logo=github-sponsors)](https://github.com/sponsors/safishamsi)
[![LinkedIn](https://img.shields.io/badge/LinkedIn-Safi%20Shamsi-0077B5?logo=linkedin)](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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+253
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"]
+2 -2
View File
@@ -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():
+1 -1
View 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}, ".")
+1 -1
View File
@@ -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
View File
@@ -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