wolverine-ai 4.5.4 β 4.6.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.
- package/bin/wolverine.js +33 -0
- package/package.json +1 -1
- package/src/brain/brain.js +5 -1
- package/src/core/runner.js +12 -0
- package/src/core/server-context.js +277 -0
- package/src/core/wolverine.js +6 -0
package/bin/wolverine.js
CHANGED
|
@@ -27,6 +27,7 @@ ${chalk.bold("Options:")}
|
|
|
27
27
|
--single Force single-worker mode (no clustering)
|
|
28
28
|
--workers <n> Force specific worker count
|
|
29
29
|
--info Show system info and exit
|
|
30
|
+
--init Scan server/ and build context map (routes, DB, config, deps)
|
|
30
31
|
|
|
31
32
|
${chalk.bold("Configuration:")}
|
|
32
33
|
server/config/settings.json Models, telemetry, limits, health checks
|
|
@@ -54,6 +55,38 @@ if (args.includes("--info")) {
|
|
|
54
55
|
process.exit(0);
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// --init: scan server/ and build context map
|
|
59
|
+
if (args.includes("--init")) {
|
|
60
|
+
const { scan } = require("../src/core/server-context");
|
|
61
|
+
console.log(chalk.blue("\n π Scanning server/ directory...\n"));
|
|
62
|
+
const ctx = scan(process.cwd());
|
|
63
|
+
if (!ctx) {
|
|
64
|
+
console.log(chalk.yellow(" No server/ directory found."));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk.green(` β
Server context built:`));
|
|
68
|
+
console.log(chalk.gray(` Routes: ${ctx.routes.reduce((s, r) => s + r.endpoints.length, 0)}`));
|
|
69
|
+
console.log(chalk.gray(` Middleware: ${ctx.middleware.length}`));
|
|
70
|
+
console.log(chalk.gray(` Database: ${ctx.database.type || "none"}${ctx.database.tables.length > 0 ? ` (${ctx.database.tables.length} tables)` : ""}`));
|
|
71
|
+
console.log(chalk.gray(` Env vars: ${ctx.envVars.length}`));
|
|
72
|
+
console.log(chalk.gray(` Files: ${ctx.structure.length}`));
|
|
73
|
+
console.log(chalk.gray(` Saved to: .wolverine/server-context.json`));
|
|
74
|
+
if (ctx.warnings && ctx.warnings.length > 0) {
|
|
75
|
+
console.log(chalk.yellow(`\n β οΈ Security warnings (${ctx.warnings.length}):`));
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (const w of ctx.warnings) {
|
|
78
|
+
const key = `${w.file}:${w.type}`;
|
|
79
|
+
if (seen.has(key)) continue;
|
|
80
|
+
seen.add(key);
|
|
81
|
+
console.log(chalk.yellow(` ${w.file}: ${w.label}`));
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
console.log(chalk.green(` No security warnings`));
|
|
85
|
+
}
|
|
86
|
+
console.log("");
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
// --update: safe framework update
|
|
58
91
|
if (args.includes("--update")) {
|
|
59
92
|
const { safeUpdate } = require("../src/skills/update");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.1",
|
|
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": {
|
package/src/brain/brain.js
CHANGED
|
@@ -138,9 +138,13 @@ const SEED_DOCS = [
|
|
|
138
138
|
metadata: { topic: "prompt-caching" },
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
|
-
text: "Platform telemetry: lightweight background process, zero-config. Default platform: api.wolverinenode.xyz. Auto-registers on first run
|
|
141
|
+
text: "Platform telemetry: lightweight background process, zero-config. Default platform: api.wolverinenode.xyz. Auto-registers on first run, heartbeats every 60s. Offline-resilient: queues up to 1440 heartbeats locally, drains on reconnect. Opt out: WOLVERINE_TELEMETRY=false.",
|
|
142
142
|
metadata: { topic: "platform-telemetry" },
|
|
143
143
|
},
|
|
144
|
+
{
|
|
145
|
+
text: "Server context scanner (wolverine --init): scans server/ directory on every startup to build .wolverine/server-context.json. Extracts routes (HTTP methods + paths from fastify/express), middleware stack, database type + tables, config structure, dependencies, env vars used (process.env.X patterns), and full file tree. Context summary auto-injected into agent's heal prompt so it knows the server's route map, DB schema, and dependencies without re-scanning. Manual scan: wolverine --init. Auto-scan: runs silently on every boot. The context is read-only β never modified by the agent.",
|
|
146
|
+
metadata: { topic: "server-context" },
|
|
147
|
+
},
|
|
144
148
|
{
|
|
145
149
|
text: "Telemetry architecture: 4 files, ~250 lines total. heartbeat.js sends one HTTP POST every 60s (5s timeout, non-blocking). register.js auto-registers and caches key in memory + disk. queue.js appends to JSONL file only on failure, trims lazily. telemetry.js collects from subsystems using optional chaining (no crashes if subsystem missing). All secrets redacted before sending. Response bodies drained immediately (res.resume). No blocking, no delays, no busy waits.",
|
|
146
150
|
metadata: { topic: "telemetry-architecture" },
|
package/src/core/runner.js
CHANGED
|
@@ -211,6 +211,18 @@ class WolverineRunner {
|
|
|
211
211
|
console.log(chalk.yellow(` β οΈ Vault init failed (non-fatal): ${err.message}`));
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// Scan server context (routes, DB, config, deps) for agent knowledge
|
|
215
|
+
try {
|
|
216
|
+
const { scan } = require("./server-context");
|
|
217
|
+
const ctx = scan(this.cwd);
|
|
218
|
+
if (ctx) {
|
|
219
|
+
const routes = ctx.routes.reduce((s, r) => s + r.endpoints.length, 0);
|
|
220
|
+
const warns = (ctx.warnings || []).length;
|
|
221
|
+
console.log(chalk.gray(` πΊοΈ Server context: ${routes} routes, ${ctx.structure.length} files, ${ctx.envVars.length} env vars`));
|
|
222
|
+
if (warns > 0) console.log(chalk.yellow(` β οΈ ${warns} security warning(s) β run wolverine --init for details`));
|
|
223
|
+
}
|
|
224
|
+
} catch {}
|
|
225
|
+
|
|
214
226
|
// Log redactor stats
|
|
215
227
|
const redactorStats = this.redactor.getStats();
|
|
216
228
|
console.log(chalk.gray(` π Secret redactor: ${redactorStats.trackedSecrets} secrets tracked from ${redactorStats.envFiles} env file(s)`));
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Server Context Scanner β builds a structured map of the server/ directory.
|
|
6
|
+
*
|
|
7
|
+
* Scans routes, middleware, config, database, dependencies, and file structure
|
|
8
|
+
* to give the AI agent full context when diagnosing errors.
|
|
9
|
+
*
|
|
10
|
+
* Runs automatically on startup (stored in .wolverine/server-context.json).
|
|
11
|
+
* Can be triggered manually: wolverine --init or require('./server-context').scan(cwd)
|
|
12
|
+
*
|
|
13
|
+
* The context is injected into the agent's system prompt so it knows the
|
|
14
|
+
* server's structure without re-scanning on every heal.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const CONTEXT_PATH = ".wolverine/server-context.json";
|
|
18
|
+
const MAX_FILE_SCAN = 200; // don't scan more than 200 files
|
|
19
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", ".wolverine", "dist", ".next", ".cache", "coverage", "__pycache__"]);
|
|
20
|
+
|
|
21
|
+
function scan(cwd) {
|
|
22
|
+
const serverDir = path.join(cwd, "server");
|
|
23
|
+
if (!fs.existsSync(serverDir)) return null;
|
|
24
|
+
|
|
25
|
+
const context = {
|
|
26
|
+
scannedAt: new Date().toISOString(),
|
|
27
|
+
routes: [],
|
|
28
|
+
middleware: [],
|
|
29
|
+
database: { tables: [], config: null },
|
|
30
|
+
config: {},
|
|
31
|
+
dependencies: {},
|
|
32
|
+
structure: [],
|
|
33
|
+
exports: [],
|
|
34
|
+
envVars: [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 1. Scan routes
|
|
38
|
+
const routesDir = path.join(serverDir, "routes");
|
|
39
|
+
if (fs.existsSync(routesDir)) {
|
|
40
|
+
for (const file of _listFiles(routesDir, ".js")) {
|
|
41
|
+
try {
|
|
42
|
+
const code = fs.readFileSync(file, "utf-8");
|
|
43
|
+
const relPath = path.relative(cwd, file);
|
|
44
|
+
const methods = [];
|
|
45
|
+
// Match fastify.get/post/put/delete/patch or app.get/post etc
|
|
46
|
+
const routeRegex = /(?:fastify|app|router)\.(get|post|put|delete|patch|options|head)\s*\(\s*['"`]([^'"`]+)/gi;
|
|
47
|
+
let m;
|
|
48
|
+
while ((m = routeRegex.exec(code)) !== null) {
|
|
49
|
+
methods.push({ method: m[1].toUpperCase(), path: m[2] });
|
|
50
|
+
}
|
|
51
|
+
// Match fastify.register with prefix
|
|
52
|
+
const registerRegex = /register\s*\(.*?prefix\s*:\s*['"`]([^'"`]+)/gi;
|
|
53
|
+
while ((m = registerRegex.exec(code)) !== null) {
|
|
54
|
+
methods.push({ method: "REGISTER", path: m[1] });
|
|
55
|
+
}
|
|
56
|
+
if (methods.length > 0) {
|
|
57
|
+
context.routes.push({ file: relPath, endpoints: methods });
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Scan middleware
|
|
64
|
+
const indexFile = path.join(serverDir, "index.js");
|
|
65
|
+
if (fs.existsSync(indexFile)) {
|
|
66
|
+
try {
|
|
67
|
+
const code = fs.readFileSync(indexFile, "utf-8");
|
|
68
|
+
// Find app.use() or fastify.register() calls
|
|
69
|
+
const mwRegex = /(?:app\.use|fastify\.register)\s*\(\s*(?:require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)|(\w+))/gi;
|
|
70
|
+
let m;
|
|
71
|
+
while ((m = mwRegex.exec(code)) !== null) {
|
|
72
|
+
context.middleware.push(m[1] || m[2]);
|
|
73
|
+
}
|
|
74
|
+
} catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Scan database
|
|
78
|
+
const dbFiles = [
|
|
79
|
+
path.join(serverDir, "lib", "db.js"),
|
|
80
|
+
path.join(serverDir, "db.js"),
|
|
81
|
+
path.join(serverDir, "models", "index.js"),
|
|
82
|
+
path.join(serverDir, "database.js"),
|
|
83
|
+
];
|
|
84
|
+
for (const dbFile of dbFiles) {
|
|
85
|
+
if (!fs.existsSync(dbFile)) continue;
|
|
86
|
+
try {
|
|
87
|
+
const code = fs.readFileSync(dbFile, "utf-8");
|
|
88
|
+
// Detect database type
|
|
89
|
+
if (/require\s*\(\s*['"]pg['"]/.test(code)) context.database.type = "postgresql";
|
|
90
|
+
else if (/require\s*\(\s*['"]better-sqlite3['"]/.test(code)) context.database.type = "sqlite";
|
|
91
|
+
else if (/require\s*\(\s*['"]mysql/.test(code)) context.database.type = "mysql";
|
|
92
|
+
else if (/require\s*\(\s*['"]mongoose['"]/.test(code)) context.database.type = "mongodb";
|
|
93
|
+
else if (/require\s*\(\s*['"]ioredis['"]/.test(code)) context.database.hasRedis = true;
|
|
94
|
+
// Find CREATE TABLE statements
|
|
95
|
+
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["`]?(\w+)/gi;
|
|
96
|
+
let m;
|
|
97
|
+
while ((m = tableRegex.exec(code)) !== null) {
|
|
98
|
+
context.database.tables.push(m[1]);
|
|
99
|
+
}
|
|
100
|
+
context.database.config = path.relative(cwd, dbFile);
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Scan config
|
|
105
|
+
const settingsPath = path.join(serverDir, "config", "settings.json");
|
|
106
|
+
if (fs.existsSync(settingsPath)) {
|
|
107
|
+
try {
|
|
108
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
109
|
+
// Only include non-secret top-level keys
|
|
110
|
+
context.config = Object.keys(settings).reduce((acc, k) => {
|
|
111
|
+
if (typeof settings[k] === "object") acc[k] = Object.keys(settings[k]);
|
|
112
|
+
else acc[k] = typeof settings[k];
|
|
113
|
+
return acc;
|
|
114
|
+
}, {});
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 5. Dependencies
|
|
119
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
120
|
+
if (fs.existsSync(pkgPath)) {
|
|
121
|
+
try {
|
|
122
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
123
|
+
context.dependencies = {
|
|
124
|
+
production: Object.keys(pkg.dependencies || {}),
|
|
125
|
+
dev: Object.keys(pkg.devDependencies || {}),
|
|
126
|
+
optional: Object.keys(pkg.optionalDependencies || {}),
|
|
127
|
+
};
|
|
128
|
+
context.nodeVersion = pkg.engines?.node || "unknown";
|
|
129
|
+
context.version = pkg.version;
|
|
130
|
+
} catch {}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 6. File structure (server/ tree)
|
|
134
|
+
const tree = [];
|
|
135
|
+
let fileCount = 0;
|
|
136
|
+
const walk = (dir, depth) => {
|
|
137
|
+
if (depth > 4 || fileCount > MAX_FILE_SCAN) return;
|
|
138
|
+
try {
|
|
139
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
140
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
141
|
+
const rel = path.relative(cwd, path.join(dir, entry.name));
|
|
142
|
+
if (entry.isDirectory()) {
|
|
143
|
+
tree.push(rel + "/");
|
|
144
|
+
walk(path.join(dir, entry.name), depth + 1);
|
|
145
|
+
} else {
|
|
146
|
+
tree.push(rel);
|
|
147
|
+
fileCount++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} catch {}
|
|
151
|
+
};
|
|
152
|
+
walk(serverDir, 0);
|
|
153
|
+
context.structure = tree;
|
|
154
|
+
|
|
155
|
+
// 7. Env vars used (scan for process.env.X patterns)
|
|
156
|
+
const envVars = new Set();
|
|
157
|
+
const scanForEnv = (dir) => {
|
|
158
|
+
if (!fs.existsSync(dir)) return;
|
|
159
|
+
for (const file of _listFiles(dir, ".js")) {
|
|
160
|
+
try {
|
|
161
|
+
const code = fs.readFileSync(file, "utf-8");
|
|
162
|
+
const envRegex = /process\.env\.([A-Z_][A-Z0-9_]*)/g;
|
|
163
|
+
let m;
|
|
164
|
+
while ((m = envRegex.exec(code)) !== null) envVars.add(m[1]);
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
scanForEnv(serverDir);
|
|
169
|
+
context.envVars = [...envVars].sort();
|
|
170
|
+
|
|
171
|
+
// 8. Security scan β detect dangerous patterns in server code
|
|
172
|
+
context.warnings = [];
|
|
173
|
+
const scanForDangers = (dir) => {
|
|
174
|
+
if (!fs.existsSync(dir)) return;
|
|
175
|
+
for (const file of _listFiles(dir, ".js")) {
|
|
176
|
+
try {
|
|
177
|
+
const code = fs.readFileSync(file, "utf-8");
|
|
178
|
+
const rel = path.relative(cwd, file);
|
|
179
|
+
// Hardcoded secrets
|
|
180
|
+
if (/['"][a-zA-Z0-9_-]{20,}['"]/.test(code)) {
|
|
181
|
+
const secretPatterns = [
|
|
182
|
+
{ regex: /sk-[a-zA-Z0-9]{20,}/g, label: "OpenAI API key" },
|
|
183
|
+
{ regex: /sk-ant-[a-zA-Z0-9_-]{20,}/g, label: "Anthropic API key" },
|
|
184
|
+
{ regex: /ghp_[a-zA-Z0-9]{36,}/g, label: "GitHub token" },
|
|
185
|
+
{ regex: /AKIA[0-9A-Z]{16}/g, label: "AWS access key" },
|
|
186
|
+
{ regex: /['"](?:password|secret|token|api_key|apikey|auth)\s*['"]?\s*[:=]\s*['"][^'"]{8,}['"]/gi, label: "Hardcoded credential" },
|
|
187
|
+
{ regex: /mongodb\+srv:\/\/[^\s'"]+/g, label: "MongoDB connection string" },
|
|
188
|
+
{ regex: /postgres:\/\/[^\s'"]+/g, label: "PostgreSQL connection string" },
|
|
189
|
+
{ regex: /redis:\/\/[^\s'"]+/g, label: "Redis connection string" },
|
|
190
|
+
];
|
|
191
|
+
for (const p of secretPatterns) {
|
|
192
|
+
if (p.regex.test(code)) {
|
|
193
|
+
context.warnings.push({ file: rel, type: "hardcoded_secret", label: p.label });
|
|
194
|
+
p.regex.lastIndex = 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// eval / Function constructor
|
|
199
|
+
if (/\beval\s*\(/.test(code)) context.warnings.push({ file: rel, type: "eval_usage", label: "eval() call" });
|
|
200
|
+
if (/new\s+Function\s*\(/.test(code)) context.warnings.push({ file: rel, type: "function_constructor", label: "new Function() call" });
|
|
201
|
+
// SQL injection risk (string concat in queries)
|
|
202
|
+
if (/\.(query|exec|prepare)\s*\(\s*['"`].*\$\{/.test(code) || /\.(query|exec)\s*\(\s*.*\+\s*(?:req|args|params|body)/i.test(code)) {
|
|
203
|
+
context.warnings.push({ file: rel, type: "sql_injection_risk", label: "String concatenation in SQL query" });
|
|
204
|
+
}
|
|
205
|
+
// Unvalidated redirect
|
|
206
|
+
if (/res\.redirect\s*\(\s*(?:req\.|args\.|params\.)/.test(code)) {
|
|
207
|
+
context.warnings.push({ file: rel, type: "open_redirect", label: "Unvalidated redirect from user input" });
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
scanForDangers(serverDir);
|
|
213
|
+
|
|
214
|
+
// Sanitize: strip any actual secret values that might have leaked into context
|
|
215
|
+
const contextStr = JSON.stringify(context);
|
|
216
|
+
const { redact } = require("../security/secret-redactor");
|
|
217
|
+
const sanitized = JSON.parse(redact(contextStr));
|
|
218
|
+
|
|
219
|
+
// Save
|
|
220
|
+
const outPath = path.join(cwd, CONTEXT_PATH);
|
|
221
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
222
|
+
fs.writeFileSync(outPath, JSON.stringify(sanitized, null, 2), "utf-8");
|
|
223
|
+
|
|
224
|
+
return sanitized;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Load cached context (fast β no rescan).
|
|
229
|
+
*/
|
|
230
|
+
function load(cwd) {
|
|
231
|
+
const ctxPath = path.join(cwd, CONTEXT_PATH);
|
|
232
|
+
if (!fs.existsSync(ctxPath)) return null;
|
|
233
|
+
try {
|
|
234
|
+
return JSON.parse(fs.readFileSync(ctxPath, "utf-8"));
|
|
235
|
+
} catch { return null; }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get a compact text summary for injecting into AI prompts.
|
|
240
|
+
*/
|
|
241
|
+
function getSummary(cwd) {
|
|
242
|
+
const ctx = load(cwd);
|
|
243
|
+
if (!ctx) return "";
|
|
244
|
+
|
|
245
|
+
const lines = [];
|
|
246
|
+
if (ctx.routes.length > 0) {
|
|
247
|
+
const allEndpoints = ctx.routes.flatMap(r => r.endpoints.map(e => `${e.method} ${e.path}`));
|
|
248
|
+
lines.push(`Routes (${allEndpoints.length}): ${allEndpoints.slice(0, 20).join(", ")}${allEndpoints.length > 20 ? ` +${allEndpoints.length - 20} more` : ""}`);
|
|
249
|
+
}
|
|
250
|
+
if (ctx.middleware.length > 0) lines.push(`Middleware: ${ctx.middleware.join(", ")}`);
|
|
251
|
+
if (ctx.database.type) lines.push(`Database: ${ctx.database.type}${ctx.database.tables.length > 0 ? ` (tables: ${ctx.database.tables.join(", ")})` : ""}${ctx.database.hasRedis ? " + Redis" : ""}`);
|
|
252
|
+
if (ctx.envVars.length > 0) lines.push(`Env vars used: ${ctx.envVars.slice(0, 15).join(", ")}${ctx.envVars.length > 15 ? ` +${ctx.envVars.length - 15} more` : ""}`);
|
|
253
|
+
if (ctx.structure.length > 0) lines.push(`Server files: ${ctx.structure.length}`);
|
|
254
|
+
if (ctx.version) lines.push(`Version: ${ctx.version}`);
|
|
255
|
+
|
|
256
|
+
return lines.length > 0 ? "SERVER CONTEXT:\n" + lines.join("\n") : "";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function _listFiles(dir, ext) {
|
|
260
|
+
const results = [];
|
|
261
|
+
let count = 0;
|
|
262
|
+
const walk = (d) => {
|
|
263
|
+
if (count > MAX_FILE_SCAN) return;
|
|
264
|
+
try {
|
|
265
|
+
for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
|
|
266
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
267
|
+
const full = path.join(d, entry.name);
|
|
268
|
+
if (entry.isDirectory()) walk(full);
|
|
269
|
+
else if (!ext || entry.name.endsWith(ext)) { results.push(full); count++; }
|
|
270
|
+
}
|
|
271
|
+
} catch {}
|
|
272
|
+
};
|
|
273
|
+
walk(dir);
|
|
274
|
+
return results;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = { scan, load, getSummary, CONTEXT_PATH };
|
package/src/core/wolverine.js
CHANGED
|
@@ -208,6 +208,12 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
let brainContext = "";
|
|
211
|
+
// Inject server context (routes, DB, config, deps) if available
|
|
212
|
+
try {
|
|
213
|
+
const { getSummary } = require("./server-context");
|
|
214
|
+
const serverCtx = getSummary(cwd);
|
|
215
|
+
if (serverCtx) brainContext += serverCtx + "\n\n";
|
|
216
|
+
} catch {}
|
|
211
217
|
// Inject relevant skill context (claw-code: pre-enrich prompt with matched tools)
|
|
212
218
|
if (skills) {
|
|
213
219
|
const skillCtx = skills.buildContext(parsed.errorMessage);
|