@zooid/transport-matrix 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1076 @@
1
+ // src/matrix-client.ts
2
+ import { randomUUID } from "crypto";
3
+ var MatrixClient = class {
4
+ homeserver;
5
+ asToken;
6
+ fetch;
7
+ constructor(opts) {
8
+ this.homeserver = opts.homeserver.replace(/\/$/, "");
9
+ this.asToken = opts.asToken;
10
+ this.fetch = opts.fetch ?? globalThis.fetch;
11
+ }
12
+ async registerBot(localpart2) {
13
+ const r = await this.fetch(`${this.homeserver}/_matrix/client/v3/register`, {
14
+ method: "POST",
15
+ headers: { Authorization: `Bearer ${this.asToken}` },
16
+ body: JSON.stringify({ type: "m.login.application_service", username: localpart2 })
17
+ });
18
+ if (r.status === 200) return await r.json();
19
+ if (r.status === 400) {
20
+ const body = await r.json();
21
+ if (body.errcode === "M_USER_IN_USE") return void 0;
22
+ }
23
+ throw new Error(`registerBot(${localpart2}) failed: ${r.status}`);
24
+ }
25
+ async resolveAlias(alias) {
26
+ const r = await this.fetch(
27
+ `${this.homeserver}/_matrix/client/v3/directory/room/${encodeURIComponent(alias)}`,
28
+ { headers: { Authorization: `Bearer ${this.asToken}` } }
29
+ );
30
+ if (r.status === 404) return null;
31
+ if (!r.ok) throw new Error(`resolveAlias(${alias}) failed: ${r.status}`);
32
+ const j = await r.json();
33
+ return j.room_id;
34
+ }
35
+ async createRoom(opts) {
36
+ const body = {
37
+ room_alias_name: opts.roomAliasName,
38
+ invite: opts.invite,
39
+ preset: opts.preset ?? "public_chat"
40
+ };
41
+ if (opts.name !== void 0) body.name = opts.name;
42
+ const r = await this.fetch(
43
+ `${this.homeserver}/_matrix/client/v3/createRoom?user_id=${encodeURIComponent(opts.senderUserId)}`,
44
+ {
45
+ method: "POST",
46
+ headers: {
47
+ Authorization: `Bearer ${this.asToken}`,
48
+ "content-type": "application/json"
49
+ },
50
+ body: JSON.stringify(body)
51
+ }
52
+ );
53
+ if (!r.ok) {
54
+ const body2 = await r.text();
55
+ throw new Error(`createRoom(${opts.roomAliasName}) failed: ${r.status} ${body2}`);
56
+ }
57
+ const j = await r.json();
58
+ return j.room_id;
59
+ }
60
+ async createRoomRaw(opts) {
61
+ const url = `${this.homeserver}/_matrix/client/v3/createRoom?user_id=${encodeURIComponent(opts.asUserId)}`;
62
+ const r = await this.fetch(url, {
63
+ method: "POST",
64
+ headers: {
65
+ Authorization: `Bearer ${this.asToken}`,
66
+ "content-type": "application/json"
67
+ },
68
+ body: JSON.stringify(opts.body)
69
+ });
70
+ if (!r.ok) throw new Error(`createRoomRaw failed: ${r.status}`);
71
+ const j = await r.json();
72
+ return j.room_id;
73
+ }
74
+ async sendStateEvent(opts) {
75
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(opts.roomId)}/state/${encodeURIComponent(opts.eventType)}/${encodeURIComponent(opts.stateKey ?? "")}?user_id=${encodeURIComponent(opts.asUserId)}`;
76
+ const r = await this.fetch(url, {
77
+ method: "PUT",
78
+ headers: {
79
+ Authorization: `Bearer ${this.asToken}`,
80
+ "content-type": "application/json"
81
+ },
82
+ body: JSON.stringify(opts.content)
83
+ });
84
+ if (!r.ok) throw new Error(`sendStateEvent ${opts.eventType} failed: ${r.status}`);
85
+ return await r.json();
86
+ }
87
+ async joinRoom(roomIdOrAlias, asUserId) {
88
+ const url = `${this.homeserver}/_matrix/client/v3/join/${encodeURIComponent(roomIdOrAlias)}?user_id=${encodeURIComponent(asUserId)}`;
89
+ const r = await this.fetch(url, {
90
+ method: "POST",
91
+ headers: { Authorization: `Bearer ${this.asToken}` },
92
+ body: "{}"
93
+ });
94
+ if (!r.ok) throw new Error(`joinRoom(${roomIdOrAlias}) failed: ${r.status}`);
95
+ }
96
+ async sendMessage(input) {
97
+ const content = { ...input.content };
98
+ if (input.threadRoot) {
99
+ content["m.relates_to"] = { rel_type: "m.thread", event_id: input.threadRoot };
100
+ }
101
+ return this.sendEvent(input.roomId, input.asUserId, "m.room.message", content);
102
+ }
103
+ async sendCustomEvent(input) {
104
+ return this.sendEvent(input.roomId, input.asUserId, input.eventType, input.content);
105
+ }
106
+ async setTyping(input) {
107
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(input.roomId)}/typing/${encodeURIComponent(input.asUserId)}?user_id=${encodeURIComponent(input.asUserId)}`;
108
+ const body = { typing: input.typing };
109
+ if (input.typing && input.timeoutMs !== void 0) body.timeout = input.timeoutMs;
110
+ const r = await this.fetch(url, {
111
+ method: "PUT",
112
+ headers: { Authorization: `Bearer ${this.asToken}` },
113
+ body: JSON.stringify(body)
114
+ });
115
+ if (!r.ok) throw new Error(`setTyping(${input.roomId}, ${input.asUserId}) failed: ${r.status}`);
116
+ }
117
+ async setDisplayName(asUserId, displayName) {
118
+ const url = `${this.homeserver}/_matrix/client/v3/profile/${encodeURIComponent(asUserId)}/displayname?user_id=${encodeURIComponent(asUserId)}`;
119
+ const r = await this.fetch(url, {
120
+ method: "PUT",
121
+ headers: {
122
+ Authorization: `Bearer ${this.asToken}`,
123
+ "content-type": "application/json"
124
+ },
125
+ body: JSON.stringify({ displayname: displayName })
126
+ });
127
+ if (!r.ok) throw new Error(`setDisplayName(${asUserId}) failed: ${r.status}`);
128
+ }
129
+ async setPresence(input) {
130
+ const url = `${this.homeserver}/_matrix/client/v3/presence/${encodeURIComponent(input.asUserId)}/status?user_id=${encodeURIComponent(input.asUserId)}`;
131
+ const body = { presence: input.presence };
132
+ if (input.statusMsg !== void 0) body.status_msg = input.statusMsg;
133
+ const r = await this.fetch(url, {
134
+ method: "PUT",
135
+ headers: { Authorization: `Bearer ${this.asToken}` },
136
+ body: JSON.stringify(body)
137
+ });
138
+ if (!r.ok) throw new Error(`setPresence(${input.asUserId}) failed: ${r.status}`);
139
+ }
140
+ /**
141
+ * Fetch a single event from a room. Used to recover thread-root context
142
+ * after a daemon restart wipes the in-memory threadStates cache.
143
+ */
144
+ async fetchEvent(roomId, eventId, asUserId) {
145
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/event/${encodeURIComponent(eventId)}?user_id=${encodeURIComponent(asUserId)}`;
146
+ const r = await this.fetch(url, {
147
+ method: "GET",
148
+ headers: { Authorization: `Bearer ${this.asToken}` }
149
+ });
150
+ if (r.status === 404) return null;
151
+ if (!r.ok) throw new Error(`fetchEvent(${eventId}) failed: ${r.status}`);
152
+ return await r.json();
153
+ }
154
+ /**
155
+ * Fetch replies to a thread root via the relations endpoint, oldest-first.
156
+ * Pass `limit` and `from` for pagination; `next_batch` echoes back when
157
+ * there are more replies. Returns `{ chunk: [] }` when the root is unknown.
158
+ */
159
+ async fetchThreadRelations(opts) {
160
+ const params = new URLSearchParams({
161
+ dir: "f",
162
+ limit: String(opts.limit ?? 100),
163
+ user_id: opts.asUserId
164
+ });
165
+ if (opts.from) params.set("from", opts.from);
166
+ const url = `${this.homeserver}/_matrix/client/v1/rooms/${encodeURIComponent(opts.roomId)}/relations/${encodeURIComponent(opts.rootEventId)}/m.thread?${params.toString()}`;
167
+ const r = await this.fetch(url, {
168
+ method: "GET",
169
+ headers: { Authorization: `Bearer ${this.asToken}` }
170
+ });
171
+ if (r.status === 404) return { chunk: [] };
172
+ if (!r.ok) throw new Error(`fetchThreadRelations(${opts.rootEventId}) failed: ${r.status}`);
173
+ const body = await r.json();
174
+ return { chunk: body.chunk ?? [], next_batch: body.next_batch };
175
+ }
176
+ /**
177
+ * Paginate the room timeline. Returns events newest-first (dir=b) per Matrix
178
+ * spec. The caller is responsible for reversing if it wants oldest-first.
179
+ */
180
+ async fetchRoomMessages(opts) {
181
+ const params = new URLSearchParams({
182
+ dir: "b",
183
+ limit: String(opts.limit ?? 50),
184
+ user_id: opts.asUserId
185
+ });
186
+ if (opts.from) params.set("from", opts.from);
187
+ if (opts.filter) params.set("filter", JSON.stringify(opts.filter));
188
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(opts.roomId)}/messages?${params.toString()}`;
189
+ const r = await this.fetch(url, {
190
+ method: "GET",
191
+ headers: { Authorization: `Bearer ${this.asToken}` }
192
+ });
193
+ if (!r.ok) throw new Error(`fetchRoomMessages(${opts.roomId}) failed: ${r.status}`);
194
+ return await r.json();
195
+ }
196
+ async getJoinedMembers(roomId, asUserId) {
197
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/joined_members?user_id=${encodeURIComponent(asUserId)}`;
198
+ const r = await this.fetch(url, {
199
+ method: "GET",
200
+ headers: { Authorization: `Bearer ${this.asToken}` }
201
+ });
202
+ if (!r.ok) throw new Error(`getJoinedMembers(${roomId}) failed: ${r.status}`);
203
+ return await r.json();
204
+ }
205
+ async fetchRoomName(roomId, asUserId) {
206
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/state/m.room.name/?user_id=${encodeURIComponent(asUserId)}`;
207
+ const r = await this.fetch(url, {
208
+ method: "GET",
209
+ headers: { Authorization: `Bearer ${this.asToken}` }
210
+ });
211
+ if (r.status === 404) return null;
212
+ if (!r.ok) throw new Error(`fetchRoomName(${roomId}) failed: ${r.status}`);
213
+ const body = await r.json();
214
+ return body.name ?? null;
215
+ }
216
+ async sendEvent(roomId, asUserId, eventType, content) {
217
+ const txn = randomUUID();
218
+ const url = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/${eventType}/${txn}?user_id=${encodeURIComponent(asUserId)}`;
219
+ const r = await this.fetch(url, {
220
+ method: "PUT",
221
+ headers: { Authorization: `Bearer ${this.asToken}` },
222
+ body: JSON.stringify(content)
223
+ });
224
+ if (!r.ok) throw new Error(`sendEvent(${eventType}) failed: ${r.status}`);
225
+ return await r.json();
226
+ }
227
+ };
228
+
229
+ // src/context-provider.ts
230
+ var MatrixContextProvider = class {
231
+ constructor(opts) {
232
+ this.opts = opts;
233
+ }
234
+ async getRoomHistory(channelId, hopts) {
235
+ const { chunk, end } = await this.opts.client.fetchRoomMessages({
236
+ roomId: channelId,
237
+ asUserId: this.opts.asUserId,
238
+ limit: hopts.limit,
239
+ from: hopts.before,
240
+ filter: { types: ["m.room.message"] }
241
+ });
242
+ const messages = [];
243
+ for (let i = chunk.length - 1; i >= 0; i--) {
244
+ const ev = chunk[i];
245
+ const msg = this.toMessage(ev);
246
+ if (msg) messages.push(msg);
247
+ }
248
+ return {
249
+ messages,
250
+ next_before: end,
251
+ has_more: end !== void 0
252
+ };
253
+ }
254
+ async getRecentThreads(channelId, hopts) {
255
+ const { chunk, end } = await this.opts.client.fetchRoomMessages({
256
+ roomId: channelId,
257
+ asUserId: this.opts.asUserId,
258
+ limit: hopts.limit,
259
+ from: hopts.before,
260
+ filter: { types: ["m.room.message"], not_rel_types: ["m.thread"] }
261
+ });
262
+ const threads = [];
263
+ for (const ev of chunk) {
264
+ if (ev.type !== "m.room.message") continue;
265
+ if (ev.content?.msgtype !== "m.text" || typeof ev.content.body !== "string") continue;
266
+ const relatesTo = ev.content["m.relates_to"];
267
+ if (relatesTo?.rel_type === "m.thread") continue;
268
+ const agent = this.opts.agentBots.get(ev.sender);
269
+ const bundled = ev.unsigned?.["m.relations"]?.["m.thread"];
270
+ const replyCount = bundled?.count ?? 0;
271
+ const latestTs = bundled?.latest_event?.origin_server_ts ?? ev.origin_server_ts;
272
+ threads.push({
273
+ id: ev.event_id,
274
+ sender: ev.sender,
275
+ text: ev.content.body,
276
+ timestamp: new Date(ev.origin_server_ts).toISOString(),
277
+ is_agent: agent !== void 0,
278
+ ...agent !== void 0 ? { agent_name: agent } : {},
279
+ reply_count: replyCount,
280
+ last_activity_at: new Date(latestTs).toISOString()
281
+ });
282
+ }
283
+ return {
284
+ threads,
285
+ next_before: end,
286
+ has_more: end !== void 0
287
+ };
288
+ }
289
+ async getThreadHistory(channelId, threadId, hopts) {
290
+ const messages = [];
291
+ if (!hopts.before) {
292
+ const root = await this.opts.client.fetchEvent(
293
+ channelId,
294
+ threadId,
295
+ this.opts.asUserId
296
+ );
297
+ if (root) {
298
+ const rootMsg = this.toMessage(root);
299
+ if (rootMsg) messages.push({ ...rootMsg, thread_id: threadId });
300
+ }
301
+ }
302
+ const { chunk, next_batch } = await this.opts.client.fetchThreadRelations({
303
+ roomId: channelId,
304
+ rootEventId: threadId,
305
+ asUserId: this.opts.asUserId,
306
+ limit: hopts.limit,
307
+ from: hopts.before
308
+ });
309
+ for (const ev of chunk) {
310
+ const reply = this.toMessage(ev);
311
+ if (reply) messages.push({ ...reply, thread_id: threadId });
312
+ }
313
+ return {
314
+ messages,
315
+ next_before: next_batch,
316
+ has_more: next_batch !== void 0
317
+ };
318
+ }
319
+ toMessage(ev) {
320
+ if (ev.type !== "m.room.message") return null;
321
+ if (ev.content?.msgtype !== "m.text" || typeof ev.content.body !== "string") return null;
322
+ const agent = this.opts.agentBots.get(ev.sender);
323
+ const relatesTo = ev.content["m.relates_to"];
324
+ const threadId = relatesTo?.rel_type === "m.thread" && relatesTo.event_id ? relatesTo.event_id : void 0;
325
+ return {
326
+ id: ev.event_id,
327
+ sender: ev.sender,
328
+ text: ev.content.body,
329
+ timestamp: new Date(ev.origin_server_ts).toISOString(),
330
+ is_agent: agent !== void 0,
331
+ ...agent !== void 0 ? { agent_name: agent } : {},
332
+ ...threadId !== void 0 ? { thread_id: threadId } : {}
333
+ };
334
+ }
335
+ async getChannelMembers(channelId) {
336
+ const { joined } = await this.opts.client.getJoinedMembers(channelId, this.opts.asUserId);
337
+ return Object.entries(joined).map(([id, info]) => {
338
+ const agent = this.opts.agentBots.get(id);
339
+ return {
340
+ id,
341
+ name: info.display_name ?? id,
342
+ is_agent: agent !== void 0,
343
+ ...agent !== void 0 ? { agent_name: agent } : {}
344
+ };
345
+ });
346
+ }
347
+ async getChannelInfo(channelId) {
348
+ const name = await this.opts.client.fetchRoomName(channelId, this.opts.asUserId);
349
+ return {
350
+ id: channelId,
351
+ name: name ?? channelId,
352
+ transport: "matrix"
353
+ };
354
+ }
355
+ };
356
+
357
+ // src/registration.ts
358
+ import { stringify } from "yaml";
359
+ function renderRegistration(c) {
360
+ return stringify(
361
+ {
362
+ id: c.id,
363
+ url: c.url,
364
+ as_token: c.asToken,
365
+ hs_token: c.hsToken,
366
+ sender_localpart: c.senderLocalpart,
367
+ rate_limited: false,
368
+ namespaces: {
369
+ users: [{ exclusive: c.exclusive ?? true, regex: c.userNamespace }],
370
+ aliases: c.aliasNamespace ? [{ exclusive: c.exclusive ?? true, regex: c.aliasNamespace }] : [],
371
+ rooms: []
372
+ }
373
+ },
374
+ { defaultStringType: "PLAIN", defaultKeyType: "PLAIN", singleQuote: true }
375
+ );
376
+ }
377
+
378
+ // src/mentions.ts
379
+ var MATRIX_TO_RE = /https:\/\/matrix\.to\/#\/(@[^"<>\s]+)/g;
380
+ var RAW_USER_RE = /(@[A-Za-z0-9._\-=/+]+:[A-Za-z0-9.\-]+)/g;
381
+ function extractMentions(event) {
382
+ const out = /* @__PURE__ */ new Set();
383
+ const c = event.content ?? {};
384
+ for (const id of c["m.mentions"]?.user_ids ?? []) out.add(id);
385
+ if (c.formatted_body) {
386
+ for (const m of c.formatted_body.matchAll(MATRIX_TO_RE)) {
387
+ out.add(decodeURIComponent(m[1]));
388
+ }
389
+ }
390
+ if (c.body && out.size === 0) {
391
+ for (const m of c.body.matchAll(RAW_USER_RE)) out.add(m[1]);
392
+ }
393
+ return [...out];
394
+ }
395
+ function stripMention(body, userId) {
396
+ const escaped = userId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
397
+ const re = new RegExp(`\\s*${escaped}\\s*`, "g");
398
+ return body.replace(re, " ").replace(/\s+/g, " ").trim();
399
+ }
400
+
401
+ // src/router.ts
402
+ function inboundThreadRoot(event) {
403
+ const r = event.content?.["m.relates_to"];
404
+ return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
405
+ }
406
+ function route(event, agents, threadStates) {
407
+ if (event.type !== "m.room.message") return [];
408
+ if (!event.content?.msgtype) return [];
409
+ const mentions = new Set(extractMentions(event));
410
+ const matches = [];
411
+ const threadRoot = inboundThreadRoot(event);
412
+ const threadState = threadRoot ? threadStates?.get(threadRoot) : void 0;
413
+ for (const a of agents) {
414
+ if (event.sender === a.userId) continue;
415
+ if (!a.rooms.includes(event.room_id ?? "")) continue;
416
+ if (a.trigger === "any") {
417
+ matches.push(a);
418
+ continue;
419
+ }
420
+ if (mentions.has(a.userId)) {
421
+ matches.push(a);
422
+ continue;
423
+ }
424
+ if (threadState) {
425
+ const lastPoster = threadState.participants.at(-1);
426
+ if (lastPoster) {
427
+ if (lastPoster === a.name) matches.push(a);
428
+ } else if (threadState.rootMentions.includes(a.name)) {
429
+ matches.push(a);
430
+ }
431
+ }
432
+ }
433
+ return matches;
434
+ }
435
+
436
+ // src/space-provisioner.ts
437
+ async function ensureWorkforceSpace(opts) {
438
+ const alias = `#${opts.spaceLocalpart}:${opts.serverName}`;
439
+ const existing = await opts.client.resolveAlias(alias);
440
+ if (existing) return existing;
441
+ const display = opts.spaceLocalpart.charAt(0).toUpperCase() + opts.spaceLocalpart.slice(1);
442
+ return opts.client.createRoomRaw({
443
+ asUserId: opts.asUserId,
444
+ body: {
445
+ room_alias_name: opts.spaceLocalpart,
446
+ name: display,
447
+ preset: opts.preset,
448
+ creation_content: { type: "m.space" }
449
+ }
450
+ });
451
+ }
452
+ function serverNameFromMxid(mxid) {
453
+ const colon = mxid.indexOf(":");
454
+ if (colon < 0) {
455
+ throw new Error(`mxid lacks server: ${mxid}`);
456
+ }
457
+ return mxid.slice(colon + 1);
458
+ }
459
+
460
+ // src/bot-pool.ts
461
+ var BotPool = class {
462
+ constructor(client, agents) {
463
+ this.client = client;
464
+ this.agents = agents;
465
+ }
466
+ async bootstrap(opts = {}) {
467
+ const aliasToId = /* @__PURE__ */ new Map();
468
+ const attachedToSpace = /* @__PURE__ */ new Set();
469
+ for (const a of this.agents) {
470
+ const lp = localpart(a.userId);
471
+ try {
472
+ await this.client.registerBot(lp);
473
+ } catch (err) {
474
+ console.warn(`[matrix] register failed for ${a.userId}: ${err.message}`);
475
+ }
476
+ try {
477
+ await this.client.setDisplayName(a.userId, a.displayName ?? lp);
478
+ } catch (err) {
479
+ console.warn(`[matrix] setDisplayName(${a.userId}) failed: ${err.message}`);
480
+ }
481
+ for (let i = 0; i < a.rooms.length; i++) {
482
+ const room = a.rooms[i];
483
+ try {
484
+ let resolved = room;
485
+ if (room.startsWith("#")) {
486
+ const cached = aliasToId.get(room);
487
+ if (cached) {
488
+ resolved = cached;
489
+ } else {
490
+ const existing = await this.client.resolveAlias(room);
491
+ if (existing) {
492
+ resolved = existing;
493
+ } else {
494
+ const colon = room.indexOf(":");
495
+ const aliasLocalpart = colon > 1 ? room.slice(1, colon) : room.slice(1);
496
+ const sender = opts.adminUserId ?? a.userId;
497
+ resolved = await this.client.createRoom({
498
+ roomAliasName: aliasLocalpart,
499
+ invite: opts.adminUserId ? [opts.adminUserId] : [],
500
+ senderUserId: sender,
501
+ name: aliasLocalpart
502
+ });
503
+ }
504
+ aliasToId.set(room, resolved);
505
+ }
506
+ }
507
+ a.rooms[i] = resolved;
508
+ await this.client.joinRoom(resolved, a.userId);
509
+ if (opts.spaceRoomId && opts.asUserId && !attachedToSpace.has(resolved)) {
510
+ attachedToSpace.add(resolved);
511
+ const via = serverNameFromMxid(a.userId);
512
+ try {
513
+ await this.client.sendStateEvent({
514
+ roomId: opts.spaceRoomId,
515
+ asUserId: opts.asUserId,
516
+ eventType: "m.space.child",
517
+ stateKey: resolved,
518
+ content: { via: [via] }
519
+ });
520
+ } catch (err) {
521
+ console.warn(
522
+ `[matrix] m.space.child(${resolved}) failed: ${err.message}`
523
+ );
524
+ }
525
+ }
526
+ } catch (err) {
527
+ console.warn(
528
+ `[matrix] join failed (${a.userId} \u2192 ${room}): ${err.message}`
529
+ );
530
+ }
531
+ }
532
+ }
533
+ }
534
+ findByUserId(userId) {
535
+ return this.agents.find((a) => a.userId === userId);
536
+ }
537
+ findByName(name) {
538
+ return this.agents.find((a) => a.name === name);
539
+ }
540
+ };
541
+ function localpart(userId) {
542
+ const m = /^@([^:]+):/.exec(userId);
543
+ if (!m) throw new Error(`bad user id: ${userId}`);
544
+ return m[1];
545
+ }
546
+
547
+ // src/transport.ts
548
+ import { Hono } from "hono";
549
+ import { timingSafeEqual } from "crypto";
550
+
551
+ // src/event-encoders.ts
552
+ var RAW_INPUT_STR_MAX = 250;
553
+ function toToolCallBody(evt) {
554
+ const out = {
555
+ session_id: evt.sessionId,
556
+ tool_call_id: evt.toolCallId,
557
+ title: evt.title
558
+ };
559
+ if (evt.kind !== void 0) out.kind = evt.kind;
560
+ if (evt.status !== void 0) out.status = evt.status;
561
+ if (evt.rawInput !== void 0) out.raw_input = truncateStrings(evt.rawInput, RAW_INPUT_STR_MAX);
562
+ if (evt.locations !== void 0) out.locations = evt.locations;
563
+ return out;
564
+ }
565
+ function truncateStrings(v, max) {
566
+ if (typeof v === "string") {
567
+ return v.length > max ? v.slice(0, max) + "\u2026 [truncated]" : v;
568
+ }
569
+ if (Array.isArray(v)) {
570
+ return v.map((item) => truncateStrings(item, max));
571
+ }
572
+ if (v && typeof v === "object") {
573
+ const out = {};
574
+ for (const [k, val] of Object.entries(v)) {
575
+ out[k] = truncateStrings(val, max);
576
+ }
577
+ return out;
578
+ }
579
+ return v;
580
+ }
581
+ function toUpdateBody(evt) {
582
+ const out = {
583
+ session_id: evt.sessionId,
584
+ tool_call_id: evt.toolCallId
585
+ };
586
+ if (evt.status !== void 0) out.status = evt.status;
587
+ if (evt.kind !== void 0) out.kind = evt.kind;
588
+ if (evt.content !== void 0) out.content = evt.content;
589
+ if (evt.rawInput !== void 0) out.raw_input = truncateStrings(evt.rawInput, RAW_INPUT_STR_MAX);
590
+ if (evt.locations !== void 0) out.locations = evt.locations;
591
+ return out;
592
+ }
593
+ function toPlanBody(evt) {
594
+ return {
595
+ session_id: evt.sessionId,
596
+ entries: evt.entries
597
+ };
598
+ }
599
+
600
+ // src/markdown-to-matrix-html.ts
601
+ import { marked } from "marked";
602
+ import sanitizeHtml from "sanitize-html";
603
+ var ALLOWED_TAGS = [
604
+ "del",
605
+ "h1",
606
+ "h2",
607
+ "h3",
608
+ "h4",
609
+ "h5",
610
+ "h6",
611
+ "blockquote",
612
+ "p",
613
+ "a",
614
+ "ul",
615
+ "ol",
616
+ "sup",
617
+ "sub",
618
+ "li",
619
+ "b",
620
+ "i",
621
+ "u",
622
+ "strong",
623
+ "em",
624
+ "s",
625
+ "code",
626
+ "hr",
627
+ "br",
628
+ "div",
629
+ "table",
630
+ "thead",
631
+ "tbody",
632
+ "tr",
633
+ "th",
634
+ "td",
635
+ "caption",
636
+ "pre",
637
+ "span",
638
+ "img",
639
+ "details",
640
+ "summary"
641
+ ];
642
+ var ALLOWED_ATTRIBUTES = {
643
+ a: ["href", "name", "target"],
644
+ img: ["width", "height", "alt", "title", "src"],
645
+ ol: ["start"],
646
+ code: ["class"],
647
+ span: ["data-mx-color", "data-mx-bg-color", "data-mx-spoiler"]
648
+ };
649
+ var ALLOWED_SCHEMES = ["https", "http", "ftp", "mailto", "magnet", "matrix"];
650
+ marked.setOptions({ gfm: true, breaks: false, async: false });
651
+ function toMatrixHtml(markdown) {
652
+ if (!markdown || !markdown.trim()) return "";
653
+ let rawHtml;
654
+ try {
655
+ const out = marked.parse(markdown);
656
+ if (typeof out !== "string") return "";
657
+ rawHtml = out;
658
+ } catch {
659
+ return "";
660
+ }
661
+ return sanitizeHtml(rawHtml, {
662
+ allowedTags: ALLOWED_TAGS,
663
+ allowedAttributes: ALLOWED_ATTRIBUTES,
664
+ allowedSchemes: ALLOWED_SCHEMES,
665
+ allowedSchemesByTag: { a: ALLOWED_SCHEMES }
666
+ });
667
+ }
668
+
669
+ // src/transport.ts
670
+ var STARTUP_GRACE_MS = 5e3;
671
+ var SEEN_EVENT_CAP = 5e3;
672
+ function inboundThreadRoot2(evt) {
673
+ const r = evt.content?.["m.relates_to"];
674
+ return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
675
+ }
676
+ function createMatrixTransport(opts) {
677
+ const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
678
+ const pool = new BotPool(client, bindings);
679
+ const sessions = /* @__PURE__ */ new Map();
680
+ const buffers = /* @__PURE__ */ new Map();
681
+ const sendQueue = /* @__PURE__ */ new Map();
682
+ const threadStates = /* @__PURE__ */ new Map();
683
+ const cutoffTs = Date.now() - STARTUP_GRACE_MS;
684
+ const seenEventIds = /* @__PURE__ */ new Set();
685
+ agents.onEvent = async (name, event) => {
686
+ const ctx = sessions.get(event.sessionId);
687
+ if (!ctx) {
688
+ console.warn(`[matrix:${name}] no session ctx for ${event.sessionId}`);
689
+ return;
690
+ }
691
+ if (event.type === "agent_message_chunk") {
692
+ const block = event.content;
693
+ if (block.type === "text" && typeof block.text === "string") {
694
+ buffers.set(event.sessionId, (buffers.get(event.sessionId) ?? "") + block.text);
695
+ } else {
696
+ console.warn(`[matrix:${name}] dropped chunk block type=${block.type}`, block);
697
+ }
698
+ return;
699
+ }
700
+ const eventType = event.type === "tool_call" ? "eco.zoon.tool_call" : event.type === "tool_call_update" ? "eco.zoon.tool_call_update" : "eco.zoon.plan";
701
+ const body = event.type === "tool_call" ? toToolCallBody(event) : event.type === "tool_call_update" ? toUpdateBody(event) : toPlanBody(event);
702
+ body["m.relates_to"] = { rel_type: "m.thread", event_id: ctx.threadRoot };
703
+ const tail = (sendQueue.get(event.sessionId) ?? Promise.resolve()).then(async () => {
704
+ try {
705
+ await client.sendCustomEvent({
706
+ roomId: ctx.roomId,
707
+ asUserId: ctx.agent.userId,
708
+ eventType,
709
+ content: body
710
+ });
711
+ } catch (err) {
712
+ console.warn(`[matrix:${name}] sendCustomEvent(${eventType}) failed:`, err);
713
+ }
714
+ });
715
+ sendQueue.set(event.sessionId, tail);
716
+ await tail;
717
+ };
718
+ agents.onApprovalRequest = async (name, req) => {
719
+ const handle = approvals.register(name, req.sessionId, req, {
720
+ timeoutMs: agents.getApprovalTimeoutMs(name)
721
+ });
722
+ return handle.decisionPromise;
723
+ };
724
+ approvals.on("registered", (handle) => {
725
+ const ctx = sessions.get(handle.sessionId);
726
+ if (!ctx) return;
727
+ const content = {
728
+ approval_id: handle.approvalId,
729
+ session_id: handle.sessionId,
730
+ tool_call_id: handle.toolCallId,
731
+ options: handle.options
732
+ };
733
+ content["m.relates_to"] = { rel_type: "m.thread", event_id: ctx.threadRoot };
734
+ if (handle.toolKind !== void 0) content.tool_kind = handle.toolKind;
735
+ if (handle.toolTitle !== void 0) content.tool_title = handle.toolTitle;
736
+ if (handle.toolInput !== void 0) content.tool_input = handle.toolInput;
737
+ void client.sendCustomEvent({
738
+ roomId: ctx.roomId,
739
+ asUserId: ctx.agent.userId,
740
+ eventType: "eco.zoon.approval_request",
741
+ content
742
+ });
743
+ });
744
+ const app = new Hono();
745
+ function authOk(authHeader) {
746
+ const h = authHeader ?? "";
747
+ if (!h.startsWith("Bearer ")) return false;
748
+ const got = h.slice(7);
749
+ if (got.length !== hsToken.length) return false;
750
+ return timingSafeEqual(Buffer.from(got), Buffer.from(hsToken));
751
+ }
752
+ app.put("/_matrix/app/v1/transactions/:txnId", async (c) => {
753
+ if (!authOk(c.req.header("authorization"))) {
754
+ return c.json({ errcode: "M_FORBIDDEN" }, 403);
755
+ }
756
+ const body = await c.req.json().catch(() => ({}));
757
+ for (const evt of body.events ?? []) {
758
+ if (evt.event_id) {
759
+ if (seenEventIds.has(evt.event_id)) {
760
+ continue;
761
+ }
762
+ seenEventIds.add(evt.event_id);
763
+ if (seenEventIds.size > SEEN_EVENT_CAP) {
764
+ const first = seenEventIds.values().next().value;
765
+ if (first !== void 0) seenEventIds.delete(first);
766
+ }
767
+ }
768
+ if (evt.origin_server_ts !== void 0 && evt.origin_server_ts < cutoffTs && evt.type === "m.room.message") {
769
+ console.log(
770
+ `[matrix] dropping stale message event ${evt.event_id} (ts=${evt.origin_server_ts}, daemon started at ${cutoffTs + STARTUP_GRACE_MS})`
771
+ );
772
+ continue;
773
+ }
774
+ if (evt.type === "eco.zoon.session_reset") {
775
+ const relates = evt.content?.["m.relates_to"];
776
+ const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
777
+ if (!threadRoot) {
778
+ console.log("[matrix] dropping eco.zoon.session_reset without thread relation");
779
+ continue;
780
+ }
781
+ console.log(`[matrix] inbound eco.zoon.session_reset in ${evt.room_id} thread=${threadRoot}`);
782
+ for (const a of bindings) {
783
+ agents.endSession(a.name, threadRoot);
784
+ }
785
+ continue;
786
+ }
787
+ if (evt.type === "eco.zoon.interrupt") {
788
+ const content = evt.content ?? {};
789
+ const relates = evt.content?.["m.relates_to"];
790
+ const threadRoot = relates?.rel_type === "m.thread" && relates.event_id ? relates.event_id : void 0;
791
+ if (threadRoot) {
792
+ const targets = [];
793
+ for (const [sessionId, ctx2] of sessions) {
794
+ if (ctx2.threadRoot === threadRoot) {
795
+ targets.push({ sessionId, agent: ctx2.agent.name });
796
+ }
797
+ }
798
+ for (const t of targets) {
799
+ console.log(
800
+ `[matrix] interrupt session=${t.sessionId} agent=${t.agent} thread=${threadRoot}` + (content.reason ? ` reason=${content.reason}` : "")
801
+ );
802
+ await agents.cancelSession(t.agent, t.sessionId).catch((err) => {
803
+ console.error(`[matrix] cancelSession(${t.agent}, ${t.sessionId}) failed:`, err);
804
+ });
805
+ }
806
+ continue;
807
+ }
808
+ if (!content.session_id) {
809
+ console.warn(`[matrix] eco.zoon.interrupt missing session_id (event_id=${evt.event_id})`);
810
+ continue;
811
+ }
812
+ const ctx = sessions.get(content.session_id);
813
+ if (!ctx) {
814
+ continue;
815
+ }
816
+ console.log(
817
+ `[matrix] interrupt session=${content.session_id} agent=${ctx.agent.name}` + (content.reason ? ` reason=${content.reason}` : "")
818
+ );
819
+ await agents.cancelSession(ctx.agent.name, content.session_id).catch((err) => {
820
+ console.error(`[matrix] cancelSession(${ctx.agent.name}, ${content.session_id}) failed:`, err);
821
+ });
822
+ continue;
823
+ }
824
+ if (evt.type === "eco.zoon.approval_response") {
825
+ const content = evt.content ?? {};
826
+ if (!content.session_id || !content.approval_id || !content.decision) continue;
827
+ const decision = content.option_id ? { decision: content.decision, optionId: content.option_id } : { decision: content.decision };
828
+ const ok = approvals.resolve(
829
+ content.session_id,
830
+ content.approval_id,
831
+ decision
832
+ );
833
+ if (!ok) console.warn(`[matrix] unknown approval ${content.approval_id}`);
834
+ continue;
835
+ }
836
+ logInbound(evt);
837
+ const promotedRoot = inboundThreadRoot2(evt) ?? evt.event_id;
838
+ const inboundRel = inboundThreadRoot2(evt);
839
+ if (evt.type === "m.room.message" && inboundRel && !threadStates.has(inboundRel) && evt.room_id) {
840
+ try {
841
+ const rebuilt = await rebuildThreadState(client, evt.room_id, inboundRel, bindings);
842
+ threadStates.set(inboundRel, rebuilt);
843
+ console.log(
844
+ `[matrix] rebuilt threadState for ${inboundRel}: participants=${rebuilt.participants.join(",")} rootMentions=${rebuilt.rootMentions.join(",")}`
845
+ );
846
+ } catch (err) {
847
+ console.warn(`[matrix] failed to rebuild threadState for ${inboundRel}:`, err);
848
+ }
849
+ }
850
+ const matches = route(evt, bindings, threadStates);
851
+ const senderIsBot = bindings.some((b) => b.userId === evt.sender);
852
+ if (evt.type === "m.room.message" && matches.length === 0 && !senderIsBot) {
853
+ console.warn(
854
+ `[matrix] no agent matched message in ${evt.room_id} from ${evt.sender} (bindings: ${bindings.map((b) => `${b.name}@${b.userId}[${b.trigger}]`).join(", ")})`
855
+ );
856
+ }
857
+ if (matches.length > 0 && promotedRoot) {
858
+ let st = threadStates.get(promotedRoot);
859
+ if (!st) {
860
+ st = { participants: [], rootMentions: [] };
861
+ threadStates.set(promotedRoot, st);
862
+ }
863
+ const msgMentions = new Set(extractMentions(evt));
864
+ for (const a of bindings) {
865
+ if (msgMentions.has(a.userId) && !st.rootMentions.includes(a.name)) {
866
+ st.rootMentions.push(a.name);
867
+ }
868
+ }
869
+ }
870
+ for (const a of matches) {
871
+ console.log(`[matrix] \u2192 ${a.name} (${a.userId})`);
872
+ void runTurn(a, evt).then(() => {
873
+ if (!promotedRoot) return;
874
+ let st = threadStates.get(promotedRoot);
875
+ if (!st) {
876
+ st = { participants: [], rootMentions: [] };
877
+ threadStates.set(promotedRoot, st);
878
+ }
879
+ if (st.participants.at(-1) !== a.name) st.participants.push(a.name);
880
+ }).catch((err) => {
881
+ console.error(`[matrix] runTurn failed for ${a.name}:`, err);
882
+ });
883
+ }
884
+ }
885
+ return c.json({});
886
+ });
887
+ app.get("/_matrix/app/v1/users/:userId", (c) => {
888
+ if (!authOk(c.req.header("authorization"))) {
889
+ return c.json({ errcode: "M_FORBIDDEN" }, 403);
890
+ }
891
+ return c.json({});
892
+ });
893
+ app.get("/_matrix/app/v1/rooms/:alias", (c) => {
894
+ if (!authOk(c.req.header("authorization"))) {
895
+ return c.json({ errcode: "M_FORBIDDEN" }, 403);
896
+ }
897
+ return c.json({ errcode: "M_NOT_FOUND" }, 404);
898
+ });
899
+ app.post("/_matrix/app/v1/ping", (c) => {
900
+ if (!authOk(c.req.header("authorization"))) {
901
+ return c.json({ errcode: "M_FORBIDDEN" }, 403);
902
+ }
903
+ return c.json({});
904
+ });
905
+ app.get("/healthz", (c) => c.text("ok"));
906
+ async function runTurn(agent, evt) {
907
+ if (!evt.room_id || !evt.event_id) return;
908
+ const inbound = inboundThreadRoot2(evt);
909
+ const threadRoot = inbound ?? evt.event_id;
910
+ const sessionKey = threadRoot;
911
+ const sessionId = await agents.ensureSession(agent.name, sessionKey, evt.room_id);
912
+ sessions.set(sessionId, { agent, roomId: evt.room_id, threadRoot });
913
+ buffers.set(sessionId, "");
914
+ const roomId = evt.room_id;
915
+ const TYPING_TTL_MS = 3e4;
916
+ const TYPING_REFRESH_MS = 25e3;
917
+ const safeTyping = (typing) => client.setTyping({ roomId, asUserId: agent.userId, typing, timeoutMs: TYPING_TTL_MS }).catch((err) => console.warn(`[matrix:${agent.name}] setTyping(${typing}) failed:`, err));
918
+ const safePresence = (presence) => client.setPresence({ asUserId: agent.userId, presence }).catch(
919
+ (err) => console.warn(`[matrix:${agent.name}] setPresence(${presence}) failed:`, err)
920
+ );
921
+ await safeTyping(true);
922
+ await safePresence("unavailable");
923
+ const refresh = setInterval(() => {
924
+ void safeTyping(true);
925
+ }, TYPING_REFRESH_MS);
926
+ try {
927
+ const rawBody = evt.content?.body ?? "";
928
+ const promptText = stripMention(rawBody, agent.userId);
929
+ await agents.prompt(agent.name, {
930
+ threadId: sessionKey,
931
+ channelId: evt.room_id,
932
+ content: [{ type: "text", text: promptText }]
933
+ });
934
+ const text = buffers.get(sessionId) ?? "";
935
+ if (text.length > 0) {
936
+ const html = toMatrixHtml(text);
937
+ const content = {
938
+ msgtype: "m.text",
939
+ body: text
940
+ };
941
+ if (html) {
942
+ const escapedPlain = "<p>" + text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;") + "</p>";
943
+ const norm = (s) => s.replace(/\s+/g, " ").trim();
944
+ if (norm(html) !== norm(escapedPlain)) {
945
+ content.format = "org.matrix.custom.html";
946
+ content.formatted_body = html;
947
+ }
948
+ }
949
+ await client.sendMessage({
950
+ roomId: evt.room_id,
951
+ asUserId: agent.userId,
952
+ content,
953
+ threadRoot
954
+ // every reply threads, full stop
955
+ });
956
+ } else {
957
+ console.warn(
958
+ `[matrix:${agent.name}] turn finished with empty buffer (session=${sessionId}); nothing sent to ${evt.room_id}`
959
+ );
960
+ }
961
+ } finally {
962
+ clearInterval(refresh);
963
+ await safeTyping(false);
964
+ await safePresence("online");
965
+ buffers.delete(sessionId);
966
+ }
967
+ }
968
+ return {
969
+ app,
970
+ bootstrap: async (bootstrapOpts = {}) => {
971
+ await pool.bootstrap({ adminUserId, ...bootstrapOpts });
972
+ await Promise.allSettled(
973
+ bindings.map(
974
+ (b) => client.setPresence({ asUserId: b.userId, presence: "online" }).catch((err) => {
975
+ console.warn(`[matrix:${b.name}] initial setPresence(online) failed:`, err);
976
+ })
977
+ )
978
+ );
979
+ },
980
+ pool
981
+ };
982
+ }
983
+ async function rebuildThreadState(client, roomId, rootEventId, bindings) {
984
+ const state = { participants: [], rootMentions: [] };
985
+ const asUser = (bindings.find((b) => b.rooms.includes(roomId)) ?? bindings[0])?.userId;
986
+ if (!asUser) return state;
987
+ const root = await client.fetchEvent(roomId, rootEventId, asUser);
988
+ if (root) {
989
+ const rootMentions = new Set(extractMentions(root));
990
+ for (const a of bindings) {
991
+ if (rootMentions.has(a.userId) && !state.rootMentions.includes(a.name)) {
992
+ state.rootMentions.push(a.name);
993
+ }
994
+ }
995
+ }
996
+ const { chunk: thread } = await client.fetchThreadRelations({
997
+ roomId,
998
+ rootEventId,
999
+ asUserId: asUser
1000
+ });
1001
+ for (const ev of thread) {
1002
+ const mentions = new Set(extractMentions(ev));
1003
+ for (const a of bindings) {
1004
+ if (mentions.has(a.userId) && !state.rootMentions.includes(a.name)) {
1005
+ state.rootMentions.push(a.name);
1006
+ }
1007
+ }
1008
+ const sender = ev.sender;
1009
+ const type = ev.type;
1010
+ if (type === "m.room.message" && sender) {
1011
+ const a = bindings.find((b) => b.userId === sender);
1012
+ if (a && state.participants.at(-1) !== a.name) state.participants.push(a.name);
1013
+ }
1014
+ }
1015
+ return state;
1016
+ }
1017
+ function logInbound(evt) {
1018
+ const sender = evt.sender ?? "?";
1019
+ const room = evt.room_id ?? "?";
1020
+ const type = evt.type ?? "?";
1021
+ if (type === "m.room.message") {
1022
+ const body = evt.content?.body ?? "";
1023
+ const mentions = evt.content?.["m.mentions"]?.user_ids;
1024
+ const mentionsStr = mentions?.length ? ` mentions=${JSON.stringify(mentions)}` : "";
1025
+ console.log(
1026
+ `[matrix] inbound msg in ${room} from ${sender}${mentionsStr}: ${truncate(body, 200)}`
1027
+ );
1028
+ } else {
1029
+ console.log(`[matrix] inbound ${type} in ${room} from ${sender}`);
1030
+ }
1031
+ }
1032
+ function truncate(s, n) {
1033
+ return s.length > n ? s.slice(0, n) + "\u2026" : s;
1034
+ }
1035
+
1036
+ // src/workforce-publisher.ts
1037
+ function buildWorkforceRoster(agents) {
1038
+ return {
1039
+ version: 1,
1040
+ agents: agents.map((a) => ({ user_id: a.userId, name: a.name, rooms: a.rooms }))
1041
+ };
1042
+ }
1043
+ async function publishWorkforce(opts) {
1044
+ await opts.client.sendStateEvent({
1045
+ roomId: opts.spaceRoomId,
1046
+ asUserId: opts.asUserId,
1047
+ eventType: "eco.zoon.workforce",
1048
+ stateKey: "",
1049
+ content: buildWorkforceRoster(opts.agents)
1050
+ });
1051
+ }
1052
+ async function startWorkforcePublisher(opts) {
1053
+ await publishWorkforce({ ...opts, agents: opts.getAgents() });
1054
+ return {
1055
+ async reload() {
1056
+ await publishWorkforce({ ...opts, agents: opts.getAgents() });
1057
+ },
1058
+ async stop() {
1059
+ }
1060
+ };
1061
+ }
1062
+ export {
1063
+ BotPool,
1064
+ MatrixClient,
1065
+ MatrixContextProvider,
1066
+ buildWorkforceRoster,
1067
+ createMatrixTransport,
1068
+ ensureWorkforceSpace,
1069
+ extractMentions,
1070
+ publishWorkforce,
1071
+ renderRegistration,
1072
+ route,
1073
+ serverNameFromMxid,
1074
+ startWorkforcePublisher
1075
+ };
1076
+ //# sourceMappingURL=index.js.map