vigthoria-cli 1.13.8 → 1.13.10

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.
@@ -18,6 +18,7 @@ import { RepoCommand } from './repo.js';
18
18
  import { ReviewCommand } from './review.js';
19
19
  import { WorkflowCommand } from './workflow.js';
20
20
  import { V4Command } from './v4.js';
21
+ import { WalletCommand } from './wallet.js';
21
22
  function isRegisterableCommand(value) {
22
23
  return Boolean(value && typeof value === 'object');
23
24
  }
@@ -136,6 +137,10 @@ const commandRegistry = [
136
137
  name: 'history',
137
138
  handler: createClassRegistrar(HistoryCommand),
138
139
  },
140
+ {
141
+ name: 'wallet',
142
+ handler: createClassRegistrar(WalletCommand),
143
+ },
139
144
  {
140
145
  name: 'replay',
141
146
  handler: createClassRegistrar(ReplayCommand),
@@ -144,7 +144,7 @@ export class LegionCommand {
144
144
  console.log(chalk.gray(' --repro-cmd <cmd> Run a local reproducibility command before spend'));
145
145
  console.log(chalk.gray(' --expect-repro-fail Require repro command to fail before execution'));
146
146
  console.log(chalk.gray(' --approve Skip initial confirmation prompt and execute'));
147
- console.log(chalk.gray(' --auto-charge Attempt direct VigCoin top-up when balance is low'));
147
+ console.log(chalk.gray(' --auto-charge Charge the canonical PAYG wallet when balance is sufficient'));
148
148
  console.log(chalk.gray(' --timeout <sec> Abort remote execution if no result within timeout (default: 120)'));
149
149
  return;
150
150
  }
@@ -195,7 +195,7 @@ export class LegionCommand {
195
195
  if (!billingGate.canProceed) {
196
196
  const resolved = await this.resolveBillingInsufficientFunds(currentQuote, billingGate, options);
197
197
  if (!resolved) {
198
- console.log(chalk.yellow('Cortex cancelled due to insufficient VigCoin balance.'));
198
+ console.log(chalk.yellow('Cortex cancelled due to insufficient PAYG credit balance.'));
199
199
  return;
200
200
  }
201
201
  billingGate = await this.evaluateBillingGate(currentQuote);
@@ -301,7 +301,7 @@ export class LegionCommand {
301
301
  console.log(chalk.gray(` Next round: ${nextRound}`));
302
302
  console.log(chalk.gray(` Failed step: ${execution.failedStepId || 'unknown'} (${execution.failedWorker || 'unknown'})`));
303
303
  console.log(chalk.gray(` Additional estimate: $${nextQuote.finalUsd.toFixed(4)} / ${nextQuote.vigcoinRequired.toFixed(3)} VIG`));
304
- const answer = (await rl.question('Confirm additional VigCoin deduction and continue? (y/N): ')).trim().toLowerCase();
304
+ const answer = (await rl.question('Confirm additional PAYG credit deduction and continue? (y/N): ')).trim().toLowerCase();
305
305
  return answer === 'y' || answer === 'yes';
306
306
  }
307
307
  finally {
@@ -844,7 +844,7 @@ export class LegionCommand {
844
844
  available: true,
845
845
  vigcoinBalance: 0,
846
846
  source: 'forced_low_credit',
847
- purchaseUrl: `${baseUrl}/music/store#vigcoins`,
847
+ purchaseUrl: 'https://hub.vigthoria.io/credits',
848
848
  };
849
849
  }
850
850
  if (forcedBalanceRaw) {
@@ -854,7 +854,7 @@ export class LegionCommand {
854
854
  available: true,
855
855
  vigcoinBalance: forced,
856
856
  source: 'forced_balance',
857
- purchaseUrl: `${baseUrl}/music/store#vigcoins`,
857
+ purchaseUrl: 'https://hub.vigthoria.io/credits',
858
858
  };
859
859
  }
860
860
  }
@@ -892,7 +892,7 @@ export class LegionCommand {
892
892
  available: true,
893
893
  vigcoinBalance: balance,
894
894
  source: endpoint,
895
- purchaseUrl: this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || `${baseUrl}/music/store#vigcoins`,
895
+ purchaseUrl: this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || 'https://hub.vigthoria.io/credits',
896
896
  };
897
897
  }
898
898
  catch (err) {
@@ -904,7 +904,7 @@ export class LegionCommand {
904
904
  available: false,
905
905
  vigcoinBalance: null,
906
906
  source: null,
907
- purchaseUrl: `${baseUrl}/music/store#vigcoins`,
907
+ purchaseUrl: 'https://hub.vigthoria.io/credits',
908
908
  error: sawUnauthorized
909
909
  ? 'Wallet session expired or unauthorized. Run: vigthoria login'
910
910
  : 'Wallet endpoint unavailable from current gateway session',
@@ -913,12 +913,10 @@ export class LegionCommand {
913
913
  async attemptDirectCharge(vigcoinNeeded) {
914
914
  const baseUrl = this.getBillingBaseUrl();
915
915
  const headers = this.getHeaders();
916
- const amount = Math.max(1, Math.ceil(vigcoinNeeded));
916
+ const amount = Math.round(Math.max(0.01, vigcoinNeeded) * 1000000) / 1000000;
917
917
  const requestId = `cortex_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
918
918
  const chargePayloads = [
919
919
  { endpoint: '/api/viagen6/vigcoin/charge', body: { amount, reason: 'cortex_legion', requestId } },
920
- { endpoint: '/api/wallet/charge', body: { amount, currency: 'VIGCOIN', reason: 'cortex_legion', requestId } },
921
- { endpoint: '/api/billing/topup', body: { vigcoin: amount, reason: 'cortex_legion', requestId } },
922
920
  ];
923
921
  for (const attempt of chargePayloads) {
924
922
  try {
@@ -929,11 +927,11 @@ export class LegionCommand {
929
927
  body: JSON.stringify(attempt.body),
930
928
  });
931
929
  const payload = await response.json().catch((err) => {
932
- this.logger.warn(this.formatLegionError(`direct VigCoin charge response ${attempt.endpoint}`, err));
930
+ this.logger.warn(this.formatLegionError(`PAYG wallet charge response ${attempt.endpoint}`, err));
933
931
  return {};
934
932
  });
935
933
  const payloadObj = (payload && typeof payload === 'object') ? payload : {};
936
- const checkoutUrl = this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || `${baseUrl}/music/store#vigcoins`;
934
+ const checkoutUrl = this.getPurchaseUrlFromPayload(baseUrl, payloadObj) || 'https://hub.vigthoria.io/credits';
937
935
  if (!response.ok) {
938
936
  continue;
939
937
  }
@@ -948,13 +946,13 @@ export class LegionCommand {
948
946
  };
949
947
  }
950
948
  catch (err) {
951
- this.logger.warn(this.formatLegionError(`direct VigCoin charge request ${attempt.endpoint}`, err));
949
+ this.logger.warn(this.formatLegionError(`PAYG wallet charge request ${attempt.endpoint}`, err));
952
950
  continue;
953
951
  }
954
952
  }
955
953
  return {
956
954
  ok: false,
957
- checkoutUrl: `${baseUrl}/music/store#vigcoins`,
955
+ checkoutUrl: 'https://hub.vigthoria.io/credits',
958
956
  note: 'No direct charge endpoint accepted this request',
959
957
  };
960
958
  }
@@ -962,18 +960,19 @@ export class LegionCommand {
962
960
  if (gate.masterAdminFree) {
963
961
  return true;
964
962
  }
965
- const spinner = createSpinner('Charging VigCoin wallet for Cortex execution...').start();
963
+ const spinner = createSpinner('Charging Vigthoria Credits for Cortex execution...').start();
966
964
  const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired);
967
965
  spinner.stop();
968
966
  if (!result.ok) {
969
967
  console.log(chalk.red(result.note || 'Wallet charge failed.'));
970
- console.log(chalk.yellow(`Complete purchase first: ${result.checkoutUrl || `${this.getBillingBaseUrl()}/music/store#vigcoins`}`));
968
+ console.log(chalk.yellow(`Complete purchase first: ${result.checkoutUrl || 'https://hub.vigthoria.io/credits'}`));
971
969
  return false;
972
970
  }
973
- console.log(chalk.green('Wallet charged for Cortex execution.'));
971
+ console.log(chalk.green('PAYG wallet charged for Cortex execution.'));
974
972
  return true;
975
973
  }
976
974
  async resolveBillingInsufficientFunds(billingQuote, gate, options) {
975
+ void options;
977
976
  this.printBillingGateSummary(billingQuote, gate);
978
977
  if (!gate.wallet.available) {
979
978
  console.log(chalk.red('Unable to verify wallet balance from server. Execution is blocked.'));
@@ -982,20 +981,8 @@ export class LegionCommand {
982
981
  }
983
982
  return false;
984
983
  }
985
- if (options.autoCharge) {
986
- const spinner = createSpinner('Attempting direct wallet charge...').start();
987
- const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired - (gate.wallet.vigcoinBalance || 0));
988
- spinner.stop();
989
- if (result.ok) {
990
- console.log(chalk.green('Direct charge succeeded. Re-checking wallet balance...'));
991
- return true;
992
- }
993
- console.log(chalk.yellow(result.note || 'Direct charge did not complete.'));
994
- console.log(chalk.yellow(`Complete purchase first: ${result.checkoutUrl || gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
995
- return false;
996
- }
997
984
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
998
- console.log(chalk.yellow('Low balance detected in non-interactive mode. Re-run with --auto-charge or top up first.'));
985
+ console.log(chalk.yellow('Low balance detected. Top up the PAYG wallet before retrying.'));
999
986
  if (gate.wallet.purchaseUrl) {
1000
987
  console.log(chalk.gray(`Billing portal: ${gate.wallet.purchaseUrl}`));
1001
988
  }
@@ -1003,21 +990,9 @@ export class LegionCommand {
1003
990
  }
1004
991
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1005
992
  try {
1006
- const answer = (await rl.question('VigCoin low. Choose action: [c]harge now, [p]urchase first, [n] cancel: ')).trim().toLowerCase();
1007
- if (answer === 'c' || answer === 'charge') {
1008
- const spinner = createSpinner('Attempting direct wallet charge...').start();
1009
- const result = await this.attemptDirectCharge(billingQuote.vigcoinRequired - (gate.wallet.vigcoinBalance || 0));
1010
- spinner.stop();
1011
- if (result.ok) {
1012
- console.log(chalk.green('Direct charge succeeded. Re-checking wallet balance...'));
1013
- return true;
1014
- }
1015
- console.log(chalk.yellow(result.note || 'Direct charge did not complete.'));
1016
- console.log(chalk.yellow(`Complete purchase first: ${result.checkoutUrl || gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
1017
- return false;
1018
- }
993
+ const answer = (await rl.question('PAYG credits low. Choose action: [p]urchase first, [n] cancel: ')).trim().toLowerCase();
1019
994
  if (answer === 'p' || answer === 'purchase') {
1020
- console.log(chalk.yellow(`Purchase VigCoin first: ${gate.wallet.purchaseUrl || `${this.getBillingBaseUrl()}/billing`}`));
995
+ console.log(chalk.yellow(`Purchase Vigthoria Credits first: ${gate.wallet.purchaseUrl || 'https://hub.vigthoria.io/credits'}`));
1021
996
  return false;
1022
997
  }
1023
998
  return false;
@@ -1035,8 +1010,8 @@ export class LegionCommand {
1035
1010
  }
1036
1011
  console.log(chalk.gray(` Estimated total (USD): $${billingQuote.retryAdjustedUsd.toFixed(4)}`) + chalk.gray(' (retry-adjusted expected)'));
1037
1012
  console.log(chalk.gray(` Platform fee: +${billingQuote.platformFeePct.toFixed(0)}% (already included in all estimates)`));
1038
- console.log(chalk.gray(` VigCoin rate: 1 VIG = $${billingQuote.vigcoinRateUsd.toFixed(4)}`));
1039
- console.log(chalk.gray(` VigCoin required: ${billingQuote.vigcoinRequired.toFixed(3)}`));
1013
+ console.log(chalk.gray(` Credit rate: 1 credit = $${billingQuote.vigcoinRateUsd.toFixed(4)}`));
1014
+ console.log(chalk.gray(` Credits required: ${billingQuote.vigcoinRequired.toFixed(3)}`));
1040
1015
  if (gate.wallet.vigcoinBalance !== null) {
1041
1016
  const color = gate.wallet.vigcoinBalance >= billingQuote.vigcoinRequired ? chalk.green : chalk.red;
1042
1017
  console.log(chalk.gray(' Wallet balance: ') + color(gate.wallet.vigcoinBalance.toFixed(3)) + (gate.wallet.source ? chalk.gray(` (source: ${gate.wallet.source})`) : ''));
@@ -1,12 +1,22 @@
1
1
  import { execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
2
3
  import { existsSync, readFileSync } from 'node:fs';
4
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
5
+ import { tmpdir } from 'node:os';
3
6
  import * as path from 'node:path';
4
7
  import { fileURLToPath } from 'node:url';
5
8
  import { promisify } from 'node:util';
6
- import { installUpdateWindows } from '../utils/tools.js';
7
9
  // ESM shim — __filename is unavailable under "type": "module".
8
10
  const __filename = fileURLToPath(import.meta.url);
9
11
  const execFileAsync = promisify(execFile);
12
+ const DEFAULT_UPDATE_MANIFEST = 'https://extension.vigthoria.io/downloads/manifest.json';
13
+ const TRUSTED_RELEASE_HOSTS = new Set([
14
+ 'extension.vigthoria.io',
15
+ 'landing.vigthoria.io',
16
+ 'cli.vigthoria.io',
17
+ 'coder.vigthoria.io',
18
+ 'market.vigthoria.io',
19
+ ]);
10
20
  function markSuccessExit() {
11
21
  process.exitCode = 0;
12
22
  }
@@ -99,6 +109,53 @@ async function getLatestVersion(packageName) {
99
109
  throw new Error(`Unable to check the latest published version: ${message}`);
100
110
  }
101
111
  }
112
+ function validateTrustedHttpsUrl(value, label) {
113
+ let parsed;
114
+ try {
115
+ parsed = new URL(value);
116
+ }
117
+ catch {
118
+ throw new Error(`${label} is not a valid URL`);
119
+ }
120
+ if (parsed.protocol !== 'https:' || !TRUSTED_RELEASE_HOSTS.has(parsed.hostname)) {
121
+ throw new Error(`${label} must use HTTPS on an approved Vigthoria release host`);
122
+ }
123
+ return parsed;
124
+ }
125
+ async function getStableRelease() {
126
+ const manifestUrl = process.env.VIGTHORIA_UPDATE_MANIFEST_URL
127
+ || DEFAULT_UPDATE_MANIFEST;
128
+ validateTrustedHttpsUrl(manifestUrl, 'Update manifest URL');
129
+ const controller = new AbortController();
130
+ const timeout = setTimeout(() => controller.abort(), 20_000);
131
+ try {
132
+ const response = await fetch(manifestUrl, {
133
+ headers: { Accept: 'application/json' },
134
+ redirect: 'follow',
135
+ signal: controller.signal,
136
+ });
137
+ if (!response.ok) {
138
+ throw new Error(`manifest returned HTTP ${response.status}`);
139
+ }
140
+ validateTrustedHttpsUrl(response.url || manifestUrl, 'Resolved manifest URL');
141
+ const document = await response.json();
142
+ const stable = document.channels?.stable;
143
+ const version = String(stable?.version || '').trim();
144
+ const url = String(stable?.url || '').trim();
145
+ const sha256 = String(stable?.sha256 || '').trim().toLowerCase();
146
+ if (!/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/.test(version)) {
147
+ throw new Error('stable manifest version is invalid');
148
+ }
149
+ validateTrustedHttpsUrl(url, 'Stable release URL');
150
+ if (!/^[a-f0-9]{64}$/.test(sha256)) {
151
+ throw new Error('stable release SHA-256 is missing or invalid');
152
+ }
153
+ return { version, url, sha256 };
154
+ }
155
+ finally {
156
+ clearTimeout(timeout);
157
+ }
158
+ }
102
159
  async function getVersionInfo() {
103
160
  const packageJson = readOwnPackageJson();
104
161
  const packageName = packageJson.name;
@@ -106,16 +163,28 @@ async function getVersionInfo() {
106
163
  if (!packageName || !current) {
107
164
  throw new Error('The CLI package metadata must include both name and version.');
108
165
  }
109
- const latest = await getLatestVersion(packageName);
166
+ let release;
167
+ let latest;
168
+ try {
169
+ release = await getStableRelease();
170
+ latest = release.version;
171
+ }
172
+ catch (manifestError) {
173
+ const reason = manifestError instanceof Error
174
+ ? manifestError.message
175
+ : String(manifestError);
176
+ console.warn(`Verified release manifest unavailable (${reason}); checking npm.`);
177
+ latest = await getLatestVersion(packageName);
178
+ }
110
179
  return {
111
180
  current,
112
181
  latest,
113
182
  packageName,
114
183
  updateAvailable: compareVersions(current, latest) < 0,
184
+ release,
115
185
  };
116
186
  }
117
- function resolveInstallCommand(packageManager, packageName, installGlobal) {
118
- const target = `${packageName}@latest`;
187
+ function resolveInstallTargetCommand(packageManager, target, installGlobal) {
119
188
  switch (packageManager) {
120
189
  case 'pnpm':
121
190
  return { command: 'pnpm', args: installGlobal ? ['add', '--global', target] : ['add', '-D', target] };
@@ -128,6 +197,9 @@ function resolveInstallCommand(packageManager, packageName, installGlobal) {
128
197
  return { command: 'npm', args: installGlobal ? ['install', '--global', target] : ['install', '--save-dev', target] };
129
198
  }
130
199
  }
200
+ function resolveInstallCommand(packageManager, packageName, installGlobal) {
201
+ return resolveInstallTargetCommand(packageManager, `${packageName}@latest`, installGlobal);
202
+ }
131
203
  function formatCommand(command) {
132
204
  return [command.command, ...command.args].map(quoteCmdArg).join(' ');
133
205
  }
@@ -135,6 +207,39 @@ async function runInstall(packageManager, packageName, installGlobal) {
135
207
  const installCommand = resolveInstallCommand(packageManager, packageName, installGlobal);
136
208
  await execPackageManager(installCommand.command, installCommand.args, 120_000);
137
209
  }
210
+ async function downloadVerifiedRelease(release) {
211
+ const directory = await mkdtemp(path.join(tmpdir(), 'vigthoria-cli-update-'));
212
+ const archive = path.join(directory, `vigthoria-cli-${release.version}.tgz`);
213
+ try {
214
+ const controller = new AbortController();
215
+ const timeout = setTimeout(() => controller.abort(), 60_000);
216
+ let response;
217
+ try {
218
+ response = await fetch(release.url, {
219
+ redirect: 'follow',
220
+ signal: controller.signal,
221
+ });
222
+ }
223
+ finally {
224
+ clearTimeout(timeout);
225
+ }
226
+ if (!response.ok) {
227
+ throw new Error(`release download returned HTTP ${response.status}`);
228
+ }
229
+ validateTrustedHttpsUrl(response.url || release.url, 'Resolved release URL');
230
+ const bytes = Buffer.from(await response.arrayBuffer());
231
+ const digest = createHash('sha256').update(bytes).digest('hex');
232
+ if (digest !== release.sha256) {
233
+ throw new Error(`release checksum mismatch (expected ${release.sha256}, received ${digest})`);
234
+ }
235
+ await writeFile(archive, bytes, { mode: 0o600 });
236
+ return { directory, archive };
237
+ }
238
+ catch (error) {
239
+ await rm(directory, { recursive: true, force: true });
240
+ throw error;
241
+ }
242
+ }
138
243
  export async function updateCommand(options = {}) {
139
244
  try {
140
245
  const info = await getVersionInfo();
@@ -152,26 +257,21 @@ export async function updateCommand(options = {}) {
152
257
  }
153
258
  const packageManager = options.packageManager ?? 'npm';
154
259
  const installGlobal = options.global ?? true;
155
- console.log(`Installing ${info.packageName}@latest with ${packageManager}...`);
156
- if (process.platform === 'win32') {
157
- const installerResult = await installUpdateWindows();
158
- if (!installerResult.success && installerResult.error === 'ENOENT') {
159
- const fallbackCommand = resolveInstallCommand(packageManager, info.packageName, installGlobal);
160
- console.warn('Windows update installer was not found (ENOENT).');
161
- console.warn('The bundled Windows installer may be missing from this installation.');
162
- console.warn(`Manual installation command: ${formatCommand(fallbackCommand)}`);
163
- console.warn('If automatic fallback fails, run the command above manually or download the latest Windows installer from the Vigthoria release page.');
164
- await execPackageManager(fallbackCommand.command, fallbackCommand.args, 120_000);
165
- console.log('Package manager installation finished successfully.');
166
- }
167
- else if (!installerResult.success) {
168
- throw new Error(installerResult.error || 'Windows installer failed to start.');
260
+ if (info.release) {
261
+ console.log(`Installing checksum-verified ${info.packageName}@${info.release.version} `
262
+ + `with ${packageManager}...`);
263
+ const downloaded = await downloadVerifiedRelease(info.release);
264
+ try {
265
+ const installCommand = resolveInstallTargetCommand(packageManager, downloaded.archive, installGlobal);
266
+ await execPackageManager(installCommand.command, installCommand.args, 120_000);
169
267
  }
170
- else {
171
- console.log('Windows installer started successfully. Complete the installer prompts to finish updating.');
268
+ finally {
269
+ await rm(downloaded.directory, { recursive: true, force: true });
172
270
  }
271
+ console.log('Verified release installation finished successfully.');
173
272
  }
174
273
  else {
274
+ console.log(`Installing ${info.packageName}@latest with ${packageManager}...`);
175
275
  await runInstall(packageManager, info.packageName, installGlobal);
176
276
  console.log('Package manager installation finished successfully.');
177
277
  }
@@ -5,6 +5,7 @@
5
5
  * vigthoria wallet history [--n 20] — recent transactions
6
6
  * vigthoria wallet cloud-status — show cloud access and model pricing
7
7
  */
8
+ import type { Command } from 'commander';
8
9
  export interface WalletOptions {
9
10
  n?: number;
10
11
  json?: boolean;
@@ -13,6 +14,7 @@ export declare class WalletCommand {
13
14
  private config;
14
15
  private logger;
15
16
  constructor();
17
+ register(program: Command): void;
16
18
  private getBaseUrl;
17
19
  private getToken;
18
20
  private getRefreshToken;
@@ -15,6 +15,31 @@ export class WalletCommand {
15
15
  this.config = new Config();
16
16
  this.logger = new Logger();
17
17
  }
18
+ register(program) {
19
+ const wallet = program
20
+ .command('wallet')
21
+ .description('Inspect monetary Vigthoria Credits used for Cloud, API, Video, and other PAYG services');
22
+ wallet
23
+ .command('balance')
24
+ .description('Show purchased and paid-subscription credit balances')
25
+ .option('--json', 'Emit machine-readable JSON output', false)
26
+ .action(async (options) => this.showBalance(options));
27
+ wallet
28
+ .command('history')
29
+ .description('Show monetary wallet transactions')
30
+ .option('-n, --n <count>', 'Maximum transactions to show', '20')
31
+ .option('--json', 'Emit machine-readable JSON output', false)
32
+ .action(async (options) => this.showHistory({
33
+ n: Number(options.n || 20),
34
+ json: options.json,
35
+ }));
36
+ wallet
37
+ .command('cloud-status')
38
+ .description('Show PAYG cloud eligibility and model pricing')
39
+ .option('--json', 'Emit machine-readable JSON output', false)
40
+ .action(async (options) => this.showCloudStatus(options));
41
+ wallet.action(async () => this.showBalance());
42
+ }
18
43
  getBaseUrl() {
19
44
  return String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
20
45
  }
@@ -99,16 +124,21 @@ export class WalletCommand {
99
124
  return;
100
125
  }
101
126
  console.log('');
102
- console.log(chalk.bold.cyan(' VigCoin Wallet'));
127
+ console.log(chalk.bold.cyan(' Vigthoria PAYG Wallet'));
103
128
  console.log(chalk.gray(' ─────────────────────────────'));
104
- console.log(` Balance: ${chalk.bold.yellow(data.balance.toLocaleString())} ⓥ`);
105
- console.log(` Lifetime earned: ${chalk.green(data.lifetimeEarned.toLocaleString())} ⓥ`);
106
- console.log(` Lifetime spent: ${chalk.red(data.lifetimeSpent.toLocaleString())} ⓥ`);
129
+ console.log(` PAYG balance: ${chalk.bold.yellow(data.balance.toLocaleString())} credits`);
130
+ const purchased = Number(data.purchasedBalance || 0);
131
+ const subscription = Number(data.subscriptionBalance || 0);
132
+ console.log(` Purchased: ${chalk.green(purchased.toLocaleString())} credits`);
133
+ console.log(` Paid subscription: ${chalk.cyan(subscription.toLocaleString())} credits`);
134
+ console.log(` Lifetime funded: ${chalk.green(data.lifetimeEarned.toLocaleString())} credits`);
135
+ console.log(` Lifetime PAYG spend: ${chalk.red(data.lifetimeSpent.toLocaleString())} credits`);
107
136
  if (data.lastUpdated) {
108
137
  console.log(` Last updated: ${chalk.gray(new Date(data.lastUpdated).toLocaleString())}`);
109
138
  }
110
139
  console.log('');
111
- console.log(chalk.gray(' Top up at: https://hub.vigthoria.io/credits'));
140
+ console.log(chalk.gray(' Music Points and other promotional rewards are product-only and are not included.'));
141
+ console.log(chalk.gray(' Top up monetary credits at: https://hub.vigthoria.io/credits'));
112
142
  console.log('');
113
143
  }
114
144
  catch (err) {
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ import { ChatCommand } from './commands/chat.js';
20
20
  import { EditCommand } from './commands/edit.js';
21
21
  import { GenerateCommand } from './commands/generate.js';
22
22
  import { ExplainCommand } from './commands/explain.js';
23
- import { handleLogin, handleLogout, statusAction } from './commands/auth.js';
23
+ import { handleLogin, handleLogout, registerAuthCommands, statusAction } from './commands/auth.js';
24
24
  import { ConfigCommand } from './commands/config.js';
25
25
  import { ReviewCommand } from './commands/review.js';
26
26
  import { HubCommand } from './commands/hub.js';
@@ -39,6 +39,7 @@ import { ForkCommand } from './commands/fork.js';
39
39
  import { CancelCommand } from './commands/cancel.js';
40
40
  import { BackgroundCommand } from './commands/background.js';
41
41
  import { SecurityCommand } from './commands/security.js';
42
+ import { WalletCommand } from './commands/wallet.js';
42
43
  import { V4Command } from './commands/v4.js';
43
44
  import { runV4Menu } from './commands/v4-menu.js';
44
45
  import { Config } from './utils/config.js';
@@ -51,6 +52,7 @@ import { fileURLToPath } from 'url';
51
52
  import { createHash } from 'crypto';
52
53
  import axios from 'axios';
53
54
  import { renderDynamicHelp, selectInteractiveCommand } from './utils/command-menu.js';
55
+ import { isIOSEnvironment, resolveClientOsSlug } from './utils/clientManifest.js';
54
56
  // ESM shim — TypeScript emits ESM under "type": "module", so __dirname
55
57
  // is not available natively. Restore the CommonJS-style helper so the
56
58
  // pre-existing package-discovery logic continues to work unchanged.
@@ -285,6 +287,23 @@ async function downloadFile(url, targetPath) {
285
287
  });
286
288
  fs.writeFileSync(targetPath, Buffer.from(response.data));
287
289
  }
290
+ const TRUSTED_RELEASE_HOSTS = new Set([
291
+ 'extension.vigthoria.io',
292
+ 'landing.vigthoria.io',
293
+ 'coder.vigthoria.io',
294
+ 'market.vigthoria.io',
295
+ ]);
296
+ function validateReleaseUrl(rawUrl, allowLocalhost = false) {
297
+ const parsed = new URL(rawUrl);
298
+ const local = parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1' || parsed.hostname === '::1';
299
+ if (parsed.protocol !== 'https:' && !(allowLocalhost && local && parsed.protocol === 'http:')) {
300
+ throw new Error('Release URLs must use HTTPS (HTTP is allowed only for an explicit localhost manifest).');
301
+ }
302
+ if (!TRUSTED_RELEASE_HOSTS.has(parsed.hostname) && !(allowLocalhost && local)) {
303
+ throw new Error(`Untrusted release host: ${parsed.hostname}`);
304
+ }
305
+ return parsed;
306
+ }
288
307
  function sha256File(filePath) {
289
308
  const hash = createHash('sha256');
290
309
  const data = fs.readFileSync(filePath);
@@ -1459,7 +1478,7 @@ Examples:
1459
1478
  .option('--cortex', 'Vigthoria Cortex: maximum intelligence execution')
1460
1479
  .option('--approve', 'Auto-approve Cortex execution prompt')
1461
1480
  .option('--no-approve', 'Require interactive approval before execution')
1462
- .option('--auto-charge', 'Attempt direct VigCoin top-up when Cortex balance is low')
1481
+ .option('--auto-charge', 'Compatibility flag; low balances still require a verified User Hub top-up')
1463
1482
  .option('--plan-only', 'Run Cortex estimator only; do not execute Legion job')
1464
1483
  .option('--force-budget', 'Allow execution when estimated budget exceeds the hard safe-stop ceiling')
1465
1484
  .option('--ignore-preflight', 'Bypass mandatory local preflight checks (no warranty)')
@@ -1558,6 +1577,8 @@ Examples:
1558
1577
  .action(async () => {
1559
1578
  await statusAction();
1560
1579
  });
1580
+ registerAuthCommands(program);
1581
+ new WalletCommand().register(program);
1561
1582
  program
1562
1583
  .command('doctor')
1563
1584
  .description('Run local Vigthoria CLI diagnostics (no network calls unless --check-api)')
@@ -1569,10 +1590,23 @@ Examples:
1569
1590
  const offline = isOfflineMode();
1570
1591
  const updateSuppressed = isUpdateCheckSuppressed();
1571
1592
  const subscription = config.get('subscription') || { plan: null, status: null, expiresAt: null };
1593
+ const iosSandbox = isIOSEnvironment();
1594
+ const platformSupport = iosSandbox
1595
+ ? 'experimental-mobile-terminal-sandbox'
1596
+ : process.platform === 'darwin'
1597
+ ? 'supported-macos-desktop'
1598
+ : process.platform === 'win32' || process.platform === 'linux'
1599
+ ? 'supported-desktop'
1600
+ : 'unsupported';
1572
1601
  const report = {
1573
1602
  cliVersion: VERSION,
1574
1603
  nodeVersion: process.version,
1575
1604
  platform: `${process.platform} ${process.arch}`,
1605
+ clientOs: resolveClientOsSlug(),
1606
+ platformSupport,
1607
+ platformLimitations: iosSandbox
1608
+ ? 'Not a native iOS app. Requires an iSH-like Linux terminal with Node.js >=20.19, npm and filesystem access; Workbench and device tooling are unavailable.'
1609
+ : null,
1576
1610
  cwd: process.cwd(),
1577
1611
  homeDir: os.homedir(),
1578
1612
  configPath: config.getConfigPath(),
@@ -1693,6 +1727,9 @@ Examples:
1693
1727
  let manifestEntry = null;
1694
1728
  if (manifestUrl) {
1695
1729
  try {
1730
+ const explicitLocalManifest = Boolean(options.manifest)
1731
+ && ['localhost', '127.0.0.1', '::1'].includes(new URL(manifestUrl).hostname);
1732
+ validateReleaseUrl(manifestUrl, explicitLocalManifest);
1696
1733
  console.log(chalk.cyan(`Checking manifest channel ${channel}...`));
1697
1734
  const response = await axios.get(manifestUrl, {
1698
1735
  timeout: 10000,
@@ -1704,6 +1741,13 @@ Examples:
1704
1741
  console.log(chalk.yellow(`Manifest has no installable entry for channel ${channel}; will check npm.`));
1705
1742
  manifestEntry = null;
1706
1743
  }
1744
+ else {
1745
+ validateReleaseUrl(manifestEntry.url, explicitLocalManifest);
1746
+ if (!/^[a-f0-9]{64}$/i.test(String(manifestEntry.sha256 || ''))) {
1747
+ console.log(chalk.yellow('Manifest entry has no valid SHA-256 checksum; refusing it and checking npm.'));
1748
+ manifestEntry = null;
1749
+ }
1750
+ }
1707
1751
  }
1708
1752
  catch (error) {
1709
1753
  console.log(chalk.yellow(`Manifest check failed: ${error.message}`));
@@ -1755,22 +1799,16 @@ Examples:
1755
1799
  try {
1756
1800
  console.log(chalk.cyan(`Downloading release package (${manifestEntry.version})...`));
1757
1801
  await downloadFile(manifestEntry.url, tmpFile);
1758
- if (manifestEntry.sha256) {
1759
- const expected = manifestEntry.sha256.toLowerCase();
1760
- const actual = sha256File(tmpFile).toLowerCase();
1761
- if (actual !== expected) {
1762
- console.error(chalk.red('Release checksum verification failed'));
1763
- console.error(chalk.red(`Expected: ${expected}`));
1764
- console.error(chalk.red(`Actual: ${actual}`));
1765
- process.exitCode = 1;
1766
- return;
1767
- }
1768
- console.log(chalk.green('Checksum verification passed'));
1769
- }
1770
- else {
1771
- console.log(chalk.yellow.bold('WARNING: manifest entry has no sha256.'));
1772
- console.log(chalk.yellow(' The release artifact will be installed without integrity verification.'));
1802
+ const expected = String(manifestEntry.sha256).toLowerCase();
1803
+ const actual = sha256File(tmpFile).toLowerCase();
1804
+ if (actual !== expected) {
1805
+ console.error(chalk.red('Release checksum verification failed'));
1806
+ console.error(chalk.red(`Expected: ${expected}`));
1807
+ console.error(chalk.red(`Actual: ${actual}`));
1808
+ process.exitCode = 1;
1809
+ return;
1773
1810
  }
1811
+ console.log(chalk.green('Checksum verification passed'));
1774
1812
  console.log(chalk.cyan('Installing update...'));
1775
1813
  await installGlobalPackageWithNpm(tmpFile);
1776
1814
  console.log(chalk.green(`Updated to version ${manifestEntry.version}`));
@@ -1864,30 +1902,6 @@ Examples:
1864
1902
  .action(() => {
1865
1903
  console.log(renderDynamicHelp(program));
1866
1904
  });
1867
- const codingCommandDefinitions = [
1868
- { name: 'edit', description: 'Edit code by describing the desired change', instruction: 'Edit the project according to this request' },
1869
- { name: 'generate', description: 'Generate code from a prompt', instruction: 'Generate code for this request' },
1870
- { name: 'explain', description: 'Explain code, errors, or project behavior', instruction: 'Explain this request clearly and practically' },
1871
- { name: 'review', description: 'Review code and suggest concrete improvements', instruction: 'Review the project or code for this request' },
1872
- { name: 'fix', description: 'Fix bugs, build failures, or test failures', instruction: 'Fix this issue in the project' },
1873
- ];
1874
- for (const commandDefinition of codingCommandDefinitions) {
1875
- program
1876
- .command(commandDefinition.name)
1877
- .description(commandDefinition.description)
1878
- .argument('[request...]', 'Request text for the coding assistant')
1879
- .option('-m, --model <model>', 'Model to use', 'code')
1880
- .option('-p, --project <path>', 'Project directory', process.cwd())
1881
- .action(async (requestParts = [], options) => {
1882
- const requestText = requestParts.join(' ').trim();
1883
- const chat = new ChatCommand(config, logger, program);
1884
- await chat.run({
1885
- model: options.model || 'code',
1886
- project: options.project || process.cwd(),
1887
- prompt: requestText ? `${commandDefinition.instruction}: ${requestText}` : commandDefinition.instruction,
1888
- });
1889
- });
1890
- }
1891
1905
  try {
1892
1906
  // Default to chat if no command
1893
1907
  if (args.length === 2) {
package/install.ps1 CHANGED
@@ -5,7 +5,7 @@
5
5
  $ErrorActionPreference = "Stop"
6
6
 
7
7
  # Configuration
8
- $CLI_VERSION = "1.13.8"
8
+ $CLI_VERSION = "1.13.10"
9
9
  $INSTALL_DIR = "$env:USERPROFILE\.vigthoria"
10
10
  $NPM_PACKAGE = "vigthoria-cli"
11
11
  $GIT_PACKAGE_URL = "git+https://market.vigthoria.io/vigthoria/vigthoria-cli.git"
package/install.sh CHANGED
@@ -26,12 +26,13 @@ else
26
26
  fi
27
27
 
28
28
  # Configuration
29
- CLI_VERSION="1.13.8"
29
+ CLI_VERSION="1.13.10"
30
30
  INSTALL_DIR="$HOME/.vigthoria"
31
31
  REPO_URL="https://market.vigthoria.io/vigthoria/vigthoria-cli"
32
32
  GIT_PACKAGE_URL="git+https://market.vigthoria.io/vigthoria/vigthoria-cli.git"
33
33
  MANIFEST_URL="${VIGTHORIA_UPDATE_MANIFEST_URL:-https://extension.vigthoria.io/downloads/manifest.json}"
34
34
  HOSTED_TARBALL_URL="https://extension.vigthoria.io/downloads/vigthoria-cli-${CLI_VERSION}.tgz"
35
+ HOSTED_TARBALL_SHA256=""
35
36
 
36
37
  resolve_release_manifest() {
37
38
  if ! command -v curl >/dev/null 2>&1 || ! command -v python3 >/dev/null 2>&1; then
@@ -52,24 +53,34 @@ try:
52
53
  stable = (data.get('channels') or {}).get('stable') or {}
53
54
  version = str(stable.get('version') or '').strip()
54
55
  url = str(stable.get('url') or '').strip()
56
+ sha256 = str(stable.get('sha256') or '').strip().lower()
55
57
  if version:
56
58
  print(version)
57
59
  if url:
58
60
  print(url)
61
+ if sha256:
62
+ print(sha256)
59
63
  except Exception:
60
64
  pass
61
65
  " 2>/dev/null || true)"
62
66
 
63
67
  if [[ -n "$resolved" ]]; then
64
- mapfile -t _manifest_lines <<< "$resolved"
65
- if [[ -n "${_manifest_lines[0]:-}" ]]; then
66
- CLI_VERSION="${_manifest_lines[0]}"
68
+ # macOS ships Bash 3.2, which does not provide mapfile/readarray.
69
+ # Parse the two-line response with POSIX sed instead.
70
+ manifest_version="$(printf '%s\n' "$resolved" | sed -n '1p')"
71
+ manifest_url="$(printf '%s\n' "$resolved" | sed -n '2p')"
72
+ manifest_sha256="$(printf '%s\n' "$resolved" | sed -n '3p')"
73
+ if [[ -n "$manifest_version" ]]; then
74
+ CLI_VERSION="$manifest_version"
67
75
  fi
68
- if [[ -n "${_manifest_lines[1]:-}" ]]; then
69
- HOSTED_TARBALL_URL="${_manifest_lines[1]}"
70
- elif [[ -n "${_manifest_lines[0]:-}" ]]; then
76
+ if [[ -n "$manifest_url" ]]; then
77
+ HOSTED_TARBALL_URL="$manifest_url"
78
+ elif [[ -n "$manifest_version" ]]; then
71
79
  HOSTED_TARBALL_URL="https://extension.vigthoria.io/downloads/vigthoria-cli-${CLI_VERSION}.tgz"
72
80
  fi
81
+ if [[ "$manifest_sha256" =~ ^[a-f0-9]{64}$ ]]; then
82
+ HOSTED_TARBALL_SHA256="$manifest_sha256"
83
+ fi
73
84
  fi
74
85
  }
75
86
 
@@ -177,10 +188,32 @@ install_cli() {
177
188
  # Create install directory
178
189
  mkdir -p "$INSTALL_DIR"
179
190
 
180
- # Option 1: Install the hosted release package.
181
- if curl -fsI "$HOSTED_TARBALL_URL" > /dev/null 2>&1; then
182
- echo "Installing hosted release package..."
183
- npm install -g --force "$HOSTED_TARBALL_URL"
191
+ # Option 1: Download and verify the exact hosted release package.
192
+ if [[ -n "$HOSTED_TARBALL_SHA256" ]]; then
193
+ echo "Downloading checksum-verified hosted release package..."
194
+ RELEASE_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/vigthoria-cli-install.XXXXXX")"
195
+ RELEASE_ARCHIVE="$RELEASE_TMP_DIR/vigthoria-cli-${CLI_VERSION}.tgz"
196
+ if ! curl -fsSL "$HOSTED_TARBALL_URL" -o "$RELEASE_ARCHIVE"; then
197
+ rm -rf "$RELEASE_TMP_DIR"
198
+ echo -e "${RED}✗ Failed to download verified release package${NC}"
199
+ exit 1
200
+ fi
201
+ if command -v shasum >/dev/null 2>&1; then
202
+ ACTUAL_SHA256="$(shasum -a 256 "$RELEASE_ARCHIVE" | awk '{print $1}')"
203
+ elif command -v sha256sum >/dev/null 2>&1; then
204
+ ACTUAL_SHA256="$(sha256sum "$RELEASE_ARCHIVE" | awk '{print $1}')"
205
+ else
206
+ rm -rf "$RELEASE_TMP_DIR"
207
+ echo -e "${RED}✗ SHA-256 verification tool is unavailable${NC}"
208
+ exit 1
209
+ fi
210
+ if [[ "$ACTUAL_SHA256" != "$HOSTED_TARBALL_SHA256" ]]; then
211
+ rm -rf "$RELEASE_TMP_DIR"
212
+ echo -e "${RED}✗ Release checksum verification failed${NC}"
213
+ exit 1
214
+ fi
215
+ npm install -g --force "$RELEASE_ARCHIVE"
216
+ rm -rf "$RELEASE_TMP_DIR"
184
217
  elif npm view vigthoria-cli version &> /dev/null; then
185
218
  echo "Installing from npm registry..."
186
219
  npm install -g --force vigthoria-cli
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vigthoria-cli",
3
- "version": "1.13.8",
3
+ "version": "1.13.10",
4
4
  "description": "Vigthoria Coder CLI - AI-powered terminal coding assistant",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",