context-mcp-server 1.0.8 → 1.1.1

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.
Files changed (81) hide show
  1. package/README.md +29 -7
  2. package/codegraph/__pycache__/affected.cpython-313.pyc +0 -0
  3. package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
  4. package/codegraph/__pycache__/callflow_html.cpython-313.pyc +0 -0
  5. package/codegraph/__pycache__/export.cpython-313.pyc +0 -0
  6. package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
  7. package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
  8. package/codegraph/__pycache__/tree_html.cpython-313.pyc +0 -0
  9. package/codegraph/affected.py +233 -0
  10. package/codegraph/cache.py +51 -2
  11. package/codegraph/callflow_html.py +273 -0
  12. package/codegraph/export.py +544 -0
  13. package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
  14. package/codegraph/extractors/ast_extractor.py +143 -16
  15. package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
  16. package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
  17. package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
  18. package/codegraph/graph/__pycache__/symbol_resolution.cpython-313.pyc +0 -0
  19. package/codegraph/graph/builder.py +10 -0
  20. package/codegraph/graph/clustering.py +247 -10
  21. package/codegraph/graph/query.py +99 -0
  22. package/codegraph/graph/symbol_resolution.py +112 -0
  23. package/codegraph/report.py +53 -0
  24. package/codegraph/server.py +112 -20
  25. package/codegraph/tree_html.py +241 -0
  26. package/package.json +2 -2
  27. package/pyproject.toml +4 -1
  28. package/src/cli.js +329 -227
  29. package/src/db.js +79 -102
  30. package/src/search.js +73 -9
  31. package/src/server.js +7 -1
  32. package/src/templates/antigravity/GEMINI.md +96 -0
  33. package/src/templates/antigravity/hooks/context-mcp-post-tool-use.js +62 -0
  34. package/src/templates/antigravity/workflows/context-resume.md +20 -0
  35. package/src/templates/antigravity/workflows/graph-build.md +23 -0
  36. package/src/templates/antigravity/workflows/save-context.md +29 -0
  37. package/src/templates/claude/CLAUDE.md +140 -0
  38. package/src/templates/claude/commands/graph-build.md +9 -0
  39. package/src/templates/claude/commands/save-context.md +19 -0
  40. package/src/templates/claude/hooks/context-mcp-post-tool-use.js +59 -0
  41. package/src/templates/claude/hooks/context-mcp-pre-tool-use.js +26 -0
  42. package/src/templates/claude/skills/SKILL.md +144 -0
  43. package/src/templates/codex/AGENTS.md +107 -0
  44. package/src/templates/codex/hooks/context-mcp-post-tool-use.js +46 -0
  45. package/src/templates/codex/hooks/context-mcp-pre-tool-use.js +23 -0
  46. package/src/templates/codex/prompts/context-resume.md +15 -0
  47. package/src/templates/codex/prompts/graph-build.md +14 -0
  48. package/src/templates/codex/prompts/save-context.md +24 -0
  49. package/src/templates/cursor/commands/context-resume.md +7 -0
  50. package/src/templates/cursor/commands/graph-build.md +7 -0
  51. package/src/templates/cursor/commands/save-context.md +12 -0
  52. package/src/templates/{cursor-rules.mdc → cursor/cursor-rules.mdc} +13 -3
  53. package/src/templates/cursor/hooks/context-mcp-post-tool-use.js +55 -0
  54. package/src/templates/gemini/GEMINI.md +92 -0
  55. package/src/templates/gemini/commands/context-resume.toml +15 -0
  56. package/src/templates/gemini/commands/graph-build.toml +14 -0
  57. package/src/templates/gemini/commands/save-context.toml +24 -0
  58. package/src/templates/gemini/hooks/context-mcp-after-tool.js +59 -0
  59. package/src/templates/gemini/hooks/context-mcp-before-tool.js +26 -0
  60. package/src/templates/vscode/commands/context-resume.prompt.md +15 -0
  61. package/src/templates/vscode/commands/graph-build.prompt.md +10 -0
  62. package/src/templates/vscode/commands/save-context.prompt.md +16 -0
  63. package/src/templates/vscode/hooks/context-mcp-post-tool-use.js +58 -0
  64. package/src/templates/windsurf/hooks/context-mcp-post-run-command.js +57 -0
  65. package/src/templates/windsurf/windsurf-rules.md +86 -0
  66. package/src/templates/windsurf/workflows/context-resume.md +11 -0
  67. package/src/templates/windsurf/workflows/graph-build.md +11 -0
  68. package/src/templates/windsurf/workflows/save-context.md +18 -0
  69. package/src/tools/codegraph.js +83 -43
  70. package/src/tools/context.js +42 -24
  71. package/src/tools/plan.js +14 -11
  72. package/uv.lock +1101 -4
  73. package/src/migrator.js +0 -124
  74. package/src/templates/AGENTS.md +0 -80
  75. package/src/templates/CLAUDE.md +0 -103
  76. package/src/templates/GEMINI.md +0 -80
  77. package/src/templates/commands/graph-build.md +0 -5
  78. package/src/templates/commands/save-context.md +0 -9
  79. package/src/templates/skills/SKILL.md +0 -108
  80. package/src/templates/windsurf-rules.md +0 -35
  81. /package/src/templates/{commands → claude/commands}/context-resume.md +0 -0
package/src/cli.js CHANGED
@@ -107,45 +107,28 @@ function printSection(title, meta = '') {
107
107
  function printUsage() {
108
108
  printBanner();
109
109
 
110
- // Terminal commands (ctx / context ...)
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 benchmark', 'token savings report (memory + graph)');
123
122
  console.log('');
124
- cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps');
125
- cmd('ctx install --<platform>', 'write MCP config + skill/rules for an AI platform');
126
- cmd('ctx install --all', 'install for all platforms at once');
123
+ cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps only');
124
+ cmd('ctx install --<platform>', 'write MCP config + skill/rules file only (no uv/npm)');
125
+ cmd('ctx install --all', 'write config + skill files for all platforms');
127
126
  cmd('ctx online [--port N]', 'start HTTP server for Claude.ai / ChatGPT');
128
127
  cmd('ctx online --close', 'stop the running HTTP server');
129
128
  cmd('ctx settings', 'view and edit config (port, host, client id/secret)');
130
129
  cmd('ctx update', 'check for and apply latest version');
131
130
  cmd('ctx help', 'show this screen');
132
131
  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
132
  }
150
133
  function clearScreen() {
151
134
  // \x1b[2J = clear screen, \x1b[3J = clear scrollback, \x1b[H = cursor home
@@ -155,7 +138,8 @@ function clearScreen() {
155
138
  // ── List (grouped by project) ─────────────────────────────────────────────────
156
139
 
157
140
  function cmdList(args) {
158
- const filterProject = args[0];
141
+ const projectFlagIdx = args.indexOf('--project');
142
+ const filterProject = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
159
143
  const entries = getContext({ project: filterProject, limit: 100 });
160
144
  const allDiscussions = listDiscussions({ project: filterProject });
161
145
  const allGraphs = listGraphs();
@@ -163,7 +147,6 @@ function cmdList(args) {
163
147
 
164
148
  printSection('Context', filterProject ? `project: ${filterProject}` : 'all projects');
165
149
 
166
- // Build per-project map, split entries into their three trees
167
150
  const projects = {};
168
151
  const ensureProj = p => {
169
152
  if (!projects[p]) projects[p] = { context: [], summary: [], plans: [] };
@@ -213,13 +196,12 @@ function cmdList(args) {
213
196
  const id = item.id.slice(0, 8);
214
197
  const tags = safeTags(item.tags);
215
198
  const pipe = secIsLast ? ' ' : '│';
216
- console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${pill(item.type || 'note')} ${bold(item.title || '(no title)')} ${faint('id:' + id)} ${faint(date)}`);
199
+ console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${bold(item.title || '(no title)')} ${faint('id:' + id)} ${faint(date)}`);
217
200
  if (tags.length) console.log(` ${color(C.darkgray, pipe)} ${faint(tags.map(t => '#' + t).join(' '))}`);
218
201
  });
219
202
  if (!secIsLast) console.log(` ${color(C.darkgray, '│')}`);
220
203
  };
221
204
 
222
- // ── Graph (build stats only) ──────────────────────────────────────────────
223
205
  if (graphBuild) {
224
206
  secIdx++;
225
207
  const isLast = secIdx === sections.length;
@@ -228,13 +210,11 @@ function cmdList(args) {
228
210
  if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
229
211
  }
230
212
 
231
- // ── Context ───────────────────────────────────────────────────────────────
232
213
  if (pData.context.length) {
233
214
  secIdx++;
234
215
  renderEntries(pData.context, 'context', secIdx === sections.length);
235
216
  }
236
217
 
237
- // ── Summary (compaction digests only — no type labels) ───────────────────
238
218
  if (pData.summary.length) {
239
219
  secIdx++;
240
220
  const isLast = secIdx === sections.length;
@@ -248,7 +228,6 @@ function cmdList(args) {
248
228
  if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
249
229
  }
250
230
 
251
- // ── Plans ─────────────────────────────────────────────────────────────────
252
231
  if (pData.plans.length) {
253
232
  secIdx++;
254
233
  const isLast = secIdx === sections.length;
@@ -285,7 +264,7 @@ function cmdSearch(args) {
285
264
  const id = entry.id.slice(0, 8);
286
265
  const type = entry.type || 'note';
287
266
  const isLast = index === results.length - 1;
288
- console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${bold(entry.title || '(no title)')}${score} ${pill(type)} ${faint('id:' + id)} ${faint(date)}`);
267
+ console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${bold(entry.title || '(no title)')}${score} ${faint('id:' + id)} ${faint(date)}`);
289
268
  });
290
269
  console.log('');
291
270
  console.log(line());
@@ -313,7 +292,6 @@ function cmdProjects() {
313
292
  console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(project.name))}${idTag} ${bar} ${faint(project.count + ' entries')}`);
314
293
  console.log(` ${color(C.darkgray, '│')}`);
315
294
 
316
- // Graph status
317
295
  if (graph) {
318
296
  const builtAt = (graph.builtAt || '').slice(0, 10);
319
297
  console.log(` ${color(C.darkgray, '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graph.nodes}n · ${graph.edges}e · ${graph.communities} clusters · ${builtAt}`)}`);
@@ -321,18 +299,15 @@ function cmdProjects() {
321
299
  console.log(` ${color(C.darkgray, '├─')} ${faint('⬡ no graph')}`);
322
300
  }
323
301
 
324
- // Recent context
325
302
  if (entries.length) {
326
303
  console.log(` ${color(C.darkgray, '├─')} ${muted('recent')}`);
327
304
  entries.forEach((e, i) => {
328
305
  const br = i === entries.length - 1 && !activeD.length ? '└─' : '├─';
329
- const type = e.type || 'note';
330
306
  const date = (e.createdAt || '').slice(0, 10);
331
- console.log(` ${color(C.darkgray, '│')} ${color(C.darkgray, br)} ${pill(type)} ${bold(e.title || '(no title)')} ${faint(date)}`);
307
+ console.log(` ${color(C.darkgray, '│')} ${color(C.darkgray, br)} ${bold(e.title || '(no title)')} ${faint(date)}`);
332
308
  });
333
309
  }
334
310
 
335
- // Active discussions
336
311
  if (activeD.length) {
337
312
  console.log(` ${color(C.darkgray, '├─')} ${muted('discussions')}`);
338
313
  activeD.forEach((d, i) => {
@@ -354,7 +329,8 @@ function cmdProjects() {
354
329
  // ── Discussions ───────────────────────────────────────────────────────────────
355
330
 
356
331
  function cmdDiscussions(args) {
357
- const filterProject = args[0];
332
+ const projectFlagIdx = args.indexOf('--project');
333
+ const filterProject = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
358
334
  const discussions = listDiscussions({ project: filterProject });
359
335
  printSection('Discussions', filterProject || 'all projects');
360
336
 
@@ -392,7 +368,8 @@ function cmdDiscussions(args) {
392
368
  // ── Summary ───────────────────────────────────────────────────────────────────
393
369
 
394
370
  function cmdSummary(args) {
395
- const project = args[0];
371
+ const projectFlagIdx = args.indexOf('--project');
372
+ const project = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
396
373
  const entries = getContext({ project, limit: 50 });
397
374
  printSection('Summary', project || 'global');
398
375
  if (!entries.length) { console.log(` ${faint('no entries to summarize')}`); return; }
@@ -408,143 +385,6 @@ function cmdSummary(args) {
408
385
  console.log('');
409
386
  }
410
387
 
411
- // ── Benchmark ────────────────────────────────────────────────────────────────
412
-
413
-
414
- function _walkBytes(dirPath) {
415
- const walkScript =
416
- `const fs=require('fs'),path=require('path');` +
417
- `function walk(d,t=0){try{for(const f of fs.readdirSync(d)){` +
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
- }
457
-
458
- // ── Run graph queries once, reuse in both sections ────────────────────────────
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
- }
465
-
466
- // ── Corpus size (run once) ────────────────────────────────────────────────────
467
- let corpusToks = null;
468
- if (graphs.length) {
469
- const bytes = _walkBytes(graphs[0].path);
470
- if (bytes) corpusToks = Math.round(bytes / 4);
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('');
493
- for (const p of projects) {
494
- const pToks = p.count * avgFullTok;
495
- const barLen = Math.min(Math.ceil(p.count / 2), 24);
496
- const bar = color(C.dblue, '█'.repeat(barLen)) + color(C.darkgray, '░'.repeat(24 - barLen));
497
- console.log(` ${color(C.darkgray, '·')} ${muted(p.name.padEnd(22))} ${bar} ${faint(p.count + ' entries · ~' + pToks.toLocaleString('en-US') + ' tok full')}`);
498
- }
499
- } else {
500
- console.log(` ${faint('no entries yet')}`);
501
- }
502
-
503
- // ── CodeGraph ─────────────────────────────────────────────────────────────────
504
- // Without context-mcp: AI reads all source files to answer structural questions
505
- // With context-mcp: AI calls codegraph_query → focused NODE/EDGE subgraph
506
- console.log(`\n ${bold(lblue('CodeGraph'))} ${faint('measured from live graph queries')}`);
507
- if (!graphs.length) {
508
- console.log(` ${faint('no graphs — run codegraph_build first')}`);
509
- } else {
510
- for (const g of graphs) {
511
- const pathShort = g.path.replace(/\\/g, '/').split('/').slice(-2).join('/');
512
- const builtAt = (g.builtAt || '').slice(0, 10);
513
- const graphReduce = corpusToks ? (corpusToks / avgQueryTok).toFixed(0) + '×' : null;
514
- const graphPct = corpusToks ? ((1 - avgQueryTok / corpusToks) * 100).toFixed(2) : null;
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`);
521
- }
522
- }
523
-
524
- // ── Combined ──────────────────────────────────────────────────────────────────
525
- // Without: read all files (no graph) + load all entries full (no memory system)
526
- // With: compact resume (memory) + one graph query (codegraph)
527
- if (graphs.length) {
528
- const resumeCount = Math.min(RESUME_LIMIT, totalEntries);
529
- const withMemTok = resumeCount * avgCompactTok;
530
- const withMcp = withMemTok + avgQueryTok;
531
- const withoutMemTok = totalEntries * avgFullTok;
532
- const withoutMcp = withoutMemTok + (corpusToks || graphs[0].nodes * 80);
533
- const totalRed = withoutMcp > 0 ? (withoutMcp / withMcp).toFixed(0) : '—';
534
- const totalPct = withoutMcp > 0 ? ((1 - withMcp / withoutMcp) * 100).toFixed(2) : '—';
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
-
542
- 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
- }
546
-
547
-
548
388
  // ── Install ───────────────────────────────────────────────────────────────────
549
389
 
550
390
  const TPLS = join(__dirname, 'templates');
@@ -574,6 +414,7 @@ const _GLOBAL_GITIGNORE_ENTRIES = [
574
414
  '.gemini/',
575
415
  '.codex/',
576
416
  '.windsurf/',
417
+ '.agents/',
577
418
  // Build outputs and session artifacts
578
419
  'codegraph-cache/',
579
420
  '.mcp.json',
@@ -588,6 +429,7 @@ function _graphForProject(graphs, projectName) {
588
429
 
589
430
  const _PROJECT_GITIGNORE_ENTRIES = [
590
431
  '.claude/', '.cursor/', '.vscode/', '.gemini/', '.codex/',
432
+ '.agents/',
591
433
  'codegraph-cache/', '.mcp.json', 'CLAUDE.md', 'GEMINI.md', 'AGENTS.md',
592
434
  ];
593
435
 
@@ -630,7 +472,7 @@ function _updateGlobalGitignore() {
630
472
  }
631
473
 
632
474
  function _writeCommands(baseDir) {
633
- const cmdsDir = join(TPLS, 'commands');
475
+ const cmdsDir = join(TPLS, 'claude', 'commands');
634
476
  const destDir = join(baseDir, '.claude', 'commands');
635
477
  for (const [name, label] of [
636
478
  ['context-resume.md', '/context-resume'],
@@ -646,20 +488,136 @@ function _writeCommands(baseDir) {
646
488
 
647
489
  const MCP_SERVER_CMD = { command: 'npx', args: ['-y', 'context-mcp-server@latest'] };
648
490
 
491
+ function _tomlString(value) {
492
+ return JSON.stringify(value);
493
+ }
494
+
495
+ function _copyHooks(platform, dotDir, dir, hookFiles) {
496
+ const hooksSrc = join(TPLS, platform, 'hooks');
497
+ const hooksDest = join(dir, dotDir, 'hooks');
498
+ for (const file of hookFiles) {
499
+ const src = join(hooksSrc, file);
500
+ if (existsSync(src)) {
501
+ _writeFile(join(hooksDest, file), readFileSync(src, 'utf8'), `${dotDir}/hooks/${file}`);
502
+ }
503
+ }
504
+ }
505
+
506
+ function _copyCodexHooks(dir) {
507
+ _copyHooks('codex', '.codex', dir, [
508
+ 'context-mcp-pre-tool-use.js',
509
+ 'context-mcp-post-tool-use.js',
510
+ ]);
511
+ }
512
+
513
+ // Read JSON from path (returns {} on missing/invalid), run mutateFn, write back.
514
+ function _mergeJsonFile(filePath, label, mutateFn) {
515
+ let obj = {};
516
+ try { obj = JSON.parse(readFileSync(filePath, 'utf8')); } catch {}
517
+ mutateFn(obj);
518
+ _writeFile(filePath, JSON.stringify(obj, null, 2), label);
519
+ return obj;
520
+ }
521
+
522
+ // Merge `entries` ({ EventName: [hookGroup] }) into an existing settings.json
523
+ // hooks object, replacing any previously installed context-mcp groups.
524
+ function _mergeHooksIntoSettings(settingsPath, entries, label) {
525
+ let settings = {};
526
+ try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch {}
527
+ settings.hooks = settings.hooks || {};
528
+ for (const [event, groups] of Object.entries(entries)) {
529
+ const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
530
+ settings.hooks[event] = existing
531
+ .filter(g => !(g.hooks || []).some(h => String(h.command || '').includes('context-mcp-')))
532
+ .concat(groups);
533
+ }
534
+ _writeFile(settingsPath, JSON.stringify(settings, null, 2), label);
535
+ return settings;
536
+ }
537
+
538
+ function _codexConfigToml(dir, includeHooks = false) {
539
+ const lines = [
540
+ '[mcp_servers.context-mcp]',
541
+ 'command = "npx"',
542
+ 'args = ["-y", "context-mcp-server@latest"]',
543
+ 'default_tools_approval_mode = "prompt"',
544
+ '',
545
+ '[mcp_servers.context-mcp.tools.context]',
546
+ 'approval_mode = "approve"',
547
+ '',
548
+ '[mcp_servers.context-mcp.tools.search]',
549
+ 'approval_mode = "approve"',
550
+ '',
551
+ '[mcp_servers.context-mcp.tools.codegraph_query]',
552
+ 'approval_mode = "approve"',
553
+ ];
554
+
555
+ if (includeHooks) {
556
+ const preHook = join(dir, '.codex', 'hooks', 'context-mcp-pre-tool-use.js');
557
+ const postHook = join(dir, '.codex', 'hooks', 'context-mcp-post-tool-use.js');
558
+ lines.push(
559
+ '',
560
+ '[[hooks.PreToolUse]]',
561
+ 'matcher = "^Bash$"',
562
+ '',
563
+ '[[hooks.PreToolUse.hooks]]',
564
+ 'type = "command"',
565
+ `command = ${_tomlString(`node ${_tomlString(preHook)}`)}`,
566
+ `command_windows = ${_tomlString(`node ${_tomlString(preHook)}`)}`,
567
+ 'timeout = 30',
568
+ 'statusMessage = "Checking shell command"',
569
+ '',
570
+ '[[hooks.PostToolUse]]',
571
+ 'matcher = "^Bash$"',
572
+ '',
573
+ '[[hooks.PostToolUse.hooks]]',
574
+ 'type = "command"',
575
+ `command = ${_tomlString(`node ${_tomlString(postHook)}`)}`,
576
+ `command_windows = ${_tomlString(`node ${_tomlString(postHook)}`)}`,
577
+ 'timeout = 30',
578
+ 'statusMessage = "Saving failed shell context"',
579
+ );
580
+ }
581
+
582
+ return `${lines.join('\n')}\n`;
583
+ }
584
+
649
585
  const PLATFORMS = {
650
586
  claude: {
651
587
  label: 'Claude Code',
652
588
  restartNote: 'Type /context-resume in Claude Code to start using context-mcp.',
653
589
  install(dir, scope) {
654
- // Install as a skill (global, works across all projects) instead of a flat CLAUDE.md
655
- const skillSrc = join(TPLS, 'skills', 'SKILL.md');
590
+ // Skill user-global, works across all projects
591
+ const skillSrc = join(TPLS, 'claude', 'skills', 'SKILL.md');
656
592
  const skillDest = join(homedir(), '.claude', 'skills', 'context-mcp', 'SKILL.md');
657
593
  if (existsSync(skillSrc)) {
658
594
  _writeFile(skillDest, readFileSync(skillSrc, 'utf8'), '~/.claude/skills/context-mcp/');
659
595
  }
660
- // Slash commands are always user-global (not project-scoped)
596
+ // Slash commands user-global
661
597
  _writeCommands(homedir());
662
- // Register via claude CLI so the server is trusted immediately (no manual trust prompt)
598
+ // Hooks write into the appropriate settings.json scope
599
+ // Project hooks live in .claude/hooks/ and are committed; user hooks in ~/.claude/hooks/
600
+ const hooksBase = scope === 'project' ? dir : homedir();
601
+ _copyHooks('claude', '.claude', hooksBase, [
602
+ 'context-mcp-pre-tool-use.js',
603
+ 'context-mcp-post-tool-use.js',
604
+ ]);
605
+ const preHook = join(hooksBase, '.claude', 'hooks', 'context-mcp-pre-tool-use.js');
606
+ const postHook = join(hooksBase, '.claude', 'hooks', 'context-mcp-post-tool-use.js');
607
+ const settingsPath = scope === 'project'
608
+ ? join(dir, '.claude', 'settings.json')
609
+ : join(homedir(), '.claude', 'settings.json');
610
+ _mergeHooksIntoSettings(settingsPath, {
611
+ PreToolUse: [{
612
+ matcher: 'Bash',
613
+ hooks: [{ type: 'command', command: preHook, timeout: 30, statusMessage: 'Checking shell command' }],
614
+ }],
615
+ PostToolUse: [{
616
+ matcher: 'Bash',
617
+ hooks: [{ type: 'command', command: postHook, timeout: 30, statusMessage: 'Saving failed shell context' }],
618
+ }],
619
+ }, scope === 'project' ? '.claude/settings.json' : '~/.claude/settings.json');
620
+ // Register MCP server via claude CLI
663
621
  const scopeFlag = scope === 'global' ? 'user' : 'project';
664
622
  const reg = spawnSync(
665
623
  'claude', ['mcp', 'add', '--scope', scopeFlag, 'context-mcp', '--', 'npx', '-y', 'context-mcp-server@latest'],
@@ -676,17 +634,34 @@ const PLATFORMS = {
676
634
  label: 'Cursor',
677
635
  restartNote: 'Restart Cursor to load the new MCP server.',
678
636
  install(dir, scope) {
637
+ // MCP config
679
638
  const mcpJson = JSON.stringify({ mcpServers: { 'context-mcp': MCP_SERVER_CMD } }, null, 2);
680
639
  _writeFile(join(dir, '.cursor', 'mcp.json'), mcpJson, '.cursor/mcp.json');
681
640
  if (scope === 'project') {
682
- const mdc = _tpl('cursor-rules.mdc');
641
+ // Rules
642
+ const mdc = _tpl('cursor/cursor-rules.mdc');
683
643
  if (mdc) _writeFile(join(dir, '.cursor', 'rules', 'context-mcp.mdc'), mdc, '.cursor/rules/context-mcp.mdc');
644
+ // Slash commands
645
+ const cmdsSrc = join(TPLS, 'cursor', 'commands');
646
+ for (const file of ['context-resume.md', 'graph-build.md', 'save-context.md']) {
647
+ const src = join(cmdsSrc, file);
648
+ if (existsSync(src)) _writeFile(join(dir, '.cursor', 'commands', file), readFileSync(src, 'utf8'), `.cursor/commands/${file}`);
649
+ }
650
+ // Hook script
651
+ _copyHooks('cursor', '.cursor', dir, ['context-mcp-post-tool-use.js']);
652
+ const hookPath = join(dir, '.cursor', 'hooks', 'context-mcp-post-tool-use.js');
653
+ // Merge into .cursor/hooks.json (must keep "version": 1)
654
+ _mergeJsonFile(join(dir, '.cursor', 'hooks.json'), '.cursor/hooks.json', obj => {
655
+ obj.version = 1;
656
+ obj.hooks = obj.hooks || {};
657
+ const strip = arr => (arr || []).filter(h => !String(h.command || '').includes('context-mcp-'));
658
+ obj.hooks.postToolUse = strip(obj.hooks.postToolUse).concat([
659
+ { command: `node "${hookPath}"`, timeout: 30 },
660
+ ]);
661
+ });
684
662
  }
685
- // Try to enable via cursor CLI (enable/disable only — no add command)
686
- const reg = spawnSync(
687
- 'cursor', ['agent', 'mcp', 'enable', 'context-mcp'],
688
- { encoding: 'utf8', shell: true },
689
- );
663
+ // Try to enable via cursor CLI
664
+ const reg = spawnSync('cursor', ['agent', 'mcp', 'enable', 'context-mcp'], { encoding: 'utf8', shell: true });
690
665
  if (reg.status === 0) {
691
666
  console.log(` ${ok('✓')} ${'enabled via cursor agent mcp'.padEnd(28)}`);
692
667
  }
@@ -695,21 +670,79 @@ const PLATFORMS = {
695
670
  vscode: {
696
671
  label: 'VS Code Copilot',
697
672
  restartNote: 'Reload VS Code window (Ctrl+Shift+P → "Reload Window").',
698
- install(dir) {
673
+ install(dir, scope) {
674
+ // MCP config
699
675
  const mcpJson = JSON.stringify({
700
676
  servers: { 'context-mcp': { type: 'stdio', ...MCP_SERVER_CMD } },
701
677
  }, null, 2);
702
678
  _writeFile(join(dir, '.vscode', 'mcp.json'), mcpJson, '.vscode/mcp.json');
679
+ if (scope === 'project') {
680
+ // Prompt files (.github/prompts/)
681
+ const cmdsSrc = join(TPLS, 'vscode', 'commands');
682
+ for (const file of ['context-resume.prompt.md', 'graph-build.prompt.md', 'save-context.prompt.md']) {
683
+ const src = join(cmdsSrc, file);
684
+ if (existsSync(src)) _writeFile(join(dir, '.github', 'prompts', file), readFileSync(src, 'utf8'), `.github/prompts/${file}`);
685
+ }
686
+ // Hook script
687
+ _copyHooks('vscode', '.vscode', dir, ['context-mcp-post-tool-use.js']);
688
+ const hookPath = join(dir, '.vscode', 'hooks', 'context-mcp-post-tool-use.js');
689
+ // Merge hooks into .vscode/settings.json under github.copilot.chat.agent.hooks
690
+ _mergeJsonFile(join(dir, '.vscode', 'settings.json'), '.vscode/settings.json', obj => {
691
+ const hooks = obj['github.copilot.chat.agent.hooks'] || {};
692
+ const strip = arr => (arr || []).filter(h => !String(h.command || '').includes('context-mcp-'));
693
+ hooks.PostToolUse = strip(hooks.PostToolUse).concat([
694
+ { type: 'command', command: `node "${hookPath}"`, timeout: 30, windows: `node "${hookPath}"` },
695
+ ]);
696
+ obj['github.copilot.chat.agent.hooks'] = hooks;
697
+ });
698
+ }
703
699
  },
704
700
  },
705
701
  gemini: {
706
702
  label: 'Gemini CLI',
707
703
  restartNote: 'Restart your Gemini CLI session.',
708
704
  install(dir, scope) {
709
- const cfg = JSON.stringify({ mcpServers: { 'context-mcp': MCP_SERVER_CMD } }, null, 2);
710
- _writeFile(join(dir, '.gemini', 'settings.json'), cfg, '.gemini/settings.json');
705
+ // MCP server config merged into existing settings.json
706
+ const settingsPath = scope === 'project'
707
+ ? join(dir, '.gemini', 'settings.json')
708
+ : join(homedir(), '.gemini', 'settings.json');
709
+ let settings = {};
710
+ try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch {}
711
+ settings.mcpServers = settings.mcpServers || {};
712
+ settings.mcpServers['context-mcp'] = MCP_SERVER_CMD;
713
+ // Hooks — write BeforeTool/AfterTool into settings.json
714
+ const hooksBase = scope === 'project' ? dir : homedir();
715
+ _copyHooks('gemini', '.gemini', hooksBase, [
716
+ 'context-mcp-before-tool.js',
717
+ 'context-mcp-after-tool.js',
718
+ ]);
719
+ const beforeHook = join(hooksBase, '.gemini', 'hooks', 'context-mcp-before-tool.js');
720
+ const afterHook = join(hooksBase, '.gemini', 'hooks', 'context-mcp-after-tool.js');
721
+ settings.hooks = settings.hooks || {};
722
+ const stripOld = (arr) => (arr || []).filter(
723
+ g => !(g.hooks || []).some(h => String(h.command || '').includes('context-mcp-')),
724
+ );
725
+ settings.hooks.BeforeTool = stripOld(settings.hooks.BeforeTool).concat([{
726
+ matcher: 'run_shell_command',
727
+ hooks: [{ type: 'command', command: `node ${beforeHook}`, timeout: 30 }],
728
+ }]);
729
+ settings.hooks.AfterTool = stripOld(settings.hooks.AfterTool).concat([{
730
+ matcher: 'run_shell_command',
731
+ hooks: [{ type: 'command', command: `node ${afterHook}`, timeout: 30 }],
732
+ }]);
733
+ _writeFile(settingsPath,
734
+ JSON.stringify(settings, null, 2),
735
+ scope === 'project' ? '.gemini/settings.json' : '~/.gemini/settings.json',
736
+ );
737
+ // Slash commands (.toml) — project-scoped only
711
738
  if (scope === 'project') {
712
- const md = _tpl('GEMINI.md');
739
+ const cmdsSrc = join(TPLS, 'gemini', 'commands');
740
+ const cmdsDest = join(dir, '.gemini', 'commands');
741
+ for (const file of ['context-resume.toml', 'graph-build.toml', 'save-context.toml']) {
742
+ const src = join(cmdsSrc, file);
743
+ if (existsSync(src)) _writeFile(join(cmdsDest, file), readFileSync(src, 'utf8'), `.gemini/commands/${file}`);
744
+ }
745
+ const md = _tpl('gemini/GEMINI.md');
713
746
  if (md) _writeFile(join(dir, 'GEMINI.md'), md, 'GEMINI.md');
714
747
  }
715
748
  },
@@ -718,12 +751,21 @@ const PLATFORMS = {
718
751
  label: 'Codex CLI',
719
752
  restartNote: 'Restart your Codex CLI session.',
720
753
  install(dir, scope) {
721
- const toml = `[[mcp_servers]]\nname = "context-mcp"\ncommand = "npx"\nargs = ["-y", "context-mcp-server@latest"]\n`;
722
- _writeFile(join(dir, '.codex', 'config.toml'), toml, '.codex/config.toml');
754
+ const includeHooks = scope === 'project';
755
+ if (includeHooks) _copyCodexHooks(dir);
756
+ _writeFile(join(dir, '.codex', 'config.toml'), _codexConfigToml(dir, includeHooks), '.codex/config.toml');
723
757
  if (scope === 'project') {
724
- const md = _tpl('AGENTS.md');
758
+ const md = _tpl('codex/AGENTS.md');
725
759
  if (md) _writeFile(join(dir, 'AGENTS.md'), md, 'AGENTS.md');
726
760
  }
761
+ // Prompts (slash commands) — always user-global; Codex only loads ~/.codex/prompts/
762
+ const promptsSrc = join(TPLS, 'codex', 'prompts');
763
+ for (const file of ['context-resume.md', 'graph-build.md', 'save-context.md']) {
764
+ const src = join(promptsSrc, file);
765
+ if (existsSync(src)) {
766
+ _writeFile(join(homedir(), '.codex', 'prompts', file), readFileSync(src, 'utf8'), `~/.codex/prompts/${file}`);
767
+ }
768
+ }
727
769
  // Register via codex CLI so server is active immediately
728
770
  const reg = spawnSync(
729
771
  'codex', ['mcp', 'add', 'context-mcp', '--', 'npx', '-y', 'context-mcp-server@latest'],
@@ -739,15 +781,63 @@ const PLATFORMS = {
739
781
  windsurf: {
740
782
  label: 'Windsurf',
741
783
  restartNote: 'Restart Windsurf to load the updated MCP config.',
742
- install(dir) {
743
- const rules = _tpl('windsurf-rules.md');
784
+ install(dir, scope) {
785
+ // Rules file
786
+ const rules = _tpl('windsurf/windsurf-rules.md');
744
787
  if (rules) _writeFile(join(dir, '.windsurf', 'rules', 'context-mcp.md'), rules, '.windsurf/rules/context-mcp.md');
745
- const globalCfgPath = join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
746
- let existing = {};
747
- try { existing = JSON.parse(readFileSync(globalCfgPath, 'utf8')); } catch {}
748
- existing.mcpServers = existing.mcpServers || {};
749
- existing.mcpServers['context-mcp'] = { command: 'npx', args: ['-y', 'context-mcp-server@latest'] };
750
- _writeFile(globalCfgPath, JSON.stringify(existing, null, 2), '~/.codeium/windsurf/mcp_config.json');
788
+ // Global MCP config (~/.codeium/windsurf/mcp_config.json)
789
+ _mergeJsonFile(join(homedir(), '.codeium', 'windsurf', 'mcp_config.json'), '~/.codeium/windsurf/mcp_config.json', obj => {
790
+ obj.mcpServers = obj.mcpServers || {};
791
+ obj.mcpServers['context-mcp'] = { command: 'npx', args: ['-y', 'context-mcp-server@latest'] };
792
+ });
793
+ if (scope === 'project') {
794
+ // Workflows (slash commands)
795
+ const wfSrc = join(TPLS, 'windsurf', 'workflows');
796
+ for (const file of ['context-resume.md', 'graph-build.md', 'save-context.md']) {
797
+ const src = join(wfSrc, file);
798
+ if (existsSync(src)) _writeFile(join(dir, '.windsurf', 'workflows', file), readFileSync(src, 'utf8'), `.windsurf/workflows/${file}`);
799
+ }
800
+ // Hook script + .windsurf/hooks.json
801
+ _copyHooks('windsurf', '.windsurf', dir, ['context-mcp-post-run-command.js']);
802
+ const hookPath = join(dir, '.windsurf', 'hooks', 'context-mcp-post-run-command.js');
803
+ _mergeJsonFile(join(dir, '.windsurf', 'hooks.json'), '.windsurf/hooks.json', obj => {
804
+ obj.hooks = obj.hooks || {};
805
+ const strip = arr => (arr || []).filter(h => !String(h.command || '').includes('context-mcp-'));
806
+ obj.hooks.post_run_command = strip(obj.hooks.post_run_command).concat([{
807
+ command: `node "${hookPath}"`,
808
+ powershell: `node "${hookPath}"`,
809
+ show_output: false,
810
+ }]);
811
+ });
812
+ }
813
+ },
814
+ },
815
+ antigravity: {
816
+ label: 'Antigravity IDE',
817
+ restartNote: 'Restart your Antigravity session to pick up hooks and rules.',
818
+ install(dir, scope) {
819
+ // Antigravity uses stdio-incompatible MCP transport — integrate via ctx CLI + GEMINI.md instead.
820
+ if (scope === 'project') {
821
+ // Post-tool hook — saves failed tool calls to context-mcp via ctx CLI
822
+ _copyHooks('antigravity', '.agents', dir, ['context-mcp-post-tool-use.js']);
823
+ const hookPath = join(dir, '.agents', 'hooks', 'context-mcp-post-tool-use.js');
824
+ _mergeJsonFile(join(dir, '.agents', 'hooks.json'), '.agents/hooks.json', obj => {
825
+ const strip = arr => (arr || []).filter(h => !String(h.command || '').includes('context-mcp-'));
826
+ obj.hooks = strip(obj.hooks).concat([{
827
+ event: 'PostToolUse',
828
+ command: `node "${hookPath}"`,
829
+ }]);
830
+ });
831
+ // Workflows (slash commands) — .agents/workflows/
832
+ const wfSrc = join(TPLS, 'antigravity', 'workflows');
833
+ for (const file of ['context-resume.md', 'graph-build.md', 'save-context.md']) {
834
+ const src = join(wfSrc, file);
835
+ if (existsSync(src)) _writeFile(join(dir, '.agents', 'workflows', file), readFileSync(src, 'utf8'), `.agents/workflows/${file}`);
836
+ }
837
+ // Rules file — Antigravity reads GEMINI.md at project root
838
+ const md = _tpl('antigravity/GEMINI.md');
839
+ if (md) _writeFile(join(dir, 'GEMINI.md'), md, 'GEMINI.md');
840
+ }
751
841
  },
752
842
  },
753
843
  };
@@ -796,13 +886,22 @@ async function cmdInstall(args) {
796
886
  console.log(` ${ok('✓')} Python environment ready — codegraph enabled`);
797
887
  }
798
888
  }
889
+ // Bootstrap store structure — creates ~/.context-mcp/, projects/, contextconfig.json
890
+ console.log(` ${bold(lblue('Store'))}`);
891
+ try {
892
+ getConfig(); // triggers DATA_DIR creation + contextconfig.json generation
893
+ console.log(` ${ok('✓')} store ready ${faint(getStorePath())}`);
894
+ console.log(` ${ok('✓')} config ready ${faint(getConfigPath())}`);
895
+ } catch (e) {
896
+ console.log(` ${bad('✗')} store init failed: ${faint(e.message)}`);
897
+ }
799
898
  console.log('');
800
899
  return;
801
900
  }
802
901
 
803
902
  if (!keys.length) {
804
903
  printSection('Install');
805
- console.log(` ${muted('Usage:')} ctx install ${faint('[--initial] [--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--all]')}`);
904
+ console.log(` ${muted('Usage:')} ctx install ${faint('[--initial] [--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--antigravity] [--all]')}`);
806
905
  console.log('');
807
906
  console.log(` ${accent('--initial ')} ${faint('Install / update Node.js + Python (codegraph) deps')}`);
808
907
  console.log('');
@@ -871,27 +970,6 @@ async function cmdInstall(args) {
871
970
  // ── Global gitignore — add context-mcp runtime files if global gitignore exists ──
872
971
  _updateGlobalGitignore();
873
972
  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
973
  }
896
974
 
897
975
  // ── Online ────────────────────────────────────────────────────────────────────
@@ -1077,7 +1155,7 @@ async function cmdAdd(existingRl) {
1077
1155
  const content = await ask('Content:');
1078
1156
  const project = await ask('Project (blank = global):');
1079
1157
  const tagsRaw = await ask('Tags (comma-separated):');
1080
- const type = await ask('Type (note/decision/code/bug/architecture/config/error):');
1158
+ const type = await ask('Type (note/compaction):');
1081
1159
 
1082
1160
  if (!existingRl) rl.close();
1083
1161
  if (!content.trim()) { console.log(` ${bad('✗')} content required`); return; }
@@ -1094,6 +1172,30 @@ async function cmdAdd(existingRl) {
1094
1172
  console.log(` ${ok('✓')} ${bold(entry.title || '(no title)')} ${faint('id:' + entry.id.slice(0, 8))}`);
1095
1173
  }
1096
1174
 
1175
+ // ── Save (non-interactive, flag-based — used by hooks and scripts) ───────────
1176
+
1177
+ function cmdSave(args) {
1178
+ // Usage: ctx save --title "..." --content "..." --project <p> --type <t> --tags <t1,t2>
1179
+ const flags = {};
1180
+ for (let i = 0; i < args.length; i++) {
1181
+ if (args[i].startsWith('--')) {
1182
+ flags[args[i].slice(2)] = args[i + 1] || '';
1183
+ i++;
1184
+ }
1185
+ }
1186
+ const content = flags.content || flags.c;
1187
+ if (!content) { console.log(` ${bad('✗')} --content required`); process.exit(1); }
1188
+ const entry = saveContext({
1189
+ title: (flags.title || flags.t || '').trim(),
1190
+ content: content.trim(),
1191
+ project: (flags.project || flags.p || '').trim() || 'global',
1192
+ tags: (flags.tags || '').split(',').map(s => s.trim()).filter(Boolean),
1193
+ type: (flags.type || 'note').trim(),
1194
+ source: 'cli',
1195
+ });
1196
+ console.log(` ${ok('✓')} saved "${entry.title || entry.id.slice(0, 8)}" → ${entry.project}`);
1197
+ }
1198
+
1097
1199
  // ── Delete ────────────────────────────────────────────────────────────────────
1098
1200
 
1099
1201
  function cmdDelete(args) {
@@ -1193,8 +1295,6 @@ async function interactive() {
1193
1295
  clearScreen(); printCompactHeader('discussions'); cmdDiscussions(rest); break;
1194
1296
  case 'summary':
1195
1297
  clearScreen(); printCompactHeader('summary'); cmdSummary(rest); break;
1196
- case 'benchmark': case 'bench':
1197
- clearScreen(); printCompactHeader('benchmark'); cmdBenchmark(); break;
1198
1298
  case 'install':
1199
1299
  clearScreen(); printCompactHeader('install'); await cmdInstall(rest); break;
1200
1300
  case 'online':
@@ -1203,6 +1303,8 @@ async function interactive() {
1203
1303
  clearScreen(); printCompactHeader('settings'); await cmdSettings(rl); break;
1204
1304
  case 'add':
1205
1305
  clearScreen(); printCompactHeader('add'); await cmdAdd(rl); break;
1306
+ case 'save':
1307
+ cmdSave(rest); break;
1206
1308
  case 'delete': case 'del': case 'rm':
1207
1309
  clearScreen(); printCompactHeader('delete'); cmdDelete(rest); break;
1208
1310
  case 'help': case '?':
@@ -1257,8 +1359,6 @@ async function checkForUpdate() {
1257
1359
  cmdDiscussions(rest); break;
1258
1360
  case 'summary':
1259
1361
  cmdSummary(rest); break;
1260
- case 'benchmark': case 'bench':
1261
- cmdBenchmark(); break;
1262
1362
  case 'install':
1263
1363
  await cmdInstall(rest);
1264
1364
  process.exit(0);
@@ -1284,6 +1384,8 @@ async function checkForUpdate() {
1284
1384
  await cmdSettings(); break;
1285
1385
  case 'add':
1286
1386
  await cmdAdd(); break;
1387
+ case 'save':
1388
+ cmdSave(rest); break;
1287
1389
  case 'delete': case 'del': case 'rm':
1288
1390
  cmdDelete(rest); break;
1289
1391
  case 'help': case '--help': case '-h':