wolverine-ai 1.0.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/PLATFORM.md +442 -0
- package/README.md +475 -0
- package/SERVER_BEST_PRACTICES.md +62 -0
- package/TELEMETRY.md +108 -0
- package/bin/wolverine.js +95 -0
- package/examples/01-basic-typo.js +31 -0
- package/examples/02-multi-file/routes/users.js +15 -0
- package/examples/02-multi-file/server.js +25 -0
- package/examples/03-syntax-error.js +23 -0
- package/examples/04-secret-leak.js +14 -0
- package/examples/05-expired-key.js +27 -0
- package/examples/06-json-config/config.json +13 -0
- package/examples/06-json-config/server.js +28 -0
- package/examples/07-rate-limit-loop.js +11 -0
- package/examples/08-sandbox-escape.js +20 -0
- package/examples/buggy-server.js +39 -0
- package/examples/demos/01-basic-typo/index.js +20 -0
- package/examples/demos/01-basic-typo/routes/api.js +13 -0
- package/examples/demos/01-basic-typo/routes/health.js +4 -0
- package/examples/demos/02-multi-file/index.js +24 -0
- package/examples/demos/02-multi-file/routes/api.js +13 -0
- package/examples/demos/02-multi-file/routes/health.js +4 -0
- package/examples/demos/03-syntax-error/index.js +18 -0
- package/examples/demos/04-secret-leak/index.js +16 -0
- package/examples/demos/05-expired-key/index.js +21 -0
- package/examples/demos/06-json-config/config.json +9 -0
- package/examples/demos/06-json-config/index.js +20 -0
- package/examples/demos/07-null-crash/index.js +16 -0
- package/examples/run-demo.js +110 -0
- package/package.json +67 -0
- package/server/config/settings.json +62 -0
- package/server/index.js +33 -0
- package/server/routes/api.js +12 -0
- package/server/routes/health.js +16 -0
- package/server/routes/time.js +12 -0
- package/src/agent/agent-engine.js +727 -0
- package/src/agent/goal-loop.js +140 -0
- package/src/agent/research-agent.js +120 -0
- package/src/agent/sub-agents.js +176 -0
- package/src/backup/backup-manager.js +321 -0
- package/src/brain/brain.js +315 -0
- package/src/brain/embedder.js +131 -0
- package/src/brain/function-map.js +263 -0
- package/src/brain/vector-store.js +267 -0
- package/src/core/ai-client.js +387 -0
- package/src/core/cluster-manager.js +144 -0
- package/src/core/config.js +89 -0
- package/src/core/error-parser.js +87 -0
- package/src/core/health-monitor.js +129 -0
- package/src/core/models.js +132 -0
- package/src/core/patcher.js +55 -0
- package/src/core/runner.js +464 -0
- package/src/core/system-info.js +141 -0
- package/src/core/verifier.js +146 -0
- package/src/core/wolverine.js +290 -0
- package/src/dashboard/server.js +1332 -0
- package/src/index.js +94 -0
- package/src/logger/event-logger.js +237 -0
- package/src/logger/pricing.js +96 -0
- package/src/logger/repair-history.js +109 -0
- package/src/logger/token-tracker.js +277 -0
- package/src/mcp/mcp-client.js +224 -0
- package/src/mcp/mcp-registry.js +228 -0
- package/src/mcp/mcp-security.js +152 -0
- package/src/monitor/perf-monitor.js +300 -0
- package/src/monitor/process-monitor.js +231 -0
- package/src/monitor/route-prober.js +191 -0
- package/src/notifications/notifier.js +227 -0
- package/src/platform/heartbeat.js +93 -0
- package/src/platform/queue.js +53 -0
- package/src/platform/register.js +64 -0
- package/src/platform/telemetry.js +76 -0
- package/src/security/admin-auth.js +150 -0
- package/src/security/injection-detector.js +174 -0
- package/src/security/rate-limiter.js +152 -0
- package/src/security/sandbox.js +128 -0
- package/src/security/secret-redactor.js +217 -0
- package/src/skills/skill-registry.js +129 -0
- package/src/skills/sql.js +375 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const chalk = require("chalk");
|
|
7
|
+
const { getModel } = require("../core/models");
|
|
8
|
+
const { aiCallWithHistory } = require("../core/ai-client");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Agent Engine — multi-turn AI agent with full claw-code tool harness.
|
|
12
|
+
*
|
|
13
|
+
* Ported tools from claw-code's tool registry:
|
|
14
|
+
*
|
|
15
|
+
* FILE OPERATIONS (claw-code: FileReadTool, FileWriteTool, FileEditTool, GlobTool, GrepTool)
|
|
16
|
+
* read_file — Read file contents with optional line range
|
|
17
|
+
* write_file — Write complete file content
|
|
18
|
+
* edit_file — Find-and-replace edit (surgical, not full rewrite)
|
|
19
|
+
* glob_files — Pattern-based file discovery (e.g. ** /*.js)
|
|
20
|
+
* grep_code — Regex search across codebase with context lines
|
|
21
|
+
*
|
|
22
|
+
* SHELL & COMMANDS (claw-code: BashTool, gitSafety, gitOperationTracking)
|
|
23
|
+
* bash_exec — Sandboxed shell command execution
|
|
24
|
+
* git_log — View recent git commits
|
|
25
|
+
* git_diff — View current changes
|
|
26
|
+
*
|
|
27
|
+
* WEB & RESEARCH (claw-code: WebFetchTool, WebSearchTool)
|
|
28
|
+
* web_fetch — Fetch a URL and return content
|
|
29
|
+
*
|
|
30
|
+
* TASK MANAGEMENT (claw-code: TaskCreateTool pattern)
|
|
31
|
+
* done — Signal completion with summary
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const TOOL_DEFINITIONS = [
|
|
35
|
+
// ── FILE OPERATIONS ──
|
|
36
|
+
{
|
|
37
|
+
type: "function",
|
|
38
|
+
function: {
|
|
39
|
+
name: "read_file",
|
|
40
|
+
description: "Read file contents. Supports optional line offset/limit for large files.",
|
|
41
|
+
parameters: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
path: { type: "string", description: "Relative path from project root" },
|
|
45
|
+
offset: { type: "number", description: "Start line (0-based, optional)" },
|
|
46
|
+
limit: { type: "number", description: "Max lines to read (optional, default: all)" },
|
|
47
|
+
},
|
|
48
|
+
required: ["path"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "function",
|
|
54
|
+
function: {
|
|
55
|
+
name: "write_file",
|
|
56
|
+
description: "Write complete content to a file. Creates parent dirs if needed.",
|
|
57
|
+
parameters: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
path: { type: "string", description: "Relative path from project root" },
|
|
61
|
+
content: { type: "string", description: "Complete file content to write" },
|
|
62
|
+
},
|
|
63
|
+
required: ["path", "content"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: "function",
|
|
69
|
+
function: {
|
|
70
|
+
name: "edit_file",
|
|
71
|
+
description: "Surgical find-and-replace edit. Replaces exact text in a file without rewriting the whole thing. Use this for small targeted fixes.",
|
|
72
|
+
parameters: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: {
|
|
75
|
+
path: { type: "string", description: "Relative path from project root" },
|
|
76
|
+
old_text: { type: "string", description: "Exact text to find (must match verbatim)" },
|
|
77
|
+
new_text: { type: "string", description: "Replacement text" },
|
|
78
|
+
},
|
|
79
|
+
required: ["path", "old_text", "new_text"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: "function",
|
|
85
|
+
function: {
|
|
86
|
+
name: "glob_files",
|
|
87
|
+
description: "Find files matching a glob pattern. Returns paths relative to project root. Use to discover project structure and find files by extension or name pattern.",
|
|
88
|
+
parameters: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
pattern: { type: "string", description: "Glob pattern (e.g. '**/*.js', 'src/**/*.json', '*.config.*')" },
|
|
92
|
+
},
|
|
93
|
+
required: ["pattern"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "function",
|
|
99
|
+
function: {
|
|
100
|
+
name: "grep_code",
|
|
101
|
+
description: "Search for a regex pattern across project files. Returns matching lines with file paths, line numbers, and optional context. More powerful than search_files.",
|
|
102
|
+
parameters: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
pattern: { type: "string", description: "Regex pattern to search for" },
|
|
106
|
+
file_glob: { type: "string", description: "File filter glob (e.g. '*.js', '*.json')" },
|
|
107
|
+
context: { type: "number", description: "Lines of context around each match (default: 0)" },
|
|
108
|
+
max_results: { type: "number", description: "Max matches to return (default: 30)" },
|
|
109
|
+
},
|
|
110
|
+
required: ["pattern"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
// ── SHELL & GIT ──
|
|
115
|
+
{
|
|
116
|
+
type: "function",
|
|
117
|
+
function: {
|
|
118
|
+
name: "bash_exec",
|
|
119
|
+
description: "Execute a shell command in the project directory. Use for running tests, checking package versions, inspecting node_modules, or any system command. Commands are sandboxed to the project directory.",
|
|
120
|
+
parameters: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
command: { type: "string", description: "Shell command to execute" },
|
|
124
|
+
timeout: { type: "number", description: "Timeout in ms (default: 10000)" },
|
|
125
|
+
},
|
|
126
|
+
required: ["command"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: "function",
|
|
132
|
+
function: {
|
|
133
|
+
name: "git_log",
|
|
134
|
+
description: "View recent git commits. Useful for understanding what changed recently.",
|
|
135
|
+
parameters: {
|
|
136
|
+
type: "object",
|
|
137
|
+
properties: {
|
|
138
|
+
count: { type: "number", description: "Number of commits (default: 10)" },
|
|
139
|
+
file: { type: "string", description: "Filter to a specific file path" },
|
|
140
|
+
},
|
|
141
|
+
required: [],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: "function",
|
|
147
|
+
function: {
|
|
148
|
+
name: "git_diff",
|
|
149
|
+
description: "View current uncommitted changes or diff between refs.",
|
|
150
|
+
parameters: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
ref: { type: "string", description: "Git ref to diff against (default: HEAD)" },
|
|
154
|
+
file: { type: "string", description: "Filter to a specific file" },
|
|
155
|
+
},
|
|
156
|
+
required: [],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
// ── WEB ──
|
|
161
|
+
{
|
|
162
|
+
type: "function",
|
|
163
|
+
function: {
|
|
164
|
+
name: "web_fetch",
|
|
165
|
+
description: "Fetch content from a URL. Use this to look up documentation, npm package info, or error solutions. Returns the text content.",
|
|
166
|
+
parameters: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: {
|
|
169
|
+
url: { type: "string", description: "URL to fetch" },
|
|
170
|
+
},
|
|
171
|
+
required: ["url"],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
// ── COMPLETION ──
|
|
176
|
+
{
|
|
177
|
+
type: "function",
|
|
178
|
+
function: {
|
|
179
|
+
name: "done",
|
|
180
|
+
description: "Call this when you have finished analyzing and fixing the issue.",
|
|
181
|
+
parameters: {
|
|
182
|
+
type: "object",
|
|
183
|
+
properties: {
|
|
184
|
+
summary: { type: "string", description: "Summary of changes made" },
|
|
185
|
+
files_modified: {
|
|
186
|
+
type: "array",
|
|
187
|
+
items: { type: "string" },
|
|
188
|
+
description: "List of files that were modified",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
required: ["summary", "files_modified"],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
// Commands that are NEVER allowed in bash_exec (claw-code: destructiveCommandWarning)
|
|
198
|
+
const BLOCKED_COMMANDS = [
|
|
199
|
+
/\brm\s+-rf\s+[/\\]/i, // rm -rf /
|
|
200
|
+
/\brmdir\s+[/\\]/i,
|
|
201
|
+
/\bformat\s+/i,
|
|
202
|
+
/\bmkfs\s+/i,
|
|
203
|
+
/\bdd\s+if=/i,
|
|
204
|
+
/\b(shutdown|reboot|halt)\b/i,
|
|
205
|
+
/\bgit\s+push\s+--force/i, // force push (claw-code: gitSafety)
|
|
206
|
+
/\bgit\s+reset\s+--hard/i,
|
|
207
|
+
/\bnpm\s+publish\b/i, // no accidental publishes
|
|
208
|
+
/\bcurl\b.*\|\s*bash/i, // pipe to bash
|
|
209
|
+
/\beval\s*\(/i,
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
class AgentEngine {
|
|
213
|
+
constructor(options = {}) {
|
|
214
|
+
this.sandbox = options.sandbox;
|
|
215
|
+
this.logger = options.logger;
|
|
216
|
+
this.cwd = options.cwd || process.cwd();
|
|
217
|
+
this.mcp = options.mcp || null; // McpRegistry for external tools
|
|
218
|
+
|
|
219
|
+
// Budget constraints (claw-code: QueryEngineConfig)
|
|
220
|
+
this.maxTurns = options.maxTurns || 15;
|
|
221
|
+
this.maxTokens = options.maxTokens || 50000;
|
|
222
|
+
|
|
223
|
+
// State
|
|
224
|
+
this.messages = [];
|
|
225
|
+
this.turnCount = 0;
|
|
226
|
+
this.totalTokens = 0;
|
|
227
|
+
this.filesRead = new Set();
|
|
228
|
+
this.filesModified = [];
|
|
229
|
+
this.toolCalls = []; // audit trail (claw-code: transcript store pattern)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Run the agent to fix an error.
|
|
234
|
+
*/
|
|
235
|
+
async run({ errorMessage, stackTrace, primaryFile, sourceCode, brainContext }) {
|
|
236
|
+
const model = getModel("reasoning");
|
|
237
|
+
|
|
238
|
+
const systemPrompt = `You are Wolverine, an autonomous Node.js server repair agent. A server crashed and you must fix it.
|
|
239
|
+
|
|
240
|
+
You have a full tool harness for investigating and fixing issues:
|
|
241
|
+
|
|
242
|
+
FILE TOOLS:
|
|
243
|
+
- read_file: Read any file (with optional offset/limit for large files)
|
|
244
|
+
- write_file: Write a complete file
|
|
245
|
+
- edit_file: Surgical find-and-replace (preferred for small fixes)
|
|
246
|
+
- glob_files: Find files by pattern (e.g. "**/*.js", "src/**/*.config.*")
|
|
247
|
+
- grep_code: Search code with regex across the project
|
|
248
|
+
|
|
249
|
+
SHELL TOOLS:
|
|
250
|
+
- bash_exec: Run any shell command (sandboxed to project dir)
|
|
251
|
+
- git_log: View recent commits
|
|
252
|
+
- git_diff: View uncommitted changes
|
|
253
|
+
|
|
254
|
+
RESEARCH:
|
|
255
|
+
- web_fetch: Fetch a URL (docs, npm packages, Stack Overflow)
|
|
256
|
+
|
|
257
|
+
Use these tools systematically:
|
|
258
|
+
1. Understand the error and its root cause
|
|
259
|
+
2. Explore related files (imports, configs, dependencies, schemas)
|
|
260
|
+
3. Check git history if relevant
|
|
261
|
+
4. Fix the issue across ALL affected files
|
|
262
|
+
5. You can edit ANY file type: .js, .json, .sql, .yaml, .env, .dockerfile, .sh, etc.
|
|
263
|
+
6. Prefer edit_file for small targeted fixes, write_file for major changes
|
|
264
|
+
7. Use grep_code to find all usages before renaming something
|
|
265
|
+
8. Use bash_exec to run tests or check dependencies
|
|
266
|
+
|
|
267
|
+
Rules:
|
|
268
|
+
- Read files before modifying them
|
|
269
|
+
- Make minimal, targeted changes
|
|
270
|
+
- When done, call the "done" tool with a summary
|
|
271
|
+
|
|
272
|
+
Project root: ${this.cwd}
|
|
273
|
+
Primary crash file: ${primaryFile}`;
|
|
274
|
+
|
|
275
|
+
this.messages = [
|
|
276
|
+
{ role: "system", content: systemPrompt },
|
|
277
|
+
{
|
|
278
|
+
role: "user",
|
|
279
|
+
content: `The server crashed with this error:\n\n**Error:** ${errorMessage}\n\n**Stack Trace:**\n\`\`\`\n${stackTrace}\n\`\`\`\n\n**Primary file (${primaryFile}):**\n\`\`\`\n${sourceCode}\n\`\`\`${brainContext ? `\n\n**Context from Wolverine Brain:**\n${brainContext}` : ""}\n\nAnalyze the error, explore any related files you need, and fix the issue. Use your tools.`,
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
this.filesRead.add(primaryFile);
|
|
284
|
+
|
|
285
|
+
// Merge MCP tools with built-in tools
|
|
286
|
+
const allTools = [...TOOL_DEFINITIONS];
|
|
287
|
+
if (this.mcp) {
|
|
288
|
+
const mcpTools = this.mcp.getToolDefinitions();
|
|
289
|
+
if (mcpTools.length > 0) {
|
|
290
|
+
allTools.push(...mcpTools);
|
|
291
|
+
console.log(chalk.gray(` 🔌 Agent has ${mcpTools.length} MCP tools available`));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
while (this.turnCount < this.maxTurns) {
|
|
296
|
+
this.turnCount++;
|
|
297
|
+
|
|
298
|
+
if (this.logger) {
|
|
299
|
+
this.logger.debug("agent.turn", `Agent turn ${this.turnCount}/${this.maxTurns}`, {
|
|
300
|
+
turnCount: this.turnCount, filesRead: Array.from(this.filesRead), tokensUsed: this.totalTokens,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log(chalk.gray(` 🤖 Agent turn ${this.turnCount}/${this.maxTurns} (${this.totalTokens} tokens used)`));
|
|
305
|
+
|
|
306
|
+
let response;
|
|
307
|
+
try {
|
|
308
|
+
response = await aiCallWithHistory({
|
|
309
|
+
model,
|
|
310
|
+
messages: this.messages,
|
|
311
|
+
tools: allTools,
|
|
312
|
+
maxTokens: 4096,
|
|
313
|
+
});
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.log(chalk.red(` Agent API error: ${err.message}`));
|
|
316
|
+
return { success: false, summary: err.message, filesModified: [], turnCount: this.turnCount, totalTokens: this.totalTokens };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (response.usage) {
|
|
320
|
+
this.totalTokens += (response.usage.prompt_tokens || 0) + (response.usage.completion_tokens || 0)
|
|
321
|
+
+ (response.usage.input_tokens || 0) + (response.usage.output_tokens || 0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (this.totalTokens > this.maxTokens) {
|
|
325
|
+
console.log(chalk.yellow(` ⚠️ Token budget exhausted (${this.totalTokens}/${this.maxTokens})`));
|
|
326
|
+
return { success: false, summary: "Token budget exhausted", filesModified: this.filesModified, turnCount: this.turnCount, totalTokens: this.totalTokens };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const choice = response.choices[0];
|
|
330
|
+
const assistantMessage = choice.message || choice;
|
|
331
|
+
this.messages.push(assistantMessage);
|
|
332
|
+
|
|
333
|
+
if (!assistantMessage.tool_calls || assistantMessage.tool_calls.length === 0) {
|
|
334
|
+
if (assistantMessage.content) {
|
|
335
|
+
console.log(chalk.gray(` 💬 ${(assistantMessage.content || "").slice(0, 200)}`));
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
success: this.filesModified.length > 0,
|
|
339
|
+
summary: assistantMessage.content || "Agent completed without tool calls",
|
|
340
|
+
filesModified: this.filesModified,
|
|
341
|
+
turnCount: this.turnCount,
|
|
342
|
+
totalTokens: this.totalTokens,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const toolCall of assistantMessage.tool_calls) {
|
|
347
|
+
const result = await this._executeTool(toolCall);
|
|
348
|
+
this.messages.push({
|
|
349
|
+
role: "tool",
|
|
350
|
+
tool_call_id: toolCall.id,
|
|
351
|
+
content: result.content,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (result.done) {
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
summary: result.summary,
|
|
358
|
+
filesModified: result.filesModified || this.filesModified,
|
|
359
|
+
turnCount: this.turnCount,
|
|
360
|
+
totalTokens: this.totalTokens,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log(chalk.yellow(` ⚠️ Max turns (${this.maxTurns}) reached.`));
|
|
367
|
+
return {
|
|
368
|
+
success: this.filesModified.length > 0,
|
|
369
|
+
summary: `Agent used all ${this.maxTurns} turns`,
|
|
370
|
+
filesModified: this.filesModified,
|
|
371
|
+
turnCount: this.turnCount,
|
|
372
|
+
totalTokens: this.totalTokens,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async _executeTool(toolCall) {
|
|
377
|
+
const name = toolCall.function.name;
|
|
378
|
+
let args;
|
|
379
|
+
try {
|
|
380
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
381
|
+
} catch {
|
|
382
|
+
return { content: "Error: Invalid JSON in tool arguments" };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Audit trail
|
|
386
|
+
this.toolCalls.push({ name, args, timestamp: Date.now() });
|
|
387
|
+
|
|
388
|
+
switch (name) {
|
|
389
|
+
case "read_file": return this._readFile(args);
|
|
390
|
+
case "write_file": return this._writeFile(args);
|
|
391
|
+
case "edit_file": return this._editFile(args);
|
|
392
|
+
case "glob_files": return this._globFiles(args);
|
|
393
|
+
case "grep_code": return this._grepCode(args);
|
|
394
|
+
case "bash_exec": return this._bashExec(args);
|
|
395
|
+
case "git_log": return this._gitLog(args);
|
|
396
|
+
case "git_diff": return this._gitDiff(args);
|
|
397
|
+
case "web_fetch": return this._webFetch(args);
|
|
398
|
+
case "done": return this._done(args);
|
|
399
|
+
// Legacy aliases
|
|
400
|
+
case "list_files": return this._globFiles({ pattern: (args.dir || ".") + "/*" + (args.pattern || "") });
|
|
401
|
+
case "search_files": return this._grepCode({ pattern: args.query, file_glob: args.file_pattern });
|
|
402
|
+
default:
|
|
403
|
+
// Check MCP tools
|
|
404
|
+
if (this.mcp && this.mcp.isMcpTool(name)) {
|
|
405
|
+
const result = await this.mcp.callTool(name, args);
|
|
406
|
+
return { content: result.error || result.content || "No result" };
|
|
407
|
+
}
|
|
408
|
+
return { content: `Unknown tool: ${name}` };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── FILE TOOLS ──
|
|
413
|
+
|
|
414
|
+
_readFile(args) {
|
|
415
|
+
const filePath = path.resolve(this.cwd, args.path);
|
|
416
|
+
try {
|
|
417
|
+
const content = this.sandbox.readFile(filePath);
|
|
418
|
+
this.filesRead.add(args.path);
|
|
419
|
+
let result = content;
|
|
420
|
+
|
|
421
|
+
if (args.offset || args.limit) {
|
|
422
|
+
const lines = content.split("\n");
|
|
423
|
+
const start = args.offset || 0;
|
|
424
|
+
const end = args.limit ? start + args.limit : lines.length;
|
|
425
|
+
result = lines.slice(start, end).map((l, i) => `${start + i + 1} | ${l}`).join("\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log(chalk.gray(` 📖 Read: ${args.path} (${content.length} chars)`));
|
|
429
|
+
if (this.logger) this.logger.debug("agent.file_read", `Read ${args.path}`, { path: args.path, size: content.length });
|
|
430
|
+
return { content: result };
|
|
431
|
+
} catch (err) {
|
|
432
|
+
return { content: `Error reading ${args.path}: ${err.message}` };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
_writeFile(args) {
|
|
437
|
+
// Guard: block writes to wolverine internals
|
|
438
|
+
if (this._isProtectedPath(args.path)) {
|
|
439
|
+
return { content: `BLOCKED: Cannot modify wolverine internal file "${args.path}". Only user project files can be modified.` };
|
|
440
|
+
}
|
|
441
|
+
const filePath = path.resolve(this.cwd, args.path);
|
|
442
|
+
try {
|
|
443
|
+
const dir = path.dirname(filePath);
|
|
444
|
+
this.sandbox.resolve(dir);
|
|
445
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
446
|
+
this.sandbox.writeFile(filePath, args.content);
|
|
447
|
+
this.filesModified.push(args.path);
|
|
448
|
+
console.log(chalk.green(` ✏️ Wrote: ${args.path}`));
|
|
449
|
+
if (this.logger) this.logger.info("agent.file_write", `Modified ${args.path}`, { path: args.path });
|
|
450
|
+
return { content: `Successfully wrote ${args.path}` };
|
|
451
|
+
} catch (err) {
|
|
452
|
+
return { content: `Error writing ${args.path}: ${err.message}` };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
_editFile(args) {
|
|
457
|
+
if (this._isProtectedPath(args.path)) {
|
|
458
|
+
return { content: `BLOCKED: Cannot modify wolverine internal file "${args.path}". Only user project files can be modified.` };
|
|
459
|
+
}
|
|
460
|
+
const filePath = path.resolve(this.cwd, args.path);
|
|
461
|
+
try {
|
|
462
|
+
const content = this.sandbox.readFile(filePath);
|
|
463
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
464
|
+
const normalizedOld = args.old_text.replace(/\r\n/g, "\n");
|
|
465
|
+
|
|
466
|
+
if (!normalized.includes(normalizedOld)) {
|
|
467
|
+
return { content: `Error: Could not find the exact text to replace in ${args.path}. Make sure old_text matches verbatim.` };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const patched = normalized.replace(normalizedOld, args.new_text.replace(/\r\n/g, "\n"));
|
|
471
|
+
this.sandbox.writeFile(filePath, patched);
|
|
472
|
+
if (!this.filesModified.includes(args.path)) this.filesModified.push(args.path);
|
|
473
|
+
console.log(chalk.green(` ✏️ Edited: ${args.path}`));
|
|
474
|
+
if (this.logger) this.logger.info("agent.file_write", `Edited ${args.path}`, { path: args.path });
|
|
475
|
+
return { content: `Successfully edited ${args.path}` };
|
|
476
|
+
} catch (err) {
|
|
477
|
+
return { content: `Error editing ${args.path}: ${err.message}` };
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
_globFiles(args) {
|
|
482
|
+
const results = [];
|
|
483
|
+
const pattern = args.pattern || "**/*";
|
|
484
|
+
const parts = pattern.split("/");
|
|
485
|
+
const ext = parts[parts.length - 1].includes("*") ? parts[parts.length - 1].replace("*", "") : null;
|
|
486
|
+
|
|
487
|
+
const walk = (dir, depth) => {
|
|
488
|
+
if (depth > 10 || results.length > 200) return;
|
|
489
|
+
let entries;
|
|
490
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
491
|
+
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
if (["node_modules", ".wolverine", ".git", "dist", "build"].includes(entry.name)) continue;
|
|
494
|
+
const fullPath = path.join(dir, entry.name);
|
|
495
|
+
const relPath = path.relative(this.cwd, fullPath).replace(/\\/g, "/");
|
|
496
|
+
|
|
497
|
+
if (entry.isDirectory()) {
|
|
498
|
+
if (pattern.includes("**")) walk(fullPath, depth + 1);
|
|
499
|
+
} else {
|
|
500
|
+
if (!ext || entry.name.endsWith(ext)) {
|
|
501
|
+
results.push(relPath);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
this.sandbox.resolve(this.cwd);
|
|
509
|
+
walk(this.cwd, 0);
|
|
510
|
+
console.log(chalk.gray(` 🔍 Glob "${pattern}": ${results.length} files`));
|
|
511
|
+
return { content: results.length > 0 ? results.join("\n") : `No files matching "${pattern}"` };
|
|
512
|
+
} catch (err) {
|
|
513
|
+
return { content: `Error: ${err.message}` };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
_grepCode(args) {
|
|
518
|
+
const results = [];
|
|
519
|
+
const maxResults = args.max_results || 30;
|
|
520
|
+
const contextLines = args.context || 0;
|
|
521
|
+
let regex;
|
|
522
|
+
try {
|
|
523
|
+
regex = new RegExp(args.pattern, "gi");
|
|
524
|
+
} catch {
|
|
525
|
+
regex = new RegExp(args.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const walk = (dir) => {
|
|
529
|
+
if (results.length >= maxResults) return;
|
|
530
|
+
let entries;
|
|
531
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
532
|
+
|
|
533
|
+
for (const entry of entries) {
|
|
534
|
+
if (results.length >= maxResults) break;
|
|
535
|
+
if (["node_modules", ".wolverine", ".git", "dist", "build"].includes(entry.name)) continue;
|
|
536
|
+
|
|
537
|
+
const fullPath = path.join(dir, entry.name);
|
|
538
|
+
if (entry.isDirectory()) { walk(fullPath); continue; }
|
|
539
|
+
|
|
540
|
+
if (args.file_glob) {
|
|
541
|
+
const ext = args.file_glob.replace("*", "");
|
|
542
|
+
if (!entry.name.endsWith(ext)) continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
try {
|
|
546
|
+
this.sandbox.resolve(fullPath);
|
|
547
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
548
|
+
const lines = content.split("\n");
|
|
549
|
+
const relPath = path.relative(this.cwd, fullPath).replace(/\\/g, "/");
|
|
550
|
+
|
|
551
|
+
for (let i = 0; i < lines.length && results.length < maxResults; i++) {
|
|
552
|
+
regex.lastIndex = 0;
|
|
553
|
+
if (regex.test(lines[i])) {
|
|
554
|
+
if (contextLines > 0) {
|
|
555
|
+
const start = Math.max(0, i - contextLines);
|
|
556
|
+
const end = Math.min(lines.length, i + contextLines + 1);
|
|
557
|
+
const ctx = lines.slice(start, end).map((l, j) => {
|
|
558
|
+
const lineNum = start + j + 1;
|
|
559
|
+
const marker = (start + j === i) ? ">" : " ";
|
|
560
|
+
return `${marker} ${lineNum} | ${l}`;
|
|
561
|
+
}).join("\n");
|
|
562
|
+
results.push(`${relPath}:${i + 1}:\n${ctx}`);
|
|
563
|
+
} else {
|
|
564
|
+
results.push(`${relPath}:${i + 1}: ${lines[i].trim()}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} catch { /* skip binary */ }
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
walk(this.cwd);
|
|
573
|
+
console.log(chalk.gray(` 🔍 Grep "${args.pattern}": ${results.length} matches`));
|
|
574
|
+
return { content: results.length > 0 ? results.join("\n\n") : `No matches for "${args.pattern}"` };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ── SHELL TOOLS ──
|
|
578
|
+
|
|
579
|
+
_bashExec(args) {
|
|
580
|
+
// Security: check for blocked commands (claw-code: destructiveCommandWarning, bashSecurity)
|
|
581
|
+
for (const blocked of BLOCKED_COMMANDS) {
|
|
582
|
+
if (blocked.test(args.command)) {
|
|
583
|
+
console.log(chalk.red(` 🛡️ Blocked dangerous command: ${args.command}`));
|
|
584
|
+
return { content: `BLOCKED: Command "${args.command}" is not allowed for safety reasons.` };
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const timeout = Math.min(args.timeout || 10000, 30000);
|
|
589
|
+
try {
|
|
590
|
+
const output = execSync(args.command, {
|
|
591
|
+
cwd: this.cwd,
|
|
592
|
+
encoding: "utf-8",
|
|
593
|
+
timeout,
|
|
594
|
+
maxBuffer: 1024 * 1024,
|
|
595
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
596
|
+
});
|
|
597
|
+
console.log(chalk.gray(` ⚡ Bash: ${args.command.slice(0, 60)}`));
|
|
598
|
+
return { content: output.slice(0, 5000) || "(no output)" };
|
|
599
|
+
} catch (err) {
|
|
600
|
+
const stderr = err.stderr ? err.stderr.slice(0, 3000) : "";
|
|
601
|
+
const stdout = err.stdout ? err.stdout.slice(0, 1000) : "";
|
|
602
|
+
return { content: `Exit code: ${err.status}\nstdout: ${stdout}\nstderr: ${stderr}` };
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
_gitLog(args) {
|
|
607
|
+
const count = args.count || 10;
|
|
608
|
+
const fileFilter = args.file ? ` -- ${args.file}` : "";
|
|
609
|
+
try {
|
|
610
|
+
const output = execSync(
|
|
611
|
+
`git log --oneline --no-decorate -n ${count}${fileFilter}`,
|
|
612
|
+
{ cwd: this.cwd, encoding: "utf-8", timeout: 5000 }
|
|
613
|
+
);
|
|
614
|
+
console.log(chalk.gray(` 📜 Git log: ${count} commits`));
|
|
615
|
+
return { content: output || "(no git history)" };
|
|
616
|
+
} catch (err) {
|
|
617
|
+
return { content: `Git log failed: ${err.message}` };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
_gitDiff(args) {
|
|
622
|
+
const ref = args.ref || "";
|
|
623
|
+
const fileFilter = args.file ? ` -- ${args.file}` : "";
|
|
624
|
+
try {
|
|
625
|
+
const output = execSync(
|
|
626
|
+
`git diff ${ref}${fileFilter}`,
|
|
627
|
+
{ cwd: this.cwd, encoding: "utf-8", timeout: 5000 }
|
|
628
|
+
);
|
|
629
|
+
console.log(chalk.gray(` 📜 Git diff`));
|
|
630
|
+
return { content: output.slice(0, 5000) || "(no changes)" };
|
|
631
|
+
} catch (err) {
|
|
632
|
+
return { content: `Git diff failed: ${err.message}` };
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ── WEB TOOLS ──
|
|
637
|
+
|
|
638
|
+
_webFetch(args) {
|
|
639
|
+
return new Promise((resolve) => {
|
|
640
|
+
const url = args.url;
|
|
641
|
+
if (!url || !url.startsWith("http")) {
|
|
642
|
+
resolve({ content: "Error: URL must start with http:// or https://" });
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const client = url.startsWith("https") ? https : http;
|
|
647
|
+
const req = client.get(url, { timeout: 10000 }, (res) => {
|
|
648
|
+
let data = "";
|
|
649
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
650
|
+
res.on("end", () => {
|
|
651
|
+
// Strip HTML tags for readability
|
|
652
|
+
const text = data.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
|
|
653
|
+
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
|
|
654
|
+
.replace(/<[^>]+>/g, " ")
|
|
655
|
+
.replace(/\s+/g, " ")
|
|
656
|
+
.trim()
|
|
657
|
+
.slice(0, 5000);
|
|
658
|
+
console.log(chalk.gray(` 🌐 Fetched: ${url.slice(0, 60)}`));
|
|
659
|
+
if (this.logger) this.logger.debug("agent.research", `Fetched ${url}`, { url });
|
|
660
|
+
resolve({ content: text || "(empty response)" });
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
req.on("error", (err) => {
|
|
665
|
+
resolve({ content: `Fetch error: ${err.message}` });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
req.on("timeout", () => {
|
|
669
|
+
req.destroy();
|
|
670
|
+
resolve({ content: "Fetch timed out after 10s" });
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ── COMPLETION ──
|
|
676
|
+
|
|
677
|
+
_done(args) {
|
|
678
|
+
console.log(chalk.green(` ✅ Agent done: ${args.summary}`));
|
|
679
|
+
if (this.logger) {
|
|
680
|
+
this.logger.info("agent.complete", args.summary, {
|
|
681
|
+
filesModified: args.files_modified,
|
|
682
|
+
turnCount: this.turnCount,
|
|
683
|
+
totalTokens: this.totalTokens,
|
|
684
|
+
toolCallCount: this.toolCalls.length,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return {
|
|
688
|
+
content: "Done",
|
|
689
|
+
done: true,
|
|
690
|
+
summary: args.summary,
|
|
691
|
+
filesModified: args.files_modified,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// ── Protected path guard ──
|
|
696
|
+
// Wolverine's own source code is off-limits to the agent.
|
|
697
|
+
// The agent should build/fix the USER's project, not modify itself.
|
|
698
|
+
_isProtectedPath(filePath) {
|
|
699
|
+
let normalized = filePath.replace(/\\/g, "/");
|
|
700
|
+
|
|
701
|
+
const cwdNorm = this.cwd.replace(/\\/g, "/");
|
|
702
|
+
if (normalized.startsWith(cwdNorm)) {
|
|
703
|
+
normalized = normalized.slice(cwdNorm.length).replace(/^\//, "");
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// WHITELIST: server/ is always editable — that's the user's project
|
|
707
|
+
if (normalized.startsWith("server/")) return false;
|
|
708
|
+
|
|
709
|
+
const protectedPrefixes = [
|
|
710
|
+
"src/", // wolverine core
|
|
711
|
+
"bin/", // wolverine CLI
|
|
712
|
+
"tests/", // wolverine tests
|
|
713
|
+
"node_modules/", // dependencies
|
|
714
|
+
".wolverine/", // internal state
|
|
715
|
+
"examples/", // test examples (not the live server)
|
|
716
|
+
];
|
|
717
|
+
const protectedExact = [
|
|
718
|
+
".env", ".env.local", ".env.production", ".env.development",
|
|
719
|
+
"package.json", "package-lock.json",
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
return protectedPrefixes.some(p => normalized.startsWith(p))
|
|
723
|
+
|| protectedExact.some(p => normalized === p);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
module.exports = { AgentEngine, TOOL_DEFINITIONS, BLOCKED_COMMANDS };
|