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.
- package/README.md +5 -5
- package/dist/commands/auth.js +48 -65
- package/dist/commands/bridge.js +12 -19
- package/dist/commands/cancel.js +15 -22
- package/dist/commands/chat.d.ts +11 -0
- package/dist/commands/chat.js +404 -248
- package/dist/commands/config.js +31 -71
- package/dist/commands/deploy.js +83 -123
- package/dist/commands/device.d.ts +35 -0
- package/dist/commands/device.js +239 -0
- package/dist/commands/edit.js +32 -39
- package/dist/commands/explain.js +18 -25
- package/dist/commands/fork.js +22 -27
- package/dist/commands/generate.js +37 -44
- package/dist/commands/history.js +20 -25
- package/dist/commands/hub.js +95 -102
- package/dist/commands/index.js +41 -46
- package/dist/commands/legion.d.ts +1 -0
- package/dist/commands/legion.js +162 -209
- package/dist/commands/preview.js +60 -98
- package/dist/commands/replay.js +27 -32
- package/dist/commands/repo.js +103 -141
- package/dist/commands/review.js +29 -36
- package/dist/commands/security.js +5 -12
- package/dist/commands/update.js +15 -49
- package/dist/commands/workflow.d.ts +8 -1
- package/dist/commands/workflow.js +53 -19
- package/dist/index.js +409 -234
- package/dist/utils/api.d.ts +5 -0
- package/dist/utils/api.js +398 -176
- package/dist/utils/bridge-client.js +11 -52
- package/dist/utils/cli-state.d.ts +54 -0
- package/dist/utils/cli-state.js +185 -0
- package/dist/utils/config.d.ts +5 -0
- package/dist/utils/config.js +35 -14
- package/dist/utils/context-ranker.js +15 -21
- package/dist/utils/files.js +5 -42
- package/dist/utils/logger.js +42 -50
- package/dist/utils/post-write-validator.js +22 -29
- package/dist/utils/project-memory.d.ts +56 -0
- package/dist/utils/project-memory.js +289 -0
- package/dist/utils/session.d.ts +29 -3
- package/dist/utils/session.js +137 -85
- package/dist/utils/task-display.js +13 -20
- package/dist/utils/tools.d.ts +19 -0
- package/dist/utils/tools.js +84 -87
- package/dist/utils/workspace-cache.js +18 -26
- package/dist/utils/workspace-stream.js +26 -64
- package/install.ps1 +14 -0
- package/package.json +5 -3
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 || '
|
|
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
|
-
|| '
|
|
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 || '
|
|
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
|
|
475
|
-
// masked by unrelated transport outages.
|
|
476
|
-
|
|
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
|
-
|
|
492
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 || !
|
|
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 =
|
|
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 (!
|
|
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) =>
|
|
757
|
-
const js = selectedJsFiles.map((filePath) =>
|
|
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 =
|
|
811
|
-
|
|
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 =
|
|
816
|
-
const screenshotPath =
|
|
817
|
-
const entryAbsolutePath =
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
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()}-${
|
|
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) || !
|
|
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 =
|
|
1981
|
+
const resolvedCandidate = fs.realpathSync(candidate);
|
|
1917
1982
|
for (const root of configuredRoots) {
|
|
1918
|
-
if (!
|
|
1983
|
+
if (!fs.existsSync(root)) {
|
|
1919
1984
|
continue;
|
|
1920
1985
|
}
|
|
1921
|
-
const resolvedRoot =
|
|
1986
|
+
const resolvedRoot = fs.realpathSync(root);
|
|
1922
1987
|
try {
|
|
1923
|
-
if (
|
|
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 || !
|
|
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:
|
|
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 =
|
|
1947
|
-
if (
|
|
1948
|
-
const pkg = JSON.parse(
|
|
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 =
|
|
1958
|
-
if (
|
|
1959
|
-
summary.readmeExcerpt =
|
|
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 =
|
|
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 =
|
|
2064
|
+
const absolutePath = path.join(rootPath, relativePath);
|
|
2000
2065
|
try {
|
|
2001
|
-
const stat =
|
|
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 =
|
|
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 || !
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
2071
|
-
entries.push(`${
|
|
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 || !
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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)
|
|
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)
|
|
2417
|
+
return safeRelative(input.slice(embeddedIndex + embeddedRoot.length));
|
|
2227
2418
|
}
|
|
2228
2419
|
}
|
|
2229
|
-
return input
|
|
2420
|
+
return safeRelative(input);
|
|
2230
2421
|
}
|
|
2231
2422
|
async ensureAgentFrontendPolish(message = '', context = {}) {
|
|
2232
2423
|
const rootPath = this.resolveAgentTargetPath(context);
|
|
2233
|
-
if (!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 =
|
|
2244
|
-
if (!
|
|
2434
|
+
const htmlPath = path.join(rootPath, 'index.html');
|
|
2435
|
+
if (!fs.existsSync(htmlPath)) {
|
|
2245
2436
|
return;
|
|
2246
2437
|
}
|
|
2247
|
-
const html =
|
|
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 || !
|
|
2443
|
+
if (!cssPath || !fs.existsSync(cssPath)) {
|
|
2253
2444
|
return;
|
|
2254
2445
|
}
|
|
2255
|
-
let css =
|
|
2256
|
-
let js =
|
|
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
|
-
|
|
2265
|
-
|
|
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
|
-
|
|
2309
|
-
nextHtml =
|
|
2499
|
+
fs.writeFileSync(htmlPath, `${nextHtml.trimEnd()}\n`, 'utf8');
|
|
2500
|
+
nextHtml = fs.readFileSync(htmlPath, 'utf8');
|
|
2310
2501
|
}
|
|
2311
|
-
if (css !==
|
|
2312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
2372
|
-
if (!
|
|
2373
|
-
|
|
2374
|
-
|
|
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) =>
|
|
2385
|
-
let jsPath = jsRefs.map((ref) =>
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
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 &&
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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 -
|
|
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-
|
|
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 =
|
|
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;
|