beehaven 0.8.19 → 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.
@@ -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 = '42cf394385d8';
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-27T18:08Z';
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.
@@ -6434,6 +6438,12 @@ complete -F _beehaven_completions beehaven`);
6434
6438
  await cmdMention(positionals[1] ?? '', positionals.slice(2), values);
6435
6439
  break;
6436
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
+ }
6437
6447
  case 'ticket':
6438
6448
  case 'board': {
6439
6449
  const { cmdTicket, cmdBoard } = await import('./commands/ticket.js');
@@ -897,9 +897,17 @@ function printBrief(brief) {
897
897
  */
898
898
  const mentions = brief.workspace?.mentions ?? [];
899
899
  const voicemail = brief.workspace?.voicemail ?? [];
900
- if (mentions.length || voicemail.length) {
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
  }
@@ -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]',
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "beehaven",
3
- "version": "0.8.19",
3
+ "version": "0.8.20",
4
4
  "description": "Beehaven CLI — command your Clearly workspace, agents, and canvas from the terminal",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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**: `voicemail` and `mentions` (both stay until acked).
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**:
package/skills/index.json CHANGED
@@ -74,6 +74,6 @@
74
74
  "files": [
75
75
  "SKILL.md"
76
76
  ],
77
- "sha": "89e5a04a420defe3"
77
+ "sha": "94c30f43b217f3e9"
78
78
  }
79
79
  ]