vigthoria-cli 1.11.37 → 1.11.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/bridge.js +8 -2
- package/dist/commands/chat.d.ts +1 -0
- package/dist/commands/chat.js +105 -10
- package/dist/index.js +5 -0
- package/dist/utils/agentRoute.js +17 -0
- package/dist/utils/api.d.ts +1 -0
- package/dist/utils/api.js +164 -47
- package/dist/utils/clientManifest.d.ts +12 -0
- package/dist/utils/clientManifest.js +68 -0
- package/dist/utils/localTestMode.d.ts +9 -0
- package/dist/utils/localTestMode.js +42 -0
- package/dist/utils/session.d.ts +1 -0
- package/dist/utils/session.js +36 -3
- package/dist/utils/templateInstantPath.js +18 -0
- package/dist/utils/v3-workspace-path.d.ts +2 -0
- package/dist/utils/v3-workspace-path.js +42 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +2 -1
- package/scripts/release/validate-no-go-gates.sh +4 -1
package/dist/commands/bridge.js
CHANGED
|
@@ -2,6 +2,12 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { createSpinner } from '../utils/logger.js';
|
|
3
3
|
import { APIClient } from '../utils/api.js';
|
|
4
4
|
import { getDesktopBridgeStatus } from '../utils/desktop-bridge-client.js';
|
|
5
|
+
function displayEndpoint(endpoint) {
|
|
6
|
+
if (/^(?:wss?|https?):\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?/i.test(endpoint)) {
|
|
7
|
+
return 'local loopback endpoint';
|
|
8
|
+
}
|
|
9
|
+
return endpoint;
|
|
10
|
+
}
|
|
5
11
|
export class BridgeCommand {
|
|
6
12
|
api;
|
|
7
13
|
constructor(config, logger) {
|
|
@@ -18,7 +24,7 @@ export class BridgeCommand {
|
|
|
18
24
|
console.log(chalk.white('DevTools Bridge (browser, port 4016):'));
|
|
19
25
|
console.log(chalk.gray(' Status: ') + (devtoolsBridge.ok ? chalk.green('Reachable') : chalk.yellow('Not running')));
|
|
20
26
|
if (devtoolsBridge.ok) {
|
|
21
|
-
console.log(chalk.gray(' Endpoint: ') + devtoolsBridge.endpoint);
|
|
27
|
+
console.log(chalk.gray(' Endpoint: ') + displayEndpoint(devtoolsBridge.endpoint));
|
|
22
28
|
}
|
|
23
29
|
else {
|
|
24
30
|
const detail = String(devtoolsBridge.error || 'Connection refused').trim() || 'Connection refused';
|
|
@@ -31,7 +37,7 @@ export class BridgeCommand {
|
|
|
31
37
|
console.log(chalk.white('Desktop Bridge (Windows desktop, port 49160):'));
|
|
32
38
|
console.log(chalk.gray(' Status: ') + (desktopBridge.ok ? chalk.green('Reachable') : chalk.yellow('Not running')));
|
|
33
39
|
if (desktopBridge.ok) {
|
|
34
|
-
console.log(chalk.gray(' Endpoint: ') + desktopBridge.endpoint);
|
|
40
|
+
console.log(chalk.gray(' Endpoint: ') + displayEndpoint(desktopBridge.endpoint));
|
|
35
41
|
if (desktopBridge.service)
|
|
36
42
|
console.log(chalk.gray(' Service: ') + desktopBridge.service);
|
|
37
43
|
if (desktopBridge.version)
|
package/dist/commands/chat.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export declare class ChatCommand {
|
|
|
68
68
|
private hasOperatorAccess;
|
|
69
69
|
private operatorAccessMessage;
|
|
70
70
|
private getDefaultChatModel;
|
|
71
|
+
private isHardExplicitAgentModelSelection;
|
|
71
72
|
private resolveInitialModel;
|
|
72
73
|
private isLegacyAgentFallbackAllowed;
|
|
73
74
|
private shouldRescueFailedAnalysis;
|
package/dist/commands/chat.js
CHANGED
|
@@ -22,10 +22,25 @@ import { inferAgentTaskType as sharedInferAgentTaskType, inferAgentTaskTypeWithC
|
|
|
22
22
|
import { resolveAgentRoute } from '../utils/agentRoute.js';
|
|
23
23
|
import { runTemplateInstantPath } from '../utils/templateInstantPath.js';
|
|
24
24
|
import { isV3StreamKeepaliveEvent } from '../utils/v3-stream-events.js';
|
|
25
|
+
// Stream health policy (corrected 2026-07-04 after TestFarm incident with
|
|
26
|
+
// Gideon Lenz / C:\vigthoria\Apps\monopoly): agentic tasks have no
|
|
27
|
+
// predictable duration, so there is intentionally NO overall wall-clock
|
|
28
|
+
// timeout -- DEFAULT_V3_AGENT_TIMEOUT_MS stays disabled (0) by default and
|
|
29
|
+
// must never kill an actively-streaming request just because it's "taking
|
|
30
|
+
// long". The only legitimate failure signal for SSE/chunked streams is
|
|
31
|
+
// silence: DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS aborts only when zero bytes
|
|
32
|
+
// arrive for a stretch, which is what "the model/agent has gone dark"
|
|
33
|
+
// actually looks like on the wire.
|
|
25
34
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
35
|
+
// Agentic tasks are open-ended by design — a build/refactor/analysis can
|
|
36
|
+
// legitimately take minutes or hours depending on scope. There must be NO
|
|
37
|
+
// overall wall-clock cap that kills an actively-streaming, healthy request.
|
|
38
|
+
// The only correct failure signal for SSE/streamed responses is silence:
|
|
39
|
+
// see DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS below, which aborts only when zero
|
|
40
|
+
// bytes arrive for a stretch — never based on total elapsed duration.
|
|
26
41
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
27
42
|
if (!rawValue) {
|
|
28
|
-
return 0;
|
|
43
|
+
return 0; // disabled — no overall duration cap
|
|
29
44
|
}
|
|
30
45
|
const parsed = Number.parseInt(rawValue, 10);
|
|
31
46
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
@@ -33,16 +48,35 @@ const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
|
33
48
|
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
34
49
|
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
|
|
35
50
|
if (!rawValue) {
|
|
36
|
-
return
|
|
51
|
+
return 60_000; // 60s with zero new stream data => abort/retry
|
|
37
52
|
}
|
|
38
53
|
const parsed = Number.parseInt(rawValue, 10);
|
|
39
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed :
|
|
54
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000;
|
|
40
55
|
})();
|
|
41
56
|
const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
|
|
42
57
|
const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS || '300000';
|
|
43
58
|
const parsed = Number.parseInt(rawValue, 10);
|
|
44
59
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
|
|
45
60
|
})();
|
|
61
|
+
/** Block-write agent JSON to VIGTHORIA_AGENT_OUTPUT (Windows KVM fsync path). */
|
|
62
|
+
function emitAgentJsonOutput(payload) {
|
|
63
|
+
const text = `${JSON.stringify(payload, null, 2)}\n`;
|
|
64
|
+
console.log(text.trimEnd());
|
|
65
|
+
const outPath = process.env.VIGTHORIA_AGENT_OUTPUT?.trim();
|
|
66
|
+
if (!outPath) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const dir = path.dirname(outPath);
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
+
const fd = fs.openSync(outPath, 'w');
|
|
72
|
+
try {
|
|
73
|
+
fs.writeSync(fd, text, undefined, 'utf8');
|
|
74
|
+
fs.fsyncSync(fd);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
fs.closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
46
80
|
export class ChatCommand {
|
|
47
81
|
config;
|
|
48
82
|
logger;
|
|
@@ -251,6 +285,19 @@ export class ChatCommand {
|
|
|
251
285
|
}
|
|
252
286
|
return preferredModel;
|
|
253
287
|
}
|
|
288
|
+
isHardExplicitAgentModelSelection(options) {
|
|
289
|
+
const requestedModel = String(options.model || '').trim().toLowerCase();
|
|
290
|
+
if (!requestedModel) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (/^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_FORCE_EXPLICIT_AGENT_MODEL || ''))) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
if (options.agent === true || options.operator === true) {
|
|
297
|
+
return !['agent', 'code', 'balanced', 'vigthoria-agent', 'vigthoria-code'].includes(requestedModel);
|
|
298
|
+
}
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
254
301
|
resolveInitialModel(options) {
|
|
255
302
|
const requestedModel = String(options.model || '').trim();
|
|
256
303
|
if (requestedModel) {
|
|
@@ -312,6 +359,16 @@ export class ChatCommand {
|
|
|
312
359
|
routeReason: 'explicit-model-selection',
|
|
313
360
|
};
|
|
314
361
|
}
|
|
362
|
+
if (this.agentMode && ['agent', 'code', 'balanced', 'vigthoria-agent', 'vigthoria-code'].includes(String(this.currentModel || '').toLowerCase())) {
|
|
363
|
+
return {
|
|
364
|
+
selectedModel: 'agent',
|
|
365
|
+
explicitModel: false,
|
|
366
|
+
heavyTask,
|
|
367
|
+
cloudEligible,
|
|
368
|
+
cloudSelected: false,
|
|
369
|
+
routeReason: 'dispatcher-eligible-agent-default',
|
|
370
|
+
};
|
|
371
|
+
}
|
|
315
372
|
if (this.lastAgentRoute?.path === 'template-instant') {
|
|
316
373
|
return {
|
|
317
374
|
selectedModel: 'agent',
|
|
@@ -816,7 +873,12 @@ export class ChatCommand {
|
|
|
816
873
|
}
|
|
817
874
|
this.v3IdleWatchInterval = setInterval(() => {
|
|
818
875
|
const idleMs = Date.now() - this.v3LastActivity;
|
|
819
|
-
if (idleMs < 15_000
|
|
876
|
+
if (idleMs < 15_000) {
|
|
877
|
+
// Activity resumed — re-arm the notice so a later stall is reported too.
|
|
878
|
+
this.v3IdleNoticeShown = false;
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (this.v3IdleNoticeShown) {
|
|
820
882
|
return;
|
|
821
883
|
}
|
|
822
884
|
this.v3IdleNoticeShown = true;
|
|
@@ -824,7 +886,11 @@ export class ChatCommand {
|
|
|
824
886
|
spinner.stop();
|
|
825
887
|
}
|
|
826
888
|
const seconds = Math.round(idleMs / 1000);
|
|
827
|
-
|
|
889
|
+
const idleTimeoutSec = Math.round(DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS / 1000);
|
|
890
|
+
process.stderr.write(chalk.yellow(` [Wait] Model still working (${seconds}s) — this can legitimately take a while for large tasks. ` +
|
|
891
|
+
(idleTimeoutSec > 0
|
|
892
|
+
? `Only complete silence (~${idleTimeoutSec}s with zero bytes received) will trigger an auto-recover/retry — active progress is never interrupted.\n`
|
|
893
|
+
: `\n`)));
|
|
828
894
|
spinner.start();
|
|
829
895
|
spinner.text = 'Waiting for model response...';
|
|
830
896
|
}, 5_000);
|
|
@@ -1406,7 +1472,7 @@ export class ChatCommand {
|
|
|
1406
1472
|
if (!this.config.isAuthenticated()) {
|
|
1407
1473
|
if (options.json) {
|
|
1408
1474
|
process.exitCode = 1;
|
|
1409
|
-
|
|
1475
|
+
emitAgentJsonOutput({ success: false, error: 'Not authenticated. Run: vigthoria login' });
|
|
1410
1476
|
}
|
|
1411
1477
|
else {
|
|
1412
1478
|
this.logger.error('Not authenticated. Run: vigthoria login');
|
|
@@ -1422,7 +1488,7 @@ export class ChatCommand {
|
|
|
1422
1488
|
this.jsonOutput = options.json === true;
|
|
1423
1489
|
this.autoApprove = options.autoApprove === true || this.jsonOutput;
|
|
1424
1490
|
this.personaOverride = options.grant === true ? 'wiener_grant' : null;
|
|
1425
|
-
this.modelExplicitlySelected =
|
|
1491
|
+
this.modelExplicitlySelected = this.isHardExplicitAgentModelSelection(options);
|
|
1426
1492
|
this.currentModel = this.resolveInitialModel(options);
|
|
1427
1493
|
this.applyNoAgentGovernance(String(options.model || this.currentModel || ''));
|
|
1428
1494
|
this.currentProjectPath = this.resolveProjectPath(options);
|
|
@@ -2788,6 +2854,28 @@ export class ChatCommand {
|
|
|
2788
2854
|
console.log(chalk.gray(' Run: vigthoria preview'));
|
|
2789
2855
|
this.printAgentRunSummary(this.lastAgentRunOutcome, evaluation, 1);
|
|
2790
2856
|
}
|
|
2857
|
+
else {
|
|
2858
|
+
const routingPolicy = this.resolveAgentExecutionPolicy(prompt);
|
|
2859
|
+
console.log(JSON.stringify({
|
|
2860
|
+
success: evaluation.executorSucceeded,
|
|
2861
|
+
mode: 'agent',
|
|
2862
|
+
model: routingPolicy.selectedModel,
|
|
2863
|
+
routingPolicy,
|
|
2864
|
+
taskId: null,
|
|
2865
|
+
contextId: null,
|
|
2866
|
+
partial: false,
|
|
2867
|
+
content: summary,
|
|
2868
|
+
statusHeadline: evaluation.statusHeadline,
|
|
2869
|
+
tasksSucceeded: 1,
|
|
2870
|
+
tasksTotal: 1,
|
|
2871
|
+
metadata: {
|
|
2872
|
+
source: 'template-instant',
|
|
2873
|
+
templateName: result.templateName,
|
|
2874
|
+
processingMs: result.processingMs,
|
|
2875
|
+
route: this.lastAgentRoute,
|
|
2876
|
+
},
|
|
2877
|
+
}, null, 2));
|
|
2878
|
+
}
|
|
2791
2879
|
return true;
|
|
2792
2880
|
}
|
|
2793
2881
|
async tryDirectSingleFileFlow(prompt) {
|
|
@@ -3328,7 +3416,8 @@ export class ChatCommand {
|
|
|
3328
3416
|
console.log(this.renderAgentAnswerForTerminal(finalAnswer));
|
|
3329
3417
|
}
|
|
3330
3418
|
}
|
|
3331
|
-
|
|
3419
|
+
const plannerFailedBeforeTasks = Boolean(liveOutcome.plannerError) && liveOutcome.tasksTotal === 0;
|
|
3420
|
+
if (!executorSucceeded && requiresWorkspaceChanges && changedFileCount === 0 && !liveOutcome.executorError && !plannerFailedBeforeTasks) {
|
|
3332
3421
|
const clientToolErrors = this.api.getClientToolErrors();
|
|
3333
3422
|
const transportErrors = this.api.getLastChatTransportErrors();
|
|
3334
3423
|
if (clientToolErrors.length > 0) {
|
|
@@ -3344,6 +3433,12 @@ export class ChatCommand {
|
|
|
3344
3433
|
liveOutcome.plannerError = transportErrors[0];
|
|
3345
3434
|
}
|
|
3346
3435
|
}
|
|
3436
|
+
if (plannerFailedBeforeTasks && !liveOutcome.executorError) {
|
|
3437
|
+
const transportErrors = this.api.getLastChatTransportErrors();
|
|
3438
|
+
if (transportErrors.length > 0) {
|
|
3439
|
+
liveOutcome.executorError = transportErrors[0];
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3347
3442
|
if (executorSucceeded) {
|
|
3348
3443
|
taskDisplay.complete(1);
|
|
3349
3444
|
}
|
|
@@ -3437,7 +3532,7 @@ export class ChatCommand {
|
|
|
3437
3532
|
if (!executorSucceeded) {
|
|
3438
3533
|
process.exitCode = 1;
|
|
3439
3534
|
}
|
|
3440
|
-
|
|
3535
|
+
emitAgentJsonOutput({
|
|
3441
3536
|
success: executorSucceeded,
|
|
3442
3537
|
mode: 'agent',
|
|
3443
3538
|
model: routingPolicy.selectedModel,
|
|
@@ -3459,7 +3554,7 @@ export class ChatCommand {
|
|
|
3459
3554
|
},
|
|
3460
3555
|
previewGate,
|
|
3461
3556
|
},
|
|
3462
|
-
}
|
|
3557
|
+
});
|
|
3463
3558
|
}
|
|
3464
3559
|
this.messages.push({ role: 'assistant', content: finalAnswer || runEvaluation.statusHeadline });
|
|
3465
3560
|
this.stopV3IdleWatch();
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,8 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
54
54
|
const __dirname = path.dirname(__filename);
|
|
55
55
|
import { APIClient, sanitizeUserFacingErrorText } from './utils/api.js';
|
|
56
56
|
import { getCliStateFile, isOfflineMode, isSafeNpmPackageSpec, isUpdateCheckSuppressed, readCachedLatestVersion, readGatewayPreflightCache, writeCachedLatestVersion, writeGatewayPreflightCache, } from './utils/cli-state.js';
|
|
57
|
+
import { applyLocalTestfarmDefaults, isLocalTestfarmMode } from './utils/localTestMode.js';
|
|
58
|
+
applyLocalTestfarmDefaults();
|
|
57
59
|
function isApiError(error) {
|
|
58
60
|
return Boolean(error &&
|
|
59
61
|
typeof error === 'object' &&
|
|
@@ -398,6 +400,9 @@ function isAuthProtectedCommand(command) {
|
|
|
398
400
|
return protectedCommands.has(command);
|
|
399
401
|
}
|
|
400
402
|
async function enforceGatewayAuthSession(config, logger, jsonOutputRequested) {
|
|
403
|
+
if (isLocalTestfarmMode()) {
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
401
406
|
if (!config.isAuthenticated()) {
|
|
402
407
|
return true;
|
|
403
408
|
}
|
package/dist/utils/agentRoute.js
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { inferAgentTaskType, isTrivialHtmlPageRequest, isAgentContinuePrompt, isAgentRetryPrompt, hasGameIntent, hasWebPageIntent, taskRequiresWorkspaceChanges, stripExecutionShaping, } from './requestIntent.js';
|
|
5
5
|
import { isFastInferenceRouterAvailable, routeWithBalanced4b, shouldUseBalancedRouter, } from './fastAgentRouter.js';
|
|
6
|
+
function forceTemplateScaffold() {
|
|
7
|
+
const v = String(process.env.VIGTHORIA_FORCE_TEMPLATE_SCAFFOLD || '').trim().toLowerCase();
|
|
8
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
9
|
+
}
|
|
10
|
+
function wantsInstantTemplatePath(prompt) {
|
|
11
|
+
return /instant[- ]template|template[- ]instant|use the instant-template path/i.test(prompt);
|
|
12
|
+
}
|
|
6
13
|
function normalizeTaskKind(value, fallback) {
|
|
7
14
|
const kind = String(value || fallback).trim().toLowerCase();
|
|
8
15
|
const allowed = new Set([
|
|
@@ -115,6 +122,16 @@ export async function resolveAgentRoute(api, prompt) {
|
|
|
115
122
|
};
|
|
116
123
|
}
|
|
117
124
|
if (hasGameIntent(stripped) && regexTask === 'game-build') {
|
|
125
|
+
if (forceTemplateScaffold() || wantsInstantTemplatePath(stripped)) {
|
|
126
|
+
return {
|
|
127
|
+
path: 'template-instant',
|
|
128
|
+
taskKind: 'game-build',
|
|
129
|
+
fastPath: 'template-instant',
|
|
130
|
+
confidence: 0.98,
|
|
131
|
+
reason: 'express game template scaffold',
|
|
132
|
+
source: 'regex',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
118
135
|
return {
|
|
119
136
|
path: 'v3-agent',
|
|
120
137
|
taskKind: 'game-build',
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -256,6 +256,7 @@ export declare class APIClient {
|
|
|
256
256
|
valid: boolean;
|
|
257
257
|
error?: string;
|
|
258
258
|
}>;
|
|
259
|
+
private isDirectV3AgentBaseUrl;
|
|
259
260
|
getV3AgentBaseUrls(preferLocal?: boolean): string[];
|
|
260
261
|
getV3AgentRunUrl(baseUrl: string): string;
|
|
261
262
|
getV3AgentContinueUrl(baseUrl: string): string;
|
package/dist/utils/api.js
CHANGED
|
@@ -12,10 +12,12 @@ import WebSocket from 'ws';
|
|
|
12
12
|
import { buildSemanticContext } from './context-ranker.js';
|
|
13
13
|
import { getChangedFiles } from './workspace-cache.js';
|
|
14
14
|
import { isSubstantiveAgentAnswer, isToolEvidenceStubAnswer } from './agentRunOutcome.js';
|
|
15
|
-
import { joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
|
|
15
|
+
import { isV3WorkspaceRootAliasPath, joinV3WorkspacePath, normalizeV3WorkspaceRelativePath } from './v3-workspace-path.js';
|
|
16
16
|
import { buildExecutionHints, isPlannerBuildTask, resolvePlannerAgentTimeoutMs } from './requestIntent.js';
|
|
17
17
|
import { isV3StreamKeepaliveEvent } from './v3-stream-events.js';
|
|
18
18
|
import { resolveV3ContextCharLimit, WORKSPACE_FILE_CHAR_CAP, } from './contextBudget.js';
|
|
19
|
+
import { isLocalTestfarmMode, fetchWithServiceTimeout } from './localTestMode.js';
|
|
20
|
+
import { buildClientManifest } from './clientManifest.js';
|
|
19
21
|
export const VIGTHORIA_HUB_CREDITS_URL = 'https://hub.vigthoria.io/credits';
|
|
20
22
|
export const VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE = 'Vigthoria Server is temporarily not available. Please try again later. If the issue persists, please contact support.';
|
|
21
23
|
export class CLIError extends Error {
|
|
@@ -258,10 +260,21 @@ export function propagateError(err) {
|
|
|
258
260
|
},
|
|
259
261
|
};
|
|
260
262
|
}
|
|
263
|
+
// Stream health policy — see matching comment in src/commands/chat.ts.
|
|
264
|
+
// No overall wall-clock timeout for agentic requests (duration is
|
|
265
|
+
// unpredictable and must never kill a healthy, actively-streaming request).
|
|
266
|
+
// Only silence (zero bytes for a stretch, see DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS)
|
|
267
|
+
// is a legitimate failure signal for SSE/chunked streams.
|
|
261
268
|
const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
269
|
+
// Agentic tasks are open-ended by design — a build/refactor/analysis can
|
|
270
|
+
// legitimately take minutes or hours depending on scope. There must be NO
|
|
271
|
+
// overall wall-clock cap that kills an actively-streaming, healthy request.
|
|
272
|
+
// The only correct failure signal for SSE/streamed responses is silence:
|
|
273
|
+
// see DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS below, which aborts only when zero
|
|
274
|
+
// bytes arrive for a stretch — never based on total elapsed duration.
|
|
262
275
|
const rawValue = process.env.VIGTHORIA_AGENT_TIMEOUT_MS || process.env.V3_AGENT_TIMEOUT_MS;
|
|
263
276
|
if (!rawValue) {
|
|
264
|
-
return 0;
|
|
277
|
+
return 0; // disabled — no overall duration cap
|
|
265
278
|
}
|
|
266
279
|
const parsed = Number.parseInt(rawValue, 10);
|
|
267
280
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
@@ -269,10 +282,10 @@ const DEFAULT_V3_AGENT_TIMEOUT_MS = (() => {
|
|
|
269
282
|
const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
|
|
270
283
|
const rawValue = process.env.VIGTHORIA_AGENT_IDLE_TIMEOUT_MS || process.env.V3_AGENT_IDLE_TIMEOUT_MS;
|
|
271
284
|
if (!rawValue) {
|
|
272
|
-
return
|
|
285
|
+
return 60_000; // 60s with zero new stream data => abort/retry
|
|
273
286
|
}
|
|
274
287
|
const parsed = Number.parseInt(rawValue, 10);
|
|
275
|
-
return Number.isFinite(parsed) && parsed >= 0 ? parsed :
|
|
288
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000;
|
|
276
289
|
})();
|
|
277
290
|
const DEFAULT_OPERATOR_TIMEOUT_MS = (() => {
|
|
278
291
|
const rawValue = process.env.VIGTHORIA_OPERATOR_TIMEOUT_MS || process.env.OPERATOR_TIMEOUT_MS;
|
|
@@ -559,6 +572,16 @@ export class APIClient {
|
|
|
559
572
|
const enforceTokenShape = options.enforceTokenShape !== false;
|
|
560
573
|
const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
|
|
561
574
|
const token = this.getAccessToken();
|
|
575
|
+
if (isLocalTestfarmMode()) {
|
|
576
|
+
if (!token) {
|
|
577
|
+
return { valid: false, error: 'Local test mode requires VIGTHORIA_LOCAL_TEST_TOKEN or VIGTHORIA_AUTH_TOKEN' };
|
|
578
|
+
}
|
|
579
|
+
const expected = String(process.env.VIGTHORIA_LOCAL_TEST_TOKEN || '').trim();
|
|
580
|
+
if (expected && token !== expected) {
|
|
581
|
+
return { valid: false, error: 'VIGTHORIA_LOCAL_TEST_TOKEN mismatch' };
|
|
582
|
+
}
|
|
583
|
+
return { valid: true };
|
|
584
|
+
}
|
|
562
585
|
if (!token) {
|
|
563
586
|
return { valid: false, error: 'No auth token configured. Run: vigthoria login' };
|
|
564
587
|
}
|
|
@@ -617,8 +640,18 @@ export class APIClient {
|
|
|
617
640
|
// Both unreachable — don't assume the stored token is bad when running offline.
|
|
618
641
|
return { valid: true };
|
|
619
642
|
}
|
|
643
|
+
isDirectV3AgentBaseUrl(baseUrl) {
|
|
644
|
+
try {
|
|
645
|
+
const parsed = new URL(String(baseUrl));
|
|
646
|
+
return parsed.port === '8030' || parsed.port.endsWith('8030');
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return /(?:127\.0\.0\.1|localhost|172\.19\.0\.1|host\.lan):(?:\d*8030|8030)/.test(String(baseUrl));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
620
652
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
621
653
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
654
|
+
const localTestMode = isLocalTestfarmMode();
|
|
622
655
|
const includeLoopbackV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1'
|
|
623
656
|
|| (preferLocal && isServerRuntime());
|
|
624
657
|
const localCandidates = [
|
|
@@ -626,30 +659,32 @@ export class APIClient {
|
|
|
626
659
|
process.env.V3_AGENT_URL,
|
|
627
660
|
...(includeLoopbackV3Agent ? ['http://127.0.0.1:8030'] : []),
|
|
628
661
|
].filter(Boolean).map((url) => String(url).replace(/\/$/, ''));
|
|
629
|
-
const remoteCandidates =
|
|
630
|
-
configuredApiUrl
|
|
631
|
-
'https://coder.vigthoria.io'
|
|
632
|
-
|
|
662
|
+
const remoteCandidates = localTestMode
|
|
663
|
+
? [configuredApiUrl]
|
|
664
|
+
: [configuredApiUrl, 'https://coder.vigthoria.io'];
|
|
665
|
+
const normalizedRemote = remoteCandidates
|
|
666
|
+
.filter(Boolean)
|
|
667
|
+
.map((url) => String(url).replace(/\/$/, ''));
|
|
633
668
|
const urls = preferLocal && localCandidates.length > 0
|
|
634
|
-
? [...localCandidates, ...
|
|
635
|
-
: [...
|
|
669
|
+
? [...localCandidates, ...normalizedRemote]
|
|
670
|
+
: [...normalizedRemote, ...localCandidates];
|
|
636
671
|
return [...new Set(urls)];
|
|
637
672
|
}
|
|
638
673
|
getV3AgentRunUrl(baseUrl) {
|
|
639
|
-
if (
|
|
674
|
+
if (this.isDirectV3AgentBaseUrl(baseUrl)) {
|
|
640
675
|
return `${baseUrl}/api/agent/run`;
|
|
641
676
|
}
|
|
642
677
|
return `${baseUrl}/api/v3-agent/run`;
|
|
643
678
|
}
|
|
644
679
|
getV3AgentContinueUrl(baseUrl) {
|
|
645
|
-
if (
|
|
680
|
+
if (this.isDirectV3AgentBaseUrl(baseUrl)) {
|
|
646
681
|
return `${baseUrl}/api/agent/continue`;
|
|
647
682
|
}
|
|
648
683
|
return `${baseUrl}/api/v3-agent/continue`;
|
|
649
684
|
}
|
|
650
685
|
getV3AgentBackgroundUrl(baseUrl, suffix = '') {
|
|
651
686
|
const cleanSuffix = suffix ? `/${suffix.replace(/^\/+/, '')}` : '';
|
|
652
|
-
if (
|
|
687
|
+
if (this.isDirectV3AgentBaseUrl(baseUrl)) {
|
|
653
688
|
return `${baseUrl}/api/agent/background${cleanSuffix}`;
|
|
654
689
|
}
|
|
655
690
|
return `${baseUrl}/api/v3-agent/background${cleanSuffix}`;
|
|
@@ -670,13 +705,20 @@ export class APIClient {
|
|
|
670
705
|
}
|
|
671
706
|
getMcpBaseUrls() {
|
|
672
707
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
708
|
+
const localTestMode = isLocalTestfarmMode();
|
|
709
|
+
const urls = localTestMode
|
|
710
|
+
? [
|
|
711
|
+
process.env.VIGTHORIA_MCP_URL,
|
|
712
|
+
process.env.MCP_SERVER_URL,
|
|
713
|
+
configuredApiUrl,
|
|
714
|
+
]
|
|
715
|
+
: [
|
|
716
|
+
process.env.VIGTHORIA_MCP_URL,
|
|
717
|
+
process.env.MCP_SERVER_URL,
|
|
718
|
+
'http://127.0.0.1:4008',
|
|
719
|
+
configuredApiUrl,
|
|
720
|
+
];
|
|
721
|
+
return [...new Set(urls.filter(Boolean).map((url) => String(url).replace(/\/$/, '')))];
|
|
680
722
|
}
|
|
681
723
|
getVigFlowBaseUrls() {
|
|
682
724
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
@@ -1165,44 +1207,52 @@ export class APIClient {
|
|
|
1165
1207
|
async runV3HealthCheck() {
|
|
1166
1208
|
const endpoints = this.getV3AgentBaseUrls(false);
|
|
1167
1209
|
const headers = await this.getV3AgentHeaders();
|
|
1210
|
+
const failures = [];
|
|
1168
1211
|
for (const baseUrl of endpoints) {
|
|
1169
1212
|
const runUrl = this.getV3AgentRunUrl(baseUrl);
|
|
1170
1213
|
// Use the lightweight GET /health endpoint — no LLM invocation, responds instantly.
|
|
1171
1214
|
// For local 8030 the path is /health directly; for remote it's /api/v3-agent/health via proxy.
|
|
1172
|
-
const healthUrl =
|
|
1215
|
+
const healthUrl = this.isDirectV3AgentBaseUrl(baseUrl)
|
|
1173
1216
|
? `${baseUrl}/health`
|
|
1174
1217
|
: `${baseUrl}/api/v3-agent/health`;
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1218
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
1219
|
+
try {
|
|
1220
|
+
const response = await fetch(healthUrl, {
|
|
1221
|
+
method: 'GET',
|
|
1222
|
+
headers,
|
|
1223
|
+
signal: AbortSignal.timeout(8000),
|
|
1224
|
+
});
|
|
1225
|
+
if (response.status === 401 || response.status === 403) {
|
|
1226
|
+
return {
|
|
1227
|
+
healthy: false,
|
|
1228
|
+
endpoint: runUrl,
|
|
1229
|
+
error: 'V3 service rejected authentication. Please re-login to refresh your token.',
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
// 200 OK or 405 Method Not Allowed both mean V3 is reachable.
|
|
1233
|
+
if (response.ok || response.status === 405) {
|
|
1234
|
+
return { healthy: true, endpoint: runUrl };
|
|
1235
|
+
}
|
|
1236
|
+
failures.push(`${healthUrl} attempt ${attempt}: HTTP ${response.status}`);
|
|
1237
|
+
}
|
|
1238
|
+
catch (error) {
|
|
1239
|
+
const reason = sanitizeUserFacingErrorText(error?.message || String(error)).slice(0, 160);
|
|
1240
|
+
failures.push(`${healthUrl} attempt ${attempt}: ${reason || 'network error'}`);
|
|
1187
1241
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
return { healthy: true, endpoint: runUrl };
|
|
1242
|
+
if (attempt < 3) {
|
|
1243
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
|
|
1191
1244
|
}
|
|
1192
1245
|
}
|
|
1193
|
-
catch {
|
|
1194
|
-
continue;
|
|
1195
|
-
}
|
|
1196
1246
|
}
|
|
1197
1247
|
return {
|
|
1198
1248
|
healthy: false,
|
|
1199
1249
|
endpoint: endpoints[0] ? this.getV3AgentRunUrl(endpoints[0]) : 'unknown',
|
|
1200
|
-
error:
|
|
1250
|
+
error: `V3 service is not reachable during startup preflight. Checked ${endpoints.length} endpoint(s). ${failures.slice(-3).join(' | ')}`,
|
|
1201
1251
|
};
|
|
1202
1252
|
}
|
|
1203
1253
|
async runV3AgentAuthPreflight(baseUrl, body, executionContext) {
|
|
1204
1254
|
const endpoint = this.getV3AgentRunUrl(baseUrl);
|
|
1205
|
-
const healthEndpoint =
|
|
1255
|
+
const healthEndpoint = this.isDirectV3AgentBaseUrl(baseUrl)
|
|
1206
1256
|
? `${baseUrl}/health`
|
|
1207
1257
|
: `${baseUrl}/api/v3-agent/health`;
|
|
1208
1258
|
const headers = await this.getV3AgentHeaders();
|
|
@@ -1579,6 +1629,9 @@ export class APIClient {
|
|
|
1579
1629
|
codebaseContext: resolvedContext.codebaseContext || '',
|
|
1580
1630
|
accountBrainContext: resolvedContext.accountBrainContext || '',
|
|
1581
1631
|
projectMemory: resolvedContext.projectMemory || '',
|
|
1632
|
+
clientManifest: buildClientManifest(resolvedContext.clientManifest && typeof resolvedContext.clientManifest === 'object'
|
|
1633
|
+
? resolvedContext.clientManifest
|
|
1634
|
+
: {}),
|
|
1582
1635
|
};
|
|
1583
1636
|
return this.compactV3Context(payload);
|
|
1584
1637
|
}
|
|
@@ -2853,7 +2906,10 @@ menu {
|
|
|
2853
2906
|
}
|
|
2854
2907
|
}
|
|
2855
2908
|
if (!relativePath && raw !== '.' && raw !== '') {
|
|
2856
|
-
|
|
2909
|
+
const treatAsWorkspaceRoot = roots.some((sourceRoot) => isV3WorkspaceRootAliasPath(raw, sourceRoot));
|
|
2910
|
+
if (!treatAsWorkspaceRoot) {
|
|
2911
|
+
return null;
|
|
2912
|
+
}
|
|
2857
2913
|
}
|
|
2858
2914
|
const absolutePath = relativePath ? joinV3WorkspacePath(rootPath, relativePath) : path.resolve(rootPath);
|
|
2859
2915
|
const resolvedRoot = path.resolve(rootPath);
|
|
@@ -3776,6 +3832,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3776
3832
|
else {
|
|
3777
3833
|
chunk = await new Promise((resolve, reject) => {
|
|
3778
3834
|
let idleTimer = null;
|
|
3835
|
+
// Idle/silence detection (NOT a task-duration timeout): this timer
|
|
3836
|
+
// only ever fires after idleTimeoutMs with ZERO new bytes on the
|
|
3837
|
+
// wire. What we do next depends on WHY it's silent:
|
|
3838
|
+
// - A client-side tool is genuinely still running (e.g. a local
|
|
3839
|
+
// build/install the CLI itself launched) -- its own duration is
|
|
3840
|
+
// unpredictable by design, so we reschedule indefinitely; this
|
|
3841
|
+
// is known, bounded-elsewhere work, not "the agent went dark".
|
|
3842
|
+
// - Otherwise, silence really does mean nothing is coming from
|
|
3843
|
+
// the agent, so after a generous grace window we recover
|
|
3844
|
+
// (if the workspace already has output) or fail cleanly.
|
|
3845
|
+
let idleRescheduleCount = 0;
|
|
3846
|
+
const maxIdleReschedules = 20; // ~20 min of genuine zero-byte silence
|
|
3779
3847
|
const clearIdleTimer = () => {
|
|
3780
3848
|
if (idleTimer) {
|
|
3781
3849
|
clearTimeout(idleTimer);
|
|
@@ -3786,6 +3854,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3786
3854
|
clearIdleTimer();
|
|
3787
3855
|
idleTimer = setTimeout(() => {
|
|
3788
3856
|
if (this.pendingV3ClientToolTasks.size > 0) {
|
|
3857
|
+
// Known local work in progress -- never cap this, its own
|
|
3858
|
+
// completion will produce the next chunk/event.
|
|
3789
3859
|
scheduleIdleTimer();
|
|
3790
3860
|
return;
|
|
3791
3861
|
}
|
|
@@ -3803,6 +3873,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
3803
3873
|
reject(stalledError);
|
|
3804
3874
|
return;
|
|
3805
3875
|
}
|
|
3876
|
+
if (idleRescheduleCount >= maxIdleReschedules) {
|
|
3877
|
+
const timeoutError = new Error(`V3 agent stream timed out after ${Math.round((idleTimeoutMs * (maxIdleReschedules + 1)) / 1000)}s with no data ` +
|
|
3878
|
+
'(model likely stopped after a thinking block without emitting a tool call or final answer).');
|
|
3879
|
+
timeoutError.name = 'AbortError';
|
|
3880
|
+
timeoutError.partialData = {
|
|
3881
|
+
task_id: events.find((event) => event && event.task_id)?.task_id || null,
|
|
3882
|
+
context_id: contextId,
|
|
3883
|
+
result: final,
|
|
3884
|
+
events,
|
|
3885
|
+
files: streamedFiles,
|
|
3886
|
+
};
|
|
3887
|
+
clearIdleTimer();
|
|
3888
|
+
reject(timeoutError);
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
idleRescheduleCount += 1;
|
|
3806
3892
|
scheduleIdleTimer();
|
|
3807
3893
|
}, idleTimeoutMs);
|
|
3808
3894
|
};
|
|
@@ -4382,6 +4468,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4382
4468
|
// ground its analysis in real files rather than returning files_analyzed: 0.
|
|
4383
4469
|
const workspacePath = executionContext.workspacePath || executionContext.projectPath || executionContext.targetPath || process.cwd();
|
|
4384
4470
|
const workspaceSummary = this.buildLocalWorkspaceSummary(workspacePath);
|
|
4471
|
+
const requestedModel = String(executionContext.model || 'code');
|
|
4472
|
+
const resolvedModel = this.resolveModelId(requestedModel);
|
|
4385
4473
|
for (const baseUrl of this.getOperatorBaseUrls()) {
|
|
4386
4474
|
const controller = new AbortController();
|
|
4387
4475
|
const timeoutId = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
@@ -4401,6 +4489,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4401
4489
|
prompt: message,
|
|
4402
4490
|
task: message,
|
|
4403
4491
|
raw_prompt: executionContext.rawPrompt || null,
|
|
4492
|
+
model: requestedModel,
|
|
4493
|
+
preferred_model: resolvedModel,
|
|
4404
4494
|
context_id: executionContext.contextId,
|
|
4405
4495
|
trace_id: executionContext.traceId,
|
|
4406
4496
|
mcp_context_id: executionContext.mcpContextId || null,
|
|
@@ -4408,7 +4498,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4408
4498
|
workspace: { path: workspacePath },
|
|
4409
4499
|
workspace_path: workspacePath,
|
|
4410
4500
|
workspace_summary: workspaceSummary,
|
|
4411
|
-
model:
|
|
4501
|
+
model: resolvedModel,
|
|
4412
4502
|
history: executionContext.history || [],
|
|
4413
4503
|
executionSurface: executionContext.executionSurface || 'cli',
|
|
4414
4504
|
clientSurface: executionContext.clientSurface || 'cli',
|
|
@@ -5915,7 +6005,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
5915
6005
|
return;
|
|
5916
6006
|
}
|
|
5917
6007
|
const headers = await this.getV3AgentHeaders();
|
|
5918
|
-
const endpoint =
|
|
6008
|
+
const endpoint = this.isDirectV3AgentBaseUrl(baseUrl)
|
|
5919
6009
|
? `${baseUrl}/api/agent/client-tool-result`
|
|
5920
6010
|
: `${baseUrl}/api/v3-agent/client-tool-result`;
|
|
5921
6011
|
const body = JSON.stringify({
|
|
@@ -6132,7 +6222,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6132
6222
|
const endpoint = process.env.VIGTHORIA_HYPERLOOP_URL || `${configuredApiUrl}/api/hyperloop/health`;
|
|
6133
6223
|
try {
|
|
6134
6224
|
const token = this.getAccessToken();
|
|
6135
|
-
const response = await
|
|
6225
|
+
const response = await fetchWithServiceTimeout(endpoint, {
|
|
6136
6226
|
method: 'GET',
|
|
6137
6227
|
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
|
6138
6228
|
});
|
|
@@ -6164,7 +6254,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6164
6254
|
const token = this.getAccessToken();
|
|
6165
6255
|
const projectPath = this.resolveAgentTargetPath(context);
|
|
6166
6256
|
try {
|
|
6167
|
-
const modulesResponse = await
|
|
6257
|
+
const modulesResponse = await fetchWithServiceTimeout(modulesEndpoint, {
|
|
6168
6258
|
method: 'GET',
|
|
6169
6259
|
headers: {
|
|
6170
6260
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
@@ -6178,7 +6268,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6178
6268
|
const compactor = modules.find((entry) => entry && entry.name === 'repo_context_compactor');
|
|
6179
6269
|
let compactContextLength = 0;
|
|
6180
6270
|
try {
|
|
6181
|
-
const probeResponse = await
|
|
6271
|
+
const probeResponse = await fetchWithServiceTimeout(endpoint, {
|
|
6182
6272
|
method: 'POST',
|
|
6183
6273
|
headers: {
|
|
6184
6274
|
'Content-Type': 'application/json',
|
|
@@ -6260,6 +6350,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
6260
6350
|
const host = process.env.VIGTHORIA_DEVTOOLS_BRIDGE_HOST || '127.0.0.1';
|
|
6261
6351
|
const port = Number.parseInt(process.env.VIGTHORIA_DEVTOOLS_BRIDGE_PORT || '4016', 10);
|
|
6262
6352
|
const endpoint = `ws://${host}:${port}/ws`;
|
|
6353
|
+
const healthEndpoint = `http://${host}:${port}/health`;
|
|
6354
|
+
try {
|
|
6355
|
+
const controller = new AbortController();
|
|
6356
|
+
const timer = setTimeout(() => controller.abort(), 1500);
|
|
6357
|
+
const response = await fetch(healthEndpoint, {
|
|
6358
|
+
signal: controller.signal,
|
|
6359
|
+
headers: { Accept: 'application/json' },
|
|
6360
|
+
});
|
|
6361
|
+
clearTimeout(timer);
|
|
6362
|
+
if (response.ok) {
|
|
6363
|
+
return {
|
|
6364
|
+
name: 'DevTools Bridge',
|
|
6365
|
+
endpoint,
|
|
6366
|
+
ok: true,
|
|
6367
|
+
details: { host, port, healthEndpoint },
|
|
6368
|
+
};
|
|
6369
|
+
}
|
|
6370
|
+
return {
|
|
6371
|
+
name: 'DevTools Bridge',
|
|
6372
|
+
endpoint,
|
|
6373
|
+
ok: false,
|
|
6374
|
+
error: `HTTP ${response.status}`,
|
|
6375
|
+
};
|
|
6376
|
+
}
|
|
6377
|
+
catch {
|
|
6378
|
+
// Fall back to a raw TCP probe for older bridge builds without /health.
|
|
6379
|
+
}
|
|
6263
6380
|
return new Promise((resolve) => {
|
|
6264
6381
|
const socket = net.connect({ host, port, timeout: 1500 }, () => {
|
|
6265
6382
|
socket.end();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type ClientManifestPayload = {
|
|
2
|
+
product_id: string;
|
|
3
|
+
version: string;
|
|
4
|
+
os: string;
|
|
5
|
+
execution_mode: string;
|
|
6
|
+
features_allowed: string[];
|
|
7
|
+
};
|
|
8
|
+
/** Detect iSH/Blink-style iOS terminal sandboxes (override, UA, or Linux-on-iOS release string). */
|
|
9
|
+
export declare function isIOSEnvironment(): boolean;
|
|
10
|
+
/** Map Node process.platform/arch to dispatcher gate OS slug. */
|
|
11
|
+
export declare function resolveClientOsSlug(): string;
|
|
12
|
+
export declare function buildClientManifest(overrides?: Partial<ClientManifestPayload>): ClientManifestPayload;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const DEFAULT_FEATURES = ['template-instant', 'hyperloop-stream'];
|
|
8
|
+
function readPackageVersion() {
|
|
9
|
+
const candidates = [
|
|
10
|
+
path.join(__dirname, '..', '..', 'package.json'),
|
|
11
|
+
path.join(__dirname, '..', 'package.json'),
|
|
12
|
+
];
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
try {
|
|
15
|
+
if (!fs.existsSync(candidate))
|
|
16
|
+
continue;
|
|
17
|
+
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
18
|
+
if (parsed.version)
|
|
19
|
+
return String(parsed.version);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// try next candidate
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return '0.0.0';
|
|
26
|
+
}
|
|
27
|
+
/** Detect iSH/Blink-style iOS terminal sandboxes (override, UA, or Linux-on-iOS release string). */
|
|
28
|
+
export function isIOSEnvironment() {
|
|
29
|
+
const override = (process.env.VIGTHORIA_OS_OVERRIDE || '').trim().toLowerCase();
|
|
30
|
+
if (override === 'ios')
|
|
31
|
+
return true;
|
|
32
|
+
const userAgent = (process.env.VIGTHORIA_CLIENT_USER_AGENT || '').trim();
|
|
33
|
+
if (/ipad|iphone|ios/i.test(userAgent))
|
|
34
|
+
return true;
|
|
35
|
+
if (process.platform === 'linux') {
|
|
36
|
+
const release = os.release().toLowerCase();
|
|
37
|
+
if (/ipad|iphone|ios/.test(release))
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/** Map Node process.platform/arch to dispatcher gate OS slug. */
|
|
43
|
+
export function resolveClientOsSlug() {
|
|
44
|
+
if (isIOSEnvironment()) {
|
|
45
|
+
return process.arch === 'arm64' ? 'ios-arm64' : 'ios-x64';
|
|
46
|
+
}
|
|
47
|
+
if (process.platform === 'win32')
|
|
48
|
+
return 'windows-x86_64';
|
|
49
|
+
if (process.platform === 'darwin') {
|
|
50
|
+
return process.arch === 'arm64' ? 'macos-arm64' : 'macos-x86_64';
|
|
51
|
+
}
|
|
52
|
+
if (process.platform === 'linux') {
|
|
53
|
+
return process.arch === 'arm64' ? 'linux-arm64' : 'linux-x86_64';
|
|
54
|
+
}
|
|
55
|
+
return `${process.platform}-${process.arch}`;
|
|
56
|
+
}
|
|
57
|
+
export function buildClientManifest(overrides = {}) {
|
|
58
|
+
const ios = isIOSEnvironment() && !overrides.os;
|
|
59
|
+
return {
|
|
60
|
+
product_id: overrides.product_id || 'vigthoria-cli',
|
|
61
|
+
version: overrides.version || readPackageVersion(),
|
|
62
|
+
os: overrides.os || resolveClientOsSlug(),
|
|
63
|
+
execution_mode: overrides.execution_mode || (ios ? 'mobile-terminal-sandbox' : 'local-user-machine'),
|
|
64
|
+
features_allowed: overrides.features_allowed?.length
|
|
65
|
+
? [...overrides.features_allowed]
|
|
66
|
+
: [...DEFAULT_FEATURES],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testfarm / Admin Factory local E2E — bypass public coder.vigthoria.io gateway
|
|
3
|
+
* when VIGTHORIA_LOCAL_TEST_MODE=1 or VIGTHORIA_LOCAL_TEST_TOKEN is set.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isLocalTestfarmMode(): boolean;
|
|
6
|
+
/** Apply once at CLI startup — idempotent via ??= */
|
|
7
|
+
export declare function applyLocalTestfarmDefaults(): void;
|
|
8
|
+
export declare function resolveHyperloopTimeoutMs(): number;
|
|
9
|
+
export declare function fetchWithServiceTimeout(url: string, init?: RequestInit): Promise<Response>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Testfarm / Admin Factory local E2E — bypass public coder.vigthoria.io gateway
|
|
3
|
+
* when VIGTHORIA_LOCAL_TEST_MODE=1 or VIGTHORIA_LOCAL_TEST_TOKEN is set.
|
|
4
|
+
*/
|
|
5
|
+
export function isLocalTestfarmMode() {
|
|
6
|
+
const flag = String(process.env.VIGTHORIA_LOCAL_TEST_MODE || '').trim().toLowerCase();
|
|
7
|
+
if (flag === '1' || flag === 'true' || flag === 'yes') {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return Boolean(String(process.env.VIGTHORIA_LOCAL_TEST_TOKEN || '').trim());
|
|
11
|
+
}
|
|
12
|
+
/** Apply once at CLI startup — idempotent via ??= */
|
|
13
|
+
export function applyLocalTestfarmDefaults() {
|
|
14
|
+
if (!isLocalTestfarmMode()) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
process.env.VIGTHORIA_ALLOW_LOCAL_SERVICES ??= '1';
|
|
18
|
+
process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT ??= '1';
|
|
19
|
+
process.env.VIGTHORIA_SELF_HOSTED_MODELS_API_URL ??= 'http://127.0.0.1:4009';
|
|
20
|
+
process.env.VIGTHORIA_HYPERLOOP_URL ??= 'http://127.0.0.1:8020';
|
|
21
|
+
process.env.VIGTHORIA_HYPERLOOP_EXECUTE_URL ??= 'http://127.0.0.1:8020/execute';
|
|
22
|
+
process.env.VIGTHORIA_HYPERLOOP_MODULES_URL ??= 'http://127.0.0.1:8020/modules';
|
|
23
|
+
process.env.VIGTHORIA_V3_AGENT_URL ??= 'http://127.0.0.1:8030';
|
|
24
|
+
process.env.VIGTHORIA_TEMPLATE_SERVICE_URL ??= 'http://127.0.0.1:4011';
|
|
25
|
+
process.env.VIGTHORIA_HYPERLOOP_TIMEOUT_MS ??= '180000';
|
|
26
|
+
const testToken = String(process.env.VIGTHORIA_LOCAL_TEST_TOKEN || '').trim();
|
|
27
|
+
if (testToken && !process.env.VIGTHORIA_AUTH_TOKEN && !process.env.VIGTHORIA_TOKEN) {
|
|
28
|
+
process.env.VIGTHORIA_AUTH_TOKEN = testToken;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function resolveHyperloopTimeoutMs() {
|
|
32
|
+
const raw = process.env.VIGTHORIA_HYPERLOOP_TIMEOUT_MS
|
|
33
|
+
|| process.env.VIGTHORIA_HTTP_TIMEOUT_MS
|
|
34
|
+
|| '120000';
|
|
35
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
36
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 120000;
|
|
37
|
+
}
|
|
38
|
+
export async function fetchWithServiceTimeout(url, init = {}) {
|
|
39
|
+
const ms = resolveHyperloopTimeoutMs();
|
|
40
|
+
const signal = init.signal ?? AbortSignal.timeout(ms);
|
|
41
|
+
return fetch(url, { ...init, signal });
|
|
42
|
+
}
|
package/dist/utils/session.d.ts
CHANGED
package/dist/utils/session.js
CHANGED
|
@@ -251,15 +251,48 @@ export class SessionManager {
|
|
|
251
251
|
return null;
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
|
+
normalizeProjectKey(projectPath) {
|
|
255
|
+
const raw = String(projectPath || '').trim();
|
|
256
|
+
if (!raw) {
|
|
257
|
+
return '';
|
|
258
|
+
}
|
|
259
|
+
// Normalize separators and collapse trailing slashes.
|
|
260
|
+
let normalized = raw.replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
261
|
+
// Canonicalize obvious Windows drive prefix casing (C:/... === c:/...).
|
|
262
|
+
const driveMatch = normalized.match(/^([a-zA-Z]):\//);
|
|
263
|
+
if (driveMatch) {
|
|
264
|
+
normalized = `${driveMatch[1].toUpperCase()}:${normalized.slice(2)}`;
|
|
265
|
+
}
|
|
266
|
+
// Windows paths are case-insensitive in practice for this CLI matching purpose.
|
|
267
|
+
if (/^[a-zA-Z]:\//.test(normalized) || normalized.startsWith('//')) {
|
|
268
|
+
return normalized.toLowerCase();
|
|
269
|
+
}
|
|
270
|
+
return normalized;
|
|
271
|
+
}
|
|
254
272
|
/**
|
|
255
273
|
* Get the most recent session for a project
|
|
256
274
|
*/
|
|
257
275
|
getLatest(project) {
|
|
258
276
|
const sessions = this.list();
|
|
259
|
-
const
|
|
260
|
-
|
|
277
|
+
const projectKey = this.normalizeProjectKey(project);
|
|
278
|
+
const projectBase = path.basename(projectKey || '').toLowerCase();
|
|
279
|
+
const exactMatches = sessions
|
|
280
|
+
.filter((s) => this.normalizeProjectKey(s.project) === projectKey)
|
|
261
281
|
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
262
|
-
|
|
282
|
+
if (exactMatches.length > 0) {
|
|
283
|
+
return this.load(exactMatches[0].id);
|
|
284
|
+
}
|
|
285
|
+
// Fallback: if old sessions stored inconsistent slash/case forms, allow
|
|
286
|
+
// latest same-folder match (e.g., C:\Proj and C:/proj) to preserve /retry UX.
|
|
287
|
+
if (projectBase) {
|
|
288
|
+
const softMatches = sessions
|
|
289
|
+
.filter((s) => path.basename(this.normalizeProjectKey(s.project) || '').toLowerCase() === projectBase)
|
|
290
|
+
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
291
|
+
if (softMatches.length > 0) {
|
|
292
|
+
return this.load(softMatches[0].id);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
263
296
|
}
|
|
264
297
|
/**
|
|
265
298
|
* List all sessions (metadata only) — corrupt or unreadable files are
|
|
@@ -170,6 +170,24 @@ export async function runTemplateInstantPath(api, vision, workspacePath, options
|
|
|
170
170
|
const target = path.join(workspacePath, entryPath);
|
|
171
171
|
const cleanVision = stripExecutionShaping(vision);
|
|
172
172
|
const preferMinimal = options.forceMinimalFallback === true;
|
|
173
|
+
const keepScaffold = String(process.env.VIGTHORIA_EXPRESS_KEEP_SCAFFOLD || process.env.CLI_EXPRESS_KEEP_SCAFFOLD || '').trim().toLowerCase();
|
|
174
|
+
if (['1', 'true', 'yes'].includes(keepScaffold)) {
|
|
175
|
+
try {
|
|
176
|
+
const stat = fs.statSync(target);
|
|
177
|
+
if (stat.isFile() && stat.size >= 8000) {
|
|
178
|
+
return {
|
|
179
|
+
ok: true,
|
|
180
|
+
entryPath,
|
|
181
|
+
templateName: 'express-preserved-scaffold',
|
|
182
|
+
confidence: 1,
|
|
183
|
+
processingMs: 0,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// fall through to normal template match
|
|
189
|
+
}
|
|
190
|
+
}
|
|
173
191
|
let html;
|
|
174
192
|
let usedFallback = false;
|
|
175
193
|
let match = {};
|
|
@@ -5,3 +5,5 @@
|
|
|
5
5
|
export declare function normalizeV3WorkspaceRelativePath(rawPath: string, rootPath?: string): string;
|
|
6
6
|
/** Join workspace root + relative path without path.resolve URI corruption on Windows. */
|
|
7
7
|
export declare function joinV3WorkspacePath(rootPath: string, relativePath: string): string;
|
|
8
|
+
/** True when rawPath points to workspace root via aliases or scrubbed placeholders. */
|
|
9
|
+
export declare function isV3WorkspaceRootAliasPath(rawPath: string, rootPath?: string): boolean;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
const WORKSPACE_PLACEHOLDER_SEGMENT = /\[(?:internal|Vigthoria service|Vigthoria Agent|Vigthoria storage|temp|home|workspace)\]/ig;
|
|
3
|
+
function stripInlineWorkspacePlaceholders(value) {
|
|
4
|
+
// Only strip placeholders when they were appended to a segment (e.g. pitfall[Vigthoria service]).
|
|
5
|
+
// Keep canonical scrubbed segments like /[internal]/ intact for dedicated mapping logic.
|
|
6
|
+
return String(value || '')
|
|
7
|
+
.replace(/(^|[^/])\[(?:internal|Vigthoria service|Vigthoria Agent|Vigthoria storage|temp|home|workspace)\](?=(?:\/|$))/ig, '$1')
|
|
8
|
+
.replace(/\/+$/g, '');
|
|
9
|
+
}
|
|
2
10
|
/**
|
|
3
11
|
* Normalize V3 SSE / tool paths to a safe workspace-relative POSIX path.
|
|
4
12
|
* Decodes vigthoria:// boundary URIs from server scrubbing before local writes.
|
|
@@ -8,6 +16,7 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
8
16
|
if (!input) {
|
|
9
17
|
return '';
|
|
10
18
|
}
|
|
19
|
+
input = stripInlineWorkspacePlaceholders(input);
|
|
11
20
|
const safeRelative = (candidate) => {
|
|
12
21
|
const stripped = String(candidate || '').replace(/^\/+/, '');
|
|
13
22
|
if (!stripped) {
|
|
@@ -43,7 +52,7 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
43
52
|
return null;
|
|
44
53
|
};
|
|
45
54
|
const stripScrubbedPlaceholder = (value) => {
|
|
46
|
-
let candidate = String(value || '').replace(/^\/+/, '');
|
|
55
|
+
let candidate = stripInlineWorkspacePlaceholders(String(value || '')).replace(/^\/+/, '');
|
|
47
56
|
// Hybrid paths models echo back after SSE scrubbing, e.g. C:/var/www/[internal]/game.js
|
|
48
57
|
const hybridWin = candidate.match(/^[a-zA-Z]:\/(?:var\/)?www\/\[internal\](?:\/(.*))?$/i);
|
|
49
58
|
if (hybridWin) {
|
|
@@ -70,7 +79,7 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
70
79
|
return safeRelative(match[groupIndex] || '');
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
|
-
const bracketSplit = candidate.split(
|
|
82
|
+
const bracketSplit = candidate.split(WORKSPACE_PLACEHOLDER_SEGMENT);
|
|
74
83
|
if (bracketSplit.length > 1) {
|
|
75
84
|
const tail = bracketSplit[bracketSplit.length - 1].replace(/^\/+/, '');
|
|
76
85
|
return safeRelative(tail);
|
|
@@ -152,3 +161,34 @@ export function joinV3WorkspacePath(rootPath, relativePath) {
|
|
|
152
161
|
const segments = relativePath.split('/').filter((segment) => segment && segment !== '.');
|
|
153
162
|
return path.join(rootPath, ...segments);
|
|
154
163
|
}
|
|
164
|
+
/** True when rawPath points to workspace root via aliases or scrubbed placeholders. */
|
|
165
|
+
export function isV3WorkspaceRootAliasPath(rawPath, rootPath) {
|
|
166
|
+
const original = String(rawPath || '').trim();
|
|
167
|
+
if (!original || original === '.') {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
const sanitized = stripInlineWorkspacePlaceholders(original).replace(/\\/g, '/').trim();
|
|
171
|
+
if (!sanitized || sanitized === '.' || sanitized === '/') {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
const candidate = sanitized.replace(/^\/+/, '');
|
|
175
|
+
if (!candidate) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
const lowered = candidate.toLowerCase();
|
|
179
|
+
if (lowered === 'workspace'
|
|
180
|
+
|| lowered === '[workspace]'
|
|
181
|
+
|| lowered === '[internal]'
|
|
182
|
+
|| lowered === 'www/[internal]'
|
|
183
|
+
|| lowered === 'var/www/[internal]'
|
|
184
|
+
|| lowered === '[vigthoria service]'
|
|
185
|
+
|| lowered === 'var/www/[vigthoria service]') {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
const normalizedRoot = String(rootPath || '').trim().replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
189
|
+
if (!normalizedRoot) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const normalizedInput = sanitized.replace(/\/+$/g, '');
|
|
193
|
+
return normalizedInput.toLowerCase() === normalizedRoot.toLowerCase();
|
|
194
|
+
}
|
package/install.ps1
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
$ErrorActionPreference = "Stop"
|
|
6
6
|
|
|
7
7
|
# Configuration
|
|
8
|
-
$CLI_VERSION = "1.11.
|
|
8
|
+
$CLI_VERSION = "1.11.42"
|
|
9
9
|
$INSTALL_DIR = "$env:USERPROFILE\.vigthoria"
|
|
10
10
|
$NPM_PACKAGE = "vigthoria-cli"
|
|
11
11
|
$GIT_PACKAGE_URL = "git+https://market.vigthoria.io/vigthoria/vigthoria-cli.git"
|
package/install.sh
CHANGED
|
@@ -26,7 +26,7 @@ else
|
|
|
26
26
|
fi
|
|
27
27
|
|
|
28
28
|
# Configuration
|
|
29
|
-
CLI_VERSION="1.11.
|
|
29
|
+
CLI_VERSION="1.11.42"
|
|
30
30
|
INSTALL_DIR="$HOME/.vigthoria"
|
|
31
31
|
REPO_URL="https://market.vigthoria.io/vigthoria/vigthoria-cli"
|
|
32
32
|
GIT_PACKAGE_URL="git+https://market.vigthoria.io/vigthoria/vigthoria-cli.git"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigthoria-cli",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.44",
|
|
4
4
|
"description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"test:legion:billing:e2e": "npm run build && node scripts/test-legion-godmode-billing-e2e.js",
|
|
71
71
|
"test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js",
|
|
72
72
|
"test:v3-workspace-path": "npm run build && node scripts/test-v3-workspace-path.js",
|
|
73
|
+
"test:session:project-match": "npm run build && node scripts/test-session-project-match.mjs",
|
|
73
74
|
"test:v3-stream-mutation": "npm run build && node scripts/test-v3-stream-mutation.js",
|
|
74
75
|
"test:context:budget": "npm run build && node scripts/test-context-budget.js",
|
|
75
76
|
"test:pitfall:context": "npm run build && node scripts/test-pitfall-context-smoke.js"
|
|
@@ -122,7 +122,10 @@ PY
|
|
|
122
122
|
echo "[7.5 policy] secured hyperloop endpoint"
|
|
123
123
|
code=$(curl -s -o /dev/null -w "%{http_code}" https://coder.vigthoria.io/api/hyperloop/health)
|
|
124
124
|
echo "unauth_hyperloop_code=$code"
|
|
125
|
-
|
|
125
|
+
# 401 = correct JSON auth error for API callers. 302 (redirect to /login) is
|
|
126
|
+
# also acceptable defense-in-depth: it still requires auth, just via the
|
|
127
|
+
# browser-session redirect path some legacy middleware uses.
|
|
128
|
+
[[ "$code" == "401" || "$code" == "302" ]]
|
|
126
129
|
|
|
127
130
|
echo "[8.2] runtime symlink guard signals"
|
|
128
131
|
rg -n "realpathSync|results.length >= maxFiles|pattern = kw.length <= 4" dist/utils/context-ranker.js >/tmp/vig-ranker-signals.txt
|