stoops 0.3.1 → 0.3.3

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,
@@ -20,11 +20,16 @@ import {
20
20
  formatTimestamp
21
21
  } from "../chunk-TGA24MC3.js";
22
22
 
23
+ // src/cli/index.ts
24
+ import { createRequire as createRequire2 } from "module";
25
+
23
26
  // src/cli/serve.ts
24
27
  import { createServer } from "http";
25
28
  import { spawn, execFileSync } from "child_process";
26
29
  import { randomUUID } from "crypto";
27
30
  import { createRequire } from "module";
31
+ import { tmpdir } from "os";
32
+ import { join as pathJoin } from "path";
28
33
 
29
34
  // src/cli/auth.ts
30
35
  import { randomBytes } from "crypto";
@@ -123,7 +128,24 @@ async function serve(options) {
123
128
  } : logServer;
124
129
  let publicUrl = serverUrl;
125
130
  let tunnelProcess = null;
126
- const storage = new InMemoryStorage();
131
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
132
+ const savePath = options.save ?? options.load ?? pathJoin(tmpdir(), `stoops-${roomName}-${timestamp}.json`);
133
+ let storage;
134
+ if (options.load) {
135
+ try {
136
+ storage = await FileBackedStorage.load(options.load);
137
+ log(`loaded room state from ${options.load}`);
138
+ } catch (err) {
139
+ if (err.code === "ENOENT") {
140
+ storage = new FileBackedStorage(options.load);
141
+ log(`no existing file at ${options.load}, starting fresh`);
142
+ } else {
143
+ throw err;
144
+ }
145
+ }
146
+ } else {
147
+ storage = new FileBackedStorage(savePath);
148
+ }
127
149
  const room = new Room(roomName, storage);
128
150
  const tokens = new TokenManager();
129
151
  const participants = /* @__PURE__ */ new Map();
@@ -513,7 +535,7 @@ Port ${port} is already in use. Another stoops instance may be running.`);
513
535
  const adminToken = tokens.generateShareToken("admin", "admin");
514
536
  const memberToken = tokens.generateShareToken("admin", "member");
515
537
  if (options.headless) {
516
- process.stdout.write(JSON.stringify({ serverUrl, publicUrl, roomName, adminToken, memberToken }) + "\n");
538
+ process.stdout.write(JSON.stringify({ serverUrl, publicUrl, roomName, adminToken, memberToken, savePath }) + "\n");
517
539
  } else if (!options.quiet) {
518
540
  let version = process.env.npm_package_version ?? "";
519
541
  if (!version) {
@@ -533,6 +555,7 @@ Port ${port} is already in use. Another stoops instance may be running.`);
533
555
  Room: ${roomName}
534
556
  Server: ${serverUrl}${publicUrl !== serverUrl ? `
535
557
  Tunnel: ${publicUrl}` : ""}
558
+ Saving: ${savePath}
536
559
 
537
560
  Join: stoops join ${joinUrl}
538
561
  Admin: stoops join ${adminUrl}
@@ -565,7 +588,7 @@ Port ${port} is already in use. Another stoops instance may be running.`);
565
588
  };
566
589
  process.on("SIGINT", shutdown);
567
590
  process.on("SIGTERM", shutdown);
568
- return { serverUrl, publicUrl, roomName, adminToken, memberToken };
591
+ return { serverUrl, publicUrl, roomName, adminToken, memberToken, savePath };
569
592
  }
570
593
  function logServer(message) {
571
594
  console.log(` [${formatTimestamp(/* @__PURE__ */ new Date())}] ${message}`);
@@ -779,7 +802,9 @@ function App({
779
802
  onCtrlC,
780
803
  onReady,
781
804
  readOnly,
782
- isAdmin
805
+ isAdmin,
806
+ version,
807
+ savePath
783
808
  }) {
784
809
  const [events, setEvents] = useState([]);
785
810
  const [agentNames, setAgentNames] = useState([]);
@@ -915,10 +940,18 @@ function App({
915
940
  if (!entry.event) {
916
941
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingTop: 1, paddingBottom: 1, children: [
917
942
  BANNER_LINES.map((line, i) => /* @__PURE__ */ jsx(Text, { color: GRADIENT[i], children: line }, i)),
943
+ version && /* @__PURE__ */ jsxs(Text, { color: C.dim, children: [
944
+ " v",
945
+ version
946
+ ] }),
918
947
  /* @__PURE__ */ jsx(Text, { children: " " }),
919
948
  /* @__PURE__ */ jsxs(Text, { children: [
920
949
  /* @__PURE__ */ jsx(Text, { color: C.dim, children: " room " }),
921
950
  /* @__PURE__ */ jsx(Text, { color: C.cyan, bold: true, children: roomName })
951
+ ] }),
952
+ savePath && /* @__PURE__ */ jsxs(Text, { children: [
953
+ /* @__PURE__ */ jsx(Text, { color: C.dim, children: " saved " }),
954
+ /* @__PURE__ */ jsx(Text, { color: C.secondary, children: savePath })
922
955
  ] })
923
956
  ] }, entry.id);
924
957
  }
@@ -986,7 +1019,9 @@ function startTUI(opts) {
986
1019
  onCtrlC: opts.onCtrlC,
987
1020
  onReady,
988
1021
  readOnly: opts.readOnly,
989
- isAdmin: opts.isAdmin
1022
+ isAdmin: opts.isAdmin,
1023
+ version: opts.version,
1024
+ savePath: opts.savePath
990
1025
  }
991
1026
  ),
992
1027
  { exitOnCtrlC: false }
@@ -1379,6 +1414,8 @@ ${lines.join("\n")}`);
1379
1414
  roomName,
1380
1415
  readOnly: isReadOnly,
1381
1416
  isAdmin: authority === "admin",
1417
+ version: options.version,
1418
+ savePath: options.savePath,
1382
1419
  onSend: isReadOnly ? void 0 : async (content) => {
1383
1420
  if (content.startsWith("/")) {
1384
1421
  await handleSlashCommand(content);
@@ -1574,7 +1611,7 @@ function toDisplayEvent(event, selfId, participantTypes) {
1574
1611
  // src/cli/claude/run.ts
1575
1612
  import { writeFileSync, mkdtempSync, rmSync, chmodSync } from "fs";
1576
1613
  import { join as join2 } from "path";
1577
- import { tmpdir } from "os";
1614
+ import { tmpdir as tmpdir2 } from "os";
1578
1615
 
1579
1616
  // src/cli/tmux.ts
1580
1617
  import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
@@ -2178,7 +2215,7 @@ async function runClaude(options) {
2178
2215
  process.exit(1);
2179
2216
  }
2180
2217
  const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
2181
- const tmpDir = mkdtempSync(join2(tmpdir(), "stoops_agent_"));
2218
+ const tmpDir = mkdtempSync(join2(tmpdir2(), "stoops_agent_"));
2182
2219
  const bridgePath = join2(tmpDir, "mcp-bridge.cjs");
2183
2220
  writeFileSync(bridgePath, MCP_STDIO_BRIDGE);
2184
2221
  chmodSync(bridgePath, 493);
@@ -2375,7 +2412,7 @@ async function pollForReady(url, timeoutMs) {
2375
2412
  import { execFileSync as execFileSync3 } from "child_process";
2376
2413
  import { writeFileSync as writeFileSync2, mkdtempSync as mkdtempSync2, mkdirSync, rmSync as rmSync2 } from "fs";
2377
2414
  import { join as join3 } from "path";
2378
- import { tmpdir as tmpdir2 } from "os";
2415
+ import { tmpdir as tmpdir3 } from "os";
2379
2416
 
2380
2417
  // src/cli/codex/tmux-bridge.ts
2381
2418
  var SPINNER_CHARS2 = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
@@ -2588,7 +2625,7 @@ async function runCodex(options) {
2588
2625
  process.exit(1);
2589
2626
  }
2590
2627
  const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
2591
- const tmpDir = mkdtempSync2(join3(tmpdir2(), "stoops_codex_"));
2628
+ const tmpDir = mkdtempSync2(join3(tmpdir3(), "stoops_codex_"));
2592
2629
  const mcpPort = new URL(setup.mcpServer.url).port;
2593
2630
  const mcpUrl = `http://127.0.0.1:${mcpPort}/mcp`;
2594
2631
  const codexConfigDir = join3(tmpDir, ".codex");
@@ -2641,6 +2678,15 @@ async function runCodex(options) {
2641
2678
  }
2642
2679
 
2643
2680
  // src/cli/index.ts
2681
+ function getVersion() {
2682
+ try {
2683
+ const require2 = createRequire2(import.meta.url);
2684
+ const pkg = require2("../../package.json");
2685
+ return pkg.version ?? "unknown";
2686
+ } catch {
2687
+ return "unknown";
2688
+ }
2689
+ }
2644
2690
  var args = process.argv.slice(2);
2645
2691
  function getFlag(name, arr = args) {
2646
2692
  const idx = arr.indexOf(`--${name}`);
@@ -2721,7 +2767,9 @@ async function main() {
2721
2767
  room: getFlag("room"),
2722
2768
  port,
2723
2769
  share: args.includes("--share"),
2724
- headless: args.includes("--headless")
2770
+ headless: args.includes("--headless"),
2771
+ save: getFlag("save"),
2772
+ load: getFlag("load")
2725
2773
  });
2726
2774
  return;
2727
2775
  }
@@ -2736,7 +2784,9 @@ async function main() {
2736
2784
  room: getFlag("room"),
2737
2785
  port,
2738
2786
  share: args.includes("--share"),
2739
- quiet: true
2787
+ quiet: true,
2788
+ save: getFlag("save"),
2789
+ load: getFlag("load")
2740
2790
  });
2741
2791
  const adminJoinUrl = buildShareUrl(result.serverUrl, result.adminToken);
2742
2792
  const participantShareUrl = buildShareUrl(
@@ -2746,7 +2796,9 @@ async function main() {
2746
2796
  await join({
2747
2797
  server: adminJoinUrl,
2748
2798
  name: getFlag("name"),
2749
- shareUrl: participantShareUrl
2799
+ shareUrl: participantShareUrl,
2800
+ version: getVersion(),
2801
+ savePath: result.savePath
2750
2802
  });
2751
2803
  return;
2752
2804
  }