context-mcp-server 1.0.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 +464 -0
- package/codegraph/__init__.py +0 -0
- package/codegraph/__main__.py +24 -0
- package/codegraph/__pycache__/__init__.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/__main__.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/config.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/scanner.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/cache.py +137 -0
- package/codegraph/config.py +31 -0
- package/codegraph/extractors/__init__.py +0 -0
- package/codegraph/extractors/__pycache__/__init__.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/audio_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/doc_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/__pycache__/image_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/ast_extractor.py +222 -0
- package/codegraph/extractors/audio_extractor.py +8 -0
- package/codegraph/extractors/doc_extractor.py +34 -0
- package/codegraph/extractors/image_extractor.py +26 -0
- package/codegraph/graph/__init__.py +0 -0
- package/codegraph/graph/__pycache__/__init__.cpython-313.pyc +0 -0
- 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/builder.py +145 -0
- package/codegraph/graph/clustering.py +40 -0
- package/codegraph/graph/query.py +283 -0
- package/codegraph/report.py +115 -0
- package/codegraph/scanner.py +92 -0
- package/codegraph/server.py +514 -0
- package/package.json +62 -0
- package/src/cli.js +1010 -0
- package/src/config.js +89 -0
- package/src/db.js +786 -0
- package/src/guard.js +20 -0
- package/src/hooks/autoContext.js +17 -0
- package/src/hooks/autoLink.js +7 -0
- package/src/http.js +765 -0
- package/src/index.js +47 -0
- package/src/search.js +50 -0
- package/src/server.js +80 -0
- package/src/summarizer.js +124 -0
- package/src/templates/AGENTS.md +76 -0
- package/src/templates/CLAUDE.md +94 -0
- package/src/templates/GEMINI.md +76 -0
- package/src/templates/cursor-rules.mdc +41 -0
- package/src/templates/windsurf-rules.md +35 -0
- package/src/tools/codegraph.js +215 -0
- package/src/tools/context.js +188 -0
- package/src/tools/discussion.js +123 -0
- package/src/tools/errorCheck.js +65 -0
- package/src/tools/fileTools.js +185 -0
- package/src/tools/gitTools.js +259 -0
- package/src/tools/search.js +55 -0
- package/src/vector.js +153 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* context-mcp CLI
|
|
4
|
+
* Browse, search, add, and manage your context store from the terminal.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
13
|
+
import {
|
|
14
|
+
saveContext, getContext,
|
|
15
|
+
deleteContext, deleteProject, listProjects,
|
|
16
|
+
listDiscussions, getStorePath, listGraphs,
|
|
17
|
+
} from './db.js';
|
|
18
|
+
import { getConfig, getConfigPath, saveConfig, saveSecretToKeytar } from './config.js';
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
20
|
+
import { search as unifiedSearch } from './search.js';
|
|
21
|
+
import { summarizeEntries } from './summarizer.js';
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
25
|
+
|
|
26
|
+
// ── ANSI color palette ────────────────────────────────────────────────────────
|
|
27
|
+
const C = {
|
|
28
|
+
reset: '\x1b[0m',
|
|
29
|
+
bold: '\x1b[1m',
|
|
30
|
+
dim: '\x1b[2m',
|
|
31
|
+
italic: '\x1b[3m',
|
|
32
|
+
navy: '\x1b[38;5;19m',
|
|
33
|
+
dblue: '\x1b[38;5;27m',
|
|
34
|
+
blue: '\x1b[38;5;33m',
|
|
35
|
+
lblue: '\x1b[38;5;39m',
|
|
36
|
+
tcyan: '\x1b[38;5;45m',
|
|
37
|
+
cyan: '\x1b[38;5;51m',
|
|
38
|
+
green: '\x1b[38;5;84m',
|
|
39
|
+
yellow: '\x1b[38;5;220m',
|
|
40
|
+
red: '\x1b[38;5;203m',
|
|
41
|
+
purple: '\x1b[38;5;135m',
|
|
42
|
+
gray: '\x1b[38;5;245m',
|
|
43
|
+
darkgray: '\x1b[38;5;238m',
|
|
44
|
+
white: '\x1b[38;5;255m',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const R = C.reset;
|
|
48
|
+
const color = (c, t) => `${c}${t}${R}`;
|
|
49
|
+
const bold = t => `${C.bold}${t}${R}`;
|
|
50
|
+
const dim = t => `${C.dim}${t}${R}`;
|
|
51
|
+
const italic = t => `${C.italic}${t}${R}`;
|
|
52
|
+
const ok = t => color(C.green, t);
|
|
53
|
+
const warn = t => color(C.yellow, t);
|
|
54
|
+
const bad = t => color(C.red, t);
|
|
55
|
+
const accent = t => color(C.tcyan, t);
|
|
56
|
+
const muted = t => color(C.gray, t);
|
|
57
|
+
const faint = t => color(C.darkgray, t);
|
|
58
|
+
const brand = t => color(C.cyan, t);
|
|
59
|
+
const lblue = t => color(C.lblue, t);
|
|
60
|
+
const highlight = t => `${C.bold}${C.white}${t}${R}`;
|
|
61
|
+
|
|
62
|
+
const GRAD = [C.navy, C.dblue, C.blue, C.lblue, C.tcyan, C.cyan];
|
|
63
|
+
const gradLine = (text, step) => `${GRAD[Math.min(step, GRAD.length - 1)]}${text}${R}`;
|
|
64
|
+
|
|
65
|
+
function line(width = 74) { return color(C.darkgray, '─'.repeat(width)); }
|
|
66
|
+
function dline(width = 74) { return color(C.dblue, '═'.repeat(width)); }
|
|
67
|
+
|
|
68
|
+
function pill(text, tone = 'tcyan') {
|
|
69
|
+
const cc = C[tone] || C.tcyan;
|
|
70
|
+
return `${cc}\x1b[7m ${text} ${R}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function safeTags(tags) { return Array.isArray(tags) ? tags : []; }
|
|
74
|
+
|
|
75
|
+
// ── Logo ──────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
const LOGO_LINES = [
|
|
78
|
+
' ██████╗ ██████╗ ███╗ ██╗████████╗███████╗██╗ ██╗████████╗',
|
|
79
|
+
'██╔════╝██╔═══██╗████╗ ██║╚══██╔══╝██╔════╝╚██╗██╔╝╚══██╔══╝',
|
|
80
|
+
'██║ ██║ ██║██╔██╗ ██║ ██║ █████╗ ╚███╔╝ ██║ ',
|
|
81
|
+
'██║ ██║ ██║██║╚██╗██║ ██║ ██╔══╝ ██╔██╗ ██║ ',
|
|
82
|
+
'╚██████╗╚██████╔╝██║ ╚████║ ██║ ███████╗██╔╝ ██╗ ██║ ',
|
|
83
|
+
' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function printBanner() {
|
|
87
|
+
console.log('');
|
|
88
|
+
LOGO_LINES.forEach((l, i) => console.log(gradLine(l, i)));
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(` ${bold(lblue('context-mcp'))} ${faint('v' + pkg.version)} ${faint('│')} ${italic(muted('persistent memory + knowledge graph for AI'))}`);
|
|
91
|
+
console.log(` ${faint('store ')} ${muted(getStorePath())}`);
|
|
92
|
+
console.log(` ${faint('config ')} ${muted(getConfigPath())}`);
|
|
93
|
+
console.log('');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Section header ────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function printSection(title, meta = '') {
|
|
99
|
+
const metaPart = meta ? ` ${faint(meta)}` : '';
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(` ${bold(lblue(title.toUpperCase()))}${metaPart}`);
|
|
102
|
+
console.log(` ${color(C.darkgray, '─'.repeat(62))}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Help ──────────────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function printUsage() {
|
|
108
|
+
printBanner();
|
|
109
|
+
printSection('Commands');
|
|
110
|
+
const cmd = (c, desc) => console.log(` ${accent(c.padEnd(40))} ${faint(desc)}`);
|
|
111
|
+
cmd('ctx', 'open interactive mode');
|
|
112
|
+
cmd('ctx list [project]', 'list entries + discussions + graphs');
|
|
113
|
+
cmd('ctx search <query>', 'keyword → semantic fallback search');
|
|
114
|
+
cmd('ctx add', 'add entry interactively');
|
|
115
|
+
cmd('ctx delete <id-prefix>', 'delete one entry');
|
|
116
|
+
cmd('ctx delete project <name|id>', 'delete all entries for a project (by name or id)');
|
|
117
|
+
cmd('ctx summary [project]', 'summarize recent entries');
|
|
118
|
+
cmd('ctx projects', 'show all projects + graphs');
|
|
119
|
+
cmd('ctx discuss [project]', 'show discussions');
|
|
120
|
+
cmd('ctx benchmark', 'token savings report (memory + graph)');
|
|
121
|
+
console.log('');
|
|
122
|
+
cmd('ctx install --<platform>', 'write MCP config + instruction file for an AI platform');
|
|
123
|
+
cmd('ctx install --all', 'install for all platforms at once');
|
|
124
|
+
cmd('ctx online [--port N]', 'start HTTP server + show credentials for Claude.ai / ChatGPT');
|
|
125
|
+
cmd('ctx settings', 'view and edit config (port, host, client id/secret)');
|
|
126
|
+
console.log('');
|
|
127
|
+
cmd('ctx help', 'show this screen');
|
|
128
|
+
console.log('');
|
|
129
|
+
}
|
|
130
|
+
function clearScreen() {
|
|
131
|
+
// \x1b[2J = clear screen, \x1b[3J = clear scrollback, \x1b[H = cursor home
|
|
132
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── List (grouped by project) ─────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
function cmdList(args) {
|
|
138
|
+
const filterProject = args[0];
|
|
139
|
+
const entries = getContext({ project: filterProject, limit: 100 });
|
|
140
|
+
const allDiscussions = listDiscussions({ project: filterProject });
|
|
141
|
+
const allGraphs = listGraphs();
|
|
142
|
+
const projectRegistry = new Map(listProjects().map(p => [p.name, p]));
|
|
143
|
+
|
|
144
|
+
printSection('Context', filterProject ? `project: ${filterProject}` : 'all projects');
|
|
145
|
+
|
|
146
|
+
// Build per-project map
|
|
147
|
+
const projects = {};
|
|
148
|
+
for (const entry of entries) {
|
|
149
|
+
const p = entry.project || 'global';
|
|
150
|
+
if (!projects[p]) projects[p] = { contexts: [], discussions: [] };
|
|
151
|
+
projects[p].contexts.push(entry);
|
|
152
|
+
}
|
|
153
|
+
for (const disc of allDiscussions) {
|
|
154
|
+
const p = disc.project || 'global';
|
|
155
|
+
if (!projects[p]) projects[p] = { contexts: [], discussions: [] };
|
|
156
|
+
projects[p].discussions.push(disc);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const projectNames = Object.keys(projects).sort();
|
|
160
|
+
|
|
161
|
+
if (!projectNames.length) {
|
|
162
|
+
console.log(` ${faint('no entries, discussions, or graphs found')}`);
|
|
163
|
+
console.log('');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const projectName of projectNames) {
|
|
168
|
+
const pData = projects[projectName];
|
|
169
|
+
const graph = allGraphs.find(g => g.path?.toLowerCase().includes(projectName.toLowerCase()));
|
|
170
|
+
const activeD = pData.discussions.filter(d => d.status === 'active').length;
|
|
171
|
+
const totalSecs = (pData.contexts.length > 0 ? 1 : 0) + (pData.discussions.length > 0 ? 1 : 0) + (graph ? 1 : 0);
|
|
172
|
+
let secIdx = 0;
|
|
173
|
+
|
|
174
|
+
const projReg = projectRegistry.get(projectName);
|
|
175
|
+
const projIdStr = projReg?.id ? faint(' id:' + projReg.id.slice(0, 8)) : '';
|
|
176
|
+
console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(projectName))}${projIdStr} ${faint(`${pData.contexts.length} entries · ${pData.discussions.length} discussions`)}${activeD ? ` ${warn('● ' + activeD + ' active')}` : ''}`);
|
|
177
|
+
console.log(` ${color(C.darkgray, '│')}`);
|
|
178
|
+
|
|
179
|
+
// ── Graph ────────────────────────────────────────────────────────────────
|
|
180
|
+
if (graph) {
|
|
181
|
+
secIdx++;
|
|
182
|
+
const isLast = secIdx === totalSecs;
|
|
183
|
+
const builtAt = (graph.builtAt || '').slice(0, 10);
|
|
184
|
+
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graph.nodes}n · ${graph.edges}e · ${graph.communities} clusters · ${builtAt}`)}`);
|
|
185
|
+
if (!isLast) console.log(` ${color(C.darkgray, '│')}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Context entries ───────────────────────────────────────────────────────
|
|
189
|
+
if (pData.contexts.length) {
|
|
190
|
+
secIdx++;
|
|
191
|
+
const isLast = secIdx === totalSecs;
|
|
192
|
+
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('memory')} ${faint(pData.contexts.length + ' entries')}`);
|
|
193
|
+
pData.contexts.forEach((item, i) => {
|
|
194
|
+
const br = i === pData.contexts.length - 1 ? '└─' : '├─';
|
|
195
|
+
const date = (item.createdAt || '').slice(0, 10);
|
|
196
|
+
const type = item.type || 'note';
|
|
197
|
+
const id = item.id.slice(0, 8);
|
|
198
|
+
const tags = safeTags(item.tags);
|
|
199
|
+
const pipe = isLast ? ' ' : '│';
|
|
200
|
+
console.log(` ${color(C.darkgray, pipe)} ${color(C.darkgray, br)} ${pill(type)} ${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 (!isLast) console.log(` ${color(C.darkgray, '│')}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Discussions ───────────────────────────────────────────────────────────
|
|
207
|
+
if (pData.discussions.length) {
|
|
208
|
+
secIdx++;
|
|
209
|
+
const isLast = secIdx === totalSecs;
|
|
210
|
+
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${muted('discussions')} ${faint(pData.discussions.length + ' total')}`);
|
|
211
|
+
pData.discussions.forEach((disc, i) => {
|
|
212
|
+
const br = i === pData.discussions.length - 1 ? '└─' : '├─';
|
|
213
|
+
const sc = disc.status === 'done' ? 'green' : 'tcyan';
|
|
214
|
+
const steps = disc.stepsSummary?.total ? faint(` ${disc.stepsSummary.done}/${disc.stepsSummary.total}`) : '';
|
|
215
|
+
const pipe = isLast ? ' ' : '│';
|
|
216
|
+
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}`);
|
|
217
|
+
if (disc.description) console.log(` ${color(C.darkgray, pipe)} ${faint(disc.description)}`);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Orphan graphs (no matching project)
|
|
223
|
+
const orphanGraphs = allGraphs.filter(g => !projectNames.some(p => g.path?.toLowerCase().includes(p.toLowerCase())));
|
|
224
|
+
if (orphanGraphs.length) {
|
|
225
|
+
console.log(`\n ${color(C.dblue, '◇')} ${muted('other graphs')}`);
|
|
226
|
+
for (const g of orphanGraphs) {
|
|
227
|
+
const pathShort = g.path.replace(/\\/g, '/').split('/').slice(-2).join('/');
|
|
228
|
+
console.log(` ${accent('⬡')} ${bold(pathShort)} ${faint(`${g.nodes}n · ${g.edges}e · ${g.communities} clusters`)}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log('');
|
|
233
|
+
console.log(line());
|
|
234
|
+
console.log(faint(` ${entries.length} entries · ${allDiscussions.length} discussions · ${allGraphs.length} graphs · ${projectNames.length} projects`));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Search ────────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function cmdSearch(args) {
|
|
240
|
+
const query = args.join(' ');
|
|
241
|
+
if (!query) { console.log(bad(' usage: ctx search <query>')); return; }
|
|
242
|
+
|
|
243
|
+
let results = unifiedSearch({ mode: 'keyword', query, limit: 10 });
|
|
244
|
+
const mode = results.length ? 'keyword' : 'semantic';
|
|
245
|
+
if (!results.length) results = unifiedSearch({ mode: 'semantic', query, limit: 10 });
|
|
246
|
+
|
|
247
|
+
printSection('Search', `${mode} · "${query}"`);
|
|
248
|
+
if (!results.length) { console.log(` ${faint('no results')}`); return; }
|
|
249
|
+
|
|
250
|
+
results.forEach((entry, index) => {
|
|
251
|
+
const score = entry.similarity !== undefined ? ok(` ${Math.round(entry.similarity * 100)}%`) : '';
|
|
252
|
+
const date = (entry.createdAt || '').slice(0, 10);
|
|
253
|
+
const id = entry.id.slice(0, 8);
|
|
254
|
+
const type = entry.type || 'note';
|
|
255
|
+
const isLast = index === results.length - 1;
|
|
256
|
+
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${bold(entry.title || '(no title)')}${score} ${pill(type)} ${faint('id:' + id)} ${faint(date)}`);
|
|
257
|
+
});
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(line());
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Projects ──────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
function cmdProjects() {
|
|
265
|
+
const projectList = listProjects();
|
|
266
|
+
const graphs = listGraphs();
|
|
267
|
+
const allDiscs = listDiscussions({});
|
|
268
|
+
printSection('Projects');
|
|
269
|
+
if (!projectList.length) { console.log(` ${faint('no projects yet')}`); return; }
|
|
270
|
+
|
|
271
|
+
for (const project of projectList) {
|
|
272
|
+
const entries = getContext({ project: project.name, limit: 3, compact: true }).filter(e => e.status !== 'archived');
|
|
273
|
+
const discs = allDiscs.filter(d => (d.project || 'global') === project.name);
|
|
274
|
+
const activeD = discs.filter(d => d.status === 'active');
|
|
275
|
+
const graph = graphs.find(g => g.path?.toLowerCase().includes(project.name.toLowerCase()));
|
|
276
|
+
|
|
277
|
+
const barLen = Math.min(Math.ceil(project.count / 2), 24);
|
|
278
|
+
const bar = color(C.dblue, '█'.repeat(barLen)) + color(C.darkgray, '░'.repeat(24 - barLen));
|
|
279
|
+
|
|
280
|
+
const idTag = project.id ? faint(' id:' + project.id.slice(0, 8)) : '';
|
|
281
|
+
console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(project.name))}${idTag} ${bar} ${faint(project.count + ' entries')}`);
|
|
282
|
+
console.log(` ${color(C.darkgray, '│')}`);
|
|
283
|
+
|
|
284
|
+
// Graph status
|
|
285
|
+
if (graph) {
|
|
286
|
+
const builtAt = (graph.builtAt || '').slice(0, 10);
|
|
287
|
+
console.log(` ${color(C.darkgray, '├─')} ${accent('⬡')} ${muted('graph')} ${faint(`${graph.nodes}n · ${graph.edges}e · ${graph.communities} clusters · ${builtAt}`)}`);
|
|
288
|
+
} else {
|
|
289
|
+
console.log(` ${color(C.darkgray, '├─')} ${faint('⬡ no graph')}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Recent context
|
|
293
|
+
if (entries.length) {
|
|
294
|
+
console.log(` ${color(C.darkgray, '├─')} ${muted('recent')}`);
|
|
295
|
+
entries.forEach((e, i) => {
|
|
296
|
+
const br = i === entries.length - 1 && !activeD.length ? '└─' : '├─';
|
|
297
|
+
const type = e.type || 'note';
|
|
298
|
+
const date = (e.createdAt || '').slice(0, 10);
|
|
299
|
+
console.log(` ${color(C.darkgray, '│')} ${color(C.darkgray, br)} ${pill(type)} ${bold(e.title || '(no title)')} ${faint(date)}`);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Active discussions
|
|
304
|
+
if (activeD.length) {
|
|
305
|
+
console.log(` ${color(C.darkgray, '├─')} ${muted('discussions')}`);
|
|
306
|
+
activeD.forEach((d, i) => {
|
|
307
|
+
const br = i === activeD.length - 1 ? '└─' : '├─';
|
|
308
|
+
const steps = d.stepsSummary?.total ? faint(` ${d.stepsSummary.done}/${d.stepsSummary.total}`) : '';
|
|
309
|
+
console.log(` ${color(C.darkgray, '│')} ${color(C.darkgray, br)} ${warn('●')} ${bold(d.name)} ${faint(d.type || 'plan')}${steps}`);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(` ${color(C.darkgray, '│')}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log('');
|
|
317
|
+
console.log(line());
|
|
318
|
+
console.log(faint(` ${projectList.length} projects · ${projectList.reduce((a, p) => a + p.count, 0)} entries · ${graphs.length} graphs`));
|
|
319
|
+
console.log('');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── Discussions ───────────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
function cmdDiscussions(args) {
|
|
325
|
+
const filterProject = args[0];
|
|
326
|
+
const discussions = listDiscussions({ project: filterProject });
|
|
327
|
+
printSection('Discussions', filterProject || 'all projects');
|
|
328
|
+
|
|
329
|
+
if (!discussions.length) {
|
|
330
|
+
console.log(` ${faint('no discussions yet')}`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const byType = {};
|
|
335
|
+
for (const disc of discussions) {
|
|
336
|
+
const t = disc.type || 'plan';
|
|
337
|
+
if (!byType[t]) byType[t] = [];
|
|
338
|
+
byType[t].push(disc);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for (const [type, items] of Object.entries(byType)) {
|
|
342
|
+
console.log(`\n ${color(C.dblue, '◆')} ${bold(lblue(type.toUpperCase()))} ${faint(items.length + '')}`);
|
|
343
|
+
items.forEach((disc, i) => {
|
|
344
|
+
const isLast = i === items.length - 1;
|
|
345
|
+
const sc = disc.status === 'done' ? 'green' : 'tcyan';
|
|
346
|
+
const steps = disc.stepsSummary?.total
|
|
347
|
+
? faint(` ${disc.stepsSummary.done}/${disc.stepsSummary.total} steps`)
|
|
348
|
+
: '';
|
|
349
|
+
const tags = safeTags(disc.tags).map(t => pill(t, 'purple')).join(' ');
|
|
350
|
+
console.log(` ${color(C.darkgray, isLast ? '└─' : '├─')} ${bold(disc.name)} ${pill(disc.status, sc)}${steps} ${tags}`);
|
|
351
|
+
if (disc.description) console.log(` ${color(C.darkgray, isLast ? ' ' : '│')} ${faint(disc.description)}`);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log('');
|
|
356
|
+
console.log(line());
|
|
357
|
+
console.log(faint(` ${discussions.length} discussion(s)`));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Summary ───────────────────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
function cmdSummary(args) {
|
|
363
|
+
const project = args[0];
|
|
364
|
+
const entries = getContext({ project, limit: 50 });
|
|
365
|
+
printSection('Summary', project || 'global');
|
|
366
|
+
if (!entries.length) { console.log(` ${faint('no entries to summarize')}`); return; }
|
|
367
|
+
|
|
368
|
+
const md = summarizeEntries(entries, { project: project || 'global' });
|
|
369
|
+
const rendered = md
|
|
370
|
+
.replace(/^## (.+)/gm, (_, t) => `\n${bold(lblue(t))}`)
|
|
371
|
+
.replace(/^### (.+)/gm, (_, t) => `\n${accent(t)}`)
|
|
372
|
+
.replace(/\*\*(.+?)\*\*/g, (_, t) => bold(t))
|
|
373
|
+
.replace(/`([^`]+)`/g, (_, t) => warn(t))
|
|
374
|
+
.replace(/^- /gm, ' • ');
|
|
375
|
+
console.log(rendered.trim());
|
|
376
|
+
console.log('');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Benchmark ────────────────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
function _walkBytes(dirPath) {
|
|
383
|
+
const walkScript =
|
|
384
|
+
`const fs=require('fs'),path=require('path');` +
|
|
385
|
+
`function walk(d,t=0){try{for(const f of fs.readdirSync(d)){` +
|
|
386
|
+
`const p=path.join(d,f);try{const s=fs.statSync(p);` +
|
|
387
|
+
`if(s.isDirectory()&&!['node_modules','.git','codegraph-cache','.venv','venv','__pycache__','dist','build','.next'].includes(f))t+=walk(p);` +
|
|
388
|
+
`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;}` +
|
|
389
|
+
`console.log(walk(${JSON.stringify(dirPath)}));`;
|
|
390
|
+
const res = spawnSync('node', ['-e', walkScript], { encoding: 'utf8', timeout: 8000 });
|
|
391
|
+
return res.stdout ? parseInt(res.stdout.trim()) : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function _sampleQueryTokens(graphPath) {
|
|
395
|
+
const questions = ['what does the server do', 'how is the graph built', 'what calls save'];
|
|
396
|
+
const sizes = [];
|
|
397
|
+
for (const q of questions) {
|
|
398
|
+
const req = JSON.stringify({ tool: 'codegraph_query', args: { path: graphPath, question: q, token_budget: 2000 } });
|
|
399
|
+
const res = spawnSync('uv', ['run', 'python', '-m', 'codegraph'],
|
|
400
|
+
{ input: req, encoding: 'utf8', cwd: process.cwd(), timeout: 12000 });
|
|
401
|
+
if (res.stdout) { try { const r = JSON.parse(res.stdout); if (!r.error) sizes.push(res.stdout.length); } catch {} }
|
|
402
|
+
}
|
|
403
|
+
return { avgTok: sizes.length ? Math.round(sizes.reduce((a, b) => a + b, 0) / sizes.length / 4) : 472, measured: sizes.length > 0 };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function cmdBenchmark() {
|
|
407
|
+
const graphs = listGraphs();
|
|
408
|
+
const projects = listProjects();
|
|
409
|
+
printSection('Benchmark', 'real token savings');
|
|
410
|
+
|
|
411
|
+
const RESUME_LIMIT = 15;
|
|
412
|
+
const COMPACT_AT = 50;
|
|
413
|
+
|
|
414
|
+
// ── Measure entry sizes from actual stored data ──────────────────────────────
|
|
415
|
+
const allEntries = getContext({ limit: 500, compact: false });
|
|
416
|
+
const totalEntries = allEntries.length;
|
|
417
|
+
let avgFullTok = 155, avgCompactTok = 50;
|
|
418
|
+
if (allEntries.length) {
|
|
419
|
+
const fullSizes = allEntries.map(e => JSON.stringify(e).length);
|
|
420
|
+
const compactSizes = allEntries.map(e =>
|
|
421
|
+
([e.title || '', (e.content || '').slice(0, 200), (e.tags || []).join(' ')].join(' ')).length);
|
|
422
|
+
avgFullTok = Math.round(fullSizes.reduce((a, b) => a + b, 0) / fullSizes.length / 4);
|
|
423
|
+
avgCompactTok = Math.round(compactSizes.reduce((a, b) => a + b, 0) / compactSizes.length / 4);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ── Run graph queries once, reuse in both sections ────────────────────────────
|
|
427
|
+
let avgQueryTok = 472, queryMeasured = false;
|
|
428
|
+
if (graphs.length) {
|
|
429
|
+
const r = _sampleQueryTokens(graphs[0].path);
|
|
430
|
+
avgQueryTok = r.avgTok;
|
|
431
|
+
queryMeasured = r.measured;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Corpus size (run once) ────────────────────────────────────────────────────
|
|
435
|
+
let corpusToks = null;
|
|
436
|
+
if (graphs.length) {
|
|
437
|
+
const bytes = _walkBytes(graphs[0].path);
|
|
438
|
+
if (bytes) corpusToks = Math.round(bytes / 4);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Memory ───────────────────────────────────────────────────────────────────
|
|
442
|
+
// Without context-mcp: AI reads all entries at full size every conversation
|
|
443
|
+
// With context-mcp: AI loads min(15, N) entries as compact previews via resume
|
|
444
|
+
console.log(`\n ${bold(lblue('Memory'))} ${faint('measured from actual stored entries')}`);
|
|
445
|
+
if (totalEntries) {
|
|
446
|
+
const resumeCount = Math.min(RESUME_LIMIT, totalEntries);
|
|
447
|
+
const withoutMemTok = totalEntries * avgFullTok; // load all entries full
|
|
448
|
+
const withMemTok = resumeCount * avgCompactTok; // resume: compact previews only
|
|
449
|
+
const memSaved = withoutMemTok - withMemTok;
|
|
450
|
+
const memReduction = (withoutMemTok / withMemTok).toFixed(1);
|
|
451
|
+
const memPct = ((1 - withMemTok / withoutMemTok) * 100).toFixed(1);
|
|
452
|
+
|
|
453
|
+
console.log(` ${faint('avg entry size (full): ')} ${muted(avgFullTok)} tok ${faint('(measured)')}`);
|
|
454
|
+
console.log(` ${faint('avg entry size (compact):')} ${muted(avgCompactTok)} tok ${faint('(measured)')}`);
|
|
455
|
+
console.log(` ${faint('stored: ')} ${muted(totalEntries)} entries ${faint('across')} ${muted(projects.length)} project(s)`);
|
|
456
|
+
console.log(` ${faint('without — load all full: ')} ${warn('~' + withoutMemTok.toLocaleString('en-US'))} tokens`);
|
|
457
|
+
console.log(` ${faint('with — resume compact:')} ${ok('~' + withMemTok.toLocaleString('en-US'))} tokens ${faint(`(${resumeCount} of ${totalEntries} entries)`)}`);
|
|
458
|
+
console.log(` ${faint('saved per chat: ')} ${ok('~' + memSaved.toLocaleString('en-US'))} tokens ${highlight(memReduction + '×')} ${ok(memPct + '%')} reduction`);
|
|
459
|
+
console.log(` ${faint('auto-compact at: ')} ${faint(COMPACT_AT + ' entries → oldest summarized to 1')}`);
|
|
460
|
+
console.log('');
|
|
461
|
+
for (const p of projects) {
|
|
462
|
+
const pToks = p.count * avgFullTok;
|
|
463
|
+
const barLen = Math.min(Math.ceil(p.count / 2), 24);
|
|
464
|
+
const bar = color(C.dblue, '█'.repeat(barLen)) + color(C.darkgray, '░'.repeat(24 - barLen));
|
|
465
|
+
console.log(` ${color(C.darkgray, '·')} ${muted(p.name.padEnd(22))} ${bar} ${faint(p.count + ' entries · ~' + pToks.toLocaleString('en-US') + ' tok full')}`);
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
console.log(` ${faint('no entries yet')}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ── CodeGraph ─────────────────────────────────────────────────────────────────
|
|
472
|
+
// Without context-mcp: AI reads all source files to answer structural questions
|
|
473
|
+
// With context-mcp: AI calls codegraph_query → focused NODE/EDGE subgraph
|
|
474
|
+
console.log(`\n ${bold(lblue('CodeGraph'))} ${faint('measured from live graph queries')}`);
|
|
475
|
+
if (!graphs.length) {
|
|
476
|
+
console.log(` ${faint('no graphs — run codegraph_build first')}`);
|
|
477
|
+
} else {
|
|
478
|
+
for (const g of graphs) {
|
|
479
|
+
const pathShort = g.path.replace(/\\/g, '/').split('/').slice(-2).join('/');
|
|
480
|
+
const builtAt = (g.builtAt || '').slice(0, 10);
|
|
481
|
+
const graphReduce = corpusToks ? (corpusToks / avgQueryTok).toFixed(0) + '×' : null;
|
|
482
|
+
const graphPct = corpusToks ? ((1 - avgQueryTok / corpusToks) * 100).toFixed(2) : null;
|
|
483
|
+
|
|
484
|
+
console.log(`\n ${accent('⬡')} ${bold(pathShort)} ${faint(builtAt)}`);
|
|
485
|
+
console.log(` ${faint('nodes:')} ${muted(g.nodes)} ${faint('edges:')} ${muted(g.edges)} ${faint('clusters:')} ${muted(g.communities)}`);
|
|
486
|
+
if (corpusToks) console.log(` ${faint('without — read all files:')} ${warn('~' + corpusToks.toLocaleString('en-US'))} tokens ${faint('(all source + config + doc files)')}`);
|
|
487
|
+
console.log(` ${faint('with — graph query: ')} ${ok('~' + avgQueryTok.toLocaleString('en-US'))} tokens ${faint(queryMeasured ? '(avg of 3 live queries)' : '(calibrated fallback)')}`);
|
|
488
|
+
if (graphReduce) console.log(` ${faint('saved per query: ')} ${highlight(graphReduce)} ${ok(graphPct + '%')} fewer tokens`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ── Combined ──────────────────────────────────────────────────────────────────
|
|
493
|
+
// Without: read all files (no graph) + load all entries full (no memory system)
|
|
494
|
+
// With: compact resume (memory) + one graph query (codegraph)
|
|
495
|
+
if (graphs.length) {
|
|
496
|
+
const resumeCount = Math.min(RESUME_LIMIT, totalEntries);
|
|
497
|
+
const withMemTok = resumeCount * avgCompactTok;
|
|
498
|
+
const withMcp = withMemTok + avgQueryTok;
|
|
499
|
+
const withoutMemTok = totalEntries * avgFullTok;
|
|
500
|
+
const withoutMcp = withoutMemTok + (corpusToks || graphs[0].nodes * 80);
|
|
501
|
+
const totalRed = withoutMcp > 0 ? (withoutMcp / withMcp).toFixed(0) : '—';
|
|
502
|
+
const totalPct = withoutMcp > 0 ? ((1 - withMcp / withoutMcp) * 100).toFixed(2) : '—';
|
|
503
|
+
|
|
504
|
+
console.log(`\n ${bold(lblue('Combined'))} ${faint('per conversation')}`);
|
|
505
|
+
console.log(` ${faint('without context-mcp: ')} ${warn('~' + withoutMcp.toLocaleString('en-US'))} tokens ${faint('(all entries full + all files read directly)')}`);
|
|
506
|
+
console.log(` ${faint('with context-mcp: ')} ${ok('~' + withMcp.toLocaleString('en-US'))} tokens ${faint('(compact resume + 1 graph query)')}`);
|
|
507
|
+
console.log(` ${faint('total reduction: ')} ${highlight(totalRed + '×')} ${ok(totalPct + '%')} fewer tokens`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
console.log('');
|
|
511
|
+
console.log(line());
|
|
512
|
+
console.log(faint(' token estimate: chars ÷ 4 · corpus = all source/config/doc files (excl. lock files, .venv, node_modules)'));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
// ── Install ───────────────────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
const TPLS = join(__dirname, 'templates');
|
|
519
|
+
|
|
520
|
+
function _tpl(name) {
|
|
521
|
+
const p = join(TPLS, name);
|
|
522
|
+
return existsSync(p) ? readFileSync(p, 'utf8') : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function _writeFile(filePath, content, label) {
|
|
526
|
+
const dir = dirname(filePath);
|
|
527
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
528
|
+
writeFileSync(filePath, content, 'utf8');
|
|
529
|
+
console.log(` ${ok('✓')} ${label.padEnd(28)} ${faint(filePath.replace(/\\/g, '/'))}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const PLATFORMS = {
|
|
533
|
+
claude: {
|
|
534
|
+
label: 'Claude Code',
|
|
535
|
+
install(cwd) {
|
|
536
|
+
const mcpJson = JSON.stringify({
|
|
537
|
+
mcpServers: { 'context-mcp': { command: 'npx', args: ['-y', 'context-mcp-server@latest'] } },
|
|
538
|
+
}, null, 2);
|
|
539
|
+
_writeFile(join(cwd, '.claude', 'mcp.json'), mcpJson, '.claude/mcp.json');
|
|
540
|
+
const md = _tpl('CLAUDE.md');
|
|
541
|
+
if (md) _writeFile(join(cwd, 'CLAUDE.md'), md, 'CLAUDE.md');
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
cursor: {
|
|
545
|
+
label: 'Cursor',
|
|
546
|
+
install(cwd) {
|
|
547
|
+
const mcpJson = JSON.stringify({
|
|
548
|
+
mcpServers: { 'context-mcp': { command: 'npx', args: ['-y', 'context-mcp-server@latest'] } },
|
|
549
|
+
}, null, 2);
|
|
550
|
+
_writeFile(join(cwd, '.cursor', 'mcp.json'), mcpJson, '.cursor/mcp.json');
|
|
551
|
+
const mdc = _tpl('cursor-rules.mdc');
|
|
552
|
+
if (mdc) _writeFile(join(cwd, '.cursor', 'rules', 'context-mcp.mdc'), mdc, '.cursor/rules/context-mcp.mdc');
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
vscode: {
|
|
556
|
+
label: 'VS Code Copilot',
|
|
557
|
+
install(cwd) {
|
|
558
|
+
const mcpJson = JSON.stringify({
|
|
559
|
+
servers: { 'context-mcp': { type: 'stdio', command: 'npx', args: ['-y', 'context-mcp-server@latest'] } },
|
|
560
|
+
}, null, 2);
|
|
561
|
+
_writeFile(join(cwd, '.vscode', 'mcp.json'), mcpJson, '.vscode/mcp.json');
|
|
562
|
+
const md = _tpl('CLAUDE.md');
|
|
563
|
+
if (md) _writeFile(join(cwd, 'CLAUDE.md'), md, 'CLAUDE.md');
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
gemini: {
|
|
567
|
+
label: 'Gemini CLI',
|
|
568
|
+
install(cwd) {
|
|
569
|
+
const cfg = JSON.stringify({
|
|
570
|
+
mcpServers: { 'context-mcp': { command: 'npx', args: ['-y', 'context-mcp-server@latest'] } },
|
|
571
|
+
}, null, 2);
|
|
572
|
+
_writeFile(join(cwd, '.gemini', 'settings.json'), cfg, '.gemini/settings.json');
|
|
573
|
+
const md = _tpl('GEMINI.md');
|
|
574
|
+
if (md) _writeFile(join(cwd, 'GEMINI.md'), md, 'GEMINI.md');
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
codex: {
|
|
578
|
+
label: 'Codex CLI',
|
|
579
|
+
install(cwd) {
|
|
580
|
+
const toml = `[[mcp_servers]]\nname = "context-mcp"\ncommand = "npx"\nargs = ["-y", "context-mcp-server@latest"]\n`;
|
|
581
|
+
_writeFile(join(cwd, '.codex', 'config.toml'), toml, '.codex/config.toml');
|
|
582
|
+
const md = _tpl('AGENTS.md');
|
|
583
|
+
if (md) _writeFile(join(cwd, 'AGENTS.md'), md, 'AGENTS.md');
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
windsurf: {
|
|
587
|
+
label: 'Windsurf',
|
|
588
|
+
install(cwd) {
|
|
589
|
+
// Local rule file
|
|
590
|
+
const rules = _tpl('windsurf-rules.md');
|
|
591
|
+
if (rules) _writeFile(join(cwd, '.windsurf', 'rules', 'context-mcp.md'), rules, '.windsurf/rules/context-mcp.md');
|
|
592
|
+
// Global Windsurf config
|
|
593
|
+
const globalCfgPath = join(homedir(), '.codeium', 'windsurf', 'mcp_config.json');
|
|
594
|
+
let existing = {};
|
|
595
|
+
try { existing = JSON.parse(readFileSync(globalCfgPath, 'utf8')); } catch {}
|
|
596
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
597
|
+
existing.mcpServers['context-mcp'] = { command: 'npx', args: ['-y', 'context-mcp-server@latest'] };
|
|
598
|
+
_writeFile(globalCfgPath, JSON.stringify(existing, null, 2), '~/.codeium/windsurf/mcp_config.json');
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
function cmdInstall(args) {
|
|
604
|
+
const flags = new Set(args.map(a => a.replace(/^--/, '')));
|
|
605
|
+
const all = flags.has('all');
|
|
606
|
+
const keys = all ? Object.keys(PLATFORMS) : Object.keys(PLATFORMS).filter(k => flags.has(k));
|
|
607
|
+
|
|
608
|
+
if (!keys.length) {
|
|
609
|
+
printSection('Install');
|
|
610
|
+
console.log(` ${muted('Usage:')} ctx install ${faint('[--claude] [--cursor] [--vscode] [--gemini] [--codex] [--windsurf] [--all]')}`);
|
|
611
|
+
console.log('');
|
|
612
|
+
console.log(` Writes MCP config file + AI instruction file for each selected platform.`);
|
|
613
|
+
console.log(` Files are written into the ${accent('current directory')} (your project root).`);
|
|
614
|
+
console.log('');
|
|
615
|
+
for (const [k, p] of Object.entries(PLATFORMS)) {
|
|
616
|
+
console.log(` ${accent(('--' + k).padEnd(14))} ${faint(p.label)}`);
|
|
617
|
+
}
|
|
618
|
+
console.log(` ${accent('--all ')} ${faint('All platforms at once')}`);
|
|
619
|
+
console.log('');
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const cwd = process.cwd();
|
|
624
|
+
printSection('Install', keys.map(k => PLATFORMS[k].label).join(', '));
|
|
625
|
+
console.log('');
|
|
626
|
+
|
|
627
|
+
for (const key of keys) {
|
|
628
|
+
console.log(` ${bold(lblue(PLATFORMS[key].label))}`);
|
|
629
|
+
try {
|
|
630
|
+
PLATFORMS[key].install(cwd);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
console.log(` ${bad('✗')} failed: ${err.message}`);
|
|
633
|
+
}
|
|
634
|
+
console.log('');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
console.log(line());
|
|
638
|
+
console.log(faint(` ${keys.length} platform(s) installed into ${cwd.replace(/\\/g, '/')}`));
|
|
639
|
+
console.log('');
|
|
640
|
+
|
|
641
|
+
// ── Python / uv setup (codegraph) ─────────────────────────────────────────
|
|
642
|
+
console.log(` ${bold(lblue('Python Codegraph'))}`);
|
|
643
|
+
const uvCheck = spawnSync('uv', ['--version'], { encoding: 'utf8' });
|
|
644
|
+
if (uvCheck.error || uvCheck.status !== 0) {
|
|
645
|
+
console.log(` ${bad('✗')} uv not found — install it from ${accent('https://docs.astral.sh/uv/')} to enable codegraph`);
|
|
646
|
+
console.log('');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
console.log(` ${ok('✓')} uv found: ${faint(uvCheck.stdout.trim())}`);
|
|
650
|
+
|
|
651
|
+
// Package root is one level up from src/
|
|
652
|
+
const __dirname_cli = dirname(fileURLToPath(import.meta.url));
|
|
653
|
+
const pkgRoot = join(__dirname_cli, '..');
|
|
654
|
+
const sync = spawnSync('uv', ['sync', '--no-dev'], { cwd: pkgRoot, encoding: 'utf8' });
|
|
655
|
+
if (sync.status !== 0) {
|
|
656
|
+
console.log(` ${bad('✗')} uv sync failed:\n${faint((sync.stderr || sync.stdout || '').trim())}`);
|
|
657
|
+
} else {
|
|
658
|
+
console.log(` ${ok('✓')} Python environment ready — codegraph enabled`);
|
|
659
|
+
}
|
|
660
|
+
console.log('');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ── Online ────────────────────────────────────────────────────────────────────
|
|
664
|
+
|
|
665
|
+
function _httpPidFile(port) {
|
|
666
|
+
const dataDir = process.env.CONTEXT_MCP_DIR || join(homedir(), '.context-mcp');
|
|
667
|
+
if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
|
|
668
|
+
return join(dataDir, `http-${port}.pid`);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Check if something is actually listening on the port (cross-platform, reliable)
|
|
672
|
+
function _isPortListening(port) {
|
|
673
|
+
const script = `const n=require('net'),s=n.createConnection({port:${port},host:'localhost'});s.setTimeout(500);s.on('connect',()=>{s.destroy();process.exit(0);});s.on('error',()=>process.exit(1));s.on('timeout',()=>{s.destroy();process.exit(1);});`;
|
|
674
|
+
const r = spawnSync(process.execPath, ['-e', script], { timeout: 2000 });
|
|
675
|
+
return r.status === 0;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function _storedPid(port) {
|
|
679
|
+
const pidPath = _httpPidFile(port);
|
|
680
|
+
if (!existsSync(pidPath)) return null;
|
|
681
|
+
const pid = parseInt(readFileSync(pidPath, 'utf8').trim() || '0');
|
|
682
|
+
return pid || null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Returns { status: 'running', pid } | { status: 'none' }
|
|
686
|
+
function _checkExistingHttpServer(port) {
|
|
687
|
+
if (!_isPortListening(port)) {
|
|
688
|
+
// Clean up stale PID file if present
|
|
689
|
+
try { unlinkSync(_httpPidFile(port)); } catch {}
|
|
690
|
+
return { status: 'none' };
|
|
691
|
+
}
|
|
692
|
+
const pid = _storedPid(port);
|
|
693
|
+
return { status: 'running', pid };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function cmdOnline(args) {
|
|
697
|
+
const portIdx = args.indexOf('--port');
|
|
698
|
+
const port = portIdx !== -1 && args[portIdx + 1] ? args[portIdx + 1] : null;
|
|
699
|
+
const hostIdx = args.indexOf('--host');
|
|
700
|
+
const host = hostIdx !== -1 && args[hostIdx + 1] ? args[hostIdx + 1] : null;
|
|
701
|
+
const git = args.includes('--access-git');
|
|
702
|
+
const restart = args.includes('--restart');
|
|
703
|
+
|
|
704
|
+
let cfg;
|
|
705
|
+
try { cfg = getConfig(); } catch { cfg = { client_id: 'context-mcp', client_secret: '(unavailable)', port: 3100, host: 'localhost' }; }
|
|
706
|
+
|
|
707
|
+
const resolvedPort = port || cfg.port || 3100;
|
|
708
|
+
const resolvedHost = host || cfg.host || 'localhost';
|
|
709
|
+
|
|
710
|
+
printSection('Online', `HTTP MCP server → Claude.ai / ChatGPT`);
|
|
711
|
+
console.log('');
|
|
712
|
+
|
|
713
|
+
// Check if a server is already running on this port
|
|
714
|
+
const existing = _checkExistingHttpServer(resolvedPort);
|
|
715
|
+
if (existing.status === 'running') {
|
|
716
|
+
if (!restart) {
|
|
717
|
+
const pidStr = existing.pid ? `pid ${existing.pid} · ` : '';
|
|
718
|
+
console.log(` ${ok('✓')} ${bold('already running')} ${faint(pidStr + 'port ' + resolvedPort)}`);
|
|
719
|
+
console.log(` ${faint('Run')} ${accent('ctx online --restart')} ${faint('to force a restart')}\n`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
if (existing.pid) { try { process.kill(existing.pid); } catch {} }
|
|
723
|
+
try { unlinkSync(_httpPidFile(resolvedPort)); } catch {}
|
|
724
|
+
const stopMsg = existing.pid ? `stopped pid ${existing.pid}` : `port ${resolvedPort} was in use`;
|
|
725
|
+
console.log(` ${warn('⚠')} restarting ${faint('(' + stopMsg + ')')}`);
|
|
726
|
+
console.log('');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Credentials
|
|
730
|
+
console.log(` ${faint('client id')} ${accent(cfg.client_id)}`);
|
|
731
|
+
console.log(` ${faint('client secret')} ${ok(cfg.client_secret)}`);
|
|
732
|
+
console.log(` ${faint('config')} ${faint(getConfigPath())}`);
|
|
733
|
+
console.log('');
|
|
734
|
+
console.log(` ${faint('endpoint')} ${accent(`http://${resolvedHost}:${resolvedPort}`)}`);
|
|
735
|
+
console.log(` ${faint('oauth')} ${faint('POST')} ${accent(`http://${resolvedHost}:${resolvedPort}/oauth/token`)}`);
|
|
736
|
+
console.log('');
|
|
737
|
+
console.log(` ${faint('To connect Claude.ai / ChatGPT:')}`);
|
|
738
|
+
console.log(` ${faint('Settings → Integrations → Add MCP Connector')}`);
|
|
739
|
+
console.log(` ${faint('URL:')} ${accent(`http://${resolvedHost}:${resolvedPort}`)}`);
|
|
740
|
+
console.log(` ${faint('Use the client id and secret above when prompted')}`);
|
|
741
|
+
console.log('');
|
|
742
|
+
|
|
743
|
+
// Build args for the HTTP server
|
|
744
|
+
const httpBin = join(__dirname, 'http.js');
|
|
745
|
+
const spawnArgs = ['--port', String(resolvedPort)];
|
|
746
|
+
if (host) spawnArgs.push('--host', resolvedHost);
|
|
747
|
+
if (git) spawnArgs.push('--access-git');
|
|
748
|
+
|
|
749
|
+
// Spawn detached so HTTP server runs in background
|
|
750
|
+
const child = spawn(process.execPath, [httpBin, ...spawnArgs], {
|
|
751
|
+
detached: true,
|
|
752
|
+
stdio: 'ignore',
|
|
753
|
+
env: { ...process.env },
|
|
754
|
+
});
|
|
755
|
+
child.unref();
|
|
756
|
+
|
|
757
|
+
// Persist PID so next invocation can kill it
|
|
758
|
+
try { writeFileSync(_httpPidFile(resolvedPort), String(child.pid)); } catch {}
|
|
759
|
+
|
|
760
|
+
console.log(` ${ok('✓')} ${bold('HTTP server started')} ${faint('pid ' + child.pid + ' · port ' + resolvedPort)}`);
|
|
761
|
+
console.log(` ${faint('Run')} ${accent('ctx online')} ${faint('again to restart · or')} ${faint('kill ' + child.pid)}\n`);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ── Settings ─────────────────────────────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
async function cmdSettings(existingRl) {
|
|
767
|
+
const rl = existingRl || readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
768
|
+
const ask = q => new Promise(resolve => rl.question(` ${accent('›')} ${muted(q)} `, resolve));
|
|
769
|
+
|
|
770
|
+
printSection('Settings', getConfigPath());
|
|
771
|
+
|
|
772
|
+
const FIELDS = [
|
|
773
|
+
{ key: 'client_id', label: 'Client ID', desc: 'OAuth client identifier' },
|
|
774
|
+
{ key: 'client_secret', label: 'Client Secret', desc: 'OAuth client secret (keep private)' },
|
|
775
|
+
{ key: 'port', label: 'HTTP Port', desc: 'Port for ctx online server', coerce: Number },
|
|
776
|
+
{ key: 'host', label: 'Host', desc: 'Bind address for ctx online server' },
|
|
777
|
+
{ key: 'access_git', label: 'Access Git', desc: 'Allow git tools (true/false)', coerce: v => v === 'true' },
|
|
778
|
+
];
|
|
779
|
+
|
|
780
|
+
let cfg;
|
|
781
|
+
try { cfg = getConfig(); } catch { cfg = {}; }
|
|
782
|
+
|
|
783
|
+
// Display current values
|
|
784
|
+
console.log('');
|
|
785
|
+
FIELDS.forEach((f, i) => {
|
|
786
|
+
const val = cfg[f.key];
|
|
787
|
+
const display = f.key === 'client_secret' ? val?.slice(0, 8) + '...' : String(val ?? '');
|
|
788
|
+
console.log(` ${faint((i + 1) + '.')} ${muted(f.label.padEnd(16))} ${accent(display)} ${faint(f.desc)}`);
|
|
789
|
+
});
|
|
790
|
+
console.log('');
|
|
791
|
+
console.log(` ${faint('Enter a number to edit, or press Enter to exit.')}`);
|
|
792
|
+
console.log('');
|
|
793
|
+
|
|
794
|
+
const choice = (await ask('Edit field (1-' + FIELDS.length + '):')).trim();
|
|
795
|
+
if (!existingRl) rl.close();
|
|
796
|
+
|
|
797
|
+
const idx = parseInt(choice) - 1;
|
|
798
|
+
if (isNaN(idx) || idx < 0 || idx >= FIELDS.length) {
|
|
799
|
+
console.log(` ${faint('no changes made')}`);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const field = FIELDS[idx];
|
|
804
|
+
const current = cfg[field.key];
|
|
805
|
+
const newValRaw = (await ask(`${field.label} [${current}]:`)).trim();
|
|
806
|
+
if (!existingRl) rl.close();
|
|
807
|
+
|
|
808
|
+
if (!newValRaw) {
|
|
809
|
+
console.log(` ${faint('no changes made')}`);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const newVal = field.coerce ? field.coerce(newValRaw) : newValRaw;
|
|
814
|
+
cfg[field.key] = newVal;
|
|
815
|
+
saveConfig(cfg);
|
|
816
|
+
console.log(` ${ok('✓')} ${bold(field.label)} updated to ${accent(String(newVal))}`);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// ── Add ───────────────────────────────────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
async function cmdAdd(existingRl) {
|
|
822
|
+
const rl = existingRl || readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
823
|
+
const ask = q => new Promise(resolve => rl.question(` ${accent('›')} ${muted(q)} `, resolve));
|
|
824
|
+
|
|
825
|
+
printSection('Add Entry');
|
|
826
|
+
const title = await ask('Title (optional):');
|
|
827
|
+
const content = await ask('Content:');
|
|
828
|
+
const project = await ask('Project (blank = global):');
|
|
829
|
+
const tagsRaw = await ask('Tags (comma-separated):');
|
|
830
|
+
const type = await ask('Type (note/decision/code/bug/architecture/config/error):');
|
|
831
|
+
|
|
832
|
+
if (!existingRl) rl.close();
|
|
833
|
+
if (!content.trim()) { console.log(` ${bad('✗')} content required`); return; }
|
|
834
|
+
|
|
835
|
+
const entry = saveContext({
|
|
836
|
+
title: title.trim(),
|
|
837
|
+
content: content.trim(),
|
|
838
|
+
project: project.trim() || 'global',
|
|
839
|
+
tags: tagsRaw.split(',').map(t => t.trim()).filter(Boolean),
|
|
840
|
+
type: type.trim() || 'note',
|
|
841
|
+
source: 'cli',
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
console.log(` ${ok('✓')} ${bold(entry.title || '(no title)')} ${faint('id:' + entry.id.slice(0, 8))}`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// ── Delete ────────────────────────────────────────────────────────────────────
|
|
848
|
+
|
|
849
|
+
function cmdDelete(args) {
|
|
850
|
+
if (args[0] === 'project') {
|
|
851
|
+
const nameOrId = args.slice(1).join(' ');
|
|
852
|
+
if (!nameOrId) {
|
|
853
|
+
console.log(` ${bad('✗')} usage: ctx delete project <name|id>`);
|
|
854
|
+
const projects = listProjects();
|
|
855
|
+
if (projects.length) {
|
|
856
|
+
console.log('');
|
|
857
|
+
for (const p of projects) {
|
|
858
|
+
const idStr = p.id ? faint(p.id.slice(0, 8)) : faint('built-in');
|
|
859
|
+
console.log(` ${faint('·')} ${muted(p.name)} ${idStr} ${faint(p.count + ' entries')}`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
const { deletedEntries, deletedDiscussions } = deleteProject(nameOrId);
|
|
865
|
+
if (!deletedEntries && !deletedDiscussions) {
|
|
866
|
+
// Try to give a helpful hint — list available projects
|
|
867
|
+
const projects = listProjects();
|
|
868
|
+
console.log(` ${bad('✗')} no project matching "${nameOrId}"`);
|
|
869
|
+
if (projects.length) {
|
|
870
|
+
console.log(` ${faint('available:')}`);
|
|
871
|
+
for (const p of projects) {
|
|
872
|
+
const idStr = p.id ? faint(' ' + p.id.slice(0, 8)) : faint(' built-in');
|
|
873
|
+
console.log(` ${muted(p.name)}${idStr} ${faint(p.count + ' entries')}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
} else {
|
|
877
|
+
const label = nameOrId.length === 36 ? nameOrId.slice(0, 8) : nameOrId;
|
|
878
|
+
console.log(` ${ok('✓')} deleted project "${label}" ${faint(deletedEntries + ' entries removed')}`);
|
|
879
|
+
}
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const partial = args[0];
|
|
884
|
+
if (!partial) {
|
|
885
|
+
console.log(` ${bad('✗')} usage: ctx delete <id-prefix>`);
|
|
886
|
+
console.log(` ${faint(' ctx delete project <name|id>')}`);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const entries = getContext({ limit: 1000 });
|
|
891
|
+
const matches = entries.filter(e => e.id.startsWith(partial));
|
|
892
|
+
|
|
893
|
+
if (!matches.length) {
|
|
894
|
+
console.log(` ${bad('✗')} no entry with id starting "${partial}"`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (matches.length > 1) {
|
|
898
|
+
console.log(` ${warn('!')} "${partial}" matches ${matches.length} entries — be more specific:`);
|
|
899
|
+
for (const m of matches) console.log(` ${faint(m.id.slice(0, 8))} ${m.title || '(no title)'}`);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const match = matches[0];
|
|
904
|
+
const { deleted } = deleteContext({ id: match.id });
|
|
905
|
+
if (deleted) {
|
|
906
|
+
console.log(` ${ok('✓')} deleted ${bold(match.title || '(no title)')} ${faint('id:' + match.id.slice(0, 8))}`);
|
|
907
|
+
} else {
|
|
908
|
+
console.log(` ${bad('✗')} delete failed`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
// ── Compact header (shown after screen clear in interactive mode) ─────────────
|
|
912
|
+
|
|
913
|
+
function printCompactHeader(cmdLabel = '') {
|
|
914
|
+
const tag = cmdLabel ? ` ${faint('›')} ${muted(cmdLabel)}` : '';
|
|
915
|
+
console.log(`\n ${bold(lblue('context-mcp'))} ${faint('v' + pkg.version)}${tag}\n`);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ── Interactive mode ──────────────────────────────────────────────────────────
|
|
919
|
+
|
|
920
|
+
async function interactive() {
|
|
921
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
922
|
+
clearScreen();
|
|
923
|
+
printBanner();
|
|
924
|
+
|
|
925
|
+
const ask = () => new Promise(resolve => rl.question(`\n ${color(C.dblue, '◆')} ${lblue('context')} ${faint('›')} `, resolve));
|
|
926
|
+
|
|
927
|
+
while (true) {
|
|
928
|
+
const input = (await ask()).trim();
|
|
929
|
+
if (!input) continue;
|
|
930
|
+
const [cmd, ...rest] = input.split(/\s+/);
|
|
931
|
+
|
|
932
|
+
const runCmd = async () => {
|
|
933
|
+
switch (cmd.toLowerCase()) {
|
|
934
|
+
case 'exit': case 'quit': case 'q':
|
|
935
|
+
rl.close(); printBye(); process.exit(0); break;
|
|
936
|
+
case 'list': case 'ls':
|
|
937
|
+
clearScreen(); printCompactHeader('list'); cmdList(rest); break;
|
|
938
|
+
case 'search':
|
|
939
|
+
clearScreen(); printCompactHeader('search'); cmdSearch(rest); break;
|
|
940
|
+
case 'projects':
|
|
941
|
+
clearScreen(); printCompactHeader('projects'); cmdProjects(); break;
|
|
942
|
+
case 'discuss': case 'discussions':
|
|
943
|
+
clearScreen(); printCompactHeader('discussions'); cmdDiscussions(rest); break;
|
|
944
|
+
case 'summary':
|
|
945
|
+
clearScreen(); printCompactHeader('summary'); cmdSummary(rest); break;
|
|
946
|
+
case 'benchmark': case 'bench':
|
|
947
|
+
clearScreen(); printCompactHeader('benchmark'); cmdBenchmark(); break;
|
|
948
|
+
case 'install':
|
|
949
|
+
clearScreen(); printCompactHeader('install'); cmdInstall(rest); break;
|
|
950
|
+
case 'online':
|
|
951
|
+
clearScreen(); printCompactHeader('online'); cmdOnline(rest); break;
|
|
952
|
+
case 'settings': case 'config':
|
|
953
|
+
clearScreen(); printCompactHeader('settings'); await cmdSettings(rl); break;
|
|
954
|
+
case 'add':
|
|
955
|
+
clearScreen(); printCompactHeader('add'); await cmdAdd(rl); break;
|
|
956
|
+
case 'delete': case 'del': case 'rm':
|
|
957
|
+
clearScreen(); printCompactHeader('delete'); cmdDelete(rest); break;
|
|
958
|
+
case 'help': case '?':
|
|
959
|
+
clearScreen(); printUsage(); break;
|
|
960
|
+
case 'clear': case 'cls':
|
|
961
|
+
clearScreen(); printBanner(); break;
|
|
962
|
+
default:
|
|
963
|
+
console.log(`\n ${bad('✗')} unknown command ${faint(cmd)} ${dim('type help')}`);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
await runCmd();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function printBye() {
|
|
972
|
+
console.log(`\n ${ok('✓')} ${bold(lblue('goodbye'))} ${faint('keep building')}\n`);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ── CLI entry point ───────────────────────────────────────────────────────────
|
|
976
|
+
|
|
977
|
+
(async () => {
|
|
978
|
+
const [, , cmd, ...rest] = process.argv;
|
|
979
|
+
|
|
980
|
+
switch ((cmd || '').toLowerCase()) {
|
|
981
|
+
case 'list': case 'ls':
|
|
982
|
+
cmdList(rest); break;
|
|
983
|
+
case 'search':
|
|
984
|
+
cmdSearch(rest); break;
|
|
985
|
+
case 'projects':
|
|
986
|
+
cmdProjects(); break;
|
|
987
|
+
case 'discuss': case 'discussions':
|
|
988
|
+
cmdDiscussions(rest); break;
|
|
989
|
+
case 'summary':
|
|
990
|
+
cmdSummary(rest); break;
|
|
991
|
+
case 'benchmark': case 'bench':
|
|
992
|
+
cmdBenchmark(); break;
|
|
993
|
+
case 'install':
|
|
994
|
+
cmdInstall(rest); break;
|
|
995
|
+
case 'online':
|
|
996
|
+
cmdOnline(rest); break;
|
|
997
|
+
case 'settings': case 'config':
|
|
998
|
+
await cmdSettings(); break;
|
|
999
|
+
case 'add':
|
|
1000
|
+
await cmdAdd(); break;
|
|
1001
|
+
case 'delete': case 'del': case 'rm':
|
|
1002
|
+
cmdDelete(rest); break;
|
|
1003
|
+
case 'help': case '--help': case '-h':
|
|
1004
|
+
printUsage(); break;
|
|
1005
|
+
case '--version': case '-v':
|
|
1006
|
+
console.log(pkg.version); break;
|
|
1007
|
+
default:
|
|
1008
|
+
await interactive();
|
|
1009
|
+
}
|
|
1010
|
+
})();
|