zooid 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24181,6 +24181,39 @@ function parseTransport(name, raw, processEnv) {
24181
24181
  }
24182
24182
  return { type: "http", port };
24183
24183
  }
24184
+ function parseRoomBinding(path, raw, serverName) {
24185
+ function normalizeAlias(alias) {
24186
+ if (alias.length === 0) {
24187
+ throw new Error(`${path}: must be a non-empty alias`);
24188
+ }
24189
+ if (!MATRIX_ROOM_IDENT_RE.test(alias)) {
24190
+ throw new Error(
24191
+ `${path}: must start with '#' or '!' (got ${JSON.stringify(alias)})`
24192
+ );
24193
+ }
24194
+ return alias.includes(":") ? alias : `${alias}:${serverName}`;
24195
+ }
24196
+ if (typeof raw === "string") {
24197
+ return { alias: normalizeAlias(raw) };
24198
+ }
24199
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
24200
+ throw new Error(`${path}: must be a string or { alias, power_level } object`);
24201
+ }
24202
+ const r = raw;
24203
+ if (typeof r.alias !== "string" || r.alias.length === 0) {
24204
+ throw new Error(`${path}.alias: must be a non-empty string`);
24205
+ }
24206
+ const out = { alias: normalizeAlias(r.alias) };
24207
+ if (r.power_level !== void 0) {
24208
+ if (typeof r.power_level !== "number" || !Number.isInteger(r.power_level)) {
24209
+ throw new Error(
24210
+ `${path}.power_level: must be an integer (got ${JSON.stringify(r.power_level)})`
24211
+ );
24212
+ }
24213
+ out.powerLevel = r.power_level;
24214
+ }
24215
+ return out;
24216
+ }
24184
24217
  function parseTransportBinding(name, entry, transports) {
24185
24218
  const present = TRANSPORT_KINDS.filter(
24186
24219
  (k) => entry[k] !== void 0 && entry[k] !== null
@@ -24250,16 +24283,8 @@ function parseTransportBinding(name, entry, transports) {
24250
24283
  throw new Error(`agents.${name}.matrix.rooms is required and must be a non-empty array`);
24251
24284
  }
24252
24285
  const rooms = [];
24253
- for (const r of block.rooms) {
24254
- if (typeof r !== "string" || r.length === 0) {
24255
- throw new Error(`agents.${name}.matrix.rooms[] must be a non-empty string`);
24256
- }
24257
- if (!MATRIX_ROOM_IDENT_RE.test(r)) {
24258
- throw new Error(
24259
- `agents.${name}.matrix.rooms[] must start with '#' or '!' (got ${JSON.stringify(r)})`
24260
- );
24261
- }
24262
- rooms.push(r.includes(":") ? r : `${r}:${serverName}`);
24286
+ for (let i = 0; i < block.rooms.length; i++) {
24287
+ rooms.push(parseRoomBinding(`agents.${name}.matrix.rooms[${i}]`, block.rooms[i], serverName));
24263
24288
  }
24264
24289
  let displayName;
24265
24290
  if (block.display_name !== void 0) {
@@ -24785,6 +24810,22 @@ var MatrixClient = class {
24785
24810
  preset: opts.preset ?? "public_chat"
24786
24811
  };
24787
24812
  if (opts.name !== void 0) body.name = opts.name;
24813
+ if (opts.roomVersion !== void 0) body.room_version = opts.roomVersion;
24814
+ if (opts.restrictedToSpaceId !== void 0) {
24815
+ body.initial_state = [
24816
+ {
24817
+ type: "m.room.join_rules",
24818
+ state_key: "",
24819
+ content: {
24820
+ join_rule: "restricted",
24821
+ allow: [{ type: "m.room_membership", room_id: opts.restrictedToSpaceId }]
24822
+ }
24823
+ }
24824
+ ];
24825
+ }
24826
+ if (opts.userPowerLevels && Object.keys(opts.userPowerLevels).length > 0) {
24827
+ body.power_level_content_override = { users: opts.userPowerLevels };
24828
+ }
24788
24829
  const r = await this.fetch(
24789
24830
  `${this.homeserver}/_matrix/client/v3/createRoom?user_id=${encodeURIComponent(opts.senderUserId)}`,
24790
24831
  {
@@ -24830,6 +24871,30 @@ var MatrixClient = class {
24830
24871
  if (!r.ok) throw new Error(`sendStateEvent ${opts.eventType} failed: ${r.status}`);
24831
24872
  return await r.json();
24832
24873
  }
24874
+ /**
24875
+ * Invite a user to a room. Sent as the inviter (`asUserId`) — that user
24876
+ * needs invite power in the room. Tolerates the "already in room /
24877
+ * already invited" responses idempotently so bootstrap can run on a
24878
+ * fresh AND a populated homeserver without branching.
24879
+ */
24880
+ async invite(opts) {
24881
+ const url2 = `${this.homeserver}/_matrix/client/v3/rooms/${encodeURIComponent(opts.roomId)}/invite?user_id=${encodeURIComponent(opts.asUserId)}`;
24882
+ const r = await this.fetch(url2, {
24883
+ method: "POST",
24884
+ headers: {
24885
+ Authorization: `Bearer ${this.asToken}`,
24886
+ "content-type": "application/json"
24887
+ },
24888
+ body: JSON.stringify({ user_id: opts.targetUserId })
24889
+ });
24890
+ if (r.ok) return;
24891
+ if (r.status === 403) {
24892
+ const body = await r.text();
24893
+ if (/already (in the room|invited|a member|joined)/i.test(body)) return;
24894
+ throw new Error(`invite(${opts.targetUserId}) failed: 403 ${body}`);
24895
+ }
24896
+ throw new Error(`invite(${opts.targetUserId}) failed: ${r.status}`);
24897
+ }
24833
24898
  async joinRoom(roomIdOrAlias, asUserId) {
24834
24899
  const url2 = `${this.homeserver}/_matrix/client/v3/join/${encodeURIComponent(roomIdOrAlias)}?user_id=${encodeURIComponent(asUserId)}`;
24835
24900
  const r = await this.fetch(url2, {
@@ -25158,7 +25223,7 @@ function route(event, agents, threadStates) {
25158
25223
  const threadState = threadRoot ? threadStates?.get(threadRoot) : void 0;
25159
25224
  for (const a of agents) {
25160
25225
  if (event.sender === a.userId) continue;
25161
- if (!a.rooms.includes(event.room_id ?? "")) continue;
25226
+ if (!a.rooms.some((r) => r.alias === event.room_id)) continue;
25162
25227
  if (a.trigger === "any") {
25163
25228
  matches.push(a);
25164
25229
  continue;
@@ -25185,15 +25250,50 @@ async function ensureWorkforceSpace(opts) {
25185
25250
  const existing = await opts.client.resolveAlias(alias);
25186
25251
  if (existing) return existing;
25187
25252
  const display = opts.spaceLocalpart.charAt(0).toUpperCase() + opts.spaceLocalpart.slice(1);
25188
- return opts.client.createRoomRaw({
25253
+ const body = {
25254
+ room_alias_name: opts.spaceLocalpart,
25255
+ name: display,
25256
+ preset: opts.preset,
25257
+ creation_content: { type: "m.space" },
25258
+ // A workspace is joined by invitation, not self-service. Pin the space's
25259
+ // join rule to invite regardless of preset so it can't be walked into
25260
+ // (which would otherwise satisfy every restricted child room's allow).
25261
+ initial_state: [{ type: "m.room.join_rules", state_key: "", content: { join_rule: "invite" } }]
25262
+ };
25263
+ if (opts.admins && opts.admins.length > 0) {
25264
+ body.invite = opts.admins;
25265
+ const users = { [opts.asUserId]: 100 };
25266
+ for (const a of opts.admins) users[a] = 100;
25267
+ body.power_level_content_override = { users };
25268
+ }
25269
+ return opts.client.createRoomRaw({ asUserId: opts.asUserId, body });
25270
+ }
25271
+ async function ensureDefaultChannel(opts) {
25272
+ const localpart2 = opts.channelLocalpart ?? "general";
25273
+ const alias = `#${localpart2}:${opts.serverName}`;
25274
+ const existing = await opts.client.resolveAlias(alias);
25275
+ if (existing) return existing;
25276
+ let userPowerLevels;
25277
+ if (opts.admins && opts.admins.length > 0) {
25278
+ userPowerLevels = { [opts.asUserId]: 100 };
25279
+ for (const a of opts.admins) userPowerLevels[a] = 100;
25280
+ }
25281
+ const roomId = await opts.client.createRoom({
25282
+ roomAliasName: localpart2,
25283
+ invite: [],
25284
+ senderUserId: opts.asUserId,
25285
+ name: localpart2.charAt(0).toUpperCase() + localpart2.slice(1),
25286
+ restrictedToSpaceId: opts.spaceId,
25287
+ ...userPowerLevels ? { userPowerLevels } : {}
25288
+ });
25289
+ await opts.client.sendStateEvent({
25290
+ roomId: opts.spaceId,
25189
25291
  asUserId: opts.asUserId,
25190
- body: {
25191
- room_alias_name: opts.spaceLocalpart,
25192
- name: display,
25193
- preset: opts.preset,
25194
- creation_content: { type: "m.space" }
25195
- }
25292
+ eventType: "m.space.child",
25293
+ stateKey: roomId,
25294
+ content: { via: [opts.serverName] }
25196
25295
  });
25296
+ return roomId;
25197
25297
  }
25198
25298
  function serverNameFromMxid(mxid) {
25199
25299
  const colon = mxid.indexOf(":");
@@ -25224,8 +25324,23 @@ var BotPool = class {
25224
25324
  } catch (err) {
25225
25325
  console.warn(`[matrix] setDisplayName(${a.userId}) failed: ${err.message}`);
25226
25326
  }
25327
+ if (opts.spaceRoomId && opts.asUserId) {
25328
+ try {
25329
+ await this.client.invite({
25330
+ roomId: opts.spaceRoomId,
25331
+ asUserId: opts.asUserId,
25332
+ targetUserId: a.userId
25333
+ });
25334
+ await this.client.joinRoom(opts.spaceRoomId, a.userId);
25335
+ } catch (err) {
25336
+ console.warn(
25337
+ `[matrix] space membership for ${a.userId} failed: ${err.message}`
25338
+ );
25339
+ }
25340
+ }
25227
25341
  for (let i = 0; i < a.rooms.length; i++) {
25228
- const room = a.rooms[i];
25342
+ const binding = a.rooms[i];
25343
+ const room = binding.alias;
25229
25344
  try {
25230
25345
  let resolved = room;
25231
25346
  if (room.startsWith("#")) {
@@ -25240,17 +25355,25 @@ var BotPool = class {
25240
25355
  const colon = room.indexOf(":");
25241
25356
  const aliasLocalpart = colon > 1 ? room.slice(1, colon) : room.slice(1);
25242
25357
  const sender = opts.adminUserId ?? a.userId;
25358
+ const userPowerLevels = buildUserPowerLevels(
25359
+ opts.asUserId,
25360
+ opts.adminUserIds,
25361
+ this.agents,
25362
+ room
25363
+ );
25243
25364
  resolved = await this.client.createRoom({
25244
25365
  roomAliasName: aliasLocalpart,
25245
25366
  invite: opts.adminUserId ? [opts.adminUserId] : [],
25246
25367
  senderUserId: sender,
25247
- name: aliasLocalpart
25368
+ name: aliasLocalpart,
25369
+ ...opts.spaceRoomId ? { restrictedToSpaceId: opts.spaceRoomId } : {},
25370
+ ...userPowerLevels ? { userPowerLevels } : {}
25248
25371
  });
25249
25372
  }
25250
25373
  aliasToId.set(room, resolved);
25251
25374
  }
25252
25375
  }
25253
- a.rooms[i] = resolved;
25376
+ binding.alias = resolved;
25254
25377
  await this.client.joinRoom(resolved, a.userId);
25255
25378
  if (opts.spaceRoomId && opts.asUserId && !attachedToSpace.has(resolved)) {
25256
25379
  attachedToSpace.add(resolved);
@@ -25289,6 +25412,18 @@ function localpart(userId) {
25289
25412
  if (!m) throw new Error(`bad user id: ${userId}`);
25290
25413
  return m[1];
25291
25414
  }
25415
+ function buildUserPowerLevels(asUserId, admins, agents, roomAlias) {
25416
+ const users = {};
25417
+ if (asUserId) users[asUserId] = 100;
25418
+ if (admins) for (const a of admins) users[a] = 100;
25419
+ for (const a of agents) {
25420
+ for (const r of a.rooms) {
25421
+ if (r.alias !== roomAlias) continue;
25422
+ if (r.powerLevel !== void 0) users[a.userId] = r.powerLevel;
25423
+ }
25424
+ }
25425
+ return Object.keys(users).length > 0 ? users : void 0;
25426
+ }
25292
25427
 
25293
25428
  // ../transport-matrix/src/transport.ts
25294
25429
  import { Hono } from "hono";
@@ -25439,12 +25574,17 @@ function toMatrixHtml(markdown) {
25439
25574
  // ../transport-matrix/src/transport.ts
25440
25575
  var STARTUP_GRACE_MS = 5e3;
25441
25576
  var SEEN_EVENT_CAP = 5e3;
25577
+ var DRAIN_QUIET_MS = 300;
25578
+ var DRAIN_MAX_MS = 3e3;
25579
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
25442
25580
  function inboundThreadRoot2(evt) {
25443
25581
  const r = evt.content?.["m.relates_to"];
25444
25582
  return r?.rel_type === "m.thread" && r.event_id ? r.event_id : void 0;
25445
25583
  }
25446
25584
  function createMatrixTransport(opts) {
25447
25585
  const { agents, approvals, client, bindings, hsToken, adminUserId } = opts;
25586
+ const drainQuietMs = opts.drainQuietMs ?? DRAIN_QUIET_MS;
25587
+ const drainMaxMs = opts.drainMaxMs ?? DRAIN_MAX_MS;
25448
25588
  const pool = new BotPool(client, bindings);
25449
25589
  const sessions = /* @__PURE__ */ new Map();
25450
25590
  const buffers = /* @__PURE__ */ new Map();
@@ -25726,6 +25866,14 @@ function createMatrixTransport(opts) {
25726
25866
  channelId: evt.room_id,
25727
25867
  content: [{ type: "text", text: promptText }]
25728
25868
  });
25869
+ const drainStart = Date.now();
25870
+ let drained = buffers.get(sessionId) ?? "";
25871
+ while (drainQuietMs > 0 && Date.now() - drainStart < drainMaxMs) {
25872
+ await delay(drainQuietMs);
25873
+ const next = buffers.get(sessionId) ?? "";
25874
+ if (next === drained) break;
25875
+ drained = next;
25876
+ }
25729
25877
  const text = buffers.get(sessionId) ?? "";
25730
25878
  if (text.length > 0) {
25731
25879
  const html = toMatrixHtml(text);
@@ -25777,7 +25925,7 @@ function createMatrixTransport(opts) {
25777
25925
  }
25778
25926
  async function rebuildThreadState(client, roomId, rootEventId, bindings) {
25779
25927
  const state = { participants: [], rootMentions: [] };
25780
- const asUser = (bindings.find((b) => b.rooms.includes(roomId)) ?? bindings[0])?.userId;
25928
+ const asUser = (bindings.find((b) => b.rooms.some((r) => r.alias === roomId)) ?? bindings[0])?.userId;
25781
25929
  if (!asUser) return state;
25782
25930
  const root = await client.fetchEvent(roomId, rootEventId, asUser);
25783
25931
  if (root) {
@@ -25832,7 +25980,11 @@ function truncate(s, n) {
25832
25980
  function buildWorkforceRoster(agents) {
25833
25981
  return {
25834
25982
  version: 1,
25835
- agents: agents.map((a) => ({ user_id: a.userId, name: a.name, rooms: a.rooms }))
25983
+ agents: agents.map((a) => ({
25984
+ user_id: a.userId,
25985
+ name: a.name,
25986
+ rooms: a.rooms.map((r) => r.alias)
25987
+ }))
25836
25988
  };
25837
25989
  }
25838
25990
  async function publishWorkforce(opts) {
@@ -31425,10 +31577,11 @@ export {
31425
31577
  MatrixClient,
31426
31578
  renderRegistration,
31427
31579
  ensureWorkforceSpace,
31580
+ ensureDefaultChannel,
31428
31581
  createMatrixTransport,
31429
31582
  startWorkforcePublisher,
31430
31583
  SpawnRegistry,
31431
31584
  startDaemonSocketServer,
31432
31585
  buildAcpRegistry
31433
31586
  };
31434
- //# sourceMappingURL=chunk-SUPTPSN3.js.map
31587
+ //# sourceMappingURL=chunk-O6E4CDTV.js.map