@gorlitzer-labs/apiary 1.13.9 → 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.
Files changed (33) hide show
  1. package/README.md +17 -3
  2. package/dist/agent/index.d.ts +73 -0
  3. package/dist/agent/index.js +1 -1
  4. package/dist/{chunk-BCQQJR5E.js → chunk-3UQBSELW.js} +7 -178
  5. package/dist/chunk-3UQBSELW.js.map +1 -0
  6. package/dist/{chunk-7JXPJO3R.js → chunk-74XU7HHN.js} +6 -6
  7. package/dist/{chunk-4A7MREYX.js → chunk-N2LPKMPO.js} +185 -2
  8. package/dist/chunk-N2LPKMPO.js.map +1 -0
  9. package/dist/chunk-OEAHTZCA.js +994 -0
  10. package/dist/chunk-OEAHTZCA.js.map +1 -0
  11. package/dist/{chunk-MEWZT5KA.js → chunk-W4KWQKPP.js} +2 -2
  12. package/dist/{chunk-WJD5EKO6.js → chunk-Z4J6TRJY.js} +154 -41
  13. package/dist/chunk-Z4J6TRJY.js.map +1 -0
  14. package/dist/cli/index.js +39 -386
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/{invites-GRROUSCE.js → invites-SJ4ZVKVX.js} +4 -4
  17. package/dist/{join-H62VB4HU.js → join-5UFFMSLS.js} +4 -4
  18. package/dist/{room-ID4DQDPT.js → room-S6ULVAEH.js} +10 -10
  19. package/dist/run-5PSUQJF4.js +270 -0
  20. package/dist/run-5PSUQJF4.js.map +1 -0
  21. package/package.json +3 -2
  22. package/dist/chunk-4A7MREYX.js.map +0 -1
  23. package/dist/chunk-BCQQJR5E.js.map +0 -1
  24. package/dist/chunk-WJD5EKO6.js.map +0 -1
  25. package/dist/chunk-XLUTCAY6.js +0 -288
  26. package/dist/chunk-XLUTCAY6.js.map +0 -1
  27. package/dist/run-C22WQZVK.js +0 -91
  28. package/dist/run-C22WQZVK.js.map +0 -1
  29. /package/dist/{chunk-7JXPJO3R.js.map → chunk-74XU7HHN.js.map} +0 -0
  30. /package/dist/{chunk-MEWZT5KA.js.map → chunk-W4KWQKPP.js.map} +0 -0
  31. /package/dist/{invites-GRROUSCE.js.map → invites-SJ4ZVKVX.js.map} +0 -0
  32. /package/dist/{join-H62VB4HU.js.map → join-5UFFMSLS.js.map} +0 -0
  33. /package/dist/{room-ID4DQDPT.js.map → room-S6ULVAEH.js.map} +0 -0
@@ -5,10 +5,10 @@ import {
5
5
  invitePrompt,
6
6
  peekInvite,
7
7
  sameApiaryServer
8
- } from "./chunk-MEWZT5KA.js";
9
- import "./chunk-BCQQJR5E.js";
8
+ } from "./chunk-W4KWQKPP.js";
9
+ import "./chunk-3UQBSELW.js";
10
10
  import "./chunk-PEB6HEFT.js";
11
- import "./chunk-4A7MREYX.js";
11
+ import "./chunk-N2LPKMPO.js";
12
12
  import "./chunk-D7DXZQWT.js";
13
13
  import "./chunk-TMXQDGMN.js";
14
14
  import "./chunk-BZ4VDWC2.js";
@@ -20,4 +20,4 @@ export {
20
20
  peekInvite,
21
21
  sameApiaryServer
22
22
  };
23
- //# sourceMappingURL=invites-GRROUSCE.js.map
23
+ //# sourceMappingURL=invites-SJ4ZVKVX.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  HEADLESS_READY,
3
3
  join
4
- } from "./chunk-7JXPJO3R.js";
5
- import "./chunk-BCQQJR5E.js";
4
+ } from "./chunk-74XU7HHN.js";
5
+ import "./chunk-3UQBSELW.js";
6
6
  import "./chunk-PEB6HEFT.js";
7
- import "./chunk-4A7MREYX.js";
7
+ import "./chunk-N2LPKMPO.js";
8
8
  import "./chunk-D7DXZQWT.js";
9
9
  import "./chunk-TMXQDGMN.js";
10
10
  import "./chunk-BZ4VDWC2.js";
@@ -12,4 +12,4 @@ export {
12
12
  HEADLESS_READY,
13
13
  join
14
14
  };
15
- //# sourceMappingURL=join-H62VB4HU.js.map
15
+ //# sourceMappingURL=join-5UFFMSLS.js.map
@@ -9,16 +9,16 @@ import {
9
9
  roomEmoji,
10
10
  roomRulesPath,
11
11
  saveRoomSession,
12
- tailnetAddress,
13
- tmuxAvailable,
14
- tmuxKillSession,
15
- tmuxSessionExists
16
- } from "./chunk-BCQQJR5E.js";
12
+ tailnetAddress
13
+ } from "./chunk-3UQBSELW.js";
17
14
  import "./chunk-PEB6HEFT.js";
18
15
  import {
19
16
  buildShareUrl,
20
- extractToken
21
- } from "./chunk-4A7MREYX.js";
17
+ extractToken,
18
+ tmuxAvailable,
19
+ tmuxKillSession,
20
+ tmuxSessionExists
21
+ } from "./chunk-N2LPKMPO.js";
22
22
  import "./chunk-D7DXZQWT.js";
23
23
  import "./chunk-TMXQDGMN.js";
24
24
  import "./chunk-BZ4VDWC2.js";
@@ -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-H62VB4HU.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-H62VB4HU.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-ID4DQDPT.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":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorlitzer-labs/apiary",
3
- "version": "1.13.9",
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/auth.ts"],"sourcesContent":["/**\n * Auth token system for apiary share links and session management.\n *\n * Two token types:\n * - Share tokens — embedded in URLs, map to an authority tier.\n * Anyone with the link joins at that tier.\n * - Session tokens — issued on join, identify a participant + authority.\n * Used for all subsequent API calls.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { AuthorityLevel } from \"../core/types.js\";\nimport { TIER_ORDER } from \"../core/authority.js\";\n\ninterface SessionData {\n participantId: string;\n authority: AuthorityLevel;\n createdAt: number;\n expiresAt: number;\n}\n\ninterface ShareTokenData {\n authority: AuthorityLevel;\n createdAt: number;\n /** null = never expires. Only the room's own founding pair gets that. */\n expiresAt: number | null;\n}\n\nexport interface TokenManagerOptions {\n /** Session token TTL in ms. Default: 24 hours. */\n sessionTtlMs?: number;\n /** Share token TTL in ms. Default: 1 hour. */\n shareTtlMs?: number;\n}\n\nconst DEFAULT_SESSION_TTL = 24 * 60 * 60 * 1000; // 24h\nconst DEFAULT_SHARE_TTL = 24 * 60 * 60 * 1000; // 24h\n\nexport class TokenManager {\n /** share token → share data */\n private _shareTokens = new Map<string, ShareTokenData>();\n /** session token → participant data */\n private _sessionTokens = new Map<string, SessionData>();\n\n private _sessionTtlMs: number;\n private _shareTtlMs: number;\n\n constructor(options?: TokenManagerOptions) {\n this._sessionTtlMs = options?.sessionTtlMs ?? DEFAULT_SESSION_TTL;\n this._shareTtlMs = options?.shareTtlMs ?? DEFAULT_SHARE_TTL;\n }\n\n /**\n * Generate a share token at the given authority tier.\n * Callers can only generate tokens at their own tier or below.\n *\n * `neverExpires` is for the room's OWN founding pair, not for links handed\n * to anybody. Those two tokens are written into ~/.apiary/sessions and are\n * the only recorded way back into a running room, so expiring them locks the\n * owner out of their own live server: observed at 26 hours, with a room\n * still serving, agents still working inside it, and `apiary room resume`\n * answering \"Invalid share token\" with no hint that time was the cause.\n *\n * A TTL there protected nothing — the token sits in a file already readable\n * only by its owner — while the thing it broke was the operator's way in. A\n * link generated by /share is a genuine hand-out and still expires.\n */\n generateShareToken(\n callerAuthority: AuthorityLevel,\n targetAuthority: AuthorityLevel,\n opts?: { neverExpires?: boolean },\n ): string | null {\n if (!canGrant(callerAuthority, targetAuthority)) return null;\n const token = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n this._shareTokens.set(token, {\n authority: targetAuthority,\n createdAt: now,\n expiresAt: opts?.neverExpires === true ? null : now + this._shareTtlMs,\n });\n return token;\n }\n\n /** Validate a share token and return its authority level. Deletes if expired. */\n validateShareToken(token: string): AuthorityLevel | null {\n const data = this._shareTokens.get(token);\n if (!data) return null;\n // A null expiry is the room's own founding token: it lives as long as the\n // room does, because it IS the room's credential rather than a hand-out.\n if (data.expiresAt !== null && Date.now() > data.expiresAt) {\n this._shareTokens.delete(token);\n return null;\n }\n return data.authority;\n }\n\n /** Create a session token for a participant. */\n createSessionToken(participantId: string, authority: AuthorityLevel): string {\n const token = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n this._sessionTokens.set(token, {\n participantId,\n authority,\n createdAt: now,\n expiresAt: now + this._sessionTtlMs,\n });\n return token;\n }\n\n /** Validate a session token and return participant data. Deletes if expired. */\n validateSessionToken(token: string): SessionData | null {\n const data = this._sessionTokens.get(token);\n if (!data) return null;\n if (Date.now() > data.expiresAt) {\n this._sessionTokens.delete(token);\n return null;\n }\n return data;\n }\n\n /** Revoke a session token (on disconnect). */\n revokeSessionToken(token: string): void {\n this._sessionTokens.delete(token);\n }\n\n /** Update the authority level for an existing session. */\n updateSessionAuthority(token: string, newAuthority: AuthorityLevel): boolean {\n const data = this._sessionTokens.get(token);\n if (!data) return false;\n data.authority = newAuthority;\n return true;\n }\n\n /** Find a session token by participant ID (for cleanup). */\n findSessionByParticipant(participantId: string): string | null {\n for (const [token, data] of this._sessionTokens) {\n if (data.participantId === participantId) return token;\n }\n return null;\n }\n\n /** Rotate a session token: create new with same participant data, delete old. */\n rotateSessionToken(oldToken: string): { newToken: string; data: SessionData } | null {\n const data = this._sessionTokens.get(oldToken);\n if (!data) return null;\n if (Date.now() > data.expiresAt) {\n this._sessionTokens.delete(oldToken);\n return null;\n }\n this._sessionTokens.delete(oldToken);\n const newToken = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n const newData: SessionData = {\n participantId: data.participantId,\n authority: data.authority,\n createdAt: now,\n expiresAt: now + this._sessionTtlMs,\n };\n this._sessionTokens.set(newToken, newData);\n return { newToken, data: newData };\n }\n\n /** Sweep expired tokens from both maps. */\n pruneExpired(): void {\n const now = Date.now();\n for (const [token, data] of this._shareTokens) {\n // `now > null` is true in JS — null coerces to 0 — so an unguarded sweep\n // would delete every never-expiring token on its first pass and undo the\n // whole point of them.\n if (data.expiresAt !== null && now > data.expiresAt) this._shareTokens.delete(token);\n }\n for (const [token, data] of this._sessionTokens) {\n if (now > data.expiresAt) this._sessionTokens.delete(token);\n }\n }\n}\n\n/**\n * Can a caller at `callerLevel` grant a share token at `targetLevel`?\n * Admin/product_owner links can only be granted by admins.\n * Everyone else can grant links at or below their own tier (excluding PO/admin).\n */\nfunction canGrant(callerLevel: AuthorityLevel, targetLevel: AuthorityLevel): boolean {\n if (targetLevel === \"admin\" || targetLevel === \"product_owner\") {\n return callerLevel === \"admin\";\n }\n return TIER_ORDER[callerLevel] >= TIER_ORDER[targetLevel];\n}\n\n/** Build a share URL from a base URL and token. */\nexport function buildShareUrl(baseUrl: string, token: string): string {\n const url = new URL(baseUrl);\n url.searchParams.set(\"token\", token);\n return url.toString();\n}\n\n/** Extract a token from a share URL. */\nexport function extractToken(url: string): string | null {\n try {\n const parsed = new URL(url);\n return parsed.searchParams.get(\"token\");\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}