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.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- 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
|
+
}
|