minovative-mind-cli 1.5.1 → 2.1.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 (79) hide show
  1. package/README.md +59 -45
  2. package/dist/commands/chat.js +10 -2
  3. package/dist/services/agent/slashCommands.js +369 -42
  4. package/dist/services/agent/toolLoop.d.ts +1 -1
  5. package/dist/services/agent/toolLoop.js +7 -2
  6. package/dist/services/agent/types.d.ts +2 -0
  7. package/dist/services/agent-tools.d.ts +9 -4
  8. package/dist/services/agent-tools.js +272 -34
  9. package/dist/services/agent.d.ts +8 -0
  10. package/dist/services/agent.js +288 -40
  11. package/dist/services/ai.d.ts +19 -5
  12. package/dist/services/ai.js +182 -36
  13. package/dist/services/changeLogger.d.ts +142 -0
  14. package/dist/services/changeLogger.js +132 -3
  15. package/dist/services/contextAgent.d.ts +6 -1
  16. package/dist/services/contextAgent.js +112 -19
  17. package/dist/services/embeddingIndex.d.ts +82 -0
  18. package/dist/services/embeddingIndex.js +613 -0
  19. package/dist/services/investigationComplexity.d.ts +45 -0
  20. package/dist/services/investigationComplexity.js +91 -0
  21. package/dist/services/metrics.d.ts +18 -0
  22. package/dist/services/metrics.js +7 -0
  23. package/dist/services/orchestration/fileLockRegistry.d.ts +125 -0
  24. package/dist/services/orchestration/fileLockRegistry.js +276 -0
  25. package/dist/services/orchestration/investigationAgent.d.ts +85 -0
  26. package/dist/services/orchestration/investigationAgent.js +362 -0
  27. package/dist/services/orchestration/investigationOrchestrator.d.ts +53 -0
  28. package/dist/services/orchestration/investigationOrchestrator.js +180 -0
  29. package/dist/services/orchestration/messageBus.d.ts +162 -0
  30. package/dist/services/orchestration/messageBus.js +225 -0
  31. package/dist/services/orchestration/orchestrator.d.ts +45 -0
  32. package/dist/services/orchestration/orchestrator.js +217 -0
  33. package/dist/services/orchestration/readCache.d.ts +79 -0
  34. package/dist/services/orchestration/readCache.js +108 -0
  35. package/dist/services/orchestration/scopedTools.d.ts +57 -0
  36. package/dist/services/orchestration/scopedTools.js +172 -0
  37. package/dist/services/orchestration/subAgent.d.ts +58 -0
  38. package/dist/services/orchestration/subAgent.js +190 -0
  39. package/dist/services/orchestration/taskGraph.d.ts +129 -0
  40. package/dist/services/orchestration/taskGraph.js +254 -0
  41. package/dist/services/proxyClient.d.ts +25 -0
  42. package/dist/services/proxyClient.js +60 -0
  43. package/dist/services/workspaceRegistry.d.ts +137 -0
  44. package/dist/services/workspaceRegistry.js +270 -0
  45. package/dist/utils/asyncContext.d.ts +16 -0
  46. package/dist/utils/asyncContext.js +25 -0
  47. package/dist/utils/config.d.ts +3 -1
  48. package/dist/utils/config.js +3 -1
  49. package/dist/utils/contextPrompts.js +10 -3
  50. package/dist/utils/dependencyTracer/modules/api.d.ts +9 -0
  51. package/dist/utils/dependencyTracer/modules/api.js +62 -0
  52. package/dist/utils/dependencyTracer/modules/graph.d.ts +9 -0
  53. package/dist/utils/dependencyTracer/modules/graph.js +23 -0
  54. package/dist/utils/dependencyTracer/modules/profiles.d.ts +7 -0
  55. package/dist/utils/dependencyTracer/modules/profiles.js +120 -0
  56. package/dist/utils/dependencyTracer/modules/resolver.d.ts +7 -0
  57. package/dist/utils/dependencyTracer/modules/resolver.js +51 -0
  58. package/dist/utils/dependencyTracer/modules/types.d.ts +4 -0
  59. package/dist/utils/dependencyTracer/modules/types.js +1 -0
  60. package/dist/utils/dependencyTracer/modules/walker.d.ts +1 -0
  61. package/dist/utils/dependencyTracer/modules/walker.js +48 -0
  62. package/dist/utils/dependencyTracer.js +31 -17
  63. package/dist/utils/excludedExtensions.js +0 -1
  64. package/dist/utils/historyPrompt.d.ts +9 -0
  65. package/dist/utils/historyPrompt.js +87 -0
  66. package/dist/utils/logo.js +7 -7
  67. package/dist/utils/paste.d.ts +21 -0
  68. package/dist/utils/paste.js +22 -1
  69. package/dist/utils/pathSecurity.d.ts +31 -0
  70. package/dist/utils/pathSecurity.js +48 -0
  71. package/dist/utils/profiles.d.ts +2 -0
  72. package/dist/utils/profiles.js +44 -0
  73. package/dist/utils/projectStorage.js +10 -7
  74. package/dist/utils/systemPrompts.d.ts +6 -3
  75. package/dist/utils/systemPrompts.js +111 -6
  76. package/dist/utils/types.d.ts +33 -0
  77. package/dist/utils/types.js +1 -0
  78. package/oclif.manifest.json +2 -2
  79. package/package.json +4 -3
@@ -4,7 +4,8 @@ import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
6
  import { SchemaType } from '@google/generative-ai';
7
- import { resolveAndValidatePath } from '../utils/pathSecurity.js';
7
+ import { resolveAndValidatePath, resolveAndValidateMultiWorkspacePath } from '../utils/pathSecurity.js';
8
+ import { workspaceRegistry } from './workspaceRegistry.js';
8
9
  import { changeLogger } from './changeLogger.js';
9
10
  import { findBestMatch, applyMatch } from '../utils/fuzzyMatch.js';
10
11
  import { validateSyntax } from '../utils/syntaxValidator.js';
@@ -13,8 +14,22 @@ import { findDependencies, formatDependencyResult } from '../utils/dependencyTra
13
14
  import { atomicWriteFile } from '../utils/atomicWrite.js';
14
15
  import { EXCLUDED_EXTENSIONS } from '../utils/excludedExtensions.js';
15
16
  import { extractSymbols } from '../utils/symbolExtractor.js';
17
+ import { getMetricCollector } from './metrics.js';
16
18
  const execAsync = promisify(exec);
17
19
  // ─── Tool Declarations for Gemini Function Calling ───────────────────
20
+ let _semanticSearchEnabled = true;
21
+ export function isSemanticSearchEnabled() {
22
+ return _semanticSearchEnabled;
23
+ }
24
+ export function setSemanticSearchEnabled(val) {
25
+ _semanticSearchEnabled = val;
26
+ }
27
+ export function getToolDeclarations() {
28
+ if (_semanticSearchEnabled) {
29
+ return toolDeclarations;
30
+ }
31
+ return toolDeclarations.filter((t) => t.name !== 'semantic_search');
32
+ }
18
33
  /**
19
34
  * FunctionDeclaration-compatible schema objects that describe
20
35
  * every tool the agent can invoke. Passed to the model at init.
@@ -22,13 +37,13 @@ const execAsync = promisify(exec);
22
37
  export const toolDeclarations = [
23
38
  {
24
39
  name: 'read_file',
25
- description: 'Read the contents of a file at the given path relative to the workspace root. Returns the file text. Use startLine and endLine to read specific chunks of massive files to avoid context limits.',
40
+ description: 'Read the contents of a file at the given path relative to the workspace root. Supports text files and native parsing of .pdf files (including math and diagrams). Use startLine and endLine to read specific chunks of massive files to avoid context limits. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
26
41
  parameters: {
27
42
  type: SchemaType.OBJECT,
28
43
  properties: {
29
44
  filePath: {
30
45
  type: SchemaType.STRING,
31
- description: 'Relative path to the file from the workspace root.',
46
+ description: 'Relative path to the file from the workspace root, or @alias/path for external workspaces.',
32
47
  },
33
48
  startLine: {
34
49
  type: SchemaType.NUMBER,
@@ -49,13 +64,13 @@ export const toolDeclarations = [
49
64
  },
50
65
  {
51
66
  name: 'write_file',
52
- description: 'Create a new file or completely overwrite an existing file with the provided content. Use modify_file for targeted edits instead.',
67
+ description: 'Create a new file or completely overwrite an existing file with the provided content. Use modify_file for targeted edits instead. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
53
68
  parameters: {
54
69
  type: SchemaType.OBJECT,
55
70
  properties: {
56
71
  filePath: {
57
72
  type: SchemaType.STRING,
58
- description: 'Relative path to the file from the workspace root.',
73
+ description: 'Relative path to the file from the workspace root, or @alias/path for external workspaces.',
59
74
  },
60
75
  content: {
61
76
  type: SchemaType.STRING,
@@ -67,13 +82,13 @@ export const toolDeclarations = [
67
82
  },
68
83
  {
69
84
  name: 'delete_file',
70
- description: 'Deletes a file from the filesystem. Use this instead of running an rm command.',
85
+ description: 'Deletes a file from the filesystem. Use this instead of running an rm command. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
71
86
  parameters: {
72
87
  type: SchemaType.OBJECT,
73
88
  properties: {
74
89
  filePath: {
75
90
  type: SchemaType.STRING,
76
- description: 'Relative path to the file to delete.',
91
+ description: 'Relative path to the file to delete, or @alias/path for external workspaces.',
77
92
  },
78
93
  },
79
94
  required: ['filePath'],
@@ -81,17 +96,17 @@ export const toolDeclarations = [
81
96
  },
82
97
  {
83
98
  name: 'rename_file',
84
- description: 'Moves or renames a file. Use this instead of running an mv command.',
99
+ description: 'Moves or renames a file. Use this instead of running an mv command. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
85
100
  parameters: {
86
101
  type: SchemaType.OBJECT,
87
102
  properties: {
88
103
  sourcePath: {
89
104
  type: SchemaType.STRING,
90
- description: 'Relative path to the file to move/rename.',
105
+ description: 'Relative path to the file to move/rename, or @alias/path for external workspaces.',
91
106
  },
92
107
  targetPath: {
93
108
  type: SchemaType.STRING,
94
- description: 'New relative path for the file.',
109
+ description: 'New relative path for the file, or @alias/path for external workspaces.',
95
110
  },
96
111
  },
97
112
  required: ['sourcePath', 'targetPath'],
@@ -99,13 +114,13 @@ export const toolDeclarations = [
99
114
  },
100
115
  {
101
116
  name: 'modify_file',
102
- description: 'Perform one or multiple targeted search-and-replace edits in a single file. The search strings must match exactly (including whitespace). This is preferred over write_file for editing existing files because it preserves the rest of the file. Use this to batch multiple changes into a single operation.',
117
+ description: 'Perform one or multiple targeted search-and-replace edits in a single file. CRITICAL REQUIREMENT: You MUST use read_file or grep_search to fetch the exact current file content BEFORE using this tool. Do NOT guess or hallucinate the searchContent without reading the exact lines first, or the edit will fail. The search strings must match the current file exactly (including whitespace). This is preferred over write_file for editing existing files. For files in external workspaces, prefix the path with @alias/ (e.g., @backend/src/routes.ts).',
103
118
  parameters: {
104
119
  type: SchemaType.OBJECT,
105
120
  properties: {
106
121
  filePath: {
107
122
  type: SchemaType.STRING,
108
- description: 'Relative path to the file from the workspace root.',
123
+ description: 'Relative path to the file from the workspace root, or @alias/path for external workspaces.',
109
124
  },
110
125
  edits: {
111
126
  type: SchemaType.ARRAY,
@@ -131,13 +146,13 @@ export const toolDeclarations = [
131
146
  },
132
147
  {
133
148
  name: 'list_directory',
134
- description: 'List files and subdirectories in a directory relative to the workspace root. Returns a recursive tree structure.',
149
+ description: 'List files and subdirectories in a directory relative to the workspace root. Returns a recursive tree structure. For directories in external workspaces, prefix the path with @alias/ (e.g., @backend/src). Use "@all" to see a summary of all registered workspaces.',
135
150
  parameters: {
136
151
  type: SchemaType.OBJECT,
137
152
  properties: {
138
153
  dirPath: {
139
154
  type: SchemaType.STRING,
140
- description: 'Relative path to the directory from the workspace root. Use "." for the root.',
155
+ description: 'Relative path to the directory from the workspace root, @alias/path for external workspaces, or "@all" for all workspaces.',
141
156
  },
142
157
  maxDepth: {
143
158
  type: SchemaType.NUMBER,
@@ -163,7 +178,7 @@ export const toolDeclarations = [
163
178
  },
164
179
  {
165
180
  name: 'grep_search',
166
- description: 'Search for a text pattern across files in the workspace. Returns matching file paths with line numbers and content snippets. Uses Extended Regular Expressions (grep -E). IMPORTANT: grep searches line-by-line. Do NOT search for long lists of Tailwind classes or multi-line strings, as they will fail if line-wrapped. Search for short, unique substrings.',
181
+ description: 'Search for a text pattern across files in the workspace. Returns matching file paths with line numbers and content snippets. Uses Extended Regular Expressions (grep -E). IMPORTANT: grep searches line-by-line. Do NOT search for long lists of Tailwind classes or multi-line strings, as they will fail if line-wrapped. Search for short, unique substrings. Use the workspace parameter to search external workspaces.',
167
182
  parameters: {
168
183
  type: SchemaType.OBJECT,
169
184
  properties: {
@@ -175,6 +190,10 @@ export const toolDeclarations = [
175
190
  type: SchemaType.STRING,
176
191
  description: 'Optional glob to restrict file types, e.g. "*.ts" or "*.py". Defaults to all files.',
177
192
  },
193
+ workspace: {
194
+ type: SchemaType.STRING,
195
+ description: 'Optional. Workspace alias to search in (e.g., "backend"), or "all" to search all registered workspaces. Defaults to the primary workspace.',
196
+ },
178
197
  },
179
198
  required: ['pattern'],
180
199
  },
@@ -240,6 +259,24 @@ export const toolDeclarations = [
240
259
  required: ['language', 'code'],
241
260
  },
242
261
  },
262
+ {
263
+ name: 'semantic_search',
264
+ description: 'Search the codebase by meaning and concept rather than exact text match. Use this when you need to find code related to a concept, pattern, or behavior but don\'t know the exact variable or function names to grep for. Examples: "error handling for API requests", "user authentication flow", "database connection pooling logic". Returns ranked results with file paths, line ranges, and similarity scores.',
265
+ parameters: {
266
+ type: SchemaType.OBJECT,
267
+ properties: {
268
+ query: {
269
+ type: SchemaType.STRING,
270
+ description: "Natural language description of what you're looking for in the codebase.",
271
+ },
272
+ topK: {
273
+ type: SchemaType.NUMBER,
274
+ description: 'Number of results to return. Defaults to 5, maximum 15.',
275
+ },
276
+ },
277
+ required: ['query'],
278
+ },
279
+ },
243
280
  ];
244
281
  let currentApprovalMode = 'ask';
245
282
  export function getApprovalMode() {
@@ -248,6 +285,13 @@ export function getApprovalMode() {
248
285
  export function setApprovalMode(mode) {
249
286
  currentApprovalMode = mode;
250
287
  }
288
+ let subAgentsEnabled = true;
289
+ export function isSubAgentsEnabled() {
290
+ return subAgentsEnabled;
291
+ }
292
+ export function setSubAgentsEnabled(enabled) {
293
+ subAgentsEnabled = enabled;
294
+ }
251
295
  /**
252
296
  * If set to 'skip-once', reverts to 'ask' after a single command is run.
253
297
  */
@@ -300,6 +344,23 @@ async function getIgnoredPaths(workspaceRoot) {
300
344
  export async function readFile(workspaceRoot, filePath, startLine, endLine, targetElements) {
301
345
  try {
302
346
  const absPath = resolveAndValidatePath(workspaceRoot, filePath);
347
+ // Handle PDF files
348
+ if (filePath.toLowerCase().endsWith('.pdf')) {
349
+ const stats = await fs.stat(absPath);
350
+ const fileSizeMB = stats.size / (1024 * 1024);
351
+ if (fileSizeMB > 10) {
352
+ return {
353
+ output: '',
354
+ error: `File exceeds the 10MB safety limit to prevent excessive credit consumption. Please split the PDF into smaller chunks. (Size: ${fileSizeMB.toFixed(2)}MB)`,
355
+ };
356
+ }
357
+ const buffer = await fs.readFile(absPath);
358
+ const base64Data = buffer.toString('base64');
359
+ return {
360
+ output: `[PDF Data Attached: ${filePath}]`,
361
+ inlineData: { mimeType: 'application/pdf', data: base64Data },
362
+ };
363
+ }
303
364
  let content = await fs.readFile(absPath, 'utf-8');
304
365
  if (targetElements && targetElements.length > 0) {
305
366
  content = extractSymbols(content, filePath, targetElements);
@@ -356,6 +417,9 @@ export async function writeFile(workspaceRoot, filePath, content) {
356
417
  }
357
418
  catch (err) {
358
419
  const message = err instanceof Error ? err.message : String(err);
420
+ const collector = getMetricCollector();
421
+ if (collector)
422
+ collector.recordWriteFailure();
359
423
  return {
360
424
  output: '',
361
425
  error: `Failed to write file "${filePath}": ${message}`,
@@ -417,6 +481,9 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
417
481
  const edit = edits[i];
418
482
  const match = findBestMatch(modified, edit.searchContent);
419
483
  if (!match) {
484
+ const collector = getMetricCollector();
485
+ if (collector)
486
+ collector.recordModifyFailure();
420
487
  if (attempt < MAX_MODIFY_RETRIES) {
421
488
  // Break out of inner loop, triggering a retry in outer loop
422
489
  modified = existing; // reset
@@ -431,6 +498,18 @@ export async function modifyFile(workspaceRoot, filePath, edits) {
431
498
  }
432
499
  modified = applyMatch(modified, match, edit.replaceContent);
433
500
  strategies.push(`Edit #${i + 1}: ${match.strategy}`);
501
+ const collector = getMetricCollector();
502
+ if (collector) {
503
+ if (match.strategy.startsWith('Exact Match')) {
504
+ collector.recordMatchTier('exact');
505
+ }
506
+ else if (match.strategy.startsWith('Levenshtein')) {
507
+ collector.recordMatchTier('levenshtein');
508
+ }
509
+ else {
510
+ collector.recordMatchTier('normalized');
511
+ }
512
+ }
434
513
  }
435
514
  // If we broke out early for a retry, the modified string will equal the existing string
436
515
  // (or we haven't completed all edits), so we continue to the next attempt.
@@ -528,7 +607,9 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
528
607
  // Truncate command output to prevent memory blowout from massive build logs
529
608
  const MAX_CMD_OUTPUT = 15_000;
530
609
  if (output.length > MAX_CMD_OUTPUT) {
531
- output = output.substring(0, MAX_CMD_OUTPUT) + `\n\n... (Output truncated: ${output.length} bytes exceeded 15KB limit. To view the rest, pipe the command to a file and read it in chunks, or use grep.)`;
610
+ output =
611
+ output.substring(0, MAX_CMD_OUTPUT) +
612
+ `\n\n... (Output truncated: ${output.length} bytes exceeded 15KB limit. To view the rest, pipe the command to a file and read it in chunks, or use grep.)`;
532
613
  }
533
614
  return { output: output || '(command produced no output)' };
534
615
  }
@@ -537,7 +618,8 @@ export async function runCommand(workspaceRoot, command, abortSignal) {
537
618
  // Also truncate error output
538
619
  const MAX_ERR_OUTPUT = 15_000;
539
620
  const truncatedMsg = message.length > MAX_ERR_OUTPUT
540
- ? message.substring(0, MAX_ERR_OUTPUT) + `\n\n... (Error output truncated: ${message.length} bytes exceeded 15KB limit. Pipe to a file if you need full logs.)`
621
+ ? message.substring(0, MAX_ERR_OUTPUT) +
622
+ `\n\n... (Error output truncated: ${message.length} bytes exceeded 15KB limit. Pipe to a file if you need full logs.)`
541
623
  : message;
542
624
  return { output: '', error: `Command failed: ${truncatedMsg}` };
543
625
  }
@@ -574,7 +656,9 @@ export async function grepSearch(workspaceRoot, pattern, fileGlob, abortSignal)
574
656
  }
575
657
  // Node.js fallback for Windows
576
658
  const regexPattern = new RegExp(pattern, 'gi'); // Emulate grep -i and global match
577
- const globPattern = fileGlob ? new RegExp('^' + fileGlob.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$', 'i') : null;
659
+ const globPattern = fileGlob
660
+ ? new RegExp('^' + fileGlob.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$', 'i')
661
+ : null;
578
662
  const results = [];
579
663
  async function walk(dir) {
580
664
  if (results.length >= 50)
@@ -811,34 +895,188 @@ export async function runDebugScript(workspaceRoot, language, code, abortSignal)
811
895
  }
812
896
  }
813
897
  /**
814
- * Dispatches a function call from the model to the appropriate local tool.
815
- * Returns the tool result as a string to feed back to the model.
898
+ * Resolves `@alias/` prefixed paths in tool arguments to the correct workspace root
899
+ * and relative path. Returns the effective workspaceRoot and the cleaned arguments.
900
+ */
901
+ function resolveWorkspaceArgs(primaryRoot, toolName, args) {
902
+ // Only resolve if workspaces are registered
903
+ if (!workspaceRegistry.hasWorkspaces()) {
904
+ return { effectiveRoot: primaryRoot, resolvedArgs: args };
905
+ }
906
+ const resolvedArgs = { ...args };
907
+ // Map of tool-name -> argument keys that hold file/dir paths
908
+ const pathArgKeys = {
909
+ read_file: ['filePath'],
910
+ write_file: ['filePath'],
911
+ delete_file: ['filePath'],
912
+ modify_file: ['filePath'],
913
+ list_directory: ['dirPath'],
914
+ find_dependencies: ['filePath'],
915
+ find_recent_changes: ['dirPath'],
916
+ rename_file: ['sourcePath', 'targetPath'],
917
+ };
918
+ const keys = pathArgKeys[toolName];
919
+ if (!keys) {
920
+ return { effectiveRoot: primaryRoot, resolvedArgs };
921
+ }
922
+ let effectiveRoot = primaryRoot;
923
+ for (const key of keys) {
924
+ const val = resolvedArgs[key];
925
+ if (typeof val !== 'string' || !val.startsWith('@'))
926
+ continue;
927
+ // Special case: "@all" for list_directory — handled separately in executeTool
928
+ if (val === '@all')
929
+ continue;
930
+ const resolved = resolveAndValidateMultiWorkspacePath(primaryRoot, val);
931
+ effectiveRoot = resolved.workspaceRoot;
932
+ resolvedArgs[key] = resolved.relativePath;
933
+ }
934
+ return { effectiveRoot, resolvedArgs };
935
+ }
936
+ /**
937
+ * Runs grep across all registered workspaces (primary + external) and merges results.
938
+ * Results from external workspaces are prefixed with @alias/ for disambiguation.
816
939
  */
940
+ async function crossWorkspaceGrep(primaryRoot, pattern, fileGlob, abortSignal) {
941
+ const allRoots = workspaceRegistry.getAllRoots(primaryRoot);
942
+ const allResults = [];
943
+ for (const { alias, root } of allRoots) {
944
+ const result = await grepSearch(root, pattern, fileGlob, abortSignal);
945
+ if (result.error)
946
+ continue;
947
+ // Extract the raw text from the XML wrapper
948
+ const cdataMatch = result.output.match(/<!\[CDATA\[\n([\s\S]*?)\n\]\]>/);
949
+ const rawText = cdataMatch ? cdataMatch[1] : result.output;
950
+ if (rawText.startsWith('No matches found'))
951
+ continue;
952
+ // Prefix each result line with the workspace alias for disambiguation
953
+ const prefix = alias ? `@${alias}/` : '';
954
+ const lines = rawText.split('\n').filter((l) => l.trim());
955
+ for (const line of lines) {
956
+ if (line.startsWith('...')) {
957
+ allResults.push(line);
958
+ }
959
+ else {
960
+ // Lines are in format "./path:lineNum:content" — prepend alias prefix
961
+ allResults.push(prefix ? line.replace(/^\.?\//, `@${alias}/`) : line);
962
+ }
963
+ }
964
+ }
965
+ if (allResults.length === 0) {
966
+ return { output: `No matches found for "${pattern}" across all workspaces.` };
967
+ }
968
+ const limited = allResults.slice(0, 80);
969
+ const resultText = limited.join('\n') +
970
+ (allResults.length > 80 ? `\n\n... (${allResults.length - 80} more results truncated)` : '');
971
+ const wrappedResult = `<workspace_file path="grep_search_results">\n<content_data><![CDATA[\n${sanitizeForCDATA(resultText)}\n]]></content_data>\n</workspace_file>`;
972
+ return { output: wrappedResult };
973
+ }
817
974
  export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
975
+ // ─── Multi-Workspace Path Resolution ─────────────────────────────
976
+ // Intercept @alias/ prefixed paths and swap workspaceRoot + relative path
977
+ // before dispatching to the underlying tool functions (which remain unchanged).
978
+ const { effectiveRoot, resolvedArgs } = resolveWorkspaceArgs(workspaceRoot, toolName, args);
979
+ let result;
818
980
  switch (toolName) {
819
981
  case 'read_file':
820
- return readFile(workspaceRoot, args.filePath, args.startLine, args.endLine, args.targetElements);
982
+ result = await readFile(effectiveRoot, resolvedArgs.filePath, resolvedArgs.startLine, resolvedArgs.endLine, resolvedArgs.targetElements);
983
+ break;
821
984
  case 'write_file':
822
- return writeFile(workspaceRoot, args.filePath, args.content);
985
+ result = await writeFile(effectiveRoot, resolvedArgs.filePath, resolvedArgs.content);
986
+ break;
823
987
  case 'delete_file':
824
- return deleteFile(workspaceRoot, args.filePath);
988
+ result = await deleteFile(effectiveRoot, resolvedArgs.filePath);
989
+ break;
825
990
  case 'rename_file':
826
- return renameFile(workspaceRoot, args.sourcePath, args.targetPath);
991
+ result = await renameFile(effectiveRoot, resolvedArgs.sourcePath, resolvedArgs.targetPath);
992
+ break;
827
993
  case 'modify_file':
828
- return modifyFile(workspaceRoot, args.filePath, args.edits);
829
- case 'list_directory':
830
- return listDirectory(workspaceRoot, args.dirPath, args.maxDepth ?? 3);
994
+ result = await modifyFile(effectiveRoot, resolvedArgs.filePath, resolvedArgs.edits);
995
+ break;
996
+ case 'list_directory': {
997
+ const dirPath = resolvedArgs.dirPath;
998
+ // Handle @all — list all registered workspace roots
999
+ if (dirPath === '@all') {
1000
+ const allRoots = workspaceRegistry.getAllRoots(workspaceRoot);
1001
+ const sections = [];
1002
+ for (const { alias, root } of allRoots) {
1003
+ const label = alias ? `@${alias}` : '(primary)';
1004
+ const subResult = await listDirectory(root, '.', 2);
1005
+ sections.push(`=== Workspace: ${label} [${root}] ===\n${subResult.output}`);
1006
+ }
1007
+ result = { output: sections.join('\n\n') };
1008
+ }
1009
+ else {
1010
+ result = await listDirectory(effectiveRoot, dirPath, resolvedArgs.maxDepth ?? 3);
1011
+ }
1012
+ break;
1013
+ }
831
1014
  case 'run_command':
832
- return runCommand(workspaceRoot, args.command, abortSignal);
1015
+ result = await runCommand(effectiveRoot, resolvedArgs.command, abortSignal);
1016
+ break;
833
1017
  case 'run_debug_script':
834
- return runDebugScript(workspaceRoot, args.language, args.code, abortSignal);
835
- case 'grep_search':
836
- return grepSearch(workspaceRoot, args.pattern, args.fileGlob, abortSignal);
1018
+ result = await runDebugScript(effectiveRoot, resolvedArgs.language, resolvedArgs.code, abortSignal);
1019
+ break;
1020
+ case 'grep_search': {
1021
+ const wsParam = resolvedArgs.workspace;
1022
+ if (wsParam === 'all') {
1023
+ // Cross-workspace search across all registered workspaces
1024
+ result = await crossWorkspaceGrep(workspaceRoot, resolvedArgs.pattern, resolvedArgs.fileGlob, abortSignal);
1025
+ }
1026
+ else if (wsParam && workspaceRegistry.get(wsParam)) {
1027
+ // Search in a specific external workspace
1028
+ const ws = workspaceRegistry.get(wsParam);
1029
+ result = await grepSearch(ws.absolutePath, resolvedArgs.pattern, resolvedArgs.fileGlob, abortSignal);
1030
+ }
1031
+ else {
1032
+ result = await grepSearch(effectiveRoot, resolvedArgs.pattern, resolvedArgs.fileGlob, abortSignal);
1033
+ }
1034
+ break;
1035
+ }
1036
+ case 'semantic_search': {
1037
+ const query = args.query;
1038
+ const topK = args.topK || 5;
1039
+ let output = '';
1040
+ try {
1041
+ const { getEmbeddingIndex } = await import('./embeddingIndex.js');
1042
+ const index = getEmbeddingIndex();
1043
+ if (!index.isReady()) {
1044
+ const loaded = await index.load(workspaceRoot);
1045
+ if (!loaded) {
1046
+ await index.buildIndex(workspaceRoot);
1047
+ await index.save(workspaceRoot);
1048
+ }
1049
+ }
1050
+ const results = await index.search(query, topK);
1051
+ if (results.length === 0) {
1052
+ output = 'No semantically similar code found. (Index might be empty or embedding failed)';
1053
+ }
1054
+ else {
1055
+ output = results
1056
+ .map((r) => `[Score: ${r.score.toFixed(3)}] ${r.filePath}:${r.startLine}-${r.endLine}\n${r.preview}`)
1057
+ .join('\n---\n');
1058
+ }
1059
+ }
1060
+ catch (e) {
1061
+ output = `Semantic search failed: ${e.message}`;
1062
+ }
1063
+ result = { output };
1064
+ break;
1065
+ }
837
1066
  case 'find_dependencies':
838
- return traceDependencies(workspaceRoot, args.filePath, args.direction, args.maxDepth);
1067
+ result = await traceDependencies(effectiveRoot, resolvedArgs.filePath, resolvedArgs.direction, resolvedArgs.maxDepth);
1068
+ break;
839
1069
  case 'find_recent_changes':
840
- return findRecentChanges(workspaceRoot, args.dirPath, args.minutes, args.maxDepth);
1070
+ result = await findRecentChanges(effectiveRoot, resolvedArgs.dirPath, resolvedArgs.minutes, resolvedArgs.maxDepth);
1071
+ break;
841
1072
  default:
842
- return { output: '', error: `Unknown tool: "${toolName}"` };
1073
+ result = { output: '', error: `Unknown tool: "${toolName}"` };
1074
+ break;
1075
+ }
1076
+ if (result.error) {
1077
+ const collector = getMetricCollector();
1078
+ if (collector)
1079
+ collector.recordToolFailure(toolName);
843
1080
  }
1081
+ return result;
844
1082
  }
@@ -15,6 +15,7 @@
15
15
  * 5. **Self-Correction Pipeline**: Compares modified files against TS/Linter diagnostics,
16
16
  * providing feedback on syntax or semantic failures back to the AI for self-healing.
17
17
  */
18
+ import { AsyncInputHandler } from './agent/inputHandler.js';
18
19
  export { AsyncInputHandler } from './agent/inputHandler.js';
19
20
  /**
20
21
  * Starts and orchestrates the primary interactive command-line interface (REPL) loop.
@@ -48,3 +49,10 @@ export { AsyncInputHandler } from './agent/inputHandler.js';
48
49
  * @param version - The Semantic Version string of the active tool distribution.
49
50
  */
50
51
  export declare function startAgentLoop(workspaceRoot: string, version: string): Promise<void>;
52
+ export declare function executeSingleTurn(workspaceRoot: string, userInput: string, chat: any, inputHandler: AsyncInputHandler, chatSessionState: {
53
+ id: string;
54
+ title: string;
55
+ }, isPlanMode: boolean, cachedContextResult?: any): Promise<{
56
+ planModeReturn?: string;
57
+ contextResult?: any;
58
+ } | void>;