@homespunapps/cli 1.0.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.
@@ -0,0 +1,37 @@
1
+ // `homespun attachment delete <attachment-id>` — soft-delete a attachment.
2
+ import { assertKnownFlags } from "../argv.js";
3
+ import { makeClient } from "../config.js";
4
+ import { fail, failFromError, printJson } from "../output.js";
5
+ const KNOWN_FLAGS = [];
6
+ const KNOWN_BOOLS = [];
7
+ export const blobDeleteHelp = `homespun attachment delete — soft-delete a attachment
8
+
9
+ Usage:
10
+ homespun attachment delete <attachment-id> [options]
11
+
12
+ Marks the attachment as deleted (DELETE /v1/attachments/:id). Idempotent: deleting an
13
+ already-deleted attachment still returns success. Tokens minted against this attachment
14
+ become unusable.
15
+
16
+ Options:
17
+ --url <url> Relay base URL (overrides HOMESPUN_URL).
18
+ --api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
19
+ -h, --help Show this help.
20
+
21
+ Output (stdout, JSON):
22
+ { attachment_id, deleted: true }`;
23
+ export async function runBlobDelete(args) {
24
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "homespun attachment delete");
25
+ const attachmentId = args.positionals[0];
26
+ if (!attachmentId) {
27
+ fail("missing <attachment-id> — 'homespun attachment delete <attachment-id>'", "invalid_args");
28
+ }
29
+ const client = makeClient(args);
30
+ try {
31
+ const r = await client.deleteBlob(attachmentId);
32
+ printJson({ attachment_id: attachmentId, ...r });
33
+ }
34
+ catch (e) {
35
+ failFromError(e);
36
+ }
37
+ }
@@ -0,0 +1,53 @@
1
+ // `homespun attachment download <attachment-id>` — fetch attachment bytes by id.
2
+ import { writeFileSync } from "node:fs";
3
+ import { assertKnownFlags } from "../argv.js";
4
+ import { makeClient } from "../config.js";
5
+ import { fail, failFromError, printJson } from "../output.js";
6
+ const KNOWN_FLAGS = ["out"];
7
+ const KNOWN_BOOLS = [];
8
+ export const blobDownloadHelp = `homespun attachment download — fetch a attachment's bytes
9
+
10
+ Usage:
11
+ homespun attachment download <attachment-id> [--out <path>] [options]
12
+
13
+ GETs the attachment bytes. With --out <path> the bytes are written to that file and
14
+ a JSON summary is printed on stdout; without --out the bytes are written to
15
+ stdout verbatim (useful for piping into another tool — but binary on a TTY
16
+ is rarely useful).
17
+
18
+ Options:
19
+ --out <path> Write bytes to <path> instead of stdout.
20
+ --url <url> Relay base URL (overrides HOMESPUN_URL).
21
+ --api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
22
+ -h, --help Show this help.
23
+
24
+ Output:
25
+ Without --out: raw bytes to stdout.
26
+ With --out: { attachment_id, written: <path>, bytes: <n> } to stdout.`;
27
+ export async function runBlobDownload(args) {
28
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "homespun attachment download");
29
+ const attachmentId = args.positionals[0];
30
+ if (!attachmentId) {
31
+ fail("missing <attachment-id> — 'homespun attachment download <attachment-id>'", "invalid_args");
32
+ }
33
+ const out = args.flags.get("out");
34
+ const client = makeClient(args);
35
+ try {
36
+ const buf = await client.downloadBlob(attachmentId);
37
+ if (out) {
38
+ writeFileSync(out, Buffer.from(buf));
39
+ printJson({
40
+ attachment_id: attachmentId,
41
+ written: out,
42
+ bytes: buf.byteLength,
43
+ });
44
+ }
45
+ else {
46
+ // Binary to stdout — useful for piping into another tool.
47
+ process.stdout.write(Buffer.from(buf));
48
+ }
49
+ }
50
+ catch (e) {
51
+ failFromError(e);
52
+ }
53
+ }
@@ -0,0 +1,50 @@
1
+ // `homespun attachment list` — enumerate YOUR agent's attachments.
2
+ //
3
+ // Lists attachments owned by the calling agent, newest first. Soft-deleted attachments
4
+ // are excluded; tokens are not enumerated here (use 'homespun attachment token list
5
+ // <attachment-id>' for that).
6
+ import { assertKnownFlags } from "../argv.js";
7
+ import { makeClient } from "../config.js";
8
+ import { fail, printJson, failFromError } from "../output.js";
9
+ const KNOWN_FLAGS = ["cursor", "limit"];
10
+ const KNOWN_BOOLS = [];
11
+ export const blobListHelp = `homespun attachment list — enumerate YOUR agent's attachments
12
+
13
+ Usage:
14
+ homespun attachment list [--cursor <token>] [--limit <n>] [options]
15
+
16
+ Returns the agent's non-deleted attachments (newest first). Paginated via opaque
17
+ cursor: when next_cursor is non-null in the response, pass it back as
18
+ --cursor to get the next page.
19
+
20
+ Options:
21
+ --cursor <token> Opaque pagination cursor from a prior response.
22
+ --limit <n> Page size (1..100). Defaults to the relay default
23
+ (50).
24
+ --url <url> Relay base URL (overrides HOMESPUN_URL).
25
+ --api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
26
+ -h, --help Show this help.
27
+
28
+ Output (stdout, JSON):
29
+ { items: AttachmentRef[], next_cursor: string | null }`;
30
+ export async function runBlobList(args) {
31
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "homespun attachment list");
32
+ const cursor = args.flags.get("cursor");
33
+ const limitRaw = args.flags.get("limit");
34
+ let limit;
35
+ if (limitRaw !== undefined) {
36
+ const n = Number(limitRaw);
37
+ if (!Number.isInteger(n) || n < 1 || n > 100) {
38
+ fail("--limit must be an integer in 1..100", "invalid_args");
39
+ }
40
+ limit = n;
41
+ }
42
+ const client = makeClient(args);
43
+ try {
44
+ const r = await client.listBlobs({ cursor, limit });
45
+ printJson(r);
46
+ }
47
+ catch (e) {
48
+ failFromError(e);
49
+ }
50
+ }
@@ -0,0 +1,37 @@
1
+ // `homespun attachment show <attachment-id>` — print a attachment's metadata.
2
+ import { assertKnownFlags } from "../argv.js";
3
+ import { makeClient } from "../config.js";
4
+ import { fail, failFromError, printJson } from "../output.js";
5
+ const KNOWN_FLAGS = [];
6
+ const KNOWN_BOOLS = [];
7
+ export const blobShowHelp = `homespun attachment show — print a attachment's metadata (no bytes)
8
+
9
+ Usage:
10
+ homespun attachment show <attachment-id> [options]
11
+
12
+ Looks up the attachment by id and prints its AttachmentRef metadata — owner, scope,
13
+ mime, size, sha256, etc. Does NOT download the bytes; use 'homespun attachment
14
+ download' for that.
15
+
16
+ Options:
17
+ --url <url> Relay base URL (overrides HOMESPUN_URL).
18
+ --api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
19
+ -h, --help Show this help.
20
+
21
+ Output (stdout, JSON):
22
+ AttachmentRef`;
23
+ export async function runBlobShow(args) {
24
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "homespun attachment show");
25
+ const attachmentId = args.positionals[0];
26
+ if (!attachmentId) {
27
+ fail("missing <attachment-id> — 'homespun attachment show <attachment-id>'", "invalid_args");
28
+ }
29
+ const client = makeClient(args);
30
+ try {
31
+ const ref = await client.getBlob(attachmentId);
32
+ printJson(ref);
33
+ }
34
+ catch (e) {
35
+ failFromError(e);
36
+ }
37
+ }
@@ -0,0 +1,133 @@
1
+ // `homespun attachment token <mint|revoke|list>` — capability URLs for a attachment.
2
+ //
3
+ // A capability URL (/b/<token>) is a participant-facing way to fetch a attachment
4
+ // without holding the agent's API key. Tokens are minted per-attachment, can be
5
+ // time-bound (--ttl) and/or single-use (--once), and are stored hashed on
6
+ // the relay — the plaintext token is returned ONCE on 'mint' and cannot be
7
+ // recovered.
8
+ //
9
+ // This file is a sub-noun dispatcher under `homespun attachment`. The attachment dispatcher
10
+ // hands us a ParsedArgs whose positionals[0] is "token" (our sub-noun
11
+ // marker), so we read the verb from positionals[1] and the args from
12
+ // positionals[2..]. Mirrors how participant.ts dispatches under `app
13
+ // app participant`.
14
+ import { assertKnownFlags } from "../argv.js";
15
+ import { makeClient } from "../config.js";
16
+ import { fail, failFromError, printJson } from "../output.js";
17
+ const MINT_FLAGS = ["ttl"];
18
+ const MINT_BOOLS = ["once"];
19
+ const NO_FLAGS = [];
20
+ const NO_BOOLS = [];
21
+ export const blobTokenHelp = `homespun attachment token — manage a attachment's capability URLs
22
+
23
+ Capability URLs let a participant (or any browser holding the URL) fetch a
24
+ attachment without the agent's API key. Tokens are stored HASHED on the relay; the
25
+ plaintext token is returned only ONCE from 'mint' — save the response before
26
+ delivering the URL.
27
+
28
+ Usage:
29
+ homespun attachment token <verb> <args>
30
+
31
+ Verbs:
32
+ mint <attachment-id> Mint a /b/<token> capability URL for one attachment.
33
+ Optional: --ttl <seconds> (defaults by scope:
34
+ 30d app / 24h agent; the caller can only
35
+ shorten), --once (token self-deletes on
36
+ first successful GET). Returns { token, url,
37
+ expires_at, ... } — ONCE.
38
+
39
+ revoke <attachment-id> <token-id>
40
+ Invalidate one previously-minted token by id.
41
+ Idempotent: revoking twice still returns success.
42
+
43
+ list <attachment-id> Enumerate the tokens minted against one attachment,
44
+ including revoked rows (for audit). Returns
45
+ { attachment_id, items: [...] } where each item carries
46
+ { token_id, token_prefix, expires_at, once,
47
+ created_at, last_used_at, use_count, revoked_at }.
48
+ The token plaintext is NEVER returned.
49
+
50
+ Options:
51
+ --ttl <seconds> (mint) per-token TTL; clamped by scope default.
52
+ --once (mint) token self-deletes on first GET.
53
+ --url <url> Relay base URL (overrides HOMESPUN_URL).
54
+ --api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
55
+ -h, --help Show this help.
56
+
57
+ Output: stdout is machine-readable JSON.`;
58
+ async function runBlobTokenMint(args) {
59
+ assertKnownFlags(args, MINT_FLAGS, MINT_BOOLS, "homespun attachment token mint");
60
+ const attachmentId = args.positionals[1];
61
+ if (!attachmentId) {
62
+ fail("missing <attachment-id> — 'homespun attachment token mint <attachment-id>'", "invalid_args");
63
+ }
64
+ const ttlRaw = args.flags.get("ttl");
65
+ const ttl = ttlRaw === undefined ? undefined : Number(ttlRaw);
66
+ if (ttlRaw !== undefined && (!Number.isInteger(ttl) || ttl <= 0)) {
67
+ fail("--ttl must be a positive integer (seconds)", "invalid_args");
68
+ }
69
+ const client = makeClient(args);
70
+ try {
71
+ const r = await client.mintBlobToken(attachmentId, {
72
+ ttlSeconds: ttl,
73
+ once: args.bools.has("once"),
74
+ });
75
+ printJson(r);
76
+ }
77
+ catch (e) {
78
+ failFromError(e);
79
+ }
80
+ }
81
+ async function runBlobTokenRevoke(args) {
82
+ assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "homespun attachment token revoke");
83
+ const attachmentId = args.positionals[1];
84
+ const tokenId = args.positionals[2];
85
+ if (!attachmentId || !tokenId) {
86
+ fail("missing arguments — 'homespun attachment token revoke <attachment-id> <token-id>'", "invalid_args");
87
+ }
88
+ const client = makeClient(args);
89
+ try {
90
+ const r = await client.revokeBlobToken(attachmentId, tokenId);
91
+ printJson(r);
92
+ }
93
+ catch (e) {
94
+ failFromError(e);
95
+ }
96
+ }
97
+ async function runBlobTokenList(args) {
98
+ assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "homespun attachment token list");
99
+ const attachmentId = args.positionals[1];
100
+ if (!attachmentId) {
101
+ fail("missing <attachment-id> — 'homespun attachment token list <attachment-id>'", "invalid_args");
102
+ }
103
+ const client = makeClient(args);
104
+ try {
105
+ const r = await client.listBlobTokens(attachmentId);
106
+ printJson(r);
107
+ }
108
+ catch (e) {
109
+ failFromError(e);
110
+ }
111
+ }
112
+ export async function runBlobToken(args) {
113
+ // positionals[0] is the verb (mint | revoke | list), positionals[1..] are
114
+ // the verb's args. (The attachment.ts dispatcher already shifted off the "token"
115
+ // marker before calling us.)
116
+ const verb = args.positionals[0];
117
+ switch (verb) {
118
+ case "mint":
119
+ await runBlobTokenMint(args);
120
+ break;
121
+ case "revoke":
122
+ await runBlobTokenRevoke(args);
123
+ break;
124
+ case "list":
125
+ await runBlobTokenList(args);
126
+ break;
127
+ case undefined:
128
+ fail("missing verb — usage: homespun attachment token <mint|revoke|list> (run 'homespun attachment token --help')", "invalid_args");
129
+ break;
130
+ default:
131
+ fail(`unknown token verb '${verb}' — expected mint|revoke|list (run 'homespun attachment token --help')`, "invalid_args");
132
+ }
133
+ }
@@ -0,0 +1,65 @@
1
+ // `homespun attachment upload` — POST /v1/attachments (multipart), two scopes.
2
+ import { readFileSync } from "node:fs";
3
+ import { basename } from "node:path";
4
+ import { assertKnownFlags } from "../argv.js";
5
+ import { makeClient } from "../config.js";
6
+ import { fail, failFromError, printJson } from "../output.js";
7
+ const KNOWN_FLAGS = ["file", "scope", "app-id", "filename", "mime"];
8
+ const KNOWN_BOOLS = [];
9
+ export const blobUploadHelp = `homespun attachment upload — upload a local file as a attachment
10
+
11
+ Usage:
12
+ homespun attachment upload --file <path> [options]
13
+
14
+ Required:
15
+ --file <path> Local file to upload.
16
+
17
+ Scope (default: agent):
18
+ --scope <s> "agent" | "app".
19
+ --app-id <id> Required when --scope=app.
20
+
21
+ Optional:
22
+ --filename <name> Display filename (otherwise basename of --file).
23
+ --mime <type> Declared Content-Type. The relay sniffs the bytes
24
+ regardless — this is advisory.
25
+ --url <url> Relay base URL (overrides HOMESPUN_URL).
26
+ --api-key <key> Agent API key (overrides HOMESPUN_API_KEY).
27
+ -h, --help Show this help.
28
+
29
+ Output (stdout, JSON):
30
+ AttachmentRef — { attachment_id, scope, mime, size, sha256, ... }`;
31
+ export async function runBlobUpload(args) {
32
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "homespun attachment upload");
33
+ const filePath = args.flags.get("file");
34
+ if (!filePath) {
35
+ fail("missing --file <path> — 'homespun attachment upload' requires a local file to upload", "invalid_args");
36
+ }
37
+ let bytes;
38
+ try {
39
+ bytes = readFileSync(filePath);
40
+ }
41
+ catch (e) {
42
+ fail(`failed to read --file '${filePath}': ${e instanceof Error ? e.message : String(e)}`, "invalid_args");
43
+ }
44
+ const scopeRaw = args.flags.get("scope") ?? "agent";
45
+ if (scopeRaw !== "agent" && scopeRaw !== "app") {
46
+ fail(`unknown --scope '${scopeRaw}', expected one of: agent, app`, "invalid_args");
47
+ }
48
+ const scope = scopeRaw;
49
+ if (scope === "app" && !args.flags.get("app-id")) {
50
+ fail("--scope=app requires --app-id <id>", "invalid_args");
51
+ }
52
+ const client = makeClient(args);
53
+ try {
54
+ const ref = await client.uploadBlob(bytes, {
55
+ scope,
56
+ appId: args.flags.get("app-id"),
57
+ filename: args.flags.get("filename") ?? basename(filePath),
58
+ mime: args.flags.get("mime"),
59
+ });
60
+ printJson(ref);
61
+ }
62
+ catch (e) {
63
+ failFromError(e);
64
+ }
65
+ }
@@ -0,0 +1,133 @@
1
+ // `homespun attachment` — manage binary attachments (attachments) on the relay.
2
+ //
3
+ // A attachment is a typed binary file (image, PDF, audio, video, etc.) owned by an
4
+ // agent and optionally bound to an App. Pages reference attachments
5
+ // by id with `format: homespun-attachment-id`; participants can fetch a attachment through a
6
+ // minted capability URL (/b/<token>) without needing the agent's API key.
7
+ //
8
+ // This file is a thin dispatcher — each verb's actual logic lives in its own
9
+ // file (attachment-upload.ts, attachment-download.ts, attachment-show.ts, attachment-delete.ts) and
10
+ // the token sub-noun is dispatched via attachment-token.ts.
11
+ //
12
+ // Most attachment verbs read their primary positional (the attachment_id) at
13
+ // positionals[0]; we slice off our own verb before delegating so each verb
14
+ // runner doesn't need to know it was reached through `homespun attachment`.
15
+ import { runBlobUpload, blobUploadHelp } from "./attachment-upload.js";
16
+ import { runBlobDownload, blobDownloadHelp } from "./attachment-download.js";
17
+ import { runBlobShow, blobShowHelp } from "./attachment-show.js";
18
+ import { runBlobList, blobListHelp } from "./attachment-list.js";
19
+ import { runBlobDelete, blobDeleteHelp } from "./attachment-delete.js";
20
+ import { runBlobToken, blobTokenHelp } from "./attachment-token.js";
21
+ import { fail } from "../output.js";
22
+ export const blobHelp = `homespun attachment — manage attachments (binary attachments) on the relay
23
+
24
+ A attachment is a typed binary file (image, PDF, audio, video, ...) the agent has
25
+ uploaded to the relay. Blobs are scoped:
26
+
27
+ agent reusable across the agent's apps (default)
28
+ app bound to one App; deleted with it
29
+
30
+ Pages reference attachments by id (the relay's schema validates the id with
31
+ \`format: homespun-attachment-id\`). For a participant-facing URL that bypasses the
32
+ agent's API key, mint a token with 'homespun attachment token mint'.
33
+
34
+ Usage:
35
+ homespun attachment <verb> [options]
36
+
37
+ Verbs:
38
+ upload Upload a local file. Required: --file. Optional:
39
+ --scope, --app-id, --filename, --mime. Prints
40
+ { attachment_id, scope, mime, size, sha256, ... }.
41
+
42
+ download <attachment-id> Download a attachment by id. Use --out <path> to write a
43
+ file (default: writes to stdout — useful for piping).
44
+
45
+ show <attachment-id> Print a attachment's metadata (HEAD-based — doesn't
46
+ download the bytes).
47
+
48
+ list Enumerate YOUR agent's non-deleted attachments (newest
49
+ first). Supports --cursor + --limit for pagination.
50
+
51
+ delete <attachment-id> Soft-delete a attachment. Idempotent.
52
+
53
+ token <verb> Capability URLs for a attachment (mint | revoke | list).
54
+ 'mint' returns a /b/<token> URL anyone can GET, with
55
+ optional --ttl and --once. 'revoke' invalidates one
56
+ token. 'list' enumerates a attachment's tokens (without
57
+ the token plaintext, which is unrecoverable).
58
+
59
+ Run \`homespun attachment <verb> --help\` for verb-specific options.
60
+
61
+ Output: stdout is machine-readable JSON. Errors go to stderr as
62
+ {"error":{"code","message"}} with a non-zero exit.`;
63
+ /**
64
+ * Build a new ParsedArgs with the leading positional (the verb) stripped.
65
+ * The downstream verb runners read their primary positional (the attachment_id)
66
+ * at positionals[0], so we hand them an args object that looks exactly like
67
+ * they were called directly — mirrors app.ts's shiftPositionals.
68
+ */
69
+ function shiftPositionals(args) {
70
+ // Propagate danglingValueFlags so the leaf runner's assertKnownFlags
71
+ // can still distinguish "unknown flag" from "missing value" — see the
72
+ // matching note in app.ts's shiftPositionals.
73
+ const out = {
74
+ positionals: args.positionals.slice(1),
75
+ flags: args.flags,
76
+ bools: args.bools,
77
+ };
78
+ if (args.danglingValueFlags !== undefined) {
79
+ out.danglingValueFlags = args.danglingValueFlags;
80
+ }
81
+ return out;
82
+ }
83
+ export async function runBlob(args) {
84
+ const verb = args.positionals[0];
85
+ // `homespun attachment token --help` (verb-level help on the token sub-noun, with no
86
+ // further sub-verb). The general --help pre-empt in index.ts only fires
87
+ // when no positional follows the noun; here a positional ("token") is
88
+ // present, so the sub-noun must own its own --help routing.
89
+ if (verb === "token" &&
90
+ args.bools.has("help") &&
91
+ args.positionals.length === 1) {
92
+ process.stdout.write(blobTokenHelp + "\n");
93
+ return;
94
+ }
95
+ // `homespun attachment list --help` — same pattern (list takes no required positional
96
+ // so the general pre-empt would already fire, but for parity with app.ts
97
+ // we route through here when args carry the "list" positional explicitly).
98
+ if (verb === "list" &&
99
+ args.bools.has("help") &&
100
+ args.positionals.length === 1) {
101
+ process.stdout.write(blobListHelp + "\n");
102
+ return;
103
+ }
104
+ const inner = shiftPositionals(args);
105
+ switch (verb) {
106
+ case "upload":
107
+ await runBlobUpload(inner);
108
+ break;
109
+ case "download":
110
+ await runBlobDownload(inner);
111
+ break;
112
+ case "show":
113
+ await runBlobShow(inner);
114
+ break;
115
+ case "list":
116
+ await runBlobList(inner);
117
+ break;
118
+ case "delete":
119
+ await runBlobDelete(inner);
120
+ break;
121
+ case "token":
122
+ await runBlobToken(inner);
123
+ break;
124
+ case undefined:
125
+ fail("missing verb — usage: homespun attachment <upload|download|show|list|delete|token> (run 'homespun attachment --help')", "invalid_args");
126
+ break;
127
+ default:
128
+ fail(`unknown attachment verb '${verb}' — expected upload|download|show|list|delete|token (run 'homespun attachment --help')`, "invalid_args");
129
+ }
130
+ }
131
+ // Re-export per-verb helps so tests / docs can import them by canonical name
132
+ // without knowing which file owns each verb.
133
+ export { blobUploadHelp, blobDownloadHelp, blobShowHelp, blobListHelp, blobDeleteHelp, blobTokenHelp, };
@@ -0,0 +1,68 @@
1
+ // `homespun agent claim <code>` — bind this agent to a human via a one-shot
2
+ // claim code the human generated in their settings UI.
3
+ //
4
+ // Flow (§6.1):
5
+ // 1. Alice opens Settings → "Claim an agent" → relay mints a one-shot code,
6
+ // shows it to her once, 15-min TTL.
7
+ // 2. Alice hands the code to the agent out-of-band (this CLI invocation
8
+ // is exactly that handoff).
9
+ // 3. CLI calls POST /v1/agents/claim with the calling agent's API key.
10
+ // 4. Relay binds Agent.ownerHumanId = alice.id, migrates app ownership.
11
+ //
12
+ // The CLI does NOT print the human's email or id — only the relay's response,
13
+ // which is { ok, owner_human_id, claimed_at }. The agent's existing API key
14
+ // keeps working.
15
+ import { HomespunClient, HomespunApiError } from "@homespunapps/core";
16
+ import { assertKnownFlags } from "../argv.js";
17
+ import { resolveConfig } from "../config.js";
18
+ import { printJson, fail } from "../output.js";
19
+ const KNOWN_FLAGS = ["url", "api-key"];
20
+ const KNOWN_BOOLS = [];
21
+ export const claimHelp = `homespun agent claim — claim this agent for a human
22
+
23
+ Usage:
24
+ homespun agent claim <code>
25
+
26
+ Binds the calling agent to the human whose one-shot claim code is provided.
27
+ The human generates the code in their settings UI (or via the relay's
28
+ POST /v1/self/claim-codes endpoint) and hands it to the agent out-of-band.
29
+
30
+ Arguments:
31
+ <code> The one-shot claim code (begins with cc_). Required.
32
+
33
+ Options:
34
+ --url <url> Relay base URL. Falls back to HOMESPUN_URL / config file.
35
+ --api-key <key> Agent API key. Falls back to HOMESPUN_API_KEY / config file.
36
+ -h, --help Show this help.
37
+
38
+ Output (stdout, JSON):
39
+ { ok: true, owner_human_id, claimed_at }
40
+
41
+ Errors:
42
+ invalid_code code is unknown, expired, or already consumed
43
+ agent_already_claimed this agent already has an owning human
44
+
45
+ Notes:
46
+ This is a one-way operation. To rotate the owner, revoke this agent
47
+ (\`homespun key revoke\`) and register a new one.`;
48
+ export async function runClaim(args) {
49
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "homespun agent claim");
50
+ const code = args.positionals[0];
51
+ if (!code) {
52
+ fail("missing required argument: <code> — run 'homespun agent claim --help'", "invalid_args");
53
+ return;
54
+ }
55
+ const creds = resolveConfig(args);
56
+ const client = new HomespunClient({ url: creds.url, apiKey: creds.apiKey });
57
+ try {
58
+ const result = await client.claimAgent(code);
59
+ printJson(result);
60
+ }
61
+ catch (err) {
62
+ if (err instanceof HomespunApiError) {
63
+ fail(err.message, err.code);
64
+ return;
65
+ }
66
+ throw err;
67
+ }
68
+ }