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.
- package/README.md +29 -7
- package/codegraph/__pycache__/affected.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/callflow_html.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/export.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/tree_html.cpython-313.pyc +0 -0
- package/codegraph/affected.py +233 -0
- package/codegraph/cache.py +51 -2
- package/codegraph/callflow_html.py +273 -0
- package/codegraph/export.py +544 -0
- package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/ast_extractor.py +143 -16
- package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/symbol_resolution.cpython-313.pyc +0 -0
- package/codegraph/graph/builder.py +10 -0
- package/codegraph/graph/clustering.py +247 -10
- package/codegraph/graph/query.py +99 -0
- package/codegraph/graph/symbol_resolution.py +112 -0
- package/codegraph/report.py +53 -0
- package/codegraph/server.py +112 -20
- package/codegraph/tree_html.py +241 -0
- package/package.json +2 -2
- package/pyproject.toml +4 -1
- package/src/cli.js +329 -227
- package/src/db.js +79 -102
- package/src/search.js +73 -9
- package/src/server.js +7 -1
- package/src/templates/antigravity/GEMINI.md +96 -0
- package/src/templates/antigravity/hooks/context-mcp-post-tool-use.js +62 -0
- package/src/templates/antigravity/workflows/context-resume.md +20 -0
- package/src/templates/antigravity/workflows/graph-build.md +23 -0
- package/src/templates/antigravity/workflows/save-context.md +29 -0
- package/src/templates/claude/CLAUDE.md +140 -0
- package/src/templates/claude/commands/graph-build.md +9 -0
- package/src/templates/claude/commands/save-context.md +19 -0
- package/src/templates/claude/hooks/context-mcp-post-tool-use.js +59 -0
- package/src/templates/claude/hooks/context-mcp-pre-tool-use.js +26 -0
- package/src/templates/claude/skills/SKILL.md +144 -0
- package/src/templates/codex/AGENTS.md +107 -0
- package/src/templates/codex/hooks/context-mcp-post-tool-use.js +46 -0
- package/src/templates/codex/hooks/context-mcp-pre-tool-use.js +23 -0
- package/src/templates/codex/prompts/context-resume.md +15 -0
- package/src/templates/codex/prompts/graph-build.md +14 -0
- package/src/templates/codex/prompts/save-context.md +24 -0
- package/src/templates/cursor/commands/context-resume.md +7 -0
- package/src/templates/cursor/commands/graph-build.md +7 -0
- package/src/templates/cursor/commands/save-context.md +12 -0
- package/src/templates/{cursor-rules.mdc → cursor/cursor-rules.mdc} +13 -3
- package/src/templates/cursor/hooks/context-mcp-post-tool-use.js +55 -0
- package/src/templates/gemini/GEMINI.md +92 -0
- package/src/templates/gemini/commands/context-resume.toml +15 -0
- package/src/templates/gemini/commands/graph-build.toml +14 -0
- package/src/templates/gemini/commands/save-context.toml +24 -0
- package/src/templates/gemini/hooks/context-mcp-after-tool.js +59 -0
- package/src/templates/gemini/hooks/context-mcp-before-tool.js +26 -0
- package/src/templates/vscode/commands/context-resume.prompt.md +15 -0
- package/src/templates/vscode/commands/graph-build.prompt.md +10 -0
- package/src/templates/vscode/commands/save-context.prompt.md +16 -0
- package/src/templates/vscode/hooks/context-mcp-post-tool-use.js +58 -0
- package/src/templates/windsurf/hooks/context-mcp-post-run-command.js +57 -0
- package/src/templates/windsurf/windsurf-rules.md +86 -0
- package/src/templates/windsurf/workflows/context-resume.md +11 -0
- package/src/templates/windsurf/workflows/graph-build.md +11 -0
- package/src/templates/windsurf/workflows/save-context.md +18 -0
- package/src/tools/codegraph.js +83 -43
- package/src/tools/context.js +42 -24
- package/src/tools/plan.js +14 -11
- package/uv.lock +1101 -4
- package/src/migrator.js +0 -124
- package/src/templates/AGENTS.md +0 -80
- package/src/templates/CLAUDE.md +0 -103
- package/src/templates/GEMINI.md +0 -80
- package/src/templates/commands/graph-build.md +0 -5
- package/src/templates/commands/save-context.md +0 -9
- package/src/templates/skills/SKILL.md +0 -108
- package/src/templates/windsurf-rules.md +0 -35
- /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
|
-
|
|
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
|
|
126
|
-
cmd('ctx install --all', '
|
|
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
|
|
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)} ${
|
|
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} ${
|
|
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)} ${
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
596
|
+
// Slash commands — user-global
|
|
661
597
|
_writeCommands(homedir());
|
|
662
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
710
|
-
|
|
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
|
|
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
|
|
722
|
-
|
|
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
|
-
|
|
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
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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/
|
|
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':
|