beehaven 0.8.17 → 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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ npx --yes beehaven@latest guide
23
23
 
24
24
  `guide` works before sign-in. It explains when Clearly is useful, gives the
25
25
  human a signup link, and shows the authenticated first artifact. `npx` uses an
26
- ephemeral npm install; it does not create a Clearly account or publish a file.
26
+ ephemeral npm install; it does not create a Clearly account or upload anything.
27
27
  Public builds send one anonymous, content-free `cli:guide` usage event unless
28
28
  `BEEHAVEN_NO_TELEMETRY` or the local `telemetry-off` file disables telemetry.
29
29
  The package also provides the `beehaven` binary for a global npm install:
@@ -36,6 +36,23 @@ beehaven guide
36
36
  The identical release is also listed under the Clearly npm organization as
37
37
  `@clearlysh/beehaven`.
38
38
 
39
+ ### Hand work to someone without an account
40
+
41
+ When an agent's work is ready before the person has signed up, `handoff` needs
42
+ no login and prints one private link:
43
+
44
+ ```bash
45
+ npx --yes beehaven@latest handoff plan.md
46
+ npx --yes beehaven@latest handoff plan.md --tasks todo.md --csv budget.csv --wait
47
+ ```
48
+
49
+ The person previews it, signs in or creates a free account, and saves it. A
50
+ document alone becomes a private draft; with `--tasks` (a Markdown checklist or a
51
+ JSON array) or `--csv` files it becomes a private project with a ticket per task
52
+ and a sheet per CSV. The link is a credential — the first person to save it owns
53
+ the work — so send it only to the person you are working with. Unsaved handoffs
54
+ expire after 7 days. `--wait` returns when they save it.
55
+
39
56
  The self-contained installer remains the recommended persistent install. It does
40
57
  not require npm to resolve dependencies on the user's machine:
41
58
 
@@ -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 = 'd66d1de0cca7';
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-27T06:15Z';
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
@@ -944,6 +944,12 @@ const AGENT_LOGIN_EXEMPT = new Set([
944
944
  * as `skills`: setup must not require the thing setup provides.
945
945
  */
946
946
  'update',
947
+ /**
948
+ * ⚠ `handoff` EXISTS FOR CALLERS WITH NO ACCOUNT. It posts one Markdown file to the relay's
949
+ * anonymous door and prints a claim link; it never opens a workspace. Gating it on an agent login
950
+ * would refuse the one person it is for — the agent whose user has not signed up yet (SGS-73).
951
+ */
952
+ 'handoff',
947
953
  ]);
948
954
  /**
949
955
  * ⚠ ALIAS → CANONICAL VERB, for the activity log's `source` column only.
@@ -1359,6 +1365,10 @@ async function run() {
1359
1365
  quote: { type: 'string' }, // document passage
1360
1366
  cell: { type: 'string' }, // sheet cell, A1 or "Tab!B4"
1361
1367
  sheet: { type: 'string' }, // sheet tab, when --cell names none
1368
+ // `handoff`: a task list file, CSV files (repeatable), and waiting for the person to save it.
1369
+ tasks: { type: 'string' },
1370
+ csv: { type: 'string', multiple: true },
1371
+ wait: { type: 'boolean', default: false },
1362
1372
  column: { type: 'string' }, // board column key or label
1363
1373
  pin: { type: 'string' }, // canvas pin to reply to
1364
1374
  // (--x / --y for a new pin and --kind are declared below, shared with other verbs)
@@ -1588,6 +1598,8 @@ async function run() {
1588
1598
  svg: { type: 'string' },
1589
1599
  x: { type: 'string' },
1590
1600
  y: { type: 'string' },
1601
+ width: { type: 'string' },
1602
+ height: { type: 'string' },
1591
1603
  w: { type: 'string' },
1592
1604
  h: { type: 'string' },
1593
1605
  // `as` is also used by teleport (text | attachment |
@@ -1736,11 +1748,17 @@ async function run() {
1736
1748
  A useful first artifact:
1737
1749
  beehaven doc add ./flow.md # after sign-in and agent login
1738
1750
 
1751
+ Work ready before they have an account? Hand it over with one private link:
1752
+ beehaven handoff ./plan.md --tasks ./todo.md --wait
1753
+ They preview it and save it to a free workspace. First to save owns it; give
1754
+ the link only to the person you are working with.
1755
+
1739
1756
  If the agent has no shell, connect Clearly MCP through browser OAuth:
1740
1757
  https://relay.clearly.sh/mcp
1741
1758
 
1742
1759
  Do not claim a file was published or a workspace was created until the CLI
1743
- returns a successful result. Anonymous publish-and-claim is not available.
1760
+ returns a successful result. A handoff link is not in their workspace until
1761
+ they save it.
1744
1762
 
1745
1763
  Docs: https://www.clearly.sh/docs/quickstart
1746
1764
  `);
@@ -1864,6 +1882,7 @@ async function run() {
1864
1882
  'composition', 'comp',
1865
1883
  'devices',
1866
1884
  'sheet', 'sheets',
1885
+ 'handoff',
1867
1886
  'slides', 'deck',
1868
1887
  'brand', 'brands',
1869
1888
  'component', 'components',
@@ -3311,6 +3330,18 @@ async function run() {
3311
3330
  console.log(' (or quit and relaunch the Hive Mac app if it owns the daemon).');
3312
3331
  break;
3313
3332
  }
3333
+ case 'handoff': {
3334
+ const { cmdHandoff } = await import('./commands/handoff.js');
3335
+ process.exitCode = await cmdHandoff(positionals[1], {
3336
+ title: values.title,
3337
+ json: !!values.json,
3338
+ tasks: values.tasks,
3339
+ csv: values.csv ?? [],
3340
+ wait: !!values.wait,
3341
+ version: getVersion(),
3342
+ });
3343
+ break;
3344
+ }
3314
3345
  case 'signup': {
3315
3346
  const email = values.email ?? positionals[1];
3316
3347
  const password = values.password;
@@ -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]
@@ -0,0 +1,227 @@
1
+ /**
2
+ * `beehaven handoff [file.md] [--tasks tasks.md|json] [--csv data.csv]… [--wait]` — give a person
3
+ * an agent's work when they have no Clearly account yet (SGS-73).
4
+ *
5
+ * Uploads a Markdown document and, optionally, the work around it — a task list and CSV sheets —
6
+ * to the relay's anonymous door, and prints ONE private claim link. The person previews it, signs
7
+ * in or signs up, and saves it: a document alone becomes a private draft; with tasks or sheets it
8
+ * becomes a private project holding the document, a ticket per task, and the sheets. No agent
9
+ * login, no workspace, no daemon: this is the step BEFORE any of those exist.
10
+ *
11
+ * ⚠ NOT `beehaven publish`. That verb ships app-marketplace packages and is unrelated.
12
+ *
13
+ * ⚠ THE LINK IS A CREDENTIAL. Whoever opens it first and saves it owns the work. The output says
14
+ * so, and says where it may go (to the person you are working with) and where it may not.
15
+ *
16
+ * ⚠ THE SERVER DECIDES AVAILABILITY. A relay without the feature answers 404, and this command
17
+ * says "not available here" with the signed-in alternative rather than guessing.
18
+ */
19
+ import { readFileSync, existsSync } from 'fs';
20
+ import { basename, extname, join } from 'path';
21
+ import { homedir } from 'os';
22
+ import { resolveRelaySelection } from '../relay-presets.js';
23
+ /** The relay's HTTPS origin for the env this CLI is configured for (prod when unconfigured). */
24
+ export function relayHttpOrigin() {
25
+ const override = process.env.BEEHAVEN_RELAY_HTTP_URL;
26
+ if (override)
27
+ return override.replace(/\/+$/, '');
28
+ let cfg = {};
29
+ try {
30
+ const p = join(process.env.BEEHAVEN_PREFIX || join(homedir(), '.beehaven'), 'config.json');
31
+ if (existsSync(p))
32
+ cfg = JSON.parse(readFileSync(p, 'utf8'));
33
+ }
34
+ catch { /* unconfigured ⇒ prod */ }
35
+ return resolveRelaySelection(cfg).url
36
+ .replace(/^wss:\/\//, 'https://').replace(/^ws:\/\//, 'http://').replace(/\/+$/, '');
37
+ }
38
+ const PRIORITY = { low: 0, medium: 1, med: 1, high: 2, urgent: 3 };
39
+ /**
40
+ * A task list is JSON (`[{ title, body?, priority? }]`) or Markdown the way an agent already writes
41
+ * one: each list item (`- [ ] …`, `- …`, `1. …`) is a task, and indented lines under it are its
42
+ * body. A trailing `(high)` / `[urgent]` sets the priority. Headings and prose between items are
43
+ * ignored, so a plan's own checklist section can be handed over as-is.
44
+ */
45
+ export function parseTasks(text) {
46
+ const trimmed = text.trim();
47
+ if (trimmed.startsWith('[')) {
48
+ const arr = JSON.parse(trimmed);
49
+ if (!Array.isArray(arr))
50
+ throw new Error('JSON task list must be an array');
51
+ return arr.map((t) => (typeof t === 'string' ? { title: t } : t));
52
+ }
53
+ const tasks = [];
54
+ let current = null;
55
+ const body = [];
56
+ const flush = () => {
57
+ if (current) {
58
+ const b = body.join('\n').trim();
59
+ if (b)
60
+ current.body = b;
61
+ tasks.push(current);
62
+ }
63
+ current = null;
64
+ body.length = 0;
65
+ };
66
+ for (const line of text.split(/\r?\n/)) {
67
+ const item = line.match(/^\s{0,3}(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(.+?)\s*$/);
68
+ if (item) {
69
+ flush();
70
+ let title = item[1];
71
+ let priority;
72
+ const pm = title.match(/\s*[([](low|medium|med|high|urgent)[)\]]\s*$/i);
73
+ if (pm) {
74
+ priority = PRIORITY[pm[1].toLowerCase()];
75
+ title = title.slice(0, pm.index).trim();
76
+ }
77
+ current = { title, ...(priority !== undefined ? { priority } : {}) };
78
+ }
79
+ else if (current && /^\s{2,}\S/.test(line)) {
80
+ body.push(line.trim());
81
+ }
82
+ else if (current && line.trim() && !/^\s/.test(line)) {
83
+ flush();
84
+ }
85
+ }
86
+ flush();
87
+ return tasks;
88
+ }
89
+ async function readInput(file) {
90
+ if (file === '-') {
91
+ const chunks = [];
92
+ for await (const c of process.stdin)
93
+ chunks.push(c);
94
+ return Buffer.concat(chunks).toString('utf8');
95
+ }
96
+ return readFileSync(file, 'utf8');
97
+ }
98
+ function usage() {
99
+ console.error(' Usage: beehaven handoff [file.md|-] [--tasks tasks.md|tasks.json] [--csv data.csv]… [--title "…"] [--wait] [--json]');
100
+ return 2;
101
+ }
102
+ async function post(url, body, version) {
103
+ try {
104
+ const res = await fetch(url, {
105
+ method: 'POST',
106
+ headers: { 'Content-Type': 'application/json', 'User-Agent': `beehaven/${version}` },
107
+ body: JSON.stringify(body),
108
+ signal: AbortSignal.timeout(20_000),
109
+ });
110
+ return { status: res.status, body: await res.json().catch(() => null) };
111
+ }
112
+ catch (e) {
113
+ return { status: 0, body: { ok: false, code: 'network', error: `Could not reach ${url}: ${e?.message ?? e}` } };
114
+ }
115
+ }
116
+ /**
117
+ * ⚠ WAITING READS THE SAME DOOR THE PAGE DOES, WITH NO IDENTITY. The agent holds the token, so it
118
+ * may ask; a saved draft answers 409 to anyone but its owner, which is exactly "someone saved it" —
119
+ * and says nothing about who.
120
+ */
121
+ async function waitForClaim(relay, token, opts) {
122
+ const deadline = Date.now() + (opts.waitMs ?? 15 * 60_000);
123
+ const every = opts.pollMs ?? 5_000;
124
+ while (Date.now() < deadline) {
125
+ const r = await post(`${relay}/publish/anon/inspect`, { token }, opts.version);
126
+ if (r.body?.code === 'already_claimed')
127
+ return 'saved';
128
+ if (r.status === 410 || r.status === 404)
129
+ return 'expired';
130
+ await new Promise((res) => setTimeout(res, every));
131
+ }
132
+ return 'timeout';
133
+ }
134
+ export async function cmdHandoff(file, opts) {
135
+ const csvFiles = opts.csv ?? [];
136
+ if (!file && !opts.tasks && !csvFiles.length)
137
+ return usage();
138
+ for (const f of [file, opts.tasks, ...csvFiles]) {
139
+ if (f && f !== '-' && !existsSync(f)) {
140
+ console.error(` No such file: ${f}`);
141
+ return 2;
142
+ }
143
+ }
144
+ let markdown = '';
145
+ let tasks = [];
146
+ const sheets = [];
147
+ try {
148
+ if (file)
149
+ markdown = await readInput(file);
150
+ if (opts.tasks)
151
+ tasks = parseTasks(readFileSync(opts.tasks, 'utf8'));
152
+ for (const f of csvFiles)
153
+ sheets.push({ title: basename(f, extname(f)), csv: readFileSync(f, 'utf8') });
154
+ }
155
+ catch (e) {
156
+ console.error(` Could not read the handoff: ${e?.message ?? e}`);
157
+ return 2;
158
+ }
159
+ if (opts.tasks && !tasks.length) {
160
+ console.error(` No tasks found in ${opts.tasks}. Use list items (- [ ] Title) or a JSON array.`);
161
+ return 2;
162
+ }
163
+ const relay = relayHttpOrigin();
164
+ const { status, body } = await post(`${relay}/publish/anon`, {
165
+ ...(markdown ? { markdown } : {}),
166
+ ...(opts.title ? { title: opts.title } : {}),
167
+ ...(tasks.length ? { tasks } : {}),
168
+ ...(sheets.length ? { sheets } : {}),
169
+ source: 'cli',
170
+ }, opts.version);
171
+ if (opts.json && !opts.wait) {
172
+ console.log(JSON.stringify(body ?? { ok: false, status }, null, 2));
173
+ return body?.ok ? 0 : 1;
174
+ }
175
+ if (!body?.ok) {
176
+ if (status === 404) {
177
+ console.error('\n Anonymous handoff is not available on this server yet.');
178
+ console.error(' Ask the person to create a free workspace, then run:');
179
+ console.error(' beehaven login && beehaven agent login <name> && beehaven doc add ' + (file && file !== '-' ? file : '<file>') + '\n');
180
+ return 1;
181
+ }
182
+ console.error(`\n Handoff failed: ${body?.error ?? `relay answered ${status}`}\n`);
183
+ return 1;
184
+ }
185
+ const days = Math.max(1, Math.round((Number(body.expiresAt) - Date.now()) / 86_400_000));
186
+ const parts = [
187
+ markdown.trim() ? 'a document' : '',
188
+ body.tasks ? `${body.tasks} task${body.tasks === 1 ? '' : 's'}` : '',
189
+ body.sheets ? `${body.sheets} sheet${body.sheets === 1 ? '' : 's'}` : '',
190
+ ].filter(Boolean).join(', ');
191
+ if (!opts.json) {
192
+ console.log(`
193
+ Ready to hand off: ${body.title} (${parts})
194
+
195
+ ${body.claimUrl}
196
+
197
+ Give this link to the person you are working with. They can preview it and
198
+ save it to their own Clearly workspace, creating a free account if needed.
199
+ The first person to save it owns it. It expires unsaved in ${days} days.
200
+ Do not post it publicly — in an issue, a commit, or a shared channel.
201
+ `);
202
+ }
203
+ if (!opts.wait)
204
+ return 0;
205
+ const token = String(body.claimUrl).split('#')[1] ?? '';
206
+ if (!opts.json)
207
+ console.log(' Waiting for them to save it (up to 15 minutes)…');
208
+ const outcome = await waitForClaim(relay, token, opts);
209
+ if (opts.json) {
210
+ console.log(JSON.stringify({ ...body, outcome }, null, 2));
211
+ return outcome === 'saved' ? 0 : 1;
212
+ }
213
+ if (outcome === 'saved') {
214
+ console.log(`
215
+ Saved. It is in their workspace now, private to them.
216
+ To keep working on it with them, ask them to connect you:
217
+ npx --yes beehaven@latest login
218
+ beehaven start --headless && beehaven connect home
219
+ beehaven agent login <your-name> --label "Continue: ${body.title}" --client cli
220
+ `);
221
+ return 0;
222
+ }
223
+ console.log(outcome === 'expired'
224
+ ? '\n The link expired before it was saved. Run handoff again for a new one.\n'
225
+ : '\n Not saved yet. The link still works; they can save it any time before it expires.\n');
226
+ return outcome === 'expired' ? 1 : 0;
227
+ }
@@ -374,6 +374,48 @@ const PAGES = {
374
374
  ],
375
375
  related: ['logout', 'whoami', 'status', 'doctor'],
376
376
  },
377
+ guide: {
378
+ synopsis: 'beehaven guide',
379
+ description: `What Clearly is and the first steps for a shell-capable agent: a read-only demo,
380
+ the signup link, login, the headless daemon, and agent login. Needs no account.`,
381
+ related: ['signup', 'login', 'agent'],
382
+ },
383
+ signup: {
384
+ synopsis: 'beehaven signup',
385
+ description: `Open the Clearly signup page in the browser. After the account exists, run
386
+ \`beehaven login\` to connect the CLI to it.`,
387
+ related: ['login', 'guide'],
388
+ },
389
+ handoff: {
390
+ synopsis: 'beehaven handoff [file.md|-] [--tasks tasks.md|json] [--csv data.csv]… [--title "…"] [--wait] [--json]',
391
+ description: `Give a person your work when they have no Clearly account yet. Needs no login.
392
+ Prints one private link. They preview it, sign in or create a free account, and save it.
393
+
394
+ A document alone becomes a private draft. Add \`--tasks\` (a Markdown checklist or a JSON
395
+ array; indented lines become a task's description, a trailing "(high)" sets priority) and/or
396
+ \`--csv\` files, and it becomes a private project: the document, a ticket per task on
397
+ its board, and a sheet per CSV. Up to 100 tasks, 5 sheets, 256 KB in total.
398
+
399
+ The link is a credential: the first person to save it owns it. Send it to the person you
400
+ are working with — never to an issue, a commit or a public channel. Unsaved handoffs expire
401
+ after 7 days. \`--wait\` blocks (up to 15 minutes) until they save it, then prints how to
402
+ connect you to their workspace so you can keep working.
403
+
404
+ Already signed in? Use \`beehaven doc add\` instead — it writes straight to the workspace.
405
+ Not \`beehaven publish\`, which ships app packages.`,
406
+ flags: [
407
+ { name: '--tasks', desc: 'Task list: Markdown list items or a JSON array of { title, body?, priority? }' },
408
+ { name: '--csv', desc: 'A CSV file to include as a sheet (repeatable)' },
409
+ { name: '--title', desc: 'Title (default: the document\'s first heading)' },
410
+ { name: '--wait', desc: 'Wait until the person saves it' },
411
+ { name: '--json', desc: 'Print the relay response as JSON' },
412
+ ],
413
+ examples: [
414
+ { cmd: 'beehaven handoff plan.md', note: 'one private draft' },
415
+ { cmd: 'beehaven handoff plan.md --tasks todo.md --csv budget.csv --wait', note: 'a project with tickets and a sheet' },
416
+ ],
417
+ related: ['login', 'doc'],
418
+ },
377
419
  logout: {
378
420
  synopsis: 'beehaven logout',
379
421
  description: 'Clear stored credentials (keychain + ~/.beehaven/config.json).',
@@ -493,13 +535,15 @@ const PAGES = {
493
535
  related: ['cd', 'status', 'actions', 'workspaces'],
494
536
  },
495
537
  canvas: {
496
- synopsis: 'beehaven canvas <open|invoke|query|actions|help|record> [args]',
538
+ synopsis: 'beehaven canvas <open|add-image|invoke|query|actions|help|record> [args]',
497
539
  description: `Interact with the FAB canvas (notes editor) from the CLI.
498
540
 
499
541
  Subcommands:
500
542
  open <noteId> Open a note in the canvas editor
501
543
  invoke <action> [json] Call an editor-client action (local UI ops)
502
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)
503
547
  actions / list List editor-client UI actions (LOCAL — what the
504
548
  canvas UI can do)
505
549
  help [domain] Show the workspace RPC catalog (CLOUD — what
@@ -521,6 +565,7 @@ const PAGES = {
521
565
  { cmd: 'beehaven canvas actions', note: 'editor UI actions (local)' },
522
566
  { cmd: 'beehaven canvas help', note: 'workspace RPC catalog (cloud)' },
523
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' },
524
569
  { cmd: 'beehaven canvas invoke insert-block \'{"type":"text","text":"hi"}\'' },
525
570
  ],
526
571
  related: ['conversations', 'call', 'actions'],
@@ -59,6 +59,7 @@ export const VERB_SURFACE = {
59
59
  login: { cls: 'STRUCTURAL', why: 'browser OAuth + keychain' },
60
60
  logout: { cls: 'STRUCTURAL', why: 'clears local credentials' },
61
61
  signup: { cls: 'STRUCTURAL', why: 'opens a browser' },
62
+ handoff: { cls: 'STRUCTURAL', why: 'posts a local file to the anonymous door; no account, no workspace' },
62
63
  whoami: { cls: 'STRUCTURAL', why: 'reads local credentials' },
63
64
  start: { cls: 'STRUCTURAL', why: 'spawns the daemon `call` needs' },
64
65
  stop: { cls: 'STRUCTURAL', why: 'daemon lifecycle' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beehaven",
3
- "version": "0.8.17",
3
+ "version": "0.8.19",
4
4
  "description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
5
5
  "private": false,
6
6
  "type": "module",
@@ -50,6 +50,7 @@ Only ONE daemon runs at a time, on `/tmp/beehaven-ipc.sock`. Either the **Hive M
50
50
 
51
51
  - **Start:** open the Mac app (the tray auto-spawns `daemon-entry.js`), OR `beehaven start --headless` for a detached, CLI-owned daemon with no Mac app (`--headless` disables the tray-death watchdog so it persists — without it a headless daemon suicides ~3s after spawn). Plain `beehaven start` (no flag) just opens the tray app. **Stop/restart:** `beehaven stop` / `beehaven restart`. **Sign in/out:** `beehaven login` / `logout` / `whoami`.
52
52
  - **`beehaven status`** — Account/Target/Tier (proves a token is on disk, *not* relay liveness). The live proof is "any command works."
53
+ - **No account yet?** `beehaven handoff plan.md [--tasks todo.md] [--csv data.csv] [--wait]` needs no login, daemon or workspace: it prints one private link the person uses to preview and save the work (a document → private draft; with tasks/CSVs → private project). The link is a credential — give it only to the person you are working with.
53
54
  - **"Cannot connect to daemon"** ⇒ nothing owns the socket → open the Mac app, or `beehaven start --headless`. **"Not connected to cloud"** ⇒ daemon up but its relay WS isn't live: if the token EXPIRED, `beehaven login` re-auths (a plain `restart` will NOT refresh it — reach for this first); if the relay just dropped (e.g. after sleep), `beehaven restart` (CLI-owned) or relaunch the Mac app.
54
55
  - **State files:** `~/.beehaven/status.json` (refreshed ~2s: `relayConnected`, `reconnectAttempts`, `relayEndpoint`, `pid`) · `~/.beehaven/logs/daemon.log` (structured) · `~/.beehaven/target.json` · `~/.beehaven/config.json`.
55
56
 
@@ -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
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "SKILL.md"
6
6
  ],
7
- "sha": "03e91dcc483f189d"
7
+ "sha": "2d33d2ecfb166fa6"
8
8
  },
9
9
  {
10
10
  "name": "clearly-canvas",
@@ -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",