vigthoria-cli 1.6.5 → 1.6.9

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.
Files changed (74) hide show
  1. package/README.md +9 -1
  2. package/dist/commands/auth.d.ts +0 -1
  3. package/dist/commands/auth.js +57 -6
  4. package/dist/commands/bridge.d.ts +7 -0
  5. package/dist/commands/bridge.js +31 -0
  6. package/dist/commands/chat.d.ts +27 -1
  7. package/dist/commands/chat.js +508 -14
  8. package/dist/commands/config.d.ts +0 -1
  9. package/dist/commands/config.js +10 -3
  10. package/dist/commands/deploy.d.ts +0 -1
  11. package/dist/commands/deploy.js +0 -1
  12. package/dist/commands/edit.d.ts +0 -1
  13. package/dist/commands/edit.js +0 -1
  14. package/dist/commands/explain.d.ts +0 -1
  15. package/dist/commands/explain.js +0 -1
  16. package/dist/commands/generate.d.ts +0 -1
  17. package/dist/commands/generate.js +0 -1
  18. package/dist/commands/hub.d.ts +0 -1
  19. package/dist/commands/hub.js +0 -1
  20. package/dist/commands/repo.d.ts +0 -1
  21. package/dist/commands/repo.js +0 -1
  22. package/dist/commands/review.d.ts +0 -1
  23. package/dist/commands/review.js +0 -1
  24. package/dist/commands/workflow.d.ts +31 -0
  25. package/dist/commands/workflow.js +140 -0
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.js +135 -11
  28. package/dist/utils/api.d.ts +210 -1
  29. package/dist/utils/api.js +1789 -47
  30. package/dist/utils/config.d.ts +14 -7
  31. package/dist/utils/config.js +22 -11
  32. package/dist/utils/files.d.ts +0 -1
  33. package/dist/utils/files.js +0 -1
  34. package/dist/utils/logger.d.ts +0 -1
  35. package/dist/utils/logger.js +0 -1
  36. package/dist/utils/session.d.ts +14 -2
  37. package/dist/utils/session.js +105 -4
  38. package/dist/utils/tools.d.ts +0 -1
  39. package/dist/utils/tools.js +0 -1
  40. package/package.json +23 -4
  41. package/dist/commands/auth.d.ts.map +0 -1
  42. package/dist/commands/auth.js.map +0 -1
  43. package/dist/commands/chat.d.ts.map +0 -1
  44. package/dist/commands/chat.js.map +0 -1
  45. package/dist/commands/config.d.ts.map +0 -1
  46. package/dist/commands/config.js.map +0 -1
  47. package/dist/commands/deploy.d.ts.map +0 -1
  48. package/dist/commands/deploy.js.map +0 -1
  49. package/dist/commands/edit.d.ts.map +0 -1
  50. package/dist/commands/edit.js.map +0 -1
  51. package/dist/commands/explain.d.ts.map +0 -1
  52. package/dist/commands/explain.js.map +0 -1
  53. package/dist/commands/generate.d.ts.map +0 -1
  54. package/dist/commands/generate.js.map +0 -1
  55. package/dist/commands/hub.d.ts.map +0 -1
  56. package/dist/commands/hub.js.map +0 -1
  57. package/dist/commands/repo.d.ts.map +0 -1
  58. package/dist/commands/repo.js.map +0 -1
  59. package/dist/commands/review.d.ts.map +0 -1
  60. package/dist/commands/review.js.map +0 -1
  61. package/dist/index.d.ts.map +0 -1
  62. package/dist/index.js.map +0 -1
  63. package/dist/utils/api.d.ts.map +0 -1
  64. package/dist/utils/api.js.map +0 -1
  65. package/dist/utils/config.d.ts.map +0 -1
  66. package/dist/utils/config.js.map +0 -1
  67. package/dist/utils/files.d.ts.map +0 -1
  68. package/dist/utils/files.js.map +0 -1
  69. package/dist/utils/logger.d.ts.map +0 -1
  70. package/dist/utils/logger.js.map +0 -1
  71. package/dist/utils/session.d.ts.map +0 -1
  72. package/dist/utils/session.js.map +0 -1
  73. package/dist/utils/tools.d.ts.map +0 -1
  74. package/dist/utils/tools.js.map +0 -1
package/dist/utils/api.js CHANGED
@@ -9,8 +9,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.APIClient = void 0;
11
11
  const axios_1 = __importDefault(require("axios"));
12
+ const crypto_1 = require("crypto");
13
+ const fs_1 = __importDefault(require("fs"));
12
14
  const https_1 = __importDefault(require("https"));
15
+ const net_1 = __importDefault(require("net"));
16
+ const path_1 = __importDefault(require("path"));
13
17
  const ws_1 = __importDefault(require("ws"));
18
+ const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
19
+ const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '1200000';
20
+ const parsed = Number.parseInt(rawValue, 10);
21
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1200000;
22
+ })();
23
+ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
24
+ const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS || '90000';
25
+ const parsed = Number.parseInt(rawValue, 10);
26
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
27
+ })();
28
+ const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
29
+ const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS || '1200000';
30
+ const parsed = Number.parseInt(rawValue, 10);
31
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1200000;
32
+ })();
14
33
  class APIClient {
15
34
  client;
16
35
  modelRouterClient;
@@ -18,6 +37,7 @@ class APIClient {
18
37
  config;
19
38
  logger;
20
39
  ws = null;
40
+ vigFlowTokens = new Map();
21
41
  constructor(config, logger) {
22
42
  this.config = config;
23
43
  this.logger = logger;
@@ -34,7 +54,7 @@ class APIClient {
34
54
  httpsAgent,
35
55
  headers: {
36
56
  'Content-Type': 'application/json',
37
- 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.5'}`,
57
+ 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
38
58
  },
39
59
  });
40
60
  // Direct AI Models API - bypasses Coder for direct model access
@@ -45,18 +65,19 @@ class APIClient {
45
65
  httpsAgent,
46
66
  headers: {
47
67
  'Content-Type': 'application/json',
48
- 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.5'}`,
68
+ 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
49
69
  },
50
70
  });
51
- // Self-hosted model router for Blackwell-backed agent fallback.
52
- this.selfHostedModelRouterClient = axios_1.default.create({
53
- baseURL: process.env.VIGTHORIA_SELF_HOSTED_MODELS_API_URL || 'http://127.0.0.1:4009',
71
+ // Self-hosted model router is opt-in only.
72
+ const selfHostedModelsApiUrl = this.getSelfHostedModelsApiUrl();
73
+ this.selfHostedModelRouterClient = selfHostedModelsApiUrl ? axios_1.default.create({
74
+ baseURL: selfHostedModelsApiUrl,
54
75
  timeout: 240000,
55
76
  headers: {
56
77
  'Content-Type': 'application/json',
57
- 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.5'}`,
78
+ 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
58
79
  },
59
- });
80
+ }) : null;
60
81
  // Add auth interceptor
61
82
  this.client.interceptors.request.use((req) => {
62
83
  const token = this.config.get('authToken');
@@ -74,7 +95,7 @@ class APIClient {
74
95
  }
75
96
  return req;
76
97
  });
77
- this.selfHostedModelRouterClient.interceptors.request.use((req) => {
98
+ this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
78
99
  const token = this.config.get('authToken');
79
100
  if (token) {
80
101
  req.headers.Authorization = `Bearer ${token}`;
@@ -92,6 +113,15 @@ class APIClient {
92
113
  throw error;
93
114
  });
94
115
  }
116
+ getSelfHostedModelsApiUrl() {
117
+ const configuredUrl = process.env.VIGTHORIA_SELF_HOSTED_MODELS_API_URL
118
+ || this.config.get('selfHostedModelsApiUrl');
119
+ if (!configuredUrl) {
120
+ return null;
121
+ }
122
+ const normalizedUrl = String(configuredUrl).trim().replace(/\/$/, '');
123
+ return normalizedUrl || null;
124
+ }
95
125
  // Authentication - Uses Vigthoria Coder /api/login endpoint
96
126
  async login(email, password) {
97
127
  try {
@@ -124,26 +154,42 @@ class APIClient {
124
154
  try {
125
155
  // Validate token by making a request to user info endpoint
126
156
  this.config.set('authToken', token);
127
- const response = await this.client.get('/api/user/profile', {
128
- headers: {
129
- Authorization: `Bearer ${token}`,
130
- Cookie: `vigthoria-auth-token=${token}`,
131
- },
132
- });
133
- if (response.data && response.data.user) {
134
- const user = response.data.user;
135
- this.config.setAuth({
136
- token,
137
- userId: user.id,
138
- email: user.email,
139
- });
140
- this.config.setSubscription({
141
- plan: user.subscription?.plan || user.subscription_plan || 'developer',
142
- status: 'active',
143
- expiresAt: undefined,
144
- });
145
- return true;
157
+ const headers = {
158
+ Authorization: `Bearer ${token}`,
159
+ Cookie: `vigthoria-auth-token=${token}`,
160
+ };
161
+ const candidateEndpoints = [
162
+ '/api/user/profile',
163
+ '/api/user/info',
164
+ '/api/auth/me',
165
+ ];
166
+ for (const endpoint of candidateEndpoints) {
167
+ try {
168
+ const response = await this.client.get(endpoint, { headers });
169
+ const profile = this.extractUserProfile(response.data);
170
+ if (!profile) {
171
+ continue;
172
+ }
173
+ this.config.setAuth({
174
+ token,
175
+ userId: profile.id,
176
+ email: profile.email,
177
+ });
178
+ this.config.setSubscription({
179
+ plan: profile.plan,
180
+ status: 'active',
181
+ expiresAt: undefined,
182
+ });
183
+ return true;
184
+ }
185
+ catch (error) {
186
+ const axiosError = error;
187
+ if (axiosError.response?.status && axiosError.response.status !== 404) {
188
+ throw error;
189
+ }
190
+ }
146
191
  }
192
+ this.config.clearAuth();
147
193
  return false;
148
194
  }
149
195
  catch (error) {
@@ -152,6 +198,27 @@ class APIClient {
152
198
  return false;
153
199
  }
154
200
  }
201
+ extractUserProfile(data) {
202
+ if (!data) {
203
+ return null;
204
+ }
205
+ const rawUser = data.user || data;
206
+ const userId = rawUser.id;
207
+ const email = rawUser.email;
208
+ if (!userId || !email) {
209
+ return null;
210
+ }
211
+ const plan = data.subscription?.plan
212
+ || rawUser.subscription?.plan
213
+ || rawUser.subscription_plan
214
+ || data.subscription_plan
215
+ || 'developer';
216
+ return {
217
+ id: userId,
218
+ email,
219
+ plan,
220
+ };
221
+ }
155
222
  async refreshToken() {
156
223
  const refreshToken = this.config.get('refreshToken');
157
224
  if (!refreshToken)
@@ -186,6 +253,1393 @@ class APIClient {
186
253
  this.logger.debug('Failed to get subscription status:', error.message);
187
254
  }
188
255
  }
256
+ getAccessToken() {
257
+ return process.env.VIGTHORIA_TOKEN
258
+ || process.env.VIGTHORIA_AUTH_TOKEN
259
+ || this.config.get('authToken')
260
+ || null;
261
+ }
262
+ getV3AgentBaseUrls() {
263
+ const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
264
+ const urls = [
265
+ process.env.VIGTHORIA_V3_AGENT_URL,
266
+ process.env.V3_AGENT_URL,
267
+ 'http://127.0.0.1:8030',
268
+ configuredApiUrl,
269
+ ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
270
+ return [...new Set(urls)];
271
+ }
272
+ getV3AgentRunUrl(baseUrl) {
273
+ if (/127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)) {
274
+ return `${baseUrl}/api/agent/run`;
275
+ }
276
+ return `${baseUrl}/api/v3-agent/run`;
277
+ }
278
+ getOperatorBaseUrls() {
279
+ const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
280
+ const urls = [
281
+ process.env.VIGTHORIA_OPERATOR_URL,
282
+ process.env.OPERATOR_URL,
283
+ 'http://127.0.0.1:4009',
284
+ configuredModelsApiUrl,
285
+ ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
286
+ return [...new Set(urls)];
287
+ }
288
+ getOperatorStreamUrl(baseUrl) {
289
+ return `${baseUrl}/api/operator/stream`;
290
+ }
291
+ getMcpBaseUrls() {
292
+ const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
293
+ const urls = [
294
+ process.env.VIGTHORIA_MCP_URL,
295
+ process.env.MCP_SERVER_URL,
296
+ 'http://127.0.0.1:4008',
297
+ configuredApiUrl,
298
+ ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
299
+ return [...new Set(urls)];
300
+ }
301
+ getVigFlowBaseUrls() {
302
+ const urls = [
303
+ process.env.VIGTHORIA_VIGFLOW_URL,
304
+ process.env.VIGFLOW_URL,
305
+ process.env.WORKFLOW_BUILDER_URL,
306
+ 'http://127.0.0.1:5060',
307
+ 'http://127.0.0.1:5050',
308
+ ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
309
+ return [...new Set(urls)];
310
+ }
311
+ getTemplateServiceBaseUrls() {
312
+ const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
313
+ const urls = [
314
+ process.env.VIGTHORIA_TEMPLATE_SERVICE_URL,
315
+ process.env.TEMPLATE_SERVICE_URL,
316
+ 'http://127.0.0.1:4011',
317
+ `${configuredApiUrl}/api/template-service`,
318
+ ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
319
+ return [...new Set(urls)];
320
+ }
321
+ isFrontendTask(message = '', context = {}) {
322
+ const prompt = String(message || '');
323
+ const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
324
+ return /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|hero|responsive)/i.test(prompt)
325
+ || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
326
+ }
327
+ normalizeWorkspaceRelativePath(filePath) {
328
+ return String(filePath || '').trim().replace(/\\/g, '/').replace(/^\.\//, '');
329
+ }
330
+ listFrontendWorkspaceFiles(rootPath) {
331
+ const results = [];
332
+ const stack = [rootPath];
333
+ while (stack.length > 0) {
334
+ const current = stack.pop();
335
+ if (!current) {
336
+ continue;
337
+ }
338
+ for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
339
+ if (entry.name === '.git' || entry.name === 'node_modules') {
340
+ continue;
341
+ }
342
+ const fullPath = path_1.default.join(current, entry.name);
343
+ if (entry.isDirectory()) {
344
+ stack.push(fullPath);
345
+ continue;
346
+ }
347
+ if (!entry.isFile()) {
348
+ continue;
349
+ }
350
+ const relativePath = this.normalizeWorkspaceRelativePath(path_1.default.relative(rootPath, fullPath));
351
+ if (relativePath) {
352
+ results.push(relativePath);
353
+ }
354
+ }
355
+ }
356
+ results.sort((left, right) => left.localeCompare(right));
357
+ return results;
358
+ }
359
+ chooseFrontendPreviewEntry(rootPath, htmlPaths, expectedFiles = []) {
360
+ const normalizedExpected = expectedFiles
361
+ .map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
362
+ .filter((filePath) => /\.html?$/i.test(filePath));
363
+ for (const expectedPath of normalizedExpected) {
364
+ if (htmlPaths.includes(expectedPath)) {
365
+ return expectedPath;
366
+ }
367
+ }
368
+ const scored = htmlPaths.map((relativePath) => {
369
+ const baseName = path_1.default.posix.basename(relativePath).toLowerCase();
370
+ const depth = relativePath.split('/').length - 1;
371
+ let score = 0;
372
+ if (relativePath.toLowerCase() === 'index.html') {
373
+ score += 200;
374
+ }
375
+ if (baseName === 'index.html') {
376
+ score += 120;
377
+ }
378
+ if (/(landing|home|main|dashboard|app)/i.test(baseName)) {
379
+ score += 60;
380
+ }
381
+ if (depth === 0) {
382
+ score += 40;
383
+ }
384
+ return { relativePath, score, depth };
385
+ }).sort((left, right) => right.score - left.score || left.depth - right.depth || left.relativePath.localeCompare(right.relativePath));
386
+ return scored[0]?.relativePath || null;
387
+ }
388
+ extractLinkedFrontendAssets(html, entryPath) {
389
+ const css = new Set();
390
+ const js = new Set();
391
+ const entryDir = path_1.default.posix.dirname(this.normalizeWorkspaceRelativePath(entryPath) || '.');
392
+ const normalizeAsset = (assetPath) => {
393
+ const clean = String(assetPath || '').trim();
394
+ if (!clean || clean.startsWith('http://') || clean.startsWith('https://') || clean.startsWith('//') || clean.startsWith('data:') || clean.startsWith('#')) {
395
+ return null;
396
+ }
397
+ const withoutQuery = clean.split('?')[0].split('#')[0];
398
+ if (!withoutQuery) {
399
+ return null;
400
+ }
401
+ if (withoutQuery.startsWith('/')) {
402
+ return this.normalizeWorkspaceRelativePath(withoutQuery.slice(1));
403
+ }
404
+ return this.normalizeWorkspaceRelativePath(path_1.default.posix.normalize(path_1.default.posix.join(entryDir, withoutQuery)));
405
+ };
406
+ const cssPattern = /<link[^>]+href=["']([^"']+\.css(?:[?#][^"']*)?)["'][^>]*>/gi;
407
+ const jsPattern = /<script[^>]+src=["']([^"']+\.(?:js|mjs|cjs)(?:[?#][^"']*)?)["'][^>]*>/gi;
408
+ let match;
409
+ while ((match = cssPattern.exec(String(html || ''))) !== null) {
410
+ const resolved = normalizeAsset(match[1]);
411
+ if (resolved) {
412
+ css.add(resolved);
413
+ }
414
+ }
415
+ while ((match = jsPattern.exec(String(html || ''))) !== null) {
416
+ const resolved = normalizeAsset(match[1]);
417
+ if (resolved) {
418
+ js.add(resolved);
419
+ }
420
+ }
421
+ return {
422
+ css: Array.from(css),
423
+ js: Array.from(js),
424
+ };
425
+ }
426
+ gatherFrontendPreviewArtifacts(message = '', context = {}) {
427
+ const rootPath = this.resolveAgentTargetPath(context);
428
+ if (!rootPath || !fs_1.default.existsSync(rootPath)) {
429
+ return { error: 'Frontend preview gate requires a readable project workspace.' };
430
+ }
431
+ const workspaceFiles = this.listFrontendWorkspaceFiles(rootPath);
432
+ const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
433
+ const htmlPaths = workspaceFiles.filter((filePath) => /\.html?$/i.test(filePath));
434
+ if (htmlPaths.length === 0) {
435
+ return { error: 'Frontend preview gate requires at least one HTML entry file in the workspace.' };
436
+ }
437
+ const htmlPath = this.chooseFrontendPreviewEntry(rootPath, htmlPaths, expectedFiles);
438
+ if (!htmlPath) {
439
+ return { error: 'Frontend preview gate could not determine a primary HTML entry file.' };
440
+ }
441
+ const html = fs_1.default.readFileSync(path_1.default.join(rootPath, htmlPath), 'utf8');
442
+ const linkedAssets = this.extractLinkedFrontendAssets(html, htmlPath);
443
+ const cssFiles = workspaceFiles.filter((filePath) => /\.css$/i.test(filePath));
444
+ const jsFiles = workspaceFiles.filter((filePath) => /\.(?:js|mjs|cjs)$/i.test(filePath));
445
+ const expectedCss = expectedFiles
446
+ .map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
447
+ .filter((filePath) => /\.css$/i.test(filePath));
448
+ const expectedJs = expectedFiles
449
+ .map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
450
+ .filter((filePath) => /\.(?:js|mjs|cjs)$/i.test(filePath));
451
+ const chooseExisting = (candidates, fallback) => {
452
+ const selected = new Set();
453
+ for (const candidate of [...candidates, ...fallback]) {
454
+ const normalized = this.normalizeWorkspaceRelativePath(candidate);
455
+ if (!normalized || selected.has(normalized)) {
456
+ continue;
457
+ }
458
+ if (!fs_1.default.existsSync(path_1.default.join(rootPath, normalized))) {
459
+ continue;
460
+ }
461
+ selected.add(normalized);
462
+ }
463
+ return Array.from(selected);
464
+ };
465
+ const selectedCssFiles = chooseExisting(linkedAssets.css, [...expectedCss, ...cssFiles]).slice(0, 12);
466
+ const selectedJsFiles = chooseExisting(linkedAssets.js, [...expectedJs, ...jsFiles]).slice(0, 12);
467
+ const css = selectedCssFiles.map((filePath) => fs_1.default.readFileSync(path_1.default.join(rootPath, filePath), 'utf8')).join('\n\n');
468
+ const js = selectedJsFiles.map((filePath) => fs_1.default.readFileSync(path_1.default.join(rootPath, filePath), 'utf8')).join('\n\n');
469
+ return { htmlPath, html, css, js, cssPaths: selectedCssFiles, jsPaths: selectedJsFiles };
470
+ }
471
+ async captureFrontendPreviewScreenshot(entryAbsolutePath, screenshotPath) {
472
+ try {
473
+ const puppeteerModule = await import('puppeteer').catch(() => null);
474
+ const puppeteer = puppeteerModule?.default || puppeteerModule;
475
+ if (!puppeteer || typeof puppeteer.launch !== 'function') {
476
+ return { captured: false, error: 'puppeteer-unavailable' };
477
+ }
478
+ const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
479
+ try {
480
+ const page = await browser.newPage();
481
+ await page.setViewport({ width: 1440, height: 960, deviceScaleFactor: 1 });
482
+ await page.goto(`file://${entryAbsolutePath}`, { waitUntil: 'networkidle0', timeout: 20000 });
483
+ await page.screenshot({ path: screenshotPath, fullPage: true });
484
+ }
485
+ finally {
486
+ await browser.close();
487
+ }
488
+ return { captured: true };
489
+ }
490
+ catch (error) {
491
+ return { captured: false, error: error?.message || String(error) };
492
+ }
493
+ }
494
+ evaluateFrontendVisualProof(summary = {}, artifacts = {}) {
495
+ const reasons = [];
496
+ const cssPaths = Array.isArray(artifacts.cssPaths) ? artifacts.cssPaths : [];
497
+ const html = String(artifacts.html || '');
498
+ const css = String(artifacts.css || '');
499
+ const js = String(artifacts.js || '');
500
+ if (!summary.hasViewportMeta) {
501
+ reasons.push('missing viewport metadata');
502
+ }
503
+ const hasStyleEvidence = cssPaths.length > 0 || /<style[\s>]/i.test(html) || css.trim().length > 0;
504
+ if (!hasStyleEvidence) {
505
+ reasons.push('missing stylesheet or inline style evidence');
506
+ }
507
+ const hasInteractionOrResponsiveEvidence = Boolean(summary.hasResponsiveSignals) || Boolean(summary.hasInteractiveSignals) || /<script[\s>]/i.test(html) || js.trim().length > 0;
508
+ if (!hasInteractionOrResponsiveEvidence) {
509
+ reasons.push('missing responsive or interactive evidence');
510
+ }
511
+ return {
512
+ ok: reasons.length === 0,
513
+ reasons,
514
+ };
515
+ }
516
+ async persistFrontendPreviewArtifacts(previewGate, rootPath, context = {}) {
517
+ if (!previewGate.required || !previewGate.entryPath) {
518
+ return previewGate;
519
+ }
520
+ try {
521
+ const artifactsDir = path_1.default.join(rootPath, '.vigthoria', 'proof', 'preview');
522
+ fs_1.default.mkdirSync(artifactsDir, { recursive: true });
523
+ const contextId = String(context.contextId || 'preview').trim() || 'preview';
524
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
525
+ const baseName = `${timestamp}-${contextId}`;
526
+ const manifestPath = path_1.default.join(artifactsDir, `${baseName}.json`);
527
+ const screenshotPath = path_1.default.join(artifactsDir, `${baseName}.png`);
528
+ const entryAbsolutePath = path_1.default.join(rootPath, previewGate.entryPath);
529
+ const previewFileUrl = `file://${entryAbsolutePath}`;
530
+ const screenshot = await this.captureFrontendPreviewScreenshot(entryAbsolutePath, screenshotPath);
531
+ const manifest = {
532
+ schemaVersion: 1,
533
+ createdAt: new Date().toISOString(),
534
+ contextId: context.contextId || null,
535
+ workspaceRoot: rootPath,
536
+ entryPath: previewGate.entryPath,
537
+ previewFileUrl,
538
+ previewCommand: `vigthoria preview -p ${rootPath}`,
539
+ previewGate,
540
+ validationArtifacts: {
541
+ summary: previewGate.summary || {},
542
+ modes: previewGate.modes || {},
543
+ backendUrl: previewGate.backendUrl || null,
544
+ processingTimeMs: previewGate.processingTimeMs || null,
545
+ assetPaths: previewGate.assetPaths || { css: [], js: [] },
546
+ },
547
+ screenshot: {
548
+ path: screenshot.captured ? screenshotPath : null,
549
+ captured: screenshot.captured,
550
+ error: screenshot.error || null,
551
+ },
552
+ };
553
+ fs_1.default.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
554
+ return {
555
+ ...previewGate,
556
+ artifacts: {
557
+ manifestPath,
558
+ screenshotPath: screenshot.captured ? screenshotPath : undefined,
559
+ previewFileUrl,
560
+ screenshotCaptured: screenshot.captured,
561
+ screenshotError: screenshot.error,
562
+ },
563
+ };
564
+ }
565
+ catch (error) {
566
+ return {
567
+ ...previewGate,
568
+ artifacts: {
569
+ ...(previewGate.artifacts || {}),
570
+ screenshotCaptured: false,
571
+ screenshotError: error?.message || String(error),
572
+ },
573
+ };
574
+ }
575
+ }
576
+ async runTemplateServicePreviewGate(message = '', context = {}) {
577
+ if (!this.isFrontendTask(message, context)) {
578
+ return { required: false, passed: true };
579
+ }
580
+ const rootPath = this.resolveAgentTargetPath(context);
581
+ const artifacts = this.gatherFrontendPreviewArtifacts(message, context);
582
+ if (artifacts.error || !artifacts.htmlPath || typeof artifacts.html !== 'string') {
583
+ return {
584
+ required: true,
585
+ passed: false,
586
+ error: artifacts.error || 'Frontend preview gate could not collect frontend artifacts.',
587
+ };
588
+ }
589
+ const html = artifacts.html;
590
+ const css = artifacts.css || '';
591
+ const js = artifacts.js || '';
592
+ const errors = [];
593
+ for (const baseUrl of this.getTemplateServiceBaseUrls()) {
594
+ try {
595
+ const response = await fetch(`${baseUrl}/preview-proof`, {
596
+ method: 'POST',
597
+ headers: {
598
+ 'Content-Type': 'application/json',
599
+ Accept: 'application/json',
600
+ },
601
+ body: JSON.stringify({
602
+ vision: String(context.rawPrompt || message || ''),
603
+ html,
604
+ css,
605
+ js,
606
+ entryPath: artifacts.htmlPath,
607
+ workspaceName: path_1.default.basename(rootPath),
608
+ }),
609
+ });
610
+ if (!response.ok) {
611
+ const errorText = await response.text().catch(() => '');
612
+ throw new Error(`Template preview proof ${response.status}: ${errorText.slice(0, 200)}`);
613
+ }
614
+ const payload = await response.json();
615
+ const modes = payload?.modes || {};
616
+ const summary = payload?.summary || {};
617
+ const visualProof = this.evaluateFrontendVisualProof(summary, artifacts);
618
+ const passed = payload?.success === true
619
+ && modes?.design?.ready === true
620
+ && modes?.live?.ready === true
621
+ && modes?.production?.ready === true
622
+ && visualProof.ok;
623
+ const previewGate = {
624
+ required: true,
625
+ passed,
626
+ backendUrl: baseUrl,
627
+ entryPath: artifacts.htmlPath,
628
+ assetPaths: {
629
+ css: artifacts.cssPaths || [],
630
+ js: artifacts.jsPaths || [],
631
+ },
632
+ modes,
633
+ summary,
634
+ processingTimeMs: Number(payload?.processing_time || 0) || undefined,
635
+ error: passed ? undefined : (visualProof.ok ? 'Template Service preview modes did not all report ready.' : `Frontend preview proof is missing required visual evidence: ${visualProof.reasons.join(', ')}.`),
636
+ };
637
+ return await this.persistFrontendPreviewArtifacts(previewGate, rootPath, context);
638
+ }
639
+ catch (error) {
640
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
641
+ }
642
+ }
643
+ return await this.persistFrontendPreviewArtifacts({
644
+ required: true,
645
+ passed: false,
646
+ entryPath: artifacts.htmlPath,
647
+ assetPaths: {
648
+ css: artifacts.cssPaths || [],
649
+ js: artifacts.jsPaths || [],
650
+ },
651
+ error: errors.join(' | ') || 'No Template Service preview endpoint was reachable.',
652
+ }, rootPath, context);
653
+ }
654
+ getMcpContextCreateUrl(baseUrl) {
655
+ return `${baseUrl}/mcp/context/create`;
656
+ }
657
+ getMcpContextUrl(baseUrl, contextId) {
658
+ return `${baseUrl}/mcp/context/${encodeURIComponent(contextId)}`;
659
+ }
660
+ async getMcpHeaders() {
661
+ const headers = {
662
+ 'Content-Type': 'application/json',
663
+ Accept: 'application/json',
664
+ };
665
+ const authToken = this.getAccessToken();
666
+ if (authToken) {
667
+ headers.Authorization = `Bearer ${authToken}`;
668
+ headers.Cookie = `vigthoria-auth-token=${authToken}`;
669
+ }
670
+ return headers;
671
+ }
672
+ async getV3AgentHeaders() {
673
+ const headers = {
674
+ 'Content-Type': 'application/json',
675
+ };
676
+ const authToken = this.getAccessToken();
677
+ if (authToken) {
678
+ headers.Authorization = `Bearer ${authToken}`;
679
+ headers.Cookie = `vigthoria-auth-token=${authToken}`;
680
+ }
681
+ const serviceKey = process.env.VIGTHORIA_V3_SERVICE_KEY
682
+ || process.env.V3_SERVICE_KEY
683
+ || process.env.HYPERLOOP_SERVICE_KEY;
684
+ if (serviceKey) {
685
+ headers['X-Service-Key'] = serviceKey;
686
+ }
687
+ return headers;
688
+ }
689
+ async executeV3AgentRunRequest(baseUrl, body, executionContext, signal) {
690
+ const makeRequest = async () => {
691
+ const headers = await this.getV3AgentHeaders();
692
+ if (executionContext.mcpContextId) {
693
+ headers['X-MCP-Context-Id'] = String(executionContext.mcpContextId);
694
+ }
695
+ return fetch(this.getV3AgentRunUrl(baseUrl), {
696
+ method: 'POST',
697
+ headers,
698
+ body: JSON.stringify(body),
699
+ signal,
700
+ });
701
+ };
702
+ let response = await makeRequest();
703
+ if (response.status !== 401) {
704
+ return response;
705
+ }
706
+ const refreshed = await this.refreshToken();
707
+ if (!refreshed) {
708
+ return response;
709
+ }
710
+ response = await makeRequest();
711
+ return response;
712
+ }
713
+ async getVigFlowAccessToken(baseUrl) {
714
+ const cachedToken = this.vigFlowTokens.get(baseUrl);
715
+ if (cachedToken) {
716
+ return cachedToken;
717
+ }
718
+ const authToken = this.getAccessToken();
719
+ if (!authToken) {
720
+ throw new Error('Not authenticated. Run vigthoria login first.');
721
+ }
722
+ const response = await axios_1.default.post(`${baseUrl}/api/auth/sso`, { token: authToken }, {
723
+ headers: {
724
+ 'Content-Type': 'application/json',
725
+ },
726
+ timeout: 30000,
727
+ });
728
+ const payload = response.data;
729
+ const vigFlowToken = String(payload.token || '').trim();
730
+ if (!vigFlowToken) {
731
+ throw new Error('VigFlow SSO response did not include a token.');
732
+ }
733
+ this.vigFlowTokens.set(baseUrl, vigFlowToken);
734
+ return vigFlowToken;
735
+ }
736
+ async getVigFlowHeaders(baseUrl) {
737
+ const token = await this.getVigFlowAccessToken(baseUrl);
738
+ return {
739
+ 'Content-Type': 'application/json',
740
+ Accept: 'application/json',
741
+ Authorization: `Bearer ${token}`,
742
+ };
743
+ }
744
+ async withVigFlow(operation, action) {
745
+ let lastError = null;
746
+ for (const baseUrl of this.getVigFlowBaseUrls()) {
747
+ try {
748
+ const headers = await this.getVigFlowHeaders(baseUrl);
749
+ return await action(baseUrl, headers);
750
+ }
751
+ catch (error) {
752
+ lastError = error;
753
+ this.logger.debug(`VigFlow ${operation} via ${baseUrl} failed:`, lastError.message);
754
+ }
755
+ }
756
+ throw lastError || new Error(`No VigFlow backend available for ${operation}.`);
757
+ }
758
+ async listVigFlowTemplates(options = {}) {
759
+ return this.withVigFlow('list templates', async (baseUrl, headers) => {
760
+ const query = new URLSearchParams();
761
+ if (options.category) {
762
+ query.set('category', options.category);
763
+ }
764
+ if (options.search) {
765
+ query.set('search', options.search);
766
+ }
767
+ const url = `${baseUrl}/api/templates${query.size > 0 ? `?${query.toString()}` : ''}`;
768
+ const response = await axios_1.default.get(url, {
769
+ headers,
770
+ timeout: 30000,
771
+ });
772
+ const payload = response.data;
773
+ return Array.isArray(payload.templates) ? payload.templates : [];
774
+ });
775
+ }
776
+ async listVigFlowWorkflows() {
777
+ return this.withVigFlow('list workflows', async (baseUrl, headers) => {
778
+ const response = await axios_1.default.get(`${baseUrl}/api/workflows`, {
779
+ headers,
780
+ timeout: 30000,
781
+ });
782
+ const payload = response.data;
783
+ return Array.isArray(payload.workflows) ? payload.workflows : [];
784
+ });
785
+ }
786
+ async resolveVigFlowWorkflow(selector) {
787
+ const normalizedSelector = String(selector || '').trim();
788
+ if (!normalizedSelector) {
789
+ throw new Error('Workflow selector is required. Provide a workflow id or name.');
790
+ }
791
+ const workflows = await this.listVigFlowWorkflows();
792
+ const byId = workflows.find((workflow) => workflow.id === normalizedSelector);
793
+ if (byId) {
794
+ return {
795
+ id: byId.id,
796
+ name: byId.name,
797
+ selector: normalizedSelector,
798
+ matchedBy: 'id',
799
+ };
800
+ }
801
+ const loweredSelector = normalizedSelector.toLowerCase();
802
+ const exactNameMatches = workflows.filter((workflow) => String(workflow.name || '').trim().toLowerCase() === loweredSelector);
803
+ if (exactNameMatches.length === 1) {
804
+ return {
805
+ id: exactNameMatches[0].id,
806
+ name: exactNameMatches[0].name,
807
+ selector: normalizedSelector,
808
+ matchedBy: 'name',
809
+ };
810
+ }
811
+ if (exactNameMatches.length > 1) {
812
+ throw new Error(`Multiple workflows matched the name "${normalizedSelector}". Use the workflow id instead.`);
813
+ }
814
+ const searchMatches = workflows.filter((workflow) => String(workflow.name || '').toLowerCase().includes(loweredSelector));
815
+ if (searchMatches.length === 1) {
816
+ return {
817
+ id: searchMatches[0].id,
818
+ name: searchMatches[0].name,
819
+ selector: normalizedSelector,
820
+ matchedBy: 'search',
821
+ };
822
+ }
823
+ if (searchMatches.length > 1) {
824
+ throw new Error(`Multiple workflows partially matched "${normalizedSelector}". Use a full workflow name or id.`);
825
+ }
826
+ throw new Error(`No VigFlow workflow matched "${normalizedSelector}".`);
827
+ }
828
+ async useVigFlowTemplate(templateId, options = {}) {
829
+ return this.withVigFlow('use template', async (baseUrl, headers) => {
830
+ const response = await axios_1.default.post(`${baseUrl}/api/templates/${encodeURIComponent(templateId)}/use`, {
831
+ name: options.name,
832
+ variables: options.variables || {},
833
+ }, {
834
+ headers,
835
+ timeout: 30000,
836
+ });
837
+ const payload = response.data;
838
+ if (!payload.workflow?.id) {
839
+ throw new Error('VigFlow use-template response did not include a workflow id.');
840
+ }
841
+ return payload.workflow;
842
+ });
843
+ }
844
+ async runVigFlowWorkflow(workflowId, options = {}) {
845
+ return this.withVigFlow('run workflow', async (baseUrl, headers) => {
846
+ const response = await axios_1.default.post(`${baseUrl}/api/executions/run/${encodeURIComponent(workflowId)}`, {
847
+ data: options.data || {},
848
+ options: options.executionOptions || {},
849
+ }, {
850
+ headers,
851
+ timeout: 60000,
852
+ });
853
+ const payload = response.data;
854
+ if (!payload.executionId) {
855
+ throw new Error('VigFlow run response did not include an execution id.');
856
+ }
857
+ return payload;
858
+ });
859
+ }
860
+ async getVigFlowExecutionStatus(executionId) {
861
+ return this.withVigFlow('execution status', async (baseUrl, headers) => {
862
+ const response = await axios_1.default.get(`${baseUrl}/api/executions/${encodeURIComponent(executionId)}`, {
863
+ headers,
864
+ timeout: 30000,
865
+ });
866
+ const payload = response.data;
867
+ if (!payload.execution?.id) {
868
+ throw new Error('VigFlow execution response did not include execution details.');
869
+ }
870
+ return payload.execution;
871
+ });
872
+ }
873
+ buildV3AgentContext(context = {}) {
874
+ const resolvedContext = this.ensureExecutionContext(context);
875
+ const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
876
+ return JSON.stringify({
877
+ workspace: resolvedContext.workspace || null,
878
+ activeFile: resolvedContext.activeFile || null,
879
+ history: resolvedContext.history || [],
880
+ agentTaskType: resolvedContext.agentTaskType || 'general',
881
+ executionSurface: resolvedContext.executionSurface || 'cli',
882
+ clientSurface: resolvedContext.clientSurface || 'cli',
883
+ localMachineCapable: resolvedContext.localMachineCapable !== false,
884
+ workspacePath: resolvedContext.workspacePath || targetPath,
885
+ projectPath: resolvedContext.projectPath || targetPath,
886
+ targetPath,
887
+ contextId: resolvedContext.contextId,
888
+ traceId: resolvedContext.traceId,
889
+ mcpContextId: resolvedContext.mcpContextId || null,
890
+ mcp_context_id: resolvedContext.mcpContextId || null,
891
+ requestStartedAt: resolvedContext.requestStartedAt,
892
+ });
893
+ }
894
+ ensureExecutionContext(context = {}) {
895
+ const existingId = String(context.contextId || context.traceId || '').trim();
896
+ const contextId = existingId || `vig-${Date.now()}-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
897
+ return {
898
+ ...context,
899
+ contextId,
900
+ traceId: context.traceId || contextId,
901
+ requestStartedAt: context.requestStartedAt || new Date().toISOString(),
902
+ };
903
+ }
904
+ async bindExecutionContext(context = {}) {
905
+ const executionContext = this.ensureExecutionContext(context);
906
+ const headers = await this.getMcpHeaders();
907
+ const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
908
+ const metadata = {
909
+ source: 'vigthoria-cli',
910
+ sharedContextId: executionContext.contextId,
911
+ traceId: executionContext.traceId,
912
+ executionSurface: executionContext.executionSurface || 'cli',
913
+ clientSurface: executionContext.clientSurface || 'cli',
914
+ workspacePath,
915
+ requestStartedAt: executionContext.requestStartedAt,
916
+ subscriptionPlan: this.config.getNormalizedPlan() || null,
917
+ email: this.config.get('email') || null,
918
+ };
919
+ const data = {
920
+ sharedContextId: executionContext.contextId,
921
+ traceId: executionContext.traceId,
922
+ requestStartedAt: executionContext.requestStartedAt,
923
+ workspacePath,
924
+ projectPath: executionContext.projectPath || workspacePath,
925
+ targetPath: executionContext.targetPath || workspacePath,
926
+ activeFile: executionContext.activeFile || null,
927
+ executionSurface: executionContext.executionSurface || 'cli',
928
+ clientSurface: executionContext.clientSurface || 'cli',
929
+ userId: this.config.get('userId') || null,
930
+ email: this.config.get('email') || null,
931
+ };
932
+ const updateExisting = async (binding) => {
933
+ const response = await fetch(this.getMcpContextUrl(binding.backendUrl, binding.mcpContextId), {
934
+ method: 'PUT',
935
+ headers,
936
+ body: JSON.stringify({ data }),
937
+ });
938
+ if (!response.ok) {
939
+ const errorText = await response.text().catch(() => '');
940
+ throw new Error(`MCP context update ${response.status}: ${errorText.slice(0, 200)}`);
941
+ }
942
+ return {
943
+ ...executionContext,
944
+ mcpContextId: binding.mcpContextId,
945
+ mcpContextBackendUrl: binding.backendUrl,
946
+ };
947
+ };
948
+ if (executionContext.mcpContextId && executionContext.mcpContextBackendUrl) {
949
+ try {
950
+ return await updateExisting({
951
+ mcpContextId: String(executionContext.mcpContextId),
952
+ backendUrl: String(executionContext.mcpContextBackendUrl),
953
+ });
954
+ }
955
+ catch (error) {
956
+ this.logger.debug('Failed to refresh existing MCP context binding:', error.message);
957
+ }
958
+ }
959
+ for (const baseUrl of this.getMcpBaseUrls()) {
960
+ try {
961
+ const createResponse = await fetch(this.getMcpContextCreateUrl(baseUrl), {
962
+ method: 'POST',
963
+ headers,
964
+ body: JSON.stringify({
965
+ userId: this.config.get('userId') || this.config.get('email') || 'vigthoria-cli',
966
+ metadata,
967
+ }),
968
+ });
969
+ if (!createResponse.ok) {
970
+ const errorText = await createResponse.text().catch(() => '');
971
+ throw new Error(`MCP context create ${createResponse.status}: ${errorText.slice(0, 200)}`);
972
+ }
973
+ const payload = await createResponse.json();
974
+ const mcpContextId = String(payload.contextId || '').trim();
975
+ if (!mcpContextId) {
976
+ throw new Error('MCP context create response did not include a contextId');
977
+ }
978
+ return await updateExisting({ mcpContextId, backendUrl: baseUrl });
979
+ }
980
+ catch (error) {
981
+ this.logger.debug(`Failed to bind MCP context via ${baseUrl}:`, error.message);
982
+ }
983
+ }
984
+ return executionContext;
985
+ }
986
+ resolveAgentTargetPath(context = {}) {
987
+ return context.targetPath || context.projectPath || context.workspacePath || context.projectRoot || process.cwd();
988
+ }
989
+ hasAgentWorkspaceOutput(context = {}) {
990
+ try {
991
+ const root = this.resolveAgentTargetPath(context);
992
+ if (!root || !fs_1.default.existsSync(root)) {
993
+ return false;
994
+ }
995
+ const stack = [root];
996
+ while (stack.length > 0) {
997
+ const current = stack.pop();
998
+ if (!current)
999
+ continue;
1000
+ const entries = fs_1.default.readdirSync(current, { withFileTypes: true });
1001
+ for (const entry of entries) {
1002
+ if (entry.name === '.git' || entry.name === 'node_modules') {
1003
+ continue;
1004
+ }
1005
+ const fullPath = path_1.default.join(current, entry.name);
1006
+ if (entry.isFile()) {
1007
+ return true;
1008
+ }
1009
+ if (entry.isDirectory()) {
1010
+ stack.push(fullPath);
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ catch {
1016
+ return false;
1017
+ }
1018
+ return false;
1019
+ }
1020
+ getAgentWorkspaceSnapshot(rootPath) {
1021
+ const stack = [rootPath];
1022
+ let fileCount = 0;
1023
+ const entries = [];
1024
+ while (stack.length > 0) {
1025
+ const current = stack.pop();
1026
+ if (!current) {
1027
+ continue;
1028
+ }
1029
+ for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
1030
+ if (entry.name === '.git' || entry.name === 'node_modules') {
1031
+ continue;
1032
+ }
1033
+ const fullPath = path_1.default.join(current, entry.name);
1034
+ if (entry.isDirectory()) {
1035
+ stack.push(fullPath);
1036
+ continue;
1037
+ }
1038
+ if (!entry.isFile()) {
1039
+ continue;
1040
+ }
1041
+ fileCount += 1;
1042
+ const stat = fs_1.default.statSync(fullPath);
1043
+ entries.push(`${path_1.default.relative(rootPath, fullPath)}:${stat.size}:${stat.mtimeMs}`);
1044
+ }
1045
+ }
1046
+ entries.sort();
1047
+ return {
1048
+ fileCount,
1049
+ paths: entries.map((entry) => entry.split(':', 1)[0]),
1050
+ signature: entries.join('|'),
1051
+ };
1052
+ }
1053
+ async waitForAgentWorkspaceSettle(context = {}, options = {}) {
1054
+ const rootPath = this.resolveAgentTargetPath(context);
1055
+ if (!rootPath || !fs_1.default.existsSync(rootPath)) {
1056
+ return;
1057
+ }
1058
+ const timeoutMs = options.timeoutMs || 15000;
1059
+ const pollMs = options.pollMs || 300;
1060
+ const stableMs = options.stableMs || 1200;
1061
+ const expectedFiles = Array.isArray(options.expectedFiles)
1062
+ ? options.expectedFiles.map((entry) => String(entry || '').trim()).filter(Boolean)
1063
+ : [];
1064
+ const start = Date.now();
1065
+ let stableSince = 0;
1066
+ let lastSignature = '';
1067
+ while (Date.now() - start < timeoutMs) {
1068
+ const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
1069
+ if (snapshot.fileCount === 0) {
1070
+ stableSince = 0;
1071
+ lastSignature = '';
1072
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
1073
+ continue;
1074
+ }
1075
+ if (expectedFiles.length > 0 && !expectedFiles.every((filePath) => snapshot.paths.includes(filePath))) {
1076
+ stableSince = 0;
1077
+ lastSignature = snapshot.signature;
1078
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
1079
+ continue;
1080
+ }
1081
+ if (snapshot.signature === lastSignature) {
1082
+ if (!stableSince) {
1083
+ stableSince = Date.now();
1084
+ }
1085
+ if (Date.now() - stableSince >= stableMs) {
1086
+ return;
1087
+ }
1088
+ }
1089
+ else {
1090
+ lastSignature = snapshot.signature;
1091
+ stableSince = Date.now();
1092
+ }
1093
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
1094
+ }
1095
+ }
1096
+ extractExpectedWorkspaceFiles(message = '', context = {}) {
1097
+ const candidates = new Set();
1098
+ const addMatches = (value) => {
1099
+ const text = String(value || '');
1100
+ const patterns = [
1101
+ /`([^`]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))`/gi,
1102
+ /\b([A-Za-z0-9_./-]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))\b/g,
1103
+ ];
1104
+ for (const pattern of patterns) {
1105
+ let match;
1106
+ while ((match = pattern.exec(text)) !== null) {
1107
+ const filePath = match[1].trim().replace(/^\.\//, '');
1108
+ if (filePath && !filePath.startsWith('http://') && !filePath.startsWith('https://')) {
1109
+ candidates.add(filePath);
1110
+ }
1111
+ }
1112
+ }
1113
+ };
1114
+ addMatches(message);
1115
+ addMatches(context.rawMessage);
1116
+ addMatches(context.agentPrompt);
1117
+ return Array.from(candidates);
1118
+ }
1119
+ captureV3AgentStreamMutation(event, streamedFiles) {
1120
+ if (!event || event.type !== 'tool_call' || !streamedFiles) {
1121
+ return;
1122
+ }
1123
+ const args = event.arguments || {};
1124
+ if ((event.name === 'write_file' || event.name === 'edit_file') && typeof args.path === 'string') {
1125
+ const filePath = args.path.trim().replace(/\\/g, '/').replace(/^\.\//, '');
1126
+ if (!filePath) {
1127
+ return;
1128
+ }
1129
+ if (event.name === 'write_file' && typeof args.content === 'string') {
1130
+ streamedFiles[filePath] = args.content;
1131
+ return;
1132
+ }
1133
+ if (event.name === 'edit_file' && typeof args.old_string === 'string' && typeof args.new_string === 'string') {
1134
+ const existing = streamedFiles[filePath];
1135
+ if (typeof existing === 'string' && existing.includes(args.old_string)) {
1136
+ streamedFiles[filePath] = existing.replace(args.old_string, args.new_string);
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+ recoverAgentWorkspaceFiles(context = {}, streamedFiles = {}, expectedFiles = []) {
1142
+ const rootPath = this.resolveAgentTargetPath(context);
1143
+ if (!rootPath || !fs_1.default.existsSync(rootPath) || Object.keys(streamedFiles).length === 0) {
1144
+ return;
1145
+ }
1146
+ const targets = expectedFiles.length > 0 ? expectedFiles : Object.keys(streamedFiles);
1147
+ for (const relativePath of targets) {
1148
+ const content = streamedFiles[relativePath];
1149
+ if (typeof content !== 'string') {
1150
+ continue;
1151
+ }
1152
+ const absolutePath = path_1.default.join(rootPath, relativePath);
1153
+ if (fs_1.default.existsSync(absolutePath)) {
1154
+ continue;
1155
+ }
1156
+ fs_1.default.mkdirSync(path_1.default.dirname(absolutePath), { recursive: true });
1157
+ fs_1.default.writeFileSync(absolutePath, content, 'utf8');
1158
+ }
1159
+ }
1160
+ async ensureAgentFrontendPolish(message = '', context = {}) {
1161
+ const rootPath = this.resolveAgentTargetPath(context);
1162
+ if (!rootPath || !fs_1.default.existsSync(rootPath)) {
1163
+ return;
1164
+ }
1165
+ const prompt = String(message || '');
1166
+ const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
1167
+ const looksLikeFrontendTask = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase)/i.test(prompt)
1168
+ || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
1169
+ if (!looksLikeFrontendTask) {
1170
+ return;
1171
+ }
1172
+ const htmlPath = path_1.default.join(rootPath, 'index.html');
1173
+ const cssPath = path_1.default.join(rootPath, 'styles.css');
1174
+ if (!fs_1.default.existsSync(htmlPath) || !fs_1.default.existsSync(cssPath)) {
1175
+ return;
1176
+ }
1177
+ const jsCandidates = ['app.js', 'script.js', 'main.js']
1178
+ .map((fileName) => path_1.default.join(rootPath, fileName))
1179
+ .filter((filePath) => fs_1.default.existsSync(filePath));
1180
+ const jsPath = jsCandidates[0] || path_1.default.join(rootPath, 'app.js');
1181
+ const html = fs_1.default.readFileSync(htmlPath, 'utf8');
1182
+ let css = fs_1.default.readFileSync(cssPath, 'utf8');
1183
+ let js = fs_1.default.existsSync(jsPath) ? fs_1.default.readFileSync(jsPath, 'utf8') : '';
1184
+ let nextHtml = html;
1185
+ const keyframesBlocks = Array.from(js.matchAll(/@keyframes[\s\S]*?\n\}/g)).map((match) => match[0]);
1186
+ if (keyframesBlocks.length > 0) {
1187
+ const migrated = keyframesBlocks.filter((block) => !css.includes(block));
1188
+ if (migrated.length > 0) {
1189
+ css = `${css.trimEnd()}\n\n/* Vigthoria CLI Recovered CSS */\n${migrated.join('\n\n')}\n`;
1190
+ }
1191
+ js = js.replace(/\n?@keyframes[\s\S]*?\n\}/g, '').trim();
1192
+ fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
1193
+ fs_1.default.writeFileSync(jsPath, js ? `${js.trimEnd()}\n` : '', 'utf8');
1194
+ }
1195
+ const wantsPricing = /(pay-as-you-go|pricing)/i.test(prompt);
1196
+ if (wantsPricing && !/(id="pricing"|Pay-as-you-go|pay-as-you-go|pricing tiers)/i.test(nextHtml)) {
1197
+ nextHtml = this.injectSectionBeforeFooter(nextHtml, `
1198
+ <section id="pricing" class="module pricing">
1199
+ <h2>Pay-as-you-go Pricing</h2>
1200
+ <p>Start with the modules you need, scale usage by workload, and keep enterprise-grade visibility across teams.</p>
1201
+ <div class="pricing-grid">
1202
+ <article>
1203
+ <h3>Builder</h3>
1204
+ <p>Usage-based coding, workflow, and storage for product teams shipping fast.</p>
1205
+ </article>
1206
+ <article>
1207
+ <h3>Studio</h3>
1208
+ <p>Metered voice and music generation with predictable controls for creative operations.</p>
1209
+ </article>
1210
+ <article>
1211
+ <h3>Scale</h3>
1212
+ <p>Hosting, finance, and orchestration capacity that expands with customer demand.</p>
1213
+ </article>
1214
+ </div>
1215
+ </section>`);
1216
+ nextHtml = this.injectNavLink(nextHtml, 'pricing', 'Pricing');
1217
+ }
1218
+ const wantsTrust = /(trust|security|secure)/i.test(prompt);
1219
+ if (wantsTrust && !/(id="trust"|id="security"|Trust and Security|Security)/i.test(nextHtml)) {
1220
+ nextHtml = this.injectSectionBeforeFooter(nextHtml, `
1221
+ <section id="trust" class="module trust">
1222
+ <h2>Trust and Security</h2>
1223
+ <p>Role-aware access, auditable workflows, and isolated infrastructure keep sensitive workloads controlled from prototype to production.</p>
1224
+ <ul class="trust-list">
1225
+ <li>Authenticated access across CLI, Code Fork, and hosted workspaces.</li>
1226
+ <li>Scalable storage and hosting paths aligned with enterprise deployment requirements.</li>
1227
+ <li>Operational visibility for pricing, usage, and automation governance.</li>
1228
+ </ul>
1229
+ </section>`);
1230
+ nextHtml = this.injectNavLink(nextHtml, 'trust', 'Trust');
1231
+ }
1232
+ if (nextHtml !== html) {
1233
+ fs_1.default.writeFileSync(htmlPath, `${nextHtml.trimEnd()}\n`, 'utf8');
1234
+ nextHtml = fs_1.default.readFileSync(htmlPath, 'utf8');
1235
+ }
1236
+ if (/classList\.add\('hidden'\)|classList\.add\("hidden"\)|classList\.add\('revealed'\)|classList\.add\("revealed"\)/.test(js)
1237
+ && !/\.hidden\b|\.revealed\b/.test(css)) {
1238
+ css = `${css.trimEnd()}\n\n/* Vigthoria CLI Visibility States */\n.hidden {\n opacity: 0;\n transform: translateY(24px);\n}\n\n.revealed {\n opacity: 1;\n transform: translateY(0);\n transition: opacity 0.7s ease, transform 0.7s ease;\n}\n`;
1239
+ fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
1240
+ }
1241
+ const combined = `${nextHtml}\n${css}\n${js}`;
1242
+ if (/IntersectionObserver|motion-reveal|\.revealed\b|vigCliFadeIn|classList\.add\('is-visible'\)|classList\.add\("is-visible"\)/i.test(combined)) {
1243
+ return;
1244
+ }
1245
+ const cssMarker = '/* Vigthoria CLI Motion Enhancement */';
1246
+ const jsMarker = '/* Vigthoria CLI Motion Enhancement */';
1247
+ if (!css.includes(cssMarker)) {
1248
+ fs_1.default.appendFileSync(cssPath, `\n\n${cssMarker}\n.hero, section {\n opacity: 0;\n transform: translateY(24px);\n animation: vigCliFadeIn 0.8s ease forwards;\n}\n\nsection {\n animation-delay: 0.12s;\n}\n\nbutton, .cta, a {\n transition: transform 0.25s ease, opacity 0.25s ease;\n}\n\nbutton:hover, .cta:hover, a:hover {\n transform: translateY(-2px);\n}\n\n.motion-reveal {\n opacity: 0;\n transform: translateY(24px);\n transition: opacity 0.7s ease, transform 0.7s ease;\n}\n\n.motion-reveal.is-visible {\n opacity: 1;\n transform: translateY(0);\n}\n\n@keyframes vigCliFadeIn {\n from {\n opacity: 0;\n transform: translateY(24px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n`, 'utf8');
1249
+ }
1250
+ if (!js.includes(jsMarker)) {
1251
+ fs_1.default.appendFileSync(jsPath, `\n\n${jsMarker}\ndocument.addEventListener('DOMContentLoaded', () => {\n const revealTargets = document.querySelectorAll('section, .hero, .project-grid > *, .journal-preview > *');\n if (typeof IntersectionObserver !== 'function') {\n revealTargets.forEach((element) => element.classList.add('is-visible'));\n return;\n }\n\n const observer = new IntersectionObserver((entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n entry.target.classList.add('is-visible');\n observer.unobserve(entry.target);\n }\n });\n }, { threshold: 0.16 });\n\n revealTargets.forEach((element, index) => {\n element.classList.add('motion-reveal');\n element.style.transitionDelay = String(Math.min(index * 60, 320)) + 'ms';\n observer.observe(element);\n });\n});\n`, 'utf8');
1252
+ }
1253
+ }
1254
+ injectSectionBeforeFooter(html, sectionMarkup) {
1255
+ if (/<footer[\s>]/i.test(html)) {
1256
+ return html.replace(/<footer/i, `${sectionMarkup}\n <footer`);
1257
+ }
1258
+ if (/<\/body>/i.test(html)) {
1259
+ return html.replace(/\s*<\/body>/i, `${sectionMarkup}\n</body>`);
1260
+ }
1261
+ return `${html.trimEnd()}\n${sectionMarkup}\n`;
1262
+ }
1263
+ injectNavLink(html, sectionId, label) {
1264
+ if (new RegExp(`href=\"#${sectionId}\"`, 'i').test(html)) {
1265
+ return html;
1266
+ }
1267
+ if (/<\/ul>/i.test(html)) {
1268
+ return html.replace(/\s*<\/ul>/i, `\n <li><a href="#${sectionId}">${label}</a></li>\n </ul>`);
1269
+ }
1270
+ return html;
1271
+ }
1272
+ formatV3AgentResponse(data) {
1273
+ const result = data?.result || {};
1274
+ if (typeof result === 'string') {
1275
+ return result;
1276
+ }
1277
+ if (typeof result?.summary === 'string' && result.summary.trim()) {
1278
+ return result.summary;
1279
+ }
1280
+ if (typeof result?.message === 'string' && result.message.trim()) {
1281
+ return result.message;
1282
+ }
1283
+ if (Array.isArray(data?.events)) {
1284
+ const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string');
1285
+ if (completionEvent) {
1286
+ return completionEvent.summary;
1287
+ }
1288
+ const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string');
1289
+ if (messageEvent) {
1290
+ return messageEvent.content;
1291
+ }
1292
+ }
1293
+ const text = JSON.stringify(data, null, 2);
1294
+ return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
1295
+ }
1296
+ async collectV3AgentStream(response, context = {}) {
1297
+ if (!response.body || typeof response.body.getReader !== 'function') {
1298
+ return response.json();
1299
+ }
1300
+ const reader = response.body.getReader();
1301
+ const decoder = new TextDecoder();
1302
+ let buffer = '';
1303
+ const events = [];
1304
+ let final = null;
1305
+ let contextId = response.headers.get('x-context-id') || String(context.contextId || '').trim() || null;
1306
+ const streamedFiles = {};
1307
+ const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
1308
+ while (true) {
1309
+ let chunk;
1310
+ try {
1311
+ const readPromise = reader.read();
1312
+ while (true) {
1313
+ const timeoutSentinel = Symbol('v3-agent-idle-timeout');
1314
+ const result = idleTimeoutMs > 0
1315
+ ? await Promise.race([
1316
+ readPromise,
1317
+ new Promise((resolve) => {
1318
+ setTimeout(() => resolve(timeoutSentinel), idleTimeoutMs);
1319
+ }),
1320
+ ])
1321
+ : await readPromise;
1322
+ if (result !== timeoutSentinel) {
1323
+ chunk = result;
1324
+ break;
1325
+ }
1326
+ if (this.hasAgentWorkspaceOutput(context)) {
1327
+ const stalledError = new Error('V3 agent stream stalled after writing workspace output');
1328
+ stalledError.name = 'AbortError';
1329
+ stalledError.partialData = {
1330
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
1331
+ context_id: contextId,
1332
+ result: final,
1333
+ events,
1334
+ files: streamedFiles,
1335
+ };
1336
+ throw stalledError;
1337
+ }
1338
+ }
1339
+ }
1340
+ catch (error) {
1341
+ if (error && error.name === 'AbortError') {
1342
+ error.partialData = {
1343
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
1344
+ context_id: contextId,
1345
+ result: final,
1346
+ events,
1347
+ files: streamedFiles,
1348
+ };
1349
+ }
1350
+ throw error;
1351
+ }
1352
+ const { done, value } = chunk;
1353
+ if (done) {
1354
+ break;
1355
+ }
1356
+ buffer += decoder.decode(value, { stream: true });
1357
+ const lines = buffer.split('\n');
1358
+ buffer = lines.pop() || '';
1359
+ for (const line of lines) {
1360
+ if (!line.startsWith('data: ')) {
1361
+ continue;
1362
+ }
1363
+ const payload = line.slice(6).trim();
1364
+ if (!payload || payload === '[DONE]') {
1365
+ continue;
1366
+ }
1367
+ const event = JSON.parse(payload);
1368
+ events.push(event);
1369
+ if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
1370
+ contextId = event.context_id.trim();
1371
+ }
1372
+ this.captureV3AgentStreamMutation(event, streamedFiles);
1373
+ if (typeof context.onStreamEvent === 'function') {
1374
+ try {
1375
+ context.onStreamEvent(event);
1376
+ }
1377
+ catch {
1378
+ // Ignore UI callback failures; never break the agent stream for them.
1379
+ }
1380
+ }
1381
+ if (event.type === 'error') {
1382
+ if (this.hasAgentWorkspaceOutput(context)) {
1383
+ return {
1384
+ task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
1385
+ context_id: contextId,
1386
+ result: final || event,
1387
+ events,
1388
+ files: streamedFiles,
1389
+ partial: true,
1390
+ };
1391
+ }
1392
+ throw new Error(event.message || 'V3 agent returned an error');
1393
+ }
1394
+ if (event.type === 'complete' || event.type === 'message') {
1395
+ final = event;
1396
+ }
1397
+ }
1398
+ }
1399
+ return {
1400
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
1401
+ context_id: contextId,
1402
+ result: final,
1403
+ events,
1404
+ files: streamedFiles,
1405
+ };
1406
+ }
1407
+ async runV3AgentWorkflow(message, context = {}) {
1408
+ const executionContext = await this.bindExecutionContext(context);
1409
+ const timeoutMs = executionContext.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
1410
+ const errors = [];
1411
+ const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
1412
+ const requestBody = {
1413
+ request: message,
1414
+ context: this.buildV3AgentContext(executionContext),
1415
+ context_id: executionContext.contextId,
1416
+ mcp_context_id: executionContext.mcpContextId || null,
1417
+ stream: true,
1418
+ };
1419
+ for (const baseUrl of this.getV3AgentBaseUrls()) {
1420
+ const controller = new AbortController();
1421
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1422
+ try {
1423
+ const response = await this.executeV3AgentRunRequest(baseUrl, requestBody, executionContext, controller.signal);
1424
+ if (!response.ok) {
1425
+ const errorText = await response.text().catch(() => '');
1426
+ throw new Error(`V3 agent ${response.status}: ${errorText.slice(0, 200)}`);
1427
+ }
1428
+ const data = await this.collectV3AgentStream(response, executionContext);
1429
+ const contextId = data.context_id || response.headers.get('x-context-id') || executionContext.contextId || null;
1430
+ const mcpContextId = response.headers.get('x-mcp-context-id') || executionContext.mcpContextId || null;
1431
+ this.recoverAgentWorkspaceFiles(executionContext, data.files || {}, expectedFiles);
1432
+ await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
1433
+ await this.ensureAgentFrontendPolish(message, executionContext);
1434
+ const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
1435
+ return {
1436
+ content: this.formatV3AgentResponse(data),
1437
+ taskId: data.task_id || null,
1438
+ contextId,
1439
+ backendUrl: baseUrl,
1440
+ metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
1441
+ };
1442
+ }
1443
+ catch (error) {
1444
+ if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
1445
+ this.recoverAgentWorkspaceFiles(executionContext, error.partialData.files || {}, expectedFiles);
1446
+ await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
1447
+ await this.ensureAgentFrontendPolish(message, executionContext);
1448
+ const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
1449
+ return {
1450
+ content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
1451
+ taskId: error.partialData.task_id || null,
1452
+ contextId: error.partialData.context_id || executionContext.contextId || null,
1453
+ backendUrl: baseUrl,
1454
+ partial: true,
1455
+ metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || executionContext.contextId || null, mcpContextId: executionContext.mcpContextId || null, previewGate },
1456
+ };
1457
+ }
1458
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
1459
+ }
1460
+ finally {
1461
+ clearTimeout(timeoutId);
1462
+ }
1463
+ }
1464
+ const onlyUnauthorizedErrors = errors.length > 0 && errors.every((entry) => /V3 agent 401:/i.test(entry));
1465
+ const usingStoredConfigToken = !process.env.VIGTHORIA_TOKEN
1466
+ && !process.env.VIGTHORIA_AUTH_TOKEN
1467
+ && Boolean(this.config.get('authToken'));
1468
+ if (onlyUnauthorizedErrors && usingStoredConfigToken) {
1469
+ this.config.clearAuth();
1470
+ throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
1471
+ }
1472
+ throw new Error(errors.join(' | '));
1473
+ }
1474
+ formatOperatorResponse(data = {}) {
1475
+ const lines = [];
1476
+ if (data.summary) {
1477
+ lines.push(String(data.summary).trim());
1478
+ }
1479
+ if (Array.isArray(data.completed_agents) && data.completed_agents.length > 0) {
1480
+ lines.push(`Completed agents: ${data.completed_agents.join(', ')}`);
1481
+ }
1482
+ if (data.details) {
1483
+ const detailText = JSON.stringify(data.details, null, 2);
1484
+ if (detailText && detailText !== '{}') {
1485
+ lines.push(detailText.length > 12000 ? `${detailText.slice(0, 12000)}\n\n[Operator output truncated]` : detailText);
1486
+ }
1487
+ }
1488
+ return lines.filter(Boolean).join('\n\n').trim() || 'Operator workflow completed.';
1489
+ }
1490
+ async runOperatorWorkflow(message, context = {}) {
1491
+ const executionContext = await this.bindExecutionContext(context);
1492
+ const timeoutMs = context.operatorTimeoutMs || DEFAULT_OPERATOR_TIMEOUT_MS;
1493
+ const errors = [];
1494
+ const authToken = this.config.get('authToken');
1495
+ for (const baseUrl of this.getOperatorBaseUrls()) {
1496
+ const controller = new AbortController();
1497
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1498
+ try {
1499
+ const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
1500
+ method: 'POST',
1501
+ headers: {
1502
+ 'Content-Type': 'application/json',
1503
+ Accept: 'text/event-stream',
1504
+ ...(executionContext.mcpContextId ? { 'X-MCP-Context-Id': String(executionContext.mcpContextId) } : {}),
1505
+ ...(authToken ? {
1506
+ Authorization: `Bearer ${authToken}`,
1507
+ Cookie: `vigthoria-auth-token=${authToken}`,
1508
+ } : {}),
1509
+ },
1510
+ body: JSON.stringify({
1511
+ prompt: message,
1512
+ task: message,
1513
+ raw_prompt: executionContext.rawPrompt || null,
1514
+ context_id: executionContext.contextId,
1515
+ trace_id: executionContext.traceId,
1516
+ mcp_context_id: executionContext.mcpContextId || null,
1517
+ context: {
1518
+ workspace: { path: executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd() },
1519
+ workspace_path: executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd(),
1520
+ model: this.resolveModelId(executionContext.model || 'code-8b'),
1521
+ history: executionContext.history || [],
1522
+ executionSurface: executionContext.executionSurface || 'cli',
1523
+ clientSurface: executionContext.clientSurface || 'cli',
1524
+ contextId: executionContext.contextId,
1525
+ traceId: executionContext.traceId,
1526
+ mcpContextId: executionContext.mcpContextId || null,
1527
+ mcp_context_id: executionContext.mcpContextId || null,
1528
+ rawPrompt: executionContext.rawPrompt || null,
1529
+ requestStartedAt: executionContext.requestStartedAt,
1530
+ },
1531
+ workflow_type: executionContext.workflowType || 'analysis_only',
1532
+ options: {
1533
+ stream: true,
1534
+ save_to_vigflow: executionContext.savePlanToVigFlow === true,
1535
+ },
1536
+ }),
1537
+ signal: controller.signal,
1538
+ });
1539
+ if (!response.ok) {
1540
+ const errorText = await response.text().catch(() => '');
1541
+ throw new Error(`Operator stream ${response.status}: ${errorText.slice(0, 200)}`);
1542
+ }
1543
+ if (!response.body || typeof response.body.getReader !== 'function') {
1544
+ const fallbackData = await response.json();
1545
+ return {
1546
+ content: this.formatOperatorResponse(fallbackData),
1547
+ workflowId: fallbackData.workflow_id || null,
1548
+ contextId: fallbackData.context_id || response.headers.get('x-context-id') || executionContext.contextId || null,
1549
+ backendUrl: baseUrl,
1550
+ savedWorkflow: fallbackData.saved_workflow || null,
1551
+ metadata: {
1552
+ source: 'operator',
1553
+ mode: 'operator',
1554
+ contextId: fallbackData.context_id || response.headers.get('x-context-id') || executionContext.contextId || null,
1555
+ mcpContextId: fallbackData.mcp_context_id || response.headers.get('x-mcp-context-id') || executionContext.mcpContextId || null,
1556
+ },
1557
+ };
1558
+ }
1559
+ const reader = response.body.getReader();
1560
+ const decoder = new TextDecoder();
1561
+ let buffer = '';
1562
+ let workflowId = null;
1563
+ let contextId = response.headers.get('x-context-id') || executionContext.contextId || null;
1564
+ let mcpContextId = response.headers.get('x-mcp-context-id') || executionContext.mcpContextId || null;
1565
+ let resultPayload = null;
1566
+ let savedWorkflow = null;
1567
+ while (true) {
1568
+ const { done, value } = await reader.read();
1569
+ if (done) {
1570
+ break;
1571
+ }
1572
+ buffer += decoder.decode(value, { stream: true });
1573
+ const chunks = buffer.split('\n\n');
1574
+ buffer = chunks.pop() || '';
1575
+ for (const chunk of chunks) {
1576
+ const lines = chunk.split('\n');
1577
+ let eventName = 'message';
1578
+ const dataLines = [];
1579
+ for (const line of lines) {
1580
+ if (line.startsWith('event:')) {
1581
+ eventName = line.slice(6).trim();
1582
+ }
1583
+ else if (line.startsWith('data:')) {
1584
+ dataLines.push(line.slice(5).trim());
1585
+ }
1586
+ }
1587
+ if (dataLines.length === 0) {
1588
+ continue;
1589
+ }
1590
+ let payload = null;
1591
+ try {
1592
+ payload = JSON.parse(dataLines.join('\n'));
1593
+ }
1594
+ catch {
1595
+ payload = { raw: dataLines.join('\n') };
1596
+ }
1597
+ if (payload.workflow_id && !workflowId) {
1598
+ workflowId = payload.workflow_id;
1599
+ }
1600
+ if (!contextId && typeof payload.context_id === 'string' && payload.context_id.trim()) {
1601
+ contextId = payload.context_id.trim();
1602
+ }
1603
+ if (!mcpContextId && typeof payload.mcp_context_id === 'string' && payload.mcp_context_id.trim()) {
1604
+ mcpContextId = payload.mcp_context_id.trim();
1605
+ }
1606
+ if (!savedWorkflow && payload.saved_workflow && typeof payload.saved_workflow === 'object') {
1607
+ savedWorkflow = payload.saved_workflow;
1608
+ }
1609
+ if (typeof context.onStreamEvent === 'function') {
1610
+ try {
1611
+ context.onStreamEvent({ type: eventName, ...payload, context_id: payload.context_id || contextId, mcp_context_id: payload.mcp_context_id || mcpContextId });
1612
+ }
1613
+ catch {
1614
+ // UI callback failures must not break operator execution.
1615
+ }
1616
+ }
1617
+ if (eventName === 'result') {
1618
+ resultPayload = payload;
1619
+ }
1620
+ if (eventName === 'error') {
1621
+ throw new Error(payload.error || payload.message || 'Operator workflow failed');
1622
+ }
1623
+ }
1624
+ }
1625
+ return {
1626
+ content: this.formatOperatorResponse(resultPayload || {}),
1627
+ workflowId,
1628
+ contextId,
1629
+ backendUrl: baseUrl,
1630
+ savedWorkflow,
1631
+ metadata: { source: 'operator', mode: 'operator', contextId, mcpContextId, savedWorkflowId: savedWorkflow?.id || null },
1632
+ };
1633
+ }
1634
+ catch (error) {
1635
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
1636
+ }
1637
+ finally {
1638
+ clearTimeout(timeoutId);
1639
+ }
1640
+ }
1641
+ throw new Error(errors.join(' | '));
1642
+ }
189
1643
  /**
190
1644
  * Chat API - Direct Vigthoria Models API Architecture
191
1645
  *
@@ -199,7 +1653,9 @@ class APIClient {
199
1653
  */
200
1654
  async chat(messages, model, useLocal = false) {
201
1655
  const resolvedModel = this.resolveModelId(model);
202
- const candidateModels = [resolvedModel];
1656
+ const candidateModels = this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()
1657
+ ? [this.getSelfHostedFallbackModelId(resolvedModel, model)]
1658
+ : [resolvedModel];
203
1659
  const fallbackModel = this.getFallbackModelId(resolvedModel);
204
1660
  if (fallbackModel && fallbackModel !== resolvedModel) {
205
1661
  candidateModels.push(fallbackModel);
@@ -216,6 +1672,9 @@ class APIClient {
216
1672
  // No more localhost fallbacks - CLI is for external users!
217
1673
  throw new Error('AI service unavailable. Please check your internet connection or try again later.');
218
1674
  }
1675
+ shouldSkipCloudRoutes(resolvedModel) {
1676
+ return this.shouldSimulateCloudFailure() && this.isCloudModelId(resolvedModel);
1677
+ }
219
1678
  async tryChatWithModel(messages, resolvedModel, requestedModel) {
220
1679
  const preferSelfHostedFirst = this.isSelfHostedPreferredModel(resolvedModel, requestedModel);
221
1680
  if (preferSelfHostedFirst) {
@@ -225,7 +1684,7 @@ class APIClient {
225
1684
  }
226
1685
  }
227
1686
  // STRATEGY 1: Direct Vigthoria Models API (api.vigthoria.io)
228
- if (!this.shouldSimulateCloudFailure()) {
1687
+ if (!this.shouldSkipCloudRoutes(resolvedModel)) {
229
1688
  try {
230
1689
  this.logger.debug(`Direct Vigthoria Models API: ${resolvedModel}`);
231
1690
  const token = this.config.get('authToken');
@@ -260,10 +1719,10 @@ class APIClient {
260
1719
  }
261
1720
  }
262
1721
  else {
263
- this.logger.debug(`Simulating cloud failure for ${resolvedModel}; skipping public API route.`);
1722
+ this.logger.debug(`Simulating cloud failure for ${resolvedModel}; skipping cloud-backed public API route.`);
264
1723
  }
265
1724
  // STRATEGY 2: Vigthoria Cloud API via Coder (authenticated users only)
266
- if (this.config.isAuthenticated() && !this.shouldSimulateCloudFailure()) {
1725
+ if (this.config.isAuthenticated() && !this.shouldSkipCloudRoutes(resolvedModel)) {
267
1726
  try {
268
1727
  this.logger.debug(`Vigthoria Cloud API fallback: ${resolvedModel}`);
269
1728
  const response = await this.client.post('/api/ai/chat', {
@@ -302,7 +1761,7 @@ class APIClient {
302
1761
  return null;
303
1762
  }
304
1763
  async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel) {
305
- if (!this.shouldTrySelfHostedFallback(resolvedModel, requestedModel)) {
1764
+ if (!this.selfHostedModelRouterClient || !this.shouldTrySelfHostedFallback(resolvedModel, requestedModel)) {
306
1765
  return null;
307
1766
  }
308
1767
  const selfHostedModel = this.getSelfHostedFallbackModelId(resolvedModel, requestedModel);
@@ -344,16 +1803,41 @@ class APIClient {
344
1803
  const cloudModels = new Set([
345
1804
  'deepseek-v3.1:671b-cloud',
346
1805
  'moonshotai/kimi-k2.5',
1806
+ 'vigthoria-cloud-pro',
1807
+ 'vigthoria-cloud-k2',
1808
+ 'vigthoria-cloud-ultra',
347
1809
  ]);
348
1810
  if (cloudModels.has(resolvedModel)) {
349
1811
  return 'vigthoria-v3-code-30b';
350
1812
  }
351
1813
  return null;
352
1814
  }
1815
+ isCloudModelId(resolvedModel) {
1816
+ return resolvedModel === 'deepseek-v3.1:671b-cloud'
1817
+ || resolvedModel === 'moonshotai/kimi-k2.5'
1818
+ || resolvedModel === 'vigthoria-cloud-pro'
1819
+ || resolvedModel === 'vigthoria-cloud-k2'
1820
+ || resolvedModel === 'vigthoria-cloud-ultra';
1821
+ }
1822
+ canUseCloudModel() {
1823
+ return this.config.hasCloudAccess();
1824
+ }
1825
+ resolvePermittedModelId(shortName) {
1826
+ const resolvedModel = this.resolveModelId(shortName);
1827
+ if (this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()) {
1828
+ const fallbackModel = this.getSelfHostedFallbackModelId(resolvedModel, shortName);
1829
+ this.logger.debug(`Blocked unauthorized cloud model ${shortName}; using fallback ${fallbackModel}`);
1830
+ return fallbackModel;
1831
+ }
1832
+ return resolvedModel;
1833
+ }
353
1834
  shouldSimulateCloudFailure() {
354
1835
  return process.env.VIGTHORIA_SIMULATE_CLOUD_FAILURE === '1';
355
1836
  }
356
1837
  shouldTrySelfHostedFallback(resolvedModel, requestedModel) {
1838
+ if (!this.selfHostedModelRouterClient) {
1839
+ return false;
1840
+ }
357
1841
  const normalizedRequested = String(requestedModel || '').toLowerCase();
358
1842
  return this.isSelfHostedPreferredModel(resolvedModel, requestedModel)
359
1843
  || normalizedRequested === 'cloud'
@@ -392,7 +1876,7 @@ class APIClient {
392
1876
  ws.send(JSON.stringify({
393
1877
  type: 'chat',
394
1878
  messages,
395
- model: this.resolveModelId(model),
1879
+ model: this.resolvePermittedModelId(model),
396
1880
  stream: true,
397
1881
  }));
398
1882
  });
@@ -421,7 +1905,7 @@ class APIClient {
421
1905
  ws.send(JSON.stringify({
422
1906
  type: 'chat',
423
1907
  messages,
424
- model: this.resolveModelId(model),
1908
+ model: this.resolvePermittedModelId(model),
425
1909
  stream: true,
426
1910
  }));
427
1911
  });
@@ -461,7 +1945,7 @@ class APIClient {
461
1945
  const response = await this.client.post('/api/ai/generate', {
462
1946
  prompt,
463
1947
  language,
464
- model: this.resolveModelId(model),
1948
+ model: this.resolvePermittedModelId(model),
465
1949
  });
466
1950
  return response.data.code;
467
1951
  }
@@ -470,7 +1954,7 @@ class APIClient {
470
1954
  const response = await this.client.post('/api/ai/generate-project', {
471
1955
  prompt,
472
1956
  projectType,
473
- model: this.resolveModelId(model),
1957
+ model: this.resolvePermittedModelId(model),
474
1958
  }, {
475
1959
  timeout: 300000, // 5 minutes for complex generation
476
1960
  });
@@ -510,7 +1994,7 @@ class APIClient {
510
1994
  // VIGTHORIA LOCAL - Self-hosted models
511
1995
  // ═══════════════════════════════════════════════════════════════
512
1996
  'fast': 'vigthoria-fast-1.7b',
513
- 'mini': 'vigthoria-fast-1.7b',
1997
+ 'mini': 'vigthoria-mini-0.6b',
514
1998
  'balanced': 'vigthoria-balanced-4b',
515
1999
  'creative': 'vigthoria-creative-9b-v4',
516
2000
  // Code Models - 30B is the default powerhouse
@@ -522,9 +2006,9 @@ class APIClient {
522
2006
  // ═══════════════════════════════════════════════════════════════
523
2007
  // VIGTHORIA CLOUD - Premium cloud models (internal routing)
524
2008
  // ═══════════════════════════════════════════════════════════════
525
- 'cloud': 'deepseek-v3.1:671b-cloud',
526
- 'cloud-reason': 'moonshotai/kimi-k2.5',
527
- 'ultra': 'deepseek-v3.1:671b-cloud',
2009
+ 'cloud': 'vigthoria-cloud-pro',
2010
+ 'cloud-reason': 'vigthoria-cloud-k2',
2011
+ 'ultra': 'vigthoria-cloud-ultra',
528
2012
  };
529
2013
  // If already a full model ID, return as-is
530
2014
  if (shortName.includes('vigthoria') || shortName.includes('/') || shortName.includes(':')) {
@@ -533,18 +2017,276 @@ class APIClient {
533
2017
  }
534
2018
  return shortName;
535
2019
  }
536
- return modelMap[shortName] || 'qwen3-coder:latest'; // Default to 30B
2020
+ return modelMap[shortName] || 'vigthoria-v3-code-30b';
537
2021
  }
538
- // Health check
539
- async healthCheck() {
2022
+ async getCoderHealth() {
540
2023
  try {
541
2024
  const response = await this.client.get('/api/health', { timeout: 10000 });
542
- return response.data?.status === 'ok' || response.data?.healthy === true;
2025
+ const ok = response.data?.status === 'ok' || response.data?.healthy === true;
2026
+ return {
2027
+ name: 'Coder API',
2028
+ endpoint: `${this.config.get('apiUrl')}/api/health`,
2029
+ ok,
2030
+ details: { health: response.data },
2031
+ };
543
2032
  }
544
- catch {
545
- return false;
2033
+ catch (error) {
2034
+ return {
2035
+ name: 'Coder API',
2036
+ endpoint: `${this.config.get('apiUrl')}/api/health`,
2037
+ ok: false,
2038
+ error: error.message,
2039
+ };
546
2040
  }
547
2041
  }
2042
+ async getModelsHealth() {
2043
+ const modelsApiUrl = this.config.get('modelsApiUrl');
2044
+ try {
2045
+ const [healthResponse, modelsResponse] = await Promise.all([
2046
+ this.modelRouterClient.get('/health', { timeout: 10000 }),
2047
+ this.modelRouterClient.get('/v1/models', { timeout: 15000 }),
2048
+ ]);
2049
+ const healthOk = healthResponse.data?.status === 'healthy'
2050
+ || healthResponse.data?.status === 'ok'
2051
+ || healthResponse.data?.healthy === true;
2052
+ const modelCount = Array.isArray(modelsResponse.data?.data) ? modelsResponse.data.data.length : 0;
2053
+ return {
2054
+ name: 'Models API',
2055
+ endpoint: `${modelsApiUrl}/health`,
2056
+ ok: healthOk && modelCount > 0,
2057
+ details: {
2058
+ health: healthResponse.data,
2059
+ modelCount,
2060
+ },
2061
+ };
2062
+ }
2063
+ catch (error) {
2064
+ return {
2065
+ name: 'Models API',
2066
+ endpoint: `${modelsApiUrl}/health`,
2067
+ ok: false,
2068
+ error: error.message,
2069
+ };
2070
+ }
2071
+ }
2072
+ async getSelfHostedHealth() {
2073
+ const selfHostedModelsApiUrl = this.getSelfHostedModelsApiUrl();
2074
+ if (!selfHostedModelsApiUrl || !this.selfHostedModelRouterClient) {
2075
+ return null;
2076
+ }
2077
+ try {
2078
+ const response = await this.selfHostedModelRouterClient.get('/health', { timeout: 10000 });
2079
+ const ok = response.data?.status === 'healthy'
2080
+ || response.data?.status === 'ok'
2081
+ || response.data?.healthy === true;
2082
+ return {
2083
+ name: 'Self-hosted Models API',
2084
+ endpoint: `${selfHostedModelsApiUrl}/health`,
2085
+ ok,
2086
+ details: { health: response.data },
2087
+ };
2088
+ }
2089
+ catch (error) {
2090
+ return {
2091
+ name: 'Self-hosted Models API',
2092
+ endpoint: `${selfHostedModelsApiUrl}/health`,
2093
+ ok: false,
2094
+ error: error.message,
2095
+ };
2096
+ }
2097
+ }
2098
+ async getV3AgentHealth() {
2099
+ const endpoint = this.getV3AgentRunUrl(this.getV3AgentBaseUrls()[0]).replace('/api/agent/run', '/health');
2100
+ try {
2101
+ const response = await fetch(endpoint, {
2102
+ method: 'GET',
2103
+ headers: await this.getV3AgentHeaders(),
2104
+ });
2105
+ if (!response.ok) {
2106
+ throw new Error(`V3 health ${response.status}`);
2107
+ }
2108
+ const data = await response.json();
2109
+ const ok = data?.status === 'ok' || data?.healthy === true;
2110
+ return {
2111
+ name: 'V3 Agent',
2112
+ endpoint,
2113
+ ok,
2114
+ details: { health: data },
2115
+ };
2116
+ }
2117
+ catch (error) {
2118
+ return {
2119
+ name: 'V3 Agent',
2120
+ endpoint,
2121
+ ok: false,
2122
+ error: error instanceof Error ? error.message : String(error),
2123
+ };
2124
+ }
2125
+ }
2126
+ async getHyperLoopHealth() {
2127
+ const endpoint = process.env.VIGTHORIA_HYPERLOOP_URL || 'http://127.0.0.1:8020/api/hyperloop/health';
2128
+ try {
2129
+ const token = this.getAccessToken();
2130
+ const response = await fetch(endpoint, {
2131
+ method: 'GET',
2132
+ headers: token ? { Authorization: `Bearer ${token}` } : undefined,
2133
+ });
2134
+ if (!response.ok) {
2135
+ throw new Error(`Hyper Loop health ${response.status}`);
2136
+ }
2137
+ const data = await response.json();
2138
+ const ok = data?.status === 'healthy' || data?.status === 'ok' || data?.healthy === true;
2139
+ return {
2140
+ name: 'Hyper Loop',
2141
+ endpoint,
2142
+ ok,
2143
+ details: { health: data },
2144
+ };
2145
+ }
2146
+ catch (error) {
2147
+ return {
2148
+ name: 'Hyper Loop',
2149
+ endpoint,
2150
+ ok: false,
2151
+ error: error instanceof Error ? error.message : String(error),
2152
+ };
2153
+ }
2154
+ }
2155
+ async getRepoMemoryHealth(context = {}) {
2156
+ const endpoint = process.env.VIGTHORIA_HYPERLOOP_EXECUTE_URL || 'http://127.0.0.1:8020/api/hyperloop/execute';
2157
+ const modulesEndpoint = process.env.VIGTHORIA_HYPERLOOP_MODULES_URL || 'http://127.0.0.1:8020/api/hyperloop/modules';
2158
+ const token = this.getAccessToken();
2159
+ const projectPath = this.resolveAgentTargetPath(context);
2160
+ try {
2161
+ const modulesResponse = await fetch(modulesEndpoint, {
2162
+ method: 'GET',
2163
+ headers: {
2164
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
2165
+ },
2166
+ });
2167
+ if (!modulesResponse.ok) {
2168
+ throw new Error(`Repo memory modules ${modulesResponse.status}`);
2169
+ }
2170
+ const modulesData = await modulesResponse.json();
2171
+ const modules = Array.isArray(modulesData?.modules) ? modulesData.modules : [];
2172
+ const compactor = modules.find((entry) => entry && entry.name === 'repo_context_compactor');
2173
+ let compactContextLength = 0;
2174
+ try {
2175
+ const probeResponse = await fetch(endpoint, {
2176
+ method: 'POST',
2177
+ headers: {
2178
+ 'Content-Type': 'application/json',
2179
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
2180
+ },
2181
+ body: JSON.stringify({
2182
+ module: 'repo_context_compactor',
2183
+ payload: {
2184
+ path: projectPath,
2185
+ request_focus: 'Capability truth status probe',
2186
+ max_files: 20,
2187
+ max_chars: 800,
2188
+ },
2189
+ context: {
2190
+ project_path: projectPath,
2191
+ source: 'vigthoria-cli-capability-truth',
2192
+ },
2193
+ }),
2194
+ });
2195
+ if (probeResponse.ok) {
2196
+ const probeData = await probeResponse.json();
2197
+ compactContextLength = String(probeData?.result?.compact_context || '').length;
2198
+ }
2199
+ }
2200
+ catch {
2201
+ // Availability is determined by the module registry; direct compactor output is supplemental only.
2202
+ }
2203
+ const ok = Boolean(compactor && compactor.initialized !== false);
2204
+ return {
2205
+ name: 'Repo Memory',
2206
+ endpoint: modulesEndpoint,
2207
+ ok,
2208
+ details: {
2209
+ projectPath,
2210
+ compactContextLength,
2211
+ moduleRegistered: Boolean(compactor),
2212
+ },
2213
+ ...(ok ? {} : { error: 'repo_context_compactor is not registered in Hyper Loop' }),
2214
+ };
2215
+ }
2216
+ catch (error) {
2217
+ return {
2218
+ name: 'Repo Memory',
2219
+ endpoint: modulesEndpoint,
2220
+ ok: false,
2221
+ error: error instanceof Error ? error.message : String(error),
2222
+ };
2223
+ }
2224
+ }
2225
+ async getDevtoolsBridgeStatus() {
2226
+ const host = process.env.VIGTHORIA_DEVTOOLS_BRIDGE_HOST || '127.0.0.1';
2227
+ const port = Number.parseInt(process.env.VIGTHORIA_DEVTOOLS_BRIDGE_PORT || '4016', 10);
2228
+ const endpoint = `ws://${host}:${port}/ws`;
2229
+ return new Promise((resolve) => {
2230
+ const socket = net_1.default.connect({ host, port, timeout: 1500 }, () => {
2231
+ socket.end();
2232
+ resolve({
2233
+ name: 'DevTools Bridge',
2234
+ endpoint,
2235
+ ok: true,
2236
+ details: { host, port },
2237
+ });
2238
+ });
2239
+ socket.on('timeout', () => {
2240
+ socket.destroy();
2241
+ resolve({
2242
+ name: 'DevTools Bridge',
2243
+ endpoint,
2244
+ ok: false,
2245
+ error: 'Connection timed out',
2246
+ });
2247
+ });
2248
+ socket.on('error', (error) => {
2249
+ resolve({
2250
+ name: 'DevTools Bridge',
2251
+ endpoint,
2252
+ ok: false,
2253
+ error: error.message,
2254
+ });
2255
+ });
2256
+ });
2257
+ }
2258
+ async getCapabilityTruthStatus(context = {}) {
2259
+ const [v3Agent, hyperLoop, repoMemory, devtoolsBridge] = await Promise.all([
2260
+ this.getV3AgentHealth(),
2261
+ this.getHyperLoopHealth(),
2262
+ this.getRepoMemoryHealth(context),
2263
+ this.getDevtoolsBridgeStatus(),
2264
+ ]);
2265
+ return {
2266
+ overallOk: v3Agent.ok && hyperLoop.ok && repoMemory.ok,
2267
+ v3Agent,
2268
+ hyperLoop,
2269
+ repoMemory,
2270
+ devtoolsBridge,
2271
+ };
2272
+ }
2273
+ async getHealthStatus() {
2274
+ const [coder, models, selfHosted] = await Promise.all([
2275
+ this.getCoderHealth(),
2276
+ this.getModelsHealth(),
2277
+ this.getSelfHostedHealth(),
2278
+ ]);
2279
+ return {
2280
+ overallOk: coder.ok && models.ok,
2281
+ coder,
2282
+ models,
2283
+ selfHosted,
2284
+ };
2285
+ }
2286
+ // Health check
2287
+ async healthCheck() {
2288
+ const status = await this.getHealthStatus();
2289
+ return status.overallOk;
2290
+ }
548
2291
  }
549
2292
  exports.APIClient = APIClient;
550
- //# sourceMappingURL=api.js.map