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/README.md +10 -11
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
- package/codegraph/graph/query.py +43 -0
- package/codegraph/server.py +20 -17
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/src/cli.js +152 -229
- package/src/db.js +923 -805
- package/src/guard.js +9 -3
- package/src/search.js +73 -9
- package/src/server.js +7 -6
- package/src/templates/AGENTS.md +56 -53
- package/src/templates/CLAUDE.md +89 -61
- package/src/templates/GEMINI.md +56 -53
- package/src/templates/commands/context-resume.md +1 -1
- package/src/templates/commands/save-context.md +6 -3
- package/src/templates/cursor-rules.mdc +3 -3
- package/src/templates/skills/SKILL.md +87 -60
- package/src/templates/windsurf-rules.md +69 -20
- package/src/tools/codegraph.js +46 -43
- package/src/tools/context.js +44 -28
- package/src/tools/gitTools.js +1 -3
- package/src/tools/plan.js +133 -0
- package/uv.lock +1 -1
- package/src/tools/discussion.js +0 -123
package/src/cli.js
CHANGED
|
@@ -107,45 +107,29 @@ function printSection(title, meta = '') {
|
|
|
107
107
|
function printUsage() {
|
|
108
108
|
printBanner();
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
printSection('Terminal commands', 'run from your shell (ctx … or context …)');
|
|
110
|
+
printSection('Commands');
|
|
112
111
|
const cmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
|
|
113
112
|
cmd('ctx', 'open interactive mode');
|
|
114
113
|
cmd('ctx list [project]', 'list entries + discussions + graphs');
|
|
115
114
|
cmd('ctx search <query>', 'keyword → semantic fallback search');
|
|
116
115
|
cmd('ctx add', 'add entry interactively');
|
|
116
|
+
cmd('ctx save --title "…" --content "…" --project <p> --type <t>', 'non-interactive save (scripts/hooks)');
|
|
117
117
|
cmd('ctx delete <id-prefix>', 'delete one entry');
|
|
118
118
|
cmd('ctx delete project <name|id>', 'delete all entries for a project');
|
|
119
119
|
cmd('ctx summary [project]', 'summarize recent entries');
|
|
120
120
|
cmd('ctx projects', 'show all projects + graphs');
|
|
121
121
|
cmd('ctx discuss [project]', 'show discussions');
|
|
122
|
-
cmd('ctx
|
|
122
|
+
cmd('ctx stats', 'storage report: entries, types, graph status');
|
|
123
123
|
console.log('');
|
|
124
|
-
cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps');
|
|
125
|
-
cmd('ctx install --<platform>', 'write MCP config + skill/rules
|
|
126
|
-
cmd('ctx install --all', '
|
|
124
|
+
cmd('ctx install --initial', 'install / update Node.js + Python (codegraph) deps only');
|
|
125
|
+
cmd('ctx install --<platform>', 'write MCP config + skill/rules file only (no uv/npm)');
|
|
126
|
+
cmd('ctx install --all', 'write config + skill files for all platforms');
|
|
127
127
|
cmd('ctx online [--port N]', 'start HTTP server for Claude.ai / ChatGPT');
|
|
128
128
|
cmd('ctx online --close', 'stop the running HTTP server');
|
|
129
129
|
cmd('ctx settings', 'view and edit config (port, host, client id/secret)');
|
|
130
130
|
cmd('ctx update', 'check for and apply latest version');
|
|
131
131
|
cmd('ctx help', 'show this screen');
|
|
132
132
|
console.log('');
|
|
133
|
-
|
|
134
|
-
// Interactive mode commands (no prefix needed)
|
|
135
|
-
printSection('Interactive mode', 'type these inside the UI — no "ctx" prefix needed');
|
|
136
|
-
const icmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
|
|
137
|
-
icmd('list [project]', 'list entries');
|
|
138
|
-
icmd('search <query>', 'search context');
|
|
139
|
-
icmd('add', 'add entry');
|
|
140
|
-
icmd('projects', 'show all projects');
|
|
141
|
-
icmd('discuss [project]', 'show discussions');
|
|
142
|
-
icmd('summary [project]', 'summarize recent entries');
|
|
143
|
-
icmd('benchmark', 'token savings report');
|
|
144
|
-
icmd('install --<platform>', 'install for a platform');
|
|
145
|
-
icmd('settings', 'edit config');
|
|
146
|
-
icmd('clear', 'clear screen');
|
|
147
|
-
icmd('exit / quit / q', 'exit interactive mode');
|
|
148
|
-
console.log('');
|
|
149
133
|
}
|
|
150
134
|
function clearScreen() {
|
|
151
135
|
// \x1b[2J = clear screen, \x1b[3J = clear scrollback, \x1b[H = cursor home
|
|
@@ -155,7 +139,8 @@ function clearScreen() {
|
|
|
155
139
|
// ── List (grouped by project) ─────────────────────────────────────────────────
|
|
156
140
|
|
|
157
141
|
function cmdList(args) {
|
|
158
|
-
const
|
|
142
|
+
const projectFlagIdx = args.indexOf('--project');
|
|
143
|
+
const filterProject = projectFlagIdx !== -1 ? args[projectFlagIdx + 1] : args[0];
|
|
159
144
|
const entries = getContext({ project: filterProject, limit: 100 });
|
|
160
145
|
const allDiscussions = listDiscussions({ project: filterProject });
|
|
161
146
|
const allGraphs = listGraphs();
|
|
@@ -163,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
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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(`${
|
|
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
|
-
|
|
200
|
-
|
|
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 ===
|
|
203
|
-
const builtAt = (
|
|
204
|
-
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${
|
|
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
|
-
|
|
209
|
-
|
|
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 ===
|
|
212
|
-
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('
|
|
213
|
-
pData.
|
|
214
|
-
const br = i === pData.
|
|
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)} ${
|
|
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
|
-
|
|
227
|
-
if (pData.discussions.length) {
|
|
232
|
+
if (pData.plans.length) {
|
|
228
233
|
secIdx++;
|
|
229
|
-
const isLast = secIdx ===
|
|
230
|
-
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('
|
|
231
|
-
pData.
|
|
232
|
-
const br
|
|
233
|
-
const
|
|
234
|
-
|
|
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}
|
|
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} ${
|
|
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)} ${
|
|
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
|
|
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
|
|
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
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
// ──
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
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
|
-
// ──
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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/
|
|
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 '
|
|
1185
|
-
clearScreen(); printCompactHeader('
|
|
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 '
|
|
1249
|
-
|
|
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':
|