sigmap 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.contextignore.example +34 -0
  2. package/CHANGELOG.md +402 -0
  3. package/LICENSE +21 -0
  4. package/README.md +601 -0
  5. package/gen-context.config.json.example +40 -0
  6. package/gen-context.js +4316 -0
  7. package/gen-project-map.js +172 -0
  8. package/package.json +67 -0
  9. package/src/config/defaults.js +61 -0
  10. package/src/config/loader.js +60 -0
  11. package/src/extractors/cpp.js +60 -0
  12. package/src/extractors/csharp.js +48 -0
  13. package/src/extractors/css.js +51 -0
  14. package/src/extractors/dart.js +58 -0
  15. package/src/extractors/dockerfile.js +49 -0
  16. package/src/extractors/go.js +61 -0
  17. package/src/extractors/html.js +39 -0
  18. package/src/extractors/java.js +49 -0
  19. package/src/extractors/javascript.js +82 -0
  20. package/src/extractors/kotlin.js +62 -0
  21. package/src/extractors/php.js +62 -0
  22. package/src/extractors/python.js +69 -0
  23. package/src/extractors/ruby.js +43 -0
  24. package/src/extractors/rust.js +72 -0
  25. package/src/extractors/scala.js +67 -0
  26. package/src/extractors/shell.js +43 -0
  27. package/src/extractors/svelte.js +51 -0
  28. package/src/extractors/swift.js +63 -0
  29. package/src/extractors/typescript.js +109 -0
  30. package/src/extractors/vue.js +66 -0
  31. package/src/extractors/yaml.js +59 -0
  32. package/src/format/cache.js +53 -0
  33. package/src/health/scorer.js +123 -0
  34. package/src/map/class-hierarchy.js +117 -0
  35. package/src/map/import-graph.js +148 -0
  36. package/src/map/route-table.js +127 -0
  37. package/src/mcp/handlers.js +433 -0
  38. package/src/mcp/server.js +128 -0
  39. package/src/mcp/tools.js +125 -0
  40. package/src/routing/classifier.js +102 -0
  41. package/src/routing/hints.js +103 -0
  42. package/src/security/patterns.js +51 -0
  43. package/src/security/scanner.js +36 -0
  44. package/src/tracking/logger.js +115 -0
@@ -0,0 +1,433 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
8
+
9
+ // Section header keywords in PROJECT_MAP.md
10
+ const MAP_SECTIONS = {
11
+ imports: '### Import graph',
12
+ classes: '### Class hierarchy',
13
+ routes: '### Route table',
14
+ };
15
+
16
+ /**
17
+ * read_context({ module? }) → string
18
+ *
19
+ * Returns the full context file, or just the sections whose file paths
20
+ * contain the given module substring.
21
+ */
22
+ function readContext(args, cwd) {
23
+ const contextPath = path.join(cwd, CONTEXT_FILE);
24
+ if (!fs.existsSync(contextPath)) {
25
+ return 'No context file found. Run: node gen-context.js';
26
+ }
27
+
28
+ const content = fs.readFileSync(contextPath, 'utf8');
29
+
30
+ if (!args || !args.module) return content;
31
+
32
+ const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
33
+ const lines = content.split('\n');
34
+ const result = [];
35
+ let capturing = false;
36
+
37
+ for (const line of lines) {
38
+ if (line.startsWith('### ')) {
39
+ const filePath = line.slice(4).trim().replace(/\\/g, '/');
40
+ // Match if file path starts with mod or contains /mod/ or /mod
41
+ capturing =
42
+ filePath === mod ||
43
+ filePath.startsWith(mod + '/') ||
44
+ filePath.includes('/' + mod + '/') ||
45
+ filePath.includes('/' + mod);
46
+ if (capturing) result.push(line);
47
+ continue;
48
+ }
49
+ if (capturing) result.push(line);
50
+ }
51
+
52
+ if (result.length === 0) return `No signatures found for module: ${mod}`;
53
+ return result.join('\n');
54
+ }
55
+
56
+ /**
57
+ * search_signatures({ query }) → string
58
+ *
59
+ * Case-insensitive search through all signature lines.
60
+ * Returns matching lines grouped by file path.
61
+ */
62
+ function searchSignatures(args, cwd) {
63
+ if (!args || !args.query) return 'Missing required argument: query';
64
+
65
+ const contextPath = path.join(cwd, CONTEXT_FILE);
66
+ if (!fs.existsSync(contextPath)) {
67
+ return 'No context file found. Run: node gen-context.js';
68
+ }
69
+
70
+ const content = fs.readFileSync(contextPath, 'utf8');
71
+ const query = args.query.toLowerCase();
72
+ const lines = content.split('\n');
73
+
74
+ const result = [];
75
+ let currentFile = '';
76
+ let fileHeaderAdded = false;
77
+
78
+ for (const line of lines) {
79
+ if (line.startsWith('### ')) {
80
+ currentFile = line.slice(4).trim();
81
+ fileHeaderAdded = false;
82
+ continue;
83
+ }
84
+ // Skip markdown fences and top-level headers
85
+ if (line.startsWith('```') || line.startsWith('## ') || line.startsWith('# ') || line.startsWith('<!--')) {
86
+ continue;
87
+ }
88
+ if (line.toLowerCase().includes(query)) {
89
+ if (currentFile && !fileHeaderAdded) {
90
+ if (result.length > 0) result.push('');
91
+ result.push(`### ${currentFile}`);
92
+ fileHeaderAdded = true;
93
+ }
94
+ result.push(line);
95
+ }
96
+ }
97
+
98
+ if (result.length === 0) return `No signatures found matching: ${args.query}`;
99
+ return result.join('\n');
100
+ }
101
+
102
+ /**
103
+ * get_map({ type }) → string
104
+ *
105
+ * Returns a section from PROJECT_MAP.md.
106
+ * type: 'imports' | 'classes' | 'routes'
107
+ */
108
+ function getMap(args, cwd) {
109
+ if (!args || !args.type) return 'Missing required argument: type';
110
+
111
+ const header = MAP_SECTIONS[args.type];
112
+ if (!header) {
113
+ return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
114
+ }
115
+
116
+ const mapPath = path.join(cwd, 'PROJECT_MAP.md');
117
+ if (!fs.existsSync(mapPath)) {
118
+ return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
119
+ }
120
+
121
+ const content = fs.readFileSync(mapPath, 'utf8');
122
+ const idx = content.indexOf(header);
123
+ if (idx === -1) {
124
+ return `Section "${header}" not found in PROJECT_MAP.md`;
125
+ }
126
+
127
+ // Extract from this header to the next ### header
128
+ const after = content.slice(idx);
129
+ const nextMatch = after.slice(header.length).search(/\n###\s/);
130
+ return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
131
+ }
132
+
133
+ /**
134
+ * create_checkpoint({ note? }) → string
135
+ *
136
+ * Returns a markdown checkpoint summarising current project state:
137
+ * - Timestamp and optional user note
138
+ * - Active git branch + last 5 commit messages
139
+ * - Token count of current context file
140
+ * - List of modules present in the context
141
+ * - Route count (if PROJECT_MAP.md exists)
142
+ */
143
+ function createCheckpoint(args, cwd) {
144
+ const note = (args && args.note) ? args.note.trim() : '';
145
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
146
+ const lines = [
147
+ '# SigMap Checkpoint',
148
+ `**Created:** ${now}`,
149
+ ];
150
+
151
+ if (note) lines.push(`**Note:** ${note}`);
152
+ lines.push('');
153
+
154
+ // ── Git info ────────────────────────────────────────────────────────────
155
+ lines.push('## Git state');
156
+ try {
157
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
158
+ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
159
+ }).trim();
160
+ lines.push(`**Branch:** ${branch}`);
161
+ } catch (_) {
162
+ lines.push('**Branch:** (not a git repo)');
163
+ }
164
+
165
+ try {
166
+ const log = execSync(
167
+ 'git log --oneline -5 --no-decorate 2>/dev/null',
168
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
169
+ ).trim();
170
+ if (log) {
171
+ lines.push('');
172
+ lines.push('**Recent commits:**');
173
+ for (const l of log.split('\n')) lines.push(`- ${l}`);
174
+ }
175
+ } catch (_) {} // ignore — not every project uses git
176
+ lines.push('');
177
+
178
+ // ── Context stats ────────────────────────────────────────────────────────
179
+ lines.push('## Context snapshot');
180
+ const contextPath = path.join(cwd, CONTEXT_FILE);
181
+ if (fs.existsSync(contextPath)) {
182
+ const content = fs.readFileSync(contextPath, 'utf8');
183
+ const tokens = Math.ceil(content.length / 4);
184
+
185
+ // Count modules (### headers are file paths)
186
+ const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
187
+ lines.push(`**Token count:** ~${tokens}`);
188
+ lines.push(`**Modules in context:** ${modules.length}`);
189
+
190
+ if (modules.length > 0) {
191
+ lines.push('');
192
+ lines.push('**Modules:**');
193
+ for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
194
+ if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
195
+ }
196
+ } else {
197
+ lines.push('_No context file found. Run: node gen-context.js_');
198
+ }
199
+ lines.push('');
200
+
201
+ // ── Route summary ────────────────────────────────────────────────────────
202
+ const mapPath = path.join(cwd, 'PROJECT_MAP.md');
203
+ if (fs.existsSync(mapPath)) {
204
+ const mapContent = fs.readFileSync(mapPath, 'utf8');
205
+ const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
206
+ if (routeLines.length > 0) {
207
+ lines.push('## Routes');
208
+ lines.push(`**Total routes detected:** ${routeLines.length}`);
209
+ lines.push('');
210
+ for (const r of routeLines.slice(0, 10)) lines.push(r);
211
+ if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
212
+ lines.push('');
213
+ }
214
+ }
215
+
216
+ lines.push('---');
217
+ lines.push('_Generated by SigMap `create_checkpoint`_');
218
+
219
+ return lines.join('\n');
220
+ }
221
+
222
+ /**
223
+ * get_routing({}) → string
224
+ *
225
+ * Reads the current context file, classifies all indexed files by complexity,
226
+ * and returns a formatted markdown routing guide showing which files belong
227
+ * to the fast/balanced/powerful model tier.
228
+ */
229
+ function getRouting(args, cwd) {
230
+ const contextPath = path.join(cwd, CONTEXT_FILE);
231
+ if (!fs.existsSync(contextPath)) {
232
+ return (
233
+ '_No context file found. Run `node gen-context.js --routing` first._\n\n' +
234
+ 'This generates routing hints that map each file to a model tier:\n' +
235
+ '- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
236
+ '- **balanced** (sonnet/gpt-4o) — standard application code\n' +
237
+ '- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
238
+ );
239
+ }
240
+
241
+ // Parse file list from context (### headings are file paths)
242
+ const content = fs.readFileSync(contextPath, 'utf8');
243
+ const fileRels = content.split('\n')
244
+ .filter((l) => l.startsWith('### '))
245
+ .map((l) => l.slice(4).trim());
246
+
247
+ // Build synthetic fileEntries for the classifier
248
+ // We don't have live sig arrays here, so rebuild from the context blocks
249
+ const entries = [];
250
+ const blocks = content.split(/^### /m).slice(1); // slice past the header
251
+ for (const block of blocks) {
252
+ const firstLine = block.split('\n')[0].trim();
253
+ const codeBlock = block.match(/```\n([\s\S]*?)```/);
254
+ const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
255
+ entries.push({ filePath: path.join(cwd, firstLine), sigs });
256
+ }
257
+
258
+ try {
259
+ const { classifyAll } = require('../../src/routing/classifier');
260
+ const { formatRoutingSection } = require('../../src/routing/hints');
261
+ const groups = classifyAll(entries, cwd);
262
+ return formatRoutingSection(groups);
263
+ } catch (err) {
264
+ return `_Routing classification failed: ${err.message}_`;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * explain_file({ path }) → string
270
+ *
271
+ * Returns a file's signatures, its direct imports, and files that import it.
272
+ * path: relative path from project root (e.g. 'src/services/auth.ts')
273
+ */
274
+ function explainFile(args, cwd) {
275
+ if (!args || !args.path) return 'Missing required argument: path';
276
+
277
+ const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
278
+ const targetAbs = path.resolve(cwd, targetRel);
279
+ const contextPath = path.join(cwd, CONTEXT_FILE);
280
+
281
+ const lines = ['# explain_file: ' + targetRel, ''];
282
+
283
+ // ── Signatures (from context file) ─────────────────────────────────────
284
+ lines.push('## Signatures');
285
+ let indexedFiles = [];
286
+
287
+ if (fs.existsSync(contextPath)) {
288
+ const ctxContent = fs.readFileSync(contextPath, 'utf8');
289
+ const ctxLines = ctxContent.split('\n');
290
+ let capturing = false;
291
+ const sigLines = [];
292
+
293
+ for (const line of ctxLines) {
294
+ if (line.startsWith('### ')) {
295
+ if (capturing) break; // already collected our block
296
+ const rel = line.slice(4).trim().replace(/\\/g, '/');
297
+ capturing = rel === targetRel || rel.endsWith('/' + targetRel) || targetRel.endsWith('/' + rel);
298
+ if (capturing) continue;
299
+ } else if (capturing) {
300
+ sigLines.push(line);
301
+ }
302
+ }
303
+
304
+ const sigs = sigLines.filter((l) => l !== '```' && l.trim() !== '');
305
+ if (sigs.length > 0) {
306
+ lines.push(...sigs);
307
+ } else {
308
+ lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
309
+ }
310
+
311
+ indexedFiles = ctxContent
312
+ .split('\n')
313
+ .filter((l) => l.startsWith('### '))
314
+ .map((l) => path.resolve(cwd, l.slice(4).trim()));
315
+ } else {
316
+ lines.push('_No context file found. Run: node gen-context.js_');
317
+ }
318
+
319
+ if (!fs.existsSync(targetAbs)) {
320
+ lines.push('');
321
+ lines.push('> File not found on disk: ' + targetRel);
322
+ return lines.join('\n');
323
+ }
324
+
325
+ lines.push('');
326
+
327
+ // ── Direct imports ────────────────────────────────────────────────────────
328
+ lines.push('## Imports (direct dependencies)');
329
+ try {
330
+ const { extractImports } = require('../map/import-graph');
331
+ const fileContent = fs.readFileSync(targetAbs, 'utf8');
332
+ const fileSet = new Set(indexedFiles);
333
+ fileSet.add(targetAbs);
334
+ const imports = extractImports(targetAbs, fileContent, fileSet);
335
+ if (imports.length > 0) {
336
+ for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
337
+ } else {
338
+ lines.push('_No resolvable relative imports found._');
339
+ }
340
+ } catch (err) {
341
+ lines.push('_Could not analyze imports: ' + err.message + '_');
342
+ }
343
+
344
+ lines.push('');
345
+
346
+ // ── Callers (reverse-import lookup) ──────────────────────────────────────
347
+ lines.push('## Callers (files that import this file)');
348
+ try {
349
+ const { extractImports } = require('../map/import-graph');
350
+ const fileSet = new Set(indexedFiles);
351
+ fileSet.add(targetAbs);
352
+ const callers = [];
353
+ for (const f of indexedFiles) {
354
+ if (f === targetAbs || !fs.existsSync(f)) continue;
355
+ try {
356
+ const fc = fs.readFileSync(f, 'utf8');
357
+ const imps = extractImports(f, fc, fileSet);
358
+ if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
359
+ } catch (_) {}
360
+ }
361
+ if (callers.length > 0) {
362
+ for (const c of callers) lines.push('- ' + c);
363
+ } else {
364
+ lines.push('_No indexed files import this file._');
365
+ }
366
+ } catch (err) {
367
+ lines.push('_Could not analyze callers: ' + err.message + '_');
368
+ }
369
+
370
+ return lines.join('\n');
371
+ }
372
+
373
+ /**
374
+ * list_modules({}) → string
375
+ *
376
+ * Lists all srcDir modules present in the context file, sorted by token count
377
+ * descending. Helps agents decide which module to query with read_context.
378
+ */
379
+ function listModules(args, cwd) {
380
+ const contextPath = path.join(cwd, CONTEXT_FILE);
381
+ if (!fs.existsSync(contextPath)) {
382
+ return 'No context file found. Run: node gen-context.js';
383
+ }
384
+
385
+ const content = fs.readFileSync(contextPath, 'utf8');
386
+ const ctxLines = content.split('\n');
387
+
388
+ const groups = {}; // key: top-level dir, value: { fileCount, tokenCount }
389
+ let currentGroup = null;
390
+ let blockBuf = [];
391
+
392
+ function flushBlock() {
393
+ if (currentGroup === null || blockBuf.length === 0) return;
394
+ if (!groups[currentGroup]) groups[currentGroup] = { fileCount: 0, tokenCount: 0 };
395
+ groups[currentGroup].fileCount++;
396
+ groups[currentGroup].tokenCount += Math.ceil(blockBuf.join('\n').length / 4);
397
+ blockBuf = [];
398
+ }
399
+
400
+ for (const line of ctxLines) {
401
+ if (line.startsWith('### ')) {
402
+ flushBlock();
403
+ const rel = line.slice(4).trim().replace(/\\/g, '/');
404
+ const parts = rel.split('/');
405
+ currentGroup = parts.length > 1 ? parts[0] : '.';
406
+ } else if (currentGroup !== null) {
407
+ blockBuf.push(line);
408
+ }
409
+ }
410
+ flushBlock();
411
+
412
+ const sorted = Object.entries(groups)
413
+ .map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
414
+ .sort((a, b) => b.tokenCount - a.tokenCount);
415
+
416
+ if (sorted.length === 0) return 'No modules found in context file.';
417
+
418
+ const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
419
+
420
+ return [
421
+ '# Modules',
422
+ '',
423
+ '| Module | Files | Tokens |',
424
+ '|--------|-------|--------|',
425
+ ...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
426
+ '',
427
+ `**Total context tokens: ~${total}**`,
428
+ '',
429
+ '_Use `read_context({ module: "name" })` to get signatures for a specific module._',
430
+ ].join('\n');
431
+ }
432
+
433
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules };
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SigMap MCP server — zero npm dependencies.
5
+ *
6
+ * Wire protocol: JSON-RPC 2.0 over stdio.
7
+ * One JSON object per line on both stdin and stdout.
8
+ *
9
+ * Supported methods:
10
+ * initialize → serverInfo + capabilities
11
+ * tools/list → 3 tool definitions
12
+ * tools/call → dispatch to handler, return result
13
+ */
14
+
15
+ const readline = require('readline');
16
+ const { TOOLS } = require('./tools');
17
+ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules } = require('./handlers');
18
+
19
+ const SERVER_INFO = {
20
+ name: 'sigmap',
21
+ version: '1.0.0',
22
+ description: 'SigMap MCP server — code signatures on demand',
23
+ };
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // JSON-RPC helpers
27
+ // ---------------------------------------------------------------------------
28
+ function respond(id, result) {
29
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
30
+ }
31
+
32
+ function respondError(id, code, message) {
33
+ process.stdout.write(
34
+ JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
35
+ );
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Method dispatcher
40
+ // ---------------------------------------------------------------------------
41
+ function dispatch(msg, cwd) {
42
+ const { method, id, params } = msg;
43
+
44
+ // Notifications (no id) need no response
45
+ if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
46
+ return;
47
+ }
48
+
49
+ if (method === 'initialize') {
50
+ respond(id, {
51
+ protocolVersion: (params && params.protocolVersion) || '2024-11-05',
52
+ serverInfo: SERVER_INFO,
53
+ capabilities: { tools: {} },
54
+ });
55
+ return;
56
+ }
57
+
58
+ if (method === 'tools/list') {
59
+ respond(id, { tools: TOOLS });
60
+ return;
61
+ }
62
+
63
+ if (method === 'tools/call') {
64
+ const name = params && params.name;
65
+ const args = (params && params.arguments) || {};
66
+
67
+ let text;
68
+ try {
69
+ if (name === 'read_context') text = readContext(args, cwd);
70
+ else if (name === 'search_signatures') text = searchSignatures(args, cwd);
71
+ else if (name === 'get_map') text = getMap(args, cwd);
72
+ else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
73
+ else if (name === 'get_routing') text = getRouting(args, cwd);
74
+ else if (name === 'explain_file') text = explainFile(args, cwd);
75
+ else if (name === 'list_modules') text = listModules(args, cwd);
76
+ else {
77
+ respondError(id, -32601, `Unknown tool: ${name}`);
78
+ return;
79
+ }
80
+ } catch (err) {
81
+ respondError(id, -32603, `Tool error: ${err.message}`);
82
+ return;
83
+ }
84
+
85
+ respond(id, {
86
+ content: [{ type: 'text', text: String(text) }],
87
+ });
88
+ return;
89
+ }
90
+
91
+ // Unknown method
92
+ if (id !== undefined && id !== null) {
93
+ respondError(id, -32601, `Method not found: ${method}`);
94
+ }
95
+ }
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Server entry point
99
+ // ---------------------------------------------------------------------------
100
+ function start(cwd) {
101
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
102
+
103
+ rl.on('line', (line) => {
104
+ const trimmed = line.trim();
105
+ if (!trimmed) return;
106
+
107
+ let msg;
108
+ try {
109
+ msg = JSON.parse(trimmed);
110
+ } catch (_) {
111
+ // Cannot respond without a valid id — ignore malformed input
112
+ return;
113
+ }
114
+
115
+ try {
116
+ dispatch(msg, cwd);
117
+ } catch (err) {
118
+ const id = (msg && msg.id) != null ? msg.id : null;
119
+ respondError(id, -32603, `Internal error: ${err.message}`);
120
+ }
121
+ });
122
+
123
+ rl.on('close', () => {
124
+ process.exit(0);
125
+ });
126
+ }
127
+
128
+ module.exports = { start };
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MCP tool definitions for SigMap.
5
+ * Three tools: read_context, search_signatures, get_map.
6
+ */
7
+
8
+ const TOOLS = [
9
+ {
10
+ name: 'read_context',
11
+ description:
12
+ 'Read extracted code signatures for the project or a specific module path. ' +
13
+ 'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
14
+ 'filtered subset when a module path is provided (~50–500 tokens).',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: {
18
+ module: {
19
+ type: 'string',
20
+ description:
21
+ 'Optional subdirectory path to scope results (e.g. "src/services"). ' +
22
+ 'Omit to get the full codebase context.',
23
+ },
24
+ },
25
+ required: [],
26
+ },
27
+ },
28
+ {
29
+ name: 'search_signatures',
30
+ description:
31
+ 'Search extracted code signatures for a keyword, function name, or class name. ' +
32
+ 'Returns matching signature lines with their file paths.',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ query: {
37
+ type: 'string',
38
+ description: 'Keyword to search for in signatures (case-insensitive).',
39
+ },
40
+ },
41
+ required: ['query'],
42
+ },
43
+ },
44
+ {
45
+ name: 'get_map',
46
+ description:
47
+ 'Read a section from PROJECT_MAP.md — import graph, class hierarchy, or route table. ' +
48
+ 'Requires gen-project-map.js to have been run first.',
49
+ inputSchema: {
50
+ type: 'object',
51
+ properties: {
52
+ type: {
53
+ type: 'string',
54
+ enum: ['imports', 'classes', 'routes'],
55
+ description: 'Which section to retrieve: imports, classes, or routes.',
56
+ },
57
+ },
58
+ required: ['type'],
59
+ },
60
+ },
61
+ {
62
+ name: 'create_checkpoint',
63
+ description:
64
+ 'Create a session checkpoint summarising current project state. ' +
65
+ 'Returns recent git commits, active branch, token count, and a ' +
66
+ 'compact snapshot of the codebase context — ideal for session handoffs ' +
67
+ 'or periodic saves during long coding sessions.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ note: {
72
+ type: 'string',
73
+ description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
74
+ },
75
+ },
76
+ required: [],
77
+ },
78
+ },
79
+ {
80
+ name: 'get_routing',
81
+ description:
82
+ 'Get model routing hints for this project — which files belong to which complexity ' +
83
+ 'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
84
+ 'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {},
88
+ required: [],
89
+ },
90
+ },
91
+ {
92
+ name: 'explain_file',
93
+ description:
94
+ 'Explain a specific file: returns its extracted signatures, direct imports ' +
95
+ '(files it depends on), and callers (files that import it). ' +
96
+ 'Ideal for understanding a file in isolation without reading raw source. ' +
97
+ 'Requires the context file to have been generated first.',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ path: {
102
+ type: 'string',
103
+ description:
104
+ 'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
105
+ 'Use the paths shown in read_context output.',
106
+ },
107
+ },
108
+ required: ['path'],
109
+ },
110
+ },
111
+ {
112
+ name: 'list_modules',
113
+ description:
114
+ 'List all top-level modules (srcDirs) present in the context file, ' +
115
+ 'sorted by token count descending. Use this to decide which module to ' +
116
+ 'pass to read_context before querying a specific area of the codebase.',
117
+ inputSchema: {
118
+ type: 'object',
119
+ properties: {},
120
+ required: [],
121
+ },
122
+ },
123
+ ];
124
+
125
+ module.exports = { TOOLS };