graphify
Skill6 starsChanged 4 months agoApache-2.0View on GitHub
any input (code, docs, papers, images) → knowledge graph → clustered communities → HTML + JSON + audit report. Use when the user wants to build, inspect, or query a cross-file knowledge graph from code, docs, papers, images, or videos with upstream graphify.
- Installs packages
115 other projects use a version of this, 97 of them edited.
Adopted by
Most recently taken up by
- thistleknot/skillsedited, first seen Nov 8, 2026
- ever-works/skillstheir own version, first seen Nov 8, 2026
- rylsherdamz-rgb/stellar-forgeedited, first seen Nov 8, 2026
- brebuilds/claude-skillsedited, first seen Nov 8, 2026
- GstMirabal/Token-Optimized-Agent-Pipelineedited, first seen Nov 8, 2026
- MMACABRE/SKILLSedited, first seen Nov 8, 2026
Used with
Projects with it also use remotion-maps, remotion-saas and 10 more.
Other files people use alongside this one.
Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
OpenAISkillremotion-maps
Remotion Map animation knowledge
Also in 4 projects that use this
OpenAISkillremotion-saas
Build an app with Remotion
Also in 4 projects that use this
royaluniondesign-sysSkilldeployment-patterns
Deployment workflows, CI/CD pipeline patterns, Docker containerization, health checks, rollback strategies, and production readiness checklists for web applications.
Also in 4 projects that use this
mstuder-slalomSkilldjango-verification
Verification loop for Django projects: migrations, linting, tests with coverage, security scans, and deployment readiness checks before release or PR.
Also in 3 projects that use this
ttmouseSkillprompt-master
提示词主控 - 智能选择合适的领域skill并生成提示词,支持自动领域分类和调度
Also in 4 projects that use this
seikaikyoSkilldoubt-driven-development
對非平凡決策啟動新鮮 context 對抗審查(找碴不背書),在修正還便宜的時候抓出錯誤方向。適用:高風險改動(production、資安敏感邏輯、不可逆操作)、不熟的程式碼、要宣稱「這樣是安全的 / 可行的」之前。機械性操作與一行修改不適用。
Also in 6 projects that use this
Other versions115 projects use a version of this.
Word for word · 0
None found.
Edited · 97
- safishamsi/graphify★ 144k29% same
- Graphify-Labs/graphify★ 144k29% same
- zaxbysauce/opencode-swarm★ 51926% same
- ZaxbyHub/opencode-swarm★ 51826% same
- nandyalu/trailarr★ 48645% same
- bdfinst/agentic-dev-team★ 30239% same
- cin12211/orca-q★ 22545% same
- sammcj/agentic-coding★ 16939% same
- EBISPOT/ols4★ 10639% same
- gavishap/omnia-vault★ 5339% same
87 more not listed
Wrote their own version · 18
- buildomator/buildomator★ 102same name
- IgorGanapolsky/ThumbGate★ 30same name
- uniskela/adhd-hub★ 8same name
- Envy-7z/katalyst★ 4same name
- hahuyhungdev/ai-coding-config★ 3same name
- Citadel-Cloud-Management/citadel-saas-factory★ 2same name
- sakuruamayday-ops/enterprise-project-application-assistant★ 2same name
- Thakurvaibhav/platform-agent-harness★ 2same name
- AuroraBackcountry/agent-config★ 1same name
- ever-works/skills★ 1same name
8 more not listed
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository116 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- skills/graphify/references/CLAUDE.mdCLAUDE.md
- agent-config-security-auditSkill
- agent-harness-designSkill
- agent-readable-docsSkill
- agent-runtime-patternsSkill
- agent-session-forensicsSkill
- ai-regression-testingSkill
- ai-slop-cleanerSkill
- api-doc-reviewSkill
- architecture-decision-recordsSkill
- architecture-simplificationSkill
- ask-claudeSkill
- ask-codexSkill
- ask-geminiSkill
- autonomous-delivery-pipelineSkill
- baseline-comparison-reviewSkill
- bigquery-cost-auditSkill
- brainstormingSkill
- browser-automation-safetySkill
- bug-huntSkill
- build-fix-minimal-diffSkill
- canonical-advisor-routingSkill
- change-impact-checkSkill
- changelog-generatorSkill
- ci-failure-readoutSkill
- codebase-migration-batchesSkill
- context-compressionSkill
- context-docsSkill
- context-minimizationSkill
- context-retrieval-loopSkill
- data-qualitySkill
- dataset-split-reviewSkill
- dbt-incremental-strategy-auditSkill
- dbt-preflightSkill
- dbt-strategySkill
- dead-code-removalSkill
- defuddleSkill
- dependency-cleanupSkill
- deploy-pipeline-runbookSkill
- dispatching-parallel-agentsSkill
- doc-consistency-checkSkill
- environment-doctorSkill
- experiment-tracking-reviewSkill
- failure-triageSkill
- filesystem-mcp-guardrailsSkill
- finishing-a-development-branchSkill
- flaky-test-investigationSkill
- gh-address-commentsSkill
- github-ci-failure-triageSkill
- handoff-disciplineSkill
- hard-won-skill-extractorSkill
- hook-safety-reviewSkill
- incident-readoutSkill
- incremental-migrationSkill
- json-canvasSkill
- local-rag-mcpSkill
- log-driven-diagnosisSkill
- managed-codebase-contextSkill
- The first 60 shown.
The file
Show its rules and text
We split this skill into the rules it gives an agent
61 rules. 7 are common, 30 are distinctive, and 24 are very specific to this project.
7
Common
Most of the 113 edited versions we read keep these.
30
Distinctive
This author's own choices. Worth a look.
24
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 18
Read UPSTREAM.md before changing compatibility, install, or license claims.
- Very specific: line 44
Do not assume graphifyy is installed.
- Distinctive: line 45
Do not auto-install without user approval.
- Very specific: line 47
If graphify exists on PATH but python -c "import graphify" fails, stop and tell the user the skill cannot use that interpreter yet.
- Distinctive: line 92
Honest audit trail: - every edge is tagged EXTRACTED, INFERRED, or AMBIGUOUS.
- Very specific: line 103
If no path was given, use .
- Common: line 105
Follow these steps in order.
- Common: line 115
If the import succeeds, print nothing and move straight to Step 2.
- Distinctive: line 119
Stop. Do not continue with detection or extraction.
- Distinctive: line 129
Ask before installing it.
- Very specific: line 157
If total_files is 0: stop with "No supported files found in [path]."
- Very specific: line 158
If skipped_sensitive is non-empty: mention file count skipped, not the file names.
- Very specific: line 159
If total_words > 2,000,000 OR total_files > 200: show the warning and the top 5 subdirectories by file count, then ask which subfolder to …
- Distinctive: line 168
Strategy: Read the god nodes from the detect output or analysis file.
- Distinctive: line 174
Read the top god node labels from detect output or analysis, then compose a short domain hint sentence, for example:
- Very specific: line 199
Read the transcript paths from graphify-out\.graphify_transcripts.json
- Distinctive: line 200
Add them to the docs list before dispatching semantic subagents in Step 3B
- Distinctive: line 202
If transcription fails for a file, print a warning and continue with the rest
- Very specific: line 204
Whisper model: Default is base. If the user passed --whisper-model <name>, set $env:GRAPHIFY_WHISPER_MODEL = "<name>" before running the …
- Distinctive: line 244
Fast path: If detection found zero docs, papers, and images (code-only corpus), skip Part B entirely and go straight to Part C.
- Common: line 248
Before dispatching subagents, print a timing estimate:
- Common: line 256
Before dispatching any subagents, check which files already have cached extraction results:
- Very specific: line 276
Only dispatch subagents for files listed in .graphify_uncached.txt.
- Common: line 284
Call the Agent tool multiple times IN THE SAME RESPONSE - one call per chunk.
- Common: line 292
All three in one message. Not three separate messages.
- Common: line 351
Wait for all subagents. For each result:
- Very specific: line 352
Check that graphify-out/.graphify_chunk_NN.json exists on disk — this is the success signal
- Very specific: line 353
If the file exists and contains valid JSON with nodes and edges, include it and save to cache
- Distinctive: line 354
If the file is missing, the subagent was likely dispatched as read-only (Explore type) — print a warning: "chunk N missing from disk — …
- Distinctive: line 355
If a subagent failed or returned invalid JSON, print a warning and skip that chunk - do not abort
- Very specific: line 357
If more than half the chunks failed or are missing, stop and tell the user to re-run and ensure subagent_type="general-purpose" is used.
- Very specific: line 484
If this step prints ERROR: Graph is empty, stop and tell the user what happened - do not proceed to labeling or visualization.
- Very specific: line 490
Read .graphify_analysis.json.
- Very specific: line 532
If --obsidian was given:
- Very specific: line 534
If --obsidian-dir <path> was also given, use that path as the vault directory.
- Very specific: line 695
If total_words from .graphify_detect.json is greater than 5,000, run:
- Distinctive: line 764
If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
- Distinctive: line 773
Do NOT paste the full report - just those three sections.
- Distinctive: line 787
Use when you've added or modified files since the last run.
- Distinctive: line 806
If new files exist, first check whether all changed files are code files:
- Very specific: line 822
If code_only is True: print [graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed), run only Step 3A …
- Very specific: line 824
If code_only is False (any changed file is a doc/paper/image): run the full Steps 3A–3C pipeline as normal.
- Distinctive: line 853
After Step 4, show the graph diff:
- Very specific: line 880
Before the merge step, save the old graph: Copy-Item graphify-out/graph.json .graphify_old.json Clean up after: Remove-Item -ErrorAction …
- Very specific: line 950
If it fails, stop and tell the user to run /graphify <path> first.
- Distinctive: line 955
Run the appropriate traversal from each starting node.
- Distinctive: line 956
Read the subgraph - node labels, edge relations, confidence tags, source locations.
- Distinctive: line 958
If the graph lacks enough information, say so - do not hallucinate edges.
- Distinctive: line 1049
After writing the answer, save it back into the graph so it improves future queries:
- Distinctive: line 1124
After writing the explanation, save it back:
- Distinctive: line 1233
Start a background watcher that monitors a folder and auto-updates the graph when files change.
- Very specific: line 1262
After every git commit, the hook detects which code files changed (via git diff HEAD~1), re-runs AST extraction on those files, and …
- Distinctive: line 1264
If a post-commit hook already exists, graphify appends to it rather than replacing it.
- Distinctive: line 1270
Run once per project to make graphify always-on in Claude Code sessions:
- Very specific: line 1288
If vertical scrolling breaks in PowerShell after running graphify, this is caused by ANSI escape sequences from the graspologic library.
- Distinctive: line 1291
Use Windows Terminal: instead of the legacy PowerShell console — Windows Terminal handles ANSI codes correctly
- Distinctive: line 1299
Never invent an edge. If unsure, use AMBIGUOUS.
- Distinctive: line 1300
Never skip the corpus check warning.
- Distinctive: line 1301
Always show token cost in the report.
- Distinctive: line 1302
Never hide cohesion scores behind symbols - show the raw number.
- Distinctive: line 1303
Never run HTML viz on a graph with more than 5,000 nodes without warning the user.
“Common” is measured: kept by at least 6 in 10 of the 113 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
/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.
Upstream
- Upstream project:
https://github.com/safishamsi/graphify - Official PyPI package:
graphifyy(doubley) - Upstream license: MIT
- This repo ships guidance for using the upstream tool. It does not vendor the Python package itself.
Read UPSTREAM.md before changing compatibility, install, or license claims.
Requirements
Required tools:
- Python 3.10+
- Official
graphifyypackage installed for thepythoninterpreter this skill will use
Check:
Get-Command graphify -ErrorAction SilentlyContinue
python -c "import graphify, sys; print(sys.executable)" 2>$nullInstall options:
uv tool install graphifyy
pipx install graphifyy
python -m pip install graphifyyRules:
- Do not assume
graphifyyis installed. - Do not auto-install without user approval.
- Show the install command before running it.
- If
graphifyexists on PATH butpython -c "import graphify"fails, stop and tell the user the skill cannot use that interpreter yet.
Manual CLI Notes
- In PowerShell, run
graphify ., not/graphify .. - To install upstream Codex integration after the package is present:
graphify codex install - Manual CLI use and skill-guided use are separate: the package must exist first in both cases.
Usage
/graphify # full pipeline on current directory → Obsidian vault
/graphify <path> # full pipeline on specific path
/graphify <path> --mode deep # thorough extraction, richer INFERRED edges
/graphify <path> --update # incremental - re-extract only new/changed files
/graphify <path> --directed # build directed graph (preserves edge direction: source→target)
/graphify <path> --whisper-model medium # use a larger Whisper model for better transcription accuracy
/graphify <path> --cluster-only # rerun clustering on existing graph
/graphify <path> --no-viz # skip visualization, just report + JSON
/graphify <path> --html # (HTML is generated by default - this flag is a no-op)
/graphify <path> --svg # also export graph.svg (embeds in Notion, GitHub)
/graphify <path> --graphml # export graph.graphml (Gephi, yEd)
/graphify <path> --neo4j # generate graphify-out/cypher.txt for Neo4j
/graphify <path> --neo4j-push bolt://localhost:7687 # push directly to Neo4j
/graphify <path> --wiki # build agent-crawlable wiki (index.md + one article per community)
/graphify <path> --obsidian # generate Obsidian vault
/graphify <path> --obsidian --obsidian-dir ~/my/vault # write vault to custom path (e.g. existing vault)
/graphify <path> --mcp # start MCP stdio server for agent access
/graphify <path> --watch # watch folder, auto-rebuild on code changes (no LLM needed)
/graphify add <url> # fetch URL, save to ./raw, update graph
/graphify add <url> --author "Name" # tag who wrote it
/graphify add <url> --contributor "Name" # tag who added it to the corpus
/graphify query "<question>" # BFS traversal - broad context
/graphify query "<question>" --dfs # DFS - trace a specific path
/graphify query "<question>" --budget 1500 # cap answer at N tokens
/graphify path "AuthModule" "Database" # shortest path between two concepts
/graphify explain "SwinTransformer" # plain-language explanation of a nodeWhat graphify is for
graphify is built around Andrej Karpathy's /raw folder workflow: drop anything into a folder - papers, tweets, screenshots, code, notes - and get a structured knowledge graph that shows you what you didn't know was connected.
Three things it does that your AI assistant alone cannot:
- Persistent graph - relationships are stored in
graphify-out/graph.jsonand survive across sessions. Ask questions weeks later without re-reading everything. - Honest audit trail - every edge is tagged EXTRACTED, INFERRED, or AMBIGUOUS. You know what was found vs invented.
- Cross-document surprise - community detection finds connections between concepts in different files that you would never think to ask about directly.
Use it for:
- A codebase you're new to (understand architecture before touching anything)
- A reading list (papers + tweets + notes → one navigable graph)
- A research corpus (citation graph + concept graph in one)
- Your personal /raw folder (drop everything in, let it grow, query it)
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.
Step 1 - Ensure graphify is installed
# Check CLI presence and Python importability
Get-Command graphify -ErrorAction SilentlyContinue
python -c "import graphify, sys; open('.graphify_python', 'w').write(sys.executable)" 2>$nullIf the import succeeds, print nothing and move straight to Step 2.
If it fails:
- Stop. Do not continue with detection or extraction.
- Tell the user
graphifyyis required and is not currently importable frompython. - Show install options:
uv tool install graphifyy
pipx install graphifyy
python -m pip install graphifyy- Ask before installing it.
Step 2 - Detect files
python -c "
import json
from graphify.detect import detect
from pathlib import Path
result = detect(Path('INPUT_PATH'))
print(json.dumps(result))
" > .graphify_detect.jsonReplace INPUT_PATH with the actual path the user provided. Do NOT cat or print the JSON - read it silently and present a clean summary instead:
Corpus: X files · ~Y words
code: N files (.py .ts .go ...)
docs: N files (.md .txt ...)
papers: N files (.pdf ...)
images: N files
video: N files (.mp4 .mp3 ...)Omit any category with 0 files from the summary.
Then act on it:
- If
total_filesis 0: stop with "No supported files found in [path]." - If
skipped_sensitiveis non-empty: mention file count skipped, not the file names. - If
total_words> 2,000,000 ORtotal_files> 200: show the warning and the top 5 subdirectories by file count, then ask which subfolder to run on. Wait for the user's answer before proceeding. - Otherwise: proceed directly to Step 2.5 if video files were detected, or Step 3 if not.
Step 2.5 - Transcribe video / audio files (only if video files detected)
Skip this step entirely if detect returned zero video files.
Video and audio files cannot be read directly. Transcribe them to text first, then treat the transcripts as doc files in Step 3.
Strategy: Read the god nodes from the detect output or analysis file. You are already a language model - write a one-sentence domain hint yourself from those labels. Then pass it to Whisper as the initial prompt. No separate API call needed.
However, if the corpus has only video files and no other docs/code, use the generic fallback prompt: "Use proper punctuation and paragraph breaks."
Step 1 - Write the Whisper prompt yourself.
Read the top god node labels from detect output or analysis, then compose a short domain hint sentence, for example:
- Labels:
transformer, attention, encoder, decoder->"Machine learning research on transformer architectures and attention mechanisms. Use proper punctuation and paragraph breaks." - Labels:
kubernetes, deployment, pod, helm->"DevOps discussion about Kubernetes deployments and Helm charts. Use proper punctuation and paragraph breaks."
Set it as $env:GRAPHIFY_WHISPER_PROMPT before running the transcription command.
Step 2 - Transcribe (PowerShell):
& (Get-Content graphify-out\.graphify_python) -c "
import json, os
from pathlib import Path
from graphify.transcribe import transcribe_all
detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text())
video_files = detect.get('files', {}).get('video', [])
prompt = os.environ.get('GRAPHIFY_WHISPER_PROMPT', 'Use proper punctuation and paragraph breaks.')
transcript_paths = transcribe_all(video_files, initial_prompt=prompt)
print(json.dumps(transcript_paths))
" | Out-File -FilePath graphify-out\.graphify_transcripts.json -Encoding utf8After transcription:
- Read the transcript paths from
graphify-out\.graphify_transcripts.json - Add them to the docs list before dispatching semantic subagents in Step 3B
- Print how many transcripts were created:
Transcribed N video file(s) -> treating as docs - If transcription fails for a file, print a warning and continue with the rest
Whisper model: Default is base. If the user passed --whisper-model <name>, set $env:GRAPHIFY_WHISPER_MODEL = "<name>" before running the command above.
Step 3 - Extract entities and relationships
Before starting: note whether --mode deep was given. You must pass DEEP_MODE=true to every subagent in Step B2 if it was. Track this from the original invocation - do not lose it.
This step has two parts: structural extraction (deterministic, free) and semantic extraction (your AI model, costs tokens).
Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.
Note: Parallelizing AST + semantic saves 5-15s on large corpora. AST is deterministic and fast; start it while subagents are processing docs/papers.
Part A - Structural extraction for code files
For any code files detected, run AST extraction in parallel with Part B subagents:
python -c "
import sys, json
from graphify.extract import collect_files, extract
from pathlib import Path
import json
code_files = []
detect = json.loads(Path('.graphify_detect.json').read_text())
for f in detect.get('files', {}).get('code', []):
code_files.extend(collect_files(Path(f)) if Path(f).is_dir() else [Path(f)])
if code_files:
result = extract(code_files)
Path('.graphify_ast.json').write_text(json.dumps(result, indent=2))
print(f'AST: {len(result[\"nodes\"])} nodes, {len(result[\"edges\"])} edges')
else:
Path('.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 (parallel subagents)
Fast path: If detection found zero docs, papers, and images (code-only corpus), skip Part B entirely and go straight to Part C. AST handles code - there is nothing for semantic subagents to do.
MANDATORY: You MUST use the Agent tool here. Reading files yourself one-by-one is forbidden - it is 5-10x slower. If you do not use the Agent tool you are doing this wrong.
Before dispatching subagents, print a timing estimate:
- Load
total_wordsand file counts from.graphify_detect.json - Estimate agents needed:
ceil(uncached_non_code_files / 22)(chunk size is 20-25) - Estimate time: ~45s per agent batch (they run in parallel, so total ≈ 45s × ceil(agents/parallel_limit))
- Print: "Semantic extraction: ~N files → X agents, estimated ~Ys"
Step B0 - Check extraction cache first
Before dispatching any subagents, check which files already have cached extraction results:
python -c "
import json
from graphify.cache import check_semantic_cache
from pathlib import Path
detect = json.loads(Path('.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 or cached_hyperedges:
Path('.graphify_cached.json').write_text(json.dumps({'nodes': cached_nodes, 'edges': cached_edges, 'hyperedges': cached_hyperedges}))
Path('.graphify_uncached.txt').write_text('\n'.join(uncached))
print(f'Cache: {len(all_files)-len(uncached)} files hit, {len(uncached)} files need extraction')
"Only dispatch subagents for files listed in .graphify_uncached.txt. If all files are cached, skip to Part C directly.
Step B1 - Split into chunks
Load files from .graphify_uncached.txt. Split into chunks of 20-25 files each. Each image gets its own chunk (vision needs separate context).
Step B2 - Dispatch ALL subagents in a single message
Call the Agent tool multiple times IN THE SAME RESPONSE - one call per chunk. This is the only way they run in parallel. If you make one Agent call, wait, then make another, you are doing it sequentially and defeating the purpose.
Concrete example for 3 chunks:
[Agent tool call 1: files 1-15]
[Agent tool call 2: files 16-30]
[Agent tool call 3: files 31-45]All three in one message. Not three separate messages.
Each subagent receives this exact prompt (substitute FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, and DEEP_MODE):
You are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment.
Output ONLY valid JSON matching the schema below - no explanation, no markdown fences, no preamble.
Files (chunk CHUNK_NUM of TOTAL_CHUNKS):
FILE_LIST
Rules:
- EXTRACTED: relationship explicit in source (import, call, citation, "see §3.2")
- INFERRED: reasonable inference (shared data structure, implied dependency)
- AMBIGUOUS: uncertain - flag for review, do not omit
Code files: focus on semantic edges AST cannot find (call relationships, shared data, arch patterns).
Do not re-extract imports - AST already has those.
Doc/paper files: extract named concepts, entities, citations. Also extract rationale — sections that explain WHY a decision was made, trade-offs chosen, or design intent. These become nodes with `rationale_for` edges pointing to the concept they explain.
Image files: use vision to understand what the image IS - do not just OCR.
UI screenshot: layout patterns, design decisions, key elements, purpose.
Chart: metric, trend/insight, data source.
Tweet/post: claim as node, author, concepts mentioned.
Diagram: components and connections.
Research figure: what it demonstrates, method, result.
Handwritten/whiteboard: ideas and arrows, mark uncertain readings AMBIGUOUS.
DEEP_MODE (if --mode deep was given): be aggressive with INFERRED edges - indirect deps,
shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting.
Semantic similarity: if two concepts in this chunk solve the same problem or represent the same idea without any structural link (no import, no call, no citation), add a `semantically_similar_to` edge marked INFERRED with a confidence_score reflecting how similar they are (0.6-0.95). Examples:
- Two functions that both validate user input but never call each other
- A class in code and a concept in a paper that describe the same algorithm
- Two error types that handle the same failure mode differently
Only add these when the similarity is genuinely non-obvious and cross-cutting. Do not add them for trivially similar things.
Hyperedges: if 3 or more nodes clearly participate together in a shared concept, flow, or pattern that is not captured by pairwise edges alone, add a hyperedge to a top-level `hyperedges` array. Examples:
- All classes that implement a common protocol or interface
- All functions in an authentication flow (even if they don't all call each other)
- All concepts from a paper section that form one coherent idea
Use sparingly — only when the group relationship adds information beyond the pairwise edges. Maximum 3 hyperedges per chunk.
If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author,
contributor onto every node from that file.
confidence_score is REQUIRED on every edge - never omit it, never use 0.5 as a default:
- EXTRACTED edges: confidence_score = 1.0 always
- INFERRED edges: reason about each edge individually.
Direct structural evidence (shared data structure, clear dependency): 0.8-0.9.
Reasonable inference with some uncertainty: 0.6-0.7.
Weak or speculative: 0.4-0.5. Most edges should be 0.6-0.9, not 0.5.
- AMBIGUOUS edges: 0.1-0.3
Output exactly this JSON (no other text):
{"nodes":[{"id":"filestem_entityname","label":"Human Readable Name","file_type":"code|document|paper|image","source_file":"relative/path","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to|rationale_for","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"relative/path","source_location":null,"weight":1.0}],"hyperedges":[{"id":"snake_case_id","label":"Human Readable Label","nodes":["node_id1","node_id2","node_id3"],"relation":"participate_in|implement|form","confidence":"EXTRACTED|INFERRED","confidence_score":0.75,"source_file":"relative/path"}],"input_tokens":0,"output_tokens":0}Step B3 - Collect, cache, and merge
Wait for all subagents. For each result:
- Check that
graphify-out/.graphify_chunk_NN.jsonexists on disk — this is the success signal - If the file exists and contains valid JSON with
nodesandedges, include it and save to cache - If the file is missing, the subagent was likely dispatched as read-only (Explore type) — print a warning: "chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent." Do not silently skip.
- If a subagent failed or returned invalid JSON, print a warning and skip that chunk - do not abort
If more than half the chunks failed or are missing, stop and tell the user to re-run and ensure subagent_type="general-purpose" is used.
Save new results to cache:
python -c "
import json
from graphify.cache import save_semantic_cache
from pathlib import Path
new = json.loads(Path('.graphify_semantic_new.json').read_text()) if Path('.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []))
print(f'Cached {saved} files')
"Merge cached + new results into .graphify_semantic.json:
python -c "
import json
from pathlib import Path
cached = json.loads(Path('.graphify_cached.json').read_text()) if Path('.graphify_cached.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
new = json.loads(Path('.graphify_semantic_new.json').read_text()) if Path('.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
all_nodes = cached['nodes'] + new.get('nodes', [])
all_edges = cached['edges'] + new.get('edges', [])
all_hyperedges = cached.get('hyperedges', []) + new.get('hyperedges', [])
seen = set()
deduped = []
for n in all_nodes:
if n['id'] not in seen:
seen.add(n['id'])
deduped.append(n)
merged = {
'nodes': deduped,
'edges': all_edges,
'hyperedges': all_hyperedges,
'input_tokens': new.get('input_tokens', 0),
'output_tokens': new.get('output_tokens', 0),
}
Path('.graphify_semantic.json').write_text(json.dumps(merged, indent=2))
print(f'Extraction complete - {len(deduped)} nodes, {len(all_edges)} edges ({len(cached[\"nodes\"])} from cache, {len(new.get(\"nodes\",[]))} new)')
"Clean up temp files: Remove-Item -ErrorAction SilentlyContinue .graphify_cached.json, .graphify_uncached.txt, .graphify_semantic_new.json
Part C - Merge AST + semantic into final extraction
python -c "
import sys, json
from pathlib import Path
ast = json.loads(Path('.graphify_ast.json').read_text())
sem = json.loads(Path('.graphify_semantic.json').read_text())
# Merge: AST nodes first, semantic nodes deduplicated by id
seen = {n['id'] for n in ast['nodes']}
merged_nodes = list(ast['nodes'])
for n in sem['nodes']:
if n['id'] not in seen:
merged_nodes.append(n)
seen.add(n['id'])
merged_edges = ast['edges'] + sem['edges']
merged_hyperedges = sem.get('hyperedges', [])
merged = {
'nodes': merged_nodes,
'edges': merged_edges,
'hyperedges': merged_hyperedges,
'input_tokens': sem.get('input_tokens', 0),
'output_tokens': sem.get('output_tokens', 0),
}
Path('.graphify_extract.json').write_text(json.dumps(merged, indent=2))
total = len(merged_nodes)
edges = len(merged_edges)
print(f'Merged: {total} nodes, {edges} edges ({len(ast[\"nodes\"])} AST + {len(sem[\"nodes\"])} semantic)')
"Step 4 - Build graph, cluster, analyze, generate outputs
New-Item -ItemType Directory -Force -Path graphify-out | Out-Null
python -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import cluster, score_all
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
from graphify.report import generate
from graphify.export import to_json
from pathlib import Path
extraction = json.loads(Path('.graphify_extract.json').read_text())
detection = json.loads(Path('.graphify_detect.json').read_text())
G = build_from_json(extraction)
communities = cluster(G)
cohesion = score_all(G, communities)
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
gods = god_nodes(G)
surprises = surprising_connections(G, communities)
labels = {cid: 'Community ' + str(cid) for cid in communities}
# Placeholder questions - regenerated with real labels in Step 5
questions = suggest_questions(G, communities, labels)
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions)
Path('graphify-out/GRAPH_REPORT.md').write_text(report)
to_json(G, communities, 'graphify-out/graph.json')
analysis = {
'communities': {str(k): v for k, v in communities.items()},
'cohesion': {str(k): v for k, v in cohesion.items()},
'gods': gods,
'surprises': surprises,
'questions': questions,
}
Path('.graphify_analysis.json').write_text(json.dumps(analysis, indent=2))
if G.number_of_nodes() == 0:
print('ERROR: Graph is empty - extraction produced no nodes.')
print('Possible causes: all files were skipped, binary-only corpus, or extraction failed.')
raise SystemExit(1)
print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges, {len(communities)} communities')
"If this step prints ERROR: Graph is empty, stop and tell the user what happened - do not proceed to labeling or visualization.
Replace INPUT_PATH with the actual path.
Step 5 - Label communities
Read .graphify_analysis.json. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading").
Then regenerate the report and save the labels for the visualizer:
python -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import score_all
from graphify.analyze import god_nodes, surprising_connections, suggest_questions
from graphify.report import generate
from pathlib import Path
extraction = json.loads(Path('.graphify_extract.json').read_text())
detection = json.loads(Path('.graphify_detect.json').read_text())
analysis = json.loads(Path('.graphify_analysis.json').read_text())
G = build_from_json(extraction)
communities = {int(k): v for k, v in analysis['communities'].items()}
cohesion = {int(k): v for k, v in analysis['cohesion'].items()}
tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
# LABELS - replace these with the names you chose above
labels = LABELS_DICT
# Regenerate questions with real community labels (labels affect question phrasing)
questions = suggest_questions(G, communities, labels)
report = generate(G, communities, cohesion, labels, analysis['gods'], analysis['surprises'], detection, tokens, 'INPUT_PATH', suggested_questions=questions)
Path('graphify-out/GRAPH_REPORT.md').write_text(report)
Path('.graphify_labels.json').write_text(json.dumps({str(k): v for k, v in labels.items()}))
print('Report updated with community labels')
"Replace LABELS_DICT with the actual dict you constructed (e.g. {0: "Attention Mechanism", 1: "Training Pipeline"}).
Replace INPUT_PATH with the actual path.
Step 6 - Generate Obsidian vault (opt-in) + HTML
Generate HTML always (unless --no-viz). Obsidian vault only if --obsidian was explicitly given — skip it otherwise, it generates one file per node.
If --obsidian was given:
- If
--obsidian-dir <path>was also given, use that path as the vault directory. Otherwise default tographify-out/obsidian.
python -c "
import sys, json
from graphify.build import build_from_json
from graphify.export import to_obsidian, to_canvas
from pathlib import Path
extraction = json.loads(Path('.graphify_extract.json').read_text())
analysis = json.loads(Path('.graphify_analysis.json').read_text())
labels_raw = json.loads(Path('.graphify_labels.json').read_text()) if Path('.graphify_labels.json').exists() else {}
G = build_from_json(extraction)
communities = {int(k): v for k, v in analysis['communities'].items()}
cohesion = {int(k): v for k, v in analysis['cohesion'].items()}
labels = {int(k): v for k, v in labels_raw.items()}
obsidian_dir = 'OBSIDIAN_DIR' # replace with --obsidian-dir value, or 'graphify-out/obsidian' if not given
n = to_obsidian(G, communities, obsidian_dir, community_labels=labels or None, cohesion=cohesion)
print(f'Obsidian vault: {n} notes in {obsidian_dir}/')
to_canvas(G, communities, f'{obsidian_dir}/graph.canvas', community_labels=labels or None)
print(f'Canvas: {obsidian_dir}/graph.canvas - open in Obsidian for structured community layout')
print()
print(f'Open {obsidian_dir}/ as a vault in Obsidian.')
print(' Graph view - nodes colored by community (set automatically)')
print(' graph.canvas - structured layout with communities as groups')
print(' _COMMUNITY_* - overview notes with cohesion scores and dataview queries')
"Generate the HTML graph (always, unless --no-viz):
python -c "
import sys, json
from graphify.build import build_from_json
from graphify.export import to_html
from pathlib import Path
extraction = json.loads(Path('.graphify_extract.json').read_text())
analysis = json.loads(Path('.graphify_analysis.json').read_text())
labels_raw = json.loads(Path('.graphify_labels.json').read_text()) if Path('.graphify_labels.json').exists() else {}
G = build_from_json(extraction)
communities = {int(k): v for k, v in analysis['communities'].items()}
labels = {int(k): v for k, v in labels_raw.items()}
if G.number_of_nodes() > 5000:
print(f'Graph has {G.number_of_nodes()} nodes - too large for HTML viz. Use Obsidian vault instead.')
else:
to_html(G, communities, 'graphify-out/graph.html', community_labels=labels or None)
print('graph.html written - open in any browser, no server needed')
"Step 7 - Neo4j export (only if --neo4j or --neo4j-push flag)
If --neo4j - generate a Cypher file for manual import:
python -c "
import sys, json
from graphify.build import build_from_json
from graphify.export import to_cypher
from pathlib import Path
G = build_from_json(json.loads(Path('.graphify_extract.json').read_text()))
to_cypher(G, 'graphify-out/cypher.txt')
print('cypher.txt written - import with: cypher-shell < graphify-out/cypher.txt')
"If --neo4j-push <uri> - push directly to a running Neo4j instance. Ask the user for credentials if not provided:
python -c "
import sys, json
from graphify.build import build_from_json
from graphify.cluster import cluster
from graphify.export import push_to_neo4j
from pathlib import Path
extraction = json.loads(Path('.graphify_extract.json').read_text())
analysis = json.loads(Path('.graphify_analysis.json').read_text())
G = build_from_json(extraction)
communities = {int(k): v for k, v in analysis['communities'].items()}
result = push_to_neo4j(G, uri='NEO4J_URI', user='NEO4J_USER', password='NEO4J_PASSWORD', communities=communities)
print(f'Pushed to Neo4j: {result[\"nodes\"]} nodes, {result[\"edges\"]} edges')
"Replace NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD with actual values. Default URI is bolt://localhost:7687, default user is neo4j. Uses MERGE - safe to re-run without creating duplicates.
Step 7b - SVG export (only if --svg flag)
python -c "
import sys, json
from graphify.build import build_from_json
from graphify.export import to_svg
from pathlib import Path
extraction = json.loads(Path('.graphify_extract.json').read_text())
analysis = json.loads(Path('.graphify_analysis.json').read_text())
labels_raw = json.loads(Path('.graphify_labels.json').read_text()) if Path('.graphify_labels.json').exists() else {}
G = build_from_json(extraction)
communities = {int(k): v for k, v in analysis['communities'].items()}
labels = {int(k): v for k, v in labels_raw.items()}
to_svg(G, communities, 'graphify-out/graph.svg', community_labels=labels or None)
print('graph.svg written - embeds in Obsidian, Notion, GitHub READMEs')
"Step 7c - GraphML export (only if --graphml flag)
python -c "
import json
from graphify.build import build_from_json
from graphify.export import to_graphml
from pathlib import Path
extraction = json.loads(Path('.graphify_extract.json').read_text())
analysis = json.loads(Path('.graphify_analysis.json').read_text())
G = build_from_json(extraction)
communities = {int(k): v for k, v in analysis['communities'].items()}
to_graphml(G, communities, 'graphify-out/graph.graphml')
print('graph.graphml written - open in Gephi, yEd, or any GraphML tool')
"Step 7d - MCP server (only if --mcp flag)
python -m graphify.serve graphify-out/graph.jsonThis starts a stdio MCP server that exposes tools: query_graph, get_node, get_neighbors, get_community, god_nodes, graph_stats, shortest_path. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live.
To configure in Claude Desktop, add to claude_desktop_config.json:
{
"mcpServers": {
"graphify": {
"command": "python",
"args": ["-m", "graphify.serve", "/absolute/path/to/graphify-out/graph.json"]
}
}
}Step 8 - Token reduction benchmark (only if total_words > 5000)
If total_words from .graphify_detect.json is greater than 5,000, run:
python -c "
import json
from graphify.benchmark import run_benchmark, print_benchmark
from pathlib import Path
detection = json.loads(Path('.graphify_detect.json').read_text())
result = run_benchmark('graphify-out/graph.json', corpus_words=detection['total_words'])
print_benchmark(result)
"Print the output directly in chat. If total_words <= 5000, skip silently - the graph value is structural clarity, not token compression, for small corpora.
Step 9 - Save manifest, update cost tracker, clean up, and report
python -c "
import json
from pathlib import Path
from datetime import datetime, timezone
from graphify.detect import save_manifest
# Save manifest for --update
detect = json.loads(Path('.graphify_detect.json').read_text())
save_manifest(detect['files'])
# Update cumulative cost tracker
extract = json.loads(Path('.graphify_extract.json').read_text())
input_tok = extract.get('input_tokens', 0)
output_tok = extract.get('output_tokens', 0)
cost_path = Path('graphify-out/cost.json')
if cost_path.exists():
cost = json.loads(cost_path.read_text())
else:
cost = {'runs': [], 'total_input_tokens': 0, 'total_output_tokens': 0}
cost['runs'].append({
'date': datetime.now(timezone.utc).isoformat(),
'input_tokens': input_tok,
'output_tokens': output_tok,
'files': detect.get('total_files', 0),
})
cost['total_input_tokens'] += input_tok
cost['total_output_tokens'] += output_tok
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
Remove-Item -ErrorAction SilentlyContinue graphify-out/.needs_updateTell the user (omit the obsidian line unless --obsidian was given):
Graph complete. Outputs in PATH_TO_DIR/graphify-out/
graph.html - interactive graph, open in browser
GRAPH_REPORT.md - audit report
graph.json - raw graph data
obsidian/ - Obsidian vault (only if --obsidian was given)If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
Then paste these sections from GRAPH_REPORT.md directly into the chat:
- God Nodes
- Surprising Connections
- Suggested Questions
Do NOT paste the full report - just those three sections. Keep it concise.
Then immediately offer to explore. Pick the single most interesting suggested question from the report - the one that crosses the most community boundaries or has the most surprising bridge node - and ask:
"The most interesting question this graph can answer: [question]. Want me to trace it?"
If the user says yes, run /graphify query "[question]" on the graph and walk them through the answer using the graph structure - which nodes connect, which community boundaries get crossed, what the path reveals. Keep going as long as they want to explore. Each answer should end with a natural follow-up ("this connects to X - want to go deeper?") so the session feels like navigation, not a one-shot report.
The graph is the map. Your job after the pipeline is to be the guide.
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.
python -c "
import sys, json
from graphify.detect import detect_incremental, save_manifest
from pathlib import Path
result = detect_incremental(Path('INPUT_PATH'))
new_total = result.get('new_total', 0)
print(json.dumps(result, indent=2))
Path('.graphify_incremental.json').write_text(json.dumps(result))
if new_total == 0:
print('No files changed since last run. Nothing to update.')
raise SystemExit(0)
print(f'{new_total} new/changed file(s) to re-extract.')
"If new files exist, first check whether all changed files are code files:
python -c "
import json
from pathlib import Path
result = json.loads(open('.graphify_incremental.json').read()) if Path('.graphify_incremental.json').exists() else {}
code_exts = {'.py','.ts','.js','.go','.rs','.java','.cpp','.c','.rb','.swift','.kt','.cs','.scala','.php','.cc','.cxx','.hpp','.h','.kts','.lua','.toc'}
new_files = result.get('new_files', {})
all_changed = [f for files in new_files.values() for f in files]
code_only = all(Path(f).suffix.lower() in code_exts for f in all_changed)
print('code_only:', code_only)
"If code_only is True: print [graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed), run only Step 3A (AST) on the changed files, skip Step 3B entirely (no subagents), then go straight to merge and Steps 4–8.
If code_only is False (any changed file is a doc/paper/image): run the full Steps 3A–3C pipeline as normal.
Then:
python -c "
import sys, json
from graphify.build import build_from_json
from graphify.export import to_json
from networkx.readwrite import json_graph
import networkx as nx
from pathlib import Path
# Load existing graph
existing_data = json.loads(Path('graphify-out/graph.json').read_text())
G_existing = json_graph.node_link_graph(existing_data, edges='links')
# Load new extraction
new_extraction = json.loads(Path('.graphify_extract.json').read_text())
G_new = build_from_json(new_extraction)
# Merge: new nodes/edges into existing graph
G_existing.update(G_new)
print(f'Merged: {G_existing.number_of_nodes()} nodes, {G_existing.number_of_edges()} edges')
" Then run Steps 4–8 on the merged graph as normal.
After Step 4, show the graph diff:
python -c "
import json
from graphify.analyze import graph_diff
from graphify.build import build_from_json
from networkx.readwrite import json_graph
import networkx as nx
from pathlib import Path
# Load old graph (before update) from backup written before merge
old_data = json.loads(Path('.graphify_old.json').read_text()) if Path('.graphify_old.json').exists() else None
new_extract = json.loads(Path('.graphify_extract.json').read_text())
G_new = build_from_json(new_extract)
if old_data:
G_old = json_graph.node_link_graph(old_data, edges='links')
diff = graph_diff(G_old, G_new)
print(diff['summary'])
if diff['new_nodes']:
print('New nodes:', ', '.join(n['label'] for n in diff['new_nodes'][:5]))
if diff['new_edges']:
print('New edges:', len(diff['new_edges']))
"Before the merge step, save the old graph: Copy-Item graphify-out/graph.json .graphify_old.json
Clean up after: Remove-Item -ErrorAction SilentlyContinue .graphify_old.json
For --cluster-only
Skip Steps 1–3. Load the existing graph from graphify-out/graph.json and re-run clustering:
python -c "
import sys, json
from graphify.cluster import cluster, score_all
from graphify.analyze import god_nodes, surprising_connections
from graphify.report import generate
from graphify.export import to_json
from networkx.readwrite import json_graph
import networkx as nx
from pathlib import Path
data = json.loads(Path('graphify-out/graph.json').read_text())
G = json_graph.node_link_graph(data, edges='links')
detection = {'total_files': 0, 'total_words': 99999, 'needs_graph': True, 'warning': None,
'files': {'code': [], 'document': [], 'paper': []}}
tokens = {'input': 0, 'output': 0}
communities = cluster(G)
cohesion = score_all(G, communities)
gods = god_nodes(G)
surprises = surprising_connections(G, communities)
labels = {cid: 'Community ' + str(cid) for cid in communities}
report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, '.')
Path('graphify-out/GRAPH_REPORT.md').write_text(report)
to_json(G, communities, 'graphify-out/graph.json')
analysis = {
'communities': {str(k): v for k, v in communities.items()},
'cohesion': {str(k): v for k, v in cohesion.items()},
'gods': gods,
'surprises': surprises,
}
Path('.graphify_analysis.json').write_text(json.dumps(analysis, indent=2))
print(f'Re-clustered: {len(communities)} communities')
"Then run Steps 5–9 as normal (label communities, generate viz, benchmark, clean up, report).
For /graphify query
Two traversal modes - choose based on the question:
| Mode | Flag | Best for |
| BFS (default) | (none) | "What is X connected to?" - broad context, nearest neighbors first |
| DFS | --dfs | "How does X reach Y?" - trace a specific chain or dependency path |
First check the graph exists:
python -c "
from pathlib import Path
if not Path('graphify-out/graph.json').exists():
print('ERROR: No graph found. Run /graphify <path> first to build the graph.')
raise SystemExit(1)
"If it fails, stop and tell the user to run /graphify <path> first.
Load graphify-out/graph.json, then:
- Find the 1-3 nodes whose label best matches key terms in the question.
- Run the appropriate traversal from each starting node.
- Read the subgraph - node labels, edge relations, confidence tags, source locations.
- Answer using only what the graph contains. Quote
source_locationwhen citing a specific fact. - If the graph lacks enough information, say so - do not hallucinate edges.
python -c "
import sys, json
from networkx.readwrite import json_graph
import networkx as nx
from pathlib import Path
data = json.loads(Path('graphify-out/graph.json').read_text())
G = json_graph.node_link_graph(data, edges='links')
question = 'QUESTION'
mode = 'MODE' # 'bfs' or 'dfs'
terms = [t.lower() for t in question.split() if len(t) > 3]
# Find best-matching start nodes
scored = []
for nid, ndata in G.nodes(data=True):
label = ndata.get('label', '').lower()
score = sum(1 for t in terms if t in label)
if score > 0:
scored.append((score, nid))
scored.sort(reverse=True)
start_nodes = [nid for _, nid in scored[:3]]
if not start_nodes:
print('No matching nodes found for query terms:', terms)
sys.exit(0)
subgraph_nodes = set()
subgraph_edges = []
if mode == 'dfs':
# DFS: follow one path as deep as possible before backtracking.
# Depth-limited to 6 to avoid traversing the whole graph.
visited = set()
stack = [(n, 0) for n in reversed(start_nodes)]
while stack:
node, depth = stack.pop()
if node in visited or depth > 6:
continue
visited.add(node)
subgraph_nodes.add(node)
for neighbor in G.neighbors(node):
if neighbor not in visited:
stack.append((neighbor, depth + 1))
subgraph_edges.append((node, neighbor))
else:
# BFS: explore all neighbors layer by layer up to depth 3.
frontier = set(start_nodes)
subgraph_nodes = set(start_nodes)
for _ in range(3):
next_frontier = set()
for n in frontier:
for neighbor in G.neighbors(n):
if neighbor not in subgraph_nodes:
next_frontier.add(neighbor)
subgraph_edges.append((n, neighbor))
subgraph_nodes.update(next_frontier)
frontier = next_frontier
# Token-budget aware output: rank by relevance, cut at budget (~4 chars/token)
token_budget = BUDGET # default 2000
char_budget = token_budget * 4
# Score each node by term overlap for ranked output
def relevance(nid):
label = G.nodes[nid].get('label', '').lower()
return sum(1 for t in terms if t in label)
ranked_nodes = sorted(subgraph_nodes, key=relevance, reverse=True)
lines = [f'Traversal: {mode.upper()} | Start: {[G.nodes[n].get(\"label\",n) for n in start_nodes]} | {len(subgraph_nodes)} nodes']
for nid in ranked_nodes:
d = G.nodes[nid]
lines.append(f' NODE {d.get(\"label\", nid)} [src={d.get(\"source_file\",\"\")} loc={d.get(\"source_location\",\"\")}]')
for u, v in subgraph_edges:
if u in subgraph_nodes and v in subgraph_nodes:
d = G.edges[u, v]
lines.append(f' EDGE {G.nodes[u].get(\"label\",u)} --{d.get(\"relation\",\"\")} [{d.get(\"confidence\",\"\")}]--> {G.nodes[v].get(\"label\",v)}')
output = '\n'.join(lines)
if len(output) > char_budget:
output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)'
print(output)
"Replace QUESTION with the user's actual question, MODE with bfs or dfs, and BUDGET with the token budget (default 2000, or whatever --budget N specifies). Then answer based on the subgraph output above.
After writing the answer, save it back into the graph so it improves future queries:
python -m graphify save-result --question "QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2Replace QUESTION with the question, ANSWER with your full answer text, SOURCE_NODES with the list of node labels you cited. This closes the feedback loop: the next --update will extract this Q&A as a node in the graph.
For /graphify path
Find the shortest path between two named concepts in the graph.
First check the graph exists:
python -c "
from pathlib import Path
if not Path('graphify-out/graph.json').exists():
print('ERROR: No graph found. Run /graphify <path> first to build the graph.')
raise SystemExit(1)
"If it fails, stop and tell the user to run /graphify <path> first.
python -c "
import json, sys
import networkx as nx
from networkx.readwrite import json_graph
from pathlib import Path
data = json.loads(Path('graphify-out/graph.json').read_text())
G = json_graph.node_link_graph(data, edges='links')
a_term = 'NODE_A'
b_term = 'NODE_B'
def find_node(term):
term = term.lower()
scored = sorted(
[(sum(1 for w in term.split() if w in G.nodes[n].get('label','').lower()), n)
for n in G.nodes()],
reverse=True
)
return scored[0][1] if scored and scored[0][0] > 0 else None
src = find_node(a_term)
tgt = find_node(b_term)
if not src or not tgt:
print(f'Could not find nodes matching: {a_term!r} or {b_term!r}')
sys.exit(0)
try:
path = nx.shortest_path(G, src, tgt)
print(f'Shortest path ({len(path)-1} hops):')
for i, nid in enumerate(path):
label = G.nodes[nid].get('label', nid)
if i < len(path) - 1:
edge = G.edges[nid, path[i+1]]
rel = edge.get('relation', '')
conf = edge.get('confidence', '')
print(f' {label} --{rel}--> [{conf}]')
else:
print(f' {label}')
except nx.NetworkXNoPath:
print(f'No path found between {a_term!r} and {b_term!r}')
except nx.NodeNotFound as e:
print(f'Node not found: {e}')
"Replace NODE_A and NODE_B with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant.
After writing the explanation, save it back:
python -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_BFor /graphify explain
Give a plain-language explanation of a single node - everything connected to it.
First check the graph exists:
python -c "
from pathlib import Path
if not Path('graphify-out/graph.json').exists():
print('ERROR: No graph found. Run /graphify <path> first to build the graph.')
raise SystemExit(1)
"If it fails, stop and tell the user to run /graphify <path> first.
python -c "
import json, sys
import networkx as nx
from networkx.readwrite import json_graph
from pathlib import Path
data = json.loads(Path('graphify-out/graph.json').read_text())
G = json_graph.node_link_graph(data, edges='links')
term = 'NODE_NAME'
term_lower = term.lower()
# Find best matching node
scored = sorted(
[(sum(1 for w in term_lower.split() if w in G.nodes[n].get('label','').lower()), n)
for n in G.nodes()],
reverse=True
)
if not scored or scored[0][0] == 0:
print(f'No node matching {term!r}')
sys.exit(0)
nid = scored[0][1]
data_n = G.nodes[nid]
print(f'NODE: {data_n.get(\"label\", nid)}')
print(f' source: {data_n.get(\"source_file\",\"unknown\")}')
print(f' type: {data_n.get(\"file_type\",\"unknown\")}')
print(f' degree: {G.degree(nid)}')
print()
print('CONNECTIONS:')
for neighbor in G.neighbors(nid):
edge = G.edges[nid, neighbor]
nlabel = G.nodes[neighbor].get('label', neighbor)
rel = edge.get('relation', '')
conf = edge.get('confidence', '')
src_file = G.nodes[neighbor].get('source_file', '')
print(f' --{rel}--> {nlabel} [{conf}] ({src_file})')
"Replace NODE_NAME with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations.
After writing the explanation, save it back:
python -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAMEFor /graphify add
Fetch a URL and add it to the corpus, then update the graph.
python -c "
import sys
from graphify.ingest import ingest
from pathlib import Path
try:
out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR')
print(f'Saved to {out}')
except ValueError as e:
print(f'error: {e}', file=sys.stderr)
sys.exit(1)
except RuntimeError as e:
print(f'error: {e}', file=sys.stderr)
sys.exit(1)
"Replace URL with the actual URL, AUTHOR with the user's name if provided, CONTRIBUTOR likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the --update pipeline on ./raw to merge the new file into the existing graph.
Supported URL types (auto-detected):
- Twitter/X → fetched via oEmbed, saved as
.mdwith tweet text and author - arXiv → abstract + metadata saved as
.md - PDF → downloaded as
.pdf - Images (.png/.jpg/.webp) → downloaded, vision extraction runs on next build
- Any webpage → converted to markdown via html2text
For --watch
Start a background watcher that monitors a folder and auto-updates the graph when files change.
python -m graphify.watch INPUT_PATH --debounce 3Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
- Code files only (.py, .ts, .go, etc.): re-runs AST extraction + rebuild + cluster immediately, no LLM needed.
graph.jsonandGRAPH_REPORT.mdare updated automatically. - Docs, papers, or images: writes a
graphify-out/needs_updateflag and prints a notification to run/graphify --update(LLM semantic re-extraction required).
Debounce (default 3s): waits until file activity stops before triggering, so a wave of parallel agent writes doesn't trigger a rebuild per file.
Press Ctrl+C to stop.
For agentic workflows: run --watch in a background terminal. Code changes from agent waves are picked up automatically between waves. If agents are also writing docs or notes, you'll need a manual /graphify --update after those waves.
For git commit hook
Install a post-commit hook that auto-rebuilds the graph after every commit. No background process needed - triggers once per commit, works with any editor.
graphify hook install # install
graphify hook uninstall # remove
graphify hook status # checkAfter every git commit, the hook detects which code files changed (via git diff HEAD~1), re-runs AST extraction on those files, and rebuilds graph.json and GRAPH_REPORT.md. Doc/image changes are ignored by the hook - run /graphify --update manually for those.
If a post-commit hook already exists, graphify appends to it rather than replacing it.
For native CLAUDE.md integration
Run once per project to make graphify always-on in Claude Code sessions:
graphify claude installThis writes a ## graphify section to the local CLAUDE.md that instructs Claude to check the graph before answering codebase questions and rebuild it after code changes. No manual /graphify needed in future sessions.
graphify claude uninstall # remove the sectionTroubleshooting
PowerShell 5.1: Vertical scrolling stops working
If vertical scrolling breaks in PowerShell after running graphify, this is caused by ANSI escape sequences from the graspologic library. Graphify v0.3.10+ suppresses this output, but if you still see the issue:
- Upgrade graphify:
pip install --upgrade graphifyy - Use Windows Terminal instead of the legacy PowerShell console — Windows Terminal handles ANSI codes correctly
- Reset your terminal: close and reopen PowerShell
- Skip graspologic: uninstall it (
pip uninstall graspologic) and graphify will fall back to NetworkX's built-in Louvain algorithm, which produces no ANSI output
Honesty Rules
- Never invent an edge. If unsure, use AMBIGUOUS.
- Never skip the corpus check warning.
- Always show token cost in the report.
- Never hide cohesion scores behind symbols - show the raw number.
- Never run HTML viz on a graph with more than 5,000 nodes without warning the user.
Verification
- Install check passed (or the user approved installation) before any pipeline step ran
- The corpus summary was shown, and the large-corpus warning was honored when triggered
- Semantic extraction used parallel subagents dispatched in a single message — not sequential self-reads
- The empty-graph error stopped the run instead of proceeding to labeling or visualization
- The expected outputs exist under the graphify-out directory, and the run's token cost was reported
- Only the three report sections were pasted into chat, followed by the exploration offer
- Every edge carries an EXTRACTED, INFERRED, or AMBIGUOUS tag — nothing invented
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

