@badgerclaw/connect 1.3.2 → 1.3.3

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.3.3",
4
4
  "description": "BadgerClaw channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -31,4 +31,4 @@
31
31
  "defaultChoice": "npm"
32
32
  }
33
33
  }
34
- }
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)}`);
@@ -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
+ }
@@ -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
  };