minovative-mind-cli 2.13.4 → 2.14.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 (37) hide show
  1. package/README.md +104 -42
  2. package/dist/commands/chat.d.ts +1 -1
  3. package/dist/commands/chat.js +1 -2
  4. package/dist/services/agent/slashCommands.js +47 -23
  5. package/dist/services/agent/syntaxAgent.js +13 -0
  6. package/dist/services/agent/types.d.ts +0 -4
  7. package/dist/services/agent-tools.d.ts +1 -1
  8. package/dist/services/agent-tools.js +2 -2
  9. package/dist/services/agent.d.ts +1 -7
  10. package/dist/services/agent.js +184 -209
  11. package/dist/services/ai.d.ts +19 -4
  12. package/dist/services/ai.js +97 -12
  13. package/dist/services/contextAgent.js +33 -8
  14. package/dist/services/investigationComplexity.js +1 -1
  15. package/dist/services/mentionEngine.d.ts +385 -0
  16. package/dist/services/mentionEngine.js +1395 -0
  17. package/dist/services/orchestration/investigationAgent.js +4 -1
  18. package/dist/services/orchestration/messageBus.d.ts +27 -4
  19. package/dist/services/orchestration/messageBus.js +206 -22
  20. package/dist/services/orchestration/orchestrator.js +4 -0
  21. package/dist/services/orchestration/scopedTools.js +16 -2
  22. package/dist/services/orchestration/subAgent.js +14 -2
  23. package/dist/services/proxyClient.d.ts +38 -2
  24. package/dist/services/proxyClient.js +42 -24
  25. package/dist/services/swebench/sweBenchRunnerService.js +1 -1
  26. package/dist/utils/config.d.ts +75 -0
  27. package/dist/utils/config.js +93 -0
  28. package/dist/utils/contextPrompts.d.ts +28 -4
  29. package/dist/utils/contextPrompts.js +70 -1
  30. package/dist/utils/historyPrompt.d.ts +166 -7
  31. package/dist/utils/historyPrompt.js +775 -30
  32. package/dist/utils/symbolExtractor.d.ts +111 -8
  33. package/dist/utils/symbolExtractor.js +616 -64
  34. package/dist/utils/systemPrompts.d.ts +3 -4
  35. package/dist/utils/systemPrompts.js +5 -34
  36. package/oclif.manifest.json +2 -2
  37. package/package.json +1 -1
@@ -1,4 +1,15 @@
1
1
  import * as path from 'node:path';
2
+ /**
3
+ * Estimates token consumption for a given string using a standard ~3.8 characters per token heuristic.
4
+ *
5
+ * @param text - The input string to estimate
6
+ * @returns Estimated token count
7
+ */
8
+ export function estimateTokens(text) {
9
+ if (!text)
10
+ return 0;
11
+ return Math.ceil(text.length / 3.8);
12
+ }
2
13
  /**
3
14
  * Calculates the leading indentation whitespace count of a source code line.
4
15
  *
@@ -20,13 +31,37 @@ function isCommentOrEmpty(line, ext) {
20
31
  const trimmed = line.trim();
21
32
  if (!trimmed)
22
33
  return true;
23
- if (ext === '.py')
34
+ if (ext === '.py' || ext === '.pyi')
24
35
  return trimmed.startsWith('#');
25
36
  return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*');
26
37
  }
27
38
  /**
28
- * Builds a robust regular expression for matching symbol declarations across supported language families
29
- * (TypeScript/JavaScript, Python, Go, and Rust).
39
+ * Parses a target symbol identifier into potential parent and member components.
40
+ * Handles patterns such as:
41
+ * - `UserService.getUser` -> `{ parentName: 'UserService', memberName: 'getUser' }`
42
+ * - `UserService#getUser` -> `{ parentName: 'UserService', memberName: 'getUser' }`
43
+ * - `UserService::getUser` -> `{ parentName: 'UserService', memberName: 'getUser' }`
44
+ * - `getUser` -> `{ parentName: null, memberName: 'getUser' }`
45
+ *
46
+ * @param rawTarget - The raw symbol string provided by the caller
47
+ * @returns Object with optional parentName and memberName
48
+ */
49
+ export function parseTargetSymbol(rawTarget) {
50
+ const trimmed = rawTarget.trim();
51
+ const sepMatch = trimmed.match(/^([A-Za-z0-9_$]+)(?:\.|#|::)([A-Za-z0-9_$]+)$/);
52
+ if (sepMatch) {
53
+ return {
54
+ parentName: sepMatch[1],
55
+ memberName: sepMatch[2],
56
+ };
57
+ }
58
+ return {
59
+ parentName: null,
60
+ memberName: trimmed,
61
+ };
62
+ }
63
+ /**
64
+ * Builds a regular expression for matching top-level symbol declarations across supported language families.
30
65
  *
31
66
  * @param symbolName - The name of the symbol to match
32
67
  * @param ext - The file extension determining language syntax rules
@@ -34,16 +69,110 @@ function isCommentOrEmpty(line, ext) {
34
69
  */
35
70
  function buildDeclarationRegex(symbolName, ext) {
36
71
  const s = symbolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
37
- if (ext === '.py') {
72
+ if (ext === '.py' || ext === '.pyi') {
38
73
  return new RegExp(`^\\s*(?:async\\s+)?(?:def|class)\\s+${s}\\b|^\\s*${s}\\s*[:=]`);
39
74
  }
40
75
  if (ext === '.go') {
41
76
  return new RegExp(`^\\s*(?:func(?:\\s+\\([^)]+\\))?\\s+${s}\\b|type\\s+${s}\\b|var\\s+${s}\\b|const\\s+${s}\\b)`);
42
77
  }
43
78
  if (ext === '.rs') {
44
- return new RegExp(`^\\s*(?:(?:pub\\s+)?(?:async\\s+)?fn\\s+${s}\\b|(?:pub\\s+)?struct\\s+${s}\\b|(?:pub\\s+)?enum\\s+${s}\\b|(?:pub\\s+)?trait\\s+${s}\\b|(?:pub\\s+)?impl(?:\\s+.*)?\\s+(?:for\\s+)?${s}\\b)`);
79
+ return new RegExp(`^\\s*(?:(?:pub(?:\\([^)]+\\))?\\s+)?(?:async\\s+|const\\s+|unsafe\\s+|extern\\s+)?fn\\s+${s}\\b|(?:pub(?:\\([^)]+\\))?\\s+)?struct\\s+${s}\\b|(?:pub(?:\\([^)]+\\))?\\s+)?enum\\s+${s}\\b|(?:pub(?:\\([^)]+\\))?\\s+)?trait\\s+${s}\\b|(?:pub(?:\\([^)]+\\))?\\s+)?impl(?:\\s+.*)?\\s+(?:for\\s+)?${s}\\b|(?:pub(?:\\([^)]+\\))?\\s+)?type\\s+${s}\\b|(?:pub(?:\\([^)]+\\))?\\s+)?const\\s+${s}\\b)`);
45
80
  }
46
- return new RegExp(`^\\s*(?:(?:export\\s+|default\\s+|async\\s+|abstract\\s+|static\\s+|public\\s+|private\\s+|protected\\s+|readonly\\s+)*(?:class|function|const|let|var|type|interface|enum)\\s+(?:[\\w<>]+\\s+)?${s}\\b|^\\s*${s}\\s*[:=]|^\\s*(?:(?:public|private|protected|static|abstract|override|async|\\s+)*)${s}\\s*(?:<[^>]+>)?\\s*\\()`);
81
+ return new RegExp(`^\\s*(?:(?:export\\s+|default\\s+|async\\s+|abstract\\s+|static\\s+|public\\s+|private\\s+|protected\\s+|readonly\\s+|declare\\s+)*(?:class|function|const|let|var|type|interface|enum)\\s+(?:[\\w<>]+\\s+)?${s}\\b|^\\s*${s}\\s*[:=]|^\\s*(?:(?:public|private|protected|static|abstract|override|async|readonly|\\s+)*)${s}\\s*(?:<[^>]+>)?\\s*\\()`);
82
+ }
83
+ /**
84
+ * Builds a regular expression for matching nested class/struct/impl member methods and fields.
85
+ *
86
+ * @param memberName - The name of the member method or property to match
87
+ * @param ext - The file extension
88
+ * @returns A RegExp matching the member definition within an enclosing body
89
+ */
90
+ function buildMemberRegex(memberName, ext) {
91
+ const m = memberName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ if (ext === '.py' || ext === '.pyi') {
93
+ return new RegExp(`^\\s*(?:async\\s+)?def\\s+${m}\\b|^\\s*${m}\\s*[:=]`);
94
+ }
95
+ if (ext === '.rs') {
96
+ return new RegExp(`^\\s*(?:(?:pub(?:\\([^)]+\\))?\\s+)?(?:async\\s+|const\\s+|unsafe\\s+|extern\\s+)?fn\\s+${m}\\b|(?:pub(?:\\([^)]+\\))?\\s+)?(?:type|const)\\s+${m}\\b)`);
97
+ }
98
+ if (ext === '.go') {
99
+ return new RegExp(`^\\s*func\\s+\\([^)]+\\)\\s+${m}\\b|^\\s*${m}\\s+`);
100
+ }
101
+ return new RegExp(`^\\s*(?:(?:public|private|protected|static|abstract|override|async|readonly|get|set|\\s+)*)${m}\\s*(?:<[^>]+>)?\\s*\\(|^\\s*(?:public|private|protected|static|readonly|\\s+)*${m}\\s*[:=;]`);
102
+ }
103
+ /**
104
+ * Infers the {@link SymbolKind} from a line and extension.
105
+ *
106
+ * @param line - The line declaring the symbol
107
+ * @param ext - The file extension
108
+ * @param isNested - Whether the symbol is inside a parent class/struct/impl
109
+ * @returns The classified SymbolKind
110
+ */
111
+ function inferSymbolKind(line, ext, isNested) {
112
+ const trimmed = line.trim();
113
+ if (ext === '.py' || ext === '.pyi') {
114
+ if (trimmed.startsWith('class '))
115
+ return 'class';
116
+ if (trimmed.includes('def '))
117
+ return isNested ? 'method' : 'function';
118
+ return 'variable';
119
+ }
120
+ if (ext === '.rs') {
121
+ if (trimmed.includes('struct '))
122
+ return 'struct';
123
+ if (trimmed.includes('enum '))
124
+ return 'enum';
125
+ if (trimmed.includes('trait '))
126
+ return 'trait';
127
+ if (trimmed.includes('impl '))
128
+ return 'impl';
129
+ if (trimmed.includes('type '))
130
+ return 'type';
131
+ if (trimmed.includes('const ') || trimmed.includes('static '))
132
+ return 'constant';
133
+ if (trimmed.includes('fn '))
134
+ return isNested ? 'method' : 'function';
135
+ return 'other';
136
+ }
137
+ if (ext === '.go') {
138
+ if (trimmed.includes('struct'))
139
+ return 'struct';
140
+ if (trimmed.includes('interface'))
141
+ return 'interface';
142
+ if (trimmed.startsWith('type '))
143
+ return 'type';
144
+ if (trimmed.startsWith('const '))
145
+ return 'constant';
146
+ if (trimmed.startsWith('var '))
147
+ return 'variable';
148
+ if (trimmed.startsWith('func ('))
149
+ return 'method';
150
+ if (trimmed.startsWith('func '))
151
+ return 'function';
152
+ return 'other';
153
+ }
154
+ // TypeScript / JavaScript
155
+ if (/\binterface\b/.test(trimmed))
156
+ return 'interface';
157
+ if (/\btype\b/.test(trimmed))
158
+ return 'type';
159
+ if (/\benum\b/.test(trimmed))
160
+ return 'enum';
161
+ if (/\bclass\b/.test(trimmed))
162
+ return 'class';
163
+ if (/\bconstructor\b/.test(trimmed))
164
+ return 'constructor';
165
+ if (/\bfunction\b/.test(trimmed) || (trimmed.includes('=>') && !isNested))
166
+ return 'function';
167
+ if (isNested && (trimmed.includes('(') || /\b(get|set)\b/.test(trimmed)))
168
+ return 'method';
169
+ if (/\b(const|readonly)\b/.test(trimmed))
170
+ return 'constant';
171
+ if (/\b(let|var)\b/.test(trimmed))
172
+ return 'variable';
173
+ if (isNested)
174
+ return 'property';
175
+ return 'other';
47
176
  }
48
177
  /**
49
178
  * Scans backwards from a declaration index to collect preceding docstrings, JSDoc blocks, decorators, and line comments.
@@ -58,9 +187,9 @@ function findPrecedingContext(lines, declarationIndex, ext) {
58
187
  let insideJsDoc = false;
59
188
  while (start > 0) {
60
189
  const prevLine = lines[start - 1].trim();
61
- const isJsDocEnd = ['.ts', '.js', '.tsx', '.jsx'].includes(ext) && prevLine === '*/';
62
- const isJsDocLine = ['.ts', '.js', '.tsx', '.jsx'].includes(ext) && prevLine.startsWith('*');
63
- const isJsDocStart = ['.ts', '.js', '.tsx', '.jsx'].includes(ext) && prevLine.startsWith('/**');
190
+ const isJsDocEnd = ['.ts', '.js', '.tsx', '.jsx', '.mts', '.cts', '.mjs', '.cjs'].includes(ext) && prevLine === '*/';
191
+ const isJsDocLine = ['.ts', '.js', '.tsx', '.jsx', '.mts', '.cts', '.mjs', '.cjs'].includes(ext) && prevLine.startsWith('*');
192
+ const isJsDocStart = ['.ts', '.js', '.tsx', '.jsx', '.mts', '.cts', '.mjs', '.cjs'].includes(ext) && prevLine.startsWith('/**');
64
193
  if (isJsDocEnd)
65
194
  insideJsDoc = true;
66
195
  if (!prevLine) {
@@ -73,9 +202,10 @@ function findPrecedingContext(lines, declarationIndex, ext) {
73
202
  }
74
203
  }
75
204
  const isDecorator = prevLine.startsWith('@');
76
- const isPythonComment = ext === '.py' && prevLine.startsWith('#');
205
+ const isPythonComment = (ext === '.py' || ext === '.pyi') && prevLine.startsWith('#');
77
206
  const isLineComment = prevLine.startsWith('//');
78
- if (isDecorator || isPythonComment || isLineComment || isJsDocEnd || isJsDocLine || isJsDocStart) {
207
+ const isRustDocOrAttr = (ext === '.rs') && (prevLine.startsWith('///') || prevLine.startsWith('//!') || prevLine.startsWith('#['));
208
+ if (isDecorator || isPythonComment || isLineComment || isJsDocEnd || isJsDocLine || isJsDocStart || isRustDocOrAttr) {
79
209
  start--;
80
210
  if (isJsDocStart) {
81
211
  insideJsDoc = false;
@@ -112,7 +242,7 @@ function scanLineValidity(line, state) {
112
242
  if (!state.inMultiLineComment && !state.inMultiLineString) {
113
243
  if (line[j] === '/' && line[j + 1] === '/')
114
244
  break;
115
- if (state.ext === '.py' && line[j] === '#')
245
+ if ((state.ext === '.py' || state.ext === '.pyi') && line[j] === '#')
116
246
  break;
117
247
  if (line[j] === '/' && line[j + 1] === '*') {
118
248
  state.inMultiLineComment = true;
@@ -208,7 +338,7 @@ function processLineChars(line, state) {
208
338
  */
209
339
  function findBlockEnd(lines, i, ext) {
210
340
  let endIndex = i;
211
- if (ext === '.py') {
341
+ if (ext === '.py' || ext === '.pyi') {
212
342
  const baseIndentation = getIndentation(lines[i]);
213
343
  for (let j = i + 1; j < lines.length; j++) {
214
344
  if (isCommentOrEmpty(lines[j], ext)) {
@@ -250,26 +380,36 @@ function addLinesToKeep(linesToKeep, start, end) {
250
380
  linesToKeep.add(k);
251
381
  }
252
382
  /**
253
- * Checks whether a line matches any of the target symbol declaration regular expressions.
383
+ * Extracts clean signature representation from lines.
254
384
  *
255
- * @param symbolRegexes - Array of compiled symbol regex objects
256
- * @param line - The source line string to test
257
- * @returns The matching SymbolRegex object if found, or null otherwise
385
+ * @param lines - File lines
386
+ * @param startLine - Start index of declaration
387
+ * @param endLine - End index of block
388
+ * @returns Cleaned signature string
258
389
  */
259
- function findMatchedRegex(symbolRegexes, line) {
260
- for (let k = 0; k < symbolRegexes.length; k++) {
261
- if (symbolRegexes[k].regex.test(line))
262
- return symbolRegexes[k];
390
+ function extractCleanSignature(lines, startLine, endLine) {
391
+ const sigLines = [];
392
+ for (let i = startLine; i <= endLine; i++) {
393
+ const line = lines[i];
394
+ sigLines.push(line);
395
+ if (line.includes('{') || line.trim().endsWith(':') || line.trim().endsWith(';'))
396
+ break;
397
+ }
398
+ const rawSig = sigLines.join('\n').trim();
399
+ const braceIdx = rawSig.indexOf('{');
400
+ if (braceIdx !== -1) {
401
+ return rawSig.substring(0, braceIdx).trim();
263
402
  }
264
- return null;
403
+ return rawSig;
265
404
  }
266
405
  /**
267
- * Extracts specified target symbols from source file content and returns their line number ranges and names.
406
+ * Extracts specified target symbols from source file content and returns their line number ranges, kind, and signatures.
407
+ * Supports granular dotted member extraction (e.g. `UserService.getUser` or `User#new`).
268
408
  *
269
409
  * @param content - The full raw string content of the source file
270
410
  * @param filePath - The file path (used to determine language syntax rules via file extension)
271
- * @param targetElements - An array of symbol names to extract
272
- * @returns Array of ExtractedSymbol objects containing symbol name, startLine, and endLine
411
+ * @param targetElements - An array of symbol names or dotted member targets to extract
412
+ * @returns Array of ExtractedSymbol objects containing symbol name, line ranges, and metadata
273
413
  */
274
414
  export function extractSymbolMetadata(content, filePath, targetElements) {
275
415
  if (!targetElements || targetElements.length === 0)
@@ -285,7 +425,16 @@ export function extractSymbolMetadata(content, filePath, targetElements) {
285
425
  validLines[i] = false;
286
426
  }
287
427
  }
288
- const symbolRegexes = targetElements.map((symbol) => ({ symbol, regex: buildDeclarationRegex(symbol, ext) }));
428
+ const symbolTargets = targetElements.map((target) => {
429
+ const { parentName, memberName } = parseTargetSymbol(target);
430
+ return {
431
+ target,
432
+ parentName,
433
+ memberName,
434
+ regex: buildDeclarationRegex(parentName || memberName, ext),
435
+ memberRegex: buildMemberRegex(memberName, ext),
436
+ };
437
+ });
289
438
  const symbols = [];
290
439
  let i = 0;
291
440
  while (i < lines.length) {
@@ -293,67 +442,470 @@ export function extractSymbolMetadata(content, filePath, targetElements) {
293
442
  i++;
294
443
  continue;
295
444
  }
296
- const matched = findMatchedRegex(symbolRegexes, lines[i]);
297
- if (matched) {
298
- const startIndex = findPrecedingContext(lines, i, ext);
299
- const endIndex = findBlockEnd(lines, i, ext);
300
- symbols.push({
301
- symbol: matched.symbol,
302
- startLine: startIndex,
303
- endLine: endIndex
304
- });
305
- i = endIndex + 1;
445
+ const line = lines[i];
446
+ for (const symTarget of symbolTargets) {
447
+ // 1. If target specified a parent (e.g. Class.method)
448
+ if (symTarget.parentName) {
449
+ if (symTarget.regex.test(line)) {
450
+ const parentStartIndex = findPrecedingContext(lines, i, ext);
451
+ const parentEndIndex = findBlockEnd(lines, i, ext);
452
+ // Scan inside the parent body for the member
453
+ for (let m = i + 1; m <= parentEndIndex; m++) {
454
+ if (!validLines[m])
455
+ continue;
456
+ if (symTarget.memberRegex.test(lines[m])) {
457
+ const memberStartIndex = findPrecedingContext(lines, m, ext);
458
+ const memberEndIndex = findBlockEnd(lines, m, ext);
459
+ const sig = extractCleanSignature(lines, memberStartIndex, memberEndIndex);
460
+ symbols.push({
461
+ symbol: symTarget.target,
462
+ parentSymbol: symTarget.parentName,
463
+ startLine: memberStartIndex,
464
+ endLine: memberEndIndex,
465
+ kind: inferSymbolKind(lines[m], ext, true),
466
+ signature: sig,
467
+ });
468
+ break;
469
+ }
470
+ }
471
+ }
472
+ }
473
+ else {
474
+ // 2. Direct top-level match or standalone member match
475
+ if (symTarget.regex.test(line)) {
476
+ const startIndex = findPrecedingContext(lines, i, ext);
477
+ const endIndex = findBlockEnd(lines, i, ext);
478
+ const sig = extractCleanSignature(lines, startIndex, endIndex);
479
+ symbols.push({
480
+ symbol: symTarget.target,
481
+ startLine: startIndex,
482
+ endLine: endIndex,
483
+ kind: inferSymbolKind(line, ext, false),
484
+ signature: sig,
485
+ });
486
+ i = endIndex;
487
+ break;
488
+ }
489
+ }
306
490
  }
307
- else {
491
+ i++;
492
+ }
493
+ // Deduplicate matched ranges
494
+ const uniqueSymbols = [];
495
+ const seenRanges = new Set();
496
+ for (const s of symbols) {
497
+ const key = `${s.symbol}:${s.startLine}:${s.endLine}`;
498
+ if (!seenRanges.has(key)) {
499
+ seenRanges.add(key);
500
+ uniqueSymbols.push(s);
501
+ }
502
+ }
503
+ return uniqueSymbols;
504
+ }
505
+ /**
506
+ * Scans a source file and extracts a comprehensive index of all declared symbols (classes, functions,
507
+ * methods, interfaces, structs, enums, types, etc.) along with their kind, line boundaries, and signatures.
508
+ *
509
+ * @param content - Source file content
510
+ * @param filePath - File path used to infer language and syntax rules
511
+ * @returns Array of all discovered ExtractedSymbol objects
512
+ */
513
+ export function extractSymbolIndex(content, filePath) {
514
+ if (!content || !content.trim())
515
+ return [];
516
+ const ext = path.extname(filePath).toLowerCase();
517
+ const lines = content.split('\n');
518
+ const symbols = [];
519
+ let i = 0;
520
+ while (i < lines.length) {
521
+ const line = lines[i];
522
+ const trimmed = line.trim();
523
+ if (!trimmed || isCommentOrEmpty(trimmed, ext)) {
308
524
  i++;
525
+ continue;
309
526
  }
527
+ // Check TS/JS declarations
528
+ if (['.ts', '.tsx', '.js', '.jsx', '.mts', '.cts', '.mjs', '.cjs'].includes(ext)) {
529
+ const classMatch = trimmed.match(/^(?:export\s+|default\s+|abstract\s+|declare\s+)*class\s+([A-Za-z0-9_$]+)/);
530
+ const ifaceMatch = trimmed.match(/^(?:export\s+|declare\s+|default\s+)*interface\s+([A-Za-z0-9_$]+)/);
531
+ const typeMatch = trimmed.match(/^(?:export\s+|declare\s+)*type\s+([A-Za-z0-9_$]+)/);
532
+ const enumMatch = trimmed.match(/^(?:export\s+|const\s+|declare\s+)*enum\s+([A-Za-z0-9_$]+)/);
533
+ const fnMatch = trimmed.match(/^(?:export\s+|default\s+|async\s+|declare\s+)*function(?:\s*\*|\s+([A-Za-z0-9_$]+))/);
534
+ const constFnMatch = trimmed.match(/^(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z0-9_$]+)\s*=>/);
535
+ if (classMatch) {
536
+ const className = classMatch[1];
537
+ const startLine = findPrecedingContext(lines, i, ext);
538
+ const endLine = findBlockEnd(lines, i, ext);
539
+ symbols.push({
540
+ symbol: className,
541
+ startLine,
542
+ endLine,
543
+ kind: 'class',
544
+ signature: extractCleanSignature(lines, i, endLine),
545
+ });
546
+ // Inspect inner members of class
547
+ for (let m = i + 1; m < endLine; m++) {
548
+ const mLine = lines[m].trim();
549
+ if (!mLine || isCommentOrEmpty(mLine, ext))
550
+ continue;
551
+ const methodMatch = mLine.match(/^(?:public\s+|private\s+|protected\s+|static\s+|async\s+|abstract\s+|override\s+|readonly\s+|get\s+|set\s+)*(constructor|[A-Za-z0-9_$]+)\s*(?:<[^>]+>)?\s*\(/);
552
+ if (methodMatch) {
553
+ const mName = methodMatch[1];
554
+ const mStart = findPrecedingContext(lines, m, ext);
555
+ const mEnd = findBlockEnd(lines, m, ext);
556
+ symbols.push({
557
+ symbol: `${className}.${mName}`,
558
+ parentSymbol: className,
559
+ startLine: mStart,
560
+ endLine: mEnd,
561
+ kind: mName === 'constructor' ? 'constructor' : 'method',
562
+ signature: extractCleanSignature(lines, m, mEnd),
563
+ });
564
+ m = mEnd;
565
+ }
566
+ }
567
+ i = endLine + 1;
568
+ continue;
569
+ }
570
+ if (ifaceMatch) {
571
+ const ifaceName = ifaceMatch[1];
572
+ const startLine = findPrecedingContext(lines, i, ext);
573
+ const endLine = findBlockEnd(lines, i, ext);
574
+ symbols.push({
575
+ symbol: ifaceName,
576
+ startLine,
577
+ endLine,
578
+ kind: 'interface',
579
+ signature: extractCleanSignature(lines, i, endLine),
580
+ });
581
+ i = endLine + 1;
582
+ continue;
583
+ }
584
+ if (typeMatch) {
585
+ const typeName = typeMatch[1];
586
+ const startLine = findPrecedingContext(lines, i, ext);
587
+ const endLine = findBlockEnd(lines, i, ext);
588
+ symbols.push({
589
+ symbol: typeName,
590
+ startLine,
591
+ endLine,
592
+ kind: 'type',
593
+ signature: extractCleanSignature(lines, i, endLine),
594
+ });
595
+ i = endLine + 1;
596
+ continue;
597
+ }
598
+ if (enumMatch) {
599
+ const enumName = enumMatch[1];
600
+ const startLine = findPrecedingContext(lines, i, ext);
601
+ const endLine = findBlockEnd(lines, i, ext);
602
+ symbols.push({
603
+ symbol: enumName,
604
+ startLine,
605
+ endLine,
606
+ kind: 'enum',
607
+ signature: extractCleanSignature(lines, i, endLine),
608
+ });
609
+ i = endLine + 1;
610
+ continue;
611
+ }
612
+ if (fnMatch && fnMatch[1]) {
613
+ const fnName = fnMatch[1];
614
+ const startLine = findPrecedingContext(lines, i, ext);
615
+ const endLine = findBlockEnd(lines, i, ext);
616
+ symbols.push({
617
+ symbol: fnName,
618
+ startLine,
619
+ endLine,
620
+ kind: 'function',
621
+ signature: extractCleanSignature(lines, i, endLine),
622
+ });
623
+ i = endLine + 1;
624
+ continue;
625
+ }
626
+ if (constFnMatch) {
627
+ const fnName = constFnMatch[1];
628
+ const startLine = findPrecedingContext(lines, i, ext);
629
+ const endLine = findBlockEnd(lines, i, ext);
630
+ symbols.push({
631
+ symbol: fnName,
632
+ startLine,
633
+ endLine,
634
+ kind: 'function',
635
+ signature: extractCleanSignature(lines, i, endLine),
636
+ });
637
+ i = endLine + 1;
638
+ continue;
639
+ }
640
+ }
641
+ else if (ext === '.py' || ext === '.pyi') {
642
+ const classMatch = trimmed.match(/^class\s+([A-Za-z0-9_$]+)/);
643
+ const fnMatch = trimmed.match(/^(?:async\s+)?def\s+([A-Za-z0-9_$]+)/);
644
+ if (classMatch) {
645
+ const className = classMatch[1];
646
+ const startLine = findPrecedingContext(lines, i, ext);
647
+ const endLine = findBlockEnd(lines, i, ext);
648
+ symbols.push({
649
+ symbol: className,
650
+ startLine,
651
+ endLine,
652
+ kind: 'class',
653
+ signature: extractCleanSignature(lines, i, endLine),
654
+ });
655
+ // Inspect inner methods
656
+ for (let m = i + 1; m <= endLine; m++) {
657
+ const mLine = lines[m];
658
+ const mTrim = mLine.trim();
659
+ const mMatch = mTrim.match(/^(?:async\s+)?def\s+([A-Za-z0-9_$]+)/);
660
+ if (mMatch) {
661
+ const mName = mMatch[1];
662
+ const mStart = findPrecedingContext(lines, m, ext);
663
+ const mEnd = findBlockEnd(lines, m, ext);
664
+ symbols.push({
665
+ symbol: `${className}.${mName}`,
666
+ parentSymbol: className,
667
+ startLine: mStart,
668
+ endLine: mEnd,
669
+ kind: 'method',
670
+ signature: extractCleanSignature(lines, m, mEnd),
671
+ });
672
+ m = mEnd;
673
+ }
674
+ }
675
+ i = endLine + 1;
676
+ continue;
677
+ }
678
+ if (fnMatch) {
679
+ const fnName = fnMatch[1];
680
+ const startLine = findPrecedingContext(lines, i, ext);
681
+ const endLine = findBlockEnd(lines, i, ext);
682
+ symbols.push({
683
+ symbol: fnName,
684
+ startLine,
685
+ endLine,
686
+ kind: 'function',
687
+ signature: extractCleanSignature(lines, i, endLine),
688
+ });
689
+ i = endLine + 1;
690
+ continue;
691
+ }
692
+ }
693
+ else if (ext === '.go') {
694
+ const typeMatch = trimmed.match(/^type\s+([A-Za-z0-9_$]+)\s+(struct|interface|\w+)/);
695
+ const funcMatch = trimmed.match(/^func\s+(?:\((?:[A-Za-z0-9_$*]+\s+)?([A-Za-z0-9_$*]+)\)\s+)?([A-Za-z0-9_$]+)/);
696
+ if (typeMatch) {
697
+ const typeName = typeMatch[1];
698
+ const typeKind = typeMatch[2] === 'struct' ? 'struct' : typeMatch[2] === 'interface' ? 'interface' : 'type';
699
+ const startLine = findPrecedingContext(lines, i, ext);
700
+ const endLine = findBlockEnd(lines, i, ext);
701
+ symbols.push({
702
+ symbol: typeName,
703
+ startLine,
704
+ endLine,
705
+ kind: typeKind,
706
+ signature: extractCleanSignature(lines, i, endLine),
707
+ });
708
+ i = endLine + 1;
709
+ continue;
710
+ }
711
+ if (funcMatch) {
712
+ const receiver = funcMatch[1]?.replace('*', '');
713
+ const fnName = funcMatch[2];
714
+ const startLine = findPrecedingContext(lines, i, ext);
715
+ const endLine = findBlockEnd(lines, i, ext);
716
+ symbols.push({
717
+ symbol: receiver ? `${receiver}.${fnName}` : fnName,
718
+ parentSymbol: receiver,
719
+ startLine,
720
+ endLine,
721
+ kind: receiver ? 'method' : 'function',
722
+ signature: extractCleanSignature(lines, i, endLine),
723
+ });
724
+ i = endLine + 1;
725
+ continue;
726
+ }
727
+ }
728
+ else if (ext === '.rs') {
729
+ const structMatch = trimmed.match(/^(?:pub(?:\([^)]+\))?\s+)?struct\s+([A-Za-z0-9_$]+)/);
730
+ const enumMatch = trimmed.match(/^(?:pub(?:\([^)]+\))?\s+)?enum\s+([A-Za-z0-9_$]+)/);
731
+ const traitMatch = trimmed.match(/^(?:pub(?:\([^)]+\))?\s+)?trait\s+([A-Za-z0-9_$]+)/);
732
+ const implMatch = trimmed.match(/^(?:pub(?:\([^)]+\))?\s+)?impl(?:\s+<[^>]+>)?\s+(?:([A-Za-z0-9_$]+)\s+for\s+)?([A-Za-z0-9_$]+)/);
733
+ const fnMatch = trimmed.match(/^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+|const\s+|unsafe\s+|extern\s+)?fn\s+([A-Za-z0-9_$]+)/);
734
+ if (structMatch) {
735
+ const name = structMatch[1];
736
+ const startLine = findPrecedingContext(lines, i, ext);
737
+ const endLine = findBlockEnd(lines, i, ext);
738
+ symbols.push({
739
+ symbol: name,
740
+ startLine,
741
+ endLine,
742
+ kind: 'struct',
743
+ signature: extractCleanSignature(lines, i, endLine),
744
+ });
745
+ i = endLine + 1;
746
+ continue;
747
+ }
748
+ if (enumMatch) {
749
+ const name = enumMatch[1];
750
+ const startLine = findPrecedingContext(lines, i, ext);
751
+ const endLine = findBlockEnd(lines, i, ext);
752
+ symbols.push({
753
+ symbol: name,
754
+ startLine,
755
+ endLine,
756
+ kind: 'enum',
757
+ signature: extractCleanSignature(lines, i, endLine),
758
+ });
759
+ i = endLine + 1;
760
+ continue;
761
+ }
762
+ if (traitMatch) {
763
+ const name = traitMatch[1];
764
+ const startLine = findPrecedingContext(lines, i, ext);
765
+ const endLine = findBlockEnd(lines, i, ext);
766
+ symbols.push({
767
+ symbol: name,
768
+ startLine,
769
+ endLine,
770
+ kind: 'trait',
771
+ signature: extractCleanSignature(lines, i, endLine),
772
+ });
773
+ i = endLine + 1;
774
+ continue;
775
+ }
776
+ if (implMatch) {
777
+ const traitName = implMatch[1];
778
+ const targetType = implMatch[2];
779
+ const implName = traitName ? `${traitName} for ${targetType}` : targetType;
780
+ const startLine = findPrecedingContext(lines, i, ext);
781
+ const endLine = findBlockEnd(lines, i, ext);
782
+ symbols.push({
783
+ symbol: implName,
784
+ parentSymbol: targetType,
785
+ startLine,
786
+ endLine,
787
+ kind: 'impl',
788
+ signature: extractCleanSignature(lines, i, endLine),
789
+ });
790
+ // Inspect inner methods in impl
791
+ for (let m = i + 1; m < endLine; m++) {
792
+ const mLine = lines[m].trim();
793
+ const mFnMatch = mLine.match(/^(?:pub(?:\([^)]+\))?\s+)?(?:async\s+|const\s+|unsafe\s+|extern\s+)?fn\s+([A-Za-z0-9_$]+)/);
794
+ if (mFnMatch) {
795
+ const fnName = mFnMatch[1];
796
+ const mStart = findPrecedingContext(lines, m, ext);
797
+ const mEnd = findBlockEnd(lines, m, ext);
798
+ symbols.push({
799
+ symbol: `${targetType}.${fnName}`,
800
+ parentSymbol: targetType,
801
+ startLine: mStart,
802
+ endLine: mEnd,
803
+ kind: 'method',
804
+ signature: extractCleanSignature(lines, m, mEnd),
805
+ });
806
+ m = mEnd;
807
+ }
808
+ }
809
+ i = endLine + 1;
810
+ continue;
811
+ }
812
+ if (fnMatch) {
813
+ const fnName = fnMatch[1];
814
+ const startLine = findPrecedingContext(lines, i, ext);
815
+ const endLine = findBlockEnd(lines, i, ext);
816
+ symbols.push({
817
+ symbol: fnName,
818
+ startLine,
819
+ endLine,
820
+ kind: 'function',
821
+ signature: extractCleanSignature(lines, i, endLine),
822
+ });
823
+ i = endLine + 1;
824
+ continue;
825
+ }
826
+ }
827
+ i++;
310
828
  }
311
829
  return symbols;
312
830
  }
313
831
  /**
314
- * Extracts specified target symbols (functions, classes, variables, interfaces, etc.) from source file content
832
+ * Chunks a source code file into discrete, structured {@link DefinitionChunk} objects.
833
+ * Each chunk encapsulates a class, interface, function, method, struct, or type declaration
834
+ * along with its line range, signature, estimated token count, and content.
835
+ *
836
+ * @param content - Source file content
837
+ * @param filePath - File path used for identifier naming and language rules
838
+ * @param options - Chunking configuration options
839
+ * @returns Array of DefinitionChunk objects
840
+ */
841
+ export function chunkDefinitions(content, filePath, options) {
842
+ if (!content || !content.trim())
843
+ return [];
844
+ const symbols = extractSymbolIndex(content, filePath);
845
+ const lines = content.split('\n');
846
+ const maxChunkLines = options?.maxChunkLines ?? 500;
847
+ const includeBodies = options?.includeBodies ?? true;
848
+ const filterKinds = options?.filterKinds ? new Set(options.filterKinds) : null;
849
+ const chunks = [];
850
+ for (const sym of symbols) {
851
+ if (filterKinds && sym.kind && !filterKinds.has(sym.kind))
852
+ continue;
853
+ let chunkLines = lines.slice(sym.startLine, sym.endLine + 1);
854
+ if (!includeBodies && (sym.kind === 'function' || sym.kind === 'method' || sym.kind === 'class')) {
855
+ const sig = sym.signature || extractCleanSignature(lines, sym.startLine, sym.endLine);
856
+ chunkLines = [sig + ' { ... }'];
857
+ }
858
+ else if (chunkLines.length > maxChunkLines) {
859
+ const truncated = chunkLines.slice(0, maxChunkLines);
860
+ truncated.push(`// ... [Truncated: ${chunkLines.length - maxChunkLines} additional lines omitted]`);
861
+ chunkLines = truncated;
862
+ }
863
+ const chunkContent = chunkLines.join('\n');
864
+ const chunkId = `${filePath}:${sym.symbol}`;
865
+ chunks.push({
866
+ id: chunkId,
867
+ symbol: sym.symbol,
868
+ parentSymbol: sym.parentSymbol,
869
+ kind: sym.kind || 'other',
870
+ startLine: sym.startLine,
871
+ endLine: sym.endLine,
872
+ signature: sym.signature || extractCleanSignature(lines, sym.startLine, sym.endLine),
873
+ doc: sym.doc,
874
+ content: chunkContent,
875
+ tokenEstimate: estimateTokens(chunkContent),
876
+ });
877
+ }
878
+ return chunks;
879
+ }
880
+ /**
881
+ * Extracts specified target symbols (functions, classes, methods, variables, interfaces, etc.) from source file content
315
882
  * using multi-language regex declarations, doc/decorator context gathering, and balanced brace/indentation block parsing.
316
883
  *
884
+ * Supports dotted / scoped member targeting (e.g. `['UserService.getUser']` or `['User#new']`).
885
+ *
317
886
  * This function significantly reduces token usage when reading large files by returning only the requested symbols
318
887
  * along with their context and omission separators.
319
888
  *
320
889
  * @param content - The full raw string content of the source file
321
890
  * @param filePath - The file path (used to determine language syntax rules via file extension)
322
891
  * @param targetElements - An array of symbol names to extract (e.g. `['extractSymbols', 'ExtractedSymbol']`)
892
+ * @param options - Formatting and chunking options
323
893
  * @returns The filtered source string containing only the matched symbol blocks and omission markers
324
894
  */
325
- export function extractSymbols(content, filePath, targetElements) {
895
+ export function extractSymbols(content, filePath, targetElements, options) {
326
896
  if (!targetElements || targetElements.length === 0)
327
897
  return content;
328
898
  const ext = path.extname(filePath).toLowerCase();
329
899
  const lines = content.split('\n');
330
900
  const linesToKeep = new Set();
331
- const validLines = new Array(lines.length).fill(true);
332
- const scanState = { inMultiLineComment: false, inMultiLineString: null, ext };
333
- for (let i = 0; i < lines.length; i++) {
334
- const startsInMulti = scanState.inMultiLineComment || scanState.inMultiLineString !== null;
335
- const lineHasValidCode = scanLineValidity(lines[i], scanState);
336
- if (startsInMulti || (!lineHasValidCode && (scanState.inMultiLineComment || scanState.inMultiLineString !== null))) {
337
- validLines[i] = false;
338
- }
339
- }
340
- const symbolRegexes = targetElements.map((symbol) => ({ symbol, regex: buildDeclarationRegex(symbol, ext) }));
341
- let i = 0;
342
- while (i < lines.length) {
343
- if (!validLines[i]) {
344
- i++;
345
- continue;
346
- }
347
- const matched = findMatchedRegex(symbolRegexes, lines[i]);
348
- if (matched) {
349
- const startIndex = findPrecedingContext(lines, i, ext);
350
- const endIndex = findBlockEnd(lines, i, ext);
351
- addLinesToKeep(linesToKeep, startIndex, endIndex);
352
- i = endIndex + 1;
353
- }
354
- else {
355
- i++;
901
+ const metadata = extractSymbolMetadata(content, filePath, targetElements);
902
+ for (const meta of metadata) {
903
+ let start = meta.startLine;
904
+ let end = meta.endLine;
905
+ if (options?.maxLinesPerSymbol && end - start + 1 > options.maxLinesPerSymbol) {
906
+ end = start + options.maxLinesPerSymbol - 1;
356
907
  }
908
+ addLinesToKeep(linesToKeep, start, end);
357
909
  }
358
910
  if (linesToKeep.size === 0) {
359
911
  return `// No specific symbols matching ${JSON.stringify(targetElements)} were found in this file.\n// Try using normal read_file without targetElements, or check spelling.`;