vigthoria-cli 1.13.13 → 1.13.20

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.
@@ -11,9 +11,13 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
11
11
  const KNOWN_AUTH_BASE_URLS = ['https://coder.vigthoria.io'];
12
12
  class HttpError extends Error {
13
13
  status;
14
- constructor(status, message) {
14
+ code;
15
+ upgradeUrl;
16
+ constructor(status, message, code, upgradeUrl) {
15
17
  super(message);
16
18
  this.status = status;
19
+ this.code = code;
20
+ this.upgradeUrl = upgradeUrl;
17
21
  }
18
22
  }
19
23
  function trimTrailingSlash(value) {
@@ -206,12 +210,19 @@ async function requestJson(url, init) {
206
210
  }
207
211
  }
208
212
  if (!response.ok) {
209
- const message = typeof body === 'object' && body && 'error' in body
210
- ? String(body.error)
211
- : typeof body === 'object' && body && 'message' in body
212
- ? String(body.message)
213
- : `Request failed with status ${response.status}`;
214
- throw new HttpError(response.status, message);
213
+ const errorBody = typeof body === 'object' && body ? body : {};
214
+ const code = String(errorBody.code || '');
215
+ const planNames = Array.isArray(errorBody.requiredPlans)
216
+ ? errorBody.requiredPlans.map(plan => plan.name || plan.id).filter(Boolean)
217
+ : [];
218
+ const message = code === 'SUBSCRIPTION_REQUIRED'
219
+ ? `This project requires a paid Vigthoria subscription.${planNames.length ? ` Eligible plans: ${planNames.join(', ')}.` : ''} Upgrade: ${errorBody.upgradeUrl || 'https://hub.vigthoria.io/subscriptions?application=coder'}`
220
+ : 'error' in errorBody && errorBody.error
221
+ ? String(errorBody.error)
222
+ : 'message' in errorBody && errorBody.message
223
+ ? String(errorBody.message)
224
+ : `Request failed with status ${response.status}`;
225
+ throw new HttpError(response.status, message, code || undefined, errorBody.upgradeUrl);
215
226
  }
216
227
  return body;
217
228
  }
@@ -316,6 +327,9 @@ export async function login(email, password) {
316
327
  return config;
317
328
  }
318
329
  catch (error) {
330
+ if (error instanceof HttpError && error.code === 'SUBSCRIPTION_REQUIRED') {
331
+ throw error;
332
+ }
319
333
  const message = humanMessage(error);
320
334
  failures.push(`${endpoint} -> ${message}`);
321
335
  }
@@ -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
  }
@@ -272,6 +272,8 @@ export declare class ChatCommand {
272
272
  private truncateText;
273
273
  private getLastUserPrompt;
274
274
  private saveSession;
275
+ private persistAgentExecutionCheckpoint;
276
+ private updateAgentExecutionCheckpoint;
275
277
  private requestPermission;
276
278
  getCurrentSessionInfo(): string;
277
279
  getChatHistory(): ChatMessage[];
@@ -3,6 +3,7 @@ import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
5
  import * as readline from 'readline';
6
+ import { randomUUID } from 'crypto';
6
7
  import { createSpinner } from '../utils/logger.js';
7
8
  import { APIClient, CLIError, classifyError, formatCLIError, sanitizeUserFacingErrorText, sanitizeUserFacingPathText, propagateError, VIGTHORIA_SERVER_TEMPORARILY_UNAVAILABLE_MESSAGE } from '../utils/api.js';
8
9
  import { AgenticTools, robustifyStreamResponse } from '../utils/tools.js';
@@ -1179,6 +1180,32 @@ export class ChatCommand {
1179
1180
  spinner.text = this.sanitizeServerPath(String(event.content || 'Routing to V3 Agent...'));
1180
1181
  return;
1181
1182
  }
1183
+ if (event.type === 'pipeline_stage') {
1184
+ if (spinner.isSpinning)
1185
+ spinner.stop();
1186
+ const stageIndex = Number.isFinite(Number(event.stage_index)) ? Number(event.stage_index) : 0;
1187
+ const stageTotal = Number.isFinite(Number(event.stage_total)) ? Number(event.stage_total) : 3;
1188
+ const stage = String(event.stage || '').trim().toLowerCase();
1189
+ const label = stage === 'dispatcher'
1190
+ ? 'Dispatcher'
1191
+ : stage === 'architect'
1192
+ ? 'Architect'
1193
+ : stage === 'executor'
1194
+ ? 'Executor'
1195
+ : 'Pipeline';
1196
+ const model = this.sanitizeServerPath(String(event.model || event.executor_model || ''));
1197
+ const route = this.sanitizeServerPath(String(event.route || ''));
1198
+ const details = [model, route ? `route=${route}` : ''].filter(Boolean).join(' · ');
1199
+ process.stderr.write(chalk.cyan(` [${stageIndex || '?'}\/${stageTotal} ${label}] `)
1200
+ + `${details || 'ready'}\n`);
1201
+ spinner.start();
1202
+ spinner.text = stage === 'dispatcher'
1203
+ ? 'Olivia routed the request. Starting Architect...'
1204
+ : stage === 'architect'
1205
+ ? 'Architect is producing the execution plan...'
1206
+ : '35B Executor is executing the approved plan...';
1207
+ return;
1208
+ }
1182
1209
  if (event.type === 'thinking') {
1183
1210
  this.v3IterationCount += 1;
1184
1211
  const iterText = this.sanitizeServerPath(event.content || '');
@@ -1481,7 +1508,8 @@ export class ChatCommand {
1481
1508
  this.sessionManager = new SessionManager();
1482
1509
  }
1483
1510
  async run(options) {
1484
- if (!this.config.isAuthenticated()) {
1511
+ const hasRuntimeToken = Boolean(process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN);
1512
+ if (!this.config.isAuthenticated() && !hasRuntimeToken) {
1485
1513
  if (options.json) {
1486
1514
  process.exitCode = 1;
1487
1515
  emitAgentJsonOutput({ success: false, error: 'Not authenticated. Run: vigthoria login' });
@@ -1811,6 +1839,35 @@ export class ChatCommand {
1811
1839
  originalPrompt: saved.originalPrompt ?? null,
1812
1840
  };
1813
1841
  }
1842
+ else if (this.currentSession.activeAgentRun?.prompt) {
1843
+ const checkpoint = this.currentSession.activeAgentRun;
1844
+ this.lastAgentRunOutcome = {
1845
+ prompt: checkpoint.prompt,
1846
+ originalPrompt: checkpoint.originalPrompt || checkpoint.prompt,
1847
+ taskId: checkpoint.executionId,
1848
+ contextId: checkpoint.contextId,
1849
+ tasksSucceeded: 0,
1850
+ tasksTotal: 0,
1851
+ failedTaskIds: [...(checkpoint.failedTaskIds || [])],
1852
+ unfinishedTaskIds: checkpoint.unfinishedTaskIds?.length
1853
+ ? [...checkpoint.unfinishedTaskIds]
1854
+ : ['resume-interrupted-agent-run'],
1855
+ qualityScore: null,
1856
+ qualityMissing: [],
1857
+ qualityBlockers: checkpoint.error ? [checkpoint.error] : [],
1858
+ hasOutput: false,
1859
+ answerContent: null,
1860
+ selfHealStatus: 'skipped',
1861
+ selfHealTool: null,
1862
+ plannerError: null,
1863
+ executorError: checkpoint.error || 'The previous client process ended before the agent run completed.',
1864
+ clientToolErrors: [],
1865
+ transportErrors: [],
1866
+ workspacePath: checkpoint.workspacePath || this.currentProjectPath,
1867
+ workspaceSyncIssue: null,
1868
+ finishedAt: Date.parse(checkpoint.updatedAt) || Date.now(),
1869
+ };
1870
+ }
1814
1871
  return;
1815
1872
  }
1816
1873
  }
@@ -2337,6 +2394,12 @@ export class ChatCommand {
2337
2394
  : '';
2338
2395
  this.logger.debug(`Agent route: ${r.path} / ${r.taskKind} [${r.source}]${routerNote} — ${r.reason}`);
2339
2396
  }
2397
+ // V3 owns high-fidelity HTML and other complex single-file rewrites, but
2398
+ // the CLI still has the authoritative local workspace. Prime the target
2399
+ // through the client-tool bridge before handing the task to V3 so the
2400
+ // model receives the real current file and the terminal records a
2401
+ // verifiable read-before-write operation.
2402
+ await this.primeBypassedTargetFileContext(resolvedPrompt);
2340
2403
  const handledByV3Workflow = await this.tryV3AgentWorkflow(resolvedPrompt);
2341
2404
  if (handledByV3Workflow) {
2342
2405
  this.saveSession();
@@ -3160,12 +3223,33 @@ export class ChatCommand {
3160
3223
  ...(resumeTaskIds.length > 0 ? { remaining_task_ids: resumeTaskIds } : {}),
3161
3224
  }
3162
3225
  : executionHints;
3226
+ const previousExecutionId = this.currentSession?.activeAgentRun?.executionId || null;
3227
+ const executionId = `cli-${Date.now()}-${randomUUID().slice(0, 8)}`;
3163
3228
  const workspaceContext = {
3164
3229
  workspacePath: workspacePath,
3165
3230
  projectPath: workspacePath,
3166
3231
  targetPath: workspacePath,
3232
+ contextId: executionId,
3233
+ executionId,
3167
3234
  ...runtimeContext,
3168
3235
  };
3236
+ this.persistAgentExecutionCheckpoint({
3237
+ executionId,
3238
+ parentExecutionId: shouldResumePlan ? previousExecutionId : null,
3239
+ contextId: executionId,
3240
+ prompt,
3241
+ originalPrompt: priorOutcome?.originalPrompt || priorOutcome?.prompt || prompt,
3242
+ workspacePath,
3243
+ userTier: String(this.config.get('subscription')?.plan || '').trim() || null,
3244
+ taskType: agentTaskType,
3245
+ workflowType,
3246
+ status: 'submitted',
3247
+ submittedAt: new Date().toISOString(),
3248
+ updatedAt: new Date().toISOString(),
3249
+ failedTaskIds: resumeTaskIds,
3250
+ unfinishedTaskIds: resumeTaskIds.length ? resumeTaskIds : ['agent-run-in-progress'],
3251
+ error: null,
3252
+ });
3169
3253
  // Start workspace watcher for bidirectional real-time sync
3170
3254
  let watcher = null;
3171
3255
  if (this.shouldStartWorkspaceWatcher(workspacePath)) {
@@ -3209,6 +3293,7 @@ export class ChatCommand {
3209
3293
  requestedModel: this.currentModel,
3210
3294
  agentExecutionPolicy: routingPolicy,
3211
3295
  legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
3296
+ approvalLevel: this.autoApprove ? 'auto' : 'confirm',
3212
3297
  rawPrompt: prompt,
3213
3298
  contextualPrompt,
3214
3299
  history: this.getMessagesForModel(),
@@ -3218,6 +3303,7 @@ export class ChatCommand {
3218
3303
  return;
3219
3304
  }
3220
3305
  if (event.type === 'plan') {
3306
+ this.updateAgentExecutionCheckpoint({ status: 'running' });
3221
3307
  taskDisplay.complete(0);
3222
3308
  const tasks = event?.plan?.tasks;
3223
3309
  if (Array.isArray(tasks) && tasks.length > 0) {
@@ -3518,6 +3604,15 @@ export class ChatCommand {
3518
3604
  : null,
3519
3605
  finishedAt: Date.now(),
3520
3606
  }, prompt);
3607
+ this.updateAgentExecutionCheckpoint({
3608
+ status: executorSucceeded ? 'completed' : 'failed',
3609
+ contextId: response.contextId || executionId,
3610
+ failedTaskIds: [...liveOutcome.failedTaskIds],
3611
+ unfinishedTaskIds: [...liveOutcome.unfinishedTaskIds],
3612
+ error: executorSucceeded
3613
+ ? null
3614
+ : (liveOutcome.executorError || liveOutcome.plannerError || runEvaluation.statusHeadline),
3615
+ });
3521
3616
  if (!this.jsonOutput && !this.directPromptMode) {
3522
3617
  if (this.lastAgentRunOutcome) {
3523
3618
  this.printAgentRunSummary(this.lastAgentRunOutcome, runEvaluation, changedFileCount);
@@ -3619,6 +3714,12 @@ export class ChatCommand {
3619
3714
  workspaceSyncIssue: null,
3620
3715
  finishedAt: Date.now(),
3621
3716
  }, prompt);
3717
+ this.updateAgentExecutionCheckpoint({
3718
+ status: 'failed',
3719
+ failedTaskIds: [...liveOutcome.failedTaskIds],
3720
+ unfinishedTaskIds: [...liveOutcome.unfinishedTaskIds],
3721
+ error: liveOutcome.executorError || safeDetail || 'Connection aborted',
3722
+ });
3622
3723
  if (!this.jsonOutput) {
3623
3724
  process.exitCode = 1;
3624
3725
  }
@@ -3688,6 +3789,7 @@ export class ChatCommand {
3688
3789
  requestedModel: this.currentModel,
3689
3790
  agentExecutionPolicy: routingPolicy,
3690
3791
  legacyFallbackAllowed: this.isLegacyAgentFallbackAllowed(),
3792
+ approvalLevel: this.autoApprove ? 'auto' : 'confirm',
3691
3793
  rawPrompt,
3692
3794
  history: this.getMessagesForModel(),
3693
3795
  onStreamEvent: spinner ? (event) => this.updateV3AgentSpinner(spinner, event) : undefined,
@@ -5021,6 +5123,24 @@ export class ChatCommand {
5021
5123
  sessionSummary: this.currentSession.memorySummary || '',
5022
5124
  });
5023
5125
  }
5126
+ persistAgentExecutionCheckpoint(checkpoint) {
5127
+ if (!this.currentSession) {
5128
+ this.currentSession = this.sessionManager.create(this.currentProjectPath, this.currentModel, this.agentMode, this.operatorMode);
5129
+ }
5130
+ this.currentSession.activeAgentRun = checkpoint;
5131
+ this.saveSession();
5132
+ }
5133
+ updateAgentExecutionCheckpoint(update) {
5134
+ if (!this.currentSession?.activeAgentRun) {
5135
+ return;
5136
+ }
5137
+ this.currentSession.activeAgentRun = {
5138
+ ...this.currentSession.activeAgentRun,
5139
+ ...update,
5140
+ updatedAt: new Date().toISOString(),
5141
+ };
5142
+ this.saveSession();
5143
+ }
5024
5144
  async requestPermission(action) {
5025
5145
  if (this.autoApprove) {
5026
5146
  return true;
@@ -40,7 +40,7 @@ export class ConfigCommand {
40
40
  console.log();
41
41
  const cwd = process.cwd();
42
42
  const configFile = path.join(cwd, '.vigthoria.json');
43
- const allowedModels = new Set(['code', 'code-35b', 'code-9b', 'balanced', 'balanced-4b']);
43
+ const allowedModels = new Set(['architect', 'code', 'assistant', 'code-35b', 'code-9b', 'balanced', 'balanced-4b']);
44
44
  const hasOverrides = Boolean(options.model ||
45
45
  options.ignorePatterns !== undefined ||
46
46
  options.autoApplyFixes !== undefined ||
@@ -71,16 +71,16 @@ export class ConfigCommand {
71
71
  let settings;
72
72
  if (nonInteractive) {
73
73
  const profileDefaults = {
74
- safe: { defaultModel: 'balanced', autoApplyFixes: false },
74
+ safe: { defaultModel: 'assistant', autoApplyFixes: false },
75
75
  balanced: { defaultModel: 'code', autoApplyFixes: false },
76
- fast: { defaultModel: 'code-9b', autoApplyFixes: true },
76
+ fast: { defaultModel: 'assistant', autoApplyFixes: true },
77
77
  };
78
78
  const profile = options.profile && profileDefaults[options.profile] ? options.profile : 'balanced';
79
79
  const defaults = profileDefaults[profile];
80
80
  const model = options.model || defaults.defaultModel;
81
81
  if (!allowedModels.has(model)) {
82
82
  this.logger.error(`Invalid --model: ${model}`);
83
- this.logger.info('Allowed values: code, code-35b, code-9b, balanced, balanced-4b');
83
+ this.logger.info('Allowed values: architect, code, assistant, code-35b, code-9b, balanced, balanced-4b');
84
84
  return;
85
85
  }
86
86
  settings = {
@@ -98,12 +98,9 @@ export class ConfigCommand {
98
98
  message: 'Default AI model:',
99
99
  choices: [
100
100
  { name: '═══ Code Models ═══', disabled: true },
101
- { name: 'Vigthoria v3 Code 35B - Flagship coding model', value: 'code' },
102
- { name: 'Vigthoria v3 Code 35B - Explicit 35B selection', value: 'code-35b' },
103
- { name: 'Vigthoria v3 Code 9B - Fast coding specialist', value: 'code-9b' },
104
- { name: '═══ General Models ═══', disabled: true },
105
- { name: 'Vigthoria Master 7.6B - Balanced general model', value: 'balanced' },
106
- { name: 'Vigthoria v3 Balanced 4B - Efficient general purpose', value: 'balanced-4b' },
101
+ { name: 'Vigthoria v4 Creative 27B - Architect and planning', value: 'architect' },
102
+ { name: 'Vigthoria v4 Code 27B - Production executor', value: 'code' },
103
+ { name: 'Vigthoria v4 Assistant 9B - Fast FIM and diagnostics', value: 'assistant' },
107
104
  ],
108
105
  default: 'code',
109
106
  },
@@ -294,10 +291,9 @@ export class ConfigCommand {
294
291
  name: 'defaultModel',
295
292
  message: 'Default AI model:',
296
293
  choices: [
297
- { name: 'Vigthoria v3 Code 35B', value: 'code' },
298
- { name: 'Vigthoria v3 Code 9B', value: 'code-9b' },
299
- { name: 'Vigthoria Master 7.6B', value: 'balanced' },
300
- { name: 'Vigthoria v3 Balanced 4B', value: 'balanced-4b' },
294
+ { name: 'Vigthoria v4 Creative 27B — Architect', value: 'architect' },
295
+ { name: 'Vigthoria v4 Code 27B — Executor', value: 'code' },
296
+ { name: 'Vigthoria v4 Assistant 9B — FIM and diagnostics', value: 'assistant' },
301
297
  ],
302
298
  default: current.preferences.defaultModel,
303
299
  },
@@ -48,19 +48,31 @@ export class LegionCommand {
48
48
  }
49
49
  getHyperloopUrls() {
50
50
  const urls = new Set();
51
- const configuredApiUrl = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
52
- if (configuredApiUrl) {
53
- urls.add(`${configuredApiUrl}/api/hyperloop`);
54
- }
55
- const envUrl = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
56
- if (envUrl) {
57
- urls.add(envUrl);
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
- urls.add(internal.replace(/\/$/, ''));
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(10000),
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
- lastError = `Legion status check at ${baseUrl} failed: ${response.status} ${describeUpstreamStatus(response.status)}`;
1503
- continue;
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)));