vigthoria-cli 1.6.28 → 1.6.30
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 +18 -4
- package/dist/commands/chat.js +13 -8
- package/dist/commands/edit.js +66 -9
- package/dist/commands/explain.js +8 -1
- package/dist/commands/generate.js +8 -1
- package/dist/commands/review.js +8 -1
- package/dist/utils/api.d.ts +38 -0
- package/dist/utils/api.js +273 -19
- package/dist/utils/logger.js +6 -1
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -217,25 +217,39 @@ 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 — use a real server-side probe for Model Auth
|
|
244
|
+
const tokenValidation = await this.api.validateToken();
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(chalk_1.default.white('Auth Scopes:'));
|
|
247
|
+
console.log(chalk_1.default.gray(' Model Auth: ') + (tokenValidation.valid ? chalk_1.default.green('Valid') : chalk_1.default.red('Invalid')) + chalk_1.default.gray(' (used by chat, agent, review, explain, generate, fix)'));
|
|
248
|
+
if (!tokenValidation.valid && tokenValidation.error) {
|
|
249
|
+
console.log(chalk_1.default.gray(' ') + chalk_1.default.red(tokenValidation.error));
|
|
250
|
+
}
|
|
251
|
+
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)'));
|
|
252
|
+
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
253
|
console.log();
|
|
240
254
|
}
|
|
241
255
|
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 {
|
|
@@ -1046,8 +1048,8 @@ class ChatCommand {
|
|
|
1046
1048
|
catch (error) {
|
|
1047
1049
|
if (spinner)
|
|
1048
1050
|
spinner.stop();
|
|
1049
|
-
|
|
1050
|
-
|
|
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);
|
|
1051
1053
|
if (this.jsonOutput) {
|
|
1052
1054
|
process.exitCode = 1;
|
|
1053
1055
|
console.log(JSON.stringify({
|
|
@@ -1056,13 +1058,16 @@ class ChatCommand {
|
|
|
1056
1058
|
model: this.currentModel,
|
|
1057
1059
|
partial: false,
|
|
1058
1060
|
content: '',
|
|
1059
|
-
error:
|
|
1061
|
+
error: errorMsg,
|
|
1062
|
+
errorCategory: cliErr.category,
|
|
1060
1063
|
metadata: {
|
|
1061
1064
|
executionPath: 'local-agent-loop',
|
|
1062
1065
|
},
|
|
1063
1066
|
}, null, 2));
|
|
1064
1067
|
}
|
|
1065
|
-
|
|
1068
|
+
else {
|
|
1069
|
+
this.logger.error(errorMsg);
|
|
1070
|
+
}
|
|
1066
1071
|
return;
|
|
1067
1072
|
}
|
|
1068
1073
|
}
|
package/dist/commands/edit.js
CHANGED
|
@@ -29,6 +29,12 @@ class EditCommand {
|
|
|
29
29
|
this.logger.error('Not authenticated. Run: vigthoria login');
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
+
// Server-side token validation — fail fast instead of waiting for 401
|
|
33
|
+
const tokenCheck = await this.api.validateToken();
|
|
34
|
+
if (!tokenCheck.valid) {
|
|
35
|
+
this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
32
38
|
// Read file
|
|
33
39
|
const file = this.fileUtils.readFile(filePath);
|
|
34
40
|
if (!file) {
|
|
@@ -46,6 +52,11 @@ class EditCommand {
|
|
|
46
52
|
this.logger.error('The --apply flag requires --instruction. Example: vigthoria edit file.ts --apply --instruction "fix the bug"');
|
|
47
53
|
return;
|
|
48
54
|
}
|
|
55
|
+
// Non-TTY stdin cannot prompt interactively
|
|
56
|
+
if (!process.stdin.isTTY) {
|
|
57
|
+
this.logger.error('No --instruction provided and stdin is not interactive. Use: vigthoria edit file.ts --instruction "..."');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
49
60
|
const answer = await inquirer_1.default.prompt([
|
|
50
61
|
{
|
|
51
62
|
type: 'input',
|
|
@@ -93,11 +104,14 @@ Return the complete modified file content:`,
|
|
|
93
104
|
], options.model);
|
|
94
105
|
spinner.stop();
|
|
95
106
|
// Extract code from response
|
|
96
|
-
|
|
107
|
+
let modifiedCode = this.extractCode(response.message, file.language);
|
|
97
108
|
if (!modifiedCode) {
|
|
98
109
|
this.logger.error('Failed to generate valid code changes');
|
|
99
110
|
return;
|
|
100
111
|
}
|
|
112
|
+
// Always deduplicate — extractCode only dedupes on the no-fence
|
|
113
|
+
// fallback path, so fenced responses (the common case) need this.
|
|
114
|
+
modifiedCode = this.deduplicateCode(modifiedCode);
|
|
101
115
|
// Show diff and apply
|
|
102
116
|
if (options.apply) {
|
|
103
117
|
await this.applyFix(file.path, file.content, modifiedCode);
|
|
@@ -108,7 +122,8 @@ Return the complete modified file content:`,
|
|
|
108
122
|
}
|
|
109
123
|
catch (error) {
|
|
110
124
|
spinner.stop();
|
|
111
|
-
|
|
125
|
+
const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
|
|
126
|
+
this.logger.error((0, api_js_1.formatCLIError)(cliErr));
|
|
112
127
|
}
|
|
113
128
|
}
|
|
114
129
|
async fix(filePath, options) {
|
|
@@ -117,6 +132,12 @@ Return the complete modified file content:`,
|
|
|
117
132
|
this.logger.error('Not authenticated. Run: vigthoria login');
|
|
118
133
|
return;
|
|
119
134
|
}
|
|
135
|
+
// Server-side token validation — fail fast instead of waiting for 401
|
|
136
|
+
const tokenCheck = await this.api.validateToken();
|
|
137
|
+
if (!tokenCheck.valid) {
|
|
138
|
+
this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
120
141
|
// Read file
|
|
121
142
|
const file = this.fileUtils.readFile(filePath);
|
|
122
143
|
if (!file) {
|
|
@@ -173,7 +194,8 @@ Return the complete modified file content:`,
|
|
|
173
194
|
}
|
|
174
195
|
catch (error) {
|
|
175
196
|
spinner.stop();
|
|
176
|
-
|
|
197
|
+
const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
|
|
198
|
+
this.logger.error((0, api_js_1.formatCLIError)(cliErr));
|
|
177
199
|
}
|
|
178
200
|
}
|
|
179
201
|
extractCode(response, language) {
|
|
@@ -216,15 +238,45 @@ Return the complete modified file content:`,
|
|
|
216
238
|
const len = lines.length;
|
|
217
239
|
if (len < 4)
|
|
218
240
|
return code;
|
|
219
|
-
// Pass 1: Remove
|
|
241
|
+
// Pass 1: Remove model stutter — consecutive runs of 3+ identical
|
|
242
|
+
// non-empty lines are collapsed to one. A pair (exactly 2) is only
|
|
243
|
+
// collapsed when it occurs at the very end of the output (trailing
|
|
244
|
+
// stutter). Pairs in the middle are kept — they may be intentional
|
|
245
|
+
// (e.g. repeated data rows, CSS rules).
|
|
220
246
|
const deduped = [];
|
|
221
|
-
|
|
247
|
+
let i = 0;
|
|
248
|
+
while (i < lines.length) {
|
|
222
249
|
const trimmed = lines[i].trim();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
250
|
+
if (!trimmed) {
|
|
251
|
+
deduped.push(lines[i]);
|
|
252
|
+
i++;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Count the run length of identical consecutive lines
|
|
256
|
+
let runEnd = i + 1;
|
|
257
|
+
while (runEnd < lines.length && lines[runEnd].trim() === trimmed) {
|
|
258
|
+
runEnd++;
|
|
226
259
|
}
|
|
227
|
-
|
|
260
|
+
const runLen = runEnd - i;
|
|
261
|
+
// Check if this run is "effectively at end" — only trailing empty
|
|
262
|
+
// lines follow the duplicate pair.
|
|
263
|
+
const isAtEffectiveEnd = runEnd === lines.length
|
|
264
|
+
|| lines.slice(runEnd).every(l => l.trim() === '');
|
|
265
|
+
if (runLen >= 3) {
|
|
266
|
+
// 3+ identical lines is almost certainly stutter — keep one
|
|
267
|
+
deduped.push(lines[i]);
|
|
268
|
+
}
|
|
269
|
+
else if (runLen === 2 && isAtEffectiveEnd) {
|
|
270
|
+
// Exactly 2 identical lines at the very end — trailing stutter
|
|
271
|
+
deduped.push(lines[i]);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// 1 line, or a pair in the middle — keep all
|
|
275
|
+
for (let j = i; j < runEnd; j++) {
|
|
276
|
+
deduped.push(lines[j]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
i = runEnd;
|
|
228
280
|
}
|
|
229
281
|
if (deduped.length < lines.length) {
|
|
230
282
|
return deduped.join('\n');
|
|
@@ -269,6 +321,11 @@ Return the complete modified file content:`,
|
|
|
269
321
|
await this.applyFix(filePath, original, modified);
|
|
270
322
|
return;
|
|
271
323
|
}
|
|
324
|
+
// Non-TTY: show the diff but don't try to prompt — re-run with --apply
|
|
325
|
+
if (!process.stdin.isTTY) {
|
|
326
|
+
this.logger.info('Non-interactive mode. Re-run with --apply to apply changes.');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
272
329
|
const { action } = await inquirer_1.default.prompt([
|
|
273
330
|
{
|
|
274
331
|
type: 'list',
|
package/dist/commands/explain.js
CHANGED
|
@@ -33,6 +33,12 @@ class ExplainCommand {
|
|
|
33
33
|
this.logger.error('Not authenticated. Run: vigthoria login');
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
+
// Server-side token validation — fail fast instead of waiting for 401
|
|
37
|
+
const tokenCheck = await this.api.validateToken();
|
|
38
|
+
if (!tokenCheck.valid) {
|
|
39
|
+
this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
36
42
|
// Read file
|
|
37
43
|
const file = this.fileUtils.readFile(filePath);
|
|
38
44
|
if (!file) {
|
|
@@ -75,7 +81,8 @@ class ExplainCommand {
|
|
|
75
81
|
}
|
|
76
82
|
catch (error) {
|
|
77
83
|
spinner.stop();
|
|
78
|
-
|
|
84
|
+
const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
|
|
85
|
+
this.logger.error((0, api_js_1.formatCLIError)(cliErr));
|
|
79
86
|
}
|
|
80
87
|
}
|
|
81
88
|
formatExplanation(explanation, detail) {
|
|
@@ -31,6 +31,12 @@ class GenerateCommand {
|
|
|
31
31
|
this.logger.error('Not authenticated. Run: vigthoria login');
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
// Server-side token validation — fail fast instead of waiting for 401
|
|
35
|
+
const tokenCheck = await this.api.validateToken();
|
|
36
|
+
if (!tokenCheck.valid) {
|
|
37
|
+
this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
34
40
|
// Determine mode
|
|
35
41
|
const proMode = options.pro === true;
|
|
36
42
|
// Auto-detect language from description if not explicitly specified
|
|
@@ -105,7 +111,8 @@ class GenerateCommand {
|
|
|
105
111
|
}
|
|
106
112
|
catch (error) {
|
|
107
113
|
spinner.stop();
|
|
108
|
-
|
|
114
|
+
const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
|
|
115
|
+
this.logger.error((0, api_js_1.formatCLIError)(cliErr));
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
/**
|
package/dist/commands/review.js
CHANGED
|
@@ -33,6 +33,12 @@ class ReviewCommand {
|
|
|
33
33
|
this.logger.error('Not authenticated. Run: vigthoria login');
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
+
// Server-side token validation — fail fast instead of waiting for 401
|
|
37
|
+
const tokenCheck = await this.api.validateToken();
|
|
38
|
+
if (!tokenCheck.valid) {
|
|
39
|
+
this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
36
42
|
// Read file
|
|
37
43
|
const file = this.fileUtils.readFile(filePath);
|
|
38
44
|
if (!file) {
|
|
@@ -65,7 +71,8 @@ class ReviewCommand {
|
|
|
65
71
|
}
|
|
66
72
|
catch (error) {
|
|
67
73
|
spinner.stop();
|
|
68
|
-
|
|
74
|
+
const cliErr = error instanceof api_js_1.CLIError ? error : (0, api_js_1.classifyError)(error);
|
|
75
|
+
this.logger.error((0, api_js_1.formatCLIError)(cliErr));
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
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;
|
|
@@ -186,6 +201,17 @@ export declare class APIClient {
|
|
|
186
201
|
private refreshToken;
|
|
187
202
|
getSubscriptionStatus(): Promise<void>;
|
|
188
203
|
private getAccessToken;
|
|
204
|
+
/**
|
|
205
|
+
* Validate the current auth token against the Coder API.
|
|
206
|
+
* Returns { valid: true } when the server accepts the token,
|
|
207
|
+
* { valid: false, error } when the token is rejected (401/403),
|
|
208
|
+
* and { valid: true } when the server is unreachable (network error)
|
|
209
|
+
* so that offline/degraded scenarios don't block the user.
|
|
210
|
+
*/
|
|
211
|
+
validateToken(): Promise<{
|
|
212
|
+
valid: boolean;
|
|
213
|
+
error?: string;
|
|
214
|
+
}>;
|
|
189
215
|
getV3AgentBaseUrls(preferLocal?: boolean): string[];
|
|
190
216
|
getV3AgentRunUrl(baseUrl: string): string;
|
|
191
217
|
getV3AgentContinueUrl(baseUrl: string): string;
|
|
@@ -395,6 +421,18 @@ export declare class APIClient {
|
|
|
395
421
|
* Returns a human-readable description of obvious errors, or empty string.
|
|
396
422
|
*/
|
|
397
423
|
private detectSyntaxErrors;
|
|
424
|
+
/**
|
|
425
|
+
* Strip comment lines that the model added during a fix but were not
|
|
426
|
+
* present in the original code. Used for syntax-only fixes where the
|
|
427
|
+
* model tends to annotate its changes with "// Fixed ..." comments.
|
|
428
|
+
*/
|
|
429
|
+
private stripInjectedComments;
|
|
430
|
+
/**
|
|
431
|
+
* Ensure the fixed code hasn't lost closing delimiters relative to the
|
|
432
|
+
* original. Counts {, }, (, ), [, ] outside strings/comments and if
|
|
433
|
+
* the fix has fewer closers than the original, appends the missing ones.
|
|
434
|
+
*/
|
|
435
|
+
private repairBracketBalance;
|
|
398
436
|
private resolveModelId;
|
|
399
437
|
private getCoderHealth;
|
|
400
438
|
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
|
|
@@ -259,6 +340,34 @@ class APIClient {
|
|
|
259
340
|
|| this.config.get('authToken')
|
|
260
341
|
|| null;
|
|
261
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* Validate the current auth token against the Coder API.
|
|
345
|
+
* Returns { valid: true } when the server accepts the token,
|
|
346
|
+
* { valid: false, error } when the token is rejected (401/403),
|
|
347
|
+
* and { valid: true } when the server is unreachable (network error)
|
|
348
|
+
* so that offline/degraded scenarios don't block the user.
|
|
349
|
+
*/
|
|
350
|
+
async validateToken() {
|
|
351
|
+
const token = this.getAccessToken();
|
|
352
|
+
if (!token) {
|
|
353
|
+
return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
await this.client.get('/api/user/profile', { timeout: 10000 });
|
|
357
|
+
return { valid: true };
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (error instanceof CLIError && error.category === 'auth') {
|
|
361
|
+
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
362
|
+
}
|
|
363
|
+
const axErr = error;
|
|
364
|
+
if (axErr.response?.status === 401 || axErr.response?.status === 403) {
|
|
365
|
+
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
366
|
+
}
|
|
367
|
+
// Network/timeout errors — don't assume token is bad
|
|
368
|
+
return { valid: true };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
262
371
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
263
372
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
264
373
|
const allowLocalV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1' || preferLocal;
|
|
@@ -2932,13 +3041,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2932
3041
|
};
|
|
2933
3042
|
}
|
|
2934
3043
|
catch (error) {
|
|
3044
|
+
const isAbort = error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
|
|
3045
|
+
if (isAbort) {
|
|
3046
|
+
const mins = Math.round(timeoutMs / 60000);
|
|
3047
|
+
throw new CLIError(`Operator workflow timed out after ${mins} minute(s). You can increase the timeout with VIGTHORIA_OPERATOR_TIMEOUT_MS.`, 'timeout', error);
|
|
3048
|
+
}
|
|
2935
3049
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
2936
3050
|
}
|
|
2937
3051
|
finally {
|
|
2938
3052
|
clearTimeout(timeoutId);
|
|
2939
3053
|
}
|
|
2940
3054
|
}
|
|
2941
|
-
throw new
|
|
3055
|
+
throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
|
|
2942
3056
|
}
|
|
2943
3057
|
/**
|
|
2944
3058
|
* Chat API - Direct Vigthoria Models API Architecture
|
|
@@ -2970,7 +3084,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2970
3084
|
}
|
|
2971
3085
|
}
|
|
2972
3086
|
// No more localhost fallbacks - CLI is for external users!
|
|
2973
|
-
throw new
|
|
3087
|
+
throw new CLIError('AI service unavailable. Please check your internet connection or try again later.', 'model_backend');
|
|
2974
3088
|
}
|
|
2975
3089
|
shouldSkipCloudRoutes(resolvedModel) {
|
|
2976
3090
|
return this.shouldSimulateCloudFailure() && this.isCloudModelId(resolvedModel);
|
|
@@ -3602,9 +3716,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3602
3716
|
.map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
|
|
3603
3717
|
.join('\n// ');
|
|
3604
3718
|
const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3719
|
+
// Build a constraint preamble that's stricter for syntax-only mode.
|
|
3720
|
+
let preamble;
|
|
3721
|
+
if (fixType === 'syntax') {
|
|
3722
|
+
preamble = allHints
|
|
3723
|
+
? `// 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`
|
|
3724
|
+
: '';
|
|
3725
|
+
}
|
|
3726
|
+
else {
|
|
3727
|
+
preamble = allHints
|
|
3728
|
+
? `// 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`
|
|
3729
|
+
: '';
|
|
3730
|
+
}
|
|
3731
|
+
const augmentedCode = preamble ? `${preamble}${code}` : code;
|
|
3608
3732
|
const response = await this.client.post('/api/ai/fix', {
|
|
3609
3733
|
code: augmentedCode,
|
|
3610
3734
|
language,
|
|
@@ -3616,16 +3740,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3616
3740
|
// If server returned no changes but we found issues, strip
|
|
3617
3741
|
// our injected comment prefix from the returned code and attempt
|
|
3618
3742
|
// a basic client-side repair.
|
|
3619
|
-
if (changes.length === 0 &&
|
|
3743
|
+
if (changes.length === 0 && preamble && fixed === augmentedCode) {
|
|
3620
3744
|
fixed = code; // restore original
|
|
3621
3745
|
}
|
|
3622
3746
|
// 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
|
|
3747
|
+
if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT')) {
|
|
3624
3748
|
const idx = fixed.indexOf('\n\n');
|
|
3625
3749
|
if (idx !== -1) {
|
|
3626
3750
|
fixed = fixed.slice(idx + 2);
|
|
3627
3751
|
}
|
|
3628
3752
|
}
|
|
3753
|
+
// For syntax-only fixes, strip any comments the model added that
|
|
3754
|
+
// weren't in the original code (e.g. "// Fixed mismatched parentheses").
|
|
3755
|
+
if (fixType === 'syntax' && fixed !== code) {
|
|
3756
|
+
fixed = this.stripInjectedComments(code, fixed, language);
|
|
3757
|
+
}
|
|
3758
|
+
// Safety net: for syntax fixes, ensure the fix didn't make bracket
|
|
3759
|
+
// balance worse. If the original had more closing delimiters than
|
|
3760
|
+
// the fix, append the missing ones.
|
|
3761
|
+
if (fixType === 'syntax' && fixed !== code) {
|
|
3762
|
+
fixed = this.repairBracketBalance(code, fixed);
|
|
3763
|
+
}
|
|
3629
3764
|
// If there are still no changes but the fixed code differs, compute
|
|
3630
3765
|
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
3631
3766
|
// every subsequent line to appear as changed.
|
|
@@ -3806,6 +3941,125 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3806
3941
|
}
|
|
3807
3942
|
return errors.join('; ');
|
|
3808
3943
|
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Strip comment lines that the model added during a fix but were not
|
|
3946
|
+
* present in the original code. Used for syntax-only fixes where the
|
|
3947
|
+
* model tends to annotate its changes with "// Fixed ..." comments.
|
|
3948
|
+
*/
|
|
3949
|
+
stripInjectedComments(original, fixed, language) {
|
|
3950
|
+
const lang = language.toLowerCase();
|
|
3951
|
+
// Only handle JS/TS/Python single-line comment patterns for now
|
|
3952
|
+
let wholeLineRe;
|
|
3953
|
+
let inlineRe;
|
|
3954
|
+
if (lang === 'python' || lang === 'py') {
|
|
3955
|
+
wholeLineRe = /^\s*#\s/;
|
|
3956
|
+
inlineRe = /\s+#\s.*$/;
|
|
3957
|
+
}
|
|
3958
|
+
else {
|
|
3959
|
+
wholeLineRe = /^\s*\/\/\s/;
|
|
3960
|
+
inlineRe = /\s+\/\/\s.*$/;
|
|
3961
|
+
}
|
|
3962
|
+
const origLines = original.split('\n');
|
|
3963
|
+
const origSet = new Set(origLines.map(l => l.trim()));
|
|
3964
|
+
const fixedLines = fixed.split('\n');
|
|
3965
|
+
const result = [];
|
|
3966
|
+
for (let idx = 0; idx < fixedLines.length; idx++) {
|
|
3967
|
+
let line = fixedLines[idx];
|
|
3968
|
+
// Remove whole-line comments not in original
|
|
3969
|
+
if (wholeLineRe.test(line) && !origSet.has(line.trim())) {
|
|
3970
|
+
continue;
|
|
3971
|
+
}
|
|
3972
|
+
// Strip inline trailing comments the model added.
|
|
3973
|
+
// Only strip if the corresponding original line at the same position
|
|
3974
|
+
// didn't have an inline comment at all.
|
|
3975
|
+
if (inlineRe.test(line)) {
|
|
3976
|
+
const origLine = idx < origLines.length ? origLines[idx] : '';
|
|
3977
|
+
if (!inlineRe.test(origLine)) {
|
|
3978
|
+
line = line.replace(inlineRe, '');
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
result.push(line);
|
|
3982
|
+
}
|
|
3983
|
+
return result.join('\n');
|
|
3984
|
+
}
|
|
3985
|
+
/**
|
|
3986
|
+
* Ensure the fixed code hasn't lost closing delimiters relative to the
|
|
3987
|
+
* original. Counts {, }, (, ), [, ] outside strings/comments and if
|
|
3988
|
+
* the fix has fewer closers than the original, appends the missing ones.
|
|
3989
|
+
*/
|
|
3990
|
+
repairBracketBalance(original, fixed) {
|
|
3991
|
+
const count = (src) => {
|
|
3992
|
+
let braces = 0, parens = 0, brackets = 0;
|
|
3993
|
+
let inStr = null;
|
|
3994
|
+
let inLine = false, inBlock = false;
|
|
3995
|
+
for (let i = 0; i < src.length; i++) {
|
|
3996
|
+
const ch = src[i], nx = src[i + 1] || '';
|
|
3997
|
+
if (inLine) {
|
|
3998
|
+
if (ch === '\n')
|
|
3999
|
+
inLine = false;
|
|
4000
|
+
continue;
|
|
4001
|
+
}
|
|
4002
|
+
if (inBlock) {
|
|
4003
|
+
if (ch === '*' && nx === '/') {
|
|
4004
|
+
inBlock = false;
|
|
4005
|
+
i++;
|
|
4006
|
+
}
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
if (inStr) {
|
|
4010
|
+
if (ch === inStr && src[i - 1] !== '\\')
|
|
4011
|
+
inStr = null;
|
|
4012
|
+
continue;
|
|
4013
|
+
}
|
|
4014
|
+
if (ch === '/' && nx === '/') {
|
|
4015
|
+
inLine = true;
|
|
4016
|
+
continue;
|
|
4017
|
+
}
|
|
4018
|
+
if (ch === '/' && nx === '*') {
|
|
4019
|
+
inBlock = true;
|
|
4020
|
+
continue;
|
|
4021
|
+
}
|
|
4022
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
4023
|
+
inStr = ch;
|
|
4024
|
+
continue;
|
|
4025
|
+
}
|
|
4026
|
+
if (ch === '{')
|
|
4027
|
+
braces++;
|
|
4028
|
+
else if (ch === '}')
|
|
4029
|
+
braces--;
|
|
4030
|
+
else if (ch === '(')
|
|
4031
|
+
parens++;
|
|
4032
|
+
else if (ch === ')')
|
|
4033
|
+
parens--;
|
|
4034
|
+
else if (ch === '[')
|
|
4035
|
+
brackets++;
|
|
4036
|
+
else if (ch === ']')
|
|
4037
|
+
brackets--;
|
|
4038
|
+
}
|
|
4039
|
+
return { braces, parens, brackets };
|
|
4040
|
+
};
|
|
4041
|
+
const orig = count(original);
|
|
4042
|
+
const fix = count(fixed);
|
|
4043
|
+
const append = [];
|
|
4044
|
+
// If original was balanced (or close) but fix is missing closers,
|
|
4045
|
+
// append them. Only act when the fix is *more unbalanced* than original.
|
|
4046
|
+
if (fix.braces > orig.braces) {
|
|
4047
|
+
for (let i = 0; i < fix.braces - orig.braces; i++)
|
|
4048
|
+
append.push('}');
|
|
4049
|
+
}
|
|
4050
|
+
if (fix.parens > orig.parens) {
|
|
4051
|
+
for (let i = 0; i < fix.parens - orig.parens; i++)
|
|
4052
|
+
append.push(')');
|
|
4053
|
+
}
|
|
4054
|
+
if (fix.brackets > orig.brackets) {
|
|
4055
|
+
for (let i = 0; i < fix.brackets - orig.brackets; i++)
|
|
4056
|
+
append.push(']');
|
|
4057
|
+
}
|
|
4058
|
+
if (append.length > 0) {
|
|
4059
|
+
return fixed.trimEnd() + '\n' + append.join('\n');
|
|
4060
|
+
}
|
|
4061
|
+
return fixed;
|
|
4062
|
+
}
|
|
3809
4063
|
// Model resolution - maps Vigthoria model names to internal IDs
|
|
3810
4064
|
// INTERNAL USE ONLY - users see only Vigthoria branding
|
|
3811
4065
|
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;
|