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.
- package/PLATFORM.md +442 -0
- package/README.md +475 -0
- package/SERVER_BEST_PRACTICES.md +62 -0
- package/TELEMETRY.md +108 -0
- package/bin/wolverine.js +95 -0
- package/examples/01-basic-typo.js +31 -0
- package/examples/02-multi-file/routes/users.js +15 -0
- package/examples/02-multi-file/server.js +25 -0
- package/examples/03-syntax-error.js +23 -0
- package/examples/04-secret-leak.js +14 -0
- package/examples/05-expired-key.js +27 -0
- package/examples/06-json-config/config.json +13 -0
- package/examples/06-json-config/server.js +28 -0
- package/examples/07-rate-limit-loop.js +11 -0
- package/examples/08-sandbox-escape.js +20 -0
- package/examples/buggy-server.js +39 -0
- package/examples/demos/01-basic-typo/index.js +20 -0
- package/examples/demos/01-basic-typo/routes/api.js +13 -0
- package/examples/demos/01-basic-typo/routes/health.js +4 -0
- package/examples/demos/02-multi-file/index.js +24 -0
- package/examples/demos/02-multi-file/routes/api.js +13 -0
- package/examples/demos/02-multi-file/routes/health.js +4 -0
- package/examples/demos/03-syntax-error/index.js +18 -0
- package/examples/demos/04-secret-leak/index.js +16 -0
- package/examples/demos/05-expired-key/index.js +21 -0
- package/examples/demos/06-json-config/config.json +9 -0
- package/examples/demos/06-json-config/index.js +20 -0
- package/examples/demos/07-null-crash/index.js +16 -0
- package/examples/run-demo.js +110 -0
- package/package.json +67 -0
- package/server/config/settings.json +62 -0
- package/server/index.js +33 -0
- package/server/routes/api.js +12 -0
- package/server/routes/health.js +16 -0
- package/server/routes/time.js +12 -0
- package/src/agent/agent-engine.js +727 -0
- package/src/agent/goal-loop.js +140 -0
- package/src/agent/research-agent.js +120 -0
- package/src/agent/sub-agents.js +176 -0
- package/src/backup/backup-manager.js +321 -0
- package/src/brain/brain.js +315 -0
- package/src/brain/embedder.js +131 -0
- package/src/brain/function-map.js +263 -0
- package/src/brain/vector-store.js +267 -0
- package/src/core/ai-client.js +387 -0
- package/src/core/cluster-manager.js +144 -0
- package/src/core/config.js +89 -0
- package/src/core/error-parser.js +87 -0
- package/src/core/health-monitor.js +129 -0
- package/src/core/models.js +132 -0
- package/src/core/patcher.js +55 -0
- package/src/core/runner.js +464 -0
- package/src/core/system-info.js +141 -0
- package/src/core/verifier.js +146 -0
- package/src/core/wolverine.js +290 -0
- package/src/dashboard/server.js +1332 -0
- package/src/index.js +94 -0
- package/src/logger/event-logger.js +237 -0
- package/src/logger/pricing.js +96 -0
- package/src/logger/repair-history.js +109 -0
- package/src/logger/token-tracker.js +277 -0
- package/src/mcp/mcp-client.js +224 -0
- package/src/mcp/mcp-registry.js +228 -0
- package/src/mcp/mcp-security.js +152 -0
- package/src/monitor/perf-monitor.js +300 -0
- package/src/monitor/process-monitor.js +231 -0
- package/src/monitor/route-prober.js +191 -0
- package/src/notifications/notifier.js +227 -0
- package/src/platform/heartbeat.js +93 -0
- package/src/platform/queue.js +53 -0
- package/src/platform/register.js +64 -0
- package/src/platform/telemetry.js +76 -0
- package/src/security/admin-auth.js +150 -0
- package/src/security/injection-detector.js +174 -0
- package/src/security/rate-limiter.js +152 -0
- package/src/security/sandbox.js +128 -0
- package/src/security/secret-redactor.js +217 -0
- package/src/skills/skill-registry.js +129 -0
- 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 };
|