@candledottv/cli 0.13.16 → 0.13.17

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 +21 -9
  2. package/dist/index.js +266 -96
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -53,6 +53,12 @@ or with Homebrew:
53
53
  Then: candle setup
54
54
  ```
55
55
 
56
+ The installer checks the download's SHA-256 and its Sigstore signature before it writes anything,
57
+ and needs nothing beyond `curl` and `sha256sum` or `shasum`. It checks the signature with `cosign` or
58
+ a signed-in `gh` when one is installed, and otherwise with Candle's own `candle verify`, offline, from
59
+ a CLI release whose SHA-256 the script carries (it downloads that `candle` and checks it against the
60
+ SHA-256 before running it). If no check can run, it installs nothing.
61
+
56
62
  `candle setup` authorizes this device from your browser, shows the agent wallets as funding
57
63
  destinations, prints the skill and MCP install lines, and runs a full health check. `candle auth
58
64
  login` on its own does just the authorization step.
@@ -85,6 +91,10 @@ vault key with `candle tee sweep` or `candle vault demote`. Promoting, moving fu
85
91
  emergency procedure, rebinding, trusted wallets and API key access levels are all in
86
92
  [Custody tiers: the vault and TEE wallets](https://docs.candle.tv/developers/cli-custody).
87
93
 
94
+ The recovery phrase ceremony (`vault init`'s offer or `vault phrase show`) waits for you to save
95
+ all 24 words offline and press Enter, then clears the screen before asking for three randomly
96
+ chosen words from your saved copy. Clear your terminal's scrollback too: the CLI cannot erase it.
97
+
88
98
  ## Commands
89
99
 
90
100
  | Command | What it does |
@@ -94,8 +104,9 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
94
104
  | `candle auth status` | Shows which storage backend is in use, both credential prefixes, the config file path, and a live validity check for each credential. |
95
105
  | `candle auth logout [--keep-key]` | Revokes the stored API key (skipped with `--keep-key`), clears local credentials and config, and prints the portal URL for revoking the device itself. |
96
106
  | `candle keys list [--scopes]` | Lists this account's API keys: prefix, name, Read, Write or Withdraw access, environment, timestamps, and which device minted each one. `--scopes` adds the raw scopes, sorted. |
97
- | `candle keys create [--access read\|write\|withdraw \| --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily\|weekly\|monthly\|never]] [--embedded-wallet deny\|allow]` | Creates a new API key and prints the plaintext exactly once, with the same optional name, expiration, and USD transaction limit the portal's create form takes. `--access` mints one of the portal's three levels (`write` trades and launches and cannot move funds; `withdraw` is Write plus `transfer:write` and `transfer:bound`: the key can move funds between your own wallets and to addresses you approved, and out of the wallet it runs to wallets you linked while signed in or marked trusted and to that wallet's vault); `--scopes` names raw scopes instead, never both. `--embedded-wallet` defaults to `deny`; `allow` lets the key spend the account's embedded wallet and is confirmed at a terminal. Stored locally only if the CLI does not already hold a working key. An account holds at most 25 active keys. |
107
+ | `candle keys create [--access read\|write\|withdraw \| --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily\|weekly\|monthly\|never]] [--embedded-wallet deny\|allow] [--no-store]` | Creates a new API key and prints the plaintext exactly once, with the same optional name, expiration, and USD transaction limit the portal's create form takes. `--access` mints one of the portal's three levels (`write` trades and launches and cannot move funds; `withdraw` is Write plus `transfer:write` and `transfer:bound`: the key can move funds between your own wallets and to addresses you approved, and out of the wallet it runs to wallets you linked while signed in or marked trusted and to that wallet's vault); `--scopes` names raw scopes instead, never both. `--embedded-wallet` defaults to `deny`; `allow` lets the key spend the account's embedded wallet and is confirmed at a terminal. Stored locally only if the CLI does not already hold a working key. `--no-store` prints the new key without saving it or changing the profile's working key or metadata; `--json` reports `stored: false`. An account holds at most 25 active keys. |
98
108
  | `candle keys access <prefix\|label\|self> (--access read\|write\|withdraw [--yes] \| --history)` | Changes an existing key's level in place (same key, wallets and caps). Widening needs the device token and the prefix typed back at a terminal; `--yes` skips the prompt when narrowing; `self` narrows the profile's own key and can never widen it. `--history` lists the key's changes and who made them. |
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. |
99
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. |
100
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. |
101
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. |
@@ -109,9 +120,9 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
109
120
  | `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. |
110
121
  | `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. |
111
122
  | `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`. |
112
- | `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), `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). |
123
+ | `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). |
113
124
  | `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. |
114
- | `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), `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. |
125
+ | `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. |
115
126
  | `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). |
116
127
  | `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. |
117
128
  | `candle secrets set\|list\|remove <name>` | Your own third-party API keys, in a keychain namespace separate from Candle's credentials (`tv.candle.cli.secrets`, or `secrets.enc` on the encrypted-file fallback). Typed on a hidden prompt, never sent to Candle, never printed after `set`; `list` shows names only. A plug-in receives one as `CANDLE_SECRET_<NAME>` only when named with `--secret`. |
@@ -121,7 +132,7 @@ emergency procedure, rebinding, trusted wallets and API key access levels are al
121
132
  | `candle tee signer new --key <prefix\|label> [--out <pem>] [--force] [--no-wait]` | 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 P-256 authorization key for Privy wallet RPC; it cannot sign Solana transactions (the TEE signs them). `--no-wait` exits 0 once the request exists. With `--json`, stdout is newline-delimited JSON: a pending event (`state`, `userCode`, `verificationUriComplete`, `expiresAt`, `fingerprint`) immediately, then an active result or error when waiting. A retry re-attaches to the same live request without changing its code or deadline. |
122
133
  | `candle tee signer status --key <prefix\|label> [--json]` | Read the current server signer state without generating a key, requesting a code or waiting. On the requesting machine, also shows the cached approval URL and code while the same request is live. |
123
134
  | `candle tee signer remove --key <prefix\|label>` | Removes this machine's key signer once it owns no wallets, and tells the server the signer is gone. Refuses while a wallet is still owned by it. Uses the device token, or the key's own API key. |
124
- | `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. |
135
+ | `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, filtered to one wallet by its id, address or exact label. |
125
136
  | `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). |
126
137
  | `candle tee new\|enable` | `new` adds a key to an existing `tee-wallets.enc` and refuses when that store is absent (`vault new-key --chain solana`, then `vault promote --from <label> --to-key <prefix>`). `--factor` does not apply. `enable <address> --vault <address>` delegates an existing key with a pinned sweep vault. `vault import-legacy --tee` migrates an existing store. |
127
138
  | `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. With `--json`, stdout is exactly one JSON value for swaps, token trades, replays and `swap status`: `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. |
@@ -213,7 +224,7 @@ which `candle verify` cannot read.
213
224
 
214
225
  No cosign or gh installed? `candle verify <file> --bundle <path>` (this CLI's own command, see the
215
226
  table above) runs the same check against the trusted root compiled into the binary, no network
216
- call required. Full walkthrough, including the installer script's own signature and the
227
+ call required. It is also what `install.sh` uses on a machine with neither. Full walkthrough, including the installer script's own signature and the
217
228
  transparency log: [Verify a Candle release](https://docs.candle.tv/developers/verify-a-candle-release).
218
229
 
219
230
  ## The `--json` contract
@@ -411,10 +422,11 @@ key; Candle builds the transaction and Privy signs it through the relay. See TEE
411
422
  below. Limit orders are a REST surface only (`/api/v1/trade/agent/orders`), not in this CLI, the
412
423
  SDK or the MCP server.
413
424
 
414
- **`keys limits`.** There is no command for setting per-key spend limits, because the API route
415
- that sets them (`PUT /keys/:prefix/limits`) structurally rejects a device token. It only accepts
416
- an agent key or a live session, since a spend limit is fund-movement authority, and this CLI's
417
- device token is scoped narrowly to key management. Manage limits from the portal.
425
+ **`keys limits`.** The API route that sets per-key spend limits (`PUT /keys/:prefix/limits`)
426
+ structurally rejects a device token: a spend limit is fund-movement authority, and this CLI's
427
+ device token is scoped narrowly to key management. So `candle keys limits` sends the profile's own
428
+ API key, and an agent key may only tighten (lower a cap, or add one where none was set). Raising
429
+ or removing a cap is refused with `LOOSEN_REQUIRES_SESSION`; the owner does that from the portal.
418
430
 
419
431
  **`devices list` / `devices revoke`.** The device-token endpoints (`GET`/`DELETE
420
432
  /device/tokens`) are session-only by design: this is the self-renewal guard that keeps a stolen
package/dist/index.js CHANGED
@@ -532,7 +532,7 @@ var init_terminal_theme = __esm(() => {
532
532
  });
533
533
 
534
534
  // src/version.ts
535
- var CLI_VERSION = "0.13.16";
535
+ var CLI_VERSION = "0.13.17";
536
536
 
537
537
  // src/presentation.ts
538
538
  function terminalStyle(deps, stream, machine = false) {
@@ -12603,9 +12603,9 @@ function kekReuseKey(passphrase, kdf) {
12603
12603
  const inputs = JSON.stringify([passphrase, kdf.salt, kdf.m, kdf.t, kdf.p, kdf.version]);
12604
12604
  return b64u(sha2562(new TextEncoder().encode(inputs)));
12605
12605
  }
12606
- async function derivePassphraseKek(passphrase, kdf, notice2) {
12606
+ async function derivePassphraseKek(passphrase, kdf, notice2, derivationLabel = "Deriving the vault key") {
12607
12607
  assertKdfInBounds(kdf);
12608
- notice2?.(`Deriving the vault key (Argon2id, ${Math.round(kdf.m / 1024)} MiB)
12608
+ notice2?.(`${derivationLabel} (Argon2id, ${Math.round(kdf.m / 1024)} MiB)
12609
12609
  `);
12610
12610
  const reuseKey = testKekReuse ? kekReuseKey(passphrase, kdf) : null;
12611
12611
  const reused = reuseKey ? testKekReuse?.get(reuseKey) : undefined;
@@ -13990,7 +13990,7 @@ function closeVault(vault) {
13990
13990
  async function unlockVault(path, raw, request, opts = {}) {
13991
13991
  const file = parseVaultFile(raw);
13992
13992
  const envelope = pickEnvelope(file, request);
13993
- const dek = await unwrapDek(file, envelope, request, opts.notice);
13993
+ const dek = await unwrapDek(file, envelope, request, opts.notice, opts.derivationLabel);
13994
13994
  if (dek.length !== DEK_BYTES) {
13995
13995
  wipe(dek);
13996
13996
  throw new VaultError("VAULT_UNLOCK_FAILED", "The unwrapped key is the wrong length; this file is corrupt.");
@@ -14030,9 +14030,9 @@ async function openWithDek(path, raw, file, envelope, dek) {
14030
14030
  throw error;
14031
14031
  }
14032
14032
  }
14033
- async function unwrapDek(file, envelope, request, notice2) {
14033
+ async function unwrapDek(file, envelope, request, notice2, derivationLabel) {
14034
14034
  if (request.factor === "passphrase") {
14035
- const kek = await derivePassphraseKek(request.passphrase, passphraseKdf(envelope), notice2);
14035
+ const kek = await derivePassphraseKek(request.passphrase, passphraseKdf(envelope), notice2, derivationLabel);
14036
14036
  return withSecret(kek, async (kekBytes) => {
14037
14037
  const kekKey2 = await importAesKey(kekBytes);
14038
14038
  return open(kekKey2, envelope.wrap, envelopeAad(file, envelope), {
@@ -38135,6 +38135,7 @@ async function keysList(args, ctx) {
38135
38135
  async function keysCreate(args, ctx) {
38136
38136
  const { deps, apiUrl, json } = ctx;
38137
38137
  const parsed = parseArgs(args, {
38138
+ booleanFlags: ["--no-store"],
38138
38139
  valueFlags: [
38139
38140
  "--scopes",
38140
38141
  "--access",
@@ -38239,11 +38240,12 @@ async function keysCreate(args, ctx) {
38239
38240
  return 1;
38240
38241
  }
38241
38242
  const body = result.body;
38243
+ const noStore = parsed.booleans.has("--no-store");
38242
38244
  const apiKeyRef = ctx.profile ? profileSecretRef(ctx.profile, "apiKey") : SECRET_REFS.apiKey;
38243
38245
  let stored = false;
38244
38246
  let storeError;
38245
38247
  try {
38246
- if (!await deps.store.get(apiKeyRef)) {
38248
+ if (!noStore && !await deps.store.get(apiKeyRef)) {
38247
38249
  await deps.store.set(apiKeyRef, body.key);
38248
38250
  if (ctx.profile) {
38249
38251
  await deps.updateProfile(ctx.profile, { keyPrefix: body.keyPrefix, scopes: body.scopes });
@@ -38277,7 +38279,7 @@ Or mint it that way: candle keys create --access withdraw --embedded-wallet allo
38277
38279
  ...body.embeddedWallet === "allowed" || body.embeddedWallet === "denied" ? [["Embedded wallet", embeddedWalletSummary(body.embeddedWallet)]] : [],
38278
38280
  [
38279
38281
  "Stored",
38280
- storeError !== undefined ? "no (store failed)" : stored ? `${deps.backend}, as this CLI's working key` : "no (the CLI already manages a different working key)"
38282
+ storeError !== undefined ? "no (store failed)" : stored ? `${deps.backend}, as this CLI's working key` : noStore ? "no (--no-store)" : "no (the CLI already manages a different working key)"
38281
38283
  ]
38282
38284
  ], style).join(`
38283
38285
  `)}
@@ -40791,32 +40793,38 @@ async function executionStatus(cfg, doFetch) {
40791
40793
  }
40792
40794
  return { ok: res.ok, status: res.status, body };
40793
40795
  };
40794
- const [wallets2, tier, limits] = await Promise.all([
40796
+ const [wallets2, tier, limits, risk] = await Promise.all([
40795
40797
  get("/api/v1/agent/wallets/embedded"),
40796
40798
  get("/api/v1/agent/tier"),
40797
- get("/api/v1/agent/keys/self/limits")
40799
+ get("/api/v1/agent/keys/self/limits"),
40800
+ get("/api/v1/agent/keys/self/risk")
40798
40801
  ]);
40799
40802
  const unreadable2 = [
40800
40803
  ["wallets", wallets2],
40801
40804
  ["tier", tier],
40802
- ["limits", limits]
40805
+ ["limits", limits],
40806
+ ["risk", risk]
40803
40807
  ].filter(([, r]) => !r.ok);
40804
40808
  const tierBody = tier.ok ? tier.body : undefined;
40809
+ const riskBody = risk.ok ? risk.body : undefined;
40810
+ const stopped = riskBody?.stopped === true;
40805
40811
  return {
40806
40812
  text: JSON.stringify({
40807
40813
  success: true,
40808
- ready: unreadable2.length === 0 ? true : undefined,
40814
+ ready: stopped ? false : unreadable2.length === 0 ? true : undefined,
40815
+ stopped: stopped ? { tripped: riskBody?.tripped ?? null, clearedBy: STOP_CLEARED_BY } : undefined,
40809
40816
  notice: tierBody?.maxExpired ? tierBody.maxExpiredNotice : undefined,
40810
40817
  updateAvailable: updateAvailable() ?? undefined,
40811
40818
  unreadable: unreadable2.length > 0 ? unreadable2.map(([name]) => name) : undefined,
40812
40819
  wallets: wallets2.body,
40813
40820
  tier: tier.body,
40814
- limits: limits.body
40821
+ limits: limits.body,
40822
+ risk: risk.body
40815
40823
  }, null, 2),
40816
40824
  ...unreadable2.length > 0 ? { isError: true } : {}
40817
40825
  };
40818
40826
  }
40819
- var SOLANA_SEED_CEILINGS, MAIN_PAYER, SWEEP_BASE_ASSETS;
40827
+ var SOLANA_SEED_CEILINGS, MAIN_PAYER, SWEEP_BASE_ASSETS, STOP_CLEARED_BY = "An account owner reviews the profile and resumes it from the agent console; an API key cannot resume it.";
40820
40828
  var init_orchestrate = __esm(() => {
40821
40829
  init_convert();
40822
40830
  init_image();
@@ -45246,13 +45254,40 @@ function buildRequest(name, args, cfg) {
45246
45254
  }
45247
45255
  case "candle_get_profile_trades": {
45248
45256
  const apiKey = requireApiKey2(cfg);
45249
- const { keyPrefix, limit } = args;
45250
- const query2 = limit === undefined ? "" : `?limit=${encodeURIComponent(String(limit))}`;
45257
+ const { keyPrefix, limit, since, until, cursor } = args;
45258
+ const params = new URLSearchParams;
45259
+ if (limit !== undefined)
45260
+ params.set("limit", String(limit));
45261
+ if (since !== undefined)
45262
+ params.set("since", since);
45263
+ if (until !== undefined)
45264
+ params.set("until", until);
45265
+ if (cursor !== undefined)
45266
+ params.set("cursor", cursor);
45251
45267
  return {
45252
- url: `${base2}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/trades${query2}`,
45268
+ url: `${base2}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/trades${params.size ? `?${params}` : ""}`,
45253
45269
  init: { method: "GET", headers: jsonHeaders(apiKey) }
45254
45270
  };
45255
45271
  }
45272
+ case "candle_get_paper_inventory": {
45273
+ return {
45274
+ url: `${base2}/api/v1/trade/agent/paper/inventory`,
45275
+ init: { method: "GET", headers: jsonHeaders(requireApiKey2(cfg)) }
45276
+ };
45277
+ }
45278
+ case "candle_get_profile_paper": {
45279
+ const { keyPrefix } = args;
45280
+ return {
45281
+ url: `${base2}/api/v1/agent/keys/${encodeURIComponent(keyPrefix)}/paper`,
45282
+ init: { method: "GET", headers: jsonHeaders(requireApiKey2(cfg)) }
45283
+ };
45284
+ }
45285
+ case "candle_get_account_pnl": {
45286
+ return {
45287
+ url: `${base2}/api/v1/agent/keys/pnl`,
45288
+ init: { method: "GET", headers: jsonHeaders(requireApiKey2(cfg)) }
45289
+ };
45290
+ }
45256
45291
  case "candle_transfer": {
45257
45292
  const apiKey = requireApiKey2(cfg);
45258
45293
  return {
@@ -45697,9 +45732,24 @@ function registerToolSubset(server, options) {
45697
45732
  }, async (args) => callAndRelay("candle_get_profile_pnl", args, getConfig(), doFetch));
45698
45733
  register("candle_get_profile_trades", {
45699
45734
  title: "Read an agent profile's trade history",
45700
- description: "Orders, actual fills, fees, timestamps and transaction hashes for this profile. Includes FAILED " + "trades, with an errorCode saying why each did not go through, so this answers 'what happened to " + "my order' as well as 'what did I trade'. Reads only; moves nothing.",
45735
+ description: "Orders, actual fills, fees, timestamps and transaction hashes for this profile. Page by time with " + "`since`, `until` and `cursor` (continue with the response's `nextCursor`). Includes FAILED " + "trades, with an errorCode saying why each did not go through, so this answers 'what happened to " + "my order' as well as 'what did I trade'. Reads only; moves nothing.",
45701
45736
  inputSchema: profileTradesShape
45702
45737
  }, async (args) => callAndRelay("candle_get_profile_trades", args, getConfig(), doFetch));
45738
+ register("candle_get_paper_inventory", {
45739
+ title: "Read this key's paper inventory",
45740
+ description: "This key's PAPER book in raw units: the positions that paper buys have credited and a paper " + "percent sell sizes itself from. Derived from admitted quotes; no money moved and the live " + "wallet is untouched. Never mix it with live holdings (candle_get_portfolio). Fails retryably " + "when the book cannot be read completely. Needs swap:write on the key. Reads only.",
45741
+ inputSchema: noArgsShape
45742
+ }, async (args) => callAndRelay("candle_get_paper_inventory", args, getConfig(), doFetch));
45743
+ register("candle_get_profile_paper", {
45744
+ title: "Read an agent profile's paper P&L",
45745
+ description: "One profile's PAPER results: the same accounting as candle_get_profile_pnl, from trades that " + "never happened. `pnl` is the paper P&L and `trades` its 50 most recent paper trades. Carries " + '`basis: "quote"`: the figures come from quotes at admission and are optimistic by the ' + "slippage and fees real execution pays. Never add them to live P&L. Reads only.",
45746
+ inputSchema: profilePnlShape
45747
+ }, async (args) => callAndRelay("candle_get_profile_paper", args, getConfig(), doFetch));
45748
+ register("candle_get_account_pnl", {
45749
+ title: "Read P&L across all of an account's agent keys",
45750
+ description: "The account's P&L split by agent profile in one read, as `candle pnl` without `--profile` " + "does: one row per profile with its ledger figures and, where it has a board entry, `board` " + "(all-time public total with creator-fee claims credited to the launching agent, so a profile's " + "board total can differ from its per-key ledger P&L). A wallet shared by several keys books a " + "round to its bound key, else the key whose buy opened it. Use candle_get_profile_pnl for one " + "profile's positions. Reads only.",
45751
+ inputSchema: noArgsShape
45752
+ }, async (args) => callAndRelay("candle_get_account_pnl", args, getConfig(), doFetch));
45703
45753
  register("candle_get_portfolio", {
45704
45754
  title: "Read what the account holds, on Solana and Hood",
45705
45755
  description: "Balances and prices for the wallets Candle already knows on this account, on both chains: the " + "embedded wallet and every TEE wallet. Solana wallets are in `embedded` and `tee` (SOL as raw " + "`lamports`, tokens as raw `amountRaw` with `decimals`). Hood wallets are in `hood.embedded` and " + "`hood.tee` (ETH as raw `wei`, ERC-20s including USDG), never in the top-level arrays. Every " + "wallet and holding carries `chain`. Prices are in `prices`: a Solana mint under its own " + "address, Hood ETH under `hood:native`, and a Hood token under `hood:<contract lowercased>`, so " + "lowercase the address before looking it up. An unpriced entry is `priceUsd: null`, never zero; " + "a Hood one says why in `unpricedReason` (no-market-row, unusable-price, stale-mark, " + "source-unavailable), and `hood.unpriced` / `hood.unpricedByReason` count them. A Hood token " + "mark older than six hours is not used, so many Hood tokens read unpriced. A wallet whose read " + "failed has null balances, never zero, and is listed in `unavailable` (Solana) or " + "`hood.unavailable`; check `complete` before quoting a total. `walletsComplete` is false only " + "when the wallet list itself was cut off, and is absent on a server that predates it: then " + "`complete: false` may be a cut-off list or a failed read. Vault and external wallets are " + "not included: Candle does not know their addresses (the Candle CLI's `candle portfolio` reads " + "them over your own RPC). Needs a key with the account:read scope; without it the answer is " + "SCOPE_MISSING. A server that predates Hood in the portfolio omits `hood` and `chain`. Reads " + "only; moves nothing." + (options.localHyperliquid ? " This local server adds `hyperliquid`: each Hood TEE wallet's Hyperliquid account (account value, " + "unrealized P&L, positions, risk level at the default thresholds) read live from Hyperliquid, with a " + "subtotal. It is not in the ledger and is never part of any total. `read: false` with a reason, or " + "entries in `unread`, means those accounts are unknown, not empty." : ""),
@@ -45721,7 +45771,7 @@ function registerToolSubset(server, options) {
45721
45771
  });
45722
45772
  register("candle_execution_status", {
45723
45773
  title: "Can this key execute right now",
45724
- description: "One call before trading: the embedded wallets to spend from, the tier that decides what " + "may be traded, and this key's own spend limits. Reads only; moves nothing. Call it when " + "a run starts, or after an authorization error, rather than inferring readiness from a " + "failed trade. If a read could not be completed the tool says which one and does NOT " + "claim the account is unready: an unreachable endpoint and a missing tier are different " + "problems with different fixes.",
45774
+ description: "One call before trading: the embedded wallets to spend from, the tier that decides what " + "may be traded, this key's own spend limits, and its loss limits and stop state (risk: " + "lossLimits, state, stopped, tripped). A stopped key reports ready: false and a top-level " + "stopped naming the bound that broke; every trade answers RISK_LIMIT_REACHED until an " + "account owner resumes it, so stop trading and say why rather than retrying. Any scope " + "can read its own. Reads only; moves nothing. Call it when " + "a run starts, or after an authorization error, rather than inferring readiness from a " + "failed trade. If a read could not be completed the tool says which one and does NOT " + "claim the account is unready: an unreachable endpoint and a missing tier are different " + "problems with different fixes.",
45725
45775
  inputSchema: {}
45726
45776
  }, async () => {
45727
45777
  const result = await executionStatus(getConfig(), doFetch);
@@ -45988,7 +46038,7 @@ async function registerEligibleProfileTool(server, env = process.env, doFetch =
45988
46038
  }
45989
46039
  return eligible;
45990
46040
  }
45991
- var VAMP_CANDIDATES_DESCRIPTION, localProfileTools, STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet. sCNDL in any eligible account wallet counts toward Pro and earns the account's Hood staking reward, except a wallet another account also claims, wallets past the 200-wallet cap, and an account with no EVM wallet. CNDL in a pending withdrawal counts toward nothing for Pro.", stakingWriteShape, TOOL_NAMES, LAB_TOOL_NAMES, PROFILE_EDIT_TOOL_NAMES, isProfileEditTool = (name) => PROFILE_EDIT_TOOL_NAMES.includes(name), OPERATION_NOT_FOUND_RULE, getMarketShape, priceHistoryShape, TOKEN_EVENTS_LIMIT_MESSAGE = "limit must be a whole number from 1 to 1000", tokenEventsLimit, TOKEN_EVENTS_KIND_NOTE = "kept back from the request and applied to the returned page by this server, so a filtered page can be shorter than limit while older events of other kinds remain: page with nextCursor as before until it is null", tokenCardShape, tokenEventsShape, tokenForensicsShape, FEED_LIMIT_MESSAGE = "limit must be a whole number from 1 to 200", feedLimit, getFeedShape, reportActivityShape, getAgentProfileShape, listAgentsShape, createPostShape, deletePostShape, listAgentPostsShape, getAgentPositionsShape, getOperationShape, vampCandidatesShape, getFeesShape, atomicPayerShape, atomicLaunchDryRunShape, claimFeesShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, setProfileWalletsShape, swapShape, tradeShape, tokenImageUrl, tokenBannerUrl, localImageShape, localLaunchTokenShape, localLaunchAndSeedShape, IMAGE_PATH_DISCLOSURE, transferShape, sweepShape, PROFILE_PNL_HYPERLIQUID;
46041
+ var VAMP_CANDIDATES_DESCRIPTION, localProfileTools, STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet. sCNDL in any eligible account wallet counts toward Pro and earns the account's Hood staking reward, except a wallet another account also claims, wallets past the 200-wallet cap, and an account with no EVM wallet. CNDL in a pending withdrawal counts toward nothing for Pro.", stakingWriteShape, TOOL_NAMES, LAB_TOOL_NAMES, PROFILE_EDIT_TOOL_NAMES, isProfileEditTool = (name) => PROFILE_EDIT_TOOL_NAMES.includes(name), OPERATION_NOT_FOUND_RULE, getMarketShape, priceHistoryShape, TOKEN_EVENTS_LIMIT_MESSAGE = "limit must be a whole number from 1 to 1000", tokenEventsLimit, TOKEN_EVENTS_KIND_NOTE = "kept back from the request and applied to the returned page by this server, so a filtered page can be shorter than limit while older events of other kinds remain: page with nextCursor as before until it is null", tokenCardShape, tokenEventsShape, tokenForensicsShape, FEED_LIMIT_MESSAGE = "limit must be a whole number from 1 to 200", feedLimit, getFeedShape, reportActivityShape, getAgentProfileShape, listAgentsShape, createPostShape, deletePostShape, listAgentPostsShape, getAgentPositionsShape, getOperationShape, vampCandidatesShape, getFeesShape, atomicPayerShape, atomicLaunchDryRunShape, claimFeesShape, resolveTokenShape, profileWalletsShape, profilePnlShape, profileTradesShape, noArgsShape, setProfileWalletsShape, swapShape, tradeShape, tokenImageUrl, tokenBannerUrl, localImageShape, localLaunchTokenShape, localLaunchAndSeedShape, IMAGE_PATH_DISCLOSURE, transferShape, sweepShape, PROFILE_PNL_HYPERLIQUID;
45992
46042
  var init_tools = __esm(() => {
45993
46043
  init_zod();
45994
46044
  init_client2();
@@ -46063,6 +46113,9 @@ var init_tools = __esm(() => {
46063
46113
  "candle_set_profile_wallets",
46064
46114
  "candle_get_profile_pnl",
46065
46115
  "candle_get_profile_trades",
46116
+ "candle_get_paper_inventory",
46117
+ "candle_get_profile_paper",
46118
+ "candle_get_account_pnl",
46066
46119
  "candle_get_portfolio",
46067
46120
  "candle_resolve_token",
46068
46121
  "candle_execution_status",
@@ -46245,8 +46298,12 @@ var init_tools = __esm(() => {
46245
46298
  };
46246
46299
  profileTradesShape = {
46247
46300
  keyPrefix: exports_external.string().describe("The profile's API key prefix"),
46248
- limit: exports_external.number().optional().describe("How many of the most recent trades to return. Default 200, max 1000.")
46301
+ limit: exports_external.number().optional().describe("How many of the most recent trades to return. Default 200, max 1000."),
46302
+ since: exports_external.string().optional().describe("Inclusive lower bound on the trade's createdAt: an ISO 8601 time or epoch milliseconds."),
46303
+ until: exports_external.string().optional().describe("Exclusive upper bound on createdAt, in the same forms as `since`."),
46304
+ cursor: exports_external.string().optional().describe("The `nextCursor` of the previous page, to continue the same window.")
46249
46305
  };
46306
+ noArgsShape = {};
46250
46307
  setProfileWalletsShape = {
46251
46308
  keyPrefix: exports_external.string().describe("The profile's API key prefix"),
46252
46309
  walletIds: exports_external.array(exports_external.string()).describe("The linked-wallet ids the profile may spend from -- ids, not addresses. This REPLACES the " + "whole set: any wallet omitted loses access. An empty array assigns none.")
@@ -76895,8 +76952,8 @@ var HELP = {
76895
76952
  description: "List live API keys: name and Read, Write or Withdraw access; --all includes revoked keys; --scopes adds the raw scopes"
76896
76953
  },
76897
76954
  {
76898
- invocation: "create [--access read|write|withdraw|trade-perps | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]] [--embedded-wallet deny|allow]",
76899
- description: "Create an API key; --access mints one of the levels (write trades and launches; only withdraw can move funds; trade-perps is exactly account:read and perps:write, for Hyperliquid perps setup and orders, and it cannot fund its own Hyperliquid account: a Relay deposit needs swap:write and a withdrawal needs perps:withdraw, each a separate grant through --scopes). A trade-perps key trades from an EVM TEE wallet promoted onto it (candle vault new-key --chain evm, then candle vault promote --from <cold> --to-key <prefix>; candle tee new is Solana only) and a signer on the trading machine (candle tee signer new --key <prefix>). --embedded-wallet defaults to deny; allow lets the key spend the account's embedded wallet and is confirmed at a terminal"
76955
+ invocation: "create [--access read|write|withdraw|trade-perps | --scopes <a,b,c>] [--label <name>] [--expires-in <days>] [--tx-limit <usd> [--reset daily|weekly|monthly|never]] [--embedded-wallet deny|allow] [--no-store]",
76956
+ description: "Create an API key; --no-store prints it without saving it as the profile's working key (default: store only if no working key exists). --access mints one of the levels (write trades and launches; only withdraw can move funds; trade-perps is exactly account:read and perps:write, for Hyperliquid perps setup and orders, and it cannot fund its own Hyperliquid account: a Relay deposit needs swap:write and a withdrawal needs perps:withdraw, each a separate grant through --scopes). A trade-perps key trades from an EVM TEE wallet promoted onto it (candle vault new-key --chain evm, then candle vault promote --from <cold> --to-key <prefix>; candle tee new is Solana only) and a signer on the trading machine (candle tee signer new --key <prefix>). --embedded-wallet defaults to deny; allow lets the key spend the account's embedded wallet and is confirmed at a terminal"
76900
76957
  },
76901
76958
  {
76902
76959
  invocation: "access <prefix|label|self> (--access read|write|withdraw [--yes] | --history)",
@@ -76910,6 +76967,10 @@ var HELP = {
76910
76967
  invocation: "limit <prefix|label> (--tx-limit <usd> [--reset daily|weekly|monthly|never] | --clear)",
76911
76968
  description: "Set or clear the USD transaction cap on an existing key, with the device token. --reset defaults to daily"
76912
76969
  },
76970
+ {
76971
+ invocation: "limits <prefix> <asset>=<raw> [<asset>=<raw> ...]",
76972
+ description: "Set a key's per-asset caps (sol, usdc, cndl, eth, usdg; a whole number of base units per transaction) with the profile's own API key. It can only lower a cap or add one; raising or removing one is refused (LOOSEN_REQUIRES_SESSION) and is the owner's, in the web key manager. The list replaces the key's caps"
76973
+ },
76913
76974
  {
76914
76975
  invocation: "self embedded-wallet deny",
76915
76976
  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"
@@ -76946,11 +77007,12 @@ var HELP = {
76946
77007
  "candle keys list",
76947
77008
  "candle keys create --access withdraw --label rebalancer",
76948
77009
  "candle keys create --access trade-perps --label perps-bot",
76949
- "candle keys create --scopes trade:write --label agent-one",
77010
+ "candle keys create --scopes trade:write --label agent-one --no-store",
76950
77011
  "candle keys access cndl --access withdraw",
76951
77012
  "candle keys access self --access read --yes",
76952
77013
  "candle keys update agent-one --embedded-wallet deny --yes",
76953
77014
  "candle keys self embedded-wallet deny",
77015
+ "candle keys limits Ab3dEf9h sol=1000000000 usdc=50000000",
76954
77016
  "candle keys wallets ck_live_ab12",
76955
77017
  "candle tee rebind --label-prefix dest- --to-key Ab3dEf9h",
76956
77018
  "candle keys signer approve ABCD-EFGH --key tr-2",
@@ -77872,6 +77934,9 @@ var TOOL_CLASSES = {
77872
77934
  candle_get_profile_wallets: READ,
77873
77935
  candle_get_profile_pnl: READ,
77874
77936
  candle_get_profile_trades: READ,
77937
+ candle_get_paper_inventory: READ,
77938
+ candle_get_profile_paper: READ,
77939
+ candle_get_account_pnl: READ,
77875
77940
  candle_get_portfolio: READ,
77876
77941
  candle_resolve_token: READ,
77877
77942
  candle_execution_status: READ,
@@ -90263,6 +90328,100 @@ ${fields(rows, style).join(`
90263
90328
  return 0;
90264
90329
  }
90265
90330
 
90331
+ // src/commands/keys-limits.ts
90332
+ init_args();
90333
+ init_client();
90334
+ init_deps();
90335
+ init_presentation();
90336
+ init_profiles();
90337
+ init_key_output();
90338
+ var USAGE8 = "Usage: candle keys limits <prefix> <asset>=<raw-amount> [<asset>=<raw-amount> ...]";
90339
+ var KEY_LIMIT_ASSETS = { sol: 9, usdc: 6, cndl: 6, eth: 18, usdg: 6 };
90340
+ var RAW_AMOUNT = /^[1-9][0-9]*$/;
90341
+ var NO_API_KEY3 = {
90342
+ code: "API_KEY_REQUIRED",
90343
+ message: "candle keys limits changes caps with an API key, and this profile holds none.",
90344
+ suggestion: "Select the profile holding the key with --profile. A device token cannot set per-asset caps."
90345
+ };
90346
+ var LOOSEN_HINT = "This command can only lower a cap. Raising or removing one needs the owner's session: use the web key manager.";
90347
+ async function keysLimits(args, ctx) {
90348
+ const { deps, apiUrl, json } = ctx;
90349
+ const parsed = parseArgs(args, { keyPrefixPositional: true });
90350
+ if ("error" in parsed) {
90351
+ writeScreenUsageFailure(deps, parsed.error, json);
90352
+ return 2;
90353
+ }
90354
+ const target = parsed.positionals[0];
90355
+ const caps = parsed.positionals.slice(1);
90356
+ if (!target || caps.length === 0) {
90357
+ writeScreenUsageFailure(deps, USAGE8, json);
90358
+ return 2;
90359
+ }
90360
+ const limits = [];
90361
+ for (const cap of caps) {
90362
+ const eq = cap.indexOf("=");
90363
+ const asset = (eq < 0 ? cap : cap.slice(0, eq)).toLowerCase();
90364
+ const raw = eq < 0 ? "" : cap.slice(eq + 1);
90365
+ if (!(asset in KEY_LIMIT_ASSETS)) {
90366
+ writeScreenUsageFailure(deps, `The asset must be one of: ${Object.keys(KEY_LIMIT_ASSETS).join(", ")}.`, json);
90367
+ return 2;
90368
+ }
90369
+ if (!RAW_AMOUNT.test(raw)) {
90370
+ writeScreenUsageFailure(deps, `${asset}=<raw> takes a positive whole number of base units (${asset} has ${KEY_LIMIT_ASSETS[asset]} decimals), with no sign, decimal point or leading zero.`, json);
90371
+ return 2;
90372
+ }
90373
+ if (limits.some((limit) => limit.asset === asset)) {
90374
+ writeScreenUsageFailure(deps, `${asset} is named twice.`, json);
90375
+ return 2;
90376
+ }
90377
+ limits.push({ asset, maxPerTxRaw: raw });
90378
+ }
90379
+ await printIdentity(ctx);
90380
+ const apiKey = await resolveApiKey(deps, ctx.profile);
90381
+ if (!apiKey) {
90382
+ writeScreenLocalFailure(deps, NO_API_KEY3, json);
90383
+ return 1;
90384
+ }
90385
+ const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(target)}/limits`, {
90386
+ method: "PUT",
90387
+ auth: "key",
90388
+ credentials: { apiKey },
90389
+ apiUrl,
90390
+ fetch: deps.fetch,
90391
+ env: deps.env,
90392
+ body: { limits }
90393
+ });
90394
+ if (!result.ok) {
90395
+ if (!json && result.code === "LOOSEN_REQUIRES_SESSION")
90396
+ writeScreenLocalFailure(deps, {
90397
+ code: "LOOSEN_REQUIRES_SESSION",
90398
+ message: "That would raise or remove a cap; nothing changed.",
90399
+ suggestion: LOOSEN_HINT
90400
+ }, json);
90401
+ else
90402
+ writeScreenFailure(deps, result, { apiUrl, authType: "key" }, json);
90403
+ return 1;
90404
+ }
90405
+ const stored = result.body.limits ?? limits;
90406
+ if (json) {
90407
+ deps.stdout.write(`${JSON.stringify({ ok: true, keyPrefix: target, limits: stored })}
90408
+ `);
90409
+ return 0;
90410
+ }
90411
+ const style = terminalStyle(deps, "stdout");
90412
+ const rows = [
90413
+ ["Key", target],
90414
+ ...stored.map((limit) => [limit.asset, `${limit.maxPerTxRaw} base units per transaction`])
90415
+ ];
90416
+ deps.stdout.write(`${success("Per-asset caps updated", style)}
90417
+
90418
+ ${heading("Caps", style)}
90419
+ ${fields(rows, style).join(`
90420
+ `)}
90421
+ `);
90422
+ return 0;
90423
+ }
90424
+
90266
90425
  // src/commands/keys-rotate.ts
90267
90426
  init_args();
90268
90427
  init_client();
@@ -90292,7 +90451,7 @@ var TOKEN_PAGE_BODY_CAP_BYTES = 32 * 1024;
90292
90451
  var TOKEN_PAGE_SECTIONS = ["thesis", "tokenomics", "lore"];
90293
90452
  var FLAG_REASONS = ["spam", "scam", "abuse", "misleading", "unsafe_link", "other"];
90294
90453
  var FLAG_DETAIL_LIMIT = 500;
90295
- var USAGE8 = {
90454
+ var USAGE9 = {
90296
90455
  about: "Usage: candle token about <address> [--chain solana|hood] [--capabilities]",
90297
90456
  claim: "Usage: candle token claim <address> [--chain solana|hood] [--creator [--challenge [--message-out <path>] | --challenge-id <id> --signature <sig>]] [--idempotency-key <key>]",
90298
90457
  section: `Usage: candle token section set <address> <thesis|tokenomics|lore> --file <path> --base-revision <n> [--clear] [--chain solana|hood] [--idempotency-key <key>]
@@ -90552,7 +90711,7 @@ function terminalTextKeepingLines(text3) {
90552
90711
  `);
90553
90712
  }
90554
90713
  async function tokenAbout(args, ctx) {
90555
- const usage4 = usageFor(ctx, USAGE8.about);
90714
+ const usage4 = usageFor(ctx, USAGE9.about);
90556
90715
  const parsed = parseArgs(args, { valueFlags: ["--chain"], booleanFlags: ["--capabilities"] });
90557
90716
  if ("error" in parsed)
90558
90717
  return usage4(parsed.error);
@@ -90598,7 +90757,7 @@ async function tokenAbout(args, ctx) {
90598
90757
  return 0;
90599
90758
  }
90600
90759
  async function tokenClaim(args, ctx) {
90601
- const usage4 = usageFor(ctx, USAGE8.claim);
90760
+ const usage4 = usageFor(ctx, USAGE9.claim);
90602
90761
  const parsed = parseArgs(args, {
90603
90762
  valueFlags: ["--chain", "--challenge-id", "--signature", "--message-out", "--idempotency-key"],
90604
90763
  booleanFlags: ["--creator", "--challenge"],
@@ -90725,7 +90884,7 @@ var SECTION_FLAGS = {
90725
90884
  restore: { valueFlags: ["--chain", "--version-id", "--base-revision", "--idempotency-key"], booleanFlags: [] }
90726
90885
  };
90727
90886
  async function tokenSection(args, ctx) {
90728
- const usage4 = usageFor(ctx, USAGE8.section);
90887
+ const usage4 = usageFor(ctx, USAGE9.section);
90729
90888
  const action = args[0];
90730
90889
  if (action !== "set" && action !== "restore")
90731
90890
  return usage4();
@@ -90818,7 +90977,7 @@ async function tokenSection(args, ctx) {
90818
90977
  return 0;
90819
90978
  }
90820
90979
  async function tokenHistory(args, ctx) {
90821
- const usage4 = usageFor(ctx, USAGE8.history);
90980
+ const usage4 = usageFor(ctx, USAGE9.history);
90822
90981
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--limit", "--cursor"] });
90823
90982
  if ("error" in parsed)
90824
90983
  return usage4(parsed.error);
@@ -90873,7 +91032,7 @@ More: --cursor ${data.nextCursor}
90873
91032
  return 0;
90874
91033
  }
90875
91034
  async function tokenFlag(args, ctx) {
90876
- const usage4 = usageFor(ctx, USAGE8.flag);
91035
+ const usage4 = usageFor(ctx, USAGE9.flag);
90877
91036
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--content", "--reason", "--detail", "--idempotency-key"] });
90878
91037
  if ("error" in parsed)
90879
91038
  return usage4(parsed.error);
@@ -90968,7 +91127,7 @@ async function tokenUpdate(args, ctx) {
90968
91127
  return 0;
90969
91128
  }
90970
91129
  async function tokenComment(args, ctx) {
90971
- const usage4 = usageFor(ctx, USAGE8.comment);
91130
+ const usage4 = usageFor(ctx, USAGE9.comment);
90972
91131
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--file", "--idempotency-key"], pathFlags: ["--file"] });
90973
91132
  if ("error" in parsed)
90974
91133
  return usage4(parsed.error);
@@ -91013,7 +91172,7 @@ async function tokenComment(args, ctx) {
91013
91172
  return 0;
91014
91173
  }
91015
91174
  async function tokenComments(args, ctx) {
91016
- const usage4 = usageFor(ctx, USAGE8.comments);
91175
+ const usage4 = usageFor(ctx, USAGE9.comments);
91017
91176
  const parsed = parseArgs(args, { valueFlags: ["--chain", "--limit", "--cursor"] });
91018
91177
  if ("error" in parsed)
91019
91178
  return usage4(parsed.error);
@@ -91100,7 +91259,7 @@ async function tokenPost(args, ctx) {
91100
91259
  }
91101
91260
 
91102
91261
  // src/commands/keys-rotate.ts
91103
- var USAGE9 = `Usage: candle keys rotate <prefix|label> [--grace <duration> | --now] [--force] [--yes] [--idempotency-key <key>]
91262
+ var USAGE10 = `Usage: candle keys rotate <prefix|label> [--grace <duration> | --now] [--force] [--yes] [--idempotency-key <key>]
91104
91263
  candle keys rotate <prefix|label> --finish`;
91105
91264
  var MAX_GRACE_SECONDS = 86400;
91106
91265
  var DEFAULT_GRACE_SECONDS = 3600;
@@ -91127,7 +91286,7 @@ async function keysRotate(args, ctx) {
91127
91286
  }
91128
91287
  const target = parsed.positionals[0];
91129
91288
  if (!target || parsed.positionals.length !== 1) {
91130
- writeScreenUsageFailure(deps, USAGE9, json);
91289
+ writeScreenUsageFailure(deps, USAGE10, json);
91131
91290
  return 2;
91132
91291
  }
91133
91292
  const finish = parsed.booleans.has("--finish");
@@ -91380,7 +91539,7 @@ init_presentation();
91380
91539
  init_profiles();
91381
91540
  init_render();
91382
91541
  init_key_output();
91383
- var NO_API_KEY3 = {
91542
+ var NO_API_KEY4 = {
91384
91543
  code: "NO_API_KEY",
91385
91544
  message: "No API key for this profile.",
91386
91545
  suggestion: "Set CANDLE_API_KEY, or run `candle keys create` and store one."
@@ -91404,7 +91563,7 @@ async function keysWalletsList(args, ctx) {
91404
91563
  await printIdentity(ctx);
91405
91564
  const apiKey = await resolveApiKey(deps, ctx.profile);
91406
91565
  if (!apiKey) {
91407
- writeScreenLocalFailure(deps, NO_API_KEY3, json);
91566
+ writeScreenLocalFailure(deps, NO_API_KEY4, json);
91408
91567
  return 1;
91409
91568
  }
91410
91569
  const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
@@ -91470,7 +91629,7 @@ async function keysWalletsSet(args, ctx) {
91470
91629
  await printIdentity(ctx);
91471
91630
  const apiKey = await resolveApiKey(deps, ctx.profile);
91472
91631
  if (!apiKey) {
91473
- writeScreenLocalFailure(deps, NO_API_KEY3, json);
91632
+ writeScreenLocalFailure(deps, NO_API_KEY4, json);
91474
91633
  return 1;
91475
91634
  }
91476
91635
  const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallets`, {
@@ -91525,7 +91684,7 @@ async function keysWalletsScope(args, ctx) {
91525
91684
  await printIdentity(ctx);
91526
91685
  const apiKey = await resolveApiKey(deps, ctx.profile);
91527
91686
  if (!apiKey) {
91528
- writeScreenLocalFailure(deps, NO_API_KEY3, json);
91687
+ writeScreenLocalFailure(deps, NO_API_KEY4, json);
91529
91688
  return 1;
91530
91689
  }
91531
91690
  const result = await apiRequest(`/api/v1/agent/keys/${encodeURIComponent(prefix)}/wallet-scope`, {
@@ -92380,7 +92539,7 @@ function poolRequestFields(flags) {
92380
92539
 
92381
92540
  // src/commands/launch.ts
92382
92541
  init_swap();
92383
- var USAGE10 = `Usage: ${LAUNCH_USAGE}`;
92542
+ var USAGE11 = `Usage: ${LAUNCH_USAGE}`;
92384
92543
  var EMBEDDED_WALLET_NAMES = new Set(LAUNCH_DEFINITION.wallet.embeddedNames);
92385
92544
  var LAUNCH_ARG_SPEC = {
92386
92545
  valueFlags: [
@@ -92491,7 +92650,7 @@ async function launch(args, ctx) {
92491
92650
  const dryRun = parsed.booleans.has("--dry-run");
92492
92651
  const check = parsed.booleans.has("--check");
92493
92652
  if (check && dryRun) {
92494
- writeUsageFailure(ctx.deps, `--check and --dry-run are two different stops; pass one. ${USAGE10}`, ctx.json);
92653
+ writeUsageFailure(ctx.deps, `--check and --dry-run are two different stops; pass one. ${USAGE11}`, ctx.json);
92495
92654
  return 2;
92496
92655
  }
92497
92656
  const fieldProblem = launchFieldProblem(flags);
@@ -92501,11 +92660,11 @@ async function launch(args, ctx) {
92501
92660
  }
92502
92661
  const curveProblem = curveFlagProblem(flags);
92503
92662
  if (curveProblem) {
92504
- writeUsageFailure(ctx.deps, `${curveProblem} ${USAGE10}`, ctx.json);
92663
+ writeUsageFailure(ctx.deps, `${curveProblem} ${USAGE11}`, ctx.json);
92505
92664
  return 2;
92506
92665
  }
92507
92666
  if (flags["--image"] !== undefined && flags["--image-url"] !== undefined) {
92508
- writeUsageFailure(ctx.deps, `Pass --image <path> or --image-url <https-url>, not both: a launch has one image. ${USAGE10}`, ctx.json);
92667
+ writeUsageFailure(ctx.deps, `Pass --image <path> or --image-url <https-url>, not both: a launch has one image. ${USAGE11}`, ctx.json);
92509
92668
  return 2;
92510
92669
  }
92511
92670
  if (flags["--image-url"] !== undefined && !flags["--image-url"].startsWith("https://")) {
@@ -92518,24 +92677,24 @@ async function launch(args, ctx) {
92518
92677
  }
92519
92678
  const missingRequired = !flags["--name"] || !flags["--symbol"] || flags["--image"] === undefined && flags["--image-url"] === undefined || !flags["--wallet"];
92520
92679
  if (parsed.positionals.length || !check && missingRequired || !validClientId(id)) {
92521
- writeUsageFailure(ctx.deps, USAGE10, ctx.json);
92680
+ writeUsageFailure(ctx.deps, USAGE11, ctx.json);
92522
92681
  return 2;
92523
92682
  }
92524
92683
  try {
92525
92684
  poolRequestFields(flags);
92526
92685
  } catch (error) {
92527
- writeUsageFailure(ctx.deps, `${error.message}. ${USAGE10}`, ctx.json);
92686
+ writeUsageFailure(ctx.deps, `${error.message}. ${USAGE11}`, ctx.json);
92528
92687
  return 2;
92529
92688
  }
92530
92689
  let feeWallet;
92531
92690
  try {
92532
92691
  feeWallet = feeWalletSelection(flags);
92533
92692
  } catch (error) {
92534
- writeUsageFailure(ctx.deps, `${error.message}. ${USAGE10}`, ctx.json);
92693
+ writeUsageFailure(ctx.deps, `${error.message}. ${USAGE11}`, ctx.json);
92535
92694
  return 2;
92536
92695
  }
92537
92696
  if (parsed.booleans.has("--require-fee-wallet") && !feeWallet) {
92538
- writeUsageFailure(ctx.deps, `--require-fee-wallet needs --fee-wallet <address>: pass the wallet this launch's fees earn to. ${USAGE10}`, ctx.json);
92697
+ writeUsageFailure(ctx.deps, `--require-fee-wallet needs --fee-wallet <address>: pass the wallet this launch's fees earn to. ${USAGE11}`, ctx.json);
92539
92698
  return 2;
92540
92699
  }
92541
92700
  try {
@@ -92543,11 +92702,11 @@ async function launch(args, ctx) {
92543
92702
  launchBuyRaw(flags["--buy"], 18);
92544
92703
  const hinted = flagChain(flags);
92545
92704
  if (hinted === "hood" && feeWalletGiven(flags)) {
92546
- writeUsageFailure(ctx.deps, `--fee-wallet is Solana only; a Hood launch cannot take it. ${USAGE10}`, ctx.json);
92705
+ writeUsageFailure(ctx.deps, `--fee-wallet is Solana only; a Hood launch cannot take it. ${USAGE11}`, ctx.json);
92547
92706
  return 2;
92548
92707
  }
92549
92708
  if (!check && hinted === "hood" && flags["--dex-version"] === undefined) {
92550
- writeUsageFailure(ctx.deps, `A Hood launch needs --dex-version v3|v4. ${USAGE10}`, ctx.json);
92709
+ writeUsageFailure(ctx.deps, `A Hood launch needs --dex-version v3|v4. ${USAGE11}`, ctx.json);
92551
92710
  return 2;
92552
92711
  }
92553
92712
  let image;
@@ -92565,7 +92724,7 @@ async function launch(args, ctx) {
92565
92724
  let rules;
92566
92725
  if (!check && launchRulesGiven(flags)) {
92567
92726
  if (hinted === "hood") {
92568
- writeUsageFailure(ctx.deps, `${LAUNCH_RULES_SOLANA_ONLY} ${USAGE10}`, ctx.json);
92727
+ writeUsageFailure(ctx.deps, `${LAUNCH_RULES_SOLANA_ONLY} ${USAGE11}`, ctx.json);
92569
92728
  return 2;
92570
92729
  }
92571
92730
  try {
@@ -93385,7 +93544,7 @@ init_presentation();
93385
93544
  init_render();
93386
93545
  init_trading();
93387
93546
  init_swap();
93388
- var USAGE11 = "Usage: candle launch status <mint-or-client-launch-id> [--wait]";
93547
+ var USAGE12 = "Usage: candle launch status <mint-or-client-launch-id> [--wait]";
93389
93548
  var LAUNCH_WAIT_MS = 10 * 60 * 1000;
93390
93549
  var LAUNCH_WAIT_POLL_MS = 1e4;
93391
93550
  var LIVE = new Set(["trading", "bonded", "migrated"]);
@@ -93431,7 +93590,7 @@ async function readStatus(ctx, key, id) {
93431
93590
  async function launchStatus(args, ctx) {
93432
93591
  const parsed = parseArgs(args, { booleanFlags: ["--wait"] });
93433
93592
  if ("error" in parsed || parsed.positionals.length !== 1 || !validClientId(parsed.positionals[0] ?? "")) {
93434
- writeUsageFailure(ctx.deps, USAGE11, ctx.json);
93593
+ writeUsageFailure(ctx.deps, USAGE12, ctx.json);
93435
93594
  return 2;
93436
93595
  }
93437
93596
  const id = parsed.positionals[0];
@@ -93923,6 +94082,9 @@ var MCP_TOOL_NAMES = [
93923
94082
  "candle_set_profile_wallets",
93924
94083
  "candle_get_profile_pnl",
93925
94084
  "candle_get_profile_trades",
94085
+ "candle_get_paper_inventory",
94086
+ "candle_get_profile_paper",
94087
+ "candle_get_account_pnl",
93926
94088
  "candle_get_portfolio",
93927
94089
  "candle_resolve_token",
93928
94090
  "candle_execution_status",
@@ -99328,7 +99490,7 @@ init_swap();
99328
99490
  init_vault_support();
99329
99491
  var WITHDRAW_SCOPE = "perps:withdraw";
99330
99492
  var ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
99331
- var USAGE12 = "Usage: candle perps withdraw <amount> --to solana|hood [--wallet <evm tee>] [--id <id>] [--keystore <path>] | candle perps withdraw status <id>";
99493
+ var USAGE13 = "Usage: candle perps withdraw <amount> --to solana|hood [--wallet <evm tee>] [--id <id>] [--keystore <path>] | candle perps withdraw status <id>";
99332
99494
  var WITHDRAW_POLL_MS = 10 * 60 * 1000;
99333
99495
  var WITHDRAW_POLL_INTERVAL_MS = 5000;
99334
99496
  var TERMINAL = new Set(["settled", "refunded", "failed"]);
@@ -99379,7 +99541,7 @@ async function withdrawStatus(ctx, id) {
99379
99541
  async function perpsWithdraw(args, ctx) {
99380
99542
  if (args[0] === "status") {
99381
99543
  if (args.length !== 2 || !ID_RE.test(args[1]))
99382
- return usage10(ctx, USAGE12);
99544
+ return usage10(ctx, USAGE13);
99383
99545
  try {
99384
99546
  return await withdrawStatus(ctx, args[1]);
99385
99547
  } catch (error) {
@@ -99398,7 +99560,7 @@ async function perpsWithdraw(args, ctx) {
99398
99560
  const [amount] = parsed.positionals;
99399
99561
  const to = parsed.values["--to"];
99400
99562
  if (parsed.positionals.length !== 1 || !amount || to !== "solana" && to !== "hood")
99401
- return usage10(ctx, USAGE12);
99563
+ return usage10(ctx, USAGE13);
99402
99564
  const amountMicros = parseWithdrawAmount(amount);
99403
99565
  if (amountMicros === null)
99404
99566
  return usage10(ctx, "The amount must be a positive USDC decimal with at most 6 decimals.");
@@ -101480,12 +101642,12 @@ function planPromotionLine2(table) {
101480
101642
  init_presentation();
101481
101643
  init_render();
101482
101644
  init_key_output();
101483
- var USAGE13 = "Usage: candle plans";
101645
+ var USAGE14 = "Usage: candle plans";
101484
101646
  async function plans(args, ctx) {
101485
101647
  const parsed = parseArgs(args, {});
101486
101648
  if ("error" in parsed || parsed.positionals.length > 0) {
101487
101649
  writeScreenUsageFailure(ctx.deps, "error" in parsed ? `${parsed.error}
101488
- ${USAGE13}` : USAGE13, ctx.json);
101650
+ ${USAGE14}` : USAGE14, ctx.json);
101489
101651
  return 2;
101490
101652
  }
101491
101653
  const result = await apiRequest("/api/v1/agent/plans", {
@@ -102100,7 +102262,7 @@ function writePosition(ctx, p) {
102100
102262
  // src/commands/pnl.ts
102101
102263
  var CHAINS = ["solana", "hood"];
102102
102264
  var CHAIN_TITLES = { solana: "Solana", hood: "Hood" };
102103
- var NO_API_KEY4 = {
102265
+ var NO_API_KEY5 = {
102104
102266
  code: "NO_API_KEY",
102105
102267
  message: "No API key for this profile.",
102106
102268
  suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
@@ -102119,7 +102281,7 @@ async function pnl(args, ctx) {
102119
102281
  }
102120
102282
  const apiKey = await resolveApiKey(deps, ctx.profile);
102121
102283
  if (!apiKey) {
102122
- writeLocalFailure(deps, NO_API_KEY4, json);
102284
+ writeLocalFailure(deps, NO_API_KEY5, json);
102123
102285
  return 1;
102124
102286
  }
102125
102287
  const perProfile = ctx.profileFlag !== undefined;
@@ -102567,7 +102729,7 @@ var UNREAD_LABELS = {
102567
102729
  usdg: "USDG",
102568
102730
  "hood-tokens": "tokens"
102569
102731
  };
102570
- var NO_API_KEY5 = {
102732
+ var NO_API_KEY6 = {
102571
102733
  code: "NO_API_KEY",
102572
102734
  message: "No API key for this profile.",
102573
102735
  suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
@@ -102602,7 +102764,7 @@ async function portfolio(args, ctx) {
102602
102764
  }
102603
102765
  const apiKey = await resolveApiKey(deps, ctx.profile);
102604
102766
  if (!apiKey) {
102605
- writeLocalFailure(deps, NO_API_KEY5, ctx.json);
102767
+ writeLocalFailure(deps, NO_API_KEY6, ctx.json);
102606
102768
  return 1;
102607
102769
  }
102608
102770
  let solana;
@@ -103673,7 +103835,7 @@ candle profile use <name>`);
103673
103835
  init_args();
103674
103836
  init_deps();
103675
103837
  init_key_output();
103676
- var USAGE14 = 'Usage: candle quant ask "<text>" [--timeout <s>] [--json]';
103838
+ var USAGE15 = 'Usage: candle quant ask "<text>" [--timeout <s>] [--json]';
103677
103839
  var QUANT_ASK_DEFAULT_TIMEOUT_S = 300;
103678
103840
  var QUANT_ASK_MAX_TIMEOUT_S = 3600;
103679
103841
  var QUANT_ASK_SEND_TRIES = 3;
@@ -103842,7 +104004,7 @@ async function quantAsk(args, ctx) {
103842
104004
  const timeoutS = timeoutRaw === undefined ? QUANT_ASK_DEFAULT_TIMEOUT_S : Number(timeoutRaw);
103843
104005
  const text4 = "error" in parsed ? "" : parsed.positionals[0] ?? "";
103844
104006
  if ("error" in parsed || parsed.positionals.length !== 1 || !text4.trim() || !Number.isInteger(timeoutS) || timeoutS < 1 || timeoutS > QUANT_ASK_MAX_TIMEOUT_S) {
103845
- writeScreenUsageFailure(ctx.deps, USAGE14, ctx.json);
104007
+ writeScreenUsageFailure(ctx.deps, USAGE15, ctx.json);
103846
104008
  return 2;
103847
104009
  }
103848
104010
  const apiKey = await resolveApiKey(ctx.deps, ctx.profile);
@@ -104051,7 +104213,7 @@ init_client();
104051
104213
  init_human_output();
104052
104214
  init_presentation();
104053
104215
  init_render();
104054
- var USAGE15 = "Usage: candle scan <contract address> [--chain solana|hood] [--json]";
104216
+ var USAGE16 = "Usage: candle scan <contract address> [--chain solana|hood] [--json]";
104055
104217
  var OTHER_CHAIN = "Candle trades Solana and Hood today; that is not a Solana or Hood contract address.";
104056
104218
  var HOOD_ADDRESS = /^0x[0-9a-fA-F]{40}$/;
104057
104219
  var SOLANA_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
@@ -104151,20 +104313,20 @@ async function scan(args, ctx) {
104151
104313
  }
104152
104314
  const named = parsed.values["--chain"];
104153
104315
  if (parsed.positionals.length !== 1 || named !== undefined && named !== "solana" && named !== "hood") {
104154
- writeUsageFailure(ctx.deps, USAGE15, ctx.json);
104316
+ writeUsageFailure(ctx.deps, USAGE16, ctx.json);
104155
104317
  return 2;
104156
104318
  }
104157
104319
  const address = parsed.positionals[0].trim();
104158
104320
  const found = chainOfAddress(address);
104159
104321
  if (found === null) {
104160
104322
  writeUsageFailure(ctx.deps, `${OTHER_CHAIN}
104161
- ${USAGE15}`, ctx.json);
104323
+ ${USAGE16}`, ctx.json);
104162
104324
  return 2;
104163
104325
  }
104164
104326
  if (named !== undefined && named !== found) {
104165
104327
  const label = (c) => c === "hood" ? "Hood" : "Solana";
104166
104328
  writeUsageFailure(ctx.deps, `That address is a ${label(found)} contract, not a ${label(named)} one. Candle trades Solana and Hood today.
104167
- ${USAGE15}`, ctx.json);
104329
+ ${USAGE16}`, ctx.json);
104168
104330
  return 2;
104169
104331
  }
104170
104332
  const chain2 = found;
@@ -106042,7 +106204,7 @@ init_deps();
106042
106204
  init_human_output();
106043
106205
  init_render();
106044
106206
  var KINDS = ["trade", "signal", "callout"];
106045
- var USAGE16 = "Usage: candle token events <mint> [--chain solana|hood] [--kind trade|signal|callout] [--since <iso-8601>] [--limit <n>] [--before <cursor>] [--json]";
106207
+ var USAGE17 = "Usage: candle token events <mint> [--chain solana|hood] [--kind trade|signal|callout] [--since <iso-8601>] [--limit <n>] [--before <cursor>] [--json]";
106046
106208
  function readEventsBody(body) {
106047
106209
  if (body === null || typeof body !== "object" || !Array.isArray(body.events))
106048
106210
  return null;
@@ -106100,11 +106262,11 @@ async function tokenEvents(args, ctx) {
106100
106262
  const since = parsed.values["--since"];
106101
106263
  const before = parsed.values["--before"];
106102
106264
  if (parsed.positionals.length !== 1 || chain2 !== "solana" && chain2 !== "hood") {
106103
- writeUsageFailure(ctx.deps, USAGE16, ctx.json);
106265
+ writeUsageFailure(ctx.deps, USAGE17, ctx.json);
106104
106266
  return 2;
106105
106267
  }
106106
106268
  if (kind !== undefined && !KINDS.includes(kind)) {
106107
- writeUsageFailure(ctx.deps, USAGE16, ctx.json);
106269
+ writeUsageFailure(ctx.deps, USAGE17, ctx.json);
106108
106270
  return 2;
106109
106271
  }
106110
106272
  if (limit !== undefined && !/^[1-9][0-9]{0,3}$/.test(limit) || Number(limit) > 1000) {
@@ -106443,7 +106605,7 @@ init_render();
106443
106605
  init_solana_verify();
106444
106606
  init_trading();
106445
106607
  init_swap();
106446
- var USAGE17 = "Usage: candle transfer --to <address|wallet name|allowlist label|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
106608
+ var USAGE18 = "Usage: candle transfer --to <address|wallet name|allowlist label|vault> --asset <SOL|USDC|CNDL|ETH|USDG>|--mint <mint>|--token <0x...> --amount <decimal|max> [--wallet <name>] [--rpc-url <url>] [--yes] [--json]";
106447
106609
  var TRANSFER_ASSETS = [...Object.keys(BASES), ...Object.keys(HOOD_BASES)];
106448
106610
  var BASE58_ADDRESS = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
106449
106611
  async function readLinkedWallets(ctx, key) {
@@ -106588,7 +106750,7 @@ async function transfer(args, ctx) {
106588
106750
  const flags2 = parsed.values;
106589
106751
  const asset = flags2["--asset"]?.toUpperCase();
106590
106752
  if (parsed.positionals.length !== 0 || !flags2["--to"] || !flags2["--amount"] || [flags2["--asset"], flags2["--mint"], flags2["--token"]].filter(Boolean).length !== 1 || asset !== undefined && !TRANSFER_ASSETS.includes(asset) || flags2["--mint"] !== undefined && !BASE58_ADDRESS.test(flags2["--mint"]) || flags2["--token"] !== undefined && !checkEvmAddress(flags2["--token"]).ok) {
106591
- writeUsageFailure(ctx.deps, USAGE17, ctx.json);
106753
+ writeUsageFailure(ctx.deps, USAGE18, ctx.json);
106592
106754
  return 2;
106593
106755
  }
106594
106756
  const to = flags2["--to"];
@@ -109585,7 +109747,10 @@ async function createVault(request2, clock) {
109585
109747
  if (raw === null) {
109586
109748
  throw new VaultError("VAULT_WRITE_FAILED", `The vault was written to ${request2.path} but could not be read back.`);
109587
109749
  }
109588
- return unlockWithPassphrase(request2.path, raw, request2.passphrase, { notice: request2.notice });
109750
+ return unlockWithPassphrase(request2.path, raw, request2.passphrase, {
109751
+ notice: request2.notice,
109752
+ derivationLabel: request2.verificationLabel
109753
+ });
109589
109754
  }
109590
109755
 
109591
109756
  // src/commands/vault-init.ts
@@ -109659,8 +109824,8 @@ async function runPhraseCeremony(ctx, vault, opts = {}) {
109659
109824
  const { closeVault: closeVault2 } = await Promise.resolve().then(() => (init_store(), exports_store));
109660
109825
  closeVault2(reopened);
109661
109826
  }
109662
- notice(deps, "stdout", "danger", `The 24 words below re-derive every key this vault derives, on any BIP-39 wallet, with no passphrase and no server.
109663
- They do NOT restore any key imported from the Phase 1 TEE wallet store; the vault file plus a factor does that.
109827
+ notice(deps, "stdout", "warning", `The 24 words below re-derive every key this vault derives, on any BIP-39 wallet, with no passphrase and no server.
109828
+ They do NOT restore any key you imported from an older wallet store; the vault file plus a factor does that.
109664
109829
  Anyone who reads them can move every derived key's funds.
109665
109830
 
109666
109831
  `);
@@ -109688,6 +109853,9 @@ Anyone who reads them can move every derived key's funds.
109688
109853
  }
109689
109854
  deps.stdout.write(`
109690
109855
  `);
109856
+ await deps.promptLine("Press Enter when you have saved the 24 words offline; the screen will clear for the check: ");
109857
+ deps.stdout.write("\x1B[2J\x1B[H");
109858
+ notice(deps, "stdout", "warning", "The phrase is no longer on screen. Clear your terminal's scrollback: the words were rendered there and this CLI cannot remove them.");
109691
109859
  const positions = randomPositions(3, words.length);
109692
109860
  let confirmed = true;
109693
109861
  for (const position of positions) {
@@ -109696,8 +109864,6 @@ Anyone who reads them can move every derived key's funds.
109696
109864
  confirmed = false;
109697
109865
  }
109698
109866
  deps.stdout.write("\x1B[2J\x1B[H");
109699
- notice(deps, "stdout", "warning", `The phrase is no longer on screen. Clear your terminal's scrollback: the words were rendered there and this CLI cannot remove them.
109700
- `.trim());
109701
109867
  notice(deps, "stdout", "warning", `This vault now records that the recovery phrase was exported; that record never resets.
109702
109868
  `.trim());
109703
109869
  words.fill("");
@@ -109768,7 +109934,8 @@ async function vaultInit(args, ctx) {
109768
109934
  strength: strengthFor(own),
109769
109935
  rootEntropy,
109770
109936
  label: parsed.values["--label"],
109771
- notice: (line) => deps.stderr.write(line)
109937
+ notice: (line) => deps.stderr.write(line),
109938
+ verificationLabel: "Verifying the vault key"
109772
109939
  }, deps));
109773
109940
  try {
109774
109941
  if (ctx.json) {
@@ -109798,12 +109965,13 @@ async function vaultInit(args, ctx) {
109798
109965
  ["Verified", "file re-read, opened with your passphrase, root blob decrypted"],
109799
109966
  ["Create a key", "candle vault new-key --chain solana"]
109800
109967
  ]);
109801
- notice(deps, "stdout", "note", `
109968
+ if (deps.platform === "darwin")
109969
+ notice(deps, "stdout", "note", `
109802
109970
  ${APPLE_ACCOUNT_NOTICE}
109803
109971
  `.trim());
109804
109972
  if (countRecoverableFactors(vault.file.envelopes) === 1) {
109805
109973
  notice(deps, "stdout", "warning", `
109806
- This vault has exactly one recoverable factor: the passphrase. Lose it and no copy of this file can be opened, and the recovery phrase becomes the only route back. Add a second when you can: candle vault enroll security-key
109974
+ This vault has exactly one recoverable factor: the passphrase. Lose it and no copy of this file can be opened, and the recovery phrase becomes the only route back. To add a security key later, run this separate optional command: candle vault enroll security-key
109807
109975
  `.trim());
109808
109976
  }
109809
109977
  notice(deps, "stdout", "note", `
@@ -111358,7 +111526,7 @@ async function promoteEvmFresh(input, fromLabel) {
111358
111526
  addKeys: [blob]
111359
111527
  }, ctx.deps));
111360
111528
  scanStart = await appendScanStart(ctx, vault.path, address, height);
111361
- await confirmAddress(ctx, "vault", destination.address, `sweep this wallet home to ${destination.address}`);
111529
+ await confirmAddress(ctx, "vault", destination.address, `promote a new TEE wallet with pinned vault ${destination.address}`);
111362
111530
  if (toKey !== undefined) {
111363
111531
  notice(ctx.deps, "stderr", "note", `${[toKeyPlanLine(toKey), ...signerStaysLines(toKey), ...targetWarnings(toKey.target, "evm")].join(`
111364
111532
  `)}
@@ -111738,7 +111906,7 @@ async function promoteFresh(ctx, parsed, fromLabel, toKey) {
111738
111906
  },
111739
111907
  addKeys: [blob]
111740
111908
  }, ctx.deps));
111741
- await confirmAddress(ctx, "vault", destination.address, `sweep this wallet home to ${destination.address}`);
111909
+ await confirmAddress(ctx, "vault", destination.address, `promote a new TEE wallet with pinned vault ${destination.address}`);
111742
111910
  if (toKey !== undefined) {
111743
111911
  notice(ctx.deps, "stderr", "note", `${[toKeyPlanLine(toKey), ...signerStaysLines(toKey), ...targetWarnings(toKey.target, "solana")].join(`
111744
111912
  `)}
@@ -114249,8 +114417,8 @@ init_store();
114249
114417
  init_vault_support();
114250
114418
  var LIST_POINTER = "One line per key, with an optional SOL balance: candle vault list";
114251
114419
  var NO_VERIFIED_BACKUP_NOTE = `No backup of this vault has ever been verified from this machine. If this file is lost, only the 24-word recovery phrase can rebuild it, and it rebuilds derived keys only. Take one now:
114252
- candle vault backup --to <path>
114253
- on a Mac with iCloud Drive: candle vault backup --to icloud`;
114420
+ candle vault backup --to <path>`;
114421
+ var ICLOUD_BACKUP_LINE = "on a Mac with iCloud Drive: candle vault backup --to icloud";
114254
114422
  async function vaultStatus(args, ctx) {
114255
114423
  const parsed = parseArgs(args, {
114256
114424
  valueFlags: ["--keystore"],
@@ -114392,7 +114560,8 @@ async function vaultStatus(args, ctx) {
114392
114560
  notice(deps, "stdout", "note", "No vault.state.json beside this vault, so an older copy of it cannot be recognized on this machine.");
114393
114561
  }
114394
114562
  if (sidecar?.lastVerifiedBackupAt === undefined)
114395
- notice(deps, "stdout", "warning", NO_VERIFIED_BACKUP_NOTE);
114563
+ notice(deps, "stdout", "warning", deps.platform === "darwin" ? `${NO_VERIFIED_BACKUP_NOTE}
114564
+ ${ICLOUD_BACKUP_LINE}` : NO_VERIFIED_BACKUP_NOTE);
114396
114565
  if (legacyPresent) {
114397
114566
  notice(deps, "stdout", "warning", `A legacy wallets.enc exists at ${legacy}; this CLI does not read it.
114398
114567
  A key it holds that was imported is already a linked wallet, managed with: candle wallets
@@ -114698,7 +114867,7 @@ init_human_output();
114698
114867
  init_release();
114699
114868
  import { dirname as dirname13, join as join15 } from "node:path";
114700
114869
  init_key_output();
114701
- var USAGE18 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
114870
+ var USAGE19 = "Usage: candle verify <file> --bundle <path> [--identity <uri>] [--issuer <url>]";
114702
114871
  async function resolveIdentity(deps, bundlePath, flag) {
114703
114872
  if (flag)
114704
114873
  return { kind: "ok", uri: flag, provenance: "identity from --identity" };
@@ -114730,19 +114899,19 @@ async function verify(args, ctx) {
114730
114899
  });
114731
114900
  if ("error" in parsed) {
114732
114901
  writeScreenUsageFailure(deps, `${parsed.error}
114733
- ${USAGE18}`, json);
114902
+ ${USAGE19}`, json);
114734
114903
  return 2;
114735
114904
  }
114736
114905
  const file = parsed.positionals[0];
114737
114906
  if (parsed.positionals.length !== 1 || file === undefined) {
114738
114907
  writeScreenUsageFailure(deps, `verify takes exactly one file.
114739
- ${USAGE18}`, json);
114908
+ ${USAGE19}`, json);
114740
114909
  return 2;
114741
114910
  }
114742
114911
  const bundlePath = parsed.values["--bundle"];
114743
114912
  if (!bundlePath) {
114744
114913
  writeScreenUsageFailure(deps, `--bundle is required.
114745
- ${USAGE18}`, json);
114914
+ ${USAGE19}`, json);
114746
114915
  return 2;
114747
114916
  }
114748
114917
  const resolved = await resolveIdentity(deps, bundlePath, parsed.values["--identity"]);
@@ -114756,7 +114925,7 @@ ${USAGE18}`, json);
114756
114925
  }
114757
114926
  if (resolved.kind === "absent") {
114758
114927
  writeScreenUsageFailure(deps, `--identity is required: there is no latest.json beside ${bundlePath} to take the release version from.
114759
- ${USAGE18}`, json);
114928
+ ${USAGE19}`, json);
114760
114929
  return 2;
114761
114930
  }
114762
114931
  const identity4 = resolved.uri;
@@ -115015,7 +115184,7 @@ init_presentation();
115015
115184
  init_profiles();
115016
115185
  init_render();
115017
115186
  init_key_output();
115018
- var USAGE19 = "Usage: candle wallets authorize [--no-browser] [--no-wait] [--timeout <seconds>] [--json]";
115187
+ var USAGE20 = "Usage: candle wallets authorize [--no-browser] [--no-wait] [--timeout <seconds>] [--json]";
115019
115188
  var AUTHORIZE_WAIT_DEFAULT_SECONDS = 600;
115020
115189
  var AUTHORIZE_POLL_MS = 3000;
115021
115190
  function authorizeUrl(apiUrl, portalOrigin) {
@@ -115038,12 +115207,12 @@ async function walletsAuthorize(args, ctx) {
115038
115207
  const parsed = parseArgs(args, { booleanFlags: ["--no-browser", "--no-wait"], valueFlags: ["--timeout"] });
115039
115208
  if ("error" in parsed) {
115040
115209
  writeScreenUsageFailure(deps, `${parsed.error}
115041
- ${USAGE19}`, json);
115210
+ ${USAGE20}`, json);
115042
115211
  return 2;
115043
115212
  }
115044
115213
  if (parsed.positionals.length > 0) {
115045
115214
  writeScreenUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}
115046
- ${USAGE19}`, json);
115215
+ ${USAGE20}`, json);
115047
115216
  return 2;
115048
115217
  }
115049
115218
  let timeoutSeconds = AUTHORIZE_WAIT_DEFAULT_SECONDS;
@@ -115052,7 +115221,7 @@ ${USAGE19}`, json);
115052
115221
  const n = Number(rawTimeout);
115053
115222
  if (!Number.isInteger(n) || n <= 0) {
115054
115223
  writeScreenUsageFailure(deps, `--timeout must be a whole number of seconds above 0, got ${rawTimeout}
115055
- ${USAGE19}`, json);
115224
+ ${USAGE20}`, json);
115056
115225
  return 2;
115057
115226
  }
115058
115227
  timeoutSeconds = n;
@@ -115159,7 +115328,7 @@ init_solana_lite();
115159
115328
  init_trading();
115160
115329
  init_swap();
115161
115330
  import { randomUUID as randomUUID16 } from "node:crypto";
115162
- var USAGE20 = "Usage: candle wallet close-empty [--wallet embedded|<tee address|label>] [--keep <mint>]... [--client-trade-id <id>] [--preview] [--yes] [--json]";
115331
+ var USAGE21 = "Usage: candle wallet close-empty [--wallet embedded|<tee address|label>] [--keep <mint>]... [--client-trade-id <id>] [--preview] [--yes] [--json]";
115163
115332
  var BASE58_ADDRESS2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
115164
115333
  var LISTED_ROWS = 50;
115165
115334
  var TOKEN_IX_CLOSE_ACCOUNT3 = 9;
@@ -115434,7 +115603,7 @@ async function closeTee(ctx, key, wallet3, name2, run2) {
115434
115603
  async function walletsCloseEmpty(args, ctx) {
115435
115604
  const lifted = splitKeep(args);
115436
115605
  if ("error" in lifted) {
115437
- writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE20}`, ctx.json);
115606
+ writeUsageFailure(ctx.deps, `${lifted.error}. ${USAGE21}`, ctx.json);
115438
115607
  return 2;
115439
115608
  }
115440
115609
  const parsed = parseArgs(lifted.rest, {
@@ -115448,7 +115617,7 @@ async function walletsCloseEmpty(args, ctx) {
115448
115617
  const named = parsed.values["--client-trade-id"];
115449
115618
  const id = named ?? `close-${randomUUID16()}`;
115450
115619
  if (parsed.positionals.length !== 0 || !validClientId(id) || !lifted.keep.every((m) => BASE58_ADDRESS2.test(m))) {
115451
- writeUsageFailure(ctx.deps, USAGE20, ctx.json);
115620
+ writeUsageFailure(ctx.deps, USAGE21, ctx.json);
115452
115621
  return 2;
115453
115622
  }
115454
115623
  const run2 = {
@@ -115956,6 +116125,7 @@ var COMMANDS = {
115956
116125
  access: keysAccess,
115957
116126
  update: keysUpdate,
115958
116127
  limit: keysLimit,
116128
+ limits: keysLimits,
115959
116129
  self: keysSelf,
115960
116130
  rotate: keysRotate,
115961
116131
  revoke: keysRevoke,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.13.16",
3
+ "version": "0.13.17",
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": {