minovative-mind-cli 2.0.0 → 2.1.1

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.
@@ -5,6 +5,20 @@ import { getAuthorizedIdToken } from './auth.js';
5
5
  import { debugLog } from '../utils/logger.js';
6
6
  import { GENERAL_CHAT_INSTRUCTION, PLAN_EXECUTION_INSTRUCTION, PLAN_MODE_INSTRUCTION, CONTEXT_SYSTEM_INSTRUCTION, INTENT_ROUTER_SYSTEM_INSTRUCTION, WEB_SEARCH_SYSTEM_INSTRUCTION, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
7
7
  import { getMetricCollector } from './metrics.js';
8
+ import { workspaceRegistry } from './workspaceRegistry.js';
9
+ function getMultiWorkspaceBlock() {
10
+ const summary = workspaceRegistry.buildPromptSummary();
11
+ if (!summary)
12
+ return '';
13
+ return `<multi_workspace>
14
+ The user has registered external workspaces that you can access using the @alias/ prefix:
15
+ ${summary}
16
+
17
+ To read, modify, or search files in an external workspace, prefix the file path with the alias (e.g., "@backend/src/routes.ts"). To search across ALL workspaces, use grep_search with workspace="all".
18
+
19
+ When the user asks to "transfer", "sync", or "port" features between projects, read the source files from one workspace and apply the changes to the target.
20
+ </multi_workspace>`;
21
+ }
8
22
  // ─── Model Overrides (for Regression Testing) ────────────────────────
9
23
  let contextModelOverride = null;
10
24
  let contextTempOverride = null;
@@ -261,7 +275,7 @@ export function getGeneralChatConfig() {
261
275
  }
262
276
  export function getPlanExecutionConfig() {
263
277
  return {
264
- systemInstruction: PLAN_EXECUTION_INSTRUCTION,
278
+ systemInstruction: PLAN_EXECUTION_INSTRUCTION.replace('{{MULTI_WORKSPACE_BLOCK}}', getMultiWorkspaceBlock()),
265
279
  tools: [{ functionDeclarations: getToolDeclarations() }],
266
280
  };
267
281
  }
@@ -505,7 +519,7 @@ export function createContextAgentSession() {
505
519
  let model = contextModelOverride || getGlobalActiveModel();
506
520
  if (model === 'auto')
507
521
  model = GEMINI_MODELS.FLASH_3_5;
508
- return new ProxyChatSession(model, CONTEXT_SYSTEM_INSTRUCTION, contextTools, {
522
+ return new ProxyChatSession(model, CONTEXT_SYSTEM_INSTRUCTION.replace('{{MULTI_WORKSPACE_BLOCK}}', getMultiWorkspaceBlock()), contextTools, {
509
523
  maxOutputTokens: MAX_OUTPUT_TOKENS,
510
524
  temperature: contextTempOverride !== null ? contextTempOverride : 1,
511
525
  topP: 0.95,
@@ -4,7 +4,7 @@ import pc from 'picocolors';
4
4
  import { createContextAgentSession, createIntentRouterSession, createWebSearchAgentSession, createExecutionComplexitySession, } from './ai.js';
5
5
  import { evaluateInvestigationComplexity } from './investigationComplexity.js';
6
6
  import { InvestigationOrchestrator } from './orchestration/investigationOrchestrator.js';
7
- import { isSubAgentsEnabled, listDirectory, grepSearch, readFile, traceDependencies, findRecentChanges } from './agent-tools.js';
7
+ import { isSubAgentsEnabled, executeTool } from './agent-tools.js';
8
8
  import { debugLog } from '../utils/logger.js';
9
9
  import { buildDependencyGraph } from '../utils/dependencyTracer.js';
10
10
  import { runEphemeralScript } from '../utils/analysisRunner.js';
@@ -192,12 +192,24 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
192
192
  if (!needsContext) {
193
193
  return { contextResult: null, targetAgent, chainedMessages: [] };
194
194
  }
195
- const projectTreeResult = await listDirectory(workspaceRoot, '.', 10);
196
- let projectTree = projectTreeResult.output;
197
- if (projectTree.length > 30000) {
198
- projectTree = projectTree.substring(0, 30000) + '\n... (Project tree truncated due to size)';
195
+ const { workspaceRegistry } = await import('./workspaceRegistry.js');
196
+ const allRoots = workspaceRegistry.getAllRoots(workspaceRoot);
197
+ let projectTree = '';
198
+ let primaryProjectType = 'Unknown';
199
+ for (const { alias, root } of allRoots) {
200
+ const label = alias ? `@${alias} (${root})` : `Primary Workspace (${root})`;
201
+ const treeResult = await executeTool(root, 'list_directory', { dirPath: '.', maxDepth: 10 });
202
+ let tree = treeResult.output;
203
+ if (tree.length > 30000) {
204
+ tree = tree.substring(0, 30000) + '\\n... (Project tree truncated due to size)';
205
+ }
206
+ const type = await detectProjectType(root);
207
+ if (!alias) {
208
+ primaryProjectType = type;
209
+ }
210
+ projectTree += `=== ${label} ===\\nProject Type: ${type}\\n${tree}\\n\\n`;
199
211
  }
200
- const projectType = await detectProjectType(workspaceRoot);
212
+ const projectType = primaryProjectType;
201
213
  // ─── Parallel Investigation Gate ─────────────────────────────
202
214
  if (isSubAgentsEnabled()) {
203
215
  // Determine complexity and domain breakdown
@@ -316,7 +328,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
316
328
  debugLog(`Context Agent finished. Selected files: ${JSON.stringify(filesToRead)}`);
317
329
  for (const filePath of filesToRead) {
318
330
  if (!relevantFiles.has(filePath)) {
319
- const readResult = await readFile(workspaceRoot, filePath);
331
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
320
332
  if (!readResult.error) {
321
333
  relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
322
334
  }
@@ -330,15 +342,23 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
330
342
  // Execution Agent won't break imports when modifying/deleting/renaming.
331
343
  const MAX_TOTAL_FILES = 15;
332
344
  try {
333
- const graph = await buildDependencyGraph(workspaceRoot);
345
+ const { resolveAndValidateMultiWorkspacePath } = await import('../utils/pathSecurity.js');
334
346
  const autoDiscovered = new Set();
335
347
  for (const filePath of filesToRead) {
336
- const reverseDeps = graph.getImportedBy(filePath);
337
- for (const dep of reverseDeps) {
338
- if (!filesToRead.includes(dep) && !autoDiscovered.has(dep)) {
339
- autoDiscovered.add(dep);
348
+ try {
349
+ const resolved = resolveAndValidateMultiWorkspacePath(workspaceRoot, filePath);
350
+ const graph = await buildDependencyGraph(resolved.workspaceRoot);
351
+ const reverseDeps = graph.getImportedBy(resolved.relativePath);
352
+ for (const dep of reverseDeps) {
353
+ const aliasedDep = resolved.alias ? `@${resolved.alias}/${dep}` : dep;
354
+ if (!filesToRead.includes(aliasedDep) && !autoDiscovered.has(aliasedDep)) {
355
+ autoDiscovered.add(aliasedDep);
356
+ }
340
357
  }
341
358
  }
359
+ catch (e) {
360
+ // Ignore path resolution errors for trace dependencies
361
+ }
342
362
  }
343
363
  // Merge auto-discovered dependents, respecting the file cap
344
364
  const remaining = MAX_TOTAL_FILES - relevantFiles.size;
@@ -347,7 +367,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
347
367
  if (added >= remaining)
348
368
  break;
349
369
  if (!relevantFiles.has(dep)) {
350
- const readResult = await readFile(workspaceRoot, dep);
370
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath: dep });
351
371
  if (!readResult.error) {
352
372
  relevantFiles.set(dep, { text: readResult.output, inlineData: readResult.inlineData });
353
373
  added++;
@@ -377,7 +397,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
377
397
  const filesToRead = args.files || [];
378
398
  let output = '';
379
399
  for (const filePath of filesToRead) {
380
- const readResult = await readFile(workspaceRoot, filePath);
400
+ const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
381
401
  if (!readResult.error) {
382
402
  relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
383
403
  output += `\n--- File: ${filePath} ---\n${readResult.output}\n`;
@@ -394,7 +414,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
394
414
  });
395
415
  }
396
416
  else if (call.name === 'list_directory') {
397
- const listRes = await listDirectory(workspaceRoot, args.dirPath, args.maxDepth || 1);
417
+ const listRes = await executeTool(workspaceRoot, 'list_directory', args);
398
418
  functionResponses.push({
399
419
  functionResponse: {
400
420
  name: call.name,
@@ -406,7 +426,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
406
426
  });
407
427
  }
408
428
  else if (call.name === 'search_codebase') {
409
- const grepRes = await grepSearch(workspaceRoot, args.pattern, args.fileGlob);
429
+ const grepRes = await executeTool(workspaceRoot, 'grep_search', { ...args, workspace: 'all' });
410
430
  functionResponses.push({
411
431
  functionResponse: {
412
432
  name: call.name,
@@ -415,44 +435,16 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
415
435
  });
416
436
  }
417
437
  else if (call.name === 'semantic_search') {
418
- const query = args.query;
419
- const topK = args.topK || 5;
420
- let output = '';
421
- try {
422
- const { getEmbeddingIndex } = await import('./embeddingIndex.js');
423
- const index = getEmbeddingIndex();
424
- if (!index.isReady()) {
425
- // Lazy load from disk or build if missing
426
- const loaded = await index.load(workspaceRoot);
427
- if (!loaded) {
428
- if (onProgress)
429
- onProgress('Building semantic search index (first run)...');
430
- await index.buildIndex(workspaceRoot, onProgress);
431
- await index.save(workspaceRoot);
432
- }
433
- }
434
- const results = await index.search(query, topK);
435
- if (results.length === 0) {
436
- output = 'No semantically similar code found. (Index might be empty or embedding failed)';
437
- }
438
- else {
439
- output = results
440
- .map((r) => `[Score: ${r.score.toFixed(3)}] ${r.filePath}:${r.startLine}-${r.endLine}\n${r.preview}`)
441
- .join('\n---\n');
442
- }
443
- }
444
- catch (e) {
445
- output = `Semantic search failed: ${e.message}`;
446
- }
438
+ const semRes = await executeTool(workspaceRoot, 'semantic_search', args);
447
439
  functionResponses.push({
448
440
  functionResponse: {
449
441
  name: call.name,
450
- response: { output },
442
+ response: { output: semRes.output },
451
443
  },
452
444
  });
453
445
  }
454
446
  else if (call.name === 'read_file') {
455
- const readRes = await readFile(workspaceRoot, args.filePath, args.startLine, args.endLine, args.targetElements);
447
+ const readRes = await executeTool(workspaceRoot, 'read_file', args);
456
448
  if (!readRes.error) {
457
449
  relevantFiles.set(args.filePath, { text: readRes.output, inlineData: readRes.inlineData });
458
450
  }
@@ -494,7 +486,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
494
486
  }
495
487
  }
496
488
  else if (call.name === 'find_dependencies') {
497
- const depResult = await traceDependencies(workspaceRoot, args.filePath, args.direction, args.maxDepth);
489
+ const depResult = await executeTool(workspaceRoot, 'find_dependencies', args);
498
490
  functionResponses.push({
499
491
  functionResponse: {
500
492
  name: call.name,
@@ -503,7 +495,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
503
495
  });
504
496
  }
505
497
  else if (call.name === 'find_recent_changes') {
506
- const recentRes = await findRecentChanges(workspaceRoot, args.dirPath, args.minutes, args.maxDepth);
498
+ const recentRes = await executeTool(workspaceRoot, 'find_recent_changes', args);
507
499
  functionResponses.push({
508
500
  functionResponse: {
509
501
  name: call.name,
@@ -11,7 +11,7 @@
11
11
  * components" + "Auth UI styling") and produces an `InvestigationResult` containing
12
12
  * the files it found and a summary of its findings.
13
13
  */
14
- import { ProxyChatSession, getContextToolDeclarations } from '../ai.js';
14
+ import { ProxyChatSession, getContextToolDeclarations, getGlobalActiveModel } from '../ai.js';
15
15
  import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
16
16
  import { CONTEXT_SYSTEM_INSTRUCTION } from '../../utils/systemPrompts.js';
17
17
  import { debugLog } from '../../utils/logger.js';
@@ -47,7 +47,10 @@ export class InvestigationAgentRunner {
47
47
  this.readCache = readCache;
48
48
  this.projectTree = projectTree;
49
49
  this.projectType = projectType;
50
- this.chat = new ProxyChatSession(GEMINI_MODELS.FLASH_3_5, this.buildSystemInstruction(), getContextToolDeclarations(), {
50
+ let model = getGlobalActiveModel();
51
+ if (model === GEMINI_MODELS.AUTO)
52
+ model = GEMINI_MODELS.FLASH_3_5;
53
+ this.chat = new ProxyChatSession(model, this.buildSystemInstruction(), getContextToolDeclarations(), {
51
54
  maxOutputTokens: MAX_OUTPUT_TOKENS,
52
55
  temperature: 1,
53
56
  topP: 0.95,
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import * as p from '@clack/prompts';
12
12
  import pc from 'picocolors';
13
- import { ProxyChatSession } from '../ai.js';
13
+ import { ProxyChatSession, getGlobalActiveModel } from '../ai.js';
14
14
  import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
15
15
  import { debugLog } from '../../utils/logger.js';
16
16
  import { MessageBus } from './messageBus.js';
@@ -61,7 +61,10 @@ export class Orchestrator {
61
61
  this.inputHandler = inputHandler;
62
62
  this.bus = new MessageBus(workspaceRoot, conversationId);
63
63
  this.locks = new FileLockRegistry();
64
- this.pmChat = new ProxyChatSession(GEMINI_MODELS.FLASH_3_5, PM_SYSTEM_INSTRUCTION, [], {
64
+ let model = getGlobalActiveModel();
65
+ if (model === GEMINI_MODELS.AUTO)
66
+ model = GEMINI_MODELS.FLASH_3_5;
67
+ this.pmChat = new ProxyChatSession(model, PM_SYSTEM_INSTRUCTION, [], {
65
68
  maxOutputTokens: MAX_OUTPUT_TOKENS,
66
69
  temperature: 0.1,
67
70
  responseMimeType: 'application/json',
@@ -4,7 +4,7 @@
4
4
  * Implements the lifecycle, health monitoring, and tool-loop execution for a single
5
5
  * parallelized sub-agent.
6
6
  */
7
- import { ProxyChatSession } from '../ai.js';
7
+ import { ProxyChatSession, getGlobalActiveModel } from '../ai.js';
8
8
  import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
9
9
  import { executeScopedTool, getScopedToolDeclarations } from './scopedTools.js';
10
10
  import { debugLog } from '../../utils/logger.js';
@@ -32,8 +32,11 @@ export class SubAgentRunner {
32
32
  this.bus = bus;
33
33
  this.locks = locks;
34
34
  this.globalContext = globalContext;
35
+ let model = getGlobalActiveModel();
36
+ if (model === GEMINI_MODELS.AUTO)
37
+ model = GEMINI_MODELS.FLASH_3_5;
35
38
  // Sub-agents default to flash-3.5 for better reasoning capabilities
36
- this.chat = new ProxyChatSession(GEMINI_MODELS.FLASH_3_5, this.buildSystemInstruction(), [{ functionDeclarations: getScopedToolDeclarations() }], {
39
+ this.chat = new ProxyChatSession(model, this.buildSystemInstruction(), [{ functionDeclarations: getScopedToolDeclarations() }], {
37
40
  maxOutputTokens: MAX_OUTPUT_TOKENS,
38
41
  temperature: 0.3, // Lower temperature for more focused execution
39
42
  topP: 0.95,
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Represents a single registered external workspace entry in the global registry.
3
+ */
4
+ export interface RegisteredWorkspace {
5
+ /** User-chosen short alias (e.g., "backend", "shared-lib"). Used as the `@alias/` prefix. */
6
+ alias: string;
7
+ /** Validated, normalized absolute path on disk. */
8
+ absolutePath: string;
9
+ /** Unix epoch timestamp (ms) when this workspace was registered. */
10
+ registeredAt: number;
11
+ }
12
+ /**
13
+ * Result of resolving an `@alias/relative/path` string against the workspace registry.
14
+ */
15
+ export interface ResolvedWorkspacePath {
16
+ /** The alias that was matched (e.g., "backend"). */
17
+ alias: string;
18
+ /** The absolute root directory of the matched registered workspace. */
19
+ workspaceRoot: string;
20
+ /** The relative path within that workspace (e.g., "src/routes.ts"). */
21
+ relativePath: string;
22
+ /** The fully resolved absolute path to the target file or directory. */
23
+ absolutePath: string;
24
+ }
25
+ /**
26
+ * Service that manages a global registry of external workspace roots.
27
+ *
28
+ * Workspaces are registered with short aliases (e.g., "backend") and referenced
29
+ * in file paths using the `@alias/path` prefix syntax. The registry is persisted
30
+ * globally at `~/.minovative-mind-cli/workspaces.json` so registrations carry
31
+ * across different primary workspaces.
32
+ *
33
+ * @remarks
34
+ * This is a singleton — use the exported `workspaceRegistry` instance.
35
+ * All filesystem access goes through this service to maintain the security
36
+ * boundary: the agent can only touch the primary workspace and explicitly
37
+ * registered secondary workspaces.
38
+ */
39
+ declare class WorkspaceRegistry {
40
+ /** In-memory map of alias → registered workspace. */
41
+ private workspaces;
42
+ /** Whether the registry has been loaded from disk. */
43
+ private initialized;
44
+ /**
45
+ * Initializes the registry by loading persisted workspace entries from disk.
46
+ * Safe to call multiple times — subsequent calls are no-ops.
47
+ */
48
+ init(): void;
49
+ /**
50
+ * Registers a new external workspace root with the given alias.
51
+ *
52
+ * @param alias - Short identifier for the workspace (e.g., "backend").
53
+ * Must be lowercase alphanumeric with hyphens/underscores, 1–30 chars.
54
+ * @param absolutePath - Absolute path to the workspace root directory.
55
+ * Must exist on disk and be a directory.
56
+ * @returns The created `RegisteredWorkspace`, or throws on validation failure.
57
+ * @throws Error if the alias is invalid, the path doesn't exist, or the alias is already taken.
58
+ */
59
+ register(alias: string, absolutePath: string): RegisteredWorkspace;
60
+ /**
61
+ * Removes a registered workspace by alias.
62
+ *
63
+ * @param alias - The alias to unregister.
64
+ * @returns `true` if the workspace was found and removed, `false` otherwise.
65
+ */
66
+ unregister(alias: string): boolean;
67
+ /**
68
+ * Returns all registered workspaces as an array, sorted by alias.
69
+ */
70
+ list(): RegisteredWorkspace[];
71
+ /**
72
+ * Returns the number of registered workspaces.
73
+ */
74
+ get size(): number;
75
+ /**
76
+ * Checks whether any external workspaces are registered.
77
+ */
78
+ hasWorkspaces(): boolean;
79
+ /**
80
+ * Looks up a workspace by alias.
81
+ *
82
+ * @param alias - The alias to find (without the `@` prefix).
83
+ * @returns The registered workspace, or `undefined` if not found.
84
+ */
85
+ get(alias: string): RegisteredWorkspace | undefined;
86
+ /**
87
+ * Returns all workspace roots (primary + registered secondaries).
88
+ *
89
+ * @param primaryRoot - The primary workspace root (from `process.cwd()`).
90
+ * @returns An array of `{ alias: string | null, root: string }` entries.
91
+ * The primary workspace has `alias: null`.
92
+ */
93
+ getAllRoots(primaryRoot: string): Array<{
94
+ alias: string | null;
95
+ root: string;
96
+ }>;
97
+ /**
98
+ * Resolves an `@alias/relative/path` string into its constituent parts.
99
+ *
100
+ * @param filePath - A file path that may or may not start with `@alias/`.
101
+ * @returns A `ResolvedWorkspacePath` if the path has a valid `@alias/` prefix
102
+ * and the alias is registered, or `null` if the path is a standard
103
+ * workspace-relative path (no `@` prefix or unrecognized alias).
104
+ * @throws Error if the path has an `@` prefix but the alias is not registered.
105
+ */
106
+ resolve(filePath: string): ResolvedWorkspacePath | null;
107
+ /**
108
+ * Checks if a given file path uses the `@alias/` prefix syntax.
109
+ *
110
+ * @param filePath - The path to check.
111
+ * @returns `true` if the path starts with `@`.
112
+ */
113
+ isAliasedPath(filePath: string): boolean;
114
+ /**
115
+ * Builds a formatted summary of all registered workspaces for system prompt injection.
116
+ *
117
+ * @returns A human-readable string listing all aliases and their paths,
118
+ * or an empty string if no workspaces are registered.
119
+ */
120
+ buildPromptSummary(): string;
121
+ /**
122
+ * Loads the workspace registry from the global config file.
123
+ * Silently handles missing or corrupted files.
124
+ */
125
+ private loadFromDisk;
126
+ /**
127
+ * Persists the current workspace registry to the global config file.
128
+ * Creates the config directory if it doesn't exist.
129
+ */
130
+ private saveToDisk;
131
+ }
132
+ /**
133
+ * Singleton instance of the WorkspaceRegistry service.
134
+ * Initialize with `workspaceRegistry.init()` during CLI startup.
135
+ */
136
+ export declare const workspaceRegistry: WorkspaceRegistry;
137
+ export {};