vigthoria-cli 1.8.19 → 1.9.2

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.
@@ -9,12 +9,48 @@
9
9
  * vigthoria legion --workers List available Legion workers
10
10
  * vigthoria legion --status Show Legion infrastructure status
11
11
  */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
12
45
  var __importDefault = (this && this.__importDefault) || function (mod) {
13
46
  return (mod && mod.__esModule) ? mod : { "default": mod };
14
47
  };
15
48
  Object.defineProperty(exports, "__esModule", { value: true });
16
49
  exports.LegionCommand = void 0;
17
50
  const chalk_1 = __importDefault(require("chalk"));
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const readline = __importStar(require("readline/promises"));
18
54
  const logger_js_1 = require("../utils/logger.js");
19
55
  const api_js_1 = require("../utils/api.js");
20
56
  // Hyper Loop / Legion runs on the Vigthoria backend only. Local user installs
@@ -42,8 +78,30 @@ class LegionCommand {
42
78
  this.config = config;
43
79
  this.logger = logger;
44
80
  }
81
+ getHyperloopUrls() {
82
+ const urls = new Set();
83
+ const configuredApiUrl = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
84
+ if (configuredApiUrl) {
85
+ urls.add(`${configuredApiUrl}/api/hyperloop`);
86
+ }
87
+ const envUrl = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
88
+ if (envUrl) {
89
+ urls.add(envUrl);
90
+ }
91
+ if ((0, api_js_1.isServerRuntime)()) {
92
+ for (const internal of buildServerHyperloopUrls()) {
93
+ urls.add(internal.replace(/\/$/, ''));
94
+ }
95
+ }
96
+ return Array.from(urls);
97
+ }
45
98
  getHeaders() {
46
99
  const headers = { 'Content-Type': 'application/json' };
100
+ const serviceKey = this.getLegionServiceKey();
101
+ if (serviceKey) {
102
+ headers['X-Service-Key'] = serviceKey;
103
+ return headers;
104
+ }
47
105
  const token = this.config.get('authToken');
48
106
  if (token) {
49
107
  headers['Authorization'] = `Bearer ${token}`;
@@ -51,7 +109,46 @@ class LegionCommand {
51
109
  }
52
110
  return headers;
53
111
  }
112
+ async readJsonResponse(response, context) {
113
+ try {
114
+ return await response.json();
115
+ }
116
+ catch (err) {
117
+ (0, api_js_1.propagateError)({
118
+ ...((err && typeof err === 'object') ? err : { message: String(err) }),
119
+ message: `${context} returned invalid JSON: ${err?.message || String(err)}`,
120
+ statusCode: response.status,
121
+ commandName: 'legion',
122
+ endpoint: response.url || context,
123
+ details: {
124
+ ...((err?.details && typeof err.details === 'object') ? err.details : {}),
125
+ command: 'legion',
126
+ endpoint: response.url || context,
127
+ context,
128
+ status: response.status,
129
+ },
130
+ });
131
+ }
132
+ }
133
+ propagateLegionApiError(context, endpoint, err) {
134
+ const original = err && typeof err === 'object' ? err : { message: String(err) };
135
+ (0, api_js_1.propagateError)({
136
+ ...original,
137
+ commandName: 'legion',
138
+ endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || endpoint || context,
139
+ details: {
140
+ ...(original.details && typeof original.details === 'object' ? original.details : {}),
141
+ command: 'legion',
142
+ endpoint: original.endpoint || original?.config?.url || original?.details?.endpoint || endpoint || context,
143
+ context,
144
+ },
145
+ });
146
+ }
54
147
  async run(request, options) {
148
+ if (options.godmode) {
149
+ await this.runGodmode(request, options);
150
+ return;
151
+ }
55
152
  if (options.workers) {
56
153
  await this.showWorkers();
57
154
  return;
@@ -64,99 +161,922 @@ class LegionCommand {
64
161
  console.log(chalk_1.default.yellow('Usage: vigthoria legion "<task description>"'));
65
162
  console.log(chalk_1.default.gray(' --workers List available Legion workers'));
66
163
  console.log(chalk_1.default.gray(' --status Show Legion infrastructure status'));
164
+ console.log(chalk_1.default.gray(' --godmode Run calculator + high-intelligence orchestration'));
67
165
  return;
68
166
  }
69
167
  await this.planAndExecute(request, options);
70
168
  }
71
- async planAndExecute(request, options) {
72
- const spinner = (0, logger_js_1.createSpinner)('Submitting to Legion parallel orchestrator...').start();
73
- const body = {
169
+ async runGodmode(request, options) {
170
+ if (!request) {
171
+ console.log(chalk_1.default.yellow('Usage: vigthoria legion --godmode "<task description>"'));
172
+ console.log(chalk_1.default.gray(' --plan-only Run calculator only (no execution)'));
173
+ console.log(chalk_1.default.gray(' --approve Skip confirmation prompt and execute'));
174
+ console.log(chalk_1.default.gray(' --auto-charge Attempt direct VigCoin top-up when balance is low'));
175
+ console.log(chalk_1.default.gray(' --timeout <sec> Abort remote execution if no result within timeout (default: 120)'));
176
+ return;
177
+ }
178
+ const workspace = options.project || process.cwd();
179
+ const scan = this.scanProject(workspace);
180
+ const selectedModels = this.resolveModelProfiles(options.models);
181
+ const quote = this.buildRoleQuote(scan, selectedModels);
182
+ const billingQuote = this.buildBillingQuote(quote);
183
+ let billingGate = await this.evaluateBillingGate(billingQuote);
184
+ this.printGodmodeQuote(workspace, scan, quote, billingQuote, billingGate);
185
+ if (options.planOnly) {
186
+ console.log(chalk_1.default.green('Godmode calculator complete (plan-only).'));
187
+ return;
188
+ }
189
+ if (!billingGate.canProceed) {
190
+ const resolved = await this.resolveBillingInsufficientFunds(billingQuote, billingGate, options);
191
+ if (!resolved) {
192
+ console.log(chalk_1.default.yellow('Godmode cancelled due to insufficient VigCoin balance.'));
193
+ return;
194
+ }
195
+ billingGate = await this.evaluateBillingGate(billingQuote);
196
+ if (!billingGate.canProceed) {
197
+ this.printBillingGateSummary(billingQuote, billingGate);
198
+ console.log(chalk_1.default.red('Billing gate still blocked after charge attempt.'));
199
+ return;
200
+ }
201
+ }
202
+ const autoApprove = options.approve === true && options.noApprove !== true;
203
+ const approved = autoApprove ? true : await this.confirmExecution();
204
+ if (!approved) {
205
+ console.log(chalk_1.default.yellow('Godmode cancelled by user.'));
206
+ return;
207
+ }
208
+ const charged = await this.collectExecutionCharge(billingQuote, billingGate);
209
+ if (!charged) {
210
+ console.log(chalk_1.default.yellow('Godmode cancelled because wallet charge was not completed.'));
211
+ return;
212
+ }
213
+ const enrichedRequest = [
214
+ '[GODMODE EXECUTION]',
74
215
  request,
75
- context: {
76
- workspace: options.project || process.cwd(),
216
+ `Workspace: ${workspace}`,
217
+ `PhaseA: files=${scan.files}, lines=${scan.lines}, import_edges=${scan.importEdges}`,
218
+ 'Role model assignment:',
219
+ ...quote.map((q) => `- ${q.role}: ${q.model} (requested_model=${q.requestedModel})`),
220
+ `Billing: plan=${billingGate.plan}, master_admin_free=${billingGate.masterAdminFree ? 'true' : 'false'}, vigcoin_required=${billingQuote.vigcoinRequired.toFixed(3)}, vigcoin_rate_usd=${billingQuote.vigcoinRateUsd.toFixed(4)}, total_usd=${billingQuote.finalUsd.toFixed(4)}`,
221
+ 'Required flow: Detective repro -> 6-role parallel attack -> Architect synthesis -> test loop until pass.',
222
+ ].join('\n');
223
+ await this.planAndExecute(enrichedRequest, options, {
224
+ workspace,
225
+ originalRequest: request,
226
+ scan,
227
+ quote,
228
+ });
229
+ }
230
+ scanProject(workspace) {
231
+ const files = [];
232
+ const stack = [workspace];
233
+ const ignored = new Set(['.git', 'node_modules', 'dist', 'build', 'coverage', '.next', '__pycache__', '.venv']);
234
+ const exts = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.py', '.go', '.rs', '.java']);
235
+ while (stack.length > 0 && files.length < 4000) {
236
+ const dir = stack.pop();
237
+ if (!dir)
238
+ continue;
239
+ let entries = [];
240
+ try {
241
+ entries = fs.readdirSync(dir, { withFileTypes: true });
242
+ }
243
+ catch {
244
+ continue;
245
+ }
246
+ for (const e of entries) {
247
+ if (ignored.has(e.name))
248
+ continue;
249
+ const full = path.join(dir, e.name);
250
+ if (e.isDirectory()) {
251
+ stack.push(full);
252
+ }
253
+ else if (e.isFile() && exts.has(path.extname(e.name).toLowerCase())) {
254
+ files.push(full);
255
+ }
256
+ }
257
+ }
258
+ let lines = 0;
259
+ let importEdges = 0;
260
+ const topFiles = [];
261
+ for (const file of files) {
262
+ try {
263
+ const content = fs.readFileSync(file, 'utf-8');
264
+ const fileLines = content.split('\n').length;
265
+ const imports = (content.match(/^\s*(import\s+.+from\s+['"].+['"]|from\s+.+\s+import\s+.+|require\(.+\))/gm) || []).length;
266
+ lines += fileLines;
267
+ importEdges += imports;
268
+ topFiles.push({ file: path.relative(workspace, file).replace(/\\/g, '/'), lines: fileLines, imports });
269
+ }
270
+ catch {
271
+ continue;
272
+ }
273
+ }
274
+ topFiles.sort((a, b) => (b.lines + b.imports * 25) - (a.lines + a.imports * 25));
275
+ return { files: files.length, lines, importEdges, topFiles: topFiles.slice(0, 8) };
276
+ }
277
+ resolveModelProfiles(modelsArg) {
278
+ const catalog = [
279
+ { id: 'openrouter:openai/gpt-5.5', requestedModel: 'gpt-5.5', provider: 'openrouter', estInputPer1M: 5.0, estOutputPer1M: 30.0, capability: { reasoning: 10, coding: 10, security: 9, speed: 6, synthesis: 10 } },
280
+ { id: 'openrouter:anthropic/claude-opus-4.7', requestedModel: 'opus-4.7', provider: 'openrouter', estInputPer1M: 5.0, estOutputPer1M: 25.0, capability: { reasoning: 10, coding: 9, security: 10, speed: 6, synthesis: 10 } },
281
+ { id: 'openrouter:openai/o3', requestedModel: 'o3', provider: 'openrouter', estInputPer1M: 2.0, estOutputPer1M: 8.0, capability: { reasoning: 10, coding: 9, security: 9, speed: 6, synthesis: 9 } },
282
+ { id: 'openrouter:google/gemini-2.5-pro', requestedModel: 'gemini-2.5-pro', provider: 'openrouter', estInputPer1M: 1.25, estOutputPer1M: 10.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
283
+ { id: 'openrouter:deepseek/deepseek-v4-pro', requestedModel: 'deepseek-v4-pro', provider: 'openrouter', estInputPer1M: 0.435, estOutputPer1M: 0.87, capability: { reasoning: 8, coding: 9, security: 8, speed: 10, synthesis: 8 } },
284
+ { id: 'openrouter:moonshotai/kimi-k2.5', requestedModel: 'kimi-k2.5', provider: 'openrouter', estInputPer1M: 0.44, estOutputPer1M: 2.0, capability: { reasoning: 9, coding: 9, security: 8, speed: 8, synthesis: 9 } },
285
+ { id: 'openrouter:deepseek/deepseek-r1', requestedModel: 'cloud-reason', provider: 'openrouter', estInputPer1M: 0.7, estOutputPer1M: 2.5, capability: { reasoning: 9, coding: 8, security: 8, speed: 7, synthesis: 8 } },
286
+ { id: 'openrouter:deepseek/deepseek-chat', requestedModel: 'cloud-pro', provider: 'openrouter', estInputPer1M: 0.32, estOutputPer1M: 0.89, capability: { reasoning: 7, coding: 8, security: 7, speed: 10, synthesis: 7 } },
287
+ ];
288
+ if (!modelsArg || !modelsArg.trim())
289
+ return catalog;
290
+ const allow = new Set(modelsArg.split(',').map((m) => m.trim()).filter(Boolean));
291
+ const filtered = catalog.filter((m) => (allow.has(m.id)
292
+ || allow.has(m.id.replace('openrouter:', ''))
293
+ || allow.has(m.requestedModel)));
294
+ return filtered.length > 0 ? filtered : catalog;
295
+ }
296
+ buildRoleQuote(scan, models) {
297
+ const roleWeights = {
298
+ detective: { reasoning: 10, coding: 8, security: 6, speed: 3, synthesis: 7 },
299
+ logic: { reasoning: 8, coding: 10, security: 3, speed: 6, synthesis: 6 },
300
+ security: { reasoning: 7, coding: 7, security: 10, speed: 5, synthesis: 6 },
301
+ performance: { reasoning: 9, coding: 9, security: 7, speed: 5, synthesis: 7 },
302
+ edge_case: { reasoning: 8, coding: 7, security: 7, speed: 7, synthesis: 5 },
303
+ integration: { reasoning: 8, coding: 8, security: 6, speed: 5, synthesis: 8 },
304
+ reviewer: { reasoning: 9, coding: 7, security: 7, speed: 6, synthesis: 8 },
305
+ architect: { reasoning: 10, coding: 9, security: 8, speed: 4, synthesis: 10 },
306
+ };
307
+ const complexity = Math.max(1, Math.ceil((scan.lines / 4000) + (scan.importEdges / 200)));
308
+ return Object.keys(roleWeights).map((role) => {
309
+ const w = roleWeights[role];
310
+ const preferredByRole = {
311
+ detective: 'gpt-5.5',
312
+ logic: 'o3',
313
+ security: 'opus-4.7',
314
+ performance: 'deepseek-v4-pro',
315
+ edge_case: 'gemini-2.5-pro',
316
+ integration: 'kimi-k2.5',
317
+ reviewer: 'opus-4.7',
318
+ architect: 'gpt-5.5',
319
+ };
320
+ let best = models.find((m) => m.requestedModel === preferredByRole[role]) || models[0];
321
+ if (!best)
322
+ best = models[0];
323
+ // Fallback scoring when preferred model is filtered out by --models.
324
+ if (!models.some((m) => m.requestedModel === preferredByRole[role])) {
325
+ let bestScore = -1;
326
+ for (const m of models) {
327
+ const score = m.capability.reasoning * w.reasoning + m.capability.coding * w.coding + m.capability.security * w.security + m.capability.speed * w.speed + m.capability.synthesis * w.synthesis;
328
+ const costPenalty = (m.estInputPer1M + m.estOutputPer1M) * 0.1;
329
+ const finalScore = score - costPenalty;
330
+ if (finalScore > bestScore) {
331
+ best = m;
332
+ bestScore = finalScore;
333
+ }
334
+ }
335
+ }
336
+ const estInputTokens = 1200 * complexity;
337
+ const estOutputTokens = 1800 * complexity;
338
+ const estCostUsd = (estInputTokens / 1_000_000) * best.estInputPer1M + (estOutputTokens / 1_000_000) * best.estOutputPer1M;
339
+ return { role, model: best.id, requestedModel: best.requestedModel, estInputTokens, estOutputTokens, estCostUsd };
340
+ });
341
+ }
342
+ buildGodmodeExplicitSteps(execution) {
343
+ const { originalRequest, workspace, scan, quote } = execution;
344
+ const quoteByRole = new Map(quote.map((row) => [row.role, row]));
345
+ const topFiles = scan.topFiles.slice(0, 5).map((file) => `${file.file} (${file.lines} lines, ${file.imports} imports)`);
346
+ const roleSequence = [
347
+ { role: 'detective', dependsOn: [] },
348
+ { role: 'logic', dependsOn: ['detective'] },
349
+ { role: 'security', dependsOn: ['detective'] },
350
+ { role: 'performance', dependsOn: ['detective'] },
351
+ { role: 'edge_case', dependsOn: ['detective'] },
352
+ { role: 'integration', dependsOn: ['detective'] },
353
+ { role: 'reviewer', dependsOn: ['detective'] },
354
+ { role: 'architect', dependsOn: ['logic', 'security', 'performance', 'edge_case', 'integration', 'reviewer'] },
355
+ ];
356
+ const roleIterationBudget = {
357
+ detective: 4,
358
+ logic: 5,
359
+ security: 5,
360
+ performance: 4,
361
+ edge_case: 4,
362
+ integration: 5,
363
+ reviewer: 5,
364
+ architect: 5,
365
+ };
366
+ const steps = roleSequence.map(({ role, dependsOn }) => {
367
+ const row = quoteByRole.get(role);
368
+ const requestedModel = row?.requestedModel || 'cloud-pro';
369
+ const model = row?.model || 'openrouter:deepseek/deepseek-chat';
370
+ const roleObjective = [
371
+ `[GODMODE:${role.toUpperCase()}]`,
372
+ originalRequest,
373
+ `Workspace: ${workspace}`,
374
+ `Execution contract: force requested_model=${requestedModel} via ${model}; do not substitute a local-only model.`,
375
+ `Project scan: files=${scan.files}, lines=${scan.lines}, import_edges=${scan.importEdges}`,
376
+ topFiles.length > 0 ? `Key files: ${topFiles.join('; ')}` : 'Key files: unavailable',
377
+ `Execution budget: at most ${roleIterationBudget[role] || 3} reasoning iterations; prioritize a one-pass final answer and avoid redundant full-repo rescans.`,
378
+ ].join('\n');
379
+ return {
380
+ step_id: role,
381
+ worker_name: 'v3_agent_worker',
382
+ objective: roleObjective,
383
+ depends_on: dependsOn,
384
+ priority: role === 'architect' ? 2 : 3,
385
+ retry_policy: {
386
+ max_attempts: 1,
387
+ strategy: 'repair',
388
+ requires_validation_failure: false,
389
+ },
390
+ payload: {
391
+ role,
392
+ requested_model: requestedModel,
393
+ quoted_model: model,
394
+ workspace,
395
+ top_files: topFiles,
396
+ max_iterations: roleIterationBudget[role] || 4,
397
+ compact_context: true,
398
+ max_dependency_chars: role === 'architect' ? 18000 : 12000,
399
+ max_artifacts: role === 'architect' ? 24 : 16,
400
+ max_context_chars: role === 'architect' ? 240000 : 180000,
401
+ max_output_tokens: role === 'detective' ? 3600 : role === 'architect' ? 3400 : role === 'reviewer' ? 3000 : 2400,
402
+ request_timeout_seconds: role === 'detective' || role === 'architect' ? 1200 : 720,
403
+ },
404
+ };
405
+ });
406
+ steps.push({
407
+ step_id: 'testing',
408
+ worker_name: 'testing_worker',
409
+ objective: [
410
+ '[GODMODE:TESTING]',
411
+ originalRequest,
412
+ `Workspace: ${workspace}`,
413
+ 'Validate the architect output, run the narrowest relevant checks, and report concrete failures if any remain.',
414
+ ].join('\n'),
415
+ depends_on: ['architect'],
416
+ priority: 2,
417
+ retry_policy: {
418
+ max_attempts: 1,
419
+ strategy: 'repair',
420
+ requires_validation_failure: false,
77
421
  },
78
- constraints: {},
422
+ payload: {
423
+ workspace,
424
+ validation_min_score: 80,
425
+ },
426
+ });
427
+ return steps;
428
+ }
429
+ buildBillingQuote(quote) {
430
+ const baseUsd = quote.reduce((sum, r) => sum + r.estCostUsd, 0);
431
+ const marginPctRaw = Number.parseFloat(String(process.env.VIGTHORIA_GODMODE_MARGIN_PCT || '10'));
432
+ const marginPct = Number.isFinite(marginPctRaw) ? Math.max(0, marginPctRaw) : 10;
433
+ const finalUsd = baseUsd * (1 + (marginPct / 100));
434
+ const vigcoinRateRaw = Number.parseFloat(String(process.env.VIGTHORIA_VIGCOIN_USD_RATE || '1'));
435
+ const vigcoinRateUsd = Number.isFinite(vigcoinRateRaw) && vigcoinRateRaw > 0 ? vigcoinRateRaw : 1;
436
+ const vigcoinRequired = finalUsd / vigcoinRateUsd;
437
+ return {
438
+ baseUsd,
439
+ marginPct,
440
+ finalUsd,
441
+ vigcoinRateUsd,
442
+ vigcoinRequired,
79
443
  };
80
- for (const baseUrl of HYPERLOOP_URLS) {
444
+ }
445
+ async evaluateBillingGate(billingQuote) {
446
+ const forcedPlan = String(process.env.VIGTHORIA_GODMODE_FORCE_PLAN || '').trim().toLowerCase();
447
+ // On-server invocations using a service key run as trusted infrastructure.
448
+ // No user wallet check is needed — cost is tracked at the service level.
449
+ const hasServiceKey = !!(process.env.HYPERLOOP_SERVICE_KEY || process.env.V3_SERVICE_KEY);
450
+ if (hasServiceKey) {
451
+ return {
452
+ plan: forcedPlan || this.config.getNormalizedPlan() || 'service',
453
+ masterAdminFree: true,
454
+ canProceed: true,
455
+ wallet: {
456
+ available: true,
457
+ vigcoinBalance: null,
458
+ source: 'service_key_access',
459
+ purchaseUrl: null,
460
+ },
461
+ };
462
+ }
463
+ const entitlement = await this.fetchGodmodeEntitlement();
464
+ const normalizedPlan = forcedPlan || entitlement.plan || this.config.getNormalizedPlan() || 'free';
465
+ const masterAdminFree = this.isMasterAdminFree(normalizedPlan, entitlement.masterAccess, entitlement.isMasterAdmin);
466
+ if (masterAdminFree) {
467
+ return {
468
+ plan: normalizedPlan,
469
+ masterAdminFree,
470
+ canProceed: true,
471
+ wallet: {
472
+ available: true,
473
+ vigcoinBalance: null,
474
+ source: 'master_admin_access',
475
+ purchaseUrl: null,
476
+ },
477
+ };
478
+ }
479
+ const wallet = await this.fetchWalletState();
480
+ if (!wallet.available || wallet.vigcoinBalance === null) {
481
+ return {
482
+ plan: normalizedPlan,
483
+ masterAdminFree,
484
+ canProceed: false,
485
+ wallet,
486
+ };
487
+ }
488
+ return {
489
+ plan: normalizedPlan,
490
+ masterAdminFree,
491
+ canProceed: wallet.vigcoinBalance >= billingQuote.vigcoinRequired,
492
+ wallet,
493
+ };
494
+ }
495
+ parseBooleanCandidate(value) {
496
+ if (typeof value === 'boolean') {
497
+ return value;
498
+ }
499
+ if (typeof value === 'number') {
500
+ if (value === 1)
501
+ return true;
502
+ if (value === 0)
503
+ return false;
504
+ return null;
505
+ }
506
+ if (typeof value === 'string') {
507
+ const v = value.trim().toLowerCase();
508
+ if (['1', 'true', 'yes', 'enabled', 'on'].includes(v))
509
+ return true;
510
+ if (['0', 'false', 'no', 'disabled', 'off'].includes(v))
511
+ return false;
512
+ }
513
+ return null;
514
+ }
515
+ async fetchGodmodeEntitlement() {
516
+ if (process.env.VIGTHORIA_GODMODE_FORCE_MASTER_ACCESS === '1') {
517
+ return { plan: this.config.getNormalizedPlan() || 'free', masterAccess: true, isMasterAdmin: true };
518
+ }
519
+ const baseUrl = this.getBillingBaseUrl();
520
+ const headers = this.getHeaders();
521
+ const endpoints = ['/api/user/subscription', '/api/user/info', '/api/user/profile'];
522
+ for (const endpoint of endpoints) {
81
523
  try {
82
- const response = await fetch(`${baseUrl}/legion/plan/execute`, {
83
- method: 'POST',
84
- headers: this.getHeaders(),
85
- body: JSON.stringify(body),
86
- signal: AbortSignal.timeout(300000),
524
+ const response = await fetch(`${baseUrl}${endpoint}`, {
525
+ method: 'GET',
526
+ signal: AbortSignal.timeout(12000),
527
+ headers,
87
528
  });
88
- if (!response.ok) {
89
- const errorText = await response.text().catch(() => '');
90
- throw new Error(`Legion API ${response.status}: ${(0, api_js_1.describeUpstreamStatus)(response.status)}`);
91
- }
92
- const result = await response.json();
93
- spinner.stop();
94
- console.log();
95
- console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Legion Execution Report ${logger_js_1.CH.hLine.repeat(34)}`));
96
- console.log();
97
- if (result.status === 'success' || result.ok) {
98
- console.log(chalk_1.default.green(` ${logger_js_1.CH.success} Legion completed successfully`));
99
- }
100
- else if (result.status === 'partial') {
101
- console.log(chalk_1.default.yellow(` ${logger_js_1.CH.warn} Legion completed partially`));
102
- }
103
- else {
104
- console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Legion execution failed`));
105
- }
106
- // Show execution graph results
107
- if (result.graph_results || result.results) {
108
- const results = result.graph_results || result.results || [];
109
- const steps = Array.isArray(results) ? results : (typeof results === 'object' ? Object.values(results) : []);
110
- if (steps.length > 0) {
111
- console.log();
112
- console.log(chalk_1.default.white(' Steps:'));
113
- for (const step of steps) {
114
- const stepObj = step;
115
- const icon = stepObj.status === 'completed' || stepObj.success
116
- ? chalk_1.default.green(logger_js_1.CH.success)
117
- : chalk_1.default.red(logger_js_1.CH.error);
118
- const name = stepObj.step_id || stepObj.worker || stepObj.name || 'step';
119
- const summary = stepObj.summary || stepObj.result || '';
120
- console.log(` ${icon} ${chalk_1.default.white(String(name))}${summary ? chalk_1.default.gray(` — ${String(summary).slice(0, 100)}`) : ''}`);
121
- }
529
+ if (!response.ok)
530
+ continue;
531
+ const payload = await response.json();
532
+ const data = (payload && typeof payload === 'object') ? payload : {};
533
+ const plan = String(data.subscription?.plan
534
+ || data.subscription_plan
535
+ || data.plan
536
+ || data.user?.subscription_plan
537
+ || data.user?.subscription?.plan
538
+ || '').trim().toLowerCase() || null;
539
+ const accessCandidates = [
540
+ data.godmode_master_access,
541
+ data.godmodeMasterAccess,
542
+ data.subscription?.godmode_master_access,
543
+ data.subscription?.godmodeMasterAccess,
544
+ data.user?.godmode_master_access,
545
+ data.user?.godmodeMasterAccess,
546
+ ];
547
+ const masterRoleCandidates = [
548
+ data.user?.isMasterAdmin,
549
+ data.user?.is_master_admin,
550
+ data.subscription?.isMasterAdmin,
551
+ data.subscription?.is_master_admin,
552
+ data.isMasterAdmin,
553
+ data.is_master_admin,
554
+ data.user?.admin_role === 'master_admin',
555
+ data.admin_role === 'master_admin',
556
+ ];
557
+ const roleHit = masterRoleCandidates
558
+ .map((c) => this.parseBooleanCandidate(c))
559
+ .find((v) => v !== null) ?? false;
560
+ for (const c of accessCandidates) {
561
+ const parsed = this.parseBooleanCandidate(c);
562
+ if (parsed !== null) {
563
+ return { plan, masterAccess: parsed, isMasterAdmin: Boolean(roleHit) };
122
564
  }
123
565
  }
124
- // Show timing and artifacts
125
- if (result.elapsed_seconds || result.elapsed) {
126
- console.log();
127
- console.log(chalk_1.default.gray(` Time: ${(result.elapsed_seconds || result.elapsed || 0).toFixed(1)}s`));
566
+ if (plan) {
567
+ return { plan, masterAccess: false, isMasterAdmin: Boolean(roleHit) };
568
+ }
569
+ }
570
+ catch (err) {
571
+ this.logger.warn(this.formatLegionError(`Godmode entitlement request ${endpoint}`, err));
572
+ continue;
573
+ }
574
+ }
575
+ return {
576
+ plan: this.config.getNormalizedPlan() || null,
577
+ masterAccess: false,
578
+ isMasterAdmin: false,
579
+ };
580
+ }
581
+ isMasterAdminFree(plan, masterAccess, isMasterAdmin) {
582
+ if (!masterAccess) {
583
+ return false;
584
+ }
585
+ if (isMasterAdmin) {
586
+ return true;
587
+ }
588
+ return plan === 'master_admin';
589
+ }
590
+ getBillingBaseUrl() {
591
+ const configured = String(this.config.get('apiUrl') || '').trim().replace(/\/$/, '');
592
+ if (configured) {
593
+ return configured;
594
+ }
595
+ return 'https://coder.vigthoria.io';
596
+ }
597
+ parseNumericCandidate(value) {
598
+ if (typeof value === 'number' && Number.isFinite(value)) {
599
+ return value;
600
+ }
601
+ if (typeof value === 'string') {
602
+ const parsed = Number.parseFloat(value.trim());
603
+ if (Number.isFinite(parsed)) {
604
+ return parsed;
605
+ }
606
+ }
607
+ return null;
608
+ }
609
+ extractVigcoinBalance(data) {
610
+ const candidates = [
611
+ data.vigcoinBalance,
612
+ data.vigcoin_balance,
613
+ data.balance,
614
+ data.credits,
615
+ data.credit_balance,
616
+ data.wallet?.vigcoinBalance,
617
+ data.wallet?.vigcoin_balance,
618
+ data.wallet?.balance,
619
+ data.wallet?.credits,
620
+ data.subscription?.vigcoinBalance,
621
+ data.subscription?.vigcoin_balance,
622
+ data.subscription?.credits,
623
+ data.user?.vigcoinBalance,
624
+ data.user?.vigcoin_balance,
625
+ data.user?.credits,
626
+ data.data?.vigcoinBalance,
627
+ data.data?.vigcoin_balance,
628
+ data.data?.balance,
629
+ data.data?.credits,
630
+ ];
631
+ for (const candidate of candidates) {
632
+ const parsed = this.parseNumericCandidate(candidate);
633
+ if (parsed !== null) {
634
+ return parsed;
635
+ }
636
+ }
637
+ return null;
638
+ }
639
+ getPurchaseUrlFromPayload(baseUrl, data) {
640
+ const raw = data.purchaseUrl || data.purchase_url || data.checkoutUrl || data.checkout_url || data.url || data.data?.checkoutUrl || data.data?.url;
641
+ if (typeof raw === 'string' && raw.trim()) {
642
+ if (/^https?:\/\//i.test(raw.trim())) {
643
+ return raw.trim();
644
+ }
645
+ return `${baseUrl}${raw.startsWith('/') ? '' : '/'}${raw.trim()}`;
646
+ }
647
+ return `${baseUrl}/billing`;
648
+ }
649
+ async fetchWalletState() {
650
+ const baseUrl = this.getBillingBaseUrl();
651
+ const headers = this.getHeaders();
652
+ const forcedLow = process.env.VIGTHORIA_GODMODE_FORCE_LOW_CREDIT === '1';
653
+ const forcedBalanceRaw = process.env.VIGTHORIA_GODMODE_FORCE_BALANCE;
654
+ if (forcedLow) {
655
+ return {
656
+ available: true,
657
+ vigcoinBalance: 0,
658
+ source: 'forced_low_credit',
659
+ purchaseUrl: `${baseUrl}/music/store#vigcoins`,
660
+ };
661
+ }
662
+ if (forcedBalanceRaw) {
663
+ const forced = this.parseNumericCandidate(forcedBalanceRaw);
664
+ if (forced !== null) {
665
+ return {
666
+ available: true,
667
+ vigcoinBalance: forced,
668
+ source: 'forced_balance',
669
+ purchaseUrl: `${baseUrl}/music/store#vigcoins`,
670
+ };
671
+ }
672
+ }
673
+ const endpoints = [
674
+ '/api/viagen6/vigcoin/balance',
675
+ '/api/user/subscription',
676
+ '/api/user/info',
677
+ '/api/user/profile',
678
+ '/api/wallet/balance',
679
+ '/api/billing/wallet',
680
+ '/api/billing/credits',
681
+ ];
682
+ for (const endpoint of endpoints) {
683
+ try {
684
+ const response = await fetch(`${baseUrl}${endpoint}`, {
685
+ method: 'GET',
686
+ signal: AbortSignal.timeout(12000),
687
+ headers,
688
+ });
689
+ if (!response.ok) {
690
+ continue;
128
691
  }
129
- if (result.content) {
130
- console.log();
131
- console.log(chalk_1.default.white(String(result.content).slice(0, 500)));
692
+ const payload = await this.readJsonResponse(response, `wallet balance request ${endpoint}`);
693
+ const payloadObj = (payload && typeof payload === 'object') ? payload : {};
694
+ const balance = this.extractVigcoinBalance(payloadObj);
695
+ if (balance === null) {
696
+ continue;
132
697
  }
133
- console.log();
134
- return;
698
+ return {
699
+ available: true,
700
+ vigcoinBalance: balance,
701
+ source: endpoint,
702
+ purchaseUrl: this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || `${baseUrl}/music/store#vigcoins`,
703
+ };
704
+ }
705
+ catch (err) {
706
+ this.logger.warn(this.formatLegionError(`wallet balance request ${endpoint}`, err));
707
+ continue;
135
708
  }
136
- catch (error) {
137
- if (error?.name === 'AbortError') {
138
- spinner.stop();
139
- this.logger.error('Legion request timed out');
140
- return;
709
+ }
710
+ return {
711
+ available: false,
712
+ vigcoinBalance: null,
713
+ source: null,
714
+ purchaseUrl: `${baseUrl}/music/store#vigcoins`,
715
+ error: 'Wallet endpoint unavailable from current gateway session',
716
+ };
717
+ }
718
+ async attemptDirectCharge(vigcoinNeeded) {
719
+ const baseUrl = this.getBillingBaseUrl();
720
+ const headers = this.getHeaders();
721
+ const amount = Math.max(1, Math.ceil(vigcoinNeeded));
722
+ const chargePayloads = [
723
+ { endpoint: '/api/viagen6/vigcoin/charge', body: { amount, reason: 'godmode_legion' } },
724
+ { endpoint: '/api/wallet/charge', body: { amount, currency: 'VIGCOIN', reason: 'godmode_legion' } },
725
+ { endpoint: '/api/billing/topup', body: { vigcoin: amount, reason: 'godmode_legion' } },
726
+ ];
727
+ for (const attempt of chargePayloads) {
728
+ try {
729
+ const response = await fetch(`${baseUrl}${attempt.endpoint}`, {
730
+ method: 'POST',
731
+ signal: AbortSignal.timeout(15000),
732
+ headers,
733
+ body: JSON.stringify(attempt.body),
734
+ });
735
+ const payload = await response.json().catch((err) => {
736
+ this.logger.warn(this.formatLegionError(`direct VigCoin charge response ${attempt.endpoint}`, err));
737
+ return {};
738
+ });
739
+ const payloadObj = (payload && typeof payload === 'object') ? payload : {};
740
+ const checkoutUrl = this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || `${baseUrl}/music/store#vigcoins`;
741
+ if (!response.ok) {
742
+ continue;
743
+ }
744
+ const charged = payloadObj.charged === true || payloadObj.success === true || payloadObj.status === 'charged';
745
+ if (charged) {
746
+ return { ok: true, checkoutUrl: checkoutUrl || undefined, note: `Charge accepted by ${attempt.endpoint}` };
141
747
  }
142
- // Try next URL
748
+ return {
749
+ ok: false,
750
+ checkoutUrl: checkoutUrl || undefined,
751
+ note: payloadObj.error || payloadObj.message || `Checkout required via ${attempt.endpoint}`,
752
+ };
753
+ }
754
+ catch (err) {
755
+ this.logger.warn(this.formatLegionError(`direct VigCoin charge request ${attempt.endpoint}`, err));
143
756
  continue;
144
757
  }
145
758
  }
759
+ return {
760
+ ok: false,
761
+ checkoutUrl: `${baseUrl}/music/store#vigcoins`,
762
+ note: 'No direct charge endpoint accepted this request',
763
+ };
764
+ }
765
+ async collectExecutionCharge(billingQuote, gate) {
766
+ if (gate.masterAdminFree) {
767
+ return true;
768
+ }
769
+ const spinner = (0, logger_js_1.createSpinner)('Charging VigCoin wallet for Godmode execution...').start();
770
+ const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired);
146
771
  spinner.stop();
147
- this.logger.error('Could not reach Hyper Loop Legion API. Is vigthoria-hyper-loop running?');
772
+ if (!result.ok) {
773
+ console.log(chalk_1.default.red(result.note || 'Wallet charge failed.'));
774
+ console.log(chalk_1.default.yellow(`Complete purchase first: ${result.checkoutUrl || `${this.getBillingBaseUrl()}/music/store#vigcoins`}`));
775
+ return false;
776
+ }
777
+ console.log(chalk_1.default.green('Wallet charged for Godmode execution.'));
778
+ return true;
779
+ }
780
+ async resolveBillingInsufficientFunds(billingQuote, gate, options) {
781
+ this.printBillingGateSummary(billingQuote, gate);
782
+ if (!gate.wallet.available) {
783
+ console.log(chalk_1.default.red('Unable to verify wallet balance from server. Execution is blocked.'));
784
+ if (gate.wallet.purchaseUrl) {
785
+ console.log(chalk_1.default.gray(`Open billing portal: ${gate.wallet.purchaseUrl}`));
786
+ }
787
+ return false;
788
+ }
789
+ if (options.autoCharge) {
790
+ const spinner = (0, logger_js_1.createSpinner)('Attempting direct wallet charge...').start();
791
+ const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired - (gate.wallet.vigcoinBalance || 0));
792
+ spinner.stop();
793
+ if (result.ok) {
794
+ console.log(chalk_1.default.green('Direct charge succeeded. Re-checking wallet balance...'));
795
+ return true;
796
+ }
797
+ console.log(chalk_1.default.yellow(result.note || 'Direct charge did not complete.'));
798
+ console.log(chalk_1.default.yellow(`Complete purchase first: ${result.checkoutUrl || gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
799
+ return false;
800
+ }
801
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
802
+ console.log(chalk_1.default.yellow('Low balance detected in non-interactive mode. Re-run with --auto-charge or top up first.'));
803
+ if (gate.wallet.purchaseUrl) {
804
+ console.log(chalk_1.default.gray(`Billing portal: ${gate.wallet.purchaseUrl}`));
805
+ }
806
+ return false;
807
+ }
808
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
809
+ try {
810
+ const answer = (await rl.question('VigCoin low. Choose action: [c]harge now, [p]urchase first, [n] cancel: ')).trim().toLowerCase();
811
+ if (answer === 'c' || answer === 'charge') {
812
+ const spinner = (0, logger_js_1.createSpinner)('Attempting direct wallet charge...').start();
813
+ const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired - (gate.wallet.vigcoinBalance || 0));
814
+ spinner.stop();
815
+ if (result.ok) {
816
+ console.log(chalk_1.default.green('Direct charge succeeded. Re-checking wallet balance...'));
817
+ return true;
818
+ }
819
+ console.log(chalk_1.default.yellow(result.note || 'Direct charge did not complete.'));
820
+ console.log(chalk_1.default.yellow(`Complete purchase first: ${result.checkoutUrl || gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
821
+ return false;
822
+ }
823
+ if (answer === 'p' || answer === 'purchase') {
824
+ console.log(chalk_1.default.yellow(`Purchase VigCoin first: ${gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
825
+ return false;
826
+ }
827
+ return false;
828
+ }
829
+ finally {
830
+ rl.close();
831
+ }
832
+ }
833
+ printBillingGateSummary(billingQuote, gate) {
834
+ console.log(chalk_1.default.white(' Billing gate:'));
835
+ console.log(chalk_1.default.gray(` Plan: ${gate.plan}`));
836
+ if (gate.masterAdminFree) {
837
+ console.log(chalk_1.default.green(' Free tier override applied (Master Admin).'));
838
+ return;
839
+ }
840
+ console.log(chalk_1.default.gray(` Estimated total (USD): $${billingQuote.finalUsd.toFixed(4)}`));
841
+ console.log(chalk_1.default.gray(` VigCoin rate: 1 VIG = $${billingQuote.vigcoinRateUsd.toFixed(4)}`));
842
+ console.log(chalk_1.default.gray(` VigCoin required: ${billingQuote.vigcoinRequired.toFixed(3)}`));
843
+ if (gate.wallet.vigcoinBalance !== null) {
844
+ const color = gate.wallet.vigcoinBalance >= billingQuote.vigcoinRequired ? chalk_1.default.green : chalk_1.default.red;
845
+ console.log(chalk_1.default.gray(' Wallet balance: ') + color(gate.wallet.vigcoinBalance.toFixed(3)) + (gate.wallet.source ? chalk_1.default.gray(` (source: ${gate.wallet.source})`) : ''));
846
+ }
847
+ else {
848
+ console.log(chalk_1.default.red(` Wallet balance: unavailable${gate.wallet.error ? ` (${gate.wallet.error})` : ''}`));
849
+ }
850
+ if (gate.wallet.purchaseUrl) {
851
+ console.log(chalk_1.default.gray(` Purchase URL: ${gate.wallet.purchaseUrl}`));
852
+ }
853
+ }
854
+ printGodmodeQuote(workspace, scan, quote, billingQuote, gate) {
855
+ const totalCost = billingQuote.finalUsd;
856
+ console.log();
857
+ console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Legion Godmode Calculator ${logger_js_1.CH.hLine.repeat(31)}`));
858
+ console.log();
859
+ console.log(chalk_1.default.gray(' Workspace: ') + chalk_1.default.white(workspace));
860
+ console.log(chalk_1.default.gray(' Files scanned: ') + chalk_1.default.white(String(scan.files)));
861
+ console.log(chalk_1.default.gray(' Lines scanned: ') + chalk_1.default.white(String(scan.lines)));
862
+ console.log(chalk_1.default.gray(' Dependency edges: ') + chalk_1.default.white(String(scan.importEdges)));
863
+ if (scan.topFiles.length > 0) {
864
+ console.log();
865
+ console.log(chalk_1.default.white(' Top context files:'));
866
+ for (const f of scan.topFiles) {
867
+ console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${f.file} (${f.lines} lines, ${f.imports} imports)`));
868
+ }
869
+ }
870
+ console.log();
871
+ console.log(chalk_1.default.white(' Role assignment and estimated cost:'));
872
+ for (const row of quote) {
873
+ console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${row.role.padEnd(11)} ${row.model} $${row.estCostUsd.toFixed(4)}`));
874
+ }
875
+ console.log();
876
+ console.log(chalk_1.default.yellow(` Estimated total: $${totalCost.toFixed(4)}`));
877
+ console.log(chalk_1.default.gray(' Flow: Estimate -> Isolation -> Parallel Attack -> Synthesis'));
878
+ console.log();
879
+ this.printBillingGateSummary(billingQuote, gate);
880
+ console.log();
881
+ }
882
+ async confirmExecution() {
883
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
884
+ console.log(chalk_1.default.yellow('Non-interactive terminal detected. Re-run with --approve to continue execution.'));
885
+ return false;
886
+ }
887
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
888
+ try {
889
+ const answer = (await rl.question('Proceed with Godmode execution? (y/N): ')).trim().toLowerCase();
890
+ return answer === 'y' || answer === 'yes';
891
+ }
892
+ finally {
893
+ rl.close();
894
+ }
895
+ }
896
+ /**
897
+ * SSE streaming URL for the Legion execution endpoint.
898
+ * Always hits Hyper Loop directly (port 8020) with the service key to avoid
899
+ * gateway JWT expiry killing long-running GodMode jobs.
900
+ */
901
+ getLegionStreamUrl() {
902
+ const envOverride = String(process.env.VIGTHORIA_HYPERLOOP_URL || '').trim().replace(/\/$/, '');
903
+ if (envOverride)
904
+ return `${envOverride}/legion/stream`;
905
+ return 'http://localhost:8020/api/hyperloop/legion/stream';
906
+ }
907
+ getLegionServiceKey() {
908
+ // Service key lets on-server CLI calls bypass gateway JWT validation
909
+ return String(process.env.HYPERLOOP_SERVICE_KEY ||
910
+ process.env.V3_SERVICE_KEY ||
911
+ '');
912
+ }
913
+ async planAndExecute(request, options, godmodeExecution) {
914
+ const explicitSteps = godmodeExecution ? this.buildGodmodeExplicitSteps(godmodeExecution) : undefined;
915
+ const workspace = godmodeExecution?.workspace || options.project || process.cwd();
916
+ const body = {
917
+ request,
918
+ context: { workspace },
919
+ constraints: {
920
+ active_only: true,
921
+ execution_timeout_seconds: options.timeoutSec,
922
+ preferred_workers: explicitSteps ? undefined : ['v3_agent_worker'],
923
+ explicit_steps: explicitSteps,
924
+ },
925
+ };
926
+ const streamUrl = this.getLegionStreamUrl();
927
+ const serviceKey = this.getLegionServiceKey();
928
+ const headers = { 'Content-Type': 'application/json' };
929
+ if (serviceKey) {
930
+ headers['X-Service-Key'] = serviceKey;
931
+ }
932
+ else {
933
+ // Fallback: pass user JWT (works if token is still valid)
934
+ const token = this.config.get('authToken');
935
+ if (token) {
936
+ headers['Authorization'] = `Bearer ${token}`;
937
+ headers['Cookie'] = `vigthoria-auth-token=${token}`;
938
+ }
939
+ }
940
+ const spinner = (0, logger_js_1.createSpinner)('Connecting to Legion SSE stream...').start();
941
+ const startTime = Date.now();
942
+ let response;
943
+ try {
944
+ // No AbortSignal timeout — SSE keeps alive; server controls lifetime
945
+ response = await fetch(streamUrl, {
946
+ method: 'POST',
947
+ headers,
948
+ body: JSON.stringify(body),
949
+ });
950
+ }
951
+ catch (connErr) {
952
+ spinner.stop();
953
+ this.logger.error(`Cannot connect to Hyper Loop at ${streamUrl}: ${connErr?.message || connErr}`);
954
+ return;
955
+ }
956
+ if (!response.ok) {
957
+ spinner.stop();
958
+ const errBody = await response.text().catch(() => '');
959
+ this.logger.error(`Legion stream ${response.status}: ${(0, api_js_1.describeUpstreamStatus)(response.status)} — ${errBody.slice(0, 200)}`);
960
+ return;
961
+ }
962
+ if (!response.body) {
963
+ spinner.stop();
964
+ this.logger.error('Legion stream returned no response body');
965
+ return;
966
+ }
967
+ // ── SSE consumer ─────────────────────────────────────────────
968
+ spinner.stop();
969
+ console.log();
970
+ console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Legion Execution Report ${logger_js_1.CH.hLine.repeat(34)}`));
971
+ console.log();
972
+ const decoder = new TextDecoder();
973
+ let buffer = '';
974
+ let finalResult = null;
975
+ let stepsTotal = 0;
976
+ let stepsDone = 0;
977
+ try {
978
+ const reader = response.body.getReader();
979
+ while (true) {
980
+ const { done, value } = await reader.read();
981
+ if (done)
982
+ break;
983
+ buffer += decoder.decode(value, { stream: true });
984
+ const lines = buffer.split('\n');
985
+ buffer = lines.pop() ?? ''; // keep incomplete last line
986
+ for (const line of lines) {
987
+ const trimmed = line.trim();
988
+ if (!trimmed || trimmed.startsWith(':'))
989
+ continue; // keep-alive or comment
990
+ if (!trimmed.startsWith('data:'))
991
+ continue;
992
+ const jsonStr = trimmed.slice(5).trim();
993
+ let evt;
994
+ try {
995
+ evt = JSON.parse(jsonStr);
996
+ }
997
+ catch {
998
+ continue;
999
+ }
1000
+ switch (evt.event) {
1001
+ case 'plan':
1002
+ stepsTotal = evt.steps_total || 0;
1003
+ console.log(chalk_1.default.gray(` Planned workers: ${stepsTotal} steps queued`));
1004
+ console.log();
1005
+ break;
1006
+ case 'batch_start':
1007
+ console.log(chalk_1.default.gray(` ▶ Running: ${evt.workers.join(', ')} (${evt.steps_done}/${evt.steps_total})`));
1008
+ break;
1009
+ case 'step_complete': {
1010
+ stepsDone = Number(evt.steps_done) || 0;
1011
+ const icon = evt.status === 'completed' ? chalk_1.default.green(logger_js_1.CH.success) : chalk_1.default.red(logger_js_1.CH.error);
1012
+ const summary = evt.summary ? chalk_1.default.gray(` — ${String(evt.summary).slice(0, 120)}`) : '';
1013
+ console.log(` ${icon} ${chalk_1.default.white(String(evt.step_id))} ${chalk_1.default.gray('[' + evt.worker + ']')}${summary}`);
1014
+ break;
1015
+ }
1016
+ case 'complete':
1017
+ finalResult = evt.result || null;
1018
+ if (evt.status === 'completed') {
1019
+ console.log();
1020
+ console.log(chalk_1.default.green(` ${logger_js_1.CH.success} Legion completed successfully`));
1021
+ }
1022
+ else if (evt.status === 'failed') {
1023
+ console.log();
1024
+ console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Legion execution failed`));
1025
+ if (evt.error)
1026
+ console.log(chalk_1.default.red(` Error: ${String(evt.error).slice(0, 300)}`));
1027
+ }
1028
+ break;
1029
+ case 'error':
1030
+ console.log(chalk_1.default.red(` ${logger_js_1.CH.error} Stream error: ${String(evt.error).slice(0, 300)}`));
1031
+ break;
1032
+ }
1033
+ }
1034
+ }
1035
+ }
1036
+ catch (streamErr) {
1037
+ this.logger.error(`Legion stream read error: ${streamErr?.message || streamErr}`);
1038
+ }
1039
+ const elapsedSec = ((Date.now() - startTime) / 1000).toFixed(1);
1040
+ if (stepsTotal > 0 && stepsDone < stepsTotal) {
1041
+ console.log(chalk_1.default.yellow(` ${logger_js_1.CH.warn} Legion stream ended after ${stepsDone}/${stepsTotal} steps`));
1042
+ }
1043
+ console.log();
1044
+ console.log(chalk_1.default.gray(` Time: ${elapsedSec}s`));
1045
+ // Show final output from architect/last step
1046
+ if (finalResult) {
1047
+ const lastStepResult = finalResult.final_output;
1048
+ const summary = lastStepResult?.result?.summary || lastStepResult?.summary || '';
1049
+ if (summary) {
1050
+ console.log();
1051
+ console.log(chalk_1.default.white(' Final output:'));
1052
+ console.log(chalk_1.default.gray(` ${String(summary).slice(0, 600)}`));
1053
+ }
1054
+ }
1055
+ console.log();
1056
+ }
1057
+ formatLegionError(context, err) {
1058
+ const message = err?.message || String(err || 'Unknown error');
1059
+ const cause = err?.cause?.message ? ` (${err.cause.message})` : '';
1060
+ return `${context} failed: ${message}${cause}`;
148
1061
  }
149
1062
  async showWorkers() {
150
1063
  const spinner = (0, logger_js_1.createSpinner)('Fetching Legion worker catalog...').start();
151
- for (const baseUrl of HYPERLOOP_URLS) {
1064
+ let lastError = null;
1065
+ for (const baseUrl of this.getHyperloopUrls()) {
152
1066
  try {
153
1067
  const response = await fetch(`${baseUrl}/legion/workers`, {
154
1068
  signal: AbortSignal.timeout(10000),
155
1069
  headers: this.getHeaders(),
156
1070
  });
157
- if (!response.ok)
1071
+ if (!response.ok) {
1072
+ const errBody = await response.text().catch((err) => {
1073
+ this.logger.warn(this.formatLegionError(`worker catalog error body at ${baseUrl}`, err));
1074
+ return '';
1075
+ });
1076
+ lastError = `Legion worker catalog request at ${baseUrl} failed: ${response.status} ${(0, api_js_1.describeUpstreamStatus)(response.status)}${errBody ? ` — ${errBody.slice(0, 200)}` : ''}`;
158
1077
  continue;
159
- const data = await response.json();
1078
+ }
1079
+ const data = await this.readJsonResponse(response, `worker catalog request at ${baseUrl}`);
160
1080
  spinner.stop();
161
1081
  console.log();
162
1082
  console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Legion Worker Catalog ${logger_js_1.CH.hLine.repeat(37)}`));
@@ -186,24 +1106,28 @@ class LegionCommand {
186
1106
  console.log();
187
1107
  return;
188
1108
  }
189
- catch {
1109
+ catch (err) {
1110
+ lastError = this.formatLegionError(`worker catalog request at ${baseUrl}`, err);
190
1111
  continue;
191
1112
  }
192
1113
  }
193
1114
  spinner.stop();
194
- this.logger.error('Could not reach Hyper Loop. Is vigthoria-hyper-loop running?');
1115
+ this.logger.error(lastError || 'Could not reach Hyper Loop. Is vigthoria-hyper-loop running?');
195
1116
  }
196
1117
  async showStatus() {
197
1118
  const spinner = (0, logger_js_1.createSpinner)('Checking Legion infrastructure...').start();
198
- for (const baseUrl of HYPERLOOP_URLS) {
1119
+ let lastError = null;
1120
+ for (const baseUrl of this.getHyperloopUrls()) {
199
1121
  try {
200
1122
  const response = await fetch(`${baseUrl}/status`, {
201
1123
  signal: AbortSignal.timeout(10000),
202
1124
  headers: this.getHeaders(),
203
1125
  });
204
- if (!response.ok)
1126
+ if (!response.ok) {
1127
+ lastError = `Legion status check at ${baseUrl} failed: ${response.status} ${(0, api_js_1.describeUpstreamStatus)(response.status)}`;
205
1128
  continue;
206
- const data = await response.json();
1129
+ }
1130
+ const data = await this.readJsonResponse(response, `status check at ${baseUrl}`);
207
1131
  spinner.stop();
208
1132
  console.log();
209
1133
  console.log(chalk_1.default.bold.white(` ${logger_js_1.CH.hLine.repeat(3)} Legion Infrastructure ${logger_js_1.CH.hLine.repeat(37)}`));
@@ -225,13 +1149,17 @@ class LegionCommand {
225
1149
  console.log();
226
1150
  return;
227
1151
  }
228
- catch {
1152
+ catch (err) {
1153
+ lastError = this.formatLegionError(`status check at ${baseUrl}`, err);
229
1154
  continue;
230
1155
  }
231
1156
  }
232
1157
  spinner.stop();
233
1158
  console.log();
234
1159
  console.log(chalk_1.default.gray(' Hyper Loop: ') + chalk_1.default.red('offline'));
1160
+ if (lastError) {
1161
+ this.logger.error(lastError);
1162
+ }
235
1163
  console.log();
236
1164
  }
237
1165
  }