context-mcp-server 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -107,45 +107,29 @@ 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)');
122
+ cmd('ctx stats', 'storage report: entries, types, graph status');
123
123
  console.log('');
124
- cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps');
125
- cmd('ctx install --<platform>', 'write MCP config + skill/rules for an AI platform');
126
- cmd('ctx install --all', 'install for all platforms at once');
124
+ cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps only');
125
+ cmd('ctx install --<platform>', 'write MCP config + skill/rules file only (no uv/npm)');
126
+ cmd('ctx install --all', 'write config + skill files for all platforms');
127
127
  cmd('ctx online [--port N]', 'start HTTP server for Claude.ai / ChatGPT');
128
128
  cmd('ctx online --close', 'stop the running HTTP server');
129
129
  cmd('ctx settings', 'view and edit config (port, host, client id/secret)');
130
130
  cmd('ctx update', 'check for and apply latest version');
131
131
  cmd('ctx help', 'show this screen');
132
132
  console.log('');
133
-
134
- // Interactive mode commands (no prefix needed)
135
- printSection('Interactive mode', 'type these inside the UI — no "ctx" prefix needed');
136
- const icmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
137
- icmd('list [project]', 'list entries');
138
- icmd('search <query>', 'search context');
139
- icmd('add', 'add entry');
140
- icmd('projects', 'show all projects');
141
- icmd('discuss [project]', 'show discussions');
142
- icmd('summary [project]', 'summarize recent entries');
143
- icmd('benchmark', 'token savings report');
144
- icmd('install --<platform>', 'install for a platform');
145
- icmd('settings', 'edit config');
146
- icmd('clear', 'clear screen');
147
- icmd('exit / quit / q', 'exit interactive mode');
148
- console.log('');
149
133
  }
150
134
  function clearScreen() {
151
135
  // \x1b[2J = clear screen, \x1b[3J = clear scrollback, \x1b[H = cursor home
@@ -155,7 +139,8 @@ function clearScreen() {
155
139
  // ── List (grouped by project) ─────────────────────────────────────────────────
156
140
 
157
141
  function cmdList(args) {
158
- const filterProject = args[0];
142
+ const projectFlagIdx = args.indexOf('--project');
143
+ const filterProject = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
159
144
  const entries = getContext({ project: filterProject, limit: 100 });
160
145
  const allDiscussions = listDiscussions({ project: filterProject });
161
146
  const allGraphs = listGraphs();
@@ -163,17 +148,19 @@ function cmdList(args) {
163
148
 
164
149
  printSection('Context', filterProject ? `project: ${filterProject}` : 'all projects');
165
150
 
166
- // Build per-project map
167
151
  const projects = {};
152
+ const ensureProj = p => {
153
+ if (!projects[p]) projects[p] = { context: [], summary: [], plans: [] };
154
+ return projects[p];
155
+ };
168
156
  for (const entry of entries) {
169
157
  const p = entry.project || 'global';
170
- if (!projects[p]) projects[p] = { contexts: [], discussions: [] };
171
- projects[p].contexts.push(entry);
158
+ const d = ensureProj(p);
159
+ if (entry.type === 'compaction') d.summary.push(entry);
160
+ else d.context.push(entry);
172
161
  }
173
162
  for (const disc of allDiscussions) {
174
- const p = disc.project || 'global';
175
- if (!projects[p]) projects[p] = { contexts: [], discussions: [] };
176
- projects[p].discussions.push(disc);
163
+ ensureProj(disc.project || 'global').plans.push(disc);
177
164
  }
178
165
 
179
166
  const projectNames = Object.keys(projects).sort();
@@ -185,73 +172,78 @@ function cmdList(args) {
185
172
  }
186
173
 
187
174
  for (const projectName of projectNames) {
188
- const pData = projects[projectName];
189
- const graph = _graphForProject(allGraphs, projectName);
190
- const activeD = pData.discussions.filter(d => d.status === 'active').length;
191
- const totalSecs = (pData.contexts.length > 0 ? 1 : 0) + (pData.discussions.length > 0 ? 1 : 0) + (graph ? 1 : 0);
192
- let secIdx = 0;
193
-
194
- const projReg = projectRegistry.get(projectName);
175
+ const pData = projects[projectName];
176
+ const graphBuild = _graphForProject(allGraphs, projectName);
177
+ const totalEntries = pData.context.length + pData.summary.length;
178
+ const activePlans = pData.plans.filter(p => p.status === 'active').length;
179
+ const sections = [
180
+ graphBuild && 'graph',
181
+ pData.context.length && 'context',
182
+ pData.summary.length && 'summary',
183
+ pData.plans.length && 'plans',
184
+ ].filter(Boolean);
185
+ let secIdx = 0;
186
+
187
+ const projReg = projectRegistry.get(projectName);
195
188
  const projIdStr = projReg?.id ? faint(' id:' + projReg.id.slice(0, 8)) : '';
196
- console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(projectName))}${projIdStr} ${faint(`${pData.contexts.length} entries · ${pData.discussions.length} discussions`)}${activeD ? ` ${warn('● ' + activeD + ' active')}` : ''}`);
189
+ console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(projectName))}${projIdStr} ${faint(`${totalEntries} entries · ${pData.plans.length} plans`)}${activePlans ? ` ${warn('● ' + activePlans + ' active')}` : ''}`);
197
190
  console.log(` ${color(C.darkgray, '│')}`);
198
191
 
199
- // ── Graph ────────────────────────────────────────────────────────────────
200
- if (graph) {
192
+ const renderEntries = (items, label, secIsLast) => {
193
+ console.log(` ${color(C.darkgray, secIsLast ? '└─' : '├─')} ${muted(label)} ${faint(items.length + ' entries')}`);
194
+ items.forEach((item, i) => {
195
+ const br = i === items.length - 1 ? '└─' : '├─';
196
+ const date = (item.createdAt || '').slice(0, 10);
197
+ const id = item.id.slice(0, 8);
198
+ const tags = safeTags(item.tags);
199
+ const pipe = secIsLast ? ' ' : '│';
200
+ console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${bold(item.title || '(no title)')} ${faint('id:' + id)} ${faint(date)}`);
201
+ if (tags.length) console.log(` ${color(C.darkgray, pipe)} ${faint(tags.map(t => '#' + t).join(' '))}`);
202
+ });
203
+ if (!secIsLast) console.log(` ${color(C.darkgray, '│')}`);
204
+ };
205
+
206
+ if (graphBuild) {
201
207
  secIdx++;
202
- const isLast = secIdx === totalSecs;
203
- const builtAt = (graph.builtAt || '').slice(0, 10);
204
- console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graph.nodes}n · ${graph.edges}e · ${graph.communities} clusters · ${builtAt}`)}`);
208
+ const isLast = secIdx === sections.length;
209
+ const builtAt = (graphBuild.builtAt || '').slice(0, 10);
210
+ console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graphBuild.nodes}n · ${graphBuild.edges}e · ${graphBuild.communities} clusters · ${builtAt}`)}`);
205
211
  if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
206
212
  }
207
213
 
208
- // ── Context entries ───────────────────────────────────────────────────────
209
- if (pData.contexts.length) {
214
+ if (pData.context.length) {
215
+ secIdx++;
216
+ renderEntries(pData.context, 'context', secIdx === sections.length);
217
+ }
218
+
219
+ if (pData.summary.length) {
210
220
  secIdx++;
211
- const isLast = secIdx === totalSecs;
212
- console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('memory')} ${faint(pData.contexts.length + ' entries')}`);
213
- pData.contexts.forEach((item, i) => {
214
- const br = i === pData.contexts.length - 1 ? '└─' : '├─';
221
+ const isLast = secIdx === sections.length;
222
+ console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('summary')} ${faint(pData.summary.length + ' compactions')}`);
223
+ pData.summary.forEach((item, i) => {
224
+ const br = i === pData.summary.length - 1 ? '└─' : '├─';
215
225
  const date = (item.createdAt || '').slice(0, 10);
216
- const type = item.type || 'note';
217
- const id = item.id.slice(0, 8);
218
- const tags = safeTags(item.tags);
219
226
  const pipe = isLast ? ' ' : '│';
220
- console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${pill(type)} ${bold(item.title || '(no title)')} ${faint('id:' + id)} ${faint(date)}`);
221
- if (tags.length) console.log(` ${color(C.darkgray, pipe)} ${faint(tags.map(t => '#' + t).join(' '))}`);
227
+ console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${faint('◎')} ${bold(item.title || '(compaction)')} ${faint(date)}`);
222
228
  });
223
229
  if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
224
230
  }
225
231
 
226
- // ── Discussions ───────────────────────────────────────────────────────────
227
- if (pData.discussions.length) {
232
+ if (pData.plans.length) {
228
233
  secIdx++;
229
- const isLast = secIdx === totalSecs;
230
- console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('discussions')} ${faint(pData.discussions.length + ' total')}`);
231
- pData.discussions.forEach((disc, i) => {
232
- const br = i === pData.discussions.length - 1 ? '└─' : '├─';
233
- const sc = disc.status === 'done' ? 'green' : 'tcyan';
234
- const steps = disc.stepsSummary?.total ? faint(` ${disc.stepsSummary.done}/${disc.stepsSummary.total}`) : '';
235
- const pipe = isLast ? ' ' : '│';
236
- console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${warn(disc.status === 'active' ? '●' : '○')} ${bold(disc.name)} ${pill(disc.status, sc)} ${faint(disc.type || 'plan')}${steps}`);
237
- if (disc.description) console.log(` ${color(C.darkgray, pipe)} ${faint(disc.description)}`);
234
+ const isLast = secIdx === sections.length;
235
+ console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('plans')} ${faint(pData.plans.length + ' total')}`);
236
+ pData.plans.forEach((plan, i) => {
237
+ const br = i === pData.plans.length - 1 ? '└─' : '├─';
238
+ const pipe = isLast ? ' ' : '';
239
+ console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${warn(plan.status === 'active' ? '●' : '○')} ${bold(plan.name)} ${pill(plan.status, plan.status === 'done' ? 'green' : 'tcyan')} ${faint((plan.description || '').slice(0, 60))}`);
238
240
  });
239
241
  }
240
242
  }
241
243
 
242
- // Orphan graphs (no matching project)
243
- const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => _graphForProject([g], p)));
244
- if (orphanGraphs.length) {
245
- console.log(`\n ${color(C.dblue, '◇')} ${muted('other graphs')}`);
246
- for (const g of orphanGraphs) {
247
- const pathShort = g.path.replace(/\\/g, '/').split('/').slice(-2).join('/');
248
- console.log(` ${accent('⬡')} ${bold(pathShort)} ${faint(`${g.nodes}n · ${g.edges}e · ${g.communities} clusters`)}`);
249
- }
250
- }
251
-
252
244
  console.log('');
253
245
  console.log(line());
254
- console.log(faint(` ${entries.length} entries · ${allDiscussions.length} discussions · ${allGraphs.length} graphs · ${projectNames.length} projects`));
246
+ console.log(faint(` ${entries.length} entries · ${allDiscussions.length} plans · ${allGraphs.length} graphs · ${projectNames.length} projects`));
255
247
  }
256
248
 
257
249
  // ── Search ────────────────────────────────────────────────────────────────────
@@ -273,7 +265,7 @@ function cmdSearch(args) {
273
265
  const id = entry.id.slice(0, 8);
274
266
  const type = entry.type || 'note';
275
267
  const isLast = index === results.length - 1;
276
- console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${bold(entry.title || '(no title)')}${score} ${pill(type)} ${faint('id:' + id)} ${faint(date)}`);
268
+ console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${bold(entry.title || '(no title)')}${score} ${faint('id:' + id)} ${faint(date)}`);
277
269
  });
278
270
  console.log('');
279
271
  console.log(line());
@@ -301,7 +293,6 @@ function cmdProjects() {
301
293
  console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(project.name))}${idTag} ${bar} ${faint(project.count + ' entries')}`);
302
294
  console.log(` ${color(C.darkgray, '│')}`);
303
295
 
304
- // Graph status
305
296
  if (graph) {
306
297
  const builtAt = (graph.builtAt || '').slice(0, 10);
307
298
  console.log(` ${color(C.darkgray, '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graph.nodes}n · ${graph.edges}e · ${graph.communities} clusters · ${builtAt}`)}`);
@@ -309,18 +300,15 @@ function cmdProjects() {
309
300
  console.log(` ${color(C.darkgray, '├─')} ${faint('⬡ no graph')}`);
310
301
  }
311
302
 
312
- // Recent context
313
303
  if (entries.length) {
314
304
  console.log(` ${color(C.darkgray, '├─')} ${muted('recent')}`);
315
305
  entries.forEach((e, i) => {
316
306
  const br = i === entries.length - 1 && !activeD.length ? '└─' : '├─';
317
- const type = e.type || 'note';
318
307
  const date = (e.createdAt || '').slice(0, 10);
319
- console.log(` ${color(C.darkgray, '│')} ${color(C.darkgray, br)} ${pill(type)} ${bold(e.title || '(no title)')} ${faint(date)}`);
308
+ console.log(` ${color(C.darkgray, '│')} ${color(C.darkgray, br)} ${bold(e.title || '(no title)')} ${faint(date)}`);
320
309
  });
321
310
  }
322
311
 
323
- // Active discussions
324
312
  if (activeD.length) {
325
313
  console.log(` ${color(C.darkgray, '├─')} ${muted('discussions')}`);
326
314
  activeD.forEach((d, i) => {
@@ -342,7 +330,8 @@ function cmdProjects() {
342
330
  // ── Discussions ───────────────────────────────────────────────────────────────
343
331
 
344
332
  function cmdDiscussions(args) {
345
- const filterProject = args[0];
333
+ const projectFlagIdx = args.indexOf('--project');
334
+ const filterProject = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
346
335
  const discussions = listDiscussions({ project: filterProject });
347
336
  printSection('Discussions', filterProject || 'all projects');
348
337
 
@@ -380,7 +369,8 @@ function cmdDiscussions(args) {
380
369
  // ── Summary ───────────────────────────────────────────────────────────────────
381
370
 
382
371
  function cmdSummary(args) {
383
- const project = args[0];
372
+ const projectFlagIdx = args.indexOf('--project');
373
+ const project = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
384
374
  const entries = getContext({ project, limit: 50 });
385
375
  printSection('Summary', project || 'global');
386
376
  if (!entries.length) { console.log(` ${faint('no entries to summarize')}`); return; }
@@ -399,137 +389,54 @@ function cmdSummary(args) {
399
389
  // ── Benchmark ────────────────────────────────────────────────────────────────
400
390
 
401
391
 
402
- function _walkBytes(dirPath) {
403
- const walkScript =
404
- `const fs=require('fs'),path=require('path');` +
405
- `function walk(d,t=0){try{for(const f of fs.readdirSync(d)){` +
406
- `const p=path.join(d,f);try{const s=fs.statSync(p);` +
407
- `if(s.isDirectory()&&!['node_modules','.git','codegraph-cache','.venv','venv','__pycache__','dist','build','.next'].includes(f))t+=walk(p);` +
408
- `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;}` +
409
- `console.log(walk(${JSON.stringify(dirPath)}));`;
410
- const res = spawnSync('node', ['-e', walkScript], { encoding: 'utf8', timeout: 8000 });
411
- return res.stdout ? parseInt(res.stdout.trim()) : null;
412
- }
413
-
414
- function _sampleQueryTokens(graphPath) {
415
- const questions = ['what does the server do', 'how is the graph built', 'what calls save'];
416
- const sizes = [];
417
- for (const q of questions) {
418
- const req = JSON.stringify({ tool: 'codegraph_query', args: { path: graphPath, question: q, token_budget: 2000 } });
419
- const res = spawnSync('uv', ['run', 'python', '-m', 'codegraph'],
420
- { input: req, encoding: 'utf8', cwd: process.cwd(), timeout: 12000 });
421
- if (res.stdout) { try { const r = JSON.parse(res.stdout); if (!r.error) sizes.push(res.stdout.length); } catch {} }
422
- }
423
- return { avgTok: sizes.length ? Math.round(sizes.reduce((a, b) => a + b, 0) / sizes.length / 4) : 472, measured: sizes.length > 0 };
424
- }
425
-
426
- function cmdBenchmark() {
427
- const graphs = listGraphs();
428
- const projects = listProjects();
429
- printSection('Benchmark', 'real token savings');
430
-
431
- const RESUME_LIMIT = 15;
432
- const COMPACT_AT = 20;
433
-
434
- // ── Measure entry sizes from actual stored data ──────────────────────────────
435
- const allEntries = getContext({ limit: 500, compact: false });
436
- const totalEntries = allEntries.length;
437
- let avgFullTok = 155, avgCompactTok = 50;
438
- if (allEntries.length) {
439
- const fullSizes = allEntries.map(e => JSON.stringify(e).length);
440
- const compactSizes = allEntries.map(e =>
441
- ([e.title || '', (e.content || '').slice(0, 200), (e.tags || []).join(' ')].join(' ')).length);
442
- avgFullTok = Math.round(fullSizes.reduce((a, b) => a + b, 0) / fullSizes.length / 4);
443
- avgCompactTok = Math.round(compactSizes.reduce((a, b) => a + b, 0) / compactSizes.length / 4);
444
- }
445
-
446
- // ── Run graph queries once, reuse in both sections ────────────────────────────
447
- let avgQueryTok = 472, queryMeasured = false;
448
- if (graphs.length) {
449
- const r = _sampleQueryTokens(graphs[0].path);
450
- avgQueryTok = r.avgTok;
451
- queryMeasured = r.measured;
452
- }
392
+ function cmdStats() {
393
+ const projects = listProjects();
394
+ const graphs = listGraphs();
395
+ const allEntries = getContext({ limit: 2000, compact: false });
453
396
 
454
- // ── Corpus size (run once) ────────────────────────────────────────────────────
455
- let corpusToks = null;
456
- if (graphs.length) {
457
- const bytes = _walkBytes(graphs[0].path);
458
- if (bytes) corpusToks = Math.round(bytes / 4);
459
- }
397
+ printSection('Stats', 'context-mcp storage report');
460
398
 
461
- // ── Memory ───────────────────────────────────────────────────────────────────
462
- // Without context-mcp: AI reads all entries at full size every conversation
463
- // With context-mcp: AI loads min(15, N) entries as compact previews via resume
464
- console.log(`\n ${bold(lblue('Memory'))} ${faint('measured from actual stored entries')}`);
465
- if (totalEntries) {
466
- const resumeCount = Math.min(RESUME_LIMIT, totalEntries);
467
- const withoutMemTok = totalEntries * avgFullTok; // load all entries full
468
- const withMemTok = resumeCount * avgCompactTok; // resume: compact previews only
469
- const memSaved = withoutMemTok - withMemTok;
470
- const memReduction = (withoutMemTok / withMemTok).toFixed(1);
471
- const memPct = ((1 - withMemTok / withoutMemTok) * 100).toFixed(1);
472
-
473
- console.log(` ${faint('avg entry size (full): ')} ${muted(avgFullTok)} tok ${faint('(measured)')}`);
474
- console.log(` ${faint('avg entry size (compact):')} ${muted(avgCompactTok)} tok ${faint('(measured)')}`);
475
- console.log(` ${faint('stored: ')} ${muted(totalEntries)} entries ${faint('across')} ${muted(projects.length)} project(s)`);
476
- console.log(` ${faint('without — load all full: ')} ${warn('~' + withoutMemTok.toLocaleString('en-US'))} tokens`);
477
- console.log(` ${faint('with — resume compact:')} ${ok('~' + withMemTok.toLocaleString('en-US'))} tokens ${faint(`(${resumeCount} of ${totalEntries} entries)`)}`);
478
- console.log(` ${faint('saved per chat: ')} ${ok('~' + memSaved.toLocaleString('en-US'))} tokens ${highlight(memReduction + '×')} ${ok(memPct + '%')} reduction`);
479
- console.log(` ${faint('auto-compact at: ')} ${faint(COMPACT_AT + ' entries → oldest summarized to 1')}`);
480
- console.log('');
399
+ // ── Projects ──────────────────────────────────────────────────────────────────
400
+ console.log(`\n ${bold(lblue('Projects'))}`);
401
+ if (!projects.length) {
402
+ console.log(` ${faint('no projects yet')}`);
403
+ } else {
481
404
  for (const p of projects) {
482
- const pToks = p.count * avgFullTok;
483
- const barLen = Math.min(Math.ceil(p.count / 2), 24);
484
- const bar = color(C.dblue, '█'.repeat(barLen)) + color(C.darkgray, '░'.repeat(24 - barLen));
485
- console.log(` ${color(C.darkgray, '·')} ${muted(p.name.padEnd(22))} ${bar} ${faint(p.count + ' entries · ~' + pToks.toLocaleString('en-US') + ' tok full')}`);
405
+ const g = graphs.find(gr => {
406
+ const gp = (gr.path || '').replace(/\\/g, '/').replace(/\/$/, '').split('/').pop();
407
+ return gp === p.name;
408
+ });
409
+ const graphStatus = g ? ok(`graph: ${g.nodes} nodes, ${g.edges} edges`) : faint('no graph');
410
+ const builtAt = g?.builtAt ? faint(' built ' + g.builtAt.slice(0, 10)) : '';
411
+ console.log(` ${bold(p.name.padEnd(24))} ${muted(String(p.count).padStart(3) + ' entries')} ${graphStatus}${builtAt}`);
486
412
  }
487
- } else {
488
- console.log(` ${faint('no entries yet')}`);
489
413
  }
490
414
 
491
- // ── CodeGraph ─────────────────────────────────────────────────────────────────
492
- // Without context-mcp: AI reads all source files to answer structural questions
493
- // With context-mcp: AI calls codegraph_query → focused NODE/EDGE subgraph
494
- console.log(`\n ${bold(lblue('CodeGraph'))} ${faint('measured from live graph queries')}`);
495
- if (!graphs.length) {
496
- console.log(` ${faint('no graphs — run codegraph_build first')}`);
415
+ // ── Entry type breakdown ──────────────────────────────────────────────────────
416
+ console.log(`\n ${bold(lblue('Entries by type'))}`);
417
+ if (!allEntries.length) {
418
+ console.log(` ${faint('no entries yet')}`);
497
419
  } else {
498
- for (const g of graphs) {
499
- const pathShort = g.path.replace(/\\/g, '/').split('/').slice(-2).join('/');
500
- const builtAt = (g.builtAt || '').slice(0, 10);
501
- const graphReduce = corpusToks ? (corpusToks / avgQueryTok).toFixed(0) + '×' : null;
502
- const graphPct = corpusToks ? ((1 - avgQueryTok / corpusToks) * 100).toFixed(2) : null;
503
-
504
- console.log(`\n ${accent('⬡')} ${bold(pathShort)} ${faint(builtAt)}`);
505
- console.log(` ${faint('nodes:')} ${muted(g.nodes)} ${faint('edges:')} ${muted(g.edges)} ${faint('clusters:')} ${muted(g.communities)}`);
506
- if (corpusToks) console.log(` ${faint('without — read all files:')} ${warn('~' + corpusToks.toLocaleString('en-US'))} tokens ${faint('(all source + config + doc files)')}`);
507
- console.log(` ${faint('with — graph query: ')} ${ok('~' + avgQueryTok.toLocaleString('en-US'))} tokens ${faint(queryMeasured ? '(avg of 3 live queries)' : '(calibrated fallback)')}`);
508
- if (graphReduce) console.log(` ${faint('saved per query: ')} ${highlight(graphReduce)} ${ok(graphPct + '%')} fewer tokens`);
420
+ const byType = {};
421
+ for (const e of allEntries) { byType[e.type] = (byType[e.type] || 0) + 1; }
422
+ for (const [type, count] of Object.entries(byType).sort((a, b) => b[1] - a[1])) {
423
+ const bar = color(C.dblue, '█'.repeat(Math.min(count, 20))) + color(C.darkgray, ''.repeat(Math.max(0, 20 - count)));
424
+ console.log(` ${type.padEnd(14)} ${bar} ${muted(count)}`);
509
425
  }
510
426
  }
511
427
 
512
- // ── Combined ──────────────────────────────────────────────────────────────────
513
- // Without: read all files (no graph) + load all entries full (no memory system)
514
- // With: compact resume (memory) + one graph query (codegraph)
515
- if (graphs.length) {
516
- const resumeCount = Math.min(RESUME_LIMIT, totalEntries);
517
- const withMemTok = resumeCount * avgCompactTok;
518
- const withMcp = withMemTok + avgQueryTok;
519
- const withoutMemTok = totalEntries * avgFullTok;
520
- const withoutMcp = withoutMemTok + (corpusToks || graphs[0].nodes * 80);
521
- const totalRed = withoutMcp > 0 ? (withoutMcp / withMcp).toFixed(0) : '—';
522
- const totalPct = withoutMcp > 0 ? ((1 - withMcp / withoutMcp) * 100).toFixed(2) : '—';
523
-
524
- console.log(`\n ${bold(lblue('Combined'))} ${faint('per conversation')}`);
525
- console.log(` ${faint('without context-mcp: ')} ${warn('~' + withoutMcp.toLocaleString('en-US'))} tokens ${faint('(all entries full + all files read directly)')}`);
526
- console.log(` ${faint('with context-mcp: ')} ${ok('~' + withMcp.toLocaleString('en-US'))} tokens ${faint('(compact resume + 1 graph query)')}`);
527
- console.log(` ${faint('total reduction: ')} ${highlight(totalRed + '×')} ${ok(totalPct + '%')} fewer tokens`);
528
- }
529
-
428
+ // ── Storage summary ───────────────────────────────────────────────────────────
429
+ const compactions = allEntries.filter(e => e.type === 'compaction').length;
430
+ const avgSize = allEntries.length
431
+ ? Math.round(allEntries.reduce((s, e) => s + (e.content || '').length, 0) / allEntries.length)
432
+ : 0;
433
+
434
+ console.log(`\n ${bold(lblue('Storage'))}`);
435
+ console.log(` ${faint('total entries: ')} ${muted(allEntries.length)}`);
436
+ console.log(` ${faint('compactions: ')} ${muted(compactions)}`);
437
+ console.log(` ${faint('avg entry size: ')} ${muted(avgSize + ' chars')}`);
438
+ console.log(` ${faint('graphs built: ')} ${muted(graphs.length)}`);
530
439
  console.log('');
531
- console.log(line());
532
- console.log(faint(' token estimate: chars ÷ 4 · corpus = all source/config/doc files (excl. lock files, .venv, node_modules)'));
533
440
  }
534
441
 
535
442
 
@@ -784,6 +691,15 @@ async function cmdInstall(args) {
784
691
  console.log(` ${ok('✓')} Python environment ready — codegraph enabled`);
785
692
  }
786
693
  }
694
+ // Bootstrap store structure — creates ~/.context-mcp/, projects/, contextconfig.json
695
+ console.log(` ${bold(lblue('Store'))}`);
696
+ try {
697
+ getConfig(); // triggers DATA_DIR creation + contextconfig.json generation
698
+ console.log(` ${ok('✓')} store ready ${faint(getStorePath())}`);
699
+ console.log(` ${ok('✓')} config ready ${faint(getConfigPath())}`);
700
+ } catch (e) {
701
+ console.log(` ${bad('✗')} store init failed: ${faint(e.message)}`);
702
+ }
787
703
  console.log('');
788
704
  return;
789
705
  }
@@ -859,27 +775,6 @@ async function cmdInstall(args) {
859
775
  // ── Global gitignore — add context-mcp runtime files if global gitignore exists ──
860
776
  _updateGlobalGitignore();
861
777
  console.log('');
862
-
863
- // ── Python / uv setup (codegraph) ─────────────────────────────────────────
864
- console.log(` ${bold(lblue('Python Codegraph'))}`);
865
- const uvCheck = spawnSync('uv', ['--version'], { encoding: 'utf8' });
866
- if (uvCheck.error || uvCheck.status !== 0) {
867
- console.log(` ${bad('✗')} uv not found — install it from ${accent('https://docs.astral.sh/uv/')} to enable codegraph`);
868
- console.log('');
869
- return;
870
- }
871
- console.log(` ${ok('✓')} uv found: ${faint(uvCheck.stdout.trim())}`);
872
-
873
- // Package root is one level up from src/
874
- const __dirname_cli = dirname(fileURLToPath(import.meta.url));
875
- const pkgRoot = join(__dirname_cli, '..');
876
- const sync = spawnSync('uv', ['--directory', pkgRoot, 'sync', '--no-dev'], { encoding: 'utf8' });
877
- if (sync.status !== 0) {
878
- console.log(` ${bad('✗')} uv sync failed:\n${faint((sync.stderr || sync.stdout || '').trim())}`);
879
- } else {
880
- console.log(` ${ok('✓')} Python environment ready — codegraph enabled`);
881
- }
882
- console.log('');
883
778
  }
884
779
 
885
780
  // ── Online ────────────────────────────────────────────────────────────────────
@@ -1065,7 +960,7 @@ async function cmdAdd(existingRl) {
1065
960
  const content = await ask('Content:');
1066
961
  const project = await ask('Project (blank = global):');
1067
962
  const tagsRaw = await ask('Tags (comma-separated):');
1068
- const type = await ask('Type (note/decision/code/bug/architecture/config/error):');
963
+ const type = await ask('Type (note/compaction):');
1069
964
 
1070
965
  if (!existingRl) rl.close();
1071
966
  if (!content.trim()) { console.log(` ${bad('✗')} content required`); return; }
@@ -1082,6 +977,30 @@ async function cmdAdd(existingRl) {
1082
977
  console.log(` ${ok('✓')} ${bold(entry.title || '(no title)')} ${faint('id:' + entry.id.slice(0, 8))}`);
1083
978
  }
1084
979
 
980
+ // ── Save (non-interactive, flag-based — used by hooks and scripts) ───────────
981
+
982
+ function cmdSave(args) {
983
+ // Usage: ctx save --title "..." --content "..." --project <p> --type <t> --tags <t1,t2>
984
+ const flags = {};
985
+ for (let i = 0; i < args.length; i++) {
986
+ if (args[i].startsWith('--')) {
987
+ flags[args[i].slice(2)] = args[i + 1] || '';
988
+ i++;
989
+ }
990
+ }
991
+ const content = flags.content || flags.c;
992
+ if (!content) { console.log(` ${bad('✗')} --content required`); process.exit(1); }
993
+ const entry = saveContext({
994
+ title: (flags.title || flags.t || '').trim(),
995
+ content: content.trim(),
996
+ project: (flags.project || flags.p || '').trim() || 'global',
997
+ tags: (flags.tags || '').split(',').map(s => s.trim()).filter(Boolean),
998
+ type: (flags.type || 'note').trim(),
999
+ source: 'cli',
1000
+ });
1001
+ console.log(` ${ok('✓')} saved "${entry.title || entry.id.slice(0, 8)}" → ${entry.project}`);
1002
+ }
1003
+
1085
1004
  // ── Delete ────────────────────────────────────────────────────────────────────
1086
1005
 
1087
1006
  function cmdDelete(args) {
@@ -1181,8 +1100,8 @@ async function interactive() {
1181
1100
  clearScreen(); printCompactHeader('discussions'); cmdDiscussions(rest); break;
1182
1101
  case 'summary':
1183
1102
  clearScreen(); printCompactHeader('summary'); cmdSummary(rest); break;
1184
- case 'benchmark': case 'bench':
1185
- clearScreen(); printCompactHeader('benchmark'); cmdBenchmark(); break;
1103
+ case 'stats':
1104
+ clearScreen(); printCompactHeader('stats'); cmdStats(); break;
1186
1105
  case 'install':
1187
1106
  clearScreen(); printCompactHeader('install'); await cmdInstall(rest); break;
1188
1107
  case 'online':
@@ -1191,6 +1110,8 @@ async function interactive() {
1191
1110
  clearScreen(); printCompactHeader('settings'); await cmdSettings(rl); break;
1192
1111
  case 'add':
1193
1112
  clearScreen(); printCompactHeader('add'); await cmdAdd(rl); break;
1113
+ case 'save':
1114
+ cmdSave(rest); break;
1194
1115
  case 'delete': case 'del': case 'rm':
1195
1116
  clearScreen(); printCompactHeader('delete'); cmdDelete(rest); break;
1196
1117
  case 'help': case '?':
@@ -1245,8 +1166,8 @@ async function checkForUpdate() {
1245
1166
  cmdDiscussions(rest); break;
1246
1167
  case 'summary':
1247
1168
  cmdSummary(rest); break;
1248
- case 'benchmark': case 'bench':
1249
- cmdBenchmark(); break;
1169
+ case 'stats':
1170
+ cmdStats(); break;
1250
1171
  case 'install':
1251
1172
  await cmdInstall(rest);
1252
1173
  process.exit(0);
@@ -1272,6 +1193,8 @@ async function checkForUpdate() {
1272
1193
  await cmdSettings(); break;
1273
1194
  case 'add':
1274
1195
  await cmdAdd(); break;
1196
+ case 'save':
1197
+ cmdSave(rest); break;
1275
1198
  case 'delete': case 'del': case 'rm':
1276
1199
  cmdDelete(rest); break;
1277
1200
  case 'help': case '--help': case '-h':