vigthoria-cli 1.11.7 → 1.11.9

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.
@@ -6,6 +6,15 @@ import inquirer from 'inquirer';
6
6
  import * as fs from 'fs';
7
7
  import * as os from 'os';
8
8
  import * as path from 'path';
9
+ function terminalColumns() {
10
+ const raw = Number(process.stdout.columns || 80);
11
+ return Number.isFinite(raw) && raw > 0 ? raw : 80;
12
+ }
13
+ function fitLine(text, reserve = 0) {
14
+ const width = Math.max(24, terminalColumns() - reserve);
15
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
16
+ return clean.length > width ? `${clean.slice(0, Math.max(1, width - 1))}…` : clean;
17
+ }
9
18
  function validateExistingDirectory(input) {
10
19
  const trimmed = String(input || '').trim().replace(/^['"]|['"]$/g, '');
11
20
  if (!trimmed) {
@@ -84,8 +93,8 @@ export async function runAgentSessionMenu(defaults = {}) {
84
93
  };
85
94
  console.log();
86
95
  console.log(chalk.cyan('═══ Agent Session Setup ═══'));
87
- console.log(chalk.gray(`Workspace: ${session.workspacePath}`));
88
- console.log(chalk.gray(`Debug: ${session.debugMode ? 'on' : 'off'} | Auto-approve: ${session.autoApprove ? 'on' : 'off'} | Auto-save: ${session.autoSave ? 'on' : 'off'}`));
96
+ console.log(chalk.gray(fitLine(`Workspace: ${session.workspacePath}`)));
97
+ console.log(chalk.gray(fitLine(`Debug: ${session.debugMode ? 'on' : 'off'} | Auto-approve: ${session.autoApprove ? 'on' : 'off'} | Auto-save: ${session.autoSave ? 'on' : 'off'}`)));
89
98
  console.log();
90
99
  const { action } = await inquirer.prompt([
91
100
  {
@@ -153,8 +162,8 @@ export async function runAgentSessionMenu(defaults = {}) {
153
162
  }
154
163
  console.log();
155
164
  console.log(chalk.cyan('── Session ready ──'));
156
- console.log(chalk.gray(`Workspace: ${session.workspacePath}`));
157
- console.log(chalk.gray(`Debug: ${session.debugMode ? 'on' : 'off'} | Auto-approve: ${session.autoApprove ? 'on' : 'off'} | Auto-save: ${session.autoSave ? 'on' : 'off'}`));
165
+ console.log(chalk.gray(fitLine(`Workspace: ${session.workspacePath}`)));
166
+ console.log(chalk.gray(fitLine(`Debug: ${session.debugMode ? 'on' : 'off'} | Auto-approve: ${session.autoApprove ? 'on' : 'off'} | Auto-save: ${session.autoSave ? 'on' : 'off'}`)));
158
167
  console.log();
159
168
  return session;
160
169
  }
@@ -48,6 +48,8 @@ export declare class ChatCommand {
48
48
  private retryPromptStreak;
49
49
  private v3SuppressThinkingStream;
50
50
  private v3StreamedAnswerBuffer;
51
+ private v3SeenToolCalls;
52
+ private v3SeenToolResults;
51
53
  private lastAgentRunOutcome;
52
54
  private isJwtExpirationError;
53
55
  private isNetworkError;
@@ -64,6 +66,7 @@ export declare class ChatCommand {
64
66
  private resolveInitialModel;
65
67
  private isLegacyAgentFallbackAllowed;
66
68
  private shouldRescueFailedAnalysis;
69
+ private isDeepAnalysisPrompt;
67
70
  private tryAnalysisLocalRescue;
68
71
  private resolveAgentExecutionPolicy;
69
72
  private getMessagesForModel;
@@ -113,6 +116,10 @@ export declare class ChatCommand {
113
116
  private sanitizeServerPath;
114
117
  private stripHiddenThoughtBlocks;
115
118
  private describeV3AgentTool;
119
+ private terminalColumns;
120
+ private fitTerminalText;
121
+ private normalizeToolOutputForDisplay;
122
+ private toolEventKey;
116
123
  private isRawV3StreamPayload;
117
124
  private consumeV3StreamPayload;
118
125
  private writeV3StreamText;
@@ -67,6 +67,8 @@ export class ChatCommand {
67
67
  retryPromptStreak = 0;
68
68
  v3SuppressThinkingStream = false;
69
69
  v3StreamedAnswerBuffer = '';
70
+ v3SeenToolCalls = new Set();
71
+ v3SeenToolResults = new Set();
70
72
  // Last completed Agent run — used by /retry, /continue, and the final summary block.
71
73
  lastAgentRunOutcome = null;
72
74
  isJwtExpirationError(error) {
@@ -262,6 +264,10 @@ export class ChatCommand {
262
264
  shouldRescueFailedAnalysis() {
263
265
  return process.env.VIGTHORIA_DISABLE_ANALYSIS_RESCUE !== '1';
264
266
  }
267
+ isDeepAnalysisPrompt(prompt) {
268
+ return /\b(deep|deeper|full|complete|thorough|systematic|architecture|architectural)\s+(analyse|analyze|analysis|review|dive|scan)\b/i.test(prompt)
269
+ || /\b(analyse|analyze|analysis|review)\b[\s\S]{0,40}\b(deep|deeper|full|complete|thorough|systematic|architecture|architectural)\b/i.test(prompt);
270
+ }
265
271
  async tryAnalysisLocalRescue(prompt, workspacePath) {
266
272
  if (!this.shouldRescueFailedAnalysis() || !this.tools) {
267
273
  return false;
@@ -702,6 +708,33 @@ export class ChatCommand {
702
708
  }
703
709
  return 'Working';
704
710
  }
711
+ terminalColumns() {
712
+ const raw = Number(process.stdout.columns || process.stderr.columns || 80);
713
+ return Number.isFinite(raw) && raw > 0 ? raw : 80;
714
+ }
715
+ fitTerminalText(text, reserve = 0) {
716
+ const width = Math.max(24, this.terminalColumns() - reserve);
717
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
718
+ const visible = clean.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
719
+ if (visible.length <= width)
720
+ return clean;
721
+ return `${visible.slice(0, Math.max(1, width - 1))}…`;
722
+ }
723
+ normalizeToolOutputForDisplay(output) {
724
+ return String(output || '')
725
+ .replace(/\/r\/n|\/n|\\n/g, '\n')
726
+ .replace(/\/r|\\r/g, '')
727
+ .replace(/\t/g, ' ');
728
+ }
729
+ toolEventKey(event) {
730
+ const name = String(event?.name || event?.tool || event?.tool_name || '').trim();
731
+ const args = event?.arguments && typeof event.arguments === 'object' ? event.arguments : {};
732
+ const target = String(args.path || args.file_path || args.pattern || args.command || event?.target || '').trim();
733
+ if (target)
734
+ return `${name}:${target}`;
735
+ const output = typeof event?.output === 'string' ? event.output.replace(/\s+/g, ' ').slice(0, 160) : '';
736
+ return `${name}:${output}`;
737
+ }
705
738
  isRawV3StreamPayload(event) {
706
739
  if (!event) {
707
740
  return false;
@@ -784,6 +817,9 @@ export class ChatCommand {
784
817
  .replace(/^\s*(?:list_dir|read_file|write_file|edit_file|glob|grep|bash)\s*$/gim, '')
785
818
  .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
786
819
  .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
820
+ .replace(/<read_file>\s*<path>[\s\S]*?<\/path>\s*<\/read_file>/gi, '')
821
+ .replace(/<(?:grep|list_directory|search_files|glob|tool_result)\b[\s\S]*?<\/(?:grep|list_directory|search_files|glob|tool_result)>/gi, '')
822
+ .replace(/bootstrap discovery completed\s*[—-]\s*inspect the most relevant files next\.?/gi, '')
787
823
  .replace(/\n{3,}/g, '\n\n');
788
824
  return output;
789
825
  }
@@ -807,15 +843,20 @@ export class ChatCommand {
807
843
  }
808
844
  this.v3LastActivity = Date.now();
809
845
  if (event.type === 'tool_call') {
846
+ const eventKey = this.toolEventKey(event);
847
+ if (this.v3SeenToolCalls.has(eventKey)) {
848
+ return;
849
+ }
850
+ this.v3SeenToolCalls.add(eventKey);
810
851
  this.v3ToolCallCount += 1;
811
852
  const toolDesc = this.describeV3AgentTool(event.tool || event.name || event.tool_name);
812
853
  const toolTarget = event.arguments?.path || event.arguments?.file_path || event.arguments?.pattern || '';
813
854
  const sanitizedTarget = this.sanitizeServerPath(String(toolTarget));
814
- const shortTarget = sanitizedTarget ? ` ${sanitizedTarget.replace(/\\/g, '/').split('/').slice(-2).join('/')}` : '';
855
+ const shortTarget = sanitizedTarget ? ` -> ${sanitizedTarget.replace(/\\/g, '/').split('/').slice(-2).join('/')}` : '';
815
856
  const stepLabel = chalk.cyan(` [${this.v3ToolCallCount}]`) + ` ${toolDesc}${shortTarget}`;
816
857
  if (spinner.isSpinning)
817
858
  spinner.stop();
818
- this.writeAgentActivityLine(stepLabel + '\n');
859
+ this.writeAgentActivityLine(this.fitTerminalText(stepLabel) + '\n');
819
860
  // Show extra detail for key tools
820
861
  const args = event.arguments || {};
821
862
  const toolName = event.name || event.tool || '';
@@ -825,16 +866,21 @@ export class ChatCommand {
825
866
  }
826
867
  else if (toolName === 'bash' && typeof args.command === 'string') {
827
868
  const command = this.sanitizeServerPath(args.command);
828
- this.writeAgentActivityLine(chalk.gray(` $ ${command.slice(0, 120)}${command.length > 120 ? '…' : ''}\n`));
869
+ this.writeAgentActivityLine(chalk.gray(` ${this.fitTerminalText(`$ ${command}`, 6)}\n`));
829
870
  }
830
871
  else if (toolName === 'read_file') {
831
- this.writeAgentActivityLine(chalk.gray(` reading ${sanitizedTarget || 'file'}\n`));
872
+ this.writeAgentActivityLine(chalk.gray(` ${this.fitTerminalText(`reading ${sanitizedTarget || 'file'}`, 6)}\n`));
832
873
  }
833
- spinner.start();
834
874
  spinner.text = `Running ${toolDesc}...`;
835
875
  return;
836
876
  }
837
877
  if (event.type === 'tool_result') {
878
+ const eventKey = this.toolEventKey(event);
879
+ if (eventKey && this.v3SeenToolResults.has(eventKey)) {
880
+ return;
881
+ }
882
+ if (eventKey)
883
+ this.v3SeenToolResults.add(eventKey);
838
884
  const success = event.success !== false;
839
885
  const toolName = event.name || event.tool || '';
840
886
  const indicator = success ? chalk.green(' ✓') : chalk.red(' ✗');
@@ -843,27 +889,30 @@ export class ChatCommand {
843
889
  this.writeAgentActivityLine(`${indicator} ${toolName}\n`);
844
890
  // Show output for failures, or brief summary for successes — sanitize server paths
845
891
  const rawOutput = typeof event.output === 'string' ? event.output.trim() : '';
846
- const output = this.sanitizeServerPath(rawOutput);
892
+ const output = this.normalizeToolOutputForDisplay(this.sanitizeServerPath(rawOutput));
847
893
  if (!success && output) {
848
894
  const sanitizedError = this.sanitizeServerPath(typeof event.error === 'string' ? event.error : output);
849
895
  const lines = sanitizedError.split('\n').slice(0, 4);
850
- this.writeAgentActivityLine(chalk.red(` ${lines.join('\n ')}\n`));
896
+ this.writeAgentActivityLine(chalk.red(`${lines.map((line) => ` ${this.fitTerminalText(line, 6)}`).join('\n')}\n`));
851
897
  }
852
898
  else if (success && output && output.length > 0) {
853
899
  if (toolName === 'read_file' || toolName === 'grep' || toolName === 'search_files' || toolName === 'list_directory') {
854
900
  const lineCount = output.split('\n').length;
855
- const preview = output.split('\n').slice(0, 3).join('\n');
901
+ const preview = output.split('\n')
902
+ .map((line) => line.trim())
903
+ .filter((line) => line && !line.startsWith('.vigthoria/memory/brain.json:'))
904
+ .slice(0, 3)
905
+ .join('\n');
856
906
  this.writeAgentActivityLine(chalk.gray(` ${lineCount} line${lineCount === 1 ? '' : 's'} returned\n`));
857
907
  if (preview && preview.length > 20 && !/^\(no matches\)$/i.test(preview.trim())) {
858
- this.writeAgentActivityLine(chalk.gray(`${preview.split('\n').map((l) => ` ${l}`).join('\n')}\n`));
908
+ this.writeAgentActivityLine(chalk.gray(`${preview.split('\n').map((line) => ` ${this.fitTerminalText(line, 6)}`).join('\n')}\n`));
859
909
  }
860
910
  }
861
911
  else {
862
- const brief = output.split('\n')[0].slice(0, 120);
863
- this.writeAgentActivityLine(chalk.gray(` ${brief}${output.length > 120 ? '…' : ''}\n`));
912
+ const brief = output.split('\n')[0];
913
+ this.writeAgentActivityLine(chalk.gray(` ${this.fitTerminalText(brief, 6)}\n`));
864
914
  }
865
915
  }
866
- spinner.start();
867
916
  spinner.text = 'Next step...';
868
917
  return;
869
918
  }
@@ -879,8 +928,7 @@ export class ChatCommand {
879
928
  const display = iterText && !/read-only analysis turn|run at least one discovery tool/i.test(iterText)
880
929
  ? iterText
881
930
  : `Step ${this.v3IterationCount}: inspecting workspace…`;
882
- this.writeAgentActivityLine(chalk.cyan(`\n── ${display} ──\n`));
883
- spinner.start();
931
+ this.writeAgentActivityLine(chalk.cyan(`\n── ${this.fitTerminalText(display, 6)} ──\n`));
884
932
  spinner.text = 'Analyzing...';
885
933
  return;
886
934
  }
@@ -913,13 +961,13 @@ export class ChatCommand {
913
961
  if (fail > 0)
914
962
  statLine += chalk.yellow(`, ${fail} failed`);
915
963
  }
916
- process.stderr.write(chalk.green(`\n✓ Complete`) + ` ${statLine}\n`);
964
+ process.stdout.write(chalk.green(`\n✓ Complete`) + ` - ${this.fitTerminalText(statLine, 14)}\n`);
917
965
  // Show seal quality score if available
918
966
  if (event.seal_score && typeof event.seal_score.overall === 'number') {
919
967
  const score = event.seal_score.overall;
920
968
  const tier = event.seal_score.tier || '';
921
969
  const scoreColor = score >= 7 ? chalk.green : score >= 5 ? chalk.yellow : chalk.red;
922
- process.stderr.write(chalk.cyan(' [Quality] ') + scoreColor(`${score}/10`) + (tier ? chalk.gray(` (${tier})`) : '') + '\n');
970
+ process.stdout.write(chalk.cyan(' [Quality] ') + scoreColor(`${score}/10`) + (tier ? chalk.gray(` (${tier})`) : '') + '\n');
923
971
  }
924
972
  return;
925
973
  }
@@ -936,35 +984,31 @@ export class ChatCommand {
936
984
  }
937
985
  if (spinner.isSpinning)
938
986
  spinner.stop();
939
- process.stderr.write(chalk.cyan(` [Plan] `) + `Task: ${planKind || 'analyzing'}`);
940
- if (quality)
941
- process.stderr.write(chalk.gray(` (${quality})`));
942
- process.stderr.write('\n');
987
+ process.stdout.write(chalk.cyan(` [Plan] `) + this.fitTerminalText(`Task: ${planKind || 'analyzing'}${quality ? ` (${quality})` : ''}`, 9) + '\n');
943
988
  if (summary) {
944
- process.stderr.write(chalk.gray(` ${summary}\n`));
989
+ process.stdout.write(chalk.gray(` ${this.fitTerminalText(summary, 6)}\n`));
945
990
  }
946
991
  if (status === 'planning' && Number.isFinite(Number(plan.elapsed_seconds))) {
947
- process.stderr.write(chalk.gray(` elapsed: ${plan.elapsed_seconds}s\n`));
992
+ process.stdout.write(chalk.gray(` elapsed: ${plan.elapsed_seconds}s\n`));
948
993
  }
949
994
  if (Array.isArray(plan.tasks) && plan.tasks.length > 0) {
950
- process.stderr.write(chalk.gray(` ${plan.total_tasks || plan.tasks.length} tasks:\n`));
995
+ process.stdout.write(chalk.gray(` ${plan.total_tasks || plan.tasks.length} tasks:\n`));
951
996
  for (const t of plan.tasks.slice(0, 10)) {
952
997
  const targets = Array.isArray(t.targets) && t.targets.length ? ` → ${t.targets.map((target) => this.sanitizeServerPath(String(target))).join(', ')}` : '';
953
- process.stderr.write(chalk.gray(` ${this.sanitizeServerPath(String(t.title || t.id))}${targets}\n`));
998
+ process.stdout.write(chalk.gray(` - ${this.fitTerminalText(`${this.sanitizeServerPath(String(t.title || t.id))}${targets}`, 10)}\n`));
954
999
  }
955
1000
  if (plan.tasks.length > 10) {
956
- process.stderr.write(chalk.gray(` ... and ${plan.tasks.length - 10} more\n`));
1001
+ process.stdout.write(chalk.gray(` ... and ${plan.tasks.length - 10} more\n`));
957
1002
  }
958
1003
  }
959
1004
  if (Array.isArray(plan.notes) && plan.notes.length > 0) {
960
1005
  for (const note of plan.notes.slice(0, 3)) {
961
- process.stderr.write(chalk.gray(` note: ${this.sanitizeServerPath(String(note))}\n`));
1006
+ process.stdout.write(chalk.gray(` note: ${this.fitTerminalText(this.sanitizeServerPath(String(note)), 12)}\n`));
962
1007
  }
963
1008
  }
964
1009
  if (Array.isArray(plan.target_files) && plan.target_files.length > 0) {
965
- process.stderr.write(chalk.gray(` Files: ${plan.target_files.map((filePath) => this.sanitizeServerPath(String(filePath))).join(', ')}\n`));
1010
+ process.stdout.write(chalk.gray(` ${this.fitTerminalText(`Files: ${plan.target_files.map((filePath) => this.sanitizeServerPath(String(filePath))).join(', ')}`, 6)}\n`));
966
1011
  }
967
- spinner.start();
968
1012
  spinner.text = status === 'planning' ? 'Planning...' : 'Executing plan...';
969
1013
  return;
970
1014
  }
@@ -1187,7 +1231,7 @@ export class ChatCommand {
1187
1231
  if (sessionConfig.debugMode) {
1188
1232
  process.env.DEBUG = process.env.DEBUG || 'vigthoria:*';
1189
1233
  }
1190
- console.log(chalk.green(`Using workspace: ${this.currentProjectPath}`));
1234
+ console.log(chalk.green(this.fitTerminalText(`Using workspace: ${this.currentProjectPath}`)));
1191
1235
  }
1192
1236
  if (this.jsonOutput && !options.prompt && !options.retry && !options.continue) {
1193
1237
  throw new Error('--json is only supported together with --prompt, --retry, or --continue.');
@@ -1988,6 +2032,7 @@ export class ChatCommand {
1988
2032
  buildLocalLoopLiveOutcome(prompt) {
1989
2033
  const liveOutcome = createLiveOutcome();
1990
2034
  liveOutcome.requiresWorkspaceChanges = this.taskRequiresWorkspaceChanges(prompt);
2035
+ liveOutcome.deepAnalysisRequired = this.isDeepAnalysisPrompt(prompt);
1991
2036
  liveOutcome.analysisToolsUsed = this.agentToolEvidence.discovery;
1992
2037
  liveOutcome.changedFileCount = this.agentToolEvidence.mutation;
1993
2038
  liveOutcome.workspaceHasOutput = this.agentToolEvidence.mutation > 0;
@@ -2500,11 +2545,14 @@ export class ChatCommand {
2500
2545
  this.v3LastActivity = Date.now();
2501
2546
  this.v3StreamingStarted = false;
2502
2547
  this.v3StreamedAnswerBuffer = '';
2503
- const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], !this.jsonOutput);
2548
+ this.v3SeenToolCalls.clear();
2549
+ this.v3SeenToolResults.clear();
2550
+ const taskDisplay = new TaskDisplay(['Analyse workspace', 'Execute tasks', 'Validate output', 'Self-heal'], false);
2504
2551
  taskDisplay.start(0);
2505
2552
  const spinner = this.jsonOutput ? null : createSpinner({
2506
2553
  text: routingPolicy.cloudSelected ? 'Routing heavy task to Vigthoria Cloud...' : 'Routing to V3 Agent...',
2507
2554
  spinner: 'clock',
2555
+ isSilent: true,
2508
2556
  }).start();
2509
2557
  // Live run telemetry, used both for the final summary block and for /retry, /continue.
2510
2558
  const liveOutcome = {
@@ -2514,6 +2562,7 @@ export class ChatCommand {
2514
2562
  qualityMissing: [],
2515
2563
  qualityBlockers: [],
2516
2564
  };
2565
+ liveOutcome.deepAnalysisRequired = this.isDeepAnalysisPrompt(prompt);
2517
2566
  const parsePlannerSummary = (raw) => {
2518
2567
  if (!raw || typeof raw !== 'string')
2519
2568
  return;
@@ -2648,6 +2697,12 @@ export class ChatCommand {
2648
2697
  if (Number(event.discovery_tools_used) > 0) {
2649
2698
  liveOutcome.analysisToolsUsed = Math.max(liveOutcome.analysisToolsUsed, Number(event.discovery_tools_used));
2650
2699
  }
2700
+ if (Number(event.discovery_read_files) > 0) {
2701
+ liveOutcome.analysisReadToolsUsed = Math.max(liveOutcome.analysisReadToolsUsed, Number(event.discovery_read_files));
2702
+ }
2703
+ if (event.analysis_deep_required === true) {
2704
+ liveOutcome.deepAnalysisRequired = true;
2705
+ }
2651
2706
  taskDisplay.complete(1);
2652
2707
  }
2653
2708
  else if (event.type === 'executor_error') {
@@ -2759,6 +2814,9 @@ export class ChatCommand {
2759
2814
  const runEvaluation = evaluateExecutorSuccess(liveOutcome);
2760
2815
  let executorSucceeded = runEvaluation.executorSucceeded;
2761
2816
  let finalAnswer = answerContent;
2817
+ if (isToolEvidenceStubAnswer(finalAnswer)) {
2818
+ finalAnswer = '';
2819
+ }
2762
2820
  const needsAnalysisRescue = !requiresWorkspaceChanges && !this.jsonOutput
2763
2821
  && (!executorSucceeded || !isSubstantiveAgentAnswer(finalAnswer));
2764
2822
  if (needsAnalysisRescue) {
@@ -2876,7 +2934,7 @@ export class ChatCommand {
2876
2934
  qualityMissing: liveOutcome.qualityMissing,
2877
2935
  qualityBlockers: liveOutcome.qualityBlockers,
2878
2936
  hasOutput: workspaceHasOutput,
2879
- answerContent: answerContent || null,
2937
+ answerContent: finalAnswer || null,
2880
2938
  selfHealStatus,
2881
2939
  selfHealTool,
2882
2940
  plannerError: liveOutcome.plannerError ? sanitizeUserFacingErrorText(liveOutcome.plannerError) : null,
@@ -2904,7 +2962,7 @@ export class ChatCommand {
2904
2962
  taskId: response.taskId || null,
2905
2963
  contextId: response.contextId || null,
2906
2964
  partial: response.partial === true || (!executorSucceeded && liveOutcome.tasksSucceeded > 0),
2907
- content: answerContent || response.content || runEvaluation.statusHeadline,
2965
+ content: finalAnswer || runEvaluation.statusHeadline,
2908
2966
  statusHeadline: runEvaluation.statusHeadline,
2909
2967
  tasksSucceeded: liveOutcome.tasksSucceeded,
2910
2968
  tasksTotal: liveOutcome.tasksTotal,
@@ -2920,7 +2978,7 @@ export class ChatCommand {
2920
2978
  },
2921
2979
  }, null, 2));
2922
2980
  }
2923
- this.messages.push({ role: 'assistant', content: answerContent || response.content || runEvaluation.statusHeadline });
2981
+ this.messages.push({ role: 'assistant', content: finalAnswer || runEvaluation.statusHeadline });
2924
2982
  watcher?.stop();
2925
2983
  return true;
2926
2984
  }
@@ -3087,10 +3145,10 @@ export class ChatCommand {
3087
3145
  ? 'Interactive Agent Chat'
3088
3146
  : 'Interactive Chat';
3089
3147
  this.logger.section(this.workflowTarget ? `${chatTitle} Via Workflow Target` : chatTitle);
3090
- console.log(chalk.gray('Type /help for commands. Type /exit to quit.'));
3091
- console.log(chalk.gray('Multi-line: end a line with \\ or start a block with {{{ and end with }}}'));
3148
+ console.log(chalk.gray(this.fitTerminalText('Type /help for commands. Type /exit to quit.')));
3149
+ console.log(chalk.gray(this.fitTerminalText('Multi-line: end a line with \\ or start a block with {{{ and end with }}}')));
3092
3150
  if (this.workflowTarget) {
3093
- console.log(chalk.gray(`Workflow target: ${this.workflowTarget}`));
3151
+ console.log(chalk.gray(this.fitTerminalText(`Workflow target: ${this.workflowTarget}`)));
3094
3152
  }
3095
3153
  const rl = readline.createInterface({
3096
3154
  input: process.stdin,
package/dist/index.js CHANGED
@@ -567,22 +567,34 @@ export async function main(args) {
567
567
  return;
568
568
  }
569
569
  }
570
- // Banner - Fixed alignment with proper padding
571
- const boxWidth = 61; // Inner content width
572
- const titleText = 'VIGTHORIA CLI - AI-Powered Coding Assistant';
573
- const versionText = `Version ${VERSION}`;
574
- // Calculate padding for centering
575
- const titlePad = Math.floor((boxWidth - 4 - titleText.length) / 2);
576
- const versionPad = Math.floor((boxWidth - 4 - versionText.length) / 2);
577
570
  if (process.env.VIGTHORIA_NO_BANNER !== '1' &&
578
571
  !jsonOutputRequested &&
579
572
  !directPromptRequested &&
580
573
  !helpOrVersionRequested) {
574
+ const terminalWidth = Math.max(36, Number(process.stdout.columns || 80));
575
+ const boxWidth = Math.max(34, Math.min(61, terminalWidth - 2));
576
+ const innerWidth = boxWidth;
577
+ const fit = (text) => {
578
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
579
+ return clean.length > innerWidth ? `${clean.slice(0, Math.max(1, innerWidth - 1))}…` : clean;
580
+ };
581
+ const center = (text, colour = (value) => value) => {
582
+ const fitted = fit(text);
583
+ const left = Math.max(0, Math.floor((innerWidth - fitted.length) / 2));
584
+ const right = Math.max(0, innerWidth - fitted.length - left);
585
+ return chalk.cyan(CH.dV) + ' '.repeat(left) + colour(fitted) + ' '.repeat(right) + chalk.cyan(CH.dV);
586
+ };
587
+ const titleLine = boxWidth < 48 ? 'VIGTHORIA CLI' : 'VIGTHORIA CLI - AI Coding Assistant';
588
+ const subtitleLine = boxWidth < 48 ? 'AI Coding Assistant' : `Version ${VERSION}`;
589
+ const versionLine = boxWidth < 48 ? `Version ${VERSION}` : '';
581
590
  console.log(chalk.cyan(CH.dTl + CH.dH.repeat(boxWidth) + CH.dTr));
582
- console.log(chalk.cyan(CH.dV + ' '.repeat(boxWidth) + CH.dV));
583
- console.log(chalk.cyan(CH.dV) + ' '.repeat(titlePad) + chalk.bold.white('VIGTHORIA CLI') + chalk.cyan(' - AI-Powered Coding Assistant') + ' '.repeat(boxWidth - titlePad - titleText.length) + chalk.cyan(CH.dV));
584
- console.log(chalk.cyan(CH.dV) + ' '.repeat(versionPad) + chalk.gray(versionText) + ' '.repeat(boxWidth - versionPad - versionText.length) + chalk.cyan(CH.dV));
585
- console.log(chalk.cyan(CH.dV + ' '.repeat(boxWidth) + CH.dV));
591
+ console.log(center(''));
592
+ console.log(center(titleLine, (value) => chalk.bold.white(value)));
593
+ console.log(center(subtitleLine, (value) => chalk.gray(value)));
594
+ if (versionLine) {
595
+ console.log(center(versionLine, (value) => chalk.gray(value)));
596
+ }
597
+ console.log(center(''));
586
598
  console.log(chalk.cyan(CH.dBl + CH.dH.repeat(boxWidth) + CH.dBr));
587
599
  console.log();
588
600
  }
@@ -18,6 +18,8 @@ export interface LiveOutcome {
18
18
  analysisReadToolsUsed: number;
19
19
  /** Final user-facing answer text (stream + response body) */
20
20
  answerContent?: string;
21
+ /** User asked for deep/project-scale analysis, so shallow evidence must not pass. */
22
+ deepAnalysisRequired?: boolean;
21
23
  streamAborted?: boolean;
22
24
  }
23
25
  export interface TaskSummaryEvent {
@@ -31,7 +33,7 @@ export interface RunEvaluation {
31
33
  uiTheme: 'success' | 'warning' | 'error';
32
34
  }
33
35
  export declare function createLiveOutcome(): LiveOutcome;
34
- /** Tool-trace stubs like `[read_file] "index.html"` — not a user-facing answer. */
36
+ /** Tool-trace stubs like `[read_file] "index.html"` or `<read_file>…</read_file>` — not a user-facing answer. */
35
37
  export declare function isToolEvidenceStubAnswer(text: string): boolean;
36
38
  /** True when text looks like a real answer, not an executor/system placeholder. */
37
39
  export declare function isSubstantiveAgentAnswer(text: string): boolean;
@@ -17,10 +17,12 @@ export function createLiveOutcome() {
17
17
  analysisToolsUsed: 0,
18
18
  analysisReadToolsUsed: 0,
19
19
  answerContent: '',
20
+ deepAnalysisRequired: false,
20
21
  streamAborted: false,
21
22
  };
22
23
  }
23
24
  const EXECUTOR_PLACEHOLDER_RE = /read-only analysis turn|run at least one discovery tool|reviewing attached files and workspace findings/i;
25
+ const BOOTSTRAP_PLACEHOLDER_RE = /bootstrap discovery completed|inspect the most relevant files next|reviewing attached files and workspace findings/i;
24
26
  const LIST_ONLY_DISCOVERY_TOOLS = new Set([
25
27
  'list_directory',
26
28
  'list_dir',
@@ -28,19 +30,53 @@ const LIST_ONLY_DISCOVERY_TOOLS = new Set([
28
30
  'dir',
29
31
  ]);
30
32
  const GENERIC_SUMMARY_RE = /^(completed the requested analysis|reviewed the workspace without writing changes|task completed|done|finished|analysis completed)([.\s]|$)/i;
31
- /** Tool-trace stubs like `[read_file] "index.html"` — not a user-facing answer. */
33
+ function looksLikeRawV3EventPayload(text) {
34
+ const trimmed = String(text || '').trim();
35
+ if (!/^[{[]/.test(trimmed))
36
+ return false;
37
+ try {
38
+ const parsed = JSON.parse(trimmed);
39
+ const root = Array.isArray(parsed) ? { events: parsed } : parsed;
40
+ if (!root || typeof root !== 'object')
41
+ return false;
42
+ if (Array.isArray(root.events) && root.events.some((event) => event && typeof event === 'object' && typeof event.type === 'string')) {
43
+ return true;
44
+ }
45
+ return Boolean(root.context_id
46
+ || root.mcp_context_id
47
+ || root.workspace_uri
48
+ || root.execution_surface
49
+ || root.task_id);
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /** Tool-trace stubs like `[read_file] "index.html"` or `<read_file>…</read_file>` — not a user-facing answer. */
32
56
  export function isToolEvidenceStubAnswer(text) {
33
57
  const trimmed = String(text || '').trim();
34
58
  if (!trimmed)
35
59
  return true;
60
+ if (looksLikeRawV3EventPayload(trimmed))
61
+ return true;
62
+ if (BOOTSTRAP_PLACEHOLDER_RE.test(trimmed))
63
+ return true;
36
64
  if (GENERIC_SUMMARY_RE.test(trimmed.replace(/\.\s*\d+\s+tool steps.*$/i, '').trim()))
37
65
  return true;
38
66
  const lines = trimmed.split('\n').map((line) => line.trim()).filter(Boolean);
39
67
  if (lines.length === 0)
40
68
  return true;
41
69
  const stubLine = /^\[(?:read_file|grep|list_directory|search_files|glob|tool_call)\]/i;
70
+ const xmlToolLine = /^<\/?(?:read_file|grep|list_directory|search_files|glob|tool_call|tool_result|path)\b[^>]*>/i;
71
+ const xmlToolBlockOnly = trimmed
72
+ .replace(/<read_file>\s*<path>[\s\S]*?<\/path>\s*<\/read_file>/gi, '')
73
+ .replace(/<(?:grep|list_directory|search_files|glob|tool_call|tool_result)\b[\s\S]*?<\/(?:grep|list_directory|search_files|glob|tool_call|tool_result)>/gi, '')
74
+ .replace(/\s+/g, '')
75
+ .trim();
76
+ if (!xmlToolBlockOnly)
77
+ return true;
42
78
  const filesOnly = /^Files inspected:/i;
43
- const stubLines = lines.filter((line) => stubLine.test(line) || filesOnly.test(line)).length;
79
+ const stubLines = lines.filter((line) => stubLine.test(line) || xmlToolLine.test(line) || filesOnly.test(line)).length;
44
80
  if (stubLines === lines.length)
45
81
  return true;
46
82
  if (stubLines > 0 && stubLines / lines.length >= 0.6 && trimmed.length < 400)
@@ -101,6 +137,7 @@ const READ_DISCOVERY_TOOLS = new Set([
101
137
  'grep',
102
138
  'list_directory',
103
139
  'glob',
140
+ 'search_files',
104
141
  'search_project',
105
142
  'list_dir',
106
143
  'dir',
@@ -171,6 +208,10 @@ export function evaluateExecutorSuccess(liveOutcome) {
171
208
  const answerText = normalizeAgentAnswerContent(liveOutcome.answerContent);
172
209
  const hasSubstantiveAnswer = isSubstantiveAgentAnswer(answerText);
173
210
  const hasDeepRead = liveOutcome.analysisReadToolsUsed > 0;
211
+ const meetsDeepAnalysisDepth = !liveOutcome.deepAnalysisRequired
212
+ || (liveOutcome.analysisToolsUsed >= 4
213
+ && liveOutcome.analysisReadToolsUsed >= 3
214
+ && answerText.length >= 700);
174
215
  if (hasFatalError && !hasSubstantiveAnswer) {
175
216
  return {
176
217
  executorSucceeded: false,
@@ -199,6 +240,13 @@ export function evaluateExecutorSuccess(liveOutcome) {
199
240
  uiTheme: 'warning',
200
241
  };
201
242
  }
243
+ if (!meetsDeepAnalysisDepth) {
244
+ return {
245
+ executorSucceeded: false,
246
+ statusHeadline: '⚠ Deep analysis was too shallow — retrying needs a project map, multiple file reads, and a structured report',
247
+ uiTheme: 'warning',
248
+ };
249
+ }
202
250
  return {
203
251
  executorSucceeded: true,
204
252
  statusHeadline: '✓ Analysis completed',
package/dist/utils/api.js CHANGED
@@ -3358,11 +3358,15 @@ document.addEventListener('DOMContentLoaded', () => {
3358
3358
  if (Array.isArray(data?.events)) {
3359
3359
  const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim());
3360
3360
  if (completionEvent) {
3361
- return sanitizeUserFacingPathText(completionEvent.summary);
3361
+ const text = this.sanitizeV3AgentResponseText(completionEvent.summary);
3362
+ if (isSubstantiveAgentAnswer(text))
3363
+ return text;
3362
3364
  }
3363
3365
  const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string' && event.content.trim());
3364
3366
  if (messageEvent) {
3365
- return this.sanitizeV3AgentResponseText(messageEvent.content);
3367
+ const text = this.sanitizeV3AgentResponseText(messageEvent.content);
3368
+ if (isSubstantiveAgentAnswer(text))
3369
+ return text;
3366
3370
  }
3367
3371
  // Synthesize a grounded answer from the tool-call evidence the
3368
3372
  // agent produced, rather than dumping the raw event trace.
@@ -3376,8 +3380,7 @@ document.addEventListener('DOMContentLoaded', () => {
3376
3380
  const fileList = Object.keys(data.files).join(', ');
3377
3381
  return `Agent wrote workspace files: ${sanitizeUserFacingPathText(fileList)}`;
3378
3382
  }
3379
- const text = sanitizeUserFacingPathText(JSON.stringify(data, null, 2));
3380
- return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
3383
+ return '';
3381
3384
  }
3382
3385
  sanitizeV3AgentResponseText(input) {
3383
3386
  let text = sanitizeUserFacingPathText(String(input || ''));
@@ -3387,6 +3390,9 @@ document.addEventListener('DOMContentLoaded', () => {
3387
3390
  .replace(/<thinking>[\s\S]*?<\/thinking>/gi, '')
3388
3391
  .replace(/<tool_call>[\s\S]*?<\/tool_call>/gi, '')
3389
3392
  .replace(/<tool_code>[\s\S]*?<\/tool_code>/gi, '')
3393
+ .replace(/<read_file>\s*<path>[\s\S]*?<\/path>\s*<\/read_file>/gi, '')
3394
+ .replace(/<(?:grep|list_directory|search_files|glob|tool_result)\b[\s\S]*?<\/(?:grep|list_directory|search_files|glob|tool_result)>/gi, '')
3395
+ .replace(/bootstrap discovery completed\s*[—-]\s*inspect the most relevant files next\.?/gi, '')
3390
3396
  .replace(/```json\s*\[\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*\]\s*```/gi, '')
3391
3397
  .replace(/```json\s*\{[\s\S]*?"tool"[\s\S]*?\}\s*```/gi, '')
3392
3398
  .replace(/^\s*(?:json\s*)?\[\s*\{[\s\S]*?"tool"[\s\S]*$/gim, '')
@@ -162,8 +162,13 @@ export class Logger {
162
162
  }
163
163
  // Section header
164
164
  section(title) {
165
+ const width = Math.max(24, Number(process.stdout.columns || 80));
166
+ const cleanTitle = String(title || '').replace(/\s+/g, ' ').trim();
167
+ const maxTitle = Math.max(10, width - 10);
168
+ const fittedTitle = cleanTitle.length > maxTitle ? `${cleanTitle.slice(0, maxTitle - 1)}…` : cleanTitle;
169
+ const side = Math.max(3, Math.floor((Math.min(width, 72) - fittedTitle.length - 2) / 2));
165
170
  console.log();
166
- console.log(chalk.bold.cyan(`${CH.hDouble.repeat(3)} ${title} ${CH.hDouble.repeat(3)}`));
171
+ console.log(chalk.bold.cyan(`${CH.hDouble.repeat(side)} ${fittedTitle} ${CH.hDouble.repeat(side)}`));
167
172
  console.log();
168
173
  }
169
174
  // Progress
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.11.7",
3
+ "version": "1.11.9",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",