@candledottv/cli 0.13.23 → 0.13.25

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 +25 -18
  2. package/dist/index.js +287 -100
  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.13.0): authorize a device from your browser, keep your own keys
3
+ The Candle CLI (current version 0.13.23): 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.
@@ -109,10 +109,10 @@ chosen words from your saved copy. Clear your terminal's scrollback too: the CLI
109
109
  | `candle keys limits <prefix> <asset>=<raw> [<asset>=<raw> ...]` | Sets a key's per-asset caps (`sol`, `usdc`, `cndl`, `eth`, `usdg`, in base units per transaction) with the profile's own API key. It can only tighten; raising or removing a cap is `LOOSEN_REQUIRES_SESSION`, and the owner does that in the web key manager. A device token cannot set these caps. |
110
110
  | `candle keys update <prefix\|label> --embedded-wallet allow\|deny [--yes]`, `candle keys self embedded-wallet deny` | Allow or deny a key the account's embedded wallet, with the device token (allowing needs the prefix typed back at a terminal). `self embedded-wallet deny` lets the profile's own key give it up with no prompt; a key can never allow itself. |
111
111
  | `candle keys signer approve <code> --key <prefix\|label> [--reject]`, `candle keys signer move <prefix\|label> [--to-key <prefix\|label>]` | Key signers for bots on a different machine from the vault. `approve` accepts (or rejects) a key's signer request with the device token, after you type the full fingerprint the trading machine printed. `move`, on the machine holding a wallet's current signer, moves every wallet on the key onto its active signer; `--to-key` moves a revoked key's wallets to a live key. |
112
- | `candle keys stop <prefix>` | Pause one key using the device token. Its requests are refused with `PROFILE_PAUSED` until resumed; the key and its local credential are kept. Repeating the command succeeds. |
113
- | `candle keys resume <prefix>` | Resume a stopped key using the device token. Repeating the command succeeds. An API key cannot stop or resume itself; a revoked key cannot be resumed. |
112
+ | `candle keys stop <prefix\|label>` | Pause one key using the device token. Its requests are refused with `PROFILE_PAUSED` until the owner resumes it in the signed-in console; the key and its local credential are kept. Repeating the command succeeds. |
113
+ | `candle keys resume <prefix\|label>` | A device token cannot resume a stopped key: the server answers `LOOSEN_REQUIRES_SESSION` and the key stays stopped. The owner resumes it in the signed-in console. An API key cannot stop or resume itself; a revoked key cannot be resumed. |
114
114
  | `candle keys rotate <prefix\|label> ([--grace <duration> \| --now] [--force] [--yes] [--idempotency-key <key>] \| --finish)` | Gives a key a new secret and keeps its prefix, label, access, limits, wallets and history. The old secret keeps working through `--grace` (default `1h`, at most `24h`) or stops at once with `--now`; `--finish` ends an open window early. When the key is the CLI's own, the new secret replaces it in the keychain. A copy in `CANDLE_API_KEY_FILE`, `CANDLE_API_KEY` or anywhere else must be updated by hand before the window ends; `candle doctor` warns while the old secret is in use. Each run sends an `Idempotency-Key` and retries a dropped connection once; if there is still no answer, rerun with the printed `--idempotency-key` to recover the first secret. |
115
- | `candle keys revoke <prefix>` | Revokes an API key by prefix. Revoking the CLI's own stored key also clears it locally. A prefix can start with `-`; the key commands accept it as typed, and `--` (`candle keys revoke -- -QVeHyie`) ends the options on any command. |
115
+ | `candle keys revoke <prefix\|label>` | Revokes an API key by prefix or label. A label resolves to its prefix the way `keys access` does; two live keys with the same label refuse and list their prefixes. Revoking the CLI's own stored key also clears it locally. A prefix can start with `-`; the key commands accept it as typed, and `--` (`candle keys revoke -- -QVeHyie`) ends the options on any command. |
116
116
  | `candle keys wallets <prefix>`, `keys wallets set <prefix> --wallets <id,id>`, `keys wallets scope <prefix> --scope all\|selected` | Each key is an agent profile with its own wallet set: list it, replace it, or limit the profile to its assigned wallets. |
117
117
  | `candle wallets` (alias `candle wallet`) | Shows the account's embedded (launch) wallets and any linked wallets, using the API key, with a `Signer` column saying whether this machine holds each linked wallet's signing key and a `Trusted` column saying whether it is yours (linked while signed in, or marked with `trust`). It links and revokes; it never generates or prints a key, which the vault now derives. |
118
118
  | `candle wallets import --chain solana\|evm`, `wallets revoke <wallet-id>` | Link a wallet you own (key from `--key-file` or a hidden prompt, sealed to Privy before it leaves the machine), or unlink one. |
@@ -120,9 +120,9 @@ chosen words from your saved copy. Clear your terminal's scrollback too: the CLI
120
120
  | `candle wallets authorize` | Gives agents access to the account's embedded wallets. The grant is yours to make in a browser (each wallet's owner is you, so neither the CLI nor the API can attach Candle's signer): this opens the Agents page's grant-only step, creates no key, and with an API key waits until the wallets show as delegated. On a new account, run it before `candle auth login`. |
121
121
  | `candle wallets trust\|untrust <label\|address\|id\|prefix*>...` | Mark linked wallets as yours so an agent's Withdraw key can move funds into them, or clear the mark. Owner only, over the device token, with a typed confirm; a trailing `*`, as in `'tr-*'`, selects by prefix. |
122
122
  | `candle wallets allow-launch\|disallow-launch <label\|address\|id\|prefix*>...` | Deprecated. Where the server has `TEE_LAUNCH_BY_SCOPE_ENABLED` on, a key's `launch:write` decides TEE launches: `allow-launch` saves the value and exits 0, and `disallow-launch` saves it, prints the server's `ALLOW_LAUNCH_NOT_ENFORCED` message and exits 1, because no launch is stopped. A server with the switch off still lets TEE wallets pay for a launch (`candle launch`) only after `allow-launch`. Owner only, over the device token; the screen shows each wallet's label, address and bound key before a typed confirm. `disallow-launch --yes` skips the confirm. |
123
- | `candle wallets close-empty [--wallet embedded\|<tee address\|label>] [--keep <mint>]... [--client-trade-id <id>] [--preview] [--yes]` | Closes a wallet's empty token accounts and returns their rent to it, after a preview: the embedded wallet (`transfer:write`), or a Solana TEE wallet bound to the key (`swap:write`), whose close transactions Candle builds, this machine checks and the key signer approves. `--preview` prints what would close — the whole preview as one JSON document on stdout under `--json` — and closes, signs and prompts nothing; headless without `--yes`, the `CONFIRMATION_REQUIRED` refusal carries that same preview under `details`. |
123
+ | `candle wallets close-empty [--wallet embedded\|<tee address\|label>] [--keep <mint>]... [--client-trade-id <id>] [--preview] [--yes]` | Closes a wallet's empty token accounts and returns their rent to it, after a preview: the embedded wallet (`swap:write` or `transfer:write`), or a Solana TEE wallet bound to the key (`swap:write`), whose close transactions Candle builds, this machine checks and the key signer approves. `--preview` prints what would close — the whole preview as one JSON document on stdout under `--json` — and closes, signs and prompts nothing; headless without `--yes`, the `CONFIRMATION_REQUIRED` refusal carries that same preview under `details`. |
124
124
  | `candle vault init\|status\|list\|new-key\|rename\|phrase show\|restore\|reconcile-exposure` | The encrypted local vault, `vault.enc` (Tier 1). `init` creates it with a passphrase factor and a 24-word recovery phrase (and no key), labels its read-back derivation as verification, and leaves security-key enrollment to the separate optional `candle vault enroll security-key` command; `new-key` derives a key inside it (`--count`, `--labels-from <file>` for many under one unlock), `list [filter] [--balances]` shows one line per key, `rename` changes a label only, `phrase show` displays the phrase on a terminal (never in `--json`, a log or a pipe), and `restore --phrase` rebuilds the derived keys on another machine with a new passphrase (`--count`, `--tee-count`, `--external-count`, `--evm-count` and `--evm-tee-count` for EVM keys and Hood TEE wallets, or a Solana gap scan with `--rpc-url`); a restored vault never allocates new keys. `reconcile-exposure` re-reads the account and adds exposure records, clearing nothing. No key ever leaves the vault to reach Candle. See [CLI custody](https://docs.candle.tv/developers/cli-custody). |
125
- | `candle vault factor list\|add passphrase\|add security-key\|add touch-id\|add passkey\|remove` (or `vault enroll <kind>`), `vault backup --to <path>\|icloud`, `vault verify-backup <path>` | What can open the vault, and proving a copy of it works: `backup` verifies the copy in full before reporting (a copy is sealed, keeping only the passphrase and security-key envelopes, unless it goes to a recognised local disk or removable drive, so a cloud folder and any path the CLI cannot place are both sealed unless `--accept-shared-domain` is passed), and `verify-backup` re-checks an existing one against this vault's key and address set. `add security-key` enrolls a FIDO2 security key (CTAP2 `hmac-secret`, user-verified with the key's PIN or biometric) through the bundled `candle-fido2` helper (`--install-helper` fetches and verifies this release's helper first); one key is not a recoverable factor, two keys are a pair, an enrolled key can authorize adding another, and the passphrase stays the recovery floor. `add touch-id` and `add passkey` are refused with `VAULT_FACTOR_UNSUPPORTED_ON_PLATFORM` until the Apple-signed helper ships. Every vault command then takes `--factor <id\|passphrase\|security-key>` to say which envelope opens it and `--device <id>` to name the key when several are attached. |
125
+ | `candle vault factor list\|add passphrase\|add security-key\|add touch-id\|add passkey\|remove` (or `vault enroll <kind>`), `vault backup --to <path>\|icloud`, `vault verify-backup <path>` | What can open the vault, and proving a copy of it works: `backup` verifies the copy in full before reporting (a copy is sealed, keeping only the passphrase and security-key envelopes, unless it goes to a recognised local disk or removable drive, so a cloud folder and any path the CLI cannot place are both sealed unless `--accept-shared-domain` is passed), and `verify-backup` re-checks an existing one against this vault's key and address set. `add security-key` enrolls a FIDO2 security key (CTAP2 `hmac-secret`, user-verified with the key's PIN or biometric) through the bundled `candle-fido2` helper (`--install-helper` fetches and verifies this release's helper first); one key is not a recoverable factor, two keys are a pair, an enrolled key can authorize adding another, and the passphrase stays the recovery floor. `add touch-id` is available in 0.13.23 on supported Macs with the signed `candle-enclave.app`; npm ships no native helper. `add passkey` remains unavailable (`VAULT_FACTOR_UNSUPPORTED_ON_PLATFORM`): the shipped helper lacks the associated-domain entitlement. Every vault command then takes `--factor <id\|passphrase\|security-key\|touch-id>` to say which envelope opens it and `--device <id>` to name the key when several are attached. |
126
126
  | `candle vault transfer\|promote\|promote-batch\|fund\|demote\|export-key` | Moving value and authority. `transfer` and `fund` sign locally after showing the decoded transaction (`transfer` from a vault key or a promoted TEE wallet, which warns when an agent may be trading it and reports the finalized transfer to Candle's history; `fund` reaches a TEE wallet from its pinned vault key, or an external wallet, with no `--yes`; both verify once, at the unlock, which names the send, then show the destination grouped in fours and take a typed `confirm`); `promote --from <cold label>` derives a fresh TEE wallet pinned to that key (an EVM vault key becomes a Hood TEE wallet), shows the pinned vault address and asks you to type `confirm` to promote the new wallet (promotion moves no funds), `promote --in-place <label> --sweep-to <cold label>` promotes a key at its own address after a typed warning, `promote-batch --pairs-from <file>` does many in place under one unlock, `--to-key` binds the result to another of your keys, and `demote` (disable then sweep; `--emergency` sweeps even when the stop request fails) sends it back to its pinned vault; `export-key` is the one ceremony that writes a single private key to a file you name. `demote` takes several addresses, or `--all --empty` for every Solana TEE wallet with nothing left to sweep, under one unlock and one typed `confirm` (`--yes` skips it), with a result line per wallet and one `--json` document. |
127
127
  | `candle external new\|list\|sweep` | External wallets for outside tools: keys on the vault's third branch (`m/44'/501'/n'/2'`), never delegated to Privy and never registered with Candle. `new` allocates one (and moves the vault to format version 3; refused in a restored vault), `list` shows them, and `sweep <external> --to <vault>` sends everything back to a named vault key, signed locally, confirmed by a typed `confirm` after the vault key's address is shown. See [Bring your own services](https://docs.candle.tv/developers/cli#bring-your-own-services). |
128
128
  | `candle sign [--file <path>] --wallet <external>... [--broadcast] [--yes]`, `candle sign message --wallet <external> [--file <path>] [--yes]` | The generic signer: a base64 transaction any tool built, legacy or v0, decoded (lookup tables resolved) and simulated over your own RPC before it is displayed and signed with an external wallet only. A vault key, a TEE wallet or an unnamed signer is refused; a failing simulation is refused with no override; `--yes` skips the confirmation prompt and nothing else. `sign message` signs the exact bytes of a file or of stdin. |
@@ -240,7 +240,7 @@ transparency log: [Verify a Candle release](https://docs.candle.tv/developers/ve
240
240
 
241
241
  Where the API has `OWNER_CONFIRM_ENABLED` on, an owner step made with the device token that widens what the account's keys can do also needs one approval in your browser, signed in as the account owner. The API answers `403 OWNER_CONFIRM_REQUIRED` and changes nothing; the CLI then:
242
242
 
243
- 1. Prints what you are approving (a summary the server built), `Open <url>` and `Code: XXXX-XXXX` on stderr, with "Only approve if you just ran this command." It opens the browser only with `--open`. Under `--json` it writes one line to stderr instead, `{"ok":false,"pending":true,"code":"OWNER_CONFIRM_REQUIRED","ownerConfirm":{"userCode","verificationUriComplete","expiresIn","step","summary"}}`, and keeps stdout for the final result.
243
+ 1. Prints what you are approving (a summary the server built), `Open <url>` and `Code: XXXX-XXXX` on stderr, with "Only approve if you just ran this command." When the API names the account the request was made as (0.13.24+), the first line reads `This needs your approval in the browser as <display name> (<short wallet>): <summary>` and a following line says the browser must be signed in as that account — so a wrong-account browser is obvious before it answers "Request not found". It opens the browser only with `--open`. Under `--json` it writes one line to stderr instead, `{"ok":false,"pending":true,"code":"OWNER_CONFIRM_REQUIRED","ownerConfirm":{"userCode","verificationUriComplete","expiresIn","step","summary","account":{"wallet","displayName"}}}`, and keeps stdout for the final result.
244
244
  2. Waits, polling every few seconds. The code expires 5 minutes after it was issued.
245
245
  3. When you approve at `/dev/agent/confirm`, sends the identical request again and prints its result. Deny it and the command exits 1 with `Denied in the browser; nothing changed.`; let it expire and it exits 1 with `The confirmation expired; nothing changed. Run the command again.`
246
246
  4. With `--no-wait` it prints the code and exits 7 without waiting. The command does not resume later: run it again without `--no-wait`, or make the change in the console.
@@ -294,22 +294,28 @@ The npm package ships no native helper: there the factor is refused with `VAULT_
294
294
  until a release build's `candle-fido2` is installed and `CANDLE_FIDO2_HELPER` points at it. No
295
295
  other factor is ever substituted for one that is refused.
296
296
 
297
- The Touch ID factor (`candle vault factor add touch-id`; not yet released) needs
298
- the signed `candle-enclave.app` helper, which the darwin tarball, the installer script and Homebrew
299
- place beside `candle` once a release ships it; a release cut before Apple's approval ships no
300
- helper and the CLI refuses the factor with a typed code that says so. The CLI verifies the
297
+ The Touch ID factor (`candle vault factor add touch-id`) is available in
298
+ [0.13.23](https://github.com/candledottv/cli/releases/tag/cli-v0.13.23) on Apple silicon Macs and Intel
299
+ Macs with a T2 chip. It needs the signed `candle-enclave.app` helper, which the darwin tarball,
300
+ the installer script and Homebrew place beside `candle`. The npm package ships no native helper:
301
+ install the matching signed bundle and set `CANDLE_ENCLAVE_HELPER`, or the command refuses with
302
+ `VAULT_HELPER_MISSING`. An untrusted helper is `VAULT_HELPER_UNTRUSTED`; Linux and a Mac without
303
+ a Secure Enclave are `VAULT_FACTOR_UNSUPPORTED_ON_PLATFORM`. The CLI verifies the
301
304
  helper's code signature with `/usr/bin/codesign` before trusting it, and `CANDLE_ENCLAVE_HELPER`
302
305
  points at a bundle elsewhere. The factor opens the vault on one Mac only and is never a recovery
303
306
  factor; the passphrase remains the recovery floor.
304
307
 
305
- The synced passkey factor (`candle vault factor add passkey`; not yet released)
306
- runs through the same signed helper on macOS 15 or later, and needs the helper's
308
+ The synced passkey factor (`candle vault factor add passkey`) remains unavailable in 0.13.23:
309
+ the shipped helper deliberately omits `webcredentials:cli.candle.tv`, so it refuses with
310
+ `VAULT_FACTOR_UNSUPPORTED_ON_PLATFORM` and explains the missing entitlement. Passkey enrollment
311
+ has not yet been checked on real Macs. A future release needs macOS 15 or later and the helper's
307
312
  associated-domains entitlement for `webcredentials:cli.candle.tv` with an embedded provisioning
308
313
  profile, plus an `apple-app-site-association` served at
309
314
  `https://cli.candle.tv/.well-known/apple-app-site-association` (status 200, no redirect,
310
315
  `Content-Type: application/json`, `{"webcredentials":{"apps":["<TEAM ID>.tv.candle.cli.enclave"]}}`).
311
- Each missing piece is a typed refusal before any passkey sheet, and the refusal names what must be
312
- served. The passkey follows the Apple account, so it is a recoverable factor; two are one factor,
316
+ A missing association refuses with `VAULT_FACTOR_UNAVAILABLE` before any passkey sheet and names
317
+ what must be served; serving it allows real enrollment when the local requirements are met.
318
+ The passkey follows the Apple account, so it is a recoverable factor; two are one factor,
313
319
  and a copy of the vault is sealed (passphrase and security-key envelopes only) by default unless it goes to a
314
320
  recognised local disk or removable drive, so that one account never holds
315
321
  both the blob and a factor that opens it. iCloud Drive and other cloud folders are sealed for that
@@ -434,7 +440,7 @@ the profile they were migrated into.
434
440
  `CANDLE_DEVICE_TOKEN` and `CANDLE_API_KEY` together mean CI needs no storage backend at all: set
435
441
  both and every command works without ever touching a keychain or the encrypted file.
436
442
 
437
- ### External key files (unreleased source support)
443
+ ### External key files (CLI 0.13.22 and later)
438
444
 
439
445
  A host may supply an absolute path with both raw key variables absent:
440
446
 
@@ -469,8 +475,9 @@ The key stays in private memory; it is never inserted into environment variables
469
475
  config or saved files. Ordinary browser/helper/hook/keychain children retain the nonsecret path;
470
476
  plugins keep their environment allowlist and receive neither the path nor key. This reduces
471
477
  environment exposure, not access by root or another process with the same UID. Extended ACLs
472
- are not audited. Source readiness does not imply an installed npm/binary release supports this;
473
- publication is a separately authorized human release.
478
+ are not audited. This is available in `candle` and the bundled `candle mcp` from CLI 0.13.22.
479
+ For standalone `candle-mcp` (`@candledottv/mcp`), this remains unreleased source support:
480
+ version 0.10.32 does not include it and needs its own next release.
474
481
 
475
482
  ## What this CLI deliberately does not do
476
483
 
package/dist/index.js CHANGED
@@ -225,6 +225,11 @@ function ownerConfirmChallengeOf(result) {
225
225
  const verificationUriComplete = str("verificationUriComplete");
226
226
  if (!confirmId || !userCode || !verificationUriComplete)
227
227
  return null;
228
+ const accountRaw = oc.account;
229
+ const account = accountRaw && typeof accountRaw === "object" && !Array.isArray(accountRaw) && typeof accountRaw.wallet === "string" ? {
230
+ wallet: accountRaw.wallet,
231
+ displayName: typeof accountRaw.displayName === "string" ? accountRaw.displayName : null
232
+ } : undefined;
228
233
  return {
229
234
  confirmId,
230
235
  userCode,
@@ -234,6 +239,7 @@ function ownerConfirmChallengeOf(result) {
234
239
  interval: Math.max(1, num("interval") ?? 5),
235
240
  step: str("step") ?? "",
236
241
  summary: str("summary") ?? "",
242
+ ...account ? { account } : {},
237
243
  reason: str("reason") ?? "required"
238
244
  };
239
245
  }
@@ -728,6 +734,23 @@ var init_agent_key_access = __esm(() => {
728
734
  ];
729
735
  });
730
736
 
737
+ // src/terminal-text.ts
738
+ function escapeTerminalText(value) {
739
+ return value.replace(/[\u0000-\u0008\u000a-\u001f\u007f-\u009f]/g, (char) => {
740
+ if (char === `
741
+ `)
742
+ return "\\n";
743
+ if (char === "\r")
744
+ return "\\r";
745
+ return `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`;
746
+ });
747
+ }
748
+ function terminalLines(value) {
749
+ return value.split(`
750
+ `).map(escapeTerminalText).join(`
751
+ `);
752
+ }
753
+
731
754
  // src/terminal-theme.ts
732
755
  var TERMINAL_TOKENS;
733
756
  var init_terminal_theme = __esm(() => {
@@ -755,7 +778,7 @@ var init_terminal_theme = __esm(() => {
755
778
  });
756
779
 
757
780
  // src/version.ts
758
- var CLI_VERSION = "0.13.23";
781
+ var CLI_VERSION = "0.13.25";
759
782
 
760
783
  // src/presentation.ts
761
784
  function terminalStyle(deps, stream, machine = false) {
@@ -775,6 +798,7 @@ function terminalStyle(deps, stream, machine = false) {
775
798
  };
776
799
  }
777
800
  function accent(text, style, role = "brand") {
801
+ text = terminalLines(text);
778
802
  if (!style.color)
779
803
  return text;
780
804
  const token = TERMINAL_TOKENS[style.theme][role];
@@ -938,6 +962,7 @@ function symbols(style) {
938
962
  return style.styled ? UNICODE_SYMBOLS : ASCII_SYMBOLS;
939
963
  }
940
964
  function sgr(text, style, code) {
965
+ text = terminalLines(text);
941
966
  return style.color && text ? `\x1B[${code}m${text}\x1B[0m` : text;
942
967
  }
943
968
  function bold(text, style) {
@@ -947,30 +972,32 @@ function dim(text, style) {
947
972
  return sgr(text, style, "2");
948
973
  }
949
974
  function heading(text, style) {
950
- return wrapText(text, proseWidth(style)).map((line) => bold(line, style)).join(`
975
+ return wrapText(terminalLines(text), proseWidth(style)).map((line) => bold(line, style)).join(`
951
976
  `);
952
977
  }
953
978
  function fields(rows, style, indent = 2) {
979
+ rows = rows.map(([label, value]) => [escapeTerminalText(label), value]);
954
980
  const budget = proseWidth(style);
955
981
  const pad = " ".repeat(Math.min(indent, Math.max(0, budget)));
956
982
  const labelWidth = Math.max(0, ...rows.map(([label]) => displayWidth(label)));
957
983
  const sideWidth = budget - pad.length - labelWidth - 2;
958
984
  const out = [];
959
985
  for (const [label, raw] of rows) {
960
- const value = raw === undefined || raw === null || raw === "" ? symbols(style).missing : String(raw);
986
+ const paint = (text) => typeof raw === "object" && raw !== null ? accent(text, style, raw.role) : text;
987
+ const value = raw === undefined || raw === null || raw === "" ? symbols(style).missing : escapeTerminalText(typeof raw === "object" ? raw.text : String(raw));
961
988
  if (sideWidth >= 1 && fitsWidth(value, sideWidth)) {
962
989
  const [first = "", ...rest] = value.split(`
963
990
  `).flatMap((line) => wrapText(line, sideWidth));
964
- out.push(`${pad}${dim(padDisplay(label, labelWidth), style)} ${first}`);
991
+ out.push(`${pad}${dim(padDisplay(label, labelWidth), style)} ${paint(first)}`);
965
992
  for (const line of rest)
966
- out.push(`${pad}${" ".repeat(labelWidth + 2)}${line}`);
993
+ out.push(`${pad}${" ".repeat(labelWidth + 2)}${paint(line)}`);
967
994
  } else {
968
- out.push(...stackedField(label, value, style, pad, budget));
995
+ out.push(...stackedField(label, value, style, pad, budget, paint));
969
996
  }
970
997
  }
971
998
  return out;
972
999
  }
973
- function stackedField(label, value, style, pad, budget) {
1000
+ function stackedField(label, value, style, pad, budget, paint) {
974
1001
  const labelBudget = Math.max(1, budget - pad.length);
975
1002
  const out = wrapText(label, labelBudget).map((line) => `${pad}${dim(line, style)}`);
976
1003
  const nested = budget - pad.length - 2;
@@ -987,11 +1014,12 @@ function stackedField(label, value, style, pad, budget) {
987
1014
  for (const part of value.split(`
988
1015
  `)) {
989
1016
  for (const line of wrapText(part, valueWidth))
990
- out.push(line ? `${valuePad}${line}` : "");
1017
+ out.push(line ? `${valuePad}${paint(line)}` : "");
991
1018
  }
992
1019
  return out;
993
1020
  }
994
1021
  function renderNotice(level, text, style, indent = 2) {
1022
+ text = terminalLines(text);
995
1023
  const sym = symbols(style);
996
1024
  const mark = level === "note" ? sym.note : level === "warning" ? sym.warning : sym.danger;
997
1025
  const budget = proseWidth(style);
@@ -1021,6 +1049,7 @@ ${renderNotice(level, text, style).join(`
1021
1049
  `);
1022
1050
  }
1023
1051
  function success(text, style) {
1052
+ text = terminalLines(text);
1024
1053
  const mark = symbols(style).ok;
1025
1054
  const hang = " ".repeat(displayWidth(mark) + 1);
1026
1055
  const budget = Math.max(1, proseWidth(style) - displayWidth(mark) - 1);
@@ -1042,17 +1071,19 @@ var init_presentation = __esm(() => {
1042
1071
  });
1043
1072
 
1044
1073
  // src/render.ts
1045
- function formatScopesForSummary(scopes) {
1046
- return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope === "transfer:bound" ? `${scope} (${TRANSFER_BOUND_NOTE})` : scope === "perps:write" ? `${scope} (${PERPS_WRITE_NOTE})` : scope).join(", ");
1047
- }
1048
1074
  function terminalText(value) {
1049
1075
  return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
1050
1076
  }
1077
+ function formatScopesForSummary(scopes) {
1078
+ return sortAgentKeyScopes(scopes).map((scope) => scope === "swap:write" ? `${scope} (${SWAP_WRITE_NOTE})` : scope === "transfer:write" ? `${scope} (${TRANSFER_WRITE_NOTE})` : scope === "transfer:bound" ? `${scope} (${TRANSFER_BOUND_NOTE})` : scope === "perps:write" ? `${scope} (${PERPS_WRITE_NOTE})` : scope).join(", ");
1079
+ }
1051
1080
  function numericColumn(rows, col) {
1052
1081
  const known = rows.map((row) => row[col] ?? "").filter((cell) => cell !== "" && cell !== "–" && cell !== "-");
1053
1082
  return known.length > 0 && known.every((cell) => NUMERIC_CELL.test(cell));
1054
1083
  }
1055
1084
  function renderTable(headers, rows, options) {
1085
+ headers = headers.map(escapeTerminalText);
1086
+ rows = rows.map((row) => row.map((cell) => escapeTerminalText(cell ?? "")));
1056
1087
  if (!options)
1057
1088
  return renderPlainTable(headers, rows);
1058
1089
  const specs = headers.map((_, col) => options.columns?.[col] ?? {});
@@ -1213,6 +1244,14 @@ function errorParts(result, ctx) {
1213
1244
  return { message: result.message, fixes: result.uiHint ? [{ label: result.uiHint }] : [] };
1214
1245
  }
1215
1246
  function renderErrorBlock(parts, style) {
1247
+ parts = {
1248
+ message: terminalLines(parts.message),
1249
+ fixes: parts.fixes.map((fix) => ({
1250
+ ...fix,
1251
+ label: terminalLines(fix.label),
1252
+ ...fix.command ? { command: terminalLines(fix.command) } : {}
1253
+ }))
1254
+ };
1216
1255
  const budget = proseWidth(style);
1217
1256
  const out = [...errorMessageLines(parts.message, style, budget)];
1218
1257
  if (parts.fixes.length === 0)
@@ -1280,7 +1319,7 @@ function writeFailure(deps, result, ctx, json) {
1280
1319
  deps.stdout.write(`${JSON.stringify(errorEnvelope(result, ctx))}
1281
1320
  `);
1282
1321
  else
1283
- deps.stderr.write(`${renderError(result, ctx)}
1322
+ deps.stderr.write(`${terminalLines(renderError(result, ctx))}
1284
1323
  `);
1285
1324
  }
1286
1325
  function writeLocalFailure(deps, failure2, json) {
@@ -1292,7 +1331,7 @@ function writeLocalFailure(deps, failure2, json) {
1292
1331
  const separator = failure2.suggestion?.includes(`
1293
1332
  `) ? `
1294
1333
  ` : " ";
1295
- deps.stderr.write(`${failure2.suggestion ? `${failure2.message}${separator}${failure2.suggestion}` : failure2.message}
1334
+ deps.stderr.write(`${terminalLines(failure2.suggestion ? `${failure2.message}${separator}${failure2.suggestion}` : failure2.message)}
1296
1335
  `);
1297
1336
  }
1298
1337
  function writeUsageFailure(deps, message, json) {
@@ -1300,7 +1339,7 @@ function writeUsageFailure(deps, message, json) {
1300
1339
  deps.stdout.write(`${JSON.stringify({ ok: false, code: "USAGE", message })}
1301
1340
  `);
1302
1341
  else
1303
- deps.stderr.write(`${message}
1342
+ deps.stderr.write(`${terminalLines(message)}
1304
1343
  `);
1305
1344
  }
1306
1345
  function portalDevicesUrl(apiUrl, portalOrigin) {
@@ -6852,7 +6891,7 @@ var init_launch_definition = __esm(() => {
6852
6891
  {
6853
6892
  key: "curve.virtualQuote",
6854
6893
  type: "decimal",
6855
- help: "exp-custom's virtual quote, in whole quote units: 1 to 100 SOL or 66000 to 6600000 CNDL; requires --curve-g and EXP_CUSTOM_CURVES.",
6894
+ help: "exp-custom's virtual quote, in whole quote units: 1 to 100 SOL or 66000 to 6600000 CNDL; requires --curve-g.",
6856
6895
  modes: [
6857
6896
  "exp-custom"
6858
6897
  ],
@@ -6891,7 +6930,7 @@ var init_launch_definition = __esm(() => {
6891
6930
  {
6892
6931
  key: "curve.threshold",
6893
6932
  type: "decimal",
6894
- help: "exp-custom's graduation threshold, in whole quote units: 1 to 250 SOL or 66000 to 16500000 CNDL; requires --curve-v and EXP_CUSTOM_CURVES.",
6933
+ help: "exp-custom's graduation threshold, in whole quote units: 1 to 250 SOL or 66000 to 16500000 CNDL; requires --curve-v.",
6895
6934
  modes: [
6896
6935
  "exp-custom"
6897
6936
  ],
@@ -20209,10 +20248,13 @@ function credentialEnvOverrides(env) {
20209
20248
  return ["CANDLE_API_KEY_FILE", "CANDLE_API_KEY", "CANDLE_DEVICE_TOKEN"].filter((name) => env[name]?.trim());
20210
20249
  }
20211
20250
  function identityLine(profile, account, apiUrl, overrides, username, options = {}) {
20212
- const address = account === undefined ? undefined : shortId(account, options.full);
20251
+ profile = profile === undefined ? undefined : escapeTerminalText(profile);
20252
+ username = username === undefined ? undefined : escapeTerminalText(username);
20253
+ overrides = overrides?.map(escapeTerminalText);
20254
+ const address = account === undefined ? undefined : escapeTerminalText(shortId(account, options.full));
20213
20255
  const shown = overrides?.length ? address ? `${address} (${overrides.join(", ")} override)` : `unknown (${overrides.join(", ")} override)` : username && address ? `${username} (${address})` : address ?? "unknown";
20214
20256
  const name = profile ?? "none";
20215
- const host = apiUrl.replace(/^https:\/\//i, "").replace(/\/+$/, "");
20257
+ const host = escapeTerminalText(apiUrl).replace(/^https:\/\//i, "").replace(/\/+$/, "");
20216
20258
  const sep2 = options.style ? symbols(options.style).note : "·";
20217
20259
  return `Profile ${options.style ? bold(name, options.style) : name} ${sep2} Account ${shown} ${sep2} ${host}`;
20218
20260
  }
@@ -25682,6 +25724,76 @@ var init_hood_legs = __esm(() => {
25682
25724
  V4_SWAP_PARAMS = { tuple: [V4_POOL_KEY, "bool", "uint", "uint", "uint", "bytes"] };
25683
25725
  });
25684
25726
 
25727
+ // src/hood-operation-wait.ts
25728
+ async function waitForHoodOperation(ctx, key, id, evidence) {
25729
+ const deadline = ctx.deps.now() + WAIT_MS;
25730
+ const hint = `candle swap status --operation ${safeText(id)}`;
25731
+ const readCtx = {
25732
+ ...ctx,
25733
+ deps: {
25734
+ ...ctx.deps,
25735
+ fetch: (input, init) => ctx.deps.fetch(input, {
25736
+ ...init,
25737
+ signal: AbortSignal.timeout(Math.max(1, deadline - ctx.deps.now()))
25738
+ })
25739
+ }
25740
+ };
25741
+ for (;; ) {
25742
+ let result2;
25743
+ try {
25744
+ result2 = responseSchema.parse(await request(readCtx, key, `/api/v1/agent/hood-tee/operations/${encodeURIComponent(id)}`));
25745
+ if (result2.operation.operationId !== id)
25746
+ throw new Error("The server returned a different operation");
25747
+ } catch (error) {
25748
+ throw new TradingError("HOOD_WAIT_UNAVAILABLE", `Could not follow Hood settlement. Do not repeat the swap. Read ${hint}.`, {
25749
+ exitCode: 3,
25750
+ details: {
25751
+ ...evidence,
25752
+ operationId: id,
25753
+ readCode: error instanceof TradingError ? error.code : "INVALID_RESPONSE"
25754
+ }
25755
+ });
25756
+ }
25757
+ const op = result2.operation;
25758
+ const held = op.guard?.recoveryRequired === true || op.recovery?.recoveryRequired === true;
25759
+ const completed = op.status === "completed" && op.legs.length === op.plannedLegCount && op.legs.every((leg) => leg.status === "landed");
25760
+ const failed = op.status === "failed" || op.status === "expired";
25761
+ const final = !held && (completed || failed);
25762
+ if (final || ctx.deps.now() >= deadline) {
25763
+ ctx.deps.stdout.write(`${JSON.stringify(result2, null, ctx.json ? undefined : 2)}
25764
+ `);
25765
+ if (!final)
25766
+ ctx.deps.stderr.write(`Still not final after ten minutes. Do not repeat the swap. Read ${hint}.
25767
+ `);
25768
+ else if (failed)
25769
+ ctx.deps.stderr.write(`Hood operation ${safeText(op.status)}. Do not repeat the swap. Read ${hint}.
25770
+ `);
25771
+ if (!final)
25772
+ return 3;
25773
+ if (completed)
25774
+ return 0;
25775
+ return op.legs.some((leg) => leg.kind === "trade" && leg.status === "landed") ? 3 : 1;
25776
+ }
25777
+ await ctx.deps.sleep(Math.min(POLL_MS, deadline - ctx.deps.now()));
25778
+ }
25779
+ }
25780
+ var WAIT_MS, POLL_MS = 5000, responseSchema;
25781
+ var init_hood_operation_wait = __esm(() => {
25782
+ init_zod();
25783
+ init_trading();
25784
+ WAIT_MS = 10 * 60000;
25785
+ responseSchema = exports_external.object({
25786
+ operation: exports_external.object({
25787
+ operationId: exports_external.string(),
25788
+ status: exports_external.enum(["active", "completed", "failed", "expired"]),
25789
+ plannedLegCount: exports_external.number().int().positive(),
25790
+ legs: exports_external.array(exports_external.object({ kind: exports_external.string(), status: exports_external.string() }).passthrough()),
25791
+ guard: exports_external.object({ recoveryRequired: exports_external.boolean() }).passthrough().optional(),
25792
+ recovery: exports_external.object({ recoveryRequired: exports_external.boolean() }).passthrough().optional()
25793
+ }).passthrough()
25794
+ }).passthrough();
25795
+ });
25796
+
25685
25797
  // src/human-output.ts
25686
25798
  function writeFields(deps, stream, rows, title) {
25687
25799
  const style = terminalStyle(deps, stream);
@@ -26005,7 +26117,7 @@ function walletRecordCheck(row, trading, records) {
26005
26117
  differences
26006
26118
  } : pass("record");
26007
26119
  }
26008
- async function inspectReport(ctx, key, row, listing, records, payable, name) {
26120
+ async function inspectReport(ctx, key, row, listing, records, payable, name, chain2) {
26009
26121
  const checks = [pass("key"), payable ? failure2("payable", payable) : pass("payable")];
26010
26122
  checks.push(listing.scopes.includes("swap:write") ? pass("scope") : failure2("scope", new TradingError("SCOPE_MISSING", "The bound key needs swap:write.")));
26011
26123
  const report = {
@@ -26018,7 +26130,7 @@ async function inspectReport(ctx, key, row, listing, records, payable, name) {
26018
26130
  return { report };
26019
26131
  }
26020
26132
  checks.push(walletRecordCheck(row, listing.rows, records));
26021
- const inspected = await inspectTradingWallet(ctx, row, listing.appId, "swap:write", "solana", {
26133
+ const inspected = await inspectTradingWallet(ctx, row, listing.appId, "swap:write", chain2 ?? rowChain(row) ?? "solana", {
26022
26134
  apiKey: key,
26023
26135
  keyPrefix: listing.keyPrefix
26024
26136
  });
@@ -26211,14 +26323,18 @@ async function buildCheck(ctx, key, wallet3, report, from, to, amount, slippage,
26211
26323
  }
26212
26324
  }
26213
26325
  async function teeCheck(args, ctx) {
26214
- const parsed = parseArgs(args, { valueFlags: ["--label-prefix", "--max", "--rpc-url"], booleanFlags: ["--sign"] });
26326
+ const parsed = parseArgs(args, {
26327
+ valueFlags: ["--label-prefix", "--max", "--rpc-url", "--chain"],
26328
+ booleanFlags: ["--sign"]
26329
+ });
26215
26330
  if ("error" in parsed) {
26216
26331
  writeUsageFailure(ctx.deps, parsed.error, ctx.json);
26217
26332
  return 2;
26218
26333
  }
26219
26334
  const max = Number(parsed.values["--max"] ?? "10");
26220
- if (!parsed.positionals.length && parsed.values["--label-prefix"] === undefined || !Number.isSafeInteger(max) || max < 1) {
26221
- writeUsageFailure(ctx.deps, "Usage: candle tee check <address|id|label>... [--label-prefix <p>] [--sign] [--max <n>] [--rpc-url <url>]", ctx.json);
26335
+ const chainFlag = parsed.values["--chain"];
26336
+ if (!parsed.positionals.length && parsed.values["--label-prefix"] === undefined || !Number.isSafeInteger(max) || max < 1 || chainFlag !== undefined && chainFlag !== "solana" && chainFlag !== "hood") {
26337
+ writeUsageFailure(ctx.deps, "Usage: candle tee check <address|id|label>... [--label-prefix <p>] [--chain solana|hood] [--sign] [--max <n>] [--rpc-url <url>]", ctx.json);
26222
26338
  return 2;
26223
26339
  }
26224
26340
  try {
@@ -26250,8 +26366,13 @@ async function teeCheck(args, ctx) {
26250
26366
  for (const name of targets) {
26251
26367
  const matches = listing.rows.filter((row) => matchesName(row, name));
26252
26368
  const payable = matches.length === 1 ? undefined : new TradingError("TEE_WALLET_REQUIRED", `"${safeText(name)}" matches ${matches.length} TEE wallets. Bound to this key: ${listing.rows.map(describeWallet).join("; ") || "no TEE wallets bound to this key"}.`);
26253
- const { report, wallet: wallet3 } = await inspectReport(ctx, key, matches.length === 1 ? matches[0] : undefined, listing, records, payable, name);
26254
- if (sign4 && wallet3)
26369
+ const { report, wallet: wallet3 } = await inspectReport(ctx, key, matches.length === 1 ? matches[0] : undefined, listing, records, payable, name, chainFlag);
26370
+ if (sign4 && wallet3?.chain === "evm")
26371
+ report.checks.push({
26372
+ ...skipped("sign", "hood"),
26373
+ message: "Signing is checked on Solana wallets only; a Hood test build would hold the wallet."
26374
+ });
26375
+ else if (sign4 && wallet3)
26255
26376
  await buildCheck(ctx, key, wallet3, report, "SOL", "USDC", "0.000001", 50, parsed.values["--rpc-url"], true);
26256
26377
  else
26257
26378
  report.checks.push(skipped("sign", sign4 ? report.checks.find((check) => check.status === "fail")?.id ?? "signer" : "--sign not set"));
@@ -26288,7 +26409,7 @@ async function swapCheck(args, ctx) {
26288
26409
  if (from.chain !== "solana" || to.chain !== "solana")
26289
26410
  throw new TradingError("VALIDATION_FAILED", "Dry run checks Solana TEE pairs; Hood and bridge pairs are unsupported.");
26290
26411
  if (parsed.values["--to"] !== undefined || parsed.booleans.has("--wait")) {
26291
- writeUsageFailure(ctx.deps, "--to and --wait are for a bridge.", ctx.json);
26412
+ writeUsageFailure(ctx.deps, "--to selects a bridge destination; --wait follows a bridge or a Hood TEE swap without --dry-run. Follow a Hood swap with candle swap status --operation <id>, or a job with candle swap status <client-trade-id>.", ctx.json);
26292
26413
  return 2;
26293
26414
  }
26294
26415
  if (from.asset === to.asset || !from.base && !to.base)
@@ -26311,7 +26432,7 @@ async function swapCheck(args, ctx) {
26311
26432
  payable = error;
26312
26433
  }
26313
26434
  const records = await readWalletRecords(ctx, key);
26314
- const { report, wallet: wallet3 } = await inspectReport(ctx, key, row, listing, records, payable, parsed.values["--wallet"]);
26435
+ const { report, wallet: wallet3 } = await inspectReport(ctx, key, row, listing, records, payable, parsed.values["--wallet"], from.chain);
26315
26436
  if (wallet3)
26316
26437
  await buildCheck(ctx, key, wallet3, report, from.asset, to.asset, parsed.values["--amount"], slippage, parsed.values["--rpc-url"], false);
26317
26438
  else
@@ -26446,7 +26567,7 @@ function printTradingResult(ctx, result2) {
26446
26567
  }
26447
26568
  const printed = ctx.json && (result2.kind === "swap" || result2.kind === "trade") ? swapJsonResult(result2) : result2;
26448
26569
  ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(printed)}
26449
- ` : `${JSON.stringify(result2, null, 2)}
26570
+ ` : `${terminalLines(JSON.stringify(result2, null, 2))}
26450
26571
  `);
26451
26572
  const job = result2.job;
26452
26573
  const operation = job?.operation;
@@ -26495,11 +26616,11 @@ async function lookupOperation(ctx, key, id, kind) {
26495
26616
  async function swapStatus(args, ctx) {
26496
26617
  const parsed = parseArgs(args, { valueFlags: ["--kind", "--operation"], booleanFlags: ["--wait"] });
26497
26618
  if (!("error" in parsed) && parsed.values["--operation"] !== undefined) {
26498
- if (parsed.positionals.length || parsed.values["--kind"] || parsed.booleans.has("--wait")) {
26499
- writeUsageFailure(ctx.deps, "Usage: candle swap status --operation <operationId>", ctx.json);
26619
+ if (parsed.positionals.length || parsed.values["--kind"]) {
26620
+ writeUsageFailure(ctx.deps, "Usage: candle swap status --operation <operationId> [--wait]", ctx.json);
26500
26621
  return 2;
26501
26622
  }
26502
- return hoodOperationRead(ctx, parsed.values["--operation"]);
26623
+ return hoodOperationRead(ctx, parsed.values["--operation"], undefined, false, parsed.booleans.has("--wait"));
26503
26624
  }
26504
26625
  if ("error" in parsed || parsed.positionals.length !== 1 || !validClientId(parsed.positionals[0] ?? "") || parsed.values["--kind"] !== undefined && !["trade", "swap", "launch"].includes(parsed.values["--kind"])) {
26505
26626
  writeUsageFailure(ctx.deps, "Usage: candle swap status <id> [--kind trade|swap|launch] [--wait]", ctx.json);
@@ -26572,7 +26693,7 @@ async function swapReceipt(args, ctx) {
26572
26693
  }
26573
26694
  describeReceipt(ctx, body, answer.job);
26574
26695
  ctx.deps.stdout.write(ctx.json ? `${JSON.stringify(body)}
26575
- ` : `${JSON.stringify(body, null, 2)}
26696
+ ` : `${terminalLines(JSON.stringify(body, null, 2))}
26576
26697
  `);
26577
26698
  return 0;
26578
26699
  } catch (error) {
@@ -26583,7 +26704,7 @@ function describeReceipt(ctx, body, job) {
26583
26704
  const settlement = body.settlement;
26584
26705
  if (settlement === null || typeof settlement !== "object") {
26585
26706
  if (typeof body.next === "string")
26586
- ctx.deps.stderr.write(`${body.next}
26707
+ ctx.deps.stderr.write(`${terminalLines(body.next)}
26587
26708
  `);
26588
26709
  return;
26589
26710
  }
@@ -26706,8 +26827,9 @@ async function swap2(args, ctx) {
26706
26827
  slippage,
26707
26828
  ...bridge
26708
26829
  });
26709
- if (flags["--to"] !== undefined || parsed.booleans.has("--wait"))
26710
- throw new TradingUsage("--to and --wait are for a bridge: SOL or USDC to ETH or USDG, or back.");
26830
+ const unsupportedWait = "--to is for a bridge: SOL or USDC to ETH or USDG, or back. --wait follows a bridge or a Hood TEE swap. Follow a Hood swap with candle swap status --operation <id>; use candle swap status <client-trade-id> for a job.";
26831
+ if (flags["--to"] !== undefined)
26832
+ throw new TradingUsage(unsupportedWait);
26711
26833
  const chain2 = pairChain(fromAsset, toAsset);
26712
26834
  const walletFlag = flags["--wallet"];
26713
26835
  const named = walletFlag === undefined ? undefined : walletNameChain(walletFlag);
@@ -26715,6 +26837,7 @@ async function swap2(args, ctx) {
26715
26837
  throw chainMismatch(`--wallet ${safeText(walletFlag)}`, named, chain2);
26716
26838
  if (chain2 === "hood")
26717
26839
  return await hoodSwap(ctx, {
26840
+ wait: parsed.booleans.has("--wait"),
26718
26841
  flags,
26719
26842
  yes: parsed.booleans.has("--yes"),
26720
26843
  id,
@@ -26722,6 +26845,8 @@ async function swap2(args, ctx) {
26722
26845
  from: fromAsset,
26723
26846
  to: toAsset
26724
26847
  });
26848
+ if (parsed.booleans.has("--wait"))
26849
+ throw new TradingUsage(unsupportedWait);
26725
26850
  const from = fromAsset.asset;
26726
26851
  const to = toAsset.asset;
26727
26852
  if (from === to)
@@ -26947,9 +27072,19 @@ async function hoodSwap(ctx, args) {
26947
27072
  const kind = from.base && to.base ? "swap" : "trade";
26948
27073
  const key = await tradingKey(ctx);
26949
27074
  const prior = await lookupOperation(ctx, key, id, kind);
27075
+ const replay = async (result2) => {
27076
+ if (!args.wait)
27077
+ return printTradingResult(ctx, result2);
27078
+ const operation = result2.job?.operation;
27079
+ if (typeof operation?.operationId !== "string")
27080
+ throw new TradingUsage(`No Hood TEE operationId is available. Follow with candle swap status ${id}; a Hood TEE swap uses candle swap status --operation <id>.`);
27081
+ return waitForHoodOperation(ctx, key, operation.operationId);
27082
+ };
26950
27083
  if (prior)
26951
- return printTradingResult(ctx, prior);
27084
+ return replay(prior);
26952
27085
  const payer = await tradingPayer(ctx, key, flags["--wallet"], "swap:write", "hood");
27086
+ if (args.wait && payer.kind !== "tee")
27087
+ throw new TradingUsage(`--wait follows a bridge or a Hood TEE swap. Follow this swap with candle swap status ${id}; a Hood TEE swap uses candle swap status --operation <id>.`);
26953
27088
  const payerAddress = payer.kind === "tee" ? payer.wallet.address : payer.address;
26954
27089
  const rpc = lazyEvmRpc(ctx, flags["--rpc-url"]);
26955
27090
  const decimals = await hoodDecimals(from, rpc);
@@ -26989,7 +27124,7 @@ async function hoodSwap(ctx, args) {
26989
27124
  ...payer.kind === "embedded" ? { deferExecution: true } : {}
26990
27125
  });
26991
27126
  if (built.job || built.status === "executed")
26992
- return printTradingResult(ctx, { ...built, clientTradeId: id, kind });
27127
+ return replay({ ...built, clientTradeId: id, kind });
26993
27128
  const body = kind === "swap" ? built.payload : built;
26994
27129
  const data = swapBuildSchema.parse(body);
26995
27130
  const echoed = kind === "swap" ? body?.recipient : body?.walletAddress;
@@ -27073,7 +27208,7 @@ async function hoodSwap(ctx, args) {
27073
27208
  treasury: () => hoodFeeTreasury(rpc()),
27074
27209
  curve: () => registeredCurveFor(rpc(), token2)
27075
27210
  });
27076
- const run = await runSequencedLegs(ctx, key, {
27211
+ const execute = () => runSequencedLegs(ctx, key, {
27077
27212
  wallet: wallet3,
27078
27213
  first: sequenced,
27079
27214
  submitPath: kind === "swap" ? "/api/v1/agent/swap/submit" : "/api/v1/trade/agent/submit",
@@ -27089,6 +27224,16 @@ async function hoodSwap(ctx, args) {
27089
27224
  notices.push(notice2);
27090
27225
  }
27091
27226
  });
27227
+ let run;
27228
+ try {
27229
+ run = await execute();
27230
+ } catch (error) {
27231
+ if (args.wait && error instanceof TradingError && (error.exitCode === 3 || error.details?.stage === "reverted"))
27232
+ return waitForHoodOperation(ctx, key, sequenced.operationId, error.details);
27233
+ throw error;
27234
+ }
27235
+ if (args.wait)
27236
+ return waitForHoodOperation(ctx, key, sequenced.operationId);
27092
27237
  const settled = kind === "swap" ? reportSettlement(ctx, run.final.settlement, { asset: to.asset, decimals: outDecimals }, id) : {};
27093
27238
  return printTradingResult(ctx, {
27094
27239
  ...run.final,
@@ -27347,18 +27492,21 @@ async function bridgeSwap(ctx, args) {
27347
27492
  return printTradingResult(ctx, { ...receipt, waited: { final: false, error: reason } });
27348
27493
  }
27349
27494
  }
27350
- async function hoodOperationRead(ctx, id, candidateHash, recover = false) {
27495
+ async function hoodOperationRead(ctx, id, candidateHash, recover = false, wait = false) {
27351
27496
  if (!validClientId(id)) {
27352
27497
  writeUsageFailure(ctx.deps, "An operationId is required", ctx.json);
27353
27498
  return 2;
27354
27499
  }
27355
27500
  try {
27356
27501
  const key = await tradingKey(ctx);
27502
+ if (wait)
27503
+ return await waitForHoodOperation(ctx, key, id);
27357
27504
  const path = `/api/v1/agent/hood-tee/operations/${encodeURIComponent(id)}${recover ? "/recover" : ""}`;
27358
27505
  const result2 = await request(ctx, key, path, recover ? candidateHash ? { candidateHash } : {} : undefined);
27359
27506
  const operation = result2.operation;
27360
27507
  const guard = operation?.guard;
27361
- ctx.deps.stdout.write(`${JSON.stringify(result2, null, ctx.json ? undefined : 2)}
27508
+ const output = JSON.stringify(result2, null, ctx.json ? undefined : 2);
27509
+ ctx.deps.stdout.write(`${ctx.json ? output : terminalLines(output)}
27362
27510
  `);
27363
27511
  if (guard)
27364
27512
  ctx.deps.stderr.write(`${safeText(guard.reason)}: ${safeText(guard.clearingCondition)} Read: candle swap status --operation ${id}.
@@ -27390,6 +27538,7 @@ var init_swap = __esm(() => {
27390
27538
  init_client();
27391
27539
  init_evm_lite();
27392
27540
  init_hood_legs();
27541
+ init_hood_operation_wait();
27393
27542
  init_human_output();
27394
27543
  init_presentation();
27395
27544
  init_profiles();
@@ -47607,11 +47756,10 @@ function mintedByLabel(mintedBy, ownDeviceTokenPrefix) {
47607
47756
  function labelCell(label) {
47608
47757
  if (!label)
47609
47758
  return "";
47610
- const cleaned = Array.from(label).map((ch) => {
47759
+ const cleaned = Array.from(escapeTerminalText(label)).map((ch) => {
47611
47760
  const code = ch.codePointAt(0) ?? 0;
47612
- const isControl = code < 32 || code >= 127 && code <= 159;
47613
47761
  const isBidiOrInvisible = code >= 8203 && code <= 8207 || code >= 8234 && code <= 8238 || code >= 8294 && code <= 8297 || code === 65279;
47614
- return isControl || isBidiOrInvisible ? " " : ch;
47762
+ return isBidiOrInvisible ? " " : ch;
47615
47763
  }).join("");
47616
47764
  return cleaned.replace(/\s+/g, " ").trim();
47617
47765
  }
@@ -47895,17 +48043,27 @@ async function keysRevoke(args, ctx) {
47895
48043
  return 2;
47896
48044
  }
47897
48045
  if (parsed.positionals.length !== 1) {
47898
- deps.stderr.write(`Usage: candle keys revoke <prefix>
48046
+ deps.stderr.write(`Usage: candle keys revoke <prefix|label>
47899
48047
  `);
47900
48048
  return 2;
47901
48049
  }
47902
- const prefix = parsed.positionals[0];
48050
+ const target = parsed.positionals[0];
47903
48051
  await printIdentity(ctx);
47904
48052
  const deviceToken = await resolveDeviceToken(deps, ctx.profile);
47905
48053
  if (!deviceToken) {
47906
48054
  writeLocalFailure(deps, NO_DEVICE_TOKEN, json);
47907
48055
  return 1;
47908
48056
  }
48057
+ const resolved = await resolveTargetKey(ctx, deviceToken, target, {
48058
+ codePrefix: "KEY_REVOKE",
48059
+ writeListFailure: (failure3) => {
48060
+ writeFailure(deps, failure3, { apiUrl, authType: "device" }, json);
48061
+ return 1;
48062
+ }
48063
+ });
48064
+ if (!resolved.ok)
48065
+ return resolved.code;
48066
+ const prefix = resolved.keyPrefix;
47909
48067
  const result2 = await apiRequest(`${KEYS_PATH3}/${encodeURIComponent(prefix)}`, {
47910
48068
  method: "DELETE",
47911
48069
  auth: "device",
@@ -47955,6 +48113,7 @@ var init_keys = __esm(() => {
47955
48113
  init_secret_store();
47956
48114
  init_key_output();
47957
48115
  init_keys_embedded_wallet();
48116
+ init_tee_rebind();
47958
48117
  ACCESS_LEVELS = {
47959
48118
  read: "read",
47960
48119
  write: "readwrite",
@@ -77034,7 +77193,7 @@ var LAUNCH_DEFINITION2 = {
77034
77193
  {
77035
77194
  key: "curve.virtualQuote",
77036
77195
  type: "decimal",
77037
- help: "exp-custom's virtual quote, in whole quote units: 1 to 100 SOL or 66000 to 6600000 CNDL; requires --curve-g and EXP_CUSTOM_CURVES.",
77196
+ help: "exp-custom's virtual quote, in whole quote units: 1 to 100 SOL or 66000 to 6600000 CNDL; requires --curve-g.",
77038
77197
  modes: [
77039
77198
  "exp-custom"
77040
77199
  ],
@@ -77073,7 +77232,7 @@ var LAUNCH_DEFINITION2 = {
77073
77232
  {
77074
77233
  key: "curve.threshold",
77075
77234
  type: "decimal",
77076
- help: "exp-custom's graduation threshold, in whole quote units: 1 to 250 SOL or 66000 to 16500000 CNDL; requires --curve-v and EXP_CUSTOM_CURVES.",
77235
+ help: "exp-custom's graduation threshold, in whole quote units: 1 to 250 SOL or 66000 to 16500000 CNDL; requires --curve-v.",
77077
77236
  modes: [
77078
77237
  "exp-custom"
77079
77238
  ],
@@ -78065,7 +78224,7 @@ var HELP = {
78065
78224
  group: "Trade",
78066
78225
  intro: "Quote, swap and bridge on Solana or Hood; read an operation",
78067
78226
  summary: "Quote, confirm and swap on Solana or Hood, bridge between them; read an operation by id",
78068
- description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent. The first buy after a launch is this command, unless the launch carried it with candle launch --buy. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana) and a named wallet must be on it. A Hood TEE wallet signs one leg at a time: approve, Permit2, trade, then the fee, each only after the one before it landed. SOL or USDC to ETH or USDG, or back, is a bridge through Relay from a TEE wallet into this key's own TEE wallet on the other chain (--to names it when there are several); Candle charges no fee, only configured raw caps and USD limits apply (unset means unlimited, zero still binds), and this machine checks Relay's deposit before it is signed. The bridge prints its deposit and returns while Relay fills; --wait follows it for up to ten minutes. While it is open, a sweep of either wallet refuses BRIDGE_IN_FLIGHT for two hours. Vault to vault: fund the Solana TEE wallet from the vault, bridge, then disable and sweep the Hood TEE wallet into its vault. Every plan can trade Candle-launched tokens and base assets, and buy or sell a token that was not launched on Candle. Each trade pays the plan's agent fee on top; a base-pair swap or a bridge between this key's own wallets pays none. candle plans shows the fees in force. With --dry-run, stdout reports per-check results and the quote; nothing is signed or sent. Otherwise with --json, stdout is one JSON value for every swap, trade, replay and status read: kind, status, clientTradeId, signature (the primary transaction, or null), signatures (a list) and raw (the original response); every human line is on stderr. The keys older versions printed stay beside them.",
78227
+ description: "Swaps run through a TEE wallet's bound key: the quote is shown and confirmed before anything is sent. The first buy after a launch is this command, unless the launch carried it with candle launch --buy. The assets decide the chain (ETH, USDG or a 0x token is Hood; SOL, USDC, CNDL or a mint is Solana) and a named wallet must be on it. A Hood TEE wallet signs one leg at a time: approve, Permit2, trade, then the fee, each only after the one before it landed. SOL or USDC to ETH or USDG, or back, is a bridge through Relay from a TEE wallet into this key's own TEE wallet on the other chain (--to names it when there are several); Candle charges no fee, only configured raw caps and USD limits apply (unset means unlimited, zero still binds), and this machine checks Relay's deposit before it is signed. The bridge prints its deposit and returns while Relay fills; --wait follows it for up to ten minutes. While it is open, a sweep of either wallet refuses BRIDGE_IN_FLIGHT for two hours. On a Hood TEE swap, --wait follows the trade and fee legs for up to ten minutes and prints the same operation response as candle swap status --operation <id>; exit 0 means completed, 1 failed, and 3 partial execution, unresolved settlement or timeout. Embedded and Solana same-chain swaps use candle swap status <client-trade-id>. Vault to vault: fund the Solana TEE wallet from the vault, bridge, then disable and sweep the Hood TEE wallet into its vault. Every plan can trade Candle-launched tokens and base assets, and buy or sell a token that was not launched on Candle. Each trade pays the plan's agent fee on top; a base-pair swap or a bridge between this key's own wallets pays none. candle plans shows the fees in force. With --dry-run, stdout reports per-check results and the quote; nothing is signed or sent. Otherwise with --json, stdout is one JSON value: swaps, trades, replays and job status reads carry kind, status, clientTradeId, signature (the primary transaction, or null), signatures (a list) and raw (the original response); Hood --wait and explicit --operation reads return the operation response. Every human line is on stderr. The keys older versions printed stay beside them.",
78069
78228
  usage: [
78070
78229
  "candle swap <from> <to> [flags]",
78071
78230
  "candle swap status <id>",
@@ -78080,8 +78239,8 @@ var HELP = {
78080
78239
  description: "Quote, confirm and swap on Solana or Hood, or bridge between them"
78081
78240
  },
78082
78241
  {
78083
- invocation: "status <id> [--kind trade|swap|launch] [--wait] | status --operation <operationId>",
78084
- description: "Read without resending; --wait follows a bridge. --operation reads a Hood TEE guard with no signing or sending, and exits 3 while held"
78242
+ invocation: "status <id> [--kind trade|swap|launch] [--wait] | status --operation <operationId> [--wait]",
78243
+ description: "Read without resending; --wait follows a bridge. --operation reads a Hood TEE guard; add --wait to follow all legs for up to ten minutes (0 completed, 1 failed, 3 partial or unresolved)"
78085
78244
  },
78086
78245
  {
78087
78246
  invocation: "recover --operation <operationId> [--hash <0x…>]",
@@ -78928,13 +79087,19 @@ var HELP = {
78928
79087
  invocation: "self embedded-wallet deny",
78929
79088
  description: "The profile's own key gives up the embedded wallet, with no prompt. A key can never allow itself: the owner does that with keys update"
78930
79089
  },
78931
- { invocation: "stop <prefix>", description: "Pause one key with the device token; reversible with keys resume" },
78932
- { invocation: "resume <prefix>", description: "Resume a stopped key with the device token" },
79090
+ {
79091
+ invocation: "stop <prefix|label>",
79092
+ description: "Pause one key with the device token (starting it again is done in the signed-in console)"
79093
+ },
79094
+ {
79095
+ invocation: "resume <prefix|label>",
79096
+ description: "Resume a stopped key in the signed-in console, because the server refuses a device token here (LOOSEN_REQUIRES_SESSION)"
79097
+ },
78933
79098
  {
78934
79099
  invocation: "rotate <prefix|label> ([--grace <duration> | --now] [--force] [--yes] | --finish)",
78935
79100
  description: "Give a key a new secret, keeping its prefix, label, access, limits, wallets and history. The old secret works through --grace (default 1h, at most 24h) or stops at once with --now; --finish ends an open window early. The profile's own key is replaced in the keychain; update CANDLE_API_KEY or CANDLE_API_KEY_FILE yourself and restart MCP. --force rotates again while a window is open"
78936
79101
  },
78937
- { invocation: "revoke <prefix>", description: "Revoke an API key permanently" },
79102
+ { invocation: "revoke <prefix|label>", description: "Revoke an API key permanently" },
78938
79103
  {
78939
79104
  invocation: "signer approve <code> --key <prefix|label> [--reject]",
78940
79105
  description: "Approve (or reject) a key's signer request with the device token; type the full fingerprint the trading machine printed"
@@ -79018,7 +79183,7 @@ var HELP = {
79018
79183
  },
79019
79184
  {
79020
79185
  invocation: "close-empty [--wallet embedded|<tee address|label>] [--keep <mint>]... [--client-trade-id <id>] [--preview] [--yes]",
79021
- description: "Close a wallet's empty token accounts and return their rent to it, after a preview: the embedded wallet (transfer:write), or a TEE wallet bound to this key, signed by its key signer (swap:write). --preview prints what would close (one JSON document on stdout under --json) and closes nothing"
79186
+ description: "Close a wallet's empty token accounts and return their rent to it, after a preview: the embedded wallet (swap:write or transfer:write), or a TEE wallet bound to this key, signed by its key signer (swap:write). --preview prints what would close (one JSON document on stdout under --json) and closes nothing"
79022
79187
  }
79023
79188
  ],
79024
79189
  flags: [
@@ -79232,8 +79397,8 @@ var HELP = {
79232
79397
  usage: ["candle tee <subcommand> [flags]"],
79233
79398
  rows: [
79234
79399
  {
79235
- invocation: "check <address|id|label>... [--label-prefix <p>] [--sign] [--max <n>] [--rpc-url <url>]",
79236
- description: "Check payable membership, record parity and this machine's signer without spending. --sign builds a tiny SOL/USDC quote, checks spend, signs and simulates; never broadcasts (default cap 10). Needs swap:write"
79400
+ invocation: "check <address|id|label>... [--label-prefix <p>] [--chain solana|hood] [--sign] [--max <n>] [--rpc-url <url>]",
79401
+ description: "Check payable membership, record parity and this machine's signer without spending. Each wallet is checked on its own chain; --chain checks every wallet against one chain (CHAIN_MISMATCH otherwise). --sign builds a tiny SOL/USDC quote on a Solana wallet, checks spend, signs and simulates; never broadcasts (default cap 10). A Hood wallet skips sign: a Hood build holds the wallet. Needs swap:write"
79237
79402
  },
79238
79403
  {
79239
79404
  invocation: "new [--label <name>]",
@@ -79299,6 +79464,7 @@ var HELP = {
79299
79464
  examples: [
79300
79465
  "candle tee check --label-prefix pool- --json",
79301
79466
  "candle tee check AgentOne --sign --rpc-url <url>",
79467
+ "candle tee check hood-1 --chain hood --json",
79302
79468
  "candle tee new --label AgentOne",
79303
79469
  "candle tee status AgentOneAddress",
79304
79470
  "candle tee sweep AgentOneAddress",
@@ -85264,6 +85430,13 @@ init_evm_lite();
85264
85430
  init_client();
85265
85431
  var EXIT_OWNER_CONFIRM_PENDING = 7;
85266
85432
  var OWN_NO_WAIT_COMMANDS = [["tee"], ["lab"], ["wallets", "authorize"]];
85433
+ function shortWallet(wallet3) {
85434
+ return wallet3.length >= 12 ? `${wallet3.slice(0, 4)}…${wallet3.slice(-4)}` : wallet3;
85435
+ }
85436
+ function accountLabel(account) {
85437
+ const short = shortWallet(account.wallet);
85438
+ return account.displayName ? `${account.displayName} (${short})` : short;
85439
+ }
85267
85440
  function ownerConfirmPendingLine(challenge) {
85268
85441
  return JSON.stringify({
85269
85442
  ok: false,
@@ -85274,7 +85447,8 @@ function ownerConfirmPendingLine(challenge) {
85274
85447
  verificationUriComplete: challenge.verificationUriComplete,
85275
85448
  expiresIn: challenge.expiresIn,
85276
85449
  step: challenge.step,
85277
- summary: challenge.summary
85450
+ summary: challenge.summary,
85451
+ ...challenge.account ? { account: challenge.account } : {}
85278
85452
  }
85279
85453
  });
85280
85454
  }
@@ -85291,10 +85465,12 @@ function cliOwnerConfirmHooks(deps, opts, state) {
85291
85465
  `);
85292
85466
  } else {
85293
85467
  const minutes = Math.max(1, Math.round(challenge.expiresIn / 60));
85468
+ const as = challenge.account ? ` as ${accountLabel(challenge.account)}` : "";
85294
85469
  deps.stderr.write([
85295
- `This needs your approval in the browser: ${challenge.summary}`,
85470
+ `This needs your approval in the browser${as}: ${challenge.summary}`,
85296
85471
  `Open ${challenge.verificationUriComplete}`,
85297
85472
  `Code: ${challenge.userCode}`,
85473
+ ...challenge.account ? [`The browser must be signed in as ${accountLabel(challenge.account)}.`] : [],
85298
85474
  "Only approve if you just ran this command.",
85299
85475
  opts.wait ? `Waiting for approval (expires in ${minutes} minute${minutes === 1 ? "" : "s"})...` : ""
85300
85476
  ].filter(Boolean).join(`
@@ -86262,7 +86438,7 @@ ${USAGE6}`, json);
86262
86438
  } catch (error) {
86263
86439
  if (!(error instanceof CoreClientError))
86264
86440
  throw error;
86265
- deps.stderr.write(`${error.code}: ${error.message}
86441
+ deps.stderr.write(`${terminalLines(`${error.code}: ${error.message}`)}
86266
86442
  `);
86267
86443
  return 1;
86268
86444
  }
@@ -91975,7 +92151,7 @@ async function help(args, ctx) {
91975
92151
  }
91976
92152
  const topic = renderTopic(word3, terminalStyle(ctx.deps, "stdout", ctx.json), args.includes("--more"));
91977
92153
  if (topic === undefined) {
91978
- ctx.deps.stderr.write(`Unknown command: ${word3}
92154
+ ctx.deps.stderr.write(`Unknown command: ${escapeTerminalText(word3)}
91979
92155
  `);
91980
92156
  ctx.deps.stderr.write(renderTopLevel());
91981
92157
  return 1;
@@ -92305,7 +92481,7 @@ async function keysAccessHistory(target, ctx) {
92305
92481
  `);
92306
92482
  return 0;
92307
92483
  }
92308
- const rows = result2.body.changes ?? [];
92484
+ const rows = (result2.body.changes ?? []).filter((row2) => row2.eventKind !== "profile_edit" && row2.change !== "profile_edit");
92309
92485
  if (rows.length === 0) {
92310
92486
  notice(deps, "stdout", "note", `No access changes on key ${resolved.keyPrefix}.`);
92311
92487
  return 0;
@@ -92615,7 +92791,7 @@ function writeRefusal(ctx, refusal3, suggestion) {
92615
92791
  `);
92616
92792
  return;
92617
92793
  }
92618
- ctx.deps.stderr.write(`${suggestion ? `${message} ${suggestion}` : message}
92794
+ ctx.deps.stderr.write(`${escapeTerminalText(suggestion ? `${message} ${suggestion}` : message)}
92619
92795
  `);
92620
92796
  }
92621
92797
  function writeApiFailure(ctx, result2, authType) {
@@ -92710,8 +92886,8 @@ function identity2(who) {
92710
92886
  if (who.kind === "candle")
92711
92887
  return "Candle";
92712
92888
  if (who.kind === "agent")
92713
- return terminalText(who.name ?? "Agent");
92714
- return terminalText(who.name ?? who.account);
92889
+ return escapeTerminalText(who.name ?? "Agent");
92890
+ return escapeTerminalText(who.name ?? who.account);
92715
92891
  }
92716
92892
  function sectionText(bytes, section2) {
92717
92893
  if (bytes.length > TOKEN_PAGE_BODY_CAP_BYTES)
@@ -92750,7 +92926,7 @@ function factValue(fact) {
92750
92926
  if (fact.raw === undefined)
92751
92927
  return fact.status;
92752
92928
  const value = fact.decimals ? rawUnits2(fact.raw, fact.decimals) : fact.raw;
92753
- return `${value}${fact.unit === "token" ? "" : ` ${fact.unit}`} (${fact.status}, ${fact.coverage}, ${terminalText(fact.source)}${fact.measuredAt ? `, ${fact.measuredAt}` : ""})`;
92929
+ return `${value}${fact.unit === "token" ? "" : ` ${fact.unit}`} (${fact.status}, ${fact.coverage}, ${escapeTerminalText(fact.source)}${fact.measuredAt ? `, ${fact.measuredAt}` : ""})`;
92754
92930
  }
92755
92931
  function rawUnits2(raw, decimals) {
92756
92932
  if (!/^\d+$/.test(raw))
@@ -92792,7 +92968,7 @@ function writeAbout(ctx, about) {
92792
92968
  if (v.linksWithheld)
92793
92969
  notice(ctx.deps, "stdout", "note", "Links are withheld pending review.");
92794
92970
  ctx.deps.stdout.write(`
92795
- ${terminalTextKeepingLines(v.text)}
92971
+ ${escapeTerminalText(v.text)}
92796
92972
  `);
92797
92973
  }
92798
92974
  writeHeading(ctx.deps, "stdout", "Supply, measured by Candle");
@@ -92801,12 +92977,7 @@ ${terminalTextKeepingLines(v.text)}
92801
92977
  ctx.deps.stdout.write(` Measurements unavailable
92802
92978
  `);
92803
92979
  else
92804
- writeFields(ctx.deps, "stdout", facts.map((f) => [terminalText(f.metric), factValue(f)]));
92805
- }
92806
- function terminalTextKeepingLines(text3) {
92807
- return text3.split(`
92808
- `).map((line) => terminalText(line.replace(/\t/g, " "))).join(`
92809
- `);
92980
+ writeFields(ctx.deps, "stdout", facts.map((f) => [escapeTerminalText(f.metric), factValue(f)]));
92810
92981
  }
92811
92982
  async function tokenAbout(args, ctx) {
92812
92983
  const usage4 = usageFor(ctx, USAGE9.about);
@@ -93120,12 +93291,12 @@ async function tokenHistory(args, ctx) {
93120
93291
  ]);
93121
93292
  if (item.text !== null)
93122
93293
  ctx.deps.stdout.write(`
93123
- ${terminalTextKeepingLines(item.text)}
93294
+ ${escapeTerminalText(item.text)}
93124
93295
  `);
93125
93296
  }
93126
93297
  if (data.nextCursor)
93127
93298
  ctx.deps.stdout.write(`
93128
- More: --cursor ${data.nextCursor}
93299
+ More: --cursor ${escapeTerminalText(data.nextCursor)}
93129
93300
  `);
93130
93301
  return 0;
93131
93302
  }
@@ -93311,12 +93482,12 @@ async function tokenComments(args, ctx) {
93311
93482
  ]);
93312
93483
  if (item.text !== null)
93313
93484
  ctx.deps.stdout.write(`
93314
- ${terminalTextKeepingLines(item.text)}
93485
+ ${escapeTerminalText(item.text)}
93315
93486
  `);
93316
93487
  }
93317
93488
  if (data.nextCursor)
93318
93489
  ctx.deps.stdout.write(`
93319
- More: --cursor ${data.nextCursor}
93490
+ More: --cursor ${escapeTerminalText(data.nextCursor)}
93320
93491
  `);
93321
93492
  return 0;
93322
93493
  }
@@ -93591,6 +93762,7 @@ init_presentation();
93591
93762
  init_profiles();
93592
93763
  init_key_output();
93593
93764
  init_keys();
93765
+ init_tee_rebind();
93594
93766
  function keysStop(args, ctx) {
93595
93767
  return setKeyPaused(args, ctx, true);
93596
93768
  }
@@ -93599,14 +93771,14 @@ function keysResume(args, ctx) {
93599
93771
  }
93600
93772
  async function setKeyPaused(args, ctx, paused) {
93601
93773
  const { deps, apiUrl, json } = ctx;
93602
- const parsed = parseArgs(args, {});
93774
+ const parsed = parseArgs(args, { keyPrefixPositional: true });
93603
93775
  if ("error" in parsed) {
93604
93776
  writeScreenUsageFailure(deps, parsed.error, json);
93605
93777
  return 2;
93606
93778
  }
93607
- const prefix = parsed.positionals[0];
93608
- if (!prefix?.trim() || parsed.positionals.length !== 1) {
93609
- writeScreenUsageFailure(deps, `Usage: candle keys ${paused ? "stop" : "resume"} <prefix>`, json);
93779
+ const target = parsed.positionals[0];
93780
+ if (!target?.trim() || parsed.positionals.length !== 1) {
93781
+ writeScreenUsageFailure(deps, `Usage: candle keys ${paused ? "stop" : "resume"} <prefix|label>`, json);
93610
93782
  return 2;
93611
93783
  }
93612
93784
  await printIdentity(ctx);
@@ -93615,6 +93787,16 @@ async function setKeyPaused(args, ctx, paused) {
93615
93787
  writeScreenLocalFailure(deps, NO_DEVICE_TOKEN, json);
93616
93788
  return 1;
93617
93789
  }
93790
+ const resolved = await resolveTargetKey(ctx, deviceToken, target, {
93791
+ codePrefix: paused ? "KEY_STOP" : "KEY_RESUME",
93792
+ writeListFailure: (failure3) => {
93793
+ writeScreenFailure(deps, failure3, { apiUrl, authType: "device" }, json);
93794
+ return 1;
93795
+ }
93796
+ });
93797
+ if (!resolved.ok)
93798
+ return resolved.code;
93799
+ const prefix = resolved.keyPrefix;
93618
93800
  const result2 = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/stop`, {
93619
93801
  method: "POST",
93620
93802
  auth: "device",
@@ -101426,7 +101608,7 @@ async function read2(ctx, argv, command) {
101426
101608
  for (const warning of result2.warnings)
101427
101609
  notice(ctx.deps, "stderr", "note", `${safeText(warning)}
101428
101610
  `.trim());
101429
- const out = (line) => ctx.deps.stdout.write(`${line}
101611
+ const out = (line) => ctx.deps.stdout.write(`${terminalLines(line)}
101430
101612
  `);
101431
101613
  out(`Hyperliquid ${result2.network}; OI floor $${result2.gate.minOpenInterestUsd} (${result2.gate.source}). Tradeability is Candle's build gate at read time, not a promise. OI caps may still refuse an order.`);
101432
101614
  if ("market" in result2) {
@@ -102844,7 +103026,7 @@ async function runPaidRead(ctx, input) {
102844
103026
  ctx.deps.stdout.write(`${JSON.stringify({ ok: true, data, ...paid.payment ? { payment: paid.payment } : {}, ...paid.servedFromCredit ? { servedFromCredit: true } : {} })}
102845
103027
  `);
102846
103028
  } else {
102847
- ctx.deps.stdout.write(`${JSON.stringify(data, null, 2)}
103029
+ ctx.deps.stdout.write(`${terminalLines(JSON.stringify(data, null, 2))}
102848
103030
  `);
102849
103031
  }
102850
103032
  return 0;
@@ -104258,6 +104440,9 @@ function financialHeading(text4, style) {
104258
104440
  function signed(text4, style) {
104259
104441
  return /^\+/.test(text4) ? accent(text4, style, "positive") : /^[-−]/.test(text4) ? accent(text4, style, "negative") : text4;
104260
104442
  }
104443
+ function signedField(text4) {
104444
+ return /^\+/.test(text4) ? { text: text4, role: "positive" } : /^[-−]/.test(text4) ? { text: text4, role: "negative" } : text4;
104445
+ }
104261
104446
  function financialTable(ctx, headers2, rows, columns) {
104262
104447
  const style = terminalStyle(ctx.deps, "stdout");
104263
104448
  const clean3 = rows.map((row2) => row2.map(terminalText));
@@ -104274,7 +104459,7 @@ function financialTable(ctx, headers2, rows, columns) {
104274
104459
  if (style.width >= 60 && table.split(`
104275
104460
  `).every((line) => displayWidth(line) <= style.width))
104276
104461
  return table;
104277
- return clean3.map((row2) => fields(headers2.map((label, col) => [label, specs[col]?.signed ? signed(row2[col] ?? "", style) : row2[col]]), style).join(`
104462
+ return clean3.map((row2) => fields(headers2.map((label, col) => [label, specs[col]?.signed ? signedField(row2[col] ?? "") : row2[col]]), style).join(`
104278
104463
  `)).join(`
104279
104464
 
104280
104465
  `);
@@ -104288,7 +104473,7 @@ function writeFinancialLines(ctx, rows) {
104288
104473
  for (const [label = "", value = "", note = ""] of rows) {
104289
104474
  const head = ` ${dim(padDisplay(label, labelWidth), style)} ${" ".repeat(valueWidth - displayWidth(value))}${signed(value, style)}`;
104290
104475
  if (displayWidth(head) > style.width) {
104291
- ctx.deps.stdout.write(`${fields([[label, signed(value, style)]], style).join(`
104476
+ ctx.deps.stdout.write(`${fields([[label, signedField(value)]], style).join(`
104292
104477
  `)}
104293
104478
  `);
104294
104479
  if (note)
@@ -106278,10 +106463,12 @@ async function quantAsk(args, ctx) {
106278
106463
  })}
106279
106464
  `);
106280
106465
  } else {
106281
- ctx.deps.stdout.write(`${answer2}
106466
+ ctx.deps.stdout.write(`${result2.paragraphs.map(escapeTerminalText).join(`
106467
+
106468
+ `)}
106282
106469
  `);
106283
106470
  if (remaining !== undefined) {
106284
- ctx.deps.stderr.write(`${remaining} Quant answer${remaining === 1 ? "" : "s"} left today${resetsAt !== undefined ? `, resets at ${resetsAt}` : ""}.
106471
+ ctx.deps.stderr.write(`${remaining} Quant answer${remaining === 1 ? "" : "s"} left today${resetsAt !== undefined ? `, resets at ${escapeTerminalText(String(resetsAt))}` : ""}.
106285
106472
  `);
106286
106473
  }
106287
106474
  }
@@ -108435,10 +108622,10 @@ function eventLine(event, widths) {
108435
108622
  const { doing, detail } = action(event);
108436
108623
  return [
108437
108624
  formatTime(event.at).padEnd(widths.time),
108438
- event.kind.padEnd(widths.kind),
108439
- who(event).padEnd(widths.who),
108440
- doing,
108441
- detail
108625
+ escapeTerminalText(event.kind).padEnd(widths.kind),
108626
+ escapeTerminalText(who(event)).padEnd(widths.who),
108627
+ escapeTerminalText(doing),
108628
+ escapeTerminalText(detail)
108442
108629
  ].filter((cell) => cell.trim().length > 0).join(" ").trimEnd();
108443
108630
  }
108444
108631
  async function tokenEvents(args, ctx) {
@@ -108519,7 +108706,7 @@ async function tokenEvents(args, ctx) {
108519
108706
  const widths = {
108520
108707
  time: Math.max(...events.map((event) => formatTime(event.at).length)),
108521
108708
  kind: Math.max(...events.map((event) => event.kind.length)),
108522
- who: Math.max(...events.map((event) => who(event).length))
108709
+ who: Math.max(...events.map((event) => escapeTerminalText(who(event)).length))
108523
108710
  };
108524
108711
  for (const event of events)
108525
108712
  out.write(`${eventLine(event, widths)}
@@ -108717,7 +108904,7 @@ ${retry}
108717
108904
  ["Result", payload.changed ? "edited" : "nothing changed"],
108718
108905
  ["Version", payload.version === null ? "-" : String(payload.version)],
108719
108906
  ...payload.changed ? [["Changed", changedOf(body)]] : [],
108720
- ["Description", payload.description ? terminalText(payload.description) : "-"],
108907
+ ["Description", payload.description ? escapeTerminalText(payload.description) : "-"],
108721
108908
  ...linkRows(payload.socials)
108722
108909
  ]);
108723
108910
  return 0;
@@ -108771,18 +108958,18 @@ async function tokenHistory2(args, ctx) {
108771
108958
  writeHeading(ctx.deps, "stdout", `version ${item.version}`);
108772
108959
  writeFields(ctx.deps, "stdout", [
108773
108960
  ["At", item.createdAt],
108774
- ["By", item.author?.name ? terminalText(item.author.name) : "Agent"],
108961
+ ["By", item.author?.name ? escapeTerminalText(item.author.name) : "Agent"],
108775
108962
  ["Changed", item.changedFields.join(", ")],
108776
108963
  ...item.changedFields.map((field) => {
108777
108964
  const now = field === "description" ? item.description : item.socials[field.slice("socials.".length)];
108778
108965
  const was = field === "description" ? item.prevDescription : item.prevSocials[field.slice("socials.".length)];
108779
- return [field, `${was ? terminalText(was) : "-"} -> ${now ? terminalText(now) : "-"}`];
108966
+ return [field, `${was ? escapeTerminalText(was) : "-"} -> ${now ? escapeTerminalText(now) : "-"}`];
108780
108967
  })
108781
108968
  ]);
108782
108969
  }
108783
108970
  if (data.nextCursor)
108784
108971
  ctx.deps.stdout.write(`
108785
- More: --cursor ${data.nextCursor}
108972
+ More: --cursor ${escapeTerminalText(data.nextCursor)}
108786
108973
  `);
108787
108974
  return 0;
108788
108975
  }
@@ -118681,7 +118868,7 @@ async function runWithConfirm(argv, deps) {
118681
118868
  async function runCommand(argv, deps) {
118682
118869
  const extracted = extractGlobalFlags(argv);
118683
118870
  if ("error" in extracted) {
118684
- deps.stderr.write(`${extracted.error}
118871
+ deps.stderr.write(`${escapeTerminalText(extracted.error)}
118685
118872
  `);
118686
118873
  return 2;
118687
118874
  }
@@ -118802,7 +118989,7 @@ async function runCommand(argv, deps) {
118802
118989
  return 1;
118803
118990
  }
118804
118991
  if (verdict.warning)
118805
- deps.stderr.write(`${verdict.warning}
118992
+ deps.stderr.write(`${terminalLines(verdict.warning)}
118806
118993
  `);
118807
118994
  }
118808
118995
  if (ROUTED_COMMANDS.has(word3) && routesToCommand(cmd, sub))
@@ -118833,7 +119020,7 @@ function splitFix(message) {
118833
119020
  }
118834
119021
  function unknownCommand(deps, token2, word3) {
118835
119022
  if (token2 !== undefined)
118836
- deps.stderr.write(`Unknown command: ${token2}
119023
+ deps.stderr.write(`Unknown command: ${escapeTerminalText(token2)}
118837
119024
  `);
118838
119025
  deps.stderr.write((word3 === undefined ? undefined : renderTopic(word3)) ?? renderTopLevel());
118839
119026
  return 1;
@@ -119049,12 +119236,12 @@ if (isMainModule) {
119049
119236
  process.exitCode = 1;
119050
119237
  if (stream === process.stderr)
119051
119238
  return;
119052
- process.stderr.write(`Unexpected error: ${err?.message ?? String(err)}
119239
+ process.stderr.write(`Unexpected error: ${escapeTerminalText(err?.message ?? String(err))}
119053
119240
  `);
119054
119241
  });
119055
119242
  }
119056
119243
  await main().catch((err) => {
119057
- process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
119244
+ process.stderr.write(`Unexpected error: ${escapeTerminalText(err instanceof Error ? err.message : String(err))}
119058
119245
  `);
119059
119246
  process.exitCode = 1;
119060
119247
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.13.23",
3
+ "version": "0.13.25",
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": {