stoops 0.3.1 → 0.3.2

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.
package/README.md CHANGED
@@ -14,9 +14,11 @@ Start a server, share a link, anyone joins from their machine with their own age
14
14
 
15
15
  https://github.com/user-attachments/assets/b9db9369-352e-4ff8-aea3-6497f7706879
16
16
 
17
- ## Try it with your agent
17
+ ## Try it with your agents
18
18
 
19
- <img width="487" height="255" alt="Screenshot 2026-03-04 at 7 46 07 PM" src="https://github.com/user-attachments/assets/3f593f1c-9b9f-471f-a3cc-890186c4e1d5" />
19
+ Claude and Codex collabing on a feature together!
20
+
21
+ <img width="1869" height="994" alt="Screenshot 2026-03-05 at 2 27 02 AM" src="https://github.com/user-attachments/assets/e724f6d4-ebaf-4bef-99d1-49297c81cacc" />
20
22
 
21
23
  ### Quick start (you + an agent)
22
24
 
@@ -33,8 +35,8 @@ The server starts and the chat UI opens. You'll see share links printed — copy
33
35
  **Terminal 2 — launch an agent:**
34
36
 
35
37
  ```bash
36
- npx stoops run claude --name Ferris # Claude Code
37
- npx stoops run codex --name Gopher # OpenAI Codex
38
+ npx stoops run claude --name MyClaude # Claude Code
39
+ npx stoops run codex --name MyCodex # OpenAI Codex
38
40
  ```
39
41
 
40
42
  This opens the agent inside a tmux session with stoops MCP tools attached. Tell the agent:
@@ -129,6 +131,15 @@ npx stoops run claude [--name <name>] [--admin] [-- <args>] #
129
131
  npx stoops run codex [--name <name>] [--admin] [-- <args>] # connect Codex as an agent
130
132
  ```
131
133
 
134
+ Room state is automatically saved to a temp file on every message. To pick a specific file:
135
+
136
+ ```bash
137
+ npx stoops --room lobby --save lobby.json # save to a specific file
138
+ npx stoops --room lobby --load lobby.json # load previous session + continue saving
139
+ ```
140
+
141
+ Load a file and everyone who joins sees the full history.
142
+
132
143
  ### TUI slash commands
133
144
 
134
145
  | Command | Who | What it does |
@@ -1,6 +1,6 @@
1
- import { a as ContentPart, R as RoomResolver, b as RoomConnection, c as RoomDataSource, T as ToolHandlerOptions } from '../types-Co2KKpkh.js';
2
- export { A as AgentIdentity, C as ClaudeSessionOptions, I as ILLMSession, d as LLMQueryStats, e as LLMSessionOptions, L as LangGraphSessionOptions, f as LocalRoomDataSource, P as ProcessorBridge, Q as QueryTurn, S as SessionCallbacks } from '../types-Co2KKpkh.js';
3
- import { R as RoomEvent, P as Participant, c as ParticipantType, C as Channel, b as Room, M as Message, a as PaginatedResult, E as EventCategory } from '../index-DGncuUqB.js';
1
+ import { a as ContentPart, R as RoomResolver, b as RoomConnection, c as RoomDataSource, T as ToolHandlerOptions } from '../types-B9xf8w53.js';
2
+ export { A as AgentIdentity, C as ClaudeSessionOptions, I as ILLMSession, d as LLMQueryStats, e as LLMSessionOptions, L as LangGraphSessionOptions, f as LocalRoomDataSource, P as ProcessorBridge, Q as QueryTurn, S as SessionCallbacks } from '../types-B9xf8w53.js';
3
+ import { R as RoomEvent, P as Participant, c as ParticipantType, C as Channel, b as Room, M as Message, a as PaginatedResult, E as EventCategory } from '../index-DwVKKxqK.js';
4
4
  import 'zod';
5
5
 
6
6
  /** Event formatting and mode descriptions for stoops agents. */
@@ -5,6 +5,8 @@ import {
5
5
  } from "./chunk-PKFZHCQF.js";
6
6
 
7
7
  // src/core/storage.ts
8
+ import { writeFile, readFile } from "fs/promises";
9
+ import { resolve } from "path";
8
10
  function paginate(items, limit, cursor, key) {
9
11
  let subset;
10
12
  if (cursor != null) {
@@ -69,6 +71,58 @@ var InMemoryStorage = class {
69
71
  return paginateByIndex(events, limit, cursor);
70
72
  }
71
73
  };
74
+ var FileBackedStorage = class _FileBackedStorage extends InMemoryStorage {
75
+ _filePath;
76
+ constructor(filePath) {
77
+ super();
78
+ this._filePath = resolve(filePath);
79
+ }
80
+ async addMessage(message) {
81
+ const result = await super.addMessage(message);
82
+ await this._flush();
83
+ return result;
84
+ }
85
+ async addEvent(event) {
86
+ await super.addEvent(event);
87
+ await this._flush();
88
+ }
89
+ async _flush() {
90
+ const data = {};
91
+ for (const [roomId, messages] of this._messages) {
92
+ if (!data[roomId]) data[roomId] = { messages: [], events: [] };
93
+ data[roomId].messages = messages;
94
+ }
95
+ for (const [roomId, events] of this._events) {
96
+ if (!data[roomId]) data[roomId] = { messages: [], events: [] };
97
+ data[roomId].events = events;
98
+ }
99
+ await writeFile(this._filePath, JSON.stringify(data, null, 2));
100
+ }
101
+ /** Load an existing file and return a FileBackedStorage that continues saving to it. */
102
+ static async load(filePath) {
103
+ const storage = new _FileBackedStorage(filePath);
104
+ const raw = await readFile(storage._filePath, "utf-8");
105
+ const data = JSON.parse(raw);
106
+ for (const [roomId, { messages, events }] of Object.entries(data)) {
107
+ storage._messages.set(
108
+ roomId,
109
+ messages.map((m) => ({ ...m, timestamp: new Date(m.timestamp) }))
110
+ );
111
+ storage._events.set(
112
+ roomId,
113
+ events.map((e) => rehydrateEvent(e))
114
+ );
115
+ }
116
+ return storage;
117
+ }
118
+ };
119
+ function rehydrateEvent(e) {
120
+ const event = { ...e, timestamp: new Date(e.timestamp) };
121
+ if ("message" in event && event.message) {
122
+ event.message = { ...event.message, timestamp: new Date(event.message.timestamp) };
123
+ }
124
+ return event;
125
+ }
72
126
 
73
127
  // src/core/channel.ts
74
128
  var Channel = class {
@@ -190,21 +244,21 @@ var Channel = class {
190
244
  if (this._disconnected) {
191
245
  return Promise.resolve(null);
192
246
  }
193
- return new Promise((resolve) => {
247
+ return new Promise((resolve2) => {
194
248
  let settled = false;
195
249
  const waiter = {
196
250
  resolve: (event) => {
197
251
  if (!settled) {
198
252
  settled = true;
199
253
  clearTimeout(timer);
200
- resolve(event);
254
+ resolve2(event);
201
255
  }
202
256
  },
203
257
  reject: () => {
204
258
  if (!settled) {
205
259
  settled = true;
206
260
  clearTimeout(timer);
207
- resolve(null);
261
+ resolve2(null);
208
262
  }
209
263
  }
210
264
  };
@@ -214,7 +268,7 @@ var Channel = class {
214
268
  settled = true;
215
269
  const idx = this._waiters.indexOf(waiter);
216
270
  if (idx !== -1) this._waiters.splice(idx, 1);
217
- resolve(null);
271
+ resolve2(null);
218
272
  }
219
273
  }, timeoutMs);
220
274
  });
@@ -245,9 +299,9 @@ var Channel = class {
245
299
  done: true
246
300
  });
247
301
  }
248
- return new Promise((resolve, reject) => {
302
+ return new Promise((resolve2, reject) => {
249
303
  this._waiters.push({
250
- resolve: (event) => resolve({ value: event, done: false }),
304
+ resolve: (event) => resolve2({ value: event, done: false }),
251
305
  reject
252
306
  });
253
307
  });
@@ -680,9 +734,10 @@ function randomName() {
680
734
 
681
735
  export {
682
736
  InMemoryStorage,
737
+ FileBackedStorage,
683
738
  Channel,
684
739
  Room,
685
740
  randomRoomName,
686
741
  randomName
687
742
  };
688
- //# sourceMappingURL=chunk-TN56PBF3.js.map
743
+ //# sourceMappingURL=chunk-XEKY3KEU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/storage.ts","../src/core/channel.ts","../src/core/room.ts","../src/core/names.ts"],"sourcesContent":["/**\n * Storage protocol and reference implementations for stoops rooms.\n *\n * # Implementing StorageProtocol\n *\n * Provide your own implementation to persist messages and events to a real\n * database. Pass it to `new Room(roomId, myStorage)`.\n *\n * Pagination contract (applies to all paginated methods):\n * - Results are returned newest-first.\n * - `cursor` is the ID of the last item on the previous page (exclusive).\n * Pass `null` to start from the most recent.\n * - `next_cursor` in the result is the cursor to pass for the next (older) page.\n * - `has_more` is true if there are older items beyond the current page.\n *\n * @example\n * // Minimal Postgres implementation sketch:\n * class PostgresStorage implements StorageProtocol {\n * async addMessage(message) {\n * await db.query(\"INSERT INTO messages ...\", [message]);\n * return message;\n * }\n * async getMessage(room_id, message_id) {\n * return db.query(\"SELECT * FROM messages WHERE id = $1\", [message_id]);\n * }\n * async getMessages(room_id, limit = 30, cursor = null) {\n * // Fetch `limit` messages before `cursor`, newest-first\n * const rows = await db.query(\"...\");\n * return { items: rows, next_cursor: ..., has_more: ... };\n * }\n * async searchMessages(room_id, query, limit = 10, cursor = null) {\n * // Full-text search, newest-first\n * }\n * async addEvent(event) {\n * await db.query(\"INSERT INTO events ...\", [event]);\n * }\n * async getEvents(room_id, category = null, limit = 50, cursor = null) {\n * // Optional category filter, newest-first\n * }\n * }\n */\n\nimport { writeFile, readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\n\nimport type { RoomEvent } from \"./events.js\";\nimport type { EventCategory, Message, PaginatedResult } from \"./types.js\";\n\n// ── StorageProtocol ───────────────────────────────────────────────────────────\n\n/**\n * Persistence interface for a room's messages and events.\n *\n * Implement this to back rooms with a real database. The reference\n * `InMemoryStorage` is suitable for testing and single-process local use.\n *\n * All methods operate on a single `room_id` — one storage instance is shared\n * across all rooms (the `room_id` partitions the data).\n */\nexport interface StorageProtocol {\n /**\n * Persist a message and return it (with any server-assigned fields set).\n * Called automatically by `Channel.sendMessage()`.\n */\n addMessage(message: Message): Promise<Message>;\n\n /**\n * Look up a single message by ID. Returns null if not found.\n * Used by agents when resolving reply context and message refs.\n */\n getMessage(room_id: string, message_id: string): Promise<Message | null>;\n\n /**\n * Paginate messages for a room, newest-first.\n *\n * `cursor` — the `id` of the last message on the previous page (exclusive).\n * Pass null to start from the most recent message.\n */\n getMessages(\n room_id: string,\n limit?: number,\n cursor?: string | null,\n ): Promise<PaginatedResult<Message>>;\n\n /**\n * Full-text search across message content, newest-first.\n *\n * `query` — keyword or phrase to search for (case-insensitive).\n * `cursor` — pagination cursor (same semantics as `getMessages`).\n */\n searchMessages(\n room_id: string,\n query: string,\n limit?: number,\n cursor?: string | null,\n ): Promise<PaginatedResult<Message>>;\n\n /**\n * Persist a room event. Called for every event that passes through the room.\n * Events are append-only — never updated or deleted.\n */\n addEvent(event: RoomEvent): Promise<void>;\n\n /**\n * Paginate events for a room, newest-first.\n *\n * `category` — optional filter (e.g. EventCategory.MESSAGE). Pass null for all.\n * `cursor` — pagination cursor (index-based for events).\n */\n getEvents(\n room_id: string,\n category?: EventCategory | null,\n limit?: number,\n cursor?: string | null,\n ): Promise<PaginatedResult<RoomEvent>>;\n}\n\n// ── Pagination helpers (used by InMemoryStorage) ──────────────────────────────\n\n/**\n * Paginate an array by item ID cursor, returning results newest-first.\n * Items are assumed to be stored oldest-first (append order).\n *\n * @internal\n */\nexport function paginate<T>(\n items: T[],\n limit: number,\n cursor: string | null | undefined,\n key: (item: T) => string,\n): PaginatedResult<T> {\n let subset: T[];\n\n if (cursor != null) {\n const cursorIdx = items.findIndex((item) => key(item) === cursor);\n if (cursorIdx === -1) {\n return { items: [], next_cursor: null, has_more: false };\n }\n subset = items.slice(0, cursorIdx);\n } else {\n subset = items;\n }\n\n const page =\n limit < subset.length ? subset.slice(-limit) : subset.slice();\n page.reverse();\n const has_more = subset.length > limit;\n const next_cursor = has_more && page.length > 0 ? key(page[page.length - 1]) : null;\n\n return { items: page, next_cursor, has_more };\n}\n\n/**\n * Paginate an array by positional index cursor, returning results newest-first.\n * Used for events, which don't have stable IDs suitable for ID-based cursors.\n *\n * @internal\n */\nexport function paginateByIndex<T>(\n items: T[],\n limit: number,\n cursor: string | null | undefined,\n): PaginatedResult<T> {\n const parsedCursor = cursor != null ? parseInt(cursor, 10) : items.length;\n const endIdx = Number.isNaN(parsedCursor) ? items.length : parsedCursor;\n const startIdx = Math.max(0, endIdx - limit);\n const page = items.slice(startIdx, endIdx).reverse();\n const has_more = startIdx > 0;\n const next_cursor = has_more ? String(startIdx) : null;\n\n return { items: page, next_cursor, has_more };\n}\n\n// ── InMemoryStorage ───────────────────────────────────────────────────────────\n\n/**\n * Reference in-memory implementation of `StorageProtocol`.\n *\n * Suitable for tests, development, and single-process local use. All data is\n * lost on process restart — not for production.\n *\n * One instance can serve multiple rooms (data is partitioned by `room_id`).\n */\nexport class InMemoryStorage implements StorageProtocol {\n protected _messages = new Map<string, Message[]>();\n protected _events = new Map<string, RoomEvent[]>();\n\n async addMessage(message: Message): Promise<Message> {\n const list = this._messages.get(message.room_id) ?? [];\n list.push(message);\n this._messages.set(message.room_id, list);\n return message;\n }\n\n async getMessage(room_id: string, message_id: string): Promise<Message | null> {\n const list = this._messages.get(room_id) ?? [];\n return list.find((m) => m.id === message_id) ?? null;\n }\n\n async getMessages(\n room_id: string,\n limit = 30,\n cursor: string | null = null,\n ): Promise<PaginatedResult<Message>> {\n const messages = this._messages.get(room_id) ?? [];\n return paginate(messages, limit, cursor, (m) => m.id);\n }\n\n async searchMessages(\n room_id: string,\n query: string,\n limit = 10,\n cursor: string | null = null,\n ): Promise<PaginatedResult<Message>> {\n const messages = this._messages.get(room_id) ?? [];\n const q = query.toLowerCase();\n const filtered = messages.filter((m) =>\n m.content.toLowerCase().includes(q),\n );\n return paginate(filtered, limit, cursor, (m) => m.id);\n }\n\n async addEvent(event: RoomEvent): Promise<void> {\n const list = this._events.get(event.room_id) ?? [];\n list.push(event);\n this._events.set(event.room_id, list);\n }\n\n async getEvents(\n room_id: string,\n category: EventCategory | null = null,\n limit = 50,\n cursor: string | null = null,\n ): Promise<PaginatedResult<RoomEvent>> {\n let events = this._events.get(room_id) ?? [];\n if (category != null) {\n events = events.filter((e) => e.category === category);\n }\n return paginateByIndex(events, limit, cursor);\n }\n}\n\n// ── FileBackedStorage ─────────────────────────────────────────────────────────\n\n/**\n * In-memory storage that persists to a JSON file on every write.\n *\n * Use `FileBackedStorage.load(path)` to restore from an existing file,\n * or `new FileBackedStorage(path)` to start fresh and save to that path.\n */\nexport class FileBackedStorage extends InMemoryStorage {\n private _filePath: string;\n\n constructor(filePath: string) {\n super();\n this._filePath = resolve(filePath);\n }\n\n async addMessage(message: Message): Promise<Message> {\n const result = await super.addMessage(message);\n await this._flush();\n return result;\n }\n\n async addEvent(event: RoomEvent): Promise<void> {\n await super.addEvent(event);\n await this._flush();\n }\n\n private async _flush(): Promise<void> {\n const data: Record<string, { messages: Message[]; events: RoomEvent[] }> = {};\n for (const [roomId, messages] of this._messages) {\n if (!data[roomId]) data[roomId] = { messages: [], events: [] };\n data[roomId].messages = messages;\n }\n for (const [roomId, events] of this._events) {\n if (!data[roomId]) data[roomId] = { messages: [], events: [] };\n data[roomId].events = events;\n }\n await writeFile(this._filePath, JSON.stringify(data, null, 2));\n }\n\n /** Load an existing file and return a FileBackedStorage that continues saving to it. */\n static async load(filePath: string): Promise<FileBackedStorage> {\n const storage = new FileBackedStorage(filePath);\n const raw = await readFile(storage._filePath, \"utf-8\");\n const data = JSON.parse(raw) as Record<string, { messages: Message[]; events: RoomEvent[] }>;\n\n for (const [roomId, { messages, events }] of Object.entries(data)) {\n storage._messages.set(\n roomId,\n messages.map((m) => ({ ...m, timestamp: new Date(m.timestamp) })),\n );\n storage._events.set(\n roomId,\n events.map((e) => rehydrateEvent(e)),\n );\n }\n\n return storage;\n }\n}\n\n/** Rehydrate Date fields that became ISO strings during JSON serialization. */\nfunction rehydrateEvent(e: RoomEvent): RoomEvent {\n const event = { ...e, timestamp: new Date(e.timestamp) } as RoomEvent;\n // Events that embed a full Message need their nested timestamp rehydrated too\n if (\"message\" in event && event.message) {\n event.message = { ...event.message, timestamp: new Date(event.message.timestamp) };\n }\n return event;\n}\n","/**\n * Channel — a participant's bidirectional connection to a room.\n *\n * Created by `Room.connect()`. Never instantiated directly.\n *\n * # Sending\n * - `sendMessage()` — persist and broadcast a chat message\n * - `emit()` — push non-message events (tool use, mode changes, etc.)\n *\n * # Receiving\n * Channels are async-iterable — use `for await (const event of channel)` to\n * consume events. Only events in the channel's `subscriptions` set are\n * delivered. Alternatively, use `receive(timeoutMs)` for polling with a\n * timeout (used by EventMultiplexer).\n *\n * # Lifecycle\n * - `updateSubscriptions()` — change which EventCategories are delivered\n * - `disconnect(silent?)` — leave the room; pass `true` to suppress the\n * ParticipantLeft broadcast\n */\n\nimport type { RoomEvent } from \"./events.js\";\nimport type { EventCategory, Message } from \"./types.js\";\nimport { MessageSchema } from \"./types.js\";\nimport type { Room } from \"./room.js\";\n\ninterface Waiter {\n resolve: (event: RoomEvent) => void;\n reject: (err: Error) => void;\n}\n\nexport class Channel {\n readonly participantId: string;\n readonly participantName: string;\n subscriptions: Set<EventCategory>;\n\n private _room: Room;\n private _queue: RoomEvent[] = [];\n private _waiters: Waiter[] = [];\n private _disconnected = false;\n\n constructor(\n room: Room,\n participantId: string,\n participantName: string,\n subscriptions: Set<EventCategory>,\n ) {\n this._room = room;\n this.participantId = participantId;\n this.participantName = participantName;\n this.subscriptions = subscriptions;\n }\n\n get roomId(): string {\n return this._room.roomId;\n }\n\n /**\n * Send a chat message from this participant.\n *\n * Persists the message to storage, broadcasts a `MessageSentEvent` to all\n * participants (including the sender), and fires `MentionedEvent` for any\n * `@name` or `@identifier` patterns found in the content.\n *\n * @param content — message text (may be empty if image is provided)\n * @param replyToId — ID of the message being replied to (optional)\n * @param image — optional image attachment\n */\n async sendMessage(\n content: string,\n replyToId?: string | null,\n image?: {\n url: string;\n mimeType: string;\n sizeBytes: number;\n } | null,\n ): Promise<Message> {\n if (this._disconnected) {\n throw new Error(\"Channel is disconnected\");\n }\n const message = MessageSchema.parse({\n room_id: this._room.roomId,\n sender_id: this.participantId,\n sender_name: this.participantName,\n content,\n reply_to_id: replyToId ?? null,\n image_url: image?.url ?? null,\n image_mime_type: image?.mimeType ?? null,\n image_size_bytes: image?.sizeBytes ?? null,\n });\n await this._room._handleMessage(message);\n return message;\n }\n\n /**\n * Emit a non-message activity event to the room.\n *\n * Use this for platform events: tool use indicators, mode changes, compaction\n * notices, etc. The event is persisted and broadcast to all subscribed\n * participants.\n */\n async emit(event: RoomEvent): Promise<void> {\n if (this._disconnected) {\n throw new Error(\"Channel is disconnected\");\n }\n await this._room._handleEvent(event);\n }\n\n /**\n * Change which event categories this channel receives.\n * Takes effect immediately — buffered events from unsubscribed categories\n * are not retroactively removed.\n */\n updateSubscriptions(categories: Set<EventCategory>): void {\n this.subscriptions = categories;\n }\n\n /**\n * Leave the room.\n *\n * @param silent — if true, suppresses the `ParticipantLeft` broadcast.\n * Agents disconnect silently to avoid chat noise.\n */\n async disconnect(silent = false): Promise<void> {\n if (!this._disconnected) {\n this._disconnected = true;\n // Wake pending waiters so async iterators exit cleanly\n const waiters = this._waiters;\n this._waiters = [];\n for (const w of waiters) {\n w.reject(new Error(\"Channel disconnected\"));\n }\n await this._room._disconnectChannel(this, silent);\n }\n }\n\n /** @internal Called by Room to mark this channel as disconnected without removing from room maps. */\n _markDisconnected(): void {\n if (!this._disconnected) {\n this._disconnected = true;\n const waiters = this._waiters;\n this._waiters = [];\n for (const w of waiters) {\n w.reject(new Error(\"Channel disconnected\"));\n }\n }\n }\n\n /** @internal Called by Room to deliver an incoming event. Filters by subscription. */\n _deliver(event: RoomEvent): void {\n if (this._disconnected) return;\n if (!this.subscriptions.has(event.category)) return;\n\n if (this._waiters.length > 0) {\n const waiter = this._waiters.shift()!;\n waiter.resolve(event);\n } else {\n this._queue.push(event);\n }\n }\n\n /**\n * Receive the next event, waiting up to `timeoutMs`.\n *\n * Returns null if no event arrives within the timeout. Drains buffered events\n * before waiting. Used by `EventMultiplexer` to fan-in events from multiple\n * rooms into a single stream.\n */\n receive(timeoutMs: number): Promise<RoomEvent | null> {\n if (this._queue.length > 0) {\n return Promise.resolve(this._queue.shift()!);\n }\n if (this._disconnected) {\n return Promise.resolve(null);\n }\n\n return new Promise<RoomEvent | null>((resolve) => {\n let settled = false;\n const waiter: Waiter = {\n resolve: (event) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n resolve(event);\n }\n },\n reject: () => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n resolve(null);\n }\n },\n };\n this._waiters.push(waiter);\n\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n const idx = this._waiters.indexOf(waiter);\n if (idx !== -1) this._waiters.splice(idx, 1);\n resolve(null);\n }\n }, timeoutMs);\n });\n }\n\n /**\n * Async iterator — yields events as they arrive.\n *\n * Used by `EventMultiplexer` to fan-in all room channels into a single stream.\n * The iterator completes when the channel is disconnected.\n *\n * @example\n * for await (const event of channel) {\n * console.log(event.type);\n * }\n */\n [Symbol.asyncIterator](): AsyncIterator<RoomEvent> {\n return {\n next: (): Promise<IteratorResult<RoomEvent>> => {\n if (this._queue.length > 0) {\n return Promise.resolve({\n value: this._queue.shift()!,\n done: false,\n });\n }\n\n if (this._disconnected) {\n return Promise.resolve({\n value: undefined as unknown as RoomEvent,\n done: true,\n });\n }\n\n return new Promise<IteratorResult<RoomEvent>>((resolve, reject) => {\n this._waiters.push({\n resolve: (event) => resolve({ value: event, done: false }),\n reject,\n });\n });\n },\n };\n }\n}\n","/**\n * Room — a shared chat space where humans and agents are all just participants.\n *\n * Transport-agnostic: no WebSockets, no HTTP. The caller owns the transport\n * and passes messages/events in via channels. This means the same Room works\n * identically in a CLI, a web server, or a test.\n *\n * # Connecting\n * Participants connect via `room.connect()`, which returns a `Channel`. The\n * channel is their bidirectional connection: they send messages and receive\n * events through it.\n *\n * # Observing\n * Call `room.observe()` to get a read-only-style channel that receives every\n * event in the room — including targeted @mention events directed at other\n * participants. Observers are NOT participants: they don't appear in\n * `listParticipants()` and don't trigger join/leave events.\n *\n * # @mention detection\n * When a message is sent, the Room scans its content for `@token` patterns and\n * fires a `MentionedEvent` for any participant whose `identifier` or display\n * `name` matches the token (case-insensitive). The mention event is delivered\n * to the mentioned participant AND to all observers.\n *\n * @example\n * const storage = new InMemoryStorage();\n * const room = new Room(\"room-1\", storage);\n *\n * const aliceChannel = await room.connect(\"alice-id\", \"Alice\");\n * const agentChannel = await room.connect(\"agent-id\", \"Agent\", \"agent\", \"my-agent\");\n * const observer = room.observe();\n *\n * await aliceChannel.sendMessage(\"hey @my-agent what do you think?\");\n * // → MessageSentEvent broadcast to all participants + observer\n * // → MentionedEvent delivered to agentChannel + observer\n */\n\nimport { Channel } from \"./channel.js\";\nimport { createEvent } from \"./events.js\";\nimport type {\n MentionedEvent,\n MessageSentEvent,\n ParticipantJoinedEvent,\n ParticipantLeftEvent,\n RoomEvent,\n} from \"./events.js\";\nimport { InMemoryStorage, type StorageProtocol } from \"./storage.js\";\nimport { EventCategory, type AuthorityLevel, type Message, type PaginatedResult, type Participant, type ParticipantType } from \"./types.js\";\n\nconst ALL_CATEGORIES = new Set<EventCategory>([\n EventCategory.MESSAGE,\n EventCategory.PRESENCE,\n EventCategory.ACTIVITY,\n EventCategory.MENTION,\n]);\n\nexport class Room {\n readonly roomId: string;\n /** Direct access to the underlying storage. Useful for bulk reads. */\n readonly storage: StorageProtocol;\n private _channels = new Map<string, Channel>();\n private _participants = new Map<string, Participant>();\n private _observers = new Set<Channel>();\n private _nextObserverId = 0;\n\n /**\n * @param roomId — stable identifier for this room (e.g. a UUID or slug)\n * @param storage — storage backend; defaults to `InMemoryStorage`\n */\n constructor(roomId: string, storage?: StorageProtocol) {\n this.roomId = roomId;\n this.storage = storage ?? new InMemoryStorage();\n }\n\n /**\n * Connect a participant and return their channel.\n */\n async connect(\n participantId: string,\n name: string,\n options?: {\n type?: ParticipantType;\n identifier?: string;\n subscribe?: Set<EventCategory>;\n silent?: boolean;\n authority?: AuthorityLevel;\n },\n ): Promise<Channel> {\n const type = options?.type ?? \"human\";\n const identifier = options?.identifier;\n const subscribe = options?.subscribe;\n const silent = options?.silent ?? false;\n const authority = options?.authority;\n const participant: Participant = {\n id: participantId, name, status: \"online\", type,\n ...(identifier ? { identifier } : {}),\n ...(authority ? { authority } : {}),\n };\n this._participants.set(participantId, participant);\n\n // If already connected, disconnect the old channel first\n const existingChannel = this._channels.get(participantId);\n if (existingChannel) {\n existingChannel._markDisconnected();\n }\n\n const subscriptions = subscribe ?? new Set(ALL_CATEGORIES);\n const channel = new Channel(this, participantId, name, subscriptions);\n this._channels.set(participantId, channel);\n\n if (!silent) {\n const event = createEvent<ParticipantJoinedEvent>({\n type: \"ParticipantJoined\",\n category: \"PRESENCE\",\n room_id: this.roomId,\n participant_id: participantId,\n participant,\n });\n await this._storeAndBroadcast(event, participantId);\n }\n\n return channel;\n }\n\n /**\n * Observe all room events without being a participant.\n *\n * Returns a channel that receives every event — broadcasts AND targeted\n * @mention events directed at other participants. Observers do NOT appear\n * in `listParticipants()` and do not emit join/leave presence events,\n * since they are not participants.\n *\n * Disconnect via `observer.disconnect()` when done.\n *\n * @example\n * const observer = room.observe();\n * for await (const event of observer) {\n * // sees everything, including mentions for other participants\n * }\n */\n observe(): Channel {\n const id = `__obs_${this.roomId}_${this._nextObserverId++}`;\n const channel = new Channel(this, id, \"__observer__\", new Set(ALL_CATEGORIES));\n this._observers.add(channel);\n return channel;\n }\n\n // ── Read methods ───────────────────────────────────────────────────────────\n\n /**\n * Paginate messages, newest-first. Pass the returned `next_cursor` to get\n * the next (older) page.\n */\n async listMessages(\n limit = 30,\n cursor: string | null = null,\n ): Promise<PaginatedResult<Message>> {\n return this.storage.getMessages(this.roomId, limit, cursor);\n }\n\n /**\n * Full-text search across message content, newest-first.\n * `query` is matched case-insensitively against message content.\n */\n async searchMessages(\n query: string,\n limit = 10,\n cursor: string | null = null,\n ): Promise<PaginatedResult<Message>> {\n return this.storage.searchMessages(this.roomId, query, limit, cursor);\n }\n\n /** All currently connected participants (including agents). Observers excluded. */\n listParticipants(): Participant[] {\n return [...this._participants.values()];\n }\n\n /**\n * Paginate room events, newest-first.\n * `category` optionally filters to one EventCategory.\n */\n async listEvents(\n category: EventCategory | null = null,\n limit = 50,\n cursor: string | null = null,\n ): Promise<PaginatedResult<RoomEvent>> {\n return this.storage.getEvents(this.roomId, category, limit, cursor);\n }\n\n /** Look up a single message by ID. Returns null if not found. */\n async getMessage(id: string): Promise<Message | null> {\n return this.storage.getMessage(this.roomId, id);\n }\n\n /** Update a participant's authority level at runtime. */\n setParticipantAuthority(participantId: string, authority: AuthorityLevel): boolean {\n const participant = this._participants.get(participantId);\n if (!participant) return false;\n participant.authority = authority;\n return true;\n }\n\n // ── Internal methods (called by Channel) ──────────────────────────────────\n\n /**\n * @internal\n * Store a message, broadcast MessageSentEvent, and fire MentionedEvents.\n *\n * @mention scanning: looks for `@token` patterns in content and matches\n * against each connected participant's `identifier` and display `name`\n * (case-insensitive). Fires a `MentionedEvent` for each match, delivered\n * to the mentioned participant AND all observers.\n */\n async _handleMessage(message: Message): Promise<void> {\n await this.storage.addMessage(message);\n\n const event = createEvent<MessageSentEvent>({\n type: \"MessageSent\",\n category: \"MESSAGE\",\n room_id: this.roomId,\n participant_id: message.sender_id,\n message,\n });\n await this._storeAndBroadcast(event);\n\n const mentions = this._detectMentions(message.content);\n for (const mentionedId of mentions) {\n const ch = this._channels.get(mentionedId);\n if (ch) {\n const mentionEvent = createEvent<MentionedEvent>({\n type: \"Mentioned\",\n category: \"MENTION\",\n room_id: this.roomId,\n participant_id: mentionedId,\n message,\n });\n await this.storage.addEvent(mentionEvent);\n ch._deliver(mentionEvent);\n // Deliver mentions to all observers too\n for (const observer of this._observers) {\n observer._deliver(mentionEvent);\n }\n }\n }\n }\n\n /** @internal Store and broadcast an activity event. */\n async _handleEvent(event: RoomEvent): Promise<void> {\n await this._storeAndBroadcast(event, event.participant_id);\n }\n\n /** @internal Remove a channel and optionally broadcast ParticipantLeftEvent. */\n async _disconnectChannel(channel: Channel, silent = false): Promise<void> {\n // Observer channels are not participants — just remove from observer set\n if (this._observers.delete(channel)) {\n return;\n }\n\n const pid = channel.participantId;\n const participant = this._participants.get(pid);\n this._channels.delete(pid);\n this._participants.delete(pid);\n\n if (!silent && participant) {\n const event = createEvent<ParticipantLeftEvent>({\n type: \"ParticipantLeft\",\n category: \"PRESENCE\",\n room_id: this.roomId,\n participant_id: pid,\n participant,\n });\n await this._storeAndBroadcast(event);\n }\n }\n\n private async _storeAndBroadcast(\n event: RoomEvent,\n exclude?: string,\n ): Promise<void> {\n await this.storage.addEvent(event);\n this._broadcast(event, exclude);\n }\n\n private _broadcast(event: RoomEvent, exclude?: string): void {\n for (const [pid, channel] of this._channels) {\n if (pid !== exclude) {\n channel._deliver(event);\n }\n }\n for (const observer of this._observers) {\n observer._deliver(event);\n }\n }\n\n /**\n * Scan message content for `@token` patterns and return matching participant IDs.\n * Matches against both `identifier` (e.g. `@my-agent`) and display `name` (e.g. `@Alice`).\n * Case-insensitive. Deduplicates — each participant appears at most once.\n */\n private _detectMentions(content: string): string[] {\n const mentionedIds: string[] = [];\n const pattern = /@([a-zA-Z0-9_-]+)/g;\n let match;\n while ((match = pattern.exec(content)) !== null) {\n const token = match[1].toLowerCase();\n for (const [pid, participant] of this._participants) {\n const matchesId = participant.identifier?.toLowerCase() === token;\n const matchesName = participant.name.toLowerCase() === token;\n if ((matchesId || matchesName) && !mentionedIds.includes(pid)) {\n mentionedIds.push(pid);\n }\n }\n }\n return mentionedIds;\n }\n}\n","/** Random display name generation for participants and rooms. */\n\nconst PLACES = [\n \"bay\", \"cove\", \"glen\", \"moor\", \"fjord\", \"cape\", \"crag\", \"bluff\", \"cliff\", \"ridge\",\n \"peak\", \"mesa\", \"butte\", \"canyon\", \"gorge\", \"ravine\", \"gulch\", \"dell\", \"dune\", \"plain\",\n \"heath\", \"fell\", \"bog\", \"marsh\", \"pond\", \"lake\", \"tarn\", \"pool\", \"harbor\", \"haven\",\n \"inlet\", \"gulf\", \"sound\", \"strait\", \"channel\", \"delta\", \"lagoon\", \"atoll\", \"shoal\", \"shore\",\n \"coast\", \"isle\", \"forest\", \"grove\", \"copse\", \"glade\", \"meadow\", \"field\", \"valley\", \"hollow\",\n \"nook\", \"ford\", \"falls\", \"spring\", \"well\", \"crest\", \"knoll\", \"summit\", \"slope\", \"basin\",\n \"bank\", \"strand\", \"loch\", \"steppe\", \"tundra\", \"prairie\", \"savanna\", \"jungle\", \"desert\",\n \"highland\", \"estuary\", \"bight\", \"spit\", \"islet\", \"island\", \"tor\", \"vale\", \"brook\", \"creek\",\n \"river\", \"weir\", \"cascade\", \"scarp\", \"tower\", \"plateau\", \"upland\", \"lowland\",\n];\n\n/** Generate a random room name like \"Glen-4827\". */\nexport function randomRoomName(): string {\n const place = PLACES[Math.floor(Math.random() * PLACES.length)];\n const digits = String(Math.floor(Math.random() * 9000) + 1000);\n return `${place[0].toUpperCase()}${place.slice(1)}-${digits}`;\n}\n\nconst NAMES = [\n \"ash\", \"kai\", \"sol\", \"pip\", \"kit\", \"zev\", \"bly\", \"rue\", \"dex\", \"nix\",\n \"wren\", \"gray\", \"clay\", \"reed\", \"roux\", \"roan\", \"jade\", \"max\", \"val\", \"xen\",\n \"zen\", \"pax\", \"jude\", \"finn\", \"sage\", \"remy\", \"nico\", \"noel\", \"lumi\", \"jules\",\n \"hero\", \"eden\", \"blake\", \"bram\", \"clem\", \"flint\", \"nox\", \"oak\", \"moss\", \"bryn\",\n \"lyra\", \"mars\", \"neve\", \"onyx\", \"sable\", \"thea\", \"koa\", \"ren\", \"ora\", \"lev\",\n \"tru\", \"vox\", \"quinn\", \"rowan\", \"avery\", \"cass\", \"greer\", \"holt\", \"arlo\", \"drew\",\n \"emery\", \"finley\", \"harley\", \"harper\", \"jamie\", \"vesper\", \"west\", \"wynne\", \"yael\",\n \"zion\", \"sawyer\", \"scout\", \"tatum\", \"toby\", \"toni\", \"riley\", \"reese\", \"morgan\",\n \"micah\", \"logan\", \"lane\", \"jordan\", \"perry\", \"piper\", \"erin\", \"dylan\", \"camden\",\n \"seren\", \"elio\", \"cael\", \"davi\", \"lyric\", \"kiran\", \"arrow\", \"riven\", \"cleo\",\n \"sora\", \"tae\", \"cade\", \"milo\",\n];\n\n/** Generate a random display name like \"Wren-4827\". */\nexport function randomName(): string {\n const name = NAMES[Math.floor(Math.random() * NAMES.length)];\n const digits = String(Math.floor(Math.random() * 9000) + 1000);\n return `${name[0].toUpperCase()}${name.slice(1)}-${digits}`;\n}\n"],"mappings":"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","names":["resolve"]}
@@ -1,5 +1,5 @@
1
- import { I as ILLMSession, R as RoomResolver, C as ClaudeSessionOptions, a as ContentPart } from '../types-Co2KKpkh.js';
2
- import '../index-DGncuUqB.js';
1
+ import { I as ILLMSession, R as RoomResolver, C as ClaudeSessionOptions, a as ContentPart } from '../types-B9xf8w53.js';
2
+ import '../index-DwVKKxqK.js';
3
3
  import 'zod';
4
4
 
5
5
  /** Claude Agent SDK session backend for stoops agents. */
package/dist/cli/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- InMemoryStorage,
3
+ FileBackedStorage,
4
4
  Room,
5
5
  randomName,
6
6
  randomRoomName
7
- } from "../chunk-TN56PBF3.js";
7
+ } from "../chunk-XEKY3KEU.js";
8
8
  import {
9
9
  EventProcessor,
10
10
  RemoteRoomDataSource,
@@ -25,6 +25,8 @@ import { createServer } from "http";
25
25
  import { spawn, execFileSync } from "child_process";
26
26
  import { randomUUID } from "crypto";
27
27
  import { createRequire } from "module";
28
+ import { tmpdir } from "os";
29
+ import { join as pathJoin } from "path";
28
30
 
29
31
  // src/cli/auth.ts
30
32
  import { randomBytes } from "crypto";
@@ -123,7 +125,24 @@ async function serve(options) {
123
125
  } : logServer;
124
126
  let publicUrl = serverUrl;
125
127
  let tunnelProcess = null;
126
- const storage = new InMemoryStorage();
128
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
129
+ const savePath = options.save ?? options.load ?? pathJoin(tmpdir(), `stoops-${roomName}-${timestamp}.json`);
130
+ let storage;
131
+ if (options.load) {
132
+ try {
133
+ storage = await FileBackedStorage.load(options.load);
134
+ log(`loaded room state from ${options.load}`);
135
+ } catch (err) {
136
+ if (err.code === "ENOENT") {
137
+ storage = new FileBackedStorage(options.load);
138
+ log(`no existing file at ${options.load}, starting fresh`);
139
+ } else {
140
+ throw err;
141
+ }
142
+ }
143
+ } else {
144
+ storage = new FileBackedStorage(savePath);
145
+ }
127
146
  const room = new Room(roomName, storage);
128
147
  const tokens = new TokenManager();
129
148
  const participants = /* @__PURE__ */ new Map();
@@ -513,7 +532,7 @@ Port ${port} is already in use. Another stoops instance may be running.`);
513
532
  const adminToken = tokens.generateShareToken("admin", "admin");
514
533
  const memberToken = tokens.generateShareToken("admin", "member");
515
534
  if (options.headless) {
516
- process.stdout.write(JSON.stringify({ serverUrl, publicUrl, roomName, adminToken, memberToken }) + "\n");
535
+ process.stdout.write(JSON.stringify({ serverUrl, publicUrl, roomName, adminToken, memberToken, savePath }) + "\n");
517
536
  } else if (!options.quiet) {
518
537
  let version = process.env.npm_package_version ?? "";
519
538
  if (!version) {
@@ -533,6 +552,7 @@ Port ${port} is already in use. Another stoops instance may be running.`);
533
552
  Room: ${roomName}
534
553
  Server: ${serverUrl}${publicUrl !== serverUrl ? `
535
554
  Tunnel: ${publicUrl}` : ""}
555
+ Saving: ${savePath}
536
556
 
537
557
  Join: stoops join ${joinUrl}
538
558
  Admin: stoops join ${adminUrl}
@@ -1574,7 +1594,7 @@ function toDisplayEvent(event, selfId, participantTypes) {
1574
1594
  // src/cli/claude/run.ts
1575
1595
  import { writeFileSync, mkdtempSync, rmSync, chmodSync } from "fs";
1576
1596
  import { join as join2 } from "path";
1577
- import { tmpdir } from "os";
1597
+ import { tmpdir as tmpdir2 } from "os";
1578
1598
 
1579
1599
  // src/cli/tmux.ts
1580
1600
  import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
@@ -2178,7 +2198,7 @@ async function runClaude(options) {
2178
2198
  process.exit(1);
2179
2199
  }
2180
2200
  const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
2181
- const tmpDir = mkdtempSync(join2(tmpdir(), "stoops_agent_"));
2201
+ const tmpDir = mkdtempSync(join2(tmpdir2(), "stoops_agent_"));
2182
2202
  const bridgePath = join2(tmpDir, "mcp-bridge.cjs");
2183
2203
  writeFileSync(bridgePath, MCP_STDIO_BRIDGE);
2184
2204
  chmodSync(bridgePath, 493);
@@ -2375,7 +2395,7 @@ async function pollForReady(url, timeoutMs) {
2375
2395
  import { execFileSync as execFileSync3 } from "child_process";
2376
2396
  import { writeFileSync as writeFileSync2, mkdtempSync as mkdtempSync2, mkdirSync, rmSync as rmSync2 } from "fs";
2377
2397
  import { join as join3 } from "path";
2378
- import { tmpdir as tmpdir2 } from "os";
2398
+ import { tmpdir as tmpdir3 } from "os";
2379
2399
 
2380
2400
  // src/cli/codex/tmux-bridge.ts
2381
2401
  var SPINNER_CHARS2 = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
@@ -2588,7 +2608,7 @@ async function runCodex(options) {
2588
2608
  process.exit(1);
2589
2609
  }
2590
2610
  const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
2591
- const tmpDir = mkdtempSync2(join3(tmpdir2(), "stoops_codex_"));
2611
+ const tmpDir = mkdtempSync2(join3(tmpdir3(), "stoops_codex_"));
2592
2612
  const mcpPort = new URL(setup.mcpServer.url).port;
2593
2613
  const mcpUrl = `http://127.0.0.1:${mcpPort}/mcp`;
2594
2614
  const codexConfigDir = join3(tmpDir, ".codex");
@@ -2721,7 +2741,9 @@ async function main() {
2721
2741
  room: getFlag("room"),
2722
2742
  port,
2723
2743
  share: args.includes("--share"),
2724
- headless: args.includes("--headless")
2744
+ headless: args.includes("--headless"),
2745
+ save: getFlag("save"),
2746
+ load: getFlag("load")
2725
2747
  });
2726
2748
  return;
2727
2749
  }
@@ -2736,7 +2758,9 @@ async function main() {
2736
2758
  room: getFlag("room"),
2737
2759
  port,
2738
2760
  share: args.includes("--share"),
2739
- quiet: true
2761
+ quiet: true,
2762
+ save: getFlag("save"),
2763
+ load: getFlag("load")
2740
2764
  });
2741
2765
  const adminJoinUrl = buildShareUrl(result.serverUrl, result.adminToken);
2742
2766
  const participantShareUrl = buildShareUrl(