wolverine-ai 6.1.2 → 6.2.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.
@@ -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 };
@@ -0,0 +1,115 @@
1
+ {
2
+ "_": "Wolverine Claw Configuration — OpenClaw agentic agent powered by Wolverine self-healing. Docs: https://github.com/bobbyswhip/Wolverine",
3
+
4
+ "_gateway": "OpenClaw gateway settings. port: WebSocket control plane port. host: bind address (127.0.0.1 = local only, 0.0.0.0 = all interfaces).",
5
+ "gateway": {
6
+ "port": 18789,
7
+ "host": "127.0.0.1"
8
+ },
9
+
10
+ "_agent": "Pi agent runtime settings. model: default LLM for agent tasks. maxTurns: max tool-use iterations per request. timeoutMs: max time per agent call.",
11
+ "agent": {
12
+ "model": "claude-sonnet-4-6",
13
+ "maxTurns": 25,
14
+ "timeoutMs": 120000
15
+ },
16
+
17
+ "_models": "AI models for different claw tasks. Provider auto-detected from name (claude-* = Anthropic, gpt-* = OpenAI).",
18
+ "models": {
19
+ "reasoning": "claude-sonnet-4-6",
20
+ "coding": "claude-sonnet-4-6",
21
+ "chat": "claude-sonnet-4-6",
22
+ "embedding": "text-embedding-3-small"
23
+ },
24
+
25
+ "_channels": "Messaging channel integrations. Enable channels by adding their config. Each channel needs its own API keys in .env.local.",
26
+ "channels": {
27
+ "terminal": {
28
+ "enabled": true
29
+ },
30
+ "discord": {
31
+ "enabled": false,
32
+ "token": "",
33
+ "allowedChannels": []
34
+ },
35
+ "slack": {
36
+ "enabled": false,
37
+ "botToken": "",
38
+ "appToken": "",
39
+ "allowedChannels": []
40
+ },
41
+ "telegram": {
42
+ "enabled": false,
43
+ "botToken": "",
44
+ "allowedUsers": []
45
+ },
46
+ "whatsapp": {
47
+ "enabled": false
48
+ }
49
+ },
50
+
51
+ "_healing": "Wolverine self-healing settings for the claw process. Same pipeline as server healing.",
52
+ "healing": {
53
+ "enabled": true,
54
+ "healTimeoutMs": 300000,
55
+ "maxHealsPerWindow": 5,
56
+ "windowMs": 300000,
57
+ "loopMaxAttempts": 3,
58
+ "loopWindowMs": 600000
59
+ },
60
+
61
+ "_skills": "OpenClaw skills configuration. Enable/disable built-in and custom skills.",
62
+ "skills": {
63
+ "codingAgent": {
64
+ "enabled": true,
65
+ "defaultAgent": "pi",
66
+ "sandbox": true,
67
+ "allowedPaths": ["wolverine-claw/workspace/"]
68
+ },
69
+ "browserControl": {
70
+ "enabled": false
71
+ },
72
+ "cron": {
73
+ "enabled": true,
74
+ "maxJobs": 10
75
+ },
76
+ "canvas": {
77
+ "enabled": false
78
+ }
79
+ },
80
+
81
+ "_workspace": "Working directory for the claw agent. Files created/modified by the agent go here. Protected from wolverine auto-update.",
82
+ "workspace": {
83
+ "path": "wolverine-claw/workspace",
84
+ "maxFileSizeMB": 50,
85
+ "allowedExtensions": ["*"]
86
+ },
87
+
88
+ "_security": "Security settings for the claw agent. dmPairing: require pairing for unknown senders. sandbox: restrict agent file access.",
89
+ "security": {
90
+ "dmPairing": true,
91
+ "sandbox": true,
92
+ "blockedCommands": ["rm -rf /", "format", "shutdown", "reboot"],
93
+ "maxConcurrentSessions": 5
94
+ },
95
+
96
+ "_logging": "Logging configuration.",
97
+ "logging": {
98
+ "level": "info",
99
+ "logFile": ".wolverine/claw.log",
100
+ "maxLogSizeMB": 50
101
+ },
102
+
103
+ "_remoteAccess": "Remote access via Tailscale or SSH tunnel. Disabled by default for security.",
104
+ "remoteAccess": {
105
+ "enabled": false,
106
+ "method": "tailscale"
107
+ },
108
+
109
+ "_backup": "Backup settings for claw workspace and config.",
110
+ "backup": {
111
+ "enabled": true,
112
+ "stabilityMs": 1800000,
113
+ "retentionDays": 7
114
+ }
115
+ }