beehaven 0.8.21 → 0.8.24
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 +72 -7
- package/dist/commands/agent-instructions.js +182 -0
- package/dist/commands/agent.js +5 -1
- package/dist/commands/canvas-image.js +57 -3
- package/dist/commands/daemon-cmd.js +32 -0
- 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/scripts/sync-skills.mjs +1 -0
- package/skills/README.md +20 -18
- package/skills/brand-identity/SKILL.md +248 -0
- package/skills/clearly-agent/SKILL.md +14 -4
- package/skills/clearly-canvas/SKILL.md +39 -2
- package/skills/clearly-canvas/authoring.md +16 -5
- package/skills/design-craft/SKILL.md +4 -0
- package/skills/index.json +10 -3
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 = '150d0d2c336a';
|
|
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-02T08:34Z';
|
|
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
|
|
@@ -1299,7 +1302,25 @@ async function run() {
|
|
|
1299
1302
|
await shorthandCmd(rawVerb, process.argv.slice(3), {});
|
|
1300
1303
|
return;
|
|
1301
1304
|
}
|
|
1305
|
+
/**
|
|
1306
|
+
* ⚠ `--x -3000` IS A NEGATIVE COORDINATE, NOT A FLAG. node's parseArgs reads any value starting
|
|
1307
|
+
* with a dash as a missing argument and dies with "Option '--x' argument is ambiguous" — which is
|
|
1308
|
+
* how `beehaven canvas add-image … --x -3000` failed (CDS-25). Join a negative NUMBER onto the
|
|
1309
|
+
* coordinate/size flag before it; nothing else is rewritten.
|
|
1310
|
+
*/
|
|
1311
|
+
const NUMERIC_FLAGS = new Set(['--x', '--y', '--width', '--height', '--w', '--h', '--dx', '--dy']);
|
|
1312
|
+
const argv = [];
|
|
1313
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
1314
|
+
const a = process.argv[i], b = process.argv[i + 1];
|
|
1315
|
+
if (NUMERIC_FLAGS.has(a) && typeof b === 'string' && /^-\d+(\.\d+)?$/.test(b)) {
|
|
1316
|
+
argv.push(`${a}=${b}`);
|
|
1317
|
+
i++;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
argv.push(a);
|
|
1321
|
+
}
|
|
1302
1322
|
const { values, positionals } = parseArgs({
|
|
1323
|
+
args: argv,
|
|
1303
1324
|
allowPositionals: true,
|
|
1304
1325
|
options: {
|
|
1305
1326
|
project: { type: 'string' },
|
|
@@ -1492,6 +1513,7 @@ async function run() {
|
|
|
1492
1513
|
// Paired Claude Code skill install policy (see commands/skills.ts)
|
|
1493
1514
|
'with-skills': { type: 'boolean', default: false },
|
|
1494
1515
|
'no-skills': { type: 'boolean', default: false },
|
|
1516
|
+
'no-agent-setup': { type: 'boolean', default: false },
|
|
1495
1517
|
// ── mobile-install metadata for `beehaven teleport <build>.ipa --composition <id>`
|
|
1496
1518
|
// (see commands/workspace.ts — wantMobileInstall branch).
|
|
1497
1519
|
// Future: parse Info.plist / AndroidManifest.xml automatically
|
|
@@ -1950,6 +1972,7 @@ async function run() {
|
|
|
1950
1972
|
* flag with two meanings is worse than a refusal that names the alternative.
|
|
1951
1973
|
*/
|
|
1952
1974
|
'ticket',
|
|
1975
|
+
'inbox',
|
|
1953
1976
|
/**
|
|
1954
1977
|
* ⚠ `trash` — CBH2-34. It pages `document-list { archived: true }` and honours `--limit` up to
|
|
1955
1978
|
* 500, announcing the clamp when the ask is higher. Left off this list the flag is REFUSED,
|
|
@@ -2944,11 +2967,24 @@ async function run() {
|
|
|
2944
2967
|
// omitted BEEHAVEN_HEADLESS=1, so the "restarted" daemon hit the
|
|
2945
2968
|
// parent-death watchdog and exited ~3s later — restart reported a PID and
|
|
2946
2969
|
// left you with nothing running. See spawnStandaloneDaemon's comment.
|
|
2947
|
-
const { spawnStandaloneDaemon } = await import('./commands/daemon-cmd.js');
|
|
2970
|
+
const { spawnStandaloneDaemon, waitForServingDaemon } = await import('./commands/daemon-cmd.js');
|
|
2948
2971
|
const { resolveRelayUrlForDaemon } = await import('./daemon.js');
|
|
2949
2972
|
const relayUrl = resolveRelayUrlForDaemon();
|
|
2950
2973
|
const pid = spawnStandaloneDaemon(values);
|
|
2951
|
-
|
|
2974
|
+
// ⚠ Report the daemon that is ACTUALLY serving — see waitForServingDaemon.
|
|
2975
|
+
const served = await waitForServingDaemon(pid);
|
|
2976
|
+
if (served.ready && served.spawnedAlive) {
|
|
2977
|
+
console.log(` Daemon restarted (PID ${pid}). Ready — socket answering.`);
|
|
2978
|
+
}
|
|
2979
|
+
else if (served.ready) {
|
|
2980
|
+
console.log(` Another supervisor (the Clearly app) restarted the daemon first — PID ${served.livePids.join(', ')} is serving. Ready.`);
|
|
2981
|
+
}
|
|
2982
|
+
else {
|
|
2983
|
+
console.error(served.livePids.length
|
|
2984
|
+
? ` Daemon process is up (PID ${served.livePids.join(', ')}) but its socket is not answering yet. 'beehaven status' shows state.`
|
|
2985
|
+
: ` The daemon did not stay up (spawned PID ${pid} exited). Run 'beehaven start --headless', or reopen the Clearly app.`);
|
|
2986
|
+
process.exitCode = 1;
|
|
2987
|
+
}
|
|
2952
2988
|
console.log(` Relay: ${color.dim(relayUrl)}`);
|
|
2953
2989
|
break;
|
|
2954
2990
|
}
|
|
@@ -3302,6 +3338,31 @@ async function run() {
|
|
|
3302
3338
|
*/
|
|
3303
3339
|
console.log(` ${color.green('✓')} Connected — your new sign-in is live`);
|
|
3304
3340
|
console.log(` Background service ${wasRunning ? 'restarted' : 'started'} (PID ${pid})`);
|
|
3341
|
+
/**
|
|
3342
|
+
* ⚠⚠ TEACH THE AGENTS AS PART OF SIGNING IN (PPO-190). "Install the CLI, log in, and your
|
|
3343
|
+
* agents handle the rest" needs the agents to KNOW about the CLI — the skills plus the
|
|
3344
|
+
* instruction block in ~/.codex/AGENTS.md, ~/.gemini/GEMINI.md and a pointer in
|
|
3345
|
+
* ~/.claude/CLAUDE.md. That used to be a separate `beehaven skills install` the installer's
|
|
3346
|
+
* closing text asked for, and a step people skip is a step that did not happen.
|
|
3347
|
+
*
|
|
3348
|
+
* ⚠ Idempotent and non-fatal: unchanged files are not rewritten, symlinked files are left
|
|
3349
|
+
* alone, and a failure here never fails the sign-in. Opt out with `--no-agent-setup` or
|
|
3350
|
+
* BEEHAVEN_NO_AGENT_SETUP=1; undo with `beehaven skills uninstall`.
|
|
3351
|
+
*/
|
|
3352
|
+
if (!values['no-agent-setup'] && process.env.BEEHAVEN_NO_AGENT_SETUP !== '1') {
|
|
3353
|
+
try {
|
|
3354
|
+
const { installAgentSkills } = await import('./commands/agent-skills.js');
|
|
3355
|
+
const { installAgentInstructions } = await import('./commands/agent-instructions.js');
|
|
3356
|
+
const sk = installAgentSkills();
|
|
3357
|
+
const ins = installAgentInstructions();
|
|
3358
|
+
const changed = [...sk.steps, ...ins.steps].filter((x) => /=(written|updated)$/.test(x)).length;
|
|
3359
|
+
console.log(` ${color.green('✓')} Your agents know about Clearly${changed ? '' : color.dim(' (already set up)')}`);
|
|
3360
|
+
console.log(color.dim(' Skills + instructions for Claude Code, Codex, Gemini and pi · undo: beehaven skills uninstall'));
|
|
3361
|
+
}
|
|
3362
|
+
catch (err) {
|
|
3363
|
+
console.log(color.dim(` Agent setup skipped: ${err?.message ?? err} — run: beehaven skills install`));
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3305
3366
|
console.log('');
|
|
3306
3367
|
break;
|
|
3307
3368
|
}
|
|
@@ -3896,7 +3957,13 @@ complete -F _beehaven_completions beehaven`);
|
|
|
3896
3957
|
*/
|
|
3897
3958
|
if (sub === 'install' || sub === 'uninstall') {
|
|
3898
3959
|
const { installAgentSkills, removeAgentSkills, AGENT_SKILL_DIRS } = await import('./commands/agent-skills.js');
|
|
3960
|
+
const { installAgentInstructions, removeAgentInstructions } = await import('./commands/agent-instructions.js');
|
|
3899
3961
|
const r = sub === 'install' ? installAgentSkills() : removeAgentSkills();
|
|
3962
|
+
// ⚠ The instruction block travels with the skills (PPO-190) — a skill is only OFFERED;
|
|
3963
|
+
// the block is what makes an agent reach for `beehaven` at all.
|
|
3964
|
+
const ins = sub === 'install' ? installAgentInstructions() : removeAgentInstructions();
|
|
3965
|
+
r.steps.push(...ins.steps);
|
|
3966
|
+
r.warnings.push(...ins.warnings);
|
|
3900
3967
|
const wrote = r.steps.filter((x) => /=(written|updated)$/.test(x)).length;
|
|
3901
3968
|
const current = r.steps.filter((x) => x.endsWith('=current')).length;
|
|
3902
3969
|
const removed = r.steps.filter((x) => x.endsWith('=removed')).length;
|
|
@@ -6647,9 +6714,7 @@ complete -F _beehaven_completions beehaven`);
|
|
|
6647
6714
|
break;
|
|
6648
6715
|
}
|
|
6649
6716
|
case 'swarm': {
|
|
6650
|
-
//
|
|
6651
|
-
// dispatch work to worker beehives. One namespace for the whole mesh
|
|
6652
|
-
// (`hive`/`hives` already name the local-apps concept).
|
|
6717
|
+
// Queen and Omni identity commands remain; node registry/dispatch was retired.
|
|
6653
6718
|
const { cmdSwarm } = await import('./commands/swarm.js');
|
|
6654
6719
|
await cmdSwarm(positionals, values);
|
|
6655
6720
|
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;
|
|
@@ -25,6 +25,50 @@ function coordinateFlag(raw, label) {
|
|
|
25
25
|
throw new Error(`${label} must be a number`);
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* ⚠ NATIVE SIZE IS READ HERE, FROM THE FILE'S OWN HEADER. The docs promise "omit both size flags to
|
|
30
|
+
* use the image's native dimensions", but nothing measured the image, so the server's placeholder
|
|
31
|
+
* box won: a 1032×320 PNG landed as a 400×400 node with its aspect squashed (CDS-26). PNG, GIF,
|
|
32
|
+
* JPEG and WebP carry their size in the first bytes; AVIF (ISO-BMFF) falls back to the server.
|
|
33
|
+
*/
|
|
34
|
+
export function imageSize(buf, mime) {
|
|
35
|
+
try {
|
|
36
|
+
if (mime === 'image/png' && buf.length >= 24 && buf.toString('ascii', 12, 16) === 'IHDR') {
|
|
37
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
38
|
+
}
|
|
39
|
+
if (mime === 'image/gif' && buf.length >= 10)
|
|
40
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) };
|
|
41
|
+
if (mime === 'image/webp' && buf.length >= 30 && buf.toString('ascii', 8, 12) === 'WEBP') {
|
|
42
|
+
const kind = buf.toString('ascii', 12, 16);
|
|
43
|
+
if (kind === 'VP8X')
|
|
44
|
+
return { width: 1 + buf.readUIntLE(24, 3), height: 1 + buf.readUIntLE(27, 3) };
|
|
45
|
+
if (kind === 'VP8L') {
|
|
46
|
+
const b = buf.readUInt32LE(21);
|
|
47
|
+
return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1 };
|
|
48
|
+
}
|
|
49
|
+
if (kind === 'VP8 ')
|
|
50
|
+
return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff };
|
|
51
|
+
}
|
|
52
|
+
if (mime === 'image/jpeg' && buf[0] === 0xff && buf[1] === 0xd8) {
|
|
53
|
+
let i = 2;
|
|
54
|
+
while (i + 9 < buf.length) {
|
|
55
|
+
if (buf[i] !== 0xff) {
|
|
56
|
+
i++;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const marker = buf[i + 1];
|
|
60
|
+
const len = buf.readUInt16BE(i + 2);
|
|
61
|
+
// SOF0–SOF15 except DHT(C4), JPG(C8), DAC(CC) carry the frame size.
|
|
62
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
63
|
+
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
64
|
+
}
|
|
65
|
+
i += 2 + len;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch { /* fall back to the server's default */ }
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
28
72
|
function absoluteRelayUrl(url) {
|
|
29
73
|
if (!url.startsWith('/'))
|
|
30
74
|
return url;
|
|
@@ -54,12 +98,19 @@ export async function cmdCanvasAddImage(positionals, values) {
|
|
|
54
98
|
throw new Error(`Image must be 1 byte to 100 MB (${stat.size} bytes given)`);
|
|
55
99
|
const x = coordinateFlag(values.x, '--x');
|
|
56
100
|
const y = coordinateFlag(values.y, '--y');
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
let width = positiveFlag(values.width, '--width');
|
|
102
|
+
let height = positiveFlag(values.height, '--height');
|
|
59
103
|
if ((x === undefined) !== (y === undefined))
|
|
60
104
|
throw new Error('Pass --x and --y together');
|
|
61
105
|
if ((width === undefined) !== (height === undefined))
|
|
62
106
|
throw new Error('Pass --width and --height together');
|
|
107
|
+
if (width === undefined) {
|
|
108
|
+
const native = imageSize(readFileSync(path), mimeType);
|
|
109
|
+
if (native && native.width > 0 && native.height > 0) {
|
|
110
|
+
width = native.width;
|
|
111
|
+
height = native.height;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
63
114
|
// Refuse an unknown target before putting bytes in storage for a canvas that cannot use them.
|
|
64
115
|
const target = await rpcOnce('composition-detail', { id: compositionId }, 30_000);
|
|
65
116
|
if (target?.error || target?.composition?.id !== compositionId) {
|
|
@@ -93,7 +144,10 @@ export async function cmdCanvasAddImage(positionals, values) {
|
|
|
93
144
|
if (placed?.ok !== true || !placed?.nodeId && !placed?.blockId) {
|
|
94
145
|
throw new Error(`Image uploaded but could not be placed: ${placed?.error ?? 'unknown error'}. Asset key: ${key}`);
|
|
95
146
|
}
|
|
96
|
-
|
|
147
|
+
// ⚠ `src` IS RETURNED — reusing an upload (a brand logo, a second placement) needed a scene read
|
|
148
|
+
// to find the URL the node stores (CDS-27).
|
|
149
|
+
const src = String(placed?.src ?? placed?.url ?? placed?.node?.src ?? absoluteRelayUrl(String(intent.publicUrl)));
|
|
150
|
+
const result = { ok: true, compositionId, nodeId: placed.nodeId ?? placed.blockId, imageFileId: recorded?.id, name, src, ...(width !== undefined ? { width, height } : {}) };
|
|
97
151
|
if (values.json)
|
|
98
152
|
console.log(JSON.stringify(result, null, 2));
|
|
99
153
|
else
|
|
@@ -138,6 +138,38 @@ export async function startHeadlessDaemon(values) {
|
|
|
138
138
|
? ` Ready — socket answering.`
|
|
139
139
|
: ` Started, but not answering yet after ${Math.round(DAEMON_READY_TIMEOUT_MS / 1000)}s. 'beehaven status' shows state.`);
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* ⚠⚠ WHO IS ACTUALLY SERVING AFTER A RESPAWN. `restart` used to print the PID it spawned and stop
|
|
143
|
+
* there. On a Mac with the Clearly app open, the tray supervisor respawns ITS daemon the moment the
|
|
144
|
+
* old one dies — so the CLI's spawn lost the single-instance race ("Refusing to start — daemon
|
|
145
|
+
* already running"), exited, and the user was told "Daemon restarted (PID 5706)" about a process
|
|
146
|
+
* that no longer existed (observed 2026-10-01; the next call said "Cannot connect to daemon").
|
|
147
|
+
* This waits for a usable socket and reports the PID that is really live.
|
|
148
|
+
*/
|
|
149
|
+
export async function waitForServingDaemon(spawnedPid) {
|
|
150
|
+
const { isDaemonRunning } = await import('../daemon-client.js');
|
|
151
|
+
const alive = (pid) => { if (!pid)
|
|
152
|
+
return false; try {
|
|
153
|
+
process.kill(pid, 0);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
} };
|
|
159
|
+
const deadline = Date.now() + DAEMON_READY_TIMEOUT_MS;
|
|
160
|
+
let ready = false;
|
|
161
|
+
while (Date.now() < deadline) {
|
|
162
|
+
await new Promise((r) => setTimeout(r, DAEMON_POLL_MS));
|
|
163
|
+
if (await isDaemonRunning()) {
|
|
164
|
+
ready = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
// The spawn died and nobody else took over: no point waiting out the clock.
|
|
168
|
+
if (!alive(spawnedPid) && findLiveDaemons().length === 0)
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return { ready, spawnedAlive: alive(spawnedPid), livePids: findLiveDaemons() };
|
|
172
|
+
}
|
|
141
173
|
export async function cmdDaemon(sub, values) {
|
|
142
174
|
switch (sub) {
|
|
143
175
|
case 'start':
|
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 ?? {};
|