@gorlitzer-labs/apiary 1.13.3

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/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/agent/index.d.ts +689 -0
  4. package/dist/agent/index.js +34 -0
  5. package/dist/agent/index.js.map +1 -0
  6. package/dist/chunk-5V7QLCGB.js +184 -0
  7. package/dist/chunk-5V7QLCGB.js.map +1 -0
  8. package/dist/chunk-AYQJIAR5.js +152 -0
  9. package/dist/chunk-AYQJIAR5.js.map +1 -0
  10. package/dist/chunk-BZ4VDWC2.js +76 -0
  11. package/dist/chunk-BZ4VDWC2.js.map +1 -0
  12. package/dist/chunk-CFY6Z6Q6.js +95 -0
  13. package/dist/chunk-CFY6Z6Q6.js.map +1 -0
  14. package/dist/chunk-CZHO4DBN.js +1719 -0
  15. package/dist/chunk-CZHO4DBN.js.map +1 -0
  16. package/dist/chunk-D7DXZQWT.js +208 -0
  17. package/dist/chunk-D7DXZQWT.js.map +1 -0
  18. package/dist/chunk-ED6ROH6B.js +2021 -0
  19. package/dist/chunk-ED6ROH6B.js.map +1 -0
  20. package/dist/chunk-PEB6HEFT.js +606 -0
  21. package/dist/chunk-PEB6HEFT.js.map +1 -0
  22. package/dist/chunk-QL66XSDB.js +157 -0
  23. package/dist/chunk-QL66XSDB.js.map +1 -0
  24. package/dist/chunk-TPNFOQA7.js +288 -0
  25. package/dist/chunk-TPNFOQA7.js.map +1 -0
  26. package/dist/chunk-ZEFRNAX6.js +1771 -0
  27. package/dist/chunk-ZEFRNAX6.js.map +1 -0
  28. package/dist/cli/index.d.ts +1 -0
  29. package/dist/cli/index.js +2432 -0
  30. package/dist/cli/index.js.map +1 -0
  31. package/dist/index-Dp3QSGog.d.ts +837 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +22 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/invites-OFXIQLLN.js +23 -0
  36. package/dist/invites-OFXIQLLN.js.map +1 -0
  37. package/dist/join-CQXEF65B.js +15 -0
  38. package/dist/join-CQXEF65B.js.map +1 -0
  39. package/dist/room-Z7KDRVYD.js +1099 -0
  40. package/dist/room-Z7KDRVYD.js.map +1 -0
  41. package/dist/run-DOADB4T7.js +90 -0
  42. package/dist/run-DOADB4T7.js.map +1 -0
  43. package/dist/update-JGE3WCQB.js +11 -0
  44. package/dist/update-JGE3WCQB.js.map +1 -0
  45. package/package.json +72 -0
@@ -0,0 +1,2432 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ join
4
+ } from "../chunk-ED6ROH6B.js";
5
+ import {
6
+ deliverInvite,
7
+ sameApiaryServer
8
+ } from "../chunk-CFY6Z6Q6.js";
9
+ import {
10
+ checkForUpdate,
11
+ printUpdateNotice
12
+ } from "../chunk-5V7QLCGB.js";
13
+ import {
14
+ agentEmoji,
15
+ clearAgentSession,
16
+ isAgentAlive,
17
+ listAgentSessions,
18
+ listRoomSessions,
19
+ resetTerminal,
20
+ resolveBindAddress,
21
+ saveAgentSession,
22
+ serve,
23
+ tmuxAttach,
24
+ tmuxAvailable,
25
+ tmuxCapturePane,
26
+ tmuxCreateSession,
27
+ tmuxInjectPaste,
28
+ tmuxInjectText,
29
+ tmuxKillSession,
30
+ tmuxPaneIsShell,
31
+ tmuxSendCommand,
32
+ tmuxSendEnter,
33
+ tmuxSendKey,
34
+ tmuxSessionExists
35
+ } from "../chunk-CZHO4DBN.js";
36
+ import "../chunk-PEB6HEFT.js";
37
+ import {
38
+ buildRuntimeMcpOptions
39
+ } from "../chunk-TPNFOQA7.js";
40
+ import {
41
+ EventProcessor,
42
+ RUNTIME_TOOL_NAMES,
43
+ SseMultiplexer,
44
+ createRuntimeMcpServer,
45
+ parseEngagementMode,
46
+ shouldReportActivity
47
+ } from "../chunk-ZEFRNAX6.js";
48
+ import {
49
+ buildShareUrl
50
+ } from "../chunk-AYQJIAR5.js";
51
+ import {
52
+ randomName
53
+ } from "../chunk-D7DXZQWT.js";
54
+ import {
55
+ contentPartsToString
56
+ } from "../chunk-QL66XSDB.js";
57
+ import "../chunk-BZ4VDWC2.js";
58
+
59
+ // src/cli/claude/run.ts
60
+ import { writeFileSync as writeFileSync2, mkdtempSync, mkdirSync, rmSync, chmodSync, existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync, renameSync as renameSync2 } from "fs";
61
+ import { join as join4 } from "path";
62
+ import { tmpdir, homedir as homedir3 } from "os";
63
+
64
+ // src/cli/composer.ts
65
+ var PROMPT_MARKER = /^\s*[›❯>]\s?/;
66
+ function composerBlock(lines) {
67
+ const tail = lines.slice(-25);
68
+ let start = -1;
69
+ for (let i = tail.length - 1; i >= 0; i--) {
70
+ if (PROMPT_MARKER.test(tail[i])) {
71
+ start = i;
72
+ break;
73
+ }
74
+ }
75
+ if (start === -1) return "";
76
+ return tail.slice(start).join(" ");
77
+ }
78
+ function submissionProbe(text) {
79
+ return text.replace(/\s+/g, " ").trim().slice(0, 40);
80
+ }
81
+ function composerStillHolds(lines, text) {
82
+ const probe = submissionProbe(text);
83
+ if (probe.length < 8) return false;
84
+ const composer = composerBlock(lines).replace(/\s+/g, " ");
85
+ return composer.includes(probe);
86
+ }
87
+
88
+ // src/cli/claude/tmux-bridge.ts
89
+ var SPINNER_CHARS = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
90
+ var BUSY_PATTERNS = [
91
+ // "← 1 agent" / "← 3 agents"
92
+ /←\s*\d+\s+agents?\b/
93
+ ];
94
+ var DIALOG_PATTERNS = [
95
+ "Enter to select",
96
+ "to navigate",
97
+ "Esc to cancel",
98
+ "Ready to code?",
99
+ "Review your answers",
100
+ "ctrl+g to edit in"
101
+ ];
102
+ var PERMISSION_PATTERNS = [
103
+ "(Y)",
104
+ "Allow ",
105
+ "Deny ",
106
+ "approve",
107
+ "Yes / No",
108
+ // Current Claude Code wording, captured from a live prompt. Without these a
109
+ // permission dialog reads as idle and the bridge pastes a room event into
110
+ // it, answering on the agent's behalf.
111
+ "Do you want to proceed?",
112
+ "don't ask again",
113
+ // The folder-trust modal, which no flag skips.
114
+ "Yes, I trust this folder"
115
+ ];
116
+ var BLOCKING_PATTERNS = [
117
+ "Do you want to proceed?",
118
+ "don't ask again",
119
+ "Yes, I trust this folder",
120
+ "Ready to code?",
121
+ "Enter to select",
122
+ /❯\s*1\.\s*Yes/,
123
+ /\(y\s*\/\s*n\)/i,
124
+ // The arrow glyphs claude renders beside it. Bare "to navigate" is prose.
125
+ /[↑↓⬆⬇]\s*(?:[↑↓⬆⬇]\s*)?to navigate/
126
+ ];
127
+ function matchesAny(text, patterns) {
128
+ const haystack = text.toLowerCase();
129
+ for (const pattern of patterns) {
130
+ if (typeof pattern === "string") {
131
+ if (haystack.includes(pattern.toLowerCase())) return true;
132
+ } else if (pattern.test(text)) {
133
+ return true;
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+ function defaultUndeliveredWarning(text) {
139
+ const preview = text.replace(/\s+/g, " ").slice(0, 80);
140
+ process.stderr.write(`apiary: message NOT delivered to the agent \u2014 still in its composer: ${preview}
141
+ `);
142
+ }
143
+ var TmuxBridge = class _TmuxBridge {
144
+ session;
145
+ queue = [];
146
+ pollTimer = null;
147
+ pollIntervalMs;
148
+ keystrokeDelayMs;
149
+ onUndelivered;
150
+ stopped = false;
151
+ blockedCount = 0;
152
+ static BLOCKED_THRESHOLD = 150;
153
+ // ~30s at 200ms poll — force inject if stuck
154
+ constructor(session, opts) {
155
+ this.session = session;
156
+ this.pollIntervalMs = opts?.pollIntervalMs ?? 200;
157
+ this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;
158
+ this.onUndelivered = opts?.onUndelivered ?? defaultUndeliveredWarning;
159
+ }
160
+ /**
161
+ * Delivery callback — drop-in replacement for the raw tmuxDeliver lambda.
162
+ * Pass `bridge.deliver.bind(bridge)` to EventProcessor.run().
163
+ */
164
+ async deliver(parts, opts) {
165
+ const text = contentPartsToString(parts);
166
+ if (!text.trim()) return;
167
+ this.inject(text, opts?.dedupe === true);
168
+ }
169
+ /**
170
+ * Detect the current TUI state by reading the screen.
171
+ * Exported for testing — the heuristic logic is in detectStateFromLines().
172
+ */
173
+ detectState() {
174
+ if (tmuxPaneIsShell(this.session)) return "absent";
175
+ const lines = this.captureScreen();
176
+ return detectStateFromLines(lines);
177
+ }
178
+ /**
179
+ * State to REPORT to the room, as opposed to the state used to decide
180
+ * whether it is safe to type. Same screen, stricter about claiming a human
181
+ * is needed — see detectReportedStateFromLines.
182
+ */
183
+ detectReportedState() {
184
+ if (tmuxPaneIsShell(this.session)) return "absent";
185
+ return detectReportedStateFromLines(this.captureScreen());
186
+ }
187
+ /**
188
+ * Extract claude's live status label from the pane (the "Sautéed for 12s",
189
+ * "Compacting conversation… 23%" lines under the prompt). Returns null when
190
+ * the agent is idle / no status visible.
191
+ *
192
+ * The room TUI calls this through the agent runtime's poll loop to surface
193
+ * "is the agent actually working or stuck?" without making the user attach
194
+ * to each tmux pane.
195
+ */
196
+ getActivityLabel() {
197
+ return extractActivityLabel(this.captureScreen());
198
+ }
199
+ /**
200
+ * Try to inject text, choosing strategy based on TUI state.
201
+ * If the state is unsafe, queues the text and starts polling.
202
+ *
203
+ * Text is flattened to a single line before injection to avoid triggering
204
+ * Claude Code's paste detection. When multi-line text arrives via
205
+ * `send-keys -l`, Claude Code detects it as a paste and collapses it into
206
+ * "[Pasted text #1 +N lines]" which may not reliably submit with Enter.
207
+ */
208
+ inject(text, dedupe) {
209
+ const flat = text.replace(/\n/g, " ");
210
+ const state = this.detectState();
211
+ switch (state) {
212
+ case "idle":
213
+ this.injectIdle(flat);
214
+ break;
215
+ case "typing":
216
+ this.injectWhileTyping(flat);
217
+ break;
218
+ default:
219
+ this.enqueue(flat, dedupe);
220
+ break;
221
+ }
222
+ }
223
+ /** Capture the screen via tmux capture-pane. */
224
+ captureScreen() {
225
+ return tmuxCapturePane(this.session);
226
+ }
227
+ /**
228
+ * Inject into an idle prompt: type text + Enter, then check it went.
229
+ *
230
+ * This used to fire a second Enter unconditionally as a "safety net" against
231
+ * Claude Code's paste detection swallowing the first. That is a guess in both
232
+ * directions: it cannot tell whether the first Enter worked, and it cannot
233
+ * tell whether the second one did either. Reading the composer back answers
234
+ * the question instead, and only sends the second Enter when it is needed.
235
+ */
236
+ injectIdle(text) {
237
+ tmuxInjectText(this.session, text);
238
+ tmuxSendEnter(this.session);
239
+ this.confirmSubmitted(text);
240
+ }
241
+ /**
242
+ * Check the message left the composer, and press Enter once more if it did not.
243
+ *
244
+ * A submitted message moves into the transcript; one still in the composer was
245
+ * never sent. If it is still there after the retry, the room is told rather
246
+ * than left to read the agent's silence as a choice.
247
+ */
248
+ confirmSubmitted(text) {
249
+ this.sleep(80);
250
+ if (!composerStillHolds(this.captureScreen(), text)) return;
251
+ tmuxSendEnter(this.session);
252
+ this.sleep(120);
253
+ if (!composerStillHolds(this.captureScreen(), text)) return;
254
+ this.onUndelivered(text);
255
+ }
256
+ /**
257
+ * Inject while the user is typing:
258
+ * 1. Ctrl+U — cut line to kill ring
259
+ * 2. Inject our text + Enter
260
+ * 3. Ctrl+Y — paste the user's text back
261
+ */
262
+ injectWhileTyping(text) {
263
+ tmuxSendKey(this.session, "C-u");
264
+ this.sleep(this.keystrokeDelayMs);
265
+ tmuxInjectText(this.session, text);
266
+ tmuxSendEnter(this.session);
267
+ this.confirmSubmitted(text);
268
+ this.sleep(this.keystrokeDelayMs);
269
+ tmuxSendKey(this.session, "C-y");
270
+ }
271
+ /** Add to queue and start polling if not already. */
272
+ enqueue(text, dedupe) {
273
+ if (dedupe && this.queue.includes(text)) return;
274
+ this.queue.push(text);
275
+ this.startPolling();
276
+ }
277
+ /** Start the polling timer to drain queued events. */
278
+ startPolling() {
279
+ if (this.pollTimer || this.stopped) return;
280
+ this.pollTimer = setInterval(() => this.drainQueue(), this.pollIntervalMs);
281
+ }
282
+ /** Stop the polling timer. */
283
+ stopPolling() {
284
+ if (this.pollTimer) {
285
+ clearInterval(this.pollTimer);
286
+ this.pollTimer = null;
287
+ }
288
+ }
289
+ /**
290
+ * Try to drain one queued event if the state is safe.
291
+ *
292
+ * Drains one event at a time rather than batching all into one multi-line
293
+ * string — multi-line text triggers Claude Code's paste detection which
294
+ * collapses it into "[Pasted text #1 +N lines]".
295
+ *
296
+ * After injecting one event, the poll continues. The next cycle re-checks
297
+ * state: if Claude is busy (streaming), remaining events wait. If idle,
298
+ * the next event is injected. Events are already flattened in inject().
299
+ */
300
+ drainQueue() {
301
+ if (this.queue.length === 0) {
302
+ this.stopPolling();
303
+ this.blockedCount = 0;
304
+ return;
305
+ }
306
+ const state = this.detectState();
307
+ if (state === "idle" || state === "typing") {
308
+ this.blockedCount = 0;
309
+ const text = this.queue.shift();
310
+ if (state === "idle") {
311
+ this.injectIdle(text);
312
+ } else {
313
+ this.injectWhileTyping(text);
314
+ }
315
+ if (this.queue.length === 0) {
316
+ this.stopPolling();
317
+ }
318
+ } else {
319
+ this.blockedCount++;
320
+ if (this.blockedCount >= _TmuxBridge.BLOCKED_THRESHOLD) {
321
+ this.blockedCount = 0;
322
+ const text = this.queue.shift();
323
+ this.injectIdle(text);
324
+ if (this.queue.length === 0) this.stopPolling();
325
+ }
326
+ }
327
+ }
328
+ /** Cleanup. */
329
+ stop() {
330
+ this.stopped = true;
331
+ this.stopPolling();
332
+ this.queue.length = 0;
333
+ }
334
+ /** Synchronous sleep — only used for tiny keystroke delays. */
335
+ sleep(ms) {
336
+ if (ms <= 0) return;
337
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
338
+ }
339
+ };
340
+ var ACTIVITY_LIVE = /([A-Za-zÀ-ÿ]+…\s*\([^)\n]*\))/;
341
+ var ACTIVITY_COOK = /([A-Za-zÀ-ÿ]+ed\s+for\s+\d+s)/;
342
+ var ACTIVITY_PROG = /((?:Compacting|Thinking|Loading|Connecting)[^()]*?(?:\s+\d+%)?)(?=\s*[(\n]|\s*$)/;
343
+ function extractActivityLabel(lines) {
344
+ if (lines.length === 0) return null;
345
+ let best = null;
346
+ for (const line of lines.slice(-25)) {
347
+ const m = line.match(ACTIVITY_PROG) ?? line.match(ACTIVITY_COOK) ?? line.match(ACTIVITY_LIVE);
348
+ if (m) best = m[1].trim();
349
+ }
350
+ return best;
351
+ }
352
+ function detectStateFromLines(lines, opts) {
353
+ if (lines.length === 0) return "unknown";
354
+ const tail = lines.slice(-30);
355
+ const tailText = tail.join("\n");
356
+ if (opts?.ignorePrompts !== true) {
357
+ for (const pattern of DIALOG_PATTERNS) {
358
+ if (tailText.includes(pattern)) return "dialog";
359
+ }
360
+ for (const pattern of PERMISSION_PATTERNS) {
361
+ if (tailText.includes(pattern)) return "permission";
362
+ }
363
+ }
364
+ const lastFew = tail.slice(-5).join("");
365
+ for (const ch of SPINNER_CHARS) {
366
+ if (lastFew.includes(ch)) return "streaming";
367
+ }
368
+ for (const pattern of BUSY_PATTERNS) {
369
+ if (pattern.test(tailText)) return "streaming";
370
+ }
371
+ const promptChar = /^[❯›](\s|$)/;
372
+ const footerChar = /^[❯›]{2}\s/;
373
+ const separatorLine = /^[─━─\-]{10,}/;
374
+ for (let i = tail.length - 1; i >= 0; i--) {
375
+ const line = tail[i].trimStart();
376
+ if (footerChar.test(line)) {
377
+ for (let j = i - 1; j >= 0; j--) {
378
+ const above = tail[j].trimStart();
379
+ if (promptChar.test(above)) {
380
+ const content = above.replace(/^[❯›]\s*/, "").trim();
381
+ return content.length === 0 ? "idle" : "typing";
382
+ }
383
+ }
384
+ break;
385
+ }
386
+ }
387
+ for (let i = tail.length - 1; i >= 0; i--) {
388
+ const line = tail[i].trimStart();
389
+ if (promptChar.test(line)) {
390
+ const above = i > 0 ? tail[i - 1].trimStart() : "";
391
+ const below = i < tail.length - 1 ? tail[i + 1].trimStart() : "";
392
+ if (separatorLine.test(above) || separatorLine.test(below)) {
393
+ const content = line.replace(/^[❯›]\s*/, "").trim();
394
+ return content.length === 0 ? "idle" : "typing";
395
+ }
396
+ }
397
+ }
398
+ return "unknown";
399
+ }
400
+ function detectReportedStateFromLines(lines) {
401
+ const loose = detectStateFromLines(lines);
402
+ if (loose !== "dialog" && loose !== "permission") return loose;
403
+ if (matchesAny(lines.slice(-30).join("\n"), BLOCKING_PATTERNS)) return loose;
404
+ return detectStateFromLines(lines, { ignorePrompts: true });
405
+ }
406
+
407
+ // src/cli/runtime-setup.ts
408
+ async function setupAgentRuntime(options) {
409
+ const agentName = options.name ?? randomName();
410
+ const authority = options.authority ?? (options.admin ? "admin" : void 0);
411
+ const pendingUrls = options.joinUrls ?? [];
412
+ const joinResults = [];
413
+ const sseMux = new SseMultiplexer();
414
+ const processor = new EventProcessor("", agentName, {
415
+ defaultMode: options.mode ?? "everyone"
416
+ });
417
+ const mcpServer = await createRuntimeMcpServer(
418
+ buildRuntimeMcpOptions({
419
+ processor,
420
+ sseMux,
421
+ joinResults,
422
+ agentName,
423
+ authority,
424
+ onRoomJoined: options.onRoomJoined
425
+ })
426
+ );
427
+ const wrappedSource = {
428
+ [Symbol.asyncIterator]() {
429
+ const inner = sseMux[Symbol.asyncIterator]();
430
+ return {
431
+ async next() {
432
+ const result = await inner.next();
433
+ if (!result.done) {
434
+ const { roomId, event } = result.value;
435
+ const jr = joinResults.find((j) => j.roomId === roomId);
436
+ if (jr) {
437
+ if (event.type === "ParticipantJoined") {
438
+ jr.dataSource.addParticipant(event.participant);
439
+ } else if (event.type === "ParticipantLeft") {
440
+ jr.dataSource.removeParticipant(event.participant_id);
441
+ }
442
+ }
443
+ }
444
+ return result;
445
+ }
446
+ };
447
+ }
448
+ };
449
+ async function cleanup() {
450
+ await processor.stop();
451
+ sseMux.close();
452
+ await mcpServer.stop();
453
+ for (const jr of joinResults) {
454
+ try {
455
+ await fetch(`${jr.serverUrl}/disconnect`, {
456
+ method: "POST",
457
+ headers: { "Content-Type": "application/json" },
458
+ body: JSON.stringify({ token: jr.sessionToken })
459
+ });
460
+ } catch {
461
+ }
462
+ }
463
+ }
464
+ let initialParts;
465
+ if (pendingUrls.length > 0) {
466
+ if (pendingUrls.length === 1) {
467
+ initialParts = [{ type: "text", text: `Use join_room("${pendingUrls[0]}") to connect.` }];
468
+ } else {
469
+ const lines = pendingUrls.map((u) => ` join_room("${u}")`);
470
+ initialParts = [{ type: "text", text: `Rooms to join:
471
+ ${lines.join("\n")}` }];
472
+ }
473
+ }
474
+ return {
475
+ agentName,
476
+ joinResults,
477
+ initialParts,
478
+ processor,
479
+ sseMux,
480
+ mcpServer,
481
+ wrappedSource,
482
+ cleanup
483
+ };
484
+ }
485
+
486
+ // src/cli/agent-state.ts
487
+ function codexStateToActivity(state) {
488
+ switch (state) {
489
+ case "absent":
490
+ return "blocked";
491
+ case "approval":
492
+ case "blocked":
493
+ return "blocked";
494
+ case "streaming":
495
+ return "working";
496
+ case "idle":
497
+ case "typing":
498
+ return "idle";
499
+ case "unknown":
500
+ default:
501
+ return void 0;
502
+ }
503
+ }
504
+ function claudeStateToActivity(state) {
505
+ switch (state) {
506
+ case "absent":
507
+ return "blocked";
508
+ case "permission":
509
+ case "dialog":
510
+ return "blocked";
511
+ case "streaming":
512
+ return "working";
513
+ case "idle":
514
+ case "typing":
515
+ return "idle";
516
+ case "unknown":
517
+ default:
518
+ return void 0;
519
+ }
520
+ }
521
+
522
+ // src/cli/approvals.ts
523
+ var APPROVALS_ENV = "APIARY_AGENT_APPROVALS";
524
+ function wantsUnattendedAgents(env = process.env) {
525
+ return env[APPROVALS_ENV]?.trim().toLowerCase() !== "ask";
526
+ }
527
+
528
+ // src/cli/claude/trust.ts
529
+ import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from "fs";
530
+ import { join as join2, resolve } from "path";
531
+ import { homedir } from "os";
532
+ function claudeConfigPath(home = homedir()) {
533
+ return join2(home, ".claude.json");
534
+ }
535
+ function preAcceptClaudeTrust(cwd, opts) {
536
+ if (opts?.enabled === false) return "skipped";
537
+ const path = opts?.configPath ?? claudeConfigPath();
538
+ const key = resolve(cwd);
539
+ if (!existsSync(path)) return "no-config";
540
+ let parsed;
541
+ let raw;
542
+ try {
543
+ raw = readFileSync(path, "utf-8");
544
+ parsed = JSON.parse(raw);
545
+ } catch {
546
+ return "failed";
547
+ }
548
+ if (typeof parsed !== "object" || parsed === null) return "failed";
549
+ const projects = parsed.projects ?? {};
550
+ if (typeof projects !== "object" || projects === null) return "failed";
551
+ const existing = projects[key];
552
+ if (existing && existing.hasTrustDialogAccepted === true) return "already-trusted";
553
+ projects[key] = { ...existing ?? {}, hasTrustDialogAccepted: true };
554
+ parsed.projects = projects;
555
+ const tmp = `${path}.apiary-${process.pid}.tmp`;
556
+ try {
557
+ writeFileSync(tmp, JSON.stringify(parsed, null, 2), { mode: 384 });
558
+ renameSync(tmp, path);
559
+ return "trusted";
560
+ } catch {
561
+ try {
562
+ unlinkSync(tmp);
563
+ } catch {
564
+ }
565
+ return "failed";
566
+ }
567
+ }
568
+
569
+ // src/cli/claude/jsonl-stats.ts
570
+ import { readdirSync, statSync, existsSync as existsSync2, openSync, readSync, closeSync } from "fs";
571
+ import { join as join3 } from "path";
572
+ import { homedir as homedir2 } from "os";
573
+ function cwdToProjectDir(cwd) {
574
+ return cwd.replace(/[/.]/g, "-");
575
+ }
576
+ function findLatestJsonl(cwd) {
577
+ const projectsDir = join3(homedir2(), ".claude", "projects", cwdToProjectDir(cwd));
578
+ if (!existsSync2(projectsDir)) return null;
579
+ let newest = null;
580
+ for (const f of readdirSync(projectsDir)) {
581
+ if (!f.endsWith(".jsonl")) continue;
582
+ const p = join3(projectsDir, f);
583
+ try {
584
+ const st = statSync(p);
585
+ if (!newest || st.mtimeMs > newest.mtime) newest = { path: p, mtime: st.mtimeMs };
586
+ } catch {
587
+ }
588
+ }
589
+ return newest?.path ?? null;
590
+ }
591
+ var tailStates = /* @__PURE__ */ new Map();
592
+ function freshState(path) {
593
+ return {
594
+ path,
595
+ offset: 0,
596
+ totals: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreateTokens: 0 },
597
+ lastTurnContextTokens: 0,
598
+ lastModel: "",
599
+ carry: ""
600
+ };
601
+ }
602
+ function readAgentMetrics(cwd) {
603
+ const path = findLatestJsonl(cwd);
604
+ if (!path) return null;
605
+ let state = tailStates.get(cwd);
606
+ if (state && state.path !== path) state = void 0;
607
+ if (!state) {
608
+ state = freshState(path);
609
+ tailStates.set(cwd, state);
610
+ }
611
+ let size;
612
+ try {
613
+ size = statSync(path).size;
614
+ } catch {
615
+ return null;
616
+ }
617
+ if (size < state.offset) {
618
+ state = freshState(path);
619
+ tailStates.set(cwd, state);
620
+ }
621
+ if (size === state.offset && state.lastModel === "") return null;
622
+ if (size === state.offset) return snapshot(state);
623
+ const chunkLen = size - state.offset;
624
+ const buf = Buffer.alloc(chunkLen);
625
+ let fd;
626
+ try {
627
+ fd = openSync(path, "r");
628
+ } catch {
629
+ return snapshot(state);
630
+ }
631
+ try {
632
+ readSync(fd, buf, 0, chunkLen, state.offset);
633
+ } finally {
634
+ closeSync(fd);
635
+ }
636
+ state.offset = size;
637
+ const text = state.carry + buf.toString("utf-8");
638
+ const newlineEnd = text.lastIndexOf("\n");
639
+ const completeLines = newlineEnd >= 0 ? text.slice(0, newlineEnd) : "";
640
+ state.carry = newlineEnd >= 0 ? text.slice(newlineEnd + 1) : text;
641
+ for (const line of completeLines.split("\n")) {
642
+ consumeLine(state, line);
643
+ }
644
+ return snapshot(state);
645
+ }
646
+ function snapshot(s) {
647
+ return {
648
+ ...s.totals,
649
+ lastTurnContextTokens: s.lastTurnContextTokens,
650
+ lastModel: s.lastModel
651
+ };
652
+ }
653
+ function consumeLine(state, line) {
654
+ if (!line.trim()) return;
655
+ let event;
656
+ try {
657
+ event = JSON.parse(line);
658
+ } catch {
659
+ return;
660
+ }
661
+ const msg = event.message;
662
+ if (!msg || typeof msg !== "object") return;
663
+ const usage = msg.usage;
664
+ if (!usage) return;
665
+ const inp = Number(usage.input_tokens) || 0;
666
+ const out = Number(usage.output_tokens) || 0;
667
+ const cr = Number(usage.cache_read_input_tokens) || 0;
668
+ const cw = Number(usage.cache_creation_input_tokens) || 0;
669
+ state.totals.inputTokens += inp;
670
+ state.totals.outputTokens += out;
671
+ state.totals.cacheReadTokens += cr;
672
+ state.totals.cacheCreateTokens += cw;
673
+ if (typeof msg.model === "string") state.lastModel = msg.model;
674
+ state.lastTurnContextTokens = inp + cr + cw;
675
+ }
676
+
677
+ // src/cli/claude/run.ts
678
+ var SKIP_PERMISSIONS_FLAG = "--dangerously-skip-permissions";
679
+ var SESSION_DIR = join4(homedir3(), ".apiary", "sessions");
680
+ function sessionFilePath(name) {
681
+ return join4(SESSION_DIR, `claude_${name}.json`);
682
+ }
683
+ function saveSession(session) {
684
+ const dir = SESSION_DIR;
685
+ if (!existsSync3(dir)) {
686
+ mkdirSync(dir, { recursive: true });
687
+ }
688
+ writeFileSync2(sessionFilePath(session.agentName), JSON.stringify(session, null, 2));
689
+ }
690
+ function clearSession(name) {
691
+ const path = sessionFilePath(name);
692
+ try {
693
+ rmSync(path);
694
+ } catch {
695
+ }
696
+ }
697
+ function pretrustCwd(cwd) {
698
+ try {
699
+ const path = join4(homedir3(), ".claude.json");
700
+ if (!existsSync3(path)) return;
701
+ const real = realpathSync(cwd);
702
+ const raw = readFileSync2(path, "utf-8");
703
+ const data = JSON.parse(raw);
704
+ if (typeof data !== "object" || data === null) return;
705
+ const projects = data.projects ??= {};
706
+ const entry = projects[real] ??= {};
707
+ const needsWrite = entry.hasTrustDialogAccepted !== true || entry.hasCompletedProjectOnboarding !== true || !(typeof entry.projectOnboardingSeenCount === "number" && entry.projectOnboardingSeenCount >= 1);
708
+ if (!needsWrite) return;
709
+ entry.hasTrustDialogAccepted = true;
710
+ entry.hasCompletedProjectOnboarding = true;
711
+ if (!(typeof entry.projectOnboardingSeenCount === "number" && entry.projectOnboardingSeenCount >= 1)) {
712
+ entry.projectOnboardingSeenCount = 1;
713
+ }
714
+ const tmpPath = `${path}.apiary-${process.pid}.tmp`;
715
+ writeFileSync2(tmpPath, JSON.stringify(data, null, 2));
716
+ renameSync2(tmpPath, path);
717
+ } catch {
718
+ }
719
+ }
720
+ function listClaudeSessions() {
721
+ if (!existsSync3(SESSION_DIR)) return [];
722
+ const files = readdirSync2(SESSION_DIR);
723
+ const sessions = [];
724
+ for (const f of files) {
725
+ if (!f.startsWith("claude_") || !f.endsWith(".json")) continue;
726
+ try {
727
+ const s = JSON.parse(readFileSync2(join4(SESSION_DIR, f), "utf-8"));
728
+ sessions.push(s);
729
+ } catch {
730
+ }
731
+ }
732
+ return sessions;
733
+ }
734
+ var MCP_STDIO_BRIDGE = [
735
+ "#!/usr/bin/env node",
736
+ '"use strict";',
737
+ 'const { createInterface } = require("readline");',
738
+ "const url = `http://127.0.0.1:${process.argv[2]}/mcp`;",
739
+ "const rl = createInterface({ input: process.stdin });",
740
+ "(async () => {",
741
+ " for await (const line of rl) {",
742
+ " if (!line.trim()) continue;",
743
+ " try {",
744
+ " const res = await fetch(url, {",
745
+ ' method: "POST",',
746
+ ' headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },',
747
+ " body: line,",
748
+ " });",
749
+ " if (res.status === 202) continue;",
750
+ " const body = await res.text();",
751
+ ' for (const bl of body.split("\\n")) {',
752
+ " const m = bl.match(/^data: (.+)/);",
753
+ ' if (m) process.stdout.write(m[1] + "\\n");',
754
+ " }",
755
+ " } catch {",
756
+ " process.exit(1);",
757
+ " }",
758
+ " }",
759
+ "})();"
760
+ ].join("\n");
761
+ async function runClaude(options) {
762
+ if (options.headless) {
763
+ const setup2 = await setupAgentRuntime(options);
764
+ const deliver = async (parts) => {
765
+ const text = contentPartsToString(parts);
766
+ if (text.trim()) process.stdout.write(text + "\n");
767
+ };
768
+ const eventLoopPromise2 = setup2.processor.run(deliver, setup2.wrappedSource, setup2.initialParts).catch(() => {
769
+ });
770
+ process.stderr.write(`MCP server: ${setup2.mcpServer.url}
771
+ `);
772
+ await new Promise((resolve3) => {
773
+ process.on("SIGINT", resolve3);
774
+ process.on("SIGTERM", resolve3);
775
+ });
776
+ await setup2.cleanup();
777
+ await eventLoopPromise2;
778
+ return;
779
+ }
780
+ if (!tmuxAvailable()) {
781
+ console.error("Error: tmux is required but not found. Install it with: brew install tmux");
782
+ process.exit(1);
783
+ }
784
+ if (options.resume) {
785
+ return resumeClaude(options);
786
+ }
787
+ const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
788
+ const tmpDir = mkdtempSync(join4(tmpdir(), "apiary_agent_"));
789
+ const bridgePath = join4(tmpDir, "mcp-bridge.cjs");
790
+ writeFileSync2(bridgePath, MCP_STDIO_BRIDGE);
791
+ chmodSync(bridgePath, 493);
792
+ const mcpPort = new URL(setup.mcpServer.url).port;
793
+ const mcpConfigPath = join4(tmpDir, "mcp.json");
794
+ const mcpConfig = {
795
+ mcpServers: {
796
+ apiary: {
797
+ type: "stdio",
798
+ command: process.execPath,
799
+ args: [bridgePath, mcpPort]
800
+ }
801
+ }
802
+ };
803
+ writeFileSync2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2));
804
+ const tmuxSession = `apiary_${setup.agentName}`;
805
+ if (tmuxSessionExists(tmuxSession)) {
806
+ tmuxKillSession(tmuxSession);
807
+ }
808
+ const home = homedir3();
809
+ const cwdFull = process.cwd();
810
+ const cwdShort = cwdFull.startsWith(home) ? "~" + cwdFull.slice(home.length) : cwdFull;
811
+ const tabTitle = `${agentEmoji(setup.agentName)} ${setup.agentName} \xB7 ${cwdShort}`;
812
+ console.log("Launching Claude Code...");
813
+ tmuxCreateSession(tmuxSession, tabTitle);
814
+ pretrustCwd(process.cwd());
815
+ if (wantsUnattendedAgents()) {
816
+ const trust = preAcceptClaudeTrust(process.cwd());
817
+ if (trust === "failed" && !options.background) {
818
+ console.log("Note: could not pre-accept Claude's folder-trust dialog \u2014 the agent may ask once.");
819
+ }
820
+ }
821
+ const extraArgs = options.extraArgs ?? [];
822
+ const flags = [`claude --mcp-config ${mcpConfigPath} --strict-mcp-config`];
823
+ if (wantsUnattendedAgents() && !extraArgs.includes(SKIP_PERMISSIONS_FLAG)) {
824
+ flags.push(SKIP_PERMISSIONS_FLAG);
825
+ }
826
+ const claudeCmd = [...flags, ...extraArgs].join(" ");
827
+ tmuxSendCommand(tmuxSession, claudeCmd);
828
+ const bridge = new TmuxBridge(tmuxSession);
829
+ const eventLoopPromise = setup.processor.run(bridge.deliver.bind(bridge), setup.wrappedSource).catch(() => {
830
+ });
831
+ let lastActivityLabel = null;
832
+ let lastState;
833
+ let lastReportAt = 0;
834
+ const activityTimer = setInterval(() => {
835
+ let label;
836
+ let state;
837
+ try {
838
+ label = bridge.getActivityLabel();
839
+ state = claudeStateToActivity(bridge.detectReportedState());
840
+ } catch {
841
+ return;
842
+ }
843
+ if (!shouldReportActivity({
844
+ label,
845
+ state,
846
+ lastLabel: lastActivityLabel,
847
+ lastState,
848
+ lastReportAt,
849
+ now: Date.now()
850
+ })) return;
851
+ lastActivityLabel = label;
852
+ lastState = state;
853
+ lastReportAt = Date.now();
854
+ setup.processor.broadcastActivity(label, state).catch(() => {
855
+ });
856
+ }, 1500);
857
+ activityTimer.unref();
858
+ let lastBroadcastCtx = -1;
859
+ const metricsTimer = setInterval(() => {
860
+ try {
861
+ const m = readAgentMetrics(process.cwd());
862
+ if (!m) return;
863
+ if (m.lastTurnContextTokens === lastBroadcastCtx) return;
864
+ lastBroadcastCtx = m.lastTurnContextTokens;
865
+ setup.processor.broadcastMetrics({
866
+ input_tokens: m.inputTokens,
867
+ output_tokens: m.outputTokens,
868
+ cache_read_tokens: m.cacheReadTokens,
869
+ cache_create_tokens: m.cacheCreateTokens,
870
+ last_ctx_tokens: m.lastTurnContextTokens,
871
+ model: m.lastModel
872
+ }).catch(() => {
873
+ });
874
+ } catch {
875
+ }
876
+ }, 1e4);
877
+ metricsTimer.unref();
878
+ for (let i = 0; i < 10; i++) {
879
+ await new Promise((r) => setTimeout(r, 500));
880
+ if (!tmuxSessionExists(tmuxSession)) {
881
+ console.error("Error: Claude Code exited during startup. Try running again.");
882
+ clearInterval(activityTimer);
883
+ clearInterval(metricsTimer);
884
+ bridge.stop();
885
+ await setup.cleanup();
886
+ try {
887
+ rmSync(tmpDir, { recursive: true });
888
+ } catch {
889
+ }
890
+ resetTerminal();
891
+ return;
892
+ }
893
+ }
894
+ const inviteAbort = new AbortController();
895
+ const invitePromise = deliverInvite({
896
+ agentName: setup.agentName,
897
+ deliver: (parts) => bridge.deliver(parts, { dedupe: true }),
898
+ hasJoined: (url) => setup.joinResults.some((jr) => sameApiaryServer(jr.serverUrl, url)),
899
+ signal: inviteAbort.signal
900
+ }).then((outcome) => {
901
+ if (outcome === "abandoned" && !options.background) {
902
+ console.log(
903
+ `Note: "${setup.agentName}" did not join the room it was invited to. The invite is still queued \u2014 check the pane, then re-run or use "apiary room resume".`
904
+ );
905
+ }
906
+ return outcome;
907
+ }).catch(() => "cancelled");
908
+ saveSession({
909
+ tmuxSession,
910
+ agentName: setup.agentName,
911
+ tmpDir,
912
+ mcpPort,
913
+ pid: process.pid
914
+ });
915
+ if (options.background) {
916
+ const noop = (() => true);
917
+ process.stdout.write = noop;
918
+ process.stderr.write = noop;
919
+ await new Promise((resolve3) => {
920
+ process.on("SIGTERM", resolve3);
921
+ process.on("SIGINT", resolve3);
922
+ });
923
+ clearInterval(activityTimer);
924
+ clearInterval(metricsTimer);
925
+ inviteAbort.abort();
926
+ await invitePromise;
927
+ bridge.stop();
928
+ await setup.cleanup();
929
+ if (tmuxSessionExists(tmuxSession)) tmuxKillSession(tmuxSession);
930
+ try {
931
+ rmSync(tmpDir, { recursive: true });
932
+ } catch {
933
+ }
934
+ clearSession(setup.agentName);
935
+ return;
936
+ }
937
+ console.log("Attaching...");
938
+ console.log(`(Ctrl+B D to detach \u2014 resume with: apiary claude ${setup.agentName} --resume)
939
+ `);
940
+ try {
941
+ await tmuxAttach(tmuxSession);
942
+ } catch {
943
+ }
944
+ if (tmuxSessionExists(tmuxSession)) {
945
+ console.log(`"${setup.agentName}" still running in background.`);
946
+ console.log(` Resume: apiary claude ${setup.agentName} --resume`);
947
+ console.log(` Stop: apiary stop ${setup.agentName}`);
948
+ const noop = (() => true);
949
+ process.stdout.write = noop;
950
+ process.stderr.write = noop;
951
+ await new Promise((resolve3) => {
952
+ process.on("SIGINT", resolve3);
953
+ process.on("SIGTERM", resolve3);
954
+ });
955
+ }
956
+ clearInterval(activityTimer);
957
+ clearInterval(metricsTimer);
958
+ inviteAbort.abort();
959
+ await invitePromise;
960
+ bridge.stop();
961
+ await setup.cleanup();
962
+ if (tmuxSessionExists(tmuxSession)) tmuxKillSession(tmuxSession);
963
+ try {
964
+ rmSync(tmpDir, { recursive: true });
965
+ } catch {
966
+ }
967
+ clearSession(setup.agentName);
968
+ resetTerminal();
969
+ }
970
+ async function resumeClaude(options) {
971
+ const name = options.name;
972
+ const sessions = listClaudeSessions();
973
+ if (sessions.length === 0) {
974
+ console.error("No active Claude sessions to resume.");
975
+ process.exit(1);
976
+ }
977
+ let session;
978
+ if (name) {
979
+ session = sessions.find((s) => s.agentName === name);
980
+ if (!session) {
981
+ console.error(`No active session named "${name}". Active sessions:`);
982
+ for (const s of sessions) console.error(` - ${s.agentName}`);
983
+ process.exit(1);
984
+ }
985
+ } else if (sessions.length === 1) {
986
+ session = sessions[0];
987
+ } else {
988
+ console.error("Multiple active sessions. Specify which one with --name:");
989
+ for (const s of sessions) console.error(` - ${s.agentName}`);
990
+ process.exit(1);
991
+ }
992
+ if (!tmuxSessionExists(session.tmuxSession)) {
993
+ console.error(`Session "${session.agentName}" tmux session is gone. Cleaning up.`);
994
+ clearSession(session.agentName);
995
+ process.exit(1);
996
+ }
997
+ try {
998
+ process.kill(session.pid, 0);
999
+ } catch {
1000
+ console.error(`Session "${session.agentName}" background process (pid ${session.pid}) is gone. Cleaning up.`);
1001
+ tmuxKillSession(session.tmuxSession);
1002
+ clearSession(session.agentName);
1003
+ try {
1004
+ rmSync(session.tmpDir, { recursive: true });
1005
+ } catch {
1006
+ }
1007
+ process.exit(1);
1008
+ }
1009
+ console.log(`Resuming session "${session.agentName}"...
1010
+ `);
1011
+ try {
1012
+ await tmuxAttach(session.tmuxSession);
1013
+ } catch {
1014
+ }
1015
+ if (tmuxSessionExists(session.tmuxSession)) {
1016
+ console.log(`"${session.agentName}" still running in background.`);
1017
+ console.log(` Resume: apiary claude ${session.agentName} --resume`);
1018
+ console.log(` Stop: apiary stop ${session.agentName}`);
1019
+ }
1020
+ }
1021
+ async function stopClaude(name, all) {
1022
+ const sessions = listClaudeSessions();
1023
+ if (sessions.length === 0) {
1024
+ if (all) return;
1025
+ console.error("No active Claude sessions.");
1026
+ process.exit(1);
1027
+ }
1028
+ if (all) {
1029
+ for (const s of sessions) {
1030
+ stopSession(s);
1031
+ }
1032
+ return;
1033
+ }
1034
+ let session;
1035
+ if (name) {
1036
+ session = sessions.find((s) => s.agentName === name);
1037
+ if (!session) {
1038
+ console.error(`No active session named "${name}". Active sessions:`);
1039
+ for (const s of sessions) console.error(` - ${s.agentName}`);
1040
+ process.exit(1);
1041
+ }
1042
+ } else if (sessions.length === 1) {
1043
+ session = sessions[0];
1044
+ } else {
1045
+ console.error("Multiple active sessions. Use --name or --all:");
1046
+ for (const s of sessions) console.error(` - ${s.agentName}`);
1047
+ process.exit(1);
1048
+ }
1049
+ stopSession(session);
1050
+ }
1051
+ function stopSession(session) {
1052
+ if (tmuxSessionExists(session.tmuxSession)) {
1053
+ tmuxKillSession(session.tmuxSession);
1054
+ }
1055
+ try {
1056
+ process.kill(session.pid, "SIGTERM");
1057
+ } catch {
1058
+ }
1059
+ try {
1060
+ rmSync(session.tmpDir, { recursive: true });
1061
+ } catch {
1062
+ }
1063
+ clearSession(session.agentName);
1064
+ resetTerminal();
1065
+ console.log(`Stopped "${session.agentName}".`);
1066
+ }
1067
+
1068
+ // src/cli/opencode/run.ts
1069
+ import { spawn } from "child_process";
1070
+ function listOpencodeSessions() {
1071
+ return listAgentSessions("opencode");
1072
+ }
1073
+ async function runOpencode(options) {
1074
+ const opencodePort = 14096 + Math.floor(Math.random() * 1e3);
1075
+ const opencodeUrl = `http://127.0.0.1:${opencodePort}`;
1076
+ const roomSessions = /* @__PURE__ */ new Map();
1077
+ async function findApiarySession() {
1078
+ try {
1079
+ const res = await fetch(`${opencodeUrl}/session`);
1080
+ if (!res.ok) return null;
1081
+ const sessions = await res.json();
1082
+ for (const sess of sessions.slice(0, 3)) {
1083
+ const msgRes = await fetch(`${opencodeUrl}/session/${sess.id}/message`);
1084
+ if (!msgRes.ok) continue;
1085
+ const messages = await msgRes.json();
1086
+ for (const msg of messages) {
1087
+ for (const part of msg.parts ?? []) {
1088
+ if (part.type === "tool" && part.tool?.includes("apiary__")) {
1089
+ return sess.id;
1090
+ }
1091
+ }
1092
+ }
1093
+ }
1094
+ return sessions.length > 0 ? sessions[0].id : null;
1095
+ } catch {
1096
+ return null;
1097
+ }
1098
+ }
1099
+ const setup = await setupAgentRuntime({
1100
+ ...options,
1101
+ joinUrls: void 0
1102
+ });
1103
+ const opencodeConfig = {
1104
+ mcp: {
1105
+ apiary: {
1106
+ type: "remote",
1107
+ url: setup.mcpServer.url,
1108
+ oauth: false
1109
+ }
1110
+ }
1111
+ };
1112
+ const extraArgs = options.extraArgs ?? [];
1113
+ const opencodeArgs = ["serve", "--port", String(opencodePort), ...extraArgs];
1114
+ console.log("Launching OpenCode...");
1115
+ let child;
1116
+ try {
1117
+ child = spawn("opencode", opencodeArgs, {
1118
+ env: {
1119
+ ...process.env,
1120
+ OPENCODE_CONFIG_CONTENT: JSON.stringify(opencodeConfig)
1121
+ },
1122
+ stdio: ["ignore", "pipe", "pipe"]
1123
+ });
1124
+ } catch {
1125
+ console.error("Error: opencode is required but not found. Install it from https://opencode.ai");
1126
+ await setup.cleanup();
1127
+ process.exit(1);
1128
+ }
1129
+ child.stderr?.on("data", (chunk) => {
1130
+ process.stderr.write(chunk);
1131
+ });
1132
+ let childExited = false;
1133
+ child.on("exit", () => {
1134
+ childExited = true;
1135
+ });
1136
+ child.on("error", async () => {
1137
+ console.error("Error: failed to start opencode. Is it installed?");
1138
+ await setup.cleanup();
1139
+ process.exit(1);
1140
+ });
1141
+ const ready = await pollForReady(opencodeUrl, 3e4);
1142
+ if (!ready) {
1143
+ console.error("OpenCode did not become ready within 30 seconds.");
1144
+ child.kill();
1145
+ await setup.cleanup();
1146
+ process.exit(1);
1147
+ }
1148
+ console.log(` OpenCode running on ${opencodeUrl}`);
1149
+ saveAgentSession({
1150
+ runtime: "opencode",
1151
+ agentName: setup.agentName,
1152
+ pid: process.pid,
1153
+ childPid: child.pid
1154
+ });
1155
+ async function deliver(parts) {
1156
+ const roomId = setup.processor.currentContextRoomId;
1157
+ if (!roomId) return;
1158
+ if (!roomSessions.has(roomId)) {
1159
+ const sid = await findApiarySession();
1160
+ if (!sid) return;
1161
+ roomSessions.set(roomId, sid);
1162
+ console.log(` Linked room ${roomId} \u2192 session ${sid}`);
1163
+ }
1164
+ const targetSession = roomSessions.get(roomId);
1165
+ const text = contentPartsToString(parts);
1166
+ if (!text.trim()) return;
1167
+ try {
1168
+ const res = await fetch(`${opencodeUrl}/session/${targetSession}/message`, {
1169
+ method: "POST",
1170
+ headers: { "Content-Type": "application/json" },
1171
+ body: JSON.stringify({
1172
+ parts: [{ type: "text", text }]
1173
+ })
1174
+ });
1175
+ await res.text();
1176
+ } catch {
1177
+ }
1178
+ }
1179
+ const eventLoopPromise = setup.processor.run(deliver, setup.wrappedSource);
1180
+ console.log(`
1181
+ OpenCode agent running.`);
1182
+ console.log(` To watch: opencode attach ${opencodeUrl}
1183
+ `);
1184
+ const exitPromise = new Promise((resolve3) => {
1185
+ child.on("exit", resolve3);
1186
+ });
1187
+ const signalPromise = new Promise((resolve3) => {
1188
+ const handler = () => {
1189
+ resolve3();
1190
+ };
1191
+ process.on("SIGINT", handler);
1192
+ process.on("SIGTERM", handler);
1193
+ });
1194
+ await Promise.race([exitPromise, signalPromise]);
1195
+ if (!childExited) {
1196
+ child.kill();
1197
+ }
1198
+ await setup.cleanup();
1199
+ clearAgentSession("opencode", setup.agentName);
1200
+ console.log("Disconnected.");
1201
+ }
1202
+ async function stopOpencode(name, all) {
1203
+ const sessions = listOpencodeSessions();
1204
+ if (sessions.length === 0) {
1205
+ if (all) return;
1206
+ console.error("No active OpenCode sessions.");
1207
+ process.exit(1);
1208
+ }
1209
+ if (all) {
1210
+ for (const s of sessions) stopOpencodeSession(s);
1211
+ return;
1212
+ }
1213
+ let session;
1214
+ if (name) {
1215
+ session = sessions.find((s) => s.agentName === name);
1216
+ if (!session) {
1217
+ console.error(`No active OpenCode session named "${name}". Active sessions:`);
1218
+ for (const s of sessions) console.error(` - ${s.agentName}`);
1219
+ process.exit(1);
1220
+ }
1221
+ } else if (sessions.length === 1) {
1222
+ session = sessions[0];
1223
+ } else {
1224
+ console.error("Multiple active OpenCode sessions. Use --name or --all:");
1225
+ for (const s of sessions) console.error(` - ${s.agentName}`);
1226
+ process.exit(1);
1227
+ }
1228
+ stopOpencodeSession(session);
1229
+ }
1230
+ function stopOpencodeSession(session) {
1231
+ if (session.childPid) {
1232
+ try {
1233
+ process.kill(session.childPid, "SIGTERM");
1234
+ } catch {
1235
+ }
1236
+ }
1237
+ if (isAgentAlive(session)) {
1238
+ try {
1239
+ process.kill(session.pid, "SIGTERM");
1240
+ } catch {
1241
+ }
1242
+ }
1243
+ clearAgentSession("opencode", session.agentName);
1244
+ console.log(`Stopped "${session.agentName}".`);
1245
+ }
1246
+ async function pollForReady(url, timeoutMs) {
1247
+ const start = Date.now();
1248
+ while (Date.now() - start < timeoutMs) {
1249
+ try {
1250
+ const res = await fetch(`${url}/session/status`);
1251
+ if (res.ok) return true;
1252
+ } catch {
1253
+ }
1254
+ await new Promise((r) => setTimeout(r, 500));
1255
+ }
1256
+ return false;
1257
+ }
1258
+
1259
+ // src/cli/codex/run.ts
1260
+ import { execFileSync as execFileSync2 } from "child_process";
1261
+ import { rmSync as rmSync3 } from "fs";
1262
+ import { homedir as homedir6 } from "os";
1263
+
1264
+ // src/cli/codex/queue.ts
1265
+ import { execFileSync } from "child_process";
1266
+ import { readFileSync as readFileSync3, readdirSync as readdirSync3, statSync as statSync2, existsSync as existsSync4 } from "fs";
1267
+ import { join as join5 } from "path";
1268
+ import { homedir as homedir4 } from "os";
1269
+ function userCodexSessionsDir(codexHome) {
1270
+ return join5(codexHome ?? process.env.CODEX_HOME ?? join5(homedir4(), ".codex"), "sessions");
1271
+ }
1272
+ function parseSessionMeta(firstLine) {
1273
+ try {
1274
+ const record = JSON.parse(firstLine);
1275
+ if (record?.type !== "session_meta") return null;
1276
+ const payload = record.payload ?? {};
1277
+ const sessionId = payload.session_id ?? payload.id;
1278
+ const startedAt = Date.parse(payload.timestamp ?? "");
1279
+ if (typeof sessionId !== "string" || typeof payload.cwd !== "string") return null;
1280
+ if (Number.isNaN(startedAt)) return null;
1281
+ return { sessionId, cwd: payload.cwd, startedAt };
1282
+ } catch {
1283
+ return null;
1284
+ }
1285
+ }
1286
+ function rolloutFiles(root) {
1287
+ const found = [];
1288
+ const walk = (dir) => {
1289
+ let entries;
1290
+ try {
1291
+ entries = readdirSync3(dir);
1292
+ } catch {
1293
+ return;
1294
+ }
1295
+ for (const entry of entries) {
1296
+ const path = join5(dir, entry);
1297
+ let stat;
1298
+ try {
1299
+ stat = statSync2(path);
1300
+ } catch {
1301
+ continue;
1302
+ }
1303
+ if (stat.isDirectory()) walk(path);
1304
+ else if (entry.startsWith("rollout-") && entry.endsWith(".jsonl")) {
1305
+ found.push({ path, mtime: stat.mtimeMs });
1306
+ }
1307
+ }
1308
+ };
1309
+ walk(root);
1310
+ return found.sort((a, b) => b.mtime - a.mtime).map((f) => f.path);
1311
+ }
1312
+ function findCodexSessionId(opts) {
1313
+ const root = opts.sessionsDir ?? userCodexSessionsDir();
1314
+ if (!existsSync4(root)) return null;
1315
+ const floor = opts.startedAfter - (opts.slackMs ?? 5e3);
1316
+ for (const path of rolloutFiles(root)) {
1317
+ let firstLine;
1318
+ try {
1319
+ firstLine = readFileSync3(path, "utf-8").split("\n", 1)[0] ?? "";
1320
+ } catch {
1321
+ continue;
1322
+ }
1323
+ const meta = parseSessionMeta(firstLine);
1324
+ if (!meta) continue;
1325
+ if (meta.cwd !== opts.cwd) continue;
1326
+ if (meta.startedAt < floor) continue;
1327
+ return meta.sessionId;
1328
+ }
1329
+ return null;
1330
+ }
1331
+ var defaultRunner = (threadId, text) => {
1332
+ execFileSync("codex", ["queue", "--thread", threadId, "--message", text], {
1333
+ stdio: "ignore"
1334
+ });
1335
+ };
1336
+ function queueCodexMessage(threadId, text, run = defaultRunner) {
1337
+ try {
1338
+ run(threadId, text);
1339
+ return true;
1340
+ } catch {
1341
+ return false;
1342
+ }
1343
+ }
1344
+
1345
+ // src/cli/codex/tmux-bridge.ts
1346
+ var SPINNER_CHARS2 = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
1347
+ var APPROVAL_PATTERNS = [
1348
+ "Would you like to",
1349
+ "needs your approval",
1350
+ "Press Enter to confirm or Esc to cancel",
1351
+ "Do you want to approve",
1352
+ // v0.15x MCP tool approval: "Allow the apiary MCP server to run tool "x"?"
1353
+ /Allow the .+ MCP server to run tool/,
1354
+ "Allow for this session",
1355
+ // Strings taken from the Codex binary itself rather than guessed.
1356
+ "Allow Codex to run",
1357
+ "Allow Codex to apply proposed code changes?",
1358
+ "enter to submit | esc to cancel"
1359
+ ];
1360
+ var BLOCKED_PATTERNS = [
1361
+ // Not a bare "Welcome to Codex" — the ordinary ready banner opens
1362
+ // "Welcome to Codex! Type a message to get started."
1363
+ /Welcome to Codex, /,
1364
+ "Finish signing in via your browser",
1365
+ "Sign in with ChatGPT",
1366
+ "Sign in with Device Code",
1367
+ "Provide your own API key",
1368
+ "Sign in to continue",
1369
+ // `[\s\S]` not `.`: the screen renders these on separate lines, and `.`
1370
+ // does not cross a newline, so the original pattern never matched.
1371
+ /You are running Codex in [\s\S]+Do you want to allow/,
1372
+ "Do you trust the files in this folder"
1373
+ ];
1374
+ var STREAMING_PATTERNS = [
1375
+ /Working\s*\(\d+[smh]/,
1376
+ // "Working (12s" or "Working (1m 30s"
1377
+ /Working\s*$/,
1378
+ // "Working" at end of line (just started)
1379
+ /esc to interrupt/
1380
+ // hint text during streaming
1381
+ ];
1382
+ var MENTION_POPUP_PATTERNS = [
1383
+ /enter\s+insert\b[\s\S]{0,40}?esc\s+close/i
1384
+ ];
1385
+ function codexMentionPopupIsOpen(lines) {
1386
+ const tail = lines.slice(-20).join("\n");
1387
+ return MENTION_POPUP_PATTERNS.some((p) => p.test(tail));
1388
+ }
1389
+ function defaultUndeliveredWarning2(text) {
1390
+ const preview = text.replace(/\s+/g, " ").slice(0, 80);
1391
+ process.stderr.write(`apiary: message NOT delivered to the agent \u2014 still in its composer: ${preview}
1392
+ `);
1393
+ }
1394
+ var CodexTmuxBridge = class {
1395
+ session;
1396
+ queue = [];
1397
+ pollTimer = null;
1398
+ pollIntervalMs;
1399
+ pasteDelayMs;
1400
+ keystrokeDelayMs;
1401
+ stopped = false;
1402
+ resolveThreadId;
1403
+ threadId = null;
1404
+ onUndelivered;
1405
+ constructor(session, opts) {
1406
+ this.session = session;
1407
+ this.pollIntervalMs = opts?.pollIntervalMs ?? 200;
1408
+ this.pasteDelayMs = opts?.pasteDelayMs ?? 150;
1409
+ this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;
1410
+ this.resolveThreadId = opts?.resolveThreadId;
1411
+ this.onUndelivered = opts?.onUndelivered ?? defaultUndeliveredWarning2;
1412
+ }
1413
+ /**
1414
+ * The Codex session id, resolved once and remembered.
1415
+ *
1416
+ * Resolution is deferred because the session does not exist at construction
1417
+ * time — Codex is still starting — and it is retried on later deliveries
1418
+ * until it succeeds.
1419
+ */
1420
+ thread() {
1421
+ if (this.threadId) return this.threadId;
1422
+ if (!this.resolveThreadId) return null;
1423
+ try {
1424
+ this.threadId = this.resolveThreadId();
1425
+ } catch {
1426
+ this.threadId = null;
1427
+ }
1428
+ return this.threadId;
1429
+ }
1430
+ /**
1431
+ * Delivery callback — drop-in replacement for EventProcessor's deliver.
1432
+ * Pass `bridge.deliver.bind(bridge)` to EventProcessor.run().
1433
+ */
1434
+ async deliver(parts, opts) {
1435
+ const text = contentPartsToString(parts);
1436
+ if (!text.trim()) return;
1437
+ this.inject(text, opts?.dedupe === true);
1438
+ }
1439
+ /**
1440
+ * Detect the current TUI state by reading the screen.
1441
+ */
1442
+ detectState() {
1443
+ if (tmuxPaneIsShell(this.session)) return "absent";
1444
+ const lines = this.captureScreen();
1445
+ return detectCodexStateFromLines(lines);
1446
+ }
1447
+ /**
1448
+ * Codex's live status text ("Working (12s • esc to interrupt)") for the room
1449
+ * strip, or null when idle. Mirrors the Claude bridge's getActivityLabel so
1450
+ * both runtimes can report the same shape.
1451
+ */
1452
+ getActivityLabel() {
1453
+ return extractCodexActivityLabel(this.captureScreen());
1454
+ }
1455
+ /**
1456
+ * Try to inject text, choosing strategy based on TUI state.
1457
+ * Text is flattened to a single line to avoid multi-line paste issues.
1458
+ */
1459
+ inject(text, dedupe) {
1460
+ const flat = text.replace(/\n/g, " ");
1461
+ const state = this.detectState();
1462
+ if (state !== "absent") {
1463
+ const threadId = this.thread();
1464
+ if (threadId && queueCodexMessage(threadId, flat)) return;
1465
+ }
1466
+ switch (state) {
1467
+ case "idle":
1468
+ this.injectIdle(flat);
1469
+ break;
1470
+ case "typing":
1471
+ this.injectWhileTyping(flat);
1472
+ break;
1473
+ default:
1474
+ this.enqueue(flat, dedupe);
1475
+ break;
1476
+ }
1477
+ }
1478
+ /** Capture the screen via tmux capture-pane. */
1479
+ captureScreen() {
1480
+ return tmuxCapturePane(this.session);
1481
+ }
1482
+ /**
1483
+ * Inject into an idle prompt using bracketed paste.
1484
+ *
1485
+ * Bracketed paste wraps text in ESC[200~...ESC[201~ so crossterm
1486
+ * delivers it as a single Paste event, bypassing the burst detector.
1487
+ * The wrapping happens in one write — see tmuxInjectPaste for why that
1488
+ * matters. After the paste, we wait for the Enter suppression window
1489
+ * (120ms) to expire, then send Enter to submit.
1490
+ */
1491
+ injectIdle(text) {
1492
+ tmuxInjectPaste(this.session, text);
1493
+ this.sleep(this.pasteDelayMs);
1494
+ this.dismissMentionPopup();
1495
+ tmuxSendEnter(this.session);
1496
+ this.confirmSubmitted(text);
1497
+ }
1498
+ /**
1499
+ * Check the message left the composer, and try once more if it did not.
1500
+ *
1501
+ * A submitted message moves into the transcript; one that is still in the
1502
+ * composer was not sent. The retry re-runs the popup guard first, because the
1503
+ * popup is what eats an Enter — and if the text is still there after that,
1504
+ * nothing this bridge can do will send it, so it says so rather than leaving
1505
+ * the room to infer silence.
1506
+ */
1507
+ confirmSubmitted(text) {
1508
+ this.sleep(this.pasteDelayMs);
1509
+ if (!composerStillHolds(this.captureScreen(), text)) return;
1510
+ this.dismissMentionPopup();
1511
+ tmuxSendEnter(this.session);
1512
+ this.sleep(this.pasteDelayMs);
1513
+ if (!composerStillHolds(this.captureScreen(), text)) return;
1514
+ this.onUndelivered(text);
1515
+ }
1516
+ /**
1517
+ * Close the @-mention popup before submitting, if the injection opened it.
1518
+ *
1519
+ * Codex classifies incoming bytes as a paste or as typing partly on timing,
1520
+ * so a payload we sent as a bracketed paste can still be read as typing on a
1521
+ * loaded machine. When that happens a `@` opens the mention popup, and Enter
1522
+ * there *inserts the highlighted completion* instead of submitting. On
1523
+ * 2026-09-10 that turned a room message reading "sooo @all" into
1524
+ * "sooo @Openai-Templates" — the top hit was an installed Codex plugin — and
1525
+ * left it unsent in the composer, so the next message appended to the same
1526
+ * stuck line and the agent looked like it was ignoring the operator.
1527
+ *
1528
+ * Escape closes the popup and leaves the composed text untouched, so the
1529
+ * Enter that follows submits what we actually injected.
1530
+ *
1531
+ * Only ever sent when the popup is on screen: Escape is also Codex's
1532
+ * interrupt, so an unconditional one would cancel a turn in progress.
1533
+ */
1534
+ dismissMentionPopup() {
1535
+ if (!codexMentionPopupIsOpen(this.captureScreen())) return;
1536
+ tmuxSendKey(this.session, "Escape");
1537
+ this.sleep(this.keystrokeDelayMs);
1538
+ }
1539
+ /**
1540
+ * Inject while the user is typing:
1541
+ * 1. Ctrl+U — cut to beginning of line (into kill buffer)
1542
+ * 2. Bracketed paste our text + Enter
1543
+ * 3. Ctrl+Y — yank user's text back from kill buffer
1544
+ */
1545
+ injectWhileTyping(text) {
1546
+ tmuxSendKey(this.session, "C-u");
1547
+ this.sleep(this.keystrokeDelayMs);
1548
+ tmuxInjectPaste(this.session, text);
1549
+ this.sleep(this.pasteDelayMs);
1550
+ this.dismissMentionPopup();
1551
+ tmuxSendEnter(this.session);
1552
+ this.confirmSubmitted(text);
1553
+ this.sleep(this.keystrokeDelayMs);
1554
+ tmuxSendKey(this.session, "C-y");
1555
+ }
1556
+ /**
1557
+ * Add to queue and start polling if not already.
1558
+ *
1559
+ * With `dedupe`, identical pending text is dropped: a caller that retries a
1560
+ * delivery it never saw land (the invite loop) would otherwise stack up N
1561
+ * copies of the same prompt, all flushing at once when the TUI frees up.
1562
+ *
1563
+ * Off by default, and deliberately so — room events must never be collapsed.
1564
+ * Two formatted events carry distinct `#ref`s so they should differ anyway,
1565
+ * but "should" is not a guarantee worth silently dropping a message on.
1566
+ */
1567
+ enqueue(text, dedupe) {
1568
+ if (dedupe && this.queue.includes(text)) return;
1569
+ this.queue.push(text);
1570
+ this.startPolling();
1571
+ }
1572
+ /** Start the polling timer to drain queued events. */
1573
+ startPolling() {
1574
+ if (this.pollTimer || this.stopped) return;
1575
+ this.pollTimer = setInterval(() => this.drainQueue(), this.pollIntervalMs);
1576
+ }
1577
+ /** Stop the polling timer. */
1578
+ stopPolling() {
1579
+ if (this.pollTimer) {
1580
+ clearInterval(this.pollTimer);
1581
+ this.pollTimer = null;
1582
+ }
1583
+ }
1584
+ /**
1585
+ * Try to drain one queued event if the state is safe.
1586
+ * Drains one at a time — each injection may trigger streaming,
1587
+ * so the next poll re-checks state before injecting more.
1588
+ */
1589
+ drainQueue() {
1590
+ if (this.queue.length === 0) {
1591
+ this.stopPolling();
1592
+ return;
1593
+ }
1594
+ const state = this.detectState();
1595
+ if (state === "idle" || state === "typing") {
1596
+ const text = this.queue.shift();
1597
+ if (state === "idle") {
1598
+ this.injectIdle(text);
1599
+ } else {
1600
+ this.injectWhileTyping(text);
1601
+ }
1602
+ if (this.queue.length === 0) {
1603
+ this.stopPolling();
1604
+ }
1605
+ }
1606
+ }
1607
+ /** Cleanup. */
1608
+ stop() {
1609
+ this.stopped = true;
1610
+ this.stopPolling();
1611
+ this.queue.length = 0;
1612
+ }
1613
+ /** Synchronous sleep — only used for tiny keystroke delays. */
1614
+ sleep(ms) {
1615
+ if (ms <= 0) return;
1616
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1617
+ }
1618
+ };
1619
+ function matchesAny2(text, patterns) {
1620
+ const haystack = text.toLowerCase();
1621
+ for (const pattern of patterns) {
1622
+ if (typeof pattern === "string") {
1623
+ if (haystack.includes(pattern.toLowerCase())) return true;
1624
+ } else if (pattern.test(text) || new RegExp(pattern.source, pattern.flags.includes("i") ? pattern.flags : pattern.flags + "i").test(text)) {
1625
+ return true;
1626
+ }
1627
+ }
1628
+ return false;
1629
+ }
1630
+ function detectCodexStateFromLines(lines) {
1631
+ if (lines.length === 0) return "unknown";
1632
+ const tail = lines.slice(-20);
1633
+ const tailText = tail.join("\n");
1634
+ if (matchesAny2(tailText, BLOCKED_PATTERNS)) return "blocked";
1635
+ if (matchesAny2(tailText, APPROVAL_PATTERNS)) return "approval";
1636
+ for (const pattern of STREAMING_PATTERNS) {
1637
+ if (pattern.test(tailText)) return "streaming";
1638
+ }
1639
+ const lastFew = tail.slice(-5).join("");
1640
+ for (const ch of SPINNER_CHARS2) {
1641
+ if (lastFew.includes(ch)) return "streaming";
1642
+ }
1643
+ const nonEmpty = tail.filter((l) => l.trim().length > 0);
1644
+ if (nonEmpty.length > 0) {
1645
+ return "idle";
1646
+ }
1647
+ return "unknown";
1648
+ }
1649
+ function extractCodexActivityLabel(lines) {
1650
+ for (const line of lines.slice(-12).reverse()) {
1651
+ const m = /(Working|Thinking|Compacting|Reviewing)\b[^)\n]*/.exec(line);
1652
+ if (!m) continue;
1653
+ const label = m[0].replace(/\s*[•·]?\s*esc to interrupt.*$/i, "").trim();
1654
+ if (label) return label.slice(0, 120);
1655
+ }
1656
+ return null;
1657
+ }
1658
+
1659
+ // src/cli/codex/launch.ts
1660
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
1661
+ import { join as join6 } from "path";
1662
+ import { homedir as homedir5 } from "os";
1663
+ var STARTUP_TIMEOUT_SEC = 15;
1664
+ var TOOL_TIMEOUT_SEC = 60;
1665
+ var PROFILE_PREFIX = "apiary-";
1666
+ function userCodexHome(env = process.env) {
1667
+ const fromEnv = env.CODEX_HOME?.trim();
1668
+ return fromEnv && fromEnv.length > 0 ? fromEnv : join6(homedir5(), ".codex");
1669
+ }
1670
+ function codexToolApprovalMode(env = process.env) {
1671
+ const override = env.APIARY_CODEX_TOOL_APPROVAL?.trim();
1672
+ if (override && override.length > 0) return override;
1673
+ return wantsUnattendedAgents(env) ? "approve" : "ask";
1674
+ }
1675
+ function codexProfileName(agentName) {
1676
+ const safe = agentName.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 64);
1677
+ return `${PROFILE_PREFIX}${/[A-Za-z0-9]/.test(safe) ? safe : "agent"}`;
1678
+ }
1679
+ function tomlString(value) {
1680
+ return JSON.stringify(value);
1681
+ }
1682
+ function renderCodexProfile(mcpUrl, approvalMode = codexToolApprovalMode(), unattended = wantsUnattendedAgents()) {
1683
+ const blocks = [
1684
+ // Top-level keys must precede every table header, or TOML parses them as
1685
+ // members of the last table.
1686
+ ...unattended ? [[
1687
+ "# Written by apiary. Layered over your own config; deleted when the agent stops.",
1688
+ "#",
1689
+ "# No approval prompts: nobody is watching this pane, so a prompt is a",
1690
+ "# permanent stall. Commands still run inside Codex's sandbox \u2014 anything",
1691
+ "# it will not allow returns a failure to the model instead of asking.",
1692
+ 'approval_policy = "never"'
1693
+ ].join("\n")] : [],
1694
+ [
1695
+ ...unattended ? [] : ["# Written by apiary. Layered over your own config; deleted when the agent stops."],
1696
+ "[mcp_servers.apiary]",
1697
+ `url = ${tomlString(mcpUrl)}`,
1698
+ `startup_timeout_sec = ${STARTUP_TIMEOUT_SEC}`,
1699
+ `tool_timeout_sec = ${TOOL_TIMEOUT_SEC}`
1700
+ ].join("\n"),
1701
+ ...RUNTIME_TOOL_NAMES.map((tool) => [
1702
+ `[mcp_servers.apiary.tools.${tool}]`,
1703
+ `approval_mode = ${tomlString(approvalMode)}`
1704
+ ].join("\n"))
1705
+ ];
1706
+ return blocks.join("\n\n") + "\n";
1707
+ }
1708
+ function codexProfilePath(agentName, codexHome = userCodexHome()) {
1709
+ return join6(codexHome, `${codexProfileName(agentName)}.config.toml`);
1710
+ }
1711
+ function prepareCodexLaunch(agentName, mcpUrl, extraArgs = [], codexHome = userCodexHome()) {
1712
+ mkdirSync2(codexHome, { recursive: true });
1713
+ const profilePath = codexProfilePath(agentName, codexHome);
1714
+ writeFileSync3(profilePath, renderCodexProfile(mcpUrl), { mode: 384 });
1715
+ const command = ["codex", "--profile", codexProfileName(agentName), ...extraArgs].join(" ");
1716
+ return { command, profilePath };
1717
+ }
1718
+ function clearCodexProfile(agentName, codexHome = userCodexHome()) {
1719
+ try {
1720
+ rmSync2(codexProfilePath(agentName, codexHome), { force: true });
1721
+ } catch {
1722
+ }
1723
+ }
1724
+ function pruneCodexProfiles(liveAgentNames, codexHome = userCodexHome()) {
1725
+ if (!existsSync5(codexHome)) return 0;
1726
+ const live = new Set(liveAgentNames.map((name) => `${codexProfileName(name)}.config.toml`));
1727
+ let removed = 0;
1728
+ try {
1729
+ for (const entry of readdirSync4(codexHome)) {
1730
+ if (!entry.startsWith(PROFILE_PREFIX) || !entry.endsWith(".config.toml")) continue;
1731
+ if (live.has(entry)) continue;
1732
+ try {
1733
+ rmSync2(join6(codexHome, entry), { force: true });
1734
+ removed++;
1735
+ } catch {
1736
+ }
1737
+ }
1738
+ } catch {
1739
+ }
1740
+ return removed;
1741
+ }
1742
+
1743
+ // src/cli/codex/run.ts
1744
+ function listCodexSessions() {
1745
+ return listAgentSessions("codex");
1746
+ }
1747
+ function codexAvailable() {
1748
+ try {
1749
+ execFileSync2("codex", ["--version"], { stdio: "ignore" });
1750
+ return true;
1751
+ } catch {
1752
+ return false;
1753
+ }
1754
+ }
1755
+ async function runCodex(options) {
1756
+ if (options.headless) {
1757
+ const setup2 = await setupAgentRuntime(options);
1758
+ const deliver = async (parts) => {
1759
+ const text = contentPartsToString(parts);
1760
+ if (text.trim()) process.stdout.write(text + "\n");
1761
+ };
1762
+ const eventLoopPromise2 = setup2.processor.run(deliver, setup2.wrappedSource, setup2.initialParts).catch(() => {
1763
+ });
1764
+ process.stderr.write(`MCP server: ${setup2.mcpServer.url}
1765
+ `);
1766
+ await new Promise((resolve3) => {
1767
+ process.on("SIGINT", resolve3);
1768
+ process.on("SIGTERM", resolve3);
1769
+ });
1770
+ await setup2.cleanup();
1771
+ await eventLoopPromise2;
1772
+ return;
1773
+ }
1774
+ if (!tmuxAvailable()) {
1775
+ console.error("Error: tmux is required but not found. Install it with: brew install tmux");
1776
+ process.exit(1);
1777
+ }
1778
+ if (!codexAvailable()) {
1779
+ console.error("Error: codex is required but not found. Install it with: npm install -g @openai/codex");
1780
+ process.exit(1);
1781
+ }
1782
+ const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
1783
+ const mcpPort = new URL(setup.mcpServer.url).port;
1784
+ const mcpUrl = `http://127.0.0.1:${mcpPort}/mcp`;
1785
+ const tmuxSession = `apiary_${setup.agentName}`;
1786
+ if (tmuxSessionExists(tmuxSession)) {
1787
+ tmuxKillSession(tmuxSession);
1788
+ }
1789
+ const home = homedir6();
1790
+ const cwdFull = process.cwd();
1791
+ const cwdShort = cwdFull.startsWith(home) ? "~" + cwdFull.slice(home.length) : cwdFull;
1792
+ const tabTitle = `${agentEmoji(setup.agentName)} ${setup.agentName} \xB7 ${cwdShort}`;
1793
+ console.log("Launching Codex...");
1794
+ tmuxCreateSession(tmuxSession, tabTitle);
1795
+ pruneCodexProfiles([
1796
+ setup.agentName,
1797
+ ...listCodexSessions().filter(isAgentAlive).map((s) => s.agentName)
1798
+ ]);
1799
+ const { command } = prepareCodexLaunch(setup.agentName, mcpUrl, options.extraArgs ?? []);
1800
+ const launchedAt = Date.now();
1801
+ tmuxSendCommand(tmuxSession, command);
1802
+ const bridge = new CodexTmuxBridge(tmuxSession, {
1803
+ resolveThreadId: () => findCodexSessionId({ cwd: cwdFull, startedAfter: launchedAt })
1804
+ });
1805
+ const eventLoopPromise = setup.processor.run(bridge.deliver.bind(bridge), setup.wrappedSource).catch(() => {
1806
+ });
1807
+ let lastActivityLabel = null;
1808
+ let lastState;
1809
+ let lastReportAt = 0;
1810
+ const activityTimer = setInterval(() => {
1811
+ let label;
1812
+ let state;
1813
+ try {
1814
+ label = bridge.getActivityLabel();
1815
+ state = codexStateToActivity(bridge.detectState());
1816
+ } catch {
1817
+ return;
1818
+ }
1819
+ if (!shouldReportActivity({
1820
+ label,
1821
+ state,
1822
+ lastLabel: lastActivityLabel,
1823
+ lastState,
1824
+ lastReportAt,
1825
+ now: Date.now()
1826
+ })) return;
1827
+ lastActivityLabel = label;
1828
+ lastState = state;
1829
+ lastReportAt = Date.now();
1830
+ setup.processor.broadcastActivity(label, state).catch(() => {
1831
+ });
1832
+ }, 1500);
1833
+ activityTimer.unref();
1834
+ for (let i = 0; i < 10; i++) {
1835
+ await new Promise((r) => setTimeout(r, 500));
1836
+ if (!tmuxSessionExists(tmuxSession)) {
1837
+ console.error("Error: Codex exited during startup. Try running again.");
1838
+ clearCodexProfile(setup.agentName);
1839
+ clearInterval(activityTimer);
1840
+ bridge.stop();
1841
+ await setup.cleanup();
1842
+ resetTerminal();
1843
+ return;
1844
+ }
1845
+ }
1846
+ saveAgentSession({
1847
+ runtime: "codex",
1848
+ agentName: setup.agentName,
1849
+ pid: process.pid,
1850
+ tmuxSession
1851
+ });
1852
+ const inviteAbort = new AbortController();
1853
+ const invitePromise = deliverInvite({
1854
+ agentName: setup.agentName,
1855
+ deliver: (parts) => bridge.deliver(parts, { dedupe: true }),
1856
+ hasJoined: (url) => setup.joinResults.some((jr) => sameApiaryServer(jr.serverUrl, url)),
1857
+ signal: inviteAbort.signal
1858
+ }).then((outcome) => {
1859
+ if (outcome === "abandoned" && !options.background) {
1860
+ console.log(
1861
+ `Note: "${setup.agentName}" did not join the room it was invited to. The invite is still queued \u2014 check the pane, then re-run or use "apiary room resume".`
1862
+ );
1863
+ }
1864
+ return outcome;
1865
+ }).catch(() => "cancelled");
1866
+ if (options.background) {
1867
+ const noop = (() => true);
1868
+ process.stdout.write = noop;
1869
+ process.stderr.write = noop;
1870
+ await new Promise((resolve3) => {
1871
+ process.on("SIGTERM", resolve3);
1872
+ process.on("SIGINT", resolve3);
1873
+ });
1874
+ inviteAbort.abort();
1875
+ await invitePromise;
1876
+ clearInterval(activityTimer);
1877
+ bridge.stop();
1878
+ await setup.cleanup();
1879
+ if (tmuxSessionExists(tmuxSession)) tmuxKillSession(tmuxSession);
1880
+ clearCodexProfile(setup.agentName);
1881
+ clearAgentSession("codex", setup.agentName);
1882
+ return;
1883
+ }
1884
+ console.log("Attaching to Codex session...\n");
1885
+ try {
1886
+ await tmuxAttach(tmuxSession);
1887
+ } catch {
1888
+ }
1889
+ inviteAbort.abort();
1890
+ await invitePromise;
1891
+ clearInterval(activityTimer);
1892
+ bridge.stop();
1893
+ await setup.cleanup();
1894
+ tmuxKillSession(tmuxSession);
1895
+ clearCodexProfile(setup.agentName);
1896
+ clearAgentSession("codex", setup.agentName);
1897
+ resetTerminal();
1898
+ console.log("Disconnected.");
1899
+ }
1900
+ async function stopCodex(name, all) {
1901
+ const sessions = listCodexSessions();
1902
+ if (sessions.length === 0) {
1903
+ if (all) return;
1904
+ console.error("No active Codex sessions.");
1905
+ process.exit(1);
1906
+ }
1907
+ if (all) {
1908
+ for (const s of sessions) stopCodexSession(s);
1909
+ return;
1910
+ }
1911
+ let session;
1912
+ if (name) {
1913
+ session = sessions.find((s) => s.agentName === name);
1914
+ if (!session) {
1915
+ console.error(`No active Codex session named "${name}". Active sessions:`);
1916
+ for (const s of sessions) console.error(` - ${s.agentName}`);
1917
+ process.exit(1);
1918
+ }
1919
+ } else if (sessions.length === 1) {
1920
+ session = sessions[0];
1921
+ } else {
1922
+ console.error("Multiple active Codex sessions. Use --name or --all:");
1923
+ for (const s of sessions) console.error(` - ${s.agentName}`);
1924
+ process.exit(1);
1925
+ }
1926
+ stopCodexSession(session);
1927
+ }
1928
+ function stopCodexSession(session) {
1929
+ if (session.tmuxSession && tmuxSessionExists(session.tmuxSession)) {
1930
+ tmuxKillSession(session.tmuxSession);
1931
+ }
1932
+ if (isAgentAlive(session)) {
1933
+ try {
1934
+ process.kill(session.pid, "SIGTERM");
1935
+ } catch {
1936
+ }
1937
+ }
1938
+ clearCodexProfile(session.agentName);
1939
+ if (session.tmpDir) {
1940
+ try {
1941
+ rmSync3(session.tmpDir, { recursive: true });
1942
+ } catch {
1943
+ }
1944
+ }
1945
+ clearAgentSession("codex", session.agentName);
1946
+ resetTerminal();
1947
+ console.log(`Stopped "${session.agentName}".`);
1948
+ }
1949
+
1950
+ // src/cli/version.ts
1951
+ import { readFileSync as readFileSync4 } from "fs";
1952
+ import { dirname, resolve as resolve2 } from "path";
1953
+ import { fileURLToPath } from "url";
1954
+ function readPackageField(field) {
1955
+ try {
1956
+ const dir = dirname(fileURLToPath(import.meta.url));
1957
+ const pkg = JSON.parse(readFileSync4(resolve2(dir, "../../package.json"), "utf-8"));
1958
+ return typeof pkg[field] === "string" ? pkg[field] : null;
1959
+ } catch {
1960
+ return null;
1961
+ }
1962
+ }
1963
+ function getVersion() {
1964
+ return readPackageField("version") ?? process.env.npm_package_version ?? "unknown";
1965
+ }
1966
+
1967
+ // src/cli/help.ts
1968
+ function printUsage(stream = console.log) {
1969
+ const color = process.stdout.isTTY;
1970
+ const Y = color ? "\x1B[33m" : "";
1971
+ const C = color ? "\x1B[36m" : "";
1972
+ const G = color ? "\x1B[32m" : "";
1973
+ const D = color ? "\x1B[2m" : "";
1974
+ const B = color ? "\x1B[1m" : "";
1975
+ const R = color ? "\x1B[0m" : "";
1976
+ const COL = 40;
1977
+ const ansi = /\x1b\[[0-9;]*m/g;
1978
+ const row = (cmd, desc) => {
1979
+ const visible = cmd.replace(ansi, "").length;
1980
+ return ` ${cmd}${" ".repeat(Math.max(2, COL - visible))}${D}${desc}${R}`;
1981
+ };
1982
+ stream("");
1983
+ stream(` ${Y}${B}apiary${R} ${D}v${getVersion()} \u2014 shared rooms for AI agents${R}`);
1984
+ stream("");
1985
+ stream(` ${G}${B}Workspace${R}`);
1986
+ stream(row(`${C}apiary room create${R}`, "Create a workspace + invite participants"));
1987
+ stream(row(`${C}apiary room ${Y}<name>${R} ${D}[--share]${R}`, "Start a workspace + join"));
1988
+ stream(row(`${C}apiary room resume ${Y}<name>${R}`, "Rejoin your workspace"));
1989
+ stream(row(`${C}apiary room list${R}`, "List your workspaces"));
1990
+ stream("");
1991
+ stream(` ${G}${B}Who can reach the room${R}`);
1992
+ stream(row(`${D}--bind ${Y}tailscale${R}`, "Your devices anywhere, via Tailscale \u2014 not the local network"));
1993
+ stream(row(`${D}--bind ${Y}lan${R}`, "Anyone on the same wifi"));
1994
+ stream(row(`${D}--share${R}`, "A public cloudflared URL, for people off your tailnet"));
1995
+ stream(` ${D}room create asks this if you don't pass it. Default: this machine only.${R}`);
1996
+ stream("");
1997
+ stream(` ${G}${B}Participants${R} ${D}(each person connects their own agents)${R}`);
1998
+ stream(row(`${C}apiary join ${Y}<url>${R} ${D}[${Y}<name>${R}${D}]${R}`, "Join a workspace"));
1999
+ stream(row(`${C}apiary claude ${Y}<name>${R} ${D}[--admin]${R}`, "Connect Claude Code to the room"));
2000
+ stream(row(`${C}apiary codex ${Y}<name>${R} ${D}[--admin]${R}`, "Connect Codex to the room"));
2001
+ stream(row(`${D}--mode ${Y}standby${R}`, "Quiet until @mentioned, pinged or whispered to"));
2002
+ stream(` ${D}In room create, suffix the alias instead: ${R}${C}bee:standby${R}${D}. The first agent${R}`);
2003
+ stream(` ${D}listens to everything, the rest go standby \u2014 one remark, one reply.${R}`);
2004
+ stream("");
2005
+ stream(` ${G}${B}In the room${R}`);
2006
+ stream(row(`${C}/watch ${Y}<agent>${R}`, "Open that agent's terminal in a new window (Ctrl+<n> too)"));
2007
+ stream(row(`${C}/setmode ${Y}<agent> <mode>${R}`, "Change who an agent listens to, live"));
2008
+ stream(row(`${C}/ping ${Y}<agent>${R}`, "Ask for a one-line status"));
2009
+ stream(` ${D}The strip shows each agent as \u2713 idle, \u2026 working, or \u23F8 needs you \u2014${R}`);
2010
+ stream(` ${D}"needs you" means it is stuck on something only you can clear.${R}`);
2011
+ stream("");
2012
+ stream(row(`${C}apiary ps${R}`, "List active rooms + agents"));
2013
+ stream(row(`${C}apiary stop ${D}[${Y}<name>${R}${D} | --all]${R}`, "Stop agents + rooms"));
2014
+ stream(row(`${C}apiary examples${R}`, "Examples + workflows"));
2015
+ stream("");
2016
+ }
2017
+ function printExamples() {
2018
+ const color = process.stdout.isTTY;
2019
+ const Y = color ? "\x1B[33m" : "";
2020
+ const C = color ? "\x1B[36m" : "";
2021
+ const G = color ? "\x1B[32m" : "";
2022
+ const D = color ? "\x1B[2m" : "";
2023
+ const B = color ? "\x1B[1m" : "";
2024
+ const R = color ? "\x1B[0m" : "";
2025
+ const log = console.log.bind(console);
2026
+ log("");
2027
+ log(` ${Y}${B}apiary examples${R}`);
2028
+ log("");
2029
+ log(` ${G}${B}Create a workspace${R}`);
2030
+ log(` ${C}apiary room create${R} ${D}# guided setup \u2014 name it, invite participants${R}`);
2031
+ log(` ${C}apiary room ${Y}sprint-42${R} ${D}# quick start \u2014 host + join immediately${R}`);
2032
+ log(` ${C}apiary room ${Y}sprint-42${R} ${D}--bind tailscale${R} ${D}# reachable from your phone, over Tailscale only${R}`);
2033
+ log(` ${C}apiary room ${Y}sprint-42${R} ${D}--share${R} ${D}# same, with a public tunnel URL${R}`);
2034
+ log(` ${D}Ctrl+C leaves the server running \u2014 rejoin anytime:${R}`);
2035
+ log(` ${C}apiary room resume ${Y}sprint-42${R}`);
2036
+ log("");
2037
+ log(` ${G}${B}Join a workspace${R} ${D}(each person connects their own agents)${R}`);
2038
+ log(` ${C}apiary join ${Y}<url>${R} ${D}# join as a participant${R}`);
2039
+ log(` ${C}apiary join ${Y}<url>${R} ${D}--guest${R} ${D}# join read-only${R}`);
2040
+ log(` ${C}apiary claude ${Y}Cleo${R} ${D}--admin${R} ${D}# connect your Claude Code agent${R}`);
2041
+ log(` ${C}apiary codex ${Y}Rex${R} ${D}# connect your Codex agent${R}`);
2042
+ log(` ${C}apiary codex ${Y}Rex${R} ${D}--mode standby${R} ${D}# quiet until @mentioned, pinged or whispered${R}`);
2043
+ log(` ${D}Tell your agent the room URL \u2014 it joins via apiary__join_room.${R}`);
2044
+ log("");
2045
+ log(` ${G}${B}Authority${R} ${D}admin > product_owner > member > guest${R}`);
2046
+ log(` ${C}/promote ${Y}<name>${R} ${D}# elevate to product owner (can manage members + modes)${R}`);
2047
+ log(` ${C}/demote ${Y}<name>${R} ${D}# drop product owner back to member${R}`);
2048
+ log(` ${C}/mute ${Y}<name>${R} ${D}# demote to guest (read-only)${R}`);
2049
+ log(` ${C}/unmute ${Y}<name>${R} ${D}# restore to member${R}`);
2050
+ log(` ${C}/kick ${Y}<name>${R} ${D}# remove from the room (admin only)${R}`);
2051
+ log(` ${C}/share ${D}--as member${R} ${D}# generate a share link at a specific tier${R}`);
2052
+ log("");
2053
+ log(` ${G}${B}TUI commands${R}`);
2054
+ log(` ${C}/who${R} ${D}list participants with their roles${R}`);
2055
+ log(` ${C}/watch ${Y}<name>${R} ${D}open that agent's terminal in a new window (or Ctrl+<n>)${R}`);
2056
+ log(` ${C}/ping ${Y}<name>${R} ${D}ping for a status check${R}`);
2057
+ log(` ${C}/setmode ${Y}<n> <m>${R} ${D}set engagement mode (admin / product owner)${R}`);
2058
+ log(` ${C}/share${R} ${D}generate share links${R}`);
2059
+ log(` ${C}/tunnel${R} ${D}start a cloudflared tunnel mid-session (admin)${R}`);
2060
+ log(` ${C}/clear${R} ${D}wipe room history (admin)${R}`);
2061
+ log(` ${C}/sound${R} ${D}toggle notification sounds${R}`);
2062
+ log(` ${C}/leave${R} ${D}disconnect${R}`);
2063
+ log("");
2064
+ log(` ${G}${B}Messaging${R} ${D}(agent tool params)${R}`);
2065
+ log(` ${D}Whisper \u2014 visible only to named recipients:${R}`);
2066
+ log(` ${C}apiary__send_message(room, content, null, ${Y}["Alice", "Bob"]${R}${C})${R}`);
2067
+ log(` ${D}Attachments \u2014 local file or image:${R}`);
2068
+ log(` ${C}apiary__send_message(room, content, null, null, ${Y}[{ type: "path", path: "..." }]${R}${C})${R}`);
2069
+ log("");
2070
+ log(` ${G}${B}Sessions${R}`);
2071
+ log(` ${C}apiary ps${R} ${D}# list rooms + agents with join links${R}`);
2072
+ log(` ${C}apiary stop ${Y}Cleo${R} ${D}# stop one agent${R}`);
2073
+ log(` ${C}apiary stop ${D}--all${R} ${D}# stop everything${R}`);
2074
+ log("");
2075
+ }
2076
+
2077
+ // src/cli/index.ts
2078
+ var args = process.argv.slice(2);
2079
+ function getFlag(name, arr = args) {
2080
+ const idx = arr.indexOf(`--${name}`);
2081
+ if (idx === -1) return void 0;
2082
+ const value = arr[idx + 1];
2083
+ if (value === void 0 || value.startsWith("--")) return void 0;
2084
+ return value;
2085
+ }
2086
+ function getAllFlags(name, arr = args) {
2087
+ const results = [];
2088
+ const flag = `--${name}`;
2089
+ for (let i = 0; i < arr.length; i++) {
2090
+ if (arr[i] === flag && arr[i + 1] && !arr[i + 1].startsWith("--")) {
2091
+ results.push(arr[i + 1]);
2092
+ }
2093
+ }
2094
+ return results;
2095
+ }
2096
+ function bindFlag(args2) {
2097
+ const spec = getFlag("bind", args2);
2098
+ if (!spec) return void 0;
2099
+ const res = resolveBindAddress(spec);
2100
+ if (!res.ok) {
2101
+ console.error(`Error: ${res.error}`);
2102
+ process.exit(1);
2103
+ }
2104
+ if (res.note) console.log(` Listening on ${res.address} \u2014 ${res.note}`);
2105
+ return res.address;
2106
+ }
2107
+ async function main() {
2108
+ if (args[0] === "examples") {
2109
+ printExamples();
2110
+ return;
2111
+ }
2112
+ if (args.includes("--help") || args.includes("-h")) {
2113
+ printUsage();
2114
+ return;
2115
+ }
2116
+ if (args.includes("--version") || args.includes("-v") || args[0] === "version") {
2117
+ try {
2118
+ const { createRequire } = await import("module");
2119
+ const require2 = createRequire(import.meta.url);
2120
+ const pkg = require2("../../package.json");
2121
+ console.log(pkg.version);
2122
+ } catch {
2123
+ console.log("unknown");
2124
+ }
2125
+ return;
2126
+ }
2127
+ if (args[0] === "update") {
2128
+ const { runUpdate } = await import("../update-JGE3WCQB.js");
2129
+ const targetVersion = args[1] && !args[1].startsWith("--") ? args[1] : void 0;
2130
+ await runUpdate(targetVersion);
2131
+ return;
2132
+ }
2133
+ printUpdateNotice();
2134
+ checkForUpdate();
2135
+ if (args[0] === "stop") {
2136
+ const rest = args.slice(1);
2137
+ const all = rest.includes("--all");
2138
+ const positional = rest.find((a) => !a.startsWith("--"));
2139
+ const name = getFlag("name", rest) ?? positional;
2140
+ const { roomStop } = await import("../room-Z7KDRVYD.js");
2141
+ if (all) {
2142
+ for (const r of listRoomSessions()) {
2143
+ try {
2144
+ await roomStop(r.roomName, { session: r, quietIfDead: true });
2145
+ } catch {
2146
+ }
2147
+ }
2148
+ await stopClaude(void 0, true);
2149
+ await stopCodex(void 0, true);
2150
+ await stopOpencode(void 0, true);
2151
+ return;
2152
+ }
2153
+ if (name) {
2154
+ const room = listRoomSessions().find((s) => s.roomName === name);
2155
+ if (room) {
2156
+ await roomStop(name, { session: room });
2157
+ return;
2158
+ }
2159
+ if (listCodexSessions().some((s) => s.agentName === name)) {
2160
+ await stopCodex(name, false);
2161
+ return;
2162
+ }
2163
+ if (listOpencodeSessions().some((s) => s.agentName === name)) {
2164
+ await stopOpencode(name, false);
2165
+ return;
2166
+ }
2167
+ }
2168
+ await stopClaude(name, false);
2169
+ return;
2170
+ }
2171
+ if (args[0] === "ps") {
2172
+ const rooms = listRoomSessions();
2173
+ const claudeAgents = listClaudeSessions().map((s) => ({ runtime: "claude", agentName: s.agentName, pid: s.pid }));
2174
+ const codexAgents = listCodexSessions().map((s) => ({ runtime: "codex", agentName: s.agentName, pid: s.pid }));
2175
+ const opencodeAgents = listOpencodeSessions().map((s) => ({ runtime: "opencode", agentName: s.agentName, pid: s.pid }));
2176
+ const agents = [...claudeAgents, ...codexAgents, ...opencodeAgents];
2177
+ if (rooms.length === 0 && agents.length === 0) {
2178
+ console.log("No active sessions.");
2179
+ return;
2180
+ }
2181
+ for (const r of rooms) {
2182
+ let alive = false;
2183
+ try {
2184
+ process.kill(r.pid, 0);
2185
+ alive = true;
2186
+ } catch {
2187
+ }
2188
+ if (!alive) continue;
2189
+ const joinUrl = buildShareUrl(
2190
+ r.publicUrl !== r.serverUrl ? r.publicUrl : r.serverUrl,
2191
+ r.memberToken
2192
+ );
2193
+ console.log(` \x1B[1m${r.roomName}\x1B[0m \x1B[2m(room, pid ${r.pid})\x1B[0m`);
2194
+ console.log(` \x1B[2mJoin:\x1B[0m \x1B[36mapiary join\x1B[0m ${joinUrl}`);
2195
+ }
2196
+ for (const s of agents) {
2197
+ let alive = false;
2198
+ try {
2199
+ process.kill(s.pid, 0);
2200
+ alive = true;
2201
+ } catch {
2202
+ }
2203
+ const status = alive ? "running" : "stale";
2204
+ console.log(` ${s.agentName} (${s.runtime}, pid ${s.pid}, ${status})`);
2205
+ }
2206
+ return;
2207
+ }
2208
+ if (args[0] === "room") {
2209
+ const sub = args[1];
2210
+ const roomArgs = args.slice(2);
2211
+ if (sub === "create") {
2212
+ const portStr = getFlag("port", roomArgs);
2213
+ const port = portStr ? parseInt(portStr, 10) : void 0;
2214
+ const { roomCreate } = await import("../room-Z7KDRVYD.js");
2215
+ await roomCreate({
2216
+ room: getFlag("room", roomArgs) ?? roomArgs.find((a) => !a.startsWith("--")),
2217
+ port,
2218
+ share: roomArgs.includes("--share"),
2219
+ expose: roomArgs.includes("--expose"),
2220
+ bind: bindFlag(roomArgs),
2221
+ foundation: !roomArgs.includes("--no-foundation")
2222
+ });
2223
+ return;
2224
+ }
2225
+ if (sub === "resume") {
2226
+ const name = roomArgs.find((a) => !a.startsWith("--"));
2227
+ if (!name) {
2228
+ console.error("Usage: apiary room resume <name>");
2229
+ process.exit(1);
2230
+ }
2231
+ const { roomResume } = await import("../room-Z7KDRVYD.js");
2232
+ await roomResume(name);
2233
+ return;
2234
+ }
2235
+ if (sub === "list" || sub === "ls") {
2236
+ const { roomList } = await import("../room-Z7KDRVYD.js");
2237
+ roomList();
2238
+ return;
2239
+ }
2240
+ if (sub === "stop") {
2241
+ const name = roomArgs.find((a) => !a.startsWith("--"));
2242
+ if (!name) {
2243
+ console.error("Usage: apiary room stop <name>");
2244
+ process.exit(1);
2245
+ }
2246
+ const { roomStop } = await import("../room-Z7KDRVYD.js");
2247
+ await roomStop(name);
2248
+ return;
2249
+ }
2250
+ {
2251
+ const allRoomArgs = args.slice(1);
2252
+ const positionals = allRoomArgs.filter((a) => !a.startsWith("--"));
2253
+ const roomName = positionals[0];
2254
+ const userName = positionals[1] ?? getFlag("name", allRoomArgs);
2255
+ const portStr = getFlag("port", allRoomArgs);
2256
+ const port = portStr ? parseInt(portStr, 10) : void 0;
2257
+ if (port !== void 0 && (isNaN(port) || port < 0 || port > 65535)) {
2258
+ console.error(`Invalid port: ${portStr}`);
2259
+ process.exit(1);
2260
+ }
2261
+ const { spawnDaemonServer, isDaemonResult } = await import("../room-Z7KDRVYD.js");
2262
+ const daemonRes = await spawnDaemonServer({
2263
+ room: roomName,
2264
+ port,
2265
+ share: allRoomArgs.includes("--share"),
2266
+ expose: allRoomArgs.includes("--expose"),
2267
+ bind: bindFlag(allRoomArgs)
2268
+ });
2269
+ if (!isDaemonResult(daemonRes)) {
2270
+ console.error(`Failed to start server: ${daemonRes.reason}`);
2271
+ if (daemonRes.portInUse) {
2272
+ console.error(`Tip: a previous room daemon is bound. Run 'apiary ps' to inspect, 'apiary stop --all' to free it.`);
2273
+ } else if (daemonRes.stderr?.trim()) {
2274
+ console.error(daemonRes.stderr);
2275
+ }
2276
+ process.exit(1);
2277
+ }
2278
+ const daemon = daemonRes;
2279
+ const adminJoinUrl = buildShareUrl(daemon.serverUrl, daemon.adminToken);
2280
+ const participantShareUrl = buildShareUrl(
2281
+ daemon.publicUrl !== daemon.serverUrl ? daemon.publicUrl : daemon.serverUrl,
2282
+ daemon.memberToken
2283
+ );
2284
+ await join({ server: adminJoinUrl, name: userName, shareUrl: participantShareUrl });
2285
+ console.log(`
2286
+ Server still running (room: ${daemon.roomName})`);
2287
+ console.log(` Rejoin: apiary room resume ${daemon.roomName}
2288
+ `);
2289
+ return;
2290
+ }
2291
+ }
2292
+ if (args[0] === "mcp") {
2293
+ const restArgs = args.slice(1);
2294
+ const joinUrls = getAllFlags("join", restArgs);
2295
+ const positionalName = restArgs.find((a, i) => {
2296
+ if (a.startsWith("--")) return false;
2297
+ if (i > 0 && (/* @__PURE__ */ new Set(["--name", "--join"])).has(restArgs[i - 1])) return false;
2298
+ return true;
2299
+ });
2300
+ const name = getFlag("name", restArgs) ?? positionalName;
2301
+ const pendingUrls = [...joinUrls];
2302
+ if (name) {
2303
+ const { consumeInvite } = await import("../invites-OFXIQLLN.js");
2304
+ const inviteUrl = consumeInvite(name);
2305
+ if (inviteUrl) pendingUrls.push(inviteUrl);
2306
+ }
2307
+ const { runMcpServer } = await import("../run-DOADB4T7.js");
2308
+ await runMcpServer({
2309
+ joinUrls: pendingUrls.length > 0 ? pendingUrls : void 0,
2310
+ name,
2311
+ admin: restArgs.includes("--admin")
2312
+ });
2313
+ return;
2314
+ }
2315
+ const isRunAlias = args[0] === "claude" || args[0] === "codex" || args[0] === "opencode";
2316
+ if (args[0] === "run" && (args[1] === "claude" || args[1] === "opencode" || args[1] === "codex") || isRunAlias) {
2317
+ const runtime = isRunAlias ? args[0] : args[1];
2318
+ const restArgs = args.slice(isRunAlias ? 1 : 2);
2319
+ const ddIndex = restArgs.indexOf("--");
2320
+ const apiaryArgs = ddIndex >= 0 ? restArgs.slice(0, ddIndex) : restArgs;
2321
+ const explicitExtra = ddIndex >= 0 ? restArgs.slice(ddIndex + 1) : [];
2322
+ const KNOWN_FLAGS = /* @__PURE__ */ new Set(["--name", "--admin", "--headless", "--resume", "--join", "--background", "--mode"]);
2323
+ const unknownArgs = [];
2324
+ for (let i = 0; i < apiaryArgs.length; i++) {
2325
+ const arg = apiaryArgs[i];
2326
+ if (arg.startsWith("--") && !KNOWN_FLAGS.has(arg)) {
2327
+ unknownArgs.push(arg);
2328
+ if (i + 1 < apiaryArgs.length && !apiaryArgs[i + 1].startsWith("--")) {
2329
+ unknownArgs.push(apiaryArgs[++i]);
2330
+ }
2331
+ }
2332
+ }
2333
+ const extraArgs = [...unknownArgs, ...explicitExtra];
2334
+ const joinUrls = getAllFlags("join", apiaryArgs);
2335
+ const positionalName = apiaryArgs.find((a, i) => {
2336
+ if (a.startsWith("--")) return false;
2337
+ if (i > 0 && KNOWN_FLAGS.has(apiaryArgs[i - 1])) return false;
2338
+ return true;
2339
+ });
2340
+ const runtimeOptions = {
2341
+ joinUrls: joinUrls.length > 0 ? joinUrls : void 0,
2342
+ name: getFlag("name", apiaryArgs) ?? positionalName,
2343
+ admin: apiaryArgs.includes("--admin"),
2344
+ headless: apiaryArgs.includes("--headless"),
2345
+ resume: apiaryArgs.includes("--resume"),
2346
+ background: apiaryArgs.includes("--background"),
2347
+ mode: parseEngagementMode(getFlag("mode", apiaryArgs)),
2348
+ extraArgs
2349
+ };
2350
+ if (runtime === "claude") {
2351
+ await runClaude(runtimeOptions);
2352
+ } else if (runtime === "opencode") {
2353
+ await runOpencode(runtimeOptions);
2354
+ } else {
2355
+ await runCodex(runtimeOptions);
2356
+ }
2357
+ return;
2358
+ }
2359
+ if (args[0] === "join") {
2360
+ const server = args[1];
2361
+ if (!server || server.startsWith("--")) {
2362
+ console.error("Usage: apiary join <url> [<name>] [--guest] [--headless]");
2363
+ process.exit(1);
2364
+ }
2365
+ const joinName = args[2] && !args[2].startsWith("--") ? args[2] : getFlag("name");
2366
+ await join({
2367
+ server,
2368
+ name: joinName,
2369
+ guest: args.includes("--guest"),
2370
+ headless: args.includes("--headless")
2371
+ });
2372
+ return;
2373
+ }
2374
+ if (args[0] === "serve") {
2375
+ const portStr = getFlag("port");
2376
+ const port = portStr ? parseInt(portStr, 10) : void 0;
2377
+ if (port !== void 0 && (isNaN(port) || port < 0 || port > 65535)) {
2378
+ console.error(`Invalid port: ${portStr}`);
2379
+ process.exit(1);
2380
+ }
2381
+ const shareTtlStr = getFlag("share-token-ttl");
2382
+ const shareTtlMs = shareTtlStr ? parseInt(shareTtlStr, 10) : void 0;
2383
+ await serve({
2384
+ room: getFlag("room"),
2385
+ port,
2386
+ share: args.includes("--share"),
2387
+ headless: args.includes("--headless"),
2388
+ expose: args.includes("--expose"),
2389
+ bind: bindFlag(args),
2390
+ corsOrigins: getAllFlags("cors-origin"),
2391
+ shareTtlMs
2392
+ });
2393
+ return;
2394
+ }
2395
+ if (args.length === 0 || args[0]?.startsWith("--")) {
2396
+ const portStr = getFlag("port");
2397
+ const port = portStr ? parseInt(portStr, 10) : void 0;
2398
+ if (port !== void 0 && (isNaN(port) || port < 0 || port > 65535)) {
2399
+ console.error(`Invalid port: ${portStr}`);
2400
+ process.exit(1);
2401
+ }
2402
+ const result = await serve({
2403
+ room: getFlag("room"),
2404
+ port,
2405
+ share: args.includes("--share"),
2406
+ quiet: true,
2407
+ expose: args.includes("--expose"),
2408
+ bind: bindFlag(args),
2409
+ corsOrigins: getAllFlags("cors-origin")
2410
+ });
2411
+ const adminJoinUrl = buildShareUrl(result.serverUrl, result.adminToken);
2412
+ const participantShareUrl = buildShareUrl(
2413
+ result.publicUrl !== result.serverUrl ? result.publicUrl : result.serverUrl,
2414
+ result.memberToken
2415
+ );
2416
+ await join({
2417
+ server: adminJoinUrl,
2418
+ name: getFlag("name"),
2419
+ shareUrl: participantShareUrl
2420
+ });
2421
+ return;
2422
+ }
2423
+ console.error(`Unknown command: ${args[0]}
2424
+ `);
2425
+ printUsage(console.error);
2426
+ process.exit(1);
2427
+ }
2428
+ main().catch((err) => {
2429
+ console.error(err);
2430
+ process.exit(1);
2431
+ });
2432
+ //# sourceMappingURL=index.js.map