vigthoria-cli 1.8.14 → 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/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 { /* ok */ }
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
- // Probe both endpoints in parallel. If EITHER succeeds the token is
441
- // valid. Only if both return 401/403 is the token truly invalid.
442
- // If both are unreachable assume the token is fine (offline scenario).
443
- const results = await Promise.allSettled([
444
- this.modelRouterClient.get('/v1/models', { timeout: 5000 }),
445
- this.client.get('/api/user/profile', { timeout: 5000 }),
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
- // Both failed — check why
452
- for (const r of results) {
453
- if (r.status === 'rejected') {
454
- const err = r.reason;
455
- if (err.response?.status === 401 || err.response?.status === 403) {
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
- if (err instanceof CLIError && err.category === 'auth') {
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 = isServerRuntime() && (process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1' || preferLocal);
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
- const allowLocal = isServerRuntime() && process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1';
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
- ...(allowLocal ? ['http://127.0.0.1:5060', 'http://127.0.0.1:5050'] : []),
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}: ${describeUpstreamStatus(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
- // Throw a clean message instead of the raw ECONNREFUSED from the last URL tried
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}: ${describeUpstreamStatus(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}: ${describeUpstreamStatus(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 ?? DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
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(`V3 agent: ${sanitizeUserFacingErrorText(event.message || '') || 'returned an 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 ?? DEFAULT_V3_AGENT_TIMEOUT_MS;
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 timeoutMs = baseTimeoutMs;
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 = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
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}: ${describeUpstreamStatus(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 = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
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
- if (continueTimeoutId) {
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
- if (timeoutId) {
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 ?? DEFAULT_OPERATOR_TIMEOUT_MS;
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 = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
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 || 'full',
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}: ${describeUpstreamStatus(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
- if (timeoutId) {
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
- // Count braces/parens/brackets outside strings and comments
3561
- let braces = 0, parens = 0, brackets = 0;
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
- braces++;
3075
+ depth++;
3597
3076
  else if (ch === '}')
3598
- braces--;
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
- catch {
3628
- return false;
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
- '- Report each distinct bug ONCE. Do NOT report the same bug multiple times with different wording.',
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
- '- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
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
- // Merge client-side heuristics, but with tight dedup to avoid
3779
- // redundant over-reporting when the model already found the bug.
3780
- const modelFoundError = issues.some(i => i.severity === 'error');
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
- // If the model already found a real error, skip non-error heuristics
3784
- // entirelythey're just padding (style, robustness, etc.)
3785
- if (modelFoundError && h.severity !== 'error')
3786
- continue;
3787
- // Semantic duplicate check: same line + (similar type OR overlapping
3788
- // keywords in the message). This catches cases where the model
3789
- // and heuristic describe the same bug with different wording.
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
- return overlap >= 2;
3810
- });
3811
- if (!isSemanticallyDuplicate) {
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 COMPLETE corrected source code as a string (not a snippet — the full file),',
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: 5000 });
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: 5000 }),
4383
- this.modelRouterClient.get('/v1/models', { timeout: 5000 }),
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: 5000 });
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(), 3000);
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(), 2000);
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
- withTimeout(this.getV3AgentHealth(), 'V3 Agent'),
4661
- withTimeout(this.getHyperLoopHealth(), 'Hyper Loop'),
4662
- withTimeout(this.getRepoMemoryHealth(context), 'Repo Memory'),
4663
- withTimeout(this.getDevtoolsBridgeStatus(), 'DevTools Bridge'),
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,