@candledottv/cli 0.13.17 → 0.13.18

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 +7 -4
  2. package/dist/index.js +280 -80
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -116,6 +116,7 @@ chosen words from your saved copy. Clear your terminal's scrollback too: the CLI
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. |
119
+ | `candle wallets allowlist [--all]` | Print the account's withdrawal allowlist: each approved address's id, chain, address and label, the same rows `candle transfer --to <allowlist label>` resolves by. `--all` adds removed rows. Approving an address is the owner's, in the agent console's Wallets tab; an API key cannot approve. An acting key can revoke an address; a read-only key cannot. |
119
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`. |
120
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. |
121
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. |
@@ -142,7 +143,7 @@ chosen words from your saved copy. Clear your terminal's scrollback too: the CLI
142
143
  | `candle launch status <mint-or-client-launch-id> [--wait]` | One launch state, `build`, `signed`, `confirmed`, `trading`, `bonded` or `migrated`, for a mint or a `clientLaunchId`. Reads `GET /api/v1/launch/status/{id}` (`launch:read`) and reports the market's own `lifecycle` and `migration` fields. `--wait` follows it until the token is trading, the launch has failed, or ten minutes pass. Nothing is sent. |
143
144
  | `candle image upload <path>` | Uploads a PNG, JPEG, GIF or WebP file, at most 10MB, and prints the `https://` URL Candle hosts it at, ready to be a launch's `imageUrl`. The type is read from the file's first bytes. The file becomes public and permanent. Needs `launch:write`; spends nothing and does not count against the launch cap. `--json` adds the content type and the size. |
144
145
  | `candle pnl [--profile <name>]` | P&L: realized net, fees, unrealized and total, then open positions, the same figures the console shows. Without `--profile`, the account's books (every profile, the web app and the CLI, one ledger; needs the Read scope), with a By agent table (each key, then Manual, then the account total). With `--profile <name>`, that profile's key's share of the same P&L: realized on its sales and the open positions that belong to it, with a row per wallet that sums to the total. Each wallet is its own average-cost pool, and a recorded move between the account's wallets carries its cost. Both show a Solana block, a Hood block, then the combined figure, the closed positions as money made, minus money lost, plus partial sells, and a `CHAIN` column (and `WALLET`, where the API names it) on positions. Unpriced positions are never valued at zero; dust (worth under one cent) is marked and still counted. A bridge between Solana and Hood is not a trade, and bridge and Relay costs are not netted. DAMM v2 LP positions are included: realized (withdrawals against the cost basis at the add, plus claimed fees), unrealized (open positions at their share of the pool plus unclaimed fees, against that basis) and vs holding (what the deposited tokens would be worth held); `Total` stays the console's token figure and `Total with LP` covers tokens and LP. |
145
- | `candle portfolio [<filter>] [--chain solana\|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--no-perps]` | Vault, TEE and embedded wallets on Solana and Hood in one table with chain, amount, price and value, subtotals per group and per chain, and a total. Solana vault and external balances are read over your own Solana RPC; Candle is sent only the mints, for prices. EVM vault keys are read over your own Hood RPC (`--evm-rpc-url`, else `CANDLE_EVM_RPC_URL`, else the built-in Hood RPC) for ETH and USDG only, priced by asset, never by address; an RPC on another chain is refused. An unpriced Hood token shows why (`no-market-row`, `stale-mark`, ...). `--chain` shows one chain. Each TEE wallet's DAMM v2 LP positions follow the tokens, valued by Candle at the same marks, and count in the total; one whose pool could not be read is shown as not read and the total says it is partial. A Hyperliquid section follows (each EVM TEE wallet's account, read from Hyperliquid directly, never in the total); `--no-perps` skips it. |
146
+ | `candle portfolio [<filter>] [--chain solana\|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--no-perps] [--no-vault]` | Vault, TEE and embedded wallets on Solana and Hood in one table with chain, amount, price and value, subtotals per group and per chain, and a total. Solana vault and external balances are read over your own Solana RPC; Candle is sent only the mints, for prices. EVM vault keys are read over your own Hood RPC (`--evm-rpc-url`, else `CANDLE_EVM_RPC_URL`, else the built-in Hood RPC) for ETH and USDG only, priced by asset, never by address; an RPC on another chain is refused. An unpriced Hood token shows why (`no-market-row`, `stale-mark`, ...). `--chain` shows one chain. Each TEE wallet's DAMM v2 LP positions follow the tokens, valued by Candle at the same marks, and count in the total; one whose pool could not be read is shown as not read and the total says it is partial. A Hyperliquid section follows (each EVM TEE wallet's account, read from Hyperliquid directly, never in the total); `--no-perps` skips it. It needs no terminal: without one, `--factor touch-id` or `passkey` still unlocks the vault, and otherwise the vault row says not read and the rest is shown, but the total is partial (exit 3); `--no-vault` leaves the vault out on purpose and may exit 0. |
146
147
  | `candle scan <contract address> [--chain solana\|hood]` | The token card for one contract, as a Quant group gets it: market, all-time high since launch, bundles, snipers, insiders, fresh wallets, wallet age, tracked wallets in the last hour, the same-name leader, forensics coverage and the Candle page, with the source of each figure. Facts only; a field with no source prints a dash. The chain is read from the address unless `--chain` names it. No key needed and none sent. Served only where `TOKEN_CARD_ENABLED` is on, and `TOKEN_CARD_DISABLED` elsewhere. |
147
148
  | `candle token clusters <mint> [--chain solana\|hood]` | Optional third-party Solana cluster estimates, with fetched time and a linked-wallet caveat. Unavailable analysis prints a reason and exits 0; `--json` emits the availability union. No key. |
148
149
  | `candle token edit <mint> [--chain solana\|hood] [--description <text>] [--website <url>] [--x <url>] [--telegram <url>] [--discord <url>] [--clear <field>]...` | As the token's creator, set Candle's copy of its description and links; `--clear` removes one. Name, symbol and image cannot be changed, and nothing is written on chain. Needs a `launch:write` key from the launching account. Served only where `TOKEN_METADATA_EDIT_ENABLED` is on, and `METADATA_EDIT_DISABLED` elsewhere. |
@@ -360,9 +361,11 @@ address is shortened (`FfU8…8pPD`; `--full` prints it whole) and an `https://`
360
361
  `auth login` and `doctor`, which carry `profile` and `account`; `auth status` and `doctor` also
361
362
  carry `cachedAccount`, the account the profile recorded, whenever a profile is resolved; scripts
362
363
  get identity from `auth status --json`. The account is cached
363
- at login from the API. Where the line is printed from that cache and `CANDLE_API_KEY` or
364
- `CANDLE_DEVICE_TOKEN` is overriding the stored credential, it reads
365
- `Account unknown (CANDLE_API_KEY override)` rather than naming an account that credential was
364
+ at login from the API. Where the line is printed and `CANDLE_API_KEY` or `CANDLE_DEVICE_TOKEN` is
365
+ overriding the stored credential, it reads `Account <address> (CANDLE_API_KEY override)` when the
366
+ overriding key could be asked which account it belongs to, and
367
+ `Account unknown (CANDLE_API_KEY override)` when it could not (offline, revoked, or a
368
+ device-token-only override), rather than naming an account that credential was
366
369
  never checked against; `auth status` and `setup` look the account up live and print what they
367
370
  get, and `auth status` and `doctor` name the account the profile recorded beside it when the two
368
371
  differ (not under an env credential override, where the live answer is not the profile's key's).
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.17";
535
+ var CLI_VERSION = "0.13.18";
536
536
 
537
537
  // src/presentation.ts
538
538
  function terminalStyle(deps, stream, machine = false) {
@@ -11163,6 +11163,25 @@ var init_solana_alt = __esm(() => {
11163
11163
  ];
11164
11164
  });
11165
11165
 
11166
+ // src/account.ts
11167
+ async function fetchAccount(deps, apiUrl, apiKey) {
11168
+ const identity = await apiRequest("/api/v1/agent/wallets/embedded", {
11169
+ auth: "key",
11170
+ credentials: { apiKey },
11171
+ apiUrl,
11172
+ fetch: deps.fetch,
11173
+ env: deps.env
11174
+ });
11175
+ const body = identity.ok ? identity.body : undefined;
11176
+ const account = body?.account;
11177
+ if (account)
11178
+ return { account, ...body?.username ? { username: body.username } : {} };
11179
+ return { failure: identity.ok ? "no account in the response" : identity.message };
11180
+ }
11181
+ var init_account = __esm(() => {
11182
+ init_client();
11183
+ });
11184
+
11166
11185
  // src/profiles.ts
11167
11186
  function assertOrdinaryProfile(config, profile) {
11168
11187
  if (profile !== undefined && config.profiles?.[profile]?.apiKeyItem !== undefined) {
@@ -11297,7 +11316,7 @@ function credentialEnvOverrides(env) {
11297
11316
  }
11298
11317
  function identityLine(profile, account, apiUrl, overrides, username, options = {}) {
11299
11318
  const address = account === undefined ? undefined : shortId(account, options.full);
11300
- const shown = overrides?.length ? `unknown (${overrides.join(", ")} override)` : username && address ? `${username} (${address})` : address ?? "unknown";
11319
+ const shown = overrides?.length ? address ? `${address} (${overrides.join(", ")} override)` : `unknown (${overrides.join(", ")} override)` : username && address ? `${username} (${address})` : address ?? "unknown";
11301
11320
  const name = profile ?? "none";
11302
11321
  const host = apiUrl.replace(/^https:\/\//i, "").replace(/\/+$/, "");
11303
11322
  const sep = options.style ? symbols(options.style).note : "·";
@@ -11308,7 +11327,20 @@ async function printIdentity(ctx) {
11308
11327
  return;
11309
11328
  const config = await ctx.deps.readConfig();
11310
11329
  const fields2 = effectiveProfileFields(config, ctx.profile);
11311
- const line = identityLine(ctx.profile, fields2.account, ctx.apiUrl, credentialEnvOverrides(ctx.deps.env), fields2.username, identityStyle(ctx.deps, "stdout"));
11330
+ const overrides = credentialEnvOverrides(ctx.deps.env);
11331
+ let account = fields2.account;
11332
+ let username = fields2.username;
11333
+ if (overrides.length > 0) {
11334
+ account = undefined;
11335
+ username = undefined;
11336
+ const key = ctx.deps.env.CANDLE_API_KEY?.trim();
11337
+ if (key) {
11338
+ const lookup = await fetchAccount(ctx.deps, ctx.apiUrl, key);
11339
+ if (lookup.account)
11340
+ account = lookup.account;
11341
+ }
11342
+ }
11343
+ const line = identityLine(ctx.profile, account, ctx.apiUrl, overrides.length > 0 ? overrides : undefined, username, identityStyle(ctx.deps, "stdout"));
11312
11344
  ctx.deps.stdout.write(`${line}
11313
11345
 
11314
11346
  `);
@@ -11352,6 +11384,7 @@ function profileTable(config, now) {
11352
11384
  }
11353
11385
  var ProfileFleetOnlyError, PRE_PROFILE_FIELDS, API_KEY_PREFIXES, API_KEY_RANDOM_LENGTH = 43, INVALID_RPC_HOST = "invalid (fix with profile set)";
11354
11386
  var init_profiles = __esm(() => {
11387
+ init_account();
11355
11388
  init_presentation();
11356
11389
  init_render();
11357
11390
  ProfileFleetOnlyError = class ProfileFleetOnlyError extends Error {
@@ -28989,13 +29022,13 @@ __export(exports_vault_support, {
28989
29022
  runVaultCommand: () => runVaultCommand,
28990
29023
  requireVaultRaw: () => requireVaultRaw,
28991
29024
  requireTty: () => requireTty,
28992
- requirePromptStreams: () => requirePromptStreams,
28993
29025
  refusePromptsWithoutTerminal: () => refusePromptsWithoutTerminal,
28994
29026
  refuseEnvPassphrase: () => refuseEnvPassphrase,
28995
29027
  openWithTypedPassphrase: () => openWithTypedPassphrase,
28996
29028
  nonDefaultVaultFooter: () => nonDefaultVaultFooter,
28997
29029
  missingVault: () => missingVault,
28998
29030
  menuSafeText: () => menuSafeText,
29031
+ isNoTerminalRefusal: () => isNoTerminalRefusal,
28999
29032
  groupAddress: () => groupAddress,
29000
29033
  findExternalEntry: () => findExternalEntry,
29001
29034
  factorMenu: () => factorMenu,
@@ -29039,22 +29072,23 @@ function requireTty(ctx, what) {
29039
29072
  }));
29040
29073
  return false;
29041
29074
  }
29042
- function requirePromptStreams(ctx, what) {
29043
- if (ctx.deps.isTTY.stdin && ctx.deps.isTTY.stderr)
29044
- return true;
29045
- writeVaultFailure(ctx, new VaultError("VAULT_UNLOCK_FAILED", `${what} needs a terminal for the passphrase prompt: standard input and standard error must both be a terminal. Standard output may be redirected; that is where the listing goes.`));
29046
- return false;
29047
- }
29048
29075
  function refusePromptsWithoutTerminal(ctx, what) {
29049
29076
  if (ctx.deps.isTTY.stdin && ctx.deps.isTTY.stderr)
29050
29077
  return ctx;
29051
29078
  const refuse2 = async () => {
29052
- throw new VaultError("VAULT_UNLOCK_FAILED", `${what} needs a terminal here: this unlock reads a vault passphrase, a security key PIN or a choice from a prompt, and standard input and standard error must both be a terminal for that. Standard output may be redirected.`, {
29079
+ throw tagNoTerminal(new VaultError("VAULT_UNLOCK_FAILED", `${what} needs a terminal here: this unlock reads a vault passphrase, a security key PIN or a choice from a prompt, and standard input and standard error must both be a terminal for that. Standard output may be redirected.`, {
29053
29080
  suggestion: "Run it from a terminal, or pass --factor touch-id or --factor passkey to unlock with a factor that reads no prompt. No environment variable or flag supplies a passphrase or PIN."
29054
- });
29081
+ }));
29055
29082
  };
29056
29083
  return { ...ctx, deps: { ...ctx.deps, promptSecret: refuse2, promptLine: refuse2 } };
29057
29084
  }
29085
+ function tagNoTerminal(error) {
29086
+ noTerminalRefusals.add(error);
29087
+ return error;
29088
+ }
29089
+ function isNoTerminalRefusal(error) {
29090
+ return error instanceof VaultError && noTerminalRefusals.has(error);
29091
+ }
29058
29092
  function vaultPathFor(ctx, parsed) {
29059
29093
  const flag = parsed.values["--keystore"];
29060
29094
  if (flag !== undefined)
@@ -29674,7 +29708,7 @@ function takeRepeatedFlag(args, flag) {
29674
29708
  }
29675
29709
  return { values, rest };
29676
29710
  }
29677
- var PASSPHRASE_ONLY_PREFIX = "Passphrase only: ";
29711
+ var noTerminalRefusals, PASSPHRASE_ONLY_PREFIX = "Passphrase only: ";
29678
29712
  var init_vault_support = __esm(() => {
29679
29713
  init_args();
29680
29714
  init_presentation();
@@ -29690,6 +29724,7 @@ var init_vault_support = __esm(() => {
29690
29724
  init_platform();
29691
29725
  init_sidecar();
29692
29726
  init_store();
29727
+ noTerminalRefusals = new WeakSet;
29693
29728
  });
29694
29729
 
29695
29730
  // src/vault/ed25519.ts
@@ -38566,7 +38601,7 @@ var init_token_clusters = __esm(() => {
38566
38601
  });
38567
38602
 
38568
38603
  // ../mcp/src/version.ts
38569
- var SERVER_VERSION = "0.10.29";
38604
+ var SERVER_VERSION = "0.10.30";
38570
38605
 
38571
38606
  // ../mcp/src/update-notice.ts
38572
38607
  function newer(a, b) {
@@ -40424,6 +40459,12 @@ async function executeTrade(args, cfg, doFetch) {
40424
40459
  let amountRaw;
40425
40460
  let resolved = {};
40426
40461
  let paper = isPaperFlag(args.paper);
40462
+ const deferExecution = isPaperFlag(args.deferExecution);
40463
+ if (deferExecution && paper) {
40464
+ return errText("deferExecution cannot be combined with paper; a paper trade records a quote and never executes", {
40465
+ clientTradeId
40466
+ });
40467
+ }
40427
40468
  try {
40428
40469
  if (args.side === "buy") {
40429
40470
  const amount = args.amount;
@@ -40532,6 +40573,9 @@ async function executeTrade(args, cfg, doFetch) {
40532
40573
  } catch (err) {
40533
40574
  return errText(err instanceof Error ? err.message : String(err), { clientTradeId });
40534
40575
  }
40576
+ if (deferExecution && paper) {
40577
+ return errText("deferExecution needs a live holding, and this sell resolved to the paper book (the wallet holds none of the mint); a paper trade never executes", { clientTradeId });
40578
+ }
40535
40579
  const posted = await postJson(`${base(cfg)}/api/v1/trade/agent/build`, apiKey, {
40536
40580
  clientTradeId,
40537
40581
  mint: args.mint,
@@ -40541,7 +40585,8 @@ async function executeTrade(args, cfg, doFetch) {
40541
40585
  payer: { type: "main" },
40542
40586
  ...args.quoteAsset !== undefined ? { quoteAsset: args.quoteAsset } : {},
40543
40587
  ...args.maxSlippageBps !== undefined ? { maxSlippageBps: args.maxSlippageBps } : {},
40544
- ...paper ? { paper: true } : {}
40588
+ ...paper ? { paper: true } : {},
40589
+ ...deferExecution ? { deferExecution: true } : {}
40545
40590
  }, doFetch);
40546
40591
  if ("thrown" in posted)
40547
40592
  return transportError("clientTradeId", clientTradeId, posted.thrown);
@@ -45166,6 +45211,13 @@ function buildRequest(name, args, cfg) {
45166
45211
  const path = kind === "launch" ? `/api/v1/launch/headless/jobs/${id}` : kind === "swap" ? `/api/v1/agent/swap/by-client-id/${id}` : `/api/v1/trade/agent/jobs/${id}`;
45167
45212
  return { url: `${base2}${path}`, init: { method: "GET", headers: jsonHeaders(apiKey) } };
45168
45213
  }
45214
+ case "candle_trade_execute": {
45215
+ const { clientTradeId } = args;
45216
+ return {
45217
+ url: `${base2}/api/v1/trade/agent/execute`,
45218
+ init: { method: "POST", headers: jsonHeaders(requireApiKey2(cfg)), body: JSON.stringify({ clientTradeId }) }
45219
+ };
45220
+ }
45169
45221
  case "candle_get_staking": {
45170
45222
  return { url: `${base2}/api/v1/agent/staking`, init: { method: "GET", headers: jsonHeaders(requireApiKey2(cfg)) } };
45171
45223
  }
@@ -45732,7 +45784,7 @@ function registerToolSubset(server, options) {
45732
45784
  }, async (args) => callAndRelay("candle_get_profile_pnl", args, getConfig(), doFetch));
45733
45785
  register("candle_get_profile_trades", {
45734
45786
  title: "Read an agent profile's trade history",
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.",
45787
+ 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'. The profile's swaps are listed among the trades by " + "time, each with kind 'swap', rail ('swap' for candle_swap, 'build' for the CLI's build and " + "submit), fromAsset, toAsset and amountRaw; a candle_swap made without a clientSwapId is not " + "recorded, so it is not listed. Reads only; moves nothing.",
45736
45788
  inputSchema: profileTradesShape
45737
45789
  }, async (args) => callAndRelay("candle_get_profile_trades", args, getConfig(), doFetch));
45738
45790
  register("candle_get_paper_inventory", {
@@ -45830,6 +45882,13 @@ function registerToolSubset(server, options) {
45830
45882
  const result = await executeTrade(args, getConfig(), doFetch);
45831
45883
  return { content: [{ type: "text", text: result.text }], ...result.isError ? { isError: true } : {} };
45832
45884
  });
45885
+ register("candle_trade_execute", {
45886
+ title: "Execute a deferred trade",
45887
+ description: "Execute a trade you built with candle_trade and deferExecution: true. MOVES REAL FUNDS: Candle signs " + "the plan it kept back from the build with the account's own delegation and broadcasts it. Takes the " + "clientTradeId and nothing else, so nothing you send can change what gets signed; to change the trade, " + `build a new one.
45888
+
45889
+ ` + "Exactly one execution per build. Calling again never broadcasts a second time: it returns the stored " + "result once the first has confirmed, and a 409 IDEMPOTENCY_CONFLICT while the outcome is still " + "unknown (read candle_get_operation with the clientTradeId, kind trade, before doing anything else). " + "More than 30 minutes after the build it is 409 QUOTE_EXPIRED: build again for a fresh quote. A " + "trade built without deferExecution has already executed and is refused here.",
45890
+ inputSchema: tradeExecuteShape
45891
+ }, async (args) => callAndRelay("candle_trade_execute", args, getConfig(), doFetch));
45833
45892
  register("candle_atomic_launch_dry_run", {
45834
45893
  title: "Dry-run an atomic Solana launch",
45835
45894
  description: "Check a Solana launch bundled with first buys and price each buy, without building anything. Runs the " + "same checks as the atomic build, in the same order (payers, Believer check, fee and spend limits, the " + "key's transaction cap), then returns the launch preview (curveTerms) and one fill per first buy: the " + "tokens it would receive and the floor the build would sign it at. Reads only; moves nothing, reserves " + "nothing, and the clientLaunchId stays unused. It does not check balances, the daily cap, or the " + "transaction's size, so the real launch can still refuse. Needs launch:write, swap:write and Pro or Max.",
@@ -46038,7 +46097,7 @@ async function registerEligibleProfileTool(server, env = process.env, doFetch =
46038
46097
  }
46039
46098
  return eligible;
46040
46099
  }
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;
46100
+ 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, tradeExecuteShape, tokenImageUrl, tokenBannerUrl, localImageShape, localLaunchTokenShape, localLaunchAndSeedShape, IMAGE_PATH_DISCLOSURE, transferShape, sweepShape, PROFILE_PNL_HYPERLIQUID;
46042
46101
  var init_tools = __esm(() => {
46043
46102
  init_zod();
46044
46103
  init_client2();
@@ -46103,6 +46162,7 @@ var init_tools = __esm(() => {
46103
46162
  "candle_get_agent_positions",
46104
46163
  "candle_get_plans",
46105
46164
  "candle_trade",
46165
+ "candle_trade_execute",
46106
46166
  "candle_launch_and_seed",
46107
46167
  "candle_atomic_launch_dry_run",
46108
46168
  "candle_swap",
@@ -46324,7 +46384,11 @@ var init_tools = __esm(() => {
46324
46384
  quoteAsset: exports_external.string().optional().describe('What the wallet spends on a buy or receives on a sell: "sol", "usdc" or "cndl" on Solana, ' + '"eth" or "usdg" on Hood. Echoing the `quoteAsset` a quote (POST /api/v1/trade/agent/quote) returned is safe. On Solana it applies only ' + "to an arbitrary mint Candle never launched (a buy or sell works on any plan) and is ignored for a Candle token, " + "whose quote comes from the token itself. On Hood it is the settlement asset of a DEX " + "trade; a USDG buy adds an approval transaction an ETH buy does not. It is not the route: " + "the cheapest path to the asset is chosen separately. Defaults to sol / ETH settlement."),
46325
46385
  maxSlippageBps: exports_external.number().int().min(0).max(5000).optional().describe("Slippage bound in bps, 0-5000 (50%). Server defaults to 100 (1%)"),
46326
46386
  clientTradeId: exports_external.string().optional().describe("Idempotency key. Auto-generated when omitted and echoed in the result. Retrying with the " + "SAME id is safe (idempotent replay); a new id is a SECOND trade."),
46327
- paper: exports_external.preprocess((value) => value === "true" || value === 1 || value === "1" ? true : value === "false" || value === 0 || value === "0" ? false : value, exports_external.boolean().optional().describe("Rehearse instead of trading. The request passes every admission rule a live trade passes " + "-- the same planner, spend gate, key cap and loss limits -- and records the quote, but " + "nothing is ever broadcast and no funds move. Use it to check that a strategy is admitted " + "before risking anything on it. A paper fill is optimistic by construction: it books the " + "quoted price, so the gap between a paper arm and a live one IS the execution cost. " + "A sell of a mint this key already paper-bought also closes that paper book when the " + "live wallet is empty, even if this flag is omitted."))
46387
+ paper: exports_external.preprocess((value) => value === "true" || value === 1 || value === "1" ? true : value === "false" || value === 0 || value === "0" ? false : value, exports_external.boolean().optional().describe("Rehearse instead of trading. The request passes every admission rule a live trade passes " + "-- the same planner, spend gate, key cap and loss limits -- and records the quote, but " + "nothing is ever broadcast and no funds move. Use it to check that a strategy is admitted " + "before risking anything on it. A paper fill is optimistic by construction: it books the " + "quoted price, so the gap between a paper arm and a live one IS the execution cost. " + "A sell of a mint this key already paper-bought also closes that paper book when the " + "live wallet is empty, even if this flag is omitted.")),
46388
+ deferExecution: exports_external.preprocess((value) => value === "true" || value === 1 || value === "1" ? true : value === "false" || value === 0 || value === "0" ? false : value, exports_external.boolean().optional().describe("Quote first, spend later. The trade is built and the result carries the quote with " + 'status "built"; NOTHING is signed or spent until candle_trade_execute is called with the same ' + "clientTradeId. The build holds for 30 minutes; executing after that is QUOTE_EXPIRED and you " + "build again for a fresh quote. Rebuilding with the same clientTradeId re-quotes it, until it has " + "been sent to candle_trade_execute. There is no cancel: let an unwanted build expire. Cannot be " + "combined with `paper`."))
46389
+ };
46390
+ tradeExecuteShape = {
46391
+ clientTradeId: exports_external.string().min(1).describe("The clientTradeId of a candle_trade built with deferExecution: true. Nothing else is accepted.")
46328
46392
  };
46329
46393
  tokenImageUrl = launchSchemaField(launchTokenShape, "imageUrl");
46330
46394
  tokenBannerUrl = launchSchemaField(launchTokenShape, "bannerUrl");
@@ -74149,29 +74213,11 @@ ${USAGE3}` : USAGE3, ctx.json);
74149
74213
  }
74150
74214
 
74151
74215
  // src/commands/auth.ts
74152
- import { homedir as homedir3 } from "node:os";
74153
- import { join as join9 } from "node:path";
74154
-
74155
- // src/account.ts
74156
- init_client();
74157
- async function fetchAccount(deps, apiUrl, apiKey) {
74158
- const identity = await apiRequest("/api/v1/agent/wallets/embedded", {
74159
- auth: "key",
74160
- credentials: { apiKey },
74161
- apiUrl,
74162
- fetch: deps.fetch,
74163
- env: deps.env
74164
- });
74165
- const body = identity.ok ? identity.body : undefined;
74166
- const account = body?.account;
74167
- if (account)
74168
- return { account, ...body?.username ? { username: body.username } : {} };
74169
- return { failure: identity.ok ? "no account in the response" : identity.message };
74170
- }
74171
-
74172
- // src/commands/auth.ts
74216
+ init_account();
74173
74217
  init_agent_key_access();
74174
74218
  init_args();
74219
+ import { homedir as homedir3 } from "node:os";
74220
+ import { join as join9 } from "node:path";
74175
74221
 
74176
74222
  // src/checks.ts
74177
74223
  init_client();
@@ -76567,9 +76613,9 @@ var HELP = {
76567
76613
  group: "Account",
76568
76614
  intro: "Holdings, prices and value of every wallet",
76569
76615
  summary: "Every wallet's holdings, prices and value on Solana and Hood (vault read over your own RPCs)",
76570
- description: "Vault, TEE and embedded wallets on Solana and Hood, each token with its chain, amount, price and value, then subtotals per group and per chain and one total. TEE and embedded balances, on both chains, come from Candle. Solana vault and external wallets are read over your own Solana RPC, and Candle is sent only the mints they hold, for prices. EVM vault keys are read over your own EVM RPC for ETH and USDG only (the CLI cannot list other tokens), which must be Hood's (chain id 4663); any other chain is refused. Their prices come from Candle by asset (ETH, USDG), never by address. Unpriced tokens are shown as unpriced with the reason (no-market-row, stale-mark, unusable-price, source-unavailable) and left out of the total. A wallet that could not be read in full is marked not read, the footer names its chain, and the exit is 3. Where Candle serves LP, each Solana TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial. A bridge moves value between the chain subtotals; it is not a trade. With a filter, only the wallets whose label or address contains it are read and shown, the subtotals and total are for those wallets, and the first line says how many matched. When Candle listed only part of the account's wallets, the result stays partial even if every wallet shown was read, and no match says so. After the totals, a Hyperliquid section (live from Hyperliquid, not in the ledger, and never in the total) lists each EVM TEE wallet's Hyperliquid account value, unrealized P&L, positions and risk at the default thresholds, read from Hyperliquid's public API on this machine; a failed read there is not read and the exit is 3. --chain solana and --no-perps read nothing from Hyperliquid.",
76616
+ description: "Vault, TEE and embedded wallets on Solana and Hood, each token with its chain, amount, price and value, then subtotals per group and per chain and one total. TEE and embedded balances, on both chains, come from Candle. Solana vault and external wallets are read over your own Solana RPC, and Candle is sent only the mints they hold, for prices. EVM vault keys are read over your own EVM RPC for ETH and USDG only (the CLI cannot list other tokens), which must be Hood's (chain id 4663); any other chain is refused. Their prices come from Candle by asset (ETH, USDG), never by address. Unpriced tokens are shown as unpriced with the reason (no-market-row, stale-mark, unusable-price, source-unavailable) and left out of the total. A wallet that could not be read in full is marked not read, the footer names its chain, and the exit is 3. Where Candle serves LP, each Solana TEE wallet's DAMM v2 positions follow the tokens (share of the pool plus unclaimed fees, valued by Candle at the same marks) and count in the total; a position whose pool could not be read is shown as not read and the total says it is partial. A bridge moves value between the chain subtotals; it is not a trade. With a filter, only the wallets whose label or address contains it are read and shown, the subtotals and total are for those wallets, and the first line says how many matched. When Candle listed only part of the account's wallets, the result stays partial even if every wallet shown was read, and no match says so. After the totals, a Hyperliquid section (live from Hyperliquid, not in the ledger, and never in the total) lists each EVM TEE wallet's Hyperliquid account value, unrealized P&L, positions and risk at the default thresholds, read from Hyperliquid's public API on this machine; a failed read there is not read and the exit is 3. --chain solana and --no-perps read nothing from Hyperliquid. The vault is read only for its addresses, so the command needs no terminal: without one, a factor that asks nothing (--factor touch-id or passkey) still unlocks it, and otherwise the vault row says not read and the other groups are shown, but the result is partial: the total says so and the exit is 3. --no-vault leaves the vault out on purpose, and then the result can be complete (exit 0).",
76571
76617
  usage: [
76572
- "candle portfolio [<filter>] [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--no-perps] [--json]"
76618
+ "candle portfolio [<filter>] [--chain solana|hood] [--rpc-url <url>] [--evm-rpc-url <url>] [--no-perps] [--no-vault] [--json]"
76573
76619
  ],
76574
76620
  rows: [
76575
76621
  {
@@ -76591,6 +76637,10 @@ var HELP = {
76591
76637
  invocation: "--no-perps",
76592
76638
  description: "Skip the Hyperliquid section: send no EVM TEE address to Hyperliquid's public API"
76593
76639
  },
76640
+ {
76641
+ invocation: "--no-vault",
76642
+ description: "Leave the vault out: no unlock, and no vault address is sent to your RPC"
76643
+ },
76594
76644
  KEYSTORE_FLAG
76595
76645
  ],
76596
76646
  examples: [
@@ -77043,6 +77093,10 @@ var HELP = {
77043
77093
  description: "Give agents access to the account's embedded wallets: opens the Agents page's grant step in your browser (no key is created) and, with an API key, waits until it lands"
77044
77094
  },
77045
77095
  { invocation: "revoke <wallet-id>", description: "Revoke a linked wallet" },
77096
+ {
77097
+ invocation: "allowlist [--all]",
77098
+ description: "The account's withdrawal allowlist (the destinations candle transfer --to <label> resolves by). Any key reads it; approving an address is the owner's, in the agent console's Wallets tab (an API key cannot approve); an acting key can revoke one, a read-only key cannot. --all adds removed rows"
77099
+ },
77046
77100
  {
77047
77101
  invocation: "trust <label|address|id|prefix*>...",
77048
77102
  description: "Mark linked wallets yours, so agents can move funds into them (owner only; typed confirm)"
@@ -77076,6 +77130,7 @@ var HELP = {
77076
77130
  "candle wallet authorize",
77077
77131
  "candle wallet import --chain solana --key-file ./signer.json",
77078
77132
  "candle wallet revoke wal_123",
77133
+ "candle wallet allowlist",
77079
77134
  "candle wallet trust 'tr-*' 'dest-*'",
77080
77135
  "candle wallet allow-launch launcher-1",
77081
77136
  "candle wallet close-empty --keep EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
@@ -77956,6 +78011,7 @@ var TOOL_CLASSES = {
77956
78011
  candle_perps_risk: READ,
77957
78012
  candle_perps_thresholds: READ,
77958
78013
  candle_trade: card("trade", "clientTradeId"),
78014
+ candle_trade_execute: card("trade"),
77959
78015
  candle_swap: card("swap", "clientSwapId"),
77960
78016
  candle_launch_token: card("launch", "clientLaunchId"),
77961
78017
  candle_launch_and_seed: card("launch", "clientLaunchId"),
@@ -78045,7 +78101,8 @@ async function readOnce(executor, target, writeKey) {
78045
78101
  if (!readsById(target))
78046
78102
  return { readable: false };
78047
78103
  const kind = target.kind === "launch" ? "launch" : "trade";
78048
- const outcome = await executor.execute("candle_get_operation", { clientId: target.opId, kind }, writeKey);
78104
+ const clientId = target.tool === "candle_trade_execute" ? target.params.clientTradeId : target.opId;
78105
+ const outcome = await executor.execute("candle_get_operation", { clientId, kind }, writeKey);
78049
78106
  if (outcome.state === "succeeded" || outcome.state === "pending") {
78050
78107
  const job = outcome.result?.job ?? outcome.result;
78051
78108
  const status = typeof job?.status === "string" ? job.status : "";
@@ -89536,8 +89593,7 @@ async function externalList(args, ctx) {
89536
89593
  return usage(ctx, `Unexpected argument: ${parsed.positionals[0]}`);
89537
89594
  if (!refuseEnvPassphrase(ctx))
89538
89595
  return 1;
89539
- if (!requireTty(ctx, "external list"))
89540
- return 1;
89596
+ const promptCtx = refusePromptsWithoutTerminal(ctx, "external list");
89541
89597
  const { deps } = ctx;
89542
89598
  const resolvedVault = vaultPathFor(ctx, parsed);
89543
89599
  if ("error" in resolvedVault)
@@ -89545,7 +89601,7 @@ async function externalList(args, ctx) {
89545
89601
  const path = resolvedVault.path;
89546
89602
  return runVaultCommand(ctx, async ({ hold }) => {
89547
89603
  const raw = await requireVaultRaw(ctx, resolvedVault);
89548
- const vault = hold((await unlockInteractively(ctx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
89604
+ const vault = hold((await unlockInteractively(promptCtx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
89549
89605
  const externals = vault.index.entries.filter((entry) => entry.role === "external");
89550
89606
  const rows = externals.map((entry) => ({
89551
89607
  address: entry.address,
@@ -92206,6 +92262,8 @@ init_solana_lite();
92206
92262
  var NATIVE_MINT2 = "So11111111111111111111111111111111111111112";
92207
92263
  var LAMPORTS_PER_SIGNATURE = 5000n;
92208
92264
  var TOKEN_ACCOUNT_RENT_LAMPORTS = 2039280n;
92265
+ var BUYER_TALLY_RENT_LAMPORTS = 1461600n;
92266
+ var REGISTER_BUYER = [144, 92, 137, 46, 209, 220, 27, 164];
92209
92267
  var DEFAULT_UNITS_PER_INSTRUCTION = 200000n;
92210
92268
  var MAX_COMPUTE_UNITS = 1400000n;
92211
92269
 
@@ -92289,7 +92347,7 @@ function readLeg(tx, index, wallet3) {
92289
92347
  if (program === MEMO_PROGRAM_ID)
92290
92348
  continue;
92291
92349
  if (program !== undefined)
92292
- others.push({ program, data: ix.data });
92350
+ others.push({ program, data: ix.data, accounts: accounts.map(key) });
92293
92351
  }
92294
92352
  if (unitPrice > 0n) {
92295
92353
  const units = unitLimit ?? (computed * DEFAULT_UNITS_PER_INSTRUCTION > MAX_COMPUTE_UNITS ? MAX_COMPUTE_UNITS : computed * DEFAULT_UNITS_PER_INSTRUCTION);
@@ -92297,12 +92355,36 @@ function readLeg(tx, index, wallet3) {
92297
92355
  }
92298
92356
  return { lamports, tokenOut, systemTransfers, others };
92299
92357
  }
92358
+ function isRegisterBuyer(data) {
92359
+ return data.length >= 8 && REGISTER_BUYER.every((byte, i) => data[i] === byte);
92360
+ }
92361
+ function readRegisterBuyer(register, swap3, wallet3) {
92362
+ if (register.program !== swap3.program)
92363
+ throw new BundleReadError("leg 1 registers the buyer with another program");
92364
+ if (register.data.length !== 12 || u323(register.data, 8) !== 0n)
92365
+ throw new BundleReadError("leg 1 registers the buyer with a proof");
92366
+ const [buyer, rentPayer, rules, tally] = register.accounts;
92367
+ if (buyer !== wallet3)
92368
+ throw new BundleReadError("leg 1 registers a buyer other than this wallet");
92369
+ if (rules === undefined || tally === undefined || !swap3.accounts.includes(rules) || !swap3.accounts.includes(tally))
92370
+ throw new BundleReadError("leg 1 registers the buyer for another pool than the one it buys");
92371
+ return rentPayer === wallet3 ? BUYER_TALLY_RENT_LAMPORTS : 0n;
92372
+ }
92300
92373
  function readLaunchBundle(args) {
92301
92374
  const launch = readLeg(decodeLeg(args.legs[0], 0), 0, args.wallet);
92302
92375
  const buy = readLeg(decodeLeg(args.legs[1], 1), 1, args.wallet);
92303
- if (buy.others.length !== 1)
92376
+ let registerLamports = 0n;
92377
+ const first = buy.others[0];
92378
+ if (first && isRegisterBuyer(first.data)) {
92379
+ if (!args.rules)
92380
+ throw new BundleReadError("leg 1 registers a buyer on a launch without rules");
92381
+ const swap4 = buy.others[1];
92382
+ if (buy.others.length !== 2 || !swap4 || isRegisterBuyer(swap4.data))
92383
+ throw new BundleReadError("leg 1 is not one curve swap");
92384
+ registerLamports = readRegisterBuyer(first, swap4, args.wallet);
92385
+ } else if (buy.others.length !== 1)
92304
92386
  throw new BundleReadError("leg 1 is not one curve swap");
92305
- const swap3 = buy.others[0];
92387
+ const swap3 = buy.others[buy.others.length - 1];
92306
92388
  if (u64(swap3.data, 8) !== args.buyRaw)
92307
92389
  throw new BundleReadError("leg 1 does not swap the confirmed --buy");
92308
92390
  if (args.minOutRaw !== undefined && u64(swap3.data, 16) !== args.minOutRaw)
@@ -92322,7 +92404,7 @@ function readLaunchBundle(args) {
92322
92404
  feeRaw = buy.tokenOut;
92323
92405
  quoteRaw = args.buyRaw + buy.tokenOut + launch.tokenOut;
92324
92406
  }
92325
- return { lamports: launch.lamports + buy.lamports, quoteRaw, feeRaw };
92407
+ return { lamports: launch.lamports + buy.lamports + registerLamports, quoteRaw, feeRaw };
92326
92408
  }
92327
92409
 
92328
92410
  // src/launch-check.ts
@@ -93131,7 +93213,8 @@ async function atomicLaunch(ctx, key, args) {
93131
93213
  wallet: wallet3.address,
93132
93214
  quote: quoteAsset,
93133
93215
  buyRaw: BigInt(buy.raw),
93134
- ...minOut !== undefined ? { minOutRaw: BigInt(minOut) } : {}
93216
+ ...minOut !== undefined ? { minOutRaw: BigInt(minOut) } : {},
93217
+ rules: args.rules !== undefined
93135
93218
  });
93136
93219
  } catch (error) {
93137
93220
  if (!(error instanceof BundleReadError))
@@ -94074,6 +94157,7 @@ var MCP_TOOL_NAMES = [
94074
94157
  "candle_get_plans",
94075
94158
  "candle_report_activity",
94076
94159
  "candle_trade",
94160
+ "candle_trade_execute",
94077
94161
  "candle_swap",
94078
94162
  "candle_transfer",
94079
94163
  "candle_sweep",
@@ -94206,7 +94290,8 @@ async function mcp(args, ctx) {
94206
94290
  }
94207
94291
  const identityConfig = await deps.readConfig();
94208
94292
  const identityFields = effectiveProfileFields(identityConfig, ctx.profile);
94209
- deps.stderr.write(`${identityLine(ctx.profile, identityFields.account, apiUrl, credentialEnvOverrides(deps.env), identityFields.username, identityStyle(deps, "stderr"))}
94293
+ const identityOverrides = credentialEnvOverrides(deps.env);
94294
+ deps.stderr.write(`${identityLine(ctx.profile, identityOverrides.length > 0 ? undefined : identityFields.account, apiUrl, identityOverrides.length > 0 ? identityOverrides : undefined, identityFields.username, identityStyle(deps, "stderr"))}
94210
94295
 
94211
94296
  `);
94212
94297
  if (parsed.booleans.has("--print-config")) {
@@ -102036,15 +102121,14 @@ async function runPlugin(name2, rawArgs, ctx) {
102036
102121
  if (split2.wallets.length > 0) {
102037
102122
  if (!refuseEnvPassphrase(ctx))
102038
102123
  return 1;
102039
- if (!requireTty(ctx, "candle <plugin> --wallet"))
102040
- return 1;
102124
+ const promptCtx = refusePromptsWithoutTerminal(ctx, "candle <plugin> --wallet");
102041
102125
  const resolvedVault = vaultPathFor(ctx, { values: {}, booleans: new Set, positionals: [] });
102042
102126
  if ("error" in resolvedVault)
102043
102127
  return usage(ctx, resolvedVault.error);
102044
102128
  const vaultPath = resolvedVault.path;
102045
102129
  const resolved = await runVaultCommand(ctx, async ({ hold }) => {
102046
102130
  const raw = await requireVaultRaw(ctx, resolvedVault);
102047
- const vault = hold((await unlockInteractively(ctx, vaultPath, raw)).vault);
102131
+ const vault = hold((await unlockInteractively(promptCtx, vaultPath, raw)).vault);
102048
102132
  for (const requested of split2.wallets) {
102049
102133
  const entry = findExternalEntry(vault.index, requested);
102050
102134
  if (entry === undefined) {
@@ -102734,14 +102818,20 @@ var NO_API_KEY6 = {
102734
102818
  message: "No API key for this profile.",
102735
102819
  suggestion: "Set CANDLE_API_KEY, or run `candle auth login` to store one."
102736
102820
  };
102821
+ var NO_TERMINAL_REASON = "not read: no terminal for its unlock";
102822
+ var NO_TERMINAL_NOTE = "The vault was not read: its unlock needs a prompt, and standard input and standard error are not both a terminal. Run it at a terminal, pass --factor touch-id or --factor passkey to unlock without a prompt, or pass --no-vault to leave it out.";
102737
102823
  async function portfolio(args, ctx) {
102738
102824
  const parsed = parseArgs(args, {
102739
102825
  valueFlags: ["--rpc-url", "--evm-rpc-url", "--keystore", "--chain"],
102740
- booleanFlags: ["--no-perps"],
102826
+ booleanFlags: ["--no-perps", "--no-vault"],
102741
102827
  pathFlags: ["--keystore"]
102742
102828
  });
102743
102829
  if ("error" in parsed)
102744
102830
  return usage(ctx, parsed.error);
102831
+ const noVault = parsed.booleans.has("--no-vault");
102832
+ if (noVault && parsed.values["--keystore"] !== undefined) {
102833
+ return usage(ctx, "--keystore names the vault to read and has no effect with --no-vault.");
102834
+ }
102745
102835
  if (parsed.positionals.length > 1)
102746
102836
  return usage(ctx, `Unexpected argument: ${parsed.positionals[1]}`);
102747
102837
  const filter = parsed.positionals[0];
@@ -102788,16 +102878,30 @@ async function portfolio(args, ctx) {
102788
102878
  let vaultEntries;
102789
102879
  let evmEntries;
102790
102880
  let vaultReason;
102881
+ let vaultSkipped = false;
102791
102882
  let known = 0;
102792
- const raw = await readVaultRaw(resolvedVault.path);
102793
- if (raw === null) {
102794
- vaultReason = `no vault at ${resolvedVault.path}`;
102795
- } else {
102883
+ const raw = noVault ? null : await readVaultRaw(resolvedVault.path);
102884
+ let vault;
102885
+ if (raw !== null) {
102796
102886
  if (!refuseEnvPassphrase(ctx))
102797
102887
  return 1;
102798
- if (!requirePromptStreams(ctx, "portfolio"))
102799
- return 1;
102800
- const vault = hold((await unlockInteractively(ctx, resolvedVault.path, raw)).vault);
102888
+ try {
102889
+ const unlockCtx = refusePromptsWithoutTerminal(ctx, "portfolio");
102890
+ vault = hold((await unlockInteractively(unlockCtx, resolvedVault.path, raw)).vault);
102891
+ } catch (error) {
102892
+ if (!isNoTerminalRefusal(error))
102893
+ throw error;
102894
+ }
102895
+ }
102896
+ if (noVault) {
102897
+ vaultReason = "not read (--no-vault)";
102898
+ } else if (raw === null) {
102899
+ vaultReason = `no vault at ${resolvedVault.path}`;
102900
+ } else if (vault === undefined) {
102901
+ vaultReason = NO_TERMINAL_REASON;
102902
+ vaultSkipped = true;
102903
+ notice(deps, "stderr", "note", NO_TERMINAL_NOTE);
102904
+ } else {
102801
102905
  vaultEntries = showSolana ? vault.index.entries.filter((entry) => entry.chain === "solana" && (entry.role === "vault" || entry.role === "external")).map((entry) => ({ address: entry.address, label: entry.label, role: entry.role })) : [];
102802
102906
  evmEntries = showHood ? vault.index.entries.filter((entry) => entry.chain === "evm" && entry.role === "vault").map((entry) => ({ address: entry.address, label: entry.label })) : [];
102803
102907
  known += vaultEntries.length + evmEntries.length;
@@ -103012,7 +103116,7 @@ async function portfolio(args, ctx) {
103012
103116
  const listStatus = walletListStatus(answered);
103013
103117
  const shownUnread = unavailable6.length > 0 || lpUnread > 0;
103014
103118
  const listNote = listStatus === "cut-off" ? "cut-off" : listStatus === "unknown" && !shownUnread ? "unknown" : null;
103015
- const complete = listNote === null && !shownUnread && !perpsPartial;
103119
+ const complete = listNote === null && !shownUnread && !perpsPartial && !vaultSkipped;
103016
103120
  const totalUsd = groups.reduce((sum, g) => sum + g.valueUsd, 0);
103017
103121
  const unpriced = groups.reduce((sum, g) => sum + g.unpriced, 0);
103018
103122
  const lp = lpSection ? {
@@ -103089,6 +103193,7 @@ async function portfolio(args, ctx) {
103089
103193
  byChain,
103090
103194
  unavailableByChain,
103091
103195
  listNote,
103196
+ vaultSkipped,
103092
103197
  lp
103093
103198
  });
103094
103199
  if (perps)
@@ -103464,7 +103569,7 @@ ${tableBlock(ctx, "LP positions", ["GROUP", "WALLET", "POSITION", "POOL", "HOLDI
103464
103569
  notes.push([
103465
103570
  "total",
103466
103571
  formatUsd(totals.totalUsd),
103467
- totals.unpriced > 0 ? `${totals.unpriced} unpriced ${totals.unpriced === 1 ? "holding" : "holdings"} not counted` : "every holding priced"
103572
+ (totals.vaultSkipped ? "partial: the vault was not read (no terminal for its unlock). " : "") + (totals.unpriced > 0 ? `${totals.unpriced} unpriced ${totals.unpriced === 1 ? "holding" : "holdings"} not counted` : totals.vaultSkipped ? "other holdings priced" : "every holding priced")
103468
103573
  ]);
103469
103574
  const style = terminalStyle(deps, "stdout");
103470
103575
  const width = Math.max(...notes.map(([label]) => displayWidth(label)));
@@ -103486,6 +103591,8 @@ ${tableBlock(ctx, "LP positions", ["GROUP", "WALLET", "POSITION", "POOL", "HOLDI
103486
103591
  if (unavailable6 > 0) {
103487
103592
  warnings.push(`${unavailable6} ${unavailable6 === 1 ? "wallet" : "wallets"} could not be read in full (${unreadByChain.map(([chain2, n]) => `${n} on ${CHAIN_NAMES[chain2]}`).join(", ")}). What was not read is marked "not read" and is not in the total.`);
103488
103593
  }
103594
+ if (totals.vaultSkipped)
103595
+ warnings.push("The vault was not read: no terminal for its unlock. Its holdings are not in the total, so the total is partial (exit 3). Run it in a terminal, or pass --no-vault to leave the vault out on purpose.");
103489
103596
  writeNotes2(ctx, warnings);
103490
103597
  writeListNote(ctx, totals.listNote);
103491
103598
  const later = [];
@@ -103539,6 +103646,7 @@ ${financialHeading("Hyperliquid (live from Hyperliquid, not in the ledger; not i
103539
103646
  }
103540
103647
 
103541
103648
  // src/commands/profile.ts
103649
+ init_account();
103542
103650
  init_args();
103543
103651
  init_client();
103544
103652
  init_human_output();
@@ -104669,8 +104777,7 @@ async function sign5(args, ctx) {
104669
104777
  return usage(ctx, solana.error);
104670
104778
  if (!refuseEnvPassphrase(ctx))
104671
104779
  return 1;
104672
- if (!requireTty(ctx, "candle sign"))
104673
- return 1;
104780
+ const promptCtx = refusePromptsWithoutTerminal(ctx, "candle sign");
104674
104781
  const { deps } = ctx;
104675
104782
  const resolvedVault = vaultPathFor(ctx, parsed);
104676
104783
  if ("error" in resolvedVault)
@@ -104700,7 +104807,7 @@ async function sign5(args, ctx) {
104700
104807
  throw error;
104701
104808
  }
104702
104809
  const raw = await requireVaultRaw(ctx, resolvedVault);
104703
- const opened = await unlockInteractively(ctx, path, raw, {
104810
+ const opened = await unlockInteractively(promptCtx, path, raw, {
104704
104811
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
104705
104812
  });
104706
104813
  const vault = hold(opened.vault);
@@ -104859,8 +104966,7 @@ async function signMessage2(args, ctx) {
104859
104966
  const requested = lifted.values[0];
104860
104967
  if (!refuseEnvPassphrase(ctx))
104861
104968
  return 1;
104862
- if (!requireTty(ctx, "candle sign message"))
104863
- return 1;
104969
+ const promptCtx = refusePromptsWithoutTerminal(ctx, "candle sign message");
104864
104970
  const { deps } = ctx;
104865
104971
  const resolvedVault = vaultPathFor(ctx, parsed);
104866
104972
  if ("error" in resolvedVault)
@@ -104874,7 +104980,7 @@ async function signMessage2(args, ctx) {
104874
104980
  throw new VaultError("VAULT_UNREADABLE", `The message could not be read: ${error instanceof Error ? error.message : error}.`);
104875
104981
  }
104876
104982
  const raw = await requireVaultRaw(ctx, resolvedVault);
104877
- const opened = await unlockInteractively(ctx, path, raw, {
104983
+ const opened = await unlockInteractively(promptCtx, path, raw, {
104878
104984
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
104879
104985
  });
104880
104986
  const vault = hold(opened.vault);
@@ -105642,8 +105748,7 @@ async function vaultRename(args, ctx) {
105642
105748
  const id = parsed.values["--id"];
105643
105749
  if (!refuseEnvPassphrase(ctx))
105644
105750
  return 1;
105645
- if (!requireTty(ctx, "vault rename"))
105646
- return 1;
105751
+ const promptCtx = refusePromptsWithoutTerminal(ctx, "vault rename");
105647
105752
  const { deps } = ctx;
105648
105753
  const resolvedVault = vaultPathFor(ctx, parsed);
105649
105754
  if ("error" in resolvedVault)
@@ -105664,7 +105769,7 @@ async function vaultRename(args, ctx) {
105664
105769
  }
105665
105770
  return runVaultCommand(ctx, async ({ hold }) => {
105666
105771
  const raw = await requireVaultRaw(ctx, resolvedVault);
105667
- const opened = await unlockInteractively(ctx, path, raw, {
105772
+ const opened = await unlockInteractively(promptCtx, path, raw, {
105668
105773
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
105669
105774
  });
105670
105775
  const vault = hold(opened.vault);
@@ -105935,10 +106040,9 @@ async function teeRename(args, ctx) {
105935
106040
  if ("error" in vaultPath)
105936
106041
  return usage(ctx, vaultPath.error);
105937
106042
  const raw = rows.some((row2) => row2.tee) ? await readVaultRaw(vaultPath.path) : null;
105938
- if (raw !== null && !requireTty(ctx, "tee rename"))
105939
- return 1;
106043
+ const promptCtx = refusePromptsWithoutTerminal(ctx, "tee rename");
105940
106044
  return runVaultCommand(ctx, async ({ hold }) => {
105941
- const vault = raw === null ? null : hold((await unlockInteractively(ctx, vaultPath.path, raw, {
106045
+ const vault = raw === null ? null : hold((await unlockInteractively(promptCtx, vaultPath.path, raw, {
105942
106046
  acceptOlderCopy: parsed.booleans.has("--accept-older-copy")
105943
106047
  })).vault);
105944
106048
  const local = [];
@@ -111130,13 +111234,12 @@ async function vaultList(args, ctx) {
111130
111234
  if (balances && "error" in evmRpc)
111131
111235
  return usage(ctx, evmRpc.error);
111132
111236
  const evmRpcUrl = "url" in evmRpc ? evmRpc.url : DEFAULT_HOOD_RPC_URL;
111133
- if (!requirePromptStreams(ctx, "vault list"))
111134
- return 1;
111237
+ const promptCtx = refusePromptsWithoutTerminal(ctx, "vault list");
111135
111238
  return runVaultCommand(ctx, async ({ hold }) => {
111136
111239
  const raw = await readVaultRaw(path);
111137
111240
  if (raw === null)
111138
111241
  throw missingVault(ctx, resolvedVault);
111139
- const vault = hold((await unlockInteractively(ctx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
111242
+ const vault = hold((await unlockInteractively(promptCtx, path, raw, { acceptOlderCopy: parsed.booleans.has("--accept-older-copy") })).vault);
111140
111243
  const all = vault.index.entries;
111141
111244
  const filtered = filter === undefined ? all : all.filter((entry) => matches2(entry, filter));
111142
111245
  const showRetired = parsed.booleans.has("--all");
@@ -115175,6 +115278,101 @@ function walletsDisallowLaunch(args, ctx) {
115175
115278
  return setAllowLaunch(args, ctx, false);
115176
115279
  }
115177
115280
 
115281
+ // src/commands/wallets-allowlist.ts
115282
+ init_args();
115283
+ init_client();
115284
+ init_deps();
115285
+ init_human_output();
115286
+ init_presentation();
115287
+ init_profiles();
115288
+ init_render();
115289
+ init_key_output();
115290
+ var EMPTY_NOTE = "No approved withdrawal addresses. An agent can move funds only to the account's own wallets and its vault.";
115291
+ var MANAGED_HINT = "Approving an address needs the owner signed in to the agent console's Wallets tab; an API key cannot approve. An acting key can revoke one; a read-only key cannot.";
115292
+ async function walletsAllowlist(args, ctx) {
115293
+ const { deps, apiUrl, json } = ctx;
115294
+ const parsed = parseArgs(args, { booleanFlags: ["--all"] });
115295
+ if ("error" in parsed) {
115296
+ writeScreenUsageFailure(deps, parsed.error, json);
115297
+ return 2;
115298
+ }
115299
+ if (parsed.positionals.length > 0) {
115300
+ writeScreenUsageFailure(deps, `Unexpected argument: ${parsed.positionals[0]}`, json);
115301
+ return 2;
115302
+ }
115303
+ await printIdentity(ctx);
115304
+ const apiKey = await resolveApiKey(deps, ctx.profile);
115305
+ if (!apiKey) {
115306
+ writeScreenLocalFailure(deps, { code: "NO_API_KEY", message: "No API key available.", suggestion: "Run: candle keys create" }, json);
115307
+ return 1;
115308
+ }
115309
+ const query3 = parsed.booleans.has("--all") ? "?includeRevoked=true" : "";
115310
+ const result = await apiRequest(`/api/v1/agent/withdrawal-addresses${query3}`, {
115311
+ method: "GET",
115312
+ auth: "key",
115313
+ credentials: { apiKey },
115314
+ apiUrl,
115315
+ fetch: deps.fetch,
115316
+ env: deps.env
115317
+ });
115318
+ if (!result.ok) {
115319
+ writeScreenFailure(deps, result, { apiUrl, authType: "key" }, json);
115320
+ return 1;
115321
+ }
115322
+ const body = result.body;
115323
+ if (!Array.isArray(body.addresses)) {
115324
+ writeScreenLocalFailure(deps, { code: "INVALID_RESPONSE", message: "The withdrawal allowlist read did not return a list." }, json);
115325
+ return 1;
115326
+ }
115327
+ const rows = body.addresses;
115328
+ if (json) {
115329
+ deps.stdout.write(`${JSON.stringify({ success: true, addresses: rows, count: rows.length })}
115330
+ `);
115331
+ return 0;
115332
+ }
115333
+ const style = terminalStyle(deps, "stdout");
115334
+ deps.stdout.write(`${heading(`Withdrawal allowlist (${rows.length})`, style)}
115335
+
115336
+ `);
115337
+ if (rows.length === 0) {
115338
+ notice(deps, "stdout", "note", EMPTY_NOTE);
115339
+ notice(deps, "stdout", "note", MANAGED_HINT);
115340
+ return 0;
115341
+ }
115342
+ if (style.width < 100) {
115343
+ for (const row2 of rows) {
115344
+ writeFields(deps, "stdout", [
115345
+ ["Id", row2.id],
115346
+ ["Address", row2.address],
115347
+ ["Label", row2.label],
115348
+ ["Chain", row2.chain],
115349
+ ["Revoked", row2.revokedAt ? "yes" : "no"]
115350
+ ]);
115351
+ deps.stdout.write(`
115352
+ `);
115353
+ }
115354
+ } else {
115355
+ deps.stdout.write(`${renderTable(["Id", "Chain", "Address", "Label", "Revoked"], rows.map((row2) => [
115356
+ row2.id,
115357
+ row2.chain,
115358
+ row2.address,
115359
+ row2.label ?? symbols(style).missing,
115360
+ row2.revokedAt ? "yes" : "no"
115361
+ ]), {
115362
+ style,
115363
+ width: style.width,
115364
+ indent: 2,
115365
+ rule: false,
115366
+ columns: [{ id: true }, { priority: 3 }, { id: true }, { priority: 2 }, { priority: 1 }]
115367
+ })}
115368
+ `);
115369
+ }
115370
+ deps.stdout.write(`
115371
+ `);
115372
+ notice(deps, "stdout", "note", MANAGED_HINT);
115373
+ return 0;
115374
+ }
115375
+
115178
115376
  // src/commands/wallets-authorize.ts
115179
115377
  init_args();
115180
115378
  init_client();
@@ -115877,6 +116075,7 @@ init_ownership();
115877
116075
  init_file_lock();
115878
116076
 
115879
116077
  // src/guard.ts
116078
+ init_account();
115880
116079
  init_profiles();
115881
116080
  async function verifyProfileAccount(ctx, config) {
115882
116081
  const { deps, profile } = ctx;
@@ -116144,7 +116343,8 @@ var COMMANDS = {
116144
116343
  "allow-launch": walletsAllowLaunch,
116145
116344
  "disallow-launch": walletsDisallowLaunch,
116146
116345
  "close-empty": walletsCloseEmpty,
116147
- authorize: walletsAuthorize
116346
+ authorize: walletsAuthorize,
116347
+ allowlist: walletsAllowlist
116148
116348
  },
116149
116349
  bare: wallets
116150
116350
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@candledottv/cli",
3
- "version": "0.13.17",
3
+ "version": "0.13.18",
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": {