@pipod/cli 0.0.0-stage → 0.1.3

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 CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # pi pod
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Run [pi](https://github.com/earendil-works/pi) coding-agent sessions in server-managed pods.
4
+
5
+ ```bash
6
+ npm install -g @pipod/cli # or from a checkout of this directory: npm ci && npm run build && npm install -g .
7
+ pipod login --server <url> # sign in to a pi pod server (the hosted one without --server)
8
+ pipod # launch a pod for the current directory and attach
9
+ pipod --help # every command and launch option
10
+ pipod update # upgrade the way it was installed
11
+ ```
12
+
13
+ Running your own server: [docs/self-host.md](https://github.com/pi-pod/pipod/blob/main/docs/self-host.md).
14
+ A self-hosted server is deployed from a checkout; install the CLI from the same checkout so the
15
+ two match.
16
+
17
+ ## Secrets
18
+
19
+ ```bash
20
+ pipod secrets set org SERVICE_TOKEN # hidden interactive prompt
21
+ pipod secrets set user SERVICE_TOKEN
22
+ pipod secrets set template/my-template SERVICE_TOKEN
23
+ pipod secrets set org SERVICE_TOKEN < /path/to/protected-value-file
24
+ ```
25
+
26
+ Values come from the hidden prompt or stdin, never `NAME=value` arguments (which
27
+ can leak through shell history and process listings). Protect input files with
28
+ mode `0600`. The server accepts names up to 128 characters and values up to
29
+ 64 KiB; list responses contain metadata only. Provider credentials belong at
30
+ org scope and are excluded from pod environments.
31
+
32
+ Authorized pod code can read inherited org/user/template values. Config bundles,
33
+ scripts and Pi files are readable configuration, not secret storage; use supported
34
+ environment references instead of literal credentials.
35
+
36
+ ## Workspace seeding
37
+
38
+ `pipod` launched from a directory without `.pi-pod/config.json` seeds the new pod from that
39
+ directory (its git repository root when inside one):
40
+
41
+ - a clean, pushed checkout whose remote tip matches `HEAD` is **cloned at that exact commit**;
42
+ - anything else — dirty trees, unpushed commits, private remotes without usable credentials,
43
+ plain directories — is **streamed in as a tar archive** (git-ignored paths, `node_modules`
44
+ and `.pi-pod/env` excluded, `.git` included when it fits).
45
+
46
+ Credentials for a private remote are forwarded only after a prompt (`--yes` approves it) and
47
+ are never stored. `--dry-run` prints the decision without prompting or creating a pod;
48
+ `--no-seed` starts the pod empty. Forks, reused pods and `--on` co-location never seed.
49
+ Configured projects (with `.pi-pod/config.json`) keep their existing behavior.
50
+
51
+ Details, limits and a manual test checklist: [docs/workspace-seeding.md](docs/workspace-seeding.md).
52
+
53
+ Local images named in a prompt (clipboard pastes, `@shot.png`, file paths) are
54
+ attached inline for the pod agent: [docs/image-attachments.md](docs/image-attachments.md).
55
+
56
+ ## Personal workstations (SaaS)
57
+
58
+ On the hosted service each user gets a whole machine of their own, which sleeps when idle. The
59
+ first command after it sleeps waits for it to come back — measured at 243 to 708 seconds in
60
+ production, so the CLI shows elapsed progress and says "several minutes" rather than promising
61
+ a number. Nothing is lost while it sleeps: every workspace stays on the workstation's disk.
62
+
63
+ The hosted service also reports the account's plan, active hours and spend cap, which `pipod
64
+ list` prints under the pods. A self-hosted server reports none of that, and the client then
65
+ shows none of it. The wire contract for both:
66
+ [docs/personal-workstation.md](docs/personal-workstation.md).
67
+
68
+ ## Extension aux bridge
69
+
70
+ Server-attested pod extensions also render in the launcher (rendering hooks) while the
71
+ pod stays authoritative: the pod owns every slash command by default (`/pod local <name>`
72
+ runs the local copy explicitly), and `ctx.modelRegistry.complete()` executes pod-side with
73
+ real auth as a turn-external call. Text-only contract, bounds, cancellation and the
74
+ pi-lightweight-llm `/transcript` setup requirement: [docs/extension-aux-bridge.md](docs/extension-aux-bridge.md).
@@ -0,0 +1,9 @@
1
+ import {
2
+ CHORD_WINDOW_MS,
3
+ wirePodChords
4
+ } from "./chunk-GNKF75X7.js";
5
+ export {
6
+ CHORD_WINDOW_MS,
7
+ wirePodChords
8
+ };
9
+ //# sourceMappingURL=chords-SOJBLC6R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,204 @@
1
+ // src/client/runtime/chords.ts
2
+ import { isKeyRelease, isKeyRepeat, matchesKey } from "@earendil-works/pi-tui";
3
+
4
+ // src/client/runtime/pod-commands.ts
5
+ var LEAVE_ACTIONS = {
6
+ detach: "keep",
7
+ archive: "archive"
8
+ };
9
+ var POD_SUBCOMMANDS = [
10
+ { name: "local", description: "run a locally rendered extension command here (the pod owns /name by default)" },
11
+ { name: "detach", description: "leave; the pod keeps running and the idle timer takes over" },
12
+ { name: "archive", description: "leave, stop the pod and hide it from the list" },
13
+ { name: "status", description: "pod id, provider, state, repo@branch, egress and idle policy" },
14
+ { name: "list", description: "pick from this install's pods" },
15
+ { name: "switch", description: "move to another pod's pi session" },
16
+ { name: "sync", description: "refresh pod-derived settings and themes" }
17
+ ];
18
+ function availableSubcommands(host) {
19
+ const hooks = {
20
+ local: host.runLocalCommand,
21
+ status: host.status,
22
+ list: host.list,
23
+ switch: host.switchPod,
24
+ sync: host.sync
25
+ };
26
+ return POD_SUBCOMMANDS.filter((c) => c.name in LEAVE_ACTIONS || hooks[c.name] !== void 0);
27
+ }
28
+ var MODIFIER_LABELS = [
29
+ ["ctrl", "Ctrl"],
30
+ ["shift", "Shift"],
31
+ ["alt", "Alt"],
32
+ ["super", "Super"]
33
+ ];
34
+ function chordKeyLabel(keyId) {
35
+ const parts = keyId.toLowerCase().split("+");
36
+ const base = parts[parts.length - 1] ?? "";
37
+ const mods = MODIFIER_LABELS.filter(([name]) => parts.slice(0, -1).includes(name)).map(([, label]) => label);
38
+ return [...mods, base.length === 1 ? base : base.charAt(0).toUpperCase() + base.slice(1)].join("-");
39
+ }
40
+ function chordHintsFor(chords) {
41
+ if (!chords || !chords.enabled) return null;
42
+ const endings = {};
43
+ for (const { name } of POD_SUBCOMMANDS) {
44
+ const key = Object.keys(chords.bindings).find((k) => chords.bindings[k] === name);
45
+ if (key !== void 0) endings[name] = chordKeyLabel(key);
46
+ }
47
+ return { prefix: chordKeyLabel(chords.prefix), endings };
48
+ }
49
+ function podCommandHelp(hints, subcommands = POD_SUBCOMMANDS) {
50
+ const nameWidth = Math.max(...subcommands.map((c) => c.name.length));
51
+ const chords = subcommands.map((c) => {
52
+ const ending = hints?.endings[c.name];
53
+ return ending ? `${hints.prefix} ${ending}` : "";
54
+ });
55
+ const chordWidth = Math.max(...chords.map((c) => c.length));
56
+ const lines = subcommands.map((c, i) => {
57
+ const chord = chordWidth > 0 ? ` ${chords[i].padEnd(chordWidth)}` : "";
58
+ return ` /pod ${c.name.padEnd(nameWidth)}${chord} \u2014 ${c.description}`;
59
+ });
60
+ return ["/pod commands:", ...lines].join("\n");
61
+ }
62
+ function parsePodCommand(text) {
63
+ const m = /^\/pod(?:\s+(\S+))?(?:\s+(.*))?$/.exec(text.trim());
64
+ if (!m) return null;
65
+ return { sub: m[1] ?? "", arg: (m[2] ?? "").trim() };
66
+ }
67
+ var PodCommandRouter = class {
68
+ constructor(bridge, host = {}) {
69
+ this.bridge = bridge;
70
+ this.host = host;
71
+ }
72
+ bridge;
73
+ host;
74
+ /** Set by the chord wiring so the help text can show what each chord runs (§8). */
75
+ chordHints = null;
76
+ setChordHints(hints) {
77
+ this.chordHints = hints;
78
+ }
79
+ /** True when `text` is a /pod invocation this router owns. */
80
+ matches(text) {
81
+ return parsePodCommand(text) !== null;
82
+ }
83
+ /** What this session can run — the listing, and the chord help, are built from it. */
84
+ available() {
85
+ return availableSubcommands(this.host);
86
+ }
87
+ /** Run a /pod command locally. The caller has already checked {@link matches}. */
88
+ async handle(text) {
89
+ const parsed = parsePodCommand(text);
90
+ if (!parsed) return;
91
+ const { sub, arg } = parsed;
92
+ const leave = LEAVE_ACTIONS[sub];
93
+ if (leave !== void 0) {
94
+ this.bridge.requestLeave(leave);
95
+ return;
96
+ }
97
+ switch (sub) {
98
+ case "local":
99
+ if (!this.host.runLocalCommand) return this.unsupported("local");
100
+ await this.host.runLocalCommand(arg);
101
+ return;
102
+ case "status": {
103
+ if (!this.host.status) return this.unsupported("status");
104
+ const lines = await this.host.status();
105
+ this.bridge.notify(lines.join("\n"));
106
+ return;
107
+ }
108
+ case "shell":
109
+ this.bridge.notify("/pod shell is not supported \u2014 a raw pod shell is not available", "warning");
110
+ return;
111
+ case "list":
112
+ if (!this.host.list) return this.unsupported("list");
113
+ await this.host.list();
114
+ return;
115
+ case "switch":
116
+ if (!this.host.switchPod) return this.unsupported("switch");
117
+ await this.host.switchPod(arg === "" ? void 0 : arg);
118
+ return;
119
+ case "sync":
120
+ if (!this.host.sync) return this.unsupported("sync");
121
+ await this.host.sync();
122
+ return;
123
+ case "":
124
+ this.bridge.notify(podCommandHelp(this.chordHints, this.available()));
125
+ return;
126
+ default:
127
+ this.bridge.notify(`unknown command: /pod ${sub} \u2014 try /pod for the list`, "warning");
128
+ return;
129
+ }
130
+ }
131
+ unsupported(sub) {
132
+ this.bridge.notify(`/pod ${sub} is not available in this session`, "warning");
133
+ }
134
+ };
135
+
136
+ // src/client/runtime/chords.ts
137
+ var CHORD_WINDOW_MS = 3e3;
138
+ function wirePodChords(bridge, router, config) {
139
+ const hints = chordHintsFor(config);
140
+ const ui = bridge.ui;
141
+ if (!hints || !ui?.onTerminalInput) return () => {
142
+ };
143
+ const status = [
144
+ hints.prefix,
145
+ Object.entries(hints.endings).map(([sub, key]) => `${key} ${sub}`).concat("? help").join(" \xB7 ")
146
+ ].join(" \u2014 ");
147
+ let pending = null;
148
+ const clear = () => {
149
+ if (!pending) return;
150
+ clearTimeout(pending.timer);
151
+ pending = null;
152
+ ui.setStatus?.("pod-chord", void 0);
153
+ };
154
+ const handler = (data) => {
155
+ if (isKeyRelease(data) || isKeyRepeat(data)) return void 0;
156
+ if (!pending) {
157
+ if (!matchesKey(data, config.prefix)) return void 0;
158
+ const timer = setTimeout(clear, CHORD_WINDOW_MS);
159
+ timer.unref?.();
160
+ pending = { raw: data, timer };
161
+ ui.setStatus?.("pod-chord", status);
162
+ return { consume: true };
163
+ }
164
+ const raw = pending.raw;
165
+ for (const [key, sub] of Object.entries(config.bindings)) {
166
+ if (!matchesKey(data, key)) continue;
167
+ clear();
168
+ void router.handle(`/pod ${sub}`).catch((e) => {
169
+ bridge.notify(`/pod ${sub} failed: ${e instanceof Error ? e.message : String(e)}`, "error");
170
+ });
171
+ return { consume: true };
172
+ }
173
+ if (matchesKey(data, "?")) {
174
+ clear();
175
+ bridge.notify(podCommandHelp(hints, router.available()));
176
+ return { consume: true };
177
+ }
178
+ if (matchesKey(data, "escape")) {
179
+ clear();
180
+ return { consume: true };
181
+ }
182
+ if (matchesKey(data, config.prefix)) {
183
+ clear();
184
+ return { data: raw };
185
+ }
186
+ clear();
187
+ return void 0;
188
+ };
189
+ router.setChordHints(hints);
190
+ const unsubscribe = ui.onTerminalInput(handler);
191
+ return () => {
192
+ clear();
193
+ router.setChordHints(null);
194
+ unsubscribe();
195
+ };
196
+ }
197
+
198
+ export {
199
+ POD_SUBCOMMANDS,
200
+ PodCommandRouter,
201
+ CHORD_WINDOW_MS,
202
+ wirePodChords
203
+ };
204
+ //# sourceMappingURL=chunk-GNKF75X7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client/runtime/chords.ts","../src/client/runtime/pod-commands.ts"],"sourcesContent":["/**\n * src/client/runtime/chords.ts — `Ctrl-\\` prefix chords for the /pod family (§5.4).\n *\n * Pure keystroke aliases: a chord calls `PodCommandRouter.handle(\"/pod <sub>\")`, so it lands\n * in exactly the code path typing the command does. The listener runs in the *local* TUI's\n * input loop, ahead of the focused component, which is what makes a chord fire mid-stream,\n * mid-compaction, with a picker open, or against a wedged pod-side pi — nothing on the chord\n * path crosses the wire until the action itself does.\n *\n * The prefix is only ever *borrowed*: it is consumed on the way in and released verbatim,\n * ahead of the key that followed, the moment that key turns out not to be an ending.\n */\nimport { isKeyRelease, isKeyRepeat, matchesKey, type KeyId } from \"@earendil-works/pi-tui\";\nimport type { HostBridge } from \"./bridge.js\";\nimport { chordHintsFor, podCommandHelp, type PodCommandRouter } from \"./pod-commands.js\";\n\nexport interface ChordConfig {\n enabled: boolean;\n /** pi-tui KeyId, e.g. `ctrl+\\`. */\n prefix: string;\n /** Ending KeyId → /pod subcommand. */\n bindings: Record<string, string>;\n}\n\n/**\n * How long a lone prefix stays armed. The byte path held 5s because it had to release the\n * prefix to a *remote* pi afterwards; here an expired prefix is dropped (the local editor\n * does nothing with a bare 0x1C, and releasing late would mean injecting input outside a\n * handler return, which pi's API does not offer). The window now only bounds how long the\n * footer hint lingers, so it is shorter.\n */\nexport const CHORD_WINDOW_MS = 3_000;\n\n/**\n * Wire the chord state machine into the bound TUI. Returns an unsubscribe; call it before\n * re-wiring, since `clearExtensionTerminalInputListeners()` drops the listener on every\n * session rebind and the module must not assume its subscription survived.\n */\nexport function wirePodChords(\n bridge: HostBridge,\n router: PodCommandRouter,\n config: ChordConfig,\n): () => void {\n const hints = chordHintsFor(config);\n const ui = bridge.ui;\n if (!hints || !ui?.onTerminalInput) return () => {};\n\n const status = [\n hints.prefix,\n Object.entries(hints.endings)\n .map(([sub, key]) => `${key} ${sub}`)\n .concat(\"? help\")\n .join(\" · \"),\n ].join(\" — \");\n\n let pending: { raw: string; timer: ReturnType<typeof setTimeout> } | null = null;\n\n const clear = (): void => {\n if (!pending) return;\n clearTimeout(pending.timer);\n pending = null;\n ui.setStatus?.(\"pod-chord\", undefined);\n };\n\n const handler = (data: string): { consume?: boolean; data?: string } | undefined => {\n // Key release and repeat events (kitty flag 2) are not presses: one lands between the two\n // chord keys and must neither arm, break, nor release a chord.\n if (isKeyRelease(data) || isKeyRepeat(data)) return undefined;\n\n if (!pending) {\n if (!matchesKey(data, config.prefix as KeyId)) return undefined;\n const timer = setTimeout(clear, CHORD_WINDOW_MS);\n timer.unref?.();\n pending = { raw: data, timer };\n ui.setStatus?.(\"pod-chord\", status);\n return { consume: true };\n }\n\n const raw = pending.raw;\n for (const [key, sub] of Object.entries(config.bindings)) {\n if (!matchesKey(data, key as KeyId)) continue;\n clear();\n void router.handle(`/pod ${sub}`).catch((e: unknown) => {\n bridge.notify(`/pod ${sub} failed: ${e instanceof Error ? e.message : String(e)}`, \"error\");\n });\n return { consume: true };\n }\n if (matchesKey(data, \"?\")) {\n clear();\n bridge.notify(podCommandHelp(hints, router.available()));\n return { consume: true };\n }\n if (matchesKey(data, \"escape\")) {\n clear();\n return { consume: true };\n }\n // A second prefix releases exactly one: a chord only ever borrows the prefix it consumed.\n if (matchesKey(data, config.prefix as KeyId)) {\n clear();\n return { data: raw };\n }\n // Anything else: the key goes on untouched and the borrowed prefix is dropped. Releasing\n // both would mean handing the editor one chunk, since a listener rewrites the chunk it was\n // given rather than injecting a second one — and the editor discards any chunk opening with\n // a control byte (`decodePrintableKey` first, then a `>= 32` test), which every modified\n // prefix does. So concatenating loses the key the user actually typed, to release a prefix\n // the editor would have ignored anyway (§5.1).\n clear();\n return undefined;\n };\n\n router.setChordHints(hints);\n const unsubscribe = ui.onTerminalInput(handler);\n return () => {\n clear();\n router.setChordHints(null);\n unsubscribe();\n };\n}\n","/**\n * src/client/runtime/pod-commands.ts — the /pod command family (§5.4).\n *\n * Client-local: intercepted in the runtime's `session.prompt` before anything touches the\n * wire, which is what makes these work unconditionally — mid-stream, mid-compaction, even\n * with a wedged pi. Lifecycle belongs to the client (the provider control plane is here);\n * agent-visible context belongs to the pod-side extension (§5.4).\n */\nimport { type HostBridge, type PodLeaveAction } from \"./bridge.js\";\nimport type { ChordConfig } from \"./chords.js\";\n\n/**\n * What the /pod commands act on. Lifecycle hooks land in later phases; a subcommand whose\n * hook is absent reports that honestly instead of pretending.\n */\nexport interface PodCommandHost {\n /** Run one locally rendered extension command by name (`/pod local <name> [args]`). */\n runLocalCommand?(arg: string): Promise<void>;\n /** Lines for `/pod status` (§5.4). */\n status?(): Promise<string[]>;\n /** `/pod list` — open the pod picker (§6.4). */\n list?(): Promise<void>;\n /** `/pod switch [pod]` — move the TUI to another pod (§6.4). */\n switchPod?(id?: string): Promise<void>;\n /** `/pod sync` — refresh pod-derived settings and themes for this session. */\n sync?(): Promise<void>;\n}\n\nconst LEAVE_ACTIONS: Record<string, PodLeaveAction> = {\n detach: \"keep\",\n archive: \"archive\",\n};\n\nexport interface PodSubcommand {\n name: string;\n description: string;\n}\n\nexport const POD_SUBCOMMANDS: ReadonlyArray<PodSubcommand> = [\n { name: \"local\", description: \"run a locally rendered extension command here (the pod owns /name by default)\" },\n { name: \"detach\", description: \"leave; the pod keeps running and the idle timer takes over\" },\n { name: \"archive\", description: \"leave, stop the pod and hide it from the list\" },\n { name: \"status\", description: \"pod id, provider, state, repo@branch, egress and idle policy\" },\n { name: \"list\", description: \"pick from this install's pods\" },\n { name: \"switch\", description: \"move to another pod's pi session\" },\n { name: \"sync\", description: \"refresh pod-derived settings and themes\" },\n];\n\n/** Which subcommands a session can actually run: the leave family, plus the host's hooks. */\nexport function availableSubcommands(host: PodCommandHost): PodSubcommand[] {\n const hooks: Record<string, unknown> = {\n local: host.runLocalCommand,\n status: host.status,\n list: host.list,\n switch: host.switchPod,\n sync: host.sync,\n };\n return POD_SUBCOMMANDS.filter((c) => c.name in LEAVE_ACTIONS || hooks[c.name] !== undefined);\n}\n\n/** What the chord wiring tells the help text: how the prefix reads, and which key runs what. */\nexport interface ChordHints {\n /** The prefix as a human writes it, e.g. `Ctrl-\\`. */\n prefix: string;\n /** Subcommand → the ending key that runs it. */\n endings: Record<string, string>;\n}\n\nconst MODIFIER_LABELS: Array<[string, string]> = [\n [\"ctrl\", \"Ctrl\"],\n [\"shift\", \"Shift\"],\n [\"alt\", \"Alt\"],\n [\"super\", \"Super\"],\n];\n\n/** A pi-tui KeyId as config and docs spell it: `ctrl+\\` → `Ctrl-\\`, `escape` → `Escape`. */\nexport function chordKeyLabel(keyId: string): string {\n const parts = keyId.toLowerCase().split(\"+\");\n const base = parts[parts.length - 1] ?? \"\";\n const mods = MODIFIER_LABELS.filter(([name]) => parts.slice(0, -1).includes(name)).map(([, label]) => label);\n return [...mods, base.length === 1 ? base : base.charAt(0).toUpperCase() + base.slice(1)].join(\"-\");\n}\n\n/** The display form of a chord config, or null when chords are off (§7). */\nexport function chordHintsFor(chords: ChordConfig | null | undefined): ChordHints | null {\n if (!chords || !chords.enabled) return null;\n const endings: Record<string, string> = {};\n for (const { name } of POD_SUBCOMMANDS) {\n const key = Object.keys(chords.bindings).find((k) => chords.bindings[k] === name);\n if (key !== undefined) endings[name] = chordKeyLabel(key);\n }\n return { prefix: chordKeyLabel(chords.prefix), endings };\n}\n\n/**\n * The bare-`/pod` listing, with a chord column when chords are wired (§8).\n *\n * `subcommands` is what this session can run. Listing the whole family regardless would\n * advertise capabilities that answer \"not available in this session\" — the listing is where\n * people learn what /pod is, so it has to be true of the session they are in.\n */\nexport function podCommandHelp(\n hints?: ChordHints | null,\n subcommands: ReadonlyArray<PodSubcommand> = POD_SUBCOMMANDS,\n): string {\n const nameWidth = Math.max(...subcommands.map((c) => c.name.length));\n const chords = subcommands.map((c) => {\n const ending = hints?.endings[c.name];\n return ending ? `${hints!.prefix} ${ending}` : \"\";\n });\n const chordWidth = Math.max(...chords.map((c) => c.length));\n const lines = subcommands.map((c, i) => {\n const chord = chordWidth > 0 ? ` ${chords[i]!.padEnd(chordWidth)}` : \"\";\n return ` /pod ${c.name.padEnd(nameWidth)}${chord} — ${c.description}`;\n });\n return [\"/pod commands:\", ...lines].join(\"\\n\");\n}\n\n/** `/pod …` → its subcommand and argument, or null when this is not a /pod invocation. */\nexport function parsePodCommand(text: string): { sub: string; arg: string } | null {\n const m = /^\\/pod(?:\\s+(\\S+))?(?:\\s+(.*))?$/.exec(text.trim());\n if (!m) return null;\n return { sub: m[1] ?? \"\", arg: (m[2] ?? \"\").trim() };\n}\n\nexport class PodCommandRouter {\n constructor(\n private readonly bridge: HostBridge,\n private readonly host: PodCommandHost = {},\n ) {}\n\n /** Set by the chord wiring so the help text can show what each chord runs (§8). */\n private chordHints: ChordHints | null = null;\n\n setChordHints(hints: ChordHints | null): void {\n this.chordHints = hints;\n }\n\n /** True when `text` is a /pod invocation this router owns. */\n matches(text: string): boolean {\n return parsePodCommand(text) !== null;\n }\n\n /** What this session can run — the listing, and the chord help, are built from it. */\n available(): PodSubcommand[] {\n return availableSubcommands(this.host);\n }\n\n /** Run a /pod command locally. The caller has already checked {@link matches}. */\n async handle(text: string): Promise<void> {\n const parsed = parsePodCommand(text);\n if (!parsed) return;\n const { sub, arg } = parsed;\n\n const leave = LEAVE_ACTIONS[sub];\n if (leave !== undefined) {\n this.bridge.requestLeave(leave);\n return;\n }\n\n switch (sub) {\n case \"local\":\n if (!this.host.runLocalCommand) return this.unsupported(\"local\");\n await this.host.runLocalCommand(arg);\n return;\n case \"status\": {\n if (!this.host.status) return this.unsupported(\"status\");\n const lines = await this.host.status();\n this.bridge.notify(lines.join(\"\\n\"));\n return;\n }\n case \"shell\":\n this.bridge.notify(\"/pod shell is not supported — a raw pod shell is not available\", \"warning\");\n return;\n case \"list\":\n if (!this.host.list) return this.unsupported(\"list\");\n await this.host.list();\n return;\n case \"switch\":\n if (!this.host.switchPod) return this.unsupported(\"switch\");\n await this.host.switchPod(arg === \"\" ? undefined : arg);\n return;\n case \"sync\":\n if (!this.host.sync) return this.unsupported(\"sync\");\n await this.host.sync();\n return;\n case \"\":\n this.bridge.notify(podCommandHelp(this.chordHints, this.available()));\n return;\n default:\n this.bridge.notify(`unknown command: /pod ${sub} — try /pod for the list`, \"warning\");\n return;\n }\n }\n\n private unsupported(sub: string): void {\n this.bridge.notify(`/pod ${sub} is not available in this session`, \"warning\");\n }\n}\n\n/** The one-line session-start hint: the leave family, and the chords that alias it (§8). */\nexport function podCommandNotice(hints?: ChordHints | null): string {\n const base = \"leave with /pod detach, or logically archive with /pod archive\";\n const keys = [\"detach\", \"archive\"].map((sub) => hints?.endings[sub]);\n if (!hints || keys.some((k) => k === undefined)) return base;\n return `${base} — or ${hints.prefix} then ${keys.join(\" / \")}`;\n}\n"],"mappings":"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","names":[]}