vigthoria-cli 1.6.4 → 1.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/utils/api.js CHANGED
@@ -9,8 +9,20 @@ 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 fs_1 = __importDefault(require("fs"));
12
13
  const https_1 = __importDefault(require("https"));
14
+ const path_1 = __importDefault(require("path"));
13
15
  const ws_1 = __importDefault(require("ws"));
16
+ const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
17
+ const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS || '1200000';
18
+ const parsed = Number.parseInt(rawValue, 10);
19
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 1200000;
20
+ })();
21
+ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
22
+ const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS || '90000';
23
+ const parsed = Number.parseInt(rawValue, 10);
24
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 90000;
25
+ })();
14
26
  class APIClient {
15
27
  client;
16
28
  modelRouterClient;
@@ -34,7 +46,7 @@ class APIClient {
34
46
  httpsAgent,
35
47
  headers: {
36
48
  'Content-Type': 'application/json',
37
- 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.4'}`,
49
+ 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.8'}`,
38
50
  },
39
51
  });
40
52
  // Direct AI Models API - bypasses Coder for direct model access
@@ -45,18 +57,19 @@ class APIClient {
45
57
  httpsAgent,
46
58
  headers: {
47
59
  'Content-Type': 'application/json',
48
- 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.4'}`,
60
+ 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.8'}`,
49
61
  },
50
62
  });
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',
63
+ // Self-hosted model router is opt-in only.
64
+ const selfHostedModelsApiUrl = this.getSelfHostedModelsApiUrl();
65
+ this.selfHostedModelRouterClient = selfHostedModelsApiUrl ? axios_1.default.create({
66
+ baseURL: selfHostedModelsApiUrl,
54
67
  timeout: 240000,
55
68
  headers: {
56
69
  'Content-Type': 'application/json',
57
- 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.4'}`,
70
+ 'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.8'}`,
58
71
  },
59
- });
72
+ }) : null;
60
73
  // Add auth interceptor
61
74
  this.client.interceptors.request.use((req) => {
62
75
  const token = this.config.get('authToken');
@@ -74,7 +87,7 @@ class APIClient {
74
87
  }
75
88
  return req;
76
89
  });
77
- this.selfHostedModelRouterClient.interceptors.request.use((req) => {
90
+ this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
78
91
  const token = this.config.get('authToken');
79
92
  if (token) {
80
93
  req.headers.Authorization = `Bearer ${token}`;
@@ -92,6 +105,15 @@ class APIClient {
92
105
  throw error;
93
106
  });
94
107
  }
108
+ getSelfHostedModelsApiUrl() {
109
+ const configuredUrl = process.env.VIGTHORIA_SELF_HOSTED_MODELS_API_URL
110
+ || this.config.get('selfHostedModelsApiUrl');
111
+ if (!configuredUrl) {
112
+ return null;
113
+ }
114
+ const normalizedUrl = String(configuredUrl).trim().replace(/\/$/, '');
115
+ return normalizedUrl || null;
116
+ }
95
117
  // Authentication - Uses Vigthoria Coder /api/login endpoint
96
118
  async login(email, password) {
97
119
  try {
@@ -124,26 +146,42 @@ class APIClient {
124
146
  try {
125
147
  // Validate token by making a request to user info endpoint
126
148
  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;
149
+ const headers = {
150
+ Authorization: `Bearer ${token}`,
151
+ Cookie: `vigthoria-auth-token=${token}`,
152
+ };
153
+ const candidateEndpoints = [
154
+ '/api/user/profile',
155
+ '/api/user/info',
156
+ '/api/auth/me',
157
+ ];
158
+ for (const endpoint of candidateEndpoints) {
159
+ try {
160
+ const response = await this.client.get(endpoint, { headers });
161
+ const profile = this.extractUserProfile(response.data);
162
+ if (!profile) {
163
+ continue;
164
+ }
165
+ this.config.setAuth({
166
+ token,
167
+ userId: profile.id,
168
+ email: profile.email,
169
+ });
170
+ this.config.setSubscription({
171
+ plan: profile.plan,
172
+ status: 'active',
173
+ expiresAt: undefined,
174
+ });
175
+ return true;
176
+ }
177
+ catch (error) {
178
+ const axiosError = error;
179
+ if (axiosError.response?.status && axiosError.response.status !== 404) {
180
+ throw error;
181
+ }
182
+ }
146
183
  }
184
+ this.config.clearAuth();
147
185
  return false;
148
186
  }
149
187
  catch (error) {
@@ -152,6 +190,27 @@ class APIClient {
152
190
  return false;
153
191
  }
154
192
  }
193
+ extractUserProfile(data) {
194
+ if (!data) {
195
+ return null;
196
+ }
197
+ const rawUser = data.user || data;
198
+ const userId = rawUser.id;
199
+ const email = rawUser.email;
200
+ if (!userId || !email) {
201
+ return null;
202
+ }
203
+ const plan = data.subscription?.plan
204
+ || rawUser.subscription?.plan
205
+ || rawUser.subscription_plan
206
+ || data.subscription_plan
207
+ || 'developer';
208
+ return {
209
+ id: userId,
210
+ email,
211
+ plan,
212
+ };
213
+ }
155
214
  async refreshToken() {
156
215
  const refreshToken = this.config.get('refreshToken');
157
216
  if (!refreshToken)
@@ -186,6 +245,519 @@ class APIClient {
186
245
  this.logger.debug('Failed to get subscription status:', error.message);
187
246
  }
188
247
  }
248
+ getAccessToken() {
249
+ return process.env.VIGTHORIA_TOKEN
250
+ || process.env.VIGTHORIA_AUTH_TOKEN
251
+ || this.config.get('authToken')
252
+ || null;
253
+ }
254
+ getV3AgentBaseUrls() {
255
+ const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
256
+ const urls = [
257
+ process.env.VIGTHORIA_V3_AGENT_URL,
258
+ process.env.V3_AGENT_URL,
259
+ 'http://127.0.0.1:8030',
260
+ configuredApiUrl,
261
+ ].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
262
+ return [...new Set(urls)];
263
+ }
264
+ getV3AgentRunUrl(baseUrl) {
265
+ if (/127\.0\.0\.1:8030|localhost:8030/.test(baseUrl)) {
266
+ return `${baseUrl}/api/agent/run`;
267
+ }
268
+ return `${baseUrl}/api/v3-agent/run`;
269
+ }
270
+ async getV3AgentHeaders() {
271
+ const headers = {
272
+ 'Content-Type': 'application/json',
273
+ };
274
+ const authToken = this.getAccessToken();
275
+ if (authToken) {
276
+ headers.Authorization = `Bearer ${authToken}`;
277
+ }
278
+ const serviceKey = process.env.VIGTHORIA_V3_SERVICE_KEY
279
+ || process.env.V3_SERVICE_KEY
280
+ || process.env.HYPERLOOP_SERVICE_KEY;
281
+ if (serviceKey) {
282
+ headers['X-Service-Key'] = serviceKey;
283
+ }
284
+ return headers;
285
+ }
286
+ buildV3AgentContext(context = {}) {
287
+ const targetPath = context.targetPath || context.projectPath || context.workspacePath || context.projectRoot || process.cwd();
288
+ return JSON.stringify({
289
+ workspace: context.workspace || null,
290
+ activeFile: context.activeFile || null,
291
+ history: context.history || [],
292
+ agentTaskType: context.agentTaskType || 'general',
293
+ executionSurface: context.executionSurface || 'cli',
294
+ clientSurface: context.clientSurface || 'cli',
295
+ localMachineCapable: context.localMachineCapable !== false,
296
+ workspacePath: context.workspacePath || targetPath,
297
+ projectPath: context.projectPath || targetPath,
298
+ targetPath,
299
+ });
300
+ }
301
+ resolveAgentTargetPath(context = {}) {
302
+ return context.targetPath || context.projectPath || context.workspacePath || context.projectRoot || process.cwd();
303
+ }
304
+ hasAgentWorkspaceOutput(context = {}) {
305
+ try {
306
+ const root = this.resolveAgentTargetPath(context);
307
+ if (!root || !fs_1.default.existsSync(root)) {
308
+ return false;
309
+ }
310
+ const stack = [root];
311
+ while (stack.length > 0) {
312
+ const current = stack.pop();
313
+ if (!current)
314
+ continue;
315
+ const entries = fs_1.default.readdirSync(current, { withFileTypes: true });
316
+ for (const entry of entries) {
317
+ if (entry.name === '.git' || entry.name === 'node_modules') {
318
+ continue;
319
+ }
320
+ const fullPath = path_1.default.join(current, entry.name);
321
+ if (entry.isFile()) {
322
+ return true;
323
+ }
324
+ if (entry.isDirectory()) {
325
+ stack.push(fullPath);
326
+ }
327
+ }
328
+ }
329
+ }
330
+ catch {
331
+ return false;
332
+ }
333
+ return false;
334
+ }
335
+ getAgentWorkspaceSnapshot(rootPath) {
336
+ const stack = [rootPath];
337
+ let fileCount = 0;
338
+ const entries = [];
339
+ while (stack.length > 0) {
340
+ const current = stack.pop();
341
+ if (!current) {
342
+ continue;
343
+ }
344
+ for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
345
+ if (entry.name === '.git' || entry.name === 'node_modules') {
346
+ continue;
347
+ }
348
+ const fullPath = path_1.default.join(current, entry.name);
349
+ if (entry.isDirectory()) {
350
+ stack.push(fullPath);
351
+ continue;
352
+ }
353
+ if (!entry.isFile()) {
354
+ continue;
355
+ }
356
+ fileCount += 1;
357
+ const stat = fs_1.default.statSync(fullPath);
358
+ entries.push(`${path_1.default.relative(rootPath, fullPath)}:${stat.size}:${stat.mtimeMs}`);
359
+ }
360
+ }
361
+ entries.sort();
362
+ return {
363
+ fileCount,
364
+ paths: entries.map((entry) => entry.split(':', 1)[0]),
365
+ signature: entries.join('|'),
366
+ };
367
+ }
368
+ async waitForAgentWorkspaceSettle(context = {}, options = {}) {
369
+ const rootPath = this.resolveAgentTargetPath(context);
370
+ if (!rootPath || !fs_1.default.existsSync(rootPath)) {
371
+ return;
372
+ }
373
+ const timeoutMs = options.timeoutMs || 15000;
374
+ const pollMs = options.pollMs || 300;
375
+ const stableMs = options.stableMs || 1200;
376
+ const expectedFiles = Array.isArray(options.expectedFiles)
377
+ ? options.expectedFiles.map((entry) => String(entry || '').trim()).filter(Boolean)
378
+ : [];
379
+ const start = Date.now();
380
+ let stableSince = 0;
381
+ let lastSignature = '';
382
+ while (Date.now() - start < timeoutMs) {
383
+ const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
384
+ if (snapshot.fileCount === 0) {
385
+ stableSince = 0;
386
+ lastSignature = '';
387
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
388
+ continue;
389
+ }
390
+ if (expectedFiles.length > 0 && !expectedFiles.every((filePath) => snapshot.paths.includes(filePath))) {
391
+ stableSince = 0;
392
+ lastSignature = snapshot.signature;
393
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
394
+ continue;
395
+ }
396
+ if (snapshot.signature === lastSignature) {
397
+ if (!stableSince) {
398
+ stableSince = Date.now();
399
+ }
400
+ if (Date.now() - stableSince >= stableMs) {
401
+ return;
402
+ }
403
+ }
404
+ else {
405
+ lastSignature = snapshot.signature;
406
+ stableSince = Date.now();
407
+ }
408
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
409
+ }
410
+ }
411
+ extractExpectedWorkspaceFiles(message = '', context = {}) {
412
+ const candidates = new Set();
413
+ const addMatches = (value) => {
414
+ const text = String(value || '');
415
+ const patterns = [
416
+ /`([^`]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))`/gi,
417
+ /\b([A-Za-z0-9_./-]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))\b/g,
418
+ ];
419
+ for (const pattern of patterns) {
420
+ let match;
421
+ while ((match = pattern.exec(text)) !== null) {
422
+ const filePath = match[1].trim().replace(/^\.\//, '');
423
+ if (filePath && !filePath.startsWith('http://') && !filePath.startsWith('https://')) {
424
+ candidates.add(filePath);
425
+ }
426
+ }
427
+ }
428
+ };
429
+ addMatches(message);
430
+ addMatches(context.rawMessage);
431
+ addMatches(context.agentPrompt);
432
+ return Array.from(candidates);
433
+ }
434
+ captureV3AgentStreamMutation(event, streamedFiles) {
435
+ if (!event || event.type !== 'tool_call' || !streamedFiles) {
436
+ return;
437
+ }
438
+ const args = event.arguments || {};
439
+ if ((event.name === 'write_file' || event.name === 'edit_file') && typeof args.path === 'string') {
440
+ const filePath = args.path.trim().replace(/\\/g, '/').replace(/^\.\//, '');
441
+ if (!filePath) {
442
+ return;
443
+ }
444
+ if (event.name === 'write_file' && typeof args.content === 'string') {
445
+ streamedFiles[filePath] = args.content;
446
+ return;
447
+ }
448
+ if (event.name === 'edit_file' && typeof args.old_string === 'string' && typeof args.new_string === 'string') {
449
+ const existing = streamedFiles[filePath];
450
+ if (typeof existing === 'string' && existing.includes(args.old_string)) {
451
+ streamedFiles[filePath] = existing.replace(args.old_string, args.new_string);
452
+ }
453
+ }
454
+ }
455
+ }
456
+ recoverAgentWorkspaceFiles(context = {}, streamedFiles = {}, expectedFiles = []) {
457
+ const rootPath = this.resolveAgentTargetPath(context);
458
+ if (!rootPath || !fs_1.default.existsSync(rootPath) || Object.keys(streamedFiles).length === 0) {
459
+ return;
460
+ }
461
+ const targets = expectedFiles.length > 0 ? expectedFiles : Object.keys(streamedFiles);
462
+ for (const relativePath of targets) {
463
+ const content = streamedFiles[relativePath];
464
+ if (typeof content !== 'string') {
465
+ continue;
466
+ }
467
+ const absolutePath = path_1.default.join(rootPath, relativePath);
468
+ if (fs_1.default.existsSync(absolutePath)) {
469
+ continue;
470
+ }
471
+ fs_1.default.mkdirSync(path_1.default.dirname(absolutePath), { recursive: true });
472
+ fs_1.default.writeFileSync(absolutePath, content, 'utf8');
473
+ }
474
+ }
475
+ async ensureAgentFrontendPolish(message = '', context = {}) {
476
+ const rootPath = this.resolveAgentTargetPath(context);
477
+ if (!rootPath || !fs_1.default.existsSync(rootPath)) {
478
+ return;
479
+ }
480
+ const prompt = String(message || '');
481
+ const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
482
+ const looksLikeFrontendTask = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase)/i.test(prompt)
483
+ || expectedFiles.some((filePath) => /\.(html|css|js)$/i.test(filePath));
484
+ if (!looksLikeFrontendTask) {
485
+ return;
486
+ }
487
+ const htmlPath = path_1.default.join(rootPath, 'index.html');
488
+ const cssPath = path_1.default.join(rootPath, 'styles.css');
489
+ if (!fs_1.default.existsSync(htmlPath) || !fs_1.default.existsSync(cssPath)) {
490
+ return;
491
+ }
492
+ const jsCandidates = ['app.js', 'script.js', 'main.js']
493
+ .map((fileName) => path_1.default.join(rootPath, fileName))
494
+ .filter((filePath) => fs_1.default.existsSync(filePath));
495
+ const jsPath = jsCandidates[0] || path_1.default.join(rootPath, 'app.js');
496
+ const html = fs_1.default.readFileSync(htmlPath, 'utf8');
497
+ let css = fs_1.default.readFileSync(cssPath, 'utf8');
498
+ let js = fs_1.default.existsSync(jsPath) ? fs_1.default.readFileSync(jsPath, 'utf8') : '';
499
+ let nextHtml = html;
500
+ const keyframesBlocks = Array.from(js.matchAll(/@keyframes[\s\S]*?\n\}/g)).map((match) => match[0]);
501
+ if (keyframesBlocks.length > 0) {
502
+ const migrated = keyframesBlocks.filter((block) => !css.includes(block));
503
+ if (migrated.length > 0) {
504
+ css = `${css.trimEnd()}\n\n/* Vigthoria CLI Recovered CSS */\n${migrated.join('\n\n')}\n`;
505
+ }
506
+ js = js.replace(/\n?@keyframes[\s\S]*?\n\}/g, '').trim();
507
+ fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
508
+ fs_1.default.writeFileSync(jsPath, js ? `${js.trimEnd()}\n` : '', 'utf8');
509
+ }
510
+ const wantsPricing = /(pay-as-you-go|pricing)/i.test(prompt);
511
+ if (wantsPricing && !/(id="pricing"|Pay-as-you-go|pay-as-you-go|pricing tiers)/i.test(nextHtml)) {
512
+ nextHtml = this.injectSectionBeforeFooter(nextHtml, `
513
+ <section id="pricing" class="module pricing">
514
+ <h2>Pay-as-you-go Pricing</h2>
515
+ <p>Start with the modules you need, scale usage by workload, and keep enterprise-grade visibility across teams.</p>
516
+ <div class="pricing-grid">
517
+ <article>
518
+ <h3>Builder</h3>
519
+ <p>Usage-based coding, workflow, and storage for product teams shipping fast.</p>
520
+ </article>
521
+ <article>
522
+ <h3>Studio</h3>
523
+ <p>Metered voice and music generation with predictable controls for creative operations.</p>
524
+ </article>
525
+ <article>
526
+ <h3>Scale</h3>
527
+ <p>Hosting, finance, and orchestration capacity that expands with customer demand.</p>
528
+ </article>
529
+ </div>
530
+ </section>`);
531
+ nextHtml = this.injectNavLink(nextHtml, 'pricing', 'Pricing');
532
+ }
533
+ const wantsTrust = /(trust|security|secure)/i.test(prompt);
534
+ if (wantsTrust && !/(id="trust"|id="security"|Trust and Security|Security)/i.test(nextHtml)) {
535
+ nextHtml = this.injectSectionBeforeFooter(nextHtml, `
536
+ <section id="trust" class="module trust">
537
+ <h2>Trust and Security</h2>
538
+ <p>Role-aware access, auditable workflows, and isolated infrastructure keep sensitive workloads controlled from prototype to production.</p>
539
+ <ul class="trust-list">
540
+ <li>Authenticated access across CLI, Code Fork, and hosted workspaces.</li>
541
+ <li>Scalable storage and hosting paths aligned with enterprise deployment requirements.</li>
542
+ <li>Operational visibility for pricing, usage, and automation governance.</li>
543
+ </ul>
544
+ </section>`);
545
+ nextHtml = this.injectNavLink(nextHtml, 'trust', 'Trust');
546
+ }
547
+ if (nextHtml !== html) {
548
+ fs_1.default.writeFileSync(htmlPath, `${nextHtml.trimEnd()}\n`, 'utf8');
549
+ nextHtml = fs_1.default.readFileSync(htmlPath, 'utf8');
550
+ }
551
+ if (/classList\.add\('hidden'\)|classList\.add\("hidden"\)|classList\.add\('revealed'\)|classList\.add\("revealed"\)/.test(js)
552
+ && !/\.hidden\b|\.revealed\b/.test(css)) {
553
+ 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`;
554
+ fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
555
+ }
556
+ const combined = `${nextHtml}\n${css}\n${js}`;
557
+ if (/IntersectionObserver|motion-reveal|\.revealed\b|vigCliFadeIn|classList\.add\('is-visible'\)|classList\.add\("is-visible"\)/i.test(combined)) {
558
+ return;
559
+ }
560
+ const cssMarker = '/* Vigthoria CLI Motion Enhancement */';
561
+ const jsMarker = '/* Vigthoria CLI Motion Enhancement */';
562
+ if (!css.includes(cssMarker)) {
563
+ 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');
564
+ }
565
+ if (!js.includes(jsMarker)) {
566
+ 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');
567
+ }
568
+ }
569
+ injectSectionBeforeFooter(html, sectionMarkup) {
570
+ if (/<footer[\s>]/i.test(html)) {
571
+ return html.replace(/<footer/i, `${sectionMarkup}\n <footer`);
572
+ }
573
+ if (/<\/body>/i.test(html)) {
574
+ return html.replace(/\s*<\/body>/i, `${sectionMarkup}\n</body>`);
575
+ }
576
+ return `${html.trimEnd()}\n${sectionMarkup}\n`;
577
+ }
578
+ injectNavLink(html, sectionId, label) {
579
+ if (new RegExp(`href=\"#${sectionId}\"`, 'i').test(html)) {
580
+ return html;
581
+ }
582
+ if (/<\/ul>/i.test(html)) {
583
+ return html.replace(/\s*<\/ul>/i, `\n <li><a href="#${sectionId}">${label}</a></li>\n </ul>`);
584
+ }
585
+ return html;
586
+ }
587
+ formatV3AgentResponse(data) {
588
+ const result = data?.result || {};
589
+ if (typeof result === 'string') {
590
+ return result;
591
+ }
592
+ if (typeof result?.summary === 'string' && result.summary.trim()) {
593
+ return result.summary;
594
+ }
595
+ if (typeof result?.message === 'string' && result.message.trim()) {
596
+ return result.message;
597
+ }
598
+ if (Array.isArray(data?.events)) {
599
+ const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string');
600
+ if (completionEvent) {
601
+ return completionEvent.summary;
602
+ }
603
+ const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string');
604
+ if (messageEvent) {
605
+ return messageEvent.content;
606
+ }
607
+ }
608
+ const text = JSON.stringify(data, null, 2);
609
+ return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
610
+ }
611
+ async collectV3AgentStream(response, context = {}) {
612
+ if (!response.body || typeof response.body.getReader !== 'function') {
613
+ return response.json();
614
+ }
615
+ const reader = response.body.getReader();
616
+ const decoder = new TextDecoder();
617
+ let buffer = '';
618
+ const events = [];
619
+ let final = null;
620
+ const streamedFiles = {};
621
+ const idleTimeoutMs = context.agentIdleTimeoutMs || DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
622
+ while (true) {
623
+ let chunk;
624
+ try {
625
+ const readPromise = reader.read();
626
+ while (true) {
627
+ const timeoutSentinel = Symbol('v3-agent-idle-timeout');
628
+ const result = idleTimeoutMs > 0
629
+ ? await Promise.race([
630
+ readPromise,
631
+ new Promise((resolve) => {
632
+ setTimeout(() => resolve(timeoutSentinel), idleTimeoutMs);
633
+ }),
634
+ ])
635
+ : await readPromise;
636
+ if (result !== timeoutSentinel) {
637
+ chunk = result;
638
+ break;
639
+ }
640
+ if (this.hasAgentWorkspaceOutput(context)) {
641
+ const stalledError = new Error('V3 agent stream stalled after writing workspace output');
642
+ stalledError.name = 'AbortError';
643
+ stalledError.partialData = {
644
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
645
+ result: final,
646
+ events,
647
+ files: streamedFiles,
648
+ };
649
+ throw stalledError;
650
+ }
651
+ }
652
+ }
653
+ catch (error) {
654
+ if (error && error.name === 'AbortError') {
655
+ error.partialData = {
656
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
657
+ result: final,
658
+ events,
659
+ files: streamedFiles,
660
+ };
661
+ }
662
+ throw error;
663
+ }
664
+ const { done, value } = chunk;
665
+ if (done) {
666
+ break;
667
+ }
668
+ buffer += decoder.decode(value, { stream: true });
669
+ const lines = buffer.split('\n');
670
+ buffer = lines.pop() || '';
671
+ for (const line of lines) {
672
+ if (!line.startsWith('data: ')) {
673
+ continue;
674
+ }
675
+ const payload = line.slice(6).trim();
676
+ if (!payload || payload === '[DONE]') {
677
+ continue;
678
+ }
679
+ const event = JSON.parse(payload);
680
+ events.push(event);
681
+ this.captureV3AgentStreamMutation(event, streamedFiles);
682
+ if (event.type === 'error') {
683
+ if (this.hasAgentWorkspaceOutput(context)) {
684
+ return {
685
+ task_id: events.find((entry) => entry && entry.task_id)?.task_id || null,
686
+ result: final || event,
687
+ events,
688
+ files: streamedFiles,
689
+ partial: true,
690
+ };
691
+ }
692
+ throw new Error(event.message || 'V3 agent returned an error');
693
+ }
694
+ if (event.type === 'complete' || event.type === 'message') {
695
+ final = event;
696
+ }
697
+ }
698
+ }
699
+ return {
700
+ task_id: events.find((event) => event && event.task_id)?.task_id || null,
701
+ result: final,
702
+ events,
703
+ files: streamedFiles,
704
+ };
705
+ }
706
+ async runV3AgentWorkflow(message, context = {}) {
707
+ const headers = await this.getV3AgentHeaders();
708
+ const timeoutMs = context.agentTimeoutMs || DEFAULT_V3_AGENT_TIMEOUT_MS;
709
+ const errors = [];
710
+ const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
711
+ for (const baseUrl of this.getV3AgentBaseUrls()) {
712
+ const controller = new AbortController();
713
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
714
+ try {
715
+ const response = await fetch(this.getV3AgentRunUrl(baseUrl), {
716
+ method: 'POST',
717
+ headers,
718
+ body: JSON.stringify({
719
+ request: message,
720
+ context: this.buildV3AgentContext(context),
721
+ stream: true,
722
+ }),
723
+ signal: controller.signal,
724
+ });
725
+ if (!response.ok) {
726
+ const errorText = await response.text().catch(() => '');
727
+ throw new Error(`V3 agent ${response.status}: ${errorText.slice(0, 200)}`);
728
+ }
729
+ const data = await this.collectV3AgentStream(response, context);
730
+ this.recoverAgentWorkspaceFiles(context, data.files || {}, expectedFiles);
731
+ await this.waitForAgentWorkspaceSettle(context, { expectedFiles });
732
+ await this.ensureAgentFrontendPolish(message, context);
733
+ return {
734
+ content: this.formatV3AgentResponse(data),
735
+ taskId: data.task_id || null,
736
+ backendUrl: baseUrl,
737
+ metadata: { source: 'v3-agent', mode: 'agent' },
738
+ };
739
+ }
740
+ catch (error) {
741
+ if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(context)) {
742
+ this.recoverAgentWorkspaceFiles(context, error.partialData.files || {}, expectedFiles);
743
+ await this.waitForAgentWorkspaceSettle(context, { expectedFiles });
744
+ await this.ensureAgentFrontendPolish(message, context);
745
+ return {
746
+ content: this.formatV3AgentResponse(error.partialData) || 'V3 agent wrote workspace files before the request timed out waiting for a final summary.',
747
+ taskId: error.partialData.task_id || null,
748
+ backendUrl: baseUrl,
749
+ partial: true,
750
+ metadata: { source: 'v3-agent', mode: 'agent', partial: true },
751
+ };
752
+ }
753
+ errors.push(`${baseUrl}: ${error?.message || String(error)}`);
754
+ }
755
+ finally {
756
+ clearTimeout(timeoutId);
757
+ }
758
+ }
759
+ throw new Error(errors.join(' | '));
760
+ }
189
761
  /**
190
762
  * Chat API - Direct Vigthoria Models API Architecture
191
763
  *
@@ -199,7 +771,9 @@ class APIClient {
199
771
  */
200
772
  async chat(messages, model, useLocal = false) {
201
773
  const resolvedModel = this.resolveModelId(model);
202
- const candidateModels = [resolvedModel];
774
+ const candidateModels = this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()
775
+ ? [this.getSelfHostedFallbackModelId(resolvedModel, model)]
776
+ : [resolvedModel];
203
777
  const fallbackModel = this.getFallbackModelId(resolvedModel);
204
778
  if (fallbackModel && fallbackModel !== resolvedModel) {
205
779
  candidateModels.push(fallbackModel);
@@ -302,7 +876,7 @@ class APIClient {
302
876
  return null;
303
877
  }
304
878
  async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel) {
305
- if (!this.shouldTrySelfHostedFallback(resolvedModel, requestedModel)) {
879
+ if (!this.selfHostedModelRouterClient || !this.shouldTrySelfHostedFallback(resolvedModel, requestedModel)) {
306
880
  return null;
307
881
  }
308
882
  const selfHostedModel = this.getSelfHostedFallbackModelId(resolvedModel, requestedModel);
@@ -350,10 +924,21 @@ class APIClient {
350
924
  }
351
925
  return null;
352
926
  }
927
+ isCloudModelId(resolvedModel) {
928
+ return resolvedModel === 'deepseek-v3.1:671b-cloud'
929
+ || resolvedModel === 'moonshotai/kimi-k2.5';
930
+ }
931
+ canUseCloudModel() {
932
+ const plan = (this.config.get('subscription').plan || '').toLowerCase();
933
+ return ['pro', 'professional', 'enterprise', 'admin', 'master_admin', 'ultra'].includes(plan);
934
+ }
353
935
  shouldSimulateCloudFailure() {
354
936
  return process.env.VIGTHORIA_SIMULATE_CLOUD_FAILURE === '1';
355
937
  }
356
938
  shouldTrySelfHostedFallback(resolvedModel, requestedModel) {
939
+ if (!this.selfHostedModelRouterClient) {
940
+ return false;
941
+ }
357
942
  const normalizedRequested = String(requestedModel || '').toLowerCase();
358
943
  return this.isSelfHostedPreferredModel(resolvedModel, requestedModel)
359
944
  || normalizedRequested === 'cloud'
@@ -535,15 +1120,99 @@ class APIClient {
535
1120
  }
536
1121
  return modelMap[shortName] || 'qwen3-coder:latest'; // Default to 30B
537
1122
  }
538
- // Health check
539
- async healthCheck() {
1123
+ async getCoderHealth() {
540
1124
  try {
541
1125
  const response = await this.client.get('/api/health', { timeout: 10000 });
542
- return response.data?.status === 'ok' || response.data?.healthy === true;
1126
+ const ok = response.data?.status === 'ok' || response.data?.healthy === true;
1127
+ return {
1128
+ name: 'Coder API',
1129
+ endpoint: `${this.config.get('apiUrl')}/api/health`,
1130
+ ok,
1131
+ details: { health: response.data },
1132
+ };
543
1133
  }
544
- catch {
545
- return false;
1134
+ catch (error) {
1135
+ return {
1136
+ name: 'Coder API',
1137
+ endpoint: `${this.config.get('apiUrl')}/api/health`,
1138
+ ok: false,
1139
+ error: error.message,
1140
+ };
1141
+ }
1142
+ }
1143
+ async getModelsHealth() {
1144
+ const modelsApiUrl = this.config.get('modelsApiUrl');
1145
+ try {
1146
+ const [healthResponse, modelsResponse] = await Promise.all([
1147
+ this.modelRouterClient.get('/health', { timeout: 10000 }),
1148
+ this.modelRouterClient.get('/v1/models', { timeout: 15000 }),
1149
+ ]);
1150
+ const healthOk = healthResponse.data?.status === 'healthy'
1151
+ || healthResponse.data?.status === 'ok'
1152
+ || healthResponse.data?.healthy === true;
1153
+ const modelCount = Array.isArray(modelsResponse.data?.data) ? modelsResponse.data.data.length : 0;
1154
+ return {
1155
+ name: 'Models API',
1156
+ endpoint: `${modelsApiUrl}/health`,
1157
+ ok: healthOk && modelCount > 0,
1158
+ details: {
1159
+ health: healthResponse.data,
1160
+ modelCount,
1161
+ },
1162
+ };
546
1163
  }
1164
+ catch (error) {
1165
+ return {
1166
+ name: 'Models API',
1167
+ endpoint: `${modelsApiUrl}/health`,
1168
+ ok: false,
1169
+ error: error.message,
1170
+ };
1171
+ }
1172
+ }
1173
+ async getSelfHostedHealth() {
1174
+ const selfHostedModelsApiUrl = this.getSelfHostedModelsApiUrl();
1175
+ if (!selfHostedModelsApiUrl || !this.selfHostedModelRouterClient) {
1176
+ return null;
1177
+ }
1178
+ try {
1179
+ const response = await this.selfHostedModelRouterClient.get('/health', { timeout: 10000 });
1180
+ const ok = response.data?.status === 'healthy'
1181
+ || response.data?.status === 'ok'
1182
+ || response.data?.healthy === true;
1183
+ return {
1184
+ name: 'Self-hosted Models API',
1185
+ endpoint: `${selfHostedModelsApiUrl}/health`,
1186
+ ok,
1187
+ details: { health: response.data },
1188
+ };
1189
+ }
1190
+ catch (error) {
1191
+ return {
1192
+ name: 'Self-hosted Models API',
1193
+ endpoint: `${selfHostedModelsApiUrl}/health`,
1194
+ ok: false,
1195
+ error: error.message,
1196
+ };
1197
+ }
1198
+ }
1199
+ async getHealthStatus() {
1200
+ const [coder, models, selfHosted] = await Promise.all([
1201
+ this.getCoderHealth(),
1202
+ this.getModelsHealth(),
1203
+ this.getSelfHostedHealth(),
1204
+ ]);
1205
+ return {
1206
+ overallOk: coder.ok && models.ok,
1207
+ coder,
1208
+ models,
1209
+ selfHosted,
1210
+ };
1211
+ }
1212
+ // Health check
1213
+ async healthCheck() {
1214
+ const status = await this.getHealthStatus();
1215
+ return status.overallOk;
547
1216
  }
548
1217
  }
549
1218
  exports.APIClient = APIClient;