sigmap 6.10.11 → 6.11.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/CHANGELOG.md +21 -0
- package/gen-context.js +8 -4
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/extractors/line-anchor.js +52 -0
- package/src/extractors/python.js +36 -8
- package/src/extractors/python_ast.py +13 -6
- package/src/extractors/typescript.js +46 -15
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,27 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [6.11.0] — 2026-06-03
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Line anchors on signatures (Surgical Context Phase 1)** — top-level TypeScript and Python signatures now carry a `:start-end` line anchor (e.g. `export class UserRepository :18-36`), so agents can read the exact lines instead of re-opening whole files. Rendered automatically by `ask`, `CLAUDE.md`, and every adapter — no consumer changes (closes #212).
|
|
18
|
+
- New shared `src/extractors/line-anchor.js` helper (`lineAt`, `anchor`, `withAnchor`).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Block-comment / docstring line-shift bug** — comment stripping that blanked `/* */` and `"""…"""` to `''` destroyed newlines and corrupted line numbers. Replaced with a newline-preserving strip so char-offset → line-number stays exact. The Python AST and regex fallback paths now produce identical anchors.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [6.10.12] — 2026-05-27
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Portable `.mcp.json` support** — MCP server registration now detects and prioritizes `.mcp.json` at the project root, making MCP configuration portable across multiple agentic harnesses (Claude, Cursor, Windsurf, etc.). Falls back to `.claude/settings.json` if `.mcp.json` doesn't exist (closes #209).
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
13
34
|
## [6.10.11] — 2026-05-22
|
|
14
35
|
|
|
15
36
|
### Fixed
|
package/gen-context.js
CHANGED
|
@@ -5901,7 +5901,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5901
5901
|
|
|
5902
5902
|
const SERVER_INFO = {
|
|
5903
5903
|
name: 'sigmap',
|
|
5904
|
-
version: '6.
|
|
5904
|
+
version: '6.11.0',
|
|
5905
5905
|
description: 'SigMap MCP server — code signatures on demand',
|
|
5906
5906
|
};
|
|
5907
5907
|
|
|
@@ -8655,7 +8655,7 @@ const path = require('path');
|
|
|
8655
8655
|
const os = require('os');
|
|
8656
8656
|
const { execSync } = require('child_process');
|
|
8657
8657
|
|
|
8658
|
-
const VERSION = '6.
|
|
8658
|
+
const VERSION = '6.11.0';
|
|
8659
8659
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
8660
8660
|
|
|
8661
8661
|
function requireSourceOrBundled(key) {
|
|
@@ -10455,9 +10455,11 @@ function registerMcp(cwd, scriptPath) {
|
|
|
10455
10455
|
args: [path.resolve(scriptPath), '--mcp'],
|
|
10456
10456
|
};
|
|
10457
10457
|
|
|
10458
|
-
// JSON mcpServers targets:
|
|
10459
|
-
// VS Code (GitHub Copilot 1.99+),
|
|
10458
|
+
// JSON mcpServers targets: portable .mcp.json (priority), Claude, Cursor,
|
|
10459
|
+
// Windsurf project, Windsurf global, VS Code (GitHub Copilot 1.99+),
|
|
10460
|
+
// OpenCode project, OpenCode global, Gemini CLI
|
|
10460
10461
|
const jsonTargets = [
|
|
10462
|
+
path.join(cwd, '.mcp.json'),
|
|
10461
10463
|
path.join(cwd, '.claude', 'settings.json'),
|
|
10462
10464
|
path.join(cwd, '.cursor', 'mcp.json'),
|
|
10463
10465
|
path.join(cwd, '.windsurf', 'mcp.json'),
|
|
@@ -10496,6 +10498,8 @@ function registerMcp(cwd, scriptPath) {
|
|
|
10496
10498
|
|
|
10497
10499
|
// Print manual snippets for all targets
|
|
10498
10500
|
console.warn('[sigmap] MCP / context server config snippets:');
|
|
10501
|
+
console.warn(' .mcp.json (portable, recommended):');
|
|
10502
|
+
console.warn(JSON.stringify({ mcpServers: { sigmap: serverEntry } }, null, 2));
|
|
10499
10503
|
console.warn(' Claude / Cursor / Windsurf / VS Code / OpenCode / Gemini CLI:');
|
|
10500
10504
|
console.warn(JSON.stringify({ mcpServers: { sigmap: serverEntry } }, null, 2));
|
|
10501
10505
|
console.warn(' Zed (~/.config/zed/settings.json):');
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Line-anchor helpers for Surgical Context (v6.11.0).
|
|
5
|
+
*
|
|
6
|
+
* Signatures carry their source location as a `:start-end` suffix so an agent
|
|
7
|
+
* can read the exact lines instead of re-opening the whole file. The anchor is
|
|
8
|
+
* a plain string suffix, which keeps the existing `string[]` signature contract
|
|
9
|
+
* intact — ranker, adapters, and CLAUDE.md render it for free.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 1-based line number of character index `idx` within `src`.
|
|
14
|
+
* Counts newlines in the prefix, so it stays correct as long as the source
|
|
15
|
+
* being indexed preserves every newline (see the newline-preserving comment
|
|
16
|
+
* strips in the extractors).
|
|
17
|
+
*
|
|
18
|
+
* @param {string} src
|
|
19
|
+
* @param {number} idx
|
|
20
|
+
* @returns {number}
|
|
21
|
+
*/
|
|
22
|
+
function lineAt(src, idx) {
|
|
23
|
+
let line = 1;
|
|
24
|
+
const end = Math.min(idx, src.length);
|
|
25
|
+
for (let i = 0; i < end; i++) {
|
|
26
|
+
if (src.charCodeAt(i) === 10) line++;
|
|
27
|
+
}
|
|
28
|
+
return line;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render an anchor suffix: ` :start-end`.
|
|
33
|
+
* @param {number} start
|
|
34
|
+
* @param {number} end
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function anchor(start, end) {
|
|
38
|
+
return ` :${start}-${end}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Append a line anchor to a signature string.
|
|
43
|
+
* @param {string} sig
|
|
44
|
+
* @param {number} start
|
|
45
|
+
* @param {number} end
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function withAnchor(sig, start, end) {
|
|
49
|
+
return `${sig}${anchor(start, end)}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { lineAt, anchor, withAnchor };
|
package/src/extractors/python.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { lineAt } = require('./line-anchor');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 1-based line of the last source line belonging to a top-level (indent 0)
|
|
8
|
+
* def/class body that starts at `startLine` (1-based). Trailing blank lines
|
|
9
|
+
* are excluded.
|
|
10
|
+
* @param {string[]} srcLines
|
|
11
|
+
* @param {number} startLine
|
|
12
|
+
* @returns {number}
|
|
13
|
+
*/
|
|
14
|
+
function pyBlockEnd(srcLines, startLine) {
|
|
15
|
+
let end = startLine;
|
|
16
|
+
for (let i = startLine; i < srcLines.length; i++) {
|
|
17
|
+
const line = srcLines[i];
|
|
18
|
+
if (line.trim() === '') continue;
|
|
19
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
20
|
+
if (indent === 0) break;
|
|
21
|
+
end = i + 1;
|
|
22
|
+
}
|
|
23
|
+
return end;
|
|
24
|
+
}
|
|
4
25
|
|
|
5
26
|
/**
|
|
6
27
|
* Try to extract signatures using the native Python AST extractor.
|
|
@@ -42,21 +63,26 @@ function extract(src, filePath) {
|
|
|
42
63
|
|
|
43
64
|
// noComments: strip only # comments, keep docstrings (needed for @decorator detection)
|
|
44
65
|
const noComments = src.replace(/#.*$/gm, '');
|
|
45
|
-
// stripped: also strip docstrings (safe for regex matching)
|
|
66
|
+
// stripped: also strip docstrings (safe for regex matching). Docstrings are
|
|
67
|
+
// blanked newline-by-newline (non-newline chars → spaces) so character
|
|
68
|
+
// offsets and line numbers stay exact for line anchors.
|
|
46
69
|
const stripped = noComments
|
|
47
|
-
.replace(/"""[\s\S]*?"""/g, '')
|
|
48
|
-
.replace(/'''[\s\S]*?'''/g, '');
|
|
70
|
+
.replace(/"""[\s\S]*?"""/g, (m) => m.replace(/[^\n]/g, ' '))
|
|
71
|
+
.replace(/'''[\s\S]*?'''/g, (m) => m.replace(/[^\n]/g, ' '));
|
|
72
|
+
const srcLines = src.split('\n');
|
|
49
73
|
|
|
50
74
|
// Classes
|
|
51
75
|
for (const m of stripped.matchAll(/^class\s+(\w+)(?:\s*\(([^)]*)\))?\s*:/gm)) {
|
|
52
76
|
const className = m[1];
|
|
53
77
|
const baseName = m[2] ? m[2].trim() : '';
|
|
54
78
|
const bodyStart = m.index + m[0].length;
|
|
79
|
+
const clsStart = lineAt(stripped, m.index);
|
|
80
|
+
const clsAnchor = ` :${clsStart}-${pyBlockEnd(srcLines, clsStart)}`;
|
|
55
81
|
|
|
56
82
|
// Try @dataclass collapse
|
|
57
83
|
const dcFields = tryExtractDataclassFields(stripped, m.index);
|
|
58
84
|
if (dcFields !== null) {
|
|
59
|
-
sigs.push(`@dataclass ${className}(${dcFields})`);
|
|
85
|
+
sigs.push(`@dataclass ${className}(${dcFields})${clsAnchor}`);
|
|
60
86
|
continue;
|
|
61
87
|
}
|
|
62
88
|
|
|
@@ -64,13 +90,13 @@ function extract(src, filePath) {
|
|
|
64
90
|
if (/(BaseModel|BaseSettings)/.test(baseName)) {
|
|
65
91
|
const bmFields = tryExtractBaseModelFields(stripped, bodyStart);
|
|
66
92
|
if (bmFields) {
|
|
67
|
-
sigs.push(`class ${className}(${baseName}) ${bmFields}`);
|
|
93
|
+
sigs.push(`class ${className}(${baseName}) ${bmFields}${clsAnchor}`);
|
|
68
94
|
continue;
|
|
69
95
|
}
|
|
70
96
|
}
|
|
71
97
|
|
|
72
98
|
const baseStr = baseName ? `(${baseName})` : '';
|
|
73
|
-
sigs.push(`class ${className}${baseStr}`);
|
|
99
|
+
sigs.push(`class ${className}${baseStr}${clsAnchor}`);
|
|
74
100
|
|
|
75
101
|
// Class-level ALL_CAPS constants
|
|
76
102
|
for (const c of extractClassConstants(stripped, bodyStart)) {
|
|
@@ -91,7 +117,9 @@ function extract(src, filePath) {
|
|
|
91
117
|
const retStr = retType ? ` → ${retType}` : '';
|
|
92
118
|
const hint = extractDocHint(src, m[2], m[0]);
|
|
93
119
|
const hintStr = hint ? ` # ${hint}` : '';
|
|
94
|
-
|
|
120
|
+
const fnStart = lineAt(stripped, m.index);
|
|
121
|
+
const fnAnchor = ` :${fnStart}-${pyBlockEnd(srcLines, fnStart)}`;
|
|
122
|
+
sigs.push(`${asyncKw}def ${m[2]}(${params})${retStr}${fnAnchor}${hintStr}`);
|
|
95
123
|
}
|
|
96
124
|
|
|
97
125
|
// FastAPI router endpoints: @router.METHOD("path") + async def name(...)
|
|
@@ -103,7 +131,7 @@ function extract(src, filePath) {
|
|
|
103
131
|
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
104
132
|
const fl = lines[j].trim();
|
|
105
133
|
const fm = fl.match(/^(?:async\s+)?def\s+(\w+)/);
|
|
106
|
-
if (fm) { sigs.push(`${rm[1].toUpperCase()} ${rm[2]} → ${fm[1]}()`); break; }
|
|
134
|
+
if (fm) { const rs = j + 1; sigs.push(`${rm[1].toUpperCase()} ${rm[2]} → ${fm[1]}() :${rs}-${pyBlockEnd(srcLines, rs)}`); break; }
|
|
107
135
|
if (fl && !fl.startsWith('@') && !fl.startsWith('#')) break;
|
|
108
136
|
}
|
|
109
137
|
}
|
|
@@ -213,6 +213,12 @@ def extract_class_constants(class_node):
|
|
|
213
213
|
yield f"{name}={val}"
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def _anchor(node):
|
|
217
|
+
"""Return a ` :start-end` line anchor for a top-level node (1-based)."""
|
|
218
|
+
end = getattr(node, "end_lineno", None) or node.lineno
|
|
219
|
+
return f" :{node.lineno}-{end}"
|
|
220
|
+
|
|
221
|
+
|
|
216
222
|
def extract_method_sig(func_node):
|
|
217
223
|
"""Format a method signature string (already indented by caller)."""
|
|
218
224
|
is_async = isinstance(func_node, ast.AsyncFunctionDef)
|
|
@@ -232,7 +238,7 @@ def extract_function_sig(func_node, src_lines=None):
|
|
|
232
238
|
ret_str = f" → {ret}" if ret else ""
|
|
233
239
|
hint = get_docstring_hint(func_node)
|
|
234
240
|
hint_str = f" # {hint}" if hint else ""
|
|
235
|
-
return f"{prefix}def {func_node.name}({params}){ret_str}{hint_str}"
|
|
241
|
+
return f"{prefix}def {func_node.name}({params}){ret_str}{_anchor(func_node)}{hint_str}"
|
|
236
242
|
|
|
237
243
|
|
|
238
244
|
def extract_fastapi_routes(tree, src_lines):
|
|
@@ -255,7 +261,7 @@ def extract_fastapi_routes(tree, src_lines):
|
|
|
255
261
|
path_node = dec.args[0]
|
|
256
262
|
if isinstance(path_node, ast.Constant):
|
|
257
263
|
path = path_node.value
|
|
258
|
-
routes.append(f"{method.upper()} {path} → {node.name}()")
|
|
264
|
+
routes.append(f"{method.upper()} {path} → {node.name}(){_anchor(node)}")
|
|
259
265
|
return routes
|
|
260
266
|
|
|
261
267
|
|
|
@@ -276,10 +282,11 @@ def extract(filepath):
|
|
|
276
282
|
if isinstance(node, ast.ClassDef):
|
|
277
283
|
bases_str = ", ".join(annotation_to_str(b) for b in node.bases if b)
|
|
278
284
|
dec_names = get_decorator_names(node)
|
|
285
|
+
cls_anchor = _anchor(node)
|
|
279
286
|
|
|
280
287
|
if is_dataclass(node):
|
|
281
288
|
fields = extract_dataclass_fields(node)
|
|
282
|
-
sigs.append(f"@dataclass {node.name}({fields})")
|
|
289
|
+
sigs.append(f"@dataclass {node.name}({fields}){cls_anchor}")
|
|
283
290
|
elif is_basemodel(node.bases):
|
|
284
291
|
bm_fields = extract_basemodel_fields(node)
|
|
285
292
|
base_label = next(
|
|
@@ -288,12 +295,12 @@ def extract(filepath):
|
|
|
288
295
|
"BaseModel"
|
|
289
296
|
)
|
|
290
297
|
if bm_fields:
|
|
291
|
-
sigs.append(f"class {node.name}({base_label}) {bm_fields}")
|
|
298
|
+
sigs.append(f"class {node.name}({base_label}) {bm_fields}{cls_anchor}")
|
|
292
299
|
else:
|
|
293
|
-
sigs.append(f"class {node.name}({base_label})")
|
|
300
|
+
sigs.append(f"class {node.name}({base_label}){cls_anchor}")
|
|
294
301
|
else:
|
|
295
302
|
base_part = f"({bases_str})" if bases_str else ""
|
|
296
|
-
sigs.append(f"class {node.name}{base_part}")
|
|
303
|
+
sigs.append(f"class {node.name}{base_part}{cls_anchor}")
|
|
297
304
|
|
|
298
305
|
# Class constants
|
|
299
306
|
for const in extract_class_constants(node):
|
|
@@ -1,37 +1,54 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { lineAt, withAnchor } = require('./line-anchor');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Extract signatures from TypeScript source code.
|
|
7
|
+
* Top-level declarations carry a `:start-end` line anchor (see line-anchor.js);
|
|
8
|
+
* indented members do not.
|
|
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 = [];
|
|
11
|
-
|
|
12
|
-
//
|
|
15
|
+
// anchors[i] is [start, end] for a top-level sig, or null for an indented member.
|
|
16
|
+
// Kept parallel to `sigs` so existing push/mutation logic stays untouched;
|
|
17
|
+
// anchors are applied once at return.
|
|
18
|
+
const anchors = [];
|
|
19
|
+
|
|
20
|
+
// Strip comments to simplify matching. Block comments are blanked
|
|
21
|
+
// newline-by-newline (non-newline chars → spaces) so character offsets AND
|
|
22
|
+
// line numbers stay exact. Line comments preserve their trailing newline.
|
|
13
23
|
const stripped = src
|
|
14
24
|
.replace(/\/\/.*$/gm, '')
|
|
15
|
-
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
25
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
|
|
26
|
+
|
|
27
|
+
// Index of the closing brace for a block whose body starts at bodyStart.
|
|
28
|
+
const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
|
|
16
29
|
|
|
17
30
|
// Exported interfaces
|
|
18
31
|
for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
|
|
32
|
+
const bodyStart = m.index + m[0].length;
|
|
19
33
|
sigs.push(`export interface ${m[1]}`);
|
|
34
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
20
35
|
// Collect members
|
|
21
|
-
const
|
|
22
|
-
const block = extractBlock(stripped, start);
|
|
36
|
+
const block = extractBlock(stripped, bodyStart);
|
|
23
37
|
const members = extractInterfaceMembers(block);
|
|
24
|
-
for (const mem of members) sigs.push(` ${mem}`);
|
|
38
|
+
for (const mem of members) { sigs.push(` ${mem}`); anchors.push(null); }
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
// Exported type aliases
|
|
28
42
|
for (const m of stripped.matchAll(/^export\s+type\s+(\w+)(?:<[^=]*>)?\s*=/gm)) {
|
|
29
43
|
sigs.push(`export type ${m[1]}`);
|
|
44
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, m.index + m[0].length)]);
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
// Exported enums
|
|
33
48
|
for (const m of stripped.matchAll(/^export\s+(?:const\s+)?enum\s+(\w+)\s*\{/gm)) {
|
|
49
|
+
const bodyStart = m.index + m[0].length;
|
|
34
50
|
sigs.push(`export enum ${m[1]}`);
|
|
51
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
// Classes (exported and internal)
|
|
@@ -39,11 +56,12 @@ function extract(src) {
|
|
|
39
56
|
for (const m of stripped.matchAll(classRegex)) {
|
|
40
57
|
const prefix = m[1] ? 'export ' : '';
|
|
41
58
|
const abs = m[2] ? 'abstract ' : '';
|
|
59
|
+
const bodyStart = m.index + m[0].length;
|
|
42
60
|
sigs.push(`${prefix}${abs}class ${m[3]}`);
|
|
43
|
-
|
|
44
|
-
const block = extractBlock(stripped,
|
|
61
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
62
|
+
const block = extractBlock(stripped, bodyStart);
|
|
45
63
|
const methods = extractClassMembers(block);
|
|
46
|
-
for (const meth of methods) sigs.push(` ${meth}`);
|
|
64
|
+
for (const meth of methods) { sigs.push(` ${meth}`); anchors.push(null); }
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
// Exported top-level functions (not methods)
|
|
@@ -53,11 +71,12 @@ function extract(src) {
|
|
|
53
71
|
const retMatch = m[0].match(/\)\s*:\s*([^{]+)\s*\{/);
|
|
54
72
|
const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 30) : '';
|
|
55
73
|
const retStr = retType ? ` → ${retType}` : '';
|
|
74
|
+
const bodyStart = m.index + m[0].length;
|
|
56
75
|
sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
|
|
76
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
|
|
57
77
|
|
|
58
78
|
// Hooks: capture compact return object shape for use* functions.
|
|
59
79
|
if (m[1].startsWith('use')) {
|
|
60
|
-
const bodyStart = m.index + m[0].length;
|
|
61
80
|
const body = stripped.slice(bodyStart, bodyStart + 800);
|
|
62
81
|
const ret = body.match(/return\s*\{([^}]{1,260})\}/);
|
|
63
82
|
if (ret) {
|
|
@@ -78,10 +97,14 @@ function extract(src) {
|
|
|
78
97
|
const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
|
|
79
98
|
const params = normalizeParams(m[2]);
|
|
80
99
|
sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
|
|
100
|
+
const bodyStart = stripped.indexOf('{', m.index + m[0].length);
|
|
101
|
+
const endLn = bodyStart !== -1
|
|
102
|
+
? lineAt(stripped, blockEndIdx(bodyStart + 1))
|
|
103
|
+
: lineAt(stripped, m.index + m[0].length);
|
|
104
|
+
anchors.push([lineAt(stripped, m.index), endLn]);
|
|
81
105
|
|
|
82
106
|
// Hooks: capture compact return object shape for use* functions.
|
|
83
107
|
if (m[1].startsWith('use')) {
|
|
84
|
-
const bodyStart = stripped.indexOf('{', m.index + m[0].length);
|
|
85
108
|
if (bodyStart !== -1) {
|
|
86
109
|
const body = stripped.slice(bodyStart, bodyStart + 800);
|
|
87
110
|
const ret = body.match(/return\s*\{([^}]{1,260})\}/);
|
|
@@ -102,22 +125,30 @@ function extract(src) {
|
|
|
102
125
|
// Zustand stores: export const useXxxStore = create<State>()(...)
|
|
103
126
|
for (const m of stripped.matchAll(/^export\s+const\s+(use\w+Store)\s*=\s*create(?:<[^>]*>)?\s*\(/gm)) {
|
|
104
127
|
const stateType = m[0].match(/create<([\w]+)>/)?.[1] || '';
|
|
128
|
+
const startLn = lineAt(stripped, m.index);
|
|
105
129
|
sigs.push(`export const ${m[1]} = create<${stateType}>(...)`);
|
|
130
|
+
anchors.push([startLn, startLn]);
|
|
106
131
|
const ifaceRe = new RegExp(`interface\\s+${stateType}\\s*\\{([\\s\\S]*?)\\}`);
|
|
107
132
|
const ifm = stripped.match(ifaceRe);
|
|
108
133
|
if (ifm) {
|
|
109
|
-
for (const fm of ifm[1].matchAll(/^\s+(\w+)\s*(?:\([^)]*\))?\s*:/gm)) sigs.push(` ${fm[1]}`);
|
|
134
|
+
for (const fm of ifm[1].matchAll(/^\s+(\w+)\s*(?:\([^)]*\))?\s*:/gm)) { sigs.push(` ${fm[1]}`); anchors.push(null); }
|
|
110
135
|
}
|
|
111
136
|
}
|
|
112
137
|
|
|
113
138
|
// API client objects: const xxxApi = { method: async () => {} }
|
|
114
139
|
for (const m of stripped.matchAll(/^(?:export\s+default\s+|const\s+)(\w*[Aa]pi\w*)\s*=\s*\{/gm)) {
|
|
115
|
-
const
|
|
140
|
+
const bodyStart = m.index + m[0].length;
|
|
141
|
+
const block = extractBlock(stripped, bodyStart);
|
|
116
142
|
const methods = [...block.matchAll(/^\s+(\w+)\s*:\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/gm)].map(mm => mm[1]);
|
|
117
|
-
if (methods.length)
|
|
143
|
+
if (methods.length) {
|
|
144
|
+
sigs.push(`${m[1]}: { ${methods.join(', ')} }`);
|
|
145
|
+
anchors.push([lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)]);
|
|
146
|
+
}
|
|
118
147
|
}
|
|
119
148
|
|
|
120
|
-
return sigs
|
|
149
|
+
return sigs
|
|
150
|
+
.map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
|
|
151
|
+
.slice(0, 35);
|
|
121
152
|
}
|
|
122
153
|
|
|
123
154
|
function extractBlock(src, startIndex) {
|
package/src/mcp/server.js
CHANGED