beehaven 0.8.29 → 0.8.33

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.
Files changed (45) hide show
  1. package/dist/agents-md-text.js +64 -0
  2. package/dist/browser-login.js +14 -4
  3. package/dist/build-channel.js +2 -2
  4. package/dist/claude-bridge.js +12 -9
  5. package/dist/clearly.js +4 -3
  6. package/dist/cli.js +26 -7
  7. package/dist/commands/agents-md.js +118 -0
  8. package/dist/commands/daemon-cmd.js +2 -0
  9. package/dist/commands/help.js +21 -0
  10. package/dist/commands/self-update.js +32 -5
  11. package/dist/commands/workspace.js +3 -3
  12. package/dist/daemon-client.js +10 -6
  13. package/dist/daemon-entry.js +1 -0
  14. package/dist/daemon-process.js +125 -0
  15. package/dist/daemon.js +131 -62
  16. package/dist/dashboard.js +118 -6
  17. package/dist/ipc-auth.js +3 -1
  18. package/dist/ipc-client.js +7 -6
  19. package/dist/ipc-path.js +92 -0
  20. package/dist/ipc-server.js +33 -33
  21. package/dist/npm-update.js +138 -0
  22. package/dist/setup-service.js +9 -4
  23. package/dist/verb-surface.js +1 -0
  24. package/dist/watcher.js +9 -23
  25. package/dist/windows-hide.js +77 -0
  26. package/package.json +1 -1
  27. package/scripts/sync-skills.mjs +6 -0
  28. package/skills/agent-doodle-space/SKILL.md +1 -1
  29. package/skills/agent-home/SKILL.md +1 -1
  30. package/skills/brand-identity/SKILL.md +22 -5
  31. package/skills/clearly-agent/SKILL.md +11 -4
  32. package/skills/clearly-canvas/SKILL.md +31 -5
  33. package/skills/clearly-canvas/authoring.md +40 -12
  34. package/skills/clearly-canvas/example.mjs +72 -69
  35. package/skills/clearly-docs/SKILL.md +151 -0
  36. package/skills/clearly-init/SKILL.md +135 -0
  37. package/skills/clearly-workflows/SKILL.md +90 -0
  38. package/skills/clearly-workspace/SKILL.md +1 -1
  39. package/skills/codebase-map/SKILL.md +1 -1
  40. package/skills/design-craft/SKILL.md +9 -6
  41. package/skills/index.json +45 -10
  42. package/skills/layout-systems/SKILL.md +14 -11
  43. package/skills/pair-on-canvas/SKILL.md +1 -1
  44. package/skills/sticker-pack/SKILL.md +132 -0
  45. 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
+ }
@@ -93,10 +93,15 @@ async function warnIfLocalDevDown() {
93
93
  * Linux; start on Windows. Failure is SURFACED — silent failure was the bug — so the person can
94
94
  * paste the URL themselves.
95
95
  */
96
- function openInBrowser(url) {
96
+ export function openInBrowser(url) {
97
97
  try {
98
- const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
99
- const proc = spawn(opener, [url], { stdio: 'ignore', detached: true });
98
+ // ⚠ Windows: `start` is a cmd.exe BUILTIN, not a program, so spawning it always failed (ENOENT)
99
+ // and login fell back to "paste this URL". The URL protocol handler is a real executable, and
100
+ // passing the URL as an argv entry keeps cmd from splitting it at `&` (CAN2-56).
101
+ const [opener, args] = process.platform === 'darwin' ? ['open', [url]]
102
+ : process.platform === 'win32' ? ['rundll32.exe', ['url.dll,FileProtocolHandler', url]]
103
+ : ['xdg-open', [url]];
104
+ const proc = spawn(opener, args, { stdio: 'ignore', detached: true, windowsHide: true });
100
105
  proc.unref();
101
106
  proc.on('error', (err) => {
102
107
  console.log(` ${color.yellow('!')} Couldn't auto-open browser: ${err.message}`);
@@ -260,7 +265,12 @@ export function persistBrowserLogin(captured) {
260
265
  userId: captured.userId,
261
266
  email: captured.email ?? null,
262
267
  displayName: captured.displayName ?? null,
263
- tokenStorage: 'keychain',
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',
264
274
  refreshToken: captured.refreshToken,
265
275
  token: captured.idToken,
266
276
  endpoint: dialUrl,
@@ -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 = 'd733c0004e58-dirty';
56
+ export const BUILD_SHA = '8f7eef9e12ce';
57
57
  /** ISO-8601 minute precision, UTC. Rewritten in `dist/` by `scripts/stamp-build-channel.mjs`. */
58
- export const BUILD_TIME = '2026-10-10T21:21Z';
58
+ export const BUILD_TIME = '2026-10-15T07:24Z';
59
59
  /**
60
60
  * The build identity as one short string, for `--version`.
61
61
  *
@@ -15,12 +15,13 @@
15
15
  * await bridge.callApp('recorder', 'record', { source: 'mic' });
16
16
  */
17
17
  import { createConnection } from 'net';
18
+ import { request as httpRequest } from 'http';
18
19
  import { existsSync, readFileSync, readdirSync } from 'fs';
19
20
  import { join } from 'path';
20
21
  import { homedir } from 'os';
21
22
  import { EventEmitter } from 'events';
22
23
  import { activeAgentWrap } from './agent-attribution.js';
23
- const IPC_SOCKET_PATH = '/tmp/beehaven-ipc.sock';
24
+ import { ipcSocketPath, watcherSocketPath, socketMayExist } from './ipc-path.js';
24
25
  const CONFIG_DIR = join(homedir(), '.beehaven');
25
26
  export class ClaudeBridge extends EventEmitter {
26
27
  socket = null;
@@ -39,11 +40,12 @@ export class ClaudeBridge extends EventEmitter {
39
40
  async connect() {
40
41
  if (this.connected)
41
42
  return true;
42
- if (!existsSync(IPC_SOCKET_PATH))
43
+ const ipcPath = ipcSocketPath();
44
+ if (!socketMayExist(ipcPath))
43
45
  return false;
44
46
  return new Promise((resolve) => {
45
47
  const timeout = setTimeout(() => resolve(false), 5_000);
46
- const socket = createConnection({ path: IPC_SOCKET_PATH });
48
+ const socket = createConnection({ path: ipcPath });
47
49
  this.socket = socket;
48
50
  socket.on('connect', () => {
49
51
  this.connected = true;
@@ -198,11 +200,12 @@ export class ClaudeBridge extends EventEmitter {
198
200
  }
199
201
  // ── Internal ───────────────────────────────────────────────────────
200
202
  sendEvent(hookEvent, extra = {}) {
201
- // Send via the watcher socket (HTTP POST to /tmp/beehaven.sock)
202
- // This is the same protocol mwah uses.
203
- if (!existsSync('/tmp/beehaven.sock'))
203
+ // Send via the watcher socket (HTTP POST to /event) — the same protocol mwah uses.
204
+ // ⚠ `http` is IMPORTED: this was `require('http')`, which is undefined in this ESM package,
205
+ // so every event threw a ReferenceError once the socket existed (CAN2-56).
206
+ const watcherPath = watcherSocketPath();
207
+ if (!socketMayExist(watcherPath))
204
208
  return;
205
- const http = require('http');
206
209
  const body = JSON.stringify({
207
210
  hook_event_name: hookEvent,
208
211
  session_id: this.sessionId,
@@ -211,8 +214,8 @@ export class ClaudeBridge extends EventEmitter {
211
214
  source: 'claude-code',
212
215
  ...extra,
213
216
  });
214
- const req = http.request({
215
- socketPath: '/tmp/beehaven.sock',
217
+ const req = httpRequest({
218
+ socketPath: watcherPath,
216
219
  path: '/event',
217
220
  method: 'POST',
218
221
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
package/dist/clearly.js CHANGED
@@ -26,6 +26,7 @@
26
26
  * product promises. GitHub / Linear / Slack ingestion is a separate project; nothing here
27
27
  * pretends otherwise, and `status` says so out loud.
28
28
  */
29
+ import './windows-hide.js'; // FIRST: no console windows flashing on Windows (CAN2-56)
29
30
  import { execFileSync, spawnSync } from 'node:child_process';
30
31
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
31
32
  import { dirname, join, resolve } from 'node:path';
@@ -47,12 +48,12 @@ const c = {
47
48
  red: (s) => `\x1b[31m${s}\x1b[0m`,
48
49
  };
49
50
  /**
50
- * ⚠ THE AGENT TURN IS THE SLOW PART. The server's `RESPOND_TIMEOUT_MS` is 300s (clearly CDS-90, was
51
- * 180s); this waits that plus the report's tail, so a turn that finishes on time is never abandoned
51
+ * ⚠ THE AGENT JOB IS THE SLOW PART. The server's `RESPOND_TIMEOUT_MS` is 900s (WDP3-62);
52
+ * this waits that plus the report's tail, so a turn that finishes on time is never abandoned
52
53
  * by its caller and a timeout here still means the turn itself ran out. The relay-forward window
53
54
  * follows the `_timeout` sent with the call.
54
55
  */
55
- export const TURN_MS = 330_000;
56
+ export const TURN_MS = 930_000;
56
57
  /**
57
58
  * ⚠ STDERR IS CAPTURED, NOT INHERITED. A failing probe is normal here — `HEAD~30` does not exist in
58
59
  * a repo with two commits — and with inherited stderr git printed
package/dist/cli.js CHANGED
@@ -16,6 +16,7 @@
16
16
  if (!process.env.BEEHAVEN_IPC_SCOPE) {
17
17
  process.env.BEEHAVEN_IPC_SCOPE = 'developer';
18
18
  }
19
+ import './windows-hide.js'; // FIRST: no console windows flashing on Windows (CAN2-56)
19
20
  import { parseArgs } from 'node:util';
20
21
  import { devModeAvailable, buildLabel, PUBLIC_BUILD } from './build-channel.js';
21
22
  import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, } from 'fs';
@@ -142,6 +143,7 @@ function printHelp() {
142
143
 
143
144
  beehaven guide Agent-first setup and signup handoff
144
145
  beehaven signup Create a free Clearly account
146
+ beehaven agents-md Add Clearly to this project's AGENTS.md
145
147
 
146
148
  Personal workspace:
147
149
  beehaven cd home
@@ -448,8 +450,9 @@ ${DEV_ONLY_HELP ? `
448
450
 
449
451
  Note: if you also run the Hive menu-bar app, the tray is the
450
452
  primary owner of the daemon. CLI start/stop only act when the tray
451
- isn't running its own daemon. The same socket (/tmp/beehaven-ipc.sock)
452
- is shared in both modes — never two daemons at once.
453
+ isn't running its own daemon. The same socket (/tmp/beehaven-ipc.sock;
454
+ a per-user named pipe on Windows) is shared in both modes — never two
455
+ daemons at once.
453
456
 
454
457
  ─────────────────────────────────────────────────────────────────────
455
458
  OPTIONS
@@ -954,6 +957,11 @@ const AGENT_LOGIN_EXEMPT = new Set([
954
957
  * would refuse the one person it is for — the agent whose user has not signed up yet (SGS-73).
955
958
  */
956
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',
957
965
  ]);
958
966
  /**
959
967
  * ⚠ ALIAS → CANONICAL VERB, for the activity log's `source` column only.
@@ -1369,6 +1377,9 @@ async function run() {
1369
1377
  state: { type: 'string' },
1370
1378
  goal: { type: 'string' },
1371
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 },
1372
1383
  /**
1373
1384
  * ⚠⚠ `beehaven sheet` / `beehaven slides` — AND AN UNDECLARED FLAG IS A FATAL, NOT A NO-OP.
1374
1385
  *
@@ -3143,6 +3154,15 @@ async function run() {
3143
3154
  console.log(' (or quit and relaunch the Hive Mac app if it owns the daemon).');
3144
3155
  break;
3145
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
+ }
3146
3166
  case 'handoff': {
3147
3167
  const { cmdHandoff } = await import('./commands/handoff.js');
3148
3168
  process.exitCode = await cmdHandoff(positionals[1], {
@@ -3171,11 +3191,10 @@ async function run() {
3171
3191
  console.log(` Sign up at ${color.cyan(signupUrl)}.`);
3172
3192
  console.log(' Then run "beehaven login" to wire the CLI to your account.');
3173
3193
  console.log('');
3174
- try {
3175
- const { spawn } = await import('child_process');
3176
- spawn('open', [signupUrl], { stdio: 'ignore', detached: true }).unref();
3177
- }
3178
- catch { }
3194
+ // The shared opener: per-platform (Windows has no `open`), and a spawn failure prints the
3195
+ // URL instead of crashing on an unhandled child 'error' event.
3196
+ const { openInBrowser } = await import('./browser-login.js');
3197
+ openInBrowser(signupUrl);
3179
3198
  break;
3180
3199
  }
3181
3200
  case 'treehouse': {
@@ -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
+ }
@@ -42,6 +42,8 @@ export function spawnStandaloneDaemon(values) {
42
42
  const child = spawn(process.execPath, [entry], {
43
43
  detached: true,
44
44
  stdio: 'ignore',
45
+ // Windows: a detached child gets its OWN console window — closing it kills the daemon. Hide it.
46
+ windowsHide: true,
45
47
  // BEEHAVEN_HEADLESS=1 is REQUIRED, not optional: daemon-entry.ts runs a
46
48
  // parent-death watchdog (polls process.ppid, self-exits ~3s after the parent
47
49
  // is gone) to enforce the tray-coupled lifecycle. A detached CLI-spawned
@@ -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` to update this npm install.';
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 === 'npm' || kind === 'npx') {
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?
@@ -26,8 +26,8 @@ import { resolveActiveAgent } from '../agent-state.js';
26
26
  import { isAgentRuntime } from '../agent-scope.js';
27
27
  import { getTarget } from '../cli-state.js';
28
28
  import { attachShellCwd, absorbShellCwd, shellTargetKey } from '../shell-session.js';
29
- /** Workspace-agent turns wait for the server's turn ceiling (300s) plus the report's tail. */
30
- export const AGENT_TURN_TIMEOUT_MS = 330_000;
29
+ /** Workspace-agent jobs wait for the server's ceiling (900s) plus the report's tail. */
30
+ export const AGENT_TURN_TIMEOUT_MS = 930_000;
31
31
  const AGENT_TURN_ACTIONS = new Set(['agent-prompt', 'agent-turn']);
32
32
  export function getTimeout(values, action) {
33
33
  // Precedence: explicit --timeout flag → BEEHAVEN_RPC_TIMEOUT_MS env → default.
@@ -40,7 +40,7 @@ export function getTimeout(values, action) {
40
40
  const env = process.env.BEEHAVEN_RPC_TIMEOUT_MS;
41
41
  if (env && Number.isFinite(Number(env)) && Number(env) > 0)
42
42
  return parseInt(env, 10);
43
- // An agent turn may run to the server's 300s ceiling (clearly CDS-90); waiting less abandons it.
43
+ // An agent job may run to the server's 900s ceiling (WDP3-62); waiting less abandons it.
44
44
  return action && AGENT_TURN_ACTIONS.has(action) ? AGENT_TURN_TIMEOUT_MS : 180_000;
45
45
  }
46
46
  /**
@@ -15,17 +15,16 @@
15
15
  * }
16
16
  */
17
17
  import net from 'net';
18
- import { existsSync } from 'fs';
19
18
  import { resolveClientAuthToken } from './ipc-auth.js';
20
19
  import { activeAgentWrap } from './agent-attribution.js';
21
- const SOCKET_PATH = '/tmp/beehaven-ipc.sock';
20
+ import { ipcSocketPath, socketMayExist } from './ipc-path.js';
22
21
  const TIMEOUT_MS = 60_000;
23
22
  const AUTH_TIMEOUT_MS = 3_000;
24
23
  /** One attempt. Resolves, never rejects. */
25
24
  function probeOnce(deadlineMs) {
26
25
  return new Promise((resolve) => {
27
26
  let connected = false;
28
- const sock = net.createConnection(SOCKET_PATH);
27
+ const sock = net.createConnection(ipcSocketPath());
29
28
  const timer = setTimeout(() => {
30
29
  sock.destroy();
31
30
  // ⚠ CONNECTED-BUT-SILENT IS NOT "DOWN". The kernel accepted us, so a listener exists.
@@ -65,7 +64,7 @@ const PROBE_BACKOFF_MS = 150;
65
64
  * that "down" is the original defect.
66
65
  */
67
66
  export async function probeDaemon() {
68
- if (!existsSync(SOCKET_PATH))
67
+ if (!socketMayExist(ipcSocketPath()))
69
68
  return { state: 'down', attempts: 0 };
70
69
  let sawListener = false;
71
70
  let last = { state: 'down', attempts: 0 };
@@ -74,6 +73,11 @@ export async function probeDaemon() {
74
73
  last = { ...r, attempts: i };
75
74
  if (r.state === 'up')
76
75
  return last;
76
+ // Nothing at the address (no socket file, or no such Windows pipe): permanent, like the
77
+ // missing-file check above — a named pipe cannot be checked before connecting, so this is
78
+ // where "nothing is running" is learned on Windows. Retrying would only add ~300 ms.
79
+ if (r.code === 'ENOENT' && !sawListener)
80
+ return last;
77
81
  if (r.state === 'silent')
78
82
  sawListener = true;
79
83
  if (i < PROBE_ATTEMPTS)
@@ -165,7 +169,7 @@ function handshake(socket) {
165
169
  * ⚠ `start --headless` NOW WAITS for the socket (see `daemon-cmd.ts`), so a script
166
170
  * that uses it should no longer reach this at all.
167
171
  */
168
- (existsSync(SOCKET_PATH)
172
+ (socketMayExist(ipcSocketPath())
169
173
  ? 'The daemon is still starting — its socket is up but it has not minted an IPC token yet. ' +
170
174
  'Wait a second and retry. (Do NOT restart: `stop` scrubs the token, which re-enters this window.)'
171
175
  : 'No IPC auth token and no daemon socket — nothing is running. ' +
@@ -192,7 +196,7 @@ function handshake(socket) {
192
196
  }
193
197
  /** Open a socket, complete auth handshake, run a single request, close. */
194
198
  async function withSocket(fn) {
195
- const socket = net.createConnection(SOCKET_PATH);
199
+ const socket = net.createConnection(ipcSocketPath());
196
200
  await new Promise((resolve, reject) => {
197
201
  socket.once('connect', () => resolve());
198
202
  socket.once('error', reject);
@@ -23,6 +23,7 @@
23
23
  * startup errors (those usually need user intervention — expired token,
24
24
  * missing config, etc).
25
25
  */
26
+ import './windows-hide.js'; // FIRST: no console windows flashing on Windows (CAN2-56)
26
27
  import { startDaemon } from './daemon.js';
27
28
  // ── Parent-watcher (tray-coupled enforcement) ──
28
29
  // The daemon is documented as tray-coupled: it must not outlive the