wolverine-ai 6.2.2 → 6.3.0
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 +1 -1
- package/src/backup/backup-manager.js +32 -4
- package/src/claw/setup.js +99 -3
- package/src/claw/standalone-agent.js +107 -312
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
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
|
}
|
package/src/claw/setup.js
CHANGED
|
@@ -522,6 +522,79 @@ function ensureEnvFile(cwd, env) {
|
|
|
522
522
|
return result;
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Ensure wolverine-ai is installed as a dependency.
|
|
527
|
+
* Skips if we're running from the wolverine repo itself.
|
|
528
|
+
*/
|
|
529
|
+
function ensureWolverineDep(cwd) {
|
|
530
|
+
// If we're inside the wolverine repo, skip
|
|
531
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
532
|
+
try {
|
|
533
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
534
|
+
if (pkg.name === "wolverine-ai") {
|
|
535
|
+
return { installed: true, isRepo: true, alreadyPresent: true };
|
|
536
|
+
}
|
|
537
|
+
// Already a dependency?
|
|
538
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
539
|
+
if (deps["wolverine-ai"]) {
|
|
540
|
+
return { installed: true, alreadyPresent: true };
|
|
541
|
+
}
|
|
542
|
+
} catch {}
|
|
543
|
+
|
|
544
|
+
// Install it
|
|
545
|
+
if (!fs.existsSync(pkgPath)) {
|
|
546
|
+
return { installed: false, reason: "no package.json" };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
console.log(chalk.gray(" Installing wolverine-ai..."));
|
|
551
|
+
execSync("npm install wolverine-ai@latest 2>&1", {
|
|
552
|
+
cwd,
|
|
553
|
+
encoding: "utf-8",
|
|
554
|
+
timeout: 120000,
|
|
555
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
556
|
+
});
|
|
557
|
+
return { installed: true, alreadyPresent: false };
|
|
558
|
+
} catch (err) {
|
|
559
|
+
return { installed: false, reason: `npm install failed: ${err.message?.split("\n")[0]}` };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Add claw-related npm scripts to the user's package.json.
|
|
565
|
+
*/
|
|
566
|
+
function addClawScripts(cwd) {
|
|
567
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
568
|
+
if (!fs.existsSync(pkgPath)) return { added: 0 };
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
572
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
573
|
+
|
|
574
|
+
const toAdd = {
|
|
575
|
+
"claw": "wolverine-claw",
|
|
576
|
+
"claw:direct": "wolverine-claw --direct",
|
|
577
|
+
"claw:info": "wolverine-claw --info",
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
let added = 0;
|
|
581
|
+
for (const [name, cmd] of Object.entries(toAdd)) {
|
|
582
|
+
if (!pkg.scripts[name]) {
|
|
583
|
+
pkg.scripts[name] = cmd;
|
|
584
|
+
added++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (added > 0) {
|
|
589
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return { added, skipped: added === 0 };
|
|
593
|
+
} catch {
|
|
594
|
+
return { added: 0 };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
525
598
|
/**
|
|
526
599
|
* Ensure openclaw is installed as a dependency.
|
|
527
600
|
*/
|
|
@@ -781,9 +854,21 @@ async function setup(cwd, options = {}) {
|
|
|
781
854
|
|
|
782
855
|
log("");
|
|
783
856
|
|
|
784
|
-
// ── Step 6: Install
|
|
857
|
+
// ── Step 6: Install dependencies ─────────────────────────────
|
|
858
|
+
log(chalk.bold(" Dependencies...\n"));
|
|
859
|
+
|
|
860
|
+
// 6a: Install wolverine-ai itself if not already a dep
|
|
861
|
+
const wolverineDepResult = ensureWolverineDep(cwd);
|
|
862
|
+
if (wolverineDepResult.installed) {
|
|
863
|
+
log(chalk.green(` ✅ wolverine-ai ${wolverineDepResult.alreadyPresent ? "already installed" : "installed"}`));
|
|
864
|
+
} else if (wolverineDepResult.isRepo) {
|
|
865
|
+
log(chalk.gray(" ○ wolverine-ai (running from repo)"));
|
|
866
|
+
} else {
|
|
867
|
+
log(chalk.yellow(` ⚠️ wolverine-ai: ${wolverineDepResult.reason}`));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// 6b: Install openclaw if not already present
|
|
785
871
|
if (!env.openclaw.localInstall) {
|
|
786
|
-
log(chalk.bold(" Dependencies...\n"));
|
|
787
872
|
const depResult = ensureOpenClawDep(cwd);
|
|
788
873
|
if (depResult.installed) {
|
|
789
874
|
log(chalk.green(` ✅ openclaw ${depResult.alreadyPresent ? "already installed" : "installed"}`));
|
|
@@ -793,9 +878,18 @@ async function setup(cwd, options = {}) {
|
|
|
793
878
|
log(chalk.gray(` ${depResult.fallback}`));
|
|
794
879
|
}
|
|
795
880
|
}
|
|
796
|
-
log("");
|
|
797
881
|
}
|
|
798
882
|
|
|
883
|
+
// 6c: Add claw npm scripts to user's package.json
|
|
884
|
+
const scriptsResult = addClawScripts(cwd);
|
|
885
|
+
if (scriptsResult.added > 0) {
|
|
886
|
+
log(chalk.green(` ✅ Added ${scriptsResult.added} npm scripts (claw, claw:info, claw:direct)`));
|
|
887
|
+
} else if (scriptsResult.skipped) {
|
|
888
|
+
log(chalk.gray(" ○ npm scripts already present"));
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
log("");
|
|
892
|
+
|
|
799
893
|
// ── Step 7: Validate ───────────────────────────────────────
|
|
800
894
|
log(chalk.bold(" Validating...\n"));
|
|
801
895
|
|
|
@@ -924,4 +1018,6 @@ module.exports = {
|
|
|
924
1018
|
validate,
|
|
925
1019
|
ensureEnvFile,
|
|
926
1020
|
ensureOpenClawDep,
|
|
1021
|
+
ensureWolverineDep,
|
|
1022
|
+
addClawScripts,
|
|
927
1023
|
};
|
|
@@ -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 };
|