vigthoria-cli 1.6.9 → 1.6.13

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.
@@ -40,6 +40,7 @@ exports.ChatCommand = void 0;
40
40
  const chalk_1 = __importDefault(require("chalk"));
41
41
  const ora_1 = __importDefault(require("ora"));
42
42
  const fs = __importStar(require("fs"));
43
+ const os = __importStar(require("os"));
43
44
  const path = __importStar(require("path"));
44
45
  const readline = __importStar(require("readline"));
45
46
  const api_js_1 = require("../utils/api.js");
@@ -64,11 +65,29 @@ class ChatCommand {
64
65
  directPromptMode = false;
65
66
  directToolContinuationCount = 0;
66
67
  currentModel = 'code';
68
+ modelExplicitlySelected = false;
67
69
  autoApprove = false;
68
70
  operatorMode = false;
69
71
  workflowTarget = null;
70
72
  savePlanToVigFlow = false;
71
73
  jsonOutput = false;
74
+ syncInteractiveModeModel(enabledMode) {
75
+ if (enabledMode === 'agent') {
76
+ if (!this.modelExplicitlySelected || this.currentModel === 'code' || !this.currentModel) {
77
+ this.currentModel = 'agent';
78
+ }
79
+ return;
80
+ }
81
+ if (enabledMode === 'operator') {
82
+ if (this.currentModel === 'agent' || !this.currentModel) {
83
+ this.currentModel = 'code-8b';
84
+ }
85
+ return;
86
+ }
87
+ if (this.currentModel === 'agent' || this.currentModel === 'code-8b' || !this.currentModel) {
88
+ this.currentModel = 'code';
89
+ }
90
+ }
72
91
  hasOperatorAccess() {
73
92
  return this.config.hasOperatorAccess();
74
93
  }
@@ -76,6 +95,73 @@ class ChatCommand {
76
95
  const currentPlan = this.config.get('subscription').plan || 'free';
77
96
  return `Operator mode requires Enterprise or admin access. Current plan: ${currentPlan}.`;
78
97
  }
98
+ getDefaultChatModel() {
99
+ const preferredModel = String(this.config.get('preferences').defaultModel || '').trim().toLowerCase();
100
+ if (!preferredModel || preferredModel === 'vigthoria-code' || preferredModel === 'vigthoria-agent') {
101
+ return 'code';
102
+ }
103
+ return preferredModel;
104
+ }
105
+ resolveInitialModel(options) {
106
+ const requestedModel = String(options.model || '').trim();
107
+ if (requestedModel) {
108
+ return requestedModel;
109
+ }
110
+ if (options.operator === true) {
111
+ return 'code-8b';
112
+ }
113
+ if (options.agent === true) {
114
+ return 'agent';
115
+ }
116
+ return this.getDefaultChatModel();
117
+ }
118
+ isLegacyAgentFallbackAllowed() {
119
+ return process.env.VIGTHORIA_ALLOW_LEGACY_AGENT_FALLBACK === '1';
120
+ }
121
+ resolveAgentExecutionPolicy(prompt) {
122
+ const explicitModel = this.modelExplicitlySelected;
123
+ const heavyTask = this.config.isComplexTask(prompt);
124
+ const cloudEligible = this.config.hasCloudAccess() && this.hasOperatorAccess();
125
+ const requiresV3Workflow = this.shouldRequireV3AgentWorkflow(prompt);
126
+ if (explicitModel) {
127
+ return {
128
+ selectedModel: this.currentModel,
129
+ explicitModel: true,
130
+ heavyTask,
131
+ cloudEligible,
132
+ cloudSelected: this.currentModel === 'cloud' || this.currentModel === 'cloud-reason' || this.currentModel === 'ultra',
133
+ routeReason: 'explicit-model-selection',
134
+ };
135
+ }
136
+ if (requiresV3Workflow) {
137
+ return {
138
+ selectedModel: 'agent',
139
+ explicitModel: false,
140
+ heavyTask,
141
+ cloudEligible,
142
+ cloudSelected: false,
143
+ routeReason: 'v3-required-frontend-task',
144
+ };
145
+ }
146
+ if (this.config.shouldUseCloudForHeavyTask(prompt)) {
147
+ return {
148
+ selectedModel: 'cloud',
149
+ explicitModel: false,
150
+ heavyTask: true,
151
+ cloudEligible: true,
152
+ cloudSelected: true,
153
+ routeReason: 'heavy-enterprise-task',
154
+ };
155
+ }
156
+ return {
157
+ selectedModel: 'agent',
158
+ explicitModel: false,
159
+ heavyTask,
160
+ cloudEligible,
161
+ cloudSelected: false,
162
+ routeReason: 'default-v3-agent',
163
+ };
164
+ }
79
165
  getMessagesForModel() {
80
166
  const memoryContext = this.sessionManager.buildMemoryContext(this.currentSession);
81
167
  const messages = [...this.messages];
@@ -218,9 +304,10 @@ class ChatCommand {
218
304
  : null;
219
305
  this.savePlanToVigFlow = options.savePlan === true;
220
306
  this.jsonOutput = options.json === true;
221
- this.autoApprove = options.autoApprove === true;
222
- this.currentModel = options.model || 'code';
223
- this.currentProjectPath = path.resolve(options.project || process.cwd());
307
+ this.autoApprove = options.autoApprove === true || this.jsonOutput;
308
+ this.modelExplicitlySelected = Boolean(String(options.model || '').trim());
309
+ this.currentModel = this.resolveInitialModel(options);
310
+ this.currentProjectPath = this.resolveProjectPath(options);
224
311
  if (this.jsonOutput && !options.prompt) {
225
312
  throw new Error('--json is only supported together with --prompt.');
226
313
  }
@@ -253,6 +340,77 @@ class ChatCommand {
253
340
  }
254
341
  fs.mkdirSync(this.currentProjectPath, { recursive: true });
255
342
  }
343
+ resolveProjectPath(options) {
344
+ const requestedProject = String(options.project || '').trim();
345
+ if (requestedProject) {
346
+ return path.resolve(requestedProject);
347
+ }
348
+ if (this.shouldUseManagedWorkspace(options)) {
349
+ const rootPath = this.getManagedWorkspaceRoot();
350
+ fs.mkdirSync(rootPath, { recursive: true });
351
+ const folderName = this.resolveManagedWorkspaceName(options);
352
+ const projectPath = this.allocateManagedWorkspacePath(rootPath, folderName);
353
+ this.config.set('project', {
354
+ ...this.config.get('project'),
355
+ rootPath,
356
+ });
357
+ return projectPath;
358
+ }
359
+ return process.cwd();
360
+ }
361
+ shouldUseManagedWorkspace(options) {
362
+ if (options.projectProvided) {
363
+ return false;
364
+ }
365
+ if (options.newProject) {
366
+ return true;
367
+ }
368
+ return Boolean(options.prompt) && (options.agent === true || options.operator === true);
369
+ }
370
+ getManagedWorkspaceRoot() {
371
+ const envRoot = String(process.env.VIGTHORIA_DEFAULT_WORKSPACE_ROOT || '').trim();
372
+ const configuredRoot = String(this.config.get('project').rootPath || '').trim();
373
+ return path.resolve(envRoot || configuredRoot || path.join(os.homedir(), 'VigthoriaProjects'));
374
+ }
375
+ resolveManagedWorkspaceName(options) {
376
+ const explicitName = typeof options.newProject === 'string' ? options.newProject.trim() : '';
377
+ if (explicitName) {
378
+ return this.slugifyWorkspaceName(explicitName);
379
+ }
380
+ if (options.prompt) {
381
+ const fromPrompt = this.slugifyWorkspaceName(options.prompt.split(/[.!?\n]/)[0] || 'project');
382
+ if (fromPrompt) {
383
+ return fromPrompt;
384
+ }
385
+ }
386
+ return `project-${new Date().toISOString().replace(/[-:TZ.]/g, '').slice(0, 14)}`;
387
+ }
388
+ slugifyWorkspaceName(input) {
389
+ return String(input || '')
390
+ .toLowerCase()
391
+ .replace(/[^a-z0-9]+/g, '-')
392
+ .replace(/^-+|-+$/g, '')
393
+ .slice(0, 48) || 'project';
394
+ }
395
+ allocateManagedWorkspacePath(rootPath, folderName) {
396
+ const basePath = path.join(rootPath, folderName);
397
+ if (!fs.existsSync(basePath)) {
398
+ return basePath;
399
+ }
400
+ const existingEntries = fs.readdirSync(basePath);
401
+ if (existingEntries.length === 0) {
402
+ return basePath;
403
+ }
404
+ let counter = 2;
405
+ while (counter < 1000) {
406
+ const candidate = path.join(rootPath, `${folderName}-${counter}`);
407
+ if (!fs.existsSync(candidate)) {
408
+ return candidate;
409
+ }
410
+ counter += 1;
411
+ }
412
+ return path.join(rootPath, `${folderName}-${Date.now()}`);
413
+ }
256
414
  initializeSession(resume) {
257
415
  if (resume) {
258
416
  this.currentSession = this.sessionManager.getLatest(this.currentProjectPath);
@@ -469,16 +627,34 @@ class ChatCommand {
469
627
  if (!this.tools) {
470
628
  throw new Error('Agent tools are not initialized.');
471
629
  }
630
+ const requiresV3Workflow = this.shouldRequireV3AgentWorkflow(prompt);
472
631
  const handledByDirectFileFlow = await this.tryDirectSingleFileFlow(prompt);
473
632
  if (handledByDirectFileFlow) {
474
633
  this.saveSession();
475
634
  return;
476
635
  }
636
+ if (this.shouldPreferLocalAgentLoop(prompt)) {
637
+ await this.runLocalAgentLoop(prompt);
638
+ return;
639
+ }
477
640
  const handledByV3Workflow = await this.tryV3AgentWorkflow(prompt);
478
641
  if (handledByV3Workflow) {
479
642
  this.saveSession();
480
643
  return;
481
644
  }
645
+ if (requiresV3Workflow) {
646
+ const errorMessage = 'This task requires the V3 agent workflow and the CLI will not fall back to the legacy local agent loop.';
647
+ this.logger.error(errorMessage);
648
+ this.messages.push({ role: 'assistant', content: errorMessage });
649
+ this.saveSession();
650
+ return;
651
+ }
652
+ await this.runLocalAgentLoop(prompt);
653
+ }
654
+ async runLocalAgentLoop(prompt) {
655
+ if (!this.tools) {
656
+ throw new Error('Agent tools are not initialized.');
657
+ }
482
658
  this.lastActionableUserInput = prompt;
483
659
  this.directToolContinuationCount = 0;
484
660
  this.tools.clearSessionApprovals();
@@ -496,7 +672,22 @@ class ChatCommand {
496
672
  const toolCalls = this.extractToolCalls(assistantMessage);
497
673
  const visibleText = this.stripToolPayloads(assistantMessage).trim();
498
674
  if (toolCalls.length === 0) {
499
- console.log(visibleText || 'Task complete.');
675
+ const finalContent = this.resolveDirectModeCompletion(prompt, visibleText);
676
+ if (this.jsonOutput) {
677
+ console.log(JSON.stringify({
678
+ success: true,
679
+ mode: 'agent',
680
+ model: this.currentModel,
681
+ partial: false,
682
+ content: finalContent,
683
+ metadata: {
684
+ executionPath: 'local-agent-loop',
685
+ },
686
+ }, null, 2));
687
+ }
688
+ else {
689
+ console.log(finalContent);
690
+ }
500
691
  this.saveSession();
501
692
  return;
502
693
  }
@@ -510,11 +701,41 @@ class ChatCommand {
510
701
  }
511
702
  catch (error) {
512
703
  spinner.fail('Agent request failed');
704
+ if (this.jsonOutput) {
705
+ process.exitCode = 1;
706
+ console.log(JSON.stringify({
707
+ success: false,
708
+ mode: 'agent',
709
+ model: this.currentModel,
710
+ partial: false,
711
+ content: '',
712
+ error: error.message,
713
+ metadata: {
714
+ executionPath: 'local-agent-loop',
715
+ },
716
+ }, null, 2));
717
+ }
513
718
  this.logger.error(error.message);
514
719
  return;
515
720
  }
516
721
  }
517
- console.log('Task complete.');
722
+ if (this.jsonOutput) {
723
+ process.exitCode = 1;
724
+ console.log(JSON.stringify({
725
+ success: false,
726
+ mode: 'agent',
727
+ model: this.currentModel,
728
+ partial: true,
729
+ content: 'Task complete.',
730
+ error: 'Agent exhausted the maximum local tool loop turns before reaching a clean completion.',
731
+ metadata: {
732
+ executionPath: 'local-agent-loop',
733
+ },
734
+ }, null, 2));
735
+ }
736
+ else {
737
+ console.log('Task complete.');
738
+ }
518
739
  this.saveSession();
519
740
  }
520
741
  async tryDirectSingleFileFlow(prompt) {
@@ -525,41 +746,51 @@ class ChatCommand {
525
746
  if (!targetFile) {
526
747
  return false;
527
748
  }
749
+ if (this.shouldBypassDirectSingleFileFlow(targetFile, prompt)) {
750
+ return false;
751
+ }
528
752
  const readCall = {
529
753
  tool: 'read_file',
530
754
  args: { path: targetFile },
531
755
  };
532
- console.log(chalk_1.default.cyan(`⚙ Executing: ${readCall.tool}`));
756
+ if (!this.jsonOutput) {
757
+ console.log(chalk_1.default.cyan(`⚙ Executing: ${readCall.tool}`));
758
+ }
533
759
  const readResult = await this.tools.execute(readCall);
534
760
  const readSummary = this.formatToolResult(readCall, readResult);
535
- console.log(readResult.success ? chalk_1.default.gray(readSummary) : chalk_1.default.red(readSummary));
761
+ if (!this.jsonOutput) {
762
+ console.log(readResult.success ? chalk_1.default.gray(readSummary) : chalk_1.default.red(readSummary));
763
+ }
536
764
  this.messages.push({ role: 'system', content: readSummary });
537
765
  if (!readResult.success || !readResult.output) {
538
766
  return false;
539
767
  }
540
- const rewriteMessages = [
541
- {
542
- role: 'system',
543
- content: [
544
- 'You are repairing a single file for a CLI agent task.',
545
- `Return only the final contents for ${targetFile}.`,
546
- 'Do not use Markdown fences.',
547
- 'Do not add explanations before or after the file contents.',
548
- 'Produce complete, runnable code or markup.',
549
- ].join('\n'),
550
- },
551
- {
552
- role: 'user',
553
- content: [
554
- `Task: ${prompt}`,
555
- `Target file: ${targetFile}`,
556
- 'Current file contents:',
557
- readResult.output,
558
- ].join('\n\n'),
559
- },
560
- ];
561
- const rewriteResponse = await this.api.chat(rewriteMessages, this.currentModel);
562
- const rewrittenContent = this.extractFinalFileContent(rewriteResponse.message, targetFile);
768
+ let rewrittenContent = this.tryDeterministicSingleFileRewrite(prompt, targetFile, readResult.output);
769
+ if (!rewrittenContent) {
770
+ const rewriteMessages = [
771
+ {
772
+ role: 'system',
773
+ content: [
774
+ 'You are repairing a single file for a CLI agent task.',
775
+ `Return only the final contents for ${targetFile}.`,
776
+ 'Do not use Markdown fences.',
777
+ 'Do not add explanations before or after the file contents.',
778
+ 'Produce complete, runnable code or markup.',
779
+ ].join('\n'),
780
+ },
781
+ {
782
+ role: 'user',
783
+ content: [
784
+ `Task: ${prompt}`,
785
+ `Target file: ${targetFile}`,
786
+ 'Current file contents:',
787
+ readResult.output,
788
+ ].join('\n\n'),
789
+ },
790
+ ];
791
+ const rewriteResponse = await this.api.chat(rewriteMessages, this.currentModel);
792
+ rewrittenContent = this.extractFinalFileContent(rewriteResponse.message, targetFile);
793
+ }
563
794
  if (!rewrittenContent) {
564
795
  return false;
565
796
  }
@@ -570,55 +801,133 @@ class ChatCommand {
570
801
  content: rewrittenContent,
571
802
  },
572
803
  };
573
- console.log(chalk_1.default.cyan(`⚙ Executing: ${writeCall.tool}`));
804
+ if (!this.jsonOutput) {
805
+ console.log(chalk_1.default.cyan(`⚙ Executing: ${writeCall.tool}`));
806
+ }
574
807
  const writeResult = await this.tools.execute(writeCall);
575
808
  const writeSummary = this.formatToolResult(writeCall, writeResult);
576
- console.log(writeResult.success ? chalk_1.default.gray(writeSummary) : chalk_1.default.red(writeSummary));
809
+ if (!this.jsonOutput) {
810
+ console.log(writeResult.success ? chalk_1.default.gray(writeSummary) : chalk_1.default.red(writeSummary));
811
+ }
577
812
  this.messages.push({ role: 'system', content: writeSummary });
578
813
  if (!writeResult.success) {
579
814
  return false;
580
815
  }
581
- console.log(`Updated ${targetFile}.`);
816
+ const previewGate = await this.api.runTemplateServicePreviewGate(prompt, {
817
+ workspacePath: this.currentProjectPath,
818
+ projectPath: this.currentProjectPath,
819
+ targetPath: this.currentProjectPath,
820
+ rawPrompt: prompt,
821
+ executionSurface: 'cli',
822
+ clientSurface: 'cli',
823
+ });
824
+ const success = previewGate.required ? previewGate.passed === true : true;
825
+ if (!success) {
826
+ process.exitCode = 1;
827
+ }
828
+ if (this.jsonOutput) {
829
+ console.log(JSON.stringify({
830
+ success,
831
+ mode: 'agent',
832
+ model: this.currentModel,
833
+ partial: false,
834
+ content: `Updated ${targetFile}.`,
835
+ metadata: {
836
+ executionPath: 'direct-single-file',
837
+ targetFile,
838
+ previewGate,
839
+ },
840
+ }, null, 2));
841
+ }
842
+ else {
843
+ console.log(`Updated ${targetFile}.`);
844
+ if (previewGate.required) {
845
+ if (previewGate.passed) {
846
+ console.log(chalk_1.default.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
847
+ }
848
+ else {
849
+ console.log(chalk_1.default.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
850
+ }
851
+ }
852
+ }
582
853
  return true;
583
854
  }
584
855
  async tryV3AgentWorkflow(prompt) {
585
856
  const runtimeContext = await this.getPromptRuntimeContext(prompt);
586
- const spinner = this.jsonOutput ? null : (0, ora_1.default)({ text: 'Thinking...', spinner: 'clock' }).start();
857
+ const routingPolicy = this.resolveAgentExecutionPolicy(prompt);
858
+ const rescueEligible = this.isSaaSRescuePrompt(prompt);
859
+ const spinner = this.jsonOutput ? null : (0, ora_1.default)({
860
+ text: routingPolicy.cloudSelected ? 'Routing heavy task to Vigthoria Cloud...' : 'Routing to V3 Agent...',
861
+ spinner: 'clock',
862
+ }).start();
587
863
  const executionPrompt = this.buildExecutionPrompt(prompt);
588
864
  const agentTaskType = this.inferAgentTaskType(prompt);
865
+ const workspaceContext = {
866
+ workspacePath: this.currentProjectPath,
867
+ projectPath: this.currentProjectPath,
868
+ targetPath: this.currentProjectPath,
869
+ ...runtimeContext,
870
+ };
589
871
  try {
590
- const response = await this.api.runV3AgentWorkflow(executionPrompt, {
872
+ const workflowPromise = this.api.runV3AgentWorkflow(executionPrompt, {
591
873
  workspace: { path: this.currentProjectPath },
592
- workspacePath: this.currentProjectPath,
593
- projectPath: this.currentProjectPath,
594
- targetPath: this.currentProjectPath,
874
+ ...workspaceContext,
595
875
  agentTaskType,
596
876
  executionSurface: 'cli',
597
877
  clientSurface: 'cli',
598
878
  localMachineCapable: true,
599
879
  agentTimeoutMs: DEFAULT_V3_AGENT_TIMEOUT_MS,
600
880
  agentIdleTimeoutMs: 90000,
881
+ model: routingPolicy.selectedModel,
882
+ requestedModel: this.currentModel,
883
+ agentExecutionPolicy: routingPolicy,
884
+ legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
601
885
  rawPrompt: prompt,
602
886
  history: this.getMessagesForModel(),
603
887
  ...runtimeContext,
604
888
  onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
605
889
  });
890
+ const response = await (rescueEligible
891
+ ? Promise.race([
892
+ workflowPromise,
893
+ new Promise((_, reject) => {
894
+ setTimeout(() => reject(new Error('V3_SAAS_SOFT_TIMEOUT')), 240000);
895
+ }),
896
+ ])
897
+ : workflowPromise);
606
898
  if (spinner) {
607
899
  spinner.stop();
608
900
  }
609
901
  const previewGate = (response.metadata?.previewGate || null);
610
- const success = previewGate?.required === true ? previewGate?.passed === true : true;
902
+ const workspaceHasOutput = this.api.hasAgentWorkspaceOutput(workspaceContext);
903
+ const success = previewGate?.required === true
904
+ ? (previewGate?.passed === true || workspaceHasOutput)
905
+ : true;
611
906
  if (!success) {
907
+ if (this.isLegacyAgentFallbackAllowed()) {
908
+ if (spinner) {
909
+ spinner.warn('Falling back to legacy CLI agent loop');
910
+ }
911
+ this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
912
+ return false;
913
+ }
914
+ const errorMessage = `V3 agent workflow returned an incomplete result and legacy fallback is disabled. ${previewGate?.error || 'Workspace changes were not fully validated.'}`;
612
915
  if (spinner) {
613
- spinner.warn('Falling back to legacy CLI agent loop');
916
+ spinner.fail('V3 agent workflow rejected the result');
614
917
  }
615
- this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
616
- return false;
918
+ this.logger.error(errorMessage);
919
+ this.messages.push({ role: 'assistant', content: errorMessage });
920
+ return true;
921
+ }
922
+ if (!this.jsonOutput && previewGate?.required && previewGate?.passed !== true && workspaceHasOutput) {
923
+ console.log(chalk_1.default.yellow(`Template Service preview gate did not fully validate this output, but generated workspace files were preserved${previewGate?.error ? `: ${previewGate.error}` : '.'}`));
617
924
  }
618
925
  if (this.jsonOutput) {
619
926
  console.log(JSON.stringify({
620
927
  success,
621
928
  mode: 'agent',
929
+ model: routingPolicy.selectedModel,
930
+ routingPolicy,
622
931
  taskId: response.taskId || null,
623
932
  contextId: response.contextId || null,
624
933
  partial: response.partial === true,
@@ -627,9 +936,11 @@ class ChatCommand {
627
936
  }, null, 2));
628
937
  }
629
938
  else if (response.content) {
939
+ console.log(chalk_1.default.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'} via model ${routingPolicy.selectedModel}`));
630
940
  console.log(response.content);
631
941
  }
632
942
  else {
943
+ console.log(chalk_1.default.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'} via model ${routingPolicy.selectedModel}`));
633
944
  console.log('V3 agent workflow completed.');
634
945
  }
635
946
  if (!this.jsonOutput && previewGate?.required) {
@@ -644,12 +955,82 @@ class ChatCommand {
644
955
  return true;
645
956
  }
646
957
  catch (error) {
958
+ if (rescueEligible && !this.api.hasAgentWorkspaceOutput(workspaceContext)) {
959
+ const rescued = await this.tryCommandLevelSaaSRescue(executionPrompt, prompt, workspaceContext, routingPolicy, spinner, error);
960
+ if (rescued) {
961
+ return true;
962
+ }
963
+ }
964
+ if (this.isLegacyAgentFallbackAllowed()) {
965
+ if (spinner) {
966
+ spinner.warn('Falling back to legacy CLI agent loop');
967
+ }
968
+ this.logger.debug(`V3 agent workflow unavailable: ${error.message}`);
969
+ return false;
970
+ }
647
971
  if (spinner) {
648
- spinner.warn('Falling back to legacy CLI agent loop');
972
+ spinner.fail('V3 agent workflow failed');
649
973
  }
650
- this.logger.debug(`V3 agent workflow unavailable: ${error.message}`);
974
+ const errorMessage = `Agent mode requires the V3 workflow and will not fall back to the legacy CLI loop. ${error.message}`;
975
+ this.logger.error(errorMessage);
976
+ this.messages.push({ role: 'assistant', content: errorMessage });
977
+ return true;
978
+ }
979
+ }
980
+ isSaaSRescuePrompt(prompt) {
981
+ return /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(prompt);
982
+ }
983
+ async tryCommandLevelSaaSRescue(executionPrompt, rawPrompt, workspaceContext, routingPolicy, spinner, error) {
984
+ const apiAny = this.api;
985
+ if (typeof apiAny.materializeEmergencySaaSWorkspace !== 'function') {
986
+ return false;
987
+ }
988
+ const appName = apiAny.materializeEmergencySaaSWorkspace(executionPrompt, workspaceContext);
989
+ if (!appName) {
651
990
  return false;
652
991
  }
992
+ if (typeof apiAny.ensureAgentFrontendPolish === 'function') {
993
+ await apiAny.ensureAgentFrontendPolish(executionPrompt, workspaceContext);
994
+ }
995
+ const previewGate = await this.api.runTemplateServicePreviewGate(executionPrompt, {
996
+ ...workspaceContext,
997
+ rawPrompt,
998
+ });
999
+ if (spinner) {
1000
+ spinner.stop();
1001
+ }
1002
+ const rescueMessage = `Recovered a local SaaS workspace scaffold for ${appName} after the V3 workflow stalled without writing files.`;
1003
+ if (this.jsonOutput) {
1004
+ console.log(JSON.stringify({
1005
+ success: true,
1006
+ mode: 'agent',
1007
+ model: routingPolicy.selectedModel,
1008
+ routingPolicy,
1009
+ taskId: null,
1010
+ contextId: null,
1011
+ partial: true,
1012
+ content: rescueMessage,
1013
+ metadata: {
1014
+ source: 'cli-saas-rescue',
1015
+ previewGate,
1016
+ rescueReason: error.message,
1017
+ },
1018
+ }, null, 2));
1019
+ }
1020
+ else {
1021
+ console.log(chalk_1.default.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'} via model ${routingPolicy.selectedModel}`));
1022
+ console.log(rescueMessage);
1023
+ if (previewGate.required) {
1024
+ if (previewGate.passed) {
1025
+ console.log(chalk_1.default.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
1026
+ }
1027
+ else {
1028
+ console.log(chalk_1.default.yellow(`Template Service preview gate: failed${previewGate.error ? ` - ${previewGate.error}` : ''}`));
1029
+ }
1030
+ }
1031
+ }
1032
+ this.messages.push({ role: 'assistant', content: rescueMessage });
1033
+ return true;
653
1034
  }
654
1035
  async startInteractiveChat() {
655
1036
  const chatTitle = this.operatorMode
@@ -694,11 +1075,17 @@ class ChatCommand {
694
1075
  this.agentMode = !this.agentMode;
695
1076
  if (this.agentMode) {
696
1077
  this.operatorMode = false;
1078
+ this.syncInteractiveModeModel('agent');
1079
+ }
1080
+ else {
1081
+ this.syncInteractiveModeModel('chat');
697
1082
  }
698
1083
  console.log(chalk_1.default.yellow(`Agent mode: ${this.agentMode ? 'ON' : 'OFF'}`));
1084
+ console.log(chalk_1.default.gray(`Model: ${this.currentModel}`));
699
1085
  if (this.currentSession) {
700
1086
  this.currentSession.agentMode = this.agentMode;
701
1087
  this.currentSession.operatorMode = this.operatorMode;
1088
+ this.currentSession.model = this.currentModel;
702
1089
  this.saveSession();
703
1090
  }
704
1091
  continue;
@@ -711,11 +1098,17 @@ class ChatCommand {
711
1098
  this.operatorMode = !this.operatorMode;
712
1099
  if (this.operatorMode) {
713
1100
  this.agentMode = false;
1101
+ this.syncInteractiveModeModel('operator');
1102
+ }
1103
+ else {
1104
+ this.syncInteractiveModeModel('chat');
714
1105
  }
715
1106
  console.log(chalk_1.default.yellow(`Operator mode: ${this.operatorMode ? 'ON' : 'OFF'}`));
1107
+ console.log(chalk_1.default.gray(`Model: ${this.currentModel}`));
716
1108
  if (this.currentSession) {
717
1109
  this.currentSession.agentMode = this.agentMode;
718
1110
  this.currentSession.operatorMode = this.operatorMode;
1111
+ this.currentSession.model = this.currentModel;
719
1112
  this.saveSession();
720
1113
  }
721
1114
  continue;
@@ -732,6 +1125,7 @@ class ChatCommand {
732
1125
  }
733
1126
  if (trimmed.startsWith('/model ')) {
734
1127
  this.currentModel = trimmed.slice(7).trim() || this.currentModel;
1128
+ this.modelExplicitlySelected = true;
735
1129
  console.log(chalk_1.default.yellow(`Model changed to: ${this.currentModel}`));
736
1130
  if (this.currentSession) {
737
1131
  this.currentSession.model = this.currentModel;
@@ -845,7 +1239,8 @@ class ChatCommand {
845
1239
  'Finish the request and stop once it is complete.',
846
1240
  ].join('\n');
847
1241
  }
848
- inferTargetFileFromPrompt(prompt) {
1242
+ inferTargetFilesFromPrompt(prompt) {
1243
+ const candidates = [];
849
1244
  const matches = Array.from(prompt.matchAll(/([A-Za-z0-9_./-]+\.[A-Za-z0-9_-]+)/g));
850
1245
  for (const match of matches) {
851
1246
  const candidate = match[1];
@@ -854,10 +1249,137 @@ class ChatCommand {
854
1249
  }
855
1250
  const resolved = path.resolve(this.currentProjectPath, candidate);
856
1251
  if (resolved.startsWith(this.currentProjectPath)) {
857
- return candidate;
1252
+ if (!candidates.includes(candidate)) {
1253
+ candidates.push(candidate);
1254
+ }
858
1255
  }
859
1256
  }
860
- return null;
1257
+ return candidates;
1258
+ }
1259
+ inferTargetFileFromPrompt(prompt) {
1260
+ return this.inferTargetFilesFromPrompt(prompt)[0] || null;
1261
+ }
1262
+ workspaceContainsHtmlEntry(rootDir) {
1263
+ const stack = [rootDir];
1264
+ while (stack.length > 0) {
1265
+ const currentDir = stack.pop();
1266
+ if (!currentDir) {
1267
+ continue;
1268
+ }
1269
+ let entries = [];
1270
+ try {
1271
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
1272
+ }
1273
+ catch {
1274
+ continue;
1275
+ }
1276
+ for (const entry of entries) {
1277
+ if (entry.name === 'node_modules' || entry.name === '.git') {
1278
+ continue;
1279
+ }
1280
+ const fullPath = path.join(currentDir, entry.name);
1281
+ if (entry.isDirectory()) {
1282
+ stack.push(fullPath);
1283
+ continue;
1284
+ }
1285
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
1286
+ return true;
1287
+ }
1288
+ }
1289
+ }
1290
+ return false;
1291
+ }
1292
+ shouldBypassDirectSingleFileFlow(targetFile, prompt) {
1293
+ const referencedFiles = this.inferTargetFilesFromPrompt(prompt);
1294
+ const editableFiles = referencedFiles.filter((filePath) => !this.isProtectedFileReferenceSafe(prompt, filePath));
1295
+ if (editableFiles.length > 1) {
1296
+ return true;
1297
+ }
1298
+ const extension = path.extname(targetFile).toLowerCase();
1299
+ return extension === '';
1300
+ }
1301
+ shouldPreferLocalAgentLoop(prompt) {
1302
+ if (this.shouldRequireV3AgentWorkflow(prompt)) {
1303
+ return false;
1304
+ }
1305
+ if (!this.directPromptMode) {
1306
+ return false;
1307
+ }
1308
+ if (this.isServerBindableWorkspace(this.currentProjectPath)) {
1309
+ return false;
1310
+ }
1311
+ return /(analyse|analyze|audit|overview|inspect|explain|review|debug|diagnos|read|summari[sz]e|investigate)/i.test(prompt);
1312
+ }
1313
+ shouldRequireV3AgentWorkflow(prompt) {
1314
+ if (!this.directPromptMode) {
1315
+ return false;
1316
+ }
1317
+ if (this.inferTargetFilesFromPrompt(prompt).length > 0) {
1318
+ return false;
1319
+ }
1320
+ return /(create the required project files and write them to the workspace|build a polished|premium|landing experience|landing page|site|website|dashboard|saas|frontend|hero|responsive|animated reveals|journal preview|contact section|mobile navigation|ui details)/i.test(prompt);
1321
+ }
1322
+ isServerBindableWorkspace(projectPath) {
1323
+ if (!projectPath || !path.isAbsolute(projectPath) || !fs.existsSync(projectPath)) {
1324
+ return false;
1325
+ }
1326
+ const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
1327
+ .split(',')
1328
+ .map((entry) => entry.trim())
1329
+ .filter(Boolean);
1330
+ let resolvedCandidate = '';
1331
+ try {
1332
+ resolvedCandidate = fs.realpathSync(projectPath);
1333
+ }
1334
+ catch {
1335
+ return false;
1336
+ }
1337
+ return configuredRoots.some((root) => {
1338
+ if (!root || !fs.existsSync(root)) {
1339
+ return false;
1340
+ }
1341
+ try {
1342
+ const resolvedRoot = fs.realpathSync(root);
1343
+ const relativePath = path.relative(resolvedRoot, resolvedCandidate);
1344
+ return relativePath === '' || !relativePath.startsWith('..');
1345
+ }
1346
+ catch {
1347
+ return false;
1348
+ }
1349
+ });
1350
+ }
1351
+ isProtectedFileReferenceSafe(prompt, filePath) {
1352
+ const normalizedPrompt = prompt.toLowerCase();
1353
+ const normalizedPath = filePath.toLowerCase();
1354
+ const protectedPhrases = [
1355
+ `do not modify ${normalizedPath}`,
1356
+ `do not modify \`${normalizedPath}\``,
1357
+ `do not modify '${normalizedPath}'`,
1358
+ `do not modify "${normalizedPath}"`,
1359
+ `don't modify ${normalizedPath}`,
1360
+ `don't modify \`${normalizedPath}\``,
1361
+ `don't modify '${normalizedPath}'`,
1362
+ `don't modify "${normalizedPath}"`,
1363
+ `leave ${normalizedPath} unchanged`,
1364
+ `leave \`${normalizedPath}\` unchanged`,
1365
+ `leave '${normalizedPath}' unchanged`,
1366
+ `leave "${normalizedPath}" unchanged`,
1367
+ `without modifying ${normalizedPath}`,
1368
+ `without modifying \`${normalizedPath}\``,
1369
+ `without modifying '${normalizedPath}'`,
1370
+ `without modifying "${normalizedPath}"`,
1371
+ ];
1372
+ return protectedPhrases.some((phrase) => normalizedPrompt.includes(phrase));
1373
+ }
1374
+ isProtectedFileReference(prompt, filePath) {
1375
+ const escapedPath = filePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1376
+ const protectedPatterns = [
1377
+ new RegExp(`do not modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
1378
+ new RegExp(`don't modify\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
1379
+ new RegExp(`leave\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?\\s+unchanged`, 'i'),
1380
+ new RegExp(`without modifying\\s+[-\u001f\s\-\"][` + "'" + `]?${escapedPath}[` + "'" + `]?`, 'i'),
1381
+ ];
1382
+ return protectedPatterns.some((pattern) => pattern.test(prompt));
861
1383
  }
862
1384
  buildContinuationPrompt() {
863
1385
  const diagnosticMode = this.isDiagnosticPrompt(this.lastActionableUserInput);
@@ -929,15 +1451,110 @@ class ChatCommand {
929
1451
  }
930
1452
  return trimmed;
931
1453
  }
1454
+ resolveDirectModeCompletion(prompt, visibleText) {
1455
+ const normalized = (visibleText || '').trim();
1456
+ if (normalized && !this.isDirectModeFollowUpQuestion(normalized)) {
1457
+ return normalized;
1458
+ }
1459
+ const fallback = this.buildLocalAnalysisFallback(prompt);
1460
+ if (fallback) {
1461
+ return fallback;
1462
+ }
1463
+ return normalized || 'Task complete.';
1464
+ }
1465
+ isDirectModeFollowUpQuestion(text) {
1466
+ return /^(would you like me|do you want me|which aspect|what aspect|can you clarify|could you clarify|should i focus on)/i.test(text.trim());
1467
+ }
1468
+ buildLocalAnalysisFallback(prompt) {
1469
+ if (!/(analyse|analyze|audit|overview|inspect|review|summari[sz]e|actual state)/i.test(prompt)) {
1470
+ return '';
1471
+ }
1472
+ try {
1473
+ const summary = [];
1474
+ const rootPath = this.currentProjectPath;
1475
+ const topLevelEntries = fs.readdirSync(rootPath, { withFileTypes: true })
1476
+ .filter((entry) => entry.name !== '.git' && entry.name !== 'node_modules')
1477
+ .slice(0, 20)
1478
+ .map((entry) => `${entry.name}${entry.isDirectory() ? '/' : ''}`);
1479
+ summary.push(`Project overview for ${path.basename(rootPath)}.`);
1480
+ summary.push(`Workspace: ${rootPath}`);
1481
+ const packageJsonPath = path.join(rootPath, 'package.json');
1482
+ if (fs.existsSync(packageJsonPath)) {
1483
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1484
+ const scripts = Object.keys(pkg.scripts || {});
1485
+ const dependencies = Object.keys(pkg.dependencies || {});
1486
+ const devDependencies = Object.keys(pkg.devDependencies || {});
1487
+ summary.push(`Package: ${pkg.name || path.basename(rootPath)}${pkg.version ? ` v${pkg.version}` : ''}`);
1488
+ if (scripts.length > 0) {
1489
+ summary.push(`Scripts: ${scripts.slice(0, 10).join(', ')}`);
1490
+ }
1491
+ summary.push(`Dependencies: ${dependencies.length} runtime, ${devDependencies.length} dev`);
1492
+ }
1493
+ if (topLevelEntries.length > 0) {
1494
+ summary.push(`Top-level entries: ${topLevelEntries.join(', ')}`);
1495
+ }
1496
+ const readmePath = path.join(rootPath, 'README.md');
1497
+ if (fs.existsSync(readmePath)) {
1498
+ const excerpt = fs.readFileSync(readmePath, 'utf8').replace(/\s+/g, ' ').trim().slice(0, 500);
1499
+ if (excerpt) {
1500
+ summary.push(`README excerpt: ${excerpt}`);
1501
+ }
1502
+ }
1503
+ return summary.join('\n');
1504
+ }
1505
+ catch {
1506
+ return '';
1507
+ }
1508
+ }
1509
+ tryDeterministicSingleFileRewrite(prompt, targetFile, currentContent) {
1510
+ const extension = path.extname(targetFile).toLowerCase();
1511
+ if (extension !== '.html') {
1512
+ return null;
1513
+ }
1514
+ const exactText = this.extractExactTextRequirement(prompt);
1515
+ if (!exactText) {
1516
+ return null;
1517
+ }
1518
+ if (!/hero\s+(?:paragraph|copy)/i.test(prompt)) {
1519
+ return null;
1520
+ }
1521
+ const heroCopyPattern = /(<p[^>]*class=["'][^"']*hero-copy[^"']*["'][^>]*>)([\s\S]*?)(<\/p>)/i;
1522
+ if (heroCopyPattern.test(currentContent)) {
1523
+ const nextContent = currentContent.replace(heroCopyPattern, `$1${exactText}$3`);
1524
+ return nextContent !== currentContent ? nextContent : null;
1525
+ }
1526
+ const heroParagraphPattern = /(<main[^>]*class=["'][^"']*hero[^"']*["'][^>]*>[\s\S]*?<p[^>]*>)([\s\S]*?)(<\/p>)/i;
1527
+ if (heroParagraphPattern.test(currentContent)) {
1528
+ const nextContent = currentContent.replace(heroParagraphPattern, `$1${exactText}$3`);
1529
+ return nextContent !== currentContent ? nextContent : null;
1530
+ }
1531
+ return null;
1532
+ }
1533
+ extractExactTextRequirement(prompt) {
1534
+ const match = prompt.match(/reads exactly:\s*([\s\S]*?)(?:\s+Do not\b|\s+Do\b|$)/i);
1535
+ if (!match || !match[1]) {
1536
+ return null;
1537
+ }
1538
+ const normalized = match[1]
1539
+ .trim()
1540
+ .replace(/^[`"']+/, '')
1541
+ .replace(/[`"']+$/, '')
1542
+ .trim();
1543
+ return normalized || null;
1544
+ }
932
1545
  async executeToolCalls(toolCalls) {
933
1546
  if (!this.tools) {
934
1547
  throw new Error('Agent tools are not initialized.');
935
1548
  }
936
1549
  for (const call of toolCalls) {
937
- console.log(chalk_1.default.cyan(`⚙ Executing: ${call.tool}`));
1550
+ if (!this.jsonOutput) {
1551
+ console.log(chalk_1.default.cyan(`⚙ Executing: ${call.tool}`));
1552
+ }
938
1553
  const result = await this.tools.execute(call);
939
1554
  const summary = this.formatToolResult(call, result);
940
- console.log(result.success ? chalk_1.default.gray(summary) : chalk_1.default.red(summary));
1555
+ if (!this.jsonOutput) {
1556
+ console.log(result.success ? chalk_1.default.gray(summary) : chalk_1.default.red(summary));
1557
+ }
941
1558
  this.messages.push({ role: 'system', content: summary });
942
1559
  }
943
1560
  }