context-mcp-server 1.0.8 → 1.1.0
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/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
- package/codegraph/graph/query.py +43 -0
- package/codegraph/server.py +20 -17
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.js +94 -183
- package/src/db.js +79 -102
- package/src/search.js +73 -9
- package/src/templates/AGENTS.md +56 -46
- package/src/templates/CLAUDE.md +89 -55
- package/src/templates/GEMINI.md +56 -46
- package/src/templates/commands/save-context.md +6 -3
- package/src/templates/skills/SKILL.md +83 -50
- package/src/templates/windsurf-rules.md +69 -20
- package/src/tools/codegraph.js +46 -43
- package/src/tools/context.js +42 -24
- package/src/tools/plan.js +14 -11
- package/uv.lock +1 -1
- package/src/migrator.js +0 -124
|
Binary file
|
|
Binary file
|
package/codegraph/graph/query.py
CHANGED
|
@@ -237,6 +237,49 @@ def _general_search(matched: list, nodes: list, edges: list) -> dict:
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
|
|
240
|
+
def module_map(graph_dict: dict, limit: int = 100) -> dict:
|
|
241
|
+
"""
|
|
242
|
+
Return a module map: for each file, its exported functions/classes and what it imports.
|
|
243
|
+
Output grouped by file, sorted by export count descending.
|
|
244
|
+
"""
|
|
245
|
+
nodes = graph_dict.get("nodes", [])
|
|
246
|
+
edges = graph_dict.get("edges", [])
|
|
247
|
+
|
|
248
|
+
files: dict[str, dict] = {}
|
|
249
|
+
for n in nodes:
|
|
250
|
+
f = n.get("file") or "unknown"
|
|
251
|
+
if f not in files:
|
|
252
|
+
files[f] = {"exports": [], "imports": set()}
|
|
253
|
+
node_type = n.get("type", "?")
|
|
254
|
+
if node_type in ("function", "class", "struct"):
|
|
255
|
+
files[f]["exports"].append({"name": n["name"], "type": node_type})
|
|
256
|
+
|
|
257
|
+
node_map = {n["id"]: n for n in nodes}
|
|
258
|
+
for e in edges:
|
|
259
|
+
from_node = node_map.get(e.get("from", ""))
|
|
260
|
+
to_node = node_map.get(e.get("to", ""))
|
|
261
|
+
if not from_node or not to_node:
|
|
262
|
+
continue
|
|
263
|
+
from_file = from_node.get("file") or "unknown"
|
|
264
|
+
to_file = to_node.get("file") or "unknown"
|
|
265
|
+
if from_file != to_file and from_file in files:
|
|
266
|
+
files[from_file]["imports"].add(to_file)
|
|
267
|
+
|
|
268
|
+
result = []
|
|
269
|
+
for fpath, data in sorted(files.items(), key=lambda x: -len(x[1]["exports"])):
|
|
270
|
+
result.append({
|
|
271
|
+
"file": fpath,
|
|
272
|
+
"exports": data["exports"][:30],
|
|
273
|
+
"imports": sorted(data["imports"])[:20],
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
"files": result[:limit],
|
|
278
|
+
"total_files": len(files),
|
|
279
|
+
"truncated": len(files) > limit,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
240
283
|
def _render_subgraph(result_nodes: list, all_edges: list, token_budget: int) -> str:
|
|
241
284
|
"""
|
|
242
285
|
Render a subgraph as structured plain text (graphify-style).
|
package/codegraph/server.py
CHANGED
|
@@ -4,10 +4,10 @@ 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 — structural question OR single-node lookup (or both)
|
|
7
|
+
codegraph_query — structural question OR single-node lookup (or both)
|
|
8
|
+
codegraph_arch — module map: every file with its exports and imports
|
|
8
9
|
codegraph_report — return full CODEGRAPH_REPORT.md
|
|
9
10
|
codegraph_nodes — list nodes of a given type
|
|
10
|
-
codegraph_path — shortest path between two concepts
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import asyncio
|
|
@@ -25,7 +25,7 @@ from .config import classify_file
|
|
|
25
25
|
from .cache import file_hash, set_cached_nodes, save_cache
|
|
26
26
|
from .extractors.ast_extractor import extract as ast_extract
|
|
27
27
|
from .graph.builder import build, to_json_dict, save_graph, load_graph
|
|
28
|
-
from .graph.query import answer as graph_answer,
|
|
28
|
+
from .graph.query import answer as graph_answer, module_map
|
|
29
29
|
from .graph.clustering import detect_communities
|
|
30
30
|
from .report import generate as generate_report
|
|
31
31
|
|
|
@@ -95,16 +95,19 @@ TOOLS = [
|
|
|
95
95
|
},
|
|
96
96
|
),
|
|
97
97
|
Tool(
|
|
98
|
-
name="
|
|
99
|
-
description=
|
|
98
|
+
name="codegraph_arch",
|
|
99
|
+
description=(
|
|
100
|
+
"Return a module map: every file with its exported functions/classes and what it imports. "
|
|
101
|
+
"Use this to understand project structure without reading any files. "
|
|
102
|
+
"Call after codegraph_build. Much faster than reading each file."
|
|
103
|
+
),
|
|
100
104
|
inputSchema={
|
|
101
105
|
"type": "object",
|
|
102
106
|
"properties": {
|
|
103
|
-
"path":
|
|
104
|
-
"
|
|
105
|
-
"to": {"type": "string"},
|
|
107
|
+
"path": {"type": "string", "description": "Project root"},
|
|
108
|
+
"limit": {"type": "integer", "description": "Max files in output (default 100)"},
|
|
106
109
|
},
|
|
107
|
-
"required": ["path"
|
|
110
|
+
"required": ["path"],
|
|
108
111
|
},
|
|
109
112
|
),
|
|
110
113
|
]
|
|
@@ -125,12 +128,11 @@ async def call_tool(name: str, arguments: dict):
|
|
|
125
128
|
|
|
126
129
|
|
|
127
130
|
async def _dispatch(name: str, args: dict):
|
|
128
|
-
if name == "codegraph_build":
|
|
129
|
-
if name == "codegraph_query":
|
|
130
|
-
if name == "
|
|
131
|
-
if name == "
|
|
132
|
-
if name == "
|
|
133
|
-
if name == "codegraph_path": return await _path(args)
|
|
131
|
+
if name == "codegraph_build": return await _build(args)
|
|
132
|
+
if name == "codegraph_query": return await _query(args)
|
|
133
|
+
if name == "codegraph_report": return await _report(args)
|
|
134
|
+
if name == "codegraph_nodes": return await _nodes(args)
|
|
135
|
+
if name == "codegraph_arch": return await _arch(args)
|
|
134
136
|
raise ValueError(f"Unknown tool: {name}")
|
|
135
137
|
|
|
136
138
|
|
|
@@ -285,11 +287,12 @@ async def _nodes(args: dict) -> dict:
|
|
|
285
287
|
return {"type": node_type, "count": len(matched), "nodes": matched[:limit]}
|
|
286
288
|
|
|
287
289
|
|
|
288
|
-
async def
|
|
290
|
+
async def _arch(args: dict) -> dict:
|
|
289
291
|
graph_dict = load_graph(args["path"])
|
|
290
292
|
if not graph_dict:
|
|
291
293
|
raise ValueError("No graph found. Run codegraph_build first.")
|
|
292
|
-
|
|
294
|
+
limit = args.get("limit", 100)
|
|
295
|
+
return module_map(graph_dict, limit=limit)
|
|
293
296
|
|
|
294
297
|
|
|
295
298
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mcp-server",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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": {
|
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.1.0"
|
|
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"
|
package/src/cli.js
CHANGED
|
@@ -107,45 +107,29 @@ function printSection(title, meta = '') {
|
|
|
107
107
|
function printUsage() {
|
|
108
108
|
printBanner();
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
printSection('Terminal commands', 'run from your shell (ctx … or context …)');
|
|
110
|
+
printSection('Commands');
|
|
112
111
|
const cmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
|
|
113
112
|
cmd('ctx', 'open interactive mode');
|
|
114
113
|
cmd('ctx list [project]', 'list entries + discussions + graphs');
|
|
115
114
|
cmd('ctx search <query>', 'keyword → semantic fallback search');
|
|
116
115
|
cmd('ctx add', 'add entry interactively');
|
|
116
|
+
cmd('ctx save --title "…" --content "…" --project <p> --type <t>', 'non-interactive save (scripts/hooks)');
|
|
117
117
|
cmd('ctx delete <id-prefix>', 'delete one entry');
|
|
118
118
|
cmd('ctx delete project <name|id>', 'delete all entries for a project');
|
|
119
119
|
cmd('ctx summary [project]', 'summarize recent entries');
|
|
120
120
|
cmd('ctx projects', 'show all projects + graphs');
|
|
121
121
|
cmd('ctx discuss [project]', 'show discussions');
|
|
122
|
-
cmd('ctx
|
|
122
|
+
cmd('ctx stats', 'storage report: entries, types, graph status');
|
|
123
123
|
console.log('');
|
|
124
|
-
cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps');
|
|
125
|
-
cmd('ctx install --<platform>', 'write MCP config + skill/rules
|
|
126
|
-
cmd('ctx install --all', '
|
|
124
|
+
cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps only');
|
|
125
|
+
cmd('ctx install --<platform>', 'write MCP config + skill/rules file only (no uv/npm)');
|
|
126
|
+
cmd('ctx install --all', 'write config + skill files for all platforms');
|
|
127
127
|
cmd('ctx online [--port N]', 'start HTTP server for Claude.ai / ChatGPT');
|
|
128
128
|
cmd('ctx online --close', 'stop the running HTTP server');
|
|
129
129
|
cmd('ctx settings', 'view and edit config (port, host, client id/secret)');
|
|
130
130
|
cmd('ctx update', 'check for and apply latest version');
|
|
131
131
|
cmd('ctx help', 'show this screen');
|
|
132
132
|
console.log('');
|
|
133
|
-
|
|
134
|
-
// Interactive mode commands (no prefix needed)
|
|
135
|
-
printSection('Interactive mode', 'type these inside the UI — no "ctx" prefix needed');
|
|
136
|
-
const icmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
|
|
137
|
-
icmd('list [project]', 'list entries');
|
|
138
|
-
icmd('search <query>', 'search context');
|
|
139
|
-
icmd('add', 'add entry');
|
|
140
|
-
icmd('projects', 'show all projects');
|
|
141
|
-
icmd('discuss [project]', 'show discussions');
|
|
142
|
-
icmd('summary [project]', 'summarize recent entries');
|
|
143
|
-
icmd('benchmark', 'token savings report');
|
|
144
|
-
icmd('install --<platform>', 'install for a platform');
|
|
145
|
-
icmd('settings', 'edit config');
|
|
146
|
-
icmd('clear', 'clear screen');
|
|
147
|
-
icmd('exit / quit / q', 'exit interactive mode');
|
|
148
|
-
console.log('');
|
|
149
133
|
}
|
|
150
134
|
function clearScreen() {
|
|
151
135
|
// \x1b[2J = clear screen, \x1b[3J = clear scrollback, \x1b[H = cursor home
|
|
@@ -155,7 +139,8 @@ function clearScreen() {
|
|
|
155
139
|
// ── List (grouped by project) ─────────────────────────────────────────────────
|
|
156
140
|
|
|
157
141
|
function cmdList(args) {
|
|
158
|
-
const
|
|
142
|
+
const projectFlagIdx = args.indexOf('--project');
|
|
143
|
+
const filterProject = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
|
|
159
144
|
const entries = getContext({ project: filterProject, limit: 100 });
|
|
160
145
|
const allDiscussions = listDiscussions({ project: filterProject });
|
|
161
146
|
const allGraphs = listGraphs();
|
|
@@ -163,7 +148,6 @@ function cmdList(args) {
|
|
|
163
148
|
|
|
164
149
|
printSection('Context', filterProject ? `project: ${filterProject}` : 'all projects');
|
|
165
150
|
|
|
166
|
-
// Build per-project map, split entries into their three trees
|
|
167
151
|
const projects = {};
|
|
168
152
|
const ensureProj = p => {
|
|
169
153
|
if (!projects[p]) projects[p] = { context: [], summary: [], plans: [] };
|
|
@@ -213,13 +197,12 @@ function cmdList(args) {
|
|
|
213
197
|
const id = item.id.slice(0, 8);
|
|
214
198
|
const tags = safeTags(item.tags);
|
|
215
199
|
const pipe = secIsLast ? ' ' : '│';
|
|
216
|
-
console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${
|
|
200
|
+
console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${bold(item.title || '(no title)')} ${faint('id:' + id)} ${faint(date)}`);
|
|
217
201
|
if (tags.length) console.log(` ${color(C.darkgray, pipe)} ${faint(tags.map(t => '#' + t).join(' '))}`);
|
|
218
202
|
});
|
|
219
203
|
if (!secIsLast) console.log(` ${color(C.darkgray, '│')}`);
|
|
220
204
|
};
|
|
221
205
|
|
|
222
|
-
// ── Graph (build stats only) ──────────────────────────────────────────────
|
|
223
206
|
if (graphBuild) {
|
|
224
207
|
secIdx++;
|
|
225
208
|
const isLast = secIdx === sections.length;
|
|
@@ -228,13 +211,11 @@ function cmdList(args) {
|
|
|
228
211
|
if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
|
|
229
212
|
}
|
|
230
213
|
|
|
231
|
-
// ── Context ───────────────────────────────────────────────────────────────
|
|
232
214
|
if (pData.context.length) {
|
|
233
215
|
secIdx++;
|
|
234
216
|
renderEntries(pData.context, 'context', secIdx === sections.length);
|
|
235
217
|
}
|
|
236
218
|
|
|
237
|
-
// ── Summary (compaction digests only — no type labels) ───────────────────
|
|
238
219
|
if (pData.summary.length) {
|
|
239
220
|
secIdx++;
|
|
240
221
|
const isLast = secIdx === sections.length;
|
|
@@ -248,7 +229,6 @@ function cmdList(args) {
|
|
|
248
229
|
if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
|
|
249
230
|
}
|
|
250
231
|
|
|
251
|
-
// ── Plans ─────────────────────────────────────────────────────────────────
|
|
252
232
|
if (pData.plans.length) {
|
|
253
233
|
secIdx++;
|
|
254
234
|
const isLast = secIdx === sections.length;
|
|
@@ -285,7 +265,7 @@ function cmdSearch(args) {
|
|
|
285
265
|
const id = entry.id.slice(0, 8);
|
|
286
266
|
const type = entry.type || 'note';
|
|
287
267
|
const isLast = index === results.length - 1;
|
|
288
|
-
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${bold(entry.title || '(no title)')}${score} ${
|
|
268
|
+
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${bold(entry.title || '(no title)')}${score} ${faint('id:' + id)} ${faint(date)}`);
|
|
289
269
|
});
|
|
290
270
|
console.log('');
|
|
291
271
|
console.log(line());
|
|
@@ -313,7 +293,6 @@ function cmdProjects() {
|
|
|
313
293
|
console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(project.name))}${idTag} ${bar} ${faint(project.count + ' entries')}`);
|
|
314
294
|
console.log(` ${color(C.darkgray, '│')}`);
|
|
315
295
|
|
|
316
|
-
// Graph status
|
|
317
296
|
if (graph) {
|
|
318
297
|
const builtAt = (graph.builtAt || '').slice(0, 10);
|
|
319
298
|
console.log(` ${color(C.darkgray, '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graph.nodes}n · ${graph.edges}e · ${graph.communities} clusters · ${builtAt}`)}`);
|
|
@@ -321,18 +300,15 @@ function cmdProjects() {
|
|
|
321
300
|
console.log(` ${color(C.darkgray, '├─')} ${faint('⬡ no graph')}`);
|
|
322
301
|
}
|
|
323
302
|
|
|
324
|
-
// Recent context
|
|
325
303
|
if (entries.length) {
|
|
326
304
|
console.log(` ${color(C.darkgray, '├─')} ${muted('recent')}`);
|
|
327
305
|
entries.forEach((e, i) => {
|
|
328
306
|
const br = i === entries.length - 1 && !activeD.length ? '└─' : '├─';
|
|
329
|
-
const type = e.type || 'note';
|
|
330
307
|
const date = (e.createdAt || '').slice(0, 10);
|
|
331
|
-
console.log(` ${color(C.darkgray, '│')} ${color(C.darkgray, br)} ${
|
|
308
|
+
console.log(` ${color(C.darkgray, '│')} ${color(C.darkgray, br)} ${bold(e.title || '(no title)')} ${faint(date)}`);
|
|
332
309
|
});
|
|
333
310
|
}
|
|
334
311
|
|
|
335
|
-
// Active discussions
|
|
336
312
|
if (activeD.length) {
|
|
337
313
|
console.log(` ${color(C.darkgray, '├─')} ${muted('discussions')}`);
|
|
338
314
|
activeD.forEach((d, i) => {
|
|
@@ -354,7 +330,8 @@ function cmdProjects() {
|
|
|
354
330
|
// ── Discussions ───────────────────────────────────────────────────────────────
|
|
355
331
|
|
|
356
332
|
function cmdDiscussions(args) {
|
|
357
|
-
const
|
|
333
|
+
const projectFlagIdx = args.indexOf('--project');
|
|
334
|
+
const filterProject = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
|
|
358
335
|
const discussions = listDiscussions({ project: filterProject });
|
|
359
336
|
printSection('Discussions', filterProject || 'all projects');
|
|
360
337
|
|
|
@@ -392,7 +369,8 @@ function cmdDiscussions(args) {
|
|
|
392
369
|
// ── Summary ───────────────────────────────────────────────────────────────────
|
|
393
370
|
|
|
394
371
|
function cmdSummary(args) {
|
|
395
|
-
const
|
|
372
|
+
const projectFlagIdx = args.indexOf('--project');
|
|
373
|
+
const project = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
|
|
396
374
|
const entries = getContext({ project, limit: 50 });
|
|
397
375
|
printSection('Summary', project || 'global');
|
|
398
376
|
if (!entries.length) { console.log(` ${faint('no entries to summarize')}`); return; }
|
|
@@ -411,137 +389,54 @@ function cmdSummary(args) {
|
|
|
411
389
|
// ── Benchmark ────────────────────────────────────────────────────────────────
|
|
412
390
|
|
|
413
391
|
|
|
414
|
-
function
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
`const p=path.join(d,f);try{const s=fs.statSync(p);` +
|
|
419
|
-
`if(s.isDirectory()&&!['node_modules','.git','codegraph-cache','.venv','venv','__pycache__','dist','build','.next'].includes(f))t+=walk(p);` +
|
|
420
|
-
`else if(s.isFile()&&/\\.(js|jsx|ts|tsx|py|json|md|yaml|yml|toml|sh|bash|env|txt|css|html|sql|go|rs|java|rb|php|c|cpp|h)$/.test(f)&&!/^(package-lock|uv\.lock|yarn\.lock|Pipfile\.lock|poetry\.lock)$/.test(path.basename(f,path.extname(f))+path.extname(f)))t+=s.size;}catch{}}}catch{}return t;}` +
|
|
421
|
-
`console.log(walk(${JSON.stringify(dirPath)}));`;
|
|
422
|
-
const res = spawnSync('node', ['-e', walkScript], { encoding: 'utf8', timeout: 8000 });
|
|
423
|
-
return res.stdout ? parseInt(res.stdout.trim()) : null;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function _sampleQueryTokens(graphPath) {
|
|
427
|
-
const questions = ['what does the server do', 'how is the graph built', 'what calls save'];
|
|
428
|
-
const sizes = [];
|
|
429
|
-
for (const q of questions) {
|
|
430
|
-
const req = JSON.stringify({ tool: 'codegraph_query', args: { path: graphPath, question: q, token_budget: 2000 } });
|
|
431
|
-
const res = spawnSync('uv', ['run', 'python', '-m', 'codegraph'],
|
|
432
|
-
{ input: req, encoding: 'utf8', cwd: process.cwd(), timeout: 12000 });
|
|
433
|
-
if (res.stdout) { try { const r = JSON.parse(res.stdout); if (!r.error) sizes.push(res.stdout.length); } catch {} }
|
|
434
|
-
}
|
|
435
|
-
return { avgTok: sizes.length ? Math.round(sizes.reduce((a, b) => a + b, 0) / sizes.length / 4) : 472, measured: sizes.length > 0 };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function cmdBenchmark() {
|
|
439
|
-
const graphs = listGraphs();
|
|
440
|
-
const projects = listProjects();
|
|
441
|
-
printSection('Benchmark', 'real token savings');
|
|
442
|
-
|
|
443
|
-
const RESUME_LIMIT = 15;
|
|
444
|
-
const COMPACT_AT = 20;
|
|
445
|
-
|
|
446
|
-
// ── Measure entry sizes from actual stored data ──────────────────────────────
|
|
447
|
-
const allEntries = getContext({ limit: 500, compact: false });
|
|
448
|
-
const totalEntries = allEntries.length;
|
|
449
|
-
let avgFullTok = 155, avgCompactTok = 50;
|
|
450
|
-
if (allEntries.length) {
|
|
451
|
-
const fullSizes = allEntries.map(e => JSON.stringify(e).length);
|
|
452
|
-
const compactSizes = allEntries.map(e =>
|
|
453
|
-
([e.title || '', (e.content || '').slice(0, 200), (e.tags || []).join(' ')].join(' ')).length);
|
|
454
|
-
avgFullTok = Math.round(fullSizes.reduce((a, b) => a + b, 0) / fullSizes.length / 4);
|
|
455
|
-
avgCompactTok = Math.round(compactSizes.reduce((a, b) => a + b, 0) / compactSizes.length / 4);
|
|
456
|
-
}
|
|
392
|
+
function cmdStats() {
|
|
393
|
+
const projects = listProjects();
|
|
394
|
+
const graphs = listGraphs();
|
|
395
|
+
const allEntries = getContext({ limit: 2000, compact: false });
|
|
457
396
|
|
|
458
|
-
|
|
459
|
-
let avgQueryTok = 472, queryMeasured = false;
|
|
460
|
-
if (graphs.length) {
|
|
461
|
-
const r = _sampleQueryTokens(graphs[0].path);
|
|
462
|
-
avgQueryTok = r.avgTok;
|
|
463
|
-
queryMeasured = r.measured;
|
|
464
|
-
}
|
|
397
|
+
printSection('Stats', 'context-mcp storage report');
|
|
465
398
|
|
|
466
|
-
// ──
|
|
467
|
-
|
|
468
|
-
if (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// ── Memory ───────────────────────────────────────────────────────────────────
|
|
474
|
-
// Without context-mcp: AI reads all entries at full size every conversation
|
|
475
|
-
// With context-mcp: AI loads min(15, N) entries as compact previews via resume
|
|
476
|
-
console.log(`\n ${bold(lblue('Memory'))} ${faint('measured from actual stored entries')}`);
|
|
477
|
-
if (totalEntries) {
|
|
478
|
-
const resumeCount = Math.min(RESUME_LIMIT, totalEntries);
|
|
479
|
-
const withoutMemTok = totalEntries * avgFullTok; // load all entries full
|
|
480
|
-
const withMemTok = resumeCount * avgCompactTok; // resume: compact previews only
|
|
481
|
-
const memSaved = withoutMemTok - withMemTok;
|
|
482
|
-
const memReduction = (withoutMemTok / withMemTok).toFixed(1);
|
|
483
|
-
const memPct = ((1 - withMemTok / withoutMemTok) * 100).toFixed(1);
|
|
484
|
-
|
|
485
|
-
console.log(` ${faint('avg entry size (full): ')} ${muted(avgFullTok)} tok ${faint('(measured)')}`);
|
|
486
|
-
console.log(` ${faint('avg entry size (compact):')} ${muted(avgCompactTok)} tok ${faint('(measured)')}`);
|
|
487
|
-
console.log(` ${faint('stored: ')} ${muted(totalEntries)} entries ${faint('across')} ${muted(projects.length)} project(s)`);
|
|
488
|
-
console.log(` ${faint('without — load all full: ')} ${warn('~' + withoutMemTok.toLocaleString('en-US'))} tokens`);
|
|
489
|
-
console.log(` ${faint('with — resume compact:')} ${ok('~' + withMemTok.toLocaleString('en-US'))} tokens ${faint(`(${resumeCount} of ${totalEntries} entries)`)}`);
|
|
490
|
-
console.log(` ${faint('saved per chat: ')} ${ok('~' + memSaved.toLocaleString('en-US'))} tokens ${highlight(memReduction + '×')} ${ok(memPct + '%')} reduction`);
|
|
491
|
-
console.log(` ${faint('auto-compact at: ')} ${faint(COMPACT_AT + ' entries → oldest summarized to 1')}`);
|
|
492
|
-
console.log('');
|
|
399
|
+
// ── Projects ──────────────────────────────────────────────────────────────────
|
|
400
|
+
console.log(`\n ${bold(lblue('Projects'))}`);
|
|
401
|
+
if (!projects.length) {
|
|
402
|
+
console.log(` ${faint('no projects yet')}`);
|
|
403
|
+
} else {
|
|
493
404
|
for (const p of projects) {
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
405
|
+
const g = graphs.find(gr => {
|
|
406
|
+
const gp = (gr.path || '').replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
|
|
407
|
+
return gp === p.name;
|
|
408
|
+
});
|
|
409
|
+
const graphStatus = g ? ok(`graph: ${g.nodes} nodes, ${g.edges} edges`) : faint('no graph');
|
|
410
|
+
const builtAt = g?.builtAt ? faint(' built ' + g.builtAt.slice(0, 10)) : '';
|
|
411
|
+
console.log(` ${bold(p.name.padEnd(24))} ${muted(String(p.count).padStart(3) + ' entries')} ${graphStatus}${builtAt}`);
|
|
498
412
|
}
|
|
499
|
-
} else {
|
|
500
|
-
console.log(` ${faint('no entries yet')}`);
|
|
501
413
|
}
|
|
502
414
|
|
|
503
|
-
// ──
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (!graphs.length) {
|
|
508
|
-
console.log(` ${faint('no graphs — run codegraph_build first')}`);
|
|
415
|
+
// ── Entry type breakdown ──────────────────────────────────────────────────────
|
|
416
|
+
console.log(`\n ${bold(lblue('Entries by type'))}`);
|
|
417
|
+
if (!allEntries.length) {
|
|
418
|
+
console.log(` ${faint('no entries yet')}`);
|
|
509
419
|
} else {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
console.log(`\n ${accent('⬡')} ${bold(pathShort)} ${faint(builtAt)}`);
|
|
517
|
-
console.log(` ${faint('nodes:')} ${muted(g.nodes)} ${faint('edges:')} ${muted(g.edges)} ${faint('clusters:')} ${muted(g.communities)}`);
|
|
518
|
-
if (corpusToks) console.log(` ${faint('without — read all files:')} ${warn('~' + corpusToks.toLocaleString('en-US'))} tokens ${faint('(all source + config + doc files)')}`);
|
|
519
|
-
console.log(` ${faint('with — graph query: ')} ${ok('~' + avgQueryTok.toLocaleString('en-US'))} tokens ${faint(queryMeasured ? '(avg of 3 live queries)' : '(calibrated fallback)')}`);
|
|
520
|
-
if (graphReduce) console.log(` ${faint('saved per query: ')} ${highlight(graphReduce)} ${ok(graphPct + '%')} fewer tokens`);
|
|
420
|
+
const byType = {};
|
|
421
|
+
for (const e of allEntries) { byType[e.type] = (byType[e.type] || 0) + 1; }
|
|
422
|
+
for (const [type, count] of Object.entries(byType).sort((a, b) => b[1] - a[1])) {
|
|
423
|
+
const bar = color(C.dblue, '█'.repeat(Math.min(count, 20))) + color(C.darkgray, '░'.repeat(Math.max(0, 20 - count)));
|
|
424
|
+
console.log(` ${type.padEnd(14)} ${bar} ${muted(count)}`);
|
|
521
425
|
}
|
|
522
426
|
}
|
|
523
427
|
|
|
524
|
-
// ──
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
console.log(`\n ${bold(lblue('Combined'))} ${faint('per conversation')}`);
|
|
537
|
-
console.log(` ${faint('without context-mcp: ')} ${warn('~' + withoutMcp.toLocaleString('en-US'))} tokens ${faint('(all entries full + all files read directly)')}`);
|
|
538
|
-
console.log(` ${faint('with context-mcp: ')} ${ok('~' + withMcp.toLocaleString('en-US'))} tokens ${faint('(compact resume + 1 graph query)')}`);
|
|
539
|
-
console.log(` ${faint('total reduction: ')} ${highlight(totalRed + '×')} ${ok(totalPct + '%')} fewer tokens`);
|
|
540
|
-
}
|
|
541
|
-
|
|
428
|
+
// ── Storage summary ───────────────────────────────────────────────────────────
|
|
429
|
+
const compactions = allEntries.filter(e => e.type === 'compaction').length;
|
|
430
|
+
const avgSize = allEntries.length
|
|
431
|
+
? Math.round(allEntries.reduce((s, e) => s + (e.content || '').length, 0) / allEntries.length)
|
|
432
|
+
: 0;
|
|
433
|
+
|
|
434
|
+
console.log(`\n ${bold(lblue('Storage'))}`);
|
|
435
|
+
console.log(` ${faint('total entries: ')} ${muted(allEntries.length)}`);
|
|
436
|
+
console.log(` ${faint('compactions: ')} ${muted(compactions)}`);
|
|
437
|
+
console.log(` ${faint('avg entry size: ')} ${muted(avgSize + ' chars')}`);
|
|
438
|
+
console.log(` ${faint('graphs built: ')} ${muted(graphs.length)}`);
|
|
542
439
|
console.log('');
|
|
543
|
-
console.log(line());
|
|
544
|
-
console.log(faint(' token estimate: chars ÷ 4 · corpus = all source/config/doc files (excl. lock files, .venv, node_modules)'));
|
|
545
440
|
}
|
|
546
441
|
|
|
547
442
|
|
|
@@ -796,6 +691,15 @@ async function cmdInstall(args) {
|
|
|
796
691
|
console.log(` ${ok('✓')} Python environment ready — codegraph enabled`);
|
|
797
692
|
}
|
|
798
693
|
}
|
|
694
|
+
// Bootstrap store structure — creates ~/.context-mcp/, projects/, contextconfig.json
|
|
695
|
+
console.log(` ${bold(lblue('Store'))}`);
|
|
696
|
+
try {
|
|
697
|
+
getConfig(); // triggers DATA_DIR creation + contextconfig.json generation
|
|
698
|
+
console.log(` ${ok('✓')} store ready ${faint(getStorePath())}`);
|
|
699
|
+
console.log(` ${ok('✓')} config ready ${faint(getConfigPath())}`);
|
|
700
|
+
} catch (e) {
|
|
701
|
+
console.log(` ${bad('✗')} store init failed: ${faint(e.message)}`);
|
|
702
|
+
}
|
|
799
703
|
console.log('');
|
|
800
704
|
return;
|
|
801
705
|
}
|
|
@@ -871,27 +775,6 @@ async function cmdInstall(args) {
|
|
|
871
775
|
// ── Global gitignore — add context-mcp runtime files if global gitignore exists ──
|
|
872
776
|
_updateGlobalGitignore();
|
|
873
777
|
console.log('');
|
|
874
|
-
|
|
875
|
-
// ── Python / uv setup (codegraph) ─────────────────────────────────────────
|
|
876
|
-
console.log(` ${bold(lblue('Python Codegraph'))}`);
|
|
877
|
-
const uvCheck = spawnSync('uv', ['--version'], { encoding: 'utf8' });
|
|
878
|
-
if (uvCheck.error || uvCheck.status !== 0) {
|
|
879
|
-
console.log(` ${bad('✗')} uv not found — install it from ${accent('https://docs.astral.sh/uv/')} to enable codegraph`);
|
|
880
|
-
console.log('');
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
console.log(` ${ok('✓')} uv found: ${faint(uvCheck.stdout.trim())}`);
|
|
884
|
-
|
|
885
|
-
// Package root is one level up from src/
|
|
886
|
-
const __dirname_cli = dirname(fileURLToPath(import.meta.url));
|
|
887
|
-
const pkgRoot = join(__dirname_cli, '..');
|
|
888
|
-
const sync = spawnSync('uv', ['--directory', pkgRoot, 'sync', '--no-dev'], { encoding: 'utf8' });
|
|
889
|
-
if (sync.status !== 0) {
|
|
890
|
-
console.log(` ${bad('✗')} uv sync failed:\n${faint((sync.stderr || sync.stdout || '').trim())}`);
|
|
891
|
-
} else {
|
|
892
|
-
console.log(` ${ok('✓')} Python environment ready — codegraph enabled`);
|
|
893
|
-
}
|
|
894
|
-
console.log('');
|
|
895
778
|
}
|
|
896
779
|
|
|
897
780
|
// ── Online ────────────────────────────────────────────────────────────────────
|
|
@@ -1077,7 +960,7 @@ async function cmdAdd(existingRl) {
|
|
|
1077
960
|
const content = await ask('Content:');
|
|
1078
961
|
const project = await ask('Project (blank = global):');
|
|
1079
962
|
const tagsRaw = await ask('Tags (comma-separated):');
|
|
1080
|
-
const type = await ask('Type (note/
|
|
963
|
+
const type = await ask('Type (note/compaction):');
|
|
1081
964
|
|
|
1082
965
|
if (!existingRl) rl.close();
|
|
1083
966
|
if (!content.trim()) { console.log(` ${bad('✗')} content required`); return; }
|
|
@@ -1094,6 +977,30 @@ async function cmdAdd(existingRl) {
|
|
|
1094
977
|
console.log(` ${ok('✓')} ${bold(entry.title || '(no title)')} ${faint('id:' + entry.id.slice(0, 8))}`);
|
|
1095
978
|
}
|
|
1096
979
|
|
|
980
|
+
// ── Save (non-interactive, flag-based — used by hooks and scripts) ───────────
|
|
981
|
+
|
|
982
|
+
function cmdSave(args) {
|
|
983
|
+
// Usage: ctx save --title "..." --content "..." --project <p> --type <t> --tags <t1,t2>
|
|
984
|
+
const flags = {};
|
|
985
|
+
for (let i = 0; i < args.length; i++) {
|
|
986
|
+
if (args[i].startsWith('--')) {
|
|
987
|
+
flags[args[i].slice(2)] = args[i + 1] || '';
|
|
988
|
+
i++;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const content = flags.content || flags.c;
|
|
992
|
+
if (!content) { console.log(` ${bad('✗')} --content required`); process.exit(1); }
|
|
993
|
+
const entry = saveContext({
|
|
994
|
+
title: (flags.title || flags.t || '').trim(),
|
|
995
|
+
content: content.trim(),
|
|
996
|
+
project: (flags.project || flags.p || '').trim() || 'global',
|
|
997
|
+
tags: (flags.tags || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
998
|
+
type: (flags.type || 'note').trim(),
|
|
999
|
+
source: 'cli',
|
|
1000
|
+
});
|
|
1001
|
+
console.log(` ${ok('✓')} saved "${entry.title || entry.id.slice(0, 8)}" → ${entry.project}`);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1097
1004
|
// ── Delete ────────────────────────────────────────────────────────────────────
|
|
1098
1005
|
|
|
1099
1006
|
function cmdDelete(args) {
|
|
@@ -1193,8 +1100,8 @@ async function interactive() {
|
|
|
1193
1100
|
clearScreen(); printCompactHeader('discussions'); cmdDiscussions(rest); break;
|
|
1194
1101
|
case 'summary':
|
|
1195
1102
|
clearScreen(); printCompactHeader('summary'); cmdSummary(rest); break;
|
|
1196
|
-
case '
|
|
1197
|
-
clearScreen(); printCompactHeader('
|
|
1103
|
+
case 'stats':
|
|
1104
|
+
clearScreen(); printCompactHeader('stats'); cmdStats(); break;
|
|
1198
1105
|
case 'install':
|
|
1199
1106
|
clearScreen(); printCompactHeader('install'); await cmdInstall(rest); break;
|
|
1200
1107
|
case 'online':
|
|
@@ -1203,6 +1110,8 @@ async function interactive() {
|
|
|
1203
1110
|
clearScreen(); printCompactHeader('settings'); await cmdSettings(rl); break;
|
|
1204
1111
|
case 'add':
|
|
1205
1112
|
clearScreen(); printCompactHeader('add'); await cmdAdd(rl); break;
|
|
1113
|
+
case 'save':
|
|
1114
|
+
cmdSave(rest); break;
|
|
1206
1115
|
case 'delete': case 'del': case 'rm':
|
|
1207
1116
|
clearScreen(); printCompactHeader('delete'); cmdDelete(rest); break;
|
|
1208
1117
|
case 'help': case '?':
|
|
@@ -1257,8 +1166,8 @@ async function checkForUpdate() {
|
|
|
1257
1166
|
cmdDiscussions(rest); break;
|
|
1258
1167
|
case 'summary':
|
|
1259
1168
|
cmdSummary(rest); break;
|
|
1260
|
-
case '
|
|
1261
|
-
|
|
1169
|
+
case 'stats':
|
|
1170
|
+
cmdStats(); break;
|
|
1262
1171
|
case 'install':
|
|
1263
1172
|
await cmdInstall(rest);
|
|
1264
1173
|
process.exit(0);
|
|
@@ -1284,6 +1193,8 @@ async function checkForUpdate() {
|
|
|
1284
1193
|
await cmdSettings(); break;
|
|
1285
1194
|
case 'add':
|
|
1286
1195
|
await cmdAdd(); break;
|
|
1196
|
+
case 'save':
|
|
1197
|
+
cmdSave(rest); break;
|
|
1287
1198
|
case 'delete': case 'del': case 'rm':
|
|
1288
1199
|
cmdDelete(rest); break;
|
|
1289
1200
|
case 'help': case '--help': case '-h':
|