wolverine-ai 6.2.3 → 6.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.1",
|
|
4
4
|
"description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,6 +30,7 @@ const NEVER_ROLLBACK = [
|
|
|
30
30
|
"server/config/settings.json",
|
|
31
31
|
"server/lib/db.js",
|
|
32
32
|
"server/lib/redis.js",
|
|
33
|
+
"wolverine-claw/config/settings.json",
|
|
33
34
|
".env",
|
|
34
35
|
".env.local",
|
|
35
36
|
".wolverine/vault/master.key",
|
|
@@ -352,10 +353,9 @@ class BackupManager {
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
_collectServerFiles() {
|
|
355
|
-
const serverDir = path.join(this.projectRoot, "server");
|
|
356
|
-
if (!fs.existsSync(serverDir)) return [];
|
|
357
356
|
const files = [];
|
|
358
357
|
const SKIP = new Set(["node_modules", ".git", ".wolverine"]);
|
|
358
|
+
const maxFileSize = 10 * 1024 * 1024; // 10MB
|
|
359
359
|
|
|
360
360
|
const walk = (dir) => {
|
|
361
361
|
let entries;
|
|
@@ -365,11 +365,39 @@ class BackupManager {
|
|
|
365
365
|
const fullPath = path.join(dir, entry.name);
|
|
366
366
|
if (entry.isDirectory()) walk(fullPath);
|
|
367
367
|
else {
|
|
368
|
-
try { if (fs.statSync(fullPath).size <=
|
|
368
|
+
try { if (fs.statSync(fullPath).size <= maxFileSize) files.push(fullPath); } catch {}
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
371
|
};
|
|
372
|
-
|
|
372
|
+
|
|
373
|
+
// 1. server/ — user server code (original behavior)
|
|
374
|
+
const serverDir = path.join(this.projectRoot, "server");
|
|
375
|
+
if (fs.existsSync(serverDir)) walk(serverDir);
|
|
376
|
+
|
|
377
|
+
// 2. wolverine-claw/ — claw config, plugins, custom skills
|
|
378
|
+
// Skip workspace/ (agent-generated files, can be large/numerous)
|
|
379
|
+
const clawDir = path.join(this.projectRoot, "wolverine-claw");
|
|
380
|
+
if (fs.existsSync(clawDir)) {
|
|
381
|
+
const clawSubdirs = ["config", "plugins", "skills"];
|
|
382
|
+
for (const sub of clawSubdirs) {
|
|
383
|
+
const subDir = path.join(clawDir, sub);
|
|
384
|
+
if (fs.existsSync(subDir)) walk(subDir);
|
|
385
|
+
}
|
|
386
|
+
// Also back up wolverine-claw/index.js directly
|
|
387
|
+
const clawIndex = path.join(clawDir, "index.js");
|
|
388
|
+
if (fs.existsSync(clawIndex)) files.push(clawIndex);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 3. .openclaw/ — OpenClaw config (if user has one)
|
|
392
|
+
const openclawDir = path.join(this.projectRoot, ".openclaw");
|
|
393
|
+
if (fs.existsSync(openclawDir)) walk(openclawDir);
|
|
394
|
+
|
|
395
|
+
// 4. Top-level openclaw config files
|
|
396
|
+
for (const cfgName of ["openclaw.yml", "openclaw.yaml", ".openclaw.yml"]) {
|
|
397
|
+
const cfgPath = path.join(this.projectRoot, cfgName);
|
|
398
|
+
if (fs.existsSync(cfgPath)) files.push(cfgPath);
|
|
399
|
+
}
|
|
400
|
+
|
|
373
401
|
return files;
|
|
374
402
|
}
|
|
375
403
|
}
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
* Wolverine Claw Standalone Agent
|
|
3
3
|
*
|
|
4
4
|
* When OpenClaw isn't installed (or Node < 22), this provides a built-in
|
|
5
|
-
* agentic terminal REPL using wolverine's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* wolverine's server healing.
|
|
5
|
+
* agentic terminal REPL using wolverine's full agent engine — all 32 tools
|
|
6
|
+
* (file ops, shell, git, diagnostics, database, network, deps, research,
|
|
7
|
+
* advanced monitoring) powered by the same AI pipeline as server healing.
|
|
9
8
|
*
|
|
10
9
|
* This makes wolverine-claw work immediately without any external deps,
|
|
11
10
|
* and openclaw becomes an optional enhancement for multi-channel support.
|
|
@@ -15,266 +14,11 @@ const fs = require("fs");
|
|
|
15
14
|
const path = require("path");
|
|
16
15
|
const readline = require("readline");
|
|
17
16
|
const chalk = require("chalk");
|
|
18
|
-
const { execSync } = require("child_process");
|
|
19
|
-
|
|
20
|
-
// ── Tool Definitions ────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
function buildTools(cwd, workspacePath, config) {
|
|
23
|
-
const sandbox = config.security?.sandbox !== false;
|
|
24
|
-
|
|
25
|
-
function resolveSafe(filePath) {
|
|
26
|
-
const resolved = path.resolve(cwd, filePath);
|
|
27
|
-
if (sandbox) {
|
|
28
|
-
// In sandbox mode, allow cwd and workspace
|
|
29
|
-
if (!resolved.startsWith(cwd)) return null;
|
|
30
|
-
// Block protected paths
|
|
31
|
-
const rel = path.relative(cwd, resolved);
|
|
32
|
-
if (rel.startsWith("node_modules") || rel.startsWith(".env") || rel.startsWith("src/")) {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return resolved;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return [
|
|
40
|
-
{
|
|
41
|
-
name: "read_file",
|
|
42
|
-
description: "Read a file's contents. Use offset and limit for large files.",
|
|
43
|
-
input_schema: {
|
|
44
|
-
type: "object",
|
|
45
|
-
properties: {
|
|
46
|
-
path: { type: "string", description: "File path relative to project root" },
|
|
47
|
-
offset: { type: "number", description: "Start line (0-indexed)" },
|
|
48
|
-
limit: { type: "number", description: "Max lines to read" },
|
|
49
|
-
},
|
|
50
|
-
required: ["path"],
|
|
51
|
-
},
|
|
52
|
-
execute: ({ path: p, offset, limit }) => {
|
|
53
|
-
const resolved = resolveSafe(p);
|
|
54
|
-
if (!resolved) return `[ERROR] Access denied: ${p}`;
|
|
55
|
-
try {
|
|
56
|
-
let content = fs.readFileSync(resolved, "utf-8");
|
|
57
|
-
if (offset || limit) {
|
|
58
|
-
const lines = content.split("\n");
|
|
59
|
-
const start = offset || 0;
|
|
60
|
-
const end = limit ? start + limit : lines.length;
|
|
61
|
-
content = lines.slice(start, end).join("\n");
|
|
62
|
-
}
|
|
63
|
-
if (content.length > 8000) content = content.slice(0, 8000) + "\n... (truncated)";
|
|
64
|
-
return content;
|
|
65
|
-
} catch (e) { return `[ERROR] ${e.message}`; }
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: "write_file",
|
|
70
|
-
description: "Write content to a file. Creates directories if needed.",
|
|
71
|
-
input_schema: {
|
|
72
|
-
type: "object",
|
|
73
|
-
properties: {
|
|
74
|
-
path: { type: "string", description: "File path relative to project root" },
|
|
75
|
-
content: { type: "string", description: "File content" },
|
|
76
|
-
},
|
|
77
|
-
required: ["path", "content"],
|
|
78
|
-
},
|
|
79
|
-
execute: ({ path: p, content }) => {
|
|
80
|
-
const resolved = resolveSafe(p);
|
|
81
|
-
if (!resolved) return `[ERROR] Access denied: ${p}`;
|
|
82
|
-
try {
|
|
83
|
-
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
84
|
-
fs.writeFileSync(resolved, content);
|
|
85
|
-
return `Written: ${p} (${content.length} bytes)`;
|
|
86
|
-
} catch (e) { return `[ERROR] ${e.message}`; }
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
name: "edit_file",
|
|
91
|
-
description: "Find and replace text in a file. Surgical single-match edit.",
|
|
92
|
-
input_schema: {
|
|
93
|
-
type: "object",
|
|
94
|
-
properties: {
|
|
95
|
-
path: { type: "string", description: "File path relative to project root" },
|
|
96
|
-
find: { type: "string", description: "Exact text to find" },
|
|
97
|
-
replace: { type: "string", description: "Replacement text" },
|
|
98
|
-
},
|
|
99
|
-
required: ["path", "find", "replace"],
|
|
100
|
-
},
|
|
101
|
-
execute: ({ path: p, find, replace }) => {
|
|
102
|
-
const resolved = resolveSafe(p);
|
|
103
|
-
if (!resolved) return `[ERROR] Access denied: ${p}`;
|
|
104
|
-
try {
|
|
105
|
-
const content = fs.readFileSync(resolved, "utf-8");
|
|
106
|
-
if (!content.includes(find)) return `[ERROR] Text not found in ${p}`;
|
|
107
|
-
const count = content.split(find).length - 1;
|
|
108
|
-
if (count > 1) return `[ERROR] ${count} matches found — provide more context for a unique match`;
|
|
109
|
-
fs.writeFileSync(resolved, content.replace(find, replace));
|
|
110
|
-
return `Edited: ${p}`;
|
|
111
|
-
} catch (e) { return `[ERROR] ${e.message}`; }
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
name: "list_dir",
|
|
116
|
-
description: "List files and directories in a path.",
|
|
117
|
-
input_schema: {
|
|
118
|
-
type: "object",
|
|
119
|
-
properties: {
|
|
120
|
-
path: { type: "string", description: "Directory path relative to project root" },
|
|
121
|
-
},
|
|
122
|
-
required: ["path"],
|
|
123
|
-
},
|
|
124
|
-
execute: ({ path: p }) => {
|
|
125
|
-
const resolved = resolveSafe(p || ".");
|
|
126
|
-
if (!resolved) return `[ERROR] Access denied: ${p}`;
|
|
127
|
-
try {
|
|
128
|
-
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
129
|
-
return entries.map(e => {
|
|
130
|
-
const prefix = e.isDirectory() ? "[DIR] " : " ";
|
|
131
|
-
let size = "";
|
|
132
|
-
if (!e.isDirectory()) {
|
|
133
|
-
try { size = ` (${fs.statSync(path.join(resolved, e.name)).size}b)`; } catch {}
|
|
134
|
-
}
|
|
135
|
-
return `${prefix}${e.name}${size}`;
|
|
136
|
-
}).join("\n");
|
|
137
|
-
} catch (e) { return `[ERROR] ${e.message}`; }
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
name: "glob_files",
|
|
142
|
-
description: "Find files matching a glob pattern (e.g., **/*.js).",
|
|
143
|
-
input_schema: {
|
|
144
|
-
type: "object",
|
|
145
|
-
properties: {
|
|
146
|
-
pattern: { type: "string", description: "Glob pattern" },
|
|
147
|
-
},
|
|
148
|
-
required: ["pattern"],
|
|
149
|
-
},
|
|
150
|
-
execute: ({ pattern }) => {
|
|
151
|
-
try {
|
|
152
|
-
// Use Node's fs.globSync if available (Node 22+), else fallback to find
|
|
153
|
-
if (fs.globSync) {
|
|
154
|
-
const matches = fs.globSync(pattern, { cwd });
|
|
155
|
-
return matches.slice(0, 50).join("\n") || "No matches";
|
|
156
|
-
}
|
|
157
|
-
// Fallback: simple recursive walk with pattern matching
|
|
158
|
-
const simplePattern = pattern.replace(/\*\*/g, "").replace(/\*/g, "");
|
|
159
|
-
const ext = path.extname(simplePattern) || "";
|
|
160
|
-
const results = [];
|
|
161
|
-
function walk(dir, depth) {
|
|
162
|
-
if (depth > 5 || results.length > 50) return;
|
|
163
|
-
try {
|
|
164
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
165
|
-
if (e.name.startsWith(".") || e.name === "node_modules") continue;
|
|
166
|
-
const full = path.join(dir, e.name);
|
|
167
|
-
if (e.isDirectory()) walk(full, depth + 1);
|
|
168
|
-
else if (!ext || e.name.endsWith(ext)) results.push(path.relative(cwd, full));
|
|
169
|
-
}
|
|
170
|
-
} catch {}
|
|
171
|
-
}
|
|
172
|
-
walk(cwd, 0);
|
|
173
|
-
return results.join("\n") || "No matches";
|
|
174
|
-
} catch (e) { return `[ERROR] ${e.message}`; }
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
name: "grep_code",
|
|
179
|
-
description: "Search file contents for a regex pattern.",
|
|
180
|
-
input_schema: {
|
|
181
|
-
type: "object",
|
|
182
|
-
properties: {
|
|
183
|
-
pattern: { type: "string", description: "Regex pattern to search for" },
|
|
184
|
-
path: { type: "string", description: "File or directory to search in (default: .)" },
|
|
185
|
-
},
|
|
186
|
-
required: ["pattern"],
|
|
187
|
-
},
|
|
188
|
-
execute: ({ pattern, path: searchPath }) => {
|
|
189
|
-
const resolved = resolveSafe(searchPath || ".");
|
|
190
|
-
if (!resolved) return `[ERROR] Access denied`;
|
|
191
|
-
try {
|
|
192
|
-
const result = execSync(
|
|
193
|
-
`grep -rn --include="*.js" --include="*.json" --include="*.ts" --include="*.md" "${pattern.replace(/"/g, '\\"')}" "${resolved}" 2>/dev/null | head -30`,
|
|
194
|
-
{ encoding: "utf-8", timeout: 10000, cwd }
|
|
195
|
-
);
|
|
196
|
-
return result.trim() || "No matches";
|
|
197
|
-
} catch {
|
|
198
|
-
// grep returns exit 1 on no match
|
|
199
|
-
return "No matches";
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
name: "bash_exec",
|
|
205
|
-
description: "Execute a shell command. 30s timeout. Dangerous commands blocked.",
|
|
206
|
-
input_schema: {
|
|
207
|
-
type: "object",
|
|
208
|
-
properties: {
|
|
209
|
-
command: { type: "string", description: "Shell command to run" },
|
|
210
|
-
},
|
|
211
|
-
required: ["command"],
|
|
212
|
-
},
|
|
213
|
-
execute: ({ command }) => {
|
|
214
|
-
// Block dangerous commands
|
|
215
|
-
const blocked = ["rm -rf /", "rm -rf /*", "mkfs", "dd if=", "shutdown", "reboot",
|
|
216
|
-
"format c:", "git push --force", "npm publish", "> /dev/sda"];
|
|
217
|
-
for (const b of blocked) {
|
|
218
|
-
if (command.includes(b)) return `[ERROR] Blocked dangerous command: ${b}`;
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
const result = execSync(command, {
|
|
222
|
-
encoding: "utf-8",
|
|
223
|
-
timeout: 30000,
|
|
224
|
-
cwd,
|
|
225
|
-
maxBuffer: 1024 * 1024,
|
|
226
|
-
});
|
|
227
|
-
const trimmed = result.trim();
|
|
228
|
-
if (trimmed.length > 4000) return trimmed.slice(0, 4000) + "\n... (truncated)";
|
|
229
|
-
return trimmed || "(no output)";
|
|
230
|
-
} catch (e) {
|
|
231
|
-
return `[ERROR] ${e.message?.split("\n")[0] || "Command failed"}`;
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
},
|
|
235
|
-
{
|
|
236
|
-
name: "done",
|
|
237
|
-
description: "Signal that you have completed the user's request. Include a summary.",
|
|
238
|
-
input_schema: {
|
|
239
|
-
type: "object",
|
|
240
|
-
properties: {
|
|
241
|
-
summary: { type: "string", description: "Summary of what was done" },
|
|
242
|
-
},
|
|
243
|
-
required: ["summary"],
|
|
244
|
-
},
|
|
245
|
-
execute: ({ summary }) => summary,
|
|
246
|
-
},
|
|
247
|
-
];
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// ── Agent Loop ──────────────────────────────────────────────────
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Run one agent turn: send messages to AI, execute tool calls, return.
|
|
254
|
-
*/
|
|
255
|
-
async function agentTurn(aiCall, model, systemPrompt, messages, tools, maxTokens) {
|
|
256
|
-
// Convert tools to AI format
|
|
257
|
-
const toolDefs = tools.map(t => ({
|
|
258
|
-
name: t.name,
|
|
259
|
-
description: t.description,
|
|
260
|
-
input_schema: t.input_schema,
|
|
261
|
-
}));
|
|
262
|
-
|
|
263
|
-
const result = await aiCall({
|
|
264
|
-
model,
|
|
265
|
-
messages,
|
|
266
|
-
tools: toolDefs,
|
|
267
|
-
maxTokens,
|
|
268
|
-
category: "tool",
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
17
|
|
|
274
18
|
// ── REPL ────────────────────────────────────────────────────────
|
|
275
19
|
|
|
276
20
|
/**
|
|
277
|
-
* Start the standalone agent REPL.
|
|
21
|
+
* Start the standalone agent REPL with all 32 wolverine tools.
|
|
278
22
|
*/
|
|
279
23
|
async function startRepl(config, options = {}) {
|
|
280
24
|
const cwd = options.cwd || process.cwd();
|
|
@@ -297,27 +41,78 @@ async function startRepl(config, options = {}) {
|
|
|
297
41
|
process.exit(1);
|
|
298
42
|
}
|
|
299
43
|
|
|
300
|
-
|
|
44
|
+
// Load the real AgentEngine + TOOL_DEFINITIONS from wolverine's agent
|
|
45
|
+
let AgentEngine, TOOL_DEFINITIONS;
|
|
46
|
+
try {
|
|
47
|
+
const agentMod = require("../agent/agent-engine");
|
|
48
|
+
AgentEngine = agentMod.AgentEngine;
|
|
49
|
+
TOOL_DEFINITIONS = agentMod.TOOL_DEFINITIONS;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.error(chalk.red(` [CLAW] Failed to load agent engine: ${e.message}`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Use the real Sandbox — same security as heal pipeline
|
|
56
|
+
const { Sandbox } = require("../security/sandbox");
|
|
57
|
+
const sandbox = new Sandbox(cwd);
|
|
301
58
|
|
|
302
|
-
|
|
59
|
+
// Instantiate the agent engine for tool execution
|
|
60
|
+
const engine = new AgentEngine({
|
|
61
|
+
cwd,
|
|
62
|
+
sandbox,
|
|
63
|
+
maxTurns,
|
|
64
|
+
maxTokens: 100000,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Count tools by category
|
|
68
|
+
const toolNames = TOOL_DEFINITIONS.map(t => t.function.name);
|
|
69
|
+
const categories = {
|
|
70
|
+
file: ["read_file", "write_file", "edit_file", "glob_files", "grep_code"],
|
|
71
|
+
shell: ["bash_exec", "git_log", "git_diff"],
|
|
72
|
+
diagnostics: ["list_dir", "move_file", "check_port", "check_env", "inspect_env", "check_memory", "list_processes", "check_logs", "check_network"],
|
|
73
|
+
database: ["inspect_db", "run_db_fix"],
|
|
74
|
+
deps: ["audit_deps", "check_migration"],
|
|
75
|
+
research: ["web_fetch"],
|
|
76
|
+
advanced: ["verify_node_modules", "inspect_certificate", "inspect_cache", "disk_cleanup", "check_file_descriptors", "check_event_loop", "check_websocket"],
|
|
77
|
+
env: ["add_env_var"],
|
|
78
|
+
server: ["restart_service"],
|
|
79
|
+
control: ["done"],
|
|
80
|
+
};
|
|
303
81
|
|
|
304
|
-
|
|
82
|
+
const systemPrompt = `You are Wolverine Claw, an agentic AI coding assistant running inside the Wolverine self-healing framework.
|
|
305
83
|
|
|
306
|
-
|
|
84
|
+
You have access to ${TOOL_DEFINITIONS.length} tools across ${Object.keys(categories).length} categories:
|
|
85
|
+
- FILE: read_file, write_file, edit_file, glob_files, grep_code
|
|
86
|
+
- SHELL & GIT: bash_exec, git_log, git_diff
|
|
87
|
+
- DIAGNOSTICS: list_dir, move_file, check_port, check_env, inspect_env, check_memory, list_processes, check_logs, check_network
|
|
88
|
+
- DATABASE: inspect_db, run_db_fix (always inspect_db FIRST)
|
|
89
|
+
- DEPENDENCIES: audit_deps, check_migration
|
|
90
|
+
- RESEARCH: web_fetch
|
|
91
|
+
- ADVANCED: verify_node_modules, inspect_certificate, inspect_cache, disk_cleanup, check_file_descriptors, check_event_loop, check_websocket
|
|
92
|
+
- ENVIRONMENT: add_env_var
|
|
93
|
+
- SERVER: restart_service
|
|
94
|
+
- CONTROL: done
|
|
95
|
+
|
|
96
|
+
Project root: ${cwd}
|
|
97
|
+
Workspace for new files: ${workspacePath}
|
|
307
98
|
|
|
308
99
|
Guidelines:
|
|
309
|
-
- Read files before editing
|
|
310
|
-
- Use edit_file for surgical changes, write_file for new files.
|
|
311
|
-
- Use grep_code and glob_files to explore the codebase
|
|
312
|
-
- Use bash_exec for git, npm,
|
|
313
|
-
-
|
|
314
|
-
-
|
|
315
|
-
-
|
|
100
|
+
- Read files before editing. Understand existing code first.
|
|
101
|
+
- Use edit_file (old_text/new_text) for surgical changes, write_file for new files.
|
|
102
|
+
- Use grep_code and glob_files to explore the codebase.
|
|
103
|
+
- Use bash_exec for git, npm, system commands.
|
|
104
|
+
- Use inspect_db before run_db_fix. Always check before modifying.
|
|
105
|
+
- Use audit_deps when dependency issues are suspected.
|
|
106
|
+
- Use check_port for EADDRINUSE, check_network for connectivity, inspect_certificate for TLS.
|
|
107
|
+
- When done with a task, call the done tool with a summary.
|
|
108
|
+
- Be concise. Fix what's asked.`;
|
|
316
109
|
|
|
317
110
|
console.log(chalk.blue.bold("\n 🐾 Wolverine Claw — Interactive Agent\n"));
|
|
318
111
|
console.log(chalk.gray(` Model: ${model}`));
|
|
319
112
|
console.log(chalk.gray(` Workspace: ${workspacePath}`));
|
|
320
|
-
console.log(chalk.gray(` Tools: ${
|
|
113
|
+
console.log(chalk.gray(` Tools: ${TOOL_DEFINITIONS.length} across ${Object.keys(categories).length} categories`));
|
|
114
|
+
console.log(chalk.gray(` file(5) shell(3) diagnostics(9) database(2) deps(2)`));
|
|
115
|
+
console.log(chalk.gray(` research(1) advanced(7) env(1) server(1) control(1)`));
|
|
321
116
|
console.log(chalk.gray(` Max turns: ${maxTurns}`));
|
|
322
117
|
console.log(chalk.gray(` Type 'exit' or Ctrl+C to quit.\n`));
|
|
323
118
|
|
|
@@ -327,7 +122,6 @@ Guidelines:
|
|
|
327
122
|
prompt: chalk.blue(" 🐾 > "),
|
|
328
123
|
});
|
|
329
124
|
|
|
330
|
-
// Conversation history
|
|
331
125
|
let messages = [];
|
|
332
126
|
|
|
333
127
|
rl.prompt();
|
|
@@ -341,7 +135,6 @@ Guidelines:
|
|
|
341
135
|
process.exit(0);
|
|
342
136
|
}
|
|
343
137
|
|
|
344
|
-
// Special commands
|
|
345
138
|
if (input === "/clear") {
|
|
346
139
|
messages = [];
|
|
347
140
|
console.log(chalk.gray(" Conversation cleared.\n"));
|
|
@@ -349,44 +142,35 @@ Guidelines:
|
|
|
349
142
|
return;
|
|
350
143
|
}
|
|
351
144
|
if (input === "/status") {
|
|
352
|
-
console.log(chalk.gray(` Messages: ${messages.length}, Model: ${model}`));
|
|
145
|
+
console.log(chalk.gray(` Messages: ${messages.length}, Model: ${model}, Tools: ${TOOL_DEFINITIONS.length}`));
|
|
146
|
+
rl.prompt();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (input === "/tools") {
|
|
150
|
+
for (const [cat, names] of Object.entries(categories)) {
|
|
151
|
+
console.log(chalk.gray(` ${cat}: ${names.join(", ")}`));
|
|
152
|
+
}
|
|
153
|
+
console.log("");
|
|
353
154
|
rl.prompt();
|
|
354
155
|
return;
|
|
355
156
|
}
|
|
356
157
|
|
|
357
|
-
// Add user message
|
|
358
158
|
messages.push({ role: "user", content: input });
|
|
359
159
|
|
|
360
|
-
// Agent loop
|
|
361
|
-
// aiCallWithHistory returns OpenAI-shaped responses:
|
|
362
|
-
// {choices: [{message: {role, content, tool_calls}}], usage}
|
|
363
|
-
// Tool calls come as: {id, type:"function", function:{name, arguments:JSON_STRING}}
|
|
364
|
-
// Tool defs must be: {type:"function", function:{name, description, parameters}}
|
|
365
160
|
let turn = 0;
|
|
366
161
|
let done = false;
|
|
367
162
|
|
|
368
|
-
// Build OpenAI-format tool definitions once
|
|
369
|
-
const toolDefs = tools.map(t => ({
|
|
370
|
-
type: "function",
|
|
371
|
-
function: {
|
|
372
|
-
name: t.name,
|
|
373
|
-
description: t.description,
|
|
374
|
-
parameters: t.input_schema,
|
|
375
|
-
},
|
|
376
|
-
}));
|
|
377
|
-
|
|
378
163
|
while (turn < maxTurns && !done) {
|
|
379
164
|
turn++;
|
|
380
165
|
try {
|
|
381
166
|
const response = await aiCallWithHistory({
|
|
382
167
|
model,
|
|
383
168
|
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
|
384
|
-
tools:
|
|
169
|
+
tools: TOOL_DEFINITIONS,
|
|
385
170
|
maxTokens: 4096,
|
|
386
171
|
category: "tool",
|
|
387
172
|
});
|
|
388
173
|
|
|
389
|
-
// Extract from OpenAI-shaped response
|
|
390
174
|
const msg = response.choices?.[0]?.message;
|
|
391
175
|
if (!msg) {
|
|
392
176
|
console.log(chalk.yellow(" (empty response)\n"));
|
|
@@ -397,14 +181,11 @@ Guidelines:
|
|
|
397
181
|
const textContent = msg.content || "";
|
|
398
182
|
const toolCalls = msg.tool_calls || [];
|
|
399
183
|
|
|
400
|
-
// Handle text content
|
|
401
184
|
if (textContent) {
|
|
402
185
|
console.log(chalk.white(`\n ${textContent.replace(/\n/g, "\n ")}\n`));
|
|
403
186
|
}
|
|
404
187
|
|
|
405
|
-
// Handle tool calls
|
|
406
188
|
if (toolCalls.length > 0) {
|
|
407
|
-
// Store assistant message with tool calls
|
|
408
189
|
messages.push({
|
|
409
190
|
role: "assistant",
|
|
410
191
|
content: textContent || null,
|
|
@@ -420,33 +201,36 @@ Guidelines:
|
|
|
420
201
|
: (tc.function?.arguments || tc.input || {});
|
|
421
202
|
} catch { toolInput = {}; }
|
|
422
203
|
|
|
423
|
-
const tool = tools.find(t => t.name === toolName);
|
|
424
|
-
|
|
425
|
-
if (!tool) {
|
|
426
|
-
console.log(chalk.yellow(` [tool] Unknown: ${toolName}`));
|
|
427
|
-
messages.push({ role: "tool", tool_call_id: tc.id, content: `[ERROR] Unknown tool: ${toolName}` });
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
204
|
if (toolName === "done") {
|
|
432
|
-
const summary =
|
|
205
|
+
const summary = toolInput.summary || toolInput.message || "Task completed.";
|
|
433
206
|
console.log(chalk.green(` ✅ ${summary}\n`));
|
|
434
207
|
messages.push({ role: "tool", tool_call_id: tc.id, content: summary });
|
|
435
208
|
done = true;
|
|
436
209
|
break;
|
|
437
210
|
}
|
|
438
211
|
|
|
439
|
-
console.log(chalk.gray(` [${toolName}] ${JSON.stringify(toolInput).slice(0,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
212
|
+
console.log(chalk.gray(` [${toolName}] ${JSON.stringify(toolInput).slice(0, 120)}`));
|
|
213
|
+
|
|
214
|
+
// Execute via the real AgentEngine — same tools as heal pipeline
|
|
215
|
+
let toolResult;
|
|
216
|
+
try {
|
|
217
|
+
const result = await engine._executeTool({
|
|
218
|
+
function: { name: toolName, arguments: JSON.stringify(toolInput) },
|
|
219
|
+
});
|
|
220
|
+
toolResult = typeof result === "string" ? result : (result?.content || JSON.stringify(result));
|
|
221
|
+
} catch (e) {
|
|
222
|
+
toolResult = `[ERROR] ${e.message}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Cap display output
|
|
226
|
+
const displayResult = toolResult.length > 300
|
|
227
|
+
? toolResult.slice(0, 300) + "..."
|
|
443
228
|
: toolResult;
|
|
444
229
|
console.log(chalk.gray(` → ${displayResult.replace(/\n/g, "\n ")}`));
|
|
445
230
|
|
|
446
231
|
messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
|
|
447
232
|
}
|
|
448
233
|
} else if (textContent) {
|
|
449
|
-
// Text-only response, no tool calls — done for this turn
|
|
450
234
|
messages.push({ role: "assistant", content: textContent });
|
|
451
235
|
done = true;
|
|
452
236
|
} else {
|
|
@@ -476,7 +260,6 @@ Guidelines:
|
|
|
476
260
|
process.send({ type: "claw_health", status: "running", detail: "standalone-agent", timestamp: Date.now() });
|
|
477
261
|
} catch {}
|
|
478
262
|
|
|
479
|
-
// Heartbeat
|
|
480
263
|
setInterval(() => {
|
|
481
264
|
try {
|
|
482
265
|
process.send({ type: "claw_heartbeat", uptime: process.uptime(), memory: process.memoryUsage(), timestamp: Date.now() });
|
|
@@ -485,4 +268,16 @@ Guidelines:
|
|
|
485
268
|
}
|
|
486
269
|
}
|
|
487
270
|
|
|
488
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Get tool definitions for external use (e.g., tests).
|
|
273
|
+
*/
|
|
274
|
+
function getToolDefinitions() {
|
|
275
|
+
try {
|
|
276
|
+
const { TOOL_DEFINITIONS } = require("../agent/agent-engine");
|
|
277
|
+
return TOOL_DEFINITIONS;
|
|
278
|
+
} catch {
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { startRepl, getToolDefinitions };
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wolverine Integration Plugin for OpenClaw
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - Brain: semantic memory, project context, and learning
|
|
6
|
-
* - Backup: workspace snapshots and rollback
|
|
7
|
-
* - Healing: error diagnosis and auto-fix pipeline
|
|
8
|
-
* - Health: process monitoring and status reporting
|
|
4
|
+
* Two integration layers:
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* 1. TOOLS — 7 wolverine tools injected into the agent toolkit
|
|
7
|
+
* (backup, rollback, brain search/learn, health, self-heal)
|
|
8
|
+
*
|
|
9
|
+
* 2. HOOKS — taps into OpenClaw's 28-hook plugin system for deep
|
|
10
|
+
* error observability and self-healing triggers:
|
|
11
|
+
* - after_tool_call: catch skill/tool failures
|
|
12
|
+
* - agent_end: catch agent crashes and timeouts
|
|
13
|
+
* - before_tool_call: detect and report tool loops
|
|
14
|
+
* - message_sent: catch channel send failures
|
|
15
|
+
* - llm_output: catch provider errors, failovers, billing blocks
|
|
16
|
+
* - subagent_ended: catch subagent failures
|
|
17
|
+
* - gateway_start/gateway_stop: lifecycle tracking
|
|
18
|
+
* - session_end: session error tracking
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
21
|
const path = require("path");
|
|
@@ -16,51 +23,348 @@ const fs = require("fs");
|
|
|
16
23
|
|
|
17
24
|
const PLUGIN_NAME = "wolverine-integration";
|
|
18
25
|
|
|
26
|
+
// ── Error tracking state ────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const _errorCounts = {
|
|
29
|
+
tool: 0,
|
|
30
|
+
agent: 0,
|
|
31
|
+
channel: 0,
|
|
32
|
+
llm: 0,
|
|
33
|
+
subagent: 0,
|
|
34
|
+
total: 0,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const _recentErrors = []; // last 50 errors for context
|
|
38
|
+
const MAX_RECENT = 50;
|
|
39
|
+
|
|
40
|
+
function _trackError(category, summary, detail) {
|
|
41
|
+
_errorCounts[category] = (_errorCounts[category] || 0) + 1;
|
|
42
|
+
_errorCounts.total++;
|
|
43
|
+
|
|
44
|
+
const entry = {
|
|
45
|
+
category,
|
|
46
|
+
summary: summary?.slice(0, 200),
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
detail: detail?.slice?.(0, 500),
|
|
49
|
+
};
|
|
50
|
+
_recentErrors.push(entry);
|
|
51
|
+
if (_recentErrors.length > MAX_RECENT) _recentErrors.shift();
|
|
52
|
+
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Main registration ───────────────────────────────────────────
|
|
57
|
+
|
|
19
58
|
/**
|
|
20
59
|
* Register the wolverine integration with the OpenClaw gateway.
|
|
60
|
+
* Supports both Plugin SDK (api.registerPluginHook) and basic EventEmitter.
|
|
21
61
|
*/
|
|
22
62
|
async function register(gateway, config) {
|
|
23
63
|
const projectRoot = path.resolve(__dirname, "../..");
|
|
24
64
|
|
|
25
|
-
// Register wolverine tools
|
|
65
|
+
// Register wolverine tools
|
|
26
66
|
const tools = buildWolverineTools(projectRoot, config);
|
|
67
|
+
_registerTools(gateway, tools);
|
|
27
68
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
gateway.skills.register(PLUGIN_NAME, {
|
|
34
|
-
description: "Wolverine self-healing integration — backup, brain, and health tools",
|
|
35
|
-
tools,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Hook into gateway events for self-healing
|
|
40
|
-
if (gateway.on) {
|
|
41
|
-
gateway.on("error", (err) => handleGatewayError(err, projectRoot));
|
|
42
|
-
gateway.on("agent:error", (err) => handleAgentError(err, projectRoot));
|
|
43
|
-
gateway.on("skill:error", (err) => handleSkillError(err, projectRoot));
|
|
69
|
+
// Register into OpenClaw Plugin SDK hooks (preferred)
|
|
70
|
+
const api = _getPluginApi(gateway);
|
|
71
|
+
if (api) {
|
|
72
|
+
_registerPluginHooks(api, projectRoot, config);
|
|
73
|
+
console.log("[CLAW] Wolverine plugin hooks registered (Plugin SDK)");
|
|
44
74
|
}
|
|
75
|
+
// Also register EventEmitter hooks (fallback / additional coverage)
|
|
76
|
+
_registerEventHooks(gateway, projectRoot);
|
|
45
77
|
|
|
46
|
-
// Periodic health reporting
|
|
78
|
+
// Periodic health reporting with error stats
|
|
47
79
|
const healthInterval = setInterval(() => {
|
|
48
80
|
reportToWolverine("claw_heartbeat", {
|
|
49
81
|
uptime: process.uptime(),
|
|
50
82
|
memory: process.memoryUsage(),
|
|
83
|
+
errorCounts: { ..._errorCounts },
|
|
51
84
|
timestamp: Date.now(),
|
|
52
85
|
});
|
|
53
86
|
}, 30000);
|
|
54
87
|
|
|
55
|
-
// Cleanup on shutdown
|
|
56
88
|
if (gateway.on) {
|
|
57
89
|
gateway.on("shutdown", () => clearInterval(healthInterval));
|
|
58
90
|
}
|
|
59
91
|
}
|
|
60
92
|
|
|
93
|
+
// ── Plugin SDK Hooks ────────────────────────────────────────────
|
|
94
|
+
|
|
61
95
|
/**
|
|
62
|
-
*
|
|
96
|
+
* Get the Plugin SDK API from the gateway (if available).
|
|
63
97
|
*/
|
|
98
|
+
function _getPluginApi(gateway) {
|
|
99
|
+
// OpenClaw exposes plugin registration via multiple paths
|
|
100
|
+
if (gateway.pluginApi) return gateway.pluginApi;
|
|
101
|
+
if (gateway.api) return gateway.api;
|
|
102
|
+
if (gateway.plugins?.api) return gateway.plugins.api;
|
|
103
|
+
if (typeof gateway.registerPluginHook === "function") return gateway;
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Register all wolverine hooks into the OpenClaw Plugin SDK.
|
|
109
|
+
*/
|
|
110
|
+
function _registerPluginHooks(api, projectRoot, config) {
|
|
111
|
+
const registerHook = api.registerPluginHook || api.register;
|
|
112
|
+
if (typeof registerHook !== "function") return;
|
|
113
|
+
|
|
114
|
+
const reg = (name, handler, priority) => {
|
|
115
|
+
try {
|
|
116
|
+
registerHook.call(api, name, { handler, priority: priority || 100 });
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// Hook not supported in this OpenClaw version — skip silently
|
|
119
|
+
try {
|
|
120
|
+
registerHook.call(api, name, handler);
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ── after_tool_call: catch skill and tool failures ──────────
|
|
126
|
+
reg("after_tool_call", (event, ctx) => {
|
|
127
|
+
if (!event) return;
|
|
128
|
+
const hasError = event.error || event.status === "error" || event.exitCode;
|
|
129
|
+
|
|
130
|
+
if (hasError) {
|
|
131
|
+
const toolName = event.toolName || event.name || "unknown";
|
|
132
|
+
const errorMsg = event.error?.message || event.error || event.stderr || "Tool failed";
|
|
133
|
+
const entry = _trackError("tool", `Tool ${toolName} failed: ${errorMsg}`);
|
|
134
|
+
|
|
135
|
+
console.error(`[CLAW] Tool error: ${toolName} — ${errorMsg.slice(0, 100)}`);
|
|
136
|
+
|
|
137
|
+
// Skill errors are wrapped as tool errors — detect them
|
|
138
|
+
const isSkill = toolName.includes("skill") || event.source === "skill";
|
|
139
|
+
|
|
140
|
+
reportToWolverine("route_error", {
|
|
141
|
+
path: `claw://tool/${toolName}`,
|
|
142
|
+
method: isSkill ? "SKILL" : "TOOL",
|
|
143
|
+
statusCode: 500,
|
|
144
|
+
message: `${isSkill ? "Skill" : "Tool"} ${toolName}: ${errorMsg}`.slice(0, 500),
|
|
145
|
+
stack: event.error?.stack || event.stderr || null,
|
|
146
|
+
file: event.file || null,
|
|
147
|
+
line: null,
|
|
148
|
+
timestamp: Date.now(),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── agent_end: catch agent failures and timeouts ────────────
|
|
154
|
+
reg("agent_end", (event, ctx) => {
|
|
155
|
+
if (!event) return;
|
|
156
|
+
const hasError = event.error || event.status === "error" || event.lifecycle === "error";
|
|
157
|
+
|
|
158
|
+
if (hasError) {
|
|
159
|
+
const reason = event.reason || event.error?.message || "Agent failed";
|
|
160
|
+
const isTimeout = reason.includes("timeout") || event.reason === "timeout";
|
|
161
|
+
const entry = _trackError("agent", `Agent ${isTimeout ? "timeout" : "error"}: ${reason}`);
|
|
162
|
+
|
|
163
|
+
console.error(`[CLAW] Agent ${isTimeout ? "timeout" : "error"}: ${reason.slice(0, 100)}`);
|
|
164
|
+
|
|
165
|
+
reportToWolverine("route_error", {
|
|
166
|
+
path: "claw://agent",
|
|
167
|
+
method: isTimeout ? "TIMEOUT" : "AGENT",
|
|
168
|
+
statusCode: isTimeout ? 408 : 500,
|
|
169
|
+
message: reason.slice(0, 500),
|
|
170
|
+
stack: event.error?.stack || null,
|
|
171
|
+
file: null,
|
|
172
|
+
line: null,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ── before_tool_call: detect tool loops ─────────────────────
|
|
179
|
+
reg("before_tool_call", (event, ctx) => {
|
|
180
|
+
if (!event) return;
|
|
181
|
+
|
|
182
|
+
// OpenClaw's loop detection may have already flagged this
|
|
183
|
+
if (event.block || event.loopDetected) {
|
|
184
|
+
const toolName = event.toolName || event.name || "unknown";
|
|
185
|
+
const reason = event.blockReason || event.reason || "Loop detected";
|
|
186
|
+
_trackError("tool", `Tool loop blocked: ${toolName} — ${reason}`);
|
|
187
|
+
|
|
188
|
+
console.warn(`[CLAW] Tool loop blocked: ${toolName} — ${reason}`);
|
|
189
|
+
|
|
190
|
+
reportToWolverine("claw_warning", {
|
|
191
|
+
category: "tool_loop",
|
|
192
|
+
tool: toolName,
|
|
193
|
+
reason,
|
|
194
|
+
timestamp: Date.now(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ── message_sent: catch channel send failures ───────────────
|
|
200
|
+
reg("message_sent", (event, ctx) => {
|
|
201
|
+
if (!event) return;
|
|
202
|
+
const failed = event.success === false || event.error;
|
|
203
|
+
|
|
204
|
+
if (failed) {
|
|
205
|
+
const channel = event.channelId || event.channel || "unknown";
|
|
206
|
+
const errorMsg = event.error?.message || event.error || "Send failed";
|
|
207
|
+
_trackError("channel", `Channel ${channel}: ${errorMsg}`);
|
|
208
|
+
|
|
209
|
+
console.error(`[CLAW] Channel send failed: ${channel} — ${errorMsg.slice(0, 100)}`);
|
|
210
|
+
|
|
211
|
+
reportToWolverine("claw_warning", {
|
|
212
|
+
category: "channel_failure",
|
|
213
|
+
channel,
|
|
214
|
+
error: errorMsg.slice(0, 500),
|
|
215
|
+
timestamp: Date.now(),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ── llm_output: catch provider errors and failovers ─────────
|
|
221
|
+
reg("llm_output", (event, ctx) => {
|
|
222
|
+
if (!event) return;
|
|
223
|
+
const hasError = event.error || event.failover;
|
|
224
|
+
|
|
225
|
+
if (hasError) {
|
|
226
|
+
const provider = event.provider || event.model || "unknown";
|
|
227
|
+
const reason = event.failoverReason || event.error?.message || event.error || "LLM error";
|
|
228
|
+
const status = event.httpCode || event.status;
|
|
229
|
+
_trackError("llm", `LLM ${provider}: ${reason} (${status || "?"})`);
|
|
230
|
+
|
|
231
|
+
// Billing errors (402) are critical — stop healing
|
|
232
|
+
const isBilling = status === 402 || reason.includes("billing");
|
|
233
|
+
// Rate limits (429) are transient
|
|
234
|
+
const isRateLimit = status === 429 || reason.includes("rate_limit");
|
|
235
|
+
|
|
236
|
+
if (isBilling) {
|
|
237
|
+
console.error(`[CLAW] BILLING ERROR: ${provider} — ${reason}`);
|
|
238
|
+
reportToWolverine("claw_critical", {
|
|
239
|
+
category: "billing",
|
|
240
|
+
provider,
|
|
241
|
+
error: reason.slice(0, 500),
|
|
242
|
+
timestamp: Date.now(),
|
|
243
|
+
});
|
|
244
|
+
} else if (!isRateLimit) {
|
|
245
|
+
// Don't spam wolverine with rate limit warnings
|
|
246
|
+
console.warn(`[CLAW] LLM error: ${provider} — ${reason.slice(0, 100)}`);
|
|
247
|
+
reportToWolverine("claw_warning", {
|
|
248
|
+
category: "llm_error",
|
|
249
|
+
provider,
|
|
250
|
+
status,
|
|
251
|
+
error: reason.slice(0, 500),
|
|
252
|
+
timestamp: Date.now(),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── subagent_ended: catch subagent failures ─────────────────
|
|
259
|
+
reg("subagent_ended", (event, ctx) => {
|
|
260
|
+
if (!event) return;
|
|
261
|
+
const failed = event.status === "error" || event.status === "timeout" || event.error;
|
|
262
|
+
|
|
263
|
+
if (failed) {
|
|
264
|
+
const agentId = event.subagentId || event.runId || "unknown";
|
|
265
|
+
const errorMsg = event.error?.message || event.error || `Subagent ${event.status}`;
|
|
266
|
+
_trackError("subagent", `Subagent ${agentId}: ${errorMsg}`);
|
|
267
|
+
|
|
268
|
+
console.error(`[CLAW] Subagent failed: ${agentId} — ${errorMsg.slice(0, 100)}`);
|
|
269
|
+
|
|
270
|
+
reportToWolverine("route_error", {
|
|
271
|
+
path: `claw://subagent/${agentId}`,
|
|
272
|
+
method: "SUBAGENT",
|
|
273
|
+
statusCode: 500,
|
|
274
|
+
message: errorMsg.slice(0, 500),
|
|
275
|
+
stack: event.error?.stack || null,
|
|
276
|
+
file: null,
|
|
277
|
+
line: null,
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// ── gateway_start: lifecycle tracking ───────────────────────
|
|
284
|
+
reg("gateway_start", (event, ctx) => {
|
|
285
|
+
console.log("[CLAW] OpenClaw gateway started — wolverine watching");
|
|
286
|
+
reportToWolverine("claw_health", {
|
|
287
|
+
status: "running",
|
|
288
|
+
detail: "gateway_start",
|
|
289
|
+
timestamp: Date.now(),
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ── gateway_stop: lifecycle tracking ────────────────────────
|
|
294
|
+
reg("gateway_stop", (event, ctx) => {
|
|
295
|
+
console.log("[CLAW] OpenClaw gateway stopping");
|
|
296
|
+
reportToWolverine("claw_health", {
|
|
297
|
+
status: "stopping",
|
|
298
|
+
detail: "gateway_stop",
|
|
299
|
+
errorCounts: { ..._errorCounts },
|
|
300
|
+
timestamp: Date.now(),
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ── session_end: track session errors ───────────────────────
|
|
305
|
+
reg("session_end", (event, ctx) => {
|
|
306
|
+
if (event?.error) {
|
|
307
|
+
console.warn(`[CLAW] Session error: ${event.error?.message || event.error}`);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── EventEmitter fallback hooks ─────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Register basic EventEmitter hooks (works with all versions).
|
|
316
|
+
*/
|
|
317
|
+
function _registerEventHooks(gateway, projectRoot) {
|
|
318
|
+
if (!gateway.on) return;
|
|
319
|
+
|
|
320
|
+
gateway.on("error", (err) => {
|
|
321
|
+
const msg = err?.message || String(err);
|
|
322
|
+
_trackError("agent", `Gateway error: ${msg}`);
|
|
323
|
+
console.error(`[CLAW] Gateway error: ${msg}`);
|
|
324
|
+
|
|
325
|
+
reportToWolverine("route_error", {
|
|
326
|
+
path: "claw://gateway",
|
|
327
|
+
method: "INTERNAL",
|
|
328
|
+
statusCode: 500,
|
|
329
|
+
message: msg.slice(0, 500),
|
|
330
|
+
stack: err?.stack || null,
|
|
331
|
+
file: null,
|
|
332
|
+
line: null,
|
|
333
|
+
timestamp: Date.now(),
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// These may or may not exist depending on OpenClaw version
|
|
338
|
+
const optionalEvents = ["agent:error", "skill:error", "channel:error", "channel:disconnect"];
|
|
339
|
+
for (const evt of optionalEvents) {
|
|
340
|
+
try {
|
|
341
|
+
gateway.on(evt, (err) => {
|
|
342
|
+
const msg = err?.message || String(err);
|
|
343
|
+
const category = evt.split(":")[0];
|
|
344
|
+
_trackError(category === "skill" ? "tool" : "agent", `${evt}: ${msg}`);
|
|
345
|
+
console.error(`[CLAW] ${evt}: ${msg.slice(0, 100)}`);
|
|
346
|
+
});
|
|
347
|
+
} catch {}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Tool registration ───────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
function _registerTools(gateway, tools) {
|
|
354
|
+
if (gateway.registerTools) {
|
|
355
|
+
gateway.registerTools(PLUGIN_NAME, tools);
|
|
356
|
+
} else if (gateway.addTools) {
|
|
357
|
+
gateway.addTools(tools);
|
|
358
|
+
} else if (gateway.skills?.register) {
|
|
359
|
+
gateway.skills.register(PLUGIN_NAME, {
|
|
360
|
+
description: "Wolverine self-healing integration — backup, brain, health, and error pipeline",
|
|
361
|
+
tools,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Wolverine tools for the agent ───────────────────────────────
|
|
367
|
+
|
|
64
368
|
function buildWolverineTools(projectRoot, config) {
|
|
65
369
|
return [
|
|
66
370
|
{
|
|
@@ -163,7 +467,7 @@ function buildWolverineTools(projectRoot, config) {
|
|
|
163
467
|
},
|
|
164
468
|
{
|
|
165
469
|
name: "wolverine_health",
|
|
166
|
-
description: "Get wolverine system health status
|
|
470
|
+
description: "Get wolverine system health status including error tracking from all OpenClaw subsystems",
|
|
167
471
|
parameters: { type: "object", properties: {} },
|
|
168
472
|
execute: async () => {
|
|
169
473
|
const status = {
|
|
@@ -171,6 +475,8 @@ function buildWolverineTools(projectRoot, config) {
|
|
|
171
475
|
memory: `${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`,
|
|
172
476
|
pid: process.pid,
|
|
173
477
|
nodeVersion: process.version,
|
|
478
|
+
errors: { ..._errorCounts },
|
|
479
|
+
recentErrors: _recentErrors.slice(-5).map(e => `[${e.category}] ${e.summary}`),
|
|
174
480
|
};
|
|
175
481
|
|
|
176
482
|
try {
|
|
@@ -211,7 +517,6 @@ function buildWolverineTools(projectRoot, config) {
|
|
|
211
517
|
required: ["error"],
|
|
212
518
|
},
|
|
213
519
|
execute: async ({ error, file }) => {
|
|
214
|
-
// Report to parent wolverine process for healing
|
|
215
520
|
reportToWolverine("route_error", {
|
|
216
521
|
path: "claw://agent",
|
|
217
522
|
method: "INTERNAL",
|
|
@@ -225,50 +530,20 @@ function buildWolverineTools(projectRoot, config) {
|
|
|
225
530
|
return "Error reported to wolverine healing pipeline.";
|
|
226
531
|
},
|
|
227
532
|
},
|
|
533
|
+
{
|
|
534
|
+
name: "wolverine_error_stats",
|
|
535
|
+
description: "Get error statistics from all OpenClaw subsystems (tools, agent, channels, LLM, subagents)",
|
|
536
|
+
parameters: { type: "object", properties: {} },
|
|
537
|
+
execute: async () => {
|
|
538
|
+
return JSON.stringify({
|
|
539
|
+
counts: { ..._errorCounts },
|
|
540
|
+
recent: _recentErrors.slice(-10),
|
|
541
|
+
}, null, 2);
|
|
542
|
+
},
|
|
543
|
+
},
|
|
228
544
|
];
|
|
229
545
|
}
|
|
230
546
|
|
|
231
|
-
/**
|
|
232
|
-
* Handle gateway errors — report to wolverine for healing.
|
|
233
|
-
*/
|
|
234
|
-
function handleGatewayError(err, projectRoot) {
|
|
235
|
-
console.error(`[CLAW] Gateway error: ${err.message}`);
|
|
236
|
-
reportToWolverine("route_error", {
|
|
237
|
-
path: "claw://gateway",
|
|
238
|
-
method: "INTERNAL",
|
|
239
|
-
statusCode: 500,
|
|
240
|
-
message: err.message,
|
|
241
|
-
stack: err.stack,
|
|
242
|
-
file: null,
|
|
243
|
-
line: null,
|
|
244
|
-
timestamp: Date.now(),
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Handle agent errors.
|
|
250
|
-
*/
|
|
251
|
-
function handleAgentError(err, projectRoot) {
|
|
252
|
-
console.error(`[CLAW] Agent error: ${err.message}`);
|
|
253
|
-
reportToWolverine("route_error", {
|
|
254
|
-
path: "claw://agent",
|
|
255
|
-
method: "INTERNAL",
|
|
256
|
-
statusCode: 500,
|
|
257
|
-
message: err.message,
|
|
258
|
-
stack: err.stack,
|
|
259
|
-
file: null,
|
|
260
|
-
line: null,
|
|
261
|
-
timestamp: Date.now(),
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Handle skill errors.
|
|
267
|
-
*/
|
|
268
|
-
function handleSkillError(err, projectRoot) {
|
|
269
|
-
console.warn(`[CLAW] Skill error (non-fatal): ${err.message}`);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
547
|
/**
|
|
273
548
|
* Report to wolverine parent process via IPC.
|
|
274
549
|
*/
|