@paneui/cli 0.0.5 → 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.
@@ -0,0 +1,79 @@
1
+ // `pane blob upload` — POST /v1/blobs (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
+ "session-id",
11
+ "artifact-id",
12
+ "filename",
13
+ "mime",
14
+ ];
15
+ const KNOWN_BOOLS = [];
16
+ export const blobUploadHelp = `pane blob upload — upload a local file as a blob
17
+
18
+ Usage:
19
+ pane blob upload --file <path> [options]
20
+
21
+ Required:
22
+ --file <path> Local file to upload.
23
+
24
+ Scope (default: agent):
25
+ --scope <s> "agent" | "session" | "artifact".
26
+ --session-id <id> Required when --scope=session.
27
+ --artifact-id <id> Required when --scope=artifact.
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
+ BlobRef — { blob_id, scope, mime, size, sha256, ... }`;
39
+ export async function runBlobUpload(args) {
40
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane blob upload");
41
+ const filePath = args.flags.get("file");
42
+ if (!filePath) {
43
+ fail("missing --file <path> — 'pane blob 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 !== "session" &&
55
+ scopeRaw !== "artifact") {
56
+ fail(`unknown --scope '${scopeRaw}' — expected one of: agent, session, artifact`, "invalid_args");
57
+ }
58
+ const scope = scopeRaw;
59
+ if (scope === "session" && !args.flags.get("session-id")) {
60
+ fail("--scope=session requires --session-id <id>", "invalid_args");
61
+ }
62
+ if (scope === "artifact" && !args.flags.get("artifact-id")) {
63
+ fail("--scope=artifact requires --artifact-id <id>", "invalid_args");
64
+ }
65
+ const client = makeClient(args);
66
+ try {
67
+ const ref = await client.uploadBlob(bytes, {
68
+ scope,
69
+ sessionId: args.flags.get("session-id"),
70
+ artifactId: args.flags.get("artifact-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
+ }
@@ -0,0 +1,135 @@
1
+ // `pane blob` — manage binary attachments (blobs) on the relay.
2
+ //
3
+ // A blob is a typed binary file (image, PDF, audio, video, etc.) owned by an
4
+ // agent and optionally bound to a session or artifact. Pages reference blobs
5
+ // by id with `format: pane-blob-id`; participants can fetch a blob through a
6
+ // minted capability URL (/b/<token>) without needing the agent's API key.
7
+ //
8
+ // This file is a thin dispatcher — each verb's actual logic lives in its own
9
+ // file (blob-upload.ts, blob-download.ts, blob-show.ts, blob-delete.ts) and
10
+ // the token sub-noun is dispatched via blob-token.ts.
11
+ //
12
+ // Most blob verbs read their primary positional (the blob_id) at
13
+ // positionals[0]; we slice off our own verb before delegating so each verb
14
+ // runner doesn't need to know it was reached through `pane blob`.
15
+ import { runBlobUpload, blobUploadHelp } from "./blob-upload.js";
16
+ import { runBlobDownload, blobDownloadHelp } from "./blob-download.js";
17
+ import { runBlobShow, blobShowHelp } from "./blob-show.js";
18
+ import { runBlobList, blobListHelp } from "./blob-list.js";
19
+ import { runBlobDelete, blobDeleteHelp } from "./blob-delete.js";
20
+ import { runBlobToken, blobTokenHelp } from "./blob-token.js";
21
+ import { fail } from "../output.js";
22
+ export const blobHelp = `pane blob — manage blobs (binary attachments) on the relay
23
+
24
+ A blob is a typed binary file (image, PDF, audio, video, ...) the agent has
25
+ uploaded to the relay. Blobs are scoped:
26
+
27
+ agent — reusable across the agent's sessions (default)
28
+ session — bound to one session; deleted with it
29
+ artifact — bound to a reusable artifact; deleted with it
30
+
31
+ Pages reference blobs by id (the relay's schema validates the id with
32
+ \`format: pane-blob-id\`). For a participant-facing URL that bypasses the
33
+ agent's API key, mint a token with 'pane blob token mint'.
34
+
35
+ Usage:
36
+ pane blob <verb> [options]
37
+
38
+ Verbs:
39
+ upload Upload a local file. Required: --file. Optional:
40
+ --scope, --session-id, --artifact-id, --filename,
41
+ --mime. Prints { blob_id, scope, mime, size, sha256,
42
+ ... }.
43
+
44
+ download <blob-id> Download a blob by id. Use --out <path> to write a
45
+ file (default: writes to stdout — useful for piping).
46
+
47
+ show <blob-id> Print a blob's metadata (HEAD-based — doesn't
48
+ download the bytes).
49
+
50
+ list Enumerate YOUR agent's non-deleted blobs (newest
51
+ first). Supports --cursor + --limit for pagination.
52
+
53
+ delete <blob-id> Soft-delete a blob. Idempotent.
54
+
55
+ token <verb> Capability URLs for a blob (mint | revoke | list).
56
+ 'mint' returns a /b/<token> URL anyone can GET, with
57
+ optional --ttl and --once. 'revoke' invalidates one
58
+ token. 'list' enumerates a blob's tokens (without
59
+ the token plaintext, which is unrecoverable).
60
+
61
+ Run \`pane blob <verb> --help\` for verb-specific options.
62
+
63
+ Output: stdout is machine-readable JSON. Errors go to stderr as
64
+ {"error":{"code","message"}} with a non-zero exit.`;
65
+ /**
66
+ * Build a new ParsedArgs with the leading positional (the verb) stripped.
67
+ * The downstream verb runners read their primary positional (the blob_id)
68
+ * at positionals[0], so we hand them an args object that looks exactly like
69
+ * they were called directly — mirrors session.ts's shiftPositionals.
70
+ */
71
+ function shiftPositionals(args) {
72
+ // Propagate danglingValueFlags so the leaf runner's assertKnownFlags
73
+ // can still distinguish "unknown flag" from "missing value" — see the
74
+ // matching note in session.ts's shiftPositionals.
75
+ const out = {
76
+ positionals: args.positionals.slice(1),
77
+ flags: args.flags,
78
+ bools: args.bools,
79
+ };
80
+ if (args.danglingValueFlags !== undefined) {
81
+ out.danglingValueFlags = args.danglingValueFlags;
82
+ }
83
+ return out;
84
+ }
85
+ export async function runBlob(args) {
86
+ const verb = args.positionals[0];
87
+ // `pane blob token --help` (verb-level help on the token sub-noun, with no
88
+ // further sub-verb). The general --help pre-empt in index.ts only fires
89
+ // when no positional follows the noun; here a positional ("token") is
90
+ // present, so the sub-noun must own its own --help routing.
91
+ if (verb === "token" &&
92
+ args.bools.has("help") &&
93
+ args.positionals.length === 1) {
94
+ process.stdout.write(blobTokenHelp + "\n");
95
+ return;
96
+ }
97
+ // `pane blob list --help` — same pattern (list takes no required positional
98
+ // so the general pre-empt would already fire, but for parity with session.ts
99
+ // we route through here when args carry the "list" positional explicitly).
100
+ if (verb === "list" &&
101
+ args.bools.has("help") &&
102
+ args.positionals.length === 1) {
103
+ process.stdout.write(blobListHelp + "\n");
104
+ return;
105
+ }
106
+ const inner = shiftPositionals(args);
107
+ switch (verb) {
108
+ case "upload":
109
+ await runBlobUpload(inner);
110
+ break;
111
+ case "download":
112
+ await runBlobDownload(inner);
113
+ break;
114
+ case "show":
115
+ await runBlobShow(inner);
116
+ break;
117
+ case "list":
118
+ await runBlobList(inner);
119
+ break;
120
+ case "delete":
121
+ await runBlobDelete(inner);
122
+ break;
123
+ case "token":
124
+ await runBlobToken(inner);
125
+ break;
126
+ case undefined:
127
+ fail("missing verb — usage: pane blob <upload|download|show|list|delete|token> (run 'pane blob --help')", "invalid_args");
128
+ break;
129
+ default:
130
+ fail(`unknown blob verb '${verb}' — expected upload|download|show|list|delete|token (run 'pane blob --help')`, "invalid_args");
131
+ }
132
+ }
133
+ // Re-export per-verb helps so tests / docs can import them by canonical name
134
+ // without knowing which file owns each verb.
135
+ export { blobUploadHelp, blobDownloadHelp, blobShowHelp, blobListHelp, blobDeleteHelp, blobTokenHelp, };
@@ -1,14 +1,19 @@
1
- // `pane config` — show the resolved relay config without a network call.
1
+ // `pane config show` — show the resolved relay config without a network call.
2
+ import { assertKnownFlags } from "../argv.js";
2
3
  import { describeConfig } from "../config.js";
3
- import { printJson } from "../output.js";
4
+ import { printJson, fail } from "../output.js";
5
+ const NO_FLAGS = [];
6
+ const NO_BOOLS = [];
4
7
  export const configHelp = `pane config — show the resolved relay config
5
8
 
6
9
  Usage:
7
- pane config [options]
10
+ pane config show [options]
8
11
 
9
- Prints the relay URL and API-key info the CLI would use, and where each value
10
- came from. Makes NO network call purely inspects flags, env vars, and the
11
- saved config file.
12
+ Verbs:
13
+ show Print the relay URL and API-key info the CLI would use,
14
+ and where each value came from. Makes NO network call —
15
+ purely inspects flags, env vars, and the saved config
16
+ file.
12
17
 
13
18
  The API key is never printed in full: only a short masked prefix.
14
19
 
@@ -26,6 +31,20 @@ Output (stdout, JSON):
26
31
  key_source, "flag" | "env" | "store" | "none"
27
32
  config_path absolute path to the CLI config file
28
33
  }`;
29
- export async function runConfig(args) {
34
+ async function runConfigShow(args) {
35
+ assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane config show");
30
36
  printJson(describeConfig(args));
31
37
  }
38
+ export async function runConfig(args) {
39
+ const verb = args.positionals[0];
40
+ switch (verb) {
41
+ case "show":
42
+ await runConfigShow(args);
43
+ break;
44
+ case undefined:
45
+ fail("missing verb — usage: pane config show (run 'pane config --help')", "invalid_args");
46
+ break;
47
+ default:
48
+ fail(`unknown config verb '${verb}' — expected show (run 'pane config --help')`, "invalid_args");
49
+ }
50
+ }
@@ -1,8 +1,24 @@
1
- // `pane create` — create a session via POST /v1/sessions.
1
+ // `pane session create` — create a session via POST /v1/sessions.
2
2
  import { createSessionSchema } from "@paneui/core";
3
+ import { assertKnownFlags } from "../argv.js";
3
4
  import { makeClient } from "../config.js";
4
5
  import { resolveJson, resolveText } from "../input.js";
5
6
  import { printJson, fail, failFromError } from "../output.js";
7
+ const KNOWN_FLAGS = [
8
+ "artifact",
9
+ "artifact-id",
10
+ "artifact-type",
11
+ "version",
12
+ "event-schema",
13
+ "input-schema",
14
+ "title",
15
+ "input-data",
16
+ "ttl",
17
+ "participants",
18
+ "metadata",
19
+ "callback",
20
+ ];
21
+ const KNOWN_BOOLS = [];
6
22
  // Translate a Zod schema path (e.g. ["participants","humans"]) back to the
7
23
  // public CLI flag the user actually typed. Without this, a `--participants 0`
8
24
  // rejection surfaces as `participants.humans: ...` — which leaks the wire
@@ -20,11 +36,13 @@ const SCHEMA_PATH_TO_FLAG = {
20
36
  metadata: "--metadata",
21
37
  callback: "--callback",
22
38
  input_data: "--input-data",
39
+ title: "--title",
23
40
  "artifact.id": "--artifact-id",
24
41
  "artifact.version": "--version",
25
42
  "artifact.type": "--artifact-type",
26
43
  "artifact.source": "--artifact",
27
44
  "artifact.event_schema": "--event-schema",
45
+ "artifact.input_schema": "--input-schema",
28
46
  };
29
47
  function schemaPathToFlag(path) {
30
48
  const dotted = path.map(String).join(".");
@@ -39,16 +57,16 @@ function schemaPathToFlag(path) {
39
57
  }
40
58
  return dotted;
41
59
  }
42
- export const createHelp = `pane create — create a Pane session
60
+ export const createHelp = `pane session create — create a Pane session
43
61
 
44
62
  A session is one use of an artifact. Supply the artifact in ONE of two ways:
45
63
 
46
64
  Reference form — instance an existing reusable artifact (the cheap path,
47
65
  no HTML re-sent):
48
- pane create --artifact-id <id|slug> [--version <n>] [--input-data <v>]
66
+ pane session create --artifact-id <id|slug> [--version <n>] [--input-data <v>]
49
67
 
50
68
  Inline form — a one-off artifact, defined on this call:
51
- pane create --artifact <path|inline> [--event-schema <path|json>] [options]
69
+ pane session create --artifact <path|inline> [--event-schema <path|json>] [options]
52
70
 
53
71
  Exactly one of --artifact-id / --artifact must be given.
54
72
 
@@ -84,8 +102,22 @@ Artifact (choose one):
84
102
  emittedBy is any non-empty subset of ["page", "agent"].
85
103
  payload is a JSON Schema; the relay validates every
86
104
  emit against it. See docs/SPEC.md for the full grammar.
105
+ --input-schema <v> Inline-form input schema. A .json file, or inline JSON.
106
+ Optional with --artifact, rejected with --artifact-id
107
+ (the schema comes from the pinned artifact version
108
+ there). When present, the session's --input-data is
109
+ validated against it AND any blob ids declared at a
110
+ "format": "pane-blob-id" site become reachable from the
111
+ page via window.pane.downloadBlob. Without it, blob
112
+ refs in --input-data are silently unreachable. See
113
+ docs/SPEC.md and #208.
87
114
 
88
115
  Options:
116
+ --title <text> Tab title shown to the human (max 80 chars, single
117
+ line). Required, with one ergonomic exception: when
118
+ --artifact-id references a named artifact, the relay
119
+ falls back to Artifact.name. Inline (--artifact …) form
120
+ always needs --title.
89
121
  --input-data <v> This instance's seed data — a JSON object (file path or
90
122
  inline JSON), validated by the relay against the artifact
91
123
  version's input_schema. The page reads it as
@@ -110,6 +142,7 @@ Output (stdout, JSON):
110
142
 
111
143
  Deliver urls.humans to the human(s); keep tokens.agent for the WS stream.`;
112
144
  export async function runCreate(args) {
145
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane session create");
113
146
  const artifactIdVal = args.flags.get("artifact-id");
114
147
  const artifactVal = args.flags.get("artifact");
115
148
  // Exactly one of the two artifact forms must be present.
@@ -126,7 +159,11 @@ export async function runCreate(args) {
126
159
  const candidate = {};
127
160
  if (artifactIdVal !== undefined) {
128
161
  // Reference form — instance an existing named artifact. --artifact /
129
- // --event-schema are not needed here.
162
+ // --event-schema / --input-schema are not used here: the artifact's
163
+ // pinned version carries them already.
164
+ if (args.flags.get("input-schema") !== undefined) {
165
+ fail("--input-schema is incompatible with --artifact-id — the input schema comes from the pinned artifact version. Author the schema on the artifact (`pane artifact create --input-schema …`) instead.", "invalid_args");
166
+ }
130
167
  const ref = { id: artifactIdVal };
131
168
  const versionRaw = args.flags.get("version");
132
169
  if (versionRaw !== undefined) {
@@ -139,12 +176,18 @@ export async function runCreate(args) {
139
176
  candidate["artifact"] = ref;
140
177
  }
141
178
  else {
142
- // Inline form — the event schema rides inside the `artifact` object; the
143
- // relay transparently creates an anonymous artifact behind it.
144
- // --event-schema is optional: omitting it makes a view-only one-off (a
145
- // report/dashboard the human only views), and the relay then rejects every
146
- // page/agent emit.
179
+ // Inline form — the event + input schemas ride inside the `artifact`
180
+ // object; the relay transparently creates an anonymous artifact behind
181
+ // it. Both schemas are optional:
182
+ // - --event-schema absent view-only one-off (no page/agent emits)
183
+ // - --input-schema absent → no input contract; --input-data passes
184
+ // through unvalidated AND any blob ids in it are unreachable from
185
+ // the page (the participant blob-download bridge walks input_data
186
+ // against the artifact version's inputSchema). Pass --input-schema
187
+ // when --input-data carries blob refs the page needs to render.
188
+ // See #208.
147
189
  const schemaVal = args.flags.get("event-schema");
190
+ const inputSchemaVal = args.flags.get("input-schema");
148
191
  const artifactType = (args.flags.get("artifact-type") ?? "html-inline");
149
192
  if (artifactType !== "html-inline" && artifactType !== "html-ref") {
150
193
  fail("--artifact-type must be 'html-inline' or 'html-ref'", "invalid_args");
@@ -158,8 +201,10 @@ export async function runCreate(args) {
158
201
  catch (e) {
159
202
  fail(e instanceof Error ? e.message : String(e), "invalid_args");
160
203
  }
161
- // Build the inline artifact object. event_schema is OMITTED entirely (not
162
- // set to undefined) when --event-schema is absent — a view-only artifact.
204
+ // Build the inline artifact object. event_schema / input_schema are
205
+ // OMITTED entirely (not set to undefined) when their flags are absent —
206
+ // omission is meaningful at the relay (view-only artifact / no input
207
+ // contract).
163
208
  const inlineArtifact = {
164
209
  type: artifactType,
165
210
  source,
@@ -172,8 +217,29 @@ export async function runCreate(args) {
172
217
  fail(e instanceof Error ? e.message : String(e), "invalid_args");
173
218
  }
174
219
  }
220
+ if (inputSchemaVal !== undefined) {
221
+ try {
222
+ const v = resolveJson(inputSchemaVal, "--input-schema");
223
+ if (v === null || typeof v !== "object" || Array.isArray(v)) {
224
+ fail("--input-schema must be a JSON object", "invalid_args");
225
+ }
226
+ inlineArtifact["input_schema"] = v;
227
+ }
228
+ catch (e) {
229
+ fail(e instanceof Error ? e.message : String(e), "invalid_args");
230
+ }
231
+ }
175
232
  candidate["artifact"] = inlineArtifact;
176
233
  }
234
+ // --title — passthrough, no client-side requiredness. The relay is the
235
+ // single source of truth: it enforces "required, with --artifact-id +
236
+ // Artifact.name as the only fallback" and the shape rules (length, control
237
+ // chars). Keeping all that server-side avoids drift between the CLI's
238
+ // pre-checks and the relay's actual rules.
239
+ const titleRaw = args.flags.get("title");
240
+ if (titleRaw !== undefined) {
241
+ candidate["title"] = titleRaw;
242
+ }
177
243
  // --input-data — per-instance seed data, applies to either form (the relay
178
244
  // validates it against the pinned version's input_schema).
179
245
  const inputDataRaw = args.flags.get("input-data");
@@ -1,10 +1,13 @@
1
- // `pane delete <id>` — close/delete a session.
1
+ // `pane session delete <id>` — close/delete a session.
2
+ import { assertKnownFlags } from "../argv.js";
2
3
  import { makeClient } from "../config.js";
3
4
  import { printJson, fail, failFromError } from "../output.js";
4
- export const deleteHelp = `pane delete — close/delete a session
5
+ const KNOWN_FLAGS = [];
6
+ const KNOWN_BOOLS = [];
7
+ export const deleteHelp = `pane session delete — close/delete a session
5
8
 
6
9
  Usage:
7
- pane delete <session-id> [options]
10
+ pane session delete <session-id> [options]
8
11
 
9
12
  Closes and deletes the session (DELETE /v1/sessions/:id). Idempotent on the
10
13
  relay side — deleting an already-closed session still succeeds.
@@ -17,6 +20,7 @@ Options:
17
20
  Output (stdout, JSON):
18
21
  { session_id, deleted: true }`;
19
22
  export async function runDelete(args) {
23
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane session delete");
20
24
  const sessionId = args.positionals[0];
21
25
  if (!sessionId)
22
26
  fail("missing <session-id>", "invalid_args");
@@ -1,5 +1,9 @@
1
+ import { assertKnownFlags } from "../argv.js";
1
2
  import { makeClient } from "../config.js";
2
3
  import { printJson, fail, failFromError } from "../output.js";
4
+ const CREATE_FLAGS = ["type", "message", "session-id"];
5
+ const LIST_FLAGS = ["limit", "before"];
6
+ const NO_BOOLS = [];
3
7
  export const feedbackHelp = `pane feedback — submit / list feedback to the relay operator
4
8
 
5
9
  Feedback is a one-shot bug report, feature request, or note from YOUR agent
@@ -48,6 +52,7 @@ async function readStdin() {
48
52
  return Buffer.concat(chunks).toString("utf8");
49
53
  }
50
54
  async function runFeedbackCreate(args) {
55
+ assertKnownFlags(args, CREATE_FLAGS, NO_BOOLS, "pane feedback create");
51
56
  const type = args.flags.get("type");
52
57
  const rawMessage = args.flags.get("message");
53
58
  const sessionId = args.flags.get("session-id");
@@ -87,6 +92,7 @@ async function runFeedbackCreate(args) {
87
92
  }
88
93
  }
89
94
  async function runFeedbackList(args) {
95
+ assertKnownFlags(args, LIST_FLAGS, NO_BOOLS, "pane feedback list");
90
96
  const limitRaw = args.flags.get("limit");
91
97
  const before = args.flags.get("before");
92
98
  let limit;
@@ -1,17 +1,21 @@
1
- // `pane keys` — inspect or revoke the calling agent's API key.
1
+ // `pane key` — inspect or revoke the calling agent's API key.
2
2
  //
3
- // Flat command namespace: `keys` is one top-level command that branches on a
4
- // positional subcommand (list / revoke). The relay scopes /v1/keys to the
3
+ // Flat command namespace: `key` is one top-level noun that branches on a
4
+ // positional verb (list / revoke). The relay scopes /v1/keys to the
5
5
  // authenticated agent, so there is exactly one key — the caller's own. Both
6
- // subcommands therefore act ONLY on the caller's own key.
6
+ // verbs therefore act ONLY on the caller's own key.
7
+ import { assertKnownFlags } from "../argv.js";
7
8
  import { makeClient } from "../config.js";
8
9
  import { printJson, fail, failFromError } from "../output.js";
9
- export const keysHelp = `pane keys — inspect or revoke YOUR agent's API key
10
+ const NO_FLAGS = [];
11
+ const NO_BOOLS = [];
12
+ const REVOKE_BOOLS = ["yes"];
13
+ export const keyHelp = `pane key — inspect or revoke YOUR agent's API key
10
14
 
11
15
  Usage:
12
- pane keys <subcommand> [options]
16
+ pane key <verb> [options]
13
17
 
14
- Subcommands:
18
+ Verbs:
15
19
  list Show YOUR agent's key info. The relay scopes keys to the
16
20
  authenticated agent — there is exactly one key per agent, your
17
21
  own. Prints { agent_id, name, key_prefix, created_at,
@@ -19,18 +23,19 @@ Subcommands:
19
23
 
20
24
  revoke Revoke YOUR OWN API key — a self-destruct. The key stops working
21
25
  IMMEDIATELY; every subsequent command fails until you run
22
- 'pane register' again to provision a new key. The relay only
26
+ 'pane agent register' again to provision a new key. The relay only
23
27
  allows revoking your own key. Requires --yes to confirm.
24
28
  Prints { revoked: true, agent_id }.
25
29
 
26
30
  Options:
27
- --yes Confirm 'keys revoke' (required — it is irreversible).
31
+ --yes Confirm 'key revoke' (required — it is irreversible).
28
32
  --url <url> Relay base URL (overrides PANE_URL).
29
33
  --api-key <key> Agent API key (overrides PANE_API_KEY).
30
34
  -h, --help Show this help.
31
35
 
32
36
  Output: stdout is machine-readable JSON.`;
33
- async function runKeysList(args) {
37
+ async function runKeyList(args) {
38
+ assertKnownFlags(args, NO_FLAGS, NO_BOOLS, "pane key list");
34
39
  const client = makeClient(args);
35
40
  try {
36
41
  const info = await client.listKeys();
@@ -40,9 +45,10 @@ async function runKeysList(args) {
40
45
  failFromError(e);
41
46
  }
42
47
  }
43
- async function runKeysRevoke(args) {
48
+ async function runKeyRevoke(args) {
49
+ assertKnownFlags(args, NO_FLAGS, REVOKE_BOOLS, "pane key revoke");
44
50
  if (!args.bools.has("yes")) {
45
- fail("'pane keys revoke' revokes YOUR OWN API key — it stops working " +
51
+ fail("'pane key revoke' revokes YOUR OWN API key — it stops working " +
46
52
  "immediately and is irreversible. Pass --yes to confirm.", "confirmation_required");
47
53
  }
48
54
  const client = makeClient(args);
@@ -58,19 +64,19 @@ async function runKeysRevoke(args) {
58
64
  failFromError(e);
59
65
  }
60
66
  }
61
- export async function runKeys(args) {
67
+ export async function runKey(args) {
62
68
  const sub = args.positionals[0];
63
69
  switch (sub) {
64
70
  case "list":
65
- await runKeysList(args);
71
+ await runKeyList(args);
66
72
  break;
67
73
  case "revoke":
68
- await runKeysRevoke(args);
74
+ await runKeyRevoke(args);
69
75
  break;
70
76
  case undefined:
71
- fail("missing subcommand — usage: pane keys <list|revoke> (run 'pane keys --help')", "invalid_args");
77
+ fail("missing verb — usage: pane key <list|revoke> (run 'pane key --help')", "invalid_args");
72
78
  break;
73
79
  default:
74
- fail(`unknown keys subcommand '${sub}' — expected list|revoke (run 'pane keys --help')`, "invalid_args");
80
+ fail(`unknown key verb '${sub}' — expected list|revoke (run 'pane key --help')`, "invalid_args");
75
81
  }
76
82
  }
@@ -0,0 +1,91 @@
1
+ // `pane session list` — enumerate YOUR agent's sessions.
2
+ //
3
+ // The recovery primitive when the create-response was dropped: the URL itself
4
+ // is unrecoverable (the relay stores only the token hash), but every other
5
+ // field of the session is intact and listable here. Pair with
6
+ // `pane session participant new` to mint a fresh URL on a session whose
7
+ // original was lost.
8
+ import { assertKnownFlags } from "../argv.js";
9
+ import { makeClient } from "../config.js";
10
+ import { printJson, fail, failFromError } from "../output.js";
11
+ const KNOWN_FLAGS = ["status", "limit", "cursor", "artifact-id"];
12
+ const KNOWN_BOOLS = [];
13
+ export const listHelp = `pane session list — list YOUR agent's sessions
14
+
15
+ Prints sessions (newest first) with no secrets in the response. Participant
16
+ tokens are stored hashed and CANNOT be recovered — if you lost a session URL,
17
+ mint a fresh one with 'pane session participant new <session-id>'.
18
+
19
+ Usage:
20
+ pane session list [options]
21
+
22
+ Options:
23
+ --status <s> open | closed | all. Default: open. The status reported
24
+ is the EFFECTIVE status — a row whose ttl is in the past
25
+ is reported as 'closed' even if not yet swept.
26
+ --limit <N> Page size (default 50, max 200).
27
+ --cursor <c> Opaque cursor from a previous page's next_cursor.
28
+ --artifact-id <i> Filter to sessions instantiated from a specific named
29
+ artifact (head id; not version id). Inline (anonymous)
30
+ artifacts cannot be filtered this way — they have no
31
+ stable handle.
32
+ --url <url> Relay base URL (overrides PANE_URL).
33
+ --api-key <key> Agent API key (overrides PANE_API_KEY).
34
+ -h, --help Show this help.
35
+
36
+ Recovery recipe (lost the URL but the session is still alive):
37
+ pane session list # find the
38
+ # session_id +
39
+ # participant_id
40
+ # you lost
41
+ pane session participant new <session-id> # mint a fresh URL
42
+ pane session participant revoke <session-id> <p-id> # invalidate the
43
+ # old URL
44
+
45
+ Output (stdout, JSON):
46
+ {
47
+ items: [
48
+ {
49
+ session_id, title, status, artifact_id, artifact_version_id,
50
+ artifact_version, participants: [...], created_at, expires_at,
51
+ has_callback
52
+ },
53
+ ...
54
+ ],
55
+ next_cursor: <opaque|null>
56
+ }`;
57
+ const STATUSES = ["open", "closed", "all"];
58
+ export async function runList(args) {
59
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane session list");
60
+ const opts = {};
61
+ const status = args.flags.get("status");
62
+ if (status !== undefined) {
63
+ if (!STATUSES.includes(status)) {
64
+ fail(`--status must be one of: ${STATUSES.join(", ")} (got '${status}')`, "invalid_args");
65
+ }
66
+ opts.status = status;
67
+ }
68
+ const limitRaw = args.flags.get("limit");
69
+ if (limitRaw !== undefined) {
70
+ const n = Number(limitRaw);
71
+ if (!Number.isInteger(n) || n <= 0 || n > 200) {
72
+ fail(`--limit must be an integer in 1..200 (got '${limitRaw}')`, "invalid_args");
73
+ }
74
+ opts.limit = n;
75
+ }
76
+ const cursor = args.flags.get("cursor");
77
+ if (cursor !== undefined && cursor !== "")
78
+ opts.cursor = cursor;
79
+ const artifactId = args.flags.get("artifact-id");
80
+ if (artifactId !== undefined && artifactId !== "") {
81
+ opts.artifact_id = artifactId;
82
+ }
83
+ const client = makeClient(args);
84
+ try {
85
+ const page = await client.listSessions(opts);
86
+ printJson(page);
87
+ }
88
+ catch (e) {
89
+ failFromError(e);
90
+ }
91
+ }