codegraph-kit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ """codegraph — cached, incremental code-graph maps so AI agents query structure
2
+ instead of reading whole files."""
3
+
4
+ __version__ = "0.1.0"
codegraph_kit/cli.py ADDED
@@ -0,0 +1,106 @@
1
+ """codegraph — cached, incremental code-graph maps so agents query structure
2
+ instead of reading whole files. Backed by graphify (tree-sitter AST extraction).
3
+
4
+ codegraph build [dir] full graph build (registers repo root)
5
+ codegraph ensure [dir] incremental refresh (only changed files re-extracted)
6
+ codegraph map [dir] compact overview: counts, per-dir breakdown, top hubs
7
+ codegraph file <path> outline of one file (defs + line ranges -> sliced reads)
8
+ codegraph sym <name> [dir] locate symbol by substring -> file + lines
9
+ codegraph callers <name> [dir] who calls it
10
+ codegraph callees <name> [dir] what it calls
11
+ codegraph deps <path> imports of a file
12
+ codegraph touch <path> re-extract one file into its repo's cache (hook use)
13
+ codegraph agent print an instruction snippet for AGENTS.md / CLAUDE.md
14
+ codegraph mcp run as an MCP server (requires `pip install codegraph-kit[mcp]`)
15
+ codegraph --version print version
16
+
17
+ Queries auto-run `ensure` first, so the graph tracks the working tree.
18
+ """
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from . import __version__, core, queries
23
+
24
+ AGENT_SNIPPET = """\
25
+ ## Code navigation — graph first, read only what you need
26
+
27
+ This repo has `codegraph` (cached tree-sitter code graphs). Before reading
28
+ source files, query the graph and then read ONLY the located line ranges:
29
+
30
+ - `codegraph map` — repo overview: size, per-directory breakdown, top hub symbols
31
+ - `codegraph file <path>` — outline of a file (definitions + line ranges)
32
+ - `codegraph sym <name>` — locate a symbol -> file:lines
33
+ - `codegraph callers <name>` / `codegraph callees <name>` — trace call edges
34
+ - `codegraph deps <path>` — what a file imports
35
+
36
+ Run `codegraph build` once per repo; afterwards every query refreshes changed
37
+ files automatically. Fall back to reading whole files only when the graph
38
+ can't answer (unsupported language, dynamic dispatch, subtle logic).
39
+ """
40
+
41
+
42
+ def main(argv=None):
43
+ args = sys.argv[1:] if argv is None else argv
44
+ if not args or args[0] in ("-h", "--help", "help"):
45
+ print(__doc__)
46
+ return 0
47
+ cmd = args[0]
48
+ tail = args[1:]
49
+
50
+ if cmd in ("--version", "-V", "version"):
51
+ print(f"codegraph {__version__}")
52
+ return 0
53
+ if cmd == "agent":
54
+ print(AGENT_SNIPPET)
55
+ return 0
56
+ if cmd == "mcp":
57
+ from .mcp_server import run # lazy: needs the [mcp] extra
58
+ run()
59
+ return 0
60
+
61
+ target = None
62
+ if cmd in ("build", "ensure", "map", "hubs"):
63
+ root = core.find_root(Path(tail[0]).resolve() if tail else Path.cwd())
64
+ elif cmd in ("file", "deps", "touch"):
65
+ if not tail:
66
+ print(f"usage: codegraph {cmd} <path>", file=sys.stderr)
67
+ return 2
68
+ target = Path(tail[0]).resolve()
69
+ root = core.find_root(target)
70
+ elif cmd in ("sym", "callers", "callees"):
71
+ if not tail:
72
+ print(f"usage: codegraph {cmd} <name> [dir]", file=sys.stderr)
73
+ return 2
74
+ root = core.find_root(Path(tail[1]).resolve() if len(tail) > 1 else Path.cwd())
75
+ else:
76
+ print(f"unknown command: {cmd}")
77
+ print(__doc__)
78
+ return 2
79
+
80
+ if cmd == "build":
81
+ g, idx, n = core.build(root)
82
+ print(f"built {root}: {n} files -> {len(g['nodes'])} nodes, {len(g['edges'])} edges")
83
+ return 0
84
+ if cmd == "touch":
85
+ core.touch(target, root)
86
+ return 0
87
+
88
+ g = core.ensure(root)
89
+
90
+ if cmd == "ensure":
91
+ print(f"{root}: {len(g['nodes'])} nodes, {len(g['edges'])} edges (fresh)")
92
+ elif cmd in ("map", "hubs"):
93
+ print(queries.q_map(root, g))
94
+ elif cmd == "file":
95
+ print(queries.q_file(root, g, target))
96
+ elif cmd == "sym":
97
+ print(queries.q_sym(root, g, tail[0]))
98
+ elif cmd in ("callers", "callees"):
99
+ print(queries.q_calls(root, g, tail[0], cmd))
100
+ elif cmd == "deps":
101
+ print(queries.q_deps(root, g, target))
102
+ return 0
103
+
104
+
105
+ if __name__ == "__main__":
106
+ sys.exit(main())
codegraph_kit/core.py ADDED
@@ -0,0 +1,181 @@
1
+ """Graph building, caching, and incremental refresh.
2
+
3
+ Graphs are stored per-repo under the cache dir (default ~/.cache/codegraph,
4
+ override with $CODEGRAPH_CACHE). Extraction is delegated to graphify
5
+ (tree-sitter AST parsing); this layer adds root discovery, mtime-based
6
+ incremental rebuilds, and root-relative path normalization.
7
+ """
8
+ import hashlib
9
+ import json
10
+ import os
11
+ import sys
12
+ import time
13
+ from pathlib import Path
14
+
15
+ CODE_EXTS = {".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".go", ".rs", ".java",
16
+ ".rb", ".c", ".h", ".cpp", ".hpp", ".cs", ".php", ".swift", ".kt", ".sh"}
17
+ SKIP_DIRS = {".git", "node_modules", "venv", ".venv", "__pycache__", "dist", "build",
18
+ ".next", "target", ".cache", "vendor", "site-packages", ".tox", "coverage"}
19
+ MAX_FILES = 5000
20
+ MAX_FILE_BYTES = 1_000_000
21
+
22
+
23
+ def cache_dir() -> Path:
24
+ env = os.environ.get("CODEGRAPH_CACHE")
25
+ return Path(env) if env else Path.home() / ".cache" / "codegraph"
26
+
27
+
28
+ def roots_file() -> Path:
29
+ return cache_dir() / "roots.json"
30
+
31
+
32
+ def repo_key(root: Path) -> Path:
33
+ return cache_dir() / hashlib.sha1(str(root).encode()).hexdigest()[:16]
34
+
35
+
36
+ def find_root(start: Path) -> Path:
37
+ p = start if start.is_dir() else start.parent
38
+ for q in [p, *p.parents]:
39
+ if (q / ".git").exists():
40
+ return q
41
+ return p
42
+
43
+
44
+ def code_files(root: Path):
45
+ out = []
46
+ for dirpath, dirnames, filenames in os.walk(root):
47
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".")]
48
+ for f in filenames:
49
+ fp = Path(dirpath) / f
50
+ if fp.suffix in CODE_EXTS and fp.stat().st_size < MAX_FILE_BYTES:
51
+ out.append(fp)
52
+ if len(out) > MAX_FILES:
53
+ print(f"WARNING: {len(out)} code files; graphing first {MAX_FILES} "
54
+ f"(largest dirs may be partial)", file=sys.stderr)
55
+ out = out[:MAX_FILES]
56
+ return out
57
+
58
+
59
+ def load(root: Path):
60
+ d = repo_key(root)
61
+ try:
62
+ graph = json.loads((d / "graph.json").read_text())
63
+ idx = json.loads((d / "index.json").read_text())
64
+ return graph, idx
65
+ except Exception:
66
+ return None, None
67
+
68
+
69
+ def save(root: Path, graph, idx):
70
+ d = repo_key(root)
71
+ d.mkdir(parents=True, exist_ok=True)
72
+ (d / "graph.json").write_text(json.dumps(graph))
73
+ (d / "index.json").write_text(json.dumps(idx))
74
+ rf = roots_file()
75
+ try:
76
+ roots = json.loads(rf.read_text()) if rf.exists() else {}
77
+ except Exception:
78
+ roots = {}
79
+ roots[str(root)] = time.strftime("%Y-%m-%dT%H:%M:%S")
80
+ rf.write_text(json.dumps(roots, indent=1))
81
+
82
+
83
+ def extract_files(paths, root):
84
+ """Extract and normalize source_file to root-relative. graphify stores paths
85
+ relative to the common ancestor of the batch (basename for single/same-dir
86
+ batches), so we resolve via that ancestor. Unattributable ('' semantic) nodes
87
+ are dropped — they'd break incremental dedup."""
88
+ from graphify.extract import extract
89
+ ok_nodes, ok_edges, failed = [], [], []
90
+ resolved = [p.resolve() for p in paths]
91
+ if not resolved:
92
+ return ok_nodes, ok_edges, failed
93
+ common = Path(os.path.commonpath([str(p) for p in resolved])) if len(resolved) > 1 else resolved[0].parent
94
+ if common.is_file():
95
+ common = common.parent
96
+
97
+ def norm(sf):
98
+ if not sf:
99
+ return None
100
+ try:
101
+ return str((common / sf).resolve().relative_to(root))
102
+ except ValueError:
103
+ return None
104
+
105
+ def collect(r):
106
+ for n in r["nodes"]:
107
+ sf = norm(n.get("source_file", ""))
108
+ if sf:
109
+ n["source_file"] = sf
110
+ ok_nodes.append(n)
111
+ for e in r["edges"]:
112
+ sf = norm(e.get("source_file", ""))
113
+ if sf:
114
+ e["source_file"] = sf
115
+ ok_edges.append(e)
116
+
117
+ try:
118
+ collect(extract(paths=resolved, parallel=len(resolved) > 4))
119
+ except Exception:
120
+ for p in resolved:
121
+ common = p.parent
122
+ try:
123
+ collect(extract(paths=[p], parallel=False))
124
+ except Exception:
125
+ failed.append(str(p))
126
+ return ok_nodes, ok_edges, failed
127
+
128
+
129
+ def build(root: Path, only_changed=False):
130
+ files = code_files(root)
131
+ graph, idx = load(root)
132
+ mtimes = {str(f.relative_to(root)): f.stat().st_mtime for f in files}
133
+ if only_changed and graph and idx:
134
+ old = idx.get("mtimes", {})
135
+ changed = [f for f in files if old.get(str(f.relative_to(root))) != f.stat().st_mtime]
136
+ deleted = set(old) - set(mtimes)
137
+ if not changed and not deleted:
138
+ return graph, idx, 0
139
+ drop = {str(f.relative_to(root)) for f in changed} | deleted
140
+ nodes = [n for n in graph["nodes"] if n.get("source_file", "") not in drop]
141
+ edges = [e for e in graph["edges"] if e.get("source_file", "") not in drop]
142
+ new_n, new_e, failed = extract_files(changed, root)
143
+ graph = {"nodes": nodes + new_n, "edges": edges + new_e}
144
+ n_processed = len(changed)
145
+ else:
146
+ new_n, new_e, failed = extract_files(files, root)
147
+ graph = {"nodes": new_n, "edges": new_e}
148
+ n_processed = len(files)
149
+ if failed:
150
+ print(f"WARNING: {len(failed)} files failed extraction: {failed[:5]}", file=sys.stderr)
151
+ idx = {"root": str(root), "mtimes": mtimes, "built": time.strftime("%Y-%m-%dT%H:%M:%S"),
152
+ "failed": failed}
153
+ save(root, graph, idx)
154
+ return graph, idx, n_processed
155
+
156
+
157
+ def ensure(root: Path):
158
+ graph, idx, n = build(root, only_changed=True)
159
+ if n:
160
+ print(f"[codegraph] refreshed {n} file(s)", file=sys.stderr)
161
+ return graph
162
+
163
+
164
+ def touch(target: Path, root: Path):
165
+ """Re-extract one file into its repo's cached graph. No-op when the repo
166
+ has no graph yet (hooks call this on every edit)."""
167
+ g, idx = load(root)
168
+ if not g:
169
+ return
170
+ f = str(target.relative_to(root))
171
+ nodes = [n for n in g["nodes"] if n.get("source_file", "") != f]
172
+ edges = [e for e in g["edges"] if e.get("source_file", "") != f]
173
+ if target.exists() and target.suffix in CODE_EXTS:
174
+ nn, ne, _ = extract_files([target], root)
175
+ nodes += nn
176
+ edges += ne
177
+ if target.exists():
178
+ idx["mtimes"][f] = target.stat().st_mtime
179
+ else:
180
+ idx["mtimes"].pop(f, None)
181
+ save(root, {"nodes": nodes, "edges": edges}, idx)
@@ -0,0 +1,84 @@
1
+ """MCP server exposing codegraph queries as tools, for agents that speak MCP
2
+ instead of shelling out. Requires the `mcp` extra: pip install codegraph-kit[mcp]
3
+
4
+ Run: codegraph mcp (stdio transport)
5
+ """
6
+ from pathlib import Path
7
+
8
+ from . import core, queries
9
+
10
+ try:
11
+ from mcp.server.fastmcp import FastMCP
12
+ except ImportError as e: # pragma: no cover
13
+ raise SystemExit(
14
+ "MCP support needs the optional dependency: pip install codegraph-kit[mcp]"
15
+ ) from e
16
+
17
+ server = FastMCP(
18
+ "codegraph",
19
+ instructions=(
20
+ "Cached tree-sitter code graphs. Query structure (outlines, symbols, "
21
+ "call edges, imports) instead of reading whole files; then read only "
22
+ "the located line ranges."
23
+ ),
24
+ )
25
+
26
+
27
+ def _root(directory: str) -> Path:
28
+ return core.find_root(Path(directory).resolve())
29
+
30
+
31
+ @server.tool()
32
+ def graph_map(directory: str) -> str:
33
+ """Repo overview: node/edge counts, per-directory breakdown, top hub symbols."""
34
+ root = _root(directory)
35
+ return queries.q_map(root, core.ensure(root))
36
+
37
+
38
+ @server.tool()
39
+ def file_outline(path: str) -> str:
40
+ """Outline of one source file: definitions with line ranges."""
41
+ target = Path(path).resolve()
42
+ root = core.find_root(target)
43
+ return queries.q_file(root, core.ensure(root), target)
44
+
45
+
46
+ @server.tool()
47
+ def find_symbol(name: str, directory: str = ".") -> str:
48
+ """Locate a symbol by substring -> file:lines."""
49
+ root = _root(directory)
50
+ return queries.q_sym(root, core.ensure(root), name)
51
+
52
+
53
+ @server.tool()
54
+ def callers(name: str, directory: str = ".") -> str:
55
+ """Who calls this symbol (matched by substring)."""
56
+ root = _root(directory)
57
+ return queries.q_calls(root, core.ensure(root), name, "callers")
58
+
59
+
60
+ @server.tool()
61
+ def callees(name: str, directory: str = ".") -> str:
62
+ """What this symbol calls (matched by substring)."""
63
+ root = _root(directory)
64
+ return queries.q_calls(root, core.ensure(root), name, "callees")
65
+
66
+
67
+ @server.tool()
68
+ def file_deps(path: str) -> str:
69
+ """Imports of a source file."""
70
+ target = Path(path).resolve()
71
+ root = core.find_root(target)
72
+ return queries.q_deps(root, core.ensure(root), target)
73
+
74
+
75
+ @server.tool()
76
+ def build_graph(directory: str) -> str:
77
+ """Full (re)build of a repo's graph. Run once per repo; queries refresh incrementally."""
78
+ root = _root(directory)
79
+ g, _idx, n = core.build(root)
80
+ return f"built {root}: {n} files -> {len(g['nodes'])} nodes, {len(g['edges'])} edges"
81
+
82
+
83
+ def run():
84
+ server.run()
@@ -0,0 +1,81 @@
1
+ """Query functions over a built graph. Each returns a plain string ready to
2
+ show to a human or an agent — the CLI prints it, the MCP server returns it."""
3
+ from collections import Counter
4
+ from pathlib import Path
5
+
6
+ from . import core
7
+
8
+
9
+ def loc(n):
10
+ return f"{n.get('source_file', '?')}:{n.get('source_location', '?')}"
11
+
12
+
13
+ def fmt_node(n):
14
+ return f"{n.get('label', n['id'])} [{n.get('type', n.get('_origin', '?'))}] {loc(n)}"
15
+
16
+
17
+ def fresh_graph(root: Path):
18
+ return core.ensure(root)
19
+
20
+
21
+ def q_map(root: Path, g) -> str:
22
+ nodes, edges = g["nodes"], g["edges"]
23
+ deg = Counter()
24
+ for e in edges:
25
+ deg[e["source"]] += 1
26
+ deg[e["target"]] += 1
27
+ byid = {n["id"]: n for n in nodes}
28
+ per_dir = Counter(n.get("source_file", "?").split("/")[0] for n in nodes)
29
+ rels = Counter(e.get("relation", "?") for e in edges)
30
+ lines = [f"{root} — {len(nodes)} nodes, {len(edges)} edges",
31
+ f"per-dir: {dict(per_dir.most_common(12))}",
32
+ f"relations: {dict(rels.most_common())}",
33
+ "top hubs:"]
34
+ for nid, d in deg.most_common(15):
35
+ n = byid.get(nid)
36
+ if n:
37
+ lines.append(f" {d:4d} {fmt_node(n)}")
38
+ return "\n".join(lines)
39
+
40
+
41
+ def q_file(root: Path, g, target: Path) -> str:
42
+ f = str(target.resolve().relative_to(root))
43
+ mine = [n for n in g["nodes"] if n.get("source_file", "") == f]
44
+ if not mine:
45
+ return f"(no graph entries for {f} — unsupported language or empty; read it directly)"
46
+ return "\n".join(fmt_node(n) for n in sorted(mine, key=lambda x: x.get("source_location", "")))
47
+
48
+
49
+ def q_sym(root: Path, g, query: str, limit: int = 30) -> str:
50
+ q = query.lower()
51
+ hits = [n for n in g["nodes"] if q in n.get("label", "").lower() or q in n["id"].lower()]
52
+ if not hits:
53
+ return f"no symbol matching '{query}' — try grep"
54
+ lines = [fmt_node(n) for n in hits[:limit]]
55
+ if len(hits) > limit:
56
+ lines.append(f"... {len(hits) - limit} more (narrow the query)")
57
+ return "\n".join(lines)
58
+
59
+
60
+ def q_calls(root: Path, g, query: str, direction: str, limit: int = 40) -> str:
61
+ """direction: 'callers' (who calls it) or 'callees' (what it calls)."""
62
+ q = query.lower()
63
+ byid = {n["id"]: n for n in g["nodes"]}
64
+ key, other = ("target", "source") if direction == "callers" else ("source", "target")
65
+ hits = [e for e in g["edges"] if e.get("relation") == "calls" and q in e[key].lower()]
66
+ if not hits:
67
+ return (f"no call edges matching '{query}' "
68
+ "(dynamic dispatch isn't captured — fall back to grep)")
69
+ lines = []
70
+ for e in hits[:limit]:
71
+ n = byid.get(e[other])
72
+ lines.append(fmt_node(n) if n else
73
+ f"{e[other]} ({e.get('source_file', '?')}:{e.get('source_location', '?')})")
74
+ return "\n".join(lines)
75
+
76
+
77
+ def q_deps(root: Path, g, target: Path) -> str:
78
+ f = str(target.resolve().relative_to(root))
79
+ lines = [f"{e['relation']:13s} {e['target']}" for e in g["edges"]
80
+ if e.get("source_file", "") == f and e.get("relation") in ("imports", "imports_from")]
81
+ return "\n".join(lines) if lines else f"(no import edges recorded for {f})"
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: codegraph-kit
3
+ Version: 0.1.0
4
+ Summary: Cached, incremental code-graph maps so AI agents query code structure instead of reading whole files. Works with any agent: CLI for Claude Code/Codex/Cursor/Aider, optional MCP server.
5
+ Project-URL: Homepage, https://github.com/nguyenminhduc9988/codegraph
6
+ Project-URL: Repository, https://github.com/nguyenminhduc9988/codegraph
7
+ Project-URL: Issues, https://github.com/nguyenminhduc9988/codegraph/issues
8
+ Author: Duc Nguyen
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agents,claude-code,code-graph,codebase-map,mcp,static-analysis,token-efficiency,tree-sitter
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: graphifyy>=0.9
24
+ Provides-Extra: dev
25
+ Requires-Dist: build; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Requires-Dist: twine; extra == 'dev'
28
+ Provides-Extra: mcp
29
+ Requires-Dist: mcp>=1.0; extra == 'mcp'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # codegraph
33
+
34
+ **Cached, incremental code-graph maps so AI agents query code structure instead of reading whole files.**
35
+
36
+ Agents burn most of their tokens reading source files to answer structural questions — "where is this defined?", "who calls this?", "what does this file import?". `codegraph` answers those questions from a cached tree-sitter AST graph in milliseconds, so the agent reads only the exact line ranges it needs.
37
+
38
+ ```
39
+ $ codegraph sym cli_fallback
40
+ CLIFallback [class] agent/cli_fallback.py:24-210
41
+ run_cascade [function] agent/cli_fallback.py:96-158
42
+
43
+ $ codegraph callers run_cascade
44
+ handle_turn [function] agent/loop.py:311-360
45
+ retry_turn [function] agent/loop.py:402-431
46
+ ```
47
+
48
+ One `build` per repo; after that, every query auto-refreshes only the files that changed since the last call (mtime-based). No daemon, no database, no API keys — a JSON cache under `~/.cache/codegraph`.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install codegraph-kit # CLI
54
+ pip install "codegraph-kit[mcp]" # CLI + MCP server
55
+ ```
56
+
57
+ Python ≥ 3.10. Parsing is done by [graphify](https://pypi.org/project/graphifyy/) (tree-sitter), which ships wheels for Python, JavaScript, TypeScript, Go, Rust, Java, Ruby, C, C++, C#, PHP, Swift, Kotlin, shell, and more.
58
+
59
+ ## Commands
60
+
61
+ | Command | What it answers |
62
+ |---|---|
63
+ | `codegraph build [dir]` | full graph build (run once per repo) |
64
+ | `codegraph map [dir]` | repo overview: size, per-directory breakdown, top hub symbols |
65
+ | `codegraph file <path>` | outline of one file: definitions + line ranges |
66
+ | `codegraph sym <name>` | where is this symbol defined? |
67
+ | `codegraph callers <name>` | who calls it? |
68
+ | `codegraph callees <name>` | what does it call? |
69
+ | `codegraph deps <path>` | what does this file import? |
70
+ | `codegraph ensure [dir]` | incremental refresh (queries do this automatically) |
71
+ | `codegraph touch <path>` | re-extract one file (for editor/agent hooks) |
72
+ | `codegraph agent` | print an instruction snippet for your agent's context file |
73
+ | `codegraph mcp` | run as an MCP server (stdio) |
74
+
75
+ ## Integrate with any agent
76
+
77
+ `codegraph` is plain CLI-over-stdout, so **any agent that can run shell commands can use it** — Claude Code, Codex CLI, Cursor, Aider, OpenHands, Goose, custom agents. Two steps:
78
+
79
+ **1. Tell the agent the graph exists.** Append the ready-made snippet to your agent's context file:
80
+
81
+ ```bash
82
+ codegraph agent >> AGENTS.md # or CLAUDE.md, .cursorrules, .github/copilot-instructions.md
83
+ ```
84
+
85
+ **2. (Optional) Keep the graph fresh on every edit.** For Claude Code, install the bundled PostToolUse hook so each `Edit`/`Write` re-extracts just that file:
86
+
87
+ ```bash
88
+ cp integrations/claude-code/codegraph-touch.sh ~/.claude/hooks/
89
+ chmod +x ~/.claude/hooks/codegraph-touch.sh
90
+ # then merge integrations/claude-code/settings-snippet.json into ~/.claude/settings.json
91
+ ```
92
+
93
+ Even without a hook, queries stay correct: every query runs an mtime check first and re-extracts anything stale.
94
+
95
+ ### MCP (for agents that don't shell out)
96
+
97
+ ```bash
98
+ pip install "codegraph-kit[mcp]"
99
+ ```
100
+
101
+ Register `codegraph mcp` as a stdio server. For Claude Code:
102
+
103
+ ```bash
104
+ claude mcp add codegraph -- codegraph mcp
105
+ ```
106
+
107
+ Tools exposed: `build_graph`, `graph_map`, `file_outline`, `find_symbol`, `callers`, `callees`, `file_deps` — same output as the CLI.
108
+
109
+ ## Why not just let the agent read files?
110
+
111
+ Reading a 1,500-line file to find one function costs ~15k tokens; `codegraph file` returns the outline in ~200 tokens, and the agent then reads only the 40-line range it needs. On large repos the difference compounds — structural questions (symbol lookup, call tracing, import mapping) stop costing file-reads entirely.
112
+
113
+ Honest limitations, printed in the output when they apply:
114
+
115
+ - **Dynamic dispatch isn't captured** — call edges come from static AST analysis; `getattr`-style calls need grep.
116
+ - **Unsupported/exotic languages** fall back to "read it directly".
117
+ - Caps: 5,000 files per repo, 1 MB per file (warned, not silent).
118
+
119
+ ## How it works
120
+
121
+ 1. `build` walks the repo (skipping `node_modules`, `venv`, `dist`, …), runs tree-sitter extraction via graphify, normalizes all paths root-relative, and writes `graph.json` + an mtime index to `~/.cache/codegraph/<repo-hash>/`.
122
+ 2. Every query calls `ensure` first: files whose mtime changed are re-extracted and spliced into the graph; deleted files are dropped. Typical refresh is a handful of files, so queries stay fast.
123
+ 3. Output is deliberately plain text with `file:line` locations — clickable in most agent UIs and trivially parseable.
124
+
125
+ Set `CODEGRAPH_CACHE` to relocate the cache (useful in CI and sandboxes).
126
+
127
+ ## License
128
+
129
+ MIT
@@ -0,0 +1,10 @@
1
+ codegraph_kit/__init__.py,sha256=hvT298W9v1mmnYe6HKgi4U1MfRNt6u7-aBa4nW2Th4k,140
2
+ codegraph_kit/cli.py,sha256=Q70kSZp5LLigQVrukoWg6bRDN7ixJQzeQ7cPxkadUOU,4042
3
+ codegraph_kit/core.py,sha256=QrGDrZc32ODJt6HFj4TedxrGJkwJcvyc7Br7wnEKqW8,6359
4
+ codegraph_kit/mcp_server.py,sha256=mICw1h5P9oiwEg1LWYTfi7j8Y8ow9zbtFd_cryc1bVE,2494
5
+ codegraph_kit/queries.py,sha256=_1E6KhyBhfxE1-BVxwIBbthSFFoeBoDKcn9OT97WS98,3164
6
+ codegraph_kit-0.1.0.dist-info/METADATA,sha256=LCFlHepudvDuCNLE-vL-29JMj8GH9j7T37TEwSZjeNY,6071
7
+ codegraph_kit-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ codegraph_kit-0.1.0.dist-info/entry_points.txt,sha256=0h9B03fi5q4ApHocwXm6W6fbzwZno_Z7cf-W6HW-PSE,53
9
+ codegraph_kit-0.1.0.dist-info/licenses/LICENSE,sha256=bIcrUqZy4o04kz-4Awxz8HWXHjdwL0YlBZ-GhsuQqgk,1067
10
+ codegraph_kit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ codegraph = codegraph_kit.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Duc Nguyen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.