@clearlysh/beehaven 0.8.16

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
package/dist/health.js ADDED
@@ -0,0 +1,56 @@
1
+ // ============================================================================
2
+ // Beehaven Daemon — Health Monitor
3
+ // Tracks uptime, connectivity, and activity. Exposes /health on the watcher socket.
4
+ // ============================================================================
5
+ import { logger } from './logger.js';
6
+ export class HealthMonitor {
7
+ getConnected;
8
+ getPendingCount;
9
+ version;
10
+ startedAt = Date.now();
11
+ lastEventAt = null;
12
+ watchdogTimer = null;
13
+ watchdogLastPing = Date.now();
14
+ constructor(getConnected, getPendingCount, version) {
15
+ this.getConnected = getConnected;
16
+ this.getPendingCount = getPendingCount;
17
+ this.version = version;
18
+ }
19
+ recordEvent() {
20
+ this.lastEventAt = Date.now();
21
+ this.watchdogLastPing = Date.now();
22
+ }
23
+ getStatus() {
24
+ const now = Date.now();
25
+ const connected = this.getConnected();
26
+ const lastEventAge = this.lastEventAt ? Math.round((now - this.lastEventAt) / 1000) : null;
27
+ let status = 'healthy';
28
+ if (!connected)
29
+ status = 'degraded';
30
+ if (!connected && lastEventAge !== null && lastEventAge > 300)
31
+ status = 'unhealthy';
32
+ return {
33
+ status,
34
+ uptime: Math.round((now - this.startedAt) / 1000),
35
+ connected,
36
+ lastEventAge,
37
+ pendingRequests: this.getPendingCount(),
38
+ version: this.version,
39
+ pid: process.pid,
40
+ };
41
+ }
42
+ /** Start watchdog — logs warning if no activity for 60s */
43
+ startWatchdog() {
44
+ this.watchdogTimer = setInterval(() => {
45
+ const elapsed = Date.now() - this.watchdogLastPing;
46
+ if (elapsed > 60000) {
47
+ logger.warn('health', `Watchdog: no activity for ${Math.round(elapsed / 1000)}s`);
48
+ }
49
+ this.watchdogLastPing = Date.now(); // reset so we don't spam
50
+ }, 30000);
51
+ }
52
+ stop() {
53
+ if (this.watchdogTimer)
54
+ clearInterval(this.watchdogTimer);
55
+ }
56
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * IPC auth — scope-aware shared secrets between daemon and local clients.
3
+ *
4
+ * Threat model: any process running as the same user can connect to
5
+ * /tmp/beehaven-ipc.sock (it's chmod 0600, which only restricts other users).
6
+ * Without per-scope auth, any same-user process can invoke privileged
7
+ * verbs — extract the Firebase ID token via `get-token`, run arbitrary
8
+ * SQL, install marketplace packages with code execution.
9
+ *
10
+ * Defense layers:
11
+ * 1. Socket file mode 0600 + parent dir 0700 (kernel-enforced same-user).
12
+ * 2. Per-launch token rotation (fresh tokens every daemon start).
13
+ * 3. Constant-time compare (timingSafeEqual).
14
+ * 4. *Two-token scope split* — see "Scope model" below.
15
+ *
16
+ * ── Scope model ──────────────────────────────────────────────────────
17
+ * Two distinct tokens, two distinct files:
18
+ *
19
+ * ~/.beehaven/ipc-auth → consumer scope (always exists)
20
+ * ~/.beehaven/ipc-auth-dev → developer scope (only if HIVE_DEV_MODE=1
21
+ * or the Mac app's Settings → Developer
22
+ * Mode toggle is on)
23
+ *
24
+ * Mwah CLI reads the consumer token only. Beehaven CLI reads the
25
+ * developer token (falling back to consumer when dev mode is off, in
26
+ * which case privileged verbs fail closed).
27
+ *
28
+ * The daemon stores both (when minted) and tags each authenticated
29
+ * client with its scope on the auth handshake. The IPC server gates
30
+ * verbs by scope:
31
+ *
32
+ * consumer → ping, auth, app-rpc (local buildings), rpc to home,
33
+ * session:announce, subscribe, send
34
+ * developer → everything consumer plus get-token, daemon-fetch,
35
+ * shell, sql, marketplace:install, admin:* RPC actions
36
+ *
37
+ * Even if a malicious script in consumer mode reads ~/.beehaven/ipc-auth
38
+ * and authenticates, it cannot exfiltrate the Firebase ID token, run
39
+ * SQL, install packages, or call admin verbs. The blast radius is
40
+ * scoped to what mwah itself does.
41
+ *
42
+ * ── Backward compat ──────────────────────────────────────────────────
43
+ * The legacy single-token shape (one file, full access) is gone. Old
44
+ * CLIs that pre-date this change get *consumer* scope automatically —
45
+ * they don't request a scope on auth, and the daemon defaults missing
46
+ * scopes to consumer. Any script that relied on `beehaven rpc get-token`
47
+ * working in a non-dev daemon will now get UNAUTHORIZED — that's the
48
+ * intended security improvement.
49
+ */
50
+ import { randomBytes, timingSafeEqual } from 'crypto';
51
+ import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync, unlinkSync } from 'fs';
52
+ import { join } from 'path';
53
+ import { homedir } from 'os';
54
+ import { keychain } from './keychain.js';
55
+ const AUTH_DIR = join(homedir(), '.beehaven');
56
+ const AUTH_FILE_CONSUMER = join(AUTH_DIR, 'ipc-auth');
57
+ const AUTH_FILE_DEVELOPER = join(AUTH_DIR, 'ipc-auth-dev');
58
+ const KEYCHAIN_ACCOUNT_CONSUMER = 'ipc-auth';
59
+ const KEYCHAIN_ACCOUNT_DEVELOPER = 'ipc-auth-dev';
60
+ const FILE_FOR_SCOPE = {
61
+ consumer: AUTH_FILE_CONSUMER,
62
+ developer: AUTH_FILE_DEVELOPER,
63
+ };
64
+ const KEYCHAIN_ACCOUNT_FOR_SCOPE = {
65
+ consumer: KEYCHAIN_ACCOUNT_CONSUMER,
66
+ developer: KEYCHAIN_ACCOUNT_DEVELOPER,
67
+ };
68
+ /**
69
+ * Mint fresh IPC auth tokens on daemon startup.
70
+ *
71
+ * Always mints a consumer token. Mints a developer token only when
72
+ * `developerMode` is true — i.e. the user explicitly opted into dev
73
+ * mode (HIVE_DEV_MODE=1 env var, or the Mac app's tray toggle).
74
+ * Without dev mode, no developer token exists on disk and beehaven CLI
75
+ * commands that need it fail with UNAUTHORIZED. That is the intended
76
+ * security default.
77
+ */
78
+ export function mintIpcAuthTokens(opts) {
79
+ try {
80
+ mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
81
+ }
82
+ catch { }
83
+ const consumer = writeScopedToken('consumer');
84
+ if (opts.developerMode) {
85
+ const developer = writeScopedToken('developer');
86
+ return { consumer, developer };
87
+ }
88
+ // Dev mode is off — actively *clear* any stale developer token left
89
+ // from a previous launch. Otherwise a user who toggled dev mode off
90
+ // would still have a usable developer token on disk.
91
+ clearScopedToken('developer');
92
+ return { consumer, developer: null };
93
+ }
94
+ function writeScopedToken(scope) {
95
+ const token = randomBytes(32).toString('hex');
96
+ const file = FILE_FOR_SCOPE[scope];
97
+ // Atomic-ish write: unlink first to drop any stale perms, then create
98
+ // with mode 0600 from the start. The chmod is belt + braces because
99
+ // mode is advisory in writeFileSync on some platforms.
100
+ try {
101
+ if (existsSync(file))
102
+ unlinkSync(file);
103
+ }
104
+ catch { }
105
+ writeFileSync(file, token, { mode: 0o600 });
106
+ try {
107
+ chmodSync(file, 0o600);
108
+ }
109
+ catch { }
110
+ /**
111
+ * ⚠⚠ THE IPC TOKEN IS NOT MIRRORED TO THE KEYCHAIN, AND THAT IS THE FIX FOR A MODAL DIALOG.
112
+ *
113
+ * The daemon is spawned DETACHED, so it has no Aqua security session — and `security
114
+ * add-generic-password` in that context cannot resolve a default keychain. macOS answers with a
115
+ * BLOCKING system window:
116
+ *
117
+ * A keychain cannot be found to store "ipc auth"
118
+ *
119
+ * Reported from a real machine, and dismissing it opened TWO MORE — because the app spawns the
120
+ * daemon three times per launch (boot, then `daemon.install()`'s deliberate restart after
121
+ * sign-in). Three mints, three dialogs. The machine's keychain is fine; the SESSION is not.
122
+ *
123
+ * ⚠ AND IT BLOCKS THE MINT. `spawnSync` waits on that window, so the token is not written while
124
+ * it is open — which is how a daemon ends up alive, owning the socket, with no token. Measured
125
+ * separately at 153 SECONDS on one keychain call.
126
+ *
127
+ * ⚠ THE MIRROR BOUGHT ALMOST NOTHING. This token is EPHEMERAL — minted per launch, scrubbed on
128
+ * shutdown, and stored in a 0600 file that is the primary source. `readScopedToken`'s keychain
129
+ * fallback only matters when the file is absent, and by design that means no daemon is running,
130
+ * so there is nothing for a stale keychain copy to authenticate against.
131
+ *
132
+ * ⚠ THIS IS NOT A CLAIM THAT THE KEYCHAIN IS UNSAFE. Long-lived secrets (the relay refresh
133
+ * token, Claude credentials) still belong there. A per-launch socket token does not.
134
+ */
135
+ void keychain; // still imported for `clearScopedToken`, which reaps any legacy mirror
136
+ return token;
137
+ }
138
+ function clearScopedToken(scope, expected) {
139
+ /**
140
+ * ⚠ A MISMATCH MEANS A NEWER DAEMON OWNS IT — leave it alone. See `clearIpcAuthToken`.
141
+ * An unreadable file is treated as ours: failing to scrub is the weaker outcome of the two.
142
+ */
143
+ if (expected) {
144
+ let current = '';
145
+ try {
146
+ current = readFileSync(FILE_FOR_SCOPE[scope], 'utf8').trim();
147
+ }
148
+ catch { /* treat as ours */ }
149
+ if (current && current !== expected)
150
+ return;
151
+ }
152
+ try {
153
+ if (existsSync(FILE_FOR_SCOPE[scope]))
154
+ unlinkSync(FILE_FOR_SCOPE[scope]);
155
+ }
156
+ catch { }
157
+ try {
158
+ keychain.delete(KEYCHAIN_ACCOUNT_FOR_SCOPE[scope]);
159
+ }
160
+ catch { }
161
+ }
162
+ /**
163
+ * Read the consumer IPC token. Used by mwah and any other consumer-
164
+ * scope client.
165
+ */
166
+ export function readIpcAuthToken() {
167
+ return readScopedToken('consumer');
168
+ }
169
+ /**
170
+ * Read the scoped IPC token. Falls back to keychain if file is missing
171
+ * (e.g. /tmp was cleared but daemon still running).
172
+ */
173
+ export function readScopedToken(scope) {
174
+ const file = FILE_FOR_SCOPE[scope];
175
+ try {
176
+ if (existsSync(file)) {
177
+ const token = readFileSync(file, 'utf-8').trim();
178
+ if (token)
179
+ return token;
180
+ }
181
+ }
182
+ catch { }
183
+ try {
184
+ return keychain.get(KEYCHAIN_ACCOUNT_FOR_SCOPE[scope]);
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ }
190
+ /**
191
+ * Remove all auth tokens on daemon shutdown so leftover file contents
192
+ * can't authenticate a connection to a newly-launched daemon. (The
193
+ * new daemon mints fresh tokens anyway — belt + suspenders.)
194
+ */
195
+ export function clearIpcAuthToken(mine) {
196
+ /**
197
+ * ⚠⚠ ONLY SCRUB WHAT THIS DAEMON MINTED — an unconditional clear DELETES A LIVE DAEMON'S TOKEN.
198
+ *
199
+ * Shutdown used to clear both scopes outright. That is correct for the last daemon standing and
200
+ * WRONG during a restart, which is the common case: `daemon.install()` deliberately restarts
201
+ * after sign-in, so two daemons overlap. The order that bites is
202
+ *
203
+ * A mints T1 -> B starts and mints T2 (overwriting the file) -> A finishes shutting
204
+ * down and deletes T2
205
+ *
206
+ * leaving B alive, owning the socket, with NO token on disk. Every later call then answers "the
207
+ * daemon is still starting — it has not minted an IPC token yet" FOREVER, and the tray sits on
208
+ * "Checking…" because `workspace:list` can never succeed. Reproduced on this machine: one
209
+ * daemon, pid file matching, socket owned, `~/.beehaven/ipc-auth` absent.
210
+ *
211
+ * ⚠ COMPARING THE VALUE IS THE FIX, not skipping the clear. The security reason for scrubbing is
212
+ * real (it shortens the window in which a captured token pairs with a live socket); what was
213
+ * missing is that a token this daemon did not write is somebody else's.
214
+ *
215
+ * ⚠ NO ARGUMENT MEANS "CLEAR REGARDLESS" and that is deliberate — an explicit `beehaven stop`
216
+ * wants everything gone, and a caller that cannot say what it minted must not silently leave a
217
+ * token behind.
218
+ */
219
+ clearScopedToken('consumer', mine?.consumer);
220
+ clearScopedToken('developer', mine?.developer);
221
+ }
222
+ /**
223
+ * Constant-time comparison. Defensive checks: rejects empty/short
224
+ * tokens even if the caller misconfigured the daemon to accept one.
225
+ * Real tokens are 64-char hex.
226
+ */
227
+ export function verifyIpcAuthToken(presented, expected) {
228
+ if (typeof presented !== 'string' || typeof expected !== 'string')
229
+ return false;
230
+ if (presented.length < 32 || expected.length < 32)
231
+ return false;
232
+ if (presented.length !== expected.length)
233
+ return false;
234
+ try {
235
+ return timingSafeEqual(Buffer.from(presented, 'utf-8'), Buffer.from(expected, 'utf-8'));
236
+ }
237
+ catch {
238
+ return false;
239
+ }
240
+ }
241
+ /**
242
+ * Match a presented token against the daemon's known consumer +
243
+ * developer tokens, returning the scope it authenticates as (or null
244
+ * if neither matches). Used by ipc-server.ts on the auth handshake.
245
+ *
246
+ * Order: developer first so a successful developer-token match returns
247
+ * the higher scope. Consumer scope is the strict subset.
248
+ */
249
+ export function resolveAuthScope(presented, tokens) {
250
+ if (tokens.developer && verifyIpcAuthToken(presented, tokens.developer)) {
251
+ return 'developer';
252
+ }
253
+ if (tokens.consumer && verifyIpcAuthToken(presented, tokens.consumer)) {
254
+ return 'consumer';
255
+ }
256
+ return null;
257
+ }
258
+ /**
259
+ * Resolve the best auth token available for the calling process.
260
+ *
261
+ * Resolution order:
262
+ * 1. BEEHAVEN_IPC_AUTH env var (used by the Electron app's spawned
263
+ * child processes — they're in the daemon's trust domain).
264
+ * 2. If BEEHAVEN_IPC_SCOPE=developer is set OR `preferDeveloper` is
265
+ * true, try the developer token first. Falls back to consumer
266
+ * if dev mode isn't on.
267
+ * 3. The consumer token.
268
+ *
269
+ * The beehaven CLI sets BEEHAVEN_IPC_SCOPE=developer at startup (in
270
+ * its entry script) so calls automatically attempt developer scope.
271
+ * Mwah and other consumer-only callers don't set the env var and get
272
+ * consumer scope. When dev mode is off, even beehaven gracefully
273
+ * downgrades to consumer scope; privileged verbs then fail with
274
+ * FORBIDDEN_SCOPE rather than the connection breaking entirely.
275
+ */
276
+ export function resolveClientAuthToken(opts) {
277
+ const fromEnv = process.env.BEEHAVEN_IPC_AUTH;
278
+ if (fromEnv && fromEnv.length >= 32)
279
+ return fromEnv;
280
+ const preferDeveloper = opts?.preferDeveloper === true || process.env.BEEHAVEN_IPC_SCOPE === 'developer';
281
+ if (preferDeveloper) {
282
+ const dev = readScopedToken('developer');
283
+ if (dev)
284
+ return dev;
285
+ }
286
+ return readScopedToken('consumer');
287
+ }
288
+ /**
289
+ * Read the developer-scope token specifically. Returns null when dev
290
+ * mode is off (no developer token on disk). Useful when a caller
291
+ * needs to explicitly check scope availability before proceeding.
292
+ */
293
+ export function resolveDeveloperAuthToken() {
294
+ const fromEnv = process.env.BEEHAVEN_IPC_AUTH;
295
+ const scopeHint = process.env.BEEHAVEN_IPC_SCOPE;
296
+ if (fromEnv && fromEnv.length >= 32 && scopeHint === 'developer')
297
+ return fromEnv;
298
+ return readScopedToken('developer');
299
+ }