wolverine-ai 1.1.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/README.md CHANGED
@@ -10,24 +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
+
13
15
  ```bash
14
- # Install from npm
15
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
+ ```
16
21
 
17
- # Or clone from GitHub
22
+ ### Or clone from GitHub
23
+
24
+ ```bash
18
25
  git clone https://github.com/bobbyswhip/Wolverine.git
19
26
  cd Wolverine
20
27
  npm install
21
-
22
- # Configure
23
28
  cp .env.example .env.local
24
- # Edit .env.local — add your OPENAI_API_KEY and generate an ADMIN_KEY
25
-
26
- # Run
29
+ # Edit .env.local — add your OPENAI_API_KEY
27
30
  npm start
28
- # or: npx wolverine server/index.js
29
31
  ```
30
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
+
31
36
  Dashboard opens at `http://localhost:PORT+1`. Server runs on `PORT`.
32
37
 
33
38
  ### Try a Demo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wolverine-ai",
3
- "version": "1.1.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": {
@@ -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
 
@@ -191,6 +191,10 @@ const SEED_DOCS = [
191
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
192
  metadata: { topic: "port-security" },
193
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
+ },
194
198
  ];
195
199
 
196
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,
@@ -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") {
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();
@@ -60,10 +60,10 @@ function collectHeartbeat(subsystems) {
60
60
  backups: backupManager?.getStats() || { total: 0, stable: 0 },
61
61
  };
62
62
 
63
- if (redactor && repairs?.lastRepair) {
63
+ if (repairs?.lastRepair) {
64
64
  payload.repairs.lastRepair = {
65
- error: redactor.redact((repairs.lastRepair?.error || "").slice(0, 150)),
66
- 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),
67
67
  tokens: repairs.lastRepair?.tokens || 0,
68
68
  cost: repairs.lastRepair?.cost || 0,
69
69
  mode: repairs.lastRepair?.mode || "",
@@ -72,7 +72,9 @@ function collectHeartbeat(subsystems) {
72
72
  };
73
73
  }
74
74
 
75
- 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);
76
78
  }
77
79
 
78
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 };