@paneui/cli 0.0.9 → 0.0.11
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 +8 -8
- package/dist/argv.js +3 -3
- package/dist/commands/agent.js +10 -2
- package/dist/commands/attachment-token.js +2 -2
- package/dist/commands/attachment-upload.js +8 -10
- package/dist/commands/attachment.js +7 -7
- package/dist/commands/claim.js +1 -1
- package/dist/commands/config.js +232 -20
- package/dist/commands/create.js +132 -21
- package/dist/commands/delete.js +12 -12
- package/dist/commands/feedback.js +5 -5
- package/dist/commands/list.js +17 -17
- package/dist/commands/logout.js +43 -13
- package/dist/commands/participant.js +38 -38
- package/dist/commands/query.js +204 -0
- package/dist/commands/records.js +285 -0
- package/dist/commands/register.js +53 -15
- package/dist/commands/send.js +17 -17
- package/dist/commands/set-key.js +92 -0
- package/dist/commands/skill.js +1 -1
- package/dist/commands/state.js +12 -12
- package/dist/commands/taste.js +3 -3
- package/dist/commands/template-records.js +195 -0
- package/dist/commands/template.js +243 -35
- package/dist/commands/trash.js +102 -0
- package/dist/commands/watch.js +22 -22
- package/dist/config.js +87 -20
- package/dist/format.js +133 -0
- package/dist/index.js +97 -20
- package/dist/output.js +1 -1
- package/dist/store.js +167 -26
- package/dist/upgrade.js +1 -1
- package/dist/version.js +2 -2
- package/package.json +5 -3
- package/dist/commands/surface.js +0 -118
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// This is the one command that needs no API key: it is the call that obtains
|
|
4
4
|
// one. If the relay runs REGISTRATION_MODE=secret, pass the shared
|
|
5
5
|
// registration secret via --secret or PANE_REGISTER_SECRET. On success the key
|
|
6
|
-
// (and relay URL) are persisted
|
|
7
|
-
// works with only PANE_URL (or nothing) set.
|
|
6
|
+
// (and relay URL) are persisted under a named profile in the CLI config file,
|
|
7
|
+
// so every later command works with only PANE_URL (or nothing) set.
|
|
8
8
|
import { registerAgent, PaneApiError } from "@paneui/core";
|
|
9
9
|
import { assertKnownFlags } from "../argv.js";
|
|
10
10
|
import { DEFAULT_RELAY_URL } from "../config.js";
|
|
11
11
|
import { printJson, fail, failUpgradeRequired } from "../output.js";
|
|
12
|
-
import { readStore,
|
|
12
|
+
import { isValidProfileName, DEFAULT_PROFILE_NAME, readStore, resolveProfile, upsertProfile, } from "../store.js";
|
|
13
13
|
import { VERSION } from "../version.js";
|
|
14
14
|
const KNOWN_FLAGS = ["name", "secret"];
|
|
15
15
|
const KNOWN_BOOLS = ["print-key"];
|
|
@@ -18,14 +18,24 @@ export const registerHelp = `pane agent register — register this agent with th
|
|
|
18
18
|
Usage:
|
|
19
19
|
pane agent register [options]
|
|
20
20
|
|
|
21
|
-
Calls POST /v1/register, then saves the returned API key (and relay URL)
|
|
22
|
-
CLI config file — so afterwards every other command
|
|
23
|
-
set (no PANE_API_KEY needed).
|
|
21
|
+
Calls POST /v1/register, then saves the returned API key (and relay URL) under
|
|
22
|
+
a named profile in the CLI config file — so afterwards every other command
|
|
23
|
+
works with only PANE_URL set (no PANE_API_KEY needed).
|
|
24
|
+
|
|
25
|
+
If --profile is omitted, the registered key goes under the currently-active
|
|
26
|
+
profile (or 'default' for a fresh install). Pass --profile <name> to keep
|
|
27
|
+
multiple environments (dev/staging/prod) side by side; switch between them
|
|
28
|
+
with 'pane config use <name>' or '--profile <name>' / PANE_PROFILE.
|
|
24
29
|
|
|
25
30
|
Options:
|
|
26
|
-
--name <n> Agent display name. The relay defaults it
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
--name <n> Agent display name on the relay. The relay defaults it
|
|
32
|
+
if omitted.
|
|
33
|
+
--profile <name> Local profile name to save under. Defaults to the active
|
|
34
|
+
profile, or 'default' on a fresh install. Letters,
|
|
35
|
+
digits, _ and -, up to 32 chars.
|
|
36
|
+
--url <url> Relay base URL. Falls back to PANE_URL, then the active
|
|
37
|
+
profile, then the hosted Pane relay. Self-hosters set
|
|
38
|
+
this.
|
|
29
39
|
--secret <s> Registration secret, sent as a Bearer token. Only needed
|
|
30
40
|
when the relay uses REGISTRATION_MODE=secret. Falls back
|
|
31
41
|
to the PANE_REGISTER_SECRET env var.
|
|
@@ -34,16 +44,42 @@ Options:
|
|
|
34
44
|
-h, --help Show this help.
|
|
35
45
|
|
|
36
46
|
Output (stdout, JSON):
|
|
37
|
-
{ agent_id, key_prefix, saved_to } (+ api_key when --print-key
|
|
47
|
+
{ agent_id, key_prefix, profile, saved_to } (+ api_key when --print-key)
|
|
38
48
|
|
|
39
49
|
The API key is saved to the CLI config file (mode 0600); it is not printed
|
|
40
50
|
unless --print-key is passed.`;
|
|
41
51
|
export async function runRegister(args) {
|
|
42
52
|
assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane agent register");
|
|
53
|
+
// Profile selection for the WRITE side: --profile flag → PANE_PROFILE env
|
|
54
|
+
// → the store's current profile → DEFAULT_PROFILE_NAME ('default') for
|
|
55
|
+
// a fresh install. We deliberately don't fall through to "no profile, use
|
|
56
|
+
// a fresh name" — the agent needs to end up somewhere callable, and
|
|
57
|
+
// 'default' is a stable, predictable home.
|
|
58
|
+
const profileFlag = args.flags.get("profile") ?? process.env.PANE_PROFILE;
|
|
43
59
|
const store = readStore();
|
|
60
|
+
const profileName = profileFlag !== undefined && profileFlag !== ""
|
|
61
|
+
? profileFlag
|
|
62
|
+
: (store.currentProfile ?? DEFAULT_PROFILE_NAME);
|
|
63
|
+
if (!isValidProfileName(profileName)) {
|
|
64
|
+
fail(`invalid profile name '${profileName}' — letters, digits, _ and -, up to 32 chars`, "invalid_args");
|
|
65
|
+
}
|
|
66
|
+
// URL precedence for the relay we're registering against:
|
|
67
|
+
// --url flag > PANE_URL env > target-profile's existing url > default.
|
|
68
|
+
// The "target profile's url" path means re-running `pane agent register
|
|
69
|
+
// --profile dev` against a profile that already exists keeps hitting the
|
|
70
|
+
// same dev relay without retyping --url.
|
|
71
|
+
let activeUrl;
|
|
72
|
+
try {
|
|
73
|
+
const active = resolveProfile(store, profileFlag);
|
|
74
|
+
activeUrl = active?.profile.url;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Selector didn't resolve — fine on register: we're about to create it.
|
|
78
|
+
activeUrl = undefined;
|
|
79
|
+
}
|
|
44
80
|
const url = args.flags.get("url") ??
|
|
45
81
|
process.env.PANE_URL ??
|
|
46
|
-
|
|
82
|
+
activeUrl ??
|
|
47
83
|
DEFAULT_RELAY_URL;
|
|
48
84
|
const name = args.flags.get("name");
|
|
49
85
|
const secret = args.flags.get("secret") ?? process.env.PANE_REGISTER_SECRET ?? undefined;
|
|
@@ -75,13 +111,15 @@ export async function runRegister(args) {
|
|
|
75
111
|
}
|
|
76
112
|
fail(e instanceof Error ? e.message : String(e), "internal");
|
|
77
113
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
114
|
+
// Save under the chosen profile. We pass setCurrent=true: the user just
|
|
115
|
+
// registered against this relay, so the only sensible follow-up is to
|
|
116
|
+
// start using it. The previous behaviour (one global URL+key) is exactly
|
|
117
|
+
// the single-profile case of this.
|
|
118
|
+
const savedTo = upsertProfile(profileName, { url: url.replace(/\/$/, ""), apiKey: result.api_key }, true);
|
|
82
119
|
const out = {
|
|
83
120
|
agent_id: result.agent_id,
|
|
84
121
|
key_prefix: result.key_prefix,
|
|
122
|
+
profile: profileName,
|
|
85
123
|
saved_to: savedTo,
|
|
86
124
|
};
|
|
87
125
|
if (args.bools.has("print-key")) {
|
package/dist/commands/send.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `pane
|
|
1
|
+
// `pane send <id>` — append an agent event to a pane.
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { assertKnownFlags } from "../argv.js";
|
|
@@ -13,23 +13,23 @@ const KNOWN_FLAGS = [
|
|
|
13
13
|
"idempotency-key",
|
|
14
14
|
];
|
|
15
15
|
const KNOWN_BOOLS = [];
|
|
16
|
-
export const sendHelp = `pane
|
|
16
|
+
export const sendHelp = `pane send — emit an agent event into a pane
|
|
17
17
|
|
|
18
18
|
Usage:
|
|
19
|
-
pane
|
|
20
|
-
pane
|
|
19
|
+
pane send <pane-id> --type <event-type> --data <path|json> [options]
|
|
20
|
+
pane send <pane-id> --type <event-type> --attachment <file-path> [options]
|
|
21
21
|
|
|
22
|
-
POSTs an event to /v1/
|
|
22
|
+
POSTs an event to /v1/panes/:id/events. The event is stamped as authored by
|
|
23
23
|
the agent (the relay derives identity from the API key — it cannot be spoofed).
|
|
24
24
|
|
|
25
25
|
Required:
|
|
26
|
-
--type <t> Event type. Must exist in the
|
|
26
|
+
--type <t> Event type. Must exist in the pane's event schema
|
|
27
27
|
with the agent in its emittedBy list.
|
|
28
28
|
--data <v> Event payload: a file path to a .json file, or inline
|
|
29
29
|
JSON. Use --data 'null' or --data '{}' for no payload.
|
|
30
30
|
|
|
31
31
|
ALTERNATIVE to --data:
|
|
32
|
-
--attachment <path> One-shot: upload <path> as a
|
|
32
|
+
--attachment <path> One-shot: upload <path> as a pane-scope attachment, then
|
|
33
33
|
send an event whose payload is the AttachmentRef. The event
|
|
34
34
|
data is { attachment: <AttachmentRef> }; declare it in your event
|
|
35
35
|
schema with \`format: pane-attachment-id\` on \`attachment.attachment_id\`.
|
|
@@ -44,10 +44,10 @@ Options:
|
|
|
44
44
|
Output (stdout, JSON):
|
|
45
45
|
{ event, deduped }`;
|
|
46
46
|
export async function runSend(args) {
|
|
47
|
-
assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane
|
|
48
|
-
const
|
|
49
|
-
if (!
|
|
50
|
-
fail("missing <
|
|
47
|
+
assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane send");
|
|
48
|
+
const paneId = args.positionals[0];
|
|
49
|
+
if (!paneId)
|
|
50
|
+
fail("missing <pane-id>", "invalid_args");
|
|
51
51
|
const type = args.flags.get("type");
|
|
52
52
|
if (!type)
|
|
53
53
|
fail("missing --type", "invalid_args");
|
|
@@ -60,8 +60,8 @@ export async function runSend(args) {
|
|
|
60
60
|
fail("missing --data or --attachment", "invalid_args");
|
|
61
61
|
}
|
|
62
62
|
const client = makeClient(args);
|
|
63
|
-
// --attachment path: upload the file as a
|
|
64
|
-
// event whose data is { attachment: <AttachmentRef> }. The
|
|
63
|
+
// --attachment path: upload the file as a pane-scope attachment, then send an
|
|
64
|
+
// event whose data is { attachment: <AttachmentRef> }. The pane's event schema
|
|
65
65
|
// is expected to declare a attachment field with format: pane-attachment-id.
|
|
66
66
|
if (blobPath !== undefined) {
|
|
67
67
|
let bytes;
|
|
@@ -73,11 +73,11 @@ export async function runSend(args) {
|
|
|
73
73
|
}
|
|
74
74
|
try {
|
|
75
75
|
const ref = await client.uploadBlob(bytes, {
|
|
76
|
-
scope: "
|
|
77
|
-
|
|
76
|
+
scope: "pane",
|
|
77
|
+
paneId: paneId,
|
|
78
78
|
filename: basename(blobPath),
|
|
79
79
|
});
|
|
80
|
-
const res = await client.sendEvent(
|
|
80
|
+
const res = await client.sendEvent(paneId, {
|
|
81
81
|
type: type,
|
|
82
82
|
data: { attachment: ref },
|
|
83
83
|
causationId: args.flags.get("causation-id"),
|
|
@@ -98,7 +98,7 @@ export async function runSend(args) {
|
|
|
98
98
|
fail(e instanceof Error ? e.message : String(e), "invalid_args");
|
|
99
99
|
}
|
|
100
100
|
try {
|
|
101
|
-
const res = await client.sendEvent(
|
|
101
|
+
const res = await client.sendEvent(paneId, {
|
|
102
102
|
type: type,
|
|
103
103
|
data,
|
|
104
104
|
causationId: args.flags.get("causation-id"),
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// `pane agent set-key <key>` — write a fresh API key into the CLI config
|
|
2
|
+
// file. The companion to the human-side rotation flow on /my-agents: after
|
|
3
|
+
// the human regenerates a key in the browser, this command lands it on
|
|
4
|
+
// the agent's machine without making them hand-edit ~/.config/pane/config.json.
|
|
5
|
+
//
|
|
6
|
+
// No relay round-trip: we trust the human-supplied key. The relay will
|
|
7
|
+
// reject it on the next call if it's wrong (401 invalid_api_key) — better
|
|
8
|
+
// than guessing here and adding a network hop for what's a local config
|
|
9
|
+
// write.
|
|
10
|
+
import { assertKnownFlags } from "../argv.js";
|
|
11
|
+
import { isValidProfileName, DEFAULT_PROFILE_NAME, readStore, resolveProfile, upsertProfile, } from "../store.js";
|
|
12
|
+
import { printJson, fail } from "../output.js";
|
|
13
|
+
const KNOWN_FLAGS = ["url"];
|
|
14
|
+
const KNOWN_BOOLS = [];
|
|
15
|
+
export const setKeyHelp = `pane agent set-key <api-key> — save a new API key to the local config
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
pane agent set-key <api-key> [--url <url>] [--profile <name>]
|
|
19
|
+
|
|
20
|
+
After regenerating an agent's API key in the relay's My-agents UI, run
|
|
21
|
+
this on the agent's machine to land the new key in the CLI config file
|
|
22
|
+
(\${XDG_CONFIG_HOME:-~/.config}/pane/config.json, mode 0600). Every later
|
|
23
|
+
command then works with no PANE_API_KEY env var.
|
|
24
|
+
|
|
25
|
+
The key is saved under the ACTIVE profile (unless --profile picks a different
|
|
26
|
+
one). To add a brand-new profile by hand (e.g. for an out-of-band key from a
|
|
27
|
+
closed-registration relay), use 'pane config add'.
|
|
28
|
+
|
|
29
|
+
If you'd rather not touch the config file at all, set the new key as the
|
|
30
|
+
PANE_API_KEY env var on the agent process — both work.
|
|
31
|
+
|
|
32
|
+
Options:
|
|
33
|
+
--url <url> Also update the saved relay URL on the target profile.
|
|
34
|
+
Useful when pointing the agent at a different relay
|
|
35
|
+
alongside the key swap.
|
|
36
|
+
--profile <name> Target this profile instead of the active one. Created
|
|
37
|
+
if it doesn't exist.
|
|
38
|
+
-h, --help Show this help.
|
|
39
|
+
|
|
40
|
+
Output (stdout, JSON):
|
|
41
|
+
{ saved_to, profile, key_prefix }
|
|
42
|
+
|
|
43
|
+
The key is never echoed back. To verify, run \`pane key list\` afterwards.`;
|
|
44
|
+
function keyPrefixOf(key) {
|
|
45
|
+
// Match the relay's keyPrefix() display width for "pane_" + 6 hex chars
|
|
46
|
+
// (11 total). Falls back to the first 8 chars for any unrecognised shape.
|
|
47
|
+
if (key.startsWith("pane_") && key.length >= 11)
|
|
48
|
+
return key.slice(0, 11);
|
|
49
|
+
return key.slice(0, 8);
|
|
50
|
+
}
|
|
51
|
+
export async function runSetKey(args) {
|
|
52
|
+
assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane agent set-key");
|
|
53
|
+
const apiKey = args.positionals[0];
|
|
54
|
+
if (!apiKey) {
|
|
55
|
+
fail("missing api-key — usage: pane agent set-key <api-key>", "invalid_args");
|
|
56
|
+
}
|
|
57
|
+
if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
|
|
58
|
+
fail("api-key must be a non-empty string", "invalid_args");
|
|
59
|
+
}
|
|
60
|
+
// Best-effort shape check. The relay generates `pane_<32 hex>`; we don't
|
|
61
|
+
// reject other shapes outright (a future format change shouldn't strand
|
|
62
|
+
// older CLIs), but we warn on something obviously wrong like leading
|
|
63
|
+
// whitespace.
|
|
64
|
+
const trimmed = apiKey.trim();
|
|
65
|
+
if (trimmed !== apiKey) {
|
|
66
|
+
fail("api-key has surrounding whitespace — copy it without leading/trailing spaces", "invalid_args");
|
|
67
|
+
}
|
|
68
|
+
// Profile selection mirrors `pane agent register`: --profile flag →
|
|
69
|
+
// PANE_PROFILE env → store's current_profile → 'default'.
|
|
70
|
+
const profileFlag = args.flags.get("profile") ?? process.env.PANE_PROFILE;
|
|
71
|
+
const store = readStore();
|
|
72
|
+
const profileName = profileFlag !== undefined && profileFlag !== ""
|
|
73
|
+
? profileFlag
|
|
74
|
+
: (store.currentProfile ?? DEFAULT_PROFILE_NAME);
|
|
75
|
+
if (!isValidProfileName(profileName)) {
|
|
76
|
+
fail(`invalid profile name '${profileName}' — letters, digits, _ and -, up to 32 chars`, "invalid_args");
|
|
77
|
+
}
|
|
78
|
+
const urlFlag = args.flags.get("url");
|
|
79
|
+
const patch = { apiKey };
|
|
80
|
+
if (urlFlag !== undefined)
|
|
81
|
+
patch.url = urlFlag;
|
|
82
|
+
const saved = upsertProfile(profileName, patch);
|
|
83
|
+
// Re-resolve so we report the prefix from the persisted value, not the
|
|
84
|
+
// argument — defensive against future write-side normalisation.
|
|
85
|
+
const after = readStore();
|
|
86
|
+
const reread = resolveProfile(after, profileName);
|
|
87
|
+
printJson({
|
|
88
|
+
saved_to: saved,
|
|
89
|
+
profile: profileName,
|
|
90
|
+
key_prefix: keyPrefixOf(reread?.profile.apiKey ?? apiKey),
|
|
91
|
+
});
|
|
92
|
+
}
|
package/dist/commands/skill.js
CHANGED
|
@@ -78,7 +78,7 @@ async function failOnNon2xx(res, target) {
|
|
|
78
78
|
if (res.ok)
|
|
79
79
|
return;
|
|
80
80
|
// 404 if the operator stripped the route, 5xx on a static-read failure.
|
|
81
|
-
//
|
|
81
|
+
// Pane the body inline — it may carry a useful message.
|
|
82
82
|
const body = await res.text().catch(() => "");
|
|
83
83
|
fail(`relay returned ${res.status} for ${target}${body ? ": " + body.slice(0, 200) : ""}`, "relay_error");
|
|
84
84
|
}
|
package/dist/commands/state.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
// `pane
|
|
1
|
+
// `pane show <id>` — snapshot of a pane, optionally long-polled.
|
|
2
2
|
import { assertKnownFlags } from "../argv.js";
|
|
3
3
|
import { makeClient } from "../config.js";
|
|
4
4
|
import { printJson, fail, failFromError } from "../output.js";
|
|
5
5
|
const KNOWN_FLAGS = ["since", "wait"];
|
|
6
6
|
const KNOWN_BOOLS = [];
|
|
7
|
-
export const stateHelp = `pane
|
|
7
|
+
export const stateHelp = `pane show — show a pane's metadata and event log
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
|
-
pane
|
|
10
|
+
pane show <pane-id> [options]
|
|
11
11
|
|
|
12
|
-
By default non-blocking: fetches
|
|
13
|
-
the event log (GET /v1/
|
|
12
|
+
By default non-blocking: fetches pane metadata (GET /v1/panes/:id) plus
|
|
13
|
+
the event log (GET /v1/panes/:id/events) and prints them together.
|
|
14
14
|
|
|
15
15
|
With --wait, blocks at the relay for up to <secs> if no new events are
|
|
16
16
|
available since the cursor — returns as soon as something lands. Use this
|
|
17
17
|
for headless polling agents that can't keep a WebSocket open (cron,
|
|
18
18
|
FaaS, slow links): poll, then re-poll using next_cursor as --since on the
|
|
19
|
-
next call. Compared to 'pane
|
|
19
|
+
next call. Compared to 'pane watch', it's higher latency per
|
|
20
20
|
round-trip but no long-lived connection.
|
|
21
21
|
|
|
22
22
|
Options:
|
|
@@ -34,10 +34,10 @@ Options:
|
|
|
34
34
|
Output (stdout, JSON):
|
|
35
35
|
{ meta, events, next_cursor }`;
|
|
36
36
|
export async function runState(args) {
|
|
37
|
-
assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane
|
|
38
|
-
const
|
|
39
|
-
if (!
|
|
40
|
-
fail("missing <
|
|
37
|
+
assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane show");
|
|
38
|
+
const paneId = args.positionals[0];
|
|
39
|
+
if (!paneId)
|
|
40
|
+
fail("missing <pane-id>", "invalid_args");
|
|
41
41
|
const since = args.flags.get("since") ?? null;
|
|
42
42
|
// --wait <secs>: hand the server the long-poll window. The relay caps
|
|
43
43
|
// this at 30s; we pass the raw value and let the relay clamp (sending
|
|
@@ -54,8 +54,8 @@ export async function runState(args) {
|
|
|
54
54
|
}
|
|
55
55
|
const client = makeClient(args);
|
|
56
56
|
try {
|
|
57
|
-
const meta = await client.
|
|
58
|
-
const page = await client.getEvents(
|
|
57
|
+
const meta = await client.getPane(paneId);
|
|
58
|
+
const page = await client.getEvents(paneId, {
|
|
59
59
|
since,
|
|
60
60
|
...(waitSeconds !== undefined ? { waitSeconds } : {}),
|
|
61
61
|
});
|
package/dist/commands/taste.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Taste notes are presentation preferences the agent has learned from human
|
|
5
5
|
// feedback ("denser layout", "no rounded corners", "use a dark header") — the
|
|
6
|
-
// kind of guidance that should outlive a single
|
|
6
|
+
// kind of guidance that should outlive a single pane. The intended loop:
|
|
7
7
|
//
|
|
8
8
|
// 1. Before generating a pane template, run `pane taste get` and feed the
|
|
9
9
|
// `taste` field into the prompt so prior preferences shape the output.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// unbounded into noise).
|
|
15
15
|
//
|
|
16
16
|
// Keep taste notes about *presentation/UI taste only* — colours, density,
|
|
17
|
-
// component preferences. Project context, todos, and per-
|
|
17
|
+
// component preferences. Project context, todos, and per-pane state belong
|
|
18
18
|
// somewhere else. Today the attachment is keyed by the agent's API key (per-agent);
|
|
19
19
|
// when pane gains first-class humans, this may move to per-human.
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
@@ -32,7 +32,7 @@ agent has picked up from human feedback ("denser table", "no rounded corners",
|
|
|
32
32
|
"use a dark header"). Read them before generating a pane template so prior
|
|
33
33
|
feedback shapes the output; rewrite them whenever the human gives new
|
|
34
34
|
presentation feedback. Keep entries about UI/presentation taste only — not
|
|
35
|
-
project context, todos, or
|
|
35
|
+
project context, todos, or pane state.
|
|
36
36
|
|
|
37
37
|
Usage:
|
|
38
38
|
pane taste <subcommand> [options]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// `pane template-records` — CRUD for template-level record collections.
|
|
2
|
+
//
|
|
3
|
+
// Owner-curated content scoped to a Template head, visible to every pane
|
|
4
|
+
// derived from any version of the template. Mirrors `pane records` (per-pane
|
|
5
|
+
// records) verb-for-verb; the only difference is the resource path goes
|
|
6
|
+
// `/templates/:id/...` instead of `/panes/:id/...`.
|
|
7
|
+
import { assertKnownFlags } from "../argv.js";
|
|
8
|
+
import { makeClient } from "../config.js";
|
|
9
|
+
import { fail, failFromError, printJson } from "../output.js";
|
|
10
|
+
import { resolveJson } from "../input.js";
|
|
11
|
+
export const templateRecordsHelp = `pane template-records — CRUD for template-level record collections
|
|
12
|
+
|
|
13
|
+
Template records are owner-curated shared content anchored to a Template head.
|
|
14
|
+
Every pane derived from any version of the template sees the same rows.
|
|
15
|
+
Declared via the template version's \`template_record_schema\`; writes are
|
|
16
|
+
owner-only (the template's agent + same-human-claimed agents). Page-side
|
|
17
|
+
reads use the in-iframe \`pane.template.records.*\` bridge (no HTTP).
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
pane template-records <verb> [options]
|
|
21
|
+
|
|
22
|
+
Verbs:
|
|
23
|
+
list <template-id|slug> <collection>
|
|
24
|
+
[--since <seq>] [--limit <n>] [--include-tombstones]
|
|
25
|
+
get <template-id|slug> <collection> <record-key>
|
|
26
|
+
upsert <template-id|slug> <collection>
|
|
27
|
+
--data <path|json> [--key <record-key>]
|
|
28
|
+
update <template-id|slug> <collection> <record-key>
|
|
29
|
+
--data <path|json> [--if-match <version>]
|
|
30
|
+
delete <template-id|slug> <collection> <record-key>
|
|
31
|
+
[--if-match <version>] [--yes]
|
|
32
|
+
|
|
33
|
+
Output (stdout): single JSON object per command.
|
|
34
|
+
Errors on stderr: {"error":{"code","message"}} with non-zero exit.`;
|
|
35
|
+
export async function runTemplateRecords(args) {
|
|
36
|
+
const verb = args.positionals[0];
|
|
37
|
+
if ((verb === undefined || verb === "help") && args.bools.has("help")) {
|
|
38
|
+
process.stdout.write(templateRecordsHelp + "\n");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (verb === undefined) {
|
|
42
|
+
fail("missing verb — pane template-records <list|get|upsert|update|delete>", "invalid_args");
|
|
43
|
+
}
|
|
44
|
+
const sub = {
|
|
45
|
+
positionals: args.positionals.slice(1),
|
|
46
|
+
flags: args.flags,
|
|
47
|
+
bools: args.bools,
|
|
48
|
+
...(args.danglingValueFlags !== undefined
|
|
49
|
+
? { danglingValueFlags: args.danglingValueFlags }
|
|
50
|
+
: {}),
|
|
51
|
+
};
|
|
52
|
+
switch (verb) {
|
|
53
|
+
case "list":
|
|
54
|
+
return runList(sub);
|
|
55
|
+
case "get":
|
|
56
|
+
return runGet(sub);
|
|
57
|
+
case "upsert":
|
|
58
|
+
return runUpsert(sub);
|
|
59
|
+
case "update":
|
|
60
|
+
return runUpdate(sub);
|
|
61
|
+
case "delete":
|
|
62
|
+
return runDelete(sub);
|
|
63
|
+
default:
|
|
64
|
+
fail(`unknown verb '${verb}' — pane template-records <list|get|upsert|update|delete>`, "invalid_args");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function runList(args) {
|
|
68
|
+
assertKnownFlags(args, ["since", "limit", "url", "api-key"], ["include-tombstones", "help"], "pane template-records list");
|
|
69
|
+
const templateId = args.positionals[0];
|
|
70
|
+
const collection = args.positionals[1];
|
|
71
|
+
if (!templateId || !collection) {
|
|
72
|
+
fail("usage: pane template-records list <template-id|slug> <collection>", "invalid_args");
|
|
73
|
+
}
|
|
74
|
+
const since = parseIntFlag(args, "since", 0);
|
|
75
|
+
const limit = parseIntFlag(args, "limit", undefined, { min: 1, max: 200 });
|
|
76
|
+
const includeTombstones = args.bools.has("include-tombstones");
|
|
77
|
+
const client = makeClient(args);
|
|
78
|
+
try {
|
|
79
|
+
const page = await client.listTemplateRecords(templateId, collection, {
|
|
80
|
+
since,
|
|
81
|
+
...(limit !== undefined ? { limit } : {}),
|
|
82
|
+
});
|
|
83
|
+
const records = includeTombstones
|
|
84
|
+
? page.records
|
|
85
|
+
: page.records.filter((r) => r.deleted_at === null);
|
|
86
|
+
printJson({
|
|
87
|
+
records,
|
|
88
|
+
next_since: page.next_since,
|
|
89
|
+
has_more: page.has_more,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
failFromError(e);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function runGet(args) {
|
|
97
|
+
assertKnownFlags(args, ["url", "api-key"], ["help"], "pane template-records get");
|
|
98
|
+
const [templateId, collection, recordKey] = args.positionals;
|
|
99
|
+
if (!templateId || !collection || !recordKey) {
|
|
100
|
+
fail("usage: pane template-records get <template-id|slug> <collection> <record-key>", "invalid_args");
|
|
101
|
+
}
|
|
102
|
+
const client = makeClient(args);
|
|
103
|
+
try {
|
|
104
|
+
const row = await client.getTemplateRecord(templateId, collection, recordKey);
|
|
105
|
+
if (!row) {
|
|
106
|
+
fail(`no template record at key '${recordKey}' in collection '${collection}'`, "template_record_not_found");
|
|
107
|
+
}
|
|
108
|
+
printJson({ record: row });
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
failFromError(e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function runUpsert(args) {
|
|
115
|
+
assertKnownFlags(args, ["data", "key", "url", "api-key"], ["help"], "pane template-records upsert");
|
|
116
|
+
const [templateId, collection] = args.positionals;
|
|
117
|
+
if (!templateId || !collection) {
|
|
118
|
+
fail("usage: pane template-records upsert <template-id|slug> <collection> --data <path|json>", "invalid_args");
|
|
119
|
+
}
|
|
120
|
+
const dataRaw = args.flags.get("data");
|
|
121
|
+
if (dataRaw === undefined) {
|
|
122
|
+
fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
|
|
123
|
+
}
|
|
124
|
+
const data = resolveJson(dataRaw, "--data");
|
|
125
|
+
const key = args.flags.get("key");
|
|
126
|
+
const client = makeClient(args);
|
|
127
|
+
try {
|
|
128
|
+
const body = { data };
|
|
129
|
+
if (key !== undefined)
|
|
130
|
+
body.record_key = key;
|
|
131
|
+
const out = await client.upsertTemplateRecord(templateId, collection, body);
|
|
132
|
+
printJson(out);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
failFromError(e);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function runUpdate(args) {
|
|
139
|
+
assertKnownFlags(args, ["data", "if-match", "url", "api-key"], ["help"], "pane template-records update");
|
|
140
|
+
const [templateId, collection, recordKey] = args.positionals;
|
|
141
|
+
if (!templateId || !collection || !recordKey) {
|
|
142
|
+
fail("usage: pane template-records update <template-id|slug> <collection> <record-key> --data <path|json>", "invalid_args");
|
|
143
|
+
}
|
|
144
|
+
const dataRaw = args.flags.get("data");
|
|
145
|
+
if (dataRaw === undefined) {
|
|
146
|
+
fail("--data is required (path to JSON file, or inline JSON)", "invalid_args");
|
|
147
|
+
}
|
|
148
|
+
const data = resolveJson(dataRaw, "--data");
|
|
149
|
+
const ifMatch = parseIntFlag(args, "if-match", undefined, { min: 0 });
|
|
150
|
+
const client = makeClient(args);
|
|
151
|
+
try {
|
|
152
|
+
const body = { data };
|
|
153
|
+
if (ifMatch !== undefined)
|
|
154
|
+
body.if_match = ifMatch;
|
|
155
|
+
const out = await client.updateTemplateRecord(templateId, collection, recordKey, body);
|
|
156
|
+
printJson(out);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
failFromError(e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function runDelete(args) {
|
|
163
|
+
assertKnownFlags(args, ["if-match", "url", "api-key"], ["yes", "help"], "pane template-records delete");
|
|
164
|
+
const [templateId, collection, recordKey] = args.positionals;
|
|
165
|
+
if (!templateId || !collection || !recordKey) {
|
|
166
|
+
fail("usage: pane template-records delete <template-id|slug> <collection> <record-key>", "invalid_args");
|
|
167
|
+
}
|
|
168
|
+
const ifMatch = parseIntFlag(args, "if-match", undefined, { min: 0 });
|
|
169
|
+
const client = makeClient(args);
|
|
170
|
+
try {
|
|
171
|
+
await client.deleteTemplateRecord(templateId, collection, recordKey, {
|
|
172
|
+
...(ifMatch !== undefined ? { ifMatch } : {}),
|
|
173
|
+
});
|
|
174
|
+
printJson({ deleted: true, key: recordKey });
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
failFromError(e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function parseIntFlag(args, name, defaultValue, bounds = {}) {
|
|
181
|
+
const raw = args.flags.get(name);
|
|
182
|
+
if (raw === undefined)
|
|
183
|
+
return defaultValue;
|
|
184
|
+
const n = Number(raw);
|
|
185
|
+
if (!Number.isInteger(n)) {
|
|
186
|
+
fail(`--${name} must be an integer`, "invalid_args");
|
|
187
|
+
}
|
|
188
|
+
if (bounds.min !== undefined && n < bounds.min) {
|
|
189
|
+
fail(`--${name} must be >= ${bounds.min}`, "invalid_args");
|
|
190
|
+
}
|
|
191
|
+
if (bounds.max !== undefined && n > bounds.max) {
|
|
192
|
+
fail(`--${name} must be <= ${bounds.max}`, "invalid_args");
|
|
193
|
+
}
|
|
194
|
+
return n;
|
|
195
|
+
}
|