@badgerclaw/connect 1.1.2 → 1.2.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/package.json
CHANGED
package/src/group-mentions.ts
CHANGED
|
@@ -29,6 +29,17 @@ function resolveMatrixRoomConfigForGroup(params: ChannelGroupContext) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): boolean {
|
|
32
|
+
// Check per-room config first (set by /bot talk on|off)
|
|
33
|
+
try {
|
|
34
|
+
const { getRoomAutoReply } = require("./matrix/monitor/bot-commands.js");
|
|
35
|
+
const roomId = params.groupId?.trim() ?? "";
|
|
36
|
+
const roomAutoReply = getRoomAutoReply(roomId);
|
|
37
|
+
if (roomAutoReply === true) return false;
|
|
38
|
+
if (roomAutoReply === false) return true;
|
|
39
|
+
} catch {
|
|
40
|
+
// bot-commands module not available, fall through
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
const resolved = resolveMatrixRoomConfigForGroup(params);
|
|
33
44
|
if (resolved) {
|
|
34
45
|
if (resolved.autoReply === true) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
5
6
|
import { getMatrixLogService } from "../sdk-runtime.js";
|
|
6
7
|
|
|
7
8
|
export type KeyBackupStatus = {
|
|
@@ -10,19 +11,12 @@ export type KeyBackupStatus = {
|
|
|
10
11
|
deviceId: string | null;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
export async function getEncryptionKeyBackupStatus(
|
|
14
|
-
client: MatrixClient,
|
|
15
|
-
): Promise<KeyBackupStatus> {
|
|
14
|
+
export async function getEncryptionKeyBackupStatus(client: MatrixClient): Promise<KeyBackupStatus> {
|
|
16
15
|
const LogService = getMatrixLogService();
|
|
17
16
|
try {
|
|
18
17
|
const backupInfo = await client.getKeyBackupVersion();
|
|
19
18
|
const whoami = await client.getWhoAmI();
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
enabled: backupInfo !== null,
|
|
23
|
-
version: backupInfo?.version ?? null,
|
|
24
|
-
deviceId,
|
|
25
|
-
};
|
|
19
|
+
return { enabled: backupInfo !== null, version: backupInfo?.version ?? null, deviceId: whoami.device_id ?? null };
|
|
26
20
|
} catch (err) {
|
|
27
21
|
LogService.warn("MatrixKeyBackup", "Failed to check key backup status:", err);
|
|
28
22
|
return { enabled: false, version: null, deviceId: null };
|
|
@@ -33,17 +27,12 @@ export async function setupKeyBackup(client: MatrixClient): Promise<void> {
|
|
|
33
27
|
const LogService = getMatrixLogService();
|
|
34
28
|
|
|
35
29
|
let deviceId = "(unknown)";
|
|
36
|
-
try {
|
|
37
|
-
const whoami = await client.getWhoAmI();
|
|
38
|
-
deviceId = whoami.device_id ?? deviceId;
|
|
39
|
-
} catch {
|
|
40
|
-
// Non-fatal
|
|
41
|
-
}
|
|
30
|
+
try { deviceId = (await client.getWhoAmI()).device_id ?? deviceId; } catch { /* non-fatal */ }
|
|
42
31
|
|
|
43
32
|
LogService.info("MatrixKeyBackup", `Crypto ready — device ID: ${deviceId}`);
|
|
44
33
|
|
|
45
34
|
if (!client.crypto) {
|
|
46
|
-
LogService.info("MatrixKeyBackup", "Crypto
|
|
35
|
+
LogService.info("MatrixKeyBackup", "Crypto not available, skipping backup setup");
|
|
47
36
|
return;
|
|
48
37
|
}
|
|
49
38
|
|
|
@@ -51,45 +40,52 @@ export async function setupKeyBackup(client: MatrixClient): Promise<void> {
|
|
|
51
40
|
let backupInfo = await client.getKeyBackupVersion();
|
|
52
41
|
|
|
53
42
|
if (!backupInfo) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
43
|
+
LogService.info("MatrixKeyBackup", "No backup found — generating Curve25519 key pair and creating backup version");
|
|
44
|
+
|
|
45
|
+
// Generate X25519 (Curve25519) key pair using Node crypto
|
|
46
|
+
const { publicKey: pubKeyObj, privateKey: privKeyObj } = generateKeyPairSync("x25519");
|
|
47
|
+
const pubKeyRaw = pubKeyObj.export({ type: "spki", format: "der" }).slice(-32);
|
|
48
|
+
const privKeyRaw = privKeyObj.export({ type: "pkcs8", format: "der" }).slice(-32);
|
|
49
|
+
const publicKeyBase64 = pubKeyRaw.toString("base64");
|
|
50
|
+
const privateKeyBase64 = privKeyRaw.toString("base64");
|
|
51
|
+
|
|
52
|
+
// Create the backup version on the server
|
|
53
|
+
await client.signAndCreateKeyBackupVersion({
|
|
54
|
+
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
|
55
|
+
auth_data: { public_key: publicKeyBase64 },
|
|
56
|
+
} as any);
|
|
57
|
+
|
|
58
|
+
// Save private key for cross-device restore
|
|
59
|
+
const backupDir = join(homedir(), ".openclaw", "backup");
|
|
60
|
+
mkdirSync(backupDir, { recursive: true });
|
|
61
|
+
writeFileSync(
|
|
62
|
+
join(backupDir, `private-key-${deviceId}.json`),
|
|
63
|
+
JSON.stringify({ privateKey: privateKeyBase64, deviceId, created: new Date().toISOString() }),
|
|
64
|
+
{ mode: 0o600 }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
backupInfo = await client.getKeyBackupVersion();
|
|
68
|
+
LogService.info("MatrixKeyBackup", `Created backup version ${backupInfo?.version}`);
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
if (!backupInfo) {
|
|
69
|
-
LogService.warn("MatrixKeyBackup", "
|
|
72
|
+
LogService.warn("MatrixKeyBackup", "No backup info after creation attempt");
|
|
70
73
|
return;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
await client.crypto.enableKeyBackup(backupInfo);
|
|
74
|
-
LogService.info(
|
|
75
|
-
|
|
76
|
-
`Key backup enabled (version ${backupInfo.version}) on device ${deviceId} — keys will upload automatically`,
|
|
77
|
-
);
|
|
78
|
-
_persistBackupRecord(backupInfo.version, deviceId);
|
|
77
|
+
LogService.info("MatrixKeyBackup", `Backup enabled v${backupInfo.version} on ${deviceId} — uploading room keys`);
|
|
78
|
+
_persistRecord(backupInfo.version, deviceId);
|
|
79
79
|
} catch (err) {
|
|
80
|
-
LogService.warn("MatrixKeyBackup", "
|
|
80
|
+
LogService.warn("MatrixKeyBackup", "Backup setup failed:", err);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function
|
|
84
|
+
function _persistRecord(version: string, deviceId: string): void {
|
|
85
85
|
try {
|
|
86
86
|
const dir = join(homedir(), ".openclaw", "backup");
|
|
87
87
|
mkdirSync(dir, { recursive: true });
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
appendFileSync(path, entry, "utf8");
|
|
92
|
-
} catch {
|
|
93
|
-
// Non-fatal — backup is enabled, record persistence is best-effort
|
|
94
|
-
}
|
|
88
|
+
appendFileSync(join(dir, "key-backup-record.log"),
|
|
89
|
+
JSON.stringify({ version, deviceId, timestamp: new Date().toISOString() }) + "\n");
|
|
90
|
+
} catch { /* non-fatal */ }
|
|
95
91
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
|
|
5
|
+
const ROOM_CONFIG_PATH = path.join(
|
|
6
|
+
process.env.HOME || "/tmp",
|
|
7
|
+
".openclaw/extensions/badgerclaw/room-config.json"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
function loadRoomConfig(): Record<string, { autoReply?: boolean }> {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(ROOM_CONFIG_PATH, "utf-8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function saveRoomConfig(config: Record<string, { autoReply?: boolean }>): void {
|
|
19
|
+
fs.writeFileSync(ROOM_CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getRoomAutoReply(roomId: string): boolean | undefined {
|
|
23
|
+
const config = loadRoomConfig();
|
|
24
|
+
return config[roomId]?.autoReply;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function handleBotCommand(params: {
|
|
28
|
+
client: MatrixClient;
|
|
29
|
+
roomId: string;
|
|
30
|
+
senderId: string;
|
|
31
|
+
body: string;
|
|
32
|
+
selfUserId: string;
|
|
33
|
+
}): Promise<boolean> {
|
|
34
|
+
const { client, roomId, senderId, body, selfUserId } = params;
|
|
35
|
+
const trimmed = body.trim();
|
|
36
|
+
|
|
37
|
+
if (!trimmed.startsWith("/bot")) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const parts = trimmed.split(/\s+/);
|
|
42
|
+
const command = parts[1]?.toLowerCase();
|
|
43
|
+
const arg = parts[2]?.toLowerCase();
|
|
44
|
+
const arg2 = parts[3];
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
switch (command) {
|
|
48
|
+
case "help": {
|
|
49
|
+
await client.sendMessage(roomId, {
|
|
50
|
+
msgtype: "m.text",
|
|
51
|
+
body: [
|
|
52
|
+
"🦡 BadgerClaw Bot Commands",
|
|
53
|
+
"",
|
|
54
|
+
"━━━ Available Everywhere ━━━",
|
|
55
|
+
"",
|
|
56
|
+
"/bot help",
|
|
57
|
+
" Show this help message with all available commands.",
|
|
58
|
+
" Works in: Any room or DM",
|
|
59
|
+
"",
|
|
60
|
+
"/bot talk on",
|
|
61
|
+
" Enable auto-reply mode. The bot will respond to every",
|
|
62
|
+
" message in this room without needing an @mention.",
|
|
63
|
+
" Perfect for 1-on-1 rooms with your AI assistant.",
|
|
64
|
+
" Works in: Any room or DM",
|
|
65
|
+
"",
|
|
66
|
+
"/bot talk off",
|
|
67
|
+
" Disable auto-reply mode. The bot will only respond",
|
|
68
|
+
" when @mentioned. Use this in busy group rooms where",
|
|
69
|
+
" you don't want the bot responding to everything.",
|
|
70
|
+
" Works in: Any room or DM",
|
|
71
|
+
"",
|
|
72
|
+
"/bot add <botname>",
|
|
73
|
+
" Invite a bot to the current room. The bot username",
|
|
74
|
+
" will be @<botname>_bot on this server.",
|
|
75
|
+
" Example: /bot add jarvis → invites @jarvis_bot",
|
|
76
|
+
" Works in: Any room",
|
|
77
|
+
"",
|
|
78
|
+
"━━━ BotBadger DM Only ━━━",
|
|
79
|
+
"",
|
|
80
|
+
"/bot new",
|
|
81
|
+
" Create a new bot. Starts a guided flow to set up",
|
|
82
|
+
" a bot name, username, and generate a pairing code",
|
|
83
|
+
" for connecting to an OpenClaw instance.",
|
|
84
|
+
" Works in: Direct message with BotBadger only",
|
|
85
|
+
"",
|
|
86
|
+
"/bot pair <name>",
|
|
87
|
+
" Generate a new pairing code for an existing bot.",
|
|
88
|
+
" Use this to connect or reconnect a bot to OpenClaw.",
|
|
89
|
+
" Works in: Direct message with BotBadger only",
|
|
90
|
+
"",
|
|
91
|
+
"/bot delete <name>",
|
|
92
|
+
" Permanently delete a bot and remove it from all rooms.",
|
|
93
|
+
" This action cannot be undone.",
|
|
94
|
+
" Works in: Direct message with BotBadger only",
|
|
95
|
+
"",
|
|
96
|
+
"/bot list",
|
|
97
|
+
" List all your bots and their connection status.",
|
|
98
|
+
" 🟢 Connected — bot is online and responding",
|
|
99
|
+
" 🔴 Not connected — bot needs pairing",
|
|
100
|
+
" Works in: Any room or DM",
|
|
101
|
+
].join("\n"),
|
|
102
|
+
});
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case "talk": {
|
|
107
|
+
if (arg === "on") {
|
|
108
|
+
const config = loadRoomConfig();
|
|
109
|
+
config[roomId] = { ...config[roomId], autoReply: true };
|
|
110
|
+
saveRoomConfig(config);
|
|
111
|
+
await client.sendMessage(roomId, {
|
|
112
|
+
msgtype: "m.text",
|
|
113
|
+
body: "✅ Auto-reply enabled — I'll respond to every message in this room.",
|
|
114
|
+
});
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (arg === "off") {
|
|
118
|
+
const config = loadRoomConfig();
|
|
119
|
+
config[roomId] = { ...config[roomId], autoReply: false };
|
|
120
|
+
saveRoomConfig(config);
|
|
121
|
+
await client.sendMessage(roomId, {
|
|
122
|
+
msgtype: "m.text",
|
|
123
|
+
body: "✅ Auto-reply disabled — I'll only respond when @mentioned.",
|
|
124
|
+
});
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
await client.sendMessage(roomId, {
|
|
128
|
+
msgtype: "m.text",
|
|
129
|
+
body: "Usage: /bot talk on or /bot talk off",
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
case "add": {
|
|
135
|
+
const botName = arg;
|
|
136
|
+
if (!botName) {
|
|
137
|
+
await client.sendMessage(roomId, {
|
|
138
|
+
msgtype: "m.text",
|
|
139
|
+
body: "Usage: /bot add <botname>",
|
|
140
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const botUserId = `@${botName}_bot:badger.signout.io`;
|
|
144
|
+
try {
|
|
145
|
+
await client.inviteUser(botUserId, roomId);
|
|
146
|
+
await client.sendMessage(roomId, {
|
|
147
|
+
msgtype: "m.text",
|
|
148
|
+
body: `✅ Invited ${botUserId} to this room.`,
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
await client.sendMessage(roomId, {
|
|
153
|
+
msgtype: "m.text",
|
|
154
|
+
body: `❌ Failed to invite ${botUserId}: ${msg}`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
default: {
|
|
161
|
+
// Unknown /bot command — show help hint
|
|
162
|
+
await client.sendMessage(roomId, {
|
|
163
|
+
msgtype: "m.text",
|
|
164
|
+
body: "Unknown command. Type /bot help for available commands.",
|
|
165
|
+
});
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error("badgerclaw: bot command error:", err);
|
|
171
|
+
return true; // Still consumed the command
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -410,6 +410,19 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
410
410
|
return;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
+
// Handle /bot commands before anything else
|
|
414
|
+
if (bodyText.trim().startsWith("/bot")) {
|
|
415
|
+
const { handleBotCommand } = await import("./bot-commands.js");
|
|
416
|
+
const handled = await handleBotCommand({
|
|
417
|
+
client,
|
|
418
|
+
roomId,
|
|
419
|
+
senderId,
|
|
420
|
+
body: bodyText,
|
|
421
|
+
selfUserId,
|
|
422
|
+
});
|
|
423
|
+
if (handled) return;
|
|
424
|
+
}
|
|
425
|
+
|
|
413
426
|
const { wasMentioned, hasExplicitMention } = resolveMentions({
|
|
414
427
|
content,
|
|
415
428
|
userId: selfUserId,
|