@paneui/cli 0.0.5 → 0.0.7

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
@@ -18,10 +18,10 @@ The binary is `pane`.
18
18
 
19
19
  ```sh
20
20
  export PANE_URL=https://relay.paneui.com # or your self-hosted relay
21
- pane register --name "my-agent" # provisions and saves an API key
21
+ pane agent register --name "my-agent" # provisions and saves an API key
22
22
  ```
23
23
 
24
- `pane register` writes the URL + API key to
24
+ `pane agent register` writes the URL + API key to
25
25
  `${XDG_CONFIG_HOME:-~/.config}/pane/config.json`. Subsequent commands need
26
26
  only `PANE_URL` (or nothing) in the environment.
27
27
 
@@ -29,20 +29,26 @@ Override per-invocation with `--url <url>` and `--api-key <key>`.
29
29
 
30
30
  ## Commands
31
31
 
32
+ Uniform `pane <noun> <verb> [options]`:
33
+
32
34
  ```
33
- pane register Provision an agent API key and save it locally
34
- pane create Create a session returns session_id, urls, tokens
35
- pane artifact Manage reusable, versioned artifacts
36
- pane state <id> Non-blocking snapshot: metadata + event log
37
- pane send <id> Emit an agent event into a session
38
- pane watch <id> Stream a session's events as JSON-lines on stdout
39
- pane delete <id> Close / delete a session
40
- pane keys Inspect or revoke your agent's API key
41
- pane config Show the resolved relay config (no network call)
42
- pane logout Clear the locally-saved URL + API key
35
+ pane agent register Provision an agent API key and save it locally
36
+ pane agent logout Clear the locally-saved URL + API key
37
+ pane surface create Create a surface — returns surface_id, urls, tokens
38
+ pane surface show <id> Non-blocking snapshot: metadata + event log
39
+ pane surface send <id> Emit an agent event into a surface
40
+ pane surface watch <id> Stream a surface's events as JSON-lines on stdout
41
+ pane surface delete <id> Close / delete a surface
42
+ pane template <verb> Manage reusable, versioned templates
43
+ pane key list | revoke Inspect or revoke your agent's API key
44
+ pane taste get | set | clear Read / write / clear UI-taste notes
45
+ pane feedback create | list Submit / list one-shot feedback to the operator
46
+ pane config show Show the resolved relay config (no network call)
47
+ pane skill show | version Fetch the relay's SKILL.md (or its version)
43
48
  ```
44
49
 
45
- Run `pane <command> --help` for command-specific options.
50
+ Run `pane <noun> --help` for that noun's verbs, and
51
+ `pane <noun> <verb> --help` for verb-specific options.
46
52
 
47
53
  ## Output
48
54
 
@@ -50,12 +56,12 @@ stdout is machine-readable JSON. Errors go to stderr as
50
56
  `{"error":{"code","message"}}` with a non-zero exit.
51
57
 
52
58
  ```sh
53
- SESSION=$(pane create --template form --schema ./q.json | jq -r .session_id)
54
- pane watch "$SESSION" | jq 'select(.type == "human_response")'
59
+ SESSION=$(pane surface create --template form --schema ./q.json | jq -r .surface_id)
60
+ pane surface watch "$SESSION" | jq 'select(.type == "human_response")'
55
61
  ```
56
62
 
57
63
  ## Links
58
64
 
59
65
  - Repo: <https://github.com/aerolalit/paneui>
60
- - Spec: <https://github.com/aerolalit/paneui/blob/main/docs/SPEC.md>
66
+ - Spec: <https://github.com/aerolalit/paneui/attachment/main/docs/SPEC.md>
61
67
  - License: MIT
package/dist/argv.js CHANGED
@@ -3,22 +3,43 @@
3
3
  // Supports:
4
4
  // --flag value --flag=value --bool -h
5
5
  // Everything that isn't a flag (or a flag's value) is a positional.
6
- /** Thrown when a value-flag is given with no argument (e.g. trailing `--url`). */
6
+ /**
7
+ * Thrown for any argv-level user error: missing value, duplicate flag, or
8
+ * (when a runner calls assertKnownFlags) an unknown flag. `hint` rides
9
+ * alongside the message and ends up in the error envelope so callers see a
10
+ * single line pointing them at the right --help.
11
+ */
7
12
  export class ArgvError extends Error {
8
- constructor(message) {
13
+ hint;
14
+ constructor(message, hint) {
9
15
  super(message);
10
16
  this.name = "ArgvError";
17
+ if (hint !== undefined)
18
+ this.hint = hint;
11
19
  }
12
20
  }
13
21
  /**
14
22
  * Parse argv tokens. `booleanFlags` lists flags that never consume a value
15
23
  * (e.g. --json, --once, --help); everything else with a `--name` form
16
24
  * consumes the next token unless written as `--name=value`.
25
+ *
26
+ * Bails with ArgvError on the first duplicate (`--foo x --foo y` or
27
+ * `--once --once`) so a typo'd repeat doesn't silently overwrite the first
28
+ * value the way a plain `Map.set` would.
29
+ *
30
+ * Does NOT throw on a value-flag with no following value. Instead it
31
+ * records the name in `danglingValueFlags` so `assertKnownFlags` can
32
+ * produce the right message — "unknown flag(s)" for typos, "requires a
33
+ * value" for genuine known-flag-missing-value cases. Without this split,
34
+ * the message was non-uniform (a `--bogus` at end of argv said "requires
35
+ * a value" while `--bogus something` said "unknown flag(s)" — same root
36
+ * cause, two messages).
17
37
  */
18
38
  export function parseArgs(tokens, booleanFlags) {
19
39
  const positionals = [];
20
40
  const flags = new Map();
21
41
  const bools = new Set();
42
+ const danglingValueFlags = new Set();
22
43
  for (let i = 0; i < tokens.length; i++) {
23
44
  const tok = tokens[i];
24
45
  if (tok === "-h" || tok === "--help") {
@@ -29,18 +50,31 @@ export function parseArgs(tokens, booleanFlags) {
29
50
  const body = tok.slice(2);
30
51
  const eq = body.indexOf("=");
31
52
  if (eq !== -1) {
32
- flags.set(body.slice(0, eq), body.slice(eq + 1));
53
+ const key = body.slice(0, eq);
54
+ if (flags.has(key)) {
55
+ throw new ArgvError(`duplicate flag: --${key}`);
56
+ }
57
+ flags.set(key, body.slice(eq + 1));
33
58
  continue;
34
59
  }
35
60
  if (booleanFlags.has(body)) {
61
+ if (bools.has(body)) {
62
+ throw new ArgvError(`duplicate flag: --${body}`);
63
+ }
36
64
  bools.add(body);
37
65
  continue;
38
66
  }
39
67
  const next = tokens[i + 1];
40
68
  if (next === undefined || next.startsWith("--")) {
41
- // A value-flag with no argument is a user error — don't silently
42
- // demote it to a boolean (which hides the mistake).
43
- throw new ArgvError(`--${body} requires a value`);
69
+ // No value follows. Don't decide whether this is a typo or a
70
+ // forgotten value record it; assertKnownFlags resolves both
71
+ // with one consistent message shape (see the field doc on
72
+ // ParsedArgs).
73
+ danglingValueFlags.add(body);
74
+ continue;
75
+ }
76
+ if (flags.has(body)) {
77
+ throw new ArgvError(`duplicate flag: --${body}`);
44
78
  }
45
79
  flags.set(body, next);
46
80
  i++;
@@ -48,5 +82,59 @@ export function parseArgs(tokens, booleanFlags) {
48
82
  }
49
83
  positionals.push(tok);
50
84
  }
51
- return { positionals, flags, bools };
85
+ return { positionals, flags, bools, danglingValueFlags };
86
+ }
87
+ /**
88
+ * Flags every command accepts. Kept here (not in each command's allow-list)
89
+ * so adding a new global flag updates one place. `url` / `api-key` are the
90
+ * relay-target overrides; `help` / `json` are universal display modes.
91
+ */
92
+ const GLOBAL_FLAGS = ["url", "api-key"];
93
+ const GLOBAL_BOOLS = ["help", "json"];
94
+ /**
95
+ * Reject anything the per-command allow-list (plus the globals above) does
96
+ * not name. Run from each leaf runner before it starts pulling values out of
97
+ * `args`. The thrown ArgvError carries a hint pointing at the verb's own
98
+ * --help, so a user fixing a typo lands on the canonical list of flags.
99
+ *
100
+ * Why per-command and not at parse time: the parser is single-pass and
101
+ * generic on purpose — adding a new flag to one verb should not require a
102
+ * shared registry. Keeping the allow-list co-located with the runner that
103
+ * consumes it means the two cannot drift.
104
+ *
105
+ * Also resolves the parser's `danglingValueFlags`: an unknown name there
106
+ * is reported alongside other unknowns ("unknown flag(s): --bogus"); a
107
+ * known name there surfaces as "--name requires a value". This is what
108
+ * keeps the error message uniform for a typo whether or not a value
109
+ * follows it.
110
+ */
111
+ export function assertKnownFlags(args, knownFlags, knownBools, helpCommand) {
112
+ const flagSet = new Set([...GLOBAL_FLAGS, ...knownFlags]);
113
+ const boolSet = new Set([...GLOBAL_BOOLS, ...knownBools]);
114
+ const dangling = args.danglingValueFlags ?? new Set();
115
+ const unknown = [];
116
+ for (const k of args.flags.keys()) {
117
+ if (!flagSet.has(k) && !boolSet.has(k))
118
+ unknown.push(`--${k}`);
119
+ }
120
+ for (const k of args.bools) {
121
+ if (!boolSet.has(k) && !flagSet.has(k))
122
+ unknown.push(`--${k}`);
123
+ }
124
+ for (const k of dangling) {
125
+ if (!flagSet.has(k) && !boolSet.has(k))
126
+ unknown.push(`--${k}`);
127
+ }
128
+ if (unknown.length > 0) {
129
+ throw new ArgvError(`unknown flag(s): ${unknown.join(", ")}`, `run \`${helpCommand} --help\` for the supported flags`);
130
+ }
131
+ // No unknowns — but a known value-flag may still have been left without
132
+ // a value. Surface the first such case with the pre-existing message
133
+ // shape ("--name requires a value"). Reporting only the first keeps the
134
+ // message simple; the user fixes that flag, re-runs, sees the next one.
135
+ for (const k of dangling) {
136
+ if (flagSet.has(k)) {
137
+ throw new ArgvError(`--${k} requires a value`);
138
+ }
139
+ }
52
140
  }
@@ -0,0 +1,54 @@
1
+ // `pane agent` — agent-lifecycle operations: register a new API key, or
2
+ // clear the locally-saved one.
3
+ //
4
+ // Both verbs are about the calling agent's identity on this machine:
5
+ // register provision an API key from the relay (one-shot bootstrap)
6
+ // logout clear the locally-saved relay URL + API key
7
+ //
8
+ // This file is a thin dispatcher — actual logic lives in register.ts and
9
+ // logout.ts.
10
+ import { runRegister } from "./register.js";
11
+ import { runLogout } from "./logout.js";
12
+ import { runClaim } from "./claim.js";
13
+ import { fail } from "../output.js";
14
+ export const agentHelp = `pane agent — manage this agent's identity on the relay
15
+
16
+ Usage:
17
+ pane agent <verb> [options]
18
+
19
+ Verbs:
20
+ register Provision an agent API key (POST /v1/register) and save it
21
+ to the CLI config file. Run this once before other commands.
22
+ claim <code> Bind this agent to a human via a one-shot claim code the
23
+ human generated in their Settings UI (POST /v1/agents/claim).
24
+ One-way; no unclaim in v1.
25
+ logout Clear the locally-saved relay URL + API key. Does NOT
26
+ revoke the key on the relay — use 'pane key revoke' for
27
+ that.
28
+
29
+ Run \`pane agent <verb> --help\` for verb-specific options.`;
30
+ export async function runAgent(args) {
31
+ // Strip the first positional (the verb) so each verb runner sees its
32
+ // own arguments at positionals[0..n].
33
+ const verbArgs = {
34
+ ...args,
35
+ positionals: args.positionals.slice(1),
36
+ };
37
+ const verb = args.positionals[0];
38
+ switch (verb) {
39
+ case "register":
40
+ await runRegister(verbArgs);
41
+ break;
42
+ case "claim":
43
+ await runClaim(verbArgs);
44
+ break;
45
+ case "logout":
46
+ await runLogout(verbArgs);
47
+ break;
48
+ case undefined:
49
+ fail("missing verb — usage: pane agent <register|claim|logout> (run 'pane agent --help')", "invalid_args");
50
+ break;
51
+ default:
52
+ fail(`unknown agent verb '${verb}' — expected register|claim|logout (run 'pane agent --help')`, "invalid_args");
53
+ }
54
+ }
@@ -0,0 +1,37 @@
1
+ // `pane 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 = `pane attachment delete — soft-delete a attachment
8
+
9
+ Usage:
10
+ pane 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 PANE_URL).
18
+ --api-key <key> Agent API key (overrides PANE_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, "pane attachment delete");
25
+ const attachmentId = args.positionals[0];
26
+ if (!attachmentId) {
27
+ fail("missing <attachment-id> — 'pane 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
+ // `pane 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 = `pane attachment download — fetch a attachment's bytes
9
+
10
+ Usage:
11
+ pane 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 PANE_URL).
21
+ --api-key <key> Agent API key (overrides PANE_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, "pane attachment download");
29
+ const attachmentId = args.positionals[0];
30
+ if (!attachmentId) {
31
+ fail("missing <attachment-id> — 'pane 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
+ // `pane 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 'pane 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 = `pane attachment list — enumerate YOUR agent's attachments
12
+
13
+ Usage:
14
+ pane 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 PANE_URL).
25
+ --api-key <key> Agent API key (overrides PANE_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, "pane 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
+ // `pane 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 = `pane attachment show — print a attachment's metadata (no bytes)
8
+
9
+ Usage:
10
+ pane 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 'pane attachment
14
+ download' for that.
15
+
16
+ Options:
17
+ --url <url> Relay base URL (overrides PANE_URL).
18
+ --api-key <key> Agent API key (overrides PANE_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, "pane attachment show");
25
+ const attachmentId = args.positionals[0];
26
+ if (!attachmentId) {
27
+ fail("missing <attachment-id> — 'pane 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
+ // `pane 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 `pane 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 `pane
13
+ // surface 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 = `pane 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
+ pane 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 template / surface TTL / 24h agent; the caller
35
+ can only 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 PANE_URL).
54
+ --api-key <key> Agent API key (overrides PANE_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, "pane attachment token mint");
60
+ const attachmentId = args.positionals[1];
61
+ if (!attachmentId) {
62
+ fail("missing <attachment-id> — 'pane 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, "pane attachment token revoke");
83
+ const attachmentId = args.positionals[1];
84
+ const tokenId = args.positionals[2];
85
+ if (!attachmentId || !tokenId) {
86
+ fail("missing arguments — 'pane 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, "pane attachment token list");
99
+ const attachmentId = args.positionals[1];
100
+ if (!attachmentId) {
101
+ fail("missing <attachment-id> — 'pane 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: pane attachment token <mint|revoke|list> (run 'pane attachment token --help')", "invalid_args");
129
+ break;
130
+ default:
131
+ fail(`unknown token verb '${verb}' — expected mint|revoke|list (run 'pane attachment token --help')`, "invalid_args");
132
+ }
133
+ }
@@ -0,0 +1,79 @@
1
+ // `pane attachment upload` — POST /v1/attachments (multipart), three 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 = [
8
+ "file",
9
+ "scope",
10
+ "surface-id",
11
+ "template-id",
12
+ "filename",
13
+ "mime",
14
+ ];
15
+ const KNOWN_BOOLS = [];
16
+ export const blobUploadHelp = `pane attachment upload — upload a local file as a attachment
17
+
18
+ Usage:
19
+ pane attachment upload --file <path> [options]
20
+
21
+ Required:
22
+ --file <path> Local file to upload.
23
+
24
+ Scope (default: agent):
25
+ --scope <s> "agent" | "surface" | "template".
26
+ --surface-id <id> Required when --scope=surface.
27
+ --template-id <id> Required when --scope=template.
28
+
29
+ Optional:
30
+ --filename <name> Display filename (otherwise basename of --file).
31
+ --mime <type> Declared Content-Type. The relay sniffs the bytes
32
+ regardless — this is advisory.
33
+ --url <url> Relay base URL (overrides PANE_URL).
34
+ --api-key <key> Agent API key (overrides PANE_API_KEY).
35
+ -h, --help Show this help.
36
+
37
+ Output (stdout, JSON):
38
+ AttachmentRef — { attachment_id, scope, mime, size, sha256, ... }`;
39
+ export async function runBlobUpload(args) {
40
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane attachment upload");
41
+ const filePath = args.flags.get("file");
42
+ if (!filePath) {
43
+ fail("missing --file <path> — 'pane attachment upload' requires a local file to upload", "invalid_args");
44
+ }
45
+ let bytes;
46
+ try {
47
+ bytes = readFileSync(filePath);
48
+ }
49
+ catch (e) {
50
+ fail(`failed to read --file '${filePath}': ${e instanceof Error ? e.message : String(e)}`, "invalid_args");
51
+ }
52
+ const scopeRaw = args.flags.get("scope") ?? "agent";
53
+ if (scopeRaw !== "agent" &&
54
+ scopeRaw !== "surface" &&
55
+ scopeRaw !== "template") {
56
+ fail(`unknown --scope '${scopeRaw}' — expected one of: agent, surface, template`, "invalid_args");
57
+ }
58
+ const scope = scopeRaw;
59
+ if (scope === "surface" && !args.flags.get("surface-id")) {
60
+ fail("--scope=surface requires --surface-id <id>", "invalid_args");
61
+ }
62
+ if (scope === "template" && !args.flags.get("template-id")) {
63
+ fail("--scope=template requires --template-id <id>", "invalid_args");
64
+ }
65
+ const client = makeClient(args);
66
+ try {
67
+ const ref = await client.uploadBlob(bytes, {
68
+ scope,
69
+ surfaceId: args.flags.get("surface-id"),
70
+ templateId: args.flags.get("template-id"),
71
+ filename: args.flags.get("filename") ?? basename(filePath),
72
+ mime: args.flags.get("mime"),
73
+ });
74
+ printJson(ref);
75
+ }
76
+ catch (e) {
77
+ failFromError(e);
78
+ }
79
+ }