@paneui/cli 0.0.8 → 0.0.10
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
package/dist/store.js
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
// Persisted CLI config: ${XDG_CONFIG_HOME or ~/.config}/pane/config.json.
|
|
2
2
|
//
|
|
3
|
-
// Holds
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// Holds one or more named profiles. Each profile is one agent identity on
|
|
4
|
+
// one relay — (url, api_key). Switching profiles is the multi-environment
|
|
5
|
+
// story: dev / staging / prod, or personal / work agents on the same relay,
|
|
6
|
+
// without re-running `pane agent register` between them.
|
|
7
|
+
//
|
|
8
|
+
// On-disk shape:
|
|
9
|
+
//
|
|
10
|
+
// {
|
|
11
|
+
// "current_profile": "prod",
|
|
12
|
+
// "profiles": {
|
|
13
|
+
// "prod": { "url": "https://…", "api_key": "pane_…" },
|
|
14
|
+
// "dev": { "url": "http://localhost:3000", "api_key": "pane_…" }
|
|
15
|
+
// }
|
|
16
|
+
// }
|
|
17
|
+
//
|
|
18
|
+
// Tiny and synchronous; no deps. Holds secrets — files written mode 0600.
|
|
6
19
|
import { readFileSync, writeFileSync, mkdirSync, chmodSync, rmSync, } from "node:fs";
|
|
7
20
|
import { homedir } from "node:os";
|
|
8
21
|
import { join, dirname } from "node:path";
|
|
22
|
+
/**
|
|
23
|
+
* Default profile name when the user runs `pane agent register` without
|
|
24
|
+
* `--profile` on a fresh install. Stable, predictable, and short enough to
|
|
25
|
+
* type in `pane --profile default …` if needed.
|
|
26
|
+
*/
|
|
27
|
+
export const DEFAULT_PROFILE_NAME = "default";
|
|
28
|
+
/** Profile-name validation (a-z, A-Z, 0-9, _ and -, 1..32 chars). */
|
|
29
|
+
const PROFILE_NAME_RX = /^[A-Za-z0-9_-]{1,32}$/;
|
|
30
|
+
export function isValidProfileName(name) {
|
|
31
|
+
return PROFILE_NAME_RX.test(name);
|
|
32
|
+
}
|
|
9
33
|
/** Absolute path to the config file (honours XDG_CONFIG_HOME). */
|
|
10
34
|
export function storePath() {
|
|
11
35
|
const base = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim() !== ""
|
|
@@ -13,52 +37,169 @@ export function storePath() {
|
|
|
13
37
|
: join(homedir(), ".config");
|
|
14
38
|
return join(base, "pane", "config.json");
|
|
15
39
|
}
|
|
16
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Read the persisted config. Returns an empty store if the file is missing,
|
|
42
|
+
* unparseable, or doesn't carry a `profiles` object.
|
|
43
|
+
*/
|
|
17
44
|
export function readStore() {
|
|
18
45
|
let text;
|
|
19
46
|
try {
|
|
20
47
|
text = readFileSync(storePath(), "utf8");
|
|
21
48
|
}
|
|
22
49
|
catch {
|
|
23
|
-
return {};
|
|
50
|
+
return { profiles: {} };
|
|
24
51
|
}
|
|
52
|
+
let parsed;
|
|
25
53
|
try {
|
|
26
|
-
|
|
27
|
-
if (parsed === null ||
|
|
28
|
-
typeof parsed !== "object" ||
|
|
29
|
-
Array.isArray(parsed)) {
|
|
30
|
-
return {};
|
|
31
|
-
}
|
|
32
|
-
const out = {};
|
|
33
|
-
if (typeof parsed.url === "string")
|
|
34
|
-
out.url = parsed.url;
|
|
35
|
-
if (typeof parsed.apiKey === "string")
|
|
36
|
-
out.apiKey = parsed.apiKey;
|
|
37
|
-
return out;
|
|
54
|
+
parsed = JSON.parse(text);
|
|
38
55
|
}
|
|
39
56
|
catch {
|
|
40
|
-
return {};
|
|
57
|
+
return { profiles: {} };
|
|
41
58
|
}
|
|
59
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
60
|
+
return { profiles: {} };
|
|
61
|
+
}
|
|
62
|
+
const obj = parsed;
|
|
63
|
+
if (!obj["profiles"] || typeof obj["profiles"] !== "object") {
|
|
64
|
+
return { profiles: {} };
|
|
65
|
+
}
|
|
66
|
+
const rawProfiles = obj["profiles"];
|
|
67
|
+
const profiles = {};
|
|
68
|
+
for (const [name, raw] of Object.entries(rawProfiles)) {
|
|
69
|
+
if (raw === null || typeof raw !== "object")
|
|
70
|
+
continue;
|
|
71
|
+
const p = raw;
|
|
72
|
+
const profile = {};
|
|
73
|
+
if (typeof p["url"] === "string")
|
|
74
|
+
profile.url = p["url"];
|
|
75
|
+
if (typeof p["api_key"] === "string")
|
|
76
|
+
profile.apiKey = p["api_key"];
|
|
77
|
+
profiles[name] = profile;
|
|
78
|
+
}
|
|
79
|
+
const currentProfile = typeof obj["current_profile"] === "string"
|
|
80
|
+
? obj["current_profile"]
|
|
81
|
+
: undefined;
|
|
82
|
+
// If the named current profile was deleted out-of-band, drop it back to
|
|
83
|
+
// undefined so the resolver can fall through to env / default URL.
|
|
84
|
+
return {
|
|
85
|
+
currentProfile: currentProfile && profiles[currentProfile] !== undefined
|
|
86
|
+
? currentProfile
|
|
87
|
+
: undefined,
|
|
88
|
+
profiles,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Serialise a Store to the on-disk JSON shape (snake_case fields). */
|
|
92
|
+
function serialize(store) {
|
|
93
|
+
const profilesOut = {};
|
|
94
|
+
for (const [name, p] of Object.entries(store.profiles)) {
|
|
95
|
+
const o = {};
|
|
96
|
+
if (p.url !== undefined)
|
|
97
|
+
o["url"] = p.url;
|
|
98
|
+
if (p.apiKey !== undefined)
|
|
99
|
+
o["api_key"] = p.apiKey;
|
|
100
|
+
profilesOut[name] = o;
|
|
101
|
+
}
|
|
102
|
+
const body = { profiles: profilesOut };
|
|
103
|
+
if (store.currentProfile !== undefined) {
|
|
104
|
+
body["current_profile"] = store.currentProfile;
|
|
105
|
+
}
|
|
106
|
+
return JSON.stringify(body, null, 2) + "\n";
|
|
42
107
|
}
|
|
43
108
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
109
|
+
* Atomically write the whole Store to disk. The file is created with mode
|
|
110
|
+
* 0600 and the parent directory is created as needed.
|
|
46
111
|
*/
|
|
47
|
-
export function
|
|
112
|
+
export function writeStoreFull(store) {
|
|
48
113
|
const path = storePath();
|
|
49
|
-
const merged = { ...readStore(), ...patch };
|
|
50
114
|
mkdirSync(dirname(path), { recursive: true });
|
|
51
|
-
writeFileSync(path,
|
|
52
|
-
// Ensure mode even
|
|
115
|
+
writeFileSync(path, serialize(store), { mode: 0o600 });
|
|
116
|
+
// Ensure mode even when the file pre-existed with looser permissions.
|
|
53
117
|
chmodSync(path, 0o600);
|
|
54
118
|
return path;
|
|
55
119
|
}
|
|
56
120
|
/**
|
|
57
|
-
*
|
|
58
|
-
* the
|
|
121
|
+
* Upsert a single profile and write back. If `setCurrent` is true, the
|
|
122
|
+
* profile becomes the active one. If the store had no current profile yet
|
|
123
|
+
* (empty store), the newly-written profile becomes current regardless —
|
|
124
|
+
* there's no other choice that makes sense.
|
|
125
|
+
*/
|
|
126
|
+
export function upsertProfile(name, patch, setCurrent = false) {
|
|
127
|
+
if (!isValidProfileName(name)) {
|
|
128
|
+
throw new Error(`invalid profile name '${name}' — must match ${PROFILE_NAME_RX} (letters, digits, underscore, dash; 1..32 chars)`);
|
|
129
|
+
}
|
|
130
|
+
const store = readStore();
|
|
131
|
+
const merged = { ...(store.profiles[name] ?? {}), ...patch };
|
|
132
|
+
store.profiles[name] = merged;
|
|
133
|
+
if (setCurrent || store.currentProfile === undefined) {
|
|
134
|
+
store.currentProfile = name;
|
|
135
|
+
}
|
|
136
|
+
return writeStoreFull(store);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Set the active profile by name. Throws if `name` is not in the store.
|
|
140
|
+
* Use `upsertProfile` if you also want to create it.
|
|
141
|
+
*/
|
|
142
|
+
export function setCurrentProfile(name) {
|
|
143
|
+
const store = readStore();
|
|
144
|
+
if (store.profiles[name] === undefined) {
|
|
145
|
+
throw new Error(`profile '${name}' does not exist — run 'pane config list' to see available profiles`);
|
|
146
|
+
}
|
|
147
|
+
store.currentProfile = name;
|
|
148
|
+
return writeStoreFull(store);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Remove a profile. If it was current, drop `current_profile` (the resolver
|
|
152
|
+
* falls through to env / default URL). If the resulting store is empty,
|
|
153
|
+
* delete the file entirely so a `readStore` looks identical to "fresh".
|
|
154
|
+
* Returns `{ path, was_current }`. Throws if the profile doesn't exist.
|
|
155
|
+
*/
|
|
156
|
+
export function removeProfile(name) {
|
|
157
|
+
const store = readStore();
|
|
158
|
+
if (store.profiles[name] === undefined) {
|
|
159
|
+
throw new Error(`profile '${name}' does not exist`);
|
|
160
|
+
}
|
|
161
|
+
const wasCurrent = store.currentProfile === name;
|
|
162
|
+
delete store.profiles[name];
|
|
163
|
+
if (wasCurrent) {
|
|
164
|
+
store.currentProfile = undefined;
|
|
165
|
+
}
|
|
166
|
+
if (Object.keys(store.profiles).length === 0) {
|
|
167
|
+
// Empty store → delete the file so a subsequent register starts fresh.
|
|
168
|
+
return { path: clearStore(), was_current: wasCurrent };
|
|
169
|
+
}
|
|
170
|
+
return { path: writeStoreFull(store), was_current: wasCurrent };
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Delete the persisted config file entirely. Idempotent — no error if the
|
|
174
|
+
* file never existed. Returns the path it targeted. Used by
|
|
175
|
+
* `pane agent logout --all` and `removeProfile` when it drains the last
|
|
176
|
+
* profile.
|
|
59
177
|
*/
|
|
60
178
|
export function clearStore() {
|
|
61
179
|
const path = storePath();
|
|
62
180
|
rmSync(path, { force: true });
|
|
63
181
|
return path;
|
|
64
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Resolve which profile to load from the store, given the optional selector
|
|
185
|
+
* (`--profile` flag or `PANE_PROFILE` env). Returns `null` if no profile
|
|
186
|
+
* matches — i.e. the caller should fall through to env / default-URL
|
|
187
|
+
* resolution. Throws if `selector` was explicit (truthy) and not found, so
|
|
188
|
+
* a typo in `--profile dev` doesn't silently fall back to the wrong relay.
|
|
189
|
+
*/
|
|
190
|
+
export function resolveProfile(store, selector) {
|
|
191
|
+
if (selector !== undefined && selector !== "") {
|
|
192
|
+
const p = store.profiles[selector];
|
|
193
|
+
if (p === undefined) {
|
|
194
|
+
const known = Object.keys(store.profiles).sort().join(", ") || "(none)";
|
|
195
|
+
throw new Error(`profile '${selector}' does not exist (known: ${known}) — run 'pane config list'`);
|
|
196
|
+
}
|
|
197
|
+
return { name: selector, profile: p };
|
|
198
|
+
}
|
|
199
|
+
if (store.currentProfile !== undefined) {
|
|
200
|
+
const p = store.profiles[store.currentProfile];
|
|
201
|
+
if (p !== undefined)
|
|
202
|
+
return { name: store.currentProfile, profile: p };
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
package/dist/upgrade.js
CHANGED
|
@@ -40,7 +40,7 @@ export function detectInstallMethod(entryPath) {
|
|
|
40
40
|
}
|
|
41
41
|
// npx caches the package under ~/Library/Caches/_npx (macOS) or
|
|
42
42
|
// ~/.npm/_npx (Linux) and runs it from a node_modules inside that dir.
|
|
43
|
-
//
|
|
43
|
+
// Pane this distinctly from a real vendored install: with an npx
|
|
44
44
|
// execution there is no project package.json owning the version and no
|
|
45
45
|
// global to upgrade — the user runs `npx @paneui/cli@<version>` each
|
|
46
46
|
// time, so the right answer is "ask the human / re-run with a newer
|
package/dist/version.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// Single source of truth for the CLI version string.
|
|
2
2
|
//
|
|
3
3
|
// - `pane --version` prints this verbatim.
|
|
4
|
-
// - Every PaneClient construction passes it as `cliVersion`, which
|
|
4
|
+
// - Every PaneClient construction passes it as `cliVersion`, which panes
|
|
5
5
|
// as the `x-pane-cli-version` header on every relay request — drives the
|
|
6
6
|
// relay's version-skew check (HTTP 426 `cli_upgrade_required`).
|
|
7
7
|
//
|
|
8
8
|
// Keep this in lockstep with packages/cli/package.json's `version` field;
|
|
9
9
|
// they're consulted in different places (here for the runtime header,
|
|
10
10
|
// package.json for npm publish + dependency resolution).
|
|
11
|
-
export const VERSION = "0.0.
|
|
11
|
+
export const VERSION = "0.0.10";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paneui/cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Command-line client for the Pane relay: create
|
|
3
|
+
"version": "0.0.10",
|
|
4
|
+
"description": "Command-line client for the Pane relay: create panes, inspect state, send and watch events.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"keywords": [
|
|
@@ -41,10 +41,12 @@
|
|
|
41
41
|
"test:unit": "vitest run"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@paneui/core": "^0.0.
|
|
44
|
+
"@paneui/core": "^0.0.10",
|
|
45
|
+
"qrcode-terminal": "^0.12.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/node": "^25.9.1",
|
|
49
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
48
50
|
"typescript": "^6.0.3",
|
|
49
51
|
"vitest": "^4.1.6"
|
|
50
52
|
}
|
package/dist/commands/surface.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
// `pane surface` — the central noun of pane: open, observe, send to, and
|
|
2
|
-
// close a surface.
|
|
3
|
-
//
|
|
4
|
-
// A surface is one *use* of an template: an open URL the human(s) interact
|
|
5
|
-
// with, plus an event log the agent reads and appends to. Every other noun
|
|
6
|
-
// (template, attachment, key, taste, feedback) exists in service of surfaces.
|
|
7
|
-
//
|
|
8
|
-
// This file is a thin dispatcher — each verb's actual logic lives in its own
|
|
9
|
-
// file (create.ts, state.ts, send.ts, watch.ts, delete.ts). The verb runners
|
|
10
|
-
// expect the surface id at positionals[0]; we slice off our own verb before
|
|
11
|
-
// delegating so they don't need to know they're being called via `surface`.
|
|
12
|
-
import { runCreate } from "./create.js";
|
|
13
|
-
import { runState } from "./state.js";
|
|
14
|
-
import { runSend } from "./send.js";
|
|
15
|
-
import { runWatch } from "./watch.js";
|
|
16
|
-
import { runDelete } from "./delete.js";
|
|
17
|
-
import { runList, listHelp } from "./list.js";
|
|
18
|
-
import { runParticipant, participantHelp } from "./participant.js";
|
|
19
|
-
import { fail } from "../output.js";
|
|
20
|
-
export const sessionHelp = `pane surface — open, observe, send to, and close surfaces
|
|
21
|
-
|
|
22
|
-
A surface is one use of an template: an open URL the human(s) interact with,
|
|
23
|
-
plus an event log the agent reads and appends to.
|
|
24
|
-
|
|
25
|
-
Usage:
|
|
26
|
-
pane surface <verb> [options]
|
|
27
|
-
|
|
28
|
-
Verbs:
|
|
29
|
-
create Create a surface (POST /v1/surfaces). Prints surface_id,
|
|
30
|
-
urls, tokens, expires_at.
|
|
31
|
-
list Enumerate YOUR agent's surfaces. The recovery primitive
|
|
32
|
-
for "I dropped the create response" — surfaces are
|
|
33
|
-
listable, but participant tokens are stored hashed and
|
|
34
|
-
CANNOT be recovered. Use 'participant new' to mint a
|
|
35
|
-
fresh URL.
|
|
36
|
-
show <id> Non-blocking snapshot: surface metadata + event log.
|
|
37
|
-
Supports --wait <secs> for relay-side long-polling.
|
|
38
|
-
send <id> Emit an agent event into a surface.
|
|
39
|
-
watch <id> Stream a surface's events as JSON-lines on stdout
|
|
40
|
-
(long-lived; the building block for pipe-readers).
|
|
41
|
-
delete <id> Close/delete a surface (DELETE /v1/surfaces/:id).
|
|
42
|
-
participant List / mint / revoke participant URLs on an existing
|
|
43
|
-
<list|new|revoke> surface. 'list' returns the participant ids you need
|
|
44
|
-
for 'revoke'; 'new' replaces the destructive 'delete
|
|
45
|
-
+ recreate' workaround for a lost URL; 'revoke'
|
|
46
|
-
invalidates one URL without touching the surface.
|
|
47
|
-
|
|
48
|
-
Run \`pane surface <verb> --help\` for verb-specific options.`;
|
|
49
|
-
/**
|
|
50
|
-
* Build a new ParsedArgs with the leading positional (the verb) stripped.
|
|
51
|
-
* The downstream verb runners (runState / runSend / runWatch / runDelete)
|
|
52
|
-
* read the surface id at positionals[0], so we hand them an args object that
|
|
53
|
-
* looks exactly like the pre-restructure invocation.
|
|
54
|
-
*/
|
|
55
|
-
function shiftPositionals(args) {
|
|
56
|
-
// Propagate danglingValueFlags too — otherwise the leaf runner's
|
|
57
|
-
// assertKnownFlags can't tell that the user wrote `--title` without a
|
|
58
|
-
// value, and falls through to a less-useful downstream error.
|
|
59
|
-
const out = {
|
|
60
|
-
positionals: args.positionals.slice(1),
|
|
61
|
-
flags: args.flags,
|
|
62
|
-
bools: args.bools,
|
|
63
|
-
};
|
|
64
|
-
if (args.danglingValueFlags !== undefined) {
|
|
65
|
-
out.danglingValueFlags = args.danglingValueFlags;
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
export async function runSession(args) {
|
|
70
|
-
const verb = args.positionals[0];
|
|
71
|
-
// `pane surface participant --help` (verb-level help on the participant
|
|
72
|
-
// sub-noun, with no further sub-verb). The general --help pre-empt in
|
|
73
|
-
// index.ts only fires when no positional follows the noun; here a
|
|
74
|
-
// positional ("participant") is present, so the sub-noun must own its own
|
|
75
|
-
// --help routing.
|
|
76
|
-
if (verb === "participant" &&
|
|
77
|
-
args.bools.has("help") &&
|
|
78
|
-
args.positionals.length === 1) {
|
|
79
|
-
process.stdout.write(participantHelp + "\n");
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
// `pane surface list --help` — same pattern.
|
|
83
|
-
if (verb === "list" &&
|
|
84
|
-
args.bools.has("help") &&
|
|
85
|
-
args.positionals.length === 1) {
|
|
86
|
-
process.stdout.write(listHelp + "\n");
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const inner = shiftPositionals(args);
|
|
90
|
-
switch (verb) {
|
|
91
|
-
case "create":
|
|
92
|
-
await runCreate(inner);
|
|
93
|
-
break;
|
|
94
|
-
case "list":
|
|
95
|
-
await runList(inner);
|
|
96
|
-
break;
|
|
97
|
-
case "show":
|
|
98
|
-
await runState(inner);
|
|
99
|
-
break;
|
|
100
|
-
case "send":
|
|
101
|
-
await runSend(inner);
|
|
102
|
-
break;
|
|
103
|
-
case "watch":
|
|
104
|
-
await runWatch(inner);
|
|
105
|
-
break;
|
|
106
|
-
case "delete":
|
|
107
|
-
await runDelete(inner);
|
|
108
|
-
break;
|
|
109
|
-
case "participant":
|
|
110
|
-
await runParticipant(inner);
|
|
111
|
-
break;
|
|
112
|
-
case undefined:
|
|
113
|
-
fail("missing verb — usage: pane surface <create|list|show|send|watch|delete|participant> (run 'pane surface --help')", "invalid_args");
|
|
114
|
-
break;
|
|
115
|
-
default:
|
|
116
|
-
fail(`unknown surface verb '${verb}' — expected create|list|show|send|watch|delete|participant (run 'pane surface --help')`, "invalid_args");
|
|
117
|
-
}
|
|
118
|
-
}
|