@clearlysh/beehaven 0.8.36 → 0.8.38

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/dist/relay.js CHANGED
@@ -518,7 +518,16 @@ export class Relay {
518
518
  expiredNoRefresh = true;
519
519
  }
520
520
  }
521
- catch { }
521
+ catch {
522
+ // A damaged cached ID token has no decodable exp, but a valid refresh session can still
523
+ // recover it. Do not strand that account in an unauthenticated reconnect loop. Opaque
524
+ // self-hosted tokens without a refresh session retain their existing dial behavior.
525
+ if (this.config.refreshToken) {
526
+ const refreshed = await this.refreshIdToken();
527
+ if (refreshed && this.config)
528
+ token = this.config.token;
529
+ }
530
+ }
522
531
  // If the token is already expired AND refresh failed, dialing would just
523
532
  // get rejected — spinning the reconnect loop forever with a dead
524
533
  // credential (the classic "daemon stuck reconnecting" state). Back off
@@ -777,10 +786,14 @@ export class Relay {
777
786
  this.heartbeatInterval = null;
778
787
  this.wsPingInterval = null;
779
788
  this.wsWakeDetector = null;
780
- // Reject pending requests
781
- for (const [, pending] of this.pendingRequests) {
789
+ // Reject pending requests.
790
+ // ⚠ CAN2-175 — SAY THE OUTCOME IS UNKNOWN, as the timeout does. A dropped socket loses the REPLY,
791
+ // not the request: a canvas batch carries on and lands on the workspace. A bare "WebSocket closed"
792
+ // read as "nothing happened", the caller resent at once, and could not tell which state was real.
793
+ for (const [requestId, pending] of this.pendingRequests) {
782
794
  clearTimeout(pending.timer);
783
- pending.reject(new Error('WebSocket closed'));
795
+ pending.reject(new Error(`WebSocket closed (code ${code ?? '?'}) before the reply (action=${pending.action ?? '?'}, requestId=${requestId}); outcome unknown: `
796
+ + 'the request may still be running on the workspace or may already have landed. Read its state before resending.'));
784
797
  }
785
798
  this.pendingRequests.clear();
786
799
  this.backoff.closed(openedAt);
@@ -1098,12 +1111,12 @@ export class Relay {
1098
1111
  return reject(new Error('WebSocket not connected'));
1099
1112
  }
1100
1113
  const requestId = Math.random().toString(36).slice(2) + Date.now().toString(36);
1114
+ const action = msg.type === 'agent-invoke' ? msg.action : msg.type;
1101
1115
  const timer = setTimeout(() => {
1102
1116
  this.pendingRequests.delete(requestId);
1103
- const action = msg.type === 'agent-invoke' ? msg.action : msg.type;
1104
1117
  reject(new Error(`Request timeout after ${timeout}ms (action=${action}, requestId=${requestId}); outcome unknown`));
1105
1118
  }, timeout);
1106
- this.pendingRequests.set(requestId, { resolve, reject, timer });
1119
+ this.pendingRequests.set(requestId, { resolve, reject, timer, action });
1107
1120
  // Send both rid (legacy) and requestId (workspace actions) for compatibility
1108
1121
  if (!this.wsSend({ ...msg, rid: requestId, requestId })) {
1109
1122
  clearTimeout(timer);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * utf8-stream — decode a socket's bytes ONCE PER CHARACTER, not once per chunk (CA-63).
3
+ *
4
+ * ⚠⚠ WHY THIS EXISTS (staging, 2026-10-23). A 402-step `beehaven call canvas-act @build.json` (90,714
5
+ * bytes, the file holding the correct C2 B7) stored ONE text node's middle dot as "��" — the
6
+ * same node, the same character, on every resend — while dozens of identical dots elsewhere in the batch
7
+ * were stored intact. Every IPC reader did `buffer += chunk.toString()`. A local socket hands the reader
8
+ * the stream in pieces (8 KB on macOS, 64 KB on Linux), so any multi-byte character that straddles a
9
+ * piece boundary was decoded as two halves, each half an invalid sequence, each a U+FFFD. Which character
10
+ * lands on a boundary depends only on the bytes before it, so the same payload broke the same character
11
+ * every time. The CLI → daemon direction corrupted what was STORED; the daemon → CLI direction corrupted
12
+ * what a reader was SHOWN (a long scene read could carry U+FFFD the canvas does not have).
13
+ *
14
+ * A `StringDecoder` holds an incomplete trailing sequence until its remaining bytes arrive, so the text is
15
+ * exactly what one decode of the whole stream would give. One decoder per stream (it carries state), and
16
+ * never shared between two sockets.
17
+ */
18
+ import { StringDecoder } from 'node:string_decoder';
19
+ /** A stateful UTF-8 decoder for ONE byte stream. A string chunk (a socket with an encoding set) passes through. */
20
+ export function utf8Stream() {
21
+ const decoder = new StringDecoder('utf8');
22
+ return (chunk) => (typeof chunk === 'string' ? chunk : decoder.write(chunk));
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clearlysh/beehaven",
3
- "version": "0.8.36",
3
+ "version": "0.8.38",
4
4
  "description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
5
5
  "private": false,
6
6
  "type": "module",
@@ -32,7 +32,7 @@ For a new composition or substantial redesign, use the workflow below.
32
32
 
33
33
  Keep the requested scope. A landing concept with a logo needs its page layouts and a usable
34
34
  logo; add application boards or a full guideline system only when the brief calls for them.
35
- Read only relevant references. Save large tool responses once and print compact summaries.
35
+ Read only relevant references. Save large tool responses once (`beehaven call … --out file.json`) and print compact summaries.
36
36
 
37
37
  ## The studio bar — what "done" means, every time
38
38
 
@@ -46,7 +46,8 @@ A composition is done when every requested artboard meets all five conditions:
46
46
  remains: text colliding or covered, contrast under WCAG (4.5:1 body, 3:1 display),
47
47
  type below the artboard's legible floor, missing media, text overflowing its box,
48
48
  placeholder copy. Fix it. For an intentional finding, use
49
- `accept:[{check,ids:[node IDs],reason}]`. Accepted findings stay visible and return `look`.
49
+ `accept:[{check,ids:[node IDs],reason}]`; one rule may name every layer with the same
50
+ deliberate finding. Accepted findings stay visible and return `look`.
50
51
  Legacy `ignore` can suppress multiple checks and is not an unconditional pass.
51
52
  2. **Every `look` finding was looked at**: type-size and palette sprawl, near-miss
52
53
  alignment, copy against the trim, text over imagery. Act on it or say why not.
@@ -117,8 +118,9 @@ Read `clearly-agent` before your first workspace call. Pin the intended workspac
117
118
  sign in once. Do not switch a user's explicit workspace to `home`.
118
119
 
119
120
  Your surface: the shell. The `beehaven` examples in this skill are literal commands to
120
- run; they are not shapes to translate into an MCP canvas-action tool. MCP artifact tools
121
- can read and edit artifact records, while canvas-specific actions use the CLI.
121
+ run; they are not shapes to translate into an MCP canvas-action tool. The Clearly connector
122
+ has its own canvas tools for agents without a shell (batched per kind of step, see
123
+ authoring.md); in a shell the CLI's mixed `canvas-act` batch takes fewer calls.
122
124
 
123
125
  ```sh
124
126
  beehaven env
@@ -156,7 +158,7 @@ builds on the last and can be reviewed alone. Build ONE commit per canvas-act ba
156
158
  `canvas.review` of the artboard and carries
157
159
  `"ticket":{"id":"<key>","status":"done","resolution":"<what changed>"}` — it lands only if that
158
160
  review is clean or look; `result.ticket` says whether it did, and the ticket logs the canvas
159
- revision and the layers the commit changed. Auto-layout, gradients, shadows, drawing and placeholders:
161
+ revision, the layers the commit changed and a link to its frame. Auto-layout, gradients, shadows, drawing and placeholders:
160
162
  see "Technique" in [authoring.md](authoring.md).
161
163
 
162
164
  `composition-board-get {compositionId}` reads the plan back. `design-start` with a project, ticket or
@@ -279,7 +281,7 @@ a detail view catches collisions, wrapping, cropping and legibility.
279
281
  The bundled helper saves pixels directly to disk so base64 does not fill your context:
280
282
 
281
283
  ```sh
282
- node <skill-directory>/review.mjs <composition-id> <frame-id-or-unique-name> /tmp/canvas-review/01
284
+ node <skill-directory>/review.mjs <composition-id> <frame-id-or-unique-name> canvas-review/01
283
285
  # Open the returned PNG using your image-viewing tool; read the JSON findings.
284
286
  ```
285
287
 
@@ -300,9 +302,15 @@ The helper prints the verdict and every finding. Work until the verdict is `clea
300
302
  use a real frame ID with `--perceive` for a preview-only check and run the text measurement
301
303
  and audit separately. A failed or missing render means visual verification is incomplete.
302
304
 
305
+ A deliberate finding (say why in the handoff): write the findings you are keeping to a file and
306
+ pass it to the same helper. They stay visible in the report and return `look`.
307
+
308
+ ```json
309
+ [{"check":"text-over-image","ids":["<text-id>","<image-id>"],"reason":"Intentional type over this photograph; rendered contrast was inspected."}]
310
+ ```
311
+
303
312
  ```sh
304
- # A deliberate finding — say why in the handoff:
305
- beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame>"],"accept":[{"check":"text-over-image","ids":["<text-id>","<image-id>"],"reason":"Intentional type over this photograph; rendered contrast was inspected."}]}}'
313
+ node <skill-directory>/review.mjs <composition-id> <frame> canvas-review/02 --accept accepted.json
306
314
  ```
307
315
 
308
316
  Asset storage is a reference-aware pipeline: an SVG may have `svgRef` instead of inline markup.
@@ -38,15 +38,34 @@ add('canvas.update-nodes', { ids: ['opening-title', 'opening-deck'], patch: { la
38
38
  add('canvas.auto-layout', { id: 'hero-copy', direction: 'v', gap: 24, sizingH: 'fixed', sizingV: 'hug' });
39
39
  add('canvas.auto-layout', { id: '01-opening', direction: 'v', gap: 64, padding: grid.margin, sizingH: 'fixed', sizingV: 'hug' });
40
40
  // Continue with the brief's central visual, supporting evidence and footer.
41
- writeFileSync('/tmp/canvas-build.json', JSON.stringify({ compositionId, batch }));
41
+ writeFileSync('canvas-build.json', JSON.stringify({ compositionId, batch }));
42
42
  ```
43
43
 
44
44
  ```sh
45
45
  beehaven call composition-create '{"title":"Project — concept","projectId":"<project-id>"}'
46
46
  node build.mjs <returned-id>
47
- beehaven call canvas-act @/tmp/canvas-build.json > /tmp/canvas-build-result.json
47
+ beehaven call canvas-act @canvas-build.json --brief
48
48
  ```
49
49
 
50
+ `--brief` prints a short receipt instead of the full JSON:
51
+ - step counts by kind;
52
+ - layers added, changed and removed;
53
+ - every failed step with its error;
54
+ - rollbacks and skips;
55
+ - lint to fix and review verdicts.
56
+
57
+ The exit code stays the same. Use it for routine batches instead of piping the JSON through a
58
+ summariser of your own. Drop it when you need a step's full value.
59
+
60
+ Keep builders, payloads and results in your **working directory**:
61
+ - Write files with your file-writing tool, not shell heredocs or `>` redirects into `/tmp`.
62
+ - Run each `beehaven` command on its own rather than chained with `&&`, `;`, `| tee` or `| head`.
63
+ - Save a large result with `beehaven call <action> '…' --out scene.json` (add `--brief` for the
64
+ receipt), not `> file`: the whole JSON goes to the file and only a line reaches the console.
65
+
66
+ Agent hosts that gate shell commands (Claude Code, Codex) refuse heredocs containing JSON and
67
+ compound pipelines outright. One recorded run lost four tool calls that way (MCP2-20).
68
+
50
69
  Check `ok`, each `results[].ok`, `count`, `skipped` and `rolledBack`. A failed headless
51
70
  batch may roll back its scene writes. Re-read the scene before deciding what to retry;
52
71
  do not blindly resend only skipped operations. Mixed live/headless actions may have
@@ -155,6 +174,7 @@ The same rules the in-app assistant works to (canvas-catalog returns them as `te
155
174
  - COMPUTED GEOMETRY: shapes from math (radial petals, spirals, grids) are computed and drawn in ONE call: canvas-act { compositionId, script } runs JavaScript with geo.petal/circle/arc/star/polygon/spiral/smooth/rotate/radial/grid/bounds (degrees, 0 = 3 o'clock, clockwise) and a read-only scene list, then applies the { action, args } steps it returns. Never copy numbers from one tool result into another call. A new top-level artboard goes beside or below what scene already holds (or leave out both x and y), never at 0,0 over an existing one.
156
175
  - SHEETS: several concepts or variants go on canvas.sheet { name, title, subtitle, cards or count, cols, cardW, cardH }: a live wrap grid of card frames that own their captions, so rows never overlap. Draw into a card with parentId = its id.
157
176
  - MEASURE: answer spacing, overlap and alignment questions with canvas.measure { frame } or { ids } and quote its numbers, never from memory or a preview.
177
+ - REFERENCES: canvas.link {id:<exact frame/item id>} returns a stable URL and Markdown for tickets, documents, comments and inbox refs. Use the returned link instead of a screenshot or a guessed canvas URL. It preserves the parent canvas permissions; rename/move retains the layer id.
158
178
  - PLACEHOLDERS: until real imagery exists, draw a native placeholder at the final size in a muted brand tone, never a flat accent block: a rect with the slot radius plus a simple motif (an ellipse sun, a vector.create mountain), grouped with canvas.group and named "<section> image placeholder", with any label in its own text node.
159
179
  - SECTIONS: one frame per page section, named exactly as its ticket says, inside the page artboard. Build a section in one canvas-act batch that ends with canvas.review of the artboard and carries ticket: { id: "<its key>", status: "done", resolution: "<what changed>" }. The ticket moves only when that review is clean or look; result.ticket says whether it did. Then start the next section.
160
180
 
@@ -215,15 +235,13 @@ the backing shows through as dark wedges at the corners.
215
235
 
216
236
  ### Images from MCP
217
237
 
218
- The artifact MCP tools do not upload local binary files or dispatch `canvas-act`.
219
- For an image already reachable by an absolute URL, read the composition with
220
- `clearly_read {type:"composition",target:<id>}`; append an image node
221
- `{id,type:"image",src,x,y,w,h,fit:"contain",parentId?}` to the returned complete node
222
- array; then call `clearly_replace {type:"composition",target:<id>,content:<JSON array>,
223
- expectedRev:<canvasNodesRev>}`. Preserve every existing node. A stale revision is a
224
- merge prompt, never a reason to drop `expectedRev`. `clearly_edit` can change an
225
- existing image's `src`, `fit` or geometry, but cannot create a node. For a local file,
226
- use the CLI upload path first; an MCP-only client needs a URL it can already access.
238
+ A connector-only agent places an image already reachable by an absolute URL (or a
239
+ `data:` URI) as an `image` node in `clearly_canvas_add_nodes`:
240
+ `{id,type:"image",name,src,x,y,w,h,fit:"contain",parentId?}`. Use PNG or JPEG: the headless
241
+ review cannot draw WebP yet and does not say so (CAN2-188), so a WebP image looks blank in the
242
+ review PNG while the editor shows it. `clearly_canvas_update_nodes`
243
+ changes an existing image's `src`, `fit` or geometry. For a local file in a shell, use the
244
+ CLI upload path first.
227
245
 
228
246
  Current headless rendering can resolve images, including workspace media. A blank image
229
247
  can indicate fetch, decoding or budget failure; check its source and inspect a live view.
@@ -236,8 +254,8 @@ design. Verify the returned artifact in a browser at realistic viewport sizes.
236
254
  ## Review and scoped perception
237
255
 
238
256
  ```sh
239
- beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame-id>"]}}' > /tmp/review.json
240
- beehaven call canvas-perceive '{"compositionId":"<id>","includePixels":true,"fitNodeIds":["<frame-id>"],"pixelScale":1}' > /tmp/perceive.json
257
+ beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame-id>"]}}' --brief
258
+ beehaven call canvas-perceive '{"compositionId":"<id>","includePixels":true,"fitNodeIds":["<frame-id>"],"pixelScale":1}' > perceive.json
241
259
  ```
242
260
 
243
261
  Use the bundled `review.mjs` to decode the PNG and keep base64 out of the conversation.
@@ -268,7 +286,12 @@ MCP's artifact tools are split by operation:
268
286
  node array — pass `expectedRev` from the read.
269
287
  - `clearly_edit` patches existing nodes with `changes:{ids,patch}`.
270
288
 
271
- Canvas action dispatch and targeted pixels use the CLI. Do not invent
272
- `clearly_canvas_act`/`clearly_canvas_perceive` tools. If only MCP is available, use the
273
- artifact schemas for supported edits and state that pixel review remains unverified.
274
- Do not claim the design was visually checked from scene JSON alone.
289
+ The connector also has native canvas tools for agents without a shell, batched per kind of
290
+ step rather than mixed (MCP2-22): `clearly_canvas_add_nodes` builds a section per call (up to
291
+ 300 frames, shapes, text, pen paths and groups of its own new layers),
292
+ `clearly_canvas_update_nodes {edits:[{ids,patch}]}` revises many layers at once,
293
+ `clearly_canvas_measure_text {texts:[…]}` measures several lines, and
294
+ `clearly_canvas_review` returns the saved artboard as an MCP image to inspect. There is no
295
+ `clearly_canvas_act`: a mixed batch is the CLI's `canvas-act`. In a shell, use the CLI; it
296
+ is fewer calls for the same scene. Do not claim the design was visually checked from scene
297
+ JSON alone.
@@ -123,14 +123,35 @@ export async function saveReviewPixels(report, prefix, fetchImage = fetch) {
123
123
  return saveReview({ ...report, preview: { ...report.preview, dataUrl: `data:image/png;base64,${bytes.toString('base64')}` } }, prefix);
124
124
  }
125
125
 
126
+ /**
127
+ * `--accept <findings.json>`: findings you have looked at and are keeping, each
128
+ * `{ check, ids: [node IDs], reason }`. They stay visible in the report and return `look`.
129
+ * Without this, a deliberate overlap (steam rising out of a bowl) could only be accepted by
130
+ * hand-writing the canvas.review call — agents did exactly that (MCP2-20).
131
+ */
132
+ export function takeAccept(argv) {
133
+ const i = argv.indexOf('--accept');
134
+ if (i < 0) return { argv, accept: undefined };
135
+ const file = argv[i + 1];
136
+ if (!file || file.startsWith('--')) throw new Error('--accept needs a JSON file: [{"check":"text-over-image","ids":["…"],"reason":"…"}]');
137
+ const accept = JSON.parse(readFileSync(file, 'utf8'));
138
+ if (!Array.isArray(accept) || !accept.every(a => a && typeof a.check === 'string' && Array.isArray(a.ids) && a.ids.length && typeof a.reason === 'string' && a.reason.trim())) {
139
+ throw new Error('--accept file must be an array of { check, ids: [node IDs], reason } with a non-empty reason.');
140
+ }
141
+ return { argv: argv.filter((_, j) => j !== i && j !== i + 1), accept };
142
+ }
143
+
126
144
  export async function main(argv = process.argv.slice(2)) {
145
+ const taken = takeAccept(argv);
146
+ argv = taken.argv;
147
+ const accept = taken.accept ? { accept: taken.accept } : {};
127
148
  if (argv.includes('--many')) {
128
149
  const args = argv.filter(a => a !== '--many');
129
- if (args.length !== 3) throw new Error('Usage: node review.mjs --many <compositionId> <artboard-ids.json> <output-directory>');
150
+ if (args.length !== 3) throw new Error('Usage: node review.mjs --many <compositionId> <artboard-ids.json> <output-directory> [--accept findings.json]');
130
151
  const [compositionId, manifest, directory] = args;
131
152
  const ids = JSON.parse(readFileSync(manifest, 'utf8'));
132
153
  if (!Array.isArray(ids) || !ids.length) throw new Error('The manifest must be a non-empty artboard ID/name array.');
133
- const response = rpc('canvas-act', { compositionId, action: 'canvas.review-many', args: { ids, preview: 'files' } });
154
+ const response = rpc('canvas-act', { compositionId, action: 'canvas.review-many', args: { ids, preview: 'files', ...accept } });
134
155
  const reports = response.results?.[0]?.value?.reviews;
135
156
  if (!Array.isArray(reports) || reports.length !== ids.length) throw new Error('Incomplete review-many result; do not claim all artboards passed.');
136
157
  const saved = [];
@@ -140,7 +161,7 @@ export async function main(argv = process.argv.slice(2)) {
140
161
  }
141
162
  const legacy = argv.includes('--perceive');
142
163
  const args = argv.filter((a) => a !== '--perceive');
143
- if (args.length !== 3) throw new Error('Usage: node review.mjs <compositionId> <frame-id-or-name> <output-prefix> [--perceive]\n--perceive: older workers, preview only; pass a frame ID.');
164
+ if (args.length !== 3) throw new Error('Usage: node review.mjs <compositionId> <frame-id-or-name> <output-prefix> [--accept findings.json] [--perceive]\n--perceive: older workers, preview only; pass a frame ID.');
144
165
  const [compositionId, id, prefix] = args;
145
166
  let report;
146
167
  if (legacy) {
@@ -150,7 +171,7 @@ export async function main(argv = process.argv.slice(2)) {
150
171
  report = { preview: response.room?.pixels, visualReviewRequired: true,
151
172
  limitations: ['Preview-only compatibility mode. Text metrics and scoped audit were not run.'] };
152
173
  } else {
153
- const response = rpc('canvas-act', { compositionId, action: 'canvas.review', args: { ids: [id], pixelScale: 1 } });
174
+ const response = rpc('canvas-act', { compositionId, action: 'canvas.review', args: { ids: [id], pixelScale: 1, ...accept } });
154
175
  report = response.results?.[0]?.value;
155
176
  if (!report?.scope) throw new Error('canvas.review unavailable. Check canvas-catalog; use --perceive for an explicitly preview-only check on an older worker.');
156
177
  }
package/skills/index.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "project-plans.md",
18
18
  "review.mjs"
19
19
  ],
20
- "sha": "e23e25bd46b02d9c"
20
+ "sha": "4b478740d8ef22c2"
21
21
  },
22
22
  {
23
23
  "name": "agent-home",