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.
Files changed (79) hide show
  1. package/PLATFORM.md +442 -0
  2. package/README.md +475 -0
  3. package/SERVER_BEST_PRACTICES.md +62 -0
  4. package/TELEMETRY.md +108 -0
  5. package/bin/wolverine.js +95 -0
  6. package/examples/01-basic-typo.js +31 -0
  7. package/examples/02-multi-file/routes/users.js +15 -0
  8. package/examples/02-multi-file/server.js +25 -0
  9. package/examples/03-syntax-error.js +23 -0
  10. package/examples/04-secret-leak.js +14 -0
  11. package/examples/05-expired-key.js +27 -0
  12. package/examples/06-json-config/config.json +13 -0
  13. package/examples/06-json-config/server.js +28 -0
  14. package/examples/07-rate-limit-loop.js +11 -0
  15. package/examples/08-sandbox-escape.js +20 -0
  16. package/examples/buggy-server.js +39 -0
  17. package/examples/demos/01-basic-typo/index.js +20 -0
  18. package/examples/demos/01-basic-typo/routes/api.js +13 -0
  19. package/examples/demos/01-basic-typo/routes/health.js +4 -0
  20. package/examples/demos/02-multi-file/index.js +24 -0
  21. package/examples/demos/02-multi-file/routes/api.js +13 -0
  22. package/examples/demos/02-multi-file/routes/health.js +4 -0
  23. package/examples/demos/03-syntax-error/index.js +18 -0
  24. package/examples/demos/04-secret-leak/index.js +16 -0
  25. package/examples/demos/05-expired-key/index.js +21 -0
  26. package/examples/demos/06-json-config/config.json +9 -0
  27. package/examples/demos/06-json-config/index.js +20 -0
  28. package/examples/demos/07-null-crash/index.js +16 -0
  29. package/examples/run-demo.js +110 -0
  30. package/package.json +67 -0
  31. package/server/config/settings.json +62 -0
  32. package/server/index.js +33 -0
  33. package/server/routes/api.js +12 -0
  34. package/server/routes/health.js +16 -0
  35. package/server/routes/time.js +12 -0
  36. package/src/agent/agent-engine.js +727 -0
  37. package/src/agent/goal-loop.js +140 -0
  38. package/src/agent/research-agent.js +120 -0
  39. package/src/agent/sub-agents.js +176 -0
  40. package/src/backup/backup-manager.js +321 -0
  41. package/src/brain/brain.js +315 -0
  42. package/src/brain/embedder.js +131 -0
  43. package/src/brain/function-map.js +263 -0
  44. package/src/brain/vector-store.js +267 -0
  45. package/src/core/ai-client.js +387 -0
  46. package/src/core/cluster-manager.js +144 -0
  47. package/src/core/config.js +89 -0
  48. package/src/core/error-parser.js +87 -0
  49. package/src/core/health-monitor.js +129 -0
  50. package/src/core/models.js +132 -0
  51. package/src/core/patcher.js +55 -0
  52. package/src/core/runner.js +464 -0
  53. package/src/core/system-info.js +141 -0
  54. package/src/core/verifier.js +146 -0
  55. package/src/core/wolverine.js +290 -0
  56. package/src/dashboard/server.js +1332 -0
  57. package/src/index.js +94 -0
  58. package/src/logger/event-logger.js +237 -0
  59. package/src/logger/pricing.js +96 -0
  60. package/src/logger/repair-history.js +109 -0
  61. package/src/logger/token-tracker.js +277 -0
  62. package/src/mcp/mcp-client.js +224 -0
  63. package/src/mcp/mcp-registry.js +228 -0
  64. package/src/mcp/mcp-security.js +152 -0
  65. package/src/monitor/perf-monitor.js +300 -0
  66. package/src/monitor/process-monitor.js +231 -0
  67. package/src/monitor/route-prober.js +191 -0
  68. package/src/notifications/notifier.js +227 -0
  69. package/src/platform/heartbeat.js +93 -0
  70. package/src/platform/queue.js +53 -0
  71. package/src/platform/register.js +64 -0
  72. package/src/platform/telemetry.js +76 -0
  73. package/src/security/admin-auth.js +150 -0
  74. package/src/security/injection-detector.js +174 -0
  75. package/src/security/rate-limiter.js +152 -0
  76. package/src/security/sandbox.js +128 -0
  77. package/src/security/secret-redactor.js +217 -0
  78. package/src/skills/skill-registry.js +129 -0
  79. package/src/skills/sql.js +375 -0
@@ -0,0 +1,228 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const chalk = require("chalk");
4
+ const { McpStdioClient, McpHttpClient } = require("./mcp-client");
5
+ const { McpSecurity } = require("./mcp-security");
6
+
7
+ /**
8
+ * MCP Registry — manages configured MCP servers and their tools.
9
+ *
10
+ * Configuration: .wolverine/mcp.json
11
+ * {
12
+ * "servers": {
13
+ * "my-server": {
14
+ * "type": "stdio", // "stdio" or "http"
15
+ * "command": "node", // for stdio
16
+ * "args": ["my-mcp-server.js"], // for stdio
17
+ * "url": "http://...", // for http
18
+ * "allowedTools": ["tool1", "tool2"] | "*",
19
+ * "env": {}, // extra env vars (values redacted in logs)
20
+ * "enabled": true // default true
21
+ * }
22
+ * }
23
+ * }
24
+ *
25
+ * Tool naming: mcp__[server]__[tool] (claw-code convention)
26
+ */
27
+
28
+ const CONFIG_PATH = ".wolverine/mcp.json";
29
+
30
+ class McpRegistry {
31
+ constructor(options = {}) {
32
+ this.projectRoot = options.projectRoot || process.cwd();
33
+ this.redactor = options.redactor;
34
+ this.logger = options.logger;
35
+
36
+ this.security = new McpSecurity({ redactor: this.redactor, logger: this.logger });
37
+ this._clients = new Map(); // serverName → client
38
+ this._toolMap = new Map(); // "mcp__server__tool" → { client, serverName, toolDef }
39
+ this._config = null;
40
+ }
41
+
42
+ /**
43
+ * Initialize — load config, connect to servers, discover tools.
44
+ */
45
+ async init() {
46
+ this._config = this._loadConfig();
47
+ if (!this._config || !this._config.servers) {
48
+ console.log(chalk.gray(" 🔌 MCP: no servers configured (.wolverine/mcp.json)"));
49
+ return;
50
+ }
51
+
52
+ const servers = Object.entries(this._config.servers);
53
+ console.log(chalk.gray(` 🔌 MCP: ${servers.length} server(s) configured`));
54
+
55
+ for (const [name, config] of servers) {
56
+ if (config.enabled === false) {
57
+ console.log(chalk.gray(` 🔌 MCP: ${name} (disabled)`));
58
+ continue;
59
+ }
60
+
61
+ try {
62
+ await this._connectServer(name, config);
63
+ } catch (err) {
64
+ console.log(chalk.yellow(` 🔌 MCP: ${name} failed to connect — ${err.message}`));
65
+ if (this.logger) {
66
+ this.logger.warn("mcp.connect_failed", `MCP ${name}: ${err.message}`, { server: name });
67
+ }
68
+ }
69
+ }
70
+
71
+ console.log(chalk.gray(` 🔌 MCP: ${this._toolMap.size} tools available from ${this._clients.size} server(s)`));
72
+ }
73
+
74
+ /**
75
+ * Get all MCP tools as agent-compatible tool definitions.
76
+ * Returns array in OpenAI function-calling format.
77
+ */
78
+ getToolDefinitions() {
79
+ const defs = [];
80
+ for (const [fullName, entry] of this._toolMap) {
81
+ const td = entry.toolDef;
82
+ defs.push({
83
+ type: "function",
84
+ function: {
85
+ name: fullName,
86
+ description: `[MCP:${entry.serverName}] ${td.description || td.name}`,
87
+ parameters: td.inputSchema || { type: "object", properties: {} },
88
+ },
89
+ });
90
+ }
91
+ return defs;
92
+ }
93
+
94
+ /**
95
+ * Get tool names for display.
96
+ */
97
+ getToolList() {
98
+ return Array.from(this._toolMap.entries()).map(([name, entry]) => ({
99
+ name,
100
+ server: entry.serverName,
101
+ description: entry.toolDef.description || "",
102
+ }));
103
+ }
104
+
105
+ /**
106
+ * Call an MCP tool. Security checks applied automatically.
107
+ */
108
+ async callTool(fullName, args) {
109
+ const entry = this._toolMap.get(fullName);
110
+ if (!entry) return { error: `Unknown MCP tool: ${fullName}` };
111
+
112
+ const { client, serverName, toolDef } = entry;
113
+ const toolName = toolDef.name;
114
+
115
+ // Security gate
116
+ const check = this.security.checkTool(serverName, toolName);
117
+ if (!check.allowed) {
118
+ console.log(chalk.red(` 🛡️ MCP blocked: ${check.reason}`));
119
+ if (this.logger) {
120
+ this.logger.warn("mcp.blocked", `MCP blocked: ${check.reason}`, { server: serverName, tool: toolName });
121
+ }
122
+ return { error: `BLOCKED: ${check.reason}` };
123
+ }
124
+
125
+ // Sanitize args (redact secrets before sending to external server)
126
+ const safeArgs = this.security.sanitizeArgs(args);
127
+
128
+ const startTime = Date.now();
129
+ try {
130
+ const result = await client.callTool(toolName, safeArgs);
131
+ const duration = Date.now() - startTime;
132
+
133
+ // Sanitize result (redact secrets, check for injection)
134
+ let content = "";
135
+ if (result.content) {
136
+ for (const item of result.content) {
137
+ if (item.type === "text") content += item.text;
138
+ }
139
+ }
140
+
141
+ const safeContent = this.security.sanitizeResult(serverName, toolName, content);
142
+
143
+ // Audit log
144
+ this.security.auditLog(serverName, toolName, safeArgs, safeContent, duration);
145
+
146
+ console.log(chalk.gray(` 🔌 MCP: ${serverName}/${toolName} (${duration}ms)`));
147
+ return { content: safeContent };
148
+ } catch (err) {
149
+ const duration = Date.now() - startTime;
150
+ this.security.auditLog(serverName, toolName, safeArgs, `ERROR: ${err.message}`, duration);
151
+ return { error: `MCP ${serverName}/${toolName}: ${err.message}` };
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Check if a tool name is an MCP tool.
157
+ */
158
+ isMcpTool(name) {
159
+ return name.startsWith("mcp__") && this._toolMap.has(name);
160
+ }
161
+
162
+ /**
163
+ * Disconnect all servers.
164
+ */
165
+ shutdown() {
166
+ for (const [name, client] of this._clients) {
167
+ try { client.disconnect(); } catch {}
168
+ }
169
+ this._clients.clear();
170
+ this._toolMap.clear();
171
+ }
172
+
173
+ /**
174
+ * Get stats for dashboard.
175
+ */
176
+ getStats() {
177
+ return {
178
+ servers: this._clients.size,
179
+ tools: this._toolMap.size,
180
+ serverList: Array.from(this._clients.keys()),
181
+ };
182
+ }
183
+
184
+ // -- Private --
185
+
186
+ async _connectServer(name, config) {
187
+ let client;
188
+ if (config.type === "http") {
189
+ client = new McpHttpClient(name, config);
190
+ } else {
191
+ client = new McpStdioClient(name, config);
192
+ }
193
+
194
+ await client.connect();
195
+ this._clients.set(name, client);
196
+
197
+ // Set security allowlist
198
+ this.security.setAllowedTools(name, config.allowedTools || []);
199
+
200
+ // Discover tools
201
+ const tools = await client.listTools();
202
+ for (const tool of tools) {
203
+ const fullName = `mcp__${name}__${tool.name}`;
204
+ this._toolMap.set(fullName, { client, serverName: name, toolDef: tool });
205
+ }
206
+
207
+ console.log(chalk.green(` 🔌 MCP: ${name} connected (${tools.length} tools)`));
208
+ if (this.logger) {
209
+ this.logger.info("mcp.connected", `MCP ${name}: ${tools.length} tools`, {
210
+ server: name, tools: tools.map(t => t.name),
211
+ });
212
+ }
213
+ }
214
+
215
+ _loadConfig() {
216
+ const configPath = path.join(this.projectRoot, CONFIG_PATH);
217
+ if (!fs.existsSync(configPath)) return null;
218
+
219
+ try {
220
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
221
+ } catch (err) {
222
+ console.log(chalk.yellow(` 🔌 MCP: config parse error — ${err.message}`));
223
+ return null;
224
+ }
225
+ }
226
+ }
227
+
228
+ module.exports = { McpRegistry };
@@ -0,0 +1,152 @@
1
+ const chalk = require("chalk");
2
+
3
+ /**
4
+ * MCP Security Layer — enforces wolverine's security standards on all MCP interactions.
5
+ *
6
+ * Every MCP tool call passes through this gate:
7
+ * 1. Tool allowlist — only explicitly approved tools can execute
8
+ * 2. Argument sanitization — redacts secrets from args before sending to MCP server
9
+ * 3. Result sanitization — redacts secrets from MCP server responses
10
+ * 4. Injection scanning — checks MCP results for prompt injection
11
+ * 5. Rate limiting — prevents MCP servers from being spammed
12
+ * 6. Audit logging — every MCP call logged to event system
13
+ *
14
+ * MCP servers are UNTRUSTED by default. They run in a separate process
15
+ * and could be malicious. Treat all MCP data as external input.
16
+ */
17
+
18
+ class McpSecurity {
19
+ constructor(options = {}) {
20
+ this.redactor = options.redactor;
21
+ this.logger = options.logger;
22
+
23
+ // Per-server tool allowlists. If a server isn't listed, ALL its tools are blocked.
24
+ // Configured in .wolverine/mcp.json under "allowedTools"
25
+ this._allowedTools = new Map(); // serverName → Set<toolName> | "*"
26
+
27
+ // Rate limiting per server
28
+ this._callCounts = new Map(); // serverName → { count, windowStart }
29
+ this._maxCallsPerMinute = options.maxCallsPerMinute || 30;
30
+
31
+ // Blocked tool name patterns (never allow these regardless of config)
32
+ this._blockedPatterns = [
33
+ /^(rm|delete|drop|truncate|format)/i,
34
+ /exec.*shell/i,
35
+ /admin.*override/i,
36
+ ];
37
+ }
38
+
39
+ /**
40
+ * Set allowed tools for a server.
41
+ * @param {string} serverName
42
+ * @param {string[]|"*"} tools — array of tool names, or "*" for all
43
+ */
44
+ setAllowedTools(serverName, tools) {
45
+ if (tools === "*") {
46
+ this._allowedTools.set(serverName, "*");
47
+ } else {
48
+ this._allowedTools.set(serverName, new Set(tools));
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Check if a tool call is allowed. Returns { allowed, reason }.
54
+ */
55
+ checkTool(serverName, toolName) {
56
+ // Check blocked patterns
57
+ for (const pattern of this._blockedPatterns) {
58
+ if (pattern.test(toolName)) {
59
+ return { allowed: false, reason: `Tool "${toolName}" matches blocked pattern` };
60
+ }
61
+ }
62
+
63
+ // Check allowlist
64
+ const allowed = this._allowedTools.get(serverName);
65
+ if (!allowed) {
66
+ return { allowed: false, reason: `Server "${serverName}" has no allowlisted tools. Configure in .wolverine/mcp.json` };
67
+ }
68
+ if (allowed !== "*" && !allowed.has(toolName)) {
69
+ return { allowed: false, reason: `Tool "${toolName}" not in allowlist for server "${serverName}"` };
70
+ }
71
+
72
+ // Rate limit
73
+ if (!this._checkRate(serverName)) {
74
+ return { allowed: false, reason: `Rate limit exceeded for server "${serverName}" (max ${this._maxCallsPerMinute}/min)` };
75
+ }
76
+
77
+ return { allowed: true };
78
+ }
79
+
80
+ /**
81
+ * Sanitize arguments before sending to MCP server.
82
+ * Redacts secrets so they never leave wolverine.
83
+ */
84
+ sanitizeArgs(args) {
85
+ if (!this.redactor || !args) return args;
86
+ return this.redactor.redactObject(args);
87
+ }
88
+
89
+ /**
90
+ * Sanitize results coming back from MCP server.
91
+ * Checks for injection and redacts any secrets that leaked.
92
+ */
93
+ sanitizeResult(serverName, toolName, result) {
94
+ if (!result) return result;
95
+
96
+ // Convert to string for scanning
97
+ const resultStr = typeof result === "string" ? result : JSON.stringify(result);
98
+
99
+ // Redact any secrets in the result
100
+ let safe = this.redactor ? this.redactor.redact(resultStr) : resultStr;
101
+
102
+ // Check for obvious injection attempts in MCP results
103
+ const injectionPatterns = [
104
+ /ignore\s+previous\s+instructions/i,
105
+ /you\s+are\s+now/i,
106
+ /system\s*:\s*/i,
107
+ /new\s+instructions?\s*:/i,
108
+ ];
109
+
110
+ for (const pattern of injectionPatterns) {
111
+ if (pattern.test(safe)) {
112
+ console.log(chalk.red(` 🚨 MCP ${serverName}/${toolName}: injection attempt detected in result`));
113
+ if (this.logger) {
114
+ this.logger.critical("security.mcp_injection", `Injection in MCP result from ${serverName}/${toolName}`, {
115
+ server: serverName, tool: toolName,
116
+ });
117
+ }
118
+ return "[BLOCKED: MCP server returned suspicious content]";
119
+ }
120
+ }
121
+
122
+ return safe;
123
+ }
124
+
125
+ /**
126
+ * Log an MCP tool call for audit trail.
127
+ */
128
+ auditLog(serverName, toolName, args, result, duration) {
129
+ if (!this.logger) return;
130
+ this.logger.info("mcp.tool_call", `MCP ${serverName}/${toolName}`, {
131
+ server: serverName,
132
+ tool: toolName,
133
+ duration,
134
+ resultSize: typeof result === "string" ? result.length : JSON.stringify(result || "").length,
135
+ });
136
+ }
137
+
138
+ // -- Private --
139
+
140
+ _checkRate(serverName) {
141
+ const now = Date.now();
142
+ let entry = this._callCounts.get(serverName);
143
+ if (!entry || now - entry.windowStart > 60000) {
144
+ entry = { count: 0, windowStart: now };
145
+ this._callCounts.set(serverName, entry);
146
+ }
147
+ entry.count++;
148
+ return entry.count <= this._maxCallsPerMinute;
149
+ }
150
+ }
151
+
152
+ module.exports = { McpSecurity };
@@ -0,0 +1,300 @@
1
+ const http = require("http");
2
+ const chalk = require("chalk");
3
+ const { getModel } = require("../core/models");
4
+ const { aiCall } = require("../core/ai-client");
5
+
6
+ /**
7
+ * Performance Monitor — detects non-error issues that degrade server quality.
8
+ *
9
+ * Monitors:
10
+ * - Endpoint response times (detects slowdowns)
11
+ * - Request rate per endpoint (detects spam/DDoS patterns)
12
+ * - Error rate per endpoint (detects failing routes)
13
+ * - Memory/CPU via process metrics (if accessible)
14
+ *
15
+ * When it detects a pain point, it uses the AI (REASONING_MODEL) to analyze
16
+ * the relevant code and suggest or apply optimizations.
17
+ *
18
+ * Architecture: Injects a lightweight proxy between wolverine and the server
19
+ * that intercepts HTTP requests to gather metrics without modifying user code.
20
+ */
21
+
22
+ class PerfMonitor {
23
+ constructor(options = {}) {
24
+ this.logger = options.logger;
25
+ this.sandbox = options.sandbox;
26
+ this.cwd = options.cwd || process.cwd();
27
+ this.port = options.port || parseInt(process.env.PORT, 10) || 3000;
28
+
29
+ // Config
30
+ this.sampleIntervalMs = options.sampleIntervalMs || 30000; // check every 30s
31
+ this.slowThresholdMs = options.slowThresholdMs || 2000; // >2s = slow
32
+ this.spikeMultiplier = options.spikeMultiplier || 5; // 5x avg = spike
33
+ this.spamThreshold = options.spamThreshold || 100; // >100 req/min to one endpoint = spam
34
+
35
+ // Metrics store
36
+ this._endpoints = {}; // path -> { totalRequests, totalTime, errors, timestamps[] }
37
+ this._timer = null;
38
+ this._running = false;
39
+ this._analysisInProgress = false;
40
+ }
41
+
42
+ /**
43
+ * Start monitoring by polling the server's response times.
44
+ */
45
+ start() {
46
+ this._running = true;
47
+
48
+ // Periodic analysis of accumulated metrics
49
+ this._timer = setInterval(() => this._analyze(), this.sampleIntervalMs);
50
+
51
+ if (this.logger) {
52
+ this.logger.info("perf.start", "Performance monitor started", {
53
+ sampleInterval: this.sampleIntervalMs,
54
+ slowThreshold: this.slowThresholdMs,
55
+ });
56
+ }
57
+ }
58
+
59
+ stop() {
60
+ this._running = false;
61
+ if (this._timer) {
62
+ clearInterval(this._timer);
63
+ this._timer = null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Record a request observation. Call this from a proxy or middleware.
69
+ */
70
+ recordRequest(endpoint, responseTimeMs, statusCode) {
71
+ if (!this._endpoints[endpoint]) {
72
+ this._endpoints[endpoint] = {
73
+ totalRequests: 0,
74
+ totalTime: 0,
75
+ errors: 0,
76
+ maxTime: 0,
77
+ timestamps: [],
78
+ recentTimes: [],
79
+ };
80
+ }
81
+
82
+ const ep = this._endpoints[endpoint];
83
+ ep.totalRequests++;
84
+ ep.totalTime += responseTimeMs;
85
+ ep.maxTime = Math.max(ep.maxTime, responseTimeMs);
86
+ ep.timestamps.push(Date.now());
87
+ ep.recentTimes.push(responseTimeMs);
88
+
89
+ // Keep only last 5 minutes of timestamps
90
+ const fiveMinAgo = Date.now() - 300000;
91
+ ep.timestamps = ep.timestamps.filter(t => t > fiveMinAgo);
92
+ ep.recentTimes = ep.recentTimes.slice(-200);
93
+
94
+ if (statusCode >= 400) {
95
+ ep.errors++;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Get current metrics snapshot for all endpoints.
101
+ */
102
+ getMetrics() {
103
+ const metrics = {};
104
+ for (const [endpoint, ep] of Object.entries(this._endpoints)) {
105
+ const avgTime = ep.totalRequests > 0 ? Math.round(ep.totalTime / ep.totalRequests) : 0;
106
+ const fiveMinAgo = Date.now() - 300000;
107
+ const recentCount = ep.timestamps.filter(t => t > fiveMinAgo).length;
108
+ const requestsPerMin = Math.round((recentCount / 5) * 10) / 10;
109
+
110
+ metrics[endpoint] = {
111
+ totalRequests: ep.totalRequests,
112
+ avgResponseMs: avgTime,
113
+ maxResponseMs: ep.maxTime,
114
+ requestsPerMin,
115
+ errorRate: ep.totalRequests > 0 ? Math.round((ep.errors / ep.totalRequests) * 100) : 0,
116
+ errors: ep.errors,
117
+ };
118
+ }
119
+ return metrics;
120
+ }
121
+
122
+ /**
123
+ * Internal — analyze metrics and flag issues.
124
+ */
125
+ async _analyze() {
126
+ if (!this._running || this._analysisInProgress) return;
127
+ if (Object.keys(this._endpoints).length === 0) return;
128
+
129
+ const issues = [];
130
+
131
+ for (const [endpoint, ep] of Object.entries(this._endpoints)) {
132
+ const avgTime = ep.totalRequests > 0 ? ep.totalTime / ep.totalRequests : 0;
133
+ const fiveMinAgo = Date.now() - 300000;
134
+ const recentCount = ep.timestamps.filter(t => t > fiveMinAgo).length;
135
+ const requestsPerMin = recentCount / 5;
136
+
137
+ // Slow endpoint detection
138
+ if (avgTime > this.slowThresholdMs) {
139
+ issues.push({
140
+ type: "slow_endpoint",
141
+ endpoint,
142
+ avgTime: Math.round(avgTime),
143
+ maxTime: ep.maxTime,
144
+ message: `${endpoint} averaging ${Math.round(avgTime)}ms (threshold: ${this.slowThresholdMs}ms)`,
145
+ });
146
+ }
147
+
148
+ // Response time spike detection
149
+ if (ep.recentTimes.length >= 10) {
150
+ const recent = ep.recentTimes.slice(-10);
151
+ const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length;
152
+ if (recentAvg > avgTime * this.spikeMultiplier && avgTime > 100) {
153
+ issues.push({
154
+ type: "spike",
155
+ endpoint,
156
+ recentAvg: Math.round(recentAvg),
157
+ overallAvg: Math.round(avgTime),
158
+ message: `${endpoint} response time spiked: ${Math.round(recentAvg)}ms (normal: ${Math.round(avgTime)}ms)`,
159
+ });
160
+ }
161
+ }
162
+
163
+ // Spam/DDoS detection
164
+ if (requestsPerMin > this.spamThreshold) {
165
+ issues.push({
166
+ type: "spam",
167
+ endpoint,
168
+ requestsPerMin: Math.round(requestsPerMin),
169
+ message: `${endpoint} receiving ${Math.round(requestsPerMin)} req/min (threshold: ${this.spamThreshold})`,
170
+ });
171
+ }
172
+
173
+ // High error rate detection
174
+ const errorRate = ep.totalRequests > 10 ? ep.errors / ep.totalRequests : 0;
175
+ if (errorRate > 0.2) {
176
+ issues.push({
177
+ type: "error_rate",
178
+ endpoint,
179
+ errorRate: Math.round(errorRate * 100),
180
+ message: `${endpoint} has ${Math.round(errorRate * 100)}% error rate (${ep.errors}/${ep.totalRequests})`,
181
+ });
182
+ }
183
+ }
184
+
185
+ if (issues.length === 0) return;
186
+
187
+ // Log all issues
188
+ for (const issue of issues) {
189
+ const eventType = {
190
+ slow_endpoint: "perf.slow_endpoint",
191
+ spike: "perf.spike_detected",
192
+ spam: "perf.attack_detected",
193
+ error_rate: "perf.slow_endpoint",
194
+ }[issue.type];
195
+
196
+ const severity = issue.type === "spam" ? "warn" : "info";
197
+
198
+ if (this.logger) {
199
+ this.logger[severity](eventType, issue.message, issue);
200
+ }
201
+ console.log(chalk.yellow(` ⚡ Perf: ${issue.message}`));
202
+ }
203
+
204
+ // If there are serious issues, request AI optimization analysis
205
+ const serious = issues.filter(i => i.type === "slow_endpoint" || i.type === "spam");
206
+ if (serious.length > 0 && !this._analysisInProgress) {
207
+ await this._requestOptimization(serious);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Ask AI to analyze performance issues and suggest optimizations.
213
+ * Uses REASONING_MODEL since this is deep analysis.
214
+ */
215
+ async _requestOptimization(issues) {
216
+ this._analysisInProgress = true;
217
+
218
+ try {
219
+ const model = getModel("chat");
220
+ const issuesSummary = issues.map(i => `- ${i.message}`).join("\n");
221
+
222
+ const result = await aiCall({
223
+ model,
224
+ systemPrompt: "You are a Node.js performance optimization expert.",
225
+ userPrompt: `The following performance issues were detected on a live server:
226
+
227
+ ${issuesSummary}
228
+
229
+ Current endpoint metrics:
230
+ ${JSON.stringify(this.getMetrics(), null, 2)}
231
+
232
+ Provide a brief analysis and actionable suggestions. Focus on:
233
+ 1. Root cause identification
234
+ 2. Quick wins (caching, rate limiting, query optimization)
235
+ 3. Whether this looks like an attack (spam patterns)
236
+
237
+ Keep your response under 300 words. Be specific and actionable.`,
238
+ maxTokens: 512,
239
+ category: "security",
240
+ });
241
+
242
+ const analysis = result.content;
243
+ console.log(chalk.cyan(`\n 📊 Performance Analysis:\n${analysis.split("\n").map(l => " " + l).join("\n")}\n`));
244
+
245
+ if (this.logger) {
246
+ this.logger.info("perf.optimization", "AI performance analysis completed", {
247
+ issues: issues.length,
248
+ analysis: analysis.slice(0, 500),
249
+ });
250
+ }
251
+ } catch (err) {
252
+ console.log(chalk.yellow(` Performance analysis failed: ${err.message}`));
253
+ } finally {
254
+ this._analysisInProgress = false;
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Create a lightweight HTTP proxy that wraps the server to capture metrics.
261
+ * Sits between wolverine and the actual server process.
262
+ */
263
+ function createMetricsProxy(targetPort, proxyPort, perfMonitor) {
264
+ const proxy = http.createServer((req, res) => {
265
+ const startTime = Date.now();
266
+
267
+ const options = {
268
+ hostname: "127.0.0.1",
269
+ port: targetPort,
270
+ path: req.url,
271
+ method: req.method,
272
+ headers: req.headers,
273
+ };
274
+
275
+ const proxyReq = http.request(options, (proxyRes) => {
276
+ const responseTime = Date.now() - startTime;
277
+ perfMonitor.recordRequest(req.url, responseTime, proxyRes.statusCode);
278
+
279
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
280
+ proxyRes.pipe(res);
281
+ });
282
+
283
+ proxyReq.on("error", (err) => {
284
+ const responseTime = Date.now() - startTime;
285
+ perfMonitor.recordRequest(req.url, responseTime, 502);
286
+ res.writeHead(502, { "Content-Type": "application/json" });
287
+ res.end(JSON.stringify({ error: "Bad Gateway", details: err.message }));
288
+ });
289
+
290
+ req.pipe(proxyReq);
291
+ });
292
+
293
+ proxy.listen(proxyPort, () => {
294
+ console.log(chalk.gray(` 📊 Metrics proxy on :${proxyPort} → :${targetPort}`));
295
+ });
296
+
297
+ return proxy;
298
+ }
299
+
300
+ module.exports = { PerfMonitor, createMetricsProxy };