@gorlitzer-labs/apiary 1.13.8 → 1.14.0

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 (36) hide show
  1. package/README.md +15 -3
  2. package/dist/agent/index.d.ts +7 -0
  3. package/dist/agent/index.js +2 -2
  4. package/dist/chunk-36WX2UAF.js +663 -0
  5. package/dist/chunk-36WX2UAF.js.map +1 -0
  6. package/dist/{chunk-3O76FESR.js → chunk-LNSDH2V5.js} +7 -7
  7. package/dist/{chunk-FTN3D6WM.js → chunk-LOONM4G3.js} +8 -177
  8. package/dist/chunk-LOONM4G3.js.map +1 -0
  9. package/dist/{chunk-63Q3KJ4W.js → chunk-N2LPKMPO.js} +186 -3
  10. package/dist/chunk-N2LPKMPO.js.map +1 -0
  11. package/dist/{chunk-YP4K2A7G.js → chunk-TMXQDGMN.js} +30 -30
  12. package/dist/chunk-TMXQDGMN.js.map +1 -0
  13. package/dist/{chunk-LN6ATWE3.js → chunk-WJD5EKO6.js} +158 -158
  14. package/dist/chunk-WJD5EKO6.js.map +1 -0
  15. package/dist/{chunk-J3T2PS3M.js → chunk-XP5F5NCQ.js} +2 -2
  16. package/dist/cli/index.js +38 -380
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/{invites-YZWCUC67.js → invites-ID5SRM3D.js} +5 -5
  19. package/dist/{join-FSKOS5BO.js → join-W53OPEHR.js} +5 -5
  20. package/dist/{room-MAUAV2XD.js → room-XC546NTX.js} +11 -11
  21. package/dist/run-6JRFWMAQ.js +164 -0
  22. package/dist/run-6JRFWMAQ.js.map +1 -0
  23. package/package.json +1 -1
  24. package/dist/chunk-63Q3KJ4W.js.map +0 -1
  25. package/dist/chunk-FTN3D6WM.js.map +0 -1
  26. package/dist/chunk-LN6ATWE3.js.map +0 -1
  27. package/dist/chunk-XKFWEK7W.js +0 -288
  28. package/dist/chunk-XKFWEK7W.js.map +0 -1
  29. package/dist/chunk-YP4K2A7G.js.map +0 -1
  30. package/dist/run-4EJB2KC3.js +0 -90
  31. package/dist/run-4EJB2KC3.js.map +0 -1
  32. /package/dist/{chunk-3O76FESR.js.map → chunk-LNSDH2V5.js.map} +0 -0
  33. /package/dist/{chunk-J3T2PS3M.js.map → chunk-XP5F5NCQ.js.map} +0 -0
  34. /package/dist/{invites-YZWCUC67.js.map → invites-ID5SRM3D.js.map} +0 -0
  35. /package/dist/{join-FSKOS5BO.js.map → join-W53OPEHR.js.map} +0 -0
  36. /package/dist/{room-MAUAV2XD.js.map → room-XC546NTX.js.map} +0 -0
@@ -0,0 +1,663 @@
1
+ import {
2
+ RemoteRoomDataSource,
3
+ buildCatchUpLines
4
+ } from "./chunk-WJD5EKO6.js";
5
+ import {
6
+ extractToken,
7
+ tmuxCapturePane,
8
+ tmuxCapturePaneStyled,
9
+ tmuxInjectText,
10
+ tmuxPaneIsShell,
11
+ tmuxSendEnter,
12
+ tmuxSendKey
13
+ } from "./chunk-N2LPKMPO.js";
14
+ import {
15
+ can,
16
+ contentPartsToString
17
+ } from "./chunk-TMXQDGMN.js";
18
+
19
+ // src/cli/composer.ts
20
+ var PROMPT_MARKER = /^\s*[›❯>]\s?/;
21
+ function composerBlock(lines) {
22
+ const tail = lines.slice(-25);
23
+ let start = -1;
24
+ for (let i = tail.length - 1; i >= 0; i--) {
25
+ if (PROMPT_MARKER.test(tail[i])) {
26
+ start = i;
27
+ break;
28
+ }
29
+ }
30
+ if (start === -1) return "";
31
+ return tail.slice(start).join(" ");
32
+ }
33
+ function submissionProbe(text) {
34
+ return text.replace(/\s+/g, " ").trim().slice(0, 40);
35
+ }
36
+ function composerStillHolds(lines, text) {
37
+ const probe = submissionProbe(text);
38
+ if (probe.length < 8) return false;
39
+ const composer = composerBlock(lines).replace(/\s+/g, " ");
40
+ return composer.includes(probe);
41
+ }
42
+
43
+ // src/cli/claude/tmux-bridge.ts
44
+ var SPINNER_CHARS = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
45
+ var BUSY_PATTERNS = [
46
+ /[A-Za-zÀ-ÿ]+…\s*\(\d+(?:m\s*\d+)?s\b/
47
+ ];
48
+ function composerHoldsOnlyGhost(styledLines) {
49
+ const SGR = /\x1b\[[0-9;]*m/g;
50
+ for (let i = styledLines.length - 1; i >= 0; i--) {
51
+ const plain = styledLines[i].replace(SGR, "").trimStart();
52
+ if (!/^[❯›](\s|$)/.test(plain)) continue;
53
+ const afterPrompt = styledLines[i].slice(styledLines[i].search(/[❯›]/) + 1);
54
+ const typed = afterPrompt.replace(/\x1b\[2m[\s\S]*?(?:\x1b\[(?:0|22)?m|$)/g, "").replace(SGR, "").trim();
55
+ return typed.length === 0;
56
+ }
57
+ return false;
58
+ }
59
+ var DIALOG_PATTERNS = [
60
+ "Enter to select",
61
+ "to navigate",
62
+ "Esc to cancel",
63
+ "Ready to code?",
64
+ "Review your answers",
65
+ "ctrl+g to edit in"
66
+ ];
67
+ var PERMISSION_PATTERNS = [
68
+ "(Y)",
69
+ "Allow ",
70
+ "Deny ",
71
+ "approve",
72
+ "Yes / No",
73
+ // Current Claude Code wording, captured from a live prompt. Without these a
74
+ // permission dialog reads as idle and the bridge pastes a room event into
75
+ // it, answering on the agent's behalf.
76
+ "Do you want to proceed?",
77
+ "don't ask again",
78
+ // The folder-trust modal, which no flag skips.
79
+ "Yes, I trust this folder"
80
+ ];
81
+ var BLOCKING_PATTERNS = [
82
+ "Do you want to proceed?",
83
+ "don't ask again",
84
+ "Yes, I trust this folder",
85
+ "Ready to code?",
86
+ "Enter to select",
87
+ /❯\s*1\.\s*Yes/,
88
+ /\(y\s*\/\s*n\)/i,
89
+ // The arrow glyphs claude renders beside it. Bare "to navigate" is prose.
90
+ /[↑↓⬆⬇]\s*(?:[↑↓⬆⬇]\s*)?to navigate/
91
+ ];
92
+ function matchesAny(text, patterns) {
93
+ const haystack = text.toLowerCase();
94
+ for (const pattern of patterns) {
95
+ if (typeof pattern === "string") {
96
+ if (haystack.includes(pattern.toLowerCase())) return true;
97
+ } else if (pattern.test(text)) {
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
103
+ function defaultUndeliveredWarning(text) {
104
+ const preview = text.replace(/\s+/g, " ").slice(0, 80);
105
+ process.stderr.write(`apiary: message NOT delivered to the agent \u2014 still in its composer: ${preview}
106
+ `);
107
+ }
108
+ var TmuxBridge = class _TmuxBridge {
109
+ session;
110
+ queue = [];
111
+ pollTimer = null;
112
+ pollIntervalMs;
113
+ keystrokeDelayMs;
114
+ onUndelivered;
115
+ attended;
116
+ stopped = false;
117
+ blockedCount = 0;
118
+ static BLOCKED_THRESHOLD = 150;
119
+ // ~30s at 200ms poll — force inject if stuck
120
+ constructor(session, opts) {
121
+ this.session = session;
122
+ this.pollIntervalMs = opts?.pollIntervalMs ?? 200;
123
+ this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;
124
+ this.onUndelivered = opts?.onUndelivered ?? defaultUndeliveredWarning;
125
+ this.attended = opts?.attended ?? false;
126
+ }
127
+ /**
128
+ * Delivery callback — drop-in replacement for the raw tmuxDeliver lambda.
129
+ * Pass `bridge.deliver.bind(bridge)` to EventProcessor.run().
130
+ */
131
+ async deliver(parts, opts) {
132
+ const text = contentPartsToString(parts);
133
+ if (!text.trim()) return;
134
+ this.inject(text, opts?.dedupe === true);
135
+ }
136
+ /**
137
+ * Detect the current TUI state by reading the screen.
138
+ * Exported for testing — the heuristic logic is in detectStateFromLines().
139
+ */
140
+ detectState() {
141
+ if (tmuxPaneIsShell(this.session)) return "absent";
142
+ const lines = this.captureScreen();
143
+ return detectStateFromLines(lines);
144
+ }
145
+ /**
146
+ * State to REPORT to the room, as opposed to the state used to decide
147
+ * whether it is safe to type. Same screen, stricter about claiming a human
148
+ * is needed — see detectReportedStateFromLines.
149
+ */
150
+ detectReportedState() {
151
+ if (tmuxPaneIsShell(this.session)) return "absent";
152
+ return detectReportedStateFromLines(this.captureScreen());
153
+ }
154
+ /**
155
+ * Extract claude's live status label from the pane (the "Sautéed for 12s",
156
+ * "Compacting conversation… 23%" lines under the prompt). Returns null when
157
+ * the agent is idle / no status visible.
158
+ *
159
+ * The room TUI calls this through the agent runtime's poll loop to surface
160
+ * "is the agent actually working or stuck?" without making the user attach
161
+ * to each tmux pane.
162
+ */
163
+ getActivityLabel() {
164
+ return extractActivityLabel(this.captureScreen());
165
+ }
166
+ /**
167
+ * Try to inject text, choosing strategy based on TUI state.
168
+ * If the state is unsafe, queues the text and starts polling.
169
+ *
170
+ * Text is flattened to a single line before injection to avoid triggering
171
+ * Claude Code's paste detection. When multi-line text arrives via
172
+ * `send-keys -l`, Claude Code detects it as a paste and collapses it into
173
+ * "[Pasted text #1 +N lines]" which may not reliably submit with Enter.
174
+ */
175
+ inject(text, dedupe) {
176
+ const flat = text.replace(/\n/g, " ");
177
+ const state = this.deliveryState();
178
+ switch (state) {
179
+ case "idle":
180
+ this.injectIdle(flat);
181
+ break;
182
+ case "typing":
183
+ this.injectWhileTyping(flat);
184
+ break;
185
+ default:
186
+ this.enqueue(flat, dedupe);
187
+ break;
188
+ }
189
+ }
190
+ /**
191
+ * The state delivery acts on. Unattended it is the screen state. Attended,
192
+ * "typing" splits in two: only placeholder text is idle; a real draft is the
193
+ * human's, so the event waits ("unknown" queues it).
194
+ */
195
+ deliveryState() {
196
+ const state = this.detectState();
197
+ if (!this.attended || state !== "typing") return state;
198
+ return composerHoldsOnlyGhost(tmuxCapturePaneStyled(this.session)) ? "idle" : "unknown";
199
+ }
200
+ /** Capture the screen via tmux capture-pane. */
201
+ captureScreen() {
202
+ return tmuxCapturePane(this.session);
203
+ }
204
+ /**
205
+ * Inject into an idle prompt: type text + Enter, then check it went.
206
+ *
207
+ * This used to fire a second Enter unconditionally as a "safety net" against
208
+ * Claude Code's paste detection swallowing the first. That is a guess in both
209
+ * directions: it cannot tell whether the first Enter worked, and it cannot
210
+ * tell whether the second one did either. Reading the composer back answers
211
+ * the question instead, and only sends the second Enter when it is needed.
212
+ */
213
+ injectIdle(text) {
214
+ tmuxInjectText(this.session, text);
215
+ tmuxSendEnter(this.session);
216
+ this.confirmSubmitted(text);
217
+ }
218
+ /**
219
+ * Check the message left the composer, and press Enter once more if it did not.
220
+ *
221
+ * A submitted message moves into the transcript; one still in the composer was
222
+ * never sent. If it is still there after the retry, the room is told rather
223
+ * than left to read the agent's silence as a choice.
224
+ */
225
+ confirmSubmitted(text) {
226
+ this.sleep(80);
227
+ if (!composerStillHolds(this.captureScreen(), text)) return;
228
+ tmuxSendEnter(this.session);
229
+ this.sleep(120);
230
+ if (!composerStillHolds(this.captureScreen(), text)) return;
231
+ this.onUndelivered(text);
232
+ }
233
+ /**
234
+ * Inject while the user is typing:
235
+ * 1. Ctrl+U — cut line to kill ring
236
+ * 2. Inject our text + Enter
237
+ * 3. Ctrl+Y — paste the user's text back
238
+ */
239
+ injectWhileTyping(text) {
240
+ tmuxSendKey(this.session, "C-u");
241
+ this.sleep(this.keystrokeDelayMs);
242
+ tmuxInjectText(this.session, text);
243
+ tmuxSendEnter(this.session);
244
+ this.confirmSubmitted(text);
245
+ this.sleep(this.keystrokeDelayMs);
246
+ tmuxSendKey(this.session, "C-y");
247
+ }
248
+ /** Add to queue and start polling if not already. */
249
+ enqueue(text, dedupe) {
250
+ if (dedupe && this.queue.includes(text)) return;
251
+ this.queue.push(text);
252
+ this.startPolling();
253
+ }
254
+ /** Start the polling timer to drain queued events. */
255
+ startPolling() {
256
+ if (this.pollTimer || this.stopped) return;
257
+ this.pollTimer = setInterval(() => this.drainQueue(), this.pollIntervalMs);
258
+ }
259
+ /** Stop the polling timer. */
260
+ stopPolling() {
261
+ if (this.pollTimer) {
262
+ clearInterval(this.pollTimer);
263
+ this.pollTimer = null;
264
+ }
265
+ }
266
+ /**
267
+ * Try to drain one queued event if the state is safe.
268
+ *
269
+ * Drains one event at a time rather than batching all into one multi-line
270
+ * string — multi-line text triggers Claude Code's paste detection which
271
+ * collapses it into "[Pasted text #1 +N lines]".
272
+ *
273
+ * After injecting one event, the poll continues. The next cycle re-checks
274
+ * state: if Claude is busy (streaming), remaining events wait. If idle,
275
+ * the next event is injected. Events are already flattened in inject().
276
+ */
277
+ drainQueue() {
278
+ if (this.queue.length === 0) {
279
+ this.stopPolling();
280
+ this.blockedCount = 0;
281
+ return;
282
+ }
283
+ const state = this.deliveryState();
284
+ if (state === "idle" || state === "typing") {
285
+ this.blockedCount = 0;
286
+ const text = this.queue.shift();
287
+ if (state === "idle") {
288
+ this.injectIdle(text);
289
+ } else {
290
+ this.injectWhileTyping(text);
291
+ }
292
+ if (this.queue.length === 0) {
293
+ this.stopPolling();
294
+ }
295
+ } else {
296
+ this.blockedCount++;
297
+ if (!this.attended && this.blockedCount >= _TmuxBridge.BLOCKED_THRESHOLD) {
298
+ this.blockedCount = 0;
299
+ const text = this.queue.shift();
300
+ this.injectIdle(text);
301
+ if (this.queue.length === 0) this.stopPolling();
302
+ }
303
+ }
304
+ }
305
+ /** Cleanup. */
306
+ stop() {
307
+ this.stopped = true;
308
+ this.stopPolling();
309
+ this.queue.length = 0;
310
+ }
311
+ /** Synchronous sleep — only used for tiny keystroke delays. */
312
+ sleep(ms) {
313
+ if (ms <= 0) return;
314
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
315
+ }
316
+ };
317
+ var ACTIVITY_LIVE = /([A-Za-zÀ-ÿ]+…\s*\([^)\n]*\))/;
318
+ var ACTIVITY_COOK = /([A-Za-zÀ-ÿ]+ed\s+for\s+\d+s)/;
319
+ var ACTIVITY_PROG = /((?:Compacting|Thinking|Loading|Connecting)[^()]*?(?:\s+\d+%)?)(?=\s*[(\n]|\s*$)/;
320
+ function extractActivityLabel(lines) {
321
+ if (lines.length === 0) return null;
322
+ let best = null;
323
+ for (const line of lines.slice(-25)) {
324
+ const m = line.match(ACTIVITY_PROG) ?? line.match(ACTIVITY_COOK) ?? line.match(ACTIVITY_LIVE);
325
+ if (m) best = m[1].trim();
326
+ }
327
+ return best;
328
+ }
329
+ function detectStateFromLines(lines, opts) {
330
+ if (lines.length === 0) return "unknown";
331
+ const tail = lines.slice(-30);
332
+ const tailText = tail.join("\n");
333
+ if (opts?.ignorePrompts !== true) {
334
+ for (const pattern of DIALOG_PATTERNS) {
335
+ if (tailText.includes(pattern)) return "dialog";
336
+ }
337
+ for (const pattern of PERMISSION_PATTERNS) {
338
+ if (tailText.includes(pattern)) return "permission";
339
+ }
340
+ }
341
+ const lastFew = tail.slice(-5).join("");
342
+ for (const ch of SPINNER_CHARS) {
343
+ if (lastFew.includes(ch)) return "streaming";
344
+ }
345
+ const statusArea = tail.slice(-10).join("\n");
346
+ for (const pattern of BUSY_PATTERNS) {
347
+ if (pattern.test(statusArea)) return "streaming";
348
+ }
349
+ const promptChar = /^[❯›](\s|$)/;
350
+ const footerChar = /^[❯›]{2}\s/;
351
+ const separatorLine = /^[─━─\-]{10,}/;
352
+ for (let i = tail.length - 1; i >= 0; i--) {
353
+ const line = tail[i].trimStart();
354
+ if (footerChar.test(line)) {
355
+ for (let j = i - 1; j >= 0; j--) {
356
+ const above = tail[j].trimStart();
357
+ if (promptChar.test(above)) {
358
+ const content = above.replace(/^[❯›]\s*/, "").trim();
359
+ return content.length === 0 ? "idle" : "typing";
360
+ }
361
+ }
362
+ break;
363
+ }
364
+ }
365
+ for (let i = tail.length - 1; i >= 0; i--) {
366
+ const line = tail[i].trimStart();
367
+ if (promptChar.test(line)) {
368
+ const above = i > 0 ? tail[i - 1].trimStart() : "";
369
+ const below = i < tail.length - 1 ? tail[i + 1].trimStart() : "";
370
+ if (separatorLine.test(above) || separatorLine.test(below)) {
371
+ const content = line.replace(/^[❯›]\s*/, "").trim();
372
+ return content.length === 0 ? "idle" : "typing";
373
+ }
374
+ }
375
+ }
376
+ return "unknown";
377
+ }
378
+ function detectReportedStateFromLines(lines) {
379
+ const loose = detectStateFromLines(lines);
380
+ if (loose !== "dialog" && loose !== "permission") return loose;
381
+ if (matchesAny(lines.slice(-30).join("\n"), BLOCKING_PATTERNS)) return loose;
382
+ return detectStateFromLines(lines, { ignorePrompts: true });
383
+ }
384
+
385
+ // src/agent/wait-for-agent.ts
386
+ var DEFAULT_WAIT_MS = 6e4;
387
+ var MAX_WAIT_MS = 12e4;
388
+ var DEFAULT_POLL_MS = 2e3;
389
+ function resolveBudgetMs(timeoutMs, maxWaitMs = MAX_WAIT_MS) {
390
+ return Math.min(Math.max(timeoutMs ?? DEFAULT_WAIT_MS, 0), maxWaitMs);
391
+ }
392
+ function satisfies(state, until, initial) {
393
+ if (state === "unknown") return false;
394
+ if (until === "change") return state !== initial;
395
+ return state === until;
396
+ }
397
+ async function waitForAgent(opts) {
398
+ const {
399
+ readState,
400
+ until = "idle",
401
+ maxWaitMs = MAX_WAIT_MS,
402
+ pollIntervalMs = DEFAULT_POLL_MS,
403
+ sleep = (ms) => new Promise((r) => {
404
+ setTimeout(r, ms).unref?.();
405
+ }),
406
+ now = () => Date.now()
407
+ } = opts;
408
+ const budget = resolveBudgetMs(opts.timeoutMs, maxWaitMs);
409
+ const started = now();
410
+ const initial = await readState();
411
+ if (initial === "unknown") {
412
+ return { result: "not-reporting", state: "unknown", waitedMs: now() - started };
413
+ }
414
+ if (satisfies(initial, until, initial)) {
415
+ return { result: "satisfied", state: initial, waitedMs: now() - started };
416
+ }
417
+ let latest = initial;
418
+ const steps = Math.max(1, Math.ceil(budget / pollIntervalMs));
419
+ for (let step = 0; step < steps; step++) {
420
+ await sleep(pollIntervalMs);
421
+ latest = await readState();
422
+ if (satisfies(latest, until, initial)) {
423
+ return { result: "satisfied", state: latest, waitedMs: now() - started };
424
+ }
425
+ }
426
+ return { result: "timeout", state: latest, waitedMs: now() - started };
427
+ }
428
+ function describeOutcome(participant, until, outcome) {
429
+ const secs = Math.round(outcome.waitedMs / 1e3);
430
+ switch (outcome.result) {
431
+ case "satisfied":
432
+ return `${participant} is now ${outcome.state} (waited ${secs}s).`;
433
+ case "timeout":
434
+ return `${participant} is still ${outcome.state} after ${secs}s \u2014 not ${until} yet. Call again to keep waiting, or get on with something else.`;
435
+ case "not-reporting":
436
+ return `${participant} does not report its state, so there is nothing to wait for. Only agents started by apiary with a Claude Code or Codex runtime report it. Ask them directly instead.`;
437
+ }
438
+ }
439
+
440
+ // src/cli/runtime-handlers.ts
441
+ function buildRuntimeMcpOptions(ctx) {
442
+ const { processor, sseMux, joinResults, agentName, authority, onRoomJoined } = ctx;
443
+ const adminAction = (build) => async (room, participant) => {
444
+ const conn = processor.resolve(room);
445
+ if (!conn) return { success: false, error: `Unknown room "${room}".` };
446
+ const ds = conn.dataSource;
447
+ const p = conn.dataSource.listParticipants().find((pp) => pp.name === participant);
448
+ if (!p) return { success: false, error: `Unknown participant "${participant}".` };
449
+ const { path, body } = build(p.id);
450
+ try {
451
+ const res = await fetch(`${ds.serverUrl}${path}`, {
452
+ method: "POST",
453
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${ds.sessionToken}` },
454
+ body: JSON.stringify(body)
455
+ });
456
+ if (!res.ok) return { success: false, error: await res.text() };
457
+ return { success: true };
458
+ } catch {
459
+ return { success: false, error: "Server unreachable." };
460
+ }
461
+ };
462
+ return {
463
+ resolver: processor,
464
+ toolOptions: {
465
+ isEventSeen: (id) => processor.isEventSeen(id),
466
+ markEventsSeen: (ids) => processor.markEventsSeen(ids),
467
+ assignRef: (id) => processor.assignRef(id),
468
+ resolveRef: (ref) => processor.resolveRef(ref)
469
+ },
470
+ authority,
471
+ onSetMode: async (room, mode) => {
472
+ const conn = processor.resolve(room);
473
+ if (!conn) return { success: false, error: `Unknown room "${room}".` };
474
+ processor.setModeForRoom(conn.dataSource.roomId, mode, false);
475
+ try {
476
+ const ds = conn.dataSource;
477
+ const res = await fetch(`${ds.serverUrl}/set-mode`, {
478
+ method: "POST",
479
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${ds.sessionToken}` },
480
+ body: JSON.stringify({ mode })
481
+ });
482
+ if (!res.ok) return { success: false, error: `Server rejected: ${await res.text()}` };
483
+ } catch {
484
+ }
485
+ return { success: true };
486
+ },
487
+ /**
488
+ * Wait for another agent to reach a state, by polling the room server's
489
+ * participant list. Two seconds of HTTP is far cheaper than the LLM turn
490
+ * the agent would otherwise spend re-reading the room to check.
491
+ */
492
+ onWaitForAgent: async (room, participant, until, timeoutSec) => {
493
+ const conn = processor.resolve(room);
494
+ if (!conn) return { success: false, error: `Unknown room "${room}".` };
495
+ const ds = conn.dataSource;
496
+ const target = conn.dataSource.listParticipants().find((pp) => pp.name === participant);
497
+ if (!target) return { success: false, error: `Unknown participant "${participant}".` };
498
+ if (target.id === conn.dataSource.selfId) {
499
+ return { success: false, error: "You cannot wait for yourself." };
500
+ }
501
+ const readState = async () => {
502
+ try {
503
+ const res = await fetch(`${ds.serverUrl}/participants`, {
504
+ headers: { Authorization: `Bearer ${ds.sessionToken}` },
505
+ signal: AbortSignal.timeout(1e4)
506
+ });
507
+ if (!res.ok) return "unknown";
508
+ const data = await res.json();
509
+ const found = data.participants?.find((pp) => pp.id === target.id);
510
+ const state = found?.agentState;
511
+ return state === "idle" || state === "working" || state === "blocked" ? state : "unknown";
512
+ } catch {
513
+ return "unknown";
514
+ }
515
+ };
516
+ const outcome = await waitForAgent({
517
+ readState,
518
+ until,
519
+ timeoutMs: timeoutSec === void 0 ? void 0 : timeoutSec * 1e3
520
+ });
521
+ return { success: true, message: describeOutcome(participant, until ?? "idle", outcome) };
522
+ },
523
+ onPing: async (room, participant) => {
524
+ const conn = processor.resolve(room);
525
+ if (!conn) return { success: false, error: `Unknown room "${room}".` };
526
+ const ds = conn.dataSource;
527
+ const p = conn.dataSource.listParticipants().find((pp) => pp.name === participant);
528
+ if (!p) return { success: false, error: `Unknown participant "${participant}".` };
529
+ try {
530
+ const res = await fetch(`${ds.serverUrl}/ping`, {
531
+ method: "POST",
532
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${ds.sessionToken}` },
533
+ body: JSON.stringify({ participantId: p.id })
534
+ });
535
+ if (!res.ok) return { success: false, error: await res.text() };
536
+ return { success: true };
537
+ } catch {
538
+ return { success: false, error: "Server unreachable." };
539
+ }
540
+ },
541
+ onJoinRoom: async (url, alias, nameOverride) => {
542
+ const token = extractToken(url);
543
+ let serverUrl;
544
+ try {
545
+ const parsed = new URL(url);
546
+ parsed.search = "";
547
+ serverUrl = parsed.toString().replace(/\/$/, "");
548
+ } catch {
549
+ serverUrl = url.replace(/\/$/, "");
550
+ }
551
+ try {
552
+ const joinName = nameOverride ?? agentName;
553
+ const joinBody = { type: "agent", name: joinName };
554
+ if (token) joinBody.token = token;
555
+ const res = await fetch(`${serverUrl}/join`, {
556
+ method: "POST",
557
+ headers: { "Content-Type": "application/json" },
558
+ body: JSON.stringify(joinBody),
559
+ signal: AbortSignal.timeout(15e3)
560
+ });
561
+ if (!res.ok) return { success: false, error: `Failed to join: ${await res.text()}` };
562
+ const data = await res.json();
563
+ const sessionToken = String(data.sessionToken ?? "");
564
+ const roomName = alias ?? String(data.roomName ?? "");
565
+ const roomId = String(data.roomId ?? "");
566
+ const roomAuthority = String(data.authority ?? "member");
567
+ const participants = data.participants ?? [];
568
+ const newParticipantId = String(data.participantId ?? "");
569
+ const dataSource = new RemoteRoomDataSource(serverUrl, sessionToken, roomId);
570
+ dataSource.setParticipants(participants);
571
+ dataSource.setSelf(newParticipantId, joinName);
572
+ if (joinResults.length === 0) {
573
+ processor.participantId = newParticipantId;
574
+ }
575
+ processor.setRoomParticipantId(roomId, newParticipantId);
576
+ const mode = processor.getModeForRoom(roomId) ?? "everyone";
577
+ processor.connectRemoteRoom(dataSource, roomName);
578
+ sseMux.addConnection(serverUrl, sessionToken, roomName, roomId);
579
+ const jr = {
580
+ serverUrl,
581
+ sessionToken,
582
+ participantId: newParticipantId,
583
+ roomName,
584
+ roomId,
585
+ authority: roomAuthority,
586
+ participants,
587
+ dataSource
588
+ };
589
+ joinResults.push(jr);
590
+ const conn = processor.resolve(roomName);
591
+ let recentLines = [];
592
+ if (conn) {
593
+ const catchUp = await buildCatchUpLines(conn, {
594
+ isEventSeen: (id) => processor.isEventSeen(id),
595
+ markEventsSeen: (ids) => processor.markEventsSeen(ids),
596
+ assignRef: (id) => processor.assignRef(id)
597
+ });
598
+ recentLines = catchUp.lines;
599
+ }
600
+ await onRoomJoined?.();
601
+ let rules;
602
+ try {
603
+ const rulesRes = await fetch(`${serverUrl}/rules`, {
604
+ headers: { Authorization: `Bearer ${sessionToken}` }
605
+ });
606
+ if (rulesRes.ok) {
607
+ const rulesData = await rulesRes.json();
608
+ if (Array.isArray(rulesData.rules) && rulesData.rules.length > 0) rules = rulesData.rules;
609
+ }
610
+ } catch {
611
+ }
612
+ return {
613
+ success: true,
614
+ roomName,
615
+ agentName: joinName,
616
+ authority: roomAuthority,
617
+ mode,
618
+ participants: participants.filter((p) => p.id !== newParticipantId).map((p) => ({ name: p.name, authority: p.authority ?? "member" })),
619
+ recentLines,
620
+ rules
621
+ };
622
+ } catch (err) {
623
+ const msg = err instanceof Error ? err.message : String(err);
624
+ return { success: false, error: `Unable to connect. Is the server running? (${serverUrl}) \u2014 ${msg}` };
625
+ }
626
+ },
627
+ onLeaveRoom: async (room) => {
628
+ const conn = processor.resolve(room);
629
+ if (!conn) return { success: false, error: `Unknown room "${room}".` };
630
+ const roomId = conn.dataSource.roomId;
631
+ const idx = joinResults.findIndex((jr) => jr.roomId === roomId);
632
+ if (idx >= 0) {
633
+ const jr = joinResults[idx];
634
+ sseMux.removeConnection(roomId);
635
+ processor.disconnectRemoteRoom(roomId);
636
+ try {
637
+ await fetch(`${jr.serverUrl}/disconnect`, {
638
+ method: "POST",
639
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${jr.sessionToken}` },
640
+ body: JSON.stringify({})
641
+ });
642
+ } catch {
643
+ }
644
+ joinResults.splice(idx, 1);
645
+ }
646
+ return { success: true };
647
+ },
648
+ // Admin actions differ only by endpoint + target tier — one helper covers all.
649
+ onAdminSetModeFor: can(authority, "set_mode_for") ? (room, participant, mode) => adminAction((id) => ({ path: "/set-mode", body: { participantId: id, mode } }))(room, participant) : void 0,
650
+ onAdminMute: can(authority, "mute") ? adminAction((id) => ({ path: "/set-authority", body: { participantId: id, authority: "guest" } })) : void 0,
651
+ onAdminUnmute: can(authority, "unmute") ? adminAction((id) => ({ path: "/set-authority", body: { participantId: id, authority: "member" } })) : void 0,
652
+ onAdminKick: can(authority, "kick") ? adminAction((id) => ({ path: "/kick", body: { participantId: id } })) : void 0,
653
+ onAdminPromote: can(authority, "promote") ? adminAction((id) => ({ path: "/set-authority", body: { participantId: id, authority: "product_owner" } })) : void 0,
654
+ onAdminDemote: can(authority, "demote") ? adminAction((id) => ({ path: "/set-authority", body: { participantId: id, authority: "member" } })) : void 0
655
+ };
656
+ }
657
+
658
+ export {
659
+ composerStillHolds,
660
+ TmuxBridge,
661
+ buildRuntimeMcpOptions
662
+ };
663
+ //# sourceMappingURL=chunk-36WX2UAF.js.map