vigthoria-cli 1.9.2 → 1.9.5
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 +389 -320
- package/dist/commands/chat.d.ts +3 -0
- package/dist/commands/chat.js +66 -15
- package/dist/commands/index.js +1 -1
- package/dist/commands/legion.d.ts +22 -19
- package/dist/commands/legion.js +550 -132
- package/dist/commands/preview.js +32 -7
- package/dist/commands/repo.js +19 -13
- 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 +114 -33
- package/dist/utils/api.d.ts +25 -70
- package/dist/utils/api.js +784 -695
- package/dist/utils/tools.d.ts +11 -0
- package/dist/utils/tools.js +222 -0
- package/package.json +7 -1
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,150 +92,56 @@ 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;
|
|
99
|
+
}
|
|
100
|
+
const withoutTags = raw.replace(/<[^>]+>/g, ' ');
|
|
101
|
+
return withoutTags.replace(/\s+/g, ' ').trim();
|
|
294
102
|
}
|
|
295
103
|
function isServerRuntime() {
|
|
296
|
-
if (process.env.
|
|
297
|
-
return true;
|
|
298
|
-
if (process.env.VIGTHORIA_SERVER_RUNTIME === '1')
|
|
104
|
+
if (process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1') {
|
|
299
105
|
return true;
|
|
300
|
-
|
|
106
|
+
}
|
|
107
|
+
const host = String(process.env.HOSTNAME || '').toLowerCase();
|
|
108
|
+
const cwd = String(process.cwd() || '').toLowerCase();
|
|
109
|
+
return host.includes('ubuntu') || cwd.startsWith('/var/www');
|
|
301
110
|
}
|
|
302
111
|
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
112
|
if (status >= 500)
|
|
312
|
-
return '
|
|
113
|
+
return 'upstream internal error';
|
|
114
|
+
if (status === 429)
|
|
115
|
+
return 'rate limited';
|
|
116
|
+
if (status === 404)
|
|
117
|
+
return 'endpoint not found';
|
|
118
|
+
if (status === 403)
|
|
119
|
+
return 'forbidden';
|
|
120
|
+
if (status === 401)
|
|
121
|
+
return 'unauthorized';
|
|
313
122
|
if (status >= 400)
|
|
314
|
-
return '
|
|
315
|
-
return '
|
|
316
|
-
}
|
|
317
|
-
const JWT_VALIDATE_EXPIRY_SKEW_MS = 60_000;
|
|
318
|
-
function validateJwtExpiry(token) {
|
|
319
|
-
if (!token || typeof token !== 'string')
|
|
320
|
-
return false;
|
|
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;
|
|
326
|
-
}
|
|
327
|
-
function validateJwt(token) {
|
|
328
|
-
if (!token || typeof token !== 'string')
|
|
329
|
-
return null;
|
|
330
|
-
const payload = decodeJwtPayload(token);
|
|
331
|
-
if (!payload)
|
|
332
|
-
return null;
|
|
333
|
-
try {
|
|
334
|
-
if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp))
|
|
335
|
-
return null;
|
|
336
|
-
if (!validateJwtExpiry(token))
|
|
337
|
-
return null;
|
|
338
|
-
return payload;
|
|
339
|
-
}
|
|
340
|
-
catch (error) {
|
|
341
|
-
if (process.env.VIGTHORIA_DEBUG_JWT === '1') {
|
|
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;
|
|
346
|
-
}
|
|
123
|
+
return 'bad request';
|
|
124
|
+
return 'ok';
|
|
347
125
|
}
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
}
|
|
397
|
-
}
|
|
398
|
-
function createApiClient(config) {
|
|
399
|
-
const apiClient = new APIClient(config, new logger_js_1.Logger());
|
|
400
|
-
return {
|
|
401
|
-
get: async (path) => {
|
|
402
|
-
try {
|
|
403
|
-
const response = await apiClient.client.get(path);
|
|
404
|
-
return response.data;
|
|
405
|
-
}
|
|
406
|
-
catch (err) {
|
|
407
|
-
propagateError(err);
|
|
408
|
-
}
|
|
409
|
-
},
|
|
410
|
-
post: async (path, body) => {
|
|
411
|
-
try {
|
|
412
|
-
const response = await apiClient.client.post(path, body);
|
|
413
|
-
return response.data;
|
|
414
|
-
}
|
|
415
|
-
catch (err) {
|
|
416
|
-
propagateError(err);
|
|
417
|
-
}
|
|
418
|
-
},
|
|
419
|
-
handleAuthError: (err) => {
|
|
420
|
-
const authError = handleAuthError(err);
|
|
421
|
-
throw new CLIError(authError.message, 'auth', { cause: err instanceof Error ? err : undefined });
|
|
126
|
+
function propagateError(err) {
|
|
127
|
+
const status = typeof err?.statusCode === 'number'
|
|
128
|
+
? err.statusCode
|
|
129
|
+
: typeof err?.status === 'number'
|
|
130
|
+
? err.status
|
|
131
|
+
: typeof err?.response?.status === 'number'
|
|
132
|
+
? err.response.status
|
|
133
|
+
: 500;
|
|
134
|
+
const endpoint = err?.endpoint || err?.config?.url || err?.details?.endpoint || 'unknown';
|
|
135
|
+
const message = sanitizeUserFacingErrorText(String(err?.message || 'API request failed'));
|
|
136
|
+
throw {
|
|
137
|
+
code: status,
|
|
138
|
+
message,
|
|
139
|
+
isAuthError: status === 401 || status === 403,
|
|
140
|
+
details: {
|
|
141
|
+
...(err?.details && typeof err.details === 'object' ? err.details : {}),
|
|
142
|
+
endpoint,
|
|
143
|
+
status,
|
|
144
|
+
originalCode: err?.code,
|
|
422
145
|
},
|
|
423
146
|
};
|
|
424
147
|
}
|
|
@@ -432,11 +155,6 @@ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
|
432
155
|
const parsed = Number.parseInt(rawValue, 10);
|
|
433
156
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
434
157
|
})();
|
|
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
158
|
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
441
159
|
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '300000';
|
|
442
160
|
const parsed = Number.parseInt(rawValue, 10);
|
|
@@ -447,11 +165,10 @@ class APIClient {
|
|
|
447
165
|
modelRouterClient;
|
|
448
166
|
selfHostedModelRouterClient;
|
|
449
167
|
config;
|
|
450
|
-
token = null;
|
|
451
|
-
expiresAt = null;
|
|
452
168
|
logger;
|
|
453
169
|
ws = null;
|
|
454
170
|
vigFlowTokens = new Map();
|
|
171
|
+
_httpsAgent = null;
|
|
455
172
|
constructor(config, logger) {
|
|
456
173
|
this.config = config;
|
|
457
174
|
this.logger = logger;
|
|
@@ -461,6 +178,7 @@ class APIClient {
|
|
|
461
178
|
keepAlive: true,
|
|
462
179
|
timeout: 30000,
|
|
463
180
|
});
|
|
181
|
+
this._httpsAgent = httpsAgent;
|
|
464
182
|
// Main Vigthoria Coder API (coder.vigthoria.io)
|
|
465
183
|
this.client = axios_1.default.create({
|
|
466
184
|
baseURL: config.get('apiUrl'),
|
|
@@ -492,77 +210,41 @@ class APIClient {
|
|
|
492
210
|
'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
|
|
493
211
|
},
|
|
494
212
|
}) : 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
|
-
// Add response interceptors for token refresh + structured errors.
|
|
213
|
+
// Add auth interceptor
|
|
214
|
+
this.client.interceptors.request.use((req) => {
|
|
215
|
+
const token = this.config.get('authToken');
|
|
216
|
+
if (token) {
|
|
217
|
+
req.headers.Authorization = `Bearer ${token}`;
|
|
218
|
+
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
219
|
+
}
|
|
220
|
+
return req;
|
|
221
|
+
});
|
|
222
|
+
this.modelRouterClient.interceptors.request.use((req) => {
|
|
223
|
+
const token = this.config.get('authToken');
|
|
224
|
+
if (token) {
|
|
225
|
+
req.headers.Authorization = `Bearer ${token}`;
|
|
226
|
+
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
227
|
+
}
|
|
228
|
+
return req;
|
|
229
|
+
});
|
|
230
|
+
this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
|
|
231
|
+
const token = this.config.get('authToken');
|
|
232
|
+
if (token) {
|
|
233
|
+
req.headers.Authorization = `Bearer ${token}`;
|
|
234
|
+
}
|
|
235
|
+
return req;
|
|
236
|
+
});
|
|
237
|
+
// Add response interceptors for token refresh + structured errors
|
|
522
238
|
const createAuthRetryInterceptor = (client) => {
|
|
523
239
|
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
|
-
});
|
|
240
|
+
if (error.response?.status === 401) {
|
|
241
|
+
const refreshed = await this.refreshToken();
|
|
242
|
+
if (refreshed && error.config) {
|
|
243
|
+
return client.request(error.config);
|
|
550
244
|
}
|
|
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
|
-
});
|
|
245
|
+
throw classifyError(error, 'auth');
|
|
565
246
|
}
|
|
247
|
+
throw classifyError(error);
|
|
566
248
|
});
|
|
567
249
|
};
|
|
568
250
|
createAuthRetryInterceptor(this.client);
|
|
@@ -571,12 +253,22 @@ class APIClient {
|
|
|
571
253
|
createAuthRetryInterceptor(this.selfHostedModelRouterClient);
|
|
572
254
|
}
|
|
573
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Destroy keep-alive sockets so the Node.js event loop can drain
|
|
258
|
+
* naturally. Call this before exiting commands that run HTTP probes
|
|
259
|
+
* (e.g. `status`) to avoid the libuv UV_HANDLE_CLOSING assertion
|
|
260
|
+
* on Windows / Node 25+.
|
|
261
|
+
*/
|
|
574
262
|
destroy() {
|
|
263
|
+
if (this._httpsAgent) {
|
|
264
|
+
this._httpsAgent.destroy();
|
|
265
|
+
this._httpsAgent = null;
|
|
266
|
+
}
|
|
575
267
|
if (this.ws) {
|
|
576
268
|
try {
|
|
577
269
|
this.ws.close();
|
|
578
270
|
}
|
|
579
|
-
catch { }
|
|
271
|
+
catch { /* ok */ }
|
|
580
272
|
this.ws = null;
|
|
581
273
|
}
|
|
582
274
|
}
|
|
@@ -656,26 +348,6 @@ class APIClient {
|
|
|
656
348
|
}
|
|
657
349
|
}
|
|
658
350
|
}
|
|
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
351
|
this.config.clearAuth();
|
|
680
352
|
return false;
|
|
681
353
|
}
|
|
@@ -685,19 +357,6 @@ class APIClient {
|
|
|
685
357
|
return false;
|
|
686
358
|
}
|
|
687
359
|
}
|
|
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
360
|
extractUserProfile(data) {
|
|
702
361
|
if (!data) {
|
|
703
362
|
return null;
|
|
@@ -734,11 +393,9 @@ class APIClient {
|
|
|
734
393
|
}
|
|
735
394
|
return true;
|
|
736
395
|
}
|
|
737
|
-
catch
|
|
738
|
-
const cliError = toCliError(error, 'AUTH_REFRESH_FAILED', 'Failed to refresh authentication token');
|
|
739
|
-
this.logger.debug(`Token refresh failed: ${cliError.message}`);
|
|
396
|
+
catch {
|
|
740
397
|
this.config.clearAuth();
|
|
741
|
-
|
|
398
|
+
return false;
|
|
742
399
|
}
|
|
743
400
|
}
|
|
744
401
|
async getSubscriptionStatus() {
|
|
@@ -773,35 +430,35 @@ class APIClient {
|
|
|
773
430
|
if (!token) {
|
|
774
431
|
return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
|
|
775
432
|
}
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
433
|
+
// Probe both endpoints in parallel. If EITHER succeeds the token is
|
|
434
|
+
// valid. Only if both return 401/403 is the token truly invalid.
|
|
435
|
+
// If both are unreachable assume the token is fine (offline scenario).
|
|
436
|
+
const results = await Promise.allSettled([
|
|
437
|
+
this.modelRouterClient.get('/v1/models', { timeout: 5000 }),
|
|
438
|
+
this.client.get('/api/user/profile', { timeout: 5000 }),
|
|
439
|
+
]);
|
|
440
|
+
for (const r of results) {
|
|
441
|
+
if (r.status === 'fulfilled')
|
|
782
442
|
return { valid: true };
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
443
|
+
}
|
|
444
|
+
// Both failed — check why
|
|
445
|
+
for (const r of results) {
|
|
446
|
+
if (r.status === 'rejected') {
|
|
447
|
+
const err = r.reason;
|
|
448
|
+
if (err.response?.status === 401 || err.response?.status === 403) {
|
|
786
449
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
787
450
|
}
|
|
788
|
-
|
|
789
|
-
if (axErr.response?.status === 401 || axErr.response?.status === 403) {
|
|
451
|
+
if (err instanceof CLIError && err.category === 'auth') {
|
|
790
452
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
791
453
|
}
|
|
792
|
-
// Try the next authenticated endpoint before deciding this is
|
|
793
|
-
// a transient network/backend issue.
|
|
794
|
-
continue;
|
|
795
454
|
}
|
|
796
455
|
}
|
|
797
|
-
//
|
|
456
|
+
// Both unreachable — don't assume token is bad
|
|
798
457
|
return { valid: true };
|
|
799
458
|
}
|
|
800
459
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
801
460
|
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;
|
|
461
|
+
const allowLocalV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1' || preferLocal;
|
|
805
462
|
const urls = [
|
|
806
463
|
process.env.VIGTHORIA_V3_AGENT_URL,
|
|
807
464
|
process.env.V3_AGENT_URL,
|
|
@@ -827,7 +484,7 @@ class APIClient {
|
|
|
827
484
|
const urls = [
|
|
828
485
|
process.env.VIGTHORIA_OPERATOR_URL,
|
|
829
486
|
process.env.OPERATOR_URL,
|
|
830
|
-
|
|
487
|
+
'http://127.0.0.1:4009',
|
|
831
488
|
configuredModelsApiUrl,
|
|
832
489
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
833
490
|
return [...new Set(urls)];
|
|
@@ -840,7 +497,7 @@ class APIClient {
|
|
|
840
497
|
const urls = [
|
|
841
498
|
process.env.VIGTHORIA_MCP_URL,
|
|
842
499
|
process.env.MCP_SERVER_URL,
|
|
843
|
-
|
|
500
|
+
'http://127.0.0.1:4008',
|
|
844
501
|
configuredApiUrl,
|
|
845
502
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
846
503
|
return [...new Set(urls)];
|
|
@@ -858,7 +515,8 @@ class APIClient {
|
|
|
858
515
|
process.env.VIGFLOW_URL,
|
|
859
516
|
process.env.WORKFLOW_BUILDER_URL,
|
|
860
517
|
`${configuredApiUrl}/api/vigflow`,
|
|
861
|
-
|
|
518
|
+
'http://127.0.0.1:5060',
|
|
519
|
+
'http://127.0.0.1:5050',
|
|
862
520
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
863
521
|
return [...new Set(urls)];
|
|
864
522
|
}
|
|
@@ -867,14 +525,11 @@ class APIClient {
|
|
|
867
525
|
const urls = [
|
|
868
526
|
process.env.VIGTHORIA_TEMPLATE_SERVICE_URL,
|
|
869
527
|
process.env.TEMPLATE_SERVICE_URL,
|
|
870
|
-
|
|
528
|
+
'http://127.0.0.1:4011',
|
|
871
529
|
`${configuredApiUrl}/api/template-service`,
|
|
872
530
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
873
531
|
return [...new Set(urls)];
|
|
874
532
|
}
|
|
875
|
-
allowLocalServiceFallbacks() {
|
|
876
|
-
return process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1' || isServerRuntime();
|
|
877
|
-
}
|
|
878
533
|
isFrontendTask(message = '', context = {}) {
|
|
879
534
|
// Never treat analysis-only tasks as frontend tasks — preview gate
|
|
880
535
|
// should not fire for read-only inspection prompts.
|
|
@@ -1196,7 +851,7 @@ class APIClient {
|
|
|
1196
851
|
});
|
|
1197
852
|
if (!response.ok) {
|
|
1198
853
|
const errorText = await response.text().catch(() => '');
|
|
1199
|
-
throw new Error(`Template preview proof ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
854
|
+
throw new Error(`Template preview proof ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
1200
855
|
}
|
|
1201
856
|
const payload = await response.json();
|
|
1202
857
|
const modes = payload?.modes || {};
|
|
@@ -1249,12 +904,6 @@ class APIClient {
|
|
|
1249
904
|
'Content-Type': 'application/json',
|
|
1250
905
|
Accept: 'application/json',
|
|
1251
906
|
};
|
|
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
907
|
const authToken = this.getAccessToken();
|
|
1259
908
|
if (authToken) {
|
|
1260
909
|
headers.Authorization = `Bearer ${authToken}`;
|
|
@@ -1266,12 +915,6 @@ class APIClient {
|
|
|
1266
915
|
const headers = {
|
|
1267
916
|
'Content-Type': 'application/json',
|
|
1268
917
|
};
|
|
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
918
|
const authToken = this.getAccessToken();
|
|
1276
919
|
if (authToken) {
|
|
1277
920
|
headers.Authorization = `Bearer ${authToken}`;
|
|
@@ -1352,7 +995,8 @@ class APIClient {
|
|
|
1352
995
|
this.logger.debug(`VigFlow ${operation} via ${baseUrl} failed:`, lastError.message);
|
|
1353
996
|
}
|
|
1354
997
|
}
|
|
1355
|
-
|
|
998
|
+
// Throw a clean message instead of the raw ECONNREFUSED from the last URL tried
|
|
999
|
+
throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
|
|
1356
1000
|
}
|
|
1357
1001
|
/**
|
|
1358
1002
|
* Build the correct sub-path for VigFlow endpoints.
|
|
@@ -1489,9 +1133,7 @@ class APIClient {
|
|
|
1489
1133
|
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
1490
1134
|
const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
|
|
1491
1135
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1492
|
-
const localWorkspaceSummary =
|
|
1493
|
-
? this.buildSemanticWorkspaceSummary(localWorkspacePath, String(resolvedContext.rawPrompt))
|
|
1494
|
-
: this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
1136
|
+
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
1495
1137
|
const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
|
|
1496
1138
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
1497
1139
|
// When the server cannot directly access the workspace (e.g. Windows
|
|
@@ -1646,6 +1288,458 @@ class APIClient {
|
|
|
1646
1288
|
const match = String(message || '').match(/called\s+([A-Z][A-Za-z0-9&\- ]{2,40})/i);
|
|
1647
1289
|
return match?.[1]?.trim() || fallback;
|
|
1648
1290
|
}
|
|
1291
|
+
materializeEmergencySaaSWorkspace(message = '', context = {}) {
|
|
1292
|
+
const rootPath = this.resolveAgentTargetPath(context);
|
|
1293
|
+
if (!rootPath) {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
fs_1.default.mkdirSync(rootPath, { recursive: true });
|
|
1297
|
+
const appName = this.extractEmergencyAppName(message);
|
|
1298
|
+
const html = `<!DOCTYPE html>
|
|
1299
|
+
<html lang="en">
|
|
1300
|
+
<head>
|
|
1301
|
+
<meta charset="UTF-8">
|
|
1302
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1303
|
+
<title>${appName}</title>
|
|
1304
|
+
<link rel="stylesheet" href="styles.css">
|
|
1305
|
+
</head>
|
|
1306
|
+
<body>
|
|
1307
|
+
<div class="app-shell">
|
|
1308
|
+
<aside class="sidebar">
|
|
1309
|
+
<div class="brand">${appName}</div>
|
|
1310
|
+
<button class="menu-toggle" id="menu-toggle" aria-label="Toggle navigation">Menu</button>
|
|
1311
|
+
<nav>
|
|
1312
|
+
<a href="#dashboard" class="nav-link active">Dashboard</a>
|
|
1313
|
+
<a href="#team" class="nav-link">Team</a>
|
|
1314
|
+
<a href="#billing" class="nav-link">Billing</a>
|
|
1315
|
+
<a href="#settings" class="nav-link">Settings</a>
|
|
1316
|
+
</nav>
|
|
1317
|
+
</aside>
|
|
1318
|
+
<main class="content">
|
|
1319
|
+
<section class="hero-card panel active-panel" id="dashboard">
|
|
1320
|
+
<div class="hero-copy">
|
|
1321
|
+
<p class="eyebrow">Dashboard</p>
|
|
1322
|
+
<h1>${appName} revenue command center</h1>
|
|
1323
|
+
<p>Track login activity, campaign velocity, billing state, and team performance from one responsive SaaS workspace.</p>
|
|
1324
|
+
</div>
|
|
1325
|
+
<form class="login-card">
|
|
1326
|
+
<h2>Login</h2>
|
|
1327
|
+
<label>Email<input type="email" placeholder="ops@${appName.toLowerCase().replace(/[^a-z0-9]+/g, '') || 'signaldesk'}.io"></label>
|
|
1328
|
+
<label>Password<input type="password" placeholder="Enter password"></label>
|
|
1329
|
+
<button type="submit">Enter dashboard</button>
|
|
1330
|
+
</form>
|
|
1331
|
+
</section>
|
|
1332
|
+
|
|
1333
|
+
<section class="stats-grid">
|
|
1334
|
+
<article class="stat-card"><span>MRR</span><strong>$284K</strong><em>+12.4%</em></article>
|
|
1335
|
+
<article class="stat-card"><span>Activation</span><strong>74%</strong><em>+6.1%</em></article>
|
|
1336
|
+
<article class="stat-card"><span>Team Seats</span><strong>128</strong><em>8 pending</em></article>
|
|
1337
|
+
<article class="stat-card"><span>Churn Risk</span><strong>2.1%</strong><em>Low</em></article>
|
|
1338
|
+
</section>
|
|
1339
|
+
|
|
1340
|
+
<section class="workspace-grid">
|
|
1341
|
+
<article class="panel chart-panel">
|
|
1342
|
+
<div class="panel-header">
|
|
1343
|
+
<h2>Analytics</h2>
|
|
1344
|
+
<button id="open-modal" type="button">Add campaign</button>
|
|
1345
|
+
</div>
|
|
1346
|
+
<div class="chart-bars" aria-label="Revenue chart">
|
|
1347
|
+
<div class="bar" style="--value: 52%"><span>Mon</span></div>
|
|
1348
|
+
<div class="bar" style="--value: 68%"><span>Tue</span></div>
|
|
1349
|
+
<div class="bar" style="--value: 74%"><span>Wed</span></div>
|
|
1350
|
+
<div class="bar" style="--value: 59%"><span>Thu</span></div>
|
|
1351
|
+
<div class="bar" style="--value: 88%"><span>Fri</span></div>
|
|
1352
|
+
</div>
|
|
1353
|
+
</article>
|
|
1354
|
+
|
|
1355
|
+
<article class="panel activity-panel">
|
|
1356
|
+
<div class="panel-header"><h2>Activity Feed</h2><span>Live</span></div>
|
|
1357
|
+
<ul class="activity-feed">
|
|
1358
|
+
<li><strong>Billing</strong><span>Enterprise invoice paid</span></li>
|
|
1359
|
+
<li><strong>Team</strong><span>New strategist invited to workspace</span></li>
|
|
1360
|
+
<li><strong>Dashboard</strong><span>KPI threshold updated for activation alerts</span></li>
|
|
1361
|
+
</ul>
|
|
1362
|
+
</article>
|
|
1363
|
+
|
|
1364
|
+
<article class="panel" id="team">
|
|
1365
|
+
<div class="panel-header"><h2>Team Management</h2><span>Owners and operators</span></div>
|
|
1366
|
+
<div class="team-list">
|
|
1367
|
+
<div><strong>Ana</strong><span>Growth lead</span></div>
|
|
1368
|
+
<div><strong>Marcus</strong><span>Billing admin</span></div>
|
|
1369
|
+
<div><strong>Lina</strong><span>Lifecycle analyst</span></div>
|
|
1370
|
+
</div>
|
|
1371
|
+
</article>
|
|
1372
|
+
|
|
1373
|
+
<article class="panel" id="billing">
|
|
1374
|
+
<div class="panel-header"><h2>Billing</h2><span>Current plan</span></div>
|
|
1375
|
+
<div class="billing-card">
|
|
1376
|
+
<strong>Scale Annual</strong>
|
|
1377
|
+
<p>Renews on 12 Oct with usage-based analytics overages.</p>
|
|
1378
|
+
<button type="button" class="secondary-action">Update payment method</button>
|
|
1379
|
+
</div>
|
|
1380
|
+
</article>
|
|
1381
|
+
|
|
1382
|
+
<article class="panel" id="settings">
|
|
1383
|
+
<div class="panel-header"><h2>Settings</h2><span>Automation and alerts</span></div>
|
|
1384
|
+
<form class="settings-form">
|
|
1385
|
+
<label>Alert threshold<input type="number" value="18"></label>
|
|
1386
|
+
<label>Weekly digest<select><option>Enabled</option><option>Paused</option></select></label>
|
|
1387
|
+
<button type="submit">Save settings</button>
|
|
1388
|
+
</form>
|
|
1389
|
+
</article>
|
|
1390
|
+
</section>
|
|
1391
|
+
</main>
|
|
1392
|
+
</div>
|
|
1393
|
+
|
|
1394
|
+
<dialog id="campaign-modal">
|
|
1395
|
+
<form method="dialog" class="modal-form">
|
|
1396
|
+
<h2>Launch campaign</h2>
|
|
1397
|
+
<label>Name<input type="text" placeholder="Retention push"></label>
|
|
1398
|
+
<label>Owner<input type="text" placeholder="Lina"></label>
|
|
1399
|
+
<menu>
|
|
1400
|
+
<button value="cancel">Cancel</button>
|
|
1401
|
+
<button value="confirm">Create</button>
|
|
1402
|
+
</menu>
|
|
1403
|
+
</form>
|
|
1404
|
+
</dialog>
|
|
1405
|
+
|
|
1406
|
+
<script src="scripts.js"></script>
|
|
1407
|
+
</body>
|
|
1408
|
+
</html>
|
|
1409
|
+
`;
|
|
1410
|
+
const css = `:root {
|
|
1411
|
+
--bg: #f2ede4;
|
|
1412
|
+
--ink: #18222f;
|
|
1413
|
+
--muted: #5c6674;
|
|
1414
|
+
--panel: rgba(255, 255, 255, 0.82);
|
|
1415
|
+
--line: rgba(24, 34, 47, 0.08);
|
|
1416
|
+
--accent: #b6542c;
|
|
1417
|
+
--accent-strong: #7f3417;
|
|
1418
|
+
--shadow: 0 24px 60px rgba(24, 34, 47, 0.12);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
* { box-sizing: border-box; }
|
|
1422
|
+
|
|
1423
|
+
body {
|
|
1424
|
+
margin: 0;
|
|
1425
|
+
font-family: "Georgia", "Times New Roman", serif;
|
|
1426
|
+
color: var(--ink);
|
|
1427
|
+
background:
|
|
1428
|
+
radial-gradient(circle at top left, rgba(182, 84, 44, 0.18), transparent 28%),
|
|
1429
|
+
radial-gradient(circle at bottom right, rgba(24, 34, 47, 0.14), transparent 30%),
|
|
1430
|
+
var(--bg);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.app-shell {
|
|
1434
|
+
min-height: 100vh;
|
|
1435
|
+
display: grid;
|
|
1436
|
+
grid-template-columns: 260px 1fr;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.sidebar {
|
|
1440
|
+
padding: 2rem 1.25rem;
|
|
1441
|
+
background: rgba(24, 34, 47, 0.94);
|
|
1442
|
+
color: #f7f2eb;
|
|
1443
|
+
position: sticky;
|
|
1444
|
+
top: 0;
|
|
1445
|
+
min-height: 100vh;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.brand {
|
|
1449
|
+
font-size: 1.6rem;
|
|
1450
|
+
font-weight: 700;
|
|
1451
|
+
margin-bottom: 1.5rem;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
.menu-toggle {
|
|
1455
|
+
display: none;
|
|
1456
|
+
margin-bottom: 1rem;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
nav {
|
|
1460
|
+
display: grid;
|
|
1461
|
+
gap: 0.6rem;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.nav-link {
|
|
1465
|
+
color: inherit;
|
|
1466
|
+
text-decoration: none;
|
|
1467
|
+
padding: 0.8rem 0.95rem;
|
|
1468
|
+
border-radius: 999px;
|
|
1469
|
+
transition: transform 0.25s ease, background-color 0.25s ease;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
.nav-link:hover,
|
|
1473
|
+
.nav-link.active {
|
|
1474
|
+
background: rgba(255, 255, 255, 0.12);
|
|
1475
|
+
transform: translateX(4px);
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
.content {
|
|
1479
|
+
padding: 2rem;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
.hero-card,
|
|
1483
|
+
.panel,
|
|
1484
|
+
.stat-card,
|
|
1485
|
+
.login-card,
|
|
1486
|
+
dialog {
|
|
1487
|
+
background: var(--panel);
|
|
1488
|
+
backdrop-filter: blur(16px);
|
|
1489
|
+
border: 1px solid var(--line);
|
|
1490
|
+
box-shadow: var(--shadow);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
.hero-card {
|
|
1494
|
+
display: grid;
|
|
1495
|
+
grid-template-columns: 1.3fr 0.9fr;
|
|
1496
|
+
gap: 1.5rem;
|
|
1497
|
+
border-radius: 32px;
|
|
1498
|
+
padding: 2rem;
|
|
1499
|
+
margin-bottom: 1.5rem;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
.eyebrow {
|
|
1503
|
+
text-transform: uppercase;
|
|
1504
|
+
letter-spacing: 0.14em;
|
|
1505
|
+
color: var(--accent-strong);
|
|
1506
|
+
font-size: 0.78rem;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
.hero-card h1,
|
|
1510
|
+
.panel h2,
|
|
1511
|
+
.login-card h2 {
|
|
1512
|
+
margin: 0 0 0.75rem;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
.login-card,
|
|
1516
|
+
.panel,
|
|
1517
|
+
.stat-card {
|
|
1518
|
+
border-radius: 24px;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.login-card,
|
|
1522
|
+
.settings-form,
|
|
1523
|
+
.modal-form {
|
|
1524
|
+
display: grid;
|
|
1525
|
+
gap: 0.85rem;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.stats-grid,
|
|
1529
|
+
.workspace-grid {
|
|
1530
|
+
display: grid;
|
|
1531
|
+
gap: 1rem;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
.stats-grid {
|
|
1535
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1536
|
+
margin-bottom: 1rem;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
.workspace-grid {
|
|
1540
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
.stat-card,
|
|
1544
|
+
.panel {
|
|
1545
|
+
padding: 1.2rem;
|
|
1546
|
+
animation: riseIn 0.7s ease forwards;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
.stat-card span,
|
|
1550
|
+
.panel-header span,
|
|
1551
|
+
.activity-feed span,
|
|
1552
|
+
.team-list span,
|
|
1553
|
+
.billing-card p {
|
|
1554
|
+
color: var(--muted);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
.panel-header {
|
|
1558
|
+
display: flex;
|
|
1559
|
+
align-items: center;
|
|
1560
|
+
justify-content: space-between;
|
|
1561
|
+
gap: 1rem;
|
|
1562
|
+
margin-bottom: 1rem;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
.chart-bars {
|
|
1566
|
+
display: grid;
|
|
1567
|
+
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
1568
|
+
gap: 0.9rem;
|
|
1569
|
+
align-items: end;
|
|
1570
|
+
min-height: 220px;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
.bar {
|
|
1574
|
+
position: relative;
|
|
1575
|
+
min-height: 180px;
|
|
1576
|
+
border-radius: 20px 20px 8px 8px;
|
|
1577
|
+
background: linear-gradient(180deg, rgba(182, 84, 44, 0.92), rgba(127, 52, 23, 0.68));
|
|
1578
|
+
transform-origin: bottom;
|
|
1579
|
+
transform: scaleY(calc(var(--value) / 100));
|
|
1580
|
+
transition: transform 0.6s ease;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
.bar span {
|
|
1584
|
+
position: absolute;
|
|
1585
|
+
left: 50%;
|
|
1586
|
+
bottom: -1.6rem;
|
|
1587
|
+
transform: translateX(-50%);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
.activity-feed,
|
|
1591
|
+
.team-list {
|
|
1592
|
+
display: grid;
|
|
1593
|
+
gap: 0.8rem;
|
|
1594
|
+
padding: 0;
|
|
1595
|
+
margin: 0;
|
|
1596
|
+
list-style: none;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
.activity-feed li,
|
|
1600
|
+
.team-list div,
|
|
1601
|
+
.billing-card {
|
|
1602
|
+
padding: 0.9rem 1rem;
|
|
1603
|
+
border-radius: 18px;
|
|
1604
|
+
background: rgba(255, 255, 255, 0.7);
|
|
1605
|
+
border: 1px solid var(--line);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
label {
|
|
1609
|
+
display: grid;
|
|
1610
|
+
gap: 0.35rem;
|
|
1611
|
+
font-size: 0.95rem;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
input,
|
|
1615
|
+
select,
|
|
1616
|
+
button {
|
|
1617
|
+
font: inherit;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
input,
|
|
1621
|
+
select {
|
|
1622
|
+
width: 100%;
|
|
1623
|
+
padding: 0.85rem 1rem;
|
|
1624
|
+
border-radius: 14px;
|
|
1625
|
+
border: 1px solid var(--line);
|
|
1626
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
button {
|
|
1630
|
+
border: none;
|
|
1631
|
+
border-radius: 999px;
|
|
1632
|
+
padding: 0.85rem 1.2rem;
|
|
1633
|
+
background: var(--accent);
|
|
1634
|
+
color: #fff9f3;
|
|
1635
|
+
cursor: pointer;
|
|
1636
|
+
transition: transform 0.25s ease, background-color 0.25s ease;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
button:hover {
|
|
1640
|
+
background: var(--accent-strong);
|
|
1641
|
+
transform: translateY(-2px);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
.secondary-action,
|
|
1645
|
+
menu button:first-child {
|
|
1646
|
+
background: rgba(24, 34, 47, 0.12);
|
|
1647
|
+
color: var(--ink);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
dialog {
|
|
1651
|
+
border-radius: 28px;
|
|
1652
|
+
padding: 0;
|
|
1653
|
+
width: min(420px, calc(100% - 2rem));
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
dialog::backdrop {
|
|
1657
|
+
background: rgba(24, 34, 47, 0.3);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
.modal-form {
|
|
1661
|
+
padding: 1.4rem;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
menu {
|
|
1665
|
+
display: flex;
|
|
1666
|
+
justify-content: flex-end;
|
|
1667
|
+
gap: 0.75rem;
|
|
1668
|
+
padding: 0;
|
|
1669
|
+
margin: 0.5rem 0 0;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
@keyframes riseIn {
|
|
1673
|
+
from {
|
|
1674
|
+
opacity: 0;
|
|
1675
|
+
transform: translateY(18px);
|
|
1676
|
+
}
|
|
1677
|
+
to {
|
|
1678
|
+
opacity: 1;
|
|
1679
|
+
transform: translateY(0);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
@media (max-width: 980px) {
|
|
1684
|
+
.app-shell,
|
|
1685
|
+
.hero-card,
|
|
1686
|
+
.stats-grid,
|
|
1687
|
+
.workspace-grid {
|
|
1688
|
+
grid-template-columns: 1fr;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
.sidebar {
|
|
1692
|
+
position: static;
|
|
1693
|
+
min-height: auto;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
.menu-toggle {
|
|
1697
|
+
display: inline-flex;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
nav {
|
|
1701
|
+
display: none;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
nav.is-open {
|
|
1705
|
+
display: grid;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
`;
|
|
1709
|
+
const js = `document.addEventListener('DOMContentLoaded', () => {
|
|
1710
|
+
const menuToggle = document.getElementById('menu-toggle');
|
|
1711
|
+
const nav = document.querySelector('nav');
|
|
1712
|
+
const modal = document.getElementById('campaign-modal');
|
|
1713
|
+
const openModal = document.getElementById('open-modal');
|
|
1714
|
+
const navLinks = document.querySelectorAll('.nav-link');
|
|
1715
|
+
|
|
1716
|
+
menuToggle?.addEventListener('click', () => nav?.classList.toggle('is-open'));
|
|
1717
|
+
openModal?.addEventListener('click', () => modal?.showModal());
|
|
1718
|
+
modal?.addEventListener('close', () => document.body.classList.remove('modal-open'));
|
|
1719
|
+
|
|
1720
|
+
navLinks.forEach((link) => {
|
|
1721
|
+
link.addEventListener('click', (event) => {
|
|
1722
|
+
event.preventDefault();
|
|
1723
|
+
navLinks.forEach((entry) => entry.classList.remove('active'));
|
|
1724
|
+
link.classList.add('active');
|
|
1725
|
+
document.querySelector(link.getAttribute('href'))?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1726
|
+
nav?.classList.remove('is-open');
|
|
1727
|
+
});
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
document.querySelectorAll('.bar').forEach((bar, index) => {
|
|
1731
|
+
bar.animate([
|
|
1732
|
+
{ transform: 'scaleY(0.15)' },
|
|
1733
|
+
{ transform: getComputedStyle(bar).transform || 'scaleY(1)' }
|
|
1734
|
+
], { duration: 600 + index * 80, fill: 'forwards', easing: 'ease-out' });
|
|
1735
|
+
});
|
|
1736
|
+
});
|
|
1737
|
+
`;
|
|
1738
|
+
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'index.html'), `${html.trimEnd()}\n`, 'utf8');
|
|
1739
|
+
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'styles.css'), `${css.trimEnd()}\n`, 'utf8');
|
|
1740
|
+
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'scripts.js'), `${js.trimEnd()}\n`, 'utf8');
|
|
1741
|
+
return appName;
|
|
1742
|
+
}
|
|
1649
1743
|
ensureExecutionContext(context = {}) {
|
|
1650
1744
|
const existingId = String(context.contextId || context.traceId || '').trim();
|
|
1651
1745
|
const contextId = existingId || `vig-${Date.now()}-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
@@ -1698,7 +1792,7 @@ class APIClient {
|
|
|
1698
1792
|
});
|
|
1699
1793
|
if (!response.ok) {
|
|
1700
1794
|
const errorText = await response.text().catch(() => '');
|
|
1701
|
-
throw new Error(`MCP context update ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
1795
|
+
throw new Error(`MCP context update ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
1702
1796
|
}
|
|
1703
1797
|
return {
|
|
1704
1798
|
...executionContext,
|
|
@@ -1729,7 +1823,7 @@ class APIClient {
|
|
|
1729
1823
|
});
|
|
1730
1824
|
if (!createResponse.ok) {
|
|
1731
1825
|
const errorText = await createResponse.text().catch(() => '');
|
|
1732
|
-
throw new Error(`MCP context create ${createResponse.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
1826
|
+
throw new Error(`MCP context create ${createResponse.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
1733
1827
|
}
|
|
1734
1828
|
const payload = await createResponse.json();
|
|
1735
1829
|
const mcpContextId = String(payload.contextId || '').trim();
|
|
@@ -1819,14 +1913,6 @@ class APIClient {
|
|
|
1819
1913
|
* Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
|
|
1820
1914
|
*/
|
|
1821
1915
|
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
1916
|
const MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
1831
1917
|
const MAX_FILE_BYTES = 200 * 1024;
|
|
1832
1918
|
const BINARY_EXTENSIONS = new Set([
|
|
@@ -2692,7 +2778,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2692
2778
|
const rescueEligibleSaaS = preferLocalV3
|
|
2693
2779
|
&& /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(message);
|
|
2694
2780
|
const timeoutMs = rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
|
|
2695
|
-
const softTimeoutMs = executionContext.agentSoftTimeoutMs || DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS;
|
|
2696
2781
|
const maxAttempts = preferLocalV3 ? 2 : 1;
|
|
2697
2782
|
let lastErrors = [];
|
|
2698
2783
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -2727,13 +2812,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2727
2812
|
for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
|
|
2728
2813
|
const controller = new AbortController();
|
|
2729
2814
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2730
|
-
const softTimeoutId = softTimeoutMs > 0 ? setTimeout(() => controller.abort(), softTimeoutMs) : null;
|
|
2731
2815
|
try {
|
|
2732
2816
|
const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
|
|
2733
|
-
clearTimeout(timeoutId);
|
|
2734
2817
|
if (!response.ok) {
|
|
2735
2818
|
const errorText = await response.text().catch(() => '');
|
|
2736
|
-
throw new Error(`V3 agent ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
2819
|
+
throw new Error(`V3 agent ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
2737
2820
|
}
|
|
2738
2821
|
const data = await this.collectV3AgentStream(response, requestExecutionContext);
|
|
2739
2822
|
// Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
|
|
@@ -2761,7 +2844,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2761
2844
|
};
|
|
2762
2845
|
const continueController = new AbortController();
|
|
2763
2846
|
const continueTimeoutId = setTimeout(() => continueController.abort(), timeoutMs);
|
|
2764
|
-
const continueSoftTimeoutId = softTimeoutMs > 0 ? setTimeout(() => continueController.abort(), softTimeoutMs) : null;
|
|
2765
2847
|
try {
|
|
2766
2848
|
const continueHeaders = await this.getV3AgentHeaders();
|
|
2767
2849
|
const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
@@ -2770,7 +2852,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2770
2852
|
body: JSON.stringify(continueBody),
|
|
2771
2853
|
signal: continueController.signal,
|
|
2772
2854
|
});
|
|
2773
|
-
clearTimeout(continueTimeoutId);
|
|
2774
2855
|
if (!continueResponse.ok) {
|
|
2775
2856
|
break; // Fall through to normal completion with partial data
|
|
2776
2857
|
}
|
|
@@ -2781,9 +2862,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2781
2862
|
}
|
|
2782
2863
|
finally {
|
|
2783
2864
|
clearTimeout(continueTimeoutId);
|
|
2784
|
-
if (continueSoftTimeoutId) {
|
|
2785
|
-
clearTimeout(continueSoftTimeoutId);
|
|
2786
|
-
}
|
|
2787
2865
|
}
|
|
2788
2866
|
}
|
|
2789
2867
|
// Use the final continuation data for workspace recovery
|
|
@@ -2838,9 +2916,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2838
2916
|
}
|
|
2839
2917
|
finally {
|
|
2840
2918
|
clearTimeout(timeoutId);
|
|
2841
|
-
if (softTimeoutId) {
|
|
2842
|
-
clearTimeout(softTimeoutId);
|
|
2843
|
-
}
|
|
2844
2919
|
}
|
|
2845
2920
|
}
|
|
2846
2921
|
lastErrors = errors;
|
|
@@ -2862,6 +2937,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2862
2937
|
this.config.clearAuth();
|
|
2863
2938
|
throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
|
|
2864
2939
|
}
|
|
2940
|
+
if (preferLocalV3
|
|
2941
|
+
&& !this.hasAgentWorkspaceOutput(executionContext)
|
|
2942
|
+
&& /(saas|dashboard|analytics|billing|team management|activity feed)/i.test(message)) {
|
|
2943
|
+
const appName = this.materializeEmergencySaaSWorkspace(message, executionContext);
|
|
2944
|
+
if (appName) {
|
|
2945
|
+
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
|
|
2946
|
+
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
2947
|
+
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
2948
|
+
return {
|
|
2949
|
+
content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
|
|
2950
|
+
taskId: null,
|
|
2951
|
+
contextId: executionContext.contextId || null,
|
|
2952
|
+
backendUrl: 'local-emergency-scaffold',
|
|
2953
|
+
partial: true,
|
|
2954
|
+
metadata: { source: 'v3-agent-emergency-scaffold', mode: 'agent', previewGate, emergencyScaffold: true },
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2865
2958
|
throw new Error(errors.join(' | '));
|
|
2866
2959
|
}
|
|
2867
2960
|
formatOperatorResponse(data = {}) {
|
|
@@ -2922,7 +3015,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2922
3015
|
workspace: { path: workspacePath },
|
|
2923
3016
|
workspace_path: workspacePath,
|
|
2924
3017
|
workspace_summary: workspaceSummary,
|
|
2925
|
-
model: this.resolveModelId(executionContext.model || 'code
|
|
3018
|
+
model: this.resolveModelId(executionContext.model || 'code'),
|
|
2926
3019
|
history: executionContext.history || [],
|
|
2927
3020
|
executionSurface: executionContext.executionSurface || 'cli',
|
|
2928
3021
|
clientSurface: executionContext.clientSurface || 'cli',
|
|
@@ -2933,7 +3026,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2933
3026
|
rawPrompt: executionContext.rawPrompt || null,
|
|
2934
3027
|
requestStartedAt: executionContext.requestStartedAt,
|
|
2935
3028
|
},
|
|
2936
|
-
workflow_type: executionContext.workflowType || '
|
|
3029
|
+
workflow_type: executionContext.workflowType || 'full',
|
|
2937
3030
|
options: {
|
|
2938
3031
|
stream: true,
|
|
2939
3032
|
save_to_vigflow: executionContext.savePlanToVigFlow === true,
|
|
@@ -2943,7 +3036,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2943
3036
|
});
|
|
2944
3037
|
if (!response.ok) {
|
|
2945
3038
|
const errorText = await response.text().catch(() => '');
|
|
2946
|
-
throw new Error(`Operator stream ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
3039
|
+
throw new Error(`Operator stream ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
2947
3040
|
}
|
|
2948
3041
|
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
2949
3042
|
const fallbackData = await response.json();
|
|
@@ -3097,7 +3190,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3097
3190
|
if (!this.shouldSkipCloudRoutes(resolvedModel)) {
|
|
3098
3191
|
try {
|
|
3099
3192
|
this.logger.debug(`Direct Vigthoria Models API: ${resolvedModel}`);
|
|
3100
|
-
const token = this.
|
|
3193
|
+
const token = this.config.get('authToken');
|
|
3101
3194
|
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
3102
3195
|
model: resolvedModel,
|
|
3103
3196
|
messages,
|
|
@@ -3211,18 +3304,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3211
3304
|
}
|
|
3212
3305
|
getFallbackModelId(resolvedModel) {
|
|
3213
3306
|
const cloudModels = new Set([
|
|
3307
|
+
'deepseek-v3.1:671b-cloud',
|
|
3214
3308
|
'moonshotai/kimi-k2.5',
|
|
3215
3309
|
'vigthoria-cloud-pro',
|
|
3216
3310
|
'vigthoria-cloud-k2',
|
|
3217
3311
|
'vigthoria-cloud-ultra',
|
|
3218
3312
|
]);
|
|
3219
3313
|
if (cloudModels.has(resolvedModel)) {
|
|
3220
|
-
return 'vigthoria-v3-code-
|
|
3314
|
+
return 'vigthoria-v3-code-30b';
|
|
3221
3315
|
}
|
|
3222
3316
|
return null;
|
|
3223
3317
|
}
|
|
3224
3318
|
isCloudModelId(resolvedModel) {
|
|
3225
|
-
return resolvedModel === '
|
|
3319
|
+
return resolvedModel === 'deepseek-v3.1:671b-cloud'
|
|
3320
|
+
|| resolvedModel === 'moonshotai/kimi-k2.5'
|
|
3226
3321
|
|| resolvedModel === 'vigthoria-cloud-pro'
|
|
3227
3322
|
|| resolvedModel === 'vigthoria-cloud-k2'
|
|
3228
3323
|
|| resolvedModel === 'vigthoria-cloud-ultra';
|
|
@@ -3232,25 +3327,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3232
3327
|
}
|
|
3233
3328
|
resolvePermittedModelId(shortName) {
|
|
3234
3329
|
const resolvedModel = this.resolveModelId(shortName);
|
|
3235
|
-
const requested = String(shortName || '').toLowerCase();
|
|
3236
3330
|
if (this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()) {
|
|
3237
3331
|
const fallbackModel = this.getSelfHostedFallbackModelId(resolvedModel, shortName);
|
|
3238
3332
|
this.logger.debug(`Blocked unauthorized cloud model ${shortName}; using fallback ${fallbackModel}`);
|
|
3239
3333
|
return fallbackModel;
|
|
3240
3334
|
}
|
|
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
3335
|
return resolvedModel;
|
|
3255
3336
|
}
|
|
3256
3337
|
shouldSimulateCloudFailure() {
|
|
@@ -3269,29 +3350,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3269
3350
|
isSelfHostedPreferredModel(resolvedModel, requestedModel) {
|
|
3270
3351
|
const normalizedRequested = String(requestedModel || '').toLowerCase();
|
|
3271
3352
|
const selfHostedModels = new Set([
|
|
3272
|
-
'vigthoria-v3-code-
|
|
3273
|
-
'vigthoria-v3-code-
|
|
3353
|
+
'vigthoria-v3-code-30b',
|
|
3354
|
+
'vigthoria-v3-code-30b:latest',
|
|
3274
3355
|
'qwen3-coder:latest',
|
|
3275
|
-
'
|
|
3356
|
+
'vigthoria-v2-code-8b',
|
|
3276
3357
|
]);
|
|
3277
3358
|
return selfHostedModels.has(resolvedModel)
|
|
3278
3359
|
|| normalizedRequested === 'agent'
|
|
3279
3360
|
|| normalizedRequested === 'code'
|
|
3280
|
-
|| normalizedRequested === 'code-
|
|
3281
|
-
|| normalizedRequested === 'code-35b'
|
|
3282
|
-
|| normalizedRequested === 'code-9b'
|
|
3361
|
+
|| normalizedRequested === 'code-30b'
|
|
3283
3362
|
|| normalizedRequested === 'pro';
|
|
3284
3363
|
}
|
|
3285
3364
|
getSelfHostedFallbackModelId(resolvedModel, requestedModel) {
|
|
3286
3365
|
if (this.isSelfHostedPreferredModel(resolvedModel, requestedModel)) {
|
|
3287
|
-
return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-
|
|
3366
|
+
return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-30b' : resolvedModel;
|
|
3288
3367
|
}
|
|
3289
|
-
return 'vigthoria-v3-code-
|
|
3368
|
+
return 'vigthoria-v3-code-30b';
|
|
3290
3369
|
}
|
|
3291
3370
|
// Streaming chat
|
|
3292
3371
|
async *chatStream(messages, model) {
|
|
3293
3372
|
const wsUrl = this.config.get('wsUrl');
|
|
3294
|
-
const token = this.
|
|
3373
|
+
const token = this.config.get('authToken');
|
|
3295
3374
|
return new Promise((resolve, reject) => {
|
|
3296
3375
|
const ws = new ws_1.default(`${wsUrl}/chat`, {
|
|
3297
3376
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -3320,7 +3399,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3320
3399
|
// Non-streaming alternative with callback
|
|
3321
3400
|
async chatWithCallback(messages, model, onChunk, onDone, onError) {
|
|
3322
3401
|
const wsUrl = this.config.get('wsUrl');
|
|
3323
|
-
const token = this.
|
|
3402
|
+
const token = this.config.get('authToken');
|
|
3324
3403
|
return new Promise((resolve, reject) => {
|
|
3325
3404
|
const ws = new ws_1.default(`${wsUrl}/chat`, {
|
|
3326
3405
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -3369,7 +3448,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3369
3448
|
// (/v1/chat/completions on api.vigthoria.io) which is the only
|
|
3370
3449
|
// backend that reliably accepts our auth token.
|
|
3371
3450
|
async chatComplete(systemPrompt, userPrompt, model, maxTokens) {
|
|
3372
|
-
const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-
|
|
3451
|
+
const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-30b';
|
|
3373
3452
|
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
3374
3453
|
model: resolvedModel,
|
|
3375
3454
|
messages: [
|
|
@@ -3449,21 +3528,76 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3449
3528
|
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
3450
3529
|
*/
|
|
3451
3530
|
ensureBalancedBraces(code) {
|
|
3452
|
-
|
|
3453
|
-
|
|
3531
|
+
// Count braces/parens/brackets outside strings and comments
|
|
3532
|
+
let braces = 0, parens = 0, brackets = 0;
|
|
3533
|
+
let inStr = null;
|
|
3534
|
+
let inLine = false, inBlock = false;
|
|
3535
|
+
for (let i = 0; i < code.length; i++) {
|
|
3536
|
+
const ch = code[i], nx = code[i + 1] || '';
|
|
3537
|
+
if (inLine) {
|
|
3538
|
+
if (ch === '\n')
|
|
3539
|
+
inLine = false;
|
|
3540
|
+
continue;
|
|
3541
|
+
}
|
|
3542
|
+
if (inBlock) {
|
|
3543
|
+
if (ch === '*' && nx === '/') {
|
|
3544
|
+
inBlock = false;
|
|
3545
|
+
i++;
|
|
3546
|
+
}
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
if (inStr) {
|
|
3550
|
+
if (ch === inStr && code[i - 1] !== '\\')
|
|
3551
|
+
inStr = null;
|
|
3552
|
+
continue;
|
|
3553
|
+
}
|
|
3554
|
+
if (ch === '/' && nx === '/') {
|
|
3555
|
+
inLine = true;
|
|
3556
|
+
continue;
|
|
3557
|
+
}
|
|
3558
|
+
if (ch === '/' && nx === '*') {
|
|
3559
|
+
inBlock = true;
|
|
3560
|
+
continue;
|
|
3561
|
+
}
|
|
3562
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
3563
|
+
inStr = ch;
|
|
3564
|
+
continue;
|
|
3565
|
+
}
|
|
3454
3566
|
if (ch === '{')
|
|
3455
|
-
|
|
3567
|
+
braces++;
|
|
3456
3568
|
else if (ch === '}')
|
|
3457
|
-
|
|
3569
|
+
braces--;
|
|
3570
|
+
else if (ch === '(')
|
|
3571
|
+
parens++;
|
|
3572
|
+
else if (ch === ')')
|
|
3573
|
+
parens--;
|
|
3574
|
+
else if (ch === '[')
|
|
3575
|
+
brackets++;
|
|
3576
|
+
else if (ch === ']')
|
|
3577
|
+
brackets--;
|
|
3578
|
+
}
|
|
3579
|
+
let result = code.trimEnd();
|
|
3580
|
+
for (let i = 0; i < braces; i++)
|
|
3581
|
+
result += '\n}';
|
|
3582
|
+
for (let i = 0; i < parens; i++)
|
|
3583
|
+
result += ')';
|
|
3584
|
+
for (let i = 0; i < brackets; i++)
|
|
3585
|
+
result += ']';
|
|
3586
|
+
return braces > 0 || parens > 0 || brackets > 0 ? result : code;
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* Quick JS/TS syntax validation using Node's built-in parser.
|
|
3590
|
+
* Returns true if the code parses without errors.
|
|
3591
|
+
*/
|
|
3592
|
+
validateJsSyntax(code) {
|
|
3593
|
+
try {
|
|
3594
|
+
// Use Function constructor to check syntax without executing
|
|
3595
|
+
new Function(code);
|
|
3596
|
+
return true;
|
|
3458
3597
|
}
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
for (let i = 0; i < depth; i++) {
|
|
3462
|
-
result += '\n}';
|
|
3463
|
-
}
|
|
3464
|
-
code = result;
|
|
3598
|
+
catch {
|
|
3599
|
+
return false;
|
|
3465
3600
|
}
|
|
3466
|
-
return code;
|
|
3467
3601
|
}
|
|
3468
3602
|
/**
|
|
3469
3603
|
* Extract the first complete function/class from code.
|
|
@@ -3571,7 +3705,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3571
3705
|
}
|
|
3572
3706
|
}
|
|
3573
3707
|
async explainCode(code, language) {
|
|
3574
|
-
const sysPrompt =
|
|
3708
|
+
const sysPrompt = [
|
|
3709
|
+
`You are a code explainer. Explain the following ${language} code clearly and concisely.`,
|
|
3710
|
+
'Focus on what it does, how it works, and any notable patterns or potential issues.',
|
|
3711
|
+
'Format your response as clean Markdown:',
|
|
3712
|
+
'- Use ## headers for major sections (e.g. ## Overview, ## How It Works, ## Key Details).',
|
|
3713
|
+
'- Use bullet points (- or *) for all lists. Do NOT use numbered lists.',
|
|
3714
|
+
'- Wrap code references in backticks.',
|
|
3715
|
+
'- Keep paragraphs short (2-3 sentences max).',
|
|
3716
|
+
'- Do NOT use raw HTML or excessive blank lines.',
|
|
3717
|
+
'- Do NOT nest numbered lists inside sections.',
|
|
3718
|
+
].join('\n');
|
|
3575
3719
|
return this.chatComplete(sysPrompt, code);
|
|
3576
3720
|
}
|
|
3577
3721
|
async reviewCode(code, language) {
|
|
@@ -3583,9 +3727,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3583
3727
|
'Rules:',
|
|
3584
3728
|
'- Return concrete, line-specific issues with severity.',
|
|
3585
3729
|
'- Every issue MUST reference a line number.',
|
|
3586
|
-
'-
|
|
3730
|
+
'- Report each distinct bug ONCE. Do NOT report the same bug multiple times with different wording.',
|
|
3731
|
+
'- For trivial/short code (< 10 lines), report ONLY actual bugs. Do NOT pad with style, robustness, or best-practice suggestions.',
|
|
3732
|
+
'- 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
3733
|
'- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
|
|
3588
|
-
'-
|
|
3734
|
+
'- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
|
|
3589
3735
|
'- Return ONLY the JSON object, no markdown fences or extra text.',
|
|
3590
3736
|
].join('\n');
|
|
3591
3737
|
let raw = {};
|
|
@@ -3600,25 +3746,40 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3600
3746
|
const score = typeof raw.score === 'number' ? raw.score : 0;
|
|
3601
3747
|
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
|
3602
3748
|
const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
3603
|
-
//
|
|
3604
|
-
//
|
|
3605
|
-
|
|
3749
|
+
// Merge client-side heuristics, but with tight dedup to avoid
|
|
3750
|
+
// redundant over-reporting when the model already found the bug.
|
|
3751
|
+
const modelFoundError = issues.some(i => i.severity === 'error');
|
|
3606
3752
|
const heuristic = this.heuristicCodeIssues(code, language);
|
|
3607
3753
|
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
|
-
}
|
|
3754
|
+
// If the model already found a real error, skip non-error heuristics
|
|
3755
|
+
// entirely — they're just padding (style, robustness, etc.)
|
|
3756
|
+
if (modelFoundError && h.severity !== 'error')
|
|
3616
3757
|
continue;
|
|
3617
|
-
|
|
3618
|
-
//
|
|
3619
|
-
//
|
|
3620
|
-
const
|
|
3621
|
-
|
|
3758
|
+
// Semantic duplicate check: same line + (similar type OR overlapping
|
|
3759
|
+
// keywords in the message). This catches cases where the model
|
|
3760
|
+
// and heuristic describe the same bug with different wording.
|
|
3761
|
+
const hWords = new Set(h.message.toLowerCase().split(/\W+/).filter(w => w.length > 3));
|
|
3762
|
+
const hTypeNorm = h.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
3763
|
+
const isSemanticallyDuplicate = issues.some((existing) => {
|
|
3764
|
+
if (existing.line !== h.line)
|
|
3765
|
+
return false;
|
|
3766
|
+
// Normalize types: "logic-error", "logic_error", "logic" all match
|
|
3767
|
+
const eTypeNorm = existing.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
3768
|
+
if (eTypeNorm === hTypeNorm || eTypeNorm.startsWith(hTypeNorm) || hTypeNorm.startsWith(eTypeNorm))
|
|
3769
|
+
return true;
|
|
3770
|
+
// Both errors on same line about the same category of problem
|
|
3771
|
+
if (existing.severity === 'error' && h.severity === 'error')
|
|
3772
|
+
return true;
|
|
3773
|
+
// Check keyword overlap — if ≥2 significant words match, it's the same finding
|
|
3774
|
+
const eWords = existing.message.toLowerCase().split(/\W+/).filter(w => w.length > 3);
|
|
3775
|
+
let overlap = 0;
|
|
3776
|
+
for (const w of eWords) {
|
|
3777
|
+
if (hWords.has(w))
|
|
3778
|
+
overlap++;
|
|
3779
|
+
}
|
|
3780
|
+
return overlap >= 2;
|
|
3781
|
+
});
|
|
3782
|
+
if (!isSemanticallyDuplicate) {
|
|
3622
3783
|
issues.push(h);
|
|
3623
3784
|
}
|
|
3624
3785
|
}
|
|
@@ -3767,9 +3928,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3767
3928
|
const sysPrompt = [
|
|
3768
3929
|
`You are a ${language} code fixer. Fix the code for: ${fixType}.`,
|
|
3769
3930
|
'Return a JSON object with:',
|
|
3770
|
-
' "fixed": the corrected code as a string,',
|
|
3931
|
+
' "fixed": the COMPLETE corrected source code as a string (not a snippet — the full file),',
|
|
3771
3932
|
' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
|
|
3772
3933
|
'Rules:',
|
|
3934
|
+
'- The "fixed" field MUST contain the entire corrected source code with ALL lines, including unchanged lines.',
|
|
3935
|
+
'- The "fixed" code MUST have balanced braces, parentheses, and brackets.',
|
|
3773
3936
|
'- Fix ONLY the issues related to the fix type.',
|
|
3774
3937
|
'- Do not add comments, do not restructure beyond the minimal fix.',
|
|
3775
3938
|
'- Return ONLY the JSON object, no markdown fences.',
|
|
@@ -3809,6 +3972,26 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3809
3972
|
if (fixType === 'syntax' && fixed !== code) {
|
|
3810
3973
|
fixed = this.repairBracketBalance(code, fixed);
|
|
3811
3974
|
}
|
|
3975
|
+
// Final bracket-balance guarantee — ensure the emitted code has
|
|
3976
|
+
// balanced braces/parens/brackets regardless of what the model returned.
|
|
3977
|
+
fixed = this.ensureBalancedBraces(fixed);
|
|
3978
|
+
// For JS/TS syntax fixes, validate the output actually parses.
|
|
3979
|
+
// If it doesn't, attempt a more aggressive bracket repair.
|
|
3980
|
+
if ((fixType === 'syntax' || fixType === 'bugs') && fixed !== code) {
|
|
3981
|
+
const lang = language.toLowerCase();
|
|
3982
|
+
if (['javascript', 'js', 'typescript', 'ts'].includes(lang)) {
|
|
3983
|
+
if (!this.validateJsSyntax(fixed)) {
|
|
3984
|
+
// Try once more: strip any remaining injected comments and re-balance
|
|
3985
|
+
let repaired = this.stripInjectedComments(code, fixed, language);
|
|
3986
|
+
repaired = this.ensureBalancedBraces(repaired);
|
|
3987
|
+
if (this.validateJsSyntax(repaired)) {
|
|
3988
|
+
fixed = repaired;
|
|
3989
|
+
}
|
|
3990
|
+
// If still invalid, return the best-effort fix — better than
|
|
3991
|
+
// silently reverting to the original broken code.
|
|
3992
|
+
}
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3812
3995
|
// If there are still no changes but the fixed code differs, compute
|
|
3813
3996
|
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
3814
3997
|
// every subsequent line to appear as changed.
|
|
@@ -4110,116 +4293,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4110
4293
|
}
|
|
4111
4294
|
// Model resolution - maps Vigthoria model names to internal IDs
|
|
4112
4295
|
// 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
4296
|
resolveModelId(shortName) {
|
|
4204
4297
|
const modelMap = {
|
|
4205
4298
|
// ═══════════════════════════════════════════════════════════════
|
|
4206
4299
|
// VIGTHORIA LOCAL - Self-hosted models
|
|
4207
4300
|
// ═══════════════════════════════════════════════════════════════
|
|
4208
|
-
'fast': 'vigthoria-
|
|
4209
|
-
'mini': 'vigthoria-
|
|
4210
|
-
'balanced': '
|
|
4211
|
-
'
|
|
4212
|
-
|
|
4213
|
-
//
|
|
4214
|
-
'code': 'vigthoria-v3-code-
|
|
4215
|
-
'code-
|
|
4216
|
-
'
|
|
4217
|
-
'
|
|
4218
|
-
'code
|
|
4219
|
-
'
|
|
4220
|
-
'agent': 'vigthoria-v3-code-35b',
|
|
4221
|
-
'vigthoria-code': 'vigthoria-v3-code-35b',
|
|
4222
|
-
'vigthoria-agent': 'vigthoria-v3-code-35b',
|
|
4301
|
+
'fast': 'vigthoria-fast-1.7b',
|
|
4302
|
+
'mini': 'vigthoria-mini-0.6b',
|
|
4303
|
+
'balanced': 'vigthoria-balanced-4b',
|
|
4304
|
+
'creative': 'vigthoria-creative-9b-v4',
|
|
4305
|
+
// Code Models - 30B is the default powerhouse
|
|
4306
|
+
'code': 'vigthoria-v3-code-30b', // Internal: self-hosted 30B on Blackwell
|
|
4307
|
+
'code-30b': 'vigthoria-v3-code-30b',
|
|
4308
|
+
'code-8b': 'vigthoria-v2-code-8b',
|
|
4309
|
+
'pro': 'vigthoria-v3-code-30b',
|
|
4310
|
+
'agent': 'vigthoria-v3-code-30b',
|
|
4311
|
+
'vigthoria-code': 'vigthoria-v3-code-30b',
|
|
4312
|
+
'vigthoria-agent': 'vigthoria-v3-code-30b',
|
|
4223
4313
|
// ═══════════════════════════════════════════════════════════════
|
|
4224
4314
|
// VIGTHORIA CLOUD - Premium cloud models (internal routing)
|
|
4225
4315
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -4227,17 +4317,18 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4227
4317
|
'cloud-reason': 'vigthoria-cloud-k2',
|
|
4228
4318
|
'ultra': 'vigthoria-cloud-ultra',
|
|
4229
4319
|
};
|
|
4320
|
+
// If already a full model ID, return as-is
|
|
4230
4321
|
if (shortName.includes('vigthoria') || shortName.includes('/') || shortName.includes(':')) {
|
|
4231
4322
|
if (modelMap[shortName]) {
|
|
4232
4323
|
return modelMap[shortName];
|
|
4233
4324
|
}
|
|
4234
4325
|
return shortName;
|
|
4235
4326
|
}
|
|
4236
|
-
return modelMap[shortName] || 'vigthoria-v3-code-
|
|
4327
|
+
return modelMap[shortName] || 'vigthoria-v3-code-30b';
|
|
4237
4328
|
}
|
|
4238
4329
|
async getCoderHealth() {
|
|
4239
4330
|
try {
|
|
4240
|
-
const response = await this.client.get('/api/health', { timeout:
|
|
4331
|
+
const response = await this.client.get('/api/health', { timeout: 5000 });
|
|
4241
4332
|
const ok = response.data?.status === 'ok' || response.data?.healthy === true;
|
|
4242
4333
|
return {
|
|
4243
4334
|
name: 'Coder API',
|
|
@@ -4259,8 +4350,8 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4259
4350
|
const modelsApiUrl = this.config.get('modelsApiUrl');
|
|
4260
4351
|
try {
|
|
4261
4352
|
const [healthResponse, modelsResponse] = await Promise.all([
|
|
4262
|
-
this.modelRouterClient.get('/health', { timeout:
|
|
4263
|
-
this.modelRouterClient.get('/v1/models', { timeout:
|
|
4353
|
+
this.modelRouterClient.get('/health', { timeout: 5000 }),
|
|
4354
|
+
this.modelRouterClient.get('/v1/models', { timeout: 5000 }),
|
|
4264
4355
|
]);
|
|
4265
4356
|
const healthOk = healthResponse.data?.status === 'healthy'
|
|
4266
4357
|
|| healthResponse.data?.status === 'ok'
|
|
@@ -4291,7 +4382,7 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4291
4382
|
return null;
|
|
4292
4383
|
}
|
|
4293
4384
|
try {
|
|
4294
|
-
const response = await this.selfHostedModelRouterClient.get('/health', { timeout:
|
|
4385
|
+
const response = await this.selfHostedModelRouterClient.get('/health', { timeout: 5000 });
|
|
4295
4386
|
const ok = response.data?.status === 'healthy'
|
|
4296
4387
|
|| response.data?.status === 'ok'
|
|
4297
4388
|
|| response.data?.healthy === true;
|
|
@@ -4311,29 +4402,6 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4311
4402
|
};
|
|
4312
4403
|
}
|
|
4313
4404
|
}
|
|
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
4405
|
async getV3AgentHealth() {
|
|
4338
4406
|
const baseUrl = this.getV3AgentBaseUrls()[0];
|
|
4339
4407
|
// Try multiple health endpoint patterns — the V3 backend may expose
|
|
@@ -4347,7 +4415,7 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4347
4415
|
for (const endpoint of candidates) {
|
|
4348
4416
|
try {
|
|
4349
4417
|
const controller = new AbortController();
|
|
4350
|
-
const timer = setTimeout(() => controller.abort(),
|
|
4418
|
+
const timer = setTimeout(() => controller.abort(), 3000);
|
|
4351
4419
|
const response = await fetch(endpoint, {
|
|
4352
4420
|
method: 'GET',
|
|
4353
4421
|
headers,
|
|
@@ -4395,7 +4463,7 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4395
4463
|
const runUrl = this.getV3AgentRunUrl(baseUrl);
|
|
4396
4464
|
try {
|
|
4397
4465
|
const controller = new AbortController();
|
|
4398
|
-
const timer = setTimeout(() => controller.abort(),
|
|
4466
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
4399
4467
|
const probe = await fetch(runUrl, { method: 'OPTIONS', headers, signal: controller.signal });
|
|
4400
4468
|
clearTimeout(timer);
|
|
4401
4469
|
if (probe.ok || probe.status === 204 || probe.status === 405) {
|
|
@@ -4518,6 +4586,20 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4518
4586
|
};
|
|
4519
4587
|
}
|
|
4520
4588
|
}
|
|
4589
|
+
async runSelfHealingCycle(_originalPrompt, _workspacePath, _context = {}) {
|
|
4590
|
+
return {
|
|
4591
|
+
healingAttempted: false,
|
|
4592
|
+
passed: true,
|
|
4593
|
+
tool: 'disabled',
|
|
4594
|
+
};
|
|
4595
|
+
}
|
|
4596
|
+
async attemptV3ServiceRecovery(reason = '', _options = {}) {
|
|
4597
|
+
const safeReason = sanitizeUserFacingErrorText(reason || 'unknown failure');
|
|
4598
|
+
return {
|
|
4599
|
+
recovered: false,
|
|
4600
|
+
message: safeReason ? `Recovery unavailable: ${safeReason}` : 'Recovery unavailable',
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4521
4603
|
async getDevtoolsBridgeStatus() {
|
|
4522
4604
|
const host = process.env.VIGTHORIA_DEVTOOLS_BRIDGE_HOST || '127.0.0.1';
|
|
4523
4605
|
const port = Number.parseInt(process.env.VIGTHORIA_DEVTOOLS_BRIDGE_PORT || '4016', 10);
|
|
@@ -4552,11 +4634,18 @@ Apply the minimum change needed to make the validator pass.`;
|
|
|
4552
4634
|
});
|
|
4553
4635
|
}
|
|
4554
4636
|
async getCapabilityTruthStatus(context = {}) {
|
|
4637
|
+
// Wrap each probe with its own 6 s timeout so they always resolve
|
|
4638
|
+
// before the outer 8 s race in auth.ts, producing real error messages
|
|
4639
|
+
// (ECONNREFUSED, 404, etc.) instead of the generic "Timed out (8s)".
|
|
4640
|
+
const withTimeout = (p, name) => Promise.race([
|
|
4641
|
+
p,
|
|
4642
|
+
new Promise(resolve => setTimeout(() => resolve({ name, endpoint: '', ok: false, error: 'Service not reachable (6 s timeout)' }), 6000)),
|
|
4643
|
+
]);
|
|
4555
4644
|
const [v3Agent, hyperLoop, repoMemory, devtoolsBridge] = await Promise.all([
|
|
4556
|
-
this.getV3AgentHealth(),
|
|
4557
|
-
this.getHyperLoopHealth(),
|
|
4558
|
-
this.getRepoMemoryHealth(context),
|
|
4559
|
-
this.getDevtoolsBridgeStatus(),
|
|
4645
|
+
withTimeout(this.getV3AgentHealth(), 'V3 Agent'),
|
|
4646
|
+
withTimeout(this.getHyperLoopHealth(), 'Hyper Loop'),
|
|
4647
|
+
withTimeout(this.getRepoMemoryHealth(context), 'Repo Memory'),
|
|
4648
|
+
withTimeout(this.getDevtoolsBridgeStatus(), 'DevTools Bridge'),
|
|
4560
4649
|
]);
|
|
4561
4650
|
return {
|
|
4562
4651
|
overallOk: v3Agent.ok && hyperLoop.ok && repoMemory.ok,
|