wolverine-ai 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PLATFORM.md CHANGED
@@ -103,6 +103,14 @@ Content-Type: application/json
103
103
  "chat": { "tokens": 25000, "cost": 0.05, "calls": 60 },
104
104
  "classify": { "tokens": 3000, "cost": 0.001, "calls": 15 },
105
105
  "develop": { "tokens": 5000, "cost": 0.03, "calls": 5 }
106
+ },
107
+ "byModel": {
108
+ "gpt-5.4-mini": { "tokens": 30000, "cost": 0.06, "calls": 40 },
109
+ "gpt-4o-mini": { "tokens": 15000, "cost": 0.02, "calls": 45 }
110
+ },
111
+ "byTool": {
112
+ "call_endpoint": { "tokens": 5000, "cost": 0.01, "calls": 20 },
113
+ "search_brain": { "tokens": 2000, "cost": 0.005, "calls": 10 }
106
114
  }
107
115
  },
108
116
 
package/README.md CHANGED
@@ -10,15 +10,29 @@ Built on patterns from [claw-code](https://github.com/instructkr/claw-code) —
10
10
 
11
11
  ## Quick Start
12
12
 
13
+ ### Install from npm
14
+
15
+ ```bash
16
+ npm i wolverine-ai
17
+ cp node_modules/wolverine-ai/.env.example .env.local
18
+ # Edit .env.local — add your OPENAI_API_KEY
19
+ npx wolverine server/index.js
20
+ ```
21
+
22
+ ### Or clone from GitHub
23
+
13
24
  ```bash
14
25
  git clone https://github.com/bobbyswhip/Wolverine.git
15
26
  cd Wolverine
16
27
  npm install
17
28
  cp .env.example .env.local
18
- # Edit .env.local — add your OPENAI_API_KEY and generate an ADMIN_KEY
29
+ # Edit .env.local — add your OPENAI_API_KEY
19
30
  npm start
20
31
  ```
21
32
 
33
+ [![npm](https://img.shields.io/npm/v/wolverine-ai)](https://www.npmjs.com/package/wolverine-ai)
34
+ [![GitHub](https://img.shields.io/github/stars/bobbyswhip/Wolverine)](https://github.com/bobbyswhip/Wolverine)
35
+
22
36
  Dashboard opens at `http://localhost:PORT+1`. Server runs on `PORT`.
23
37
 
24
38
  ### Try a Demo
@@ -359,7 +373,8 @@ Startup:
359
373
  - Auto-registers on first run, retries every 60s until platform responds
360
374
  - Saves key to `.wolverine/platform-key` (survives restarts)
361
375
  - Sends one ~2KB JSON POST every 60 seconds (5s timeout, non-blocking)
362
- - Payload matches [PLATFORM.md](PLATFORM.md) spec: `instanceId`, `server`, `process`, `routes`, `repairs`, `usage`, `brain`, `backups`
376
+ - Payload matches [PLATFORM.md](PLATFORM.md) spec: `instanceId`, `server`, `process`, `routes`, `repairs`, `usage` (tokens/cost/calls + `byCategory` + `byModel` + `byTool`), `brain`, `backups`
377
+ - Platform analytics aggregates across all servers: total tokens/cost, breakdown by category (heal/chat/develop/security/classify/research/brain), by model, by tool
363
378
  - Secrets redacted before sending
364
379
  - Offline-resilient: queues up to 1440 heartbeats locally, drains on reconnect
365
380
 
@@ -19,13 +19,21 @@ server/
19
19
 
20
20
  ## Rules
21
21
 
22
+ ### Ports
23
+ - **Development**: use port 3000 (standard, no admin required, firewall-friendly)
24
+ - **Production**: use port 443 (HTTPS) or 80 (HTTP) behind a reverse proxy (nginx/caddy)
25
+ - **Never** use random high ports in production — they bypass firewalls and confuse load balancers
26
+ - **Always** use HTTPS in production — terminate TLS at the reverse proxy, not in Node
27
+ - Dashboard runs on port+1 automatically (3001 in dev, not exposed in prod)
28
+
22
29
  ### Security
23
30
  - Never expose secrets in responses — use env vars, never hardcode
24
- - Validate ALL input — use express.json() with size limits
25
- - Use helmet() for HTTP security headers in production
31
+ - Validate ALL input — Fastify has built-in JSON schema validation
32
+ - Use HTTPS in production reverse proxy (nginx/caddy) handles TLS
26
33
  - Rate limit public endpoints
27
- - Sanitize user input before database queries
34
+ - Sanitize user input before database queries — use the SQL skill
28
35
  - Never return stack traces in production error responses
36
+ - Use the sqlGuard() middleware on all routes that accept user input
29
37
 
30
38
  ### Scalability
31
39
  - Keep routes thin — business logic goes in services/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Self-healing Node.js server framework powered by AI. Catches crashes, diagnoses errors, generates fixes, verifies, and restarts — automatically.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  },
19
19
 
20
20
  "server": {
21
- "port": 6969,
21
+ "port": 3000,
22
22
  "maxRetries": 3,
23
23
  "maxMemoryMB": 512
24
24
  },
@@ -1,6 +1,7 @@
1
1
  const chalk = require("chalk");
2
2
  const { aiCall } = require("../core/ai-client");
3
3
  const { getModel } = require("../core/models");
4
+ const { redact } = require("../security/secret-redactor");
4
5
 
5
6
  /**
6
7
  * Research Agent — deep research + learning from experience.
@@ -18,7 +19,6 @@ class ResearchAgent {
18
19
  constructor(options = {}) {
19
20
  this.brain = options.brain;
20
21
  this.logger = options.logger;
21
- this.redactor = options.redactor;
22
22
  }
23
23
 
24
24
  /**
@@ -50,8 +50,8 @@ class ResearchAgent {
50
50
  async recordAttempt({ errorMessage, filePath, fix, success, explanation }) {
51
51
  if (!this.brain || !this.brain._initialized) return;
52
52
 
53
- const safeError = this.redactor ? this.redactor.redact(errorMessage) : errorMessage;
54
- const safeExplanation = this.redactor ? this.redactor.redact(explanation || fix || "") : (explanation || fix || "");
53
+ const safeError = redact(errorMessage);
54
+ const safeExplanation = redact(explanation || fix || "");
55
55
 
56
56
  const namespace = success ? "fixes" : "errors";
57
57
  const prefix = success ? "FIXED" : "FAILED";
@@ -66,7 +66,7 @@ class ResearchAgent {
66
66
  * Stores findings in brain for future reference.
67
67
  */
68
68
  async research(errorMessage, context) {
69
- const safeError = this.redactor ? this.redactor.redact(errorMessage) : errorMessage;
69
+ const safeError = redact(errorMessage);
70
70
 
71
71
  console.log(chalk.magenta(` 🔬 Deep research (${getModel("research")})...`));
72
72
 
@@ -139,6 +139,62 @@ const SEED_DOCS = [
139
139
  text: "Telemetry architecture: 4 files, ~250 lines total. heartbeat.js sends one HTTP POST every 60s (5s timeout, non-blocking). register.js auto-registers and caches key in memory + disk. queue.js appends to JSONL file only on failure, trims lazily. telemetry.js collects from subsystems using optional chaining (no crashes if subsystem missing). All secrets redacted before sending. Response bodies drained immediately (res.resume). No blocking, no delays, no busy waits.",
140
140
  metadata: { topic: "telemetry-architecture" },
141
141
  },
142
+ {
143
+ text: "Server uses Fastify (migrated from Express). 5.6x faster routing: ~114k req/s vs Express ~20k req/s. Routes are async plugin functions: async function routes(fastify) { fastify.get('/', async () => ({...})); } module.exports = routes. Registered in index.js with fastify.register(require('./routes/X'), {prefix:'/X'}). JSON parsing is built-in, no middleware needed.",
144
+ metadata: { topic: "fastify" },
145
+ },
146
+ {
147
+ text: "npm package: wolverine-ai on npmjs.com. Install: npm i wolverine-ai. CLI: npx wolverine server/index.js. v1.0.0, 79 files, 125KB compressed. Includes src/, bin/, server/, examples/. GitHub: https://github.com/bobbyswhip/Wolverine",
148
+ metadata: { topic: "npm-package" },
149
+ },
150
+ {
151
+ text: "Dashboard has 9 panels: Overview (stats cards + recent events), Events (live SSE stream), Performance (endpoint metrics), Analytics (memory/CPU charts, route health, response times), Command (admin chat with 3-route classifier), Backups (server/ snapshots with status badges), Brain (vector store stats + function map), Repairs (error/resolution audit trail with tokens and cost), Tools (agent tool harness listing), Usage (token analytics by model/category/tool with USD costs).",
152
+ metadata: { topic: "dashboard-panels" },
153
+ },
154
+ {
155
+ text: "Command interface routing: AI classifier (CLASSIFIER_MODEL) returns SIMPLE/TOOLS/AGENT. SIMPLE = brain knowledge only (CHAT_MODEL, no tools). TOOLS = live data with function calling (TOOL_MODEL, call_endpoint/read_file/search_brain). AGENT SMALL = smart edit (CODING_MODEL, 1 AI call, structured JSON file operations). AGENT MEDIUM = single agent (REASONING_MODEL, 8 turns). AGENT LARGE = sub-agents (explore→plan→fix).",
156
+ metadata: { topic: "command-routing" },
157
+ },
158
+ {
159
+ text: "Smart edit: for SMALL tier tasks, one AI call returns JSON with file operations: [{action:'create',path:'server/routes/X.js',content:'...'},{action:'edit',path:'server/index.js',find:'...',replace:'...'}]. Creates backup before changes, restarts server after, tests endpoint, rescans brain with new routes. Skills auto-injected into prompt when relevant.",
160
+ metadata: { topic: "smart-edit" },
161
+ },
162
+ {
163
+ text: "Token tracking: every AI call tracked with input/output tokens + USD cost. Categories: heal, develop, chat, security, classify, research, brain. Tracked by model, by category, by tool. Persisted to .wolverine/usage.json (aggregates) and .wolverine/usage-history.jsonl (full timeline). Auto-saves on every call. Dashboard shows charts + cost breakdowns. Pricing from src/logger/pricing.js, customizable via .wolverine/pricing.json.",
164
+ metadata: { topic: "token-tracking" },
165
+ },
166
+ {
167
+ text: "Repair history: dedicated audit trail at .wolverine/repair-history.json. Each entry: error, file, line, resolution, success, mode (fast/agent/sub-agents), model, tokens, cost, iteration, duration, filesModified. Dashboard Repairs panel shows stats (total, success rate, total cost, avg tokens) + scrollable history with per-repair details.",
168
+ metadata: { topic: "repair-history" },
169
+ },
170
+ {
171
+ text: "Skill registry: auto-discovers skills from src/skills/ on startup. Each skill exports SKILL_NAME, SKILL_DESCRIPTION, SKILL_KEYWORDS, SKILL_USAGE. Registry matches skills to commands using token scoring (claw-code pattern). Matched skills get injected into agent prompts before AI calls. SQL skill auto-injects when building database features.",
172
+ metadata: { topic: "skill-registry" },
173
+ },
174
+ {
175
+ text: "Notifications: detects human-required errors (expired keys, billing, service down, certs, permissions, disk). Classifies errors as AI-fixable vs human-required using pattern matching. Generates AI summary (CHAT_MODEL). Fires before wasting tokens on repair. Console alert + dashboard event + optional webhook. Categories: auth, billing, service, cert, permission, disk.",
176
+ metadata: { topic: "notifications" },
177
+ },
178
+ {
179
+ text: "MCP integration: connect external tools via Model Context Protocol. Configure in .wolverine/mcp.json with per-server tool allowlists. Security: arg sanitization (secrets redacted before sending to MCP servers), result injection scanning, rate limiting per server, audit logging. Tools appear as mcp__server__tool in the agent. Supports stdio and HTTP transports.",
180
+ metadata: { topic: "mcp" },
181
+ },
182
+ {
183
+ text: "Demos: 7 demo servers in examples/demos/. Demo runner (examples/run-demo.js) copies demo into server/, runs wolverine, restores on exit. npm run demo:list shows all demos. Each demo is a proper Fastify server with routes/ that mirrors the real server/ structure. Tests: basic typo, multi-file, syntax error, secret leak, expired key, JSON config, null crash.",
184
+ metadata: { topic: "demos" },
185
+ },
186
+ {
187
+ text: "10 configurable models: REASONING_MODEL (multi-file agent), CODING_MODEL (code repair, Responses API for codex), CHAT_MODEL (simple text), TOOL_MODEL (function calling), CLASSIFIER_MODEL (routing), AUDIT_MODEL (injection detection), COMPACTING_MODEL (brain text compression), RESEARCH_MODEL (deep research), TEXT_EMBEDDING_MODEL (vectors). All in server/config/settings.json. Reasoning models auto-get 4x token limits for chain-of-thought.",
188
+ metadata: { topic: "model-slots" },
189
+ },
190
+ {
191
+ text: "Port best practices: development uses port 3000 (standard, no admin, firewall-friendly). Production uses 443 (HTTPS) or 80 (HTTP) behind a reverse proxy (nginx/caddy). Never use random high ports in production — they bypass firewalls and confuse load balancers. Always use HTTPS in production — terminate TLS at the proxy, not in Node. Dashboard auto-runs on port+1. Wolverine warns on startup if the port is non-standard.",
192
+ metadata: { topic: "port-security" },
193
+ },
194
+ {
195
+ text: "Secret redaction is a singleton: initRedactor(projectRoot) called once on startup, then redact(text), redactObj(obj), hasSecrets(text) available everywhere via require('../security/secret-redactor'). No need to pass redactor instances. Every outbound path auto-redacts: event logger, repair history, telemetry heartbeats, brain memories, AI calls, dashboard output. Env variable values replaced with process.env.KEY_NAME.",
196
+ metadata: { topic: "redaction-singleton" },
197
+ },
142
198
  ];
143
199
 
144
200
  class Brain {
@@ -10,7 +10,7 @@ const { EventLogger, EVENT_TYPES } = require("../logger/event-logger");
10
10
  const { DashboardServer } = require("../dashboard/server");
11
11
  const { PerfMonitor } = require("../monitor/perf-monitor");
12
12
  const { Brain } = require("../brain/brain");
13
- const { SecretRedactor } = require("../security/secret-redactor");
13
+ const { initRedactor, getRedactor } = require("../security/secret-redactor");
14
14
  const { McpRegistry } = require("../mcp/mcp-registry");
15
15
  const { TokenTracker } = require("../logger/token-tracker");
16
16
  const { RepairHistory } = require("../logger/repair-history");
@@ -43,7 +43,7 @@ class WolverineRunner {
43
43
 
44
44
  // Core subsystems
45
45
  this.sandbox = new Sandbox(this.cwd);
46
- this.redactor = new SecretRedactor(this.cwd);
46
+ this.redactor = initRedactor(this.cwd);
47
47
  this.rateLimiter = new RateLimiter({
48
48
  maxCallsPerWindow: parseInt(process.env.WOLVERINE_RATE_MAX_CALLS, 10) || 10,
49
49
  windowMs: parseInt(process.env.WOLVERINE_RATE_WINDOW_MS, 10) || 600000,
@@ -140,6 +140,20 @@ class WolverineRunner {
140
140
  maxRetries: this.maxRetries,
141
141
  });
142
142
 
143
+ // Port safety check
144
+ const port = parseInt(process.env.PORT, 10) || 3000;
145
+ const safeDevPorts = [3000, 3001, 8080, 8443];
146
+ const safeProdPorts = [80, 443, 8080, 8443];
147
+ const env = process.env.NODE_ENV || "development";
148
+
149
+ if (env === "production" && port !== 443 && port !== 80 && port !== 8443 && port !== 8080) {
150
+ console.log(chalk.yellow(` ⚠️ Port ${port} in production — recommend 443 (HTTPS) or 80 (HTTP) behind a reverse proxy`));
151
+ } else if (env !== "production" && !safeDevPorts.includes(port) && port < 1024) {
152
+ console.log(chalk.yellow(` ⚠️ Port ${port} requires root/admin — use 3000 for local development`));
153
+ } else if (port > 9999) {
154
+ console.log(chalk.yellow(` ⚠️ Port ${port} is non-standard — use 3000 (dev) or 443 (prod) for best compatibility`));
155
+ }
156
+
143
157
  // Initialize brain (scan project, seed docs, embed function map)
144
158
  try {
145
159
  await this.brain.init();
@@ -23,10 +23,12 @@ const { EVENT_TYPES } = require("../logger/event-logger");
23
23
  *
24
24
  * The engine tries fast path first. If that fails verification, it escalates to the agent.
25
25
  */
26
- async function heal({ stderr, cwd, sandbox, redactor, notifier, rateLimiter, backupManager, logger, brain, mcp, skills, repairHistory }) {
26
+ async function heal({ stderr, cwd, sandbox, notifier, rateLimiter, backupManager, logger, brain, mcp, skills, repairHistory }) {
27
27
  const healStartTime = Date.now();
28
- // Redact secrets from stderr BEFORE any processing, logging, or AI calls
29
- const safeStderr = redactor ? redactor.redact(stderr) : stderr;
28
+ const { redact, hasSecrets } = require("../security/secret-redactor");
29
+
30
+ // Redact secrets BEFORE any processing, logging, or AI calls
31
+ const safeStderr = redact(stderr);
30
32
 
31
33
  if (logger) logger.info(EVENT_TYPES.HEAL_START, "Wolverine detected a crash", { stderr: safeStderr.slice(0, 500) });
32
34
  console.log(chalk.yellow("\n🐺 Wolverine detected a crash. Analyzing...\n"));
@@ -35,13 +37,11 @@ async function heal({ stderr, cwd, sandbox, redactor, notifier, rateLimiter, bac
35
37
  const parsed = parseError(stderr);
36
38
  const errorSignature = RateLimiter.signature(parsed.errorMessage, parsed.filePath);
37
39
 
38
- // Redact the parsed fields — these go to AI, brain, and logs
39
- if (redactor) {
40
- parsed.errorMessage = redactor.redact(parsed.errorMessage);
41
- parsed.stackTrace = redactor.redact(parsed.stackTrace);
42
- }
40
+ // Redact parsed fields — these go to AI, brain, and logs
41
+ parsed.errorMessage = redact(parsed.errorMessage);
42
+ parsed.stackTrace = redact(parsed.stackTrace);
43
43
 
44
- if (redactor && redactor.containsSecrets(stderr)) {
44
+ if (hasSecrets(stderr)) {
45
45
  console.log(chalk.yellow(" 🔐 Secrets detected in error output — redacted before AI/brain/logs"));
46
46
  }
47
47
 
@@ -136,7 +136,7 @@ async function heal({ stderr, cwd, sandbox, redactor, notifier, rateLimiter, bac
136
136
  }
137
137
 
138
138
  // 6. Research — check past attempts to avoid loops
139
- const researcher = new ResearchAgent({ brain, logger, redactor });
139
+ const researcher = new ResearchAgent({ brain, logger });
140
140
  let researchContext = "";
141
141
  try {
142
142
  researchContext = await researcher.buildFixContext(parsed.errorMessage);
@@ -154,7 +154,8 @@ class DashboardServer {
154
154
  return;
155
155
  }
156
156
 
157
- const safeCommand = this.redactor ? this.redactor.redact(command) : command;
157
+ const { redact } = require("../security/secret-redactor");
158
+ const safeCommand = redact(command);
158
159
 
159
160
  if (this._isSecretExtractionRequest(command)) {
160
161
  const refusal = this._buildSecretRefusal();
@@ -763,7 +764,7 @@ ${context ? "\nBrain:\n" + context : ""}`,
763
764
  req.on("error", (e) => resolve(`Error: ${e.message}`));
764
765
  req.on("timeout", () => { req.destroy(); resolve("Timeout"); });
765
766
  });
766
- toolResult = this.redactor ? this.redactor.redact(toolResult) : toolResult;
767
+ const { redact: _r } = require("../security/secret-redactor"); toolResult = _r(toolResult);
767
768
  console.log(chalk.green(` 🌐 → ${toolResult.slice(0, 80)}`));
768
769
  } catch (e) { toolResult = `Error: ${e.message}`; }
769
770
  } else if (tc.function.name === "read_file") {
@@ -1029,6 +1030,7 @@ main{overflow-y:auto;padding:24px}
1029
1030
  <a data-panel="events">📋 Events</a>
1030
1031
  <a data-panel="perf">⚡ Performance</a>
1031
1032
  <a data-panel="analytics">📊 Analytics</a>
1033
+ <a data-panel="processes">⚙️ Processes</a>
1032
1034
  <div class="sep"></div>
1033
1035
  <div class="label">Agent</div>
1034
1036
  <a data-panel="command">💬 Command</a>
@@ -1059,6 +1061,32 @@ main{overflow-y:auto;padding:24px}
1059
1061
  </div>
1060
1062
  <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
1063
  <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>
1064
+ <div class="panel" id="p-processes">
1065
+ <div class="stats" style="grid-template-columns:repeat(4,1fr)">
1066
+ <div class="stat-card heal"><div class="stat-val" id="proc-pid">-</div><div class="stat-lbl">Server PID</div></div>
1067
+ <div class="stat-card up"><div class="stat-val" id="proc-up">-</div><div class="stat-lbl">Uptime</div></div>
1068
+ <div class="stat-card err"><div class="stat-val" id="proc-mem">-</div><div class="stat-lbl">Memory (RSS)</div></div>
1069
+ <div class="stat-card brain"><div class="stat-val" id="proc-cpu">-</div><div class="stat-lbl">CPU %</div></div>
1070
+ </div>
1071
+ <div class="row2">
1072
+ <div class="card">
1073
+ <h3>System</h3>
1074
+ <div id="proc-sys"><div class="empty">Loading...</div></div>
1075
+ </div>
1076
+ <div class="card">
1077
+ <h3>Process Health</h3>
1078
+ <div id="proc-health"><div class="empty">Loading...</div></div>
1079
+ </div>
1080
+ </div>
1081
+ <div class="card">
1082
+ <h3>Memory Timeline</h3>
1083
+ <div id="proc-mem-chart" style="height:160px"></div>
1084
+ </div>
1085
+ <div class="card">
1086
+ <h3>Active Connections & Listeners</h3>
1087
+ <div id="proc-listen"><div class="empty">Loading...</div></div>
1088
+ </div>
1089
+ </div>
1062
1090
  <div class="panel" id="p-analytics">
1063
1091
  <div class="stats" style="grid-template-columns:repeat(4,1fr)">
1064
1092
  <div class="stat-card heal"><div class="stat-val" id="a-mem">-</div><div class="stat-lbl">Memory (RSS)</div></div>
@@ -1280,6 +1308,59 @@ async function refresh(){
1280
1308
  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
1309
  }).join('');
1282
1310
  }
1311
+ // Processes panel
1312
+ if(proc){
1313
+ $('proc-pid').textContent=proc.pid||'-';
1314
+ if(proc.current){
1315
+ $('proc-mem').textContent=proc.current.rss+'MB';
1316
+ $('proc-cpu').textContent=proc.current.cpu+'%';
1317
+ }
1318
+ // Uptime from server stats
1319
+ if(sr.session){
1320
+ const u=Math.round((sr.session.uptime||0)/1000);
1321
+ const h=Math.floor(u/3600),m=Math.floor((u%3600)/60),s=u%60;
1322
+ $('proc-up').textContent=(h>0?h+'h ':'')+(m>0?m+'m ':'')+s+'s';
1323
+ }
1324
+ // Leak detection
1325
+ if(proc.leakDetection){
1326
+ const ld=proc.leakDetection;
1327
+ $('proc-health').innerHTML='<div class="mrow"><span>Leak Detection</span><span class="vals">'+(ld.warning?'<span style="color:var(--yellow)">⚠️ Growing</span>':'<span style="color:var(--green)">✅ Stable</span>')+' ('+ld.consecutiveGrowth+'/'+ld.threshold+' samples)</span></div>'
1328
+ +'<div class="mrow"><span>Peak Memory</span><span class="vals"><b>'+(proc.peak?.memory||0)+'MB</b></span></div>'
1329
+ +'<div class="mrow"><span>Avg Memory</span><span class="vals"><b>'+(proc.average?.memory||0)+'MB</b></span></div>'
1330
+ +'<div class="mrow"><span>Avg CPU</span><span class="vals"><b>'+(proc.average?.cpu||0)+'%</b></span></div>'
1331
+ +'<div class="mrow"><span>Process Alive</span><span class="vals">'+(proc.alive?'<span style="color:var(--green)">✅ Yes</span>':'<span style="color:var(--red)">❌ No</span>')+'</span></div>';
1332
+ }
1333
+ // Memory timeline chart
1334
+ if(proc.samples&&proc.samples.length>1){
1335
+ const s=proc.samples,max=Math.max(...s.map(e=>e.rss))||1,w=$('proc-mem-chart').offsetWidth||500,h=150;
1336
+ const bw=Math.max(3,Math.floor(w/s.length)-1);
1337
+ let svg='<svg width="'+w+'" height="'+h+'">';
1338
+ s.forEach((e,i)=>{
1339
+ const bh=Math.max(2,Math.round((e.rss/max)*h*0.85));
1340
+ const c=e.rss>400?'var(--red)':e.rss>200?'var(--yellow)':'var(--blue)';
1341
+ svg+='<rect x="'+(i*(bw+1))+'" y="'+(h-bh)+'" width="'+bw+'" height="'+bh+'" fill="'+c+'" rx="1"><title>'+e.rss+'MB</title></rect>';
1342
+ });
1343
+ svg+='</svg>';
1344
+ $('proc-mem-chart').innerHTML=svg;
1345
+ }
1346
+ }
1347
+ // System info
1348
+ const sysInfo=await fetch(B+'/api/system').then(r=>r.json()).catch(()=>({}));
1349
+ if(sysInfo&&sysInfo.cpu){
1350
+ $('proc-sys').innerHTML=[
1351
+ '<div class="mrow"><span>Platform</span><span class="vals">'+esc(sysInfo.platform)+'/'+esc(sysInfo.arch)+'</span></div>',
1352
+ '<div class="mrow"><span>CPU</span><span class="vals">'+esc((sysInfo.cpu.model||'').slice(0,40))+' ('+sysInfo.cpu.cores+' cores)</span></div>',
1353
+ '<div class="mrow"><span>RAM</span><span class="vals">'+sysInfo.memory.totalGB+'GB total, '+sysInfo.memory.freeGB+'GB free ('+sysInfo.memory.usedPercent+'%)</span></div>',
1354
+ '<div class="mrow"><span>Disk</span><span class="vals">'+sysInfo.disk.totalGB+'GB total, '+sysInfo.disk.freeGB+'GB free ('+sysInfo.disk.usedPercent+'%)</span></div>',
1355
+ '<div class="mrow"><span>Node</span><span class="vals">'+esc(sysInfo.nodeVersion)+'</span></div>',
1356
+ '<div class="mrow"><span>Hostname</span><span class="vals">'+esc(sysInfo.hostname)+'</span></div>',
1357
+ '<div class="mrow"><span>Environment</span><span class="vals">'+esc(sysInfo.environment?.type||'unknown')+(sysInfo.environment?.cloud?' ('+sysInfo.environment.cloud+')':'')+'</span></div>',
1358
+ ].join('');
1359
+ // Listening ports
1360
+ $('proc-listen').innerHTML='<div class="mrow"><span>Server</span><span class="vals">:'+(sysInfo.hostname?'running':'?')+'</span></div>'
1361
+ +'<div class="mrow"><span>Dashboard</span><span class="vals">:'+P+'</span></div>'
1362
+ +'<div class="mrow"><span>Workers Recommended</span><span class="vals">'+sysInfo.recommended?.workers+'</span></div>';
1363
+ }
1283
1364
  // Analytics: process + routes
1284
1365
  if(proc&&proc.current){
1285
1366
  $('a-mem').textContent=proc.current.rss+'MB';
package/src/index.js CHANGED
@@ -11,7 +11,7 @@ const { HealthMonitor } = require("./core/health-monitor");
11
11
  const { Sandbox, SandboxViolationError } = require("./security/sandbox");
12
12
  const { RateLimiter } = require("./security/rate-limiter");
13
13
  const { detectInjection } = require("./security/injection-detector");
14
- const { SecretRedactor } = require("./security/secret-redactor");
14
+ const { SecretRedactor, initRedactor, redact, redactObj, hasSecrets } = require("./security/secret-redactor");
15
15
  const { AdminAuth } = require("./security/admin-auth");
16
16
  const { BackupManager } = require("./backup/backup-manager");
17
17
  const { EventLogger, EVENT_TYPES, SEVERITY } = require("./logger/event-logger");
@@ -91,9 +91,6 @@ class EventLogger extends EventEmitter {
91
91
  this._recentEvents = [];
92
92
  this._maxRecent = 1000;
93
93
 
94
- // Secret redactor — if set, all events get redacted before persist/emit
95
- this.redactor = null;
96
-
97
94
  // Session tracking
98
95
  this.sessionId = Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
99
96
  this.sessionStart = Date.now();
@@ -101,11 +98,9 @@ class EventLogger extends EventEmitter {
101
98
  }
102
99
 
103
100
  /**
104
- * Attach a SecretRedactor. All events will be redacted before storage/emit.
101
+ * @deprecated Use singleton redact() kept for backwards compat
105
102
  */
106
- setRedactor(redactor) {
107
- this.redactor = redactor;
108
- }
103
+ setRedactor() {}
109
104
 
110
105
  /**
111
106
  * Log an event. This is the primary API.
@@ -117,8 +112,10 @@ class EventLogger extends EventEmitter {
117
112
  */
118
113
  log(type, severity, message, data = {}) {
119
114
  // Redact secrets before they hit storage or the wire
120
- const safeMessage = this.redactor ? this.redactor.redact(message) : message;
121
- const safeData = this.redactor ? this.redactor.redactObject(data) : data;
115
+ // Universal redaction via singleton no instance needed
116
+ const { redact, redactObj } = require("../security/secret-redactor");
117
+ const safeMessage = redact(message);
118
+ const safeData = redactObj(data);
122
119
 
123
120
  const event = {
124
121
  id: `${this.sessionId}-${(++this._eventCount).toString(36)}`,
@@ -37,14 +37,15 @@ class RepairHistory {
37
37
  duration, // ms from crash to fix
38
38
  filesModified, // files changed
39
39
  }) {
40
+ const { redact } = require("../security/secret-redactor");
40
41
  const entry = {
41
42
  id: Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 4),
42
43
  timestamp: Date.now(),
43
44
  iso: new Date().toISOString(),
44
- error: (error || "").slice(0, 200),
45
+ error: redact((error || "").slice(0, 200)),
45
46
  file,
46
47
  line,
47
- resolution: (resolution || "").slice(0, 300),
48
+ resolution: redact((resolution || "").slice(0, 300)),
48
49
  success,
49
50
  mode,
50
51
  model,
@@ -13,7 +13,7 @@ let _v = null;
13
13
  function collectHeartbeat(subsystems) {
14
14
  if (!_v) { try { _v = require("../../package.json").version; } catch { _v = "0.0.0"; } }
15
15
 
16
- const { processMonitor, routeProber, tokenTracker, repairHistory, backupManager, brain, redactor } = subsystems;
16
+ const { processMonitor, routeProber, tokenTracker, repairHistory, backupManager, brain } = subsystems;
17
17
  const proc = processMonitor?.getMetrics();
18
18
  const usage = tokenTracker?.getAnalytics();
19
19
  const repairs = repairHistory?.getStats();
@@ -52,16 +52,18 @@ function collectHeartbeat(subsystems) {
52
52
  totalCost: usage?.session?.totalCostUsd || 0,
53
53
  totalCalls: usage?.session?.totalCalls || 0,
54
54
  byCategory: usage?.byCategory || {},
55
+ byModel: usage?.byModel || {},
56
+ byTool: usage?.byTool || {},
55
57
  },
56
58
 
57
59
  brain: { totalMemories: brain?.getStats()?.totalEntries || 0 },
58
60
  backups: backupManager?.getStats() || { total: 0, stable: 0 },
59
61
  };
60
62
 
61
- if (redactor && repairs?.lastRepair) {
63
+ if (repairs?.lastRepair) {
62
64
  payload.repairs.lastRepair = {
63
- error: redactor.redact((repairs.lastRepair?.error || "").slice(0, 150)),
64
- resolution: redactor.redact((repairs.lastRepair?.resolution || "").slice(0, 150)),
65
+ error: (repairs.lastRepair?.error || "").slice(0, 150),
66
+ resolution: (repairs.lastRepair?.resolution || "").slice(0, 150),
65
67
  tokens: repairs.lastRepair?.tokens || 0,
66
68
  cost: repairs.lastRepair?.cost || 0,
67
69
  mode: repairs.lastRepair?.mode || "",
@@ -70,7 +72,9 @@ function collectHeartbeat(subsystems) {
70
72
  };
71
73
  }
72
74
 
73
- return payload;
75
+ // Pre-flight security: redact entire payload before it leaves the process
76
+ const { redactObj } = require("../security/secret-redactor");
77
+ return redactObj(payload);
74
78
  }
75
79
 
76
80
  module.exports = { collectHeartbeat, INSTANCE_ID };
@@ -214,4 +214,50 @@ class SecretRedactor {
214
214
  }
215
215
  }
216
216
 
217
- module.exports = { SecretRedactor };
217
+ // ── Singleton ──
218
+ // One instance, initialized once, used everywhere.
219
+ // Import { redact, redactObj } from anywhere — no need to pass the redactor around.
220
+
221
+ let _instance = null;
222
+
223
+ /**
224
+ * Initialize the singleton. Call once on startup from runner.
225
+ */
226
+ function initRedactor(projectRoot) {
227
+ _instance = new SecretRedactor(projectRoot);
228
+ return _instance;
229
+ }
230
+
231
+ /**
232
+ * Get the singleton instance.
233
+ */
234
+ function getRedactor() {
235
+ return _instance;
236
+ }
237
+
238
+ /**
239
+ * Redact a string. Safe to call even if not initialized (returns input unchanged).
240
+ * This is the universal function — use it everywhere instead of this.redactor?.redact().
241
+ */
242
+ function redact(text) {
243
+ if (!_instance || !text || typeof text !== "string") return text;
244
+ return _instance.redact(text);
245
+ }
246
+
247
+ /**
248
+ * Redact all string values in an object (deep). Safe to call if not initialized.
249
+ */
250
+ function redactObj(obj) {
251
+ if (!_instance) return obj;
252
+ return _instance.redactObject(obj);
253
+ }
254
+
255
+ /**
256
+ * Check if a string contains secrets.
257
+ */
258
+ function hasSecrets(text) {
259
+ if (!_instance || !text) return false;
260
+ return _instance.containsSecrets(text);
261
+ }
262
+
263
+ module.exports = { SecretRedactor, initRedactor, getRedactor, redact, redactObj, hasSecrets };