@badgerclaw/connect 1.3.2 → 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.
- package/package.json +5 -2
- package/scripts/postinstall.js +34 -0
- package/src/matrix/monitor/auto-join.ts +7 -0
- package/src/matrix/monitor/bot-commands.ts +74 -0
- package/src/matrix/monitor/chat-history.ts +75 -0
- package/src/matrix/monitor/events.ts +82 -0
- package/src/matrix/monitor/handler.ts +35 -5
- package/src/types.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@badgerclaw/connect",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "BadgerClaw channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"localPath": "extensions/badgerclaw",
|
|
31
31
|
"defaultChoice": "npm"
|
|
32
32
|
}
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"postinstall": "node scripts/postinstall.js"
|
|
33
36
|
}
|
|
34
|
-
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall.js — patches openclaw package.json exports map to expose
|
|
4
|
+
* plugin-sdk/compat subpath, which was removed in openclaw >=2026.3.24.
|
|
5
|
+
* This runs automatically after `npm install` in the plugin directory.
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const OPENCLAW_PKG = path.join(
|
|
11
|
+
path.dirname(require.resolve('openclaw/package.json')),
|
|
12
|
+
'package.json'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const COMPAT_EXPORT = './plugin-sdk/compat';
|
|
16
|
+
const COMPAT_ENTRY = {
|
|
17
|
+
types: './dist/plugin-sdk/compat.d.ts',
|
|
18
|
+
default: './dist/plugin-sdk/compat.js',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(fs.readFileSync(OPENCLAW_PKG, 'utf-8'));
|
|
23
|
+
if (!pkg.exports) pkg.exports = {};
|
|
24
|
+
if (!pkg.exports[COMPAT_EXPORT]) {
|
|
25
|
+
pkg.exports[COMPAT_EXPORT] = COMPAT_ENTRY;
|
|
26
|
+
fs.writeFileSync(OPENCLAW_PKG, JSON.stringify(pkg, null, 2));
|
|
27
|
+
console.log('[badgerclaw] patched openclaw exports map: added plugin-sdk/compat');
|
|
28
|
+
} else {
|
|
29
|
+
console.log('[badgerclaw] openclaw exports map already has plugin-sdk/compat');
|
|
30
|
+
}
|
|
31
|
+
} catch (e) {
|
|
32
|
+
// Non-fatal — openclaw may not be installed yet
|
|
33
|
+
console.log('[badgerclaw] postinstall: could not patch openclaw exports map:', e.message);
|
|
34
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { RuntimeEnv } from "openclaw/plugin-sdk/matrix";
|
|
|
3
3
|
import { getMatrixRuntime } from "../../runtime.js";
|
|
4
4
|
import type { CoreConfig } from "../../types.js";
|
|
5
5
|
import { loadMatrixSdk } from "../sdk-runtime.js";
|
|
6
|
+
import { initRoomHistory } from "./chat-history.js";
|
|
6
7
|
|
|
7
8
|
// Track clients that already have auto-join registered to prevent duplicate listeners
|
|
8
9
|
const autoJoinRegistered = new WeakSet<object>();
|
|
@@ -73,6 +74,9 @@ export function registerMatrixAutoJoin(params: {
|
|
|
73
74
|
// AutojoinRoomsMixin handles the join, so listen for room.join to run post-join logic
|
|
74
75
|
client.on("room.join", async (roomId: string, _joinEvent: unknown) => {
|
|
75
76
|
logVerbose(`badgerclaw: bot joined room ${roomId} (always mode), running post-join handshake`);
|
|
77
|
+
if (cfg.chatHistory?.enabled) {
|
|
78
|
+
initRoomHistory(roomId);
|
|
79
|
+
}
|
|
76
80
|
await postJoinEncryptionHandshake(roomId);
|
|
77
81
|
});
|
|
78
82
|
return;
|
|
@@ -111,6 +115,9 @@ export function registerMatrixAutoJoin(params: {
|
|
|
111
115
|
try {
|
|
112
116
|
await client.joinRoom(roomId);
|
|
113
117
|
logVerbose(`badgerclaw: joined room ${roomId}`);
|
|
118
|
+
if (cfg.chatHistory?.enabled) {
|
|
119
|
+
initRoomHistory(roomId);
|
|
120
|
+
}
|
|
114
121
|
await postJoinEncryptionHandshake(roomId);
|
|
115
122
|
} catch (err) {
|
|
116
123
|
runtime.error?.(`badgerclaw: failed to join room ${roomId}: ${String(err)}`);
|
|
@@ -342,6 +342,80 @@ export async function handleBotCommand(params: {
|
|
|
342
342
|
return true;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
case "delete": {
|
|
346
|
+
const nameArg = parts.slice(2).join(" ").trim().replace(/^@/, "").split(":")[0].replace(/_bot$/i, "").toLowerCase();
|
|
347
|
+
if (!nameArg) {
|
|
348
|
+
await client.sendMessage(roomId, {
|
|
349
|
+
msgtype: "m.text",
|
|
350
|
+
body: "Usage: /bot delete <name>\nExample: /bot delete jarvis\n\n⚠️ This permanently deletes the bot from the database and Matrix.",
|
|
351
|
+
});
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const botUserId = `@${nameArg}_bot:badger.signout.io`;
|
|
356
|
+
|
|
357
|
+
// Load JWT token from ~/.badgerclaw/auth.json
|
|
358
|
+
let apiToken: string | null = null;
|
|
359
|
+
try {
|
|
360
|
+
const authPath = require("path").join(process.env.HOME || "/tmp", ".badgerclaw", "auth.json");
|
|
361
|
+
const authData = JSON.parse(require("fs").readFileSync(authPath, "utf-8"));
|
|
362
|
+
apiToken = authData.access_token || null;
|
|
363
|
+
} catch {
|
|
364
|
+
// no token
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!apiToken) {
|
|
368
|
+
await client.sendMessage(roomId, {
|
|
369
|
+
msgtype: "m.text",
|
|
370
|
+
body: "❌ Not authenticated. Run `badgerclaw login` first.",
|
|
371
|
+
});
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Find the bot by user_id via API
|
|
376
|
+
try {
|
|
377
|
+
const listResp = await fetch("https://api.badgerclaw.ai/api/v1/bots", {
|
|
378
|
+
headers: { Authorization: `Bearer ${apiToken}`, "Content-Type": "application/json" },
|
|
379
|
+
});
|
|
380
|
+
if (!listResp.ok) throw new Error(`Failed to list bots: ${listResp.status}`);
|
|
381
|
+
const bots: Array<{ id: string; bot_user_id: string; bot_name: string }> = await listResp.json();
|
|
382
|
+
const bot = bots.find((b) => b.bot_user_id.toLowerCase() === botUserId.toLowerCase());
|
|
383
|
+
|
|
384
|
+
if (!bot) {
|
|
385
|
+
await client.sendMessage(roomId, {
|
|
386
|
+
msgtype: "m.text",
|
|
387
|
+
body: `❌ Bot @${nameArg}_bot not found. Check the name and try again.`,
|
|
388
|
+
});
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Delete permanently
|
|
393
|
+
const delResp = await fetch(`https://api.badgerclaw.ai/api/v1/bots/${bot.id}`, {
|
|
394
|
+
method: "DELETE",
|
|
395
|
+
headers: { Authorization: `Bearer ${apiToken}`, "Content-Type": "application/json" },
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (delResp.ok) {
|
|
399
|
+
await client.sendMessage(roomId, {
|
|
400
|
+
msgtype: "m.text",
|
|
401
|
+
body: `🗑️ Bot **${bot.bot_name}** (@${nameArg}_bot) permanently deleted.`,
|
|
402
|
+
});
|
|
403
|
+
} else {
|
|
404
|
+
const err = await delResp.json().catch(() => ({ detail: delResp.statusText }));
|
|
405
|
+
await client.sendMessage(roomId, {
|
|
406
|
+
msgtype: "m.text",
|
|
407
|
+
body: `❌ Failed to delete bot: ${err.detail || delResp.status}`,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
} catch (e) {
|
|
411
|
+
await client.sendMessage(roomId, {
|
|
412
|
+
msgtype: "m.text",
|
|
413
|
+
body: `❌ Error deleting bot: ${e}`,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
345
419
|
default: {
|
|
346
420
|
await client.sendMessage(roomId, {
|
|
347
421
|
msgtype: "m.text",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
|
|
5
|
+
const HISTORY_DIR = path.join(os.homedir(), ".openclaw", "extensions", "badgerclaw", "history");
|
|
6
|
+
const MAX_MESSAGES = 200;
|
|
7
|
+
|
|
8
|
+
export interface HistoryEntry {
|
|
9
|
+
ts: string; // ISO timestamp
|
|
10
|
+
sender: string; // display name or username
|
|
11
|
+
text: string; // message text
|
|
12
|
+
role: "user" | "bot";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getHistoryPath(roomId: string): string {
|
|
16
|
+
// Sanitize roomId for filename: replace ! and : with safe chars
|
|
17
|
+
const safe = roomId.replace(/[!:]/g, "_").replace(/\./g, "-");
|
|
18
|
+
return path.join(HISTORY_DIR, `${safe}.md`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ensureHistoryDir(): void {
|
|
22
|
+
if (!fs.existsSync(HISTORY_DIR)) {
|
|
23
|
+
fs.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readHistory(roomId: string): HistoryEntry[] {
|
|
28
|
+
const filePath = getHistoryPath(roomId);
|
|
29
|
+
if (!fs.existsSync(filePath)) return [];
|
|
30
|
+
try {
|
|
31
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
32
|
+
const lines = content.split("\n").filter((l: string) => l.startsWith("["));
|
|
33
|
+
return lines.map((line: string) => {
|
|
34
|
+
// Format: [2026-03-27T08:05:00Z] [user] sender: text
|
|
35
|
+
const match = line.match(/^\[([^\]]+)\] \[(user|bot)\] ([^:]+): (.+)$/);
|
|
36
|
+
if (!match) return null;
|
|
37
|
+
return { ts: match[1], role: match[2] as "user" | "bot", sender: match[3], text: match[4] };
|
|
38
|
+
}).filter(Boolean) as HistoryEntry[];
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function appendHistory(roomId: string, entry: HistoryEntry, roomName?: string): void {
|
|
45
|
+
ensureHistoryDir();
|
|
46
|
+
const filePath = getHistoryPath(roomId);
|
|
47
|
+
|
|
48
|
+
// Read existing, append, trim to 200
|
|
49
|
+
let entries = readHistory(roomId);
|
|
50
|
+
entries.push(entry);
|
|
51
|
+
if (entries.length > MAX_MESSAGES) {
|
|
52
|
+
entries = entries.slice(entries.length - MAX_MESSAGES);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Write header + entries
|
|
56
|
+
const header = `# Chat History — ${roomId}\n# Group: ${roomName || roomId} | Updated: ${new Date().toISOString()}\n# Last ${MAX_MESSAGES} messages (rolling)\n\n`;
|
|
57
|
+
const body = entries.map(e => `[${e.ts}] [${e.role}] ${e.sender}: ${e.text}`).join("\n");
|
|
58
|
+
fs.writeFileSync(filePath, header + body + "\n", "utf8");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatHistoryForContext(roomId: string): string | null {
|
|
62
|
+
const entries = readHistory(roomId);
|
|
63
|
+
if (entries.length === 0) return null;
|
|
64
|
+
const lines = entries.map(e => `${e.sender}: ${e.text}`).join("\n");
|
|
65
|
+
return `## Recent conversation history (last ${entries.length} messages):\n${lines}\n\n---\n`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function initRoomHistory(roomId: string, roomName?: string): void {
|
|
69
|
+
ensureHistoryDir();
|
|
70
|
+
const filePath = getHistoryPath(roomId);
|
|
71
|
+
if (!fs.existsSync(filePath)) {
|
|
72
|
+
const header = `# Chat History — ${roomId}\n# Group: ${roomName || roomId} | Created: ${new Date().toISOString()}\n# Last ${MAX_MESSAGES} messages (rolling)\n\n`;
|
|
73
|
+
fs.writeFileSync(filePath, header, "utf8");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -164,5 +164,87 @@ export function registerMatrixMonitorEvents(params: {
|
|
|
164
164
|
`badgerclaw: member event room=${roomId} stateKey=${stateKey} membership=${membership ?? "unknown"}`,
|
|
165
165
|
);
|
|
166
166
|
}
|
|
167
|
+
|
|
168
|
+
// Auto-pair: iOS app sends this event after generating a pair code
|
|
169
|
+
// The plugin redeems the code and adds the bot account to OpenClaw config
|
|
170
|
+
if (eventType === "com.badgerclaw.autopair") {
|
|
171
|
+
const content = event?.content as {
|
|
172
|
+
pair_code?: string;
|
|
173
|
+
bot_name?: string;
|
|
174
|
+
bot_user_id?: string;
|
|
175
|
+
owner_matrix_id?: string;
|
|
176
|
+
} | undefined;
|
|
177
|
+
|
|
178
|
+
const pairCode = content?.pair_code;
|
|
179
|
+
const botName = content?.bot_name;
|
|
180
|
+
const botUserId = content?.bot_user_id;
|
|
181
|
+
|
|
182
|
+
if (!pairCode || !botUserId) {
|
|
183
|
+
logVerboseMessage(`badgerclaw: autopair event missing fields room=${roomId}`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
logger.info(`badgerclaw: autopair event received — redeeming code for ${botUserId}`, { roomId });
|
|
188
|
+
|
|
189
|
+
void (async () => {
|
|
190
|
+
try {
|
|
191
|
+
const resp = await fetch("https://api.badgerclaw.ai/api/v1/pairing/redeem", {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "Content-Type": "application/json" },
|
|
194
|
+
body: JSON.stringify({ code: pairCode }),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if (!resp.ok) {
|
|
198
|
+
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
|
199
|
+
logger.warn(`badgerclaw: autopair redeem failed for ${botUserId}: ${err.detail || resp.status}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const data = await resp.json() as {
|
|
204
|
+
homeserver: string;
|
|
205
|
+
access_token: string;
|
|
206
|
+
user_id: string;
|
|
207
|
+
bot_name: string;
|
|
208
|
+
device_id: string;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Write new bot account to OpenClaw config
|
|
212
|
+
const fs = await import("fs");
|
|
213
|
+
const configPath = require("path").join(process.env.HOME || "/tmp", ".openclaw", "openclaw.json");
|
|
214
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
215
|
+
|
|
216
|
+
if (!config.channels) config.channels = {};
|
|
217
|
+
if (!config.channels.badgerclaw) config.channels.badgerclaw = {};
|
|
218
|
+
if (!config.channels.badgerclaw.accounts) config.channels.badgerclaw.accounts = {};
|
|
219
|
+
|
|
220
|
+
// Derive account key from bot localpart (e.g. @think_bot:... → "think")
|
|
221
|
+
const localpart = data.user_id.split(":")[0].replace("@", "").replace(/_bot$/, "");
|
|
222
|
+
|
|
223
|
+
config.channels.badgerclaw.accounts[localpart] = {
|
|
224
|
+
userId: data.user_id,
|
|
225
|
+
accessToken: data.access_token,
|
|
226
|
+
homeserver: data.homeserver,
|
|
227
|
+
encryption: true,
|
|
228
|
+
deviceId: data.device_id,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
232
|
+
|
|
233
|
+
logger.info(`badgerclaw: ✅ autopair complete — added account "${localpart}" (${data.user_id}) to OpenClaw config. Restart gateway to activate.`);
|
|
234
|
+
|
|
235
|
+
// Notify in the room
|
|
236
|
+
try {
|
|
237
|
+
await client.sendMessage(roomId, {
|
|
238
|
+
msgtype: "m.text",
|
|
239
|
+
body: `✅ Bot **${data.bot_name}** (${data.user_id}) has been automatically paired and added to your OpenClaw instance.\n\nRestart your OpenClaw gateway to activate: \`openclaw gateway restart\``,
|
|
240
|
+
});
|
|
241
|
+
} catch {
|
|
242
|
+
// non-fatal
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
logger.error(`badgerclaw: autopair failed for ${botUserId}: ${String(e)}`);
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
}
|
|
167
249
|
});
|
|
168
250
|
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
resolveMatrixAllowListMatch,
|
|
31
31
|
resolveMatrixAllowListMatches,
|
|
32
32
|
} from "./allowlist.js";
|
|
33
|
+
import { appendHistory, formatHistoryForContext } from "./chat-history.js";
|
|
33
34
|
import {
|
|
34
35
|
resolveMatrixBodyForAgent,
|
|
35
36
|
resolveMatrixInboundSenderLabel,
|
|
@@ -630,13 +631,33 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
630
631
|
});
|
|
631
632
|
|
|
632
633
|
const groupSystemPrompt = roomConfig?.systemPrompt?.trim() || undefined;
|
|
634
|
+
|
|
635
|
+
// Append inbound user message to chat history
|
|
636
|
+
if (cfg.chatHistory?.enabled) {
|
|
637
|
+
appendHistory(roomId, {
|
|
638
|
+
ts: new Date().toISOString(),
|
|
639
|
+
role: "user",
|
|
640
|
+
sender: senderName || senderId,
|
|
641
|
+
text: bodyText,
|
|
642
|
+
}, roomName);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Inject rolling history into BodyForAgent context
|
|
646
|
+
let bodyForAgent = resolveMatrixBodyForAgent({
|
|
647
|
+
isDirectMessage,
|
|
648
|
+
bodyText,
|
|
649
|
+
senderLabel,
|
|
650
|
+
});
|
|
651
|
+
if (cfg.chatHistory?.enabled) {
|
|
652
|
+
const history = formatHistoryForContext(roomId);
|
|
653
|
+
if (history) {
|
|
654
|
+
bodyForAgent = history + bodyForAgent;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
633
658
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
634
659
|
Body: body,
|
|
635
|
-
BodyForAgent:
|
|
636
|
-
isDirectMessage,
|
|
637
|
-
bodyText,
|
|
638
|
-
senderLabel,
|
|
639
|
-
}),
|
|
660
|
+
BodyForAgent: bodyForAgent,
|
|
640
661
|
RawBody: bodyText,
|
|
641
662
|
CommandBody: bodyText,
|
|
642
663
|
From: isDirectMessage ? `badgerclaw:${senderId}` : `badgerclaw:channel:${roomId}`,
|
|
@@ -775,6 +796,15 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
|
|
|
775
796
|
accountId: route.accountId,
|
|
776
797
|
tableMode,
|
|
777
798
|
});
|
|
799
|
+
if (cfg.chatHistory?.enabled && payload?.text) {
|
|
800
|
+
const botSender = resolveMatrixSenderUsername(selfUserId) || selfUserId;
|
|
801
|
+
appendHistory(roomId, {
|
|
802
|
+
ts: new Date().toISOString(),
|
|
803
|
+
role: "bot",
|
|
804
|
+
sender: botSender,
|
|
805
|
+
text: payload.text,
|
|
806
|
+
}, roomName);
|
|
807
|
+
}
|
|
778
808
|
didSendReply = true;
|
|
779
809
|
},
|
|
780
810
|
onError: (err, info) => {
|
package/src/types.ts
CHANGED