@clearlysh/beehaven 0.8.21 → 0.8.23
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/agent-sessions.js +1 -1
- package/dist/build-channel.js +2 -2
- package/dist/cli.js +39 -5
- package/dist/commands/agent-instructions.js +182 -0
- package/dist/commands/agent.js +5 -1
- package/dist/commands/help.js +6 -26
- package/dist/commands/inbox.js +1 -1
- package/dist/commands/swarm.js +7 -217
- package/dist/commands/workspace.js +16 -0
- package/dist/daemon.js +22 -76
- package/dist/ipc-server.js +1 -1
- package/dist/relay.js +4 -36
- package/dist/reserved-type.js +60 -0
- package/dist/verb-surface.js +1 -0
- package/package.json +1 -1
- package/skills/clearly-agent/SKILL.md +5 -4
- package/skills/index.json +1 -1
package/dist/agent-sessions.js
CHANGED
|
@@ -210,7 +210,7 @@ export class AgentSessions {
|
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
212
|
const base = this.wsBase().replace(/\/$/, '');
|
|
213
|
-
const url = `${base}/my/
|
|
213
|
+
const url = `${base}/my/workspace/ws?token=${encodeURIComponent(spec.token)}&clientType=agent&client=agent&deviceId=agent-${encodeURIComponent(agentId)}`;
|
|
214
214
|
let ws;
|
|
215
215
|
let opened = false;
|
|
216
216
|
let settled = false;
|
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 = '8585349e92e9-dirty';
|
|
57
57
|
/** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
|
|
58
|
-
export const BUILD_TIME = '2026-
|
|
58
|
+
export const BUILD_TIME = '2026-10-02T00:47Z';
|
|
59
59
|
/**
|
|
60
60
|
* The build identity as one short string, for `--version`.
|
|
61
61
|
*
|
package/dist/cli.js
CHANGED
|
@@ -309,7 +309,8 @@ ${DEV_ONLY_HELP ? ` PROJECT — build your own Durable Object backend (HIVE_DE
|
|
|
309
309
|
app setup <name> Re-run a hive's setup script
|
|
310
310
|
install <pkg> Shorthand for "app install <pkg>"
|
|
311
311
|
skills install Teach every agent about Clearly —
|
|
312
|
-
writes the shipped SKILL.md files
|
|
312
|
+
writes the shipped SKILL.md files + the
|
|
313
|
+
AGENTS.md / GEMINI.md / CLAUDE.md block
|
|
313
314
|
skills <list|sync|remove> Manage skills paired with installed hives
|
|
314
315
|
|
|
315
316
|
Deep dive: beehaven help hives Mental model + install/connect/call loop
|
|
@@ -437,7 +438,9 @@ ${DEV_ONLY_HELP ? `
|
|
|
437
438
|
ACCOUNT
|
|
438
439
|
─────────────────────────────────────────────────────────────────────
|
|
439
440
|
|
|
440
|
-
login Sign in (browser) — primary flow
|
|
441
|
+
login Sign in (browser) — primary flow; also teaches
|
|
442
|
+
your agents (skills + AGENTS.md block;
|
|
443
|
+
--no-agent-setup to skip)
|
|
441
444
|
login --email <e> --password <p> Self-hosted email/password (set BEEHAVEN_API_URL)
|
|
442
445
|
logout Clear local credentials + keychain
|
|
443
446
|
signup Open sign-up page in browser
|
|
@@ -1492,6 +1495,7 @@ async function run() {
|
|
|
1492
1495
|
// Paired Claude Code skill install policy (see commands/skills.ts)
|
|
1493
1496
|
'with-skills': { type: 'boolean', default: false },
|
|
1494
1497
|
'no-skills': { type: 'boolean', default: false },
|
|
1498
|
+
'no-agent-setup': { type: 'boolean', default: false },
|
|
1495
1499
|
// ── mobile-install metadata for `beehaven teleport <build>.ipa --composition <id>`
|
|
1496
1500
|
// (see commands/workspace.ts — wantMobileInstall branch).
|
|
1497
1501
|
// Future: parse Info.plist / AndroidManifest.xml automatically
|
|
@@ -1950,6 +1954,7 @@ async function run() {
|
|
|
1950
1954
|
* flag with two meanings is worse than a refusal that names the alternative.
|
|
1951
1955
|
*/
|
|
1952
1956
|
'ticket',
|
|
1957
|
+
'inbox',
|
|
1953
1958
|
/**
|
|
1954
1959
|
* ⚠ `trash` — CBH2-34. It pages `document-list { archived: true }` and honours `--limit` up to
|
|
1955
1960
|
* 500, announcing the clamp when the ask is higher. Left off this list the flag is REFUSED,
|
|
@@ -3302,6 +3307,31 @@ async function run() {
|
|
|
3302
3307
|
*/
|
|
3303
3308
|
console.log(` ${color.green('✓')} Connected — your new sign-in is live`);
|
|
3304
3309
|
console.log(` Background service ${wasRunning ? 'restarted' : 'started'} (PID ${pid})`);
|
|
3310
|
+
/**
|
|
3311
|
+
* ⚠⚠ TEACH THE AGENTS AS PART OF SIGNING IN (PPO-190). "Install the CLI, log in, and your
|
|
3312
|
+
* agents handle the rest" needs the agents to KNOW about the CLI — the skills plus the
|
|
3313
|
+
* instruction block in ~/.codex/AGENTS.md, ~/.gemini/GEMINI.md and a pointer in
|
|
3314
|
+
* ~/.claude/CLAUDE.md. That used to be a separate `beehaven skills install` the installer's
|
|
3315
|
+
* closing text asked for, and a step people skip is a step that did not happen.
|
|
3316
|
+
*
|
|
3317
|
+
* ⚠ Idempotent and non-fatal: unchanged files are not rewritten, symlinked files are left
|
|
3318
|
+
* alone, and a failure here never fails the sign-in. Opt out with `--no-agent-setup` or
|
|
3319
|
+
* BEEHAVEN_NO_AGENT_SETUP=1; undo with `beehaven skills uninstall`.
|
|
3320
|
+
*/
|
|
3321
|
+
if (!values['no-agent-setup'] && process.env.BEEHAVEN_NO_AGENT_SETUP !== '1') {
|
|
3322
|
+
try {
|
|
3323
|
+
const { installAgentSkills } = await import('./commands/agent-skills.js');
|
|
3324
|
+
const { installAgentInstructions } = await import('./commands/agent-instructions.js');
|
|
3325
|
+
const sk = installAgentSkills();
|
|
3326
|
+
const ins = installAgentInstructions();
|
|
3327
|
+
const changed = [...sk.steps, ...ins.steps].filter((x) => /=(written|updated)$/.test(x)).length;
|
|
3328
|
+
console.log(` ${color.green('✓')} Your agents know about Clearly${changed ? '' : color.dim(' (already set up)')}`);
|
|
3329
|
+
console.log(color.dim(' Skills + instructions for Claude Code, Codex, Gemini and pi · undo: beehaven skills uninstall'));
|
|
3330
|
+
}
|
|
3331
|
+
catch (err) {
|
|
3332
|
+
console.log(color.dim(` Agent setup skipped: ${err?.message ?? err} — run: beehaven skills install`));
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3305
3335
|
console.log('');
|
|
3306
3336
|
break;
|
|
3307
3337
|
}
|
|
@@ -3896,7 +3926,13 @@ complete -F _beehaven_completions beehaven`);
|
|
|
3896
3926
|
*/
|
|
3897
3927
|
if (sub === 'install' || sub === 'uninstall') {
|
|
3898
3928
|
const { installAgentSkills, removeAgentSkills, AGENT_SKILL_DIRS } = await import('./commands/agent-skills.js');
|
|
3929
|
+
const { installAgentInstructions, removeAgentInstructions } = await import('./commands/agent-instructions.js');
|
|
3899
3930
|
const r = sub === 'install' ? installAgentSkills() : removeAgentSkills();
|
|
3931
|
+
// ⚠ The instruction block travels with the skills (PPO-190) — a skill is only OFFERED;
|
|
3932
|
+
// the block is what makes an agent reach for `beehaven` at all.
|
|
3933
|
+
const ins = sub === 'install' ? installAgentInstructions() : removeAgentInstructions();
|
|
3934
|
+
r.steps.push(...ins.steps);
|
|
3935
|
+
r.warnings.push(...ins.warnings);
|
|
3900
3936
|
const wrote = r.steps.filter((x) => /=(written|updated)$/.test(x)).length;
|
|
3901
3937
|
const current = r.steps.filter((x) => x.endsWith('=current')).length;
|
|
3902
3938
|
const removed = r.steps.filter((x) => x.endsWith('=removed')).length;
|
|
@@ -6647,9 +6683,7 @@ complete -F _beehaven_completions beehaven`);
|
|
|
6647
6683
|
break;
|
|
6648
6684
|
}
|
|
6649
6685
|
case 'swarm': {
|
|
6650
|
-
//
|
|
6651
|
-
// dispatch work to worker beehives. One namespace for the whole mesh
|
|
6652
|
-
// (`hive`/`hives` already name the local-apps concept).
|
|
6686
|
+
// Queen and Omni identity commands remain; node registry/dispatch was retired.
|
|
6653
6687
|
const { cmdSwarm } = await import('./commands/swarm.js');
|
|
6654
6688
|
await cmdSwarm(positionals, values);
|
|
6655
6689
|
break;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-instructions — the "use the beehaven CLI" block in each agent's GLOBAL instruction file.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ THE MISSING HALF OF "install the CLI, log in, and your agents handle the rest" (PPO-190).
|
|
5
|
+
* `skills install` put SKILL.md files on disk, but a skill is only OFFERED to a model when its
|
|
6
|
+
* description matches. The instruction block is what makes an agent reach for `beehaven` at all
|
|
7
|
+
* when the user says "put this on my board". The Mac app has written it on every launch
|
|
8
|
+
* (`apps/electron/electron/agent-skills-installer.ts`); the CLI never did, so a CLI-only user's
|
|
9
|
+
* Codex and Gemini had skills they never used.
|
|
10
|
+
*
|
|
11
|
+
* ⚠⚠ BYTE-IDENTICAL TO THE MAC APP'S BLOCK, MARKER LINE INCLUDED. Both writers splice between the
|
|
12
|
+
* same BLOCK_START / BLOCK_END. If their text differed by one character, a machine with both would
|
|
13
|
+
* rewrite the file on every app launch and every CLI install, each undoing the other. One text
|
|
14
|
+
* means both converge and either uninstaller removes it. `agentInstructionParity.test.mjs` in the
|
|
15
|
+
* root repo pins the two copies together.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ CLAUDE GETS THE SHORT POINTER — `~/.claude/CLAUDE.md` is prepended to every Claude Code
|
|
18
|
+
* session in every repository, so it carries a pointer to the skills, not a manual. Same rule as
|
|
19
|
+
* the Mac app; see its note on `AGENT_INSTRUCTION_FILES`.
|
|
20
|
+
*
|
|
21
|
+
* ⚠ ONLY OUR BLOCK IS EVER TOUCHED. Text around it is preserved byte for byte; a file with no
|
|
22
|
+
* block gets one appended; removal deletes the block and nothing else.
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'fs';
|
|
25
|
+
import os from 'os';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
const MANAGED_MARKER = 'managed-by: clearly-mac-app';
|
|
28
|
+
export const BLOCK_START = '<!-- clearly:beehaven-cli:start -->';
|
|
29
|
+
export const BLOCK_END = '<!-- clearly:beehaven-cli:end -->';
|
|
30
|
+
/** Resolved at call time so tests can point HOME at a temp dir. */
|
|
31
|
+
export function agentInstructionFiles(home = os.homedir()) {
|
|
32
|
+
return [
|
|
33
|
+
{ agent: 'codex', file: path.join(home, '.codex', 'AGENTS.md') },
|
|
34
|
+
{ agent: 'gemini', file: path.join(home, '.gemini', 'GEMINI.md') },
|
|
35
|
+
{ agent: 'claude', file: path.join(home, '.claude', 'CLAUDE.md') },
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
/** The managed block appended to AGENTS.md / GEMINI.md. Keep identical to the Mac app's. */
|
|
39
|
+
export function instructionBlock() {
|
|
40
|
+
return [
|
|
41
|
+
BLOCK_START,
|
|
42
|
+
`<!-- ${MANAGED_MARKER}. Edits inside this block are overwritten on app launch; edit outside it. -->`,
|
|
43
|
+
'',
|
|
44
|
+
'## Clearly — drive it with the `beehaven` CLI',
|
|
45
|
+
'',
|
|
46
|
+
'Clearly (clearly.sh) is a workspace — documents, projects, boards, tickets and a design',
|
|
47
|
+
'canvas. Reach it from the shell with the `beehaven` CLI, NOT an HTTP API you construct.',
|
|
48
|
+
'',
|
|
49
|
+
'⚠ SIGN IN FIRST. Every call is attributed to an agent identity, and without one the CLI',
|
|
50
|
+
'refuses. `login` MINTS the name if it does not exist yet, and binds this terminal:',
|
|
51
|
+
'',
|
|
52
|
+
'```sh',
|
|
53
|
+
'beehaven agent login <your-name> # required — mints + binds, once per session',
|
|
54
|
+
'beehaven connect home # target the personal workspace',
|
|
55
|
+
'beehaven actions # list every action available there',
|
|
56
|
+
'beehaven call <action> \'{...}\'',
|
|
57
|
+
'```',
|
|
58
|
+
'',
|
|
59
|
+
'Read the full skills before driving it — they carry the exact verbs and node shapes:',
|
|
60
|
+
'',
|
|
61
|
+
'- `~/.<agent>/skills/clearly-agent/SKILL.md` — **read this first**: identity, sessions, your brief',
|
|
62
|
+
'- `~/.<agent>/skills/beehaven-cli/SKILL.md` — connect / call / sql / logs, the daemon',
|
|
63
|
+
'- `~/.<agent>/skills/clearly-workspace/SKILL.md` — audit what a workspace holds and who made it',
|
|
64
|
+
'- `~/.<agent>/skills/clearly-canvas/SKILL.md` — build + perceive a canvas, comm blocks, code diffs',
|
|
65
|
+
'- `~/.<agent>/skills/design-craft/SKILL.md` — **load before any visual work**: grid, type, colour, the studio pass',
|
|
66
|
+
'- `~/.<agent>/skills/layout-systems/SKILL.md` — grids and systems across many artboards',
|
|
67
|
+
'- `~/.<agent>/skills/pair-on-canvas/SKILL.md` — drive a coding task from a spec on the canvas',
|
|
68
|
+
'- `~/.<agent>/skills/codebase-map/SKILL.md` — draw a repo’s architecture on the canvas',
|
|
69
|
+
'',
|
|
70
|
+
'If a command reports the daemon is unreachable, `beehaven login` re-auths and',
|
|
71
|
+
'`beehaven start --headless` starts a daemon when the Mac app is not running.',
|
|
72
|
+
BLOCK_END,
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
/** The short form, for `~/.claude/CLAUDE.md`. Keep identical to the Mac app's `claudeBlock()`. */
|
|
76
|
+
export function claudeBlock() {
|
|
77
|
+
return [
|
|
78
|
+
BLOCK_START,
|
|
79
|
+
`<!-- ${MANAGED_MARKER}. Edits inside this block are overwritten on app launch; edit outside it. -->`,
|
|
80
|
+
'',
|
|
81
|
+
'**Clearly workspace** — drive it from the shell with the `beehaven` CLI (documents, projects,',
|
|
82
|
+
'boards, tickets, canvas). Sign in once per session first: `beehaven agent login <name>` —',
|
|
83
|
+
'it mints the identity if new. Full detail is in the skills, `clearly-agent` first.',
|
|
84
|
+
BLOCK_END,
|
|
85
|
+
].join('\n');
|
|
86
|
+
}
|
|
87
|
+
/** Insert or replace our block, leaving everything around it untouched. Pure. */
|
|
88
|
+
export function upsertBlock(existing, block) {
|
|
89
|
+
const start = existing.indexOf(BLOCK_START);
|
|
90
|
+
const end = existing.indexOf(BLOCK_END);
|
|
91
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
92
|
+
return existing.slice(0, start) + block + existing.slice(end + BLOCK_END.length);
|
|
93
|
+
}
|
|
94
|
+
return existing.trim() ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
|
|
95
|
+
}
|
|
96
|
+
/** Remove our block (and the blank line it was appended after). Pure. */
|
|
97
|
+
export function stripBlock(existing) {
|
|
98
|
+
const start = existing.indexOf(BLOCK_START);
|
|
99
|
+
const end = existing.indexOf(BLOCK_END);
|
|
100
|
+
if (start === -1 || end === -1 || end < start)
|
|
101
|
+
return existing;
|
|
102
|
+
const before = existing.slice(0, start).replace(/\n{1,2}$/, '\n');
|
|
103
|
+
const after = existing.slice(end + BLOCK_END.length).replace(/^\n/, '');
|
|
104
|
+
const out = (before + after);
|
|
105
|
+
return out.trim() ? out : '';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Write the block into every agent's global instruction file. Idempotent: an unchanged file is
|
|
109
|
+
* reported `=current` and not rewritten.
|
|
110
|
+
*
|
|
111
|
+
* ⚠ A SYMLINKED INSTRUCTION FILE IS LEFT ALONE. People keep dotfiles in a repo and link them in;
|
|
112
|
+
* writing through the link would silently edit their checkout. Same rule as the skills installer.
|
|
113
|
+
*/
|
|
114
|
+
export function installAgentInstructions(home = os.homedir()) {
|
|
115
|
+
const steps = [];
|
|
116
|
+
const warnings = [];
|
|
117
|
+
for (const { agent, file } of agentInstructionFiles(home)) {
|
|
118
|
+
try {
|
|
119
|
+
/**
|
|
120
|
+
* ⚠ ONLY FOR AGENTS THAT ARE INSTALLED. Creating `~/.gemini/GEMINI.md` for someone without
|
|
121
|
+
* Gemini invents an agent: the skills installer keys on that same directory existing, so
|
|
122
|
+
* the NEXT run would install skills for it too. Same rule as `installAgentSkills`.
|
|
123
|
+
*/
|
|
124
|
+
if (!fs.existsSync(path.dirname(file))) {
|
|
125
|
+
steps.push(`${agent}/instructions=agent-absent`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (isSymlink(file)) {
|
|
129
|
+
steps.push(`${agent}/instructions=symlink-skipped`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let existing = '';
|
|
133
|
+
try {
|
|
134
|
+
existing = fs.readFileSync(file, 'utf8');
|
|
135
|
+
}
|
|
136
|
+
catch { /* first run */ }
|
|
137
|
+
const next = upsertBlock(existing, agent === 'claude' ? claudeBlock() : instructionBlock());
|
|
138
|
+
if (next === existing) {
|
|
139
|
+
steps.push(`${agent}/instructions=current`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
fs.writeFileSync(file, next, { mode: 0o644 });
|
|
143
|
+
steps.push(`${agent}/instructions=${existing ? 'updated' : 'written'}`);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
warnings.push(`${agent}/instructions: ${e?.message ?? String(e)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { steps, warnings };
|
|
150
|
+
}
|
|
151
|
+
/** Remove only our block. A file that becomes empty is deleted only if we created all of it. */
|
|
152
|
+
export function removeAgentInstructions(home = os.homedir()) {
|
|
153
|
+
const steps = [];
|
|
154
|
+
const warnings = [];
|
|
155
|
+
for (const { agent, file } of agentInstructionFiles(home)) {
|
|
156
|
+
try {
|
|
157
|
+
if (!fs.existsSync(file) || isSymlink(file))
|
|
158
|
+
continue;
|
|
159
|
+
const existing = fs.readFileSync(file, 'utf8');
|
|
160
|
+
const next = stripBlock(existing);
|
|
161
|
+
if (next === existing)
|
|
162
|
+
continue;
|
|
163
|
+
if (next)
|
|
164
|
+
fs.writeFileSync(file, next);
|
|
165
|
+
else
|
|
166
|
+
fs.unlinkSync(file);
|
|
167
|
+
steps.push(`${agent}/instructions=removed`);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
warnings.push(`${agent}/instructions: ${e?.message ?? String(e)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { steps, warnings };
|
|
174
|
+
}
|
|
175
|
+
function isSymlink(file) {
|
|
176
|
+
try {
|
|
177
|
+
return fs.lstatSync(file).isSymbolicLink();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -845,7 +845,11 @@ async function agentLogin(args, values) {
|
|
|
845
845
|
console.error(` ${yellow('⚠')} Signed in, but this workspace refused ${cyan(name)}: ${outcome.reason}`);
|
|
846
846
|
if (/revoked|not active|unknown agent|not admitted/i.test(`${outcome.scope} ${outcome.reason}`)) {
|
|
847
847
|
console.error(dim(` The agent is archived or not admitted here. Restore it in Settings → Agent access,`));
|
|
848
|
-
|
|
848
|
+
// ⚠ The workspace names the live identity in its refusal (PPO-190). The old advice —
|
|
849
|
+
// `beehaven call agent-list` — ran AS this refused identity and was refused in turn.
|
|
850
|
+
if (!/Log in as one:/.test(outcome.reason)) {
|
|
851
|
+
console.error(dim(` or log in as an active agent: beehaven agent login <name> (the Settings → Agent access page lists them).`));
|
|
852
|
+
}
|
|
849
853
|
}
|
|
850
854
|
console.error(dim(` Calls from this session run as ${name} and will be refused — they will NOT fall back to another agent.`));
|
|
851
855
|
process.exitCode = 1;
|
package/dist/commands/help.js
CHANGED
|
@@ -1830,42 +1830,22 @@ const PAGES = {
|
|
|
1830
1830
|
related: ['app', 'pipe', 'pwd'],
|
|
1831
1831
|
},
|
|
1832
1832
|
swarm: {
|
|
1833
|
-
synopsis: 'beehaven swarm <
|
|
1834
|
-
description: `The
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
WORKER hives execute the work the queen dispatches. This machine registers
|
|
1838
|
-
itself when the daemon connects — the Mac defaults to QUEEN, a
|
|
1839
|
-
headless/container node to WORKER.
|
|
1840
|
-
|
|
1841
|
-
Mesh subcommands:
|
|
1842
|
-
list Every beehive: role, kind, hives, online state
|
|
1843
|
-
queen Which NODE is orchestrating right now
|
|
1844
|
-
role <queen|worker> Make THIS machine the queen (or a worker)
|
|
1845
|
-
whoami This machine's beehive identity
|
|
1846
|
-
dispatch <cap> <action> [json] Route work to the best worker (--target pins one)
|
|
1847
|
-
forget <deviceId> Drop a stale OFFLINE node
|
|
1848
|
-
|
|
1849
|
-
The bees (AI):
|
|
1833
|
+
synopsis: 'beehaven swarm <bees|sample-honey|forge-omni> [args]',
|
|
1834
|
+
description: `The two AI bees. The unfinished node registry and dispatch
|
|
1835
|
+
protocol were retired; they do not route work.
|
|
1836
|
+
|
|
1850
1837
|
bees 👑 Queen + 🐝 Omni status
|
|
1851
1838
|
sample-honey 👑 Queen ingests the WHOLE workspace corpus
|
|
1852
1839
|
(memories, brain, prompts, brand voice) and gets fat
|
|
1853
1840
|
forge-omni [--force] 👑 Queen distills a LEAN copy of you → 🐝 Omni,
|
|
1854
1841
|
your personal-assistant AI TWIN (--force re-samples)
|
|
1855
1842
|
|
|
1856
|
-
|
|
1857
|
-
the AI). Omni is the owner's twin: log in as it with \`agent login omni-bee\`.
|
|
1858
|
-
Distinct from \`hives\` (local apps) — a beehive is a whole NODE.`,
|
|
1843
|
+
Omni is the owner's twin: log in as it with \`agent login omni-bee\`.`,
|
|
1859
1844
|
flags: [
|
|
1860
|
-
{
|
|
1861
|
-
name: '--target <deviceId>',
|
|
1862
|
-
desc: 'dispatch: pin to a specific beehive instead of auto-selecting',
|
|
1863
|
-
},
|
|
1864
1845
|
{ name: '--force', desc: 'forge-omni: re-sample the honey before distilling' },
|
|
1865
1846
|
],
|
|
1866
1847
|
examples: [
|
|
1867
|
-
{ cmd: 'beehaven swarm
|
|
1868
|
-
{ cmd: 'beehaven swarm role queen', note: 'make this machine the queen' },
|
|
1848
|
+
{ cmd: 'beehaven swarm bees', note: 'see Queen and Omni' },
|
|
1869
1849
|
{ cmd: 'beehaven swarm sample-honey', note: 'Queen ingests everything' },
|
|
1870
1850
|
{ cmd: 'beehaven swarm forge-omni', note: 'forge your AI twin' },
|
|
1871
1851
|
{ cmd: 'beehaven agent login omni-bee', note: 'log in as your twin' },
|
package/dist/commands/inbox.js
CHANGED
|
@@ -82,7 +82,7 @@ async function call(action, input, values) {
|
|
|
82
82
|
async function printList(box, values) {
|
|
83
83
|
const res = await call('inbox-list', {
|
|
84
84
|
box, ...(values.search ? { q: String(values.search) } : {}), ...(values.as ? { as: String(values.as) } : {}),
|
|
85
|
-
...(values.limit ? { limit: Number(values.limit) } : {}),
|
|
85
|
+
...(values.limit !== undefined ? { limit: Number(values.limit) } : {}),
|
|
86
86
|
}, values);
|
|
87
87
|
const threads = res.threads ?? [];
|
|
88
88
|
const counts = res.counts ?? {};
|
package/dist/commands/swarm.js
CHANGED
|
@@ -1,26 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* swarm
|
|
3
|
-
*
|
|
4
|
-
* A **beehive** is a node running the daemon + hives (this Mac, a headless
|
|
5
|
-
* server, or a Cloudflare container). A **queen hive** orchestrates; **worker
|
|
6
|
-
* hives** execute. This machine registers itself automatically when the daemon
|
|
7
|
-
* connects (see daemon.ts `registerBeehive`) — these verbs let you SEE the
|
|
8
|
-
* swarm, designate roles, and route work.
|
|
9
|
-
*
|
|
10
|
-
* beehaven swarm list # every beehive: role, kind, hives, online
|
|
11
|
-
* beehaven swarm queen # who's orchestrating
|
|
12
|
-
* beehaven swarm role <queen|worker> # make THIS machine the queen (or a worker)
|
|
13
|
-
* beehaven swarm whoami # this machine's beehive identity
|
|
14
|
-
* beehaven swarm dispatch <cap> <action> [json] # route work to the best worker
|
|
15
|
-
* beehaven swarm forget <deviceId> # drop a stale offline node
|
|
16
|
-
*
|
|
17
|
-
* All cloud state lives in the always-on Home DO (rpc/swarm-rpcs.ts); these
|
|
18
|
-
* verbs reach it through the daemon over IPC via `rpcOnce`. (One namespace on
|
|
19
|
-
* purpose — `hive`/`hives` already name the local-apps concept.)
|
|
2
|
+
* swarm — the Queen and Omni identity commands. The unfinished node registry and
|
|
3
|
+
* dispatch protocol were retired; these two bees remain independent of it.
|
|
20
4
|
*/
|
|
21
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
-
import { homedir, hostname, platform } from 'node:os';
|
|
23
|
-
import { join } from 'node:path';
|
|
24
5
|
import { rpcOnce } from '../ipc-client.js';
|
|
25
6
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
26
7
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
@@ -28,50 +9,28 @@ const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
|
28
9
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
29
10
|
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
30
11
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
31
|
-
const CONFIG_DIR = join(homedir(), '.beehaven');
|
|
32
|
-
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
33
|
-
/** This machine's stable beehive id — MUST match relay.ts `deviceId`. */
|
|
34
|
-
function thisDeviceId() {
|
|
35
|
-
return `${hostname()}-${platform()}`;
|
|
36
|
-
}
|
|
37
|
-
function loadConfig() {
|
|
38
|
-
try {
|
|
39
|
-
return existsSync(CONFIG_FILE) ? JSON.parse(readFileSync(CONFIG_FILE, 'utf8')) : {};
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return {};
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function saveConfig(update) {
|
|
46
|
-
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
47
|
-
writeFileSync(CONFIG_FILE, JSON.stringify({ ...loadConfig(), ...update }, null, 2));
|
|
48
|
-
}
|
|
49
12
|
/** Unwrap the daemon's { result } envelope (rpcOnce sometimes returns it raw). */
|
|
50
13
|
function unwrap(r) {
|
|
51
14
|
return r?.result ?? r ?? {};
|
|
52
15
|
}
|
|
53
16
|
// ══════════════════════════════ swarm ══════════════════════════════
|
|
54
17
|
export async function cmdSwarm(positionals, values) {
|
|
55
|
-
const sub = positionals[1] ?? '
|
|
56
|
-
const rest = positionals.slice(2);
|
|
18
|
+
const sub = positionals[1] ?? 'bees';
|
|
57
19
|
switch (sub) {
|
|
58
20
|
case 'list':
|
|
59
21
|
case 'ls':
|
|
60
|
-
return swarmList();
|
|
61
22
|
case 'queen':
|
|
62
|
-
return swarmQueen();
|
|
63
23
|
case 'role':
|
|
64
|
-
return swarmRole(rest);
|
|
65
24
|
case 'whoami':
|
|
66
25
|
case 'status':
|
|
67
26
|
case 'me':
|
|
68
|
-
return swarmWhoami();
|
|
69
27
|
case 'dispatch':
|
|
70
28
|
case 'run':
|
|
71
|
-
return swarmDispatch(rest, values);
|
|
72
29
|
case 'forget':
|
|
73
30
|
case 'rm':
|
|
74
|
-
|
|
31
|
+
console.log(' The node swarm registry and dispatch protocol were retired.');
|
|
32
|
+
console.log(dim(' Use beehaven swarm bees | sample-honey | forge-omni for Queen and Omni.'));
|
|
33
|
+
return;
|
|
75
34
|
// The bees — 👑 Queen (samples all the honey) + 🐝 Omni (the owner's twin).
|
|
76
35
|
case 'bees':
|
|
77
36
|
return swarmBees();
|
|
@@ -83,121 +42,8 @@ export async function cmdSwarm(positionals, values) {
|
|
|
83
42
|
return swarmForgeOmni(values);
|
|
84
43
|
default:
|
|
85
44
|
console.log(` Unknown swarm subcommand '${sub}'.`);
|
|
86
|
-
console.log(dim(' Try: beehaven swarm
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
async function swarmList() {
|
|
90
|
-
let res;
|
|
91
|
-
try {
|
|
92
|
-
res = unwrap(await rpcOnce('swarm-list', {}, 8_000));
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
console.log(red(' Cannot reach the swarm.') + dim(' Is the daemon running? beehaven status'));
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const beehives = res?.beehives ?? [];
|
|
99
|
-
if (!beehives.length) {
|
|
100
|
-
console.log('');
|
|
101
|
-
console.log(' 🐝 No beehives registered yet.');
|
|
102
|
-
console.log(dim(' This machine registers automatically when the daemon connects.'));
|
|
103
|
-
console.log(dim(' Bring more online: `beehaven start --headless` on a server, or a cloud container.'));
|
|
104
|
-
console.log('');
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const me = thisDeviceId();
|
|
108
|
-
console.log('');
|
|
109
|
-
console.log(` 🐝 ${bold('Swarm')} ${dim(`— ${res.online}/${res.count} online`)}`);
|
|
110
|
-
console.log(dim(' ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
111
|
-
for (const b of beehives) {
|
|
112
|
-
const dot = b.online ? green('●') : dim('○');
|
|
113
|
-
const crown = b.role === 'queen' ? '👑' : '🐝';
|
|
114
|
-
const mine = b.deviceId === me ? cyan(' ← this machine') : '';
|
|
115
|
-
const caps = (b.capabilities ?? []).slice(0, 8).join(', ') || dim('no hives');
|
|
116
|
-
console.log(` ${dot} ${crown} ${cyan(String(b.hostname).padEnd(22))} ${dim(String(b.kind).padEnd(15))} ${(b.role === 'queen' ? yellow('queen') : 'worker').padEnd(7)}${mine}`);
|
|
117
|
-
console.log(` ${dim(b.deviceId)}`);
|
|
118
|
-
console.log(` ${dim('hives:')} ${caps}${b.load ? dim(` load:${b.load}`) : ''}`);
|
|
119
|
-
}
|
|
120
|
-
console.log('');
|
|
121
|
-
console.log(dim(` queen: ${res.queen ? green(res.queen) : 'none — promote one with `beehaven swarm role queen`'}`));
|
|
122
|
-
console.log('');
|
|
123
|
-
}
|
|
124
|
-
async function swarmQueen() {
|
|
125
|
-
let res;
|
|
126
|
-
try {
|
|
127
|
-
res = unwrap(await rpcOnce('swarm-queen', {}, 8_000));
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
console.log(red(' Cannot reach the swarm.'));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const q = res?.queen;
|
|
134
|
-
console.log('');
|
|
135
|
-
if (!q) {
|
|
136
|
-
console.log(' 👑 No queen designated.');
|
|
137
|
-
console.log(dim(' Make this machine the queen: beehaven swarm role queen'));
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
console.log(` 👑 ${bold('Queen')} — ${cyan(q.hostname)} ${q.online ? green('(online)') : dim('(offline)')}`);
|
|
141
|
-
console.log(` ${dim(q.deviceId)} · ${dim(q.kind)}`);
|
|
142
|
-
console.log(` ${dim('hives:')} ${(q.capabilities ?? []).join(', ') || dim('none')}`);
|
|
143
|
-
}
|
|
144
|
-
console.log('');
|
|
145
|
-
}
|
|
146
|
-
async function swarmDispatch(rest, values) {
|
|
147
|
-
const capability = rest[0];
|
|
148
|
-
const action = rest[1];
|
|
149
|
-
if (!capability || !action) {
|
|
150
|
-
console.log(' Usage: beehaven swarm dispatch <capability> <action> [json]');
|
|
151
|
-
console.log(dim(' capability = the hive to run (e.g. motion-studio, voice)'));
|
|
152
|
-
console.log(dim(' e.g. beehaven swarm dispatch voice synthesize \'{"text":"hi"}\''));
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
let input = {};
|
|
156
|
-
if (rest[2]) {
|
|
157
|
-
try {
|
|
158
|
-
input = JSON.parse(rest[2]);
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
console.log(red(' Invalid JSON payload.'));
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
45
|
+
console.log(dim(' Try: beehaven swarm bees | sample-honey | forge-omni'));
|
|
164
46
|
}
|
|
165
|
-
const targetDeviceId = typeof values?.target === 'string' ? values.target : undefined;
|
|
166
|
-
console.log(dim(` → dispatching ${capability}:${action}${targetDeviceId ? ` @${targetDeviceId}` : ' to the best worker'}…`));
|
|
167
|
-
let res;
|
|
168
|
-
try {
|
|
169
|
-
res = unwrap(await rpcOnce('swarm-dispatch', { capability, action, input, targetDeviceId }, 300_000));
|
|
170
|
-
}
|
|
171
|
-
catch (e) {
|
|
172
|
-
console.log(red(` Dispatch failed: ${e?.message ?? e}`));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (res?.error || res?.ok === false) {
|
|
176
|
-
console.log(red(` ✗ ${res.error ?? 'dispatch failed'}`) + (res.code ? dim(` [${res.code}]`) : ''));
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
console.log(green(` ✓ ${capability}:${action}`) + dim(` on ${res.targetDeviceId} · ${res.durationMs}ms`));
|
|
180
|
-
console.log(JSON.stringify(res.result, null, 2));
|
|
181
|
-
}
|
|
182
|
-
async function swarmForget(rest) {
|
|
183
|
-
const deviceId = rest[0];
|
|
184
|
-
if (!deviceId) {
|
|
185
|
-
console.log(' Usage: beehaven swarm forget <deviceId>');
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
let res;
|
|
189
|
-
try {
|
|
190
|
-
res = unwrap(await rpcOnce('swarm-forget', { deviceId }, 8_000));
|
|
191
|
-
}
|
|
192
|
-
catch (e) {
|
|
193
|
-
console.log(red(` ${e?.message ?? e}`));
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (res?.error) {
|
|
197
|
-
console.log(red(` ✗ ${res.error}`));
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
console.log(green(` ✓ forgot beehive ${deviceId}`));
|
|
201
47
|
}
|
|
202
48
|
// ══════════════════════════════ the bees ══════════════════════════════
|
|
203
49
|
// 👑 Queen samples ALL the honey (the full corpus, gets fat); 🐝 Omni is the
|
|
@@ -283,59 +129,3 @@ async function swarmForgeOmni(values) {
|
|
|
283
129
|
console.log(dim(` Log in as your twin: ${res.loginCommand || 'beehaven agent login omni-bee'}`));
|
|
284
130
|
console.log('');
|
|
285
131
|
}
|
|
286
|
-
// ─────────────────────── this machine's role/identity ───────────────────────
|
|
287
|
-
async function swarmRole(rest) {
|
|
288
|
-
const role = (rest[0] ?? '').toLowerCase();
|
|
289
|
-
if (role !== 'queen' && role !== 'worker') {
|
|
290
|
-
const current = loadConfig().role ?? (process.env.BEEHAVEN_HEADLESS === '1' ? 'worker' : 'queen');
|
|
291
|
-
console.log(' Usage: beehaven swarm role <queen|worker>');
|
|
292
|
-
console.log(dim(` This machine is currently: ${current === 'queen' ? yellow('👑 queen') : 'worker'}`));
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
// 1) Persist locally so it survives restarts + the daemon re-registers with it.
|
|
296
|
-
saveConfig({ role });
|
|
297
|
-
// 2) Update the cloud row immediately so the swarm reflects it now (the daemon
|
|
298
|
-
// catches up its own role on its next register, ~2 min, or `beehaven restart`).
|
|
299
|
-
const deviceId = thisDeviceId();
|
|
300
|
-
let synced = false;
|
|
301
|
-
try {
|
|
302
|
-
const r = unwrap(await rpcOnce('hive-set-role', { deviceId, role }, 8_000));
|
|
303
|
-
synced = !!r?.ok;
|
|
304
|
-
}
|
|
305
|
-
catch {
|
|
306
|
-
/* daemon offline — local config still saved; applies on next connect */
|
|
307
|
-
}
|
|
308
|
-
console.log('');
|
|
309
|
-
console.log(` ${green('✓')} This machine is now the ${role === 'queen' ? yellow('👑 queen hive') : 'worker hive'}.`);
|
|
310
|
-
console.log(dim(synced
|
|
311
|
-
? ' Synced to the swarm. It orchestrates from here.'
|
|
312
|
-
: ' Saved locally — applies when the daemon next connects (beehaven restart to apply now).'));
|
|
313
|
-
console.log('');
|
|
314
|
-
}
|
|
315
|
-
async function swarmWhoami() {
|
|
316
|
-
const cfg = loadConfig();
|
|
317
|
-
const deviceId = thisDeviceId();
|
|
318
|
-
const role = cfg.role ?? (process.env.BEEHAVEN_HEADLESS === '1' ? 'worker' : 'queen');
|
|
319
|
-
const kind = process.env.BEEHAVEN_KIND ?? (process.env.BEEHAVEN_HEADLESS === '1' ? 'headless' : 'local-mac');
|
|
320
|
-
console.log('');
|
|
321
|
-
console.log(` 🐝 ${bold('This beehive')}`);
|
|
322
|
-
console.log(` deviceId: ${cyan(deviceId)}`);
|
|
323
|
-
console.log(` role: ${role === 'queen' ? yellow('👑 queen') : 'worker'} ${dim('(beehaven swarm role <queen|worker>)')}`);
|
|
324
|
-
console.log(` kind: ${kind}`);
|
|
325
|
-
// Best-effort live view from the swarm registry.
|
|
326
|
-
try {
|
|
327
|
-
const res = unwrap(await rpcOnce('swarm-list', {}, 5_000));
|
|
328
|
-
const me = (res?.beehives ?? []).find((b) => b.deviceId === deviceId);
|
|
329
|
-
if (me) {
|
|
330
|
-
console.log(` online: ${me.online ? green('yes') : dim('no')}`);
|
|
331
|
-
console.log(` hives: ${(me.capabilities ?? []).join(', ') || dim('none')}`);
|
|
332
|
-
}
|
|
333
|
-
else {
|
|
334
|
-
console.log(dim(' not yet registered in the swarm — the daemon registers on connect.'));
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
catch {
|
|
338
|
-
/* daemon offline */
|
|
339
|
-
}
|
|
340
|
-
console.log('');
|
|
341
|
-
}
|
|
@@ -860,6 +860,22 @@ export async function cmdCall(action, rawInput, values) {
|
|
|
860
860
|
input = { ...input, action: canonical, name: canonical };
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
|
+
// ⚠ `type` IS THE WIRE'S ACTION NAME (PPO-190): a top-level `type` would be overwritten by the
|
|
864
|
+
// daemon's `{ ...input, type: action }` and lost with `ok: true`. Move it to the verb's own field
|
|
865
|
+
// or refuse with the right spelling — never drop it in silence. See reserved-type.ts.
|
|
866
|
+
{
|
|
867
|
+
const { reconcileReservedType } = await import('../reserved-type.js');
|
|
868
|
+
const r = reconcileReservedType(action, input);
|
|
869
|
+
if (r.kind === 'refused') {
|
|
870
|
+
console.error(` ${r.error}`);
|
|
871
|
+
process.exit(1);
|
|
872
|
+
}
|
|
873
|
+
if (r.kind === 'moved') {
|
|
874
|
+
input = r.input;
|
|
875
|
+
if (!values.json)
|
|
876
|
+
console.error(` · ${r.note}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
863
879
|
// Confirm spend BEFORE resolving a building or touching the daemon — a cancelled call must
|
|
864
880
|
// cost nothing and behave identically whether the input came from argv, a file, or stdin.
|
|
865
881
|
// It runs after the parse because the gate is input-aware: `engine:'handroll'` and any call
|
package/dist/daemon.js
CHANGED
|
@@ -2501,9 +2501,27 @@ export async function startDaemon(opts) {
|
|
|
2501
2501
|
});
|
|
2502
2502
|
});
|
|
2503
2503
|
// Local client raw sends → forward to DO via relay
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2504
|
+
const ipcLiveQueries = new Map();
|
|
2505
|
+
ipc.on('send', (msg, clientId) => {
|
|
2506
|
+
if (!relay.isConnected() || !relay.wsSend(msg))
|
|
2507
|
+
return;
|
|
2508
|
+
const queryId = typeof msg?.queryId === 'string' ? msg.queryId : '';
|
|
2509
|
+
if (!queryId || !clientId)
|
|
2510
|
+
return;
|
|
2511
|
+
if (msg.type === 'sql:subscribe') {
|
|
2512
|
+
if (!ipcLiveQueries.has(clientId)) {
|
|
2513
|
+
ipcLiveQueries.set(clientId, new Set());
|
|
2514
|
+
ipc.onClientDisconnect(clientId, () => {
|
|
2515
|
+
for (const ownedId of ipcLiveQueries.get(clientId) ?? []) {
|
|
2516
|
+
relay.wsSend({ type: 'sql:unsubscribe', queryId: ownedId });
|
|
2517
|
+
}
|
|
2518
|
+
ipcLiveQueries.delete(clientId);
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
ipcLiveQueries.get(clientId).add(queryId);
|
|
2522
|
+
}
|
|
2523
|
+
else if (msg.type === 'sql:unsubscribe') {
|
|
2524
|
+
ipcLiveQueries.get(clientId)?.delete(queryId);
|
|
2507
2525
|
}
|
|
2508
2526
|
});
|
|
2509
2527
|
// Local client HTTP proxy: make a fetch with the daemon's Firebase ID
|
|
@@ -3059,57 +3077,6 @@ export async function startDaemon(opts) {
|
|
|
3059
3077
|
if (daemonAppManager) {
|
|
3060
3078
|
relay.sendLocalAppStatus(daemonAppManager.status());
|
|
3061
3079
|
}
|
|
3062
|
-
// Capabilities may have changed (install/uninstall) — re-advertise this
|
|
3063
|
-
// node's beehive record so the swarm's dispatch matching stays accurate.
|
|
3064
|
-
registerBeehive();
|
|
3065
|
-
}
|
|
3066
|
-
// ── Distributed hive mesh — advertise this daemon as a beehive ──
|
|
3067
|
-
//
|
|
3068
|
-
// The node declares its ROLE (queen orchestrates / worker executes), its
|
|
3069
|
-
// KIND (this Mac / a headless server / a cloud container), and its
|
|
3070
|
-
// CAPABILITIES (the hives it can run — the dispatch match keys). The user's
|
|
3071
|
-
// Mac defaults to QUEEN (the natural orchestrator — where the human is); a
|
|
3072
|
-
// headless/container daemon defaults to WORKER. `beehaven swarm role` (config
|
|
3073
|
-
// `role`) or BEEHAVEN_ROLE overrides. See rpc/swarm-rpcs.ts for the queen
|
|
3074
|
-
// + dispatch side.
|
|
3075
|
-
function beehiveRole() {
|
|
3076
|
-
const env = (process.env.BEEHAVEN_ROLE || '').toLowerCase();
|
|
3077
|
-
if (env === 'queen' || env === 'worker')
|
|
3078
|
-
return env;
|
|
3079
|
-
const cfgRole = String(loadConfig()?.role || '').toLowerCase();
|
|
3080
|
-
if (cfgRole === 'queen' || cfgRole === 'worker')
|
|
3081
|
-
return cfgRole;
|
|
3082
|
-
return process.env.BEEHAVEN_HEADLESS === '1' ? 'worker' : 'queen';
|
|
3083
|
-
}
|
|
3084
|
-
function beehiveKind() {
|
|
3085
|
-
const env = (process.env.BEEHAVEN_KIND || '').toLowerCase();
|
|
3086
|
-
if (env === 'local-mac' || env === 'headless' || env === 'cloud-container')
|
|
3087
|
-
return env;
|
|
3088
|
-
return process.env.BEEHAVEN_HEADLESS === '1' ? 'headless' : 'local-mac';
|
|
3089
|
-
}
|
|
3090
|
-
function beehiveCapabilities() {
|
|
3091
|
-
try {
|
|
3092
|
-
const s = daemonAppManager?.status();
|
|
3093
|
-
const caps = new Set(Array.isArray(s?.buildingTypes) ? s.buildingTypes : []);
|
|
3094
|
-
// Surface app names too so `swarm list` reads friendly (motion-studio,
|
|
3095
|
-
// voice, recorder …) even before a building instance has spun up.
|
|
3096
|
-
for (const a of s?.apps ?? [])
|
|
3097
|
-
if (a?.name)
|
|
3098
|
-
caps.add(String(a.name));
|
|
3099
|
-
return [...caps];
|
|
3100
|
-
}
|
|
3101
|
-
catch {
|
|
3102
|
-
return [];
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
function registerBeehive() {
|
|
3106
|
-
if (!relay.isConnected())
|
|
3107
|
-
return;
|
|
3108
|
-
relay.sendSwarmRegister({
|
|
3109
|
-
role: beehiveRole(),
|
|
3110
|
-
kind: beehiveKind(),
|
|
3111
|
-
capabilities: beehiveCapabilities(),
|
|
3112
|
-
});
|
|
3113
3080
|
}
|
|
3114
3081
|
/** Sync a single uninstall to the Home DO. */
|
|
3115
3082
|
function syncUninstallToCloud(name) {
|
|
@@ -4220,18 +4187,6 @@ export async function startDaemon(opts) {
|
|
|
4220
4187
|
setTimeout(() => {
|
|
4221
4188
|
syncAvatarFromCloud().catch(() => { });
|
|
4222
4189
|
}, 250);
|
|
4223
|
-
// Register as a beehive the moment the WS opens so the node appears in
|
|
4224
|
-
// the swarm immediately; syncInstalledAppsToCloud + the periodic tick
|
|
4225
|
-
// below re-register with the full capability list once hives finish
|
|
4226
|
-
// loading. Deferred slightly so the capability scan sees loaded hives.
|
|
4227
|
-
setTimeout(() => {
|
|
4228
|
-
try {
|
|
4229
|
-
registerBeehive();
|
|
4230
|
-
}
|
|
4231
|
-
catch {
|
|
4232
|
-
/* best-effort */
|
|
4233
|
-
}
|
|
4234
|
-
}, 500);
|
|
4235
4190
|
// #111 — if startup rehydrated a Claude credential from keychain,
|
|
4236
4191
|
// tell the worker now that the WS is open. Worker handler at
|
|
4237
4192
|
// `case 'canvas-credential-rehydrated':` refreshes daemon_confirmed_at
|
|
@@ -4430,16 +4385,7 @@ export async function startDaemon(opts) {
|
|
|
4430
4385
|
setInterval(() => checkAndAutoUpdate(), UPDATE_CHECK_INTERVAL);
|
|
4431
4386
|
// Reset the LOCAL metrics window every 5 minutes (no network).
|
|
4432
4387
|
setInterval(() => metrics.resetWindow(), 5 * 60_000);
|
|
4433
|
-
|
|
4434
|
-
* ⚠⚠ NO TIMED FRAMES TO THE CLOUD (2026-09-29). Two intervals lived here: a `telemetry` frame
|
|
4435
|
-
* and a `swarm-register` re-advertise, each every 2 minutes. Neither had a consumer — the
|
|
4436
|
-
* workspace has no handler for either type and nothing inserts into `beehives` — but each one
|
|
4437
|
-
* woke the user's Hive, reopened its portal (the portal idle-closes after 60 s), woke the
|
|
4438
|
-
* workspace and re-hydrated all ~111 tables. Measured on staging: that kept one Hive resident
|
|
4439
|
-
* ~50% of every hour per connected daemon (~11K GB-s/day) and made the workspace re-read ~15K
|
|
4440
|
-
* rows per reopen. An idle daemon must cost nothing; it speaks when something happens.
|
|
4441
|
-
* Registration still runs on connect and whenever installed apps change (above).
|
|
4442
|
-
*/
|
|
4388
|
+
// No timed telemetry or swarm-register frames: an idle daemon must cost nothing.
|
|
4443
4389
|
// Start REPL
|
|
4444
4390
|
if (opts?.repl !== false && !process.env.BEEHAVEN_NO_REPL)
|
|
4445
4391
|
repl.start();
|
package/dist/ipc-server.js
CHANGED
|
@@ -471,7 +471,7 @@ export class IpcServer extends EventEmitter {
|
|
|
471
471
|
break;
|
|
472
472
|
case 'send':
|
|
473
473
|
// Forward raw message to relay → DO
|
|
474
|
-
this.emit('send', msg.msg);
|
|
474
|
+
this.emit('send', msg.msg, client.id);
|
|
475
475
|
break;
|
|
476
476
|
case 'app-rpc':
|
|
477
477
|
// Forward RPC to a local app building (not the cloud relay).
|
package/dist/relay.js
CHANGED
|
@@ -522,21 +522,17 @@ export class Relay {
|
|
|
522
522
|
this.scheduleReconnect();
|
|
523
523
|
return;
|
|
524
524
|
}
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
// transparently, so every workspace RPC (canvas-invoke, actions, table sync) round-trips
|
|
529
|
-
// exactly as before — plus the daemon shares the same socket the web app uses. (Plain
|
|
530
|
-
// synchronous URL — no pre-connect HTTP hop — so nothing can stall the dial.)
|
|
525
|
+
// Workspace traffic terminates on the workspace DO itself. A Hive portal can close after
|
|
526
|
+
// 60 s while leaving the daemon socket open; then cloud pushes and local-tool requests
|
|
527
|
+
// disappear until the daemon sends another frame. The direct socket hibernates safely.
|
|
531
528
|
const params = new URLSearchParams({
|
|
532
529
|
token,
|
|
533
530
|
client: process.env.BEEHAVEN_CLIENT || 'daemon',
|
|
534
531
|
clientType: process.env.BEEHAVEN_CLIENT_TYPE || 'daemon',
|
|
535
|
-
wsType: 'workspace',
|
|
536
532
|
});
|
|
537
533
|
if (this.deviceId)
|
|
538
534
|
params.set('deviceId', String(this.deviceId).slice(0, 64));
|
|
539
|
-
const url = `${this.wsBase()}/my/
|
|
535
|
+
const url = `${this.wsBase()}/my/workspace/ws?${params.toString()}`;
|
|
540
536
|
const ws = new WebSocket(url);
|
|
541
537
|
// Wait for open or first error/close before resolving
|
|
542
538
|
await new Promise((resolve) => {
|
|
@@ -1228,34 +1224,6 @@ export class Relay {
|
|
|
1228
1224
|
sendLocalAppStatus(status) {
|
|
1229
1225
|
this.wsSend({ type: 'local-app-status', ...status });
|
|
1230
1226
|
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Advertise this daemon as a BEEHIVE in the distributed hive mesh.
|
|
1233
|
-
*
|
|
1234
|
-
* The Home DO's `case 'swarm-register'` upserts a durable node row (keyed by
|
|
1235
|
-
* this stable deviceId) + maps deviceId → this live session, which is how the
|
|
1236
|
-
* swarm knows the node is online and how the queen targets a dispatch at it.
|
|
1237
|
-
* Re-sent on connect, whenever capabilities change, and on a periodic cadence
|
|
1238
|
-
* (see daemon.ts `registerBeehive`) so it also serves as the beehive's
|
|
1239
|
-
* keepalive and self-heals after a DO hibernation dropped the live map.
|
|
1240
|
-
*
|
|
1241
|
-
* role 'queen' (orchestrator) | 'worker' (executor)
|
|
1242
|
-
* kind 'local-mac' | 'headless' | 'cloud-container'
|
|
1243
|
-
* capabilities the hive/building names this node can run (dispatch match keys)
|
|
1244
|
-
*/
|
|
1245
|
-
sendSwarmRegister(opts) {
|
|
1246
|
-
if (!this.connected)
|
|
1247
|
-
return;
|
|
1248
|
-
this.wsSend({
|
|
1249
|
-
type: 'swarm-register',
|
|
1250
|
-
deviceId: this.deviceId,
|
|
1251
|
-
role: opts.role,
|
|
1252
|
-
kind: opts.kind,
|
|
1253
|
-
capabilities: opts.capabilities,
|
|
1254
|
-
hostname: hostname(),
|
|
1255
|
-
os: platform(),
|
|
1256
|
-
root: homedir(),
|
|
1257
|
-
});
|
|
1258
|
-
}
|
|
1259
1227
|
/** Start periodic token self-refresh (every 50 min, for headless/long-running daemons) */
|
|
1260
1228
|
tokenRefreshTimer = null;
|
|
1261
1229
|
startTokenRefresh() {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reserved-type — `type` is the WIRE'S action name, so a top-level `type` in a call's input is lost.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ SILENT DATA LOSS (PPO-190). The daemon sends `{ ...input, type: action }`. An agent that calls
|
|
5
|
+
* `ticket-create '{"title":"…","type":"bug"}'` gets `ok: true` and a ticket that is NOT a bug: its
|
|
6
|
+
* `type` was overwritten by `"ticket-create"` before the workspace ever saw it. AGENTS.md has
|
|
7
|
+
* carried "use `issueType`, not `type`" as a warning, which is a rule every new agent has to be
|
|
8
|
+
* told and every one of them breaks once.
|
|
9
|
+
*
|
|
10
|
+
* The verbs already accept a non-colliding spelling (`issueType`, `linkType`, `proposalType`,
|
|
11
|
+
* `kind`). This moves `type` onto that spelling when the verb has one, and refuses with the right
|
|
12
|
+
* spelling when it does not — either way, nothing is dropped in silence.
|
|
13
|
+
*/
|
|
14
|
+
/** Verb → the field the workspace reads instead of `type`. */
|
|
15
|
+
export const TYPE_ALIASES = {
|
|
16
|
+
// Each entry is a verb whose handler reads this field (verified against apps/cloudflare/src).
|
|
17
|
+
'ticket-create': 'issueType',
|
|
18
|
+
'ticket-update': 'issueType',
|
|
19
|
+
'ticket-link-create': 'linkType',
|
|
20
|
+
'proposal-create': 'proposalType',
|
|
21
|
+
'composition-create': 'kind',
|
|
22
|
+
'record-thought': 'kind',
|
|
23
|
+
'thought-record': 'kind',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Reconcile a top-level `type` with the wire's use of the name. Pure.
|
|
27
|
+
*
|
|
28
|
+
* ⚠ A `type` EQUAL TO THE ACTION is left alone — that is a caller spelling the action twice, which
|
|
29
|
+
* the wire already agrees with. Nested `type`s (canvas nodes, `updates.type`) never collide and are
|
|
30
|
+
* never inspected.
|
|
31
|
+
*/
|
|
32
|
+
export function reconcileReservedType(action, input) {
|
|
33
|
+
if (!input || typeof input !== 'object' || !Object.prototype.hasOwnProperty.call(input, 'type'))
|
|
34
|
+
return { kind: 'none' };
|
|
35
|
+
const value = input.type;
|
|
36
|
+
if (value === undefined || value === action)
|
|
37
|
+
return { kind: 'none' };
|
|
38
|
+
const field = TYPE_ALIASES[action];
|
|
39
|
+
if (field) {
|
|
40
|
+
const { type: _dropped, ...rest } = input;
|
|
41
|
+
if (rest[field] !== undefined && rest[field] !== value) {
|
|
42
|
+
return {
|
|
43
|
+
kind: 'refused',
|
|
44
|
+
error: `both "type" (${JSON.stringify(value)}) and "${field}" (${JSON.stringify(rest[field])}) were given — `
|
|
45
|
+
+ `"type" is the action name on the wire and would be dropped; pass only "${field}".`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: 'moved',
|
|
50
|
+
field,
|
|
51
|
+
input: { ...rest, [field]: value },
|
|
52
|
+
note: `"type" is reserved for the action name on the wire — sent as "${field}": ${JSON.stringify(value)}`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
kind: 'refused',
|
|
57
|
+
error: `"type" is reserved for the action name on the wire, so ${action} would never see ${JSON.stringify(value)}. `
|
|
58
|
+
+ `Use the verb's own field (see: beehaven describe ${action}).`,
|
|
59
|
+
};
|
|
60
|
+
}
|
package/dist/verb-surface.js
CHANGED
|
@@ -140,6 +140,7 @@ export const VERB_SURFACE = {
|
|
|
140
140
|
describe: { cls: 'BEYOND', why: 'alias', aliasOf: 'explain' },
|
|
141
141
|
rpc: { cls: 'BEYOND', why: 'the raw gateway envelope, below `call`' },
|
|
142
142
|
ticket: { cls: 'BEYOND', why: 'resolves a board before creating, and refuses a numeric priority the API inverts' },
|
|
143
|
+
inbox: { cls: 'PROVISIONAL', why: 'reads and manages workspace message threads; keep until door traffic is measured' },
|
|
143
144
|
comment: { cls: 'BEYOND', why: 'addresses ANY surface by key/title and turns per-surface anchor flags into one call; prints who the @s reached' },
|
|
144
145
|
comments: { cls: 'BEYOND', why: 'served by the comment module', aliasOf: 'comment' },
|
|
145
146
|
mention: { cls: 'BEYOND', why: 'resolves an 8-char id prefix before acking, and reads your mention queue as whichever identity you are' },
|
package/package.json
CHANGED
|
@@ -211,7 +211,8 @@ ask the owner.
|
|
|
211
211
|
`beehaven call agent-list '{}'` shows the live roster; log in as one of those.
|
|
212
212
|
- ⚠ **`workspace-sql` takes `query`**, not `sql`. The wrong key is silently dropped and you get the
|
|
213
213
|
SCHEMA back — `ok: true`, a confident list of tables, not your answer.
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
-
|
|
217
|
-
|
|
214
|
+
- **`document-create` takes `markdown` or `html`.** `content`, `body` and `text` are accepted as
|
|
215
|
+
markdown aliases, but prefer `markdown`.
|
|
216
|
+
- **`ticket-create` / `ticket-update` take `issueType` and `body`.** The wire uses `type` as the action
|
|
217
|
+
name, so the CLI moves a top-level `type` to `issueType` for you and refuses it on verbs with no
|
|
218
|
+
such field. `description` is accepted as `body`.
|