@gorlitzer-labs/apiary 1.14.0 → 1.15.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.
@@ -5,8 +5,8 @@ import {
5
5
  invitePrompt,
6
6
  peekInvite,
7
7
  sameApiaryServer
8
- } from "./chunk-XP5F5NCQ.js";
9
- import "./chunk-LOONM4G3.js";
8
+ } from "./chunk-W4KWQKPP.js";
9
+ import "./chunk-3UQBSELW.js";
10
10
  import "./chunk-PEB6HEFT.js";
11
11
  import "./chunk-N2LPKMPO.js";
12
12
  import "./chunk-D7DXZQWT.js";
@@ -20,4 +20,4 @@ export {
20
20
  peekInvite,
21
21
  sameApiaryServer
22
22
  };
23
- //# sourceMappingURL=invites-ID5SRM3D.js.map
23
+ //# sourceMappingURL=invites-SJ4ZVKVX.js.map
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  HEADLESS_READY,
3
3
  join
4
- } from "./chunk-LNSDH2V5.js";
5
- import "./chunk-LOONM4G3.js";
4
+ } from "./chunk-74XU7HHN.js";
5
+ import "./chunk-3UQBSELW.js";
6
6
  import "./chunk-PEB6HEFT.js";
7
7
  import "./chunk-N2LPKMPO.js";
8
8
  import "./chunk-D7DXZQWT.js";
@@ -12,4 +12,4 @@ export {
12
12
  HEADLESS_READY,
13
13
  join
14
14
  };
15
- //# sourceMappingURL=join-W53OPEHR.js.map
15
+ //# sourceMappingURL=join-5UFFMSLS.js.map
@@ -10,7 +10,7 @@ import {
10
10
  roomRulesPath,
11
11
  saveRoomSession,
12
12
  tailnetAddress
13
- } from "./chunk-LOONM4G3.js";
13
+ } from "./chunk-3UQBSELW.js";
14
14
  import "./chunk-PEB6HEFT.js";
15
15
  import {
16
16
  buildShareUrl,
@@ -847,7 +847,7 @@ ${daemonRes.stderr.split("\n").map((l) => " " + l).join("\n")}`);
847
847
  console.log(` ${D2}Rules:${R2} ${C2}${shortenPath(roomRulesPath(daemon.roomName))}${R2} ${D2}(edit to customize \u2014 hot-reloads)${R2}`);
848
848
  console.log(` Press Ctrl+C to leave \u2014 server keeps running.
849
849
  `);
850
- const { join: join2 } = await import("./join-W53OPEHR.js");
850
+ const { join: join2 } = await import("./join-5UFFMSLS.js");
851
851
  const adminJoinUrl = buildShareUrl(daemon.serverUrl, daemon.adminToken);
852
852
  const expectedAgents = participants.filter((p) => p.role === "agent").map((p) => p.alias);
853
853
  await join2({ server: adminJoinUrl, name: hostName, expectedAgents });
@@ -973,7 +973,7 @@ ${daemonRes.stderr.split("\n").map((l) => " " + l).join("\n")}`);
973
973
  }
974
974
  console.log(` Press Ctrl+C to leave \u2014 server keeps running.
975
975
  `);
976
- const { join: join2 } = await import("./join-W53OPEHR.js");
976
+ const { join: join2 } = await import("./join-5UFFMSLS.js");
977
977
  const adminJoinUrl = buildShareUrl(serverUrl, adminToken);
978
978
  await join2({ server: adminJoinUrl, name: resumeHostName });
979
979
  console.log(`
@@ -1096,4 +1096,4 @@ export {
1096
1096
  spawnDaemonServer,
1097
1097
  writeInvite
1098
1098
  };
1099
- //# sourceMappingURL=room-XC546NTX.js.map
1099
+ //# sourceMappingURL=room-S6ULVAEH.js.map
@@ -0,0 +1,270 @@
1
+ import {
2
+ TmuxBridge,
3
+ buildRuntimeMcpOptions,
4
+ trackRoomEvent
5
+ } from "./chunk-OEAHTZCA.js";
6
+ import {
7
+ EventProcessor,
8
+ SseMultiplexer,
9
+ classifyEvent,
10
+ createStdioRuntimeMcpServer
11
+ } from "./chunk-Z4J6TRJY.js";
12
+ import "./chunk-N2LPKMPO.js";
13
+ import {
14
+ randomName
15
+ } from "./chunk-D7DXZQWT.js";
16
+ import "./chunk-TMXQDGMN.js";
17
+ import "./chunk-BZ4VDWC2.js";
18
+
19
+ // src/cli/mcp/wake.ts
20
+ function resolveWakeConfig(env) {
21
+ const setting = (env.APIARY_MCP_WAKE ?? "").trim().toLowerCase();
22
+ if (setting === "off" || setting === "0" || setting === "false") {
23
+ return { policy: "off", reason: "APIARY_MCP_WAKE=off" };
24
+ }
25
+ if ((env.VITEST || env.NODE_ENV === "test") && env.APIARY_MCP_WAKE_IN_TESTS !== "1") {
26
+ return { policy: "off", reason: "running under a test runner (APIARY_MCP_WAKE_IN_TESTS=1 to opt in)" };
27
+ }
28
+ const pane = env.TMUX_PANE?.trim();
29
+ if (!pane || !/^%\d+$/.test(pane)) {
30
+ return { policy: "off", reason: "not running inside tmux (no $TMUX_PANE)" };
31
+ }
32
+ return { policy: setting === "addressed" ? "addressed" : "mode", pane };
33
+ }
34
+ function describeWake(config) {
35
+ if (config.policy === "off") {
36
+ return `wake: off \u2014 ${config.reason ?? "disabled"}; call catch_up() to see room messages`;
37
+ }
38
+ if (config.policy === "addressed") {
39
+ return `wake: on (APIARY_MCP_WAKE=addressed) \u2014 @mentions, whispers and pings nudge tmux pane ${config.pane}`;
40
+ }
41
+ return `wake: on (follows engagement mode) \u2014 in everyone/people/agents a room message from someone else nudges tmux pane ${config.pane}, at most once per room until catch_up; in standby only @mentions, whispers and pings do. APIARY_MCP_WAKE=addressed for addressed-only, =off to disable`;
42
+ }
43
+ function addressedKind(event, selfId, participants) {
44
+ const senderId = event.type === "Mentioned" ? event.message.sender_id : event.type === "Pinged" ? event.pinger_id : event.participant_id;
45
+ if (senderId === selfId) return null;
46
+ const senderType = participants.find((p) => p.id === senderId)?.type ?? "human";
47
+ if (classifyEvent(event, "standby-everyone", selfId, senderType, senderId) !== "trigger") return null;
48
+ if (event.type === "Pinged") return "ping";
49
+ if (event.type === "Mentioned") return "mention";
50
+ return "whisper";
51
+ }
52
+ function isRoomMessageFor(event, selfId, participants, mode) {
53
+ if (event.type !== "MessageSent") return false;
54
+ if (mode.startsWith("standby-")) return false;
55
+ const senderId = event.message.sender_id;
56
+ if (senderId === selfId || event.participant_id === selfId) return false;
57
+ const recipients = event.message.recipients;
58
+ if (recipients && recipients.length > 0 && !recipients.includes(selfId)) return false;
59
+ const senderType = participants.find((p) => p.id === senderId)?.type ?? "human";
60
+ return classifyEvent(event, mode, selfId, senderType, senderId) === "trigger";
61
+ }
62
+ function safeLabel(name) {
63
+ const cleaned = name.replace(/[^A-Za-z0-9 _.-]/g, "").trim().slice(0, 40);
64
+ return cleaned || "someone";
65
+ }
66
+ var VERB = {
67
+ mention: "mentioned you",
68
+ whisper: "whispered to you",
69
+ ping: "pinged you"
70
+ };
71
+ function wakeLine(room, sender, kind) {
72
+ const r = safeLabel(room);
73
+ return `apiary: ${safeLabel(sender)} ${VERB[kind]} in room "${r}". Read it with apiary__catch_up("${r}") and answer there with apiary__send_message.`;
74
+ }
75
+ function roomMessageLine(room, sender) {
76
+ const r = safeLabel(room);
77
+ return `apiary: new messages in room "${r}" from ${safeLabel(sender)}. Read them with apiary__catch_up("${r}") and answer there with apiary__send_message if a reply is needed.`;
78
+ }
79
+ function lostLine(room) {
80
+ const r = safeLabel(room);
81
+ return `apiary: lost the connection to room "${r}" and could not re-join it. Call apiary__catch_up("${r}") to see why.`;
82
+ }
83
+ var McpWaker = class {
84
+ constructor(nudger, options = {}) {
85
+ this.nudger = nudger;
86
+ this.policy = options.policy ?? "mode";
87
+ this.plainDelayMs = options.plainDelayMs ?? 300;
88
+ }
89
+ seen = /* @__PURE__ */ new Set();
90
+ /** Rooms with a nudge the agent has not answered with catch_up yet. */
91
+ pending = /* @__PURE__ */ new Set();
92
+ /** Plain nudges waiting out `plainDelayMs`, by room. */
93
+ scheduled = /* @__PURE__ */ new Map();
94
+ policy;
95
+ plainDelayMs;
96
+ /**
97
+ * Feed one room event. `mode` is the agent's engagement mode for that room
98
+ * (default "everyone", the MCP client's default). Returns true when the event
99
+ * produced (or scheduled) a nudge.
100
+ */
101
+ onEvent(roomName, event, selfId, participants, mode = "everyone") {
102
+ const kind = addressedKind(event, selfId, participants);
103
+ if (kind) {
104
+ const key = event.type === "Pinged" ? event.id : event.message.id;
105
+ if (this.seen.has(key)) return false;
106
+ this.remember(key);
107
+ this.cancelScheduled(roomName);
108
+ this.pending.add(roomName);
109
+ const senderName = event.type === "Pinged" ? participants.find((p) => p.id === event.pinger_id)?.name ?? "someone" : event.message.sender_name;
110
+ this.nudger.nudge(wakeLine(roomName, senderName, kind));
111
+ return true;
112
+ }
113
+ if (this.policy !== "mode") return false;
114
+ if (!isRoomMessageFor(event, selfId, participants, mode)) return false;
115
+ const message = event.message;
116
+ if (this.seen.has(message.id)) return false;
117
+ if (this.pending.has(roomName) || this.scheduled.has(roomName)) return false;
118
+ const fire = () => {
119
+ this.scheduled.delete(roomName);
120
+ if (this.seen.has(message.id) || this.pending.has(roomName)) return;
121
+ this.remember(message.id);
122
+ this.pending.add(roomName);
123
+ this.nudger.nudge(roomMessageLine(roomName, message.sender_name));
124
+ };
125
+ if (this.plainDelayMs <= 0) {
126
+ fire();
127
+ } else {
128
+ const timer = setTimeout(fire, this.plainDelayMs);
129
+ timer.unref?.();
130
+ this.scheduled.set(roomName, { messageId: message.id, timer });
131
+ }
132
+ return true;
133
+ }
134
+ /** The agent read the room (catch_up): the next message may wake it again. */
135
+ caughtUp(roomName) {
136
+ this.pending.delete(roomName);
137
+ this.cancelScheduled(roomName);
138
+ }
139
+ /** Is a nudge outstanding for this room? (For tests and the startup line.) */
140
+ isPending(roomName) {
141
+ return this.pending.has(roomName) || this.scheduled.has(roomName);
142
+ }
143
+ /** Drop any waiting timers (on shutdown). */
144
+ stop() {
145
+ for (const room of [...this.scheduled.keys()]) this.cancelScheduled(room);
146
+ }
147
+ cancelScheduled(roomName) {
148
+ const s = this.scheduled.get(roomName);
149
+ if (!s) return;
150
+ clearTimeout(s.timer);
151
+ this.scheduled.delete(roomName);
152
+ }
153
+ remember(key) {
154
+ this.seen.add(key);
155
+ if (this.seen.size > 500) this.seen.delete(this.seen.values().next().value);
156
+ }
157
+ };
158
+
159
+ // src/cli/mcp/run.ts
160
+ async function runMcpServer(options) {
161
+ const agentName = options.name ?? randomName();
162
+ const authority = options.authority ?? (options.admin ? "admin" : void 0);
163
+ const joinResults = [];
164
+ const sseMux = new SseMultiplexer();
165
+ const processor = new EventProcessor("", agentName, {
166
+ defaultMode: "everyone",
167
+ pullOnly: true
168
+ });
169
+ const wake = resolveWakeConfig(process.env);
170
+ const bridge = wake.pane && wake.policy !== "off" ? new TmuxBridge(wake.pane, { attended: true }) : null;
171
+ const nudge = (text) => {
172
+ bridge?.deliver([{ type: "text", text }], { dedupe: true }).catch(() => {
173
+ });
174
+ };
175
+ const waker = bridge && wake.policy !== "off" ? new McpWaker({ nudge }, { policy: wake.policy }) : null;
176
+ const wrappedSource = {
177
+ [Symbol.asyncIterator]() {
178
+ const inner = sseMux[Symbol.asyncIterator]();
179
+ return {
180
+ async next() {
181
+ const result = await inner.next();
182
+ if (!result.done) {
183
+ const { roomId, event } = result.value;
184
+ const jr = joinResults.find((j) => j.roomId === roomId);
185
+ if (jr) {
186
+ trackRoomEvent(jr, event);
187
+ try {
188
+ waker?.onEvent(
189
+ jr.roomName,
190
+ event,
191
+ jr.participantId,
192
+ jr.dataSource.listParticipants(),
193
+ processor.getModeForRoom(roomId)
194
+ );
195
+ } catch {
196
+ }
197
+ }
198
+ }
199
+ return result;
200
+ }
201
+ };
202
+ }
203
+ };
204
+ const log = (line) => process.stderr.write(`apiary: ${line}
205
+ `);
206
+ const mcpOptions = buildRuntimeMcpOptions({
207
+ processor,
208
+ sseMux,
209
+ joinResults,
210
+ agentName,
211
+ authority,
212
+ log,
213
+ onRoomLost: (jr) => {
214
+ if (waker) nudge(lostLine(jr.roomName));
215
+ }
216
+ });
217
+ const mcpServer = await createStdioRuntimeMcpServer({
218
+ ...mcpOptions,
219
+ // Reading a room re-arms its wake-up.
220
+ onCatchUp: (room) => {
221
+ const roomId = processor.resolve(room)?.dataSource.roomId;
222
+ const jr = joinResults.find((j) => j.roomId === roomId);
223
+ if (jr) waker?.caughtUp(jr.roomName);
224
+ }
225
+ });
226
+ const deliver = async (_parts) => {
227
+ };
228
+ const eventLoopPromise = processor.run(deliver, wrappedSource).catch(() => {
229
+ });
230
+ process.stderr.write(`apiary mcp server running as "${agentName}"
231
+ `);
232
+ if (options.joinUrls && options.joinUrls.length > 0) {
233
+ process.stderr.write(`pending rooms: ${options.joinUrls.join(", ")}
234
+ `);
235
+ }
236
+ process.stderr.write(`use join_room(url) to connect to a room
237
+ `);
238
+ process.stderr.write(`${describeWake(wake)}
239
+ `);
240
+ async function cleanup() {
241
+ waker?.stop();
242
+ bridge?.stop();
243
+ await processor.stop();
244
+ sseMux.close();
245
+ await mcpServer.stop();
246
+ for (const jr of joinResults) {
247
+ try {
248
+ await fetch(`${jr.serverUrl}/disconnect`, {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify({ token: jr.sessionToken })
252
+ });
253
+ } catch {
254
+ }
255
+ }
256
+ }
257
+ await Promise.race([
258
+ mcpServer.closed,
259
+ new Promise((resolve) => {
260
+ process.on("SIGINT", resolve);
261
+ process.on("SIGTERM", resolve);
262
+ })
263
+ ]);
264
+ await cleanup();
265
+ await eventLoopPromise;
266
+ }
267
+ export {
268
+ runMcpServer
269
+ };
270
+ //# sourceMappingURL=run-5PSUQJF4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/mcp/wake.ts","../src/cli/mcp/run.ts"],"sourcesContent":["/**\n * Wake an `apiary mcp` session when the room has something for it.\n *\n * `apiary mcp` is pull-based: the agent sees room traffic only when it calls\n * catch_up(), so an idle session at its prompt never notices a message until\n * a human types. When the MCP server runs inside tmux — Claude Code passes\n * $TMUX_PANE through to its stdio MCP servers — it can nudge that pane.\n *\n * The nudge is FIXED text built by apiary, never room content. Typed input is\n * read by the agent as its operator's instruction, and a room is shared with\n * other agents and people; letting their words arrive that way would let anyone\n * in the room drive this session. The message itself is read through\n * catch_up(), where it arrives as room data. Only the room name and the sender's\n * name are interpolated, and both are reduced to a safe character set.\n *\n * What wakes it follows the agent's engagement mode for that room — the same\n * promise join_room makes (\"everyone — all messages are pushed to you\"):\n *\n * - active modes (everyone, people, agents): a message from someone else that\n * the mode lets through wakes it, with \"new messages in room X from Y\";\n * - standby modes: only events addressed to it — an @mention, a whisper\n * naming it, or a ping;\n * - in every mode, an addressed event wakes it with its own line.\n *\n * Cost and loops are bounded by coalescing: at most ONE outstanding plain\n * nudge per room until the agent calls catch_up() for that room, which re-arms\n * it. An addressed event may still wake a room that has a plain nudge\n * outstanding, but never twice for one message. The agent's own messages,\n * joins, leaves and activity never wake it.\n *\n * `APIARY_MCP_WAKE=addressed` keeps the older rule (addressed events only,\n * whatever the mode); `APIARY_MCP_WAKE=off` turns waking off.\n */\n\nimport { classifyEvent, type EngagementMode } from \"../../agent/engagement.js\";\nimport type { RoomEvent } from \"../../core/events.js\";\nimport type { Participant } from \"../../core/types.js\";\n\n/**\n * - \"mode\" — follow the room's engagement mode (the default)\n * - \"addressed\" — only @mentions, whispers and pings, whatever the mode\n * - \"off\" — never nudge\n */\nexport type WakePolicy = \"mode\" | \"addressed\" | \"off\";\n\nexport interface WakeConfig {\n policy: WakePolicy;\n /** tmux pane to nudge, from $TMUX_PANE. */\n pane?: string;\n /** Why waking is off, for the startup line on stderr. */\n reason?: string;\n}\n\n/** Decide whether and where to wake, from the environment. */\nexport function resolveWakeConfig(env: NodeJS.ProcessEnv): WakeConfig {\n const setting = (env.APIARY_MCP_WAKE ?? \"\").trim().toLowerCase();\n if (setting === \"off\" || setting === \"0\" || setting === \"false\") {\n return { policy: \"off\", reason: \"APIARY_MCP_WAKE=off\" };\n }\n // A test run (vitest sets VITEST, and its children inherit it) never types\n // into a pane: under tmux it would be the developer's own session. A test\n // that really wants waking opts in with APIARY_MCP_WAKE_IN_TESTS=1.\n if ((env.VITEST || env.NODE_ENV === \"test\") && env.APIARY_MCP_WAKE_IN_TESTS !== \"1\") {\n return { policy: \"off\", reason: \"running under a test runner (APIARY_MCP_WAKE_IN_TESTS=1 to opt in)\" };\n }\n const pane = env.TMUX_PANE?.trim();\n if (!pane || !/^%\\d+$/.test(pane)) {\n return { policy: \"off\", reason: \"not running inside tmux (no $TMUX_PANE)\" };\n }\n return { policy: setting === \"addressed\" ? \"addressed\" : \"mode\", pane };\n}\n\n/** The startup line on stderr, so an operator can see which rule is live. */\nexport function describeWake(config: WakeConfig): string {\n if (config.policy === \"off\") {\n return `wake: off — ${config.reason ?? \"disabled\"}; call catch_up() to see room messages`;\n }\n if (config.policy === \"addressed\") {\n return `wake: on (APIARY_MCP_WAKE=addressed) — @mentions, whispers and pings nudge tmux pane ${config.pane}`;\n }\n return `wake: on (follows engagement mode) — in everyone/people/agents a room message from someone else ` +\n `nudges tmux pane ${config.pane}, at most once per room until catch_up; in standby only @mentions, ` +\n `whispers and pings do. APIARY_MCP_WAKE=addressed for addressed-only, =off to disable`;\n}\n\nexport type WakeKind = \"mention\" | \"whisper\" | \"ping\";\n\n/**\n * Is this event aimed at this agent? Same rule a standby agent wakes on, so\n * the two paths cannot disagree about what \"addressed\" means.\n */\nexport function addressedKind(\n event: RoomEvent,\n selfId: string,\n participants: Participant[],\n): WakeKind | null {\n const senderId =\n event.type === \"Mentioned\" ? event.message.sender_id\n : event.type === \"Pinged\" ? event.pinger_id\n : event.participant_id;\n if (senderId === selfId) return null;\n const senderType = participants.find((p) => p.id === senderId)?.type ?? \"human\";\n if (classifyEvent(event, \"standby-everyone\", selfId, senderType, senderId) !== \"trigger\") return null;\n if (event.type === \"Pinged\") return \"ping\";\n if (event.type === \"Mentioned\") return \"mention\";\n return \"whisper\";\n}\n\n/**\n * Is this an ordinary room message the agent's mode says should reach it?\n *\n * Only a MessageSent from someone else counts — never the agent's own message,\n * never join/leave/activity — and only when the mode's sender filter lets it\n * through (`people` wakes on humans, `agents` on other agents). Standby modes\n * never wake for an ordinary message. A whisper between two others is not\n * something the agent can read, so it does not count either.\n */\nexport function isRoomMessageFor(\n event: RoomEvent,\n selfId: string,\n participants: Participant[],\n mode: EngagementMode,\n): boolean {\n if (event.type !== \"MessageSent\") return false;\n if (mode.startsWith(\"standby-\")) return false;\n const senderId = event.message.sender_id;\n if (senderId === selfId || event.participant_id === selfId) return false;\n const recipients = event.message.recipients;\n if (recipients && recipients.length > 0 && !recipients.includes(selfId)) return false;\n const senderType = participants.find((p) => p.id === senderId)?.type ?? \"human\";\n return classifyEvent(event, mode, selfId, senderType, senderId) === \"trigger\";\n}\n\n/** Keep a room-controlled name from carrying anything but a plain label. */\nexport function safeLabel(name: string): string {\n const cleaned = name.replace(/[^A-Za-z0-9 _.-]/g, \"\").trim().slice(0, 40);\n return cleaned || \"someone\";\n}\n\nconst VERB: Record<WakeKind, string> = {\n mention: \"mentioned you\",\n whisper: \"whispered to you\",\n ping: \"pinged you\",\n};\n\n/** The line typed for an addressed event. Fixed wording; two sanitised names. */\nexport function wakeLine(room: string, sender: string, kind: WakeKind): string {\n const r = safeLabel(room);\n return `apiary: ${safeLabel(sender)} ${VERB[kind]} in room \"${r}\". ` +\n `Read it with apiary__catch_up(\"${r}\") and answer there with apiary__send_message.`;\n}\n\n/** The line typed for ordinary room messages. Fixed wording; two sanitised names. */\nexport function roomMessageLine(room: string, sender: string): string {\n const r = safeLabel(room);\n return `apiary: new messages in room \"${r}\" from ${safeLabel(sender)}. ` +\n `Read them with apiary__catch_up(\"${r}\") and answer there with apiary__send_message if a reply is needed.`;\n}\n\n/** The line typed when a room is lost for good. Fixed wording; one sanitised name. */\nexport function lostLine(room: string): string {\n const r = safeLabel(room);\n return `apiary: lost the connection to room \"${r}\" and could not re-join it. ` +\n `Call apiary__catch_up(\"${r}\") to see why.`;\n}\n\nexport interface Nudger {\n nudge(text: string): void;\n}\n\nexport interface McpWakerOptions {\n /** \"mode\" (default) follows the engagement mode; \"addressed\" only wakes when addressed. */\n policy?: Exclude<WakePolicy, \"off\">;\n /**\n * How long a plain-message nudge waits before it is typed. The server sends\n * an @mention as a MessageSent followed by a Mentioned for the same message;\n * waiting a moment lets the Mentioned upgrade it to the addressed line\n * instead of losing to the generic one. 0 types it at once.\n */\n plainDelayMs?: number;\n}\n\n/**\n * Turns room events into nudges.\n *\n * - One nudge per message: a whisper that also @mentions the agent arrives as\n * two events and must not wake it twice.\n * - One outstanding plain nudge per room: a busy room produces one line, not\n * one per message, until `caughtUp(room)` re-arms it.\n */\nexport class McpWaker {\n private seen = new Set<string>();\n /** Rooms with a nudge the agent has not answered with catch_up yet. */\n private pending = new Set<string>();\n /** Plain nudges waiting out `plainDelayMs`, by room. */\n private scheduled = new Map<string, { messageId: string; timer: ReturnType<typeof setTimeout> }>();\n private readonly policy: Exclude<WakePolicy, \"off\">;\n private readonly plainDelayMs: number;\n\n constructor(private nudger: Nudger, options: McpWakerOptions = {}) {\n this.policy = options.policy ?? \"mode\";\n this.plainDelayMs = options.plainDelayMs ?? 300;\n }\n\n /**\n * Feed one room event. `mode` is the agent's engagement mode for that room\n * (default \"everyone\", the MCP client's default). Returns true when the event\n * produced (or scheduled) a nudge.\n */\n onEvent(\n roomName: string,\n event: RoomEvent,\n selfId: string,\n participants: Participant[],\n mode: EngagementMode = \"everyone\",\n ): boolean {\n const kind = addressedKind(event, selfId, participants);\n if (kind) {\n const key = event.type === \"Pinged\" ? event.id : (event as { message: { id: string } }).message.id;\n if (this.seen.has(key)) return false;\n this.remember(key);\n // An addressed line covers the room: drop a plain nudge still waiting.\n this.cancelScheduled(roomName);\n this.pending.add(roomName);\n const senderName =\n event.type === \"Pinged\"\n ? participants.find((p) => p.id === event.pinger_id)?.name ?? \"someone\"\n : (event as { message: { sender_name: string } }).message.sender_name;\n this.nudger.nudge(wakeLine(roomName, senderName, kind));\n return true;\n }\n\n if (this.policy !== \"mode\") return false;\n if (!isRoomMessageFor(event, selfId, participants, mode)) return false;\n const message = (event as Extract<RoomEvent, { type: \"MessageSent\" }>).message;\n if (this.seen.has(message.id)) return false;\n // Coalesce: the room already has a nudge out (or one about to go out).\n if (this.pending.has(roomName) || this.scheduled.has(roomName)) return false;\n\n const fire = () => {\n this.scheduled.delete(roomName);\n if (this.seen.has(message.id) || this.pending.has(roomName)) return;\n this.remember(message.id);\n this.pending.add(roomName);\n this.nudger.nudge(roomMessageLine(roomName, message.sender_name));\n };\n if (this.plainDelayMs <= 0) {\n fire();\n } else {\n const timer = setTimeout(fire, this.plainDelayMs);\n timer.unref?.();\n this.scheduled.set(roomName, { messageId: message.id, timer });\n }\n return true;\n }\n\n /** The agent read the room (catch_up): the next message may wake it again. */\n caughtUp(roomName: string): void {\n this.pending.delete(roomName);\n this.cancelScheduled(roomName);\n }\n\n /** Is a nudge outstanding for this room? (For tests and the startup line.) */\n isPending(roomName: string): boolean {\n return this.pending.has(roomName) || this.scheduled.has(roomName);\n }\n\n /** Drop any waiting timers (on shutdown). */\n stop(): void {\n for (const room of [...this.scheduled.keys()]) this.cancelScheduled(room);\n }\n\n private cancelScheduled(roomName: string): void {\n const s = this.scheduled.get(roomName);\n if (!s) return;\n clearTimeout(s.timer);\n this.scheduled.delete(roomName);\n }\n\n private remember(key: string): void {\n this.seen.add(key);\n if (this.seen.size > 500) this.seen.delete(this.seen.values().next().value as string);\n }\n}\n","/**\n * apiary mcp — standalone MCP server for any MCP client.\n *\n * Unlike `apiary claude` (which wraps Claude Code in tmux), this exposes\n * apiary tools as a stdio MCP server. Any MCP client (Claude Code, Cursor,\n * Windsurf, etc.) can add it to their MCP config and call join_room()\n * to participate in apiary rooms on the fly.\n *\n * No tmux. No wrapper. Just tools.\n *\n * Event delivery is pull-based: the agent calls catch_up() to get new events.\n * Inside tmux, room messages also nudge its pane as its engagement mode says\n * (see wake.ts), so an idle session notices them without anyone typing.\n * A lost session (server restart, dead URL) is re-joined (see rejoin.ts).\n * The EventProcessor still runs in the background, classifying events via the\n * engagement model and buffering content — catch_up() returns the formatted,\n * classified result.\n *\n * The MCP tool handlers are the same ones the tmux/HTTP runtimes use; they are\n * built by the shared runtime-handlers factory so this path can't drift.\n */\n\nimport { randomName } from \"../../core/names.js\";\nimport { EventProcessor } from \"../../agent/event-processor.js\";\nimport { SseMultiplexer } from \"../../agent/sse-multiplexer.js\";\nimport { createStdioRuntimeMcpServer } from \"../../agent/mcp/runtime.js\";\nimport type { AuthorityLevel } from \"../../core/types.js\";\nimport type { LabeledEvent } from \"../../agent/multiplexer.js\";\nimport type { ContentPart } from \"../../agent/types.js\";\nimport { buildRuntimeMcpOptions, trackRoomEvent, type JoinResult } from \"../runtime-handlers.js\";\nimport { TmuxBridge } from \"../claude/tmux-bridge.js\";\nimport { McpWaker, describeWake, lostLine, resolveWakeConfig } from \"./wake.js\";\n\nexport interface McpServerOptions {\n name?: string;\n /** Legacy CLI surface flag. Equivalent to `authority: \"admin\"`. */\n admin?: boolean;\n /** Pre-declared authority for tool exposure. See AgentRuntimeOptions. */\n authority?: AuthorityLevel;\n joinUrls?: string[];\n}\n\nexport async function runMcpServer(options: McpServerOptions): Promise<void> {\n const agentName = options.name ?? randomName();\n const authority: AuthorityLevel | undefined = options.authority\n ?? (options.admin ? \"admin\" : undefined);\n const joinResults: JoinResult[] = [];\n\n // ── SSE multiplexer (starts empty, grows as agent joins rooms) ────────\n const sseMux = new SseMultiplexer();\n\n // ── EventProcessor (selfId set on first join) ─────────────────────────\n const processor = new EventProcessor(\"\", agentName, {\n defaultMode: \"everyone\",\n pullOnly: true,\n });\n\n // ── Wake the session's own tmux pane when the room has something for it ─\n const wake = resolveWakeConfig(process.env);\n const bridge = wake.pane && wake.policy !== \"off\" ? new TmuxBridge(wake.pane, { attended: true }) : null;\n const nudge = (text: string) => {\n bridge?.deliver([{ type: \"text\", text }], { dedupe: true }).catch(() => {});\n };\n const waker = bridge && wake.policy !== \"off\"\n ? new McpWaker({ nudge }, { policy: wake.policy })\n : null;\n\n // ── Wrap SSE source for participant cache updates and wake-ups ────────\n const wrappedSource: AsyncIterable<LabeledEvent> = {\n [Symbol.asyncIterator]() {\n const inner = sseMux[Symbol.asyncIterator]();\n return {\n async next() {\n const result = await inner.next();\n if (!result.done) {\n const { roomId, event } = result.value;\n const jr = joinResults.find((j) => j.roomId === roomId);\n if (jr) {\n trackRoomEvent(jr, event);\n try {\n waker?.onEvent(\n jr.roomName,\n event,\n jr.participantId,\n jr.dataSource.listParticipants(),\n processor.getModeForRoom(roomId),\n );\n } catch {\n // A failed nudge must never stop the event stream; catch_up still works.\n }\n }\n }\n return result;\n },\n };\n },\n };\n\n // ── Create stdio MCP server (shared handler set) ──────────────────────\n // Re-joins after a server restart or a dead URL are logged on stderr; a\n // room lost for good also nudges the pane (fixed text) so an idle agent\n // finds out without waiting for its next call.\n const log = (line: string) => process.stderr.write(`apiary: ${line}\\n`);\n const mcpOptions = buildRuntimeMcpOptions({\n processor,\n sseMux,\n joinResults,\n agentName,\n authority,\n log,\n onRoomLost: (jr) => {\n if (waker) nudge(lostLine(jr.roomName));\n },\n });\n const mcpServer = await createStdioRuntimeMcpServer({\n ...mcpOptions,\n // Reading a room re-arms its wake-up.\n onCatchUp: (room) => {\n const roomId = processor.resolve(room)?.dataSource.roomId;\n const jr = joinResults.find((j) => j.roomId === roomId);\n if (jr) waker?.caughtUp(jr.roomName);\n },\n });\n\n // ── Start event loop in background ────────────────────────────────────\n // Delivery is a no-op: events are buffered in the EventProcessor and\n // returned via catch_up(). We still run the loop so engagement\n // classification, buffering, and seen-event tracking happen in real-time.\n const deliver = async (_parts: ContentPart[]) => {\n // No-op — standalone MCP is pull-based.\n // Events are classified and buffered by EventProcessor.\n // Agent pulls them via catch_up().\n };\n\n const eventLoopPromise = processor\n .run(deliver, wrappedSource)\n .catch(() => {});\n\n process.stderr.write(`apiary mcp server running as \"${agentName}\"\\n`);\n if (options.joinUrls && options.joinUrls.length > 0) {\n process.stderr.write(`pending rooms: ${options.joinUrls.join(\", \")}\\n`);\n }\n process.stderr.write(`use join_room(url) to connect to a room\\n`);\n process.stderr.write(`${describeWake(wake)}\\n`);\n\n // ── Cleanup ───────────────────────────────────────────────────────────\n async function cleanup(): Promise<void> {\n waker?.stop();\n bridge?.stop();\n await processor.stop();\n sseMux.close();\n await mcpServer.stop();\n\n for (const jr of joinResults) {\n try {\n await fetch(`${jr.serverUrl}/disconnect`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ token: jr.sessionToken }),\n });\n } catch {\n // Server may be down\n }\n }\n }\n\n // Wait for stdio transport to close (MCP client disconnected)\n // or process signal\n await Promise.race([\n mcpServer.closed,\n new Promise<void>((resolve) => {\n process.on(\"SIGINT\", resolve);\n process.on(\"SIGTERM\", resolve);\n }),\n ]);\n\n await cleanup();\n await eventLoopPromise;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsDO,SAAS,kBAAkB,KAAoC;AACpE,QAAM,WAAW,IAAI,mBAAmB,IAAI,KAAK,EAAE,YAAY;AAC/D,MAAI,YAAY,SAAS,YAAY,OAAO,YAAY,SAAS;AAC/D,WAAO,EAAE,QAAQ,OAAO,QAAQ,sBAAsB;AAAA,EACxD;AAIA,OAAK,IAAI,UAAU,IAAI,aAAa,WAAW,IAAI,6BAA6B,KAAK;AACnF,WAAO,EAAE,QAAQ,OAAO,QAAQ,qEAAqE;AAAA,EACvG;AACA,QAAM,OAAO,IAAI,WAAW,KAAK;AACjC,MAAI,CAAC,QAAQ,CAAC,SAAS,KAAK,IAAI,GAAG;AACjC,WAAO,EAAE,QAAQ,OAAO,QAAQ,0CAA0C;AAAA,EAC5E;AACA,SAAO,EAAE,QAAQ,YAAY,cAAc,cAAc,QAAQ,KAAK;AACxE;AAGO,SAAS,aAAa,QAA4B;AACvD,MAAI,OAAO,WAAW,OAAO;AAC3B,WAAO,oBAAe,OAAO,UAAU,UAAU;AAAA,EACnD;AACA,MAAI,OAAO,WAAW,aAAa;AACjC,WAAO,6FAAwF,OAAO,IAAI;AAAA,EAC5G;AACA,SAAO,yHACe,OAAO,IAAI;AAEnC;AAQO,SAAS,cACd,OACA,QACA,cACiB;AACjB,QAAM,WACJ,MAAM,SAAS,cAAc,MAAM,QAAQ,YACzC,MAAM,SAAS,WAAW,MAAM,YAChC,MAAM;AACV,MAAI,aAAa,OAAQ,QAAO;AAChC,QAAM,aAAa,aAAa,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,GAAG,QAAQ;AACxE,MAAI,cAAc,OAAO,oBAAoB,QAAQ,YAAY,QAAQ,MAAM,UAAW,QAAO;AACjG,MAAI,MAAM,SAAS,SAAU,QAAO;AACpC,MAAI,MAAM,SAAS,YAAa,QAAO;AACvC,SAAO;AACT;AAWO,SAAS,iBACd,OACA,QACA,cACA,MACS;AACT,MAAI,MAAM,SAAS,cAAe,QAAO;AACzC,MAAI,KAAK,WAAW,UAAU,EAAG,QAAO;AACxC,QAAM,WAAW,MAAM,QAAQ;AAC/B,MAAI,aAAa,UAAU,MAAM,mBAAmB,OAAQ,QAAO;AACnE,QAAM,aAAa,MAAM,QAAQ;AACjC,MAAI,cAAc,WAAW,SAAS,KAAK,CAAC,WAAW,SAAS,MAAM,EAAG,QAAO;AAChF,QAAM,aAAa,aAAa,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,GAAG,QAAQ;AACxE,SAAO,cAAc,OAAO,MAAM,QAAQ,YAAY,QAAQ,MAAM;AACtE;AAGO,SAAS,UAAU,MAAsB;AAC9C,QAAM,UAAU,KAAK,QAAQ,qBAAqB,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,EAAE;AACxE,SAAO,WAAW;AACpB;AAEA,IAAM,OAAiC;AAAA,EACrC,SAAS;AAAA,EACT,SAAS;AAAA,EACT,MAAM;AACR;AAGO,SAAS,SAAS,MAAc,QAAgB,MAAwB;AAC7E,QAAM,IAAI,UAAU,IAAI;AACxB,SAAO,WAAW,UAAU,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,aAAa,CAAC,qCAC3B,CAAC;AACvC;AAGO,SAAS,gBAAgB,MAAc,QAAwB;AACpE,QAAM,IAAI,UAAU,IAAI;AACxB,SAAO,iCAAiC,CAAC,UAAU,UAAU,MAAM,CAAC,sCAC9B,CAAC;AACzC;AAGO,SAAS,SAAS,MAAsB;AAC7C,QAAM,IAAI,UAAU,IAAI;AACxB,SAAO,wCAAwC,CAAC,sDACpB,CAAC;AAC/B;AA0BO,IAAM,WAAN,MAAe;AAAA,EASpB,YAAoB,QAAgB,UAA2B,CAAC,GAAG;AAA/C;AAClB,SAAK,SAAS,QAAQ,UAAU;AAChC,SAAK,eAAe,QAAQ,gBAAgB;AAAA,EAC9C;AAAA,EAXQ,OAAO,oBAAI,IAAY;AAAA;AAAA,EAEvB,UAAU,oBAAI,IAAY;AAAA;AAAA,EAE1B,YAAY,oBAAI,IAAyE;AAAA,EAChF;AAAA,EACA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYjB,QACE,UACA,OACA,QACA,cACA,OAAuB,YACd;AACT,UAAM,OAAO,cAAc,OAAO,QAAQ,YAAY;AACtD,QAAI,MAAM;AACR,YAAM,MAAM,MAAM,SAAS,WAAW,MAAM,KAAM,MAAsC,QAAQ;AAChG,UAAI,KAAK,KAAK,IAAI,GAAG,EAAG,QAAO;AAC/B,WAAK,SAAS,GAAG;AAEjB,WAAK,gBAAgB,QAAQ;AAC7B,WAAK,QAAQ,IAAI,QAAQ;AACzB,YAAM,aACJ,MAAM,SAAS,WACX,aAAa,KAAK,CAAC,MAAM,EAAE,OAAO,MAAM,SAAS,GAAG,QAAQ,YAC3D,MAA+C,QAAQ;AAC9D,WAAK,OAAO,MAAM,SAAS,UAAU,YAAY,IAAI,CAAC;AACtD,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,WAAW,OAAQ,QAAO;AACnC,QAAI,CAAC,iBAAiB,OAAO,QAAQ,cAAc,IAAI,EAAG,QAAO;AACjE,UAAM,UAAW,MAAsD;AACvE,QAAI,KAAK,KAAK,IAAI,QAAQ,EAAE,EAAG,QAAO;AAEtC,QAAI,KAAK,QAAQ,IAAI,QAAQ,KAAK,KAAK,UAAU,IAAI,QAAQ,EAAG,QAAO;AAEvE,UAAM,OAAO,MAAM;AACjB,WAAK,UAAU,OAAO,QAAQ;AAC9B,UAAI,KAAK,KAAK,IAAI,QAAQ,EAAE,KAAK,KAAK,QAAQ,IAAI,QAAQ,EAAG;AAC7D,WAAK,SAAS,QAAQ,EAAE;AACxB,WAAK,QAAQ,IAAI,QAAQ;AACzB,WAAK,OAAO,MAAM,gBAAgB,UAAU,QAAQ,WAAW,CAAC;AAAA,IAClE;AACA,QAAI,KAAK,gBAAgB,GAAG;AAC1B,WAAK;AAAA,IACP,OAAO;AACL,YAAM,QAAQ,WAAW,MAAM,KAAK,YAAY;AAChD,YAAM,QAAQ;AACd,WAAK,UAAU,IAAI,UAAU,EAAE,WAAW,QAAQ,IAAI,MAAM,CAAC;AAAA,IAC/D;AACA,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,SAAS,UAAwB;AAC/B,SAAK,QAAQ,OAAO,QAAQ;AAC5B,SAAK,gBAAgB,QAAQ;AAAA,EAC/B;AAAA;AAAA,EAGA,UAAU,UAA2B;AACnC,WAAO,KAAK,QAAQ,IAAI,QAAQ,KAAK,KAAK,UAAU,IAAI,QAAQ;AAAA,EAClE;AAAA;AAAA,EAGA,OAAa;AACX,eAAW,QAAQ,CAAC,GAAG,KAAK,UAAU,KAAK,CAAC,EAAG,MAAK,gBAAgB,IAAI;AAAA,EAC1E;AAAA,EAEQ,gBAAgB,UAAwB;AAC9C,UAAM,IAAI,KAAK,UAAU,IAAI,QAAQ;AACrC,QAAI,CAAC,EAAG;AACR,iBAAa,EAAE,KAAK;AACpB,SAAK,UAAU,OAAO,QAAQ;AAAA,EAChC;AAAA,EAEQ,SAAS,KAAmB;AAClC,SAAK,KAAK,IAAI,GAAG;AACjB,QAAI,KAAK,KAAK,OAAO,IAAK,MAAK,KAAK,OAAO,KAAK,KAAK,OAAO,EAAE,KAAK,EAAE,KAAe;AAAA,EACtF;AACF;;;ACjPA,eAAsB,aAAa,SAA0C;AAC3E,QAAM,YAAY,QAAQ,QAAQ,WAAW;AAC7C,QAAM,YAAwC,QAAQ,cAChD,QAAQ,QAAQ,UAAU;AAChC,QAAM,cAA4B,CAAC;AAGnC,QAAM,SAAS,IAAI,eAAe;AAGlC,QAAM,YAAY,IAAI,eAAe,IAAI,WAAW;AAAA,IAClD,aAAa;AAAA,IACb,UAAU;AAAA,EACZ,CAAC;AAGD,QAAM,OAAO,kBAAkB,QAAQ,GAAG;AAC1C,QAAM,SAAS,KAAK,QAAQ,KAAK,WAAW,QAAQ,IAAI,WAAW,KAAK,MAAM,EAAE,UAAU,KAAK,CAAC,IAAI;AACpG,QAAM,QAAQ,CAAC,SAAiB;AAC9B,YAAQ,QAAQ,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,GAAG,EAAE,QAAQ,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EAC5E;AACA,QAAM,QAAQ,UAAU,KAAK,WAAW,QACpC,IAAI,SAAS,EAAE,MAAM,GAAG,EAAE,QAAQ,KAAK,OAAO,CAAC,IAC/C;AAGJ,QAAM,gBAA6C;AAAA,IACjD,CAAC,OAAO,aAAa,IAAI;AACvB,YAAM,QAAQ,OAAO,OAAO,aAAa,EAAE;AAC3C,aAAO;AAAA,QACL,MAAM,OAAO;AACX,gBAAM,SAAS,MAAM,MAAM,KAAK;AAChC,cAAI,CAAC,OAAO,MAAM;AAChB,kBAAM,EAAE,QAAQ,MAAM,IAAI,OAAO;AACjC,kBAAM,KAAK,YAAY,KAAK,CAAC,MAAM,EAAE,WAAW,MAAM;AACtD,gBAAI,IAAI;AACN,6BAAe,IAAI,KAAK;AACxB,kBAAI;AACF,uBAAO;AAAA,kBACL,GAAG;AAAA,kBACH;AAAA,kBACA,GAAG;AAAA,kBACH,GAAG,WAAW,iBAAiB;AAAA,kBAC/B,UAAU,eAAe,MAAM;AAAA,gBACjC;AAAA,cACF,QAAQ;AAAA,cAER;AAAA,YACF;AAAA,UACF;AACA,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAMA,QAAM,MAAM,CAAC,SAAiB,QAAQ,OAAO,MAAM,WAAW,IAAI;AAAA,CAAI;AACtE,QAAM,aAAa,uBAAuB;AAAA,IACxC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY,CAAC,OAAO;AAClB,UAAI,MAAO,OAAM,SAAS,GAAG,QAAQ,CAAC;AAAA,IACxC;AAAA,EACF,CAAC;AACD,QAAM,YAAY,MAAM,4BAA4B;AAAA,IAClD,GAAG;AAAA;AAAA,IAEH,WAAW,CAAC,SAAS;AACnB,YAAM,SAAS,UAAU,QAAQ,IAAI,GAAG,WAAW;AACnD,YAAM,KAAK,YAAY,KAAK,CAAC,MAAM,EAAE,WAAW,MAAM;AACtD,UAAI,GAAI,QAAO,SAAS,GAAG,QAAQ;AAAA,IACrC;AAAA,EACF,CAAC;AAMD,QAAM,UAAU,OAAO,WAA0B;AAAA,EAIjD;AAEA,QAAM,mBAAmB,UACtB,IAAI,SAAS,aAAa,EAC1B,MAAM,MAAM;AAAA,EAAC,CAAC;AAEjB,UAAQ,OAAO,MAAM,iCAAiC,SAAS;AAAA,CAAK;AACpE,MAAI,QAAQ,YAAY,QAAQ,SAAS,SAAS,GAAG;AACnD,YAAQ,OAAO,MAAM,kBAAkB,QAAQ,SAAS,KAAK,IAAI,CAAC;AAAA,CAAI;AAAA,EACxE;AACA,UAAQ,OAAO,MAAM;AAAA,CAA2C;AAChE,UAAQ,OAAO,MAAM,GAAG,aAAa,IAAI,CAAC;AAAA,CAAI;AAG9C,iBAAe,UAAyB;AACtC,WAAO,KAAK;AACZ,YAAQ,KAAK;AACb,UAAM,UAAU,KAAK;AACrB,WAAO,MAAM;AACb,UAAM,UAAU,KAAK;AAErB,eAAW,MAAM,aAAa;AAC5B,UAAI;AACF,cAAM,MAAM,GAAG,GAAG,SAAS,eAAe;AAAA,UACxC,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,OAAO,GAAG,aAAa,CAAC;AAAA,QACjD,CAAC;AAAA,MACH,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAIA,QAAM,QAAQ,KAAK;AAAA,IACjB,UAAU;AAAA,IACV,IAAI,QAAc,CAAC,YAAY;AAC7B,cAAQ,GAAG,UAAU,OAAO;AAC5B,cAAQ,GAAG,WAAW,OAAO;AAAA,IAC/B,CAAC;AAAA,EACH,CAAC;AAED,QAAM,QAAQ;AACd,QAAM;AACR;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorlitzer-labs/apiary",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Multiplayer rooms for AI agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -59,6 +59,7 @@
59
59
  "ink": "^6.8.0",
60
60
  "react": "^19.2.4",
61
61
  "uuid": "^11.0.0",
62
+ "vite": "8.3.5",
62
63
  "zod": "^4.0.0"
63
64
  },
64
65
  "devDependencies": {
@@ -68,6 +69,6 @@
68
69
  "tsup": "^8.0.0",
69
70
  "tsx": "^4.0.0",
70
71
  "typescript": "^5.7.0",
71
- "vitest": "^3.0.0"
72
+ "vitest": "^5.0.5"
72
73
  }
73
74
  }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/composer.ts","../src/cli/claude/tmux-bridge.ts","../src/agent/wait-for-agent.ts","../src/cli/runtime-handlers.ts"],"sourcesContent":["/**\n * Did the text we injected actually get submitted?\n *\n * Both bridges type into a TUI and then assume it worked. On 2026-09-10 that\n * assumption cost seven minutes of apparent silence: a room message reached a\n * Codex agent, the `@`-mention popup swallowed the Enter, and the message sat in\n * the composer while apiary reported success and moved on. Nobody learned it had\n * failed until a human read the pane.\n *\n * The check is deliberately narrow. After a submit, the text appears in the\n * *transcript* — that is success, not failure — so the presence of our text on\n * screen proves nothing. What distinguishes the two is where it sits: an\n * unsubmitted message is still in the composer, which both TUIs render as the\n * last prompt-marked block at the bottom of the screen.\n */\n\n/** Prompt markers: Codex renders the composer with ›, Claude with ❯ (> on older builds). */\nconst PROMPT_MARKER = /^\\s*[›❯>]\\s?/;\n\n/**\n * The composer's contents: everything from the last prompt marker down.\n *\n * Long messages wrap, so the marker line alone is not the whole composer — the\n * rest of the block below it belongs to the same input.\n */\nexport function composerBlock(lines: string[]): string {\n const tail = lines.slice(-25);\n let start = -1;\n for (let i = tail.length - 1; i >= 0; i--) {\n if (PROMPT_MARKER.test(tail[i])) {\n start = i;\n break;\n }\n }\n if (start === -1) return \"\";\n return tail.slice(start).join(\" \");\n}\n\n/**\n * A stable fragment of the message to look for.\n *\n * The head of the text rather than the whole of it: the composer wraps, the pane\n * truncates, and a trailing `@mention` may already have been rewritten by the\n * very popup we are checking for — so the tail is exactly the part that cannot\n * be relied on. Whitespace is collapsed because wrapping introduces its own.\n */\nexport function submissionProbe(text: string): string {\n return text.replace(/\\s+/g, \" \").trim().slice(0, 40);\n}\n\n/**\n * True when the message is still sitting in the composer, i.e. it was NOT\n * submitted. A message short enough to produce no usable probe is reported as\n * submitted rather than guessed at — a false \"undelivered\" would have the\n * caller resend, and a duplicate room message is worse than an unchecked one.\n */\nexport function composerStillHolds(lines: string[], text: string): boolean {\n const probe = submissionProbe(text);\n if (probe.length < 8) return false;\n const composer = composerBlock(lines).replace(/\\s+/g, \" \");\n return composer.includes(probe);\n}\n","/**\n * TmuxBridge — state-aware event injection into Claude Code.\n *\n * Reads the Claude Code TUI screen via `tmux capture-pane`, detects the\n * current UI state, and applies the right injection strategy:\n *\n * idle → inject directly\n * typing → Ctrl+U (cut), inject, Ctrl+Y (restore)\n * dialog → queue and poll\n * permission → queue and poll\n * streaming → queue and poll\n * unknown → queue and poll (safe default)\n *\n * Events that can't be injected immediately are queued and drained\n * when the state becomes safe.\n */\n\nimport {\n tmuxPaneIsShell,\n tmuxCapturePane,\n tmuxCapturePaneStyled,\n tmuxInjectText,\n tmuxSendEnter,\n tmuxSendKey,\n} from \"../tmux.js\";\nimport { composerStillHolds } from \"../composer.js\";\nimport { contentPartsToString } from \"../../agent/prompts.js\";\nimport type { ContentPart } from \"../../agent/types.js\";\n\nexport type TuiState =\n /** The CLI is not running — the pane has fallen back to a shell. */\n | \"absent\"\n | \"idle\"\n | \"typing\"\n | \"dialog\"\n | \"permission\"\n | \"streaming\"\n | \"unknown\";\n\n// Spinner characters used by Claude Code during streaming\nconst SPINNER_CHARS = \"⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\";\n\n/**\n * Claude Code is mid-turn but shows none of the braille spinner glyphs.\n *\n * Current Claude Code (checked on v2.1.299) draws its live status with other\n * glyphs — \"✢ Perusing… (2s · ↓ 88 tokens · thinking)\" — so the spinner scan\n * misses it. The shape is stable even though the verb rotates: a word, an\n * ellipsis, and a parenthesised elapsed time. The finished form (\"✻ Brewed for\n * 1s · done 11:33 AM\") has no ellipsis and does not match.\n *\n * \"← 1 agent\" in the footer used to be read as \"a subagent is running\". On\n * v2.1.299 it is the opposite: every idle session shows it (four live panes\n * checked), and it disappears while a background agent runs. Treating it as\n * busy made every idle Claude Code pane look busy forever, so events only got\n * through via the 30s force-inject. A background agent also no longer blocks\n * submission — a room nudge typed while one ran was answered normally.\n */\nconst BUSY_PATTERNS: RegExp[] = [\n /[A-Za-zÀ-ÿ]+…\\s*\\(\\d+(?:m\\s*\\d+)?s\\b/,\n];\n\n/**\n * Does Claude Code's composer hold nothing the human typed?\n *\n * A plain capture cannot tell a draft from the dim placeholder or suggestion\n * Claude Code shows in an empty composer (\"Try \\\"fix typecheck errors\\\"\",\n * \"check the room for messages\") — both read as text after ❯. The styled\n * capture can: the placeholder is drawn dim (ESC[2m), a draft is not.\n */\nexport function composerHoldsOnlyGhost(styledLines: string[]): boolean {\n const SGR = /\\x1b\\[[0-9;]*m/g;\n for (let i = styledLines.length - 1; i >= 0; i--) {\n const plain = styledLines[i].replace(SGR, \"\").trimStart();\n if (!/^[❯›](\\s|$)/.test(plain)) continue;\n const afterPrompt = styledLines[i].slice(styledLines[i].search(/[❯›]/) + 1);\n const typed = afterPrompt\n .replace(/\\x1b\\[2m[\\s\\S]*?(?:\\x1b\\[(?:0|22)?m|$)/g, \"\")\n .replace(SGR, \"\")\n .trim();\n return typed.length === 0;\n }\n return false;\n}\n\n// Patterns that indicate a selection/question dialog\nconst DIALOG_PATTERNS = [\n \"Enter to select\",\n \"to navigate\",\n \"Esc to cancel\",\n \"Ready to code?\",\n \"Review your answers\",\n \"ctrl+g to edit in\",\n];\n\n// Patterns that indicate a permission/confirmation prompt\nconst PERMISSION_PATTERNS = [\n \"(Y)\",\n \"Allow \",\n \"Deny \",\n \"approve\",\n \"Yes / No\",\n // Current Claude Code wording, captured from a live prompt. Without these a\n // permission dialog reads as idle and the bridge pastes a room event into\n // it, answering on the agent's behalf.\n \"Do you want to proceed?\",\n \"don't ask again\",\n // The folder-trust modal, which no flag skips.\n \"Yes, I trust this folder\",\n];\n\n/**\n * Patterns specific enough to put \"⏸ needs you\" in front of a human.\n *\n * DIALOG_PATTERNS and PERMISSION_PATTERNS above deliberately over-match, and\n * for their original consumer that is correct: they gate DELIVERY, where a\n * false positive only queues a room event for the drain loop to retry. Cheap.\n *\n * That reasoning was then reused for a second consumer with the opposite cost\n * — the agent state reported to the room — where a false positive stops the\n * operator and makes them wait on an agent that needs nothing. Observed: the\n * bare substring `\"approve\"` matched an agent's own tool output, \"hard cap, set\n * by the approved top\", and the room strip read \"⏸ needs you\" at an agent 23\n * minutes into a harness run. `\"to navigate\"` is the same trap and worse — it\n * matches any sentence about navigating, in a project whose entire subject is\n * navigating by bearings.\n *\n * So: two lists. Over-match to protect the agent, under-match before\n * interrupting the human. These are UI chrome a prompt actually renders —\n * wordings and shapes, not vocabulary that can occur in prose.\n */\nconst BLOCKING_PATTERNS: Array<string | RegExp> = [\n \"Do you want to proceed?\",\n \"don't ask again\",\n \"Yes, I trust this folder\",\n \"Ready to code?\",\n \"Enter to select\",\n /❯\\s*1\\.\\s*Yes/,\n /\\(y\\s*\\/\\s*n\\)/i,\n // The arrow glyphs claude renders beside it. Bare \"to navigate\" is prose.\n /[↑↓⬆⬇]\\s*(?:[↑↓⬆⬇]\\s*)?to navigate/,\n];\n\n/** Match a mixed string/RegExp pattern list, case-insensitively for strings. */\nfunction matchesAny(text: string, patterns: Array<string | RegExp>): boolean {\n const haystack = text.toLowerCase();\n for (const pattern of patterns) {\n if (typeof pattern === \"string\") {\n if (haystack.includes(pattern.toLowerCase())) return true;\n } else if (pattern.test(text)) {\n return true;\n }\n }\n return false;\n}\n\nexport interface DeliverOptions {\n /**\n * Drop this text if an identical copy is already queued. For callers that\n * retry a delivery they cannot observe landing (the room invite loop).\n */\n dedupe?: boolean;\n}\n\nexport interface TmuxBridgeOptions {\n /** How often to poll when events are queued (ms). Default: 200 */\n pollIntervalMs?: number;\n /** How long to wait between Ctrl+U/inject/Ctrl+Y steps (ms). Default: 50 */\n keystrokeDelayMs?: number;\n /**\n * Called when a message could not be got into the agent, after the retry.\n *\n * Delivery used to be assumed: apiary typed, reported success and moved on,\n * so a swallowed submit looked exactly like an agent choosing not to answer.\n * This is the hook that makes the difference visible to someone.\n */\n onUndelivered?: (text: string) => void;\n /**\n * A human drives this pane (`apiary mcp` inside their own session). Default\n * false, for panes apiary launched and nobody types in.\n *\n * When true, text goes in only when the composer is empty — never cut and\n * restored around a draft (Ctrl+U clears one line, so a wrapped draft was\n * split in two and half of it sent with the event, seen live) — and never\n * forced after ~30s: a human session can sit on a real permission prompt for\n * minutes, and typing into it would answer the prompt on their behalf.\n */\n attended?: boolean;\n}\n\n/**\n * Last resort when no one has wired a handler: say it on stderr, which lands in\n * the launcher pane. Better than nothing, and a great deal better than success.\n */\nfunction defaultUndeliveredWarning(text: string): void {\n const preview = text.replace(/\\s+/g, \" \").slice(0, 80);\n process.stderr.write(`apiary: message NOT delivered to the agent — still in its composer: ${preview}\\n`);\n}\n\nexport class TmuxBridge {\n private session: string;\n private queue: string[] = [];\n private pollTimer: ReturnType<typeof setInterval> | null = null;\n private pollIntervalMs: number;\n private keystrokeDelayMs: number;\n private onUndelivered: (text: string) => void;\n private attended: boolean;\n private stopped = false;\n private blockedCount = 0;\n private static readonly BLOCKED_THRESHOLD = 150; // ~30s at 200ms poll — force inject if stuck\n\n constructor(session: string, opts?: TmuxBridgeOptions) {\n this.session = session;\n this.pollIntervalMs = opts?.pollIntervalMs ?? 200;\n this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;\n this.onUndelivered = opts?.onUndelivered ?? defaultUndeliveredWarning;\n this.attended = opts?.attended ?? false;\n }\n\n /**\n * Delivery callback — drop-in replacement for the raw tmuxDeliver lambda.\n * Pass `bridge.deliver.bind(bridge)` to EventProcessor.run().\n */\n async deliver(parts: ContentPart[], opts?: DeliverOptions): Promise<void> {\n const text = contentPartsToString(parts);\n if (!text.trim()) return;\n\n this.inject(text, opts?.dedupe === true);\n }\n\n /**\n * Detect the current TUI state by reading the screen.\n * Exported for testing — the heuristic logic is in detectStateFromLines().\n */\n detectState(): TuiState {\n // See the Codex bridge: a shell prompt is indistinguishable from an idle\n // composer once you are only looking at rendered text.\n if (tmuxPaneIsShell(this.session)) return \"absent\";\n const lines = this.captureScreen();\n return detectStateFromLines(lines);\n }\n\n /**\n * State to REPORT to the room, as opposed to the state used to decide\n * whether it is safe to type. Same screen, stricter about claiming a human\n * is needed — see detectReportedStateFromLines.\n */\n detectReportedState(): TuiState {\n if (tmuxPaneIsShell(this.session)) return \"absent\";\n return detectReportedStateFromLines(this.captureScreen());\n }\n\n /**\n * Extract claude's live status label from the pane (the \"Sautéed for 12s\",\n * \"Compacting conversation… 23%\" lines under the prompt). Returns null when\n * the agent is idle / no status visible.\n *\n * The room TUI calls this through the agent runtime's poll loop to surface\n * \"is the agent actually working or stuck?\" without making the user attach\n * to each tmux pane.\n */\n getActivityLabel(): string | null {\n return extractActivityLabel(this.captureScreen());\n }\n\n /**\n * Try to inject text, choosing strategy based on TUI state.\n * If the state is unsafe, queues the text and starts polling.\n *\n * Text is flattened to a single line before injection to avoid triggering\n * Claude Code's paste detection. When multi-line text arrives via\n * `send-keys -l`, Claude Code detects it as a paste and collapses it into\n * \"[Pasted text #1 +N lines]\" which may not reliably submit with Enter.\n */\n private inject(text: string, dedupe: boolean): void {\n const flat = text.replace(/\\n/g, \" \");\n const state = this.deliveryState();\n\n switch (state) {\n case \"idle\":\n this.injectIdle(flat);\n break;\n case \"typing\":\n this.injectWhileTyping(flat);\n break;\n default:\n // dialog, permission, streaming, unknown — queue it\n this.enqueue(flat, dedupe);\n break;\n }\n }\n\n /**\n * The state delivery acts on. Unattended it is the screen state. Attended,\n * \"typing\" splits in two: only placeholder text is idle; a real draft is the\n * human's, so the event waits (\"unknown\" queues it).\n */\n private deliveryState(): TuiState {\n const state = this.detectState();\n if (!this.attended || state !== \"typing\") return state;\n return composerHoldsOnlyGhost(tmuxCapturePaneStyled(this.session)) ? \"idle\" : \"unknown\";\n }\n\n /** Capture the screen via tmux capture-pane. */\n private captureScreen(): string[] {\n return tmuxCapturePane(this.session);\n }\n\n /**\n * Inject into an idle prompt: type text + Enter, then check it went.\n *\n * This used to fire a second Enter unconditionally as a \"safety net\" against\n * Claude Code's paste detection swallowing the first. That is a guess in both\n * directions: it cannot tell whether the first Enter worked, and it cannot\n * tell whether the second one did either. Reading the composer back answers\n * the question instead, and only sends the second Enter when it is needed.\n */\n private injectIdle(text: string): void {\n tmuxInjectText(this.session, text);\n tmuxSendEnter(this.session);\n this.confirmSubmitted(text);\n }\n\n /**\n * Check the message left the composer, and press Enter once more if it did not.\n *\n * A submitted message moves into the transcript; one still in the composer was\n * never sent. If it is still there after the retry, the room is told rather\n * than left to read the agent's silence as a choice.\n */\n private confirmSubmitted(text: string): void {\n this.sleep(80);\n if (!composerStillHolds(this.captureScreen(), text)) return;\n\n tmuxSendEnter(this.session);\n this.sleep(120);\n if (!composerStillHolds(this.captureScreen(), text)) return;\n\n this.onUndelivered(text);\n }\n\n /**\n * Inject while the user is typing:\n * 1. Ctrl+U — cut line to kill ring\n * 2. Inject our text + Enter\n * 3. Ctrl+Y — paste the user's text back\n */\n private injectWhileTyping(text: string): void {\n // Cut user's current input\n tmuxSendKey(this.session, \"C-u\");\n this.sleep(this.keystrokeDelayMs);\n\n // Inject our event, then confirm rather than hope\n tmuxInjectText(this.session, text);\n tmuxSendEnter(this.session);\n this.confirmSubmitted(text);\n this.sleep(this.keystrokeDelayMs);\n\n // Restore user's text\n tmuxSendKey(this.session, \"C-y\");\n }\n\n /** Add to queue and start polling if not already. */\n private enqueue(text: string, dedupe: boolean): void {\n // Off by default — room events must never be collapsed. Only callers that\n // retry a delivery they cannot observe landing (the invite loop) opt in.\n if (dedupe && this.queue.includes(text)) return;\n this.queue.push(text);\n this.startPolling();\n }\n\n /** Start the polling timer to drain queued events. */\n private startPolling(): void {\n if (this.pollTimer || this.stopped) return;\n this.pollTimer = setInterval(() => this.drainQueue(), this.pollIntervalMs);\n }\n\n /** Stop the polling timer. */\n private stopPolling(): void {\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n }\n\n /**\n * Try to drain one queued event if the state is safe.\n *\n * Drains one event at a time rather than batching all into one multi-line\n * string — multi-line text triggers Claude Code's paste detection which\n * collapses it into \"[Pasted text #1 +N lines]\".\n *\n * After injecting one event, the poll continues. The next cycle re-checks\n * state: if Claude is busy (streaming), remaining events wait. If idle,\n * the next event is injected. Events are already flattened in inject().\n */\n private drainQueue(): void {\n if (this.queue.length === 0) {\n this.stopPolling();\n this.blockedCount = 0;\n return;\n }\n\n const state = this.deliveryState();\n if (state === \"idle\" || state === \"typing\") {\n this.blockedCount = 0;\n const text = this.queue.shift()!;\n\n if (state === \"idle\") {\n this.injectIdle(text);\n } else {\n this.injectWhileTyping(text);\n }\n\n if (this.queue.length === 0) {\n this.stopPolling();\n }\n // else: keep polling to drain remaining events\n } else {\n // Any non-injectable state (streaming, dialog, permission, unknown)\n this.blockedCount++;\n if (!this.attended && this.blockedCount >= TmuxBridge.BLOCKED_THRESHOLD) {\n // Stuck too long — force inject to unblock\n this.blockedCount = 0;\n const text = this.queue.shift()!;\n this.injectIdle(text);\n if (this.queue.length === 0) this.stopPolling();\n }\n }\n }\n\n /** Cleanup. */\n stop(): void {\n this.stopped = true;\n this.stopPolling();\n this.queue.length = 0;\n }\n\n /** Synchronous sleep — only used for tiny keystroke delays. */\n private sleep(ms: number): void {\n if (ms <= 0) return;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n }\n}\n\n// ── Activity label extraction ────────────────────────────────────────────────\n\n/**\n * Pull claude's status string out of a captured pane.\n *\n * Claude's TUI shows these markers above the input prompt while it's working:\n * ✻ Fluttering… (38s · ↓ 2.1k tokens) <- the LIVE form, present throughout\n * ✻ Sautéed for 12s <- the FINISHED form, a moment at the end\n * ✻ Compacting conversation… 23%\n * ✻ Thinking…\n *\n * Three patterns, tried narrowest-first so the broad one cannot swallow the\n * others:\n * - PROG: a known long-op verb (Compacting/Thinking/Loading/Connecting)\n * followed by an optional description and optional N%. Stops at the first\n * `(` so it doesn't capture a trailing metadata blob.\n * - COOK: cooking-verb + \"for Ns\", the form claude leaves behind once a turn\n * ends. Matches unicode letters (Sautéed) via the Latin-1 supplement range.\n * - LIVE: any word, an ellipsis, and a parenthesised progress group. Matched\n * on SHAPE rather than vocabulary, because the verb is whimsical and\n * rotates — Improvising, Fluttering, Reticulating, Cogitating. A word list\n * goes stale the next time someone adds one, which is exactly how this\n * broke: with only COOK and PROG, a *working* agent matched nothing and\n * reported no label, so the room strip showed a bare glyph for as long as\n * the work took. The one moment the operator most needs to know what is\n * happening was the one moment it said nothing.\n *\n * LIVE deliberately keeps its parenthesised group — \"(38s · ↓ 2.1k tokens)\" is\n * the elapsed time and stream progress, which is what distinguishes \"moving\"\n * from \"wedged\". PROG still stops at `(` because its verbs carry their own\n * detail, and it is tried first so that stays true.\n *\n * Scan the last ~25 lines, return the newest match. Null when none → idle.\n */\nconst ACTIVITY_LIVE = /([A-Za-zÀ-ÿ]+…\\s*\\([^)\\n]*\\))/;\nconst ACTIVITY_COOK = /([A-Za-zÀ-ÿ]+ed\\s+for\\s+\\d+s)/;\nconst ACTIVITY_PROG = /((?:Compacting|Thinking|Loading|Connecting)[^()]*?(?:\\s+\\d+%)?)(?=\\s*[(\\n]|\\s*$)/;\nexport function extractActivityLabel(lines: string[]): string | null {\n if (lines.length === 0) return null;\n let best: string | null = null;\n for (const line of lines.slice(-25)) {\n // PROG first, then COOK, then LIVE. LIVE is the broadest — any word plus a\n // parenthesised group — so it must go last or it swallows lines the narrower\n // patterns own: \"Compacting conversation… (2m 9s · ↑ 3.3k tokens)\" is\n // reported as \"Compacting conversation…\" on purpose, and LIVE-first turned\n // that into \"conversation… (2m 9s · ↑ 3.3k tokens)\".\n const m = line.match(ACTIVITY_PROG) ?? line.match(ACTIVITY_COOK) ?? line.match(ACTIVITY_LIVE);\n if (m) best = m[1].trim();\n }\n return best;\n}\n\n// ── State detection heuristics ──────────────────────────────────────────────\n\n/**\n * Detect TUI state from capture-pane output lines.\n * Exported separately so it can be unit-tested without tmux.\n *\n * Claude Code's TUI layout (v2.1+):\n * ────────────────────────\n * ❯ <user input here>\n * ────────────────────────\n * PR #2 /ide ...\n *\n * The ❯ prompt sits between separator lines (─). No ❯❯ footer in v2.1+.\n */\nexport function detectStateFromLines(\n lines: string[],\n opts?: { ignorePrompts?: boolean },\n): TuiState {\n if (lines.length === 0) return \"unknown\";\n\n // Work with the last ~30 lines (the visible bottom of the screen)\n const tail = lines.slice(-30);\n const tailText = tail.join(\"\\n\");\n\n // `ignorePrompts` asks \"what is this screen doing APART from any prompt?\" —\n // used by detectReportedStateFromLines to answer the human's question once\n // the loose lists have been found to be matching prose rather than chrome.\n if (opts?.ignorePrompts !== true) {\n // 1. Dialog: selection/question/plan approval\n for (const pattern of DIALOG_PATTERNS) {\n if (tailText.includes(pattern)) return \"dialog\";\n }\n\n // 2. Permission prompt\n for (const pattern of PERMISSION_PATTERNS) {\n if (tailText.includes(pattern)) return \"permission\";\n }\n }\n\n // 3. Streaming: spinner characters in the last few lines\n const lastFew = tail.slice(-5).join(\"\");\n for (const ch of SPINNER_CHARS) {\n if (lastFew.includes(ch)) return \"streaming\";\n }\n\n // 3b. Busy with no spinner — a subagent is running. Checked after the\n // dialog and permission patterns on purpose: a prompt that needs a human\n // outranks \"also busy\", because only one of those the user can clear.\n // Only the bottom of the screen: the live status sits just above the\n // composer, and the same shape further up is output the agent printed.\n const statusArea = tail.slice(-10).join(\"\\n\");\n for (const pattern of BUSY_PATTERNS) {\n if (pattern.test(statusArea)) return \"streaming\";\n }\n\n // 4. Look for the ❯/› prompt line near the bottom.\n // Supports both old layout (❯❯ footer) and new layout (separator lines).\n const promptChar = /^[❯›](\\s|$)/;\n const footerChar = /^[❯›]{2}\\s/;\n const separatorLine = /^[─━─\\-]{10,}/;\n\n // Strategy A: old layout — find ❯❯ footer then ❯ prompt above it\n for (let i = tail.length - 1; i >= 0; i--) {\n const line = tail[i].trimStart();\n if (footerChar.test(line)) {\n // Found old-style footer, look for prompt above\n for (let j = i - 1; j >= 0; j--) {\n const above = tail[j].trimStart();\n if (promptChar.test(above)) {\n const content = above.replace(/^[❯›]\\s*/, \"\").trim();\n return content.length === 0 ? \"idle\" : \"typing\";\n }\n }\n break;\n }\n }\n\n // Strategy B: new layout — find ❯ prompt between/near separator lines\n for (let i = tail.length - 1; i >= 0; i--) {\n const line = tail[i].trimStart();\n if (promptChar.test(line)) {\n // Verify it's Claude's prompt by checking for separator line nearby\n const above = i > 0 ? tail[i - 1].trimStart() : \"\";\n const below = i < tail.length - 1 ? tail[i + 1].trimStart() : \"\";\n if (separatorLine.test(above) || separatorLine.test(below)) {\n const content = line.replace(/^[❯›]\\s*/, \"\").trim();\n return content.length === 0 ? \"idle\" : \"typing\";\n }\n }\n }\n\n return \"unknown\";\n}\n\n/**\n * The same screen read, but tuned for a human rather than for the delivery\n * queue: only claim a prompt is waiting when one is actually on screen.\n *\n * `detectStateFromLines` leans toward \"there is a prompt\" because a false\n * positive there merely queues an event. This one leans the other way, because\n * its false positive is \"⏸ needs you\" in the operator's face at an agent that\n * needs nothing — which costs a person's attention, and teaches them to stop\n * trusting the strip. When the loose lists fire but no specific prompt wording\n * is present, the screen is re-read with the prompt scan disabled so the\n * spinner and composer get to answer instead.\n */\nexport function detectReportedStateFromLines(lines: string[]): TuiState {\n const loose = detectStateFromLines(lines);\n if (loose !== \"dialog\" && loose !== \"permission\") return loose;\n if (matchesAny(lines.slice(-30).join(\"\\n\"), BLOCKING_PATTERNS)) return loose;\n return detectStateFromLines(lines, { ignorePrompts: true });\n}\n","/**\n * Wait for another agent to reach a state.\n *\n * Without this, an agent that needs to act after another one finishes has two\n * options, both bad: re-read the room on a loop (a full LLM turn per check,\n * which is the expensive thing), or guess and collide.\n *\n * It became buildable once runtimes started reporting the state they read off\n * their own CLI's screen, because \"working\" and \"blocked\" look identical from\n * the room's side and only one of them ever resolves on its own.\n *\n * Deliberately bounded rather than open-ended: the call has to return inside\n * the MCP client's tool timeout, so it reports the state it last saw and lets\n * the caller decide whether to wait again. A tool that hangs past the client's\n * timeout fails the turn, which is worse than one that says \"still working\".\n */\n\n/** The states a runtime reports. `unknown` means it reports nothing. */\nexport type AgentWaitState = \"idle\" | \"working\" | \"blocked\" | \"unknown\";\n\n/** What the caller is waiting for. */\nexport type WaitTarget = \"idle\" | \"blocked\" | \"working\" | \"change\";\n\nexport interface WaitForAgentOptions {\n /** Read the target's current state. `unknown` if it isn't reporting. */\n readState: () => Promise<AgentWaitState>;\n /** What to wait for. Default \"idle\" — \"they finished, my turn\". */\n until?: WaitTarget;\n /** Give up after this long. Default 60s, capped by `maxWaitMs`. */\n timeoutMs?: number;\n /** Hard ceiling, to stay inside the MCP client's tool timeout. */\n maxWaitMs?: number;\n /** Gap between reads. Default 2s — this is cheap, unlike an LLM turn. */\n pollIntervalMs?: number;\n sleep?: (ms: number) => Promise<void>;\n now?: () => number;\n}\n\nexport type WaitOutcome =\n | { result: \"satisfied\"; state: AgentWaitState; waitedMs: number }\n | { result: \"timeout\"; state: AgentWaitState; waitedMs: number }\n /** The target reports no state at all — waiting would never end. */\n | { result: \"not-reporting\"; state: \"unknown\"; waitedMs: number };\n\nexport const DEFAULT_WAIT_MS = 60_000;\nexport const MAX_WAIT_MS = 120_000;\nconst DEFAULT_POLL_MS = 2_000;\n\n/**\n * Clamp a requested wait to something that returns inside the MCP client's\n * tool timeout. Exported so the clamp is testable without actually waiting.\n */\nexport function resolveBudgetMs(timeoutMs: number | undefined, maxWaitMs: number = MAX_WAIT_MS): number {\n return Math.min(Math.max(timeoutMs ?? DEFAULT_WAIT_MS, 0), maxWaitMs);\n}\n\n/** Is `state` what the caller asked for? */\nexport function satisfies(state: AgentWaitState, until: WaitTarget, initial: AgentWaitState): boolean {\n if (state === \"unknown\") return false;\n if (until === \"change\") return state !== initial;\n return state === until;\n}\n\n/**\n * Poll until the target agent reaches `until`, or the deadline passes.\n *\n * Returns `not-reporting` immediately if the agent reports no state, rather\n * than burning the whole timeout on something that cannot change: a runtime\n * with no heartbeat (or one that has died) would otherwise look exactly like\n * an agent that is simply taking a long time.\n */\nexport async function waitForAgent(opts: WaitForAgentOptions): Promise<WaitOutcome> {\n const {\n readState,\n until = \"idle\",\n maxWaitMs = MAX_WAIT_MS,\n pollIntervalMs = DEFAULT_POLL_MS,\n sleep = (ms: number) => new Promise<void>((r) => { setTimeout(r, ms).unref?.(); }),\n now = () => Date.now(),\n } = opts;\n\n const budget = resolveBudgetMs(opts.timeoutMs, maxWaitMs);\n const started = now();\n\n const initial = await readState();\n if (initial === \"unknown\") {\n return { result: \"not-reporting\", state: \"unknown\", waitedMs: now() - started };\n }\n if (satisfies(initial, until, initial)) {\n return { result: \"satisfied\", state: initial, waitedMs: now() - started };\n }\n\n let latest: AgentWaitState = initial;\n // Counted rather than wall-clocked so an injected instant sleep cannot spin.\n const steps = Math.max(1, Math.ceil(budget / pollIntervalMs));\n for (let step = 0; step < steps; step++) {\n await sleep(pollIntervalMs);\n latest = await readState();\n if (satisfies(latest, until, initial)) {\n return { result: \"satisfied\", state: latest, waitedMs: now() - started };\n }\n }\n\n return { result: \"timeout\", state: latest, waitedMs: now() - started };\n}\n\n/** One line for the agent, saying what happened and what to do next. */\nexport function describeOutcome(\n participant: string,\n until: WaitTarget,\n outcome: WaitOutcome,\n): string {\n const secs = Math.round(outcome.waitedMs / 1000);\n switch (outcome.result) {\n case \"satisfied\":\n return `${participant} is now ${outcome.state} (waited ${secs}s).`;\n case \"timeout\":\n return `${participant} is still ${outcome.state} after ${secs}s — not ${until} yet. `\n + `Call again to keep waiting, or get on with something else.`;\n case \"not-reporting\":\n return `${participant} does not report its state, so there is nothing to wait for. `\n + `Only agents started by apiary with a Claude Code or Codex runtime report it. `\n + `Ask them directly instead.`;\n }\n}\n","/**\n * Shared MCP-server option builder for the CLI runtime.\n *\n * Both entry points wire the SAME set of local MCP tool handlers against an\n * EventProcessor + SseMultiplexer:\n * - `setupAgentRuntime` (apiary claude / codex / opencode — tmux/HTTP delivery)\n * - `runMcpServer` (apiary mcp — stdio, pull-based)\n *\n * The handlers proxy tool calls (join/leave/set-mode/ping + admin\n * set-mode-for/mute/unmute/kick/promote/demote) to the apiary server over HTTP.\n * Keeping them here stops the two entry points from drifting — which they had:\n * the stdio path was missing admin promote/demote and the room-rules fetch on\n * join. This builder is the single source for both.\n */\n\nimport { extractToken } from \"./auth.js\";\nimport { can } from \"../core/authority.js\";\nimport { RemoteRoomDataSource } from \"../agent/remote-room-data-source.js\";\nimport { EventProcessor } from \"../agent/event-processor.js\";\nimport { SseMultiplexer } from \"../agent/sse-multiplexer.js\";\nimport { buildCatchUpLines } from \"../agent/tool-handlers.js\";\nimport {\n waitForAgent,\n describeOutcome,\n type AgentWaitState,\n type WaitTarget,\n} from \"../agent/wait-for-agent.js\";\nimport { type EngagementMode } from \"../agent/engagement.js\";\nimport type { Participant, AuthorityLevel } from \"../core/types.js\";\nimport type { RuntimeMcpServerOptions, JoinRoomResult } from \"../agent/mcp/runtime.js\";\n\n/** A room the agent has joined, tracked for cache updates + cleanup. */\nexport interface JoinResult {\n serverUrl: string;\n sessionToken: string;\n participantId: string;\n roomName: string;\n roomId: string;\n authority: string;\n participants: Participant[];\n dataSource: RemoteRoomDataSource;\n}\n\nexport interface RuntimeHandlerContext {\n processor: EventProcessor;\n sseMux: SseMultiplexer;\n /** Mutable — join/leave handlers push and splice as the agent moves rooms. */\n joinResults: JoinResult[];\n agentName: string;\n authority: AuthorityLevel | undefined;\n /** Fired after a successful join (the tmux/HTTP path repaints its UI). */\n onRoomJoined?: () => void | Promise<void>;\n}\n\n/**\n * Build the `RuntimeMcpServerOptions` shared by every CLI runtime entry point.\n * Pass the result straight to `createRuntimeMcpServer` / `createStdioRuntimeMcpServer`.\n */\nexport function buildRuntimeMcpOptions(ctx: RuntimeHandlerContext): RuntimeMcpServerOptions {\n const { processor, sseMux, joinResults, agentName, authority, onRoomJoined } = ctx;\n\n /** Resolve a room + named participant, then POST to the apiary server. */\n const adminAction = (build: (id: string) => { path: string; body: Record<string, unknown> }) =>\n async (room: string, participant: string) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n const ds = conn.dataSource as RemoteRoomDataSource;\n const p = conn.dataSource.listParticipants().find((pp) => pp.name === participant);\n if (!p) return { success: false, error: `Unknown participant \"${participant}\".` };\n const { path, body } = build(p.id);\n try {\n const res = await fetch(`${ds.serverUrl}${path}`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${ds.sessionToken}` },\n body: JSON.stringify(body),\n });\n if (!res.ok) return { success: false, error: await res.text() };\n return { success: true };\n } catch {\n return { success: false, error: \"Server unreachable.\" };\n }\n };\n\n return {\n resolver: processor,\n toolOptions: {\n isEventSeen: (id) => processor.isEventSeen(id),\n markEventsSeen: (ids) => processor.markEventsSeen(ids),\n assignRef: (id) => processor.assignRef(id),\n resolveRef: (ref) => processor.resolveRef(ref),\n },\n authority,\n\n onSetMode: async (room, mode) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n processor.setModeForRoom(conn.dataSource.roomId, mode as EngagementMode, false);\n try {\n const ds = conn.dataSource as RemoteRoomDataSource;\n const res = await fetch(`${ds.serverUrl}/set-mode`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${ds.sessionToken}` },\n body: JSON.stringify({ mode }),\n });\n if (!res.ok) return { success: false, error: `Server rejected: ${await res.text()}` };\n } catch {\n // Server unreachable, local mode still set\n }\n return { success: true };\n },\n\n /**\n * Wait for another agent to reach a state, by polling the room server's\n * participant list. Two seconds of HTTP is far cheaper than the LLM turn\n * the agent would otherwise spend re-reading the room to check.\n */\n onWaitForAgent: async (room, participant, until, timeoutSec) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n const ds = conn.dataSource as RemoteRoomDataSource;\n const target = conn.dataSource.listParticipants().find((pp) => pp.name === participant);\n if (!target) return { success: false, error: `Unknown participant \"${participant}\".` };\n if (target.id === conn.dataSource.selfId) {\n return { success: false, error: \"You cannot wait for yourself.\" };\n }\n\n const readState = async (): Promise<AgentWaitState> => {\n try {\n const res = await fetch(`${ds.serverUrl}/participants`, {\n headers: { Authorization: `Bearer ${ds.sessionToken}` },\n signal: AbortSignal.timeout(10_000),\n });\n if (!res.ok) return \"unknown\";\n const data = (await res.json()) as {\n participants?: Array<{ id: string; agentState?: string }>;\n };\n const found = data.participants?.find((pp) => pp.id === target.id);\n const state = found?.agentState;\n return state === \"idle\" || state === \"working\" || state === \"blocked\" ? state : \"unknown\";\n } catch {\n // A transient failure must not read as a state change.\n return \"unknown\";\n }\n };\n\n const outcome = await waitForAgent({\n readState,\n until,\n timeoutMs: timeoutSec === undefined ? undefined : timeoutSec * 1000,\n });\n return { success: true, message: describeOutcome(participant, until ?? \"idle\", outcome) };\n },\n\n onPing: async (room, participant) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n const ds = conn.dataSource as RemoteRoomDataSource;\n const p = conn.dataSource.listParticipants().find((pp) => pp.name === participant);\n if (!p) return { success: false, error: `Unknown participant \"${participant}\".` };\n try {\n const res = await fetch(`${ds.serverUrl}/ping`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${ds.sessionToken}` },\n body: JSON.stringify({ participantId: p.id }),\n });\n if (!res.ok) return { success: false, error: await res.text() };\n return { success: true };\n } catch {\n return { success: false, error: \"Server unreachable.\" };\n }\n },\n\n onJoinRoom: async (url, alias, nameOverride) => {\n const token = extractToken(url);\n let serverUrl: string;\n try {\n const parsed = new URL(url);\n parsed.search = \"\";\n serverUrl = parsed.toString().replace(/\\/$/, \"\");\n } catch {\n serverUrl = url.replace(/\\/$/, \"\");\n }\n\n try {\n const joinName = nameOverride ?? agentName;\n const joinBody: Record<string, unknown> = { type: \"agent\", name: joinName };\n if (token) joinBody.token = token;\n\n const res = await fetch(`${serverUrl}/join`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(joinBody),\n signal: AbortSignal.timeout(15_000),\n });\n if (!res.ok) return { success: false, error: `Failed to join: ${await res.text()}` };\n\n const data = await res.json() as Record<string, unknown>;\n const sessionToken = String(data.sessionToken ?? \"\");\n const roomName = alias ?? String(data.roomName ?? \"\");\n const roomId = String(data.roomId ?? \"\");\n const roomAuthority = String(data.authority ?? \"member\");\n const participants = (data.participants as Participant[]) ?? [];\n const newParticipantId = String(data.participantId ?? \"\");\n\n const dataSource = new RemoteRoomDataSource(serverUrl, sessionToken, roomId);\n dataSource.setParticipants(participants);\n dataSource.setSelf(newParticipantId, joinName);\n\n // Set global selfId on first join; always set per-room selfId\n if (joinResults.length === 0) {\n processor.participantId = newParticipantId;\n }\n processor.setRoomParticipantId(roomId, newParticipantId);\n\n // Register in EventProcessor and SSE multiplexer\n const mode = processor.getModeForRoom(roomId) ?? \"everyone\";\n processor.connectRemoteRoom(dataSource, roomName);\n sseMux.addConnection(serverUrl, sessionToken, roomName, roomId);\n\n // Track for cleanup\n const jr: JoinResult = {\n serverUrl,\n sessionToken,\n participantId: newParticipantId,\n roomName,\n roomId,\n authority: roomAuthority,\n participants,\n dataSource,\n };\n joinResults.push(jr);\n\n // Build recent activity lines for the response\n const conn = processor.resolve(roomName);\n let recentLines: string[] = [];\n if (conn) {\n const catchUp = await buildCatchUpLines(conn, {\n isEventSeen: (id) => processor.isEventSeen(id),\n markEventsSeen: (ids) => processor.markEventsSeen(ids),\n assignRef: (id) => processor.assignRef(id),\n });\n recentLines = catchUp.lines;\n // TODO: catchUp.imageBlocks not surfaced here — the runtime injection\n // path doesn't support image content blocks yet. Wire up when adding\n // image delivery to the tmux/HTTP agent path.\n }\n\n await onRoomJoined?.();\n\n // Fetch this room's rules so they show up in the join response. Best\n // effort — older servers without /rules return 404 and we just skip.\n let rules: string[] | undefined;\n try {\n const rulesRes = await fetch(`${serverUrl}/rules`, {\n headers: { Authorization: `Bearer ${sessionToken}` },\n });\n if (rulesRes.ok) {\n const rulesData = (await rulesRes.json()) as { rules?: string[] };\n if (Array.isArray(rulesData.rules) && rulesData.rules.length > 0) rules = rulesData.rules;\n }\n } catch {\n /* old server or transient — skip */\n }\n\n return {\n success: true,\n roomName,\n agentName: joinName,\n authority: roomAuthority,\n mode,\n participants: participants\n .filter((p) => p.id !== newParticipantId)\n .map((p) => ({ name: p.name, authority: (p as any).authority ?? \"member\" })),\n recentLines,\n rules,\n } as JoinRoomResult;\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { success: false, error: `Unable to connect. Is the server running? (${serverUrl}) — ${msg}` };\n }\n },\n\n onLeaveRoom: async (room) => {\n const conn = processor.resolve(room);\n if (!conn) return { success: false, error: `Unknown room \"${room}\".` };\n const roomId = conn.dataSource.roomId;\n\n const idx = joinResults.findIndex((jr) => jr.roomId === roomId);\n if (idx >= 0) {\n const jr = joinResults[idx];\n sseMux.removeConnection(roomId);\n processor.disconnectRemoteRoom(roomId);\n\n try {\n await fetch(`${jr.serverUrl}/disconnect`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${jr.sessionToken}` },\n body: JSON.stringify({}),\n });\n } catch {\n // Server may be down\n }\n\n joinResults.splice(idx, 1);\n }\n return { success: true };\n },\n\n // Admin actions differ only by endpoint + target tier — one helper covers all.\n onAdminSetModeFor: can(authority, \"set_mode_for\")\n ? (room, participant, mode) =>\n adminAction((id) => ({ path: \"/set-mode\", body: { participantId: id, mode } }))(room, participant)\n : undefined,\n onAdminMute: can(authority, \"mute\")\n ? adminAction((id) => ({ path: \"/set-authority\", body: { participantId: id, authority: \"guest\" } }))\n : undefined,\n onAdminUnmute: can(authority, \"unmute\")\n ? adminAction((id) => ({ path: \"/set-authority\", body: { participantId: id, authority: \"member\" } }))\n : undefined,\n onAdminKick: can(authority, \"kick\")\n ? adminAction((id) => ({ path: \"/kick\", body: { participantId: id } }))\n : undefined,\n onAdminPromote: can(authority, \"promote\")\n ? adminAction((id) => ({ path: \"/set-authority\", body: { participantId: id, authority: \"product_owner\" } }))\n : undefined,\n onAdminDemote: can(authority, \"demote\")\n ? adminAction((id) => ({ path: \"/set-authority\", body: { participantId: id, authority: \"member\" } }))\n : undefined,\n };\n}\n"],"mappings":"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","names":[]}