vigthoria-cli 1.9.2 → 1.9.8

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