@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,932 @@
1
+ /**
2
+ * `beehaven canvas` — edit the Clearly editor canvas from the CLI.
3
+ *
4
+ * Wraps the Home DO's `ui-open`, `canvas-invoke`, `device-record-start/stop`
5
+ * actions with a friendlier surface. Auto-fills `deviceId` from the pinned
6
+ * device (see `beehaven devices pin <clientId>`).
7
+ *
8
+ * beehaven canvas open <noteId> # open the composition on the pinned device
9
+ * beehaven canvas invoke <action> [json-args] # run a registered editor action
10
+ * beehaven canvas query <action> # alias for `invoke` on `*.snapshot` actions
11
+ * beehaven canvas actions # list all action ids registered on the client
12
+ * beehaven canvas record start [sessionId] # start recording on pinned device
13
+ * beehaven canvas record stop # stop recording
14
+ *
15
+ * Flags:
16
+ * --device <clientId> override the pinned device for this call
17
+ * --client-type <type> restrict to mac-app / web / ios-app
18
+ * --note <noteId> stamp the noteId on the envelope (useful in logs)
19
+ * --json output raw JSON
20
+ */
21
+ import { execFile } from 'node:child_process';
22
+ import { readFileSync, existsSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { homedir } from 'node:os';
25
+ import { rpcOnce } from '../ipc-client.js';
26
+ import { getPinnedDevice } from './devices.js';
27
+ import { withUtm } from '../utm.js';
28
+ // ⚠ The ONE origin resolver — see `canvasUrl`. This module is dependency-free by design.
29
+ import { resolveRelaySelection, appOriginForEnv } from '../relay-presets.js';
30
+ /** Build the web canvas URL for a composition. `--url <base>` overrides the origin entirely;
31
+ * `--dev` forces localhost; otherwise it follows the env the daemon is dialling.
32
+ *
33
+ * ⚠⚠ THIS WAS THE THIRD SPELLING OF THE ORIGIN IN THE TREE (CBH-45), and the odd one out: it
34
+ * said `https://clearly.sh` — the APEX — while `open` said `https://www.clearly.sh`. The apex
35
+ * 301s to www, so every canvas link handed out from here carried a redirect, and reading the two
36
+ * side by side gave no way to tell which was authoritative.
37
+ *
38
+ * ⚠ IT ALSO IGNORED `beehaven env`: a daemon pointed at a local relay still printed a production
39
+ * URL unless the caller happened to remember `--dev`. `appOriginForEnv` is the one resolver now,
40
+ * so a link's origin follows the workspace it came from. `--dev` / `CLEARLY_DEV` still win, since
41
+ * that is an explicit statement about where the reader is. */
42
+ function canvasUrl(noteId, values) {
43
+ if (values.url)
44
+ return withUtm(`${String(values.url).replace(/\/+$/, '')}/c/${noteId}`);
45
+ if (values.dev || process.env.CLEARLY_DEV)
46
+ return withUtm(`https://localhost:3000/c/${noteId}`);
47
+ let base = 'https://www.clearly.sh';
48
+ try {
49
+ // ⚠ STATIC IMPORTS, not `require` — this package is ESM ("type": "module"), where `require` is
50
+ // not defined, and `canvasUrl` is sync so `await import` is unavailable. `relay-presets` is
51
+ // deliberately dependency-free precisely so every surface can import it directly.
52
+ const p = join(homedir(), '.beehaven', 'config.json');
53
+ const cfg = existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : {};
54
+ base = appOriginForEnv(resolveRelaySelection(cfg).envName) ?? base;
55
+ }
56
+ catch { /* an unreadable config means production, which is what it always was */ }
57
+ return withUtm(`${base}/c/${noteId}`);
58
+ }
59
+ /** Open a URL in the user's default browser (a fresh live client). Cross-platform. */
60
+ function launchInBrowser(url) {
61
+ try {
62
+ if (process.platform === 'darwin')
63
+ execFile('open', [url], () => { });
64
+ else if (process.platform === 'win32')
65
+ execFile('cmd', ['/c', 'start', '', url], () => { });
66
+ else
67
+ execFile('xdg-open', [url], () => { });
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ function parseJsonOrBail(raw, fallback = {}) {
75
+ if (!raw)
76
+ return fallback;
77
+ try {
78
+ const parsed = JSON.parse(raw);
79
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
80
+ return parsed;
81
+ return fallback;
82
+ }
83
+ catch {
84
+ console.error(` Invalid JSON: ${raw}`);
85
+ process.exit(1);
86
+ }
87
+ }
88
+ function resolveTarget(values) {
89
+ const cliDevice = values.device;
90
+ const cliType = values['client-type'];
91
+ if (cliDevice || cliType)
92
+ return { deviceId: cliDevice, clientType: cliType };
93
+ const pinned = getPinnedDevice();
94
+ if (!pinned)
95
+ return {};
96
+ return { deviceId: pinned.clientId, clientType: pinned.clientType };
97
+ }
98
+ export async function cmdCanvas(sub, positionals, values) {
99
+ switch (sub) {
100
+ case 'open':
101
+ return cmdCanvasOpen(positionals, values);
102
+ case 'invoke':
103
+ case 'run':
104
+ return cmdCanvasInvoke(positionals, values);
105
+ case 'query':
106
+ case 'snapshot':
107
+ return cmdCanvasQuery(positionals, values);
108
+ case 'actions':
109
+ case 'list':
110
+ return cmdCanvasActions(values);
111
+ // ── review + verify (read the canvas, self-test every action) ──────────
112
+ case 'perceive':
113
+ case 'look':
114
+ return cmdCanvasPerceive(positionals, values);
115
+ case 'status':
116
+ return cmdCanvasStatus(positionals, values);
117
+ case 'verify':
118
+ case 'selftest':
119
+ return cmdCanvasVerify(positionals, values);
120
+ case 'prompt':
121
+ case 'ask':
122
+ return cmdCanvasPrompt(positionals, values);
123
+ case 'goto':
124
+ case 'nav':
125
+ case 'navigate':
126
+ return cmdCanvasGoto(positionals, values);
127
+ case 'help':
128
+ case 'catalog':
129
+ return cmdCanvasCatalog(positionals, values);
130
+ case 'record':
131
+ return cmdCanvasRecord(positionals, values);
132
+ // ── Composition-level verbs (task #84) ────────────────────
133
+ // Thin wrappers over the workspace RPCs so agents and humans
134
+ // can manage compositions without remembering the RPC names.
135
+ // Each verb maps to one cloud-side action; the canonical
136
+ // catalog is `beehaven canvas help --search composition`.
137
+ case 'new':
138
+ case 'create':
139
+ return cmdCompositionCreate(positionals, values);
140
+ case 'rename':
141
+ return cmdCompositionRename(positionals, values);
142
+ case 'delete':
143
+ case 'rm':
144
+ return cmdCompositionDelete(positionals, values);
145
+ case 'detail':
146
+ case 'get':
147
+ return cmdCompositionDetail(positionals, values);
148
+ case 'export':
149
+ return cmdCompositionExport(positionals, values);
150
+ // ── Quick text-onto-canvas (companion to teleport --as text) ──
151
+ // `beehaven canvas paste-text "string..."` or stdin pipe:
152
+ // cat note.md | beehaven canvas paste-text --composition <id>
153
+ // Drops a text block on the canvas without needing a file path.
154
+ // Useful for one-shot agent dictation, LLM scratch, etc.
155
+ case 'paste-text':
156
+ case 'text':
157
+ return cmdCanvasPasteText(positionals, values);
158
+ // ── publish-landing — built site dir → site comp → embed block ─
159
+ // `beehaven canvas publish-landing ./dist --composition <id>`
160
+ // composes compose-create + compose-push + canvas-invoke into one
161
+ // shot. Closes the loop on "Claude generates landing → lands on
162
+ // user's pasteboard as a live, interactive iframe block".
163
+ case 'publish-landing':
164
+ case 'publish-site':
165
+ return cmdCanvasPublishLanding(positionals, values);
166
+ default:
167
+ printHelp();
168
+ return;
169
+ }
170
+ }
171
+ async function cmdCanvasPublishLanding(positionals, values) {
172
+ const dir = positionals[0];
173
+ if (!dir) {
174
+ console.error(' Usage: beehaven canvas publish-landing <dist-dir> --composition <id>');
175
+ console.error(' Takes a pre-built static-site directory (Vite dist/, Next out/,');
176
+ console.error(' Astro dist/, SvelteKit build/, or plain HTML), creates a site');
177
+ console.error(' composition, pushes the bundle, and embeds the served URL as a');
178
+ console.error(' website block on the target pasteboard.');
179
+ process.exit(1);
180
+ }
181
+ const targetCompositionId = values.composition;
182
+ if (!targetCompositionId) {
183
+ console.error(' --composition <id> required (the pasteboard to embed the landing on).');
184
+ process.exit(1);
185
+ }
186
+ const { resolve } = await import('path');
187
+ const { statSync } = await import('fs');
188
+ const absDir = resolve(dir);
189
+ try {
190
+ const s = statSync(absDir);
191
+ if (!s.isDirectory()) {
192
+ console.error(` Not a directory: ${absDir}`);
193
+ process.exit(1);
194
+ }
195
+ }
196
+ catch {
197
+ console.error(` Directory not found: ${absDir}`);
198
+ process.exit(1);
199
+ }
200
+ const title = values.title || 'Landing — published from CLI';
201
+ console.log(` Publishing landing from ${absDir}…`);
202
+ console.log(` Target pasteboard: ${targetCompositionId}`);
203
+ // Step 1 — create a fresh site composition. The site bundle lives
204
+ // here; the target pasteboard just embeds the served URL.
205
+ console.log(' [1/3] Creating site composition…');
206
+ const create = await rpcOnce('composition-create', { title, type: 'site' }, 30_000);
207
+ const siteCompositionId = create?.id ?? create?.result?.id;
208
+ if (!siteCompositionId) {
209
+ console.error(` composition-create failed: ${create?.error ?? JSON.stringify(create)}`);
210
+ process.exit(1);
211
+ }
212
+ console.log(` → site comp: ${siteCompositionId}`);
213
+ // Step 2 — delegate to the existing compose-push flow. The push
214
+ // command was extended (this round) to accept `dir` + `composition`
215
+ // overrides so we don't need to chdir or write a .beehaven.json
216
+ // link file first.
217
+ console.log(' [2/3] Building + pushing bundle…');
218
+ const { cmdComposePush } = await import('./compose.js');
219
+ await cmdComposePush({
220
+ ...values,
221
+ dir: absDir,
222
+ composition: siteCompositionId,
223
+ });
224
+ // Step 3 — embed the served URL as a website block on the target.
225
+ // iframe-embed RPC resolves siteCompositionId → relay URL, sandboxed.
226
+ console.log(' [3/3] Embedding on target pasteboard…');
227
+ const embed = await rpcOnce('iframe-embed', {
228
+ siteCompositionId,
229
+ targetCompositionId,
230
+ title,
231
+ }, 30_000);
232
+ if (embed?.error) {
233
+ // Fall back to a direct canvas-invoke that adds a website block.
234
+ // Saves the user from needing to redo step 1 + 2.
235
+ console.error(` iframe-embed failed: ${embed.error}`);
236
+ console.error(' Manual fallback:');
237
+ console.error(` beehaven call composition-detail '{"id":"${siteCompositionId}"}' # gets the served URL`);
238
+ process.exit(1);
239
+ }
240
+ const block = embed?.block ?? embed?.result?.block;
241
+ console.log(` ✓ Published: site comp ${siteCompositionId} embedded as block ${block?.id ?? '?'} on pasteboard ${targetCompositionId}`);
242
+ console.log(` ✓ Open it: https://clearly.sh/c/${targetCompositionId}`);
243
+ return print(embed, values);
244
+ }
245
+ async function cmdCanvasPasteText(positionals, values) {
246
+ // Read text from positional or stdin. Stdin is the natural pipe
247
+ // path for `cat file | beehaven canvas paste-text`; positional is
248
+ // the natural ad-hoc form for `beehaven canvas paste-text "hi"`.
249
+ // Args override stdin if both are supplied so the explicit form
250
+ // wins.
251
+ let text = positionals[0] ?? '';
252
+ if (!text && !process.stdin.isTTY) {
253
+ const chunks = [];
254
+ for await (const chunk of process.stdin)
255
+ chunks.push(chunk);
256
+ text = Buffer.concat(chunks).toString('utf-8');
257
+ }
258
+ text = text.trim();
259
+ if (!text) {
260
+ console.error(' Usage: beehaven canvas paste-text "your text" [--composition <id>]');
261
+ console.error(' or: cat file.md | beehaven canvas paste-text [--composition <id>]');
262
+ process.exit(1);
263
+ }
264
+ const compositionId = values.composition;
265
+ if (!compositionId) {
266
+ console.error(' --composition <id> required (the pasteboard to land the text on).');
267
+ console.error(' List compositions: beehaven sql "SELECT id, title FROM compositions ORDER BY updated_at DESC LIMIT 10"');
268
+ process.exit(1);
269
+ }
270
+ const lines = text.split(/\r?\n/).length;
271
+ // Same placement heuristic as teleport --as text (workspace.ts).
272
+ // Keeps the two paths visually consistent.
273
+ const x = 80, y = 80;
274
+ const width = 720;
275
+ const height = Math.max(120, Math.min(2400, lines * 22 + 60));
276
+ const out = await rpcOnce('canvas-add-text', {
277
+ compositionId,
278
+ text,
279
+ x,
280
+ y,
281
+ width,
282
+ height,
283
+ source: 'paste-text',
284
+ }, 15_000);
285
+ return print(out, values);
286
+ }
287
+ function printHelp() {
288
+ console.log(`Usage:
289
+ beehaven canvas open <noteId>
290
+ beehaven canvas invoke <action> [json-args]
291
+ beehaven canvas query <snapshot-action>
292
+ beehaven canvas actions [--archetype X] [--status X] [--runtime X] [--scope X] [--callable-only]
293
+ beehaven canvas perceive [id] [--format json] [--pixels] [--max N] # read the live "room"
294
+ beehaven canvas status [id] # compact canvas health summary
295
+ beehaven canvas verify [--composition id] [--keep] [--image-url url] # self-test every canvas action
296
+ beehaven canvas prompt "draw a robot" [--composition id] # send a prompt (= bottom-bar prompt tool)
297
+ beehaven canvas goto recents | <id> # navigate the app (router.push) — verify reload
298
+ beehaven canvas new "Title" # create empty pasteboard
299
+ beehaven canvas rename <id> "New title" # rename a pasteboard
300
+ beehaven canvas detail <id> # full pasteboard payload
301
+ beehaven canvas delete <id> # soft-delete a pasteboard
302
+ beehaven canvas export <id> [--format pdf|svg|png|json]
303
+ beehaven canvas paste-text "..." --composition <id> # text → text block (or pipe stdin)
304
+ beehaven canvas help [domain] # workspace RPC catalog (cloud), grouped
305
+ beehaven canvas help --search <q> # keyword search the catalog
306
+ beehaven canvas record start [sessionId]
307
+ beehaven canvas record stop
308
+
309
+ Flags:
310
+ --device <clientId> target a specific connected device
311
+ --client-type <type> restrict to mac-app | web | ios-app
312
+ --note <noteId> stamp noteId on the envelope
313
+ --json output raw JSON
314
+
315
+ Canvas-actions filters:
316
+ --archetype <kind> image | text | link | pdf | media
317
+ --status <state> live | partial | pending | mcp
318
+ --runtime <where> local | worker | do | mcp
319
+ --scope <which> all (default) | block | composition
320
+ --callable-only drop status=pending rows
321
+
322
+ Note: 'canvas actions' lists what the EDITOR CLIENT can do locally
323
+ (open palette, focus block, etc.). 'canvas help' lists what the
324
+ WORKSPACE DO exposes as RPCs (add-text, add-document, time-travel, ...).
325
+
326
+ Run 'beehaven devices' to see connected devices, and 'beehaven devices pin <id>'
327
+ to pin one as the default target.`);
328
+ }
329
+ // ── open ────────────────────────────────────────────────────────────────────
330
+ async function cmdCanvasOpen(positionals, values) {
331
+ const noteId = positionals[0];
332
+ if (!noteId) {
333
+ console.error(' Usage: beehaven canvas open <noteId> [--launch] [--dev]');
334
+ process.exit(1);
335
+ }
336
+ const url = canvasUrl(noteId, values);
337
+ // --launch / --browser → open a REAL browser window (a fresh live client).
338
+ // Use this when nothing is connected (canvas-perceive shows delivered:0).
339
+ if (values.launch || values.browser) {
340
+ const ok = launchInBrowser(url);
341
+ console.log(ok ? ` 🌐 Opening ${url}` : ` Open this URL in a browser: ${url}`);
342
+ try {
343
+ await rpcOnce('ui-open', { window: 'canvas', noteId, title: values.title }, 6_000);
344
+ }
345
+ catch {
346
+ /* no client yet — the new tab will load it */
347
+ }
348
+ return ok ? undefined : process.exit(1);
349
+ }
350
+ const target = resolveTarget(values);
351
+ // `ui-open` is a broadcast — when targeting is required, invoke the
352
+ // `device.open` alias via canvas-invoke so only the chosen device reacts.
353
+ if (target.deviceId || target.clientType) {
354
+ const out = await rpcOnce('canvas-invoke', {
355
+ action: 'composition.open',
356
+ args: { noteId, title: values.title },
357
+ deviceId: target.deviceId,
358
+ clientType: target.clientType,
359
+ noteId,
360
+ }, 15_000);
361
+ return print(out, values);
362
+ }
363
+ const out = await rpcOnce('ui-open', { window: 'canvas', noteId, title: values.title }, 15_000);
364
+ const delivered = out?.delivered ?? out?.result?.delivered;
365
+ if (delivered === 0)
366
+ console.log(` No browser connected. Launch one: beehaven canvas open ${noteId} --launch → ${url}`);
367
+ return print(out, values);
368
+ }
369
+ // ── invoke ──────────────────────────────────────────────────────────────────
370
+ async function cmdCanvasInvoke(positionals, values) {
371
+ const action = positionals[0];
372
+ const argsRaw = positionals[1];
373
+ if (!action) {
374
+ console.error(' Usage: beehaven canvas invoke <action> [json-args]');
375
+ process.exit(1);
376
+ }
377
+ const args = parseJsonOrBail(argsRaw, {});
378
+ const target = resolveTarget(values);
379
+ // awaitResult routes the handler's return value back through
380
+ // _canvasInvokeWaiters in the worker DO (task #81). Default ON for
381
+ // the CLI so `beehaven canvas invoke` actually shows what the
382
+ // action did — agents need that. Opt out with --no-await for the
383
+ // legacy fire-and-forget shape (delivery ack only).
384
+ const wait = values['no-await'] ? false : true;
385
+ const out = await rpcOnce('canvas-invoke', {
386
+ action,
387
+ args,
388
+ deviceId: target.deviceId,
389
+ clientType: target.clientType,
390
+ noteId: values.note,
391
+ awaitResult: wait,
392
+ timeoutMs: wait ? 7500 : undefined,
393
+ }, wait ? 30_000 : 10_000);
394
+ return print(out, values);
395
+ }
396
+ // ── query ───────────────────────────────────────────────────────────────────
397
+ // Alias for invoke on *.snapshot actions. Since the client snapshot actions
398
+ // currently write state back into the composition row via deltas (not a
399
+ // direct reply), `query` is equivalent to `invoke` today. Kept as a separate
400
+ // sub-command so when an awaitResult channel lands, agents can switch sub
401
+ // without rewriting scripts.
402
+ async function cmdCanvasQuery(positionals, values) {
403
+ const action = positionals[0];
404
+ if (!action) {
405
+ console.error(' Usage: beehaven canvas query <snapshot-action>');
406
+ process.exit(1);
407
+ }
408
+ const suggested = action.endsWith('.snapshot') ? action : `${action}.snapshot`;
409
+ return cmdCanvasInvoke([suggested, positionals[1] ?? ''], values);
410
+ }
411
+ // ── actions ─────────────────────────────────────────────────────────────────
412
+ // Asks the client to dump its action registry by invoking a reserved
413
+ // `editor.list-actions` action; the handler is registered by the editor on
414
+ // mount and responds via the sql:delta stream. If the editor isn't open,
415
+ // the call times out — that's the signal agents should `open` first.
416
+ async function cmdCanvasActions(values) {
417
+ const target = resolveTarget(values);
418
+ // Forward filter flags to the registered editor.list-actions handler
419
+ // (registered by FloatingWindowsManager — closes the LLM-on-canvas
420
+ // framework gap so the CLI can discover what verbs are callable
421
+ // without bundling its own copy of the manifest).
422
+ const filterArgs = {};
423
+ if (values.archetype)
424
+ filterArgs.archetype = values.archetype;
425
+ if (values.status)
426
+ filterArgs.status = values.status;
427
+ if (values.runtime)
428
+ filterArgs.runtime = values.runtime;
429
+ if (values['callable-only'])
430
+ filterArgs.callableOnly = true;
431
+ if (values.scope)
432
+ filterArgs.scope = values.scope;
433
+ const out = await rpcOnce('canvas-invoke', {
434
+ action: 'editor.list-actions',
435
+ args: filterArgs,
436
+ deviceId: target.deviceId,
437
+ clientType: target.clientType,
438
+ awaitResult: true,
439
+ timeoutMs: 7500,
440
+ }, 15_000);
441
+ // If the result envelope arrived with a `value` (handler return),
442
+ // render the manifest in tabular form. Otherwise fall through to
443
+ // the legacy ack pretty-printer.
444
+ if (values.json || values.format === 'json') {
445
+ console.log(JSON.stringify(out, null, 2));
446
+ return;
447
+ }
448
+ const v = out?.value ?? out?.result?.value;
449
+ if (v && (v.blocks || v.compositions)) {
450
+ printManifest(v, filterArgs);
451
+ return;
452
+ }
453
+ // editor.list-actions returns { actions: [{ id, label, group, shortcut, description }] }
454
+ if (v && Array.isArray(v.actions)) {
455
+ const acts = v.actions;
456
+ if (values.scope && values.scope !== 'all') {
457
+ /* scope filter is client-side already */
458
+ }
459
+ let lastGroup = '';
460
+ for (const a of acts.sort((x, y) => (x.group || '').localeCompare(y.group || '') || x.id.localeCompare(y.id))) {
461
+ if ((a.group || '') !== lastGroup) {
462
+ lastGroup = a.group || '';
463
+ console.log(`\n ${lastGroup || 'other'}`);
464
+ }
465
+ console.log(` ${a.id}${a.shortcut ? ` (${a.shortcut})` : ''}${a.description ? ` — ${a.description}` : a.label ? ` — ${a.label}` : ''}`);
466
+ }
467
+ console.log(`\n ${acts.length} actions. Call one: beehaven canvas invoke <id> '<json-args>'`);
468
+ return;
469
+ }
470
+ return print(out, values);
471
+ }
472
+ function printManifest(value, filters) {
473
+ const blocks = Array.isArray(value.blocks) ? value.blocks : [];
474
+ const comps = Array.isArray(value.compositions) ? value.compositions : [];
475
+ const filterDesc = Object.entries(filters)
476
+ .filter(([, v]) => v !== undefined && v !== false)
477
+ .map(([k, v]) => `${k}=${v}`)
478
+ .join(' ');
479
+ if (blocks.length) {
480
+ console.log(`\n Block actions${filterDesc ? ` [${filterDesc}]` : ''} — ${blocks.length}\n`);
481
+ for (const a of blocks) {
482
+ const arch = Array.isArray(a.archetypes) ? a.archetypes.join(',') : a.archetypes;
483
+ console.log(` ${String(a.id).padEnd(34)} ${String(a.status).padEnd(8)} ${String(a.runtime).padEnd(7)} ${arch}`);
484
+ }
485
+ }
486
+ if (comps.length) {
487
+ console.log(`\n Composition actions — ${comps.length}\n`);
488
+ for (const a of comps) {
489
+ console.log(` ${String(a.id).padEnd(34)} ${String(a.status).padEnd(8)} ${String(a.runtime).padEnd(7)}`);
490
+ }
491
+ }
492
+ if (!blocks.length && !comps.length) {
493
+ console.log(' (no actions matched)');
494
+ }
495
+ }
496
+ // ── perceive / status / verify (review the canvas + self-test actions) ───────
497
+ // perceive → the read RPC (room: gaze / contents / focus / instruments /
498
+ // prepress). status → a compact health summary built from it.
499
+ // verify → drives every canvas.* action against the live tab on SCRATCH
500
+ // nodes, then deletes everything it created (baseline node diff) and
501
+ // restores print settings. Safe + self-cleaning; never touches
502
+ // pre-existing user content.
503
+ async function cmdCanvasPerceive(positionals, values) {
504
+ const compositionId = positionals[0] || values.composition;
505
+ const wantJson = !!values.json || values.format === 'json';
506
+ const input = { format: wantJson ? 'json' : 'text' };
507
+ if (compositionId)
508
+ input.compositionId = compositionId;
509
+ if (values.pixels)
510
+ input.includePixels = true;
511
+ if (values.max)
512
+ input.maxObjects = Number(values.max);
513
+ const out = await rpcOnce('canvas-perceive', input, 20_000);
514
+ const r = out?.result ?? out;
515
+ if (wantJson) {
516
+ console.log(JSON.stringify(r, null, 2));
517
+ return;
518
+ }
519
+ if (typeof r?.text === 'string') {
520
+ console.log(r.text);
521
+ if (r.live === false)
522
+ console.log('\n (live:false — no open tab; showing persisted scene. Open one: beehaven canvas open <id> --launch)');
523
+ return;
524
+ }
525
+ console.log(JSON.stringify(r, null, 2));
526
+ }
527
+ async function cmdCanvasStatus(positionals, values) {
528
+ const compositionId = positionals[0] || values.composition;
529
+ const input = { format: 'json' };
530
+ if (compositionId)
531
+ input.compositionId = compositionId;
532
+ const out = await rpcOnce('canvas-perceive', input, 20_000);
533
+ const r = out?.result ?? out;
534
+ if (values.json || values.format === 'json') {
535
+ console.log(JSON.stringify(r, null, 2));
536
+ return;
537
+ }
538
+ const delivered = Number(r?.delivered ?? 0);
539
+ const live = !!r?.live;
540
+ const room = r?.room;
541
+ console.log(' Canvas status');
542
+ console.log(` Tab connected : ${delivered > 0 ? `yes · ${delivered} client${delivered === 1 ? '' : 's'}` : 'NO (open one: beehaven canvas open <id> --launch)'}`);
543
+ console.log(` State : ${live ? 'LIVE — reading the open tab' : 'persisted scene (no live tab)'}`);
544
+ if (room) {
545
+ const contents = Array.isArray(room.contents) ? room.contents.length : 0;
546
+ const sel = Array.isArray(room.focus?.selected) ? room.focus.selected.length : 0;
547
+ const tool = room.focus?.tool ?? room.focus?.mode ?? '—';
548
+ console.log(` Composition : ${room.room?.title ?? room.room?.id ?? compositionId ?? '—'}`);
549
+ console.log(` Objects : ${contents}`);
550
+ console.log(` Selection : ${sel}`);
551
+ console.log(` Active tool : ${tool}`);
552
+ if (room.gaze?.zoomPct != null)
553
+ console.log(` Zoom : ${room.gaze.zoomPct}%`);
554
+ const pp = room.prepress;
555
+ if (pp)
556
+ console.log(` Prepress : ${pp.dpi ?? '?'}dpi · bleed ${pp.bleedMm ?? 0}mm · safe ${pp.safeMm ?? 0}mm${pp.cropMarks ? ' · crop-marks' : ''}${pp.mirror ? ' · mirror' : ''}`);
557
+ }
558
+ else if (delivered === 0) {
559
+ console.log(' (no live room — open a tab to inspect gaze / selection / tool)');
560
+ }
561
+ console.log(`\n Review: beehaven canvas perceive${compositionId ? ` ${compositionId}` : ''}`);
562
+ console.log(` Verify: beehaven canvas verify${compositionId ? ` --composition ${compositionId}` : ''}`);
563
+ }
564
+ async function cmdCanvasPrompt(positionals, values) {
565
+ const text = positionals.join(' ').trim() || String(values.text || '').trim();
566
+ if (!text) {
567
+ console.error(' Usage: beehaven canvas prompt "draw a robot" [--composition <id>]');
568
+ process.exit(1);
569
+ }
570
+ const target = resolveTarget(values);
571
+ // The SAME action as the bottom-bar Prompt tool — drives `canvas.prompt` on the
572
+ // live web tab (the composer is a web surface) → submitPromptText → the
573
+ // Librarian. Watch the result: `beehaven canvas perceive` shows "Prompt tool:
574
+ // THINKING (generating…)", then the generated node appears in contents.
575
+ const out = await rpcOnce('canvas-invoke', {
576
+ action: 'canvas.prompt',
577
+ args: { text },
578
+ deviceId: target.deviceId,
579
+ clientType: target.clientType ?? (target.deviceId ? undefined : 'web'),
580
+ noteId: values.composition,
581
+ awaitResult: true,
582
+ timeoutMs: 7500,
583
+ }, 15_000);
584
+ return print(out, values);
585
+ }
586
+ async function cmdCanvasGoto(positionals, values) {
587
+ const dest = (positionals[0] || '').trim();
588
+ if (!dest) {
589
+ console.error(' Usage: beehaven canvas goto recents | <compositionId>');
590
+ process.exit(1);
591
+ }
592
+ const path = dest === 'recents' ? '/recents' : dest.startsWith('/') ? dest : `/c/${dest}`;
593
+ // Drives the app router via ui-navigate (router.push). `goto recents` then
594
+ // `goto <id>` round-trips the composition → a fresh load from PERSISTED data,
595
+ // so you can verify generated artwork survives reload.
596
+ const out = await rpcOnce('ui-navigate', { path }, 15_000);
597
+ if (!(values.json || values.format === 'json'))
598
+ console.log(` → navigated connected client(s) to ${path}`);
599
+ return print(out, values);
600
+ }
601
+ async function cmdCanvasVerify(positionals, values) {
602
+ const compositionId = positionals[0] || values.composition;
603
+ const target = resolveTarget(values);
604
+ const wantJson = !!values.json || values.format === 'json';
605
+ // A MUTATING sweep must hit ONE editor surface, never broadcast to every
606
+ // connected client (mac-app + web + daemon would each run the action →
607
+ // duplicate scratch nodes). The canvas editor is a web surface, so default
608
+ // to clientType:'web' unless an explicit --device / --client-type was given.
609
+ const invTarget = {
610
+ deviceId: target.deviceId,
611
+ clientType: target.clientType ?? (target.deviceId ? undefined : 'web'),
612
+ };
613
+ // Drive one editor action on the live tab; return the normalized envelope
614
+ // { ok, value, error, delivered } (see canvas-rpcs.ts canvas-invoke).
615
+ async function invoke(action, args = {}, timeoutMs = 7500) {
616
+ const out = await rpcOnce('canvas-invoke', {
617
+ action,
618
+ args,
619
+ deviceId: invTarget.deviceId,
620
+ clientType: invTarget.clientType,
621
+ noteId: compositionId,
622
+ awaitResult: true,
623
+ timeoutMs,
624
+ }, timeoutMs + 8000);
625
+ return out?.result ?? out;
626
+ }
627
+ // 1. Require a live tab — list-nodes both proves connectivity and gives the
628
+ // baseline node set used for cleanup.
629
+ const baseEnv = await invoke('canvas.list-nodes');
630
+ if (!baseEnv || baseEnv.ok === false || !baseEnv.value) {
631
+ const reason = baseEnv?.error || 'no-client-connected';
632
+ console.error(` ✗ No live canvas tab (${reason}).`);
633
+ console.error(` Open one: beehaven canvas open ${compositionId || '<id>'} --launch`);
634
+ console.error(' Verify drives the real editor, so a tab must be open.');
635
+ process.exit(1);
636
+ }
637
+ const baselineIds = new Set((baseEnv.value.nodes || []).map((n) => String(n.id)));
638
+ // Capture baseline print settings — restored at the end so set-print is non-destructive.
639
+ let basePrepress = null;
640
+ try {
641
+ const pr = await rpcOnce('canvas-perceive', { format: 'json', compositionId }, 15_000);
642
+ basePrepress = (pr?.result ?? pr)?.room?.prepress ?? null;
643
+ }
644
+ catch {
645
+ /* perceive optional */
646
+ }
647
+ const rows = [];
648
+ const ids = {};
649
+ let failed = 0;
650
+ async function step(action, args, opts = {}) {
651
+ try {
652
+ const env = await invoke(action, args);
653
+ const ok = !!env && env.ok !== false && env.error == null && (!opts.assert || opts.assert(env.value));
654
+ if (ok && opts.capture && env.value?.id)
655
+ ids[opts.capture] = String(env.value.id);
656
+ rows.push({
657
+ action,
658
+ status: ok ? 'PASS' : 'FAIL',
659
+ note: ok ? opts.note : env?.error || 'assertion failed',
660
+ });
661
+ if (!ok)
662
+ failed++;
663
+ }
664
+ catch (e) {
665
+ rows.push({ action, status: 'FAIL', note: e?.message || String(e) });
666
+ failed++;
667
+ }
668
+ }
669
+ const skip = (action, note) => rows.push({ action, status: 'SKIP', note });
670
+ // 2. Sweep — every canvas.* action, on scratch nodes.
671
+ rows.push({
672
+ action: 'canvas.list-nodes',
673
+ status: 'PASS',
674
+ note: `${baselineIds.size} nodes baseline`,
675
+ });
676
+ await step('canvas.perceive', { format: 'text' }, { assert: (v) => v != null });
677
+ await step('canvas.create-node', { type: 'rect', x: 60, y: 60, w: 80, h: 80, fill: '#7CC0F5' }, { capture: 'A', assert: (v) => !!v?.id });
678
+ await step('canvas.create-node', { type: 'ellipse', x: 180, y: 60, w: 80, h: 80, fill: '#F59E0B' }, { capture: 'B', assert: (v) => !!v?.id });
679
+ await step('canvas.create-node', { type: 'text', x: 60, y: 180, text: '__verify__', size: 18 }, { capture: 'C', assert: (v) => !!v?.id });
680
+ if (ids.A)
681
+ await step('canvas.update-nodes', { ids: [ids.A], patch: { x: 100, y: 100, fill: '#22C55E', name: '__verify__A' } }, { assert: (v) => (v?.updated ?? 0) >= 1 });
682
+ if (ids.A && ids.B)
683
+ await step('canvas.set-selection', { ids: [ids.A, ids.B] }, {});
684
+ if (ids.A && ids.B)
685
+ await step('canvas.arrange', { op: 'align-left', ids: [ids.A, ids.B] }, {});
686
+ if (ids.A)
687
+ await step('canvas.order', { op: 'front', ids: [ids.A] }, {});
688
+ if (ids.A)
689
+ await step('canvas.duplicate', { ids: [ids.A] }, {});
690
+ if (ids.A)
691
+ await step('canvas.effects', { op: 'shadow', ids: [ids.A] }, {});
692
+ await step('canvas.set-tool', { mode: 'move' }, {});
693
+ await step('canvas.viewport', { op: 'zoom-in' }, { assert: (v) => v?.zoomPct != null });
694
+ await invoke('canvas.viewport', { op: 'reset' }); // restore zoom (not scored)
695
+ if (ids.B)
696
+ await step('canvas.clipboard', { op: 'copy', ids: [ids.B] }, {});
697
+ await step('canvas.clipboard', { op: 'paste' }, {});
698
+ if (ids.C)
699
+ await step('canvas.component', { op: 'create', ids: [ids.C] }, {});
700
+ await step('canvas.select', { op: 'all' }, {});
701
+ await invoke('canvas.select', { op: 'none' }); // deselect (not scored)
702
+ await step('canvas.snapshot', {}, {});
703
+ await step('canvas.set-print', { dpi: 300, bleedMm: 3 }, {});
704
+ await step('canvas.preflight', {}, { assert: (v) => v != null });
705
+ await step('canvas.add-artboard', { wMm: 76, hMm: 76 }, {});
706
+ await step('canvas.add-document', { x: 320, y: 60, title: '__verify__doc' }, { assert: (v) => !!v?.id });
707
+ await step('canvas.history', { op: 'undo' }, { assert: (v) => v != null });
708
+ await invoke('canvas.history', { op: 'redo' }); // restore (not scored)
709
+ if (values['image-url'])
710
+ await step('canvas.add-image', { src: String(values['image-url']), x: 320, y: 200 }, { assert: (v) => !!v?.id });
711
+ else
712
+ skip('canvas.add-image', 'pass --image-url <url> to test (no scratch asset)');
713
+ skip('canvas.export-bundle', 'triggers a file download in the tab — run manually');
714
+ skip('canvas.add-comment', 'comment pins have no programmatic removal yet');
715
+ // 3. Cleanup — delete every node that did NOT exist before the sweep, restore print.
716
+ let cleaned = 0;
717
+ if (!values.keep) {
718
+ try {
719
+ const afterEnv = await invoke('canvas.list-nodes');
720
+ const afterIds = (afterEnv?.value?.nodes || []).map((n) => String(n.id));
721
+ const scratch = afterIds.filter((id) => !baselineIds.has(id));
722
+ if (scratch.length) {
723
+ const del = await invoke('canvas.delete', { ids: scratch });
724
+ cleaned = del?.value?.deleted ?? scratch.length;
725
+ }
726
+ if (basePrepress)
727
+ await invoke('canvas.set-print', {
728
+ dpi: basePrepress.dpi,
729
+ bleedMm: basePrepress.bleedMm,
730
+ safeMm: basePrepress.safeMm,
731
+ cropMarks: !!basePrepress.cropMarks,
732
+ regMarks: !!basePrepress.regMarks,
733
+ mirror: !!basePrepress.mirror,
734
+ });
735
+ }
736
+ catch (e) {
737
+ rows.push({ action: 'cleanup', status: 'FAIL', note: e?.message || String(e) });
738
+ failed++;
739
+ }
740
+ }
741
+ rows.push({
742
+ action: 'canvas.delete',
743
+ status: cleaned > 0 || values.keep ? 'PASS' : 'SKIP',
744
+ note: values.keep ? '--keep: scratch left in place' : `cleaned ${cleaned} scratch node(s)`,
745
+ });
746
+ // 4. Report.
747
+ if (wantJson) {
748
+ console.log(JSON.stringify({ ok: failed === 0, failed, rows }, null, 2));
749
+ return;
750
+ }
751
+ const pass = rows.filter((r) => r.status === 'PASS').length;
752
+ const skipped = rows.filter((r) => r.status === 'SKIP').length;
753
+ console.log(`\n Canvas action verification — ${compositionId || '(pinned tab)'}\n`);
754
+ for (const r of rows) {
755
+ const mark = r.status === 'PASS' ? '✓' : r.status === 'FAIL' ? '✗' : '·';
756
+ console.log(` ${mark} ${r.action.padEnd(22)} ${r.status}${r.note ? ` — ${r.note}` : ''}`);
757
+ }
758
+ console.log(`\n ${pass} passed · ${failed} failed · ${skipped} skipped${values.keep ? ' · scratch KEPT (--keep)' : ' · scratch cleaned'}`);
759
+ if (failed > 0)
760
+ process.exit(1);
761
+ }
762
+ // ── catalog (#113) ──────────────────────────────────────────────────────────
763
+ // Calls the workspace's `canvas:actions` virtual RPC and pretty-prints
764
+ // the grouped result. Single source of truth for canvas capabilities —
765
+ // agents AND humans use the same lookup. The actual catalog lives in
766
+ // the cloudflare worker at apps/cloudflare/src/agent/canvas-rpc-catalog.ts.
767
+ async function cmdCanvasCatalog(positionals, values) {
768
+ const domain = positionals[0];
769
+ const search = values.search;
770
+ const compact = !!values.compact;
771
+ const wantJson = !!values.json || values.format === 'json';
772
+ const input = {};
773
+ if (wantJson) {
774
+ input.format = 'json';
775
+ if (search)
776
+ input.q = search;
777
+ else if (domain)
778
+ input.domain = domain;
779
+ }
780
+ else {
781
+ input.format = 'markdown';
782
+ input.compact = compact;
783
+ if (search) {
784
+ input.format = 'json';
785
+ input.q = search;
786
+ }
787
+ else if (domain) {
788
+ input.format = 'json';
789
+ input.domain = domain;
790
+ }
791
+ }
792
+ const raw = await rpcOnce('canvas:actions', input, 15_000);
793
+ if (wantJson) {
794
+ console.log(JSON.stringify(raw, null, 2));
795
+ return;
796
+ }
797
+ const result = raw?.result ?? raw;
798
+ if (result?.format === 'markdown' && typeof result.text === 'string') {
799
+ console.log(result.text);
800
+ return;
801
+ }
802
+ // JSON path (search or domain filter) — render to a friendly list.
803
+ if (result?.hits && Array.isArray(result.hits)) {
804
+ console.log(`Matches for "${search}":\n`);
805
+ for (const e of result.hits) {
806
+ console.log(` ${e.id.padEnd(36)} ${e.summary}`);
807
+ }
808
+ return;
809
+ }
810
+ if (result?.entries && Array.isArray(result.entries)) {
811
+ console.log(`${result.domain}:\n`);
812
+ for (const e of result.entries) {
813
+ console.log(` ${e.id.padEnd(36)} ${e.summary}`);
814
+ }
815
+ return;
816
+ }
817
+ console.log(JSON.stringify(raw, null, 2));
818
+ }
819
+ // ── composition lifecycle (#84) ─────────────────────────────────────────────
820
+ // Thin wrappers over the workspace RPCs (composition-create, -patch,
821
+ // -delete, -detail, -export-print-pdf). Composition is the canvas
822
+ // document; these verbs are the agent-friendly shape of that catalog.
823
+ async function cmdCompositionCreate(positionals, values) {
824
+ const title = positionals[0] || values.title || 'Untitled';
825
+ // composition-create accepts { title, data?, projectId?, teamId?, frameWidth?, frameHeight? }
826
+ // — keep the CLI surface minimal; the agent path uses the full RPC.
827
+ const out = await rpcOnce('composition-create', { title }, 30_000);
828
+ return print(out, values);
829
+ }
830
+ async function cmdCompositionRename(positionals, values) {
831
+ const id = positionals[0];
832
+ const title = positionals[1];
833
+ if (!id || !title) {
834
+ console.error(' Usage: beehaven canvas rename <id> "New title"');
835
+ process.exit(1);
836
+ }
837
+ // composition-patch is the canonical rename path — passes `{ id, patch: { title } }`.
838
+ // It also dispatches a canvas-invoke composition.rename behind the
839
+ // scenes so any open editor tab refreshes its header live.
840
+ const out = await rpcOnce('composition-patch', { id, patch: { title } }, 30_000);
841
+ return print(out, values);
842
+ }
843
+ async function cmdCompositionDelete(positionals, values) {
844
+ const id = positionals[0];
845
+ if (!id) {
846
+ console.error(' Usage: beehaven canvas delete <id>');
847
+ process.exit(1);
848
+ }
849
+ if (!values.yes) {
850
+ console.error(' Refusing without --yes. (Soft-delete; recoverable from time capsules.)');
851
+ process.exit(1);
852
+ }
853
+ const out = await rpcOnce('composition-delete', { id }, 30_000);
854
+ return print(out, values);
855
+ }
856
+ async function cmdCompositionDetail(positionals, values) {
857
+ const id = positionals[0];
858
+ if (!id) {
859
+ console.error(' Usage: beehaven canvas detail <id>');
860
+ process.exit(1);
861
+ }
862
+ const out = await rpcOnce('composition-detail', { id }, 30_000);
863
+ return print(out, values);
864
+ }
865
+ async function cmdCompositionExport(positionals, values) {
866
+ const id = positionals[0];
867
+ if (!id) {
868
+ console.error(' Usage: beehaven canvas export <id> [--format pdf|svg|png|json]');
869
+ process.exit(1);
870
+ }
871
+ const format = values.format || 'pdf';
872
+ // Only PDF has a server-side worker today (composition-export-print-pdf).
873
+ // svg / png / json are local-only — they require an open browser tab
874
+ // running the export pipeline. Route them through canvas-invoke
875
+ // composition.export so the editor tab handles it; PDF goes direct.
876
+ if (format === 'pdf') {
877
+ const out = await rpcOnce('composition-export-print-pdf', { id }, 60_000);
878
+ return print(out, values);
879
+ }
880
+ const target = resolveTarget(values);
881
+ const out = await rpcOnce('canvas-invoke', {
882
+ action: 'composition.export',
883
+ args: { compositionId: id, format },
884
+ deviceId: target.deviceId,
885
+ clientType: target.clientType,
886
+ noteId: id,
887
+ }, 60_000);
888
+ return print(out, values);
889
+ }
890
+ // ── record ──────────────────────────────────────────────────────────────────
891
+ async function cmdCanvasRecord(positionals, values) {
892
+ const verb = positionals[0];
893
+ const target = resolveTarget(values);
894
+ if (verb === 'start') {
895
+ const sessionId = positionals[1];
896
+ const out = await rpcOnce('device-record-start', {
897
+ sessionId,
898
+ deviceId: target.deviceId,
899
+ clientType: target.clientType,
900
+ }, 15_000);
901
+ return print(out, values);
902
+ }
903
+ if (verb === 'stop') {
904
+ const out = await rpcOnce('device-record-stop', {
905
+ deviceId: target.deviceId,
906
+ clientType: target.clientType,
907
+ }, 15_000);
908
+ return print(out, values);
909
+ }
910
+ console.error(' Usage: beehaven canvas record start|stop');
911
+ process.exit(1);
912
+ }
913
+ // ── output ──────────────────────────────────────────────────────────────────
914
+ function print(value, values) {
915
+ if (values.json || values.format === 'json') {
916
+ console.log(JSON.stringify(value, null, 2));
917
+ return;
918
+ }
919
+ if (!value || typeof value !== 'object') {
920
+ console.log(value);
921
+ return;
922
+ }
923
+ const v = value;
924
+ if ('delivered' in v && 'ok' in v) {
925
+ const ok = v.ok ? 'ok' : 'no-target';
926
+ console.log(` → ${ok} · delivered to ${v.delivered} device${v.delivered === 1 ? '' : 's'}${v.action ? ` · ${v.action}` : ''}`);
927
+ if (!v.ok)
928
+ console.log(` Hint: run 'beehaven devices' to verify a session is connected.`);
929
+ return;
930
+ }
931
+ console.log(JSON.stringify(value, null, 2));
932
+ }