beehaven 0.8.18 → 0.8.20
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/build-channel.js +2 -2
- package/dist/cli.js +12 -0
- package/dist/commands/agent.js +11 -3
- package/dist/commands/canvas-image.js +101 -0
- package/dist/commands/canvas.js +5 -0
- package/dist/commands/help.js +32 -2
- package/dist/commands/inbox.js +208 -0
- package/package.json +2 -2
- package/skills/clearly-agent/SKILL.md +35 -1
- package/skills/clearly-canvas/SKILL.md +4 -1
- package/skills/clearly-canvas/authoring.md +56 -5
- package/skills/index.json +2 -2
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 = '046981bccb35-dirty';
|
|
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-29T05:23Z';
|
|
59
59
|
/**
|
|
60
60
|
* The build identity as one short string, for `--version`.
|
|
61
61
|
*
|
package/dist/cli.js
CHANGED
|
@@ -185,6 +185,7 @@ function printHelp() {
|
|
|
185
185
|
beehaven mv <src> <dest> Move, or rename
|
|
186
186
|
|
|
187
187
|
Work:
|
|
188
|
+
beehaven inbox Messages to you from people and agents (read · send · reply · done · who)
|
|
188
189
|
beehaven ticket create "<t>" File a card on a board (list · move · edit · show · comment)
|
|
189
190
|
beehaven board list The boards you can file to (create · delete · report · backlog)
|
|
190
191
|
beehaven sprint list Plan, run and close a sprint (create · plan · start · complete)
|
|
@@ -1575,6 +1576,9 @@ async function run() {
|
|
|
1575
1576
|
kind: { type: 'string' },
|
|
1576
1577
|
tags: { type: 'string' },
|
|
1577
1578
|
title: { type: 'string' },
|
|
1579
|
+
// `beehaven inbox send <to> "…" --subject "…" --refs CLR-42,<doc id>` (WIB-1).
|
|
1580
|
+
subject: { type: 'string' },
|
|
1581
|
+
refs: { type: 'string' },
|
|
1578
1582
|
source: { type: 'string' },
|
|
1579
1583
|
info: { type: 'boolean', default: false },
|
|
1580
1584
|
// #109 — `beehaven conversations export` flags.
|
|
@@ -1598,6 +1602,8 @@ async function run() {
|
|
|
1598
1602
|
svg: { type: 'string' },
|
|
1599
1603
|
x: { type: 'string' },
|
|
1600
1604
|
y: { type: 'string' },
|
|
1605
|
+
width: { type: 'string' },
|
|
1606
|
+
height: { type: 'string' },
|
|
1601
1607
|
w: { type: 'string' },
|
|
1602
1608
|
h: { type: 'string' },
|
|
1603
1609
|
// `as` is also used by teleport (text | attachment |
|
|
@@ -6432,6 +6438,12 @@ complete -F _beehaven_completions beehaven`);
|
|
|
6432
6438
|
await cmdMention(positionals[1] ?? '', positionals.slice(2), values);
|
|
6433
6439
|
break;
|
|
6434
6440
|
}
|
|
6441
|
+
// WIB-1 — messages between the people and agents of a workspace.
|
|
6442
|
+
case 'inbox': {
|
|
6443
|
+
const { cmdInbox } = await import('./commands/inbox.js');
|
|
6444
|
+
await cmdInbox(positionals[1] ?? '', positionals.slice(2), values);
|
|
6445
|
+
break;
|
|
6446
|
+
}
|
|
6435
6447
|
case 'ticket':
|
|
6436
6448
|
case 'board': {
|
|
6437
6449
|
const { cmdTicket, cmdBoard } = await import('./commands/ticket.js');
|
package/dist/commands/agent.js
CHANGED
|
@@ -897,9 +897,17 @@ function printBrief(brief) {
|
|
|
897
897
|
*/
|
|
898
898
|
const mentions = brief.workspace?.mentions ?? [];
|
|
899
899
|
const voicemail = brief.workspace?.voicemail ?? [];
|
|
900
|
-
|
|
900
|
+
/** WIB-1 — people and agents writing TO this agent. Oldest first; reading one clears it. */
|
|
901
|
+
const inbox = brief.workspace?.inboxThreads ?? [];
|
|
902
|
+
if (mentions.length || voicemail.length || inbox.length) {
|
|
901
903
|
console.log('');
|
|
902
|
-
console.log(` ${bold('Addressed to you')} ${dim(`(${mentions.length + voicemail.length})`)}`);
|
|
904
|
+
console.log(` ${bold('Addressed to you')} ${dim(`(${mentions.length + voicemail.length + inbox.length})`)}`);
|
|
905
|
+
for (const t of inbox.slice(0, 5)) {
|
|
906
|
+
console.log(` ${dim('·')} ${cyan('message')} from ${t.from}: “${t.subject}” ${dim(`${t.unread} unread · ${t.threadId}`)}`);
|
|
907
|
+
console.log(` ${String(t.preview ?? '').replace(/\s+/g, ' ').slice(0, 120)}`);
|
|
908
|
+
}
|
|
909
|
+
if (inbox.length)
|
|
910
|
+
console.log(dim(` Read with \`beehaven inbox read <id>\`, answer with \`beehaven inbox reply <id> "…"\`, then \`beehaven inbox done <id>\`.`));
|
|
903
911
|
for (const v of voicemail.slice(0, 3)) {
|
|
904
912
|
console.log(` ${dim('·')} ${cyan('voicemail')} from ${v.from}: ${String(v.text ?? '').replace(/\s+/g, ' ').slice(0, 100)}`);
|
|
905
913
|
}
|
|
@@ -912,7 +920,7 @@ function printBrief(brief) {
|
|
|
912
920
|
console.log(dim(` Act, reply with \`beehaven comment <ref> "…"\`, then \`beehaven mention ack <id>\`.`));
|
|
913
921
|
}
|
|
914
922
|
if (!sessions.length && !assigned.length && !pending.length && !memories.length && !focus.length) {
|
|
915
|
-
if (!mentions.length && !voicemail.length)
|
|
923
|
+
if (!mentions.length && !voicemail.length && !inbox.length)
|
|
916
924
|
console.log(dim(' First visit — nothing to catch up on.'));
|
|
917
925
|
return;
|
|
918
926
|
}
|
|
@@ -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]
|
package/dist/commands/help.js
CHANGED
|
@@ -113,7 +113,34 @@ const PAGES = {
|
|
|
113
113
|
{ cmd: 'beehaven mention who jin' },
|
|
114
114
|
{ cmd: 'beehaven mention resolve "cc @omni and @bob"' },
|
|
115
115
|
],
|
|
116
|
-
related: ['comment'],
|
|
116
|
+
related: ['comment', 'inbox'],
|
|
117
|
+
},
|
|
118
|
+
inbox: {
|
|
119
|
+
synopsis: 'beehaven inbox [box|read <id>|send <to> "…"|reply <id> "…"|done <id>|add|leave|who|count]',
|
|
120
|
+
description: 'Messages between the people and agents of this workspace — everyone can message everyone. You act as '
|
|
121
|
+
+ 'whoever this terminal is signed in as: an agent (`beehaven agent login <name>`) has its OWN inbox, '
|
|
122
|
+
+ 'separate from its owner\'s, and its unread threads are printed in its brief under "Addressed to you".\n\n'
|
|
123
|
+
+ 'Reading a thread marks it read — that is the acknowledgement. Reply in the thread, then `done` it '
|
|
124
|
+
+ '(archive); a new reply brings it back. Sending runs no AI: an agent you message reads it next session.\n\n'
|
|
125
|
+
+ 'Boxes: inbox (default — threads someone else has written in), unread, starred, sent, archived, all. '
|
|
126
|
+
+ '`<to>` takes the handles `beehaven inbox who` lists, comma-separated.',
|
|
127
|
+
flags: [
|
|
128
|
+
{ name: '--search "<q>"', desc: 'On a box: only threads whose subject or messages contain it.' },
|
|
129
|
+
{ name: '--subject "<s>"', desc: 'On send: the thread subject (defaults to the first line).' },
|
|
130
|
+
{ name: '--refs <a,b>', desc: 'On send/reply: workspace items to link — ids, keys (CLR-42) or exact titles.' },
|
|
131
|
+
{ name: '--as agent:<id>', desc: 'On a box/read: a person reading an agent they own. Read-only; marks nothing read.' },
|
|
132
|
+
{ name: '--limit <n>', desc: 'On a box: how many threads (default 50).' },
|
|
133
|
+
{ name: '--json', desc: 'Raw result.' },
|
|
134
|
+
],
|
|
135
|
+
examples: [
|
|
136
|
+
{ cmd: 'beehaven inbox' },
|
|
137
|
+
{ cmd: 'beehaven inbox read it_m1x2_ab12cd34' },
|
|
138
|
+
{ cmd: 'beehaven inbox send ada,pixel "Draft is in CLR-42 — can you review?" --refs CLR-42' },
|
|
139
|
+
{ cmd: 'beehaven inbox reply it_m1x2_ab12cd34 "Done — see the second option."' },
|
|
140
|
+
{ cmd: 'beehaven inbox done it_m1x2_ab12cd34' },
|
|
141
|
+
{ cmd: 'beehaven inbox who' },
|
|
142
|
+
],
|
|
143
|
+
related: ['mention', 'comment'],
|
|
117
144
|
},
|
|
118
145
|
ticket: {
|
|
119
146
|
synopsis: 'beehaven ticket <create|list|move|edit|show|comment|comments|delete> [args]',
|
|
@@ -535,13 +562,15 @@ const PAGES = {
|
|
|
535
562
|
related: ['cd', 'status', 'actions', 'workspaces'],
|
|
536
563
|
},
|
|
537
564
|
canvas: {
|
|
538
|
-
synopsis: 'beehaven canvas <open|invoke|query|actions|help|record> [args]',
|
|
565
|
+
synopsis: 'beehaven canvas <open|add-image|invoke|query|actions|help|record> [args]',
|
|
539
566
|
description: `Interact with the FAB canvas (notes editor) from the CLI.
|
|
540
567
|
|
|
541
568
|
Subcommands:
|
|
542
569
|
open <noteId> Open a note in the canvas editor
|
|
543
570
|
invoke <action> [json] Call an editor-client action (local UI ops)
|
|
544
571
|
query <action> Read editor-client state
|
|
572
|
+
add-image <file> Upload a local image and place it on a composition
|
|
573
|
+
(--composition <id>, optional x/y/width/height)
|
|
545
574
|
actions / list List editor-client UI actions (LOCAL — what the
|
|
546
575
|
canvas UI can do)
|
|
547
576
|
help [domain] Show the workspace RPC catalog (CLOUD — what
|
|
@@ -563,6 +592,7 @@ const PAGES = {
|
|
|
563
592
|
{ cmd: 'beehaven canvas actions', note: 'editor UI actions (local)' },
|
|
564
593
|
{ cmd: 'beehaven canvas help', note: 'workspace RPC catalog (cloud)' },
|
|
565
594
|
{ cmd: 'beehaven canvas help --search "image"', note: 'find image-related RPCs' },
|
|
595
|
+
{ cmd: 'beehaven canvas add-image ./photo.png --composition <id>', note: 'upload a local image and place it on the canvas' },
|
|
566
596
|
{ cmd: 'beehaven canvas invoke insert-block \'{"type":"text","text":"hi"}\'' },
|
|
567
597
|
],
|
|
568
598
|
related: ['conversations', 'call', 'actions'],
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven inbox` — messages between the people and agents of a workspace (WIB-1).
|
|
3
|
+
*
|
|
4
|
+
* beehaven inbox [box] your threads — box: inbox (default) | unread |
|
|
5
|
+
* starred | sent | archived | all --search "…"
|
|
6
|
+
* beehaven inbox read <threadId> open a thread (marks it read)
|
|
7
|
+
* beehaven inbox send <to> "message" start a thread — <to> is one or more handles,
|
|
8
|
+
* comma-separated: "ada,pixel"
|
|
9
|
+
* --subject "…" --refs CLR-42,<doc id> (workspace items to link)
|
|
10
|
+
* beehaven inbox reply <threadId> "message" answer in a thread
|
|
11
|
+
* beehaven inbox done <threadId> archive it — handled (a reply brings it back)
|
|
12
|
+
* beehaven inbox archive|unarchive|star|unstar|mute|unmute|unread <threadId>
|
|
13
|
+
* beehaven inbox add <threadId> <to> bring more people or agents in
|
|
14
|
+
* beehaven inbox leave <threadId> stop receiving a thread
|
|
15
|
+
* beehaven inbox who [query] everyone you can message (people and agents)
|
|
16
|
+
* beehaven inbox count unread messages and threads
|
|
17
|
+
*
|
|
18
|
+
* ⚠ YOU ACT AS WHOEVER THIS TERMINAL IS SIGNED IN AS. An agent (`beehaven agent login <name>`)
|
|
19
|
+
* has its own inbox — messages to it are not its owner's, and it cannot read anyone else's.
|
|
20
|
+
* ⚠ READING IS THE ACKNOWLEDGEMENT. There is no separate ack: `read` clears the unread count,
|
|
21
|
+
* and `done` takes it out of the inbox once you have acted.
|
|
22
|
+
*/
|
|
23
|
+
import { agentRpc as rpcOnce, getTimeout } from './workspace.js';
|
|
24
|
+
const c = {
|
|
25
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
26
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
27
|
+
violet: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
28
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
29
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
30
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
31
|
+
};
|
|
32
|
+
/** ⚠ `rpcOnce` returns `{ error }` as a VALUE and never throws. */
|
|
33
|
+
function failure(res) {
|
|
34
|
+
if (!res || typeof res !== 'object')
|
|
35
|
+
return 'no answer from the workspace';
|
|
36
|
+
if (typeof res.error === 'string' && res.error)
|
|
37
|
+
return res.error;
|
|
38
|
+
if (res.ok === false)
|
|
39
|
+
return String(res.message ?? res.code ?? 'the workspace refused the call');
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function die(msg, hint) {
|
|
43
|
+
console.error(` Error: ${msg}`);
|
|
44
|
+
if (hint)
|
|
45
|
+
console.error(c.dim(` ${hint}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const ago = (ts) => {
|
|
49
|
+
const d = Date.now() - Number(ts || 0);
|
|
50
|
+
if (d < 60_000)
|
|
51
|
+
return 'just now';
|
|
52
|
+
if (d < 3_600_000)
|
|
53
|
+
return `${Math.round(d / 60_000)}m ago`;
|
|
54
|
+
if (d < 86_400_000)
|
|
55
|
+
return `${Math.round(d / 3_600_000)}h ago`;
|
|
56
|
+
return `${Math.round(d / 86_400_000)}d ago`;
|
|
57
|
+
};
|
|
58
|
+
const who = (p) => (p?.kind === 'agent' ? c.violet(String(p.name)) : String(p?.name ?? 'someone'));
|
|
59
|
+
const list = (s) => s.split(',').map((x) => x.trim()).filter(Boolean);
|
|
60
|
+
const BOXES = new Set(['inbox', 'unread', 'starred', 'sent', 'archived', 'all']);
|
|
61
|
+
const MARKS = {
|
|
62
|
+
done: { archived: true, read: true },
|
|
63
|
+
archive: { archived: true },
|
|
64
|
+
unarchive: { archived: false },
|
|
65
|
+
star: { starred: true },
|
|
66
|
+
unstar: { starred: false },
|
|
67
|
+
mute: { muted: true },
|
|
68
|
+
unmute: { muted: false },
|
|
69
|
+
unread: { read: false },
|
|
70
|
+
};
|
|
71
|
+
async function call(action, input, values) {
|
|
72
|
+
const res = await rpcOnce(action, input, getTimeout(values));
|
|
73
|
+
const f = failure(res);
|
|
74
|
+
if (f)
|
|
75
|
+
die(f, res?.hint);
|
|
76
|
+
if (values.json) {
|
|
77
|
+
console.log(JSON.stringify(res, null, 2));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
return res;
|
|
81
|
+
}
|
|
82
|
+
async function printList(box, values) {
|
|
83
|
+
const res = await call('inbox-list', {
|
|
84
|
+
box, ...(values.search ? { q: String(values.search) } : {}), ...(values.as ? { as: String(values.as) } : {}),
|
|
85
|
+
...(values.limit ? { limit: Number(values.limit) } : {}),
|
|
86
|
+
}, values);
|
|
87
|
+
const threads = res.threads ?? [];
|
|
88
|
+
const counts = res.counts ?? {};
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(` ${c.bold(box === 'inbox' ? 'Inbox' : box[0].toUpperCase() + box.slice(1))}${res.as ? c.dim(` — reading ${res.as}, read-only`) : ''} ${c.dim(`(${counts.unread ?? 0} unread in ${counts.threads ?? 0} thread${counts.threads === 1 ? '' : 's'})`)}`);
|
|
91
|
+
if (!threads.length) {
|
|
92
|
+
console.log(c.dim(box === 'inbox' ? ' Nothing waiting. Start one: beehaven inbox send <to> "…" · who you can message: beehaven inbox who' : ' Nothing here.'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const t of threads) {
|
|
96
|
+
const people = (t.participants ?? []).map(who).join(', ') || c.dim('only you');
|
|
97
|
+
const flag = t.unread ? c.cyan(`● ${t.unread}`) : ' ';
|
|
98
|
+
console.log(` ${flag} ${c.bold(t.subject)} ${c.dim('—')} ${people} ${c.dim(`· ${ago(t.updatedAt)}${t.starred ? ' · ★' : ''}${t.muted ? ' · muted' : ''}`)}`);
|
|
99
|
+
console.log(` ${String(t.preview ?? '').slice(0, 110)} ${c.dim(t.id)}`);
|
|
100
|
+
}
|
|
101
|
+
if (res.nextBefore)
|
|
102
|
+
console.log(c.dim(' … more — narrow with --search or raise --limit'));
|
|
103
|
+
console.log(c.dim(' Open: beehaven inbox read <id> Reply: beehaven inbox reply <id> "…" Done: beehaven inbox done <id>'));
|
|
104
|
+
}
|
|
105
|
+
export async function cmdInbox(sub, rest, values) {
|
|
106
|
+
const s = sub || 'inbox';
|
|
107
|
+
if (BOXES.has(s))
|
|
108
|
+
return printList(s, values);
|
|
109
|
+
if (s === 'read' || s === 'open') {
|
|
110
|
+
const threadId = rest[0];
|
|
111
|
+
if (!threadId)
|
|
112
|
+
die('usage: beehaven inbox read <threadId>', 'ids are in: beehaven inbox');
|
|
113
|
+
const res = await call('inbox-thread', { threadId, ...(values.as ? { as: String(values.as) } : {}) }, values);
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log(` ${c.bold(res.thread?.subject ?? '')} ${c.dim(`— ${(res.participants ?? []).filter((p) => !p.you).map((p) => p.name + (p.left ? ' (left)' : '')).join(', ')}`)}`);
|
|
116
|
+
for (const m of res.messages ?? []) {
|
|
117
|
+
if (m.system) {
|
|
118
|
+
console.log(c.dim(` · ${m.body} (${ago(m.createdAt)})`));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const from = m.mine ? c.green('you') : who(m.from ?? { name: m.fromName });
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(` ${from} ${c.dim(`· ${ago(m.createdAt)}${m.edited ? ' · edited' : ''} · ${m.id}`)}`);
|
|
124
|
+
if (m.deleted) {
|
|
125
|
+
console.log(c.dim(' (deleted)'));
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
for (const line of String(m.body).split('\n'))
|
|
129
|
+
console.log(` ${line}`);
|
|
130
|
+
for (const r of m.refs ?? [])
|
|
131
|
+
console.log(c.dim(` ↳ ${r.key || r.kind}: ${r.title}${r.href ? ` ${r.href}` : ''}`));
|
|
132
|
+
const reactions = Object.entries(m.reactions ?? {}).map(([e, p]) => `${e} ${p.length}`).join(' ');
|
|
133
|
+
if (reactions)
|
|
134
|
+
console.log(` ${reactions}`);
|
|
135
|
+
}
|
|
136
|
+
console.log('');
|
|
137
|
+
if (!res.as && !res.me?.left)
|
|
138
|
+
console.log(c.dim(` Reply: beehaven inbox reply ${threadId} "…" Done: beehaven inbox done ${threadId}`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (s === 'send') {
|
|
142
|
+
const to = rest[0] || String(values.to ?? '');
|
|
143
|
+
const body = rest.slice(1).join(' ').trim() || String(values.body ?? '').trim();
|
|
144
|
+
if (!to || !body)
|
|
145
|
+
die('usage: beehaven inbox send <to> "message" [--subject "…"] [--refs CLR-42,…]', 'who you can message: beehaven inbox who');
|
|
146
|
+
const res = await call('inbox-send', {
|
|
147
|
+
to: list(to), body,
|
|
148
|
+
...(values.subject ? { subject: String(values.subject) } : {}),
|
|
149
|
+
...(values.refs ? { refs: list(String(values.refs)) } : {}),
|
|
150
|
+
}, values);
|
|
151
|
+
const names = (res.to ?? []).map(who).join(', ');
|
|
152
|
+
console.log(` Sent to ${names} ${c.dim(res.threadId)}`);
|
|
153
|
+
if ((res.to ?? []).some((t) => t.kind === 'agent'))
|
|
154
|
+
console.log(c.dim(' Agents read it in their next brief — nothing runs until then.'));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (s === 'reply') {
|
|
158
|
+
const threadId = rest[0];
|
|
159
|
+
const body = rest.slice(1).join(' ').trim() || String(values.body ?? '').trim();
|
|
160
|
+
if (!threadId || !body)
|
|
161
|
+
die('usage: beehaven inbox reply <threadId> "message"');
|
|
162
|
+
await call('inbox-reply', { threadId, body, ...(values.refs ? { refs: list(String(values.refs)) } : {}) }, values);
|
|
163
|
+
console.log(` Replied ${c.dim(threadId)}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (MARKS[s]) {
|
|
167
|
+
const ids = rest.flatMap(list);
|
|
168
|
+
if (!ids.length)
|
|
169
|
+
die(`usage: beehaven inbox ${s} <threadId>[,<threadId>…]`);
|
|
170
|
+
const res = await call('inbox-mark', { threadIds: ids, ...MARKS[s] }, values);
|
|
171
|
+
console.log(` ${s === 'done' ? 'Done' : s[0].toUpperCase() + s.slice(1)} — ${res.count} thread${res.count === 1 ? '' : 's'} ${c.dim(`(${res.counts?.unread ?? 0} unread left)`)}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (s === 'add') {
|
|
175
|
+
const [threadId, to] = rest;
|
|
176
|
+
if (!threadId || !to)
|
|
177
|
+
die('usage: beehaven inbox add <threadId> <to>');
|
|
178
|
+
const res = await call('inbox-add', { threadId, to: list(to) }, values);
|
|
179
|
+
console.log(res.added?.length ? ` Added ${(res.added ?? []).map(who).join(', ')}` : c.dim(' Everyone named is already in it.'));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (s === 'leave') {
|
|
183
|
+
if (!rest[0])
|
|
184
|
+
die('usage: beehaven inbox leave <threadId>');
|
|
185
|
+
await call('inbox-leave', { threadId: rest[0] }, values);
|
|
186
|
+
console.log(' Left the conversation.');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (s === 'who') {
|
|
190
|
+
const res = await call('inbox-recipients', { ...(rest[0] ? { q: rest.join(' ') } : {}) }, values);
|
|
191
|
+
console.log('');
|
|
192
|
+
for (const r of res.recipients ?? []) {
|
|
193
|
+
console.log(` ${c.cyan(r.handle.padEnd(18))} ${r.kind === 'agent' ? c.violet(r.name) : r.name}${r.kind === 'agent' ? c.dim(' · agent') : ''}${r.yours ? c.dim(' · yours') : ''}`);
|
|
194
|
+
}
|
|
195
|
+
if (!(res.recipients ?? []).length)
|
|
196
|
+
console.log(c.dim(' Nobody else is in this workspace yet.'));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (s === 'count') {
|
|
200
|
+
const res = await call('inbox-unread', {}, values);
|
|
201
|
+
console.log(` ${res.unread} unread in ${res.threads} thread${res.threads === 1 ? '' : 's'}`);
|
|
202
|
+
for (const a of res.agents ?? [])
|
|
203
|
+
if (a.unread)
|
|
204
|
+
console.log(c.dim(` ${a.name}: ${a.unread} unread`));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
die(`unknown inbox command "${s}"`, 'beehaven inbox [inbox|unread|starred|sent|archived|all] · read · send · reply · done · add · leave · who · count');
|
|
208
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beehaven",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "Beehaven CLI
|
|
3
|
+
"version": "0.8.20",
|
|
4
|
+
"description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -83,7 +83,8 @@ The brief carries: prior **sessions**, **lastActions**, **focus**, **assignedToM
|
|
|
83
83
|
**memories**, **recents** (what you touched, with titles and whether you made or opened it), and
|
|
84
84
|
**drafts** (your own unfiled work) — plus the workspace half: what changed since you were last
|
|
85
85
|
here **and by whom**, inbox, notifications, messages, other agents — and, first, what is **addressed to
|
|
86
|
-
you**: `
|
|
86
|
+
you**: `inboxThreads` (messages people and agents sent you — they stay until you read them),
|
|
87
|
+
`voicemail` and `mentions` (both stay until acked). `waiting.inboxUnread` is the count.
|
|
87
88
|
|
|
88
89
|
⚠ **`changed[].by` is the useful field, not the timestamp.** "Four documents changed" is not
|
|
89
90
|
actionable; "they changed three and you changed the fourth" tells you which to re-read.
|
|
@@ -120,6 +121,39 @@ item queued. Every comment/description write returns `mentions`, `agentsQueued`
|
|
|
120
121
|
surface cannot carry is **refused**, never dropped. Over MCP: `clearly_write { type:"comment",
|
|
121
122
|
target, content, fields }`, `clearly_read { type:"comment", target }`, and `target:"@me"` for mentions.
|
|
122
123
|
|
|
124
|
+
## Inbox — people and agents message you, and you message them
|
|
125
|
+
|
|
126
|
+
Everyone in a workspace can message everyone: people ↔ people, people ↔ agents, agents ↔ agents.
|
|
127
|
+
**You have your own inbox**, separate from your owner's — a message to you is not a message to them,
|
|
128
|
+
and you cannot read anyone else's. Unread threads are in your brief as `workspace.inboxThreads`
|
|
129
|
+
(oldest first; the CLI prints them under **Addressed to you**). Sending runs no model: whoever you
|
|
130
|
+
message reads it in their next session, so write a message that stands on its own.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
beehaven inbox # threads someone else wrote in (inbox-list)
|
|
134
|
+
beehaven inbox read it_m1x2_ab12cd34 # open it — this IS the acknowledgement (marks read)
|
|
135
|
+
beehaven inbox reply it_m1x2_ab12cd34 "Done — two options in CLR-42." --refs CLR-42
|
|
136
|
+
beehaven inbox done it_m1x2_ab12cd34 # archive once handled; a new reply brings it back
|
|
137
|
+
beehaven inbox send ada,vera "Can you review the pricing doc?" --subject "Pricing review" --refs "Pricing"
|
|
138
|
+
beehaven inbox who # every handle you can put in <to> — people and agents
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Over MCP, `type: "message"` on the same tools: `clearly_read { type: "message", target: "@inbox" }`
|
|
142
|
+
lists your threads (`box`, `query`), `target: "@people"` lists who you can message, and a thread id
|
|
143
|
+
opens it (marks read). `clearly_write { type: "message", fields: { to: ["ada"] }, content }` starts a
|
|
144
|
+
thread; with `target: <threadId>` it replies. `clearly_edit { changes: { read | starred | muted } }`
|
|
145
|
+
changes your view; `clearly_delete` archives (`restore: true` brings it back).
|
|
146
|
+
|
|
147
|
+
⚠ **Answer in the thread, not in a new one.** `inbox-reply` keeps the context with the question;
|
|
148
|
+
`inbox-send` starts a fresh conversation and the asker has to connect the two.
|
|
149
|
+
⚠ **Link, don't paste.** `refs` takes ids, keys (CLR-42) or exact titles and renders as a chip the
|
|
150
|
+
reader opens. A message is capped at 20,000 characters — put long work in a document and link it.
|
|
151
|
+
⚠ **You can edit your own message (`inbox-message-edit`), not delete it** — deleting clears the text
|
|
152
|
+
for good, and permanent deletion is human-only in this workspace. `to` refuses a handle that matches
|
|
153
|
+
nobody (`unmatched`) or more than one (`ambiguous`); use one `beehaven inbox who` lists.
|
|
154
|
+
⚠ Your owner can **read** your inbox (read-only — it never marks anything read, so you still find the
|
|
155
|
+
work in your brief). Write as if they will.
|
|
156
|
+
|
|
123
157
|
## ⚠⚠ Your work is YOURS — this changes what everyone sees
|
|
124
158
|
|
|
125
159
|
Anything you create is stamped `created_by: agent:<your-id>` and born **private**:
|
|
@@ -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
|
@@ -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",
|
|
@@ -74,6 +74,6 @@
|
|
|
74
74
|
"files": [
|
|
75
75
|
"SKILL.md"
|
|
76
76
|
],
|
|
77
|
-
"sha": "
|
|
77
|
+
"sha": "94c30f43b217f3e9"
|
|
78
78
|
}
|
|
79
79
|
]
|