@clearlysh/beehaven 0.8.30 → 0.8.32
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/agents-md-text.js +64 -0
- package/dist/browser-login.js +6 -1
- package/dist/build-channel.js +2 -2
- package/dist/cli.js +18 -0
- package/dist/commands/agents-md.js +118 -0
- package/dist/commands/help.js +21 -0
- package/dist/commands/self-update.js +32 -5
- package/dist/daemon.js +104 -30
- package/dist/dashboard.js +118 -6
- package/dist/ipc-server.js +11 -8
- package/dist/npm-update.js +138 -0
- package/dist/verb-surface.js +1 -0
- package/package.json +1 -1
- package/scripts/sync-skills.mjs +6 -0
- package/skills/agent-doodle-space/SKILL.md +1 -1
- package/skills/agent-home/SKILL.md +1 -1
- package/skills/brand-identity/SKILL.md +1 -0
- package/skills/clearly-agent/SKILL.md +1 -1
- package/skills/clearly-canvas/SKILL.md +1 -0
- package/skills/clearly-docs/SKILL.md +151 -0
- package/skills/clearly-init/SKILL.md +135 -0
- package/skills/clearly-workflows/SKILL.md +90 -0
- package/skills/clearly-workspace/SKILL.md +1 -1
- package/skills/codebase-map/SKILL.md +1 -1
- package/skills/index.json +43 -8
- package/skills/pair-on-canvas/SKILL.md +1 -1
- package/skills/sticker-pack/SKILL.md +132 -0
- package/skills/visualize/SKILL.md +188 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents-md-text — the Clearly block for a PROJECT's AGENTS.md, as `beehaven agents-md` writes it
|
|
3
|
+
* (CAN2-66).
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ A COPY, PINNED. The source of truth is the monorepo's `lib/connections/agentsMd.ts`, which the
|
|
6
|
+
* Integrations page, each agent's details and the MCP `clearly-agents-md` prompt render. This
|
|
7
|
+
* package cannot import it (it ships on its own), so the text is repeated here and the monorepo's
|
|
8
|
+
* `lib/__tests__/agentsMdCliParity.test.mjs` compares the two OUTPUTS, name argument included.
|
|
9
|
+
* Change both or the test fails — a stale copy does not render wrong, it instructs wrong.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ NO IMPORTS, so that test can load this file directly.
|
|
12
|
+
*/
|
|
13
|
+
export const CLI_BLOCK_START = '<!-- clearly:cli:start -->';
|
|
14
|
+
export const CLI_BLOCK_END = '<!-- clearly:cli:end -->';
|
|
15
|
+
export const MCP_BLOCK_START = '<!-- clearly:mcp:start -->';
|
|
16
|
+
export const MCP_BLOCK_END = '<!-- clearly:mcp:end -->';
|
|
17
|
+
/** What goes in CLAUDE.md so Claude Code reads the AGENTS.md block too. */
|
|
18
|
+
export const CLAUDE_MD_IMPORT = '@AGENTS.md';
|
|
19
|
+
/** The block for agents that use the `beehaven` CLI. */
|
|
20
|
+
export function agentsMdCliBlock(agentName) {
|
|
21
|
+
const name = (agentName ?? '').trim();
|
|
22
|
+
const login = name
|
|
23
|
+
? `\`beehaven agent login ${name} --label "<what you are doing>"\` — you are ${name} in this workspace.`
|
|
24
|
+
: '`beehaven agent login <name> --label "<what you are doing>"`, using your own name (claude, codex, gemini).';
|
|
25
|
+
return [
|
|
26
|
+
CLI_BLOCK_START,
|
|
27
|
+
'<!-- managed-by: clearly. Run `beehaven agents-md` to update; edit outside this block. -->',
|
|
28
|
+
'',
|
|
29
|
+
'## Clearly workspace',
|
|
30
|
+
'',
|
|
31
|
+
'This project keeps its documents, plans, boards and tickets in Clearly. Use the `beehaven` CLI',
|
|
32
|
+
'for anything the team should see, not files in this repo.',
|
|
33
|
+
'',
|
|
34
|
+
`- Sign in once per session: ${login}`,
|
|
35
|
+
' If it says the daemon is not running: `beehaven start --headless`.',
|
|
36
|
+
'- Read the `clearly-agent` skill before anything else, and `clearly-canvas` before design work',
|
|
37
|
+
' (`beehaven skills install` puts them in place).',
|
|
38
|
+
'- `beehaven actions` lists what you can call; `beehaven call <action> \'<json>\'` calls it.',
|
|
39
|
+
'- Search before you create, file work in the project it belongs to, and end the session with',
|
|
40
|
+
' `beehaven agent handoff "what you did · what is next"` so the next one starts there.',
|
|
41
|
+
CLI_BLOCK_END,
|
|
42
|
+
].join('\n');
|
|
43
|
+
}
|
|
44
|
+
/** The block for agents connected over MCP (the `clearly` server). */
|
|
45
|
+
export function agentsMdMcpBlock() {
|
|
46
|
+
return [
|
|
47
|
+
MCP_BLOCK_START,
|
|
48
|
+
'<!-- managed-by: clearly-mcp. Re-run the `clearly-agents-md` prompt to update; edit outside this block. -->',
|
|
49
|
+
'',
|
|
50
|
+
'## Clearly workspace (connected over MCP as `clearly`)',
|
|
51
|
+
'',
|
|
52
|
+
'This project keeps its documents, plans, boards and tickets in Clearly. Use the `clearly_*`',
|
|
53
|
+
'tools, not files in this repo, for anything the team should see.',
|
|
54
|
+
'',
|
|
55
|
+
'- Start with `clearly_catalog` to see what exists; search with `clearly_grep` / `clearly_glob`.',
|
|
56
|
+
'- Read with `clearly_read`, make new work with `clearly_create`, change it with `clearly_edit`,',
|
|
57
|
+
' and discuss it with `clearly_comment`. They take a `target`: a path (`~/Q1/Plan.md`), a key',
|
|
58
|
+
' (`CLR-42`), an id or an exact title. The extension names the type (`.md` document, `.csv`',
|
|
59
|
+
' sheet, `.board.json` board); a trailing slash means a project.',
|
|
60
|
+
'- Search before you write, and write decisions back so the next session inherits them.',
|
|
61
|
+
'- If a job is not covered by a listed tool, say so rather than guessing an action name.',
|
|
62
|
+
MCP_BLOCK_END,
|
|
63
|
+
].join('\n');
|
|
64
|
+
}
|
package/dist/browser-login.js
CHANGED
|
@@ -265,7 +265,12 @@ export function persistBrowserLogin(captured) {
|
|
|
265
265
|
userId: captured.userId,
|
|
266
266
|
email: captured.email ?? null,
|
|
267
267
|
displayName: captured.displayName ?? null,
|
|
268
|
-
|
|
268
|
+
/**
|
|
269
|
+
* ⚠ SAY WHERE THE TOKEN REALLY IS (CAN2-65). This was always 'keychain', so on Windows —
|
|
270
|
+
* which has no keychain here — `whoami` reported a keychain that did not exist, and
|
|
271
|
+
* `Relay.loadConfig` / the refresh path shelled out to macOS `security` on every load.
|
|
272
|
+
*/
|
|
273
|
+
tokenStorage: keychain.available ? 'keychain' : 'file',
|
|
269
274
|
refreshToken: captured.refreshToken,
|
|
270
275
|
token: captured.idToken,
|
|
271
276
|
endpoint: dialUrl,
|
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 = '3152df6cf1d7-dirty';
|
|
57
57
|
/** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
|
|
58
|
-
export const BUILD_TIME = '2026-10-
|
|
58
|
+
export const BUILD_TIME = '2026-10-13T08:55Z';
|
|
59
59
|
/**
|
|
60
60
|
* The build identity as one short string, for `--version`.
|
|
61
61
|
*
|
package/dist/cli.js
CHANGED
|
@@ -143,6 +143,7 @@ function printHelp() {
|
|
|
143
143
|
|
|
144
144
|
beehaven guide Agent-first setup and signup handoff
|
|
145
145
|
beehaven signup Create a free Clearly account
|
|
146
|
+
beehaven agents-md Add Clearly to this project's AGENTS.md
|
|
146
147
|
|
|
147
148
|
Personal workspace:
|
|
148
149
|
beehaven cd home
|
|
@@ -956,6 +957,11 @@ const AGENT_LOGIN_EXEMPT = new Set([
|
|
|
956
957
|
* would refuse the one person it is for — the agent whose user has not signed up yet (SGS-73).
|
|
957
958
|
*/
|
|
958
959
|
'handoff',
|
|
960
|
+
/**
|
|
961
|
+
* ⚠ `agents-md` WRITES A FILE IN THIS REPO AND NOTHING ELSE — it never opens a workspace, and it is
|
|
962
|
+
* part of setting a project up, which must not require the login it helps a teammate do (CAN2-66).
|
|
963
|
+
*/
|
|
964
|
+
'agents-md',
|
|
959
965
|
]);
|
|
960
966
|
/**
|
|
961
967
|
* ⚠ ALIAS → CANONICAL VERB, for the activity log's `source` column only.
|
|
@@ -1371,6 +1377,9 @@ async function run() {
|
|
|
1371
1377
|
state: { type: 'string' },
|
|
1372
1378
|
goal: { type: 'string' },
|
|
1373
1379
|
remove: { type: 'boolean', default: false },
|
|
1380
|
+
// `agents-md`: the MCP block instead of the CLI one; print it instead of writing it (CAN2-66).
|
|
1381
|
+
mcp: { type: 'boolean', default: false },
|
|
1382
|
+
print: { type: 'boolean', default: false },
|
|
1374
1383
|
/**
|
|
1375
1384
|
* ⚠⚠ `beehaven sheet` / `beehaven slides` — AND AN UNDECLARED FLAG IS A FATAL, NOT A NO-OP.
|
|
1376
1385
|
*
|
|
@@ -3145,6 +3154,15 @@ async function run() {
|
|
|
3145
3154
|
console.log(' (or quit and relaunch the Hive Mac app if it owns the daemon).');
|
|
3146
3155
|
break;
|
|
3147
3156
|
}
|
|
3157
|
+
case 'agents-md': {
|
|
3158
|
+
const { cmdAgentsMd } = await import('./commands/agents-md.js');
|
|
3159
|
+
process.exitCode = cmdAgentsMd(positionals[1], {
|
|
3160
|
+
mcp: !!values.mcp,
|
|
3161
|
+
print: !!values.print,
|
|
3162
|
+
remove: !!values.remove,
|
|
3163
|
+
});
|
|
3164
|
+
break;
|
|
3165
|
+
}
|
|
3148
3166
|
case 'handoff': {
|
|
3149
3167
|
const { cmdHandoff } = await import('./commands/handoff.js');
|
|
3150
3168
|
process.exitCode = await cmdHandoff(positionals[1], {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* beehaven agents-md — put the Clearly block in THIS project's AGENTS.md (CAN2-66).
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ TWO FILES, TWO AUDIENCES. `beehaven login` already writes a GLOBAL block into
|
|
5
|
+
* ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md and ~/.gemini/GEMINI.md (agent-instructions.ts), so every
|
|
6
|
+
* agent on THIS machine knows Clearly. A project's AGENTS.md is for everyone else who opens the
|
|
7
|
+
* repo — a teammate's Codex, a cloud agent, CI — and it is committed and shared. That is why this
|
|
8
|
+
* is its own command the person (or their agent, asked first) runs, not a side effect of install.
|
|
9
|
+
*
|
|
10
|
+
* ⚠ THE MARKERS ARE THE UPDATE MECHANISM, as on every other surface (lib/connections/agentsMd.ts):
|
|
11
|
+
* re-running replaces only what lies between our markers and leaves the rest byte for byte. The
|
|
12
|
+
* CLI and MCP blocks have their own markers, so a repo used both ways keeps both.
|
|
13
|
+
*
|
|
14
|
+
* ⚠ AGENTS.md IS THE FILE; CLAUDE.md POINTS AT IT with one `@AGENTS.md` line — one copy to keep
|
|
15
|
+
* current. An existing CLAUDE.md gains the line only if it lacks it; nothing else in it is touched.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
18
|
+
import { join, resolve } from 'path';
|
|
19
|
+
import { execFileSync } from 'child_process';
|
|
20
|
+
import { color } from '../spinner.js';
|
|
21
|
+
import { CLI_BLOCK_START, CLI_BLOCK_END, MCP_BLOCK_START, MCP_BLOCK_END, CLAUDE_MD_IMPORT, agentsMdCliBlock, agentsMdMcpBlock, } from '../agents-md-text.js';
|
|
22
|
+
/** Our block replaced between its markers, or appended after one blank line. Keeps CRLF files CRLF. */
|
|
23
|
+
export function spliceBlock(text, block, start, end) {
|
|
24
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
25
|
+
const b = block.split('\n').join(eol);
|
|
26
|
+
const i = text.indexOf(start);
|
|
27
|
+
const j = i >= 0 ? text.indexOf(end, i) : -1;
|
|
28
|
+
if (i >= 0 && j >= 0)
|
|
29
|
+
return text.slice(0, i) + b + text.slice(j + end.length);
|
|
30
|
+
if (!text.trim())
|
|
31
|
+
return b + eol;
|
|
32
|
+
return text.replace(/\s*$/, '') + eol + eol + b + eol;
|
|
33
|
+
}
|
|
34
|
+
/** Our block taken out, with the blank line that separated it. Everything else stays. */
|
|
35
|
+
export function removeBlock(text, start, end) {
|
|
36
|
+
const i = text.indexOf(start);
|
|
37
|
+
const j = i >= 0 ? text.indexOf(end, i) : -1;
|
|
38
|
+
if (i < 0 || j < 0)
|
|
39
|
+
return text;
|
|
40
|
+
const before = text.slice(0, i).replace(/(\r?\n)*$/, '');
|
|
41
|
+
const after = text.slice(j + end.length).replace(/^(\r?\n)*/, '');
|
|
42
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
43
|
+
if (!before)
|
|
44
|
+
return after;
|
|
45
|
+
return after ? before + eol + eol + after : before + eol;
|
|
46
|
+
}
|
|
47
|
+
/** The repository root when `dir` is inside a git work tree, else `dir` itself. */
|
|
48
|
+
export function projectRoot(dir) {
|
|
49
|
+
try {
|
|
50
|
+
const top = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
51
|
+
cwd: dir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true, timeout: 5_000,
|
|
52
|
+
}).trim();
|
|
53
|
+
return top || dir;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return dir;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Write (or remove) the block in `<root>/AGENTS.md` and make CLAUDE.md import it. */
|
|
60
|
+
export function writeProjectAgentsMd(opts) {
|
|
61
|
+
const root = projectRoot(resolve(opts.dir));
|
|
62
|
+
const [start, end] = opts.mcp ? [MCP_BLOCK_START, MCP_BLOCK_END] : [CLI_BLOCK_START, CLI_BLOCK_END];
|
|
63
|
+
const agentsPath = join(root, 'AGENTS.md');
|
|
64
|
+
const claudePath = join(root, 'CLAUDE.md');
|
|
65
|
+
const files = [];
|
|
66
|
+
const had = existsSync(agentsPath);
|
|
67
|
+
const before = had ? readFileSync(agentsPath, 'utf8') : '';
|
|
68
|
+
if (opts.remove) {
|
|
69
|
+
const next = removeBlock(before, start, end);
|
|
70
|
+
if (next !== before)
|
|
71
|
+
writeFileSync(agentsPath, next);
|
|
72
|
+
files.push({ path: agentsPath, change: next !== before ? 'removed' : 'absent' });
|
|
73
|
+
return { root, files };
|
|
74
|
+
}
|
|
75
|
+
const next = spliceBlock(before, opts.mcp ? agentsMdMcpBlock() : agentsMdCliBlock(), start, end);
|
|
76
|
+
if (next !== before)
|
|
77
|
+
writeFileSync(agentsPath, next);
|
|
78
|
+
files.push({ path: agentsPath, change: !had ? 'created' : next !== before ? 'updated' : 'current' });
|
|
79
|
+
const claude = existsSync(claudePath) ? readFileSync(claudePath, 'utf8') : null;
|
|
80
|
+
if (claude === null) {
|
|
81
|
+
writeFileSync(claudePath, CLAUDE_MD_IMPORT + '\n');
|
|
82
|
+
files.push({ path: claudePath, change: 'created' });
|
|
83
|
+
}
|
|
84
|
+
else if (!claude.split(/\r?\n/).some((l) => l.trim() === CLAUDE_MD_IMPORT)) {
|
|
85
|
+
const eol = claude.includes('\r\n') ? '\r\n' : '\n';
|
|
86
|
+
writeFileSync(claudePath, claude.replace(/\s*$/, '') + (claude.trim() ? eol + eol : '') + CLAUDE_MD_IMPORT + eol);
|
|
87
|
+
files.push({ path: claudePath, change: 'updated' });
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
files.push({ path: claudePath, change: 'current' });
|
|
91
|
+
}
|
|
92
|
+
return { root, files };
|
|
93
|
+
}
|
|
94
|
+
export function cmdAgentsMd(dir, opts) {
|
|
95
|
+
if (opts.print) {
|
|
96
|
+
process.stdout.write((opts.mcp ? agentsMdMcpBlock() : agentsMdCliBlock()) + '\n');
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
let r;
|
|
100
|
+
try {
|
|
101
|
+
r = writeProjectAgentsMd({ dir: dir ?? process.cwd(), mcp: opts.mcp, remove: opts.remove });
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
console.error(` ${color.red('✗')} ${err?.message ?? err}`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
console.log('');
|
|
108
|
+
for (const f of r.files) {
|
|
109
|
+
const mark = f.change === 'current' || f.change === 'absent' ? color.dim('·') : color.green('✓');
|
|
110
|
+
console.log(` ${mark} ${f.path} ${color.dim(f.change)}`);
|
|
111
|
+
}
|
|
112
|
+
const changed = r.files.some((f) => f.change !== 'current' && f.change !== 'absent');
|
|
113
|
+
if (changed && !opts.remove) {
|
|
114
|
+
console.log(color.dim(`\n Commit ${r.files.map((f) => f.path.slice(r.root.length + 1)).join(' and ')} so every agent that opens this project knows the workspace.`));
|
|
115
|
+
}
|
|
116
|
+
console.log('');
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
package/dist/commands/help.js
CHANGED
|
@@ -413,6 +413,27 @@ const PAGES = {
|
|
|
413
413
|
\`beehaven login\` to connect the CLI to it.`,
|
|
414
414
|
related: ['login', 'guide'],
|
|
415
415
|
},
|
|
416
|
+
'agents-md': {
|
|
417
|
+
synopsis: 'beehaven agents-md [dir] [--mcp] [--print] [--remove]',
|
|
418
|
+
description: `Add the Clearly block to this project's AGENTS.md, so every agent that opens the
|
|
419
|
+
repository — a teammate's Codex or Claude Code, a cloud agent, CI — knows the workspace and how
|
|
420
|
+
to reach it. Writes at the git root (or the directory given), and makes CLAUDE.md import it with
|
|
421
|
+
one \`@AGENTS.md\` line. Needs no login.
|
|
422
|
+
|
|
423
|
+
The block sits between Clearly's markers: run it again to update the block, and everything
|
|
424
|
+
else in the file stays as it was. \`beehaven login\` already teaches the agents on THIS machine
|
|
425
|
+
(their global instruction files); this is the shared, committed copy for everyone else.`,
|
|
426
|
+
flags: [
|
|
427
|
+
{ name: '--mcp', desc: 'Write the block for agents connected over MCP instead of the CLI' },
|
|
428
|
+
{ name: '--print', desc: 'Print the block and write nothing' },
|
|
429
|
+
{ name: '--remove', desc: 'Take the block out again' },
|
|
430
|
+
],
|
|
431
|
+
examples: [
|
|
432
|
+
{ cmd: 'beehaven agents-md', note: 'AGENTS.md + CLAUDE.md at the repository root' },
|
|
433
|
+
{ cmd: 'beehaven agents-md --mcp', note: 'for a team that connects over MCP' },
|
|
434
|
+
],
|
|
435
|
+
related: ['skills', 'login'],
|
|
436
|
+
},
|
|
416
437
|
handoff: {
|
|
417
438
|
synopsis: 'beehaven handoff [file.md|-] [--tasks tasks.md|json] [--csv data.csv]… [--title "…"] [--wait] [--json]',
|
|
418
439
|
description: `Give a person your work when they have no Clearly account yet. Needs no login.
|
|
@@ -43,9 +43,10 @@ import { execFileSync } from 'node:child_process';
|
|
|
43
43
|
import { createHash } from 'node:crypto';
|
|
44
44
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
45
45
|
import { homedir, tmpdir } from 'node:os';
|
|
46
|
-
import { join, sep } from 'node:path';
|
|
46
|
+
import { dirname, join, sep } from 'node:path';
|
|
47
47
|
import { fileURLToPath } from 'node:url';
|
|
48
|
-
import { usableUpdateDigest } from '../version-utils.js';
|
|
48
|
+
import { compareVersions, usableUpdateDigest } from '../version-utils.js';
|
|
49
|
+
import { npmUpdatePlan, latestNpmVersion, installedNpmVersion, installNpmVersionSync } from '../npm-update.js';
|
|
49
50
|
/**
|
|
50
51
|
* Which install this process is.
|
|
51
52
|
*
|
|
@@ -83,8 +84,9 @@ export function updateHint(kind = installKind()) {
|
|
|
83
84
|
return 'Run \x1b[1mbeehaven update\x1b[0m';
|
|
84
85
|
if (kind === 'npx')
|
|
85
86
|
return 'Run `npx @clearlysh/beehaven@latest` for the current npm release.';
|
|
87
|
+
// npm installs update themselves since CAN2-57 (the daemon hourly, `beehaven update` on demand).
|
|
86
88
|
if (kind === 'npm')
|
|
87
|
-
return 'Run `npm install -g @clearlysh/beehaven@latest`
|
|
89
|
+
return 'Run \x1b[1mbeehaven update\x1b[0m (or `npm install -g @clearlysh/beehaven@latest`).';
|
|
88
90
|
return 'This is a linked dev build — `npm run build` in beehaven_sdk/packages/cli.';
|
|
89
91
|
}
|
|
90
92
|
/**
|
|
@@ -113,9 +115,11 @@ export function runSelfUpdate(kind = installKind()) {
|
|
|
113
115
|
if (kind === 'linked') {
|
|
114
116
|
return { ok: false, message: 'This is a linked development build — refusing to overwrite it.\n cd beehaven_sdk/packages/cli && npm run build' };
|
|
115
117
|
}
|
|
116
|
-
if (kind === '
|
|
118
|
+
if (kind === 'npx') {
|
|
117
119
|
return { ok: false, message: updateHint(kind) };
|
|
118
120
|
}
|
|
121
|
+
if (kind === 'npm')
|
|
122
|
+
return runNpmSelfUpdate();
|
|
119
123
|
const feed = (process.env.BEEHAVEN_CLI_FEED || 'https://downloads.clearly.sh/cli/').replace(/\/+$/, '');
|
|
120
124
|
const installerUrl = process.env.BEEHAVEN_INSTALL_URL || 'https://www.clearly.sh/install.sh';
|
|
121
125
|
const temp = mkdtempSync(join(tmpdir(), 'beehaven-self-update-'));
|
|
@@ -154,9 +158,32 @@ export function runSelfUpdate(kind = installKind()) {
|
|
|
154
158
|
rmSync(temp, { recursive: true, force: true });
|
|
155
159
|
}
|
|
156
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* ⚠ npm installs (the only Windows install) update with npm itself — see `npm-update.ts` for why
|
|
163
|
+
* it refuses rather than guessing when this copy is not npm's global install or the prefix is not
|
|
164
|
+
* writable. The daemon restarts itself after ITS hourly update; after this manual one the running
|
|
165
|
+
* daemon keeps the old code until restarted, so the message says so.
|
|
166
|
+
*/
|
|
167
|
+
function runNpmSelfUpdate() {
|
|
168
|
+
const manual = 'Update it by hand: npm install -g @clearlysh/beehaven@latest';
|
|
169
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
170
|
+
const plan = npmUpdatePlan(pkgRoot);
|
|
171
|
+
if (!plan.ok)
|
|
172
|
+
return { ok: false, message: `Can't update this npm install here: ${plan.reason}.\n ${manual}` };
|
|
173
|
+
const latest = latestNpmVersion(plan.npm);
|
|
174
|
+
if (!latest)
|
|
175
|
+
return { ok: false, message: `Update failed: could not read the latest version from npm.\n ${manual}` };
|
|
176
|
+
const current = installedNpmVersion(plan.globalRoot) ?? '0.0.0';
|
|
177
|
+
if (compareVersions(latest, current) <= 0)
|
|
178
|
+
return { ok: true, message: `Already up to date (${current}).` };
|
|
179
|
+
const r = installNpmVersionSync(plan, latest);
|
|
180
|
+
return r.ok
|
|
181
|
+
? { ok: true, message: `${r.message} A daemon that is already running keeps the old version until: beehaven restart` }
|
|
182
|
+
: { ok: false, message: `${r.message}\n ${manual}` };
|
|
183
|
+
}
|
|
157
184
|
/** True when this install can update itself at all — what the notifier uses to pick its wording. */
|
|
158
185
|
export function canSelfUpdate(kind = installKind()) {
|
|
159
|
-
return kind === 'user';
|
|
186
|
+
return kind === 'user' || kind === 'npm';
|
|
160
187
|
}
|
|
161
188
|
/**
|
|
162
189
|
* Is a bundled CLI present that the shell can NEVER reach, whatever its version?
|
package/dist/daemon.js
CHANGED
|
@@ -46,6 +46,7 @@ import { homedir, tmpdir } from 'os';
|
|
|
46
46
|
import { join, dirname, relative } from 'path';
|
|
47
47
|
import { fileURLToPath } from 'url';
|
|
48
48
|
import { compareVersions, usableUpdateDigest } from './version-utils.js';
|
|
49
|
+
import { npmUpdatePlan, installNpmVersion } from './npm-update.js';
|
|
49
50
|
import { installKind } from './commands/self-update.js';
|
|
50
51
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
51
52
|
const CONFIG_DIR = join(homedir(), '.beehaven');
|
|
@@ -963,9 +964,85 @@ function updatesAreManagedExternally() {
|
|
|
963
964
|
*
|
|
964
965
|
* ⚠ NOTHING ELSE CALLS IT. The daemon's timer is still the only production caller.
|
|
965
966
|
*/
|
|
967
|
+
/** Said once per daemon, not hourly — see the managed-updates note below. */
|
|
968
|
+
let announcedNpmUpdateBlocked = false;
|
|
969
|
+
/**
|
|
970
|
+
* Start the freshly installed version and leave (shared by the curl and npm update paths).
|
|
971
|
+
* A CLI-spawned headless daemon is detached, with no supervisor to interpret exit 75, so it starts
|
|
972
|
+
* the new entry in a helper that waits until this process releases the IPC socket.
|
|
973
|
+
*/
|
|
974
|
+
async function restartForUpdate() {
|
|
975
|
+
if (process.env.BEEHAVEN_CLI_OWNED_DAEMON === '1') {
|
|
976
|
+
const child = spawn(process.execPath, [join(__dirname, 'daemon-entry.js'), '--restart-after-update', String(process.pid)], {
|
|
977
|
+
detached: true,
|
|
978
|
+
stdio: 'ignore',
|
|
979
|
+
env: process.env,
|
|
980
|
+
});
|
|
981
|
+
child.on('error', (error) => console.error(` [update] Restart helper failed: ${error.message}`));
|
|
982
|
+
if (!child.pid)
|
|
983
|
+
throw new Error('could not start update restart helper');
|
|
984
|
+
child.unref();
|
|
985
|
+
}
|
|
986
|
+
if (daemonRelay?.isConnected())
|
|
987
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
988
|
+
process.exit(75);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* ⚠⚠ npm INSTALLS (THE ONLY WINDOWS INSTALL) NEVER AUTO-UPDATED (CAN2-57). This routine re-ran the
|
|
992
|
+
* bash installer, so it skipped every npm install and a Windows user stayed on their first version.
|
|
993
|
+
* They now update with npm itself — to the version the release manifest names (stable only) — when
|
|
994
|
+
* this copy IS npm's global install and its folder is writable; otherwise one line says why and
|
|
995
|
+
* what to run. No rollback file: `install.sh` cannot reinstall an npm package; a bad release is
|
|
996
|
+
* undone with `npm install -g @clearlysh/beehaven@<previous>`.
|
|
997
|
+
*/
|
|
998
|
+
async function checkAndAutoUpdateNpm() {
|
|
999
|
+
// A headless daemon's stdout goes nowhere, so the update story also goes to daemon.log.
|
|
1000
|
+
const say = (m, level = 'info') => {
|
|
1001
|
+
(level === 'error' ? console.error : console.log)(` [update] ${m}`);
|
|
1002
|
+
try {
|
|
1003
|
+
logger[level]('daemon', `[update] ${m}`);
|
|
1004
|
+
}
|
|
1005
|
+
catch { /* logging never blocks an update */ }
|
|
1006
|
+
};
|
|
1007
|
+
try {
|
|
1008
|
+
const controller = new AbortController();
|
|
1009
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1010
|
+
const res = await fetch(LATEST_MANIFEST, { signal: controller.signal });
|
|
1011
|
+
clearTimeout(timeout);
|
|
1012
|
+
if (!res.ok)
|
|
1013
|
+
return;
|
|
1014
|
+
const latest = (await res.json())?.version;
|
|
1015
|
+
if (typeof latest !== 'string')
|
|
1016
|
+
return;
|
|
1017
|
+
const current = getCurrentVersion();
|
|
1018
|
+
if (compareVersions(latest, current) <= 0)
|
|
1019
|
+
return;
|
|
1020
|
+
const plan = npmUpdatePlan(join(__dirname, '..'));
|
|
1021
|
+
if (!plan.ok) {
|
|
1022
|
+
if (!announcedNpmUpdateBlocked) {
|
|
1023
|
+
announcedNpmUpdateBlocked = true;
|
|
1024
|
+
say(`${latest} is available, but this npm install cannot update itself: ${plan.reason}. Run: npm install -g @clearlysh/beehaven@latest`);
|
|
1025
|
+
}
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
say(`Updating ${current} → ${latest} with npm...`);
|
|
1029
|
+
const r = await installNpmVersion(plan, latest);
|
|
1030
|
+
if (!r.ok)
|
|
1031
|
+
throw new Error(r.message);
|
|
1032
|
+
if (getCurrentVersion() !== latest)
|
|
1033
|
+
throw new Error(`npm did not install ${latest}`);
|
|
1034
|
+
say(`Updated to v${latest} — restarting...`);
|
|
1035
|
+
await restartForUpdate();
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
say(`Update failed: ${error?.message ?? error}`, 'error');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
966
1041
|
export async function checkAndAutoUpdate() {
|
|
967
1042
|
const owner = updatesAreManagedExternally();
|
|
968
1043
|
const runningKind = installKind(join(__dirname, 'daemon-entry.js'));
|
|
1044
|
+
if (!owner && runningKind === 'npm')
|
|
1045
|
+
return checkAndAutoUpdateNpm();
|
|
969
1046
|
if (owner || runningKind !== 'user') {
|
|
970
1047
|
// Said once at startup, not hourly — a line every hour about a thing that is working as
|
|
971
1048
|
// designed is how a log stops being read.
|
|
@@ -1017,19 +1094,6 @@ export async function checkAndAutoUpdate() {
|
|
|
1017
1094
|
throw new Error(`installer did not install ${latest}`);
|
|
1018
1095
|
}
|
|
1019
1096
|
console.log(` [update] Updated to v${latest} — restarting...`);
|
|
1020
|
-
if (process.env.BEEHAVEN_CLI_OWNED_DAEMON === '1') {
|
|
1021
|
-
// A CLI-spawned headless daemon is detached, with no supervisor to interpret exit 75.
|
|
1022
|
-
// Start the new entry in a helper that waits until this process releases the IPC socket.
|
|
1023
|
-
const child = spawn(process.execPath, [join(__dirname, 'daemon-entry.js'), '--restart-after-update', String(process.pid)], {
|
|
1024
|
-
detached: true,
|
|
1025
|
-
stdio: 'ignore',
|
|
1026
|
-
env: process.env,
|
|
1027
|
-
});
|
|
1028
|
-
child.on('error', (error) => console.error(` [update] Restart helper failed: ${error.message}`));
|
|
1029
|
-
if (!child.pid)
|
|
1030
|
-
throw new Error('could not start update restart helper');
|
|
1031
|
-
child.unref();
|
|
1032
|
-
}
|
|
1033
1097
|
// Drop the temp tarball BEFORE exiting. `process.exit` skips `finally`,
|
|
1034
1098
|
// so cleaning up only there would leak the whole release into /tmp on every
|
|
1035
1099
|
// SUCCESSFUL update — the one path that always runs.
|
|
@@ -1046,13 +1110,9 @@ export async function checkAndAutoUpdate() {
|
|
|
1046
1110
|
catch { }
|
|
1047
1111
|
installerPath = null;
|
|
1048
1112
|
}
|
|
1049
|
-
//
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
}
|
|
1053
|
-
// Exit with non-zero so launchd/systemd restarts with the new version
|
|
1054
|
-
// (KeepAlive > SuccessfulExit > false means exit 0 stays down)
|
|
1055
|
-
process.exit(75);
|
|
1113
|
+
// Start the new version and exit non-zero, so launchd/systemd also restarts with it
|
|
1114
|
+
// (KeepAlive > SuccessfulExit > false means exit 0 stays down).
|
|
1115
|
+
await restartForUpdate();
|
|
1056
1116
|
}
|
|
1057
1117
|
catch (error) {
|
|
1058
1118
|
console.error(` [update] Update failed: ${error?.message ?? error}`);
|
|
@@ -4056,7 +4116,7 @@ export async function startDaemon(opts) {
|
|
|
4056
4116
|
// so browser frontends become virtual IPC clients — same
|
|
4057
4117
|
// pipeline, app-rpc, broadcast fan-out as Claude's path.
|
|
4058
4118
|
dashboard.setAppBridge({
|
|
4059
|
-
createVirtualClient: (send, subs) => ipc.createVirtualClient(send, subs),
|
|
4119
|
+
createVirtualClient: (send, subs, scope) => ipc.createVirtualClient(send, subs, scope),
|
|
4060
4120
|
injectMessage: (id, msg) => ipc.injectMessage(id, msg),
|
|
4061
4121
|
releaseVirtualClient: (id) => ipc.releaseVirtualClient(id),
|
|
4062
4122
|
});
|
|
@@ -4081,15 +4141,29 @@ export async function startDaemon(opts) {
|
|
|
4081
4141
|
}
|
|
4082
4142
|
})();
|
|
4083
4143
|
// ── Dashboard (local observability UI) ──────────────────────────────
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4144
|
+
/**
|
|
4145
|
+
* ⚠⚠ A LOOPBACK HTTP SERVER IS OPEN TO EVERY USER AND PROGRAM ON THE MACHINE (CAN2-64), so a
|
|
4146
|
+
* daemon opens one only when something will use it. The Mac app's daemon does (the avatar UI
|
|
4147
|
+
* talks over /apps/ws); a HEADLESS daemon — the CLI on Windows and Linux, CI, servers — has no
|
|
4148
|
+
* window to show it in and, on a shared Windows machine, would hand every other signed-in user
|
|
4149
|
+
* a port into this one's daemon. It starts there only on request: BEEHAVEN_DASHBOARD=1,
|
|
4150
|
+
* `"dashboard": true` in ~/.beehaven/config.json, or developer mode.
|
|
4151
|
+
*/
|
|
4152
|
+
const headlessDaemon = process.env.BEEHAVEN_HEADLESS === '1' || process.env.BEEHAVEN_HEADLESS === 'true';
|
|
4153
|
+
const dashboardWanted = !headlessDaemon || developerMode || config.dashboard === true || process.env.BEEHAVEN_DASHBOARD === '1';
|
|
4154
|
+
if (!dashboardWanted) {
|
|
4155
|
+
console.log(' [dashboard] off in a headless daemon (set BEEHAVEN_DASHBOARD=1 to open it on 127.0.0.1)');
|
|
4156
|
+
}
|
|
4157
|
+
if (dashboardWanted)
|
|
4158
|
+
dashboard = new Dashboard({
|
|
4159
|
+
port: config.dashboardPort ?? 9800,
|
|
4160
|
+
host: '127.0.0.1',
|
|
4161
|
+
explorerTarget: `http://127.0.0.1:${config.devPort ?? 8787}`,
|
|
4162
|
+
explorerSecret: config.explorerSecret ?? config.adminSecret ?? '',
|
|
4163
|
+
projectDir: config.linkedProject ?? '',
|
|
4164
|
+
});
|
|
4091
4165
|
dashboard
|
|
4092
|
-
|
|
4166
|
+
?.start()
|
|
4093
4167
|
.then(() => {
|
|
4094
4168
|
console.log(` [dashboard] Beehaven Dashboard at ${dashboard.url}`);
|
|
4095
4169
|
})
|
|
@@ -4098,7 +4172,7 @@ export async function startDaemon(opts) {
|
|
|
4098
4172
|
});
|
|
4099
4173
|
// Forward daemon events to dashboard
|
|
4100
4174
|
watcher.on('event', (event) => {
|
|
4101
|
-
dashboard
|
|
4175
|
+
dashboard?.pushEvent({
|
|
4102
4176
|
type: event.hook_event_name || event.type || 'unknown',
|
|
4103
4177
|
timestamp: event.timestamp || Date.now(),
|
|
4104
4178
|
data: {
|