sigmap 6.11.1 → 6.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.11.1",
3
+ "version": "6.13.0",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "6.11.1",
3
+ "version": "6.13.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "6.11.1",
3
+ "version": "6.13.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,57 +1,88 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from JavaScript source code.
7
+ * Top-level declarations and class members carry a `:start-end` line anchor
8
+ * (see line-anchor.js); kept parallel to `sigs` and applied once at return.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
8
12
  function extract(src) {
9
13
  if (!src || typeof src !== 'string') return [];
10
14
  const sigs = [];
15
+ const anchors = [];
11
16
  const returnHints = buildReturnHints(src);
12
17
 
18
+ // Block comments are blanked newline-by-newline (non-newline chars → spaces)
19
+ // so character offsets AND line numbers stay exact for anchors.
13
20
  const stripped = src
14
21
  .replace(/\/\/.*$/gm, '')
15
- .replace(/\/\*[\s\S]*?\*\//g, '');
22
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
23
+
24
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
25
+ // End line for a function whose match ends at `matchEnd` (before its body brace).
26
+ const fnEndLine = (matchEnd, startLn) => {
27
+ const brace = stripped.indexOf('{', matchEnd);
28
+ return brace !== -1 ? lineAt(stripped, blockEndIdx(brace + 1)) : startLn;
29
+ };
16
30
 
17
31
  // Classes
18
32
  const classRegex = /^(export\s+(?:default\s+)?)?class\s+(\w+)(?:\s+extends\s+[\w.]+)?\s*\{/gm;
19
33
  for (const m of stripped.matchAll(classRegex)) {
20
34
  const prefix = m[1] ? m[1].trim() + ' ' : '';
35
+ const bodyStart = m.index + m[0].length;
21
36
  sigs.push(`${prefix}class ${m[2]}`);
22
- const block = extractBlock(stripped, m.index + m[0].length);
23
- for (const meth of extractClassMembers(block, returnHints)) sigs.push(` ${meth}`);
37
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
38
+ const block = extractBlock(stripped, bodyStart);
39
+ for (const meth of extractClassMembers(block, returnHints)) {
40
+ sigs.push(` ${meth.text}`);
41
+ anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
42
+ }
24
43
  }
25
44
 
26
45
  // Exported named functions
27
46
  for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
28
47
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
29
48
  const retStr = formatReturnHint(returnHints.get(m[1]));
49
+ const startLn = lineAt(stripped, m.index);
30
50
  sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
51
+ anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
31
52
  }
32
53
 
33
54
  // Exported arrow functions
34
55
  for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
35
56
  const asyncKw = m[0].includes('async') ? 'async ' : '';
36
57
  const retStr = formatReturnHint(returnHints.get(m[1]));
58
+ const startLn = lineAt(stripped, m.index);
37
59
  sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
60
+ anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
38
61
  }
39
62
 
40
63
  // module.exports = { ... }
41
64
  const moduleExports = stripped.match(/^module\.exports\s*=\s*\{([^}]+)\}/m);
42
65
  if (moduleExports) {
43
66
  const names = moduleExports[1].split(',').map((s) => s.trim()).filter(Boolean);
44
- if (names.length > 0) sigs.push(`module.exports = { ${names.join(', ')} }`);
67
+ if (names.length > 0) {
68
+ const startLn = lineAt(stripped, moduleExports.index);
69
+ sigs.push(`module.exports = { ${names.join(', ')} }`);
70
+ anchors.push([startLn, lineAt(stripped, moduleExports.index + moduleExports[0].length)]);
71
+ }
45
72
  }
46
73
 
47
74
  // Top-level named functions (non-exported)
48
75
  for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
49
76
  const asyncKw = m[0].startsWith('async') ? 'async ' : '';
50
77
  const retStr = formatReturnHint(returnHints.get(m[1]));
78
+ const startLn = lineAt(stripped, m.index);
51
79
  sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
80
+ anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
52
81
  }
53
82
 
54
- return sigs.slice(0, 25);
83
+ return sigs
84
+ .map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
85
+ .slice(0, 25);
55
86
  }
56
87
 
57
88
  function extractBlock(src, startIndex) {
@@ -66,15 +97,21 @@ function extractBlock(src, startIndex) {
66
97
  return src.slice(startIndex, i - 1);
67
98
  }
68
99
 
100
+ // Returns members as { text, start, end } where start/end are char offsets
101
+ // WITHIN `block` (end = the method's closing brace), so the caller can resolve
102
+ // per-method line anchors that span the method body.
69
103
  function extractClassMembers(block, returnHints) {
70
104
  const members = [];
71
105
  for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
72
106
  if (/^_/.test(m[1])) continue;
73
- if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
107
+ const bodyStart = m.index + m[0].length; // just past the opening brace
108
+ const end = bodyStart + extractBlock(block, bodyStart).length;
109
+ const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
110
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
74
111
  const isAsync = m[0].includes('async ') ? 'async ' : '';
75
112
  const isStatic = m[0].includes('static ') ? 'static ' : '';
76
113
  const retStr = formatReturnHint(returnHints.get(m[1]));
77
- members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`);
114
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
78
115
  }
79
116
  return members.slice(0, 8);
80
117
  }
@@ -35,7 +35,10 @@ function extract(src) {
35
35
  // Collect members
36
36
  const block = extractBlock(stripped, bodyStart);
37
37
  const members = extractInterfaceMembers(block);
38
- for (const mem of members) { sigs.push(` ${mem}`); anchors.push(null); }
38
+ for (const mem of members) {
39
+ sigs.push(` ${mem.text}`);
40
+ anchors.push([lineAt(stripped, bodyStart + mem.start), lineAt(stripped, bodyStart + mem.end)]);
41
+ }
39
42
  }
40
43
 
41
44
  // Exported type aliases
@@ -61,7 +64,10 @@ function extract(src) {
61
64
  anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
62
65
  const block = extractBlock(stripped, bodyStart);
63
66
  const methods = extractClassMembers(block);
64
- for (const meth of methods) { sigs.push(` ${meth}`); anchors.push(null); }
67
+ for (const meth of methods) {
68
+ sigs.push(` ${meth.text}`);
69
+ anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
70
+ }
65
71
  }
66
72
 
67
73
  // Exported top-level functions (not methods)
@@ -163,22 +169,29 @@ function extractBlock(src, startIndex) {
163
169
  return src.slice(startIndex, i - 1);
164
170
  }
165
171
 
172
+ // Returns members as { text, start, end } where start/end are char offsets
173
+ // WITHIN `block`, so the caller can resolve member line anchors.
166
174
  function extractInterfaceMembers(block) {
167
175
  const members = [];
168
176
  for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)(\??):\s*([^;]+);/gm)) {
169
177
  const readonly = m[1] ? 'readonly ' : '';
170
178
  const optional = m[3] ? '?' : '';
171
179
  const typeStr = m[4].trim().replace(/\s+/g, ' ').slice(0, 35);
172
- members.push(`${readonly}${m[2]}${optional}: ${typeStr}`);
180
+ const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
181
+ members.push({ text: `${readonly}${m[2]}${optional}: ${typeStr}`, start, end: m.index + m[0].length });
173
182
  }
174
183
  for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
175
- members.push(`${m[1]}(${normalizeParams(m[2])})`);
184
+ const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
185
+ members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
176
186
  }
177
187
  return members.slice(0, 8);
178
188
  }
179
189
 
180
190
  const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
181
191
 
192
+ // Returns members as { text, start, end } where start/end are char offsets
193
+ // WITHIN `block` (end = the method's closing brace), so the caller can resolve
194
+ // per-method line anchors that span the method body.
182
195
  function extractClassMembers(block) {
183
196
  const members = [];
184
197
  // Public methods (skip private/protected/_ prefixed and control-flow keywords)
@@ -186,13 +199,16 @@ function extractClassMembers(block) {
186
199
  for (const m of block.matchAll(methodRe)) {
187
200
  if (_CTRL_KEYWORDS.has(m[1])) continue;
188
201
  if (/^(private|protected|_)/.test(m[1])) continue;
189
- if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
202
+ const bodyStart = m.index + m[0].length; // just past the opening brace
203
+ const end = bodyStart + extractBlock(block, bodyStart).length;
204
+ const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
205
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
190
206
  const isAsync = m[0].includes('async ') ? 'async ' : '';
191
207
  const isStatic = m[0].includes('static ') ? 'static ' : '';
192
208
  const retMatch = m[0].match(/\)\s*:\s*([^{;]+)\s*\{/);
193
209
  const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 20) : '';
194
210
  const retStr = retType ? ` → ${retType}` : '';
195
- members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`);
211
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
196
212
  }
197
213
  return members.slice(0, 8);
198
214
  }
@@ -304,6 +304,106 @@ function sparkline(values) {
304
304
  }).join('');
305
305
  }
306
306
 
307
+ function escapeAttr(s) {
308
+ return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => (
309
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
310
+ ));
311
+ }
312
+
313
+ // Surgical Context (v6.12.0): read the published token-reduction benchmark and
314
+ // aggregate it for the dashboard panel. Numbers are never hand-typed — they come
315
+ // straight from benchmarks/reports/token-reduction.json.
316
+ function readTokenReduction(cwd) {
317
+ const p = path.join(cwd, 'benchmarks', 'reports', 'token-reduction.json');
318
+ let data;
319
+ try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; }
320
+ const repos = Array.isArray(data.repos) ? data.repos : [];
321
+ if (repos.length === 0) return null;
322
+
323
+ let baseline = 0, signatures = 0, surgical = 0, hasSurgical = false;
324
+ for (const r of repos) {
325
+ baseline += toNumber(r.rawTokens) || 0;
326
+ signatures += toNumber(r.finalTokens) || 0;
327
+ const s = toNumber(r.surgicalTokens);
328
+ if (s !== null) { surgical += s; hasSurgical = true; }
329
+ }
330
+
331
+ const out = {
332
+ version: data.version || null,
333
+ repoCount: repos.length,
334
+ baseline,
335
+ signatures,
336
+ savedPct: baseline > 0 ? Math.round((1 - signatures / baseline) * 1000) / 10 : 0,
337
+ perRepo: repos.map((r) => ({
338
+ repo: r.repo,
339
+ language: r.language,
340
+ rawTokens: toNumber(r.rawTokens) || 0,
341
+ finalTokens: toNumber(r.finalTokens) || 0,
342
+ reductionPct: toNumber(r.reductionPct) || 0,
343
+ })),
344
+ };
345
+ if (hasSurgical) {
346
+ out.surgical = surgical;
347
+ out.surgicalSavedPct = baseline > 0 ? Math.round((1 - surgical / baseline) * 1000) / 10 : 0;
348
+ }
349
+ return out;
350
+ }
351
+
352
+ function tokenReductionPanelHtml(tr) {
353
+ if (!tr) {
354
+ return '<div class="panel"><div class="label">Token Reduction</div>' +
355
+ '<div class="value" style="font-size:13px">No token-reduction benchmark found — run the token benchmark to populate this panel.</div></div>';
356
+ }
357
+ const fmt = (n) => Number(n).toLocaleString('en-US');
358
+ const tiers = [
359
+ { label: 'Whole-file baseline', value: fmt(tr.baseline) + ' tok' },
360
+ { label: 'Ranked signatures (ask)', value: fmt(tr.signatures) + ' tok' },
361
+ ];
362
+ if (tr.surgical != null) {
363
+ tiers.push({ label: 'Surgical (index + delta)', value: fmt(tr.surgical) + ' tok' });
364
+ tiers.push({ label: 'Saved (surgical)', value: tr.surgicalSavedPct + '%' });
365
+ } else {
366
+ tiers.push({ label: 'Saved', value: tr.savedPct + '%' });
367
+ }
368
+ const tierHtml = tiers.map((t) =>
369
+ `<div class="card"><div class="label">${escapeAttr(t.label)}</div><div class="value">${escapeAttr(t.value)}</div></div>`
370
+ ).join('');
371
+
372
+ // Proportional comparison bar (baseline = full width).
373
+ const sigPct = tr.baseline > 0 ? Math.max(0.4, (tr.signatures / tr.baseline) * 100) : 0;
374
+ const surgPct = (tr.surgical != null && tr.baseline > 0) ? Math.max(0.4, (tr.surgical / tr.baseline) * 100) : null;
375
+ const barRow = (label, pct, color) =>
376
+ `<div style="margin:4px 0;font-size:11px;color:#8ea0d9">${escapeAttr(label)}</div>` +
377
+ `<div style="background:#0a0f1e;border:1px solid #223056;border-radius:6px;height:14px;overflow:hidden">` +
378
+ `<div style="width:${pct.toFixed(1)}%;height:100%;background:${color}"></div></div>`;
379
+ const bars = [
380
+ barRow('Whole-file baseline (100%)', 100, '#3a4a78'),
381
+ barRow(`Ranked signatures — ${tr.savedPct}% saved`, sigPct, '#2e7d6b'),
382
+ surgPct != null ? barRow(`Surgical — ${tr.surgicalSavedPct}% saved`, surgPct, '#5ad1a8') : '',
383
+ ].filter(Boolean).join('');
384
+
385
+ const rows = tr.perRepo.slice(0, 8).map((r) =>
386
+ `<tr><td>${escapeAttr(r.repo)}</td><td>${escapeAttr(r.language)}</td>` +
387
+ `<td style="text-align:right">${fmt(r.rawTokens)}</td>` +
388
+ `<td style="text-align:right">${fmt(r.finalTokens)}</td>` +
389
+ `<td style="text-align:right">${r.reductionPct}%</td></tr>`
390
+ ).join('');
391
+
392
+ return [
393
+ '<div class="panel">',
394
+ `<div class="label">Token Reduction — ${tr.repoCount} benchmark repos${tr.version ? ' · v' + escapeAttr(tr.version) : ''}</div>`,
395
+ `<div class="grid" style="margin:8px 0">${tierHtml}</div>`,
396
+ bars,
397
+ '<table style="width:100%;border-collapse:collapse;margin-top:10px;font-size:12px">',
398
+ '<thead><tr style="color:#8ea0d9;text-align:left">',
399
+ '<th>Repo</th><th>Lang</th><th style="text-align:right">Baseline</th><th style="text-align:right">Signatures</th><th style="text-align:right">Saved</th>',
400
+ '</tr></thead>',
401
+ `<tbody>${rows}</tbody>`,
402
+ '</table>',
403
+ '</div>',
404
+ ].join('');
405
+ }
406
+
307
407
  function buildDashboardData(cwd, health) {
308
408
  const entries = readLog(cwd);
309
409
  const recent = entries.slice(-30);
@@ -326,15 +426,19 @@ function buildDashboardData(cwd, health) {
326
426
  extractorCoverage: coverage.pct,
327
427
  };
328
428
 
429
+ const tokenReduction = readTokenReduction(cwd);
430
+
329
431
  return {
330
432
  summary,
331
433
  tokenReductionTrend,
332
434
  hitAt5Trend,
333
435
  coverage,
436
+ tokenReduction,
334
437
  charts: {
335
438
  tokenReductionSvg: lineChartSvg(tokenReductionTrend, 'Token reduction trend (last 30 tracked runs)', '%'),
336
439
  hitAt5Svg: lineChartSvg(hitAt5Trend, 'hit@5 trend (last 30 benchmark runs)', ''),
337
440
  coverageSvg: barChartSvg(coverage.perLanguage),
441
+ tokenSavingsPanel: tokenReductionPanelHtml(tokenReduction),
338
442
  },
339
443
  };
340
444
  }
@@ -379,6 +483,7 @@ function generateDashboardHtml(cwd, health) {
379
483
  '<h1>SigMap v2.10 dashboard</h1>',
380
484
  '<div class="sub">Self-contained report. No external scripts, styles, or network calls.</div>',
381
485
  `<div class="grid">${cardHtml}</div>`,
486
+ data.charts.tokenSavingsPanel,
382
487
  `<div class="panel">${data.charts.tokenReductionSvg}</div>`,
383
488
  `<div class="panel">${data.charts.hitAt5Svg}</div>`,
384
489
  `<div class="panel">${data.charts.coverageSvg}</div>`,
@@ -448,4 +448,61 @@ function getImpact(args, cwd) {
448
448
  }
449
449
  }
450
450
 
451
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact };
451
+ /**
452
+ * get_lines({ file, start, end }) → string
453
+ *
454
+ * Surgical Context demand-driven fetch: returns an exact, clamped line range from a
455
+ * source file. The path is resolved inside the project root (no traversal escape) and
456
+ * the returned lines are secret-scanned via the same redactor used for signatures.
457
+ */
458
+ function getLines(args, cwd) {
459
+ if (!args || !args.file) return 'Missing required argument: file';
460
+
461
+ const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
462
+ const abs = path.resolve(cwd, rel);
463
+
464
+ // Sandbox: refuse paths that resolve outside the project root.
465
+ const root = path.resolve(cwd);
466
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
467
+ return `Refused: ${rel} resolves outside the project root`;
468
+ }
469
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
470
+ return `File not found: ${rel}`;
471
+ }
472
+
473
+ const start = parseInt(args.start, 10);
474
+ const end = parseInt(args.end, 10);
475
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
476
+ return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
477
+ }
478
+
479
+ let lines;
480
+ try {
481
+ lines = fs.readFileSync(abs, 'utf8').split('\n');
482
+ } catch (err) {
483
+ return `Could not read ${rel}: ${err.message}`;
484
+ }
485
+
486
+ const total = lines.length;
487
+ const from = Math.max(1, Math.min(start, end));
488
+ const to = Math.min(total, Math.max(start, end));
489
+ if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
490
+
491
+ const slice = lines.slice(from - 1, to);
492
+
493
+ // Redaction scan: reuse the signature secret scanner line-by-line.
494
+ let safeLines = slice;
495
+ try {
496
+ const { scan } = require('../security/scanner');
497
+ safeLines = scan(slice, rel).safe;
498
+ } catch (_) {} // non-fatal: fall back to raw slice
499
+
500
+ return [
501
+ `# ${rel}:${from}-${to}`,
502
+ '```',
503
+ ...safeLines,
504
+ '```',
505
+ ].join('\n');
506
+ }
507
+
508
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines };
package/src/mcp/server.js CHANGED
@@ -8,17 +8,17 @@
8
8
  *
9
9
  * Supported methods:
10
10
  * initialize → serverInfo + capabilities
11
- * tools/list → 3 tool definitions
11
+ * tools/list → 10 tool definitions
12
12
  * tools/call → dispatch to handler, return result
13
13
  */
14
14
 
15
15
  const readline = require('readline');
16
16
  const { TOOLS } = require('./tools');
17
- const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact } = require('./handlers');
17
+ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines } = require('./handlers');
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '6.11.1',
21
+ version: '6.13.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -75,6 +75,7 @@ function dispatch(msg, cwd) {
75
75
  else if (name === 'list_modules') text = listModules(args, cwd);
76
76
  else if (name === 'query_context') text = queryContext(args, cwd);
77
77
  else if (name === 'get_impact') text = getImpact(args, cwd);
78
+ else if (name === 'get_lines') text = getLines(args, cwd);
78
79
  else {
79
80
  respondError(id, -32601, `Unknown tool: ${name}`);
80
81
  return;
package/src/mcp/tools.js CHANGED
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * MCP tool definitions for SigMap.
5
- * Three tools: read_context, search_signatures, get_map.
4
+ * MCP tool definitions for SigMap (10 tools).
5
+ * read_context, search_signatures, get_map, create_checkpoint, get_routing,
6
+ * explain_file, list_modules, query_context, get_impact, get_lines.
6
7
  */
7
8
 
8
9
  const TOOLS = [
@@ -168,6 +169,34 @@ const TOOLS = [
168
169
  required: ['file'],
169
170
  },
170
171
  },
172
+ {
173
+ name: 'get_lines',
174
+ description:
175
+ 'Fetch an exact line range from a source file on demand — the Surgical Context ' +
176
+ 'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
177
+ 'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
178
+ 'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
179
+ inputSchema: {
180
+ type: 'object',
181
+ properties: {
182
+ file: {
183
+ type: 'string',
184
+ description:
185
+ 'Relative path from the project root (e.g. "src/config/loader.js"). ' +
186
+ 'Use the path shown in a signature anchor. Use forward slashes.',
187
+ },
188
+ start: {
189
+ type: 'number',
190
+ description: '1-based start line (inclusive). Clamped to the file bounds.',
191
+ },
192
+ end: {
193
+ type: 'number',
194
+ description: '1-based end line (inclusive). Clamped to the file bounds.',
195
+ },
196
+ },
197
+ required: ['file', 'start', 'end'],
198
+ },
199
+ },
171
200
  ];
172
201
 
173
202
  module.exports = { TOOLS };