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.
Files changed (58) hide show
  1. package/README.md +464 -0
  2. package/codegraph/__init__.py +0 -0
  3. package/codegraph/__main__.py +24 -0
  4. package/codegraph/__pycache__/__init__.cpython-313.pyc +0 -0
  5. package/codegraph/__pycache__/__main__.cpython-313.pyc +0 -0
  6. package/codegraph/__pycache__/cache.cpython-313.pyc +0 -0
  7. package/codegraph/__pycache__/config.cpython-313.pyc +0 -0
  8. package/codegraph/__pycache__/report.cpython-313.pyc +0 -0
  9. package/codegraph/__pycache__/scanner.cpython-313.pyc +0 -0
  10. package/codegraph/__pycache__/server.cpython-313.pyc +0 -0
  11. package/codegraph/cache.py +137 -0
  12. package/codegraph/config.py +31 -0
  13. package/codegraph/extractors/__init__.py +0 -0
  14. package/codegraph/extractors/__pycache__/__init__.cpython-313.pyc +0 -0
  15. package/codegraph/extractors/__pycache__/ast_extractor.cpython-313.pyc +0 -0
  16. package/codegraph/extractors/__pycache__/audio_extractor.cpython-313.pyc +0 -0
  17. package/codegraph/extractors/__pycache__/doc_extractor.cpython-313.pyc +0 -0
  18. package/codegraph/extractors/__pycache__/image_extractor.cpython-313.pyc +0 -0
  19. package/codegraph/extractors/ast_extractor.py +222 -0
  20. package/codegraph/extractors/audio_extractor.py +8 -0
  21. package/codegraph/extractors/doc_extractor.py +34 -0
  22. package/codegraph/extractors/image_extractor.py +26 -0
  23. package/codegraph/graph/__init__.py +0 -0
  24. package/codegraph/graph/__pycache__/__init__.cpython-313.pyc +0 -0
  25. package/codegraph/graph/__pycache__/builder.cpython-313.pyc +0 -0
  26. package/codegraph/graph/__pycache__/clustering.cpython-313.pyc +0 -0
  27. package/codegraph/graph/__pycache__/query.cpython-313.pyc +0 -0
  28. package/codegraph/graph/builder.py +145 -0
  29. package/codegraph/graph/clustering.py +40 -0
  30. package/codegraph/graph/query.py +283 -0
  31. package/codegraph/report.py +115 -0
  32. package/codegraph/scanner.py +92 -0
  33. package/codegraph/server.py +514 -0
  34. package/package.json +62 -0
  35. package/src/cli.js +1010 -0
  36. package/src/config.js +89 -0
  37. package/src/db.js +786 -0
  38. package/src/guard.js +20 -0
  39. package/src/hooks/autoContext.js +17 -0
  40. package/src/hooks/autoLink.js +7 -0
  41. package/src/http.js +765 -0
  42. package/src/index.js +47 -0
  43. package/src/search.js +50 -0
  44. package/src/server.js +80 -0
  45. package/src/summarizer.js +124 -0
  46. package/src/templates/AGENTS.md +76 -0
  47. package/src/templates/CLAUDE.md +94 -0
  48. package/src/templates/GEMINI.md +76 -0
  49. package/src/templates/cursor-rules.mdc +41 -0
  50. package/src/templates/windsurf-rules.md +35 -0
  51. package/src/tools/codegraph.js +215 -0
  52. package/src/tools/context.js +188 -0
  53. package/src/tools/discussion.js +123 -0
  54. package/src/tools/errorCheck.js +65 -0
  55. package/src/tools/fileTools.js +185 -0
  56. package/src/tools/gitTools.js +259 -0
  57. package/src/tools/search.js +55 -0
  58. package/src/vector.js +153 -0
@@ -0,0 +1,215 @@
1
+ /**
2
+ * src/tools/codegraph.js — CodeGraph tools bridged to Python subprocess.
3
+ * Spawns `uv run python -m codegraph` with JSON on stdin, reads JSON from stdout.
4
+ */
5
+
6
+ import { spawnSync } from 'node:child_process';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const REPO_ROOT = join(__dirname, '..', '..');
12
+
13
+ function callPython(tool, args) {
14
+ const result = spawnSync('uv', ['run', 'python', '-m', 'codegraph'], {
15
+ input: JSON.stringify({ tool, args }),
16
+ encoding: 'utf8',
17
+ cwd: REPO_ROOT,
18
+ timeout: 120_000,
19
+ });
20
+ if (result.error) throw new Error(`codegraph subprocess failed: ${result.error.message}`);
21
+ if (result.status !== 0) throw new Error(result.stderr?.trim() || 'codegraph error');
22
+ const out = result.stdout.trim();
23
+ if (!out) throw new Error('codegraph returned no output');
24
+ return JSON.parse(out);
25
+ }
26
+
27
+ export const definitions = [
28
+ {
29
+ name: 'codegraph_build',
30
+ description:
31
+ 'Scan a project directory and build the knowledge graph from code files. ' +
32
+ 'Uses AST extraction for code files. For docs and PDFs, call codegraph_extract ' +
33
+ 'afterward — the AI reads and extracts concepts, then calls codegraph_add_nodes.',
34
+ inputSchema: {
35
+ type: 'object',
36
+ properties: {
37
+ path: { type: 'string', description: 'Absolute path to project root' },
38
+ cluster: { type: 'boolean', description: 'Run community detection after build (default true)' },
39
+ },
40
+ required: ['path'],
41
+ },
42
+ },
43
+ {
44
+ name: 'codegraph_extract',
45
+ description:
46
+ 'Return raw content of changed code and doc/PDF files so the AI can write descriptions. ' +
47
+ 'Code files: lists existing AST nodes — AI writes a description for each. ' +
48
+ 'Doc files: AI extracts new concept nodes. ' +
49
+ 'Call after codegraph_build, then call codegraph_add_nodes with results. ' +
50
+ 'Pass force:true to re-enrich all files (not just changed ones).',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: {
54
+ path: { type: 'string', description: 'Project root (same as codegraph_build)' },
55
+ limit: { type: 'integer', description: 'Max files to return per call (default 10)' },
56
+ force: { type: 'boolean', description: 'Return all files, not just changed (for re-enrichment)' },
57
+ },
58
+ required: ['path'],
59
+ },
60
+ },
61
+ {
62
+ name: 'codegraph_add_nodes',
63
+ description:
64
+ 'Add concept nodes extracted by the AI into the graph. ' +
65
+ 'Call after reading codegraph_extract output. ' +
66
+ 'Each node: name, type, file, and optionally description and relations.',
67
+ inputSchema: {
68
+ type: 'object',
69
+ properties: {
70
+ path: { type: 'string', description: 'Project root' },
71
+ nodes: {
72
+ type: 'array',
73
+ description: 'Concept nodes to add',
74
+ items: {
75
+ type: 'object',
76
+ properties: {
77
+ name: { type: 'string' },
78
+ type: { type: 'string', description: 'class|function|concept|service|decision|requirement' },
79
+ file: { type: 'string', description: 'Relative file path this concept came from' },
80
+ description: { type: 'string' },
81
+ relations: {
82
+ type: 'array',
83
+ items: {
84
+ type: 'object',
85
+ properties: {
86
+ name: { type: 'string' },
87
+ relation: { type: 'string', description: 'depends-on|uses|implements|defines|documents' },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ required: ['name', 'type', 'file'],
93
+ },
94
+ },
95
+ },
96
+ required: ['path', 'nodes'],
97
+ },
98
+ },
99
+ {
100
+ name: 'codegraph_query',
101
+ description:
102
+ 'Ask a structural/dependency question about the codebase. ' +
103
+ 'Pure graph traversal — returns NODE/EDGE structured text truncated to token_budget. ' +
104
+ 'Good for: "what does module X depend on?", "what calls function Y?", "what is the path from A to B?". ' +
105
+ 'NOT for: bug investigation, logic errors, or understanding what code actually does — read the file directly for those.',
106
+ inputSchema: {
107
+ type: 'object',
108
+ properties: {
109
+ path: { type: 'string', description: 'Project root' },
110
+ question: { type: 'string', description: 'Natural language question' },
111
+ token_budget: { type: 'integer', description: 'Max tokens in response (default 2000)' },
112
+ },
113
+ required: ['path', 'question'],
114
+ },
115
+ },
116
+ {
117
+ name: 'codegraph_explain',
118
+ description:
119
+ 'Look up a node by name — returns description, type, file, and direct neighbors (depends_on + used_by). ' +
120
+ 'Use to understand what a specific function/class/module does and how it connects. ' +
121
+ 'Descriptions are AI-written via codegraph_add_nodes.',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ path: { type: 'string', description: 'Project root' },
126
+ node: { type: 'string', description: 'Node name or partial name' },
127
+ },
128
+ required: ['path', 'node'],
129
+ },
130
+ },
131
+ {
132
+ name: 'codegraph_report',
133
+ description: 'Return CODEGRAPH_REPORT.md — god nodes, clusters, surprising connections, suggested questions.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: { path: { type: 'string' } },
137
+ required: ['path'],
138
+ },
139
+ },
140
+ {
141
+ name: 'codegraph_nodes',
142
+ description: 'List all nodes of a given type in the graph.',
143
+ inputSchema: {
144
+ type: 'object',
145
+ properties: {
146
+ path: { type: 'string' },
147
+ type: { type: 'string', enum: ['class', 'function', 'module', 'concept', 'service', 'file', 'struct', 'table'] },
148
+ limit: { type: 'integer', description: 'Max results (default 50)' },
149
+ },
150
+ required: ['path', 'type'],
151
+ },
152
+ },
153
+ {
154
+ name: 'codegraph_path',
155
+ description: 'Find the shortest relationship path between two concepts in the graph.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ path: { type: 'string' },
160
+ from: { type: 'string' },
161
+ to: { type: 'string' },
162
+ },
163
+ required: ['path', 'from', 'to'],
164
+ },
165
+ },
166
+ ];
167
+
168
+ export const TOOL_NAMES = new Set(definitions.map(d => d.name));
169
+
170
+ export function handle(name, args, state) {
171
+ const result = callPython(name, args);
172
+
173
+ // Persist graph metadata + save/update a context entry as a visible build record
174
+ if (name === 'codegraph_build' && result.success) {
175
+ import('../db.js').then(({ saveGraph, saveContext, updateContext, getContext }) => {
176
+ saveGraph({
177
+ path: args.path,
178
+ nodes: result.nodes,
179
+ edges: result.edges,
180
+ communities: result.communities,
181
+ cached: result.cached,
182
+ changed: result.changed,
183
+ time_ms: result.time_ms,
184
+ summary: result.summary || '',
185
+ });
186
+
187
+ const project = state?.sessionProject || 'global';
188
+ const title = `CodeGraph — ${args.path}`;
189
+ const content = [
190
+ `nodes: ${result.nodes} | edges: ${result.edges} | communities: ${result.communities}`,
191
+ `cached: ${result.cached} | changed: ${result.changed} | time: ${result.time_ms}ms`,
192
+ result.summary || '',
193
+ ].filter(Boolean).join('\n');
194
+
195
+ const existing = getContext({ project, tags: ['codegraph'], limit: 100 })
196
+ .find(e => e.title === title);
197
+
198
+ if (existing) {
199
+ updateContext({ id: existing.id, content, status: 'active' });
200
+ } else {
201
+ saveContext({
202
+ project,
203
+ sessionId: state?.sessionId || null,
204
+ title,
205
+ content,
206
+ type: 'architecture',
207
+ source: 'auto',
208
+ tags: ['codegraph', 'graph-build'],
209
+ });
210
+ }
211
+ }).catch(() => {});
212
+ }
213
+
214
+ return result;
215
+ }
@@ -0,0 +1,188 @@
1
+ import {
2
+ saveContext, updateContext, getContext, deleteContext,
3
+ listProjects, findDuplicate, archiveExpired, linkContextToDiscussion,
4
+ listDiscussions, listGraphs, countContext, shouldCompact, compactProject,
5
+ ensureProject, getProjectRoot,
6
+ } from '../db.js';
7
+ import { summarizeEntries } from '../summarizer.js';
8
+ import { fireAutoLink } from '../hooks/autoLink.js';
9
+
10
+ function autoDigest(entries, project) {
11
+ if (entries.length <= 10) return null;
12
+ return summarizeEntries(entries, { project: project || 'global', topN: 5 });
13
+ }
14
+
15
+ export const definition = {
16
+ name: 'context',
17
+ description:
18
+ `Factual memory — record what happened, what was decided, what broke, what was built.\n` +
19
+ `• "resume" — START HERE every conversation. Loads recent context, active discussions, and graph status for a project.\n` +
20
+ `• "save" — Store a note, decision, bug, or code snippet. Auto-deduplicates.\n` +
21
+ `• "get" — Load recent entries (compact previews). Auto-digests when large.\n` +
22
+ `• "update" — Edit an existing entry by id (any field).\n` +
23
+ `• "delete" — Remove an entry by id.\n` +
24
+ `• "list_projects"— Show all projects and entry counts.`,
25
+ inputSchema: {
26
+ type: 'object',
27
+ properties: {
28
+ action: { type: 'string', enum: ['resume', 'save', 'get', 'update', 'delete', 'list_projects'] },
29
+ content: { type: 'string' },
30
+ title: { type: 'string' },
31
+ project: { type: 'string' },
32
+ rootPath: { type: 'string', description: 'Absolute path to the project root directory. Stored on first call and used to sandbox file/git tool access.' },
33
+ type: { type: 'string', enum: ['decision', 'note', 'code', 'bug', 'architecture', 'config', 'summary', 'error'] },
34
+ status: { type: 'string', enum: ['active', 'archived'] },
35
+ tags: { type: 'array', items: { type: 'string' } },
36
+ source: { type: 'string', enum: ['user', 'ai-summary', 'file', 'web', 'cli', 'auto'] },
37
+ files: { type: 'array', items: { type: 'object' } },
38
+ codeRefs: { type: 'array', items: { type: 'object' } },
39
+ relations: { type: 'array', items: { type: 'object' } },
40
+ expiresAt: { type: 'string' },
41
+ limit: { type: 'number' },
42
+ includeArchived: { type: 'boolean' },
43
+ id: { type: 'string' },
44
+ },
45
+ required: ['action'],
46
+ },
47
+ outputSchema: {
48
+ type: 'object',
49
+ properties: {
50
+ success: { type: 'boolean' },
51
+ id: { type: 'string' },
52
+ message: { type: 'string' },
53
+ entries: { type: 'array' },
54
+ count: { type: 'number' },
55
+ digest: { type: 'string' },
56
+ projects: { type: 'array' },
57
+ storePath:{ type: 'string' },
58
+ },
59
+ },
60
+ };
61
+
62
+ export async function handle(args, state) {
63
+ const { getStorePath } = await import('../db.js');
64
+
65
+ switch (args.action) {
66
+
67
+ case 'resume': {
68
+ const proj = args.project || null;
69
+ archiveExpired(proj);
70
+
71
+ // Set project on state so autoLink works for subsequent saves
72
+ if (proj) state.sessionProject = proj;
73
+
74
+ // Store rootPath with project (first time only) and load it onto session state
75
+ if (proj) ensureProject(proj, args.rootPath || undefined);
76
+ const storedRoot = proj ? getProjectRoot(proj) : null;
77
+ state.projectRootPath = args.rootPath || storedRoot || null;
78
+
79
+ const entries = getContext({ project: proj, limit: 15, compact: true })
80
+ .filter(e => e.status !== 'archived');
81
+ const discussions = listDiscussions({ project: proj, status: 'active' });
82
+ const allGraphs = listGraphs();
83
+ const graph = proj
84
+ ? allGraphs.find(g => g.path?.toLowerCase().includes(proj.toLowerCase())) || allGraphs[0] || null
85
+ : allGraphs[0] || null;
86
+ const totalEntries = countContext(proj);
87
+
88
+ // Auto-restore single active discussion
89
+ if (discussions.length === 1) state.discussionId = discussions[0].id;
90
+
91
+ const digest = totalEntries > 10
92
+ ? autoDigest(getContext({ project: proj, limit: 30 }), proj)
93
+ : null;
94
+
95
+ const graphStatus = graph
96
+ ? { built: true, path: graph.path, nodes: graph.nodes, edges: graph.edges, communities: graph.communities, builtAt: graph.builtAt }
97
+ : { built: false };
98
+
99
+ return {
100
+ recentEntries: entries,
101
+ activeDiscussions: discussions,
102
+ restoredDiscussion: discussions.length === 1 ? { id: discussions[0].id, name: discussions[0].name } : null,
103
+ codegraph: graphStatus,
104
+ digest: digest || undefined,
105
+ stats: { totalEntries, projects: listProjects().length },
106
+ message: `Loaded ${totalEntries} entries for project "${proj || 'global'}".${discussions.length === 1 ? ` Auto-linked to discussion "${discussions[0].name}".` : ''}`,
107
+ rootPath: state.projectRootPath || undefined,
108
+ hint: graphStatus.built
109
+ ? `Graph ready (${graphStatus.nodes} nodes). Use codegraph_query for structural questions.`
110
+ : 'No graph built yet. Call codegraph_build on the project root to enable graph queries.',
111
+ };
112
+ }
113
+
114
+ case 'save': {
115
+ if (!args.content) throw new Error('content is required for save');
116
+ if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
117
+ const dupe = findDuplicate(args.content, args.project);
118
+ if (dupe) {
119
+ const updated = updateContext({
120
+ id: dupe.id, content: args.content,
121
+ title: args.title || dupe.title, tags: args.tags || dupe.tags,
122
+ type: args.type || dupe.type, status: args.status || dupe.status,
123
+ expiresAt: args.expiresAt !== undefined ? args.expiresAt : dupe.expiresAt,
124
+ files: args.files || dupe.files, codeRefs: args.codeRefs || dupe.codeRefs,
125
+ relations: args.relations || dupe.relations,
126
+ });
127
+ fireAutoLink(updated.id, state);
128
+ return { success: true, id: updated.id, deduplicated: true,
129
+ message: `Updated existing entry "${updated.title || updated.id}" (auto-dedup).` };
130
+ }
131
+ const entry = saveContext({ ...args });
132
+ fireAutoLink(entry.id, state);
133
+
134
+ // Auto-compact when too many entries accumulate
135
+ let compaction = null;
136
+ if (shouldCompact(entry.project)) {
137
+ const old = getContext({ project: entry.project, limit: 500 });
138
+ const { summarizeEntries: summarize } = await import('../summarizer.js');
139
+ const cs = summarize(old.slice(old.length - 30), { project: entry.project || 'global', sessionLabel: 'auto-compaction', topN: 5 });
140
+ compaction = compactProject(entry.project, cs);
141
+ }
142
+
143
+ return { success: true, id: entry.id, deduplicated: false,
144
+ compaction: compaction ? { removedCount: compaction.removedCount, message: `Auto-compacted ${compaction.removedCount} old entries into summary.` } : null,
145
+ message: `Saved context "${entry.title || entry.id}" under project "${entry.project}".` };
146
+ }
147
+
148
+ case 'get': {
149
+ if (!args.project && state.sessionProject) args = { ...args, project: state.sessionProject };
150
+ archiveExpired(args.project);
151
+ const includeArchived = args.includeArchived === true;
152
+ let entries = getContext({ project: args.project, tags: args.tags, limit: args.limit, compact: true });
153
+ if (!includeArchived) entries = entries.filter(e => e.status !== 'archived');
154
+ const fullEntries = entries.length > 10
155
+ ? getContext({ project: args.project, tags: args.tags, limit: args.limit })
156
+ .filter(e => includeArchived || e.status !== 'archived')
157
+ : null;
158
+ const digest = fullEntries ? autoDigest(fullEntries, args.project) : null;
159
+ return {
160
+ entries, count: entries.length, digest: digest || undefined,
161
+ message: entries.length
162
+ ? `Found ${entries.length} entries.${digest ? ' Auto-digest included.' : ''} Use search for full content.`
163
+ : 'No context found.',
164
+ };
165
+ }
166
+
167
+ case 'update': {
168
+ if (!args.id) throw new Error('id is required for update');
169
+ const updated = updateContext({ ...args });
170
+ if (!updated) throw new Error(`No entry found with id "${args.id}"`);
171
+ fireAutoLink(updated.id, state);
172
+ return { success: true, id: updated.id, version: updated.version,
173
+ message: `Updated entry "${updated.title || updated.id}" (v${updated.version}).` };
174
+ }
175
+
176
+ case 'delete': {
177
+ if (!args.id) throw new Error('id is required for delete');
178
+ return deleteContext(args);
179
+ }
180
+
181
+ case 'list_projects': {
182
+ return { projects: listProjects(), storePath: getStorePath() };
183
+ }
184
+
185
+ default:
186
+ throw new Error(`Unknown context action: ${args.action}`);
187
+ }
188
+ }
@@ -0,0 +1,123 @@
1
+ import {
2
+ saveDiscussion, getDiscussion, listDiscussions, deleteDiscussion,
3
+ updateDiscussion, updateDiscussionStep, clearDiscussionLink,
4
+ } from '../db.js';
5
+
6
+ export const definition = {
7
+ name: 'discussion',
8
+ description:
9
+ `Forward-looking thinking space — plans, research, ideas, design.\n` +
10
+ `• "save" — Create or update a discussion.\n` +
11
+ `• "update" — Patch specific fields without touching the rest.\n` +
12
+ `• "get" — Retrieve by name or id (full content + steps).\n` +
13
+ `• "list" — List discussions (filterable). Header + stepsSummary only.\n` +
14
+ `• "delete" — Remove by name or id.\n` +
15
+ `• "update_step" — Mark a step done/in-progress. Auto-closes when all done.`,
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ action: { type: 'string', enum: ['save', 'get', 'list', 'delete', 'update', 'update_step'] },
20
+ name: { type: 'string' },
21
+ id: { type: 'string' },
22
+ project: { type: 'string' },
23
+ title: { type: 'string' },
24
+ description: { type: 'string' },
25
+ content: { type: 'string' },
26
+ type: { type: 'string', enum: ['plan', 'research', 'idea', 'design', 'implementation', 'review', 'thread'] },
27
+ status: { type: 'string', enum: ['active', 'done'] },
28
+ tags: { type: 'array', items: { type: 'string' } },
29
+ parentId: { type: 'string' },
30
+ linkedContextIds: { type: 'array', items: { type: 'string' } },
31
+ steps: { type: 'array', items: { type: 'object' } },
32
+ stepId: { type: 'string' },
33
+ stepStatus: { type: 'string', enum: ['pending', 'in-progress', 'done', 'skipped'] },
34
+ linkedContextId: { type: 'string' },
35
+ filterStatus: { type: 'string' },
36
+ filterType: { type: 'string' },
37
+ },
38
+ required: ['action'],
39
+ },
40
+ outputSchema: {
41
+ type: 'object',
42
+ properties: {
43
+ success: { type: 'boolean' },
44
+ id: { type: 'string' },
45
+ name: { type: 'string' },
46
+ discussion: { type: 'object' },
47
+ discussions: { type: 'array' },
48
+ step: { type: 'object' },
49
+ message: { type: 'string' },
50
+ },
51
+ },
52
+ };
53
+
54
+ export async function handle(args, state) {
55
+ switch (args.action) {
56
+ case 'save': {
57
+ if (!args.name) throw new Error('name is required for save');
58
+ const disc = saveDiscussion({ ...args, sessionId: args.sessionId || state.sessionId });
59
+ if (disc.status === 'active') state.discussionId = disc.id;
60
+ else if (state.discussionId === disc.id) state.discussionId = null;
61
+ return { success: true, id: disc.id, name: disc.name,
62
+ message: `Discussion "${disc.name}" saved (${disc.type}, ${disc.status}).` };
63
+ }
64
+
65
+ case 'get': {
66
+ if (!args.name && !args.id) throw new Error('name or id is required for get');
67
+ const disc = getDiscussion({ name: args.name, id: args.id, project: args.project });
68
+ return disc
69
+ ? { discussion: disc }
70
+ : { discussion: null, message: `No discussion found for "${args.name || args.id}".` };
71
+ }
72
+
73
+ case 'update': {
74
+ if (!args.name && !args.id) throw new Error('name or id is required for update');
75
+ const updated = updateDiscussion({ ...args });
76
+ if (!updated) throw new Error(`No discussion found for "${args.name || args.id}".`);
77
+ if (updated.status !== 'active' && state.discussionId === updated.id) state.discussionId = null;
78
+ if (updated.status === 'active') state.discussionId = updated.id;
79
+ return { success: true, id: updated.id, name: updated.name, status: updated.status,
80
+ message: `Discussion "${updated.name}" updated (${updated.status}).` };
81
+ }
82
+
83
+ case 'list': {
84
+ return { discussions: listDiscussions({ project: args.project, status: args.filterStatus, type: args.filterType }) };
85
+ }
86
+
87
+ case 'delete': {
88
+ if (!args.name && !args.id) throw new Error('name or id is required for delete');
89
+ const toDelete = getDiscussion({ name: args.name, id: args.id });
90
+ if (toDelete) {
91
+ for (const ctxId of (toDelete.linkedContextIds || [])) clearDiscussionLink(ctxId);
92
+ }
93
+ const del = deleteDiscussion({ name: args.name, id: args.id });
94
+ if (state.discussionId) {
95
+ const matchById = args.id && args.id === state.discussionId;
96
+ const matchByName = args.name && del.deleted > 0;
97
+ if (matchById || matchByName) state.discussionId = null;
98
+ }
99
+ return { ...del, message: `Deleted ${del.deleted} discussion(s).` };
100
+ }
101
+
102
+ case 'update_step': {
103
+ if (!args.name && !args.id) throw new Error('name or id is required for update_step');
104
+ if (!args.stepId) throw new Error('stepId is required for update_step');
105
+ const updated = updateDiscussionStep({
106
+ discussionName: args.name,
107
+ discussionId: args.id,
108
+ stepId: args.stepId,
109
+ status: args.stepStatus,
110
+ linkedContextId: args.linkedContextId,
111
+ });
112
+ if (!updated) throw new Error('Discussion or step not found.');
113
+ if (updated.discussion.status === 'done' && state.discussionId === updated.discussion.id) {
114
+ state.discussionId = null;
115
+ }
116
+ return { success: true, step: updated.step, discussionStatus: updated.discussion.status,
117
+ message: `Step updated. Discussion "${updated.discussion.name}" is now "${updated.discussion.status}".` };
118
+ }
119
+
120
+ default:
121
+ throw new Error(`Unknown discussion action: ${args.action}`);
122
+ }
123
+ }
@@ -0,0 +1,65 @@
1
+ import { saveContext, getContext } from '../db.js';
2
+ import { search as unifiedSearch } from '../search.js';
3
+ import { fireAutoLink } from '../hooks/autoLink.js';
4
+
5
+ export const definition = {
6
+ name: 'error_check',
7
+ description:
8
+ `Diagnose and track terminal errors.\n` +
9
+ `• action "check" — Search memory for past occurrences of this error.\n` +
10
+ `• action "save" — Record a new error, the command that caused it, and the solution.`,
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ action: { type: 'string', enum: ['check', 'save'] },
15
+ errorMessage: { type: 'string' },
16
+ command: { type: 'string' },
17
+ solution: { type: 'string' },
18
+ project: { type: 'string' },
19
+ },
20
+ required: ['action', 'errorMessage'],
21
+ },
22
+ outputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ success: { type: 'boolean' },
26
+ found: { type: 'boolean' },
27
+ matches: { type: 'array' },
28
+ message: { type: 'string' },
29
+ },
30
+ },
31
+ };
32
+
33
+ export async function handle(args, state) {
34
+ const { action, errorMessage, command, solution, project } = args;
35
+
36
+ if (action === 'check') {
37
+ const results = unifiedSearch({ mode: 'semantic', query: errorMessage, project, limit: 5 })
38
+ .filter(r => r.type === 'bug');
39
+ if (results.length > 0 && results[0].similarity > 0.4) {
40
+ return {
41
+ success: true, found: true,
42
+ matches: results.map(r => ({ title: r.title, solution: r.content, similarity: Math.round(r.similarity * 100) + '%', id: r.id })),
43
+ message: `Found ${results.length} similar past error(s).`,
44
+ };
45
+ }
46
+ return { success: true, found: false, message: 'No similar past errors found in memory.' };
47
+ }
48
+
49
+ if (action === 'save') {
50
+ if (!solution) throw new Error('solution is required for save action');
51
+ const entry = saveContext({
52
+ project,
53
+ sessionId: state.sessionId || null,
54
+ title: `Error: ${errorMessage.split('\n')[0].slice(0, 60)}`,
55
+ content: `Command: ${command || 'unknown'}\n\nError:\n${errorMessage}\n\nSolution:\n${solution}`,
56
+ type: 'bug',
57
+ status: 'done',
58
+ tags: ['error-log', command].filter(Boolean),
59
+ });
60
+ fireAutoLink(entry.id, state);
61
+ return { success: true, message: `Error logged to context (id: ${entry.id.slice(0, 8)}).` };
62
+ }
63
+
64
+ throw new Error(`Unknown error_check action: ${action}`);
65
+ }