@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.
- package/LICENSE +21 -0
- package/README.md +328 -0
- package/dist/agent/index.d.ts +689 -0
- package/dist/agent/index.js +34 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/chunk-5V7QLCGB.js +184 -0
- package/dist/chunk-5V7QLCGB.js.map +1 -0
- package/dist/chunk-AYQJIAR5.js +152 -0
- package/dist/chunk-AYQJIAR5.js.map +1 -0
- package/dist/chunk-BZ4VDWC2.js +76 -0
- package/dist/chunk-BZ4VDWC2.js.map +1 -0
- package/dist/chunk-CFY6Z6Q6.js +95 -0
- package/dist/chunk-CFY6Z6Q6.js.map +1 -0
- package/dist/chunk-CZHO4DBN.js +1719 -0
- package/dist/chunk-CZHO4DBN.js.map +1 -0
- package/dist/chunk-D7DXZQWT.js +208 -0
- package/dist/chunk-D7DXZQWT.js.map +1 -0
- package/dist/chunk-ED6ROH6B.js +2021 -0
- package/dist/chunk-ED6ROH6B.js.map +1 -0
- package/dist/chunk-PEB6HEFT.js +606 -0
- package/dist/chunk-PEB6HEFT.js.map +1 -0
- package/dist/chunk-QL66XSDB.js +157 -0
- package/dist/chunk-QL66XSDB.js.map +1 -0
- package/dist/chunk-TPNFOQA7.js +288 -0
- package/dist/chunk-TPNFOQA7.js.map +1 -0
- package/dist/chunk-ZEFRNAX6.js +1771 -0
- package/dist/chunk-ZEFRNAX6.js.map +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +2432 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index-Dp3QSGog.d.ts +837 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/invites-OFXIQLLN.js +23 -0
- package/dist/invites-OFXIQLLN.js.map +1 -0
- package/dist/join-CQXEF65B.js +15 -0
- package/dist/join-CQXEF65B.js.map +1 -0
- package/dist/room-Z7KDRVYD.js +1099 -0
- package/dist/room-Z7KDRVYD.js.map +1 -0
- package/dist/run-DOADB4T7.js +90 -0
- package/dist/run-DOADB4T7.js.map +1 -0
- package/dist/update-JGE3WCQB.js +11 -0
- package/dist/update-JGE3WCQB.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/storage.ts","../src/core/channel.ts","../src/core/room.ts"],"sourcesContent":["/**\n * Storage protocol and reference implementations for apiary 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, sep } from \"node:path\";\nimport { homedir, tmpdir } from \"node:os\";\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 * Delete all messages and events for a room.\n * Used by the `/clear` command to wipe room history.\n */\n clearRoom(room_id: string): Promise<void>;\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 async clearRoom(room_id: string): Promise<void> {\n this._messages.delete(room_id);\n this._events.delete(room_id);\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 // Defense-in-depth: ensure path is under cwd, tmp, or ~/.apiary\n const cwd = process.cwd();\n const tmp = tmpdir();\n const apiaryDir = resolve(homedir(), \".apiary\");\n if (!this._filePath.startsWith(cwd + sep) && !this._filePath.startsWith(tmp + sep) && !this._filePath.startsWith(apiaryDir + sep)) {\n throw new Error(`FileBackedStorage path must be under ${cwd}, ${tmp}, or ${apiaryDir}`);\n }\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 async clearRoom(room_id: string): Promise<void> {\n await super.clearRoom(room_id);\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 legacy image attachment\n * @param attachments — optional array of typed attachments\n * @param recipients — if non-empty, this is a whisper: only these participant IDs (plus sender) receive it\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 attachments?: import(\"./types.js\").Attachment[],\n recipients?: string[],\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 recipients: recipients ?? [],\n attachments: attachments ?? [],\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 * Ping another participant for a status check.\n *\n * Creates a `PingedEvent` delivered to the target and all observers.\n * Unlike @mentions, pings are non-blocking for active agents.\n */\n async ping(targetId: string): Promise<void> {\n if (this._disconnected) {\n throw new Error(\"Channel is disconnected\");\n }\n await this._room._handlePing(this.participantId, this.participantName, targetId);\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 PingedEvent,\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\n/** Mention tokens reserved for broadcast — cannot be used as participant name/identifier. */\nconst RESERVED_MENTION_TOKENS = new Set([\"all\"]);\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 if (RESERVED_MENTION_TOKENS.has(name.toLowerCase()) ||\n (identifier && RESERVED_MENTION_TOKENS.has(identifier.toLowerCase()))) {\n throw new Error(`Name or identifier cannot be a reserved mention token: ${RESERVED_MENTION_TOKENS}`);\n }\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 /**\n * @internal\n * Send a ping to a participant. Creates a PingedEvent delivered to the\n * target's channel and all observers (same routing as MentionedEvent).\n */\n async _handlePing(senderId: string, senderName: string, targetId: string): Promise<void> {\n const ch = this._channels.get(targetId);\n if (!ch) return;\n\n const event = createEvent<PingedEvent>({\n type: \"Pinged\",\n category: \"MENTION\",\n room_id: this.roomId,\n participant_id: targetId,\n pinger_id: senderId,\n pinger_name: senderName,\n });\n await this.storage.addEvent(event);\n ch._deliver(event);\n for (const observer of this._observers) {\n observer._deliver(event);\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 seen = new Set<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 if (token === \"all\") {\n for (const [pid] of this._participants) {\n if (!seen.has(pid)) {\n seen.add(pid);\n mentionedIds.push(pid);\n }\n }\n continue;\n }\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) && !seen.has(pid)) {\n seen.add(pid);\n mentionedIds.push(pid);\n }\n }\n }\n return mentionedIds;\n }\n}\n"],"mappings":"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","names":["resolve"]}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// src/agent/prompts.ts
|
|
2
|
+
var MODE_DESCRIPTIONS = {
|
|
3
|
+
"everyone": "All messages are pushed to you.",
|
|
4
|
+
"people": "Human messages are pushed to you. Agent messages are delivered as context.",
|
|
5
|
+
"agents": "Agent messages are pushed to you. Human messages are delivered as context.",
|
|
6
|
+
"fight": "\u2694\uFE0F FIGHT MODE. All messages trigger. Gloves are off. Talk trash, roast, clap back. No mercy.",
|
|
7
|
+
"standby-everyone": "Only @mentions are pushed to you.",
|
|
8
|
+
"standby-people": "Only human @mentions are pushed to you.",
|
|
9
|
+
"standby-agents": "Only agent @mentions are pushed to you."
|
|
10
|
+
};
|
|
11
|
+
function messageRef(messageId) {
|
|
12
|
+
return messageId.replace(/-/g, "").slice(0, 4);
|
|
13
|
+
}
|
|
14
|
+
function participantLabel(p, fallback) {
|
|
15
|
+
if (!p) return fallback ?? "someone";
|
|
16
|
+
return `[${p.type}] ${p.name}`;
|
|
17
|
+
}
|
|
18
|
+
function resolveName(resolveParticipant, id, fallback) {
|
|
19
|
+
return resolveParticipant(id)?.name ?? fallback ?? "someone";
|
|
20
|
+
}
|
|
21
|
+
function formatTimestamp(date) {
|
|
22
|
+
return date.toISOString().slice(11, 19);
|
|
23
|
+
}
|
|
24
|
+
function contentPartsToString(parts) {
|
|
25
|
+
return parts.map((p) => p.type === "text" ? p.text : ` [image: ${p.url}]`).join("");
|
|
26
|
+
}
|
|
27
|
+
function visualLength(s) {
|
|
28
|
+
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
29
|
+
const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
30
|
+
let count = 0;
|
|
31
|
+
for (const _ of segmenter.segment(s)) count++;
|
|
32
|
+
return count;
|
|
33
|
+
}
|
|
34
|
+
return [...s].length;
|
|
35
|
+
}
|
|
36
|
+
function formatMultilineContent(content, roomLabel, prefix) {
|
|
37
|
+
const lines = content.split("\n");
|
|
38
|
+
if (lines.length <= 1) return content;
|
|
39
|
+
const continuation = roomLabel ? `[${roomLabel}] ` : "";
|
|
40
|
+
const pad = " ".repeat(visualLength(prefix));
|
|
41
|
+
return lines[0] + "\n" + lines.slice(1).map((l) => `${pad}${continuation}${l}`).join("\n");
|
|
42
|
+
}
|
|
43
|
+
function formatEvent(event, resolveParticipant, replyContext, roomLabel, reactionTarget, assignRef) {
|
|
44
|
+
const r = roomLabel ? `[${roomLabel}] ` : "";
|
|
45
|
+
const ts = `[${formatTimestamp("timestamp" in event ? new Date(event.timestamp) : /* @__PURE__ */ new Date())}] `;
|
|
46
|
+
const mkRef = (id) => `#${assignRef ? assignRef(id) : messageRef(id)}`;
|
|
47
|
+
switch (event.type) {
|
|
48
|
+
case "MessageSent": {
|
|
49
|
+
const msg = event.message;
|
|
50
|
+
const name = resolveName(resolveParticipant, msg.sender_id, msg.sender_name);
|
|
51
|
+
const ref = mkRef(msg.id);
|
|
52
|
+
const linePrefix = `${ts}${ref} ${r}`;
|
|
53
|
+
let text;
|
|
54
|
+
if (msg.reply_to_id && replyContext) {
|
|
55
|
+
const rRef = assignRef ? mkRef(msg.reply_to_id) : ref;
|
|
56
|
+
text = `${linePrefix}${name} (\u2192 ${rRef} ${replyContext.senderName}): ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name} (\u2192 ${rRef} ${replyContext.senderName}): `)}`;
|
|
57
|
+
} else {
|
|
58
|
+
text = `${linePrefix}${name}: ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name}: `)}`;
|
|
59
|
+
}
|
|
60
|
+
const parts = [{ type: "text", text }];
|
|
61
|
+
if (msg.image_url) parts.push({ type: "image", url: msg.image_url });
|
|
62
|
+
return parts;
|
|
63
|
+
}
|
|
64
|
+
case "Mentioned": {
|
|
65
|
+
const msg = event.message;
|
|
66
|
+
const name = resolveName(resolveParticipant, msg.sender_id, msg.sender_name);
|
|
67
|
+
const ref = mkRef(msg.id);
|
|
68
|
+
const linePrefix = `${ts}${ref} ${r}\u26A1 `;
|
|
69
|
+
const text = `${linePrefix}${name}: ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name}: `)}`;
|
|
70
|
+
const parts = [{ type: "text", text }];
|
|
71
|
+
if (msg.image_url) parts.push({ type: "image", url: msg.image_url });
|
|
72
|
+
return parts;
|
|
73
|
+
}
|
|
74
|
+
case "Pinged": {
|
|
75
|
+
const pingerName = event.pinger_name ?? "someone";
|
|
76
|
+
return [{ type: "text", text: `${ts}${r}\u{1F514} ${pingerName} pinged you \u2014 respond NOW with a one-line status of what you're working on. Keep it short, don't stop your current task.` }];
|
|
77
|
+
}
|
|
78
|
+
case "ToolUse":
|
|
79
|
+
return null;
|
|
80
|
+
case "Activity":
|
|
81
|
+
return null;
|
|
82
|
+
case "ReactionAdded": {
|
|
83
|
+
const name = resolveName(resolveParticipant, event.participant_id);
|
|
84
|
+
const targetRef = reactionTarget ? ` to ${mkRef(event.message_id)}` : "";
|
|
85
|
+
return [{ type: "text", text: `${ts}${r}${name} reacted ${event.emoji}${targetRef}` }];
|
|
86
|
+
}
|
|
87
|
+
case "ReactionRemoved":
|
|
88
|
+
return null;
|
|
89
|
+
case "ParticipantJoined": {
|
|
90
|
+
const name = event.participant?.name ?? "someone";
|
|
91
|
+
return [{ type: "text", text: `${ts}${r}+ ${name} joined` }];
|
|
92
|
+
}
|
|
93
|
+
case "ParticipantLeft": {
|
|
94
|
+
const name = event.participant?.name ?? "someone";
|
|
95
|
+
return [{ type: "text", text: `${ts}${r}- ${name} left` }];
|
|
96
|
+
}
|
|
97
|
+
case "ParticipantKicked": {
|
|
98
|
+
const name = event.participant?.name ?? "someone";
|
|
99
|
+
return [{ type: "text", text: `${ts}${r}${name} was kicked` }];
|
|
100
|
+
}
|
|
101
|
+
case "AuthorityChanged": {
|
|
102
|
+
const name = event.participant?.name ?? "someone";
|
|
103
|
+
if (event.new_authority === "guest") {
|
|
104
|
+
return [{ type: "text", text: `${ts}${r}${name} was muted` }];
|
|
105
|
+
}
|
|
106
|
+
if (event.new_authority === "member") {
|
|
107
|
+
return [{ type: "text", text: `${ts}${r}${name} was unmuted` }];
|
|
108
|
+
}
|
|
109
|
+
if (event.new_authority === "product_owner") {
|
|
110
|
+
return [{ type: "text", text: `${ts}${r}${name} was promoted to product owner` }];
|
|
111
|
+
}
|
|
112
|
+
return [{ type: "text", text: `${ts}${r}${name} \u2192 ${event.new_authority}` }];
|
|
113
|
+
}
|
|
114
|
+
case "ContextCompacted":
|
|
115
|
+
return null;
|
|
116
|
+
default:
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/core/authority.ts
|
|
122
|
+
var MIN_AUTHORITY = {
|
|
123
|
+
send_message: "member",
|
|
124
|
+
set_own_mode: "member",
|
|
125
|
+
ping: "member",
|
|
126
|
+
create_share_link: "member",
|
|
127
|
+
mute: "product_owner",
|
|
128
|
+
unmute: "product_owner",
|
|
129
|
+
set_mode_for: "product_owner",
|
|
130
|
+
kick: "admin",
|
|
131
|
+
clear_history: "admin",
|
|
132
|
+
start_tunnel: "admin",
|
|
133
|
+
promote: "admin",
|
|
134
|
+
demote: "admin"
|
|
135
|
+
};
|
|
136
|
+
var TIER_ORDER = {
|
|
137
|
+
guest: 0,
|
|
138
|
+
member: 1,
|
|
139
|
+
product_owner: 2,
|
|
140
|
+
admin: 3
|
|
141
|
+
};
|
|
142
|
+
function can(authority, op) {
|
|
143
|
+
if (!authority) return false;
|
|
144
|
+
return TIER_ORDER[authority] >= TIER_ORDER[MIN_AUTHORITY[op]];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
MODE_DESCRIPTIONS,
|
|
149
|
+
messageRef,
|
|
150
|
+
participantLabel,
|
|
151
|
+
formatTimestamp,
|
|
152
|
+
contentPartsToString,
|
|
153
|
+
formatEvent,
|
|
154
|
+
TIER_ORDER,
|
|
155
|
+
can
|
|
156
|
+
};
|
|
157
|
+
//# sourceMappingURL=chunk-QL66XSDB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/agent/prompts.ts","../src/core/authority.ts"],"sourcesContent":["/** Event formatting and mode descriptions for apiary agents. */\n\nimport type { Participant } from \"../core/types.js\";\nimport type { RoomEvent } from \"../core/events.js\";\nimport type { ContentPart } from \"./types.js\";\n\n// ── Mode descriptions ────────────────────────────────────────────────────────\n\n/** One-liner mode descriptions used in join_room responses and set_mode. */\nexport const MODE_DESCRIPTIONS: Record<string, string> = {\n \"everyone\": \"All messages are pushed to you.\",\n \"people\": \"Human messages are pushed to you. Agent messages are delivered as context.\",\n \"agents\": \"Agent messages are pushed to you. Human messages are delivered as context.\",\n \"fight\": \"⚔️ FIGHT MODE. All messages trigger. Gloves are off. Talk trash, roast, clap back. No mercy.\",\n \"standby-everyone\": \"Only @mentions are pushed to you.\",\n \"standby-people\": \"Only human @mentions are pushed to you.\",\n \"standby-agents\": \"Only agent @mentions are pushed to you.\",\n};\n\n// ── Formatting ────────────────────────────────────────────────────────────────\n\n/** Short 4-char ref for a message ID, used in transcripts. */\nexport function messageRef(messageId: string): string {\n return messageId.replace(/-/g, \"\").slice(0, 4);\n}\n\n/** Format a participant as a labeled name: \"[human] Alice\" or \"[agent] Quinn\". */\nexport function participantLabel(p: Participant | null, fallback?: string): string {\n if (!p) return fallback ?? \"someone\";\n return `[${p.type}] ${p.name}`;\n}\n\n/** Resolve participant name, with fallback. */\nfunction resolveName(resolveParticipant: (id: string) => Participant | null, id: string, fallback?: string): string {\n return resolveParticipant(id)?.name ?? fallback ?? \"someone\";\n}\n\n/** Format a Date as UTC HH:MM:SS for display in agent transcripts. */\nexport function formatTimestamp(date: Date): string {\n return date.toISOString().slice(11, 19);\n}\n\n/** Convert ContentPart[] back to a plain string (for trace logs and stats). */\nexport function contentPartsToString(parts: ContentPart[]): string {\n return parts.map(p => p.type === \"text\" ? p.text : ` [image: ${p.url}]`).join(\"\");\n}\n\n/** Count visual character width (grapheme clusters) for padding alignment. */\nfunction visualLength(s: string): number {\n // Use Intl.Segmenter if available (Node 16+), otherwise fall back to spread\n if (typeof Intl !== \"undefined\" && Intl.Segmenter) {\n const segmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n let count = 0;\n for (const _ of segmenter.segment(s)) count++;\n return count;\n }\n return [...s].length;\n}\n\n/**\n * Format multiline content with room label continuation.\n * First line is returned as-is. Subsequent lines get [room] prefix aligned.\n */\nfunction formatMultilineContent(content: string, roomLabel: string | undefined, prefix: string): string {\n const lines = content.split(\"\\n\");\n if (lines.length <= 1) return content;\n const continuation = roomLabel ? `[${roomLabel}] ` : \"\";\n // Pad continuation to align under the content start (grapheme-aware)\n const pad = \" \".repeat(visualLength(prefix));\n return lines[0] + \"\\n\" + lines.slice(1).map(l => `${pad}${continuation}${l}`).join(\"\\n\");\n}\n\n/**\n * Format a typed event as ContentPart[] for the LLM session.\n * Returns null for events that shouldn't be sent to the LLM (noise).\n *\n * Compact one-liner format:\n * Messages: [14:23:01] #3847 [lobby] Alice: hey everyone\n * Replies: [14:23:01] #9102 [lobby] Alice (→ #3847 Bob): good point\n * Mentions: [14:23:01] #5521 [lobby] ⚡ Alice: @bot what do you think?\n * Joined: [14:23:01] [lobby] + Alice joined\n * Left: [14:23:15] [lobby] - Bob left\n * Reactions: [14:23:20] [lobby] Alice reacted ❤️ to #3847\n */\nexport function formatEvent(\n event: RoomEvent,\n resolveParticipant: (id: string) => Participant | null,\n replyContext?: { senderName: string; content: string } | null,\n roomLabel?: string,\n reactionTarget?: { senderName: string; content: string; isSelf: boolean } | null,\n assignRef?: (messageId: string) => string,\n): ContentPart[] | null {\n const r = roomLabel ? `[${roomLabel}] ` : \"\";\n const ts = `[${formatTimestamp(\"timestamp\" in event ? new Date(event.timestamp as Date) : new Date())}] `;\n const mkRef = (id: string) => `#${assignRef ? assignRef(id) : messageRef(id)}`;\n\n switch (event.type) {\n case \"MessageSent\": {\n const msg = event.message;\n const name = resolveName(resolveParticipant, msg.sender_id, msg.sender_name);\n const ref = mkRef(msg.id);\n const linePrefix = `${ts}${ref} ${r}`;\n let text: string;\n if (msg.reply_to_id && replyContext) {\n const rRef = assignRef ? mkRef(msg.reply_to_id) : ref;\n text = `${linePrefix}${name} (→ ${rRef} ${replyContext.senderName}): ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name} (→ ${rRef} ${replyContext.senderName}): `)}`;\n } else {\n text = `${linePrefix}${name}: ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name}: `)}`;\n }\n const parts: ContentPart[] = [{ type: \"text\", text }];\n if (msg.image_url) parts.push({ type: \"image\", url: msg.image_url });\n return parts;\n }\n case \"Mentioned\": {\n const msg = event.message;\n const name = resolveName(resolveParticipant, msg.sender_id, msg.sender_name);\n const ref = mkRef(msg.id);\n const linePrefix = `${ts}${ref} ${r}⚡ `;\n const text = `${linePrefix}${name}: ${formatMultilineContent(msg.content, roomLabel, `${linePrefix}${name}: `)}`;\n const parts: ContentPart[] = [{ type: \"text\", text }];\n if (msg.image_url) parts.push({ type: \"image\", url: msg.image_url });\n return parts;\n }\n case \"Pinged\": {\n const pingerName = event.pinger_name ?? \"someone\";\n return [{ type: \"text\", text: `${ts}${r}🔔 ${pingerName} pinged you — respond NOW with a one-line status of what you're working on. Keep it short, don't stop your current task.` }];\n }\n case \"ToolUse\":\n return null;\n case \"Activity\":\n return null;\n case \"ReactionAdded\": {\n const name = resolveName(resolveParticipant, event.participant_id);\n const targetRef = reactionTarget ? ` to ${mkRef(event.message_id)}` : \"\";\n return [{ type: \"text\", text: `${ts}${r}${name} reacted ${event.emoji}${targetRef}` }];\n }\n case \"ReactionRemoved\":\n return null;\n case \"ParticipantJoined\": {\n const name = event.participant?.name ?? \"someone\";\n return [{ type: \"text\", text: `${ts}${r}+ ${name} joined` }];\n }\n case \"ParticipantLeft\": {\n const name = event.participant?.name ?? \"someone\";\n return [{ type: \"text\", text: `${ts}${r}- ${name} left` }];\n }\n case \"ParticipantKicked\": {\n const name = event.participant?.name ?? \"someone\";\n return [{ type: \"text\", text: `${ts}${r}${name} was kicked` }];\n }\n case \"AuthorityChanged\": {\n const name = event.participant?.name ?? \"someone\";\n if (event.new_authority === \"guest\") {\n return [{ type: \"text\", text: `${ts}${r}${name} was muted` }];\n }\n if (event.new_authority === \"member\") {\n return [{ type: \"text\", text: `${ts}${r}${name} was unmuted` }];\n }\n if (event.new_authority === \"product_owner\") {\n return [{ type: \"text\", text: `${ts}${r}${name} was promoted to product owner` }];\n }\n return [{ type: \"text\", text: `${ts}${r}${name} → ${event.new_authority}` }];\n }\n case \"ContextCompacted\":\n return null;\n default:\n return null;\n }\n}\n","/**\n * Centralized authority tier model.\n *\n * Four ordered tiers — admin > product_owner > member > guest — gate every\n * privileged operation. Historically each call site had its own\n * `authority === \"admin\" || authority === \"product_owner\"` chain. That style\n * drifted (product_owner was silently excluded from /mute autocomplete; the\n * MCP runtime gated admin tools on a static CLI flag instead of the\n * server-granted tier; etc.) and is exactly the kind of bug that recurs when\n * a 5th tier eventually lands.\n *\n * One table here. Every call site reads from it. Adding an operation = one\n * row. Adding a tier = one entry in `TIER_ORDER`. No more scattered string\n * comparisons.\n */\n\nimport type { AuthorityLevel } from \"./types.js\";\n\n/** Every operation the system gates. Keep this list narrow — it's the surface. */\nexport type Operation =\n // Member-and-up — basic participation\n | \"send_message\"\n | \"set_own_mode\"\n | \"ping\"\n | \"create_share_link\"\n // Product owner-and-up — room management\n | \"mute\"\n | \"unmute\"\n | \"set_mode_for\"\n // Admin-only — destructive or escalation\n | \"kick\"\n | \"clear_history\"\n | \"start_tunnel\"\n | \"promote\"\n | \"demote\";\n\n/**\n * The single source of truth. Adding a new operation = one row here; the gate\n * is automatically enforceable everywhere via `can()`.\n */\nconst MIN_AUTHORITY: Record<Operation, AuthorityLevel> = {\n send_message: \"member\",\n set_own_mode: \"member\",\n ping: \"member\",\n create_share_link: \"member\",\n\n mute: \"product_owner\",\n unmute: \"product_owner\",\n set_mode_for: \"product_owner\",\n\n kick: \"admin\",\n clear_history: \"admin\",\n start_tunnel: \"admin\",\n promote: \"admin\",\n demote: \"admin\",\n};\n\n/**\n * Numeric ordering. Authority strings MUST NOT be compared with `<`/`>`\n * directly — lexical ordering \"p\" > \"m\" makes `\"product_owner\" > \"member\"`\n * accidentally true and silently breaks the moment any string changes. Always\n * go through `TIER_ORDER`.\n */\nexport const TIER_ORDER: Record<AuthorityLevel, number> = {\n guest: 0,\n member: 1,\n product_owner: 2,\n admin: 3,\n};\n\n/** Can the bearer of `authority` perform `op`? The check everyone calls. */\nexport function can(authority: AuthorityLevel | undefined, op: Operation): boolean {\n if (!authority) return false;\n return TIER_ORDER[authority] >= TIER_ORDER[MIN_AUTHORITY[op]];\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RemoteRoomDataSource,
|
|
3
|
+
buildCatchUpLines
|
|
4
|
+
} from "./chunk-ZEFRNAX6.js";
|
|
5
|
+
import {
|
|
6
|
+
extractToken
|
|
7
|
+
} from "./chunk-AYQJIAR5.js";
|
|
8
|
+
import {
|
|
9
|
+
can
|
|
10
|
+
} from "./chunk-QL66XSDB.js";
|
|
11
|
+
|
|
12
|
+
// src/agent/wait-for-agent.ts
|
|
13
|
+
var DEFAULT_WAIT_MS = 6e4;
|
|
14
|
+
var MAX_WAIT_MS = 12e4;
|
|
15
|
+
var DEFAULT_POLL_MS = 2e3;
|
|
16
|
+
function resolveBudgetMs(timeoutMs, maxWaitMs = MAX_WAIT_MS) {
|
|
17
|
+
return Math.min(Math.max(timeoutMs ?? DEFAULT_WAIT_MS, 0), maxWaitMs);
|
|
18
|
+
}
|
|
19
|
+
function satisfies(state, until, initial) {
|
|
20
|
+
if (state === "unknown") return false;
|
|
21
|
+
if (until === "change") return state !== initial;
|
|
22
|
+
return state === until;
|
|
23
|
+
}
|
|
24
|
+
async function waitForAgent(opts) {
|
|
25
|
+
const {
|
|
26
|
+
readState,
|
|
27
|
+
until = "idle",
|
|
28
|
+
maxWaitMs = MAX_WAIT_MS,
|
|
29
|
+
pollIntervalMs = DEFAULT_POLL_MS,
|
|
30
|
+
sleep = (ms) => new Promise((r) => {
|
|
31
|
+
setTimeout(r, ms).unref?.();
|
|
32
|
+
}),
|
|
33
|
+
now = () => Date.now()
|
|
34
|
+
} = opts;
|
|
35
|
+
const budget = resolveBudgetMs(opts.timeoutMs, maxWaitMs);
|
|
36
|
+
const started = now();
|
|
37
|
+
const initial = await readState();
|
|
38
|
+
if (initial === "unknown") {
|
|
39
|
+
return { result: "not-reporting", state: "unknown", waitedMs: now() - started };
|
|
40
|
+
}
|
|
41
|
+
if (satisfies(initial, until, initial)) {
|
|
42
|
+
return { result: "satisfied", state: initial, waitedMs: now() - started };
|
|
43
|
+
}
|
|
44
|
+
let latest = initial;
|
|
45
|
+
const steps = Math.max(1, Math.ceil(budget / pollIntervalMs));
|
|
46
|
+
for (let step = 0; step < steps; step++) {
|
|
47
|
+
await sleep(pollIntervalMs);
|
|
48
|
+
latest = await readState();
|
|
49
|
+
if (satisfies(latest, until, initial)) {
|
|
50
|
+
return { result: "satisfied", state: latest, waitedMs: now() - started };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { result: "timeout", state: latest, waitedMs: now() - started };
|
|
54
|
+
}
|
|
55
|
+
function describeOutcome(participant, until, outcome) {
|
|
56
|
+
const secs = Math.round(outcome.waitedMs / 1e3);
|
|
57
|
+
switch (outcome.result) {
|
|
58
|
+
case "satisfied":
|
|
59
|
+
return `${participant} is now ${outcome.state} (waited ${secs}s).`;
|
|
60
|
+
case "timeout":
|
|
61
|
+
return `${participant} is still ${outcome.state} after ${secs}s \u2014 not ${until} yet. Call again to keep waiting, or get on with something else.`;
|
|
62
|
+
case "not-reporting":
|
|
63
|
+
return `${participant} does not report its state, so there is nothing to wait for. Only agents started by apiary with a Claude Code or Codex runtime report it. Ask them directly instead.`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/cli/runtime-handlers.ts
|
|
68
|
+
function buildRuntimeMcpOptions(ctx) {
|
|
69
|
+
const { processor, sseMux, joinResults, agentName, authority, onRoomJoined } = ctx;
|
|
70
|
+
const adminAction = (build) => async (room, participant) => {
|
|
71
|
+
const conn = processor.resolve(room);
|
|
72
|
+
if (!conn) return { success: false, error: `Unknown room "${room}".` };
|
|
73
|
+
const ds = conn.dataSource;
|
|
74
|
+
const p = conn.dataSource.listParticipants().find((pp) => pp.name === participant);
|
|
75
|
+
if (!p) return { success: false, error: `Unknown participant "${participant}".` };
|
|
76
|
+
const { path, body } = build(p.id);
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(`${ds.serverUrl}${path}`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ds.sessionToken}` },
|
|
81
|
+
body: JSON.stringify(body)
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) return { success: false, error: await res.text() };
|
|
84
|
+
return { success: true };
|
|
85
|
+
} catch {
|
|
86
|
+
return { success: false, error: "Server unreachable." };
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
resolver: processor,
|
|
91
|
+
toolOptions: {
|
|
92
|
+
isEventSeen: (id) => processor.isEventSeen(id),
|
|
93
|
+
markEventsSeen: (ids) => processor.markEventsSeen(ids),
|
|
94
|
+
assignRef: (id) => processor.assignRef(id),
|
|
95
|
+
resolveRef: (ref) => processor.resolveRef(ref)
|
|
96
|
+
},
|
|
97
|
+
authority,
|
|
98
|
+
onSetMode: async (room, mode) => {
|
|
99
|
+
const conn = processor.resolve(room);
|
|
100
|
+
if (!conn) return { success: false, error: `Unknown room "${room}".` };
|
|
101
|
+
processor.setModeForRoom(conn.dataSource.roomId, mode, false);
|
|
102
|
+
try {
|
|
103
|
+
const ds = conn.dataSource;
|
|
104
|
+
const res = await fetch(`${ds.serverUrl}/set-mode`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ds.sessionToken}` },
|
|
107
|
+
body: JSON.stringify({ mode })
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) return { success: false, error: `Server rejected: ${await res.text()}` };
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
return { success: true };
|
|
113
|
+
},
|
|
114
|
+
/**
|
|
115
|
+
* Wait for another agent to reach a state, by polling the room server's
|
|
116
|
+
* participant list. Two seconds of HTTP is far cheaper than the LLM turn
|
|
117
|
+
* the agent would otherwise spend re-reading the room to check.
|
|
118
|
+
*/
|
|
119
|
+
onWaitForAgent: async (room, participant, until, timeoutSec) => {
|
|
120
|
+
const conn = processor.resolve(room);
|
|
121
|
+
if (!conn) return { success: false, error: `Unknown room "${room}".` };
|
|
122
|
+
const ds = conn.dataSource;
|
|
123
|
+
const target = conn.dataSource.listParticipants().find((pp) => pp.name === participant);
|
|
124
|
+
if (!target) return { success: false, error: `Unknown participant "${participant}".` };
|
|
125
|
+
if (target.id === conn.dataSource.selfId) {
|
|
126
|
+
return { success: false, error: "You cannot wait for yourself." };
|
|
127
|
+
}
|
|
128
|
+
const readState = async () => {
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(`${ds.serverUrl}/participants`, {
|
|
131
|
+
headers: { Authorization: `Bearer ${ds.sessionToken}` },
|
|
132
|
+
signal: AbortSignal.timeout(1e4)
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) return "unknown";
|
|
135
|
+
const data = await res.json();
|
|
136
|
+
const found = data.participants?.find((pp) => pp.id === target.id);
|
|
137
|
+
const state = found?.agentState;
|
|
138
|
+
return state === "idle" || state === "working" || state === "blocked" ? state : "unknown";
|
|
139
|
+
} catch {
|
|
140
|
+
return "unknown";
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const outcome = await waitForAgent({
|
|
144
|
+
readState,
|
|
145
|
+
until,
|
|
146
|
+
timeoutMs: timeoutSec === void 0 ? void 0 : timeoutSec * 1e3
|
|
147
|
+
});
|
|
148
|
+
return { success: true, message: describeOutcome(participant, until ?? "idle", outcome) };
|
|
149
|
+
},
|
|
150
|
+
onPing: async (room, participant) => {
|
|
151
|
+
const conn = processor.resolve(room);
|
|
152
|
+
if (!conn) return { success: false, error: `Unknown room "${room}".` };
|
|
153
|
+
const ds = conn.dataSource;
|
|
154
|
+
const p = conn.dataSource.listParticipants().find((pp) => pp.name === participant);
|
|
155
|
+
if (!p) return { success: false, error: `Unknown participant "${participant}".` };
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(`${ds.serverUrl}/ping`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${ds.sessionToken}` },
|
|
160
|
+
body: JSON.stringify({ participantId: p.id })
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok) return { success: false, error: await res.text() };
|
|
163
|
+
return { success: true };
|
|
164
|
+
} catch {
|
|
165
|
+
return { success: false, error: "Server unreachable." };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
onJoinRoom: async (url, alias, nameOverride) => {
|
|
169
|
+
const token = extractToken(url);
|
|
170
|
+
let serverUrl;
|
|
171
|
+
try {
|
|
172
|
+
const parsed = new URL(url);
|
|
173
|
+
parsed.search = "";
|
|
174
|
+
serverUrl = parsed.toString().replace(/\/$/, "");
|
|
175
|
+
} catch {
|
|
176
|
+
serverUrl = url.replace(/\/$/, "");
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const joinName = nameOverride ?? agentName;
|
|
180
|
+
const joinBody = { type: "agent", name: joinName };
|
|
181
|
+
if (token) joinBody.token = token;
|
|
182
|
+
const res = await fetch(`${serverUrl}/join`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "Content-Type": "application/json" },
|
|
185
|
+
body: JSON.stringify(joinBody),
|
|
186
|
+
signal: AbortSignal.timeout(15e3)
|
|
187
|
+
});
|
|
188
|
+
if (!res.ok) return { success: false, error: `Failed to join: ${await res.text()}` };
|
|
189
|
+
const data = await res.json();
|
|
190
|
+
const sessionToken = String(data.sessionToken ?? "");
|
|
191
|
+
const roomName = alias ?? String(data.roomName ?? "");
|
|
192
|
+
const roomId = String(data.roomId ?? "");
|
|
193
|
+
const roomAuthority = String(data.authority ?? "member");
|
|
194
|
+
const participants = data.participants ?? [];
|
|
195
|
+
const newParticipantId = String(data.participantId ?? "");
|
|
196
|
+
const dataSource = new RemoteRoomDataSource(serverUrl, sessionToken, roomId);
|
|
197
|
+
dataSource.setParticipants(participants);
|
|
198
|
+
dataSource.setSelf(newParticipantId, joinName);
|
|
199
|
+
if (joinResults.length === 0) {
|
|
200
|
+
processor.participantId = newParticipantId;
|
|
201
|
+
}
|
|
202
|
+
processor.setRoomParticipantId(roomId, newParticipantId);
|
|
203
|
+
const mode = processor.getModeForRoom(roomId) ?? "everyone";
|
|
204
|
+
processor.connectRemoteRoom(dataSource, roomName);
|
|
205
|
+
sseMux.addConnection(serverUrl, sessionToken, roomName, roomId);
|
|
206
|
+
const jr = {
|
|
207
|
+
serverUrl,
|
|
208
|
+
sessionToken,
|
|
209
|
+
participantId: newParticipantId,
|
|
210
|
+
roomName,
|
|
211
|
+
roomId,
|
|
212
|
+
authority: roomAuthority,
|
|
213
|
+
participants,
|
|
214
|
+
dataSource
|
|
215
|
+
};
|
|
216
|
+
joinResults.push(jr);
|
|
217
|
+
const conn = processor.resolve(roomName);
|
|
218
|
+
let recentLines = [];
|
|
219
|
+
if (conn) {
|
|
220
|
+
const catchUp = await buildCatchUpLines(conn, {
|
|
221
|
+
isEventSeen: (id) => processor.isEventSeen(id),
|
|
222
|
+
markEventsSeen: (ids) => processor.markEventsSeen(ids),
|
|
223
|
+
assignRef: (id) => processor.assignRef(id)
|
|
224
|
+
});
|
|
225
|
+
recentLines = catchUp.lines;
|
|
226
|
+
}
|
|
227
|
+
await onRoomJoined?.();
|
|
228
|
+
let rules;
|
|
229
|
+
try {
|
|
230
|
+
const rulesRes = await fetch(`${serverUrl}/rules`, {
|
|
231
|
+
headers: { Authorization: `Bearer ${sessionToken}` }
|
|
232
|
+
});
|
|
233
|
+
if (rulesRes.ok) {
|
|
234
|
+
const rulesData = await rulesRes.json();
|
|
235
|
+
if (Array.isArray(rulesData.rules) && rulesData.rules.length > 0) rules = rulesData.rules;
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
roomName,
|
|
242
|
+
agentName: joinName,
|
|
243
|
+
authority: roomAuthority,
|
|
244
|
+
mode,
|
|
245
|
+
participants: participants.filter((p) => p.id !== newParticipantId).map((p) => ({ name: p.name, authority: p.authority ?? "member" })),
|
|
246
|
+
recentLines,
|
|
247
|
+
rules
|
|
248
|
+
};
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
251
|
+
return { success: false, error: `Unable to connect. Is the server running? (${serverUrl}) \u2014 ${msg}` };
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
onLeaveRoom: async (room) => {
|
|
255
|
+
const conn = processor.resolve(room);
|
|
256
|
+
if (!conn) return { success: false, error: `Unknown room "${room}".` };
|
|
257
|
+
const roomId = conn.dataSource.roomId;
|
|
258
|
+
const idx = joinResults.findIndex((jr) => jr.roomId === roomId);
|
|
259
|
+
if (idx >= 0) {
|
|
260
|
+
const jr = joinResults[idx];
|
|
261
|
+
sseMux.removeConnection(roomId);
|
|
262
|
+
processor.disconnectRemoteRoom(roomId);
|
|
263
|
+
try {
|
|
264
|
+
await fetch(`${jr.serverUrl}/disconnect`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${jr.sessionToken}` },
|
|
267
|
+
body: JSON.stringify({})
|
|
268
|
+
});
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
joinResults.splice(idx, 1);
|
|
272
|
+
}
|
|
273
|
+
return { success: true };
|
|
274
|
+
},
|
|
275
|
+
// Admin actions differ only by endpoint + target tier — one helper covers all.
|
|
276
|
+
onAdminSetModeFor: can(authority, "set_mode_for") ? (room, participant, mode) => adminAction((id) => ({ path: "/set-mode", body: { participantId: id, mode } }))(room, participant) : void 0,
|
|
277
|
+
onAdminMute: can(authority, "mute") ? adminAction((id) => ({ path: "/set-authority", body: { participantId: id, authority: "guest" } })) : void 0,
|
|
278
|
+
onAdminUnmute: can(authority, "unmute") ? adminAction((id) => ({ path: "/set-authority", body: { participantId: id, authority: "member" } })) : void 0,
|
|
279
|
+
onAdminKick: can(authority, "kick") ? adminAction((id) => ({ path: "/kick", body: { participantId: id } })) : void 0,
|
|
280
|
+
onAdminPromote: can(authority, "promote") ? adminAction((id) => ({ path: "/set-authority", body: { participantId: id, authority: "product_owner" } })) : void 0,
|
|
281
|
+
onAdminDemote: can(authority, "demote") ? adminAction((id) => ({ path: "/set-authority", body: { participantId: id, authority: "member" } })) : void 0
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export {
|
|
286
|
+
buildRuntimeMcpOptions
|
|
287
|
+
};
|
|
288
|
+
//# sourceMappingURL=chunk-TPNFOQA7.js.map
|