beehaven 0.8.18 → 0.8.19

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.
@@ -53,9 +53,9 @@ export const PUBLIC_BUILD = true;
53
53
  * ⚠ REWRITTEN IN `dist/` AT BUILD TIME, like `PUBLIC_BUILD`. `unstamped` is what a source checkout
54
54
  * carries and what `tsx`/`vitest` see, and it is honest: nothing has stamped them.
55
55
  */
56
- export const BUILD_SHA = 'cea58c529fe1';
56
+ export const BUILD_SHA = '42cf394385d8';
57
57
  /** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
58
- export const BUILD_TIME = '2026-09-27T07:47Z';
58
+ export const BUILD_TIME = '2026-09-27T18:08Z';
59
59
  /**
60
60
  * The build identity as one short string, for `--version`.
61
61
  *
package/dist/cli.js CHANGED
@@ -1598,6 +1598,8 @@ async function run() {
1598
1598
  svg: { type: 'string' },
1599
1599
  x: { type: 'string' },
1600
1600
  y: { type: 'string' },
1601
+ width: { type: 'string' },
1602
+ height: { type: 'string' },
1601
1603
  w: { type: 'string' },
1602
1604
  h: { type: 'string' },
1603
1605
  // `as` is also used by teleport (text | attachment |
@@ -0,0 +1,101 @@
1
+ /** Upload a local raster and place it on a composition without sending base64 over the relay. */
2
+ import { readFileSync, statSync, existsSync } from 'node:fs';
3
+ import { basename, extname, join, resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { rpcOnce } from '../ipc-client.js';
7
+ const IMAGE_MIME = {
8
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
9
+ '.webp': 'image/webp', '.gif': 'image/gif', '.avif': 'image/avif',
10
+ };
11
+ const MAX_BYTES = 100 * 1024 * 1024;
12
+ function positiveFlag(raw, label) {
13
+ if (raw === undefined)
14
+ return undefined;
15
+ const value = Number(raw);
16
+ if (!Number.isFinite(value) || value <= 0)
17
+ throw new Error(`${label} must be a positive number`);
18
+ return value;
19
+ }
20
+ function coordinateFlag(raw, label) {
21
+ if (raw === undefined)
22
+ return undefined;
23
+ const value = Number(raw);
24
+ if (!Number.isFinite(value))
25
+ throw new Error(`${label} must be a number`);
26
+ return value;
27
+ }
28
+ function absoluteRelayUrl(url) {
29
+ if (!url.startsWith('/'))
30
+ return url;
31
+ const configPath = join(homedir(), '.beehaven', 'config.json');
32
+ let endpoint = 'wss://relay.clearly.sh';
33
+ if (existsSync(configPath)) {
34
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
35
+ if (config.endpoint)
36
+ endpoint = String(config.endpoint);
37
+ }
38
+ return endpoint.replace(/^wss:/, 'https:').replace(/^ws:/, 'http:').replace(/\/$/, '') + url;
39
+ }
40
+ export async function cmdCanvasAddImage(positionals, values) {
41
+ const input = positionals[0];
42
+ const compositionId = String(values.composition ?? values.comp ?? values.c ?? '').trim();
43
+ if (!input || !compositionId) {
44
+ throw new Error('Usage: beehaven canvas add-image <local.png|jpg|webp|gif|avif> --composition <id> [--x N --y N --width N --height N]');
45
+ }
46
+ const path = resolve(input);
47
+ const mimeType = IMAGE_MIME[extname(path).toLowerCase()];
48
+ if (!mimeType)
49
+ throw new Error('Supported image formats: PNG, JPEG, WebP, GIF, AVIF');
50
+ const stat = statSync(path);
51
+ if (!stat.isFile())
52
+ throw new Error(`Not a file: ${path}`);
53
+ if (!stat.size || stat.size > MAX_BYTES)
54
+ throw new Error(`Image must be 1 byte to 100 MB (${stat.size} bytes given)`);
55
+ const x = coordinateFlag(values.x, '--x');
56
+ const y = coordinateFlag(values.y, '--y');
57
+ const width = positiveFlag(values.width, '--width');
58
+ const height = positiveFlag(values.height, '--height');
59
+ if ((x === undefined) !== (y === undefined))
60
+ throw new Error('Pass --x and --y together');
61
+ if ((width === undefined) !== (height === undefined))
62
+ throw new Error('Pass --width and --height together');
63
+ // Refuse an unknown target before putting bytes in storage for a canvas that cannot use them.
64
+ const target = await rpcOnce('composition-detail', { id: compositionId }, 30_000);
65
+ if (target?.error || target?.composition?.id !== compositionId) {
66
+ throw new Error(`Canvas not found or not accessible: ${compositionId}`);
67
+ }
68
+ const name = basename(path);
69
+ const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 64);
70
+ const key = `compositions/${compositionId}/images/${randomUUID()}-${safeName}`;
71
+ const intent = await rpcOnce('get-upload-url', {
72
+ key, contentType: mimeType, maxSize: stat.size, size: stat.size,
73
+ }, 30_000);
74
+ if (intent?.error || !intent?.uploadUrl || !intent?.publicUrl) {
75
+ throw new Error(`Could not prepare image upload: ${intent?.error ?? 'missing upload or image URL'}`);
76
+ }
77
+ const uploaded = await fetch(absoluteRelayUrl(String(intent.uploadUrl)), {
78
+ method: 'PUT', headers: { 'Content-Type': mimeType }, body: readFileSync(path),
79
+ });
80
+ if (!uploaded.ok)
81
+ throw new Error(`Image upload failed: HTTP ${uploaded.status}`);
82
+ const recorded = await rpcOnce('file-uploaded', {
83
+ key, name, mimeType, size: stat.size, composition: true,
84
+ }, 30_000);
85
+ if (recorded?.ok === false || recorded?.error) {
86
+ throw new Error(`Image uploaded but could not be registered: ${recorded?.error ?? 'unknown error'}`);
87
+ }
88
+ const placed = await rpcOnce('canvas-add-image', {
89
+ compositionId, imageUrl: absoluteRelayUrl(String(intent.publicUrl)),
90
+ ...(x !== undefined ? { x, y } : {}),
91
+ ...(width !== undefined ? { width, height } : {}),
92
+ }, 30_000);
93
+ if (placed?.ok !== true || !placed?.nodeId && !placed?.blockId) {
94
+ throw new Error(`Image uploaded but could not be placed: ${placed?.error ?? 'unknown error'}. Asset key: ${key}`);
95
+ }
96
+ const result = { ok: true, compositionId, nodeId: placed.nodeId ?? placed.blockId, imageFileId: recorded?.id, name };
97
+ if (values.json)
98
+ console.log(JSON.stringify(result, null, 2));
99
+ else
100
+ console.log(` Added ${name} to canvas ${compositionId} (node ${result.nodeId})`);
101
+ }
@@ -155,6 +155,10 @@ export async function cmdCanvas(sub, positionals, values) {
155
155
  case 'paste-text':
156
156
  case 'text':
157
157
  return cmdCanvasPasteText(positionals, values);
158
+ case 'add-image': {
159
+ const { cmdCanvasAddImage } = await import('./canvas-image.js');
160
+ return cmdCanvasAddImage(positionals, values);
161
+ }
158
162
  // ── publish-landing — built site dir → site comp → embed block ─
159
163
  // `beehaven canvas publish-landing ./dist --composition <id>`
160
164
  // composes compose-create + compose-push + canvas-invoke into one
@@ -301,6 +305,7 @@ function printHelp() {
301
305
  beehaven canvas delete <id> # soft-delete a pasteboard
302
306
  beehaven canvas export <id> [--format pdf|svg|png|json]
303
307
  beehaven canvas paste-text "..." --composition <id> # text → text block (or pipe stdin)
308
+ beehaven canvas add-image ./photo.png --composition <id> [--x N --y N --width N --height N]
304
309
  beehaven canvas help [domain] # workspace RPC catalog (cloud), grouped
305
310
  beehaven canvas help --search <q> # keyword search the catalog
306
311
  beehaven canvas record start [sessionId]
@@ -535,13 +535,15 @@ const PAGES = {
535
535
  related: ['cd', 'status', 'actions', 'workspaces'],
536
536
  },
537
537
  canvas: {
538
- synopsis: 'beehaven canvas <open|invoke|query|actions|help|record> [args]',
538
+ synopsis: 'beehaven canvas <open|add-image|invoke|query|actions|help|record> [args]',
539
539
  description: `Interact with the FAB canvas (notes editor) from the CLI.
540
540
 
541
541
  Subcommands:
542
542
  open <noteId> Open a note in the canvas editor
543
543
  invoke <action> [json] Call an editor-client action (local UI ops)
544
544
  query <action> Read editor-client state
545
+ add-image <file> Upload a local image and place it on a composition
546
+ (--composition <id>, optional x/y/width/height)
545
547
  actions / list List editor-client UI actions (LOCAL — what the
546
548
  canvas UI can do)
547
549
  help [domain] Show the workspace RPC catalog (CLOUD — what
@@ -563,6 +565,7 @@ const PAGES = {
563
565
  { cmd: 'beehaven canvas actions', note: 'editor UI actions (local)' },
564
566
  { cmd: 'beehaven canvas help', note: 'workspace RPC catalog (cloud)' },
565
567
  { cmd: 'beehaven canvas help --search "image"', note: 'find image-related RPCs' },
568
+ { cmd: 'beehaven canvas add-image ./photo.png --composition <id>', note: 'upload a local image and place it on the canvas' },
566
569
  { cmd: 'beehaven canvas invoke insert-block \'{"type":"text","text":"hi"}\'' },
567
570
  ],
568
571
  related: ['conversations', 'call', 'actions'],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "beehaven",
3
- "version": "0.8.18",
4
- "description": "Beehaven CLI \u2014 command your Clearly workspace, agents, and canvas from the terminal",
3
+ "version": "0.8.19",
4
+ "description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "bin": {
@@ -142,7 +142,10 @@ matches the chosen direction, then adapt and inspect it. It is not an art direct
142
142
  a quality guarantee. Custom compositions should use native nodes and your own builder.
143
143
 
144
144
  Read [authoring.md](authoring.md) before building. It covers the exact coordinate,
145
- batch, text, asset and MCP contracts. Consult `canvas-catalog` for action schemas
145
+ batch, text, image-upload and MCP contracts. For a local image, use
146
+ `beehaven canvas add-image <file> --composition <id>` when the installed CLI supports
147
+ it; read authoring.md for the current CLI fallback and the MCP URL-only path.
148
+ Consult `canvas-catalog` for action schemas
146
149
  instead of loading an exhaustive reference.
147
150
 
148
151
  ## 4. Build a representative surface, then complete the artifact
@@ -105,11 +105,62 @@ Use `canvas.place {kind,id,x,y,w,h,name,parentId?}` to put a workspace object (d
105
105
  deck, sheet, board, ticket, project, site or file) on the canvas as a live card. With
106
106
  `parentId`, `x/y` are relative to that frame, like `canvas.create-node`. See
107
107
  [project-plans.md](project-plans.md).
108
- Use `canvas.list-sources`/`canvas.place-source` for library assets. A native image
109
- node accepts `src`. Upload local media using `canvas-upload-image` (discover its current
110
- schema), then place its returned URL. Inspect the actual asset before using it. Prefer
111
- provided, reusable or purpose-made imagery to random URL placeholders. Label temporary
112
- assets in the handoff and replace them when final assets are part of the request.
108
+ Use `canvas.list-sources`/`canvas.place-source` for an existing library asset. A native
109
+ image node accepts an absolute `http(s)` `src`, never a local path or raw base64.
110
+ Inspect the actual asset before placing it. Prefer provided, reusable or purpose-made
111
+ imagery to random URL placeholders. Label temporary assets in the handoff.
112
+
113
+ ### Local images from the CLI
114
+
115
+ `beehaven canvas add-image` uploads a PNG, JPEG, WebP, GIF or AVIF from disk through
116
+ `get-upload-url` → binary PUT → `file-uploaded {composition:true}`, then appends an
117
+ image node with `canvas-add-image`. The binary does not cross the relay WebSocket.
118
+ This matters above about 900 KB: sending multi-megabyte base64 through
119
+ `canvas-upload-image` can exceed the relay message limit. The asset is filed under
120
+ the composition rather than added as a loose item in Recents.
121
+
122
+ ```sh
123
+ beehaven canvas add-image ./photo.png --composition <id> \
124
+ --x 120 --y 160 --width 600 --height 400 --json
125
+ ```
126
+
127
+ The command reports `nodeId` and `imageFileId`. Coordinates are world coordinates;
128
+ omit both `--x`/`--y` to use canvas auto-placement, and omit both size flags to use
129
+ the image's native dimensions. Check `beehaven canvas` help on the installed CLI:
130
+ older versions do not have this command. On one of those, use the documented
131
+ `get-upload-url` → binary PUT → `file-uploaded` sequence, then
132
+ `canvas-act {action:"canvas.create-node",args:{type:"image",src:<absolute URL>,x,y,w,h}}`.
133
+ The upload intent's `publicUrl` may be `/f/...`; prefix the active relay's HTTPS
134
+ origin before storing it as `src`. `beehaven teleport --composition` stores a file
135
+ but does not place an image node. `canvas-upload-image` remains useful for small
136
+ inline images, followed by `canvas.create-node` or `canvas-add-image` using its `url`.
137
+ The CLI command caps a file at 100 MiB, and the workspace plan may impose a smaller
138
+ single-file or remaining-storage limit. Uploading and rendering are separate checks:
139
+ the headless renderer has image-count and memory budgets, so inspect a review PNG
140
+ after adding large images instead of treating upload success as visual proof.
141
+ In the editor, paste/drop also uses presigned binary PUT. Saved canvas images are
142
+ registered under the composition without adding loose cards to Recents. If a large
143
+ upload fails, retry the upload; it must not be inlined into the scene JSON.
144
+
145
+ For a composed artboard, nest the image with `canvas.update-nodes` using the returned
146
+ `nodeId` and `patch:{parentId:<frame id>,name:"descriptive layer name"}`. The
147
+ membership-only patch keeps its world position. Review the artboard after this:
148
+ reviewing a frame excludes overlapping top-level images that are not its children.
149
+ Image nodes are square-edged by default. If you set `radius` for a rounded crop,
150
+ omit a square backing tile behind it or give that tile the same radius; otherwise
151
+ the backing shows through as dark wedges at the corners.
152
+
153
+ ### Images from MCP
154
+
155
+ The seven artifact MCP tools do not upload local binary files or dispatch `canvas-act`.
156
+ For an image already reachable by an absolute URL, read the composition with
157
+ `clearly_read {type:"composition",target:<id>}`; append an image node
158
+ `{id,type:"image",src,x,y,w,h,fit:"contain",parentId?}` to the returned complete node
159
+ array; then call `clearly_write {type:"composition",target:<id>,content:<JSON array>,
160
+ expectedRev:<canvasNodesRev>}`. Preserve every existing node. A stale revision is a
161
+ merge prompt, never a reason to drop `expectedRev`. `clearly_edit` can change an
162
+ existing image's `src`, `fit` or geometry, but cannot create a node. For a local file,
163
+ use the CLI upload path first; an MCP-only client needs a URL it can already access.
113
164
 
114
165
  Current headless rendering can resolve images, including workspace media. A blank image
115
166
  can indicate fetch, decoding or budget failure; check its source and inspect a live view.
package/skills/index.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "project-plans.md",
18
18
  "review.mjs"
19
19
  ],
20
- "sha": "fe0256c5ce9dea7c"
20
+ "sha": "21faa5f8d14beefe"
21
21
  },
22
22
  {
23
23
  "name": "agent-home",