@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 +200 -40
- package/package.json +1 -1
- package/src/group-mentions.ts +28 -10
- package/src/matrix/monitor/auto-join.ts +41 -34
- package/src/matrix/monitor/bot-commands.ts +216 -56
- package/src/matrix/monitor/chat-history.ts +75 -0
- package/src/matrix/monitor/handler.ts +76 -10
- package/src/onboarding.ts +10 -0
- package/src/types.ts +3 -0
- package/room-config.json +0 -8
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
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
31
|
+
5. **Copy the pairing code** (BCK-XXXX-XXXX)
|
|
37
32
|
|
|
38
|
-
> **Need a new code?** Type `/bot pair
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
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
|
|
87
|
-
2. **Add to a room** — type `/bot add
|
|
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
|
|
89
|
+
Your OpenClaw agent will receive messages and respond — fully end-to-end encrypted.
|
|
91
90
|
|
|
92
91
|
---
|
|
93
92
|
|
|
94
|
-
##
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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 (
|
|
280
|
+
BadgerClaw Server (Matrix homeserver)
|
|
121
281
|
│
|
|
122
282
|
│ E2EE Messages
|
|
123
283
|
▼
|
package/package.json
CHANGED
package/src/group-mentions.ts
CHANGED
|
@@ -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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
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=${
|
|
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
|
-
//
|
|
44
|
-
(async () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
84
|
-
" message
|
|
85
|
-
"
|
|
86
|
-
"
|
|
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
|
|
90
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
170
|
+
" Also accepts: /bot add @jarvis_bot",
|
|
99
171
|
"",
|
|
100
|
-
"
|
|
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
|
-
"
|
|
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
|
|
119
|
-
"
|
|
120
|
-
"
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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:
|
|
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
|
-
|
|
142
|
-
|
|
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 —
|
|
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
|
-
|
|
160
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
283
|
+
await client.inviteUser(addBotUserId, roomId);
|
|
170
284
|
await client.sendMessage(roomId, {
|
|
171
285
|
msgtype: "m.text",
|
|
172
|
-
body: `✅ Invited ${
|
|
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 ${
|
|
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;
|
|
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
|
-
?
|
|
509
|
+
? roomConfigJsonAutoReply === true
|
|
477
510
|
? false
|
|
478
|
-
:
|
|
511
|
+
: roomConfigJsonAutoReply === false
|
|
479
512
|
? true
|
|
480
|
-
:
|
|
481
|
-
?
|
|
482
|
-
:
|
|
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:
|
|
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