@haven_ai/cli 0.1.36-alpha.0 → 0.2.0-alpha.0

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
@@ -245,7 +245,7 @@ driving this CLI must never hold its user's password.
245
245
  ```bash
246
246
  haven login --json
247
247
  # {"ok":true,"verification_url":"https://app.haven…/device?code=ABCD-2345",
248
- # "user_code":"ABCD-2345","device_code":"…","expires_at":"…"}
248
+ # "user_code":"ABCD-2345","device_code":"…","interval":5,"expires_at":"…"}
249
249
  ```
250
250
 
251
251
  Under `--json` that object is printed **before** polling begins, so an agent
@@ -263,8 +263,8 @@ poll round per invocation, so nothing holds your turn open:
263
263
  ```bash
264
264
  haven login --api <api-url> --json --no-wait
265
265
  # { "ok": true, "verification_url": "…", "user_code": "ABCD-2345",
266
- # "device_code": "…", "expires_at": "…" } <- hand your user the link
267
- haven login --poll <device_code> # repeat until it stops saying pending
266
+ # "device_code": "…", "interval": 5, "expires_at": "…" } <- hand your user the link
267
+ haven login --poll <device_code> # wait interval seconds first; repeat until it stops saying pending
268
268
  ```
269
269
 
270
270
  Exit codes carry the outcome an agent acts on: **0** once approved (the same
package/dist/cli.cjs CHANGED
@@ -429,7 +429,7 @@ ${failure.hint}` : failure.message);
429
429
  }
430
430
 
431
431
  // src/agent-guidance-text.ts
432
- 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@<channel> 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. The `<channel>` in that command is the tag your deployment names \u2014 read it from `/.well-known/haven.json` (`packages.cli.channel`), never a tag you pick. Do not hold the process open while you wait: under `--json`, pass `--no-wait` to get the link object back at once, then poll it with `haven login --poll <device_code>` \u2014 one round per invocation, exit 3 while it is still pending, 0 once approved. 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 funding card on `<host>/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";
432
+ 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@<channel> 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. The `<channel>` in that command is the tag your deployment names \u2014 read it from `/.well-known/haven.json` (`packages.cli.channel`), never a tag you pick. Do not hold the process open while you wait: under `--json`, pass `--no-wait` to get the link object back at once, then poll it with `haven login --poll <device_code>` \u2014 one round per invocation, exit 3 while it is still pending, 0 once approved. 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. `/.well-known/haven.json` names `chains.default` as the deployment's expected chain, but after login `haven wallets funding` prints the address, the amount and which chain in one place; confirm that chain before you message your user. Without a CLI session, the dashboard's funding card shows the address and amount and its Receive-funds screen names the chain; never assume one: a testnet deployment and production both call themselves Haven. Before you write that message, read the manifest: `environment` says whether this deployment is `production`, and each `chains.supported` entry says whether that chain is a `testnet`. Real money is at stake only on a non-testnet chain of a `production` deployment \u2014 tell your user which case theirs is.\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 funding card on `<host>/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";
433
433
 
434
434
  // src/token.ts
435
435
  function sessionExpiry(token) {
@@ -604,7 +604,10 @@ async function runConnector(connectorCommand, spawner, onStderr) {
604
604
 
605
605
  // src/commands.ts
606
606
  var DEFAULT_API = "https://havenbackend-production-8a00.up.railway.app";
607
- var CLI_VERSION = "0.1.36-alpha.0";
607
+ var CLI_VERSION = "0.2.0-alpha.0";
608
+ function accountAddressOf(s) {
609
+ return s.account_address ?? s.safe_address ?? "";
610
+ }
608
611
  async function run(argv, deps = {}) {
609
612
  const out = deps.out ?? ((l) => process.stdout.write(`${l}
610
613
  `));
@@ -731,6 +734,7 @@ async function deviceLogin(args, d, baseUrl) {
731
734
  verification_url: start.verification_url,
732
735
  user_code: start.user_code,
733
736
  device_code: start.device_code,
737
+ interval: start.interval,
734
738
  expires_at: new Date(deadline).toISOString()
735
739
  },
736
740
  () => `Open ${start.verification_url}
@@ -874,35 +878,35 @@ async function cmdWhoami(args, d) {
874
878
  }
875
879
  async function cmdWalletsList(args, d) {
876
880
  const { api } = await authed(args, d);
877
- const { safes } = await api.get("/user/safes");
881
+ const { safes } = await api.get("/user/accounts");
878
882
  emit(
879
883
  d,
880
884
  args.flags.json,
881
885
  safes,
882
886
  () => safes.length === 0 ? "No Haven wallets yet." : table(
883
887
  ["NAME", "NETWORK", "ADDRESS", "DEFAULT"],
884
- safes.map((s) => [s.name, chainName(s.chain_id), truncateAddress(s.safe_address), s.is_default ? "\u2713" : ""])
888
+ safes.map((s) => [s.name, chainName(s.chain_id), truncateAddress(accountAddressOf(s)), s.is_default ? "\u2713" : ""])
885
889
  )
886
890
  );
887
891
  return EXIT.ok;
888
892
  }
889
893
  async function cmdWalletsBalances(args, d) {
890
894
  const { api } = await authed(args, d);
891
- const { safes } = await api.get("/user/safes");
895
+ const { safes } = await api.get("/user/accounts");
892
896
  const safe = pickSafe(safes, args.flags.safe);
893
897
  if (!safe) {
894
898
  if (args.flags.safe) throw new UsageError(`No wallet matches "${args.flags.safe}".`);
895
899
  throw new CliApiError("No Haven wallet found.", 404);
896
900
  }
897
901
  const { balances } = await api.get(
898
- `/balances/${safe.safe_address}?chain_id=${safe.chain_id}`
902
+ `/balances/${accountAddressOf(safe)}?chain_id=${safe.chain_id}`
899
903
  );
900
904
  emit(
901
905
  d,
902
906
  args.flags.json,
903
- { safe: safe.name, chainId: safe.chain_id, balances },
907
+ { account: safe.name, safe: safe.name, chainId: safe.chain_id, balances },
904
908
  () => [
905
- `${safe.name} \xB7 ${chainName(safe.chain_id)} \xB7 ${truncateAddress(safe.safe_address)}`,
909
+ `${safe.name} \xB7 ${chainName(safe.chain_id)} \xB7 ${truncateAddress(accountAddressOf(safe))}`,
906
910
  balances.length === 0 ? " (no balances)" : table(["TOKEN", "BALANCE"], balances.map((b) => [b.symbol, b.formatted]))
907
911
  ].join("\n")
908
912
  );
@@ -911,17 +915,17 @@ async function cmdWalletsBalances(args, d) {
911
915
  function pickSafe(safes, ref) {
912
916
  if (!ref) return safes.find((s) => s.is_default) ?? safes[0];
913
917
  const lower = ref.toLowerCase();
914
- return safes.find((s) => s.id === ref || s.safe_address.toLowerCase() === lower);
918
+ return safes.find((s) => s.id === ref || accountAddressOf(s).toLowerCase() === lower);
915
919
  }
916
920
  async function cmdWalletsFunding(args, d) {
917
921
  const { api } = await authed(args, d);
918
- const { safes } = await api.get("/user/safes");
922
+ const { safes } = await api.get("/user/accounts");
919
923
  const safe = pickSafe(safes, args.flags.safe);
920
924
  if (!safe) {
921
925
  if (args.flags.safe) throw new UsageError(`No wallet matches "${args.flags.safe}".`);
922
926
  throw new CliApiError("No Haven wallet found.", 404);
923
927
  }
924
- const funding = await api.get(`/user/safes/${safe.id}/funding`);
928
+ const funding = await api.get(`/user/accounts/${safe.id}/funding`);
925
929
  if (!args.flags.wait) {
926
930
  emitFunding(d, funding);
927
931
  return EXIT.ok;
@@ -942,7 +946,7 @@ async function cmdWalletsFunding(args, d) {
942
946
  );
943
947
  }
944
948
  await d.sleep(pollMs);
945
- current = await api.get(`/user/safes/${safe.id}/funding`);
949
+ current = await api.get(`/user/accounts/${safe.id}/funding`);
946
950
  if (current.funded) break;
947
951
  d.o.note(`Still waiting after ${elapsedLabel(Date.now() - started)} \u2014 funded: no.`);
948
952
  }
@@ -1059,11 +1063,13 @@ async function cmdBudgetGrant(args, d) {
1059
1063
  EXIT.refused
1060
1064
  );
1061
1065
  }
1062
- if (!agent.safe_address || !agent.safe_chain_id) {
1066
+ const accountAddress = agent.account_address ?? agent.safe_address;
1067
+ const accountChainId = agent.account_chain_id ?? agent.safe_chain_id;
1068
+ if (!accountAddress || !accountChainId) {
1063
1069
  throw new HavenCliError(`Agent ${id} has no wallet assigned yet \u2014 connect it first.`, EXIT.refused);
1064
1070
  }
1065
1071
  const { balances } = await api.get(
1066
- `/balances/${agent.safe_address}?chain_id=${agent.safe_chain_id}`
1072
+ `/balances/${accountAddress}?chain_id=${accountChainId}`
1067
1073
  );
1068
1074
  const wanted = args.flags.token.trim().toUpperCase();
1069
1075
  const token = balances.find((b) => b.symbol.toUpperCase() === wanted);
@@ -1232,13 +1238,13 @@ async function cmdWalletRename(args, d) {
1232
1238
  const name = nameParts.join(" ").trim();
1233
1239
  if (!id || !name) throw new UsageError("Usage: haven wallets rename <id> <name>");
1234
1240
  const { api } = await authed(args, d);
1235
- await api.put(`/user/safes/${id}`, { name });
1236
- emit(d, args.flags.json, { ok: true, safe_id: id, name }, () => `Wallet ${id} renamed to "${name}".`);
1241
+ await api.put(`/user/accounts/${id}`, { name });
1242
+ emit(d, args.flags.json, { ok: true, account_id: id, safe_id: id, name }, () => `Wallet ${id} renamed to "${name}".`);
1237
1243
  return EXIT.ok;
1238
1244
  }
1239
1245
  async function resolveSafeId(args, api) {
1240
1246
  if (!args.flags.safe) return void 0;
1241
- const { safes } = await api.get("/user/safes");
1247
+ const { safes } = await api.get("/user/accounts");
1242
1248
  const safe = pickSafe(safes, args.flags.safe);
1243
1249
  if (!safe) throw new UsageError(`No wallet matches "${args.flags.safe}".`);
1244
1250
  return safe.id;
@@ -1250,7 +1256,10 @@ async function cmdActivityList(args, d) {
1250
1256
  offset: String(args.flags.offset ?? 0),
1251
1257
  limit: String(args.flags.limit ?? 25)
1252
1258
  });
1253
- if (safeId) params.set("safeId", safeId);
1259
+ if (safeId) {
1260
+ params.set("accountId", safeId);
1261
+ params.set("safeId", safeId);
1262
+ }
1254
1263
  if (args.flags.agent) params.set("agentId", args.flags.agent);
1255
1264
  const { transactions } = await api.get(`/transactions?${params.toString()}`);
1256
1265
  const visible = args.flags.direction ? transactions.filter((t) => t.direction === args.flags.direction) : transactions;
@@ -1279,7 +1288,10 @@ async function cmdActivityExport(args, d) {
1279
1288
  offset: String(args.flags.offset ?? 0),
1280
1289
  limit: String(args.flags.limit ?? 1e3)
1281
1290
  });
1282
- if (safeId) params.set("safeId", safeId);
1291
+ if (safeId) {
1292
+ params.set("accountId", safeId);
1293
+ params.set("safeId", safeId);
1294
+ }
1283
1295
  if (args.flags.agent) params.set("agentId", args.flags.agent);
1284
1296
  const { transactions } = await api.get(`/transactions?${params.toString()}`);
1285
1297
  const visible = args.flags.direction ? transactions.filter((t) => t.direction === args.flags.direction) : transactions;
@@ -1295,7 +1307,8 @@ async function cmdActivityExport(args, d) {
1295
1307
  "safe_address",
1296
1308
  "agent_name",
1297
1309
  "tx_hash",
1298
- "chain_id"
1310
+ "chain_id",
1311
+ "account_address"
1299
1312
  ];
1300
1313
  const rows = visible.map((t) => [
1301
1314
  new Date(t.timestamp * 1e3).toISOString(),
@@ -1306,16 +1319,17 @@ async function cmdActivityExport(args, d) {
1306
1319
  t.tokenSymbol ?? t.asset ?? "",
1307
1320
  t.tokenAddress ?? "",
1308
1321
  (t.direction === "in" ? t.from : t.to) ?? "",
1309
- t.safeAddress ?? "",
1322
+ t.accountAddress ?? t.safeAddress ?? "",
1310
1323
  t.agentName ?? "",
1311
1324
  t.hash,
1312
- t.chainId != null ? String(t.chainId) : ""
1325
+ t.chainId != null ? String(t.chainId) : "",
1326
+ t.accountAddress ?? t.safeAddress ?? ""
1313
1327
  ]);
1314
1328
  d.o.text(toCsv(headers, rows), { format: "csv", rows: rows.length });
1315
1329
  return EXIT.ok;
1316
1330
  }
1317
1331
  function exportType(t) {
1318
- if (t.activityType === "delegate_sweep") return "allowance funding";
1332
+ if (t.activityType === "delegate_sweep") return "sweep";
1319
1333
  if (t.source === "x402") return "x402";
1320
1334
  if (t.source === "mpp_demo") return "mpp";
1321
1335
  return t.direction === "in" ? "receive" : "send";
@@ -1350,14 +1364,14 @@ async function cmdCatalogList(args, d) {
1350
1364
  return EXIT.ok;
1351
1365
  }
1352
1366
  async function resolveWalletAndToken(args, api, symbol) {
1353
- const { safes } = await api.get("/user/safes");
1367
+ const { safes } = await api.get("/user/accounts");
1354
1368
  if (safes.length === 0) {
1355
1369
  throw new HavenCliError("No wallet on this account yet \u2014 finish onboarding first.", EXIT.refused);
1356
1370
  }
1357
- const safe = args.flags.safe ? safes.find((s) => s.id === args.flags.safe || s.safe_address === args.flags.safe) : safes.find((s) => s.is_default) ?? safes[0];
1371
+ const safe = args.flags.safe ? safes.find((s) => s.id === args.flags.safe || accountAddressOf(s) === args.flags.safe) : safes.find((s) => s.is_default) ?? safes[0];
1358
1372
  if (!safe) throw new UsageError(`No wallet matches --safe ${args.flags.safe}`);
1359
1373
  const { balances } = await api.get(
1360
- `/balances/${safe.safe_address}?chain_id=${safe.chain_id}`
1374
+ `/balances/${accountAddressOf(safe)}?chain_id=${safe.chain_id}`
1361
1375
  );
1362
1376
  const wanted = symbol.trim().toUpperCase();
1363
1377
  const token = balances.find((b) => b.symbol.toUpperCase() === wanted);
@@ -1404,6 +1418,10 @@ async function cmdAgentsConnect(args, d) {
1404
1418
  if (!amount.ok) throw new UsageError(amount.message);
1405
1419
  const setup = await api.post("/agent-connection-setups", {
1406
1420
  name,
1421
+ // #2908: both id keys for the window — `account_id` is the #2907 twin
1422
+ // (landing in P0's second round), `safe_id` is what a server without it
1423
+ // reads; the request works against either.
1424
+ account_id: safeId,
1407
1425
  safe_id: safeId,
1408
1426
  allowances: [
1409
1427
  {