@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 +22 -16
- package/dist/argv.js +95 -7
- package/dist/commands/agent.js +54 -0
- package/dist/commands/attachment-delete.js +37 -0
- package/dist/commands/attachment-download.js +53 -0
- package/dist/commands/attachment-list.js +50 -0
- package/dist/commands/attachment-show.js +37 -0
- package/dist/commands/attachment-token.js +133 -0
- package/dist/commands/attachment-upload.js +79 -0
- package/dist/commands/attachment.js +135 -0
- package/dist/commands/claim.js +68 -0
- package/dist/commands/config.js +26 -7
- package/dist/commands/create.js +120 -54
- package/dist/commands/delete.js +15 -11
- package/dist/commands/feedback.js +10 -4
- 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 +66 -12
- package/dist/commands/skill.js +20 -11
- package/dist/commands/state.js +16 -12
- package/dist/commands/surface.js +118 -0
- package/dist/commands/taste.js +22 -14
- package/dist/commands/{artifact.js → template.js} +94 -67
- package/dist/commands/watch.js +25 -21
- package/dist/config.js +4 -4
- package/dist/index.js +90 -85
- package/dist/input.js +4 -4
- package/dist/output.js +1 -1
- package/dist/store.js +2 -2
- package/dist/version.js +1 -1
- package/package.json +5 -5
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
|
|
38
|
-
pane watch <id> Stream a
|
|
39
|
-
pane delete <id> Close / delete a
|
|
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 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 <
|
|
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 .
|
|
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/
|
|
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
|
-
/**
|
|
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,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
|
+
}
|