vigthoria-cli 1.9.9 → 1.9.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +5 -5
  2. package/dist/commands/auth.js +48 -65
  3. package/dist/commands/bridge.js +12 -19
  4. package/dist/commands/cancel.js +15 -22
  5. package/dist/commands/chat.d.ts +11 -0
  6. package/dist/commands/chat.js +404 -248
  7. package/dist/commands/config.js +31 -71
  8. package/dist/commands/deploy.js +83 -123
  9. package/dist/commands/device.d.ts +35 -0
  10. package/dist/commands/device.js +239 -0
  11. package/dist/commands/edit.js +32 -39
  12. package/dist/commands/explain.js +18 -25
  13. package/dist/commands/fork.js +22 -27
  14. package/dist/commands/generate.js +37 -44
  15. package/dist/commands/history.js +20 -25
  16. package/dist/commands/hub.js +95 -102
  17. package/dist/commands/index.js +41 -46
  18. package/dist/commands/legion.d.ts +1 -0
  19. package/dist/commands/legion.js +162 -209
  20. package/dist/commands/preview.js +60 -98
  21. package/dist/commands/replay.js +27 -32
  22. package/dist/commands/repo.js +103 -141
  23. package/dist/commands/review.js +29 -36
  24. package/dist/commands/security.js +5 -12
  25. package/dist/commands/update.js +15 -49
  26. package/dist/commands/workflow.d.ts +8 -1
  27. package/dist/commands/workflow.js +53 -19
  28. package/dist/index.js +409 -234
  29. package/dist/utils/api.d.ts +5 -0
  30. package/dist/utils/api.js +398 -176
  31. package/dist/utils/bridge-client.js +11 -52
  32. package/dist/utils/cli-state.d.ts +54 -0
  33. package/dist/utils/cli-state.js +185 -0
  34. package/dist/utils/config.d.ts +5 -0
  35. package/dist/utils/config.js +35 -14
  36. package/dist/utils/context-ranker.js +15 -21
  37. package/dist/utils/files.js +5 -42
  38. package/dist/utils/logger.js +42 -50
  39. package/dist/utils/post-write-validator.js +22 -29
  40. package/dist/utils/project-memory.d.ts +56 -0
  41. package/dist/utils/project-memory.js +289 -0
  42. package/dist/utils/session.d.ts +29 -3
  43. package/dist/utils/session.js +137 -85
  44. package/dist/utils/task-display.js +13 -20
  45. package/dist/utils/tools.d.ts +19 -0
  46. package/dist/utils/tools.js +84 -87
  47. package/dist/utils/workspace-cache.js +18 -26
  48. package/dist/utils/workspace-stream.js +26 -64
  49. package/install.ps1 +14 -0
  50. package/package.json +5 -3
  51. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  52. package/scripts/release/validate-no-go-gates.sh +2 -2
package/dist/utils/api.js CHANGED
@@ -1,27 +1,15 @@
1
- "use strict";
2
1
  /**
3
2
  * API Client for Vigthoria Backend
4
3
  * Connects to coder.vigthoria.io API endpoints
5
4
  */
6
- var __importDefault = (this && this.__importDefault) || function (mod) {
7
- return (mod && mod.__esModule) ? mod : { "default": mod };
8
- };
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.APIClient = exports.CLIError = void 0;
11
- exports.classifyError = classifyError;
12
- exports.formatCLIError = formatCLIError;
13
- exports.sanitizeUserFacingErrorText = sanitizeUserFacingErrorText;
14
- exports.isServerRuntime = isServerRuntime;
15
- exports.describeUpstreamStatus = describeUpstreamStatus;
16
- exports.propagateError = propagateError;
17
- const axios_1 = __importDefault(require("axios"));
18
- const crypto_1 = require("crypto");
19
- const fs_1 = __importDefault(require("fs"));
20
- const https_1 = __importDefault(require("https"));
21
- const net_1 = __importDefault(require("net"));
22
- const path_1 = __importDefault(require("path"));
23
- const ws_1 = __importDefault(require("ws"));
24
- class CLIError extends Error {
5
+ import axios from 'axios';
6
+ import { randomUUID } from 'crypto';
7
+ import fs from 'fs';
8
+ import https from 'https';
9
+ import net from 'net';
10
+ import path from 'path';
11
+ import WebSocket from 'ws';
12
+ export class CLIError extends Error {
25
13
  category;
26
14
  statusCode;
27
15
  endpoint;
@@ -35,9 +23,8 @@ class CLIError extends Error {
35
23
  this.cause = opts.cause;
36
24
  }
37
25
  }
38
- exports.CLIError = CLIError;
39
26
  /** Classify an axios or fetch error into a structured CLIError. */
40
- function classifyError(error, fallbackCategory = 'network') {
27
+ export function classifyError(error, fallbackCategory = 'network') {
41
28
  if (error instanceof CLIError)
42
29
  return error;
43
30
  const axErr = error;
@@ -69,7 +56,7 @@ function classifyError(error, fallbackCategory = 'network') {
69
56
  return new CLIError(message, fallbackCategory, { statusCode: status, endpoint, cause: error instanceof Error ? error : undefined });
70
57
  }
71
58
  /** Format a CLIError for user-facing display. */
72
- function formatCLIError(err) {
59
+ export function formatCLIError(err) {
73
60
  const tag = `[${err.category}]`;
74
61
  switch (err.category) {
75
62
  case 'auth':
@@ -92,13 +79,76 @@ function formatCLIError(err) {
92
79
  return `${tag} ${err.message}`;
93
80
  }
94
81
  }
95
- function sanitizeUserFacingErrorText(input) {
82
+ export function sanitizeUserFacingPathText(input) {
83
+ let text = String(input || '');
84
+ if (!text) {
85
+ return text;
86
+ }
87
+ // Path token used for "<service>/<sub>" captures. Excludes whitespace and
88
+ // any character that commonly terminates a path in CLI output.
89
+ const pathToken = String.raw `[^\s"'\)\]\}<>]+`;
90
+ // The replacements are applied in order — more specific patterns first.
91
+ // Every entry must cover the case WITH AND WITHOUT a trailing slash so
92
+ // bracketed paths like `[/tmp/vig-remote-xyz]` are also caught.
93
+ const replacements = [
94
+ // ── Temp-workspace directories used for remote (Windows/macOS) clients
95
+ [new RegExp(String.raw `/var/www/\.vigthoria/v3-temp/vig-remote-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
96
+ [new RegExp(String.raw `\.vigthoria/v3-temp/vig-remote-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
97
+ [new RegExp(String.raw `/tmp/vig-remote(?:-server)?-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
98
+ [new RegExp(String.raw `/var/tmp/vig-remote(?:-server)?-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
99
+ [new RegExp(String.raw `/tmp/vig-fork-[A-Za-z0-9._-]+(?:/${pathToken})?`, 'gi'), 'workspace'],
100
+ // ── Bare temp basenames after their /tmp/ prefix is already gone
101
+ [/\bvig-remote(?:-server)?-[A-Za-z0-9._-]+/gi, 'workspace'],
102
+ [/\bvig-fork-[A-Za-z0-9._-]+/gi, 'workspace'],
103
+ // ── V3 Code Agent installation tree and sibling services
104
+ [new RegExp(String.raw `/var/www/V3-Code-Agent(?:/${pathToken})?`, 'gi'), '[Vigthoria Agent]'],
105
+ [new RegExp(String.raw `/var/www/(?:agent-vigthoria|vigthoria-model-router|vigthoria-devtools-bridge|vigthoria-hyper-loop|vigthoria-mcp-server|vigthoria-template-service|vigthoria-security-devops|vigthoria-coder|vigthoria-code|Vigthoria-Code-2|vigthoria-asset-storage|vigthoria-storage-service|vigthoria-hosted)(?:/${pathToken})?`, 'gi'), '[Vigthoria service]'],
106
+ // ── User workspace mount + long-term storage backing path
107
+ [new RegExp(String.raw `/var/www/(?:vigthoria-user-workspaces|\.vigthoria)(?:/${pathToken})?`, 'gi'), 'workspace'],
108
+ [new RegExp(String.raw `/var/lib/vigthoria-workspaces(?:/${pathToken})?`, 'gi'), 'workspace'],
109
+ [new RegExp(String.raw `/mnt/storage-box/vigthoria(?:/${pathToken})?`, 'gi'), '[Vigthoria storage]'],
110
+ // ── Home / root leaks
111
+ [/\/home\/[A-Za-z0-9._-]+/gi, '[home]'],
112
+ [new RegExp(String.raw `/root(?:/${pathToken})?`, 'gi'), '[home]'],
113
+ // ── Backstop: any other /var/www tree
114
+ [new RegExp(String.raw `/var/www(?:/${pathToken})?`, 'gi'), '[Vigthoria service]'],
115
+ // ── Generic temp roots (after the more specific vig- patterns above)
116
+ [new RegExp(String.raw `/tmp(?:/${pathToken})?`, 'gi'), '[temp]'],
117
+ [new RegExp(String.raw `/var/tmp(?:/${pathToken})?`, 'gi'), '[temp]'],
118
+ ];
119
+ text = text.replace(/\\/g, '/');
120
+ for (const [pattern, replacement] of replacements) {
121
+ text = text.replace(pattern, replacement);
122
+ }
123
+ // Normalise abstract boundary URIs for human-friendly display.
124
+ text = text
125
+ .replace(/vigthoria:\/\/workspace\/?/gi, 'workspace/')
126
+ .replace(/vigthoria:\/\/temp\/?/gi, '[temp]/')
127
+ .replace(/vigthoria:\/\/(?:agent|service|storage|home|server-internal)\/?[^\s"'\)\]\}<>]*/gi, '[internal]');
128
+ // Strip server-internal phrasing so the LLM/CLI never says "server filesystem".
129
+ text = text
130
+ .replace(/outside bound workspace root(?:\s+[^\s.,;:)\]]+)?/gi, 'outside the workspace')
131
+ .replace(/\bbound workspace root(?:\s+[^\s.,;:)\]]+)?/gi, 'workspace')
132
+ .replace(/\boutside the active workspace sandbox(?::\s*[^\s.,;:)\]]+)?/gi, 'outside the workspace')
133
+ .replace(/\bescapes workspace root(?:\s+[^\s.,;:)\]]+)?/gi, 'escapes the workspace')
134
+ .replace(/\boutside allowed roots[^.]*\./gi, 'outside the workspace.')
135
+ .replace(/\bAllowed (?:server )?roots:[^\n]*/gi, 'Allowed locations: workspace only')
136
+ .replace(/\bBound server workspace:\s*\S+/gi, 'Bound workspace: workspace/')
137
+ .replace(/\bserver filesystem\b/gi, 'workspace sandbox')
138
+ .replace(/\bon the server\b/gi, 'in the workspace')
139
+ .replace(/\[(?:\s*)\]/g, '[workspace]')
140
+ .replace(/(^|\s)\.\//g, '$1')
141
+ .replace(/([^:])\/\/+/g, '$1/')
142
+ .replace(/[ \t]+/g, ' ');
143
+ return text;
144
+ }
145
+ export function sanitizeUserFacingErrorText(input) {
96
146
  const raw = String(input || '').trim();
97
147
  if (!raw) {
98
148
  return '';
99
149
  }
100
150
  const withoutTags = raw.replace(/<[^>]+>/g, ' ');
101
- return withoutTags.replace(/\s+/g, ' ').trim();
151
+ return sanitizeUserFacingPathText(withoutTags).replace(/\s+/g, ' ').trim();
102
152
  }
103
153
  const TRUSTED_TOKEN_HOST_PATTERN = /(^|\.)vigthoria\.io$/i;
104
154
  function isLoopbackHost(hostname) {
@@ -132,7 +182,7 @@ function resolveAxiosRequestUrl(req) {
132
182
  }
133
183
  return direct;
134
184
  }
135
- function isServerRuntime() {
185
+ export function isServerRuntime() {
136
186
  if (process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1') {
137
187
  return true;
138
188
  }
@@ -140,7 +190,7 @@ function isServerRuntime() {
140
190
  const cwd = String(process.cwd() || '').toLowerCase();
141
191
  return host.includes('ubuntu') || cwd.startsWith('/var/www');
142
192
  }
143
- function describeUpstreamStatus(status) {
193
+ export function describeUpstreamStatus(status) {
144
194
  if (status >= 500)
145
195
  return 'upstream internal error';
146
196
  if (status === 429)
@@ -155,7 +205,7 @@ function describeUpstreamStatus(status) {
155
205
  return 'bad request';
156
206
  return 'ok';
157
207
  }
158
- function propagateError(err) {
208
+ export function propagateError(err) {
159
209
  const status = typeof err?.statusCode === 'number'
160
210
  ? err.statusCode
161
211
  : typeof err?.status === 'number'
@@ -198,7 +248,7 @@ const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
198
248
  const parsed = Number.parseInt(rawValue, 10);
199
249
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
200
250
  })();
201
- class APIClient {
251
+ export class APIClient {
202
252
  client;
203
253
  modelRouterClient;
204
254
  selfHostedModelRouterClient;
@@ -212,14 +262,14 @@ class APIClient {
212
262
  this.config = config;
213
263
  this.logger = logger;
214
264
  // Create HTTPS agent with proper settings for cross-platform compatibility
215
- const httpsAgent = new https_1.default.Agent({
265
+ const httpsAgent = new https.Agent({
216
266
  rejectUnauthorized: true, // Enforce SSL verification
217
267
  keepAlive: true,
218
268
  timeout: 30000,
219
269
  });
220
270
  this._httpsAgent = httpsAgent;
221
271
  // Main Vigthoria Coder API (coder.vigthoria.io)
222
- this.client = axios_1.default.create({
272
+ this.client = axios.create({
223
273
  baseURL: config.get('apiUrl'),
224
274
  timeout: 300000, // 5 minutes — covers review, fix, and agent relay flows
225
275
  httpsAgent,
@@ -230,7 +280,7 @@ class APIClient {
230
280
  });
231
281
  // Direct AI Models API - bypasses Coder for direct model access
232
282
  // This is the centralized Vigthoria API that routes to Model Router
233
- this.modelRouterClient = axios_1.default.create({
283
+ this.modelRouterClient = axios.create({
234
284
  baseURL: config.get('modelsApiUrl') || 'https://api.vigthoria.io',
235
285
  timeout: 180000, // 3 minutes for model inference
236
286
  httpsAgent,
@@ -241,7 +291,7 @@ class APIClient {
241
291
  });
242
292
  // Self-hosted model router is opt-in only.
243
293
  const selfHostedModelsApiUrl = this.getSelfHostedModelsApiUrl();
244
- this.selfHostedModelRouterClient = selfHostedModelsApiUrl ? axios_1.default.create({
294
+ this.selfHostedModelRouterClient = selfHostedModelsApiUrl ? axios.create({
245
295
  baseURL: selfHostedModelsApiUrl,
246
296
  timeout: 240000,
247
297
  headers: {
@@ -338,7 +388,7 @@ class APIClient {
338
388
  });
339
389
  // Set subscription from user data
340
390
  this.config.setSubscription({
341
- plan: user.subscription?.plan || 'developer',
391
+ plan: user.subscription?.plan || 'community',
342
392
  status: 'active',
343
393
  expiresAt: undefined,
344
394
  });
@@ -413,7 +463,7 @@ class APIClient {
413
463
  || rawUser.subscription?.plan
414
464
  || rawUser.subscription_plan
415
465
  || data.subscription_plan
416
- || 'developer';
466
+ || 'community';
417
467
  return {
418
468
  id: userId,
419
469
  email,
@@ -445,7 +495,7 @@ class APIClient {
445
495
  const response = await this.client.get('/api/user/subscription');
446
496
  const data = response.data;
447
497
  this.config.setSubscription({
448
- plan: data.plan || data.subscription_plan || 'developer',
498
+ plan: data.plan || data.subscription_plan || 'community',
449
499
  status: data.status || 'active',
450
500
  expiresAt: data.expiresAt || data.expires_at
451
501
  });
@@ -467,19 +517,20 @@ class APIClient {
467
517
  async validateToken(options = {}) {
468
518
  const allowNetworkFailOpen = options.allowNetworkFailOpen !== false;
469
519
  const enforceTokenShape = options.enforceTokenShape !== false;
520
+ const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
470
521
  const token = this.getAccessToken();
471
522
  if (!token) {
472
523
  return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
473
524
  }
474
- // Fast-fail obviously malformed tokens so invalid-token checks don't get
475
- // masked by unrelated transport outages.
476
- if (enforceTokenShape) {
525
+ // Fast-fail obviously malformed ENV override tokens so invalid-token checks
526
+ // don't get masked by unrelated transport outages. Persisted login tokens may
527
+ // be non-JWT in some deployments and must still be gateway-validated server-side.
528
+ if (enforceTokenShape && explicitEnvToken) {
477
529
  const looksLikeJwt = token.split('.').length === 3;
478
530
  if (!looksLikeJwt || token.length < 40) {
479
531
  return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
480
532
  }
481
533
  }
482
- const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
483
534
  const headers = {
484
535
  Authorization: `Bearer ${token}`,
485
536
  Cookie: `vigthoria-auth-token=${token}`,
@@ -488,23 +539,37 @@ class APIClient {
488
539
  // Probe protected canonical endpoints in parallel so stale local endpoint overrides
489
540
  // cannot mask an invalid gateway token during preflight.
490
541
  const results = await Promise.allSettled([
491
- axios_1.default.get(`${canonicalBaseUrl}/api/user/profile`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
492
- axios_1.default.get(`${canonicalBaseUrl}/api/user/subscription`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
542
+ axios.get(`${canonicalBaseUrl}/api/user/profile`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
543
+ axios.get(`${canonicalBaseUrl}/api/user/subscription`, { timeout: 5000, headers, httpsAgent: this._httpsAgent ?? undefined }),
493
544
  ]);
494
545
  for (const r of results) {
495
546
  if (r.status === 'fulfilled')
496
547
  return { valid: true };
497
548
  }
498
- for (const r of results) {
499
- if (r.status === 'rejected') {
500
- const err = r.reason;
501
- if (err.response?.status === 401 || err.response?.status === 403) {
502
- return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
503
- }
504
- if (err instanceof CLIError && err.category === 'auth') {
505
- return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
549
+ const sawUnauthorized = results.some((r) => r.status === 'rejected' && ((r.reason?.response?.status === 401) || (r.reason?.response?.status === 403) || (r.reason instanceof CLIError && r.reason.category === 'auth')));
550
+ if (sawUnauthorized) {
551
+ // For persisted CLI sessions, attempt one refresh before failing auth.
552
+ if (!explicitEnvToken) {
553
+ const refreshed = await this.refreshToken();
554
+ if (refreshed) {
555
+ const retryToken = this.getAccessToken();
556
+ if (retryToken) {
557
+ const retryHeaders = {
558
+ Authorization: `Bearer ${retryToken}`,
559
+ Cookie: `vigthoria-auth-token=${retryToken}`,
560
+ };
561
+ const retryResults = await Promise.allSettled([
562
+ axios.get(`${canonicalBaseUrl}/api/user/profile`, { timeout: 5000, headers: retryHeaders, httpsAgent: this._httpsAgent ?? undefined }),
563
+ axios.get(`${canonicalBaseUrl}/api/user/subscription`, { timeout: 5000, headers: retryHeaders, httpsAgent: this._httpsAgent ?? undefined }),
564
+ ]);
565
+ for (const rr of retryResults) {
566
+ if (rr.status === 'fulfilled')
567
+ return { valid: true };
568
+ }
569
+ }
506
570
  }
507
571
  }
572
+ return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
508
573
  }
509
574
  if (explicitEnvToken || !allowNetworkFailOpen) {
510
575
  return { valid: false, error: 'Auth token expired or invalid. Run: vigthoria login' };
@@ -624,11 +689,11 @@ class APIClient {
624
689
  if (!current) {
625
690
  continue;
626
691
  }
627
- for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
692
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
628
693
  if (entry.name === '.git' || entry.name === 'node_modules') {
629
694
  continue;
630
695
  }
631
- const fullPath = path_1.default.join(current, entry.name);
696
+ const fullPath = path.join(current, entry.name);
632
697
  if (entry.isDirectory()) {
633
698
  stack.push(fullPath);
634
699
  continue;
@@ -636,7 +701,7 @@ class APIClient {
636
701
  if (!entry.isFile()) {
637
702
  continue;
638
703
  }
639
- const relativePath = this.normalizeWorkspaceRelativePath(path_1.default.relative(rootPath, fullPath));
704
+ const relativePath = this.normalizeWorkspaceRelativePath(path.relative(rootPath, fullPath));
640
705
  if (relativePath) {
641
706
  results.push(relativePath);
642
707
  }
@@ -655,7 +720,7 @@ class APIClient {
655
720
  }
656
721
  }
657
722
  const scored = htmlPaths.map((relativePath) => {
658
- const baseName = path_1.default.posix.basename(relativePath).toLowerCase();
723
+ const baseName = path.posix.basename(relativePath).toLowerCase();
659
724
  const depth = relativePath.split('/').length - 1;
660
725
  let score = 0;
661
726
  if (relativePath.toLowerCase() === 'index.html') {
@@ -677,7 +742,7 @@ class APIClient {
677
742
  extractLinkedFrontendAssets(html, entryPath) {
678
743
  const css = new Set();
679
744
  const js = new Set();
680
- const entryDir = path_1.default.posix.dirname(this.normalizeWorkspaceRelativePath(entryPath) || '.');
745
+ const entryDir = path.posix.dirname(this.normalizeWorkspaceRelativePath(entryPath) || '.');
681
746
  const normalizeAsset = (assetPath) => {
682
747
  const clean = String(assetPath || '').trim();
683
748
  if (!clean || clean.startsWith('http://') || clean.startsWith('https://') || clean.startsWith('//') || clean.startsWith('data:') || clean.startsWith('#')) {
@@ -690,7 +755,7 @@ class APIClient {
690
755
  if (withoutQuery.startsWith('/')) {
691
756
  return this.normalizeWorkspaceRelativePath(withoutQuery.slice(1));
692
757
  }
693
- return this.normalizeWorkspaceRelativePath(path_1.default.posix.normalize(path_1.default.posix.join(entryDir, withoutQuery)));
758
+ return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery)));
694
759
  };
695
760
  const cssPattern = /<link[^>]+href=["']([^"']+\.css(?:[?#][^"']*)?)["'][^>]*>/gi;
696
761
  const jsPattern = /<script[^>]+src=["']([^"']+\.(?:js|mjs|cjs)(?:[?#][^"']*)?)["'][^>]*>/gi;
@@ -714,7 +779,7 @@ class APIClient {
714
779
  }
715
780
  gatherFrontendPreviewArtifacts(message = '', context = {}) {
716
781
  const rootPath = this.resolveAgentTargetPath(context);
717
- if (!rootPath || !fs_1.default.existsSync(rootPath)) {
782
+ if (!rootPath || !fs.existsSync(rootPath)) {
718
783
  return { error: 'Frontend preview gate requires a readable project workspace.' };
719
784
  }
720
785
  const workspaceFiles = this.listFrontendWorkspaceFiles(rootPath);
@@ -727,7 +792,7 @@ class APIClient {
727
792
  if (!htmlPath) {
728
793
  return { error: 'Frontend preview gate could not determine a primary HTML entry file.' };
729
794
  }
730
- const html = fs_1.default.readFileSync(path_1.default.join(rootPath, htmlPath), 'utf8');
795
+ const html = fs.readFileSync(path.join(rootPath, htmlPath), 'utf8');
731
796
  const linkedAssets = this.extractLinkedFrontendAssets(html, htmlPath);
732
797
  const cssFiles = workspaceFiles.filter((filePath) => /\.css$/i.test(filePath));
733
798
  const jsFiles = workspaceFiles.filter((filePath) => /\.(?:js|mjs|cjs)$/i.test(filePath));
@@ -744,7 +809,7 @@ class APIClient {
744
809
  if (!normalized || selected.has(normalized)) {
745
810
  continue;
746
811
  }
747
- if (!fs_1.default.existsSync(path_1.default.join(rootPath, normalized))) {
812
+ if (!fs.existsSync(path.join(rootPath, normalized))) {
748
813
  continue;
749
814
  }
750
815
  selected.add(normalized);
@@ -753,8 +818,8 @@ class APIClient {
753
818
  };
754
819
  const selectedCssFiles = chooseExisting(linkedAssets.css, [...expectedCss, ...cssFiles]).slice(0, 12);
755
820
  const selectedJsFiles = chooseExisting(linkedAssets.js, [...expectedJs, ...jsFiles]).slice(0, 12);
756
- const css = selectedCssFiles.map((filePath) => fs_1.default.readFileSync(path_1.default.join(rootPath, filePath), 'utf8')).join('\n\n');
757
- const js = selectedJsFiles.map((filePath) => fs_1.default.readFileSync(path_1.default.join(rootPath, filePath), 'utf8')).join('\n\n');
821
+ const css = selectedCssFiles.map((filePath) => fs.readFileSync(path.join(rootPath, filePath), 'utf8')).join('\n\n');
822
+ const js = selectedJsFiles.map((filePath) => fs.readFileSync(path.join(rootPath, filePath), 'utf8')).join('\n\n');
758
823
  return { htmlPath, html, css, js, cssPaths: selectedCssFiles, jsPaths: selectedJsFiles };
759
824
  }
760
825
  async captureFrontendPreviewScreenshot(entryAbsolutePath, screenshotPath) {
@@ -807,14 +872,14 @@ class APIClient {
807
872
  return previewGate;
808
873
  }
809
874
  try {
810
- const artifactsDir = path_1.default.join(rootPath, '.vigthoria', 'proof', 'preview');
811
- fs_1.default.mkdirSync(artifactsDir, { recursive: true });
875
+ const artifactsDir = path.join(rootPath, '.vigthoria', 'proof', 'preview');
876
+ fs.mkdirSync(artifactsDir, { recursive: true });
812
877
  const contextId = String(context.contextId || 'preview').trim() || 'preview';
813
878
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
814
879
  const baseName = `${timestamp}-${contextId}`;
815
- const manifestPath = path_1.default.join(artifactsDir, `${baseName}.json`);
816
- const screenshotPath = path_1.default.join(artifactsDir, `${baseName}.png`);
817
- const entryAbsolutePath = path_1.default.join(rootPath, previewGate.entryPath);
880
+ const manifestPath = path.join(artifactsDir, `${baseName}.json`);
881
+ const screenshotPath = path.join(artifactsDir, `${baseName}.png`);
882
+ const entryAbsolutePath = path.join(rootPath, previewGate.entryPath);
818
883
  const previewFileUrl = `file://${entryAbsolutePath}`;
819
884
  const screenshot = await this.captureFrontendPreviewScreenshot(entryAbsolutePath, screenshotPath);
820
885
  const manifest = {
@@ -839,7 +904,7 @@ class APIClient {
839
904
  error: screenshot.error || null,
840
905
  },
841
906
  };
842
- fs_1.default.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
907
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
843
908
  return {
844
909
  ...previewGate,
845
910
  artifacts: {
@@ -889,7 +954,7 @@ class APIClient {
889
954
  css,
890
955
  js,
891
956
  entryPath: artifacts.htmlPath,
892
- workspaceName: path_1.default.basename(rootPath),
957
+ workspaceName: path.basename(rootPath),
893
958
  });
894
959
  // Skip preview proof if payload is still too large (> 4 MB)
895
960
  if (Buffer.byteLength(proofPayload, 'utf8') > 4 * 1024 * 1024) {
@@ -1021,7 +1086,7 @@ class APIClient {
1021
1086
  if (!authToken) {
1022
1087
  throw new Error('Not authenticated. Run vigthoria login first.');
1023
1088
  }
1024
- const response = await axios_1.default.post(`${baseUrl}/api/auth/sso`, { token: authToken }, {
1089
+ const response = await axios.post(`${baseUrl}/api/auth/sso`, { token: authToken }, {
1025
1090
  headers: {
1026
1091
  'Content-Type': 'application/json',
1027
1092
  },
@@ -1081,7 +1146,7 @@ class APIClient {
1081
1146
  query.set('search', options.search);
1082
1147
  }
1083
1148
  const url = `${this.vigFlowEndpoint(baseUrl, '/templates')}${query.size > 0 ? `?${query.toString()}` : ''}`;
1084
- const response = await axios_1.default.get(url, {
1149
+ const response = await axios.get(url, {
1085
1150
  headers,
1086
1151
  timeout: 30000,
1087
1152
  });
@@ -1091,7 +1156,7 @@ class APIClient {
1091
1156
  }
1092
1157
  async listVigFlowWorkflows() {
1093
1158
  return this.withVigFlow('list workflows', async (baseUrl, headers) => {
1094
- const response = await axios_1.default.get(this.vigFlowEndpoint(baseUrl, '/workflows'), {
1159
+ const response = await axios.get(this.vigFlowEndpoint(baseUrl, '/workflows'), {
1095
1160
  headers,
1096
1161
  timeout: 30000,
1097
1162
  });
@@ -1143,7 +1208,7 @@ class APIClient {
1143
1208
  }
1144
1209
  async useVigFlowTemplate(templateId, options = {}) {
1145
1210
  return this.withVigFlow('use template', async (baseUrl, headers) => {
1146
- const response = await axios_1.default.post(`${this.vigFlowEndpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`)}`, {
1211
+ const response = await axios.post(`${this.vigFlowEndpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`)}`, {
1147
1212
  name: options.name,
1148
1213
  variables: options.variables || {},
1149
1214
  }, {
@@ -1159,7 +1224,7 @@ class APIClient {
1159
1224
  }
1160
1225
  async runVigFlowWorkflow(workflowId, options = {}) {
1161
1226
  return this.withVigFlow('run workflow', async (baseUrl, headers) => {
1162
- const response = await axios_1.default.post(`${this.vigFlowEndpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`)}`, {
1227
+ const response = await axios.post(`${this.vigFlowEndpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`)}`, {
1163
1228
  data: options.data || {},
1164
1229
  options: options.executionOptions || {},
1165
1230
  }, {
@@ -1175,7 +1240,7 @@ class APIClient {
1175
1240
  }
1176
1241
  async getVigFlowExecutionStatus(executionId) {
1177
1242
  return this.withVigFlow('execution status', async (baseUrl, headers) => {
1178
- const response = await axios_1.default.get(`${this.vigFlowEndpoint(baseUrl, `/executions/${encodeURIComponent(executionId)}`)}`, {
1243
+ const response = await axios.get(`${this.vigFlowEndpoint(baseUrl, `/executions/${encodeURIComponent(executionId)}`)}`, {
1179
1244
  headers,
1180
1245
  timeout: 30000,
1181
1246
  });
@@ -1218,7 +1283,7 @@ class APIClient {
1218
1283
  projectPath: effectiveWorkspacePath,
1219
1284
  targetPath: effectiveWorkspacePath,
1220
1285
  localWorkspacePath: localWorkspacePath || null,
1221
- localWorkspaceName: localWorkspacePath ? path_1.default.basename(localWorkspacePath) : null,
1286
+ localWorkspaceName: localWorkspacePath ? path.basename(localWorkspacePath) : null,
1222
1287
  localWorkspaceSummary,
1223
1288
  // Signal to the server that the workspace filesystem is not locally
1224
1289
  // accessible — it must hydrate a temp directory from the provided
@@ -1338,7 +1403,7 @@ class APIClient {
1338
1403
  projectPath: resolvedContext.projectPath || targetPath,
1339
1404
  targetPath,
1340
1405
  localWorkspacePath: targetPath,
1341
- localWorkspaceName: targetPath ? path_1.default.basename(targetPath) : null,
1406
+ localWorkspaceName: targetPath ? path.basename(targetPath) : null,
1342
1407
  contextId: resolvedContext.contextId,
1343
1408
  traceId: resolvedContext.traceId,
1344
1409
  requestStartedAt: resolvedContext.requestStartedAt,
@@ -1353,7 +1418,7 @@ class APIClient {
1353
1418
  if (!rootPath) {
1354
1419
  return null;
1355
1420
  }
1356
- fs_1.default.mkdirSync(rootPath, { recursive: true });
1421
+ fs.mkdirSync(rootPath, { recursive: true });
1357
1422
  const appName = this.extractEmergencyAppName(message);
1358
1423
  const html = `<!DOCTYPE html>
1359
1424
  <html lang="en">
@@ -1795,14 +1860,14 @@ menu {
1795
1860
  });
1796
1861
  });
1797
1862
  `;
1798
- fs_1.default.writeFileSync(path_1.default.join(rootPath, 'index.html'), `${html.trimEnd()}\n`, 'utf8');
1799
- fs_1.default.writeFileSync(path_1.default.join(rootPath, 'styles.css'), `${css.trimEnd()}\n`, 'utf8');
1800
- fs_1.default.writeFileSync(path_1.default.join(rootPath, 'scripts.js'), `${js.trimEnd()}\n`, 'utf8');
1863
+ fs.writeFileSync(path.join(rootPath, 'index.html'), `${html.trimEnd()}\n`, 'utf8');
1864
+ fs.writeFileSync(path.join(rootPath, 'styles.css'), `${css.trimEnd()}\n`, 'utf8');
1865
+ fs.writeFileSync(path.join(rootPath, 'scripts.js'), `${js.trimEnd()}\n`, 'utf8');
1801
1866
  return appName;
1802
1867
  }
1803
1868
  ensureExecutionContext(context = {}) {
1804
1869
  const existingId = String(context.contextId || context.traceId || '').trim();
1805
- const contextId = existingId || `vig-${Date.now()}-${(0, crypto_1.randomUUID)().slice(0, 8)}`;
1870
+ const contextId = existingId || `vig-${Date.now()}-${randomUUID().slice(0, 8)}`;
1806
1871
  return {
1807
1872
  ...context,
1808
1873
  contextId,
@@ -1906,21 +1971,21 @@ menu {
1906
1971
  }
1907
1972
  resolveServerBindableWorkspacePath(context = {}) {
1908
1973
  const candidate = this.resolveAgentTargetPath(context);
1909
- if (!candidate || this.isLikelyWindowsPath(candidate) || !path_1.default.isAbsolute(candidate) || !fs_1.default.existsSync(candidate)) {
1974
+ if (!candidate || this.isLikelyWindowsPath(candidate) || !path.isAbsolute(candidate) || !fs.existsSync(candidate)) {
1910
1975
  return '';
1911
1976
  }
1912
1977
  const configuredRoots = (process.env.VIGTHORIA_SERVER_WORKSPACE_ROOTS || '/var/www/vigthoria-user-workspaces,/var/lib/vigthoria-workspaces')
1913
1978
  .split(',')
1914
1979
  .map((entry) => entry.trim())
1915
1980
  .filter(Boolean);
1916
- const resolvedCandidate = fs_1.default.realpathSync(candidate);
1981
+ const resolvedCandidate = fs.realpathSync(candidate);
1917
1982
  for (const root of configuredRoots) {
1918
- if (!fs_1.default.existsSync(root)) {
1983
+ if (!fs.existsSync(root)) {
1919
1984
  continue;
1920
1985
  }
1921
- const resolvedRoot = fs_1.default.realpathSync(root);
1986
+ const resolvedRoot = fs.realpathSync(root);
1922
1987
  try {
1923
- if (path_1.default.relative(resolvedRoot, resolvedCandidate) === '' || !path_1.default.relative(resolvedRoot, resolvedCandidate).startsWith('..')) {
1988
+ if (path.relative(resolvedRoot, resolvedCandidate) === '' || !path.relative(resolvedRoot, resolvedCandidate).startsWith('..')) {
1924
1989
  return resolvedCandidate;
1925
1990
  }
1926
1991
  }
@@ -1931,21 +1996,21 @@ menu {
1931
1996
  return '';
1932
1997
  }
1933
1998
  buildLocalWorkspaceSummary(rootPath) {
1934
- if (!rootPath || !fs_1.default.existsSync(rootPath)) {
1999
+ if (!rootPath || !fs.existsSync(rootPath)) {
1935
2000
  return null;
1936
2001
  }
1937
2002
  try {
1938
2003
  const summary = {
1939
2004
  path: rootPath,
1940
- name: path_1.default.basename(rootPath),
2005
+ name: path.basename(rootPath),
1941
2006
  files: [],
1942
2007
  };
1943
2008
  const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
1944
2009
  summary.fileCount = snapshot.fileCount;
1945
2010
  summary.files = snapshot.paths.slice(0, 40);
1946
- const packageJsonPath = path_1.default.join(rootPath, 'package.json');
1947
- if (fs_1.default.existsSync(packageJsonPath)) {
1948
- const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
2011
+ const packageJsonPath = path.join(rootPath, 'package.json');
2012
+ if (fs.existsSync(packageJsonPath)) {
2013
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
1949
2014
  summary.packageJson = {
1950
2015
  name: pkg.name || null,
1951
2016
  version: pkg.version || null,
@@ -1954,9 +2019,9 @@ menu {
1954
2019
  devDependencies: Object.keys(pkg.devDependencies || {}).slice(0, 20),
1955
2020
  };
1956
2021
  }
1957
- const readmePath = path_1.default.join(rootPath, 'README.md');
1958
- if (fs_1.default.existsSync(readmePath)) {
1959
- summary.readmeExcerpt = fs_1.default.readFileSync(readmePath, 'utf8').slice(0, 2500);
2022
+ const readmePath = path.join(rootPath, 'README.md');
2023
+ if (fs.existsSync(readmePath)) {
2024
+ summary.readmeExcerpt = fs.readFileSync(readmePath, 'utf8').slice(0, 2500);
1960
2025
  }
1961
2026
  // Hydrate workspace: include actual file contents so the V3 server
1962
2027
  // can populate the remote workspace before the agent starts.
@@ -1991,17 +2056,17 @@ menu {
1991
2056
  for (const relativePath of filePaths) {
1992
2057
  if (totalBytes >= MAX_TOTAL_BYTES)
1993
2058
  break;
1994
- const ext = path_1.default.extname(relativePath).toLowerCase();
2059
+ const ext = path.extname(relativePath).toLowerCase();
1995
2060
  if (BINARY_EXTENSIONS.has(ext))
1996
2061
  continue;
1997
2062
  if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
1998
2063
  continue;
1999
- const absolutePath = path_1.default.join(rootPath, relativePath);
2064
+ const absolutePath = path.join(rootPath, relativePath);
2000
2065
  try {
2001
- const stat = fs_1.default.statSync(absolutePath);
2066
+ const stat = fs.statSync(absolutePath);
2002
2067
  if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.size === 0)
2003
2068
  continue;
2004
- const content = fs_1.default.readFileSync(absolutePath, 'utf8');
2069
+ const content = fs.readFileSync(absolutePath, 'utf8');
2005
2070
  // Skip likely binary content (high ratio of non-printable chars)
2006
2071
  if (/[\x00-\x08\x0e-\x1f]/.test(content.slice(0, 512)))
2007
2072
  continue;
@@ -2017,7 +2082,7 @@ menu {
2017
2082
  hasAgentWorkspaceOutput(context = {}) {
2018
2083
  try {
2019
2084
  const root = this.resolveAgentTargetPath(context);
2020
- if (!root || !fs_1.default.existsSync(root)) {
2085
+ if (!root || !fs.existsSync(root)) {
2021
2086
  return false;
2022
2087
  }
2023
2088
  const stack = [root];
@@ -2025,12 +2090,12 @@ menu {
2025
2090
  const current = stack.pop();
2026
2091
  if (!current)
2027
2092
  continue;
2028
- const entries = fs_1.default.readdirSync(current, { withFileTypes: true });
2093
+ const entries = fs.readdirSync(current, { withFileTypes: true });
2029
2094
  for (const entry of entries) {
2030
2095
  if (entry.name === '.git' || entry.name === 'node_modules') {
2031
2096
  continue;
2032
2097
  }
2033
- const fullPath = path_1.default.join(current, entry.name);
2098
+ const fullPath = path.join(current, entry.name);
2034
2099
  if (entry.isFile()) {
2035
2100
  return true;
2036
2101
  }
@@ -2054,11 +2119,11 @@ menu {
2054
2119
  if (!current) {
2055
2120
  continue;
2056
2121
  }
2057
- for (const entry of fs_1.default.readdirSync(current, { withFileTypes: true })) {
2122
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
2058
2123
  if (entry.name === '.git' || entry.name === 'node_modules') {
2059
2124
  continue;
2060
2125
  }
2061
- const fullPath = path_1.default.join(current, entry.name);
2126
+ const fullPath = path.join(current, entry.name);
2062
2127
  if (entry.isDirectory()) {
2063
2128
  stack.push(fullPath);
2064
2129
  continue;
@@ -2067,8 +2132,8 @@ menu {
2067
2132
  continue;
2068
2133
  }
2069
2134
  fileCount += 1;
2070
- const stat = fs_1.default.statSync(fullPath);
2071
- entries.push(`${path_1.default.relative(rootPath, fullPath)}:${stat.size}:${stat.mtimeMs}`);
2135
+ const stat = fs.statSync(fullPath);
2136
+ entries.push(`${path.relative(rootPath, fullPath)}:${stat.size}:${stat.mtimeMs}`);
2072
2137
  }
2073
2138
  }
2074
2139
  entries.sort();
@@ -2080,7 +2145,7 @@ menu {
2080
2145
  }
2081
2146
  async waitForAgentWorkspaceSettle(context = {}, options = {}) {
2082
2147
  const rootPath = this.resolveAgentTargetPath(context);
2083
- if (!rootPath || !fs_1.default.existsSync(rootPath)) {
2148
+ if (!rootPath || !fs.existsSync(rootPath)) {
2084
2149
  return;
2085
2150
  }
2086
2151
  const timeoutMs = options.timeoutMs || 15000;
@@ -2145,7 +2210,36 @@ menu {
2145
2210
  return Array.from(candidates);
2146
2211
  }
2147
2212
  captureV3AgentStreamMutation(event, streamedFiles, serverRoot) {
2148
- if (!event || event.type !== 'tool_call' || !streamedFiles) {
2213
+ if (!event || !streamedFiles) {
2214
+ return;
2215
+ }
2216
+ if (event.type === 'file_mutation' && typeof event.path === 'string') {
2217
+ const filePath = this.normalizeAgentWorkspaceRelativePath(event.path, serverRoot || undefined);
2218
+ if (!filePath) {
2219
+ return;
2220
+ }
2221
+ if (event.action === 'delete') {
2222
+ delete streamedFiles[filePath];
2223
+ return;
2224
+ }
2225
+ if (typeof event.content === 'string') {
2226
+ streamedFiles[filePath] = event.content;
2227
+ }
2228
+ return;
2229
+ }
2230
+ if (event.type === 'workspace_snapshot' && event.files && typeof event.files === 'object') {
2231
+ for (const [rawPath, content] of Object.entries(event.files)) {
2232
+ if (typeof content !== 'string') {
2233
+ continue;
2234
+ }
2235
+ const filePath = this.normalizeAgentWorkspaceRelativePath(rawPath, serverRoot || undefined);
2236
+ if (filePath) {
2237
+ streamedFiles[filePath] = content;
2238
+ }
2239
+ }
2240
+ return;
2241
+ }
2242
+ if (event.type !== 'tool_call') {
2149
2243
  return;
2150
2244
  }
2151
2245
  const args = event.arguments || {};
@@ -2166,18 +2260,95 @@ menu {
2166
2260
  }
2167
2261
  }
2168
2262
  }
2263
+ applyV3AgentStreamEventToWorkspace(event, context = {}, serverRoot) {
2264
+ if (!event || !['file_mutation', 'workspace_snapshot'].includes(String(event.type || ''))) {
2265
+ return false;
2266
+ }
2267
+ const rootPath = this.resolveAgentTargetPath(context);
2268
+ if (!rootPath) {
2269
+ return false;
2270
+ }
2271
+ if (event.type === 'workspace_snapshot' && event.files && typeof event.files === 'object') {
2272
+ let applied = false;
2273
+ for (const [rawPath, content] of Object.entries(event.files)) {
2274
+ if (typeof content !== 'string') {
2275
+ continue;
2276
+ }
2277
+ applied = this.writeV3AgentWorkspaceFile(rootPath, rawPath, content, serverRoot || undefined) || applied;
2278
+ }
2279
+ return applied;
2280
+ }
2281
+ if (event.type === 'file_mutation' && typeof event.path === 'string') {
2282
+ const relativePath = this.normalizeAgentWorkspaceRelativePath(event.path, serverRoot || undefined);
2283
+ if (!relativePath) {
2284
+ return false;
2285
+ }
2286
+ if (event.action === 'delete') {
2287
+ return this.deleteV3AgentWorkspaceFile(rootPath, relativePath);
2288
+ }
2289
+ if (typeof event.content === 'string') {
2290
+ return this.writeV3AgentWorkspaceFile(rootPath, relativePath, event.content, rootPath);
2291
+ }
2292
+ }
2293
+ return false;
2294
+ }
2295
+ writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
2296
+ const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, sourceRoot || rootPath);
2297
+ if (!relativePath) {
2298
+ return false;
2299
+ }
2300
+ const absolutePath = path.resolve(rootPath, relativePath);
2301
+ const resolvedRoot = path.resolve(rootPath);
2302
+ if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
2303
+ this.logger.warn(`Refusing to write V3 file outside workspace: ${rawPath}`);
2304
+ return false;
2305
+ }
2306
+ try {
2307
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
2308
+ if (fs.existsSync(absolutePath)) {
2309
+ const existing = fs.readFileSync(absolutePath, 'utf8');
2310
+ if (existing === content) {
2311
+ return false;
2312
+ }
2313
+ }
2314
+ fs.writeFileSync(absolutePath, content, 'utf8');
2315
+ return true;
2316
+ }
2317
+ catch (error) {
2318
+ this.logger.warn(`Failed to write V3 file ${relativePath}: ${error.message}`);
2319
+ return false;
2320
+ }
2321
+ }
2322
+ deleteV3AgentWorkspaceFile(rootPath, rawPath) {
2323
+ const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, rootPath);
2324
+ if (!relativePath) {
2325
+ return false;
2326
+ }
2327
+ const absolutePath = path.resolve(rootPath, relativePath);
2328
+ const resolvedRoot = path.resolve(rootPath);
2329
+ if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
2330
+ this.logger.warn(`Refusing to delete V3 file outside workspace: ${rawPath}`);
2331
+ return false;
2332
+ }
2333
+ try {
2334
+ if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
2335
+ fs.unlinkSync(absolutePath);
2336
+ return true;
2337
+ }
2338
+ }
2339
+ catch (error) {
2340
+ this.logger.warn(`Failed to delete V3 file ${relativePath}: ${error.message}`);
2341
+ }
2342
+ return false;
2343
+ }
2169
2344
  recoverAgentWorkspaceFiles(context = {}, streamedFiles = {}, expectedFiles = []) {
2170
2345
  const rootPath = this.resolveAgentTargetPath(context);
2171
2346
  if (!rootPath || Object.keys(streamedFiles).length === 0) {
2172
2347
  return;
2173
2348
  }
2174
- // Create the local workspace directory if it doesn't exist yet.
2175
- // This is needed for remote CLI clients where the user specified a
2176
- // path (e.g., C:\vigthoria\Apps\pacman_rogue) that may not have
2177
- // been created before the V3 run.
2178
- if (!fs_1.default.existsSync(rootPath)) {
2349
+ if (!fs.existsSync(rootPath)) {
2179
2350
  try {
2180
- fs_1.default.mkdirSync(rootPath, { recursive: true });
2351
+ fs.mkdirSync(rootPath, { recursive: true });
2181
2352
  }
2182
2353
  catch {
2183
2354
  return;
@@ -2193,12 +2364,7 @@ menu {
2193
2364
  if (typeof content !== 'string') {
2194
2365
  continue;
2195
2366
  }
2196
- const absolutePath = path_1.default.join(rootPath, relativePath);
2197
- if (fs_1.default.existsSync(absolutePath)) {
2198
- continue;
2199
- }
2200
- fs_1.default.mkdirSync(path_1.default.dirname(absolutePath), { recursive: true });
2201
- fs_1.default.writeFileSync(absolutePath, content, 'utf8');
2367
+ this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath);
2202
2368
  }
2203
2369
  }
2204
2370
  normalizeAgentWorkspaceRelativePath(rawPath, rootPath) {
@@ -2206,10 +2372,35 @@ menu {
2206
2372
  if (!input) {
2207
2373
  return '';
2208
2374
  }
2375
+ const safeRelative = (candidate) => {
2376
+ const stripped = String(candidate || '').replace(/^\/+/, '');
2377
+ if (!stripped || /^[a-zA-Z]:\//.test(stripped)) {
2378
+ return '';
2379
+ }
2380
+ const normalized = path.posix.normalize(stripped);
2381
+ if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
2382
+ return '';
2383
+ }
2384
+ return normalized;
2385
+ };
2386
+ const internalWorkspacePatterns = [
2387
+ /^\/?var\/www\/\.vigthoria\/v3-temp\/vig-remote-[^/]+\/(.+)$/i,
2388
+ /^\.vigthoria\/v3-temp\/vig-remote-[^/]+\/(.+)$/i,
2389
+ /^\/?tmp\/vig-remote(?:-server)?-[^/]+\/(.+)$/i,
2390
+ /^\/?tmp\/vig-fork-[^/]+\/(.+)$/i,
2391
+ /^\/?home\/user\/(.+)$/i,
2392
+ /^\/?root\/(.+)$/i,
2393
+ ];
2394
+ for (const pattern of internalWorkspacePatterns) {
2395
+ const match = input.match(pattern);
2396
+ if (match && match[1]) {
2397
+ return safeRelative(match[1]);
2398
+ }
2399
+ }
2209
2400
  const normalizedRoot = String(rootPath || '').trim().replace(/\\/g, '/').replace(/\/+$/g, '');
2210
2401
  if (normalizedRoot) {
2211
2402
  const rootNoLeadingSlash = normalizedRoot.replace(/^\//, '');
2212
- const rootBase = path_1.default.posix.basename(normalizedRoot);
2403
+ const rootBase = path.posix.basename(normalizedRoot);
2213
2404
  const prefixes = [
2214
2405
  `${normalizedRoot}/`,
2215
2406
  `${rootNoLeadingSlash}/`,
@@ -2217,20 +2408,20 @@ menu {
2217
2408
  ];
2218
2409
  for (const prefix of prefixes) {
2219
2410
  if (input.startsWith(prefix)) {
2220
- return input.slice(prefix.length).replace(/^\//, '');
2411
+ return safeRelative(input.slice(prefix.length));
2221
2412
  }
2222
2413
  }
2223
2414
  const embeddedRoot = `/${rootBase}/`;
2224
2415
  const embeddedIndex = input.indexOf(embeddedRoot);
2225
2416
  if (embeddedIndex >= 0) {
2226
- return input.slice(embeddedIndex + embeddedRoot.length).replace(/^\//, '');
2417
+ return safeRelative(input.slice(embeddedIndex + embeddedRoot.length));
2227
2418
  }
2228
2419
  }
2229
- return input.replace(/^\//, '');
2420
+ return safeRelative(input);
2230
2421
  }
2231
2422
  async ensureAgentFrontendPolish(message = '', context = {}) {
2232
2423
  const rootPath = this.resolveAgentTargetPath(context);
2233
- if (!rootPath || !fs_1.default.existsSync(rootPath)) {
2424
+ if (!rootPath || !fs.existsSync(rootPath)) {
2234
2425
  return;
2235
2426
  }
2236
2427
  const prompt = String(message || '');
@@ -2240,20 +2431,20 @@ menu {
2240
2431
  if (!looksLikeFrontendTask) {
2241
2432
  return;
2242
2433
  }
2243
- const htmlPath = path_1.default.join(rootPath, 'index.html');
2244
- if (!fs_1.default.existsSync(htmlPath)) {
2434
+ const htmlPath = path.join(rootPath, 'index.html');
2435
+ if (!fs.existsSync(htmlPath)) {
2245
2436
  return;
2246
2437
  }
2247
- const html = fs_1.default.readFileSync(htmlPath, 'utf8');
2438
+ const html = fs.readFileSync(htmlPath, 'utf8');
2248
2439
  const ensuredAssets = this.ensureReferencedFrontendAssets(rootPath, html, prompt, 'Vigthoria CLI');
2249
2440
  const cssPath = ensuredAssets.cssPath;
2250
2441
  const jsPath = ensuredAssets.jsPath;
2251
2442
  let nextHtml = ensuredAssets.html;
2252
- if (!cssPath || !fs_1.default.existsSync(cssPath)) {
2443
+ if (!cssPath || !fs.existsSync(cssPath)) {
2253
2444
  return;
2254
2445
  }
2255
- let css = fs_1.default.readFileSync(cssPath, 'utf8');
2256
- let js = fs_1.default.existsSync(jsPath) ? fs_1.default.readFileSync(jsPath, 'utf8') : '';
2446
+ let css = fs.readFileSync(cssPath, 'utf8');
2447
+ let js = fs.existsSync(jsPath) ? fs.readFileSync(jsPath, 'utf8') : '';
2257
2448
  const keyframesBlocks = Array.from(js.matchAll(/@keyframes[\s\S]*?\n\}/g)).map((match) => match[0]);
2258
2449
  if (keyframesBlocks.length > 0) {
2259
2450
  const migrated = keyframesBlocks.filter((block) => !css.includes(block));
@@ -2261,8 +2452,8 @@ menu {
2261
2452
  css = `${css.trimEnd()}\n\n/* Vigthoria CLI Recovered CSS */\n${migrated.join('\n\n')}\n`;
2262
2453
  }
2263
2454
  js = js.replace(/\n?@keyframes[\s\S]*?\n\}/g, '').trim();
2264
- fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2265
- fs_1.default.writeFileSync(jsPath, js ? `${js.trimEnd()}\n` : '', 'utf8');
2455
+ fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2456
+ fs.writeFileSync(jsPath, js ? `${js.trimEnd()}\n` : '', 'utf8');
2266
2457
  }
2267
2458
  const wantsPricing = /(pay-as-you-go|pricing)/i.test(prompt);
2268
2459
  if (wantsPricing && !/(id="pricing"|Pay-as-you-go|pay-as-you-go|pricing tiers)/i.test(nextHtml)) {
@@ -2305,24 +2496,24 @@ menu {
2305
2496
  nextHtml = repairedAssets.html;
2306
2497
  css = repairedAssets.css;
2307
2498
  if (nextHtml !== html) {
2308
- fs_1.default.writeFileSync(htmlPath, `${nextHtml.trimEnd()}\n`, 'utf8');
2309
- nextHtml = fs_1.default.readFileSync(htmlPath, 'utf8');
2499
+ fs.writeFileSync(htmlPath, `${nextHtml.trimEnd()}\n`, 'utf8');
2500
+ nextHtml = fs.readFileSync(htmlPath, 'utf8');
2310
2501
  }
2311
- if (css !== fs_1.default.readFileSync(cssPath, 'utf8')) {
2312
- fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2502
+ if (css !== fs.readFileSync(cssPath, 'utf8')) {
2503
+ fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2313
2504
  }
2314
2505
  if (/classList\.add\('hidden'\)|classList\.add\("hidden"\)|classList\.add\('revealed'\)|classList\.add\("revealed"\)/.test(js)
2315
2506
  && !/\.hidden\b|\.revealed\b/.test(css)) {
2316
2507
  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`;
2317
- fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2508
+ fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2318
2509
  }
2319
2510
  if (/^\s*sections\./m.test(js) && !/(?:const|let|var)\s+sections\s*=/.test(js)) {
2320
2511
  js = `const sections = document.querySelectorAll('section');\n\n${js.trimStart()}`;
2321
- fs_1.default.writeFileSync(jsPath, `${js.trimEnd()}\n`, 'utf8');
2512
+ fs.writeFileSync(jsPath, `${js.trimEnd()}\n`, 'utf8');
2322
2513
  }
2323
2514
  if (!/@media|matchMedia|mobile-nav|hamburger|menu-toggle/i.test(`${nextHtml}\n${css}\n${js}`)) {
2324
2515
  css = `${css.trimEnd()}\n\n/* Vigthoria CLI Responsive Baseline */\n@media (max-width: 900px) {\n .nav-links, .nav-list, nav ul {\n display: none;\n flex-direction: column;\n gap: 0.75rem;\n }\n\n .nav-links.is-open, .nav-list.is-open, nav ul.is-open {\n display: flex;\n }\n\n .mobile-nav-toggle, #menu-toggle, .menu-toggle {\n display: inline-flex;\n }\n}\n`;
2325
- fs_1.default.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2516
+ fs.writeFileSync(cssPath, `${css.trimEnd()}\n`, 'utf8');
2326
2517
  }
2327
2518
  const combined = `${nextHtml}\n${css}\n${js}`;
2328
2519
  if (/IntersectionObserver|motion-reveal|\.revealed\b|vigCliFadeIn|classList\.add\('is-visible'\)|classList\.add\("is-visible"\)/i.test(combined)) {
@@ -2331,10 +2522,10 @@ menu {
2331
2522
  const cssMarker = '/* Vigthoria CLI Motion Enhancement */';
2332
2523
  const jsMarker = '/* Vigthoria CLI Motion Enhancement */';
2333
2524
  if (!css.includes(cssMarker)) {
2334
- 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');
2525
+ fs.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');
2335
2526
  }
2336
2527
  if (!js.includes(jsMarker)) {
2337
- 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');
2528
+ fs.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');
2338
2529
  }
2339
2530
  }
2340
2531
  injectSectionBeforeFooter(html, sectionMarkup) {
@@ -2368,10 +2559,10 @@ menu {
2368
2559
  if (!normalized) {
2369
2560
  return '';
2370
2561
  }
2371
- const absolutePath = path_1.default.join(rootPath, normalized);
2372
- if (!fs_1.default.existsSync(absolutePath)) {
2373
- fs_1.default.mkdirSync(path_1.default.dirname(absolutePath), { recursive: true });
2374
- fs_1.default.writeFileSync(absolutePath, content, 'utf8');
2562
+ const absolutePath = path.join(rootPath, normalized);
2563
+ if (!fs.existsSync(absolutePath)) {
2564
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
2565
+ fs.writeFileSync(absolutePath, content, 'utf8');
2375
2566
  }
2376
2567
  return absolutePath;
2377
2568
  };
@@ -2381,8 +2572,8 @@ menu {
2381
2572
  const jsRefs = Array.from(html.matchAll(/<script[^>]+src=["']([^"']+\.(?:js|mjs)(?:\?[^"']*)?)["']/gi))
2382
2573
  .map((match) => localAssetPath(match[1]))
2383
2574
  .filter(Boolean);
2384
- let cssPath = cssRefs.map((ref) => path_1.default.join(rootPath, ref)).find((candidate) => fs_1.default.existsSync(candidate)) || '';
2385
- let jsPath = jsRefs.map((ref) => path_1.default.join(rootPath, ref)).find((candidate) => fs_1.default.existsSync(candidate)) || '';
2575
+ let cssPath = cssRefs.map((ref) => path.join(rootPath, ref)).find((candidate) => fs.existsSync(candidate)) || '';
2576
+ let jsPath = jsRefs.map((ref) => path.join(rootPath, ref)).find((candidate) => fs.existsSync(candidate)) || '';
2386
2577
  const cssTemplate = this.buildFallbackFrontendCss(surfaceLabel);
2387
2578
  const jsTemplate = this.buildFallbackFrontendJs(surfaceLabel);
2388
2579
  if (!cssPath && cssRefs.length > 0) {
@@ -2410,7 +2601,7 @@ menu {
2410
2601
  return {
2411
2602
  html,
2412
2603
  cssPath,
2413
- jsPath: jsPath || path_1.default.join(rootPath, 'scripts.js'),
2604
+ jsPath: jsPath || path.join(rootPath, 'scripts.js'),
2414
2605
  };
2415
2606
  }
2416
2607
  buildFallbackFrontendCss(surfaceLabel) {
@@ -2561,7 +2752,7 @@ document.addEventListener('DOMContentLoaded', () => {
2561
2752
  return true;
2562
2753
  }
2563
2754
  const sanitized = candidate.split('?')[0].split('#')[0].replace(/^\//, '');
2564
- return !sanitized || fs_1.default.existsSync(path_1.default.join(rootPath, sanitized));
2755
+ return !sanitized || fs.existsSync(path.join(rootPath, sanitized));
2565
2756
  };
2566
2757
  const repairedHtml = html.replace(/(<img\b[^>]*\ssrc=["'])([^"']+)(["'][^>]*>)/gi, (match, prefix, assetPath, suffix) => {
2567
2758
  if (hasLocalAsset(assetPath)) {
@@ -2583,22 +2774,22 @@ document.addEventListener('DOMContentLoaded', () => {
2583
2774
  formatV3AgentResponse(data) {
2584
2775
  const result = data?.result || {};
2585
2776
  if (typeof result === 'string') {
2586
- return result;
2777
+ return sanitizeUserFacingPathText(result);
2587
2778
  }
2588
2779
  if (typeof result?.summary === 'string' && result.summary.trim()) {
2589
- return result.summary;
2780
+ return sanitizeUserFacingPathText(result.summary);
2590
2781
  }
2591
2782
  if (typeof result?.message === 'string' && result.message.trim()) {
2592
- return result.message;
2783
+ return sanitizeUserFacingPathText(result.message);
2593
2784
  }
2594
2785
  if (Array.isArray(data?.events)) {
2595
2786
  const completionEvent = [...data.events].reverse().find((event) => event && event.type === 'complete' && typeof event.summary === 'string' && event.summary.trim());
2596
2787
  if (completionEvent) {
2597
- return completionEvent.summary;
2788
+ return sanitizeUserFacingPathText(completionEvent.summary);
2598
2789
  }
2599
2790
  const messageEvent = [...data.events].reverse().find((event) => event && event.type === 'message' && typeof event.content === 'string' && event.content.trim());
2600
2791
  if (messageEvent) {
2601
- return messageEvent.content;
2792
+ return sanitizeUserFacingPathText(messageEvent.content);
2602
2793
  }
2603
2794
  // Synthesize a grounded answer from the tool-call evidence the
2604
2795
  // agent produced, rather than dumping the raw event trace.
@@ -2610,9 +2801,9 @@ document.addEventListener('DOMContentLoaded', () => {
2610
2801
  // Last resort: if data has files written, report them.
2611
2802
  if (data?.files && typeof data.files === 'object' && Object.keys(data.files).length > 0) {
2612
2803
  const fileList = Object.keys(data.files).join(', ');
2613
- return `Agent wrote workspace files: ${fileList}`;
2804
+ return `Agent wrote workspace files: ${sanitizeUserFacingPathText(fileList)}`;
2614
2805
  }
2615
- const text = JSON.stringify(data, null, 2);
2806
+ const text = sanitizeUserFacingPathText(JSON.stringify(data, null, 2));
2616
2807
  return text.length > 12000 ? `${text.slice(0, 12000)}\n\n[V3 agent output truncated]` : text;
2617
2808
  }
2618
2809
  /**
@@ -2630,27 +2821,27 @@ document.addEventListener('DOMContentLoaded', () => {
2630
2821
  if (event.type === 'tool_result' && event.success && typeof event.output === 'string') {
2631
2822
  const name = event.name || 'unknown_tool';
2632
2823
  if (name === 'read_file' && typeof event.target === 'string') {
2633
- filesRead.push(event.target);
2824
+ filesRead.push(sanitizeUserFacingPathText(event.target));
2634
2825
  }
2635
2826
  else if ((name === 'write_file' || name === 'create_file') && typeof event.target === 'string') {
2636
- filesWritten.push(event.target);
2827
+ filesWritten.push(sanitizeUserFacingPathText(event.target));
2637
2828
  }
2638
2829
  else {
2639
2830
  // Keep last ~300 chars of output for context
2640
2831
  const excerpt = event.output.length > 300 ? event.output.slice(-300) : event.output;
2641
- toolResults.push(`[${name}] ${excerpt}`);
2832
+ toolResults.push(`[${name}] ${sanitizeUserFacingPathText(excerpt)}`);
2642
2833
  }
2643
2834
  }
2644
2835
  if (event.type === 'assistant' && typeof event.content === 'string' && event.content.trim()) {
2645
- assistantFragments.push(event.content.trim());
2836
+ assistantFragments.push(sanitizeUserFacingPathText(event.content.trim()));
2646
2837
  }
2647
2838
  // Some servers emit 'text' events for incremental assistant text
2648
2839
  if (event.type === 'text' && typeof event.content === 'string' && event.content.trim()) {
2649
- assistantFragments.push(event.content.trim());
2840
+ assistantFragments.push(sanitizeUserFacingPathText(event.content.trim()));
2650
2841
  }
2651
2842
  // Some servers emit content_block_delta for streamed text
2652
2843
  if (event.type === 'content_block_delta' && typeof event.delta?.text === 'string' && event.delta.text.trim()) {
2653
- assistantFragments.push(event.delta.text.trim());
2844
+ assistantFragments.push(sanitizeUserFacingPathText(event.delta.text.trim()));
2654
2845
  }
2655
2846
  }
2656
2847
  // Concatenate ALL assistant text fragments in order — keeps full
@@ -2674,6 +2865,25 @@ document.addEventListener('DOMContentLoaded', () => {
2674
2865
  ? sections.join('\n\n')
2675
2866
  : '';
2676
2867
  }
2868
+ sanitizeV3AgentEventForUser(event) {
2869
+ const sanitizeValue = (value) => {
2870
+ if (typeof value === 'string') {
2871
+ return sanitizeUserFacingPathText(value);
2872
+ }
2873
+ if (Array.isArray(value)) {
2874
+ return value.map((entry) => sanitizeValue(entry));
2875
+ }
2876
+ if (value && typeof value === 'object') {
2877
+ const copy = {};
2878
+ for (const [key, entry] of Object.entries(value)) {
2879
+ copy[key] = sanitizeValue(entry);
2880
+ }
2881
+ return copy;
2882
+ }
2883
+ return value;
2884
+ };
2885
+ return sanitizeValue(event);
2886
+ }
2677
2887
  async collectV3AgentStream(response, context = {}) {
2678
2888
  if (!response.body || typeof response.body.getReader !== 'function') {
2679
2889
  return response.json();
@@ -2747,7 +2957,8 @@ document.addEventListener('DOMContentLoaded', () => {
2747
2957
  continue;
2748
2958
  }
2749
2959
  const event = JSON.parse(payload);
2750
- events.push(event);
2960
+ const userEvent = this.sanitizeV3AgentEventForUser(event);
2961
+ events.push(userEvent);
2751
2962
  if (!contextId && typeof event.context_id === 'string' && event.context_id.trim()) {
2752
2963
  contextId = event.context_id.trim();
2753
2964
  }
@@ -2755,6 +2966,7 @@ document.addEventListener('DOMContentLoaded', () => {
2755
2966
  serverWorkspaceRoot = event.workspace_root.trim();
2756
2967
  }
2757
2968
  this.captureV3AgentStreamMutation(event, streamedFiles, serverWorkspaceRoot);
2969
+ this.applyV3AgentStreamEventToWorkspace(event, context, serverWorkspaceRoot);
2758
2970
  // Empty workspace guard: if the remote agent lists its root
2759
2971
  // and finds nothing while our local workspace has files, the
2760
2972
  // workspace was not hydrated. Abort early with a clear error
@@ -2771,7 +2983,7 @@ document.addEventListener('DOMContentLoaded', () => {
2771
2983
  || /^\[\/tmp\/vig-remote-server-[^\]]+\]\s*$/.test(listOutput);
2772
2984
  if (looksEmpty) {
2773
2985
  const localPath = this.resolveAgentTargetPath(context);
2774
- const localHasFiles = localPath && fs_1.default.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
2986
+ const localHasFiles = localPath && fs.existsSync(localPath) && this.hasAgentWorkspaceOutput({ ...context, projectPath: localPath, targetPath: localPath });
2775
2987
  if (localHasFiles) {
2776
2988
  throw new Error('Remote workspace is empty — the V3 server did not receive your project files. '
2777
2989
  + 'Your local workspace has files but the remote agent sees an empty directory. '
@@ -2781,7 +2993,7 @@ document.addEventListener('DOMContentLoaded', () => {
2781
2993
  }
2782
2994
  if (typeof context.onStreamEvent === 'function') {
2783
2995
  try {
2784
- context.onStreamEvent(event);
2996
+ context.onStreamEvent(userEvent);
2785
2997
  }
2786
2998
  catch {
2787
2999
  // Ignore UI callback failures; never break the agent stream for them.
@@ -3095,6 +3307,8 @@ document.addEventListener('DOMContentLoaded', () => {
3095
3307
  mcp_context_id: executionContext.mcpContextId || null,
3096
3308
  rawPrompt: executionContext.rawPrompt || null,
3097
3309
  requestStartedAt: executionContext.requestStartedAt,
3310
+ vigthoriaBrain: executionContext.vigthoriaBrain || null,
3311
+ vigthoria_brain: executionContext.vigthoriaBrain || null,
3098
3312
  },
3099
3313
  workflow_type: executionContext.workflowType || 'full',
3100
3314
  options: {
@@ -3335,7 +3549,7 @@ document.addEventListener('DOMContentLoaded', () => {
3335
3549
  try {
3336
3550
  this.logger.debug(`Canonical Vigthoria Cloud fallback: ${resolvedModel}`);
3337
3551
  const token = this.getAccessToken();
3338
- const response = await axios_1.default.post('https://coder.vigthoria.io/api/ai/chat', {
3552
+ const response = await axios.post('https://coder.vigthoria.io/api/ai/chat', {
3339
3553
  messages,
3340
3554
  model: resolvedModel,
3341
3555
  maxTokens: this.config.get('preferences').maxTokens,
@@ -3469,13 +3683,20 @@ document.addEventListener('DOMContentLoaded', () => {
3469
3683
  const selfHostedModels = new Set([
3470
3684
  'vigthoria-v3-code-35b',
3471
3685
  'vigthoria-v3-code-35b:latest',
3686
+ 'vigthoria-v3-code-9b',
3687
+ 'vigthoria-v3-code-9b:latest',
3688
+ 'vigthoria-v3-balanced-4b',
3689
+ 'vigthoria-v3-balanced-4b:latest',
3472
3690
  'qwen3-coder:latest',
3473
- 'vigthoria-v2-code-8b',
3474
3691
  ]);
3475
3692
  return selfHostedModels.has(resolvedModel)
3476
3693
  || normalizedRequested === 'agent'
3477
3694
  || normalizedRequested === 'code'
3478
3695
  || normalizedRequested === 'code-30b'
3696
+ || normalizedRequested === 'code-35b'
3697
+ || normalizedRequested === 'code-9b'
3698
+ || normalizedRequested === 'balanced'
3699
+ || normalizedRequested === 'balanced-4b'
3479
3700
  || normalizedRequested === 'pro';
3480
3701
  }
3481
3702
  getSelfHostedFallbackModelId(resolvedModel, requestedModel) {
@@ -3489,7 +3710,7 @@ document.addEventListener('DOMContentLoaded', () => {
3489
3710
  const wsUrl = this.config.get('wsUrl');
3490
3711
  const token = this.config.get('authToken');
3491
3712
  return new Promise((resolve, reject) => {
3492
- const ws = new ws_1.default(`${wsUrl}/chat`, {
3713
+ const ws = new WebSocket(`${wsUrl}/chat`, {
3493
3714
  headers: { Authorization: `Bearer ${token}` },
3494
3715
  });
3495
3716
  ws.on('open', () => {
@@ -3518,7 +3739,7 @@ document.addEventListener('DOMContentLoaded', () => {
3518
3739
  const wsUrl = this.config.get('wsUrl');
3519
3740
  const token = this.config.get('authToken');
3520
3741
  return new Promise((resolve, reject) => {
3521
- const ws = new ws_1.default(`${wsUrl}/chat`, {
3742
+ const ws = new WebSocket(`${wsUrl}/chat`, {
3522
3743
  headers: { Authorization: `Bearer ${token}` },
3523
3744
  });
3524
3745
  ws.on('open', () => {
@@ -4415,15 +4636,17 @@ document.addEventListener('DOMContentLoaded', () => {
4415
4636
  // ═══════════════════════════════════════════════════════════════
4416
4637
  // VIGTHORIA LOCAL - Self-hosted models
4417
4638
  // ═══════════════════════════════════════════════════════════════
4418
- 'fast': 'vigthoria-fast-1.7b',
4639
+ 'fast': 'vigthoria-v3-balanced-4b',
4419
4640
  'mini': 'vigthoria-mini-0.6b',
4420
- 'balanced': 'vigthoria-balanced-4b',
4421
- 'balanced-4b': 'vigthoria-balanced-4b',
4641
+ 'balanced': 'vigthoria-v3-balanced-4b',
4642
+ 'balanced-4b': 'vigthoria-v3-balanced-4b',
4422
4643
  'creative': 'vigthoria-creative-9b-v4',
4423
- // Code Models - 30B is the default powerhouse
4644
+ // Code Models - 35B is the default powerhouse
4424
4645
  'code': 'vigthoria-v3-code-35b', // Internal: self-hosted 35B on Blackwell
4425
4646
  'code-30b': 'vigthoria-v3-code-35b',
4426
- 'code-8b': 'vigthoria-v2-code-8b',
4647
+ 'code-35b': 'vigthoria-v3-code-35b',
4648
+ 'code-8b': 'vigthoria-v3-code-9b',
4649
+ 'code-9b': 'vigthoria-v3-code-9b',
4427
4650
  'pro': 'vigthoria-v3-code-35b',
4428
4651
  'agent': 'vigthoria-v3-code-35b',
4429
4652
  'vigthoria-code': 'vigthoria-v3-code-35b',
@@ -4723,7 +4946,7 @@ document.addEventListener('DOMContentLoaded', () => {
4723
4946
  const port = Number.parseInt(process.env.VIGTHORIA_DEVTOOLS_BRIDGE_PORT || '4016', 10);
4724
4947
  const endpoint = `ws://${host}:${port}/ws`;
4725
4948
  return new Promise((resolve) => {
4726
- const socket = net_1.default.connect({ host, port, timeout: 1500 }, () => {
4949
+ const socket = net.connect({ host, port, timeout: 1500 }, () => {
4727
4950
  socket.end();
4728
4951
  resolve({
4729
4952
  name: 'DevTools Bridge',
@@ -4792,4 +5015,3 @@ document.addEventListener('DOMContentLoaded', () => {
4792
5015
  return status.overallOk;
4793
5016
  }
4794
5017
  }
4795
- exports.APIClient = APIClient;