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
package/src/index.js ADDED
@@ -0,0 +1,94 @@
1
+ // Wolverine Node.js — Public API
2
+
3
+ const { heal } = require("./core/wolverine");
4
+ const { WolverineRunner } = require("./core/runner");
5
+ const { requestRepair } = require("./core/ai-client");
6
+ const { parseError } = require("./core/error-parser");
7
+ const { getModel, getModelConfig, MODEL_ROLES } = require("./core/models");
8
+ const { applyPatch } = require("./core/patcher");
9
+ const { verifyFix } = require("./core/verifier");
10
+ const { HealthMonitor } = require("./core/health-monitor");
11
+ const { Sandbox, SandboxViolationError } = require("./security/sandbox");
12
+ const { RateLimiter } = require("./security/rate-limiter");
13
+ const { detectInjection } = require("./security/injection-detector");
14
+ const { SecretRedactor } = require("./security/secret-redactor");
15
+ const { AdminAuth } = require("./security/admin-auth");
16
+ const { BackupManager } = require("./backup/backup-manager");
17
+ const { EventLogger, EVENT_TYPES, SEVERITY } = require("./logger/event-logger");
18
+ const { TokenTracker } = require("./logger/token-tracker");
19
+ const { AgentEngine } = require("./agent/agent-engine");
20
+ const { ResearchAgent } = require("./agent/research-agent");
21
+ const { GoalLoop } = require("./agent/goal-loop");
22
+ const { spawnAgent, spawnParallel, exploreAndFix } = require("./agent/sub-agents");
23
+ const { McpRegistry } = require("./mcp/mcp-registry");
24
+ const { McpSecurity } = require("./mcp/mcp-security");
25
+ const { PerfMonitor } = require("./monitor/perf-monitor");
26
+ const { DashboardServer } = require("./dashboard/server");
27
+ const { Notifier } = require("./notifications/notifier");
28
+ const { Brain } = require("./brain/brain");
29
+ const { VectorStore } = require("./brain/vector-store");
30
+ const { embed, embedBatch, compact } = require("./brain/embedder");
31
+ const { scanProject } = require("./brain/function-map");
32
+ const { detect: detectSystem } = require("./core/system-info");
33
+ const { ClusterManager } = require("./core/cluster-manager");
34
+ const { loadConfig, getConfig } = require("./core/config");
35
+ const { sqlGuard, SafeDB, scanForInjection } = require("./skills/sql");
36
+
37
+ module.exports = {
38
+ // Core
39
+ heal,
40
+ WolverineRunner,
41
+ requestRepair,
42
+ parseError,
43
+ applyPatch,
44
+ verifyFix,
45
+ HealthMonitor,
46
+ // Models
47
+ getModel,
48
+ getModelConfig,
49
+ MODEL_ROLES,
50
+ // Security
51
+ Sandbox,
52
+ SandboxViolationError,
53
+ RateLimiter,
54
+ detectInjection,
55
+ SecretRedactor,
56
+ AdminAuth,
57
+ // Backup
58
+ BackupManager,
59
+ // Logger
60
+ EventLogger,
61
+ EVENT_TYPES,
62
+ SEVERITY,
63
+ TokenTracker,
64
+ // Agent
65
+ AgentEngine,
66
+ ResearchAgent,
67
+ GoalLoop,
68
+ spawnAgent,
69
+ spawnParallel,
70
+ exploreAndFix,
71
+ McpRegistry,
72
+ McpSecurity,
73
+ // Monitor
74
+ PerfMonitor,
75
+ // Dashboard
76
+ DashboardServer,
77
+ // Notifications
78
+ Notifier,
79
+ // Brain
80
+ Brain,
81
+ VectorStore,
82
+ embed,
83
+ embedBatch,
84
+ compact,
85
+ scanProject,
86
+ detectSystem,
87
+ ClusterManager,
88
+ loadConfig,
89
+ getConfig,
90
+ // Skills
91
+ sqlGuard,
92
+ SafeDB,
93
+ scanForInjection,
94
+ };
@@ -0,0 +1,237 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { EventEmitter } = require("events");
4
+
5
+ /**
6
+ * Event Logger — central event bus and persistence layer for all wolverine activity.
7
+ *
8
+ * Every action wolverine takes is logged as a structured event:
9
+ * - Crashes, heals, rollbacks, verification results
10
+ * - Performance metrics, health checks
11
+ * - Agent actions (multi-file analysis, research, optimization)
12
+ * - Security events (injection attempts, sandbox violations)
13
+ *
14
+ * Events are:
15
+ * 1. Emitted live via EventEmitter (for dashboard SSE streaming)
16
+ * 2. Persisted to .wolverine/events/ as daily JSON files
17
+ * 3. Queryable by type, time range, severity
18
+ */
19
+
20
+ const EVENT_TYPES = {
21
+ // Process lifecycle
22
+ PROCESS_START: "process.start",
23
+ PROCESS_CRASH: "process.crash",
24
+ PROCESS_HEALTHY: "process.healthy",
25
+ PROCESS_STOP: "process.stop",
26
+
27
+ // Healing pipeline
28
+ HEAL_START: "heal.start",
29
+ HEAL_PARSE: "heal.parse",
30
+ HEAL_INJECTION_SCAN: "heal.injection_scan",
31
+ HEAL_AI_REQUEST: "heal.ai_request",
32
+ HEAL_AI_RESPONSE: "heal.ai_response",
33
+ HEAL_PATCH_APPLIED: "heal.patch_applied",
34
+ HEAL_PATCH_FAILED: "heal.patch_failed",
35
+ HEAL_VERIFIED: "heal.verified",
36
+ HEAL_VERIFICATION_FAILED: "heal.verification_failed",
37
+ HEAL_ROLLBACK: "heal.rollback",
38
+ HEAL_SUCCESS: "heal.success",
39
+ HEAL_FAILED: "heal.failed",
40
+
41
+ // Agent activity
42
+ AGENT_TURN: "agent.turn",
43
+ AGENT_FILE_READ: "agent.file_read",
44
+ AGENT_FILE_WRITE: "agent.file_write",
45
+ AGENT_RESEARCH: "agent.research",
46
+ AGENT_COMPLETE: "agent.complete",
47
+
48
+ // Security
49
+ SECURITY_INJECTION_DETECTED: "security.injection_detected",
50
+ SECURITY_SANDBOX_VIOLATION: "security.sandbox_violation",
51
+ SECURITY_RATE_LIMITED: "security.rate_limited",
52
+
53
+ // Backup
54
+ BACKUP_CREATED: "backup.created",
55
+ BACKUP_VERIFIED: "backup.verified",
56
+ BACKUP_STABLE: "backup.stable",
57
+ BACKUP_ROLLBACK: "backup.rollback",
58
+ BACKUP_PRUNED: "backup.pruned",
59
+
60
+ // Performance monitoring
61
+ PERF_SLOW_ENDPOINT: "perf.slow_endpoint",
62
+ PERF_SPIKE_DETECTED: "perf.spike_detected",
63
+ PERF_ATTACK_DETECTED: "perf.attack_detected",
64
+ PERF_OPTIMIZATION: "perf.optimization",
65
+
66
+ // Notifications
67
+ NOTIFY_HUMAN_REQUIRED: "notify.human_required",
68
+
69
+ // Health checks
70
+ HEALTH_PASS: "health.pass",
71
+ HEALTH_FAIL: "health.fail",
72
+ HEALTH_UNRESPONSIVE: "health.unresponsive",
73
+ };
74
+
75
+ const SEVERITY = {
76
+ DEBUG: "debug",
77
+ INFO: "info",
78
+ WARN: "warn",
79
+ ERROR: "error",
80
+ CRITICAL: "critical",
81
+ };
82
+
83
+ class EventLogger extends EventEmitter {
84
+ constructor(projectRoot) {
85
+ super();
86
+ this.projectRoot = path.resolve(projectRoot);
87
+ this.eventsDir = path.join(this.projectRoot, ".wolverine", "events");
88
+ this._ensureDir();
89
+
90
+ // In-memory ring buffer for recent events (dashboard queries)
91
+ this._recentEvents = [];
92
+ this._maxRecent = 1000;
93
+
94
+ // Secret redactor — if set, all events get redacted before persist/emit
95
+ this.redactor = null;
96
+
97
+ // Session tracking
98
+ this.sessionId = Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 6);
99
+ this.sessionStart = Date.now();
100
+ this._eventCount = 0;
101
+ }
102
+
103
+ /**
104
+ * Attach a SecretRedactor. All events will be redacted before storage/emit.
105
+ */
106
+ setRedactor(redactor) {
107
+ this.redactor = redactor;
108
+ }
109
+
110
+ /**
111
+ * Log an event. This is the primary API.
112
+ *
113
+ * @param {string} type - One of EVENT_TYPES
114
+ * @param {string} severity - One of SEVERITY
115
+ * @param {string} message - Human-readable description
116
+ * @param {object} data - Structured metadata
117
+ */
118
+ log(type, severity, message, data = {}) {
119
+ // 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;
122
+
123
+ const event = {
124
+ id: `${this.sessionId}-${(++this._eventCount).toString(36)}`,
125
+ type,
126
+ severity,
127
+ message: safeMessage,
128
+ data: safeData,
129
+ timestamp: Date.now(),
130
+ iso: new Date().toISOString(),
131
+ sessionId: this.sessionId,
132
+ };
133
+
134
+ // In-memory ring buffer
135
+ this._recentEvents.push(event);
136
+ if (this._recentEvents.length > this._maxRecent) {
137
+ this._recentEvents.shift();
138
+ }
139
+
140
+ // Persist to daily file
141
+ this._persist(event);
142
+
143
+ // Emit for live streaming (dashboard SSE)
144
+ this.emit("event", event);
145
+
146
+ return event;
147
+ }
148
+
149
+ // Convenience methods
150
+ info(type, message, data) { return this.log(type, SEVERITY.INFO, message, data); }
151
+ warn(type, message, data) { return this.log(type, SEVERITY.WARN, message, data); }
152
+ error(type, message, data) { return this.log(type, SEVERITY.ERROR, message, data); }
153
+ critical(type, message, data) { return this.log(type, SEVERITY.CRITICAL, message, data); }
154
+ debug(type, message, data) { return this.log(type, SEVERITY.DEBUG, message, data); }
155
+
156
+ /**
157
+ * Query recent events by type and/or severity.
158
+ */
159
+ query({ type, severity, limit = 100, since } = {}) {
160
+ let results = this._recentEvents;
161
+
162
+ if (type) {
163
+ results = results.filter(e => e.type === type || e.type.startsWith(type + "."));
164
+ }
165
+ if (severity) {
166
+ results = results.filter(e => e.severity === severity);
167
+ }
168
+ if (since) {
169
+ results = results.filter(e => e.timestamp >= since);
170
+ }
171
+
172
+ return results.slice(-limit);
173
+ }
174
+
175
+ /**
176
+ * Get summary stats for the current session.
177
+ */
178
+ getSessionStats() {
179
+ const counts = {};
180
+ for (const event of this._recentEvents) {
181
+ const category = event.type.split(".")[0];
182
+ counts[category] = (counts[category] || 0) + 1;
183
+ }
184
+
185
+ const errors = this._recentEvents.filter(e => e.severity === SEVERITY.ERROR || e.severity === SEVERITY.CRITICAL);
186
+ const heals = this._recentEvents.filter(e => e.type === EVENT_TYPES.HEAL_SUCCESS);
187
+ const rollbacks = this._recentEvents.filter(e => e.type === EVENT_TYPES.HEAL_ROLLBACK);
188
+
189
+ return {
190
+ sessionId: this.sessionId,
191
+ uptime: Date.now() - this.sessionStart,
192
+ totalEvents: this._eventCount,
193
+ categories: counts,
194
+ errors: errors.length,
195
+ heals: heals.length,
196
+ rollbacks: rollbacks.length,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Load events from a specific date's log file.
202
+ */
203
+ loadDay(dateStr) {
204
+ const filePath = path.join(this.eventsDir, `${dateStr}.jsonl`);
205
+ if (!fs.existsSync(filePath)) return [];
206
+
207
+ const lines = fs.readFileSync(filePath, "utf-8").trim().split("\n");
208
+ return lines.filter(Boolean).map(line => {
209
+ try { return JSON.parse(line); } catch { return null; }
210
+ }).filter(Boolean);
211
+ }
212
+
213
+ /**
214
+ * Get all available log dates.
215
+ */
216
+ getAvailableDates() {
217
+ if (!fs.existsSync(this.eventsDir)) return [];
218
+ return fs.readdirSync(this.eventsDir)
219
+ .filter(f => f.endsWith(".jsonl"))
220
+ .map(f => f.replace(".jsonl", ""))
221
+ .sort();
222
+ }
223
+
224
+ // -- Private --
225
+
226
+ _ensureDir() {
227
+ fs.mkdirSync(this.eventsDir, { recursive: true });
228
+ }
229
+
230
+ _persist(event) {
231
+ const dateStr = new Date(event.timestamp).toISOString().slice(0, 10);
232
+ const filePath = path.join(this.eventsDir, `${dateStr}.jsonl`);
233
+ fs.appendFileSync(filePath, JSON.stringify(event) + "\n", "utf-8");
234
+ }
235
+ }
236
+
237
+ module.exports = { EventLogger, EVENT_TYPES, SEVERITY };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Model Pricing — maps model names to per-million-token costs.
3
+ *
4
+ * Users can override in .wolverine/pricing.json. Defaults based on
5
+ * OpenAI published pricing as of April 2026.
6
+ *
7
+ * All values are USD per 1 million tokens.
8
+ */
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+
13
+ const DEFAULT_PRICING = {
14
+ // GPT-5.4 family
15
+ "gpt-5.4": { input: 2.50, output: 15.00 },
16
+ "gpt-5.4-mini": { input: 0.75, output: 4.50 },
17
+ "gpt-5.4-nano": { input: 0.20, output: 1.25 },
18
+
19
+ // GPT-5 family (estimated from 5.4 pricing)
20
+ "gpt-5-nano": { input: 0.15, output: 1.00 },
21
+
22
+ // GPT-4o family
23
+ "gpt-4o": { input: 2.50, output: 10.00 },
24
+ "gpt-4o-mini": { input: 0.15, output: 0.60 },
25
+
26
+ // O-series reasoning
27
+ "o4-mini": { input: 1.10, output: 4.40 },
28
+ "o4-mini-deep-research": { input: 2.00, output: 8.00 },
29
+
30
+ // Codex
31
+ "gpt-5.1-codex-mini": { input: 1.50, output: 6.00 },
32
+ "codex-mini-latest": { input: 1.50, output: 6.00 },
33
+ "gpt-5.3-codex": { input: 2.50, output: 10.00 },
34
+
35
+ // Embeddings
36
+ "text-embedding-3-small": { input: 0.02, output: 0.00 },
37
+ "text-embedding-3-large": { input: 0.13, output: 0.00 },
38
+
39
+ // Fallback for unknown models
40
+ "_default": { input: 1.00, output: 4.00 },
41
+ };
42
+
43
+ let _customPricing = null;
44
+
45
+ /**
46
+ * Get pricing for a model. Checks custom overrides first, then defaults.
47
+ * Returns { input, output } in USD per million tokens.
48
+ */
49
+ function getModelPricing(modelName) {
50
+ // Check custom pricing
51
+ if (_customPricing && _customPricing[modelName]) {
52
+ return _customPricing[modelName];
53
+ }
54
+
55
+ // Check defaults — try exact match, then prefix match
56
+ if (DEFAULT_PRICING[modelName]) {
57
+ return DEFAULT_PRICING[modelName];
58
+ }
59
+
60
+ // Prefix matching: "gpt-5.4-mini-2026-03" → "gpt-5.4-mini"
61
+ for (const [key, val] of Object.entries(DEFAULT_PRICING)) {
62
+ if (key !== "_default" && modelName.startsWith(key)) {
63
+ return val;
64
+ }
65
+ }
66
+
67
+ return DEFAULT_PRICING._default;
68
+ }
69
+
70
+ /**
71
+ * Calculate cost in USD for a given model and token counts.
72
+ */
73
+ function calculateCost(modelName, inputTokens, outputTokens) {
74
+ const pricing = getModelPricing(modelName);
75
+ const inputCost = (inputTokens / 1_000_000) * pricing.input;
76
+ const outputCost = (outputTokens / 1_000_000) * pricing.output;
77
+ return {
78
+ input: inputCost,
79
+ output: outputCost,
80
+ total: inputCost + outputCost,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Load custom pricing overrides from .wolverine/pricing.json.
86
+ */
87
+ function loadCustomPricing(projectRoot) {
88
+ const pricingPath = path.join(projectRoot, ".wolverine", "pricing.json");
89
+ if (fs.existsSync(pricingPath)) {
90
+ try {
91
+ _customPricing = JSON.parse(fs.readFileSync(pricingPath, "utf-8"));
92
+ } catch {}
93
+ }
94
+ }
95
+
96
+ module.exports = { getModelPricing, calculateCost, loadCustomPricing, DEFAULT_PRICING };
@@ -0,0 +1,109 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Repair History — dedicated log of every error + resolution + cost.
6
+ *
7
+ * Separate from the general event log — this is a structured repair audit trail.
8
+ * Each entry tracks: what broke, what fixed it, how many tokens it cost, which model.
9
+ *
10
+ * Persisted to .wolverine/repair-history.json
11
+ */
12
+
13
+ const HISTORY_FILE = ".wolverine/repair-history.json";
14
+
15
+ class RepairHistory {
16
+ constructor(projectRoot) {
17
+ this.projectRoot = path.resolve(projectRoot);
18
+ this.historyPath = path.join(this.projectRoot, HISTORY_FILE);
19
+ this._repairs = [];
20
+ this._load();
21
+ }
22
+
23
+ /**
24
+ * Record a repair attempt.
25
+ */
26
+ record({
27
+ error, // error message
28
+ file, // file that crashed
29
+ line, // line number
30
+ resolution, // what was done to fix it
31
+ success, // boolean
32
+ mode, // "fast" | "agent" | "research"
33
+ model, // which model fixed it
34
+ tokens, // total tokens used
35
+ cost, // USD cost
36
+ iteration, // which goal loop iteration
37
+ duration, // ms from crash to fix
38
+ filesModified, // files changed
39
+ }) {
40
+ const entry = {
41
+ id: Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 4),
42
+ timestamp: Date.now(),
43
+ iso: new Date().toISOString(),
44
+ error: (error || "").slice(0, 200),
45
+ file,
46
+ line,
47
+ resolution: (resolution || "").slice(0, 300),
48
+ success,
49
+ mode,
50
+ model,
51
+ tokens: tokens || 0,
52
+ cost: Math.round((cost || 0) * 1000000) / 1000000,
53
+ iteration: iteration || 1,
54
+ duration: duration || 0,
55
+ filesModified: filesModified || [],
56
+ };
57
+
58
+ this._repairs.push(entry);
59
+ this._save();
60
+ return entry;
61
+ }
62
+
63
+ /**
64
+ * Get all repair entries.
65
+ */
66
+ getAll() {
67
+ return this._repairs;
68
+ }
69
+
70
+ /**
71
+ * Get summary stats.
72
+ */
73
+ getStats() {
74
+ const total = this._repairs.length;
75
+ const successes = this._repairs.filter(r => r.success).length;
76
+ const failures = total - successes;
77
+ const totalTokens = this._repairs.reduce((sum, r) => sum + (r.tokens || 0), 0);
78
+ const totalCost = this._repairs.reduce((sum, r) => sum + (r.cost || 0), 0);
79
+ const avgTokensPerRepair = total > 0 ? Math.round(totalTokens / total) : 0;
80
+
81
+ return {
82
+ total,
83
+ successes,
84
+ failures,
85
+ successRate: total > 0 ? Math.round((successes / total) * 100) : 0,
86
+ totalTokens,
87
+ totalCost: Math.round(totalCost * 10000) / 10000,
88
+ avgTokensPerRepair,
89
+ };
90
+ }
91
+
92
+ _load() {
93
+ if (!fs.existsSync(this.historyPath)) return;
94
+ try {
95
+ this._repairs = JSON.parse(fs.readFileSync(this.historyPath, "utf-8"));
96
+ } catch { this._repairs = []; }
97
+ }
98
+
99
+ _save() {
100
+ try {
101
+ fs.mkdirSync(path.dirname(this.historyPath), { recursive: true });
102
+ const tmp = this.historyPath + ".tmp";
103
+ fs.writeFileSync(tmp, JSON.stringify(this._repairs, null, 2), "utf-8");
104
+ fs.renameSync(tmp, this.historyPath);
105
+ } catch {}
106
+ }
107
+ }
108
+
109
+ module.exports = { RepairHistory };