wolverine-ai 4.5.4 β†’ 4.6.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/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,25 @@ 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\n`));
74
+ process.exit(0);
75
+ }
76
+
57
77
  // --update: safe framework update
58
78
  if (args.includes("--update")) {
59
79
  const { safeUpdate } = require("../src/skills/update");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "4.5.4",
3
+ "version": "4.6.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": {
@@ -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 (retries every 60s until platform responds), saves key to .wolverine/platform-key. Heartbeat payload matches PLATFORM.md spec: instanceId, server (name/port/uptime/status/pid), process (memoryMB/cpuPercent), routes, repairs, usage (tokens/cost/calls/byCategory), brain, backups. Offline-resilient: queues up to 1440 heartbeats locally, drains on reconnect. No chalk dependency, cached version/key in memory, minimal IO. Opt out: WOLVERINE_TELEMETRY=false. Override URL: WOLVERINE_PLATFORM_URL.",
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" },
@@ -211,6 +211,16 @@ 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, load } = 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
+ console.log(chalk.gray(` πŸ—ΊοΈ Server context: ${routes} routes, ${ctx.structure.length} files, ${ctx.envVars.length} env vars`));
221
+ }
222
+ } catch {}
223
+
214
224
  // Log redactor stats
215
225
  const redactorStats = this.redactor.getStats();
216
226
  console.log(chalk.gray(` πŸ” Secret redactor: ${redactorStats.trackedSecrets} secrets tracked from ${redactorStats.envFiles} env file(s)`));
@@ -0,0 +1,229 @@
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
+ // Save
172
+ const outPath = path.join(cwd, CONTEXT_PATH);
173
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
174
+ fs.writeFileSync(outPath, JSON.stringify(context, null, 2), "utf-8");
175
+
176
+ return context;
177
+ }
178
+
179
+ /**
180
+ * Load cached context (fast β€” no rescan).
181
+ */
182
+ function load(cwd) {
183
+ const ctxPath = path.join(cwd, CONTEXT_PATH);
184
+ if (!fs.existsSync(ctxPath)) return null;
185
+ try {
186
+ return JSON.parse(fs.readFileSync(ctxPath, "utf-8"));
187
+ } catch { return null; }
188
+ }
189
+
190
+ /**
191
+ * Get a compact text summary for injecting into AI prompts.
192
+ */
193
+ function getSummary(cwd) {
194
+ const ctx = load(cwd);
195
+ if (!ctx) return "";
196
+
197
+ const lines = [];
198
+ if (ctx.routes.length > 0) {
199
+ const allEndpoints = ctx.routes.flatMap(r => r.endpoints.map(e => `${e.method} ${e.path}`));
200
+ lines.push(`Routes (${allEndpoints.length}): ${allEndpoints.slice(0, 20).join(", ")}${allEndpoints.length > 20 ? ` +${allEndpoints.length - 20} more` : ""}`);
201
+ }
202
+ if (ctx.middleware.length > 0) lines.push(`Middleware: ${ctx.middleware.join(", ")}`);
203
+ if (ctx.database.type) lines.push(`Database: ${ctx.database.type}${ctx.database.tables.length > 0 ? ` (tables: ${ctx.database.tables.join(", ")})` : ""}${ctx.database.hasRedis ? " + Redis" : ""}`);
204
+ 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` : ""}`);
205
+ if (ctx.structure.length > 0) lines.push(`Server files: ${ctx.structure.length}`);
206
+ if (ctx.version) lines.push(`Version: ${ctx.version}`);
207
+
208
+ return lines.length > 0 ? "SERVER CONTEXT:\n" + lines.join("\n") : "";
209
+ }
210
+
211
+ function _listFiles(dir, ext) {
212
+ const results = [];
213
+ let count = 0;
214
+ const walk = (d) => {
215
+ if (count > MAX_FILE_SCAN) return;
216
+ try {
217
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
218
+ if (SKIP_DIRS.has(entry.name)) continue;
219
+ const full = path.join(d, entry.name);
220
+ if (entry.isDirectory()) walk(full);
221
+ else if (!ext || entry.name.endsWith(ext)) { results.push(full); count++; }
222
+ }
223
+ } catch {}
224
+ };
225
+ walk(dir);
226
+ return results;
227
+ }
228
+
229
+ module.exports = { scan, load, getSummary, CONTEXT_PATH };
@@ -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);