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
@@ -0,0 +1,495 @@
1
+ /**
2
+ * IPC Client — connects to the beehaven daemon via Unix socket.
3
+ *
4
+ * Used by CLI commands (actions, call, pipe, watch, ls, cat) to communicate
5
+ * with the running daemon, which forwards requests to the cloud DO.
6
+ *
7
+ * Protocol: newline-delimited JSON over /tmp/beehaven-ipc.sock
8
+ */
9
+ import { existsSync } from 'node:fs';
10
+ import { createConnection } from 'net';
11
+ import { EventEmitter } from 'events';
12
+ import { resolveClientAuthToken } from './ipc-auth.js';
13
+ import { activeAgentWrap } from './agent-attribution.js';
14
+ const IPC_SOCKET_PATH = '/tmp/beehaven-ipc.sock';
15
+ /** Default timeout for RPC calls (ms) */
16
+ const DEFAULT_TIMEOUT = 30_000;
17
+ /**
18
+ * How long to wait for a daemon that is UP but has not minted its token yet.
19
+ *
20
+ * ⚠ Bounded by what the condition actually is, not by generosity: the daemon writes the token
21
+ * immediately after binding the socket, so the real window is well under a second. Three seconds
22
+ * covers a loaded machine and still fails fast enough that a genuinely broken daemon is reported
23
+ * rather than waited on.
24
+ */
25
+ const TOKEN_MINT_WAIT_MS = 3_000;
26
+ const TOKEN_MINT_POLL_MS = 50;
27
+ /** Backing store for the synchronous sleep below — never written, only waited on. */
28
+ const WAIT_CELL = new Int32Array(new SharedArrayBuffer(4));
29
+ export class IpcClient extends EventEmitter {
30
+ socket = null;
31
+ buffer = '';
32
+ socketPath;
33
+ defaultTimeout;
34
+ ridCounter = 0;
35
+ pending = new Map();
36
+ _connected = false;
37
+ explicitAuthToken;
38
+ /**
39
+ * Pending auth handshake — resolves when the daemon confirms `auth-result`
40
+ * or rejects on timeout/mismatch. Held during connect() so callers only
41
+ * see a resolved promise after the socket is fully usable.
42
+ */
43
+ authResolver = null;
44
+ constructor(opts) {
45
+ super();
46
+ /**
47
+ * ⚠ `$BEEHAVEN_IPC_SOCKET` IS AN ADDRESS OVERRIDE, NOT A FEATURE FLAG. It exists so a test can
48
+ * stand a fake daemon in front of `rpcOnce` and read the frame that actually goes on the wire
49
+ * — the only way to guard the CALL SITE rather than the helper it calls, which is the exact
50
+ * distinction that let 28 verbs ship un-attributed. Unset, nothing changes.
51
+ */
52
+ this.socketPath = opts?.socketPath ?? process.env.BEEHAVEN_IPC_SOCKET ?? IPC_SOCKET_PATH;
53
+ this.defaultTimeout = opts?.timeout ?? DEFAULT_TIMEOUT;
54
+ this.explicitAuthToken = opts?.authToken;
55
+ }
56
+ /**
57
+ * Connect to the daemon IPC socket. Resolves when the auth handshake
58
+ * completes and the client is ready to send RPCs.
59
+ *
60
+ * Flow:
61
+ * 1. Open socket, wait for the initial `status` frame.
62
+ * 2. Send `{ type: 'auth', token: ... }` with the token read from
63
+ * ~/.beehaven/ipc-auth (or $BEEHAVEN_IPC_AUTH, or explicit option).
64
+ * 3. Wait for `auth-result`. On ok=true, resolve connect(). On ok=false,
65
+ * reject with a clear message so the CLI can tell the user to restart
66
+ * the daemon (tokens rotate every launch).
67
+ */
68
+ connect() {
69
+ return new Promise((resolve, reject) => {
70
+ const timeout = setTimeout(() => {
71
+ reject(new Error('Connection timeout — is the beehaven daemon running? Run `beehaven start --headless` (or open the Clearly Dev Mac app).'));
72
+ }, 5_000);
73
+ const socket = createConnection({ path: this.socketPath });
74
+ this.socket = socket;
75
+ let connectedInfo = null;
76
+ socket.on('connect', () => {
77
+ this._connected = true;
78
+ });
79
+ socket.on('data', (data) => {
80
+ this.buffer += data.toString();
81
+ const lines = this.buffer.split('\n');
82
+ this.buffer = lines.pop() || '';
83
+ for (const line of lines) {
84
+ if (!line.trim())
85
+ continue;
86
+ try {
87
+ const msg = JSON.parse(line);
88
+ // Intercept the status + auth-result frames here so the
89
+ // auth handshake stays encapsulated in connect().
90
+ if (msg.type === 'status' && !connectedInfo) {
91
+ connectedInfo = { clientId: msg.clientId, daemonPid: msg.daemonPid };
92
+ // If the daemon doesn't require auth (old build, or
93
+ // explicitly disabled for tests), skip the handshake.
94
+ if (msg.requireAuth === false) {
95
+ clearTimeout(timeout);
96
+ resolve(connectedInfo);
97
+ this.emit('status', msg);
98
+ continue;
99
+ }
100
+ // Resolve the token: explicit override > env > file > keychain
101
+ /**
102
+ * ⚠⚠ THE TRANSIENT CASE IS WAITED OUT, NOT REPORTED. The daemon mints the token
103
+ * immediately after its socket comes up, so a command arriving in that window is
104
+ * not broken — it is early. Telling the caller to "wait a second and retry" made
105
+ * every script and every agent implement that retry itself, and the ones that did
106
+ * not read a startup race as a broken install.
107
+ *
108
+ * ⚠ ONLY WHEN THE SOCKET IS UP. No socket means nothing is running and no amount of
109
+ * waiting produces a token, so that path must keep failing at once — otherwise
110
+ * `beehaven call` with no daemon pauses before saying the obvious.
111
+ */
112
+ const readToken = () => this.explicitAuthToken === undefined
113
+ ? resolveClientAuthToken()
114
+ : this.explicitAuthToken;
115
+ let token = readToken();
116
+ if (!token && this.explicitAuthToken === undefined && existsSync(this.socketPath)) {
117
+ const deadline = Date.now() + TOKEN_MINT_WAIT_MS;
118
+ while (!token && Date.now() < deadline) {
119
+ /**
120
+ * ⚠ SYNCHRONOUS ON PURPOSE. This runs inside the socket's `data` handler; an
121
+ * `await` here would let the next frame interleave mid-handshake and be parsed
122
+ * against `authResolver === null`.
123
+ */
124
+ Atomics.wait(WAIT_CELL, 0, 0, TOKEN_MINT_POLL_MS);
125
+ token = readToken();
126
+ }
127
+ }
128
+ if (!token) {
129
+ clearTimeout(timeout);
130
+ this._connected = false;
131
+ try {
132
+ socket.destroy();
133
+ }
134
+ catch { }
135
+ reject(new Error(
136
+ /**
137
+ * ⚠⚠ Same trap as daemon-client.ts — and the same fix. `stop` scrubs both
138
+ * tokens; `start --headless` used to return before the replacement was minted,
139
+ * so a `stop && start && call` sequence landed in a window with no token at
140
+ * all. The old text advised a RESTART, which deletes the fresh token and
141
+ * re-enters that window.
142
+ *
143
+ * ⚠ THE SOCKET IS THE DISCRIMINATOR: up means "still starting, wait"; absent
144
+ * means "nothing is running, start one". Opposite actions.
145
+ */
146
+ (existsSync(this.socketPath)
147
+ ? 'The daemon is still starting — its socket is up but it has not minted an IPC token yet. ' +
148
+ 'Wait a second and retry. (Do NOT restart: `stop` scrubs the token, which re-enters this window.)'
149
+ : 'No IPC auth token and no daemon socket — nothing is running. ' +
150
+ 'Start one: `beehaven start --headless` (prefix HIVE_DEV_MODE=1 for admin/shell verbs), ' +
151
+ 'or open the Clearly Dev Mac app if it owns the daemon.')));
152
+ return;
153
+ }
154
+ // Send the handshake and wait for auth-result.
155
+ this.authResolver = {
156
+ resolve: () => {
157
+ clearTimeout(timeout);
158
+ resolve(connectedInfo);
159
+ this.emit('status', msg);
160
+ },
161
+ reject: (e) => {
162
+ clearTimeout(timeout);
163
+ this._connected = false;
164
+ try {
165
+ socket.destroy();
166
+ }
167
+ catch { }
168
+ reject(e);
169
+ },
170
+ };
171
+ try {
172
+ socket.write(JSON.stringify({ type: 'auth', token }) + '\n');
173
+ }
174
+ catch { }
175
+ continue;
176
+ }
177
+ if (msg.type === 'auth-result') {
178
+ if (this.authResolver) {
179
+ const { resolve: r, reject: rj } = this.authResolver;
180
+ this.authResolver = null;
181
+ if (msg.ok)
182
+ r();
183
+ else
184
+ rj(new Error(msg.error || 'IPC auth rejected by daemon'));
185
+ }
186
+ continue;
187
+ }
188
+ this.handleMessage(msg, resolve, timeout);
189
+ }
190
+ catch { }
191
+ }
192
+ });
193
+ socket.on('close', () => {
194
+ this._connected = false;
195
+ if (this.authResolver) {
196
+ const { reject: rj } = this.authResolver;
197
+ this.authResolver = null;
198
+ rj(new Error('Connection closed during auth handshake'));
199
+ }
200
+ this.rejectAll('Connection closed');
201
+ this.emit('close');
202
+ });
203
+ socket.on('error', (err) => {
204
+ clearTimeout(timeout);
205
+ this._connected = false;
206
+ if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
207
+ reject(new Error(
208
+ // Name the CLI-owned fix FIRST. The Mac app is one of two valid
209
+ // daemon owners, and an agent in a terminal cannot open it.
210
+ //
211
+ // ⚠⚠ IT NO LONGER PRESCRIBES `beehaven stop` — CBH2-22. It used to end *"If it
212
+ // claims one is already running, `beehaven stop` first"*, and that sentence is the
213
+ // destructive half of the ticket: a `stop` SCRUBS THE IPC TOKEN and drops the agent
214
+ // session, and this error is reachable from a TRANSIENT connect failure on a daemon
215
+ // that is alive and recovers by itself (observed: the PID was up throughout and the
216
+ // next call succeeded). So the advice on a single failure is to look before acting.
217
+ // `beehaven status` now distinguishes "alive but not answering" from "not running"
218
+ // and names the right fix for each — which is the one place that decision belongs.
219
+ 'Cannot connect to daemon. Check with `beehaven status` first — it says whether one '
220
+ + 'is alive but busy (retry) or genuinely absent (`beehaven start --headless`, or '
221
+ + 'open the Clearly Dev Mac app).'));
222
+ }
223
+ else {
224
+ reject(err);
225
+ }
226
+ });
227
+ });
228
+ }
229
+ /** Whether the socket is connected */
230
+ get connected() {
231
+ return this._connected;
232
+ }
233
+ /**
234
+ * Send an RPC request to the daemon → DO and wait for response.
235
+ *
236
+ * ⚠⚠ THIS IS THE ATTRIBUTION CHOKEPOINT — IT IS THE ONLY PLACE `type:'rpc'` IS BUILT.
237
+ *
238
+ * `ipc-server.ts` forwards every `type:'rpc'` frame to the relay; the daemon's other message
239
+ * types (`app-rpc`, `omni:*`, `daemon-fetch`, `send`) are the LOCAL doors and are sent by other
240
+ * methods. So "did this call reach the workspace?" and "did it come through here?" are the same
241
+ * question, which is what makes this the one place the wrap belongs.
242
+ *
243
+ * ⚠⚠ IT USED TO BE OPT-IN, AND THE OPT-IN IS WHAT FAILED. A verb was attributed only if its
244
+ * module imported `agentRpc` instead of `rpcOnce` — three modules did, twelve did not, and the
245
+ * difference was invisible: same call, same result, no error, just no `agent_activity` row and
246
+ * no `source`. 28 of 47 remote verbs were dark, and a traffic report over that column read their
247
+ * silence as "nobody uses this".
248
+ *
249
+ * ⚠⚠ AND `rpcOnce` WAS NOT LOW ENOUGH. Putting it there still missed every caller that holds a
250
+ * LONG-LIVED client: `omni`'s `describe-action`, the four workspace calls behind `beehaven pipe`,
251
+ * and the action-catalog fetch. Those are exactly the shape the original defect had — a second
252
+ * door nobody remembered — so the wrap belongs at the frame, not at one convenience wrapper
253
+ * around it. `agent-attribution.test.ts` fails on any module that opens a third.
254
+ *
255
+ * ⚠ THE WRAP IS IDEMPOTENT (`alreadyAttributed`), so `agentRpc`, `cmdCall` and the interactive
256
+ * shell — all of which still wrap ABOVE this — are unaffected. `cmdCall` in particular MUST keep
257
+ * wrapping first: it resolves the cross-DO building envelope afterwards, and `agent-invoke` has
258
+ * to travel INSIDE that proxy or the activity row lands in the wrong workspace.
259
+ *
260
+ * ⚠ NO AGENT ⇒ NO WRAP. A signed-out user, an expired identity, a bootstrap verb and a
261
+ * daemon-local verb all pass through untouched — attribution is added where there is something
262
+ * to attribute, never invented.
263
+ */
264
+ call(action, input = {}, timeout) {
265
+ const wrapped = activeAgentWrap(action, input, {});
266
+ action = wrapped.action;
267
+ input = wrapped.input;
268
+ return new Promise((resolve, reject) => {
269
+ if (!this.socket || !this._connected) {
270
+ return reject(new Error('Not connected'));
271
+ }
272
+ const rid = `cli_${++this.ridCounter}_${Date.now().toString(36)}`;
273
+ const ms = timeout ?? this.defaultTimeout;
274
+ // Forward `_timeout` inside the payload so the daemon's relay-forward window
275
+ // matches our wait (the daemon strips it before dispatch; default is 60s, too
276
+ // short for long jobs). Give the local timer a small grace so the daemon's
277
+ // own timeout fires FIRST and returns a clean error instead of us abandoning.
278
+ const timer = setTimeout(() => {
279
+ this.pending.delete(rid);
280
+ reject(new Error(`Request timeout after ${ms}ms`));
281
+ }, ms + 5_000);
282
+ this.pending.set(rid, { resolve, reject, timer });
283
+ this.send({ type: 'rpc', action, input: { ...input, _timeout: ms }, rid });
284
+ });
285
+ }
286
+ /**
287
+ * Drive DAEMON OMNI — the Claude Agent SDK session hosted in the daemon.
288
+ *
289
+ * Resolves when the turn is ACCEPTED, not when it finishes. The answer
290
+ * arrives as `omni:event` broadcasts (`agent:text-delta`, `agent:tool-call`,
291
+ * `agent:done`, …), so subscribe BEFORE calling this if you want the reply —
292
+ * a coding turn can run for minutes and a call that blocked for it would be
293
+ * indistinguishable from a hang.
294
+ *
295
+ * Its own method rather than `call()` because `call()` sends `type:'rpc'`,
296
+ * which the daemon forwards to the CLOUD. These verbs are local-only.
297
+ */
298
+ omni(
299
+ // `answer` resolves a question the turn is parked on; `interrupt` stops the
300
+ // turn without ending the conversation.
301
+ /**
302
+ * ⚠⚠ `company-status` WAS MISSING FROM THIS UNION, AND THAT IS WHY THE DIAGNOSTIC HAD NO CALLER.
303
+ * The daemon handled it and `ipc-server` routed it — both correct, both committed — but nothing
304
+ * could reach it: the documented invocation (`beehaven call --ipc omni:company-status`) is not a
305
+ * real flag, and TypeScript refused any other caller because the verb was not in this list. A
306
+ * surface whose entire job is reporting that something is silently wrong was itself silently
307
+ * unreachable. Add the verb HERE when you add one to `ipc-server`'s switch.
308
+ */
309
+ verb, input = {}, timeout) {
310
+ return new Promise((resolve, reject) => {
311
+ if (!this.socket || !this._connected)
312
+ return reject(new Error('Not connected'));
313
+ const rid = `cli_${++this.ridCounter}_${Date.now().toString(36)}`;
314
+ const ms = timeout ?? this.defaultTimeout;
315
+ const timer = setTimeout(() => {
316
+ this.pending.delete(rid);
317
+ reject(new Error(`Request timeout after ${ms}ms`));
318
+ }, ms);
319
+ this.pending.set(rid, { resolve, reject, timer });
320
+ this.send({ type: `omni:${verb}`, input, rid });
321
+ });
322
+ }
323
+ /** Send a app-rpc to a local (or cloud-routed) building and wait for response */
324
+ appCall(action, input = {}, opts) {
325
+ return new Promise((resolve, reject) => {
326
+ if (!this.socket || !this._connected) {
327
+ return reject(new Error('Not connected'));
328
+ }
329
+ const rid = `cli_${++this.ridCounter}_${Date.now().toString(36)}`;
330
+ const ms = opts?.timeout ?? this.defaultTimeout;
331
+ const timer = setTimeout(() => {
332
+ this.pending.delete(rid);
333
+ reject(new Error(`Request timeout after ${ms}ms`));
334
+ }, ms);
335
+ this.pending.set(rid, { resolve, reject, timer });
336
+ this.send({
337
+ type: 'app-rpc',
338
+ action,
339
+ input,
340
+ rid,
341
+ building: opts?.building ?? 'ableton',
342
+ instanceId: opts?.instanceId ?? 'live',
343
+ // Optional signed AgentIdentity token — the daemon verifies it and
344
+ // attaches a CallerContext to the building call. Absent ⇒ anonymous.
345
+ ...(opts?.agent ? { agent: opts.agent } : {}),
346
+ });
347
+ });
348
+ }
349
+ /** Run a multi-step pipeline against one or more buildings and wait for result */
350
+ pipeline(steps, opts) {
351
+ return new Promise((resolve, reject) => {
352
+ if (!this.socket || !this._connected) {
353
+ return reject(new Error('Not connected'));
354
+ }
355
+ const rid = `cli_${++this.ridCounter}_${Date.now().toString(36)}`;
356
+ const ms = opts?.timeout ?? 120_000; // pipelines can take longer
357
+ const timer = setTimeout(() => {
358
+ this.pending.delete(rid);
359
+ reject(new Error(`Pipeline timeout after ${ms}ms`));
360
+ }, ms);
361
+ this.pending.set(rid, { resolve, reject, timer });
362
+ this.send({
363
+ type: 'pipeline',
364
+ steps,
365
+ rid,
366
+ title: opts?.title,
367
+ building: opts?.building ?? 'ableton',
368
+ instanceId: opts?.instanceId ?? 'live',
369
+ });
370
+ });
371
+ }
372
+ /** Send a raw message (fire-and-forget) */
373
+ send(msg) {
374
+ if (!this.socket || !this._connected)
375
+ return;
376
+ try {
377
+ this.socket.write(JSON.stringify(msg) + '\n');
378
+ }
379
+ catch { }
380
+ }
381
+ /** Subscribe to broadcast patterns (e.g. "agent:*", "stream:*") */
382
+ subscribe(patterns) {
383
+ this.send({ type: 'subscribe', patterns });
384
+ }
385
+ /** Disconnect from daemon */
386
+ disconnect() {
387
+ if (this.socket) {
388
+ this.rejectAll('Disconnected');
389
+ this.socket.destroy();
390
+ this.socket = null;
391
+ this._connected = false;
392
+ }
393
+ }
394
+ // ── Internal ───
395
+ handleMessage(msg, initialResolve, initialTimeout) {
396
+ switch (msg.type) {
397
+ case 'status':
398
+ // Initial connection status — resolve the connect() promise
399
+ if (initialResolve) {
400
+ if (initialTimeout)
401
+ clearTimeout(initialTimeout);
402
+ initialResolve({ clientId: msg.clientId, daemonPid: msg.daemonPid });
403
+ }
404
+ this.emit('status', msg);
405
+ break;
406
+ case 'rpc-result':
407
+ if (msg.rid && this.pending.has(msg.rid)) {
408
+ const p = this.pending.get(msg.rid);
409
+ clearTimeout(p.timer);
410
+ this.pending.delete(msg.rid);
411
+ p.resolve(msg.result);
412
+ }
413
+ break;
414
+ case 'rpc-error':
415
+ if (msg.rid && this.pending.has(msg.rid)) {
416
+ const p = this.pending.get(msg.rid);
417
+ clearTimeout(p.timer);
418
+ this.pending.delete(msg.rid);
419
+ p.reject(new Error(msg.error));
420
+ }
421
+ break;
422
+ case 'broadcast':
423
+ this.emit('broadcast', msg.msg);
424
+ break;
425
+ case 'pong':
426
+ this.emit('pong');
427
+ break;
428
+ }
429
+ }
430
+ rejectAll(reason) {
431
+ for (const [rid, p] of this.pending) {
432
+ clearTimeout(p.timer);
433
+ p.reject(new Error(reason));
434
+ }
435
+ this.pending.clear();
436
+ }
437
+ }
438
+ // ── Convenience: one-shot RPC ──
439
+ /**
440
+ * Connect to daemon, execute a single RPC, disconnect.
441
+ * For CLI commands that need a single request-response.
442
+ *
443
+ * ⚠ ATTRIBUTION IS NOT DONE HERE — see `IpcClient.call`, which is the frame this eventually sends
444
+ * and therefore the chokepoint. It was briefly applied at this level and that was one layer too
445
+ * high: every caller holding a LONG-LIVED client (`omni`, `beehaven pipe`, the catalog fetch)
446
+ * still went out un-attributed, which is the same "second door nobody remembered" shape as the
447
+ * defect being fixed.
448
+ */
449
+ export async function rpcOnce(action, input = {}, timeout) {
450
+ const client = new IpcClient();
451
+ try {
452
+ await client.connect();
453
+ const result = await client.call(action, input, timeout);
454
+ return result;
455
+ }
456
+ finally {
457
+ client.disconnect();
458
+ }
459
+ }
460
+ /**
461
+ * Connect to daemon, execute a single app-rpc against a local building, disconnect.
462
+ */
463
+ export async function appRpcOnce(action, input = {}, opts) {
464
+ const client = new IpcClient();
465
+ try {
466
+ await client.connect();
467
+ return await client.appCall(action, input, opts);
468
+ }
469
+ finally {
470
+ client.disconnect();
471
+ }
472
+ }
473
+ /**
474
+ * Connect to daemon, run a multi-step pipeline, disconnect.
475
+ */
476
+ export async function pipelineOnce(steps, opts) {
477
+ const client = new IpcClient();
478
+ try {
479
+ await client.connect();
480
+ return await client.pipeline(steps, opts);
481
+ }
482
+ finally {
483
+ client.disconnect();
484
+ }
485
+ }
486
+ /**
487
+ * Connect to daemon and return a connected client.
488
+ * For commands that need persistent connection (pipe, watch).
489
+ */
490
+ export async function connectClient(opts) {
491
+ const client = new IpcClient(opts);
492
+ await client.connect();
493
+ return client;
494
+ }
495
+ export { IPC_SOCKET_PATH };