@badgerclaw/connect 1.0.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/SETUP.md +131 -0
  3. package/index.ts +23 -0
  4. package/openclaw.plugin.json +1 -0
  5. package/package.json +32 -0
  6. package/src/actions.ts +195 -0
  7. package/src/channel.ts +461 -0
  8. package/src/config-schema.ts +62 -0
  9. package/src/connect.ts +17 -0
  10. package/src/directory-live.ts +209 -0
  11. package/src/group-mentions.ts +52 -0
  12. package/src/matrix/accounts.ts +114 -0
  13. package/src/matrix/actions/client.ts +47 -0
  14. package/src/matrix/actions/limits.ts +6 -0
  15. package/src/matrix/actions/messages.ts +126 -0
  16. package/src/matrix/actions/pins.ts +84 -0
  17. package/src/matrix/actions/reactions.ts +102 -0
  18. package/src/matrix/actions/room.ts +85 -0
  19. package/src/matrix/actions/summary.ts +75 -0
  20. package/src/matrix/actions/types.ts +85 -0
  21. package/src/matrix/actions.ts +15 -0
  22. package/src/matrix/active-client.ts +32 -0
  23. package/src/matrix/client/config.ts +245 -0
  24. package/src/matrix/client/create-client.ts +125 -0
  25. package/src/matrix/client/logging.ts +46 -0
  26. package/src/matrix/client/runtime.ts +4 -0
  27. package/src/matrix/client/shared.ts +210 -0
  28. package/src/matrix/client/startup.ts +29 -0
  29. package/src/matrix/client/storage.ts +131 -0
  30. package/src/matrix/client/types.ts +34 -0
  31. package/src/matrix/client-bootstrap.ts +47 -0
  32. package/src/matrix/client.ts +14 -0
  33. package/src/matrix/credentials.ts +125 -0
  34. package/src/matrix/deps.ts +126 -0
  35. package/src/matrix/format.ts +22 -0
  36. package/src/matrix/index.ts +11 -0
  37. package/src/matrix/monitor/access-policy.ts +126 -0
  38. package/src/matrix/monitor/allowlist.ts +94 -0
  39. package/src/matrix/monitor/auto-join.ts +72 -0
  40. package/src/matrix/monitor/direct.ts +152 -0
  41. package/src/matrix/monitor/events.ts +168 -0
  42. package/src/matrix/monitor/handler.ts +768 -0
  43. package/src/matrix/monitor/inbound-body.ts +28 -0
  44. package/src/matrix/monitor/index.ts +414 -0
  45. package/src/matrix/monitor/location.ts +100 -0
  46. package/src/matrix/monitor/media.ts +118 -0
  47. package/src/matrix/monitor/mentions.ts +62 -0
  48. package/src/matrix/monitor/replies.ts +124 -0
  49. package/src/matrix/monitor/room-info.ts +55 -0
  50. package/src/matrix/monitor/rooms.ts +47 -0
  51. package/src/matrix/monitor/threads.ts +68 -0
  52. package/src/matrix/monitor/types.ts +39 -0
  53. package/src/matrix/poll-types.ts +167 -0
  54. package/src/matrix/probe.ts +69 -0
  55. package/src/matrix/sdk-runtime.ts +18 -0
  56. package/src/matrix/send/client.ts +99 -0
  57. package/src/matrix/send/formatting.ts +93 -0
  58. package/src/matrix/send/media.ts +230 -0
  59. package/src/matrix/send/targets.ts +150 -0
  60. package/src/matrix/send/types.ts +110 -0
  61. package/src/matrix/send-queue.ts +28 -0
  62. package/src/matrix/send.ts +267 -0
  63. package/src/onboarding.ts +331 -0
  64. package/src/outbound.ts +58 -0
  65. package/src/resolve-targets.ts +125 -0
  66. package/src/runtime.ts +6 -0
  67. package/src/secret-input.ts +13 -0
  68. package/src/test-mocks.ts +53 -0
  69. package/src/tool-actions.ts +164 -0
  70. package/src/types.ts +118 -0
package/src/channel.ts ADDED
@@ -0,0 +1,461 @@
1
+ import {
2
+ buildOpenGroupPolicyWarning,
3
+ collectAllowlistProviderGroupPolicyWarnings,
4
+ createScopedAccountConfigAccessors,
5
+ createScopedChannelConfigBase,
6
+ createScopedDmSecurityResolver,
7
+ } from "openclaw/plugin-sdk/compat";
8
+ import {
9
+ applyAccountNameToChannelSection,
10
+ buildChannelConfigSchema,
11
+ buildProbeChannelStatusSummary,
12
+ collectStatusIssuesFromLastError,
13
+ DEFAULT_ACCOUNT_ID,
14
+ normalizeAccountId,
15
+ PAIRING_APPROVED_MESSAGE,
16
+ type ChannelPlugin,
17
+ } from "openclaw/plugin-sdk/matrix";
18
+ import { buildTrafficStatusSummary } from "/opt/homebrew/lib/node_modules/openclaw/extensions/shared/channel-status-summary.js";
19
+ import { matrixMessageActions } from "./actions.js";
20
+ import { MatrixConfigSchema } from "./config-schema.js";
21
+ import { listMatrixDirectoryGroupsLive, listMatrixDirectoryPeersLive } from "./directory-live.js";
22
+ import {
23
+ resolveMatrixGroupRequireMention,
24
+ resolveMatrixGroupToolPolicy,
25
+ } from "./group-mentions.js";
26
+ import {
27
+ listMatrixAccountIds,
28
+ resolveMatrixAccountConfig,
29
+ resolveDefaultMatrixAccountId,
30
+ resolveMatrixAccount,
31
+ type ResolvedMatrixAccount,
32
+ } from "./matrix/accounts.js";
33
+ import { resolveMatrixAuth } from "./matrix/client.js";
34
+ import { normalizeMatrixAllowList, normalizeMatrixUserId } from "./matrix/monitor/allowlist.js";
35
+ import { probeMatrix } from "./matrix/probe.js";
36
+ import { sendMessageMatrix } from "./matrix/send.js";
37
+ import { badgerclawOnboardingAdapter } from "./onboarding.js";
38
+ import { matrixOutbound } from "./outbound.js";
39
+ import { resolveMatrixTargets } from "./resolve-targets.js";
40
+ import { normalizeSecretInputString } from "./secret-input.js";
41
+ import type { CoreConfig } from "./types.js";
42
+
43
+ // Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
44
+ let matrixStartupLock: Promise<void> = Promise.resolve();
45
+
46
+ const meta = {
47
+ id: "badgerclaw",
48
+ label: "BadgerClaw",
49
+ selectionLabel: "BadgerClaw (plugin)",
50
+ docsPath: "/channels/badgerclaw",
51
+ docsLabel: "badgerclaw",
52
+ blurb: "open protocol; configure a homeserver + access token.",
53
+ order: 70,
54
+ quickstartAllowFrom: true,
55
+ };
56
+
57
+ function normalizeMatrixMessagingTarget(raw: string): string | undefined {
58
+ let normalized = raw.trim();
59
+ if (!normalized) {
60
+ return undefined;
61
+ }
62
+ const lowered = normalized.toLowerCase();
63
+ if (lowered.startsWith("badgerclaw:")) {
64
+ normalized = normalized.slice("badgerclaw:".length).trim();
65
+ }
66
+ const stripped = normalized.replace(/^(room|channel|user):/i, "").trim();
67
+ return stripped || undefined;
68
+ }
69
+
70
+ function buildMatrixConfigUpdate(
71
+ cfg: CoreConfig,
72
+ input: {
73
+ homeserver?: string;
74
+ userId?: string;
75
+ accessToken?: string;
76
+ password?: string;
77
+ deviceName?: string;
78
+ initialSyncLimit?: number;
79
+ },
80
+ ): CoreConfig {
81
+ const existing = cfg.channels?.badgerclaw ?? {};
82
+ return {
83
+ ...cfg,
84
+ channels: {
85
+ ...cfg.channels,
86
+ badgerclaw: {
87
+ ...existing,
88
+ enabled: true,
89
+ ...(input.homeserver ? { homeserver: input.homeserver } : {}),
90
+ ...(input.userId ? { userId: input.userId } : {}),
91
+ ...(input.accessToken ? { accessToken: input.accessToken } : {}),
92
+ ...(input.password ? { password: input.password } : {}),
93
+ ...(input.deviceName ? { deviceName: input.deviceName } : {}),
94
+ ...(typeof input.initialSyncLimit === "number"
95
+ ? { initialSyncLimit: input.initialSyncLimit }
96
+ : {}),
97
+ },
98
+ },
99
+ };
100
+ }
101
+
102
+ const matrixConfigAccessors = createScopedAccountConfigAccessors({
103
+ resolveAccount: ({ cfg, accountId }) =>
104
+ resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }),
105
+ resolveAllowFrom: (account) => account.dm?.allowFrom,
106
+ formatAllowFrom: (allowFrom) => normalizeMatrixAllowList(allowFrom),
107
+ });
108
+
109
+ const matrixConfigBase = createScopedChannelConfigBase<ResolvedMatrixAccount, CoreConfig>({
110
+ sectionKey: "badgerclaw",
111
+ listAccountIds: listMatrixAccountIds,
112
+ resolveAccount: (cfg, accountId) => resolveMatrixAccount({ cfg, accountId }),
113
+ defaultAccountId: resolveDefaultMatrixAccountId,
114
+ clearBaseFields: [
115
+ "name",
116
+ "homeserver",
117
+ "userId",
118
+ "accessToken",
119
+ "password",
120
+ "deviceName",
121
+ "initialSyncLimit",
122
+ ],
123
+ });
124
+
125
+ const resolveMatrixDmPolicy = createScopedDmSecurityResolver<ResolvedMatrixAccount>({
126
+ channelKey: "badgerclaw",
127
+ resolvePolicy: (account) => account.config.dm?.policy,
128
+ resolveAllowFrom: (account) => account.config.dm?.allowFrom,
129
+ allowFromPathSuffix: "dm.",
130
+ normalizeEntry: (raw) => normalizeMatrixUserId(raw),
131
+ });
132
+
133
+ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
134
+ id: "badgerclaw",
135
+ meta,
136
+ onboarding: badgerclawOnboardingAdapter,
137
+ pairing: {
138
+ idLabel: "matrixUserId",
139
+ normalizeAllowEntry: (entry) => entry.replace(/^badgerclaw:/i, ""),
140
+ notifyApproval: async ({ id }) => {
141
+ await sendMessageMatrix(`user:${id}`, PAIRING_APPROVED_MESSAGE);
142
+ },
143
+ },
144
+ capabilities: {
145
+ chatTypes: ["direct", "group", "thread"],
146
+ polls: true,
147
+ reactions: true,
148
+ threads: true,
149
+ media: true,
150
+ },
151
+ reload: { configPrefixes: ["channels.badgerclaw"] },
152
+ configSchema: buildChannelConfigSchema(MatrixConfigSchema),
153
+ config: {
154
+ ...matrixConfigBase,
155
+ isConfigured: (account) => account.configured,
156
+ describeAccount: (account) => ({
157
+ accountId: account.accountId,
158
+ name: account.name,
159
+ enabled: account.enabled,
160
+ configured: account.configured,
161
+ baseUrl: account.homeserver,
162
+ }),
163
+ ...matrixConfigAccessors,
164
+ },
165
+ security: {
166
+ resolveDmPolicy: resolveMatrixDmPolicy,
167
+ collectWarnings: ({ account, cfg }) => {
168
+ return collectAllowlistProviderGroupPolicyWarnings({
169
+ cfg: cfg as CoreConfig,
170
+ providerConfigPresent: (cfg as CoreConfig).channels?.badgerclaw !== undefined,
171
+ configuredGroupPolicy: account.config.groupPolicy,
172
+ collect: (groupPolicy) =>
173
+ groupPolicy === "open"
174
+ ? [
175
+ buildOpenGroupPolicyWarning({
176
+ surface: "BadgerClaw rooms",
177
+ openBehavior: "allows any room to trigger (mention-gated)",
178
+ remediation:
179
+ 'Set channels.badgerclaw.groupPolicy="allowlist" + channels.badgerclaw.groups (and optionally channels.badgerclaw.groupAllowFrom) to restrict rooms',
180
+ }),
181
+ ]
182
+ : [],
183
+ });
184
+ },
185
+ },
186
+ groups: {
187
+ resolveRequireMention: resolveMatrixGroupRequireMention,
188
+ resolveToolPolicy: resolveMatrixGroupToolPolicy,
189
+ },
190
+ threading: {
191
+ resolveReplyToMode: ({ cfg, accountId }) =>
192
+ resolveMatrixAccountConfig({ cfg: cfg as CoreConfig, accountId }).replyToMode ?? "off",
193
+ buildToolContext: ({ context, hasRepliedRef }) => {
194
+ const currentTarget = context.To;
195
+ return {
196
+ currentChannelId: currentTarget?.trim() || undefined,
197
+ currentThreadTs:
198
+ context.MessageThreadId != null ? String(context.MessageThreadId) : context.ReplyToId,
199
+ hasRepliedRef,
200
+ };
201
+ },
202
+ },
203
+ messaging: {
204
+ normalizeTarget: normalizeMatrixMessagingTarget,
205
+ targetResolver: {
206
+ looksLikeId: (raw) => {
207
+ const trimmed = raw.trim();
208
+ if (!trimmed) {
209
+ return false;
210
+ }
211
+ if (/^(badgerclaw:)?[!#@]/i.test(trimmed)) {
212
+ return true;
213
+ }
214
+ return trimmed.includes(":");
215
+ },
216
+ hint: "<room|alias|user>",
217
+ },
218
+ },
219
+ directory: {
220
+ self: async () => null,
221
+ listPeers: async ({ cfg, accountId, query, limit }) => {
222
+ const account = resolveMatrixAccount({ cfg: cfg as CoreConfig, accountId });
223
+ const q = query?.trim().toLowerCase() || "";
224
+ const ids = new Set<string>();
225
+
226
+ for (const entry of account.config.dm?.allowFrom ?? []) {
227
+ const raw = String(entry).trim();
228
+ if (!raw || raw === "*") {
229
+ continue;
230
+ }
231
+ ids.add(raw.replace(/^badgerclaw:/i, ""));
232
+ }
233
+
234
+ for (const entry of account.config.groupAllowFrom ?? []) {
235
+ const raw = String(entry).trim();
236
+ if (!raw || raw === "*") {
237
+ continue;
238
+ }
239
+ ids.add(raw.replace(/^badgerclaw:/i, ""));
240
+ }
241
+
242
+ const groups = account.config.groups ?? account.config.rooms ?? {};
243
+ for (const room of Object.values(groups)) {
244
+ for (const entry of room.users ?? []) {
245
+ const raw = String(entry).trim();
246
+ if (!raw || raw === "*") {
247
+ continue;
248
+ }
249
+ ids.add(raw.replace(/^badgerclaw:/i, ""));
250
+ }
251
+ }
252
+
253
+ return Array.from(ids)
254
+ .map((raw) => raw.trim())
255
+ .filter(Boolean)
256
+ .map((raw) => {
257
+ const lowered = raw.toLowerCase();
258
+ const cleaned = lowered.startsWith("user:") ? raw.slice("user:".length).trim() : raw;
259
+ if (cleaned.startsWith("@")) {
260
+ return `user:${cleaned}`;
261
+ }
262
+ return cleaned;
263
+ })
264
+ .filter((id) => (q ? id.toLowerCase().includes(q) : true))
265
+ .slice(0, limit && limit > 0 ? limit : undefined)
266
+ .map((id) => {
267
+ const raw = id.startsWith("user:") ? id.slice("user:".length) : id;
268
+ const incomplete = !raw.startsWith("@") || !raw.includes(":");
269
+ return {
270
+ kind: "user",
271
+ id,
272
+ ...(incomplete ? { name: "incomplete id; expected @user:server" } : {}),
273
+ };
274
+ });
275
+ },
276
+ listGroups: async ({ cfg, accountId, query, limit }) => {
277
+ const account = resolveMatrixAccount({ cfg: cfg as CoreConfig, accountId });
278
+ const q = query?.trim().toLowerCase() || "";
279
+ const groups = account.config.groups ?? account.config.rooms ?? {};
280
+ const ids = Object.keys(groups)
281
+ .map((raw) => raw.trim())
282
+ .filter((raw) => Boolean(raw) && raw !== "*")
283
+ .map((raw) => raw.replace(/^badgerclaw:/i, ""))
284
+ .map((raw) => {
285
+ const lowered = raw.toLowerCase();
286
+ if (lowered.startsWith("room:") || lowered.startsWith("channel:")) {
287
+ return raw;
288
+ }
289
+ if (raw.startsWith("!")) {
290
+ return `room:${raw}`;
291
+ }
292
+ return raw;
293
+ })
294
+ .filter((id) => (q ? id.toLowerCase().includes(q) : true))
295
+ .slice(0, limit && limit > 0 ? limit : undefined)
296
+ .map((id) => ({ kind: "group", id }) as const);
297
+ return ids;
298
+ },
299
+ listPeersLive: async ({ cfg, accountId, query, limit }) =>
300
+ listMatrixDirectoryPeersLive({ cfg, accountId, query, limit }),
301
+ listGroupsLive: async ({ cfg, accountId, query, limit }) =>
302
+ listMatrixDirectoryGroupsLive({ cfg, accountId, query, limit }),
303
+ },
304
+ resolver: {
305
+ resolveTargets: async ({ cfg, inputs, kind, runtime }) =>
306
+ resolveMatrixTargets({ cfg, inputs, kind, runtime }),
307
+ },
308
+ actions: matrixMessageActions,
309
+ setup: {
310
+ resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
311
+ applyAccountName: ({ cfg, accountId, name }) =>
312
+ applyAccountNameToChannelSection({
313
+ cfg: cfg as CoreConfig,
314
+ channelKey: "badgerclaw",
315
+ accountId,
316
+ name,
317
+ }),
318
+ validateInput: ({ input }) => {
319
+ if (input.useEnv) {
320
+ return null;
321
+ }
322
+ if (!input.homeserver?.trim()) {
323
+ return "BadgerClaw requires --homeserver";
324
+ }
325
+ const accessToken = input.accessToken?.trim();
326
+ const password = normalizeSecretInputString(input.password);
327
+ const userId = input.userId?.trim();
328
+ if (!accessToken && !password) {
329
+ return "BadgerClaw requires --access-token or --password";
330
+ }
331
+ if (!accessToken) {
332
+ if (!userId) {
333
+ return "BadgerClaw requires --user-id when using --password";
334
+ }
335
+ if (!password) {
336
+ return "BadgerClaw requires --password when using --user-id";
337
+ }
338
+ }
339
+ return null;
340
+ },
341
+ applyAccountConfig: ({ cfg, input }) => {
342
+ const namedConfig = applyAccountNameToChannelSection({
343
+ cfg: cfg as CoreConfig,
344
+ channelKey: "badgerclaw",
345
+ accountId: DEFAULT_ACCOUNT_ID,
346
+ name: input.name,
347
+ });
348
+ if (input.useEnv) {
349
+ return {
350
+ ...namedConfig,
351
+ channels: {
352
+ ...namedConfig.channels,
353
+ badgerclaw: {
354
+ ...namedConfig.channels?.badgerclaw,
355
+ enabled: true,
356
+ },
357
+ },
358
+ } as CoreConfig;
359
+ }
360
+ return buildMatrixConfigUpdate(namedConfig as CoreConfig, {
361
+ homeserver: input.homeserver?.trim(),
362
+ userId: input.userId?.trim(),
363
+ accessToken: input.accessToken?.trim(),
364
+ password: normalizeSecretInputString(input.password),
365
+ deviceName: input.deviceName?.trim(),
366
+ initialSyncLimit: input.initialSyncLimit,
367
+ });
368
+ },
369
+ },
370
+ outbound: matrixOutbound,
371
+ status: {
372
+ defaultRuntime: {
373
+ accountId: DEFAULT_ACCOUNT_ID,
374
+ running: false,
375
+ lastStartAt: null,
376
+ lastStopAt: null,
377
+ lastError: null,
378
+ },
379
+ collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("badgerclaw", accounts),
380
+ buildChannelSummary: ({ snapshot }) =>
381
+ buildProbeChannelStatusSummary(snapshot, { baseUrl: snapshot.baseUrl ?? null }),
382
+ probeAccount: async ({ account, timeoutMs, cfg }) => {
383
+ try {
384
+ const auth = await resolveMatrixAuth({
385
+ cfg: cfg as CoreConfig,
386
+ accountId: account.accountId,
387
+ });
388
+ return await probeMatrix({
389
+ homeserver: auth.homeserver,
390
+ accessToken: auth.accessToken,
391
+ userId: auth.userId,
392
+ timeoutMs,
393
+ });
394
+ } catch (err) {
395
+ return {
396
+ ok: false,
397
+ error: err instanceof Error ? err.message : String(err),
398
+ elapsedMs: 0,
399
+ };
400
+ }
401
+ },
402
+ buildAccountSnapshot: ({ account, runtime, probe }) => ({
403
+ accountId: account.accountId,
404
+ name: account.name,
405
+ enabled: account.enabled,
406
+ configured: account.configured,
407
+ baseUrl: account.homeserver,
408
+ running: runtime?.running ?? false,
409
+ lastStartAt: runtime?.lastStartAt ?? null,
410
+ lastStopAt: runtime?.lastStopAt ?? null,
411
+ lastError: runtime?.lastError ?? null,
412
+ probe,
413
+ lastProbeAt: runtime?.lastProbeAt ?? null,
414
+ ...buildTrafficStatusSummary(runtime),
415
+ }),
416
+ },
417
+ gateway: {
418
+ startAccount: async (ctx) => {
419
+ const account = ctx.account;
420
+ ctx.setStatus({
421
+ accountId: account.accountId,
422
+ baseUrl: account.homeserver,
423
+ });
424
+ ctx.log?.info(`[${account.accountId}] starting provider (${account.homeserver ?? "badgerclaw"})`);
425
+
426
+ // Serialize startup: wait for any previous startup to complete import phase.
427
+ // This works around a race condition with concurrent dynamic imports.
428
+ //
429
+ // INVARIANT: The import() below cannot hang because:
430
+ // 1. It only loads local ESM modules with no circular awaits
431
+ // 2. Module initialization is synchronous (no top-level await in ./matrix/index.js)
432
+ // 3. The lock only serializes the import phase, not the provider startup
433
+ const previousLock = matrixStartupLock;
434
+ let releaseLock: () => void = () => {};
435
+ matrixStartupLock = new Promise<void>((resolve) => {
436
+ releaseLock = resolve;
437
+ });
438
+ await previousLock;
439
+
440
+ // Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
441
+ // Wrap in try/finally to ensure lock is released even if import fails.
442
+ let monitorMatrixProvider: typeof import("./matrix/index.js").monitorMatrixProvider;
443
+ try {
444
+ const module = await import("./matrix/index.js");
445
+ monitorMatrixProvider = module.monitorMatrixProvider;
446
+ } finally {
447
+ // Release lock after import completes or fails
448
+ releaseLock();
449
+ }
450
+
451
+ return monitorMatrixProvider({
452
+ runtime: ctx.runtime,
453
+ abortSignal: ctx.abortSignal,
454
+ mediaMaxMb: account.config.mediaMaxMb,
455
+ initialSyncLimit: account.config.initialSyncLimit,
456
+ replyToMode: account.config.replyToMode,
457
+ accountId: account.accountId,
458
+ });
459
+ },
460
+ },
461
+ };
@@ -0,0 +1,62 @@
1
+ import {
2
+ AllowFromListSchema,
3
+ buildNestedDmConfigSchema,
4
+ DmPolicySchema,
5
+ GroupPolicySchema,
6
+ } from "openclaw/plugin-sdk/compat";
7
+ import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk/matrix";
8
+ import { z } from "zod";
9
+ import { buildSecretInputSchema } from "./secret-input.js";
10
+
11
+ const matrixActionSchema = z
12
+ .object({
13
+ reactions: z.boolean().optional(),
14
+ messages: z.boolean().optional(),
15
+ pins: z.boolean().optional(),
16
+ memberInfo: z.boolean().optional(),
17
+ channelInfo: z.boolean().optional(),
18
+ })
19
+ .optional();
20
+
21
+ const matrixRoomSchema = z
22
+ .object({
23
+ enabled: z.boolean().optional(),
24
+ allow: z.boolean().optional(),
25
+ requireMention: z.boolean().optional(),
26
+ tools: ToolPolicySchema,
27
+ autoReply: z.boolean().optional(),
28
+ users: AllowFromListSchema,
29
+ skills: z.array(z.string()).optional(),
30
+ systemPrompt: z.string().optional(),
31
+ })
32
+ .optional();
33
+
34
+ export const MatrixConfigSchema = z.object({
35
+ name: z.string().optional(),
36
+ enabled: z.boolean().optional(),
37
+ defaultAccount: z.string().optional(),
38
+ accounts: z.record(z.string(), z.unknown()).optional(),
39
+ markdown: MarkdownConfigSchema,
40
+ homeserver: z.string().optional(),
41
+ userId: z.string().optional(),
42
+ accessToken: z.string().optional(),
43
+ password: buildSecretInputSchema().optional(),
44
+ deviceName: z.string().optional(),
45
+ initialSyncLimit: z.number().optional(),
46
+ encryption: z.boolean().optional(),
47
+ allowlistOnly: z.boolean().optional(),
48
+ groupPolicy: GroupPolicySchema.optional(),
49
+ replyToMode: z.enum(["off", "first", "all"]).optional(),
50
+ threadReplies: z.enum(["off", "inbound", "always"]).optional(),
51
+ textChunkLimit: z.number().optional(),
52
+ chunkMode: z.enum(["length", "newline"]).optional(),
53
+ responsePrefix: z.string().optional(),
54
+ mediaMaxMb: z.number().optional(),
55
+ autoJoin: z.enum(["always", "allowlist", "off"]).optional(),
56
+ autoJoinAllowlist: AllowFromListSchema,
57
+ groupAllowFrom: AllowFromListSchema,
58
+ dm: buildNestedDmConfigSchema(),
59
+ groups: z.object({}).catchall(matrixRoomSchema).optional(),
60
+ rooms: z.object({}).catchall(matrixRoomSchema).optional(),
61
+ actions: matrixActionSchema,
62
+ });
package/src/connect.ts ADDED
@@ -0,0 +1,17 @@
1
+ export async function redeemPairingCode(code: string): Promise<{
2
+ homeserver: string;
3
+ access_token: string;
4
+ user_id: string;
5
+ bot_name: string;
6
+ }> {
7
+ const resp = await fetch("https://api.badger.signout.io/api/v1/pairing/redeem", {
8
+ method: "POST",
9
+ headers: { "Content-Type": "application/json" },
10
+ body: JSON.stringify({ code }),
11
+ });
12
+ if (!resp.ok) {
13
+ const err = await resp.json().catch(() => ({ detail: resp.statusText }));
14
+ throw new Error(err.detail || "Failed to redeem pairing code");
15
+ }
16
+ return resp.json();
17
+ }