wolverine-ai 4.7.0 → 4.8.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 +1 -1
- package/src/agent/agent-engine.js +98 -2
- package/src/brain/brain.js +1 -1
- package/src/core/ai-client.js +5 -0
- package/src/core/config.js +6 -2
- package/src/core/runner.js +36 -27
- package/src/core/wolverine.js +17 -17
- package/src/skills/deps.js +5 -0
- package/src/vault/vault-manager.js +37 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wolverine-ai",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.8.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": {
|
|
@@ -121,7 +121,7 @@ const TOOL_DEFINITIONS = [
|
|
|
121
121
|
type: "object",
|
|
122
122
|
properties: {
|
|
123
123
|
command: { type: "string", description: "Shell command to execute" },
|
|
124
|
-
timeout: { type: "number", description: "Timeout in ms (default:
|
|
124
|
+
timeout: { type: "number", description: "Timeout in ms (default: 30000, max: 60000)" },
|
|
125
125
|
},
|
|
126
126
|
required: ["command"],
|
|
127
127
|
},
|
|
@@ -442,6 +442,22 @@ const TOOL_DEFINITIONS = [
|
|
|
442
442
|
},
|
|
443
443
|
},
|
|
444
444
|
},
|
|
445
|
+
// ── ENVIRONMENT ──
|
|
446
|
+
{
|
|
447
|
+
type: "function",
|
|
448
|
+
function: {
|
|
449
|
+
name: "add_env_var",
|
|
450
|
+
description: "Append a key=value pair to .env.local. Only adds if the key does not already exist. Use for missing environment variable errors.",
|
|
451
|
+
parameters: {
|
|
452
|
+
type: "object",
|
|
453
|
+
properties: {
|
|
454
|
+
key: { type: "string", description: "Environment variable name (e.g. DATABASE_URL)" },
|
|
455
|
+
value: { type: "string", description: "Value to set" },
|
|
456
|
+
},
|
|
457
|
+
required: ["key", "value"],
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
},
|
|
445
461
|
// ── TASK MANAGEMENT ──
|
|
446
462
|
{
|
|
447
463
|
type: "function",
|
|
@@ -814,6 +830,7 @@ class AgentEngine {
|
|
|
814
830
|
case "check_file_descriptors": return this._checkFileDescriptors(args);
|
|
815
831
|
case "check_event_loop": return this._checkEventLoop(args);
|
|
816
832
|
case "check_websocket": return this._checkWebsocket(args);
|
|
833
|
+
case "add_env_var": return this._addEnvVar(args);
|
|
817
834
|
case "done": return this._done(args);
|
|
818
835
|
// Legacy aliases
|
|
819
836
|
case "list_files": return this._globFiles({ pattern: (args.dir || ".") + "/*" + (args.pattern || "") });
|
|
@@ -1204,7 +1221,8 @@ class AgentEngine {
|
|
|
1204
1221
|
try {
|
|
1205
1222
|
let Database;
|
|
1206
1223
|
try { Database = require("better-sqlite3"); } catch {
|
|
1207
|
-
|
|
1224
|
+
// Fallback: try PostgreSQL via pg if available
|
|
1225
|
+
return this._inspectDbPg(args);
|
|
1208
1226
|
}
|
|
1209
1227
|
const db = new Database(dbPath, { readonly: true });
|
|
1210
1228
|
let result;
|
|
@@ -1236,6 +1254,55 @@ class AgentEngine {
|
|
|
1236
1254
|
} catch (e) { return { content: `DB error: ${e.message}` }; }
|
|
1237
1255
|
}
|
|
1238
1256
|
|
|
1257
|
+
async _inspectDbPg(args) {
|
|
1258
|
+
let pg;
|
|
1259
|
+
try { pg = require("pg"); } catch {
|
|
1260
|
+
return { content: "Neither better-sqlite3 nor pg is installed. Run: npm install better-sqlite3 (for SQLite) or npm install pg (for PostgreSQL)" };
|
|
1261
|
+
}
|
|
1262
|
+
// db_path is treated as a connection string or uses DATABASE_URL
|
|
1263
|
+
const connectionString = args.db_path.startsWith("postgres")
|
|
1264
|
+
? args.db_path
|
|
1265
|
+
: process.env.DATABASE_URL;
|
|
1266
|
+
if (!connectionString) {
|
|
1267
|
+
return { content: "PostgreSQL: no connection string. Set DATABASE_URL or pass a postgres:// URI as db_path." };
|
|
1268
|
+
}
|
|
1269
|
+
const client = new pg.Client({ connectionString, statement_timeout: 10000 });
|
|
1270
|
+
try {
|
|
1271
|
+
await client.connect();
|
|
1272
|
+
let result;
|
|
1273
|
+
if (args.action === "tables") {
|
|
1274
|
+
const res = await client.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename");
|
|
1275
|
+
result = res.rows.map(r => r.tablename).join("\n") || "(no tables)";
|
|
1276
|
+
} else if (args.action === "schema") {
|
|
1277
|
+
const res = await client.query(`SELECT table_name, column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position`);
|
|
1278
|
+
const tables = {};
|
|
1279
|
+
for (const row of res.rows) {
|
|
1280
|
+
if (!tables[row.table_name]) tables[row.table_name] = [];
|
|
1281
|
+
tables[row.table_name].push(` ${row.column_name} ${row.data_type}${row.is_nullable === "NO" ? " NOT NULL" : ""}`);
|
|
1282
|
+
}
|
|
1283
|
+
result = Object.entries(tables).map(([t, cols]) => `TABLE ${t}:\n${cols.join("\n")}`).join("\n\n") || "(no tables)";
|
|
1284
|
+
} else if (args.action === "query") {
|
|
1285
|
+
if (!args.sql) return { content: "Error: sql required for query action" };
|
|
1286
|
+
const upper = args.sql.trim().toUpperCase();
|
|
1287
|
+
if (!upper.startsWith("SELECT") && !upper.startsWith("SHOW")) {
|
|
1288
|
+
return { content: "BLOCKED: inspect_db only allows SELECT/SHOW for PostgreSQL. Use run_db_fix for writes." };
|
|
1289
|
+
}
|
|
1290
|
+
const res = await client.query(args.sql);
|
|
1291
|
+
result = JSON.stringify(res.rows.slice(0, 50), null, 2);
|
|
1292
|
+
if (res.rows.length > 50) result += `\n... (${res.rows.length} total rows, showing first 50)`;
|
|
1293
|
+
} else {
|
|
1294
|
+
result = "Unknown action. Use: tables, schema, or query";
|
|
1295
|
+
}
|
|
1296
|
+
const { redact } = require("../security/secret-redactor");
|
|
1297
|
+
console.log(chalk.gray(` 🗃️ DB (pg) ${args.action}: ${args.db_path}`));
|
|
1298
|
+
return { content: redact(result) };
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
return { content: `PostgreSQL error: ${e.message}` };
|
|
1301
|
+
} finally {
|
|
1302
|
+
try { await client.end(); } catch {}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1239
1306
|
_runDbFix(args) {
|
|
1240
1307
|
const dbPath = path.resolve(this.cwd, args.db_path);
|
|
1241
1308
|
try {
|
|
@@ -1752,6 +1819,35 @@ class AgentEngine {
|
|
|
1752
1819
|
};
|
|
1753
1820
|
}
|
|
1754
1821
|
|
|
1822
|
+
// ── Environment variable tool ──
|
|
1823
|
+
// Controlled exception to _isProtectedPath: allows APPENDING to .env.local only.
|
|
1824
|
+
_addEnvVar(args) {
|
|
1825
|
+
const key = (args.key || "").trim();
|
|
1826
|
+
const value = args.value || "";
|
|
1827
|
+
if (!key || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1828
|
+
return { content: "Error: invalid env var name. Must match [A-Za-z_][A-Za-z0-9_]*" };
|
|
1829
|
+
}
|
|
1830
|
+
const envPath = path.resolve(this.cwd, ".env.local");
|
|
1831
|
+
try {
|
|
1832
|
+
// Check if key already exists
|
|
1833
|
+
if (fs.existsSync(envPath)) {
|
|
1834
|
+
const existing = fs.readFileSync(envPath, "utf-8");
|
|
1835
|
+
const keyRegex = new RegExp(`^${key}=`, "m");
|
|
1836
|
+
if (keyRegex.test(existing)) {
|
|
1837
|
+
return { content: `${key} already exists in .env.local — not overwriting.` };
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
// Append the key=value pair
|
|
1841
|
+
const line = `${key}=${value}\n`;
|
|
1842
|
+
fs.appendFileSync(envPath, line, "utf-8");
|
|
1843
|
+
console.log(chalk.green(` 🔑 Added ${key} to .env.local`));
|
|
1844
|
+
if (this.logger) this.logger.info("agent.env_var_add", `Added ${key} to .env.local`);
|
|
1845
|
+
return { content: `Successfully added ${key} to .env.local` };
|
|
1846
|
+
} catch (err) {
|
|
1847
|
+
return { content: `Error adding env var: ${err.message}` };
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1755
1851
|
// ── Protected path guard ──
|
|
1756
1852
|
// Wolverine's own source code is off-limits to the agent.
|
|
1757
1853
|
// The agent should build/fix the USER's project, not modify itself.
|
package/src/brain/brain.js
CHANGED
|
@@ -154,7 +154,7 @@ const SEED_DOCS = [
|
|
|
154
154
|
metadata: { topic: "fastify" },
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
|
-
text: "npm package: wolverine-ai on npmjs.com (
|
|
157
|
+
text: "npm package: wolverine-ai on npmjs.com (latest). Install: npm i wolverine-ai. CLI: npx wolverine server/index.js. 85 files, 190KB compressed. Includes src/, bin/, examples/. Server directory created from src/templates/server/ on first run (never overwritten). GitHub: https://github.com/bobbyswhip/Wolverine. Unified billing: all AI calls route through inference proxy with credit-based billing. WOLVERINE_API_KEY authenticates through billing proxy, WOLVERINE_GPU_KEY for direct GPU access. 3 providers: openai, anthropic, wolverine (self-hosted GPU via Vast.ai).",
|
|
158
158
|
metadata: { topic: "npm-package" },
|
|
159
159
|
},
|
|
160
160
|
{
|
package/src/core/ai-client.js
CHANGED
|
@@ -659,6 +659,11 @@ Include both if needed, or just one.`;
|
|
|
659
659
|
const result = await aiCall({ model, systemPrompt, userPrompt, maxTokens: 2048, category: "reasoning" });
|
|
660
660
|
const content = (result.content || "").trim();
|
|
661
661
|
|
|
662
|
+
// Guard: cap length before regex extraction to prevent catastrophic backtracking
|
|
663
|
+
if (content.length > 50000) {
|
|
664
|
+
throw new Error("AI response too large for regex extraction (>50K chars)");
|
|
665
|
+
}
|
|
666
|
+
|
|
662
667
|
// Strip thinking tags (Gemma), markdown fences, and any prefix text
|
|
663
668
|
let cleaned = content
|
|
664
669
|
.replace(/<\|channel>.*?<channel\|>/gs, "")
|
package/src/core/config.js
CHANGED
|
@@ -15,11 +15,14 @@ const path = require("path");
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
let _config = null;
|
|
18
|
+
let _configRoot = null;
|
|
19
|
+
|
|
20
|
+
function setConfigRoot(root) { _configRoot = root; }
|
|
18
21
|
|
|
19
22
|
function loadConfig() {
|
|
20
23
|
if (_config) return _config;
|
|
21
24
|
|
|
22
|
-
const configPath = path.join(process.cwd(), "server", "config", "settings.json");
|
|
25
|
+
const configPath = path.join(_configRoot || process.cwd(), "server", "config", "settings.json");
|
|
23
26
|
let fileConfig = {};
|
|
24
27
|
if (fs.existsSync(configPath)) {
|
|
25
28
|
try {
|
|
@@ -109,6 +112,7 @@ function getConfig(dotPath) {
|
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
function resetConfig() { _config = null; }
|
|
115
|
+
function resetConfigRoot() { _configRoot = null; }
|
|
112
116
|
|
|
113
117
|
/**
|
|
114
118
|
* Migrate old provider-based config to new flat models format.
|
|
@@ -153,4 +157,4 @@ function _migrateAndEnsureDefaults(fileConfig, configPath) {
|
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
module.exports = { loadConfig, getConfig, resetConfig };
|
|
160
|
+
module.exports = { loadConfig, getConfig, resetConfig, setConfigRoot, resetConfigRoot };
|
package/src/core/runner.js
CHANGED
|
@@ -45,14 +45,30 @@ class WolverineRunner {
|
|
|
45
45
|
this.child = null;
|
|
46
46
|
this.running = false;
|
|
47
47
|
|
|
48
|
+
// Stability tracking
|
|
49
|
+
this._lastStartTime = null;
|
|
50
|
+
this._lastBackupId = null;
|
|
51
|
+
this._stabilityTimer = null;
|
|
52
|
+
this._stderrBuffer = "";
|
|
53
|
+
this._healInProgress = false;
|
|
54
|
+
this._healStatus = null; // { active, file, error, phase, startedAt, iteration }
|
|
55
|
+
|
|
56
|
+
this._initSubsystems(options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize all subsystems — extracted from constructor for readability.
|
|
61
|
+
*/
|
|
62
|
+
_initSubsystems(options) {
|
|
48
63
|
// Core subsystems
|
|
49
64
|
this.sandbox = new Sandbox(this.cwd);
|
|
50
65
|
this.redactor = initRedactor(this.cwd);
|
|
66
|
+
const cfg = loadConfig();
|
|
51
67
|
this.rateLimiter = new RateLimiter({
|
|
52
|
-
maxCallsPerWindow:
|
|
53
|
-
windowMs:
|
|
54
|
-
minGapMs:
|
|
55
|
-
maxTokensPerHour:
|
|
68
|
+
maxCallsPerWindow: cfg.rateLimiting.maxCallsPerWindow,
|
|
69
|
+
windowMs: cfg.rateLimiting.windowMs,
|
|
70
|
+
minGapMs: cfg.rateLimiting.minGapMs,
|
|
71
|
+
maxTokensPerHour: cfg.rateLimiting.maxTokensPerHour,
|
|
56
72
|
maxGlobalHealsPerWindow: parseInt(process.env.WOLVERINE_RATE_MAX_GLOBAL_HEALS, 10) || 5,
|
|
57
73
|
globalWindowMs: parseInt(process.env.WOLVERINE_RATE_GLOBAL_WINDOW_MS, 10) || 300000,
|
|
58
74
|
});
|
|
@@ -68,14 +84,14 @@ class WolverineRunner {
|
|
|
68
84
|
});
|
|
69
85
|
|
|
70
86
|
// Health monitoring
|
|
71
|
-
const port =
|
|
87
|
+
const port = cfg.server.port;
|
|
72
88
|
this.healthMonitor = new HealthMonitor({
|
|
73
89
|
port,
|
|
74
90
|
path: options.healthPath || "/health",
|
|
75
|
-
intervalMs:
|
|
76
|
-
timeoutMs:
|
|
77
|
-
failThreshold:
|
|
78
|
-
startDelayMs:
|
|
91
|
+
intervalMs: cfg.healthCheck.intervalMs,
|
|
92
|
+
timeoutMs: cfg.healthCheck.timeoutMs,
|
|
93
|
+
failThreshold: cfg.healthCheck.failThreshold,
|
|
94
|
+
startDelayMs: cfg.healthCheck.startDelayMs,
|
|
79
95
|
});
|
|
80
96
|
|
|
81
97
|
// Performance monitoring
|
|
@@ -89,6 +105,9 @@ class WolverineRunner {
|
|
|
89
105
|
// Process monitor — heartbeat, memory, CPU, leak detection
|
|
90
106
|
this.processMonitor = new ProcessMonitor({ logger: this.logger });
|
|
91
107
|
|
|
108
|
+
// Brain — semantic memory + project context
|
|
109
|
+
this.brain = new Brain(this.cwd);
|
|
110
|
+
|
|
92
111
|
// Route prober — tests all routes periodically
|
|
93
112
|
this.routeProber = new RouteProber({
|
|
94
113
|
port,
|
|
@@ -98,9 +117,9 @@ class WolverineRunner {
|
|
|
98
117
|
|
|
99
118
|
// Error monitor — detects caught 500 errors without process crash
|
|
100
119
|
this.errorMonitor = new ErrorMonitor({
|
|
101
|
-
threshold:
|
|
102
|
-
windowMs:
|
|
103
|
-
cooldownMs:
|
|
120
|
+
threshold: cfg.errorMonitor.threshold,
|
|
121
|
+
windowMs: cfg.errorMonitor.windowMs,
|
|
122
|
+
cooldownMs: cfg.errorMonitor.cooldownMs,
|
|
104
123
|
logger: this.logger,
|
|
105
124
|
onError: (routePath, errorDetails) => this._healFromError(routePath, errorDetails),
|
|
106
125
|
});
|
|
@@ -111,9 +130,6 @@ class WolverineRunner {
|
|
|
111
130
|
windowMs: parseInt(process.env.WOLVERINE_LOOP_WINDOW_MS, 10) || 600000,
|
|
112
131
|
});
|
|
113
132
|
|
|
114
|
-
// Brain — semantic memory + project context
|
|
115
|
-
this.brain = new Brain(this.cwd);
|
|
116
|
-
|
|
117
133
|
// Skills — discoverable capabilities
|
|
118
134
|
this.skills = new SkillRegistry();
|
|
119
135
|
this.skills.load();
|
|
@@ -143,14 +159,6 @@ class WolverineRunner {
|
|
|
143
159
|
routeProber: this.routeProber,
|
|
144
160
|
errorMonitor: this.errorMonitor,
|
|
145
161
|
});
|
|
146
|
-
|
|
147
|
-
// Stability tracking
|
|
148
|
-
this._lastStartTime = null;
|
|
149
|
-
this._lastBackupId = null;
|
|
150
|
-
this._stabilityTimer = null;
|
|
151
|
-
this._stderrBuffer = "";
|
|
152
|
-
this._healInProgress = false;
|
|
153
|
-
this._healStatus = null; // { active, file, error, phase, startedAt, iteration }
|
|
154
162
|
}
|
|
155
163
|
|
|
156
164
|
async start() {
|
|
@@ -465,9 +473,10 @@ class WolverineRunner {
|
|
|
465
473
|
this._healInProgress = false;
|
|
466
474
|
return;
|
|
467
475
|
}
|
|
468
|
-
//
|
|
476
|
+
// Pass through directly — _healAndRestart checks _healInProgress internally,
|
|
477
|
+
// so release it just before the call to avoid a race window
|
|
469
478
|
this._healInProgress = false;
|
|
470
|
-
await this._healAndRestart();
|
|
479
|
+
await this._healAndRestart({ skipHealLockCheck: true });
|
|
471
480
|
} catch (err) {
|
|
472
481
|
// #5: Prevent unhandled errors in health callback from crashing the parent
|
|
473
482
|
console.log(chalk.red(` ⚠️ Health callback error: ${err.message}`));
|
|
@@ -570,8 +579,8 @@ class WolverineRunner {
|
|
|
570
579
|
this.errorMonitor.reset();
|
|
571
580
|
}
|
|
572
581
|
|
|
573
|
-
async _healAndRestart() {
|
|
574
|
-
if (this._healInProgress) return;
|
|
582
|
+
async _healAndRestart(options) {
|
|
583
|
+
if (this._healInProgress && !options?.skipHealLockCheck) return;
|
|
575
584
|
// #9: Bail if stop() was called during the window between crash and heal
|
|
576
585
|
if (this._shuttingDown) return;
|
|
577
586
|
this._healInProgress = true;
|
package/src/core/wolverine.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const { execSync } = require("child_process");
|
|
1
5
|
const chalk = require("chalk");
|
|
2
6
|
const { parseError } = require("./error-parser");
|
|
3
|
-
const { requestRepair, getClient, aiCall, _trackOp } = require("./ai-client");
|
|
7
|
+
const { requestRepair, getClient, aiCall, _trackOp, getTrackerSnapshot } = require("./ai-client");
|
|
4
8
|
const { getModel } = require("./models");
|
|
5
9
|
const { applyPatch } = require("./patcher");
|
|
6
10
|
const { verifyFix } = require("./verifier");
|
|
7
11
|
const { Sandbox, SandboxViolationError } = require("../security/sandbox");
|
|
8
12
|
const { RateLimiter } = require("../security/rate-limiter");
|
|
9
13
|
const { detectInjection } = require("../security/injection-detector");
|
|
14
|
+
const { redact, hasSecrets } = require("../security/secret-redactor");
|
|
10
15
|
const { BackupManager } = require("../backup/backup-manager");
|
|
11
16
|
const { AgentEngine } = require("../agent/agent-engine");
|
|
12
17
|
const { ResearchAgent } = require("../agent/research-agent");
|
|
@@ -14,6 +19,7 @@ const { GoalLoop } = require("../agent/goal-loop");
|
|
|
14
19
|
const { exploreAndFix, spawnParallel } = require("../agent/sub-agents");
|
|
15
20
|
const { EVENT_TYPES } = require("../logger/event-logger");
|
|
16
21
|
const { diagnose: diagnoseDeps } = require("../skills/deps");
|
|
22
|
+
const { getSummary: getServerContextSummary } = require("./server-context");
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
25
|
* The Wolverine healing engine — v3.
|
|
@@ -55,9 +61,7 @@ async function heal(opts) {
|
|
|
55
61
|
async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager, logger, brain, mcp, skills, repairHistory, routeContext }) {
|
|
56
62
|
const healStartTime = Date.now();
|
|
57
63
|
// Snapshot token tracker at heal start — diff at end = FULL pipeline cost
|
|
58
|
-
const { getTrackerSnapshot } = require("./ai-client");
|
|
59
64
|
const _snapshot = getTrackerSnapshot();
|
|
60
|
-
const { redact, hasSecrets } = require("../security/secret-redactor");
|
|
61
65
|
|
|
62
66
|
// Guard: don't burn tokens on empty stderr (signal kills, clean shutdowns, etc.)
|
|
63
67
|
if (!stderr || stderr.trim().length < 10) {
|
|
@@ -191,8 +195,6 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
191
195
|
let backupSourceCode = "";
|
|
192
196
|
if (hasFile && backupManager) {
|
|
193
197
|
try {
|
|
194
|
-
const fs = require("fs");
|
|
195
|
-
const path = require("path");
|
|
196
198
|
const stableBackups = backupManager.getAll().filter(b => b.status === "stable" || b.status === "verified");
|
|
197
199
|
if (stableBackups.length > 0) {
|
|
198
200
|
const latest = stableBackups[stableBackups.length - 1];
|
|
@@ -210,8 +212,7 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
210
212
|
let brainContext = "";
|
|
211
213
|
// Inject server context (routes, DB, config, deps) if available
|
|
212
214
|
try {
|
|
213
|
-
const
|
|
214
|
-
const serverCtx = getSummary(cwd);
|
|
215
|
+
const serverCtx = getServerContextSummary(cwd);
|
|
215
216
|
if (serverCtx) brainContext += serverCtx + "\n\n";
|
|
216
217
|
} catch {}
|
|
217
218
|
// Inject relevant skill context (claw-code: pre-enrich prompt with matched tools)
|
|
@@ -348,7 +349,6 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
348
349
|
|
|
349
350
|
// Execute shell commands first (npm install, mkdir, etc.)
|
|
350
351
|
if (repair.commands && Array.isArray(repair.commands)) {
|
|
351
|
-
const { execSync } = require("child_process");
|
|
352
352
|
for (const cmd of repair.commands) {
|
|
353
353
|
// Block dangerous commands
|
|
354
354
|
if (/rm\s+-rf\s+[/\\]|format\s+c:|mkfs/i.test(cmd)) {
|
|
@@ -554,7 +554,6 @@ async function _healImpl({ stderr, cwd, sandbox, notifier, rateLimiter, backupMa
|
|
|
554
554
|
* Returns { fixed: boolean, action: string }
|
|
555
555
|
*/
|
|
556
556
|
async function tryOperationalFix(parsed, cwd, logger, sandbox) {
|
|
557
|
-
const { execSync } = require("child_process");
|
|
558
557
|
const msg = parsed.errorMessage || "";
|
|
559
558
|
|
|
560
559
|
// Pattern 1: Dependency issues — use deps skill for structured diagnosis
|
|
@@ -581,8 +580,6 @@ async function tryOperationalFix(parsed, cwd, logger, sandbox) {
|
|
|
581
580
|
|| msg.match(/cannot find.*?'([^']+\.\w+)'/i);
|
|
582
581
|
if (enoent) {
|
|
583
582
|
const missingFile = enoent[1];
|
|
584
|
-
const fs = require("fs");
|
|
585
|
-
const path = require("path");
|
|
586
583
|
|
|
587
584
|
// Only auto-create if it's inside the project and looks like a config/data file
|
|
588
585
|
const rel = path.relative(cwd, missingFile).replace(/\\/g, "/");
|
|
@@ -639,7 +636,6 @@ async function tryOperationalFix(parsed, cwd, logger, sandbox) {
|
|
|
639
636
|
const permFile = msg.match(/(?:EACCES|EPERM).*?'([^']+)'/);
|
|
640
637
|
if (permFile) {
|
|
641
638
|
try {
|
|
642
|
-
const fs = require("fs");
|
|
643
639
|
fs.chmodSync(permFile[1], 0o755);
|
|
644
640
|
console.log(chalk.blue(` 🔑 Fixed permissions on: ${permFile[1]}`));
|
|
645
641
|
return { fixed: true, action: `Fixed permissions (chmod 755) on: ${permFile[1]}` };
|
|
@@ -671,7 +667,6 @@ async function tryOperationalFix(parsed, cwd, logger, sandbox) {
|
|
|
671
667
|
// Pattern 5: ENOSPC — disk full, try automated cleanup
|
|
672
668
|
if (/ENOSPC/.test(msg)) {
|
|
673
669
|
try {
|
|
674
|
-
const os = require("os");
|
|
675
670
|
const backupDir = path.join(os.homedir(), ".wolverine-safe-backups", "snapshots");
|
|
676
671
|
let cleaned = 0;
|
|
677
672
|
if (fs.existsSync(backupDir)) {
|
|
@@ -717,9 +712,6 @@ async function tryOperationalFix(parsed, cwd, logger, sandbox) {
|
|
|
717
712
|
* Returns a JSON string with empty/default values, or null if can't infer.
|
|
718
713
|
*/
|
|
719
714
|
function _inferJsonConfig(missingFile, cwd, parsed) {
|
|
720
|
-
const fs = require("fs");
|
|
721
|
-
const path = require("path");
|
|
722
|
-
|
|
723
715
|
// Find which source file loads the missing config
|
|
724
716
|
const basename = path.basename(missingFile);
|
|
725
717
|
const sourceFile = parsed.filePath;
|
|
@@ -728,10 +720,18 @@ function _inferJsonConfig(missingFile, cwd, parsed) {
|
|
|
728
720
|
// #17: Escape all regex special characters in basename to prevent regex injection
|
|
729
721
|
const escapedBasename = basename.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
730
722
|
|
|
723
|
+
// #23: Guard against regex construction failure on unusual filenames
|
|
724
|
+
let configVarRegex;
|
|
725
|
+
try {
|
|
726
|
+
configVarRegex = new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*(?:require|JSON\\.parse).*${escapedBasename}`);
|
|
727
|
+
} catch {
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
731
|
try {
|
|
732
732
|
const source = fs.readFileSync(sourceFile, "utf-8");
|
|
733
733
|
// Look for property accesses on the loaded config: config.apiUrl, config.timeout, etc.
|
|
734
|
-
const configVarMatch = source.match(
|
|
734
|
+
const configVarMatch = source.match(configVarRegex);
|
|
735
735
|
if (!configVarMatch) return null;
|
|
736
736
|
|
|
737
737
|
const varName = configVarMatch[1];
|
package/src/skills/deps.js
CHANGED
|
@@ -161,14 +161,19 @@ function findUnused(cwd) {
|
|
|
161
161
|
|
|
162
162
|
// Scan all .js/.ts/.mjs/.cjs files for require/import statements
|
|
163
163
|
const usedPackages = new Set();
|
|
164
|
+
let _fileCount = 0;
|
|
165
|
+
const FILE_SCAN_CAP = 5000;
|
|
164
166
|
const scanDir = (dir) => {
|
|
167
|
+
if (_fileCount >= FILE_SCAN_CAP) return;
|
|
165
168
|
let entries;
|
|
166
169
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
167
170
|
for (const entry of entries) {
|
|
171
|
+
if (_fileCount >= FILE_SCAN_CAP) return;
|
|
168
172
|
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".wolverine") continue;
|
|
169
173
|
const fullPath = path.join(dir, entry.name);
|
|
170
174
|
if (entry.isDirectory()) { scanDir(fullPath); continue; }
|
|
171
175
|
if (!/\.(js|ts|mjs|cjs|jsx|tsx)$/.test(entry.name)) continue;
|
|
176
|
+
_fileCount++;
|
|
172
177
|
try {
|
|
173
178
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
174
179
|
// Match require("X") and import ... from "X"
|
|
@@ -16,9 +16,9 @@ const path = require("path");
|
|
|
16
16
|
* - Survives git pull, npm install, auto-update (lives in .wolverine/)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
const VAULT_DIR = () => path.join(process.cwd(), ".wolverine", "vault");
|
|
20
|
-
const MASTER_KEY_PATH = () => path.join(VAULT_DIR(), "master.key");
|
|
21
|
-
const ETH_VAULT_PATH = () => path.join(VAULT_DIR(), "eth.vault");
|
|
19
|
+
const VAULT_DIR = (projectRoot) => path.join(projectRoot || process.cwd(), ".wolverine", "vault");
|
|
20
|
+
const MASTER_KEY_PATH = (projectRoot) => path.join(VAULT_DIR(projectRoot), "master.key");
|
|
21
|
+
const ETH_VAULT_PATH = (projectRoot) => path.join(VAULT_DIR(projectRoot), "eth.vault");
|
|
22
22
|
|
|
23
23
|
const ALGORITHM = "aes-256-gcm";
|
|
24
24
|
const IV_LENGTH = 16;
|
|
@@ -28,26 +28,26 @@ const AUTH_TAG_LENGTH = 16;
|
|
|
28
28
|
* Initialize the vault. Idempotent — creates keys only if missing.
|
|
29
29
|
* Called during runner startup before any server code runs.
|
|
30
30
|
*/
|
|
31
|
-
async function initVault() {
|
|
32
|
-
const vaultDir = VAULT_DIR();
|
|
31
|
+
async function initVault(projectRoot) {
|
|
32
|
+
const vaultDir = VAULT_DIR(projectRoot);
|
|
33
33
|
fs.mkdirSync(vaultDir, { recursive: true });
|
|
34
34
|
|
|
35
35
|
let created = false;
|
|
36
36
|
|
|
37
37
|
// Master encryption key
|
|
38
|
-
if (!fs.existsSync(MASTER_KEY_PATH())) {
|
|
38
|
+
if (!fs.existsSync(MASTER_KEY_PATH(projectRoot))) {
|
|
39
39
|
const masterKey = crypto.randomBytes(32);
|
|
40
|
-
fs.writeFileSync(MASTER_KEY_PATH(), masterKey);
|
|
41
|
-
try { fs.chmodSync(MASTER_KEY_PATH(), 0o600); } catch {}
|
|
40
|
+
fs.writeFileSync(MASTER_KEY_PATH(projectRoot), masterKey);
|
|
41
|
+
try { fs.chmodSync(MASTER_KEY_PATH(projectRoot), 0o600); } catch {}
|
|
42
42
|
masterKey.fill(0);
|
|
43
43
|
created = true;
|
|
44
44
|
console.log(" 🔐 Vault: master encryption key generated");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// Ethereum private key (encrypted)
|
|
48
|
-
if (!fs.existsSync(ETH_VAULT_PATH())) {
|
|
48
|
+
if (!fs.existsSync(ETH_VAULT_PATH(projectRoot))) {
|
|
49
49
|
const ethKey = crypto.randomBytes(32);
|
|
50
|
-
await encryptAndStore(ethKey);
|
|
50
|
+
await encryptAndStore(ethKey, { projectRoot });
|
|
51
51
|
ethKey.fill(0);
|
|
52
52
|
created = true;
|
|
53
53
|
console.log(" 🔐 Vault: ethereum wallet created");
|
|
@@ -59,18 +59,19 @@ async function initVault() {
|
|
|
59
59
|
/**
|
|
60
60
|
* Check if vault is fully initialized.
|
|
61
61
|
*/
|
|
62
|
-
function isVaultInitialized() {
|
|
63
|
-
return fs.existsSync(MASTER_KEY_PATH()) && fs.existsSync(ETH_VAULT_PATH());
|
|
62
|
+
function isVaultInitialized(projectRoot) {
|
|
63
|
+
return fs.existsSync(MASTER_KEY_PATH(projectRoot)) && fs.existsSync(ETH_VAULT_PATH(projectRoot));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Encrypt a private key Buffer and write to eth.vault.
|
|
68
68
|
* Wipes the master key from memory after use.
|
|
69
69
|
*/
|
|
70
|
-
async function encryptAndStore(keyBuf) {
|
|
70
|
+
async function encryptAndStore(keyBuf, options) {
|
|
71
|
+
const projectRoot = options?.projectRoot;
|
|
71
72
|
let masterKey = null;
|
|
72
73
|
try {
|
|
73
|
-
masterKey = fs.readFileSync(MASTER_KEY_PATH());
|
|
74
|
+
masterKey = fs.readFileSync(MASTER_KEY_PATH(projectRoot));
|
|
74
75
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
75
76
|
const cipher = crypto.createCipheriv(ALGORITHM, masterKey, iv);
|
|
76
77
|
const encrypted = Buffer.concat([cipher.update(keyBuf), cipher.final()]);
|
|
@@ -83,13 +84,13 @@ async function encryptAndStore(keyBuf) {
|
|
|
83
84
|
authTag: authTag.toString("hex"),
|
|
84
85
|
ciphertext: encrypted.toString("hex"),
|
|
85
86
|
created: new Date().toISOString(),
|
|
86
|
-
rotated: null,
|
|
87
|
+
rotated: options?.rotated || null,
|
|
87
88
|
};
|
|
88
89
|
|
|
89
|
-
const tmpPath = ETH_VAULT_PATH() + ".tmp";
|
|
90
|
+
const tmpPath = ETH_VAULT_PATH(projectRoot) + ".tmp";
|
|
90
91
|
fs.writeFileSync(tmpPath, JSON.stringify(vault, null, 2), "utf-8");
|
|
91
|
-
fs.renameSync(tmpPath, ETH_VAULT_PATH());
|
|
92
|
-
try { fs.chmodSync(ETH_VAULT_PATH(), 0o600); } catch {}
|
|
92
|
+
fs.renameSync(tmpPath, ETH_VAULT_PATH(projectRoot));
|
|
93
|
+
try { fs.chmodSync(ETH_VAULT_PATH(projectRoot), 0o600); } catch {}
|
|
93
94
|
} finally {
|
|
94
95
|
if (masterKey) masterKey.fill(0);
|
|
95
96
|
}
|
|
@@ -99,15 +100,15 @@ async function encryptAndStore(keyBuf) {
|
|
|
99
100
|
* Decrypt the Ethereum private key. Returns a Buffer.
|
|
100
101
|
* CALLER MUST call .fill(0) on the returned Buffer when done.
|
|
101
102
|
*/
|
|
102
|
-
function decryptPrivateKey() {
|
|
103
|
-
if (!isVaultInitialized()) {
|
|
103
|
+
function decryptPrivateKey(projectRoot) {
|
|
104
|
+
if (!isVaultInitialized(projectRoot)) {
|
|
104
105
|
throw new Error("vault not initialized");
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
let masterKey = null;
|
|
108
109
|
try {
|
|
109
|
-
masterKey = fs.readFileSync(MASTER_KEY_PATH());
|
|
110
|
-
const vault = JSON.parse(fs.readFileSync(ETH_VAULT_PATH(), "utf-8"));
|
|
110
|
+
masterKey = fs.readFileSync(MASTER_KEY_PATH(projectRoot));
|
|
111
|
+
const vault = JSON.parse(fs.readFileSync(ETH_VAULT_PATH(projectRoot), "utf-8"));
|
|
111
112
|
|
|
112
113
|
if (vault.version !== 1) throw new Error("unsupported vault version");
|
|
113
114
|
|
|
@@ -128,16 +129,11 @@ function decryptPrivateKey() {
|
|
|
128
129
|
* Re-encrypt with a fresh IV. Defensive measure if key material was
|
|
129
130
|
* potentially exposed in an error message.
|
|
130
131
|
*/
|
|
131
|
-
async function rotateEncryption() {
|
|
132
|
+
async function rotateEncryption(projectRoot) {
|
|
132
133
|
let keyBuf = null;
|
|
133
134
|
try {
|
|
134
|
-
keyBuf = decryptPrivateKey();
|
|
135
|
-
await encryptAndStore(keyBuf);
|
|
136
|
-
|
|
137
|
-
// Update rotated timestamp
|
|
138
|
-
const vault = JSON.parse(fs.readFileSync(ETH_VAULT_PATH(), "utf-8"));
|
|
139
|
-
vault.rotated = new Date().toISOString();
|
|
140
|
-
fs.writeFileSync(ETH_VAULT_PATH(), JSON.stringify(vault, null, 2), "utf-8");
|
|
135
|
+
keyBuf = decryptPrivateKey(projectRoot);
|
|
136
|
+
await encryptAndStore(keyBuf, { rotated: new Date().toISOString(), projectRoot });
|
|
141
137
|
} finally {
|
|
142
138
|
if (keyBuf) keyBuf.fill(0);
|
|
143
139
|
}
|
|
@@ -147,11 +143,11 @@ async function rotateEncryption() {
|
|
|
147
143
|
* Export vault contents for backup. Returns raw Buffers.
|
|
148
144
|
* Caller MUST wipe masterKey after writing to backup.
|
|
149
145
|
*/
|
|
150
|
-
function exportVaultForBackup() {
|
|
151
|
-
if (!isVaultInitialized()) return null;
|
|
146
|
+
function exportVaultForBackup(projectRoot) {
|
|
147
|
+
if (!isVaultInitialized(projectRoot)) return null;
|
|
152
148
|
return {
|
|
153
|
-
masterKey: fs.readFileSync(MASTER_KEY_PATH()),
|
|
154
|
-
vaultFile: fs.readFileSync(ETH_VAULT_PATH(), "utf-8"),
|
|
149
|
+
masterKey: fs.readFileSync(MASTER_KEY_PATH(projectRoot)),
|
|
150
|
+
vaultFile: fs.readFileSync(ETH_VAULT_PATH(projectRoot), "utf-8"),
|
|
155
151
|
};
|
|
156
152
|
}
|
|
157
153
|
|
|
@@ -159,18 +155,18 @@ function exportVaultForBackup() {
|
|
|
159
155
|
* Import vault from backup. Only used during catastrophic recovery
|
|
160
156
|
* when both vault files are missing.
|
|
161
157
|
*/
|
|
162
|
-
function importVaultFromBackup(masterKeyBuf, vaultFileStr) {
|
|
163
|
-
const vaultDir = VAULT_DIR();
|
|
158
|
+
function importVaultFromBackup(masterKeyBuf, vaultFileStr, projectRoot) {
|
|
159
|
+
const vaultDir = VAULT_DIR(projectRoot);
|
|
164
160
|
fs.mkdirSync(vaultDir, { recursive: true });
|
|
165
161
|
|
|
166
|
-
fs.writeFileSync(MASTER_KEY_PATH(), masterKeyBuf);
|
|
167
|
-
try { fs.chmodSync(MASTER_KEY_PATH(), 0o600); } catch {}
|
|
162
|
+
fs.writeFileSync(MASTER_KEY_PATH(projectRoot), masterKeyBuf);
|
|
163
|
+
try { fs.chmodSync(MASTER_KEY_PATH(projectRoot), 0o600); } catch {}
|
|
168
164
|
|
|
169
|
-
fs.writeFileSync(ETH_VAULT_PATH(), vaultFileStr, "utf-8");
|
|
170
|
-
try { fs.chmodSync(ETH_VAULT_PATH(), 0o600); } catch {}
|
|
165
|
+
fs.writeFileSync(ETH_VAULT_PATH(projectRoot), vaultFileStr, "utf-8");
|
|
166
|
+
try { fs.chmodSync(ETH_VAULT_PATH(projectRoot), 0o600); } catch {}
|
|
171
167
|
}
|
|
172
168
|
|
|
173
|
-
function getVaultPath() { return VAULT_DIR(); }
|
|
169
|
+
function getVaultPath(projectRoot) { return VAULT_DIR(projectRoot); }
|
|
174
170
|
|
|
175
171
|
module.exports = {
|
|
176
172
|
initVault,
|