@clearlysh/beehaven 0.8.16

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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
@@ -0,0 +1,418 @@
1
+ /**
2
+ * `beehaven slides` — presentations.
3
+ *
4
+ * ⚠⚠ SEVENTEEN `deck-*` ACTIONS AND NO CLI DOOR. Same shape as `sheet`: a first-class artifact
5
+ * — a deck is a document row, with a key, project filing, trash and search like anything else —
6
+ * reachable only as `beehaven call deck-add-slide '{…}'`. Thirteen of the seventeen also had no
7
+ * input schema until 2026-09-05, so the arguments could only be learned by reading the handler.
8
+ *
9
+ * ⚠ THE TRAPS THIS MAKES UNTYPEABLE:
10
+ *
11
+ * · `deck-move-slide` REQUIRES `to` and refuses without it (`code: "no-target"`) — not something
12
+ * the verb name suggests. This verb takes it positionally, so it cannot be forgotten.
13
+ * · `deck-render` vs `deck-export` are near-synonyms that do different jobs: render is to LOOK
14
+ * at a deck, export is to take the FILE out. Two subcommands, `show` and `export`, named for
15
+ * the job rather than the mechanism.
16
+ * · A layout typed from memory does not error — an unrecognised one silently falls back to
17
+ * `titleBody`. `slides layouts` is one call and is named in the refusal.
18
+ *
19
+ * ⚠ `agentRpc`, NEVER bare `rpcOnce` — see the note in `ticket.ts`.
20
+ */
21
+ import { agentRpc as rpcOnce } from './workspace.js';
22
+ /** Only `pptx` returns a `filename`; the text formats return bare `content`. Falling back to
23
+ * `${id}.${format}` wrote `PLA-3518.markdown` — an id nobody recognises and an extension no
24
+ * editor associates. Fall back to the deck's TITLE and a real extension. */
25
+ export const DECK_EXPORT_EXT = {
26
+ markdown: 'md', notes: 'txt', html: 'html', svg: 'svg', pptx: 'pptx',
27
+ // `slides show`'s formats. `png` is the only one that arrives as a data: URI.
28
+ png: 'png', tsv: 'tsv',
29
+ };
30
+ /** Extension for a `data:` URI's mime type. `image/svg+xml` → `svg`, not `svg+xml`. */
31
+ export const DATA_URI_EXT = {
32
+ 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/svg+xml': 'svg',
33
+ 'application/pdf': 'pdf',
34
+ };
35
+ /**
36
+ * ⚠⚠ THE EXPORT DECISION, EXTRACTED SO A GUARD CAN EXECUTE IT (CBH2-42).
37
+ *
38
+ * This logic used to live inline in the `export` branch, which made it unreachable from a test —
39
+ * so its guard (`slides-export-writes.test.ts`) could only read the file as TEXT and assert that
40
+ * identifiers like `writeFileSync` appeared somewhere in it. That guard called itself behavioural
41
+ * and was not: every string it matched also survives in a COMMENT, so the fix could be deleted and
42
+ * the suite stayed green. Extraction IS the fix to that criticism, not a tidy-up — the same move
43
+ * that rescued `clipToRange` in `sheet-rpcs.ts`.
44
+ *
45
+ * Everything here is a DECISION; the caller does the IO (two `writeFileSync`, one `console.log`).
46
+ * `exists` and `resolvePath` are INJECTED rather than imported, so the clobber refusal and the
47
+ * filename derivation can both be exercised without touching a real filesystem or a real cwd.
48
+ *
49
+ * ⚠ BASE64 vs TEXT IS DECIDED BY WHICH FIELD CAME BACK, never by the format name. Keying on
50
+ * `format === 'pptx'` breaks the day another binary format is added, and writing a base64 STRING
51
+ * into a .pptx produces a file that opens as garbage rather than failing loudly.
52
+ *
53
+ * Sabotage-tested (2026-09-05): removing the clobber refusal fails 1, keying on the format name
54
+ * fails 3, and falling back to `id.format` fails 2.
55
+ *
56
+ * ⚠⚠ THREE VERBS SHARE IT NOW (CBH2-85): `slides export`, `slides show` and `sheet get`. `--out`
57
+ * and `-o` are registered GLOBALLY in `parseArgs`, so EVERY verb accepts them — acceptance has
58
+ * never been evidence of support, and those two rendered/printed and wrote no file. The clobber
59
+ * refusal and the extension fallback are both non-obvious and already tested; a second and third
60
+ * copy of them is how one caller keeps refusing to overwrite while another silently does.
61
+ *
62
+ * ⚠ IT IS SHAPED BY THE PAYLOAD, NOT BY THE VERB, which is what lets a sheet reuse it: hand it
63
+ * `{ content, filename }` and it plans one text file, the same way `deck-export`'s markdown does.
64
+ * The name stays `planDeckExport` — renaming it would churn every guard that names it for no gain.
65
+ *
66
+ * ⚠⚠ A `data:` URI IS BINARY AND MUST BE DECODED. `deck-render { format: 'png' }` answers with
67
+ * `slides: ['data:image/png;base64,…']`, and writing those STRINGS to `.png` files produces images
68
+ * that open as garbage — exactly the failure the base64-vs-text rule above exists to prevent, one
69
+ * payload shape further out. The extension follows the URI's own mime type rather than the
70
+ * requested format, so a fallback to SVG (which `deck-render` does silently when rasterising
71
+ * fails) lands in a file named `.svg`, not a `.png` containing SVG.
72
+ */
73
+ export function planDeckExport(res, opts, exists, resolvePath) {
74
+ const fmt = String(opts.format ?? 'pptx');
75
+ const ext = DECK_EXPORT_EXT[fmt] ?? fmt;
76
+ const name = String(res?.filename || `${res?.title || opts.id}.${ext}`);
77
+ /**
78
+ * ⚠⚠ `svg` IS A THIRD PAYLOAD SHAPE — `slides: [...]`, one entry per slide, with NO `base64`
79
+ * and NO `content`. Measured on a 12-slide deck. The first cut of this verb knew only the two
80
+ * shapes, so `--format svg` refused with "neither base64 nor content" on a format the help
81
+ * offers. **A deck of N slides is N SVGs** — there is no single-file form to fall back to, so
82
+ * `--out` names the BASE and the slides land beside it as `<base>-1.svg` … `<base>-N.svg`.
83
+ */
84
+ const slides = Array.isArray(res?.slides) ? res.slides : [];
85
+ if (slides.length) {
86
+ /**
87
+ * ⚠ ANY KNOWN EXTENSION IS STRIPPED, NOT ONLY THE ONE THIS RUN EMITS. `slides show --out
88
+ * shot.png` whose render DEGRADES to SVG would otherwise write `shot.png-1.svg` — a name
89
+ * carrying two extensions, the first of which is wrong. Only extensions this CLI itself emits
90
+ * are stripped, so `My.Deck.Name` keeps its dots.
91
+ */
92
+ const known = new Set([...Object.values(DECK_EXPORT_EXT), ...Object.values(DATA_URI_EXT)]);
93
+ const base = String(opts.out ?? name).replace(/\.([A-Za-z0-9]+)$/, (m, e) => (known.has(String(e).toLowerCase()) ? '' : m));
94
+ const files = slides.map((s, i) => {
95
+ const body = typeof s === 'string' ? s : String(s?.svg ?? s?.content ?? '');
96
+ // ⚠ See the header: a `data:` URI carries BINARY, and its own mime names the extension.
97
+ const uri = /^data:([\w.+-]+\/[\w.+-]+);base64,(.*)$/s.exec(body);
98
+ if (uri) {
99
+ const mimeExt = DATA_URI_EXT[uri[1]] ?? uri[1].split('/')[1] ?? ext;
100
+ return {
101
+ dest: resolvePath(`${base}-${i + 1}.${mimeExt}`),
102
+ body: { kind: 'binary', base64: uri[2] },
103
+ };
104
+ }
105
+ return { dest: resolvePath(`${base}-${i + 1}.${ext}`), body: { kind: 'text', content: body } };
106
+ });
107
+ const clash = opts.force ? null : files.find((f) => exists(f.dest));
108
+ if (clash) {
109
+ return { ok: false, code: 'exists', error: `${clash.dest} already exists`,
110
+ hint: `This export writes ${files.length} files. Pass --force to overwrite, or --out <base> to write elsewhere.` };
111
+ }
112
+ return { ok: true, files };
113
+ }
114
+ const dest = resolvePath(String(opts.out ?? name));
115
+ // ⚠ An export is a READ. Silently overwriting a neighbouring file is not something a read
116
+ // should be able to do; `--force` is the opt-in.
117
+ if (exists(dest) && !opts.force) {
118
+ return { ok: false, code: 'exists', error: `${dest} already exists`,
119
+ hint: 'Pass --force to overwrite, or --out <path> to write elsewhere.' };
120
+ }
121
+ if (typeof res?.base64 === 'string' && res.base64) {
122
+ return { ok: true, files: [{ dest, body: { kind: 'binary', base64: res.base64 } }] };
123
+ }
124
+ if (typeof res?.content === 'string') {
125
+ return { ok: true, files: [{ dest, body: { kind: 'text', content: res.content } }] };
126
+ }
127
+ // ⚠ Say what came back rather than writing an empty file that looks like a successful export.
128
+ return { ok: false, code: 'no-payload',
129
+ error: 'the export returned neither `base64`, `content` nor `slides` — nothing to write',
130
+ hint: `Keys returned: ${Object.keys(res ?? {}).join(', ')}` };
131
+ }
132
+ const c = {
133
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
134
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
135
+ };
136
+ function getTimeout(values) {
137
+ const t = Number(values?.timeout);
138
+ return Number.isFinite(t) && t > 0 ? t : 30_000;
139
+ }
140
+ function failure(res) {
141
+ if (!res || typeof res !== 'object')
142
+ return null;
143
+ if (typeof res.error === 'string' && res.error)
144
+ return res.error;
145
+ if (res.ok === false)
146
+ return String(res.message ?? res.code ?? 'the workspace refused the call');
147
+ return null;
148
+ }
149
+ function die(msg, hint) {
150
+ console.error(` ${msg}`);
151
+ if (hint)
152
+ console.error(c.dim(` ${hint}`));
153
+ process.exit(1);
154
+ }
155
+ export async function cmdSlides(sub, rest, values) {
156
+ const t = getTimeout(values);
157
+ if (!sub || sub === 'help') {
158
+ console.log(`
159
+ ${c.cyan('beehaven slides')} — presentations
160
+
161
+ slides list Every deck, newest first
162
+ slides new "<title>" Create one (--project <id>, --aspect 16:9|4:3)
163
+ slides get <id> The outline (--slide N for one slide in full)
164
+ slides add <id> "<title>" Append a slide (--layout, --bullets "a|b|c", --notes, --at N)
165
+ slides move <id> <n> <to> Reorder — <to> is the 1-based destination
166
+ slides rm <id> <n> Delete a slide
167
+ slides layouts The layout ids you may pass to --layout
168
+ slides show <id> Render it to look at (--slide N)
169
+ slides export <id> Take the file out (--format pptx|html|markdown|notes|svg)
170
+
171
+ ${c.dim('<id> is a deck id or its key. <n> is a 1-based slide number or a slide id.')}
172
+ ${c.dim('show renders a picture; export produces a file. They are not the same verb.')}
173
+ `);
174
+ return;
175
+ }
176
+ if (sub === 'list') {
177
+ const res = await rpcOnce('deck-list', {}, t, values);
178
+ const f = failure(res);
179
+ if (f)
180
+ die(f);
181
+ if (values.json) {
182
+ console.log(JSON.stringify(res, null, 2));
183
+ return;
184
+ }
185
+ const rows = res?.decks ?? [];
186
+ if (!rows.length) {
187
+ console.log(' No decks.');
188
+ return;
189
+ }
190
+ console.log('');
191
+ for (const d of rows) {
192
+ const n = d.slideCount ?? d.slides?.length;
193
+ console.log(` ${c.cyan(String(d.key ?? d.id ?? ''))} ${String(d.title ?? 'Untitled')}${n != null ? c.dim(` · ${n} slides`) : ''}`);
194
+ }
195
+ console.log('');
196
+ return;
197
+ }
198
+ if (sub === 'new' || sub === 'create') {
199
+ const title = rest.join(' ').trim();
200
+ if (!title)
201
+ die('a title is required', 'beehaven slides new "Q1 review"');
202
+ const res = await rpcOnce('deck-create', {
203
+ title,
204
+ ...(values.aspect ? { aspect: String(values.aspect) } : {}),
205
+ ...(values.project ? { projectId: String(values.project) } : {}),
206
+ }, t, values);
207
+ const f = failure(res);
208
+ if (f)
209
+ die(f);
210
+ if (values.json) {
211
+ console.log(JSON.stringify(res, null, 2));
212
+ return;
213
+ }
214
+ console.log(` Created ${c.cyan(String(res?.key ?? res?.id ?? ''))}`);
215
+ console.log(c.dim(' Add slides: beehaven slides add <id> "<title>" --bullets "a|b|c"'));
216
+ return;
217
+ }
218
+ if (sub === 'get') {
219
+ const id = rest[0];
220
+ if (!id)
221
+ die('a deck id or key is required', 'beehaven slides get CLR-42');
222
+ const res = await rpcOnce('deck-get', {
223
+ id,
224
+ ...(values.slide ? { slide: Number(values.slide) } : {}),
225
+ }, t, values);
226
+ const f = failure(res);
227
+ if (f)
228
+ die(f);
229
+ if (values.json) {
230
+ console.log(JSON.stringify(res, null, 2));
231
+ return;
232
+ }
233
+ const outline = res?.outline ?? [];
234
+ if (!outline.length) {
235
+ console.log(JSON.stringify(res, null, 2));
236
+ return;
237
+ }
238
+ console.log('');
239
+ outline.forEach((s, i) => console.log(` ${String(i + 1).padStart(2)}. ${String(s.title ?? s.layout ?? '')}`));
240
+ console.log('');
241
+ return;
242
+ }
243
+ if (sub === 'add') {
244
+ const [id, ...titleParts] = rest;
245
+ if (!id)
246
+ die('a deck id or key is required', 'beehaven slides add CLR-42 "Results"');
247
+ const res = await rpcOnce('deck-add-slide', {
248
+ id,
249
+ ...(titleParts.length ? { title: titleParts.join(' ') } : {}),
250
+ ...(values.layout ? { layout: String(values.layout) } : {}),
251
+ // ⚠ PIPE-SEPARATED, not comma — a bullet routinely contains a comma and almost never a pipe.
252
+ ...(values.bullets ? { bullets: String(values.bullets).split('|').map((b) => b.trim()).filter(Boolean) } : {}),
253
+ ...(values.notes ? { notes: String(values.notes) } : {}),
254
+ ...(values.at ? { at: Number(values.at) } : {}),
255
+ }, t, values);
256
+ const f = failure(res);
257
+ if (f)
258
+ die(f);
259
+ if (values.json) {
260
+ console.log(JSON.stringify(res, null, 2));
261
+ return;
262
+ }
263
+ console.log(` Added slide to ${c.cyan(id)}`);
264
+ return;
265
+ }
266
+ if (sub === 'move') {
267
+ const [id, slide, to] = rest;
268
+ // ⚠ `to` POSITIONALLY, because the action refuses without it and the verb name gives no hint.
269
+ if (!id || !slide || !to) {
270
+ die('usage: beehaven slides move <id> <n> <to>', 'e.g. beehaven slides move CLR-42 5 2 — <to> is the 1-based destination and is required.');
271
+ }
272
+ const res = await rpcOnce('deck-move-slide', { id, slide, to: Number(to) }, t, values);
273
+ const f = failure(res);
274
+ if (f)
275
+ die(f);
276
+ if (values.json) {
277
+ console.log(JSON.stringify(res, null, 2));
278
+ return;
279
+ }
280
+ console.log(` Moved slide ${slide} → ${to} in ${c.cyan(id)}`);
281
+ return;
282
+ }
283
+ if (sub === 'rm' || sub === 'delete') {
284
+ const [id, slide] = rest;
285
+ if (!id || !slide)
286
+ die('usage: beehaven slides rm <id> <n>');
287
+ const res = await rpcOnce('deck-delete-slide', { id, slide }, t, values);
288
+ const f = failure(res);
289
+ if (f)
290
+ die(f);
291
+ if (values.json) {
292
+ console.log(JSON.stringify(res, null, 2));
293
+ return;
294
+ }
295
+ // ⚠ The action returns the slide it removed precisely so this can say WHAT went.
296
+ const gone = res?.removed?.title;
297
+ console.log(` Removed slide ${slide}${gone ? ` — ${c.dim(String(gone))}` : ''} from ${c.cyan(id)}`);
298
+ return;
299
+ }
300
+ if (sub === 'layouts') {
301
+ const res = await rpcOnce('deck-layouts', {}, t, values);
302
+ const f = failure(res);
303
+ if (f)
304
+ die(f);
305
+ if (values.json) {
306
+ console.log(JSON.stringify(res, null, 2));
307
+ return;
308
+ }
309
+ console.log('');
310
+ for (const l of (res?.layouts ?? []))
311
+ console.log(` ${c.cyan(String(l.id))} ${c.dim(String(l.label ?? ''))}`);
312
+ console.log('');
313
+ return;
314
+ }
315
+ if (sub === 'show' || sub === 'render') {
316
+ const id = rest[0];
317
+ if (!id)
318
+ die('a deck id or key is required');
319
+ const res = await rpcOnce('deck-render', {
320
+ id,
321
+ ...(values.slide ? { slide: Number(values.slide) } : {}),
322
+ ...(values.format ? { format: String(values.format) } : {}),
323
+ }, t, values);
324
+ const f = failure(res);
325
+ if (f)
326
+ die(f);
327
+ /**
328
+ * ⚠⚠ IT ACCEPTED `--out` AND WROTE NOTHING (CBH2-85) — `--out`/`-o` are registered globally in
329
+ * `parseArgs`, so this verb took the flag, ignored it, and printed a JSON envelope holding
330
+ * megabytes of base64 PNG to the terminal. Same accept-and-ignore class as `slides export`
331
+ * before CBH2-2, and the same fix: `planDeckExport` owns the decision, this branch does IO.
332
+ *
333
+ * ⚠ THE FLAG IS THE OPT-IN. Without it the behaviour is byte-identical to before — a render is
334
+ * usually looked at, not saved, and writing files from a read verb by default is a surprise.
335
+ */
336
+ const outPath = (values.out ?? values.output);
337
+ if (!outPath) {
338
+ console.log(JSON.stringify(res, null, 2));
339
+ return;
340
+ }
341
+ const { writeFileSync, existsSync } = await import('fs');
342
+ const { resolve } = await import('path');
343
+ const plan = planDeckExport(res,
344
+ // ⚠ THE SERVER'S OWN `format`, NOT THE REQUESTED ONE. `deck-render` falls back to SVG when
345
+ // rasterising fails (`degraded`), so trusting `--format png` would name a `.png` holding SVG.
346
+ { format: String(res?.format ?? values.format ?? 'svg'), out: outPath, id, force: !!values.force }, existsSync, resolve);
347
+ if (!plan.ok)
348
+ die(plan.error, plan.hint);
349
+ if (res?.degraded)
350
+ console.log(` ${c.dim(`⚠ ${String(res.degraded)}`)}`);
351
+ for (const file of plan.files) {
352
+ const buf = file.body.kind === 'binary'
353
+ ? Buffer.from(file.body.base64, 'base64')
354
+ : Buffer.from(file.body.content, 'utf8');
355
+ writeFileSync(file.dest, buf);
356
+ console.log(` Wrote ${c.cyan(file.dest)}${c.dim(` · ${buf.length} bytes`)}`);
357
+ }
358
+ return;
359
+ }
360
+ if (sub === 'export') {
361
+ const id = rest[0];
362
+ if (!id)
363
+ die('a deck id or key is required');
364
+ const res = await rpcOnce('deck-export', {
365
+ id,
366
+ format: String(values.format ?? 'pptx'),
367
+ }, t, values);
368
+ const f = failure(res);
369
+ if (f)
370
+ die(f);
371
+ if (values.json) {
372
+ console.log(JSON.stringify(res, null, 2));
373
+ return;
374
+ }
375
+ /**
376
+ * ⚠⚠ WRITE THE FILE. THIS PRINTED THE PAYLOAD TO STDOUT (fixed 2026-09-05, CBH2-2).
377
+ *
378
+ * The help I wrote for this verb says export "produces a FILE to take away", and it produced
379
+ * no file anywhere — it dumped the envelope to stdout, which for the DEFAULT format (pptx)
380
+ * is 16.5 KB of base64 into the terminal. `--out` and `-o` are registered globally in
381
+ * `parseArgs`, so they were accepted and silently ignored: the exact accept-and-ignore class
382
+ * this sprint keeps finding, in a verb written during the sprint.
383
+ *
384
+ * ⚠ THE ACTION ALREADY HAD EVERYTHING NEEDED — it returns `filename` and either `base64`
385
+ * (pptx) or `content` (text formats). Nothing had to be added server-side; the verb simply
386
+ * never used them.
387
+ *
388
+ * ⚠ BASE64 vs TEXT IS DECIDED BY WHICH FIELD CAME BACK, not by the format name. Keying on
389
+ * `format === 'pptx'` would break the day another binary format is added, and writing a
390
+ * base64 STRING to a .pptx produces a file that opens as garbage rather than failing.
391
+ *
392
+ * ⚠ REFUSES TO CLOBBER. An export is a read; silently overwriting a neighbouring file is not
393
+ * something a read should be able to do. `--force` is the opt-in.
394
+ */
395
+ const { writeFileSync, existsSync } = await import('fs');
396
+ const { resolve } = await import('path');
397
+ // ⚠ The DECISION is `planDeckExport` (pure, executed by the guard); this branch does only IO.
398
+ const plan = planDeckExport(res, { format: String(values.format ?? 'pptx'), out: (values.out ?? values.output), id, force: !!values.force }, existsSync, resolve);
399
+ if (!plan.ok)
400
+ die(plan.error, plan.hint);
401
+ /**
402
+ * ⚠ THE SIZE IS MEASURED HERE, NOT TAKEN FROM THE SERVER. `deck-export` reports `bytes` as a
403
+ * CHARACTER count: a 3,993-character markdown deck is 4,046 bytes on disk once the em-dashes
404
+ * and ⚠ glyphs are UTF-8 encoded, and the CLI printed the server's 3,993 beside a file that
405
+ * `ls` calls 4,046. Small, and exactly the class this sprint is about — a number that looks
406
+ * like an answer to a question nobody asked it.
407
+ */
408
+ for (const f of plan.files) {
409
+ const buf = f.body.kind === 'binary'
410
+ ? Buffer.from(f.body.base64, 'base64')
411
+ : Buffer.from(f.body.content, 'utf8');
412
+ writeFileSync(f.dest, buf);
413
+ console.log(` Wrote ${c.cyan(f.dest)}${c.dim(` · ${buf.length} bytes`)}`);
414
+ }
415
+ return;
416
+ }
417
+ die(`unknown subcommand "${sub}"`, 'beehaven slides help');
418
+ }