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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "6.2.2",
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 <= 10 * 1024 * 1024) files.push(fullPath); } catch {}
368
+ try { if (fs.statSync(fullPath).size <= maxFileSize) files.push(fullPath); } catch {}
369
369
  }
370
370
  }
371
371
  };
372
- walk(serverDir);
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 OpenClaw ────────────────────────────────
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 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.
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
- const tools = buildTools(cwd, workspacePath, config);
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
- const systemPrompt = `You are Wolverine Claw, an agentic AI coding assistant running inside the Wolverine self-healing framework.
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
- You have access to tools for reading, writing, editing files, searching code, and running shell commands. You operate in the project at: ${cwd}
82
+ const systemPrompt = `You are Wolverine Claw, an agentic AI coding assistant running inside the Wolverine self-healing framework.
305
83
 
306
- Your workspace for creating new files is: ${workspacePath}
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 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.`;
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: ${tools.length} (read, write, edit, list, glob, grep, bash, done)`));
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: toolDefs,
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 = tool.execute(toolInput);
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, 100)}`));
440
- const toolResult = tool.execute(toolInput);
441
- const displayResult = toolResult.length > 200
442
- ? toolResult.slice(0, 200) + "..."
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
- module.exports = { startRepl, buildTools };
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 };