vigthoria-cli 1.6.20 → 1.6.21

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.
@@ -46,6 +46,11 @@ export declare class ChatCommand {
46
46
  private resolveAgentExecutionPolicy;
47
47
  private getMessagesForModel;
48
48
  private isDiagnosticPrompt;
49
+ /**
50
+ * Returns true when the prompt is a simple lookup / analysis / read-only
51
+ * question — these should use analysis_only workflow, not full_autonomy.
52
+ */
53
+ private isAnalysisLookupPrompt;
49
54
  private isBrowserTaskPrompt;
50
55
  private inferAgentTaskType;
51
56
  private buildTaskShapingInstructions;
@@ -195,6 +195,13 @@ class ChatCommand {
195
195
  isDiagnosticPrompt(prompt) {
196
196
  return /(startup|start up|won'?t start|doesn'?t start|crash|crashes|error|errors|failing|fails|issue|issues|bug|bugs|diagnos|debug|runtime|log|logs|exception|traceback|stack trace|yaml|blocking|blocker)/i.test(prompt);
197
197
  }
198
+ /**
199
+ * Returns true when the prompt is a simple lookup / analysis / read-only
200
+ * question — these should use analysis_only workflow, not full_autonomy.
201
+ */
202
+ isAnalysisLookupPrompt(prompt) {
203
+ return /^(what|which|where|how many|who|find|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|count|read|look at|tell me|locate|search for|does .* exist)/i.test(prompt.trim());
204
+ }
198
205
  isBrowserTaskPrompt(prompt) {
199
206
  return /(browser|chrome|devtools|console|dom|network tab|network request|frontend runtime|client-side|client side|rendering|page load|websocket|ui bug|inspect element)/i.test(prompt);
200
207
  }
@@ -360,7 +367,13 @@ class ChatCommand {
360
367
  }
361
368
  async run(options) {
362
369
  if (!this.config.isAuthenticated()) {
363
- this.logger.error('Not authenticated. Run: vigthoria login');
370
+ if (options.json) {
371
+ process.exitCode = 1;
372
+ console.log(JSON.stringify({ success: false, error: 'Not authenticated. Run: vigthoria login' }, null, 2));
373
+ }
374
+ else {
375
+ this.logger.error('Not authenticated. Run: vigthoria login');
376
+ }
364
377
  return;
365
378
  }
366
379
  this.agentMode = options.agent === true;
@@ -611,7 +624,8 @@ class ChatCommand {
611
624
  }
612
625
  catch (error) {
613
626
  if (spinner) {
614
- spinner.fail('Workflow target execution failed');
627
+ spinner.stop();
628
+ this.logger.error('Workflow target execution failed');
615
629
  }
616
630
  throw error;
617
631
  }
@@ -624,7 +638,9 @@ class ChatCommand {
624
638
  const runtimeContext = await this.getPromptRuntimeContext(prompt);
625
639
  const spinner = this.jsonOutput ? null : (0, logger_js_1.createSpinner)({ text: 'Thinking like an operator...', spinner: 'clock' }).start();
626
640
  const executionPrompt = this.buildExecutionPrompt(prompt);
627
- const workflowType = this.isDiagnosticPrompt(prompt) ? 'analysis_only' : 'full_autonomy';
641
+ const workflowType = this.isDiagnosticPrompt(prompt) || this.isAnalysisLookupPrompt(prompt)
642
+ ? 'analysis_only'
643
+ : 'full_autonomy';
628
644
  try {
629
645
  const response = await this.api.runOperatorWorkflow(executionPrompt, {
630
646
  workspacePath: this.currentProjectPath,
@@ -665,9 +681,12 @@ class ChatCommand {
665
681
  }
666
682
  catch (error) {
667
683
  if (spinner) {
668
- spinner.fail('Operator workflow failed');
684
+ spinner.stop();
669
685
  }
670
686
  const errorMsg = error.message || 'Operator workflow failed with an unknown error.';
687
+ if (!this.jsonOutput) {
688
+ this.logger.error('Operator workflow failed');
689
+ }
671
690
  if (this.jsonOutput) {
672
691
  process.exitCode = 1;
673
692
  console.log(JSON.stringify({
@@ -686,11 +705,26 @@ class ChatCommand {
686
705
  this.lastActionableUserInput = prompt;
687
706
  // In non-agent chat mode the model has no tool access. Inject a
688
707
  // grounding constraint so it doesn't fabricate file contents.
708
+ // Also inject a real file listing so the model can reference actual
709
+ // paths instead of guessing.
689
710
  const needsGrounding = /(repo|file|code|project|workspace|source|inspect|analyze|audit|review)/i.test(prompt);
690
711
  if (needsGrounding && !this.messages.some(m => m.role === 'system' && m.content.includes('no direct file access'))) {
712
+ let groundingContent = 'You are in simple chat mode with no direct file access or tools. Do not fabricate file contents, search results, or analysis steps. If the user asks you to modify, edit, or deeply inspect files, advise them to use agent mode (vig chat --agent) for grounded repo analysis.';
713
+ // Inject a workspace file listing so the model has real paths
714
+ try {
715
+ const snapshot = this.api.getAgentWorkspaceSnapshot(this.currentProjectPath);
716
+ if (snapshot && snapshot.paths.length > 0) {
717
+ const listing = snapshot.paths.slice(0, 60).join('\n');
718
+ groundingContent += `\n\nWorkspace: ${this.currentProjectPath}\nFile listing (${snapshot.fileCount} files total):\n${listing}`;
719
+ if (snapshot.fileCount > 60) {
720
+ groundingContent += `\n... and ${snapshot.fileCount - 60} more files.`;
721
+ }
722
+ }
723
+ }
724
+ catch { /* ignore snapshot errors */ }
691
725
  this.messages.push({
692
726
  role: 'system',
693
- content: 'You are in simple chat mode with no direct file access. Do not fabricate file contents, search results, or analysis steps. If the user asks about specific files, advise them to use agent mode (vig chat --agent) for grounded repo analysis.',
727
+ content: groundingContent,
694
728
  });
695
729
  }
696
730
  this.messages.push({ role: 'user', content: this.buildExecutionPrompt(prompt) });
@@ -719,7 +753,9 @@ class ChatCommand {
719
753
  }
720
754
  catch (error) {
721
755
  if (spinner)
722
- spinner.fail('Failed to get response');
756
+ spinner.stop();
757
+ if (!this.jsonOutput)
758
+ this.logger.error('Failed to get response');
723
759
  const errorMsg = error.message;
724
760
  if (this.jsonOutput) {
725
761
  process.exitCode = 1;
@@ -830,7 +866,9 @@ class ChatCommand {
830
866
  }
831
867
  catch (error) {
832
868
  if (spinner)
833
- spinner.fail('Agent request failed');
869
+ spinner.stop();
870
+ if (!this.jsonOutput)
871
+ this.logger.error('Agent request failed');
834
872
  if (this.jsonOutput) {
835
873
  process.exitCode = 1;
836
874
  console.log(JSON.stringify({
@@ -1040,14 +1078,15 @@ class ChatCommand {
1040
1078
  if (!success) {
1041
1079
  if (this.isLegacyAgentFallbackAllowed()) {
1042
1080
  if (spinner) {
1043
- spinner.warn('Falling back to legacy CLI agent loop');
1081
+ spinner.stop();
1044
1082
  }
1083
+ this.logger.warn('Falling back to legacy CLI agent loop');
1045
1084
  this.logger.debug(`V3 agent workflow returned an incomplete result: ${previewGate?.error || 'workspace changes were not fully validated'}`);
1046
1085
  return false;
1047
1086
  }
1048
1087
  const errorMessage = `V3 agent workflow returned an incomplete result and legacy fallback is disabled. ${previewGate?.error || 'Workspace changes were not fully validated.'}`;
1049
1088
  if (spinner) {
1050
- spinner.fail('V3 agent workflow rejected the result');
1089
+ spinner.stop();
1051
1090
  }
1052
1091
  this.logger.error(errorMessage);
1053
1092
  this.messages.push({ role: 'assistant', content: errorMessage });
@@ -1109,13 +1148,14 @@ class ChatCommand {
1109
1148
  }
1110
1149
  if (this.isLegacyAgentFallbackAllowed()) {
1111
1150
  if (spinner) {
1112
- spinner.warn('Falling back to legacy CLI agent loop');
1151
+ spinner.stop();
1113
1152
  }
1153
+ this.logger.warn('Falling back to legacy CLI agent loop');
1114
1154
  this.logger.debug(`V3 agent workflow unavailable: ${error.message}`);
1115
1155
  return false;
1116
1156
  }
1117
1157
  if (spinner) {
1118
- spinner.fail('V3 agent workflow failed');
1158
+ spinner.stop();
1119
1159
  }
1120
1160
  const errorMessage = `Agent mode requires the V3 workflow and will not fall back to the legacy CLI loop. ${error.message}`;
1121
1161
  this.logger.error(errorMessage);
@@ -151,7 +151,8 @@ class DeployCommand {
151
151
  console.log(chalk_1.default.gray(' Upgrade to a subdomain for permanent hosting.\n'));
152
152
  }
153
153
  catch (error) {
154
- spinner.fail('Deploy failed');
154
+ spinner.stop();
155
+ this.logger.error('Deploy failed');
155
156
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
156
157
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
157
158
  }
@@ -204,7 +205,8 @@ class DeployCommand {
204
205
  console.log(chalk_1.default.gray(' ✓ Unlimited traffic included\n'));
205
206
  }
206
207
  catch (error) {
207
- spinner.fail('Deploy failed');
208
+ spinner.stop();
209
+ this.logger.error('Deploy failed');
208
210
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
209
211
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
210
212
  }
@@ -258,7 +260,8 @@ class DeployCommand {
258
260
  }
259
261
  }
260
262
  catch (error) {
261
- spinner.fail('Deploy failed');
263
+ spinner.stop();
264
+ this.logger.error('Deploy failed');
262
265
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
263
266
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
264
267
  }
@@ -334,7 +337,8 @@ class DeployCommand {
334
337
  console.log(chalk_1.default.cyan('\n Subscribe: vig deploy --subdomain myapp\n'));
335
338
  }
336
339
  catch (error) {
337
- spinner.fail('Failed to fetch plans');
340
+ spinner.stop();
341
+ this.logger.error('Failed to fetch plans');
338
342
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
339
343
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
340
344
  }
@@ -374,7 +378,8 @@ class DeployCommand {
374
378
  }
375
379
  }
376
380
  catch (error) {
377
- spinner.fail('List failed');
381
+ spinner.stop();
382
+ this.logger.error('List failed');
378
383
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
379
384
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
380
385
  }
@@ -401,7 +406,8 @@ class DeployCommand {
401
406
  console.log(JSON.stringify(data, null, 2));
402
407
  }
403
408
  catch (error) {
404
- spinner.fail('Status check failed');
409
+ spinner.stop();
410
+ this.logger.error('Status check failed');
405
411
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
406
412
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
407
413
  }
@@ -434,7 +440,8 @@ class DeployCommand {
434
440
  }
435
441
  }
436
442
  catch (error) {
437
- spinner.fail('Verification failed');
443
+ spinner.stop();
444
+ this.logger.error('Verification failed');
438
445
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
439
446
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
440
447
  }
@@ -468,7 +475,8 @@ class DeployCommand {
468
475
  console.log(chalk_1.default.gray('\n Your project files are still in your repository.\n'));
469
476
  }
470
477
  catch (error) {
471
- spinner.fail('Remove failed');
478
+ spinner.stop();
479
+ this.logger.error('Remove failed');
472
480
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
473
481
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
474
482
  }
@@ -41,6 +41,11 @@ class EditCommand {
41
41
  // Get instruction
42
42
  let instruction = options.instruction;
43
43
  if (!instruction) {
44
+ // --apply requires --instruction to avoid hanging on non-interactive terminals
45
+ if (options.apply) {
46
+ this.logger.error('The --apply flag requires --instruction. Example: vigthoria edit file.ts --apply --instruction "fix the bug"');
47
+ return;
48
+ }
44
49
  const answer = await inquirer_1.default.prompt([
45
50
  {
46
51
  type: 'input',
@@ -148,12 +153,17 @@ Return the complete modified code:`,
148
153
  }
149
154
  }
150
155
  extractCode(response, language) {
151
- // Try to extract code block
156
+ // Try to extract code block (language-specific first, then any)
152
157
  const codeBlockRegex = new RegExp(`\`\`\`(?:${language})?\\n([\\s\\S]*?)\`\`\``, 'i');
153
158
  const match = response.match(codeBlockRegex);
154
159
  if (match) {
155
160
  return match[1].trim();
156
161
  }
162
+ // Try generic code block
163
+ const genericMatch = response.match(/```\w*\n([\s\S]*?)```/);
164
+ if (genericMatch) {
165
+ return genericMatch[1].trim();
166
+ }
157
167
  // If no code block, check if response looks like code
158
168
  const trimmed = response.trim();
159
169
  if (!trimmed.startsWith('```') && !trimmed.includes('Here') && !trimmed.includes('I ')) {
@@ -399,7 +399,8 @@ class RepoCommand {
399
399
  console.log(chalk_1.default.gray('\nTip: Use `vigthoria repo pull <name>` to restore this project anywhere.\n'));
400
400
  }
401
401
  catch (error) {
402
- spinner.fail('Push failed');
402
+ spinner.stop();
403
+ this.logger.error('Push failed');
403
404
  const errMsg = this.formatRepoError(error);
404
405
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
405
406
  }
@@ -486,7 +487,8 @@ class RepoCommand {
486
487
  console.log();
487
488
  }
488
489
  catch (error) {
489
- spinner.fail('Pull failed');
490
+ spinner.stop();
491
+ this.logger.error('Pull failed');
490
492
  const errMsg = this.formatRepoError(error);
491
493
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
492
494
  }
@@ -584,7 +586,8 @@ class RepoCommand {
584
586
  console.log(chalk_1.default.gray(' vigthoria repo share <name> - Share a project\n'));
585
587
  }
586
588
  catch (error) {
587
- spinner.fail('List failed');
589
+ spinner.stop();
590
+ this.logger.error('List failed');
588
591
  const errMsg = this.formatRepoError(error);
589
592
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
590
593
  }
@@ -634,7 +637,8 @@ class RepoCommand {
634
637
  }
635
638
  }
636
639
  catch (error) {
637
- spinner.fail('Status check failed');
640
+ spinner.stop();
641
+ this.logger.error('Status check failed');
638
642
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
639
643
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
640
644
  }
@@ -666,7 +670,8 @@ class RepoCommand {
666
670
  console.log(chalk_1.default.gray('\n Anyone with this link can view/download the project.\n'));
667
671
  }
668
672
  catch (error) {
669
- spinner.fail('Share failed');
673
+ spinner.stop();
674
+ this.logger.error('Share failed');
670
675
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
671
676
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
672
677
  }
@@ -700,7 +705,8 @@ class RepoCommand {
700
705
  console.log(chalk_1.default.gray('\nNote: Your local files are not affected.\n'));
701
706
  }
702
707
  catch (error) {
703
- spinner.fail('Delete failed');
708
+ spinner.stop();
709
+ this.logger.error('Delete failed');
704
710
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
705
711
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
706
712
  }
@@ -728,7 +734,8 @@ class RepoCommand {
728
734
  await this.pull(data.project.project_name, options);
729
735
  }
730
736
  catch (error) {
731
- spinner.fail('Clone failed');
737
+ spinner.stop();
738
+ this.logger.error('Clone failed');
732
739
  const errMsg = error instanceof Error ? error.message : 'Unknown error';
733
740
  console.log(chalk_1.default.red(`\n❌ Error: ${errMsg}\n`));
734
741
  }
@@ -217,6 +217,13 @@ export declare class APIClient {
217
217
  private getVigFlowAccessToken;
218
218
  private getVigFlowHeaders;
219
219
  private withVigFlow;
220
+ /**
221
+ * Build the correct sub-path for VigFlow endpoints.
222
+ * Local servers (e.g. localhost:5060) need `/api/…` prefix.
223
+ * The remote gateway URL already ends with `/api/vigflow`, so appending
224
+ * another `/api/…` would double the prefix and cause 404s.
225
+ */
226
+ private vigFlowEndpoint;
220
227
  listVigFlowTemplates(options?: {
221
228
  category?: string;
222
229
  search?: string;
@@ -339,6 +346,11 @@ export declare class APIClient {
339
346
  }[];
340
347
  suggestions: string[];
341
348
  }>;
349
+ /**
350
+ * Lightweight client-side heuristic scan: catches common code smells
351
+ * so review never returns "score 30, no issues".
352
+ */
353
+ private heuristicCodeIssues;
342
354
  fixCode(code: string, language: string, fixType: string): Promise<{
343
355
  fixed: string;
344
356
  changes: {
package/dist/utils/api.js CHANGED
@@ -793,6 +793,19 @@ class APIClient {
793
793
  }
794
794
  throw lastError || new Error(`No VigFlow backend available for ${operation}.`);
795
795
  }
796
+ /**
797
+ * Build the correct sub-path for VigFlow endpoints.
798
+ * Local servers (e.g. localhost:5060) need `/api/…` prefix.
799
+ * The remote gateway URL already ends with `/api/vigflow`, so appending
800
+ * another `/api/…` would double the prefix and cause 404s.
801
+ */
802
+ vigFlowEndpoint(baseUrl, subPath) {
803
+ if (/\/api\/vigflow\/?$/i.test(baseUrl)) {
804
+ // Remote gateway – subPath like '/templates' is enough
805
+ return `${baseUrl.replace(/\/$/, '')}${subPath}`;
806
+ }
807
+ return `${baseUrl}/api${subPath}`;
808
+ }
796
809
  async listVigFlowTemplates(options = {}) {
797
810
  return this.withVigFlow('list templates', async (baseUrl, headers) => {
798
811
  const query = new URLSearchParams();
@@ -802,7 +815,7 @@ class APIClient {
802
815
  if (options.search) {
803
816
  query.set('search', options.search);
804
817
  }
805
- const url = `${baseUrl}/api/templates${query.size > 0 ? `?${query.toString()}` : ''}`;
818
+ const url = `${this.vigFlowEndpoint(baseUrl, '/templates')}${query.size > 0 ? `?${query.toString()}` : ''}`;
806
819
  const response = await axios_1.default.get(url, {
807
820
  headers,
808
821
  timeout: 30000,
@@ -813,7 +826,7 @@ class APIClient {
813
826
  }
814
827
  async listVigFlowWorkflows() {
815
828
  return this.withVigFlow('list workflows', async (baseUrl, headers) => {
816
- const response = await axios_1.default.get(`${baseUrl}/api/workflows`, {
829
+ const response = await axios_1.default.get(this.vigFlowEndpoint(baseUrl, '/workflows'), {
817
830
  headers,
818
831
  timeout: 30000,
819
832
  });
@@ -865,7 +878,7 @@ class APIClient {
865
878
  }
866
879
  async useVigFlowTemplate(templateId, options = {}) {
867
880
  return this.withVigFlow('use template', async (baseUrl, headers) => {
868
- const response = await axios_1.default.post(`${baseUrl}/api/templates/${encodeURIComponent(templateId)}/use`, {
881
+ const response = await axios_1.default.post(`${this.vigFlowEndpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`)}`, {
869
882
  name: options.name,
870
883
  variables: options.variables || {},
871
884
  }, {
@@ -881,7 +894,7 @@ class APIClient {
881
894
  }
882
895
  async runVigFlowWorkflow(workflowId, options = {}) {
883
896
  return this.withVigFlow('run workflow', async (baseUrl, headers) => {
884
- const response = await axios_1.default.post(`${baseUrl}/api/executions/run/${encodeURIComponent(workflowId)}`, {
897
+ const response = await axios_1.default.post(`${this.vigFlowEndpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`)}`, {
885
898
  data: options.data || {},
886
899
  options: options.executionOptions || {},
887
900
  }, {
@@ -897,7 +910,7 @@ class APIClient {
897
910
  }
898
911
  async getVigFlowExecutionStatus(executionId) {
899
912
  return this.withVigFlow('execution status', async (baseUrl, headers) => {
900
- const response = await axios_1.default.get(`${baseUrl}/api/executions/${encodeURIComponent(executionId)}`, {
913
+ const response = await axios_1.default.get(`${this.vigFlowEndpoint(baseUrl, `/executions/${encodeURIComponent(executionId)}`)}`, {
901
914
  headers,
902
915
  timeout: 30000,
903
916
  });
@@ -918,6 +931,11 @@ class APIClient {
918
931
  const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
919
932
  const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
920
933
  const resolvedModel = this.resolvePermittedModelId(requestedModel);
934
+ // When the server cannot directly access the workspace (e.g. Windows
935
+ // client), use the local path as a hint and flag that the workspace
936
+ // files are provided inline in localWorkspaceSummary.workspaceFiles.
937
+ const effectiveWorkspacePath = serverWorkspacePath || localWorkspacePath || null;
938
+ const needsHydration = !serverWorkspacePath && !!localWorkspacePath;
921
939
  const payload = {
922
940
  workspace: resolvedContext.workspace || null,
923
941
  activeFile: resolvedContext.activeFile || null,
@@ -931,12 +949,16 @@ class APIClient {
931
949
  executionSurface: resolvedContext.executionSurface || 'cli',
932
950
  clientSurface: resolvedContext.clientSurface || 'cli',
933
951
  localMachineCapable: resolvedContext.localMachineCapable !== false,
934
- workspacePath: serverWorkspacePath || null,
935
- projectPath: serverWorkspacePath || null,
936
- targetPath: serverWorkspacePath || null,
952
+ workspacePath: effectiveWorkspacePath,
953
+ projectPath: effectiveWorkspacePath,
954
+ targetPath: effectiveWorkspacePath,
937
955
  localWorkspacePath: localWorkspacePath || null,
938
956
  localWorkspaceName: localWorkspacePath ? path_1.default.basename(localWorkspacePath) : null,
939
957
  localWorkspaceSummary,
958
+ // Signal to the server that the workspace filesystem is not locally
959
+ // accessible — it must hydrate a temp directory from the provided
960
+ // workspaceFiles before the agent starts using tools.
961
+ workspaceHydrationRequired: needsHydration,
940
962
  contextId: resolvedContext.contextId,
941
963
  traceId: resolvedContext.traceId,
942
964
  mcpContextId: resolvedContext.mcpContextId || null,
@@ -2333,7 +2355,7 @@ document.addEventListener('DOMContentLoaded', () => {
2333
2355
  const toolResults = [];
2334
2356
  const filesRead = [];
2335
2357
  const filesWritten = [];
2336
- let lastAssistantText = '';
2358
+ const assistantFragments = [];
2337
2359
  for (const event of events) {
2338
2360
  if (!event)
2339
2361
  continue;
@@ -2352,16 +2374,22 @@ document.addEventListener('DOMContentLoaded', () => {
2352
2374
  }
2353
2375
  }
2354
2376
  if (event.type === 'assistant' && typeof event.content === 'string' && event.content.trim()) {
2355
- lastAssistantText = event.content.trim();
2377
+ assistantFragments.push(event.content.trim());
2356
2378
  }
2357
2379
  // Some servers emit 'text' events for incremental assistant text
2358
2380
  if (event.type === 'text' && typeof event.content === 'string' && event.content.trim()) {
2359
- lastAssistantText = event.content.trim();
2381
+ assistantFragments.push(event.content.trim());
2382
+ }
2383
+ // Some servers emit content_block_delta for streamed text
2384
+ if (event.type === 'content_block_delta' && typeof event.delta?.text === 'string' && event.delta.text.trim()) {
2385
+ assistantFragments.push(event.delta.text.trim());
2360
2386
  }
2361
2387
  }
2362
- // Prefer the last assistant text the model emitted
2363
- if (lastAssistantText.length > 20) {
2364
- return lastAssistantText;
2388
+ // Concatenate ALL assistant text fragments in order — keeps full
2389
+ // multi-turn reasoning instead of only the last fragment.
2390
+ const fullAssistantText = assistantFragments.join('\n\n').trim();
2391
+ if (fullAssistantText.length > 20) {
2392
+ return fullAssistantText;
2365
2393
  }
2366
2394
  // Otherwise build a summary from tool evidence
2367
2395
  const sections = [];
@@ -3200,13 +3228,19 @@ document.addEventListener('DOMContentLoaded', () => {
3200
3228
  // Prepend a forceful scope-enforcement instruction so the model
3201
3229
  // doesn't expand a small task into an oversized glossy page.
3202
3230
  const scopedPrompt = [
3203
- 'IMPORTANT — SCOPE CONSTRAINTS:',
3204
- '1. Follow the user\'s scope literally. Output ONLY what was requested.',
3205
- '2. If the user says "tiny", "small", "simple", or "minimal", produce ≤ 50 lines of code.',
3206
- '3. Do NOT add: hero sections, animations, gradients, Google Fonts, Font Awesome, responsive breakpoints, or landing-page patterns UNLESS the user explicitly requests them.',
3207
- '4. Do NOT add external CDN links or dependencies unless the user asks for them.',
3208
- '5. Prefer inline styles or a small <style> block. No large CSS frameworks.',
3209
- '6. Return raw code only no markdown fences, no explanations.',
3231
+ 'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
3232
+ '1. Output ONLY what the user explicitly asked for. Nothing more.',
3233
+ '2. If the prompt is 15 words, produce ≤ 80 lines of code maximum.',
3234
+ '3. If the user says "tiny", "small", "simple", "minimal", or "basic", produce 50 lines.',
3235
+ '4. NEVER add any of these unless the user EXPLICITLY requests them:',
3236
+ ' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
3237
+ ' - CSS animations, gradients, neon effects, glass-morphism, particles',
3238
+ ' - Google Fonts, Font Awesome, external CDN links, icon libraries',
3239
+ ' - Responsive breakpoints, media queries (unless asked)',
3240
+ ' - Multiple pages or components when one was requested',
3241
+ '5. Prefer inline styles or a small <style> block. No CSS frameworks.',
3242
+ '6. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
3243
+ '7. Match the complexity of the request: a "hello world" is 1-5 lines, a "button" is 5-15 lines.',
3210
3244
  '',
3211
3245
  prompt,
3212
3246
  ].join('\n');
@@ -3243,22 +3277,62 @@ document.addEventListener('DOMContentLoaded', () => {
3243
3277
  const response = await this.client.post('/api/ai/review', {
3244
3278
  code,
3245
3279
  language,
3280
+ instructions: 'Return concrete, line-specific issues with severity. Every issue MUST reference a line number. If the score is below 50, you MUST list at least 2 specific issues.',
3246
3281
  });
3247
3282
  const raw = response.data ?? {};
3248
3283
  const score = typeof raw.score === 'number' ? raw.score : 0;
3249
3284
  const issues = Array.isArray(raw.issues) ? raw.issues : [];
3250
3285
  const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
3251
- // Prevent contradictory output: low score but zero issues
3286
+ // Prevent contradictory output: low score but zero issues.
3287
+ // Run lightweight client-side heuristics to produce concrete findings.
3252
3288
  if (score < 50 && issues.length === 0) {
3253
- issues.push({
3254
- type: 'quality',
3255
- line: 0,
3256
- message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. This typically means the review model returned incomplete data. Consider re-running the review or inspecting the file manually.`,
3257
- severity: 'warning',
3258
- });
3289
+ const heuristic = this.heuristicCodeIssues(code, language);
3290
+ if (heuristic.length > 0) {
3291
+ issues.push(...heuristic);
3292
+ }
3293
+ else {
3294
+ issues.push({
3295
+ type: 'quality',
3296
+ line: 1,
3297
+ message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. Re-run the review or inspect the file manually.`,
3298
+ severity: 'warning',
3299
+ });
3300
+ }
3259
3301
  }
3260
3302
  return { score, issues, suggestions };
3261
3303
  }
3304
+ /**
3305
+ * Lightweight client-side heuristic scan: catches common code smells
3306
+ * so review never returns "score 30, no issues".
3307
+ */
3308
+ heuristicCodeIssues(code, language) {
3309
+ const issues = [];
3310
+ const lines = code.split('\n');
3311
+ for (let i = 0; i < lines.length; i++) {
3312
+ const line = lines[i];
3313
+ const lineNum = i + 1;
3314
+ // console.log left in production code
3315
+ if (/\bconsole\.(log|debug|info)\b/.test(line) && !/\/\//.test(line.slice(0, line.indexOf('console')))) {
3316
+ issues.push({ type: 'quality', line: lineNum, message: 'console.log/debug statement — remove or replace with proper logging.', severity: 'warning' });
3317
+ }
3318
+ // TODO/FIXME/HACK comments
3319
+ if (/\b(TODO|FIXME|HACK|XXX)\b/.test(line)) {
3320
+ issues.push({ type: 'maintainability', line: lineNum, message: `Unresolved ${line.match(/\b(TODO|FIXME|HACK|XXX)\b/)?.[0]} comment.`, severity: 'info' });
3321
+ }
3322
+ // Empty catch blocks
3323
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line)) {
3324
+ issues.push({ type: 'error-handling', line: lineNum, message: 'Empty catch block — errors are silently swallowed.', severity: 'warning' });
3325
+ }
3326
+ // Very long lines (> 200 chars)
3327
+ if (line.length > 200) {
3328
+ issues.push({ type: 'style', line: lineNum, message: `Line exceeds 200 characters (${line.length}).`, severity: 'info' });
3329
+ }
3330
+ // Limit to 10 heuristic issues
3331
+ if (issues.length >= 10)
3332
+ break;
3333
+ }
3334
+ return issues;
3335
+ }
3262
3336
  async fixCode(code, language, fixType) {
3263
3337
  // Client-side syntax pre-check: detect obvious errors and include
3264
3338
  // them in the request so the model has concrete signals.
@@ -5,7 +5,15 @@ import { type Options as OraOptions, type Ora } from 'ora';
5
5
  export type { Ora };
6
6
  /**
7
7
  * Create an ora spinner that writes to stderr so it never
8
- * pollutes stdout JSON output or triggers PowerShell error styling.
8
+ * pollutes stdout JSON output.
9
+ *
10
+ * IMPORTANT: On Windows PowerShell, any stderr output triggers
11
+ * NativeCommandError styling. The spinner animation itself is
12
+ * tolerable, but `spinner.fail(msg)` writes the message to stderr
13
+ * which produces ugly red PowerShell errors.
14
+ *
15
+ * Prefer: spinner.stop() then Logger.error(msg) — which writes to
16
+ * stdout — instead of spinner.fail(msg).
9
17
  */
10
18
  export declare function createSpinner(textOrOpts: string | OraOptions): Ora;
11
19
  export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'success';
@@ -12,7 +12,15 @@ const chalk_1 = __importDefault(require("chalk"));
12
12
  const ora_1 = __importDefault(require("ora"));
13
13
  /**
14
14
  * Create an ora spinner that writes to stderr so it never
15
- * pollutes stdout JSON output or triggers PowerShell error styling.
15
+ * pollutes stdout JSON output.
16
+ *
17
+ * IMPORTANT: On Windows PowerShell, any stderr output triggers
18
+ * NativeCommandError styling. The spinner animation itself is
19
+ * tolerable, but `spinner.fail(msg)` writes the message to stderr
20
+ * which produces ugly red PowerShell errors.
21
+ *
22
+ * Prefer: spinner.stop() then Logger.error(msg) — which writes to
23
+ * stdout — instead of spinner.fail(msg).
16
24
  */
17
25
  function createSpinner(textOrOpts) {
18
26
  const opts = typeof textOrOpts === 'string' ? { text: textOrOpts } : textOrOpts;
@@ -35,7 +43,10 @@ class Logger {
35
43
  console.log(chalk_1.default.yellow('⚠'), ...args);
36
44
  }
37
45
  error(...args) {
38
- console.error(chalk_1.default.red('✗'), ...args);
46
+ // Write error messages to stdout (not stderr) to avoid triggering
47
+ // PowerShell NativeCommandError styling. The red ✗ prefix already
48
+ // signals an error visually; stderr redirection is unnecessary.
49
+ console.log(chalk_1.default.red('✗'), ...args);
39
50
  }
40
51
  success(...args) {
41
52
  console.log(chalk_1.default.green('✓'), ...args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.20",
3
+ "version": "1.6.21",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [