@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@badgerclaw/connect",
3
- "version": "1.3.2",
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: resolveMatrixBodyForAgent({
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
@@ -114,5 +114,8 @@ export type CoreConfig = {
114
114
  ackReaction?: string;
115
115
  ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all" | "off" | "none";
116
116
  };
117
+ chatHistory?: {
118
+ enabled: boolean;
119
+ };
117
120
  [key: string]: unknown;
118
121
  };