@paneui/cli 0.0.4 → 0.0.6

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 session create Create a session — returns session_id, urls, tokens
38
+ pane session show <id> Non-blocking snapshot: metadata + event log
39
+ pane session send <id> Emit an agent event into a session
40
+ pane session watch <id> Stream a session's events as JSON-lines on stdout
41
+ pane session delete <id> Close / delete a session
42
+ pane artifact <verb> Manage reusable, versioned artifacts
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,8 +56,8 @@ 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 session create --template form --schema ./q.json | jq -r .session_id)
60
+ pane session watch "$SESSION" | jq 'select(.type == "human_response")'
55
61
  ```
56
62
 
57
63
  ## Links
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,41 @@
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 { fail } from "../output.js";
13
+ export const agentHelp = `pane agent — manage this agent's identity on the relay
14
+
15
+ Usage:
16
+ pane agent <verb> [options]
17
+
18
+ Verbs:
19
+ register Provision an agent API key (POST /v1/register) and save it
20
+ to the CLI config file. Run this once before other commands.
21
+ logout Clear the locally-saved relay URL + API key. Does NOT
22
+ revoke the key on the relay — use 'pane key revoke' for
23
+ that.
24
+
25
+ Run \`pane agent <verb> --help\` for verb-specific options.`;
26
+ export async function runAgent(args) {
27
+ const verb = args.positionals[0];
28
+ switch (verb) {
29
+ case "register":
30
+ await runRegister(args);
31
+ break;
32
+ case "logout":
33
+ await runLogout(args);
34
+ break;
35
+ case undefined:
36
+ fail("missing verb — usage: pane agent <register|logout> (run 'pane agent --help')", "invalid_args");
37
+ break;
38
+ default:
39
+ fail(`unknown agent verb '${verb}' — expected register|logout (run 'pane agent --help')`, "invalid_args");
40
+ }
41
+ }
@@ -5,17 +5,38 @@
5
5
  // delete).
6
6
  // An artifact is a reusable UI template (HTML + event schema + optional input
7
7
  // schema); a session is one *use* of one version of it. Authoring an artifact
8
- // once and instancing it via `pane create --artifact-id` removes the per-use
9
- // cost of regenerating the same HTML.
8
+ // once and instancing it via `pane session create --artifact-id` removes the
9
+ // per-use cost of regenerating the same HTML.
10
10
  import { createArtifactSchema, createArtifactVersionSchema, patchArtifactMetadataSchema, } from "@paneui/core";
11
+ import { assertKnownFlags } from "../argv.js";
11
12
  import { makeClient } from "../config.js";
12
13
  import { resolveJson, resolveText } from "../input.js";
13
14
  import { printJson, fail, failFromError } from "../output.js";
15
+ const CREATE_FLAGS = [
16
+ "name",
17
+ "slug",
18
+ "description",
19
+ "tags",
20
+ "artifact",
21
+ "artifact-type",
22
+ "event-schema",
23
+ "input-schema",
24
+ ];
25
+ const VERSION_FLAGS = [
26
+ "artifact",
27
+ "artifact-type",
28
+ "event-schema",
29
+ "input-schema",
30
+ ];
31
+ const UPDATE_FLAGS = ["name", "slug", "description", "tags"];
32
+ const NO_FLAGS = [];
33
+ const NO_BOOLS = [];
34
+ const DELETE_BOOLS = ["yes"];
14
35
  export const artifactHelp = `pane artifact — manage reusable, versioned artifacts
15
36
 
16
37
  An artifact is a reusable UI template: HTML + an event schema + an optional
17
38
  input schema. A session is one use of one version of it. Author an artifact
18
- once, then instance it many times with 'pane create --artifact-id <id|slug>'
39
+ once, then instance it many times with 'pane session create --artifact-id <id|slug>'
19
40
  instead of regenerating the HTML on every session.
20
41
 
21
42
  Usage:
@@ -61,7 +82,7 @@ Subcommands:
61
82
  pane artifact delete <id|slug> --yes
62
83
  Permanently deletes the artifact and all its versions. Refused
63
84
  (409 conflict) if any session in any state still references one
64
- of the artifact's versions — run 'pane delete <session-id>' on
85
+ of the artifact's versions — run 'pane session delete <session-id>' on
65
86
  each first, or wait for the relay's TTL sweeper to reclaim them.
66
87
  Prints { artifact, deleted: true } on success.
67
88
 
@@ -169,6 +190,7 @@ function resolveTags(args) {
169
190
  return tags.length > 0 ? tags : undefined;
170
191
  }
171
192
  async function runArtifactCreate(args) {
193
+ assertKnownFlags(args, CREATE_FLAGS, NO_BOOLS, "pane artifact create");
172
194
  const name = args.flags.get("name");
173
195
  if (!name)
174
196
  fail("missing --name", "invalid_args");
@@ -217,6 +239,7 @@ async function runArtifactCreate(args) {
217
239
  }
218
240
  }
219
241
  async function runArtifactVersion(args) {
242
+ assertKnownFlags(args, VERSION_FLAGS, NO_BOOLS, "pane artifact version");
220
243
  const idOrSlug = args.positionals[1];
221
244
  if (!idOrSlug) {
222
245
  fail("missing artifact <id|slug> — usage: pane artifact version <id|slug>", "invalid_args");
@@ -252,6 +275,7 @@ async function runArtifactVersion(args) {
252
275
  }
253
276
  }
254
277
  async function runArtifactUpdate(args) {
278
+ assertKnownFlags(args, UPDATE_FLAGS, NO_BOOLS, "pane artifact update");
255
279
  const idOrSlug = args.positionals[1];
256
280
  if (!idOrSlug) {
257
281
  fail("missing artifact <id|slug> — usage: pane artifact update <id|slug>", "invalid_args");
@@ -289,6 +313,7 @@ async function runArtifactUpdate(args) {
289
313
  }
290
314
  }
291
315
  async function runArtifactSearch(args, query) {
316
+ assertKnownFlags(args, NO_FLAGS, NO_BOOLS, query === undefined ? "pane artifact list" : "pane artifact search");
292
317
  const client = makeClient(args);
293
318
  try {
294
319
  const res = await client.searchArtifacts(query);
@@ -299,6 +324,7 @@ async function runArtifactSearch(args, query) {
299
324
  }
300
325
  }
301
326
  async function runArtifactShow(args) {
327
+ assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane artifact show");
302
328
  const idOrSlug = args.positionals[1];
303
329
  if (!idOrSlug) {
304
330
  fail("missing artifact <id|slug> — usage: pane artifact show <id|slug>", "invalid_args");
@@ -318,6 +344,7 @@ async function runArtifactShow(args) {
318
344
  // envelope. `--yes` is required because there's no Undo button on a delete
319
345
  // and the same `pane artifact create` slug isn't reservable once gone.
320
346
  async function runArtifactDelete(args) {
347
+ assertKnownFlags(args, NO_FLAGS, DELETE_BOOLS, "pane artifact delete");
321
348
  const idOrSlug = args.positionals[1];
322
349
  if (!idOrSlug) {
323
350
  fail("missing artifact <id|slug> — usage: pane artifact delete <id|slug> --yes", "invalid_args");
@@ -0,0 +1,37 @@
1
+ // `pane blob delete <blob-id>` — soft-delete a blob.
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 blob delete — soft-delete a blob
8
+
9
+ Usage:
10
+ pane blob delete <blob-id> [options]
11
+
12
+ Marks the blob as deleted (DELETE /v1/blobs/:id). Idempotent: deleting an
13
+ already-deleted blob still returns success. Tokens minted against this blob
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
+ { blob_id, deleted: true }`;
23
+ export async function runBlobDelete(args) {
24
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane blob delete");
25
+ const blobId = args.positionals[0];
26
+ if (!blobId) {
27
+ fail("missing <blob-id> — 'pane blob delete <blob-id>'", "invalid_args");
28
+ }
29
+ const client = makeClient(args);
30
+ try {
31
+ const r = await client.deleteBlob(blobId);
32
+ printJson({ blob_id: blobId, ...r });
33
+ }
34
+ catch (e) {
35
+ failFromError(e);
36
+ }
37
+ }
@@ -0,0 +1,49 @@
1
+ // `pane blob download <blob-id>` — fetch blob 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 blob download — fetch a blob's bytes
9
+
10
+ Usage:
11
+ pane blob download <blob-id> [--out <path>] [options]
12
+
13
+ GETs the blob 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: { blob_id, written: <path>, bytes: <n> } to stdout.`;
27
+ export async function runBlobDownload(args) {
28
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane blob download");
29
+ const blobId = args.positionals[0];
30
+ if (!blobId) {
31
+ fail("missing <blob-id> — 'pane blob download <blob-id>'", "invalid_args");
32
+ }
33
+ const out = args.flags.get("out");
34
+ const client = makeClient(args);
35
+ try {
36
+ const buf = await client.downloadBlob(blobId);
37
+ if (out) {
38
+ writeFileSync(out, Buffer.from(buf));
39
+ printJson({ blob_id: blobId, written: out, bytes: buf.byteLength });
40
+ }
41
+ else {
42
+ // Binary to stdout — useful for piping into another tool.
43
+ process.stdout.write(Buffer.from(buf));
44
+ }
45
+ }
46
+ catch (e) {
47
+ failFromError(e);
48
+ }
49
+ }
@@ -0,0 +1,50 @@
1
+ // `pane blob list` — enumerate YOUR agent's blobs.
2
+ //
3
+ // Lists blobs owned by the calling agent, newest first. Soft-deleted blobs
4
+ // are excluded; tokens are not enumerated here (use 'pane blob token list
5
+ // <blob-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 blob list — enumerate YOUR agent's blobs
12
+
13
+ Usage:
14
+ pane blob list [--cursor <token>] [--limit <n>] [options]
15
+
16
+ Returns the agent's non-deleted blobs (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: BlobRef[], next_cursor: string | null }`;
30
+ export async function runBlobList(args) {
31
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane blob 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 blob show <blob-id>` — print a blob'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 blob show — print a blob's metadata (no bytes)
8
+
9
+ Usage:
10
+ pane blob show <blob-id> [options]
11
+
12
+ Looks up the blob by id and prints its BlobRef metadata — owner, scope,
13
+ mime, size, sha256, etc. Does NOT download the bytes; use 'pane blob
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
+ BlobRef`;
23
+ export async function runBlobShow(args) {
24
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane blob show");
25
+ const blobId = args.positionals[0];
26
+ if (!blobId) {
27
+ fail("missing <blob-id> — 'pane blob show <blob-id>'", "invalid_args");
28
+ }
29
+ const client = makeClient(args);
30
+ try {
31
+ const ref = await client.getBlob(blobId);
32
+ printJson(ref);
33
+ }
34
+ catch (e) {
35
+ failFromError(e);
36
+ }
37
+ }
@@ -0,0 +1,133 @@
1
+ // `pane blob token <mint|revoke|list>` — capability URLs for a blob.
2
+ //
3
+ // A capability URL (/b/<token>) is a participant-facing way to fetch a blob
4
+ // without holding the agent's API key. Tokens are minted per-blob, 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 blob`. The blob 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
+ // session 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 blob token — manage a blob's capability URLs
22
+
23
+ Capability URLs let a participant (or any browser holding the URL) fetch a
24
+ blob 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 blob token <verb> <args>
30
+
31
+ Verbs:
32
+ mint <blob-id> Mint a /b/<token> capability URL for one blob.
33
+ Optional: --ttl <seconds> (defaults by scope:
34
+ 30d artifact / session 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 <blob-id> <token-id>
40
+ Invalidate one previously-minted token by id.
41
+ Idempotent: revoking twice still returns success.
42
+
43
+ list <blob-id> Enumerate the tokens minted against one blob,
44
+ including revoked rows (for audit). Returns
45
+ { blob_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 blob token mint");
60
+ const blobId = args.positionals[1];
61
+ if (!blobId) {
62
+ fail("missing <blob-id> — 'pane blob token mint <blob-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(blobId, {
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 blob token revoke");
83
+ const blobId = args.positionals[1];
84
+ const tokenId = args.positionals[2];
85
+ if (!blobId || !tokenId) {
86
+ fail("missing arguments — 'pane blob token revoke <blob-id> <token-id>'", "invalid_args");
87
+ }
88
+ const client = makeClient(args);
89
+ try {
90
+ const r = await client.revokeBlobToken(blobId, 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 blob token list");
99
+ const blobId = args.positionals[1];
100
+ if (!blobId) {
101
+ fail("missing <blob-id> — 'pane blob token list <blob-id>'", "invalid_args");
102
+ }
103
+ const client = makeClient(args);
104
+ try {
105
+ const r = await client.listBlobTokens(blobId);
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 blob.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 blob token <mint|revoke|list> (run 'pane blob token --help')", "invalid_args");
129
+ break;
130
+ default:
131
+ fail(`unknown token verb '${verb}' — expected mint|revoke|list (run 'pane blob token --help')`, "invalid_args");
132
+ }
133
+ }