@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 +21 -15
- package/dist/argv.js +95 -7
- package/dist/commands/agent.js +41 -0
- package/dist/commands/artifact.js +31 -4
- package/dist/commands/blob-delete.js +37 -0
- package/dist/commands/blob-download.js +49 -0
- package/dist/commands/blob-list.js +50 -0
- package/dist/commands/blob-show.js +37 -0
- package/dist/commands/blob-token.js +133 -0
- package/dist/commands/blob-upload.js +79 -0
- package/dist/commands/blob.js +135 -0
- package/dist/commands/config.js +26 -7
- package/dist/commands/create.js +78 -12
- package/dist/commands/delete.js +7 -3
- package/dist/commands/feedback.js +6 -0
- package/dist/commands/{keys.js → key.js} +23 -17
- package/dist/commands/list.js +91 -0
- package/dist/commands/logout.js +10 -6
- package/dist/commands/participant.js +137 -0
- package/dist/commands/register.js +8 -4
- package/dist/commands/send.js +54 -6
- package/dist/commands/session.js +118 -0
- package/dist/commands/skill.js +20 -11
- package/dist/commands/state.js +9 -5
- package/dist/commands/taste.js +36 -19
- package/dist/commands/watch.js +10 -6
- package/dist/config.js +4 -4
- package/dist/index.js +85 -80
- package/dist/input.js +2 -2
- package/dist/output.js +1 -1
- package/dist/store.js +2 -2
- package/dist/version.js +1 -1
- package/package.json +4 -4
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
|
|
34
|
-
pane
|
|
35
|
-
pane
|
|
36
|
-
pane
|
|
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
|
|
41
|
-
pane
|
|
42
|
-
pane
|
|
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 <
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
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
|
|
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
|
+
}
|