@openclaw/matrix 2026.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/index.ts +18 -0
  3. package/openclaw.plugin.json +11 -0
  4. package/package.json +36 -0
  5. package/src/actions.ts +185 -0
  6. package/src/channel.directory.test.ts +56 -0
  7. package/src/channel.ts +417 -0
  8. package/src/config-schema.ts +62 -0
  9. package/src/directory-live.ts +175 -0
  10. package/src/group-mentions.ts +61 -0
  11. package/src/matrix/accounts.test.ts +83 -0
  12. package/src/matrix/accounts.ts +63 -0
  13. package/src/matrix/actions/client.ts +53 -0
  14. package/src/matrix/actions/messages.ts +120 -0
  15. package/src/matrix/actions/pins.ts +70 -0
  16. package/src/matrix/actions/reactions.ts +84 -0
  17. package/src/matrix/actions/room.ts +88 -0
  18. package/src/matrix/actions/summary.ts +77 -0
  19. package/src/matrix/actions/types.ts +84 -0
  20. package/src/matrix/actions.ts +15 -0
  21. package/src/matrix/active-client.ts +11 -0
  22. package/src/matrix/client/config.ts +165 -0
  23. package/src/matrix/client/create-client.ts +127 -0
  24. package/src/matrix/client/logging.ts +35 -0
  25. package/src/matrix/client/runtime.ts +4 -0
  26. package/src/matrix/client/shared.ts +169 -0
  27. package/src/matrix/client/storage.ts +131 -0
  28. package/src/matrix/client/types.ts +34 -0
  29. package/src/matrix/client.test.ts +57 -0
  30. package/src/matrix/client.ts +9 -0
  31. package/src/matrix/credentials.ts +103 -0
  32. package/src/matrix/deps.ts +57 -0
  33. package/src/matrix/format.test.ts +34 -0
  34. package/src/matrix/format.ts +22 -0
  35. package/src/matrix/index.ts +11 -0
  36. package/src/matrix/monitor/allowlist.ts +58 -0
  37. package/src/matrix/monitor/auto-join.ts +68 -0
  38. package/src/matrix/monitor/direct.ts +105 -0
  39. package/src/matrix/monitor/events.ts +103 -0
  40. package/src/matrix/monitor/handler.ts +645 -0
  41. package/src/matrix/monitor/index.ts +279 -0
  42. package/src/matrix/monitor/location.ts +83 -0
  43. package/src/matrix/monitor/media.test.ts +103 -0
  44. package/src/matrix/monitor/media.ts +113 -0
  45. package/src/matrix/monitor/mentions.ts +31 -0
  46. package/src/matrix/monitor/replies.ts +96 -0
  47. package/src/matrix/monitor/room-info.ts +58 -0
  48. package/src/matrix/monitor/rooms.ts +43 -0
  49. package/src/matrix/monitor/threads.ts +64 -0
  50. package/src/matrix/monitor/types.ts +39 -0
  51. package/src/matrix/poll-types.test.ts +22 -0
  52. package/src/matrix/poll-types.ts +157 -0
  53. package/src/matrix/probe.ts +70 -0
  54. package/src/matrix/send/client.ts +63 -0
  55. package/src/matrix/send/formatting.ts +92 -0
  56. package/src/matrix/send/media.ts +220 -0
  57. package/src/matrix/send/targets.test.ts +102 -0
  58. package/src/matrix/send/targets.ts +144 -0
  59. package/src/matrix/send/types.ts +109 -0
  60. package/src/matrix/send.test.ts +172 -0
  61. package/src/matrix/send.ts +255 -0
  62. package/src/onboarding.ts +432 -0
  63. package/src/outbound.ts +53 -0
  64. package/src/resolve-targets.ts +89 -0
  65. package/src/runtime.ts +14 -0
  66. package/src/tool-actions.ts +160 -0
  67. package/src/types.ts +95 -0
@@ -0,0 +1,432 @@
1
+ import {
2
+ addWildcardAllowFrom,
3
+ formatDocsLink,
4
+ promptChannelAccessConfig,
5
+ type ChannelOnboardingAdapter,
6
+ type ChannelOnboardingDmPolicy,
7
+ type WizardPrompter,
8
+ } from "openclaw/plugin-sdk";
9
+ import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
10
+ import { listMatrixDirectoryPeersLive } from "./directory-live.js";
11
+ import { resolveMatrixAccount } from "./matrix/accounts.js";
12
+ import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js";
13
+ import type { CoreConfig, DmPolicy } from "./types.js";
14
+
15
+ const channel = "matrix" as const;
16
+
17
+ function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) {
18
+ const allowFrom = policy === "open" ? addWildcardAllowFrom(cfg.channels?.matrix?.dm?.allowFrom) : undefined;
19
+ return {
20
+ ...cfg,
21
+ channels: {
22
+ ...cfg.channels,
23
+ matrix: {
24
+ ...cfg.channels?.matrix,
25
+ dm: {
26
+ ...cfg.channels?.matrix?.dm,
27
+ policy,
28
+ ...(allowFrom ? { allowFrom } : {}),
29
+ },
30
+ },
31
+ },
32
+ };
33
+ }
34
+
35
+ async function noteMatrixAuthHelp(prompter: WizardPrompter): Promise<void> {
36
+ await prompter.note(
37
+ [
38
+ "Matrix requires a homeserver URL.",
39
+ "Use an access token (recommended) or a password (logs in and stores a token).",
40
+ "With access token: user ID is fetched automatically.",
41
+ "Env vars supported: MATRIX_HOMESERVER, MATRIX_USER_ID, MATRIX_ACCESS_TOKEN, MATRIX_PASSWORD.",
42
+ `Docs: ${formatDocsLink("/channels/matrix", "channels/matrix")}`,
43
+ ].join("\n"),
44
+ "Matrix setup",
45
+ );
46
+ }
47
+
48
+ async function promptMatrixAllowFrom(params: {
49
+ cfg: CoreConfig;
50
+ prompter: WizardPrompter;
51
+ }): Promise<CoreConfig> {
52
+ const { cfg, prompter } = params;
53
+ const existingAllowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? [];
54
+ const account = resolveMatrixAccount({ cfg });
55
+ const canResolve = Boolean(account.configured);
56
+
57
+ const parseInput = (raw: string) =>
58
+ raw
59
+ .split(/[\n,;]+/g)
60
+ .map((entry) => entry.trim())
61
+ .filter(Boolean);
62
+
63
+ const isFullUserId = (value: string) => value.startsWith("@") && value.includes(":");
64
+
65
+ while (true) {
66
+ const entry = await prompter.text({
67
+ message: "Matrix allowFrom (username or user id)",
68
+ placeholder: "@user:server",
69
+ initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
70
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
71
+ });
72
+ const parts = parseInput(String(entry));
73
+ const resolvedIds: string[] = [];
74
+ let unresolved: string[] = [];
75
+
76
+ for (const part of parts) {
77
+ if (isFullUserId(part)) {
78
+ resolvedIds.push(part);
79
+ continue;
80
+ }
81
+ if (!canResolve) {
82
+ unresolved.push(part);
83
+ continue;
84
+ }
85
+ const results = await listMatrixDirectoryPeersLive({
86
+ cfg,
87
+ query: part,
88
+ limit: 5,
89
+ }).catch(() => []);
90
+ const match = results.find((result) => result.id);
91
+ if (match?.id) {
92
+ resolvedIds.push(match.id);
93
+ if (results.length > 1) {
94
+ await prompter.note(
95
+ `Multiple matches for "${part}", using ${match.id}.`,
96
+ "Matrix allowlist",
97
+ );
98
+ }
99
+ } else {
100
+ unresolved.push(part);
101
+ }
102
+ }
103
+
104
+ if (unresolved.length > 0) {
105
+ await prompter.note(
106
+ `Could not resolve: ${unresolved.join(", ")}. Use full @user:server IDs.`,
107
+ "Matrix allowlist",
108
+ );
109
+ continue;
110
+ }
111
+
112
+ const unique = [
113
+ ...new Set([
114
+ ...existingAllowFrom.map((item) => String(item).trim()).filter(Boolean),
115
+ ...resolvedIds,
116
+ ]),
117
+ ];
118
+ return {
119
+ ...cfg,
120
+ channels: {
121
+ ...cfg.channels,
122
+ matrix: {
123
+ ...cfg.channels?.matrix,
124
+ enabled: true,
125
+ dm: {
126
+ ...cfg.channels?.matrix?.dm,
127
+ policy: "allowlist",
128
+ allowFrom: unique,
129
+ },
130
+ },
131
+ },
132
+ };
133
+ }
134
+ }
135
+
136
+ function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist" | "disabled") {
137
+ return {
138
+ ...cfg,
139
+ channels: {
140
+ ...cfg.channels,
141
+ matrix: {
142
+ ...cfg.channels?.matrix,
143
+ enabled: true,
144
+ groupPolicy,
145
+ },
146
+ },
147
+ };
148
+ }
149
+
150
+ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) {
151
+ const groups = Object.fromEntries(roomKeys.map((key) => [key, { allow: true }]));
152
+ return {
153
+ ...cfg,
154
+ channels: {
155
+ ...cfg.channels,
156
+ matrix: {
157
+ ...cfg.channels?.matrix,
158
+ enabled: true,
159
+ groups,
160
+ },
161
+ },
162
+ };
163
+ }
164
+
165
+ const dmPolicy: ChannelOnboardingDmPolicy = {
166
+ label: "Matrix",
167
+ channel,
168
+ policyKey: "channels.matrix.dm.policy",
169
+ allowFromKey: "channels.matrix.dm.allowFrom",
170
+ getCurrent: (cfg) => (cfg as CoreConfig).channels?.matrix?.dm?.policy ?? "pairing",
171
+ setPolicy: (cfg, policy) => setMatrixDmPolicy(cfg as CoreConfig, policy),
172
+ promptAllowFrom: promptMatrixAllowFrom,
173
+ };
174
+
175
+ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
176
+ channel,
177
+ getStatus: async ({ cfg }) => {
178
+ const account = resolveMatrixAccount({ cfg: cfg as CoreConfig });
179
+ const configured = account.configured;
180
+ const sdkReady = isMatrixSdkAvailable();
181
+ return {
182
+ channel,
183
+ configured,
184
+ statusLines: [
185
+ `Matrix: ${configured ? "configured" : "needs homeserver + access token or password"}`,
186
+ ],
187
+ selectionHint: !sdkReady
188
+ ? "install @vector-im/matrix-bot-sdk"
189
+ : configured
190
+ ? "configured"
191
+ : "needs auth",
192
+ };
193
+ },
194
+ configure: async ({ cfg, runtime, prompter, forceAllowFrom }) => {
195
+ let next = cfg as CoreConfig;
196
+ await ensureMatrixSdkInstalled({
197
+ runtime,
198
+ confirm: async (message) =>
199
+ await prompter.confirm({
200
+ message,
201
+ initialValue: true,
202
+ }),
203
+ });
204
+ const existing = next.channels?.matrix ?? {};
205
+ const account = resolveMatrixAccount({ cfg: next });
206
+ if (!account.configured) {
207
+ await noteMatrixAuthHelp(prompter);
208
+ }
209
+
210
+ const envHomeserver = process.env.MATRIX_HOMESERVER?.trim();
211
+ const envUserId = process.env.MATRIX_USER_ID?.trim();
212
+ const envAccessToken = process.env.MATRIX_ACCESS_TOKEN?.trim();
213
+ const envPassword = process.env.MATRIX_PASSWORD?.trim();
214
+ const envReady = Boolean(envHomeserver && (envAccessToken || (envUserId && envPassword)));
215
+
216
+ if (
217
+ envReady &&
218
+ !existing.homeserver &&
219
+ !existing.userId &&
220
+ !existing.accessToken &&
221
+ !existing.password
222
+ ) {
223
+ const useEnv = await prompter.confirm({
224
+ message: "Matrix env vars detected. Use env values?",
225
+ initialValue: true,
226
+ });
227
+ if (useEnv) {
228
+ next = {
229
+ ...next,
230
+ channels: {
231
+ ...next.channels,
232
+ matrix: {
233
+ ...next.channels?.matrix,
234
+ enabled: true,
235
+ },
236
+ },
237
+ };
238
+ if (forceAllowFrom) {
239
+ next = await promptMatrixAllowFrom({ cfg: next, prompter });
240
+ }
241
+ return { cfg: next };
242
+ }
243
+ }
244
+
245
+ const homeserver = String(
246
+ await prompter.text({
247
+ message: "Matrix homeserver URL",
248
+ initialValue: existing.homeserver ?? envHomeserver,
249
+ validate: (value) => {
250
+ const raw = String(value ?? "").trim();
251
+ if (!raw) return "Required";
252
+ if (!/^https?:\/\//i.test(raw)) return "Use a full URL (https://...)";
253
+ return undefined;
254
+ },
255
+ }),
256
+ ).trim();
257
+
258
+ let accessToken = existing.accessToken ?? "";
259
+ let password = existing.password ?? "";
260
+ let userId = existing.userId ?? "";
261
+
262
+ if (accessToken || password) {
263
+ const keep = await prompter.confirm({
264
+ message: "Matrix credentials already configured. Keep them?",
265
+ initialValue: true,
266
+ });
267
+ if (!keep) {
268
+ accessToken = "";
269
+ password = "";
270
+ userId = "";
271
+ }
272
+ }
273
+
274
+ if (!accessToken && !password) {
275
+ // Ask auth method FIRST before asking for user ID
276
+ const authMode = (await prompter.select({
277
+ message: "Matrix auth method",
278
+ options: [
279
+ { value: "token", label: "Access token (user ID fetched automatically)" },
280
+ { value: "password", label: "Password (requires user ID)" },
281
+ ],
282
+ })) as "token" | "password";
283
+
284
+ if (authMode === "token") {
285
+ accessToken = String(
286
+ await prompter.text({
287
+ message: "Matrix access token",
288
+ validate: (value) => (value?.trim() ? undefined : "Required"),
289
+ }),
290
+ ).trim();
291
+ // With access token, we can fetch the userId automatically - don't prompt for it
292
+ // The client.ts will use whoami() to get it
293
+ userId = "";
294
+ } else {
295
+ // Password auth requires user ID upfront
296
+ userId = String(
297
+ await prompter.text({
298
+ message: "Matrix user ID",
299
+ initialValue: existing.userId ?? envUserId,
300
+ validate: (value) => {
301
+ const raw = String(value ?? "").trim();
302
+ if (!raw) return "Required";
303
+ if (!raw.startsWith("@")) return "Matrix user IDs should start with @";
304
+ if (!raw.includes(":")) return "Matrix user IDs should include a server (:server)";
305
+ return undefined;
306
+ },
307
+ }),
308
+ ).trim();
309
+ password = String(
310
+ await prompter.text({
311
+ message: "Matrix password",
312
+ validate: (value) => (value?.trim() ? undefined : "Required"),
313
+ }),
314
+ ).trim();
315
+ }
316
+ }
317
+
318
+ const deviceName = String(
319
+ await prompter.text({
320
+ message: "Matrix device name (optional)",
321
+ initialValue: existing.deviceName ?? "OpenClaw Gateway",
322
+ }),
323
+ ).trim();
324
+
325
+ // Ask about E2EE encryption
326
+ const enableEncryption = await prompter.confirm({
327
+ message: "Enable end-to-end encryption (E2EE)?",
328
+ initialValue: existing.encryption ?? false,
329
+ });
330
+
331
+ next = {
332
+ ...next,
333
+ channels: {
334
+ ...next.channels,
335
+ matrix: {
336
+ ...next.channels?.matrix,
337
+ enabled: true,
338
+ homeserver,
339
+ userId: userId || undefined,
340
+ accessToken: accessToken || undefined,
341
+ password: password || undefined,
342
+ deviceName: deviceName || undefined,
343
+ encryption: enableEncryption || undefined,
344
+ },
345
+ },
346
+ };
347
+
348
+ if (forceAllowFrom) {
349
+ next = await promptMatrixAllowFrom({ cfg: next, prompter });
350
+ }
351
+
352
+ const existingGroups = next.channels?.matrix?.groups ?? next.channels?.matrix?.rooms;
353
+ const accessConfig = await promptChannelAccessConfig({
354
+ prompter,
355
+ label: "Matrix rooms",
356
+ currentPolicy: next.channels?.matrix?.groupPolicy ?? "allowlist",
357
+ currentEntries: Object.keys(existingGroups ?? {}),
358
+ placeholder: "!roomId:server, #alias:server, Project Room",
359
+ updatePrompt: Boolean(existingGroups),
360
+ });
361
+ if (accessConfig) {
362
+ if (accessConfig.policy !== "allowlist") {
363
+ next = setMatrixGroupPolicy(next, accessConfig.policy);
364
+ } else {
365
+ let roomKeys = accessConfig.entries;
366
+ if (accessConfig.entries.length > 0) {
367
+ try {
368
+ const resolvedIds: string[] = [];
369
+ const unresolved: string[] = [];
370
+ for (const entry of accessConfig.entries) {
371
+ const trimmed = entry.trim();
372
+ if (!trimmed) continue;
373
+ const cleaned = trimmed.replace(/^(room|channel):/i, "").trim();
374
+ if (cleaned.startsWith("!") && cleaned.includes(":")) {
375
+ resolvedIds.push(cleaned);
376
+ continue;
377
+ }
378
+ const matches = await listMatrixDirectoryGroupsLive({
379
+ cfg: next,
380
+ query: trimmed,
381
+ limit: 10,
382
+ });
383
+ const exact = matches.find(
384
+ (match) => (match.name ?? "").toLowerCase() === trimmed.toLowerCase(),
385
+ );
386
+ const best = exact ?? matches[0];
387
+ if (best?.id) {
388
+ resolvedIds.push(best.id);
389
+ } else {
390
+ unresolved.push(entry);
391
+ }
392
+ }
393
+ roomKeys = [
394
+ ...resolvedIds,
395
+ ...unresolved.map((entry) => entry.trim()).filter(Boolean),
396
+ ];
397
+ if (resolvedIds.length > 0 || unresolved.length > 0) {
398
+ await prompter.note(
399
+ [
400
+ resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined,
401
+ unresolved.length > 0
402
+ ? `Unresolved (kept as typed): ${unresolved.join(", ")}`
403
+ : undefined,
404
+ ]
405
+ .filter(Boolean)
406
+ .join("\n"),
407
+ "Matrix rooms",
408
+ );
409
+ }
410
+ } catch (err) {
411
+ await prompter.note(
412
+ `Room lookup failed; keeping entries as typed. ${String(err)}`,
413
+ "Matrix rooms",
414
+ );
415
+ }
416
+ }
417
+ next = setMatrixGroupPolicy(next, "allowlist");
418
+ next = setMatrixGroupRooms(next, roomKeys);
419
+ }
420
+ }
421
+
422
+ return { cfg: next };
423
+ },
424
+ dmPolicy,
425
+ disable: (cfg) => ({
426
+ ...(cfg as CoreConfig),
427
+ channels: {
428
+ ...(cfg as CoreConfig).channels,
429
+ matrix: { ...(cfg as CoreConfig).channels?.matrix, enabled: false },
430
+ },
431
+ }),
432
+ };
@@ -0,0 +1,53 @@
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
2
+
3
+ import { getMatrixRuntime } from "./runtime.js";
4
+ import { sendMessageMatrix, sendPollMatrix } from "./matrix/send.js";
5
+
6
+ export const matrixOutbound: ChannelOutboundAdapter = {
7
+ deliveryMode: "direct",
8
+ chunker: (text, limit) => getMatrixRuntime().channel.text.chunkMarkdownText(text, limit),
9
+ chunkerMode: "markdown",
10
+ textChunkLimit: 4000,
11
+ sendText: async ({ to, text, deps, replyToId, threadId }) => {
12
+ const send = deps?.sendMatrix ?? sendMessageMatrix;
13
+ const resolvedThreadId =
14
+ threadId !== undefined && threadId !== null ? String(threadId) : undefined;
15
+ const result = await send(to, text, {
16
+ replyToId: replyToId ?? undefined,
17
+ threadId: resolvedThreadId,
18
+ });
19
+ return {
20
+ channel: "matrix",
21
+ messageId: result.messageId,
22
+ roomId: result.roomId,
23
+ };
24
+ },
25
+ sendMedia: async ({ to, text, mediaUrl, deps, replyToId, threadId }) => {
26
+ const send = deps?.sendMatrix ?? sendMessageMatrix;
27
+ const resolvedThreadId =
28
+ threadId !== undefined && threadId !== null ? String(threadId) : undefined;
29
+ const result = await send(to, text, {
30
+ mediaUrl,
31
+ replyToId: replyToId ?? undefined,
32
+ threadId: resolvedThreadId,
33
+ });
34
+ return {
35
+ channel: "matrix",
36
+ messageId: result.messageId,
37
+ roomId: result.roomId,
38
+ };
39
+ },
40
+ sendPoll: async ({ to, poll, threadId }) => {
41
+ const resolvedThreadId =
42
+ threadId !== undefined && threadId !== null ? String(threadId) : undefined;
43
+ const result = await sendPollMatrix(to, poll, {
44
+ threadId: resolvedThreadId,
45
+ });
46
+ return {
47
+ channel: "matrix",
48
+ messageId: result.eventId,
49
+ roomId: result.roomId,
50
+ pollId: result.eventId,
51
+ };
52
+ },
53
+ };
@@ -0,0 +1,89 @@
1
+ import type {
2
+ ChannelDirectoryEntry,
3
+ ChannelResolveKind,
4
+ ChannelResolveResult,
5
+ RuntimeEnv,
6
+ } from "openclaw/plugin-sdk";
7
+
8
+ import {
9
+ listMatrixDirectoryGroupsLive,
10
+ listMatrixDirectoryPeersLive,
11
+ } from "./directory-live.js";
12
+
13
+ function pickBestGroupMatch(
14
+ matches: ChannelDirectoryEntry[],
15
+ query: string,
16
+ ): ChannelDirectoryEntry | undefined {
17
+ if (matches.length === 0) return undefined;
18
+ const normalized = query.trim().toLowerCase();
19
+ if (normalized) {
20
+ const exact = matches.find((match) => {
21
+ const name = match.name?.trim().toLowerCase();
22
+ const handle = match.handle?.trim().toLowerCase();
23
+ const id = match.id.trim().toLowerCase();
24
+ return name === normalized || handle === normalized || id === normalized;
25
+ });
26
+ if (exact) return exact;
27
+ }
28
+ return matches[0];
29
+ }
30
+
31
+ export async function resolveMatrixTargets(params: {
32
+ cfg: unknown;
33
+ inputs: string[];
34
+ kind: ChannelResolveKind;
35
+ runtime?: RuntimeEnv;
36
+ }): Promise<ChannelResolveResult[]> {
37
+ const results: ChannelResolveResult[] = [];
38
+ for (const input of params.inputs) {
39
+ const trimmed = input.trim();
40
+ if (!trimmed) {
41
+ results.push({ input, resolved: false, note: "empty input" });
42
+ continue;
43
+ }
44
+ if (params.kind === "user") {
45
+ if (trimmed.startsWith("@") && trimmed.includes(":")) {
46
+ results.push({ input, resolved: true, id: trimmed });
47
+ continue;
48
+ }
49
+ try {
50
+ const matches = await listMatrixDirectoryPeersLive({
51
+ cfg: params.cfg,
52
+ query: trimmed,
53
+ limit: 5,
54
+ });
55
+ const best = matches[0];
56
+ results.push({
57
+ input,
58
+ resolved: Boolean(best?.id),
59
+ id: best?.id,
60
+ name: best?.name,
61
+ note: matches.length > 1 ? "multiple matches; chose first" : undefined,
62
+ });
63
+ } catch (err) {
64
+ params.runtime?.error?.(`matrix resolve failed: ${String(err)}`);
65
+ results.push({ input, resolved: false, note: "lookup failed" });
66
+ }
67
+ continue;
68
+ }
69
+ try {
70
+ const matches = await listMatrixDirectoryGroupsLive({
71
+ cfg: params.cfg,
72
+ query: trimmed,
73
+ limit: 5,
74
+ });
75
+ const best = pickBestGroupMatch(matches, trimmed);
76
+ results.push({
77
+ input,
78
+ resolved: Boolean(best?.id),
79
+ id: best?.id,
80
+ name: best?.name,
81
+ note: matches.length > 1 ? "multiple matches; chose first" : undefined,
82
+ });
83
+ } catch (err) {
84
+ params.runtime?.error?.(`matrix resolve failed: ${String(err)}`);
85
+ results.push({ input, resolved: false, note: "lookup failed" });
86
+ }
87
+ }
88
+ return results;
89
+ }
package/src/runtime.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
+
3
+ let runtime: PluginRuntime | null = null;
4
+
5
+ export function setMatrixRuntime(next: PluginRuntime) {
6
+ runtime = next;
7
+ }
8
+
9
+ export function getMatrixRuntime(): PluginRuntime {
10
+ if (!runtime) {
11
+ throw new Error("Matrix runtime not initialized");
12
+ }
13
+ return runtime;
14
+ }