vigthoria-cli 1.8.15 → 1.9.2
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 +2 -6
- package/dist/commands/auth.d.ts +49 -21
- package/dist/commands/auth.js +385 -343
- package/dist/commands/chat.d.ts +10 -2
- package/dist/commands/chat.js +328 -93
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +40 -20
- package/dist/commands/index.d.ts +12 -0
- package/dist/commands/index.js +182 -0
- package/dist/commands/legion.d.ts +39 -0
- package/dist/commands/legion.js +999 -71
- package/dist/index.d.ts +3 -1
- package/dist/index.js +506 -28
- package/dist/utils/api.d.ts +74 -18
- package/dist/utils/api.js +701 -805
- package/dist/utils/config.js +9 -10
- package/dist/utils/context-ranker.d.ts +24 -0
- package/dist/utils/context-ranker.js +147 -0
- package/dist/utils/post-write-validator.d.ts +25 -0
- package/dist/utils/post-write-validator.js +138 -0
- package/dist/utils/session.d.ts +19 -0
- package/dist/utils/session.js +91 -6
- package/dist/utils/task-display.d.ts +31 -0
- package/dist/utils/task-display.js +115 -0
- package/dist/utils/tools.d.ts +15 -0
- package/dist/utils/tools.js +341 -58
- package/dist/utils/workspace-cache.d.ts +31 -0
- package/dist/utils/workspace-cache.js +96 -0
- package/package.json +7 -3
package/dist/utils/api.js
CHANGED
|
@@ -8,11 +8,18 @@ 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;
|
|
11
14
|
exports.classifyError = classifyError;
|
|
12
15
|
exports.formatCLIError = formatCLIError;
|
|
13
16
|
exports.sanitizeUserFacingErrorText = sanitizeUserFacingErrorText;
|
|
14
17
|
exports.isServerRuntime = isServerRuntime;
|
|
15
18
|
exports.describeUpstreamStatus = describeUpstreamStatus;
|
|
19
|
+
exports.validateJwtExpiry = validateJwtExpiry;
|
|
20
|
+
exports.validateJwt = validateJwt;
|
|
21
|
+
exports.refreshJwtIfNeeded = refreshJwtIfNeeded;
|
|
22
|
+
exports.createApiClient = createApiClient;
|
|
16
23
|
const axios_1 = __importDefault(require("axios"));
|
|
17
24
|
const crypto_1 = require("crypto");
|
|
18
25
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -20,26 +27,202 @@ const https_1 = __importDefault(require("https"));
|
|
|
20
27
|
const net_1 = __importDefault(require("net"));
|
|
21
28
|
const path_1 = __importDefault(require("path"));
|
|
22
29
|
const ws_1 = __importDefault(require("ws"));
|
|
23
|
-
const
|
|
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");
|
|
24
34
|
class CLIError extends Error {
|
|
25
35
|
category;
|
|
26
36
|
statusCode;
|
|
27
37
|
endpoint;
|
|
38
|
+
code;
|
|
39
|
+
details;
|
|
40
|
+
isCritical;
|
|
28
41
|
constructor(message, category, opts) {
|
|
29
42
|
super(message);
|
|
30
43
|
this.name = 'CLIError';
|
|
31
44
|
this.category = category;
|
|
32
45
|
this.statusCode = opts?.statusCode;
|
|
33
46
|
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);
|
|
34
50
|
if (opts?.cause)
|
|
35
51
|
this.cause = opts.cause;
|
|
36
52
|
}
|
|
37
53
|
}
|
|
38
54
|
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
|
+
}
|
|
39
214
|
/** Classify an axios or fetch error into a structured CLIError. */
|
|
40
215
|
function classifyError(error, fallbackCategory = 'network') {
|
|
41
216
|
if (error instanceof CLIError)
|
|
42
217
|
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
|
+
}
|
|
43
226
|
const axErr = error;
|
|
44
227
|
const status = axErr?.response?.status;
|
|
45
228
|
const endpoint = axErr?.config?.url || axErr?.config?.baseURL || '';
|
|
@@ -92,36 +275,7 @@ function formatCLIError(err) {
|
|
|
92
275
|
return `${tag} ${err.message}`;
|
|
93
276
|
}
|
|
94
277
|
}
|
|
95
|
-
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
96
|
-
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
97
|
-
if (!rawValue) {
|
|
98
|
-
// No total timeout by default for long-running SSE agent workflows.
|
|
99
|
-
return 0;
|
|
100
|
-
}
|
|
101
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
102
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
103
|
-
})();
|
|
104
|
-
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
105
|
-
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
|
|
106
|
-
if (!rawValue) {
|
|
107
|
-
// Keep stream open indefinitely unless user configures an idle limit.
|
|
108
|
-
return 0;
|
|
109
|
-
}
|
|
110
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
111
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
112
|
-
})();
|
|
113
|
-
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
114
|
-
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS;
|
|
115
|
-
if (!rawValue) {
|
|
116
|
-
// BMAD/operator flows can be long-running; do not cap by default.
|
|
117
|
-
return 0;
|
|
118
|
-
}
|
|
119
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
120
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
121
|
-
})();
|
|
122
278
|
// Sanitize an upstream error string before exposing it to the end user.
|
|
123
|
-
// Strips URLs, IPs:ports, absolute server paths, and bare hostnames so the
|
|
124
|
-
// CLI never reveals internal infrastructure to remote users.
|
|
125
279
|
function sanitizeUserFacingErrorText(input) {
|
|
126
280
|
if (!input)
|
|
127
281
|
return '';
|
|
@@ -131,19 +285,13 @@ function sanitizeUserFacingErrorText(input) {
|
|
|
131
285
|
out = out.replace(/\b(?:localhost|127\.0\.0\.1)(?::\d+)?\b/gi, '[redacted-host]');
|
|
132
286
|
out = out.replace(/\b[a-z0-9.-]+\.vigthoria\.io\b/gi, '[redacted-host]');
|
|
133
287
|
out = out.replace(/(?:[A-Za-z]:)?[\\/](?:var|opt|tmp|home|root|etc|usr)[\\/][^\s'"<>)]*/gi, '[redacted-path]');
|
|
134
|
-
// Windows drive-letter paths (e.g. C:\Users\Name\AppData\...).
|
|
135
288
|
out = out.replace(/[A-Za-z]:\\[^\s'"<>)]+/g, '[redacted-path]');
|
|
136
|
-
// UNC paths (\\server\share\...).
|
|
137
289
|
out = out.replace(/\\\\[^\s'"<>)]+/g, '[redacted-path]');
|
|
138
|
-
out = out.replace(/\{\s*"detail"\s*:\s*"[^"]*"\s*\}/g, '');
|
|
139
290
|
out = out.replace(/\s+/g, ' ').trim();
|
|
140
291
|
if (out.length > 160)
|
|
141
292
|
out = out.slice(0, 160) + '...';
|
|
142
293
|
return out;
|
|
143
294
|
}
|
|
144
|
-
// True only when this CLI process is running on the Vigthoria server itself.
|
|
145
|
-
// Local user installations must NEVER attempt internal loopback endpoints,
|
|
146
|
-
// because the resulting fetch errors include the URL we tried (leak vector).
|
|
147
295
|
function isServerRuntime() {
|
|
148
296
|
if (process.env.VIGTHORIA_RUN_MODE === 'server')
|
|
149
297
|
return true;
|
|
@@ -166,15 +314,144 @@ function describeUpstreamStatus(status) {
|
|
|
166
314
|
return 'Request was rejected by the service.';
|
|
167
315
|
return 'Unexpected response from service.';
|
|
168
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
|
+
}
|
|
347
|
+
}
|
|
348
|
+
async function refreshJwtIfNeeded(state) {
|
|
349
|
+
const runtimeClient = state;
|
|
350
|
+
try {
|
|
351
|
+
const currentToken = runtimeClient.getAccessToken?.() || runtimeClient.config?.get('authToken') || state.token;
|
|
352
|
+
if (!currentToken) {
|
|
353
|
+
state.token = null;
|
|
354
|
+
state.expiresAt = null;
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
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
|
+
}
|
|
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 });
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
426
|
+
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '1200000';
|
|
427
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
428
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1200000;
|
|
429
|
+
})();
|
|
430
|
+
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
431
|
+
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS || '90000';
|
|
432
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
433
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
434
|
+
})();
|
|
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
|
+
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
441
|
+
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '300000';
|
|
442
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
443
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
|
|
444
|
+
})();
|
|
169
445
|
class APIClient {
|
|
170
446
|
client;
|
|
171
447
|
modelRouterClient;
|
|
172
448
|
selfHostedModelRouterClient;
|
|
173
449
|
config;
|
|
450
|
+
token = null;
|
|
451
|
+
expiresAt = null;
|
|
174
452
|
logger;
|
|
175
453
|
ws = null;
|
|
176
454
|
vigFlowTokens = new Map();
|
|
177
|
-
_httpsAgent = null;
|
|
178
455
|
constructor(config, logger) {
|
|
179
456
|
this.config = config;
|
|
180
457
|
this.logger = logger;
|
|
@@ -184,7 +461,6 @@ class APIClient {
|
|
|
184
461
|
keepAlive: true,
|
|
185
462
|
timeout: 30000,
|
|
186
463
|
});
|
|
187
|
-
this._httpsAgent = httpsAgent;
|
|
188
464
|
// Main Vigthoria Coder API (coder.vigthoria.io)
|
|
189
465
|
this.client = axios_1.default.create({
|
|
190
466
|
baseURL: config.get('apiUrl'),
|
|
@@ -211,47 +487,82 @@ class APIClient {
|
|
|
211
487
|
this.selfHostedModelRouterClient = selfHostedModelsApiUrl ? axios_1.default.create({
|
|
212
488
|
baseURL: selfHostedModelsApiUrl,
|
|
213
489
|
timeout: 240000,
|
|
214
|
-
httpsAgent,
|
|
215
490
|
headers: {
|
|
216
491
|
'Content-Type': 'application/json',
|
|
217
492
|
'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
|
|
218
493
|
},
|
|
219
494
|
}) : null;
|
|
220
|
-
// Add auth
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
495
|
+
// Add auth interceptors that validate JWT expiry before every request.
|
|
496
|
+
const createAuthRequestInterceptor = (client, includeCookie) => {
|
|
497
|
+
client.interceptors.request.use(async (req) => {
|
|
498
|
+
const skipAuthRefresh = Boolean(req.__skipAuthRefresh) || req.url?.includes('/api/token/refresh') || req.url?.includes('/api/login');
|
|
499
|
+
if (!skipAuthRefresh) {
|
|
500
|
+
await refreshJwtIfNeeded(this);
|
|
501
|
+
}
|
|
502
|
+
const token = this.getAccessToken();
|
|
503
|
+
if (token) {
|
|
504
|
+
const payload = validateJwt(token);
|
|
505
|
+
if (!payload && !skipAuthRefresh) {
|
|
506
|
+
throw new CLIError('Authentication token is expired or invalid. Please run: vigthoria login', 'auth', { endpoint: req.url });
|
|
507
|
+
}
|
|
508
|
+
req.headers.Authorization = `Bearer ${token}`;
|
|
509
|
+
if (includeCookie) {
|
|
510
|
+
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return req;
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
createAuthRequestInterceptor(this.client, true);
|
|
517
|
+
createAuthRequestInterceptor(this.modelRouterClient, true);
|
|
518
|
+
if (this.selfHostedModelRouterClient) {
|
|
519
|
+
createAuthRequestInterceptor(this.selfHostedModelRouterClient, false);
|
|
520
|
+
}
|
|
521
|
+
// Add response interceptors for token refresh + structured errors.
|
|
245
522
|
const createAuthRetryInterceptor = (client) => {
|
|
246
523
|
client.interceptors.response.use((res) => res, async (error) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
|
|
524
|
+
const originalConfig = error.config;
|
|
525
|
+
try {
|
|
526
|
+
if (error.response?.status === 401) {
|
|
527
|
+
if (originalConfig?.__authRetry) {
|
|
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
|
+
});
|
|
251
550
|
}
|
|
252
|
-
throw classifyError(error
|
|
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
|
+
});
|
|
253
565
|
}
|
|
254
|
-
throw classifyError(error);
|
|
255
566
|
});
|
|
256
567
|
};
|
|
257
568
|
createAuthRetryInterceptor(this.client);
|
|
@@ -260,22 +571,12 @@ class APIClient {
|
|
|
260
571
|
createAuthRetryInterceptor(this.selfHostedModelRouterClient);
|
|
261
572
|
}
|
|
262
573
|
}
|
|
263
|
-
/**
|
|
264
|
-
* Destroy keep-alive sockets so the Node.js event loop can drain
|
|
265
|
-
* naturally. Call this before exiting commands that run HTTP probes
|
|
266
|
-
* (e.g. `status`) to avoid the libuv UV_HANDLE_CLOSING assertion
|
|
267
|
-
* on Windows / Node 25+.
|
|
268
|
-
*/
|
|
269
574
|
destroy() {
|
|
270
|
-
if (this._httpsAgent) {
|
|
271
|
-
this._httpsAgent.destroy();
|
|
272
|
-
this._httpsAgent = null;
|
|
273
|
-
}
|
|
274
575
|
if (this.ws) {
|
|
275
576
|
try {
|
|
276
577
|
this.ws.close();
|
|
277
578
|
}
|
|
278
|
-
catch {
|
|
579
|
+
catch { }
|
|
279
580
|
this.ws = null;
|
|
280
581
|
}
|
|
281
582
|
}
|
|
@@ -355,6 +656,26 @@ class APIClient {
|
|
|
355
656
|
}
|
|
356
657
|
}
|
|
357
658
|
}
|
|
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');
|
|
358
679
|
this.config.clearAuth();
|
|
359
680
|
return false;
|
|
360
681
|
}
|
|
@@ -364,6 +685,19 @@ class APIClient {
|
|
|
364
685
|
return false;
|
|
365
686
|
}
|
|
366
687
|
}
|
|
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
|
+
}
|
|
367
701
|
extractUserProfile(data) {
|
|
368
702
|
if (!data) {
|
|
369
703
|
return null;
|
|
@@ -400,9 +734,11 @@ class APIClient {
|
|
|
400
734
|
}
|
|
401
735
|
return true;
|
|
402
736
|
}
|
|
403
|
-
catch {
|
|
737
|
+
catch (error) {
|
|
738
|
+
const cliError = toCliError(error, 'AUTH_REFRESH_FAILED', 'Failed to refresh authentication token');
|
|
739
|
+
this.logger.debug(`Token refresh failed: ${cliError.message}`);
|
|
404
740
|
this.config.clearAuth();
|
|
405
|
-
|
|
741
|
+
throw cliError;
|
|
406
742
|
}
|
|
407
743
|
}
|
|
408
744
|
async getSubscriptionStatus() {
|
|
@@ -437,35 +773,35 @@ class APIClient {
|
|
|
437
773
|
if (!token) {
|
|
438
774
|
return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
|
|
439
775
|
}
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
]);
|
|
447
|
-
for (const r of results) {
|
|
448
|
-
if (r.status === 'fulfilled')
|
|
776
|
+
// Validate against authenticated Coder endpoints so we verify
|
|
777
|
+
// the actual Vigthoria Gateway user session state.
|
|
778
|
+
const authEndpoints = ['/api/user/profile', '/api/user/subscription'];
|
|
779
|
+
for (const endpoint of authEndpoints) {
|
|
780
|
+
try {
|
|
781
|
+
await this.client.get(endpoint, { timeout: 10000 });
|
|
449
782
|
return { valid: true };
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (r.status === 'rejected') {
|
|
454
|
-
const err = r.reason;
|
|
455
|
-
if (err.response?.status === 401 || err.response?.status === 403) {
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
if (error instanceof CLIError && error.category === 'auth') {
|
|
456
786
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
457
787
|
}
|
|
458
|
-
|
|
788
|
+
const axErr = error;
|
|
789
|
+
if (axErr.response?.status === 401 || axErr.response?.status === 403) {
|
|
459
790
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
460
791
|
}
|
|
792
|
+
// Try the next authenticated endpoint before deciding this is
|
|
793
|
+
// a transient network/backend issue.
|
|
794
|
+
continue;
|
|
461
795
|
}
|
|
462
796
|
}
|
|
463
|
-
//
|
|
797
|
+
// Auth endpoints unreachable — do not misclassify as invalid token.
|
|
464
798
|
return { valid: true };
|
|
465
799
|
}
|
|
466
800
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
467
801
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
468
|
-
const allowLocalV3Agent =
|
|
802
|
+
const allowLocalV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1'
|
|
803
|
+
|| this.allowLocalServiceFallbacks()
|
|
804
|
+
|| preferLocal;
|
|
469
805
|
const urls = [
|
|
470
806
|
process.env.VIGTHORIA_V3_AGENT_URL,
|
|
471
807
|
process.env.V3_AGENT_URL,
|
|
@@ -488,12 +824,11 @@ class APIClient {
|
|
|
488
824
|
}
|
|
489
825
|
getOperatorBaseUrls() {
|
|
490
826
|
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
491
|
-
const allowLocal = isServerRuntime() && process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
492
827
|
const urls = [
|
|
493
828
|
process.env.VIGTHORIA_OPERATOR_URL,
|
|
494
829
|
process.env.OPERATOR_URL,
|
|
830
|
+
...(this.allowLocalServiceFallbacks() ? ['http://127.0.0.1:4009'] : []),
|
|
495
831
|
configuredModelsApiUrl,
|
|
496
|
-
...(allowLocal ? ['http://127.0.0.1:4009'] : []),
|
|
497
832
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
498
833
|
return [...new Set(urls)];
|
|
499
834
|
}
|
|
@@ -502,38 +837,44 @@ class APIClient {
|
|
|
502
837
|
}
|
|
503
838
|
getMcpBaseUrls() {
|
|
504
839
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
505
|
-
const allowLocal = isServerRuntime() && process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
506
840
|
const urls = [
|
|
507
841
|
process.env.VIGTHORIA_MCP_URL,
|
|
508
842
|
process.env.MCP_SERVER_URL,
|
|
843
|
+
...(this.allowLocalServiceFallbacks() ? ['http://127.0.0.1:4008'] : []),
|
|
509
844
|
configuredApiUrl,
|
|
510
|
-
...(allowLocal ? ['http://127.0.0.1:4008'] : []),
|
|
511
845
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
512
846
|
return [...new Set(urls)];
|
|
513
847
|
}
|
|
514
848
|
getVigFlowBaseUrls() {
|
|
515
849
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
516
|
-
|
|
850
|
+
// Put the remote gateway first, since local VigFlow servers are
|
|
851
|
+
// rarely running for end-user CLI installations. This avoids
|
|
852
|
+
// wasting connection-attempt time on 127.0.0.1 and hitting the
|
|
853
|
+
// remote gateway only after the local attempts have already
|
|
854
|
+
// errored — which surfaces as a confusing "last error" 404 in
|
|
855
|
+
// some setups.
|
|
517
856
|
const urls = [
|
|
518
857
|
process.env.VIGTHORIA_VIGFLOW_URL,
|
|
519
858
|
process.env.VIGFLOW_URL,
|
|
520
859
|
process.env.WORKFLOW_BUILDER_URL,
|
|
521
860
|
`${configuredApiUrl}/api/vigflow`,
|
|
522
|
-
...(
|
|
861
|
+
...(this.allowLocalServiceFallbacks() ? ['http://127.0.0.1:5060', 'http://127.0.0.1:5050'] : []),
|
|
523
862
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
524
863
|
return [...new Set(urls)];
|
|
525
864
|
}
|
|
526
865
|
getTemplateServiceBaseUrls() {
|
|
527
866
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
528
|
-
const allowLocal = isServerRuntime() && process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
529
867
|
const urls = [
|
|
530
868
|
process.env.VIGTHORIA_TEMPLATE_SERVICE_URL,
|
|
531
869
|
process.env.TEMPLATE_SERVICE_URL,
|
|
870
|
+
...(this.allowLocalServiceFallbacks() ? ['http://127.0.0.1:4011'] : []),
|
|
532
871
|
`${configuredApiUrl}/api/template-service`,
|
|
533
|
-
...(allowLocal ? ['http://127.0.0.1:4011'] : []),
|
|
534
872
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
535
873
|
return [...new Set(urls)];
|
|
536
874
|
}
|
|
875
|
+
allowLocalServiceFallbacks() {
|
|
876
|
+
return process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1' || isServerRuntime();
|
|
877
|
+
}
|
|
537
878
|
isFrontendTask(message = '', context = {}) {
|
|
538
879
|
// Never treat analysis-only tasks as frontend tasks — preview gate
|
|
539
880
|
// should not fire for read-only inspection prompts.
|
|
@@ -855,7 +1196,7 @@ class APIClient {
|
|
|
855
1196
|
});
|
|
856
1197
|
if (!response.ok) {
|
|
857
1198
|
const errorText = await response.text().catch(() => '');
|
|
858
|
-
throw new Error(`Template preview proof ${response.status}: ${
|
|
1199
|
+
throw new Error(`Template preview proof ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
859
1200
|
}
|
|
860
1201
|
const payload = await response.json();
|
|
861
1202
|
const modes = payload?.modes || {};
|
|
@@ -908,6 +1249,12 @@ class APIClient {
|
|
|
908
1249
|
'Content-Type': 'application/json',
|
|
909
1250
|
Accept: 'application/json',
|
|
910
1251
|
};
|
|
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
|
+
}
|
|
911
1258
|
const authToken = this.getAccessToken();
|
|
912
1259
|
if (authToken) {
|
|
913
1260
|
headers.Authorization = `Bearer ${authToken}`;
|
|
@@ -919,6 +1266,12 @@ class APIClient {
|
|
|
919
1266
|
const headers = {
|
|
920
1267
|
'Content-Type': 'application/json',
|
|
921
1268
|
};
|
|
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
|
+
}
|
|
922
1275
|
const authToken = this.getAccessToken();
|
|
923
1276
|
if (authToken) {
|
|
924
1277
|
headers.Authorization = `Bearer ${authToken}`;
|
|
@@ -999,8 +1352,7 @@ class APIClient {
|
|
|
999
1352
|
this.logger.debug(`VigFlow ${operation} via ${baseUrl} failed:`, lastError.message);
|
|
1000
1353
|
}
|
|
1001
1354
|
}
|
|
1002
|
-
|
|
1003
|
-
throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
|
|
1355
|
+
throw lastError || new Error(`No VigFlow backend available for ${operation}.`);
|
|
1004
1356
|
}
|
|
1005
1357
|
/**
|
|
1006
1358
|
* Build the correct sub-path for VigFlow endpoints.
|
|
@@ -1137,7 +1489,9 @@ class APIClient {
|
|
|
1137
1489
|
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
1138
1490
|
const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
|
|
1139
1491
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1140
|
-
const localWorkspaceSummary =
|
|
1492
|
+
const localWorkspaceSummary = resolvedContext.rawPrompt
|
|
1493
|
+
? this.buildSemanticWorkspaceSummary(localWorkspacePath, String(resolvedContext.rawPrompt))
|
|
1494
|
+
: this.buildLocalWorkspaceSummary(localWorkspacePath);
|
|
1141
1495
|
const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
|
|
1142
1496
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
1143
1497
|
// When the server cannot directly access the workspace (e.g. Windows
|
|
@@ -1292,458 +1646,6 @@ class APIClient {
|
|
|
1292
1646
|
const match = String(message || '').match(/called\s+([A-Z][A-Za-z0-9&\- ]{2,40})/i);
|
|
1293
1647
|
return match?.[1]?.trim() || fallback;
|
|
1294
1648
|
}
|
|
1295
|
-
materializeEmergencySaaSWorkspace(message = '', context = {}) {
|
|
1296
|
-
const rootPath = this.resolveAgentTargetPath(context);
|
|
1297
|
-
if (!rootPath) {
|
|
1298
|
-
return null;
|
|
1299
|
-
}
|
|
1300
|
-
fs_1.default.mkdirSync(rootPath, { recursive: true });
|
|
1301
|
-
const appName = this.extractEmergencyAppName(message);
|
|
1302
|
-
const html = `<!DOCTYPE html>
|
|
1303
|
-
<html lang="en">
|
|
1304
|
-
<head>
|
|
1305
|
-
<meta charset="UTF-8">
|
|
1306
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1307
|
-
<title>${appName}</title>
|
|
1308
|
-
<link rel="stylesheet" href="styles.css">
|
|
1309
|
-
</head>
|
|
1310
|
-
<body>
|
|
1311
|
-
<div class="app-shell">
|
|
1312
|
-
<aside class="sidebar">
|
|
1313
|
-
<div class="brand">${appName}</div>
|
|
1314
|
-
<button class="menu-toggle" id="menu-toggle" aria-label="Toggle navigation">Menu</button>
|
|
1315
|
-
<nav>
|
|
1316
|
-
<a href="#dashboard" class="nav-link active">Dashboard</a>
|
|
1317
|
-
<a href="#team" class="nav-link">Team</a>
|
|
1318
|
-
<a href="#billing" class="nav-link">Billing</a>
|
|
1319
|
-
<a href="#settings" class="nav-link">Settings</a>
|
|
1320
|
-
</nav>
|
|
1321
|
-
</aside>
|
|
1322
|
-
<main class="content">
|
|
1323
|
-
<section class="hero-card panel active-panel" id="dashboard">
|
|
1324
|
-
<div class="hero-copy">
|
|
1325
|
-
<p class="eyebrow">Dashboard</p>
|
|
1326
|
-
<h1>${appName} revenue command center</h1>
|
|
1327
|
-
<p>Track login activity, campaign velocity, billing state, and team performance from one responsive SaaS workspace.</p>
|
|
1328
|
-
</div>
|
|
1329
|
-
<form class="login-card">
|
|
1330
|
-
<h2>Login</h2>
|
|
1331
|
-
<label>Email<input type="email" placeholder="ops@${appName.toLowerCase().replace(/[^a-z0-9]+/g, '') || 'signaldesk'}.io"></label>
|
|
1332
|
-
<label>Password<input type="password" placeholder="Enter password"></label>
|
|
1333
|
-
<button type="submit">Enter dashboard</button>
|
|
1334
|
-
</form>
|
|
1335
|
-
</section>
|
|
1336
|
-
|
|
1337
|
-
<section class="stats-grid">
|
|
1338
|
-
<article class="stat-card"><span>MRR</span><strong>$284K</strong><em>+12.4%</em></article>
|
|
1339
|
-
<article class="stat-card"><span>Activation</span><strong>74%</strong><em>+6.1%</em></article>
|
|
1340
|
-
<article class="stat-card"><span>Team Seats</span><strong>128</strong><em>8 pending</em></article>
|
|
1341
|
-
<article class="stat-card"><span>Churn Risk</span><strong>2.1%</strong><em>Low</em></article>
|
|
1342
|
-
</section>
|
|
1343
|
-
|
|
1344
|
-
<section class="workspace-grid">
|
|
1345
|
-
<article class="panel chart-panel">
|
|
1346
|
-
<div class="panel-header">
|
|
1347
|
-
<h2>Analytics</h2>
|
|
1348
|
-
<button id="open-modal" type="button">Add campaign</button>
|
|
1349
|
-
</div>
|
|
1350
|
-
<div class="chart-bars" aria-label="Revenue chart">
|
|
1351
|
-
<div class="bar" style="--value: 52%"><span>Mon</span></div>
|
|
1352
|
-
<div class="bar" style="--value: 68%"><span>Tue</span></div>
|
|
1353
|
-
<div class="bar" style="--value: 74%"><span>Wed</span></div>
|
|
1354
|
-
<div class="bar" style="--value: 59%"><span>Thu</span></div>
|
|
1355
|
-
<div class="bar" style="--value: 88%"><span>Fri</span></div>
|
|
1356
|
-
</div>
|
|
1357
|
-
</article>
|
|
1358
|
-
|
|
1359
|
-
<article class="panel activity-panel">
|
|
1360
|
-
<div class="panel-header"><h2>Activity Feed</h2><span>Live</span></div>
|
|
1361
|
-
<ul class="activity-feed">
|
|
1362
|
-
<li><strong>Billing</strong><span>Enterprise invoice paid</span></li>
|
|
1363
|
-
<li><strong>Team</strong><span>New strategist invited to workspace</span></li>
|
|
1364
|
-
<li><strong>Dashboard</strong><span>KPI threshold updated for activation alerts</span></li>
|
|
1365
|
-
</ul>
|
|
1366
|
-
</article>
|
|
1367
|
-
|
|
1368
|
-
<article class="panel" id="team">
|
|
1369
|
-
<div class="panel-header"><h2>Team Management</h2><span>Owners and operators</span></div>
|
|
1370
|
-
<div class="team-list">
|
|
1371
|
-
<div><strong>Ana</strong><span>Growth lead</span></div>
|
|
1372
|
-
<div><strong>Marcus</strong><span>Billing admin</span></div>
|
|
1373
|
-
<div><strong>Lina</strong><span>Lifecycle analyst</span></div>
|
|
1374
|
-
</div>
|
|
1375
|
-
</article>
|
|
1376
|
-
|
|
1377
|
-
<article class="panel" id="billing">
|
|
1378
|
-
<div class="panel-header"><h2>Billing</h2><span>Current plan</span></div>
|
|
1379
|
-
<div class="billing-card">
|
|
1380
|
-
<strong>Scale Annual</strong>
|
|
1381
|
-
<p>Renews on 12 Oct with usage-based analytics overages.</p>
|
|
1382
|
-
<button type="button" class="secondary-action">Update payment method</button>
|
|
1383
|
-
</div>
|
|
1384
|
-
</article>
|
|
1385
|
-
|
|
1386
|
-
<article class="panel" id="settings">
|
|
1387
|
-
<div class="panel-header"><h2>Settings</h2><span>Automation and alerts</span></div>
|
|
1388
|
-
<form class="settings-form">
|
|
1389
|
-
<label>Alert threshold<input type="number" value="18"></label>
|
|
1390
|
-
<label>Weekly digest<select><option>Enabled</option><option>Paused</option></select></label>
|
|
1391
|
-
<button type="submit">Save settings</button>
|
|
1392
|
-
</form>
|
|
1393
|
-
</article>
|
|
1394
|
-
</section>
|
|
1395
|
-
</main>
|
|
1396
|
-
</div>
|
|
1397
|
-
|
|
1398
|
-
<dialog id="campaign-modal">
|
|
1399
|
-
<form method="dialog" class="modal-form">
|
|
1400
|
-
<h2>Launch campaign</h2>
|
|
1401
|
-
<label>Name<input type="text" placeholder="Retention push"></label>
|
|
1402
|
-
<label>Owner<input type="text" placeholder="Lina"></label>
|
|
1403
|
-
<menu>
|
|
1404
|
-
<button value="cancel">Cancel</button>
|
|
1405
|
-
<button value="confirm">Create</button>
|
|
1406
|
-
</menu>
|
|
1407
|
-
</form>
|
|
1408
|
-
</dialog>
|
|
1409
|
-
|
|
1410
|
-
<script src="scripts.js"></script>
|
|
1411
|
-
</body>
|
|
1412
|
-
</html>
|
|
1413
|
-
`;
|
|
1414
|
-
const css = `:root {
|
|
1415
|
-
--bg: #f2ede4;
|
|
1416
|
-
--ink: #18222f;
|
|
1417
|
-
--muted: #5c6674;
|
|
1418
|
-
--panel: rgba(255, 255, 255, 0.82);
|
|
1419
|
-
--line: rgba(24, 34, 47, 0.08);
|
|
1420
|
-
--accent: #b6542c;
|
|
1421
|
-
--accent-strong: #7f3417;
|
|
1422
|
-
--shadow: 0 24px 60px rgba(24, 34, 47, 0.12);
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
* { box-sizing: border-box; }
|
|
1426
|
-
|
|
1427
|
-
body {
|
|
1428
|
-
margin: 0;
|
|
1429
|
-
font-family: "Georgia", "Times New Roman", serif;
|
|
1430
|
-
color: var(--ink);
|
|
1431
|
-
background:
|
|
1432
|
-
radial-gradient(circle at top left, rgba(182, 84, 44, 0.18), transparent 28%),
|
|
1433
|
-
radial-gradient(circle at bottom right, rgba(24, 34, 47, 0.14), transparent 30%),
|
|
1434
|
-
var(--bg);
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
.app-shell {
|
|
1438
|
-
min-height: 100vh;
|
|
1439
|
-
display: grid;
|
|
1440
|
-
grid-template-columns: 260px 1fr;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
.sidebar {
|
|
1444
|
-
padding: 2rem 1.25rem;
|
|
1445
|
-
background: rgba(24, 34, 47, 0.94);
|
|
1446
|
-
color: #f7f2eb;
|
|
1447
|
-
position: sticky;
|
|
1448
|
-
top: 0;
|
|
1449
|
-
min-height: 100vh;
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
.brand {
|
|
1453
|
-
font-size: 1.6rem;
|
|
1454
|
-
font-weight: 700;
|
|
1455
|
-
margin-bottom: 1.5rem;
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
.menu-toggle {
|
|
1459
|
-
display: none;
|
|
1460
|
-
margin-bottom: 1rem;
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
nav {
|
|
1464
|
-
display: grid;
|
|
1465
|
-
gap: 0.6rem;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
.nav-link {
|
|
1469
|
-
color: inherit;
|
|
1470
|
-
text-decoration: none;
|
|
1471
|
-
padding: 0.8rem 0.95rem;
|
|
1472
|
-
border-radius: 999px;
|
|
1473
|
-
transition: transform 0.25s ease, background-color 0.25s ease;
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
.nav-link:hover,
|
|
1477
|
-
.nav-link.active {
|
|
1478
|
-
background: rgba(255, 255, 255, 0.12);
|
|
1479
|
-
transform: translateX(4px);
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
.content {
|
|
1483
|
-
padding: 2rem;
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
.hero-card,
|
|
1487
|
-
.panel,
|
|
1488
|
-
.stat-card,
|
|
1489
|
-
.login-card,
|
|
1490
|
-
dialog {
|
|
1491
|
-
background: var(--panel);
|
|
1492
|
-
backdrop-filter: blur(16px);
|
|
1493
|
-
border: 1px solid var(--line);
|
|
1494
|
-
box-shadow: var(--shadow);
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
.hero-card {
|
|
1498
|
-
display: grid;
|
|
1499
|
-
grid-template-columns: 1.3fr 0.9fr;
|
|
1500
|
-
gap: 1.5rem;
|
|
1501
|
-
border-radius: 32px;
|
|
1502
|
-
padding: 2rem;
|
|
1503
|
-
margin-bottom: 1.5rem;
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
.eyebrow {
|
|
1507
|
-
text-transform: uppercase;
|
|
1508
|
-
letter-spacing: 0.14em;
|
|
1509
|
-
color: var(--accent-strong);
|
|
1510
|
-
font-size: 0.78rem;
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
.hero-card h1,
|
|
1514
|
-
.panel h2,
|
|
1515
|
-
.login-card h2 {
|
|
1516
|
-
margin: 0 0 0.75rem;
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
.login-card,
|
|
1520
|
-
.panel,
|
|
1521
|
-
.stat-card {
|
|
1522
|
-
border-radius: 24px;
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
.login-card,
|
|
1526
|
-
.settings-form,
|
|
1527
|
-
.modal-form {
|
|
1528
|
-
display: grid;
|
|
1529
|
-
gap: 0.85rem;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
.stats-grid,
|
|
1533
|
-
.workspace-grid {
|
|
1534
|
-
display: grid;
|
|
1535
|
-
gap: 1rem;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
.stats-grid {
|
|
1539
|
-
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1540
|
-
margin-bottom: 1rem;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
.workspace-grid {
|
|
1544
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
.stat-card,
|
|
1548
|
-
.panel {
|
|
1549
|
-
padding: 1.2rem;
|
|
1550
|
-
animation: riseIn 0.7s ease forwards;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
.stat-card span,
|
|
1554
|
-
.panel-header span,
|
|
1555
|
-
.activity-feed span,
|
|
1556
|
-
.team-list span,
|
|
1557
|
-
.billing-card p {
|
|
1558
|
-
color: var(--muted);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
.panel-header {
|
|
1562
|
-
display: flex;
|
|
1563
|
-
align-items: center;
|
|
1564
|
-
justify-content: space-between;
|
|
1565
|
-
gap: 1rem;
|
|
1566
|
-
margin-bottom: 1rem;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
.chart-bars {
|
|
1570
|
-
display: grid;
|
|
1571
|
-
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
1572
|
-
gap: 0.9rem;
|
|
1573
|
-
align-items: end;
|
|
1574
|
-
min-height: 220px;
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
.bar {
|
|
1578
|
-
position: relative;
|
|
1579
|
-
min-height: 180px;
|
|
1580
|
-
border-radius: 20px 20px 8px 8px;
|
|
1581
|
-
background: linear-gradient(180deg, rgba(182, 84, 44, 0.92), rgba(127, 52, 23, 0.68));
|
|
1582
|
-
transform-origin: bottom;
|
|
1583
|
-
transform: scaleY(calc(var(--value) / 100));
|
|
1584
|
-
transition: transform 0.6s ease;
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
.bar span {
|
|
1588
|
-
position: absolute;
|
|
1589
|
-
left: 50%;
|
|
1590
|
-
bottom: -1.6rem;
|
|
1591
|
-
transform: translateX(-50%);
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
.activity-feed,
|
|
1595
|
-
.team-list {
|
|
1596
|
-
display: grid;
|
|
1597
|
-
gap: 0.8rem;
|
|
1598
|
-
padding: 0;
|
|
1599
|
-
margin: 0;
|
|
1600
|
-
list-style: none;
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
.activity-feed li,
|
|
1604
|
-
.team-list div,
|
|
1605
|
-
.billing-card {
|
|
1606
|
-
padding: 0.9rem 1rem;
|
|
1607
|
-
border-radius: 18px;
|
|
1608
|
-
background: rgba(255, 255, 255, 0.7);
|
|
1609
|
-
border: 1px solid var(--line);
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
label {
|
|
1613
|
-
display: grid;
|
|
1614
|
-
gap: 0.35rem;
|
|
1615
|
-
font-size: 0.95rem;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
input,
|
|
1619
|
-
select,
|
|
1620
|
-
button {
|
|
1621
|
-
font: inherit;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
input,
|
|
1625
|
-
select {
|
|
1626
|
-
width: 100%;
|
|
1627
|
-
padding: 0.85rem 1rem;
|
|
1628
|
-
border-radius: 14px;
|
|
1629
|
-
border: 1px solid var(--line);
|
|
1630
|
-
background: rgba(255, 255, 255, 0.92);
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
button {
|
|
1634
|
-
border: none;
|
|
1635
|
-
border-radius: 999px;
|
|
1636
|
-
padding: 0.85rem 1.2rem;
|
|
1637
|
-
background: var(--accent);
|
|
1638
|
-
color: #fff9f3;
|
|
1639
|
-
cursor: pointer;
|
|
1640
|
-
transition: transform 0.25s ease, background-color 0.25s ease;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
button:hover {
|
|
1644
|
-
background: var(--accent-strong);
|
|
1645
|
-
transform: translateY(-2px);
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
.secondary-action,
|
|
1649
|
-
menu button:first-child {
|
|
1650
|
-
background: rgba(24, 34, 47, 0.12);
|
|
1651
|
-
color: var(--ink);
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
dialog {
|
|
1655
|
-
border-radius: 28px;
|
|
1656
|
-
padding: 0;
|
|
1657
|
-
width: min(420px, calc(100% - 2rem));
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
dialog::backdrop {
|
|
1661
|
-
background: rgba(24, 34, 47, 0.3);
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
.modal-form {
|
|
1665
|
-
padding: 1.4rem;
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
menu {
|
|
1669
|
-
display: flex;
|
|
1670
|
-
justify-content: flex-end;
|
|
1671
|
-
gap: 0.75rem;
|
|
1672
|
-
padding: 0;
|
|
1673
|
-
margin: 0.5rem 0 0;
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
@keyframes riseIn {
|
|
1677
|
-
from {
|
|
1678
|
-
opacity: 0;
|
|
1679
|
-
transform: translateY(18px);
|
|
1680
|
-
}
|
|
1681
|
-
to {
|
|
1682
|
-
opacity: 1;
|
|
1683
|
-
transform: translateY(0);
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
@media (max-width: 980px) {
|
|
1688
|
-
.app-shell,
|
|
1689
|
-
.hero-card,
|
|
1690
|
-
.stats-grid,
|
|
1691
|
-
.workspace-grid {
|
|
1692
|
-
grid-template-columns: 1fr;
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
.sidebar {
|
|
1696
|
-
position: static;
|
|
1697
|
-
min-height: auto;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
.menu-toggle {
|
|
1701
|
-
display: inline-flex;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
nav {
|
|
1705
|
-
display: none;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
nav.is-open {
|
|
1709
|
-
display: grid;
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
`;
|
|
1713
|
-
const js = `document.addEventListener('DOMContentLoaded', () => {
|
|
1714
|
-
const menuToggle = document.getElementById('menu-toggle');
|
|
1715
|
-
const nav = document.querySelector('nav');
|
|
1716
|
-
const modal = document.getElementById('campaign-modal');
|
|
1717
|
-
const openModal = document.getElementById('open-modal');
|
|
1718
|
-
const navLinks = document.querySelectorAll('.nav-link');
|
|
1719
|
-
|
|
1720
|
-
menuToggle?.addEventListener('click', () => nav?.classList.toggle('is-open'));
|
|
1721
|
-
openModal?.addEventListener('click', () => modal?.showModal());
|
|
1722
|
-
modal?.addEventListener('close', () => document.body.classList.remove('modal-open'));
|
|
1723
|
-
|
|
1724
|
-
navLinks.forEach((link) => {
|
|
1725
|
-
link.addEventListener('click', (event) => {
|
|
1726
|
-
event.preventDefault();
|
|
1727
|
-
navLinks.forEach((entry) => entry.classList.remove('active'));
|
|
1728
|
-
link.classList.add('active');
|
|
1729
|
-
document.querySelector(link.getAttribute('href'))?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1730
|
-
nav?.classList.remove('is-open');
|
|
1731
|
-
});
|
|
1732
|
-
});
|
|
1733
|
-
|
|
1734
|
-
document.querySelectorAll('.bar').forEach((bar, index) => {
|
|
1735
|
-
bar.animate([
|
|
1736
|
-
{ transform: 'scaleY(0.15)' },
|
|
1737
|
-
{ transform: getComputedStyle(bar).transform || 'scaleY(1)' }
|
|
1738
|
-
], { duration: 600 + index * 80, fill: 'forwards', easing: 'ease-out' });
|
|
1739
|
-
});
|
|
1740
|
-
});
|
|
1741
|
-
`;
|
|
1742
|
-
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'index.html'), `${html.trimEnd()}\n`, 'utf8');
|
|
1743
|
-
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'styles.css'), `${css.trimEnd()}\n`, 'utf8');
|
|
1744
|
-
fs_1.default.writeFileSync(path_1.default.join(rootPath, 'scripts.js'), `${js.trimEnd()}\n`, 'utf8');
|
|
1745
|
-
return appName;
|
|
1746
|
-
}
|
|
1747
1649
|
ensureExecutionContext(context = {}) {
|
|
1748
1650
|
const existingId = String(context.contextId || context.traceId || '').trim();
|
|
1749
1651
|
const contextId = existingId || `vig-${Date.now()}-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
@@ -1796,7 +1698,7 @@ menu {
|
|
|
1796
1698
|
});
|
|
1797
1699
|
if (!response.ok) {
|
|
1798
1700
|
const errorText = await response.text().catch(() => '');
|
|
1799
|
-
throw new Error(`MCP context update ${response.status}: ${
|
|
1701
|
+
throw new Error(`MCP context update ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
1800
1702
|
}
|
|
1801
1703
|
return {
|
|
1802
1704
|
...executionContext,
|
|
@@ -1827,7 +1729,7 @@ menu {
|
|
|
1827
1729
|
});
|
|
1828
1730
|
if (!createResponse.ok) {
|
|
1829
1731
|
const errorText = await createResponse.text().catch(() => '');
|
|
1830
|
-
throw new Error(`MCP context create ${createResponse.status}: ${
|
|
1732
|
+
throw new Error(`MCP context create ${createResponse.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
1831
1733
|
}
|
|
1832
1734
|
const payload = await createResponse.json();
|
|
1833
1735
|
const mcpContextId = String(payload.contextId || '').trim();
|
|
@@ -1917,6 +1819,14 @@ menu {
|
|
|
1917
1819
|
* Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
|
|
1918
1820
|
*/
|
|
1919
1821
|
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
|
+
}
|
|
1920
1830
|
const MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
1921
1831
|
const MAX_FILE_BYTES = 200 * 1024;
|
|
1922
1832
|
const BINARY_EXTENSIONS = new Set([
|
|
@@ -2184,11 +2094,6 @@ menu {
|
|
|
2184
2094
|
if (!looksLikeFrontendTask) {
|
|
2185
2095
|
return;
|
|
2186
2096
|
}
|
|
2187
|
-
// Skip motion/scroll enhancements for games — they use canvas, not section-based layouts
|
|
2188
|
-
const looksLikeGame = /\bgame\b|arcade|pac.?man|tetris|platformer|roguelike|breakout|pong|snake\s+game|tower\s+defense|playable/i.test(prompt);
|
|
2189
|
-
if (looksLikeGame) {
|
|
2190
|
-
return;
|
|
2191
|
-
}
|
|
2192
2097
|
const htmlPath = path_1.default.join(rootPath, 'index.html');
|
|
2193
2098
|
if (!fs_1.default.existsSync(htmlPath)) {
|
|
2194
2099
|
return;
|
|
@@ -2635,7 +2540,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2635
2540
|
let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
|
|
2636
2541
|
let serverWorkspaceRoot = null;
|
|
2637
2542
|
const streamedFiles = {};
|
|
2638
|
-
const idleTimeoutMs = context.agentIdleTimeoutMs
|
|
2543
|
+
const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
|
|
2639
2544
|
while (true) {
|
|
2640
2545
|
let chunk;
|
|
2641
2546
|
try {
|
|
@@ -2704,19 +2609,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2704
2609
|
serverWorkspaceRoot = event.workspace_root.trim();
|
|
2705
2610
|
}
|
|
2706
2611
|
this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
|
|
2707
|
-
// Real-time workspace streaming: apply file mutations to local disk immediately
|
|
2708
|
-
if (event.type === 'file_mutation') {
|
|
2709
|
-
const localRoot = context.projectPath || context.workspacePath || context.targetPath;
|
|
2710
|
-
if (localRoot && typeof event.path === 'string') {
|
|
2711
|
-
(0, workspace_stream_js_1.applyFileMutation)(event, localRoot);
|
|
2712
|
-
if (typeof event.content === 'string') {
|
|
2713
|
-
const relPath = this.normalizeAgentWorkspaceRelativePath(event.path, serverWorkspaceRoot || undefined);
|
|
2714
|
-
if (relPath) {
|
|
2715
|
-
streamedFiles[relPath] = event.content;
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
}
|
|
2720
2612
|
// Empty workspace guard: if the remote agent lists its root
|
|
2721
2613
|
// and finds nothing while our local workspace has files, the
|
|
2722
2614
|
// workspace was not hydrated. Abort early with a clear error
|
|
@@ -2773,7 +2665,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2773
2665
|
partial: true,
|
|
2774
2666
|
};
|
|
2775
2667
|
}
|
|
2776
|
-
throw new Error(
|
|
2668
|
+
throw new Error(event.message || 'V3 agent returned an error');
|
|
2777
2669
|
}
|
|
2778
2670
|
if (event.type === 'complete' || event.type === 'message') {
|
|
2779
2671
|
final = event;
|
|
@@ -2791,13 +2683,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2791
2683
|
}
|
|
2792
2684
|
async runV3AgentWorkflow(message, context = {}) {
|
|
2793
2685
|
const executionContext = await this.bindExecutionContext(context);
|
|
2794
|
-
const baseTimeoutMs = executionContext.agentTimeoutMs
|
|
2686
|
+
const baseTimeoutMs = executionContext.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
|
|
2795
2687
|
const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
|
|
2796
2688
|
const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
|
|
2797
2689
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
2798
2690
|
const preferLocalV3 = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|responsive|animated|create the required project files and write them to the workspace)/i.test(message)
|
|
2799
2691
|
&& context.localMachineCapable !== false;
|
|
2800
|
-
const
|
|
2692
|
+
const rescueEligibleSaaS = preferLocalV3
|
|
2693
|
+
&& /(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;
|
|
2801
2696
|
const maxAttempts = preferLocalV3 ? 2 : 1;
|
|
2802
2697
|
let lastErrors = [];
|
|
2803
2698
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -2831,12 +2726,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2831
2726
|
};
|
|
2832
2727
|
for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
|
|
2833
2728
|
const controller = new AbortController();
|
|
2834
|
-
const timeoutId =
|
|
2729
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2730
|
+
const softTimeoutId = softTimeoutMs > 0 ? setTimeout(() => controller.abort(), softTimeoutMs) : null;
|
|
2835
2731
|
try {
|
|
2836
2732
|
const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
|
|
2733
|
+
clearTimeout(timeoutId);
|
|
2837
2734
|
if (!response.ok) {
|
|
2838
2735
|
const errorText = await response.text().catch(() => '');
|
|
2839
|
-
throw new Error(`V3 agent ${response.status}: ${
|
|
2736
|
+
throw new Error(`V3 agent ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
2840
2737
|
}
|
|
2841
2738
|
const data = await this.collectV3AgentStream(response, requestExecutionContext);
|
|
2842
2739
|
// Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
|
|
@@ -2863,7 +2760,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2863
2760
|
stream: true,
|
|
2864
2761
|
};
|
|
2865
2762
|
const continueController = new AbortController();
|
|
2866
|
-
const continueTimeoutId =
|
|
2763
|
+
const continueTimeoutId = setTimeout(() => continueController.abort(), timeoutMs);
|
|
2764
|
+
const continueSoftTimeoutId = softTimeoutMs > 0 ? setTimeout(() => continueController.abort(), softTimeoutMs) : null;
|
|
2867
2765
|
try {
|
|
2868
2766
|
const continueHeaders = await this.getV3AgentHeaders();
|
|
2869
2767
|
const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
@@ -2872,6 +2770,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2872
2770
|
body: JSON.stringify(continueBody),
|
|
2873
2771
|
signal: continueController.signal,
|
|
2874
2772
|
});
|
|
2773
|
+
clearTimeout(continueTimeoutId);
|
|
2875
2774
|
if (!continueResponse.ok) {
|
|
2876
2775
|
break; // Fall through to normal completion with partial data
|
|
2877
2776
|
}
|
|
@@ -2881,8 +2780,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2881
2780
|
break; // Fall through to normal completion with partial data
|
|
2882
2781
|
}
|
|
2883
2782
|
finally {
|
|
2884
|
-
|
|
2885
|
-
|
|
2783
|
+
clearTimeout(continueTimeoutId);
|
|
2784
|
+
if (continueSoftTimeoutId) {
|
|
2785
|
+
clearTimeout(continueSoftTimeoutId);
|
|
2886
2786
|
}
|
|
2887
2787
|
}
|
|
2888
2788
|
}
|
|
@@ -2898,7 +2798,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2898
2798
|
contextId: finalContextId,
|
|
2899
2799
|
backendUrl: baseUrl,
|
|
2900
2800
|
partial: continuationData.checkpointed === true,
|
|
2901
|
-
changedFiles: continuationData.files || data.files || {},
|
|
2902
2801
|
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
2903
2802
|
};
|
|
2904
2803
|
}
|
|
@@ -2917,7 +2816,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2917
2816
|
taskId: data.task_id || null,
|
|
2918
2817
|
contextId,
|
|
2919
2818
|
backendUrl: baseUrl,
|
|
2920
|
-
changedFiles: data.files || {},
|
|
2921
2819
|
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
2922
2820
|
};
|
|
2923
2821
|
}
|
|
@@ -2933,15 +2831,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2933
2831
|
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
2934
2832
|
backendUrl: baseUrl,
|
|
2935
2833
|
partial: true,
|
|
2936
|
-
changedFiles: error.partialData.files || {},
|
|
2937
2834
|
metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null, mcpContextId: requestExecutionContext.mcpContextId || executionContext.mcpContextId || null, previewGate },
|
|
2938
2835
|
};
|
|
2939
2836
|
}
|
|
2940
2837
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
2941
2838
|
}
|
|
2942
2839
|
finally {
|
|
2943
|
-
|
|
2944
|
-
|
|
2840
|
+
clearTimeout(timeoutId);
|
|
2841
|
+
if (softTimeoutId) {
|
|
2842
|
+
clearTimeout(softTimeoutId);
|
|
2945
2843
|
}
|
|
2946
2844
|
}
|
|
2947
2845
|
}
|
|
@@ -2964,24 +2862,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2964
2862
|
this.config.clearAuth();
|
|
2965
2863
|
throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
|
|
2966
2864
|
}
|
|
2967
|
-
if (preferLocalV3
|
|
2968
|
-
&& !this.hasAgentWorkspaceOutput(executionContext)
|
|
2969
|
-
&& /(saas|dashboard|analytics|billing|team management|activity feed)/i.test(message)) {
|
|
2970
|
-
const appName = this.materializeEmergencySaaSWorkspace(message, executionContext);
|
|
2971
|
-
if (appName) {
|
|
2972
|
-
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
|
|
2973
|
-
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
2974
|
-
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
2975
|
-
return {
|
|
2976
|
-
content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
|
|
2977
|
-
taskId: null,
|
|
2978
|
-
contextId: executionContext.contextId || null,
|
|
2979
|
-
backendUrl: 'local-emergency-scaffold',
|
|
2980
|
-
partial: true,
|
|
2981
|
-
metadata: { source: 'v3-agent-emergency-scaffold', mode: 'agent', previewGate, emergencyScaffold: true },
|
|
2982
|
-
};
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
2865
|
throw new Error(errors.join(' | '));
|
|
2986
2866
|
}
|
|
2987
2867
|
formatOperatorResponse(data = {}) {
|
|
@@ -3009,7 +2889,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3009
2889
|
}
|
|
3010
2890
|
async runOperatorWorkflow(message, context = {}) {
|
|
3011
2891
|
const executionContext = await this.bindExecutionContext(context);
|
|
3012
|
-
const timeoutMs = context.operatorTimeoutMs
|
|
2892
|
+
const timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
|
|
3013
2893
|
const errors = [];
|
|
3014
2894
|
const authToken = this.config.get('authToken');
|
|
3015
2895
|
// Collect a lightweight workspace file listing so the operator can
|
|
@@ -3018,7 +2898,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3018
2898
|
const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
3019
2899
|
for (const baseUrl of this.getOperatorBaseUrls()) {
|
|
3020
2900
|
const controller = new AbortController();
|
|
3021
|
-
const timeoutId =
|
|
2901
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3022
2902
|
try {
|
|
3023
2903
|
const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
|
|
3024
2904
|
method: 'POST',
|
|
@@ -3042,7 +2922,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3042
2922
|
workspace: { path: workspacePath },
|
|
3043
2923
|
workspace_path: workspacePath,
|
|
3044
2924
|
workspace_summary: workspaceSummary,
|
|
3045
|
-
model: this.resolveModelId(executionContext.model || 'code'),
|
|
2925
|
+
model: this.resolveModelId(executionContext.model || 'code-9b'),
|
|
3046
2926
|
history: executionContext.history || [],
|
|
3047
2927
|
executionSurface: executionContext.executionSurface || 'cli',
|
|
3048
2928
|
clientSurface: executionContext.clientSurface || 'cli',
|
|
@@ -3053,7 +2933,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3053
2933
|
rawPrompt: executionContext.rawPrompt || null,
|
|
3054
2934
|
requestStartedAt: executionContext.requestStartedAt,
|
|
3055
2935
|
},
|
|
3056
|
-
workflow_type: executionContext.workflowType || '
|
|
2936
|
+
workflow_type: executionContext.workflowType || 'analysis_only',
|
|
3057
2937
|
options: {
|
|
3058
2938
|
stream: true,
|
|
3059
2939
|
save_to_vigflow: executionContext.savePlanToVigFlow === true,
|
|
@@ -3063,7 +2943,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3063
2943
|
});
|
|
3064
2944
|
if (!response.ok) {
|
|
3065
2945
|
const errorText = await response.text().catch(() => '');
|
|
3066
|
-
throw new Error(`Operator stream ${response.status}: ${
|
|
2946
|
+
throw new Error(`Operator stream ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
3067
2947
|
}
|
|
3068
2948
|
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
3069
2949
|
const fallbackData = await response.json();
|
|
@@ -3165,9 +3045,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3165
3045
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3166
3046
|
}
|
|
3167
3047
|
finally {
|
|
3168
|
-
|
|
3169
|
-
clearTimeout(timeoutId);
|
|
3170
|
-
}
|
|
3048
|
+
clearTimeout(timeoutId);
|
|
3171
3049
|
}
|
|
3172
3050
|
}
|
|
3173
3051
|
throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
|
|
@@ -3219,7 +3097,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3219
3097
|
if (!this.shouldSkipCloudRoutes(resolvedModel)) {
|
|
3220
3098
|
try {
|
|
3221
3099
|
this.logger.debug(`Direct Vigthoria Models API: ${resolvedModel}`);
|
|
3222
|
-
const token = this.
|
|
3100
|
+
const token = this.getAccessToken();
|
|
3223
3101
|
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
3224
3102
|
model: resolvedModel,
|
|
3225
3103
|
messages,
|
|
@@ -3333,20 +3211,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3333
3211
|
}
|
|
3334
3212
|
getFallbackModelId(resolvedModel) {
|
|
3335
3213
|
const cloudModels = new Set([
|
|
3336
|
-
'deepseek-v3.1:671b-cloud',
|
|
3337
3214
|
'moonshotai/kimi-k2.5',
|
|
3338
3215
|
'vigthoria-cloud-pro',
|
|
3339
3216
|
'vigthoria-cloud-k2',
|
|
3340
3217
|
'vigthoria-cloud-ultra',
|
|
3341
3218
|
]);
|
|
3342
3219
|
if (cloudModels.has(resolvedModel)) {
|
|
3343
|
-
return 'vigthoria-v3-code-
|
|
3220
|
+
return 'vigthoria-v3-code-35b';
|
|
3344
3221
|
}
|
|
3345
3222
|
return null;
|
|
3346
3223
|
}
|
|
3347
3224
|
isCloudModelId(resolvedModel) {
|
|
3348
|
-
return resolvedModel === '
|
|
3349
|
-
|| resolvedModel === 'moonshotai/kimi-k2.5'
|
|
3225
|
+
return resolvedModel === 'moonshotai/kimi-k2.5'
|
|
3350
3226
|
|| resolvedModel === 'vigthoria-cloud-pro'
|
|
3351
3227
|
|| resolvedModel === 'vigthoria-cloud-k2'
|
|
3352
3228
|
|| resolvedModel === 'vigthoria-cloud-ultra';
|
|
@@ -3356,11 +3232,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3356
3232
|
}
|
|
3357
3233
|
resolvePermittedModelId(shortName) {
|
|
3358
3234
|
const resolvedModel = this.resolveModelId(shortName);
|
|
3235
|
+
const requested = String(shortName || '').toLowerCase();
|
|
3359
3236
|
if (this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()) {
|
|
3360
3237
|
const fallbackModel = this.getSelfHostedFallbackModelId(resolvedModel, shortName);
|
|
3361
3238
|
this.logger.debug(`Blocked unauthorized cloud model ${shortName}; using fallback ${fallbackModel}`);
|
|
3362
3239
|
return fallbackModel;
|
|
3363
3240
|
}
|
|
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
|
+
}
|
|
3364
3254
|
return resolvedModel;
|
|
3365
3255
|
}
|
|
3366
3256
|
shouldSimulateCloudFailure() {
|
|
@@ -3379,27 +3269,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3379
3269
|
isSelfHostedPreferredModel(resolvedModel, requestedModel) {
|
|
3380
3270
|
const normalizedRequested = String(requestedModel || '').toLowerCase();
|
|
3381
3271
|
const selfHostedModels = new Set([
|
|
3382
|
-
'vigthoria-v3-code-
|
|
3383
|
-
'vigthoria-v3-code-
|
|
3272
|
+
'vigthoria-v3-code-35b',
|
|
3273
|
+
'vigthoria-v3-code-35b:latest',
|
|
3384
3274
|
'qwen3-coder:latest',
|
|
3385
|
-
'
|
|
3275
|
+
'vigthoria_c1_m',
|
|
3386
3276
|
]);
|
|
3387
3277
|
return selfHostedModels.has(resolvedModel)
|
|
3388
3278
|
|| normalizedRequested === 'agent'
|
|
3389
3279
|
|| normalizedRequested === 'code'
|
|
3390
|
-
|| normalizedRequested === 'code-
|
|
3280
|
+
|| normalizedRequested === 'code-35b'
|
|
3281
|
+
|| normalizedRequested === 'code-35b'
|
|
3282
|
+
|| normalizedRequested === 'code-9b'
|
|
3391
3283
|
|| normalizedRequested === 'pro';
|
|
3392
3284
|
}
|
|
3393
3285
|
getSelfHostedFallbackModelId(resolvedModel, requestedModel) {
|
|
3394
3286
|
if (this.isSelfHostedPreferredModel(resolvedModel, requestedModel)) {
|
|
3395
|
-
return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-
|
|
3287
|
+
return resolvedModel === 'qwen3-coder:latest' ? 'vigthoria-v3-code-35b' : resolvedModel;
|
|
3396
3288
|
}
|
|
3397
|
-
return 'vigthoria-v3-code-
|
|
3289
|
+
return 'vigthoria-v3-code-35b';
|
|
3398
3290
|
}
|
|
3399
3291
|
// Streaming chat
|
|
3400
3292
|
async *chatStream(messages, model) {
|
|
3401
3293
|
const wsUrl = this.config.get('wsUrl');
|
|
3402
|
-
const token = this.
|
|
3294
|
+
const token = this.getAccessToken();
|
|
3403
3295
|
return new Promise((resolve, reject) => {
|
|
3404
3296
|
const ws = new ws_1.default(`${wsUrl}/chat`, {
|
|
3405
3297
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -3428,7 +3320,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3428
3320
|
// Non-streaming alternative with callback
|
|
3429
3321
|
async chatWithCallback(messages, model, onChunk, onDone, onError) {
|
|
3430
3322
|
const wsUrl = this.config.get('wsUrl');
|
|
3431
|
-
const token = this.
|
|
3323
|
+
const token = this.getAccessToken();
|
|
3432
3324
|
return new Promise((resolve, reject) => {
|
|
3433
3325
|
const ws = new ws_1.default(`${wsUrl}/chat`, {
|
|
3434
3326
|
headers: { Authorization: `Bearer ${token}` },
|
|
@@ -3477,7 +3369,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3477
3369
|
// (/v1/chat/completions on api.vigthoria.io) which is the only
|
|
3478
3370
|
// backend that reliably accepts our auth token.
|
|
3479
3371
|
async chatComplete(systemPrompt, userPrompt, model, maxTokens) {
|
|
3480
|
-
const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-
|
|
3372
|
+
const resolvedModel = model ? this.resolvePermittedModelId(model) : 'vigthoria-v3-code-35b';
|
|
3481
3373
|
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
3482
3374
|
model: resolvedModel,
|
|
3483
3375
|
messages: [
|
|
@@ -3557,76 +3449,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3557
3449
|
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
3558
3450
|
*/
|
|
3559
3451
|
ensureBalancedBraces(code) {
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
let inStr = null;
|
|
3563
|
-
let inLine = false, inBlock = false;
|
|
3564
|
-
for (let i = 0; i < code.length; i++) {
|
|
3565
|
-
const ch = code[i], nx = code[i + 1] || '';
|
|
3566
|
-
if (inLine) {
|
|
3567
|
-
if (ch === '\n')
|
|
3568
|
-
inLine = false;
|
|
3569
|
-
continue;
|
|
3570
|
-
}
|
|
3571
|
-
if (inBlock) {
|
|
3572
|
-
if (ch === '*' && nx === '/') {
|
|
3573
|
-
inBlock = false;
|
|
3574
|
-
i++;
|
|
3575
|
-
}
|
|
3576
|
-
continue;
|
|
3577
|
-
}
|
|
3578
|
-
if (inStr) {
|
|
3579
|
-
if (ch === inStr && code[i - 1] !== '\\')
|
|
3580
|
-
inStr = null;
|
|
3581
|
-
continue;
|
|
3582
|
-
}
|
|
3583
|
-
if (ch === '/' && nx === '/') {
|
|
3584
|
-
inLine = true;
|
|
3585
|
-
continue;
|
|
3586
|
-
}
|
|
3587
|
-
if (ch === '/' && nx === '*') {
|
|
3588
|
-
inBlock = true;
|
|
3589
|
-
continue;
|
|
3590
|
-
}
|
|
3591
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
3592
|
-
inStr = ch;
|
|
3593
|
-
continue;
|
|
3594
|
-
}
|
|
3452
|
+
let depth = 0;
|
|
3453
|
+
for (const ch of code) {
|
|
3595
3454
|
if (ch === '{')
|
|
3596
|
-
|
|
3455
|
+
depth++;
|
|
3597
3456
|
else if (ch === '}')
|
|
3598
|
-
|
|
3599
|
-
else if (ch === '(')
|
|
3600
|
-
parens++;
|
|
3601
|
-
else if (ch === ')')
|
|
3602
|
-
parens--;
|
|
3603
|
-
else if (ch === '[')
|
|
3604
|
-
brackets++;
|
|
3605
|
-
else if (ch === ']')
|
|
3606
|
-
brackets--;
|
|
3607
|
-
}
|
|
3608
|
-
let result = code.trimEnd();
|
|
3609
|
-
for (let i = 0; i < braces; i++)
|
|
3610
|
-
result += '\n}';
|
|
3611
|
-
for (let i = 0; i < parens; i++)
|
|
3612
|
-
result += ')';
|
|
3613
|
-
for (let i = 0; i < brackets; i++)
|
|
3614
|
-
result += ']';
|
|
3615
|
-
return braces > 0 || parens > 0 || brackets > 0 ? result : code;
|
|
3616
|
-
}
|
|
3617
|
-
/**
|
|
3618
|
-
* Quick JS/TS syntax validation using Node's built-in parser.
|
|
3619
|
-
* Returns true if the code parses without errors.
|
|
3620
|
-
*/
|
|
3621
|
-
validateJsSyntax(code) {
|
|
3622
|
-
try {
|
|
3623
|
-
// Use Function constructor to check syntax without executing
|
|
3624
|
-
new Function(code);
|
|
3625
|
-
return true;
|
|
3457
|
+
depth--;
|
|
3626
3458
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3459
|
+
if (depth > 0) {
|
|
3460
|
+
let result = code.trimEnd();
|
|
3461
|
+
for (let i = 0; i < depth; i++) {
|
|
3462
|
+
result += '\n}';
|
|
3463
|
+
}
|
|
3464
|
+
code = result;
|
|
3629
3465
|
}
|
|
3466
|
+
return code;
|
|
3630
3467
|
}
|
|
3631
3468
|
/**
|
|
3632
3469
|
* Extract the first complete function/class from code.
|
|
@@ -3734,17 +3571,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3734
3571
|
}
|
|
3735
3572
|
}
|
|
3736
3573
|
async explainCode(code, language) {
|
|
3737
|
-
const sysPrompt =
|
|
3738
|
-
`You are a code explainer. Explain the following ${language} code clearly and concisely.`,
|
|
3739
|
-
'Focus on what it does, how it works, and any notable patterns or potential issues.',
|
|
3740
|
-
'Format your response as clean Markdown:',
|
|
3741
|
-
'- Use ## headers for major sections (e.g. ## Overview, ## How It Works, ## Key Details).',
|
|
3742
|
-
'- Use bullet points (- or *) for all lists. Do NOT use numbered lists.',
|
|
3743
|
-
'- Wrap code references in backticks.',
|
|
3744
|
-
'- Keep paragraphs short (2-3 sentences max).',
|
|
3745
|
-
'- Do NOT use raw HTML or excessive blank lines.',
|
|
3746
|
-
'- Do NOT nest numbered lists inside sections.',
|
|
3747
|
-
].join('\n');
|
|
3574
|
+
const sysPrompt = `You are a code explainer. Explain the following ${language} code clearly and concisely. Focus on what it does, how it works, and any notable patterns or potential issues.`;
|
|
3748
3575
|
return this.chatComplete(sysPrompt, code);
|
|
3749
3576
|
}
|
|
3750
3577
|
async reviewCode(code, language) {
|
|
@@ -3756,11 +3583,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3756
3583
|
'Rules:',
|
|
3757
3584
|
'- Return concrete, line-specific issues with severity.',
|
|
3758
3585
|
'- Every issue MUST reference a line number.',
|
|
3759
|
-
'-
|
|
3760
|
-
'- For trivial/short code (< 10 lines), report ONLY actual bugs. Do NOT pad with style, robustness, or best-practice suggestions.',
|
|
3761
|
-
'- 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.',
|
|
3586
|
+
'- If the score is below 50, list at least 2 specific issues.',
|
|
3762
3587
|
'- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
|
|
3763
|
-
'-
|
|
3588
|
+
'- Only report style issues AFTER listing all real bugs.',
|
|
3764
3589
|
'- Return ONLY the JSON object, no markdown fences or extra text.',
|
|
3765
3590
|
].join('\n');
|
|
3766
3591
|
let raw = {};
|
|
@@ -3775,40 +3600,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3775
3600
|
const score = typeof raw.score === 'number' ? raw.score : 0;
|
|
3776
3601
|
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
|
3777
3602
|
const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
3778
|
-
//
|
|
3779
|
-
//
|
|
3780
|
-
|
|
3603
|
+
// Always run client-side heuristics and merge any findings the
|
|
3604
|
+
// server missed. This ensures arithmetic/logic bugs are surfaced
|
|
3605
|
+
// even when the server only reports style issues like console.log.
|
|
3781
3606
|
const heuristic = this.heuristicCodeIssues(code, language);
|
|
3782
3607
|
for (const h of heuristic) {
|
|
3783
|
-
//
|
|
3784
|
-
//
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
const hWords = new Set(h.message.toLowerCase().split(/\W+/).filter(w => w.length > 3));
|
|
3791
|
-
const hTypeNorm = h.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
3792
|
-
const isSemanticallyDuplicate = issues.some((existing) => {
|
|
3793
|
-
if (existing.line !== h.line)
|
|
3794
|
-
return false;
|
|
3795
|
-
// Normalize types: "logic-error", "logic_error", "logic" all match
|
|
3796
|
-
const eTypeNorm = existing.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
3797
|
-
if (eTypeNorm === hTypeNorm || eTypeNorm.startsWith(hTypeNorm) || hTypeNorm.startsWith(eTypeNorm))
|
|
3798
|
-
return true;
|
|
3799
|
-
// Both errors on same line about the same category of problem
|
|
3800
|
-
if (existing.severity === 'error' && h.severity === 'error')
|
|
3801
|
-
return true;
|
|
3802
|
-
// Check keyword overlap — if ≥2 significant words match, it's the same finding
|
|
3803
|
-
const eWords = existing.message.toLowerCase().split(/\W+/).filter(w => w.length > 3);
|
|
3804
|
-
let overlap = 0;
|
|
3805
|
-
for (const w of eWords) {
|
|
3806
|
-
if (hWords.has(w))
|
|
3807
|
-
overlap++;
|
|
3608
|
+
// Always include critical logic bugs (severity error) from heuristics
|
|
3609
|
+
// regardless of server results — these catch wrong-operator bugs the
|
|
3610
|
+
// server frequently misses.
|
|
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);
|
|
3808
3615
|
}
|
|
3809
|
-
|
|
3810
|
-
}
|
|
3811
|
-
|
|
3616
|
+
continue;
|
|
3617
|
+
}
|
|
3618
|
+
// For non-critical heuristics, avoid duplicating issues the server
|
|
3619
|
+
// already reported on the same line with the same type.
|
|
3620
|
+
const isDuplicate = issues.some((existing) => existing.line === h.line && existing.type === h.type);
|
|
3621
|
+
if (!isDuplicate) {
|
|
3812
3622
|
issues.push(h);
|
|
3813
3623
|
}
|
|
3814
3624
|
}
|
|
@@ -3957,11 +3767,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3957
3767
|
const sysPrompt = [
|
|
3958
3768
|
`You are a ${language} code fixer. Fix the code for: ${fixType}.`,
|
|
3959
3769
|
'Return a JSON object with:',
|
|
3960
|
-
' "fixed": the
|
|
3770
|
+
' "fixed": the corrected code as a string,',
|
|
3961
3771
|
' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
|
|
3962
3772
|
'Rules:',
|
|
3963
|
-
'- The "fixed" field MUST contain the entire corrected source code with ALL lines, including unchanged lines.',
|
|
3964
|
-
'- The "fixed" code MUST have balanced braces, parentheses, and brackets.',
|
|
3965
3773
|
'- Fix ONLY the issues related to the fix type.',
|
|
3966
3774
|
'- Do not add comments, do not restructure beyond the minimal fix.',
|
|
3967
3775
|
'- Return ONLY the JSON object, no markdown fences.',
|
|
@@ -4001,26 +3809,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4001
3809
|
if (fixType === 'syntax' && fixed !== code) {
|
|
4002
3810
|
fixed = this.repairBracketBalance(code, fixed);
|
|
4003
3811
|
}
|
|
4004
|
-
// Final bracket-balance guarantee — ensure the emitted code has
|
|
4005
|
-
// balanced braces/parens/brackets regardless of what the model returned.
|
|
4006
|
-
fixed = this.ensureBalancedBraces(fixed);
|
|
4007
|
-
// For JS/TS syntax fixes, validate the output actually parses.
|
|
4008
|
-
// If it doesn't, attempt a more aggressive bracket repair.
|
|
4009
|
-
if ((fixType === 'syntax' || fixType === 'bugs') && fixed !== code) {
|
|
4010
|
-
const lang = language.toLowerCase();
|
|
4011
|
-
if (['javascript', 'js', 'typescript', 'ts'].includes(lang)) {
|
|
4012
|
-
if (!this.validateJsSyntax(fixed)) {
|
|
4013
|
-
// Try once more: strip any remaining injected comments and re-balance
|
|
4014
|
-
let repaired = this.stripInjectedComments(code, fixed, language);
|
|
4015
|
-
repaired = this.ensureBalancedBraces(repaired);
|
|
4016
|
-
if (this.validateJsSyntax(repaired)) {
|
|
4017
|
-
fixed = repaired;
|
|
4018
|
-
}
|
|
4019
|
-
// If still invalid, return the best-effort fix — better than
|
|
4020
|
-
// silently reverting to the original broken code.
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
3812
|
// If there are still no changes but the fixed code differs, compute
|
|
4025
3813
|
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
4026
3814
|
// every subsequent line to appear as changed.
|
|
@@ -4322,23 +4110,116 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4322
4110
|
}
|
|
4323
4111
|
// Model resolution - maps Vigthoria model names to internal IDs
|
|
4324
4112
|
// 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
|
+
}
|
|
4325
4203
|
resolveModelId(shortName) {
|
|
4326
4204
|
const modelMap = {
|
|
4327
4205
|
// ═══════════════════════════════════════════════════════════════
|
|
4328
4206
|
// VIGTHORIA LOCAL - Self-hosted models
|
|
4329
4207
|
// ═══════════════════════════════════════════════════════════════
|
|
4330
|
-
'fast': 'vigthoria-
|
|
4331
|
-
'mini': 'vigthoria-
|
|
4332
|
-
'balanced': '
|
|
4333
|
-
'
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
'code
|
|
4337
|
-
'code-
|
|
4338
|
-
'
|
|
4339
|
-
'
|
|
4340
|
-
'
|
|
4341
|
-
'
|
|
4208
|
+
'fast': 'vigthoria-v3-code-35b',
|
|
4209
|
+
'mini': 'vigthoria-v3-code-35b',
|
|
4210
|
+
'balanced': 'vigthoria_master',
|
|
4211
|
+
'balanced-4b': 'vigthoria-balanced-4b',
|
|
4212
|
+
'creative': 'vigthoria-v3-code-35b',
|
|
4213
|
+
// Code models
|
|
4214
|
+
'code': 'vigthoria-v3-code-35b',
|
|
4215
|
+
'code-30b': 'vigthoria-v3-code-35b',
|
|
4216
|
+
'code-35b': 'vigthoria-v3-code-35b',
|
|
4217
|
+
'code-8b': 'vigthoria_c1_m',
|
|
4218
|
+
'code-9b': 'vigthoria_c1_m',
|
|
4219
|
+
'pro': 'vigthoria-v3-code-35b',
|
|
4220
|
+
'agent': 'vigthoria-v3-code-35b',
|
|
4221
|
+
'vigthoria-code': 'vigthoria-v3-code-35b',
|
|
4222
|
+
'vigthoria-agent': 'vigthoria-v3-code-35b',
|
|
4342
4223
|
// ═══════════════════════════════════════════════════════════════
|
|
4343
4224
|
// VIGTHORIA CLOUD - Premium cloud models (internal routing)
|
|
4344
4225
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -4346,18 +4227,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4346
4227
|
'cloud-reason': 'vigthoria-cloud-k2',
|
|
4347
4228
|
'ultra': 'vigthoria-cloud-ultra',
|
|
4348
4229
|
};
|
|
4349
|
-
// If already a full model ID, return as-is
|
|
4350
4230
|
if (shortName.includes('vigthoria') || shortName.includes('/') || shortName.includes(':')) {
|
|
4351
4231
|
if (modelMap[shortName]) {
|
|
4352
4232
|
return modelMap[shortName];
|
|
4353
4233
|
}
|
|
4354
4234
|
return shortName;
|
|
4355
4235
|
}
|
|
4356
|
-
return modelMap[shortName] || 'vigthoria-v3-code-
|
|
4236
|
+
return modelMap[shortName] || 'vigthoria-v3-code-35b';
|
|
4357
4237
|
}
|
|
4358
4238
|
async getCoderHealth() {
|
|
4359
4239
|
try {
|
|
4360
|
-
const response = await this.client.get('/api/health', { timeout:
|
|
4240
|
+
const response = await this.client.get('/api/health', { timeout: 10000 });
|
|
4361
4241
|
const ok = response.data?.status === 'ok' || response.data?.healthy === true;
|
|
4362
4242
|
return {
|
|
4363
4243
|
name: 'Coder API',
|
|
@@ -4379,8 +4259,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4379
4259
|
const modelsApiUrl = this.config.get('modelsApiUrl');
|
|
4380
4260
|
try {
|
|
4381
4261
|
const [healthResponse, modelsResponse] = await Promise.all([
|
|
4382
|
-
this.modelRouterClient.get('/health', { timeout:
|
|
4383
|
-
this.modelRouterClient.get('/v1/models', { timeout:
|
|
4262
|
+
this.modelRouterClient.get('/health', { timeout: 10000 }),
|
|
4263
|
+
this.modelRouterClient.get('/v1/models', { timeout: 15000 }),
|
|
4384
4264
|
]);
|
|
4385
4265
|
const healthOk = healthResponse.data?.status === 'healthy'
|
|
4386
4266
|
|| healthResponse.data?.status === 'ok'
|
|
@@ -4411,7 +4291,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4411
4291
|
return null;
|
|
4412
4292
|
}
|
|
4413
4293
|
try {
|
|
4414
|
-
const response = await this.selfHostedModelRouterClient.get('/health', { timeout:
|
|
4294
|
+
const response = await this.selfHostedModelRouterClient.get('/health', { timeout: 10000 });
|
|
4415
4295
|
const ok = response.data?.status === 'healthy'
|
|
4416
4296
|
|| response.data?.status === 'ok'
|
|
4417
4297
|
|| response.data?.healthy === true;
|
|
@@ -4431,6 +4311,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4431
4311
|
};
|
|
4432
4312
|
}
|
|
4433
4313
|
}
|
|
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
|
+
}
|
|
4434
4337
|
async getV3AgentHealth() {
|
|
4435
4338
|
const baseUrl = this.getV3AgentBaseUrls()[0];
|
|
4436
4339
|
// Try multiple health endpoint patterns — the V3 backend may expose
|
|
@@ -4444,7 +4347,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4444
4347
|
for (const endpoint of candidates) {
|
|
4445
4348
|
try {
|
|
4446
4349
|
const controller = new AbortController();
|
|
4447
|
-
const timer = setTimeout(() => controller.abort(),
|
|
4350
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
4448
4351
|
const response = await fetch(endpoint, {
|
|
4449
4352
|
method: 'GET',
|
|
4450
4353
|
headers,
|
|
@@ -4492,7 +4395,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4492
4395
|
const runUrl = this.getV3AgentRunUrl(baseUrl);
|
|
4493
4396
|
try {
|
|
4494
4397
|
const controller = new AbortController();
|
|
4495
|
-
const timer = setTimeout(() => controller.abort(),
|
|
4398
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
4496
4399
|
const probe = await fetch(runUrl, { method: 'OPTIONS', headers, signal: controller.signal });
|
|
4497
4400
|
clearTimeout(timer);
|
|
4498
4401
|
if (probe.ok || probe.status === 204 || probe.status === 405) {
|
|
@@ -4649,18 +4552,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4649
4552
|
});
|
|
4650
4553
|
}
|
|
4651
4554
|
async getCapabilityTruthStatus(context = {}) {
|
|
4652
|
-
// Wrap each probe with its own 6 s timeout so they always resolve
|
|
4653
|
-
// before the outer 8 s race in auth.ts, producing real error messages
|
|
4654
|
-
// (ECONNREFUSED, 404, etc.) instead of the generic "Timed out (8s)".
|
|
4655
|
-
const withTimeout = (p, name) => Promise.race([
|
|
4656
|
-
p,
|
|
4657
|
-
new Promise(resolve => setTimeout(() => resolve({ name, endpoint: '', ok: false, error: 'Service not reachable (6 s timeout)' }), 6000)),
|
|
4658
|
-
]);
|
|
4659
4555
|
const [v3Agent, hyperLoop, repoMemory, devtoolsBridge] = await Promise.all([
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4556
|
+
this.getV3AgentHealth(),
|
|
4557
|
+
this.getHyperLoopHealth(),
|
|
4558
|
+
this.getRepoMemoryHealth(context),
|
|
4559
|
+
this.getDevtoolsBridgeStatus(),
|
|
4664
4560
|
]);
|
|
4665
4561
|
return {
|
|
4666
4562
|
overallOk: v3Agent.ok && hyperLoop.ok && repoMemory.ok,
|