vigthoria-cli 1.9.5 → 1.9.9

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/README.md CHANGED
@@ -99,7 +99,7 @@ If you see `ENOTFOUND registry.npmjs.org`, try these solutions:
99
99
  4. **Direct tarball download:**
100
100
  ```bash
101
101
  # Download directly
102
- npm install -g https://coder.vigthoria.io/releases/vigthoria-cli-1.9.3.tgz
102
+ npm install -g https://coder.vigthoria.io/releases/vigthoria-cli-1.9.9.tgz
103
103
  ```
104
104
 
105
105
  5. **Use Git clone method (no npm registry needed):**
@@ -20,6 +20,7 @@ const fs_1 = require("fs");
20
20
  const os_1 = require("os");
21
21
  const path_1 = __importDefault(require("path"));
22
22
  const readline_1 = __importDefault(require("readline"));
23
+ const config_js_1 = require("../utils/config.js");
23
24
  const DEFAULT_API_URL = 'https://coder.vigthoria.io';
24
25
  const CONFIG_DIR = path_1.default.join((0, os_1.homedir)(), '.vigthoria');
25
26
  const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
@@ -65,6 +66,36 @@ function ensureConfigDir() {
65
66
  // Best-effort on non-POSIX filesystems.
66
67
  }
67
68
  }
69
+ function syncSharedConfig(config) {
70
+ try {
71
+ const shared = new config_js_1.Config();
72
+ shared.set('apiUrl', trimTrailingSlash(config.apiUrl || getApiUrl()));
73
+ if (config.token) {
74
+ shared.set('authToken', config.token);
75
+ }
76
+ if (config.user?.id) {
77
+ shared.set('userId', String(config.user.id));
78
+ }
79
+ if (config.user?.email) {
80
+ shared.set('email', String(config.user.email));
81
+ }
82
+ }
83
+ catch {
84
+ // Keep legacy auth flow working even if shared config write fails.
85
+ }
86
+ }
87
+ function clearSharedConfigAuth() {
88
+ try {
89
+ const shared = new config_js_1.Config();
90
+ shared.set('authToken', null);
91
+ shared.set('refreshToken', null);
92
+ shared.set('userId', null);
93
+ shared.set('email', null);
94
+ }
95
+ catch {
96
+ // Ignore shared config clear failures during logout.
97
+ }
98
+ }
68
99
  function humanMessage(error) {
69
100
  const raw = error instanceof Error ? error.message : String(error);
70
101
  if (/^\s*</.test(raw) || /<!doctype html/i.test(raw)) {
@@ -128,24 +159,44 @@ function saveAuthConfig(config) {
128
159
  catch {
129
160
  // Best-effort on non-POSIX filesystems.
130
161
  }
162
+ syncSharedConfig(config);
131
163
  }
132
164
  function clearAuthConfig() {
133
165
  if ((0, fs_1.existsSync)(CONFIG_FILE)) {
134
166
  (0, fs_1.rmSync)(CONFIG_FILE, { force: true });
135
167
  }
168
+ clearSharedConfigAuth();
136
169
  }
137
170
  function getAuthToken() {
138
171
  return process.env.VIGTHORIA_TOKEN || loadAuthConfig().token;
139
172
  }
140
173
  async function requestJson(url, init) {
141
- const response = await fetch(url, {
142
- ...init,
143
- headers: {
144
- accept: 'application/json',
145
- 'content-type': 'application/json',
146
- ...(init.headers || {}),
147
- },
148
- });
174
+ const timeoutMsRaw = Number(process.env.VIGTHORIA_AUTH_TIMEOUT_MS || 8000);
175
+ const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 8000;
176
+ const controller = new AbortController();
177
+ const timeoutHandle = setTimeout(() => controller.abort(), timeoutMs);
178
+ let response;
179
+ try {
180
+ response = await fetch(url, {
181
+ ...init,
182
+ signal: controller.signal,
183
+ headers: {
184
+ accept: 'application/json',
185
+ 'content-type': 'application/json',
186
+ ...(init.headers || {}),
187
+ },
188
+ });
189
+ }
190
+ catch (error) {
191
+ const name = error?.name;
192
+ if (name === 'AbortError') {
193
+ throw new Error(`Authentication request timed out after ${timeoutMs}ms`);
194
+ }
195
+ throw error;
196
+ }
197
+ finally {
198
+ clearTimeout(timeoutHandle);
199
+ }
149
200
  const text = await response.text();
150
201
  let body = {};
151
202
  if (text.trim()) {
@@ -425,10 +476,38 @@ async function statusAction() {
425
476
  catch {
426
477
  // Keep local status available even if remote whoami endpoint is down.
427
478
  }
479
+ const sharedConfig = new config_js_1.Config();
480
+ let overallStatus = 'Unknown';
481
+ let coderStatus = 'Unknown';
482
+ let modelsStatus = 'Unknown';
483
+ try {
484
+ const [coderResponse, modelsResponse] = await Promise.all([
485
+ fetch(`${trimTrailingSlash(config.apiUrl)}/api/health`),
486
+ fetch('https://api.vigthoria.io/health'),
487
+ ]);
488
+ const coderJson = await coderResponse.json().catch(() => ({}));
489
+ const modelsJson = await modelsResponse.json().catch(() => ({}));
490
+ const coderOk = coderResponse.ok && (coderJson.status === 'ok' || coderJson.healthy === true);
491
+ const modelsOk = modelsResponse.ok && (modelsJson.status === 'ok' || modelsJson.status === 'healthy' || modelsJson.healthy === true);
492
+ coderStatus = coderOk ? 'Online' : 'Offline';
493
+ modelsStatus = modelsOk ? 'Online' : 'Offline';
494
+ overallStatus = coderOk && modelsOk ? 'Healthy' : 'Degraded';
495
+ }
496
+ catch {
497
+ // Status should still render even if health probes fail.
498
+ }
499
+ const plan = String(sharedConfig.get('subscription')?.plan || '').trim();
500
+ console.log(chalk_1.default.white('Account Status'));
428
501
  console.log(chalk_1.default.green('Logged in.'));
429
502
  if (user?.email) {
430
503
  console.log(`Account: ${user.email}`);
431
504
  }
505
+ if (plan) {
506
+ console.log(`Plan: ${plan.toUpperCase()}`);
507
+ }
508
+ console.log(`Overall: ${overallStatus}`);
509
+ console.log(`Coder API: ${coderStatus}`);
510
+ console.log(`Models API: ${modelsStatus}`);
432
511
  console.log(`API: ${config.apiUrl}`);
433
512
  process.exitCode = 0;
434
513
  }
@@ -19,14 +19,9 @@ class BridgeCommand {
19
19
  console.log();
20
20
  console.log(chalk_1.default.white('DevTools Bridge:'));
21
21
  console.log(chalk_1.default.gray(' Status: ') + (bridge.ok ? chalk_1.default.green('Reachable') : chalk_1.default.yellow('Not running')));
22
- // Only show error details for unexpected failures — suppress
23
- // "Connection timed out" / "ECONNREFUSED" for the normal
24
- // not-running case to keep the UX clean.
25
- if (bridge.error && bridge.ok === false) {
26
- const isExpectedDown = /timed? out|ECONNREFUSED|connect ECONNREFUSED|Connection refused/i.test(bridge.error);
27
- if (!isExpectedDown) {
28
- console.log(chalk_1.default.gray(' Detail: ') + chalk_1.default.yellow(bridge.error));
29
- }
22
+ if (!bridge.ok) {
23
+ const detail = String(bridge.error || 'Connection refused').trim() || 'Connection refused';
24
+ console.log(chalk_1.default.gray(' Error: ') + chalk_1.default.yellow(detail));
30
25
  }
31
26
  console.log(chalk_1.default.gray(' Browser tasks: ') + (bridge.ok
32
27
  ? chalk_1.default.green('Local browser observability is available for debugging flows.')
@@ -66,9 +66,12 @@ const DEFAULT_V3_AGENT_IDLE_TIMEOUT_MS = (() => {
66
66
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
67
67
  })();
68
68
  const DEFAULT_V3_AGENT_SOFT_TIMEOUT_MS = (() => {
69
- const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS || '300000';
69
+ const rawValue = process.env.VIGTHORIA_AGENT_SOFT_TIMEOUT_MS || process.env.V3_AGENT_SOFT_TIMEOUT_MS;
70
+ if (!rawValue) {
71
+ return 0;
72
+ }
70
73
  const parsed = Number.parseInt(rawValue, 10);
71
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 300000;
74
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
72
75
  })();
73
76
  class ChatCommand {
74
77
  config;
@@ -192,7 +195,9 @@ class ChatCommand {
192
195
  if (attempt >= retries || (!this.isNetworkError(error) && !this.isTimeoutError(error))) {
193
196
  this.handleApiError(error, context);
194
197
  }
195
- this.logger.warn(`${context} failed due to ${this.isTimeoutError(error) ? 'timeout' : 'network error'}; retrying (${attempt + 1}/${retries})...`);
198
+ if (!this.jsonOutput) {
199
+ this.logger.warn(`${context} failed due to ${this.isTimeoutError(error) ? 'timeout' : 'network error'}; retrying (${attempt + 1}/${retries})...`);
200
+ }
196
201
  await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
197
202
  }
198
203
  }
@@ -221,7 +226,9 @@ class ChatCommand {
221
226
  reason: 'governance-blocked-model',
222
227
  };
223
228
  this.currentModel = effectiveModel;
224
- this.logger.warn(`Model ${requestedModel} is not permitted for no-agent chat; using ${effectiveModel}`);
229
+ if (!this.jsonOutput) {
230
+ this.logger.warn(`Model ${requestedModel} is not permitted for no-agent chat; using ${effectiveModel}`);
231
+ }
225
232
  return;
226
233
  }
227
234
  this.modelGovernanceFallback = null;
@@ -248,7 +255,7 @@ class ChatCommand {
248
255
  }
249
256
  operatorAccessMessage() {
250
257
  const currentPlan = this.config.get('subscription').plan || 'free';
251
- return `Operator mode requires Enterprise or admin access. Current plan: ${currentPlan}.`;
258
+ return `Operator mode requires Pro, Ultra, Enterprise, or admin access. Current plan: ${currentPlan}.`;
252
259
  }
253
260
  getDefaultChatModel() {
254
261
  const preferredModel = String(this.config.get('preferences').defaultModel || '').trim().toLowerCase();
@@ -877,11 +884,16 @@ class ChatCommand {
877
884
  return;
878
885
  }
879
886
  if (options.prompt) {
880
- // Wrap in a timeout to guarantee bridge cleanup even if the agent
881
- // loop hangs (e.g. model takes forever on a turn).
882
- const BRIDGE_TIMEOUT_MS = 180_000; // 3 minutes max for a single prompt
887
+ const bridgePromptTimeoutMs = (() => {
888
+ const rawValue = process.env.VIGTHORIA_BRIDGE_PROMPT_TIMEOUT_MS || process.env.V3_BRIDGE_PROMPT_TIMEOUT_MS;
889
+ if (!rawValue) {
890
+ return 0;
891
+ }
892
+ const parsed = Number.parseInt(rawValue, 10);
893
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
894
+ })();
883
895
  let timedOut = false;
884
- const timeoutId = options.bridge
896
+ const timeoutId = options.bridge && bridgePromptTimeoutMs > 0
885
897
  ? setTimeout(() => {
886
898
  timedOut = true;
887
899
  const b = (0, bridge_client_js_1.getBridgeClient)();
@@ -890,10 +902,10 @@ class ChatCommand {
890
902
  b.destroy();
891
903
  }
892
904
  if (!this.jsonOutput) {
893
- this.logger.error('Bridge prompt timed out after 3 minutes.');
905
+ this.logger.error('Bridge prompt timed out after ' + Math.round(bridgePromptTimeoutMs / 1000) + ' seconds.');
894
906
  }
895
907
  process.exitCode = 1;
896
- }, BRIDGE_TIMEOUT_MS)
908
+ }, bridgePromptTimeoutMs)
897
909
  : null;
898
910
  await this.handleDirectPrompt(options.prompt);
899
911
  if (timeoutId)
@@ -1093,7 +1105,7 @@ class ChatCommand {
1093
1105
  async handleDirectPrompt(prompt) {
1094
1106
  // Suppress all setup banners in direct-prompt mode so only the final
1095
1107
  // answer reaches stdout. Interactive (REPL) mode still shows them.
1096
- if (!this.jsonOutput && !this.directPromptMode) {
1108
+ if (!this.jsonOutput) {
1097
1109
  console.log(chalk_1.default.cyan('Running single prompt in direct mode.'));
1098
1110
  console.log(chalk_1.default.gray(`Model: ${this.currentModel}`));
1099
1111
  console.log(chalk_1.default.gray(`Project: ${this.currentProjectPath}`));
@@ -1745,12 +1757,12 @@ class ChatCommand {
1745
1757
  tool: 'read_file',
1746
1758
  args: { path: targetFile },
1747
1759
  };
1748
- if (!this.jsonOutput && !this.directPromptMode) {
1760
+ if (!this.jsonOutput) {
1749
1761
  console.log(chalk_1.default.cyan(`⚙ Executing: ${readCall.tool}`));
1750
1762
  }
1751
1763
  const readResult = await this.tools.execute(readCall);
1752
1764
  const readSummary = this.formatToolResult(readCall, readResult);
1753
- if (!this.jsonOutput && !this.directPromptMode) {
1765
+ if (!this.jsonOutput) {
1754
1766
  console.log(readResult.success ? chalk_1.default.gray(readSummary) : chalk_1.default.red(readSummary));
1755
1767
  }
1756
1768
  this.messages.push({ role: 'system', content: readSummary });
@@ -1793,12 +1805,12 @@ class ChatCommand {
1793
1805
  content: rewrittenContent,
1794
1806
  },
1795
1807
  };
1796
- if (!this.jsonOutput && !this.directPromptMode) {
1808
+ if (!this.jsonOutput) {
1797
1809
  console.log(chalk_1.default.cyan(`⚙ Executing: ${writeCall.tool}`));
1798
1810
  }
1799
1811
  const writeResult = await this.tools.execute(writeCall);
1800
1812
  const writeSummary = this.formatToolResult(writeCall, writeResult);
1801
- if (!this.jsonOutput && !this.directPromptMode) {
1813
+ if (!this.jsonOutput) {
1802
1814
  console.log(writeResult.success ? chalk_1.default.gray(writeSummary) : chalk_1.default.red(writeSummary));
1803
1815
  }
1804
1816
  this.messages.push({ role: 'system', content: writeSummary });
@@ -1972,7 +1984,7 @@ class ChatCommand {
1972
1984
  }
1973
1985
  else if (this.v3StreamingStarted) {
1974
1986
  // Content was already streamed to stdout in real-time; skip duplicate print.
1975
- if (!this.directPromptMode) {
1987
+ if (!this.jsonOutput) {
1976
1988
  console.log(chalk_1.default.gray(`Agent routing: ${routingPolicy.cloudSelected ? 'Vigthoria Cloud' : 'V3 Agent'}`));
1977
1989
  }
1978
1990
  }
@@ -1988,7 +2000,7 @@ class ChatCommand {
1988
2000
  }
1989
2001
  console.log('V3 agent workflow completed.');
1990
2002
  }
1991
- if (!this.jsonOutput && !this.directPromptMode && previewGate?.required) {
2003
+ if (!this.jsonOutput && previewGate?.required) {
1992
2004
  if (previewGate.passed) {
1993
2005
  console.log(chalk_1.default.gray(`Template Service preview gate: passed via ${previewGate.backendUrl || 'unknown backend'}`));
1994
2006
  }
@@ -2897,7 +2909,7 @@ class ChatCommand {
2897
2909
  }
2898
2910
  // In direct-prompt mode (--prompt), suppress verbose tool output so
2899
2911
  // only the final answer prints. In interactive mode, show full detail.
2900
- const verbose = !this.jsonOutput && !this.directPromptMode;
2912
+ const verbose = !this.jsonOutput;
2901
2913
  for (const call of toolCalls) {
2902
2914
  if (verbose) {
2903
2915
  console.log(chalk_1.default.cyan(`⚙ Executing: ${call.tool}`));
@@ -76,6 +76,7 @@ const HYPERLOOP_URLS = (0, api_js_1.isServerRuntime)()
76
76
  const CORTEX_WARN_BUDGET_USD = 3.5;
77
77
  const CORTEX_HARD_BUDGET_USD = 5.0;
78
78
  const CORTEX_MAX_ROUNDS = 2;
79
+ const CORTEX_PLATFORM_FEE_PCT = 10;
79
80
  const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
80
81
  class LegionCommand {
81
82
  config;
@@ -209,13 +210,13 @@ class LegionCommand {
209
210
  if (billingQuote.retryAdjustedUsd > CORTEX_WARN_BUDGET_USD) {
210
211
  console.log(chalk_1.default.yellow(`Estimated spend exceeds warning threshold ($${CORTEX_WARN_BUDGET_USD.toFixed(2)}).`));
211
212
  }
212
- if (billingQuote.retryAdjustedUsd > CORTEX_HARD_BUDGET_USD && !options.forceBudget) {
213
- console.log(chalk_1.default.red(`Estimated spend exceeds hard budget ceiling ($${CORTEX_HARD_BUDGET_USD.toFixed(2)}).`));
213
+ let billingGate = await this.evaluateBillingGate(billingQuote);
214
+ this.printCortexQuote(workspace, scan, quote, billingQuote, billingGate);
215
+ if (billingQuote.retryAdjustedUsd > CORTEX_HARD_BUDGET_USD && !options.forceBudget && !billingGate.masterAdminFree) {
216
+ console.log(chalk_1.default.red(`Estimated spend exceeds hard budget ceiling (${CORTEX_HARD_BUDGET_USD.toFixed(2)}).`));
214
217
  console.log(chalk_1.default.yellow('Re-run with --force-budget to continue.'));
215
218
  return;
216
219
  }
217
- let billingGate = await this.evaluateBillingGate(billingQuote);
218
- this.printCortexQuote(workspace, scan, quote, billingQuote, billingGate);
219
220
  if (options.planOnly) {
220
221
  console.log(chalk_1.default.green('Cortex estimator complete (plan-only).'));
221
222
  return;
@@ -326,6 +327,7 @@ class LegionCommand {
326
327
  tier: previousQuote.tier,
327
328
  vigcoinRateUsd: previousQuote.vigcoinRateUsd,
328
329
  vigcoinRequired: retryAdjustedUsd / previousQuote.vigcoinRateUsd,
330
+ platformFeePct: previousQuote.platformFeePct,
329
331
  };
330
332
  }
331
333
  async confirmAdditionalLoopCharge(nextQuote, nextRound, execution) {
@@ -640,7 +642,9 @@ class LegionCommand {
640
642
  const baseUsd = quote.reduce((sum, r) => sum + r.estCostUsd, 0);
641
643
  const marginPctRaw = Number.parseFloat(String(process.env.VIGTHORIA_CORTEX_MARGIN_PCT || process.env.VIGTHORIA_GODMODE_MARGIN_PCT || '10'));
642
644
  const marginPct = Number.isFinite(marginPctRaw) ? Math.max(0, marginPctRaw) : 10;
643
- const finalUsd = baseUsd * (1 + (marginPct / 100));
645
+ const platformFeePct = CORTEX_PLATFORM_FEE_PCT;
646
+ const platformFeeMultiplier = 1 + (platformFeePct / 100);
647
+ const finalUsd = baseUsd * (1 + (marginPct / 100)) * platformFeeMultiplier;
644
648
  // Retry-adjusted estimate: all 7 critical roles can trigger the quality-gate
645
649
  // retry in state_manager.py (adds +2 iterations per degraded role).
646
650
  // Probability of retry per critical role ≈ 45% (from production telemetry).
@@ -651,13 +655,13 @@ class LegionCommand {
651
655
  const retryExtra = quote
652
656
  .filter((r) => CRITICAL_ROLES.has(r.role))
653
657
  .reduce((sum, r) => sum + r.estCostUsd * RETRY_PROB * RETRY_ITER_RATIO, 0);
654
- const retryAdjustedUsd = (baseUsd + retryExtra) * (1 + (marginPct / 100));
658
+ const retryAdjustedUsd = (baseUsd + retryExtra) * (1 + (marginPct / 100)) * platformFeeMultiplier;
655
659
  // Range bounds.
656
660
  const rangeMinUsd = finalUsd; // single pass, no retries
657
661
  const maxRetryExtra = quote
658
662
  .filter((r) => CRITICAL_ROLES.has(r.role))
659
663
  .reduce((sum, r) => sum + r.estCostUsd * RETRY_ITER_RATIO, 0);
660
- const rangeMaxUsd = (baseUsd + maxRetryExtra) * (1 + (marginPct / 100));
664
+ const rangeMaxUsd = (baseUsd + maxRetryExtra) * (1 + (marginPct / 100)) * platformFeeMultiplier;
661
665
  const vigcoinRateRaw = Number.parseFloat(String(process.env.VIGTHORIA_VIGCOIN_USD_RATE || '1'));
662
666
  const vigcoinRateUsd = Number.isFinite(vigcoinRateRaw) && vigcoinRateRaw > 0 ? vigcoinRateRaw : 1;
663
667
  const vigcoinRequired = retryAdjustedUsd / vigcoinRateUsd;
@@ -671,6 +675,7 @@ class LegionCommand {
671
675
  tier,
672
676
  vigcoinRateUsd,
673
677
  vigcoinRequired,
678
+ platformFeePct,
674
679
  };
675
680
  }
676
681
  async evaluateBillingGate(billingQuote) {
@@ -1069,6 +1074,7 @@ class LegionCommand {
1069
1074
  return;
1070
1075
  }
1071
1076
  console.log(chalk_1.default.gray(` Estimated total (USD): $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk_1.default.gray(' (retry-adjusted expected)'));
1077
+ console.log(chalk_1.default.gray(` Platform fee: +${billingQuote.platformFeePct.toFixed(0)}% (already included in all estimates)`));
1072
1078
  console.log(chalk_1.default.gray(` VigCoin rate: 1 VIG = $${billingQuote.vigcoinRateUsd.toFixed(4)}`));
1073
1079
  console.log(chalk_1.default.gray(` VigCoin required: ${billingQuote.vigcoinRequired.toFixed(3)}`));
1074
1080
  if (gate.wallet.vigcoinBalance !== null) {
@@ -1104,7 +1110,9 @@ class LegionCommand {
1104
1110
  console.log();
1105
1111
  console.log(chalk_1.default.white(' Role assignment and estimated cost:'));
1106
1112
  for (const row of quote) {
1107
- console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${row.role.padEnd(11)} ${row.model} $${row.estCostUsd.toFixed(4)}`));
1113
+ const publicModelLabel = row.requestedModel || row.model.replace(/^openrouter:/i, '').split('/').pop() || 'managed-model';
1114
+ const roleEstWithFee = row.estCostUsd * (1 + (billingQuote.platformFeePct / 100));
1115
+ console.log(chalk_1.default.gray(` ${logger_js_1.CH.bullet} ${row.role.padEnd(11)} ${publicModelLabel} $${roleEstWithFee.toFixed(4)}`));
1108
1116
  }
1109
1117
  console.log();
1110
1118
  console.log(chalk_1.default.yellow(` Cost range (single-pass best case): $${billingQuote.rangeMinUsd.toFixed(4)}`));
@@ -1113,6 +1121,7 @@ class LegionCommand {
1113
1121
  console.log(chalk_1.default.gray(' Retry model: 45% chance per critical role triggers quality-gate (+40% iterations per retry).'));
1114
1122
  console.log(chalk_1.default.gray(' A mid-run checkpoint will appear when 70% of the expected estimate is consumed.'));
1115
1123
  console.log(chalk_1.default.gray(' Flow: Estimate -> Isolation -> Parallel Attack -> Synthesis'));
1124
+ console.log(chalk_1.default.gray(` All displayed costs include platform fee (+${billingQuote.platformFeePct.toFixed(0)}%).`));
1116
1125
  console.log();
1117
1126
  this.printBillingGateSummary(billingQuote, gate);
1118
1127
  console.log();
@@ -1224,7 +1233,7 @@ class LegionCommand {
1224
1233
  : 10;
1225
1234
  const baseEstimateUsd = (cortexExecution?.quote || []).reduce((s, q) => s + q.estCostUsd, 0);
1226
1235
  const budgetCheckpointThreshold = cortexExecution
1227
- ? baseEstimateUsd * (1 + marginPctMidrun / 100) * 0.70
1236
+ ? baseEstimateUsd * (1 + marginPctMidrun / 100) * (1 + CORTEX_PLATFORM_FEE_PCT / 100) * 0.70
1228
1237
  : Infinity;
1229
1238
  let accumulatedEstUsd = 0;
1230
1239
  let budgetCheckpointFired = false;
@@ -1274,7 +1283,7 @@ class LegionCommand {
1274
1283
  }
1275
1284
  // Track spend and trigger mid-run budget checkpoint.
1276
1285
  const stepRole = String(evt.step_id || '');
1277
- accumulatedEstUsd += roleQuoteIndex.get(stepRole) || 0;
1286
+ accumulatedEstUsd += (roleQuoteIndex.get(stepRole) || 0) * (1 + CORTEX_PLATFORM_FEE_PCT / 100);
1278
1287
  completedRoleSummaries.push({ role: stepRole, status: String(evt.status || ''), summary: stepSummaryRaw.slice(0, 200) });
1279
1288
  if (!budgetCheckpointFired
1280
1289
  && cortexExecution
@@ -1288,7 +1297,7 @@ class LegionCommand {
1288
1297
  .map((q) => q.role);
1289
1298
  const remainingEstUsd = (cortexExecution.quote || [])
1290
1299
  .filter((q) => remainingRoles.includes(q.role))
1291
- .reduce((s, q) => s + q.estCostUsd, 0);
1300
+ .reduce((s, q) => s + (q.estCostUsd * (1 + CORTEX_PLATFORM_FEE_PCT / 100)), 0);
1292
1301
  console.log();
1293
1302
  console.log(chalk_1.default.bold.yellow(' ━━━ Mid-Run Budget Checkpoint ━━━'));
1294
1303
  console.log(chalk_1.default.gray(` Consumed so far (estimated): $${accumulatedEstUsd.toFixed(4)}`));
@@ -0,0 +1,20 @@
1
+ import { Config } from '../utils/config.js';
2
+ import { Logger } from '../utils/logger.js';
3
+ interface SecurityOptions {
4
+ dir?: string;
5
+ json?: boolean;
6
+ issueId?: string;
7
+ apply?: boolean;
8
+ }
9
+ export declare class SecurityCommand {
10
+ private config;
11
+ private logger;
12
+ constructor(config: Config, logger: Logger);
13
+ private getMcpBaseUrl;
14
+ private resolveDir;
15
+ private execute;
16
+ scan(options: SecurityOptions): Promise<void>;
17
+ score(options: SecurityOptions): Promise<void>;
18
+ fix(options: SecurityOptions): Promise<void>;
19
+ }
20
+ export {};
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SecurityCommand = void 0;
7
+ const axios_1 = __importDefault(require("axios"));
8
+ const path_1 = __importDefault(require("path"));
9
+ class SecurityCommand {
10
+ config;
11
+ logger;
12
+ constructor(config, logger) {
13
+ this.config = config;
14
+ this.logger = logger;
15
+ }
16
+ getMcpBaseUrl() {
17
+ const fromEnv = process.env.VIGTHORIA_MCP_URL || process.env.MCP_SERVER_URL;
18
+ if (fromEnv && fromEnv.trim()) {
19
+ return fromEnv.replace(/\/$/, '');
20
+ }
21
+ return 'http://127.0.0.1:4008';
22
+ }
23
+ resolveDir(dir) {
24
+ return path_1.default.resolve(dir || process.cwd());
25
+ }
26
+ async execute(tool, parameters) {
27
+ const baseUrl = this.getMcpBaseUrl();
28
+ const response = await axios_1.default.post(`${baseUrl}/mcp/execute`, {
29
+ tool,
30
+ parameters,
31
+ context: {
32
+ source: 'vigthoria-cli',
33
+ client: 'vsec'
34
+ }
35
+ }, {
36
+ timeout: 120000,
37
+ validateStatus: (status) => status >= 200 && status < 500,
38
+ });
39
+ if (!response.data?.success) {
40
+ const errorText = response.data?.error || `Tool ${tool} failed`;
41
+ throw new Error(errorText);
42
+ }
43
+ const result = response.data?.result || {};
44
+ return result.result || result;
45
+ }
46
+ async scan(options) {
47
+ const dir = this.resolveDir(options.dir);
48
+ const result = await this.execute('security_scan', { dir });
49
+ if (options.json) {
50
+ console.log(JSON.stringify(result, null, 2));
51
+ return;
52
+ }
53
+ this.logger.info(`Security scan complete for: ${dir}`);
54
+ this.logger.info(`Score: ${result.score}/100 (${result.grade})`);
55
+ this.logger.info(`Files scanned: ${result.scannedFiles}`);
56
+ this.logger.info(`Issues found: ${result.issueCount}`);
57
+ const issues = Array.isArray(result.issues) ? result.issues : [];
58
+ for (const issue of issues.slice(0, 20)) {
59
+ console.log(`- [${issue.severity}] ${issue.id} ${issue.file}:${issue.line} ${issue.message}`);
60
+ }
61
+ if (issues.length > 20) {
62
+ console.log(`... ${issues.length - 20} more issues`);
63
+ }
64
+ }
65
+ async score(options) {
66
+ const dir = this.resolveDir(options.dir);
67
+ const result = await this.execute('security_score', { dir });
68
+ if (options.json) {
69
+ console.log(JSON.stringify(result, null, 2));
70
+ return;
71
+ }
72
+ this.logger.info(`Security score for ${dir}`);
73
+ this.logger.info(`${result.score}/100 (${result.grade})`);
74
+ this.logger.info(result.summary || '');
75
+ }
76
+ async fix(options) {
77
+ const dir = this.resolveDir(options.dir);
78
+ const result = await this.execute('security_fix', {
79
+ dir,
80
+ issue_id: options.issueId,
81
+ confirm: Boolean(options.apply),
82
+ });
83
+ if (options.json) {
84
+ console.log(JSON.stringify(result, null, 2));
85
+ return;
86
+ }
87
+ this.logger.info(`Security fix plan for ${dir}`);
88
+ this.logger.info(result.message || 'Fix plan generated');
89
+ const actions = Array.isArray(result.plannedActions) ? result.plannedActions : [];
90
+ for (const action of actions.slice(0, 25)) {
91
+ console.log(`- ${action.issue_id} ${action.file}: ${action.action}`);
92
+ }
93
+ if (actions.length > 25) {
94
+ console.log(`... ${actions.length - 25} more actions`);
95
+ }
96
+ }
97
+ }
98
+ exports.SecurityCommand = SecurityCommand;
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ const history_js_1 = require("./commands/history.js");
76
76
  const replay_js_1 = require("./commands/replay.js");
77
77
  const fork_js_1 = require("./commands/fork.js");
78
78
  const cancel_js_1 = require("./commands/cancel.js");
79
+ const security_js_1 = require("./commands/security.js");
79
80
  const config_js_2 = require("./utils/config.js");
80
81
  const logger_js_1 = require("./utils/logger.js");
81
82
  const chalk_1 = __importDefault(require("chalk"));
@@ -124,7 +125,7 @@ function getVersion() {
124
125
  console.error(chalk_1.default.gray(`Unable to read package version: ${message}`));
125
126
  }
126
127
  }
127
- return '1.9.3';
128
+ return '0.0.0';
128
129
  }
129
130
  function validateReleaseMetadata() {
130
131
  try {
@@ -150,20 +151,15 @@ function validateReleaseMetadata() {
150
151
  'vigthoria edit',
151
152
  'vigthoria update',
152
153
  'vigthoria doctor',
153
- 'version 1.9.3',
154
- 'vigthoria-cli-1.9.3.tgz',
155
154
  ];
156
155
  return (pkg.name === 'vigthoria-cli' &&
157
- pkg.version === '1.9.3' &&
156
+ typeof pkg.version === 'string' &&
158
157
  pkg.description === 'Vigthoria Coder CLI - AI-powered terminal coding assistant' &&
159
158
  pkg.main === 'dist/index.js' &&
160
159
  bins.vigthoria === 'dist/index.js' &&
161
160
  bins.vig === 'dist/index.js' &&
162
161
  bins['vigthoria-chat'] === 'dist/index.js' &&
163
- requiredReadmePhrases.every((phrase) => readme.includes(phrase)) &&
164
- /Version\s+1\.9\.3/i.test(readme) &&
165
- !readme.includes('vigthoria-cli-1.9.2.tgz') &&
166
- !/version\s+1\.9\.2/i.test(readme));
162
+ requiredReadmePhrases.every((phrase) => readme.includes(phrase)));
167
163
  }
168
164
  catch (error) {
169
165
  const message = error instanceof Error ? error.message : String(error);
@@ -173,12 +169,6 @@ function validateReleaseMetadata() {
173
169
  return false;
174
170
  }
175
171
  }
176
- const VERSION = getVersion();
177
- const VIGTHORIA_DEFAULT_MANIFEST_URL = process.env.VIGTHORIA_UPDATE_MANIFEST_URL || "https://coder.vigthoria.io/releases/manifest.json";
178
- /**
179
- * Compare semantic versions properly
180
- * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
181
- */
182
172
  function compareVersions(v1, v2) {
183
173
  const parts1 = v1.split('.').map(Number);
184
174
  const parts2 = v2.split('.').map(Number);
@@ -192,6 +182,8 @@ function compareVersions(v1, v2) {
192
182
  }
193
183
  return 0;
194
184
  }
185
+ const VERSION = getVersion();
186
+ const VIGTHORIA_DEFAULT_MANIFEST_URL = process.env.VIGTHORIA_UPDATE_MANIFEST_URL || "https://coder.vigthoria.io/releases/manifest.json";
195
187
  function resolveManifestEntry(manifest, channel) {
196
188
  const normalized = String(channel || 'stable').trim() || 'stable';
197
189
  if (manifest.channels && manifest.channels[normalized]) {
@@ -476,7 +468,7 @@ async function main(args) {
476
468
  if (invokedBinaryName === 'vigthoria-chat') {
477
469
  const knownCommands = new Set([
478
470
  'chat', 'chat-resume', 'agent', 'edit', 'generate', 'explain', 'fix', 'review', 'cancel',
479
- 'hub', 'repo', 'deploy', 'operator', 'workflow', 'flow', 'login', 'logout', 'status', 'config', 'update',
471
+ 'hub', 'repo', 'deploy', 'operator', 'workflow', 'flow', 'security', 'vsec', 'login', 'logout', 'status', 'config', 'update',
480
472
  '--help', '-h', '--version', '-V', 'help', 'version',
481
473
  ]);
482
474
  if (!firstArg || firstArg.startsWith('-') || !knownCommands.has(firstArg)) {
@@ -647,6 +639,40 @@ async function main(args) {
647
639
  const bridge = new bridge_js_1.BridgeCommand(config, logger);
648
640
  await bridge.status();
649
641
  });
642
+ // Security command group - Security DevOps scans and fix plans
643
+ const securityCommand = program
644
+ .command('security')
645
+ .alias('vsec')
646
+ .description('Run security scans, scores, and fix plans via Vigthoria MCP security tools');
647
+ securityCommand
648
+ .command('scan')
649
+ .description('Scan project for security issues')
650
+ .option('-d, --dir <path>', 'Directory to scan (default: current directory)')
651
+ .option('--json', 'Emit JSON output', false)
652
+ .action(async (options) => {
653
+ const security = new security_js_1.SecurityCommand(config, logger);
654
+ await security.scan({ dir: options.dir, json: options.json });
655
+ });
656
+ securityCommand
657
+ .command('score')
658
+ .description('Calculate project security score')
659
+ .option('-d, --dir <path>', 'Directory to score (default: current directory)')
660
+ .option('--json', 'Emit JSON output', false)
661
+ .action(async (options) => {
662
+ const security = new security_js_1.SecurityCommand(config, logger);
663
+ await security.score({ dir: options.dir, json: options.json });
664
+ });
665
+ securityCommand
666
+ .command('fix')
667
+ .description('Generate security fix plan')
668
+ .option('-d, --dir <path>', 'Directory to inspect (default: current directory)')
669
+ .option('-i, --issue-id <id>', 'Single issue id to target')
670
+ .option('--apply', 'Request auto-apply mode (safe mode still requires explicit patching)', false)
671
+ .option('--json', 'Emit JSON output', false)
672
+ .action(async (options) => {
673
+ const security = new security_js_1.SecurityCommand(config, logger);
674
+ await security.fix({ dir: options.dir, issueId: options.issueId, apply: options.apply, json: options.json });
675
+ });
650
676
  // Edit command - Edit files with AI
651
677
  program
652
678
  .command('edit <file>')