vigthoria-cli 1.8.15 → 1.8.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/chat.d.ts +1 -2
- package/dist/commands/chat.js +107 -60
- package/dist/index.js +146 -8
- package/dist/utils/api.d.ts +13 -17
- package/dist/utils/api.js +170 -746
- package/package.json +1 -1
package/dist/utils/api.js
CHANGED
|
@@ -20,7 +20,6 @@ const https_1 = __importDefault(require("https"));
|
|
|
20
20
|
const net_1 = __importDefault(require("net"));
|
|
21
21
|
const path_1 = __importDefault(require("path"));
|
|
22
22
|
const ws_1 = __importDefault(require("ws"));
|
|
23
|
-
const workspace_stream_js_1 = require("./workspace-stream.js");
|
|
24
23
|
class CLIError extends Error {
|
|
25
24
|
category;
|
|
26
25
|
statusCode;
|
|
@@ -92,36 +91,7 @@ function formatCLIError(err) {
|
|
|
92
91
|
return `${tag} ${err.message}`;
|
|
93
92
|
}
|
|
94
93
|
}
|
|
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
94
|
// 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
95
|
function sanitizeUserFacingErrorText(input) {
|
|
126
96
|
if (!input)
|
|
127
97
|
return '';
|
|
@@ -131,19 +101,13 @@ function sanitizeUserFacingErrorText(input) {
|
|
|
131
101
|
out = out.replace(/\b(?:localhost|127\.0\.0\.1)(?::\d+)?\b/gi, '[redacted-host]');
|
|
132
102
|
out = out.replace(/\b[a-z0-9.-]+\.vigthoria\.io\b/gi, '[redacted-host]');
|
|
133
103
|
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
104
|
out = out.replace(/[A-Za-z]:\\[^\s'"<>)]+/g, '[redacted-path]');
|
|
136
|
-
// UNC paths (\\server\share\...).
|
|
137
105
|
out = out.replace(/\\\\[^\s'"<>)]+/g, '[redacted-path]');
|
|
138
|
-
out = out.replace(/\{\s*"detail"\s*:\s*"[^"]*"\s*\}/g, '');
|
|
139
106
|
out = out.replace(/\s+/g, ' ').trim();
|
|
140
107
|
if (out.length > 160)
|
|
141
108
|
out = out.slice(0, 160) + '...';
|
|
142
109
|
return out;
|
|
143
110
|
}
|
|
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
111
|
function isServerRuntime() {
|
|
148
112
|
if (process.env.VIGTHORIA_RUN_MODE === 'server')
|
|
149
113
|
return true;
|
|
@@ -166,6 +130,21 @@ function describeUpstreamStatus(status) {
|
|
|
166
130
|
return 'Request was rejected by the service.';
|
|
167
131
|
return 'Unexpected response from service.';
|
|
168
132
|
}
|
|
133
|
+
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
134
|
+
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '1200000';
|
|
135
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
136
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1200000;
|
|
137
|
+
})();
|
|
138
|
+
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
139
|
+
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS || '90000';
|
|
140
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
141
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
|
|
142
|
+
})();
|
|
143
|
+
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
144
|
+
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '300000';
|
|
145
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
146
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
|
|
147
|
+
})();
|
|
169
148
|
class APIClient {
|
|
170
149
|
client;
|
|
171
150
|
modelRouterClient;
|
|
@@ -174,7 +153,6 @@ class APIClient {
|
|
|
174
153
|
logger;
|
|
175
154
|
ws = null;
|
|
176
155
|
vigFlowTokens = new Map();
|
|
177
|
-
_httpsAgent = null;
|
|
178
156
|
constructor(config, logger) {
|
|
179
157
|
this.config = config;
|
|
180
158
|
this.logger = logger;
|
|
@@ -184,7 +162,6 @@ class APIClient {
|
|
|
184
162
|
keepAlive: true,
|
|
185
163
|
timeout: 30000,
|
|
186
164
|
});
|
|
187
|
-
this._httpsAgent = httpsAgent;
|
|
188
165
|
// Main Vigthoria Coder API (coder.vigthoria.io)
|
|
189
166
|
this.client = axios_1.default.create({
|
|
190
167
|
baseURL: config.get('apiUrl'),
|
|
@@ -211,7 +188,6 @@ class APIClient {
|
|
|
211
188
|
this.selfHostedModelRouterClient = selfHostedModelsApiUrl ? axios_1.default.create({
|
|
212
189
|
baseURL: selfHostedModelsApiUrl,
|
|
213
190
|
timeout: 240000,
|
|
214
|
-
httpsAgent,
|
|
215
191
|
headers: {
|
|
216
192
|
'Content-Type': 'application/json',
|
|
217
193
|
'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
|
|
@@ -260,22 +236,12 @@ class APIClient {
|
|
|
260
236
|
createAuthRetryInterceptor(this.selfHostedModelRouterClient);
|
|
261
237
|
}
|
|
262
238
|
}
|
|
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
239
|
destroy() {
|
|
270
|
-
if (this._httpsAgent) {
|
|
271
|
-
this._httpsAgent.destroy();
|
|
272
|
-
this._httpsAgent = null;
|
|
273
|
-
}
|
|
274
240
|
if (this.ws) {
|
|
275
241
|
try {
|
|
276
242
|
this.ws.close();
|
|
277
243
|
}
|
|
278
|
-
catch {
|
|
244
|
+
catch { }
|
|
279
245
|
this.ws = null;
|
|
280
246
|
}
|
|
281
247
|
}
|
|
@@ -355,6 +321,25 @@ class APIClient {
|
|
|
355
321
|
}
|
|
356
322
|
}
|
|
357
323
|
}
|
|
324
|
+
// All profile endpoints failed — fall back to JWT payload claims so the
|
|
325
|
+
// token is still usable without identity fields being null.
|
|
326
|
+
const jwtPayload = this.decodeJwtPayload(token);
|
|
327
|
+
const fallbackId = String(jwtPayload?.sub || jwtPayload?.user_id || jwtPayload?.id || '').trim();
|
|
328
|
+
const fallbackEmail = String(jwtPayload?.email || '').trim();
|
|
329
|
+
const fallbackPlan = String(jwtPayload?.plan || jwtPayload?.subscription_plan || 'developer').trim();
|
|
330
|
+
if (fallbackId || fallbackEmail) {
|
|
331
|
+
this.config.setAuth({
|
|
332
|
+
token,
|
|
333
|
+
userId: fallbackId || fallbackEmail,
|
|
334
|
+
email: fallbackEmail || fallbackId,
|
|
335
|
+
});
|
|
336
|
+
this.config.setSubscription({
|
|
337
|
+
plan: fallbackPlan,
|
|
338
|
+
status: 'active',
|
|
339
|
+
expiresAt: undefined,
|
|
340
|
+
});
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
358
343
|
this.config.clearAuth();
|
|
359
344
|
return false;
|
|
360
345
|
}
|
|
@@ -364,6 +349,19 @@ class APIClient {
|
|
|
364
349
|
return false;
|
|
365
350
|
}
|
|
366
351
|
}
|
|
352
|
+
decodeJwtPayload(token) {
|
|
353
|
+
try {
|
|
354
|
+
const parts = token.split('.');
|
|
355
|
+
if (parts.length !== 3) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const payload = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
359
|
+
return JSON.parse(payload);
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
367
365
|
extractUserProfile(data) {
|
|
368
366
|
if (!data) {
|
|
369
367
|
return null;
|
|
@@ -437,35 +435,40 @@ class APIClient {
|
|
|
437
435
|
if (!token) {
|
|
438
436
|
return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
|
|
439
437
|
}
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
this.
|
|
446
|
-
|
|
447
|
-
for (const r of results) {
|
|
448
|
-
if (r.status === 'fulfilled')
|
|
449
|
-
return { valid: true };
|
|
438
|
+
// Verify auth against the Model Router (api.vigthoria.io) which is
|
|
439
|
+
// the backend all AI commands actually use. Falls back to the Coder
|
|
440
|
+
// profile endpoint when the Model Router is unreachable so that
|
|
441
|
+
// offline/degraded scenarios don't block the user.
|
|
442
|
+
try {
|
|
443
|
+
await this.modelRouterClient.get('/v1/models', { timeout: 10000 });
|
|
444
|
+
return { valid: true };
|
|
450
445
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
446
|
+
catch (mrError) {
|
|
447
|
+
const mrAxErr = mrError;
|
|
448
|
+
if (mrAxErr.response?.status === 401 || mrAxErr.response?.status === 403) {
|
|
449
|
+
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
450
|
+
}
|
|
451
|
+
// Model Router unreachable — try Coder profile as fallback
|
|
452
|
+
try {
|
|
453
|
+
await this.client.get('/api/user/profile', { timeout: 10000 });
|
|
454
|
+
return { valid: true };
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
if (error instanceof CLIError && error.category === 'auth') {
|
|
456
458
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
457
459
|
}
|
|
458
|
-
|
|
460
|
+
const axErr = error;
|
|
461
|
+
if (axErr.response?.status === 401 || axErr.response?.status === 403) {
|
|
459
462
|
return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
|
|
460
463
|
}
|
|
464
|
+
// Both unreachable — don't assume token is bad
|
|
465
|
+
return { valid: true };
|
|
461
466
|
}
|
|
462
467
|
}
|
|
463
|
-
// Both unreachable — don't assume token is bad
|
|
464
|
-
return { valid: true };
|
|
465
468
|
}
|
|
466
469
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
467
470
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
468
|
-
const allowLocalV3Agent =
|
|
471
|
+
const allowLocalV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1' || preferLocal;
|
|
469
472
|
const urls = [
|
|
470
473
|
process.env.VIGTHORIA_V3_AGENT_URL,
|
|
471
474
|
process.env.V3_AGENT_URL,
|
|
@@ -488,12 +491,11 @@ class APIClient {
|
|
|
488
491
|
}
|
|
489
492
|
getOperatorBaseUrls() {
|
|
490
493
|
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
491
|
-
const allowLocal = isServerRuntime() && process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
492
494
|
const urls = [
|
|
493
495
|
process.env.VIGTHORIA_OPERATOR_URL,
|
|
494
496
|
process.env.OPERATOR_URL,
|
|
497
|
+
'http://127.0.0.1:4009',
|
|
495
498
|
configuredModelsApiUrl,
|
|
496
|
-
...(allowLocal ? ['http://127.0.0.1:4009'] : []),
|
|
497
499
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
498
500
|
return [...new Set(urls)];
|
|
499
501
|
}
|
|
@@ -502,35 +504,39 @@ class APIClient {
|
|
|
502
504
|
}
|
|
503
505
|
getMcpBaseUrls() {
|
|
504
506
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
505
|
-
const allowLocal = isServerRuntime() && process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
506
507
|
const urls = [
|
|
507
508
|
process.env.VIGTHORIA_MCP_URL,
|
|
508
509
|
process.env.MCP_SERVER_URL,
|
|
510
|
+
'http://127.0.0.1:4008',
|
|
509
511
|
configuredApiUrl,
|
|
510
|
-
...(allowLocal ? ['http://127.0.0.1:4008'] : []),
|
|
511
512
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
512
513
|
return [...new Set(urls)];
|
|
513
514
|
}
|
|
514
515
|
getVigFlowBaseUrls() {
|
|
515
516
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
516
|
-
|
|
517
|
+
// Put the remote gateway first, since local VigFlow servers are
|
|
518
|
+
// rarely running for end-user CLI installations. This avoids
|
|
519
|
+
// wasting connection-attempt time on 127.0.0.1 and hitting the
|
|
520
|
+
// remote gateway only after the local attempts have already
|
|
521
|
+
// errored — which surfaces as a confusing "last error" 404 in
|
|
522
|
+
// some setups.
|
|
517
523
|
const urls = [
|
|
518
524
|
process.env.VIGTHORIA_VIGFLOW_URL,
|
|
519
525
|
process.env.VIGFLOW_URL,
|
|
520
526
|
process.env.WORKFLOW_BUILDER_URL,
|
|
521
527
|
`${configuredApiUrl}/api/vigflow`,
|
|
522
|
-
|
|
528
|
+
'http://127.0.0.1:5060',
|
|
529
|
+
'http://127.0.0.1:5050',
|
|
523
530
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
524
531
|
return [...new Set(urls)];
|
|
525
532
|
}
|
|
526
533
|
getTemplateServiceBaseUrls() {
|
|
527
534
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
528
|
-
const allowLocal = isServerRuntime() && process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
|
|
529
535
|
const urls = [
|
|
530
536
|
process.env.VIGTHORIA_TEMPLATE_SERVICE_URL,
|
|
531
537
|
process.env.TEMPLATE_SERVICE_URL,
|
|
538
|
+
'http://127.0.0.1:4011',
|
|
532
539
|
`${configuredApiUrl}/api/template-service`,
|
|
533
|
-
...(allowLocal ? ['http://127.0.0.1:4011'] : []),
|
|
534
540
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
535
541
|
return [...new Set(urls)];
|
|
536
542
|
}
|
|
@@ -855,7 +861,7 @@ class APIClient {
|
|
|
855
861
|
});
|
|
856
862
|
if (!response.ok) {
|
|
857
863
|
const errorText = await response.text().catch(() => '');
|
|
858
|
-
throw new Error(`Template preview proof ${response.status}: ${
|
|
864
|
+
throw new Error(`Template preview proof ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
859
865
|
}
|
|
860
866
|
const payload = await response.json();
|
|
861
867
|
const modes = payload?.modes || {};
|
|
@@ -999,8 +1005,7 @@ class APIClient {
|
|
|
999
1005
|
this.logger.debug(`VigFlow ${operation} via ${baseUrl} failed:`, lastError.message);
|
|
1000
1006
|
}
|
|
1001
1007
|
}
|
|
1002
|
-
|
|
1003
|
-
throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
|
|
1008
|
+
throw lastError || new Error(`No VigFlow backend available for ${operation}.`);
|
|
1004
1009
|
}
|
|
1005
1010
|
/**
|
|
1006
1011
|
* Build the correct sub-path for VigFlow endpoints.
|
|
@@ -1292,458 +1297,6 @@ class APIClient {
|
|
|
1292
1297
|
const match = String(message || '').match(/called\s+([A-Z][A-Za-z0-9&\- ]{2,40})/i);
|
|
1293
1298
|
return match?.[1]?.trim() || fallback;
|
|
1294
1299
|
}
|
|
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
1300
|
ensureExecutionContext(context = {}) {
|
|
1748
1301
|
const existingId = String(context.contextId || context.traceId || '').trim();
|
|
1749
1302
|
const contextId = existingId || `vig-${Date.now()}-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
|
|
@@ -1796,7 +1349,7 @@ menu {
|
|
|
1796
1349
|
});
|
|
1797
1350
|
if (!response.ok) {
|
|
1798
1351
|
const errorText = await response.text().catch(() => '');
|
|
1799
|
-
throw new Error(`MCP context update ${response.status}: ${
|
|
1352
|
+
throw new Error(`MCP context update ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
1800
1353
|
}
|
|
1801
1354
|
return {
|
|
1802
1355
|
...executionContext,
|
|
@@ -1827,7 +1380,7 @@ menu {
|
|
|
1827
1380
|
});
|
|
1828
1381
|
if (!createResponse.ok) {
|
|
1829
1382
|
const errorText = await createResponse.text().catch(() => '');
|
|
1830
|
-
throw new Error(`MCP context create ${createResponse.status}: ${
|
|
1383
|
+
throw new Error(`MCP context create ${createResponse.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
1831
1384
|
}
|
|
1832
1385
|
const payload = await createResponse.json();
|
|
1833
1386
|
const mcpContextId = String(payload.contextId || '').trim();
|
|
@@ -2184,11 +1737,6 @@ menu {
|
|
|
2184
1737
|
if (!looksLikeFrontendTask) {
|
|
2185
1738
|
return;
|
|
2186
1739
|
}
|
|
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
1740
|
const htmlPath = path_1.default.join(rootPath, 'index.html');
|
|
2193
1741
|
if (!fs_1.default.existsSync(htmlPath)) {
|
|
2194
1742
|
return;
|
|
@@ -2635,7 +2183,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2635
2183
|
let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
|
|
2636
2184
|
let serverWorkspaceRoot = null;
|
|
2637
2185
|
const streamedFiles = {};
|
|
2638
|
-
const idleTimeoutMs = context.agentIdleTimeoutMs
|
|
2186
|
+
const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
|
|
2639
2187
|
while (true) {
|
|
2640
2188
|
let chunk;
|
|
2641
2189
|
try {
|
|
@@ -2704,19 +2252,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2704
2252
|
serverWorkspaceRoot = event.workspace_root.trim();
|
|
2705
2253
|
}
|
|
2706
2254
|
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
2255
|
// Empty workspace guard: if the remote agent lists its root
|
|
2721
2256
|
// and finds nothing while our local workspace has files, the
|
|
2722
2257
|
// workspace was not hydrated. Abort early with a clear error
|
|
@@ -2773,7 +2308,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2773
2308
|
partial: true,
|
|
2774
2309
|
};
|
|
2775
2310
|
}
|
|
2776
|
-
throw new Error(
|
|
2311
|
+
throw new Error(event.message || 'V3 agent returned an error');
|
|
2777
2312
|
}
|
|
2778
2313
|
if (event.type === 'complete' || event.type === 'message') {
|
|
2779
2314
|
final = event;
|
|
@@ -2791,13 +2326,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2791
2326
|
}
|
|
2792
2327
|
async runV3AgentWorkflow(message, context = {}) {
|
|
2793
2328
|
const executionContext = await this.bindExecutionContext(context);
|
|
2794
|
-
const baseTimeoutMs = executionContext.agentTimeoutMs
|
|
2329
|
+
const baseTimeoutMs = executionContext.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
|
|
2795
2330
|
const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
|
|
2796
2331
|
const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
|
|
2797
2332
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
2798
2333
|
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
2334
|
&& context.localMachineCapable !== false;
|
|
2800
|
-
const
|
|
2335
|
+
const rescueEligibleSaaS = preferLocalV3
|
|
2336
|
+
&& /(saas|dashboard|analytics|billing|team management|activity feed|login screen)/i.test(message);
|
|
2337
|
+
const timeoutMs = rescueEligibleSaaS ? Math.min(baseTimeoutMs, 210000) : baseTimeoutMs;
|
|
2801
2338
|
const maxAttempts = preferLocalV3 ? 2 : 1;
|
|
2802
2339
|
let lastErrors = [];
|
|
2803
2340
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
@@ -2831,12 +2368,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2831
2368
|
};
|
|
2832
2369
|
for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
|
|
2833
2370
|
const controller = new AbortController();
|
|
2834
|
-
const timeoutId =
|
|
2371
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2835
2372
|
try {
|
|
2836
2373
|
const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, requestExecutionContext, controller.signal);
|
|
2374
|
+
clearTimeout(timeoutId);
|
|
2837
2375
|
if (!response.ok) {
|
|
2838
2376
|
const errorText = await response.text().catch(() => '');
|
|
2839
|
-
throw new Error(`V3 agent ${response.status}: ${
|
|
2377
|
+
throw new Error(`V3 agent ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
2840
2378
|
}
|
|
2841
2379
|
const data = await this.collectV3AgentStream(response, requestExecutionContext);
|
|
2842
2380
|
// Auto-continuation: if the agent checkpointed (budget exceeded), continue automatically
|
|
@@ -2863,7 +2401,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2863
2401
|
stream: true,
|
|
2864
2402
|
};
|
|
2865
2403
|
const continueController = new AbortController();
|
|
2866
|
-
const continueTimeoutId =
|
|
2404
|
+
const continueTimeoutId = setTimeout(() => continueController.abort(), timeoutMs);
|
|
2867
2405
|
try {
|
|
2868
2406
|
const continueHeaders = await this.getV3AgentHeaders();
|
|
2869
2407
|
const continueResponse = await fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
@@ -2872,6 +2410,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2872
2410
|
body: JSON.stringify(continueBody),
|
|
2873
2411
|
signal: continueController.signal,
|
|
2874
2412
|
});
|
|
2413
|
+
clearTimeout(continueTimeoutId);
|
|
2875
2414
|
if (!continueResponse.ok) {
|
|
2876
2415
|
break; // Fall through to normal completion with partial data
|
|
2877
2416
|
}
|
|
@@ -2881,9 +2420,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2881
2420
|
break; // Fall through to normal completion with partial data
|
|
2882
2421
|
}
|
|
2883
2422
|
finally {
|
|
2884
|
-
|
|
2885
|
-
clearTimeout(continueTimeoutId);
|
|
2886
|
-
}
|
|
2423
|
+
clearTimeout(continueTimeoutId);
|
|
2887
2424
|
}
|
|
2888
2425
|
}
|
|
2889
2426
|
// Use the final continuation data for workspace recovery
|
|
@@ -2898,7 +2435,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2898
2435
|
contextId: finalContextId,
|
|
2899
2436
|
backendUrl: baseUrl,
|
|
2900
2437
|
partial: continuationData.checkpointed === true,
|
|
2901
|
-
changedFiles: continuationData.files || data.files || {},
|
|
2902
2438
|
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
2903
2439
|
};
|
|
2904
2440
|
}
|
|
@@ -2917,7 +2453,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2917
2453
|
taskId: data.task_id || null,
|
|
2918
2454
|
contextId,
|
|
2919
2455
|
backendUrl: baseUrl,
|
|
2920
|
-
changedFiles: data.files || {},
|
|
2921
2456
|
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
2922
2457
|
};
|
|
2923
2458
|
}
|
|
@@ -2933,16 +2468,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2933
2468
|
contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null,
|
|
2934
2469
|
backendUrl: baseUrl,
|
|
2935
2470
|
partial: true,
|
|
2936
|
-
changedFiles: error.partialData.files || {},
|
|
2937
2471
|
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
2472
|
};
|
|
2939
2473
|
}
|
|
2940
2474
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
2941
2475
|
}
|
|
2942
2476
|
finally {
|
|
2943
|
-
|
|
2944
|
-
clearTimeout(timeoutId);
|
|
2945
|
-
}
|
|
2477
|
+
clearTimeout(timeoutId);
|
|
2946
2478
|
}
|
|
2947
2479
|
}
|
|
2948
2480
|
lastErrors = errors;
|
|
@@ -2964,24 +2496,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
2964
2496
|
this.config.clearAuth();
|
|
2965
2497
|
throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
|
|
2966
2498
|
}
|
|
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
2499
|
throw new Error(errors.join(' | '));
|
|
2986
2500
|
}
|
|
2987
2501
|
formatOperatorResponse(data = {}) {
|
|
@@ -3009,7 +2523,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3009
2523
|
}
|
|
3010
2524
|
async runOperatorWorkflow(message, context = {}) {
|
|
3011
2525
|
const executionContext = await this.bindExecutionContext(context);
|
|
3012
|
-
const timeoutMs = context.operatorTimeoutMs
|
|
2526
|
+
const timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
|
|
3013
2527
|
const errors = [];
|
|
3014
2528
|
const authToken = this.config.get('authToken');
|
|
3015
2529
|
// Collect a lightweight workspace file listing so the operator can
|
|
@@ -3018,7 +2532,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3018
2532
|
const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
3019
2533
|
for (const baseUrl of this.getOperatorBaseUrls()) {
|
|
3020
2534
|
const controller = new AbortController();
|
|
3021
|
-
const timeoutId =
|
|
2535
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
3022
2536
|
try {
|
|
3023
2537
|
const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
|
|
3024
2538
|
method: 'POST',
|
|
@@ -3042,7 +2556,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3042
2556
|
workspace: { path: workspacePath },
|
|
3043
2557
|
workspace_path: workspacePath,
|
|
3044
2558
|
workspace_summary: workspaceSummary,
|
|
3045
|
-
model: this.resolveModelId(executionContext.model || 'code'),
|
|
2559
|
+
model: this.resolveModelId(executionContext.model || 'code-8b'),
|
|
3046
2560
|
history: executionContext.history || [],
|
|
3047
2561
|
executionSurface: executionContext.executionSurface || 'cli',
|
|
3048
2562
|
clientSurface: executionContext.clientSurface || 'cli',
|
|
@@ -3053,7 +2567,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3053
2567
|
rawPrompt: executionContext.rawPrompt || null,
|
|
3054
2568
|
requestStartedAt: executionContext.requestStartedAt,
|
|
3055
2569
|
},
|
|
3056
|
-
workflow_type: executionContext.workflowType || '
|
|
2570
|
+
workflow_type: executionContext.workflowType || 'analysis_only',
|
|
3057
2571
|
options: {
|
|
3058
2572
|
stream: true,
|
|
3059
2573
|
save_to_vigflow: executionContext.savePlanToVigFlow === true,
|
|
@@ -3063,7 +2577,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3063
2577
|
});
|
|
3064
2578
|
if (!response.ok) {
|
|
3065
2579
|
const errorText = await response.text().catch(() => '');
|
|
3066
|
-
throw new Error(`Operator stream ${response.status}: ${
|
|
2580
|
+
throw new Error(`Operator stream ${response.status}: ${sanitizeUserFacingErrorText(errorText)}`);
|
|
3067
2581
|
}
|
|
3068
2582
|
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
3069
2583
|
const fallbackData = await response.json();
|
|
@@ -3165,9 +2679,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3165
2679
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
3166
2680
|
}
|
|
3167
2681
|
finally {
|
|
3168
|
-
|
|
3169
|
-
clearTimeout(timeoutId);
|
|
3170
|
-
}
|
|
2682
|
+
clearTimeout(timeoutId);
|
|
3171
2683
|
}
|
|
3172
2684
|
}
|
|
3173
2685
|
throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
|
|
@@ -3557,76 +3069,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3557
3069
|
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
3558
3070
|
*/
|
|
3559
3071
|
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
|
-
}
|
|
3072
|
+
let depth = 0;
|
|
3073
|
+
for (const ch of code) {
|
|
3595
3074
|
if (ch === '{')
|
|
3596
|
-
|
|
3075
|
+
depth++;
|
|
3597
3076
|
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;
|
|
3077
|
+
depth--;
|
|
3626
3078
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3079
|
+
if (depth > 0) {
|
|
3080
|
+
let result = code.trimEnd();
|
|
3081
|
+
for (let i = 0; i < depth; i++) {
|
|
3082
|
+
result += '\n}';
|
|
3083
|
+
}
|
|
3084
|
+
code = result;
|
|
3629
3085
|
}
|
|
3086
|
+
return code;
|
|
3630
3087
|
}
|
|
3631
3088
|
/**
|
|
3632
3089
|
* Extract the first complete function/class from code.
|
|
@@ -3734,17 +3191,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3734
3191
|
}
|
|
3735
3192
|
}
|
|
3736
3193
|
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');
|
|
3194
|
+
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
3195
|
return this.chatComplete(sysPrompt, code);
|
|
3749
3196
|
}
|
|
3750
3197
|
async reviewCode(code, language) {
|
|
@@ -3756,11 +3203,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3756
3203
|
'Rules:',
|
|
3757
3204
|
'- Return concrete, line-specific issues with severity.',
|
|
3758
3205
|
'- 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.',
|
|
3206
|
+
'- If the score is below 50, list at least 2 specific issues.',
|
|
3762
3207
|
'- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
|
|
3763
|
-
'-
|
|
3208
|
+
'- Only report style issues AFTER listing all real bugs.',
|
|
3764
3209
|
'- Return ONLY the JSON object, no markdown fences or extra text.',
|
|
3765
3210
|
].join('\n');
|
|
3766
3211
|
let raw = {};
|
|
@@ -3775,40 +3220,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3775
3220
|
const score = typeof raw.score === 'number' ? raw.score : 0;
|
|
3776
3221
|
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
|
3777
3222
|
const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
3778
|
-
//
|
|
3779
|
-
//
|
|
3780
|
-
|
|
3223
|
+
// Always run client-side heuristics and merge any findings the
|
|
3224
|
+
// server missed. This ensures arithmetic/logic bugs are surfaced
|
|
3225
|
+
// even when the server only reports style issues like console.log.
|
|
3781
3226
|
const heuristic = this.heuristicCodeIssues(code, language);
|
|
3782
3227
|
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++;
|
|
3228
|
+
// Always include critical logic bugs (severity error) from heuristics
|
|
3229
|
+
// regardless of server results — these catch wrong-operator bugs the
|
|
3230
|
+
// server frequently misses.
|
|
3231
|
+
if (h.severity === 'error') {
|
|
3232
|
+
const exactDuplicate = issues.some((existing) => existing.line === h.line && existing.message === h.message);
|
|
3233
|
+
if (!exactDuplicate) {
|
|
3234
|
+
issues.push(h);
|
|
3808
3235
|
}
|
|
3809
|
-
|
|
3810
|
-
}
|
|
3811
|
-
|
|
3236
|
+
continue;
|
|
3237
|
+
}
|
|
3238
|
+
// For non-critical heuristics, avoid duplicating issues the server
|
|
3239
|
+
// already reported on the same line with the same type.
|
|
3240
|
+
const isDuplicate = issues.some((existing) => existing.line === h.line && existing.type === h.type);
|
|
3241
|
+
if (!isDuplicate) {
|
|
3812
3242
|
issues.push(h);
|
|
3813
3243
|
}
|
|
3814
3244
|
}
|
|
@@ -3957,11 +3387,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3957
3387
|
const sysPrompt = [
|
|
3958
3388
|
`You are a ${language} code fixer. Fix the code for: ${fixType}.`,
|
|
3959
3389
|
'Return a JSON object with:',
|
|
3960
|
-
' "fixed": the
|
|
3390
|
+
' "fixed": the corrected code as a string,',
|
|
3961
3391
|
' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
|
|
3962
3392
|
'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
3393
|
'- Fix ONLY the issues related to the fix type.',
|
|
3966
3394
|
'- Do not add comments, do not restructure beyond the minimal fix.',
|
|
3967
3395
|
'- Return ONLY the JSON object, no markdown fences.',
|
|
@@ -4001,26 +3429,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4001
3429
|
if (fixType === 'syntax' && fixed !== code) {
|
|
4002
3430
|
fixed = this.repairBracketBalance(code, fixed);
|
|
4003
3431
|
}
|
|
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
3432
|
// If there are still no changes but the fixed code differs, compute
|
|
4025
3433
|
// a semantic diff using LCS so inserted/removed lines don't cause
|
|
4026
3434
|
// every subsequent line to appear as changed.
|
|
@@ -4357,7 +3765,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4357
3765
|
}
|
|
4358
3766
|
async getCoderHealth() {
|
|
4359
3767
|
try {
|
|
4360
|
-
const response = await this.client.get('/api/health', { timeout:
|
|
3768
|
+
const response = await this.client.get('/api/health', { timeout: 10000 });
|
|
4361
3769
|
const ok = response.data?.status === 'ok' || response.data?.healthy === true;
|
|
4362
3770
|
return {
|
|
4363
3771
|
name: 'Coder API',
|
|
@@ -4379,8 +3787,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4379
3787
|
const modelsApiUrl = this.config.get('modelsApiUrl');
|
|
4380
3788
|
try {
|
|
4381
3789
|
const [healthResponse, modelsResponse] = await Promise.all([
|
|
4382
|
-
this.modelRouterClient.get('/health', { timeout:
|
|
4383
|
-
this.modelRouterClient.get('/v1/models', { timeout:
|
|
3790
|
+
this.modelRouterClient.get('/health', { timeout: 10000 }),
|
|
3791
|
+
this.modelRouterClient.get('/v1/models', { timeout: 15000 }),
|
|
4384
3792
|
]);
|
|
4385
3793
|
const healthOk = healthResponse.data?.status === 'healthy'
|
|
4386
3794
|
|| healthResponse.data?.status === 'ok'
|
|
@@ -4411,7 +3819,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4411
3819
|
return null;
|
|
4412
3820
|
}
|
|
4413
3821
|
try {
|
|
4414
|
-
const response = await this.selfHostedModelRouterClient.get('/health', { timeout:
|
|
3822
|
+
const response = await this.selfHostedModelRouterClient.get('/health', { timeout: 10000 });
|
|
4415
3823
|
const ok = response.data?.status === 'healthy'
|
|
4416
3824
|
|| response.data?.status === 'ok'
|
|
4417
3825
|
|| response.data?.healthy === true;
|
|
@@ -4431,6 +3839,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4431
3839
|
};
|
|
4432
3840
|
}
|
|
4433
3841
|
}
|
|
3842
|
+
async attemptV3ServiceRecovery(reason = '', options = {}) {
|
|
3843
|
+
const attempts = Math.max(1, Number(options.attempts || 2));
|
|
3844
|
+
const delayMs = Math.max(0, Number(options.delayMs || 1200));
|
|
3845
|
+
let lastError = '';
|
|
3846
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
3847
|
+
const health = await this.getV3AgentHealth();
|
|
3848
|
+
if (health.ok) {
|
|
3849
|
+
const msg = attempt === 1
|
|
3850
|
+
? 'V3 service is reachable.'
|
|
3851
|
+
: `V3 service recovered after retry ${attempt}.`;
|
|
3852
|
+
return { recovered: true, message: msg, endpoint: health.endpoint };
|
|
3853
|
+
}
|
|
3854
|
+
lastError = health.error || 'health probe failed';
|
|
3855
|
+
if (attempt < attempts && delayMs > 0) {
|
|
3856
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const reasonText = sanitizeUserFacingErrorText(reason || lastError || 'unknown failure');
|
|
3860
|
+
return {
|
|
3861
|
+
recovered: false,
|
|
3862
|
+
message: reasonText ? `Recovery failed: ${reasonText}` : 'Recovery failed: V3 service is still unreachable.',
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
4434
3865
|
async getV3AgentHealth() {
|
|
4435
3866
|
const baseUrl = this.getV3AgentBaseUrls()[0];
|
|
4436
3867
|
// Try multiple health endpoint patterns — the V3 backend may expose
|
|
@@ -4444,7 +3875,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4444
3875
|
for (const endpoint of candidates) {
|
|
4445
3876
|
try {
|
|
4446
3877
|
const controller = new AbortController();
|
|
4447
|
-
const timer = setTimeout(() => controller.abort(),
|
|
3878
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
4448
3879
|
const response = await fetch(endpoint, {
|
|
4449
3880
|
method: 'GET',
|
|
4450
3881
|
headers,
|
|
@@ -4492,7 +3923,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4492
3923
|
const runUrl = this.getV3AgentRunUrl(baseUrl);
|
|
4493
3924
|
try {
|
|
4494
3925
|
const controller = new AbortController();
|
|
4495
|
-
const timer = setTimeout(() => controller.abort(),
|
|
3926
|
+
const timer = setTimeout(() => controller.abort(), 5000);
|
|
4496
3927
|
const probe = await fetch(runUrl, { method: 'OPTIONS', headers, signal: controller.signal });
|
|
4497
3928
|
clearTimeout(timer);
|
|
4498
3929
|
if (probe.ok || probe.status === 204 || probe.status === 405) {
|
|
@@ -4649,18 +4080,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4649
4080
|
});
|
|
4650
4081
|
}
|
|
4651
4082
|
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
4083
|
const [v3Agent, hyperLoop, repoMemory, devtoolsBridge] = await Promise.all([
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4084
|
+
this.getV3AgentHealth(),
|
|
4085
|
+
this.getHyperLoopHealth(),
|
|
4086
|
+
this.getRepoMemoryHealth(context),
|
|
4087
|
+
this.getDevtoolsBridgeStatus(),
|
|
4664
4088
|
]);
|
|
4665
4089
|
return {
|
|
4666
4090
|
overallOk: v3Agent.ok && hyperLoop.ok && repoMemory.ok,
|