badgerclaw 0.1.7 → 1.4.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.
Potentially problematic release.
This version of badgerclaw might be problematic. Click here for more details.
- package/CHANGELOG.md +104 -0
- package/SETUP.md +291 -0
- package/index.ts +47 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +32 -34
- package/scripts/postinstall.js +34 -0
- package/src/actions.ts +195 -0
- package/src/channel.ts +461 -0
- package/src/config-schema.ts +62 -0
- package/src/connect.ts +17 -0
- package/src/directory-live.ts +209 -0
- package/src/group-mentions.ts +103 -0
- package/src/matrix/accounts.ts +114 -0
- package/src/matrix/actions/client.ts +47 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.ts +126 -0
- package/src/matrix/actions/pins.ts +84 -0
- package/src/matrix/actions/reactions.ts +102 -0
- package/src/matrix/actions/room.ts +85 -0
- package/src/matrix/actions/summary.ts +75 -0
- package/src/matrix/actions/types.ts +85 -0
- package/src/matrix/actions.ts +15 -0
- package/src/matrix/active-client.ts +32 -0
- package/src/matrix/client/backup.ts +91 -0
- package/src/matrix/client/config.ts +274 -0
- package/src/matrix/client/create-client.ts +125 -0
- package/src/matrix/client/logging.ts +46 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.ts +223 -0
- package/src/matrix/client/startup.ts +29 -0
- package/src/matrix/client/storage.ts +131 -0
- package/src/matrix/client/types.ts +34 -0
- package/src/matrix/client-bootstrap.ts +47 -0
- package/src/matrix/client.ts +14 -0
- package/src/matrix/credentials.ts +125 -0
- package/src/matrix/deps.ts +126 -0
- package/src/matrix/format.ts +22 -0
- package/src/matrix/index.ts +11 -0
- package/src/matrix/monitor/access-policy.ts +126 -0
- package/src/matrix/monitor/allowlist.ts +94 -0
- package/src/matrix/monitor/auto-join.ts +126 -0
- package/src/matrix/monitor/bot-commands.ts +431 -0
- package/src/matrix/monitor/chat-history.ts +75 -0
- package/src/matrix/monitor/direct.ts +152 -0
- package/src/matrix/monitor/events.ts +250 -0
- package/src/matrix/monitor/handler.ts +847 -0
- package/src/matrix/monitor/inbound-body.ts +28 -0
- package/src/matrix/monitor/index.ts +414 -0
- package/src/matrix/monitor/location.ts +100 -0
- package/src/matrix/monitor/media.ts +118 -0
- package/src/matrix/monitor/mentions.ts +62 -0
- package/src/matrix/monitor/replies.ts +124 -0
- package/src/matrix/monitor/room-info.ts +55 -0
- package/src/matrix/monitor/rooms.ts +47 -0
- package/src/matrix/monitor/threads.ts +68 -0
- package/src/matrix/monitor/types.ts +39 -0
- package/src/matrix/poll-types.ts +167 -0
- package/src/matrix/probe.ts +69 -0
- package/src/matrix/sdk-runtime.ts +18 -0
- package/src/matrix/send/client.ts +99 -0
- package/src/matrix/send/formatting.ts +93 -0
- package/src/matrix/send/media.ts +230 -0
- package/src/matrix/send/targets.ts +150 -0
- package/src/matrix/send/types.ts +110 -0
- package/src/matrix/send-queue.ts +28 -0
- package/src/matrix/send.ts +267 -0
- package/src/onboarding.ts +350 -0
- package/src/outbound.ts +58 -0
- package/src/resolve-targets.ts +125 -0
- package/src/runtime.ts +6 -0
- package/src/secret-input.ts +13 -0
- package/src/test-mocks.ts +53 -0
- package/src/tool-actions.ts +164 -0
- package/src/types.ts +121 -0
- package/README.md +0 -32
- package/dist/commands/autopair.d.ts +0 -3
- package/dist/commands/autopair.js +0 -102
- package/dist/commands/autopair.js.map +0 -1
- package/dist/commands/bot.d.ts +0 -2
- package/dist/commands/bot.js +0 -94
- package/dist/commands/bot.js.map +0 -1
- package/dist/commands/login.d.ts +0 -2
- package/dist/commands/login.js +0 -88
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -2
- package/dist/commands/logout.js +0 -36
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/status.d.ts +0 -2
- package/dist/commands/status.js +0 -23
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/watch.d.ts +0 -2
- package/dist/commands/watch.js +0 -29
- package/dist/commands/watch.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -23
- package/dist/index.js.map +0 -1
- package/dist/lib/api.d.ts +0 -4
- package/dist/lib/api.js +0 -37
- package/dist/lib/api.js.map +0 -1
- package/dist/lib/auth.d.ts +0 -11
- package/dist/lib/auth.js +0 -48
- package/dist/lib/auth.js.map +0 -1
- package/dist/lib/pkce.d.ts +0 -2
- package/dist/lib/pkce.js +0 -15
- package/dist/lib/pkce.js.map +0 -1
package/src/actions.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createActionGate,
|
|
3
|
+
readNumberParam,
|
|
4
|
+
readStringParam,
|
|
5
|
+
type ChannelMessageActionAdapter,
|
|
6
|
+
type ChannelMessageActionContext,
|
|
7
|
+
type ChannelMessageActionName,
|
|
8
|
+
type ChannelToolSend,
|
|
9
|
+
} from "openclaw/plugin-sdk/matrix";
|
|
10
|
+
import { resolveMatrixAccount } from "./matrix/accounts.js";
|
|
11
|
+
import { handleMatrixAction } from "./tool-actions.js";
|
|
12
|
+
import type { CoreConfig } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export const matrixMessageActions: ChannelMessageActionAdapter = {
|
|
15
|
+
listActions: ({ cfg }) => {
|
|
16
|
+
const account = resolveMatrixAccount({ cfg: cfg as CoreConfig });
|
|
17
|
+
if (!account.enabled || !account.configured) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const gate = createActionGate((cfg as CoreConfig).channels?.badgerclaw?.actions);
|
|
21
|
+
const actions = new Set<ChannelMessageActionName>(["send", "poll"]);
|
|
22
|
+
if (gate("reactions")) {
|
|
23
|
+
actions.add("react");
|
|
24
|
+
actions.add("reactions");
|
|
25
|
+
}
|
|
26
|
+
if (gate("messages")) {
|
|
27
|
+
actions.add("read");
|
|
28
|
+
actions.add("edit");
|
|
29
|
+
actions.add("delete");
|
|
30
|
+
}
|
|
31
|
+
if (gate("pins")) {
|
|
32
|
+
actions.add("pin");
|
|
33
|
+
actions.add("unpin");
|
|
34
|
+
actions.add("list-pins");
|
|
35
|
+
}
|
|
36
|
+
if (gate("memberInfo")) {
|
|
37
|
+
actions.add("member-info");
|
|
38
|
+
}
|
|
39
|
+
if (gate("channelInfo")) {
|
|
40
|
+
actions.add("channel-info");
|
|
41
|
+
}
|
|
42
|
+
return Array.from(actions);
|
|
43
|
+
},
|
|
44
|
+
supportsAction: ({ action }) => action !== "poll",
|
|
45
|
+
extractToolSend: ({ args }): ChannelToolSend | null => {
|
|
46
|
+
const action = typeof args.action === "string" ? args.action.trim() : "";
|
|
47
|
+
if (action !== "sendMessage") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const to = typeof args.to === "string" ? args.to : undefined;
|
|
51
|
+
if (!to) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return { to };
|
|
55
|
+
},
|
|
56
|
+
handleAction: async (ctx: ChannelMessageActionContext) => {
|
|
57
|
+
const { action, params, cfg } = ctx;
|
|
58
|
+
const resolveRoomId = () =>
|
|
59
|
+
readStringParam(params, "roomId") ??
|
|
60
|
+
readStringParam(params, "channelId") ??
|
|
61
|
+
readStringParam(params, "to", { required: true });
|
|
62
|
+
|
|
63
|
+
if (action === "send") {
|
|
64
|
+
const to = readStringParam(params, "to", { required: true });
|
|
65
|
+
const content = readStringParam(params, "message", {
|
|
66
|
+
required: true,
|
|
67
|
+
allowEmpty: true,
|
|
68
|
+
});
|
|
69
|
+
const mediaUrl = readStringParam(params, "media", { trim: false });
|
|
70
|
+
const replyTo = readStringParam(params, "replyTo");
|
|
71
|
+
const threadId = readStringParam(params, "threadId");
|
|
72
|
+
return await handleMatrixAction(
|
|
73
|
+
{
|
|
74
|
+
action: "sendMessage",
|
|
75
|
+
to,
|
|
76
|
+
content,
|
|
77
|
+
mediaUrl: mediaUrl ?? undefined,
|
|
78
|
+
replyToId: replyTo ?? undefined,
|
|
79
|
+
threadId: threadId ?? undefined,
|
|
80
|
+
},
|
|
81
|
+
cfg as CoreConfig,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (action === "react") {
|
|
86
|
+
const messageId = readStringParam(params, "messageId", { required: true });
|
|
87
|
+
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
|
|
88
|
+
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
|
|
89
|
+
return await handleMatrixAction(
|
|
90
|
+
{
|
|
91
|
+
action: "react",
|
|
92
|
+
roomId: resolveRoomId(),
|
|
93
|
+
messageId,
|
|
94
|
+
emoji,
|
|
95
|
+
remove,
|
|
96
|
+
},
|
|
97
|
+
cfg as CoreConfig,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (action === "reactions") {
|
|
102
|
+
const messageId = readStringParam(params, "messageId", { required: true });
|
|
103
|
+
const limit = readNumberParam(params, "limit", { integer: true });
|
|
104
|
+
return await handleMatrixAction(
|
|
105
|
+
{
|
|
106
|
+
action: "reactions",
|
|
107
|
+
roomId: resolveRoomId(),
|
|
108
|
+
messageId,
|
|
109
|
+
limit,
|
|
110
|
+
},
|
|
111
|
+
cfg as CoreConfig,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (action === "read") {
|
|
116
|
+
const limit = readNumberParam(params, "limit", { integer: true });
|
|
117
|
+
return await handleMatrixAction(
|
|
118
|
+
{
|
|
119
|
+
action: "readMessages",
|
|
120
|
+
roomId: resolveRoomId(),
|
|
121
|
+
limit,
|
|
122
|
+
before: readStringParam(params, "before"),
|
|
123
|
+
after: readStringParam(params, "after"),
|
|
124
|
+
},
|
|
125
|
+
cfg as CoreConfig,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (action === "edit") {
|
|
130
|
+
const messageId = readStringParam(params, "messageId", { required: true });
|
|
131
|
+
const content = readStringParam(params, "message", { required: true });
|
|
132
|
+
return await handleMatrixAction(
|
|
133
|
+
{
|
|
134
|
+
action: "editMessage",
|
|
135
|
+
roomId: resolveRoomId(),
|
|
136
|
+
messageId,
|
|
137
|
+
content,
|
|
138
|
+
},
|
|
139
|
+
cfg as CoreConfig,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (action === "delete") {
|
|
144
|
+
const messageId = readStringParam(params, "messageId", { required: true });
|
|
145
|
+
return await handleMatrixAction(
|
|
146
|
+
{
|
|
147
|
+
action: "deleteMessage",
|
|
148
|
+
roomId: resolveRoomId(),
|
|
149
|
+
messageId,
|
|
150
|
+
},
|
|
151
|
+
cfg as CoreConfig,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (action === "pin" || action === "unpin" || action === "list-pins") {
|
|
156
|
+
const messageId =
|
|
157
|
+
action === "list-pins"
|
|
158
|
+
? undefined
|
|
159
|
+
: readStringParam(params, "messageId", { required: true });
|
|
160
|
+
return await handleMatrixAction(
|
|
161
|
+
{
|
|
162
|
+
action:
|
|
163
|
+
action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins",
|
|
164
|
+
roomId: resolveRoomId(),
|
|
165
|
+
messageId,
|
|
166
|
+
},
|
|
167
|
+
cfg as CoreConfig,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (action === "member-info") {
|
|
172
|
+
const userId = readStringParam(params, "userId", { required: true });
|
|
173
|
+
return await handleMatrixAction(
|
|
174
|
+
{
|
|
175
|
+
action: "memberInfo",
|
|
176
|
+
userId,
|
|
177
|
+
roomId: readStringParam(params, "roomId") ?? readStringParam(params, "channelId"),
|
|
178
|
+
},
|
|
179
|
+
cfg as CoreConfig,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (action === "channel-info") {
|
|
184
|
+
return await handleMatrixAction(
|
|
185
|
+
{
|
|
186
|
+
action: "channelInfo",
|
|
187
|
+
roomId: resolveRoomId(),
|
|
188
|
+
},
|
|
189
|
+
cfg as CoreConfig,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new Error(`Action ${action} is not supported for provider matrix.`);
|
|
194
|
+
},
|
|
195
|
+
};
|
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
|
+
};
|