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.
- package/dist/commands/auth.js +14 -4
- package/dist/commands/chat.js +18 -11
- package/dist/commands/edit.js +44 -3
- package/dist/commands/explain.js +2 -1
- package/dist/commands/generate.js +2 -1
- package/dist/commands/review.js +2 -1
- package/dist/utils/api.d.ts +27 -0
- package/dist/utils/api.js +246 -20
- package/dist/utils/logger.js +6 -1
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
228
|
+
console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.yellow(capabilityStatus.hyperLoop.error));
|
|
227
229
|
}
|
|
228
|
-
|
|
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
|
-
|
|
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() {
|
package/dist/commands/chat.js
CHANGED
|
@@ -846,7 +846,8 @@ class ChatCommand {
|
|
|
846
846
|
if (spinner) {
|
|
847
847
|
spinner.stop();
|
|
848
848
|
}
|
|
849
|
-
const
|
|
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
|
-
|
|
928
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
1011
|
-
'Use list_dir
|
|
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
|
-
|
|
1048
|
-
|
|
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:
|
|
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
|
-
|
|
1068
|
+
else {
|
|
1069
|
+
this.logger.error(errorMsg);
|
|
1070
|
+
}
|
|
1064
1071
|
return;
|
|
1065
1072
|
}
|
|
1066
1073
|
}
|
package/dist/commands/edit.js
CHANGED
|
@@ -108,7 +108,8 @@ Return the complete modified file content:`,
|
|
|
108
108
|
}
|
|
109
109
|
catch (error) {
|
|
110
110
|
spinner.stop();
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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() !== '');
|
package/dist/commands/explain.js
CHANGED
|
@@ -75,7 +75,8 @@ class ExplainCommand {
|
|
|
75
75
|
}
|
|
76
76
|
catch (error) {
|
|
77
77
|
spinner.stop();
|
|
78
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/dist/commands/review.js
CHANGED
|
@@ -65,7 +65,8 @@ class ReviewCommand {
|
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
67
|
spinner.stop();
|
|
68
|
-
|
|
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) {
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -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 || '
|
|
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 :
|
|
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
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
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 &&
|
|
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) {
|
package/dist/utils/logger.js
CHANGED
|
@@ -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
|
-
|
|
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;
|