@vultisig/cli 2.19.7 → 2.19.11

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/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # @vultisig/cli
2
2
 
3
+ ## 2.19.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1129](https://github.com/vultisig/vultisig-sdk/pull/1129) [`5949742`](https://github.com/vultisig/vultisig-sdk/commit/59497426a238a75576e92d18023747d66c9d4e7a) Thanks [@neavra](https://github.com/neavra)! - fix(portfolio): report per-chain failures instead of silently swallowing them. The `portfolio` command now fetches each chain independently — one unreachable chain no longer fails the whole command, and a fiat-value lookup failure no longer silently drops the value. The `-o json` envelope always carries a `failures: [{ chain, stage, error }]` array (empty on full success), partial failures still exit 0, and an all-chains-failed run exits with a network error (code 3).
8
+
9
+ - Updated dependencies [[`3bf18a1`](https://github.com/vultisig/vultisig-sdk/commit/3bf18a18606fd1b45d50abb562eb6c3011182d48)]:
10
+ - @vultisig/sdk@2.19.11
11
+
12
+ ## 2.19.10
13
+
14
+ ### Patch Changes
15
+
16
+ - [#1076](https://github.com/vultisig/vultisig-sdk/pull/1076) [`5179edd`](https://github.com/vultisig/vultisig-sdk/commit/5179edd9eb25b392941f9649a455e92bfd5bf8b5) Thanks [@neavra](https://github.com/neavra)! - fix(chains): validate `chains --add <chain>` against the registry before persisting
17
+
18
+ An invalid `chains --add fakechain` correctly reported INVALID_CHAIN but still wrote
19
+ "fakechain" to the vault's chain list (chain resolution falls back to the raw user
20
+ string when the name doesn't match the registry). Every subsequent address-deriving
21
+ command then re-derived the bogus chain and dumped a stack trace to stderr until it was
22
+ manually removed. The `--add` path now validates against the supported-chain registry
23
+ first and fails closed — nothing is persisted for an unsupported chain.
24
+
25
+ - [#1127](https://github.com/vultisig/vultisig-sdk/pull/1127) [`dea1efa`](https://github.com/vultisig/vultisig-sdk/commit/dea1efae6fbd2975de4ac5a26b2e8210999a0f4f) Thanks [@neavra](https://github.com/neavra)! - fix(cli): honor VULTISIG_CONFIG_DIR for vault storage
26
+
27
+ The documented `VULTISIG_CONFIG_DIR` env var was a no-op for vault storage: the
28
+ CLI constructed the SDK without a storage override, so vaults, the active-vault
29
+ pointer and cache always resolved to `~/.vultisig` even when the var pointed
30
+ elsewhere — only `config.json` and the agent journal honored it, producing a
31
+ split-brain config location that broke CI/container isolation and multi-tenant
32
+ use. The CLI now roots SDK vault storage at `getConfigDir()` via a shared
33
+ `createVaultStorage()` helper. When the var is unset it falls back to
34
+ `~/.vultisig`, so the default location is unchanged.
35
+
36
+ - [#1126](https://github.com/vultisig/vultisig-sdk/pull/1126) [`6054ff5`](https://github.com/vultisig/vultisig-sdk/commit/6054ff599e4133c9853f31e8ca2413ab52f606fb) Thanks [@neavra](https://github.com/neavra)! - fix(mpc): keep keygen tracing off stdout so `-o json` output stays parseable
37
+
38
+ The DKLS and Schnorr keygen/reshare/key-import ceremonies logged progress
39
+ (session ids, raw wire messages, "keygen complete", …) to stdout via ungated
40
+ `console.log`. stdout is the machine channel for the CLI's `-o json` mode, so
41
+ the documented `create fast … -o json` agent flow produced unparseable stdout
42
+ (`JSON.parse(stdout)` failed on the leading garbage) and leaked MPC internals
43
+ into terminals and CI logs.
44
+
45
+ Route that tracing through a gated logger that writes to stderr only when
46
+ `VULTISIG_DEBUG=1`, so stdout carries only the final JSON envelope while
47
+ the debug output stays available to humans on demand. No keygen behavior
48
+ changes — only the log sink moves off stdout.
49
+
50
+ - [#1077](https://github.com/vultisig/vultisig-sdk/pull/1077) [`5e5494c`](https://github.com/vultisig/vultisig-sdk/commit/5e5494cf103ea5c17431b6a6231b30931eb004bb) Thanks [@neavra](https://github.com/neavra)! - Tolerate a corrupt `activeVaultId.json` at startup. A truncated or unparseable
51
+ active-vault pointer used to throw during CLI initialization, which broke every
52
+ command — including `vultisig vaults`, the one you run to recover. The pointer
53
+ read now fails open (treated as "no active vault") and self-heals by clearing
54
+ the bad pointer, so vaults still list with none marked active.
55
+ - Updated dependencies [[`280956f`](https://github.com/vultisig/vultisig-sdk/commit/280956f1743564ea271a03c4735f70151b63f161), [`90070f3`](https://github.com/vultisig/vultisig-sdk/commit/90070f39be011821f7508c7ff094025861dce040), [`0e3f86d`](https://github.com/vultisig/vultisig-sdk/commit/0e3f86db82ed086b1e200a6f83434a638f593c17), [`6054ff5`](https://github.com/vultisig/vultisig-sdk/commit/6054ff599e4133c9853f31e8ca2413ab52f606fb), [`b37e726`](https://github.com/vultisig/vultisig-sdk/commit/b37e7264db3291adca1cb366f1311446d6add439), [`1b2636b`](https://github.com/vultisig/vultisig-sdk/commit/1b2636b535736a711fc537a87d2f51090fe6342d), [`2c9d34e`](https://github.com/vultisig/vultisig-sdk/commit/2c9d34e0837f68d92769c7aefa566ffb1c0c52c7), [`ffc75a6`](https://github.com/vultisig/vultisig-sdk/commit/ffc75a6e76af699a78b0fc3411ab052ce5000c91), [`fc595a1`](https://github.com/vultisig/vultisig-sdk/commit/fc595a17cb4901af1dfeac2521b93bb75823068f), [`776c539`](https://github.com/vultisig/vultisig-sdk/commit/776c5398764314f140f18ab4f86a1801fe9fdfe6)]:
56
+ - @vultisig/sdk@2.19.10
57
+ - @vultisig/core-chain@2.24.2
58
+
3
59
  ## 2.19.7
4
60
 
5
61
  ### Patch Changes
package/README.md CHANGED
@@ -1153,6 +1153,43 @@ Configuration is stored in `~/.vultisig/`:
1153
1153
  > truth) and are covered by a doc-lint test that fails if this table drifts from the code. Run
1154
1154
  > `vultisig --help` for the same list.
1155
1155
 
1156
+ ### Partial failures (`portfolio`)
1157
+
1158
+ The `portfolio` command fetches every chain independently, so one unreachable chain no longer
1159
+ fails the whole command. The `-o json` envelope always carries a `failures` array (empty when
1160
+ everything succeeded):
1161
+
1162
+ ```jsonc
1163
+ {
1164
+ "success": true,
1165
+ "v": 1,
1166
+ "data": {
1167
+ "portfolio": { "totalValue": { ... }, "chainBalances": [ /* only the chains that loaded */ ] },
1168
+ "currency": "usd",
1169
+ "failures": [
1170
+ { "chain": "Bitcoin", "stage": "balance", "error": "ECONNREFUSED btc-rpc" },
1171
+ { "chain": "Ethereum", "stage": "value", "error": "pricing service unavailable" }
1172
+ ]
1173
+ }
1174
+ }
1175
+ ```
1176
+
1177
+ - `stage: "balance"` — the balance fetch failed; the chain is omitted from `chainBalances`.
1178
+ - `stage: "value"` — the balance loaded but its fiat value did not; the chain still appears in
1179
+ `chainBalances` (without a `value`) and is also listed here.
1180
+ - `error` is a concise single-line message — never a stack trace or filesystem path.
1181
+
1182
+ **Partial-success exit contract:** if _some_ chains loaded, the command exits **0** and reports
1183
+ the rest under `failures`. Machine consumers should branch on `data.failures.length`, not `$?`.
1184
+ If _every_ chain fails to fetch a balance, the command exits **3** (network error, retryable).
1185
+ On the human-readable (table) output, failures are printed as `Warning:` lines below the table.
1186
+
1187
+ > **Note on `totalValue`:** `failures` describes the per-chain _breakdown_ pass (`chainBalances`).
1188
+ > `portfolio.totalValue` is computed by an independent best-effort aggregate that includes token
1189
+ > values (not just native) and silently omits any chain/token it could not price. It is therefore
1190
+ > not guaranteed to be consistent with `chainBalances`/`failures` — treat it as an approximate
1191
+ > total, and rely on `failures` (not the total) to detect which chains had problems.
1192
+
1156
1193
  ## Troubleshooting
1157
1194
 
1158
1195
  ### "No active vault" error
package/dist/index.js CHANGED
@@ -5839,6 +5839,10 @@ async function confirmSwap() {
5839
5839
  }
5840
5840
 
5841
5841
  // src/commands/balance.ts
5842
+ function conciseError(err) {
5843
+ const message = err instanceof Error ? err.message : String(err);
5844
+ return message.split("\n")[0].trim() || "Unknown error";
5845
+ }
5842
5846
  async function executeBalance(ctx2, options = {}) {
5843
5847
  const vault = await ctx2.ensureActiveVault();
5844
5848
  const spinner = createSpinner("Loading balances...");
@@ -5876,257 +5880,82 @@ async function executePortfolio(ctx2, options = {}) {
5876
5880
  const spinner = createSpinner(`Loading portfolio in ${currencyName}...`);
5877
5881
  const totalValue = await vault.getTotalValue(currency);
5878
5882
  const chains = vault.chains;
5879
- const chainBalances = await Promise.all(
5883
+ const results = await Promise.all(
5880
5884
  chains.map(async (chain) => {
5881
- const balance = await vault.balance(chain);
5885
+ let balance;
5886
+ try {
5887
+ balance = await vault.balance(chain);
5888
+ } catch (err) {
5889
+ return { failure: { chain, stage: "balance", error: conciseError(err) } };
5890
+ }
5882
5891
  try {
5883
5892
  const value = await vault.getValue(chain, void 0, currency);
5884
- return { chain, balance, value };
5885
- } catch {
5886
- return { chain, balance };
5893
+ return { entry: { chain, balance, value } };
5894
+ } catch (err) {
5895
+ return { entry: { chain, balance }, failure: { chain, stage: "value", error: conciseError(err) } };
5887
5896
  }
5888
5897
  })
5889
5898
  );
5899
+ const chainBalances = [];
5900
+ const failures = [];
5901
+ for (const result of results) {
5902
+ if (result.entry) chainBalances.push(result.entry);
5903
+ if (result.failure) failures.push(result.failure);
5904
+ }
5905
+ if (failures.length > 0 && chainBalances.length === 0) {
5906
+ spinner.fail("Portfolio failed to load");
5907
+ throw new NetworkError(
5908
+ `Failed to load balances for all ${failures.length} chain(s): ${failures.map((f) => `${f.chain} (${f.error})`).join("; ")}`,
5909
+ "All chain balance fetches failed \u2014 likely a network/RPC issue",
5910
+ ["Check your internet connection", "Retry in a few moments"]
5911
+ );
5912
+ }
5890
5913
  const portfolio = { totalValue, chainBalances };
5891
5914
  spinner.succeed("Portfolio loaded");
5892
5915
  if (isJsonOutput()) {
5893
- outputJson({ portfolio, currency });
5916
+ outputJson({ portfolio, currency, failures });
5894
5917
  return;
5895
5918
  }
5896
5919
  displayPortfolio(portfolio, currency, options.raw ?? false);
5920
+ if (failures.length > 0) {
5921
+ warn(`
5922
+ Warning: ${failures.length} chain(s) failed to load fully:`);
5923
+ for (const f of failures) {
5924
+ warn(` - ${f.chain} (${f.stage}): ${f.error}`);
5925
+ }
5926
+ }
5897
5927
  }
5898
5928
 
5899
5929
  // src/commands/chains.ts
5900
5930
  import { SUPPORTED_CHAINS } from "@vultisig/sdk";
5901
5931
  import chalk3 from "chalk";
5902
- async function executeChains(ctx2, options = {}) {
5903
- const vault = await ctx2.ensureActiveVault();
5904
- if (options.addAll) {
5905
- const currentCount = vault.chains.length;
5906
- const spinner = createSpinner(`Adding all ${SUPPORTED_CHAINS.length} supported chains...`);
5907
- await vault.setChains([...SUPPORTED_CHAINS]);
5908
- const addedCount = SUPPORTED_CHAINS.length - currentCount;
5909
- spinner.succeed(`Added ${addedCount} chains (${SUPPORTED_CHAINS.length} total)`);
5910
- if (isJsonOutput()) {
5911
- outputJson({ chains: [...vault.chains], added: addedCount, total: SUPPORTED_CHAINS.length });
5912
- return;
5913
- }
5914
- info(chalk3.gray("\nAll supported chains are now enabled."));
5915
- return;
5916
- }
5917
- if (options.add) {
5918
- const alreadyActive = vault.chains.includes(options.add);
5919
- if (!alreadyActive) {
5920
- await vault.addChain(options.add);
5921
- }
5922
- const address = await vault.address(options.add);
5923
- if (isJsonOutput()) {
5924
- outputJson({ chain: options.add, alreadyActive, address, chains: [...vault.chains] });
5925
- return;
5926
- }
5927
- success(alreadyActive ? `
5928
- Chain already active: ${options.add}` : `
5929
- + Added chain: ${options.add}`);
5930
- info(`Address: ${address}`);
5931
- } else if (options.remove) {
5932
- await vault.removeChain(options.remove);
5933
- if (isJsonOutput()) {
5934
- outputJson({ chain: options.remove, removed: true, chains: [...vault.chains] });
5935
- return;
5936
- }
5937
- success(`
5938
- + Removed chain: ${options.remove}`);
5939
- } else {
5940
- const chains = vault.chains;
5941
- if (isJsonOutput()) {
5942
- outputJson({ chains: [...chains] });
5943
- return;
5944
- }
5945
- printResult(chalk3.cyan("\nActive Chains:\n"));
5946
- chains.forEach((chain) => {
5947
- printResult(` - ${chain}`);
5948
- });
5949
- info(chalk3.gray(`
5950
- ${chains.length} of ${SUPPORTED_CHAINS.length} chains enabled`));
5951
- info(chalk3.gray("Use --add <chain>, --add-all, or --remove <chain>"));
5952
- }
5953
- }
5954
- async function executeAddresses(ctx2) {
5955
- const vault = await ctx2.ensureActiveVault();
5956
- const spinner = createSpinner("Loading addresses...");
5957
- const addresses = await vault.addresses();
5958
- spinner.succeed("Addresses loaded");
5959
- if (isJsonOutput()) {
5960
- outputJson({ addresses });
5961
- return;
5962
- }
5963
- displayAddresses(addresses);
5964
- }
5965
5932
 
5966
- // src/commands/tokens.ts
5967
- import chalk4 from "chalk";
5968
- import inquirer3 from "inquirer";
5969
- async function executeTokens(ctx2, options) {
5970
- await ctx2.ensureActiveVault();
5971
- if (options.discover) {
5972
- await discoverTokens(ctx2, options.chain);
5973
- return;
5974
- }
5975
- if (options.add) {
5976
- let symbol = options.symbol;
5977
- let name = options.name;
5978
- let decimals = options.decimals;
5979
- const prompts = [];
5980
- if (!symbol) {
5981
- prompts.push({
5982
- type: "input",
5983
- name: "symbol",
5984
- message: "Enter token symbol (e.g., USDT):",
5985
- validate: (input) => input.trim() !== "" || "Symbol is required"
5986
- });
5987
- }
5988
- if (!name) {
5989
- prompts.push({
5990
- type: "input",
5991
- name: "name",
5992
- message: "Enter token name (e.g., Tether USD):",
5993
- validate: (input) => input.trim() !== "" || "Name is required"
5994
- });
5995
- }
5996
- if (decimals === void 0) {
5997
- prompts.push({
5998
- type: "number",
5999
- name: "decimals",
6000
- message: "Enter token decimals:",
6001
- default: 18,
6002
- validate: (input) => input >= 0 || "Decimals must be non-negative"
6003
- });
6004
- }
6005
- if (prompts.length > 0) {
6006
- requireInteractive("Provide --symbol, --name, and --decimals flags for non-interactive token addition.");
6007
- const answers = await inquirer3.prompt(prompts);
6008
- symbol = symbol || answers.symbol?.trim();
6009
- name = name || answers.name?.trim();
6010
- decimals = decimals ?? answers.decimals;
6011
- }
6012
- await addToken(ctx2, {
6013
- chain: options.chain,
6014
- contractAddress: options.add,
6015
- symbol,
6016
- name,
6017
- decimals
6018
- });
6019
- } else if (options.remove) {
6020
- await removeToken(ctx2, options.chain, options.remove);
6021
- } else {
6022
- await listTokens(ctx2, options.chain);
6023
- }
6024
- }
6025
- async function addToken(ctx2, options) {
6026
- const vault = await ctx2.ensureActiveVault();
6027
- await vault.addToken(options.chain, {
6028
- id: `${options.chain}-${options.contractAddress}`,
6029
- contractAddress: options.contractAddress,
6030
- symbol: options.symbol,
6031
- name: options.name,
6032
- decimals: options.decimals,
6033
- chainId: options.chain,
6034
- isNative: false
6035
- });
6036
- success(`
6037
- + Added token ${options.symbol} on ${options.chain}`);
6038
- }
6039
- async function removeToken(ctx2, chain, tokenId) {
6040
- const vault = await ctx2.ensureActiveVault();
6041
- await vault.removeToken(chain, tokenId);
6042
- success(`
6043
- + Removed token ${tokenId} from ${chain}`);
6044
- }
6045
- async function discoverTokens(ctx2, chain) {
6046
- const vault = await ctx2.ensureActiveVault();
6047
- const spinner = createSpinner(`Discovering tokens on ${chain}...`);
6048
- let discovered;
5933
+ // src/core/active-vault.ts
5934
+ var ACTIVE_VAULT_ID_KEY = "activeVaultId";
5935
+ async function loadActiveVaultSafely(sdk) {
5936
+ let activeId;
6049
5937
  try {
6050
- discovered = await vault.discoverTokens(chain);
5938
+ activeId = await sdk.storage.get(ACTIVE_VAULT_ID_KEY);
6051
5939
  } catch (err) {
6052
- spinner.fail(`Token discovery failed for ${chain}`);
6053
- throw err;
6054
- }
6055
- if (discovered.length === 0) {
6056
- spinner.succeed(`No new tokens found on ${chain}`);
6057
- if (isJsonOutput()) {
6058
- outputJson({ chain, discovered: [], count: 0 });
5940
+ process.stderr.write(
5941
+ `Warning: active vault pointer is unreadable and was ignored (no active vault). ${err?.message ?? ""}
5942
+ `
5943
+ );
5944
+ try {
5945
+ await sdk.setActiveVault(null);
5946
+ } catch {
6059
5947
  }
6060
- return;
5948
+ return { vault: null, corruptPointer: true };
6061
5949
  }
6062
- const existingTokens = vault.getTokens(chain);
6063
- const existingAddresses = new Set(existingTokens.map((t) => t.contractAddress ?? t.id));
6064
- const newTokens = discovered.filter((d) => d.contractAddress && !existingAddresses.has(d.contractAddress));
6065
- for (const d of newTokens) {
6066
- await vault.addToken(chain, {
6067
- id: d.contractAddress,
6068
- symbol: d.ticker,
6069
- name: d.ticker,
6070
- decimals: d.decimals,
6071
- contractAddress: d.contractAddress,
6072
- chainId: chain,
6073
- isNative: false
6074
- });
5950
+ if (activeId === null || activeId === void 0) {
5951
+ return { vault: null, corruptPointer: false };
6075
5952
  }
6076
- const allTokens = vault.getTokens(chain);
6077
- spinner.succeed(`Discovered ${newTokens.length} new token(s) on ${chain}`);
6078
- if (isJsonOutput()) {
6079
- outputJson({
6080
- chain,
6081
- discovered: newTokens.map((d) => ({
6082
- symbol: d.ticker,
6083
- contractAddress: d.contractAddress,
6084
- decimals: d.decimals
6085
- })),
6086
- count: newTokens.length
6087
- });
6088
- return;
6089
- }
6090
- for (const d of newTokens) {
6091
- printResult(` ${d.ticker} (${d.contractAddress})`);
6092
- }
6093
- info(chalk4.gray(`
6094
- ${allTokens.length} total token(s) tracked on ${chain}`));
5953
+ return { vault: await sdk.getVaultById(activeId), corruptPointer: false };
6095
5954
  }
6096
- async function listTokens(ctx2, chain) {
6097
- const vault = await ctx2.ensureActiveVault();
6098
- const spinner = createSpinner(`Loading tokens for ${chain}...`);
6099
- const tokens = vault.getTokens(chain);
6100
- spinner.succeed(`Tokens loaded for ${chain}`);
6101
- if (isJsonOutput()) {
6102
- outputJson({ chain, tokens: tokens || [] });
6103
- return;
6104
- }
6105
- if (!tokens || tokens.length === 0) {
6106
- warn(`
6107
- No tokens configured for ${chain}`);
6108
- info(chalk4.gray(`
6109
- Use --add <contractAddress> to add a token`));
6110
- } else {
6111
- printResult(chalk4.cyan(`
6112
- Tokens for ${chain}:
6113
- `));
6114
- const table = tokens.map((token) => ({
6115
- Symbol: token.symbol,
6116
- Name: token.name,
6117
- Contract: token.contractAddress || "N/A",
6118
- Decimals: token.decimals,
6119
- Native: token.isNative ? "Yes" : "No"
6120
- }));
6121
- printTable(table);
6122
- info(chalk4.gray(`
6123
- Use --add <contractAddress> to add or --remove <tokenId> to remove`));
6124
- }
5955
+ function shouldAutoSelectActiveVault(hasActiveVault, corruptPointer, vaultCount) {
5956
+ return !hasActiveVault && !corruptPointer && vaultCount > 0;
6125
5957
  }
6126
5958
 
6127
- // src/commands/transaction.ts
6128
- import { Chain, Vultisig as Vultisig2 } from "@vultisig/sdk";
6129
-
6130
5959
  // src/agent/broadcastJournal.ts
6131
5960
  import { createHash } from "node:crypto";
6132
5961
  import {
@@ -6703,7 +6532,242 @@ import { getConfigPath, loadConfig, saveConfig } from "@vultisig/client-shared";
6703
6532
  // src/core/vault-discovery.ts
6704
6533
  import { discoverVaultFiles, SEARCH_DIRS } from "@vultisig/client-shared";
6705
6534
 
6535
+ // src/commands/chains.ts
6536
+ async function executeChains(ctx2, options = {}) {
6537
+ const vault = await ctx2.ensureActiveVault();
6538
+ if (options.addAll) {
6539
+ const currentCount = vault.chains.length;
6540
+ const spinner = createSpinner(`Adding all ${SUPPORTED_CHAINS.length} supported chains...`);
6541
+ await vault.setChains([...SUPPORTED_CHAINS]);
6542
+ const addedCount = SUPPORTED_CHAINS.length - currentCount;
6543
+ spinner.succeed(`Added ${addedCount} chains (${SUPPORTED_CHAINS.length} total)`);
6544
+ if (isJsonOutput()) {
6545
+ outputJson({ chains: [...vault.chains], added: addedCount, total: SUPPORTED_CHAINS.length });
6546
+ return;
6547
+ }
6548
+ info(chalk3.gray("\nAll supported chains are now enabled."));
6549
+ return;
6550
+ }
6551
+ if (options.add) {
6552
+ if (!SUPPORTED_CHAINS.includes(options.add)) {
6553
+ throw new InvalidChainError(
6554
+ `Unsupported chain: "${options.add}"`,
6555
+ 'Run "vultisig chains" to see the supported chains, or check the spelling.',
6556
+ void 0,
6557
+ { chain: String(options.add) }
6558
+ );
6559
+ }
6560
+ const alreadyActive = vault.chains.includes(options.add);
6561
+ if (!alreadyActive) {
6562
+ await vault.addChain(options.add);
6563
+ }
6564
+ const address = await vault.address(options.add);
6565
+ if (isJsonOutput()) {
6566
+ outputJson({ chain: options.add, alreadyActive, address, chains: [...vault.chains] });
6567
+ return;
6568
+ }
6569
+ success(alreadyActive ? `
6570
+ Chain already active: ${options.add}` : `
6571
+ + Added chain: ${options.add}`);
6572
+ info(`Address: ${address}`);
6573
+ } else if (options.remove) {
6574
+ await vault.removeChain(options.remove);
6575
+ if (isJsonOutput()) {
6576
+ outputJson({ chain: options.remove, removed: true, chains: [...vault.chains] });
6577
+ return;
6578
+ }
6579
+ success(`
6580
+ + Removed chain: ${options.remove}`);
6581
+ } else {
6582
+ const chains = vault.chains;
6583
+ if (isJsonOutput()) {
6584
+ outputJson({ chains: [...chains] });
6585
+ return;
6586
+ }
6587
+ printResult(chalk3.cyan("\nActive Chains:\n"));
6588
+ chains.forEach((chain) => {
6589
+ printResult(` - ${chain}`);
6590
+ });
6591
+ info(chalk3.gray(`
6592
+ ${chains.length} of ${SUPPORTED_CHAINS.length} chains enabled`));
6593
+ info(chalk3.gray("Use --add <chain>, --add-all, or --remove <chain>"));
6594
+ }
6595
+ }
6596
+ async function executeAddresses(ctx2) {
6597
+ const vault = await ctx2.ensureActiveVault();
6598
+ const spinner = createSpinner("Loading addresses...");
6599
+ const addresses = await vault.addresses();
6600
+ spinner.succeed("Addresses loaded");
6601
+ if (isJsonOutput()) {
6602
+ outputJson({ addresses });
6603
+ return;
6604
+ }
6605
+ displayAddresses(addresses);
6606
+ }
6607
+
6608
+ // src/commands/tokens.ts
6609
+ import chalk4 from "chalk";
6610
+ import inquirer3 from "inquirer";
6611
+ async function executeTokens(ctx2, options) {
6612
+ await ctx2.ensureActiveVault();
6613
+ if (options.discover) {
6614
+ await discoverTokens(ctx2, options.chain);
6615
+ return;
6616
+ }
6617
+ if (options.add) {
6618
+ let symbol = options.symbol;
6619
+ let name = options.name;
6620
+ let decimals = options.decimals;
6621
+ const prompts = [];
6622
+ if (!symbol) {
6623
+ prompts.push({
6624
+ type: "input",
6625
+ name: "symbol",
6626
+ message: "Enter token symbol (e.g., USDT):",
6627
+ validate: (input) => input.trim() !== "" || "Symbol is required"
6628
+ });
6629
+ }
6630
+ if (!name) {
6631
+ prompts.push({
6632
+ type: "input",
6633
+ name: "name",
6634
+ message: "Enter token name (e.g., Tether USD):",
6635
+ validate: (input) => input.trim() !== "" || "Name is required"
6636
+ });
6637
+ }
6638
+ if (decimals === void 0) {
6639
+ prompts.push({
6640
+ type: "number",
6641
+ name: "decimals",
6642
+ message: "Enter token decimals:",
6643
+ default: 18,
6644
+ validate: (input) => input >= 0 || "Decimals must be non-negative"
6645
+ });
6646
+ }
6647
+ if (prompts.length > 0) {
6648
+ requireInteractive("Provide --symbol, --name, and --decimals flags for non-interactive token addition.");
6649
+ const answers = await inquirer3.prompt(prompts);
6650
+ symbol = symbol || answers.symbol?.trim();
6651
+ name = name || answers.name?.trim();
6652
+ decimals = decimals ?? answers.decimals;
6653
+ }
6654
+ await addToken(ctx2, {
6655
+ chain: options.chain,
6656
+ contractAddress: options.add,
6657
+ symbol,
6658
+ name,
6659
+ decimals
6660
+ });
6661
+ } else if (options.remove) {
6662
+ await removeToken(ctx2, options.chain, options.remove);
6663
+ } else {
6664
+ await listTokens(ctx2, options.chain);
6665
+ }
6666
+ }
6667
+ async function addToken(ctx2, options) {
6668
+ const vault = await ctx2.ensureActiveVault();
6669
+ await vault.addToken(options.chain, {
6670
+ id: `${options.chain}-${options.contractAddress}`,
6671
+ contractAddress: options.contractAddress,
6672
+ symbol: options.symbol,
6673
+ name: options.name,
6674
+ decimals: options.decimals,
6675
+ chainId: options.chain,
6676
+ isNative: false
6677
+ });
6678
+ success(`
6679
+ + Added token ${options.symbol} on ${options.chain}`);
6680
+ }
6681
+ async function removeToken(ctx2, chain, tokenId) {
6682
+ const vault = await ctx2.ensureActiveVault();
6683
+ await vault.removeToken(chain, tokenId);
6684
+ success(`
6685
+ + Removed token ${tokenId} from ${chain}`);
6686
+ }
6687
+ async function discoverTokens(ctx2, chain) {
6688
+ const vault = await ctx2.ensureActiveVault();
6689
+ const spinner = createSpinner(`Discovering tokens on ${chain}...`);
6690
+ let discovered;
6691
+ try {
6692
+ discovered = await vault.discoverTokens(chain);
6693
+ } catch (err) {
6694
+ spinner.fail(`Token discovery failed for ${chain}`);
6695
+ throw err;
6696
+ }
6697
+ if (discovered.length === 0) {
6698
+ spinner.succeed(`No new tokens found on ${chain}`);
6699
+ if (isJsonOutput()) {
6700
+ outputJson({ chain, discovered: [], count: 0 });
6701
+ }
6702
+ return;
6703
+ }
6704
+ const existingTokens = vault.getTokens(chain);
6705
+ const existingAddresses = new Set(existingTokens.map((t) => t.contractAddress ?? t.id));
6706
+ const newTokens = discovered.filter((d) => d.contractAddress && !existingAddresses.has(d.contractAddress));
6707
+ for (const d of newTokens) {
6708
+ await vault.addToken(chain, {
6709
+ id: d.contractAddress,
6710
+ symbol: d.ticker,
6711
+ name: d.ticker,
6712
+ decimals: d.decimals,
6713
+ contractAddress: d.contractAddress,
6714
+ chainId: chain,
6715
+ isNative: false
6716
+ });
6717
+ }
6718
+ const allTokens = vault.getTokens(chain);
6719
+ spinner.succeed(`Discovered ${newTokens.length} new token(s) on ${chain}`);
6720
+ if (isJsonOutput()) {
6721
+ outputJson({
6722
+ chain,
6723
+ discovered: newTokens.map((d) => ({
6724
+ symbol: d.ticker,
6725
+ contractAddress: d.contractAddress,
6726
+ decimals: d.decimals
6727
+ })),
6728
+ count: newTokens.length
6729
+ });
6730
+ return;
6731
+ }
6732
+ for (const d of newTokens) {
6733
+ printResult(` ${d.ticker} (${d.contractAddress})`);
6734
+ }
6735
+ info(chalk4.gray(`
6736
+ ${allTokens.length} total token(s) tracked on ${chain}`));
6737
+ }
6738
+ async function listTokens(ctx2, chain) {
6739
+ const vault = await ctx2.ensureActiveVault();
6740
+ const spinner = createSpinner(`Loading tokens for ${chain}...`);
6741
+ const tokens = vault.getTokens(chain);
6742
+ spinner.succeed(`Tokens loaded for ${chain}`);
6743
+ if (isJsonOutput()) {
6744
+ outputJson({ chain, tokens: tokens || [] });
6745
+ return;
6746
+ }
6747
+ if (!tokens || tokens.length === 0) {
6748
+ warn(`
6749
+ No tokens configured for ${chain}`);
6750
+ info(chalk4.gray(`
6751
+ Use --add <contractAddress> to add a token`));
6752
+ } else {
6753
+ printResult(chalk4.cyan(`
6754
+ Tokens for ${chain}:
6755
+ `));
6756
+ const table = tokens.map((token) => ({
6757
+ Symbol: token.symbol,
6758
+ Name: token.name,
6759
+ Contract: token.contractAddress || "N/A",
6760
+ Decimals: token.decimals,
6761
+ Native: token.isNative ? "Yes" : "No"
6762
+ }));
6763
+ printTable(table);
6764
+ info(chalk4.gray(`
6765
+ Use --add <contractAddress> to add or --remove <tokenId> to remove`));
6766
+ }
6767
+ }
6768
+
6706
6769
  // src/commands/transaction.ts
6770
+ import { Chain, Vultisig as Vultisig2 } from "@vultisig/sdk";
6707
6771
  async function executeSend(ctx2, params) {
6708
6772
  const vault = await ctx2.ensureActiveVault();
6709
6773
  if (!Object.values(Chain).includes(params.chain)) {
@@ -14534,7 +14598,7 @@ var cachedVersion = null;
14534
14598
  function getVersion() {
14535
14599
  if (cachedVersion) return cachedVersion;
14536
14600
  if (true) {
14537
- cachedVersion = "2.19.7";
14601
+ cachedVersion = "2.19.11";
14538
14602
  return cachedVersion;
14539
14603
  }
14540
14604
  try {
@@ -16150,7 +16214,7 @@ Error: ${error2.message}`));
16150
16214
  async loadAllVaults() {
16151
16215
  const spinner = createSpinner2("Loading vaults...").start();
16152
16216
  try {
16153
- const activeVault = await this.ctx.sdk.getActiveVault();
16217
+ const { vault: activeVault, corruptPointer } = await loadActiveVaultSafely(this.ctx.sdk);
16154
16218
  if (activeVault) {
16155
16219
  this.ctx.addVault(activeVault);
16156
16220
  await this.ctx.setActiveVault(activeVault);
@@ -16164,7 +16228,7 @@ Error: ${error2.message}`));
16164
16228
  this.eventBuffer.setupVaultListeners(vault);
16165
16229
  }
16166
16230
  });
16167
- if (!this.ctx.getActiveVault() && this.ctx.getVaults().size > 0) {
16231
+ if (shouldAutoSelectActiveVault(!!this.ctx.getActiveVault(), corruptPointer, this.ctx.getVaults().size)) {
16168
16232
  const firstVault = this.ctx.getVaults().values().next().value;
16169
16233
  await this.ctx.setActiveVault(firstVault);
16170
16234
  }
@@ -16174,6 +16238,11 @@ Error: ${error2.message}`));
16174
16238
  } else {
16175
16239
  spinner.succeed("No vaults found");
16176
16240
  }
16241
+ if (corruptPointer && !this.ctx.getActiveVault() && this.ctx.getVaults().size > 0) {
16242
+ console.log(
16243
+ chalk14.yellow('Active vault pointer was corrupt and has been reset. Use "vault <name>" to pick one.')
16244
+ );
16245
+ }
16177
16246
  } catch (error2) {
16178
16247
  if (this.ctx.getVaults().size > 0) {
16179
16248
  spinner.succeed(`Loaded ${this.ctx.getVaults().size} vault(s)`);
@@ -16522,6 +16591,18 @@ complete -c vsig -n "__fish_seen_subcommand_from import export" -a "(__fish_comp
16522
16591
  `.trim();
16523
16592
  }
16524
16593
 
16594
+ // src/lib/config.ts
16595
+ import { FileStorage } from "@vultisig/sdk/node";
16596
+ import { homedir as homedir6 } from "os";
16597
+ import { join as join6 } from "path";
16598
+ function getConfigDir() {
16599
+ const override = process.env.VULTISIG_CONFIG_DIR?.trim();
16600
+ return override ? override : join6(homedir6(), ".vultisig");
16601
+ }
16602
+ function createVaultStorage() {
16603
+ return new FileStorage({ basePath: getConfigDir() });
16604
+ }
16605
+
16525
16606
  // src/lib/errors.ts
16526
16607
  import chalk15 from "chalk";
16527
16608
 
@@ -16596,6 +16677,7 @@ async function init(vaultOverride, unlockPassword, passwordTTL) {
16596
16677
  const globalOptions = program.opts();
16597
16678
  const serverEndpoints = resolveServerEndpoints(globalOptions);
16598
16679
  const sdk = new Vultisig6({
16680
+ storage: createVaultStorage(),
16599
16681
  onPasswordRequired: createPasswordCallback(),
16600
16682
  ...serverEndpoints ? { serverEndpoints } : {},
16601
16683
  ...passwordTTL !== void 0 ? { passwordCache: { defaultTTL: passwordTTL } } : {}
@@ -16609,7 +16691,7 @@ async function init(vaultOverride, unlockPassword, passwordTTL) {
16609
16691
  throw new Error(`Vault not found: "${vaultSelector}"`);
16610
16692
  }
16611
16693
  } else {
16612
- vault = await sdk.getActiveVault();
16694
+ vault = (await loadActiveVaultSafely(sdk)).vault;
16613
16695
  }
16614
16696
  if (vault) {
16615
16697
  await ctx.setActiveVault(vault);
@@ -17440,6 +17522,7 @@ program.command("schema", { hidden: true }).description("Output machine-readable
17440
17522
  async function startInteractiveMode() {
17441
17523
  const serverEndpoints = resolveServerEndpoints(parseServerEndpointOverridesFromArgv(process.argv.slice(2)));
17442
17524
  const sdk = new Vultisig6({
17525
+ storage: createVaultStorage(),
17443
17526
  onPasswordRequired: createPasswordCallback(),
17444
17527
  ...serverEndpoints ? { serverEndpoints } : {}
17445
17528
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vultisig/cli",
3
- "version": "2.19.7",
3
+ "version": "2.19.11",
4
4
  "description": "The self-custody MPC wallet CLI for AI coding agents (Claude Code, Cursor, OpenCode). Natural-language agent mode, 36+ chains, DKLS23 threshold signatures. Seedless.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,9 +74,9 @@
74
74
  "@napi-rs/keyring": "^1.3.0",
75
75
  "@noble/hashes": "^2.2.0",
76
76
  "@vultisig/client-shared": "^0.2.17",
77
- "@vultisig/core-chain": "^2.24.0",
77
+ "@vultisig/core-chain": "^2.24.2",
78
78
  "@vultisig/rujira": "^52.0.0",
79
- "@vultisig/sdk": "^2.19.7",
79
+ "@vultisig/sdk": "^2.19.11",
80
80
  "chalk": "^5.6.2",
81
81
  "cli-table3": "^0.6.5",
82
82
  "commander": "^15.0.0",