stoops 0.1.0 → 0.2.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.
@@ -0,0 +1,692 @@
1
+ import {
2
+ EVENT_ROLE
3
+ } from "./chunk-HQS7HBZR.js";
4
+
5
+ // src/agent/prompts.ts
6
+ var MODE_DESCRIPTIONS = {
7
+ "everyone": "All messages are pushed to you.",
8
+ "people": "Human messages are pushed to you. Agent messages are delivered as context.",
9
+ "agents": "Agent messages are pushed to you. Human messages are delivered as context.",
10
+ "me": "Only your person's messages are pushed to you. Others are delivered as context.",
11
+ "standby-everyone": "Only @mentions are pushed to you.",
12
+ "standby-people": "Only human @mentions are pushed to you.",
13
+ "standby-agents": "Only agent @mentions are pushed to you.",
14
+ "standby-me": "Only your person's @mentions are pushed to you."
15
+ };
16
+ var SYSTEM_PREAMBLE = `You are a participant in group chats. You may be connected to multiple rooms at once \u2014 events from all of them flow to you, labeled with the room name.
17
+
18
+ ## How this works
19
+ - Messages appear labeled: "[Design Room] [human] Alice: hey everyone"
20
+ - Replies: "[Design Room] [human] Alice (replying to [human] Bob): good point"
21
+ - @mentions: "\u26A1 [Design Room] [human] Alice mentioned you: @Bob what do you think?"
22
+ - All your tools require a room name as the first parameter
23
+ - Rooms have a stable \`identifier\` (e.g., design-room) that doesn't change even if renamed
24
+ - Message references like #3847 are internal tool labels only. Never include them in messages \u2014 participants don't see them.
25
+
26
+ ## Your memory
27
+ You have no persistent memory between sessions. Each time you start, you're waking up fresh. Your conversations are still there \u2014 read them via tools.
28
+
29
+ If you lack context for something someone references, say so directly \u2014 don't invent explanations.
30
+
31
+ ## Your person
32
+ You were created by someone \u2014 that's your person. You know their participant ID from your identity block below. Their messages carry more weight: they're the one who set you up and knows what they want from you. In group rooms, stay tuned to them even when others are talking.
33
+
34
+ When someone who isn't your person addresses you in a group room, respond if it's useful and natural. But don't lose track of who you're ultimately here for.
35
+
36
+ ## Engagement modes
37
+ Each room has a mode controlling when you evaluate and respond:
38
+ - everyone \u2014 all messages trigger evaluation. Respond when you have something genuine to add.
39
+ - people \u2014 any human message triggers you. Agent messages are buffered as context.
40
+ - agents \u2014 any agent message triggers you. Human messages are buffered as context.
41
+ - me \u2014 only your person's messages trigger evaluation. Read everything else quietly.
42
+ - standby-everyone \u2014 only @mentions wake you. Stay silent unless directly called, by anyone.
43
+ - standby-people \u2014 only human @mentions wake you.
44
+ - standby-agents \u2014 only agent @mentions wake you.
45
+ - standby-me \u2014 only your person's @mention wakes you.
46
+
47
+ Non-everyone rooms show the mode in the room label (e.g., "[Design Room \u2014 people]").`;
48
+ function getSystemPreamble(identifier, personParticipantId) {
49
+ const lines = [];
50
+ if (identifier) lines.push(`Your identifier: @${identifier}`);
51
+ if (personParticipantId) lines.push(`Your person's participant ID: ${personParticipantId}`);
52
+ if (identifier) lines.push(`Recognize other participants by their identifier. Address them by their current display name.`);
53
+ const identityBlock = lines.length > 0 ? `## Your identity
54
+ ${lines.join("\n")}
55
+
56
+ ` : "";
57
+ return identityBlock + SYSTEM_PREAMBLE;
58
+ }
59
+ function messageRef(messageId) {
60
+ return messageId.replace(/-/g, "").slice(0, 4);
61
+ }
62
+ function participantLabel(p, fallback) {
63
+ if (!p) return fallback ?? "someone";
64
+ return `[${p.type}] ${p.name}`;
65
+ }
66
+ function resolveName(resolveParticipant, id, fallback) {
67
+ return resolveParticipant(id)?.name ?? fallback ?? "someone";
68
+ }
69
+ function formatTimestamp(date) {
70
+ return date.toISOString().slice(11, 19);
71
+ }
72
+ function contentPartsToString(parts) {
73
+ return parts.map((p) => p.type === "text" ? p.text : ` [image: ${p.url}]`).join("");
74
+ }
75
+ function visualLength(s) {
76
+ if (typeof Intl !== "undefined" && Intl.Segmenter) {
77
+ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
78
+ let count = 0;
79
+ for (const _ of segmenter.segment(s)) count++;
80
+ return count;
81
+ }
82
+ return [...s].length;
83
+ }
84
+ function formatMultilineContent(content, roomLabel, prefix) {
85
+ const lines = content.split("\n");
86
+ if (lines.length <= 1) return content;
87
+ const continuation = roomLabel ? `[${roomLabel}] ` : "";
88
+ const pad = " ".repeat(visualLength(prefix));
89
+ return lines[0] + "\n" + lines.slice(1).map((l) => `${pad}${continuation}${l}`).join("\n");
90
+ }
91
+ function formatEvent(event, resolveParticipant, replyContext, roomLabel, reactionTarget, assignRef) {
92
+ const r = roomLabel ? `[${roomLabel}] ` : "";
93
+ const ts = `[${formatTimestamp("timestamp" in event ? new Date(event.timestamp) : /* @__PURE__ */ new Date())}] `;
94
+ const mkRef = (id) => `#${assignRef ? assignRef(id) : messageRef(id)}`;
95
+ switch (event.type) {
96
+ case "MessageSent": {
97
+ const msg = event.message;
98
+ const name = resolveName(resolveParticipant, msg.sender_id, msg.sender_name);
99
+ const ref = mkRef(msg.id);
100
+ const linePrefix = `${ts}${ref} ${r}`;
101
+ let text;
102
+ if (msg.reply_to_id && replyContext) {
103
+ const rRef = assignRef ? mkRef(msg.reply_to_id) : ref;
104
+ text = `${linePrefix}${name} (\u2192 ${rRef} ${replyContext.senderName}): ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name} (\u2192 ${rRef} ${replyContext.senderName}): `)}`;
105
+ } else {
106
+ text = `${linePrefix}${name}: ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name}: `)}`;
107
+ }
108
+ const parts = [{ type: "text", text }];
109
+ if (msg.image_url) parts.push({ type: "image", url: msg.image_url });
110
+ return parts;
111
+ }
112
+ case "Mentioned": {
113
+ const msg = event.message;
114
+ const name = resolveName(resolveParticipant, msg.sender_id, msg.sender_name);
115
+ const ref = mkRef(msg.id);
116
+ const linePrefix = `${ts}${ref} ${r}\u26A1 `;
117
+ const text = `${linePrefix}${name}: ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name}: `)}`;
118
+ const parts = [{ type: "text", text }];
119
+ if (msg.image_url) parts.push({ type: "image", url: msg.image_url });
120
+ return parts;
121
+ }
122
+ case "ToolUse":
123
+ return null;
124
+ case "Activity":
125
+ return null;
126
+ case "ReactionAdded": {
127
+ const name = resolveName(resolveParticipant, event.participant_id);
128
+ const targetRef = reactionTarget ? ` to ${mkRef(event.message_id)}` : "";
129
+ return [{ type: "text", text: `${ts}${r}${name} reacted ${event.emoji}${targetRef}` }];
130
+ }
131
+ case "ReactionRemoved":
132
+ return null;
133
+ case "ParticipantJoined": {
134
+ const name = event.participant?.name ?? "someone";
135
+ return [{ type: "text", text: `${ts}${r}+ ${name} joined` }];
136
+ }
137
+ case "ParticipantLeft": {
138
+ const name = event.participant?.name ?? "someone";
139
+ return [{ type: "text", text: `${ts}${r}- ${name} left` }];
140
+ }
141
+ case "ContextCompacted":
142
+ return null;
143
+ default:
144
+ return null;
145
+ }
146
+ }
147
+
148
+ // src/agent/engagement.ts
149
+ var VALID_MODES = /* @__PURE__ */ new Set([
150
+ "me",
151
+ "people",
152
+ "agents",
153
+ "everyone",
154
+ "standby-me",
155
+ "standby-people",
156
+ "standby-agents",
157
+ "standby-everyone"
158
+ ]);
159
+ function isValidMode(mode) {
160
+ return VALID_MODES.has(mode);
161
+ }
162
+ function senderMatches(filter, senderType, senderId, personParticipantId) {
163
+ switch (filter) {
164
+ case "everyone":
165
+ return true;
166
+ case "people":
167
+ return senderType === "human";
168
+ case "agents":
169
+ return senderType === "agent";
170
+ case "me":
171
+ return !!personParticipantId && senderId === personParticipantId;
172
+ default:
173
+ return false;
174
+ }
175
+ }
176
+ function classify(event, mode, selfId, senderType, senderId, personParticipantId) {
177
+ const role = EVENT_ROLE[event.type];
178
+ if (role === "internal") return "drop";
179
+ if (role !== "mention" && event.participant_id === selfId) return "drop";
180
+ const isStandby = mode.startsWith("standby-");
181
+ const filter = isStandby ? mode.slice(8) : mode;
182
+ if (isStandby) {
183
+ if (role === "mention" && event.participant_id === selfId && senderMatches(filter, senderType, senderId, personParticipantId)) return "trigger";
184
+ return "drop";
185
+ }
186
+ if (role === "mention") return "drop";
187
+ if (role === "message") {
188
+ return senderMatches(filter, senderType, senderId, personParticipantId) ? "trigger" : "content";
189
+ }
190
+ if (role === "ambient") return "content";
191
+ return "drop";
192
+ }
193
+ var StoopsEngagement = class {
194
+ _modes = /* @__PURE__ */ new Map();
195
+ _defaultMode;
196
+ _personParticipantId;
197
+ constructor(defaultMode, personParticipantId) {
198
+ this._defaultMode = defaultMode;
199
+ this._personParticipantId = personParticipantId;
200
+ }
201
+ /** Get the engagement mode for a room. Falls back to the default mode. */
202
+ getMode(roomId) {
203
+ return this._modes.get(roomId) ?? this._defaultMode;
204
+ }
205
+ /** Set the engagement mode for a room. */
206
+ setMode(roomId, mode) {
207
+ this._modes.set(roomId, mode);
208
+ }
209
+ /** Called when a room is disconnected. Removes the room's mode so it doesn't linger. */
210
+ onRoomDisconnected(roomId) {
211
+ this._modes.delete(roomId);
212
+ }
213
+ classify(event, roomId, selfId, senderType, senderId) {
214
+ const mode = this._modes.get(roomId) ?? this._defaultMode;
215
+ return classify(event, mode, selfId, senderType, senderId, this._personParticipantId);
216
+ }
217
+ };
218
+ function classifyEvent(event, mode, selfId, senderType, senderId, personParticipantId) {
219
+ return classify(event, mode, selfId, senderType, senderId, personParticipantId);
220
+ }
221
+
222
+ // src/agent/mcp/runtime.ts
223
+ import { createServer } from "http";
224
+ import { z } from "zod";
225
+
226
+ // src/agent/tool-handlers.ts
227
+ function resolveOrError(resolver, roomName) {
228
+ const conn = resolver.resolve(roomName);
229
+ if (!conn) {
230
+ return {
231
+ error: true,
232
+ result: { content: [{ type: "text", text: `Unknown room "${roomName}".` }] }
233
+ };
234
+ }
235
+ return { error: false, conn };
236
+ }
237
+ function textResult(text) {
238
+ return { content: [{ type: "text", text }] };
239
+ }
240
+ async function formatMsgLine(msg, conn, mkRef) {
241
+ const ts = formatTimestamp(new Date(msg.timestamp));
242
+ const ref = mkRef(msg.id);
243
+ const imageNote = msg.image_url ? ` [[img:${msg.image_url}]]` : "";
244
+ let line = `[${ts}] ${ref} ${msg.sender_name}: ${msg.content}${imageNote}`;
245
+ if (msg.reply_to_id) {
246
+ const target = await conn.dataSource.getMessage(msg.reply_to_id);
247
+ if (target) {
248
+ const targetRef = mkRef(target.id);
249
+ line = `[${ts}] ${ref} ${msg.sender_name} (\u2192 ${targetRef} ${target.sender_name}): ${msg.content}${imageNote}`;
250
+ }
251
+ }
252
+ return line;
253
+ }
254
+ async function buildCatchUpLines(conn, options) {
255
+ const result = await conn.dataSource.getEvents(null, 50, null);
256
+ const chronological = [...result.items].reverse();
257
+ let startIdx = chronological.length;
258
+ for (let i = 0; i < chronological.length; i++) {
259
+ if (!options.isEventSeen?.(chronological[i].id)) {
260
+ startIdx = i;
261
+ break;
262
+ }
263
+ }
264
+ const unseen = chronological.slice(startIdx);
265
+ const lines = [];
266
+ const seenIds = [];
267
+ const mkRef = (id) => `#${options.assignRef?.(id) ?? messageRef(id)}`;
268
+ for (const event of unseen) {
269
+ seenIds.push(event.id);
270
+ const ts = formatTimestamp(new Date(event.timestamp));
271
+ if (event.type === "MessageSent") {
272
+ lines.push(await formatMsgLine(event.message, conn, (id) => mkRef(id)));
273
+ } else if (event.type === "ParticipantJoined") {
274
+ const participant = conn.dataSource.listParticipants().find((p) => p.id === event.participant_id);
275
+ const name = participant?.name ?? event.participant_id;
276
+ lines.push(`[${ts}] + ${name} joined`);
277
+ } else if (event.type === "ParticipantLeft") {
278
+ const name = event.participant?.name ?? event.participant_id;
279
+ lines.push(`[${ts}] - ${name} left`);
280
+ } else if (event.type === "ReactionAdded") {
281
+ const participant = conn.dataSource.listParticipants().find((p) => p.id === event.participant_id);
282
+ const name = participant?.name ?? event.participant_id;
283
+ const target = await conn.dataSource.getMessage(event.message_id);
284
+ const targetRef = target ? ` to ${mkRef(target.id)}` : "";
285
+ lines.push(`[${ts}] ${name} reacted ${event.emoji}${targetRef}`);
286
+ }
287
+ }
288
+ if (seenIds.length > 0) options.markEventsSeen?.(seenIds);
289
+ return lines;
290
+ }
291
+ async function handleCatchUp(resolver, args, options) {
292
+ const r = resolveOrError(resolver, args.room);
293
+ if (r.error) return r.result;
294
+ const lines = await buildCatchUpLines(r.conn, options);
295
+ const out = [`Catching up on [${args.room}]:`];
296
+ if (lines.length > 0) {
297
+ out.push("", ...lines);
298
+ } else {
299
+ out.push("", "(nothing new)");
300
+ }
301
+ return { content: [{ type: "text", text: out.join("\n") }] };
302
+ }
303
+ async function handleSearchByText(resolver, args, options) {
304
+ const r = resolveOrError(resolver, args.room);
305
+ if (r.error) return r.result;
306
+ const { conn } = r;
307
+ const count = args.count ?? 3;
308
+ const mkRef = (id) => `#${options.assignRef?.(id) ?? messageRef(id)}`;
309
+ const searchResult = await conn.dataSource.searchMessages(args.query, 50, args.cursor ?? null);
310
+ const totalVisible = searchResult.items.length;
311
+ if (totalVisible === 0) {
312
+ return textResult(`No messages found in [${args.room}] matching "${args.query}".`);
313
+ }
314
+ const toShow = searchResult.items.slice(0, count);
315
+ const recentResult = await conn.dataSource.getMessages(100, null);
316
+ const recentChron = [...recentResult.items].reverse();
317
+ const msgIdxMap = /* @__PURE__ */ new Map();
318
+ recentChron.forEach((m, i) => msgIdxMap.set(m.id, i));
319
+ const clusters = [];
320
+ let newerCount = null;
321
+ for (const match of [...toShow].reverse()) {
322
+ const cluster = [];
323
+ const matchIdx = msgIdxMap.get(match.id);
324
+ if (matchIdx !== void 0) {
325
+ if (matchIdx > 0) {
326
+ cluster.push(await formatMsgLine(recentChron[matchIdx - 1], conn, mkRef));
327
+ }
328
+ cluster.push(await formatMsgLine(match, conn, mkRef) + " \u2190");
329
+ if (matchIdx < recentChron.length - 1) {
330
+ cluster.push(await formatMsgLine(recentChron[matchIdx + 1], conn, mkRef));
331
+ }
332
+ if (newerCount === null && match.id === toShow[0].id) {
333
+ newerCount = recentChron.length - matchIdx - 1;
334
+ }
335
+ } else {
336
+ cluster.push(await formatMsgLine(match, conn, mkRef) + " \u2190");
337
+ }
338
+ clusters.push(cluster);
339
+ }
340
+ const shownCount = toShow.length;
341
+ const totalNote = searchResult.has_more ? `showing ${shownCount} of 50+` : `${shownCount} of ${totalVisible}`;
342
+ const out = [`Search results in [${args.room}] for "${args.query}" (${totalNote} matches):`, ""];
343
+ for (let i = 0; i < clusters.length; i++) {
344
+ for (const line of clusters[i]) {
345
+ out.push(` ${line}`);
346
+ }
347
+ if (i < clusters.length - 1) out.push("");
348
+ }
349
+ if (newerCount !== null && newerCount > 0) {
350
+ out.push("", `${newerCount} newer message${newerCount === 1 ? "" : "s"} in this room.`);
351
+ }
352
+ if (searchResult.has_more && searchResult.next_cursor) {
353
+ out.push(`(cursor: "${searchResult.next_cursor}" for next ${count} matches)`);
354
+ }
355
+ return textResult(out.join("\n"));
356
+ }
357
+ async function handleSearchByMessage(resolver, args, options) {
358
+ const r = resolveOrError(resolver, args.room);
359
+ if (r.error) return r.result;
360
+ const { conn } = r;
361
+ const direction = args.direction ?? "before";
362
+ const count = args.count ?? 10;
363
+ const mkRef = (id) => `#${options.assignRef?.(id) ?? messageRef(id)}`;
364
+ const rawRef = args.ref.startsWith("#") ? args.ref.slice(1) : args.ref;
365
+ const anchorId = options.resolveRef?.(rawRef) ?? rawRef;
366
+ const anchor = await conn.dataSource.getMessage(anchorId);
367
+ if (!anchor) return textResult(`Message ${args.ref} not found.`);
368
+ const recentResult = await conn.dataSource.getMessages(100, null);
369
+ const recentChron = [...recentResult.items].reverse();
370
+ const anchorIdx = recentChron.findIndex((m) => m.id === anchor.id);
371
+ let displayMessages;
372
+ let newerCount;
373
+ if (direction === "before") {
374
+ const beforeResult = await conn.dataSource.getMessages(count, anchor.id);
375
+ const beforeMessages = [...beforeResult.items].reverse();
376
+ displayMessages = [...beforeMessages, anchor];
377
+ newerCount = anchorIdx >= 0 ? recentChron.length - anchorIdx - 1 : 100;
378
+ } else {
379
+ if (anchorIdx >= 0) {
380
+ const afterMessages = recentChron.slice(anchorIdx + 1, anchorIdx + 1 + count);
381
+ displayMessages = [anchor, ...afterMessages];
382
+ newerCount = recentChron.length - anchorIdx - 1 - afterMessages.length;
383
+ } else {
384
+ displayMessages = [anchor];
385
+ newerCount = 100;
386
+ }
387
+ }
388
+ const anchorRef = mkRef(anchor.id);
389
+ const lines = [`Context in [${args.room}] around ${anchorRef}:`, ""];
390
+ for (const msg of displayMessages) {
391
+ const line = await formatMsgLine(msg, conn, mkRef);
392
+ const isAnchor = msg.id === anchor.id;
393
+ lines.push(` ${line}${isAnchor ? " \u2190" : ""}`);
394
+ }
395
+ if (newerCount > 0) {
396
+ const countLabel = newerCount >= 100 ? "100+" : String(newerCount);
397
+ lines.push("", `${countLabel} newer message${newerCount === 1 ? "" : "s"} in this room.`);
398
+ }
399
+ return textResult(lines.join("\n"));
400
+ }
401
+ async function handleSendMessage(resolver, args, options) {
402
+ const r = resolveOrError(resolver, args.room);
403
+ if (r.error) return r.result;
404
+ const image = args.image_url ? {
405
+ url: args.image_url,
406
+ mimeType: args.image_mime_type ?? "image/*",
407
+ sizeBytes: args.image_size_bytes ?? 0
408
+ } : null;
409
+ let replyToId = args.reply_to_id;
410
+ if (replyToId) {
411
+ const rawRef = replyToId.startsWith("#") ? replyToId.slice(1) : replyToId;
412
+ replyToId = options.resolveRef?.(rawRef) ?? replyToId;
413
+ }
414
+ const message = await r.conn.dataSource.sendMessage(args.content, replyToId, image);
415
+ const ref = options.assignRef?.(message.id) ?? messageRef(message.id);
416
+ return textResult(`Message sent #${ref}.`);
417
+ }
418
+
419
+ // src/agent/mcp/runtime.ts
420
+ function formatJoinResponse(result) {
421
+ const lines = [];
422
+ lines.push(`Joined ${result.roomName} as "${result.agentName}" (${result.authority})`);
423
+ lines.push("");
424
+ if (result.mode) {
425
+ lines.push(`Mode: ${result.mode}`);
426
+ const desc = MODE_DESCRIPTIONS[result.mode];
427
+ if (desc) lines.push(` ${desc}`);
428
+ lines.push(` Change with set_mode.`);
429
+ lines.push("");
430
+ }
431
+ if (result.personName) {
432
+ lines.push(`Person: ${result.personName}`);
433
+ lines.push(` Your person's messages always reach you regardless of mode.`);
434
+ lines.push("");
435
+ }
436
+ if (result.participants && result.participants.length > 0) {
437
+ lines.push("Participants:");
438
+ for (const p of result.participants) {
439
+ lines.push(` ${p.name} (${p.authority})`);
440
+ }
441
+ lines.push("");
442
+ }
443
+ if (result.recentLines && result.recentLines.length > 0) {
444
+ lines.push("Recent:");
445
+ for (const line of result.recentLines) {
446
+ lines.push(` ${line}`);
447
+ }
448
+ lines.push("");
449
+ lines.push(`${result.recentLines.length} message${result.recentLines.length === 1 ? "" : "s"} shown. Use catch_up("${result.roomName}") for more.`);
450
+ }
451
+ return lines.join("\n");
452
+ }
453
+ function registerTools(server, opts) {
454
+ const { resolver, toolOptions } = opts;
455
+ server.tool(
456
+ "stoops__catch_up",
457
+ "List your rooms and status. Call with no arguments to see connected rooms. With a room name, returns recent activity you haven't seen.",
458
+ {
459
+ room: z.string().optional().describe("Room name. Omit to list all connected rooms.")
460
+ },
461
+ { readOnlyHint: true },
462
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
463
+ async ({ room }) => {
464
+ if (!room) {
465
+ const rooms = resolver.listAll();
466
+ if (rooms.length === 0) {
467
+ return textResult("Not connected to any rooms.");
468
+ }
469
+ const lines = ["Connected rooms:", ""];
470
+ for (const r of rooms) {
471
+ const idPart = r.identifier ? ` [${r.identifier}]` : "";
472
+ lines.push(` ${r.name}${idPart} \u2014 ${r.mode} (${r.participantCount} participants)`);
473
+ if (r.lastMessage) lines.push(` Last: ${r.lastMessage}`);
474
+ }
475
+ return textResult(lines.join("\n"));
476
+ }
477
+ return handleCatchUp(resolver, { room }, toolOptions);
478
+ }
479
+ );
480
+ server.tool(
481
+ "stoops__search_by_text",
482
+ "Search chat history by keyword.",
483
+ {
484
+ room: z.string().describe("Room name"),
485
+ query: z.string().describe("Keyword or phrase to search for"),
486
+ count: z.number().int().min(1).max(10).default(3).optional().describe("Number of matches (default 3)"),
487
+ cursor: z.string().optional().describe("Pagination cursor")
488
+ },
489
+ { readOnlyHint: true },
490
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
491
+ async (args) => handleSearchByText(resolver, args, toolOptions)
492
+ );
493
+ server.tool(
494
+ "stoops__search_by_message",
495
+ "Show messages around a known message ref.",
496
+ {
497
+ room: z.string().describe("Room name"),
498
+ ref: z.string().describe("Message ref (e.g. #3847)"),
499
+ direction: z.enum(["before", "after"]).default("before").optional().describe("'before' to scroll back, 'after' to scroll forward"),
500
+ count: z.number().int().min(1).max(50).default(10).optional().describe("Number of messages (default 10)")
501
+ },
502
+ { readOnlyHint: true },
503
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
504
+ async (args) => handleSearchByMessage(resolver, args, toolOptions)
505
+ );
506
+ server.tool(
507
+ "stoops__send_message",
508
+ "Send a message to a room.",
509
+ {
510
+ room: z.string().describe("Room name"),
511
+ content: z.string().describe("Message content. @name will notify that participant \u2014 use sparingly."),
512
+ reply_to_id: z.string().optional().describe("Message ref to reply to (e.g. #3847).")
513
+ },
514
+ { readOnlyHint: false, destructiveHint: false },
515
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
516
+ async (args) => handleSendMessage(resolver, args, toolOptions)
517
+ );
518
+ server.tool(
519
+ "stoops__set_mode",
520
+ "Change your engagement mode. Controls which messages are pushed to you: everyone \u2014 all messages, people \u2014 human messages only, agents \u2014 agent messages only, me \u2014 your person only. Prefix with standby- for @mentions only.",
521
+ {
522
+ room: z.string().describe("Room name"),
523
+ mode: z.string().describe("Engagement mode")
524
+ },
525
+ { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
526
+ async ({ room, mode }) => {
527
+ if (!opts.onSetMode) return textResult("Mode changes not supported.");
528
+ if (!isValidMode(mode)) {
529
+ return textResult(`Invalid mode "${mode}". Valid modes: everyone, people, agents, me, standby-everyone, standby-people, standby-agents, standby-me.`);
530
+ }
531
+ const result = await opts.onSetMode(room, mode);
532
+ return result.success ? textResult(`Mode set to ${mode} for [${room}].`) : textResult(result.error ?? "Failed to set mode.");
533
+ }
534
+ );
535
+ server.tool(
536
+ "stoops__join_room",
537
+ "Join a room. Returns your identity, participants, mode, and recent activity.",
538
+ {
539
+ url: z.string().describe("Share URL to join"),
540
+ alias: z.string().optional().describe("Local alias for the room (if name collides)")
541
+ },
542
+ { readOnlyHint: false, destructiveHint: false },
543
+ async ({ url, alias }) => {
544
+ if (!opts.onJoinRoom) return textResult("Joining rooms not supported.");
545
+ const result = await opts.onJoinRoom(url, alias);
546
+ if (!result.success) return textResult(result.error ?? "Failed to join room.");
547
+ if (result.roomName && result.agentName) {
548
+ return textResult(formatJoinResponse(result));
549
+ }
550
+ return textResult(`Joined room successfully.`);
551
+ }
552
+ );
553
+ server.tool(
554
+ "stoops__leave_room",
555
+ "Leave a room. Events stop flowing from it.",
556
+ {
557
+ room: z.string().describe("Room name to leave")
558
+ },
559
+ { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
560
+ async ({ room }) => {
561
+ if (!opts.onLeaveRoom) return textResult("Leaving rooms not supported.");
562
+ const result = await opts.onLeaveRoom(room);
563
+ return result.success ? textResult(`Left [${room}].`) : textResult(result.error ?? "Failed to leave room.");
564
+ }
565
+ );
566
+ if (opts.admin) {
567
+ server.tool(
568
+ "stoops__admin__set_mode_for",
569
+ "Admin: set engagement mode for another participant.",
570
+ {
571
+ room: z.string().describe("Room name"),
572
+ participant: z.string().describe("Participant name"),
573
+ mode: z.string().describe("Engagement mode to set")
574
+ },
575
+ { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
576
+ async ({ room, participant, mode }) => {
577
+ if (!opts.onAdminSetModeFor) return textResult("Admin mode changes not supported.");
578
+ if (!isValidMode(mode)) {
579
+ return textResult(`Invalid mode "${mode}". Valid modes: everyone, people, agents, me, standby-everyone, standby-people, standby-agents, standby-me.`);
580
+ }
581
+ const result = await opts.onAdminSetModeFor(room, participant, mode);
582
+ return result.success ? textResult(`Set ${participant}'s mode to ${mode} in [${room}].`) : textResult(result.error ?? "Failed to set mode.");
583
+ }
584
+ );
585
+ server.tool(
586
+ "stoops__admin__kick",
587
+ "Admin: kick a participant from a room.",
588
+ {
589
+ room: z.string().describe("Room name"),
590
+ participant: z.string().describe("Participant name to kick")
591
+ },
592
+ { readOnlyHint: false, destructiveHint: true },
593
+ async ({ room, participant }) => {
594
+ if (!opts.onAdminKick) return textResult("Admin kick not supported.");
595
+ const result = await opts.onAdminKick(room, participant);
596
+ return result.success ? textResult(`Kicked ${participant} from [${room}].`) : textResult(result.error ?? "Failed to kick participant.");
597
+ }
598
+ );
599
+ server.tool(
600
+ "stoops__admin__mute",
601
+ "Admin: make a participant read-only (demote to observer).",
602
+ {
603
+ room: z.string().describe("Room name"),
604
+ participant: z.string().describe("Participant name to mute")
605
+ },
606
+ { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
607
+ async ({ room, participant }) => {
608
+ if (!opts.onAdminMute) return textResult("Admin mute not supported.");
609
+ const result = await opts.onAdminMute(room, participant);
610
+ return result.success ? textResult(`Muted ${participant} in [${room}] (observer).`) : textResult(result.error ?? "Failed to mute participant.");
611
+ }
612
+ );
613
+ server.tool(
614
+ "stoops__admin__unmute",
615
+ "Admin: restore a muted participant (promote to participant).",
616
+ {
617
+ room: z.string().describe("Room name"),
618
+ participant: z.string().describe("Participant name to unmute")
619
+ },
620
+ { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
621
+ async ({ room, participant }) => {
622
+ if (!opts.onAdminUnmute) return textResult("Admin unmute not supported.");
623
+ const result = await opts.onAdminUnmute(room, participant);
624
+ return result.success ? textResult(`Unmuted ${participant} in [${room}] (participant).`) : textResult(result.error ?? "Failed to unmute participant.");
625
+ }
626
+ );
627
+ }
628
+ }
629
+ async function createRuntimeMcpServer(opts) {
630
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
631
+ const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
632
+ const httpServer = createServer(async (req, res) => {
633
+ if (req.url !== "/mcp") {
634
+ res.writeHead(404).end();
635
+ return;
636
+ }
637
+ const reqServer = new McpServer({ name: "stoops_runtime", version: "1.0.0" });
638
+ registerTools(reqServer, opts);
639
+ const transport = new StreamableHTTPServerTransport({
640
+ sessionIdGenerator: void 0
641
+ });
642
+ await reqServer.connect(transport);
643
+ let body;
644
+ if (req.method === "POST") {
645
+ const chunks = [];
646
+ for await (const chunk of req) chunks.push(chunk);
647
+ try {
648
+ body = JSON.parse(Buffer.concat(chunks).toString());
649
+ } catch {
650
+ body = void 0;
651
+ }
652
+ }
653
+ await transport.handleRequest(req, res, body);
654
+ });
655
+ const port = await new Promise((resolve, reject) => {
656
+ httpServer.listen(0, "127.0.0.1", () => {
657
+ const addr = httpServer.address();
658
+ if (addr && typeof addr === "object") resolve(addr.port);
659
+ else reject(new Error("Could not determine server port"));
660
+ });
661
+ httpServer.once("error", reject);
662
+ });
663
+ const url = `http://127.0.0.1:${port}/mcp`;
664
+ let stopPromise = null;
665
+ const stop = () => {
666
+ if (!stopPromise) {
667
+ stopPromise = new Promise(
668
+ (resolve, reject) => httpServer.close((err) => err ? reject(err) : resolve())
669
+ );
670
+ }
671
+ return stopPromise;
672
+ };
673
+ return { url, stop };
674
+ }
675
+
676
+ export {
677
+ getSystemPreamble,
678
+ messageRef,
679
+ participantLabel,
680
+ formatTimestamp,
681
+ contentPartsToString,
682
+ formatEvent,
683
+ StoopsEngagement,
684
+ classifyEvent,
685
+ buildCatchUpLines,
686
+ handleCatchUp,
687
+ handleSearchByText,
688
+ handleSearchByMessage,
689
+ handleSendMessage,
690
+ createRuntimeMcpServer
691
+ };
692
+ //# sourceMappingURL=chunk-BLGV3QN4.js.map