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,1332 @@
1
+ const http = require("http");
2
+ const chalk = require("chalk");
3
+ const { AdminAuth } = require("../security/admin-auth");
4
+ const { AgentEngine } = require("../agent/agent-engine");
5
+
6
+ /**
7
+ * Wolverine Dashboard Server — real-time web UI + agent command interface.
8
+ *
9
+ * Command routing (claw-code pattern):
10
+ * ALL commands → UTILITY_MODEL classifier (1 word: CHAT or AGENT) → handler
11
+ *
12
+ * No regex. No heuristics. The AI decides the route, every time.
13
+ */
14
+
15
+ class DashboardServer {
16
+ constructor(options = {}) {
17
+ this.port = options.port || (parseInt(process.env.WOLVERINE_DASHBOARD_PORT, 10) || (parseInt(process.env.PORT, 10) || 3000) + 1);
18
+ this.logger = options.logger;
19
+ this.backupManager = options.backupManager;
20
+ this.perfMonitor = options.perfMonitor;
21
+ this.healthMonitor = options.healthMonitor;
22
+ this.brain = options.brain;
23
+ this.sandbox = options.sandbox;
24
+ this.redactor = options.redactor;
25
+ this.scriptPath = options.scriptPath;
26
+ this.runner = options.runner;
27
+ this.tokenTracker = options.tokenTracker;
28
+ this.skills = options.skills;
29
+ this.repairHistory = options.repairHistory;
30
+ this.processMonitor = options.processMonitor;
31
+ this.routeProber = options.routeProber;
32
+
33
+ this.auth = new AdminAuth();
34
+ this._sseClients = new Set();
35
+ this._server = null;
36
+ this._commandRunning = false;
37
+
38
+ // Conversation memory (claw-code: TranscriptStore pattern)
39
+ // Maintains rolling context window across chat turns
40
+ this._chatHistory = []; // { role, content } messages
41
+ this._maxHistoryTurns = 20; // keep last 20 exchanges
42
+ this._compactAfter = 30; // compact older messages after this many
43
+ }
44
+
45
+ start() {
46
+ if (this.logger) {
47
+ this.logger.on("event", (event) => this._broadcast(event));
48
+ }
49
+
50
+ this._server = http.createServer((req, res) => {
51
+ res.setHeader("Access-Control-Allow-Origin", "*");
52
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
53
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Key");
54
+
55
+ if (req.method === "OPTIONS") { res.writeHead(204); res.end(); return; }
56
+
57
+ if (req.url === "/api/events/stream") return this._handleSSE(req, res);
58
+ if (req.url === "/api/events") return this._handleEvents(req, res);
59
+ if (req.url === "/api/stats") return this._handleStats(req, res);
60
+ if (req.url === "/api/metrics") return this._handleMetrics(req, res);
61
+ if (req.url === "/api/backups") return this._handleBackups(req, res);
62
+ if (req.url === "/api/brain") return this._handleBrain(req, res);
63
+ if (req.url === "/api/usage") return this._handleUsage(req, res);
64
+ if (req.url === "/api/repairs") return this._handleRepairs(req, res);
65
+ if (req.url === "/api/system") return this._handleSystem(req, res);
66
+ if (req.url === "/api/process") return this._handleProcess(req, res);
67
+ if (req.url === "/api/routes") return this._handleRoutes(req, res);
68
+ if (req.url === "/api/usage/history") return this._handleUsageHistory(req, res);
69
+ if (req.url === "/api/auth/verify" && req.method === "POST") return this._handleAuthVerify(req, res);
70
+ if (req.url === "/api/command" && req.method === "POST") return this._handleCommand(req, res);
71
+ if (req.url === "/api/chat/clear" && req.method === "POST") {
72
+ this._chatHistory = [];
73
+ res.writeHead(200, { "Content-Type": "application/json" });
74
+ res.end(JSON.stringify({ cleared: true }));
75
+ return;
76
+ }
77
+ if (req.url === "/" || req.url === "/dashboard") return this._handleDashboard(req, res);
78
+
79
+ res.writeHead(404, { "Content-Type": "application/json" });
80
+ res.end(JSON.stringify({ error: "Not found" }));
81
+ });
82
+
83
+ this._server.on("error", (err) => {
84
+ if (err.code === "EADDRINUSE") {
85
+ console.log(chalk.yellow(` ⚠️ Dashboard port ${this.port} in use — trying ${this.port + 1}`));
86
+ this.port++;
87
+ this._server.listen(this.port, () => {
88
+ console.log(chalk.magenta(` 🖥️ Dashboard: http://localhost:${this.port}`));
89
+ });
90
+ } else {
91
+ console.log(chalk.yellow(` ⚠️ Dashboard failed to start: ${err.message}`));
92
+ }
93
+ });
94
+
95
+ this._server.listen(this.port, () => {
96
+ console.log(chalk.magenta(` 🖥️ Dashboard: http://localhost:${this.port}`));
97
+ });
98
+ }
99
+
100
+ stop() {
101
+ for (const client of this._sseClients) client.end();
102
+ this._sseClients.clear();
103
+ if (this._server) this._server.close();
104
+ }
105
+
106
+ _broadcast(event) {
107
+ const data = `data: ${JSON.stringify(event)}\n\n`;
108
+ for (const client of this._sseClients) client.write(data);
109
+ }
110
+
111
+ // ── Auth ──
112
+
113
+ _handleAuthVerify(req, res) {
114
+ this._readBody(req, (body) => {
115
+ const fakeReq = { ...req, headers: { ...req.headers, "x-admin-key": body.key } };
116
+ Object.defineProperty(fakeReq, "socket", { get: () => req.socket });
117
+ Object.defineProperty(fakeReq, "connection", { get: () => req.connection });
118
+ const result = this.auth.validate(fakeReq);
119
+
120
+ if (result.authorized) {
121
+ res.writeHead(200, {
122
+ "Content-Type": "application/json",
123
+ "Set-Cookie": `wolverine_admin_key=${encodeURIComponent(body.key)}; Path=/; SameSite=Strict; Max-Age=31536000`,
124
+ });
125
+ res.end(JSON.stringify({ authorized: true }));
126
+ } else {
127
+ res.writeHead(403, { "Content-Type": "application/json" });
128
+ res.end(JSON.stringify({ authorized: false, reason: result.reason }));
129
+ }
130
+ });
131
+ }
132
+
133
+ // ── Command Interface ──
134
+
135
+ _handleCommand(req, res) {
136
+ const authResult = this.auth.validate(req);
137
+ if (!authResult.authorized) {
138
+ res.writeHead(403, { "Content-Type": "application/json" });
139
+ res.end(JSON.stringify({ error: "Forbidden", reason: authResult.reason }));
140
+ return;
141
+ }
142
+
143
+ if (this._commandRunning) {
144
+ res.writeHead(429, { "Content-Type": "application/json" });
145
+ res.end(JSON.stringify({ error: "Agent is already running a command. Wait for it to finish." }));
146
+ return;
147
+ }
148
+
149
+ this._readBody(req, async (body) => {
150
+ const command = body.command;
151
+ if (!command || typeof command !== "string" || command.trim().length === 0) {
152
+ res.writeHead(400, { "Content-Type": "application/json" });
153
+ res.end(JSON.stringify({ error: "No command provided" }));
154
+ return;
155
+ }
156
+
157
+ const safeCommand = this.redactor ? this.redactor.redact(command) : command;
158
+
159
+ if (this._isSecretExtractionRequest(command)) {
160
+ const refusal = this._buildSecretRefusal();
161
+ res.writeHead(200, { "Content-Type": "application/json" });
162
+ res.end(JSON.stringify({
163
+ success: true,
164
+ summary: refusal,
165
+ filesModified: [],
166
+ turns: 1,
167
+ tokens: 0,
168
+ mode: "chat",
169
+ }));
170
+ return;
171
+ }
172
+
173
+ console.log(chalk.magenta(`\n 🎯 Admin command: ${safeCommand.slice(0, 100)}`));
174
+ if (this.logger) {
175
+ this.logger.info("agent.command", `Admin command: ${safeCommand.slice(0, 200)}`, { command: safeCommand });
176
+ }
177
+
178
+ this._commandRunning = true;
179
+
180
+ const { getModel } = require("../core/models");
181
+ // Helper to broadcast progress via SSE
182
+ const progress = (type, msg) => {
183
+ if (this.logger) this.logger.debug(type, msg, {});
184
+ };
185
+
186
+ try {
187
+ // STEP 1: AI classifier
188
+ progress("classify", `Classifying with ${getModel("classifier")}...`);
189
+ const plan = await this._classify(safeCommand);
190
+ progress("classify", `Route: ${plan.route} ${plan.tier}`);
191
+
192
+ if (plan.route === "SIMPLE") {
193
+ progress("chat.start", `Simple chat (${getModel("chat")}) — brain lookup...`);
194
+ const result = await this._simpleChat(safeCommand);
195
+ res.writeHead(200, { "Content-Type": "application/json" });
196
+ res.end(JSON.stringify(result));
197
+ return;
198
+ }
199
+
200
+ if (plan.route === "TOOLS") {
201
+ progress("chat.start", `Tool chat (${getModel("tool")}) — using tools...`);
202
+ const result = await this._chat(safeCommand);
203
+ res.writeHead(200, { "Content-Type": "application/json" });
204
+ res.end(JSON.stringify(result));
205
+ return;
206
+ }
207
+
208
+ // STEP 2: Route by tier
209
+ // SMALL → single AI call, no agent loop (cheapest, fastest)
210
+ // MEDIUM/LARGE → agent with tool loop
211
+ let result;
212
+
213
+ // Conversation context for all tiers
214
+ const chatContext = this._chatHistory.slice(-6).map(m => `${m.role}: ${m.content}`).join("\n");
215
+
216
+ if (plan.tier === "SMALL") {
217
+ progress("develop", `Smart edit (${getModel("coding")}) — 1 AI call...`);
218
+ result = await this._directEdit(safeCommand, chatContext);
219
+ } else if (plan.tier === "MEDIUM") {
220
+ progress("agent.turn", `Agent mode (${getModel("reasoning")}) — MEDIUM tier`);
221
+
222
+ let brainContext = "";
223
+ if (this.brain && this.brain._initialized) {
224
+ try { brainContext = await this.brain.getContext(command); } catch {}
225
+ }
226
+
227
+ const agent = new AgentEngine({
228
+ sandbox: this.sandbox,
229
+ logger: this.logger,
230
+ cwd: process.cwd(),
231
+ maxTurns: 8,
232
+ maxTokens: 25000,
233
+ });
234
+
235
+ const agentResult = await agent.run({
236
+ errorMessage: safeCommand + (chatContext ? "\n\nConversation:\n" + chatContext : ""),
237
+ stackTrace: "",
238
+ primaryFile: "N/A",
239
+ sourceCode: "",
240
+ brainContext,
241
+ });
242
+
243
+ result = {
244
+ success: agentResult.success,
245
+ summary: agentResult.summary,
246
+ filesModified: agentResult.filesModified,
247
+ turns: agentResult.turnCount,
248
+ tokens: agentResult.totalTokens,
249
+ mode: "agent",
250
+ tier: "MEDIUM",
251
+ };
252
+ } else {
253
+ // LARGE tier — sub-agents: explore → plan → fix
254
+ const { exploreAndFix } = require("../agent/sub-agents");
255
+ progress("agent.spawn", `Sub-agents (explore → plan → fix) — LARGE tier`);
256
+
257
+ let brainContext = "";
258
+ if (this.brain && this.brain._initialized) {
259
+ try { brainContext = await this.brain.getContext(command); } catch {}
260
+ }
261
+
262
+ const subResult = await exploreAndFix(
263
+ safeCommand + (chatContext ? "\n\nConversation:\n" + chatContext : ""),
264
+ { sandbox: this.sandbox, logger: this.logger, cwd: process.cwd(), mcp: this.runner?.mcp, brainContext }
265
+ );
266
+
267
+ result = {
268
+ success: subResult.success,
269
+ summary: subResult.summary,
270
+ filesModified: subResult.filesModified || [],
271
+ turns: (subResult.exploration?.turnCount || 0) + (subResult.plan?.turnCount || 0) + (subResult.fix?.turnCount || 0),
272
+ tokens: subResult.totalTokens,
273
+ mode: "sub-agents",
274
+ tier: "LARGE",
275
+ };
276
+ }
277
+
278
+ // Add to chat history
279
+ this._chatHistory.push({ role: "user", content: safeCommand });
280
+ this._chatHistory.push({ role: "assistant", content: `[${result.mode}: ${result.summary}. Files: ${(result.filesModified || []).join(", ") || "none"}]` });
281
+
282
+ if (this.brain && this.brain._initialized && result.success) {
283
+ this.brain.remember("learnings",
284
+ `Admin command: ${safeCommand}. Result: ${result.summary}. Files: ${result.filesModified.join(", ")}`,
285
+ { type: "admin-command" }
286
+ ).catch(() => {});
287
+ }
288
+
289
+ res.writeHead(200, { "Content-Type": "application/json" });
290
+ res.end(JSON.stringify(result));
291
+ } catch (err) {
292
+ res.writeHead(500, { "Content-Type": "application/json" });
293
+ res.end(JSON.stringify({ error: err.message }));
294
+ } finally {
295
+ this._commandRunning = false;
296
+ }
297
+ });
298
+ }
299
+
300
+ _isSecretExtractionRequest(command) {
301
+ if (!command || typeof command !== "string") return false;
302
+
303
+ const normalized = command.toLowerCase().trim();
304
+ const secretTerms = [
305
+ "api key", "apikey", "openai key", "admin key", "token", "secret", "password", ".env", "environment variable", "env var",
306
+ ];
307
+ const extractionTerms = [
308
+ "what is", "show", "tell me", "give me", "reveal", "print", "display", "dump", "return", "read", "find", "where is",
309
+ ];
310
+
311
+ const asksForSecret = secretTerms.some((term) => normalized.includes(term));
312
+ const asksToExtract = extractionTerms.some((term) => normalized.includes(term));
313
+
314
+ return asksForSecret && asksToExtract;
315
+ }
316
+
317
+ _buildSecretRefusal() {
318
+ return "I can’t reveal API keys or other secrets. If you need to verify configuration, check the relevant environment variables such as process.env.OPENAI_API_KEY or process.env.WOLVERINE_ADMIN_KEY on the server without printing their values.";
319
+ }
320
+
321
+ // ── AI Classifier + Tier (UTILITY_MODEL, one call) ──
322
+
323
+ async _classify(command) {
324
+ const { aiCall } = require("../core/ai-client");
325
+ const { getModel } = require("../core/models");
326
+
327
+ try {
328
+ const result = await aiCall({
329
+ model: getModel("classifier"),
330
+ systemPrompt: "Route a command. Respond with two words: ROUTE SIZE.\nROUTE: SIMPLE (general knowledge/explanation, no live data needed), TOOLS (needs live server data, file contents, or endpoint calls), AGENT (create/modify/fix code).\nSIZE: SMALL, MEDIUM, LARGE.\nExamples: 'what is wolverine' → SIMPLE SMALL. 'what time is it' → TOOLS SMALL. 'show me index.js' → TOOLS SMALL. 'add endpoint' → AGENT SMALL. 'build auth' → AGENT LARGE.",
331
+ userPrompt: command,
332
+ maxTokens: 10,
333
+ category: "classify",
334
+ });
335
+
336
+ const raw = (result.content || "").trim().toUpperCase();
337
+ // Parse flexibly — three routes: SIMPLE (no tools), TOOLS (with tools), AGENT (code changes)
338
+ let route = "SIMPLE";
339
+ if (raw.includes("AGENT")) route = "AGENT";
340
+ else if (raw.includes("TOOL")) route = "TOOLS";
341
+ else if (raw.includes("SIMPLE")) route = "SIMPLE";
342
+ const tier = raw.includes("LARGE") ? "LARGE" : raw.includes("MEDIUM") ? "MEDIUM" : "SMALL";
343
+
344
+ console.log(chalk.gray(` 🏷️ Route: ${route} ${tier} (raw: "${raw}")`));
345
+ return { route, tier };
346
+ } catch (err) {
347
+ console.log(chalk.yellow(` 🏷️ Classifier failed: ${err.message} — defaulting to CHAT`));
348
+ return { route: "CHAT", tier: "SMALL" };
349
+ }
350
+ }
351
+
352
+ // ── Smart Edit (SMALL tier) — ONE AI call → structured file operations ──
353
+ // Returns JSON with file operations: create, edit, mount.
354
+ // Follows server best practices: routes in server/routes/, index.js is just wiring.
355
+
356
+ async _directEdit(command, chatContext) {
357
+ const { aiCall } = require("../core/ai-client");
358
+ const { getModel } = require("../core/models");
359
+ const fs = require("fs");
360
+ const path = require("path");
361
+ const http = require("http");
362
+
363
+ const cwd = process.cwd();
364
+ const serverDir = path.join(cwd, "server");
365
+ console.log(chalk.gray(` 📐 Tier: SMALL (smart edit, 1 AI call)`));
366
+
367
+ // Read the current server structure
368
+ const indexPath = this.scriptPath ? path.relative(cwd, this.scriptPath).replace(/\\/g, "/") : "server/index.js";
369
+ let indexContent = "";
370
+ try { indexContent = this.sandbox.readFile(path.resolve(cwd, indexPath)); } catch {}
371
+
372
+ // Read existing route files for context
373
+ const routesDir = path.join(serverDir, "routes");
374
+ let existingRoutes = "";
375
+ if (fs.existsSync(routesDir)) {
376
+ const routeFiles = fs.readdirSync(routesDir).filter(f => f.endsWith(".js"));
377
+ for (const rf of routeFiles.slice(0, 5)) {
378
+ const content = fs.readFileSync(path.join(routesDir, rf), "utf-8");
379
+ existingRoutes += `\n--- server/routes/${rf} ---\n${content}\n`;
380
+ }
381
+ }
382
+
383
+ // Inject relevant skill context (claw-code: pre-enrich with matched skills)
384
+ let skillContext = "";
385
+ if (this.skills) {
386
+ skillContext = this.skills.buildContext(command);
387
+ if (skillContext) console.log(chalk.gray(` 🔧 Skills matched for this task`));
388
+ }
389
+
390
+ // One AI call: return a JSON plan with file operations
391
+ const result = await aiCall({
392
+ model: getModel("coding"),
393
+ systemPrompt: `You are a Node.js server architect. Given an instruction, return ONLY valid JSON describing file operations.
394
+
395
+ RULES:
396
+ - Server uses Fastify (NOT Express). Routes are async functions registered as plugins.
397
+ - New endpoints go in server/routes/ as separate files (one per resource)
398
+ - server/index.js is ONLY for wiring: fastify.register(require("./routes/X"), { prefix: "/X" })
399
+ - Route file format: async function routes(fastify) { fastify.get("/", async () => ({...})); } module.exports = routes;
400
+ - If a route file already exists for this resource, edit it. Otherwise create a new one.
401
+ - If index.js needs a new register, include that edit.
402
+ - If a skill is available below, USE IT in the generated code instead of building from scratch.
403
+ ${skillContext}
404
+
405
+ Return ONLY this JSON format:
406
+ {
407
+ "summary": "what was done",
408
+ "operations": [
409
+ { "action": "create", "path": "server/routes/time.js", "content": "full file content" },
410
+ { "action": "edit", "path": "server/index.js", "find": "exact text to find", "replace": "replacement text" }
411
+ ]
412
+ }`,
413
+ userPrompt: `Instruction: ${command}${chatContext ? "\nConversation: " + chatContext : ""}
414
+
415
+ Current server/index.js:
416
+ ${indexContent}
417
+
418
+ Existing route files:
419
+ ${existingRoutes || "(none)"}`,
420
+ maxTokens: 2048,
421
+ category: "develop",
422
+ });
423
+
424
+ const raw = (result.content || "").trim().replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
425
+ let plan;
426
+ try { plan = JSON.parse(raw); }
427
+ catch { return { success: false, summary: "AI returned invalid JSON: " + raw.slice(0, 100), filesModified: [], turns: 1, tokens: 0, mode: "direct", tier: "SMALL" }; }
428
+
429
+ const tokens = (result.usage?.prompt_tokens || result.usage?.input_tokens || 0)
430
+ + (result.usage?.completion_tokens || result.usage?.output_tokens || 0);
431
+
432
+ // Backup entire server/ before making changes
433
+ if (this.runner && this.runner.backupManager) {
434
+ const bid = this.runner.backupManager.createBackup(null);
435
+ this.runner.backupManager.markVerified(bid);
436
+ console.log(chalk.gray(` 💾 Backup created: ${bid}`));
437
+ }
438
+
439
+ // Execute operations
440
+ const filesModified = [];
441
+ for (const op of (plan.operations || [])) {
442
+ const filePath = path.resolve(cwd, op.path);
443
+ const relPath = op.path;
444
+
445
+ // Protected path check
446
+ const { AgentEngine } = require("../agent/agent-engine");
447
+ const guard = new AgentEngine({ cwd });
448
+ if (guard._isProtectedPath(relPath)) {
449
+ console.log(chalk.red(` 🛡️ Blocked: ${relPath}`));
450
+ continue;
451
+ }
452
+
453
+ if (op.action === "create") {
454
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
455
+ this.sandbox.writeFile(filePath, op.content);
456
+ filesModified.push(relPath);
457
+ console.log(chalk.green(` 📝 Created: ${relPath}`));
458
+ } else if (op.action === "edit" && op.find && op.replace !== undefined) {
459
+ try {
460
+ const current = this.sandbox.readFile(filePath);
461
+ if (current.includes(op.find)) {
462
+ this.sandbox.writeFile(filePath, current.replace(op.find, op.replace));
463
+ filesModified.push(relPath);
464
+ console.log(chalk.green(` ✏️ Edited: ${relPath}`));
465
+ } else {
466
+ console.log(chalk.yellow(` ⚠️ Could not find text to replace in ${relPath}`));
467
+ }
468
+ } catch (e) { console.log(chalk.yellow(` ⚠️ ${relPath}: ${e.message}`)); }
469
+ }
470
+ }
471
+
472
+ // Restart the server to pick up changes, then test
473
+ const port = parseInt(process.env.PORT, 10) || 3000;
474
+ const endpointMatch = command.match(/\/\w[\w/]*/);
475
+
476
+ if (filesModified.length > 0 && this.runner) {
477
+ console.log(chalk.blue(` 🔄 Restarting server to apply changes...`));
478
+ this.runner.restart();
479
+
480
+ // Wait for server to come back up
481
+ await new Promise(r => setTimeout(r, 3000));
482
+
483
+ // Test the endpoint if one was mentioned
484
+ if (endpointMatch) {
485
+ const testPath = endpointMatch[0];
486
+ console.log(chalk.gray(` 🧪 Testing ${testPath}...`));
487
+ try {
488
+ const testResult = await new Promise((resolve) => {
489
+ const req = http.get(`http://127.0.0.1:${port}${testPath}`, { timeout: 5000 }, (res) => {
490
+ let body = "";
491
+ res.on("data", (d) => { body += d; });
492
+ res.on("end", () => resolve({ status: res.statusCode, body: body.slice(0, 200) }));
493
+ });
494
+ req.on("error", (e) => resolve({ status: 0, body: e.message }));
495
+ req.on("timeout", () => { req.destroy(); resolve({ status: 0, body: "timeout" }); });
496
+ });
497
+ if (testResult.status >= 200 && testResult.status < 400) {
498
+ console.log(chalk.green(` ✅ ${testPath} → ${testResult.status}: ${testResult.body.slice(0, 80)}`));
499
+ plan.summary += ` | Tested: ${testPath} → ${testResult.status}: ${testResult.body.slice(0, 60)}`;
500
+ } else {
501
+ console.log(chalk.yellow(` ⚠️ ${testPath} → ${testResult.status}: ${testResult.body.slice(0, 80)}`));
502
+ plan.summary += ` | Test: ${testPath} → ${testResult.status}`;
503
+ }
504
+ } catch {}
505
+ }
506
+ }
507
+
508
+ // Update brain: rescan function map + embed new knowledge
509
+ if (this.brain && this.brain._initialized && filesModified.length > 0) {
510
+ try {
511
+ const { scanProject, mapToChunks } = require("../brain/function-map");
512
+ const { embedBatch } = require("../brain/embedder");
513
+
514
+ // Rescan the project to pick up new routes/functions
515
+ this.brain.functionMap = scanProject(cwd);
516
+ console.log(chalk.gray(` 🧠 Rescanned: ${this.brain.functionMap.routes.length} routes, ${this.brain.functionMap.functions.length} functions`));
517
+
518
+ // Clear old function entries and re-embed
519
+ const oldEntries = this.brain.store.getNamespace("functions");
520
+ for (const e of oldEntries) this.brain.store.delete(e.id);
521
+
522
+ const chunks = mapToChunks(this.brain.functionMap);
523
+ if (chunks.length > 0) {
524
+ const texts = chunks.map(c => c.text);
525
+ const embeddings = await embedBatch(texts);
526
+ for (let i = 0; i < chunks.length; i++) {
527
+ this.brain.store.add("functions", chunks[i].text, embeddings[i], chunks[i].metadata);
528
+ }
529
+ }
530
+
531
+ // Also remember the specific change
532
+ await this.brain.remember("learnings", `Built: ${command}. Files: ${filesModified.join(", ")}. Routes: ${this.brain.functionMap.routes.map(r => r.method + " " + r.path).join(", ")}`, { type: "server-change" });
533
+ this.brain.store.save();
534
+ console.log(chalk.gray(` 🧠 Brain updated: ${this.brain.store.getStats().totalEntries} memories`));
535
+ } catch (e) { console.log(chalk.yellow(` ⚠️ Brain update failed: ${e.message}`)); }
536
+ }
537
+
538
+ if (this.logger) this.logger.info("agent.file_write", `Smart edit: ${plan.summary}`, { files: filesModified, tokens });
539
+
540
+ return {
541
+ success: filesModified.length > 0,
542
+ summary: plan.summary,
543
+ filesModified,
544
+ turns: 1,
545
+ tokens,
546
+ mode: "direct",
547
+ tier: "SMALL",
548
+ };
549
+ }
550
+
551
+ // ── Simple Chat — CHAT_MODEL, no tools, brain context only ──
552
+
553
+ async _simpleChat(safeCommand) {
554
+ const { aiCallWithHistory } = require("../core/ai-client");
555
+ const { getModel } = require("../core/models");
556
+
557
+ let context = "";
558
+ if (this.brain && this.brain._initialized) {
559
+ try { context = await this.brain.getContext(safeCommand); } catch {}
560
+ }
561
+
562
+ const systemMessage = {
563
+ role: "system",
564
+ content: `You are Wolverine, an AI-powered autonomous Node.js server agent. Answer the admin's question thoroughly using your knowledge.
565
+
566
+ You are a self-healing server harness that:
567
+ - Monitors a Node.js server process, catches crashes, and repairs them using AI
568
+ - Has a multi-turn agent with tools: read_file, write_file, edit_file, glob_files, grep_code, bash_exec, git_log, git_diff, web_fetch
569
+ - Has a brain (vector database) that stores errors, fixes, learnings, and server function maps
570
+ - Has skills (SQL injection prevention, database interface)
571
+ - Has a goal loop that retries with research when fixes fail
572
+ - Has a real-time dashboard with event streaming, usage analytics, and admin command interface
573
+ - Protects secrets with redaction, validates with injection detection, rate limits API calls
574
+ - Supports MCP for external tool integration
575
+ - Has 10 configurable model slots for cost optimization
576
+
577
+ RULES:
578
+ - NEVER output API keys, passwords, tokens, or secrets
579
+ - Be detailed and thorough — the admin wants to understand the system
580
+ - If they need live data or file contents, suggest they rephrase to use tools
581
+ ${context ? "\nBrain context:\n" + context : ""}`,
582
+ };
583
+
584
+ this._chatHistory.push({ role: "user", content: safeCommand });
585
+ this._compactHistory();
586
+
587
+ const messages = [systemMessage, ...this._chatHistory];
588
+ const response = await aiCallWithHistory({
589
+ model: getModel("chat"),
590
+ messages,
591
+ maxTokens: 1024,
592
+ category: "chat",
593
+ });
594
+
595
+ const content = (response.choices[0].message || response.choices[0]).content || "";
596
+ this._chatHistory.push({ role: "assistant", content });
597
+
598
+ const tokens = (response.usage?.prompt_tokens || response.usage?.input_tokens || 0)
599
+ + (response.usage?.completion_tokens || response.usage?.output_tokens || 0);
600
+ console.log(chalk.cyan(` 💬 Simple chat (${getModel("chat")}, ${tokens} tokens)`));
601
+
602
+ return {
603
+ success: true,
604
+ summary: content,
605
+ filesModified: [],
606
+ turns: 1,
607
+ tokens,
608
+ mode: "chat",
609
+ toolsUsed: [],
610
+ historyLength: this._chatHistory.length,
611
+ };
612
+ }
613
+
614
+ // ── Tool Chat — TOOL_MODEL with call_endpoint, read_file, search_brain ──
615
+ // No tool calling (nano models can't handle it). Instead:
616
+ // 1. Pre-fetch relevant endpoints from the server
617
+ // 2. Search brain for context
618
+ // 3. Pack everything into the prompt
619
+ // 4. One simple AI call — no tools, no function calling
620
+
621
+ async _chat(safeCommand) {
622
+ const { aiCallWithHistory } = require("../core/ai-client");
623
+ const { getModel } = require("../core/models");
624
+ const http = require("http");
625
+
626
+ // Brain context
627
+ let context = "";
628
+ if (this.brain && this.brain._initialized) {
629
+ try { context = await this.brain.getContext(safeCommand); } catch {}
630
+ }
631
+
632
+ // Available endpoints for the model to know about
633
+ let endpointList = "";
634
+ if (this.brain && this.brain.functionMap) {
635
+ const routes = (this.brain.functionMap.routes || []).filter(r => r.method === "GET" || r.method === "*");
636
+ if (routes.length > 0) {
637
+ endpointList = "\nAvailable GET endpoints: " + routes.map(r => r.path).join(", ");
638
+ }
639
+ }
640
+
641
+ // Chat tools — lightweight, no file access
642
+ const chatTools = [
643
+ {
644
+ type: "function",
645
+ function: {
646
+ name: "call_endpoint",
647
+ description: "Call one of the server's own endpoints. Use for live data: time, health, users, etc.",
648
+ parameters: { type: "object", properties: { path: { type: "string", description: "Endpoint path e.g. /time, /health, /api/users" } }, required: ["path"] },
649
+ },
650
+ },
651
+ {
652
+ type: "function",
653
+ function: {
654
+ name: "read_file",
655
+ description: "Read a file from the server/ directory. Use when asked about source code, configs, or file contents.",
656
+ parameters: { type: "object", properties: { path: { type: "string", description: "Relative path e.g. server/index.js, server/routes/api.js" } }, required: ["path"] },
657
+ },
658
+ },
659
+ {
660
+ type: "function",
661
+ function: {
662
+ name: "search_brain",
663
+ description: "Search wolverine's memory for past errors, fixes, learnings, or server knowledge.",
664
+ parameters: { type: "object", properties: { query: { type: "string", description: "What to search for" } }, required: ["query"] },
665
+ },
666
+ },
667
+ ];
668
+
669
+ // 3. Build endpoint list from function map
670
+ const getEndpoints = [];
671
+ if (this.brain && this.brain.functionMap) {
672
+ for (const r of (this.brain.functionMap.routes || [])) {
673
+ if ((r.method === "GET" || r.method === "*") && r.path !== "/") getEndpoints.push(r.path);
674
+ }
675
+ }
676
+
677
+ const systemMessage = {
678
+ role: "system",
679
+ content: `You are Wolverine, an AI server agent managing a live Node.js server.
680
+
681
+ MANDATORY: You MUST use call_endpoint to get real data. NEVER answer from training data when an endpoint exists.
682
+
683
+ TOOLS:
684
+ - call_endpoint(path) — Call server endpoints for live data (time, health, users, etc.)
685
+ - read_file(path) — Read source code or config files (e.g. "server/index.js")
686
+ - search_brain(query) — Search memory for past errors, fixes, learnings
687
+
688
+ AVAILABLE ENDPOINTS: ${getEndpoints.length > 0 ? getEndpoints.join(", ") : "none discovered yet"}
689
+
690
+ EXAMPLES:
691
+ - User asks "what time is it" and /time exists → call_endpoint("/time") FIRST, then answer with the data
692
+ - User asks "who are the users" and /api/users exists → call_endpoint("/api/users") FIRST
693
+ - User asks "is the server healthy" → call_endpoint("/health") FIRST
694
+
695
+ Always end your response with: [Used: tool_name /path] or [Used: none]
696
+
697
+ RULES:
698
+ - NEVER output API keys, passwords, tokens, or secrets
699
+ - If code changes needed, tell user to phrase as a build command
700
+ ${context ? "\nBrain:\n" + context : ""}`,
701
+ };
702
+
703
+ this._chatHistory.push({ role: "user", content: safeCommand });
704
+ this._compactHistory();
705
+
706
+ let messages = [systemMessage, ...this._chatHistory];
707
+ let totalTokens = 0;
708
+ const toolsUsed = [];
709
+
710
+ // Use TOOL_MODEL (supports function calling) for chat with tools
711
+ const chatModel = getModel("tool");
712
+ if (this.logger) this.logger.debug("chat.start", `Chat: ${safeCommand.slice(0, 60)}`, { model: chatModel, endpoints: getEndpoints });
713
+
714
+ for (let round = 0; round < 2; round++) {
715
+ const response = await aiCallWithHistory({
716
+ model: chatModel,
717
+ messages,
718
+ tools: chatTools,
719
+ maxTokens: 512,
720
+ });
721
+
722
+ totalTokens += (response.usage?.prompt_tokens || response.usage?.input_tokens || 0)
723
+ + (response.usage?.completion_tokens || response.usage?.output_tokens || 0);
724
+
725
+ const msg = response.choices[0].message || response.choices[0];
726
+
727
+ if (!msg.tool_calls || msg.tool_calls.length === 0) {
728
+ // Extract content — handle different model response formats
729
+ let content = msg.content || msg.output_text || "";
730
+ if (!content && response._raw && response._raw.output_text) content = response._raw.output_text;
731
+
732
+ // If content is still empty but we have tool results, synthesize a response
733
+ if (!content && toolsUsed.length > 0) {
734
+ content = "(Tool was called but model returned no text. Check tool results above.)";
735
+ }
736
+
737
+ if (toolsUsed.length > 0 && !content.includes("[Used:")) {
738
+ content += `\n[Used: ${toolsUsed.join(", ")}]`;
739
+ }
740
+ this._chatHistory.push({ role: "assistant", content });
741
+ console.log(chalk.cyan(` 💬 Chat (${chatModel}, ${totalTokens} tokens, tools: ${toolsUsed.join(", ") || "none"})`));
742
+ if (this.logger) this.logger.info("chat.response", `Chat (tools: ${toolsUsed.join(", ") || "none"})`, { tokens: totalTokens, tools: toolsUsed });
743
+ return { success: true, summary: content, filesModified: [], turns: 1, tokens: totalTokens, mode: "chat", toolsUsed, historyLength: this._chatHistory.length };
744
+ }
745
+
746
+ messages.push(msg);
747
+ for (const tc of msg.tool_calls) {
748
+ let toolResult = "";
749
+ const args = JSON.parse(tc.function.arguments || "{}");
750
+
751
+ if (tc.function.name === "call_endpoint") {
752
+ const port = parseInt(process.env.PORT, 10) || 3000;
753
+ toolsUsed.push(`call_endpoint ${args.path}`);
754
+ console.log(chalk.gray(` 🌐 Calling: GET ${args.path}`));
755
+ if (this.logger) this.logger.debug("chat.tool", `GET ${args.path}`, { tool: "call_endpoint", path: args.path });
756
+ try {
757
+ toolResult = await new Promise((resolve) => {
758
+ const req = http.get(`http://127.0.0.1:${port}${args.path}`, { timeout: 3000 }, (res) => {
759
+ let body = "";
760
+ res.on("data", (d) => { body += d; });
761
+ res.on("end", () => resolve(`HTTP ${res.statusCode}: ${body.slice(0, 500)}`));
762
+ });
763
+ req.on("error", (e) => resolve(`Error: ${e.message}`));
764
+ req.on("timeout", () => { req.destroy(); resolve("Timeout"); });
765
+ });
766
+ toolResult = this.redactor ? this.redactor.redact(toolResult) : toolResult;
767
+ console.log(chalk.green(` 🌐 → ${toolResult.slice(0, 80)}`));
768
+ } catch (e) { toolResult = `Error: ${e.message}`; }
769
+ } else if (tc.function.name === "read_file") {
770
+ const filePath = args.path || "";
771
+ toolsUsed.push(`read_file ${filePath}`);
772
+ console.log(chalk.gray(` 📖 Reading: ${filePath}`));
773
+ if (this.logger) this.logger.debug("chat.tool", `Read ${filePath}`, { tool: "read_file", path: filePath });
774
+ try {
775
+ const fs = require("fs");
776
+ const path = require("path");
777
+ const fullPath = path.resolve(process.cwd(), filePath);
778
+ // Security: only allow reading from server/ directory or project root files
779
+ const rel = path.relative(process.cwd(), fullPath).replace(/\\/g, "/");
780
+ if (rel.startsWith("..") || rel.startsWith("node_modules")) {
781
+ toolResult = "BLOCKED: Can only read files within the project.";
782
+ } else {
783
+ const content = fs.readFileSync(fullPath, "utf-8");
784
+ toolResult = this.redactor ? this.redactor.redact(content.slice(0, 3000)) : content.slice(0, 3000);
785
+ if (content.length > 3000) toolResult += "\n... (truncated, " + content.length + " chars total)";
786
+ }
787
+ console.log(chalk.green(` 📖 → ${toolResult.slice(0, 60)}`));
788
+ } catch (e) { toolResult = `Error reading file: ${e.message}`; }
789
+ } else if (tc.function.name === "run_command") {
790
+ const cmd = args.command || "";
791
+ toolsUsed.push(`run_command "${cmd.slice(0, 30)}"`);
792
+ console.log(chalk.gray(` ⚡ Running: ${cmd.slice(0, 60)}`));
793
+ if (this.logger) this.logger.debug("chat.tool", `Cmd: ${cmd}`, { tool: "run_command" });
794
+ try {
795
+ const { execSync } = require("child_process");
796
+ // Block dangerous commands
797
+ if (/rm\s+-rf|rmdir|format|mkfs|git\s+push\s+--force/i.test(cmd)) {
798
+ toolResult = "BLOCKED: Dangerous command.";
799
+ } else {
800
+ const output = execSync(cmd, { cwd: process.cwd(), encoding: "utf-8", timeout: 10000, maxBuffer: 512 * 1024 });
801
+ toolResult = this.redactor ? this.redactor.redact(output.slice(0, 2000)) : output.slice(0, 2000);
802
+ }
803
+ console.log(chalk.green(` ⚡ → ${toolResult.slice(0, 60)}`));
804
+ } catch (e) { toolResult = `Command failed: ${(e.stderr || e.message || "").slice(0, 500)}`; }
805
+ } else if (tc.function.name === "search_brain") {
806
+ toolsUsed.push(`search_brain "${(args.query || "").slice(0, 30)}"`);
807
+ console.log(chalk.gray(` 🧠 Searching: ${args.query}`));
808
+ if (this.logger) this.logger.debug("chat.tool", `Brain: ${args.query}`, { tool: "search_brain" });
809
+ if (this.brain && this.brain._initialized) {
810
+ try {
811
+ const results = await this.brain.recall(args.query, { topK: 3 });
812
+ toolResult = results.length > 0 ? results.map(r => `[${r.namespace}] ${r.text}`).join("\n") : "No matching memories.";
813
+ } catch { toolResult = "Brain search failed."; }
814
+ } else { toolResult = "Brain not initialized."; }
815
+ }
816
+
817
+ messages.push({ role: "tool", tool_call_id: tc.id, content: toolResult });
818
+ }
819
+ }
820
+
821
+ this._chatHistory.push({ role: "assistant", content: "Tool loop exhausted." });
822
+ return { success: true, summary: "Tool loop exhausted", filesModified: [], turns: 1, tokens: totalTokens, mode: "chat", toolsUsed, historyLength: this._chatHistory.length };
823
+ }
824
+
825
+ // claw-code: TranscriptStore.compact() — prune old messages to manage context window
826
+ _compactHistory() {
827
+ if (this._chatHistory.length <= this._compactAfter) return;
828
+
829
+ // Keep the last N turns (user+assistant pairs)
830
+ const keepMessages = this._maxHistoryTurns * 2;
831
+ const excess = this._chatHistory.length - keepMessages;
832
+ if (excess <= 0) return;
833
+
834
+ // Summarize the oldest messages into one context message
835
+ const old = this._chatHistory.slice(0, excess);
836
+ const summary = old.map(m => `${m.role}: ${m.content.slice(0, 100)}`).join(" | ");
837
+
838
+ // Replace old messages with a single summary
839
+ this._chatHistory = [
840
+ { role: "assistant", content: `[Earlier conversation summary: ${summary}]` },
841
+ ...this._chatHistory.slice(excess),
842
+ ];
843
+
844
+ console.log(chalk.gray(` 📝 Compacted ${excess} old messages into summary`));
845
+ }
846
+
847
+ // ── Read-only endpoints ──
848
+
849
+ _handleSSE(req, res) {
850
+ res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" });
851
+ res.write(`data: ${JSON.stringify({ type: "connected", timestamp: Date.now() })}\n\n`);
852
+ this._sseClients.add(res);
853
+ req.on("close", () => this._sseClients.delete(res));
854
+ }
855
+
856
+ _handleEvents(req, res) {
857
+ res.writeHead(200, { "Content-Type": "application/json" });
858
+ res.end(JSON.stringify(this.logger ? this.logger.query({ limit: 200 }) : []));
859
+ }
860
+
861
+ _handleStats(req, res) {
862
+ res.writeHead(200, { "Content-Type": "application/json" });
863
+ res.end(JSON.stringify({
864
+ session: this.logger ? this.logger.getSessionStats() : {},
865
+ backups: this.backupManager ? this.backupManager.getStats() : {},
866
+ health: this.healthMonitor ? this.healthMonitor.getStats() : {},
867
+ }));
868
+ }
869
+
870
+ _handleMetrics(req, res) {
871
+ res.writeHead(200, { "Content-Type": "application/json" });
872
+ res.end(JSON.stringify(this.perfMonitor ? this.perfMonitor.getMetrics() : {}));
873
+ }
874
+
875
+ _handleBackups(req, res) {
876
+ res.writeHead(200, { "Content-Type": "application/json" });
877
+ res.end(JSON.stringify(this.backupManager ? this.backupManager.manifest.backups : []));
878
+ }
879
+
880
+ _handleUsage(req, res) {
881
+ res.writeHead(200, { "Content-Type": "application/json" });
882
+ res.end(JSON.stringify(this.tokenTracker ? this.tokenTracker.getAnalytics() : {}));
883
+ }
884
+
885
+ _handleUsageHistory(req, res) {
886
+ res.writeHead(200, { "Content-Type": "application/json" });
887
+ res.end(JSON.stringify(this.tokenTracker ? this.tokenTracker.getAggregates() : {}));
888
+ }
889
+
890
+ _handleSystem(req, res) {
891
+ const { detect } = require("../core/system-info");
892
+ res.writeHead(200, { "Content-Type": "application/json" });
893
+ res.end(JSON.stringify(detect()));
894
+ }
895
+
896
+ _handleProcess(req, res) {
897
+ res.writeHead(200, { "Content-Type": "application/json" });
898
+ res.end(JSON.stringify(this.processMonitor ? this.processMonitor.getMetrics() : {}));
899
+ }
900
+
901
+ _handleRoutes(req, res) {
902
+ res.writeHead(200, { "Content-Type": "application/json" });
903
+ const metrics = this.routeProber ? this.routeProber.getMetrics() : {};
904
+ const summary = this.routeProber ? this.routeProber.getSummary() : {};
905
+ res.end(JSON.stringify({ routes: metrics, summary }));
906
+ }
907
+
908
+ _handleRepairs(req, res) {
909
+ res.writeHead(200, { "Content-Type": "application/json" });
910
+ res.end(JSON.stringify(this.repairHistory ? { repairs: this.repairHistory.getAll(), stats: this.repairHistory.getStats() } : { repairs: [], stats: {} }));
911
+ }
912
+
913
+ _handleBrain(req, res) {
914
+ res.writeHead(200, { "Content-Type": "application/json" });
915
+ res.end(JSON.stringify(this.brain ? this.brain.getStats() : {}));
916
+ }
917
+
918
+ _handleDashboard(req, res) {
919
+ res.writeHead(200, { "Content-Type": "text/html" });
920
+ res.end(DASHBOARD_HTML.replace(/__PORT__/g, String(this.port)));
921
+ }
922
+
923
+ _readBody(req, cb) {
924
+ let body = "";
925
+ req.on("data", (chunk) => { body += chunk; });
926
+ req.on("end", () => {
927
+ try { cb(JSON.parse(body)); } catch { cb({}); }
928
+ });
929
+ }
930
+ }
931
+
932
+ const DASHBOARD_HTML = `<!DOCTYPE html>
933
+ <html lang="en"><head>
934
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
935
+ <title>Wolverine Dashboard</title>
936
+ <style>
937
+ *{margin:0;padding:0;box-sizing:border-box}
938
+ :root{--bg:#0a0e14;--surface:#12171f;--surface2:#1a2030;--border:#1e2a3a;--text:#c5cdd8;--text2:#6b7a8d;--accent:#f0883e;--green:#3fb950;--red:#f85149;--blue:#58a6ff;--yellow:#d29922;--purple:#bc8cff}
939
+ body{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);display:grid;grid-template-columns:220px 1fr;grid-template-rows:56px 1fr;height:100vh;overflow:hidden}
940
+ header{grid-column:1/3;background:var(--surface);border-bottom:1px solid var(--border);display:flex;align-items:center;padding:0 24px;gap:16px}
941
+ header h1{font-size:1.1rem;color:var(--accent);font-weight:700;display:flex;align-items:center;gap:8px}
942
+ header h1 svg{width:24px;height:24px}
943
+ .conn{width:8px;height:8px;border-radius:50%;background:var(--red);transition:background .3s}
944
+ .conn.on{background:var(--green)}
945
+ .header-stat{font-size:.75rem;color:var(--text2);margin-left:auto;display:flex;gap:20px}
946
+ .header-stat span{color:var(--text)}
947
+ .auth-badge{font-size:.7rem;padding:3px 10px;border-radius:20px;background:rgba(248,81,73,.15);color:var(--red)}
948
+ .auth-badge.ok{background:rgba(63,185,80,.15);color:var(--green)}
949
+ nav{background:var(--surface);border-right:1px solid var(--border);padding:16px 0;overflow-y:auto}
950
+ nav a{display:flex;align-items:center;gap:10px;padding:10px 20px;color:var(--text2);text-decoration:none;font-size:.85rem;transition:all .15s;cursor:pointer;border-left:3px solid transparent}
951
+ nav a:hover{background:var(--surface2);color:var(--text)}
952
+ nav a.active{color:var(--accent);border-left-color:var(--accent);background:rgba(240,136,62,.06)}
953
+ nav .sep{height:1px;background:var(--border);margin:12px 16px}
954
+ nav .label{font-size:.65rem;text-transform:uppercase;letter-spacing:1px;color:var(--text2);padding:12px 20px 6px;font-weight:600}
955
+ main{overflow-y:auto;padding:24px}
956
+ .panel{display:none}.panel.active{display:block}
957
+ .stats{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:24px}
958
+ .stat-card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:18px 20px;position:relative;overflow:hidden}
959
+ .stat-card::after{content:'';position:absolute;top:0;left:0;right:0;height:3px}
960
+ .stat-card.heal::after{background:var(--green)}.stat-card.err::after{background:var(--red)}
961
+ .stat-card.roll::after{background:var(--yellow)}.stat-card.brain::after{background:var(--purple)}
962
+ .stat-card.up::after{background:var(--blue)}
963
+ .stat-val{font-size:1.8rem;font-weight:700;color:var(--text);line-height:1}
964
+ .stat-lbl{font-size:.7rem;color:var(--text2);margin-top:6px;text-transform:uppercase;letter-spacing:.5px}
965
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:20px;margin-bottom:16px}
966
+ .card h3{font-size:.85rem;color:var(--text2);margin-bottom:14px;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
967
+ .row2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
968
+ .ev{padding:10px 14px;border-radius:6px;margin-bottom:4px;font-size:.8rem;display:grid;grid-template-columns:72px 170px 1fr;gap:10px;transition:background .1s}
969
+ .ev:hover{background:var(--surface2)}
970
+ .ev .t{color:var(--text2);font-family:'SF Mono',monospace;font-size:.75rem}
971
+ .ev .tp{font-family:'SF Mono',monospace;font-size:.75rem}
972
+ .ev .m{color:var(--text);word-break:break-word}
973
+ .sev-info .tp{color:var(--green)}.sev-warn .tp{color:var(--yellow)}
974
+ .sev-error .tp,.sev-critical .tp{color:var(--red)}.sev-debug .tp{color:var(--text2)}
975
+ .ev-list{max-height:calc(100vh - 280px);overflow-y:auto}
976
+ .mrow{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid var(--border);font-size:.82rem}
977
+ .mrow:last-child{border:none}
978
+ .mrow .ep{color:var(--blue);font-family:'SF Mono',monospace;font-size:.78rem}
979
+ .mrow .vals{color:var(--text2)}.mrow .vals b{color:var(--text);font-weight:500}
980
+ .badge{display:inline-block;padding:3px 10px;border-radius:20px;font-size:.7rem;font-weight:600;letter-spacing:.3px}
981
+ .badge-stable{background:rgba(63,185,80,.15);color:var(--green)}
982
+ .badge-verified{background:rgba(88,166,255,.15);color:var(--blue)}
983
+ .badge-unstable{background:rgba(248,81,73,.15);color:var(--red)}
984
+ .ns-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
985
+ .ns-card{background:var(--surface2);border-radius:8px;padding:14px;text-align:center}
986
+ .ns-card .n{font-size:1.6rem;font-weight:700;color:var(--purple)}
987
+ .ns-card .l{font-size:.7rem;color:var(--text2);margin-top:4px;text-transform:uppercase}
988
+ .empty{color:var(--text2);font-size:.82rem;padding:20px 0;text-align:center}
989
+ .chat-wrap{display:flex;flex-direction:column;height:calc(100vh - 140px)}
990
+ .chat-log{flex:1;overflow-y:auto;padding:4px 0}
991
+ .chat-msg{padding:12px 16px;margin:6px 0;border-radius:8px;font-size:.85rem;line-height:1.5;max-width:85%;word-break:break-word}
992
+ .chat-msg.user{background:var(--surface2);margin-left:auto;border-bottom-right-radius:2px}
993
+ .chat-msg.agent{background:rgba(240,136,62,.08);border:1px solid rgba(240,136,62,.15);border-bottom-left-radius:2px}
994
+ .chat-msg.system{background:rgba(88,166,255,.08);color:var(--blue);font-size:.78rem;text-align:center;max-width:100%}
995
+ .chat-msg .files{margin-top:8px;font-size:.75rem;color:var(--text2)}
996
+ .chat-msg .files span{color:var(--green)}
997
+ .chat-input{display:flex;gap:10px;padding:14px 0 0}
998
+ .chat-input input{flex:1;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;color:var(--text);font-size:.85rem;outline:none}
999
+ .chat-input input:focus{border-color:var(--accent)}
1000
+ .chat-input button{background:var(--accent);color:white;border:none;border-radius:8px;padding:12px 24px;font-weight:600;cursor:pointer;font-size:.85rem}
1001
+ .chat-input button:hover{opacity:.9}
1002
+ .chat-input button:disabled{opacity:.4;cursor:not-allowed}
1003
+ .auth-gate{max-width:400px;margin:60px auto;text-align:center}
1004
+ .auth-gate h2{color:var(--accent);margin-bottom:8px;font-size:1.1rem}
1005
+ .auth-gate p{color:var(--text2);font-size:.82rem;margin-bottom:20px}
1006
+ .auth-gate input{width:100%;background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 16px;color:var(--text);font-size:.85rem;outline:none;text-align:center;letter-spacing:2px;margin-bottom:12px}
1007
+ .auth-gate input:focus{border-color:var(--accent)}
1008
+ .auth-gate button{background:var(--accent);color:white;border:none;border-radius:8px;padding:10px 32px;font-weight:600;cursor:pointer}
1009
+ .auth-gate .err{color:var(--red);font-size:.78rem;margin-top:8px}
1010
+ .chat-msg .mode-tag{display:inline-block;font-size:.65rem;padding:2px 6px;border-radius:4px;margin-bottom:4px}
1011
+ .mode-tag.chat{background:rgba(88,166,255,.15);color:var(--blue)}
1012
+ .mode-tag.agent{background:rgba(240,136,62,.15);color:var(--accent)}
1013
+ ::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}
1014
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
1015
+ </style></head>
1016
+ <body>
1017
+ <header>
1018
+ <h1><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg> Wolverine</h1>
1019
+ <div class="conn" id="status"></div>
1020
+ <span class="auth-badge" id="auth-badge">Locked</span>
1021
+ <div class="header-stat">
1022
+ <div>Session <span id="h-uptime">0s</span></div>
1023
+ <div>Events <span id="h-events">0</span></div>
1024
+ </div>
1025
+ </header>
1026
+ <nav>
1027
+ <div class="label">Monitor</div>
1028
+ <a class="active" data-panel="overview">📊 Overview</a>
1029
+ <a data-panel="events">📋 Events</a>
1030
+ <a data-panel="perf">⚡ Performance</a>
1031
+ <a data-panel="analytics">📊 Analytics</a>
1032
+ <div class="sep"></div>
1033
+ <div class="label">Agent</div>
1034
+ <a data-panel="command">💬 Command</a>
1035
+ <div class="sep"></div>
1036
+ <div class="label">System</div>
1037
+ <a data-panel="backups">💾 Backups</a>
1038
+ <a data-panel="brain">🧠 Brain</a>
1039
+ <a data-panel="tools">🔧 Tools</a>
1040
+ <a data-panel="repairs">🔧 Repairs</a>
1041
+ <a data-panel="usage">📈 Usage</a>
1042
+ </nav>
1043
+ <main>
1044
+ <div class="panel active" id="p-overview">
1045
+ <div class="stats">
1046
+ <div class="stat-card heal"><div class="stat-val" id="s-heals">0</div><div class="stat-lbl">Heals</div></div>
1047
+ <div class="stat-card err"><div class="stat-val" id="s-errors">0</div><div class="stat-lbl">Errors</div></div>
1048
+ <div class="stat-card roll"><div class="stat-val" id="s-rollbacks">0</div><div class="stat-lbl">Rollbacks</div></div>
1049
+ <div class="stat-card brain"><div class="stat-val" id="s-memories">0</div><div class="stat-lbl">Memories</div></div>
1050
+ <div class="stat-card up"><div class="stat-val" id="s-uptime">0s</div><div class="stat-lbl">Uptime</div></div>
1051
+ </div>
1052
+ <div class="row2">
1053
+ <div class="card"><h3>Recent Events</h3><div class="ev-list" id="ov-events" style="max-height:400px"></div></div>
1054
+ <div>
1055
+ <div class="card"><h3>Backups</h3><div id="ov-backups"><div class="empty">No backups yet</div></div></div>
1056
+ <div class="card"><h3>Performance</h3><div id="ov-metrics"><div class="empty">No traffic yet</div></div></div>
1057
+ </div>
1058
+ </div>
1059
+ </div>
1060
+ <div class="panel" id="p-events"><div class="card"><h3>Live Event Stream</h3><div class="ev-list" id="ev-all"></div></div></div>
1061
+ <div class="panel" id="p-perf"><div class="card"><h3>Endpoint Metrics</h3><div id="perf-list"><div class="empty">No traffic yet</div></div></div></div>
1062
+ <div class="panel" id="p-analytics">
1063
+ <div class="stats" style="grid-template-columns:repeat(4,1fr)">
1064
+ <div class="stat-card heal"><div class="stat-val" id="a-mem">-</div><div class="stat-lbl">Memory (RSS)</div></div>
1065
+ <div class="stat-card up"><div class="stat-val" id="a-cpu">-</div><div class="stat-lbl">CPU %</div></div>
1066
+ <div class="stat-card brain"><div class="stat-val" id="a-routes">-</div><div class="stat-lbl">Routes Monitored</div></div>
1067
+ <div class="stat-card err"><div class="stat-val" id="a-health">-</div><div class="stat-lbl">Route Health</div></div>
1068
+ </div>
1069
+ <div class="row2">
1070
+ <div class="card"><h3>Memory Over Time</h3><div id="a-mem-chart" style="height:150px"></div></div>
1071
+ <div class="card"><h3>CPU Over Time</h3><div id="a-cpu-chart" style="height:150px"></div></div>
1072
+ </div>
1073
+ <div class="card"><h3>Route Response Times</h3><div id="a-route-list"><div class="empty">Waiting for first probe cycle...</div></div></div>
1074
+ </div>
1075
+ <div class="panel" id="p-command">
1076
+ <div id="cmd-auth" class="auth-gate">
1077
+ <h2>🔐 Admin Authentication</h2>
1078
+ <p>Enter your WOLVERINE_ADMIN_KEY to access the agent command interface.</p>
1079
+ <input type="password" id="auth-input" placeholder="Admin key" autocomplete="off">
1080
+ <br><button onclick="doAuth()">Authenticate</button>
1081
+ <div class="err" id="auth-err"></div>
1082
+ </div>
1083
+ <div id="cmd-chat" style="display:none" class="chat-wrap">
1084
+ <div class="chat-log" id="chat-log">
1085
+ <div class="chat-msg system">Chat has conversation memory. Questions → chat model. Build commands → agent. AI routes automatically.</div>
1086
+ </div>
1087
+ <div class="chat-input">
1088
+ <input type="text" id="cmd-input" placeholder="Ask a question or give a command..." onkeydown="if(event.key==='Enter')sendCmd()">
1089
+ <button id="cmd-btn" onclick="sendCmd()">Send</button>
1090
+ <button onclick="clearChat()" style="background:var(--surface2);color:var(--text2);border:1px solid var(--border);border-radius:8px;padding:12px 16px;cursor:pointer;font-size:.8rem">Clear</button>
1091
+ </div>
1092
+ </div>
1093
+ </div>
1094
+ <div class="panel" id="p-backups"><div class="card"><h3>Backup History</h3><div id="bk-list"><div class="empty">No backups</div></div></div></div>
1095
+ <div class="panel" id="p-brain">
1096
+ <div class="card"><h3>Brain Statistics</h3><div class="ns-grid" id="br-ns"></div></div>
1097
+ <div class="card" style="margin-top:16px"><h3>Function Map</h3><div id="br-fmap"><div class="empty">Loading...</div></div></div>
1098
+ </div>
1099
+ <div class="panel" id="p-tools"><div class="card"><h3>Agent Tool Harness</h3><div id="tool-list"></div></div></div>
1100
+ <div class="panel" id="p-repairs">
1101
+ <div class="stats" style="grid-template-columns:repeat(4,1fr)">
1102
+ <div class="stat-card heal"><div class="stat-val" id="r-total">0</div><div class="stat-lbl">Total Repairs</div></div>
1103
+ <div class="stat-card up"><div class="stat-val" id="r-rate">0%</div><div class="stat-lbl">Success Rate</div></div>
1104
+ <div class="stat-card err"><div class="stat-val" id="r-cost">$0</div><div class="stat-lbl">Repair Cost</div></div>
1105
+ <div class="stat-card brain"><div class="stat-val" id="r-avg">0</div><div class="stat-lbl">Avg Tokens/Repair</div></div>
1106
+ </div>
1107
+ <div class="card"><h3>Repair History</h3><div id="r-list"><div class="empty">No repairs yet — crashes will be logged here with resolution details</div></div></div>
1108
+ </div>
1109
+ <div class="panel" id="p-usage">
1110
+ <div class="stats" style="grid-template-columns:repeat(3,1fr)">
1111
+ <div class="stat-card heal"><div class="stat-val" id="u-total">0</div><div class="stat-lbl">Total Tokens</div></div>
1112
+ <div class="stat-card err"><div class="stat-val" id="u-cost">$0</div><div class="stat-lbl">Total Cost (USD)</div></div>
1113
+ <div class="stat-card up"><div class="stat-val" id="u-calls">0</div><div class="stat-lbl">API Calls</div></div>
1114
+ <div class="stat-card brain"><div class="stat-val" id="u-tpm">0</div><div class="stat-lbl">Tokens/min</div></div>
1115
+ </div>
1116
+ <div class="row2">
1117
+ <div class="card"><h3>By Category</h3><div id="u-cat"></div></div>
1118
+ <div class="card"><h3>By Model</h3><div id="u-model"></div></div>
1119
+ </div>
1120
+ <div class="card"><h3>By Tool</h3><div id="u-tool"></div></div>
1121
+ <div class="card"><h3>Usage Over Time</h3><div id="u-chart" style="height:200px;position:relative"></div></div>
1122
+ </div>
1123
+ </main>
1124
+ <script>
1125
+ const P=__PORT__,B='http://localhost:'+P;
1126
+ const $=s=>document.getElementById(s);
1127
+ const esc=s=>s?s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'):'';
1128
+ let adminKey=null;
1129
+
1130
+ (function(){const m=document.cookie.match(/wolverine_admin_key=([^;]+)/);if(m){adminKey=decodeURIComponent(m[1]);verifyKey(adminKey);}})();
1131
+
1132
+ document.querySelectorAll('nav a[data-panel]').forEach(a=>{
1133
+ a.onclick=()=>{
1134
+ document.querySelectorAll('nav a').forEach(x=>x.classList.remove('active'));
1135
+ document.querySelectorAll('.panel').forEach(x=>x.classList.remove('active'));
1136
+ a.classList.add('active');$('p-'+a.dataset.panel).classList.add('active');
1137
+ };
1138
+ });
1139
+
1140
+ async function doAuth(){const key=$('auth-input').value.trim();if(!key){$('auth-err').textContent='Enter a key';return;}await verifyKey(key);}
1141
+
1142
+ async function verifyKey(key){
1143
+ try{
1144
+ const r=await fetch(B+'/api/auth/verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({key})});
1145
+ const d=await r.json();
1146
+ if(d.authorized){adminKey=key;$('cmd-auth').style.display='none';$('cmd-chat').style.display='flex';$('auth-badge').textContent='Admin';$('auth-badge').classList.add('ok');}
1147
+ else{$('auth-err').textContent=d.reason||'Invalid key';$('auth-badge').textContent='Locked';$('auth-badge').classList.remove('ok');}
1148
+ }catch(e){$('auth-err').textContent='Connection failed';}
1149
+ }
1150
+
1151
+ async function sendCmd(){
1152
+ const input=$('cmd-input'),btn=$('cmd-btn'),cmd=input.value.trim();
1153
+ if(!cmd||!adminKey)return;
1154
+ addChat('user',cmd);input.value='';btn.disabled=true;btn.textContent='Running...';
1155
+ const statusEl=document.createElement('div');
1156
+ statusEl.className='chat-msg system';
1157
+ statusEl.id='cmd-status';
1158
+ statusEl.textContent='Starting...';
1159
+ $('chat-log').appendChild(statusEl);
1160
+ $('chat-log').scrollTop=$('chat-log').scrollHeight;
1161
+ try{
1162
+ const r=await fetch(B+'/api/command',{method:'POST',headers:{'Content-Type':'application/json','X-Admin-Key':adminKey},body:JSON.stringify({command:cmd})});
1163
+ const d=await r.json();
1164
+ const old=document.getElementById('cmd-status');if(old)old.remove();
1165
+ if(d.error){addChat('agent',d.error+(d.reason?' — '+d.reason:''));}
1166
+ else{
1167
+ const mode=d.mode||'agent';
1168
+ let msg=(d.summary||'Done.');
1169
+ if(d.filesModified&&d.filesModified.length>0)msg+='\\n\\nFiles: '+d.filesModified.join(', ');
1170
+ const ctx=d.historyLength?' | ctx:'+d.historyLength:'';
1171
+ const tools=d.toolsUsed&&d.toolsUsed.length?' | tools: '+d.toolsUsed.join(', '):'';
1172
+ msg+='\\n('+mode+', '+(d.turns||0)+' turn'+(d.turns!==1?'s':'')+', '+(d.tokens||0)+' tokens'+ctx+tools+')';
1173
+ addChat('agent',msg,d.filesModified,mode);
1174
+ }
1175
+ }catch(e){const log=$('chat-log');if(log.lastChild)log.removeChild(log.lastChild);addChat('agent','Error: '+e.message);}
1176
+ finally{btn.disabled=false;btn.textContent='Send';$('cmd-input').focus();}
1177
+ }
1178
+
1179
+ async function clearChat(){
1180
+ try{await fetch(B+'/api/chat/clear',{method:'POST',headers:{'X-Admin-Key':adminKey}});}catch{}
1181
+ $('chat-log').innerHTML='<div class="chat-msg system">Conversation cleared. Memory reset.</div>';
1182
+ }
1183
+
1184
+ function addChat(role,text,files,mode){
1185
+ const log=$('chat-log'),div=document.createElement('div');
1186
+ div.className='chat-msg '+role;
1187
+ let html='';
1188
+ if(mode)html+='<span class="mode-tag '+mode+'">'+mode+'</span><br>';
1189
+ html+=esc(text).replace(/\\n/g,'<br>');
1190
+ if(files&&files.length)html+='<div class="files">Modified: '+files.map(f=>'<span>'+esc(f)+'</span>').join(', ')+'</div>';
1191
+ div.innerHTML=html;log.appendChild(div);log.scrollTop=log.scrollHeight;
1192
+ }
1193
+
1194
+ const es=new EventSource(B+'/api/events/stream');
1195
+ es.onopen=()=>$('status').classList.add('on');
1196
+ es.onerror=()=>$('status').classList.remove('on');
1197
+ es.onmessage=e=>{
1198
+ const ev=JSON.parse(e.data);if(ev.type==='connected')return;
1199
+ addEvent(ev);
1200
+ // Update command status with live progress
1201
+ const st=document.getElementById('cmd-status');
1202
+ if(st){
1203
+ const icons={classify:'🏷️',chat:'💬','chat.tool':'🔌','chat.start':'🧠','chat.response':'✅',security:'🛡️',heal:'🔧',develop:'🔨','agent.turn':'🤖','agent.file_read':'📖','agent.file_write':'✏️','agent.command':'🎯','mcp.tool_call':'🔌',goal:'🎯'};
1204
+ const icon=icons[ev.type]||icons[ev.type.split('.')[0]]||'⏳';
1205
+ st.textContent=icon+' '+ev.message.slice(0,120);
1206
+ $('chat-log').scrollTop=$('chat-log').scrollHeight;
1207
+ }
1208
+ };
1209
+
1210
+ function addEvent(ev){
1211
+ ['ov-events','ev-all'].forEach(id=>{const el=$(id);if(!el)return;const d=document.createElement('div');d.className='ev sev-'+ev.severity;
1212
+ d.innerHTML='<span class="t">'+new Date(ev.timestamp).toLocaleTimeString()+'</span><span class="tp">'+esc(ev.type)+'</span><span class="m">'+esc(ev.message)+'</span>';
1213
+ el.prepend(d);if(el.children.length>300)el.removeChild(el.lastChild);});
1214
+ }
1215
+
1216
+ async function refresh(){
1217
+ try{
1218
+ const [sr,mr,br2,brn,usage,repairs,proc,routeData]=await Promise.all([fetch(B+'/api/stats').then(r=>r.json()),fetch(B+'/api/metrics').then(r=>r.json()),fetch(B+'/api/backups').then(r=>r.json()),fetch(B+'/api/brain').then(r=>r.json()),fetch(B+'/api/usage').then(r=>r.json()).catch(()=>({})),fetch(B+'/api/repairs').then(r=>r.json()).catch(()=>({repairs:[],stats:{}})),fetch(B+'/api/process').then(r=>r.json()).catch(()=>({})),fetch(B+'/api/routes').then(r=>r.json()).catch(()=>({routes:{},summary:{}}))]);
1219
+ if(sr.session){$('s-heals').textContent=sr.session.heals||0;$('s-errors').textContent=sr.session.errors||0;$('s-rollbacks').textContent=sr.session.rollbacks||0;$('h-events').textContent=sr.session.totalEvents||0;
1220
+ const s=Math.round((sr.session.uptime||0)/1000),m=Math.floor(s/60),h=Math.floor(m/60);
1221
+ const ut=h>0?h+'h '+m%60+'m':m>0?m+'m '+s%60+'s':s+'s';$('s-uptime').textContent=ut;$('h-uptime').textContent=ut;}
1222
+ $('s-memories').textContent=brn.totalEntries||0;
1223
+ if(brn.namespaces)$('br-ns').innerHTML=Object.entries(brn.namespaces).map(([k,v])=>'<div class="ns-card"><div class="n">'+v+'</div><div class="l">'+esc(k)+'</div></div>').join('');
1224
+ if(brn.functionMap){const fm=brn.functionMap;$('br-fmap').innerHTML=['Routes:'+fm.routes,'Functions:'+fm.functions,'Classes:'+fm.classes,'Files:'+fm.files].map(x=>'<div class="mrow"><span>'+x.split(':')[0]+'</span><span class="vals"><b>'+x.split(':')[1]+'</b></span></div>').join('');}
1225
+ const eps=Object.entries(mr);const mh=eps.length?eps.map(([p,m])=>'<div class="mrow"><span class="ep">'+esc(p)+'</span><span class="vals"><b>'+m.avgResponseMs+'ms</b> avg &middot; '+m.requestsPerMin+' req/min &middot; '+m.errorRate+'% err</span></div>').join(''):'<div class="empty">No traffic yet</div>';
1226
+ $('ov-metrics').innerHTML=mh;$('perf-list').innerHTML=mh;
1227
+ const bh=br2.length?br2.slice(-15).reverse().map(b=>'<div class="mrow"><span>'+new Date(b.timestamp).toLocaleString()+'</span><span><span class="badge badge-'+b.status+'">'+b.status+'</span> '+b.files.length+' file(s)</span></div>').join(''):'<div class="empty">No backups</div>';
1228
+ $('ov-backups').innerHTML=bh;$('bk-list').innerHTML=bh;
1229
+ // Usage analytics
1230
+ if(usage&&usage.session){
1231
+ $('u-total').textContent=(usage.session.totalTokens||0).toLocaleString();
1232
+ $('u-cost').textContent='$'+(usage.session.totalCostUsd||0).toFixed(4);
1233
+ $('u-calls').textContent=usage.session.totalCalls||0;
1234
+ $('u-tpm').textContent=usage.session.tokensPerMinute||0;
1235
+ const catColors={heal:'var(--red)',develop:'var(--blue)',chat:'var(--green)',security:'var(--yellow)',classify:'var(--text2)',research:'var(--purple)',brain:'var(--purple)',mcp:'var(--accent)'};
1236
+ const fmt$=n=>'$'+(n||0).toFixed(4);
1237
+ if(usage.byCategory){
1238
+ $('u-cat').innerHTML=Object.entries(usage.byCategory).sort((a,b)=>b[1].cost-a[1].cost).map(([k,v])=>'<div class="mrow"><span class="ep" style="color:'+(catColors[k]||'var(--text)')+'">'+k+'</span><span class="vals"><b>'+fmt$(v.cost)+'</b> &middot; '+v.total.toLocaleString()+' tokens (in:'+v.input.toLocaleString()+' out:'+v.output.toLocaleString()+') &middot; '+v.calls+' calls</span></div>').join('')||'<div class="empty">No usage yet</div>';
1239
+ }
1240
+ if(usage.byModel){
1241
+ $('u-model').innerHTML=Object.entries(usage.byModel).sort((a,b)=>b[1].cost-a[1].cost).map(([k,v])=>'<div class="mrow"><span class="ep">'+k+'</span><span class="vals"><b>'+fmt$(v.cost)+'</b> &middot; '+v.total.toLocaleString()+' tokens (in:'+v.input.toLocaleString()+' out:'+v.output.toLocaleString()+') &middot; '+v.calls+' calls</span></div>').join('')||'<div class="empty">No usage yet</div>';
1242
+ }
1243
+ if(usage.byTool){
1244
+ $('u-tool').innerHTML=Object.entries(usage.byTool).sort((a,b)=>b[1].total-a[1].total).map(([k,v])=>'<div class="mrow"><span class="ep">'+k+'</span><span class="vals"><b>'+v.total.toLocaleString()+'</b> tokens &middot; '+v.calls+' calls</span></div>').join('')||'<div class="empty">No tool usage yet</div>';
1245
+ }
1246
+ // Timeline chart
1247
+ if(usage.timeline&&usage.timeline.length>1){
1248
+ const tl=usage.timeline;
1249
+ const max=Math.max(...tl.map(e=>e.tokens))||1;
1250
+ const w=$('u-chart').offsetWidth||600;
1251
+ const h=180;
1252
+ const barW=Math.max(4,Math.floor(w/tl.length)-2);
1253
+ const catC={heal:'#f85149',develop:'#58a6ff',chat:'#3fb950',security:'#d29922',classify:'#6b7a8d',research:'#bc8cff',brain:'#bc8cff'};
1254
+ let svg='<svg width="'+w+'" height="'+h+'" style="display:block">';
1255
+ tl.forEach((e,i)=>{
1256
+ const bh=Math.max(2,Math.round((e.tokens/max)*h*0.85));
1257
+ const x=i*(barW+2);
1258
+ const color=catC[e.cat]||'#58a6ff';
1259
+ svg+='<rect x="'+x+'" y="'+(h-bh)+'" width="'+barW+'" height="'+bh+'" fill="'+color+'" rx="2"><title>'+e.tokens+' tokens ('+e.cat+', '+e.model+')</title></rect>';
1260
+ });
1261
+ svg+='</svg>';
1262
+ $('u-chart').innerHTML=svg;
1263
+ }else{
1264
+ $('u-chart').innerHTML='<div class="empty">Chart appears after multiple API calls</div>';
1265
+ }
1266
+ }
1267
+ // Repair history
1268
+ if(repairs&&repairs.stats){
1269
+ const rs=repairs.stats;
1270
+ $('r-total').textContent=rs.total||0;
1271
+ $('r-rate').textContent=(rs.successRate||0)+'%';
1272
+ $('r-cost').textContent='$'+(rs.totalCost||0).toFixed(4);
1273
+ $('r-avg').textContent=(rs.avgTokensPerRepair||0).toLocaleString();
1274
+ }
1275
+ if(repairs&&repairs.repairs&&repairs.repairs.length>0){
1276
+ $('r-list').innerHTML=repairs.repairs.slice(-20).reverse().map(r=>{
1277
+ const icon=r.success?'✅':'❌';
1278
+ const date=new Date(r.timestamp).toLocaleString();
1279
+ const cost='$'+(r.cost||0).toFixed(4);
1280
+ return '<div class="mrow" style="flex-wrap:wrap"><div style="flex:1"><span style="margin-right:6px">'+icon+'</span><span class="ep">'+esc(r.error).slice(0,60)+'</span></div><span class="vals">'+r.mode+' &middot; '+r.tokens.toLocaleString()+' tokens &middot; '+cost+' &middot; iter '+r.iteration+' &middot; '+(r.duration/1000).toFixed(1)+'s</span><div style="width:100%;font-size:.75rem;color:var(--text2);margin-top:4px">'+date+' — '+esc(r.resolution).slice(0,100)+'</div></div>';
1281
+ }).join('');
1282
+ }
1283
+ // Analytics: process + routes
1284
+ if(proc&&proc.current){
1285
+ $('a-mem').textContent=proc.current.rss+'MB';
1286
+ $('a-cpu').textContent=proc.current.cpu+'%';
1287
+ }
1288
+ if(routeData&&routeData.summary){
1289
+ const rs=routeData.summary;
1290
+ $('a-routes').textContent=rs.totalRoutes||0;
1291
+ $('a-health').textContent=(rs.healthy||0)+'/'+(rs.totalRoutes||0);
1292
+ }
1293
+ // Memory chart
1294
+ if(proc&&proc.samples&&proc.samples.length>1){
1295
+ const s=proc.samples,max=Math.max(...s.map(e=>e.rss))||1,w=$('a-mem-chart').offsetWidth||500,h=140;
1296
+ const bw=Math.max(3,Math.floor(w/s.length)-1);
1297
+ let svg='<svg width="'+w+'" height="'+h+'">';
1298
+ s.forEach((e,i)=>{const bh=Math.max(2,Math.round((e.rss/max)*h*0.85));svg+='<rect x="'+(i*(bw+1))+'" y="'+(h-bh)+'" width="'+bw+'" height="'+bh+'" fill="var(--blue)" rx="1"><title>'+e.rss+'MB</title></rect>';});
1299
+ svg+='</svg>';$('a-mem-chart').innerHTML=svg;
1300
+ }
1301
+ // CPU chart
1302
+ if(proc&&proc.samples&&proc.samples.length>1){
1303
+ const s=proc.samples,max=Math.max(...s.map(e=>e.cpu),1),w=$('a-cpu-chart').offsetWidth||500,h=140;
1304
+ const bw=Math.max(3,Math.floor(w/s.length)-1);
1305
+ let svg='<svg width="'+w+'" height="'+h+'">';
1306
+ s.forEach((e,i)=>{const bh=Math.max(2,Math.round((e.cpu/max)*h*0.85));const c=e.cpu>80?'var(--red)':e.cpu>50?'var(--yellow)':'var(--green)';svg+='<rect x="'+(i*(bw+1))+'" y="'+(h-bh)+'" width="'+bw+'" height="'+bh+'" fill="'+c+'" rx="1"><title>'+e.cpu+'%</title></rect>';});
1307
+ svg+='</svg>';$('a-cpu-chart').innerHTML=svg;
1308
+ }
1309
+ // Route list
1310
+ if(routeData&&routeData.routes){
1311
+ const routes=Object.entries(routeData.routes);
1312
+ if(routes.length>0){
1313
+ const trendIcon={stable:'→',degrading:'↗',improving:'↘'};
1314
+ $('a-route-list').innerHTML=routes.sort((a,b)=>b[1].avgMs-a[1].avgMs).map(([p,m])=>{
1315
+ const icon=m.healthy?'🟢':'🔴';
1316
+ const trend=trendIcon[m.trend]||'→';
1317
+ return '<div class="mrow"><span>'+icon+' <span class="ep">'+esc(p)+'</span></span><span class="vals"><b>'+m.avgMs+'ms</b> avg ('+m.minMs+'-'+m.maxMs+') '+trend+' &middot; '+m.errors+' errs &middot; '+m.samples+' probes</span></div>';
1318
+ }).join('');
1319
+ }
1320
+ }
1321
+ }catch(e){}
1322
+ }
1323
+
1324
+ const tools=[{n:'read_file',d:'Read file with offset/limit',c:'file'},{n:'write_file',d:'Write complete file',c:'file'},{n:'edit_file',d:'Find-and-replace edit',c:'file'},{n:'glob_files',d:'Pattern file discovery',c:'file'},{n:'grep_code',d:'Regex search with context',c:'file'},{n:'bash_exec',d:'Sandboxed shell',c:'shell'},{n:'git_log',d:'Recent commits',c:'shell'},{n:'git_diff',d:'Uncommitted changes',c:'shell'},{n:'web_fetch',d:'Fetch URL',c:'web'},{n:'done',d:'Signal completion',c:'ctrl'}];
1325
+ const cc={file:'var(--blue)',shell:'var(--yellow)',web:'var(--purple)',ctrl:'var(--green)'};
1326
+ $('tool-list').innerHTML=tools.map(t=>'<div class="mrow"><span class="ep" style="color:'+cc[t.c]+'">'+t.n+'</span><span class="vals">'+esc(t.d)+'</span></div>').join('');
1327
+
1328
+ refresh();setInterval(refresh,5000);
1329
+ fetch(B+'/api/events').then(r=>r.json()).then(evs=>evs.forEach(addEvent)).catch(()=>{});
1330
+ </script></body></html>`;
1331
+
1332
+ module.exports = { DashboardServer };