vigthoria-cli 1.13.13 → 1.13.18
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/background.d.ts +3 -0
- package/dist/commands/background.js +58 -1
- package/dist/commands/chat.js +36 -1
- package/dist/commands/legion.js +48 -13
- package/dist/index.js +12 -2
- package/dist/utils/api.d.ts +9 -0
- package/dist/utils/api.js +207 -13
- package/dist/utils/clientManifest.d.ts +9 -0
- package/dist/utils/clientManifest.js +11 -1
- package/dist/utils/command-menu.js +6 -0
- package/dist/utils/contextBudget.d.ts +5 -1
- package/dist/utils/contextBudget.js +7 -4
- package/dist/utils/requestIntent.js +5 -4
- package/dist/utils/v3-workspace-path.js +22 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/package.json +2 -1
- package/scripts/release/validate-no-go-gates.sh +41 -5
|
@@ -4,6 +4,8 @@ interface BackgroundOptions {
|
|
|
4
4
|
json?: boolean;
|
|
5
5
|
limit?: number;
|
|
6
6
|
workspace?: string;
|
|
7
|
+
deny?: boolean;
|
|
8
|
+
model?: string;
|
|
7
9
|
}
|
|
8
10
|
export declare class BackgroundCommand {
|
|
9
11
|
private config;
|
|
@@ -16,5 +18,6 @@ export declare class BackgroundCommand {
|
|
|
16
18
|
apply(jobId: string, options?: BackgroundOptions): Promise<void>;
|
|
17
19
|
cancel(jobId: string, options?: BackgroundOptions): Promise<void>;
|
|
18
20
|
private extractBackgroundOutcome;
|
|
21
|
+
approve(jobId: string, approvalId: string, options?: BackgroundOptions): Promise<void>;
|
|
19
22
|
}
|
|
20
23
|
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { createSpinner, CH } from '../utils/logger.js';
|
|
3
3
|
import { APIClient } from '../utils/api.js';
|
|
4
|
+
import { buildExecutionHints, inferAgentTaskType } from '../utils/requestIntent.js';
|
|
4
5
|
export class BackgroundCommand {
|
|
5
6
|
config;
|
|
6
7
|
logger;
|
|
@@ -17,6 +18,15 @@ export class BackgroundCommand {
|
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
const workspacePath = options.workspace || process.cwd();
|
|
21
|
+
const requestedModel = String(options.model || 'agent').trim().toLowerCase();
|
|
22
|
+
const allowedModels = new Set(['agent', 'cloud', 'cloud-reason', 'ultra']);
|
|
23
|
+
if (!allowedModels.has(requestedModel)) {
|
|
24
|
+
this.logger.error('Background model must be one of: agent, cloud, cloud-reason, ultra');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const cloudSelected = ['cloud', 'cloud-reason', 'ultra'].includes(requestedModel);
|
|
28
|
+
const agentTaskType = inferAgentTaskType(prompt);
|
|
29
|
+
const routedHints = buildExecutionHints(agentTaskType, prompt);
|
|
20
30
|
const spinner = createSpinner('Starting background agent job...').start();
|
|
21
31
|
try {
|
|
22
32
|
const job = await this.api.startV3BackgroundJob(prompt, {
|
|
@@ -27,6 +37,19 @@ export class BackgroundCommand {
|
|
|
27
37
|
executionSurface: 'cli',
|
|
28
38
|
clientSurface: 'cli',
|
|
29
39
|
clientToolExecution: false,
|
|
40
|
+
model: requestedModel,
|
|
41
|
+
requestedModel,
|
|
42
|
+
agentTaskType,
|
|
43
|
+
workflowType: routedHints.workflow_type,
|
|
44
|
+
executionHints: {
|
|
45
|
+
...routedHints,
|
|
46
|
+
cloud_selected: cloudSelected,
|
|
47
|
+
// Detached jobs must preserve the selected transport. This stops a
|
|
48
|
+
// paid cloud request from being silently reinterpreted as a local
|
|
49
|
+
// router alias during recovery.
|
|
50
|
+
enforce_cloud_only: cloudSelected,
|
|
51
|
+
background_model: requestedModel,
|
|
52
|
+
},
|
|
30
53
|
});
|
|
31
54
|
spinner.stop();
|
|
32
55
|
if (options.json) {
|
|
@@ -37,6 +60,7 @@ export class BackgroundCommand {
|
|
|
37
60
|
console.log(chalk.gray(` Job: ${job.job_id}`));
|
|
38
61
|
console.log(chalk.gray(` Status: ${job.status}`));
|
|
39
62
|
console.log(chalk.gray(` Workspace: ${workspacePath}`));
|
|
63
|
+
console.log(chalk.gray(` Model: ${requestedModel}`));
|
|
40
64
|
console.log();
|
|
41
65
|
console.log(chalk.gray(`Use ${chalk.cyan(`vigthoria background status ${job.job_id}`)} to check progress.`));
|
|
42
66
|
console.log(chalk.gray(`Use ${chalk.cyan(`vigthoria background apply ${job.job_id}`)} when it is completed.`));
|
|
@@ -100,6 +124,14 @@ export class BackgroundCommand {
|
|
|
100
124
|
}
|
|
101
125
|
if (job.error)
|
|
102
126
|
console.log(chalk.red(`Error: ${job.error}`));
|
|
127
|
+
const approvals = Array.isArray(job.pending_approvals) ? job.pending_approvals : [];
|
|
128
|
+
if (approvals.length > 0) {
|
|
129
|
+
console.log(chalk.yellow(`Gate: ${approvals.length} command approval(s) required`));
|
|
130
|
+
for (const approval of approvals) {
|
|
131
|
+
console.log(chalk.gray(` ${approval.approval_id}: ${approval.command || '(empty command)'}`));
|
|
132
|
+
console.log(chalk.gray(` Resolve: vigthoria background approve ${job.job_id} ${approval.approval_id}`));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
103
135
|
console.log(chalk.gray(`\n${String(job.request || '').slice(0, 500)}`));
|
|
104
136
|
if (job.status === 'completed' && (job.file_count || 0) > 0) {
|
|
105
137
|
console.log(chalk.gray(`\nUse ${chalk.cyan(`vigthoria background apply ${job.job_id}`)} to apply files locally.`));
|
|
@@ -170,6 +202,8 @@ export class BackgroundCommand {
|
|
|
170
202
|
let tasksCompleted = Number(result.tasks_completed ?? result.tasksCompleted ?? 0);
|
|
171
203
|
let tasksTotal = Number(result.tasks_total ?? result.tasksTotal ?? 0);
|
|
172
204
|
let success = typeof result.success === 'boolean' ? result.success : null;
|
|
205
|
+
if (result.type === 'error')
|
|
206
|
+
success = false;
|
|
173
207
|
for (const event of events) {
|
|
174
208
|
if (!event || typeof event !== 'object')
|
|
175
209
|
continue;
|
|
@@ -192,7 +226,7 @@ export class BackgroundCommand {
|
|
|
192
226
|
if (success == null && tasksTotal > 0) {
|
|
193
227
|
success = tasksCompleted >= tasksTotal;
|
|
194
228
|
}
|
|
195
|
-
if (success == null && job?.status === 'completed') {
|
|
229
|
+
if (success == null && job?.status === 'completed' && result.type !== 'error') {
|
|
196
230
|
success = true;
|
|
197
231
|
}
|
|
198
232
|
if (success == null && job?.status === 'failed') {
|
|
@@ -211,4 +245,27 @@ export class BackgroundCommand {
|
|
|
211
245
|
}
|
|
212
246
|
return { tasksCompleted, tasksTotal, success, headline };
|
|
213
247
|
}
|
|
248
|
+
async approve(jobId, approvalId, options = {}) {
|
|
249
|
+
if (!jobId || !approvalId) {
|
|
250
|
+
this.logger.error('Usage: vigthoria background approve <job-id> <approval-id>');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const approved = !options.deny;
|
|
254
|
+
const spinner = createSpinner(`${approved ? 'Approving' : 'Denying'} background command...`).start();
|
|
255
|
+
try {
|
|
256
|
+
const result = await this.api.resolveV3BackgroundApproval(jobId, approvalId, approved, 'once');
|
|
257
|
+
spinner.stop();
|
|
258
|
+
if (options.json) {
|
|
259
|
+
console.log(JSON.stringify({ ...result, job_id: jobId, approval_id: approvalId, approved }, null, 2));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
console.log(approved
|
|
263
|
+
? chalk.green(`${CH.success} Approved ${approvalId} once for ${jobId}`)
|
|
264
|
+
: chalk.yellow(`${CH.warn} Denied ${approvalId} for ${jobId}`));
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
spinner.stop();
|
|
268
|
+
this.logger.error(error?.message || String(error));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
214
271
|
}
|
package/dist/commands/chat.js
CHANGED
|
@@ -1179,6 +1179,32 @@ export class ChatCommand {
|
|
|
1179
1179
|
spinner.text = this.sanitizeServerPath(String(event.content || 'Routing to V3 Agent...'));
|
|
1180
1180
|
return;
|
|
1181
1181
|
}
|
|
1182
|
+
if (event.type === 'pipeline_stage') {
|
|
1183
|
+
if (spinner.isSpinning)
|
|
1184
|
+
spinner.stop();
|
|
1185
|
+
const stageIndex = Number.isFinite(Number(event.stage_index)) ? Number(event.stage_index) : 0;
|
|
1186
|
+
const stageTotal = Number.isFinite(Number(event.stage_total)) ? Number(event.stage_total) : 3;
|
|
1187
|
+
const stage = String(event.stage || '').trim().toLowerCase();
|
|
1188
|
+
const label = stage === 'dispatcher'
|
|
1189
|
+
? 'Dispatcher'
|
|
1190
|
+
: stage === 'architect'
|
|
1191
|
+
? 'Architect'
|
|
1192
|
+
: stage === 'executor'
|
|
1193
|
+
? 'Executor'
|
|
1194
|
+
: 'Pipeline';
|
|
1195
|
+
const model = this.sanitizeServerPath(String(event.model || event.executor_model || ''));
|
|
1196
|
+
const route = this.sanitizeServerPath(String(event.route || ''));
|
|
1197
|
+
const details = [model, route ? `route=${route}` : ''].filter(Boolean).join(' · ');
|
|
1198
|
+
process.stderr.write(chalk.cyan(` [${stageIndex || '?'}\/${stageTotal} ${label}] `)
|
|
1199
|
+
+ `${details || 'ready'}\n`);
|
|
1200
|
+
spinner.start();
|
|
1201
|
+
spinner.text = stage === 'dispatcher'
|
|
1202
|
+
? 'Olivia routed the request. Starting Architect...'
|
|
1203
|
+
: stage === 'architect'
|
|
1204
|
+
? 'Architect is producing the execution plan...'
|
|
1205
|
+
: '35B Executor is executing the approved plan...';
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1182
1208
|
if (event.type === 'thinking') {
|
|
1183
1209
|
this.v3IterationCount += 1;
|
|
1184
1210
|
const iterText = this.sanitizeServerPath(event.content || '');
|
|
@@ -1481,7 +1507,8 @@ export class ChatCommand {
|
|
|
1481
1507
|
this.sessionManager = new SessionManager();
|
|
1482
1508
|
}
|
|
1483
1509
|
async run(options) {
|
|
1484
|
-
|
|
1510
|
+
const hasRuntimeToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
|
|
1511
|
+
if (!this.config.isAuthenticated() && !hasRuntimeToken) {
|
|
1485
1512
|
if (options.json) {
|
|
1486
1513
|
process.exitCode = 1;
|
|
1487
1514
|
emitAgentJsonOutput({ success: false, error: 'Not authenticated. Run: vigthoria login' });
|
|
@@ -2337,6 +2364,12 @@ export class ChatCommand {
|
|
|
2337
2364
|
: '';
|
|
2338
2365
|
this.logger.debug(`Agent route: ${r.path} / ${r.taskKind} [${r.source}]${routerNote} — ${r.reason}`);
|
|
2339
2366
|
}
|
|
2367
|
+
// V3 owns high-fidelity HTML and other complex single-file rewrites, but
|
|
2368
|
+
// the CLI still has the authoritative local workspace. Prime the target
|
|
2369
|
+
// through the client-tool bridge before handing the task to V3 so the
|
|
2370
|
+
// model receives the real current file and the terminal records a
|
|
2371
|
+
// verifiable read-before-write operation.
|
|
2372
|
+
await this.primeBypassedTargetFileContext(resolvedPrompt);
|
|
2340
2373
|
const handledByV3Workflow = await this.tryV3AgentWorkflow(resolvedPrompt);
|
|
2341
2374
|
if (handledByV3Workflow) {
|
|
2342
2375
|
this.saveSession();
|
|
@@ -3209,6 +3242,7 @@ export class ChatCommand {
|
|
|
3209
3242
|
requestedModel: this.currentModel,
|
|
3210
3243
|
agentExecutionPolicy: routingPolicy,
|
|
3211
3244
|
legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
|
|
3245
|
+
approvalLevel: this.autoApprove ? 'auto' : 'confirm',
|
|
3212
3246
|
rawPrompt: prompt,
|
|
3213
3247
|
contextualPrompt,
|
|
3214
3248
|
history: this.getMessagesForModel(),
|
|
@@ -3688,6 +3722,7 @@ export class ChatCommand {
|
|
|
3688
3722
|
requestedModel: this.currentModel,
|
|
3689
3723
|
agentExecutionPolicy: routingPolicy,
|
|
3690
3724
|
legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
|
|
3725
|
+
approvalLevel: this.autoApprove ? 'auto' : 'confirm',
|
|
3691
3726
|
rawPrompt,
|
|
3692
3727
|
history: this.getMessagesForModel(),
|
|
3693
3728
|
onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
|
package/dist/commands/legion.js
CHANGED
|
@@ -48,19 +48,31 @@ export class LegionCommand {
|
|
|
48
48
|
}
|
|
49
49
|
getHyperloopUrls() {
|
|
50
50
|
const urls = new Set();
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
const normalizeBase = (raw) => {
|
|
52
|
+
let base = String(raw || '').trim().replace(/\/$/, '');
|
|
53
|
+
base = base.replace(/\/api\/hyperloop\/(?:health|modules|execute)$/i, '/api/hyperloop');
|
|
54
|
+
if (base && !/\/api\/hyperloop$/i.test(base))
|
|
55
|
+
base += '/api/hyperloop';
|
|
56
|
+
return base;
|
|
57
|
+
};
|
|
58
|
+
// On-server checks must try loopback first. The former configured 10.0.0.2
|
|
59
|
+
// route could consume the entire timeout while the same node was healthy.
|
|
59
60
|
if (isServerRuntime()) {
|
|
60
61
|
for (const internal of buildServerHyperloopUrls()) {
|
|
61
|
-
|
|
62
|
+
const normalized = normalizeBase(internal);
|
|
63
|
+
if (normalized)
|
|
64
|
+
urls.add(normalized);
|
|
62
65
|
}
|
|
63
66
|
}
|
|
67
|
+
const envUrl = normalizeBase(process.env.VIGTHORIA_HYPERLOOP_URL || '');
|
|
68
|
+
if (envUrl)
|
|
69
|
+
urls.add(envUrl);
|
|
70
|
+
const configuredApiUrl = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
|
|
71
|
+
if (configuredApiUrl) {
|
|
72
|
+
const normalized = normalizeBase(configuredApiUrl);
|
|
73
|
+
if (normalized)
|
|
74
|
+
urls.add(normalized);
|
|
75
|
+
}
|
|
64
76
|
return Array.from(urls);
|
|
65
77
|
}
|
|
66
78
|
getHeaders() {
|
|
@@ -1495,19 +1507,42 @@ export class LegionCommand {
|
|
|
1495
1507
|
for (const baseUrl of this.getHyperloopUrls()) {
|
|
1496
1508
|
try {
|
|
1497
1509
|
const response = await fetch(`${baseUrl}/status`, {
|
|
1498
|
-
signal: AbortSignal.timeout(
|
|
1510
|
+
signal: AbortSignal.timeout(8000),
|
|
1499
1511
|
headers: this.getHeaders(),
|
|
1500
1512
|
});
|
|
1513
|
+
let data;
|
|
1514
|
+
let healthOnly = false;
|
|
1501
1515
|
if (!response.ok) {
|
|
1502
|
-
|
|
1503
|
-
|
|
1516
|
+
// /status is authenticated, while the root /health endpoint is the
|
|
1517
|
+
// authoritative liveness probe. A CLI without the internal service
|
|
1518
|
+
// credential must still report a live node as online rather than
|
|
1519
|
+
// falling through to a stale private-network address.
|
|
1520
|
+
const parsed = new URL(baseUrl);
|
|
1521
|
+
const healthUrl = /^(?:127\.0\.0\.1|localhost|::1)$/i.test(parsed.hostname)
|
|
1522
|
+
? `${parsed.origin}/health`
|
|
1523
|
+
: `${baseUrl}/health`;
|
|
1524
|
+
const healthResponse = await fetch(healthUrl, {
|
|
1525
|
+
signal: AbortSignal.timeout(3000),
|
|
1526
|
+
headers: this.getHeaders(),
|
|
1527
|
+
});
|
|
1528
|
+
if (!healthResponse.ok) {
|
|
1529
|
+
lastError = `Legion status check at ${baseUrl} failed: ${response.status} ${describeUpstreamStatus(response.status)}`;
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
data = await this.readJsonResponse(healthResponse, `health check at ${healthUrl}`);
|
|
1533
|
+
healthOnly = true;
|
|
1534
|
+
}
|
|
1535
|
+
else {
|
|
1536
|
+
data = await this.readJsonResponse(response, `status check at ${baseUrl}`);
|
|
1504
1537
|
}
|
|
1505
|
-
const data = await this.readJsonResponse(response, `status check at ${baseUrl}`);
|
|
1506
1538
|
spinner.stop();
|
|
1507
1539
|
console.log();
|
|
1508
1540
|
console.log(chalk.bold.white(` ${CH.hLine.repeat(3)} Legion Infrastructure ${CH.hLine.repeat(37)}`));
|
|
1509
1541
|
console.log();
|
|
1510
1542
|
console.log(chalk.gray(` Hyper Loop: `) + chalk.green('online'));
|
|
1543
|
+
if (healthOnly) {
|
|
1544
|
+
console.log(chalk.gray(' Control plane: liveness verified; authenticated worker details not exposed'));
|
|
1545
|
+
}
|
|
1511
1546
|
if (data.workers || data.active_workers) {
|
|
1512
1547
|
const count = data.workers || data.active_workers;
|
|
1513
1548
|
console.log(chalk.gray(` Active workers: `) + chalk.white(String(typeof count === 'number' ? count : Object.keys(count).length)));
|
package/dist/index.js
CHANGED
|
@@ -472,10 +472,10 @@ async function enforceGatewayAuthSession(config, logger, jsonOutputRequested) {
|
|
|
472
472
|
if (isLocalTestfarmMode()) {
|
|
473
473
|
return true;
|
|
474
474
|
}
|
|
475
|
-
|
|
475
|
+
const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
|
|
476
|
+
if (!config.isAuthenticated() && !explicitEnvToken) {
|
|
476
477
|
return true;
|
|
477
478
|
}
|
|
478
|
-
const explicitEnvToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
|
|
479
479
|
// Offline mode disables every outbound preflight call.
|
|
480
480
|
if (isOfflineMode()) {
|
|
481
481
|
return true;
|
|
@@ -1317,6 +1317,7 @@ Examples:
|
|
|
1317
1317
|
.command('start <prompt...>')
|
|
1318
1318
|
.description('Start an Agent job in the background and return immediately')
|
|
1319
1319
|
.option('--workspace <path>', 'Local workspace path', process.cwd())
|
|
1320
|
+
.option('--model <model>', 'Execution lane: agent, cloud, cloud-reason, or ultra', 'agent')
|
|
1320
1321
|
.option('--json', 'Emit JSON output', false)
|
|
1321
1322
|
.action(async (prompt, options) => {
|
|
1322
1323
|
const bg = new BackgroundCommand(config, logger);
|
|
@@ -1357,6 +1358,15 @@ Examples:
|
|
|
1357
1358
|
const bg = new BackgroundCommand(config, logger);
|
|
1358
1359
|
await bg.cancel(jobId, options);
|
|
1359
1360
|
});
|
|
1361
|
+
backgroundCommand
|
|
1362
|
+
.command('approve <jobId> <approvalId>')
|
|
1363
|
+
.description('Resolve a pending two-stage command gate for a background job')
|
|
1364
|
+
.option('--deny', 'Deny the command instead of approving it', false)
|
|
1365
|
+
.option('--json', 'Emit JSON output', false)
|
|
1366
|
+
.action(async (jobId, approvalId, options) => {
|
|
1367
|
+
const bg = new BackgroundCommand(config, logger);
|
|
1368
|
+
await bg.approve(jobId, approvalId, options);
|
|
1369
|
+
});
|
|
1360
1370
|
backgroundCommand.action(() => {
|
|
1361
1371
|
backgroundCommand.outputHelp();
|
|
1362
1372
|
});
|
package/dist/utils/api.d.ts
CHANGED
|
@@ -261,6 +261,7 @@ export declare class APIClient {
|
|
|
261
261
|
getV3AgentRunUrl(baseUrl: string): string;
|
|
262
262
|
getV3AgentContinueUrl(baseUrl: string): string;
|
|
263
263
|
getV3AgentBackgroundUrl(baseUrl: string, suffix?: string): string;
|
|
264
|
+
getV3AgentApprovalUrl(baseUrl: string): string;
|
|
264
265
|
getOperatorBaseUrls(): string[];
|
|
265
266
|
getOperatorStreamUrl(baseUrl: string): string;
|
|
266
267
|
getMcpBaseUrls(): string[];
|
|
@@ -359,6 +360,12 @@ export declare class APIClient {
|
|
|
359
360
|
private buildPublicWorkspaceDescriptor;
|
|
360
361
|
private buildPublicRuntimeEnvironment;
|
|
361
362
|
private buildLocalWorkspaceSummary;
|
|
363
|
+
/**
|
|
364
|
+
* Build complete workspace hydration separately from model context.
|
|
365
|
+
* Remote audits must inspect the same byte-complete assets the user has
|
|
366
|
+
* locally; context compaction must never become filesystem compaction.
|
|
367
|
+
*/
|
|
368
|
+
private buildOutOfBandWorkspaceFiles;
|
|
362
369
|
/**
|
|
363
370
|
* Collect text file contents from the workspace for V3 agent hydration.
|
|
364
371
|
* Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
|
|
@@ -405,10 +412,12 @@ export declare class APIClient {
|
|
|
405
412
|
listV3BackgroundJobs(limit?: number): Promise<any[]>;
|
|
406
413
|
getV3BackgroundJob(jobId: string): Promise<any>;
|
|
407
414
|
cancelV3BackgroundJob(jobId: string): Promise<any>;
|
|
415
|
+
resolveV3BackgroundApproval(jobId: string, approvalId: string, approved: boolean, allowMode?: string): Promise<any>;
|
|
408
416
|
getV3BackgroundJobFiles(jobId: string): Promise<{
|
|
409
417
|
status: string;
|
|
410
418
|
files: Record<string, string>;
|
|
411
419
|
file_count: number;
|
|
420
|
+
confirmed_mutations?: string[];
|
|
412
421
|
local_workspace_path?: string;
|
|
413
422
|
}>;
|
|
414
423
|
applyV3BackgroundJobFiles(jobId: string, context?: Record<string, any>): Promise<{
|
package/dist/utils/api.js
CHANGED
|
@@ -659,6 +659,7 @@ export class APIClient {
|
|
|
659
659
|
getV3AgentBaseUrls(preferLocal = false) {
|
|
660
660
|
const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
|
|
661
661
|
const localTestMode = isLocalTestfarmMode();
|
|
662
|
+
const explicitV3EndpointConfigured = Boolean(process.env.VIGTHORIA_V3_AGENT_URL || process.env.V3_AGENT_URL);
|
|
662
663
|
const includeLoopbackV3Agent = process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1'
|
|
663
664
|
|| (preferLocal && isServerRuntime());
|
|
664
665
|
const localCandidates = [
|
|
@@ -672,7 +673,10 @@ export class APIClient {
|
|
|
672
673
|
const normalizedRemote = remoteCandidates
|
|
673
674
|
.filter(Boolean)
|
|
674
675
|
.map((url) => String(url).replace(/\/$/, ''));
|
|
675
|
-
|
|
676
|
+
// An explicit V3 endpoint is an operator routing decision, not a fallback.
|
|
677
|
+
// Keep it ahead of the public Coder relay so stale remote user credentials
|
|
678
|
+
// cannot abort a healthy service-key-authenticated internal CLI run.
|
|
679
|
+
const urls = (preferLocal || localTestMode || explicitV3EndpointConfigured) && localCandidates.length > 0
|
|
676
680
|
? [...localCandidates, ...normalizedRemote]
|
|
677
681
|
: [...normalizedRemote, ...localCandidates];
|
|
678
682
|
return [...new Set(urls)];
|
|
@@ -696,6 +700,12 @@ export class APIClient {
|
|
|
696
700
|
}
|
|
697
701
|
return `${baseUrl}/api/v3-agent/background${cleanSuffix}`;
|
|
698
702
|
}
|
|
703
|
+
getV3AgentApprovalUrl(baseUrl) {
|
|
704
|
+
if (this.isDirectV3AgentBaseUrl(baseUrl)) {
|
|
705
|
+
return `${baseUrl}/api/agent/approval`;
|
|
706
|
+
}
|
|
707
|
+
return `${baseUrl}/api/v3-agent/approval`;
|
|
708
|
+
}
|
|
699
709
|
getOperatorBaseUrls() {
|
|
700
710
|
const configuredModelsApiUrl = String(this.config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
|
|
701
711
|
const urls = [
|
|
@@ -876,6 +886,7 @@ export class APIClient {
|
|
|
876
886
|
extractLinkedFrontendAssets(html, entryPath) {
|
|
877
887
|
const css = new Set();
|
|
878
888
|
const js = new Set();
|
|
889
|
+
const assets = new Set();
|
|
879
890
|
const entryDir = path.posix.dirname(this.normalizeWorkspaceRelativePath(entryPath) || '.');
|
|
880
891
|
const normalizeAsset = (assetPath) => {
|
|
881
892
|
const clean = String(assetPath || '').trim();
|
|
@@ -887,7 +898,10 @@ export class APIClient {
|
|
|
887
898
|
return null;
|
|
888
899
|
}
|
|
889
900
|
if (withoutQuery.startsWith('/')) {
|
|
890
|
-
|
|
901
|
+
// Preview/runtime serve a nested HTML entry from its containing
|
|
902
|
+
// directory, so `/styles.css` beside `showcase/index.html` resolves to
|
|
903
|
+
// `showcase/styles.css` in the hydrated workspace.
|
|
904
|
+
return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery.slice(1))));
|
|
891
905
|
}
|
|
892
906
|
return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery)));
|
|
893
907
|
};
|
|
@@ -906,9 +920,17 @@ export class APIClient {
|
|
|
906
920
|
js.add(resolved);
|
|
907
921
|
}
|
|
908
922
|
}
|
|
923
|
+
const assetPattern = /<(?:link|img|source|video|audio)\b[^>]+(?:href|src)=["']([^"']+)["'][^>]*>/gi;
|
|
924
|
+
while ((match = assetPattern.exec(String(html || ''))) !== null) {
|
|
925
|
+
const resolved = normalizeAsset(match[1]);
|
|
926
|
+
if (resolved) {
|
|
927
|
+
assets.add(resolved);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
909
930
|
return {
|
|
910
931
|
css: Array.from(css),
|
|
911
932
|
js: Array.from(js),
|
|
933
|
+
assets: Array.from(assets),
|
|
912
934
|
};
|
|
913
935
|
}
|
|
914
936
|
async buildFrontendForPreview(rootPath) {
|
|
@@ -1568,10 +1590,15 @@ export class APIClient {
|
|
|
1568
1590
|
'styles.css', 'style.css', 'README.md', 'manifest.json',
|
|
1569
1591
|
]);
|
|
1570
1592
|
/** Keep critical workspace files for server hydration when context must shrink. */
|
|
1571
|
-
compactWorkspaceFilesToBudget(files, budgetChars) {
|
|
1593
|
+
compactWorkspaceFilesToBudget(files, budgetChars, priorityPaths = [], mandatoryPaths = priorityPaths) {
|
|
1572
1594
|
const entries = Object.entries(files);
|
|
1595
|
+
const explicitPriority = new Map(priorityPaths.map((filePath, index) => [this.normalizeWorkspaceRelativePath(filePath), index]));
|
|
1596
|
+
const mandatory = new Set(mandatoryPaths.map((filePath) => this.normalizeWorkspaceRelativePath(filePath)));
|
|
1573
1597
|
const rank = (filePath) => {
|
|
1574
1598
|
const normalized = filePath.replace(/\\/g, '/');
|
|
1599
|
+
const explicitIndex = explicitPriority.get(normalized);
|
|
1600
|
+
if (explicitIndex !== undefined)
|
|
1601
|
+
return -100 + Math.min(explicitIndex, 50);
|
|
1575
1602
|
const base = normalized.split('/').pop() || '';
|
|
1576
1603
|
if (APIClient.HYDRATION_PRIORITY_BASENAMES.has(base))
|
|
1577
1604
|
return 0;
|
|
@@ -1589,6 +1616,28 @@ export class APIClient {
|
|
|
1589
1616
|
const clipped = content.length > maxContentLen ? `${content.slice(0, maxContentLen)}\n/* … truncated for remote context … */` : content;
|
|
1590
1617
|
const entryLen = JSON.stringify(filePath).length + 1 + JSON.stringify(clipped).length + 1;
|
|
1591
1618
|
if (used + entryLen > budgetChars) {
|
|
1619
|
+
const normalizedPath = this.normalizeWorkspaceRelativePath(filePath);
|
|
1620
|
+
if (mandatory.has(normalizedPath)) {
|
|
1621
|
+
// Explicit prompt targets are hydration requirements, not ranking
|
|
1622
|
+
// hints. Skipping an HTML entry merely because a larger targeted
|
|
1623
|
+
// stylesheet consumed the provisional budget creates false preview
|
|
1624
|
+
// failures in the remote V3 workspace. Keep the complete explicit
|
|
1625
|
+
// file and let later context-compaction phases shed history/metadata.
|
|
1626
|
+
trimmed[filePath] = clipped;
|
|
1627
|
+
used += entryLen;
|
|
1628
|
+
}
|
|
1629
|
+
else if (explicitPriority.has(normalizedPath)) {
|
|
1630
|
+
// Linked preview assets are important enough to hydrate, but unlike
|
|
1631
|
+
// edit targets they may be compacted. Keep a bounded prefix so the
|
|
1632
|
+
// remote static server resolves the asset instead of returning 404.
|
|
1633
|
+
const envelope = JSON.stringify(filePath).length + 128;
|
|
1634
|
+
const available = Math.max(256, budgetChars - used - envelope);
|
|
1635
|
+
const dependencyClip = content.length > available
|
|
1636
|
+
? `${content.slice(0, available)}\n/* … compacted linked preview dependency … */`
|
|
1637
|
+
: content;
|
|
1638
|
+
trimmed[filePath] = dependencyClip;
|
|
1639
|
+
used += JSON.stringify(filePath).length + 1 + JSON.stringify(dependencyClip).length + 1;
|
|
1640
|
+
}
|
|
1592
1641
|
continue;
|
|
1593
1642
|
}
|
|
1594
1643
|
trimmed[filePath] = clipped;
|
|
@@ -1603,16 +1652,30 @@ export class APIClient {
|
|
|
1603
1652
|
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
1604
1653
|
const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
|
|
1605
1654
|
const localWorkspaceSummary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
|
|
1655
|
+
if (resolvedContext.workspaceFilesOutOfBand === true && localWorkspaceSummary?.workspaceFiles) {
|
|
1656
|
+
localWorkspaceSummary.workspaceHydration = {
|
|
1657
|
+
delivery: 'out-of-band',
|
|
1658
|
+
fileCount: Object.keys(localWorkspaceSummary.workspaceFiles).length,
|
|
1659
|
+
complete: true,
|
|
1660
|
+
};
|
|
1661
|
+
delete localWorkspaceSummary.workspaceFiles;
|
|
1662
|
+
}
|
|
1606
1663
|
const requestedModel = String(resolvedContext.model || resolvedContext.requestedModel || 'agent');
|
|
1607
1664
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
1608
1665
|
const localWorkspaceName = this.getDisplayWorkspaceName(localWorkspacePath);
|
|
1609
1666
|
const localWorkspaceRef = localWorkspaceName ? `vigthoria://local-workspace/${localWorkspaceName}` : null;
|
|
1610
1667
|
const executionSurface = String(resolvedContext.executionSurface || resolvedContext.clientSurface || 'cli');
|
|
1611
1668
|
const localMachineCapable = resolvedContext.localMachineCapable !== false;
|
|
1669
|
+
// A workspace under a configured server root is already available to the
|
|
1670
|
+
// V3 service. Keep tool execution on the server in that case; delegating
|
|
1671
|
+
// tools back to the CLI can turn server paths into invalid client paths.
|
|
1672
|
+
// An explicit caller override remains authoritative for specialist flows.
|
|
1612
1673
|
const clientToolExecution = resolvedContext.clientToolExecution === false
|
|
1613
1674
|
? false
|
|
1614
1675
|
: (resolvedContext.clientToolExecution === true
|
|
1615
|
-
|| (
|
|
1676
|
+
|| (!serverWorkspacePath
|
|
1677
|
+
&& localMachineCapable
|
|
1678
|
+
&& ['cli', 'fork', 'local-ide', 'desktop', 'local'].includes(executionSurface)));
|
|
1616
1679
|
const publicRuntimeEnvironment = this.buildPublicRuntimeEnvironment(resolvedContext.agentRuntime, {
|
|
1617
1680
|
localWorkspacePath,
|
|
1618
1681
|
serverWorkspacePath,
|
|
@@ -1658,6 +1721,8 @@ export class APIClient {
|
|
|
1658
1721
|
requestedModelResolved: resolvedModel,
|
|
1659
1722
|
agentExecutionPolicy: resolvedContext.agentExecutionPolicy || null,
|
|
1660
1723
|
legacyFallbackAllowed: resolvedContext.legacyFallbackAllowed === true,
|
|
1724
|
+
approvalLevel: resolvedContext.approvalLevel === 'auto' ? 'auto' : 'confirm',
|
|
1725
|
+
approval_level: resolvedContext.approvalLevel === 'auto' ? 'auto' : 'confirm',
|
|
1661
1726
|
executionSurface: resolvedContext.executionSurface || 'cli',
|
|
1662
1727
|
clientSurface: resolvedContext.clientSurface || 'cli',
|
|
1663
1728
|
localMachineCapable,
|
|
@@ -1752,11 +1817,11 @@ export class APIClient {
|
|
|
1752
1817
|
const overhead = json.length - JSON.stringify(summary.workspaceFiles).length;
|
|
1753
1818
|
const budget = LIMIT - overhead - 1024;
|
|
1754
1819
|
if (budget > 0) {
|
|
1755
|
-
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, budget);
|
|
1820
|
+
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, budget, summary.focusFiles || [], summary.explicitFocusFiles || summary.focusFiles || []);
|
|
1756
1821
|
summary.workspaceFilesCompaction = Object.keys(summary.workspaceFiles).length > 0 ? 'priority-trimmed' : 'empty';
|
|
1757
1822
|
}
|
|
1758
1823
|
else if (hydrationRequired) {
|
|
1759
|
-
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 8_000);
|
|
1824
|
+
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 8_000, summary.focusFiles || [], summary.explicitFocusFiles || summary.focusFiles || []);
|
|
1760
1825
|
summary.workspaceFilesCompaction = 'priority-minimal';
|
|
1761
1826
|
}
|
|
1762
1827
|
else {
|
|
@@ -1781,7 +1846,7 @@ export class APIClient {
|
|
|
1781
1846
|
return finish();
|
|
1782
1847
|
}
|
|
1783
1848
|
if (hydrationRequired && summary?.workspaceFiles && typeof summary.workspaceFiles === 'object') {
|
|
1784
|
-
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 4_000);
|
|
1849
|
+
summary.workspaceFiles = this.compactWorkspaceFilesToBudget(summary.workspaceFiles, 4_000, summary.focusFiles || [], summary.explicitFocusFiles || summary.focusFiles || []);
|
|
1785
1850
|
summary.workspaceFilesCompaction = 'priority-minimal';
|
|
1786
1851
|
json = JSON.stringify(payload);
|
|
1787
1852
|
if (json.length <= LIMIT)
|
|
@@ -2657,8 +2722,30 @@ menu {
|
|
|
2657
2722
|
};
|
|
2658
2723
|
const snapshot = this.getAgentWorkspaceSnapshot(rootPath);
|
|
2659
2724
|
const candidatePaths = snapshot.paths.filter((entry) => !/(^|[\/\\])\.vigthoria([\/\\]|$)/.test(entry));
|
|
2725
|
+
const focusFiles = this.extractExpectedWorkspaceFiles(requestFocus)
|
|
2726
|
+
.map((entry) => this.normalizeWorkspaceRelativePath(entry))
|
|
2727
|
+
.filter((entry) => candidatePaths.includes(entry));
|
|
2728
|
+
const linkedFocusAssets = [];
|
|
2729
|
+
for (const focusPath of focusFiles) {
|
|
2730
|
+
if (!/\.html?$/i.test(focusPath))
|
|
2731
|
+
continue;
|
|
2732
|
+
try {
|
|
2733
|
+
const html = fs.readFileSync(path.join(rootPath, focusPath), 'utf8');
|
|
2734
|
+
const linkedAssets = this.extractLinkedFrontendAssets(html, focusPath);
|
|
2735
|
+
for (const assetPath of [...linkedAssets.assets, ...linkedAssets.css, ...linkedAssets.js]) {
|
|
2736
|
+
if (candidatePaths.includes(assetPath) && !linkedFocusAssets.includes(assetPath)) {
|
|
2737
|
+
linkedFocusAssets.push(assetPath);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
catch {
|
|
2742
|
+
// Missing/unreadable focused HTML remains visible to the server via
|
|
2743
|
+
// the original focus list; dependency expansion is best effort.
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
const hydrationFocusFiles = [...focusFiles, ...linkedFocusAssets];
|
|
2660
2747
|
const agentStatePaths = this.collectAgentStateSyncPaths(rootPath);
|
|
2661
|
-
let orderedPaths = [...agentStatePaths, ...candidatePaths];
|
|
2748
|
+
let orderedPaths = [...agentStatePaths, ...hydrationFocusFiles, ...candidatePaths];
|
|
2662
2749
|
try {
|
|
2663
2750
|
const ranked = buildSemanticContext(rootPath, requestFocus, 60).topFiles.map((file) => file.path);
|
|
2664
2751
|
const changes = getChangedFiles(rootPath, candidatePaths);
|
|
@@ -2670,6 +2757,8 @@ menu {
|
|
|
2670
2757
|
// silently dropped progress.json and forced /continue to re-plan.
|
|
2671
2758
|
for (const filePath of agentStatePaths)
|
|
2672
2759
|
ordered.add(filePath);
|
|
2760
|
+
for (const filePath of hydrationFocusFiles)
|
|
2761
|
+
ordered.add(filePath);
|
|
2673
2762
|
for (const filePath of changes.changed)
|
|
2674
2763
|
ordered.add(filePath);
|
|
2675
2764
|
for (const filePath of ranked)
|
|
@@ -2685,9 +2774,11 @@ menu {
|
|
|
2685
2774
|
};
|
|
2686
2775
|
}
|
|
2687
2776
|
catch {
|
|
2688
|
-
orderedPaths = [...agentStatePaths, ...candidatePaths];
|
|
2777
|
+
orderedPaths = [...agentStatePaths, ...hydrationFocusFiles, ...candidatePaths];
|
|
2689
2778
|
}
|
|
2690
2779
|
summary.fileCount = snapshot.fileCount;
|
|
2780
|
+
summary.focusFiles = hydrationFocusFiles;
|
|
2781
|
+
summary.explicitFocusFiles = focusFiles;
|
|
2691
2782
|
summary.files = orderedPaths.slice(0, 40);
|
|
2692
2783
|
const packageJsonPath = path.join(rootPath, 'package.json');
|
|
2693
2784
|
if (fs.existsSync(packageJsonPath)) {
|
|
@@ -2714,6 +2805,22 @@ menu {
|
|
|
2714
2805
|
return null;
|
|
2715
2806
|
}
|
|
2716
2807
|
}
|
|
2808
|
+
/**
|
|
2809
|
+
* Build complete workspace hydration separately from model context.
|
|
2810
|
+
* Remote audits must inspect the same byte-complete assets the user has
|
|
2811
|
+
* locally; context compaction must never become filesystem compaction.
|
|
2812
|
+
*/
|
|
2813
|
+
buildOutOfBandWorkspaceFiles(context) {
|
|
2814
|
+
const resolvedContext = this.ensureExecutionContext(context);
|
|
2815
|
+
const localWorkspacePath = this.resolveAgentTargetPath(resolvedContext);
|
|
2816
|
+
const serverWorkspacePath = this.resolveServerBindableWorkspacePath(resolvedContext);
|
|
2817
|
+
if (serverWorkspacePath || !localWorkspacePath)
|
|
2818
|
+
return undefined;
|
|
2819
|
+
const promptFocus = String(resolvedContext.rawPrompt || resolvedContext.contextualPrompt || resolvedContext.prompt || '');
|
|
2820
|
+
const summary = this.buildLocalWorkspaceSummary(localWorkspacePath, promptFocus);
|
|
2821
|
+
const files = summary?.workspaceFiles;
|
|
2822
|
+
return files && typeof files === 'object' ? files : undefined;
|
|
2823
|
+
}
|
|
2717
2824
|
/**
|
|
2718
2825
|
* Collect text file contents from the workspace for V3 agent hydration.
|
|
2719
2826
|
* Budget: up to ~2 MB total, per-file cap 200 KB, skip binary extensions.
|
|
@@ -2722,7 +2829,7 @@ menu {
|
|
|
2722
2829
|
const MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
2723
2830
|
const MAX_FILE_BYTES = 200 * 1024;
|
|
2724
2831
|
const BINARY_EXTENSIONS = new Set([
|
|
2725
|
-
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.
|
|
2832
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp', '.avif',
|
|
2726
2833
|
'.mp3', '.mp4', '.wav', '.ogg', '.webm', '.flac', '.aac',
|
|
2727
2834
|
'.zip', '.gz', '.tar', '.rar', '.7z', '.bz2',
|
|
2728
2835
|
'.exe', '.dll', '.so', '.dylib', '.bin', '.dat',
|
|
@@ -3028,7 +3135,9 @@ menu {
|
|
|
3028
3135
|
const args = event.arguments || {};
|
|
3029
3136
|
const serverRoot = String(context.__v3ServerWorkspaceRoot || '').trim();
|
|
3030
3137
|
const isRunCommand = name === 'run_command';
|
|
3031
|
-
const pathArg = isRunCommand
|
|
3138
|
+
const pathArg = isRunCommand
|
|
3139
|
+
? (args.cwd ?? args.path ?? args.directory ?? '.')
|
|
3140
|
+
: (args.path || args.directory || args.cwd || '.');
|
|
3032
3141
|
let target = this.resolveV3ClientToolPath(rootPath, pathArg, serverRoot ? [serverRoot] : []);
|
|
3033
3142
|
if (!target && isRunCommand) {
|
|
3034
3143
|
target = this.resolveV3ClientToolPath(rootPath, '.', serverRoot ? [serverRoot] : []);
|
|
@@ -4120,6 +4229,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4120
4229
|
const expectedFiles = this.extractExpectedWorkspaceFiles(message, executionContext);
|
|
4121
4230
|
const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
|
|
4122
4231
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
4232
|
+
const workspaceFiles = this.buildOutOfBandWorkspaceFiles(executionContext);
|
|
4123
4233
|
const preferLocalV3 = /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|responsive|animated|create the required project files and write them to the workspace)/i.test(message)
|
|
4124
4234
|
&& context.localMachineCapable !== false;
|
|
4125
4235
|
const timeoutMs = resolvePlannerAgentTimeoutMs(baseTimeoutMs, String(executionContext.agentTaskType || ''), String(executionContext.rawPrompt || message || ''));
|
|
@@ -4149,7 +4259,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4149
4259
|
strict_mode: useRelaxedAttempt ? false : requestExecutionContext.legacyFallbackAllowed !== true,
|
|
4150
4260
|
context: useRelaxedAttempt
|
|
4151
4261
|
? this.buildMinimalV3AgentContext(requestExecutionContext)
|
|
4152
|
-
: this.buildV3AgentContext(requestExecutionContext),
|
|
4262
|
+
: this.buildV3AgentContext({ ...requestExecutionContext, workspaceFilesOutOfBand: !!workspaceFiles }),
|
|
4263
|
+
workspace_files: useRelaxedAttempt ? undefined : workspaceFiles,
|
|
4153
4264
|
context_id: contextIdOverride ?? requestExecutionContext.contextId,
|
|
4154
4265
|
mcp_context_id: useRelaxedAttempt ? null : requestExecutionContext.mcpContextId || null,
|
|
4155
4266
|
stream: true,
|
|
@@ -4364,18 +4475,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4364
4475
|
await this.ensureV3ServiceKey();
|
|
4365
4476
|
const executionContext = await this.bindExecutionContext({
|
|
4366
4477
|
...context,
|
|
4478
|
+
// Background callers historically supplied only workspace metadata.
|
|
4479
|
+
// Without the request in the execution context, buildV3AgentContext()
|
|
4480
|
+
// classified every detached job from an empty prompt and emitted an
|
|
4481
|
+
// `analysis_only` workflow. V3 would then inspect the workspace but was
|
|
4482
|
+
// correctly forbidden from writing. Preserve an explicit caller value,
|
|
4483
|
+
// otherwise make the submitted job request authoritative for routing.
|
|
4484
|
+
rawPrompt: context.rawPrompt || context.prompt || message,
|
|
4485
|
+
prompt: context.prompt || context.rawPrompt || message,
|
|
4367
4486
|
backgroundJob: true,
|
|
4368
4487
|
clientToolExecution: false,
|
|
4369
4488
|
});
|
|
4370
4489
|
const requestedModel = String(executionContext.model || executionContext.requestedModel || 'agent');
|
|
4371
4490
|
const resolvedModel = this.resolvePermittedModelId(requestedModel);
|
|
4491
|
+
const workspaceFiles = this.buildOutOfBandWorkspaceFiles(executionContext);
|
|
4372
4492
|
const body = {
|
|
4373
4493
|
request: message,
|
|
4374
4494
|
context: this.buildV3AgentContext({
|
|
4375
4495
|
...executionContext,
|
|
4376
4496
|
backgroundJob: true,
|
|
4377
4497
|
clientToolExecution: false,
|
|
4498
|
+
workspaceFilesOutOfBand: !!workspaceFiles,
|
|
4378
4499
|
}),
|
|
4500
|
+
workspace_files: workspaceFiles,
|
|
4379
4501
|
stream: false,
|
|
4380
4502
|
model: resolvedModel,
|
|
4381
4503
|
mcp_context_id: executionContext.mcpContextId || undefined,
|
|
@@ -4464,6 +4586,33 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4464
4586
|
}
|
|
4465
4587
|
throw new Error(`Unable to cancel background job: ${errors.join('; ')}`);
|
|
4466
4588
|
}
|
|
4589
|
+
async resolveV3BackgroundApproval(jobId, approvalId, approved, allowMode = 'once') {
|
|
4590
|
+
await this.ensureV3ServiceKey();
|
|
4591
|
+
const errors = [];
|
|
4592
|
+
for (const baseUrl of this.getV3AgentBaseUrls(false)) {
|
|
4593
|
+
try {
|
|
4594
|
+
const response = await fetch(this.getV3AgentApprovalUrl(baseUrl), {
|
|
4595
|
+
method: 'POST',
|
|
4596
|
+
headers: await this.getV3AgentHeaders(),
|
|
4597
|
+
body: JSON.stringify({
|
|
4598
|
+
context_id: jobId,
|
|
4599
|
+
approval_id: approvalId,
|
|
4600
|
+
approved,
|
|
4601
|
+
allow_mode: allowMode,
|
|
4602
|
+
}),
|
|
4603
|
+
});
|
|
4604
|
+
if (!response.ok) {
|
|
4605
|
+
errors.push(`${baseUrl}: ${response.status}`);
|
|
4606
|
+
continue;
|
|
4607
|
+
}
|
|
4608
|
+
return await response.json();
|
|
4609
|
+
}
|
|
4610
|
+
catch (error) {
|
|
4611
|
+
errors.push(`${baseUrl}: ${error?.message || String(error)}`);
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
throw new Error(`Unable to resolve background approval: ${errors.join('; ')}`);
|
|
4615
|
+
}
|
|
4467
4616
|
async getV3BackgroundJobFiles(jobId) {
|
|
4468
4617
|
await this.ensureV3ServiceKey();
|
|
4469
4618
|
const errors = [];
|
|
@@ -4486,7 +4635,47 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4486
4635
|
throw new Error(`Unable to fetch background job files: ${errors.join('; ')}`);
|
|
4487
4636
|
}
|
|
4488
4637
|
async applyV3BackgroundJobFiles(jobId, context = {}) {
|
|
4489
|
-
const data = await
|
|
4638
|
+
const [data, job] = await Promise.all([
|
|
4639
|
+
this.getV3BackgroundJobFiles(jobId),
|
|
4640
|
+
this.getV3BackgroundJob(jobId),
|
|
4641
|
+
]);
|
|
4642
|
+
if (job?.status !== 'completed' || job?.result?.success === false || job?.result?.type === 'error') {
|
|
4643
|
+
throw new Error(`Background job ${jobId} is not a successful completed job and cannot be applied.`);
|
|
4644
|
+
}
|
|
4645
|
+
const mutatedPaths = new Set();
|
|
4646
|
+
const addMutatedPath = (value) => {
|
|
4647
|
+
const normalized = this.normalizeWorkspaceRelativePath(String(value || ''));
|
|
4648
|
+
if (!normalized || path.isAbsolute(normalized) || normalized.split('/').includes('..'))
|
|
4649
|
+
return;
|
|
4650
|
+
mutatedPaths.add(normalized);
|
|
4651
|
+
};
|
|
4652
|
+
for (const filePath of data.confirmed_mutations || []) {
|
|
4653
|
+
addMutatedPath(filePath);
|
|
4654
|
+
}
|
|
4655
|
+
for (const event of Array.isArray(job?.events) ? job.events : []) {
|
|
4656
|
+
if (event?.type === 'file_mutation' && event?.action !== 'delete') {
|
|
4657
|
+
addMutatedPath(event.path);
|
|
4658
|
+
}
|
|
4659
|
+
if (event?.type === 'executor_complete') {
|
|
4660
|
+
let summary = event.summary;
|
|
4661
|
+
if (typeof summary === 'string') {
|
|
4662
|
+
try {
|
|
4663
|
+
summary = JSON.parse(summary);
|
|
4664
|
+
}
|
|
4665
|
+
catch {
|
|
4666
|
+
summary = null;
|
|
4667
|
+
}
|
|
4668
|
+
}
|
|
4669
|
+
if (summary?.status === 'completed') {
|
|
4670
|
+
for (const filePath of [...(summary.changed_files || []), ...(summary.created_files || [])]) {
|
|
4671
|
+
addMutatedPath(filePath);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
if (mutatedPaths.size === 0) {
|
|
4677
|
+
throw new Error(`Background job ${jobId} has no confirmed successful file mutations to apply.`);
|
|
4678
|
+
}
|
|
4490
4679
|
const rootPath = this.resolveAgentTargetPath({
|
|
4491
4680
|
...context,
|
|
4492
4681
|
workspacePath: context.workspacePath || data.local_workspace_path,
|
|
@@ -4499,6 +4688,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
4499
4688
|
const applied = [];
|
|
4500
4689
|
const skipped = [];
|
|
4501
4690
|
for (const [relativePath, content] of Object.entries(data.files || {})) {
|
|
4691
|
+
const normalizedRelativePath = this.normalizeWorkspaceRelativePath(relativePath);
|
|
4692
|
+
if (!mutatedPaths.has(normalizedRelativePath)) {
|
|
4693
|
+
skipped.push(relativePath);
|
|
4694
|
+
continue;
|
|
4695
|
+
}
|
|
4502
4696
|
if (typeof content !== 'string') {
|
|
4503
4697
|
skipped.push(relativePath);
|
|
4504
4698
|
continue;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
export type ClientManifestPayload = {
|
|
2
2
|
product_id: string;
|
|
3
|
+
context_contract_version: string;
|
|
4
|
+
source_product_id: string;
|
|
5
|
+
active_application_id: string;
|
|
6
|
+
active_surface_id: string;
|
|
7
|
+
target_product_id: string;
|
|
8
|
+
workspace_kind: string;
|
|
9
|
+
selected_engine_id: string;
|
|
10
|
+
capability_catalog_version: string;
|
|
11
|
+
capability_scope_id: string;
|
|
3
12
|
version: string;
|
|
4
13
|
os: string;
|
|
5
14
|
execution_mode: string;
|
|
@@ -56,8 +56,18 @@ export function resolveClientOsSlug() {
|
|
|
56
56
|
}
|
|
57
57
|
export function buildClientManifest(overrides = {}) {
|
|
58
58
|
const ios = isIOSEnvironment() && !overrides.os;
|
|
59
|
+
const productId = overrides.product_id || process.env.VIGTHORIA_SOURCE_PRODUCT_ID || 'vigthoria-cli';
|
|
59
60
|
return {
|
|
60
|
-
product_id:
|
|
61
|
+
product_id: productId,
|
|
62
|
+
context_contract_version: overrides.context_contract_version || '2.0',
|
|
63
|
+
source_product_id: overrides.source_product_id || process.env.VIGTHORIA_SOURCE_PRODUCT_ID || productId,
|
|
64
|
+
active_application_id: overrides.active_application_id || process.env.VIGTHORIA_ACTIVE_APPLICATION_ID || 'cli-agent-chat',
|
|
65
|
+
active_surface_id: overrides.active_surface_id || process.env.VIGTHORIA_ACTIVE_SURFACE_ID || 'agent-session',
|
|
66
|
+
target_product_id: overrides.target_product_id || 'none',
|
|
67
|
+
workspace_kind: overrides.workspace_kind || process.env.VIGTHORIA_WORKSPACE_KIND || 'local-development-workspace',
|
|
68
|
+
selected_engine_id: overrides.selected_engine_id || 'none',
|
|
69
|
+
capability_catalog_version: overrides.capability_catalog_version || process.env.VIGTHORIA_CAPABILITY_CATALOG_VERSION || '2026.08.03.1',
|
|
70
|
+
capability_scope_id: overrides.capability_scope_id || process.env.VIGTHORIA_CAPABILITY_SCOPE_ID || 'cli.ecosystem-user',
|
|
61
71
|
version: overrides.version || readPackageVersion(),
|
|
62
72
|
os: overrides.os || resolveClientOsSlug(),
|
|
63
73
|
execution_mode: overrides.execution_mode || (ios ? 'mobile-terminal-sandbox' : 'local-user-machine'),
|
|
@@ -3,6 +3,10 @@ const AGENT_COMMANDS = new Set([
|
|
|
3
3
|
'agent', 'chat', 'chat-resume', 'operator', 'legion', 'workflow',
|
|
4
4
|
'history', 'replay', 'fork', 'cancel',
|
|
5
5
|
]);
|
|
6
|
+
// Subprocess protocols are not user-facing commands. Presenting one in the
|
|
7
|
+
// root picker caused `v4-menu` to be launched without its required output
|
|
8
|
+
// file, which is exactly the failure reported from CLI 1.13.13.
|
|
9
|
+
const INTERNAL_COMMANDS = new Set(['v4-menu']);
|
|
6
10
|
function familyFor(commandPath) {
|
|
7
11
|
const root = commandPath.split(' ')[0];
|
|
8
12
|
if (root === 'repo')
|
|
@@ -15,6 +19,8 @@ export function buildCommandCatalog(program) {
|
|
|
15
19
|
const entries = [];
|
|
16
20
|
const visit = (command, parents) => {
|
|
17
21
|
const commandPath = [...parents, command.name()].join(' ');
|
|
22
|
+
if (parents.length === 0 && INTERNAL_COMMANDS.has(commandPath))
|
|
23
|
+
return;
|
|
18
24
|
entries.push({
|
|
19
25
|
path: commandPath,
|
|
20
26
|
description: command.description() || '',
|
|
@@ -15,7 +15,11 @@ export declare function estimateJsonTokens(value: unknown): number;
|
|
|
15
15
|
export declare function resolveInputBudget(runtimeNctx?: number, maxOutput?: number): number;
|
|
16
16
|
/**
|
|
17
17
|
* Char limit for the initial V3 context JSON payload.
|
|
18
|
-
*
|
|
18
|
+
* The hydration payload is transport-only: the server writes workspaceFiles
|
|
19
|
+
* to an isolated directory and exposes only a compact filename summary to the
|
|
20
|
+
* model. It can therefore use the full calculated input envelope without
|
|
21
|
+
* consuming system/tool framing tokens. Clamp at 180k, far below the gateway
|
|
22
|
+
* body limit, while preserving compaction for genuinely large workspaces.
|
|
19
23
|
*/
|
|
20
24
|
export declare function resolveV3ContextCharLimit(runtimeNctx?: number): number;
|
|
21
25
|
export declare function compactToolListingOutput(name: string, output: string, charCap: number): string;
|
|
@@ -29,13 +29,16 @@ export function resolveInputBudget(runtimeNctx = DEFAULT_CODE_RUNTIME_CTX, maxOu
|
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
31
|
* Char limit for the initial V3 context JSON payload.
|
|
32
|
-
*
|
|
32
|
+
* The hydration payload is transport-only: the server writes workspaceFiles
|
|
33
|
+
* to an isolated directory and exposes only a compact filename summary to the
|
|
34
|
+
* model. It can therefore use the full calculated input envelope without
|
|
35
|
+
* consuming system/tool framing tokens. Clamp at 180k, far below the gateway
|
|
36
|
+
* body limit, while preserving compaction for genuinely large workspaces.
|
|
33
37
|
*/
|
|
34
38
|
export function resolveV3ContextCharLimit(runtimeNctx = DEFAULT_CODE_RUNTIME_CTX) {
|
|
35
39
|
const inputBudget = resolveInputBudget(runtimeNctx);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
return Math.min(100_000, Math.max(32_000, charLimit));
|
|
40
|
+
const hydrationChars = inputBudget * CHARS_PER_TOKEN_EST;
|
|
41
|
+
return Math.min(180_000, Math.max(32_000, hydrationChars));
|
|
39
42
|
}
|
|
40
43
|
export function compactToolListingOutput(name, output, charCap) {
|
|
41
44
|
const text = String(output || '');
|
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const CLI_SHAPING_BLOCK = /(?:^|\n\n)Platform:\s*(?:Windows|macOS|Linux)\.[\s\S]*?(?=\n\n[A-Z][^\n]{0,40}:|\s*$)/i;
|
|
6
6
|
const READONLY_SHAPING_BLOCK = /(?:^|\n\n)(?:Read-only analysis mode is active\.|Diagnostic mode is active\.)[\s\S]*?(?=\n\n[A-Z][^\n]{0,40}:|\s*$)/i;
|
|
7
|
-
const BUILD_VERBS = /\b(build|create|make|implement|complete|fix|repair|edit|modify|write|generate|add|finish|scaffold|develop|update|change|refactor|want|need|require|looking for|let's|let us|we need|we should|erstelle|erstellen|schreib|schreibe|bearbeite)\b/i;
|
|
8
|
-
const ARTIFACT_NOUNS = /\b(file|files|project|game|spiel|app|website|html5|frontend|component|feature|code|workspace|repo|clone|replica|remake|page|site|screen|button|popup|modal|alert|canvas|sprite|level|levels|enemy|enemies|character|npc|world|scene|script|module|api|database|config|landing|dashboard|platformer|arcade|playable|collectible|scoreboard|leaderboard|pitfall|pacman|rogue|html|css|javascript|typescript)\b/i;
|
|
7
|
+
const BUILD_VERBS = /\b(build|create|make|implement|complete|redesign|overhaul|rebuild|transform|polish|art-direct|fix|repair|edit|modify|write|generate|add|finish|scaffold|develop|update|change|refactor|want|need|require|looking for|let's|let us|we need|we should|erstelle|erstellen|schreib|schreibe|bearbeite)\b/i;
|
|
8
|
+
const ARTIFACT_NOUNS = /\b(file|files|project|game|spiel|app|website|html5|frontend|component|feature|code|workspace|repo|clone|replica|remake|page|site|design|showcase|screen|button|popup|modal|alert|canvas|sprite|level|levels|enemy|enemies|character|npc|world|scene|script|module|api|database|config|landing|dashboard|platformer|arcade|playable|collectible|scoreboard|leaderboard|pitfall|pacman|rogue|html|css|javascript|typescript)\b/i;
|
|
9
9
|
/** Canvas/playable games only — bare "html5" or "website" must NOT match. */
|
|
10
10
|
const GAME_INTENT = /\b(game|spiel|playable|babylon(?:\.js|js)?|vge|webgl|3d\s+(?:game|runner|prototype|world|experience)|html5\s+(?:game|canvas)|html5\s+game|canvas\s+game|game\s+canvas|arcade|platformer|side[- ]?scroller|pitfall|pac[- ]?man|tetris|snake|breakout|pong|roguelike|metroidvania|tower\s+defense|clone\s+of\s+(?:a\s+)?(?:game|pitfall|pac|mario|zelda|arcade)|gameplay|collectible|boss\s+fight|level\s+design|sprite\s+sheet|wild\s+(?:boar|pig|forest)|forest\s+pig)\b/i;
|
|
11
11
|
const WEB_PAGE_INTENT = /\b(website|web\s*site|webpage|web\s*page|landing\s+page|home\s*page|index\.html|html\s+page|static\s+page|single\s+page|popup|alert|modal|hello\s+world)\b/i;
|
|
12
|
+
const WEB_SHOWCASE_INTENT = /\b(web[- ]?design\s+showcase|frontend|marketing\s+site|dashboard|saas)\b/i;
|
|
12
13
|
const CLONE_BUILD = /\bclone\s+of\b|\b(build|create|make)\s+(?:a|an|the|us|me)?\s*(?:clone|replica|remake|port)\b/i;
|
|
13
14
|
const READ_ONLY_INTENT = /\b(analy[sz]e|analyse|analysis|audit|review|inspect|proof|understand|summari[sz]e|scan|read[\s-]?only|where we left|what is|how does|show me|tell me|identify\s+gaps?|gap\s+analysis|production\s+blockers?)\b/i;
|
|
14
|
-
const EXPLICIT_WRITE_VERBS = /\b(implement|write|edit|modify|update|fix|repair|create|build|generate|scaffold|deploy|refactor|develop|integrate|correct|apply|patch)\b/i;
|
|
15
|
+
const EXPLICIT_WRITE_VERBS = /\b(implement|write|edit|modify|update|redesign|overhaul|rebuild|transform|polish|art-direct|fix|repair|create|build|generate|scaffold|deploy|refactor|develop|integrate|correct|apply|patch)\b/i;
|
|
15
16
|
const TRIVIAL_HTML_PAGE = /\b(hello\s*world|simple\s+(?:html|web|page|site)|html5\s+website|html\s+5\s+website|popup|alert\s*\(|show\s+a\s+popup|one\s+page|single\s+html)\b/i;
|
|
16
17
|
/** User explicitly grants write permission after an analysis or confirmation gate. */
|
|
17
18
|
const WRITE_PERMISSION_GRANT = /\b(?:hereby\s+confirm|i\s+confirm|confirm(?:ed)?\s+that|give\s+(?:you|permission)|granting\s+permission|allow\s+(?:you|me\s+to)|you\s+(?:may|can|have\s+permission\s+to)|permission\s+to|authorized?\s+to|approve(?:d)?\s+(?:the\s+)?(?:changes?|writes?|edits?)|go\s+ahead\s+and(?:\s+write|\s+apply|\s+make|\s+do)?|do\s+all\s+the\s+changes|apply\s+(?:the\s+)?(?:changes?|fixes?|fix)|proceed\s+with\s+(?:the\s+)?(?:changes?|fixes?|implementation|writes?)|make\s+(?:the\s+)?changes|please\s+write)\b/i;
|
|
@@ -111,7 +112,7 @@ export function isContinueOrRetryPrompt(prompt) {
|
|
|
111
112
|
}
|
|
112
113
|
export function hasWebPageIntent(prompt) {
|
|
113
114
|
const text = stripExecutionShaping(prompt);
|
|
114
|
-
return WEB_PAGE_INTENT.test(text) || /\bhtml5\s+website\b/i.test(text);
|
|
115
|
+
return WEB_PAGE_INTENT.test(text) || WEB_SHOWCASE_INTENT.test(text) || /\bhtml5\s+website\b/i.test(text);
|
|
115
116
|
}
|
|
116
117
|
export function isTrivialHtmlPageRequest(prompt) {
|
|
117
118
|
const text = stripExecutionShaping(prompt);
|
|
@@ -4,7 +4,7 @@ function stripInlineWorkspacePlaceholders(value) {
|
|
|
4
4
|
// Only strip placeholders when they were appended to a segment (e.g. pitfall[Vigthoria service]).
|
|
5
5
|
// Keep canonical scrubbed segments like /[internal]/ intact for dedicated mapping logic.
|
|
6
6
|
return String(value || '')
|
|
7
|
-
.replace(/(
|
|
7
|
+
.replace(/([^/])\[(?:internal|Vigthoria service|Vigthoria Agent|Vigthoria storage|temp|home|workspace)\](?=(?:\/|$))/ig, '$1')
|
|
8
8
|
.replace(/\/+$/g, '');
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
@@ -16,6 +16,9 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
16
16
|
if (!input) {
|
|
17
17
|
return '';
|
|
18
18
|
}
|
|
19
|
+
input = input
|
|
20
|
+
.replace(/^var\/?\[internal\]/i, '[internal]')
|
|
21
|
+
.replace(/^varwww\[internal\]/i, '[internal]');
|
|
19
22
|
input = stripInlineWorkspacePlaceholders(input);
|
|
20
23
|
const safeRelative = (candidate) => {
|
|
21
24
|
const stripped = String(candidate || '').replace(/^\/+/, '');
|
|
@@ -64,6 +67,8 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
64
67
|
}
|
|
65
68
|
const placeholderPatterns = [
|
|
66
69
|
[/^(?:var\/)?www\/\[internal\](?:\/(.*))?$/i, 1],
|
|
70
|
+
[/^var\/?\[internal\](?:\/(.*))?$/i, 1],
|
|
71
|
+
[/^varwww\[internal\](?:\/(.*))?$/i, 1],
|
|
67
72
|
[/^\[internal\](?:\/(.*))?$/i, 1],
|
|
68
73
|
[/^\[Vigthoria service\](?:\/(.*))?$/i, 1],
|
|
69
74
|
[/^\[Vigthoria Agent\](?:\/(.*))?$/i, 1],
|
|
@@ -134,6 +139,9 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
134
139
|
}
|
|
135
140
|
const rootNoLeadingSlash = normalizedRoot.replace(/^\//, '');
|
|
136
141
|
const rootBase = path.posix.basename(normalizedRoot);
|
|
142
|
+
if (input.replace(/\/+$/g, '').toLowerCase() === normalizedRoot.toLowerCase()) {
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
137
145
|
const prefixes = [
|
|
138
146
|
`${normalizedRoot}/`,
|
|
139
147
|
`${rootNoLeadingSlash}/`,
|
|
@@ -150,6 +158,11 @@ export function normalizeV3WorkspaceRelativePath(rawPath, rootPath) {
|
|
|
150
158
|
return safeRelative(input.slice(embeddedIndex + embeddedRoot.length));
|
|
151
159
|
}
|
|
152
160
|
}
|
|
161
|
+
// An unmatched POSIX absolute path belongs to another filesystem boundary.
|
|
162
|
+
// Never reinterpret it as a workspace-relative path by merely trimming '/'.
|
|
163
|
+
if (input.startsWith('/')) {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
153
166
|
const relative = safeRelative(input);
|
|
154
167
|
if (relative === 'workspace') {
|
|
155
168
|
return '';
|
|
@@ -167,7 +180,11 @@ export function isV3WorkspaceRootAliasPath(rawPath, rootPath) {
|
|
|
167
180
|
if (!original || original === '.') {
|
|
168
181
|
return true;
|
|
169
182
|
}
|
|
170
|
-
const
|
|
183
|
+
const compactCanonical = original
|
|
184
|
+
.replace(/\\/g, '/')
|
|
185
|
+
.replace(/^var\/?\[internal\]/i, '[internal]')
|
|
186
|
+
.replace(/^varwww\[internal\]/i, '[internal]');
|
|
187
|
+
const sanitized = stripInlineWorkspacePlaceholders(compactCanonical).trim();
|
|
171
188
|
if (!sanitized || sanitized === '.' || sanitized === '/') {
|
|
172
189
|
return true;
|
|
173
190
|
}
|
|
@@ -181,6 +198,9 @@ export function isV3WorkspaceRootAliasPath(rawPath, rootPath) {
|
|
|
181
198
|
|| lowered === '[internal]'
|
|
182
199
|
|| lowered === 'www/[internal]'
|
|
183
200
|
|| lowered === 'var/www/[internal]'
|
|
201
|
+
|| lowered === 'var[internal]'
|
|
202
|
+
|| lowered === 'var/[internal]'
|
|
203
|
+
|| lowered === 'varwww[internal]'
|
|
184
204
|
|| lowered === '[vigthoria service]'
|
|
185
205
|
|| lowered === 'var/www/[vigthoria service]') {
|
|
186
206
|
return true;
|
package/install.ps1
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
$ErrorActionPreference = "Stop"
|
|
6
6
|
|
|
7
7
|
# Configuration
|
|
8
|
-
$CLI_VERSION = "1.13.
|
|
8
|
+
$CLI_VERSION = "1.13.18"
|
|
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.13.
|
|
29
|
+
CLI_VERSION="1.13.18"
|
|
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.13.
|
|
3
|
+
"version": "1.13.18",
|
|
4
4
|
"description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"test:legion:billing:e2e": "npm run build && node scripts/test-legion-godmode-billing-e2e.js",
|
|
73
73
|
"test:windows:v3-sync": "npm run build && node scripts/test-windows-v3-sync-recovery.js",
|
|
74
74
|
"test:v3-workspace-path": "npm run build && node scripts/test-v3-workspace-path.js",
|
|
75
|
+
"test:v3-server-tool-execution": "npm run build && node scripts/test-v3-server-tool-execution.js",
|
|
75
76
|
"test:session:project-match": "npm run build && node scripts/test-session-project-match.mjs",
|
|
76
77
|
"test:v3-stream-mutation": "npm run build && node scripts/test-v3-stream-mutation.js",
|
|
77
78
|
"test:context:budget": "npm run build && node scripts/test-context-budget.js",
|
|
@@ -5,6 +5,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
5
5
|
cd "$ROOT"
|
|
6
6
|
|
|
7
7
|
CLI="node dist/index.js"
|
|
8
|
+
SERVICE_IDENTITY_MODE="${VIGTHORIA_TEST_SERVICE_IDENTITY:-0}"
|
|
8
9
|
unset V3_SERVICE_KEY
|
|
9
10
|
unset HYPERLOOP_SERVICE_KEY
|
|
10
11
|
# Release validation runs against local model-router in this environment
|
|
@@ -27,6 +28,7 @@ printf '%s
|
|
|
27
28
|
' "$AUTH_JSON" > /tmp/vig-auth-preflight.json
|
|
28
29
|
python3 - << 'PY'
|
|
29
30
|
import json
|
|
31
|
+
import re
|
|
30
32
|
with open('/tmp/vig-auth-preflight.json','r',encoding='utf-8') as f:
|
|
31
33
|
j=json.load(f)
|
|
32
34
|
msg='Vigthoria Gate way user authentification failed. Please log out and login again.'
|
|
@@ -35,6 +37,15 @@ assert j.get('error') == msg, f"unexpected auth message: {j.get('error')}"
|
|
|
35
37
|
print('[pass] auth preflight message')
|
|
36
38
|
PY
|
|
37
39
|
|
|
40
|
+
if [[ "$SERVICE_IDENTITY_MODE" == "1" ]]; then
|
|
41
|
+
# From this point onward the no-go suite exercises server-local model and
|
|
42
|
+
# tool routes without borrowing a human account session. The invalid-token
|
|
43
|
+
# gateway assertion above always runs against the canonical public boundary.
|
|
44
|
+
export VIGTHORIA_LOCAL_TEST_MODE=1
|
|
45
|
+
export VIGTHORIA_LOCAL_TEST_TOKEN="vigthoria-release-service-identity"
|
|
46
|
+
export VIGTHORIA_AUTH_TOKEN="$VIGTHORIA_LOCAL_TEST_TOKEN"
|
|
47
|
+
fi
|
|
48
|
+
|
|
38
49
|
echo "[1.1] ranker includes agent.py top10 for SSE prompt"
|
|
39
50
|
node - << 'EOF2'
|
|
40
51
|
import('./dist/utils/context-ranker.js').then((m)=>{
|
|
@@ -62,7 +73,17 @@ if (!tsc || !tsc.ran || !tsc.passed) process.exit(1);
|
|
|
62
73
|
EOF2
|
|
63
74
|
|
|
64
75
|
echo "[6.2] balanced-4b no-agent path"
|
|
65
|
-
|
|
76
|
+
for attempt in 1 2 3; do
|
|
77
|
+
if $CLI chat --no-agent --model balanced-4b --prompt "reply exactly: ok" --json >/tmp/vig-balanced.json; then
|
|
78
|
+
break
|
|
79
|
+
fi
|
|
80
|
+
if [[ "$attempt" == "3" ]]; then
|
|
81
|
+
echo "[fail] balanced-4b remained unavailable after 3 attempts"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
echo "[retry] balanced-4b transient failure ($attempt/3)"
|
|
85
|
+
sleep 2
|
|
86
|
+
done
|
|
66
87
|
python3 - << 'PY'
|
|
67
88
|
import json
|
|
68
89
|
j=json.load(open('/tmp/vig-balanced.json'))
|
|
@@ -76,13 +97,17 @@ $CLI hyper-loop status >/tmp/vig-hyperloop.txt
|
|
|
76
97
|
$CLI devtools connect >/tmp/vig-devtools.txt
|
|
77
98
|
|
|
78
99
|
echo "[6.11] operator flow"
|
|
79
|
-
|
|
80
|
-
|
|
100
|
+
if [[ "$SERVICE_IDENTITY_MODE" == "1" ]]; then
|
|
101
|
+
echo "[skip] paid-user operator probe (isolated release service identity)"
|
|
102
|
+
else
|
|
103
|
+
$CLI operator --prompt "reply with exactly: OK" --json >/tmp/vig-operator.json
|
|
104
|
+
python3 - << 'PY'
|
|
81
105
|
import json
|
|
82
106
|
j=json.load(open('/tmp/vig-operator.json'))
|
|
83
107
|
assert j.get('success') is True, j
|
|
84
108
|
print('[pass] operator flow')
|
|
85
109
|
PY
|
|
110
|
+
fi
|
|
86
111
|
|
|
87
112
|
echo "[7.1/7.3/7.4] local service health"
|
|
88
113
|
for u in http://localhost:8030/health http://localhost:4009/health http://localhost:4011/health; do
|
|
@@ -95,6 +120,7 @@ echo "[7.2/7.8/12.3] models inventory"
|
|
|
95
120
|
curl -s http://localhost:4009/v1/models >/tmp/vig-models.json
|
|
96
121
|
python3 - << 'PY'
|
|
97
122
|
import json
|
|
123
|
+
import re
|
|
98
124
|
ids={m.get('id','') for m in json.load(open('/tmp/vig-models.json')).get('data',[])}
|
|
99
125
|
if ids:
|
|
100
126
|
has_router = any(
|
|
@@ -110,7 +136,7 @@ if ids:
|
|
|
110
136
|
'vigthoria-fast-9b', 'vigthoria-fast-9b:latest',
|
|
111
137
|
)
|
|
112
138
|
)
|
|
113
|
-
has_code = any('vigthoria-v3
|
|
139
|
+
has_code = any(re.search(r'^vigthoria-v3(?:\.\d+)?-code-35b(?:-|:|$)', i) for i in ids)
|
|
114
140
|
assert has_router, f'missing router model in inventory: {sorted(ids)}'
|
|
115
141
|
assert has_creative, f'missing creative/fast model in inventory: {sorted(ids)}'
|
|
116
142
|
assert has_code, f'missing code model in inventory: {sorted(ids)}'
|
|
@@ -136,7 +162,17 @@ rg -n "coder.vigthoria.io/releases" install.ps1 install.sh README.md >/tmp/vig-r
|
|
|
136
162
|
cat /tmp/vig-release-urls.txt
|
|
137
163
|
|
|
138
164
|
echo "[12.1] no-agent governance fallback"
|
|
139
|
-
|
|
165
|
+
for attempt in 1 2 3; do
|
|
166
|
+
if $CLI chat --no-agent --model creative --prompt "say ok" --json >/tmp/vig-creative.json; then
|
|
167
|
+
break
|
|
168
|
+
fi
|
|
169
|
+
if [[ "$attempt" == "3" ]]; then
|
|
170
|
+
echo "[fail] governance fallback remained unavailable after 3 attempts"
|
|
171
|
+
exit 1
|
|
172
|
+
fi
|
|
173
|
+
echo "[retry] governance fallback transient failure ($attempt/3)"
|
|
174
|
+
sleep 2
|
|
175
|
+
done
|
|
140
176
|
python3 - << 'PY'
|
|
141
177
|
import json
|
|
142
178
|
j=json.load(open('/tmp/vig-creative.json'))
|