@badgerclaw/connect 1.3.1 → 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/SETUP.md CHANGED
@@ -6,7 +6,6 @@ Connect your OpenClaw AI agent to BadgerClaw encrypted chat rooms.
6
6
 
7
7
  - **BadgerClaw iOS app** installed and logged in
8
8
  - **OpenClaw** installed on your machine (`npm install -g openclaw`)
9
- - Both on the same network (not required, just for initial setup)
10
9
 
11
10
  ---
12
11
 
@@ -14,28 +13,24 @@ Connect your OpenClaw AI agent to BadgerClaw encrypted chat rooms.
14
13
 
15
14
  Open the BadgerClaw app on your phone.
16
15
 
17
- 1. Open any room or DM
18
- 2. Type: `/bot create <name>`
19
- - Example: `/bot create jarvis`
20
- 3. BotBadger responds with:
16
+ 1. Open any room or DM with BotBadger
17
+ 2. Type: `/bot new`
18
+ 3. Follow the guided flow to set a bot name and username
19
+ 4. BotBadger responds with a pairing code:
21
20
 
22
21
  ```
23
22
  🦡 Bot created!
24
23
 
25
24
  Name: jarvis
26
- ID: @abc123_jarvis_bot:badger.signout.io
27
-
28
25
  Connect to OpenClaw:
29
- openclaw plugins install @badgerclaw/connect
30
26
  openclaw badgerclaw connect BCK-A8F3-X9K2
31
27
 
32
28
  Code expires in 24 hours.
33
- Run /bot pair jarvis to generate a new code.
34
29
  ```
35
30
 
36
- 4. **Copy the pairing code** (BCK-XXXX-XXXX)
31
+ 5. **Copy the pairing code** (BCK-XXXX-XXXX)
37
32
 
38
- > **Need a new code?** Type `/bot pair jarvis` to regenerate.
33
+ > **Need a new code?** Type `/bot pair <name>` to regenerate.
39
34
 
40
35
  ---
41
36
 
@@ -44,38 +39,42 @@ Run /bot pair jarvis to generate a new code.
44
39
  On your machine (Mac, PC, Linux, Pi), run:
45
40
 
46
41
  ```bash
47
- # From GitHub (current method)
42
+ openclaw plugins install @badgerclaw/connect
43
+ ```
44
+
45
+ Or install from source:
46
+
47
+ ```bash
48
48
  git clone https://github.com/darkstaar4/badgerclaw-plugin.git
49
49
  openclaw plugins install ./badgerclaw-plugin
50
-
51
- # From npm (coming soon)
52
- # openclaw plugins install @badgerclaw/connect
53
50
  ```
54
51
 
55
52
  ---
56
53
 
57
54
  ## Step 3: Connect with Pairing Code
58
55
 
59
- Run the OpenClaw configure wizard:
56
+ ```bash
57
+ openclaw badgerclaw connect BCK-XXXX-XXXX
58
+ ```
59
+
60
+ Or run the full configure wizard:
60
61
 
61
62
  ```bash
62
63
  openclaw configure
63
64
  ```
64
65
 
65
- 1. Select **BadgerClaw** as your channel
66
- 2. Enter your pairing code: `BCK-XXXX-XXXX`
67
- 3. The plugin automatically:
68
- - Validates the code with BadgerClaw servers
69
- - Retrieves your bot's credentials
70
- - Enables end-to-end encryption
71
- - Configures auto-join for room invites
72
- 4. Done! Your agent is connected.
66
+ The plugin automatically:
67
+ - Validates the code with BadgerClaw servers
68
+ - Retrieves your bot's credentials
69
+ - Enables end-to-end encryption
70
+ - Configures auto-join for room invites
71
+ - Sets up auto-reply in group chats
73
72
 
74
73
  ---
75
74
 
76
75
  ## Step 4: Start Chatting
77
76
 
78
- Restart the OpenClaw gateway to apply the new config:
77
+ Restart the OpenClaw gateway:
79
78
 
80
79
  ```bash
81
80
  openclaw gateway restart
@@ -83,30 +82,191 @@ openclaw gateway restart
83
82
 
84
83
  Now in the BadgerClaw app:
85
84
 
86
- 1. **DM your bot** — find it in your contacts (`@xxx_jarvis_bot:badger.signout.io`) and send a message
87
- 2. **Add to a room** — type `/bot add @jarvis` in any room
85
+ 1. **DM your bot** — find it in your contacts and send a message
86
+ 2. **Add to a room** — type `/bot add <name>` in any room
88
87
  3. **Add to a Space** — add the bot to any room inside a Space
89
88
 
90
- Your OpenClaw agent will receive messages and respond through BadgerClaw — fully end-to-end encrypted.
89
+ Your OpenClaw agent will receive messages and respond — fully end-to-end encrypted.
91
90
 
92
91
  ---
93
92
 
94
- ## Troubleshooting
93
+ ## Bot Commands
94
+
95
+ Use these commands in any BadgerClaw room or DM where your bot is present.
96
+
97
+ | Command | Description |
98
+ |---------|-------------|
99
+ | `/bot help` | Show all available commands |
100
+ | `/bot talk on` | **Enable auto-reply** — bot responds to every message without needing an @mention. If multiple bots are in the room, you'll be asked to specify which one. |
101
+ | `/bot talk on @botname` | **Enable auto-reply for a specific bot** — use when multiple bots are in the room. |
102
+ | `/bot talk off` | **Disable auto-reply for ALL bots** — kill switch. All bots in the room go quiet and only respond when @mentioned. |
103
+ | `/bot add <name>` | Invite a bot to the current room (e.g. `/bot add jarvis`) |
104
+ | `/bot list` | List all your bots and their connection status |
105
+
106
+ ### How Auto-Reply Works
107
+
108
+ - **New installs:** Auto-reply is **ON by default** in all rooms
109
+ - **Per-room toggle:** `/bot talk on` and `/bot talk off` override the default for that specific room
110
+ - **Multi-bot rooms:** If multiple bots are present, `/bot talk on` asks which bot to enable. `/bot talk off` silences all bots at once.
111
+ - **@mentions always work:** Even with auto-reply off, the bot responds when @mentioned
112
+
113
+ ---
114
+
115
+ ## Group Chat Configuration
116
+
117
+ ### Default Behavior
118
+
119
+ When you install BadgerClaw with a fresh pairing code, the plugin automatically configures:
120
+
121
+ ```json
122
+ {
123
+ "groupPolicy": "open",
124
+ "groups": {
125
+ "*": {
126
+ "autoReply": true,
127
+ "enabled": true
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ This means:
134
+ - Bot joins any room it's invited to
135
+ - Bot responds to all messages (no @mention required)
136
+ - Bot uses **your OpenClaw workspace personality** (SOUL.md, IDENTITY.md, etc.) in all conversations — groups and DMs alike
137
+
138
+ When the bot joins a new encrypted room, it sends a hint message explaining the `/bot talk on|off` commands so room members know how to control it.
139
+
140
+ ### Your Personality, Everywhere
141
+
142
+ BadgerClaw respects your OpenClaw workspace files. If you've configured a custom personality (SOUL.md), the bot uses that personality in groups and DMs. This means:
143
+
144
+ - A "pirate assistant" SOUL.md → bot talks like a pirate in groups
145
+ - A "professional COO" SOUL.md → bot acts professional in groups
146
+ - No SOUL.md → bot uses the default helpful assistant behavior
147
+
148
+ **If your bot stays silent in groups**, your workspace files may include instructions like "stay silent in groups" or "only respond when needed." To fix this:
149
+
150
+ 1. Edit your SOUL.md to allow group responses, OR
151
+ 2. Add a group-specific system prompt override:
152
+
153
+ ```json
154
+ {
155
+ "channels": {
156
+ "badgerclaw": {
157
+ "groups": {
158
+ "*": {
159
+ "autoReply": true,
160
+ "systemPrompt": "You are a helpful AI assistant. Always respond to messages in group chats."
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+ ```
167
+
168
+ ### Per-Room Configuration
169
+
170
+ You can configure specific rooms differently:
171
+
172
+ ```json
173
+ {
174
+ "channels": {
175
+ "badgerclaw": {
176
+ "groups": {
177
+ "*": { "autoReply": true },
178
+ "!specificRoomId:server": {
179
+ "autoReply": false,
180
+ "systemPrompt": "Custom prompt for this room only"
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Upgrading from v1.1.x or Earlier
191
+
192
+ If you installed BadgerClaw before v1.2.0, your config is missing the group auto-reply settings. The bot may not respond in group chats without @mentions.
193
+
194
+ **Quick fix — run the update command:**
195
+
196
+ ```bash
197
+ openclaw plugins update @badgerclaw/connect
198
+ openclaw configure
199
+ ```
200
+
201
+ Select BadgerClaw and re-enter a pairing code. The new onboarding will add the missing group config automatically.
202
+
203
+ **Manual fix — if you don't want to re-pair:**
204
+
205
+ Add this to your `~/.openclaw/openclaw.json` inside `channels.badgerclaw`:
206
+
207
+ ```json
208
+ {
209
+ "channels": {
210
+ "badgerclaw": {
211
+ "groupPolicy": "open",
212
+ "groups": {
213
+ "*": {
214
+ "autoReply": true,
215
+ "enabled": true
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ ```
222
+
223
+ Then restart: `openclaw gateway restart`
224
+
225
+ ---
226
+
227
+ ## FAQ
228
+
229
+ ### Bot doesn't respond in group chats
230
+
231
+ 1. Send `/bot talk on` in the room to enable auto-reply
232
+ 2. Make sure the bot is a member of the room (`/bot add <name>`)
233
+ 3. Check OpenClaw is running: `openclaw status`
234
+
235
+ ### Bot shows typing indicator but never replies
236
+
237
+ Your workspace personality (SOUL.md) may tell the agent to stay silent in groups. BadgerClaw respects your workspace files, so if SOUL.md says "don't respond in groups," the bot won't.
238
+
239
+ **Fix options:**
240
+ 1. Edit your SOUL.md to allow group responses
241
+ 2. Add a `systemPrompt` override in `channels.badgerclaw.groups.*` that tells the agent to respond (see Group Chat Configuration above)
242
+ 3. Run `openclaw configure` and reconfigure BadgerClaw
243
+
244
+ ### `/bot talk on` doesn't work
245
+
246
+ After changing bot commands, you may need to restart the gateway:
247
+
248
+ ```bash
249
+ openclaw gateway restart
250
+ ```
251
+
252
+ ### Bot responds with wrong personality
253
+
254
+ The bot uses your **OpenClaw workspace personality** (SOUL.md, IDENTITY.md) for both DMs and groups. To set a different personality for groups only, add a `systemPrompt` in `channels.badgerclaw.groups.*` in your config.
255
+
256
+ ### Bot keeps saying "NO_REPLY" or stays silent
257
+
258
+ Your workspace files (SOUL.md) instruct the agent to stay silent in groups. BadgerClaw respects your personality — this is intentional.
259
+
260
+ **To fix:** Add a `systemPrompt` override to your groups config that tells the agent to always respond. See the "Your Personality, Everywhere" section above. Then restart: `openclaw gateway restart`
95
261
 
96
262
  ### "Invalid pairing code"
97
- - Code may have expired (24h limit). Run `/bot pair <name>` in the app to get a new one.
98
- - Make sure you're entering the code exactly as shown (BCK-XXXX-XXXX).
99
263
 
100
- ### Bot doesn't respond
101
- 1. Check OpenClaw is running: `openclaw status`
102
- 2. Check gateway logs: `openclaw logs --follow`
103
- 3. Verify the BadgerClaw channel is configured: `openclaw channels status`
264
+ - Code expires after 24 hours. Run `/bot pair <name>` in the app for a new one.
265
+ - Enter the code exactly as shown (BCK-XXXX-XXXX).
104
266
 
105
267
  ### "Pairing code already redeemed"
106
- Each code is one-time use. Run `/bot pair <name>` to generate a fresh one.
107
268
 
108
- ### Need to reconnect
109
- Run `openclaw configure` again and enter a new pairing code.
269
+ Each code is one-time use. Run `/bot pair <name>` to generate a fresh one.
110
270
 
111
271
  ---
112
272
 
@@ -115,9 +275,9 @@ Run `openclaw configure` again and enter a new pairing code.
115
275
  ```
116
276
  Your Phone (BadgerClaw App)
117
277
 
118
- │ E2EE Messages
278
+ │ E2EE Messages (Megolm/Olm)
119
279
 
120
- BadgerClaw Server (badger.signout.io)
280
+ BadgerClaw Server (Matrix homeserver)
121
281
 
122
282
  │ E2EE Messages
123
283
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@badgerclaw/connect",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "BadgerClaw channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -38,26 +38,44 @@ export function resolveMatrixGroupRequireMention(params: ChannelGroupContext): b
38
38
  ".openclaw/extensions/badgerclaw/room-config.json"
39
39
  );
40
40
  const raw = fs.readFileSync(configPath, "utf-8");
41
- const roomConfig = JSON.parse(raw) as Record<string, { autoReply?: boolean }>;
41
+ const roomConfig = JSON.parse(raw) as Record<string, { autoReply?: boolean; activeBots?: string[] }>;
42
42
  const rawGroupId = params.groupId?.trim() ?? "";
43
- // Try multiple formats: raw, stripped prefixes, just the room ID part
44
43
  const variants = [
45
44
  rawGroupId,
46
45
  rawGroupId.replace(/^badgerclaw:/, ""),
47
46
  rawGroupId.replace(/^channel:/, ""),
48
47
  rawGroupId.replace(/^room:/, ""),
49
48
  ];
49
+ const configKeys = Object.keys(roomConfig);
50
50
  for (const variant of variants) {
51
- if (variant && roomConfig[variant]?.autoReply === true) {
52
- console.log(`[badgerclaw] room-config autoReply=true for ${variant}`);
53
- return false;
54
- }
55
- if (variant && roomConfig[variant]?.autoReply === false) {
56
- console.log(`[badgerclaw] room-config autoReply=false for ${variant}`);
57
- return true;
51
+ if (!variant) continue;
52
+ const matchedKey = configKeys.find(
53
+ (k) => k.toLowerCase() === variant.toLowerCase()
54
+ );
55
+ if (matchedKey) {
56
+ const entry = roomConfig[matchedKey];
57
+ // New format: activeBots list
58
+ if (Array.isArray(entry.activeBots)) {
59
+ if (entry.activeBots.length > 0) {
60
+ console.log(`[badgerclaw] room-config activeBots=[${entry.activeBots.map(b => b.split(":")[0]).join(",")}] for ${matchedKey}`);
61
+ return false; // At least one bot active — let handler do per-bot filtering
62
+ } else {
63
+ console.log(`[badgerclaw] room-config activeBots=[] (all off) for ${matchedKey}`);
64
+ return true; // No bots active
65
+ }
66
+ }
67
+ // Legacy format
68
+ if (entry.autoReply === true) {
69
+ console.log(`[badgerclaw] room-config autoReply=true for ${matchedKey} (lookup=${variant})`);
70
+ return false;
71
+ }
72
+ if (entry.autoReply === false) {
73
+ console.log(`[badgerclaw] room-config autoReply=false for ${matchedKey} (lookup=${variant})`);
74
+ return true;
75
+ }
58
76
  }
59
77
  }
60
- console.log(`[badgerclaw] room-config: no match for groupId="${rawGroupId}", keys=${Object.keys(roomConfig).join(",")}`);
78
+ console.log(`[badgerclaw] room-config: no match for groupId="${rawGroupId}", keys=${configKeys.join(",")}`);
61
79
  } catch (err) {
62
80
  console.log(`[badgerclaw] room-config error: ${err}`);
63
81
  }
@@ -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>();
@@ -34,47 +35,49 @@ export function registerMatrixAutoJoin(params: {
34
35
  }
35
36
  autoJoinRegistered.add(client);
36
37
 
38
+ // After joining a room, send a notice in encrypted rooms to force Megolm session rotation.
39
+ // When the bot joins, other clients may use a cached outbound Megolm session that doesn't
40
+ // include the bot. Sending a message causes compliant clients to detect the new member and
41
+ // rotate their session so subsequent messages are decryptable by the bot.
42
+ async function postJoinEncryptionHandshake(roomId: string) {
43
+ try {
44
+ const encryptionState = await client
45
+ .getRoomStateEvent(roomId, "m.room.encryption", "")
46
+ .catch(() => null);
47
+
48
+ if (!encryptionState) {
49
+ logVerbose(`badgerclaw: room ${roomId} is not encrypted, skipping handshake`);
50
+ return;
51
+ }
52
+
53
+ logVerbose(`badgerclaw: room ${roomId} is encrypted, sending notice to trigger key rotation`);
54
+ await client.sendNotice(
55
+ roomId,
56
+ "🔐 Connected — encryption active.\n\n" +
57
+ "I'll respond when @mentioned. To toggle auto-reply:\n" +
58
+ "• /bot talk on — I'll respond to every message\n" +
59
+ "• /bot talk off — I'll only respond when @mentioned\n" +
60
+ "• /bot help — see all commands",
61
+ );
62
+ logVerbose(`badgerclaw: sent encryption handshake + hint notice in room ${roomId}`);
63
+ } catch (err) {
64
+ runtime.log?.(`badgerclaw: encryption handshake failed for room ${roomId}: ${String(err)}`);
65
+ }
66
+ }
67
+
37
68
  if (autoJoin === "always") {
38
69
  // Use the built-in autojoin mixin for "always" mode
39
70
  const { AutojoinRoomsMixin } = loadMatrixSdk();
40
71
  AutojoinRoomsMixin.setupOnClient(client);
41
72
  logVerbose("badgerclaw: auto-join enabled for all invites");
42
73
 
43
- // Also join any rooms with pending invites from before startup
44
- (async () => {
45
- try {
46
- const syncData = await client.doRequest("GET", "/_matrix/client/v3/sync", { timeout: "0" });
47
- const invitedRooms = Object.keys(syncData?.rooms?.invite ?? {});
48
- for (const roomId of invitedRooms) {
49
- try {
50
- await client.joinRoom(roomId);
51
- logVerbose(`badgerclaw: joined pending invite room ${roomId}`);
52
- } catch (err) {
53
- logVerbose(`badgerclaw: failed to join pending invite ${roomId}: ${err}`);
54
- }
55
- }
56
- } catch {
57
- // Ignore sync errors
58
- }
59
- })();
60
-
61
- // When bot joins a new room, send a presence message to force Megolm session rotation
62
- // This ensures all clients create a new session that includes the bot's device
63
- client.on("room.join", async (roomId: string) => {
64
- logVerbose(`badgerclaw: bot joined room ${roomId}, requesting key share`);
65
- try {
66
- // Send a dummy state event to signal our device presence
67
- // This forces clients to include our device in the next Megolm session
68
- await client.sendStateEvent(roomId, "m.room.member", await client.getUserId(), {
69
- membership: "join",
70
- displayname: "testbot",
71
- }).catch(() => {});
72
-
73
- // Also send a notice message to trigger session rotation
74
- await client.sendNotice(roomId, "🦡 Bot connected to this room.").catch(() => {});
75
- } catch (err) {
76
- logVerbose(`badgerclaw: failed to send join notice in ${roomId}: ${err}`);
74
+ // AutojoinRoomsMixin handles the join, so listen for room.join to run post-join logic
75
+ client.on("room.join", async (roomId: string, _joinEvent: unknown) => {
76
+ logVerbose(`badgerclaw: bot joined room ${roomId} (always mode), running post-join handshake`);
77
+ if (cfg.chatHistory?.enabled) {
78
+ initRoomHistory(roomId);
77
79
  }
80
+ await postJoinEncryptionHandshake(roomId);
78
81
  });
79
82
  return;
80
83
  }
@@ -112,6 +115,10 @@ export function registerMatrixAutoJoin(params: {
112
115
  try {
113
116
  await client.joinRoom(roomId);
114
117
  logVerbose(`badgerclaw: joined room ${roomId}`);
118
+ if (cfg.chatHistory?.enabled) {
119
+ initRoomHistory(roomId);
120
+ }
121
+ await postJoinEncryptionHandshake(roomId);
115
122
  } catch (err) {
116
123
  runtime.error?.(`badgerclaw: failed to join room ${roomId}: ${String(err)}`);
117
124
  }
@@ -13,7 +13,12 @@ const SESSION_STORE_PATH = path.join(
13
13
  ".openclaw/agents/main/sessions/sessions.json"
14
14
  );
15
15
 
16
- function loadRoomConfig(): Record<string, { autoReply?: boolean }> {
16
+ type RoomConfig = {
17
+ autoReply?: boolean; // legacy — kept for migration
18
+ activeBots?: string[]; // per-bot auto-reply list
19
+ };
20
+
21
+ function loadRoomConfig(): Record<string, RoomConfig> {
17
22
  try {
18
23
  return JSON.parse(fs.readFileSync(ROOM_CONFIG_PATH, "utf-8"));
19
24
  } catch {
@@ -21,14 +26,13 @@ function loadRoomConfig(): Record<string, { autoReply?: boolean }> {
21
26
  }
22
27
  }
23
28
 
24
- function saveRoomConfig(config: Record<string, { autoReply?: boolean }>): void {
29
+ function saveRoomConfig(config: Record<string, RoomConfig>): void {
25
30
  fs.writeFileSync(ROOM_CONFIG_PATH, JSON.stringify(config, null, 2));
26
31
  }
27
32
 
28
33
  function setGroupActivation(roomId: string, activation: "always" | "mention"): void {
29
34
  try {
30
35
  const store = JSON.parse(fs.readFileSync(SESSION_STORE_PATH, "utf-8"));
31
- // Session key format: agent:main:badgerclaw:channel:!roomid (lowercase)
32
36
  const sessionKey = `agent:main:badgerclaw:channel:${roomId.toLowerCase()}`;
33
37
  if (!store[sessionKey]) {
34
38
  store[sessionKey] = {};
@@ -41,9 +45,82 @@ function setGroupActivation(roomId: string, activation: "always" | "mention"): v
41
45
  }
42
46
  }
43
47
 
48
+ /**
49
+ * Check if a specific bot has auto-reply enabled in a room.
50
+ * Returns true if the bot is in the activeBots list.
51
+ * Migrates legacy autoReply boolean to activeBots format.
52
+ */
53
+ export function isBotAutoReplyEnabled(roomId: string, botUserId: string): boolean | undefined {
54
+ const config = loadRoomConfig();
55
+ const normalizedRoomId = roomId.toLowerCase();
56
+ const keys = Object.keys(config);
57
+ const matchedKey = keys.find((k) => k.toLowerCase() === normalizedRoomId);
58
+ if (!matchedKey) return undefined;
59
+
60
+ const room = config[matchedKey];
61
+
62
+ // New format: check activeBots list
63
+ if (Array.isArray(room.activeBots)) {
64
+ return room.activeBots.some(
65
+ (b) => b.toLowerCase() === botUserId.toLowerCase()
66
+ );
67
+ }
68
+
69
+ // Legacy migration: if autoReply is set but no activeBots, treat as legacy
70
+ if (typeof room.autoReply === "boolean") {
71
+ return room.autoReply;
72
+ }
73
+
74
+ return undefined;
75
+ }
76
+
77
+ // Legacy compat export
44
78
  export function getRoomAutoReply(roomId: string): boolean | undefined {
45
79
  const config = loadRoomConfig();
46
- return config[roomId]?.autoReply;
80
+ const normalizedRoomId = roomId.toLowerCase();
81
+ const keys = Object.keys(config);
82
+ const matchedKey = keys.find((k) => k.toLowerCase() === normalizedRoomId);
83
+ if (!matchedKey) return undefined;
84
+
85
+ const room = config[matchedKey];
86
+ if (Array.isArray(room.activeBots) && room.activeBots.length > 0) {
87
+ return true; // At least one bot is active
88
+ }
89
+ return room.autoReply;
90
+ }
91
+
92
+ function addBotToActive(roomId: string, botUserId: string): void {
93
+ const config = loadRoomConfig();
94
+ const normalizedRoomId = roomId.toLowerCase();
95
+ if (!config[normalizedRoomId]) {
96
+ config[normalizedRoomId] = {};
97
+ }
98
+ const room = config[normalizedRoomId];
99
+ if (!Array.isArray(room.activeBots)) {
100
+ room.activeBots = [];
101
+ }
102
+ const botLower = botUserId.toLowerCase();
103
+ if (!room.activeBots.some((b) => b.toLowerCase() === botLower)) {
104
+ room.activeBots.push(botUserId);
105
+ }
106
+ // Clear legacy field
107
+ delete room.autoReply;
108
+ saveRoomConfig(config);
109
+ }
110
+
111
+ function removeAllActiveBots(roomId: string): void {
112
+ const config = loadRoomConfig();
113
+ const normalizedRoomId = roomId.toLowerCase();
114
+ if (!config[normalizedRoomId]) {
115
+ config[normalizedRoomId] = {};
116
+ }
117
+ config[normalizedRoomId].activeBots = [];
118
+ delete config[normalizedRoomId].autoReply;
119
+ saveRoomConfig(config);
120
+ }
121
+
122
+ function getDisplayName(userId: string): string {
123
+ return userId.split(":")[0];
47
124
  }
48
125
 
49
126
  export async function handleBotCommand(params: {
@@ -63,7 +140,6 @@ export async function handleBotCommand(params: {
63
140
  const parts = trimmed.split(/\s+/);
64
141
  const command = parts[1]?.toLowerCase();
65
142
  const arg = parts[2]?.toLowerCase();
66
- const arg2 = parts[3];
67
143
 
68
144
  try {
69
145
  switch (command) {
@@ -77,112 +153,196 @@ export async function handleBotCommand(params: {
77
153
  "",
78
154
  "/bot help",
79
155
  " Show this help message with all available commands.",
80
- " Works in: Any room or DM",
81
156
  "",
82
157
  "/bot talk on",
83
- " Enable auto-reply mode. The bot will respond to every",
84
- " message in this room without needing an @mention.",
85
- " Perfect for 1-on-1 rooms with your AI assistant.",
86
- " Works in: Any room or DM",
158
+ " Enable auto-reply for this bot. It will respond to",
159
+ " every message without needing an @mention.",
160
+ " If multiple bots are in the room, specify which one:",
161
+ " /bot talk on @botname",
87
162
  "",
88
163
  "/bot talk off",
89
- " Disable auto-reply mode. The bot will only respond",
90
- " when @mentioned. Use this in busy group rooms where",
91
- " you don't want the bot responding to everything.",
92
- " Works in: Any room or DM",
164
+ " Disable auto-reply for ALL bots in this room.",
165
+ " All bots go back to mention-only mode.",
93
166
  "",
94
167
  "/bot add <botname>",
95
- " Invite a bot to the current room. The bot username",
96
- " will be @<botname>_bot on this server.",
168
+ " Invite a bot to the current room.",
97
169
  " Example: /bot add jarvis → invites @jarvis_bot",
98
- " Works in: Any room",
170
+ " Also accepts: /bot add @jarvis_bot",
99
171
  "",
100
- "━━━ BotBadger DM Only ━━━",
101
- "",
102
- "/bot new",
103
- " Create a new bot. Starts a guided flow to set up",
104
- " a bot name, username, and generate a pairing code",
105
- " for connecting to an OpenClaw instance.",
106
- " Works in: Direct message with BotBadger only",
107
- "",
108
- "/bot pair <name>",
109
- " Generate a new pairing code for an existing bot.",
110
- " Use this to connect or reconnect a bot to OpenClaw.",
111
- " Works in: Direct message with BotBadger only",
172
+ "/bot list",
173
+ " List all bots in this room and their auto-reply status.",
112
174
  "",
113
- "/bot delete <name>",
114
- " Permanently delete a bot and remove it from all rooms.",
115
- " This action cannot be undone.",
116
- " Works in: Direct message with BotBadger only",
175
+ "━━━ BotBadger DM Only ━━━",
117
176
  "",
118
- "/bot list",
119
- " List all your bots and their connection status.",
120
- " 🟢 Connectedbot is online and responding",
121
- " 🔴 Not connected — bot needs pairing",
122
- " Works in: Any room or DM",
177
+ "/bot new — Create a new bot",
178
+ "/bot pair <name> Get a pairing code",
179
+ "/bot delete <name> Permanently delete a bot",
123
180
  ].join("\n"),
124
181
  });
125
182
  return true;
126
183
  }
127
184
 
128
185
  case "talk": {
186
+ // Resolve bot members in the room
187
+ const botSuffix = "_bot:badger.signout.io";
188
+ let roomBots: string[] = [];
189
+ try {
190
+ const members = await client.getJoinedRoomMembers(roomId);
191
+ roomBots = members.filter((m: string) => m.includes(botSuffix) && m !== selfUserId);
192
+ } catch {
193
+ // If we can't list members, proceed as single-bot
194
+ }
195
+ const multipleBots = roomBots.length > 0;
196
+
197
+ // Check if a specific bot was mentioned: /bot talk on @botname
198
+ const mentionArg = parts.slice(3).join(" ").trim();
199
+ const mentionedBot = mentionArg
200
+ ? mentionArg.startsWith("@") ? mentionArg : `@${mentionArg}`
201
+ : null;
202
+
203
+ // If a bot was mentioned and it's not us, ignore the command (let that bot handle it)
204
+ if (mentionedBot) {
205
+ const mentionLower = mentionedBot.toLowerCase();
206
+ const selfLower = selfUserId.toLowerCase();
207
+ const selfLocalpart = selfLower.split(":")[0];
208
+ if (!selfLower.startsWith(mentionLower) && !mentionLower.startsWith(selfLocalpart)) {
209
+ return false;
210
+ }
211
+ }
212
+
129
213
  if (arg === "on") {
130
- const config = loadRoomConfig();
131
- config[roomId] = { ...config[roomId], autoReply: true };
132
- saveRoomConfig(config);
214
+ // Multiple bots + no specific mention → ask which bot
215
+ if (multipleBots && !mentionedBot) {
216
+ const allBots = [selfUserId, ...roomBots];
217
+ const config = loadRoomConfig();
218
+ const normalizedRoomId = roomId.toLowerCase();
219
+ const active = config[normalizedRoomId]?.activeBots ?? [];
220
+
221
+ const botList = allBots
222
+ .map((b, i) => {
223
+ const name = getDisplayName(b);
224
+ const isActive = active.some((a) => a.toLowerCase() === b.toLowerCase());
225
+ return ` ${i + 1}. ${name} ${isActive ? "🟢 auto-reply on" : "⚪ mention-only"}`;
226
+ })
227
+ .join("\n");
228
+
229
+ await client.sendMessage(roomId, {
230
+ msgtype: "m.text",
231
+ body: [
232
+ "🦡 Multiple bots in this room:",
233
+ "",
234
+ botList,
235
+ "",
236
+ "Specify which bot: /bot talk on @botname",
237
+ ].join("\n"),
238
+ });
239
+ return true;
240
+ }
241
+
242
+ // Add THIS bot to activeBots
243
+ addBotToActive(roomId, selfUserId);
133
244
  setGroupActivation(roomId, "always");
134
245
  await client.sendMessage(roomId, {
135
246
  msgtype: "m.text",
136
- body: "✅ Auto-reply enabled — I'll respond to every message in this room.",
247
+ body: `✅ Auto-reply enabled for ${getDisplayName(selfUserId)} — I'll respond to every message in this room.`,
137
248
  });
138
249
  return true;
139
250
  }
251
+
140
252
  if (arg === "off") {
141
- const config = loadRoomConfig();
142
- config[roomId] = { ...config[roomId], autoReply: false };
143
- saveRoomConfig(config);
253
+ // Kill switch — remove ALL bots from activeBots
254
+ removeAllActiveBots(roomId);
144
255
  setGroupActivation(roomId, "mention");
145
256
  await client.sendMessage(roomId, {
146
257
  msgtype: "m.text",
147
- body: "✅ Auto-reply disabled — I'll only respond when @mentioned.",
258
+ body: "✅ Auto-reply disabled for all bots bots will only respond when @mentioned.",
148
259
  });
149
260
  return true;
150
261
  }
262
+
151
263
  await client.sendMessage(roomId, {
152
264
  msgtype: "m.text",
153
- body: "Usage: /bot talk on or /bot talk off",
265
+ body: "Usage: /bot talk on [@botname] or /bot talk off",
154
266
  });
155
267
  return true;
156
268
  }
157
269
 
158
270
  case "add": {
159
- const botName = arg;
160
- if (!botName) {
271
+ let rawName = parts.slice(2).join(" ").trim();
272
+ if (!rawName) {
161
273
  await client.sendMessage(roomId, {
162
274
  msgtype: "m.text",
163
- body: "Usage: /bot add <botname>",
275
+ body: "Usage: /bot add <botname>\nExample: /bot add jarvis or /bot add @jarvis_bot",
164
276
  });
165
277
  return true;
166
278
  }
167
- const botUserId = `@${botName}_bot:badger.signout.io`;
279
+ rawName = rawName.replace(/^@/, "").split(":")[0].replace(/_bot$/i, "").toLowerCase();
280
+ const addBotUserId = `@${rawName}_bot:badger.signout.io`;
281
+ const addBotDisplay = `@${rawName}_bot`;
168
282
  try {
169
- await client.inviteUser(botUserId, roomId);
283
+ await client.inviteUser(addBotUserId, roomId);
170
284
  await client.sendMessage(roomId, {
171
285
  msgtype: "m.text",
172
- body: `✅ Invited ${botUserId} to this room.`,
286
+ body: `✅ Invited ${addBotDisplay} to this room.`,
173
287
  });
174
288
  } catch (err) {
175
289
  const msg = err instanceof Error ? err.message : String(err);
290
+ const cleanMsg = msg.replace(/:badger\.signout\.io/g, "");
176
291
  await client.sendMessage(roomId, {
177
292
  msgtype: "m.text",
178
- body: `❌ Failed to invite ${botUserId}: ${msg}`,
293
+ body: `❌ Failed to invite ${addBotDisplay}: ${cleanMsg}`,
179
294
  });
180
295
  }
181
296
  return true;
182
297
  }
183
298
 
299
+ case "list": {
300
+ const botSuffix2 = "_bot:badger.signout.io";
301
+ let botsInRoom: string[] = [];
302
+ try {
303
+ const members = await client.getJoinedRoomMembers(roomId);
304
+ botsInRoom = members.filter((m: string) => m.includes(botSuffix2));
305
+ } catch {
306
+ botsInRoom = [selfUserId];
307
+ }
308
+
309
+ if (botsInRoom.length === 0) {
310
+ await client.sendMessage(roomId, {
311
+ msgtype: "m.text",
312
+ body: "No bots in this room.",
313
+ });
314
+ return true;
315
+ }
316
+
317
+ const config = loadRoomConfig();
318
+ const normalizedRoomId = roomId.toLowerCase();
319
+ const active = config[normalizedRoomId]?.activeBots ?? [];
320
+ // Legacy: if autoReply is true and no activeBots, all bots are active
321
+ const legacyAllActive = !Array.isArray(config[normalizedRoomId]?.activeBots) && config[normalizedRoomId]?.autoReply === true;
322
+
323
+ const botList = botsInRoom
324
+ .map((b) => {
325
+ const displayName = getDisplayName(b);
326
+ const isMe = b === selfUserId;
327
+ const isActive = legacyAllActive || active.some((a) => a.toLowerCase() === b.toLowerCase());
328
+ return ` ${isActive ? "🟢" : "⚪"} ${displayName}${isMe ? " (me)" : ""} — ${isActive ? "auto-reply on" : "mention-only"}`;
329
+ })
330
+ .join("\n");
331
+
332
+ await client.sendMessage(roomId, {
333
+ msgtype: "m.text",
334
+ body: [
335
+ "🦡 Bots in this room:",
336
+ "",
337
+ botList,
338
+ "",
339
+ `${botsInRoom.length} bot${botsInRoom.length === 1 ? "" : "s"} total`,
340
+ ].join("\n"),
341
+ });
342
+ return true;
343
+ }
344
+
184
345
  default: {
185
- // Unknown /bot command — show help hint
186
346
  await client.sendMessage(roomId, {
187
347
  msgtype: "m.text",
188
348
  body: "Unknown command. Type /bot help for available commands.",
@@ -192,6 +352,6 @@ export async function handleBotCommand(params: {
192
352
  }
193
353
  } catch (err) {
194
354
  console.error("badgerclaw: bot command error:", err);
195
- return true; // Still consumed the command
355
+ return true;
196
356
  }
197
357
  }
@@ -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,
@@ -472,14 +473,50 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
472
473
  });
473
474
  return;
474
475
  }
476
+ // Check room-config.json first (set by /bot talk on|off) — it takes priority
477
+ // over the OpenClaw config wildcard entry. Supports per-bot activeBots list.
478
+ const roomConfigJsonAutoReply = (() => {
479
+ try {
480
+ const fs = require("fs");
481
+ const path = require("path");
482
+ const configPath = path.join(
483
+ process.env.HOME || "/tmp",
484
+ ".openclaw/extensions/badgerclaw/room-config.json"
485
+ );
486
+ const raw = fs.readFileSync(configPath, "utf-8");
487
+ const roomCfg = JSON.parse(raw) as Record<string, { autoReply?: boolean; activeBots?: string[] }>;
488
+ const normalizedRoomId = roomId.toLowerCase();
489
+ const keys = Object.keys(roomCfg);
490
+ const matchedKey = keys.find((k) => k.toLowerCase() === normalizedRoomId);
491
+ if (matchedKey) {
492
+ const entry = roomCfg[matchedKey];
493
+ // New per-bot format: check if THIS bot is in activeBots
494
+ if (Array.isArray(entry.activeBots)) {
495
+ const botActive = entry.activeBots.some(
496
+ (b: string) => b.toLowerCase() === selfUserId.toLowerCase()
497
+ );
498
+ return botActive; // true = this bot auto-replies, false = mention-only
499
+ }
500
+ // Legacy format
501
+ if (typeof entry.autoReply === "boolean") {
502
+ return entry.autoReply;
503
+ }
504
+ }
505
+ } catch { /* ignore */ }
506
+ return undefined;
507
+ })();
475
508
  const shouldRequireMention = isRoom
476
- ? roomConfig?.autoReply === true
509
+ ? roomConfigJsonAutoReply === true
477
510
  ? false
478
- : roomConfig?.autoReply === false
511
+ : roomConfigJsonAutoReply === false
479
512
  ? true
480
- : typeof roomConfig?.requireMention === "boolean"
481
- ? roomConfig?.requireMention
482
- : true
513
+ : roomConfig?.autoReply === true
514
+ ? false
515
+ : roomConfig?.autoReply === false
516
+ ? true
517
+ : typeof roomConfig?.requireMention === "boolean"
518
+ ? roomConfig?.requireMention
519
+ : true
483
520
  : false;
484
521
  const shouldBypassMention =
485
522
  allowTextCommands &&
@@ -594,13 +631,33 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
594
631
  });
595
632
 
596
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
+
597
658
  const ctxPayload = core.channel.reply.finalizeInboundContext({
598
659
  Body: body,
599
- BodyForAgent: resolveMatrixBodyForAgent({
600
- isDirectMessage,
601
- bodyText,
602
- senderLabel,
603
- }),
660
+ BodyForAgent: bodyForAgent,
604
661
  RawBody: bodyText,
605
662
  CommandBody: bodyText,
606
663
  From: isDirectMessage ? `badgerclaw:${senderId}` : `badgerclaw:channel:${roomId}`,
@@ -739,6 +796,15 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam
739
796
  accountId: route.accountId,
740
797
  tableMode,
741
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
+ }
742
808
  didSendReply = true;
743
809
  },
744
810
  onError: (err, info) => {
package/src/onboarding.ts CHANGED
@@ -245,6 +245,16 @@ export const badgerclawOnboardingAdapter: ChannelOnboardingAdapter = {
245
245
  autoJoin: "always",
246
246
  // Open policy: bot responds in any room it's invited to
247
247
  groupPolicy: "open",
248
+ // Default group config: auto-reply in all rooms.
249
+ // No systemPrompt override — the bot respects the user's workspace personality
250
+ // (SOUL.md, IDENTITY.md, etc.). When the bot joins a group, it sends a hint
251
+ // message about /bot talk on|off so users know how to control it.
252
+ groups: {
253
+ "*": {
254
+ autoReply: true,
255
+ enabled: true,
256
+ },
257
+ },
248
258
  dm: {
249
259
  ...next.channels?.badgerclaw?.dm,
250
260
  policy: "open",
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
  };
package/room-config.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "!oVrHNyMFgnOCfmjlLU:badger.signout.io": {
3
- "autoReply": true
4
- },
5
- "!hcLBnfZOzoIHENRxuU:badger.signout.io": {
6
- "autoReply": true
7
- }
8
- }