vigthoria-cli 1.6.27 → 1.6.29

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.
@@ -217,25 +217,35 @@ class AuthCommand {
217
217
  console.log();
218
218
  console.log(chalk_1.default.white('Capability Truth:'));
219
219
  console.log(chalk_1.default.gray(' Overall: ') + (capabilityStatus.overallOk ? chalk_1.default.green('Verified') : chalk_1.default.yellow('Partial')));
220
+ // V3 Agent — used by agent/chat commands
220
221
  console.log(chalk_1.default.gray(' V3 Agent: ') + (capabilityStatus.v3Agent.ok ? chalk_1.default.green('Reachable') : chalk_1.default.red('Unavailable')));
221
222
  if (capabilityStatus.v3Agent.error) {
222
223
  console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.red(capabilityStatus.v3Agent.error));
223
224
  }
224
- console.log(chalk_1.default.gray(' Hyper Loop: ') + (capabilityStatus.hyperLoop.ok ? chalk_1.default.green('Reachable') : chalk_1.default.red('Unavailable')));
225
+ // Hyper Loop optional orchestration layer
226
+ console.log(chalk_1.default.gray(' Hyper Loop: ') + (capabilityStatus.hyperLoop.ok ? chalk_1.default.green('Reachable') : chalk_1.default.yellow('Unavailable (does not affect AI commands)')));
225
227
  if (capabilityStatus.hyperLoop.error) {
226
- console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.red(capabilityStatus.hyperLoop.error));
228
+ console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.yellow(capabilityStatus.hyperLoop.error));
227
229
  }
228
- console.log(chalk_1.default.gray(' Repo Memory: ') + (capabilityStatus.repoMemory.ok ? chalk_1.default.green('Active') : chalk_1.default.yellow('Unavailable')));
230
+ // Repo Memory separate auth scope, only affects repo commands
231
+ console.log(chalk_1.default.gray(' Repo Memory: ') + (capabilityStatus.repoMemory.ok ? chalk_1.default.green('Active') : chalk_1.default.yellow('Unavailable (does not affect AI commands)')));
229
232
  if (capabilityStatus.repoMemory.details?.compactContextLength !== undefined) {
230
233
  console.log(chalk_1.default.gray(' Compact Context: ') + chalk_1.default.cyan(`${capabilityStatus.repoMemory.details.compactContextLength} chars`));
231
234
  }
232
235
  if (capabilityStatus.repoMemory.error) {
233
236
  console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.yellow(capabilityStatus.repoMemory.error));
234
237
  }
235
- console.log(chalk_1.default.gray(' DevTools Bridge: ') + (capabilityStatus.devtoolsBridge.ok ? chalk_1.default.green('Reachable') : chalk_1.default.gray('Not running')));
238
+ // DevTools Bridge local service, not required
239
+ console.log(chalk_1.default.gray(' DevTools Bridge: ') + (capabilityStatus.devtoolsBridge.ok ? chalk_1.default.green('Reachable') : chalk_1.default.gray('Not running (optional)')));
236
240
  if (capabilityStatus.devtoolsBridge.error) {
237
241
  console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.gray(capabilityStatus.devtoolsBridge.error));
238
242
  }
243
+ // Auth scope summary
244
+ console.log();
245
+ console.log(chalk_1.default.white('Auth Scopes:'));
246
+ console.log(chalk_1.default.gray(' Model Auth: ') + (this.config.isAuthenticated() ? chalk_1.default.green('Active') : chalk_1.default.red('Missing')) + chalk_1.default.gray(' (used by chat, agent, review, explain, generate, fix)'));
247
+ console.log(chalk_1.default.gray(' Repo Auth: ') + (capabilityStatus.repoMemory.ok ? chalk_1.default.green('Active') : chalk_1.default.yellow('Inactive')) + chalk_1.default.gray(' (used by repo push/pull/list only)'));
248
+ console.log(chalk_1.default.gray(' Bridge Auth: ') + (capabilityStatus.devtoolsBridge.ok ? chalk_1.default.green('Connected') : chalk_1.default.gray('N/A')) + chalk_1.default.gray(' (used by --bridge flag only)'));
239
249
  console.log();
240
250
  }
241
251
  printLoginSuccess() {
@@ -846,7 +846,8 @@ class ChatCommand {
846
846
  if (spinner) {
847
847
  spinner.stop();
848
848
  }
849
- const errorMsg = error.message || 'Operator workflow failed with an unknown error.';
849
+ const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
850
+ const errorMsg = (0, api_js_1.formatCLIError)(cliErr);
850
851
  if (!this.jsonOutput) {
851
852
  this.logger.error('Operator workflow failed');
852
853
  }
@@ -857,6 +858,7 @@ class ChatCommand {
857
858
  mode: 'operator',
858
859
  content: '',
859
860
  error: errorMsg,
861
+ errorCategory: cliErr.category,
860
862
  }, null, 2));
861
863
  }
862
864
  else {
@@ -924,9 +926,8 @@ class ChatCommand {
924
926
  catch (error) {
925
927
  if (spinner)
926
928
  spinner.stop();
927
- if (!this.jsonOutput)
928
- this.logger.error('Failed to get response');
929
- const errorMsg = error.message;
929
+ const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
930
+ const errorMsg = (0, api_js_1.formatCLIError)(cliErr);
930
931
  if (this.jsonOutput) {
931
932
  process.exitCode = 1;
932
933
  console.log(JSON.stringify({
@@ -935,6 +936,7 @@ class ChatCommand {
935
936
  model: this.currentModel,
936
937
  content: '',
937
938
  error: errorMsg,
939
+ errorCategory: cliErr.category,
938
940
  }, null, 2));
939
941
  }
940
942
  else {
@@ -1002,13 +1004,15 @@ class ChatCommand {
1002
1004
  if (toolCalls.length === 0) {
1003
1005
  // Phase 5: Quality gate — if the agent tries to conclude on the first
1004
1006
  // turn without any discovery, push it to gather evidence first.
1005
- if (turn === 0 && this.agentToolEvidence.discovery === 0 && this.isDiagnosticPrompt(prompt)) {
1007
+ // Applies to diagnostic prompts AND any direct-prompt agent call where
1008
+ // the model failed to invoke tools (prevents truncated output).
1009
+ if (turn === 0 && this.agentToolEvidence.discovery === 0 && (this.isDiagnosticPrompt(prompt) || this.directPromptMode)) {
1006
1010
  this.messages.push({
1007
1011
  role: 'system',
1008
1012
  content: [
1009
1013
  'Quality gate: you concluded without using any discovery tools (list_dir, glob, read_file, grep).',
1010
- 'Before answering a diagnostic or audit question, you MUST inspect the project with tools.',
1011
- 'Use list_dir and read_file to gather concrete evidence, then provide your answer.',
1014
+ 'You MUST use tools to gather concrete evidence before providing your answer.',
1015
+ 'Use list_dir to explore the workspace, then read_file or grep to inspect relevant files.',
1012
1016
  ].join('\n'),
1013
1017
  });
1014
1018
  this.directToolContinuationCount += 1;
@@ -1044,8 +1048,8 @@ class ChatCommand {
1044
1048
  catch (error) {
1045
1049
  if (spinner)
1046
1050
  spinner.stop();
1047
- if (!this.jsonOutput)
1048
- this.logger.error('Agent request failed');
1051
+ const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
1052
+ const errorMsg = (0, api_js_1.formatCLIError)(cliErr);
1049
1053
  if (this.jsonOutput) {
1050
1054
  process.exitCode = 1;
1051
1055
  console.log(JSON.stringify({
@@ -1054,13 +1058,16 @@ class ChatCommand {
1054
1058
  model: this.currentModel,
1055
1059
  partial: false,
1056
1060
  content: '',
1057
- error: error.message,
1061
+ error: errorMsg,
1062
+ errorCategory: cliErr.category,
1058
1063
  metadata: {
1059
1064
  executionPath: 'local-agent-loop',
1060
1065
  },
1061
1066
  }, null, 2));
1062
1067
  }
1063
- this.logger.error(error.message);
1068
+ else {
1069
+ this.logger.error(errorMsg);
1070
+ }
1064
1071
  return;
1065
1072
  }
1066
1073
  }
@@ -108,7 +108,8 @@ Return the complete modified file content:`,
108
108
  }
109
109
  catch (error) {
110
110
  spinner.stop();
111
- this.logger.error('Edit failed:', error.message);
111
+ const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
112
+ this.logger.error((0, api_js_1.formatCLIError)(cliErr));
112
113
  }
113
114
  }
114
115
  async fix(filePath, options) {
@@ -173,7 +174,8 @@ Return the complete modified file content:`,
173
174
  }
174
175
  catch (error) {
175
176
  spinner.stop();
176
- this.logger.error('Fix failed:', error.message);
177
+ const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
178
+ this.logger.error((0, api_js_1.formatCLIError)(cliErr));
177
179
  }
178
180
  }
179
181
  extractCode(response, language) {
@@ -216,7 +218,46 @@ Return the complete modified file content:`,
216
218
  const len = lines.length;
217
219
  if (len < 4)
218
220
  return code;
219
- // Check if the second half is a near-duplicate of the first half
221
+ // Pass 1: Remove model stutter consecutive runs of 3+ identical
222
+ // non-empty lines are collapsed to one. A pair (exactly 2) is only
223
+ // collapsed when it occurs at the very end of the output (trailing
224
+ // stutter). Pairs in the middle are kept — they may be intentional
225
+ // (e.g. repeated data rows, CSS rules).
226
+ const deduped = [];
227
+ let i = 0;
228
+ while (i < lines.length) {
229
+ const trimmed = lines[i].trim();
230
+ if (!trimmed) {
231
+ deduped.push(lines[i]);
232
+ i++;
233
+ continue;
234
+ }
235
+ // Count the run length of identical consecutive lines
236
+ let runEnd = i + 1;
237
+ while (runEnd < lines.length && lines[runEnd].trim() === trimmed) {
238
+ runEnd++;
239
+ }
240
+ const runLen = runEnd - i;
241
+ if (runLen >= 3) {
242
+ // 3+ identical lines is almost certainly stutter — keep one
243
+ deduped.push(lines[i]);
244
+ }
245
+ else if (runLen === 2 && runEnd === lines.length) {
246
+ // Exactly 2 identical lines at the very end — trailing stutter
247
+ deduped.push(lines[i]);
248
+ }
249
+ else {
250
+ // 1 line, or a pair in the middle — keep all
251
+ for (let j = i; j < runEnd; j++) {
252
+ deduped.push(lines[j]);
253
+ }
254
+ }
255
+ i = runEnd;
256
+ }
257
+ if (deduped.length < lines.length) {
258
+ return deduped.join('\n');
259
+ }
260
+ // Pass 2: Check if the second half is a near-duplicate of the first half
220
261
  for (let splitAt = Math.floor(len * 0.4); splitAt <= Math.ceil(len * 0.6); splitAt++) {
221
262
  const firstHalf = lines.slice(0, splitAt);
222
263
  const secondHalf = lines.slice(splitAt).filter(l => l.trim() !== '');
@@ -75,7 +75,8 @@ class ExplainCommand {
75
75
  }
76
76
  catch (error) {
77
77
  spinner.stop();
78
- this.logger.error('Explanation failed:', error.message);
78
+ const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
79
+ this.logger.error((0, api_js_1.formatCLIError)(cliErr));
79
80
  }
80
81
  }
81
82
  formatExplanation(explanation, detail) {
@@ -105,7 +105,8 @@ class GenerateCommand {
105
105
  }
106
106
  catch (error) {
107
107
  spinner.stop();
108
- this.logger.error('Generation failed:', error.message);
108
+ const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
109
+ this.logger.error((0, api_js_1.formatCLIError)(cliErr));
109
110
  }
110
111
  }
111
112
  /**
@@ -65,7 +65,8 @@ class ReviewCommand {
65
65
  }
66
66
  catch (error) {
67
67
  spinner.stop();
68
- this.logger.error('Review failed:', error.message);
68
+ const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
69
+ this.logger.error((0, api_js_1.formatCLIError)(cliErr));
69
70
  }
70
71
  }
71
72
  printTextReview(review) {
@@ -4,6 +4,21 @@
4
4
  */
5
5
  import { Config } from './config.js';
6
6
  import { Logger } from './logger.js';
7
+ export type CLIErrorCategory = 'auth' | 'repo_session' | 'model_backend' | 'bridge' | 'network' | 'timeout' | 'parsing' | 'tool_execution';
8
+ export declare class CLIError extends Error {
9
+ category: CLIErrorCategory;
10
+ statusCode?: number;
11
+ endpoint?: string;
12
+ constructor(message: string, category: CLIErrorCategory, opts?: {
13
+ statusCode?: number;
14
+ endpoint?: string;
15
+ cause?: Error;
16
+ });
17
+ }
18
+ /** Classify an axios or fetch error into a structured CLIError. */
19
+ export declare function classifyError(error: unknown, fallbackCategory?: CLIErrorCategory): CLIError;
20
+ /** Format a CLIError for user-facing display. */
21
+ export declare function formatCLIError(err: CLIError): string;
7
22
  export interface ChatMessage {
8
23
  role: 'user' | 'assistant' | 'system';
9
24
  content: string;
@@ -395,6 +410,18 @@ export declare class APIClient {
395
410
  * Returns a human-readable description of obvious errors, or empty string.
396
411
  */
397
412
  private detectSyntaxErrors;
413
+ /**
414
+ * Strip comment lines that the model added during a fix but were not
415
+ * present in the original code. Used for syntax-only fixes where the
416
+ * model tends to annotate its changes with "// Fixed ..." comments.
417
+ */
418
+ private stripInjectedComments;
419
+ /**
420
+ * Ensure the fixed code hasn't lost closing delimiters relative to the
421
+ * original. Counts {, }, (, ), [, ] outside strings/comments and if
422
+ * the fix has fewer closers than the original, appends the missing ones.
423
+ */
424
+ private repairBracketBalance;
398
425
  private resolveModelId;
399
426
  private getCoderHealth;
400
427
  private getModelsHealth;
package/dist/utils/api.js CHANGED
@@ -7,7 +7,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  return (mod && mod.__esModule) ? mod : { "default": mod };
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.APIClient = void 0;
10
+ exports.APIClient = exports.CLIError = void 0;
11
+ exports.classifyError = classifyError;
12
+ exports.formatCLIError = formatCLIError;
11
13
  const axios_1 = __importDefault(require("axios"));
12
14
  const crypto_1 = require("crypto");
13
15
  const fs_1 = __importDefault(require("fs"));
@@ -15,6 +17,77 @@ const https_1 = __importDefault(require("https"));
15
17
  const net_1 = __importDefault(require("net"));
16
18
  const path_1 = __importDefault(require("path"));
17
19
  const ws_1 = __importDefault(require("ws"));
20
+ class CLIError extends Error {
21
+ category;
22
+ statusCode;
23
+ endpoint;
24
+ constructor(message, category, opts) {
25
+ super(message);
26
+ this.name = 'CLIError';
27
+ this.category = category;
28
+ this.statusCode = opts?.statusCode;
29
+ this.endpoint = opts?.endpoint;
30
+ if (opts?.cause)
31
+ this.cause = opts.cause;
32
+ }
33
+ }
34
+ exports.CLIError = CLIError;
35
+ /** Classify an axios or fetch error into a structured CLIError. */
36
+ function classifyError(error, fallbackCategory = 'network') {
37
+ if (error instanceof CLIError)
38
+ return error;
39
+ const axErr = error;
40
+ const status = axErr?.response?.status;
41
+ const endpoint = axErr?.config?.url || axErr?.config?.baseURL || '';
42
+ const message = axErr?.response?.data
43
+ ? typeof axErr.response.data.error === 'string'
44
+ ? axErr.response.data.error
45
+ : typeof axErr.response.data.message === 'string'
46
+ ? axErr.response.data.message
47
+ : error.message
48
+ : error.message || String(error);
49
+ if (status === 401 || status === 403) {
50
+ // Distinguish repo/community auth from model auth
51
+ if (/community|repo/i.test(endpoint)) {
52
+ return new CLIError(message, 'repo_session', { statusCode: status, endpoint });
53
+ }
54
+ return new CLIError(message, 'auth', { statusCode: status, endpoint });
55
+ }
56
+ if (status && status >= 500) {
57
+ return new CLIError(message, 'model_backend', { statusCode: status, endpoint });
58
+ }
59
+ if (/timeout|ETIMEDOUT|ESOCKETTIMEDOUT|aborted/i.test(message)) {
60
+ return new CLIError(message, 'timeout', { endpoint });
61
+ }
62
+ if (/ECONNREFUSED|ENOTFOUND|ENETUNREACH|EAI_AGAIN|fetch failed/i.test(message)) {
63
+ return new CLIError(message, 'network', { endpoint });
64
+ }
65
+ return new CLIError(message, fallbackCategory, { statusCode: status, endpoint, cause: error instanceof Error ? error : undefined });
66
+ }
67
+ /** Format a CLIError for user-facing display. */
68
+ function formatCLIError(err) {
69
+ const tag = `[${err.category}]`;
70
+ switch (err.category) {
71
+ case 'auth':
72
+ return `${tag} Authentication failed${err.statusCode ? ` (${err.statusCode})` : ''}. Run: vigthoria login`;
73
+ case 'repo_session':
74
+ return `${tag} Repository session expired or missing${err.statusCode ? ` (${err.statusCode})` : ''}. This does not affect AI commands. Re-authenticate repo with: vigthoria repo list`;
75
+ case 'model_backend':
76
+ return `${tag} Model backend error${err.statusCode ? ` (${err.statusCode})` : ''}: ${err.message}`;
77
+ case 'bridge':
78
+ return `${tag} Bridge connection error: ${err.message}`;
79
+ case 'network':
80
+ return `${tag} Network error: ${err.message}. Check your internet connection.`;
81
+ case 'timeout':
82
+ return `${tag} Request timed out: ${err.message}`;
83
+ case 'parsing':
84
+ return `${tag} Response parsing error: ${err.message}`;
85
+ case 'tool_execution':
86
+ return `${tag} Tool execution error: ${err.message}`;
87
+ default:
88
+ return `${tag} ${err.message}`;
89
+ }
90
+ }
18
91
  const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
19
92
  const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '1200000';
20
93
  const parsed = Number.parseInt(rawValue, 10);
@@ -26,9 +99,9 @@ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
26
99
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
27
100
  })();
28
101
  const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
29
- const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '3900000';
102
+ const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '300000';
30
103
  const parsed = Number.parseInt(rawValue, 10);
31
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 3900000;
104
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
32
105
  })();
33
106
  class APIClient {
34
107
  client;
@@ -102,16 +175,24 @@ class APIClient {
102
175
  }
103
176
  return req;
104
177
  });
105
- // Add response interceptor for token refresh
106
- this.client.interceptors.response.use((res) => res, async (error) => {
107
- if (error.response?.status === 401) {
108
- const refreshed = await this.refreshToken();
109
- if (refreshed && error.config) {
110
- return this.client.request(error.config);
178
+ // Add response interceptors for token refresh + structured errors
179
+ const createAuthRetryInterceptor = (client) => {
180
+ client.interceptors.response.use((res) => res, async (error) => {
181
+ if (error.response?.status === 401) {
182
+ const refreshed = await this.refreshToken();
183
+ if (refreshed && error.config) {
184
+ return client.request(error.config);
185
+ }
186
+ throw classifyError(error, 'auth');
111
187
  }
112
- }
113
- throw error;
114
- });
188
+ throw classifyError(error);
189
+ });
190
+ };
191
+ createAuthRetryInterceptor(this.client);
192
+ createAuthRetryInterceptor(this.modelRouterClient);
193
+ if (this.selfHostedModelRouterClient) {
194
+ createAuthRetryInterceptor(this.selfHostedModelRouterClient);
195
+ }
115
196
  }
116
197
  getSelfHostedModelsApiUrl() {
117
198
  const configuredUrl = process.env.VIGTHORIA_SELF_HOSTED_MODELS_API_URL
@@ -2932,13 +3013,18 @@ document.addEventListener('DOMContentLoaded', () => {
2932
3013
  };
2933
3014
  }
2934
3015
  catch (error) {
3016
+ const isAbort = error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
3017
+ if (isAbort) {
3018
+ const mins = Math.round(timeoutMs / 60000);
3019
+ throw new CLIError(`Operator workflow timed out after ${mins} minute(s). You can increase the timeout with VIGTHORIA_OPERATOR_TIMEOUT_MS.`, 'timeout', error);
3020
+ }
2935
3021
  errors.push(`${baseUrl}: ${error?.message || String(error)}`);
2936
3022
  }
2937
3023
  finally {
2938
3024
  clearTimeout(timeoutId);
2939
3025
  }
2940
3026
  }
2941
- throw new Error(errors.join(' | '));
3027
+ throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
2942
3028
  }
2943
3029
  /**
2944
3030
  * Chat API - Direct Vigthoria Models API Architecture
@@ -2970,7 +3056,7 @@ document.addEventListener('DOMContentLoaded', () => {
2970
3056
  }
2971
3057
  }
2972
3058
  // No more localhost fallbacks - CLI is for external users!
2973
- throw new Error('AI service unavailable. Please check your internet connection or try again later.');
3059
+ throw new CLIError('AI service unavailable. Please check your internet connection or try again later.', 'model_backend');
2974
3060
  }
2975
3061
  shouldSkipCloudRoutes(resolvedModel) {
2976
3062
  return this.shouldSimulateCloudFailure() && this.isCloudModelId(resolvedModel);
@@ -3589,7 +3675,7 @@ document.addEventListener('DOMContentLoaded', () => {
3589
3675
  if (fixType === 'bugs' || fixType === 'logic')
3590
3676
  return issue.type === 'logic' || issue.severity === 'error';
3591
3677
  if (fixType === 'syntax')
3592
- return issue.severity === 'error';
3678
+ return issue.type !== 'logic' && issue.severity === 'error';
3593
3679
  if (fixType === 'style')
3594
3680
  return issue.type === 'style' || issue.type === 'quality';
3595
3681
  if (fixType === 'security')
@@ -3602,9 +3688,19 @@ document.addEventListener('DOMContentLoaded', () => {
3602
3688
  .map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
3603
3689
  .join('\n// ');
3604
3690
  const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
3605
- const augmentedCode = allHints
3606
- ? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n// IMPORTANT: Fix ONLY these specific bugs. Do not add comments, do not restructure the code, do not add or remove lines beyond the minimal fix.\n\n${code}`
3607
- : code;
3691
+ // Build a constraint preamble that's stricter for syntax-only mode.
3692
+ let preamble;
3693
+ if (fixType === 'syntax') {
3694
+ preamble = allHints
3695
+ ? `// SYNTAX ERRORS DETECTED BY CLIENT:\n// ${allHints}\n// RULES: Fix ONLY bracket/parenthesis/brace mismatches and keyword typos. Do NOT rename functions, do NOT add comments, do NOT restructure or reformat. Output the corrected code ONLY.\n\n`
3696
+ : '';
3697
+ }
3698
+ else {
3699
+ preamble = allHints
3700
+ ? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n// IMPORTANT: Fix ONLY these specific bugs. Do not add comments, do not restructure the code, do not add or remove lines beyond the minimal fix.\n\n`
3701
+ : '';
3702
+ }
3703
+ const augmentedCode = preamble ? `${preamble}${code}` : code;
3608
3704
  const response = await this.client.post('/api/ai/fix', {
3609
3705
  code: augmentedCode,
3610
3706
  language,
@@ -3616,16 +3712,27 @@ document.addEventListener('DOMContentLoaded', () => {
3616
3712
  // If server returned no changes but we found issues, strip
3617
3713
  // our injected comment prefix from the returned code and attempt
3618
3714
  // a basic client-side repair.
3619
- if (changes.length === 0 && allHints && fixed === augmentedCode) {
3715
+ if (changes.length === 0 && preamble && fixed === augmentedCode) {
3620
3716
  fixed = code; // restore original
3621
3717
  }
3622
3718
  // Strip the injected comment block if it leaked into the output
3623
- if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT:')) {
3719
+ if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT')) {
3624
3720
  const idx = fixed.indexOf('\n\n');
3625
3721
  if (idx !== -1) {
3626
3722
  fixed = fixed.slice(idx + 2);
3627
3723
  }
3628
3724
  }
3725
+ // For syntax-only fixes, strip any comments the model added that
3726
+ // weren't in the original code (e.g. "// Fixed mismatched parentheses").
3727
+ if (fixType === 'syntax' && fixed !== code) {
3728
+ fixed = this.stripInjectedComments(code, fixed, language);
3729
+ }
3730
+ // Safety net: for syntax fixes, ensure the fix didn't make bracket
3731
+ // balance worse. If the original had more closing delimiters than
3732
+ // the fix, append the missing ones.
3733
+ if (fixType === 'syntax' && fixed !== code) {
3734
+ fixed = this.repairBracketBalance(code, fixed);
3735
+ }
3629
3736
  // If there are still no changes but the fixed code differs, compute
3630
3737
  // a semantic diff using LCS so inserted/removed lines don't cause
3631
3738
  // every subsequent line to appear as changed.
@@ -3806,6 +3913,125 @@ document.addEventListener('DOMContentLoaded', () => {
3806
3913
  }
3807
3914
  return errors.join('; ');
3808
3915
  }
3916
+ /**
3917
+ * Strip comment lines that the model added during a fix but were not
3918
+ * present in the original code. Used for syntax-only fixes where the
3919
+ * model tends to annotate its changes with "// Fixed ..." comments.
3920
+ */
3921
+ stripInjectedComments(original, fixed, language) {
3922
+ const lang = language.toLowerCase();
3923
+ // Only handle JS/TS/Python single-line comment patterns for now
3924
+ let wholeLineRe;
3925
+ let inlineRe;
3926
+ if (lang === 'python' || lang === 'py') {
3927
+ wholeLineRe = /^\s*#\s/;
3928
+ inlineRe = /\s+#\s.*$/;
3929
+ }
3930
+ else {
3931
+ wholeLineRe = /^\s*\/\/\s/;
3932
+ inlineRe = /\s+\/\/\s.*$/;
3933
+ }
3934
+ const origLines = original.split('\n');
3935
+ const origSet = new Set(origLines.map(l => l.trim()));
3936
+ const fixedLines = fixed.split('\n');
3937
+ const result = [];
3938
+ for (let idx = 0; idx < fixedLines.length; idx++) {
3939
+ let line = fixedLines[idx];
3940
+ // Remove whole-line comments not in original
3941
+ if (wholeLineRe.test(line) && !origSet.has(line.trim())) {
3942
+ continue;
3943
+ }
3944
+ // Strip inline trailing comments the model added.
3945
+ // Only strip if the corresponding original line at the same position
3946
+ // didn't have an inline comment at all.
3947
+ if (inlineRe.test(line)) {
3948
+ const origLine = idx < origLines.length ? origLines[idx] : '';
3949
+ if (!inlineRe.test(origLine)) {
3950
+ line = line.replace(inlineRe, '');
3951
+ }
3952
+ }
3953
+ result.push(line);
3954
+ }
3955
+ return result.join('\n');
3956
+ }
3957
+ /**
3958
+ * Ensure the fixed code hasn't lost closing delimiters relative to the
3959
+ * original. Counts {, }, (, ), [, ] outside strings/comments and if
3960
+ * the fix has fewer closers than the original, appends the missing ones.
3961
+ */
3962
+ repairBracketBalance(original, fixed) {
3963
+ const count = (src) => {
3964
+ let braces = 0, parens = 0, brackets = 0;
3965
+ let inStr = null;
3966
+ let inLine = false, inBlock = false;
3967
+ for (let i = 0; i < src.length; i++) {
3968
+ const ch = src[i], nx = src[i + 1] || '';
3969
+ if (inLine) {
3970
+ if (ch === '\n')
3971
+ inLine = false;
3972
+ continue;
3973
+ }
3974
+ if (inBlock) {
3975
+ if (ch === '*' && nx === '/') {
3976
+ inBlock = false;
3977
+ i++;
3978
+ }
3979
+ continue;
3980
+ }
3981
+ if (inStr) {
3982
+ if (ch === inStr && src[i - 1] !== '\\')
3983
+ inStr = null;
3984
+ continue;
3985
+ }
3986
+ if (ch === '/' && nx === '/') {
3987
+ inLine = true;
3988
+ continue;
3989
+ }
3990
+ if (ch === '/' && nx === '*') {
3991
+ inBlock = true;
3992
+ continue;
3993
+ }
3994
+ if (ch === '"' || ch === "'" || ch === '`') {
3995
+ inStr = ch;
3996
+ continue;
3997
+ }
3998
+ if (ch === '{')
3999
+ braces++;
4000
+ else if (ch === '}')
4001
+ braces--;
4002
+ else if (ch === '(')
4003
+ parens++;
4004
+ else if (ch === ')')
4005
+ parens--;
4006
+ else if (ch === '[')
4007
+ brackets++;
4008
+ else if (ch === ']')
4009
+ brackets--;
4010
+ }
4011
+ return { braces, parens, brackets };
4012
+ };
4013
+ const orig = count(original);
4014
+ const fix = count(fixed);
4015
+ const append = [];
4016
+ // If original was balanced (or close) but fix is missing closers,
4017
+ // append them. Only act when the fix is *more unbalanced* than original.
4018
+ if (fix.braces > orig.braces) {
4019
+ for (let i = 0; i < fix.braces - orig.braces; i++)
4020
+ append.push('}');
4021
+ }
4022
+ if (fix.parens > orig.parens) {
4023
+ for (let i = 0; i < fix.parens - orig.parens; i++)
4024
+ append.push(')');
4025
+ }
4026
+ if (fix.brackets > orig.brackets) {
4027
+ for (let i = 0; i < fix.brackets - orig.brackets; i++)
4028
+ append.push(']');
4029
+ }
4030
+ if (append.length > 0) {
4031
+ return fixed.trimEnd() + '\n' + append.join('\n');
4032
+ }
4033
+ return fixed;
4034
+ }
3809
4035
  // Model resolution - maps Vigthoria model names to internal IDs
3810
4036
  // INTERNAL USE ONLY - users see only Vigthoria branding
3811
4037
  resolveModelId(shortName) {
@@ -24,7 +24,12 @@ const ora_1 = __importDefault(require("ora"));
24
24
  */
25
25
  function createSpinner(textOrOpts) {
26
26
  const opts = typeof textOrOpts === 'string' ? { text: textOrOpts } : textOrOpts;
27
- return (0, ora_1.default)({ ...opts, stream: process.stderr });
27
+ // Suppress spinner animation when stderr is not a TTY (piped output,
28
+ // CI, non-interactive terminals). The spinner object still works — its
29
+ // .start()/.stop()/.succeed() methods are no-ops — so callers don't
30
+ // need conditional logic.
31
+ const isSilent = !process.stderr.isTTY;
32
+ return (0, ora_1.default)({ ...opts, stream: process.stderr, isSilent });
28
33
  }
29
34
  class Logger {
30
35
  verbose = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.6.27",
3
+ "version": "1.6.29",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "main": "dist/index.js",
6
6
  "files": [