@candledottv/cli 0.11.19 → 0.11.21

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.
Files changed (3) hide show
  1. package/README.md +26 -1
  2. package/dist/index.js +691 -175
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @candledottv/cli
2
2
 
3
- The Candle CLI (current version 0.11.19): authorize a device from your browser, keep your own keys
3
+ The Candle CLI (current version 0.11.21): authorize a device from your browser, keep your own keys
4
4
  in an encrypted vault on this machine, hand an agent a TEE wallet it can trade, and manage API
5
5
  keys, wallets, and setup health from the terminal. Zero runtime dependencies; the whole thing is
6
6
  one self-contained `dist/index.js` that runs under plain Node.
@@ -8,6 +8,30 @@ one self-contained `dist/index.js` that runs under plain Node.
8
8
  The full custody guide is [CLI custody](https://docs.candle.tv/developers/cli-custody), and every
9
9
  command and flag is in the [Candle CLI reference](https://docs.candle.tv/developers/cli).
10
10
 
11
+ `candle vault rename --pairs-from <file> [--dry-run]` renames 1–256 vault or external keys under
12
+ one unlock and one commit. Use `<label|address|id> <new-label>` lines or CSV columns `from,to`
13
+ (picked by name; quote names with spaces or commas). Blank lines and `#` comments are skipped.
14
+ Every finding refuses the whole file before any write; swaps within the batch work. `--dry-run`
15
+ unlocks and prints the plan without writing; `--json` returns
16
+ `{ ok, renamed: [{ id, address, role, from, to }] }` (plus `dryRun: true` for a dry run).
17
+ Single and batch rename require new labels of at most 64 characters, without edge whitespace,
18
+ control characters or a leading `-`; names are never trimmed. Existing longer labels still
19
+ resolve as targets. A terminal is required. TEE wallets are refused by `vault rename`; use
20
+ `candle tee rename`.
21
+
22
+ `candle tee rename <label|address|id> <new-label>` renames a TEE wallet on both sides: the label
23
+ Candle holds and, when the wallet's key is in this vault, the vault entry.
24
+ `candle tee rename --pairs-from <file> [--dry-run]` does 1–256 of them from the same file shape
25
+ (lines, or CSV columns `from,to`) under the same label rule (at most 64 characters, never
26
+ trimmed). It is owner only (the device token; an API key cannot rename a wallet). One server
27
+ call renames every row, all or nothing, then one vault commit; the vault is unlocked once, and
28
+ only when a named TEE wallet may be in it. A new name a vault key outside the rename already has
29
+ refuses before the server is called. `--json` returns
30
+ `{ ok, renamed: [{ id, chain, address, from, to, server, vault }] }`. If the vault write fails
31
+ after the server accepted, the command prints address-keyed rows (`<address> <new-label>`, `rerun`
32
+ under `--json`) to run again: the old label no longer resolves on the server, and a re-run leaves
33
+ the server label alone and finishes the vault.
34
+
11
35
  ## Quick start
12
36
 
13
37
  ```
@@ -93,6 +117,7 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
93
117
  | `candle tee status\|disable\|sweep\|fund` | Operate a TEE wallet (Tier 2): `status` shows its server state and on-chain balances, `disable` stops the agent (exit 0 only on a verified stop, 3 while pending), `sweep --rpc-url <url>` moves everything to the pinned vault, signed locally (closing DAMM v2 positions after verifying each server-built close; `--emergency` sweeps when the API cannot be read and moves the position NFT instead), and `fund` prints what your vault signs. On a Hood wallet (0x), `status` shows ETH, USDG and the gas reserve, and `sweep` moves USDG, WETH, every `--token` and then ETH last. See [CLI custody](https://docs.candle.tv/developers/cli-custody). |
94
118
  | `candle tee signer new --key <prefix\|label> [--out <pem>] [--force]` | On the trading machine, with that key's API key: generates the key's signer here and waits for the owner to approve it with `keys signer approve`. The key's wallets then trade from this machine. `--out` also writes a plaintext PEM for an SDK process. |
95
119
  | `candle tee rebind <wallet...> --to-key <prefix\|label>`, `candle tee rebinds [wallet]` | Move TEE wallets to another key on the same account (owner only, preview then `confirm`; funds and the relay signer do not move), and list the rebind history. |
120
+ | `candle tee rename <label\|address\|id> <new-label>`, `candle tee rename --pairs-from <file> [--dry-run]` | Rename one TEE wallet, or 1–256 from a pairs file: the label Candle holds and the vault entry when there is one (owner only; one server call, all or nothing, then one vault commit). |
96
121
  | `candle tee new\|enable` | The legacy path: `new` seals a fresh key in its own `tee-wallets.enc` and `enable <address> --vault <address>` delegates it with a pinned sweep vault. New setups use `vault promote`; `vault import-legacy --tee` migrates an existing store. |
97
122
  | `candle swap <from> <to> --amount <n>\|--percent <n> --wallet <tee> [--to <tee>] [--wait]` | Quote, confirm and swap through the TEE wallet's bound key; the first buy after a launch is this command, unless `candle launch --buy` made it. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana). SOL or USDC to ETH or USDG, or back, is a bridge through Relay into this key's own TEE wallet on the other chain (`--to` names it when there are several); `--wait` follows it for up to ten minutes. |
98
123
  | `candle swap status <id> [--kind trade\|swap\|launch] [--wait]` | Read an operation without resending it; `--wait` follows a bridge until it ends. |
package/dist/index.js CHANGED
@@ -1319,6 +1319,7 @@ var init_errors = __esm(() => {
1319
1319
  "VAULT_LABEL_TAKEN",
1320
1320
  "VAULT_LABEL_UNCHANGED",
1321
1321
  "VAULT_RENAME_ROLE_REFUSED",
1322
+ "VAULT_RENAME_BATCH_REFUSED",
1322
1323
  "PROMOTE_BATCH_REFUSED",
1323
1324
  "WALLET_LIMIT_REACHED",
1324
1325
  "TIER_REQUIRED",
@@ -61914,9 +61915,9 @@ function verifyPerpsBuild2(build, opts) {
61914
61915
  return { ok: false, reason: `action type ${String(type)} is not one Candle builds` };
61915
61916
  }
61916
61917
  if (type === "order") {
61917
- const refusal = checkBuilderField2(action, opts.builder);
61918
- if (refusal)
61919
- return { ok: false, reason: refusal };
61918
+ const refusal2 = checkBuilderField2(action, opts.builder);
61919
+ if (refusal2)
61920
+ return { ok: false, reason: refusal2 };
61920
61921
  } else if ("builder" in action) {
61921
61922
  return { ok: false, reason: `a ${type} action carries no builder` };
61922
61923
  }
@@ -63166,7 +63167,7 @@ init_render();
63166
63167
  init_secret_store();
63167
63168
 
63168
63169
  // src/version.ts
63169
- var CLI_VERSION = "0.11.19";
63170
+ var CLI_VERSION = "0.11.21";
63170
63171
 
63171
63172
  // src/commands/auth.ts
63172
63173
  init_keys_embedded_wallet();
@@ -64067,8 +64068,12 @@ var HELP = {
64067
64068
  description: "Derive the next Solana key (m/44'/501'/n'/0') or EVM key (m/44'/60'/n'/0/0), or n of them under one unlock; the name must be free"
64068
64069
  },
64069
64070
  {
64070
- invocation: "rename <label|address> <new-label> [--id <entry-id>]",
64071
- description: "Rename one key. The address, the derivation and the key blob do not change"
64071
+ invocation: "rename <label|address|id> <new-label> [--id <entry-id>]",
64072
+ description: "Rename one vault or external key. New names: at most 64 characters, no edge whitespace. The address, the derivation and the key blob do not change"
64073
+ },
64074
+ {
64075
+ invocation: " rename --pairs-from <file> [--dry-run]",
64076
+ description: "Rename 1–256 vault or external keys under one unlock and one commit. All findings refuse the whole file; swaps are allowed. --dry-run prints the plan and writes nothing; --json returns the renamed rows"
64072
64077
  },
64073
64078
  { invocation: "phrase show", description: "Show the 24-word recovery phrase (terminal only)" },
64074
64079
  {
@@ -64149,7 +64154,11 @@ var HELP = {
64149
64154
  },
64150
64155
  {
64151
64156
  invocation: "--pairs-from <file>",
64152
- description: "promote-batch: one '<label> <destination>' per line, or a CSV with label and sweep_to columns (max 256). Every row is checked against the whole set before anything is written, and each key is committed on its own, so an interrupted batch keeps what landed and re-running the same file resumes."
64157
+ description: "rename: one '<label|address|id> <new-label>' per line, or CSV columns from,to (1–256 rows; blank lines and # comments skipped). No trimming of names; use CSV for names with spaces. promote-batch: one '<label> <destination>' per line, or a CSV with label and sweep_to columns (max 256). Every row is checked against the whole set before anything is written, and each key is committed on its own, so an interrupted batch keeps what landed and re-running the same file resumes."
64158
+ },
64159
+ {
64160
+ invocation: "--dry-run",
64161
+ description: "rename --pairs-from: unlock, check every row, print the plan, and write nothing"
64153
64162
  },
64154
64163
  {
64155
64164
  invocation: "--to-key <prefix|label>",
@@ -64163,6 +64172,7 @@ var HELP = {
64163
64172
  "candle vault transfer 0x000000000000000000000000000000000000dEaD --amount 0.5 --asset USDG --from hood-cold",
64164
64173
  "candle vault list cn-s",
64165
64174
  "candle vault rename key-7 treasury-cold",
64175
+ "candle vault rename --pairs-from ./rename-plan.csv --dry-run",
64166
64176
  "candle vault new-key --chain solana --labels-from ./replacement-names.txt",
64167
64177
  "candle vault promote-batch --pairs-from ./promote-plan.csv",
64168
64178
  "candle vault promote-batch --pairs-from ./promote-plan.csv --to-key tr-01",
@@ -64214,12 +64224,30 @@ var HELP = {
64214
64224
  description: "Move TEE wallets to another key on this account, which is how a key gets TEE wallets. Name them, or --label-prefix for every wallet whose label starts with it (owner only; funds do not move)"
64215
64225
  },
64216
64226
  { invocation: "rebinds [wallet]", description: "List TEE wallet rebinds for this account (owner only)" },
64227
+ {
64228
+ invocation: "rename <label|address|id> <new-label>",
64229
+ description: "Rename one TEE wallet: the label Candle holds and, when its key is in this vault, the vault entry (owner only). New names: at most 64 characters, no edge whitespace. Funds, the address and the key do not change"
64230
+ },
64231
+ {
64232
+ invocation: " rename --pairs-from <file> [--dry-run]",
64233
+ description: "Rename 1–256 TEE wallets with one server call, all or nothing, then one vault commit. Every finding refuses the whole file. The vault is unlocked once, only when a named TEE wallet may be in it. --dry-run prints the plan and changes nothing; if the vault write fails after the server accepted, it prints address-keyed rows to re-run"
64234
+ },
64217
64235
  {
64218
64236
  invocation: "signer new --key <prefix|label> [--out <pem>] [--force]",
64219
64237
  description: "On the trading machine, with that key's API key: generate the key's signer here and wait for the owner to approve it. Its wallets then trade from this machine. --out also writes a plaintext PEM for an SDK process (weaker than the secret store); --force adds a signer while an older signer on THIS machine still owns wallets; it moves, revokes and deletes nothing"
64220
64238
  }
64221
64239
  ],
64222
- flags: [KEYSTORE_FLAG],
64240
+ flags: [
64241
+ KEYSTORE_FLAG,
64242
+ {
64243
+ invocation: "--pairs-from <file>",
64244
+ description: "rename: one '<label|address|id> <new-label>' per line, or CSV columns from,to (1–256 rows; blank lines and # comments skipped). No trimming of names; use CSV for names with spaces."
64245
+ },
64246
+ {
64247
+ invocation: "--dry-run",
64248
+ description: "rename --pairs-from: resolve and check every row, print the plan, and change nothing"
64249
+ }
64250
+ ],
64223
64251
  examples: [
64224
64252
  "candle tee new --label AgentOne",
64225
64253
  "candle tee status AgentOneAddress",
@@ -64227,6 +64255,8 @@ var HELP = {
64227
64255
  "candle tee sweep 0x000000000000000000000000000000000000dEaD --emergency --from-block 1200000",
64228
64256
  "candle tee rebind tr-01 tr-02 --to-key Ab3dEf9h",
64229
64257
  "candle tee rebind --label-prefix dest- --to-key Ab3dEf9h",
64258
+ "candle tee rename tr-01 desk-01",
64259
+ "candle tee rename --pairs-from ./tee-names.csv --dry-run",
64230
64260
  "candle tee signer new --key tr-2"
64231
64261
  ],
64232
64262
  env: ENV_LOCAL_SIGNING
@@ -74618,6 +74648,644 @@ init_swap();
74618
74648
  init_tee();
74619
74649
  init_tee_rebind();
74620
74650
 
74651
+ // src/commands/tee-rename.ts
74652
+ init_args();
74653
+ init_deps();
74654
+ init_profiles();
74655
+ init_render();
74656
+ init_errors();
74657
+
74658
+ // src/vault/labels.ts
74659
+ function entriesWithLabel(index, label) {
74660
+ return index.entries.filter((entry) => entry.label === label);
74661
+ }
74662
+ function duplicateLabels(index) {
74663
+ const byLabel = new Map;
74664
+ for (const entry of index.entries) {
74665
+ const holders = byLabel.get(entry.label);
74666
+ if (holders === undefined)
74667
+ byLabel.set(entry.label, [entry]);
74668
+ else
74669
+ holders.push(entry);
74670
+ }
74671
+ const out = [];
74672
+ for (const [label, entries] of byLabel) {
74673
+ if (entries.length > 1)
74674
+ out.push({ label, entries });
74675
+ }
74676
+ return out;
74677
+ }
74678
+ function hasControlCharacter(label) {
74679
+ for (const char of label) {
74680
+ const code = char.codePointAt(0) ?? 0;
74681
+ if (code < 32 || code === 127)
74682
+ return true;
74683
+ }
74684
+ return false;
74685
+ }
74686
+ function validateLabel(label) {
74687
+ if (label.trim().length === 0)
74688
+ return "A key's label cannot be empty.";
74689
+ if (hasControlCharacter(label)) {
74690
+ return "A key's label cannot contain a newline, a tab or a control character.";
74691
+ }
74692
+ if (label.startsWith("-")) {
74693
+ return `A key's label cannot begin with "-": it would be read as a flag everywhere a label is typed.`;
74694
+ }
74695
+ if (label !== label.trim())
74696
+ return "A key's label cannot begin or end with whitespace.";
74697
+ if (label.length > 64)
74698
+ return "A key's label cannot exceed 64 characters.";
74699
+ return;
74700
+ }
74701
+ function resolveRenameTarget(index, old, id) {
74702
+ if (id !== undefined) {
74703
+ const entry = index.entries.find((candidate) => candidate.id === id);
74704
+ return entry === undefined ? { kind: "none" } : { kind: "found", entry, by: "id" };
74705
+ }
74706
+ const byLabel = entriesWithLabel(index, old);
74707
+ if (byLabel.length === 1)
74708
+ return { kind: "found", entry: byLabel[0], by: "label" };
74709
+ if (byLabel.length > 1)
74710
+ return { kind: "ambiguous", by: "label", candidates: byLabel };
74711
+ const byAddress = index.entries.filter((entry) => entry.address === old);
74712
+ if (byAddress.length === 1)
74713
+ return { kind: "found", entry: byAddress[0], by: "address" };
74714
+ if (byAddress.length > 1)
74715
+ return { kind: "ambiguous", by: "address", candidates: byAddress };
74716
+ const byId = index.entries.find((entry) => entry.id === old);
74717
+ if (byId !== undefined)
74718
+ return { kind: "found", entry: byId, by: "id" };
74719
+ return { kind: "none" };
74720
+ }
74721
+
74722
+ // src/vault/rename-batch.ts
74723
+ var MAX_RENAME_BATCH = 256;
74724
+ function csvCells(text) {
74725
+ const cells = [];
74726
+ let cell = "";
74727
+ let quoted = false;
74728
+ let closed = false;
74729
+ for (let i = 0;i < text.length; i++) {
74730
+ const char = text[i];
74731
+ if (quoted) {
74732
+ if (char === '"') {
74733
+ if (text[i + 1] === '"') {
74734
+ cell += '"';
74735
+ i++;
74736
+ } else {
74737
+ quoted = false;
74738
+ closed = true;
74739
+ }
74740
+ } else
74741
+ cell += char;
74742
+ } else if (char === ",") {
74743
+ cells.push(cell);
74744
+ cell = "";
74745
+ closed = false;
74746
+ } else if (char === '"' && cell === "" && !closed)
74747
+ quoted = true;
74748
+ else if (char === '"' || closed)
74749
+ return;
74750
+ else
74751
+ cell += char;
74752
+ }
74753
+ if (quoted)
74754
+ return;
74755
+ cells.push(cell);
74756
+ return cells;
74757
+ }
74758
+ function parseRenamePairs(contents) {
74759
+ const lines = contents.split(/\r?\n/).map((text, at) => ({ text, line: at + 1 })).filter(({ text }) => text.trim() !== "" && !/^\s*#/.test(text));
74760
+ const rows = [];
74761
+ const findings = [];
74762
+ const header = csvCells(lines[0]?.text ?? "")?.map((cell) => cell.trim()) ?? [];
74763
+ const csv = header.length > 1 && (header.includes("from") || header.includes("to"));
74764
+ if (csv && (header.filter((name2) => name2 === "from").length !== 1 || header.filter((name2) => name2 === "to").length !== 1)) {
74765
+ return {
74766
+ rows,
74767
+ findings: [{ line: lines[0]?.line ?? 0, problem: "A CSV needs exactly one from and one to column." }]
74768
+ };
74769
+ }
74770
+ const data = csv ? lines.slice(1) : lines;
74771
+ if (data.length < 1 || data.length > MAX_RENAME_BATCH) {
74772
+ findings.push({ line: 0, problem: `A rename file must hold 1 to ${MAX_RENAME_BATCH} rows; found ${data.length}.` });
74773
+ }
74774
+ for (const { text, line } of data) {
74775
+ let from;
74776
+ let to;
74777
+ if (csv) {
74778
+ const cells = csvCells(text);
74779
+ if (cells === undefined || cells.length !== header.length) {
74780
+ findings.push({ line, problem: "Malformed CSV row or field count does not match the header." });
74781
+ continue;
74782
+ }
74783
+ from = cells[header.indexOf("from")];
74784
+ to = cells[header.indexOf("to")];
74785
+ } else {
74786
+ const match = /^(\S+)[ \t]+(\S+)([ \t]*)$/.exec(text);
74787
+ if (match !== null)
74788
+ [from, to] = [match[1], `${match[2]}${match[3]}`];
74789
+ }
74790
+ if (from === undefined || from === "" || to === undefined) {
74791
+ findings.push({ line, problem: 'A row needs "<label|address|id> <new-label>" (CSV columns: from,to).' });
74792
+ continue;
74793
+ }
74794
+ rows.push({ line, from, to });
74795
+ const invalid = validateLabel(to);
74796
+ if (invalid !== undefined)
74797
+ findings.push({ line, problem: invalid });
74798
+ }
74799
+ for (const field of ["from", "to"]) {
74800
+ const seen = new Map;
74801
+ for (const row of rows) {
74802
+ const earlier = seen.get(row[field]);
74803
+ if (earlier !== undefined)
74804
+ findings.push({ line: row.line, problem: `Duplicate ${field} ${row[field]} (line ${earlier}).` });
74805
+ else
74806
+ seen.set(row[field], row.line);
74807
+ }
74808
+ }
74809
+ return { rows, findings };
74810
+ }
74811
+ function planBatchRename(index, rows) {
74812
+ const plan = [];
74813
+ const findings = [];
74814
+ const seen = new Map;
74815
+ for (const row of rows) {
74816
+ const match = resolveRenameTarget(index, row.from);
74817
+ if (match.kind !== "found") {
74818
+ findings.push({
74819
+ line: row.line,
74820
+ problem: match.kind === "none" ? `No key matches ${row.from}.` : `${row.from} is ambiguous; use an id. Candidates: ${match.candidates.map((entry2) => `${entry2.address} (${entry2.id})`).join(", ")}.`
74821
+ });
74822
+ continue;
74823
+ }
74824
+ const entry = match.entry;
74825
+ const earlier = seen.get(entry.id);
74826
+ if (earlier !== undefined)
74827
+ findings.push({ line: row.line, problem: `Same key as line ${earlier}, named by another handle.` });
74828
+ else
74829
+ seen.set(entry.id, row.line);
74830
+ if (entry.role !== "vault" && entry.role !== "external") {
74831
+ findings.push({
74832
+ line: row.line,
74833
+ problem: `${entry.label} is a TEE wallet; vault rename only changes vault or external keys. Use candle tee rename.`
74834
+ });
74835
+ }
74836
+ if (entry.label === row.to)
74837
+ findings.push({ line: row.line, problem: `${row.to} is already this key's label.` });
74838
+ plan.push({ ...row, entry });
74839
+ }
74840
+ const targets = new Set(plan.map(({ entry }) => entry.id));
74841
+ for (const row of plan) {
74842
+ if (index.entries.some((entry) => entry.label === row.to && !targets.has(entry.id))) {
74843
+ findings.push({ line: row.line, problem: `New name ${row.to} is taken by a key outside the batch.` });
74844
+ }
74845
+ }
74846
+ return { plan, findings };
74847
+ }
74848
+
74849
+ // src/commands/tee-rename.ts
74850
+ init_store();
74851
+ init_tee();
74852
+
74853
+ // src/commands/vault-rename.ts
74854
+ init_args();
74855
+ init_errors();
74856
+ init_store();
74857
+ init_vault_support();
74858
+ var RENAME_USAGE = "Usage: candle vault rename <label|address|id> <new-label> [--id <entry-id>] | --pairs-from <file> [--dry-run]";
74859
+ var SAME_STRING_LINE = "The two arguments are the same string; nothing to do.";
74860
+ async function vaultRename(args, ctx) {
74861
+ const parsed = parseArgs(args, {
74862
+ valueFlags: ["--keystore", "--id", "--pairs-from"],
74863
+ booleanFlags: ["--accept-older-copy", "--dry-run"],
74864
+ pathFlags: ["--keystore", "--pairs-from"]
74865
+ });
74866
+ if ("error" in parsed)
74867
+ return usage(ctx, parsed.error);
74868
+ const [old, next, extra] = parsed.positionals;
74869
+ const pairsFile = parsed.values["--pairs-from"];
74870
+ const batch = pairsFile !== undefined;
74871
+ if (batch ? parsed.positionals.length > 0 || parsed.values["--id"] !== undefined : old === undefined || next === undefined || extra !== undefined || parsed.booleans.has("--dry-run")) {
74872
+ return usage(ctx, RENAME_USAGE);
74873
+ }
74874
+ if (!batch && next !== undefined) {
74875
+ const invalid = validateLabel(next);
74876
+ if (invalid !== undefined)
74877
+ return usage(ctx, invalid);
74878
+ if (old === next)
74879
+ return usage(ctx, SAME_STRING_LINE);
74880
+ }
74881
+ const id = parsed.values["--id"];
74882
+ if (!refuseEnvPassphrase(ctx))
74883
+ return 1;
74884
+ if (!requireTty(ctx, "vault rename"))
74885
+ return 1;
74886
+ const { deps } = ctx;
74887
+ const resolvedVault = vaultPathFor(ctx, parsed);
74888
+ if ("error" in resolvedVault)
74889
+ return usage(ctx, resolvedVault.error);
74890
+ const path = resolvedVault.path;
74891
+ let rows = [];
74892
+ if (pairsFile !== undefined) {
74893
+ let contents;
74894
+ try {
74895
+ contents = await deps.readFile(pairsFile);
74896
+ } catch (error) {
74897
+ return usage(ctx, `Could not read --pairs-from: ${error instanceof Error ? error.message : error}`);
74898
+ }
74899
+ const parsedFile = parseRenamePairs(contents);
74900
+ if (parsedFile.findings.length > 0)
74901
+ return batchRefusal(ctx, parsedFile.findings, 2);
74902
+ rows = parsedFile.rows;
74903
+ }
74904
+ return runVaultCommand(ctx, async ({ hold }) => {
74905
+ const raw = await requireVaultRaw(ctx, resolvedVault);
74906
+ const opened = await unlockInteractively(ctx, path, raw, {
74907
+ acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
74908
+ });
74909
+ const vault = hold(opened.vault);
74910
+ if (batch) {
74911
+ const { plan, findings } = planBatchRename(vault.index, rows);
74912
+ if (findings.length > 0)
74913
+ return batchRefusal(ctx, findings, 1);
74914
+ const renamed = plan.map(({ entry: entry2, to }) => ({
74915
+ id: entry2.id,
74916
+ address: entry2.address,
74917
+ role: entry2.role,
74918
+ from: entry2.label,
74919
+ to
74920
+ }));
74921
+ const dryRun = parsed.booleans.has("--dry-run");
74922
+ if (!dryRun) {
74923
+ const names = new Map(renamed.map(({ id: id2, to }) => [id2, to]));
74924
+ await commitVault(vault, {
74925
+ index: {
74926
+ hd: vault.index.hd,
74927
+ entries: vault.index.entries.map((entry2) => names.has(entry2.id) ? { ...entry2, label: names.get(entry2.id) } : entry2)
74928
+ }
74929
+ }, deps);
74930
+ }
74931
+ if (ctx.json)
74932
+ writeJson(deps, { ok: true, renamed, ...dryRun ? { dryRun: true } : {} });
74933
+ else {
74934
+ deps.stdout.write(dryRun ? `Dry run: nothing was written.
74935
+ ` : `Renamed ${renamed.length} keys in one commit.
74936
+ `);
74937
+ for (const row of renamed)
74938
+ deps.stdout.write(` ${row.from} -> ${row.to} ${row.address} (${row.id}, ${row.role})
74939
+ `);
74940
+ }
74941
+ return 0;
74942
+ }
74943
+ if (old === undefined || next === undefined)
74944
+ return usage(ctx, RENAME_USAGE);
74945
+ const entry = resolveTarget(vault.index, old, id);
74946
+ if (entry.role === "tee-wallet") {
74947
+ throw new VaultError("VAULT_RENAME_ROLE_REFUSED", `${entry.label} is a TEE wallet. Its label was sent to Candle when it was enabled and \`candle wallets\` lists that copy, so renaming it here would give one wallet two names.`, {
74948
+ suggestion: `A vault key or an external wallet renames here. A TEE wallet renames on both sides with: candle tee rename ${entry.address} <new-label>`
74949
+ });
74950
+ }
74951
+ if (entry.label === next) {
74952
+ throw new VaultError("VAULT_LABEL_UNCHANGED", `${next} is already this key's label. Nothing was written.`, {
74953
+ suggestion: "Nothing to rename. `candle vault status --unlock` lists every label."
74954
+ });
74955
+ }
74956
+ if (entriesWithLabel(vault.index, next).length > 0) {
74957
+ throw new VaultError("VAULT_LABEL_TAKEN", `A key labelled ${next} already exists in this vault. Nothing was written.`, {
74958
+ suggestion: "Choose a name no key has, or rename that key first: `candle vault status --unlock` lists them."
74959
+ });
74960
+ }
74961
+ const from = entry.label;
74962
+ await commitVault(vault, {
74963
+ index: {
74964
+ hd: vault.index.hd,
74965
+ entries: vault.index.entries.map((candidate) => candidate.id === entry.id ? { ...candidate, label: next } : candidate)
74966
+ }
74967
+ }, deps);
74968
+ if (ctx.json) {
74969
+ writeJson(deps, { ok: true, id: entry.id, address: entry.address, role: entry.role, from, to: next });
74970
+ return 0;
74971
+ }
74972
+ deps.stdout.write(`Renamed ${from} to ${next}.
74973
+ `);
74974
+ deps.stdout.write(` address ${entry.address}
74975
+ `);
74976
+ deps.stdout.write(` id ${entry.id}
74977
+ `);
74978
+ deps.stdout.write(` role ${entry.role}
74979
+ `);
74980
+ deps.stdout.write(` unchanged address, derivation path, key blob, every envelope
74981
+ `);
74982
+ return 0;
74983
+ });
74984
+ }
74985
+ function batchRefusal(ctx, findings, exit) {
74986
+ const message = "Batch rename refused. Nothing was written.";
74987
+ if (ctx.json)
74988
+ writeJson(ctx.deps, { ok: false, code: "VAULT_RENAME_BATCH_REFUSED", message, findings });
74989
+ else {
74990
+ ctx.deps.stderr.write(`${message}
74991
+ `);
74992
+ for (const finding of findings)
74993
+ ctx.deps.stderr.write(` ${finding.line === 0 ? "file" : `line ${finding.line}`}: ${finding.problem}
74994
+ `);
74995
+ }
74996
+ return exit;
74997
+ }
74998
+ function resolveTarget(index, old, id) {
74999
+ const match = resolveRenameTarget(index, old, id);
75000
+ if (match.kind === "found")
75001
+ return match.entry;
75002
+ if (match.kind === "none") {
75003
+ throw new VaultError("VAULT_LABEL_NOT_FOUND", id === undefined ? `No key in this vault is called ${old}, and no key has that address or id.` : `No key in this vault has the id ${id}.`, { suggestion: "List them with their labels: `candle vault status --unlock`" });
75004
+ }
75005
+ const candidates = match.candidates.map((entry) => `${entry.address} (${entry.id})`).join(", ");
75006
+ const count2 = match.candidates.length;
75007
+ const first = match.candidates[0];
75008
+ throw new VaultError("VAULT_LABEL_AMBIGUOUS", match.by === "label" ? `${count2} keys in this vault are called ${old}, so this rename would not say which one it meant. Nothing was written.` : `${count2} keys in this vault have the address ${old}, so this rename would not say which one it meant. Nothing was written.`, {
75009
+ suggestion: match.by === "label" ? `Name one by address or id. The candidates are ${candidates}.` : `Name one by id. The candidates are ${candidates}. Re-run: \`candle vault rename ${old} <new-label> --id ${first.id}\`.`
75010
+ });
75011
+ }
75012
+
75013
+ // src/commands/tee-rename.ts
75014
+ init_vault_support();
75015
+ var RENAME_PATH = "/api/v1/agent/linked-wallets/rename";
75016
+ var TEE_RENAME_USAGE = "Usage: candle tee rename <label|address|id> <new-label> | --pairs-from <file> [--dry-run]";
75017
+ var DEVICE_TOKEN_REQUIRED2 = {
75018
+ code: "DEVICE_TOKEN_REQUIRED",
75019
+ message: "Renaming a TEE wallet needs the device token, the owner's credential; an API key cannot do it.",
75020
+ suggestion: "Run: candle auth login"
75021
+ };
75022
+ function rerunFile(rows) {
75023
+ if (rows.every((row) => !/[\s,"]/.test(row.to)))
75024
+ return rows.map((row) => `${row.from} ${row.to}`);
75025
+ return ["from,to", ...rows.map((row) => `${row.from},"${row.to.replaceAll('"', '""')}"`)];
75026
+ }
75027
+ function refusal(ctx, findings, exit) {
75028
+ const message = "TEE rename refused. Nothing was changed.";
75029
+ if (ctx.json)
75030
+ writeJson(ctx.deps, { ok: false, code: "TEE_RENAME_BATCH_REFUSED", message, findings });
75031
+ else {
75032
+ ctx.deps.stderr.write(`${message}
75033
+ `);
75034
+ for (const finding of findings)
75035
+ ctx.deps.stderr.write(` ${finding.line === 0 ? "file" : `line ${finding.line}`}: ${finding.problem}
75036
+ `);
75037
+ }
75038
+ return exit;
75039
+ }
75040
+ function serverFailure(ctx, result, pairs, batch) {
75041
+ const { deps, apiUrl, json } = ctx;
75042
+ if (result.status === 404) {
75043
+ writeLocalFailure(deps, {
75044
+ code: "TEE_RENAME_UNSUPPORTED",
75045
+ message: "This Candle API does not support renaming TEE wallets yet; nothing changed."
75046
+ }, json);
75047
+ return 1;
75048
+ }
75049
+ const raw = result.raw?.error?.findings;
75050
+ if (Array.isArray(raw) && raw.length > 0) {
75051
+ const findings = raw.map((finding) => {
75052
+ const ids = Array.isArray(finding.matches) ? ` Candidates: ${finding.matches.map((m) => String(m.id)).join(", ")}.` : "";
75053
+ return {
75054
+ line: batch ? pairs[Number(finding.row)]?.line ?? 0 : 0,
75055
+ problem: `${String(finding.problem)}${ids}`
75056
+ };
75057
+ });
75058
+ if (batch)
75059
+ return refusal(ctx, findings, 1);
75060
+ writeLocalFailure(deps, {
75061
+ code: "TEE_RENAME_REFUSED",
75062
+ message: `${findings.map((finding) => finding.problem).join(" ")} Nothing was changed.`,
75063
+ suggestion: "Run: candle wallets, to see this account's linked wallets and their labels."
75064
+ }, json);
75065
+ return 1;
75066
+ }
75067
+ writeFailure(deps, result, { apiUrl, authType: "device" }, json);
75068
+ return 1;
75069
+ }
75070
+ function vaultEntriesFor(entries, row) {
75071
+ const lower = row.address.toLowerCase();
75072
+ return entries.filter((entry) => entry.role === "tee-wallet" && (entry.linkedWalletId === row.id || entry.address === row.address || entry.chain === "evm" && row.chain === "evm" && entry.address.toLowerCase() === lower));
75073
+ }
75074
+ function unknownCommit(ctx, rows, onServer, inVault) {
75075
+ const { deps, json } = ctx;
75076
+ const changing = new Set([...onServer.map((row) => row.id), ...inVault.map(({ row }) => row.id)]);
75077
+ const rerun = rows.filter((row) => changing.has(row.id)).map((row) => ({ from: row.address, to: row.label }));
75078
+ const message = `The rename request got no answer, so whether the server applied it is unknown. Nothing was written to the vault. ${rerun.length} wallet${rerun.length === 1 ? "" : "s"} may or may not carry the new name on the server.`;
75079
+ if (json)
75080
+ writeJson(deps, { ok: false, code: "TEE_RENAME_SERVER_UNKNOWN", message, rerun });
75081
+ else {
75082
+ deps.stderr.write(`${message}
75083
+ `);
75084
+ deps.stderr.write(`Re-run it with these rows, saved to a file (the old labels may no longer resolve):
75085
+ `);
75086
+ for (const line of rerunFile(rerun))
75087
+ deps.stderr.write(` ${line}
75088
+ `);
75089
+ deps.stderr.write(`Then run: candle tee rename --pairs-from <file>
75090
+ `);
75091
+ }
75092
+ return 1;
75093
+ }
75094
+ async function teeRename(args, ctx) {
75095
+ const { deps, apiUrl, json } = ctx;
75096
+ if (!refuseEnvPassphrase2(ctx))
75097
+ return 1;
75098
+ const parsed = parseArgs(args, {
75099
+ valueFlags: ["--pairs-from"],
75100
+ booleanFlags: ["--dry-run", "--accept-older-copy"],
75101
+ pathFlags: ["--pairs-from"]
75102
+ });
75103
+ if ("error" in parsed)
75104
+ return usage(ctx, parsed.error);
75105
+ const [old, next, extra] = parsed.positionals;
75106
+ const pairsFile = parsed.values["--pairs-from"];
75107
+ const batch = pairsFile !== undefined;
75108
+ const dryRun = parsed.booleans.has("--dry-run");
75109
+ if (batch ? parsed.positionals.length > 0 : old === undefined || next === undefined || extra !== undefined || dryRun) {
75110
+ return usage(ctx, TEE_RENAME_USAGE);
75111
+ }
75112
+ let pairs;
75113
+ if (pairsFile !== undefined) {
75114
+ let contents;
75115
+ try {
75116
+ contents = await deps.readFile(pairsFile);
75117
+ } catch (error) {
75118
+ return usage(ctx, `Could not read --pairs-from: ${error instanceof Error ? error.message : error}`);
75119
+ }
75120
+ const parsedFile = parseRenamePairs(contents);
75121
+ if (parsedFile.findings.length > 0)
75122
+ return refusal(ctx, parsedFile.findings, 2);
75123
+ pairs = parsedFile.rows;
75124
+ } else {
75125
+ const invalid = validateLabel(next);
75126
+ if (invalid !== undefined)
75127
+ return usage(ctx, invalid);
75128
+ if (old === next)
75129
+ return usage(ctx, SAME_STRING_LINE);
75130
+ pairs = [{ line: 0, from: old, to: next }];
75131
+ }
75132
+ await printIdentity(ctx);
75133
+ const deviceToken = await resolveDeviceToken(deps, ctx.profile);
75134
+ if (!deviceToken) {
75135
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
75136
+ return 1;
75137
+ }
75138
+ const call = (body) => apiRequest(RENAME_PATH, {
75139
+ method: "POST",
75140
+ body,
75141
+ auth: "device",
75142
+ credentials: { deviceToken },
75143
+ apiUrl,
75144
+ fetch: deps.fetch,
75145
+ env: deps.env
75146
+ });
75147
+ const preview = await call({
75148
+ dryRun: true,
75149
+ renames: pairs.map((pair) => ({ wallet: pair.from, label: pair.to }))
75150
+ });
75151
+ if (!preview.ok)
75152
+ return serverFailure(ctx, preview, pairs, batch);
75153
+ const shown = preview.body;
75154
+ const rows = [...shown.renamed ?? [], ...shown.unchanged ?? []].sort((a, b) => a.row - b.row);
75155
+ if (rows.length !== pairs.length || rows.some((row, at) => row.row !== at || row.label !== pairs[at]?.to)) {
75156
+ writeLocalFailure(deps, {
75157
+ code: "TEE_RENAME_PREVIEW_INVALID",
75158
+ message: "The API's preview did not answer every row; nothing was changed."
75159
+ }, json);
75160
+ return 1;
75161
+ }
75162
+ const vaultPath = vaultPathFor(ctx, { values: {}, booleans: new Set, positionals: [] });
75163
+ if ("error" in vaultPath)
75164
+ return usage(ctx, vaultPath.error);
75165
+ const raw = rows.some((row) => row.tee) ? await readVaultRaw(vaultPath.path) : null;
75166
+ if (raw !== null && !requireTty(ctx, "tee rename"))
75167
+ return 1;
75168
+ return runVaultCommand(ctx, async ({ hold }) => {
75169
+ const vault = raw === null ? null : hold((await unlockInteractively(ctx, vaultPath.path, raw, {
75170
+ acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
75171
+ })).vault);
75172
+ const local = [];
75173
+ const findings = [];
75174
+ let twice = false;
75175
+ if (vault !== null) {
75176
+ for (const row of rows) {
75177
+ const line = pairs[row.row]?.line ?? 0;
75178
+ const entries = vaultEntriesFor(vault.index.entries, row);
75179
+ if (entries.length > 1) {
75180
+ twice = true;
75181
+ findings.push({
75182
+ line,
75183
+ problem: `The vault holds ${entries.length} TEE entries for ${row.address}; a rename would give them one name.`
75184
+ });
75185
+ } else if (entries[0] !== undefined)
75186
+ local.push({ row, entry: entries[0] });
75187
+ }
75188
+ const targets = new Set(local.map(({ entry }) => entry.id));
75189
+ for (const { row, entry } of local) {
75190
+ if (entry.label === row.label)
75191
+ continue;
75192
+ if (vault.index.entries.some((other) => other.label === row.label && !targets.has(other.id))) {
75193
+ findings.push({
75194
+ line: pairs[row.row]?.line ?? 0,
75195
+ problem: `New name ${row.label} is taken by a vault key outside this rename.`
75196
+ });
75197
+ }
75198
+ }
75199
+ }
75200
+ if (findings.length > 0) {
75201
+ if (batch)
75202
+ return refusal(ctx, findings, 1);
75203
+ writeLocalFailure(deps, {
75204
+ code: twice ? "VAULT_LABEL_AMBIGUOUS" : "VAULT_LABEL_TAKEN",
75205
+ message: `${findings.map((finding) => finding.problem).join(" ")} Nothing was changed.`,
75206
+ suggestion: twice ? "`candle vault list` shows both entries." : "Choose a name no vault key has: `candle vault list` shows them."
75207
+ }, json);
75208
+ return 1;
75209
+ }
75210
+ const localById = new Map(local.map(({ row, entry }) => [row.id, entry]));
75211
+ const receipt = rows.map((row) => {
75212
+ const entry = localById.get(row.id);
75213
+ return {
75214
+ id: row.id,
75215
+ chain: row.chain,
75216
+ address: row.address,
75217
+ from: row.from,
75218
+ to: row.label,
75219
+ server: row.from === row.label ? "unchanged" : "renamed",
75220
+ vault: entry === undefined ? "none" : entry.label === row.label ? "unchanged" : "renamed"
75221
+ };
75222
+ });
75223
+ const onServer = rows.filter((row) => row.from !== row.label);
75224
+ const inVault = local.filter(({ row, entry }) => entry.label !== row.label);
75225
+ const print = (headline) => {
75226
+ if (json)
75227
+ return writeJson(deps, { ok: true, renamed: receipt, ...dryRun ? { dryRun: true } : {} });
75228
+ deps.stdout.write(`${headline}
75229
+ `);
75230
+ for (const row of receipt) {
75231
+ const sides = row.server === "renamed" && row.vault === "renamed" ? "server and vault" : row.server === "renamed" ? "server" : row.vault === "renamed" ? "vault; the server already had this name" : "already named";
75232
+ const was = row.server === "renamed" ? row.from : localById.get(row.id)?.label ?? row.from;
75233
+ deps.stdout.write(` ${was ?? "(none)"} -> ${row.to} ${row.address} (${sides})
75234
+ `);
75235
+ }
75236
+ };
75237
+ if (dryRun) {
75238
+ print("Dry run: nothing was changed.");
75239
+ return 0;
75240
+ }
75241
+ if (onServer.length === 0 && inVault.length === 0) {
75242
+ print("Nothing to change: every wallet already has its new name.");
75243
+ return 0;
75244
+ }
75245
+ if (onServer.length > 0) {
75246
+ const committed = await call({ renames: onServer.map((row) => ({ id: row.id, label: row.label })) });
75247
+ if (!committed.ok) {
75248
+ if (committed.status === 0)
75249
+ return unknownCommit(ctx, rows, onServer, inVault);
75250
+ return serverFailure(ctx, committed, onServer.map((row) => pairs[row.row]), batch);
75251
+ }
75252
+ }
75253
+ if (vault !== null && inVault.length > 0) {
75254
+ const names = new Map(inVault.map(({ row, entry }) => [entry.id, row.label]));
75255
+ try {
75256
+ await commitVault(vault, {
75257
+ index: {
75258
+ hd: vault.index.hd,
75259
+ entries: vault.index.entries.map((entry) => names.has(entry.id) ? { ...entry, label: names.get(entry.id) } : entry)
75260
+ }
75261
+ }, deps);
75262
+ } catch (error) {
75263
+ const rerun = inVault.map(({ row }) => ({ from: row.address, to: row.label }));
75264
+ const cause = isVaultError(error) ? error.code : "VAULT_UNREADABLE";
75265
+ const why = error instanceof Error ? error.message : String(error);
75266
+ const message = `The server renamed ${onServer.length} wallet${onServer.length === 1 ? "" : "s"}, but the vault write failed (${cause}: ${why}). ${rerun.length} vault ${rerun.length === 1 ? "entry still has its" : "entries still have their"} old name.`;
75267
+ if (json) {
75268
+ writeJson(deps, { ok: false, code: "TEE_RENAME_VAULT_INCOMPLETE", message, cause, rerun, renamed: receipt });
75269
+ } else {
75270
+ deps.stderr.write(`${message}
75271
+ `);
75272
+ deps.stderr.write(`Finish it with these rows, saved to a file (the old labels no longer resolve):
75273
+ `);
75274
+ for (const line of rerunFile(rerun))
75275
+ deps.stderr.write(` ${line}
75276
+ `);
75277
+ deps.stderr.write(`Then run: candle tee rename --pairs-from <file>
75278
+ `);
75279
+ }
75280
+ return 1;
75281
+ }
75282
+ }
75283
+ const changed = receipt.filter((row) => row.server === "renamed" || row.vault === "renamed").length;
75284
+ print(`Renamed ${changed} wallet${changed === 1 ? "" : "s"}.`);
75285
+ return 0;
75286
+ });
75287
+ }
75288
+
74621
75289
  // src/commands/transfer.ts
74622
75290
  init_args();
74623
75291
  init_evm_lite();
@@ -79621,9 +80289,9 @@ async function refuseWithoutRoom(ctx) {
79621
80289
  ctx.deps.stderr.write(unreadableRoomLine(read.reason));
79622
80290
  return;
79623
80291
  }
79624
- const refusal = singleRoomRefusal(read.room);
79625
- if (refusal !== null)
79626
- throw refusal;
80292
+ const refusal2 = singleRoomRefusal(read.room);
80293
+ if (refusal2 !== null)
80294
+ throw refusal2;
79627
80295
  }
79628
80296
  async function reopenFromDisk(path, reopen, previous) {
79629
80297
  closeVault(previous);
@@ -80396,15 +81064,15 @@ function annotate(error, row, classified, pinnedBy, promotedBy) {
80396
81064
  var CONTINUATION_SENTENCE = (total) => `Each row was checked against the vault as it will be when the rows above it have run, so this is
80397
81065
  ` + `every row that fails on that basis. Fix these rows and run again; the next run re-checks all ${total}
80398
81066
  ` + `from scratch.`;
80399
- function writeBatchRefusal(deps, json, refusal) {
80400
- const n = refusal.rows.length;
81067
+ function writeBatchRefusal(deps, json, refusal2) {
81068
+ const n = refusal2.rows.length;
80401
81069
  if (json) {
80402
81070
  deps.stdout.write(`${JSON.stringify({
80403
81071
  ok: false,
80404
81072
  code: "PROMOTE_BATCH_REFUSED",
80405
- message: `${n} of ${refusal.total} rows cannot run; nothing was written.`,
81073
+ message: `${n} of ${refusal2.total} rows cannot run; nothing was written.`,
80406
81074
  suggestion: "Fix the rows below and run again; the next run re-checks every row from scratch.",
80407
- rows: refusal.rows.map((row) => ({
81075
+ rows: refusal2.rows.map((row) => ({
80408
81076
  line: row.line,
80409
81077
  label: row.label,
80410
81078
  destination: row.destination,
@@ -80416,18 +81084,18 @@ function writeBatchRefusal(deps, json, refusal) {
80416
81084
  `);
80417
81085
  return;
80418
81086
  }
80419
- const table = renderTable(["line", "label", "destination", "code", "why"], refusal.rows.map((row) => [
81087
+ const table = renderTable(["line", "label", "destination", "code", "why"], refusal2.rows.map((row) => [
80420
81088
  String(row.line),
80421
81089
  row.label,
80422
81090
  row.destination,
80423
81091
  row.code,
80424
81092
  row.why !== undefined ? `${row.message} ${row.why}` : row.message
80425
81093
  ]));
80426
- deps.stderr.write(`This batch was refused and NOTHING was written. ${n} of ${refusal.total} rows cannot run:
81094
+ deps.stderr.write(`This batch was refused and NOTHING was written. ${n} of ${refusal2.total} rows cannot run:
80427
81095
 
80428
81096
  ${table}
80429
81097
 
80430
- ${CONTINUATION_SENTENCE(refusal.total)}
81098
+ ${CONTINUATION_SENTENCE(refusal2.total)}
80431
81099
  `);
80432
81100
  }
80433
81101
  function parseValueUsdCell(raw) {
@@ -81248,159 +81916,6 @@ function finish(ctx, opts) {
81248
81916
  return exit;
81249
81917
  }
81250
81918
 
81251
- // src/commands/vault-rename.ts
81252
- init_args();
81253
- init_errors();
81254
-
81255
- // src/vault/labels.ts
81256
- function entriesWithLabel(index, label) {
81257
- return index.entries.filter((entry) => entry.label === label);
81258
- }
81259
- function duplicateLabels(index) {
81260
- const byLabel = new Map;
81261
- for (const entry of index.entries) {
81262
- const holders = byLabel.get(entry.label);
81263
- if (holders === undefined)
81264
- byLabel.set(entry.label, [entry]);
81265
- else
81266
- holders.push(entry);
81267
- }
81268
- const out = [];
81269
- for (const [label, entries] of byLabel) {
81270
- if (entries.length > 1)
81271
- out.push({ label, entries });
81272
- }
81273
- return out;
81274
- }
81275
- function hasControlCharacter(label) {
81276
- for (const char of label) {
81277
- const code = char.codePointAt(0) ?? 0;
81278
- if (code < 32 || code === 127)
81279
- return true;
81280
- }
81281
- return false;
81282
- }
81283
- function validateLabel(label) {
81284
- if (label.trim().length === 0)
81285
- return "A key's label cannot be empty.";
81286
- if (hasControlCharacter(label)) {
81287
- return "A key's label cannot contain a newline, a tab or a control character.";
81288
- }
81289
- if (label.startsWith("-")) {
81290
- return `A key's label cannot begin with "-": it would be read as a flag everywhere a label is typed.`;
81291
- }
81292
- return;
81293
- }
81294
- function resolveRenameTarget(index, old, id) {
81295
- if (id !== undefined) {
81296
- const entry = index.entries.find((candidate) => candidate.id === id);
81297
- return entry === undefined ? { kind: "none" } : { kind: "found", entry, by: "id" };
81298
- }
81299
- const byLabel = entriesWithLabel(index, old);
81300
- if (byLabel.length === 1)
81301
- return { kind: "found", entry: byLabel[0], by: "label" };
81302
- if (byLabel.length > 1)
81303
- return { kind: "ambiguous", by: "label", candidates: byLabel };
81304
- const byAddress = index.entries.filter((entry) => entry.address === old);
81305
- if (byAddress.length === 1)
81306
- return { kind: "found", entry: byAddress[0], by: "address" };
81307
- if (byAddress.length > 1)
81308
- return { kind: "ambiguous", by: "address", candidates: byAddress };
81309
- return { kind: "none" };
81310
- }
81311
-
81312
- // src/commands/vault-rename.ts
81313
- init_store();
81314
- init_vault_support();
81315
- var RENAME_USAGE = "Usage: candle vault rename <label|address> <new-label> [--id <entry-id>]";
81316
- var SAME_STRING_LINE = "The two arguments are the same string; nothing to do.";
81317
- async function vaultRename(args, ctx) {
81318
- const parsed = parseArgs(args, {
81319
- valueFlags: ["--keystore", "--id"],
81320
- booleanFlags: ["--accept-older-copy"],
81321
- pathFlags: ["--keystore"]
81322
- });
81323
- if ("error" in parsed)
81324
- return usage(ctx, parsed.error);
81325
- const [old, next, extra] = parsed.positionals;
81326
- if (old === undefined || next === undefined || extra !== undefined)
81327
- return usage(ctx, RENAME_USAGE);
81328
- const invalid = validateLabel(next);
81329
- if (invalid !== undefined)
81330
- return usage(ctx, invalid);
81331
- if (old === next)
81332
- return usage(ctx, SAME_STRING_LINE);
81333
- const id = parsed.values["--id"];
81334
- if (!refuseEnvPassphrase(ctx))
81335
- return 1;
81336
- if (!requireTty(ctx, "vault rename"))
81337
- return 1;
81338
- const { deps } = ctx;
81339
- const resolvedVault = vaultPathFor(ctx, parsed);
81340
- if ("error" in resolvedVault)
81341
- return usage(ctx, resolvedVault.error);
81342
- const path = resolvedVault.path;
81343
- return runVaultCommand(ctx, async ({ hold }) => {
81344
- const raw = await requireVaultRaw(ctx, resolvedVault);
81345
- const opened = await unlockInteractively(ctx, path, raw, {
81346
- acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
81347
- });
81348
- const vault = hold(opened.vault);
81349
- const entry = resolveTarget(vault.index, old, id);
81350
- if (entry.role === "tee-wallet") {
81351
- throw new VaultError("VAULT_RENAME_ROLE_REFUSED", `${entry.label} is a TEE wallet. Its label was sent to Candle when it was enabled and \`candle wallets\` lists that copy, so renaming it here would give one wallet two names and nothing reconciles them.`, {
81352
- suggestion: "A vault key or an external wallet renames here. For a TEE wallet, nothing in this release changes the name on either side."
81353
- });
81354
- }
81355
- if (entry.label === next) {
81356
- throw new VaultError("VAULT_LABEL_UNCHANGED", `${next} is already this key's label. Nothing was written.`, {
81357
- suggestion: "Nothing to rename. `candle vault status --unlock` lists every label."
81358
- });
81359
- }
81360
- if (entriesWithLabel(vault.index, next).length > 0) {
81361
- throw new VaultError("VAULT_LABEL_TAKEN", `A key labelled ${next} already exists in this vault. Nothing was written.`, {
81362
- suggestion: "Choose a name no key has, or rename that key first: `candle vault status --unlock` lists them."
81363
- });
81364
- }
81365
- const from = entry.label;
81366
- await commitVault(vault, {
81367
- index: {
81368
- hd: vault.index.hd,
81369
- entries: vault.index.entries.map((candidate) => candidate.id === entry.id ? { ...candidate, label: next } : candidate)
81370
- }
81371
- }, deps);
81372
- if (ctx.json) {
81373
- writeJson(deps, { ok: true, id: entry.id, address: entry.address, role: entry.role, from, to: next });
81374
- return 0;
81375
- }
81376
- deps.stdout.write(`Renamed ${from} to ${next}.
81377
- `);
81378
- deps.stdout.write(` address ${entry.address}
81379
- `);
81380
- deps.stdout.write(` id ${entry.id}
81381
- `);
81382
- deps.stdout.write(` role ${entry.role}
81383
- `);
81384
- deps.stdout.write(` unchanged address, derivation path, key blob, every envelope
81385
- `);
81386
- return 0;
81387
- });
81388
- }
81389
- function resolveTarget(index, old, id) {
81390
- const match = resolveRenameTarget(index, old, id);
81391
- if (match.kind === "found")
81392
- return match.entry;
81393
- if (match.kind === "none") {
81394
- throw new VaultError("VAULT_LABEL_NOT_FOUND", id === undefined ? `No key in this vault is called ${old}, and no key has that address or id.` : `No key in this vault has the id ${id}.`, { suggestion: "List them with their labels: `candle vault status --unlock`" });
81395
- }
81396
- const candidates = match.candidates.map((entry) => `${entry.address} (${entry.id})`).join(", ");
81397
- const count2 = match.candidates.length;
81398
- const first = match.candidates[0];
81399
- throw new VaultError("VAULT_LABEL_AMBIGUOUS", match.by === "label" ? `${count2} keys in this vault are called ${old}, so this rename would not say which one it meant. Nothing was written.` : `${count2} keys in this vault have the address ${old}, so this rename would not say which one it meant. Nothing was written.`, {
81400
- suggestion: match.by === "label" ? `Name one by address or id. The candidates are ${candidates}.` : `Name one by id. The candidates are ${candidates}. Re-run: \`candle vault rename ${old} <new-label> --id ${first.id}\`.`
81401
- });
81402
- }
81403
-
81404
81919
  // src/commands/vault-restore.ts
81405
81920
  init_args();
81406
81921
  init_deps();
@@ -82810,7 +83325,7 @@ var PREVIEW_PATH = "/api/v1/agent/tee-wallets/allow-launch/preview";
82810
83325
  var capabilityPath = (id) => `/api/v1/agent/wallets/${encodeURIComponent(id)}/capabilities`;
82811
83326
  var USAGE_ALLOW = "Usage: candle wallets allow-launch <label|address|id|prefix*>... [--json]";
82812
83327
  var USAGE_DISALLOW = "Usage: candle wallets disallow-launch <label|address|id|prefix*>... [--yes] [--json]";
82813
- var DEVICE_TOKEN_REQUIRED2 = {
83328
+ var DEVICE_TOKEN_REQUIRED3 = {
82814
83329
  code: "DEVICE_TOKEN_REQUIRED",
82815
83330
  message: "Changing whether a TEE wallet may launch needs the device token, the owner's credential; an API key cannot do it.",
82816
83331
  suggestion: "Run: candle auth login"
@@ -82879,7 +83394,7 @@ async function setAllowLaunch(args, ctx, enabled) {
82879
83394
  await printIdentity(ctx);
82880
83395
  const deviceToken = await resolveDeviceToken(deps, ctx.profile);
82881
83396
  if (!deviceToken) {
82882
- writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED2, json);
83397
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED3, json);
82883
83398
  return 1;
82884
83399
  }
82885
83400
  if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
@@ -83127,7 +83642,7 @@ init_promote_support();
83127
83642
  var TRUST_PATH = "/api/v1/agent/linked-wallets/trust";
83128
83643
  var USAGE_TRUST = "Usage: candle wallets trust <label|address|id|prefix*>... [--json]";
83129
83644
  var USAGE_UNTRUST = "Usage: candle wallets untrust <label|address|id|prefix*>... [--yes] [--json]";
83130
- var DEVICE_TOKEN_REQUIRED3 = {
83645
+ var DEVICE_TOKEN_REQUIRED4 = {
83131
83646
  code: "DEVICE_TOKEN_REQUIRED",
83132
83647
  message: "Marking a wallet trusted needs the device token, the owner's credential; an API key cannot do it.",
83133
83648
  suggestion: "Run: candle auth login"
@@ -83205,7 +83720,7 @@ async function setTrust(args, ctx, trusted) {
83205
83720
  await printIdentity(ctx);
83206
83721
  const deviceToken = await resolveDeviceToken(deps, ctx.profile);
83207
83722
  if (!deviceToken) {
83208
- writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED3, json);
83723
+ writeLocalFailure(deps, DEVICE_TOKEN_REQUIRED4, json);
83209
83724
  return 1;
83210
83725
  }
83211
83726
  if (!skipConfirm && (!deps.isTTY.stdin || !deps.isTTY.stdout)) {
@@ -83542,6 +84057,7 @@ var COMMANDS = {
83542
84057
  sweep: teeSweep,
83543
84058
  rebind: teeRebind,
83544
84059
  rebinds: teeRebinds,
84060
+ rename: teeRename,
83545
84061
  signer: teeSigner
83546
84062
  }
83547
84063
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.11.19",
3
+ "version": "0.11.21",
4
4
  "description": "The Candle CLI: authorize a device from your browser, then manage API keys, wallets, and setup health from the terminal",
5
5
  "type": "module",
6
6
  "bin": {