@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.
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- 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 };
|