@paneui/cli 0.0.3 → 0.0.5
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/dist/commands/artifact.js +39 -3
- package/dist/commands/create.js +37 -1
- package/dist/commands/feedback.js +127 -0
- package/dist/commands/register.js +9 -1
- package/dist/commands/skill.js +136 -0
- package/dist/commands/state.js +34 -5
- package/dist/commands/taste.js +28 -19
- package/dist/commands/watch.js +83 -18
- package/dist/config.js +22 -1
- package/dist/index.js +36 -3
- package/dist/output.js +37 -0
- package/dist/upgrade.js +115 -0
- package/dist/version.js +11 -0
- package/package.json +2 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// `pane artifact` — manage reusable, versioned artifacts.
|
|
2
2
|
//
|
|
3
3
|
// Flat command namespace: `artifact` is one top-level command that branches on
|
|
4
|
-
// a positional subcommand (create / version / update / search / list / show
|
|
4
|
+
// a positional subcommand (create / version / update / search / list / show /
|
|
5
|
+
// delete).
|
|
5
6
|
// An artifact is a reusable UI template (HTML + event schema + optional input
|
|
6
7
|
// schema); a session is one *use* of one version of it. Authoring an artifact
|
|
7
8
|
// once and instancing it via `pane create --artifact-id` removes the per-use
|
|
@@ -27,6 +28,9 @@ Subcommands:
|
|
|
27
28
|
search Search the agent's named artifacts (lean — no HTML).
|
|
28
29
|
list List the agent's named artifacts (search with no query).
|
|
29
30
|
show Show a full artifact: head metadata + its version list.
|
|
31
|
+
delete Permanently delete an artifact and ALL its versions. Requires
|
|
32
|
+
--yes. Refused with 409 conflict if any session (open or
|
|
33
|
+
closed) still references the artifact — delete those first.
|
|
30
34
|
|
|
31
35
|
pane artifact create --name <n> --artifact <path|inline>
|
|
32
36
|
[--event-schema <path|json>] [--slug <s>]
|
|
@@ -54,6 +58,13 @@ Subcommands:
|
|
|
54
58
|
pane artifact show <id|slug>
|
|
55
59
|
Prints the full artifact: head metadata + every version's content.
|
|
56
60
|
|
|
61
|
+
pane artifact delete <id|slug> --yes
|
|
62
|
+
Permanently deletes the artifact and all its versions. Refused
|
|
63
|
+
(409 conflict) if any session in any state still references one
|
|
64
|
+
of the artifact's versions — run 'pane delete <session-id>' on
|
|
65
|
+
each first, or wait for the relay's TTL sweeper to reclaim them.
|
|
66
|
+
Prints { artifact, deleted: true } on success.
|
|
67
|
+
|
|
57
68
|
Options:
|
|
58
69
|
--name <n> Artifact display name (required for 'create').
|
|
59
70
|
--slug <s> Stable, agent-chosen handle (unique per agent). The
|
|
@@ -301,6 +312,28 @@ async function runArtifactShow(args) {
|
|
|
301
312
|
failFromError(e);
|
|
302
313
|
}
|
|
303
314
|
}
|
|
315
|
+
// `pane artifact delete <id|slug> --yes` — remove an artifact (and, server-
|
|
316
|
+
// side, all its versions). The relay refuses with 409 conflict if any
|
|
317
|
+
// session still references it; the CLI surfaces that as the relay-supplied
|
|
318
|
+
// envelope. `--yes` is required because there's no Undo button on a delete
|
|
319
|
+
// and the same `pane artifact create` slug isn't reservable once gone.
|
|
320
|
+
async function runArtifactDelete(args) {
|
|
321
|
+
const idOrSlug = args.positionals[1];
|
|
322
|
+
if (!idOrSlug) {
|
|
323
|
+
fail("missing artifact <id|slug> — usage: pane artifact delete <id|slug> --yes", "invalid_args");
|
|
324
|
+
}
|
|
325
|
+
if (!args.bools.has("yes")) {
|
|
326
|
+
fail("'pane artifact delete' permanently removes the artifact and all its versions — it is destructive. Pass --yes to confirm.", "invalid_args");
|
|
327
|
+
}
|
|
328
|
+
const client = makeClient(args);
|
|
329
|
+
try {
|
|
330
|
+
await client.deleteArtifact(idOrSlug);
|
|
331
|
+
printJson({ artifact: idOrSlug, deleted: true });
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
failFromError(e);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
304
337
|
export async function runArtifact(args) {
|
|
305
338
|
const sub = args.positionals[0];
|
|
306
339
|
switch (sub) {
|
|
@@ -322,10 +355,13 @@ export async function runArtifact(args) {
|
|
|
322
355
|
case "show":
|
|
323
356
|
await runArtifactShow(args);
|
|
324
357
|
break;
|
|
358
|
+
case "delete":
|
|
359
|
+
await runArtifactDelete(args);
|
|
360
|
+
break;
|
|
325
361
|
case undefined:
|
|
326
|
-
fail("missing subcommand — usage: pane artifact <create|version|update|search|list|show> (run 'pane artifact --help')", "invalid_args");
|
|
362
|
+
fail("missing subcommand — usage: pane artifact <create|version|update|search|list|show|delete> (run 'pane artifact --help')", "invalid_args");
|
|
327
363
|
break;
|
|
328
364
|
default:
|
|
329
|
-
fail(`unknown artifact subcommand '${sub}' — expected create|version|update|search|list|show (run 'pane artifact --help')`, "invalid_args");
|
|
365
|
+
fail(`unknown artifact subcommand '${sub}' — expected create|version|update|search|list|show|delete (run 'pane artifact --help')`, "invalid_args");
|
|
330
366
|
}
|
|
331
367
|
}
|
package/dist/commands/create.js
CHANGED
|
@@ -3,6 +3,42 @@ import { createSessionSchema } from "@paneui/core";
|
|
|
3
3
|
import { makeClient } from "../config.js";
|
|
4
4
|
import { resolveJson, resolveText } from "../input.js";
|
|
5
5
|
import { printJson, fail, failFromError } from "../output.js";
|
|
6
|
+
// Translate a Zod schema path (e.g. ["participants","humans"]) back to the
|
|
7
|
+
// public CLI flag the user actually typed. Without this, a `--participants 0`
|
|
8
|
+
// rejection surfaces as `participants.humans: ...` — which leaks the wire
|
|
9
|
+
// shape and refers to no flag the user could fix.
|
|
10
|
+
//
|
|
11
|
+
// Match strategy: longest prefix wins. Schema paths whose top segment isn't
|
|
12
|
+
// in the table fall back to dotted notation so we degrade gracefully on
|
|
13
|
+
// fields that don't have a single corresponding flag (e.g. `artifact.source`
|
|
14
|
+
// — there's no single --artifact-source flag for the inline form, just
|
|
15
|
+
// --artifact pointing at the whole blob).
|
|
16
|
+
const SCHEMA_PATH_TO_FLAG = {
|
|
17
|
+
participants: "--participants",
|
|
18
|
+
"participants.humans": "--participants",
|
|
19
|
+
ttl: "--ttl",
|
|
20
|
+
metadata: "--metadata",
|
|
21
|
+
callback: "--callback",
|
|
22
|
+
input_data: "--input-data",
|
|
23
|
+
"artifact.id": "--artifact-id",
|
|
24
|
+
"artifact.version": "--version",
|
|
25
|
+
"artifact.type": "--artifact-type",
|
|
26
|
+
"artifact.source": "--artifact",
|
|
27
|
+
"artifact.event_schema": "--event-schema",
|
|
28
|
+
};
|
|
29
|
+
function schemaPathToFlag(path) {
|
|
30
|
+
const dotted = path.map(String).join(".");
|
|
31
|
+
// Longest prefix that has a mapping. Try the full path first, then strip
|
|
32
|
+
// one trailing segment at a time. Falls back to dotted notation as the
|
|
33
|
+
// honest default.
|
|
34
|
+
for (let i = path.length; i > 0; i--) {
|
|
35
|
+
const prefix = path.slice(0, i).map(String).join(".");
|
|
36
|
+
const flag = SCHEMA_PATH_TO_FLAG[prefix];
|
|
37
|
+
if (flag !== undefined)
|
|
38
|
+
return flag;
|
|
39
|
+
}
|
|
40
|
+
return dotted;
|
|
41
|
+
}
|
|
6
42
|
export const createHelp = `pane create — create a Pane session
|
|
7
43
|
|
|
8
44
|
A session is one use of an artifact. Supply the artifact in ONE of two ways:
|
|
@@ -184,7 +220,7 @@ export async function runCreate(args) {
|
|
|
184
220
|
const parsed = createSessionSchema.safeParse(candidate);
|
|
185
221
|
if (!parsed.success) {
|
|
186
222
|
const issue = parsed.error.issues[0];
|
|
187
|
-
const where = issue && issue.path.length > 0 ? issue.path
|
|
223
|
+
const where = issue && issue.path.length > 0 ? schemaPathToFlag(issue.path) : "request";
|
|
188
224
|
fail(`invalid create request: ${where}: ${issue ? issue.message : "validation failed"}`, "invalid_args", parsed.error.flatten());
|
|
189
225
|
}
|
|
190
226
|
const req = parsed.data;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { makeClient } from "../config.js";
|
|
2
|
+
import { printJson, fail, failFromError } from "../output.js";
|
|
3
|
+
export const feedbackHelp = `pane feedback — submit / list feedback to the relay operator
|
|
4
|
+
|
|
5
|
+
Feedback is a one-shot bug report, feature request, or note from YOUR agent
|
|
6
|
+
to whoever runs the relay. Submissions are stored in the relay DB; the
|
|
7
|
+
operator triages out of band.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
pane feedback <subcommand> [options]
|
|
11
|
+
|
|
12
|
+
Subcommands:
|
|
13
|
+
create Submit one feedback row. Requires --type and --message.
|
|
14
|
+
Prints { id, type, created_at } — the message is not echoed back.
|
|
15
|
+
|
|
16
|
+
list List YOUR agent's own submissions, newest first. Prints
|
|
17
|
+
{ items: [...], next_before?: <cursor> }. Pass --before <cursor>
|
|
18
|
+
from a previous page to fetch the next page.
|
|
19
|
+
|
|
20
|
+
Options for 'create':
|
|
21
|
+
--type <bug|feature|note> Feedback category. Required.
|
|
22
|
+
--message <text|-> Message body. Pass '-' to read from stdin.
|
|
23
|
+
1..4000 chars after trim.
|
|
24
|
+
--session-id <id> Optional session this feedback relates to;
|
|
25
|
+
must belong to YOUR agent.
|
|
26
|
+
|
|
27
|
+
Options for 'list':
|
|
28
|
+
--limit <N> Page size (default 50, max 100).
|
|
29
|
+
--before <cursor> Opaque cursor from a previous page's next_before.
|
|
30
|
+
|
|
31
|
+
Global:
|
|
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
|
+
Examples:
|
|
37
|
+
pane feedback create --type bug --message "watch hangs on empty session"
|
|
38
|
+
echo "long-form note..." | pane feedback create --type note --message -
|
|
39
|
+
pane feedback list --limit 20
|
|
40
|
+
|
|
41
|
+
Output: stdout is machine-readable JSON.`;
|
|
42
|
+
const FEEDBACK_TYPES = ["bug", "feature", "note"];
|
|
43
|
+
async function readStdin() {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for await (const chunk of process.stdin) {
|
|
46
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
47
|
+
}
|
|
48
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
49
|
+
}
|
|
50
|
+
async function runFeedbackCreate(args) {
|
|
51
|
+
const type = args.flags.get("type");
|
|
52
|
+
const rawMessage = args.flags.get("message");
|
|
53
|
+
const sessionId = args.flags.get("session-id");
|
|
54
|
+
if (type === undefined) {
|
|
55
|
+
fail("'pane feedback create' requires --type <bug|feature|note>", "invalid_args");
|
|
56
|
+
}
|
|
57
|
+
if (!FEEDBACK_TYPES.includes(type)) {
|
|
58
|
+
fail(`unknown --type '${type}' — expected one of: ${FEEDBACK_TYPES.join(", ")}`, "invalid_args");
|
|
59
|
+
}
|
|
60
|
+
if (rawMessage === undefined) {
|
|
61
|
+
fail("'pane feedback create' requires --message <text|-> (use '-' to read from stdin)", "invalid_args");
|
|
62
|
+
}
|
|
63
|
+
let message;
|
|
64
|
+
if (rawMessage === "-") {
|
|
65
|
+
if (process.stdin.isTTY) {
|
|
66
|
+
fail("'pane feedback create --message -' expects feedback on stdin, but stdin is a TTY", "invalid_args");
|
|
67
|
+
}
|
|
68
|
+
message = await readStdin();
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
message = rawMessage;
|
|
72
|
+
}
|
|
73
|
+
if (message.trim().length === 0) {
|
|
74
|
+
fail("feedback message must not be empty or whitespace-only", "invalid_args");
|
|
75
|
+
}
|
|
76
|
+
const client = makeClient(args);
|
|
77
|
+
try {
|
|
78
|
+
const res = await client.submitFeedback({
|
|
79
|
+
type: type,
|
|
80
|
+
message,
|
|
81
|
+
...(sessionId !== undefined ? { sessionId } : {}),
|
|
82
|
+
});
|
|
83
|
+
printJson(res);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
failFromError(e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function runFeedbackList(args) {
|
|
90
|
+
const limitRaw = args.flags.get("limit");
|
|
91
|
+
const before = args.flags.get("before");
|
|
92
|
+
let limit;
|
|
93
|
+
if (limitRaw !== undefined) {
|
|
94
|
+
const n = Number(limitRaw);
|
|
95
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
96
|
+
fail(`--limit must be a positive integer, got '${limitRaw}'`, "invalid_args");
|
|
97
|
+
}
|
|
98
|
+
limit = n;
|
|
99
|
+
}
|
|
100
|
+
const client = makeClient(args);
|
|
101
|
+
try {
|
|
102
|
+
const page = await client.listFeedback({
|
|
103
|
+
...(limit !== undefined ? { limit } : {}),
|
|
104
|
+
...(before !== undefined ? { before } : {}),
|
|
105
|
+
});
|
|
106
|
+
printJson(page);
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
failFromError(e);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export async function runFeedback(args) {
|
|
113
|
+
const sub = args.positionals[0];
|
|
114
|
+
switch (sub) {
|
|
115
|
+
case "create":
|
|
116
|
+
await runFeedbackCreate(args);
|
|
117
|
+
break;
|
|
118
|
+
case "list":
|
|
119
|
+
await runFeedbackList(args);
|
|
120
|
+
break;
|
|
121
|
+
case undefined:
|
|
122
|
+
fail("missing subcommand — usage: pane feedback <create|list> (run 'pane feedback --help')", "invalid_args");
|
|
123
|
+
break;
|
|
124
|
+
default:
|
|
125
|
+
fail(`unknown feedback subcommand '${sub}' — expected create|list (run 'pane feedback --help')`, "invalid_args");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// works with only PANE_URL (or nothing) set.
|
|
8
8
|
import { registerAgent, PaneApiError } from "@paneui/core";
|
|
9
9
|
import { DEFAULT_RELAY_URL } from "../config.js";
|
|
10
|
-
import { printJson, fail } from "../output.js";
|
|
10
|
+
import { printJson, fail, failUpgradeRequired } from "../output.js";
|
|
11
11
|
import { readStore, writeStore } from "../store.js";
|
|
12
|
+
import { VERSION } from "../version.js";
|
|
12
13
|
export const registerHelp = `pane register — register this agent with the relay and save the key locally
|
|
13
14
|
|
|
14
15
|
Usage:
|
|
@@ -48,10 +49,17 @@ export async function runRegister(args) {
|
|
|
48
49
|
url: url.replace(/\/$/, ""),
|
|
49
50
|
...(name !== undefined ? { name } : {}),
|
|
50
51
|
...(secret !== undefined && secret !== "" ? { secret } : {}),
|
|
52
|
+
cliVersion: VERSION,
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
catch (e) {
|
|
54
56
|
if (e instanceof PaneApiError) {
|
|
57
|
+
// 426 cli_upgrade_required goes through the shared upgrade-message
|
|
58
|
+
// path (stderr block + exit 75) so the SKILL.md's instructions to the
|
|
59
|
+
// agent's harness fire on `pane register` too.
|
|
60
|
+
if (e.status === 426 && e.code === "cli_upgrade_required") {
|
|
61
|
+
failUpgradeRequired(e);
|
|
62
|
+
}
|
|
55
63
|
if (e.status === 429) {
|
|
56
64
|
fail("registration rate limit exceeded — try again later", "rate_limited", undefined, { hint: e.hint, retryable: e.retryable, docs_url: e.docsUrl });
|
|
57
65
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// `pane skill` — fetch the relay's SKILL.md, or just its version.
|
|
2
|
+
//
|
|
3
|
+
// The relay serves its skill at GET /skills/pane/SKILL.md and its version
|
|
4
|
+
// at GET /skills/pane/SKILL.md/version (see
|
|
5
|
+
// packages/relay/src/http/routes/skill.ts). The skill is auto-updating:
|
|
6
|
+
// the relay's deployed image owns both the body and the version, so the
|
|
7
|
+
// agent always reads what the relay it's actually talking to wants it
|
|
8
|
+
// to read.
|
|
9
|
+
//
|
|
10
|
+
// Two subcommands:
|
|
11
|
+
// `pane skill` — print the full markdown to stdout (the
|
|
12
|
+
// install / refresh path; pipe to a file).
|
|
13
|
+
// `pane skill version` — print just the relay's skill version (the
|
|
14
|
+
// "is my local copy stale?" probe). The agent
|
|
15
|
+
// compares this to the `<!-- pane skill v… -->`
|
|
16
|
+
// comment in its local skill file and re-runs
|
|
17
|
+
// `pane skill > <path>` when they differ.
|
|
18
|
+
//
|
|
19
|
+
// Both are unauthenticated — the skill route is public on the relay and
|
|
20
|
+
// an agent on a too-old CLI must be able to read the upgrade instructions
|
|
21
|
+
// even before it has registered (or before its key was minted).
|
|
22
|
+
import { resolveRelayUrl } from "../config.js";
|
|
23
|
+
import { fail } from "../output.js";
|
|
24
|
+
import { VERSION } from "../version.js";
|
|
25
|
+
export const skillHelp = `pane skill — fetch the relay's SKILL.md (or its version)
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
pane skill Print the full skill to stdout.
|
|
29
|
+
pane skill version [--plain] Print just the relay's skill version.
|
|
30
|
+
|
|
31
|
+
The skill is auto-updating: the relay's deployed image owns the version,
|
|
32
|
+
so this is always the skill that matches the relay you are talking to.
|
|
33
|
+
|
|
34
|
+
Unauthenticated — no API key needed. An agent can call either form
|
|
35
|
+
before 'pane register' to bootstrap or refresh its local skill copy.
|
|
36
|
+
|
|
37
|
+
Subcommands:
|
|
38
|
+
(bare) Fetch GET /skills/pane/SKILL.md and write the raw
|
|
39
|
+
markdown to stdout. Pipe to your local skill path:
|
|
40
|
+
pane skill > ~/.claude/skills/pane/SKILL.md
|
|
41
|
+
version Fetch GET /skills/pane/SKILL.md/version and print
|
|
42
|
+
the relay's skill version. Default output is the
|
|
43
|
+
JSON envelope; --plain prints just the version
|
|
44
|
+
string so an agent can compare it inline in shell.
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--plain (with 'version' only) print the bare version
|
|
48
|
+
string on stdout, no JSON envelope. Useful inside
|
|
49
|
+
a shell pipeline: \`if [ "$(pane skill version
|
|
50
|
+
--plain)" != "$LOCAL" ]; then ...\`.
|
|
51
|
+
--url <url> Relay base URL (overrides PANE_URL).
|
|
52
|
+
-h, --help Show this help.
|
|
53
|
+
|
|
54
|
+
Output (stdout):
|
|
55
|
+
(bare) Raw markdown, as served by the relay.
|
|
56
|
+
version { "version": "1.0.0" } — or '1.0.0\\n' with --plain.
|
|
57
|
+
|
|
58
|
+
Errors (stderr): { "error": { "code", "message" } } and non-zero exit.`;
|
|
59
|
+
// Shared fetch with the consistent x-pane-cli-version header (the skill
|
|
60
|
+
// routes are exempt from the version-skew middleware, but sending it lets
|
|
61
|
+
// access logs see which CLI versions are reading the skill).
|
|
62
|
+
async function fetchOrFail(url) {
|
|
63
|
+
try {
|
|
64
|
+
return await fetch(url, {
|
|
65
|
+
headers: { "x-pane-cli-version": VERSION },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
70
|
+
fail(`could not reach ${url}: ${msg}`, "fetch_error");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function failOnNon2xx(res, target) {
|
|
74
|
+
if (res.ok)
|
|
75
|
+
return;
|
|
76
|
+
// 404 if the operator stripped the route, 5xx on a static-read failure.
|
|
77
|
+
// Surface the body inline — it may carry a useful message.
|
|
78
|
+
const body = await res.text().catch(() => "");
|
|
79
|
+
fail(`relay returned ${res.status} for ${target}${body ? ": " + body.slice(0, 200) : ""}`, "relay_error");
|
|
80
|
+
}
|
|
81
|
+
// `pane skill` (no positional) — print the full skill.
|
|
82
|
+
async function runSkillFetch(args) {
|
|
83
|
+
const url = resolveRelayUrl(args);
|
|
84
|
+
const target = url + "/skills/pane/SKILL.md";
|
|
85
|
+
const res = await fetchOrFail(target);
|
|
86
|
+
await failOnNon2xx(res, target);
|
|
87
|
+
const text = await res.text();
|
|
88
|
+
process.stdout.write(text);
|
|
89
|
+
// Ensure the markdown ends with a newline so a pipe-reader (cat | xargs |
|
|
90
|
+
// claude) sees a clean line-terminated boundary even if the relay served
|
|
91
|
+
// a file without a trailing newline.
|
|
92
|
+
if (!text.endsWith("\n"))
|
|
93
|
+
process.stdout.write("\n");
|
|
94
|
+
}
|
|
95
|
+
// `pane skill version [--plain]` — print just the version.
|
|
96
|
+
async function runSkillVersion(args) {
|
|
97
|
+
const url = resolveRelayUrl(args);
|
|
98
|
+
const target = url + "/skills/pane/SKILL.md/version";
|
|
99
|
+
const res = await fetchOrFail(target);
|
|
100
|
+
await failOnNon2xx(res, target);
|
|
101
|
+
// The relay returns { version: "x.y.z" }. We tolerate a missing/
|
|
102
|
+
// malformed body so a misbehaving relay can't crash this probe — fall
|
|
103
|
+
// through to "0.0.0" the same way the relay does when its own SKILL.md
|
|
104
|
+
// lacks a version comment.
|
|
105
|
+
let body;
|
|
106
|
+
try {
|
|
107
|
+
body = await res.json();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
body = null;
|
|
111
|
+
}
|
|
112
|
+
const version = body !== null &&
|
|
113
|
+
typeof body === "object" &&
|
|
114
|
+
typeof body.version === "string"
|
|
115
|
+
? body.version
|
|
116
|
+
: "0.0.0";
|
|
117
|
+
if (args.bools.has("plain")) {
|
|
118
|
+
process.stdout.write(version + "\n");
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
process.stdout.write(JSON.stringify({ version }) + "\n");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export async function runSkill(args) {
|
|
125
|
+
const sub = args.positionals[0];
|
|
126
|
+
switch (sub) {
|
|
127
|
+
case undefined:
|
|
128
|
+
await runSkillFetch(args);
|
|
129
|
+
break;
|
|
130
|
+
case "version":
|
|
131
|
+
await runSkillVersion(args);
|
|
132
|
+
break;
|
|
133
|
+
default:
|
|
134
|
+
fail(`unknown skill subcommand '${sub}' — expected 'version' or no subcommand (run 'pane skill --help')`, "invalid_args");
|
|
135
|
+
}
|
|
136
|
+
}
|
package/dist/commands/state.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `pane state <id>` —
|
|
1
|
+
// `pane state <id>` — snapshot of a session, optionally long-polled.
|
|
2
2
|
import { makeClient } from "../config.js";
|
|
3
3
|
import { printJson, fail, failFromError } from "../output.js";
|
|
4
4
|
export const stateHelp = `pane state — show a session's metadata and event log
|
|
@@ -6,11 +6,24 @@ export const stateHelp = `pane state — show a session's metadata and event log
|
|
|
6
6
|
Usage:
|
|
7
7
|
pane state <session-id> [options]
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
log (GET /v1/sessions/:id/events) and prints them together.
|
|
9
|
+
By default non-blocking: fetches session metadata (GET /v1/sessions/:id) plus
|
|
10
|
+
the event log (GET /v1/sessions/:id/events) and prints them together.
|
|
11
|
+
|
|
12
|
+
With --wait, blocks at the relay for up to <secs> if no new events are
|
|
13
|
+
available since the cursor — returns as soon as something lands. Use this
|
|
14
|
+
for headless polling agents that can't keep a WebSocket open (cron,
|
|
15
|
+
FaaS, slow links): poll, then re-poll using next_cursor as --since on the
|
|
16
|
+
next call. Compared to 'pane watch', it's higher latency per round-trip
|
|
17
|
+
but no long-lived connection.
|
|
11
18
|
|
|
12
19
|
Options:
|
|
13
|
-
--since <cursor> Only return events after this opaque cursor.
|
|
20
|
+
--since <cursor> Only return events after this opaque cursor. Pass
|
|
21
|
+
next_cursor from the previous call to chain pages.
|
|
22
|
+
--wait <secs> Long-poll window. The relay holds the request open
|
|
23
|
+
for up to this many seconds, capped server-side at
|
|
24
|
+
30. Without --since, this still returns immediately
|
|
25
|
+
with whatever events exist — long-poll only blocks
|
|
26
|
+
when there are NO new events to return.
|
|
14
27
|
--url <url> Relay base URL (overrides PANE_URL).
|
|
15
28
|
--api-key <key> Agent API key (overrides PANE_API_KEY).
|
|
16
29
|
-h, --help Show this help.
|
|
@@ -22,10 +35,26 @@ export async function runState(args) {
|
|
|
22
35
|
if (!sessionId)
|
|
23
36
|
fail("missing <session-id>", "invalid_args");
|
|
24
37
|
const since = args.flags.get("since") ?? null;
|
|
38
|
+
// --wait <secs>: hand the server the long-poll window. The relay caps
|
|
39
|
+
// this at 30s; we pass the raw value and let the relay clamp (sending
|
|
40
|
+
// a higher number is not an error, just a clamp). 0 or unset means
|
|
41
|
+
// non-blocking — the default snapshot behaviour.
|
|
42
|
+
let waitSeconds;
|
|
43
|
+
const waitRaw = args.flags.get("wait");
|
|
44
|
+
if (waitRaw !== undefined) {
|
|
45
|
+
const n = Number(waitRaw);
|
|
46
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
47
|
+
fail("--wait must be a non-negative number of seconds", "invalid_args");
|
|
48
|
+
}
|
|
49
|
+
waitSeconds = n;
|
|
50
|
+
}
|
|
25
51
|
const client = makeClient(args);
|
|
26
52
|
try {
|
|
27
53
|
const meta = await client.getSession(sessionId);
|
|
28
|
-
const page = await client.getEvents(sessionId, {
|
|
54
|
+
const page = await client.getEvents(sessionId, {
|
|
55
|
+
since,
|
|
56
|
+
...(waitSeconds !== undefined ? { waitSeconds } : {}),
|
|
57
|
+
});
|
|
29
58
|
printJson({ meta, events: page.events, next_cursor: page.next_cursor });
|
|
30
59
|
}
|
|
31
60
|
catch (e) {
|
package/dist/commands/taste.js
CHANGED
|
@@ -37,16 +37,18 @@ Subcommands:
|
|
|
37
37
|
{ taste: string|null, updated_at: string|null, bytes: number }.
|
|
38
38
|
taste is null and bytes is 0 when notes have never been written.
|
|
39
39
|
|
|
40
|
-
set Whole-blob replace.
|
|
41
|
-
(
|
|
42
|
-
|
|
40
|
+
set Whole-blob replace. Source the markdown via --file <path>,
|
|
41
|
+
--file - (read stdin), or by piping into 'pane taste set' with
|
|
42
|
+
no flag. The relay rejects empty/whitespace-only payloads and
|
|
43
|
+
caps the blob at MAX_TASTE_BYTES (utf8). To clear the notes,
|
|
43
44
|
use 'pane taste clear', not 'set' with an empty body.
|
|
44
45
|
|
|
45
46
|
clear Delete the notes. Requires --yes (it is destructive). Prints
|
|
46
47
|
{ cleared: true }.
|
|
47
48
|
|
|
48
49
|
Options:
|
|
49
|
-
--file <path
|
|
50
|
+
--file <path|-> Source for 'set' — a file path, or '-' to read stdin
|
|
51
|
+
explicitly. Omit to fall back to piped stdin.
|
|
50
52
|
--yes Confirm 'clear'.
|
|
51
53
|
--url <url> Relay base URL (overrides PANE_URL).
|
|
52
54
|
--api-key <key> Agent API key (overrides PANE_API_KEY).
|
|
@@ -54,17 +56,17 @@ Options:
|
|
|
54
56
|
|
|
55
57
|
Examples:
|
|
56
58
|
pane taste get
|
|
57
|
-
echo "- denser layout\\n- no rounded corners" | pane taste set
|
|
58
59
|
pane taste set --file ./taste.md
|
|
60
|
+
pane taste set --file - # explicit stdin
|
|
61
|
+
echo "- denser layout" | pane taste set
|
|
59
62
|
pane taste clear --yes
|
|
60
63
|
|
|
61
64
|
Output: stdout is machine-readable JSON.`;
|
|
62
|
-
// Drain process.stdin to a utf8 string.
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
+
// Drain process.stdin to a utf8 string. The caller is responsible for
|
|
66
|
+
// deciding that stdin should be read (e.g. an explicit `--file -`, or a
|
|
67
|
+
// non-TTY stdin where data is actually piped). In a TTY this would block
|
|
68
|
+
// waiting for ^D, so the caller MUST gate on `process.stdin.isTTY` first.
|
|
65
69
|
async function readStdin() {
|
|
66
|
-
if (process.stdin.isTTY)
|
|
67
|
-
return "";
|
|
68
70
|
const chunks = [];
|
|
69
71
|
for await (const chunk of process.stdin) {
|
|
70
72
|
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
@@ -83,15 +85,19 @@ async function runTasteGet(args) {
|
|
|
83
85
|
}
|
|
84
86
|
async function runTasteSet(args) {
|
|
85
87
|
const filePath = args.flags.get("file");
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
// Source the blob deterministically — no isTTY-flag fusing, because
|
|
89
|
+
// `!process.stdin.isTTY` is true under every non-interactive caller
|
|
90
|
+
// (pipes, redirects, closed fd, CI, agent harnesses) and would wrongly
|
|
91
|
+
// reject `--file` for the entire target audience. See issue #148.
|
|
92
|
+
//
|
|
93
|
+
// --file - → explicit stdin sentinel
|
|
94
|
+
// --file <path> → read that path (works in TTY and non-TTY alike)
|
|
95
|
+
// (no --file) → fall back to stdin IF non-TTY; error in a TTY
|
|
93
96
|
let taste;
|
|
94
|
-
if (filePath
|
|
97
|
+
if (filePath === "-") {
|
|
98
|
+
taste = await readStdin();
|
|
99
|
+
}
|
|
100
|
+
else if (filePath !== undefined) {
|
|
95
101
|
try {
|
|
96
102
|
taste = readFileSync(filePath, "utf8");
|
|
97
103
|
}
|
|
@@ -99,9 +105,12 @@ async function runTasteSet(args) {
|
|
|
99
105
|
fail(`failed to read --file '${filePath}': ${e instanceof Error ? e.message : String(e)}`, "invalid_args");
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
|
-
else {
|
|
108
|
+
else if (!process.stdin.isTTY) {
|
|
103
109
|
taste = await readStdin();
|
|
104
110
|
}
|
|
111
|
+
else {
|
|
112
|
+
fail("'pane taste set' needs input — pass --file <path>, pipe markdown on stdin, or use --file -", "invalid_args");
|
|
113
|
+
}
|
|
105
114
|
if (taste.trim().length === 0) {
|
|
106
115
|
fail("'pane taste set' refuses an empty or whitespace-only blob — use 'pane taste clear --yes' to delete the notes", "invalid_args");
|
|
107
116
|
}
|
package/dist/commands/watch.js
CHANGED
|
@@ -7,6 +7,7 @@ import { openStream } from "@paneui/core";
|
|
|
7
7
|
import { resolveConfig } from "../config.js";
|
|
8
8
|
import { PaneClient } from "@paneui/core";
|
|
9
9
|
import { printJsonLine, fail } from "../output.js";
|
|
10
|
+
import { VERSION } from "../version.js";
|
|
10
11
|
export const watchHelp = `pane watch — stream a session's events as JSON-lines
|
|
11
12
|
|
|
12
13
|
Usage:
|
|
@@ -20,12 +21,30 @@ exits 0.
|
|
|
20
21
|
Modes:
|
|
21
22
|
(bare) Run until SIGINT (Ctrl-C). Exit 0.
|
|
22
23
|
--once Exit 0 after the first event.
|
|
23
|
-
--type <t>
|
|
24
|
+
--type <t[,t2,…]> Exit 0 after the first event whose type is in this
|
|
25
|
+
comma-separated set. Without --filter-type, stdout
|
|
26
|
+
still prints EVERY event until the match — --type
|
|
27
|
+
controls the EXIT condition, --filter-type controls
|
|
28
|
+
the OUTPUT.
|
|
24
29
|
|
|
25
30
|
Options:
|
|
31
|
+
--filter-type <t[,t2,…]>
|
|
32
|
+
Print only events whose type is in this set.
|
|
33
|
+
system.* events (lifecycle: participant.joined,
|
|
34
|
+
session.expired, …) and the terminal {"type":
|
|
35
|
+
"_closed"} line always pass through, so the
|
|
36
|
+
harness still sees them. Combine with --type X
|
|
37
|
+
--filter-type X for "stream only X events and
|
|
38
|
+
exit on the first one" — the literal-reading of
|
|
39
|
+
--type alone that agents often expect.
|
|
26
40
|
--since <cursor> Replay only events after this opaque cursor.
|
|
27
|
-
--timeout <secs> Fail with code ws_timeout if
|
|
28
|
-
|
|
41
|
+
--timeout <secs> Wall-clock max wait. Fail with code ws_timeout if
|
|
42
|
+
the natural exit condition (--once, --type, session
|
|
43
|
+
close) doesn't happen within this many seconds.
|
|
44
|
+
Frames arriving DO NOT reset the timer — this is
|
|
45
|
+
the budget for "give up on the human", not an idle
|
|
46
|
+
detector. Without --once or --type, bare watch
|
|
47
|
+
will simply exit non-zero at the deadline.
|
|
29
48
|
--url <url> Relay base URL (overrides PANE_URL).
|
|
30
49
|
--api-key <key> Agent API key (overrides PANE_API_KEY).
|
|
31
50
|
-h, --help Show this help.
|
|
@@ -34,14 +53,52 @@ Each line is one event envelope: { id, session_id, author, ts, type, data,
|
|
|
34
53
|
causation_id, idempotency_key }. The terminal line is {"type":"_closed"}.
|
|
35
54
|
|
|
36
55
|
Pattern — Claude Code Monitor tool: run \`pane watch <id> --type form.submitted\`
|
|
37
|
-
as a monitored process; the harness re-invokes the model when the line lands
|
|
56
|
+
as a monitored process; the harness re-invokes the model when the line lands.
|
|
57
|
+
|
|
58
|
+
Wait for any of several events:
|
|
59
|
+
pane watch <id> --type form.submitted,form.cancelled --timeout 60
|
|
60
|
+
|
|
61
|
+
Stream only matching events to stdout, exit on the first:
|
|
62
|
+
pane watch <id> --type form.submitted --filter-type form.submitted`;
|
|
63
|
+
// Parse a comma-separated event-type list (e.g. "form.submitted,form.cancelled")
|
|
64
|
+
// into a Set. Empty/whitespace entries are dropped. Returns null when the flag
|
|
65
|
+
// wasn't given (so callers can distinguish "no filter" from "empty filter").
|
|
66
|
+
// Exported for unit-test coverage; the wrapper around the actual openStream
|
|
67
|
+
// integration is hard to test in isolation.
|
|
68
|
+
export function parseTypeList(raw) {
|
|
69
|
+
if (raw === undefined)
|
|
70
|
+
return null;
|
|
71
|
+
const types = raw
|
|
72
|
+
.split(",")
|
|
73
|
+
.map((t) => t.trim())
|
|
74
|
+
.filter((t) => t.length > 0);
|
|
75
|
+
return new Set(types);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Decide whether `--filter-type` lets this event through to stdout. Lifecycle
|
|
79
|
+
* `system.*` events always pass — without that an agent waiting on
|
|
80
|
+
* `--filter-type form.submitted` would never see `system.participant.joined`
|
|
81
|
+
* and miss the "the human opened the URL" signal. Exported for testing.
|
|
82
|
+
*/
|
|
83
|
+
export function shouldPrintEvent(eventType, filterTypes) {
|
|
84
|
+
if (filterTypes === null)
|
|
85
|
+
return true;
|
|
86
|
+
if (eventType.startsWith("system."))
|
|
87
|
+
return true;
|
|
88
|
+
return filterTypes.has(eventType);
|
|
89
|
+
}
|
|
38
90
|
export async function runWatch(args) {
|
|
39
91
|
const sessionId = args.positionals[0];
|
|
40
92
|
if (!sessionId)
|
|
41
93
|
fail("missing <session-id>", "invalid_args");
|
|
42
94
|
const cfg = resolveConfig(args);
|
|
43
95
|
const since = args.flags.get("since") ?? null;
|
|
44
|
-
|
|
96
|
+
// --type controls the EXIT condition (set of types that trigger exit 0
|
|
97
|
+
// on first match). --filter-type controls OUTPUT (the only event types
|
|
98
|
+
// printed to stdout; system.* and _closed always pass through). Each
|
|
99
|
+
// flag is independent — combine them only if you really want both.
|
|
100
|
+
const exitTypes = parseTypeList(args.flags.get("type"));
|
|
101
|
+
const filterTypes = parseTypeList(args.flags.get("filter-type"));
|
|
45
102
|
const once = args.bools.has("once");
|
|
46
103
|
let timeoutSec = null;
|
|
47
104
|
const timeoutRaw = args.flags.get("timeout");
|
|
@@ -51,7 +108,11 @@ export async function runWatch(args) {
|
|
|
51
108
|
fail("--timeout must be a positive number", "invalid_args");
|
|
52
109
|
timeoutSec = t;
|
|
53
110
|
}
|
|
54
|
-
const client = new PaneClient({
|
|
111
|
+
const client = new PaneClient({
|
|
112
|
+
url: cfg.url,
|
|
113
|
+
apiKey: cfg.apiKey,
|
|
114
|
+
cliVersion: VERSION,
|
|
115
|
+
});
|
|
55
116
|
let exited = false;
|
|
56
117
|
const finish = (code) => {
|
|
57
118
|
if (exited)
|
|
@@ -73,19 +134,20 @@ export async function runWatch(args) {
|
|
|
73
134
|
// Track whether the relay told us the session expired before the socket
|
|
74
135
|
// closed — a 1006/1008/1011 close after that is still a clean shutdown.
|
|
75
136
|
let sawSessionExpired = false;
|
|
76
|
-
//
|
|
137
|
+
// Wall-clock timeout. The reporter's mental model (#137) and the skill
|
|
138
|
+
// text both treat this as "max wait until something happens" — i.e. an
|
|
139
|
+
// agent giving up on a human who never acts. The previous behaviour
|
|
140
|
+
// (clear the timer on first frame, never re-arm) made `--timeout`
|
|
141
|
+
// useless once any frame arrived, even a system.participant.joined
|
|
142
|
+
// emitted the moment a human connected. Frames now DO NOT reset the
|
|
143
|
+
// timer; the only ways `--timeout` doesn't fire are the natural exit
|
|
144
|
+
// conditions (--once, --type match, session close) finishing first.
|
|
77
145
|
let timer;
|
|
78
146
|
if (timeoutSec !== null) {
|
|
79
147
|
timer = setTimeout(() => {
|
|
80
|
-
fail(`no
|
|
148
|
+
fail(`no terminal condition met within ${timeoutSec}s`, "ws_timeout");
|
|
81
149
|
}, timeoutSec * 1000);
|
|
82
150
|
}
|
|
83
|
-
const sawFrame = () => {
|
|
84
|
-
if (timer) {
|
|
85
|
-
clearTimeout(timer);
|
|
86
|
-
timer = undefined;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
151
|
const handle = openStream({
|
|
90
152
|
wsBaseUrl: client.wsBaseUrl,
|
|
91
153
|
sessionId: sessionId,
|
|
@@ -93,11 +155,14 @@ export async function runWatch(args) {
|
|
|
93
155
|
since,
|
|
94
156
|
}, {
|
|
95
157
|
onReplayComplete: () => {
|
|
96
|
-
|
|
158
|
+
// No-op: replay-complete is informational, no timer interaction.
|
|
97
159
|
},
|
|
98
160
|
onEvent: (event) => {
|
|
99
|
-
|
|
100
|
-
|
|
161
|
+
// Output filter: print only events the agent asked for. See
|
|
162
|
+
// shouldPrintEvent — system.* lifecycle events always pass.
|
|
163
|
+
if (shouldPrintEvent(event.type, filterTypes)) {
|
|
164
|
+
printJsonLine(event);
|
|
165
|
+
}
|
|
101
166
|
// A system.session.expired event means the session is closing.
|
|
102
167
|
if (event.type === "system.session.expired") {
|
|
103
168
|
sawSessionExpired = true;
|
|
@@ -108,7 +173,7 @@ export async function runWatch(args) {
|
|
|
108
173
|
finish(0);
|
|
109
174
|
return;
|
|
110
175
|
}
|
|
111
|
-
if (
|
|
176
|
+
if (exitTypes !== null && exitTypes.has(event.type)) {
|
|
112
177
|
finish(0);
|
|
113
178
|
}
|
|
114
179
|
},
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { PaneClient } from "@paneui/core";
|
|
4
4
|
import { fail } from "./output.js";
|
|
5
5
|
import { readStore, storePath } from "./store.js";
|
|
6
|
+
import { VERSION } from "./version.js";
|
|
6
7
|
/**
|
|
7
8
|
* Resolve url + apiKey and report the SOURCE of each, WITHOUT making a network
|
|
8
9
|
* call and WITHOUT failing on a missing value (unlike `resolveConfig`). The
|
|
@@ -73,5 +74,25 @@ export function resolveConfig(args) {
|
|
|
73
74
|
/** Build a PaneClient from resolved config. */
|
|
74
75
|
export function makeClient(args) {
|
|
75
76
|
const cfg = resolveConfig(args);
|
|
76
|
-
return new PaneClient({
|
|
77
|
+
return new PaneClient({
|
|
78
|
+
url: cfg.url,
|
|
79
|
+
apiKey: cfg.apiKey,
|
|
80
|
+
// Sent as `x-pane-cli-version` on every relay request so the relay can
|
|
81
|
+
// return 426 cli_upgrade_required when this CLI is too old. Single
|
|
82
|
+
// source: ./version.ts.
|
|
83
|
+
cliVersion: VERSION,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve just the relay URL — same precedence as `resolveConfig` but
|
|
88
|
+
* without insisting on an API key. For commands that hit unauthenticated
|
|
89
|
+
* relay routes (e.g. `pane skill` → GET /skills/pane/SKILL.md).
|
|
90
|
+
*/
|
|
91
|
+
export function resolveRelayUrl(args) {
|
|
92
|
+
const store = readStore();
|
|
93
|
+
const url = args.flags.get("url") ??
|
|
94
|
+
process.env.PANE_URL ??
|
|
95
|
+
store.url ??
|
|
96
|
+
DEFAULT_RELAY_URL;
|
|
97
|
+
return url.replace(/\/$/, "");
|
|
77
98
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,8 +14,12 @@ import { runConfig, configHelp } from "./commands/config.js";
|
|
|
14
14
|
import { runLogout, logoutHelp } from "./commands/logout.js";
|
|
15
15
|
import { runKeys, keysHelp } from "./commands/keys.js";
|
|
16
16
|
import { runTaste, tasteHelp } from "./commands/taste.js";
|
|
17
|
+
import { runFeedback, feedbackHelp } from "./commands/feedback.js";
|
|
17
18
|
import { runDelete, deleteHelp } from "./commands/delete.js";
|
|
18
|
-
|
|
19
|
+
import { runSkill, skillHelp } from "./commands/skill.js";
|
|
20
|
+
import { VERSION } from "./version.js";
|
|
21
|
+
import { PaneApiError } from "@paneui/core";
|
|
22
|
+
import { failUpgradeRequired } from "./output.js";
|
|
19
23
|
const ROOT_HELP = `pane — a round-trip UI channel between agents and humans
|
|
20
24
|
|
|
21
25
|
Usage:
|
|
@@ -27,7 +31,7 @@ Commands:
|
|
|
27
31
|
create Create a session (POST /v1/sessions). Prints session_id,
|
|
28
32
|
urls, tokens, expires_at.
|
|
29
33
|
artifact Manage reusable, versioned artifacts (create / version /
|
|
30
|
-
update / search / list / show).
|
|
34
|
+
update / search / list / show / delete).
|
|
31
35
|
state <id> Non-blocking snapshot: session metadata + event log.
|
|
32
36
|
send <id> Emit an agent event into a session.
|
|
33
37
|
watch <id> Stream a session's events as JSON-lines on stdout
|
|
@@ -38,8 +42,13 @@ Commands:
|
|
|
38
42
|
(get / set / clear) — presentation preferences the agent
|
|
39
43
|
has learned from human feedback and reads before
|
|
40
44
|
generating a pane artifact.
|
|
45
|
+
feedback Submit / list one-shot feedback to the relay operator
|
|
46
|
+
(create / list) — bug reports, feature requests, notes.
|
|
41
47
|
config Show the resolved relay config (no network call).
|
|
42
48
|
logout Clear the locally-saved relay URL + API key.
|
|
49
|
+
skill Fetch the relay's SKILL.md to stdout, or just its
|
|
50
|
+
version with 'pane skill version'. Used to install
|
|
51
|
+
and keep the local skill copy in sync; no API key.
|
|
43
52
|
|
|
44
53
|
Run \`pane <command> --help\` for command-specific options.
|
|
45
54
|
|
|
@@ -65,7 +74,14 @@ Output: stdout is machine-readable JSON; errors go to stderr as
|
|
|
65
74
|
// handled from rawArgv[0] before parseArgs runs, so it never needs to be a
|
|
66
75
|
// boolean flag — and keeping it out lets `pane create --version <n>` /
|
|
67
76
|
// `pane artifact version` consume a value as a normal value-flag.
|
|
68
|
-
const BOOLEAN_FLAGS = new Set([
|
|
77
|
+
const BOOLEAN_FLAGS = new Set([
|
|
78
|
+
"json",
|
|
79
|
+
"once",
|
|
80
|
+
"help",
|
|
81
|
+
"print-key",
|
|
82
|
+
"yes",
|
|
83
|
+
"plain",
|
|
84
|
+
]);
|
|
69
85
|
async function main() {
|
|
70
86
|
const rawArgv = process.argv.slice(2);
|
|
71
87
|
// Version: handle before anything else.
|
|
@@ -105,8 +121,10 @@ async function main() {
|
|
|
105
121
|
delete: deleteHelp,
|
|
106
122
|
keys: keysHelp,
|
|
107
123
|
taste: tasteHelp,
|
|
124
|
+
feedback: feedbackHelp,
|
|
108
125
|
config: configHelp,
|
|
109
126
|
logout: logoutHelp,
|
|
127
|
+
skill: skillHelp,
|
|
110
128
|
};
|
|
111
129
|
if (!(command in helps)) {
|
|
112
130
|
process.stderr.write(JSON.stringify({
|
|
@@ -149,15 +167,30 @@ async function main() {
|
|
|
149
167
|
case "taste":
|
|
150
168
|
await runTaste(args);
|
|
151
169
|
break;
|
|
170
|
+
case "feedback":
|
|
171
|
+
await runFeedback(args);
|
|
172
|
+
break;
|
|
152
173
|
case "config":
|
|
153
174
|
await runConfig(args);
|
|
154
175
|
break;
|
|
155
176
|
case "logout":
|
|
156
177
|
await runLogout();
|
|
157
178
|
break;
|
|
179
|
+
case "skill":
|
|
180
|
+
await runSkill(args);
|
|
181
|
+
break;
|
|
158
182
|
}
|
|
159
183
|
}
|
|
160
184
|
main().catch((err) => {
|
|
185
|
+
// Funnel 426 cli_upgrade_required through the dedicated upgrade-message
|
|
186
|
+
// path so a command that throws raw (instead of going through
|
|
187
|
+
// failFromError) still produces the exact stderr block + exit 75 the
|
|
188
|
+
// SKILL.md tells the agent's harness to expect.
|
|
189
|
+
if (err instanceof PaneApiError &&
|
|
190
|
+
err.code === "cli_upgrade_required" &&
|
|
191
|
+
err.status === 426) {
|
|
192
|
+
failUpgradeRequired(err);
|
|
193
|
+
}
|
|
161
194
|
process.stderr.write(JSON.stringify({
|
|
162
195
|
error: {
|
|
163
196
|
code: "internal",
|
package/dist/output.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// stdout/stderr helpers. The CLI is JSON-by-default: machine-readable on
|
|
2
2
|
// stdout, human errors on stderr.
|
|
3
3
|
import { PaneApiError } from "@paneui/core";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { detectInstallMethod, upgradeCommandFor, formatUpgradeMessage, EXIT_CLI_UPGRADE_REQUIRED, } from "./upgrade.js";
|
|
4
6
|
/** Print a value as pretty JSON to stdout. */
|
|
5
7
|
export function printJson(value) {
|
|
6
8
|
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
@@ -29,6 +31,16 @@ export function fail(message, code = "error", details, extra) {
|
|
|
29
31
|
}
|
|
30
32
|
/** Translate a thrown error (incl. PaneApiError) into a fail() exit. */
|
|
31
33
|
export function failFromError(err) {
|
|
34
|
+
// 426 cli_upgrade_required gets its own dedicated exit path: a
|
|
35
|
+
// human-readable upgrade message on stderr and a stable exit code
|
|
36
|
+
// (sysexits EX_TEMPFAIL = 75) that the SKILL.md instructs the agent's
|
|
37
|
+
// harness to branch on. Everything else falls through to the generic
|
|
38
|
+
// JSON envelope below.
|
|
39
|
+
if (err instanceof PaneApiError &&
|
|
40
|
+
err.code === "cli_upgrade_required" &&
|
|
41
|
+
err.status === 426) {
|
|
42
|
+
failUpgradeRequired(err);
|
|
43
|
+
}
|
|
32
44
|
if (err instanceof PaneApiError) {
|
|
33
45
|
fail(err.message, err.code, err.details, {
|
|
34
46
|
hint: err.hint,
|
|
@@ -38,3 +50,28 @@ export function failFromError(err) {
|
|
|
38
50
|
}
|
|
39
51
|
fail(err instanceof Error ? err.message : String(err), "internal");
|
|
40
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Print the upgrade message to stderr and exit 75. Pulled out of
|
|
55
|
+
* failFromError so the top-level main().catch can also funnel through it
|
|
56
|
+
* — the two entry points must produce identical output for the SKILL.md's
|
|
57
|
+
* "if you see exit 75…" instructions to be reliable.
|
|
58
|
+
*
|
|
59
|
+
* The install-method detection reads `import.meta.url` of the CLI entry,
|
|
60
|
+
* resolved from the call site that imports this module. Inlining the
|
|
61
|
+
* resolution here keeps each command's own error-handling free of the
|
|
62
|
+
* detail.
|
|
63
|
+
*/
|
|
64
|
+
export function failUpgradeRequired(err) {
|
|
65
|
+
// The CLI entry is packages/cli/dist/index.js (after build) or
|
|
66
|
+
// packages/cli/src/index.ts (when running from source via tsx). Either
|
|
67
|
+
// way, the detector only looks at the path's shape, so resolving from
|
|
68
|
+
// *this* file works — output.ts sits alongside index.ts/index.js in
|
|
69
|
+
// both layouts.
|
|
70
|
+
const entryPath = fileURLToPath(import.meta.url);
|
|
71
|
+
const method = detectInstallMethod(entryPath);
|
|
72
|
+
const details = (err.details ?? {});
|
|
73
|
+
const minVersion = typeof details.min_version === "string" ? details.min_version : "0.0.0";
|
|
74
|
+
const command = upgradeCommandFor(method, minVersion);
|
|
75
|
+
process.stderr.write(formatUpgradeMessage(err, method, command) + "\n");
|
|
76
|
+
process.exit(EXIT_CLI_UPGRADE_REQUIRED);
|
|
77
|
+
}
|
package/dist/upgrade.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// CLI auto-upgrade helpers — install-method detection and message formatting.
|
|
2
|
+
//
|
|
3
|
+
// Called by the top-level error handler when a relay returns 426
|
|
4
|
+
// `cli_upgrade_required`. The goal is to print a single, machine-parseable
|
|
5
|
+
// line the agent can lift verbatim — and tell the human (or the agent's
|
|
6
|
+
// harness) exactly what to run instead of a generic "upgrade @paneui/cli".
|
|
7
|
+
//
|
|
8
|
+
// Detection is best-effort: we inspect `process.execPath` and the CLI's own
|
|
9
|
+
// install path. There's no programmatic "ask npm what installed me" API, so
|
|
10
|
+
// the heuristics below are matched against the well-known install layouts
|
|
11
|
+
// of each package manager. Anything unrecognized lands in `unknown`, which
|
|
12
|
+
// means "tell the agent to ask the human" rather than guess.
|
|
13
|
+
/**
|
|
14
|
+
* Detection rules, ordered most-specific to least. Each rule looks at the
|
|
15
|
+
* directory the CLI is running from — caller passes `import.meta.url`-
|
|
16
|
+
* derived absolute path for the CLI entry. The actual file at that path
|
|
17
|
+
* doesn't need to exist; we're only pattern-matching the path itself, so
|
|
18
|
+
* tests can pass synthetic strings.
|
|
19
|
+
*
|
|
20
|
+
* Patterns are deliberately loose (substring tests, not exact prefixes) so
|
|
21
|
+
* the same rule handles per-user installs (`~/.npm-global/lib/node_modules/`),
|
|
22
|
+
* system installs (`/usr/lib/node_modules/`), and the macOS/Linux
|
|
23
|
+
* variations within each manager — without listing every layout.
|
|
24
|
+
*/
|
|
25
|
+
export function detectInstallMethod(entryPath) {
|
|
26
|
+
// Volta wraps every binary in a shim under ~/.volta/tools/image/packages/
|
|
27
|
+
// and re-exports it via ~/.volta/bin/. Either path is a positive match.
|
|
28
|
+
if (entryPath.includes("/.volta/"))
|
|
29
|
+
return "volta";
|
|
30
|
+
// Bun's global registry: ~/.bun/install/global/node_modules/@paneui/cli/...
|
|
31
|
+
if (entryPath.includes("/.bun/install/global/"))
|
|
32
|
+
return "bun-global";
|
|
33
|
+
// npm global, in both common shapes:
|
|
34
|
+
// /usr/(local/)?lib/node_modules/@paneui/cli/... (system)
|
|
35
|
+
// ~/.npm-global/lib/node_modules/@paneui/cli/... (npm prefix)
|
|
36
|
+
// ~/.nvm/versions/node/vXX/lib/node_modules/... (nvm)
|
|
37
|
+
if (/\/lib\/node_modules\/@paneui\/cli\//.test(entryPath) ||
|
|
38
|
+
/\/lib\/node_modules\/\.bin\//.test(entryPath)) {
|
|
39
|
+
return "npm-global";
|
|
40
|
+
}
|
|
41
|
+
// npx caches the package under ~/Library/Caches/_npx (macOS) or
|
|
42
|
+
// ~/.npm/_npx (Linux) and runs it from a node_modules inside that dir.
|
|
43
|
+
// Surface this distinctly from a real vendored install: with an npx
|
|
44
|
+
// execution there is no project package.json owning the version and no
|
|
45
|
+
// global to upgrade — the user runs `npx @paneui/cli@<version>` each
|
|
46
|
+
// time, so the right answer is "ask the human / re-run with a newer
|
|
47
|
+
// explicit version".
|
|
48
|
+
if (entryPath.includes("/_npx/"))
|
|
49
|
+
return "unknown";
|
|
50
|
+
// Vendored: the CLI lives inside the *project's* node_modules — i.e. the
|
|
51
|
+
// user did `npm i @paneui/cli` (no -g) and runs it via a local script.
|
|
52
|
+
// We can't safely upgrade this for them; package.json owns it.
|
|
53
|
+
if (entryPath.includes("/node_modules/@paneui/cli/"))
|
|
54
|
+
return "vendored";
|
|
55
|
+
// pnpm temp, asdf, or anything else.
|
|
56
|
+
return "unknown";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Returns the shell command the human (or the agent, in a sandbox it owns)
|
|
60
|
+
* can run to upgrade @paneui/cli to satisfy `minVersion`. `null` means "no
|
|
61
|
+
* portable command exists — escalate to the human."
|
|
62
|
+
*
|
|
63
|
+
* Always pin the upgrade target to `>=${minVersion}` instead of `@latest`
|
|
64
|
+
* so a self-hosted relay that requires 0.0.7 doesn't drag the client to a
|
|
65
|
+
* future 0.1.0 that may have its own incompatibilities. The trailing
|
|
66
|
+
* `@latest`-equivalent is fine for the operator who deliberately
|
|
67
|
+
* fast-forwards.
|
|
68
|
+
*/
|
|
69
|
+
export function upgradeCommandFor(method, minVersion) {
|
|
70
|
+
const spec = `@paneui/cli@>=${minVersion}`;
|
|
71
|
+
switch (method) {
|
|
72
|
+
case "npm-global":
|
|
73
|
+
return `npm install -g ${spec}`;
|
|
74
|
+
case "bun-global":
|
|
75
|
+
return `bun install -g ${spec}`;
|
|
76
|
+
case "volta":
|
|
77
|
+
return `volta install ${spec}`;
|
|
78
|
+
case "vendored":
|
|
79
|
+
case "unknown":
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The deterministic stderr block the CLI prints on a 426 response. The agent
|
|
85
|
+
* is expected to read this verbatim and (per SKILL.md) run the printed
|
|
86
|
+
* command, then re-run its original `pane` invocation once. Format is held
|
|
87
|
+
* stable across CLI versions so the skill's instructions don't drift — a
|
|
88
|
+
* change here is a contract change.
|
|
89
|
+
*/
|
|
90
|
+
export function formatUpgradeMessage(err, method, command) {
|
|
91
|
+
// The relay's 426 payload puts the two version strings under details. We
|
|
92
|
+
// tolerate a missing/malformed details object so a misbehaving relay
|
|
93
|
+
// can't crash the CLI's own error path — show whatever we have.
|
|
94
|
+
const details = (err.details ?? {});
|
|
95
|
+
const minVersion = typeof details.min_version === "string" ? details.min_version : "?";
|
|
96
|
+
const yourVersion = typeof details.your_version === "string" ? details.your_version : "?";
|
|
97
|
+
const lines = [];
|
|
98
|
+
lines.push(`pane: this relay requires @paneui/cli >= ${minVersion} (you have ${yourVersion}).`);
|
|
99
|
+
if (command !== null) {
|
|
100
|
+
lines.push(`To upgrade: ${command}`);
|
|
101
|
+
}
|
|
102
|
+
else if (method === "vendored") {
|
|
103
|
+
lines.push("Install method: vendored (inside a project's node_modules). Bump the @paneui/cli version in that project's package.json and re-install — the CLI isn't safe to upgrade globally for a vendored install.");
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
lines.push("Install method: unknown. Ask the human to upgrade @paneui/cli — the install path didn't match any pattern we recognize (npm-global, bun-global, volta).");
|
|
107
|
+
}
|
|
108
|
+
return lines.join("\n");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Stable exit code used by the CLI on a `cli_upgrade_required` response.
|
|
112
|
+
* Sysexits.h's `EX_TEMPFAIL` — "temporary failure; retry after fixing".
|
|
113
|
+
* Documented in SKILL.md so an agent's harness can branch on it.
|
|
114
|
+
*/
|
|
115
|
+
export const EXIT_CLI_UPGRADE_REQUIRED = 75;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Single source of truth for the CLI version string.
|
|
2
|
+
//
|
|
3
|
+
// - `pane --version` prints this verbatim.
|
|
4
|
+
// - Every PaneClient construction passes it as `cliVersion`, which surfaces
|
|
5
|
+
// as the `x-pane-cli-version` header on every relay request — drives the
|
|
6
|
+
// relay's version-skew check (HTTP 426 `cli_upgrade_required`).
|
|
7
|
+
//
|
|
8
|
+
// Keep this in lockstep with packages/cli/package.json's `version` field;
|
|
9
|
+
// they're consulted in different places (here for the runtime header,
|
|
10
|
+
// package.json for npm publish + dependency resolution).
|
|
11
|
+
export const VERSION = "0.0.5";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paneui/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Command-line client for the Pane relay: create sessions, inspect state, send and watch events.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"test:unit": "vitest run"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@paneui/core": "^0.0.
|
|
44
|
+
"@paneui/core": "^0.0.5"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.7.0",
|