@haven_ai/cli 0.0.0-dev.202609061907.51d5faf → 0.0.0-dev.202609062207.ef78277

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,9 +38,9 @@ The CLI talks to the hosted Haven backend by default. Point it elsewhere with
38
38
  `--api <url>` or `HAVEN_API_URL` (e.g. a local backend at
39
39
  `http://localhost:3001`).
40
40
 
41
- > **This version: login, read, and backend-only management.** On-chain,
42
- > owner-signed actions (budgets, send) are signed in the
43
- > dashboard — this CLI never holds your keys. See
41
+ > **This version: login, read, backend-only management, and budget
42
+ > construct-and-hand-off.** On-chain, owner-signed actions (budget signature,
43
+ > send) are signed in the dashboard — this CLI never holds your keys. See
44
44
  > [`docs/research/haven-cli.md`](../../docs/research/haven-cli.md) for the full
45
45
  > design and roadmap.
46
46
 
@@ -123,8 +123,45 @@ haven contacts add <name> <address> | contacts remove <id>
123
123
  haven agents connect --name <name> --budget <amount> --token USDC --period <minutes>
124
124
  haven agents connect --name <name> --budget 25 --token USDC --period 1440 --run
125
125
  haven agents connect --status <setupId> [--wait]
126
+
127
+ # budgets: construct-and-hand-off (#2539) — the CLI never signs
128
+ haven budget grant <agentId> --amount 25 --token USDC --period 1440
129
+ haven budget grant <agentId> --amount 25 --token USDC --period 1440 --recipient <address> --wait
130
+ haven budget revoke <agentId> <delegationHash> [--wait]
126
131
  ```
127
132
 
133
+ ### `haven budget grant` / `haven budget revoke` (#2539)
134
+
135
+ Later budget changes — a second token, a raise, a recipient pin, a stop — no
136
+ longer require describing where to click. Both commands CONSTRUCT the
137
+ signature request and print a dashboard link; **the human signs in the
138
+ browser**, with their passkey or wallet, every time. The CLI never signs and
139
+ never calls `activate` — that is the whole design.
140
+
141
+ - `budget grant <agentId> --amount <n> --token USDC --period <minutes>`
142
+ builds the pending delegation (period is whole **minutes**, at least `1` —
143
+ this rail has no one-time budget, and the CLI refuses `--period 0` rather
144
+ than letting `/delegations/build` answer it with a 400. `agents connect`
145
+ does take `--period 0`, but that sets `reset_period_min` on a different
146
+ route). `--amount` is in
147
+ whole tokens, read from your wallet's balances exactly like
148
+ `agents connect`; `--recipient` pins the budget to one address (omit for an
149
+ open budget); `--expires` takes unix seconds (default: 90 days).
150
+ - `budget revoke <agentId> <delegationHash>` prepares the sponsored
151
+ revocation (no gas, one signature). The hash comes from
152
+ `haven agents show` or the dashboard.
153
+ - `--wait` on either command polls until the human's signature lands (grant:
154
+ the hash turns `active`; revoke: the row turns `revoked`), 5 s interval, 15
155
+ minute ceiling. The build is idempotent for the same parameters while it is
156
+ still pending and unexpired — the dashboard form re-running the same grant
157
+ returns the same hash, so `--wait` converges instead of chasing a version
158
+ that never activates.
159
+ - Under `--json`, grant returns the backend's build object —
160
+ `{ build_id, typed_data_hash, signing_url, delegation_hash, version }`
161
+ (`build_id` and `typed_data_hash` are the delegation hash, named for API
162
+ clarity) — plus `agent_id` and `status`. The link first, the settled status
163
+ after: the same two-emission shape as device login.
164
+
128
165
  ### `haven agents connect`
129
166
 
130
167
  Does what the dashboard's connect modal does, from a terminal: creates the
@@ -150,8 +187,9 @@ the human's decision.
150
187
 
151
188
  Two flags the issue sketched and this does not have, so you are not left
152
189
  looking for them: **`--recipient`** (a recipient pin lives in the delegation's
153
- caveat enforcers and is set when the human approves the budget — no API field
154
- takes one here) and **`haven agents create`** (`POST /agents` requires a
190
+ caveat enforcers; `budget grant` above can set one, but a connect setup has no
191
+ API field to carry it — the human adds it when approving) and
192
+ **`haven agents create`** (`POST /agents` requires a
155
193
  delegate address, and a CLI an agent drives must never hold a signing key —
156
194
  `connect` is the path that generates one locally, on your machine).
157
195
 
package/dist/cli.cjs CHANGED
@@ -24,7 +24,10 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
24
24
  "--budget",
25
25
  "--token",
26
26
  "--period",
27
- "--status"
27
+ "--status",
28
+ "--amount",
29
+ "--recipient",
30
+ "--expires"
28
31
  ]);
29
32
  function parseArgs(argv) {
30
33
  const positionals = [];
@@ -74,6 +77,14 @@ function parseArgs(argv) {
74
77
  } else if (arg === "--direction") {
75
78
  if (value !== "in" && value !== "out") throw new Error('--direction must be "in" or "out"');
76
79
  flags.direction = value;
80
+ } else if (arg === "--recipient") {
81
+ flags.recipient = value;
82
+ } else if (arg === "--amount") {
83
+ flags.amount = value;
84
+ } else if (arg === "--expires") {
85
+ const n = Number(value);
86
+ if (!Number.isInteger(n) || n <= 0) throw new Error("--expires must be a unix timestamp in seconds");
87
+ flags.expires = n;
77
88
  } else if (arg === "--format") {
78
89
  if (value !== "csv" && value !== "sie") throw new Error('--format must be "csv" or "sie"');
79
90
  flags.format = value;
@@ -135,6 +146,13 @@ function helpText() {
135
146
  " contacts add <name> <address> Save an address under a name",
136
147
  " contacts remove <id> Forget one",
137
148
  "",
149
+ "Budgets (construct-and-hand-off \u2014 the CLI never signs, #2539):",
150
+ " budget grant <agentId> --amount <n> --token USDC --period <minutes>",
151
+ " [--recipient <address>] [--expires <unix-s>] [--wait]",
152
+ " Prints a signing link; the human signs in the dashboard.",
153
+ " budget revoke <agentId> <delegationHash> [--wait]",
154
+ " Prints a revocation link; the human signs in the dashboard.",
155
+ "",
138
156
  "Options:",
139
157
  " --json One JSON value on stdout, prose on stderr, on every",
140
158
  " command including refusals: { ok: false, error: { code, message, hint? } }",
@@ -394,7 +412,7 @@ ${failure.hint}` : failure.message);
394
412
  }
395
413
 
396
414
  // src/agent-guidance-text.ts
397
- var HAVEN_AGENT_RUNBOOK_MD = "# Haven for agents \u2014 set your user up to let you pay\n\nYou are an AI agent whose user has no Haven account yet, or has one but has not connected you.\n\n> Links below are paths on this same host: resolve them against the URL you fetched this file from.\n\n## What Haven is, in three sentences\n\nYour user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.\n\n## The sequence\n\nFour of the six steps are your user's \u2014 each needs a human signature or a human decision. The rest is yours. If they already have a funded account, start at step 3.\n\n1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey (Face ID / Touch ID) or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, `npx @haven_ai/cli login --api <api-url>` afterwards gets you a scoped session for steps 3-4 (that flag or `HAVEN_API_URL`, on the first command \u2014 the session then remembers the backend; **the CLI's built-in default is Haven's hosted production backend**, so on any other deployment an omitted flag connects you somewhere real and wrong rather than failing) \u2014 they approve a code in the browser, you never hold their password. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.\n2. **HUMAN \u2014 fund it.** USDC only, no ETH: Haven sponsors the gas. `haven wallets funding` prints the address, the amount **and which chain** in one place; without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain. Read the chain off whichever you used and put it in your message \u2014 never assume one: a testnet deployment and production both call themselves Haven.\n3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back. With a CLI session (step 1) you can do this step yourself: `haven agents connect --name <n> --budget <amount> --token USDC --period <minutes>` prints the same connector command and approval link; add `--run` to do step 4 too.\n4. **YOU \u2014 run the connector command** in that prompt (below). It makes your signing key locally, registering only the public address.\n5. **HUMAN \u2014 approve the budget** with their passkey, in the Haven tab they created the agent in: it advances to the approval step by itself once your run registers.\n6. **YOU \u2014 verify, then pay.**\n\n## Hand-off scripts\n\nSend these as your own message, `<host>` replaced by the host you fetched this file from. Say what you cannot do, not only what they must.\n\n**Before signup** (step 1):\n\n> I can do everything except the parts that need your signature. Please open `<host>/signup?next=/agents&via=agent` \u2014 name, email, password, then a passkey (Face ID / Touch ID) or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? `<host>/login?next=/agents`.\n\n**At the passkey step:**\n\n> The passkey must be made on your own device \u2014 it is what keeps the account yours, so nobody, Haven included, can move your funds without you. If this browser cannot, open `<host>/onboarding?next=/agents` on your phone and finish there.\n\n**At funding** (step 2):\n\n> Your Haven account needs USDC before I can pay for anything \u2014 USDC only, no ETH: Haven sponsors the gas. Before you send anything, let me get you the exact address **and network** \u2014 `haven wallets funding` prints both, and the dashboard shows the same. Please do not send to an address or a chain I have not confirmed with you; a small amount first is fine.\n\n**At the budget** (step 3):\n\n> On `<host>/agents`, create an agent for me and set a budget \u2014 say 25 USDC per day. That is the limit I cannot exceed. It hands back a setup prompt: paste it to me and I run it here.\n\n**At budget approval** (step 5) \u2014 the moment your run reports that approval is required, before anything else. Send the first if your run carried `approval.url`, the second if it did not.\n\n> Setup is done on my side. Approve the budget here: <approval.url>. Approve it with your passkey; nothing can be spent until you do.\n\n> Setup is done on my side. Go back to the Haven tab where you created the agent \u2014 it should now be asking you to approve the budget. Approve it with your passkey; nothing can be spent until you do.\n\n## What you run\n\nThe setup prompt carries a **connector command** shaped like this:\n\n```\nnpx -y @haven_ai/connect@<channel> --setup EXAMPLE-SETUP-TOKEN-NOT-REAL --api <api-url> --ack-local-tools\n```\n\nRun the prompt's version, not this one: the token is one-time, the API URL is the backend's own, and `<channel>` is the npm tag your prompt names \u2014 never a tag you pick. The rules below come from that prompt, so \"me\" in them is your user, not Haven.\n\n- If you are an AI agent running this command yourself rather than a human pasting it, you should append --json: the connector then emits one machine-readable, secret-free result object on stdout with progress on stderr, and returns promptly instead of blocking while it waits for the budget approval.\n- When a --json outcome reports approval.required: true, your first action must be to relay the approval instruction to me in your own reply \u2014 if the outcome carries approval.url, give me that link; otherwise tell me to return to Haven and approve this agent's budget \u2014 before verifying the connection, restarting anything, or any other step. Never build that link yourself: relay the one the outcome gave you or none at all. Any restart the outcome asks for is a separate instruction to give me afterwards, once the approval is done.\n- If you ran the command without --json, the connector waits for the approval itself and prints its next steps when it finishes: relay the budget-approval instruction to me \u2014 the approval link if those steps printed one, otherwise that you need to return to Haven and approve this agent's budget \u2014 only if those printed next steps still ask for it. If they report the budget as already approved, there is nothing for me to approve.\n- Only two changes to the command above are permitted, and no others: appending --json, and \u2014 only if the connector refuses because it could not determine the agent runtime \u2014 re-running it once with --runtime <name> added, naming the harness you are running in, using one of the values that refusal lists. Never invent a runtime name and never change anything else.\n- Do not print private keys, API keys, credential file contents, or config secrets in chat or logs.\n\nIf the approval wait times out, nothing is lost \u2014 your agent is registered and the budget is still waiting to be approved. Send your user the `approval.url` your run reported, or, if it carried none, ask them to finish it in that same Haven tab. The outcome carries no setup id, so never assemble an approval link out of parts \u2014 relay the whole one it gave you or none at all.\n\n## How to verify\n\nCall `haven_get_agent`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus `spend_authority_readiness`:\n\n- `ready` \u2014 a budget is live; you can pay.\n- `needs_approval` \u2014 the connector finished, nobody approved yet. Ask your user again, in their Haven tab; there is no queue to wait in.\n- `revoked` \u2014 the credential is not active; ask your user to create a new agent.\n\n`ready` covers hosted identity and the budget only, not your local signer. Check that with `npx -y @haven_ai/connect@<channel> --doctor`, the same tag your prompt named \u2014 a separate command, so the two-changes rule does not bind it.\n\n## If you cannot open a browser\n\nNothing here needs you to. Steps 1-3 are links: hand your user the full `<host>/\u2026` URL and ask them to say when it is done. Step 5 is a link only when your run reported one in `approval.url` \u2014 otherwise it is the tab they already have open, as above. Then poll `haven_get_agent` until it reads `ready`. Do not route around the sign-in wall \u2014 it makes the account theirs, not yours.\n\n## Vocabulary\n\n| Term | What it is |\n|---|---|\n| **setup prompt** | The text the dashboard hands your user to paste to you. Carries the command and its rules. |\n| **connector command** | The `npx -y @haven_ai/connect@\u2026` line you run. One-time token, one use. |\n| **agent credential** | Your API key (`sk_agent_\u2026`), written to `~/.haven`. It identifies you; alone it cannot move money. |\n| **delegate key** | Your signing key, made on this machine and never sent anywhere. |\n| **budget** | The on-chain delegation your user signed. It authorises the payment; Haven constructs and relays it. |\n\nNext: [your agent hit a 402](/402.md) \xB7 [everything agent-readable](/llms.txt)\n";
415
+ var HAVEN_AGENT_RUNBOOK_MD = "# Haven for agents \u2014 set your user up to let you pay\n\nYou are an AI agent whose user has no Haven account yet, or has one but has not connected you.\n\n> Links below are paths on this same host: resolve them against the URL you fetched this file from.\n\n## What Haven is, in three sentences\n\nYour user gives you a **budget on their own account** \u2014 not their wallet, and not a key to their funds. The budget is a delegation they sign, enforced on-chain: a payment over it, to the wrong recipient, or past its expiry is refused at execution time, not by a dashboard promise. Haven constructs and relays the payments; you get an agent credential and a signing key made on your machine, and your user can revoke the budget without you and without Haven.\n\n## The sequence\n\nFour of the six steps are your user's \u2014 each needs a human signature or a human decision. The rest is yours. If they already have a funded account, start at step 3.\n\n1. **HUMAN \u2014 create the account.** Name, email, password, then a passkey (Face ID / Touch ID) or a wallet. Never offer to enter any of it: you must not have their password, and the passkey is bound to their device. With a terminal, `npx @haven_ai/cli login --api <api-url>` afterwards gets you a scoped session for steps 3-4 (that flag or `HAVEN_API_URL`, on the first command \u2014 the session then remembers the backend; **the CLI's built-in default is Haven's hosted production backend**, so on any other deployment an omitted flag connects you somewhere real and wrong rather than failing) \u2014 they approve a code in the browser, you never hold their password. It can set up agents and read the account; it cannot sign, approve a budget, move funds, or rotate any agent's keys.\n2. **HUMAN \u2014 fund it.** USDC only, no ETH: Haven sponsors the gas. `haven wallets funding` prints the address, the amount **and which chain** in one place; without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain. Read the chain off whichever you used and put it in your message \u2014 never assume one: a testnet deployment and production both call themselves Haven.\n3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back. With a CLI session (step 1) you can do this step yourself: `haven agents connect --name <n> --budget <amount> --token USDC --period <minutes>` prints the same connector command and approval link; add `--run` to do step 4 too.\n4. **YOU \u2014 run the connector command** in that prompt (below). It makes your signing key locally, registering only the public address.\n5. **HUMAN \u2014 approve the budget** with their passkey, in the Haven tab they created the agent in: it advances to the approval step by itself once your run registers.\n6. **YOU \u2014 verify, then pay.**\n\n## Budget changes later (second token, raise, revoke)\n\nThe first budget rides the setup above. After the agent exists, a CLI session can construct the LATER changes \u2014 a second token, a bigger amount, a recipient pin, a stop \u2014 and hand your user a link; the human still signs, every time:\n\n```\nhaven budget grant <agentId> --amount <n> --token USDC --period <minutes> [--recipient <address>] [--wait]\nhaven budget revoke <agentId> <delegationHash> [--wait]\n```\n\nThe CLI never signs: it prints a dashboard link, your user opens it and signs with their passkey or wallet. `--wait` polls until the human's signature lands. The hash for `revoke` is in `haven agents show <agentId>`.\n\n## Hand-off scripts\n\nSend these as your own message, `<host>` replaced by the host you fetched this file from. Say what you cannot do, not only what they must.\n\n**Before signup** (step 1):\n\n> I can do everything except the parts that need your signature. Please open `<host>/signup?next=/agents&via=agent` \u2014 name, email, password, then a passkey (Face ID / Touch ID) or a wallet. That is your account's key: I should not have it, and I will never ask for your password. Already have one? `<host>/login?next=/agents`.\n\n**At the passkey step:**\n\n> The passkey must be made on your own device \u2014 it is what keeps the account yours, so nobody, Haven included, can move your funds without you. If this browser cannot, open `<host>/onboarding?next=/agents` on your phone and finish there.\n\n**At funding** (step 2):\n\n> Your Haven account needs USDC before I can pay for anything \u2014 USDC only, no ETH: Haven sponsors the gas. Before you send anything, let me get you the exact address **and network** \u2014 `haven wallets funding` prints both, and the dashboard shows the same. Please do not send to an address or a chain I have not confirmed with you; a small amount first is fine.\n\n**At the budget** (step 3):\n\n> On `<host>/agents`, create an agent for me and set a budget \u2014 say 25 USDC per day. That is the limit I cannot exceed. It hands back a setup prompt: paste it to me and I run it here.\n\n**At budget approval** (step 5) \u2014 the moment your run reports that approval is required, before anything else. Send the first if your run carried `approval.url`, the second if it did not.\n\n> Setup is done on my side. Approve the budget here: <approval.url>. Approve it with your passkey; nothing can be spent until you do.\n\n> Setup is done on my side. Go back to the Haven tab where you created the agent \u2014 it should now be asking you to approve the budget. Approve it with your passkey; nothing can be spent until you do.\n\n## What you run\n\nThe setup prompt carries a **connector command** shaped like this:\n\n```\nnpx -y @haven_ai/connect@<channel> --setup EXAMPLE-SETUP-TOKEN-NOT-REAL --api <api-url> --ack-local-tools\n```\n\nRun the prompt's version, not this one: the token is one-time, the API URL is the backend's own, and `<channel>` is the npm tag your prompt names \u2014 never a tag you pick. The rules below come from that prompt, so \"me\" in them is your user, not Haven.\n\n- If you are an AI agent running this command yourself rather than a human pasting it, you should append --json: the connector then emits one machine-readable, secret-free result object on stdout with progress on stderr, and returns promptly instead of blocking while it waits for the budget approval.\n- When a --json outcome reports approval.required: true, your first action must be to relay the approval instruction to me in your own reply \u2014 if the outcome carries approval.url, give me that link; otherwise tell me to return to Haven and approve this agent's budget \u2014 before verifying the connection, restarting anything, or any other step. Never build that link yourself: relay the one the outcome gave you or none at all. Any restart the outcome asks for is a separate instruction to give me afterwards, once the approval is done.\n- If you ran the command without --json, the connector waits for the approval itself and prints its next steps when it finishes: relay the budget-approval instruction to me \u2014 the approval link if those steps printed one, otherwise that you need to return to Haven and approve this agent's budget \u2014 only if those printed next steps still ask for it. If they report the budget as already approved, there is nothing for me to approve.\n- Only two changes to the command above are permitted, and no others: appending --json, and \u2014 only if the connector refuses because it could not determine the agent runtime \u2014 re-running it once with --runtime <name> added, naming the harness you are running in, using one of the values that refusal lists. Never invent a runtime name and never change anything else.\n- Do not print private keys, API keys, credential file contents, or config secrets in chat or logs.\n\nIf the approval wait times out, nothing is lost \u2014 your agent is registered and the budget is still waiting to be approved. Send your user the `approval.url` your run reported, or, if it carried none, ask them to finish it in that same Haven tab. The outcome carries no setup id, so never assemble an approval link out of parts \u2014 relay the whole one it gave you or none at all.\n\n## How to verify\n\nCall `haven_get_agent`, one of the Haven MCP tools the connector wires into your runtime in step 4. It returns identity plus `spend_authority_readiness`:\n\n- `ready` \u2014 a budget is live; you can pay.\n- `needs_approval` \u2014 the connector finished, nobody approved yet. Ask your user again, in their Haven tab; there is no queue to wait in.\n- `revoked` \u2014 the credential is not active; ask your user to create a new agent.\n\n`ready` covers hosted identity and the budget only, not your local signer. Check that with `npx -y @haven_ai/connect@<channel> --doctor`, the same tag your prompt named \u2014 a separate command, so the two-changes rule does not bind it.\n\n## If you cannot open a browser\n\nNothing here needs you to. Steps 1-3 are links: hand your user the full `<host>/\u2026` URL and ask them to say when it is done. Step 5 is a link only when your run reported one in `approval.url` \u2014 otherwise it is the tab they already have open, as above. Then poll `haven_get_agent` until it reads `ready`. Do not route around the sign-in wall \u2014 it makes the account theirs, not yours.\n\n## Vocabulary\n\n| Term | What it is |\n|---|---|\n| **setup prompt** | The text the dashboard hands your user to paste to you. Carries the command and its rules. |\n| **connector command** | The `npx -y @haven_ai/connect@\u2026` line you run. One-time token, one use. |\n| **agent credential** | Your API key (`sk_agent_\u2026`), written to `~/.haven`. It identifies you; alone it cannot move money. |\n| **delegate key** | Your signing key, made on this machine and never sent anywhere. |\n| **budget** | The on-chain delegation your user signed. It authorises the payment; Haven constructs and relays it. |\n\nNext: [your agent hit a 402](/402.md) \xB7 [everything agent-readable](/llms.txt)\n";
398
416
 
399
417
  // src/token.ts
400
418
  function sessionExpiry(token) {
@@ -569,7 +587,7 @@ async function runConnector(connectorCommand, spawner, onStderr) {
569
587
 
570
588
  // src/commands.ts
571
589
  var DEFAULT_API = "https://havenbackend-production-8a00.up.railway.app";
572
- var CLI_VERSION = "0.0.0-dev.202609061907.51d5faf";
590
+ var CLI_VERSION = "0.0.0-dev.202609062207.ef78277";
573
591
  async function run(argv, deps = {}) {
574
592
  const out = deps.out ?? ((l) => process.stdout.write(`${l}
575
593
  `));
@@ -648,6 +666,10 @@ async function dispatch(args, d) {
648
666
  return cmdAgentRename(args, d);
649
667
  case "budget show":
650
668
  return cmdBudgetShow(args, d);
669
+ case "budget grant":
670
+ return cmdBudgetGrant(args, d);
671
+ case "budget revoke":
672
+ return cmdBudgetRevoke(args, d);
651
673
  case "wallets rename":
652
674
  return cmdWalletRename(args, d);
653
675
  case "activity list":
@@ -927,6 +949,142 @@ function budgetSummary(allowances) {
927
949
  if (!allowances || allowances.length === 0) return "\u2014";
928
950
  return allowances.map((a) => `${a.allowance_amount} ${a.token_symbol}`).join(", ");
929
951
  }
952
+ var BUDGET_WAIT_TIMEOUT_S = 15 * 60;
953
+ var BUDGET_WAIT_INTERVAL_MS = 5e3;
954
+ async function cmdBudgetGrant(args, d) {
955
+ const id = args.positionals[0];
956
+ if (!id) {
957
+ throw new UsageError("Usage: haven budget grant <agentId> --amount 25 --token USDC --period <minutes> [--recipient <address>] [--expires <unix-seconds>] [--wait]");
958
+ }
959
+ if (!args.flags.amount || !args.flags.token || args.flags.period === void 0) {
960
+ throw new UsageError("--amount, --token and --period are required (period is whole minutes, at least 1)");
961
+ }
962
+ if (args.flags.period < 1) {
963
+ throw new UsageError("--period must be at least 1 minute for a budget grant. A recurring budget is the only shape this rail has; `haven agents connect --period 0` is a different field on a different route.");
964
+ }
965
+ const { api } = await authed(args, d);
966
+ const agent = await api.get(`/agents/${id}`);
967
+ if (agent.account_type !== "delegator_hybrid") {
968
+ throw new HavenCliError(
969
+ `Agent ${id} is not on the delegation rail \u2014 budgets are managed from the dashboard for this account.`,
970
+ EXIT.refused
971
+ );
972
+ }
973
+ if (!agent.safe_address || !agent.safe_chain_id) {
974
+ throw new HavenCliError(`Agent ${id} has no wallet assigned yet \u2014 connect it first.`, EXIT.refused);
975
+ }
976
+ const { balances } = await api.get(
977
+ `/balances/${agent.safe_address}?chain_id=${agent.safe_chain_id}`
978
+ );
979
+ const wanted = args.flags.token.trim().toUpperCase();
980
+ const token = balances.find((b) => b.symbol.toUpperCase() === wanted);
981
+ if (!token || !token.address) {
982
+ const known = balances.map((b) => b.symbol).join(", ");
983
+ throw new UsageError(`Unknown token ${args.flags.token} on this wallet's chain. Available: ${known || "none"}`);
984
+ }
985
+ const amount = parseTokenAmount(args.flags.amount, token.decimals, token.symbol);
986
+ if (!amount.ok) throw new UsageError(amount.message);
987
+ const built = await api.post(`/agents/${id}/delegations/build`, {
988
+ token_address: token.address,
989
+ recipient_address: args.flags.recipient ?? null,
990
+ budget_atomic: amount.atomic,
991
+ period_seconds: args.flags.period * 60,
992
+ ...args.flags.expires !== void 0 ? { expires_at: args.flags.expires } : {}
993
+ });
994
+ const emitGrant = (status) => emit(
995
+ d,
996
+ args.flags.json,
997
+ { ...built, agent_id: id, status: status ?? "pending" },
998
+ () => [
999
+ `Budget of ${args.flags.amount} ${token.symbol} per ${args.flags.period} minutes built for agent ${id}.`,
1000
+ args.flags.recipient ? `Recipient pin: ${args.flags.recipient}` : null,
1001
+ "Open this link and sign \u2014 the budget goes live the moment you do:",
1002
+ built.signing_url,
1003
+ `Delegation: ${built.delegation_hash} (version ${built.version})`,
1004
+ status === "active" ? "Signed and active." : "Waiting for your signature. Run the same command with --wait to poll until it is active."
1005
+ ].filter((line) => line !== null).join("\n")
1006
+ );
1007
+ emitGrant();
1008
+ if (!args.flags.wait) return EXIT.ok;
1009
+ const deadline = Date.now() + BUDGET_WAIT_TIMEOUT_S * 1e3;
1010
+ while (Date.now() < deadline) {
1011
+ await d.sleep(BUDGET_WAIT_INTERVAL_MS);
1012
+ const { delegations } = await api.get(`/agents/${id}/delegations`);
1013
+ const mine = delegations.find((row) => row.delegation_hash === built.delegation_hash);
1014
+ if (mine?.status === "active") {
1015
+ emitGrant("active");
1016
+ return EXIT.ok;
1017
+ }
1018
+ if (mine?.status === "revoked" || mine?.status === "replaced") {
1019
+ throw new HavenCliError(
1020
+ `Delegation ${built.delegation_hash} is now ${mine.status} without being signed.`,
1021
+ EXIT.refused
1022
+ );
1023
+ }
1024
+ }
1025
+ throw new HavenCliError(
1026
+ `Timed out waiting for delegation ${built.delegation_hash} to go active (${BUDGET_WAIT_TIMEOUT_S / 60} minutes). The signing link stays valid until it expires \u2014 sign it, or rerun with --wait.`,
1027
+ EXIT.failed
1028
+ );
1029
+ }
1030
+ async function cmdBudgetRevoke(args, d) {
1031
+ const [id, hash] = args.positionals;
1032
+ if (!id || !hash) throw new UsageError("Usage: haven budget revoke <agentId> <delegationHash> [--wait]");
1033
+ if (!/^0x[0-9a-fA-F]{64}$/.test(hash)) {
1034
+ throw new UsageError("The delegation hash must be 0x followed by 64 hex characters \u2014 `haven agents show` or the dashboard lists them.");
1035
+ }
1036
+ const { api } = await authed(args, d);
1037
+ const { delegations } = await api.get(`/agents/${id}/delegations`);
1038
+ const row = delegations.find((r) => r.delegation_hash.toLowerCase() === hash.toLowerCase());
1039
+ if (!row) {
1040
+ throw new HavenCliError(`No delegation ${hash} on agent ${id}.`, EXIT.refused);
1041
+ }
1042
+ if (row.status === "revoked") {
1043
+ throw new HavenCliError(`Delegation ${hash} is already revoked.`, EXIT.refused);
1044
+ }
1045
+ if (row.status === "replaced") {
1046
+ throw new HavenCliError(
1047
+ `Delegation ${hash} was already replaced by a newer grant \u2014 nothing to revoke.`,
1048
+ EXIT.refused
1049
+ );
1050
+ }
1051
+ const prepared = await api.post(`/agents/${id}/delegations/${hash}/revoke`, {});
1052
+ const revokeUrl = prepared.revocation_url;
1053
+ if (!revokeUrl) {
1054
+ throw new HavenCliError(
1055
+ "The backend did not return a revocation link \u2014 it may be older than #2539. Finish the revocation in the dashboard.",
1056
+ EXIT.failed
1057
+ );
1058
+ }
1059
+ const emitRevoke = (status) => emit(
1060
+ d,
1061
+ args.flags.json,
1062
+ { agent_id: id, delegation_hash: hash, status, ...prepared },
1063
+ () => [
1064
+ "Revocation prepared \u2014 one signature, sponsored (no gas).",
1065
+ "Open this link and sign to stop this budget:",
1066
+ revokeUrl,
1067
+ `Delegation: ${hash}`,
1068
+ status === "revoked" ? "Revoked." : "The budget keeps working until you sign. Run the same command with --wait to poll until it is revoked."
1069
+ ].filter((line) => line !== null).join("\n")
1070
+ );
1071
+ emitRevoke("pending_revoke");
1072
+ if (!args.flags.wait) return EXIT.ok;
1073
+ const deadline = Date.now() + BUDGET_WAIT_TIMEOUT_S * 1e3;
1074
+ while (Date.now() < deadline) {
1075
+ await d.sleep(BUDGET_WAIT_INTERVAL_MS);
1076
+ const { delegations: after } = await api.get(`/agents/${id}/delegations`);
1077
+ const mine = after.find((r) => r.delegation_hash.toLowerCase() === hash.toLowerCase());
1078
+ if (mine?.status === "revoked") {
1079
+ emitRevoke("revoked");
1080
+ return EXIT.ok;
1081
+ }
1082
+ }
1083
+ throw new HavenCliError(
1084
+ `Timed out waiting for delegation ${hash} to be revoked (${BUDGET_WAIT_TIMEOUT_S / 60} minutes). The link stays valid \u2014 sign it, or rerun with --wait.`,
1085
+ EXIT.failed
1086
+ );
1087
+ }
930
1088
  function resetLabel(mins) {
931
1089
  if (mins === 0) return "one-time";
932
1090
  if (mins === 1440) return "daily";