minovative-mind-cli 2.11.3 → 2.11.5

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.
@@ -1,14 +1,34 @@
1
1
  import path from 'path';
2
2
  import { executeTool, getToolDeclarations as getBaseToolDeclarations } from '../agent-tools.js';
3
3
  import { MessageBus } from './messageBus.js';
4
+ import { resolveAndValidateMultiWorkspacePath } from '../../utils/pathSecurity.js';
4
5
  import { debugLog } from '../../utils/logger.js';
5
6
  /**
6
- * Extended tool declarations for sub-agents, merging the standard tools
7
- * with orchestration-specific ones (e.g., post_message, read_messages).
7
+ * Resolves a file path to its canonical absolute path for lock registry keying,
8
+ * accounting for multi-workspace alias resolution and default primary sub-path auto-focusing.
9
+ *
10
+ * @param workspaceRoot - The base primary workspace root directory
11
+ * @param filePath - The relative or aliased file path
12
+ * @param options - Optional sub-path override configuration
13
+ * @returns Absolute canonical file path
14
+ */
15
+ export function resolveCanonicalLockPath(workspaceRoot, filePath, options) {
16
+ try {
17
+ const resolved = resolveAndValidateMultiWorkspacePath(workspaceRoot, filePath, options);
18
+ return resolved.absolutePath;
19
+ }
20
+ catch {
21
+ return path.isAbsolute(filePath) ? filePath : path.resolve(workspaceRoot, filePath);
22
+ }
23
+ }
24
+ /**
25
+ * Returns Gemini tool declarations available to sub-agents during orchestration.
26
+ * In addition to standard agent tools (read/write/search/command/etc.), includes
27
+ * inter-agent communication tools (post_message, read_messages).
8
28
  */
9
29
  export function getScopedToolDeclarations() {
10
- return [
11
- ...getBaseToolDeclarations({ isExecutionAgent: true }),
30
+ const baseTools = getBaseToolDeclarations({ isExecutionAgent: true });
31
+ const orchestrationTools = [
12
32
  {
13
33
  name: 'post_message',
14
34
  description: 'Post a semantic message to the orchestration bus to coordinate with other agents.',
@@ -23,15 +43,15 @@ export function getScopedToolDeclarations() {
23
43
  type: 'STRING',
24
44
  description: 'The semantic intent or message content',
25
45
  },
26
- toAgent: {
27
- type: 'STRING',
28
- description: 'Optional target agent ID (required for "request")',
29
- },
30
46
  affectedFiles: {
31
47
  type: 'ARRAY',
32
48
  items: { type: 'STRING' },
33
49
  description: 'Files involved (required for "discovery")',
34
50
  },
51
+ toAgent: {
52
+ type: 'STRING',
53
+ description: 'Optional target agent ID (required for "request")',
54
+ },
35
55
  },
36
56
  required: ['type', 'content'],
37
57
  },
@@ -45,16 +65,23 @@ export function getScopedToolDeclarations() {
45
65
  },
46
66
  },
47
67
  ];
68
+ return [...baseTools, ...orchestrationTools];
48
69
  }
49
70
  /**
50
- * Wraps the global executeTool to provide sub-agent context.
71
+ * Executes a tool within the sub-agent execution boundary.
72
+ * Handles concurrency locking for file-mutating operations, sends progress
73
+ * notifications to the orchestrator, and logs activity to the message bus.
51
74
  *
52
- * 1. Captures tool execution into the MessageBus (Layer 1: zero-cost logs).
53
- * 2. Implements FileLocks for write/modify operations to prevent race conditions.
54
- * 3. Handles `post_message` and `read_messages` directly.
55
- * 4. Calls a heartbeat callback to notify the orchestrator this agent is alive.
75
+ * @param name - Tool function name
76
+ * @param args - Tool arguments object
77
+ * @param workspaceRoot - Primary workspace root directory
78
+ * @param agentId - Unique ID of the executing agent
79
+ * @param bus - Shared message bus instance
80
+ * @param locks - Shared file lock registry instance
81
+ * @param onProgress - Callback to notify parent of sub-agent progress
82
+ * @param options - Optional sub-path auto-focus and override configuration
56
83
  */
57
- export async function executeScopedTool(name, args, workspaceRoot, agentId, bus, locks, onProgress) {
84
+ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus, locks, onProgress, options) {
58
85
  // Update heartbeat so the orchestrator knows we are making progress
59
86
  onProgress();
60
87
  const timestamp = Date.now();
@@ -97,10 +124,25 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
97
124
  let lockedFile = null;
98
125
  let diffContext = null;
99
126
  if (name === 'write_file' || name === 'modify_file' || name === 'delete_file') {
100
- lockedFile = args.filePath;
101
- if (lockedFile) {
102
- debugLog(`Agent "${agentId}" requesting lock for "${lockedFile}" (tool: ${name})`);
103
- onProgress(`waiting for lock on ${lockedFile.split('/').pop()}...`);
127
+ const rawPath = args.filePath;
128
+ if (rawPath) {
129
+ lockedFile = resolveCanonicalLockPath(workspaceRoot, rawPath, options);
130
+ debugLog(`Agent "${agentId}" requesting lock for "${lockedFile}" (raw: "${rawPath}", tool: ${name})`);
131
+ onProgress(`waiting for lock on ${path.basename(lockedFile)}...`);
132
+ const lockRes = await locks.acquire(lockedFile, agentId);
133
+ onProgress(`acquired lock, executing...`);
134
+ diffContext = lockRes.previousDiff;
135
+ if (lockRes.forceReleased) {
136
+ debugLog(`Agent "${agentId}" got forced lock on "${lockedFile}" (previous owner stalled)`);
137
+ }
138
+ }
139
+ }
140
+ else if (name === 'rename_file') {
141
+ const rawPath = args.sourcePath;
142
+ if (rawPath) {
143
+ lockedFile = resolveCanonicalLockPath(workspaceRoot, rawPath, options);
144
+ debugLog(`Agent "${agentId}" requesting lock for "${lockedFile}" (raw: "${rawPath}", tool: ${name})`);
145
+ onProgress(`waiting for lock on ${path.basename(lockedFile)}...`);
104
146
  const lockRes = await locks.acquire(lockedFile, agentId);
105
147
  onProgress(`acquired lock, executing...`);
106
148
  diffContext = lockRes.previousDiff;
@@ -1,4 +1,5 @@
1
- import { debugLog } from '../utils/logger.js';
1
+ import { debugLog, isDebugOn } from '../utils/logger.js';
2
+ import { GEMINI_MODELS } from '../utils/config.js';
2
3
  import { getMetricCollector } from './metrics.js';
3
4
  /**
4
5
  * ============================================================================
@@ -98,6 +99,7 @@ export class ProxyClient {
98
99
  const BASE_DELAY_MS = 2000;
99
100
  const MAX_DELAY_MS = 30000;
100
101
  let attempt = 0;
102
+ let activeModel = modelName;
101
103
  retryLoop: while (true) {
102
104
  if (abortSignal?.aborted) {
103
105
  const err = new Error('Operation aborted');
@@ -111,7 +113,7 @@ export class ProxyClient {
111
113
  'X-Firebase-Auth': `Bearer ${idToken}`,
112
114
  },
113
115
  body: JSON.stringify({
114
- model: modelName,
116
+ model: activeModel,
115
117
  contents,
116
118
  tools,
117
119
  toolConfig,
@@ -120,25 +122,39 @@ export class ProxyClient {
120
122
  }),
121
123
  signal: abortSignal,
122
124
  });
123
- debugLog(`Proxy Request to ${modelName} complete. Status: ${response.status} ${response.statusText}`);
124
- if ((response.status === 429 || response.status === 503 || response.status === 502 || response.status === 500 || response.status === 504) && attempt < MAX_RETRIES) {
125
- if (abortSignal?.aborted) {
126
- const err = new Error('Operation aborted');
127
- err.name = 'AbortError';
128
- throw err;
125
+ debugLog(`Proxy Request to ${activeModel} complete. Status: ${response.status} ${response.statusText}`);
126
+ if (response.status === 429 || response.status === 503 || response.status === 502 || response.status === 500 || response.status === 504) {
127
+ if (attempt < MAX_RETRIES) {
128
+ if (abortSignal?.aborted) {
129
+ const err = new Error('Operation aborted');
130
+ err.name = 'AbortError';
131
+ throw err;
132
+ }
133
+ const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
134
+ const delayTime = Math.round(exponentialDelay * (1.0 + Math.random() * 0.5));
135
+ if (isDebugOn()) {
136
+ process.stdout.write('\n');
137
+ console.warn(`Server error or rate limit hit (${response.status}). Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
138
+ }
139
+ await delay(delayTime, abortSignal);
140
+ if (abortSignal?.aborted) {
141
+ const err = new Error('Operation aborted');
142
+ err.name = 'AbortError';
143
+ throw err;
144
+ }
145
+ attempt++;
146
+ continue;
129
147
  }
130
- const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
131
- const delayTime = Math.round(exponentialDelay * (1.0 + Math.random() * 0.5));
132
- process.stdout.write('\n');
133
- console.warn(`Server error or rate limit hit (${response.status}). Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
134
- await delay(delayTime, abortSignal);
135
- if (abortSignal?.aborted) {
136
- const err = new Error('Operation aborted');
137
- err.name = 'AbortError';
138
- throw err;
148
+ else if (activeModel !== GEMINI_MODELS.FLASH_LITE) {
149
+ debugLog(`Rate limit retries exhausted (${MAX_RETRIES}/${MAX_RETRIES}) for ${activeModel}. Automatically falling back to ${GEMINI_MODELS.FLASH_LITE}...`);
150
+ if (isDebugOn()) {
151
+ process.stdout.write('\n');
152
+ console.warn(`Rate limit retries exhausted on ${activeModel}. Automatically switching to Flash-Lite (${GEMINI_MODELS.FLASH_LITE}) to complete request...`);
153
+ }
154
+ activeModel = GEMINI_MODELS.FLASH_LITE;
155
+ attempt = 0;
156
+ continue retryLoop;
139
157
  }
140
- attempt++;
141
- continue;
142
158
  }
143
159
  if (response.status === 401) {
144
160
  let details = '';
@@ -227,8 +243,8 @@ export class ProxyClient {
227
243
  if (data.usage.remainingBalance !== undefined) {
228
244
  globalSessionAccumulatedUsage.remainingBalance = data.usage.remainingBalance;
229
245
  }
230
- globalSessionAccumulatedUsage.modelsUsed[modelName] =
231
- (globalSessionAccumulatedUsage.modelsUsed[modelName] || 0) + 1;
246
+ globalSessionAccumulatedUsage.modelsUsed[activeModel] =
247
+ (globalSessionAccumulatedUsage.modelsUsed[activeModel] || 0) + 1;
232
248
  }
233
249
  if (data.groundingMetadata) {
234
250
  groundingMetadata = data.groundingMetadata;
@@ -264,8 +280,10 @@ export class ProxyClient {
264
280
  if (attempt < MAX_RETRIES) {
265
281
  const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
266
282
  const delayTime = Math.round(exponentialDelay * (1.0 + Math.random() * 0.5));
267
- process.stdout.write('\n');
268
- console.warn(`Server error or rate limit hit during stream. Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
283
+ if (isDebugOn()) {
284
+ process.stdout.write('\n');
285
+ console.warn(`Server error or rate limit hit during stream. Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
286
+ }
269
287
  await delay(delayTime, abortSignal);
270
288
  if (abortSignal?.aborted) {
271
289
  const err = new Error('Operation aborted');
@@ -275,6 +293,16 @@ export class ProxyClient {
275
293
  attempt++;
276
294
  continue retryLoop;
277
295
  }
296
+ else if (activeModel !== GEMINI_MODELS.FLASH_LITE) {
297
+ debugLog(`Stream rate limit retries exhausted (${MAX_RETRIES}/${MAX_RETRIES}) for ${activeModel}. Automatically falling back to ${GEMINI_MODELS.FLASH_LITE}...`);
298
+ if (isDebugOn()) {
299
+ process.stdout.write('\n');
300
+ console.warn(`Stream rate limit exhausted on ${activeModel}. Automatically switching to Flash-Lite (${GEMINI_MODELS.FLASH_LITE}) to complete request...`);
301
+ }
302
+ activeModel = GEMINI_MODELS.FLASH_LITE;
303
+ attempt = 0;
304
+ continue retryLoop;
305
+ }
278
306
  }
279
307
  throw streamError;
280
308
  }
@@ -297,8 +325,6 @@ export class ProxyClient {
297
325
  */
298
326
  async generateViaBYOK(apiKey, modelName, contents, tools, toolConfig, systemInstruction, generationConfig, streamCallbacks, abortSignal) {
299
327
  const isStreaming = Boolean(streamCallbacks?.onChunk);
300
- const endpoint = isStreaming ? 'streamGenerateContent?alt=sse&key=' : 'generateContent?key=';
301
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(modelName)}:${endpoint}${encodeURIComponent(apiKey)}`;
302
328
  const formattedSystemInstruction = typeof systemInstruction === 'string' ? { parts: [{ text: systemInstruction }] } : systemInstruction;
303
329
  const payload = { contents };
304
330
  if (tools && tools.length > 0)
@@ -313,42 +339,58 @@ export class ProxyClient {
313
339
  const BASE_DELAY_MS = 2000;
314
340
  const MAX_DELAY_MS = 30000;
315
341
  let attempt = 0;
316
- while (true) {
342
+ let activeModel = modelName;
343
+ retryLoop: while (true) {
317
344
  if (abortSignal?.aborted) {
318
345
  const err = new Error('Operation aborted');
319
346
  err.name = 'AbortError';
320
347
  throw err;
321
348
  }
349
+ const endpoint = isStreaming ? 'streamGenerateContent?alt=sse&key=' : 'generateContent?key=';
350
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(activeModel)}:${endpoint}${encodeURIComponent(apiKey)}`;
322
351
  const response = await fetch(url, {
323
352
  method: 'POST',
324
353
  headers: { 'Content-Type': 'application/json' },
325
354
  body: JSON.stringify(payload),
326
355
  signal: abortSignal,
327
356
  });
328
- debugLog(`BYOK Request to ${modelName} complete. Status: ${response.status} ${response.statusText}`);
329
- if ((response.status === 429 ||
357
+ debugLog(`BYOK Request to ${activeModel} complete. Status: ${response.status} ${response.statusText}`);
358
+ if (response.status === 429 ||
330
359
  response.status === 503 ||
331
360
  response.status === 502 ||
332
361
  response.status === 500 ||
333
- response.status === 504) &&
334
- attempt < MAX_RETRIES) {
335
- if (abortSignal?.aborted) {
336
- const err = new Error('Operation aborted');
337
- err.name = 'AbortError';
338
- throw err;
362
+ response.status === 504) {
363
+ if (attempt < MAX_RETRIES) {
364
+ if (abortSignal?.aborted) {
365
+ const err = new Error('Operation aborted');
366
+ err.name = 'AbortError';
367
+ throw err;
368
+ }
369
+ const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
370
+ const delayTime = Math.round(exponentialDelay * (1.0 + Math.random() * 0.5));
371
+ if (isDebugOn()) {
372
+ process.stdout.write('\n');
373
+ console.warn(`Server error or rate limit hit (${response.status}). Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
374
+ }
375
+ await delay(delayTime, abortSignal);
376
+ if (abortSignal?.aborted) {
377
+ const err = new Error('Operation aborted');
378
+ err.name = 'AbortError';
379
+ throw err;
380
+ }
381
+ attempt++;
382
+ continue;
339
383
  }
340
- const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
341
- const delayTime = Math.round(exponentialDelay * (1.0 + Math.random() * 0.5));
342
- process.stdout.write('\n');
343
- console.warn(`Server error or rate limit hit (${response.status}). Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
344
- await delay(delayTime, abortSignal);
345
- if (abortSignal?.aborted) {
346
- const err = new Error('Operation aborted');
347
- err.name = 'AbortError';
348
- throw err;
384
+ else if (activeModel !== GEMINI_MODELS.FLASH_LITE) {
385
+ debugLog(`BYOK rate limit retries exhausted (${MAX_RETRIES}/${MAX_RETRIES}) for ${activeModel}. Automatically falling back to ${GEMINI_MODELS.FLASH_LITE}...`);
386
+ if (isDebugOn()) {
387
+ process.stdout.write('\n');
388
+ console.warn(`BYOK rate limit retries exhausted on ${activeModel}. Automatically switching to Flash-Lite (${GEMINI_MODELS.FLASH_LITE}) to complete request...`);
389
+ }
390
+ activeModel = GEMINI_MODELS.FLASH_LITE;
391
+ attempt = 0;
392
+ continue retryLoop;
349
393
  }
350
- attempt++;
351
- continue;
352
394
  }
353
395
  if (!response.ok) {
354
396
  let errorData = {};
@@ -427,7 +469,7 @@ export class ProxyClient {
427
469
  creditsUsed: 0,
428
470
  remainingBalance: 0,
429
471
  };
430
- accumulateTurnUsage(data.usageMetadata, modelName);
472
+ accumulateTurnUsage(data.usageMetadata, activeModel);
431
473
  }
432
474
  }
433
475
  catch (parseError) {
@@ -436,6 +478,49 @@ export class ProxyClient {
436
478
  }
437
479
  }
438
480
  }
481
+ catch (streamError) {
482
+ if (abortSignal?.aborted || streamError.name === 'AbortError' || streamError.message?.includes('abort')) {
483
+ const err = new Error('Operation aborted');
484
+ err.name = 'AbortError';
485
+ throw err;
486
+ }
487
+ if (streamError.message?.includes('429') ||
488
+ streamError.message?.includes('502') ||
489
+ streamError.message?.includes('503') ||
490
+ streamError.message?.includes('500') ||
491
+ streamError.message?.includes('504') ||
492
+ streamError.message?.includes('Bad Gateway') ||
493
+ streamError.message?.includes('RESOURCE_EXHAUSTED') ||
494
+ streamError.message?.includes('Too Many Requests')) {
495
+ if (attempt < MAX_RETRIES) {
496
+ const exponentialDelay = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * Math.pow(2, attempt));
497
+ const delayTime = Math.round(exponentialDelay * (1.0 + Math.random() * 0.5));
498
+ if (isDebugOn()) {
499
+ process.stdout.write('\n');
500
+ console.warn(`Server error or rate limit hit during BYOK stream. Retrying in ${(delayTime / 1000).toFixed(1)}s... (Attempt ${attempt + 1}/${MAX_RETRIES})`);
501
+ }
502
+ await delay(delayTime, abortSignal);
503
+ if (abortSignal?.aborted) {
504
+ const err = new Error('Operation aborted');
505
+ err.name = 'AbortError';
506
+ throw err;
507
+ }
508
+ attempt++;
509
+ continue retryLoop;
510
+ }
511
+ else if (activeModel !== GEMINI_MODELS.FLASH_LITE) {
512
+ debugLog(`BYOK stream rate limit retries exhausted (${MAX_RETRIES}/${MAX_RETRIES}) for ${activeModel}. Automatically falling back to ${GEMINI_MODELS.FLASH_LITE}...`);
513
+ if (isDebugOn()) {
514
+ process.stdout.write('\n');
515
+ console.warn(`BYOK stream rate limit exhausted on ${activeModel}. Automatically switching to Flash-Lite (${GEMINI_MODELS.FLASH_LITE}) to complete request...`);
516
+ }
517
+ activeModel = GEMINI_MODELS.FLASH_LITE;
518
+ attempt = 0;
519
+ continue retryLoop;
520
+ }
521
+ }
522
+ throw streamError;
523
+ }
439
524
  finally {
440
525
  reader.releaseLock();
441
526
  }
@@ -469,7 +554,7 @@ export class ProxyClient {
469
554
  creditsUsed: 0,
470
555
  remainingBalance: 0,
471
556
  };
472
- accumulateTurnUsage(data.usageMetadata, modelName);
557
+ accumulateTurnUsage(data.usageMetadata, activeModel);
473
558
  }
474
559
  }
475
560
  return {
@@ -24,15 +24,27 @@ export interface Profile {
24
24
  * Result of resolving an `@alias/relative/path` string against the workspace registry.
25
25
  */
26
26
  export interface ResolvedWorkspacePath {
27
- /** The alias that was matched (e.g., "backend"). */
28
- alias: string;
29
- /** The absolute root directory of the matched registered workspace. */
27
+ /** The alias that was matched (e.g., "backend"), or null for primary workspace. */
28
+ alias: string | null;
29
+ /** The absolute root directory of the matched registered workspace or primary workspace. */
30
30
  workspaceRoot: string;
31
31
  /** The relative path within that workspace (e.g., "src/routes.ts"). */
32
32
  relativePath: string;
33
33
  /** The fully resolved absolute path to the target file or directory. */
34
34
  absolutePath: string;
35
+ /** Whether the path was resolved via primary sub-path auto-focusing. */
36
+ isAutoFocused?: boolean;
35
37
  }
38
+ /**
39
+ * Returns the global configuration directory for Minovative Mind CLI.
40
+ * During automated tests, returns an isolated temporary directory to prevent test runs
41
+ * from modifying the developer's live workspace configurations.
42
+ */
43
+ export declare function getGlobalConfigDir(): string;
44
+ /**
45
+ * Returns the active path to the workspace registry JSON file.
46
+ */
47
+ export declare function getRegistryFile(): string;
36
48
  /**
37
49
  * Service that manages a global registry of external workspace roots.
38
50
  *
@@ -54,11 +66,16 @@ declare class WorkspaceRegistry {
54
66
  private profiles;
55
67
  /** Whether the registry has been loaded from disk. */
56
68
  private initialized;
69
+ private primarySubPath;
57
70
  /**
58
71
  * Initializes the registry by loading persisted workspace entries from disk.
59
72
  * Safe to call multiple times — subsequent calls are no-ops.
60
73
  */
61
74
  init(): void;
75
+ /**
76
+ * Internal guard to guarantee the registry is loaded before any read or write operation.
77
+ */
78
+ private ensureInitialized;
62
79
  /**
63
80
  * Registers a new external workspace root with the given alias under a profile.
64
81
  *
@@ -124,15 +141,71 @@ declare class WorkspaceRegistry {
124
141
  root: string;
125
142
  }>;
126
143
  /**
127
- * Resolves an `@alias/relative/path` string into its constituent parts.
144
+ * Sets the active primary sub-path for auto-focusing relative file queries.
145
+ *
146
+ * @param subPath - The relative directory path within the primary workspace (e.g., "src/services"), or null to clear.
147
+ */
148
+ setPrimarySubPath(subPath: string | null): void;
149
+ /**
150
+ * Returns the currently active primary sub-path, or null if none is set.
151
+ */
152
+ getPrimarySubPath(): string | null;
153
+ /**
154
+ * Returns whether a primary sub-path is currently active.
155
+ */
156
+ hasPrimarySubPath(): boolean;
157
+ /**
158
+ * Clears the active primary sub-path.
159
+ */
160
+ clearPrimarySubPath(): void;
161
+ /**
162
+ * Gets the focused root directory for the given primary workspace root.
163
+ * If a primary sub-path is active, returns the resolved sub-path directory;
164
+ * otherwise returns the primary root.
165
+ *
166
+ * @param primaryRoot - The base primary workspace root directory.
167
+ */
168
+ getFocusedRoot(primaryRoot: string): string;
169
+ /**
170
+ * Checks whether a target path is securely contained within a given boundary root.
171
+ *
172
+ * @param targetPath - The absolute or relative target path.
173
+ * @param boundaryRoot - The boundary root directory.
174
+ */
175
+ isPathWithinBoundary(targetPath: string, boundaryRoot: string): boolean;
176
+ /**
177
+ * Finds a registered workspace that contains the given file or directory path.
178
+ *
179
+ * @param targetPath - The file or directory path to check.
180
+ * @returns The matching `RegisteredWorkspace` or null if not found.
181
+ */
182
+ findWorkspaceForPath(targetPath: string): RegisteredWorkspace | null;
183
+ /**
184
+ * Checks whether a path resides inside the primary workspace or any registered workspace.
185
+ *
186
+ * @param targetPath - The file or directory path to inspect.
187
+ * @param primaryRoot - Optional primary workspace root.
188
+ */
189
+ isInsideRegisteredWorkspace(targetPath: string, primaryRoot?: string): boolean;
190
+ /**
191
+ * Resolves an `@alias/relative/path` string or primary workspace path into constituent parts.
128
192
  *
129
193
  * @param filePath - A file path that may or may not start with `@alias/`.
130
- * @returns A `ResolvedWorkspacePath` if the path has a valid `@alias/` prefix
131
- * and the alias is registered, or `null` if the path is a standard
132
- * workspace-relative path (no `@` prefix or unrecognized alias).
194
+ * @param options - Optional resolution options including primaryRoot and autoFocusSubPath.
195
+ * @returns A `ResolvedWorkspacePath` if resolved, or `null` if the path cannot be resolved.
133
196
  * @throws Error if the path has an `@` prefix but the alias is not registered.
134
197
  */
135
- resolve(filePath: string): ResolvedWorkspacePath | null;
198
+ resolve(filePath: string, options?: {
199
+ primaryRoot?: string;
200
+ autoFocusSubPath?: boolean;
201
+ }): ResolvedWorkspacePath | null;
202
+ /**
203
+ * Resolves a relative path against the primary root with automatic sub-path focusing.
204
+ *
205
+ * @param primaryRoot - The base primary workspace root directory.
206
+ * @param relativePath - The target relative path to resolve.
207
+ */
208
+ resolveWithAutoFocus(primaryRoot: string, relativePath: string): ResolvedWorkspacePath;
136
209
  /**
137
210
  * Checks if a given file path uses the `@alias/` prefix syntax.
138
211
  *