context-mcp-server 1.0.5 → 1.0.7
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.
- package/README.md +99 -359
- package/codegraph/server.py +37 -46
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/src/assests/main.png +0 -0
- package/src/cli.js +35 -8
- package/src/db.js +28 -9
- package/src/hooks/autoContext.js +1 -1
- package/src/templates/CLAUDE.md +8 -3
- package/src/templates/commands/context-resume.md +3 -1
- package/src/templates/skills/SKILL.md +9 -7
- package/src/tools/codegraph.js +12 -78
- package/src/tools/context.js +29 -9
- package/src/tools/gitTools.js +3 -4
- package/uv.lock +1 -1
- package/codegraph/extractors/audio_extractor.py +0 -8
- package/codegraph/extractors/doc_extractor.py +0 -34
- package/codegraph/extractors/image_extractor.py +0 -26
package/codegraph/server.py
CHANGED
|
@@ -4,8 +4,7 @@ codegraph/server.py — MCP server exposing codebase knowledge graph tools.
|
|
|
4
4
|
|
|
5
5
|
Tools:
|
|
6
6
|
codegraph_build — scan project, extract AST nodes, build graph (local only, no API)
|
|
7
|
-
codegraph_query —
|
|
8
|
-
codegraph_explain — look up a specific node: type, file, connections
|
|
7
|
+
codegraph_query — structural question OR single-node lookup (or both); replaces codegraph_explain
|
|
9
8
|
codegraph_report — return full CODEGRAPH_REPORT.md
|
|
10
9
|
codegraph_nodes — list nodes of a given type
|
|
11
10
|
codegraph_path — shortest path between two concepts
|
|
@@ -56,36 +55,21 @@ TOOLS = [
|
|
|
56
55
|
Tool(
|
|
57
56
|
name="codegraph_query",
|
|
58
57
|
description=(
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
58
|
+
"Ask a structural question about the codebase OR look up a specific node by name — or both in one call. "
|
|
59
|
+
"Pass `question` for natural-language traversal: what calls X, what does module Y depend on. "
|
|
60
|
+
"Pass `node` for fast single-node lookup: returns type, file, depends_on, used_by. "
|
|
61
|
+
"Pass both to get node detail + surrounding graph context together. "
|
|
62
|
+
"Returns structured text within token_budget. Use before reading any files."
|
|
64
63
|
),
|
|
65
64
|
inputSchema={
|
|
66
65
|
"type": "object",
|
|
67
66
|
"properties": {
|
|
68
67
|
"path": {"type": "string", "description": "Project root"},
|
|
69
|
-
"question": {"type": "string", "description": "Natural language question"},
|
|
68
|
+
"question": {"type": "string", "description": "Natural language question about the codebase"},
|
|
69
|
+
"node": {"type": "string", "description": "Node name or partial name to look up (type, file, deps, callers)"},
|
|
70
70
|
"token_budget": {"type": "integer", "description": "Max tokens in response (default 2000)"},
|
|
71
71
|
},
|
|
72
|
-
"required": ["path"
|
|
73
|
-
},
|
|
74
|
-
),
|
|
75
|
-
Tool(
|
|
76
|
-
name="codegraph_explain",
|
|
77
|
-
description=(
|
|
78
|
-
"Look up a specific function, class, or module by name — returns its type, file location, "
|
|
79
|
-
"and all direct connections (what it depends on, what uses it). "
|
|
80
|
-
"Use when you already know the name and want its full context in the graph."
|
|
81
|
-
),
|
|
82
|
-
inputSchema={
|
|
83
|
-
"type": "object",
|
|
84
|
-
"properties": {
|
|
85
|
-
"path": {"type": "string", "description": "Project root"},
|
|
86
|
-
"node": {"type": "string", "description": "Node name or partial name"},
|
|
87
|
-
},
|
|
88
|
-
"required": ["path", "node"],
|
|
72
|
+
"required": ["path"],
|
|
89
73
|
},
|
|
90
74
|
),
|
|
91
75
|
Tool(
|
|
@@ -143,7 +127,7 @@ async def call_tool(name: str, arguments: dict):
|
|
|
143
127
|
async def _dispatch(name: str, args: dict):
|
|
144
128
|
if name == "codegraph_build": return await _build(args)
|
|
145
129
|
if name == "codegraph_query": return await _query(args)
|
|
146
|
-
if name == "codegraph_explain": return await
|
|
130
|
+
if name == "codegraph_explain": return await _query(args)
|
|
147
131
|
if name == "codegraph_report": return await _report(args)
|
|
148
132
|
if name == "codegraph_nodes": return await _nodes(args)
|
|
149
133
|
if name == "codegraph_path": return await _path(args)
|
|
@@ -223,19 +207,8 @@ async def _build(args: dict) -> dict:
|
|
|
223
207
|
|
|
224
208
|
# ── Query / Report / Nodes / Path ─────────────────────────────────────────────
|
|
225
209
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if not graph_dict:
|
|
229
|
-
raise ValueError("No graph found. Run codegraph_build first.")
|
|
230
|
-
return graph_answer(args["question"], graph_dict, token_budget=args.get("token_budget", 2000))
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
async def _explain(args: dict) -> dict:
|
|
234
|
-
graph_dict = load_graph(args["path"])
|
|
235
|
-
if not graph_dict:
|
|
236
|
-
raise ValueError("No graph found. Run codegraph_build first.")
|
|
237
|
-
|
|
238
|
-
query = args["node"].lower()
|
|
210
|
+
def _explain_node(node_name: str, graph_dict: dict) -> dict:
|
|
211
|
+
query = node_name.lower()
|
|
239
212
|
nodes = graph_dict.get("nodes", [])
|
|
240
213
|
edges = graph_dict.get("edges", [])
|
|
241
214
|
|
|
@@ -244,8 +217,8 @@ async def _explain(args: dict) -> dict:
|
|
|
244
217
|
match = next((n for n in nodes if query in n.get("name", "").lower()), None)
|
|
245
218
|
if not match:
|
|
246
219
|
candidates = [n["name"] for n in nodes if query in n.get("id", "").lower()]
|
|
247
|
-
return {"found": False, "query":
|
|
248
|
-
"message": f"No node matching '{
|
|
220
|
+
return {"found": False, "query": node_name,
|
|
221
|
+
"message": f"No node matching '{node_name}'.",
|
|
249
222
|
"suggestions": candidates[:10]}
|
|
250
223
|
|
|
251
224
|
nid = match["id"]
|
|
@@ -254,13 +227,13 @@ async def _explain(args: dict) -> dict:
|
|
|
254
227
|
if e.get("from") == nid:
|
|
255
228
|
t = next((n for n in nodes if n.get("id") == e.get("to")), None)
|
|
256
229
|
depends_on.append({"name": t["name"] if t else e["to"],
|
|
257
|
-
"file": t.get("file","") if t else "",
|
|
258
|
-
"relation": e.get("relation","→")})
|
|
230
|
+
"file": t.get("file", "") if t else "",
|
|
231
|
+
"relation": e.get("relation", "→")})
|
|
259
232
|
elif e.get("to") == nid:
|
|
260
233
|
s = next((n for n in nodes if n.get("id") == e.get("from")), None)
|
|
261
234
|
used_by.append({"name": s["name"] if s else e["from"],
|
|
262
|
-
"file": s.get("file","") if s else "",
|
|
263
|
-
"relation": e.get("relation","→")})
|
|
235
|
+
"file": s.get("file", "") if s else "",
|
|
236
|
+
"relation": e.get("relation", "→")})
|
|
264
237
|
|
|
265
238
|
return {
|
|
266
239
|
"found": True,
|
|
@@ -270,10 +243,28 @@ async def _explain(args: dict) -> dict:
|
|
|
270
243
|
"description": match.get("description") or None,
|
|
271
244
|
"depends_on": depends_on[:20],
|
|
272
245
|
"used_by": used_by[:20],
|
|
273
|
-
"hint": None,
|
|
274
246
|
}
|
|
275
247
|
|
|
276
248
|
|
|
249
|
+
async def _query(args: dict) -> dict:
|
|
250
|
+
graph_dict = load_graph(args["path"])
|
|
251
|
+
if not graph_dict:
|
|
252
|
+
raise ValueError("No graph found. Run codegraph_build first.")
|
|
253
|
+
|
|
254
|
+
question = args.get("question")
|
|
255
|
+
node_name = args.get("node")
|
|
256
|
+
|
|
257
|
+
if not question and not node_name:
|
|
258
|
+
raise ValueError("Provide at least one of: question, node")
|
|
259
|
+
|
|
260
|
+
result = {}
|
|
261
|
+
if node_name:
|
|
262
|
+
result["node"] = _explain_node(node_name, graph_dict)
|
|
263
|
+
if question:
|
|
264
|
+
result["query"] = graph_answer(question, graph_dict, token_budget=args.get("token_budget", 2000))
|
|
265
|
+
return result
|
|
266
|
+
|
|
267
|
+
|
|
277
268
|
async def _report(args: dict) -> dict:
|
|
278
269
|
report_path = Path(args["path"]) / "codegraph-cache" / "CODEGRAPH_REPORT.md"
|
|
279
270
|
if report_path.exists():
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "Persistent AI memory + codebase knowledge graph MCP server. Works across Claude Code, Cursor, Gemini CLI, Codex, Windsurf, VS Code Copilot, Claude.ai, and ChatGPT.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"context-mcp": "./src/index.js",
|
|
8
8
|
"context-mcp-server": "./src/index.js",
|
|
9
9
|
"context-mcp-http": "./src/http.js",
|
|
10
|
-
"ctx": "./src/cli.js"
|
|
10
|
+
"ctx": "./src/cli.js",
|
|
11
|
+
"context": "./src/cli.js"
|
|
11
12
|
},
|
|
12
13
|
"scripts": {
|
|
13
14
|
"mcp": "node src/index.js",
|
package/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codegraph-mcp"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.7"
|
|
8
8
|
description = "Codebase knowledge graph MCP server — AST extraction, graph queries, community detection"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
Binary file
|
package/src/cli.js
CHANGED
|
@@ -107,8 +107,8 @@ function printSection(title, meta = '') {
|
|
|
107
107
|
function printUsage() {
|
|
108
108
|
printBanner();
|
|
109
109
|
|
|
110
|
-
// Terminal commands (ctx ...)
|
|
111
|
-
printSection('Terminal commands', 'run from your shell');
|
|
110
|
+
// Terminal commands (ctx / context ...)
|
|
111
|
+
printSection('Terminal commands', 'run from your shell (ctx … or context …)');
|
|
112
112
|
const cmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
|
|
113
113
|
cmd('ctx', 'open interactive mode');
|
|
114
114
|
cmd('ctx list [project]', 'list entries + discussions + graphs');
|
|
@@ -131,8 +131,8 @@ function printUsage() {
|
|
|
131
131
|
cmd('ctx help', 'show this screen');
|
|
132
132
|
console.log('');
|
|
133
133
|
|
|
134
|
-
// Interactive mode commands (no
|
|
135
|
-
printSection('Interactive mode', 'type these inside
|
|
134
|
+
// Interactive mode commands (no prefix needed)
|
|
135
|
+
printSection('Interactive mode', 'type these inside the UI — no "ctx" prefix needed');
|
|
136
136
|
const icmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
|
|
137
137
|
icmd('list [project]', 'list entries');
|
|
138
138
|
icmd('search <query>', 'search context');
|
|
@@ -186,7 +186,7 @@ function cmdList(args) {
|
|
|
186
186
|
|
|
187
187
|
for (const projectName of projectNames) {
|
|
188
188
|
const pData = projects[projectName];
|
|
189
|
-
const graph = allGraphs
|
|
189
|
+
const graph = _graphForProject(allGraphs, projectName);
|
|
190
190
|
const activeD = pData.discussions.filter(d => d.status === 'active').length;
|
|
191
191
|
const totalSecs = (pData.contexts.length > 0 ? 1 : 0) + (pData.discussions.length > 0 ? 1 : 0) + (graph ? 1 : 0);
|
|
192
192
|
let secIdx = 0;
|
|
@@ -240,7 +240,7 @@ function cmdList(args) {
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
// Orphan graphs (no matching project)
|
|
243
|
-
const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => g
|
|
243
|
+
const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => _graphForProject([g], p)));
|
|
244
244
|
if (orphanGraphs.length) {
|
|
245
245
|
console.log(`\n ${color(C.dblue, '◇')} ${muted('other graphs')}`);
|
|
246
246
|
for (const g of orphanGraphs) {
|
|
@@ -292,7 +292,7 @@ function cmdProjects() {
|
|
|
292
292
|
const entries = getContext({ project: project.name, limit: 3, compact: true }).filter(e => e.status !== 'archived');
|
|
293
293
|
const discs = allDiscs.filter(d => (d.project || 'global') === project.name);
|
|
294
294
|
const activeD = discs.filter(d => d.status === 'active');
|
|
295
|
-
const graph = graphs
|
|
295
|
+
const graph = _graphForProject(graphs, project.name);
|
|
296
296
|
|
|
297
297
|
const barLen = Math.min(Math.ceil(project.count / 2), 24);
|
|
298
298
|
const bar = color(C.dblue, '█'.repeat(barLen)) + color(C.darkgray, '░'.repeat(24 - barLen));
|
|
@@ -429,7 +429,7 @@ function cmdBenchmark() {
|
|
|
429
429
|
printSection('Benchmark', 'real token savings');
|
|
430
430
|
|
|
431
431
|
const RESUME_LIMIT = 15;
|
|
432
|
-
const COMPACT_AT =
|
|
432
|
+
const COMPACT_AT = 20;
|
|
433
433
|
|
|
434
434
|
// ── Measure entry sizes from actual stored data ──────────────────────────────
|
|
435
435
|
const allEntries = getContext({ limit: 500, compact: false });
|
|
@@ -567,6 +567,30 @@ const _GLOBAL_GITIGNORE_ENTRIES = [
|
|
|
567
567
|
'.mcp.json',
|
|
568
568
|
];
|
|
569
569
|
|
|
570
|
+
// Match a graph to a project by exact last-path-component comparison (not substring)
|
|
571
|
+
function _graphForProject(graphs, projectName) {
|
|
572
|
+
const norm = p => (p || '').toLowerCase().replace(/\\/g, '/').replace(/\/$/, '');
|
|
573
|
+
const name = projectName.toLowerCase();
|
|
574
|
+
return graphs.find(g => norm(g.path).split('/').pop() === name) || null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const _PROJECT_GITIGNORE_ENTRIES = [
|
|
578
|
+
'.claude/', '.cursor/', '.vscode/', '.gemini/', '.codex/',
|
|
579
|
+
'codegraph-cache/', '.mcp.json', 'CLAUDE.md', 'GEMINI.md', 'AGENTS.md',
|
|
580
|
+
];
|
|
581
|
+
|
|
582
|
+
function _updateProjectGitignore(projectDir) {
|
|
583
|
+
const giPath = join(projectDir, '.gitignore');
|
|
584
|
+
const existing = existsSync(giPath) ? readFileSync(giPath, 'utf8') : '';
|
|
585
|
+
const lines = existing.split(/\r?\n/);
|
|
586
|
+
const missing = _PROJECT_GITIGNORE_ENTRIES.filter(e => !lines.includes(e));
|
|
587
|
+
if (!missing.length) return;
|
|
588
|
+
const block = '\n# context-mcp — written by ctx install\n' + missing.join('\n') + '\n';
|
|
589
|
+
writeFileSync(giPath, (existing ? existing.trimEnd() : '') + block, 'utf8');
|
|
590
|
+
console.log(` ${ok('✓')} ${'project .gitignore'.padEnd(28)} ${faint(giPath.replace(/\\/g, '/'))}`);
|
|
591
|
+
for (const e of missing) console.log(` ${faint('+ ' + e)}`);
|
|
592
|
+
}
|
|
593
|
+
|
|
570
594
|
function _updateGlobalGitignore() {
|
|
571
595
|
// Resolve global gitignore path: git config > ~/.gitignore_global > ~/.gitignore
|
|
572
596
|
let giPath = null;
|
|
@@ -829,6 +853,9 @@ async function cmdInstall(args) {
|
|
|
829
853
|
console.log(faint(` ${keys.length} platform(s) installed · scope: ${scope} · ${destLabel}`));
|
|
830
854
|
console.log('');
|
|
831
855
|
|
|
856
|
+
// ── Project .gitignore — add context-mcp entries for this project ──────────
|
|
857
|
+
_updateProjectGitignore(process.cwd());
|
|
858
|
+
|
|
832
859
|
// ── Global gitignore — add context-mcp runtime files if global gitignore exists ──
|
|
833
860
|
_updateGlobalGitignore();
|
|
834
861
|
console.log('');
|
package/src/db.js
CHANGED
|
@@ -23,6 +23,11 @@ const PROJECTS_PATH = join(DATA_DIR, 'projects.json');
|
|
|
23
23
|
|
|
24
24
|
const MAX_CONTENT_LENGTH = 5000;
|
|
25
25
|
const PREVIEW_LENGTH = 200;
|
|
26
|
+
|
|
27
|
+
// Normalize file paths for cross-platform comparison (Windows case + slash variants)
|
|
28
|
+
function normPath(p) {
|
|
29
|
+
return p ? p.toLowerCase().replace(/\\/g, '/').replace(/\/$/, '') : '';
|
|
30
|
+
}
|
|
26
31
|
const WRITE_DEBOUNCE_MS = 500;
|
|
27
32
|
const LOCK_WAIT_TIMEOUT_MS = 2000;
|
|
28
33
|
|
|
@@ -126,9 +131,9 @@ function mergeStore(latest, local) {
|
|
|
126
131
|
if (_changedDiscussionNames.has(disc.name)) discussionsByName.set(disc.name, disc);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
const graphsByPath = new Map((latest.graphs || []).map(g => [g.path, g]));
|
|
134
|
+
const graphsByPath = new Map((latest.graphs || []).map(g => [normPath(g.path), g]));
|
|
130
135
|
for (const graph of (local.graphs || [])) {
|
|
131
|
-
if (_changedGraphPaths.has(graph.path)) graphsByPath.set(graph.path, graph);
|
|
136
|
+
if (_changedGraphPaths.has(graph.path)) graphsByPath.set(normPath(graph.path), graph);
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
const projectsById = new Map((latest.projects || []).map(p => [p.id, p]));
|
|
@@ -307,10 +312,15 @@ export function updateContext({ id, content, title, tags, type, status, files, c
|
|
|
307
312
|
* @param {Object} opts
|
|
308
313
|
* @param {boolean} opts.compact - If true, returns previews instead of full content (saves tokens)
|
|
309
314
|
*/
|
|
310
|
-
export function getContext({ project, tags, limit = 20, compact = false } = {}) {
|
|
315
|
+
export function getContext({ project, tags, limit = 20, compact = false, ids } = {}) {
|
|
311
316
|
refreshFromDisk();
|
|
312
317
|
const store = load();
|
|
313
318
|
let results = store.contexts;
|
|
319
|
+
if (ids && ids.length) {
|
|
320
|
+
const idSet = new Set(ids);
|
|
321
|
+
results = results.filter(c => idSet.has(c.id));
|
|
322
|
+
return compact ? results.map(compactEntry) : results;
|
|
323
|
+
}
|
|
314
324
|
if (project) results = results.filter(c => c.project === project || c.project === 'global');
|
|
315
325
|
if (tags && tags.length) {
|
|
316
326
|
const tagList = Array.isArray(tags) ? tags : tags.split(',').map(t => t.trim());
|
|
@@ -343,12 +353,14 @@ export function searchContext({ query, project, limit = 10, compact = false }) {
|
|
|
343
353
|
return compact ? sliced.map(compactEntry) : sliced;
|
|
344
354
|
}
|
|
345
355
|
|
|
346
|
-
export function deleteContext({ id }) {
|
|
356
|
+
export function deleteContext({ id, ids }) {
|
|
347
357
|
refreshFromDisk();
|
|
348
358
|
const store = load();
|
|
349
359
|
const before = store.contexts.length;
|
|
350
|
-
const
|
|
351
|
-
|
|
360
|
+
const idSet = new Set(ids && ids.length ? ids : (id ? [id] : []));
|
|
361
|
+
if (!idSet.size) return { deleted: 0 };
|
|
362
|
+
const removed = store.contexts.filter(c => idSet.has(c.id));
|
|
363
|
+
store.contexts = store.contexts.filter(c => !idSet.has(c.id));
|
|
352
364
|
if (store.contexts.length < before) {
|
|
353
365
|
for (const entry of removed) {
|
|
354
366
|
_deletedContextIds.add(entry.id);
|
|
@@ -713,7 +725,7 @@ export function flushStore() { flushToDisk(); }
|
|
|
713
725
|
|
|
714
726
|
// ── Auto-compaction ───────────────────────────────────────────────────────────
|
|
715
727
|
|
|
716
|
-
const COMPACTION_THRESHOLD =
|
|
728
|
+
const COMPACTION_THRESHOLD = 20;
|
|
717
729
|
const COMPACTION_TARGET = 30;
|
|
718
730
|
|
|
719
731
|
export function shouldCompact(project) {
|
|
@@ -753,7 +765,14 @@ export function compactProject(project, summaryContent) {
|
|
|
753
765
|
export function saveGraph({ path, nodes, edges, communities, cached, changed, time_ms, summary }) {
|
|
754
766
|
refreshFromDisk();
|
|
755
767
|
const store = load();
|
|
756
|
-
|
|
768
|
+
// Deduplicate: collapse any case/slash variants of same path, keep newest
|
|
769
|
+
const dupes = store.graphs.filter(g => normPath(g.path) === normPath(path));
|
|
770
|
+
if (dupes.length > 1) {
|
|
771
|
+
const keep = dupes.reduce((a, b) => (a.builtAt >= b.builtAt ? a : b));
|
|
772
|
+
store.graphs = store.graphs.filter(g => normPath(g.path) !== normPath(path));
|
|
773
|
+
store.graphs.push(keep);
|
|
774
|
+
}
|
|
775
|
+
const existing = store.graphs.find(g => normPath(g.path) === normPath(path));
|
|
757
776
|
const record = {
|
|
758
777
|
path,
|
|
759
778
|
nodes: nodes ?? existing?.nodes ?? 0,
|
|
@@ -777,7 +796,7 @@ export function saveGraph({ path, nodes, edges, communities, cached, changed, ti
|
|
|
777
796
|
|
|
778
797
|
export function getGraph(path) {
|
|
779
798
|
const store = load();
|
|
780
|
-
if (path) return store.graphs.find(g => g.path === path) || null;
|
|
799
|
+
if (path) return store.graphs.find(g => normPath(g.path) === normPath(path)) || null;
|
|
781
800
|
return store.graphs;
|
|
782
801
|
}
|
|
783
802
|
|
package/src/hooks/autoContext.js
CHANGED
|
@@ -3,7 +3,7 @@ import { fireAutoLink } from './autoLink.js';
|
|
|
3
3
|
|
|
4
4
|
export function saveAutoContext({ title, content, type, files, state, tags = [] }) {
|
|
5
5
|
const entry = saveContext({
|
|
6
|
-
project: state.sessionProject ||
|
|
6
|
+
project: state.sessionProject || null,
|
|
7
7
|
sessionId: state.sessionId || null,
|
|
8
8
|
title,
|
|
9
9
|
content,
|
package/src/templates/CLAUDE.md
CHANGED
|
@@ -15,7 +15,12 @@ Every conversation starts with `context.resume`. Every codebase question uses `c
|
|
|
15
15
|
|
|
16
16
|
## 1. Start of Every Conversation (MANDATORY)
|
|
17
17
|
|
|
18
|
-
Call `context` tool
|
|
18
|
+
Call `context` tool **before anything else** with:
|
|
19
|
+
- `action: "resume"`
|
|
20
|
+
- `project: "<basename of git repo root dir>"` — infer from cwd if not stated
|
|
21
|
+
- `rootPath: "<absolute path to git repo root>"` — required for sandbox + graph lookup
|
|
22
|
+
|
|
23
|
+
Both fields are required: `project` names the memory bucket, `rootPath` enables exact graph matching and file sandboxing.
|
|
19
24
|
|
|
20
25
|
Returns:
|
|
21
26
|
- `recentEntries` — decisions, bugs, notes from previous conversations
|
|
@@ -34,7 +39,7 @@ Then:
|
|
|
34
39
|
|
|
35
40
|
**After graph build or rebuild** — every time `codegraph_build` completes:
|
|
36
41
|
```
|
|
37
|
-
context.save type: "architecture" title: "ContextGraph built — <project>"
|
|
42
|
+
context.save project: "<project>" type: "architecture" title: "ContextGraph built — <project>"
|
|
38
43
|
content: "nodes: X | edges: Y | communities: Z"
|
|
39
44
|
```
|
|
40
45
|
|
|
@@ -59,7 +64,7 @@ Do NOT save: routine reads, search results, temporary debugging dead-ends.
|
|
|
59
64
|
|
|
60
65
|
Feature spans multiple sessions → `discussion.create` or `discussion.update`.
|
|
61
66
|
Need past info → `search` before asking user.
|
|
62
|
-
Always pass `project`. Auto-compact fires at >
|
|
67
|
+
Always pass `project`. Auto-compact fires at >20 entries.
|
|
63
68
|
|
|
64
69
|
---
|
|
65
70
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
Call the `context` MCP tool with `action: "resume"
|
|
1
|
+
Call the `context` MCP tool with `action: "resume"`, `project: "$ARGUMENTS"` (if no argument given, infer the project name from the current working directory name), and `rootPath: "<absolute path to the project root / git repo root>"`.
|
|
2
|
+
|
|
3
|
+
Both `project` and `rootPath` are required: `project` names the memory bucket, `rootPath` enables exact graph lookup and file sandboxing.
|
|
2
4
|
|
|
3
5
|
This loads:
|
|
4
6
|
- Recent decisions, bugs, and notes from past sessions
|
|
@@ -18,9 +18,12 @@ Persistent memory + codebase knowledge graph across every conversation.
|
|
|
18
18
|
|
|
19
19
|
## MANDATORY: Start of Every Conversation
|
|
20
20
|
|
|
21
|
-
Call `context` tool
|
|
21
|
+
Call `context` tool **before any tool or response** with:
|
|
22
|
+
- `action: "resume"`
|
|
23
|
+
- `project: "<basename of git repo root dir>"` — infer from `cwd` if not stated
|
|
24
|
+
- `rootPath: "<absolute path to git repo root>"` — required for sandbox + graph lookup
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
Both fields are required: `project` names the memory bucket, `rootPath` enables exact graph matching and file sandboxing.
|
|
24
27
|
|
|
25
28
|
Returns:
|
|
26
29
|
- `recentEntries` — decisions, bugs, notes from past sessions
|
|
@@ -40,7 +43,7 @@ Then:
|
|
|
40
43
|
**1. After graph build or rebuild**
|
|
41
44
|
Every time `codegraph_build` completes successfully, immediately call:
|
|
42
45
|
```
|
|
43
|
-
context.save type: "architecture" title: "ContextGraph built — <project>"
|
|
46
|
+
context.save project: "<project>" type: "architecture" title: "ContextGraph built — <project>"
|
|
44
47
|
content: "nodes: X | edges: Y | communities: Z | built: <timestamp>"
|
|
45
48
|
```
|
|
46
49
|
|
|
@@ -70,7 +73,7 @@ Do NOT save for: routine file reads, search results, explanations of existing co
|
|
|
70
73
|
| Deploy / release step discovered | `note` |
|
|
71
74
|
| Milestone / feature / task completed | `note` |
|
|
72
75
|
|
|
73
|
-
Always pass `project`. Feature spans multiple sessions → `discussion.create` or `discussion.update`. Need past info → `search` before asking user. Auto-compact fires at >
|
|
76
|
+
Always pass `project`. Feature spans multiple sessions → `discussion.create` or `discussion.update`. Need past info → `search` before asking user. Auto-compact fires at >20 entries.
|
|
74
77
|
|
|
75
78
|
---
|
|
76
79
|
|
|
@@ -86,9 +89,8 @@ Parses codebase into AST graph via tree-sitter. Extracts functions, classes, imp
|
|
|
86
89
|
|
|
87
90
|
### Query (free, instant, forever)
|
|
88
91
|
```
|
|
89
|
-
codegraph_query(path, question)
|
|
90
|
-
|
|
91
|
-
codegraph_path(path, from, to) → shortest path between two concepts
|
|
92
|
+
codegraph_query(path, question?, node?) → structural question OR single-node lookup (or both in one call)
|
|
93
|
+
codegraph_path(path, from, to) → shortest path between two concepts
|
|
92
94
|
codegraph_nodes(path, type) → list all nodes of a type
|
|
93
95
|
codegraph_report(path) → god nodes, clusters, surprises
|
|
94
96
|
```
|
package/src/tools/codegraph.js
CHANGED
|
@@ -40,92 +40,23 @@ export const definitions = [
|
|
|
40
40
|
required: ['path'],
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
|
-
{
|
|
44
|
-
name: 'codegraph_extract',
|
|
45
|
-
description:
|
|
46
|
-
'Return raw content of changed code and doc/PDF files so the AI can write descriptions. ' +
|
|
47
|
-
'Code files: lists existing AST nodes — AI writes a description for each. ' +
|
|
48
|
-
'Doc files: AI extracts new concept nodes. ' +
|
|
49
|
-
'Call after codegraph_build, then call codegraph_add_nodes with results. ' +
|
|
50
|
-
'Pass force:true to re-enrich all files (not just changed ones).',
|
|
51
|
-
inputSchema: {
|
|
52
|
-
type: 'object',
|
|
53
|
-
properties: {
|
|
54
|
-
path: { type: 'string', description: 'Project root (same as codegraph_build)' },
|
|
55
|
-
limit: { type: 'integer', description: 'Max files to return per call (default 10)' },
|
|
56
|
-
force: { type: 'boolean', description: 'Return all files, not just changed (for re-enrichment)' },
|
|
57
|
-
},
|
|
58
|
-
required: ['path'],
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
name: 'codegraph_add_nodes',
|
|
63
|
-
description:
|
|
64
|
-
'Add concept nodes extracted by the AI into the graph. ' +
|
|
65
|
-
'Call after reading codegraph_extract output. ' +
|
|
66
|
-
'Each node: name, type, file, and optionally description and relations.',
|
|
67
|
-
inputSchema: {
|
|
68
|
-
type: 'object',
|
|
69
|
-
properties: {
|
|
70
|
-
path: { type: 'string', description: 'Project root' },
|
|
71
|
-
nodes: {
|
|
72
|
-
type: 'array',
|
|
73
|
-
description: 'Concept nodes to add',
|
|
74
|
-
items: {
|
|
75
|
-
type: 'object',
|
|
76
|
-
properties: {
|
|
77
|
-
name: { type: 'string' },
|
|
78
|
-
type: { type: 'string', description: 'class|function|concept|service|decision|requirement' },
|
|
79
|
-
file: { type: 'string', description: 'Relative file path this concept came from' },
|
|
80
|
-
description: { type: 'string' },
|
|
81
|
-
relations: {
|
|
82
|
-
type: 'array',
|
|
83
|
-
items: {
|
|
84
|
-
type: 'object',
|
|
85
|
-
properties: {
|
|
86
|
-
name: { type: 'string' },
|
|
87
|
-
relation: { type: 'string', description: 'depends-on|uses|implements|defines|documents' },
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
required: ['name', 'type', 'file'],
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
required: ['path', 'nodes'],
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
43
|
{
|
|
100
44
|
name: 'codegraph_query',
|
|
101
45
|
description:
|
|
102
|
-
'Ask a structural
|
|
103
|
-
'
|
|
104
|
-
'
|
|
105
|
-
'
|
|
46
|
+
'Ask a structural question about the codebase OR look up a specific node by name — or both in one call. ' +
|
|
47
|
+
'Pass `question` for natural-language traversal: "what does module X depend on?", "what calls function Y?". ' +
|
|
48
|
+
'Pass `node` for fast single-node lookup: returns type, file, depends_on, used_by. ' +
|
|
49
|
+
'Pass both to get node detail + surrounding graph context together. ' +
|
|
50
|
+
'Returns structured text within token_budget. Use before reading any files.',
|
|
106
51
|
inputSchema: {
|
|
107
52
|
type: 'object',
|
|
108
53
|
properties: {
|
|
109
54
|
path: { type: 'string', description: 'Project root' },
|
|
110
|
-
question: { type: 'string', description: 'Natural language question' },
|
|
55
|
+
question: { type: 'string', description: 'Natural language question about the codebase' },
|
|
56
|
+
node: { type: 'string', description: 'Node name or partial name to look up (type, file, deps, callers)' },
|
|
111
57
|
token_budget: { type: 'integer', description: 'Max tokens in response (default 2000)' },
|
|
112
58
|
},
|
|
113
|
-
required: ['path'
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: 'codegraph_explain',
|
|
118
|
-
description:
|
|
119
|
-
'Look up a node by name — returns description, type, file, and direct neighbors (depends_on + used_by). ' +
|
|
120
|
-
'Use to understand what a specific function/class/module does and how it connects. ' +
|
|
121
|
-
'Descriptions are AI-written via codegraph_add_nodes.',
|
|
122
|
-
inputSchema: {
|
|
123
|
-
type: 'object',
|
|
124
|
-
properties: {
|
|
125
|
-
path: { type: 'string', description: 'Project root' },
|
|
126
|
-
node: { type: 'string', description: 'Node name or partial name' },
|
|
127
|
-
},
|
|
128
|
-
required: ['path', 'node'],
|
|
59
|
+
required: ['path'],
|
|
129
60
|
},
|
|
130
61
|
},
|
|
131
62
|
{
|
|
@@ -184,7 +115,10 @@ export function handle(name, args, state) {
|
|
|
184
115
|
summary: result.summary || '',
|
|
185
116
|
});
|
|
186
117
|
|
|
187
|
-
const
|
|
118
|
+
const inferredProject = args.path
|
|
119
|
+
? args.path.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop()
|
|
120
|
+
: null;
|
|
121
|
+
const project = state?.sessionProject || inferredProject || null;
|
|
188
122
|
const title = `CodeGraph — ${args.path}`;
|
|
189
123
|
const content = [
|
|
190
124
|
`nodes: ${result.nodes} | edges: ${result.edges} | communities: ${result.communities}`,
|
package/src/tools/context.js
CHANGED
|
@@ -28,9 +28,9 @@ export const definition = {
|
|
|
28
28
|
`Factual memory — record what happened, what was decided, what broke, what was built.\n` +
|
|
29
29
|
`• "resume" — START HERE every conversation. Loads recent context, active discussions, and graph status for a project.\n` +
|
|
30
30
|
`• "save" — Store a note, decision, bug, or code snippet. Auto-deduplicates.\n` +
|
|
31
|
-
`• "get" — Load
|
|
31
|
+
`• "get" — Load entries. Pass id/ids to fetch specific ones, or project/tags/limit for recent.\n` +
|
|
32
32
|
`• "update" — Edit an existing entry by id (any field).\n` +
|
|
33
|
-
`• "delete" — Remove
|
|
33
|
+
`• "delete" — Remove one entry (id) or multiple at once (ids: [...]).\n` +
|
|
34
34
|
`• "list_projects"— Show all projects and entry counts.`,
|
|
35
35
|
inputSchema: {
|
|
36
36
|
type: 'object',
|
|
@@ -50,7 +50,8 @@ export const definition = {
|
|
|
50
50
|
expiresAt: { type: 'string' },
|
|
51
51
|
limit: { type: 'number' },
|
|
52
52
|
includeArchived: { type: 'boolean' },
|
|
53
|
-
id: { type: 'string' },
|
|
53
|
+
id: { type: 'string', description: 'Single entry ID (get/update/delete)' },
|
|
54
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Multiple entry IDs — fetch or delete several at once' },
|
|
54
55
|
},
|
|
55
56
|
required: ['action'],
|
|
56
57
|
},
|
|
@@ -92,9 +93,15 @@ export async function handle(args, state) {
|
|
|
92
93
|
.filter(e => e.status !== 'archived');
|
|
93
94
|
const discussions = listDiscussions({ project: proj, status: 'active' });
|
|
94
95
|
const allGraphs = listGraphs();
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const np = p => (p || '').toLowerCase().replace(/\\/g, '/');
|
|
97
|
+
const graph = resolvedRoot
|
|
98
|
+
? allGraphs.find(g => np(g.path) === np(resolvedRoot)) || null
|
|
99
|
+
: proj
|
|
100
|
+
? allGraphs.find(g => {
|
|
101
|
+
const parts = np(g.path).split('/');
|
|
102
|
+
return parts[parts.length - 1] === proj.toLowerCase();
|
|
103
|
+
}) || null
|
|
104
|
+
: null;
|
|
98
105
|
const totalEntries = countContext(proj);
|
|
99
106
|
|
|
100
107
|
// Auto-restore single active discussion
|
|
@@ -183,8 +190,20 @@ export async function handle(args, state) {
|
|
|
183
190
|
|
|
184
191
|
case 'get': {
|
|
185
192
|
if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
|
|
186
|
-
archiveExpired(args.project);
|
|
187
193
|
const includeArchived = args.includeArchived === true;
|
|
194
|
+
|
|
195
|
+
// Fetch by specific ID(s) — bypass project/tag/limit filters
|
|
196
|
+
const ids = args.ids || (args.id ? [args.id] : null);
|
|
197
|
+
if (ids) {
|
|
198
|
+
const entries = getContext({ ids, compact: false })
|
|
199
|
+
.filter(e => includeArchived || e.status !== 'archived');
|
|
200
|
+
return {
|
|
201
|
+
entries, count: entries.length,
|
|
202
|
+
message: entries.length ? `Found ${entries.length} entries.` : 'No entries found for given IDs.',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
archiveExpired(args.project);
|
|
188
207
|
let entries = getContext({ project: args.project, tags: args.tags, limit: args.limit, compact: true });
|
|
189
208
|
if (!includeArchived) entries = entries.filter(e => e.status !== 'archived');
|
|
190
209
|
const fullEntries = entries.length > 10
|
|
@@ -210,8 +229,9 @@ export async function handle(args, state) {
|
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
case 'delete': {
|
|
213
|
-
if (!args.id) throw new Error('id is required for delete');
|
|
214
|
-
|
|
232
|
+
if (!args.id && !args.ids) throw new Error('id or ids is required for delete');
|
|
233
|
+
const result = deleteContext(args);
|
|
234
|
+
return { ...result, message: `Deleted ${result.deleted} entr${result.deleted === 1 ? 'y' : 'ies'}.` };
|
|
215
235
|
}
|
|
216
236
|
|
|
217
237
|
case 'list_projects': {
|