@clearlysh/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 +18 -1
- package/dist/build-channel.js +2 -2
- package/dist/cli.js +32 -1
- package/dist/commands/canvas-image.js +101 -0
- package/dist/commands/canvas.js +5 -0
- package/dist/commands/handoff.js +227 -0
- package/dist/commands/help.js +46 -1
- package/dist/verb-surface.js +1 -0
- package/package.json +1 -1
- package/skills/beehaven-cli/SKILL.md +1 -0
- package/skills/clearly-canvas/SKILL.md +4 -1
- package/skills/clearly-canvas/authoring.md +56 -5
- package/skills/index.json +2 -2
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
|
|
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
|
|
package/dist/build-channel.js
CHANGED
|
@@ -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 = '
|
|
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-
|
|
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.
|
|
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
|
+
}
|
package/dist/commands/canvas.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/commands/help.js
CHANGED
|
@@ -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'],
|
package/dist/verb-surface.js
CHANGED
|
@@ -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
|
@@ -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,
|
|
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
|
|
109
|
-
node accepts `src
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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": "
|
|
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": "
|
|
20
|
+
"sha": "21faa5f8d14beefe"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "agent-home",
|