badgerclaw 0.2.9
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/README.md +54 -0
- package/dist/claude-code/mcp-server.js +597 -0
- package/dist/index.js +131 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# BadgerClaw CLI
|
|
2
|
+
|
|
3
|
+
One-click bot provisioning for BadgerClaw.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g badgerclaw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Authenticate
|
|
15
|
+
badgerclaw login
|
|
16
|
+
|
|
17
|
+
# Check status
|
|
18
|
+
badgerclaw status
|
|
19
|
+
|
|
20
|
+
# Manage bots
|
|
21
|
+
badgerclaw bot create mybot
|
|
22
|
+
badgerclaw bot list
|
|
23
|
+
badgerclaw bot delete mybot
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Development
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install
|
|
30
|
+
npm run build
|
|
31
|
+
node dist/index.js --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Local Backend
|
|
35
|
+
|
|
36
|
+
Use environment variables to point the CLI at your local API and website instead of production.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Shorthand — sets both API and auth URL to localhost defaults
|
|
40
|
+
BADGERCLAW_ENV=local node dist/index.js login
|
|
41
|
+
|
|
42
|
+
# Or override URLs individually
|
|
43
|
+
BADGERCLAW_API_URL=http://localhost:8000 \
|
|
44
|
+
BADGERCLAW_AUTH_URL=http://localhost:5500 \
|
|
45
|
+
node dist/index.js login
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Variable | Default (production) | Local value |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `BADGERCLAW_ENV` | _(unset)_ | `local` |
|
|
51
|
+
| `BADGERCLAW_API_URL` | `https://api.badger.signout.io` | `http://localhost:8000` |
|
|
52
|
+
| `BADGERCLAW_AUTH_URL` | `https://badgerclaw.ai` | `http://localhost:5500` |
|
|
53
|
+
|
|
54
|
+
Make sure the local API (`badgerclaw-api`) and website (`serve.py --local`) are both running before testing.
|
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/claude-code/mcp-server.ts
|
|
31
|
+
var mcp_server_exports = {};
|
|
32
|
+
__export(mcp_server_exports, {
|
|
33
|
+
BadgerclawMCPServer: () => BadgerclawMCPServer
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(mcp_server_exports);
|
|
36
|
+
var import_server = require("@modelcontextprotocol/sdk/server/index.js");
|
|
37
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
38
|
+
var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
39
|
+
var import_http = __toESM(require("http"));
|
|
40
|
+
var import_axios = __toESM(require("axios"));
|
|
41
|
+
var import_fs = __toESM(require("fs"));
|
|
42
|
+
var import_path = __toESM(require("path"));
|
|
43
|
+
var import_os = __toESM(require("os"));
|
|
44
|
+
var GATEWAY_BASE = "http://localhost:7331";
|
|
45
|
+
var USAGE_LOG_DIR = import_path.default.join(import_os.default.homedir(), ".openclaw", "extensions", "badgerclaw");
|
|
46
|
+
var USAGE_LOG_FILE = import_path.default.join(USAGE_LOG_DIR, "claude-code-usage.jsonl");
|
|
47
|
+
var BadgerclawMCPServer = class {
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.httpServer = null;
|
|
50
|
+
this.messageQueue = [];
|
|
51
|
+
this.processing = false;
|
|
52
|
+
this.currentAbortController = null;
|
|
53
|
+
// Progress tracking
|
|
54
|
+
this.activeContext = null;
|
|
55
|
+
this.lastProgressAt = 0;
|
|
56
|
+
this.PROGRESS_RATE_LIMIT_MS = 3e3;
|
|
57
|
+
// Usage tracking (per processing run)
|
|
58
|
+
this.sessionUsage = null;
|
|
59
|
+
this.port = options.port;
|
|
60
|
+
this.sessionId = options.sessionId;
|
|
61
|
+
this.server = new import_server.Server(
|
|
62
|
+
{
|
|
63
|
+
name: "badgerclaw",
|
|
64
|
+
version: "0.2.4"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
capabilities: {
|
|
68
|
+
tools: {},
|
|
69
|
+
experimental: { "claude/channel": {} }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
this.server.fallbackNotificationHandler = async (notification) => {
|
|
74
|
+
this.handleClaudeNotification(notification);
|
|
75
|
+
};
|
|
76
|
+
this.registerTools();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Inspect a notification coming from Claude Code and extract progress hints.
|
|
80
|
+
* The notification may carry tool_use blocks or logging messages.
|
|
81
|
+
*/
|
|
82
|
+
handleClaudeNotification(notification) {
|
|
83
|
+
try {
|
|
84
|
+
const method = notification?.method || "";
|
|
85
|
+
const params = notification?.params || {};
|
|
86
|
+
if (method === "notifications/message" || method === "notifications/claude/channel") {
|
|
87
|
+
const content = params?.content || params?.message || "";
|
|
88
|
+
this.detectToolActivity(content);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Given arbitrary content (string or object array) from Claude Code,
|
|
95
|
+
* detect tool_use blocks and emit progress updates.
|
|
96
|
+
*/
|
|
97
|
+
detectToolActivity(content) {
|
|
98
|
+
if (!content) return;
|
|
99
|
+
let blocks = [];
|
|
100
|
+
if (typeof content === "string") {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(content);
|
|
103
|
+
if (Array.isArray(parsed)) blocks = parsed;
|
|
104
|
+
} catch {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
} else if (Array.isArray(content)) {
|
|
108
|
+
blocks = content;
|
|
109
|
+
} else if (typeof content === "object" && content.type) {
|
|
110
|
+
blocks = [content];
|
|
111
|
+
}
|
|
112
|
+
for (const block of blocks) {
|
|
113
|
+
if (block?.type === "tool_use") {
|
|
114
|
+
const toolName = block.name || "";
|
|
115
|
+
const toolInput = block.input || {};
|
|
116
|
+
const progressMsg = this.formatToolProgress(toolName, toolInput);
|
|
117
|
+
if (progressMsg) {
|
|
118
|
+
this.sendProgress(progressMsg).catch(() => {
|
|
119
|
+
});
|
|
120
|
+
if (this.sessionUsage) this.sessionUsage.toolCallsCount++;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Map a Claude Code native tool name + input to a human-readable progress line.
|
|
127
|
+
*/
|
|
128
|
+
formatToolProgress(toolName, input) {
|
|
129
|
+
const name = toolName.toLowerCase();
|
|
130
|
+
if (name === "read" || name === "read_file" || name === "readfile") {
|
|
131
|
+
const filePath = input.file_path || input.path || input.filename || "";
|
|
132
|
+
const short = filePath ? `\`${import_path.default.basename(filePath)}\`` : "file";
|
|
133
|
+
return `Reading ${short}...`;
|
|
134
|
+
}
|
|
135
|
+
if (name === "edit" || name === "edit_file" || name === "multiedit" || name === "write" || name === "write_file") {
|
|
136
|
+
const filePath = input.file_path || input.path || input.filename || "";
|
|
137
|
+
const short = filePath ? `\`${import_path.default.basename(filePath)}\`` : "file";
|
|
138
|
+
return `Editing ${short}...`;
|
|
139
|
+
}
|
|
140
|
+
if (name === "bash" || name === "run_command" || name === "shell" || name === "execute") {
|
|
141
|
+
const cmd = input.command || input.cmd || "";
|
|
142
|
+
const short = cmd ? cmd.split(" ")[0] : "";
|
|
143
|
+
return short ? `Running \`${short}\`...` : "Running command...";
|
|
144
|
+
}
|
|
145
|
+
if (name === "glob" || name === "find_files") {
|
|
146
|
+
return "Searching files...";
|
|
147
|
+
}
|
|
148
|
+
if (name === "grep" || name === "search") {
|
|
149
|
+
return "Searching codebase...";
|
|
150
|
+
}
|
|
151
|
+
if (name === "webfetch" || name === "web_fetch" || name === "websearch" || name === "web_search") {
|
|
152
|
+
return "Fetching from web...";
|
|
153
|
+
}
|
|
154
|
+
if (name === "reply" || name === "search_messages" || name === "get_recent_messages" || name === "set_project_directory") {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return `Using \`${toolName}\`...`;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Send a progress message to the gateway, respecting the rate limit.
|
|
161
|
+
* Requires an active context (botId + roomId).
|
|
162
|
+
*/
|
|
163
|
+
async sendProgress(message) {
|
|
164
|
+
if (!this.activeContext) return;
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
if (now - this.lastProgressAt < this.PROGRESS_RATE_LIMIT_MS) return;
|
|
167
|
+
this.lastProgressAt = now;
|
|
168
|
+
try {
|
|
169
|
+
await import_axios.default.post(`${GATEWAY_BASE}/claude-code/reply`, {
|
|
170
|
+
botId: this.activeContext.botId,
|
|
171
|
+
roomId: this.activeContext.roomId,
|
|
172
|
+
message,
|
|
173
|
+
progress: true
|
|
174
|
+
}, { timeout: 8e3 });
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Send the "Working on it..." message immediately (bypasses rate limit).
|
|
180
|
+
*/
|
|
181
|
+
async sendWorkingOnIt() {
|
|
182
|
+
if (!this.activeContext) return;
|
|
183
|
+
this.lastProgressAt = 0;
|
|
184
|
+
await this.sendProgress("Working on it...");
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Append a usage record to the JSONL log file.
|
|
188
|
+
*/
|
|
189
|
+
logUsage(usage, durationMs) {
|
|
190
|
+
try {
|
|
191
|
+
import_fs.default.mkdirSync(USAGE_LOG_DIR, { recursive: true });
|
|
192
|
+
const record = {
|
|
193
|
+
sessionId: usage.sessionId,
|
|
194
|
+
botId: usage.botId,
|
|
195
|
+
roomId: usage.roomId,
|
|
196
|
+
toolCallsCount: usage.toolCallsCount,
|
|
197
|
+
durationMs,
|
|
198
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
199
|
+
};
|
|
200
|
+
import_fs.default.appendFileSync(USAGE_LOG_FILE, JSON.stringify(record) + "\n", "utf-8");
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error(`[mcp-server] Usage log write failed: ${err.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
registerTools() {
|
|
206
|
+
this.server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({
|
|
207
|
+
tools: [
|
|
208
|
+
{
|
|
209
|
+
name: "reply",
|
|
210
|
+
description: "Send a message back to the Matrix room. Use this to respond to the user in the chat room.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: {
|
|
214
|
+
botId: { type: "string", description: "Bot ID to reply as" },
|
|
215
|
+
roomId: { type: "string", description: "Matrix room ID to reply in" },
|
|
216
|
+
message: { type: "string", description: "Message text to send" }
|
|
217
|
+
},
|
|
218
|
+
required: ["botId", "roomId", "message"]
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "search_messages",
|
|
223
|
+
description: "Search the message vault using vector + keyword hybrid search. Returns semantically relevant messages from chat history.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {
|
|
227
|
+
query: { type: "string", description: "Search query" },
|
|
228
|
+
roomId: { type: "string", description: "Optional room ID to filter by" },
|
|
229
|
+
limit: { type: "number", description: "Max results (default 10)" }
|
|
230
|
+
},
|
|
231
|
+
required: ["query"]
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "get_recent_messages",
|
|
236
|
+
description: "Get the last N messages from a room's rolling history.",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
roomId: { type: "string", description: "Matrix room ID" },
|
|
241
|
+
limit: { type: "number", description: "Number of messages (default 30)" }
|
|
242
|
+
},
|
|
243
|
+
required: ["roomId"]
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "set_project_directory",
|
|
248
|
+
description: "Change the working directory for this Claude Code session.",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
path: { type: "string", description: "Absolute or ~ path to project directory" }
|
|
253
|
+
},
|
|
254
|
+
required: ["path"]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
]
|
|
258
|
+
}));
|
|
259
|
+
this.server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
|
|
260
|
+
const { name, arguments: args } = request.params;
|
|
261
|
+
if (this.sessionUsage && name !== "reply") {
|
|
262
|
+
this.sessionUsage.toolCallsCount++;
|
|
263
|
+
}
|
|
264
|
+
switch (name) {
|
|
265
|
+
case "reply":
|
|
266
|
+
return this.handleReply(args);
|
|
267
|
+
case "search_messages":
|
|
268
|
+
return this.handleSearchMessages(args);
|
|
269
|
+
case "get_recent_messages":
|
|
270
|
+
return this.handleGetRecentMessages(args);
|
|
271
|
+
case "set_project_directory":
|
|
272
|
+
return this.handleSetProjectDirectory(args);
|
|
273
|
+
default:
|
|
274
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
async handleReply(args) {
|
|
279
|
+
try {
|
|
280
|
+
if (this.sessionUsage) {
|
|
281
|
+
const durationMs = Date.now() - this.sessionUsage.startedAt;
|
|
282
|
+
this.logUsage(this.sessionUsage, durationMs);
|
|
283
|
+
this.sessionUsage = null;
|
|
284
|
+
}
|
|
285
|
+
const MAX_MSG_SIZE = 6e4;
|
|
286
|
+
const messages = [];
|
|
287
|
+
if (Buffer.byteLength(args.message, "utf-8") > MAX_MSG_SIZE) {
|
|
288
|
+
const chunks = splitMessage(args.message, MAX_MSG_SIZE);
|
|
289
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
290
|
+
const label = chunks.length > 1 ? `(Part ${i + 1}/${chunks.length})
|
|
291
|
+
` : "";
|
|
292
|
+
messages.push(label + chunks[i]);
|
|
293
|
+
}
|
|
294
|
+
} else {
|
|
295
|
+
messages.push(args.message);
|
|
296
|
+
}
|
|
297
|
+
for (const msg of messages) {
|
|
298
|
+
await import_axios.default.post(`${GATEWAY_BASE}/claude-code/reply`, {
|
|
299
|
+
botId: args.botId,
|
|
300
|
+
roomId: args.roomId,
|
|
301
|
+
message: msg
|
|
302
|
+
}, { timeout: 15e3 });
|
|
303
|
+
}
|
|
304
|
+
return { content: [{ type: "text", text: "Reply sent." }] };
|
|
305
|
+
} catch (err) {
|
|
306
|
+
const errMsg = err.response?.data?.message || err.message || "Unknown error";
|
|
307
|
+
return { content: [{ type: "text", text: `Failed to send reply: ${errMsg}` }], isError: true };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async handleSearchMessages(args) {
|
|
311
|
+
try {
|
|
312
|
+
const params = { query: args.query };
|
|
313
|
+
if (args.roomId) params.roomId = args.roomId;
|
|
314
|
+
if (args.limit) params.limit = String(args.limit);
|
|
315
|
+
const resp = await import_axios.default.get(`${GATEWAY_BASE}/messages/search`, {
|
|
316
|
+
params,
|
|
317
|
+
timeout: 15e3
|
|
318
|
+
});
|
|
319
|
+
return { content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }] };
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const errMsg = err.response?.data?.message || err.message || "Unknown error";
|
|
322
|
+
return { content: [{ type: "text", text: `Search failed: ${errMsg}` }], isError: true };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async handleGetRecentMessages(args) {
|
|
326
|
+
try {
|
|
327
|
+
const params = { roomId: args.roomId };
|
|
328
|
+
if (args.limit) params.limit = String(args.limit);
|
|
329
|
+
const resp = await import_axios.default.get(`${GATEWAY_BASE}/messages/recent`, {
|
|
330
|
+
params,
|
|
331
|
+
timeout: 15e3
|
|
332
|
+
});
|
|
333
|
+
return { content: [{ type: "text", text: JSON.stringify(resp.data, null, 2) }] };
|
|
334
|
+
} catch (err) {
|
|
335
|
+
const errMsg = err.response?.data?.message || err.message || "Unknown error";
|
|
336
|
+
return { content: [{ type: "text", text: `Failed to get messages: ${errMsg}` }], isError: true };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async handleSetProjectDirectory(args) {
|
|
340
|
+
try {
|
|
341
|
+
const resolvedPath = args.path.replace(/^~/, process.env.HOME || "");
|
|
342
|
+
process.chdir(resolvedPath);
|
|
343
|
+
return { content: [{ type: "text", text: `Working directory changed to: ${resolvedPath}` }] };
|
|
344
|
+
} catch (err) {
|
|
345
|
+
return { content: [{ type: "text", text: `Failed to change directory: ${err.message}` }], isError: true };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Push a message to Claude Code by writing to the pending-input file.
|
|
350
|
+
* The expect wrapper script polls this file and types the content
|
|
351
|
+
* into Claude Code's TUI as user input.
|
|
352
|
+
*/
|
|
353
|
+
async pushToClaudeCode(message) {
|
|
354
|
+
try {
|
|
355
|
+
const msgFile = import_path.default.join(import_os.default.homedir(), ".badgerclaw", "pending-input.txt");
|
|
356
|
+
import_fs.default.writeFileSync(msgFile, message, "utf-8");
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.error(`[mcp-server] Failed to write pending input: ${err.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async processQueue() {
|
|
362
|
+
if (this.processing || this.messageQueue.length === 0) return;
|
|
363
|
+
this.processing = true;
|
|
364
|
+
while (this.messageQueue.length > 0) {
|
|
365
|
+
const msg = this.messageQueue.shift();
|
|
366
|
+
this.currentAbortController = new AbortController();
|
|
367
|
+
this.activeContext = { botId: msg.botId, roomId: msg.roomId };
|
|
368
|
+
this.sessionUsage = {
|
|
369
|
+
sessionId: this.sessionId,
|
|
370
|
+
botId: msg.botId,
|
|
371
|
+
roomId: msg.roomId,
|
|
372
|
+
toolCallsCount: 0,
|
|
373
|
+
startedAt: Date.now()
|
|
374
|
+
};
|
|
375
|
+
await this.sendWorkingOnIt();
|
|
376
|
+
const formatted = `[Room: ${msg.roomName}] ${msg.sender}: ${msg.message}
|
|
377
|
+
|
|
378
|
+
Reply using the reply tool with botId="${msg.botId}" and roomId="${msg.roomId}"`;
|
|
379
|
+
await this.pushToClaudeCode(formatted);
|
|
380
|
+
this.currentAbortController = null;
|
|
381
|
+
}
|
|
382
|
+
this.processing = false;
|
|
383
|
+
this.activeContext = null;
|
|
384
|
+
if (this.sessionUsage) {
|
|
385
|
+
const durationMs = Date.now() - this.sessionUsage.startedAt;
|
|
386
|
+
this.logUsage(this.sessionUsage, durationMs);
|
|
387
|
+
this.sessionUsage = null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
enqueueMessage(msg) {
|
|
391
|
+
const MAX_QUEUE_DEPTH = 10;
|
|
392
|
+
if (this.messageQueue.length >= MAX_QUEUE_DEPTH) {
|
|
393
|
+
this.messageQueue.shift();
|
|
394
|
+
console.warn("[mcp-server] Queue full, dropped oldest message");
|
|
395
|
+
}
|
|
396
|
+
this.messageQueue.push(msg);
|
|
397
|
+
this.processQueue();
|
|
398
|
+
}
|
|
399
|
+
cancelCurrentOperation() {
|
|
400
|
+
if (this.currentAbortController) {
|
|
401
|
+
this.currentAbortController.abort();
|
|
402
|
+
this.currentAbortController = null;
|
|
403
|
+
}
|
|
404
|
+
this.messageQueue.length = 0;
|
|
405
|
+
this.processing = false;
|
|
406
|
+
this.activeContext = null;
|
|
407
|
+
if (this.sessionUsage) {
|
|
408
|
+
const durationMs = Date.now() - this.sessionUsage.startedAt;
|
|
409
|
+
this.logUsage(this.sessionUsage, durationMs);
|
|
410
|
+
this.sessionUsage = null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Wrap the transport's onmessage handler to intercept all incoming
|
|
415
|
+
* JSON-RPC messages from Claude Code and detect tool activity.
|
|
416
|
+
*
|
|
417
|
+
* This is called after the Server has connected to the transport so
|
|
418
|
+
* we can chain onto the existing handler the SDK set up.
|
|
419
|
+
*/
|
|
420
|
+
installTransportSpy() {
|
|
421
|
+
const transport = this.server.transport;
|
|
422
|
+
if (!transport) return;
|
|
423
|
+
const originalOnMessage = transport.onmessage;
|
|
424
|
+
transport.onmessage = (msg, extra) => {
|
|
425
|
+
try {
|
|
426
|
+
this.spyOnJSONRPCMessage(msg);
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
if (originalOnMessage) originalOnMessage.call(transport, msg, extra);
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Inspect a raw JSON-RPC message for tool_use blocks emitted by Claude Code.
|
|
434
|
+
*
|
|
435
|
+
* Claude Code sends sampling/createMessage results back through the MCP
|
|
436
|
+
* protocol; those responses embed the model's tool_use content blocks.
|
|
437
|
+
* We look for those here.
|
|
438
|
+
*/
|
|
439
|
+
spyOnJSONRPCMessage(msg) {
|
|
440
|
+
if (!msg || typeof msg !== "object") return;
|
|
441
|
+
const content = [];
|
|
442
|
+
if (msg.result?.content) content.push(...Array.isArray(msg.result.content) ? msg.result.content : []);
|
|
443
|
+
if (msg.params?.content) content.push(...Array.isArray(msg.params.content) ? msg.params.content : []);
|
|
444
|
+
if (msg.params?.message?.content) {
|
|
445
|
+
const mc = msg.params.message.content;
|
|
446
|
+
content.push(...Array.isArray(mc) ? mc : []);
|
|
447
|
+
}
|
|
448
|
+
for (const block of content) {
|
|
449
|
+
if (block?.type === "tool_use") {
|
|
450
|
+
const toolName = block.name || "";
|
|
451
|
+
const toolInput = block.input || {};
|
|
452
|
+
const progressMsg = this.formatToolProgress(toolName, toolInput);
|
|
453
|
+
if (progressMsg) {
|
|
454
|
+
this.sendProgress(progressMsg).catch(() => {
|
|
455
|
+
});
|
|
456
|
+
if (this.sessionUsage) this.sessionUsage.toolCallsCount++;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
startHttpServer() {
|
|
462
|
+
this.httpServer = import_http.default.createServer((req, res) => {
|
|
463
|
+
let body = "";
|
|
464
|
+
req.on("data", (chunk) => body += chunk);
|
|
465
|
+
req.on("end", () => {
|
|
466
|
+
try {
|
|
467
|
+
if (req.method === "POST" && req.url === "/message") {
|
|
468
|
+
const msg = JSON.parse(body);
|
|
469
|
+
this.enqueueMessage(msg);
|
|
470
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
471
|
+
res.end(JSON.stringify({ ok: true, queued: this.messageQueue.length }));
|
|
472
|
+
} else if (req.method === "POST" && req.url === "/cancel") {
|
|
473
|
+
this.cancelCurrentOperation();
|
|
474
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
475
|
+
res.end(JSON.stringify({ ok: true, message: "Cancelled" }));
|
|
476
|
+
} else if (req.method === "GET" && req.url === "/health") {
|
|
477
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
478
|
+
res.end(JSON.stringify({
|
|
479
|
+
ok: true,
|
|
480
|
+
sessionId: this.sessionId,
|
|
481
|
+
queueDepth: this.messageQueue.length,
|
|
482
|
+
processing: this.processing
|
|
483
|
+
}));
|
|
484
|
+
} else {
|
|
485
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
486
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
487
|
+
}
|
|
488
|
+
} catch (err) {
|
|
489
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
490
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
this.httpServer.listen(this.port, "127.0.0.1", () => {
|
|
495
|
+
console.log(`[mcp-server] Listening on http://127.0.0.1:${this.port}`);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async injectSessionContext() {
|
|
499
|
+
try {
|
|
500
|
+
const statusResp = await import_axios.default.get(`${GATEWAY_BASE}/claude-code/status`, { timeout: 1e4 });
|
|
501
|
+
const rooms = statusResp.data?.rooms || [];
|
|
502
|
+
const sessionRooms = rooms.filter((r) => r.sessionId === this.sessionId);
|
|
503
|
+
if (sessionRooms.length === 0) return;
|
|
504
|
+
const contextParts = [];
|
|
505
|
+
for (const room of sessionRooms) {
|
|
506
|
+
try {
|
|
507
|
+
const recentResp = await import_axios.default.get(`${GATEWAY_BASE}/messages/recent`, {
|
|
508
|
+
params: { roomId: room.roomId, limit: "30" },
|
|
509
|
+
timeout: 1e4
|
|
510
|
+
});
|
|
511
|
+
const messages = recentResp.data?.messages || [];
|
|
512
|
+
if (messages.length > 0) {
|
|
513
|
+
const formatted = messages.map((m) => `[${m.timestamp}] ${m.sender}: ${m.text}`).join("\n");
|
|
514
|
+
contextParts.push(`## Recent messages in ${room.roomName}:
|
|
515
|
+
${formatted}`);
|
|
516
|
+
}
|
|
517
|
+
if (messages.length > 0) {
|
|
518
|
+
const summaryTexts = messages.slice(-5).map((m) => m.text).join(" ");
|
|
519
|
+
const summaryQuery = summaryTexts.slice(0, 200);
|
|
520
|
+
try {
|
|
521
|
+
const vaultResp = await import_axios.default.get(`${GATEWAY_BASE}/messages/search`, {
|
|
522
|
+
params: { query: summaryQuery, roomId: room.roomId, limit: "10" },
|
|
523
|
+
timeout: 1e4
|
|
524
|
+
});
|
|
525
|
+
const results = vaultResp.data?.results || [];
|
|
526
|
+
if (results.length > 0) {
|
|
527
|
+
const formatted = results.map((r) => `[${r.timestamp}] ${r.sender}: ${r.text} (score: ${r.score})`).join("\n");
|
|
528
|
+
contextParts.push(`## Relevant older context from ${room.roomName}:
|
|
529
|
+
${formatted}`);
|
|
530
|
+
}
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (contextParts.length > 0) {
|
|
538
|
+
await this.pushToClaudeCode(
|
|
539
|
+
"--- Session Context (recovered from history) ---\n\n" + contextParts.join("\n\n") + "\n\n--- End of context ---"
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
} catch (err) {
|
|
543
|
+
console.error(`[mcp-server] Session context injection failed: ${err.message}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async start() {
|
|
547
|
+
this.startHttpServer();
|
|
548
|
+
this.injectSessionContext().catch(() => {
|
|
549
|
+
});
|
|
550
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
551
|
+
await this.server.connect(transport);
|
|
552
|
+
this.installTransportSpy();
|
|
553
|
+
console.log(`[mcp-server] MCP server connected (session: ${this.sessionId})`);
|
|
554
|
+
}
|
|
555
|
+
async stop() {
|
|
556
|
+
if (this.httpServer) {
|
|
557
|
+
this.httpServer.close();
|
|
558
|
+
this.httpServer = null;
|
|
559
|
+
}
|
|
560
|
+
await this.server.close();
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
function splitMessage(text, maxBytes) {
|
|
564
|
+
const chunks = [];
|
|
565
|
+
const lines = text.split("\n");
|
|
566
|
+
let current = "";
|
|
567
|
+
for (const line of lines) {
|
|
568
|
+
const candidate = current ? current + "\n" + line : line;
|
|
569
|
+
if (Buffer.byteLength(candidate, "utf-8") > maxBytes && current) {
|
|
570
|
+
chunks.push(current);
|
|
571
|
+
current = line;
|
|
572
|
+
} else {
|
|
573
|
+
current = candidate;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (current) chunks.push(current);
|
|
577
|
+
return chunks;
|
|
578
|
+
}
|
|
579
|
+
if (require.main === module) {
|
|
580
|
+
const port = parseInt(process.env.BADGERCLAW_MCP_PORT || "7332", 10);
|
|
581
|
+
const sessionId = process.env.BADGERCLAW_SESSION_ID || "default";
|
|
582
|
+
const server = new BadgerclawMCPServer({ port, sessionId });
|
|
583
|
+
server.start().catch((err) => {
|
|
584
|
+
console.error(`[mcp-server] Fatal: ${err.message}`);
|
|
585
|
+
process.exit(1);
|
|
586
|
+
});
|
|
587
|
+
process.on("SIGINT", () => {
|
|
588
|
+
server.stop().then(() => process.exit(0));
|
|
589
|
+
});
|
|
590
|
+
process.on("SIGTERM", () => {
|
|
591
|
+
server.stop().then(() => process.exit(0));
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
595
|
+
0 && (module.exports = {
|
|
596
|
+
BadgerclawMCPServer
|
|
597
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var po=Object.create;var ge=Object.defineProperty;var fo=Object.getOwnPropertyDescriptor;var ho=Object.getOwnPropertyNames;var yo=Object.getPrototypeOf,wo=Object.prototype.hasOwnProperty;var bo=(e,t)=>()=>(e&&(t=e(e=0)),t);var _o=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),$o=(e,t)=>{for(var o in t)ge(e,o,{get:t[o],enumerable:!0})},ct=(e,t,o,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of ho(t))!wo.call(e,n)&&n!==o&&ge(e,n,{get:()=>t[n],enumerable:!(s=fo(t,n))||s.enumerable});return e};var i=(e,t,o)=>(o=e!=null?po(yo(e)):{},ct(t||!e||!e.__esModule?ge(o,"default",{value:e,enumerable:!0}):o,e)),lt=e=>ct(ge({},"__esModule",{value:!0}),e);var U=_o((Fn,Oo)=>{Oo.exports={name:"badgerclaw",version:"0.2.9",description:"BadgerClaw CLI \u2014 one-click bot provisioning",main:"dist/index.js",bin:{badgerclaw:"./dist/index.js"},files:["dist/","README.md"],scripts:{build:"node build.mjs",test:'echo "Error: no test specified" && exit 1'},keywords:[],author:"",license:"ISC",dependencies:{"@modelcontextprotocol/sdk":"^1.29.0",axios:"^1.6.0",chalk:"^4.1.2",commander:"^12.0.0",eventsource:"^1.1.2",open:"^8.4.2",ora:"^5.4.1"},devDependencies:{"@types/eventsource":"^1.1.15","@types/node":"^20.0.0",esbuild:"^0.28.0",typescript:"^5.3.0"}}});var nt={};$o(nt,{getActiveSessions:()=>ot,launchClaudeCode:()=>Ze,launchMCPServer:()=>Qe,recordSession:()=>et,stopSession:()=>tt});function ze(){try{if(I.default.existsSync(Je))return JSON.parse(I.default.readFileSync(Je,"utf-8"))}catch{}return[]}function Ye(e){I.default.mkdirSync(V,{recursive:!0}),I.default.writeFileSync(Je,JSON.stringify(e,null,2))}function xe(e){try{return process.kill(e,0),!0}catch{return!1}}function Vo(){if(process.env.TMUX)return"tmux";try{if((0,L.execSync)(`osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"'`,{stdio:"pipe",timeout:3e3}),I.default.existsSync("/Applications/iTerm.app"))return"iterm2"}catch{}return process.platform==="darwin"?"terminal":"direct"}function Ke(e){let t=R.default.join(V,"claude-launcher.exp"),o=R.default.join(V,"pending-input.txt");I.default.writeFileSync(o,"","utf-8");let s=`#!/usr/bin/expect -f
|
|
3
|
+
set timeout -1
|
|
4
|
+
set msgfile "${o}"
|
|
5
|
+
|
|
6
|
+
proc check_and_send_messages {} {
|
|
7
|
+
global msgfile
|
|
8
|
+
if {![file exists $msgfile]} return
|
|
9
|
+
set fsize [file size $msgfile]
|
|
10
|
+
if {$fsize == 0} return
|
|
11
|
+
|
|
12
|
+
# Read the message
|
|
13
|
+
set f [open $msgfile r]
|
|
14
|
+
set content [read $f]
|
|
15
|
+
close $f
|
|
16
|
+
|
|
17
|
+
# Clear the file immediately
|
|
18
|
+
set f [open $msgfile w]
|
|
19
|
+
close $f
|
|
20
|
+
|
|
21
|
+
# Trim whitespace
|
|
22
|
+
set content [string trim $content]
|
|
23
|
+
if {$content eq ""} return
|
|
24
|
+
|
|
25
|
+
# Type the message into Claude Code and press Enter
|
|
26
|
+
send -- "$content\\r"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
spawn ${e}
|
|
30
|
+
|
|
31
|
+
# Wait for Claude Code to render, then accept trust prompt
|
|
32
|
+
sleep 3
|
|
33
|
+
send "\\r"
|
|
34
|
+
|
|
35
|
+
# Interactive mode with periodic message file polling (every 2 seconds)
|
|
36
|
+
interact timeout 2 {
|
|
37
|
+
check_and_send_messages
|
|
38
|
+
}
|
|
39
|
+
`;return I.default.mkdirSync(V,{recursive:!0}),I.default.writeFileSync(t,s,{mode:493}),t}function Xe(e){return Object.entries(e).map(([t,o])=>`export ${t}="${o}"`).join("; ")}function Wo(e,t,o){let s=Ke(e),n=Xe(o),r=`
|
|
40
|
+
tell application "iTerm"
|
|
41
|
+
activate
|
|
42
|
+
set newWindow to (create window with default profile)
|
|
43
|
+
tell current session of newWindow
|
|
44
|
+
write text "cd ${Ae(t)} && ${n} && ${Ae(s)}"
|
|
45
|
+
end tell
|
|
46
|
+
end tell
|
|
47
|
+
`;(0,L.execSync)(`osascript -e '${r.replace(/'/g,`'"'"'`)}'`,{stdio:"pipe",timeout:1e4})}function qo(e,t,o){let s=Ke(e),n=Xe(o),a=`
|
|
48
|
+
tell application "Terminal"
|
|
49
|
+
activate
|
|
50
|
+
do script "${`cd ${Ae(t)} && ${n} && ${Ae(s)}`.replace(/"/g,'\\"')}"
|
|
51
|
+
end tell
|
|
52
|
+
`;(0,L.execSync)(`osascript -e '${a.replace(/'/g,`'"'"'`)}'`,{stdio:"pipe",timeout:1e4})}function Jo(e,t,o){let s=Ke(e),n=Xe(o),r=`badgerclaw-${o.BADGERCLAW_SESSION_ID||"default"}`,a=`cd ${t} && ${n} && ${s}`;(0,L.execSync)(`tmux new-session -d -s ${r} "${a}"`,{stdio:"pipe",timeout:1e4})}function Ae(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function Qe(e){let t=R.default.join(__dirname,"..","claude-code","mcp-server.js"),o=I.default.existsSync(t)?t:R.default.join(__dirname,"mcp-server.js"),s=(0,L.spawn)("node",[o],{env:{...process.env,BADGERCLAW_MCP_PORT:String(e.port),BADGERCLAW_SESSION_ID:e.sessionId},cwd:e.projectDir,stdio:"ignore",detached:!0});return s.unref(),s}function Ze(e){let t=R.default.join(R.default.dirname(process.argv[1]||""),"..","lib","node_modules","badgerclaw","dist","claude-code","mcp-server.js"),o=R.default.join(__dirname,"..","claude-code","mcp-server.js"),s=R.default.join(__dirname,"mcp-server.js"),n=I.default.existsSync(t)?t:I.default.existsSync(o)?o:s,r={BADGERCLAW_MCP_PORT:String(e.port),BADGERCLAW_SESSION_ID:e.sessionId},a=JSON.stringify({mcpServers:{badgerclaw:{command:"node",args:[n],env:r}}}),c=R.default.join(V,"mcp-config.json");I.default.mkdirSync(V,{recursive:!0}),I.default.writeFileSync(c,a,"utf-8");let l=`claude --dangerously-skip-permissions --mcp-config ${c}`;switch(Vo()){case"iterm2":Wo(l,e.projectDir,r);break;case"terminal":qo(l,e.projectDir,r);break;case"tmux":Jo(l,e.projectDir,r);break;case"direct":(0,L.spawn)(l,[],{shell:!0,cwd:e.projectDir,env:{...process.env,...r},stdio:"ignore",detached:!0}).unref();break}}function et(e,t,o,s,n){let r=ze().filter(a=>a.sessionId!==e);r.push({sessionId:e,pid:t,mcpPid:o,port:s,projectDir:n,startedAt:new Date().toISOString()}),Ye(r)}function tt(e){let t=ze(),o=t.find(n=>n.sessionId===e);if(!o)return!1;if(o.mcpPid&&xe(o.mcpPid))try{process.kill(o.mcpPid,"SIGTERM")}catch{}if(o.pid&&xe(o.pid))try{process.kill(o.pid,"SIGTERM")}catch{}try{(0,L.execSync)(`tmux kill-session -t badgerclaw-${e} 2>/dev/null`,{stdio:"pipe",timeout:3e3})}catch{}let s=t.filter(n=>n.sessionId!==e);return Ye(s),!0}function ot(){let e=ze(),t=e.filter(o=>xe(o.mcpPid)||xe(o.pid));return t.length!==e.length&&Ye(t),t}var L,I,R,Wt,V,Je,Pe=bo(()=>{"use strict";L=require("child_process"),I=i(require("fs")),R=i(require("path")),Wt=i(require("os")),V=R.default.join(Wt.default.homedir(),".badgerclaw"),Je=R.default.join(V,"claude-sessions.json")});var uo=require("commander");var Tt=require("commander"),H=i(require("chalk")),Nt=i(require("ora")),Ot=i(require("open")),z=i(require("os")),jt=i(require("crypto"));var Te=i(require("crypto"));function dt(){return Te.default.randomBytes(32).toString("base64url")}function mt(e){return Te.default.createHash("sha256").update(e).digest("base64url")}var Z=i(require("fs")),Ne=i(require("path")),ut=i(require("os")),gt=Ne.default.join(ut.default.homedir(),".badgerclaw"),Oe=Ne.default.join(gt,"auth.json");function f(){try{let e=Z.default.readFileSync(Oe,"utf-8");return JSON.parse(e)}catch{return null}}function pe(e){Z.default.mkdirSync(gt,{recursive:!0}),Z.default.writeFileSync(Oe,JSON.stringify(e,null,2),{mode:384})}function pt(){try{Z.default.unlinkSync(Oe)}catch{}}function fe(){let e=f();return e?new Date(e.expires_at)>new Date:!1}function he(e){let t=e.match(/^@?([^:]+)/);return t?t[1]:e}var ee=i(require("axios"));var ft=process.env.BADGERCLAW_ENV==="local",j=process.env.BADGERCLAW_API_URL??(ft?"http://localhost:8000":"https://api.badger.signout.io"),ht=process.env.BADGERCLAW_AUTH_URL??(ft?"http://localhost:5500":"https://badgerclaw.ai");var we=j,ye=null;async function je(){let e=f();if(!e?.refresh_token||!e?.email)return null;try{let t=await ee.default.post(`${we}/api/v1/user/token/refresh`,{refresh_token:e.refresh_token,email:e.email}),o=t.data?.result??t.data,s=o?.access_token;if(!s)return null;let n=o.expires_in||3600,r=new Date(Date.now()+n*1e3).toISOString();return pe({...e,access_token:s,expires_at:r}),s}catch{return null}}function Be(e){return e.interceptors.response.use(t=>(t.data&&typeof t.data=="object"&&"result"in t.data&&"errors"in t.data&&(t.data=t.data.result),t),t=>{let o=t.response?.data?.errors;return o?.length&&(t.message=o[0]),Promise.reject(t)}),e}function yt(e){return e.interceptors.response.use(void 0,async t=>{let o=t.config;if(t.response?.status!==401||o._retried)return Promise.reject(t);o._retried=!0,ye||(ye=je().finally(()=>{ye=null}));let s=await ye;return s?(o.headers.Authorization=`Bearer ${s}`,e.request(o)):Promise.reject(t)}),e}function v(){let e=f(),t={"Content-Type":"application/json"};e&&(t.Authorization=`Bearer ${e.access_token}`);let o=ee.default.create({baseURL:we,headers:t});return Be(o),yt(o),o}function wt(){return Be(ee.default.create({baseURL:we,headers:{"Content-Type":"application/json"}}))}function be(e){let t=ee.default.create({baseURL:we,headers:{"Content-Type":"application/json",Authorization:`Bearer ${e}`}});return Be(t),yt(t),t}var bt=require("commander"),T=i(require("chalk")),Le=i(require("ora")),_t=i(require("os")),$t=i(require("path"));var En=$t.default.join(_t.default.homedir(),".openclaw","openclaw.json");async function te(e,t,o,s){let n=f();if(!n)return;let r=s?null:(0,Le.default)(`Pairing bot: ${t}...`).start();try{let a=await fetch(`${j}/api/v1/pairing/redeem`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({code:e})});if(!a.ok){let D=await a.json().catch(()=>({errors:[a.statusText]}));r?.fail(T.default.red(`Failed to redeem ${t}: ${D.errors?.[0]||a.status}`));return}let c=await a.json(),l=c.result??c,{execSync:m}=await import("child_process"),w=`channels.badgerclaw.accounts.${l.user_id.split(":")[0].replace("@","").replace(/_bot$/,"")}`;m(`openclaw config set ${w}.userId "${l.user_id}"`,{encoding:"utf-8",timeout:1e4,stdio:"pipe"}),m(`openclaw config set ${w}.accessToken "${l.access_token}"`,{encoding:"utf-8",timeout:1e4,stdio:"pipe"}),m(`openclaw config set ${w}.homeserver "${l.homeserver}"`,{encoding:"utf-8",timeout:1e4,stdio:"pipe"}),m(`openclaw config set ${w}.encryption true`,{encoding:"utf-8",timeout:1e4,stdio:"pipe"}),l.device_id&&m(`openclaw config set ${w}.deviceId "${l.device_id}"`,{encoding:"utf-8",timeout:1e4,stdio:"pipe"}),await fetch(`${j}/api/v1/openclaw/pending-pairs/${e}/claim`,{method:"POST",headers:{Authorization:`Bearer ${n.access_token}`}}).catch(()=>{}),r?.succeed(T.default.green(`\u2705 ${l.bot_name} (${l.user_id}) paired \u2014 bot is live!`))}catch(a){r?.fail(T.default.red(`Failed to pair ${t}: ${a.message}`))}}async function oe(e=!1){let t=f();if(!t)return 0;let o=be(t.access_token);try{let{data:s}=await o.get("/api/v1/openclaw/pending-pairs");if(!s||s.length===0)return 0;let n=new Map;for(let a of s)n.set(a.bot_user_id,a);for(let a of s)n.get(a.bot_user_id)!==a&&await o.post(`/api/v1/openclaw/pending-pairs/${a.pair_code}/claim`).catch(()=>{});let r=0;for(let a of n.values()){let c=e?null:(0,Le.default)(`Pairing bot: ${a.bot_name} (${a.bot_user_id})`).start();try{await te(a.pair_code,a.bot_name,a.bot_user_id,e),await o.post(`/api/v1/openclaw/pending-pairs/${a.pair_code}/claim`).catch(()=>{}),r++}catch(l){c?.fail(T.default.red(`Error pairing ${a.bot_name}: ${l}`))}}if(r>0)try{let{execSync:a}=await import("child_process");a("openclaw gateway restart",{stdio:"ignore"}),e||console.log(T.default.green(`
|
|
53
|
+
\u26A1 ${r} bot(s) paired \u2014 gateway restarted, bots are live!`))}catch{e||console.log(T.default.yellow(`
|
|
54
|
+
\u26A1 ${r} bot(s) paired. Run: openclaw gateway restart`))}return r}catch{return 0}}var St=new bt.Command("autopair").description("Check for pending bot pairs and connect them to OpenClaw automatically").action(async()=>{if(!f()){console.log(T.default.yellow("Not logged in. Run `badgerclaw login` first."));return}console.log(T.default.dim("Checking for pending bot pairs...")),await oe(!1)===0&&console.log(T.default.dim("No pending pairs found."))});var E=i(require("chalk")),ne=i(require("fs")),se=i(require("path")),Ue=i(require("os")),Me=se.default.join(Ue.default.homedir(),".badgerclaw","update-check.json"),So=1440*60*1e3,Co="https://registry.npmjs.org/badgerclaw/latest",vo="https://registry.npmjs.org/@badgerclaw%2Fconnect/latest";function Io(){try{return JSON.parse(ne.default.readFileSync(Me,"utf-8"))}catch{return null}}function ko(e){try{ne.default.mkdirSync(se.default.dirname(Me),{recursive:!0}),ne.default.writeFileSync(Me,JSON.stringify(e))}catch{}}function _e(e,t){let o=m=>m.replace(/[^0-9.]/g,"").split(".").map(Number),[s,n,r]=o(e),[a,c,l]=o(t);return a!==s?a>s:c!==n?c>n:l>r}function Ct(){try{let e=se.default.join(Ue.default.homedir(),".openclaw"),t=se.default.join(e,"node_modules","@badgerclaw","connect","package.json");return JSON.parse(ne.default.readFileSync(t,"utf-8")).version||null}catch{return null}}async function $e(e,t=!1){try{let o=Io(),s=Date.now();if(o&&s-o.lastCheck<So){if(_e(e,o.latestVersion)&&vt(e,o.latestVersion),o.latestPluginVersion){let h=Ct();h&&_e(h,o.latestPluginVersion)&&It(h,o.latestPluginVersion)}return}let n=new AbortController,r=setTimeout(()=>n.abort(),3e3),[a,c]=await Promise.allSettled([fetch(Co,{signal:n.signal}),fetch(vo,{signal:n.signal})]);clearTimeout(r);let l=e,m;if(a.status==="fulfilled"&&a.value.ok&&(l=(await a.value.json()).version),c.status==="fulfilled"&&c.value.ok&&(m=(await c.value.json()).version),ko({lastCheck:s,latestVersion:l,latestPluginVersion:m}),_e(e,l)&&vt(e,l),m){let h=Ct();h&&_e(h,m)&&It(h,m)}}catch{}}function vt(e,t){console.log(E.default.yellow(`
|
|
55
|
+
\u26A0\uFE0F Update available: badgerclaw ${E.default.dim(e)} \u2192 ${E.default.green(t)}`)+E.default.dim(`
|
|
56
|
+
Run: `)+E.default.cyan("npm install -g badgerclaw")+`
|
|
57
|
+
`)}function It(e,t){console.log(E.default.yellow(`
|
|
58
|
+
\u26A0\uFE0F Plugin update available: @badgerclaw/connect ${E.default.dim(e)} \u2192 ${E.default.green(t)}`)+E.default.dim(`
|
|
59
|
+
Run: `)+E.default.cyan("badgerclaw setup")+`
|
|
60
|
+
`)}var Et=i(require("os"));var Se=i(require("fs")),J=i(require("path")),re=i(require("os")),ae=require("child_process"),kt="ai.badgerclaw.watch",xt=J.default.join(re.default.homedir(),"Library","LaunchAgents"),Fe=J.default.join(xt,`${kt}.plist`);function xo(){try{return(0,ae.execSync)("which badgerclaw",{encoding:"utf-8"}).trim()}catch{return"/opt/homebrew/bin/badgerclaw"}}function Ao(){try{return(0,ae.execSync)("which node",{encoding:"utf-8"}).trim()}catch{return"/opt/homebrew/bin/node"}}function Po(e){let t=J.default.join(re.default.homedir(),".badgerclaw"),o=Ao(),s=J.default.dirname(o);return`<?xml version="1.0" encoding="UTF-8"?>
|
|
61
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
62
|
+
<plist version="1.0">
|
|
63
|
+
<dict>
|
|
64
|
+
<key>Label</key>
|
|
65
|
+
<string>${kt}</string>
|
|
66
|
+
<key>ProgramArguments</key>
|
|
67
|
+
<array>
|
|
68
|
+
<string>${o}</string>
|
|
69
|
+
<string>${e}</string>
|
|
70
|
+
<string>heartbeat</string>
|
|
71
|
+
</array>
|
|
72
|
+
<key>EnvironmentVariables</key>
|
|
73
|
+
<dict>
|
|
74
|
+
<key>PATH</key>
|
|
75
|
+
<string>${s}:/usr/local/bin:/usr/bin:/bin</string>
|
|
76
|
+
</dict>
|
|
77
|
+
<key>RunAtLoad</key>
|
|
78
|
+
<true/>
|
|
79
|
+
<key>KeepAlive</key>
|
|
80
|
+
<true/>
|
|
81
|
+
<key>StandardOutPath</key>
|
|
82
|
+
<string>${t}/heartbeat.log</string>
|
|
83
|
+
<key>StandardErrorPath</key>
|
|
84
|
+
<string>${t}/heartbeat.log</string>
|
|
85
|
+
<key>ThrottleInterval</key>
|
|
86
|
+
<integer>30</integer>
|
|
87
|
+
</dict>
|
|
88
|
+
</plist>`}function At(){if(re.default.platform()==="darwin")try{let e=xo();Se.default.mkdirSync(xt,{recursive:!0}),Se.default.mkdirSync(J.default.join(re.default.homedir(),".badgerclaw"),{recursive:!0}),Se.default.writeFileSync(Fe,Po(e));try{(0,ae.execSync)(`launchctl unload "${Fe}" 2>/dev/null`)}catch{}(0,ae.execSync)(`launchctl load "${Fe}"`),console.log("\x1B[2mPair-watcher registered as background service (auto-starts on login).\x1B[0m")}catch{console.log("\x1B[2mNote: could not register background service. Run `badgerclaw watch` manually.\x1B[0m")}}var Ce=i(require("fs")),ie=i(require("path")),ve=i(require("os")),B=require("child_process"),Ge="badgerclaw-watch.service",Pt=ie.default.join(ve.default.homedir(),".config","systemd","user"),Ro=ie.default.join(Pt,Ge);function Eo(){try{return(0,B.execSync)("which badgerclaw",{encoding:"utf-8"}).trim()}catch{return"/usr/local/bin/badgerclaw"}}function Do(){try{return(0,B.execSync)("which node",{encoding:"utf-8"}).trim()}catch{return"/usr/bin/node"}}function To(e){let t=Do(),o=ie.default.dirname(t);return`[Unit]
|
|
89
|
+
Description=BadgerClaw background watcher (heartbeat + pair events)
|
|
90
|
+
After=network-online.target
|
|
91
|
+
Wants=network-online.target
|
|
92
|
+
|
|
93
|
+
[Service]
|
|
94
|
+
Type=simple
|
|
95
|
+
ExecStart=${t} ${e} heartbeat
|
|
96
|
+
Restart=always
|
|
97
|
+
RestartSec=30
|
|
98
|
+
Environment=PATH=${o}:/usr/local/bin:/usr/bin:/bin
|
|
99
|
+
StandardOutput=append:%h/.badgerclaw/heartbeat.log
|
|
100
|
+
StandardError=append:%h/.badgerclaw/heartbeat.log
|
|
101
|
+
|
|
102
|
+
[Install]
|
|
103
|
+
WantedBy=default.target
|
|
104
|
+
`}function No(){try{return(0,B.execSync)("systemctl --user --version",{stdio:"ignore"}),!0}catch{return!1}}function Rt(){if(ve.default.platform()==="linux"){if(!No()){console.log("\x1B[2mNote: systemctl --user not available. Run `badgerclaw heartbeat` manually or start it under a process manager.\x1B[0m");return}try{let e=Eo();Ce.default.mkdirSync(Pt,{recursive:!0}),Ce.default.mkdirSync(ie.default.join(ve.default.homedir(),".badgerclaw"),{recursive:!0}),Ce.default.writeFileSync(Ro,To(e)),(0,B.execSync)("systemctl --user daemon-reload",{stdio:"ignore"});try{(0,B.execSync)(`systemctl --user restart ${Ge}`,{stdio:"ignore"})}catch{}(0,B.execSync)(`systemctl --user enable --now ${Ge}`,{stdio:"ignore"});try{let t=(0,B.execSync)("whoami",{encoding:"utf-8"}).trim();(0,B.execSync)(`loginctl enable-linger ${t}`,{stdio:"ignore"})}catch{}console.log("\x1B[2mHeartbeat daemon registered as systemd user service (auto-starts on login).\x1B[0m")}catch{console.log("\x1B[2mNote: could not register systemd user service. Run `badgerclaw heartbeat` manually.\x1B[0m")}}}function Dt(){let e=Et.default.platform();if(e==="darwin")return At();if(e==="linux")return Rt();console.log("\x1B[2mNote: background daemon install not supported on "+e+". Run `badgerclaw heartbeat` manually.\x1B[0m")}var jo=2e3,Bo=12e4,Bt=new Tt.Command("login").description("Log in to BadgerClaw via browser").action(async()=>{let{version:e}=U();$e(e);let t=dt(),o=mt(t),s=`${ht}/cli-auth?code=${o}`;console.log(H.default.yellow("Opening browser for authentication...")),console.log(H.default.dim(`If the browser doesn't open, visit: ${s}`)),await(0,Ot.default)(s);let n=(0,Nt.default)("Waiting for authentication...").start(),r=wt(),a=Date.now();for(;Date.now()-a<Bo;){try{let c=await r.post(`/api/v1/openclaw/cli/auth/poll/${o}`,{code_verifier:t,code_challenge:o});if(c.status===429){await new Promise(l=>setTimeout(l,5e3));continue}if(c.data?.access_token){let{access_token:l,user_id:m,expires_at:h,refresh_token:w,email:D}=c.data,$=jt.default.createHash("sha256").update(`${z.default.hostname()}-${z.default.platform()}-${z.default.arch()}`).digest("hex").slice(0,16),C=`openclaw-${z.default.hostname().toLowerCase().replace(/[^a-z0-9]/g,"-")}-${$}`;pe({access_token:l,user_id:m,instance_id:C,expires_at:h,refresh_token:w,email:D});try{let{version:go}=U();await r.post("/api/v1/openclaw/register",{instance_id:C,label:z.default.hostname(),version:go},{headers:{Authorization:`Bearer ${l}`}})}catch{}n.succeed(H.default.green(`Logged in as ${he(m)}`));let it=await oe(!0);it>0&&(console.log(H.default.green(`\u2705 ${it} bot(s) automatically paired to OpenClaw.`)),console.log(H.default.yellow("Run: openclaw gateway restart to activate."))),Dt();return}}catch{}await new Promise(c=>setTimeout(c,jo))}n.fail(H.default.red("Authentication timed out. Please try again.")),process.exit(1)});var Lt=require("commander"),He=i(require("chalk"));var Mt=new Lt.Command("logout").description("Disconnect this machine and log out of BadgerClaw").action(async()=>{let e=f();if(!e){console.log(He.default.yellow("Not logged in."));return}try{let t=be(e.access_token),{version:o}=U();await t.post("/api/v1/openclaw/register",{instance_id:e.instance_id,label:require("os").hostname(),version:o,online:!1})}catch{}pt(),console.log(He.default.green("Logged out \u2014 this machine is now disconnected."))});var Ut=require("commander"),Ve=i(require("chalk"));var Ft=new Ut.Command("status").description("Show connected instance info").action(async()=>{let{version:e}=U();await $e(e);let t=f();(!t||!fe())&&(console.log(Ve.default.red("Not logged in. Run `badgerclaw login` to authenticate.")),process.exit(1)),console.log(Ve.default.green("Authenticated")),console.log(` User: ${he(t.user_id)}`),console.log(` Instance: ${t.instance_id}`),console.log(` Expires: ${new Date(t.expires_at).toLocaleDateString()}`)});var ce=require("commander"),x=i(require("chalk")),Ie=i(require("ora"));function We(){fe()||(console.log(x.default.red("Not logged in. Run `badgerclaw login` to authenticate.")),process.exit(1))}function Lo(e){return/^[a-z0-9_]{4,20}$/.test(e)}function Mo(e){return e.replace(/_bot$/,"")}var Uo=new ce.Command("create").description("Create a new bot").argument("<name>","Bot name (4-20 chars, lowercase alphanumeric + underscores)").action(async e=>{We(),Lo(e)||(console.log(x.default.red("Invalid bot name. Must be 4-20 characters, lowercase alphanumeric and underscores only.")),process.exit(1));let t=(0,Ie.default)(`Creating bot "${e}"...`).start();try{await v().post("/api/v1/openclaw/bots",{username:e}),t.succeed(x.default.green(`Bot "${e}" created successfully!`))}catch(o){let s=o.response?.data?.errors?.[0]||o.message;t.fail(x.default.red(`Failed to create bot: ${s}`)),process.exit(1)}}),Fo=new ce.Command("list").description("List your bots").action(async()=>{We();let e=(0,Ie.default)("Fetching bots...").start();try{let s=(await v().get("/api/v1/openclaw/bots")).data?.bots||[];if(e.stop(),s.length===0){console.log(x.default.yellow("No bots found. Create one with `badgerclaw bot create <name>`."));return}console.log(x.default.green(`Your bots (${s.length}):
|
|
105
|
+
`));for(let n of s){let r=Mo(n.username||n.name),a=n.active!==!1?x.default.green("active"):x.default.dim("inactive");console.log(` ${x.default.bold(r)} ${a}`)}}catch(t){let o=t.response?.data?.errors?.[0]||t.message;e.fail(x.default.red(`Failed to list bots: ${o}`)),process.exit(1)}}),Go=new ce.Command("delete").description("Deactivate a bot").argument("<name>","Bot name to deactivate").action(async e=>{We();let t=(0,Ie.default)(`Deactivating bot "${e}"...`).start();try{await v().delete(`/api/v1/openclaw/bots/${e}`),t.succeed(x.default.green(`Bot "${e}" deactivated.`))}catch(o){let s=o.response?.data?.errors?.[0]||o.message;t.fail(x.default.red(`Failed to deactivate bot: ${s}`)),process.exit(1)}}),Gt=new ce.Command("bot").description("Manage bots").addCommand(Uo).addCommand(Fo).addCommand(Go);var Zt=require("commander"),g=i(require("chalk"));var qe=i(require("axios")),Ho=7331,Ht=`http://localhost:${Ho}`;async function le(){try{return(await qe.default.get(`${Ht}/health`,{timeout:5e3})).data}catch{return{status:"stopped",pid:null,lastRestart:null,pluginVersion:"unknown",bots:[]}}}async function Y(){try{return{success:!0,message:(await qe.default.post(`${Ht}/restart`,{},{timeout:15e3})).data?.message||"Gateway restart initiated"}}catch(e){try{let{execSync:t}=await import("child_process");return t("openclaw gateway restart",{stdio:"pipe",timeout:15e3}),{success:!0,message:"Gateway restarted via CLI fallback"}}catch{return{success:!1,message:e.message||"Failed to restart gateway"}}}}var zt=require("commander"),d=i(require("chalk"));var N=i(require("os")),Vt=i(require("crypto"));function de(){let e=f();if(e?.instance_id)return e.instance_id;let t=Vt.default.createHash("sha256").update(`${N.default.hostname()}-${N.default.platform()}-${N.default.arch()}`).digest("hex").slice(0,16);return`openclaw-${N.default.hostname().toLowerCase().replace(/[^a-z0-9]/g,"-")}-${t}`}function ke(){return{hostname:N.default.hostname(),os:N.default.platform(),arch:N.default.arch(),uptimeSeconds:Math.floor(N.default.uptime()),memFreeMb:Math.floor(N.default.freemem()/1024/1024)}}var S=require("child_process"),u=i(require("fs")),b=i(require("path")),me=i(require("os")),K=i(require("axios"));async function M(e){return e.command_type==="update_cli"?Yo(e.payload?.target_version):e.command_type==="update_plugin"?Ko(e.payload?.target_version):e.command_type==="restart_gateway"?zo():e.command_type==="leave_room"?nn(e.payload):e.command_type==="start_claude_code"?tn(e.payload):e.command_type==="stop_claude_code"?on(e.payload):{success:!1,message:`Unknown command: ${e.command_type}`}}function zo(){try{return(0,S.execSync)("openclaw gateway restart",{stdio:"pipe",timeout:15e3}),{success:!0,message:"Gateway restarted"}}catch{try{return{success:!0,message:`Gateway restarted via probe: ${(0,S.execSync)("curl -s -X POST http://localhost:7331/restart",{stdio:"pipe",timeout:1e4}).toString()}`}}catch(e){return{success:!1,message:`Gateway restart failed: ${e.message||"unknown error"}`}}}}function Yo(e){let t=e?`badgerclaw@${e}`:"badgerclaw@latest";try{(0,S.execSync)(`npm install -g ${t}`,{stdio:"pipe",timeout:12e4});let o=(0,S.execSync)("npm list -g badgerclaw --json",{stdio:"pipe"}).toString(),n=JSON.parse(o)?.dependencies?.badgerclaw?.version||"unknown";return Qo(),{success:!0,message:`Updated CLI to ${n}`,newVersion:n}}catch(o){let s=o.stderr?.toString()||o.message||"Unknown error";return s.includes("EACCES")||s.includes("permission")?{success:!1,message:"Permission denied. Run: sudo npm install -g badgerclaw"}:{success:!1,message:`CLI update failed: ${s.slice(0,500)}`}}}function Ko(e){let t=b.default.join(me.default.homedir(),".openclaw","extensions","badgerclaw");if(!u.default.existsSync(t))return{success:!1,message:"Cannot find plugin directory (~/.openclaw/extensions/badgerclaw). Re-run: badgerclaw setup"};let o=e?`@badgerclaw/connect@${e}`:"@badgerclaw/connect@latest";try{(0,S.execSync)(`npm install ${o}`,{cwd:t,stdio:"pipe",timeout:12e4});let s=b.default.join(t,"node_modules","@badgerclaw","connect"),n=b.default.join(s,"package.json"),r="unknown";u.default.existsSync(n)&&(r=JSON.parse(u.default.readFileSync(n,"utf-8")).version||"unknown"),Xo(s,t);let a=b.default.join(t,"package.json");if(u.default.existsSync(a)&&u.default.existsSync(n))try{let c=JSON.parse(u.default.readFileSync(a,"utf-8")),l=JSON.parse(u.default.readFileSync(n,"utf-8"));c.version=r,l.openclaw&&(c.openclaw={...c.openclaw,...l.openclaw}),u.default.writeFileSync(a,JSON.stringify(c,null,2)+`
|
|
106
|
+
`)}catch{}try{(0,S.execSync)("openclaw gateway restart",{stdio:"pipe",timeout:1e4})}catch{}return{success:!0,message:`Updated plugin to ${r}, gateway restarted`,newVersion:r}}catch(s){return{success:!1,message:`Plugin update failed: ${(s.stderr?.toString()||s.message||"").slice(0,500)}`}}}function Xo(e,t){let o=b.default.join(e,"dist"),s=b.default.join(t,"dist");u.default.existsSync(o)&&(u.default.mkdirSync(s,{recursive:!0}),st(o,s));let n=b.default.join(e,"openclaw.plugin.json");u.default.existsSync(n)&&u.default.copyFileSync(n,b.default.join(t,"openclaw.plugin.json"));let r=b.default.join(e,"scripts"),a=b.default.join(t,"scripts");u.default.existsSync(r)&&(u.default.mkdirSync(a,{recursive:!0}),st(r,a));for(let c of["index.ts","src","STREAMING.md","SETUP.md","CHANGELOG.md"]){let l=b.default.join(t,c);u.default.existsSync(l)&&(u.default.statSync(l).isDirectory()?u.default.rmSync(l,{recursive:!0,force:!0}):u.default.unlinkSync(l))}}function st(e,t){for(let o of u.default.readdirSync(e,{withFileTypes:!0})){let s=b.default.join(e,o.name),n=b.default.join(t,o.name);o.isDirectory()?(u.default.mkdirSync(n,{recursive:!0}),st(s,n)):u.default.copyFileSync(s,n)}}function Qo(){try{try{(0,S.execSync)('pkill -f "badgerclaw heartbeat"',{stdio:"pipe",timeout:5e3})}catch{}try{(0,S.execSync)('pkill -f "badgerclaw watch"',{stdio:"pipe",timeout:5e3})}catch{}(0,S.execSync)("sleep 1",{stdio:"pipe"});let e=(0,S.execSync)("which badgerclaw",{stdio:"pipe"}).toString().trim();(0,S.execSync)(`nohup ${e} heartbeat > /dev/null 2>&1 &`,{stdio:"pipe",shell:"/bin/zsh"}),(0,S.execSync)(`nohup ${e} watch > /dev/null 2>&1 &`,{stdio:"pipe",shell:"/bin/zsh"})}catch{}}function Zo(){let e=b.default.join(me.default.homedir(),".badgerclaw","workspace");u.default.mkdirSync(e,{recursive:!0});let t=b.default.join(e,".git");return u.default.existsSync(t)||(0,S.execSync)("git init",{cwd:e,stdio:"pipe",timeout:5e3}),u.default.writeFileSync(b.default.join(e,"CLAUDE.md"),`You are a chat assistant connected to Matrix rooms via BadgerClaw.
|
|
107
|
+
|
|
108
|
+
When you receive a message, it will be formatted like:
|
|
109
|
+
[Room: RoomName] Sender: message text
|
|
110
|
+
Reply using the reply tool with botId="..." and roomId="..."
|
|
111
|
+
|
|
112
|
+
ALWAYS use the \`reply\` MCP tool to send your response back to the chat room.
|
|
113
|
+
Use the exact botId and roomId provided in the message \u2014 do not modify them.
|
|
114
|
+
Keep responses concise and helpful.
|
|
115
|
+
`),e}var Re=b.default.join(me.default.homedir(),".badgerclaw","claude-code.lock");function en(){try{u.default.mkdirSync(b.default.dirname(Re),{recursive:!0});try{let e=u.default.statSync(Re);if(Date.now()-e.mtimeMs<6e4)return!1;u.default.unlinkSync(Re)}catch{}return u.default.writeFileSync(Re,String(process.pid),{flag:"wx"}),!0}catch{return!1}}async function tn(e){if(!e?.bot_id)return{success:!1,message:"Missing bot_id in payload"};if(!en())return{success:!0,message:"Claude Code start already in progress, skipping duplicate"};try{let t=e.bot_user_id?.startsWith("@")?e.bot_user_id:await qt(e.bot_id),o=7332,{launchClaudeCode:s,recordSession:n,getActiveSessions:r,stopSession:a}=(Pe(),lt(nt)),c=r();for(let h of c)console.log(`[command-executor] Stopping existing session ${h.sessionId}`),a(h.sessionId);try{(0,S.execSync)(`lsof -ti :${o} | xargs kill -9 2>/dev/null`,{stdio:"pipe",timeout:3e3})}catch{}let l=e.session_id||`session-${Date.now()}`;await K.default.post("http://localhost:7331/claude-code/toggle",{botId:t,enabled:!0,sessionId:l},{timeout:1e4});let m=Zo();return s({sessionId:l,port:o,projectDir:m}),n(l,0,0,o,m),{success:!0,message:`Claude Code started globally for bot ${t} (session: ${l}, port: ${o})`}}catch(t){return{success:!1,message:`Failed to start Claude Code: ${t.response?.data?.message||t.message||"Unknown error"}`}}}async function qt(e){if(e.startsWith("@"))return e;try{let o=(await K.default.get("http://localhost:7331/health",{timeout:1e4})).data?.bots||[],s=b.default.join(me.default.homedir(),".badgerclaw","bot-mapping.json");if(u.default.existsSync(s)){let n=JSON.parse(u.default.readFileSync(s,"utf-8"));if(n[e])return n[e]}return o.length===1?o[0].botId:(console.warn(`[command-executor] Could not resolve bot UUID ${e} to Matrix ID, trying as-is`),e)}catch{return e}}async function on(e){if(!e?.bot_id)return{success:!1,message:"Missing bot_id in payload"};try{let t=e.bot_user_id?.startsWith("@")?e.bot_user_id:await qt(e.bot_id),o=e.session_id;if(!o)try{let s=await K.default.get("http://localhost:7331/claude-code/status",{timeout:5e3}),r=(s.data?.bots||s.data?.rooms||[]).find(a=>a.botId===t);r&&(o=r.sessionId)}catch{}if(await K.default.post("http://localhost:7331/claude-code/toggle",{botId:t,enabled:!1},{timeout:1e4}),o){let{stopSession:s}=(Pe(),lt(nt));s(o)}return{success:!0,message:"Claude Code stopped"}}catch(t){return{success:!1,message:`Failed to stop Claude Code: ${t.response?.data?.message||t.message||"Unknown error"}`}}}async function nn(e){if(!e?.bot_id||!e?.room_id)return{success:!1,message:"Missing bot_id or room_id in payload"};try{return{success:!0,message:(await K.default.post("http://localhost:7331/leave-room",{botId:e.bot_id,roomId:e.room_id},{timeout:15e3})).data?.message||"Left room"}}catch(t){return{success:!1,message:`Failed to leave room: ${t.response?.data?.message||t.message||"Unknown error"}`}}}var Jt=3e4,Yt="/api/v1/dashboard/heartbeat",Ee=new Map,sn=3e4;function De(e,t){if(e!=="start_claude_code"&&e!=="stop_claude_code")return!1;let o=`${e}:${t||"unknown"}`,s=Date.now();for(let[n,r]of Ee)s-r>sn&&Ee.delete(n);return Ee.has(o)?!0:(Ee.set(o,s),!1)}function Kt(e,t){let o=ke();return{instance_id:de(),timestamp:new Date().toISOString(),cli_version:t,plugin_version:e.pluginVersion||"unknown",hostname:o.hostname,os:o.os,arch:o.arch,uptime_seconds:o.uptimeSeconds,mem_free_mb:o.memFreeMb,gateway_status:e.status,gateway_pid:e.pid,last_gateway_restart:e.lastRestart,bots:e.bots.map(s=>({bot_id:s.botId,bot_username:s.botUsername,status:s.status,activity_state:s.activityState||"idle",active_task:s.activeTask||null,last_activity_at:s.lastActivity,messages_received:s.messagesReceived,messages_sent:s.messagesSent,chunked_messages:s.chunkedMessages,total_chunks_sent:s.totalChunksSent,errors:s.errors,last_error:s.lastError,last_error_at:s.lastErrorAt,uptime_seconds:s.uptimeSeconds,rooms_active:s.roomsActive,room_details:(s.roomDetails||[]).map(n=>({roomId:n.roomId,roomName:n.roomName,workspaceName:n.workspaceName||null,messagesInRoom:n.messagesInRoom,lastActivityInRoom:n.lastActivityInRoom}))}))}}async function k(){let{version:e}=U(),t=await le(),o=Kt(t,e);await v().post(Yt,o)}function rn(e,t){if(!e||e.status!==t.status)return!0;let o=new Map(e.bots.map(s=>[s.botId,s]));for(let s of t.bots){let n=o.get(s.botId);if(!n||n.status!==s.status||n.errors!==s.errors)return!0}return e.bots.length!==t.bots.length}var Xt=new zt.Command("heartbeat").description("Run heartbeat daemon \u2014 reports telemetry to the BadgerClaw dashboard every 30s").action(async()=>{let e=f();e||(console.log(d.default.yellow("Not logged in. Run `badgerclaw login` first.")),process.exit(1));let{version:t}=U();console.log(d.default.green(`Heartbeat daemon started (v${t})`)),console.log(d.default.dim(` Instance: ${de()}`)),console.log(d.default.dim(` Interval: ${Jt/1e3}s`)),console.log(d.default.dim(` Press Ctrl+C to stop.
|
|
116
|
+
`));let o=null,s=async()=>{try{await Qt();let n=await le(),r=Kt(n,t);rn(o,n)&&o!==null&&console.log(d.default.cyan(` [${new Date().toISOString()}] State change detected \u2014 pushing immediately`));let c=v(),l=await c.post(Yt,r),m=n.bots.length,h=n.bots.filter($=>$.status==="running").length;console.log(d.default.dim(` [${new Date().toISOString()}] gateway=${n.status} bots=${h}/${m} running mem=${r.mem_free_mb}MB free`)),o=n;let w=l.data?.pending_commands||[],D=!1;for(let $ of w)try{if(console.log(d.default.cyan(` [${new Date().toISOString()}] Received command: ${$.command_type} (${$.id})`)),await c.post(`/api/v1/dashboard/commands/${$.id}/ack`),De($.command_type,$.payload?.bot_id)){console.log(d.default.dim(` [${new Date().toISOString()}] Skipping duplicate ${$.command_type}`));continue}let C=await M($);console.log(C.success?d.default.green(` [${new Date().toISOString()}] ${C.message}`):d.default.red(` [${new Date().toISOString()}] ${C.message}`)),await c.post(`/api/v1/dashboard/commands/${$.id}/result`,{status:C.success?"success":"failed",result:C.message,new_version:C.newVersion||null}),$.command_type==="update_cli"&&C.success&&(D=!0)}catch(C){console.log(d.default.dim(` [${new Date().toISOString()}] Command ${$.id} error: ${C.message}`))}if(D){console.log(d.default.cyan(` [${new Date().toISOString()}] CLI updated \u2014 restarting in 2s...`));try{await k()}catch{}setTimeout(()=>process.exit(0),2e3);return}}catch(n){console.log(d.default.dim(` [${new Date().toISOString()}] Heartbeat failed: ${n.message}`))}};await s(),setInterval(s,Jt),cn(e.access_token,t),await new Promise(()=>{})});async function Qt(){let e=f();if(!e?.expires_at)return;let t=new Date(e.expires_at).getTime(),o=Date.now(),s=300*1e3;if(t-o<s){console.log(d.default.dim(` [${y()}] Token expires soon \u2014 refreshing proactively...`));let n=await je();console.log(n?d.default.green(` [${y()}] Token refreshed successfully`):d.default.yellow(` [${y()}] Token refresh failed \u2014 no refresh_token available`))}}var an=2700*1e3;function cn(e,t){let o=3e3,s=3e4,n=null;async function r(){try{await Qt();let c=f()?.access_token||e,l=require("eventsource"),m=new l(`${j}/api/v1/openclaw/events`,{headers:{Authorization:`Bearer ${c}`}});m.onopen=()=>{o=3e3,console.log(d.default.dim(` [${y()}] SSE connected \u2014 listening for real-time events`)),n&&clearTimeout(n),n=setTimeout(()=>{console.log(d.default.dim(` [${y()}] SSE proactive reconnect \u2014 refreshing token before expiry`)),m.close(),r()},an)},m.onmessage=async h=>{try{let w=JSON.parse(h.data);await ln(w)}catch{}},m.onerror=async()=>{m.close(),n&&(clearTimeout(n),n=null),console.log(d.default.dim(` [${y()}] SSE disconnected \u2014 reconnecting in ${o/1e3}s...`)),setTimeout(r,o),o=Math.min(o*1.5,s)}}catch(a){console.log(d.default.dim(` [${y()}] SSE connection failed: ${a.message}`)),setTimeout(r,o),o=Math.min(o*1.5,s)}}r()}function y(){return new Date().toISOString()}async function ln(e){if(e.type==="pair"){console.log(d.default.cyan(` [${y()}] Pair event: ${e.bot_name}`)),await te(e.pair_code,e.bot_name,e.bot_user_id,!0);try{let{execSync:t}=await import("child_process");t("openclaw gateway restart",{stdio:"ignore"}),console.log(d.default.green(` [${y()}] Gateway restarted \u2014 ${e.bot_name} is live`))}catch{}try{await k()}catch{}}else if(e.type==="bot.delete"&&e.bot_user_id){let t=e.bot_user_id,o=t.split(":")[0].replace("@","").replace(/_bot$/,"");console.log(d.default.yellow(` [${y()}] Bot deleted: ${t}`));try{let s=await import("fs"),n=await import("path"),r=await import("os"),a=n.join(r.homedir(),".openclaw","openclaw.json"),c=JSON.parse(s.readFileSync(a,"utf-8")),l=!1;if(c.channels?.badgerclaw?.accounts?.[o]&&(delete c.channels.badgerclaw.accounts[o],l=!0),c.agents?.list){let m=c.agents.list.length;c.agents.list=c.agents.list.filter(h=>h.id!==o),c.agents.list.length!==m&&(l=!0)}if(l){s.writeFileSync(a,JSON.stringify(c,null,2)),console.log(d.default.green(` [${y()}] Removed "${o}" from openclaw.json`));let{execSync:m}=await import("child_process");try{m("openclaw gateway restart",{stdio:"ignore"}),console.log(d.default.green(` [${y()}] Gateway restarted`))}catch{}}}catch(s){console.log(d.default.red(` [${y()}] Failed to clean up bot: ${s}`))}try{await k()}catch{}}else if(e.type==="gateway-restart"){console.log(d.default.cyan(` [${y()}] Remote gateway-restart received`));let t=await Y();console.log(t.success?d.default.green(` [${y()}] Gateway restarted`):d.default.red(` [${y()}] Gateway restart failed: ${t.message}`));try{await k()}catch{}}else if(e.type==="claude_code.start"&&e.bot_id){if(console.log(d.default.cyan(` [${y()}] Claude Code START: bot=${e.bot_id}`)),De("start_claude_code",e.bot_id)){console.log(d.default.dim(` [${y()}] Skipping duplicate start_claude_code (already processed)`));return}try{let t=await M({id:e.command_id||`cc-start-${Date.now()}`,command_type:"start_claude_code",payload:{bot_id:e.bot_id,bot_user_id:e.bot_user_id,room_id:e.room_id,session_id:e.session_id}});e.command_id&&await v().post(`/api/v1/dashboard/commands/${e.command_id}/result`,{status:t.success?"success":"failed",result:t.message}).catch(()=>{})}catch{}try{await k()}catch{}}else if(e.type==="claude_code.stop"&&e.bot_id){if(console.log(d.default.cyan(` [${y()}] Claude Code STOP: bot=${e.bot_id}`)),De("stop_claude_code",e.bot_id)){console.log(d.default.dim(` [${y()}] Skipping duplicate stop_claude_code (already processed)`));return}try{let t=await M({id:e.command_id||`cc-stop-${Date.now()}`,command_type:"stop_claude_code",payload:{bot_id:e.bot_id,bot_user_id:e.bot_user_id,room_id:e.room_id,session_id:e.session_id}});e.command_id&&await v().post(`/api/v1/dashboard/commands/${e.command_id}/result`,{status:t.success?"success":"failed",result:t.message}).catch(()=>{})}catch{}try{await k()}catch{}}else if(e.type==="command.execute"&&e.command_id){if(console.log(d.default.cyan(` [${y()}] Command: ${e.command_type} (${e.command_id})`)),De(e.command_type,e.payload?.bot_id)){console.log(d.default.dim(` [${y()}] Skipping duplicate ${e.command_type} (already processed)`));return}try{let t=v();await t.post(`/api/v1/dashboard/commands/${e.command_id}/ack`);let o=await M({id:e.command_id,command_type:e.command_type,payload:e.payload});await t.post(`/api/v1/dashboard/commands/${e.command_id}/result`,{status:o.success?"success":"failed",result:o.message,new_version:o.newVersion||null}),e.command_type==="update_cli"&&o.success&&(console.log(d.default.cyan(` [${y()}] CLI updated \u2014 restarting in 2s...`)),setTimeout(()=>process.exit(0),2e3))}catch{}try{await k()}catch{}}}var eo=new Zt.Command("watch").description("Watch for bot pair events in real-time (no polling)").action(async()=>{let e=f();e||(console.log(g.default.yellow("Not logged in.")),process.exit(1)),await oe(!1),console.log(g.default.green("\u{1F534} Listening for pair events... (Ctrl+C to stop)"));let t=require("eventsource"),o=new t(`${j}/api/v1/openclaw/events`,{headers:{Authorization:`Bearer ${e.access_token}`}});o.onmessage=async s=>{try{let n=JSON.parse(s.data);if(n.type==="pair"){console.log(g.default.cyan(`
|
|
117
|
+
\u{1F4F1} Pair event received: ${n.bot_name}`)),await te(n.pair_code,n.bot_name,n.bot_user_id,!1);try{let{execSync:r}=await import("child_process");r("openclaw gateway restart",{stdio:"ignore"}),console.log(g.default.green(" \u2705 Gateway restarted \u2014 bot is live!"))}catch{console.log(g.default.dim(" Gateway restart failed \u2014 run: openclaw gateway restart"))}}else if(n.type==="gateway-restart"){console.log(g.default.cyan(`
|
|
118
|
+
\u{1F504} Remote gateway-restart command received`));let r=await Y();r.success?console.log(g.default.green(` \u2705 Gateway restarted: ${r.message}`)):console.log(g.default.red(` \u274C Gateway restart failed: ${r.message}`));try{await k(),console.log(g.default.dim(" Heartbeat pushed with updated status."))}catch{console.log(g.default.dim(" Could not push heartbeat."))}}else if(n.type==="bot.delete"&&n.bot_user_id){let r=n.bot_user_id,a=r.split(":")[0].replace("@","").replace(/_bot$/,"");console.log(g.default.yellow(`
|
|
119
|
+
\u{1F5D1}\uFE0F Bot deleted: ${r} \u2014 removing from OpenClaw config...`));try{let c=await import("fs"),l=await import("path"),m=await import("os"),h=l.join(m.homedir(),".openclaw","openclaw.json"),w=JSON.parse(c.readFileSync(h,"utf-8")),D=!1;if(w.channels?.badgerclaw?.accounts?.[a]&&(delete w.channels.badgerclaw.accounts[a],D=!0),w.agents?.list){let $=w.agents.list.length;w.agents.list=w.agents.list.filter(C=>C.id!==a),w.agents.list.length!==$&&(D=!0)}if(D){c.writeFileSync(h,JSON.stringify(w,null,2)),console.log(g.default.green(` \u2705 Removed "${a}" from openclaw.json`));let{execSync:$}=await import("child_process");try{$("openclaw gateway restart",{stdio:"ignore"}),console.log(g.default.green(" \u2705 Gateway restarted"))}catch{console.log(g.default.dim(" Gateway restart failed \u2014 restart manually"))}}else console.log(g.default.dim(` "${a}" not found in openclaw.json \u2014 nothing to remove`))}catch(c){console.log(g.default.red(` Failed to update openclaw.json: ${c}`))}}else if(n.type==="claude_code.start"&&n.bot_id&&n.room_id){console.log(g.default.cyan(`
|
|
120
|
+
\u{1F916} Claude Code START received: bot=${n.bot_id} room=${n.room_id}`));try{let r=await M({id:n.command_id||`cc-start-${Date.now()}`,command_type:"start_claude_code",payload:{bot_id:n.bot_id,room_id:n.room_id,session_id:n.session_id}});if(console.log(r.success?g.default.green(` \u2705 ${r.message}`):g.default.red(` \u274C ${r.message}`)),n.command_id)try{await v().post(`/api/v1/dashboard/commands/${n.command_id}/result`,{status:r.success?"success":"failed",result:r.message})}catch{}try{await k()}catch{}}catch(r){console.log(g.default.red(` Claude Code start error: ${r.message}`))}}else if(n.type==="claude_code.stop"&&n.bot_id&&n.room_id){console.log(g.default.cyan(`
|
|
121
|
+
\u{1F916} Claude Code STOP received: bot=${n.bot_id} room=${n.room_id}`));try{let r=await M({id:n.command_id||`cc-stop-${Date.now()}`,command_type:"stop_claude_code",payload:{bot_id:n.bot_id,room_id:n.room_id,session_id:n.session_id}});if(console.log(r.success?g.default.green(` \u2705 ${r.message}`):g.default.red(` \u274C ${r.message}`)),n.command_id)try{await v().post(`/api/v1/dashboard/commands/${n.command_id}/result`,{status:r.success?"success":"failed",result:r.message})}catch{}try{await k()}catch{}}catch(r){console.log(g.default.red(` Claude Code stop error: ${r.message}`))}}else if(n.type==="command.execute"&&n.command_id){console.log(g.default.cyan(`
|
|
122
|
+
\u26A1 SSE command received: ${n.command_type} (${n.command_id})`));try{let r=v();await r.post(`/api/v1/dashboard/commands/${n.command_id}/ack`);let a=await M({id:n.command_id,command_type:n.command_type,payload:n.payload});console.log(a.success?g.default.green(` \u2705 ${a.message}`):g.default.red(` \u274C ${a.message}`)),await r.post(`/api/v1/dashboard/commands/${n.command_id}/result`,{status:a.success?"success":"failed",result:a.message,new_version:a.newVersion||null});try{await k()}catch{}n.command_type==="update_cli"&&a.success&&(console.log(g.default.cyan(" CLI updated \u2014 restarting in 2s...")),setTimeout(()=>process.exit(0),2e3))}catch(r){console.log(g.default.red(` Command ${n.command_id} error: ${r.message}`))}}}catch{}},o.onerror=()=>{console.log(g.default.dim(" Reconnecting..."))},await new Promise(()=>{})});var to=require("commander"),A=i(require("chalk")),rt=require("child_process"),O=i(require("fs")),oo=i(require("os")),no=i(require("path")),X=no.default.join(oo.default.homedir(),".openclaw","openclaw.json"),ue=X+".badgerclaw-stash";function dn(){if(!O.default.existsSync(X))return null;try{let e=JSON.parse(O.default.readFileSync(X,"utf-8")),t=e.channels?.badgerclaw;return t?(delete e.channels.badgerclaw,e.plugins?.entries?.badgerclaw&&delete e.plugins.entries.badgerclaw,O.default.writeFileSync(X,JSON.stringify(e,null,2)),O.default.writeFileSync(ue,JSON.stringify(t,null,2)),t):null}catch{return null}}function mn(){if(O.default.existsSync(ue))try{let e=JSON.parse(O.default.readFileSync(ue,"utf-8")),t=JSON.parse(O.default.readFileSync(X,"utf-8"));t.channels=t.channels||{},t.channels.badgerclaw=e,O.default.writeFileSync(X,JSON.stringify(t,null,2)),O.default.unlinkSync(ue)}catch(e){console.log(A.default.yellow(` \u26A0\uFE0F Could not restore config: ${e.message}`)),console.log(A.default.yellow(` Your bot credentials are backed up at: ${ue}`))}}var so=new to.Command("setup").description("Install or update the OpenClaw BadgerClaw plugin safely (handles config automatically)").action(async()=>{console.log(A.default.green(`
|
|
123
|
+
\u{1F9A1} BadgerClaw Setup
|
|
124
|
+
`));let e=dn();e&&console.log(A.default.dim(" Existing bot config stashed temporarily...")),console.log(A.default.dim(" Installing @badgerclaw/connect plugin..."));let t=(0,rt.spawnSync)("openclaw",["plugins","install","@badgerclaw/connect","--dangerously-force-unsafe-install"],{stdio:"inherit",shell:!0});e&&(mn(),console.log(A.default.dim(" Bot config restored."))),t.status!==0&&(console.log(A.default.red(`
|
|
125
|
+
\u274C Plugin install failed. Your bot config has been restored.`)),process.exit(1)),console.log(A.default.green(`
|
|
126
|
+
\u2705 BadgerClaw plugin installed successfully!`)),console.log(A.default.dim(`
|
|
127
|
+
Restarting OpenClaw gateway to load plugin...`)),(0,rt.spawnSync)("openclaw",["gateway","restart"],{stdio:"inherit",shell:!0}).status===0?console.log(A.default.green(" Gateway restarted.")):console.log(A.default.yellow(" Gateway restart failed or not running \u2014 start it manually: openclaw gateway start")),console.log(A.default.dim("\nNext: run `badgerclaw login` to authenticate."))});var ao=require("commander"),_=i(require("chalk"));var io=new ao.Command("dashboard").description("Show local diagnostic dashboard \u2014 machine health, gateway status, bot telemetry").action(async()=>{f()||(console.log(_.default.yellow("Not logged in. Run `badgerclaw login` first.")),process.exit(1));let{version:t}=U(),o=ke(),s=await le();console.log(_.default.bold.green(`
|
|
128
|
+
BadgerClaw Dashboard
|
|
129
|
+
`)),console.log(_.default.bold(" Instance")),console.log(` ID: ${de()}`),console.log(` CLI Version: ${t}`),console.log(` Plugin: ${s.pluginVersion}`),console.log(),console.log(_.default.bold(" Machine")),console.log(` Hostname: ${o.hostname}`),console.log(` OS: ${o.os} / ${o.arch}`),console.log(` Uptime: ${ro(o.uptimeSeconds)}`),console.log(` Free Memory: ${o.memFreeMb} MB`),console.log();let n=s.status==="running"?_.default.green:s.status==="error"?_.default.red:_.default.yellow;if(console.log(_.default.bold(" Gateway")),console.log(` Status: ${n(s.status)}`),console.log(` PID: ${s.pid??"N/A"}`),console.log(` Last Restart: ${s.lastRestart??"N/A"}`),console.log(),s.bots.length===0)console.log(_.default.bold(" Bots")),console.log(_.default.dim(" No bots detected. Pair bots at https://badgerclaw.ai")),console.log();else{console.log(_.default.bold(` Bots (${s.bots.length})`)),console.log();for(let r of s.bots){let a=r.status==="running"?_.default.green:r.status==="error"?_.default.red:_.default.yellow;if(console.log(` ${_.default.bold(r.botUsername)} ${a(`[${r.status}]`)}`),console.log(` ID: ${r.botId}`),console.log(` Uptime: ${ro(r.uptimeSeconds)}`),console.log(` Messages: ${r.messagesReceived} in / ${r.messagesSent} out`),r.chunkedMessages>0&&console.log(` Chunked: ${r.chunkedMessages} messages, ${r.totalChunksSent} chunks`),console.log(` Rooms: ${r.roomsActive} active`),console.log(` Errors: ${r.errors}`),r.lastError&&(console.log(` Last Error: ${_.default.red(r.lastError)}`),console.log(` Error At: ${r.lastErrorAt}`)),console.log(` Last Active: ${r.lastActivity??"N/A"}`),r.roomDetails&&r.roomDetails.length>0){console.log(_.default.dim(" Rooms:"));for(let c of r.roomDetails)console.log(_.default.dim(` ${c.roomName} \u2014 ${c.messagesInRoom} msgs, last: ${c.lastActivityInRoom??"N/A"}`))}console.log()}}console.log(_.default.dim(` Last checked: ${new Date().toISOString()}`)),console.log()});function ro(e){if(e<60)return`${e}s`;if(e<3600)return`${Math.floor(e/60)}m ${e%60}s`;let t=Math.floor(e/3600),o=Math.floor(e%3600/60);return t<24?`${t}h ${o}m`:`${Math.floor(t/24)}d ${t%24}h ${o}m`}var at=require("commander"),Q=i(require("chalk")),co=i(require("ora"));var un=new at.Command("restart").description("Restart the OpenClaw gateway via the plugin probe endpoint").action(async()=>{f()||(console.log(Q.default.yellow("Not logged in. Run `badgerclaw login` first.")),process.exit(1));let t=(0,co.default)("Restarting gateway...").start(),o=await Y();if(o.success){t.succeed(Q.default.green(`Gateway restarted: ${o.message}`));try{await k(),console.log(Q.default.dim(" Heartbeat pushed with updated status."))}catch{console.log(Q.default.dim(" Could not push heartbeat \u2014 daemon may not be running."))}}else t.fail(Q.default.red(`Gateway restart failed: ${o.message}`)),process.exit(1)}),lo=new at.Command("gateway").description("Manage the OpenClaw gateway").addCommand(un);var G=require("commander"),p=i(require("chalk")),q=i(require("ora")),F=i(require("axios"));Pe();var W="http://localhost:7331",gn=new G.Command("start").description("Start a Claude Code session for a bot+room").requiredOption("--bot <botId>","Bot ID or username").requiredOption("--room <roomId>","Matrix room ID").option("--session-id <id>","Session ID (default: auto-generated)").option("--port <port>","MCP server port (default: 7332)","7332").option("--project <dir>","Project directory",process.cwd()).action(async e=>{f()||(console.log(p.default.yellow("Not logged in. Run `badgerclaw login` first.")),process.exit(1));let o=e.sessionId||`session-${Date.now()}`,s=parseInt(e.port,10),n=e.project.replace(/^~/,process.env.HOME||""),r=(0,q.default)("Starting Claude Code session...").start();try{await F.default.post(`${W}/claude-code/toggle`,{botId:e.bot,roomId:e.room,enabled:!0,sessionId:o},{timeout:1e4}),r.text="Gateway toggled \u2014 launching MCP server...";let c=Qe({sessionId:o,port:s,projectDir:n}).pid||0;await new Promise(l=>setTimeout(l,1500)),r.text="MCP server running \u2014 launching Claude Code...",Ze({sessionId:o,port:s,projectDir:n}),et(o,0,c,s,n),r.succeed(p.default.green("Claude Code session started")+p.default.dim(` (session: ${o}, port: ${s})`)),console.log(p.default.dim(` Bot: ${e.bot}`)),console.log(p.default.dim(` Room: ${e.room}`)),console.log(p.default.dim(` Project: ${n}`)),console.log(p.default.dim(" Claude Code should open in a new terminal window."))}catch(a){let c=a.response?.data?.message||a.message||"Unknown error";r.fail(p.default.red(`Failed to start Claude Code: ${c}`)),process.exit(1)}}),pn=new G.Command("stop").description("Stop a Claude Code session").option("--bot <botId>","Bot ID or username").option("--room <roomId>","Matrix room ID").option("--session-id <id>","Session ID to stop").action(async e=>{f()||(console.log(p.default.yellow("Not logged in. Run `badgerclaw login` first.")),process.exit(1));let o=(0,q.default)("Stopping Claude Code session...").start();try{e.bot&&e.room&&await F.default.post(`${W}/claude-code/toggle`,{botId:e.bot,roomId:e.room,enabled:!1},{timeout:1e4});let s=e.sessionId;if(!s)try{let a=((await F.default.get(`${W}/claude-code/status`,{timeout:5e3})).data?.rooms||[]).find(c=>c.botId===e.bot&&c.roomId===e.room);a&&(s=a.sessionId)}catch{}s?(tt(s),o.succeed(p.default.green(`Claude Code session stopped (${s})`))):(o.succeed(p.default.green("Claude Code toggled off at gateway")),console.log(p.default.dim(" No local session found to kill \u2014 may need to close Claude Code manually.")))}catch(s){let n=s.response?.data?.message||s.message||"Unknown error";o.fail(p.default.red(`Failed to stop Claude Code: ${n}`)),process.exit(1)}}),fn=new G.Command("status").description("Show active Claude Code sessions").action(async()=>{f()||(console.log(p.default.yellow("Not logged in. Run `badgerclaw login` first.")),process.exit(1));let t=(0,q.default)("Fetching Claude Code status...").start();try{let o=[];try{o=(await F.default.get(`${W}/claude-code/status`,{timeout:5e3})).data?.rooms||[]}catch{}let s=ot();if(t.stop(),o.length===0&&s.length===0){console.log(p.default.dim("No active Claude Code sessions."));return}if(o.length>0){console.log(p.default.bold(`
|
|
130
|
+
Gateway Claude Code Rooms:`)),console.log(p.default.dim(" Bot".padEnd(30)+"Room".padEnd(30)+"Session".padEnd(20)+"Enabled")),console.log(p.default.dim(" "+"-".repeat(88)));for(let n of o){let r=n.enabled?p.default.green("ON"):p.default.dim("OFF");console.log(` ${(n.botId||"").padEnd(30)}${(n.roomName||n.roomId||"").padEnd(30)}${(n.sessionId||"-").padEnd(20)}${r}`)}}if(s.length>0){console.log(p.default.bold(`
|
|
131
|
+
Local Sessions:`)),console.log(p.default.dim(" Session".padEnd(25)+"Port".padEnd(8)+"MCP PID".padEnd(10)+"Project".padEnd(40)+"Started")),console.log(p.default.dim(" "+"-".repeat(90)));for(let n of s)console.log(` ${n.sessionId.padEnd(25)}${String(n.port).padEnd(8)}${String(n.mcpPid).padEnd(10)}${n.projectDir.padEnd(40)}${n.startedAt}`)}console.log("")}catch(o){t.fail(p.default.red(`Failed to get status: ${o.message}`)),process.exit(1)}}),hn=new G.Command("group").description("Group bots/rooms into a shared Claude Code session").requiredOption("--session <id>","Session ID to group under").requiredOption("--bot <botId>","Bot ID or username").requiredOption("--room <roomId>","Matrix room ID").action(async e=>{let t=(0,q.default)("Grouping bot+room into session...").start();try{await F.default.post(`${W}/claude-code/toggle`,{botId:e.bot,roomId:e.room,enabled:!0,sessionId:e.session},{timeout:1e4}),t.succeed(p.default.green(`Grouped ${e.bot} + ${e.room} into session "${e.session}"`))}catch(o){let s=o.response?.data?.message||o.message||"Unknown error";t.fail(p.default.red(`Failed to group: ${s}`)),process.exit(1)}}),yn=new G.Command("allow").description("Add a user to the Claude Code allowlist for a bot+room").requiredOption("--bot <botId>","Bot ID or username").requiredOption("--room <roomId>","Matrix room ID").requiredOption("--user <userId>","Matrix user ID to allow (e.g. @alice:server)").action(async e=>{let t=(0,q.default)("Adding user to allowlist...").start();try{await F.default.post(`${W}/claude-code/allow`,{botId:e.bot,roomId:e.room,userId:e.user},{timeout:1e4}),t.succeed(p.default.green(`Added ${e.user} to allowlist for ${e.bot} in ${e.room}`))}catch(o){let s=o.response?.data?.message||o.message||"Unknown error";t.fail(p.default.red(`Failed to add user: ${s}`)),process.exit(1)}}),wn=new G.Command("revoke").description("Remove a user from the Claude Code allowlist for a bot+room").requiredOption("--bot <botId>","Bot ID or username").requiredOption("--room <roomId>","Matrix room ID").requiredOption("--user <userId>","Matrix user ID to revoke").action(async e=>{let t=(0,q.default)("Removing user from allowlist...").start();try{await F.default.post(`${W}/claude-code/revoke`,{botId:e.bot,roomId:e.room,userId:e.user},{timeout:1e4}),t.succeed(p.default.green(`Removed ${e.user} from allowlist for ${e.bot} in ${e.room}`))}catch(o){let s=o.response?.data?.message||o.message||"Unknown error";t.fail(p.default.red(`Failed to revoke user: ${s}`)),process.exit(1)}}),mo=new G.Command("claude-code").description("Manage Claude Code sessions (relay Matrix messages to local Claude Code)").addCommand(gn).addCommand(pn).addCommand(fn).addCommand(hn).addCommand(yn).addCommand(wn);var P=new uo.Command;P.name("badgerclaw").description("BadgerClaw CLI \u2014 one-click bot provisioning").version("0.2.9");P.addCommand(Bt);P.addCommand(Mt);P.addCommand(Ft);P.addCommand(Gt);P.addCommand(St);P.addCommand(eo);P.addCommand(so);P.addCommand(Xt);P.addCommand(io);P.addCommand(lo);P.addCommand(mo);P.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "badgerclaw",
|
|
3
|
+
"version": "0.2.9",
|
|
4
|
+
"description": "BadgerClaw CLI — one-click bot provisioning",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"badgerclaw": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "node build.mjs",
|
|
15
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
|
+
"axios": "^1.6.0",
|
|
23
|
+
"chalk": "^4.1.2",
|
|
24
|
+
"commander": "^12.0.0",
|
|
25
|
+
"eventsource": "^1.1.2",
|
|
26
|
+
"open": "^8.4.2",
|
|
27
|
+
"ora": "^5.4.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/eventsource": "^1.1.15",
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"esbuild": "^0.28.0",
|
|
33
|
+
"typescript": "^5.3.0"
|
|
34
|
+
}
|
|
35
|
+
}
|