context-mcp-server 1.0.8 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -7
- package/codegraph/__pycache__/affected.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/callflow_html.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/export.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
- package/codegraph/__pycache__/tree_html.cpython-313.pyc +0 -0
- package/codegraph/affected.py +233 -0
- package/codegraph/cache.py +51 -2
- package/codegraph/callflow_html.py +273 -0
- package/codegraph/export.py +544 -0
- package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
- package/codegraph/extractors/ast_extractor.py +143 -16
- package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
- package/codegraph/graph/__pycache__/symbol_resolution.cpython-313.pyc +0 -0
- package/codegraph/graph/builder.py +10 -0
- package/codegraph/graph/clustering.py +247 -10
- package/codegraph/graph/query.py +99 -0
- package/codegraph/graph/symbol_resolution.py +112 -0
- package/codegraph/report.py +53 -0
- package/codegraph/server.py +112 -20
- package/codegraph/tree_html.py +241 -0
- package/package.json +2 -2
- package/pyproject.toml +4 -1
- package/src/cli.js +329 -227
- package/src/db.js +79 -102
- package/src/search.js +73 -9
- package/src/server.js +7 -1
- package/src/templates/antigravity/GEMINI.md +96 -0
- package/src/templates/antigravity/hooks/context-mcp-post-tool-use.js +62 -0
- package/src/templates/antigravity/workflows/context-resume.md +20 -0
- package/src/templates/antigravity/workflows/graph-build.md +23 -0
- package/src/templates/antigravity/workflows/save-context.md +29 -0
- package/src/templates/claude/CLAUDE.md +140 -0
- package/src/templates/claude/commands/graph-build.md +9 -0
- package/src/templates/claude/commands/save-context.md +19 -0
- package/src/templates/claude/hooks/context-mcp-post-tool-use.js +59 -0
- package/src/templates/claude/hooks/context-mcp-pre-tool-use.js +26 -0
- package/src/templates/claude/skills/SKILL.md +144 -0
- package/src/templates/codex/AGENTS.md +107 -0
- package/src/templates/codex/hooks/context-mcp-post-tool-use.js +46 -0
- package/src/templates/codex/hooks/context-mcp-pre-tool-use.js +23 -0
- package/src/templates/codex/prompts/context-resume.md +15 -0
- package/src/templates/codex/prompts/graph-build.md +14 -0
- package/src/templates/codex/prompts/save-context.md +24 -0
- package/src/templates/cursor/commands/context-resume.md +7 -0
- package/src/templates/cursor/commands/graph-build.md +7 -0
- package/src/templates/cursor/commands/save-context.md +12 -0
- package/src/templates/{cursor-rules.mdc → cursor/cursor-rules.mdc} +13 -3
- package/src/templates/cursor/hooks/context-mcp-post-tool-use.js +55 -0
- package/src/templates/gemini/GEMINI.md +92 -0
- package/src/templates/gemini/commands/context-resume.toml +15 -0
- package/src/templates/gemini/commands/graph-build.toml +14 -0
- package/src/templates/gemini/commands/save-context.toml +24 -0
- package/src/templates/gemini/hooks/context-mcp-after-tool.js +59 -0
- package/src/templates/gemini/hooks/context-mcp-before-tool.js +26 -0
- package/src/templates/vscode/commands/context-resume.prompt.md +15 -0
- package/src/templates/vscode/commands/graph-build.prompt.md +10 -0
- package/src/templates/vscode/commands/save-context.prompt.md +16 -0
- package/src/templates/vscode/hooks/context-mcp-post-tool-use.js +58 -0
- package/src/templates/windsurf/hooks/context-mcp-post-run-command.js +57 -0
- package/src/templates/windsurf/windsurf-rules.md +86 -0
- package/src/templates/windsurf/workflows/context-resume.md +11 -0
- package/src/templates/windsurf/workflows/graph-build.md +11 -0
- package/src/templates/windsurf/workflows/save-context.md +18 -0
- package/src/tools/codegraph.js +83 -43
- package/src/tools/context.js +42 -24
- package/src/tools/plan.js +14 -11
- package/uv.lock +1101 -4
- package/src/migrator.js +0 -124
- package/src/templates/AGENTS.md +0 -80
- package/src/templates/CLAUDE.md +0 -103
- package/src/templates/GEMINI.md +0 -80
- package/src/templates/commands/graph-build.md +0 -5
- package/src/templates/commands/save-context.md +0 -9
- package/src/templates/skills/SKILL.md +0 -108
- package/src/templates/windsurf-rules.md +0 -35
- /package/src/templates/{commands → claude/commands}/context-resume.md +0 -0
package/src/db.js
CHANGED
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
import {
|
|
16
16
|
readFileSync, writeFileSync, mkdirSync, existsSync,
|
|
17
17
|
openSync, closeSync, unlinkSync, renameSync, chmodSync, rmdirSync,
|
|
18
|
+
readdirSync,
|
|
18
19
|
} from 'node:fs';
|
|
19
20
|
import { homedir, platform } from 'node:os';
|
|
20
21
|
import { join } from 'node:path';
|
|
21
22
|
import { randomUUID } from 'node:crypto';
|
|
22
|
-
import { runMigration } from './migrator.js';
|
|
23
23
|
|
|
24
24
|
const DATA_DIR = process.env.CONTEXT_MCP_DIR || join(homedir(), '.context-mcp');
|
|
25
25
|
const PROJECTS_DIR = join(DATA_DIR, 'projects');
|
|
@@ -74,7 +74,6 @@ let _dirtyProjects = new Set();
|
|
|
74
74
|
let _dirty = false;
|
|
75
75
|
let _writeTimer = null;
|
|
76
76
|
let _generation = 0;
|
|
77
|
-
let _migrated = false;
|
|
78
77
|
|
|
79
78
|
// ── File I/O helpers ─────────────────────────────────────────────────────────
|
|
80
79
|
|
|
@@ -122,7 +121,18 @@ function _readObj(filePath, defaults) {
|
|
|
122
121
|
|
|
123
122
|
function loadProjectsIndex() {
|
|
124
123
|
if (_projectsIndex) return _projectsIndex;
|
|
125
|
-
if (!existsSync(PROJECTS_PATH)) {
|
|
124
|
+
if (!existsSync(PROJECTS_PATH)) {
|
|
125
|
+
// Fall back to scanning the projects dir so CLI works without projects.json
|
|
126
|
+
_projectsIndex = [];
|
|
127
|
+
if (existsSync(PROJECTS_DIR)) {
|
|
128
|
+
try {
|
|
129
|
+
for (const slug of readdirSync(PROJECTS_DIR, { withFileTypes: true })) {
|
|
130
|
+
if (slug.isDirectory()) _projectsIndex.push({ name: slug.name, slug: slug.name });
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
return _projectsIndex;
|
|
135
|
+
}
|
|
126
136
|
try {
|
|
127
137
|
const d = JSON.parse(readFileSync(PROJECTS_PATH, 'utf8'));
|
|
128
138
|
_projectsIndex = Array.isArray(d.projects) ? d.projects : [];
|
|
@@ -130,21 +140,6 @@ function loadProjectsIndex() {
|
|
|
130
140
|
return _projectsIndex;
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
// ── Migration ─────────────────────────────────────────────────────────────────
|
|
134
|
-
|
|
135
|
-
function migrate() {
|
|
136
|
-
if (_migrated) return;
|
|
137
|
-
_migrated = true;
|
|
138
|
-
runMigration({
|
|
139
|
-
dataDir: DATA_DIR,
|
|
140
|
-
projectsDir: PROJECTS_DIR,
|
|
141
|
-
projectsPath: PROJECTS_PATH,
|
|
142
|
-
slugify,
|
|
143
|
-
flushFile: _flushFile,
|
|
144
|
-
projectsIndex: loadProjectsIndex(),
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
143
|
// ── Per-project data loading ─────────────────────────────────────────────────
|
|
149
144
|
|
|
150
145
|
function loadProjectData(name) {
|
|
@@ -166,7 +161,6 @@ function getAllEntries(projectName) {
|
|
|
166
161
|
return [...data.context, ...data.summary];
|
|
167
162
|
}
|
|
168
163
|
|
|
169
|
-
// Find an entry by ID, optionally scoped to a project.
|
|
170
164
|
function findEntryById(id, projectHint) {
|
|
171
165
|
const search = (data) => {
|
|
172
166
|
for (const arr of [data.context, data.summary]) {
|
|
@@ -184,7 +178,6 @@ function findEntryById(id, projectHint) {
|
|
|
184
178
|
const e = search(data);
|
|
185
179
|
if (e) return { entry: e, projectName: name };
|
|
186
180
|
}
|
|
187
|
-
// Load all remaining projects
|
|
188
181
|
const idx = loadProjectsIndex();
|
|
189
182
|
for (const proj of idx) {
|
|
190
183
|
if (_projectData.has(proj.name) || proj.name === projectHint) continue;
|
|
@@ -194,7 +187,6 @@ function findEntryById(id, projectHint) {
|
|
|
194
187
|
return null;
|
|
195
188
|
}
|
|
196
189
|
|
|
197
|
-
// Remove an entry from its array in the project data.
|
|
198
190
|
function removeEntryFromData(data, entry) {
|
|
199
191
|
if (treeFor(entry) === 'summary') {
|
|
200
192
|
data.summary = data.summary.filter(e => e.id !== entry.id);
|
|
@@ -223,7 +215,7 @@ function flushProjectToDisk(name) {
|
|
|
223
215
|
_flushFile(discussFilePath(name), { discussions: data.discussions });
|
|
224
216
|
}
|
|
225
217
|
|
|
226
|
-
function flushToDisk() {
|
|
218
|
+
export function flushToDisk() {
|
|
227
219
|
if (!_dirty) return;
|
|
228
220
|
_writeTimer = null;
|
|
229
221
|
|
|
@@ -244,11 +236,8 @@ process.on('exit', flushToDisk);
|
|
|
244
236
|
process.on('SIGINT', () => { flushToDisk(); process.exit(); });
|
|
245
237
|
process.on('SIGTERM', () => { flushToDisk(); process.exit(); });
|
|
246
238
|
|
|
247
|
-
// ── Initialise: run migration lazily on first access ─────────────────────────
|
|
248
|
-
|
|
249
239
|
function init() {
|
|
250
240
|
loadProjectsIndex();
|
|
251
|
-
migrate();
|
|
252
241
|
}
|
|
253
242
|
|
|
254
243
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -283,6 +272,8 @@ function compactEntry(e) {
|
|
|
283
272
|
updatedAt: e.updatedAt || null,
|
|
284
273
|
preview: truncate(e.content, PREVIEW_LENGTH),
|
|
285
274
|
};
|
|
275
|
+
if (e.why) compact.why = e.why;
|
|
276
|
+
if (e.outcome) compact.outcome = e.outcome;
|
|
286
277
|
if (e.files && e.files.length) compact.files = e.files;
|
|
287
278
|
if (e.codeRefs && e.codeRefs.length) compact.codeRefs = e.codeRefs;
|
|
288
279
|
if (e.expiresAt) compact.expiresAt = e.expiresAt;
|
|
@@ -291,7 +282,26 @@ function compactEntry(e) {
|
|
|
291
282
|
|
|
292
283
|
// ── Context entries ──────────────────────────────────────────────────────────
|
|
293
284
|
|
|
294
|
-
|
|
285
|
+
const VALID_TYPES = new Set(['note', 'compaction']);
|
|
286
|
+
|
|
287
|
+
function computeImportance({ files = [], why = '', outcome = '', tags = [], type } = {}) {
|
|
288
|
+
if (type === 'compaction') return 5;
|
|
289
|
+
let score = 0;
|
|
290
|
+
if (Array.isArray(files) && files.length > 0) score += 2;
|
|
291
|
+
if (why && why.trim()) score += 1;
|
|
292
|
+
if (outcome && outcome.trim()) score += 1;
|
|
293
|
+
if (Array.isArray(tags) && tags.some(t => t === 'plan' || t === 'decision')) score += 1;
|
|
294
|
+
return score;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const _SECRET_PATTERN = /("?(?:api[-_]?key|password|passwd|pwd|token|secret|authorization|auth_token|access_token|refresh_token|bearer|cookie|signature|private[-_]?key|client[-_]?secret)"?\s*[:=]\s*)("[^"]*"|'[^']*'|\S+)/gi;
|
|
298
|
+
|
|
299
|
+
function redactSecrets(text) {
|
|
300
|
+
if (typeof text !== 'string') return text;
|
|
301
|
+
return text.replace(_SECRET_PATTERN, '$1[REDACTED]');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function saveContext({ project, content, why = '', outcome = '', tags = [], source = 'user', title = '',
|
|
295
305
|
type = 'note', status = 'active', files = [], codeRefs = [],
|
|
296
306
|
sessionId = null, parentId = null, expiresAt = null, rootPath = null }) {
|
|
297
307
|
init();
|
|
@@ -299,6 +309,9 @@ export function saveContext({ project, content, tags = [], source = 'user', titl
|
|
|
299
309
|
ensureProject(projectName, rootPath || undefined);
|
|
300
310
|
const data = loadProjectData(projectName);
|
|
301
311
|
const now = new Date().toISOString();
|
|
312
|
+
const validatedType = VALID_TYPES.has(type) ? type : 'note';
|
|
313
|
+
const normalizedFiles = Array.isArray(files) ? files : [];
|
|
314
|
+
const normalizedTags = normalizeTags(tags);
|
|
302
315
|
const entry = {
|
|
303
316
|
id: randomUUID(),
|
|
304
317
|
project: projectName,
|
|
@@ -306,29 +319,31 @@ export function saveContext({ project, content, tags = [], source = 'user', titl
|
|
|
306
319
|
parentId: parentId || sessionId || `project:${projectName}`,
|
|
307
320
|
nodeType: 'entry',
|
|
308
321
|
version: 1,
|
|
309
|
-
title: truncate(title,
|
|
310
|
-
content: truncate(content, MAX_CONTENT_LENGTH),
|
|
311
|
-
|
|
322
|
+
title: truncate(title, 120),
|
|
323
|
+
content: redactSecrets(truncate(content, MAX_CONTENT_LENGTH)),
|
|
324
|
+
why: redactSecrets(truncate(why || '', 300)),
|
|
325
|
+
outcome: redactSecrets(truncate(outcome || '', 300)),
|
|
326
|
+
type: validatedType,
|
|
312
327
|
status,
|
|
313
|
-
tags:
|
|
328
|
+
tags: normalizedTags,
|
|
314
329
|
source: normalizeSource(source),
|
|
315
|
-
files:
|
|
330
|
+
files: normalizedFiles,
|
|
316
331
|
codeRefs: Array.isArray(codeRefs) ? codeRefs : [],
|
|
332
|
+
importance: computeImportance({ files: normalizedFiles, why, outcome, tags: normalizedTags, type: validatedType }),
|
|
317
333
|
discussionId: null,
|
|
318
334
|
createdAt: now,
|
|
319
335
|
updatedAt: null,
|
|
320
336
|
expiresAt: expiresAt || null,
|
|
321
337
|
};
|
|
322
338
|
const tree = treeFor(entry);
|
|
323
|
-
if (tree === '
|
|
324
|
-
else if (tree === 'summary') data.summary.push(entry);
|
|
339
|
+
if (tree === 'summary') data.summary.push(entry);
|
|
325
340
|
else data.context.push(entry);
|
|
326
341
|
_dirtyProjects.add(projectName);
|
|
327
342
|
markDirty();
|
|
328
343
|
return entry;
|
|
329
344
|
}
|
|
330
345
|
|
|
331
|
-
export function updateContext({ id, content, title, tags, type, status, files, codeRefs, sessionId, parentId, expiresAt }) {
|
|
346
|
+
export function updateContext({ id, content, why, outcome, title, tags, type, status, files, codeRefs, sessionId, parentId, expiresAt }) {
|
|
332
347
|
init();
|
|
333
348
|
const found = findEntryById(id);
|
|
334
349
|
if (!found) return null;
|
|
@@ -337,9 +352,11 @@ export function updateContext({ id, content, title, tags, type, status, files, c
|
|
|
337
352
|
|
|
338
353
|
const oldTree = treeFor(entry);
|
|
339
354
|
if (content !== undefined) entry.content = truncate(content, MAX_CONTENT_LENGTH);
|
|
340
|
-
if (
|
|
355
|
+
if (why !== undefined) entry.why = truncate(why || '', 300);
|
|
356
|
+
if (outcome !== undefined) entry.outcome = truncate(outcome || '', 300);
|
|
357
|
+
if (title !== undefined) entry.title = truncate(title, 120);
|
|
341
358
|
if (tags !== undefined) entry.tags = normalizeTags(tags);
|
|
342
|
-
if (type !== undefined) entry.type = type;
|
|
359
|
+
if (type !== undefined) entry.type = VALID_TYPES.has(type) ? type : entry.type;
|
|
343
360
|
if (status !== undefined) entry.status = status;
|
|
344
361
|
if (files !== undefined) entry.files = Array.isArray(files) ? files : [];
|
|
345
362
|
if (codeRefs !== undefined) entry.codeRefs = Array.isArray(codeRefs) ? codeRefs : [];
|
|
@@ -353,11 +370,8 @@ export function updateContext({ id, content, title, tags, type, status, files, c
|
|
|
353
370
|
const newTree = treeFor(entry);
|
|
354
371
|
if (newTree !== oldTree) {
|
|
355
372
|
removeEntryFromData(data, entry);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (newTree === 'graph') data.graph.entries.push(tempEntry);
|
|
359
|
-
else if (newTree === 'summary') data.summary.push(tempEntry);
|
|
360
|
-
else data.context.push(tempEntry);
|
|
373
|
+
if (newTree === 'summary') data.summary.push(entry);
|
|
374
|
+
else data.context.push(entry);
|
|
361
375
|
}
|
|
362
376
|
|
|
363
377
|
_dirtyProjects.add(projectName);
|
|
@@ -478,7 +492,7 @@ export function deleteProject(nameOrId) {
|
|
|
478
492
|
|
|
479
493
|
// Count before removing
|
|
480
494
|
const data = _projectData.get(projectName) || loadProjectData(projectName);
|
|
481
|
-
const ctxCount = data.context.length + data.
|
|
495
|
+
const ctxCount = data.context.length + data.summary.length;
|
|
482
496
|
const discCount = data.discussions.length;
|
|
483
497
|
|
|
484
498
|
// Remove project directory from disk
|
|
@@ -605,7 +619,7 @@ export function saveDiscussion({ name, title, description, content, project, tag
|
|
|
605
619
|
project: project !== undefined ? (project || 'global') : (prev?.project ?? 'global'),
|
|
606
620
|
sessionId: sessionId !== undefined ? (sessionId || null) : (prev?.sessionId ?? null),
|
|
607
621
|
parentId: parentId !== undefined ? (parentId || null) : (prev?.parentId ?? null),
|
|
608
|
-
title: title !== undefined ? truncate(title || name,
|
|
622
|
+
title: title !== undefined ? truncate(title || name, 120) : (prev?.title ?? name),
|
|
609
623
|
description: description !== undefined ? (description || '') : (prev?.description ?? ''),
|
|
610
624
|
content: content !== undefined ? truncate(content || '', MAX_CONTENT_LENGTH) : (prev?.content ?? ''),
|
|
611
625
|
type: type !== undefined ? (VALID_DISCUSSION_TYPES.has(type) ? type : 'plan') : (prev?.type ?? 'plan'),
|
|
@@ -651,7 +665,7 @@ export function updateDiscussion({ id, name, title, description, content, status
|
|
|
651
665
|
if (found) { disc = found; projName = pName; break; }
|
|
652
666
|
}
|
|
653
667
|
if (!disc) return null;
|
|
654
|
-
if (title !== undefined) disc.title = truncate(title || disc.name,
|
|
668
|
+
if (title !== undefined) disc.title = truncate(title || disc.name, 120);
|
|
655
669
|
if (description !== undefined) disc.description = description || '';
|
|
656
670
|
if (content !== undefined) disc.content = truncate(content || '', MAX_CONTENT_LENGTH);
|
|
657
671
|
if (type !== undefined) disc.type = VALID_DISCUSSION_TYPES.has(type) ? type : disc.type;
|
|
@@ -747,28 +761,6 @@ export function linkContextToDiscussion({ discussionId, discussionName, contextI
|
|
|
747
761
|
return { discussionId: disc.id, contextId };
|
|
748
762
|
}
|
|
749
763
|
|
|
750
|
-
export function getContextByDiscussion(discussionId) {
|
|
751
|
-
init();
|
|
752
|
-
const idx = loadProjectsIndex();
|
|
753
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
754
|
-
const results = [];
|
|
755
|
-
for (const name of seen) {
|
|
756
|
-
results.push(...getAllEntries(name).filter(c => c.discussionId === discussionId));
|
|
757
|
-
}
|
|
758
|
-
return results;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
export function clearDiscussionLink(contextId) {
|
|
762
|
-
init();
|
|
763
|
-
const found = findEntryById(contextId);
|
|
764
|
-
if (!found) return null;
|
|
765
|
-
found.entry.discussionId = null;
|
|
766
|
-
found.entry.updatedAt = new Date().toISOString();
|
|
767
|
-
_dirtyProjects.add(found.projectName);
|
|
768
|
-
markDirty();
|
|
769
|
-
return found.entry;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
764
|
export function deleteDiscussion({ name, id }) {
|
|
773
765
|
init();
|
|
774
766
|
const idx = loadProjectsIndex();
|
|
@@ -790,38 +782,6 @@ export function deleteDiscussion({ name, id }) {
|
|
|
790
782
|
return { deleted: 0 };
|
|
791
783
|
}
|
|
792
784
|
|
|
793
|
-
export function updateDiscussionStep({ discussionName, discussionId, stepId, status, linkedContextId }) {
|
|
794
|
-
init();
|
|
795
|
-
let disc = null;
|
|
796
|
-
let projName = null;
|
|
797
|
-
const idx = loadProjectsIndex();
|
|
798
|
-
const seen = new Set([..._projectData.keys(), ...idx.map(p => p.name)]);
|
|
799
|
-
for (const pName of seen) {
|
|
800
|
-
const d = loadProjectData(pName);
|
|
801
|
-
const found = discussionId
|
|
802
|
-
? d.discussions.find(x => x.id === discussionId)
|
|
803
|
-
: d.discussions.find(x => x.name === discussionName);
|
|
804
|
-
if (found) { disc = found; projName = pName; break; }
|
|
805
|
-
}
|
|
806
|
-
if (!disc) return null;
|
|
807
|
-
const step = (disc.steps || []).find(s => s.id === stepId);
|
|
808
|
-
if (!step) return null;
|
|
809
|
-
if (status) step.status = status;
|
|
810
|
-
if (status === 'done') step.completedAt = new Date().toISOString();
|
|
811
|
-
if (linkedContextId) {
|
|
812
|
-
if (!Array.isArray(step.linkedContextIds)) step.linkedContextIds = [];
|
|
813
|
-
if (!step.linkedContextIds.includes(linkedContextId)) step.linkedContextIds.push(linkedContextId);
|
|
814
|
-
if (!Array.isArray(disc.linkedContextIds)) disc.linkedContextIds = [];
|
|
815
|
-
if (!disc.linkedContextIds.includes(linkedContextId)) disc.linkedContextIds.push(linkedContextId);
|
|
816
|
-
}
|
|
817
|
-
const allDone = disc.steps.every(s => s.status === 'done' || s.status === 'skipped');
|
|
818
|
-
if (allDone && disc.status !== 'done') disc.status = 'done';
|
|
819
|
-
disc.updatedAt = new Date().toISOString();
|
|
820
|
-
_dirtyProjects.add(projName);
|
|
821
|
-
markDirty();
|
|
822
|
-
return { discussion: disc, step };
|
|
823
|
-
}
|
|
824
|
-
|
|
825
785
|
// ── Auto-operations ───────────────────────────────────────────────────────────
|
|
826
786
|
|
|
827
787
|
export function archiveExpired(project) {
|
|
@@ -862,21 +822,39 @@ const COMPACTION_THRESHOLD = 20;
|
|
|
862
822
|
const COMPACTION_TARGET = 30;
|
|
863
823
|
|
|
864
824
|
export function shouldCompact(project) {
|
|
865
|
-
|
|
825
|
+
init();
|
|
826
|
+
// Only count non-compaction entries — compaction summaries themselves don't trigger further compaction
|
|
827
|
+
const proj = project || 'global';
|
|
828
|
+
const data = loadProjectData(proj);
|
|
829
|
+
const nonCompaction = [...data.context, ...data.summary].filter(e => e.type !== 'compaction');
|
|
830
|
+
return nonCompaction.length > COMPACTION_THRESHOLD;
|
|
866
831
|
}
|
|
867
832
|
|
|
868
|
-
export function compactProject(project, summaryContent) {
|
|
833
|
+
export function compactProject(project, summaryContent, { skipSummaryEntry = false } = {}) {
|
|
869
834
|
init();
|
|
870
835
|
const proj = project || 'global';
|
|
871
836
|
const data = loadProjectData(proj);
|
|
837
|
+
const now = Date.now();
|
|
872
838
|
const entries = data.context
|
|
873
|
-
.
|
|
839
|
+
.filter(e => e.type !== 'compaction') // never remove existing compaction summaries
|
|
840
|
+
.sort((a, b) => {
|
|
841
|
+
// Higher score = more expendable; oldest + least important drops first
|
|
842
|
+
const ageDaysA = (now - new Date(a.createdAt || 0).getTime()) / 86_400_000;
|
|
843
|
+
const ageDaysB = (now - new Date(b.createdAt || 0).getTime()) / 86_400_000;
|
|
844
|
+
const scoreA = ageDaysA * 0.7 + (5 - (a.importance ?? 0)) * 0.3;
|
|
845
|
+
const scoreB = ageDaysB * 0.7 + (5 - (b.importance ?? 0)) * 0.3;
|
|
846
|
+
return scoreB - scoreA;
|
|
847
|
+
});
|
|
874
848
|
if (entries.length < COMPACTION_TARGET) return null;
|
|
875
849
|
const toRemove = new Set(entries.slice(0, COMPACTION_TARGET).map(e => e.id));
|
|
876
850
|
const removed = entries.filter(e => toRemove.has(e.id));
|
|
877
851
|
for (const entry of removed) removeEntryFromData(data, entry);
|
|
878
852
|
_dirtyProjects.add(proj);
|
|
879
853
|
markDirty();
|
|
854
|
+
// If AI already saved a compaction entry this call, don't create a duplicate
|
|
855
|
+
if (skipSummaryEntry) {
|
|
856
|
+
return { removedCount: removed.length, summaryId: null };
|
|
857
|
+
}
|
|
880
858
|
const summary = saveContext({
|
|
881
859
|
project: proj,
|
|
882
860
|
title: `Compacted ${removed.length} entries — ${new Date().toISOString().slice(0, 10)}`,
|
|
@@ -892,7 +870,6 @@ export function compactProject(project, summaryContent) {
|
|
|
892
870
|
|
|
893
871
|
export function saveGraph({ path, nodes, edges, communities, cached, changed, time_ms, summary }) {
|
|
894
872
|
init();
|
|
895
|
-
// Find project by rootPath matching graph path
|
|
896
873
|
const idx = loadProjectsIndex();
|
|
897
874
|
const proj = idx.find(p => normPath(p.rootPath) === normPath(path));
|
|
898
875
|
const projName = proj ? proj.name : 'global';
|
package/src/search.js
CHANGED
|
@@ -1,13 +1,71 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* search.js — unified search entry point
|
|
3
|
-
*
|
|
4
|
-
* Single function replaces direct calls to searchContext / vectorSearch / findRelated
|
|
5
|
-
* across index.js, cli.js, and error_check. db.js + vector.js stay as low-level impls.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { getContext, searchContext } from './db.js';
|
|
9
2
|
import { vectorSearch, findRelated } from './vector.js';
|
|
10
3
|
|
|
4
|
+
// ── Vocabulary cache (lazy singleton per process) ─────────────────────────────
|
|
5
|
+
|
|
6
|
+
let _vocabCache = null;
|
|
7
|
+
|
|
8
|
+
function buildVocab(entries) {
|
|
9
|
+
const vocab = new Set();
|
|
10
|
+
for (const e of entries) {
|
|
11
|
+
const text = `${e.title || ''} ${e.content || ''}`.toLowerCase();
|
|
12
|
+
for (const w of text.split(/\W+/)) {
|
|
13
|
+
if (w.length > 2) vocab.add(w);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return vocab;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ── Levenshtein fuzzy correction ──────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function levenshtein(a, b) {
|
|
22
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
23
|
+
for (let i = 0; i < a.length; i++) {
|
|
24
|
+
const curr = [i + 1];
|
|
25
|
+
for (let j = 0; j < b.length; j++) {
|
|
26
|
+
curr.push(Math.min(prev[j + 1] + 1, curr[j] + 1, prev[j] + (a[i] === b[j] ? 0 : 1)));
|
|
27
|
+
}
|
|
28
|
+
prev.splice(0, prev.length, ...curr);
|
|
29
|
+
}
|
|
30
|
+
return prev[b.length];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function maxEditDist(len) {
|
|
34
|
+
if (len <= 4) return 1;
|
|
35
|
+
if (len <= 12) return 2;
|
|
36
|
+
return 3;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fuzzyCorrect(word, vocab) {
|
|
40
|
+
const max = maxEditDist(word.length);
|
|
41
|
+
let bestWord = word;
|
|
42
|
+
let bestDist = max + 1;
|
|
43
|
+
for (const candidate of vocab) {
|
|
44
|
+
if (Math.abs(candidate.length - word.length) > max) continue;
|
|
45
|
+
const dist = levenshtein(word, candidate);
|
|
46
|
+
if (dist < bestDist) { bestDist = dist; bestWord = candidate; }
|
|
47
|
+
}
|
|
48
|
+
return bestDist <= max ? bestWord : word;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Smart snippet (window centered on first match position) ───────────────────
|
|
52
|
+
|
|
53
|
+
function snippet(text, terms, windowSize = 200) {
|
|
54
|
+
if (!text) return text;
|
|
55
|
+
const lower = text.toLowerCase();
|
|
56
|
+
let bestPos = -1;
|
|
57
|
+
for (const term of terms) {
|
|
58
|
+
const idx = lower.indexOf(term.toLowerCase());
|
|
59
|
+
if (idx !== -1 && (bestPos === -1 || idx < bestPos)) bestPos = idx;
|
|
60
|
+
}
|
|
61
|
+
if (bestPos === -1) return text.slice(0, windowSize);
|
|
62
|
+
const start = Math.max(0, bestPos - Math.floor(windowSize / 3));
|
|
63
|
+
const end = Math.min(text.length, start + windowSize);
|
|
64
|
+
return (start > 0 ? '…' : '') + text.slice(start, end) + (end < text.length ? '…' : '');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Unified search ────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
11
69
|
/**
|
|
12
70
|
* @param {Object} opts
|
|
13
71
|
* @param {string} opts.query - search query (keyword/semantic)
|
|
@@ -21,7 +79,14 @@ export function search({ query, mode = 'semantic', project, limit = 10, id, comp
|
|
|
21
79
|
switch (mode) {
|
|
22
80
|
case 'keyword': {
|
|
23
81
|
if (!query) throw new Error('query required for keyword search');
|
|
24
|
-
|
|
82
|
+
if (!_vocabCache) {
|
|
83
|
+
_vocabCache = buildVocab(getContext({ project, limit: 500 }));
|
|
84
|
+
}
|
|
85
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
86
|
+
const corrected = terms.map(t => fuzzyCorrect(t, _vocabCache));
|
|
87
|
+
const correctedQuery = corrected.join(' ');
|
|
88
|
+
const results = searchContext({ query: correctedQuery, project, limit, compact: false });
|
|
89
|
+
return results.map(e => ({ ...e, content: snippet(e.content, corrected) }));
|
|
25
90
|
}
|
|
26
91
|
case 'semantic': {
|
|
27
92
|
if (!query) throw new Error('query required for semantic search');
|
|
@@ -33,7 +98,6 @@ export function search({ query, mode = 'semantic', project, limit = 10, id, comp
|
|
|
33
98
|
const all = getContext({ limit: 1000 });
|
|
34
99
|
const target = all.find(e => e.id === id || e.id.startsWith(id));
|
|
35
100
|
if (!target) throw new Error(`No entry found with id starting "${id}"`);
|
|
36
|
-
// explicit relations first, semantic enrichment for remainder
|
|
37
101
|
const explicitIds = new Set([
|
|
38
102
|
...(target.relations || []).map(r => r.id),
|
|
39
103
|
...(target.relatedBy || []).map(r => r.id),
|
package/src/server.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
3
6
|
|
|
4
7
|
import { getConfig } from './config.js';
|
|
5
8
|
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const { version } = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
11
|
+
|
|
6
12
|
import * as contextTool from './tools/context.js';
|
|
7
13
|
import * as searchTool from './tools/search.js';
|
|
8
14
|
import * as planTool from './tools/plan.js';
|
|
@@ -23,7 +29,7 @@ export function createServer({ enableFileTools = false, enableGitTools = getConf
|
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
const server = new Server(
|
|
26
|
-
{ name: 'context-mcp', version
|
|
32
|
+
{ name: 'context-mcp', version },
|
|
27
33
|
{ capabilities: { tools: {} } }
|
|
28
34
|
);
|
|
29
35
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Context-MCP — Antigravity IDE Usage Guide
|
|
2
|
+
|
|
3
|
+
Persistent memory + codebase knowledge graph via `ctx` CLI commands.
|
|
4
|
+
Run `ctx resume` to start every session. Use `ctx search` to recall past work. Use `ctx save` to persist decisions. Use `ctx add` to record discoveries.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. Start of Every Conversation (MANDATORY)
|
|
9
|
+
|
|
10
|
+
Run `ctx resume --project <project>` (shell command) before anything else.
|
|
11
|
+
|
|
12
|
+
Returns recent entries, active plans, graph status.
|
|
13
|
+
|
|
14
|
+
- Graph built → use `ctx graph query` / `ctx graph arch` before reading files
|
|
15
|
+
- Graph not built → run `ctx graph build <path>` first
|
|
16
|
+
- `totalEntries ≥ 20` → save compaction summary FIRST (see Rule 4)
|
|
17
|
+
- Active plans present → read them before starting new work
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Save Triggers (MANDATORY)
|
|
22
|
+
|
|
23
|
+
Run `ctx save` after finishing anything worth keeping:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
ctx save --project <project> --title "..." --why "..." --outcome "..." --type note
|
|
27
|
+
ctx save --project <project> --title "..." --content "..." --type bug
|
|
28
|
+
ctx save --project <project> --title "Session summary — YYYY-MM-DD" --content "..." --type compaction
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
| Trigger | Command |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Task / fix / feature complete | `ctx save` with `--why` + `--outcome` + `--files` |
|
|
34
|
+
| Decision made | `ctx save` with `--why` + `--outcome` |
|
|
35
|
+
| Discovery / constraint | `ctx save` with `--content` |
|
|
36
|
+
| User says "save this" | `ctx save` with `--title` + `--content` |
|
|
37
|
+
| Compact memory | `ctx save --type compaction` |
|
|
38
|
+
|
|
39
|
+
**Do NOT save:** routine reads, search results, explanations of existing code.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. Plans (MANDATORY for multi-file work)
|
|
44
|
+
|
|
45
|
+
**Create a plan when:** editing 2+ files, multi-step implementation, refactor, multi-file bug fix.
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
ctx plan save --project <project> --name "..." --content "..."
|
|
49
|
+
ctx plan done --project <project> --name "..." # deletes the plan
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Check active plans on resume — don't create duplicates.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 4. Auto-Summary at ≥ 20 Entries (MANDATORY)
|
|
57
|
+
|
|
58
|
+
When `totalEntries ≥ 20`, run this BEFORE the user's task:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
ctx save --project <project> --type compaction \
|
|
62
|
+
--title "Session summary — YYYY-MM-DD" \
|
|
63
|
+
--content "<what was built, decided, broke, current state>"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 5. Search Before Asking
|
|
69
|
+
|
|
70
|
+
Run `ctx search "<query>" --project <project>` before asking the user to re-explain past work.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 6. ContextGraph CLI
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
ctx graph build <path> → build AST graph (run once, incremental)
|
|
78
|
+
ctx graph arch <path> → module map: files, exports, imports
|
|
79
|
+
ctx graph query <path> "<question>" → structural question about the codebase
|
|
80
|
+
ctx graph nodes <path> <type> → list all nodes of a type
|
|
81
|
+
ctx graph report <path> → god nodes, clusters, surprising connections
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Use `ctx graph arch` first. Never read files for structure questions.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 7. Rules
|
|
89
|
+
|
|
90
|
+
1. `ctx resume` first — before any tool or response
|
|
91
|
+
2. Always pass `--project`
|
|
92
|
+
3. Save on task complete — `--why` + `--outcome` + `--files`
|
|
93
|
+
4. Compaction at ≥ 20 entries — before starting task
|
|
94
|
+
5. Plan before multi-file work — `ctx plan done` deletes it
|
|
95
|
+
6. `ctx search` before asking about past work
|
|
96
|
+
7. `ctx graph` before reading files
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Antigravity IDE PostToolUse hook for context-mcp.
|
|
5
|
+
*
|
|
6
|
+
* Input (stdin JSON): {
|
|
7
|
+
* toolCall: { name, args },
|
|
8
|
+
* stepIdx, conversationId, workspacePaths,
|
|
9
|
+
* error ← present when the tool invocation failed
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* Saves failed tool invocations to context-mcp so the next session can see
|
|
13
|
+
* what broke. Exits silently when the tool succeeded or no error is present.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
process.stdin.resume();
|
|
19
|
+
process.stdin.setEncoding('utf8');
|
|
20
|
+
|
|
21
|
+
let input = '';
|
|
22
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
23
|
+
process.stdin.on('end', () => {
|
|
24
|
+
let event = {};
|
|
25
|
+
try {
|
|
26
|
+
event = input.trim() ? JSON.parse(input) : {};
|
|
27
|
+
} catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const toolCall = event.toolCall || {};
|
|
32
|
+
const toolName = toolCall.name || '';
|
|
33
|
+
const error = event.error;
|
|
34
|
+
|
|
35
|
+
if (!error || !toolName) return;
|
|
36
|
+
|
|
37
|
+
const args = toolCall.args || {};
|
|
38
|
+
const argsSnippet = Object.keys(args).length
|
|
39
|
+
? JSON.stringify(args).slice(0, 200)
|
|
40
|
+
: '';
|
|
41
|
+
|
|
42
|
+
const cwd = (event.workspacePaths || [])[0] || process.cwd();
|
|
43
|
+
const project = cwd.replace(/\\/g, '/').replace(/\/$/, '').split('/').pop() || 'default';
|
|
44
|
+
|
|
45
|
+
const content = [
|
|
46
|
+
`Tool: ${toolName}`,
|
|
47
|
+
argsSnippet ? `Args: ${argsSnippet}` : null,
|
|
48
|
+
`Error: ${String(error).slice(0, 4000)}`,
|
|
49
|
+
].filter(Boolean).join('\n\n');
|
|
50
|
+
|
|
51
|
+
spawnSync('ctx', [
|
|
52
|
+
'save',
|
|
53
|
+
'--project', project,
|
|
54
|
+
'--type', 'bug',
|
|
55
|
+
'--title', `Failed tool: ${toolName.slice(0, 80)}`,
|
|
56
|
+
'--content', content,
|
|
57
|
+
], {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
shell: true,
|
|
60
|
+
stdio: 'ignore',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# context-resume
|
|
2
|
+
|
|
3
|
+
Resume context for the current project from persistent memory.
|
|
4
|
+
|
|
5
|
+
## Steps
|
|
6
|
+
|
|
7
|
+
1. Call the `context` MCP tool with `action: "resume"` and `project: "<current project name>"`.
|
|
8
|
+
2. Read `recentEntries` — the last saved notes, decisions, and bug records.
|
|
9
|
+
3. Read `activePlans` — any in-progress multi-step plans.
|
|
10
|
+
4. Check `codegraph.built`:
|
|
11
|
+
- `true` → graph tools are available; use `codegraph_arch` before reading files
|
|
12
|
+
- `false` → run `codegraph_build` when structural questions arise
|
|
13
|
+
5. If `stats.totalEntries ≥ 20`, save a compaction summary before starting work:
|
|
14
|
+
```
|
|
15
|
+
context(action:"save", type:"compaction",
|
|
16
|
+
title:"Session summary — YYYY-MM-DD",
|
|
17
|
+
content:"<full summary of session>",
|
|
18
|
+
project:"<project>")
|
|
19
|
+
```
|
|
20
|
+
6. Summarize what was found: recent work, open plans, graph status.
|