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