minovative-mind-cli 2.5.1 → 2.6.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 (43) hide show
  1. package/README.md +28 -25
  2. package/dist/commands/chat.js +1 -1
  3. package/dist/services/agent/inputHandler.d.ts +9 -0
  4. package/dist/services/agent/inputHandler.js +34 -0
  5. package/dist/services/agent/slashCommands.js +158 -37
  6. package/dist/services/agent/syntaxAgent.d.ts +40 -0
  7. package/dist/services/agent/syntaxAgent.js +237 -23
  8. package/dist/services/agent/toolLoop.js +10 -1
  9. package/dist/services/agent/types.d.ts +1 -0
  10. package/dist/services/agent-tools.d.ts +156 -1
  11. package/dist/services/agent-tools.js +259 -67
  12. package/dist/services/agent.d.ts +74 -0
  13. package/dist/services/agent.js +192 -30
  14. package/dist/services/ai.d.ts +5 -0
  15. package/dist/services/ai.js +80 -87
  16. package/dist/services/chatHistoryService.d.ts +11 -0
  17. package/dist/services/chatHistoryService.js +20 -1
  18. package/dist/services/contextAgent.d.ts +1 -1
  19. package/dist/services/contextAgent.js +9 -29
  20. package/dist/services/orchestration/investigationAgent.d.ts +2 -1
  21. package/dist/services/orchestration/investigationAgent.js +7 -2
  22. package/dist/services/orchestration/investigationOrchestrator.d.ts +1 -1
  23. package/dist/services/orchestration/investigationOrchestrator.js +13 -2
  24. package/dist/services/orchestration/orchestrator.js +21 -4
  25. package/dist/services/orchestration/subAgent.d.ts +2 -1
  26. package/dist/services/orchestration/subAgent.js +12 -6
  27. package/dist/services/workspaceRegistry.d.ts +7 -0
  28. package/dist/services/workspaceRegistry.js +22 -0
  29. package/dist/utils/analysisRunner.d.ts +27 -4
  30. package/dist/utils/analysisRunner.js +100 -20
  31. package/dist/utils/config.d.ts +2 -0
  32. package/dist/utils/config.js +2 -0
  33. package/dist/utils/fuzzyMatch.d.ts +32 -0
  34. package/dist/utils/fuzzyMatch.js +215 -27
  35. package/dist/utils/localSyntaxValidator.d.ts +2 -2
  36. package/dist/utils/localSyntaxValidator.js +280 -81
  37. package/dist/utils/performanceAuditor.d.ts +2 -7
  38. package/dist/utils/performanceAuditor.js +541 -89
  39. package/dist/utils/projectStorage.js +9 -0
  40. package/dist/utils/systemPrompts.d.ts +3 -2
  41. package/dist/utils/systemPrompts.js +29 -5
  42. package/oclif.manifest.json +2 -2
  43. package/package.json +1 -1
@@ -1,26 +1,24 @@
1
- import { GoogleGenerativeAI } from '@google/generative-ai';
1
+ import { GoogleGenerativeAI, SchemaType, } from '@google/generative-ai';
2
2
  import { GEMINI_MODELS, DEFAULT_MODEL, MAX_OUTPUT_TOKENS, isByokEnabled } from '../utils/config.js';
3
3
  import { getToolDeclarations } from './agent-tools.js';
4
4
  import { getMetricCollector } from './metrics.js';
5
5
  import { getAuthorizedIdToken } from './auth.js';
6
6
  import { debugLog } from '../utils/logger.js';
7
7
  import { readCache, writeCache } from '../utils/projectStorage.js';
8
- 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';
8
+ 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, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, } from '../utils/systemPrompts.js';
9
9
  import { workspaceRegistry } from './workspaceRegistry.js';
10
10
  import { loadCredentials } from '../utils/credentialStore.js';
11
11
  import { ProxyClient, accumulateTurnUsage } from './proxyClient.js';
12
12
  function getMultiWorkspaceBlock() {
13
13
  const summary = workspaceRegistry.buildPromptSummary();
14
- if (!summary)
15
- return '';
16
- return `<multi_workspace>
17
- The user has registered external workspaces that you can access using the @alias/ prefix:
18
- ${summary}
19
-
20
- 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".
21
-
22
- 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.
23
- </multi_workspace>`;
14
+ const primaryRoot = process.cwd();
15
+ const primaryName = primaryRoot.split(/[/\\]/).pop() || 'Primary';
16
+ let block = `<workspace_context>\nYour current primary workspace (the default "./" root) is:\n- ./ (Workspace: ${primaryName}) → ${primaryRoot}\n`;
17
+ if (summary) {
18
+ block += `\nThe user has also registered external workspaces that you can access using the @alias/ prefix:\n${summary}\n\nTo 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".\n\nWhen 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.\n`;
19
+ }
20
+ block += `</workspace_context>`;
21
+ return block;
24
22
  }
25
23
  // ─── Model Overrides (for Regression Testing) ────────────────────────
26
24
  let contextModelOverride = null;
@@ -57,7 +55,7 @@ export function getGlobalActiveModel() {
57
55
  const proxyClient = new ProxyClient();
58
56
  // ─── History Limits ──────────────────────────────────────────────────
59
57
  /** Maximum number of Content entries to keep in the sliding history window. */
60
- const MAX_HISTORY_ENTRIES = 100;
58
+ const MAX_HISTORY_ENTRIES = 500;
61
59
  /**
62
60
  * Maximum character length for a single Part's text content.
63
61
  * Anything beyond this is truncated with an ellipsis marker so the proxy
@@ -157,7 +155,7 @@ export class ProxyChatSession {
157
155
  const pruned = this.history.slice(0, trimCount);
158
156
  this.history = this.history.slice(trimCount);
159
157
  if (this.sessionId && this.workspaceRoot) {
160
- const archiveFile = `archived_history_${this.sessionId}.json`;
158
+ const archiveFile = `archives/archived_history_${this.sessionId}.json`;
161
159
  const existingArchive = readCache(this.workspaceRoot, archiveFile) || [];
162
160
  await writeCache(this.workspaceRoot, archiveFile, [...existingArchive, ...pruned]);
163
161
  }
@@ -359,9 +357,7 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
359
357
  const idToken = await getAuthorizedIdToken();
360
358
  if (!idToken)
361
359
  return text;
362
- let model = getGlobalActiveModel();
363
- if (model === 'auto')
364
- model = GEMINI_MODELS.FLASH_LITE;
360
+ let model = GEMINI_MODELS.FLASH_LITE;
365
361
  const parts = [{ text }];
366
362
  if (inlineData) {
367
363
  parts.push({ inlineData });
@@ -414,7 +410,10 @@ export async function compressTextUsingFlashLite(text, instruction = "<directive
414
410
  }
415
411
  catch (error) {
416
412
  debugLog(`Failed to compress text using flash-lite: ${error}`);
417
- if (error?.status === 401 || error?.status === 403 || error?.message?.includes('API_KEY_INVALID') || error?.message?.includes('quota')) {
413
+ if (error?.status === 401 ||
414
+ error?.status === 403 ||
415
+ error?.message?.includes('API_KEY_INVALID') ||
416
+ error?.message?.includes('quota')) {
418
417
  throw new Error('AI_AUTH_ERROR: Your API key or quota is invalid. Please run /config-key or re-login.');
419
418
  }
420
419
  return text; // fallback to raw text if compression fails
@@ -430,30 +429,14 @@ export function getContextToolDeclarations() {
430
429
  return [
431
430
  {
432
431
  functionDeclarations: [
433
- {
434
- name: 'select_files',
435
- description: 'Select files to investigate further. WARNING: This reads the entire file into your context window, consuming tokens. Only use this if you MUST read the contents to find other dependencies or understand architecture. If you already know the file is relevant to the task, do NOT use this tool—simply add it to finish_investigation.',
436
- parameters: {
437
- type: 'OBJECT',
438
- properties: {
439
- files: {
440
- type: 'ARRAY',
441
- items: { type: 'STRING' },
442
- description: 'Array of file paths to read',
443
- },
444
- reasoning: { type: 'STRING', description: 'Why you selected these files' },
445
- },
446
- required: ['files', 'reasoning'],
447
- },
448
- },
449
432
  {
450
433
  name: 'search_codebase',
451
434
  description: 'Search the codebase for a pattern.',
452
435
  parameters: {
453
- type: 'OBJECT',
436
+ type: SchemaType.OBJECT,
454
437
  properties: {
455
- pattern: { type: 'STRING', description: 'Pattern to search for' },
456
- fileGlob: { type: 'STRING', description: 'Optional glob to filter files' },
438
+ pattern: { type: SchemaType.STRING, description: 'Pattern to search for' },
439
+ fileGlob: { type: SchemaType.STRING, description: 'Optional glob to filter files' },
457
440
  },
458
441
  required: ['pattern'],
459
442
  },
@@ -462,14 +445,14 @@ export function getContextToolDeclarations() {
462
445
  name: 'read_file',
463
446
  description: 'Read a single file. Supports text files and native parsing of .pdf files (including math and diagrams). Use startLine and endLine to read specific chunks of massive text files. WARNING: Reading files consumes tokens. Only read if you MUST understand the file contents or find dependencies. If you already know it is relevant, simply add it to finish_investigation without reading it.',
464
447
  parameters: {
465
- type: 'OBJECT',
448
+ type: SchemaType.OBJECT,
466
449
  properties: {
467
- filePath: { type: 'STRING', description: 'Path to the file' },
468
- startLine: { type: 'NUMBER', description: 'Optional. 1-indexed starting line number.' },
469
- endLine: { type: 'NUMBER', description: 'Optional. 1-indexed ending line number (inclusive).' },
450
+ filePath: { type: SchemaType.STRING, description: 'Path to the file' },
451
+ startLine: { type: SchemaType.NUMBER, description: 'Optional. 1-indexed starting line number.' },
452
+ endLine: { type: SchemaType.NUMBER, description: 'Optional. 1-indexed ending line number (inclusive).' },
470
453
  targetElements: {
471
- type: 'ARRAY',
472
- items: { type: 'STRING' },
454
+ type: SchemaType.ARRAY,
455
+ items: { type: SchemaType.STRING },
473
456
  description: 'Optional. An array of specific function names, class names, or variables to extract. The tool will intelligently locate and return only the blocks defining these elements.',
474
457
  },
475
458
  },
@@ -480,10 +463,10 @@ export function getContextToolDeclarations() {
480
463
  name: 'list_directory',
481
464
  description: 'List the contents of a directory. Returns files, folders, and their sizes.',
482
465
  parameters: {
483
- type: 'OBJECT',
466
+ type: SchemaType.OBJECT,
484
467
  properties: {
485
- dirPath: { type: 'STRING', description: 'Path to the directory to list (e.g., "src/components")' },
486
- maxDepth: { type: 'NUMBER', description: 'Maximum depth to traverse (default 1)' },
468
+ dirPath: { type: SchemaType.STRING, description: 'Path to the directory to list (e.g., "src/components")' },
469
+ maxDepth: { type: SchemaType.NUMBER, description: 'Maximum depth to traverse (default 1)' },
487
470
  },
488
471
  required: ['dirPath'],
489
472
  },
@@ -492,15 +475,15 @@ export function getContextToolDeclarations() {
492
475
  name: 'find_dependencies',
493
476
  description: 'Trace the dependency graph for a file. Returns what the file imports (forward) and what files import it (reverse). Critical for understanding the blast radius before modifying, deleting, or renaming a file.',
494
477
  parameters: {
495
- type: 'OBJECT',
478
+ type: SchemaType.OBJECT,
496
479
  properties: {
497
- filePath: { type: 'STRING', description: 'Relative path to the file to trace dependencies for.' },
480
+ filePath: { type: SchemaType.STRING, description: 'Relative path to the file to trace dependencies for.' },
498
481
  direction: {
499
- type: 'STRING',
482
+ type: SchemaType.STRING,
500
483
  description: 'Direction to trace: "both" (default), "forward", or "reverse".',
501
484
  },
502
485
  maxDepth: {
503
- type: 'NUMBER',
486
+ type: SchemaType.NUMBER,
504
487
  description: 'Maximum traversal depth (default 3, max 5).',
505
488
  },
506
489
  },
@@ -511,18 +494,18 @@ export function getContextToolDeclarations() {
511
494
  name: 'find_recent_changes',
512
495
  description: 'Find files that have been modified recently within the workspace. Useful for understanding what the user was just working on if they ask vague questions like "why is it failing?". Automatically ignores .git, node_modules, etc.',
513
496
  parameters: {
514
- type: 'OBJECT',
497
+ type: SchemaType.OBJECT,
515
498
  properties: {
516
499
  dirPath: {
517
- type: 'STRING',
500
+ type: SchemaType.STRING,
518
501
  description: 'Relative path to directory to search from. Defaults to workspace root ".".',
519
502
  },
520
503
  minutes: {
521
- type: 'NUMBER',
504
+ type: SchemaType.NUMBER,
522
505
  description: 'Look for files modified within this many minutes. Defaults to 60.',
523
506
  },
524
507
  maxDepth: {
525
- type: 'NUMBER',
508
+ type: SchemaType.NUMBER,
526
509
  description: 'Maximum depth to traverse. Defaults to 5.',
527
510
  },
528
511
  },
@@ -530,35 +513,20 @@ export function getContextToolDeclarations() {
530
513
  },
531
514
  {
532
515
  name: 'run_analysis_script',
533
- description: 'Write and execute a disposable analysis script to structurally map code in the workspace. ' +
534
- 'Use this to get exact line ranges for functions, classes, and variables by leveraging the ' +
535
- "language's native AST parser (e.g., TypeScript compiler API, Python ast module, go/parser). " +
536
- 'You can also use this to probe the development environment — detecting available runtimes ' +
537
- '(e.g., node --version, python3 --version), checking if ports are in use, identifying project ' +
538
- 'type (monorepo, package manager), or diagnosing system-level issues (disk space, memory) that ' +
539
- 'may affect execution. ' +
540
- 'For complex investigation tasks, you can write lightweight ML scripts (e.g., TF-IDF cosine similarity ' +
541
- 'to rank file relevance, Z-score outlier detection for anomalous log lines, K-Means clustering, or Naive ' +
542
- 'Bayes classification). Default to "node" for generic math/analysis as a safe baseline, but act like ' +
543
- 'a native inhabitant of the host environment — if Python, Go, Rust, or specialized libraries are available ' +
544
- 'in the project context, leverage the host\'s native runtimes and standard libraries for maximum efficiency. ' +
545
- 'The script is executed from a temporary directory and automatically cleaned up after execution. ' +
546
- 'Output should be structured JSON to stdout. ' +
547
- 'Use the results to make precise read_file calls with exact startLine/endLine instead of guessing. ' +
548
- 'CRITICAL: Do not use this tool on binary, document, or non-code files (e.g. PDF, image, audio, docx).',
516
+ description: "Write and execute a disposable analysis script to structurally map code in the workspace. Use this to get exact line ranges for functions, classes, and variables by leveraging the language's native AST parser (e.g., TypeScript compiler API, Python ast module, go/parser). You can also use this to probe the development environment — detecting available runtimes (e.g., node --version, python3 --version), checking if ports are in use, identifying project type (monorepo, package manager), or diagnosing system-level issues (disk space, memory) that may affect execution. For complex investigation tasks, you can write lightweight ML scripts (e.g., TF-IDF cosine similarity to rank file relevance, Z-score outlier detection for anomalous log lines, K-Means clustering, or Naive Bayes classification). Default to \"node\" for generic math/analysis as a safe baseline, but act like a native inhabitant of the host environment — if Python, Go, Rust, or specialized libraries are available in the project context, leverage the host's native runtimes and standard libraries for maximum efficiency. The script is executed from a temporary directory and automatically cleaned up after execution. Output should be structured JSON to stdout. Use the results to make precise read_file calls with exact startLine/endLine instead of guessing. CRITICAL: Do not use this tool on binary, document, or non-code files (e.g. PDF, image, audio, docx).",
549
517
  parameters: {
550
- type: 'OBJECT',
518
+ type: SchemaType.OBJECT,
551
519
  properties: {
552
520
  language: {
553
- type: 'STRING',
521
+ type: SchemaType.STRING,
554
522
  description: 'The runtime to use: "node", "ts-node", "python", "bash", "go", or "rust".',
555
523
  },
556
524
  code: {
557
- type: 'STRING',
525
+ type: SchemaType.STRING,
558
526
  description: 'The analysis script code. Should output structured JSON to stdout with structural information (name, type, startLine, endLine for each code element).',
559
527
  },
560
528
  targetFile: {
561
- type: 'STRING',
529
+ type: SchemaType.STRING,
562
530
  description: 'The workspace file being analyzed. Used for logging and context only.',
563
531
  },
564
532
  },
@@ -569,12 +537,12 @@ export function getContextToolDeclarations() {
569
537
  name: 'finish_investigation',
570
538
  description: 'Call this when you have gathered enough context.',
571
539
  parameters: {
572
- type: 'OBJECT',
540
+ type: SchemaType.OBJECT,
573
541
  properties: {
574
- summary: { type: 'STRING', description: 'Summary of your findings' },
542
+ summary: { type: SchemaType.STRING, description: 'Summary of your findings' },
575
543
  relevantFiles: {
576
- type: 'ARRAY',
577
- items: { type: 'STRING' },
544
+ type: SchemaType.ARRAY,
545
+ items: { type: SchemaType.STRING },
578
546
  description: 'Paths of files that are relevant to the user request',
579
547
  },
580
548
  },
@@ -585,9 +553,9 @@ export function getContextToolDeclarations() {
585
553
  name: 'perform_web_search',
586
554
  description: 'Search the web for documentation, solutions, or real-time information.',
587
555
  parameters: {
588
- type: 'OBJECT',
556
+ type: SchemaType.OBJECT,
589
557
  properties: {
590
- query: { type: 'STRING', description: 'The search query to look up on the web' },
558
+ query: { type: SchemaType.STRING, description: 'The search query to look up on the web' },
591
559
  },
592
560
  required: ['query'],
593
561
  },
@@ -599,7 +567,7 @@ export function getContextToolDeclarations() {
599
567
  export function createContextAgentSession() {
600
568
  const contextTools = getContextToolDeclarations();
601
569
  let model = contextModelOverride || getGlobalActiveModel();
602
- if (model === 'auto')
570
+ if (model === 'auto' || model.includes('claude'))
603
571
  model = GEMINI_MODELS.FLASH;
604
572
  return new ProxyChatSession(model, CONTEXT_SYSTEM_INSTRUCTION.replace('{{MULTI_WORKSPACE_BLOCK}}', getMultiWorkspaceBlock()), contextTools, {
605
573
  maxOutputTokens: MAX_OUTPUT_TOKENS,
@@ -611,7 +579,7 @@ export function createContextAgentSession() {
611
579
  // ─── Intent Router Service ───────────────────────────────────────────
612
580
  export function createIntentRouterSession() {
613
581
  let model = getGlobalActiveModel();
614
- if (model === 'auto')
582
+ if (model === 'auto' || model.includes('claude'))
615
583
  model = GEMINI_MODELS.FLASH_LITE;
616
584
  return new ProxyChatSession(model, INTENT_ROUTER_SYSTEM_INSTRUCTION, [], // no tools
617
585
  { temperature: 0, responseMimeType: 'application/json' });
@@ -619,7 +587,7 @@ export function createIntentRouterSession() {
619
587
  // ─── Execution Complexity Router Service ──────────────────────────────
620
588
  export function createExecutionComplexitySession() {
621
589
  let model = getGlobalActiveModel();
622
- if (model === 'auto')
590
+ if (model === 'auto' || model.includes('claude'))
623
591
  model = GEMINI_MODELS.FLASH_LITE;
624
592
  return new ProxyChatSession(model, EXECUTION_COMPLEXITY_SYSTEM_INSTRUCTION, [], // no tools
625
593
  { temperature: 0, responseMimeType: 'application/json' });
@@ -627,7 +595,7 @@ export function createExecutionComplexitySession() {
627
595
  // ─── Investigation Complexity Router Service ─────────────────────────
628
596
  export function createInvestigationComplexitySession() {
629
597
  let model = getGlobalActiveModel();
630
- if (model === 'auto')
598
+ if (model === 'auto' || model.includes('claude'))
631
599
  model = GEMINI_MODELS.FLASH_LITE;
632
600
  return new ProxyChatSession(model, INVESTIGATION_COMPLEXITY_SYSTEM_INSTRUCTION, [], // no tools
633
601
  { temperature: 0, responseMimeType: 'application/json' });
@@ -635,7 +603,7 @@ export function createInvestigationComplexitySession() {
635
603
  // ─── Web Search Agent Service ───────────────────────────────────────────
636
604
  export function createWebSearchAgentSession() {
637
605
  let model = getGlobalActiveModel();
638
- if (model === 'auto')
606
+ if (model === 'auto' || model.includes('claude'))
639
607
  model = GEMINI_MODELS.FLASH;
640
608
  return new ProxyChatSession(model, WEB_SEARCH_SYSTEM_INSTRUCTION, [{ googleSearch: {} }], {
641
609
  maxOutputTokens: MAX_OUTPUT_TOKENS,
@@ -644,6 +612,31 @@ export function createWebSearchAgentSession() {
644
612
  topK: 40,
645
613
  });
646
614
  }
615
+ // ─── History Summarizer Agent Service ─────────────────────────────────
616
+ export function createHistorySummarizerSession() {
617
+ let model = getGlobalActiveModel();
618
+ if (model === 'auto' || model.includes('claude'))
619
+ model = GEMINI_MODELS.FLASH_LITE;
620
+ return new ProxyChatSession(model, HISTORY_SUMMARIZER_SYSTEM_INSTRUCTION, [], { temperature: 0.2, maxOutputTokens: MAX_OUTPUT_TOKENS });
621
+ }
622
+ /**
623
+ * Summarizes an array of Content history entries using Gemini Flash Lite.
624
+ */
625
+ export async function summarizeChatHistory(history) {
626
+ if (!history || history.length === 0) {
627
+ return '';
628
+ }
629
+ try {
630
+ const session = createHistorySummarizerSession();
631
+ session.loadRawHistory(JSON.parse(JSON.stringify(history)));
632
+ const result = await session.sendMessage('Summarize the preceding conversation history following your compression rules.');
633
+ return result.response.text() || '';
634
+ }
635
+ catch (error) {
636
+ debugLog(`Failed to summarize chat history: ${error?.message || error}`);
637
+ return '';
638
+ }
639
+ }
647
640
  /**
648
641
  * Generates a concise title for a chat session based on the user's first message.
649
642
  */
@@ -658,7 +651,7 @@ export async function generateChatTitle(firstMessage) {
658
651
  const instruction = "<identity>\nYou are a helpful assistant that generates extremely concise chat titles based on a user's first message.\n</identity>\n\n<directives>\nOutput ONLY the title, no quotes, no markdown, no punctuation.\n</directives>";
659
652
  const contents = [{ role: 'user', parts: [{ text: firstMessage.substring(0, 500) }] }];
660
653
  let model = getGlobalActiveModel();
661
- if (model === 'auto')
654
+ if (model === 'auto' || model.includes('claude'))
662
655
  model = GEMINI_MODELS.FLASH_LITE;
663
656
  const byokEnabled = await isByokEnabled();
664
657
  let result;
@@ -702,7 +695,7 @@ export async function generateChatTitle(firstMessage) {
702
695
  }
703
696
  }
704
697
  title = result.thought || title;
705
- title = title.replace(/["']/g, '').trim();
698
+ title = title.replace(/"/g, '').replace(/'/g, '').trim();
706
699
  return title ? title : firstMessage.substring(0, maxLength);
707
700
  }
708
701
  catch (error) {
@@ -25,6 +25,8 @@ export interface ChatSessionData {
25
25
  totalInputTokens?: number;
26
26
  /** Optional total cumulative output tokens consumed during the session. */
27
27
  totalOutputTokens?: number;
28
+ /** Optional total cumulative credits used during the session. */
29
+ totalCreditsUsed?: number;
28
30
  /** Optional name of the Git branch active when this session was last updated. */
29
31
  gitBranch?: string;
30
32
  /** Optional flag indicating whether auto-approval of commands was enabled in this session. */
@@ -72,6 +74,15 @@ declare class ChatHistoryService {
72
74
  * @returns A promise that resolves when the session has been successfully written to the cache.
73
75
  */
74
76
  saveSession(session: ChatSessionData): Promise<void>;
77
+ /**
78
+ * Updates the user-friendly title of an existing chat session in the local workspace cache.
79
+ * If the session with the matching ID exists, its title is updated and persisted to `chat_sessions.json`.
80
+ *
81
+ * @param id - The unique identifier of the chat session to update.
82
+ * @param title - The new title string to set for the chat session.
83
+ * @returns A promise that resolves to `true` if the session title was updated and saved, or `false` if the session was not found or the service is uninitialized.
84
+ */
85
+ updateSessionTitle(id: string, title: string): Promise<boolean>;
75
86
  /**
76
87
  * Deletes a chat session from the local workspace cache and removes its associated archived history file.
77
88
  * If the session or its archived history file does not exist, the operation completes without throwing an error.
@@ -56,6 +56,25 @@ class ChatHistoryService {
56
56
  }
57
57
  await writeCache(this.workspaceRoot, 'chat_sessions.json', sessions);
58
58
  }
59
+ /**
60
+ * Updates the user-friendly title of an existing chat session in the local workspace cache.
61
+ * If the session with the matching ID exists, its title is updated and persisted to `chat_sessions.json`.
62
+ *
63
+ * @param id - The unique identifier of the chat session to update.
64
+ * @param title - The new title string to set for the chat session.
65
+ * @returns A promise that resolves to `true` if the session title was updated and saved, or `false` if the session was not found or the service is uninitialized.
66
+ */
67
+ async updateSessionTitle(id, title) {
68
+ if (!this.workspaceRoot || !id)
69
+ return false;
70
+ const sessions = this.getSessions();
71
+ const session = sessions.find((s) => s.id === id);
72
+ if (!session)
73
+ return false;
74
+ session.title = title;
75
+ await writeCache(this.workspaceRoot, 'chat_sessions.json', sessions);
76
+ return true;
77
+ }
59
78
  /**
60
79
  * Deletes a chat session from the local workspace cache and removes its associated archived history file.
61
80
  * If the session or its archived history file does not exist, the operation completes without throwing an error.
@@ -87,7 +106,7 @@ class ChatHistoryService {
87
106
  const storageDir = getProjectStorageDir(this.workspaceRoot);
88
107
  await Promise.all(ids.map(async (id) => {
89
108
  try {
90
- const archivePath = path.join(storageDir, `archived_history_${id}.json`);
109
+ const archivePath = path.join(storageDir, 'archives', `archived_history_${id}.json`);
91
110
  await fs.unlink(archivePath);
92
111
  }
93
112
  catch {
@@ -19,7 +19,7 @@ export declare function evaluateExecutionComplexity(userRequest: string, investi
19
19
  export declare function gatherContext(workspaceRoot: string, userRequest: string, chatHistory: string | undefined, inputHandler: {
20
20
  getAndClear: () => string;
21
21
  waitForPrompt: () => Promise<void>;
22
- }, abortSignal: AbortSignal, onProgress?: (msg: string) => void): Promise<{
22
+ }, abortSignal: AbortSignal, onProgress?: (msg: string) => void, onToolCall?: (msg: string, label: string) => void): Promise<{
23
23
  contextResult: ContextAgentResult | null;
24
24
  targetAgent: 'CHAT' | 'EXECUTE';
25
25
  chainedMessages: string[];
@@ -181,7 +181,7 @@ Number of Relevant Files: ${numRelevantFiles}`;
181
181
  return 'HARD';
182
182
  }
183
183
  }
184
- export async function gatherContext(workspaceRoot, userRequest, chatHistory = '', inputHandler, abortSignal, onProgress) {
184
+ export async function gatherContext(workspaceRoot, userRequest, chatHistory = '', inputHandler, abortSignal, onProgress, onToolCall) {
185
185
  // Always skip slash commands for zero latency
186
186
  if (userRequest.startsWith('/')) {
187
187
  return { contextResult: null, targetAgent: 'EXECUTE', chainedMessages: [] };
@@ -268,7 +268,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
268
268
  const complexity = await evaluateInvestigationComplexity(userRequest, projectType, approxFiles, chatHistory);
269
269
  if (complexity.strategy === 'PARALLEL' && complexity.agentAssignments.length > 0) {
270
270
  const orchestrator = new InvestigationOrchestrator();
271
- const parallelResult = await orchestrator.runParallelInvestigation(userRequest, complexity.agentAssignments, workspaceRoot, projectTree, projectType, chatHistory, abortSignal, onProgress);
271
+ const parallelResult = await orchestrator.runParallelInvestigation(userRequest, complexity.agentAssignments, workspaceRoot, projectTree, projectType, chatHistory, abortSignal, onProgress, onToolCall);
272
272
  if (parallelResult !== null) {
273
273
  // If parallel investigation succeeded, we are done. Return immediately.
274
274
  parallelResult.isParallel = true;
@@ -338,10 +338,6 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
338
338
  const filesToRead = args.relevantFiles || [];
339
339
  logMsg = ` [Context Agent] Finished investigation (Selected ${filesToRead.length} files)`;
340
340
  }
341
- else if (call.name === 'select_files') {
342
- const filesToRead = args.files || [];
343
- logMsg = ` [Context Agent] Selected ${filesToRead.length} files to read`;
344
- }
345
341
  else if (call.name === 'search_codebase') {
346
342
  logMsg = ` [Context Agent] Searching codebase for: "${args.pattern}"`;
347
343
  }
@@ -366,8 +362,12 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
366
362
  const target = args.targetFile ? ` for: ${args.targetFile}` : '';
367
363
  logMsg = ` [Context Agent] Running analysis script${target}`;
368
364
  }
369
- if (onProgress) {
370
- onProgress(logMsg.trim().replace(/^\[Context Agent\] /, ''));
365
+ const cleanMsg = logMsg.trim().replace(/^\[Context Agent\] /, '');
366
+ if (onToolCall) {
367
+ onToolCall(cleanMsg, 'Context Agent');
368
+ }
369
+ else if (onProgress) {
370
+ onProgress(cleanMsg);
371
371
  }
372
372
  else {
373
373
  console.log(pc.dim(logMsg));
@@ -443,26 +443,6 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
443
443
  });
444
444
  break;
445
445
  }
446
- else if (call.name === 'select_files') {
447
- const filesToRead = args.files || [];
448
- let output = '';
449
- for (const filePath of filesToRead) {
450
- const readResult = await executeTool(workspaceRoot, 'read_file', { filePath });
451
- if (!readResult.error) {
452
- relevantFiles.set(filePath, { text: readResult.output, inlineData: readResult.inlineData });
453
- output += `\n--- File: ${filePath} ---\n${readResult.output}\n`;
454
- }
455
- else {
456
- output += `\n--- File: ${filePath} ---\nError: ${readResult.error}\n`;
457
- }
458
- }
459
- functionResponses.push({
460
- functionResponse: {
461
- name: call.name,
462
- response: { output: output || 'No files read.' },
463
- },
464
- });
465
- }
466
446
  else if (call.name === 'list_directory') {
467
447
  const listRes = await executeTool(workspaceRoot, 'list_directory', args);
468
448
  functionResponses.push({
@@ -499,7 +479,7 @@ export async function gatherContext(workspaceRoot, userRequest, chatHistory = ''
499
479
  else if (call.name === 'perform_web_search') {
500
480
  try {
501
481
  const webSession = createWebSearchAgentSession();
502
- const webResult = await webSession.sendMessage(`Please search the web for the following query and summarize your findings:\n"${args.query}"`);
482
+ const webResult = await webSession.sendMessage(`Please search the web for the following query and summarize your findings:\n"${args.query}"`, undefined, abortSignal);
503
483
  const grounding = webResult.response.groundingMetadata?.();
504
484
  if (grounding?.webSearchQueries && grounding.webSearchQueries.length > 0) {
505
485
  const webMsg = `Web Search: ${grounding.webSearchQueries.map((q) => `"${q}"`).join(', ')}`;
@@ -79,9 +79,10 @@ export declare class InvestigationAgentRunner {
79
79
  * @param chatHistory - Recent conversation history for context.
80
80
  * @param abortSignal - Signal to cancel execution.
81
81
  * @param onProgress - Callback for spinner/terminal updates.
82
+ * @param onTool - Callback for tool invocation logging.
82
83
  * @returns The investigation result with discovered files and summary.
83
84
  */
84
- execute(userRequest: string, chatHistory: string, abortSignal: AbortSignal, onProgress?: (msg: string) => void): Promise<InvestigationResult>;
85
+ execute(userRequest: string, chatHistory: string, abortSignal: AbortSignal, onProgress?: (msg: string) => void, onTool?: (msg: string) => void): Promise<InvestigationResult>;
85
86
  /**
86
87
  * Accumulates token usage from the chat session.
87
88
  */
@@ -17,6 +17,7 @@ import { CONTEXT_SYSTEM_INSTRUCTION } from '../../utils/systemPrompts.js';
17
17
  import { debugLog } from '../../utils/logger.js';
18
18
  import { listDirectory, grepSearch, readFile, traceDependencies, findRecentChanges } from '../agent-tools.js';
19
19
  import { runEphemeralScript } from '../../utils/analysisRunner.js';
20
+ import { formatToolCall } from '../agent/toolLoop.js';
20
21
  // ─── Investigation Agent Runner ──────────────────────────────────────
21
22
  /**
22
23
  * Executes a read-only investigation within a scoped set of domains.
@@ -50,7 +51,7 @@ export class InvestigationAgentRunner {
50
51
  this.projectTree = projectTree;
51
52
  this.projectType = projectType;
52
53
  let model = getGlobalActiveModel();
53
- if (model === GEMINI_MODELS.AUTO)
54
+ if (model === GEMINI_MODELS.AUTO || model.includes('claude'))
54
55
  model = GEMINI_MODELS.FLASH;
55
56
  this.chat = new ProxyChatSession(model, this.buildSystemInstruction(), getContextToolDeclarations(), {
56
57
  maxOutputTokens: MAX_OUTPUT_TOKENS,
@@ -88,9 +89,10 @@ export class InvestigationAgentRunner {
88
89
  * @param chatHistory - Recent conversation history for context.
89
90
  * @param abortSignal - Signal to cancel execution.
90
91
  * @param onProgress - Callback for spinner/terminal updates.
92
+ * @param onTool - Callback for tool invocation logging.
91
93
  * @returns The investigation result with discovered files and summary.
92
94
  */
93
- async execute(userRequest, chatHistory, abortSignal, onProgress) {
95
+ async execute(userRequest, chatHistory, abortSignal, onProgress, onTool) {
94
96
  debugLog(`InvestigationAgent [${this.agentLabel}]: Starting execution for domains: ${this.domains.join(', ')}`);
95
97
  this.lastHeartbeat = Date.now();
96
98
  const relevantFiles = new Map();
@@ -140,6 +142,9 @@ export class InvestigationAgentRunner {
140
142
  this.pingHeartbeat();
141
143
  const args = call.args;
142
144
  const logPrefix = `[${this.agentLabel}]`;
145
+ if (onTool) {
146
+ onTool(formatToolCall(call.name, args));
147
+ }
143
148
  if (call.name === 'finish_investigation') {
144
149
  summary = args.summary || '';
145
150
  const filesToRead = args.relevantFiles || [];
@@ -35,7 +35,7 @@ export declare class InvestigationOrchestrator {
35
35
  * @param onProgress - Callback for spinner/terminal updates.
36
36
  * @returns Merged context result, or null if all agents failed.
37
37
  */
38
- runParallelInvestigation(userRequest: string, agentAssignments: AgentAssignment[], workspaceRoot: string, projectTree: string, projectType: string, chatHistory: string, abortSignal: AbortSignal, onProgress?: (msg: string) => void): Promise<ContextAgentResult | null>;
38
+ runParallelInvestigation(userRequest: string, agentAssignments: AgentAssignment[], workspaceRoot: string, projectTree: string, projectType: string, chatHistory: string, abortSignal: AbortSignal, onProgress?: (msg: string) => void, onToolCallCallback?: (msg: string, label: string) => void): Promise<ContextAgentResult | null>;
39
39
  /**
40
40
  * Merges multiple `InvestigationResult` objects into a single `ContextAgentResult`.
41
41
  *
@@ -44,7 +44,7 @@ export class InvestigationOrchestrator {
44
44
  * @param onProgress - Callback for spinner/terminal updates.
45
45
  * @returns Merged context result, or null if all agents failed.
46
46
  */
47
- async runParallelInvestigation(userRequest, agentAssignments, workspaceRoot, projectTree, projectType, chatHistory, abortSignal, onProgress) {
47
+ async runParallelInvestigation(userRequest, agentAssignments, workspaceRoot, projectTree, projectType, chatHistory, abortSignal, onProgress, onToolCallCallback) {
48
48
  const agentCount = agentAssignments.length;
49
49
  p.log.info(pc.cyan(`🔍 Parallel Investigation (${agentCount} agents)`) +
50
50
  '\n' +
@@ -62,13 +62,24 @@ export class InvestigationOrchestrator {
62
62
  const results = [];
63
63
  for (let i = 0; i < agents.length; i += MAX_CONCURRENT) {
64
64
  const chunk = agents.slice(i, i + MAX_CONCURRENT);
65
+ const chunkAssignments = agentAssignments.slice(i, i + MAX_CONCURRENT);
65
66
  const chunkPromises = chunk.map((agent, chunkIndex) => {
66
67
  const globalIndex = i + chunkIndex;
68
+ const assignment = chunkAssignments[chunkIndex];
69
+ const onToolCall = (msg) => {
70
+ if (onToolCallCallback) {
71
+ onToolCallCallback(msg, assignment.agentLabel);
72
+ }
73
+ else {
74
+ process.stdout.write('\x1b[2K\r'); // Clear the spinner line temporarily
75
+ p.log.step(`${pc.dim(`[${assignment.agentLabel}]`)} ${msg}`);
76
+ }
77
+ };
67
78
  return agent.execute(userRequest, chatHistory, abortSignal, (msg) => {
68
79
  if (onProgress) {
69
80
  onProgress(`Agent ${globalIndex + 1}/${agentCount}: ${msg}`);
70
81
  }
71
- });
82
+ }, onToolCall);
72
83
  });
73
84
  const chunkResults = await Promise.all(chunkPromises);
74
85
  results.push(...chunkResults);