wolverine-ai 6.2.0 → 6.2.2
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/bin/wolverine-claw.js
CHANGED
|
@@ -131,10 +131,12 @@ if (args.includes("--info")) {
|
|
|
131
131
|
} catch {
|
|
132
132
|
try {
|
|
133
133
|
const { execSync } = require("child_process");
|
|
134
|
-
const ver = execSync("npx openclaw --version
|
|
134
|
+
const ver = execSync("npx --yes openclaw --version", {
|
|
135
|
+
encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"],
|
|
136
|
+
}).trim();
|
|
135
137
|
openclawVersion = ver || "available via npx";
|
|
136
138
|
} catch {
|
|
137
|
-
openclawVersion = "not installed (
|
|
139
|
+
openclawVersion = "not installed (standalone agent mode)";
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
console.log(chalk.gray(` OpenClaw: ${openclawVersion}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.2",
|
|
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": {
|
|
@@ -65,11 +65,10 @@
|
|
|
65
65
|
"dotenv": "^16.4.7",
|
|
66
66
|
"fastify": "^5.8.4",
|
|
67
67
|
"openai": "^4.73.0",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
68
|
+
"viem": "^2.0.0",
|
|
69
|
+
"x402": "^1.1.0"
|
|
70
70
|
},
|
|
71
71
|
"optionalDependencies": {
|
|
72
|
-
"openclaw": "^1.0.0",
|
|
73
72
|
"@coinbase/cdp-sdk": "^1.46.1",
|
|
74
73
|
"@privy-io/server-auth": "1.14.0",
|
|
75
74
|
"@x402/core": "^2.9.0",
|
|
@@ -77,6 +76,7 @@
|
|
|
77
76
|
"better-sqlite3": "^11.0.0",
|
|
78
77
|
"ethers": "^6.0.0",
|
|
79
78
|
"ioredis": "^5.0.0",
|
|
79
|
+
"openclaw": "^1.0.0",
|
|
80
80
|
"pg": "^8.0.0",
|
|
81
81
|
"stripe": "^18.0.0"
|
|
82
82
|
},
|
package/src/claw/setup.js
CHANGED
|
@@ -247,19 +247,41 @@ function detectWolverine(cwd) {
|
|
|
247
247
|
brainExists: fs.existsSync(path.join(cwd, ".wolverine", "brain")),
|
|
248
248
|
};
|
|
249
249
|
|
|
250
|
+
// Check if we're inside the wolverine repo itself
|
|
250
251
|
try {
|
|
251
252
|
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
252
|
-
if (pkg.name === "wolverine-ai"
|
|
253
|
+
if (pkg.name === "wolverine-ai") {
|
|
253
254
|
result.installed = true;
|
|
254
|
-
result.version = pkg.version
|
|
255
|
+
result.version = pkg.version;
|
|
256
|
+
return result;
|
|
255
257
|
}
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
+
// Check if wolverine-ai is a dependency
|
|
259
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.optionalDependencies };
|
|
260
|
+
if (deps["wolverine-ai"]) {
|
|
258
261
|
result.installed = true;
|
|
259
|
-
result.version =
|
|
262
|
+
result.version = deps["wolverine-ai"];
|
|
260
263
|
}
|
|
261
264
|
} catch {}
|
|
262
265
|
|
|
266
|
+
// Check if wolverine.js exists in src/ (repo checkout)
|
|
267
|
+
if (fs.existsSync(path.join(cwd, "src", "core", "wolverine.js"))) {
|
|
268
|
+
result.installed = true;
|
|
269
|
+
try {
|
|
270
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
|
|
271
|
+
result.version = pkg.version;
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check node_modules
|
|
276
|
+
if (!result.installed) {
|
|
277
|
+
try {
|
|
278
|
+
const pkgPath = require.resolve("wolverine-ai/package.json", { paths: [cwd] });
|
|
279
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
280
|
+
result.installed = true;
|
|
281
|
+
result.version = pkg.version;
|
|
282
|
+
} catch {}
|
|
283
|
+
}
|
|
284
|
+
|
|
263
285
|
return result;
|
|
264
286
|
}
|
|
265
287
|
|
|
@@ -325,13 +347,48 @@ function mergeConfig(openclawConfig, defaults) {
|
|
|
325
347
|
|
|
326
348
|
// ── Scaffolding ─────────────────────────────────────────────────
|
|
327
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Find the wolverine-ai template directory.
|
|
352
|
+
* Works whether wolverine is the project root OR installed in node_modules.
|
|
353
|
+
*/
|
|
354
|
+
function _findTemplateDir() {
|
|
355
|
+
// 1. Relative to this file (we're in src/claw/setup.js → ../../wolverine-claw/)
|
|
356
|
+
const fromSrc = path.join(__dirname, "..", "..", "wolverine-claw");
|
|
357
|
+
if (fs.existsSync(path.join(fromSrc, "index.js"))) return fromSrc;
|
|
358
|
+
|
|
359
|
+
// 2. Via require.resolve (works when wolverine-ai is a node_modules dep)
|
|
360
|
+
try {
|
|
361
|
+
const pkgPath = require.resolve("wolverine-ai/package.json");
|
|
362
|
+
const pkgDir = path.dirname(pkgPath);
|
|
363
|
+
const fromPkg = path.join(pkgDir, "wolverine-claw");
|
|
364
|
+
if (fs.existsSync(path.join(fromPkg, "index.js"))) return fromPkg;
|
|
365
|
+
} catch {}
|
|
366
|
+
|
|
367
|
+
// 3. Check common node_modules locations
|
|
368
|
+
const candidates = [
|
|
369
|
+
path.join(process.cwd(), "node_modules", "wolverine-ai", "wolverine-claw"),
|
|
370
|
+
path.join(os.homedir(), "node_modules", "wolverine-ai", "wolverine-claw"),
|
|
371
|
+
];
|
|
372
|
+
for (const c of candidates) {
|
|
373
|
+
if (fs.existsSync(path.join(c, "index.js"))) return c;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
328
379
|
/**
|
|
329
380
|
* Scaffold the wolverine-claw directory from template + merged config.
|
|
330
381
|
*/
|
|
331
382
|
function scaffold(cwd, mergedConfig, env) {
|
|
332
383
|
const clawDir = path.join(cwd, "wolverine-claw");
|
|
384
|
+
const templateDir = _findTemplateDir();
|
|
333
385
|
const results = { created: [], skipped: [], errors: [] };
|
|
334
386
|
|
|
387
|
+
if (!templateDir) {
|
|
388
|
+
results.errors.push("Could not find wolverine-ai templates. Is wolverine-ai installed?");
|
|
389
|
+
return results;
|
|
390
|
+
}
|
|
391
|
+
|
|
335
392
|
// Create directories
|
|
336
393
|
const dirs = ["config", "plugins", "workspace", "skills"];
|
|
337
394
|
for (const d of dirs) {
|
|
@@ -352,12 +409,11 @@ function scaffold(cwd, mergedConfig, env) {
|
|
|
352
409
|
}
|
|
353
410
|
|
|
354
411
|
// Copy index.js from template
|
|
355
|
-
const
|
|
356
|
-
if (!fs.existsSync(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
fs.copyFileSync(templateIndex, indexSrc);
|
|
412
|
+
const indexDest = path.join(clawDir, "index.js");
|
|
413
|
+
if (!fs.existsSync(indexDest)) {
|
|
414
|
+
const indexSrc = path.join(templateDir, "index.js");
|
|
415
|
+
if (fs.existsSync(indexSrc)) {
|
|
416
|
+
fs.copyFileSync(indexSrc, indexDest);
|
|
361
417
|
results.created.push("index.js");
|
|
362
418
|
}
|
|
363
419
|
} else {
|
|
@@ -367,15 +423,10 @@ function scaffold(cwd, mergedConfig, env) {
|
|
|
367
423
|
// Copy plugin
|
|
368
424
|
const pluginDest = path.join(clawDir, "plugins", "wolverine-integration.js");
|
|
369
425
|
if (!fs.existsSync(pluginDest)) {
|
|
370
|
-
const pluginSrc = path.join(
|
|
426
|
+
const pluginSrc = path.join(templateDir, "plugins", "wolverine-integration.js");
|
|
371
427
|
if (fs.existsSync(pluginSrc)) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const templatePlugin = path.join(__dirname, "..", "..", "wolverine-claw", "plugins", "wolverine-integration.js");
|
|
375
|
-
if (fs.existsSync(templatePlugin)) {
|
|
376
|
-
fs.copyFileSync(templatePlugin, pluginDest);
|
|
377
|
-
results.created.push("plugins/wolverine-integration.js");
|
|
378
|
-
}
|
|
428
|
+
fs.copyFileSync(pluginSrc, pluginDest);
|
|
429
|
+
results.created.push("plugins/wolverine-integration.js");
|
|
379
430
|
}
|
|
380
431
|
} else {
|
|
381
432
|
results.skipped.push("plugins/wolverine-integration.js (already exists)");
|
|
@@ -511,12 +562,17 @@ function ensureOpenClawDep(cwd) {
|
|
|
511
562
|
function validate(cwd, env) {
|
|
512
563
|
const checks = [];
|
|
513
564
|
|
|
514
|
-
// Node version
|
|
565
|
+
// Node version — not critical since standalone agent works on Node 20+
|
|
566
|
+
const nodeMinOk = env.node.major >= 20;
|
|
515
567
|
checks.push({
|
|
516
|
-
name: "Node.js
|
|
517
|
-
pass:
|
|
518
|
-
detail: env.node.ok
|
|
519
|
-
|
|
568
|
+
name: "Node.js",
|
|
569
|
+
pass: nodeMinOk,
|
|
570
|
+
detail: env.node.ok
|
|
571
|
+
? `${env.node.version} (full OpenClaw support)`
|
|
572
|
+
: nodeMinOk
|
|
573
|
+
? `${env.node.version} (standalone agent mode — upgrade to 22+ for OpenClaw)`
|
|
574
|
+
: `${env.node.version} (need >= 20)`,
|
|
575
|
+
critical: !nodeMinOk,
|
|
520
576
|
});
|
|
521
577
|
|
|
522
578
|
// OpenClaw available
|
|
@@ -646,10 +702,10 @@ async function setup(cwd, options = {}) {
|
|
|
646
702
|
|
|
647
703
|
log("");
|
|
648
704
|
|
|
649
|
-
// ── Step 2:
|
|
705
|
+
// ── Step 2: Node version check ──────────────────────────────
|
|
650
706
|
if (!env.node.ok) {
|
|
651
|
-
log(chalk.
|
|
652
|
-
|
|
707
|
+
log(chalk.yellow(" ⚠️ Node.js 22+ recommended (required for OpenClaw multi-channel)."));
|
|
708
|
+
log(chalk.gray(" Standalone agent mode works on Node 20+.\n"));
|
|
653
709
|
}
|
|
654
710
|
|
|
655
711
|
if (dryRun) {
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wolverine Claw Standalone Agent
|
|
3
|
+
*
|
|
4
|
+
* When OpenClaw isn't installed (or Node < 22), this provides a built-in
|
|
5
|
+
* agentic terminal REPL using wolverine's own AI client. Users get an
|
|
6
|
+
* interactive coding agent that can read/write/edit files, run commands,
|
|
7
|
+
* search code, and self-heal — all powered by the same AI pipeline as
|
|
8
|
+
* wolverine's server healing.
|
|
9
|
+
*
|
|
10
|
+
* This makes wolverine-claw work immediately without any external deps,
|
|
11
|
+
* and openclaw becomes an optional enhancement for multi-channel support.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const readline = require("readline");
|
|
17
|
+
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
|
+
|
|
274
|
+
// ── REPL ────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Start the standalone agent REPL.
|
|
278
|
+
*/
|
|
279
|
+
async function startRepl(config, options = {}) {
|
|
280
|
+
const cwd = options.cwd || process.cwd();
|
|
281
|
+
const workspacePath = path.resolve(cwd, config.workspace?.path || "wolverine-claw/workspace");
|
|
282
|
+
const model = config.agent?.model || config.models?.coding || "claude-sonnet-4-6";
|
|
283
|
+
const maxTurns = config.agent?.maxTurns || 25;
|
|
284
|
+
|
|
285
|
+
// Ensure workspace exists
|
|
286
|
+
if (!fs.existsSync(workspacePath)) {
|
|
287
|
+
fs.mkdirSync(workspacePath, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Load AI client
|
|
291
|
+
let aiCallWithHistory;
|
|
292
|
+
try {
|
|
293
|
+
const aiClient = require("../core/ai-client");
|
|
294
|
+
aiCallWithHistory = aiClient.aiCallWithHistory;
|
|
295
|
+
} catch (e) {
|
|
296
|
+
console.error(chalk.red(` [CLAW] Failed to load AI client: ${e.message}`));
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const tools = buildTools(cwd, workspacePath, config);
|
|
301
|
+
|
|
302
|
+
const systemPrompt = `You are Wolverine Claw, an agentic AI coding assistant running inside the Wolverine self-healing framework.
|
|
303
|
+
|
|
304
|
+
You have access to tools for reading, writing, editing files, searching code, and running shell commands. You operate in the project at: ${cwd}
|
|
305
|
+
|
|
306
|
+
Your workspace for creating new files is: ${workspacePath}
|
|
307
|
+
|
|
308
|
+
Guidelines:
|
|
309
|
+
- Read files before editing them. Understand existing code before making changes.
|
|
310
|
+
- Use edit_file for surgical changes, write_file for new files.
|
|
311
|
+
- Use grep_code and glob_files to explore the codebase before making assumptions.
|
|
312
|
+
- Use bash_exec for git, npm, and system commands.
|
|
313
|
+
- When done with a task, use the done tool with a summary.
|
|
314
|
+
- Be concise. Fix what's asked, don't add unnecessary changes.
|
|
315
|
+
- Protected paths (read-only): src/, node_modules/, .env files.`;
|
|
316
|
+
|
|
317
|
+
console.log(chalk.blue.bold("\n 🐾 Wolverine Claw — Interactive Agent\n"));
|
|
318
|
+
console.log(chalk.gray(` Model: ${model}`));
|
|
319
|
+
console.log(chalk.gray(` Workspace: ${workspacePath}`));
|
|
320
|
+
console.log(chalk.gray(` Tools: ${tools.length} (read, write, edit, list, glob, grep, bash, done)`));
|
|
321
|
+
console.log(chalk.gray(` Max turns: ${maxTurns}`));
|
|
322
|
+
console.log(chalk.gray(` Type 'exit' or Ctrl+C to quit.\n`));
|
|
323
|
+
|
|
324
|
+
const rl = readline.createInterface({
|
|
325
|
+
input: process.stdin,
|
|
326
|
+
output: process.stdout,
|
|
327
|
+
prompt: chalk.blue(" 🐾 > "),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Conversation history
|
|
331
|
+
let messages = [];
|
|
332
|
+
|
|
333
|
+
rl.prompt();
|
|
334
|
+
|
|
335
|
+
rl.on("line", async (line) => {
|
|
336
|
+
const input = line.trim();
|
|
337
|
+
if (!input) { rl.prompt(); return; }
|
|
338
|
+
if (input === "exit" || input === "quit") {
|
|
339
|
+
console.log(chalk.gray("\n Goodbye.\n"));
|
|
340
|
+
rl.close();
|
|
341
|
+
process.exit(0);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Special commands
|
|
345
|
+
if (input === "/clear") {
|
|
346
|
+
messages = [];
|
|
347
|
+
console.log(chalk.gray(" Conversation cleared.\n"));
|
|
348
|
+
rl.prompt();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (input === "/status") {
|
|
352
|
+
console.log(chalk.gray(` Messages: ${messages.length}, Model: ${model}`));
|
|
353
|
+
rl.prompt();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Add user message
|
|
358
|
+
messages.push({ role: "user", content: input });
|
|
359
|
+
|
|
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
|
+
let turn = 0;
|
|
366
|
+
let done = false;
|
|
367
|
+
|
|
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
|
+
while (turn < maxTurns && !done) {
|
|
379
|
+
turn++;
|
|
380
|
+
try {
|
|
381
|
+
const response = await aiCallWithHistory({
|
|
382
|
+
model,
|
|
383
|
+
messages: [{ role: "system", content: systemPrompt }, ...messages],
|
|
384
|
+
tools: toolDefs,
|
|
385
|
+
maxTokens: 4096,
|
|
386
|
+
category: "tool",
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Extract from OpenAI-shaped response
|
|
390
|
+
const msg = response.choices?.[0]?.message;
|
|
391
|
+
if (!msg) {
|
|
392
|
+
console.log(chalk.yellow(" (empty response)\n"));
|
|
393
|
+
done = true;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const textContent = msg.content || "";
|
|
398
|
+
const toolCalls = msg.tool_calls || [];
|
|
399
|
+
|
|
400
|
+
// Handle text content
|
|
401
|
+
if (textContent) {
|
|
402
|
+
console.log(chalk.white(`\n ${textContent.replace(/\n/g, "\n ")}\n`));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Handle tool calls
|
|
406
|
+
if (toolCalls.length > 0) {
|
|
407
|
+
// Store assistant message with tool calls
|
|
408
|
+
messages.push({
|
|
409
|
+
role: "assistant",
|
|
410
|
+
content: textContent || null,
|
|
411
|
+
tool_calls: toolCalls,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
for (const tc of toolCalls) {
|
|
415
|
+
const toolName = tc.function?.name || tc.name;
|
|
416
|
+
let toolInput = {};
|
|
417
|
+
try {
|
|
418
|
+
toolInput = typeof tc.function?.arguments === "string"
|
|
419
|
+
? JSON.parse(tc.function.arguments)
|
|
420
|
+
: (tc.function?.arguments || tc.input || {});
|
|
421
|
+
} catch { toolInput = {}; }
|
|
422
|
+
|
|
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
|
+
if (toolName === "done") {
|
|
432
|
+
const summary = tool.execute(toolInput);
|
|
433
|
+
console.log(chalk.green(` ✅ ${summary}\n`));
|
|
434
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: summary });
|
|
435
|
+
done = true;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
console.log(chalk.gray(` [${toolName}] ${JSON.stringify(toolInput).slice(0, 100)}`));
|
|
440
|
+
const toolResult = tool.execute(toolInput);
|
|
441
|
+
const displayResult = toolResult.length > 200
|
|
442
|
+
? toolResult.slice(0, 200) + "..."
|
|
443
|
+
: toolResult;
|
|
444
|
+
console.log(chalk.gray(` → ${displayResult.replace(/\n/g, "\n ")}`));
|
|
445
|
+
|
|
446
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
|
|
447
|
+
}
|
|
448
|
+
} else if (textContent) {
|
|
449
|
+
// Text-only response, no tool calls — done for this turn
|
|
450
|
+
messages.push({ role: "assistant", content: textContent });
|
|
451
|
+
done = true;
|
|
452
|
+
} else {
|
|
453
|
+
done = true;
|
|
454
|
+
}
|
|
455
|
+
} catch (err) {
|
|
456
|
+
console.log(chalk.red(` [error] ${err.message}`));
|
|
457
|
+
messages.push({ role: "assistant", content: `Error: ${err.message}` });
|
|
458
|
+
done = true;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (turn >= maxTurns) {
|
|
463
|
+
console.log(chalk.yellow(` ⚠️ Max turns (${maxTurns}) reached.\n`));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
rl.prompt();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
rl.on("close", () => {
|
|
470
|
+
process.exit(0);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Report health to wolverine parent
|
|
474
|
+
if (typeof process.send === "function") {
|
|
475
|
+
try {
|
|
476
|
+
process.send({ type: "claw_health", status: "running", detail: "standalone-agent", timestamp: Date.now() });
|
|
477
|
+
} catch {}
|
|
478
|
+
|
|
479
|
+
// Heartbeat
|
|
480
|
+
setInterval(() => {
|
|
481
|
+
try {
|
|
482
|
+
process.send({ type: "claw_heartbeat", uptime: process.uptime(), memory: process.memoryUsage(), timestamp: Date.now() });
|
|
483
|
+
} catch {}
|
|
484
|
+
}, 30000);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
module.exports = { startRepl, buildTools };
|
package/wolverine-claw/index.js
CHANGED
|
@@ -95,8 +95,8 @@ async function start() {
|
|
|
95
95
|
|
|
96
96
|
} catch (err) {
|
|
97
97
|
if (err.code === "MODULE_NOT_FOUND" && err.message.includes("openclaw")) {
|
|
98
|
-
console.log("[CLAW] OpenClaw not installed,
|
|
99
|
-
await
|
|
98
|
+
console.log("[CLAW] OpenClaw not installed, starting standalone agent...");
|
|
99
|
+
await startStandaloneAgent();
|
|
100
100
|
} else {
|
|
101
101
|
console.error(`[CLAW] Startup failed: ${err.message}`);
|
|
102
102
|
console.error(err.stack);
|
|
@@ -106,6 +106,31 @@ async function start() {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Fallback: start wolverine's built-in standalone agent.
|
|
111
|
+
* No external deps needed — uses wolverine's own AI client + tools.
|
|
112
|
+
*/
|
|
113
|
+
async function startStandaloneAgent() {
|
|
114
|
+
try {
|
|
115
|
+
// Load dotenv for AI keys
|
|
116
|
+
const dotenv = require("dotenv");
|
|
117
|
+
dotenv.config({ path: path.resolve(__dirname, "..", ".env.local") });
|
|
118
|
+
dotenv.config({ path: path.resolve(__dirname, "..", ".env") });
|
|
119
|
+
|
|
120
|
+
const { startRepl } = require("../src/claw/standalone-agent");
|
|
121
|
+
reportHealth("running");
|
|
122
|
+
await startRepl(config, { cwd: path.resolve(__dirname, "..") });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error(`[CLAW] Standalone agent failed: ${err.message}`);
|
|
125
|
+
console.error(err.stack);
|
|
126
|
+
|
|
127
|
+
// Last resort: try CLI mode
|
|
128
|
+
const gatewayPort = config.gateway?.port || 18789;
|
|
129
|
+
const gatewayHost = config.gateway?.host || "127.0.0.1";
|
|
130
|
+
await startClawCLI(gatewayPort, gatewayHost);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
109
134
|
/**
|
|
110
135
|
* Fallback: spawn openclaw gateway via CLI (npx openclaw gateway).
|
|
111
136
|
*/
|
|
File without changes
|