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.
- package/.contextignore.example +34 -0
- package/CHANGELOG.md +402 -0
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/gen-context.config.json.example +40 -0
- package/gen-context.js +4316 -0
- package/gen-project-map.js +172 -0
- package/package.json +67 -0
- package/src/config/defaults.js +61 -0
- package/src/config/loader.js +60 -0
- package/src/extractors/cpp.js +60 -0
- package/src/extractors/csharp.js +48 -0
- package/src/extractors/css.js +51 -0
- package/src/extractors/dart.js +58 -0
- package/src/extractors/dockerfile.js +49 -0
- package/src/extractors/go.js +61 -0
- package/src/extractors/html.js +39 -0
- package/src/extractors/java.js +49 -0
- package/src/extractors/javascript.js +82 -0
- package/src/extractors/kotlin.js +62 -0
- package/src/extractors/php.js +62 -0
- package/src/extractors/python.js +69 -0
- package/src/extractors/ruby.js +43 -0
- package/src/extractors/rust.js +72 -0
- package/src/extractors/scala.js +67 -0
- package/src/extractors/shell.js +43 -0
- package/src/extractors/svelte.js +51 -0
- package/src/extractors/swift.js +63 -0
- package/src/extractors/typescript.js +109 -0
- package/src/extractors/vue.js +66 -0
- package/src/extractors/yaml.js +59 -0
- package/src/format/cache.js +53 -0
- package/src/health/scorer.js +123 -0
- package/src/map/class-hierarchy.js +117 -0
- package/src/map/import-graph.js +148 -0
- package/src/map/route-table.js +127 -0
- package/src/mcp/handlers.js +433 -0
- package/src/mcp/server.js +128 -0
- package/src/mcp/tools.js +125 -0
- package/src/routing/classifier.js +102 -0
- package/src/routing/hints.js +103 -0
- package/src/security/patterns.js +51 -0
- package/src/security/scanner.js +36 -0
- 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 };
|
package/src/mcp/tools.js
ADDED
|
@@ -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 };
|