@haven_ai/cli 0.0.0-dev.202609042048.ad6cc9b → 0.0.0-dev.202609050705.51c3d92
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 +58 -1
- package/dist/cli.cjs +225 -118
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +225 -118
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +207 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +207 -109
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -69,7 +69,10 @@ function helpText() {
|
|
|
69
69
|
"Auth:",
|
|
70
70
|
" login [--email <e>] Sign in (password via prompt or HAVEN_PASSWORD)",
|
|
71
71
|
" logout Clear the saved session",
|
|
72
|
-
" whoami Show the signed-in user",
|
|
72
|
+
" whoami Show the signed-in user, session expiry and API URL",
|
|
73
|
+
"",
|
|
74
|
+
"For agents:",
|
|
75
|
+
" guide Print the agent onboarding runbook (same text as /for-agents.md)",
|
|
73
76
|
"",
|
|
74
77
|
"Read:",
|
|
75
78
|
" wallets list List your Haven wallets",
|
|
@@ -93,11 +96,14 @@ function helpText() {
|
|
|
93
96
|
" contacts add <name> <address> | contacts remove <id>",
|
|
94
97
|
"",
|
|
95
98
|
"Options:",
|
|
96
|
-
" --json
|
|
99
|
+
" --json One JSON value on stdout, prose on stderr, on every",
|
|
100
|
+
" command including refusals: { ok: false, error: { code, message, hint? } }",
|
|
97
101
|
" --yes, -y Skip the confirmation prompt for destructive actions",
|
|
98
102
|
" --api <url> Backend URL (default: HAVEN_API_URL or http://localhost:3001)",
|
|
99
103
|
" --help, --version",
|
|
100
104
|
"",
|
|
105
|
+
"Exit codes: 0 ok \xB7 1 failed \xB7 2 usage \xB7 3 not authenticated \xB7 4 refused \xB7 5 network",
|
|
106
|
+
"",
|
|
101
107
|
"On-chain actions (deploy, budgets, approvers, send) are signed in the",
|
|
102
108
|
"dashboard \u2014 this CLI reads and manages; it never holds your keys."
|
|
103
109
|
].join("\n");
|
|
@@ -240,53 +246,157 @@ function toCsv(headers, rows) {
|
|
|
240
246
|
return lines.join("\r\n");
|
|
241
247
|
}
|
|
242
248
|
|
|
249
|
+
// src/errors.ts
|
|
250
|
+
var EXIT = {
|
|
251
|
+
ok: 0,
|
|
252
|
+
/** Something failed and none of the specific codes below describe it. */
|
|
253
|
+
failed: 1,
|
|
254
|
+
/** The command line was wrong: unknown command, missing argument, bad flag. */
|
|
255
|
+
usage: 2,
|
|
256
|
+
/** No stored session, or the backend rejected the one we have. Log in again. */
|
|
257
|
+
notAuthenticated: 3,
|
|
258
|
+
/** Authenticated, and the backend refused anyway (403, 410, other 4xx). */
|
|
259
|
+
refused: 4,
|
|
260
|
+
/** The backend could not be reached at all. */
|
|
261
|
+
network: 5
|
|
262
|
+
};
|
|
263
|
+
var UsageError = class extends Error {
|
|
264
|
+
hint;
|
|
265
|
+
constructor(message, hint) {
|
|
266
|
+
super(message);
|
|
267
|
+
this.name = "UsageError";
|
|
268
|
+
this.hint = hint;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
function toFailure(err) {
|
|
272
|
+
if (err instanceof UsageError) {
|
|
273
|
+
return { code: "usage", exit: EXIT.usage, message: err.message, hint: err.hint };
|
|
274
|
+
}
|
|
275
|
+
if (err instanceof CliApiError) {
|
|
276
|
+
if (err.status === 0) {
|
|
277
|
+
return {
|
|
278
|
+
code: "network",
|
|
279
|
+
exit: EXIT.network,
|
|
280
|
+
message: err.message,
|
|
281
|
+
hint: "Check the network and `--api`, then retry."
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (err.status === 401) {
|
|
285
|
+
return {
|
|
286
|
+
code: "not_authenticated",
|
|
287
|
+
exit: EXIT.notAuthenticated,
|
|
288
|
+
message: err.message,
|
|
289
|
+
hint: "Run `haven login` (or set HAVEN_EMAIL and HAVEN_PASSWORD)."
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (err.status >= 400 && err.status < 500) {
|
|
293
|
+
return { code: "refused", exit: EXIT.refused, message: err.message };
|
|
294
|
+
}
|
|
295
|
+
return { code: "failed", exit: EXIT.failed, message: err.message };
|
|
296
|
+
}
|
|
297
|
+
return { code: "failed", exit: EXIT.failed, message: err instanceof Error ? err.message : String(err) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/output.ts
|
|
301
|
+
function createOutput(json, out, err) {
|
|
302
|
+
return {
|
|
303
|
+
json,
|
|
304
|
+
data(payload, human) {
|
|
305
|
+
out(json ? JSON.stringify(payload, null, 2) : human());
|
|
306
|
+
},
|
|
307
|
+
text(content, meta) {
|
|
308
|
+
out(json ? JSON.stringify({ ok: true, ...meta, content }, null, 2) : content);
|
|
309
|
+
},
|
|
310
|
+
note(line) {
|
|
311
|
+
if (json) err(line);
|
|
312
|
+
else out(line);
|
|
313
|
+
},
|
|
314
|
+
failure(failure) {
|
|
315
|
+
const body = {
|
|
316
|
+
ok: false,
|
|
317
|
+
error: {
|
|
318
|
+
code: failure.code,
|
|
319
|
+
message: failure.message,
|
|
320
|
+
...failure.hint ? { hint: failure.hint } : {}
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
if (json) out(JSON.stringify(body, null, 2));
|
|
324
|
+
else err(failure.hint ? `${failure.message}
|
|
325
|
+
${failure.hint}` : failure.message);
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/agent-guidance-text.ts
|
|
331
|
+
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.\n2. **HUMAN \u2014 fund it.** USDC on Base, to the address the dashboard shows.\n3. **HUMAN \u2014 create the agent, set its budget**, and paste you the **setup prompt** it hands back.\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 on Base before I can pay for anything \u2014 USDC only, no ETH: Haven sponsors the gas. The dashboard shows the address to send it to; 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";
|
|
332
|
+
|
|
333
|
+
// src/token.ts
|
|
334
|
+
function sessionExpiry(token) {
|
|
335
|
+
const segment = token.split(".")[1];
|
|
336
|
+
if (!segment) return null;
|
|
337
|
+
try {
|
|
338
|
+
const json = Buffer.from(segment.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
|
|
339
|
+
const claims = JSON.parse(json);
|
|
340
|
+
if (typeof claims.exp !== "number" || !Number.isFinite(claims.exp)) return null;
|
|
341
|
+
return new Date(claims.exp * 1e3).toISOString();
|
|
342
|
+
} catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
243
347
|
// src/commands.ts
|
|
244
348
|
var DEFAULT_API = "https://havenbackend-production-8a00.up.railway.app";
|
|
245
|
-
var CLI_VERSION = "0.0.0-dev.
|
|
349
|
+
var CLI_VERSION = "0.0.0-dev.202609050705.51c3d92";
|
|
246
350
|
async function run(argv, deps = {}) {
|
|
351
|
+
const out = deps.out ?? ((l) => process.stdout.write(`${l}
|
|
352
|
+
`));
|
|
353
|
+
const err = deps.err ?? ((l) => process.stderr.write(`${l}
|
|
354
|
+
`));
|
|
355
|
+
const json = argv.includes("--json");
|
|
356
|
+
const o = createOutput(json, out, err);
|
|
247
357
|
const d = {
|
|
248
358
|
sessionStore: deps.sessionStore ?? createSessionStore(),
|
|
249
359
|
makeApi: deps.makeApi ?? ((baseUrl, token) => createCliApi({ baseUrl, token })),
|
|
250
360
|
promptPassword: deps.promptPassword ?? (() => Promise.reject(new Error("No password input available"))),
|
|
251
|
-
out
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
env: deps.env ?? process.env
|
|
361
|
+
out,
|
|
362
|
+
err,
|
|
363
|
+
env: deps.env ?? process.env,
|
|
364
|
+
o
|
|
256
365
|
};
|
|
257
366
|
let args;
|
|
258
367
|
try {
|
|
259
368
|
args = parseArgs(argv);
|
|
260
369
|
} catch (e) {
|
|
261
|
-
d
|
|
262
|
-
return 1;
|
|
370
|
+
return fail(d, new UsageError(e instanceof Error ? e.message : String(e), "Run `haven --help`."));
|
|
263
371
|
}
|
|
264
372
|
if (args.flags.version) {
|
|
265
|
-
|
|
266
|
-
return
|
|
373
|
+
o.data({ version: CLI_VERSION }, () => CLI_VERSION);
|
|
374
|
+
return EXIT.ok;
|
|
267
375
|
}
|
|
268
376
|
if (args.flags.help || !args.command) {
|
|
269
|
-
|
|
270
|
-
return
|
|
377
|
+
o.data({ help: helpText() }, () => helpText());
|
|
378
|
+
return EXIT.ok;
|
|
271
379
|
}
|
|
272
380
|
try {
|
|
273
381
|
return await dispatch(args, d);
|
|
274
382
|
} catch (e) {
|
|
275
|
-
|
|
276
|
-
d.err(e.message);
|
|
277
|
-
return e.status === 401 ? 2 : 1;
|
|
278
|
-
}
|
|
279
|
-
d.err(e instanceof Error ? e.message : String(e));
|
|
280
|
-
return 1;
|
|
383
|
+
return fail(d, e);
|
|
281
384
|
}
|
|
282
385
|
}
|
|
386
|
+
function fail(d, err) {
|
|
387
|
+
const failure = toFailure(err);
|
|
388
|
+
d.o.failure(failure);
|
|
389
|
+
return failure.exit;
|
|
390
|
+
}
|
|
283
391
|
async function dispatch(args, d) {
|
|
284
392
|
const key = args.sub ? `${args.command} ${args.sub}` : args.command;
|
|
285
393
|
switch (key) {
|
|
394
|
+
case "guide":
|
|
395
|
+
return cmdGuide(args, d);
|
|
286
396
|
case "login":
|
|
287
397
|
return cmdLogin(args, d);
|
|
288
398
|
case "logout":
|
|
289
|
-
return cmdLogout(d);
|
|
399
|
+
return cmdLogout(args, d);
|
|
290
400
|
case "whoami":
|
|
291
401
|
return cmdWhoami(args, d);
|
|
292
402
|
case "wallets list":
|
|
@@ -324,8 +434,7 @@ async function dispatch(args, d) {
|
|
|
324
434
|
case "contacts remove":
|
|
325
435
|
return cmdContactsRemove(args, d);
|
|
326
436
|
default:
|
|
327
|
-
|
|
328
|
-
return 1;
|
|
437
|
+
throw new UsageError(`Unknown command: ${key}.`, "Run `haven --help` for the command list.");
|
|
329
438
|
}
|
|
330
439
|
}
|
|
331
440
|
function baseUrlFor(args, d, session) {
|
|
@@ -333,40 +442,52 @@ function baseUrlFor(args, d, session) {
|
|
|
333
442
|
}
|
|
334
443
|
async function authed(args, d) {
|
|
335
444
|
const session = await d.sessionStore.load();
|
|
336
|
-
if (!session) throw new CliApiError("Not authenticated.
|
|
445
|
+
if (!session) throw new CliApiError("Not authenticated.", 401);
|
|
337
446
|
return { session, api: d.makeApi(baseUrlFor(args, d, session), session.token) };
|
|
338
447
|
}
|
|
339
|
-
function emit(d,
|
|
340
|
-
d.
|
|
448
|
+
function emit(d, _json, data, human) {
|
|
449
|
+
d.o.data(data, human);
|
|
450
|
+
}
|
|
451
|
+
async function cmdGuide(_args, d) {
|
|
452
|
+
d.o.data({ ok: true, format: "markdown", content: HAVEN_AGENT_RUNBOOK_MD }, () => HAVEN_AGENT_RUNBOOK_MD);
|
|
453
|
+
return EXIT.ok;
|
|
341
454
|
}
|
|
342
455
|
async function cmdLogin(args, d) {
|
|
343
456
|
const email = args.flags.email ?? d.env.HAVEN_EMAIL;
|
|
344
457
|
if (!email) {
|
|
345
|
-
|
|
346
|
-
return 1;
|
|
458
|
+
throw new UsageError("An email is required.", "Pass --email <address>, or set HAVEN_EMAIL.");
|
|
347
459
|
}
|
|
348
460
|
const password = d.env.HAVEN_PASSWORD ?? await d.promptPassword();
|
|
349
461
|
if (!password) {
|
|
350
|
-
|
|
351
|
-
return 1;
|
|
462
|
+
throw new UsageError("A password is required.", "Set HAVEN_PASSWORD for a non-interactive run.");
|
|
352
463
|
}
|
|
353
464
|
const baseUrl = baseUrlFor(args, d, null);
|
|
354
465
|
const api = d.makeApi(baseUrl);
|
|
355
466
|
const res = await api.post("/auth/login", { email, password });
|
|
356
467
|
await d.sessionStore.save({ token: res.token, apiBaseUrl: baseUrl, user: res.user });
|
|
357
|
-
emit(
|
|
358
|
-
|
|
468
|
+
emit(
|
|
469
|
+
d,
|
|
470
|
+
args.flags.json,
|
|
471
|
+
{ ok: true, email: res.user.email, expires_at: sessionExpiry(res.token), user: res.user, apiBaseUrl: baseUrl },
|
|
472
|
+
() => `Signed in as ${res.user.email}.`
|
|
473
|
+
);
|
|
474
|
+
return EXIT.ok;
|
|
359
475
|
}
|
|
360
|
-
async function cmdLogout(d) {
|
|
476
|
+
async function cmdLogout(args, d) {
|
|
361
477
|
await d.sessionStore.clear();
|
|
362
|
-
d.
|
|
363
|
-
return
|
|
478
|
+
emit(d, args.flags.json, { ok: true, signed_out: true }, () => "Signed out.");
|
|
479
|
+
return EXIT.ok;
|
|
364
480
|
}
|
|
365
481
|
async function cmdWhoami(args, d) {
|
|
366
|
-
const { api } = await authed(args, d);
|
|
482
|
+
const { session, api } = await authed(args, d);
|
|
367
483
|
const user = await api.get("/auth/me");
|
|
368
|
-
emit(
|
|
369
|
-
|
|
484
|
+
emit(
|
|
485
|
+
d,
|
|
486
|
+
args.flags.json,
|
|
487
|
+
{ ...user, id: user.id, email: user.email, expires_at: sessionExpiry(session.token), api_url: session.apiBaseUrl },
|
|
488
|
+
() => `${user.email}${user.name ? ` (${user.name})` : ""}`
|
|
489
|
+
);
|
|
490
|
+
return EXIT.ok;
|
|
370
491
|
}
|
|
371
492
|
async function cmdWalletsList(args, d) {
|
|
372
493
|
const { api } = await authed(args, d);
|
|
@@ -380,15 +501,15 @@ async function cmdWalletsList(args, d) {
|
|
|
380
501
|
safes.map((s) => [s.name, chainName(s.chain_id), truncateAddress(s.safe_address), s.is_default ? "\u2713" : ""])
|
|
381
502
|
)
|
|
382
503
|
);
|
|
383
|
-
return
|
|
504
|
+
return EXIT.ok;
|
|
384
505
|
}
|
|
385
506
|
async function cmdWalletsBalances(args, d) {
|
|
386
507
|
const { api } = await authed(args, d);
|
|
387
508
|
const { safes } = await api.get("/user/safes");
|
|
388
509
|
const safe = pickSafe(safes, args.flags.safe);
|
|
389
510
|
if (!safe) {
|
|
390
|
-
|
|
391
|
-
|
|
511
|
+
if (args.flags.safe) throw new UsageError(`No wallet matches "${args.flags.safe}".`);
|
|
512
|
+
throw new CliApiError("No Haven wallet found.", 404);
|
|
392
513
|
}
|
|
393
514
|
const { balances } = await api.get(
|
|
394
515
|
`/balances/${safe.safe_address}?chain_id=${safe.chain_id}`
|
|
@@ -402,7 +523,7 @@ async function cmdWalletsBalances(args, d) {
|
|
|
402
523
|
balances.length === 0 ? " (no balances)" : table(["TOKEN", "BALANCE"], balances.map((b) => [b.symbol, b.formatted]))
|
|
403
524
|
].join("\n")
|
|
404
525
|
);
|
|
405
|
-
return
|
|
526
|
+
return EXIT.ok;
|
|
406
527
|
}
|
|
407
528
|
function pickSafe(safes, ref) {
|
|
408
529
|
if (!ref) return safes.find((s) => s.is_default) ?? safes[0];
|
|
@@ -421,14 +542,11 @@ async function cmdAgentsList(args, d) {
|
|
|
421
542
|
agents.map((a) => [a.id, a.name, a.status, budgetSummary(a.allowances)])
|
|
422
543
|
)
|
|
423
544
|
);
|
|
424
|
-
return
|
|
545
|
+
return EXIT.ok;
|
|
425
546
|
}
|
|
426
547
|
async function cmdAgentsShow(args, d) {
|
|
427
548
|
const id = args.positionals[0];
|
|
428
|
-
if (!id)
|
|
429
|
-
d.err("Usage: haven agents show <id>");
|
|
430
|
-
return 1;
|
|
431
|
-
}
|
|
549
|
+
if (!id) throw new UsageError("Usage: haven agents show <id>");
|
|
432
550
|
const { api } = await authed(args, d);
|
|
433
551
|
const agent = await api.get(`/agents/${id}`);
|
|
434
552
|
emit(
|
|
@@ -441,14 +559,11 @@ async function cmdAgentsShow(args, d) {
|
|
|
441
559
|
`budget: ${budgetSummary(agent.allowances)}`
|
|
442
560
|
].join("\n")
|
|
443
561
|
);
|
|
444
|
-
return
|
|
562
|
+
return EXIT.ok;
|
|
445
563
|
}
|
|
446
564
|
async function cmdBudgetShow(args, d) {
|
|
447
565
|
const id = args.positionals[0];
|
|
448
|
-
if (!id)
|
|
449
|
-
d.err("Usage: haven budget show <agentId>");
|
|
450
|
-
return 1;
|
|
451
|
-
}
|
|
566
|
+
if (!id) throw new UsageError("Usage: haven budget show <agentId>");
|
|
452
567
|
const { api } = await authed(args, d);
|
|
453
568
|
const agent = await api.get(`/agents/${id}`);
|
|
454
569
|
const allowances = agent.allowances ?? [];
|
|
@@ -461,7 +576,7 @@ async function cmdBudgetShow(args, d) {
|
|
|
461
576
|
allowances.map((a) => [a.token_symbol, a.allowance_amount, resetLabel(a.reset_period_min)])
|
|
462
577
|
)
|
|
463
578
|
);
|
|
464
|
-
return
|
|
579
|
+
return EXIT.ok;
|
|
465
580
|
}
|
|
466
581
|
function budgetSummary(allowances) {
|
|
467
582
|
if (!allowances || allowances.length === 0) return "\u2014";
|
|
@@ -476,75 +591,64 @@ function resetLabel(mins) {
|
|
|
476
591
|
}
|
|
477
592
|
async function cmdAgentLifecycle(args, d, action) {
|
|
478
593
|
const id = args.positionals[0];
|
|
479
|
-
if (!id) {
|
|
480
|
-
d.err(`Usage: haven agents ${action} <id>`);
|
|
481
|
-
return 1;
|
|
482
|
-
}
|
|
594
|
+
if (!id) throw new UsageError(`Usage: haven agents ${action} <id>`);
|
|
483
595
|
const { api } = await authed(args, d);
|
|
484
596
|
await api.post(`/agents/${id}/${action}`);
|
|
485
|
-
|
|
486
|
-
|
|
597
|
+
const status = action === "pause" ? "paused" : "resumed";
|
|
598
|
+
emit(d, args.flags.json, { ok: true, agent_id: id, status }, () => `Agent ${id} ${status}.`);
|
|
599
|
+
return EXIT.ok;
|
|
487
600
|
}
|
|
488
601
|
async function cmdAgentRevoke(args, d) {
|
|
489
602
|
const id = args.positionals[0];
|
|
490
|
-
if (!id)
|
|
491
|
-
d.err("Usage: haven agents revoke <id> --yes");
|
|
492
|
-
return 1;
|
|
493
|
-
}
|
|
603
|
+
if (!id) throw new UsageError("Usage: haven agents revoke <id> --yes");
|
|
494
604
|
if (!args.flags.yes) {
|
|
495
|
-
|
|
496
|
-
|
|
605
|
+
throw new UsageError(
|
|
606
|
+
`This permanently revokes agent ${id}.`,
|
|
607
|
+
"Re-run with --yes to confirm. Revoke is terminal \u2014 the agent cannot go back to active."
|
|
608
|
+
);
|
|
497
609
|
}
|
|
498
610
|
const { api } = await authed(args, d);
|
|
499
611
|
await api.post(`/agents/${id}/revoke`);
|
|
500
|
-
|
|
501
|
-
|
|
612
|
+
emit(
|
|
613
|
+
d,
|
|
614
|
+
args.flags.json,
|
|
615
|
+
{ ok: true, agent_id: id, status: "revoked" },
|
|
616
|
+
() => `Agent ${id} revoked. To also remove its on-chain allowance, use the dashboard.`
|
|
617
|
+
);
|
|
618
|
+
return EXIT.ok;
|
|
502
619
|
}
|
|
503
620
|
async function cmdAgentRotateKey(args, d) {
|
|
504
621
|
const id = args.positionals[0];
|
|
505
|
-
if (!id)
|
|
506
|
-
d.err("Usage: haven agents rotate-key <id>");
|
|
507
|
-
return 1;
|
|
508
|
-
}
|
|
622
|
+
if (!id) throw new UsageError("Usage: haven agents rotate-key <id>");
|
|
509
623
|
const { api } = await authed(args, d);
|
|
510
624
|
const res = await api.post(`/agents/${id}/rotate-key`);
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
d.out("New API key (shown once \u2014 store it now; the old key stops working):");
|
|
515
|
-
d.out(res.api_key);
|
|
516
|
-
}
|
|
517
|
-
return 0;
|
|
625
|
+
d.o.note("New API key (shown once \u2014 store it now; the old key stops working):");
|
|
626
|
+
emit(d, args.flags.json, res, () => res.api_key);
|
|
627
|
+
return EXIT.ok;
|
|
518
628
|
}
|
|
519
629
|
async function cmdAgentRename(args, d) {
|
|
520
630
|
const [id, ...nameParts] = args.positionals;
|
|
521
631
|
const name = nameParts.join(" ").trim();
|
|
522
|
-
if (!id || !name)
|
|
523
|
-
d.err("Usage: haven agents rename <id> <name>");
|
|
524
|
-
return 1;
|
|
525
|
-
}
|
|
632
|
+
if (!id || !name) throw new UsageError("Usage: haven agents rename <id> <name>");
|
|
526
633
|
const { api } = await authed(args, d);
|
|
527
634
|
await api.put(`/agents/${id}`, { name });
|
|
528
|
-
d.
|
|
529
|
-
return
|
|
635
|
+
emit(d, args.flags.json, { ok: true, agent_id: id, name }, () => `Agent ${id} renamed to "${name}".`);
|
|
636
|
+
return EXIT.ok;
|
|
530
637
|
}
|
|
531
638
|
async function cmdWalletRename(args, d) {
|
|
532
639
|
const [id, ...nameParts] = args.positionals;
|
|
533
640
|
const name = nameParts.join(" ").trim();
|
|
534
|
-
if (!id || !name)
|
|
535
|
-
d.err("Usage: haven wallets rename <id> <name>");
|
|
536
|
-
return 1;
|
|
537
|
-
}
|
|
641
|
+
if (!id || !name) throw new UsageError("Usage: haven wallets rename <id> <name>");
|
|
538
642
|
const { api } = await authed(args, d);
|
|
539
643
|
await api.put(`/user/safes/${id}`, { name });
|
|
540
|
-
d.
|
|
541
|
-
return
|
|
644
|
+
emit(d, args.flags.json, { ok: true, safe_id: id, name }, () => `Wallet ${id} renamed to "${name}".`);
|
|
645
|
+
return EXIT.ok;
|
|
542
646
|
}
|
|
543
647
|
async function resolveSafeId(args, api) {
|
|
544
648
|
if (!args.flags.safe) return void 0;
|
|
545
649
|
const { safes } = await api.get("/user/safes");
|
|
546
650
|
const safe = pickSafe(safes, args.flags.safe);
|
|
547
|
-
if (!safe) throw new
|
|
651
|
+
if (!safe) throw new UsageError(`No wallet matches "${args.flags.safe}".`);
|
|
548
652
|
return safe.id;
|
|
549
653
|
}
|
|
550
654
|
async function cmdActivityList(args, d) {
|
|
@@ -573,7 +677,7 @@ async function cmdActivityList(args, d) {
|
|
|
573
677
|
])
|
|
574
678
|
)
|
|
575
679
|
);
|
|
576
|
-
return
|
|
680
|
+
return EXIT.ok;
|
|
577
681
|
}
|
|
578
682
|
async function cmdActivityExport(args, d) {
|
|
579
683
|
if (args.flags.format === "sie") return exportSie(args, d);
|
|
@@ -615,8 +719,8 @@ async function cmdActivityExport(args, d) {
|
|
|
615
719
|
t.hash,
|
|
616
720
|
t.chainId != null ? String(t.chainId) : ""
|
|
617
721
|
]);
|
|
618
|
-
d.
|
|
619
|
-
return
|
|
722
|
+
d.o.text(toCsv(headers, rows), { format: "csv", rows: rows.length });
|
|
723
|
+
return EXIT.ok;
|
|
620
724
|
}
|
|
621
725
|
function exportType(t) {
|
|
622
726
|
if (t.activityType === "delegate_sweep") return "allowance funding";
|
|
@@ -636,8 +740,8 @@ async function exportSie(args, d) {
|
|
|
636
740
|
if (args.flags.to) params.set("to", args.flags.to);
|
|
637
741
|
if (args.flags.company) params.set("company", args.flags.company);
|
|
638
742
|
const content = await api.getText(`/accounting/export?${params.toString()}`);
|
|
639
|
-
d.
|
|
640
|
-
return
|
|
743
|
+
d.o.text(content, { format: "sie" });
|
|
744
|
+
return EXIT.ok;
|
|
641
745
|
}
|
|
642
746
|
async function cmdCatalogList(args, d) {
|
|
643
747
|
const { api } = await authed(args, d);
|
|
@@ -651,7 +755,7 @@ async function cmdCatalogList(args, d) {
|
|
|
651
755
|
entries.map((e) => [e.name, e.category, e.rail, e.price_display ?? "\u2014", e.status])
|
|
652
756
|
)
|
|
653
757
|
);
|
|
654
|
-
return
|
|
758
|
+
return EXIT.ok;
|
|
655
759
|
}
|
|
656
760
|
async function cmdContactsList(args, d) {
|
|
657
761
|
const { api } = await authed(args, d);
|
|
@@ -662,30 +766,24 @@ async function cmdContactsList(args, d) {
|
|
|
662
766
|
contacts,
|
|
663
767
|
() => contacts.length === 0 ? "No contacts yet." : table(["ID", "NAME", "ADDRESS"], contacts.map((c) => [c.id, c.name, truncateAddress(c.address)]))
|
|
664
768
|
);
|
|
665
|
-
return
|
|
769
|
+
return EXIT.ok;
|
|
666
770
|
}
|
|
667
771
|
async function cmdContactsAdd(args, d) {
|
|
668
772
|
const [address, ...nameParts] = [...args.positionals].reverse();
|
|
669
773
|
const name = nameParts.reverse().join(" ").trim();
|
|
670
|
-
if (!name || !address)
|
|
671
|
-
d.err("Usage: haven contacts add <name> <address>");
|
|
672
|
-
return 1;
|
|
673
|
-
}
|
|
774
|
+
if (!name || !address) throw new UsageError("Usage: haven contacts add <name> <address>");
|
|
674
775
|
const { api } = await authed(args, d);
|
|
675
776
|
const contact = await api.post("/contacts", { name, address });
|
|
676
777
|
emit(d, args.flags.json, contact, () => `Added contact "${contact.name}" (${truncateAddress(contact.address)}).`);
|
|
677
|
-
return
|
|
778
|
+
return EXIT.ok;
|
|
678
779
|
}
|
|
679
780
|
async function cmdContactsRemove(args, d) {
|
|
680
781
|
const id = args.positionals[0];
|
|
681
|
-
if (!id)
|
|
682
|
-
d.err("Usage: haven contacts remove <id>");
|
|
683
|
-
return 1;
|
|
684
|
-
}
|
|
782
|
+
if (!id) throw new UsageError("Usage: haven contacts remove <id>");
|
|
685
783
|
const { api } = await authed(args, d);
|
|
686
784
|
await api.del(`/contacts/${id}`);
|
|
687
|
-
d.
|
|
688
|
-
return
|
|
785
|
+
emit(d, args.flags.json, { ok: true, contact_id: id, removed: true }, () => `Contact ${id} removed.`);
|
|
786
|
+
return EXIT.ok;
|
|
689
787
|
}
|
|
690
788
|
|
|
691
789
|
export { CliApiError, createCliApi, createSessionStore, helpText, parseArgs, run, sessionPath };
|