@gorlitzer-labs/apiary 1.13.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/agent/index.d.ts +689 -0
  4. package/dist/agent/index.js +34 -0
  5. package/dist/agent/index.js.map +1 -0
  6. package/dist/chunk-5V7QLCGB.js +184 -0
  7. package/dist/chunk-5V7QLCGB.js.map +1 -0
  8. package/dist/chunk-AYQJIAR5.js +152 -0
  9. package/dist/chunk-AYQJIAR5.js.map +1 -0
  10. package/dist/chunk-BZ4VDWC2.js +76 -0
  11. package/dist/chunk-BZ4VDWC2.js.map +1 -0
  12. package/dist/chunk-CFY6Z6Q6.js +95 -0
  13. package/dist/chunk-CFY6Z6Q6.js.map +1 -0
  14. package/dist/chunk-CZHO4DBN.js +1719 -0
  15. package/dist/chunk-CZHO4DBN.js.map +1 -0
  16. package/dist/chunk-D7DXZQWT.js +208 -0
  17. package/dist/chunk-D7DXZQWT.js.map +1 -0
  18. package/dist/chunk-ED6ROH6B.js +2021 -0
  19. package/dist/chunk-ED6ROH6B.js.map +1 -0
  20. package/dist/chunk-PEB6HEFT.js +606 -0
  21. package/dist/chunk-PEB6HEFT.js.map +1 -0
  22. package/dist/chunk-QL66XSDB.js +157 -0
  23. package/dist/chunk-QL66XSDB.js.map +1 -0
  24. package/dist/chunk-TPNFOQA7.js +288 -0
  25. package/dist/chunk-TPNFOQA7.js.map +1 -0
  26. package/dist/chunk-ZEFRNAX6.js +1771 -0
  27. package/dist/chunk-ZEFRNAX6.js.map +1 -0
  28. package/dist/cli/index.d.ts +1 -0
  29. package/dist/cli/index.js +2432 -0
  30. package/dist/cli/index.js.map +1 -0
  31. package/dist/index-Dp3QSGog.d.ts +837 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +22 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/invites-OFXIQLLN.js +23 -0
  36. package/dist/invites-OFXIQLLN.js.map +1 -0
  37. package/dist/join-CQXEF65B.js +15 -0
  38. package/dist/join-CQXEF65B.js.map +1 -0
  39. package/dist/room-Z7KDRVYD.js +1099 -0
  40. package/dist/room-Z7KDRVYD.js.map +1 -0
  41. package/dist/run-DOADB4T7.js +90 -0
  42. package/dist/run-DOADB4T7.js.map +1 -0
  43. package/dist/update-JGE3WCQB.js +11 -0
  44. package/dist/update-JGE3WCQB.js.map +1 -0
  45. package/package.json +72 -0
@@ -0,0 +1,606 @@
1
+ import {
2
+ EventCategory,
3
+ MessageSchema,
4
+ createEvent
5
+ } from "./chunk-BZ4VDWC2.js";
6
+
7
+ // src/core/storage.ts
8
+ import { writeFile, readFile } from "fs/promises";
9
+ import { resolve, sep } from "path";
10
+ import { homedir, tmpdir } from "os";
11
+ function paginate(items, limit, cursor, key) {
12
+ let subset;
13
+ if (cursor != null) {
14
+ const cursorIdx = items.findIndex((item) => key(item) === cursor);
15
+ if (cursorIdx === -1) {
16
+ return { items: [], next_cursor: null, has_more: false };
17
+ }
18
+ subset = items.slice(0, cursorIdx);
19
+ } else {
20
+ subset = items;
21
+ }
22
+ const page = limit < subset.length ? subset.slice(-limit) : subset.slice();
23
+ page.reverse();
24
+ const has_more = subset.length > limit;
25
+ const next_cursor = has_more && page.length > 0 ? key(page[page.length - 1]) : null;
26
+ return { items: page, next_cursor, has_more };
27
+ }
28
+ function paginateByIndex(items, limit, cursor) {
29
+ const parsedCursor = cursor != null ? parseInt(cursor, 10) : items.length;
30
+ const endIdx = Number.isNaN(parsedCursor) ? items.length : parsedCursor;
31
+ const startIdx = Math.max(0, endIdx - limit);
32
+ const page = items.slice(startIdx, endIdx).reverse();
33
+ const has_more = startIdx > 0;
34
+ const next_cursor = has_more ? String(startIdx) : null;
35
+ return { items: page, next_cursor, has_more };
36
+ }
37
+ var InMemoryStorage = class {
38
+ _messages = /* @__PURE__ */ new Map();
39
+ _events = /* @__PURE__ */ new Map();
40
+ async addMessage(message) {
41
+ const list = this._messages.get(message.room_id) ?? [];
42
+ list.push(message);
43
+ this._messages.set(message.room_id, list);
44
+ return message;
45
+ }
46
+ async getMessage(room_id, message_id) {
47
+ const list = this._messages.get(room_id) ?? [];
48
+ return list.find((m) => m.id === message_id) ?? null;
49
+ }
50
+ async getMessages(room_id, limit = 30, cursor = null) {
51
+ const messages = this._messages.get(room_id) ?? [];
52
+ return paginate(messages, limit, cursor, (m) => m.id);
53
+ }
54
+ async searchMessages(room_id, query, limit = 10, cursor = null) {
55
+ const messages = this._messages.get(room_id) ?? [];
56
+ const q = query.toLowerCase();
57
+ const filtered = messages.filter(
58
+ (m) => m.content.toLowerCase().includes(q)
59
+ );
60
+ return paginate(filtered, limit, cursor, (m) => m.id);
61
+ }
62
+ async addEvent(event) {
63
+ const list = this._events.get(event.room_id) ?? [];
64
+ list.push(event);
65
+ this._events.set(event.room_id, list);
66
+ }
67
+ async getEvents(room_id, category = null, limit = 50, cursor = null) {
68
+ let events = this._events.get(room_id) ?? [];
69
+ if (category != null) {
70
+ events = events.filter((e) => e.category === category);
71
+ }
72
+ return paginateByIndex(events, limit, cursor);
73
+ }
74
+ async clearRoom(room_id) {
75
+ this._messages.delete(room_id);
76
+ this._events.delete(room_id);
77
+ }
78
+ };
79
+ var FileBackedStorage = class _FileBackedStorage extends InMemoryStorage {
80
+ _filePath;
81
+ constructor(filePath) {
82
+ super();
83
+ this._filePath = resolve(filePath);
84
+ const cwd = process.cwd();
85
+ const tmp = tmpdir();
86
+ const apiaryDir = resolve(homedir(), ".apiary");
87
+ if (!this._filePath.startsWith(cwd + sep) && !this._filePath.startsWith(tmp + sep) && !this._filePath.startsWith(apiaryDir + sep)) {
88
+ throw new Error(`FileBackedStorage path must be under ${cwd}, ${tmp}, or ${apiaryDir}`);
89
+ }
90
+ }
91
+ async addMessage(message) {
92
+ const result = await super.addMessage(message);
93
+ await this._flush();
94
+ return result;
95
+ }
96
+ async addEvent(event) {
97
+ await super.addEvent(event);
98
+ await this._flush();
99
+ }
100
+ async clearRoom(room_id) {
101
+ await super.clearRoom(room_id);
102
+ await this._flush();
103
+ }
104
+ async _flush() {
105
+ const data = {};
106
+ for (const [roomId, messages] of this._messages) {
107
+ if (!data[roomId]) data[roomId] = { messages: [], events: [] };
108
+ data[roomId].messages = messages;
109
+ }
110
+ for (const [roomId, events] of this._events) {
111
+ if (!data[roomId]) data[roomId] = { messages: [], events: [] };
112
+ data[roomId].events = events;
113
+ }
114
+ await writeFile(this._filePath, JSON.stringify(data, null, 2));
115
+ }
116
+ /** Load an existing file and return a FileBackedStorage that continues saving to it. */
117
+ static async load(filePath) {
118
+ const storage = new _FileBackedStorage(filePath);
119
+ const raw = await readFile(storage._filePath, "utf-8");
120
+ const data = JSON.parse(raw);
121
+ for (const [roomId, { messages, events }] of Object.entries(data)) {
122
+ storage._messages.set(
123
+ roomId,
124
+ messages.map((m) => ({ ...m, timestamp: new Date(m.timestamp) }))
125
+ );
126
+ storage._events.set(
127
+ roomId,
128
+ events.map((e) => rehydrateEvent(e))
129
+ );
130
+ }
131
+ return storage;
132
+ }
133
+ };
134
+ function rehydrateEvent(e) {
135
+ const event = { ...e, timestamp: new Date(e.timestamp) };
136
+ if ("message" in event && event.message) {
137
+ event.message = { ...event.message, timestamp: new Date(event.message.timestamp) };
138
+ }
139
+ return event;
140
+ }
141
+
142
+ // src/core/channel.ts
143
+ var Channel = class {
144
+ participantId;
145
+ participantName;
146
+ subscriptions;
147
+ _room;
148
+ _queue = [];
149
+ _waiters = [];
150
+ _disconnected = false;
151
+ constructor(room, participantId, participantName, subscriptions) {
152
+ this._room = room;
153
+ this.participantId = participantId;
154
+ this.participantName = participantName;
155
+ this.subscriptions = subscriptions;
156
+ }
157
+ get roomId() {
158
+ return this._room.roomId;
159
+ }
160
+ /**
161
+ * Send a chat message from this participant.
162
+ *
163
+ * Persists the message to storage, broadcasts a `MessageSentEvent` to all
164
+ * participants (including the sender), and fires `MentionedEvent` for any
165
+ * `@name` or `@identifier` patterns found in the content.
166
+ *
167
+ * @param content — message text (may be empty if image is provided)
168
+ * @param replyToId — ID of the message being replied to (optional)
169
+ * @param image — optional legacy image attachment
170
+ * @param attachments — optional array of typed attachments
171
+ * @param recipients — if non-empty, this is a whisper: only these participant IDs (plus sender) receive it
172
+ */
173
+ async sendMessage(content, replyToId, image, attachments, recipients) {
174
+ if (this._disconnected) {
175
+ throw new Error("Channel is disconnected");
176
+ }
177
+ const message = MessageSchema.parse({
178
+ room_id: this._room.roomId,
179
+ sender_id: this.participantId,
180
+ sender_name: this.participantName,
181
+ content,
182
+ reply_to_id: replyToId ?? null,
183
+ recipients: recipients ?? [],
184
+ attachments: attachments ?? [],
185
+ image_url: image?.url ?? null,
186
+ image_mime_type: image?.mimeType ?? null,
187
+ image_size_bytes: image?.sizeBytes ?? null
188
+ });
189
+ await this._room._handleMessage(message);
190
+ return message;
191
+ }
192
+ /**
193
+ * Ping another participant for a status check.
194
+ *
195
+ * Creates a `PingedEvent` delivered to the target and all observers.
196
+ * Unlike @mentions, pings are non-blocking for active agents.
197
+ */
198
+ async ping(targetId) {
199
+ if (this._disconnected) {
200
+ throw new Error("Channel is disconnected");
201
+ }
202
+ await this._room._handlePing(this.participantId, this.participantName, targetId);
203
+ }
204
+ /**
205
+ * Emit a non-message activity event to the room.
206
+ *
207
+ * Use this for platform events: tool use indicators, mode changes, compaction
208
+ * notices, etc. The event is persisted and broadcast to all subscribed
209
+ * participants.
210
+ */
211
+ async emit(event) {
212
+ if (this._disconnected) {
213
+ throw new Error("Channel is disconnected");
214
+ }
215
+ await this._room._handleEvent(event);
216
+ }
217
+ /**
218
+ * Change which event categories this channel receives.
219
+ * Takes effect immediately — buffered events from unsubscribed categories
220
+ * are not retroactively removed.
221
+ */
222
+ updateSubscriptions(categories) {
223
+ this.subscriptions = categories;
224
+ }
225
+ /**
226
+ * Leave the room.
227
+ *
228
+ * @param silent — if true, suppresses the `ParticipantLeft` broadcast.
229
+ * Agents disconnect silently to avoid chat noise.
230
+ */
231
+ async disconnect(silent = false) {
232
+ if (!this._disconnected) {
233
+ this._disconnected = true;
234
+ const waiters = this._waiters;
235
+ this._waiters = [];
236
+ for (const w of waiters) {
237
+ w.reject(new Error("Channel disconnected"));
238
+ }
239
+ await this._room._disconnectChannel(this, silent);
240
+ }
241
+ }
242
+ /** @internal Called by Room to mark this channel as disconnected without removing from room maps. */
243
+ _markDisconnected() {
244
+ if (!this._disconnected) {
245
+ this._disconnected = true;
246
+ const waiters = this._waiters;
247
+ this._waiters = [];
248
+ for (const w of waiters) {
249
+ w.reject(new Error("Channel disconnected"));
250
+ }
251
+ }
252
+ }
253
+ /** @internal Called by Room to deliver an incoming event. Filters by subscription. */
254
+ _deliver(event) {
255
+ if (this._disconnected) return;
256
+ if (!this.subscriptions.has(event.category)) return;
257
+ if (this._waiters.length > 0) {
258
+ const waiter = this._waiters.shift();
259
+ waiter.resolve(event);
260
+ } else {
261
+ this._queue.push(event);
262
+ }
263
+ }
264
+ /**
265
+ * Receive the next event, waiting up to `timeoutMs`.
266
+ *
267
+ * Returns null if no event arrives within the timeout. Drains buffered events
268
+ * before waiting. Used by `EventMultiplexer` to fan-in events from multiple
269
+ * rooms into a single stream.
270
+ */
271
+ receive(timeoutMs) {
272
+ if (this._queue.length > 0) {
273
+ return Promise.resolve(this._queue.shift());
274
+ }
275
+ if (this._disconnected) {
276
+ return Promise.resolve(null);
277
+ }
278
+ return new Promise((resolve2) => {
279
+ let settled = false;
280
+ const waiter = {
281
+ resolve: (event) => {
282
+ if (!settled) {
283
+ settled = true;
284
+ clearTimeout(timer);
285
+ resolve2(event);
286
+ }
287
+ },
288
+ reject: () => {
289
+ if (!settled) {
290
+ settled = true;
291
+ clearTimeout(timer);
292
+ resolve2(null);
293
+ }
294
+ }
295
+ };
296
+ this._waiters.push(waiter);
297
+ const timer = setTimeout(() => {
298
+ if (!settled) {
299
+ settled = true;
300
+ const idx = this._waiters.indexOf(waiter);
301
+ if (idx !== -1) this._waiters.splice(idx, 1);
302
+ resolve2(null);
303
+ }
304
+ }, timeoutMs);
305
+ });
306
+ }
307
+ /**
308
+ * Async iterator — yields events as they arrive.
309
+ *
310
+ * Used by `EventMultiplexer` to fan-in all room channels into a single stream.
311
+ * The iterator completes when the channel is disconnected.
312
+ *
313
+ * @example
314
+ * for await (const event of channel) {
315
+ * console.log(event.type);
316
+ * }
317
+ */
318
+ [Symbol.asyncIterator]() {
319
+ return {
320
+ next: () => {
321
+ if (this._queue.length > 0) {
322
+ return Promise.resolve({
323
+ value: this._queue.shift(),
324
+ done: false
325
+ });
326
+ }
327
+ if (this._disconnected) {
328
+ return Promise.resolve({
329
+ value: void 0,
330
+ done: true
331
+ });
332
+ }
333
+ return new Promise((resolve2, reject) => {
334
+ this._waiters.push({
335
+ resolve: (event) => resolve2({ value: event, done: false }),
336
+ reject
337
+ });
338
+ });
339
+ }
340
+ };
341
+ }
342
+ };
343
+
344
+ // src/core/room.ts
345
+ var ALL_CATEGORIES = /* @__PURE__ */ new Set([
346
+ EventCategory.MESSAGE,
347
+ EventCategory.PRESENCE,
348
+ EventCategory.ACTIVITY,
349
+ EventCategory.MENTION
350
+ ]);
351
+ var RESERVED_MENTION_TOKENS = /* @__PURE__ */ new Set(["all"]);
352
+ var Room = class {
353
+ roomId;
354
+ /** Direct access to the underlying storage. Useful for bulk reads. */
355
+ storage;
356
+ _channels = /* @__PURE__ */ new Map();
357
+ _participants = /* @__PURE__ */ new Map();
358
+ _observers = /* @__PURE__ */ new Set();
359
+ _nextObserverId = 0;
360
+ /**
361
+ * @param roomId — stable identifier for this room (e.g. a UUID or slug)
362
+ * @param storage — storage backend; defaults to `InMemoryStorage`
363
+ */
364
+ constructor(roomId, storage) {
365
+ this.roomId = roomId;
366
+ this.storage = storage ?? new InMemoryStorage();
367
+ }
368
+ /**
369
+ * Connect a participant and return their channel.
370
+ */
371
+ async connect(participantId, name, options) {
372
+ const type = options?.type ?? "human";
373
+ const identifier = options?.identifier;
374
+ const subscribe = options?.subscribe;
375
+ const silent = options?.silent ?? false;
376
+ const authority = options?.authority;
377
+ if (RESERVED_MENTION_TOKENS.has(name.toLowerCase()) || identifier && RESERVED_MENTION_TOKENS.has(identifier.toLowerCase())) {
378
+ throw new Error(`Name or identifier cannot be a reserved mention token: ${RESERVED_MENTION_TOKENS}`);
379
+ }
380
+ const participant = {
381
+ id: participantId,
382
+ name,
383
+ status: "online",
384
+ type,
385
+ ...identifier ? { identifier } : {},
386
+ ...authority ? { authority } : {}
387
+ };
388
+ this._participants.set(participantId, participant);
389
+ const existingChannel = this._channels.get(participantId);
390
+ if (existingChannel) {
391
+ existingChannel._markDisconnected();
392
+ }
393
+ const subscriptions = subscribe ?? new Set(ALL_CATEGORIES);
394
+ const channel = new Channel(this, participantId, name, subscriptions);
395
+ this._channels.set(participantId, channel);
396
+ if (!silent) {
397
+ const event = createEvent({
398
+ type: "ParticipantJoined",
399
+ category: "PRESENCE",
400
+ room_id: this.roomId,
401
+ participant_id: participantId,
402
+ participant
403
+ });
404
+ await this._storeAndBroadcast(event, participantId);
405
+ }
406
+ return channel;
407
+ }
408
+ /**
409
+ * Observe all room events without being a participant.
410
+ *
411
+ * Returns a channel that receives every event — broadcasts AND targeted
412
+ * @mention events directed at other participants. Observers do NOT appear
413
+ * in `listParticipants()` and do not emit join/leave presence events,
414
+ * since they are not participants.
415
+ *
416
+ * Disconnect via `observer.disconnect()` when done.
417
+ *
418
+ * @example
419
+ * const observer = room.observe();
420
+ * for await (const event of observer) {
421
+ * // sees everything, including mentions for other participants
422
+ * }
423
+ */
424
+ observe() {
425
+ const id = `__obs_${this.roomId}_${this._nextObserverId++}`;
426
+ const channel = new Channel(this, id, "__observer__", new Set(ALL_CATEGORIES));
427
+ this._observers.add(channel);
428
+ return channel;
429
+ }
430
+ // ── Read methods ───────────────────────────────────────────────────────────
431
+ /**
432
+ * Paginate messages, newest-first. Pass the returned `next_cursor` to get
433
+ * the next (older) page.
434
+ */
435
+ async listMessages(limit = 30, cursor = null) {
436
+ return this.storage.getMessages(this.roomId, limit, cursor);
437
+ }
438
+ /**
439
+ * Full-text search across message content, newest-first.
440
+ * `query` is matched case-insensitively against message content.
441
+ */
442
+ async searchMessages(query, limit = 10, cursor = null) {
443
+ return this.storage.searchMessages(this.roomId, query, limit, cursor);
444
+ }
445
+ /** All currently connected participants (including agents). Observers excluded. */
446
+ listParticipants() {
447
+ return [...this._participants.values()];
448
+ }
449
+ /**
450
+ * Paginate room events, newest-first.
451
+ * `category` optionally filters to one EventCategory.
452
+ */
453
+ async listEvents(category = null, limit = 50, cursor = null) {
454
+ return this.storage.getEvents(this.roomId, category, limit, cursor);
455
+ }
456
+ /** Look up a single message by ID. Returns null if not found. */
457
+ async getMessage(id) {
458
+ return this.storage.getMessage(this.roomId, id);
459
+ }
460
+ /** Update a participant's authority level at runtime. */
461
+ setParticipantAuthority(participantId, authority) {
462
+ const participant = this._participants.get(participantId);
463
+ if (!participant) return false;
464
+ participant.authority = authority;
465
+ return true;
466
+ }
467
+ // ── Internal methods (called by Channel) ──────────────────────────────────
468
+ /**
469
+ * @internal
470
+ * Store a message, broadcast MessageSentEvent, and fire MentionedEvents.
471
+ *
472
+ * @mention scanning: looks for `@token` patterns in content and matches
473
+ * against each connected participant's `identifier` and display `name`
474
+ * (case-insensitive). Fires a `MentionedEvent` for each match, delivered
475
+ * to the mentioned participant AND all observers.
476
+ */
477
+ async _handleMessage(message) {
478
+ await this.storage.addMessage(message);
479
+ const event = createEvent({
480
+ type: "MessageSent",
481
+ category: "MESSAGE",
482
+ room_id: this.roomId,
483
+ participant_id: message.sender_id,
484
+ message
485
+ });
486
+ await this._storeAndBroadcast(event);
487
+ const mentions = this._detectMentions(message.content);
488
+ for (const mentionedId of mentions) {
489
+ const ch = this._channels.get(mentionedId);
490
+ if (ch) {
491
+ const mentionEvent = createEvent({
492
+ type: "Mentioned",
493
+ category: "MENTION",
494
+ room_id: this.roomId,
495
+ participant_id: mentionedId,
496
+ message
497
+ });
498
+ await this.storage.addEvent(mentionEvent);
499
+ ch._deliver(mentionEvent);
500
+ for (const observer of this._observers) {
501
+ observer._deliver(mentionEvent);
502
+ }
503
+ }
504
+ }
505
+ }
506
+ /**
507
+ * @internal
508
+ * Send a ping to a participant. Creates a PingedEvent delivered to the
509
+ * target's channel and all observers (same routing as MentionedEvent).
510
+ */
511
+ async _handlePing(senderId, senderName, targetId) {
512
+ const ch = this._channels.get(targetId);
513
+ if (!ch) return;
514
+ const event = createEvent({
515
+ type: "Pinged",
516
+ category: "MENTION",
517
+ room_id: this.roomId,
518
+ participant_id: targetId,
519
+ pinger_id: senderId,
520
+ pinger_name: senderName
521
+ });
522
+ await this.storage.addEvent(event);
523
+ ch._deliver(event);
524
+ for (const observer of this._observers) {
525
+ observer._deliver(event);
526
+ }
527
+ }
528
+ /** @internal Store and broadcast an activity event. */
529
+ async _handleEvent(event) {
530
+ await this._storeAndBroadcast(event, event.participant_id);
531
+ }
532
+ /** @internal Remove a channel and optionally broadcast ParticipantLeftEvent. */
533
+ async _disconnectChannel(channel, silent = false) {
534
+ if (this._observers.delete(channel)) {
535
+ return;
536
+ }
537
+ const pid = channel.participantId;
538
+ const participant = this._participants.get(pid);
539
+ this._channels.delete(pid);
540
+ this._participants.delete(pid);
541
+ if (!silent && participant) {
542
+ const event = createEvent({
543
+ type: "ParticipantLeft",
544
+ category: "PRESENCE",
545
+ room_id: this.roomId,
546
+ participant_id: pid,
547
+ participant
548
+ });
549
+ await this._storeAndBroadcast(event);
550
+ }
551
+ }
552
+ async _storeAndBroadcast(event, exclude) {
553
+ await this.storage.addEvent(event);
554
+ this._broadcast(event, exclude);
555
+ }
556
+ _broadcast(event, exclude) {
557
+ for (const [pid, channel] of this._channels) {
558
+ if (pid !== exclude) {
559
+ channel._deliver(event);
560
+ }
561
+ }
562
+ for (const observer of this._observers) {
563
+ observer._deliver(event);
564
+ }
565
+ }
566
+ /**
567
+ * Scan message content for `@token` patterns and return matching participant IDs.
568
+ * Matches against both `identifier` (e.g. `@my-agent`) and display `name` (e.g. `@Alice`).
569
+ * Case-insensitive. Deduplicates — each participant appears at most once.
570
+ */
571
+ _detectMentions(content) {
572
+ const seen = /* @__PURE__ */ new Set();
573
+ const mentionedIds = [];
574
+ const pattern = /@([a-zA-Z0-9_-]+)/g;
575
+ let match;
576
+ while ((match = pattern.exec(content)) !== null) {
577
+ const token = match[1].toLowerCase();
578
+ if (token === "all") {
579
+ for (const [pid] of this._participants) {
580
+ if (!seen.has(pid)) {
581
+ seen.add(pid);
582
+ mentionedIds.push(pid);
583
+ }
584
+ }
585
+ continue;
586
+ }
587
+ for (const [pid, participant] of this._participants) {
588
+ const matchesId = participant.identifier?.toLowerCase() === token;
589
+ const matchesName = participant.name.toLowerCase() === token;
590
+ if ((matchesId || matchesName) && !seen.has(pid)) {
591
+ seen.add(pid);
592
+ mentionedIds.push(pid);
593
+ }
594
+ }
595
+ }
596
+ return mentionedIds;
597
+ }
598
+ };
599
+
600
+ export {
601
+ InMemoryStorage,
602
+ FileBackedStorage,
603
+ Channel,
604
+ Room
605
+ };
606
+ //# sourceMappingURL=chunk-PEB6HEFT.js.map