vigthoria-cli 1.9.2 → 1.9.8
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/README.md +15 -5
- package/dist/commands/auth.d.ts +28 -38
- package/dist/commands/auth.js +461 -313
- package/dist/commands/bridge.js +3 -8
- package/dist/commands/chat.d.ts +3 -0
- package/dist/commands/chat.js +97 -34
- package/dist/commands/index.js +1 -1
- package/dist/commands/legion.d.ts +22 -19
- package/dist/commands/legion.js +561 -134
- package/dist/commands/preview.js +32 -7
- package/dist/commands/repo.js +19 -13
- package/dist/commands/security.d.ts +20 -0
- package/dist/commands/security.js +98 -0
- package/dist/commands/update.d.ts +9 -0
- package/dist/commands/update.js +235 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +147 -40
- package/dist/utils/api.d.ts +25 -70
- package/dist/utils/api.js +875 -693
- package/dist/utils/config.js +1 -1
- package/dist/utils/tools.d.ts +11 -0
- package/dist/utils/tools.js +251 -5
- package/install.ps1 +322 -0
- package/install.sh +314 -0
- package/package.json +18 -3
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +159 -0
- package/scripts/release/publish-cli-release.sh +73 -0
- package/scripts/release/validate-no-go-gates.sh +129 -0
- package/scripts/release/verify-runtime-consistency.mjs +64 -0
package/dist/utils/api.js
CHANGED
|
@@ -8,18 +8,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
8
8
|
};
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
exports.APIClient = exports.CLIError = void 0;
|
|
11
|
-
exports.handleApiError = handleApiError;
|
|
12
|
-
exports.handleAuthError = handleAuthError;
|
|
13
|
-
exports.propagateError = propagateError;
|
|
14
11
|
exports.classifyError = classifyError;
|
|
15
12
|
exports.formatCLIError = formatCLIError;
|
|
16
13
|
exports.sanitizeUserFacingErrorText = sanitizeUserFacingErrorText;
|
|
17
14
|
exports.isServerRuntime = isServerRuntime;
|
|
18
15
|
exports.describeUpstreamStatus = describeUpstreamStatus;
|
|
19
|
-
exports.
|
|
20
|
-
exports.validateJwt = validateJwt;
|
|
21
|
-
exports.refreshJwtIfNeeded = refreshJwtIfNeeded;
|
|
22
|
-
exports.createApiClient = createApiClient;
|
|
16
|
+
exports.propagateError = propagateError;
|
|
23
17
|
const axios_1 = __importDefault(require("axios"));
|
|
24
18
|
const crypto_1 = require("crypto");
|
|
25
19
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -27,202 +21,25 @@ const https_1 = __importDefault(require("https"));
|
|
|
27
21
|
const net_1 = __importDefault(require("net"));
|
|
28
22
|
const path_1 = __importDefault(require("path"));
|
|
29
23
|
const ws_1 = __importDefault(require("ws"));
|
|
30
|
-
const logger_js_1 = require("./logger.js");
|
|
31
|
-
const context_ranker_js_1 = require("./context-ranker.js");
|
|
32
|
-
const post_write_validator_js_1 = require("./post-write-validator.js");
|
|
33
|
-
const workspace_cache_js_1 = require("./workspace-cache.js");
|
|
34
24
|
class CLIError extends Error {
|
|
35
25
|
category;
|
|
36
26
|
statusCode;
|
|
37
27
|
endpoint;
|
|
38
|
-
code;
|
|
39
|
-
details;
|
|
40
|
-
isCritical;
|
|
41
28
|
constructor(message, category, opts) {
|
|
42
29
|
super(message);
|
|
43
30
|
this.name = 'CLIError';
|
|
44
31
|
this.category = category;
|
|
45
32
|
this.statusCode = opts?.statusCode;
|
|
46
33
|
this.endpoint = opts?.endpoint;
|
|
47
|
-
this.code = category === 'auth' || category === 'repo_session' ? 'AUTH_REQUIRED' : category.toUpperCase();
|
|
48
|
-
this.details = { status: opts?.statusCode, endpoint: opts?.endpoint };
|
|
49
|
-
this.isCritical = category === 'auth' || category === 'repo_session' || category === 'model_backend' || Boolean(opts?.statusCode && opts.statusCode >= 500);
|
|
50
34
|
if (opts?.cause)
|
|
51
35
|
this.cause = opts.cause;
|
|
52
36
|
}
|
|
53
37
|
}
|
|
54
38
|
exports.CLIError = CLIError;
|
|
55
|
-
function toCliError(error, fallbackCode = 'API_ERROR', fallbackMessage = 'API request failed') {
|
|
56
|
-
if (isCliError(error))
|
|
57
|
-
return error;
|
|
58
|
-
const axErr = error;
|
|
59
|
-
const status = axErr?.response?.status;
|
|
60
|
-
const fetchStatus = typeof error?.status === 'number' ? error.status : undefined;
|
|
61
|
-
const effectiveStatus = status ?? fetchStatus;
|
|
62
|
-
const data = axErr?.response?.data;
|
|
63
|
-
const message = typeof data?.error === 'string'
|
|
64
|
-
? data.error
|
|
65
|
-
: typeof data?.message === 'string'
|
|
66
|
-
? data.message
|
|
67
|
-
: error instanceof Error && error.message
|
|
68
|
-
? error.message
|
|
69
|
-
: fallbackMessage;
|
|
70
|
-
const code = effectiveStatus === 401 || effectiveStatus === 403
|
|
71
|
-
? 'AUTH_REQUIRED'
|
|
72
|
-
: effectiveStatus && effectiveStatus >= 500
|
|
73
|
-
? 'SERVER_ERROR'
|
|
74
|
-
: /timeout|ETIMEDOUT|ESOCKETTIMEDOUT|aborted/i.test(message)
|
|
75
|
-
? 'TIMEOUT'
|
|
76
|
-
: /ECONNREFUSED|ENOTFOUND|ENETUNREACH|EAI_AGAIN|fetch failed/i.test(message)
|
|
77
|
-
? 'NETWORK_ERROR'
|
|
78
|
-
: fallbackCode;
|
|
79
|
-
return {
|
|
80
|
-
code,
|
|
81
|
-
message,
|
|
82
|
-
details: {
|
|
83
|
-
status: effectiveStatus,
|
|
84
|
-
endpoint: axErr?.config?.url || axErr?.config?.baseURL,
|
|
85
|
-
data,
|
|
86
|
-
},
|
|
87
|
-
isCritical: code === 'AUTH_REQUIRED' || code === 'AUTH_REFRESH_FAILED' || code === 'SERVER_ERROR' || fallbackCode === 'API_ERROR',
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
function isCliError(error) {
|
|
91
|
-
return Boolean(error && typeof error === 'object' && 'code' in error && 'message' in error && 'isCritical' in error);
|
|
92
|
-
}
|
|
93
|
-
function isRetryableApiError(error) {
|
|
94
|
-
const status = error instanceof CLIError
|
|
95
|
-
? error.statusCode
|
|
96
|
-
: typeof error.details?.status === 'number'
|
|
97
|
-
? error.details.status
|
|
98
|
-
: undefined;
|
|
99
|
-
const code = String(error.code || '').toUpperCase();
|
|
100
|
-
const message = String(error.message || '');
|
|
101
|
-
if (status === 401 || status === 403)
|
|
102
|
-
return false;
|
|
103
|
-
if (status !== undefined && status >= 500)
|
|
104
|
-
return true;
|
|
105
|
-
return code === 'TIMEOUT'
|
|
106
|
-
|| code === 'NETWORK_ERROR'
|
|
107
|
-
|| /timeout|timed out|ECONNRESET|ECONNREFUSED|ENOTFOUND|ENETUNREACH|EAI_AGAIN/i.test(message);
|
|
108
|
-
}
|
|
109
|
-
function decodeJwtPayload(token) {
|
|
110
|
-
try {
|
|
111
|
-
const parts = token.split('.');
|
|
112
|
-
if (parts.length !== 3 || !parts[1])
|
|
113
|
-
return null;
|
|
114
|
-
const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/');
|
|
115
|
-
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), '=');
|
|
116
|
-
const decoded = JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
|
|
117
|
-
return decoded && typeof decoded === 'object' ? decoded : null;
|
|
118
|
-
}
|
|
119
|
-
catch (error) {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function getJwtExpiresAt(token) {
|
|
124
|
-
if (!token)
|
|
125
|
-
return null;
|
|
126
|
-
const payload = decodeJwtPayload(token);
|
|
127
|
-
const exp = payload?.exp;
|
|
128
|
-
return typeof exp === 'number' && Number.isFinite(exp) ? exp * 1000 : null;
|
|
129
|
-
}
|
|
130
|
-
const JWT_EXPIRY_SKEW_MS = 30_000;
|
|
131
|
-
function isJwtUsable(token) {
|
|
132
|
-
const expiresAt = getJwtExpiresAt(token);
|
|
133
|
-
return Boolean(token && expiresAt !== null && Date.now() + JWT_EXPIRY_SKEW_MS < expiresAt);
|
|
134
|
-
}
|
|
135
|
-
function isJwtExpired(token) {
|
|
136
|
-
const expiresAt = getJwtExpiresAt(token);
|
|
137
|
-
return Boolean(token && expiresAt !== null && Date.now() + JWT_EXPIRY_SKEW_MS >= expiresAt);
|
|
138
|
-
}
|
|
139
|
-
function shouldAttemptJwtRefresh(token) {
|
|
140
|
-
return Boolean(token && (!isJwtUsable(token) || isJwtExpired(token)));
|
|
141
|
-
}
|
|
142
|
-
function wrapApiError(error, fallbackCode = 'API_ERROR', fallbackMessage = 'API request failed') {
|
|
143
|
-
const cliError = toCliError(error, fallbackCode, fallbackMessage);
|
|
144
|
-
return { ...cliError, details: { ...(cliError.details || {}), retryable: isRetryableApiError(cliError) } };
|
|
145
|
-
}
|
|
146
|
-
function handleApiError(error) {
|
|
147
|
-
return wrapApiError(error, 'API_ERROR', 'API request failed');
|
|
148
|
-
}
|
|
149
|
-
function handleAuthError(error) {
|
|
150
|
-
const cliError = toCliError(error, 'AUTH_ERROR', 'Authentication failed. Please run: vigthoria login');
|
|
151
|
-
if (cliError.code === 'AUTH_REQUIRED' || cliError.code === 'AUTH_ERROR') {
|
|
152
|
-
return { ...cliError, code: 'AUTH_REQUIRED', message: cliError.message || 'Authentication failed. Please run: vigthoria login', isCritical: true };
|
|
153
|
-
}
|
|
154
|
-
return cliError;
|
|
155
|
-
}
|
|
156
|
-
function propagateError(err) {
|
|
157
|
-
const responseData = err?.response?.data;
|
|
158
|
-
const existingDetails = err?.details && typeof err.details === 'object' ? err.details : {};
|
|
159
|
-
const status = typeof err?.response?.status === 'number'
|
|
160
|
-
? err.response.status
|
|
161
|
-
: typeof err?.status === 'number'
|
|
162
|
-
? err.status
|
|
163
|
-
: typeof err?.statusCode === 'number'
|
|
164
|
-
? err.statusCode
|
|
165
|
-
: typeof existingDetails.status === 'number'
|
|
166
|
-
? existingDetails.status
|
|
167
|
-
: typeof err?.code === 'number'
|
|
168
|
-
? err.code
|
|
169
|
-
: 500;
|
|
170
|
-
const endpoint = err?.endpoint || err?.config?.url || err?.config?.baseURL || existingDetails.endpoint || 'unknown';
|
|
171
|
-
const command = err?.commandName || err?.command || existingDetails.command || 'unknown';
|
|
172
|
-
const message = typeof responseData?.error === 'string'
|
|
173
|
-
? responseData.error
|
|
174
|
-
: typeof responseData?.message === 'string'
|
|
175
|
-
? responseData.message
|
|
176
|
-
: typeof err?.message === 'string' && err.message
|
|
177
|
-
? err.message
|
|
178
|
-
: 'API request failed';
|
|
179
|
-
const originalCode = err?.code;
|
|
180
|
-
const isAuthError = status === 401
|
|
181
|
-
|| status === 403
|
|
182
|
-
|| err?.isAuthError === true
|
|
183
|
-
|| String(originalCode || '').toUpperCase() === 'AUTH_REQUIRED';
|
|
184
|
-
const apiError = {
|
|
185
|
-
code: status,
|
|
186
|
-
message,
|
|
187
|
-
details: {
|
|
188
|
-
...existingDetails,
|
|
189
|
-
command,
|
|
190
|
-
endpoint,
|
|
191
|
-
status,
|
|
192
|
-
data: responseData ?? existingDetails.data,
|
|
193
|
-
originalCode,
|
|
194
|
-
originalName: err?.name,
|
|
195
|
-
},
|
|
196
|
-
isAuthError,
|
|
197
|
-
};
|
|
198
|
-
const logPayload = {
|
|
199
|
-
code: apiError.code,
|
|
200
|
-
message: apiError.message,
|
|
201
|
-
command,
|
|
202
|
-
endpoint,
|
|
203
|
-
status,
|
|
204
|
-
isAuthError: apiError.isAuthError,
|
|
205
|
-
};
|
|
206
|
-
try {
|
|
207
|
-
console.error('[Vigthoria API Error]', JSON.stringify(logPayload));
|
|
208
|
-
}
|
|
209
|
-
catch {
|
|
210
|
-
console.error('[Vigthoria API Error]', logPayload);
|
|
211
|
-
}
|
|
212
|
-
throw apiError;
|
|
213
|
-
}
|
|
214
39
|
/** Classify an axios or fetch error into a structured CLIError. */
|
|
215
40
|
function classifyError(error, fallbackCategory = 'network') {
|
|
216
41
|
if (error instanceof CLIError)
|
|
217
42
|
return error;
|
|
218
|
-
const structuredApiError = error;
|
|
219
|
-
if (structuredApiError && typeof structuredApiError.code === 'number' && typeof structuredApiError.message === 'string') {
|
|
220
|
-
return new CLIError(structuredApiError.message, structuredApiError.isAuthError ? 'auth' : structuredApiError.code >= 500 ? 'model_backend' : fallbackCategory, {
|
|
221
|
-
statusCode: structuredApiError.code,
|
|
222
|
-
endpoint: typeof structuredApiError.details?.endpoint === 'string' ? structuredApiError.details.endpoint : undefined,
|
|
223
|
-
cause: error instanceof Error ? error : undefined,
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
43
|
const axErr = error;
|
|
227
44
|
const status = axErr?.response?.status;
|
|
228
45
|
const endpoint = axErr?.config?.url || axErr?.config?.baseURL || '';
|
|
@@ -275,183 +92,121 @@ function formatCLIError(err) {
|
|
|
275
92
|
return `${tag} ${err.message}`;
|
|
276
93
|
}
|
|
277
94
|
}
|
|
278
|
-
// Sanitize an upstream error string before exposing it to the end user.
|
|
279
95
|
function sanitizeUserFacingErrorText(input) {
|
|
280
|
-
|
|
96
|
+
const raw = String(input || '').trim();
|
|
97
|
+
if (!raw) {
|
|
281
98
|
return '';
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
out = out.replace(/\b(?:localhost|127\.0\.0\.1)(?::\d+)?\b/gi, '[redacted-host]');
|
|
286
|
-
out = out.replace(/\b[a-z0-9.-]+\.vigthoria\.io\b/gi, '[redacted-host]');
|
|
287
|
-
out = out.replace(/(?:[A-Za-z]:)?[\\/](?:var|opt|tmp|home|root|etc|usr)[\\/][^\s'"<>)]*/gi, '[redacted-path]');
|
|
288
|
-
out = out.replace(/[A-Za-z]:\\[^\s'"<>)]+/g, '[redacted-path]');
|
|
289
|
-
out = out.replace(/\\\\[^\s'"<>)]+/g, '[redacted-path]');
|
|
290
|
-
out = out.replace(/\s+/g, ' ').trim();
|
|
291
|
-
if (out.length > 160)
|
|
292
|
-
out = out.slice(0, 160) + '...';
|
|
293
|
-
return out;
|
|
294
|
-
}
|
|
295
|
-
function isServerRuntime() {
|
|
296
|
-
if (process.env.VIGTHORIA_RUN_MODE === 'server')
|
|
297
|
-
return true;
|
|
298
|
-
if (process.env.VIGTHORIA_SERVER_RUNTIME === '1')
|
|
299
|
-
return true;
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
function describeUpstreamStatus(status) {
|
|
303
|
-
if (status === 401 || status === 403)
|
|
304
|
-
return 'Authentication failed. Please run vigthoria login.';
|
|
305
|
-
if (status === 404)
|
|
306
|
-
return 'Requested service endpoint was not found.';
|
|
307
|
-
if (status === 408 || status === 504)
|
|
308
|
-
return 'Upstream service timed out.';
|
|
309
|
-
if (status === 429)
|
|
310
|
-
return 'Rate limit reached. Please retry shortly.';
|
|
311
|
-
if (status >= 500)
|
|
312
|
-
return 'Upstream service is temporarily unavailable.';
|
|
313
|
-
if (status >= 400)
|
|
314
|
-
return 'Request was rejected by the service.';
|
|
315
|
-
return 'Unexpected response from service.';
|
|
99
|
+
}
|
|
100
|
+
const withoutTags = raw.replace(/<[^>]+>/g, ' ');
|
|
101
|
+
return withoutTags.replace(/\s+/g, ' ').trim();
|
|
316
102
|
}
|
|
317
|
-
const
|
|
318
|
-
function
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const payload = decodeJwtPayload(token);
|
|
322
|
-
const exp = payload?.exp;
|
|
323
|
-
if (typeof exp !== 'number' || !Number.isFinite(exp))
|
|
324
|
-
return false;
|
|
325
|
-
return Date.now() + JWT_VALIDATE_EXPIRY_SKEW_MS < exp * 1000;
|
|
103
|
+
const TRUSTED_TOKEN_HOST_PATTERN = /(^|\.)vigthoria\.io$/i;
|
|
104
|
+
function isLoopbackHost(hostname) {
|
|
105
|
+
const host = String(hostname || '').toLowerCase();
|
|
106
|
+
return host === 'localhost' || host === '127.0.0.1';
|
|
326
107
|
}
|
|
327
|
-
function
|
|
328
|
-
if (!token || typeof token !== 'string')
|
|
329
|
-
return null;
|
|
330
|
-
const payload = decodeJwtPayload(token);
|
|
331
|
-
if (!payload)
|
|
332
|
-
return null;
|
|
108
|
+
function isTrustedTokenDestination(rawUrl) {
|
|
333
109
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return null;
|
|
338
|
-
return payload;
|
|
110
|
+
const parsed = new URL(rawUrl);
|
|
111
|
+
const host = parsed.hostname.toLowerCase();
|
|
112
|
+
return TRUSTED_TOKEN_HOST_PATTERN.test(host) || isLoopbackHost(host);
|
|
339
113
|
}
|
|
340
|
-
catch
|
|
341
|
-
|
|
342
|
-
// Avoid logging token contents; only expose parser failure class for diagnostics.
|
|
343
|
-
console.debug('Failed to validate JWT payload:', error instanceof Error ? error.message : String(error));
|
|
344
|
-
}
|
|
345
|
-
return null;
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
346
116
|
}
|
|
347
117
|
}
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
118
|
+
function resolveAxiosRequestUrl(req) {
|
|
119
|
+
const direct = String(req?.url || '').trim();
|
|
120
|
+
const base = String(req?.baseURL || '').trim();
|
|
121
|
+
if (!direct && !base)
|
|
122
|
+
return '';
|
|
123
|
+
if (/^https?:\/\//i.test(direct))
|
|
124
|
+
return direct;
|
|
125
|
+
if (base) {
|
|
126
|
+
try {
|
|
127
|
+
return new URL(direct || '', base).toString();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return base;
|
|
356
131
|
}
|
|
357
|
-
const decodedExpiresAt = getJwtExpiresAt(currentToken);
|
|
358
|
-
state.token = currentToken;
|
|
359
|
-
state.expiresAt = decodedExpiresAt;
|
|
360
|
-
const expired = (typeof state.isExpired === 'function' ? state.isExpired() : shouldAttemptJwtRefresh(currentToken)) || decodedExpiresAt === null;
|
|
361
|
-
if (!expired) {
|
|
362
|
-
return currentToken;
|
|
363
|
-
}
|
|
364
|
-
const refreshToken = runtimeClient.getRefreshToken?.() || runtimeClient.config?.get('refreshToken');
|
|
365
|
-
if (refreshToken && runtimeClient.refreshToken) {
|
|
366
|
-
const refreshed = await runtimeClient.refreshToken().catch((error) => {
|
|
367
|
-
runtimeClient.logger?.debug?.(`JWT refresh request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
368
|
-
throw error;
|
|
369
|
-
});
|
|
370
|
-
const nextToken = runtimeClient.getAccessToken?.() || runtimeClient.config?.get('authToken') || null;
|
|
371
|
-
if (refreshed && nextToken && validateJwtExpiry(nextToken)) {
|
|
372
|
-
state.token = nextToken;
|
|
373
|
-
state.expiresAt = getJwtExpiresAt(nextToken);
|
|
374
|
-
return nextToken;
|
|
375
|
-
}
|
|
376
|
-
runtimeClient.logger?.debug?.('JWT refresh endpoint did not return a usable replacement token');
|
|
377
|
-
}
|
|
378
|
-
state.token = null;
|
|
379
|
-
state.expiresAt = null;
|
|
380
|
-
runtimeClient.config?.clearAuth();
|
|
381
|
-
throw new CLIError('Authentication token is expired or invalid. Please run: vigthoria login', 'auth');
|
|
382
|
-
}
|
|
383
|
-
catch (error) {
|
|
384
|
-
if (error instanceof CLIError) {
|
|
385
|
-
throw error;
|
|
386
|
-
}
|
|
387
|
-
const cliError = toCliError(error, 'AUTH_REFRESH_FAILED', 'Authentication refresh failed. Please run: vigthoria login');
|
|
388
|
-
state.token = null;
|
|
389
|
-
state.expiresAt = null;
|
|
390
|
-
runtimeClient.config?.clearAuth();
|
|
391
|
-
throw new CLIError(cliError.message, 'auth', {
|
|
392
|
-
statusCode: typeof cliError.details?.status === 'number' ? cliError.details.status : undefined,
|
|
393
|
-
endpoint: typeof cliError.details?.endpoint === 'string' ? cliError.details.endpoint : undefined,
|
|
394
|
-
cause: error instanceof Error ? error : undefined,
|
|
395
|
-
});
|
|
396
132
|
}
|
|
133
|
+
return direct;
|
|
397
134
|
}
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
135
|
+
function isServerRuntime() {
|
|
136
|
+
if (process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1') {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
const host = String(process.env.HOSTNAME || '').toLowerCase();
|
|
140
|
+
const cwd = String(process.cwd() || '').toLowerCase();
|
|
141
|
+
return host.includes('ubuntu') || cwd.startsWith('/var/www');
|
|
142
|
+
}
|
|
143
|
+
function describeUpstreamStatus(status) {
|
|
144
|
+
if (status >= 500)
|
|
145
|
+
return 'upstream internal error';
|
|
146
|
+
if (status === 429)
|
|
147
|
+
return 'rate limited';
|
|
148
|
+
if (status === 404)
|
|
149
|
+
return 'endpoint not found';
|
|
150
|
+
if (status === 403)
|
|
151
|
+
return 'forbidden';
|
|
152
|
+
if (status === 401)
|
|
153
|
+
return 'unauthorized';
|
|
154
|
+
if (status >= 400)
|
|
155
|
+
return 'bad request';
|
|
156
|
+
return 'ok';
|
|
157
|
+
}
|
|
158
|
+
function propagateError(err) {
|
|
159
|
+
const status = typeof err?.statusCode === 'number'
|
|
160
|
+
? err.statusCode
|
|
161
|
+
: typeof err?.status === 'number'
|
|
162
|
+
? err.status
|
|
163
|
+
: typeof err?.response?.status === 'number'
|
|
164
|
+
? err.response.status
|
|
165
|
+
: 500;
|
|
166
|
+
const endpoint = err?.endpoint || err?.config?.url || err?.details?.endpoint || 'unknown';
|
|
167
|
+
const message = sanitizeUserFacingErrorText(String(err?.message || 'API request failed'));
|
|
168
|
+
throw {
|
|
169
|
+
code: status,
|
|
170
|
+
message,
|
|
171
|
+
isAuthError: status === 401 || status === 403,
|
|
172
|
+
details: {
|
|
173
|
+
...(err?.details && typeof err.details === 'object' ? err.details : {}),
|
|
174
|
+
endpoint,
|
|
175
|
+
status,
|
|
176
|
+
originalCode: err?.code,
|
|
422
177
|
},
|
|
423
178
|
};
|
|
424
179
|
}
|
|
425
180
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
426
|
-
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS
|
|
181
|
+
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
182
|
+
if (!rawValue) {
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
427
185
|
const parsed = Number.parseInt(rawValue, 10);
|
|
428
|
-
return Number.isFinite(parsed) && parsed
|
|
186
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
429
187
|
})();
|
|
430
188
|
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
431
189
|
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS || '90000';
|
|
432
190
|
const parsed = Number.parseInt(rawValue, 10);
|
|
433
191
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
434
192
|
})();
|
|
435
|
-
const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
|
|
436
|
-
const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS || '300000';
|
|
437
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
438
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
|
|
439
|
-
})();
|
|
440
193
|
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
441
|
-
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS
|
|
194
|
+
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS;
|
|
195
|
+
if (!rawValue) {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
442
198
|
const parsed = Number.parseInt(rawValue, 10);
|
|
443
|
-
return Number.isFinite(parsed) && parsed
|
|
199
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
444
200
|
})();
|
|
445
201
|
class APIClient {
|
|
446
202
|
client;
|
|
447
203
|
modelRouterClient;
|
|
448
204
|
selfHostedModelRouterClient;
|
|
449
205
|
config;
|
|
450
|
-
token = null;
|
|
451
|
-
expiresAt = null;
|
|
452
206
|
logger;
|
|
453
207
|
ws = null;
|
|
454
208
|
vigFlowTokens = new Map();
|
|
209
|
+
_httpsAgent = null;
|
|
455
210
|
constructor(config, logger) {
|
|
456
211
|
this.config = config;
|
|
457
212
|
this.logger = logger;
|
|
@@ -461,6 +216,7 @@ class APIClient {
|
|
|
461
216
|
keepAlive: true,
|
|
462
217
|
timeout: 30000,
|
|
463
218
|
});
|
|
219
|
+
this._httpsAgent = httpsAgent;
|
|
464
220
|
// Main Vigthoria Coder API (coder.vigthoria.io)
|
|
465
221
|
this.client = axios_1.default.create({
|
|
466
222
|
baseURL: config.get('apiUrl'),
|
|
@@ -492,77 +248,44 @@ class APIClient {
|
|
|
492
248
|
'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
|
|
493
249
|
},
|
|
494
250
|
}) : null;
|
|
495
|
-
// Add auth
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
251
|
+
// Add auth interceptor
|
|
252
|
+
this.client.interceptors.request.use((req) => {
|
|
253
|
+
const token = this.getAccessToken();
|
|
254
|
+
const destination = resolveAxiosRequestUrl(req);
|
|
255
|
+
if (token && isTrustedTokenDestination(destination)) {
|
|
256
|
+
req.headers.Authorization = `Bearer ${token}`;
|
|
257
|
+
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
258
|
+
}
|
|
259
|
+
return req;
|
|
260
|
+
});
|
|
261
|
+
this.modelRouterClient.interceptors.request.use((req) => {
|
|
262
|
+
const token = this.getAccessToken();
|
|
263
|
+
const destination = resolveAxiosRequestUrl(req);
|
|
264
|
+
if (token && isTrustedTokenDestination(destination)) {
|
|
265
|
+
req.headers.Authorization = `Bearer ${token}`;
|
|
266
|
+
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
267
|
+
}
|
|
268
|
+
return req;
|
|
269
|
+
});
|
|
270
|
+
this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
|
|
271
|
+
const token = this.getAccessToken();
|
|
272
|
+
const destination = resolveAxiosRequestUrl(req);
|
|
273
|
+
if (token && isTrustedTokenDestination(destination)) {
|
|
274
|
+
req.headers.Authorization = `Bearer ${token}`;
|
|
275
|
+
}
|
|
276
|
+
return req;
|
|
277
|
+
});
|
|
278
|
+
// Add response interceptors for token refresh + structured errors
|
|
522
279
|
const createAuthRetryInterceptor = (client) => {
|
|
523
280
|
client.interceptors.response.use((res) => res, async (error) => {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if (error.
|
|
527
|
-
|
|
528
|
-
throw new CLIError('Authentication failed after token refresh. Please run: vigthoria login', 'auth', {
|
|
529
|
-
statusCode: 401,
|
|
530
|
-
endpoint: originalConfig.url,
|
|
531
|
-
cause: error instanceof Error ? error : undefined,
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
const refreshedToken = await refreshJwtIfNeeded(this);
|
|
535
|
-
if (refreshedToken && originalConfig) {
|
|
536
|
-
originalConfig.__authRetry = true;
|
|
537
|
-
const token = refreshedToken;
|
|
538
|
-
if (token) {
|
|
539
|
-
originalConfig.headers = originalConfig.headers || {};
|
|
540
|
-
originalConfig.headers.Authorization = `Bearer ${token}`;
|
|
541
|
-
originalConfig.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
542
|
-
}
|
|
543
|
-
return client.request(originalConfig);
|
|
544
|
-
}
|
|
545
|
-
throw new CLIError('Authentication token expired or was rejected. Please run: vigthoria login', 'auth', {
|
|
546
|
-
statusCode: 401,
|
|
547
|
-
endpoint: originalConfig?.url,
|
|
548
|
-
cause: error instanceof Error ? error : undefined,
|
|
549
|
-
});
|
|
281
|
+
if (error.response?.status === 401) {
|
|
282
|
+
const refreshed = await this.refreshToken();
|
|
283
|
+
if (refreshed && error.config) {
|
|
284
|
+
return client.request(error.config);
|
|
550
285
|
}
|
|
551
|
-
throw classifyError(error);
|
|
552
|
-
}
|
|
553
|
-
catch (interceptorError) {
|
|
554
|
-
if (interceptorError instanceof CLIError) {
|
|
555
|
-
throw interceptorError;
|
|
556
|
-
}
|
|
557
|
-
const authError = error.response?.status === 401
|
|
558
|
-
? handleAuthError(interceptorError)
|
|
559
|
-
: toCliError(interceptorError, 'API_ERROR', 'API request failed');
|
|
560
|
-
throw new CLIError(authError.message, authError.code === 'AUTH_REQUIRED' ? 'auth' : 'network', {
|
|
561
|
-
statusCode: error.response?.status,
|
|
562
|
-
endpoint: originalConfig?.url,
|
|
563
|
-
cause: interceptorError instanceof Error ? interceptorError : error,
|
|
564
|
-
});
|
|
286
|
+
throw classifyError(error, 'auth');
|
|
565
287
|
}
|
|
288
|
+
throw classifyError(error);
|
|
566
289
|
});
|
|
567
290
|
};
|
|
568
291
|
createAuthRetryInterceptor(this.client);
|
|
@@ -571,12 +294,22 @@ class APIClient {
|
|
|
571
294
|
createAuthRetryInterceptor(this.selfHostedModelRouterClient);
|
|
572
295
|
}
|
|
573
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Destroy keep-alive sockets so the Node.js event loop can drain
|
|
299
|
+
* naturally. Call this before exiting commands that run HTTP probes
|
|
300
|
+
* (e.g. `status`) to avoid the libuv UV_HANDLE_CLOSING assertion
|
|
301
|
+
* on Windows / Node 25+.
|
|
302
|
+
*/
|
|
574
303
|
destroy() {
|
|
304
|
+
if (this._httpsAgent) {
|
|
305
|
+
this._httpsAgent.destroy();
|
|
306
|
+
this._httpsAgent = null;
|
|
307
|
+
}
|
|
575
308
|
if (this.ws) {
|
|
576
309
|
try {
|
|
577
310
|
this.ws.close();
|
|
578
311
|
}
|
|
579
|
-
catch { }
|
|
312
|
+
catch { /* ok */ }
|
|
580
313
|
this.ws = null;
|
|
581
314
|
}
|
|
582
315
|
}
|
|
@@ -656,26 +389,6 @@ class APIClient {
|
|
|
656
389
|
}
|
|
657
390
|
}
|
|
658
391
|
}
|
|
659
|
-
// All profile endpoints failed — fall back to JWT payload claims so the
|
|
660
|
-
// token is still usable without identity fields being null.
|
|
661
|
-
const jwtPayload = this.decodeJwtPayload(token);
|
|
662
|
-
const fallbackId = String(jwtPayload?.sub || jwtPayload?.user_id || jwtPayload?.id || '').trim();
|
|
663
|
-
const fallbackEmail = String(jwtPayload?.email || '').trim();
|
|
664
|
-
const fallbackPlan = String(jwtPayload?.plan || jwtPayload?.subscription_plan || 'developer').trim();
|
|
665
|
-
if (fallbackId || fallbackEmail) {
|
|
666
|
-
this.config.setAuth({
|
|
667
|
-
token,
|
|
668
|
-
userId: fallbackId || fallbackEmail,
|
|
669
|
-
email: fallbackEmail || fallbackId,
|
|
670
|
-
});
|
|
671
|
-
this.config.setSubscription({
|
|
672
|
-
plan: fallbackPlan,
|
|
673
|
-
status: 'active',
|
|
674
|
-
expiresAt: undefined,
|
|
675
|
-
});
|
|
676
|
-
return true;
|
|
677
|
-
}
|
|
678
|
-
this.logger.warn('Token validation failed: no profile endpoint or JWT identity claim matched');
|
|
679
392
|
this.config.clearAuth();
|
|
680
393
|
return false;
|
|
681
394
|
}
|
|
@@ -685,19 +398,6 @@ class APIClient {
|
|
|
685
398
|
return false;
|
|
686
399
|
}
|
|
687
400
|
}
|
|
688
|
-
decodeJwtPayload(token) {
|
|
689
|
-
try {
|
|
690
|
-
const parts = token.split('.');
|
|
691
|
-
if (parts.length !== 3) {
|
|
692
|
-
return null;
|
|
693
|
-
}
|
|
694
|
-
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
695
|
-
return JSON.parse(payload);
|
|
696
|
-
}
|
|
697
|
-
catch {
|
|
698
|
-
return null;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
401
|
extractUserProfile(data) {
|
|
702
402
|
if (!data) {
|
|
703
403
|
return null;
|
|
@@ -734,11 +434,9 @@ class APIClient {
|
|
|
734
434
|
}
|
|
735
435
|
return true;
|
|
736
436
|
}
|
|
737
|
-
catch
|
|
738
|
-
const cliError = toCliError(error, 'AUTH_REFRESH_FAILED', 'Failed to refresh authentication token');
|
|
739
|
-
this.logger.debug(`Token refresh failed: ${cliError.message}`);
|
|
437
|
+
catch {
|
|
740
438
|
this.config.clearAuth();
|
|
741
|
-
|
|
439
|
+
return false;
|
|
742
440
|
}
|
|
743
441
|
}
|
|
744
442
|
async getSubscriptionStatus() {
|
|
@@ -773,40 +471,48 @@ class APIClient {
|
|
|
773
471
|
if (!token) {
|
|
774
472
|
return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
|
|
775
473
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
474
|
+
const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
|
|
475
|
+
const headers = {
|
|
476
|
+
Authorization: `Bearer ${token}`,
|
|
477
|
+
Cookie: `vigthoria-auth-token=${token}`,
|
|
478
|
+
};
|
|
479
|
+
const canonicalBaseUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
480
|
+
// Probe protected canonical endpoints in parallel so stale local endpoint overrides
|
|
481
|
+
// cannot mask an invalid gateway token during preflight.
|
|
482
|
+
const results = await Promise.allSettled([
|
|
483
|
+
axios_1.default.get(`${canonicalBaseUrl}/api/user/profile`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
|
|
484
|
+
axios_1.default.get(`${canonicalBaseUrl}/api/user/subscription`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
|
|
485
|
+
]);
|
|
486
|
+
for (const r of results) {
|
|
487
|
+
if (r.status === 'fulfilled')
|
|
782
488
|
return { valid: true };
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
489
|
+
}
|
|
490
|
+
for (const r of results) {
|
|
491
|
+
if (r.status === 'rejected') {
|
|
492
|
+
const err = r.reason;
|
|
493
|
+
if (err.response?.status === 401 || err.response?.status === 403) {
|
|
786
494
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
787
495
|
}
|
|
788
|
-
|
|
789
|
-
if (axErr.response?.status === 401 || axErr.response?.status === 403) {
|
|
496
|
+
if (err instanceof CLIError && err.category === 'auth') {
|
|
790
497
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
791
498
|
}
|
|
792
|
-
// Try the next authenticated endpoint before deciding this is
|
|
793
|
-
// a transient network/backend issue.
|
|
794
|
-
continue;
|
|
795
499
|
}
|
|
796
500
|
}
|
|
797
|
-
|
|
501
|
+
if (explicitEnvToken) {
|
|
502
|
+
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
503
|
+
}
|
|
504
|
+
// Both unreachable — don't assume the stored token is bad when running offline.
|
|
798
505
|
return { valid: true };
|
|
799
506
|
}
|
|
800
507
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
801
508
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
802
|
-
const allowLocalV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1'
|
|
803
|
-
|| this.allowLocalServiceFallbacks()
|
|
804
|
-
|| preferLocal;
|
|
509
|
+
const allowLocalV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1' || preferLocal;
|
|
805
510
|
const urls = [
|
|
806
511
|
process.env.VIGTHORIA_V3_AGENT_URL,
|
|
807
512
|
process.env.V3_AGENT_URL,
|
|
808
513
|
...(allowLocalV3Agent ? ['http://127.0.0.1:8030'] : []),
|
|
809
514
|
configuredApiUrl,
|
|
515
|
+
'https://coder.vigthoria.io',
|
|
810
516
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
811
517
|
return [...new Set(urls)];
|
|
812
518
|
}
|
|
@@ -827,8 +533,9 @@ class APIClient {
|
|
|
827
533
|
const urls = [
|
|
828
534
|
process.env.VIGTHORIA_OPERATOR_URL,
|
|
829
535
|
process.env.OPERATOR_URL,
|
|
830
|
-
|
|
536
|
+
'http://127.0.0.1:4009',
|
|
831
537
|
configuredModelsApiUrl,
|
|
538
|
+
'https://api.vigthoria.io',
|
|
832
539
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
833
540
|
return [...new Set(urls)];
|
|
834
541
|
}
|
|
@@ -840,7 +547,7 @@ class APIClient {
|
|
|
840
547
|
const urls = [
|
|
841
548
|
process.env.VIGTHORIA_MCP_URL,
|
|
842
549
|
process.env.MCP_SERVER_URL,
|
|
843
|
-
|
|
550
|
+
'http://127.0.0.1:4008',
|
|
844
551
|
configuredApiUrl,
|
|
845
552
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
846
553
|
return [...new Set(urls)];
|
|
@@ -858,7 +565,8 @@ class APIClient {
|
|
|
858
565
|
process.env.VIGFLOW_URL,
|
|
859
566
|
process.env.WORKFLOW_BUILDER_URL,
|
|
860
567
|
`${configuredApiUrl}/api/vigflow`,
|
|
861
|
-
|
|
568
|
+
'http://127.0.0.1:5060',
|
|
569
|
+
'http://127.0.0.1:5050',
|
|
862
570
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
863
571
|
return [...new Set(urls)];
|
|
864
572
|
}
|
|
@@ -867,14 +575,11 @@ class APIClient {
|
|
|
867
575
|
const urls = [
|
|
868
576
|
process.env.VIGTHORIA_TEMPLATE_SERVICE_URL,
|
|
869
577
|
process.env.TEMPLATE_SERVICE_URL,
|
|
870
|
-
|
|
578
|
+
'http://127.0.0.1:4011',
|
|
871
579
|
`${configuredApiUrl}/api/template-service`,
|
|
872
580
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
873
581
|
return [...new Set(urls)];
|
|
874
582
|
}
|
|
875
|
-
allowLocalServiceFallbacks() {
|
|
876
|
-
return process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1' || isServerRuntime();
|
|
877
|
-
}
|
|
878
583
|
isFrontendTask(message = '', context = {}) {
|
|
879
584
|
// Never treat analysis-only tasks as frontend tasks — preview gate
|
|
880
585
|
// should not fire for read-only inspection prompts.
|
|
@@ -1196,7 +901,7 @@ class APIClient {
|
|
|
1196
901
|
});
|
|
1197
902
|
if (!response.ok) {
|
|
1198
903
|
const errorText = await response.text().catch(() => '');
|
|
1199
|
-
throw new Error(`Template preview proof ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
904
|
+
throw new Error(`Template preview proof ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
1200
905
|
}
|
|
1201
906
|
const payload = await response.json();
|
|
1202
907
|
const modes = payload?.modes || {};
|
|
@@ -1249,12 +954,6 @@ class APIClient {
|
|
|
1249
954
|
'Content-Type': 'application/json',
|
|
1250
955
|
Accept: 'application/json',
|
|
1251
956
|
};
|
|
1252
|
-
try {
|
|
1253
|
-
await refreshJwtIfNeeded(this);
|
|
1254
|
-
}
|
|
1255
|
-
catch (error) {
|
|
1256
|
-
throw toCliError(error, 'AUTH_REFRESH_FAILED', 'Failed to refresh authentication token before API request');
|
|
1257
|
-
}
|
|
1258
957
|
const authToken = this.getAccessToken();
|
|
1259
958
|
if (authToken) {
|
|
1260
959
|
headers.Authorization = `Bearer ${authToken}`;
|
|
@@ -1266,12 +965,6 @@ class APIClient {
|
|
|
1266
965
|
const headers = {
|
|
1267
966
|
'Content-Type': 'application/json',
|
|
1268
967
|
};
|
|
1269
|
-
try {
|
|
1270
|
-
await refreshJwtIfNeeded(this);
|
|
1271
|
-
}
|
|
1272
|
-
catch (error) {
|
|
1273
|
-
throw toCliError(error, 'AUTH_REFRESH_FAILED', 'Failed to refresh authentication token before API request');
|
|
1274
|
-
}
|
|
1275
968
|
const authToken = this.getAccessToken();
|
|
1276
969
|
if (authToken) {
|
|
1277
970
|
headers.Authorization = `Bearer ${authToken}`;
|
|
@@ -1352,7 +1045,8 @@ class APIClient {
|
|
|
1352
1045
|
this.logger.debug(`VigFlow ${operation} via ${baseUrl} failed:`, lastError.message);
|
|
1353
1046
|
}
|
|
1354
1047
|
}
|
|
1355
|
-
|
|
1048
|
+
// Throw a clean message instead of the raw ECONNREFUSED from the last URL tried
|
|
1049
|
+
throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
|
|
1356
1050
|
}
|
|
1357
1051
|
/**
|
|
1358
1052
|
* Build the correct sub-path for VigFlow endpoints.
|
|
@@ -1489,9 +1183,7 @@ class APIClient {
|
|
|
1489
1183
|
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
1490
1184
|
const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
|
|
1491
1185
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1492
|
-
const localWorkspaceSummary =
|
|
1493
|
-
? this.buildSemanticWorkspaceSummary(localWorkspacePath, String(resolvedContext.rawPrompt))
|
|
1494
|
-
: this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
1186
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
1495
1187
|
const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
|
|
1496
1188
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
1497
1189
|
// When the server cannot directly access the workspace (e.g. Windows
|
|
@@ -1646,6 +1338,458 @@ class APIClient {
|
|
|
1646
1338
|
const match = String(message || '').match(/called\s+([A-Z][A-Za-z0-9&\- ]{2,40})/i);
|
|
1647
1339
|
return match?.[1]?.trim() || fallback;
|
|
1648
1340
|
}
|
|
1341
|
+
materializeEmergencySaaSWorkspace(message = '', context = {}) {
|
|
1342
|
+
const rootPath = this.resolveAgentTargetPath(context);
|
|
1343
|
+
if (!rootPath) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
fs_1.default.mkdirSync(rootPath, { recursive: true });
|
|
1347
|
+
const appName = this.extractEmergencyAppName(message);
|
|
1348
|
+
const html = `<!DOCTYPE html>
|
|
1349
|
+
<html lang="en">
|
|
1350
|
+
<head>
|
|
1351
|
+
<meta charset="UTF-8">
|
|
1352
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1353
|
+
<title>${appName}</title>
|
|
1354
|
+
<link rel="stylesheet" href="styles.css">
|
|
1355
|
+
</head>
|
|
1356
|
+
<body>
|
|
1357
|
+
<div class="app-shell">
|
|
1358
|
+
<aside class="sidebar">
|
|
1359
|
+
<div class="brand">${appName}</div>
|
|
1360
|
+
<button class="menu-toggle" id="menu-toggle" aria-label="Toggle navigation">Menu</button>
|
|
1361
|
+
<nav>
|
|
1362
|
+
<a href="#dashboard" class="nav-link active">Dashboard</a>
|
|
1363
|
+
<a href="#team" class="nav-link">Team</a>
|
|
1364
|
+
<a href="#billing" class="nav-link">Billing</a>
|
|
1365
|
+
<a href="#settings" class="nav-link">Settings</a>
|
|
1366
|
+
</nav>
|
|
1367
|
+
</aside>
|
|
1368
|
+
<main class="content">
|
|
1369
|
+
<section class="hero-card panel active-panel" id="dashboard">
|
|
1370
|
+
<div class="hero-copy">
|
|
1371
|
+
<p class="eyebrow">Dashboard</p>
|
|
1372
|
+
<h1>${appName} revenue command center</h1>
|
|
1373
|
+
<p>Track login activity, campaign velocity, billing state, and team performance from one responsive SaaS workspace.</p>
|
|
1374
|
+
</div>
|
|
1375
|
+
<form class="login-card">
|
|
1376
|
+
<h2>Login</h2>
|
|
1377
|
+
<label>Email<input type="email" placeholder="ops@${appName.toLowerCase().replace(/[^a-z0-9]+/g, '') || 'signaldesk'}.io"></label>
|
|
1378
|
+
<label>Password<input type="password" placeholder="Enter password"></label>
|
|
1379
|
+
<button type="submit">Enter dashboard</button>
|
|
1380
|
+
</form>
|
|
1381
|
+
</section>
|
|
1382
|
+
|
|
1383
|
+
<section class="stats-grid">
|
|
1384
|
+
<article class="stat-card"><span>MRR</span><strong>$284K</strong><em>+12.4%</em></article>
|
|
1385
|
+
<article class="stat-card"><span>Activation</span><strong>74%</strong><em>+6.1%</em></article>
|
|
1386
|
+
<article class="stat-card"><span>Team Seats</span><strong>128</strong><em>8 pending</em></article>
|
|
1387
|
+
<article class="stat-card"><span>Churn Risk</span><strong>2.1%</strong><em>Low</em></article>
|
|
1388
|
+
</section>
|
|
1389
|
+
|
|
1390
|
+
<section class="workspace-grid">
|
|
1391
|
+
<article class="panel chart-panel">
|
|
1392
|
+
<div class="panel-header">
|
|
1393
|
+
<h2>Analytics</h2>
|
|
1394
|
+
<button id="open-modal" type="button">Add campaign</button>
|
|
1395
|
+
</div>
|
|
1396
|
+
<div class="chart-bars" aria-label="Revenue chart">
|
|
1397
|
+
<div class="bar" style="--value: 52%"><span>Mon</span></div>
|
|
1398
|
+
<div class="bar" style="--value: 68%"><span>Tue</span></div>
|
|
1399
|
+
<div class="bar" style="--value: 74%"><span>Wed</span></div>
|
|
1400
|
+
<div class="bar" style="--value: 59%"><span>Thu</span></div>
|
|
1401
|
+
<div class="bar" style="--value: 88%"><span>Fri</span></div>
|
|
1402
|
+
</div>
|
|
1403
|
+
</article>
|
|
1404
|
+
|
|
1405
|
+
<article class="panel activity-panel">
|
|
1406
|
+
<div class="panel-header"><h2>Activity Feed</h2><span>Live</span></div>
|
|
1407
|
+
<ul class="activity-feed">
|
|
1408
|
+
<li><strong>Billing</strong><span>Enterprise invoice paid</span></li>
|
|
1409
|
+
<li><strong>Team</strong><span>New strategist invited to workspace</span></li>
|
|
1410
|
+
<li><strong>Dashboard</strong><span>KPI threshold updated for activation alerts</span></li>
|
|
1411
|
+
</ul>
|
|
1412
|
+
</article>
|
|
1413
|
+
|
|
1414
|
+
<article class="panel" id="team">
|
|
1415
|
+
<div class="panel-header"><h2>Team Management</h2><span>Owners and operators</span></div>
|
|
1416
|
+
<div class="team-list">
|
|
1417
|
+
<div><strong>Ana</strong><span>Growth lead</span></div>
|
|
1418
|
+
<div><strong>Marcus</strong><span>Billing admin</span></div>
|
|
1419
|
+
<div><strong>Lina</strong><span>Lifecycle analyst</span></div>
|
|
1420
|
+
</div>
|
|
1421
|
+
</article>
|
|
1422
|
+
|
|
1423
|
+
<article class="panel" id="billing">
|
|
1424
|
+
<div class="panel-header"><h2>Billing</h2><span>Current plan</span></div>
|
|
1425
|
+
<div class="billing-card">
|
|
1426
|
+
<strong>Scale Annual</strong>
|
|
1427
|
+
<p>Renews on 12 Oct with usage-based analytics overages.</p>
|
|
1428
|
+
<button type="button" class="secondary-action">Update payment method</button>
|
|
1429
|
+
</div>
|
|
1430
|
+
</article>
|
|
1431
|
+
|
|
1432
|
+
<article class="panel" id="settings">
|
|
1433
|
+
<div class="panel-header"><h2>Settings</h2><span>Automation and alerts</span></div>
|
|
1434
|
+
<form class="settings-form">
|
|
1435
|
+
<label>Alert threshold<input type="number" value="18"></label>
|
|
1436
|
+
<label>Weekly digest<select><option>Enabled</option><option>Paused</option></select></label>
|
|
1437
|
+
<button type="submit">Save settings</button>
|
|
1438
|
+
</form>
|
|
1439
|
+
</article>
|
|
1440
|
+
</section>
|
|
1441
|
+
</main>
|
|
1442
|
+
</div>
|
|
1443
|
+
|
|
1444
|
+
<dialog id="campaign-modal">
|
|
1445
|
+
<form method="dialog" class="modal-form">
|
|
1446
|
+
<h2>Launch campaign</h2>
|
|
1447
|
+
<label>Name<input type="text" placeholder="Retention push"></label>
|
|
1448
|
+
<label>Owner<input type="text" placeholder="Lina"></label>
|
|
1449
|
+
<menu>
|
|
1450
|
+
<button value="cancel">Cancel</button>
|
|
1451
|
+
<button value="confirm">Create</button>
|
|
1452
|
+
</menu>
|
|
1453
|
+
</form>
|
|
1454
|
+
</dialog>
|
|
1455
|
+
|
|
1456
|
+
<script src="scripts.js"></script>
|
|
1457
|
+
</body>
|
|
1458
|
+
</html>
|
|
1459
|
+
`;
|
|
1460
|
+
const css = `:root {
|
|
1461
|
+
--bg: #f2ede4;
|
|
1462
|
+
--ink: #18222f;
|
|
1463
|
+
--muted: #5c6674;
|
|
1464
|
+
--panel: rgba(255, 255, 255, 0.82);
|
|
1465
|
+
--line: rgba(24, 34, 47, 0.08);
|
|
1466
|
+
--accent: #b6542c;
|
|
1467
|
+
--accent-strong: #7f3417;
|
|
1468
|
+
--shadow: 0 24px 60px rgba(24, 34, 47, 0.12);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
* { box-sizing: border-box; }
|
|
1472
|
+
|
|
1473
|
+
body {
|
|
1474
|
+
margin: 0;
|
|
1475
|
+
font-family: "Georgia", "Times New Roman", serif;
|
|
1476
|
+
color: var(--ink);
|
|
1477
|
+
background:
|
|
1478
|
+
radial-gradient(circle at top left, rgba(182, 84, 44, 0.18), transparent 28%),
|
|
1479
|
+
radial-gradient(circle at bottom right, rgba(24, 34, 47, 0.14), transparent 30%),
|
|
1480
|
+
var(--bg);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
.app-shell {
|
|
1484
|
+
min-height: 100vh;
|
|
1485
|
+
display: grid;
|
|
1486
|
+
grid-template-columns: 260px 1fr;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
.sidebar {
|
|
1490
|
+
padding: 2rem 1.25rem;
|
|
1491
|
+
background: rgba(24, 34, 47, 0.94);
|
|
1492
|
+
color: #f7f2eb;
|
|
1493
|
+
position: sticky;
|
|
1494
|
+
top: 0;
|
|
1495
|
+
min-height: 100vh;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
.brand {
|
|
1499
|
+
font-size: 1.6rem;
|
|
1500
|
+
font-weight: 700;
|
|
1501
|
+
margin-bottom: 1.5rem;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
.menu-toggle {
|
|
1505
|
+
display: none;
|
|
1506
|
+
margin-bottom: 1rem;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
nav {
|
|
1510
|
+
display: grid;
|
|
1511
|
+
gap: 0.6rem;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.nav-link {
|
|
1515
|
+
color: inherit;
|
|
1516
|
+
text-decoration: none;
|
|
1517
|
+
padding: 0.8rem 0.95rem;
|
|
1518
|
+
border-radius: 999px;
|
|
1519
|
+
transition: transform 0.25s ease, background-color 0.25s ease;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
.nav-link:hover,
|
|
1523
|
+
.nav-link.active {
|
|
1524
|
+
background: rgba(255, 255, 255, 0.12);
|
|
1525
|
+
transform: translateX(4px);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.content {
|
|
1529
|
+
padding: 2rem;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.hero-card,
|
|
1533
|
+
.panel,
|
|
1534
|
+
.stat-card,
|
|
1535
|
+
.login-card,
|
|
1536
|
+
dialog {
|
|
1537
|
+
background: var(--panel);
|
|
1538
|
+
backdrop-filter: blur(16px);
|
|
1539
|
+
border: 1px solid var(--line);
|
|
1540
|
+
box-shadow: var(--shadow);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
.hero-card {
|
|
1544
|
+
display: grid;
|
|
1545
|
+
grid-template-columns: 1.3fr 0.9fr;
|
|
1546
|
+
gap: 1.5rem;
|
|
1547
|
+
border-radius: 32px;
|
|
1548
|
+
padding: 2rem;
|
|
1549
|
+
margin-bottom: 1.5rem;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
.eyebrow {
|
|
1553
|
+
text-transform: uppercase;
|
|
1554
|
+
letter-spacing: 0.14em;
|
|
1555
|
+
color: var(--accent-strong);
|
|
1556
|
+
font-size: 0.78rem;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
.hero-card h1,
|
|
1560
|
+
.panel h2,
|
|
1561
|
+
.login-card h2 {
|
|
1562
|
+
margin: 0 0 0.75rem;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
.login-card,
|
|
1566
|
+
.panel,
|
|
1567
|
+
.stat-card {
|
|
1568
|
+
border-radius: 24px;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
.login-card,
|
|
1572
|
+
.settings-form,
|
|
1573
|
+
.modal-form {
|
|
1574
|
+
display: grid;
|
|
1575
|
+
gap: 0.85rem;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.stats-grid,
|
|
1579
|
+
.workspace-grid {
|
|
1580
|
+
display: grid;
|
|
1581
|
+
gap: 1rem;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.stats-grid {
|
|
1585
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1586
|
+
margin-bottom: 1rem;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
.workspace-grid {
|
|
1590
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
.stat-card,
|
|
1594
|
+
.panel {
|
|
1595
|
+
padding: 1.2rem;
|
|
1596
|
+
animation: riseIn 0.7s ease forwards;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
.stat-card span,
|
|
1600
|
+
.panel-header span,
|
|
1601
|
+
.activity-feed span,
|
|
1602
|
+
.team-list span,
|
|
1603
|
+
.billing-card p {
|
|
1604
|
+
color: var(--muted);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
.panel-header {
|
|
1608
|
+
display: flex;
|
|
1609
|
+
align-items: center;
|
|
1610
|
+
justify-content: space-between;
|
|
1611
|
+
gap: 1rem;
|
|
1612
|
+
margin-bottom: 1rem;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
.chart-bars {
|
|
1616
|
+
display: grid;
|
|
1617
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
1618
|
+
gap: 0.9rem;
|
|
1619
|
+
align-items: end;
|
|
1620
|
+
min-height: 220px;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
.bar {
|
|
1624
|
+
position: relative;
|
|
1625
|
+
min-height: 180px;
|
|
1626
|
+
border-radius: 20px 20px 8px 8px;
|
|
1627
|
+
background: linear-gradient(180deg, rgba(182, 84, 44, 0.92), rgba(127, 52, 23, 0.68));
|
|
1628
|
+
transform-origin: bottom;
|
|
1629
|
+
transform: scaleY(calc(var(--value) / 100));
|
|
1630
|
+
transition: transform 0.6s ease;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
.bar span {
|
|
1634
|
+
position: absolute;
|
|
1635
|
+
left: 50%;
|
|
1636
|
+
bottom: -1.6rem;
|
|
1637
|
+
transform: translateX(-50%);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
.activity-feed,
|
|
1641
|
+
.team-list {
|
|
1642
|
+
display: grid;
|
|
1643
|
+
gap: 0.8rem;
|
|
1644
|
+
padding: 0;
|
|
1645
|
+
margin: 0;
|
|
1646
|
+
list-style: none;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
.activity-feed li,
|
|
1650
|
+
.team-list div,
|
|
1651
|
+
.billing-card {
|
|
1652
|
+
padding: 0.9rem 1rem;
|
|
1653
|
+
border-radius: 18px;
|
|
1654
|
+
background: rgba(255, 255, 255, 0.7);
|
|
1655
|
+
border: 1px solid var(--line);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
label {
|
|
1659
|
+
display: grid;
|
|
1660
|
+
gap: 0.35rem;
|
|
1661
|
+
font-size: 0.95rem;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
input,
|
|
1665
|
+
select,
|
|
1666
|
+
button {
|
|
1667
|
+
font: inherit;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
input,
|
|
1671
|
+
select {
|
|
1672
|
+
width: 100%;
|
|
1673
|
+
padding: 0.85rem 1rem;
|
|
1674
|
+
border-radius: 14px;
|
|
1675
|
+
border: 1px solid var(--line);
|
|
1676
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
button {
|
|
1680
|
+
border: none;
|
|
1681
|
+
border-radius: 999px;
|
|
1682
|
+
padding: 0.85rem 1.2rem;
|
|
1683
|
+
background: var(--accent);
|
|
1684
|
+
color: #fff9f3;
|
|
1685
|
+
cursor: pointer;
|
|
1686
|
+
transition: transform 0.25s ease, background-color 0.25s ease;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
button:hover {
|
|
1690
|
+
background: var(--accent-strong);
|
|
1691
|
+
transform: translateY(-2px);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
.secondary-action,
|
|
1695
|
+
menu button:first-child {
|
|
1696
|
+
background: rgba(24, 34, 47, 0.12);
|
|
1697
|
+
color: var(--ink);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
dialog {
|
|
1701
|
+
border-radius: 28px;
|
|
1702
|
+
padding: 0;
|
|
1703
|
+
width: min(420px, calc(100% - 2rem));
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
dialog::backdrop {
|
|
1707
|
+
background: rgba(24, 34, 47, 0.3);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
.modal-form {
|
|
1711
|
+
padding: 1.4rem;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
menu {
|
|
1715
|
+
display: flex;
|
|
1716
|
+
justify-content: flex-end;
|
|
1717
|
+
gap: 0.75rem;
|
|
1718
|
+
padding: 0;
|
|
1719
|
+
margin: 0.5rem 0 0;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
@keyframes riseIn {
|
|
1723
|
+
from {
|
|
1724
|
+
opacity: 0;
|
|
1725
|
+
transform: translateY(18px);
|
|
1726
|
+
}
|
|
1727
|
+
to {
|
|
1728
|
+
opacity: 1;
|
|
1729
|
+
transform: translateY(0);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
@media (max-width: 980px) {
|
|
1734
|
+
.app-shell,
|
|
1735
|
+
.hero-card,
|
|
1736
|
+
.stats-grid,
|
|
1737
|
+
.workspace-grid {
|
|
1738
|
+
grid-template-columns: 1fr;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
.sidebar {
|
|
1742
|
+
position: static;
|
|
1743
|
+
min-height: auto;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
.menu-toggle {
|
|
1747
|
+
display: inline-flex;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
nav {
|
|
1751
|
+
display: none;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
nav.is-open {
|
|
1755
|
+
display: grid;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
`;
|
|
1759
|
+
const js = `document.addEventListener('DOMContentLoaded', () => {
|
|
1760
|
+
const menuToggle = document.getElementById('menu-toggle');
|
|
1761
|
+
const nav = document.querySelector('nav');
|
|
1762
|
+
const modal = document.getElementById('campaign-modal');
|
|
1763
|
+
const openModal = document.getElementById('open-modal');
|
|
1764
|
+
const navLinks = document.querySelectorAll('.nav-link');
|
|
1765
|
+
|
|
1766
|
+
menuToggle?.addEventListener('click', () => nav?.classList.toggle('is-open'));
|
|
1767
|
+
openModal?.addEventListener('click', () => modal?.showModal());
|
|
1768
|
+
modal?.addEventListener('close', () => document.body.classList.remove('modal-open'));
|
|
1769
|
+
|
|
1770
|
+
navLinks.forEach((link) => {
|
|
1771
|
+
link.addEventListener('click', (event) => {
|
|
1772
|
+
event.preventDefault();
|
|
1773
|
+
navLinks.forEach((entry) => entry.classList.remove('active'));
|
|
1774
|
+
link.classList.add('active');
|
|
1775
|
+
document.querySelector(link.getAttribute('href'))?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1776
|
+
nav?.classList.remove('is-open');
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
document.querySelectorAll('.bar').forEach((bar, index) => {
|
|
1781
|
+
bar.animate([
|
|
1782
|
+
{ transform: 'scaleY(0.15)' },
|
|
1783
|
+
{ transform: getComputedStyle(bar).transform || 'scaleY(1)' }
|
|
1784
|
+
], { duration: 600 + index * 80, fill: 'forwards', easing: 'ease-out' });
|
|
1785
|
+
});
|
|
1786
|
+
});
|
|
1787
|
+
`;
|
|
1788
|
+
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'index.html'), `${html.trimEnd()}\n`, 'utf8');
|
|
1789
|
+
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'styles.css'), `${css.trimEnd()}\n`, 'utf8');
|
|
1790
|
+
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'scripts.js'), `${js.trimEnd()}\n`, 'utf8');
|
|
1791
|
+
return appName;
|
|
1792
|
+
}
|
|
1649
1793
|
ensureExecutionContext(context = {}) {
|
|
1650
1794
|
const existingId = String(context.contextId || context.traceId || '').trim();
|
|
1651
1795
|
const contextId = existingId || `vig-${Date.now()}-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
@@ -1698,7 +1842,7 @@ class APIClient {
|
|
|
1698
1842
|
});
|
|
1699
1843
|
if (!response.ok) {
|
|
1700
1844
|
const errorText = await response.text().catch(() => '');
|
|
1701
|
-
throw new Error(`MCP context update ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
1845
|
+
throw new Error(`MCP context update ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
1702
1846
|
}
|
|
1703
1847
|
return {
|
|
1704
1848
|
...executionContext,
|
|
@@ -1729,7 +1873,7 @@ class APIClient {
|
|
|
1729
1873
|
});
|
|
1730
1874
|
if (!createResponse.ok) {
|
|
1731
1875
|
const errorText = await createResponse.text().catch(() => '');
|
|
1732
|
-
throw new Error(`MCP context create ${createResponse.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
1876
|
+
throw new Error(`MCP context create ${createResponse.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
1733
1877
|
}
|
|
1734
1878
|
const payload = await createResponse.json();
|
|
1735
1879
|
const mcpContextId = String(payload.contextId || '').trim();
|
|
@@ -1819,14 +1963,6 @@ class APIClient {
|
|
|
1819
1963
|
* Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
|
|
1820
1964
|
*/
|
|
1821
1965
|
collectWorkspaceFileContents(rootPath, filePaths) {
|
|
1822
|
-
// Prioritise files that changed since last agent run — budget goes to them first.
|
|
1823
|
-
if (rootPath && filePaths.length > 0) {
|
|
1824
|
-
try {
|
|
1825
|
-
const { changed, unchanged } = (0, workspace_cache_js_1.getChangedFiles)(rootPath, filePaths);
|
|
1826
|
-
filePaths = [...changed, ...unchanged];
|
|
1827
|
-
}
|
|
1828
|
-
catch { /* non-fatal */ }
|
|
1829
|
-
}
|
|
1830
1966
|
const MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
1831
1967
|
const MAX_FILE_BYTES = 200 * 1024;
|
|
1832
1968
|
const BINARY_EXTENSIONS = new Set([
|
|
@@ -2683,7 +2819,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2683
2819
|
}
|
|
2684
2820
|
async runV3AgentWorkflow(message, context = {}) {
|
|
2685
2821
|
const executionContext = await this.bindExecutionContext(context);
|
|
2686
|
-
const
|
|
2822
|
+
const requestedTimeoutMs = Number(executionContext.agentTimeoutMs ?? DEFAULT_V3_AGENT_TIMEOUT_MS);
|
|
2823
|
+
const baseTimeoutMs = Number.isFinite(requestedTimeoutMs) && requestedTimeoutMs > 0 ? requestedTimeoutMs : 0;
|
|
2687
2824
|
const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
|
|
2688
2825
|
const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
|
|
2689
2826
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
@@ -2691,8 +2828,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2691
2828
|
&& context.localMachineCapable !== false;
|
|
2692
2829
|
const rescueEligibleSaaS = preferLocalV3
|
|
2693
2830
|
&& /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(message);
|
|
2694
|
-
const timeoutMs = rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
|
|
2695
|
-
const softTimeoutMs = executionContext.agentSoftTimeoutMs || DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS;
|
|
2831
|
+
const timeoutMs = baseTimeoutMs > 0 && rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
|
|
2696
2832
|
const maxAttempts = preferLocalV3 ? 2 : 1;
|
|
2697
2833
|
let lastErrors = [];
|
|
2698
2834
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -2726,14 +2862,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2726
2862
|
};
|
|
2727
2863
|
for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
|
|
2728
2864
|
const controller = new AbortController();
|
|
2729
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2730
|
-
const softTimeoutId = softTimeoutMs > 0 ? setTimeout(() => controller.abort(), softTimeoutMs) : null;
|
|
2865
|
+
const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
2731
2866
|
try {
|
|
2732
2867
|
const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
|
|
2733
|
-
clearTimeout(timeoutId);
|
|
2734
2868
|
if (!response.ok) {
|
|
2735
2869
|
const errorText = await response.text().catch(() => '');
|
|
2736
|
-
throw new Error(`V3 agent ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
2870
|
+
throw new Error(`V3 agent ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
2737
2871
|
}
|
|
2738
2872
|
const data = await this.collectV3AgentStream(response, requestExecutionContext);
|
|
2739
2873
|
// Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
|
|
@@ -2760,8 +2894,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2760
2894
|
stream: true,
|
|
2761
2895
|
};
|
|
2762
2896
|
const continueController = new AbortController();
|
|
2763
|
-
const continueTimeoutId = setTimeout(() => continueController.abort(), timeoutMs);
|
|
2764
|
-
const continueSoftTimeoutId = softTimeoutMs > 0 ? setTimeout(() => continueController.abort(), softTimeoutMs) : null;
|
|
2897
|
+
const continueTimeoutId = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
|
|
2765
2898
|
try {
|
|
2766
2899
|
const continueHeaders = await this.getV3AgentHeaders();
|
|
2767
2900
|
const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
@@ -2770,7 +2903,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2770
2903
|
body: JSON.stringify(continueBody),
|
|
2771
2904
|
signal: continueController.signal,
|
|
2772
2905
|
});
|
|
2773
|
-
clearTimeout(continueTimeoutId);
|
|
2774
2906
|
if (!continueResponse.ok) {
|
|
2775
2907
|
break; // Fall through to normal completion with partial data
|
|
2776
2908
|
}
|
|
@@ -2780,10 +2912,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2780
2912
|
break; // Fall through to normal completion with partial data
|
|
2781
2913
|
}
|
|
2782
2914
|
finally {
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
clearTimeout(continueSoftTimeoutId);
|
|
2786
|
-
}
|
|
2915
|
+
if (continueTimeoutId)
|
|
2916
|
+
clearTimeout(continueTimeoutId);
|
|
2787
2917
|
}
|
|
2788
2918
|
}
|
|
2789
2919
|
// Use the final continuation data for workspace recovery
|
|
@@ -2837,10 +2967,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2837
2967
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
2838
2968
|
}
|
|
2839
2969
|
finally {
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
clearTimeout(softTimeoutId);
|
|
2843
|
-
}
|
|
2970
|
+
if (timeoutId)
|
|
2971
|
+
clearTimeout(timeoutId);
|
|
2844
2972
|
}
|
|
2845
2973
|
}
|
|
2846
2974
|
lastErrors = errors;
|
|
@@ -2862,6 +2990,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2862
2990
|
this.config.clearAuth();
|
|
2863
2991
|
throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
|
|
2864
2992
|
}
|
|
2993
|
+
if (preferLocalV3
|
|
2994
|
+
&& !this.hasAgentWorkspaceOutput(executionContext)
|
|
2995
|
+
&& /(saas|dashboard|analytics|billing|team management|activity feed)/i.test(message)) {
|
|
2996
|
+
const appName = this.materializeEmergencySaaSWorkspace(message, executionContext);
|
|
2997
|
+
if (appName) {
|
|
2998
|
+
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
|
|
2999
|
+
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
3000
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
3001
|
+
return {
|
|
3002
|
+
content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
|
|
3003
|
+
taskId: null,
|
|
3004
|
+
contextId: executionContext.contextId || null,
|
|
3005
|
+
backendUrl: 'local-emergency-scaffold',
|
|
3006
|
+
partial: true,
|
|
3007
|
+
metadata: { source: 'v3-agent-emergency-scaffold', mode: 'agent', previewGate, emergencyScaffold: true },
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
2865
3011
|
throw new Error(errors.join(' | '));
|
|
2866
3012
|
}
|
|
2867
3013
|
formatOperatorResponse(data = {}) {
|
|
@@ -2889,7 +3035,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2889
3035
|
}
|
|
2890
3036
|
async runOperatorWorkflow(message, context = {}) {
|
|
2891
3037
|
const executionContext = await this.bindExecutionContext(context);
|
|
2892
|
-
const
|
|
3038
|
+
const requestedOperatorTimeoutMs = Number(context.operatorTimeoutMs ?? DEFAULT_OPERATOR_TIMEOUT_MS);
|
|
3039
|
+
const timeoutMs = Number.isFinite(requestedOperatorTimeoutMs) && requestedOperatorTimeoutMs > 0
|
|
3040
|
+
? requestedOperatorTimeoutMs
|
|
3041
|
+
: 0;
|
|
2893
3042
|
const errors = [];
|
|
2894
3043
|
const authToken = this.config.get('authToken');
|
|
2895
3044
|
// Collect a lightweight workspace file listing so the operator can
|
|
@@ -2898,7 +3047,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2898
3047
|
const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
2899
3048
|
for (const baseUrl of this.getOperatorBaseUrls()) {
|
|
2900
3049
|
const controller = new AbortController();
|
|
2901
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3050
|
+
const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
2902
3051
|
try {
|
|
2903
3052
|
const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
|
|
2904
3053
|
method: 'POST',
|
|
@@ -2922,7 +3071,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2922
3071
|
workspace: { path: workspacePath },
|
|
2923
3072
|
workspace_path: workspacePath,
|
|
2924
3073
|
workspace_summary: workspaceSummary,
|
|
2925
|
-
model: this.resolveModelId(executionContext.model || 'code
|
|
3074
|
+
model: this.resolveModelId(executionContext.model || 'code'),
|
|
2926
3075
|
history: executionContext.history || [],
|
|
2927
3076
|
executionSurface: executionContext.executionSurface || 'cli',
|
|
2928
3077
|
clientSurface: executionContext.clientSurface || 'cli',
|
|
@@ -2933,7 +3082,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2933
3082
|
rawPrompt: executionContext.rawPrompt || null,
|
|
2934
3083
|
requestStartedAt: executionContext.requestStartedAt,
|
|
2935
3084
|
},
|
|
2936
|
-
workflow_type: executionContext.workflowType || '
|
|
3085
|
+
workflow_type: executionContext.workflowType || 'full',
|
|
2937
3086
|
options: {
|
|
2938
3087
|
stream: true,
|
|
2939
3088
|
save_to_vigflow: executionContext.savePlanToVigFlow === true,
|
|
@@ -2943,7 +3092,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2943
3092
|
});
|
|
2944
3093
|
if (!response.ok) {
|
|
2945
3094
|
const errorText = await response.text().catch(() => '');
|
|
2946
|
-
throw new Error(`Operator stream ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
3095
|
+
throw new Error(`Operator stream ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
2947
3096
|
}
|
|
2948
3097
|
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
2949
3098
|
const fallbackData = await response.json();
|
|
@@ -3045,7 +3194,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3045
3194
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3046
3195
|
}
|
|
3047
3196
|
finally {
|
|
3048
|
-
|
|
3197
|
+
if (timeoutId)
|
|
3198
|
+
clearTimeout(timeoutId);
|
|
3049
3199
|
}
|
|
3050
3200
|
}
|
|
3051
3201
|
throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
|
|
@@ -3161,6 +3311,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3161
3311
|
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3162
3312
|
this.logger.debug(`Vigthoria Cloud API failed for ${resolvedModel}: ${errMsg}`);
|
|
3163
3313
|
}
|
|
3314
|
+
try {
|
|
3315
|
+
this.logger.debug(`Canonical Vigthoria Cloud fallback: ${resolvedModel}`);
|
|
3316
|
+
const token = this.getAccessToken();
|
|
3317
|
+
const response = await axios_1.default.post('https://coder.vigthoria.io/api/ai/chat', {
|
|
3318
|
+
messages,
|
|
3319
|
+
model: resolvedModel,
|
|
3320
|
+
maxTokens: this.config.get('preferences').maxTokens,
|
|
3321
|
+
temperature: 0.7,
|
|
3322
|
+
}, {
|
|
3323
|
+
timeout: 180000,
|
|
3324
|
+
httpsAgent: this._httpsAgent ?? undefined,
|
|
3325
|
+
headers: token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {},
|
|
3326
|
+
});
|
|
3327
|
+
if (response.data?.success !== false) {
|
|
3328
|
+
const content = response.data.response || response.data.message || response.data.content;
|
|
3329
|
+
if (typeof content === 'string' && content.trim()) {
|
|
3330
|
+
return {
|
|
3331
|
+
id: response.data.id || `vigthoria-coder-canonical-${Date.now()}`,
|
|
3332
|
+
message: content,
|
|
3333
|
+
model: response.data.model || resolvedModel || requestedModel,
|
|
3334
|
+
usage: response.data.usage,
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
catch (error) {
|
|
3340
|
+
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
3341
|
+
this.logger.debug(`Canonical Vigthoria Cloud fallback failed for ${resolvedModel}: ${errMsg}`);
|
|
3342
|
+
}
|
|
3164
3343
|
}
|
|
3165
3344
|
if (!preferSelfHostedFirst) {
|
|
3166
3345
|
const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
|
|
@@ -3211,6 +3390,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3211
3390
|
}
|
|
3212
3391
|
getFallbackModelId(resolvedModel) {
|
|
3213
3392
|
const cloudModels = new Set([
|
|
3393
|
+
'deepseek-v3.1:671b-cloud',
|
|
3214
3394
|
'moonshotai/kimi-k2.5',
|
|
3215
3395
|
'vigthoria-cloud-pro',
|
|
3216
3396
|
'vigthoria-cloud-k2',
|
|
@@ -3222,7 +3402,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3222
3402
|
return null;
|
|
3223
3403
|
}
|
|
3224
3404
|
isCloudModelId(resolvedModel) {
|
|
3225
|
-
return resolvedModel === '
|
|
3405
|
+
return resolvedModel === 'deepseek-v3.1:671b-cloud'
|
|
3406
|
+
|| resolvedModel === 'moonshotai/kimi-k2.5'
|
|
3226
3407
|
|| resolvedModel === 'vigthoria-cloud-pro'
|
|
3227
3408
|
|| resolvedModel === 'vigthoria-cloud-k2'
|
|
3228
3409
|
|| resolvedModel === 'vigthoria-cloud-ultra';
|
|
@@ -3231,26 +3412,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3231
3412
|
return this.config.hasCloudAccess();
|
|
3232
3413
|
}
|
|
3233
3414
|
resolvePermittedModelId(shortName) {
|
|
3415
|
+
const normalizedRequested = String(shortName || '').trim().toLowerCase();
|
|
3416
|
+
const blockedModels = new Set(['fast', 'mini', 'creative', 'creative-v3', 'creative-v4']);
|
|
3417
|
+
if (blockedModels.has(normalizedRequested)) {
|
|
3418
|
+
this.logger.debug(`Blocked governed model ${shortName}; using fallback vigthoria-v3-code-35b`);
|
|
3419
|
+
return 'vigthoria-v3-code-35b';
|
|
3420
|
+
}
|
|
3234
3421
|
const resolvedModel = this.resolveModelId(shortName);
|
|
3235
|
-
const requested = String(shortName || '').toLowerCase();
|
|
3236
3422
|
if (this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()) {
|
|
3237
3423
|
const fallbackModel = this.getSelfHostedFallbackModelId(resolvedModel, shortName);
|
|
3238
3424
|
this.logger.debug(`Blocked unauthorized cloud model ${shortName}; using fallback ${fallbackModel}`);
|
|
3239
3425
|
return fallbackModel;
|
|
3240
3426
|
}
|
|
3241
|
-
const blockedRequestedModels = new Set(['fast', 'mini', 'creative', 'creative-v3', 'creative-v4']);
|
|
3242
|
-
const blockedResolvedModels = new Set([
|
|
3243
|
-
'vigthoria-creative-9b-v4',
|
|
3244
|
-
'vigthoria-fast-1.7b',
|
|
3245
|
-
'vigthoria-mini-0.6b',
|
|
3246
|
-
'vigthoria_p1_m',
|
|
3247
|
-
'vigthoria_r1_s'
|
|
3248
|
-
]);
|
|
3249
|
-
if (blockedRequestedModels.has(requested) || blockedResolvedModels.has(resolvedModel)) {
|
|
3250
|
-
const fallbackModel = 'vigthoria-v3-code-35b';
|
|
3251
|
-
this.logger.warn(`Model ${shortName} is not permitted for CLI operational workflows; using ${fallbackModel}`);
|
|
3252
|
-
return fallbackModel;
|
|
3253
|
-
}
|
|
3254
3427
|
return resolvedModel;
|
|
3255
3428
|
}
|
|
3256
3429
|
shouldSimulateCloudFailure() {
|
|
@@ -3272,14 +3445,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3272
3445
|
'vigthoria-v3-code-35b',
|
|
3273
3446
|
'vigthoria-v3-code-35b:latest',
|
|
3274
3447
|
'qwen3-coder:latest',
|
|
3275
|
-
'
|
|
3448
|
+
'vigthoria-v2-code-8b',
|
|
3276
3449
|
]);
|
|
3277
3450
|
return selfHostedModels.has(resolvedModel)
|
|
3278
3451
|
|| normalizedRequested === 'agent'
|
|
3279
3452
|
|| normalizedRequested === 'code'
|
|
3280
|
-
|| normalizedRequested === 'code-
|
|
3281
|
-
|| normalizedRequested === 'code-35b'
|
|
3282
|
-
|| normalizedRequested === 'code-9b'
|
|
3453
|
+
|| normalizedRequested === 'code-30b'
|
|
3283
3454
|
|| normalizedRequested === 'pro';
|
|
3284
3455
|
}
|
|
3285
3456
|
getSelfHostedFallbackModelId(resolvedModel, requestedModel) {
|
|
@@ -3291,7 +3462,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3291
3462
|
// Streaming chat
|
|
3292
3463
|
async *chatStream(messages, model) {
|
|
3293
3464
|
const wsUrl = this.config.get('wsUrl');
|
|
3294
|
-
const token = this.
|
|
3465
|
+
const token = this.config.get('authToken');
|
|
3295
3466
|
return new Promise((resolve, reject) => {
|
|
3296
3467
|
const ws = new ws_1.default(`${wsUrl}/chat`, {
|
|
3297
3468
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -3320,7 +3491,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3320
3491
|
// Non-streaming alternative with callback
|
|
3321
3492
|
async chatWithCallback(messages, model, onChunk, onDone, onError) {
|
|
3322
3493
|
const wsUrl = this.config.get('wsUrl');
|
|
3323
|
-
const token = this.
|
|
3494
|
+
const token = this.config.get('authToken');
|
|
3324
3495
|
return new Promise((resolve, reject) => {
|
|
3325
3496
|
const ws = new ws_1.default(`${wsUrl}/chat`, {
|
|
3326
3497
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -3449,21 +3620,76 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3449
3620
|
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
3450
3621
|
*/
|
|
3451
3622
|
ensureBalancedBraces(code) {
|
|
3452
|
-
|
|
3453
|
-
|
|
3623
|
+
// Count braces/parens/brackets outside strings and comments
|
|
3624
|
+
let braces = 0, parens = 0, brackets = 0;
|
|
3625
|
+
let inStr = null;
|
|
3626
|
+
let inLine = false, inBlock = false;
|
|
3627
|
+
for (let i = 0; i < code.length; i++) {
|
|
3628
|
+
const ch = code[i], nx = code[i + 1] || '';
|
|
3629
|
+
if (inLine) {
|
|
3630
|
+
if (ch === '\n')
|
|
3631
|
+
inLine = false;
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
if (inBlock) {
|
|
3635
|
+
if (ch === '*' && nx === '/') {
|
|
3636
|
+
inBlock = false;
|
|
3637
|
+
i++;
|
|
3638
|
+
}
|
|
3639
|
+
continue;
|
|
3640
|
+
}
|
|
3641
|
+
if (inStr) {
|
|
3642
|
+
if (ch === inStr && code[i - 1] !== '\\')
|
|
3643
|
+
inStr = null;
|
|
3644
|
+
continue;
|
|
3645
|
+
}
|
|
3646
|
+
if (ch === '/' && nx === '/') {
|
|
3647
|
+
inLine = true;
|
|
3648
|
+
continue;
|
|
3649
|
+
}
|
|
3650
|
+
if (ch === '/' && nx === '*') {
|
|
3651
|
+
inBlock = true;
|
|
3652
|
+
continue;
|
|
3653
|
+
}
|
|
3654
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
3655
|
+
inStr = ch;
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3454
3658
|
if (ch === '{')
|
|
3455
|
-
|
|
3659
|
+
braces++;
|
|
3456
3660
|
else if (ch === '}')
|
|
3457
|
-
|
|
3661
|
+
braces--;
|
|
3662
|
+
else if (ch === '(')
|
|
3663
|
+
parens++;
|
|
3664
|
+
else if (ch === ')')
|
|
3665
|
+
parens--;
|
|
3666
|
+
else if (ch === '[')
|
|
3667
|
+
brackets++;
|
|
3668
|
+
else if (ch === ']')
|
|
3669
|
+
brackets--;
|
|
3670
|
+
}
|
|
3671
|
+
let result = code.trimEnd();
|
|
3672
|
+
for (let i = 0; i < braces; i++)
|
|
3673
|
+
result += '\n}';
|
|
3674
|
+
for (let i = 0; i < parens; i++)
|
|
3675
|
+
result += ')';
|
|
3676
|
+
for (let i = 0; i < brackets; i++)
|
|
3677
|
+
result += ']';
|
|
3678
|
+
return braces > 0 || parens > 0 || brackets > 0 ? result : code;
|
|
3679
|
+
}
|
|
3680
|
+
/**
|
|
3681
|
+
* Quick JS/TS syntax validation using Node's built-in parser.
|
|
3682
|
+
* Returns true if the code parses without errors.
|
|
3683
|
+
*/
|
|
3684
|
+
validateJsSyntax(code) {
|
|
3685
|
+
try {
|
|
3686
|
+
// Use Function constructor to check syntax without executing
|
|
3687
|
+
new Function(code);
|
|
3688
|
+
return true;
|
|
3458
3689
|
}
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
for (let i = 0; i < depth; i++) {
|
|
3462
|
-
result += '\n}';
|
|
3463
|
-
}
|
|
3464
|
-
code = result;
|
|
3690
|
+
catch {
|
|
3691
|
+
return false;
|
|
3465
3692
|
}
|
|
3466
|
-
return code;
|
|
3467
3693
|
}
|
|
3468
3694
|
/**
|
|
3469
3695
|
* Extract the first complete function/class from code.
|
|
@@ -3571,7 +3797,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3571
3797
|
}
|
|
3572
3798
|
}
|
|
3573
3799
|
async explainCode(code, language) {
|
|
3574
|
-
const sysPrompt =
|
|
3800
|
+
const sysPrompt = [
|
|
3801
|
+
`You are a code explainer. Explain the following ${language} code clearly and concisely.`,
|
|
3802
|
+
'Focus on what it does, how it works, and any notable patterns or potential issues.',
|
|
3803
|
+
'Format your response as clean Markdown:',
|
|
3804
|
+
'- Use ## headers for major sections (e.g. ## Overview, ## How It Works, ## Key Details).',
|
|
3805
|
+
'- Use bullet points (- or *) for all lists. Do NOT use numbered lists.',
|
|
3806
|
+
'- Wrap code references in backticks.',
|
|
3807
|
+
'- Keep paragraphs short (2-3 sentences max).',
|
|
3808
|
+
'- Do NOT use raw HTML or excessive blank lines.',
|
|
3809
|
+
'- Do NOT nest numbered lists inside sections.',
|
|
3810
|
+
].join('\n');
|
|
3575
3811
|
return this.chatComplete(sysPrompt, code);
|
|
3576
3812
|
}
|
|
3577
3813
|
async reviewCode(code, language) {
|
|
@@ -3583,9 +3819,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3583
3819
|
'Rules:',
|
|
3584
3820
|
'- Return concrete, line-specific issues with severity.',
|
|
3585
3821
|
'- Every issue MUST reference a line number.',
|
|
3586
|
-
'-
|
|
3822
|
+
'- Report each distinct bug ONCE. Do NOT report the same bug multiple times with different wording.',
|
|
3823
|
+
'- For trivial/short code (< 10 lines), report ONLY actual bugs. Do NOT pad with style, robustness, or best-practice suggestions.',
|
|
3824
|
+
'- If you find a real bug (wrong operator, logic error, type mismatch), report ONLY that bug. Do NOT also suggest input validation, type checking, or error handling unless those are ACTUAL bugs.',
|
|
3587
3825
|
'- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
|
|
3588
|
-
'-
|
|
3826
|
+
'- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
|
|
3589
3827
|
'- Return ONLY the JSON object, no markdown fences or extra text.',
|
|
3590
3828
|
].join('\n');
|
|
3591
3829
|
let raw = {};
|
|
@@ -3600,25 +3838,40 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3600
3838
|
const score = typeof raw.score === 'number' ? raw.score : 0;
|
|
3601
3839
|
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
|
3602
3840
|
const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
3603
|
-
//
|
|
3604
|
-
//
|
|
3605
|
-
|
|
3841
|
+
// Merge client-side heuristics, but with tight dedup to avoid
|
|
3842
|
+
// redundant over-reporting when the model already found the bug.
|
|
3843
|
+
const modelFoundError = issues.some(i => i.severity === 'error');
|
|
3606
3844
|
const heuristic = this.heuristicCodeIssues(code, language);
|
|
3607
3845
|
for (const h of heuristic) {
|
|
3608
|
-
//
|
|
3609
|
-
//
|
|
3610
|
-
|
|
3611
|
-
if (h.severity === 'error') {
|
|
3612
|
-
const exactDuplicate = issues.some((existing) => existing.line === h.line && existing.message === h.message);
|
|
3613
|
-
if (!exactDuplicate) {
|
|
3614
|
-
issues.push(h);
|
|
3615
|
-
}
|
|
3846
|
+
// If the model already found a real error, skip non-error heuristics
|
|
3847
|
+
// entirely — they're just padding (style, robustness, etc.)
|
|
3848
|
+
if (modelFoundError && h.severity !== 'error')
|
|
3616
3849
|
continue;
|
|
3617
|
-
|
|
3618
|
-
//
|
|
3619
|
-
//
|
|
3620
|
-
const
|
|
3621
|
-
|
|
3850
|
+
// Semantic duplicate check: same line + (similar type OR overlapping
|
|
3851
|
+
// keywords in the message). This catches cases where the model
|
|
3852
|
+
// and heuristic describe the same bug with different wording.
|
|
3853
|
+
const hWords = new Set(h.message.toLowerCase().split(/\W+/).filter(w => w.length > 3));
|
|
3854
|
+
const hTypeNorm = h.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
3855
|
+
const isSemanticallyDuplicate = issues.some((existing) => {
|
|
3856
|
+
if (existing.line !== h.line)
|
|
3857
|
+
return false;
|
|
3858
|
+
// Normalize types: "logic-error", "logic_error", "logic" all match
|
|
3859
|
+
const eTypeNorm = existing.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
3860
|
+
if (eTypeNorm === hTypeNorm || eTypeNorm.startsWith(hTypeNorm) || hTypeNorm.startsWith(eTypeNorm))
|
|
3861
|
+
return true;
|
|
3862
|
+
// Both errors on same line about the same category of problem
|
|
3863
|
+
if (existing.severity === 'error' && h.severity === 'error')
|
|
3864
|
+
return true;
|
|
3865
|
+
// Check keyword overlap — if ≥2 significant words match, it's the same finding
|
|
3866
|
+
const eWords = existing.message.toLowerCase().split(/\W+/).filter(w => w.length > 3);
|
|
3867
|
+
let overlap = 0;
|
|
3868
|
+
for (const w of eWords) {
|
|
3869
|
+
if (hWords.has(w))
|
|
3870
|
+
overlap++;
|
|
3871
|
+
}
|
|
3872
|
+
return overlap >= 2;
|
|
3873
|
+
});
|
|
3874
|
+
if (!isSemanticallyDuplicate) {
|
|
3622
3875
|
issues.push(h);
|
|
3623
3876
|
}
|
|
3624
3877
|
}
|
|
@@ -3767,9 +4020,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3767
4020
|
const sysPrompt = [
|
|
3768
4021
|
`You are a ${language} code fixer. Fix the code for: ${fixType}.`,
|
|
3769
4022
|
'Return a JSON object with:',
|
|
3770
|
-
' "fixed": the corrected code as a string,',
|
|
4023
|
+
' "fixed": the COMPLETE corrected source code as a string (not a snippet — the full file),',
|
|
3771
4024
|
' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
|
|
3772
4025
|
'Rules:',
|
|
4026
|
+
'- The "fixed" field MUST contain the entire corrected source code with ALL lines, including unchanged lines.',
|
|
4027
|
+
'- The "fixed" code MUST have balanced braces, parentheses, and brackets.',
|
|
3773
4028
|
'- Fix ONLY the issues related to the fix type.',
|
|
3774
4029
|
'- Do not add comments, do not restructure beyond the minimal fix.',
|
|
3775
4030
|
'- Return ONLY the JSON object, no markdown fences.',
|
|
@@ -3809,6 +4064,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3809
4064
|
if (fixType === 'syntax' && fixed !== code) {
|
|
3810
4065
|
fixed = this.repairBracketBalance(code, fixed);
|
|
3811
4066
|
}
|
|
4067
|
+
// Final bracket-balance guarantee — ensure the emitted code has
|
|
4068
|
+
// balanced braces/parens/brackets regardless of what the model returned.
|
|
4069
|
+
fixed = this.ensureBalancedBraces(fixed);
|
|
4070
|
+
// For JS/TS syntax fixes, validate the output actually parses.
|
|
4071
|
+
// If it doesn't, attempt a more aggressive bracket repair.
|
|
4072
|
+
if ((fixType === 'syntax' || fixType === 'bugs') && fixed !== code) {
|
|
4073
|
+
const lang = language.toLowerCase();
|
|
4074
|
+
if (['javascript', 'js', 'typescript', 'ts'].includes(lang)) {
|
|
4075
|
+
if (!this.validateJsSyntax(fixed)) {
|
|
4076
|
+
// Try once more: strip any remaining injected comments and re-balance
|
|
4077
|
+
let repaired = this.stripInjectedComments(code, fixed, language);
|
|
4078
|
+
repaired = this.ensureBalancedBraces(repaired);
|
|
4079
|
+
if (this.validateJsSyntax(repaired)) {
|
|
4080
|
+
fixed = repaired;
|
|
4081
|
+
}
|
|
4082
|
+
// If still invalid, return the best-effort fix — better than
|
|
4083
|
+
// silently reverting to the original broken code.
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
3812
4087
|
// If there are still no changes but the fixed code differs, compute
|
|
3813
4088
|
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
3814
4089
|
// every subsequent line to appear as changed.
|
|
@@ -4110,112 +4385,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4110
4385
|
}
|
|
4111
4386
|
// Model resolution - maps Vigthoria model names to internal IDs
|
|
4112
4387
|
// INTERNAL USE ONLY - users see only Vigthoria branding
|
|
4113
|
-
/**
|
|
4114
|
-
* Build workspace summary re-ordered by semantic relevance to the prompt.
|
|
4115
|
-
* Changed files are listed first, then keyword-matched files, then the rest.
|
|
4116
|
-
* Falls back to plain buildLocalWorkspaceSummary when no prompt is provided.
|
|
4117
|
-
*/
|
|
4118
|
-
buildSemanticWorkspaceSummary(workspacePath, prompt) {
|
|
4119
|
-
const summary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
4120
|
-
if (!summary?.workspaceFiles || !prompt)
|
|
4121
|
-
return summary;
|
|
4122
|
-
try {
|
|
4123
|
-
const { topFiles } = (0, context_ranker_js_1.buildSemanticContext)(workspacePath, prompt, 15);
|
|
4124
|
-
if (topFiles.length === 0)
|
|
4125
|
-
return summary;
|
|
4126
|
-
const prioritySet = new Set(topFiles.map(f => f.path));
|
|
4127
|
-
const allFiles = summary.workspaceFiles;
|
|
4128
|
-
const reordered = {};
|
|
4129
|
-
// Semantically ranked files first
|
|
4130
|
-
for (const f of topFiles) {
|
|
4131
|
-
if (allFiles[f.path] !== undefined)
|
|
4132
|
-
reordered[f.path] = allFiles[f.path];
|
|
4133
|
-
}
|
|
4134
|
-
// Remaining files after priority set
|
|
4135
|
-
for (const [p, c] of Object.entries(allFiles)) {
|
|
4136
|
-
if (!prioritySet.has(p))
|
|
4137
|
-
reordered[p] = c;
|
|
4138
|
-
}
|
|
4139
|
-
return { ...summary, workspaceFiles: reordered };
|
|
4140
|
-
}
|
|
4141
|
-
catch {
|
|
4142
|
-
return summary;
|
|
4143
|
-
}
|
|
4144
|
-
}
|
|
4145
|
-
/**
|
|
4146
|
-
* Self-healing cycle: run post-write validators and, if errors are found,
|
|
4147
|
-
* send a targeted correction prompt to the V3 agent (max one healing round).
|
|
4148
|
-
*
|
|
4149
|
-
* This is a best-effort operation — failures never propagate to the user as
|
|
4150
|
-
* hard errors; they are surfaced as a status line in the terminal output.
|
|
4151
|
-
*/
|
|
4152
|
-
async runSelfHealingCycle(originalPrompt, workspacePath, context = {}) {
|
|
4153
|
-
// Guard: don't heal analysis tasks or recursive healing rounds
|
|
4154
|
-
if (context._isHealingRound || this.isAnalysisOnlyTask(originalPrompt, context)) {
|
|
4155
|
-
return { healingAttempted: false, passed: true, tool: 'none' };
|
|
4156
|
-
}
|
|
4157
|
-
let validations = [];
|
|
4158
|
-
try {
|
|
4159
|
-
validations = await (0, post_write_validator_js_1.runPostWriteValidation)(workspacePath);
|
|
4160
|
-
}
|
|
4161
|
-
catch {
|
|
4162
|
-
return { healingAttempted: false, passed: true, tool: 'none' };
|
|
4163
|
-
}
|
|
4164
|
-
const failures = validations.filter(v => v.ran && !v.passed);
|
|
4165
|
-
if (failures.length === 0) {
|
|
4166
|
-
// All validators passed — update cache to reflect current state
|
|
4167
|
-
try {
|
|
4168
|
-
const { getAgentWorkspaceSnapshot } = this;
|
|
4169
|
-
if (typeof getAgentWorkspaceSnapshot === 'function') {
|
|
4170
|
-
const snap = getAgentWorkspaceSnapshot.call(this, workspacePath);
|
|
4171
|
-
if (snap?.paths?.length > 0)
|
|
4172
|
-
(0, workspace_cache_js_1.updateWorkspaceCache)(workspacePath, snap.paths);
|
|
4173
|
-
}
|
|
4174
|
-
}
|
|
4175
|
-
catch { /* non-fatal */ }
|
|
4176
|
-
return { healingAttempted: false, passed: true, tool: 'none' };
|
|
4177
|
-
}
|
|
4178
|
-
const errorText = (0, post_write_validator_js_1.formatValidationErrors)(failures);
|
|
4179
|
-
const healPrompt = `The code you just generated has the following validation errors. Fix ONLY these errors — do not change anything else:
|
|
4180
|
-
|
|
4181
|
-
${errorText}
|
|
4182
|
-
|
|
4183
|
-
Apply the minimum change needed to make the validator pass.`;
|
|
4184
|
-
try {
|
|
4185
|
-
await this.runV3AgentWorkflow(healPrompt, {
|
|
4186
|
-
...context,
|
|
4187
|
-
workspacePath,
|
|
4188
|
-
projectPath: workspacePath,
|
|
4189
|
-
targetPath: workspacePath,
|
|
4190
|
-
agentTaskType: 'debugging',
|
|
4191
|
-
agentTimeoutMs: 90_000,
|
|
4192
|
-
_isHealingRound: true,
|
|
4193
|
-
});
|
|
4194
|
-
// Re-run validators to check healing success
|
|
4195
|
-
const recheck = await (0, post_write_validator_js_1.runPostWriteValidation)(workspacePath);
|
|
4196
|
-
const passed = recheck.filter(r => r.ran).every(r => r.passed);
|
|
4197
|
-
return { healingAttempted: true, passed, tool: failures.map(f => f.tool).join('+') };
|
|
4198
|
-
}
|
|
4199
|
-
catch {
|
|
4200
|
-
return { healingAttempted: true, passed: false, tool: failures.map(f => f.tool).join('+') };
|
|
4201
|
-
}
|
|
4202
|
-
}
|
|
4203
4388
|
resolveModelId(shortName) {
|
|
4204
4389
|
const modelMap = {
|
|
4205
4390
|
// ═══════════════════════════════════════════════════════════════
|
|
4206
4391
|
// VIGTHORIA LOCAL - Self-hosted models
|
|
4207
4392
|
// ═══════════════════════════════════════════════════════════════
|
|
4208
|
-
'fast': 'vigthoria-
|
|
4209
|
-
'mini': 'vigthoria-
|
|
4210
|
-
'balanced': '
|
|
4393
|
+
'fast': 'vigthoria-fast-1.7b',
|
|
4394
|
+
'mini': 'vigthoria-mini-0.6b',
|
|
4395
|
+
'balanced': 'vigthoria-balanced-4b',
|
|
4211
4396
|
'balanced-4b': 'vigthoria-balanced-4b',
|
|
4212
|
-
'creative': 'vigthoria-
|
|
4213
|
-
// Code
|
|
4214
|
-
'code': 'vigthoria-v3-code-35b',
|
|
4397
|
+
'creative': 'vigthoria-creative-9b-v4',
|
|
4398
|
+
// Code Models - 30B is the default powerhouse
|
|
4399
|
+
'code': 'vigthoria-v3-code-35b', // Internal: self-hosted 35B on Blackwell
|
|
4215
4400
|
'code-30b': 'vigthoria-v3-code-35b',
|
|
4216
|
-
'code-
|
|
4217
|
-
'code-8b': 'vigthoria_c1_m',
|
|
4218
|
-
'code-9b': 'vigthoria_c1_m',
|
|
4401
|
+
'code-8b': 'vigthoria-v2-code-8b',
|
|
4219
4402
|
'pro': 'vigthoria-v3-code-35b',
|
|
4220
4403
|
'agent': 'vigthoria-v3-code-35b',
|
|
4221
4404
|
'vigthoria-code': 'vigthoria-v3-code-35b',
|
|
@@ -4227,6 +4410,7 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4227
4410
|
'cloud-reason': 'vigthoria-cloud-k2',
|
|
4228
4411
|
'ultra': 'vigthoria-cloud-ultra',
|
|
4229
4412
|
};
|
|
4413
|
+
// If already a full model ID, return as-is
|
|
4230
4414
|
if (shortName.includes('vigthoria') || shortName.includes('/') || shortName.includes(':')) {
|
|
4231
4415
|
if (modelMap[shortName]) {
|
|
4232
4416
|
return modelMap[shortName];
|
|
@@ -4237,7 +4421,7 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4237
4421
|
}
|
|
4238
4422
|
async getCoderHealth() {
|
|
4239
4423
|
try {
|
|
4240
|
-
const response = await this.client.get('/api/health', { timeout:
|
|
4424
|
+
const response = await this.client.get('/api/health', { timeout: 5000 });
|
|
4241
4425
|
const ok = response.data?.status === 'ok' || response.data?.healthy === true;
|
|
4242
4426
|
return {
|
|
4243
4427
|
name: 'Coder API',
|
|
@@ -4259,8 +4443,8 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4259
4443
|
const modelsApiUrl = this.config.get('modelsApiUrl');
|
|
4260
4444
|
try {
|
|
4261
4445
|
const [healthResponse, modelsResponse] = await Promise.all([
|
|
4262
|
-
this.modelRouterClient.get('/health', { timeout:
|
|
4263
|
-
this.modelRouterClient.get('/v1/models', { timeout:
|
|
4446
|
+
this.modelRouterClient.get('/health', { timeout: 5000 }),
|
|
4447
|
+
this.modelRouterClient.get('/v1/models', { timeout: 5000 }),
|
|
4264
4448
|
]);
|
|
4265
4449
|
const healthOk = healthResponse.data?.status === 'healthy'
|
|
4266
4450
|
|| healthResponse.data?.status === 'ok'
|
|
@@ -4291,7 +4475,7 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4291
4475
|
return null;
|
|
4292
4476
|
}
|
|
4293
4477
|
try {
|
|
4294
|
-
const response = await this.selfHostedModelRouterClient.get('/health', { timeout:
|
|
4478
|
+
const response = await this.selfHostedModelRouterClient.get('/health', { timeout: 5000 });
|
|
4295
4479
|
const ok = response.data?.status === 'healthy'
|
|
4296
4480
|
|| response.data?.status === 'ok'
|
|
4297
4481
|
|| response.data?.healthy === true;
|
|
@@ -4311,29 +4495,6 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4311
4495
|
};
|
|
4312
4496
|
}
|
|
4313
4497
|
}
|
|
4314
|
-
async attemptV3ServiceRecovery(reason = '', options = {}) {
|
|
4315
|
-
const attempts = Math.max(1, Number(options.attempts || 2));
|
|
4316
|
-
const delayMs = Math.max(0, Number(options.delayMs || 1200));
|
|
4317
|
-
let lastError = '';
|
|
4318
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
4319
|
-
const health = await this.getV3AgentHealth();
|
|
4320
|
-
if (health.ok) {
|
|
4321
|
-
const msg = attempt === 1
|
|
4322
|
-
? 'V3 service is reachable.'
|
|
4323
|
-
: `V3 service recovered after retry ${attempt}.`;
|
|
4324
|
-
return { recovered: true, message: msg, endpoint: health.endpoint };
|
|
4325
|
-
}
|
|
4326
|
-
lastError = health.error || 'health probe failed';
|
|
4327
|
-
if (attempt < attempts && delayMs > 0) {
|
|
4328
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4329
|
-
}
|
|
4330
|
-
}
|
|
4331
|
-
const reasonText = sanitizeUserFacingErrorText(reason || lastError || 'unknown failure');
|
|
4332
|
-
return {
|
|
4333
|
-
recovered: false,
|
|
4334
|
-
message: reasonText ? `Recovery failed: ${reasonText}` : 'Recovery failed: V3 service is still unreachable.',
|
|
4335
|
-
};
|
|
4336
|
-
}
|
|
4337
4498
|
async getV3AgentHealth() {
|
|
4338
4499
|
const baseUrl = this.getV3AgentBaseUrls()[0];
|
|
4339
4500
|
// Try multiple health endpoint patterns — the V3 backend may expose
|
|
@@ -4347,7 +4508,7 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4347
4508
|
for (const endpoint of candidates) {
|
|
4348
4509
|
try {
|
|
4349
4510
|
const controller = new AbortController();
|
|
4350
|
-
const timer = setTimeout(() => controller.abort(),
|
|
4511
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
4351
4512
|
const response = await fetch(endpoint, {
|
|
4352
4513
|
method: 'GET',
|
|
4353
4514
|
headers,
|
|
@@ -4395,7 +4556,7 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4395
4556
|
const runUrl = this.getV3AgentRunUrl(baseUrl);
|
|
4396
4557
|
try {
|
|
4397
4558
|
const controller = new AbortController();
|
|
4398
|
-
const timer = setTimeout(() => controller.abort(),
|
|
4559
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
4399
4560
|
const probe = await fetch(runUrl, { method: 'OPTIONS', headers, signal: controller.signal });
|
|
4400
4561
|
clearTimeout(timer);
|
|
4401
4562
|
if (probe.ok || probe.status === 204 || probe.status === 405) {
|
|
@@ -4518,6 +4679,20 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4518
4679
|
};
|
|
4519
4680
|
}
|
|
4520
4681
|
}
|
|
4682
|
+
async runSelfHealingCycle(_originalPrompt, _workspacePath, _context = {}) {
|
|
4683
|
+
return {
|
|
4684
|
+
healingAttempted: false,
|
|
4685
|
+
passed: true,
|
|
4686
|
+
tool: 'disabled',
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
async attemptV3ServiceRecovery(reason = '', _options = {}) {
|
|
4690
|
+
const safeReason = sanitizeUserFacingErrorText(reason || 'unknown failure');
|
|
4691
|
+
return {
|
|
4692
|
+
recovered: false,
|
|
4693
|
+
message: safeReason ? `Recovery unavailable: ${safeReason}` : 'Recovery unavailable',
|
|
4694
|
+
};
|
|
4695
|
+
}
|
|
4521
4696
|
async getDevtoolsBridgeStatus() {
|
|
4522
4697
|
const host = process.env.VIGTHORIA_DEVTOOLS_BRIDGE_HOST || '127.0.0.1';
|
|
4523
4698
|
const port = Number.parseInt(process.env.VIGTHORIA_DEVTOOLS_BRIDGE_PORT || '4016', 10);
|
|
@@ -4552,11 +4727,18 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4552
4727
|
});
|
|
4553
4728
|
}
|
|
4554
4729
|
async getCapabilityTruthStatus(context = {}) {
|
|
4730
|
+
// Wrap each probe with its own 6 s timeout so they always resolve
|
|
4731
|
+
// before the outer 8 s race in auth.ts, producing real error messages
|
|
4732
|
+
// (ECONNREFUSED, 404, etc.) instead of the generic "Timed out (8s)".
|
|
4733
|
+
const withTimeout = (p, name) => Promise.race([
|
|
4734
|
+
p,
|
|
4735
|
+
new Promise(resolve => setTimeout(() => resolve({ name, endpoint: '', ok: false, error: 'Service not reachable (6 s timeout)' }), 6000)),
|
|
4736
|
+
]);
|
|
4555
4737
|
const [v3Agent, hyperLoop, repoMemory, devtoolsBridge] = await Promise.all([
|
|
4556
|
-
this.getV3AgentHealth(),
|
|
4557
|
-
this.getHyperLoopHealth(),
|
|
4558
|
-
this.getRepoMemoryHealth(context),
|
|
4559
|
-
this.getDevtoolsBridgeStatus(),
|
|
4738
|
+
withTimeout(this.getV3AgentHealth(), 'V3 Agent'),
|
|
4739
|
+
withTimeout(this.getHyperLoopHealth(), 'Hyper Loop'),
|
|
4740
|
+
withTimeout(this.getRepoMemoryHealth(context), 'Repo Memory'),
|
|
4741
|
+
withTimeout(this.getDevtoolsBridgeStatus(), 'DevTools Bridge'),
|
|
4560
4742
|
]);
|
|
4561
4743
|
return {
|
|
4562
4744
|
overallOk: v3Agent.ok && hyperLoop.ok && repoMemory.ok,
|