vigthoria-cli 1.13.20 → 1.13.22
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/completions/_vigthoria +49 -0
- package/completions/vigthoria.bash +6 -0
- package/completions/vigthoria.fish +41 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +63 -128
- package/dist/commands/background.d.ts +0 -2
- package/dist/commands/background.js +26 -34
- package/dist/commands/bridge.js +45 -41
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +22 -16
- package/dist/commands/chat.d.ts +3 -42
- package/dist/commands/chat.js +378 -1123
- package/dist/commands/coding-registration.d.ts +4 -0
- package/dist/commands/coding-registration.js +24 -0
- package/dist/commands/config.js +23 -29
- package/dist/commands/conversation-registration.d.ts +4 -0
- package/dist/commands/conversation-registration.js +122 -0
- package/dist/commands/deploy.d.ts +8 -2
- package/dist/commands/deploy.js +92 -90
- package/dist/commands/device.d.ts +1 -2
- package/dist/commands/device.js +37 -21
- package/dist/commands/doctor-registration.d.ts +4 -0
- package/dist/commands/doctor-registration.js +200 -0
- package/dist/commands/edit.js +22 -33
- package/dist/commands/explain.js +7 -10
- package/dist/commands/fork.d.ts +1 -2
- package/dist/commands/fork.js +72 -22
- package/dist/commands/game.d.ts +1 -1
- package/dist/commands/game.js +17 -6
- package/dist/commands/generate.js +7 -9
- package/dist/commands/history.js +11 -7
- package/dist/commands/hub.d.ts +2 -2
- package/dist/commands/hub.js +34 -35
- package/dist/commands/legion.d.ts +0 -1
- package/dist/commands/legion.js +62 -79
- package/dist/commands/music.d.ts +1 -2
- package/dist/commands/music.js +62 -27
- package/dist/commands/platform-registration.d.ts +4 -0
- package/dist/commands/platform-registration.js +45 -0
- package/dist/commands/preview.d.ts +0 -1
- package/dist/commands/preview.js +82 -35
- package/dist/commands/product-run-registration.d.ts +4 -0
- package/dist/commands/product-run-registration.js +490 -0
- package/dist/commands/replay.d.ts +1 -2
- package/dist/commands/replay.js +15 -11
- package/dist/commands/repo.d.ts +14 -7
- package/dist/commands/repo.js +245 -196
- package/dist/commands/review.js +9 -12
- package/dist/commands/security.d.ts +1 -2
- package/dist/commands/security.js +12 -6
- package/dist/commands/update-registration.d.ts +3 -0
- package/dist/commands/update-registration.js +338 -0
- package/dist/commands/v4-registration.d.ts +4 -0
- package/dist/commands/v4-registration.js +24 -0
- package/dist/commands/v4.d.ts +1 -1
- package/dist/commands/v4.js +46 -8
- package/dist/commands/wallet.d.ts +1 -3
- package/dist/commands/wallet.js +18 -45
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +52 -39
- package/dist/index.js +122 -1589
- package/dist/utils/agent-stream-state.d.ts +27 -0
- package/dist/utils/agent-stream-state.js +93 -0
- package/dist/utils/agentRoute.d.ts +2 -1
- package/dist/utils/agentRoute.js +22 -9
- package/dist/utils/agentRunOutcome.d.ts +16 -0
- package/dist/utils/agentRunOutcome.js +37 -2
- package/dist/utils/api-client-factory.d.ts +8 -0
- package/dist/utils/api-client-factory.js +8 -0
- package/dist/utils/api.d.ts +44 -266
- package/dist/utils/api.js +647 -3180
- package/dist/utils/auth-session.d.ts +41 -0
- package/dist/utils/auth-session.js +224 -0
- package/dist/utils/brain-hub-client.d.ts +0 -4
- package/dist/utils/brain-hub-client.js +6 -3
- package/dist/utils/bridge-client.js +2 -0
- package/dist/utils/capability-health-service.d.ts +48 -0
- package/dist/utils/capability-health-service.js +64 -0
- package/dist/utils/chat-prompt-policy.d.ts +18 -0
- package/dist/utils/chat-prompt-policy.js +61 -0
- package/dist/utils/chat-tool-call-parser.d.ts +11 -0
- package/dist/utils/chat-tool-call-parser.js +71 -0
- package/dist/utils/cli-state.d.ts +22 -4
- package/dist/utils/cli-state.js +105 -47
- package/dist/utils/code-operations-service.d.ts +104 -0
- package/dist/utils/code-operations-service.js +837 -0
- package/dist/utils/codebase-indexer.d.ts +1 -0
- package/dist/utils/codebase-indexer.js +28 -8
- package/dist/utils/command-contract.d.ts +61 -0
- package/dist/utils/command-contract.js +149 -0
- package/dist/utils/command-policy.d.ts +10 -0
- package/dist/utils/command-policy.js +54 -0
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +220 -94
- package/dist/utils/context-ranker.js +11 -2
- package/dist/utils/context-state.d.ts +39 -0
- package/dist/utils/context-state.js +52 -0
- package/dist/utils/deckEvents.d.ts +0 -5
- package/dist/utils/deckEvents.js +3 -2
- package/dist/utils/desktop-bridge-client.js +2 -0
- package/dist/utils/direct-output-policy.d.ts +3 -0
- package/dist/utils/direct-output-policy.js +76 -0
- package/dist/utils/durable-json.d.ts +57 -0
- package/dist/utils/durable-json.js +319 -0
- package/dist/utils/fastAgentRouter.d.ts +5 -6
- package/dist/utils/fastAgentRouter.js +17 -25
- package/dist/utils/files.js +21 -13
- package/dist/utils/frontend-preview-service.d.ts +76 -0
- package/dist/utils/frontend-preview-service.js +482 -0
- package/dist/utils/goaEvents.d.ts +0 -4
- package/dist/utils/goaEvents.js +5 -2
- package/dist/utils/logger.d.ts +2 -1
- package/dist/utils/logger.js +27 -20
- package/dist/utils/mcp-context-client.d.ts +24 -0
- package/dist/utils/mcp-context-client.js +58 -0
- package/dist/utils/model-governance.d.ts +18 -0
- package/dist/utils/model-governance.js +75 -0
- package/dist/utils/model-transport-service.d.ts +44 -0
- package/dist/utils/model-transport-service.js +111 -0
- package/dist/utils/mutation-journal.d.ts +42 -0
- package/dist/utils/mutation-journal.js +387 -0
- package/dist/utils/network-policy.d.ts +24 -0
- package/dist/utils/network-policy.js +202 -0
- package/dist/utils/operator-client.d.ts +45 -0
- package/dist/utils/operator-client.js +159 -0
- package/dist/utils/post-write-validator.js +7 -9
- package/dist/utils/preview-screenshot-adapter.d.ts +13 -0
- package/dist/utils/preview-screenshot-adapter.js +36 -0
- package/dist/utils/process-policy.d.ts +23 -0
- package/dist/utils/process-policy.js +284 -0
- package/dist/utils/project-memory.d.ts +13 -3
- package/dist/utils/project-memory.js +70 -17
- package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
- package/dist/utils/prompt-workspace-resolver.js +64 -0
- package/dist/utils/release-install.d.ts +11 -0
- package/dist/utils/release-install.js +155 -0
- package/dist/utils/release-policy.d.ts +64 -0
- package/dist/utils/release-policy.js +293 -0
- package/dist/utils/requestIntent.js +22 -3
- package/dist/utils/runtime-capability.d.ts +8 -0
- package/dist/utils/runtime-capability.js +16 -0
- package/dist/utils/secret-policy.d.ts +20 -0
- package/dist/utils/secret-policy.js +160 -0
- package/dist/utils/session.d.ts +17 -3
- package/dist/utils/session.js +354 -110
- package/dist/utils/tool-approval-service.d.ts +17 -0
- package/dist/utils/tool-approval-service.js +78 -0
- package/dist/utils/tool-capability-providers.d.ts +57 -0
- package/dist/utils/tool-capability-providers.js +57 -0
- package/dist/utils/tools.d.ts +2 -23
- package/dist/utils/tools.js +216 -508
- package/dist/utils/update-policy.d.ts +18 -0
- package/dist/utils/update-policy.js +137 -0
- package/dist/utils/v3-agent-client.d.ts +40 -0
- package/dist/utils/v3-agent-client.js +165 -0
- package/dist/utils/v3-workspace-service.d.ts +28 -0
- package/dist/utils/v3-workspace-service.js +279 -0
- package/dist/utils/vigflow-client.d.ts +95 -0
- package/dist/utils/vigflow-client.js +105 -0
- package/dist/utils/workspace-boundary.d.ts +45 -0
- package/dist/utils/workspace-boundary.js +377 -0
- package/dist/utils/workspace-brain-service.d.ts +2 -0
- package/dist/utils/workspace-brain-service.js +11 -1
- package/dist/utils/workspace-cache.js +37 -15
- package/dist/utils/workspace-stream.js +35 -10
- package/install.ps1 +85 -142
- package/install.sh +75 -80
- package/package.json +43 -9
- package/release-policy.json +43 -0
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
- package/scripts/release/generate-completions.mjs +24 -0
- package/scripts/release/install-release.mjs +10 -0
- package/scripts/release/publish-cli-release.mjs +116 -0
- package/scripts/release/publish-cli-release.sh +2 -70
- package/scripts/release/resolve-release-manifest.mjs +26 -0
- package/scripts/release/test-balanced-model-live.sh +42 -0
- package/scripts/release/validate-live-service-gates.sh +90 -0
- package/scripts/release/validate-no-go-gates.sh +135 -174
- package/scripts/release/verify-runtime-consistency.mjs +4 -1
- package/dist/commands/index.d.ts +0 -12
- package/dist/commands/index.js +0 -194
- package/dist/commands/update.d.ts +0 -9
- package/dist/commands/update.js +0 -301
- package/dist/utils/templateInstantPath.d.ts +0 -18
- package/dist/utils/templateInstantPath.js +0 -228
package/dist/utils/api.js
CHANGED
|
@@ -3,23 +3,38 @@
|
|
|
3
3
|
* Connects to coder.vigthoria.io API endpoints
|
|
4
4
|
*/
|
|
5
5
|
import axios from 'axios';
|
|
6
|
-
import { execFile } from 'child_process';
|
|
7
6
|
import { randomUUID } from 'crypto';
|
|
8
7
|
import fs from 'fs';
|
|
9
8
|
import https from 'https';
|
|
10
9
|
import net from 'net';
|
|
11
10
|
import path from 'path';
|
|
12
|
-
import WebSocket from 'ws';
|
|
13
11
|
import { globSync } from 'glob';
|
|
12
|
+
import { AuthSessionService, subscribeAuthInvalidation } from './auth-session.js';
|
|
13
|
+
import { appendContextLayer } from './context-state.js';
|
|
14
14
|
import { buildSemanticContext } from './context-ranker.js';
|
|
15
15
|
import { getChangedFiles } from './workspace-cache.js';
|
|
16
16
|
import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
|
|
17
|
-
import { isV3WorkspaceRootAliasPath, joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
|
|
18
17
|
import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
|
|
19
18
|
import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
|
|
20
19
|
import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
|
|
21
20
|
import { isLocalTestfarmMode, fetchWithServiceTimeout } from './localTestMode.js';
|
|
22
21
|
import { buildClientManifest } from './clientManifest.js';
|
|
22
|
+
import { assertTrustedEndpoint, installAxiosNetworkPolicy, installGlobalFetchPolicy } from './network-policy.js';
|
|
23
|
+
import { assertSafeRelativePath, resolveWorkspacePath } from './workspace-boundary.js';
|
|
24
|
+
import { parseSupportedProcess } from './process-policy.js';
|
|
25
|
+
import { OptionalPuppeteerScreenshotAdapter } from './preview-screenshot-adapter.js';
|
|
26
|
+
import { ModelGovernance } from './model-governance.js';
|
|
27
|
+
import { OperatorClient, OperatorClientError } from './operator-client.js';
|
|
28
|
+
import { CodeOperationsService } from './code-operations-service.js';
|
|
29
|
+
import { McpContextClient } from './mcp-context-client.js';
|
|
30
|
+
import { FrontendPreviewService } from './frontend-preview-service.js';
|
|
31
|
+
import { V3AgentClient } from './v3-agent-client.js';
|
|
32
|
+
import { V3WorkspaceService } from './v3-workspace-service.js';
|
|
33
|
+
import { CapabilityHealthService, } from './capability-health-service.js';
|
|
34
|
+
import { ModelTransportService } from './model-transport-service.js';
|
|
35
|
+
import { VigFlowClient, } from './vigflow-client.js';
|
|
36
|
+
import { MutationJournal, finalizeMutationTransaction, } from './mutation-journal.js';
|
|
37
|
+
import { isSensitivePath, redactSensitiveText, safeChildProcessEnv, scanOutboundContext, } from './secret-policy.js';
|
|
23
38
|
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
|
|
24
39
|
export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
|
|
25
40
|
export class CLIError extends Error {
|
|
@@ -183,28 +198,13 @@ export function sanitizeUserFacingErrorText(input) {
|
|
|
183
198
|
return '';
|
|
184
199
|
}
|
|
185
200
|
const withoutTags = raw.replace(/<[^>]+>/g, ' ');
|
|
186
|
-
return sanitizeUserFacingPathText(withoutTags)
|
|
201
|
+
return redactSensitiveText(sanitizeUserFacingPathText(withoutTags))
|
|
187
202
|
.replace(/\\[nr]/g, ' ')
|
|
188
203
|
.replace(/https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?(?:\/[^\s'")\]]*)?/gi, 'internal AI service')
|
|
189
204
|
.replace(/\b(?:deepseek|openrouter)\/[A-Za-z0-9._/-]+\b/gi, 'cloud model')
|
|
190
205
|
.replace(/\s+/g, ' ')
|
|
191
206
|
.trim();
|
|
192
207
|
}
|
|
193
|
-
const TRUSTED_TOKEN_HOST_PATTERN = /(^|\.)vigthoria\.io$/i;
|
|
194
|
-
function isLoopbackHost(hostname) {
|
|
195
|
-
const host = String(hostname || '').toLowerCase();
|
|
196
|
-
return host === 'localhost' || host === '127.0.0.1';
|
|
197
|
-
}
|
|
198
|
-
function isTrustedTokenDestination(rawUrl) {
|
|
199
|
-
try {
|
|
200
|
-
const parsed = new URL(rawUrl);
|
|
201
|
-
const host = parsed.hostname.toLowerCase();
|
|
202
|
-
return TRUSTED_TOKEN_HOST_PATTERN.test(host) || isLoopbackHost(host);
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
208
|
function resolveAxiosRequestUrl(req) {
|
|
209
209
|
const direct = String(req?.url || '').trim();
|
|
210
210
|
const base = String(req?.baseURL || '').trim();
|
|
@@ -222,14 +222,6 @@ function resolveAxiosRequestUrl(req) {
|
|
|
222
222
|
}
|
|
223
223
|
return direct;
|
|
224
224
|
}
|
|
225
|
-
export function isServerRuntime() {
|
|
226
|
-
if (process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES === '1') {
|
|
227
|
-
return true;
|
|
228
|
-
}
|
|
229
|
-
const host = String(process.env.HOSTNAME || '').toLowerCase();
|
|
230
|
-
const cwd = String(process.cwd() || '').toLowerCase();
|
|
231
|
-
return host.includes('ubuntu') || cwd.startsWith('/var/www');
|
|
232
|
-
}
|
|
233
225
|
export function describeUpstreamStatus(status) {
|
|
234
226
|
if (status >= 500)
|
|
235
227
|
return 'upstream internal error';
|
|
@@ -307,19 +299,141 @@ export class APIClient {
|
|
|
307
299
|
modelRouterClient;
|
|
308
300
|
selfHostedModelRouterClient;
|
|
309
301
|
config;
|
|
302
|
+
authSession;
|
|
303
|
+
unsubscribeAuthInvalidation = null;
|
|
310
304
|
logger;
|
|
311
|
-
|
|
312
|
-
|
|
305
|
+
modelGovernance;
|
|
306
|
+
codeOperations;
|
|
307
|
+
mcpContextClient;
|
|
308
|
+
v3AgentClient;
|
|
309
|
+
v3WorkspaceService;
|
|
310
|
+
capabilityHealthService;
|
|
311
|
+
modelTransportService;
|
|
312
|
+
operatorClient;
|
|
313
|
+
vigFlowClient;
|
|
313
314
|
_httpsAgent = null;
|
|
314
315
|
lastChatTransportErrors = [];
|
|
315
316
|
clientToolErrors = [];
|
|
316
317
|
pendingV3ClientToolTasks = new Set();
|
|
317
318
|
/** Tracks files mutated via client_tool_request during an active V3 SSE stream. */
|
|
318
319
|
activeV3StreamedFiles = null;
|
|
319
|
-
|
|
320
|
-
|
|
320
|
+
lifecycleAbortController = new AbortController();
|
|
321
|
+
frontendPreviewService;
|
|
322
|
+
withLifecycleSignal(signal) {
|
|
323
|
+
return signal
|
|
324
|
+
? AbortSignal.any([signal, this.lifecycleAbortController.signal])
|
|
325
|
+
: this.lifecycleAbortController.signal;
|
|
326
|
+
}
|
|
327
|
+
fetch(input, init = {}) {
|
|
328
|
+
return fetch(input, { ...init, signal: this.withLifecycleSignal(init.signal) });
|
|
329
|
+
}
|
|
330
|
+
constructor(config, logger, dependencies = {}) {
|
|
321
331
|
this.config = config;
|
|
322
332
|
this.logger = logger;
|
|
333
|
+
this.authSession = new AuthSessionService(config);
|
|
334
|
+
this.modelGovernance = new ModelGovernance({
|
|
335
|
+
hasCloudAccess: () => this.config.hasCloudAccess(),
|
|
336
|
+
hasSelfHostedEndpoint: () => Boolean(this.selfHostedModelRouterClient),
|
|
337
|
+
simulateCloudFailure: () => process.env.VIGTHORIA_SIMULATE_CLOUD_FAILURE === '1',
|
|
338
|
+
debug: (message) => this.logger.debug(message),
|
|
339
|
+
});
|
|
340
|
+
this.codeOperations = new CodeOperationsService((systemPrompt, userPrompt, model, maxTokens) => this.chatComplete(systemPrompt, userPrompt, model, maxTokens));
|
|
341
|
+
this.mcpContextClient = new McpContextClient({
|
|
342
|
+
getBaseUrls: () => this.getMcpBaseUrls(),
|
|
343
|
+
getAccessToken: () => this.getAccessToken(),
|
|
344
|
+
fetch: (input, init) => this.fetch(input, init),
|
|
345
|
+
sanitizeError: sanitizeUserFacingErrorText,
|
|
346
|
+
debug: (message, detail) => this.logger.debug(message, detail),
|
|
347
|
+
});
|
|
348
|
+
this.v3AgentClient = new V3AgentClient({
|
|
349
|
+
getConfiguredApiUrl: () => String(this.config.get('apiUrl') || 'https://coder.vigthoria.io'),
|
|
350
|
+
getAccessToken: () => this.getAccessToken(),
|
|
351
|
+
getServiceKey: () => this.config.get('v3ServiceKey'),
|
|
352
|
+
setServiceKey: (key) => this.config.set('v3ServiceKey', key),
|
|
353
|
+
fetchServiceKey: async (token) => {
|
|
354
|
+
const response = await this.client.get('/api/cli/v3-service-key', {
|
|
355
|
+
headers: { Authorization: `Bearer ${token}` }, timeout: 6000,
|
|
356
|
+
});
|
|
357
|
+
return response.data?.v3ServiceKey || null;
|
|
358
|
+
},
|
|
359
|
+
refreshToken: () => this.refreshToken(),
|
|
360
|
+
fetch: (input, init) => this.fetch(input, init),
|
|
361
|
+
sanitizeError: sanitizeUserFacingErrorText,
|
|
362
|
+
debug: (message) => this.logger.debug(message),
|
|
363
|
+
});
|
|
364
|
+
this.v3WorkspaceService = new V3WorkspaceService({
|
|
365
|
+
resolveTargetPath: (context) => this.resolveAgentTargetPath(context),
|
|
366
|
+
warn: (message) => this.logger.warn(message),
|
|
367
|
+
});
|
|
368
|
+
this.capabilityHealthService = new CapabilityHealthService({
|
|
369
|
+
getCoderHealth: () => this.getCoderHealth(),
|
|
370
|
+
getModelsHealth: () => this.getModelsHealth(),
|
|
371
|
+
getSelfHostedHealth: () => this.getSelfHostedHealth(),
|
|
372
|
+
getV3Health: () => this.getV3AgentHealth(),
|
|
373
|
+
getHyperLoopHealth: () => this.getHyperLoopHealth(),
|
|
374
|
+
getRepoMemoryHealth: (context) => this.getRepoMemoryHealth(context),
|
|
375
|
+
getDevtoolsBridgeHealth: () => this.probeDevtoolsBridgeStatus(),
|
|
376
|
+
modelUsesV3: (requestedModel) => this.modelGovernance.isSelfHostedPreferredModel(this.resolveModelId(requestedModel), requestedModel),
|
|
377
|
+
});
|
|
378
|
+
this.modelTransportService = new ModelTransportService({
|
|
379
|
+
resolveModelId: (model) => this.modelGovernance.resolveModelId(model),
|
|
380
|
+
resolvePermittedModelId: (model) => this.modelGovernance.resolvePermittedModelId(model),
|
|
381
|
+
hasCloudAccess: () => this.config.hasCloudAccess(),
|
|
382
|
+
isAuthenticated: () => this.config.isAuthenticated(),
|
|
383
|
+
isCloudModelId: (model) => this.modelGovernance.isCloudModelId(model),
|
|
384
|
+
shouldSkipCloudRoutes: (model) => this.modelGovernance.shouldSkipCloudRoutes(model),
|
|
385
|
+
isSelfHostedPreferred: (model, requested) => this.modelGovernance.isSelfHostedPreferredModel(model, requested),
|
|
386
|
+
shouldTrySelfHosted: (model, requested) => this.modelGovernance.shouldTrySelfHostedFallback(model, requested),
|
|
387
|
+
getSelfHostedFallback: (model, requested) => this.modelGovernance.getSelfHostedFallbackModelId(model, requested),
|
|
388
|
+
getFallbackModel: (model) => this.modelGovernance.getFallbackModelId(model),
|
|
389
|
+
getMaxTokens: () => this.config.get('preferences').maxTokens,
|
|
390
|
+
hasSelfHostedTransport: () => Boolean(this.selfHostedModelRouterClient),
|
|
391
|
+
postModels: async (body) => (await this.modelRouterClient.post('/v1/chat/completions', body)).data,
|
|
392
|
+
postCoder: async (body) => (await this.client.post('/api/ai/chat', body)).data,
|
|
393
|
+
postCanonicalCoder: async (body) => {
|
|
394
|
+
const endpoint = 'https://coder.vigthoria.io/api/ai/chat';
|
|
395
|
+
const token = this.getAccessToken();
|
|
396
|
+
assertTrustedEndpoint(endpoint, { audience: 'coder', credentialBearing: Boolean(token) });
|
|
397
|
+
return (await axios.post(endpoint, body, {
|
|
398
|
+
timeout: 180000,
|
|
399
|
+
httpsAgent: this._httpsAgent ?? undefined,
|
|
400
|
+
headers: token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {},
|
|
401
|
+
signal: this.withLifecycleSignal(),
|
|
402
|
+
maxRedirects: 0,
|
|
403
|
+
})).data;
|
|
404
|
+
},
|
|
405
|
+
postSelfHosted: async (body) => {
|
|
406
|
+
if (!this.selfHostedModelRouterClient)
|
|
407
|
+
throw new Error('Dedicated model transport is unavailable.');
|
|
408
|
+
return (await this.selfHostedModelRouterClient.post('/v1/chat/completions', body, { headers: { 'x-agent-mode': 'true' } })).data;
|
|
409
|
+
},
|
|
410
|
+
debug: (message) => this.logger.debug(message),
|
|
411
|
+
});
|
|
412
|
+
this.operatorClient = new OperatorClient({
|
|
413
|
+
getBaseUrls: () => this.getOperatorBaseUrls(),
|
|
414
|
+
getAuthToken: () => this.config.get('authToken'),
|
|
415
|
+
fetch: (input, init) => this.fetch(input, init),
|
|
416
|
+
sanitizeError: sanitizeUserFacingErrorText,
|
|
417
|
+
});
|
|
418
|
+
this.frontendPreviewService = new FrontendPreviewService({
|
|
419
|
+
getBaseUrls: () => this.getTemplateServiceBaseUrls(),
|
|
420
|
+
getAccessToken: () => this.getAccessToken(),
|
|
421
|
+
fetch: (input, init) => this.fetch(input, init),
|
|
422
|
+
resolveTargetPath: (context) => this.resolveAgentTargetPath(context),
|
|
423
|
+
extractExpectedFiles: (message, context) => this.extractExpectedWorkspaceFiles(message, context),
|
|
424
|
+
isAnalysisOnlyTask: (message, context) => this.isAnalysisOnlyTask(message, context),
|
|
425
|
+
sanitizeError: sanitizeUserFacingErrorText,
|
|
426
|
+
screenshotAdapter: dependencies.screenshotAdapter || new OptionalPuppeteerScreenshotAdapter(),
|
|
427
|
+
});
|
|
428
|
+
this.vigFlowClient = new VigFlowClient({
|
|
429
|
+
getBaseUrls: () => this.getVigFlowBaseUrls(),
|
|
430
|
+
getAccessToken: () => this.getAccessToken(),
|
|
431
|
+
debug: (message, detail) => this.logger.debug(message, detail),
|
|
432
|
+
});
|
|
433
|
+
this.unsubscribeAuthInvalidation = subscribeAuthInvalidation(() => {
|
|
434
|
+
this.vigFlowClient.clearCredentials();
|
|
435
|
+
});
|
|
436
|
+
installGlobalFetchPolicy();
|
|
323
437
|
// Create HTTPS agent with proper settings for cross-platform compatibility
|
|
324
438
|
const httpsAgent = new https.Agent({
|
|
325
439
|
rejectUnauthorized: true, // Enforce SSL verification
|
|
@@ -358,11 +472,22 @@ export class APIClient {
|
|
|
358
472
|
'User-Agent': `Vigthoria-CLI/${process.env.npm_package_version || '1.6.9'}`,
|
|
359
473
|
},
|
|
360
474
|
}) : null;
|
|
475
|
+
installAxiosNetworkPolicy(this.client, 'coder');
|
|
476
|
+
installAxiosNetworkPolicy(this.modelRouterClient, 'models');
|
|
477
|
+
if (this.selfHostedModelRouterClient)
|
|
478
|
+
installAxiosNetworkPolicy(this.selfHostedModelRouterClient, 'models');
|
|
479
|
+
for (const client of [this.client, this.modelRouterClient, this.selfHostedModelRouterClient].filter(Boolean)) {
|
|
480
|
+
client.interceptors.request.use((request) => {
|
|
481
|
+
request.signal = this.withLifecycleSignal(request.signal);
|
|
482
|
+
return request;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
361
485
|
// Add auth interceptor
|
|
362
486
|
this.client.interceptors.request.use((req) => {
|
|
363
487
|
const token = this.getAccessToken();
|
|
364
488
|
const destination = resolveAxiosRequestUrl(req);
|
|
365
|
-
if (token
|
|
489
|
+
if (token) {
|
|
490
|
+
assertTrustedEndpoint(destination, { audience: 'coder', credentialBearing: true });
|
|
366
491
|
req.headers.Authorization = `Bearer ${token}`;
|
|
367
492
|
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
368
493
|
}
|
|
@@ -371,7 +496,8 @@ export class APIClient {
|
|
|
371
496
|
this.modelRouterClient.interceptors.request.use((req) => {
|
|
372
497
|
const token = this.getAccessToken();
|
|
373
498
|
const destination = resolveAxiosRequestUrl(req);
|
|
374
|
-
if (token
|
|
499
|
+
if (token) {
|
|
500
|
+
assertTrustedEndpoint(destination, { audience: 'models', credentialBearing: true });
|
|
375
501
|
req.headers.Authorization = `Bearer ${token}`;
|
|
376
502
|
req.headers.Cookie = `vigthoria-auth-token=${token}`;
|
|
377
503
|
}
|
|
@@ -380,7 +506,8 @@ export class APIClient {
|
|
|
380
506
|
this.selfHostedModelRouterClient?.interceptors.request.use((req) => {
|
|
381
507
|
const token = this.getAccessToken();
|
|
382
508
|
const destination = resolveAxiosRequestUrl(req);
|
|
383
|
-
if (token
|
|
509
|
+
if (token) {
|
|
510
|
+
assertTrustedEndpoint(destination, { audience: 'models', credentialBearing: true });
|
|
384
511
|
req.headers.Authorization = `Bearer ${token}`;
|
|
385
512
|
}
|
|
386
513
|
return req;
|
|
@@ -411,17 +538,13 @@ export class APIClient {
|
|
|
411
538
|
* on Windows / Node 25+.
|
|
412
539
|
*/
|
|
413
540
|
destroy() {
|
|
541
|
+
this.lifecycleAbortController.abort();
|
|
542
|
+
this.unsubscribeAuthInvalidation?.();
|
|
543
|
+
this.unsubscribeAuthInvalidation = null;
|
|
414
544
|
if (this._httpsAgent) {
|
|
415
545
|
this._httpsAgent.destroy();
|
|
416
546
|
this._httpsAgent = null;
|
|
417
547
|
}
|
|
418
|
-
if (this.ws) {
|
|
419
|
-
try {
|
|
420
|
-
this.ws.close();
|
|
421
|
-
}
|
|
422
|
-
catch { /* ok */ }
|
|
423
|
-
this.ws = null;
|
|
424
|
-
}
|
|
425
548
|
}
|
|
426
549
|
/** Exposed for Balanced 4B agent routing (local inference :8016). */
|
|
427
550
|
getSelfHostedModelsApiUrl() {
|
|
@@ -441,10 +564,8 @@ export class APIClient {
|
|
|
441
564
|
throw new Error(response.data.error || 'Login failed');
|
|
442
565
|
}
|
|
443
566
|
const { token, user } = response.data;
|
|
444
|
-
this.
|
|
445
|
-
|
|
446
|
-
userId: user.id,
|
|
447
|
-
email: user.email,
|
|
567
|
+
await this.authSession.validateAndPersistToken(token, {
|
|
568
|
+
identity: { userId: user.id, email: user.email },
|
|
448
569
|
});
|
|
449
570
|
// Set subscription from user data
|
|
450
571
|
this.config.setSubscription({
|
|
@@ -463,92 +584,16 @@ export class APIClient {
|
|
|
463
584
|
}
|
|
464
585
|
async loginWithToken(token) {
|
|
465
586
|
try {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const headers = {
|
|
469
|
-
Authorization: `Bearer ${token}`,
|
|
470
|
-
Cookie: `vigthoria-auth-token=${token}`,
|
|
471
|
-
};
|
|
472
|
-
const candidateEndpoints = [
|
|
473
|
-
'/api/user/profile',
|
|
474
|
-
'/api/user/info',
|
|
475
|
-
'/api/auth/me',
|
|
476
|
-
];
|
|
477
|
-
for (const endpoint of candidateEndpoints) {
|
|
478
|
-
try {
|
|
479
|
-
const response = await this.client.get(endpoint, { headers });
|
|
480
|
-
const profile = this.extractUserProfile(response.data);
|
|
481
|
-
if (!profile) {
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
this.config.setAuth({
|
|
485
|
-
token,
|
|
486
|
-
userId: profile.id,
|
|
487
|
-
email: profile.email,
|
|
488
|
-
});
|
|
489
|
-
this.config.setSubscription({
|
|
490
|
-
plan: profile.plan,
|
|
491
|
-
status: 'active',
|
|
492
|
-
expiresAt: undefined,
|
|
493
|
-
});
|
|
494
|
-
return true;
|
|
495
|
-
}
|
|
496
|
-
catch (error) {
|
|
497
|
-
const axiosError = error;
|
|
498
|
-
if (axiosError.response?.status && axiosError.response.status !== 404) {
|
|
499
|
-
throw error;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
this.config.clearAuth();
|
|
504
|
-
return false;
|
|
587
|
+
await this.authSession.validateAndPersistToken(token);
|
|
588
|
+
return true;
|
|
505
589
|
}
|
|
506
590
|
catch (error) {
|
|
507
|
-
this.config.clearAuth();
|
|
508
591
|
this.logger.error('Token validation failed:', error.message);
|
|
509
592
|
return false;
|
|
510
593
|
}
|
|
511
594
|
}
|
|
512
|
-
extractUserProfile(data) {
|
|
513
|
-
if (!data) {
|
|
514
|
-
return null;
|
|
515
|
-
}
|
|
516
|
-
const rawUser = data.user || data;
|
|
517
|
-
const userId = rawUser.id;
|
|
518
|
-
const email = rawUser.email;
|
|
519
|
-
if (!userId || !email) {
|
|
520
|
-
return null;
|
|
521
|
-
}
|
|
522
|
-
const plan = data.subscription?.plan
|
|
523
|
-
|| rawUser.subscription?.plan
|
|
524
|
-
|| rawUser.subscription_plan
|
|
525
|
-
|| data.subscription_plan
|
|
526
|
-
|| 'community';
|
|
527
|
-
return {
|
|
528
|
-
id: userId,
|
|
529
|
-
email,
|
|
530
|
-
plan,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
595
|
async refreshToken() {
|
|
534
|
-
|
|
535
|
-
if (!refreshToken)
|
|
536
|
-
return false;
|
|
537
|
-
try {
|
|
538
|
-
const response = await this.client.post('/api/token/refresh', {
|
|
539
|
-
refresh_token: refreshToken
|
|
540
|
-
});
|
|
541
|
-
const { token, access_token, refresh_token } = response.data;
|
|
542
|
-
this.config.set('authToken', token || access_token);
|
|
543
|
-
if (refresh_token) {
|
|
544
|
-
this.config.set('refreshToken', refresh_token);
|
|
545
|
-
}
|
|
546
|
-
return true;
|
|
547
|
-
}
|
|
548
|
-
catch {
|
|
549
|
-
this.config.clearAuth();
|
|
550
|
-
return false;
|
|
551
|
-
}
|
|
596
|
+
return this.authSession.refresh();
|
|
552
597
|
}
|
|
553
598
|
async getSubscriptionStatus() {
|
|
554
599
|
try {
|
|
@@ -565,10 +610,7 @@ export class APIClient {
|
|
|
565
610
|
}
|
|
566
611
|
}
|
|
567
612
|
getAccessToken() {
|
|
568
|
-
return
|
|
569
|
-
|| process.env.VIGTHORIA_AUTH_TOKEN
|
|
570
|
-
|| this.config.get('authToken')
|
|
571
|
-
|| null;
|
|
613
|
+
return this.authSession.getAccessToken();
|
|
572
614
|
}
|
|
573
615
|
/**
|
|
574
616
|
* Validate the current auth token against the Coder API.
|
|
@@ -648,62 +690,22 @@ export class APIClient {
|
|
|
648
690
|
return { valid: true };
|
|
649
691
|
}
|
|
650
692
|
isDirectV3AgentBaseUrl(baseUrl) {
|
|
651
|
-
|
|
652
|
-
const parsed = new URL(String(baseUrl));
|
|
653
|
-
return parsed.port === '8030' || parsed.port.endsWith('8030');
|
|
654
|
-
}
|
|
655
|
-
catch {
|
|
656
|
-
return /(?:127\.0\.0\.1|localhost|172\.19\.0\.1|host\.lan):(?:\d*8030|8030)/.test(String(baseUrl));
|
|
657
|
-
}
|
|
693
|
+
return this.v3AgentClient.isDirectBaseUrl(baseUrl);
|
|
658
694
|
}
|
|
659
695
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
660
|
-
|
|
661
|
-
const localTestMode = isLocalTestfarmMode();
|
|
662
|
-
const explicitV3EndpointConfigured = Boolean(process.env.VIGTHORIA_V3_AGENT_URL || process.env.V3_AGENT_URL);
|
|
663
|
-
const includeLoopbackV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1'
|
|
664
|
-
|| (preferLocal && isServerRuntime());
|
|
665
|
-
const localCandidates = [
|
|
666
|
-
process.env.VIGTHORIA_V3_AGENT_URL,
|
|
667
|
-
process.env.V3_AGENT_URL,
|
|
668
|
-
...(includeLoopbackV3Agent ? ['http://127.0.0.1:8030'] : []),
|
|
669
|
-
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
670
|
-
const remoteCandidates = localTestMode
|
|
671
|
-
? [configuredApiUrl]
|
|
672
|
-
: [configuredApiUrl, 'https://coder.vigthoria.io'];
|
|
673
|
-
const normalizedRemote = remoteCandidates
|
|
674
|
-
.filter(Boolean)
|
|
675
|
-
.map((url) => String(url).replace(/\/$/, ''));
|
|
676
|
-
// An explicit endpoint is an operator routing decision. External installs
|
|
677
|
-
// have none and continue to use the public Coder gateway first.
|
|
678
|
-
const urls = (preferLocal || localTestMode || explicitV3EndpointConfigured) && localCandidates.length > 0
|
|
679
|
-
? [...localCandidates, ...normalizedRemote]
|
|
680
|
-
: [...normalizedRemote, ...localCandidates];
|
|
681
|
-
return [...new Set(urls)];
|
|
696
|
+
return this.v3AgentClient.getBaseUrls(preferLocal);
|
|
682
697
|
}
|
|
683
698
|
getV3AgentRunUrl(baseUrl) {
|
|
684
|
-
|
|
685
|
-
return `${baseUrl}/api/agent/run`;
|
|
686
|
-
}
|
|
687
|
-
return `${baseUrl}/api/v3-agent/run`;
|
|
699
|
+
return this.v3AgentClient.runUrl(baseUrl);
|
|
688
700
|
}
|
|
689
701
|
getV3AgentContinueUrl(baseUrl) {
|
|
690
|
-
|
|
691
|
-
return `${baseUrl}/api/agent/continue`;
|
|
692
|
-
}
|
|
693
|
-
return `${baseUrl}/api/v3-agent/continue`;
|
|
702
|
+
return this.v3AgentClient.continueUrl(baseUrl);
|
|
694
703
|
}
|
|
695
704
|
getV3AgentBackgroundUrl(baseUrl, suffix = '') {
|
|
696
|
-
|
|
697
|
-
if (this.isDirectV3AgentBaseUrl(baseUrl)) {
|
|
698
|
-
return `${baseUrl}/api/agent/background${cleanSuffix}`;
|
|
699
|
-
}
|
|
700
|
-
return `${baseUrl}/api/v3-agent/background${cleanSuffix}`;
|
|
705
|
+
return this.v3AgentClient.backgroundUrl(baseUrl, suffix);
|
|
701
706
|
}
|
|
702
707
|
getV3AgentApprovalUrl(baseUrl) {
|
|
703
|
-
|
|
704
|
-
return `${baseUrl}/api/agent/approval`;
|
|
705
|
-
}
|
|
706
|
-
return `${baseUrl}/api/v3-agent/approval`;
|
|
708
|
+
return this.v3AgentClient.approvalUrl(baseUrl);
|
|
707
709
|
}
|
|
708
710
|
getOperatorBaseUrls() {
|
|
709
711
|
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
@@ -716,9 +718,6 @@ export class APIClient {
|
|
|
716
718
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
717
719
|
return [...new Set(urls)];
|
|
718
720
|
}
|
|
719
|
-
getOperatorStreamUrl(baseUrl) {
|
|
720
|
-
return `${baseUrl}/api/operator/stream`;
|
|
721
|
-
}
|
|
722
721
|
getMcpBaseUrls() {
|
|
723
722
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
724
723
|
const localTestMode = isLocalTestfarmMode();
|
|
@@ -764,22 +763,6 @@ export class APIClient {
|
|
|
764
763
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
765
764
|
return [...new Set(urls)];
|
|
766
765
|
}
|
|
767
|
-
isFrontendTask(message = '', context = {}) {
|
|
768
|
-
// Never treat analysis-only tasks as frontend tasks — preview gate
|
|
769
|
-
// should not fire for read-only inspection prompts.
|
|
770
|
-
if (this.isAnalysisOnlyTask(message, context)) {
|
|
771
|
-
return false;
|
|
772
|
-
}
|
|
773
|
-
const prompt = String(message || '');
|
|
774
|
-
const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
|
|
775
|
-
const hasHtmlEntry = expectedFiles.some((filePath) => /\.html?$/i.test(filePath));
|
|
776
|
-
return /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|hero|responsive)/i.test(prompt)
|
|
777
|
-
|| hasHtmlEntry;
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Returns true when the prompt describes a read-only / analysis task.
|
|
781
|
-
* Used to suppress preview gate and other write-oriented side effects.
|
|
782
|
-
*/
|
|
783
766
|
isAnalysisOnlyTask(message = '', context = {}) {
|
|
784
767
|
const prompt = String(message || '').toLowerCase();
|
|
785
768
|
const taskType = String(context.agentTaskType || '').toLowerCase();
|
|
@@ -789,797 +772,51 @@ export class APIClient {
|
|
|
789
772
|
return true;
|
|
790
773
|
return /^(what|which|how many|list|show|check|inspect|analyze|analyse|audit|explain|describe|summarize|summarise|review|overview|find|count|read|look at|tell me|diagnos)/i.test(prompt.trim());
|
|
791
774
|
}
|
|
775
|
+
runTemplateServicePreviewGate(message = '', context = {}) {
|
|
776
|
+
return this.frontendPreviewService.runTemplateServicePreviewGate(message, context);
|
|
777
|
+
}
|
|
792
778
|
normalizeWorkspaceRelativePath(filePath) {
|
|
793
|
-
return
|
|
779
|
+
return this.frontendPreviewService.normalizeWorkspaceRelativePath(filePath);
|
|
794
780
|
}
|
|
795
781
|
collectAgentStateSyncPaths(rootPath) {
|
|
796
|
-
|
|
797
|
-
const progressPath = path.join(rootPath, '.vigthoria', 'agent-state', 'progress.json');
|
|
798
|
-
if (!fs.existsSync(progressPath)) {
|
|
799
|
-
return paths;
|
|
800
|
-
}
|
|
801
|
-
paths.push('.vigthoria/agent-state/progress.json');
|
|
802
|
-
const tasksDir = path.join(rootPath, '.vigthoria', 'agent-state', 'tasks');
|
|
803
|
-
if (!fs.existsSync(tasksDir)) {
|
|
804
|
-
return paths;
|
|
805
|
-
}
|
|
806
|
-
try {
|
|
807
|
-
for (const entry of fs.readdirSync(tasksDir, { withFileTypes: true })) {
|
|
808
|
-
if (!entry.isFile() || !entry.name.endsWith('.json')) {
|
|
809
|
-
continue;
|
|
810
|
-
}
|
|
811
|
-
paths.push(`.vigthoria/agent-state/tasks/${entry.name}`);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
catch {
|
|
815
|
-
// Ignore unreadable task snapshots.
|
|
816
|
-
}
|
|
817
|
-
return paths;
|
|
818
|
-
}
|
|
819
|
-
listFrontendWorkspaceFiles(rootPath) {
|
|
820
|
-
const results = [];
|
|
821
|
-
const stack = [rootPath];
|
|
822
|
-
while (stack.length > 0) {
|
|
823
|
-
const current = stack.pop();
|
|
824
|
-
if (!current) {
|
|
825
|
-
continue;
|
|
826
|
-
}
|
|
827
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
828
|
-
if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.vigthoria') {
|
|
829
|
-
continue;
|
|
830
|
-
}
|
|
831
|
-
const fullPath = path.join(current, entry.name);
|
|
832
|
-
if (entry.isDirectory()) {
|
|
833
|
-
stack.push(fullPath);
|
|
834
|
-
continue;
|
|
835
|
-
}
|
|
836
|
-
if (!entry.isFile()) {
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
const relativePath = this.normalizeWorkspaceRelativePath(path.relative(rootPath, fullPath));
|
|
840
|
-
if (relativePath) {
|
|
841
|
-
results.push(relativePath);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
results.sort((left, right) => left.localeCompare(right));
|
|
846
|
-
return results;
|
|
847
|
-
}
|
|
848
|
-
chooseFrontendPreviewEntry(rootPath, htmlPaths, expectedFiles = []) {
|
|
849
|
-
const normalizedExpected = expectedFiles
|
|
850
|
-
.map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
|
|
851
|
-
.filter((filePath) => /\.html?$/i.test(filePath));
|
|
852
|
-
for (const expectedPath of normalizedExpected) {
|
|
853
|
-
if (htmlPaths.includes(expectedPath)) {
|
|
854
|
-
return expectedPath;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
const scored = htmlPaths.map((relativePath) => {
|
|
858
|
-
const baseName = path.posix.basename(relativePath).toLowerCase();
|
|
859
|
-
const depth = relativePath.split('/').length - 1;
|
|
860
|
-
let score = 0;
|
|
861
|
-
const normalizedPath = relativePath.toLowerCase();
|
|
862
|
-
const topLevelDir = normalizedPath.split('/')[0];
|
|
863
|
-
if (['dist', 'build', 'out'].includes(topLevelDir)) {
|
|
864
|
-
score += 320;
|
|
865
|
-
}
|
|
866
|
-
else if (topLevelDir === 'public') {
|
|
867
|
-
score += 180;
|
|
868
|
-
}
|
|
869
|
-
if (normalizedPath === 'index.html') {
|
|
870
|
-
score += 200;
|
|
871
|
-
}
|
|
872
|
-
if (baseName === 'index.html') {
|
|
873
|
-
score += 120;
|
|
874
|
-
}
|
|
875
|
-
if (/(landing|home|main|dashboard|app)/i.test(baseName)) {
|
|
876
|
-
score += 60;
|
|
877
|
-
}
|
|
878
|
-
if (depth === 0) {
|
|
879
|
-
score += 40;
|
|
880
|
-
}
|
|
881
|
-
return { relativePath, score, depth };
|
|
882
|
-
}).sort((left, right) => right.score - left.score || left.depth - right.depth || left.relativePath.localeCompare(right.relativePath));
|
|
883
|
-
return scored[0]?.relativePath || null;
|
|
782
|
+
return this.frontendPreviewService.collectAgentStateSyncPaths(rootPath);
|
|
884
783
|
}
|
|
885
784
|
extractLinkedFrontendAssets(html, entryPath) {
|
|
886
|
-
|
|
887
|
-
const js = new Set();
|
|
888
|
-
const assets = new Set();
|
|
889
|
-
const entryDir = path.posix.dirname(this.normalizeWorkspaceRelativePath(entryPath) || '.');
|
|
890
|
-
const normalizeAsset = (assetPath) => {
|
|
891
|
-
const clean = String(assetPath || '').trim();
|
|
892
|
-
if (!clean || clean.startsWith('http://') || clean.startsWith('https://') || clean.startsWith('//') || clean.startsWith('data:') || clean.startsWith('#')) {
|
|
893
|
-
return null;
|
|
894
|
-
}
|
|
895
|
-
const withoutQuery = clean.split('?')[0].split('#')[0];
|
|
896
|
-
if (!withoutQuery) {
|
|
897
|
-
return null;
|
|
898
|
-
}
|
|
899
|
-
if (withoutQuery.startsWith('/')) {
|
|
900
|
-
return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery.slice(1))));
|
|
901
|
-
}
|
|
902
|
-
return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery)));
|
|
903
|
-
};
|
|
904
|
-
const cssPattern = /<link[^>]+href=["']([^"']+\.css(?:[?#][^"']*)?)["'][^>]*>/gi;
|
|
905
|
-
const jsPattern = /<script[^>]+src=["']([^"']+\.(?:js|mjs|cjs)(?:[?#][^"']*)?)["'][^>]*>/gi;
|
|
906
|
-
let match;
|
|
907
|
-
while ((match = cssPattern.exec(String(html || ''))) !== null) {
|
|
908
|
-
const resolved = normalizeAsset(match[1]);
|
|
909
|
-
if (resolved) {
|
|
910
|
-
css.add(resolved);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
while ((match = jsPattern.exec(String(html || ''))) !== null) {
|
|
914
|
-
const resolved = normalizeAsset(match[1]);
|
|
915
|
-
if (resolved) {
|
|
916
|
-
js.add(resolved);
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
const assetPattern = /<(?:link|img|source|video|audio)\b[^>]+(?:href|src)=["']([^"']+)["'][^>]*>/gi;
|
|
920
|
-
while ((match = assetPattern.exec(String(html || ''))) !== null) {
|
|
921
|
-
const resolved = normalizeAsset(match[1]);
|
|
922
|
-
if (resolved)
|
|
923
|
-
assets.add(resolved);
|
|
924
|
-
}
|
|
925
|
-
return {
|
|
926
|
-
css: Array.from(css),
|
|
927
|
-
js: Array.from(js),
|
|
928
|
-
assets: Array.from(assets),
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
async buildFrontendForPreview(rootPath) {
|
|
932
|
-
const packagePath = path.join(rootPath, 'package.json');
|
|
933
|
-
if (!fs.existsSync(packagePath)) {
|
|
934
|
-
return { attempted: false, succeeded: false };
|
|
935
|
-
}
|
|
936
|
-
try {
|
|
937
|
-
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
938
|
-
if (!packageJson.scripts?.build) {
|
|
939
|
-
return { attempted: false, succeeded: false };
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
catch (error) {
|
|
943
|
-
return { attempted: false, succeeded: false, error: `Invalid package.json: ${error?.message || String(error)}` };
|
|
944
|
-
}
|
|
945
|
-
const npmCommand = process.platform === 'win32' ? (process.env.ComSpec || 'cmd.exe') : 'npm';
|
|
946
|
-
const npmArgs = process.platform === 'win32'
|
|
947
|
-
? ['/d', '/s', '/c', 'npm run build']
|
|
948
|
-
: ['run', 'build'];
|
|
949
|
-
return await new Promise((resolve) => {
|
|
950
|
-
execFile(npmCommand, npmArgs, { cwd: rootPath, timeout: 150_000, windowsHide: true, maxBuffer: 2 * 1024 * 1024 }, (error) => {
|
|
951
|
-
if (error) {
|
|
952
|
-
resolve({ attempted: true, succeeded: false, error: sanitizeUserFacingErrorText(error.message).slice(0, 300) });
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
resolve({ attempted: true, succeeded: true });
|
|
956
|
-
});
|
|
957
|
-
});
|
|
958
|
-
}
|
|
959
|
-
gatherFrontendPreviewArtifacts(message = '', context = {}) {
|
|
960
|
-
const rootPath = this.resolveAgentTargetPath(context);
|
|
961
|
-
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
962
|
-
return { error: 'Frontend preview gate requires a readable project workspace.' };
|
|
963
|
-
}
|
|
964
|
-
const workspaceFiles = this.listFrontendWorkspaceFiles(rootPath);
|
|
965
|
-
const expectedFiles = this.extractExpectedWorkspaceFiles(message, context);
|
|
966
|
-
const htmlPaths = workspaceFiles.filter((filePath) => /\.html?$/i.test(filePath));
|
|
967
|
-
if (htmlPaths.length === 0) {
|
|
968
|
-
return { error: 'Frontend preview gate requires at least one HTML entry file in the workspace.' };
|
|
969
|
-
}
|
|
970
|
-
const htmlPath = this.chooseFrontendPreviewEntry(rootPath, htmlPaths, expectedFiles);
|
|
971
|
-
if (!htmlPath) {
|
|
972
|
-
return { error: 'Frontend preview gate could not determine a primary HTML entry file.' };
|
|
973
|
-
}
|
|
974
|
-
const html = fs.readFileSync(path.join(rootPath, htmlPath), 'utf8');
|
|
975
|
-
const linkedAssets = this.extractLinkedFrontendAssets(html, htmlPath);
|
|
976
|
-
const cssFiles = workspaceFiles.filter((filePath) => /\.css$/i.test(filePath));
|
|
977
|
-
const jsFiles = workspaceFiles.filter((filePath) => /\.(?:js|mjs|cjs)$/i.test(filePath));
|
|
978
|
-
const expectedCss = expectedFiles
|
|
979
|
-
.map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
|
|
980
|
-
.filter((filePath) => /\.css$/i.test(filePath));
|
|
981
|
-
const expectedJs = expectedFiles
|
|
982
|
-
.map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
|
|
983
|
-
.filter((filePath) => /\.(?:js|mjs|cjs)$/i.test(filePath));
|
|
984
|
-
const chooseExisting = (candidates, fallback) => {
|
|
985
|
-
const selected = new Set();
|
|
986
|
-
for (const candidate of [...candidates, ...fallback]) {
|
|
987
|
-
const normalized = this.normalizeWorkspaceRelativePath(candidate);
|
|
988
|
-
if (!normalized || selected.has(normalized)) {
|
|
989
|
-
continue;
|
|
990
|
-
}
|
|
991
|
-
if (!fs.existsSync(path.join(rootPath, normalized))) {
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
selected.add(normalized);
|
|
995
|
-
}
|
|
996
|
-
return Array.from(selected);
|
|
997
|
-
};
|
|
998
|
-
const selectedCssFiles = chooseExisting(linkedAssets.css, [...expectedCss, ...cssFiles]).slice(0, 12);
|
|
999
|
-
const selectedJsFiles = chooseExisting(linkedAssets.js, [...expectedJs, ...jsFiles]).slice(0, 12);
|
|
1000
|
-
const css = selectedCssFiles.map((filePath) => fs.readFileSync(path.join(rootPath, filePath), 'utf8')).join('\n\n');
|
|
1001
|
-
const js = selectedJsFiles.map((filePath) => fs.readFileSync(path.join(rootPath, filePath), 'utf8')).join('\n\n');
|
|
1002
|
-
return { htmlPath, html, css, js, cssPaths: selectedCssFiles, jsPaths: selectedJsFiles };
|
|
1003
|
-
}
|
|
1004
|
-
async captureFrontendPreviewScreenshot(entryAbsolutePath, screenshotPath) {
|
|
1005
|
-
try {
|
|
1006
|
-
const puppeteerModule = await import('puppeteer').catch(() => null);
|
|
1007
|
-
const puppeteer = puppeteerModule?.default || puppeteerModule;
|
|
1008
|
-
if (!puppeteer || typeof puppeteer.launch !== 'function') {
|
|
1009
|
-
return { captured: false, error: 'puppeteer-unavailable' };
|
|
1010
|
-
}
|
|
1011
|
-
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
|
|
1012
|
-
try {
|
|
1013
|
-
const page = await browser.newPage();
|
|
1014
|
-
await page.setViewport({ width: 1440, height: 960, deviceScaleFactor: 1 });
|
|
1015
|
-
await page.goto(`file://${entryAbsolutePath}`, { waitUntil: 'networkidle0', timeout: 20000 });
|
|
1016
|
-
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
1017
|
-
}
|
|
1018
|
-
finally {
|
|
1019
|
-
await browser.close();
|
|
1020
|
-
}
|
|
1021
|
-
return { captured: true };
|
|
1022
|
-
}
|
|
1023
|
-
catch (error) {
|
|
1024
|
-
return { captured: false, error: error?.message || String(error) };
|
|
1025
|
-
}
|
|
785
|
+
return this.frontendPreviewService.extractLinkedFrontendAssets(html, entryPath);
|
|
1026
786
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const cssPaths = Array.isArray(artifacts.cssPaths) ? artifacts.cssPaths : [];
|
|
1030
|
-
const html = String(artifacts.html || '');
|
|
1031
|
-
const css = String(artifacts.css || '');
|
|
1032
|
-
const js = String(artifacts.js || '');
|
|
1033
|
-
if (!summary.hasViewportMeta) {
|
|
1034
|
-
reasons.push('missing viewport metadata');
|
|
1035
|
-
}
|
|
1036
|
-
const hasStyleEvidence = cssPaths.length > 0 || /<style[\s>]/i.test(html) || css.trim().length > 0;
|
|
1037
|
-
if (!hasStyleEvidence) {
|
|
1038
|
-
reasons.push('missing stylesheet or inline style evidence');
|
|
1039
|
-
}
|
|
1040
|
-
const hasInteractionOrResponsiveEvidence = Boolean(summary.hasResponsiveSignals) || Boolean(summary.hasInteractiveSignals) || /<script[\s>]/i.test(html) || js.trim().length > 0;
|
|
1041
|
-
if (!hasInteractionOrResponsiveEvidence) {
|
|
1042
|
-
reasons.push('missing responsive or interactive evidence');
|
|
1043
|
-
}
|
|
1044
|
-
return {
|
|
1045
|
-
ok: reasons.length === 0,
|
|
1046
|
-
reasons,
|
|
1047
|
-
};
|
|
787
|
+
listVigFlowTemplates(options = {}) {
|
|
788
|
+
return this.vigFlowClient.listTemplates(options);
|
|
1048
789
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
return previewGate;
|
|
1052
|
-
}
|
|
1053
|
-
try {
|
|
1054
|
-
const artifactsDir = path.join(rootPath, '.vigthoria', 'proof', 'preview');
|
|
1055
|
-
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
1056
|
-
const contextId = String(context.contextId || 'preview').trim() || 'preview';
|
|
1057
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1058
|
-
const baseName = `${timestamp}-${contextId}`;
|
|
1059
|
-
const manifestPath = path.join(artifactsDir, `${baseName}.json`);
|
|
1060
|
-
const screenshotPath = path.join(artifactsDir, `${baseName}.png`);
|
|
1061
|
-
const entryAbsolutePath = path.join(rootPath, previewGate.entryPath);
|
|
1062
|
-
const previewFileUrl = `file://${entryAbsolutePath}`;
|
|
1063
|
-
const screenshot = await this.captureFrontendPreviewScreenshot(entryAbsolutePath, screenshotPath);
|
|
1064
|
-
const manifest = {
|
|
1065
|
-
schemaVersion: 1,
|
|
1066
|
-
createdAt: new Date().toISOString(),
|
|
1067
|
-
contextId: context.contextId || null,
|
|
1068
|
-
workspaceRoot: rootPath,
|
|
1069
|
-
entryPath: previewGate.entryPath,
|
|
1070
|
-
previewFileUrl,
|
|
1071
|
-
previewCommand: `vigthoria preview -p ${rootPath}`,
|
|
1072
|
-
previewGate,
|
|
1073
|
-
validationArtifacts: {
|
|
1074
|
-
summary: previewGate.summary || {},
|
|
1075
|
-
modes: previewGate.modes || {},
|
|
1076
|
-
backendUrl: previewGate.backendUrl || null,
|
|
1077
|
-
processingTimeMs: previewGate.processingTimeMs || null,
|
|
1078
|
-
assetPaths: previewGate.assetPaths || { css: [], js: [] },
|
|
1079
|
-
},
|
|
1080
|
-
screenshot: {
|
|
1081
|
-
path: screenshot.captured ? screenshotPath : null,
|
|
1082
|
-
captured: screenshot.captured,
|
|
1083
|
-
error: screenshot.error || null,
|
|
1084
|
-
},
|
|
1085
|
-
};
|
|
1086
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
1087
|
-
return {
|
|
1088
|
-
...previewGate,
|
|
1089
|
-
artifacts: {
|
|
1090
|
-
manifestPath,
|
|
1091
|
-
screenshotPath: screenshot.captured ? screenshotPath : undefined,
|
|
1092
|
-
previewFileUrl,
|
|
1093
|
-
screenshotCaptured: screenshot.captured,
|
|
1094
|
-
screenshotError: screenshot.error,
|
|
1095
|
-
},
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
catch (error) {
|
|
1099
|
-
return {
|
|
1100
|
-
...previewGate,
|
|
1101
|
-
artifacts: {
|
|
1102
|
-
...(previewGate.artifacts || {}),
|
|
1103
|
-
screenshotCaptured: false,
|
|
1104
|
-
screenshotError: error?.message || String(error),
|
|
1105
|
-
},
|
|
1106
|
-
};
|
|
1107
|
-
}
|
|
790
|
+
listVigFlowWorkflows() {
|
|
791
|
+
return this.vigFlowClient.listWorkflows();
|
|
1108
792
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
return { required: false, passed: true };
|
|
1112
|
-
}
|
|
1113
|
-
const rootPath = this.resolveAgentTargetPath(context);
|
|
1114
|
-
let artifacts = this.gatherFrontendPreviewArtifacts(message, context);
|
|
1115
|
-
let buildResult = null;
|
|
1116
|
-
if (rootPath && artifacts.error?.includes('at least one HTML entry file')) {
|
|
1117
|
-
buildResult = await this.buildFrontendForPreview(rootPath);
|
|
1118
|
-
if (buildResult.attempted) {
|
|
1119
|
-
artifacts = this.gatherFrontendPreviewArtifacts(message, context);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
if (artifacts.error || !artifacts.htmlPath || typeof artifacts.html !== 'string') {
|
|
1123
|
-
const buildDetail = buildResult?.attempted
|
|
1124
|
-
? ` Build retry ${buildResult.succeeded ? 'completed but produced no HTML entry' : `failed: ${buildResult.error || 'unknown build error'}`}.`
|
|
1125
|
-
: '';
|
|
1126
|
-
return {
|
|
1127
|
-
required: true,
|
|
1128
|
-
passed: false,
|
|
1129
|
-
error: `${artifacts.error || 'Frontend preview gate could not collect frontend artifacts.'}${buildDetail}`,
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
// Cap artifact sizes to prevent 413 Payload Too Large
|
|
1133
|
-
const PREVIEW_MAX_ARTIFACT_BYTES = 250 * 1024;
|
|
1134
|
-
const html = artifacts.html.slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
|
|
1135
|
-
const css = (artifacts.css || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
|
|
1136
|
-
const js = (artifacts.js || '').slice(0, PREVIEW_MAX_ARTIFACT_BYTES);
|
|
1137
|
-
const errors = [];
|
|
1138
|
-
for (const baseUrl of this.getTemplateServiceBaseUrls()) {
|
|
1139
|
-
try {
|
|
1140
|
-
const proofPayload = JSON.stringify({
|
|
1141
|
-
vision: String(context.rawPrompt || message || '').slice(0, 2000),
|
|
1142
|
-
html,
|
|
1143
|
-
css,
|
|
1144
|
-
js,
|
|
1145
|
-
entryPath: artifacts.htmlPath,
|
|
1146
|
-
workspaceName: path.basename(rootPath),
|
|
1147
|
-
});
|
|
1148
|
-
// Skip preview proof if payload is still too large (> 4 MB)
|
|
1149
|
-
if (Buffer.byteLength(proofPayload, 'utf8') > 4 * 1024 * 1024) {
|
|
1150
|
-
return {
|
|
1151
|
-
required: true,
|
|
1152
|
-
passed: true,
|
|
1153
|
-
error: 'Preview proof skipped: payload exceeds size limit.',
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
const internalServiceKey = process.env.VIGTHORIA_TEMPLATE_SERVICE_KEY
|
|
1157
|
-
|| process.env.V3_SERVICE_KEY;
|
|
1158
|
-
const authToken = this.getAccessToken();
|
|
1159
|
-
const response = await fetch(`${baseUrl}/preview-proof`, {
|
|
1160
|
-
method: 'POST',
|
|
1161
|
-
headers: {
|
|
1162
|
-
'Content-Type': 'application/json',
|
|
1163
|
-
Accept: 'application/json',
|
|
1164
|
-
...(authToken ? {
|
|
1165
|
-
Authorization: `Bearer ${authToken}`,
|
|
1166
|
-
Cookie: `vigthoria-auth-token=${authToken}`,
|
|
1167
|
-
} : {}),
|
|
1168
|
-
...(internalServiceKey ? { 'x-vigthoria-service-key': internalServiceKey } : {}),
|
|
1169
|
-
},
|
|
1170
|
-
body: proofPayload,
|
|
1171
|
-
});
|
|
1172
|
-
if (!response.ok) {
|
|
1173
|
-
const errorText = await response.text().catch(() => '');
|
|
1174
|
-
throw new Error(`Template preview proof ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
1175
|
-
}
|
|
1176
|
-
const payload = await response.json();
|
|
1177
|
-
const modes = payload?.modes || {};
|
|
1178
|
-
const summary = payload?.summary || {};
|
|
1179
|
-
const visualProof = this.evaluateFrontendVisualProof(summary, artifacts);
|
|
1180
|
-
const passed = payload?.success === true
|
|
1181
|
-
&& modes?.design?.ready === true
|
|
1182
|
-
&& modes?.live?.ready === true
|
|
1183
|
-
&& modes?.production?.ready === true
|
|
1184
|
-
&& visualProof.ok;
|
|
1185
|
-
const previewGate = {
|
|
1186
|
-
required: true,
|
|
1187
|
-
passed,
|
|
1188
|
-
backendUrl: baseUrl,
|
|
1189
|
-
entryPath: artifacts.htmlPath,
|
|
1190
|
-
assetPaths: {
|
|
1191
|
-
css: artifacts.cssPaths || [],
|
|
1192
|
-
js: artifacts.jsPaths || [],
|
|
1193
|
-
},
|
|
1194
|
-
modes,
|
|
1195
|
-
summary,
|
|
1196
|
-
processingTimeMs: Number(payload?.processing_time || 0) || undefined,
|
|
1197
|
-
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(', ')}.`),
|
|
1198
|
-
};
|
|
1199
|
-
return await this.persistFrontendPreviewArtifacts(previewGate, rootPath, context);
|
|
1200
|
-
}
|
|
1201
|
-
catch (error) {
|
|
1202
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
return await this.persistFrontendPreviewArtifacts({
|
|
1206
|
-
required: true,
|
|
1207
|
-
passed: false,
|
|
1208
|
-
entryPath: artifacts.htmlPath,
|
|
1209
|
-
assetPaths: {
|
|
1210
|
-
css: artifacts.cssPaths || [],
|
|
1211
|
-
js: artifacts.jsPaths || [],
|
|
1212
|
-
},
|
|
1213
|
-
error: errors.join(' | ') || 'No Template Service preview endpoint was reachable.',
|
|
1214
|
-
}, rootPath, context);
|
|
793
|
+
resolveVigFlowWorkflow(selector) {
|
|
794
|
+
return this.vigFlowClient.resolveWorkflow(selector);
|
|
1215
795
|
}
|
|
1216
|
-
|
|
1217
|
-
return
|
|
796
|
+
useVigFlowTemplate(templateId, options = {}) {
|
|
797
|
+
return this.vigFlowClient.useTemplate(templateId, options);
|
|
1218
798
|
}
|
|
1219
|
-
|
|
1220
|
-
return
|
|
799
|
+
runVigFlowWorkflow(workflowId, options = {}) {
|
|
800
|
+
return this.vigFlowClient.runWorkflow(workflowId, options);
|
|
1221
801
|
}
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
'Content-Type': 'application/json',
|
|
1225
|
-
Accept: 'application/json',
|
|
1226
|
-
};
|
|
1227
|
-
const authToken = this.getAccessToken();
|
|
1228
|
-
if (authToken) {
|
|
1229
|
-
headers.Authorization = `Bearer ${authToken}`;
|
|
1230
|
-
headers.Cookie = `vigthoria-auth-token=${authToken}`;
|
|
1231
|
-
headers['X-Vigthoria-Token'] = authToken;
|
|
1232
|
-
headers['X-Auth-Token'] = authToken;
|
|
1233
|
-
}
|
|
1234
|
-
return headers;
|
|
802
|
+
getVigFlowExecutionStatus(executionId) {
|
|
803
|
+
return this.vigFlowClient.executionStatus(executionId);
|
|
1235
804
|
}
|
|
1236
805
|
async getV3AgentHeaders() {
|
|
1237
|
-
|
|
1238
|
-
'Content-Type': 'application/json',
|
|
1239
|
-
Accept: 'text/event-stream',
|
|
1240
|
-
};
|
|
1241
|
-
const authToken = this.getAccessToken();
|
|
1242
|
-
if (authToken) {
|
|
1243
|
-
headers.Authorization = `Bearer ${authToken}`;
|
|
1244
|
-
headers.Cookie = `vigthoria-auth-token=${authToken}`;
|
|
1245
|
-
}
|
|
1246
|
-
const serviceKey = process.env.VIGTHORIA_V3_SERVICE_KEY
|
|
1247
|
-
|| process.env.V3_SERVICE_KEY
|
|
1248
|
-
|| process.env.HYPERLOOP_SERVICE_KEY
|
|
1249
|
-
|| this.config.get('v3ServiceKey');
|
|
1250
|
-
if (serviceKey) {
|
|
1251
|
-
headers['X-Service-Key'] = serviceKey;
|
|
1252
|
-
}
|
|
1253
|
-
return headers;
|
|
806
|
+
return this.v3AgentClient.headers();
|
|
1254
807
|
}
|
|
1255
|
-
/**
|
|
1256
|
-
* Ensure the V3 service key is available in config.
|
|
1257
|
-
* If not already set via env or stored config, fetches it from the Coder API
|
|
1258
|
-
* using the current user's auth token and caches it for this session and beyond.
|
|
1259
|
-
*/
|
|
1260
808
|
async ensureV3ServiceKey() {
|
|
1261
|
-
|
|
1262
|
-
|| process.env.V3_SERVICE_KEY
|
|
1263
|
-
|| process.env.HYPERLOOP_SERVICE_KEY
|
|
1264
|
-
|| this.config.get('v3ServiceKey');
|
|
1265
|
-
if (existing)
|
|
1266
|
-
return;
|
|
1267
|
-
try {
|
|
1268
|
-
const token = this.getAccessToken();
|
|
1269
|
-
if (!token)
|
|
1270
|
-
return;
|
|
1271
|
-
const response = await this.client.get('/api/cli/v3-service-key', {
|
|
1272
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
1273
|
-
timeout: 6000,
|
|
1274
|
-
});
|
|
1275
|
-
const key = response.data?.v3ServiceKey;
|
|
1276
|
-
if (key) {
|
|
1277
|
-
this.config.set('v3ServiceKey', key);
|
|
1278
|
-
this.logger.debug('V3 service key retrieved and cached from Coder API.');
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
catch {
|
|
1282
|
-
// Non-fatal: fall through; the health check will surface the auth error.
|
|
1283
|
-
}
|
|
809
|
+
return this.v3AgentClient.ensureServiceKey();
|
|
1284
810
|
}
|
|
1285
811
|
async runV3HealthCheck() {
|
|
1286
|
-
|
|
1287
|
-
const headers = await this.getV3AgentHeaders();
|
|
1288
|
-
const failures = [];
|
|
1289
|
-
for (const baseUrl of endpoints) {
|
|
1290
|
-
const runUrl = this.getV3AgentRunUrl(baseUrl);
|
|
1291
|
-
// Use the lightweight GET /health endpoint — no LLM invocation, responds instantly.
|
|
1292
|
-
// For local 8030 the path is /health directly; for remote it's /api/v3-agent/health via proxy.
|
|
1293
|
-
const healthUrl = this.isDirectV3AgentBaseUrl(baseUrl)
|
|
1294
|
-
? `${baseUrl}/health`
|
|
1295
|
-
: `${baseUrl}/api/v3-agent/health`;
|
|
1296
|
-
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
1297
|
-
try {
|
|
1298
|
-
const response = await fetch(healthUrl, {
|
|
1299
|
-
method: 'GET',
|
|
1300
|
-
headers,
|
|
1301
|
-
signal: AbortSignal.timeout(8000),
|
|
1302
|
-
});
|
|
1303
|
-
if (response.status === 401 || response.status === 403) {
|
|
1304
|
-
return {
|
|
1305
|
-
healthy: false,
|
|
1306
|
-
endpoint: runUrl,
|
|
1307
|
-
error: 'V3 service rejected authentication. Please re-login to refresh your token.',
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
// 200 OK or 405 Method Not Allowed both mean V3 is reachable.
|
|
1311
|
-
if (response.ok || response.status === 405) {
|
|
1312
|
-
return { healthy: true, endpoint: runUrl };
|
|
1313
|
-
}
|
|
1314
|
-
failures.push(`${healthUrl} attempt ${attempt}: HTTP ${response.status}`);
|
|
1315
|
-
}
|
|
1316
|
-
catch (error) {
|
|
1317
|
-
const reason = sanitizeUserFacingErrorText(error?.message || String(error)).slice(0, 160);
|
|
1318
|
-
failures.push(`${healthUrl} attempt ${attempt}: ${reason || 'network error'}`);
|
|
1319
|
-
}
|
|
1320
|
-
if (attempt < 3) {
|
|
1321
|
-
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
return {
|
|
1326
|
-
healthy: false,
|
|
1327
|
-
endpoint: endpoints[0] ? this.getV3AgentRunUrl(endpoints[0]) : 'unknown',
|
|
1328
|
-
error: `V3 service is not reachable during startup preflight. Checked ${endpoints.length} endpoint(s). ${failures.slice(-3).join(' | ')}`,
|
|
1329
|
-
};
|
|
812
|
+
return this.v3AgentClient.health();
|
|
1330
813
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
const healthEndpoint = this.isDirectV3AgentBaseUrl(baseUrl)
|
|
1334
|
-
? `${baseUrl}/health`
|
|
1335
|
-
: `${baseUrl}/api/v3-agent/health`;
|
|
1336
|
-
const headers = await this.getV3AgentHeaders();
|
|
1337
|
-
headers['X-Vigthoria-Preflight'] = '1';
|
|
1338
|
-
if (executionContext.mcpContextId) {
|
|
1339
|
-
headers['X-MCP-Context-Id'] = String(executionContext.mcpContextId);
|
|
1340
|
-
}
|
|
1341
|
-
const controller = new AbortController();
|
|
1342
|
-
const timeoutId = setTimeout(() => controller.abort(), 12000);
|
|
1343
|
-
try {
|
|
1344
|
-
const response = await fetch(healthEndpoint, {
|
|
1345
|
-
method: 'GET',
|
|
1346
|
-
headers,
|
|
1347
|
-
signal: controller.signal,
|
|
1348
|
-
});
|
|
1349
|
-
if (response.status === 401 || response.status === 403) {
|
|
1350
|
-
const errorText = await response.text().catch(() => '');
|
|
1351
|
-
return {
|
|
1352
|
-
ok: false,
|
|
1353
|
-
status: response.status,
|
|
1354
|
-
endpoint: healthEndpoint,
|
|
1355
|
-
reason: sanitizeUserFacingErrorText(errorText).slice(0, 200) || 'Unauthorized',
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
// Any non-auth response means token acceptance passed at the V3 service layer.
|
|
1359
|
-
return { ok: true, status: response.status, endpoint: healthEndpoint };
|
|
1360
|
-
}
|
|
1361
|
-
catch (error) {
|
|
1362
|
-
const reason = sanitizeUserFacingErrorText(error?.message || String(error)).slice(0, 200);
|
|
1363
|
-
// Preflight should not block agent execution on transient timeout/abort network conditions.
|
|
1364
|
-
if (/aborted|timeout|timed out|network/i.test(reason)) {
|
|
1365
|
-
return {
|
|
1366
|
-
ok: true,
|
|
1367
|
-
status: 0,
|
|
1368
|
-
endpoint: healthEndpoint,
|
|
1369
|
-
reason,
|
|
1370
|
-
};
|
|
1371
|
-
}
|
|
1372
|
-
return {
|
|
1373
|
-
ok: false,
|
|
1374
|
-
status: 0,
|
|
1375
|
-
endpoint: healthEndpoint,
|
|
1376
|
-
reason,
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
finally {
|
|
1380
|
-
clearTimeout(timeoutId);
|
|
1381
|
-
}
|
|
814
|
+
runV3AgentAuthPreflight(baseUrl, executionContext) {
|
|
815
|
+
return this.v3AgentClient.preflight(baseUrl, executionContext);
|
|
1382
816
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
const headers = await this.getV3AgentHeaders();
|
|
1386
|
-
if (executionContext.mcpContextId) {
|
|
1387
|
-
headers['X-MCP-Context-Id'] = String(executionContext.mcpContextId);
|
|
1388
|
-
}
|
|
1389
|
-
return fetch(this.getV3AgentRunUrl(baseUrl), {
|
|
1390
|
-
method: 'POST',
|
|
1391
|
-
headers,
|
|
1392
|
-
body: JSON.stringify(body),
|
|
1393
|
-
signal,
|
|
1394
|
-
});
|
|
1395
|
-
};
|
|
1396
|
-
let response = await makeRequest();
|
|
1397
|
-
if (response.status !== 401) {
|
|
1398
|
-
return response;
|
|
1399
|
-
}
|
|
1400
|
-
const refreshed = await this.refreshToken();
|
|
1401
|
-
if (!refreshed) {
|
|
1402
|
-
return response;
|
|
1403
|
-
}
|
|
1404
|
-
response = await makeRequest();
|
|
1405
|
-
return response;
|
|
817
|
+
executeV3AgentRunRequest(baseUrl, body, executionContext, signal) {
|
|
818
|
+
return this.v3AgentClient.run(baseUrl, body, executionContext, signal);
|
|
1406
819
|
}
|
|
1407
|
-
async getVigFlowAccessToken(baseUrl) {
|
|
1408
|
-
const cachedToken = this.vigFlowTokens.get(baseUrl);
|
|
1409
|
-
if (cachedToken) {
|
|
1410
|
-
return cachedToken;
|
|
1411
|
-
}
|
|
1412
|
-
const authToken = this.getAccessToken();
|
|
1413
|
-
if (!authToken) {
|
|
1414
|
-
throw new Error('Not authenticated. Run vigthoria login first.');
|
|
1415
|
-
}
|
|
1416
|
-
const response = await axios.post(`${baseUrl}/api/auth/sso`, { token: authToken }, {
|
|
1417
|
-
headers: {
|
|
1418
|
-
'Content-Type': 'application/json',
|
|
1419
|
-
},
|
|
1420
|
-
timeout: 30000,
|
|
1421
|
-
});
|
|
1422
|
-
const payload = response.data;
|
|
1423
|
-
const vigFlowToken = String(payload.token || '').trim();
|
|
1424
|
-
if (!vigFlowToken) {
|
|
1425
|
-
throw new Error('VigFlow SSO response did not include a token.');
|
|
1426
|
-
}
|
|
1427
|
-
this.vigFlowTokens.set(baseUrl, vigFlowToken);
|
|
1428
|
-
return vigFlowToken;
|
|
1429
|
-
}
|
|
1430
|
-
async getVigFlowHeaders(baseUrl) {
|
|
1431
|
-
const token = await this.getVigFlowAccessToken(baseUrl);
|
|
1432
|
-
return {
|
|
1433
|
-
'Content-Type': 'application/json',
|
|
1434
|
-
Accept: 'application/json',
|
|
1435
|
-
Authorization: `Bearer ${token}`,
|
|
1436
|
-
};
|
|
1437
|
-
}
|
|
1438
|
-
async withVigFlow(operation, action) {
|
|
1439
|
-
let lastError = null;
|
|
1440
|
-
for (const baseUrl of this.getVigFlowBaseUrls()) {
|
|
1441
|
-
try {
|
|
1442
|
-
const headers = await this.getVigFlowHeaders(baseUrl);
|
|
1443
|
-
return await action(baseUrl, headers);
|
|
1444
|
-
}
|
|
1445
|
-
catch (error) {
|
|
1446
|
-
lastError = error;
|
|
1447
|
-
this.logger.debug(`VigFlow ${operation} via ${baseUrl} failed:`, lastError.message);
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
// Throw a clean message instead of the raw ECONNREFUSED from the last URL tried
|
|
1451
|
-
throw new Error(`No VigFlow backend available for ${operation}. The workflow service is not deployed or not reachable.`);
|
|
1452
|
-
}
|
|
1453
|
-
/**
|
|
1454
|
-
* Build the correct sub-path for VigFlow endpoints.
|
|
1455
|
-
* Local servers (e.g. localhost:5060) need `/api/…` prefix.
|
|
1456
|
-
* The remote gateway URL already ends with `/api/vigflow`, so appending
|
|
1457
|
-
* another `/api/…` would double the prefix and cause 404s.
|
|
1458
|
-
*/
|
|
1459
|
-
vigFlowEndpoint(baseUrl, subPath) {
|
|
1460
|
-
if (/\/api\/vigflow\/?$/i.test(baseUrl)) {
|
|
1461
|
-
// Remote gateway – subPath like '/templates' is enough
|
|
1462
|
-
return `${baseUrl.replace(/\/$/, '')}${subPath}`;
|
|
1463
|
-
}
|
|
1464
|
-
return `${baseUrl}/api${subPath}`;
|
|
1465
|
-
}
|
|
1466
|
-
async listVigFlowTemplates(options = {}) {
|
|
1467
|
-
return this.withVigFlow('list templates', async (baseUrl, headers) => {
|
|
1468
|
-
const query = new URLSearchParams();
|
|
1469
|
-
if (options.category) {
|
|
1470
|
-
query.set('category', options.category);
|
|
1471
|
-
}
|
|
1472
|
-
if (options.search) {
|
|
1473
|
-
query.set('search', options.search);
|
|
1474
|
-
}
|
|
1475
|
-
const url = `${this.vigFlowEndpoint(baseUrl, '/templates')}${query.size > 0 ? `?${query.toString()}` : ''}`;
|
|
1476
|
-
const response = await axios.get(url, {
|
|
1477
|
-
headers,
|
|
1478
|
-
timeout: 30000,
|
|
1479
|
-
});
|
|
1480
|
-
const payload = response.data;
|
|
1481
|
-
return Array.isArray(payload.templates) ? payload.templates : [];
|
|
1482
|
-
});
|
|
1483
|
-
}
|
|
1484
|
-
async listVigFlowWorkflows() {
|
|
1485
|
-
return this.withVigFlow('list workflows', async (baseUrl, headers) => {
|
|
1486
|
-
const response = await axios.get(this.vigFlowEndpoint(baseUrl, '/workflows'), {
|
|
1487
|
-
headers,
|
|
1488
|
-
timeout: 30000,
|
|
1489
|
-
});
|
|
1490
|
-
const payload = response.data;
|
|
1491
|
-
return Array.isArray(payload.workflows) ? payload.workflows : [];
|
|
1492
|
-
});
|
|
1493
|
-
}
|
|
1494
|
-
async resolveVigFlowWorkflow(selector) {
|
|
1495
|
-
const normalizedSelector = String(selector || '').trim();
|
|
1496
|
-
if (!normalizedSelector) {
|
|
1497
|
-
throw new Error('Workflow selector is required. Provide a workflow id or name.');
|
|
1498
|
-
}
|
|
1499
|
-
const workflows = await this.listVigFlowWorkflows();
|
|
1500
|
-
const byId = workflows.find((workflow) => workflow.id === normalizedSelector);
|
|
1501
|
-
if (byId) {
|
|
1502
|
-
return {
|
|
1503
|
-
id: byId.id,
|
|
1504
|
-
name: byId.name,
|
|
1505
|
-
selector: normalizedSelector,
|
|
1506
|
-
matchedBy: 'id',
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
1509
|
-
const loweredSelector = normalizedSelector.toLowerCase();
|
|
1510
|
-
const exactNameMatches = workflows.filter((workflow) => String(workflow.name || '').trim().toLowerCase() === loweredSelector);
|
|
1511
|
-
if (exactNameMatches.length === 1) {
|
|
1512
|
-
return {
|
|
1513
|
-
id: exactNameMatches[0].id,
|
|
1514
|
-
name: exactNameMatches[0].name,
|
|
1515
|
-
selector: normalizedSelector,
|
|
1516
|
-
matchedBy: 'name',
|
|
1517
|
-
};
|
|
1518
|
-
}
|
|
1519
|
-
if (exactNameMatches.length > 1) {
|
|
1520
|
-
throw new Error(`Multiple workflows matched the name "${normalizedSelector}". Use the workflow id instead.`);
|
|
1521
|
-
}
|
|
1522
|
-
const searchMatches = workflows.filter((workflow) => String(workflow.name || '').toLowerCase().includes(loweredSelector));
|
|
1523
|
-
if (searchMatches.length === 1) {
|
|
1524
|
-
return {
|
|
1525
|
-
id: searchMatches[0].id,
|
|
1526
|
-
name: searchMatches[0].name,
|
|
1527
|
-
selector: normalizedSelector,
|
|
1528
|
-
matchedBy: 'search',
|
|
1529
|
-
};
|
|
1530
|
-
}
|
|
1531
|
-
if (searchMatches.length > 1) {
|
|
1532
|
-
throw new Error(`Multiple workflows partially matched "${normalizedSelector}". Use a full workflow name or id.`);
|
|
1533
|
-
}
|
|
1534
|
-
throw new Error(`No VigFlow workflow matched "${normalizedSelector}".`);
|
|
1535
|
-
}
|
|
1536
|
-
async useVigFlowTemplate(templateId, options = {}) {
|
|
1537
|
-
return this.withVigFlow('use template', async (baseUrl, headers) => {
|
|
1538
|
-
const response = await axios.post(`${this.vigFlowEndpoint(baseUrl, `/templates/${encodeURIComponent(templateId)}/use`)}`, {
|
|
1539
|
-
name: options.name,
|
|
1540
|
-
variables: options.variables || {},
|
|
1541
|
-
}, {
|
|
1542
|
-
headers,
|
|
1543
|
-
timeout: 30000,
|
|
1544
|
-
});
|
|
1545
|
-
const payload = response.data;
|
|
1546
|
-
if (!payload.workflow?.id) {
|
|
1547
|
-
throw new Error('VigFlow use-template response did not include a workflow id.');
|
|
1548
|
-
}
|
|
1549
|
-
return payload.workflow;
|
|
1550
|
-
});
|
|
1551
|
-
}
|
|
1552
|
-
async runVigFlowWorkflow(workflowId, options = {}) {
|
|
1553
|
-
return this.withVigFlow('run workflow', async (baseUrl, headers) => {
|
|
1554
|
-
const response = await axios.post(`${this.vigFlowEndpoint(baseUrl, `/executions/run/${encodeURIComponent(workflowId)}`)}`, {
|
|
1555
|
-
data: options.data || {},
|
|
1556
|
-
options: options.executionOptions || {},
|
|
1557
|
-
}, {
|
|
1558
|
-
headers,
|
|
1559
|
-
timeout: 60000,
|
|
1560
|
-
});
|
|
1561
|
-
const payload = response.data;
|
|
1562
|
-
if (!payload.executionId) {
|
|
1563
|
-
throw new Error('VigFlow run response did not include an execution id.');
|
|
1564
|
-
}
|
|
1565
|
-
return payload;
|
|
1566
|
-
});
|
|
1567
|
-
}
|
|
1568
|
-
async getVigFlowExecutionStatus(executionId) {
|
|
1569
|
-
return this.withVigFlow('execution status', async (baseUrl, headers) => {
|
|
1570
|
-
const response = await axios.get(`${this.vigFlowEndpoint(baseUrl, `/executions/${encodeURIComponent(executionId)}`)}`, {
|
|
1571
|
-
headers,
|
|
1572
|
-
timeout: 30000,
|
|
1573
|
-
});
|
|
1574
|
-
const payload = response.data;
|
|
1575
|
-
if (!payload.execution?.id) {
|
|
1576
|
-
throw new Error('VigFlow execution response did not include execution details.');
|
|
1577
|
-
}
|
|
1578
|
-
return payload.execution;
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
/** Legacy cap — prefer resolveV3ContextCharLimit() for token-aligned budgets. */
|
|
1582
|
-
static V3_CONTEXT_CHAR_LIMIT = 100_000;
|
|
1583
820
|
static HYDRATION_PRIORITY_BASENAMES = new Set([
|
|
1584
821
|
'package.json', 'index.html', 'game.js', 'main.js', 'app.js', 'script.js',
|
|
1585
822
|
'styles.css', 'style.css', 'README.md', 'manifest.json',
|
|
@@ -1634,7 +871,6 @@ export class APIClient {
|
|
|
1634
871
|
}
|
|
1635
872
|
buildV3AgentContext(context = {}) {
|
|
1636
873
|
const resolvedContext = this.ensureExecutionContext(context);
|
|
1637
|
-
const targetPath = resolvedContext.targetPath || resolvedContext.projectPath || resolvedContext.workspacePath || resolvedContext.projectRoot || process.cwd();
|
|
1638
874
|
const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
|
|
1639
875
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1640
876
|
const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
|
|
@@ -1687,6 +923,11 @@ export class APIClient {
|
|
|
1687
923
|
executionHints.requires_file_changes = false;
|
|
1688
924
|
}
|
|
1689
925
|
}
|
|
926
|
+
const contextState = resolvedContext.mcpContextId
|
|
927
|
+
? appendContextLayer(resolvedContext.contextState, 'mcp-context', {
|
|
928
|
+
metadata: { contextId: resolvedContext.mcpContextId, semanticAuthority: 'tool-evidence-only' },
|
|
929
|
+
})
|
|
930
|
+
: resolvedContext.contextState || null;
|
|
1690
931
|
const payload = {
|
|
1691
932
|
workspace: this.buildPublicWorkspaceDescriptor(resolvedContext.workspace, {
|
|
1692
933
|
localWorkspacePath,
|
|
@@ -1738,11 +979,13 @@ export class APIClient {
|
|
|
1738
979
|
codebaseContext: resolvedContext.codebaseContext || '',
|
|
1739
980
|
accountBrainContext: resolvedContext.accountBrainContext || '',
|
|
1740
981
|
projectMemory: resolvedContext.projectMemory || '',
|
|
982
|
+
contextState,
|
|
983
|
+
contextCompatibilityAdapters: contextState?.compatibilityAdapters || null,
|
|
1741
984
|
clientManifest: buildClientManifest(resolvedContext.clientManifest && typeof resolvedContext.clientManifest === 'object'
|
|
1742
985
|
? resolvedContext.clientManifest
|
|
1743
986
|
: {}),
|
|
1744
987
|
};
|
|
1745
|
-
return this.compactV3Context(payload);
|
|
988
|
+
return this.compactV3Context(scanOutboundContext(payload).value);
|
|
1746
989
|
}
|
|
1747
990
|
/**
|
|
1748
991
|
* Compact a V3 context payload so the serialized JSON stays under
|
|
@@ -1875,34 +1118,35 @@ export class APIClient {
|
|
|
1875
1118
|
}
|
|
1876
1119
|
return finish();
|
|
1877
1120
|
}
|
|
1878
|
-
/**
|
|
1879
|
-
|
|
1880
|
-
|
|
1121
|
+
/** Remove server-side workspace aliases before delegated local execution. */
|
|
1122
|
+
normalizePortableRunCommand(command, serverRoot = '') {
|
|
1123
|
+
const cmd = String(command || '').trim();
|
|
1881
1124
|
if (!cmd)
|
|
1882
1125
|
return cmd;
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
[
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
}
|
|
1902
|
-
return cmd;
|
|
1126
|
+
const match = cmd.match(/^\s*cd\s+(?:"([^"]+)"|'([^']+)'|([^\s;&]+))\s*(?:&&|;)\s*([\s\S]+)$/i);
|
|
1127
|
+
if (!match)
|
|
1128
|
+
return cmd;
|
|
1129
|
+
const requestedCwd = String(match[1] || match[2] || match[3] || '').trim().replace(/\\/g, '/').replace(/\/$/, '');
|
|
1130
|
+
const normalizedServerRoot = String(serverRoot || '').trim().replace(/\\/g, '/').replace(/\/$/, '');
|
|
1131
|
+
const workspaceAliases = new Set([
|
|
1132
|
+
'.',
|
|
1133
|
+
'vigthoria://workspace',
|
|
1134
|
+
'vigthoria://server-internal',
|
|
1135
|
+
'/vigthoria/workspace',
|
|
1136
|
+
'/workspace',
|
|
1137
|
+
'[internal]',
|
|
1138
|
+
normalizedServerRoot,
|
|
1139
|
+
].filter(Boolean).map((value) => value.toLowerCase()));
|
|
1140
|
+
// The delegated client already executes with a validated workspace cwd.
|
|
1141
|
+
// Strip only exact root aliases; preserve real relative subdirectory cd
|
|
1142
|
+
// commands so project-specific workflows keep their intended scope.
|
|
1143
|
+
return workspaceAliases.has(requestedCwd.toLowerCase()) ? String(match[4] || '').trim() : cmd;
|
|
1903
1144
|
}
|
|
1904
1145
|
resolveRunCommandCwd(rootPath, args, serverRoot) {
|
|
1905
|
-
|
|
1146
|
+
// run_command owns a cwd field. A model-supplied source `path` belongs to
|
|
1147
|
+
// file tools and must not silently move command execution into that file's
|
|
1148
|
+
// parent directory (which breaks root-level validation scripts).
|
|
1149
|
+
const cwdRaw = args.cwd ?? args.directory ?? '.';
|
|
1906
1150
|
let target = this.resolveV3ClientToolPath(rootPath, cwdRaw, serverRoot ? [serverRoot] : []);
|
|
1907
1151
|
if (!target) {
|
|
1908
1152
|
target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
|
|
@@ -2027,6 +1271,7 @@ export class APIClient {
|
|
|
2027
1271
|
requestStartedAt: resolvedContext.requestStartedAt,
|
|
2028
1272
|
vigthoriaBrain: dedupedBrain,
|
|
2029
1273
|
vigthoria_brain: dedupedBrain,
|
|
1274
|
+
contextState: resolvedContext.contextState || null,
|
|
2030
1275
|
});
|
|
2031
1276
|
}
|
|
2032
1277
|
extractEmergencyAppName(message = '', fallback = 'Signal Desk') {
|
|
@@ -2497,18 +1742,18 @@ menu {
|
|
|
2497
1742
|
}
|
|
2498
1743
|
async bindExecutionContext(context = {}) {
|
|
2499
1744
|
const executionContext = this.ensureExecutionContext(context);
|
|
2500
|
-
const headers = await this.getMcpHeaders();
|
|
2501
1745
|
const localWorkspacePath = this.resolveAgentTargetPath(executionContext);
|
|
2502
1746
|
const workspacePath = this.resolveServerBindableWorkspacePath(executionContext);
|
|
2503
1747
|
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, String(executionContext.rawPrompt || executionContext.contextualPrompt || executionContext.prompt || ''));
|
|
1748
|
+
const workspaceAlias = `vigthoria://workspace/${encodeURIComponent(path.basename(localWorkspacePath || workspacePath || 'workspace'))}`;
|
|
2504
1749
|
const metadata = {
|
|
2505
1750
|
source: 'vigthoria-cli',
|
|
2506
1751
|
sharedContextId: executionContext.contextId,
|
|
2507
1752
|
traceId: executionContext.traceId,
|
|
2508
1753
|
executionSurface: executionContext.executionSurface || 'cli',
|
|
2509
1754
|
clientSurface: executionContext.clientSurface || 'cli',
|
|
2510
|
-
workspacePath:
|
|
2511
|
-
localWorkspacePath:
|
|
1755
|
+
workspacePath: workspaceAlias,
|
|
1756
|
+
localWorkspacePath: workspaceAlias,
|
|
2512
1757
|
localWorkspaceSummary,
|
|
2513
1758
|
runtimeEnvironment: executionContext.agentRuntime || null,
|
|
2514
1759
|
requestStartedAt: executionContext.requestStartedAt,
|
|
@@ -2519,10 +1764,10 @@ menu {
|
|
|
2519
1764
|
sharedContextId: executionContext.contextId,
|
|
2520
1765
|
traceId: executionContext.traceId,
|
|
2521
1766
|
requestStartedAt: executionContext.requestStartedAt,
|
|
2522
|
-
workspacePath:
|
|
2523
|
-
projectPath:
|
|
2524
|
-
targetPath:
|
|
2525
|
-
localWorkspacePath:
|
|
1767
|
+
workspacePath: workspaceAlias,
|
|
1768
|
+
projectPath: workspaceAlias,
|
|
1769
|
+
targetPath: workspaceAlias,
|
|
1770
|
+
localWorkspacePath: workspaceAlias,
|
|
2526
1771
|
localWorkspaceSummary,
|
|
2527
1772
|
runtimeEnvironment: executionContext.agentRuntime || null,
|
|
2528
1773
|
activeFile: executionContext.activeFile || null,
|
|
@@ -2531,68 +1776,22 @@ menu {
|
|
|
2531
1776
|
userId: this.config.get('userId') || null,
|
|
2532
1777
|
email: this.config.get('email') || null,
|
|
2533
1778
|
};
|
|
2534
|
-
const
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
}
|
|
2544
|
-
return {
|
|
2545
|
-
...executionContext,
|
|
2546
|
-
localWorkspacePath: localWorkspacePath || executionContext.localWorkspacePath || null,
|
|
2547
|
-
targetPath: localWorkspacePath || executionContext.targetPath || executionContext.projectPath || executionContext.workspacePath || null,
|
|
2548
|
-
projectPath: localWorkspacePath || executionContext.projectPath || executionContext.targetPath || executionContext.workspacePath || null,
|
|
2549
|
-
workspacePath: localWorkspacePath || executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || null,
|
|
2550
|
-
mcpContextId: binding.mcpContextId,
|
|
2551
|
-
mcpContextBackendUrl: binding.backendUrl,
|
|
2552
|
-
};
|
|
2553
|
-
};
|
|
2554
|
-
if (executionContext.mcpContextId && executionContext.mcpContextBackendUrl) {
|
|
2555
|
-
try {
|
|
2556
|
-
return await updateExisting({
|
|
2557
|
-
mcpContextId: String(executionContext.mcpContextId),
|
|
2558
|
-
backendUrl: String(executionContext.mcpContextBackendUrl),
|
|
2559
|
-
});
|
|
2560
|
-
}
|
|
2561
|
-
catch (error) {
|
|
2562
|
-
this.logger.debug('Failed to refresh existing MCP context binding:', error.message);
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
for (const baseUrl of this.getMcpBaseUrls()) {
|
|
2566
|
-
try {
|
|
2567
|
-
const createResponse = await fetch(this.getMcpContextCreateUrl(baseUrl), {
|
|
2568
|
-
method: 'POST',
|
|
2569
|
-
headers,
|
|
2570
|
-
body: JSON.stringify({
|
|
2571
|
-
userId: this.config.get('userId') || this.config.get('email') || 'vigthoria-cli',
|
|
2572
|
-
metadata,
|
|
2573
|
-
}),
|
|
2574
|
-
});
|
|
2575
|
-
if (!createResponse.ok) {
|
|
2576
|
-
const errorText = await createResponse.text().catch(() => '');
|
|
2577
|
-
throw new Error(`MCP context create ${createResponse.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
2578
|
-
}
|
|
2579
|
-
const payload = await createResponse.json();
|
|
2580
|
-
const mcpContextId = String(payload.contextId || '').trim();
|
|
2581
|
-
if (!mcpContextId) {
|
|
2582
|
-
throw new Error('MCP context create response did not include a contextId');
|
|
2583
|
-
}
|
|
2584
|
-
return await updateExisting({ mcpContextId, backendUrl: baseUrl });
|
|
2585
|
-
}
|
|
2586
|
-
catch (error) {
|
|
2587
|
-
this.logger.debug(`Failed to bind MCP context via ${baseUrl}:`, error.message);
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
1779
|
+
const binding = await this.mcpContextClient.bind({
|
|
1780
|
+
existing: executionContext.mcpContextId && executionContext.mcpContextBackendUrl ? {
|
|
1781
|
+
mcpContextId: String(executionContext.mcpContextId),
|
|
1782
|
+
backendUrl: String(executionContext.mcpContextBackendUrl),
|
|
1783
|
+
} : null,
|
|
1784
|
+
userId: this.config.get('userId') || this.config.get('email') || 'vigthoria-cli',
|
|
1785
|
+
metadata,
|
|
1786
|
+
data,
|
|
1787
|
+
});
|
|
2590
1788
|
return {
|
|
2591
1789
|
...executionContext,
|
|
2592
1790
|
localWorkspacePath: localWorkspacePath || executionContext.localWorkspacePath || null,
|
|
2593
1791
|
targetPath: localWorkspacePath || executionContext.targetPath || executionContext.projectPath || executionContext.workspacePath || null,
|
|
2594
1792
|
projectPath: localWorkspacePath || executionContext.projectPath || executionContext.targetPath || executionContext.workspacePath || null,
|
|
2595
1793
|
workspacePath: localWorkspacePath || executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || null,
|
|
1794
|
+
...(binding ? { mcpContextId: binding.mcpContextId, mcpContextBackendUrl: binding.backendUrl } : {}),
|
|
2596
1795
|
};
|
|
2597
1796
|
}
|
|
2598
1797
|
isServerManagedTempWorkspacePath(pathValue) {
|
|
@@ -2704,7 +1903,7 @@ menu {
|
|
|
2704
1903
|
files: [],
|
|
2705
1904
|
};
|
|
2706
1905
|
const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
|
|
2707
|
-
const candidatePaths = snapshot.paths.filter((entry) =>
|
|
1906
|
+
const candidatePaths = snapshot.paths.filter((entry) => !isSensitivePath(entry));
|
|
2708
1907
|
const focusFiles = this.extractExpectedWorkspaceFiles(requestFocus)
|
|
2709
1908
|
.map((entry) => this.normalizeWorkspaceRelativePath(entry))
|
|
2710
1909
|
.filter((entry) => candidatePaths.includes(entry));
|
|
@@ -2726,7 +1925,9 @@ menu {
|
|
|
2726
1925
|
}
|
|
2727
1926
|
}
|
|
2728
1927
|
const hydrationFocusFiles = [...focusFiles, ...linkedFocusAssets];
|
|
2729
|
-
|
|
1928
|
+
// Runtime state remains local. Resume/auth/session material under
|
|
1929
|
+
// .vigthoria is never hydrated into a remote agent workspace.
|
|
1930
|
+
const agentStatePaths = this.collectAgentStateSyncPaths(rootPath).filter((entry) => !isSensitivePath(entry));
|
|
2730
1931
|
let orderedPaths = [...agentStatePaths, ...hydrationFocusFiles, ...candidatePaths];
|
|
2731
1932
|
try {
|
|
2732
1933
|
const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
|
|
@@ -2807,271 +2008,28 @@ menu {
|
|
|
2807
2008
|
* Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
|
|
2808
2009
|
*/
|
|
2809
2010
|
collectWorkspaceFileContents(rootPath, filePaths) {
|
|
2810
|
-
|
|
2811
|
-
const MAX_FILE_BYTES = 200 * 1024;
|
|
2812
|
-
const BINARY_EXTENSIONS = new Set([
|
|
2813
|
-
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.avif',
|
|
2814
|
-
'.mp3', '.mp4', '.wav', '.ogg', '.webm', '.flac', '.aac',
|
|
2815
|
-
'.zip', '.gz', '.tar', '.rar', '.7z', '.bz2',
|
|
2816
|
-
'.exe', '.dll', '.so', '.dylib', '.bin', '.dat',
|
|
2817
|
-
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
2818
|
-
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
2819
|
-
'.db', '.sqlite', '.sqlite3',
|
|
2820
|
-
'.pyc', '.pyo', '.class', '.o', '.obj',
|
|
2821
|
-
'.DS_Store', '.lock',
|
|
2822
|
-
]);
|
|
2823
|
-
const result = {};
|
|
2824
|
-
let totalBytes = 0;
|
|
2825
|
-
for (const relativePath of filePaths) {
|
|
2826
|
-
if (totalBytes >= MAX_TOTAL_BYTES)
|
|
2827
|
-
break;
|
|
2828
|
-
const ext = path.extname(relativePath).toLowerCase();
|
|
2829
|
-
if (BINARY_EXTENSIONS.has(ext))
|
|
2830
|
-
continue;
|
|
2831
|
-
if (/(^|[\/\\])\.(git|hg)([\/\\]|$)/.test(relativePath))
|
|
2832
|
-
continue;
|
|
2833
|
-
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
2834
|
-
const isAgentStateSync = /^\.vigthoria\/agent-state\//i.test(normalizedPath);
|
|
2835
|
-
if (!isAgentStateSync && /(^|[\/\\])\.vigthoria([\/\\]|$)/.test(relativePath))
|
|
2836
|
-
continue;
|
|
2837
|
-
const absolutePath = path.join(rootPath, relativePath);
|
|
2838
|
-
try {
|
|
2839
|
-
const stat = fs.statSync(absolutePath);
|
|
2840
|
-
if (!stat.isFile() || stat.size > MAX_FILE_BYTES || stat.size === 0)
|
|
2841
|
-
continue;
|
|
2842
|
-
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
2843
|
-
// Skip likely binary content (high ratio of non-printable chars)
|
|
2844
|
-
if (/[\x00-\x08\x0e-\x1f]/.test(content.slice(0, 512)))
|
|
2845
|
-
continue;
|
|
2846
|
-
result[relativePath] = content;
|
|
2847
|
-
totalBytes += Buffer.byteLength(content, 'utf8');
|
|
2848
|
-
}
|
|
2849
|
-
catch {
|
|
2850
|
-
// Skip unreadable files
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
return result;
|
|
2011
|
+
return this.v3WorkspaceService.collectFileContents(rootPath, filePaths);
|
|
2854
2012
|
}
|
|
2855
2013
|
hasAgentWorkspaceOutput(context = {}) {
|
|
2856
|
-
|
|
2857
|
-
const root = this.resolveAgentTargetPath(context);
|
|
2858
|
-
if (!root || !fs.existsSync(root)) {
|
|
2859
|
-
return false;
|
|
2860
|
-
}
|
|
2861
|
-
const stack = [root];
|
|
2862
|
-
while (stack.length > 0) {
|
|
2863
|
-
const current = stack.pop();
|
|
2864
|
-
if (!current)
|
|
2865
|
-
continue;
|
|
2866
|
-
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
2867
|
-
for (const entry of entries) {
|
|
2868
|
-
if (entry.name === '.git' || entry.name === 'node_modules') {
|
|
2869
|
-
continue;
|
|
2870
|
-
}
|
|
2871
|
-
const fullPath = path.join(current, entry.name);
|
|
2872
|
-
if (entry.isFile()) {
|
|
2873
|
-
return true;
|
|
2874
|
-
}
|
|
2875
|
-
if (entry.isDirectory()) {
|
|
2876
|
-
stack.push(fullPath);
|
|
2877
|
-
}
|
|
2878
|
-
}
|
|
2879
|
-
}
|
|
2880
|
-
}
|
|
2881
|
-
catch {
|
|
2882
|
-
return false;
|
|
2883
|
-
}
|
|
2884
|
-
return false;
|
|
2014
|
+
return this.v3WorkspaceService.hasOutput(context);
|
|
2885
2015
|
}
|
|
2886
2016
|
getAgentWorkspaceSnapshot(rootPath) {
|
|
2887
|
-
|
|
2888
|
-
let fileCount = 0;
|
|
2889
|
-
const entries = [];
|
|
2890
|
-
while (stack.length > 0) {
|
|
2891
|
-
const current = stack.pop();
|
|
2892
|
-
if (!current) {
|
|
2893
|
-
continue;
|
|
2894
|
-
}
|
|
2895
|
-
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
2896
|
-
if (entry.name === '.git' || entry.name === 'node_modules') {
|
|
2897
|
-
continue;
|
|
2898
|
-
}
|
|
2899
|
-
const fullPath = path.join(current, entry.name);
|
|
2900
|
-
if (entry.isDirectory()) {
|
|
2901
|
-
stack.push(fullPath);
|
|
2902
|
-
continue;
|
|
2903
|
-
}
|
|
2904
|
-
if (!entry.isFile()) {
|
|
2905
|
-
continue;
|
|
2906
|
-
}
|
|
2907
|
-
fileCount += 1;
|
|
2908
|
-
const stat = fs.statSync(fullPath);
|
|
2909
|
-
entries.push(`${path.relative(rootPath, fullPath)}:${stat.size}:${stat.mtimeMs}`);
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
entries.sort();
|
|
2913
|
-
return {
|
|
2914
|
-
fileCount,
|
|
2915
|
-
paths: entries.map((entry) => entry.split(':', 1)[0]),
|
|
2916
|
-
signature: entries.join('|'),
|
|
2917
|
-
};
|
|
2017
|
+
return this.v3WorkspaceService.snapshot(rootPath);
|
|
2918
2018
|
}
|
|
2919
2019
|
async waitForAgentWorkspaceSettle(context = {}, options = {}) {
|
|
2920
|
-
|
|
2921
|
-
if (!rootPath || !fs.existsSync(rootPath)) {
|
|
2922
|
-
return;
|
|
2923
|
-
}
|
|
2924
|
-
const timeoutMs = options.timeoutMs || 15000;
|
|
2925
|
-
const pollMs = options.pollMs || 300;
|
|
2926
|
-
const stableMs = options.stableMs || 1200;
|
|
2927
|
-
const expectedFiles = Array.isArray(options.expectedFiles)
|
|
2928
|
-
? options.expectedFiles.map((entry) => String(entry || '').trim()).filter(Boolean)
|
|
2929
|
-
: [];
|
|
2930
|
-
const start = Date.now();
|
|
2931
|
-
let stableSince = 0;
|
|
2932
|
-
let lastSignature = '';
|
|
2933
|
-
while (Date.now() - start < timeoutMs) {
|
|
2934
|
-
const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
|
|
2935
|
-
if (snapshot.fileCount === 0) {
|
|
2936
|
-
stableSince = 0;
|
|
2937
|
-
lastSignature = '';
|
|
2938
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
2939
|
-
continue;
|
|
2940
|
-
}
|
|
2941
|
-
if (expectedFiles.length > 0 && !expectedFiles.every((filePath) => snapshot.paths.includes(filePath))) {
|
|
2942
|
-
stableSince = 0;
|
|
2943
|
-
lastSignature = snapshot.signature;
|
|
2944
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
2945
|
-
continue;
|
|
2946
|
-
}
|
|
2947
|
-
if (snapshot.signature === lastSignature) {
|
|
2948
|
-
if (!stableSince) {
|
|
2949
|
-
stableSince = Date.now();
|
|
2950
|
-
}
|
|
2951
|
-
if (Date.now() - stableSince >= stableMs) {
|
|
2952
|
-
return;
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
else {
|
|
2956
|
-
lastSignature = snapshot.signature;
|
|
2957
|
-
stableSince = Date.now();
|
|
2958
|
-
}
|
|
2959
|
-
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
2960
|
-
}
|
|
2020
|
+
return this.v3WorkspaceService.waitForSettle(context, options);
|
|
2961
2021
|
}
|
|
2962
2022
|
extractExpectedWorkspaceFiles(message = '', context = {}) {
|
|
2963
|
-
|
|
2964
|
-
const addMatches = (value) => {
|
|
2965
|
-
const text = String(value || '');
|
|
2966
|
-
const patterns = [
|
|
2967
|
-
/`([^`]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))`/gi,
|
|
2968
|
-
/\b([A-Za-z0-9_./-]+\.(?:html|css|js|jsx|ts|tsx|json|md|py|sh))\b/g,
|
|
2969
|
-
];
|
|
2970
|
-
for (const pattern of patterns) {
|
|
2971
|
-
let match;
|
|
2972
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
2973
|
-
const filePath = match[1].trim().replace(/^\.\//, '');
|
|
2974
|
-
if (filePath && !filePath.startsWith('http://') && !filePath.startsWith('https://')) {
|
|
2975
|
-
candidates.add(filePath);
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
}
|
|
2979
|
-
};
|
|
2980
|
-
addMatches(message);
|
|
2981
|
-
addMatches(context.rawMessage);
|
|
2982
|
-
addMatches(context.agentPrompt);
|
|
2983
|
-
return Array.from(candidates);
|
|
2023
|
+
return this.v3WorkspaceService.extractExpectedFiles(message, context);
|
|
2984
2024
|
}
|
|
2985
2025
|
captureV3AgentStreamMutation(event, streamedFiles, serverRoot) {
|
|
2986
|
-
|
|
2987
|
-
return;
|
|
2988
|
-
}
|
|
2989
|
-
if (event.type === 'file_mutation' && typeof event.path === 'string') {
|
|
2990
|
-
const filePath = this.normalizeAgentWorkspaceRelativePath(event.path, serverRoot || undefined);
|
|
2991
|
-
if (!filePath) {
|
|
2992
|
-
return;
|
|
2993
|
-
}
|
|
2994
|
-
if (event.action === 'delete') {
|
|
2995
|
-
delete streamedFiles[filePath];
|
|
2996
|
-
return;
|
|
2997
|
-
}
|
|
2998
|
-
if (typeof event.content === 'string') {
|
|
2999
|
-
streamedFiles[filePath] = event.content;
|
|
3000
|
-
}
|
|
3001
|
-
return;
|
|
3002
|
-
}
|
|
3003
|
-
// workspace_snapshot is handled by applyV3AgentStreamEventToWorkspace only —
|
|
3004
|
-
// never count hydrated baselines as "files changed".
|
|
3005
|
-
// Only count confirmed mutations — not workspace snapshots (hydrated baseline)
|
|
3006
|
-
// or tool_call previews that may still fail client-side / read-only guards.
|
|
3007
|
-
if (event.type === 'tool_result' && event.success) {
|
|
3008
|
-
const name = String(event.name || event.tool || '').trim();
|
|
3009
|
-
const args = event.arguments || {};
|
|
3010
|
-
const targetPath = typeof event.target === 'string'
|
|
3011
|
-
? event.target
|
|
3012
|
-
: (typeof args.path === 'string' ? args.path : '');
|
|
3013
|
-
if ((name === 'write_file' || name === 'edit_file') && targetPath) {
|
|
3014
|
-
const filePath = this.normalizeAgentWorkspaceRelativePath(targetPath, serverRoot || undefined);
|
|
3015
|
-
if (filePath && typeof args.content === 'string') {
|
|
3016
|
-
streamedFiles[filePath] = args.content;
|
|
3017
|
-
}
|
|
3018
|
-
}
|
|
3019
|
-
}
|
|
2026
|
+
this.v3WorkspaceService.captureMutation(event, streamedFiles, serverRoot);
|
|
3020
2027
|
}
|
|
3021
2028
|
applyV3AgentStreamEventToWorkspace(event, context = {}, serverRoot) {
|
|
3022
|
-
|
|
3023
|
-
return false;
|
|
3024
|
-
}
|
|
3025
|
-
const rootPath = this.resolveAgentTargetPath(context);
|
|
3026
|
-
if (!rootPath) {
|
|
3027
|
-
return false;
|
|
3028
|
-
}
|
|
3029
|
-
if (event.type === 'workspace_snapshot' && event.files && typeof event.files === 'object') {
|
|
3030
|
-
let applied = false;
|
|
3031
|
-
for (const [rawPath, content] of Object.entries(event.files)) {
|
|
3032
|
-
if (typeof content !== 'string') {
|
|
3033
|
-
continue;
|
|
3034
|
-
}
|
|
3035
|
-
applied = this.writeV3AgentWorkspaceFile(rootPath, rawPath, content, serverRoot || undefined) || applied;
|
|
3036
|
-
}
|
|
3037
|
-
return applied;
|
|
3038
|
-
}
|
|
3039
|
-
if (event.type === 'file_mutation' && typeof event.path === 'string') {
|
|
3040
|
-
const relativePath = this.normalizeAgentWorkspaceRelativePath(event.path, serverRoot || undefined);
|
|
3041
|
-
if (!relativePath) {
|
|
3042
|
-
return false;
|
|
3043
|
-
}
|
|
3044
|
-
if (event.action === 'delete') {
|
|
3045
|
-
return this.deleteV3AgentWorkspaceFile(rootPath, relativePath);
|
|
3046
|
-
}
|
|
3047
|
-
if (typeof event.content === 'string') {
|
|
3048
|
-
return this.writeV3AgentWorkspaceFile(rootPath, relativePath, event.content, rootPath);
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
return false;
|
|
2029
|
+
return this.v3WorkspaceService.applyStreamEvent(event, context, serverRoot);
|
|
3052
2030
|
}
|
|
3053
2031
|
resolveV3ClientToolPath(rootPath, rawPath, sourceRoots = []) {
|
|
3054
|
-
|
|
3055
|
-
const roots = [rootPath, ...sourceRoots.filter(Boolean)];
|
|
3056
|
-
let relativePath = '';
|
|
3057
|
-
for (const sourceRoot of roots) {
|
|
3058
|
-
relativePath = this.normalizeAgentWorkspaceRelativePath(raw, sourceRoot);
|
|
3059
|
-
if (relativePath || raw === '.' || raw === '') {
|
|
3060
|
-
break;
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
if (!relativePath && raw !== '.' && raw !== '') {
|
|
3064
|
-
const treatAsWorkspaceRoot = roots.some((sourceRoot) => isV3WorkspaceRootAliasPath(raw, sourceRoot));
|
|
3065
|
-
if (!treatAsWorkspaceRoot) {
|
|
3066
|
-
return null;
|
|
3067
|
-
}
|
|
3068
|
-
}
|
|
3069
|
-
const absolutePath = relativePath ? joinV3WorkspacePath(rootPath, relativePath) : path.resolve(rootPath);
|
|
3070
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
3071
|
-
if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
|
|
3072
|
-
return null;
|
|
3073
|
-
}
|
|
3074
|
-
return { relativePath: relativePath || '.', absolutePath };
|
|
2032
|
+
return this.v3WorkspaceService.resolveClientToolPath(rootPath, rawPath, sourceRoots);
|
|
3075
2033
|
}
|
|
3076
2034
|
recordV3ClientToolMutation(event, context, result) {
|
|
3077
2035
|
if (!result.success) {
|
|
@@ -3117,7 +2075,7 @@ menu {
|
|
|
3117
2075
|
const serverRoot = String(context.__v3ServerWorkspaceRoot || '').trim();
|
|
3118
2076
|
const isRunCommand = name === 'run_command';
|
|
3119
2077
|
const pathArg = isRunCommand
|
|
3120
|
-
? (args.cwd ?? args.
|
|
2078
|
+
? (args.cwd ?? args.directory ?? '.')
|
|
3121
2079
|
: (args.path || args.directory || args.cwd || '.');
|
|
3122
2080
|
let target = this.resolveV3ClientToolPath(rootPath, pathArg, serverRoot ? [serverRoot] : []);
|
|
3123
2081
|
if (!target && isRunCommand) {
|
|
@@ -3151,7 +2109,26 @@ menu {
|
|
|
3151
2109
|
return result;
|
|
3152
2110
|
}
|
|
3153
2111
|
if (name === 'read_file') {
|
|
3154
|
-
|
|
2112
|
+
const content = fs.readFileSync(target.absolutePath, 'utf8');
|
|
2113
|
+
const lines = content.split('\n');
|
|
2114
|
+
const startValue = args.start_line ?? args.startLine ?? args.fromLine ?? args.lineStart;
|
|
2115
|
+
const endValue = args.end_line ?? args.endLine ?? args.toLine ?? args.lineEnd;
|
|
2116
|
+
let startLine = Number.parseInt(String(startValue ?? '1'), 10);
|
|
2117
|
+
let endLine = Number.parseInt(String(endValue ?? '0'), 10);
|
|
2118
|
+
if (!Number.isFinite(startLine) || startLine < 1)
|
|
2119
|
+
startLine = 1;
|
|
2120
|
+
if (startLine > lines.length)
|
|
2121
|
+
startLine = lines.length;
|
|
2122
|
+
if (!Number.isFinite(endLine) || endLine === 0)
|
|
2123
|
+
endLine = lines.length;
|
|
2124
|
+
if (endLine < startLine)
|
|
2125
|
+
endLine = startLine;
|
|
2126
|
+
if (endLine > lines.length)
|
|
2127
|
+
endLine = lines.length;
|
|
2128
|
+
return {
|
|
2129
|
+
success: true,
|
|
2130
|
+
output: lines.slice(startLine - 1, endLine).join('\n'),
|
|
2131
|
+
};
|
|
3155
2132
|
}
|
|
3156
2133
|
if (name === 'list_directory') {
|
|
3157
2134
|
const entries = fs.readdirSync(target.absolutePath, { withFileTypes: true })
|
|
@@ -3162,6 +2139,15 @@ menu {
|
|
|
3162
2139
|
if (name === 'glob' || name === 'search_files' || name === 'grep') {
|
|
3163
2140
|
const rawPattern = String(args.pattern || args.query || '').trim();
|
|
3164
2141
|
const pattern = rawPattern.toLowerCase();
|
|
2142
|
+
let contentPattern = null;
|
|
2143
|
+
if (rawPattern) {
|
|
2144
|
+
try {
|
|
2145
|
+
contentPattern = new RegExp(rawPattern, 'i');
|
|
2146
|
+
}
|
|
2147
|
+
catch {
|
|
2148
|
+
contentPattern = null;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
3165
2151
|
const matches = [];
|
|
3166
2152
|
const walk = (dir) => {
|
|
3167
2153
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -3176,8 +2162,11 @@ menu {
|
|
|
3176
2162
|
if (name !== 'glob' && pattern) {
|
|
3177
2163
|
try {
|
|
3178
2164
|
const content = fs.readFileSync(absolute, 'utf8');
|
|
3179
|
-
|
|
3180
|
-
|
|
2165
|
+
const lines = content.split('\n');
|
|
2166
|
+
const lineIndex = lines.findIndex((line) => (contentPattern ? contentPattern.test(line) : line.toLowerCase().includes(pattern)));
|
|
2167
|
+
if (relative.toLowerCase().includes(pattern.replace(/\*/g, '')) || lineIndex >= 0) {
|
|
2168
|
+
const displayLine = lineIndex >= 0 ? lineIndex + 1 : 1;
|
|
2169
|
+
matches.push(`${relative}:${displayLine}: ${lineIndex >= 0 ? lines[lineIndex].trim() : '(path match)'}`);
|
|
3181
2170
|
}
|
|
3182
2171
|
}
|
|
3183
2172
|
catch {
|
|
@@ -3193,9 +2182,19 @@ menu {
|
|
|
3193
2182
|
cwd: searchRoot,
|
|
3194
2183
|
nodir: true,
|
|
3195
2184
|
dot: false,
|
|
2185
|
+
follow: false,
|
|
3196
2186
|
ignore: ['**/node_modules/**', '**/.git/**'],
|
|
3197
2187
|
})
|
|
3198
2188
|
.map((entry) => String(entry).replace(/\\/g, '/'))
|
|
2189
|
+
.filter((entry) => {
|
|
2190
|
+
try {
|
|
2191
|
+
resolveWorkspacePath(searchRoot, entry);
|
|
2192
|
+
return true;
|
|
2193
|
+
}
|
|
2194
|
+
catch {
|
|
2195
|
+
return false;
|
|
2196
|
+
}
|
|
2197
|
+
})
|
|
3199
2198
|
.sort((left, right) => left.localeCompare(right));
|
|
3200
2199
|
return { success: true, output: globbed.slice(0, 200).join('\n') || '(no matches)' };
|
|
3201
2200
|
}
|
|
@@ -3224,9 +2223,9 @@ menu {
|
|
|
3224
2223
|
return { success: true, output: `Syntax check passed: ${target.relativePath}` };
|
|
3225
2224
|
}
|
|
3226
2225
|
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') {
|
|
3227
|
-
const {
|
|
2226
|
+
const { execFileSync } = await import('child_process');
|
|
3228
2227
|
try {
|
|
3229
|
-
|
|
2228
|
+
execFileSync(process.execPath, ['--check', target.absolutePath], { stdio: 'pipe', timeout: 10_000, windowsHide: true, env: safeChildProcessEnv() });
|
|
3230
2229
|
return { success: true, output: `Syntax check passed: ${target.relativePath}` };
|
|
3231
2230
|
}
|
|
3232
2231
|
catch (error) {
|
|
@@ -3242,36 +2241,73 @@ menu {
|
|
|
3242
2241
|
if (!command)
|
|
3243
2242
|
return { success: false, output: '', error: 'run_command requires command.' };
|
|
3244
2243
|
try {
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
2244
|
+
command = this.normalizePortableRunCommand(command, serverRoot);
|
|
2245
|
+
}
|
|
2246
|
+
catch (error) {
|
|
2247
|
+
return { success: false, output: '', error: error.message };
|
|
2248
|
+
}
|
|
2249
|
+
let processRequest;
|
|
2250
|
+
try {
|
|
2251
|
+
processRequest = parseSupportedProcess(command);
|
|
3248
2252
|
}
|
|
3249
2253
|
catch (error) {
|
|
3250
2254
|
return { success: false, output: '', error: error.message };
|
|
3251
2255
|
}
|
|
3252
|
-
|
|
3253
|
-
|
|
2256
|
+
let cwd = this.resolveRunCommandCwd(rootPath, args, serverRoot);
|
|
2257
|
+
// V3 models sometimes attach a virtual cwd such as /workspace even
|
|
2258
|
+
// when the named validation script is visibly rooted in the bound
|
|
2259
|
+
// client project. Prefer the real project root for an existing,
|
|
2260
|
+
// workspace-relative Node/Python entrypoint so external clients never
|
|
2261
|
+
// depend on a server-only filesystem alias.
|
|
2262
|
+
const processName = path.basename(processRequest.executable).toLowerCase();
|
|
2263
|
+
const rootScript = String(/^(?:node|python|python3)(?:\.exe)?$/.test(processName)
|
|
2264
|
+
? processRequest.args.find((arg) => !arg.startsWith('-')) || ''
|
|
2265
|
+
: '').replace(/\\/g, '/');
|
|
2266
|
+
if (rootScript && !path.isAbsolute(rootScript) && !rootScript.startsWith('../')) {
|
|
2267
|
+
const rootCandidate = path.resolve(rootPath, rootScript);
|
|
2268
|
+
const normalizedRoot = path.resolve(rootPath);
|
|
2269
|
+
if ((rootCandidate === normalizedRoot || rootCandidate.startsWith(`${normalizedRoot}${path.sep}`))
|
|
2270
|
+
&& fs.existsSync(rootCandidate)
|
|
2271
|
+
&& fs.statSync(rootCandidate).isFile()) {
|
|
2272
|
+
cwd = normalizedRoot;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const { execFile } = await import('child_process');
|
|
2276
|
+
// The V3 run_command contract expresses timeout in seconds (default
|
|
2277
|
+
// 30, maximum 300). child_process expects milliseconds. Passing the
|
|
2278
|
+
// model value through unchanged turned a normal timeout=30 request
|
|
2279
|
+
// into 30ms, which reliably killed real validators while tiny unit
|
|
2280
|
+
// commands appeared healthy.
|
|
2281
|
+
const requestedTimeoutSeconds = Number(args.timeout ?? 30);
|
|
2282
|
+
const timeoutSeconds = Number.isFinite(requestedTimeoutSeconds)
|
|
2283
|
+
? Math.min(300, Math.max(1, requestedTimeoutSeconds))
|
|
2284
|
+
: 30;
|
|
2285
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
3254
2286
|
return await new Promise((resolve) => {
|
|
3255
|
-
const timeout = Number(args.timeout || 30000);
|
|
3256
2287
|
const finish = (error, stdout, stderr) => {
|
|
3257
2288
|
const stdoutText = String(stdout || '');
|
|
3258
2289
|
const stderrText = String(stderr || '');
|
|
3259
2290
|
const combined = [stdoutText, stderrText].filter(Boolean).join('\n').slice(0, 12000);
|
|
2291
|
+
const failureDetail = error
|
|
2292
|
+
? [
|
|
2293
|
+
error.killed ? `Command timed out after ${timeoutSeconds}s` : String(error.message || error),
|
|
2294
|
+
combined,
|
|
2295
|
+
].filter(Boolean).join('\n').slice(0, 12000)
|
|
2296
|
+
: '';
|
|
3260
2297
|
resolve({
|
|
3261
2298
|
success: !error,
|
|
3262
2299
|
output: combined,
|
|
3263
|
-
error:
|
|
2300
|
+
error: failureDetail,
|
|
3264
2301
|
});
|
|
3265
2302
|
};
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
}
|
|
3274
|
-
exec(command, { cwd, timeout, maxBuffer: 1024 * 1024, env: process.env }, finish);
|
|
2303
|
+
execFile(processRequest.executable, processRequest.args, {
|
|
2304
|
+
cwd,
|
|
2305
|
+
timeout: timeoutMs,
|
|
2306
|
+
maxBuffer: 1024 * 1024,
|
|
2307
|
+
env: safeChildProcessEnv(),
|
|
2308
|
+
windowsHide: true,
|
|
2309
|
+
encoding: 'utf8',
|
|
2310
|
+
}, finish);
|
|
3275
2311
|
});
|
|
3276
2312
|
}
|
|
3277
2313
|
return { success: false, output: '', error: `Unsupported client tool: ${name}` };
|
|
@@ -3310,84 +2346,13 @@ menu {
|
|
|
3310
2346
|
}
|
|
3311
2347
|
}
|
|
3312
2348
|
writeV3AgentWorkspaceFile(rootPath, rawPath, content, sourceRoot) {
|
|
3313
|
-
|
|
3314
|
-
if (!relativePath) {
|
|
3315
|
-
return false;
|
|
3316
|
-
}
|
|
3317
|
-
const absolutePath = joinV3WorkspacePath(rootPath, relativePath);
|
|
3318
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
3319
|
-
if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
|
|
3320
|
-
this.logger.warn(`Refusing to write V3 file outside workspace: ${rawPath}`);
|
|
3321
|
-
return false;
|
|
3322
|
-
}
|
|
3323
|
-
try {
|
|
3324
|
-
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
3325
|
-
if (fs.existsSync(absolutePath)) {
|
|
3326
|
-
const existing = fs.readFileSync(absolutePath, 'utf8');
|
|
3327
|
-
if (existing === content) {
|
|
3328
|
-
return false;
|
|
3329
|
-
}
|
|
3330
|
-
}
|
|
3331
|
-
fs.writeFileSync(absolutePath, content, 'utf8');
|
|
3332
|
-
return true;
|
|
3333
|
-
}
|
|
3334
|
-
catch (error) {
|
|
3335
|
-
this.logger.warn(`Failed to write V3 file ${relativePath}: ${error.message}`);
|
|
3336
|
-
return false;
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
deleteV3AgentWorkspaceFile(rootPath, rawPath) {
|
|
3340
|
-
const relativePath = this.normalizeAgentWorkspaceRelativePath(rawPath, rootPath);
|
|
3341
|
-
if (!relativePath) {
|
|
3342
|
-
return false;
|
|
3343
|
-
}
|
|
3344
|
-
const absolutePath = joinV3WorkspacePath(rootPath, relativePath);
|
|
3345
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
3346
|
-
if (absolutePath !== resolvedRoot && !absolutePath.startsWith(resolvedRoot + path.sep)) {
|
|
3347
|
-
this.logger.warn(`Refusing to delete V3 file outside workspace: ${rawPath}`);
|
|
3348
|
-
return false;
|
|
3349
|
-
}
|
|
3350
|
-
try {
|
|
3351
|
-
if (fs.existsSync(absolutePath) && fs.statSync(absolutePath).isFile()) {
|
|
3352
|
-
fs.unlinkSync(absolutePath);
|
|
3353
|
-
return true;
|
|
3354
|
-
}
|
|
3355
|
-
}
|
|
3356
|
-
catch (error) {
|
|
3357
|
-
this.logger.warn(`Failed to delete V3 file ${relativePath}: ${error.message}`);
|
|
3358
|
-
}
|
|
3359
|
-
return false;
|
|
2349
|
+
return this.v3WorkspaceService.writeFile(rootPath, rawPath, content, sourceRoot);
|
|
3360
2350
|
}
|
|
3361
2351
|
recoverAgentWorkspaceFiles(context = {}, streamedFiles = {}, expectedFiles = []) {
|
|
3362
|
-
|
|
3363
|
-
if (!rootPath || Object.keys(streamedFiles).length === 0) {
|
|
3364
|
-
return;
|
|
3365
|
-
}
|
|
3366
|
-
if (!fs.existsSync(rootPath)) {
|
|
3367
|
-
try {
|
|
3368
|
-
fs.mkdirSync(rootPath, { recursive: true });
|
|
3369
|
-
}
|
|
3370
|
-
catch {
|
|
3371
|
-
return;
|
|
3372
|
-
}
|
|
3373
|
-
}
|
|
3374
|
-
const targets = expectedFiles.length > 0
|
|
3375
|
-
? Array.from(new Set([...expectedFiles, ...Object.keys(streamedFiles)]))
|
|
3376
|
-
: Object.keys(streamedFiles);
|
|
3377
|
-
for (const targetPath of targets) {
|
|
3378
|
-
const relativePath = this.normalizeAgentWorkspaceRelativePath(targetPath, rootPath);
|
|
3379
|
-
if (!relativePath) {
|
|
3380
|
-
continue;
|
|
3381
|
-
}
|
|
3382
|
-
const content = streamedFiles[relativePath];
|
|
3383
|
-
if (typeof content !== 'string') {
|
|
3384
|
-
continue;
|
|
3385
|
-
}
|
|
3386
|
-
this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath);
|
|
3387
|
-
}
|
|
2352
|
+
this.v3WorkspaceService.recover(context, streamedFiles, expectedFiles);
|
|
3388
2353
|
}
|
|
3389
2354
|
normalizeAgentWorkspaceRelativePath(rawPath, rootPath) {
|
|
3390
|
-
return
|
|
2355
|
+
return this.v3WorkspaceService.normalizeRelativePath(rawPath, rootPath);
|
|
3391
2356
|
}
|
|
3392
2357
|
async ensureAgentFrontendPolish(message = '', context = {}) {
|
|
3393
2358
|
const rootPath = this.resolveAgentTargetPath(context);
|
|
@@ -3522,14 +2487,23 @@ menu {
|
|
|
3522
2487
|
if (!candidate || /^https?:\/\//i.test(candidate) || /^data:/i.test(candidate) || candidate.startsWith('#')) {
|
|
3523
2488
|
return '';
|
|
3524
2489
|
}
|
|
3525
|
-
|
|
2490
|
+
const assetReference = candidate.split('?')[0].split('#')[0];
|
|
2491
|
+
if (/^\/{2,}/.test(assetReference))
|
|
2492
|
+
return '';
|
|
2493
|
+
const relative = assetReference.replace(/^\//, '');
|
|
2494
|
+
try {
|
|
2495
|
+
return assertSafeRelativePath(relative);
|
|
2496
|
+
}
|
|
2497
|
+
catch {
|
|
2498
|
+
return '';
|
|
2499
|
+
}
|
|
3526
2500
|
};
|
|
3527
2501
|
const ensureTextAsset = (relativePath, content) => {
|
|
3528
2502
|
const normalized = localAssetPath(relativePath);
|
|
3529
2503
|
if (!normalized) {
|
|
3530
2504
|
return '';
|
|
3531
2505
|
}
|
|
3532
|
-
const absolutePath =
|
|
2506
|
+
const absolutePath = resolveWorkspacePath(rootPath, normalized, { allowMissing: true });
|
|
3533
2507
|
if (!fs.existsSync(absolutePath)) {
|
|
3534
2508
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
3535
2509
|
fs.writeFileSync(absolutePath, content, 'utf8');
|
|
@@ -3542,8 +2516,8 @@ menu {
|
|
|
3542
2516
|
const jsRefs = Array.from(html.matchAll(/<script[^>]+src=["']([^"']+\.(?:js|mjs)(?:\?[^"']*)?)["']/gi))
|
|
3543
2517
|
.map((match) => localAssetPath(match[1]))
|
|
3544
2518
|
.filter(Boolean);
|
|
3545
|
-
let cssPath = cssRefs.map((ref) =>
|
|
3546
|
-
let jsPath = jsRefs.map((ref) =>
|
|
2519
|
+
let cssPath = cssRefs.map((ref) => resolveWorkspacePath(rootPath, ref, { allowMissing: true })).find((candidate) => fs.existsSync(candidate)) || '';
|
|
2520
|
+
let jsPath = jsRefs.map((ref) => resolveWorkspacePath(rootPath, ref, { allowMissing: true })).find((candidate) => fs.existsSync(candidate)) || '';
|
|
3547
2521
|
const cssTemplate = this.buildFallbackFrontendCss(surfaceLabel);
|
|
3548
2522
|
const jsTemplate = this.buildFallbackFrontendJs(surfaceLabel);
|
|
3549
2523
|
if (!cssPath && cssRefs.length > 0) {
|
|
@@ -3721,8 +2695,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3721
2695
|
if (!candidate || /^https?:\/\//i.test(candidate) || /^data:/i.test(candidate) || candidate.startsWith('#')) {
|
|
3722
2696
|
return true;
|
|
3723
2697
|
}
|
|
3724
|
-
const
|
|
3725
|
-
|
|
2698
|
+
const assetReference = candidate.split('?')[0].split('#')[0];
|
|
2699
|
+
if (/^\/{2,}/.test(assetReference))
|
|
2700
|
+
return false;
|
|
2701
|
+
const sanitized = assetReference.replace(/^\//, '');
|
|
2702
|
+
if (!sanitized)
|
|
2703
|
+
return true;
|
|
2704
|
+
try {
|
|
2705
|
+
return fs.existsSync(resolveWorkspacePath(rootPath, sanitized, { allowMissing: true }));
|
|
2706
|
+
}
|
|
2707
|
+
catch {
|
|
2708
|
+
return false;
|
|
2709
|
+
}
|
|
3726
2710
|
};
|
|
3727
2711
|
const repairedHtml = html.replace(/(<img\b[^>]*\ssrc=["'])([^"']+)(["'][^>]*>)/gi, (match, prefix, assetPath, suffix) => {
|
|
3728
2712
|
if (hasLocalAsset(assetPath)) {
|
|
@@ -3733,7 +2717,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3733
2717
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 800"><defs><linearGradient id="heroGradient" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#0f172a"/><stop offset="1" stop-color="#2563eb"/></linearGradient></defs><rect width="1200" height="800" rx="40" ry="40" fill="url(#heroGradient)"/><circle cx="220" cy="210" r="130" fill="rgba(255,255,255,0.08)"/><circle cx="980" cy="600" r="170" fill="rgba(255,255,255,0.12)"/><text x="84" y="700" fill="#ffffff" font-family="Arial, sans-serif" font-size="64" font-weight="700">${safeLabel}</text></svg>`;
|
|
3734
2718
|
return `${prefix}data:image/svg+xml;utf8,${encodeURIComponent(svg)}${suffix}`;
|
|
3735
2719
|
});
|
|
3736
|
-
const repairedCss = css.replace(/url\((["']?)([^)"']+)\1\)/gi, (match,
|
|
2720
|
+
const repairedCss = css.replace(/url\((["']?)([^)"']+)\1\)/gi, (match, _quote, assetPath) => {
|
|
3737
2721
|
if (hasLocalAsset(assetPath)) {
|
|
3738
2722
|
return match;
|
|
3739
2723
|
}
|
|
@@ -3920,12 +2904,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3920
2904
|
: {};
|
|
3921
2905
|
return { ...streamed, ...clientMutations };
|
|
3922
2906
|
}
|
|
3923
|
-
finalizeV3AgentWorkflowResponse(data, response) {
|
|
3924
|
-
|
|
2907
|
+
finalizeV3AgentWorkflowResponse(data, response, journal = null) {
|
|
2908
|
+
let changedFiles = this.resolveAgentChangedFiles(data);
|
|
2909
|
+
const previewGate = response.metadata?.previewGate;
|
|
2910
|
+
let mutation;
|
|
2911
|
+
if (journal) {
|
|
2912
|
+
const proofSucceeded = previewGate?.required === true
|
|
2913
|
+
? previewGate.passed === true && previewGate.skipped !== true
|
|
2914
|
+
: true;
|
|
2915
|
+
mutation = finalizeMutationTransaction(journal, proofSucceeded);
|
|
2916
|
+
if (!proofSucceeded)
|
|
2917
|
+
changedFiles = {};
|
|
2918
|
+
}
|
|
3925
2919
|
this.activeV3StreamedFiles = null;
|
|
3926
|
-
this.activeV3StreamContext = null;
|
|
3927
2920
|
return {
|
|
3928
2921
|
...response,
|
|
2922
|
+
metadata: {
|
|
2923
|
+
...(response.metadata || {}),
|
|
2924
|
+
...(journal ? { operationId: journal.operationId } : {}),
|
|
2925
|
+
...(mutation ? { mutation, partialMutation: mutation.partialMutation } : {}),
|
|
2926
|
+
...(previewGate && mutation ? {
|
|
2927
|
+
previewGate: {
|
|
2928
|
+
...previewGate,
|
|
2929
|
+
operationId: journal?.operationId,
|
|
2930
|
+
mutation,
|
|
2931
|
+
partialMutation: mutation.partialMutation,
|
|
2932
|
+
},
|
|
2933
|
+
} : {}),
|
|
2934
|
+
},
|
|
3929
2935
|
changedFiles,
|
|
3930
2936
|
};
|
|
3931
2937
|
}
|
|
@@ -3944,7 +2950,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3944
2950
|
let serverWorkspaceRoot = null;
|
|
3945
2951
|
const streamedFiles = {};
|
|
3946
2952
|
this.activeV3StreamedFiles = streamedFiles;
|
|
3947
|
-
this.activeV3StreamContext = context;
|
|
3948
2953
|
context.__v3StreamedFiles = streamedFiles;
|
|
3949
2954
|
const idleTimeoutMs = (() => {
|
|
3950
2955
|
const base = context.agentIdleTimeoutMs ?? DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS;
|
|
@@ -4205,6 +3210,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4205
3210
|
}
|
|
4206
3211
|
async runV3AgentWorkflow(message, context = {}) {
|
|
4207
3212
|
const executionContext = await this.bindExecutionContext(context);
|
|
3213
|
+
const mutationRoot = this.resolveAgentTargetPath(executionContext);
|
|
3214
|
+
let mutationJournal = null;
|
|
3215
|
+
if (mutationRoot && fs.existsSync(mutationRoot) && fs.statSync(mutationRoot).isDirectory()) {
|
|
3216
|
+
mutationJournal = new MutationJournal(mutationRoot, executionContext.operationId);
|
|
3217
|
+
executionContext.operationId = mutationJournal.operationId;
|
|
3218
|
+
executionContext.__mutationJournal = mutationJournal;
|
|
3219
|
+
}
|
|
4208
3220
|
const requestedTimeoutMs = Number(executionContext.agentTimeoutMs ?? DEFAULT_V3_AGENT_TIMEOUT_MS);
|
|
4209
3221
|
const baseTimeoutMs = Number.isFinite(requestedTimeoutMs) && requestedTimeoutMs > 0 ? requestedTimeoutMs : 0;
|
|
4210
3222
|
const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
|
|
@@ -4244,11 +3256,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4244
3256
|
workspace_files: useRelaxedAttempt ? undefined : workspaceFiles,
|
|
4245
3257
|
context_id: contextIdOverride ?? requestExecutionContext.contextId,
|
|
4246
3258
|
mcp_context_id: useRelaxedAttempt ? null : requestExecutionContext.mcpContextId || null,
|
|
3259
|
+
operation_id: executionContext.operationId || undefined,
|
|
4247
3260
|
stream: true,
|
|
4248
3261
|
});
|
|
4249
3262
|
for (const baseUrl of this.getV3AgentBaseUrls(preferLocalV3)) {
|
|
4250
3263
|
let contextIdOverride;
|
|
4251
|
-
const authPreflight = await this.runV3AgentAuthPreflight(baseUrl,
|
|
3264
|
+
const authPreflight = await this.runV3AgentAuthPreflight(baseUrl, requestExecutionContext);
|
|
4252
3265
|
if (!authPreflight.ok) {
|
|
4253
3266
|
errors.push(`V3 auth preflight ${authPreflight.status} [${authPreflight.endpoint}]: ${authPreflight.reason || 'Unauthorized'}`);
|
|
4254
3267
|
continue;
|
|
@@ -4299,10 +3312,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4299
3312
|
const continueTimeoutId = timeoutMs > 0 ? setTimeout(() => continueController.abort(), timeoutMs) : null;
|
|
4300
3313
|
try {
|
|
4301
3314
|
const continueHeaders = await this.getV3AgentHeaders();
|
|
4302
|
-
|
|
3315
|
+
if (executionContext.operationId) {
|
|
3316
|
+
continueHeaders['Idempotency-Key'] = String(executionContext.operationId);
|
|
3317
|
+
continueHeaders['X-Vigthoria-Operation-Id'] = String(executionContext.operationId);
|
|
3318
|
+
}
|
|
3319
|
+
const continueResponse = await this.fetch(this.getV3AgentContinueUrl(baseUrl), {
|
|
4303
3320
|
method: 'POST',
|
|
4304
3321
|
headers: { ...continueHeaders, 'Content-Type': 'application/json' },
|
|
4305
|
-
body: JSON.stringify(continueBody),
|
|
3322
|
+
body: JSON.stringify(scanOutboundContext(continueBody).value),
|
|
4306
3323
|
signal: continueController.signal,
|
|
4307
3324
|
});
|
|
4308
3325
|
if (!continueResponse.ok) {
|
|
@@ -4331,17 +3348,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4331
3348
|
backendUrl: baseUrl,
|
|
4332
3349
|
partial: continuationData.checkpointed === true,
|
|
4333
3350
|
metadata: { source: 'v3-agent', mode: 'agent', contextId: finalContextId, continuations, previewGate },
|
|
4334
|
-
});
|
|
3351
|
+
}, mutationJournal);
|
|
4335
3352
|
}
|
|
4336
3353
|
const contextId = data.context_id || response.headers.get('x-context-id') || requestExecutionContext.contextId || null;
|
|
4337
3354
|
const mcpContextId = response.headers.get('x-mcp-context-id') || requestExecutionContext.mcpContextId || null;
|
|
4338
3355
|
this.recoverAgentWorkspaceFiles(executionContext, data.files || {}, expectedFiles);
|
|
4339
3356
|
if (data.terminal_error) {
|
|
4340
3357
|
// A pre-existing local project is not proof that this run produced
|
|
4341
|
-
// valid output.
|
|
4342
|
-
//
|
|
3358
|
+
// valid output. A terminal planner/executor error fails proof and
|
|
3359
|
+
// causes the operation journal to roll back streamed mutations.
|
|
4343
3360
|
const failedPreviewGate = {
|
|
4344
|
-
required:
|
|
3361
|
+
required: true,
|
|
4345
3362
|
passed: false,
|
|
4346
3363
|
skipped: true,
|
|
4347
3364
|
error: 'Agent execution failed before preview validation.',
|
|
@@ -4360,22 +3377,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4360
3377
|
previewGate: failedPreviewGate,
|
|
4361
3378
|
workflowError: data.terminal_error,
|
|
4362
3379
|
},
|
|
4363
|
-
});
|
|
3380
|
+
}, mutationJournal);
|
|
4364
3381
|
}
|
|
4365
3382
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles });
|
|
4366
3383
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
4367
3384
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
4368
|
-
if (previewGate.required && previewGate.passed !== true && !this.hasAgentWorkspaceOutput(executionContext)) {
|
|
4369
|
-
errors.push(`${baseUrl}: workflow rejected the result - ${previewGate.error || 'Workspace changes were not fully validated.'}`);
|
|
4370
|
-
continue;
|
|
4371
|
-
}
|
|
4372
3385
|
return this.finalizeV3AgentWorkflowResponse(data, {
|
|
4373
3386
|
content: this.formatV3AgentResponse(data),
|
|
4374
3387
|
taskId: data.task_id || null,
|
|
4375
3388
|
contextId,
|
|
4376
3389
|
backendUrl: baseUrl,
|
|
4377
3390
|
metadata: { source: 'v3-agent', mode: 'agent', contextId, mcpContextId, previewGate },
|
|
4378
|
-
});
|
|
3391
|
+
}, mutationJournal);
|
|
4379
3392
|
}
|
|
4380
3393
|
catch (error) {
|
|
4381
3394
|
if (error && error.name === 'AbortError' && error.partialData && this.hasAgentWorkspaceOutput(executionContext)) {
|
|
@@ -4390,7 +3403,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4390
3403
|
backendUrl: baseUrl,
|
|
4391
3404
|
partial: true,
|
|
4392
3405
|
metadata: { source: 'v3-agent', mode: 'agent', partial: true, contextId: error.partialData.context_id || requestExecutionContext.contextId || executionContext.contextId || null, mcpContextId: requestExecutionContext.mcpContextId || executionContext.mcpContextId || null, previewGate },
|
|
4393
|
-
});
|
|
3406
|
+
}, mutationJournal);
|
|
3407
|
+
}
|
|
3408
|
+
if (mutationJournal?.hasMutations()) {
|
|
3409
|
+
const mutation = mutationJournal.rollback();
|
|
3410
|
+
mutationJournal = null;
|
|
3411
|
+
this.activeV3StreamedFiles = null;
|
|
3412
|
+
const ambiguousError = new Error(`V3 mutation outcome is ambiguous for operation ${mutation.operationId}; workspace rollback was attempted and no alternate backend retry was allowed.`);
|
|
3413
|
+
ambiguousError.operationId = mutation.operationId;
|
|
3414
|
+
ambiguousError.mutation = mutation;
|
|
3415
|
+
ambiguousError.partialMutation = mutation.partialMutation;
|
|
3416
|
+
throw ambiguousError;
|
|
4394
3417
|
}
|
|
4395
3418
|
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4396
3419
|
}
|
|
@@ -4418,7 +3441,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4418
3441
|
if (onlyUnauthorizedErrors && usingStoredConfigToken) {
|
|
4419
3442
|
const gatewayTokenCheck = await this.validateToken({ allowNetworkFailOpen: true, enforceTokenShape: true });
|
|
4420
3443
|
if (!gatewayTokenCheck.valid) {
|
|
4421
|
-
this.
|
|
3444
|
+
this.authSession.clearLocalCredentials();
|
|
4422
3445
|
throw new Error('V3 agent authentication failed. The stored CLI login token is invalid or expired. Run vigthoria login again.');
|
|
4423
3446
|
}
|
|
4424
3447
|
const rejectedEndpoints = Array.from(new Set(errors
|
|
@@ -4440,17 +3463,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4440
3463
|
await this.waitForAgentWorkspaceSettle(executionContext, { expectedFiles: ['index.html', 'styles.css', 'scripts.js'] });
|
|
4441
3464
|
await this.ensureAgentFrontendPolish(message, executionContext);
|
|
4442
3465
|
const previewGate = await this.runTemplateServicePreviewGate(message, executionContext);
|
|
4443
|
-
return {
|
|
3466
|
+
return this.finalizeV3AgentWorkflowResponse({}, {
|
|
4444
3467
|
content: `Recovered a local SaaS workspace scaffold for ${appName} after repeated V3 materialization failures.`,
|
|
4445
3468
|
taskId: null,
|
|
4446
3469
|
contextId: executionContext.contextId || null,
|
|
4447
3470
|
backendUrl: 'local-emergency-scaffold',
|
|
4448
3471
|
partial: true,
|
|
4449
3472
|
metadata: { source: 'v3-agent-emergency-scaffold', mode: 'agent', previewGate, emergencyScaffold: true },
|
|
4450
|
-
};
|
|
3473
|
+
}, mutationJournal);
|
|
4451
3474
|
}
|
|
4452
3475
|
}
|
|
4453
|
-
|
|
3476
|
+
const finalError = new Error(errors.join(' | '));
|
|
3477
|
+
if (mutationJournal) {
|
|
3478
|
+
const mutation = mutationJournal.rollback();
|
|
3479
|
+
mutationJournal = null;
|
|
3480
|
+
finalError.operationId = mutation.operationId;
|
|
3481
|
+
finalError.mutation = mutation;
|
|
3482
|
+
finalError.partialMutation = mutation.partialMutation;
|
|
3483
|
+
}
|
|
3484
|
+
throw finalError;
|
|
4454
3485
|
}
|
|
4455
3486
|
async startV3BackgroundJob(message, context = {}) {
|
|
4456
3487
|
await this.ensureV3ServiceKey();
|
|
@@ -4470,1674 +3501,181 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4470
3501
|
...executionContext,
|
|
4471
3502
|
backgroundJob: true,
|
|
4472
3503
|
clientToolExecution: false,
|
|
4473
|
-
workspaceFilesOutOfBand: !!workspaceFiles,
|
|
4474
|
-
}),
|
|
4475
|
-
workspace_files: workspaceFiles,
|
|
4476
|
-
stream: false,
|
|
4477
|
-
model: resolvedModel,
|
|
4478
|
-
mcp_context_id: executionContext.mcpContextId || undefined,
|
|
4479
|
-
};
|
|
4480
|
-
|
|
4481
|
-
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
4482
|
-
try {
|
|
4483
|
-
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl), {
|
|
4484
|
-
method: 'POST',
|
|
4485
|
-
headers: await this.getV3AgentHeaders(),
|
|
4486
|
-
body: JSON.stringify(body),
|
|
4487
|
-
});
|
|
4488
|
-
if (!response.ok) {
|
|
4489
|
-
errors.push(`${baseUrl}: ${response.status} ${await response.text().catch(() => '')}`);
|
|
4490
|
-
continue;
|
|
4491
|
-
}
|
|
4492
|
-
return await response.json();
|
|
4493
|
-
}
|
|
4494
|
-
catch (error) {
|
|
4495
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
4498
|
-
throw new Error(`Unable to start background job: ${errors.join('; ')}`);
|
|
4499
|
-
}
|
|
4500
|
-
async listV3BackgroundJobs(limit = 20) {
|
|
4501
|
-
await this.ensureV3ServiceKey();
|
|
4502
|
-
const errors = [];
|
|
4503
|
-
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
4504
|
-
try {
|
|
4505
|
-
const response = await fetch(`${this.getV3AgentBackgroundUrl(baseUrl)}?limit=${encodeURIComponent(String(limit))}`, {
|
|
4506
|
-
method: 'GET',
|
|
4507
|
-
headers: await this.getV3AgentHeaders(),
|
|
4508
|
-
});
|
|
4509
|
-
if (!response.ok) {
|
|
4510
|
-
errors.push(`${baseUrl}: ${response.status}`);
|
|
4511
|
-
continue;
|
|
4512
|
-
}
|
|
4513
|
-
const data = await response.json();
|
|
4514
|
-
return Array.isArray(data.jobs) ? data.jobs : [];
|
|
4515
|
-
}
|
|
4516
|
-
catch (error) {
|
|
4517
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4518
|
-
}
|
|
4519
|
-
}
|
|
4520
|
-
throw new Error(`Unable to list background jobs: ${errors.join('; ')}`);
|
|
4521
|
-
}
|
|
4522
|
-
async getV3BackgroundJob(jobId) {
|
|
4523
|
-
await this.ensureV3ServiceKey();
|
|
4524
|
-
const errors = [];
|
|
4525
|
-
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
4526
|
-
try {
|
|
4527
|
-
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, encodeURIComponent(jobId)), {
|
|
4528
|
-
method: 'GET',
|
|
4529
|
-
headers: await this.getV3AgentHeaders(),
|
|
4530
|
-
});
|
|
4531
|
-
if (!response.ok) {
|
|
4532
|
-
errors.push(`${baseUrl}: ${response.status}`);
|
|
4533
|
-
continue;
|
|
4534
|
-
}
|
|
4535
|
-
return await response.json();
|
|
4536
|
-
}
|
|
4537
|
-
catch (error) {
|
|
4538
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4539
|
-
}
|
|
4540
|
-
}
|
|
4541
|
-
throw new Error(`Unable to get background job: ${errors.join('; ')}`);
|
|
4542
|
-
}
|
|
4543
|
-
async cancelV3BackgroundJob(jobId) {
|
|
4544
|
-
await this.ensureV3ServiceKey();
|
|
4545
|
-
const errors = [];
|
|
4546
|
-
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
4547
|
-
try {
|
|
4548
|
-
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/cancel`), {
|
|
4549
|
-
method: 'POST',
|
|
4550
|
-
headers: await this.getV3AgentHeaders(),
|
|
4551
|
-
});
|
|
4552
|
-
if (!response.ok) {
|
|
4553
|
-
errors.push(`${baseUrl}: ${response.status}`);
|
|
4554
|
-
continue;
|
|
4555
|
-
}
|
|
4556
|
-
return await response.json();
|
|
4557
|
-
}
|
|
4558
|
-
catch (error) {
|
|
4559
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4560
|
-
}
|
|
4561
|
-
}
|
|
4562
|
-
throw new Error(`Unable to cancel background job: ${errors.join('; ')}`);
|
|
4563
|
-
}
|
|
4564
|
-
async resolveV3BackgroundApproval(jobId, approvalId, approved, allowMode = 'once') {
|
|
4565
|
-
await this.ensureV3ServiceKey();
|
|
4566
|
-
const errors = [];
|
|
4567
|
-
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
4568
|
-
try {
|
|
4569
|
-
const response = await fetch(this.getV3AgentApprovalUrl(baseUrl), {
|
|
4570
|
-
method: 'POST',
|
|
4571
|
-
headers: await this.getV3AgentHeaders(),
|
|
4572
|
-
body: JSON.stringify({
|
|
4573
|
-
context_id: jobId,
|
|
4574
|
-
approval_id: approvalId,
|
|
4575
|
-
approved,
|
|
4576
|
-
allow_mode: allowMode,
|
|
4577
|
-
}),
|
|
4578
|
-
});
|
|
4579
|
-
if (!response.ok) {
|
|
4580
|
-
errors.push(`${baseUrl}: ${response.status}`);
|
|
4581
|
-
continue;
|
|
4582
|
-
}
|
|
4583
|
-
return await response.json();
|
|
4584
|
-
}
|
|
4585
|
-
catch (error) {
|
|
4586
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4587
|
-
}
|
|
4588
|
-
}
|
|
4589
|
-
throw new Error(`Unable to resolve background approval: ${errors.join('; ')}`);
|
|
4590
|
-
}
|
|
4591
|
-
async getV3BackgroundJobFiles(jobId) {
|
|
4592
|
-
await this.ensureV3ServiceKey();
|
|
4593
|
-
const errors = [];
|
|
4594
|
-
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
4595
|
-
try {
|
|
4596
|
-
const response = await fetch(this.getV3AgentBackgroundUrl(baseUrl, `${encodeURIComponent(jobId)}/files`), {
|
|
4597
|
-
method: 'GET',
|
|
4598
|
-
headers: await this.getV3AgentHeaders(),
|
|
4599
|
-
});
|
|
4600
|
-
if (!response.ok) {
|
|
4601
|
-
errors.push(`${baseUrl}: ${response.status}`);
|
|
4602
|
-
continue;
|
|
4603
|
-
}
|
|
4604
|
-
return await response.json();
|
|
4605
|
-
}
|
|
4606
|
-
catch (error) {
|
|
4607
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4608
|
-
}
|
|
4609
|
-
}
|
|
4610
|
-
throw new Error(`Unable to fetch background job files: ${errors.join('; ')}`);
|
|
4611
|
-
}
|
|
4612
|
-
async applyV3BackgroundJobFiles(jobId, context = {}) {
|
|
4613
|
-
const [data, job] = await Promise.all([
|
|
4614
|
-
this.getV3BackgroundJobFiles(jobId),
|
|
4615
|
-
this.getV3BackgroundJob(jobId),
|
|
4616
|
-
]);
|
|
4617
|
-
if (job?.status !== 'completed' || job?.result?.success === false || job?.result?.type === 'error') {
|
|
4618
|
-
throw new Error(`Background job ${jobId} is not a successful completed job and cannot be applied.`);
|
|
4619
|
-
}
|
|
4620
|
-
const mutatedPaths = new Set();
|
|
4621
|
-
const addMutatedPath = (value) => {
|
|
4622
|
-
const normalized = this.normalizeWorkspaceRelativePath(String(value || ''));
|
|
4623
|
-
if (!normalized || path.isAbsolute(normalized) || normalized.split('/').includes('..'))
|
|
4624
|
-
return;
|
|
4625
|
-
mutatedPaths.add(normalized);
|
|
4626
|
-
};
|
|
4627
|
-
for (const filePath of (data.confirmed_mutations || []))
|
|
4628
|
-
addMutatedPath(filePath);
|
|
4629
|
-
for (const event of (Array.isArray(job?.events) ? job.events : [])) {
|
|
4630
|
-
if (event?.type === 'file_mutation' && event?.action !== 'delete')
|
|
4631
|
-
addMutatedPath(event.path);
|
|
4632
|
-
if (event?.type === 'executor_complete') {
|
|
4633
|
-
let summary = event.summary;
|
|
4634
|
-
if (typeof summary === 'string') {
|
|
4635
|
-
try {
|
|
4636
|
-
summary = JSON.parse(summary);
|
|
4637
|
-
}
|
|
4638
|
-
catch {
|
|
4639
|
-
summary = null;
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
if (summary?.status === 'completed') {
|
|
4643
|
-
for (const filePath of [...(summary.changed_files || []), ...(summary.created_files || [])]) {
|
|
4644
|
-
addMutatedPath(filePath);
|
|
4645
|
-
}
|
|
4646
|
-
}
|
|
4647
|
-
}
|
|
4648
|
-
}
|
|
4649
|
-
if (mutatedPaths.size === 0) {
|
|
4650
|
-
throw new Error(`Background job ${jobId} has no confirmed successful file mutations to apply.`);
|
|
4651
|
-
}
|
|
4652
|
-
const rootPath = this.resolveAgentTargetPath({
|
|
4653
|
-
...context,
|
|
4654
|
-
workspacePath: context.workspacePath || data.local_workspace_path,
|
|
4655
|
-
projectPath: context.projectPath || data.local_workspace_path,
|
|
4656
|
-
targetPath: context.targetPath || data.local_workspace_path,
|
|
4657
|
-
});
|
|
4658
|
-
if (!rootPath) {
|
|
4659
|
-
throw new Error('No local workspace path available for applying background job files.');
|
|
4660
|
-
}
|
|
4661
|
-
const applied = [];
|
|
4662
|
-
const skipped = [];
|
|
4663
|
-
for (const [relativePath, content] of Object.entries(data.files || {})) {
|
|
4664
|
-
const normalizedRelativePath = this.normalizeWorkspaceRelativePath(relativePath);
|
|
4665
|
-
if (!mutatedPaths.has(normalizedRelativePath)) {
|
|
4666
|
-
skipped.push(relativePath);
|
|
4667
|
-
continue;
|
|
4668
|
-
}
|
|
4669
|
-
if (typeof content !== 'string') {
|
|
4670
|
-
skipped.push(relativePath);
|
|
4671
|
-
continue;
|
|
4672
|
-
}
|
|
4673
|
-
if (this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath)) {
|
|
4674
|
-
applied.push(relativePath);
|
|
4675
|
-
}
|
|
4676
|
-
else {
|
|
4677
|
-
skipped.push(relativePath);
|
|
4678
|
-
}
|
|
4679
|
-
}
|
|
4680
|
-
return { applied, skipped, status: data.status };
|
|
4681
|
-
}
|
|
4682
|
-
formatOperatorResponse(data = {}) {
|
|
4683
|
-
// If the server returned a direct answer field, prefer it (for lookup tasks)
|
|
4684
|
-
if (typeof data.answer === 'string' && data.answer.trim()) {
|
|
4685
|
-
return data.answer.trim();
|
|
4686
|
-
}
|
|
4687
|
-
if (typeof data.result === 'string' && data.result.trim()) {
|
|
4688
|
-
return data.result.trim();
|
|
4689
|
-
}
|
|
4690
|
-
const lines = [];
|
|
4691
|
-
if (data.summary) {
|
|
4692
|
-
lines.push(String(data.summary).trim());
|
|
4693
|
-
}
|
|
4694
|
-
if (Array.isArray(data.completed_agents) && data.completed_agents.length > 0) {
|
|
4695
|
-
lines.push(`Completed agents: ${data.completed_agents.join(', ')}`);
|
|
4696
|
-
}
|
|
4697
|
-
if (data.details) {
|
|
4698
|
-
const detailText = JSON.stringify(data.details, null, 2);
|
|
4699
|
-
if (detailText && detailText !== '{}') {
|
|
4700
|
-
lines.push(detailText.length > 12000 ? `${detailText.slice(0, 12000)}\n\n[Operator output truncated]` : detailText);
|
|
4701
|
-
}
|
|
4702
|
-
}
|
|
4703
|
-
return lines.filter(Boolean).join('\n\n').trim() || 'Operator workflow completed.';
|
|
4704
|
-
}
|
|
4705
|
-
async runOperatorWorkflow(message, context = {}) {
|
|
4706
|
-
const executionContext = await this.bindExecutionContext(context);
|
|
4707
|
-
const requestedOperatorTimeoutMs = Number(context.operatorTimeoutMs ?? DEFAULT_OPERATOR_TIMEOUT_MS);
|
|
4708
|
-
const timeoutMs = Number.isFinite(requestedOperatorTimeoutMs) && requestedOperatorTimeoutMs > 0
|
|
4709
|
-
? requestedOperatorTimeoutMs
|
|
4710
|
-
: 0;
|
|
4711
|
-
const errors = [];
|
|
4712
|
-
const authToken = this.config.get('authToken');
|
|
4713
|
-
// Collect a lightweight workspace file listing so the operator can
|
|
4714
|
-
// ground its analysis in real files rather than returning files_analyzed: 0.
|
|
4715
|
-
const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
|
|
4716
|
-
const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
4717
|
-
const requestedModel = String(executionContext.model || 'code');
|
|
4718
|
-
const resolvedModel = this.resolveModelId(requestedModel);
|
|
4719
|
-
for (const baseUrl of this.getOperatorBaseUrls()) {
|
|
4720
|
-
const controller = new AbortController();
|
|
4721
|
-
const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
4722
|
-
try {
|
|
4723
|
-
const response = await fetch(this.getOperatorStreamUrl(baseUrl), {
|
|
4724
|
-
method: 'POST',
|
|
4725
|
-
headers: {
|
|
4726
|
-
'Content-Type': 'application/json',
|
|
4727
|
-
Accept: 'text/event-stream',
|
|
4728
|
-
...(executionContext.mcpContextId ? { 'X-MCP-Context-Id': String(executionContext.mcpContextId) } : {}),
|
|
4729
|
-
...(authToken ? {
|
|
4730
|
-
Authorization: `Bearer ${authToken}`,
|
|
4731
|
-
Cookie: `vigthoria-auth-token=${authToken}`,
|
|
4732
|
-
} : {}),
|
|
4733
|
-
},
|
|
4734
|
-
body: JSON.stringify({
|
|
4735
|
-
prompt: message,
|
|
4736
|
-
task: message,
|
|
4737
|
-
raw_prompt: executionContext.rawPrompt || null,
|
|
4738
|
-
model: requestedModel,
|
|
4739
|
-
preferred_model: resolvedModel,
|
|
4740
|
-
context_id: executionContext.contextId,
|
|
4741
|
-
trace_id: executionContext.traceId,
|
|
4742
|
-
mcp_context_id: executionContext.mcpContextId || null,
|
|
4743
|
-
context: {
|
|
4744
|
-
workspace: { path: workspacePath },
|
|
4745
|
-
workspace_path: workspacePath,
|
|
4746
|
-
workspace_summary: workspaceSummary,
|
|
4747
|
-
model: resolvedModel,
|
|
4748
|
-
history: executionContext.history || [],
|
|
4749
|
-
executionSurface: executionContext.executionSurface || 'cli',
|
|
4750
|
-
clientSurface: executionContext.clientSurface || 'cli',
|
|
4751
|
-
contextId: executionContext.contextId,
|
|
4752
|
-
traceId: executionContext.traceId,
|
|
4753
|
-
mcpContextId: executionContext.mcpContextId || null,
|
|
4754
|
-
mcp_context_id: executionContext.mcpContextId || null,
|
|
4755
|
-
rawPrompt: executionContext.rawPrompt || null,
|
|
4756
|
-
requestStartedAt: executionContext.requestStartedAt,
|
|
4757
|
-
vigthoriaBrain: executionContext.vigthoriaBrain || null,
|
|
4758
|
-
vigthoria_brain: executionContext.vigthoriaBrain || null,
|
|
4759
|
-
},
|
|
4760
|
-
workflow_type: executionContext.workflowType || 'full',
|
|
4761
|
-
options: {
|
|
4762
|
-
stream: true,
|
|
4763
|
-
save_to_vigflow: executionContext.savePlanToVigFlow === true,
|
|
4764
|
-
},
|
|
4765
|
-
}),
|
|
4766
|
-
signal: controller.signal,
|
|
4767
|
-
});
|
|
4768
|
-
if (!response.ok) {
|
|
4769
|
-
const errorText = await response.text().catch(() => '');
|
|
4770
|
-
throw new Error(`Operator stream ${response.status}: ${sanitizeUserFacingErrorText(errorText).slice(0, 200)}`);
|
|
4771
|
-
}
|
|
4772
|
-
if (!response.body || typeof response.body.getReader !== 'function') {
|
|
4773
|
-
const fallbackData = await response.json();
|
|
4774
|
-
return {
|
|
4775
|
-
content: this.formatOperatorResponse(fallbackData),
|
|
4776
|
-
workflowId: fallbackData.workflow_id || null,
|
|
4777
|
-
contextId: fallbackData.context_id || response.headers.get('x-context-id') || executionContext.contextId || null,
|
|
4778
|
-
backendUrl: baseUrl,
|
|
4779
|
-
savedWorkflow: fallbackData.saved_workflow || null,
|
|
4780
|
-
metadata: {
|
|
4781
|
-
source: 'operator',
|
|
4782
|
-
mode: 'operator',
|
|
4783
|
-
contextId: fallbackData.context_id || response.headers.get('x-context-id') || executionContext.contextId || null,
|
|
4784
|
-
mcpContextId: fallbackData.mcp_context_id || response.headers.get('x-mcp-context-id') || executionContext.mcpContextId || null,
|
|
4785
|
-
},
|
|
4786
|
-
};
|
|
4787
|
-
}
|
|
4788
|
-
const reader = response.body.getReader();
|
|
4789
|
-
const decoder = new TextDecoder();
|
|
4790
|
-
let buffer = '';
|
|
4791
|
-
let workflowId = null;
|
|
4792
|
-
let contextId = response.headers.get('x-context-id') || executionContext.contextId || null;
|
|
4793
|
-
let mcpContextId = response.headers.get('x-mcp-context-id') || executionContext.mcpContextId || null;
|
|
4794
|
-
let resultPayload = null;
|
|
4795
|
-
let savedWorkflow = null;
|
|
4796
|
-
while (true) {
|
|
4797
|
-
const { done, value } = await reader.read();
|
|
4798
|
-
if (done) {
|
|
4799
|
-
break;
|
|
4800
|
-
}
|
|
4801
|
-
buffer += decoder.decode(value, { stream: true });
|
|
4802
|
-
const chunks = buffer.split('\n\n');
|
|
4803
|
-
buffer = chunks.pop() || '';
|
|
4804
|
-
for (const chunk of chunks) {
|
|
4805
|
-
const lines = chunk.split('\n');
|
|
4806
|
-
let eventName = 'message';
|
|
4807
|
-
const dataLines = [];
|
|
4808
|
-
for (const line of lines) {
|
|
4809
|
-
if (line.startsWith('event:')) {
|
|
4810
|
-
eventName = line.slice(6).trim();
|
|
4811
|
-
}
|
|
4812
|
-
else if (line.startsWith('data:')) {
|
|
4813
|
-
dataLines.push(line.slice(5).trim());
|
|
4814
|
-
}
|
|
4815
|
-
}
|
|
4816
|
-
if (dataLines.length === 0) {
|
|
4817
|
-
continue;
|
|
4818
|
-
}
|
|
4819
|
-
let payload = null;
|
|
4820
|
-
try {
|
|
4821
|
-
payload = JSON.parse(dataLines.join('\n'));
|
|
4822
|
-
}
|
|
4823
|
-
catch {
|
|
4824
|
-
payload = { raw: dataLines.join('\n') };
|
|
4825
|
-
}
|
|
4826
|
-
if (payload.workflow_id && !workflowId) {
|
|
4827
|
-
workflowId = payload.workflow_id;
|
|
4828
|
-
}
|
|
4829
|
-
if (!contextId && typeof payload.context_id === 'string' && payload.context_id.trim()) {
|
|
4830
|
-
contextId = payload.context_id.trim();
|
|
4831
|
-
}
|
|
4832
|
-
if (!mcpContextId && typeof payload.mcp_context_id === 'string' && payload.mcp_context_id.trim()) {
|
|
4833
|
-
mcpContextId = payload.mcp_context_id.trim();
|
|
4834
|
-
}
|
|
4835
|
-
if (!savedWorkflow && payload.saved_workflow && typeof payload.saved_workflow === 'object') {
|
|
4836
|
-
savedWorkflow = payload.saved_workflow;
|
|
4837
|
-
}
|
|
4838
|
-
if (typeof context.onStreamEvent === 'function') {
|
|
4839
|
-
try {
|
|
4840
|
-
context.onStreamEvent({ type: eventName, ...payload, context_id: payload.context_id || contextId, mcp_context_id: payload.mcp_context_id || mcpContextId });
|
|
4841
|
-
}
|
|
4842
|
-
catch {
|
|
4843
|
-
// UI callback failures must not break operator execution.
|
|
4844
|
-
}
|
|
4845
|
-
}
|
|
4846
|
-
if (eventName === 'result') {
|
|
4847
|
-
resultPayload = payload;
|
|
4848
|
-
}
|
|
4849
|
-
if (eventName === 'error') {
|
|
4850
|
-
throw new Error(payload.error || payload.message || 'Operator workflow failed');
|
|
4851
|
-
}
|
|
4852
|
-
}
|
|
4853
|
-
}
|
|
4854
|
-
return {
|
|
4855
|
-
content: this.formatOperatorResponse(resultPayload || {}),
|
|
4856
|
-
workflowId,
|
|
4857
|
-
contextId,
|
|
4858
|
-
backendUrl: baseUrl,
|
|
4859
|
-
savedWorkflow,
|
|
4860
|
-
metadata: { source: 'operator', mode: 'operator', contextId, mcpContextId, savedWorkflowId: savedWorkflow?.id || null },
|
|
4861
|
-
};
|
|
4862
|
-
}
|
|
4863
|
-
catch (error) {
|
|
4864
|
-
const isAbort = error?.name === 'AbortError' || error?.code === 'ABORT_ERR';
|
|
4865
|
-
if (isAbort) {
|
|
4866
|
-
const mins = Math.round(timeoutMs / 60000);
|
|
4867
|
-
throw new CLIError(`Operator workflow timed out after ${mins} minute(s). You can increase the timeout with VIGTHORIA_OPERATOR_TIMEOUT_MS.`, 'timeout', error);
|
|
4868
|
-
}
|
|
4869
|
-
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4870
|
-
}
|
|
4871
|
-
finally {
|
|
4872
|
-
if (timeoutId)
|
|
4873
|
-
clearTimeout(timeoutId);
|
|
4874
|
-
}
|
|
4875
|
-
}
|
|
4876
|
-
throw new CLIError(`Operator workflow failed on all endpoints: ${errors.join(' | ')}`, 'model_backend');
|
|
4877
|
-
}
|
|
4878
|
-
/**
|
|
4879
|
-
* Chat API - Direct Vigthoria Models API Architecture
|
|
4880
|
-
*
|
|
4881
|
-
* PRIMARY: api.vigthoria.io - Direct access to Vigthoria Model Router
|
|
4882
|
-
* This is the centralized public API for all AI operations.
|
|
4883
|
-
* Works for both authenticated and unauthenticated users.
|
|
4884
|
-
*
|
|
4885
|
-
* FALLBACK: Coder Cloud API - For authenticated users only
|
|
4886
|
-
*
|
|
4887
|
-
* NO localhost fallbacks - CLI is for external users, not server-side!
|
|
4888
|
-
*/
|
|
4889
|
-
async chat(messages, model, useLocal = false) {
|
|
4890
|
-
this.lastChatTransportErrors = [];
|
|
4891
|
-
const resolvedModel = this.resolveModelId(model);
|
|
4892
|
-
const candidateModels = this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()
|
|
4893
|
-
? [this.getSelfHostedFallbackModelId(resolvedModel, model)]
|
|
4894
|
-
: [resolvedModel];
|
|
4895
|
-
const fallbackModel = this.getFallbackModelId(resolvedModel);
|
|
4896
|
-
if (fallbackModel && fallbackModel !== resolvedModel) {
|
|
4897
|
-
candidateModels.push(fallbackModel);
|
|
4898
|
-
}
|
|
4899
|
-
for (const candidateModel of candidateModels) {
|
|
4900
|
-
const response = await this.tryChatWithModel(messages, candidateModel, model);
|
|
4901
|
-
if (response) {
|
|
4902
|
-
if (candidateModel !== resolvedModel) {
|
|
4903
|
-
this.logger.debug(`Recovered chat request using fallback model: ${candidateModel}`);
|
|
4904
|
-
}
|
|
4905
|
-
return response;
|
|
4906
|
-
}
|
|
4907
|
-
}
|
|
4908
|
-
// Keep route diagnostics internal. Users only need to know the Vigthoria service is temporarily unavailable.
|
|
4909
|
-
if (this.lastChatTransportErrors.length > 0) {
|
|
4910
|
-
this.logger.debug(`Chat transport failures: ${this.lastChatTransportErrors.slice(0, 4).join(' | ')}`);
|
|
4911
|
-
}
|
|
4912
|
-
throw new CLIError(VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE, 'model_backend');
|
|
4913
|
-
}
|
|
4914
|
-
shouldSkipCloudRoutes(resolvedModel) {
|
|
4915
|
-
return this.shouldSimulateCloudFailure() && this.isCloudModelId(resolvedModel);
|
|
4916
|
-
}
|
|
4917
|
-
async tryChatWithModel(messages, resolvedModel, requestedModel) {
|
|
4918
|
-
const routeFailures = [];
|
|
4919
|
-
const preferSelfHostedFirst = this.isSelfHostedPreferredModel(resolvedModel, requestedModel);
|
|
4920
|
-
if (preferSelfHostedFirst) {
|
|
4921
|
-
const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
|
|
4922
|
-
if (selfHostedResponse) {
|
|
4923
|
-
return selfHostedResponse;
|
|
4924
|
-
}
|
|
4925
|
-
}
|
|
4926
|
-
// STRATEGY 1: Direct Vigthoria Models API (api.vigthoria.io)
|
|
4927
|
-
// Cloud aliases must be charged through Coder wallet gates first.
|
|
4928
|
-
if (!this.shouldSkipCloudRoutes(resolvedModel) && !this.isCloudModelId(resolvedModel)) {
|
|
4929
|
-
try {
|
|
4930
|
-
this.logger.debug(`Direct Vigthoria Models API: ${resolvedModel}`);
|
|
4931
|
-
const token = this.getAccessToken();
|
|
4932
|
-
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
4933
|
-
model: resolvedModel,
|
|
4934
|
-
messages,
|
|
4935
|
-
max_tokens: this.config.get('preferences').maxTokens,
|
|
4936
|
-
temperature: 0.7,
|
|
4937
|
-
stream: false,
|
|
4938
|
-
}, {
|
|
4939
|
-
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
4940
|
-
});
|
|
4941
|
-
if (response.data.choices && response.data.choices.length > 0) {
|
|
4942
|
-
const content = response.data.choices[0].message?.content || response.data.choices[0].text;
|
|
4943
|
-
if (typeof content !== 'string' || !content.trim()) {
|
|
4944
|
-
this.logger.debug(`Direct API returned empty message content for ${resolvedModel}`);
|
|
4945
|
-
}
|
|
4946
|
-
else {
|
|
4947
|
-
return {
|
|
4948
|
-
id: response.data.id || `vigthoria-${Date.now()}`,
|
|
4949
|
-
message: content,
|
|
4950
|
-
model: response.data.model || resolvedModel || requestedModel,
|
|
4951
|
-
usage: response.data.usage,
|
|
4952
|
-
};
|
|
4953
|
-
}
|
|
4954
|
-
}
|
|
4955
|
-
this.logger.debug(`Direct API returned no choices for ${resolvedModel}`);
|
|
4956
|
-
}
|
|
4957
|
-
catch (error) {
|
|
4958
|
-
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
4959
|
-
this.logger.debug(`Direct Vigthoria Models API failed for ${resolvedModel}: ${errMsg}`);
|
|
4960
|
-
routeFailures.push(`models:${String(errMsg).slice(0, 120)}`);
|
|
4961
|
-
}
|
|
4962
|
-
}
|
|
4963
|
-
else {
|
|
4964
|
-
if (this.isCloudModelId(resolvedModel)) {
|
|
4965
|
-
this.logger.debug(`Cloud model ${resolvedModel} routed through Coder wallet gate first; skipping direct models API route.`);
|
|
4966
|
-
}
|
|
4967
|
-
else {
|
|
4968
|
-
this.logger.debug(`Simulating cloud failure for ${resolvedModel}; skipping cloud-backed public API route.`);
|
|
4969
|
-
}
|
|
4970
|
-
}
|
|
4971
|
-
// STRATEGY 2: Vigthoria Cloud API via Coder (authenticated users only)
|
|
4972
|
-
if (this.config.isAuthenticated() && !this.shouldSkipCloudRoutes(resolvedModel)) {
|
|
4973
|
-
try {
|
|
4974
|
-
this.logger.debug(`Vigthoria Cloud API fallback: ${resolvedModel}`);
|
|
4975
|
-
const response = await this.client.post('/api/ai/chat', {
|
|
4976
|
-
messages,
|
|
4977
|
-
model: resolvedModel,
|
|
4978
|
-
maxTokens: this.config.get('preferences').maxTokens,
|
|
4979
|
-
temperature: 0.7,
|
|
4980
|
-
cloudConsent: this.isCloudModelId(resolvedModel),
|
|
4981
|
-
});
|
|
4982
|
-
if (response.data.success !== false) {
|
|
4983
|
-
const content = response.data.response || response.data.message || response.data.content;
|
|
4984
|
-
if (typeof content !== 'string' || !content.trim()) {
|
|
4985
|
-
this.logger.debug(`Cloud API returned empty message content for ${resolvedModel}`);
|
|
4986
|
-
}
|
|
4987
|
-
else {
|
|
4988
|
-
return {
|
|
4989
|
-
id: response.data.id || `vigthoria-coder-${Date.now()}`,
|
|
4990
|
-
message: content,
|
|
4991
|
-
model: response.data.model || resolvedModel || requestedModel,
|
|
4992
|
-
usage: response.data.usage,
|
|
4993
|
-
};
|
|
4994
|
-
}
|
|
4995
|
-
}
|
|
4996
|
-
this.logger.debug(`Cloud API returned success=false for ${resolvedModel}`);
|
|
4997
|
-
}
|
|
4998
|
-
catch (error) {
|
|
4999
|
-
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
5000
|
-
this.logger.debug(`Vigthoria Cloud API failed for ${resolvedModel}: ${errMsg}`);
|
|
5001
|
-
routeFailures.push(`coder:${String(errMsg).slice(0, 120)}`);
|
|
5002
|
-
}
|
|
5003
|
-
try {
|
|
5004
|
-
this.logger.debug(`Canonical Vigthoria Cloud fallback: ${resolvedModel}`);
|
|
5005
|
-
const token = this.getAccessToken();
|
|
5006
|
-
const response = await axios.post('https://coder.vigthoria.io/api/ai/chat', {
|
|
5007
|
-
messages,
|
|
5008
|
-
model: resolvedModel,
|
|
5009
|
-
maxTokens: this.config.get('preferences').maxTokens,
|
|
5010
|
-
temperature: 0.7,
|
|
5011
|
-
cloudConsent: this.isCloudModelId(resolvedModel),
|
|
5012
|
-
}, {
|
|
5013
|
-
timeout: 180000,
|
|
5014
|
-
httpsAgent: this._httpsAgent ?? undefined,
|
|
5015
|
-
headers: token ? { Authorization: `Bearer ${token}`, Cookie: `vigthoria-auth-token=${token}` } : {},
|
|
5016
|
-
});
|
|
5017
|
-
if (response.data?.success !== false) {
|
|
5018
|
-
const content = response.data.response || response.data.message || response.data.content;
|
|
5019
|
-
if (typeof content === 'string' && content.trim()) {
|
|
5020
|
-
return {
|
|
5021
|
-
id: response.data.id || `vigthoria-coder-canonical-${Date.now()}`,
|
|
5022
|
-
message: content,
|
|
5023
|
-
model: response.data.model || resolvedModel || requestedModel,
|
|
5024
|
-
usage: response.data.usage,
|
|
5025
|
-
};
|
|
5026
|
-
}
|
|
5027
|
-
}
|
|
5028
|
-
}
|
|
5029
|
-
catch (error) {
|
|
5030
|
-
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
5031
|
-
this.logger.debug(`Canonical Vigthoria Cloud fallback failed for ${resolvedModel}: ${errMsg}`);
|
|
5032
|
-
routeFailures.push(`coder-canonical:${String(errMsg).slice(0, 120)}`);
|
|
5033
|
-
}
|
|
5034
|
-
}
|
|
5035
|
-
if (!preferSelfHostedFirst) {
|
|
5036
|
-
const selfHostedResponse = await this.trySelfHostedChatWithModel(messages, resolvedModel, requestedModel);
|
|
5037
|
-
if (selfHostedResponse) {
|
|
5038
|
-
return selfHostedResponse;
|
|
5039
|
-
}
|
|
5040
|
-
}
|
|
5041
|
-
if (routeFailures.length > 0) {
|
|
5042
|
-
this.lastChatTransportErrors = routeFailures;
|
|
5043
|
-
}
|
|
5044
|
-
return null;
|
|
5045
|
-
}
|
|
5046
|
-
async trySelfHostedChatWithModel(messages, resolvedModel, requestedModel) {
|
|
5047
|
-
if (!this.selfHostedModelRouterClient || !this.shouldTrySelfHostedFallback(resolvedModel, requestedModel)) {
|
|
5048
|
-
return null;
|
|
5049
|
-
}
|
|
5050
|
-
const selfHostedModel = this.getSelfHostedFallbackModelId(resolvedModel, requestedModel);
|
|
5051
|
-
try {
|
|
5052
|
-
this.logger.debug(`Dedicated Vigthoria model endpoint fallback: ${selfHostedModel}`);
|
|
5053
|
-
const response = await this.selfHostedModelRouterClient.post('/v1/chat/completions', {
|
|
5054
|
-
model: selfHostedModel,
|
|
5055
|
-
messages,
|
|
5056
|
-
max_tokens: this.config.get('preferences').maxTokens,
|
|
5057
|
-
temperature: 0.7,
|
|
5058
|
-
stream: false,
|
|
5059
|
-
}, {
|
|
5060
|
-
headers: {
|
|
5061
|
-
'x-agent-mode': 'true',
|
|
5062
|
-
},
|
|
5063
|
-
});
|
|
5064
|
-
if (response.data.choices && response.data.choices.length > 0) {
|
|
5065
|
-
const content = response.data.choices[0].message?.content || response.data.choices[0].text;
|
|
5066
|
-
if (typeof content !== 'string' || !content.trim()) {
|
|
5067
|
-
this.logger.debug(`Dedicated Vigthoria model endpoint returned empty message content for ${selfHostedModel}`);
|
|
5068
|
-
return null;
|
|
5069
|
-
}
|
|
5070
|
-
return {
|
|
5071
|
-
id: response.data.id || `vigthoria-dedicated-${Date.now()}`,
|
|
5072
|
-
message: content,
|
|
5073
|
-
model: response.data.model || selfHostedModel,
|
|
5074
|
-
usage: response.data.usage,
|
|
5075
|
-
};
|
|
5076
|
-
}
|
|
5077
|
-
this.logger.debug(`Dedicated Vigthoria model endpoint returned no choices for ${selfHostedModel}`);
|
|
5078
|
-
}
|
|
5079
|
-
catch (error) {
|
|
5080
|
-
const errMsg = error.response?.data?.error || error.message || 'Unknown error';
|
|
5081
|
-
this.logger.debug(`Dedicated Vigthoria model endpoint fallback failed for ${selfHostedModel}: ${errMsg}`);
|
|
5082
|
-
}
|
|
5083
|
-
return null;
|
|
5084
|
-
}
|
|
5085
|
-
getFallbackModelId(resolvedModel) {
|
|
5086
|
-
const cloudModels = new Set([
|
|
5087
|
-
'deepseek-v3.1:671b-cloud',
|
|
5088
|
-
'moonshotai/kimi-k2.5',
|
|
5089
|
-
'vigthoria-cloud-pro',
|
|
5090
|
-
'vigthoria-cloud-k2',
|
|
5091
|
-
'vigthoria-cloud-ultra',
|
|
5092
|
-
]);
|
|
5093
|
-
if (cloudModels.has(resolvedModel)) {
|
|
5094
|
-
return 'Vigthoria-v4-Code-27B';
|
|
5095
|
-
}
|
|
5096
|
-
return null;
|
|
5097
|
-
}
|
|
5098
|
-
isCloudModelId(resolvedModel) {
|
|
5099
|
-
return resolvedModel === 'deepseek-v3.1:671b-cloud'
|
|
5100
|
-
|| resolvedModel === 'moonshotai/kimi-k2.5'
|
|
5101
|
-
|| resolvedModel === 'vigthoria-cloud-pro'
|
|
5102
|
-
|| resolvedModel === 'vigthoria-cloud-k2'
|
|
5103
|
-
|| resolvedModel === 'vigthoria-cloud-ultra';
|
|
5104
|
-
}
|
|
5105
|
-
canUseCloudModel() {
|
|
5106
|
-
return this.config.hasCloudAccess();
|
|
5107
|
-
}
|
|
5108
|
-
resolvePermittedModelId(shortName) {
|
|
5109
|
-
const normalizedRequested = String(shortName || '').trim().toLowerCase();
|
|
5110
|
-
const blockedModels = new Set(['mini', 'creative-v3', 'creative-v4']);
|
|
5111
|
-
if (blockedModels.has(normalizedRequested)) {
|
|
5112
|
-
this.logger.debug(`Blocked retired model ${shortName}; using fallback Vigthoria-v4-Code-27B`);
|
|
5113
|
-
return 'Vigthoria-v4-Code-27B';
|
|
5114
|
-
}
|
|
5115
|
-
const resolvedModel = this.resolveModelId(shortName);
|
|
5116
|
-
if (this.isCloudModelId(resolvedModel) && !this.canUseCloudModel()) {
|
|
5117
|
-
const fallbackModel = this.getSelfHostedFallbackModelId(resolvedModel, shortName);
|
|
5118
|
-
this.logger.debug(`Blocked unauthorized cloud model ${shortName}; using fallback ${fallbackModel}`);
|
|
5119
|
-
return fallbackModel;
|
|
5120
|
-
}
|
|
5121
|
-
return resolvedModel;
|
|
5122
|
-
}
|
|
5123
|
-
shouldSimulateCloudFailure() {
|
|
5124
|
-
return process.env.VIGTHORIA_SIMULATE_CLOUD_FAILURE === '1';
|
|
5125
|
-
}
|
|
5126
|
-
shouldTrySelfHostedFallback(resolvedModel, requestedModel) {
|
|
5127
|
-
if (!this.selfHostedModelRouterClient) {
|
|
5128
|
-
return false;
|
|
5129
|
-
}
|
|
5130
|
-
const normalizedRequested = String(requestedModel || '').toLowerCase();
|
|
5131
|
-
return this.isSelfHostedPreferredModel(resolvedModel, requestedModel)
|
|
5132
|
-
|| normalizedRequested === 'cloud'
|
|
5133
|
-
|| normalizedRequested === 'cloud-reason'
|
|
5134
|
-
|| normalizedRequested === 'ultra';
|
|
5135
|
-
}
|
|
5136
|
-
isSelfHostedPreferredModel(resolvedModel, requestedModel) {
|
|
5137
|
-
const normalizedRequested = String(requestedModel || '').toLowerCase();
|
|
5138
|
-
const selfHostedModels = new Set([
|
|
5139
|
-
'Vigthoria-v4-Creative-27B',
|
|
5140
|
-
'Vigthoria-v4-Code-27B',
|
|
5141
|
-
'Vigthoria-v4-Assistant-9B',
|
|
5142
|
-
'vigthoria-v3-code-35b',
|
|
5143
|
-
'vigthoria-v3-code-35b:latest',
|
|
5144
|
-
'vigthoria-v3-code-9b',
|
|
5145
|
-
'vigthoria-v3-code-9b:latest',
|
|
5146
|
-
'vigthoria-v3-balanced-4b',
|
|
5147
|
-
'vigthoria-v3-balanced-4b:latest',
|
|
5148
|
-
'qwen3-coder:latest',
|
|
5149
|
-
]);
|
|
5150
|
-
return selfHostedModels.has(resolvedModel)
|
|
5151
|
-
|| normalizedRequested === 'agent'
|
|
5152
|
-
|| normalizedRequested === 'architect'
|
|
5153
|
-
|| normalizedRequested === 'assistant'
|
|
5154
|
-
|| normalizedRequested === 'code'
|
|
5155
|
-
|| normalizedRequested === 'code-30b'
|
|
5156
|
-
|| normalizedRequested === 'code-35b'
|
|
5157
|
-
|| normalizedRequested === 'code-9b'
|
|
5158
|
-
|| normalizedRequested === 'balanced'
|
|
5159
|
-
|| normalizedRequested === 'balanced-4b'
|
|
5160
|
-
|| normalizedRequested === 'pro';
|
|
5161
|
-
}
|
|
5162
|
-
getSelfHostedFallbackModelId(resolvedModel, requestedModel) {
|
|
5163
|
-
if (this.isSelfHostedPreferredModel(resolvedModel, requestedModel)) {
|
|
5164
|
-
return resolvedModel === 'qwen3-coder:latest' ? 'Vigthoria-v4-Code-27B' : resolvedModel;
|
|
5165
|
-
}
|
|
5166
|
-
return 'Vigthoria-v4-Code-27B';
|
|
5167
|
-
}
|
|
5168
|
-
// Streaming chat
|
|
5169
|
-
async *chatStream(messages, model) {
|
|
5170
|
-
const wsUrl = this.config.get('wsUrl');
|
|
5171
|
-
const token = this.config.get('authToken');
|
|
5172
|
-
return new Promise((resolve, reject) => {
|
|
5173
|
-
const ws = new WebSocket(`${wsUrl}/chat`, {
|
|
5174
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
5175
|
-
});
|
|
5176
|
-
ws.on('open', () => {
|
|
5177
|
-
ws.send(JSON.stringify({
|
|
5178
|
-
type: 'chat',
|
|
5179
|
-
messages,
|
|
5180
|
-
model: this.resolvePermittedModelId(model),
|
|
5181
|
-
stream: true,
|
|
5182
|
-
}));
|
|
5183
|
-
});
|
|
5184
|
-
ws.on('message', (data) => {
|
|
5185
|
-
const chunk = JSON.parse(data.toString());
|
|
5186
|
-
// Yield chunk - but this pattern won't work directly
|
|
5187
|
-
// Need to use event emitter pattern instead
|
|
5188
|
-
});
|
|
5189
|
-
ws.on('error', (error) => {
|
|
5190
|
-
reject(error);
|
|
5191
|
-
});
|
|
5192
|
-
ws.on('close', () => {
|
|
5193
|
-
resolve(undefined);
|
|
5194
|
-
});
|
|
5195
|
-
});
|
|
5196
|
-
}
|
|
5197
|
-
// Non-streaming alternative with callback
|
|
5198
|
-
async chatWithCallback(messages, model, onChunk, onDone, onError) {
|
|
5199
|
-
const wsUrl = this.config.get('wsUrl');
|
|
5200
|
-
const token = this.config.get('authToken');
|
|
5201
|
-
return new Promise((resolve, reject) => {
|
|
5202
|
-
const ws = new WebSocket(`${wsUrl}/chat`, {
|
|
5203
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
5204
|
-
});
|
|
5205
|
-
ws.on('open', () => {
|
|
5206
|
-
ws.send(JSON.stringify({
|
|
5207
|
-
type: 'chat',
|
|
5208
|
-
messages,
|
|
5209
|
-
model: this.resolvePermittedModelId(model),
|
|
5210
|
-
stream: true,
|
|
5211
|
-
}));
|
|
5212
|
-
});
|
|
5213
|
-
ws.on('message', (data) => {
|
|
5214
|
-
try {
|
|
5215
|
-
const chunk = JSON.parse(data.toString());
|
|
5216
|
-
if (chunk.type === 'content') {
|
|
5217
|
-
onChunk(chunk.content);
|
|
5218
|
-
}
|
|
5219
|
-
else if (chunk.type === 'done') {
|
|
5220
|
-
onDone();
|
|
5221
|
-
ws.close();
|
|
5222
|
-
resolve();
|
|
5223
|
-
}
|
|
5224
|
-
else if (chunk.type === 'error') {
|
|
5225
|
-
onError(new Error(chunk.error));
|
|
5226
|
-
ws.close();
|
|
5227
|
-
reject(new Error(chunk.error));
|
|
5228
|
-
}
|
|
5229
|
-
}
|
|
5230
|
-
catch (e) {
|
|
5231
|
-
// Raw text chunk
|
|
5232
|
-
onChunk(data.toString());
|
|
5233
|
-
}
|
|
5234
|
-
});
|
|
5235
|
-
ws.on('error', (error) => {
|
|
5236
|
-
onError(error);
|
|
5237
|
-
reject(error);
|
|
5238
|
-
});
|
|
5239
|
-
ws.on('close', () => {
|
|
5240
|
-
resolve();
|
|
5241
|
-
});
|
|
5242
|
-
});
|
|
5243
|
-
}
|
|
5244
|
-
// ─── Chat completion helper ────────────────────────────────────────
|
|
5245
|
-
// Routes all AI file commands through the Model Router
|
|
5246
|
-
// (/v1/chat/completions on api.vigthoria.io) which is the only
|
|
5247
|
-
// backend that reliably accepts our auth token.
|
|
5248
|
-
async chatComplete(systemPrompt, userPrompt, model, maxTokens) {
|
|
5249
|
-
const resolvedModel = model ? this.resolvePermittedModelId(model) : 'Vigthoria-v4-Code-27B';
|
|
5250
|
-
const response = await this.modelRouterClient.post('/v1/chat/completions', {
|
|
5251
|
-
model: resolvedModel,
|
|
5252
|
-
messages: [
|
|
5253
|
-
{ role: 'system', content: systemPrompt },
|
|
5254
|
-
{ role: 'user', content: userPrompt },
|
|
5255
|
-
],
|
|
5256
|
-
max_tokens: maxTokens || this.config.get('preferences').maxTokens || 4096,
|
|
5257
|
-
temperature: 0.3,
|
|
5258
|
-
stream: false,
|
|
5259
|
-
});
|
|
5260
|
-
const content = response.data.choices?.[0]?.message?.content || response.data.choices?.[0]?.text || '';
|
|
5261
|
-
return typeof content === 'string' ? content : '';
|
|
5262
|
-
}
|
|
5263
|
-
// Code operations - Using Vigthoria Centralized API (via Model Router)
|
|
5264
|
-
async generateCode(prompt, language, model) {
|
|
5265
|
-
const isNonHtmlLang = !['html', 'css'].includes(language.toLowerCase());
|
|
5266
|
-
const wordCount = prompt.trim().split(/\s+/).length;
|
|
5267
|
-
// Prepend a forceful scope-enforcement instruction so the model
|
|
5268
|
-
// doesn't expand a small task into an oversized glossy page.
|
|
5269
|
-
const buildScopedPrompt = (retry) => {
|
|
5270
|
-
if (retry && isNonHtmlLang) {
|
|
5271
|
-
// Ultra-minimal retry prompt — no long instruction block
|
|
5272
|
-
return `OUTPUT ONLY RAW ${language.toUpperCase()} CODE. MAXIMUM 10 LINES. NO DOM. NO HTML. NO CSS. NO ANIMATION. NO VALIDATION. NO COMMENTS.\n\n${prompt}`;
|
|
5273
|
-
}
|
|
5274
|
-
const lines = [
|
|
5275
|
-
'IMPORTANT — MANDATORY SCOPE CONSTRAINTS (violation = failure):',
|
|
5276
|
-
`1. You MUST output ONLY ${language.toUpperCase()} code. Do NOT output HTML, CSS, or DOM manipulation unless the language is html/css.`,
|
|
5277
|
-
`2. The target language is: ${language}. Do not switch to a different language.`,
|
|
5278
|
-
'3. Output ONLY what the user explicitly asked for. Nothing more.',
|
|
5279
|
-
'4. If the prompt is ≤ 15 words, produce ≤ 80 lines of code maximum.',
|
|
5280
|
-
'5. If the user says "tiny", "small", "simple", "minimal", or "basic", produce ≤ 50 lines.',
|
|
5281
|
-
'6. NEVER add any of these unless the user EXPLICITLY requests them:',
|
|
5282
|
-
' - Hero sections, CTAs, testimonials, pricing tables, footers, navbars',
|
|
5283
|
-
' - CSS animations, gradients, neon effects, glass-morphism, particles',
|
|
5284
|
-
' - Google Fonts, Font Awesome, external CDN links, icon libraries',
|
|
5285
|
-
' - DOM manipulation, document.createElement, innerHTML',
|
|
5286
|
-
' - Responsive breakpoints, media queries (unless asked)',
|
|
5287
|
-
' - Multiple pages or components when one was requested',
|
|
5288
|
-
' - Input validation, error handling beyond what was requested',
|
|
5289
|
-
'7. Prefer inline styles or a small <style> block ONLY if the language is html. No CSS frameworks.',
|
|
5290
|
-
'8. Return raw code only — no markdown fences, no explanations, no comments about what could be added.',
|
|
5291
|
-
'9. Match the complexity of the request: a "hello world" is 1-5 lines, a "multiply function" is 3-5 lines, a "button" is 5-15 lines.',
|
|
5292
|
-
];
|
|
5293
|
-
if (isNonHtmlLang) {
|
|
5294
|
-
lines.push(`10. This is a PURE ${language.toUpperCase()} task. ABSOLUTELY NO document.*, window.*, DOM, createElement, innerHTML, querySelector, addEventListener. Output a plain ${language} function/module ONLY.`);
|
|
5295
|
-
}
|
|
5296
|
-
lines.push('', prompt);
|
|
5297
|
-
return lines.join('\n');
|
|
5298
|
-
};
|
|
5299
|
-
// First attempt — route through Model Router chat completions
|
|
5300
|
-
const systemPrompt = `You are a code generator. Output ONLY raw ${language} code. No markdown fences, no explanations, no commentary. Just the code.`;
|
|
5301
|
-
let code = await this.chatComplete(systemPrompt, buildScopedPrompt(false), model);
|
|
5302
|
-
// Strip markdown fences if model included them
|
|
5303
|
-
code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
5304
|
-
// Client-side validation: reject DOM-polluted or over-engineered responses for non-HTML languages
|
|
5305
|
-
const needsRetry = isNonHtmlLang && (this.codeContainsDomPollution(code) ||
|
|
5306
|
-
this.codeIsOverEngineered(code, prompt));
|
|
5307
|
-
if (needsRetry) {
|
|
5308
|
-
// Retry once with stronger constraint — via Model Router
|
|
5309
|
-
code = await this.chatComplete(systemPrompt, buildScopedPrompt(true), model);
|
|
5310
|
-
code = code.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
5311
|
-
// If still polluted, strip DOM code client-side
|
|
5312
|
-
if (this.codeContainsDomPollution(code)) {
|
|
5313
|
-
code = this.stripDomPollution(code, language);
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
// Final cleanup: for non-HTML, if still over-engineered after retry,
|
|
5317
|
-
// extract only the first complete function/class definition.
|
|
5318
|
-
if (isNonHtmlLang && this.codeIsOverEngineered(code, prompt)) {
|
|
5319
|
-
code = this.extractFirstFunction(code);
|
|
5320
|
-
}
|
|
5321
|
-
// Ensure balanced braces — model sometimes truncates closing braces
|
|
5322
|
-
code = this.ensureBalancedBraces(code);
|
|
5323
|
-
return code;
|
|
5324
|
-
}
|
|
5325
|
-
/**
|
|
5326
|
-
* Ensure code has balanced curly braces by appending missing closing braces.
|
|
5327
|
-
*/
|
|
5328
|
-
ensureBalancedBraces(code) {
|
|
5329
|
-
// Count braces/parens/brackets outside strings and comments
|
|
5330
|
-
let braces = 0, parens = 0, brackets = 0;
|
|
5331
|
-
let inStr = null;
|
|
5332
|
-
let inLine = false, inBlock = false;
|
|
5333
|
-
for (let i = 0; i < code.length; i++) {
|
|
5334
|
-
const ch = code[i], nx = code[i + 1] || '';
|
|
5335
|
-
if (inLine) {
|
|
5336
|
-
if (ch === '\n')
|
|
5337
|
-
inLine = false;
|
|
5338
|
-
continue;
|
|
5339
|
-
}
|
|
5340
|
-
if (inBlock) {
|
|
5341
|
-
if (ch === '*' && nx === '/') {
|
|
5342
|
-
inBlock = false;
|
|
5343
|
-
i++;
|
|
5344
|
-
}
|
|
5345
|
-
continue;
|
|
5346
|
-
}
|
|
5347
|
-
if (inStr) {
|
|
5348
|
-
if (ch === inStr && code[i - 1] !== '\\')
|
|
5349
|
-
inStr = null;
|
|
5350
|
-
continue;
|
|
5351
|
-
}
|
|
5352
|
-
if (ch === '/' && nx === '/') {
|
|
5353
|
-
inLine = true;
|
|
5354
|
-
continue;
|
|
5355
|
-
}
|
|
5356
|
-
if (ch === '/' && nx === '*') {
|
|
5357
|
-
inBlock = true;
|
|
5358
|
-
continue;
|
|
5359
|
-
}
|
|
5360
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
5361
|
-
inStr = ch;
|
|
5362
|
-
continue;
|
|
5363
|
-
}
|
|
5364
|
-
if (ch === '{')
|
|
5365
|
-
braces++;
|
|
5366
|
-
else if (ch === '}')
|
|
5367
|
-
braces--;
|
|
5368
|
-
else if (ch === '(')
|
|
5369
|
-
parens++;
|
|
5370
|
-
else if (ch === ')')
|
|
5371
|
-
parens--;
|
|
5372
|
-
else if (ch === '[')
|
|
5373
|
-
brackets++;
|
|
5374
|
-
else if (ch === ']')
|
|
5375
|
-
brackets--;
|
|
5376
|
-
}
|
|
5377
|
-
let result = code.trimEnd();
|
|
5378
|
-
for (let i = 0; i < braces; i++)
|
|
5379
|
-
result += '\n}';
|
|
5380
|
-
for (let i = 0; i < parens; i++)
|
|
5381
|
-
result += ')';
|
|
5382
|
-
for (let i = 0; i < brackets; i++)
|
|
5383
|
-
result += ']';
|
|
5384
|
-
return braces > 0 || parens > 0 || brackets > 0 ? result : code;
|
|
5385
|
-
}
|
|
5386
|
-
/**
|
|
5387
|
-
* Quick JS/TS syntax validation using Node's built-in parser.
|
|
5388
|
-
* Returns true if the code parses without errors.
|
|
5389
|
-
*/
|
|
5390
|
-
validateJsSyntax(code) {
|
|
5391
|
-
try {
|
|
5392
|
-
// Use Function constructor to check syntax without executing
|
|
5393
|
-
new Function(code);
|
|
5394
|
-
return true;
|
|
5395
|
-
}
|
|
5396
|
-
catch {
|
|
5397
|
-
return false;
|
|
5398
|
-
}
|
|
5399
|
-
}
|
|
5400
|
-
/**
|
|
5401
|
-
* Extract the first complete function/class from code.
|
|
5402
|
-
* Used as last-resort when the model keeps over-producing.
|
|
5403
|
-
*/
|
|
5404
|
-
extractFirstFunction(code) {
|
|
5405
|
-
const lines = code.split('\n');
|
|
5406
|
-
const funcStart = lines.findIndex(l => /^(export\s+)?(const\s+\w+\s*=\s*(\(|function)|function\s+\w+|class\s+\w+|def\s+\w+|fn\s+\w+|func\s+\w+)/.test(l.trim()));
|
|
5407
|
-
if (funcStart === -1)
|
|
5408
|
-
return code;
|
|
5409
|
-
let braceDepth = 0;
|
|
5410
|
-
let foundOpen = false;
|
|
5411
|
-
const result = [];
|
|
5412
|
-
for (let i = funcStart; i < lines.length; i++) {
|
|
5413
|
-
const line = lines[i];
|
|
5414
|
-
result.push(line);
|
|
5415
|
-
braceDepth += (line.match(/{/g) || []).length;
|
|
5416
|
-
braceDepth -= (line.match(/}/g) || []).length;
|
|
5417
|
-
if (braceDepth > 0)
|
|
5418
|
-
foundOpen = true;
|
|
5419
|
-
if (foundOpen && braceDepth <= 0) {
|
|
5420
|
-
break;
|
|
5421
|
-
}
|
|
5422
|
-
}
|
|
5423
|
-
// If we reached the end without balancing braces, add closing
|
|
5424
|
-
if (braceDepth > 0) {
|
|
5425
|
-
result.push('};');
|
|
5426
|
-
}
|
|
5427
|
-
return result.join('\n');
|
|
5428
|
-
}
|
|
5429
|
-
/**
|
|
5430
|
-
* Detect if code is excessively over-engineered for a short prompt.
|
|
5431
|
-
* E.g. a "multiply function" request producing 20+ lines.
|
|
5432
|
-
*/
|
|
5433
|
-
codeIsOverEngineered(code, prompt) {
|
|
5434
|
-
const wordCount = prompt.trim().split(/\s+/).length;
|
|
5435
|
-
const lineCount = code.trim().split('\n').length;
|
|
5436
|
-
// Short prompt (≤15 words) should not produce more than 15 lines
|
|
5437
|
-
if (wordCount <= 15 && lineCount > 15)
|
|
5438
|
-
return true;
|
|
5439
|
-
return false;
|
|
5440
|
-
}
|
|
5441
|
-
/**
|
|
5442
|
-
* Detect if generated code contains DOM/HTML pollution inappropriate
|
|
5443
|
-
* for a pure programming language like JavaScript, Python, etc.
|
|
5444
|
-
*/
|
|
5445
|
-
codeContainsDomPollution(code) {
|
|
5446
|
-
const domPatterns = /document\.(createElement|querySelector|getElementById|getElementsBy|body|head|addEventListener)|innerHTML|\.style\.(cssText|position|transform|animation)|@keyframes|\.appendChild|\.removeChild|window\.(onload|addEventListener)|CSSAnimation|new\s+Animation\b|\.getAnimations\s*\(|\.animate\s*\(/;
|
|
5447
|
-
return domPatterns.test(code);
|
|
5448
|
-
}
|
|
5449
|
-
/**
|
|
5450
|
-
* Strip DOM pollution from generated code, keeping only the pure logic.
|
|
5451
|
-
* Used as last-resort fallback when the model repeatedly ignores constraints.
|
|
5452
|
-
*/
|
|
5453
|
-
stripDomPollution(code, language) {
|
|
5454
|
-
const lines = code.split('\n');
|
|
5455
|
-
const cleanLines = [];
|
|
5456
|
-
let insideDomBlock = false;
|
|
5457
|
-
let braceDepth = 0;
|
|
5458
|
-
for (const line of lines) {
|
|
5459
|
-
// Detect start of DOM blocks
|
|
5460
|
-
if (/document\.|\.style\.|\.appendChild|\.removeChild|\.textContent\s*=|@keyframes|addEventListener|CSSAnimation|\.getAnimations|\.animate\s*\(/.test(line) && !insideDomBlock) {
|
|
5461
|
-
insideDomBlock = true;
|
|
5462
|
-
braceDepth = 0;
|
|
5463
|
-
}
|
|
5464
|
-
if (insideDomBlock) {
|
|
5465
|
-
braceDepth += (line.match(/{/g) || []).length;
|
|
5466
|
-
braceDepth -= (line.match(/}/g) || []).length;
|
|
5467
|
-
if (braceDepth <= 0) {
|
|
5468
|
-
insideDomBlock = false;
|
|
5469
|
-
}
|
|
5470
|
-
continue; // Skip DOM lines
|
|
5471
|
-
}
|
|
5472
|
-
cleanLines.push(line);
|
|
5473
|
-
}
|
|
5474
|
-
const cleaned = cleanLines.join('\n').trim();
|
|
5475
|
-
// If we stripped too aggressively (less than 2 lines left), return a minimal stub
|
|
5476
|
-
if (cleaned.split('\n').length < 2) {
|
|
5477
|
-
return `// Auto-generated: model returned DOM code for a ${language} task.\n// Please regenerate or provide more specific instructions.`;
|
|
5478
|
-
}
|
|
5479
|
-
return cleaned;
|
|
5480
|
-
}
|
|
5481
|
-
// Senior Developer Mode - Planning + Generation + Quality Check
|
|
5482
|
-
async generateProject(prompt, projectType, model) {
|
|
5483
|
-
const sysPrompt = [
|
|
5484
|
-
`You are a senior developer. Generate a complete ${projectType} project.`,
|
|
5485
|
-
'Return a JSON object with these fields:',
|
|
5486
|
-
' "code": the full source code as a string,',
|
|
5487
|
-
' "plan": an object describing the architecture,',
|
|
5488
|
-
' "quality": { "lineCount": number, "score": number (0-100) }',
|
|
5489
|
-
'Return ONLY the JSON object, no markdown fences.',
|
|
5490
|
-
].join('\n');
|
|
5491
|
-
const raw = await this.chatComplete(sysPrompt, prompt, model, 8192);
|
|
5492
|
-
try {
|
|
5493
|
-
const cleaned = raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
5494
|
-
const parsed = JSON.parse(cleaned);
|
|
5495
|
-
return {
|
|
5496
|
-
code: parsed.code || raw,
|
|
5497
|
-
plan: parsed.plan,
|
|
5498
|
-
quality: parsed.quality,
|
|
5499
|
-
};
|
|
5500
|
-
}
|
|
5501
|
-
catch {
|
|
5502
|
-
return { code: raw.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim() };
|
|
5503
|
-
}
|
|
5504
|
-
}
|
|
5505
|
-
async explainCode(code, language) {
|
|
5506
|
-
const sysPrompt = [
|
|
5507
|
-
`You are a code explainer. Explain the following ${language} code clearly and concisely.`,
|
|
5508
|
-
'Focus on what it does, how it works, and any notable patterns or potential issues.',
|
|
5509
|
-
'Format your response as clean Markdown:',
|
|
5510
|
-
'- Use ## headers for major sections (e.g. ## Overview, ## How It Works, ## Key Details).',
|
|
5511
|
-
'- Use bullet points (- or *) for all lists. Do NOT use numbered lists.',
|
|
5512
|
-
'- Wrap code references in backticks.',
|
|
5513
|
-
'- Keep paragraphs short (2-3 sentences max).',
|
|
5514
|
-
'- Do NOT use raw HTML or excessive blank lines.',
|
|
5515
|
-
'- Do NOT nest numbered lists inside sections.',
|
|
5516
|
-
].join('\n');
|
|
5517
|
-
return this.chatComplete(sysPrompt, code);
|
|
5518
|
-
}
|
|
5519
|
-
async reviewCode(code, language) {
|
|
5520
|
-
const sysPrompt = [
|
|
5521
|
-
`You are a senior code reviewer for ${language}. Analyze the code and return a JSON object with:`,
|
|
5522
|
-
' "score": number 0-100,',
|
|
5523
|
-
' "issues": [{ "type": string, "line": number, "message": string, "severity": "error"|"warning"|"info" }],',
|
|
5524
|
-
' "suggestions": [string]',
|
|
5525
|
-
'Rules:',
|
|
5526
|
-
'- Return concrete, line-specific issues with severity.',
|
|
5527
|
-
'- Every issue MUST reference a line number.',
|
|
5528
|
-
'- Report each distinct bug ONCE. Do NOT report the same bug multiple times with different wording.',
|
|
5529
|
-
'- For trivial/short code (< 10 lines), report ONLY actual bugs. Do NOT pad with style, robustness, or best-practice suggestions.',
|
|
5530
|
-
'- If you find a real bug (wrong operator, logic error, type mismatch), report ONLY that bug. Do NOT also suggest input validation, type checking, or error handling unless those are ACTUAL bugs.',
|
|
5531
|
-
'- Prioritize REAL BUGS: wrong operators, logic errors, off-by-one, type mismatches.',
|
|
5532
|
-
'- Do NOT suggest adding error handling, input validation, or documentation as issues unless the user explicitly asked for a style review.',
|
|
5533
|
-
'- Return ONLY the JSON object, no markdown fences or extra text.',
|
|
5534
|
-
].join('\n');
|
|
5535
|
-
let raw = {};
|
|
5536
|
-
try {
|
|
5537
|
-
const result = await this.chatComplete(sysPrompt, code);
|
|
5538
|
-
const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
5539
|
-
raw = JSON.parse(cleaned);
|
|
5540
|
-
}
|
|
5541
|
-
catch {
|
|
5542
|
-
// If parsing fails, return minimal review
|
|
5543
|
-
}
|
|
5544
|
-
const score = typeof raw.score === 'number' ? raw.score : 0;
|
|
5545
|
-
const issues = Array.isArray(raw.issues) ? raw.issues : [];
|
|
5546
|
-
const suggestions = Array.isArray(raw.suggestions) ? raw.suggestions : [];
|
|
5547
|
-
// Merge client-side heuristics, but with tight dedup to avoid
|
|
5548
|
-
// redundant over-reporting when the model already found the bug.
|
|
5549
|
-
const modelFoundError = issues.some(i => i.severity === 'error');
|
|
5550
|
-
const heuristic = this.heuristicCodeIssues(code, language);
|
|
5551
|
-
for (const h of heuristic) {
|
|
5552
|
-
// If the model already found a real error, skip non-error heuristics
|
|
5553
|
-
// entirely — they're just padding (style, robustness, etc.)
|
|
5554
|
-
if (modelFoundError && h.severity !== 'error')
|
|
5555
|
-
continue;
|
|
5556
|
-
// Semantic duplicate check: same line + (similar type OR overlapping
|
|
5557
|
-
// keywords in the message). This catches cases where the model
|
|
5558
|
-
// and heuristic describe the same bug with different wording.
|
|
5559
|
-
const hWords = new Set(h.message.toLowerCase().split(/\W+/).filter(w => w.length > 3));
|
|
5560
|
-
const hTypeNorm = h.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
5561
|
-
const isSemanticallyDuplicate = issues.some((existing) => {
|
|
5562
|
-
if (existing.line !== h.line)
|
|
5563
|
-
return false;
|
|
5564
|
-
// Normalize types: "logic-error", "logic_error", "logic" all match
|
|
5565
|
-
const eTypeNorm = existing.type.toLowerCase().replace(/[^a-z]/g, '');
|
|
5566
|
-
if (eTypeNorm === hTypeNorm || eTypeNorm.startsWith(hTypeNorm) || hTypeNorm.startsWith(eTypeNorm))
|
|
5567
|
-
return true;
|
|
5568
|
-
// Both errors on same line about the same category of problem
|
|
5569
|
-
if (existing.severity === 'error' && h.severity === 'error')
|
|
5570
|
-
return true;
|
|
5571
|
-
// Check keyword overlap — if ≥2 significant words match, it's the same finding
|
|
5572
|
-
const eWords = existing.message.toLowerCase().split(/\W+/).filter(w => w.length > 3);
|
|
5573
|
-
let overlap = 0;
|
|
5574
|
-
for (const w of eWords) {
|
|
5575
|
-
if (hWords.has(w))
|
|
5576
|
-
overlap++;
|
|
5577
|
-
}
|
|
5578
|
-
return overlap >= 2;
|
|
5579
|
-
});
|
|
5580
|
-
if (!isSemanticallyDuplicate) {
|
|
5581
|
-
issues.push(h);
|
|
5582
|
-
}
|
|
5583
|
-
}
|
|
5584
|
-
// Sort: errors first, then warnings, then info
|
|
5585
|
-
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
5586
|
-
issues.sort((a, b) => (severityOrder[a.severity] ?? 3) - (severityOrder[b.severity] ?? 3));
|
|
5587
|
-
// Prevent contradictory output: low score but zero issues.
|
|
5588
|
-
if (score < 50 && issues.length === 0) {
|
|
5589
|
-
issues.push({
|
|
5590
|
-
type: 'quality',
|
|
5591
|
-
line: 1,
|
|
5592
|
-
message: `The analysis returned a low quality score (${score}/100) but did not enumerate specific issues. Re-run the review or inspect the file manually.`,
|
|
5593
|
-
severity: 'warning',
|
|
5594
|
-
});
|
|
5595
|
-
}
|
|
5596
|
-
return { score, issues, suggestions };
|
|
3504
|
+
workspaceFilesOutOfBand: !!workspaceFiles,
|
|
3505
|
+
}),
|
|
3506
|
+
workspace_files: workspaceFiles,
|
|
3507
|
+
stream: false,
|
|
3508
|
+
model: resolvedModel,
|
|
3509
|
+
mcp_context_id: executionContext.mcpContextId || undefined,
|
|
3510
|
+
};
|
|
3511
|
+
return this.v3AgentClient.requestBackground('POST', '', body);
|
|
5597
3512
|
}
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
if (
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
break;
|
|
5640
|
-
}
|
|
5641
|
-
}
|
|
5642
|
-
}
|
|
5643
|
-
// Division where subtraction/addition is expected in subtract
|
|
5644
|
-
if (/\breturn\s+\w+\s*\+\s*\w+/.test(line)) {
|
|
5645
|
-
for (let j = i; j >= Math.max(0, i - 5); j--) {
|
|
5646
|
-
if (/function\s+subtract\b/i.test(lines[j]) || /\bsubtract\s*[=(]/.test(lines[j])) {
|
|
5647
|
-
issues.push({ type: 'logic', line: lineNum, message: 'Addition operator in a function named "subtract" — likely should be subtraction (-).', severity: 'error' });
|
|
5648
|
-
break;
|
|
3513
|
+
async listV3BackgroundJobs(limit = 20) {
|
|
3514
|
+
const data = await this.v3AgentClient.requestBackground('GET', '', undefined, `?limit=${encodeURIComponent(String(limit))}`);
|
|
3515
|
+
return Array.isArray(data.jobs) ? data.jobs : [];
|
|
3516
|
+
}
|
|
3517
|
+
async getV3BackgroundJob(jobId) {
|
|
3518
|
+
return this.v3AgentClient.requestBackground('GET', encodeURIComponent(jobId));
|
|
3519
|
+
}
|
|
3520
|
+
async cancelV3BackgroundJob(jobId) {
|
|
3521
|
+
return this.v3AgentClient.requestBackground('POST', `${encodeURIComponent(jobId)}/cancel`);
|
|
3522
|
+
}
|
|
3523
|
+
async resolveV3BackgroundApproval(jobId, approvalId, approved, allowMode = 'once') {
|
|
3524
|
+
return this.v3AgentClient.resolveApproval({ context_id: jobId, approval_id: approvalId, approved, allow_mode: allowMode });
|
|
3525
|
+
}
|
|
3526
|
+
async getV3BackgroundJobFiles(jobId) {
|
|
3527
|
+
return this.v3AgentClient.requestBackground('GET', `${encodeURIComponent(jobId)}/files`);
|
|
3528
|
+
}
|
|
3529
|
+
async applyV3BackgroundJobFiles(jobId, context = {}) {
|
|
3530
|
+
const [data, job] = await Promise.all([
|
|
3531
|
+
this.getV3BackgroundJobFiles(jobId),
|
|
3532
|
+
this.getV3BackgroundJob(jobId),
|
|
3533
|
+
]);
|
|
3534
|
+
if (job?.status !== 'completed' || job?.result?.success === false || job?.result?.type === 'error') {
|
|
3535
|
+
throw new Error(`Background job ${jobId} is not a successful completed job and cannot be applied.`);
|
|
3536
|
+
}
|
|
3537
|
+
const mutatedPaths = new Set();
|
|
3538
|
+
const addMutatedPath = (value) => {
|
|
3539
|
+
const normalized = this.normalizeWorkspaceRelativePath(String(value || ''));
|
|
3540
|
+
if (!normalized || path.isAbsolute(normalized) || normalized.split('/').includes('..'))
|
|
3541
|
+
return;
|
|
3542
|
+
mutatedPaths.add(normalized);
|
|
3543
|
+
};
|
|
3544
|
+
for (const filePath of (data.confirmed_mutations || []))
|
|
3545
|
+
addMutatedPath(filePath);
|
|
3546
|
+
for (const event of (Array.isArray(job?.events) ? job.events : [])) {
|
|
3547
|
+
if (event?.type === 'file_mutation' && event?.action !== 'delete')
|
|
3548
|
+
addMutatedPath(event.path);
|
|
3549
|
+
if (event?.type === 'executor_complete') {
|
|
3550
|
+
let summary = event.summary;
|
|
3551
|
+
if (typeof summary === 'string') {
|
|
3552
|
+
try {
|
|
3553
|
+
summary = JSON.parse(summary);
|
|
5649
3554
|
}
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
// Accumulator/reducer bug: subtraction used in a reduce/total/sum context
|
|
5653
|
-
// e.g. "sum - item.price" in a function named calculateTotal, getTotal, etc.
|
|
5654
|
-
if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity|count)\b/.test(line) || /\w+\.\s*(price|cost|amount|value|total)\s*-\s*/.test(line)) {
|
|
5655
|
-
// Check enclosing function/method name for total/sum/aggregate semantics
|
|
5656
|
-
for (let j = i; j >= Math.max(0, i - 10); j--) {
|
|
5657
|
-
if (/(function\s+)?(calculate|calc|get|compute|sum|total|aggregate|accumulate)\s*(Total|Sum|Cost|Price|Amount|All)?\s*[=(]/i.test(lines[j]) || /\.(reduce|forEach|map)\s*\(/.test(lines[j])) {
|
|
5658
|
-
// Also check if this looks like an accumulator pattern (sum - something instead of sum + something)
|
|
5659
|
-
if (/\w+\s*-\s*\w+\.\s*(price|cost|amount|value|total|quantity)\b/.test(line)) {
|
|
5660
|
-
issues.push({ type: 'logic', line: lineNum, message: 'Subtraction used in an accumulation context (total/sum/reduce) — likely should be addition (+).', severity: 'error' });
|
|
5661
|
-
}
|
|
5662
|
-
break;
|
|
3555
|
+
catch {
|
|
3556
|
+
summary = null;
|
|
5663
3557
|
}
|
|
5664
3558
|
}
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
for (let j = i; j >= Math.max(0, i - 8); j--) {
|
|
5669
|
-
const funcMatch = lines[j].match(/function\s+(\w+)/i) || lines[j].match(/(?:const|let|var)\s+(\w+)\s*=/i);
|
|
5670
|
-
if (funcMatch) {
|
|
5671
|
-
const funcName = funcMatch[1].toLowerCase();
|
|
5672
|
-
if ((funcName.includes('total') || funcName.includes('sum') || funcName.includes('add')) && /\s-\s/.test(line) && !/\s\+\s/.test(line)) {
|
|
5673
|
-
const alreadyReported = issues.some(iss => iss.line === lineNum && iss.type === 'logic');
|
|
5674
|
-
if (!alreadyReported) {
|
|
5675
|
-
issues.push({ type: 'logic', line: lineNum, message: `Subtraction operator in "${funcMatch[1]}" — function name implies addition. Likely should use + instead of -.`, severity: 'error' });
|
|
5676
|
-
}
|
|
5677
|
-
}
|
|
5678
|
-
break;
|
|
3559
|
+
if (summary?.status === 'completed') {
|
|
3560
|
+
for (const filePath of [...(summary.changed_files || []), ...(summary.created_files || [])]) {
|
|
3561
|
+
addMutatedPath(filePath);
|
|
5679
3562
|
}
|
|
5680
3563
|
}
|
|
5681
3564
|
}
|
|
5682
|
-
// Off-by-one: comparing with === where <= or >= may be needed
|
|
5683
|
-
// (not implemented yet — would require more complex AST analysis)
|
|
5684
|
-
// Limit to 15 heuristic issues
|
|
5685
|
-
if (issues.length >= 15)
|
|
5686
|
-
break;
|
|
5687
|
-
}
|
|
5688
|
-
return issues;
|
|
5689
|
-
}
|
|
5690
|
-
async fixCode(code, language, fixType) {
|
|
5691
|
-
// Client-side syntax pre-check: detect obvious errors and include
|
|
5692
|
-
// them in the request so the model has concrete signals.
|
|
5693
|
-
const syntaxHints = fixType === 'bugs' || fixType === 'syntax' ? this.detectSyntaxErrors(code, language) : '';
|
|
5694
|
-
// Run heuristic logic analysis, but ONLY pass hints relevant to the fixType
|
|
5695
|
-
const heuristicIssues = this.heuristicCodeIssues(code, language);
|
|
5696
|
-
const relevantIssues = heuristicIssues.filter(issue => {
|
|
5697
|
-
if (fixType === 'bugs' || fixType === 'logic')
|
|
5698
|
-
return issue.type === 'logic' || issue.severity === 'error';
|
|
5699
|
-
if (fixType === 'syntax')
|
|
5700
|
-
return issue.type !== 'logic' && issue.severity === 'error';
|
|
5701
|
-
if (fixType === 'style')
|
|
5702
|
-
return issue.type === 'style' || issue.type === 'quality';
|
|
5703
|
-
if (fixType === 'security')
|
|
5704
|
-
return issue.type === 'security';
|
|
5705
|
-
if (fixType === 'performance')
|
|
5706
|
-
return issue.type === 'performance';
|
|
5707
|
-
return true; // 'all' or unknown fixType: include everything
|
|
5708
|
-
});
|
|
5709
|
-
const logicHints = relevantIssues
|
|
5710
|
-
.map(i => `Line ${i.line}: [${i.type}] ${i.message}`)
|
|
5711
|
-
.join('\n// ');
|
|
5712
|
-
const allHints = [syntaxHints, logicHints].filter(Boolean).join('\n// ');
|
|
5713
|
-
// Build a constraint preamble that's stricter for syntax-only mode.
|
|
5714
|
-
let preamble;
|
|
5715
|
-
if (fixType === 'syntax') {
|
|
5716
|
-
preamble = allHints
|
|
5717
|
-
? `// SYNTAX ERRORS DETECTED BY CLIENT:\n// ${allHints}\n// RULES: Fix ONLY bracket/parenthesis/brace mismatches and keyword typos. Do NOT rename functions, do NOT add comments, do NOT restructure or reformat. Output the corrected code ONLY.\n\n`
|
|
5718
|
-
: '';
|
|
5719
|
-
}
|
|
5720
|
-
else {
|
|
5721
|
-
preamble = allHints
|
|
5722
|
-
? `// BUGS DETECTED BY STATIC ANALYSIS — YOU MUST FIX THESE:\n// ${allHints}\n// IMPORTANT: Fix ONLY these specific bugs. Do not add comments, do not restructure the code, do not add or remove lines beyond the minimal fix.\n\n`
|
|
5723
|
-
: '';
|
|
5724
|
-
}
|
|
5725
|
-
const augmentedCode = preamble ? `${preamble}${code}` : code;
|
|
5726
|
-
const sysPrompt = [
|
|
5727
|
-
`You are a ${language} code fixer. Fix the code for: ${fixType}.`,
|
|
5728
|
-
'Return a JSON object with:',
|
|
5729
|
-
' "fixed": the COMPLETE corrected source code as a string (not a snippet — the full file),',
|
|
5730
|
-
' "changes": [{ "line": number, "before": string, "after": string, "reason": string }]',
|
|
5731
|
-
'Rules:',
|
|
5732
|
-
'- The "fixed" field MUST contain the entire corrected source code with ALL lines, including unchanged lines.',
|
|
5733
|
-
'- The "fixed" code MUST have balanced braces, parentheses, and brackets.',
|
|
5734
|
-
'- Fix ONLY the issues related to the fix type.',
|
|
5735
|
-
'- Do not add comments, do not restructure beyond the minimal fix.',
|
|
5736
|
-
'- Return ONLY the JSON object, no markdown fences.',
|
|
5737
|
-
].join('\n');
|
|
5738
|
-
let raw = {};
|
|
5739
|
-
try {
|
|
5740
|
-
const result = await this.chatComplete(sysPrompt, augmentedCode);
|
|
5741
|
-
const cleaned = result.replace(/^```[\w]*\n?/gm, '').replace(/\n?```$/gm, '').trim();
|
|
5742
|
-
raw = JSON.parse(cleaned);
|
|
5743
|
-
}
|
|
5744
|
-
catch {
|
|
5745
|
-
// If parsing fails, fall through to client-side handling
|
|
5746
|
-
}
|
|
5747
|
-
let fixed = typeof raw.fixed === 'string' ? raw.fixed : (typeof raw.code === 'string' ? raw.code : code);
|
|
5748
|
-
let changes = Array.isArray(raw.changes) ? raw.changes : [];
|
|
5749
|
-
// If server returned no changes but we found issues, strip
|
|
5750
|
-
// our injected comment prefix from the returned code and attempt
|
|
5751
|
-
// a basic client-side repair.
|
|
5752
|
-
if (changes.length === 0 && preamble && fixed === augmentedCode) {
|
|
5753
|
-
fixed = code; // restore original
|
|
5754
|
-
}
|
|
5755
|
-
// Strip the injected comment block if it leaked into the output
|
|
5756
|
-
if (fixed.startsWith('// BUGS DETECTED BY STATIC ANALYSIS') || fixed.startsWith('// SYNTAX ERRORS DETECTED BY CLIENT')) {
|
|
5757
|
-
const idx = fixed.indexOf('\n\n');
|
|
5758
|
-
if (idx !== -1) {
|
|
5759
|
-
fixed = fixed.slice(idx + 2);
|
|
5760
|
-
}
|
|
5761
3565
|
}
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
if (fixType === 'syntax' && fixed !== code) {
|
|
5765
|
-
fixed = this.stripInjectedComments(code, fixed, language);
|
|
5766
|
-
}
|
|
5767
|
-
// Safety net: for syntax fixes, ensure the fix didn't make bracket
|
|
5768
|
-
// balance worse. If the original had more closing delimiters than
|
|
5769
|
-
// the fix, append the missing ones.
|
|
5770
|
-
if (fixType === 'syntax' && fixed !== code) {
|
|
5771
|
-
fixed = this.repairBracketBalance(code, fixed);
|
|
5772
|
-
}
|
|
5773
|
-
// Final bracket-balance guarantee — ensure the emitted code has
|
|
5774
|
-
// balanced braces/parens/brackets regardless of what the model returned.
|
|
5775
|
-
fixed = this.ensureBalancedBraces(fixed);
|
|
5776
|
-
// For JS/TS syntax fixes, validate the output actually parses.
|
|
5777
|
-
// If it doesn't, attempt a more aggressive bracket repair.
|
|
5778
|
-
if ((fixType === 'syntax' || fixType === 'bugs') && fixed !== code) {
|
|
5779
|
-
const lang = language.toLowerCase();
|
|
5780
|
-
if (['javascript', 'js', 'typescript', 'ts'].includes(lang)) {
|
|
5781
|
-
if (!this.validateJsSyntax(fixed)) {
|
|
5782
|
-
// Try once more: strip any remaining injected comments and re-balance
|
|
5783
|
-
let repaired = this.stripInjectedComments(code, fixed, language);
|
|
5784
|
-
repaired = this.ensureBalancedBraces(repaired);
|
|
5785
|
-
if (this.validateJsSyntax(repaired)) {
|
|
5786
|
-
fixed = repaired;
|
|
5787
|
-
}
|
|
5788
|
-
// If still invalid, return the best-effort fix — better than
|
|
5789
|
-
// silently reverting to the original broken code.
|
|
5790
|
-
}
|
|
5791
|
-
}
|
|
3566
|
+
if (mutatedPaths.size === 0) {
|
|
3567
|
+
throw new Error(`Background job ${jobId} has no confirmed successful file mutations to apply.`);
|
|
5792
3568
|
}
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
changes = this.computeSemanticDiff(code, fixed, cleanReason);
|
|
3569
|
+
const rootPath = this.resolveAgentTargetPath({
|
|
3570
|
+
...context,
|
|
3571
|
+
workspacePath: context.workspacePath || data.local_workspace_path,
|
|
3572
|
+
projectPath: context.projectPath || data.local_workspace_path,
|
|
3573
|
+
targetPath: context.targetPath || data.local_workspace_path,
|
|
3574
|
+
});
|
|
3575
|
+
if (!rootPath) {
|
|
3576
|
+
throw new Error('No local workspace path available for applying background job files.');
|
|
5802
3577
|
}
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
computeSemanticDiff(original, fixed, reason) {
|
|
5811
|
-
const origLines = original.split('\n');
|
|
5812
|
-
const fixedLines = fixed.split('\n');
|
|
5813
|
-
const changes = [];
|
|
5814
|
-
// Build LCS table
|
|
5815
|
-
const m = origLines.length;
|
|
5816
|
-
const n = fixedLines.length;
|
|
5817
|
-
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
5818
|
-
for (let i = 1; i <= m; i++) {
|
|
5819
|
-
for (let j = 1; j <= n; j++) {
|
|
5820
|
-
if (origLines[i - 1] === fixedLines[j - 1]) {
|
|
5821
|
-
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
5822
|
-
}
|
|
5823
|
-
else {
|
|
5824
|
-
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
5825
|
-
}
|
|
3578
|
+
const applied = [];
|
|
3579
|
+
const skipped = [];
|
|
3580
|
+
for (const [relativePath, content] of Object.entries(data.files || {})) {
|
|
3581
|
+
const normalizedRelativePath = this.normalizeWorkspaceRelativePath(relativePath);
|
|
3582
|
+
if (!mutatedPaths.has(normalizedRelativePath)) {
|
|
3583
|
+
skipped.push(relativePath);
|
|
3584
|
+
continue;
|
|
5826
3585
|
}
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
const ops = [];
|
|
5831
|
-
while (i > 0 || j > 0) {
|
|
5832
|
-
if (i > 0 && j > 0 && origLines[i - 1] === fixedLines[j - 1]) {
|
|
5833
|
-
ops.push({ type: 'keep', origLine: i, fixedLine: j, text: origLines[i - 1] });
|
|
5834
|
-
i--;
|
|
5835
|
-
j--;
|
|
3586
|
+
if (typeof content !== 'string') {
|
|
3587
|
+
skipped.push(relativePath);
|
|
3588
|
+
continue;
|
|
5836
3589
|
}
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
j--;
|
|
3590
|
+
if (this.writeV3AgentWorkspaceFile(rootPath, relativePath, content, rootPath)) {
|
|
3591
|
+
applied.push(relativePath);
|
|
5840
3592
|
}
|
|
5841
3593
|
else {
|
|
5842
|
-
|
|
5843
|
-
i--;
|
|
5844
|
-
}
|
|
5845
|
-
}
|
|
5846
|
-
ops.reverse();
|
|
5847
|
-
// Merge adjacent remove+add pairs into single change entries
|
|
5848
|
-
let idx = 0;
|
|
5849
|
-
while (idx < ops.length) {
|
|
5850
|
-
const op = ops[idx];
|
|
5851
|
-
if (op.type === 'remove') {
|
|
5852
|
-
// Look ahead for a matching 'add' immediately after
|
|
5853
|
-
if (idx + 1 < ops.length && ops[idx + 1].type === 'add') {
|
|
5854
|
-
changes.push({
|
|
5855
|
-
line: op.origLine,
|
|
5856
|
-
before: op.text,
|
|
5857
|
-
after: ops[idx + 1].text,
|
|
5858
|
-
reason: reason || 'AI-suggested fix',
|
|
5859
|
-
});
|
|
5860
|
-
idx += 2;
|
|
5861
|
-
continue;
|
|
5862
|
-
}
|
|
5863
|
-
changes.push({
|
|
5864
|
-
line: op.origLine,
|
|
5865
|
-
before: op.text,
|
|
5866
|
-
after: '(line removed)',
|
|
5867
|
-
reason: reason || 'AI-suggested fix',
|
|
5868
|
-
});
|
|
5869
|
-
}
|
|
5870
|
-
else if (op.type === 'add') {
|
|
5871
|
-
changes.push({
|
|
5872
|
-
line: op.fixedLine,
|
|
5873
|
-
before: '(new line)',
|
|
5874
|
-
after: op.text,
|
|
5875
|
-
reason: reason || 'AI-suggested fix',
|
|
5876
|
-
});
|
|
3594
|
+
skipped.push(relativePath);
|
|
5877
3595
|
}
|
|
5878
|
-
idx++;
|
|
5879
|
-
}
|
|
5880
|
-
if (changes.length === 0) {
|
|
5881
|
-
changes.push({ line: 1, before: '(whitespace changes)', after: '(see fixed file)', reason: reason || 'AI-suggested fix' });
|
|
5882
3596
|
}
|
|
5883
|
-
return
|
|
3597
|
+
return { applied, skipped, status: data.status };
|
|
5884
3598
|
}
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
const
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
if (inBlockComment) {
|
|
5908
|
-
if (ch === '*' && next === '/') {
|
|
5909
|
-
inBlockComment = false;
|
|
5910
|
-
i++;
|
|
5911
|
-
}
|
|
5912
|
-
continue;
|
|
5913
|
-
}
|
|
5914
|
-
if (inString) {
|
|
5915
|
-
if (ch === inString && code[i - 1] !== '\\')
|
|
5916
|
-
inString = null;
|
|
5917
|
-
continue;
|
|
5918
|
-
}
|
|
5919
|
-
if (ch === '/' && next === '/') {
|
|
5920
|
-
inLineComment = true;
|
|
5921
|
-
continue;
|
|
5922
|
-
}
|
|
5923
|
-
if (ch === '/' && next === '*') {
|
|
5924
|
-
inBlockComment = true;
|
|
5925
|
-
continue;
|
|
5926
|
-
}
|
|
5927
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
5928
|
-
inString = ch;
|
|
5929
|
-
continue;
|
|
5930
|
-
}
|
|
5931
|
-
if (ch === '{')
|
|
5932
|
-
braces++;
|
|
5933
|
-
else if (ch === '}')
|
|
5934
|
-
braces--;
|
|
5935
|
-
else if (ch === '[')
|
|
5936
|
-
brackets++;
|
|
5937
|
-
else if (ch === ']')
|
|
5938
|
-
brackets--;
|
|
5939
|
-
else if (ch === '(')
|
|
5940
|
-
parens++;
|
|
5941
|
-
else if (ch === ')')
|
|
5942
|
-
parens--;
|
|
5943
|
-
}
|
|
5944
|
-
if (braces !== 0)
|
|
5945
|
-
errors.push(`Mismatched braces (balance: ${braces > 0 ? '+' : ''}${braces})`);
|
|
5946
|
-
if (brackets !== 0)
|
|
5947
|
-
errors.push(`Mismatched brackets (balance: ${brackets > 0 ? '+' : ''}${brackets})`);
|
|
5948
|
-
if (parens !== 0)
|
|
5949
|
-
errors.push(`Mismatched parentheses (balance: ${parens > 0 ? '+' : ''}${parens})`);
|
|
5950
|
-
// Detect obvious keyword typos
|
|
5951
|
-
const lines = code.split('\n');
|
|
5952
|
-
for (let li = 0; li < lines.length; li++) {
|
|
5953
|
-
const line = lines[li];
|
|
5954
|
-
if (/\bfuncion\b|\bfuction\b|\bfuntion\b/i.test(line))
|
|
5955
|
-
errors.push(`Line ${li + 1}: Misspelled 'function'`);
|
|
5956
|
-
if (/\bretrun\b/i.test(line))
|
|
5957
|
-
errors.push(`Line ${li + 1}: Misspelled 'return'`);
|
|
5958
|
-
if (/\bconts\b|\bcosnt\b/i.test(line))
|
|
5959
|
-
errors.push(`Line ${li + 1}: Misspelled 'const'`);
|
|
5960
|
-
}
|
|
3599
|
+
async runOperatorWorkflow(message, context = {}) {
|
|
3600
|
+
const executionContext = await this.bindExecutionContext(context);
|
|
3601
|
+
const requestedOperatorTimeoutMs = Number(context.operatorTimeoutMs ?? DEFAULT_OPERATOR_TIMEOUT_MS);
|
|
3602
|
+
const timeoutMs = Number.isFinite(requestedOperatorTimeoutMs) && requestedOperatorTimeoutMs > 0
|
|
3603
|
+
? requestedOperatorTimeoutMs
|
|
3604
|
+
: 0;
|
|
3605
|
+
const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
|
|
3606
|
+
const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
3607
|
+
const operatorWorkspaceAlias = `vigthoria://workspace/${encodeURIComponent(path.basename(workspacePath))}`;
|
|
3608
|
+
const requestedModel = String(executionContext.model || 'code');
|
|
3609
|
+
const resolvedModel = this.resolveModelId(requestedModel);
|
|
3610
|
+
try {
|
|
3611
|
+
return await this.operatorClient.run({
|
|
3612
|
+
message,
|
|
3613
|
+
executionContext,
|
|
3614
|
+
workspaceAlias: operatorWorkspaceAlias,
|
|
3615
|
+
workspaceSummary,
|
|
3616
|
+
requestedModel,
|
|
3617
|
+
resolvedModel,
|
|
3618
|
+
timeoutMs,
|
|
3619
|
+
onStreamEvent: typeof context.onStreamEvent === 'function' ? context.onStreamEvent : undefined,
|
|
3620
|
+
});
|
|
5961
3621
|
}
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
const line = lines[li];
|
|
5966
|
-
if (/^\s*(def|class|if|elif|else|for|while|try|except|with)\b/.test(line) && !line.trimEnd().endsWith(':') && !line.trimEnd().endsWith('\\')) {
|
|
5967
|
-
errors.push(`Line ${li + 1}: Missing colon after '${line.trim().split(/\s/)[0]}'`);
|
|
5968
|
-
}
|
|
3622
|
+
catch (error) {
|
|
3623
|
+
if (error instanceof OperatorClientError) {
|
|
3624
|
+
throw new CLIError(error.message, error.category, { cause: error });
|
|
5969
3625
|
}
|
|
3626
|
+
throw error;
|
|
5970
3627
|
}
|
|
5971
|
-
return errors.join('; ');
|
|
5972
3628
|
}
|
|
5973
3629
|
/**
|
|
5974
|
-
*
|
|
5975
|
-
*
|
|
5976
|
-
*
|
|
3630
|
+
* Chat API - Direct Vigthoria Models API Architecture
|
|
3631
|
+
*
|
|
3632
|
+
* PRIMARY: api.vigthoria.io - Direct access to Vigthoria Model Router
|
|
3633
|
+
* This is the centralized public API for all AI operations.
|
|
3634
|
+
* Works for both authenticated and unauthenticated users.
|
|
3635
|
+
*
|
|
3636
|
+
* FALLBACK: Coder Cloud API - For authenticated users only
|
|
3637
|
+
*
|
|
3638
|
+
* NO localhost fallbacks - CLI is for external users, not server-side!
|
|
5977
3639
|
*/
|
|
5978
|
-
|
|
5979
|
-
const
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
if (
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
}
|
|
5987
|
-
else {
|
|
5988
|
-
wholeLineRe = /^\s*\/\/\s/;
|
|
5989
|
-
inlineRe = /\s+\/\/\s.*$/;
|
|
5990
|
-
}
|
|
5991
|
-
const origLines = original.split('\n');
|
|
5992
|
-
const origSet = new Set(origLines.map(l => l.trim()));
|
|
5993
|
-
const fixedLines = fixed.split('\n');
|
|
5994
|
-
const result = [];
|
|
5995
|
-
for (let idx = 0; idx < fixedLines.length; idx++) {
|
|
5996
|
-
let line = fixedLines[idx];
|
|
5997
|
-
// Remove whole-line comments not in original
|
|
5998
|
-
if (wholeLineRe.test(line) && !origSet.has(line.trim())) {
|
|
5999
|
-
continue;
|
|
6000
|
-
}
|
|
6001
|
-
// Strip inline trailing comments the model added.
|
|
6002
|
-
// Only strip if the corresponding original line at the same position
|
|
6003
|
-
// didn't have an inline comment at all.
|
|
6004
|
-
if (inlineRe.test(line)) {
|
|
6005
|
-
const origLine = idx < origLines.length ? origLines[idx] : '';
|
|
6006
|
-
if (!inlineRe.test(origLine)) {
|
|
6007
|
-
line = line.replace(inlineRe, '');
|
|
6008
|
-
}
|
|
6009
|
-
}
|
|
6010
|
-
result.push(line);
|
|
6011
|
-
}
|
|
6012
|
-
return result.join('\n');
|
|
3640
|
+
async chat(messages, model, _useLocal = false) {
|
|
3641
|
+
const response = await this.modelTransportService.chat(messages, model);
|
|
3642
|
+
this.lastChatTransportErrors = this.modelTransportService.getLastErrors();
|
|
3643
|
+
if (response)
|
|
3644
|
+
return response;
|
|
3645
|
+
if (this.lastChatTransportErrors.length > 0)
|
|
3646
|
+
this.logger.debug(`Chat transport failures: ${this.lastChatTransportErrors.slice(0, 4).join(' | ')}`);
|
|
3647
|
+
throw new CLIError(VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE, 'model_backend');
|
|
6013
3648
|
}
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
if (ch === inStr && src[i - 1] !== '\\')
|
|
6040
|
-
inStr = null;
|
|
6041
|
-
continue;
|
|
6042
|
-
}
|
|
6043
|
-
if (ch === '/' && nx === '/') {
|
|
6044
|
-
inLine = true;
|
|
6045
|
-
continue;
|
|
6046
|
-
}
|
|
6047
|
-
if (ch === '/' && nx === '*') {
|
|
6048
|
-
inBlock = true;
|
|
6049
|
-
continue;
|
|
6050
|
-
}
|
|
6051
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
6052
|
-
inStr = ch;
|
|
6053
|
-
continue;
|
|
6054
|
-
}
|
|
6055
|
-
if (ch === '{')
|
|
6056
|
-
braces++;
|
|
6057
|
-
else if (ch === '}')
|
|
6058
|
-
braces--;
|
|
6059
|
-
else if (ch === '(')
|
|
6060
|
-
parens++;
|
|
6061
|
-
else if (ch === ')')
|
|
6062
|
-
parens--;
|
|
6063
|
-
else if (ch === '[')
|
|
6064
|
-
brackets++;
|
|
6065
|
-
else if (ch === ']')
|
|
6066
|
-
brackets--;
|
|
6067
|
-
}
|
|
6068
|
-
return { braces, parens, brackets };
|
|
6069
|
-
};
|
|
6070
|
-
const orig = count(original);
|
|
6071
|
-
const fix = count(fixed);
|
|
6072
|
-
const append = [];
|
|
6073
|
-
// If original was balanced (or close) but fix is missing closers,
|
|
6074
|
-
// append them. Only act when the fix is *more unbalanced* than original.
|
|
6075
|
-
if (fix.braces > orig.braces) {
|
|
6076
|
-
for (let i = 0; i < fix.braces - orig.braces; i++)
|
|
6077
|
-
append.push('}');
|
|
6078
|
-
}
|
|
6079
|
-
if (fix.parens > orig.parens) {
|
|
6080
|
-
for (let i = 0; i < fix.parens - orig.parens; i++)
|
|
6081
|
-
append.push(')');
|
|
6082
|
-
}
|
|
6083
|
-
if (fix.brackets > orig.brackets) {
|
|
6084
|
-
for (let i = 0; i < fix.brackets - orig.brackets; i++)
|
|
6085
|
-
append.push(']');
|
|
6086
|
-
}
|
|
6087
|
-
if (append.length > 0) {
|
|
6088
|
-
return fixed.trimEnd() + '\n' + append.join('\n');
|
|
6089
|
-
}
|
|
6090
|
-
return fixed;
|
|
3649
|
+
resolvePermittedModelId(shortName) {
|
|
3650
|
+
return this.modelGovernance.resolvePermittedModelId(shortName);
|
|
3651
|
+
}
|
|
3652
|
+
// ─── Chat completion helper ────────────────────────────────────────
|
|
3653
|
+
// Routes all AI file commands through the Model Router
|
|
3654
|
+
// (/v1/chat/completions on api.vigthoria.io) which is the only
|
|
3655
|
+
// backend that reliably accepts our auth token.
|
|
3656
|
+
async chatComplete(systemPrompt, userPrompt, model, maxTokens) {
|
|
3657
|
+
return this.modelTransportService.complete(systemPrompt, userPrompt, model, maxTokens);
|
|
3658
|
+
}
|
|
3659
|
+
// Compatibility facade for code generation/review commands.
|
|
3660
|
+
generateCode(prompt, language, model) {
|
|
3661
|
+
return this.codeOperations.generateCode(prompt, language, model);
|
|
3662
|
+
}
|
|
3663
|
+
generateProject(prompt, projectType, model) {
|
|
3664
|
+
return this.codeOperations.generateProject(prompt, projectType, model);
|
|
3665
|
+
}
|
|
3666
|
+
explainCode(code, language) {
|
|
3667
|
+
return this.codeOperations.explainCode(code, language);
|
|
3668
|
+
}
|
|
3669
|
+
reviewCode(code, language) {
|
|
3670
|
+
return this.codeOperations.reviewCode(code, language);
|
|
3671
|
+
}
|
|
3672
|
+
fixCode(code, language, fixType) {
|
|
3673
|
+
return this.codeOperations.fixCode(code, language, fixType);
|
|
6091
3674
|
}
|
|
6092
3675
|
// Model resolution - maps Vigthoria model names to internal IDs
|
|
6093
3676
|
// INTERNAL USE ONLY - users see only Vigthoria branding
|
|
6094
3677
|
resolveModelId(shortName) {
|
|
6095
|
-
|
|
6096
|
-
// ═══════════════════════════════════════════════════════════════
|
|
6097
|
-
// Vigthoria server infrastructure models
|
|
6098
|
-
// ═══════════════════════════════════════════════════════════════
|
|
6099
|
-
'fast': 'Vigthoria-v4-Assistant-9B',
|
|
6100
|
-
'mini': 'vigthoria-mini-0.6b',
|
|
6101
|
-
'balanced': 'Vigthoria-v4-Assistant-9B',
|
|
6102
|
-
'balanced-4b': 'Vigthoria-v4-Assistant-9B',
|
|
6103
|
-
'assistant': 'Vigthoria-v4-Assistant-9B',
|
|
6104
|
-
'creative': 'Vigthoria-v4-Creative-27B',
|
|
6105
|
-
'architect': 'Vigthoria-v4-Creative-27B',
|
|
6106
|
-
// Code Models - 35B is the default powerhouse
|
|
6107
|
-
'code': 'Vigthoria-v4-Code-27B',
|
|
6108
|
-
'code-30b': 'Vigthoria-v4-Code-27B',
|
|
6109
|
-
'code-35b': 'Vigthoria-v4-Code-27B',
|
|
6110
|
-
'code-8b': 'Vigthoria-v4-Assistant-9B',
|
|
6111
|
-
'code-9b': 'Vigthoria-v4-Assistant-9B',
|
|
6112
|
-
'pro': 'Vigthoria-v4-Code-27B',
|
|
6113
|
-
'agent': 'Vigthoria-v4-Code-27B',
|
|
6114
|
-
'vigthoria-code': 'Vigthoria-v4-Code-27B',
|
|
6115
|
-
'vigthoria-agent': 'Vigthoria-v4-Code-27B',
|
|
6116
|
-
// ═══════════════════════════════════════════════════════════════
|
|
6117
|
-
// VIGTHORIA CLOUD - current billing catalog aliases
|
|
6118
|
-
// ═══════════════════════════════════════════════════════════════
|
|
6119
|
-
'cloud': 'vigthoria-cloud-balanced',
|
|
6120
|
-
'cloud-code': 'vigthoria-cloud-code',
|
|
6121
|
-
'cloud-balanced': 'vigthoria-cloud-balanced',
|
|
6122
|
-
'cloud-fast': 'vigthoria-cloud-fast',
|
|
6123
|
-
'cloud-power': 'vigthoria-cloud-power',
|
|
6124
|
-
'cloud-maximum': 'vigthoria-cloud-maximum',
|
|
6125
|
-
'cloud-reason': 'vigthoria-cloud-power',
|
|
6126
|
-
'ultra': 'vigthoria-cloud-maximum',
|
|
6127
|
-
// Legacy aliases kept for backward compatibility
|
|
6128
|
-
'cloud-pro': 'vigthoria-cloud-power',
|
|
6129
|
-
'cloud-k2': 'vigthoria-cloud-power',
|
|
6130
|
-
'cloud-ultra': 'vigthoria-cloud-maximum',
|
|
6131
|
-
};
|
|
6132
|
-
// If already a full model ID, return as-is
|
|
6133
|
-
const normalizedShortName = String(shortName || '').toLowerCase();
|
|
6134
|
-
if (normalizedShortName.includes('vigthoria') || shortName.includes('/') || shortName.includes(':')) {
|
|
6135
|
-
if (modelMap[normalizedShortName]) {
|
|
6136
|
-
return modelMap[normalizedShortName];
|
|
6137
|
-
}
|
|
6138
|
-
return shortName;
|
|
6139
|
-
}
|
|
6140
|
-
return modelMap[normalizedShortName] || 'Vigthoria-v4-Code-27B';
|
|
3678
|
+
return this.modelGovernance.resolveModelId(shortName);
|
|
6141
3679
|
}
|
|
6142
3680
|
async getCoderHealth() {
|
|
6143
3681
|
try {
|
|
@@ -6164,75 +3702,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6164
3702
|
* so users see a clear pass/fail before "Planning..." hangs on slow routes.
|
|
6165
3703
|
*/
|
|
6166
3704
|
async runChatModelPreflight(requestedModel = 'agent') {
|
|
6167
|
-
|
|
6168
|
-
const probes = [
|
|
6169
|
-
{
|
|
6170
|
-
name: 'Vigthoria Coder API',
|
|
6171
|
-
run: async () => {
|
|
6172
|
-
const health = await this.getCoderHealth();
|
|
6173
|
-
return { ok: health.ok, error: health.error };
|
|
6174
|
-
},
|
|
6175
|
-
},
|
|
6176
|
-
{
|
|
6177
|
-
name: 'Vigthoria Models API',
|
|
6178
|
-
run: async () => {
|
|
6179
|
-
const health = await this.getModelsHealth();
|
|
6180
|
-
return { ok: health.ok, error: health.error };
|
|
6181
|
-
},
|
|
6182
|
-
},
|
|
6183
|
-
];
|
|
6184
|
-
const selfHostedHealth = await this.getSelfHostedHealth();
|
|
6185
|
-
if (selfHostedHealth) {
|
|
6186
|
-
const sh = selfHostedHealth;
|
|
6187
|
-
probes.unshift({
|
|
6188
|
-
name: sh.name || 'Dedicated Models API',
|
|
6189
|
-
run: async () => ({ ok: sh.ok, error: sh.error }),
|
|
6190
|
-
});
|
|
6191
|
-
}
|
|
6192
|
-
if (this.isSelfHostedPreferredModel(this.resolveModelId(requestedModel), requestedModel)) {
|
|
6193
|
-
// Agent/code models should also accept V3 health as a usable inference path.
|
|
6194
|
-
probes.push({
|
|
6195
|
-
name: 'V3 Agent API',
|
|
6196
|
-
run: async () => {
|
|
6197
|
-
const v3 = await this.runV3HealthCheck();
|
|
6198
|
-
return { ok: v3.healthy, error: v3.error };
|
|
6199
|
-
},
|
|
6200
|
-
});
|
|
6201
|
-
}
|
|
6202
|
-
const results = await Promise.all(probes.map(async (probe) => {
|
|
6203
|
-
try {
|
|
6204
|
-
const result = await probe.run();
|
|
6205
|
-
routes.push({ name: probe.name, ok: result.ok, error: result.error });
|
|
6206
|
-
return result.ok;
|
|
6207
|
-
}
|
|
6208
|
-
catch (error) {
|
|
6209
|
-
const message = error?.message || String(error);
|
|
6210
|
-
routes.push({ name: probe.name, ok: false, error: message });
|
|
6211
|
-
return false;
|
|
6212
|
-
}
|
|
6213
|
-
}));
|
|
6214
|
-
const healthy = results.some(Boolean);
|
|
6215
|
-
const inferenceRoutes = routes.filter((route) => route.name !== 'V3 Agent API');
|
|
6216
|
-
const firstHealthy = inferenceRoutes.find((route) => route.ok) || routes.find((route) => route.ok);
|
|
6217
|
-
return {
|
|
6218
|
-
healthy,
|
|
6219
|
-
endpoint: firstHealthy?.name || 'api.vigthoria.io',
|
|
6220
|
-
error: healthy
|
|
6221
|
-
? undefined
|
|
6222
|
-
: 'No Vigthoria model backend responded during preflight. Run `vigthoria login` and check your internet connection.',
|
|
6223
|
-
routes,
|
|
6224
|
-
};
|
|
3705
|
+
return this.capabilityHealthService.runModelPreflight(requestedModel);
|
|
6225
3706
|
}
|
|
6226
3707
|
mapPreflightEndpointToRoute(endpoint) {
|
|
6227
|
-
|
|
6228
|
-
if (normalized.includes('coder'))
|
|
6229
|
-
return 'coder';
|
|
6230
|
-
if (normalized.includes('models'))
|
|
6231
|
-
return 'models';
|
|
6232
|
-
if (normalized.includes('dedicated') || normalized.includes('self-hosted') || normalized.includes('selfhosted')) {
|
|
6233
|
-
return 'selfhosted';
|
|
6234
|
-
}
|
|
6235
|
-
return null;
|
|
3708
|
+
return this.capabilityHealthService.mapPreflightEndpoint(endpoint);
|
|
6236
3709
|
}
|
|
6237
3710
|
getLastChatTransportErrors() {
|
|
6238
3711
|
return this.lastChatTransportErrors;
|
|
@@ -6272,7 +3745,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6272
3745
|
error: String(result.error || ''),
|
|
6273
3746
|
});
|
|
6274
3747
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
6275
|
-
const response = await fetch(endpoint, {
|
|
3748
|
+
const response = await this.fetch(endpoint, {
|
|
6276
3749
|
method: 'POST',
|
|
6277
3750
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
6278
3751
|
body,
|
|
@@ -6404,7 +3877,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6404
3877
|
try {
|
|
6405
3878
|
const controller = new AbortController();
|
|
6406
3879
|
const timer = setTimeout(() => controller.abort(), 12000);
|
|
6407
|
-
const response = await fetch(endpoint, {
|
|
3880
|
+
const response = await this.fetch(endpoint, {
|
|
6408
3881
|
method: 'GET',
|
|
6409
3882
|
headers,
|
|
6410
3883
|
signal: controller.signal,
|
|
@@ -6452,7 +3925,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6452
3925
|
try {
|
|
6453
3926
|
const controller = new AbortController();
|
|
6454
3927
|
const timer = setTimeout(() => controller.abort(), 2000);
|
|
6455
|
-
const probe = await fetch(runUrl, { method: 'OPTIONS', headers, signal: controller.signal });
|
|
3928
|
+
const probe = await this.fetch(runUrl, { method: 'OPTIONS', headers, signal: controller.signal });
|
|
6456
3929
|
clearTimeout(timer);
|
|
6457
3930
|
if (probe.ok || probe.status === 204 || probe.status === 405) {
|
|
6458
3931
|
return {
|
|
@@ -6518,6 +3991,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6518
3991
|
...(hyperLoopServiceKey ? { 'X-Service-Key': hyperLoopServiceKey } : {}),
|
|
6519
3992
|
};
|
|
6520
3993
|
const projectPath = this.resolveAgentTargetPath(context);
|
|
3994
|
+
const projectAlias = `vigthoria://workspace/${encodeURIComponent(path.basename(projectPath || 'workspace'))}`;
|
|
6521
3995
|
try {
|
|
6522
3996
|
const modulesResponse = await fetchWithServiceTimeout(modulesEndpoint, {
|
|
6523
3997
|
method: 'GET',
|
|
@@ -6537,13 +4011,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6537
4011
|
body: JSON.stringify({
|
|
6538
4012
|
module: 'repo_context_compactor',
|
|
6539
4013
|
payload: {
|
|
6540
|
-
path:
|
|
4014
|
+
path: projectAlias,
|
|
6541
4015
|
request_focus: 'Capability truth status probe',
|
|
6542
4016
|
max_files: 20,
|
|
6543
4017
|
max_chars: 800,
|
|
6544
4018
|
},
|
|
6545
4019
|
context: {
|
|
6546
|
-
project_path:
|
|
4020
|
+
project_path: projectAlias,
|
|
6547
4021
|
source: 'vigthoria-cli-capability-truth',
|
|
6548
4022
|
},
|
|
6549
4023
|
}),
|
|
@@ -6608,19 +4082,38 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6608
4082
|
message: 'V3 service is reachable again — retrying the workflow once.',
|
|
6609
4083
|
};
|
|
6610
4084
|
}
|
|
6611
|
-
async
|
|
4085
|
+
async probeDevtoolsBridgeStatus() {
|
|
6612
4086
|
const host = process.env.VIGTHORIA_DEVTOOLS_BRIDGE_HOST || '127.0.0.1';
|
|
6613
4087
|
const port = Number.parseInt(process.env.VIGTHORIA_DEVTOOLS_BRIDGE_PORT || '4016', 10);
|
|
6614
4088
|
const endpoint = `ws://${host}:${port}/ws`;
|
|
6615
4089
|
const healthEndpoint = `http://${host}:${port}/health`;
|
|
4090
|
+
// The HTTP policy protects fetch, but the legacy compatibility probe below
|
|
4091
|
+
// opens a raw TCP socket. Validate once before either transport so offline
|
|
4092
|
+
// mode and explicit-loopback consent cannot be bypassed by the fallback.
|
|
4093
|
+
try {
|
|
4094
|
+
assertTrustedEndpoint(healthEndpoint, { audience: 'bridge' });
|
|
4095
|
+
}
|
|
4096
|
+
catch (error) {
|
|
4097
|
+
return {
|
|
4098
|
+
name: 'DevTools Bridge',
|
|
4099
|
+
endpoint,
|
|
4100
|
+
ok: false,
|
|
4101
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
6616
4104
|
try {
|
|
6617
4105
|
const controller = new AbortController();
|
|
6618
4106
|
const timer = setTimeout(() => controller.abort(), 1500);
|
|
6619
|
-
|
|
6620
|
-
|
|
6621
|
-
|
|
6622
|
-
|
|
6623
|
-
|
|
4107
|
+
let response;
|
|
4108
|
+
try {
|
|
4109
|
+
response = await this.fetch(healthEndpoint, {
|
|
4110
|
+
signal: controller.signal,
|
|
4111
|
+
headers: { Accept: 'application/json' },
|
|
4112
|
+
});
|
|
4113
|
+
}
|
|
4114
|
+
finally {
|
|
4115
|
+
clearTimeout(timer);
|
|
4116
|
+
}
|
|
6624
4117
|
if (response.ok) {
|
|
6625
4118
|
return {
|
|
6626
4119
|
name: 'DevTools Bridge',
|
|
@@ -6668,40 +4161,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6668
4161
|
});
|
|
6669
4162
|
});
|
|
6670
4163
|
}
|
|
4164
|
+
async getDevtoolsBridgeStatus() {
|
|
4165
|
+
return this.probeDevtoolsBridgeStatus();
|
|
4166
|
+
}
|
|
6671
4167
|
async getCapabilityTruthStatus(context = {}) {
|
|
6672
|
-
|
|
6673
|
-
// Keep a finite ceiling, but do not turn a healthy slow edge into the old
|
|
6674
|
-
// hard 10-second preflight failure seen on external client machines.
|
|
6675
|
-
const withTimeout = (p, name) => Promise.race([
|
|
6676
|
-
p,
|
|
6677
|
-
new Promise(resolve => setTimeout(() => resolve({ name, endpoint: '', ok: false, error: 'Service not reachable (15 s timeout)' }), 15000)),
|
|
6678
|
-
]);
|
|
6679
|
-
const [v3Agent, hyperLoop, repoMemory, devtoolsBridge] = await Promise.all([
|
|
6680
|
-
withTimeout(this.getV3AgentHealth(), 'V3 Agent'),
|
|
6681
|
-
withTimeout(this.getHyperLoopHealth(), 'Hyper Loop'),
|
|
6682
|
-
withTimeout(this.getRepoMemoryHealth(context), 'Repo Memory'),
|
|
6683
|
-
withTimeout(this.getDevtoolsBridgeStatus(), 'DevTools Bridge'),
|
|
6684
|
-
]);
|
|
6685
|
-
return {
|
|
6686
|
-
overallOk: v3Agent.ok && (hyperLoop.ok || repoMemory.ok),
|
|
6687
|
-
v3Agent,
|
|
6688
|
-
hyperLoop,
|
|
6689
|
-
repoMemory,
|
|
6690
|
-
devtoolsBridge,
|
|
6691
|
-
};
|
|
4168
|
+
return this.capabilityHealthService.getCapabilityTruth(context);
|
|
6692
4169
|
}
|
|
6693
4170
|
async getHealthStatus() {
|
|
6694
|
-
|
|
6695
|
-
this.getCoderHealth(),
|
|
6696
|
-
this.getModelsHealth(),
|
|
6697
|
-
this.getSelfHostedHealth(),
|
|
6698
|
-
]);
|
|
6699
|
-
return {
|
|
6700
|
-
overallOk: coder.ok && models.ok,
|
|
6701
|
-
coder,
|
|
6702
|
-
models,
|
|
6703
|
-
selfHosted,
|
|
6704
|
-
};
|
|
4171
|
+
return this.capabilityHealthService.getHealth();
|
|
6705
4172
|
}
|
|
6706
4173
|
// Health check
|
|
6707
4174
|
async healthCheck() {
|