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.
Files changed (81) hide show
  1. package/README.md +29 -7
  2. package/codegraph/__pycache__/affected.cpython-313.pyc +0 -0
  3. package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
  4. package/codegraph/__pycache__/callflow_html.cpython-313.pyc +0 -0
  5. package/codegraph/__pycache__/export.cpython-313.pyc +0 -0
  6. package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
  7. package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
  8. package/codegraph/__pycache__/tree_html.cpython-313.pyc +0 -0
  9. package/codegraph/affected.py +233 -0
  10. package/codegraph/cache.py +51 -2
  11. package/codegraph/callflow_html.py +273 -0
  12. package/codegraph/export.py +544 -0
  13. package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
  14. package/codegraph/extractors/ast_extractor.py +143 -16
  15. package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
  16. package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
  17. package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
  18. package/codegraph/graph/__pycache__/symbol_resolution.cpython-313.pyc +0 -0
  19. package/codegraph/graph/builder.py +10 -0
  20. package/codegraph/graph/clustering.py +247 -10
  21. package/codegraph/graph/query.py +99 -0
  22. package/codegraph/graph/symbol_resolution.py +112 -0
  23. package/codegraph/report.py +53 -0
  24. package/codegraph/server.py +112 -20
  25. package/codegraph/tree_html.py +241 -0
  26. package/package.json +2 -2
  27. package/pyproject.toml +4 -1
  28. package/src/cli.js +329 -227
  29. package/src/db.js +79 -102
  30. package/src/search.js +73 -9
  31. package/src/server.js +7 -1
  32. package/src/templates/antigravity/GEMINI.md +96 -0
  33. package/src/templates/antigravity/hooks/context-mcp-post-tool-use.js +62 -0
  34. package/src/templates/antigravity/workflows/context-resume.md +20 -0
  35. package/src/templates/antigravity/workflows/graph-build.md +23 -0
  36. package/src/templates/antigravity/workflows/save-context.md +29 -0
  37. package/src/templates/claude/CLAUDE.md +140 -0
  38. package/src/templates/claude/commands/graph-build.md +9 -0
  39. package/src/templates/claude/commands/save-context.md +19 -0
  40. package/src/templates/claude/hooks/context-mcp-post-tool-use.js +59 -0
  41. package/src/templates/claude/hooks/context-mcp-pre-tool-use.js +26 -0
  42. package/src/templates/claude/skills/SKILL.md +144 -0
  43. package/src/templates/codex/AGENTS.md +107 -0
  44. package/src/templates/codex/hooks/context-mcp-post-tool-use.js +46 -0
  45. package/src/templates/codex/hooks/context-mcp-pre-tool-use.js +23 -0
  46. package/src/templates/codex/prompts/context-resume.md +15 -0
  47. package/src/templates/codex/prompts/graph-build.md +14 -0
  48. package/src/templates/codex/prompts/save-context.md +24 -0
  49. package/src/templates/cursor/commands/context-resume.md +7 -0
  50. package/src/templates/cursor/commands/graph-build.md +7 -0
  51. package/src/templates/cursor/commands/save-context.md +12 -0
  52. package/src/templates/{cursor-rules.mdc → cursor/cursor-rules.mdc} +13 -3
  53. package/src/templates/cursor/hooks/context-mcp-post-tool-use.js +55 -0
  54. package/src/templates/gemini/GEMINI.md +92 -0
  55. package/src/templates/gemini/commands/context-resume.toml +15 -0
  56. package/src/templates/gemini/commands/graph-build.toml +14 -0
  57. package/src/templates/gemini/commands/save-context.toml +24 -0
  58. package/src/templates/gemini/hooks/context-mcp-after-tool.js +59 -0
  59. package/src/templates/gemini/hooks/context-mcp-before-tool.js +26 -0
  60. package/src/templates/vscode/commands/context-resume.prompt.md +15 -0
  61. package/src/templates/vscode/commands/graph-build.prompt.md +10 -0
  62. package/src/templates/vscode/commands/save-context.prompt.md +16 -0
  63. package/src/templates/vscode/hooks/context-mcp-post-tool-use.js +58 -0
  64. package/src/templates/windsurf/hooks/context-mcp-post-run-command.js +57 -0
  65. package/src/templates/windsurf/windsurf-rules.md +86 -0
  66. package/src/templates/windsurf/workflows/context-resume.md +11 -0
  67. package/src/templates/windsurf/workflows/graph-build.md +11 -0
  68. package/src/templates/windsurf/workflows/save-context.md +18 -0
  69. package/src/tools/codegraph.js +83 -43
  70. package/src/tools/context.js +42 -24
  71. package/src/tools/plan.js +14 -11
  72. package/uv.lock +1101 -4
  73. package/src/migrator.js +0 -124
  74. package/src/templates/AGENTS.md +0 -80
  75. package/src/templates/CLAUDE.md +0 -103
  76. package/src/templates/GEMINI.md +0 -80
  77. package/src/templates/commands/graph-build.md +0 -5
  78. package/src/templates/commands/save-context.md +0 -9
  79. package/src/templates/skills/SKILL.md +0 -108
  80. package/src/templates/windsurf-rules.md +0 -35
  81. /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)) { _projectsIndex = []; return _projectsIndex; }
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
- export function saveContext({ project, content, tags = [], source = 'user', title = '',
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, 60),
310
- content: truncate(content, MAX_CONTENT_LENGTH),
311
- type,
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: normalizeTags(tags),
328
+ tags: normalizedTags,
314
329
  source: normalizeSource(source),
315
- files: Array.isArray(files) ? 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 === 'graph') data.graph.entries.push(entry);
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 (title !== undefined) entry.title = truncate(title, 60);
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
- // Re-add with updated tree
357
- const tempEntry = { ...entry };
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.graph.entries.length + data.summary.length;
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, 80) : (prev?.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, 80);
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
- return countContext(project) > COMPACTION_THRESHOLD;
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
- .sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
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
- return searchContext({ query, project, limit, compact });
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: '1.0.0' },
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.