wolverine-ai 1.7.1 → 2.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/README.md +42 -15
- package/package.json +2 -1
- package/src/agent/agent-engine.js +80 -1
- package/src/agent/sub-agents.js +3 -3
- package/src/brain/brain.js +6 -2
- package/src/brain/embedder.js +5 -3
- package/src/core/ai-client.js +243 -212
- package/src/core/models.js +16 -9
- package/src/core/wolverine.js +11 -24
- package/src/index.js +4 -0
- package/src/logger/pricing.js +10 -0
- package/src/platform/telemetry.js +20 -0
- package/src/skills/deps.js +449 -0
package/src/index.js
CHANGED
|
@@ -34,6 +34,7 @@ const { detect: detectSystem } = require("./core/system-info");
|
|
|
34
34
|
const { ClusterManager } = require("./core/cluster-manager");
|
|
35
35
|
const { loadConfig, getConfig } = require("./core/config");
|
|
36
36
|
const { sqlGuard, SafeDB, scanForInjection, idempotencyGuard, idempotencyAfterHook } = require("./skills/sql");
|
|
37
|
+
const { diagnose: diagnoseDeps, healthReport: depsHealthReport, getMigration } = require("./skills/deps");
|
|
37
38
|
|
|
38
39
|
module.exports = {
|
|
39
40
|
// Core
|
|
@@ -95,4 +96,7 @@ module.exports = {
|
|
|
95
96
|
scanForInjection,
|
|
96
97
|
idempotencyGuard,
|
|
97
98
|
idempotencyAfterHook,
|
|
99
|
+
diagnoseDeps,
|
|
100
|
+
depsHealthReport,
|
|
101
|
+
getMigration,
|
|
98
102
|
};
|
package/src/logger/pricing.js
CHANGED
|
@@ -36,6 +36,16 @@ const DEFAULT_PRICING = {
|
|
|
36
36
|
"text-embedding-3-small": { input: 0.02, output: 0.00 },
|
|
37
37
|
"text-embedding-3-large": { input: 0.13, output: 0.00 },
|
|
38
38
|
|
|
39
|
+
// Anthropic Claude family
|
|
40
|
+
"claude-opus-4": { input: 15.00, output: 75.00 },
|
|
41
|
+
"claude-sonnet-4": { input: 3.00, output: 15.00 },
|
|
42
|
+
"claude-haiku-4": { input: 0.80, output: 4.00 },
|
|
43
|
+
"claude-3-5-sonnet": { input: 3.00, output: 15.00 },
|
|
44
|
+
"claude-3-5-haiku": { input: 0.80, output: 4.00 },
|
|
45
|
+
"claude-3-opus": { input: 15.00, output: 75.00 },
|
|
46
|
+
"claude-3-sonnet": { input: 3.00, output: 15.00 },
|
|
47
|
+
"claude-3-haiku": { input: 0.25, output: 1.25 },
|
|
48
|
+
|
|
39
49
|
// Fallback for unknown models
|
|
40
50
|
"_default": { input: 1.00, output: 4.00 },
|
|
41
51
|
};
|
|
@@ -54,6 +54,7 @@ function collectHeartbeat(subsystems) {
|
|
|
54
54
|
byCategory: usage?.byCategory || {},
|
|
55
55
|
byModel: usage?.byModel || {},
|
|
56
56
|
byTool: usage?.byTool || {},
|
|
57
|
+
byProvider: _aggregateByProvider(usage?.byModel || {}),
|
|
57
58
|
},
|
|
58
59
|
|
|
59
60
|
brain: { totalMemories: brain?.getStats()?.totalEntries || 0 },
|
|
@@ -77,4 +78,23 @@ function collectHeartbeat(subsystems) {
|
|
|
77
78
|
return redactObj(payload);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Aggregate usage by provider (openai vs anthropic) from byModel data.
|
|
83
|
+
* Any new model/provider automatically flows through — no code changes needed.
|
|
84
|
+
*/
|
|
85
|
+
function _aggregateByProvider(byModel) {
|
|
86
|
+
const { detectProvider } = require("../core/models");
|
|
87
|
+
const byProvider = {};
|
|
88
|
+
for (const [model, stats] of Object.entries(byModel || {})) {
|
|
89
|
+
const provider = detectProvider(model);
|
|
90
|
+
if (!byProvider[provider]) byProvider[provider] = { input: 0, output: 0, total: 0, calls: 0, cost: 0 };
|
|
91
|
+
byProvider[provider].input += stats.input || 0;
|
|
92
|
+
byProvider[provider].output += stats.output || 0;
|
|
93
|
+
byProvider[provider].total += stats.total || 0;
|
|
94
|
+
byProvider[provider].calls += stats.calls || 0;
|
|
95
|
+
byProvider[provider].cost += stats.cost || 0;
|
|
96
|
+
}
|
|
97
|
+
return byProvider;
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
module.exports = { collectHeartbeat, INSTANCE_ID };
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Manager Skill — structured npm dependency analysis + repair.
|
|
3
|
+
*
|
|
4
|
+
* The agent often sees errors caused by dependency issues (version conflicts,
|
|
5
|
+
* missing peer deps, deprecated APIs, vulnerabilities) and wastes tokens trying
|
|
6
|
+
* to "fix" code when the real problem is in package.json or node_modules.
|
|
7
|
+
*
|
|
8
|
+
* This skill provides structured analysis BEFORE the AI runs, so the agent
|
|
9
|
+
* gets actionable context like "express@4.21 has a known vulnerability,
|
|
10
|
+
* upgrade to 4.22" instead of guessing from a stack trace.
|
|
11
|
+
*
|
|
12
|
+
* Capabilities:
|
|
13
|
+
* 1. Audit: npm audit wrapper — finds vulnerabilities with fix commands
|
|
14
|
+
* 2. Outdated: detects packages with available updates
|
|
15
|
+
* 3. Peer deps: finds missing/conflicting peer dependencies
|
|
16
|
+
* 4. Unused: detects packages in package.json not imported anywhere
|
|
17
|
+
* 5. Lock repair: detects and fixes corrupted package-lock.json
|
|
18
|
+
* 6. Version conflicts: finds packages requiring incompatible versions
|
|
19
|
+
* 7. Migration knowledge: knows common upgrade paths (express→fastify, etc.)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { execSync } = require("child_process");
|
|
23
|
+
const fs = require("fs");
|
|
24
|
+
const path = require("path");
|
|
25
|
+
const chalk = require("chalk");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run npm audit and return structured vulnerability data.
|
|
29
|
+
* @param {string} cwd — project root
|
|
30
|
+
* @returns {{ vulnerabilities: number, critical: number, high: number, fixes: string[] }}
|
|
31
|
+
*/
|
|
32
|
+
function audit(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = execSync("npm audit --json 2>/dev/null", { cwd, encoding: "utf-8", timeout: 30000 });
|
|
35
|
+
const data = JSON.parse(raw);
|
|
36
|
+
const meta = data.metadata || {};
|
|
37
|
+
const vulns = meta.vulnerabilities || {};
|
|
38
|
+
const total = (vulns.critical || 0) + (vulns.high || 0) + (vulns.moderate || 0) + (vulns.low || 0);
|
|
39
|
+
|
|
40
|
+
const fixes = [];
|
|
41
|
+
if (total > 0) {
|
|
42
|
+
try {
|
|
43
|
+
const fixOutput = execSync("npm audit fix --dry-run --json 2>/dev/null", { cwd, encoding: "utf-8", timeout: 30000 });
|
|
44
|
+
const fixData = JSON.parse(fixOutput);
|
|
45
|
+
if (fixData.added || fixData.updated || fixData.removed) {
|
|
46
|
+
fixes.push("npm audit fix");
|
|
47
|
+
}
|
|
48
|
+
} catch { fixes.push("npm audit fix (may require --force for breaking changes)"); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
vulnerabilities: total,
|
|
53
|
+
critical: vulns.critical || 0,
|
|
54
|
+
high: vulns.high || 0,
|
|
55
|
+
moderate: vulns.moderate || 0,
|
|
56
|
+
low: vulns.low || 0,
|
|
57
|
+
fixes,
|
|
58
|
+
};
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// npm audit exits non-zero when vulnerabilities exist
|
|
61
|
+
try {
|
|
62
|
+
const data = JSON.parse(e.stdout || "{}");
|
|
63
|
+
const vulns = (data.metadata || {}).vulnerabilities || {};
|
|
64
|
+
return {
|
|
65
|
+
vulnerabilities: (vulns.critical || 0) + (vulns.high || 0) + (vulns.moderate || 0) + (vulns.low || 0),
|
|
66
|
+
critical: vulns.critical || 0,
|
|
67
|
+
high: vulns.high || 0,
|
|
68
|
+
moderate: vulns.moderate || 0,
|
|
69
|
+
low: vulns.low || 0,
|
|
70
|
+
fixes: ["npm audit fix"],
|
|
71
|
+
};
|
|
72
|
+
} catch { return { vulnerabilities: 0, critical: 0, high: 0, moderate: 0, low: 0, fixes: [], error: e.message?.slice(0, 100) }; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Find outdated packages.
|
|
78
|
+
* @param {string} cwd
|
|
79
|
+
* @returns {Array<{ name, current, wanted, latest, type }>}
|
|
80
|
+
*/
|
|
81
|
+
function outdated(cwd) {
|
|
82
|
+
try {
|
|
83
|
+
const raw = execSync("npm outdated --json 2>/dev/null", { cwd, encoding: "utf-8", timeout: 30000 });
|
|
84
|
+
const data = JSON.parse(raw || "{}");
|
|
85
|
+
return Object.entries(data).map(([name, info]) => ({
|
|
86
|
+
name,
|
|
87
|
+
current: info.current,
|
|
88
|
+
wanted: info.wanted,
|
|
89
|
+
latest: info.latest,
|
|
90
|
+
type: info.type || "dependencies",
|
|
91
|
+
}));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// npm outdated exits non-zero when outdated packages exist
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(e.stdout || "{}");
|
|
96
|
+
return Object.entries(data).map(([name, info]) => ({
|
|
97
|
+
name,
|
|
98
|
+
current: info.current,
|
|
99
|
+
wanted: info.wanted,
|
|
100
|
+
latest: info.latest,
|
|
101
|
+
type: info.type || "dependencies",
|
|
102
|
+
}));
|
|
103
|
+
} catch { return []; }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Find missing or conflicting peer dependencies.
|
|
109
|
+
* @param {string} cwd
|
|
110
|
+
* @returns {Array<{ package, requires, missing }>}
|
|
111
|
+
*/
|
|
112
|
+
function checkPeerDeps(cwd) {
|
|
113
|
+
const issues = [];
|
|
114
|
+
try {
|
|
115
|
+
const raw = execSync("npm ls --json --depth=1 2>/dev/null", { cwd, encoding: "utf-8", timeout: 30000 });
|
|
116
|
+
const data = JSON.parse(raw || "{}");
|
|
117
|
+
|
|
118
|
+
const walk = (deps, parentName = "root") => {
|
|
119
|
+
if (!deps) return;
|
|
120
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
121
|
+
if (info.peerMissing) {
|
|
122
|
+
for (const peer of info.peerMissing) {
|
|
123
|
+
issues.push({ package: name, requires: peer.requires, missing: peer.requiredBy || name });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (info.problems) {
|
|
127
|
+
for (const problem of info.problems) {
|
|
128
|
+
if (problem.includes("peer dep")) {
|
|
129
|
+
issues.push({ package: name, requires: problem, missing: parentName });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (info.dependencies) walk(info.dependencies, name);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
walk(data.dependencies);
|
|
137
|
+
} catch (e) {
|
|
138
|
+
try {
|
|
139
|
+
const data = JSON.parse(e.stdout || "{}");
|
|
140
|
+
if (data.problems) {
|
|
141
|
+
for (const p of data.problems) {
|
|
142
|
+
issues.push({ package: "root", requires: p, missing: "project" });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
147
|
+
return issues;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Find packages in package.json that aren't imported anywhere in the project.
|
|
152
|
+
* @param {string} cwd
|
|
153
|
+
* @returns {string[]} — unused package names
|
|
154
|
+
*/
|
|
155
|
+
function findUnused(cwd) {
|
|
156
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
157
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
158
|
+
|
|
159
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
160
|
+
const allDeps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
161
|
+
|
|
162
|
+
// Scan all .js/.ts/.mjs/.cjs files for require/import statements
|
|
163
|
+
const usedPackages = new Set();
|
|
164
|
+
const scanDir = (dir) => {
|
|
165
|
+
let entries;
|
|
166
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
167
|
+
for (const entry of entries) {
|
|
168
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".wolverine") continue;
|
|
169
|
+
const fullPath = path.join(dir, entry.name);
|
|
170
|
+
if (entry.isDirectory()) { scanDir(fullPath); continue; }
|
|
171
|
+
if (!/\.(js|ts|mjs|cjs|jsx|tsx)$/.test(entry.name)) continue;
|
|
172
|
+
try {
|
|
173
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
174
|
+
// Match require("X") and import ... from "X"
|
|
175
|
+
const requires = content.match(/require\s*\(\s*["']([^"'./][^"']*)["']\s*\)/g) || [];
|
|
176
|
+
const imports = content.match(/from\s+["']([^"'./][^"']*)["']/g) || [];
|
|
177
|
+
for (const r of requires) {
|
|
178
|
+
const m = r.match(/["']([^"']+)["']/);
|
|
179
|
+
if (m) usedPackages.add(m[1].split("/")[0]);
|
|
180
|
+
}
|
|
181
|
+
for (const i of imports) {
|
|
182
|
+
const m = i.match(/["']([^"']+)["']/);
|
|
183
|
+
if (m) usedPackages.add(m[1].split("/")[0]);
|
|
184
|
+
}
|
|
185
|
+
} catch {}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
scanDir(cwd);
|
|
189
|
+
|
|
190
|
+
// Filter: only report deps that are truly unused
|
|
191
|
+
// Exclude common false positives (types, plugins, bin-only packages)
|
|
192
|
+
const falsePositives = new Set(["typescript", "@types", "eslint", "prettier", "nodemon", "ts-node"]);
|
|
193
|
+
return allDeps.filter(dep => {
|
|
194
|
+
const baseName = dep.startsWith("@") ? dep : dep.split("/")[0];
|
|
195
|
+
return !usedPackages.has(baseName) && !falsePositives.has(baseName) && !baseName.startsWith("@types/");
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if package-lock.json is healthy.
|
|
201
|
+
* @param {string} cwd
|
|
202
|
+
* @returns {{ healthy: boolean, issue?: string, fix?: string }}
|
|
203
|
+
*/
|
|
204
|
+
function checkLockFile(cwd) {
|
|
205
|
+
const lockPath = path.join(cwd, "package-lock.json");
|
|
206
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
207
|
+
|
|
208
|
+
if (!fs.existsSync(lockPath)) {
|
|
209
|
+
return { healthy: false, issue: "No package-lock.json found", fix: "npm install" };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
|
|
214
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
215
|
+
|
|
216
|
+
// Check lock version
|
|
217
|
+
if (!lock.lockfileVersion || lock.lockfileVersion < 2) {
|
|
218
|
+
return { healthy: false, issue: `Outdated lockfile version ${lock.lockfileVersion || 1}`, fix: "rm package-lock.json && npm install" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if lock matches package.json
|
|
222
|
+
const pkgDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
223
|
+
const lockDeps = lock.packages?.[""]?.dependencies || {};
|
|
224
|
+
const lockDevDeps = lock.packages?.[""]?.devDependencies || {};
|
|
225
|
+
const allLockDeps = { ...lockDeps, ...lockDevDeps };
|
|
226
|
+
|
|
227
|
+
for (const [name] of Object.entries(pkgDeps)) {
|
|
228
|
+
if (!allLockDeps[name] && !lock.dependencies?.[name]) {
|
|
229
|
+
return { healthy: false, issue: `${name} in package.json but missing from lock`, fix: "npm install" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { healthy: true };
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return { healthy: false, issue: `Corrupted lock file: ${e.message?.slice(0, 60)}`, fix: "rm package-lock.json && npm install" };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Diagnose a dependency-related error and return structured fix recommendations.
|
|
241
|
+
* This is the main entry point — call it from the heal pipeline.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} errorMessage
|
|
244
|
+
* @param {string} cwd
|
|
245
|
+
* @returns {{ diagnosed: boolean, category: string, summary: string, fixes: string[] }}
|
|
246
|
+
*/
|
|
247
|
+
function diagnose(errorMessage, cwd) {
|
|
248
|
+
const msg = (errorMessage || "").toLowerCase();
|
|
249
|
+
|
|
250
|
+
// Pattern: Cannot find module 'X'
|
|
251
|
+
const missingMatch = errorMessage.match(/Cannot find module '([^']+)'/);
|
|
252
|
+
if (missingMatch) {
|
|
253
|
+
const mod = missingMatch[1];
|
|
254
|
+
if (!mod.startsWith(".") && !mod.startsWith("/")) {
|
|
255
|
+
// Check if it's in package.json
|
|
256
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
257
|
+
let inPkg = false;
|
|
258
|
+
try {
|
|
259
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
260
|
+
inPkg = !!(pkg.dependencies?.[mod] || pkg.devDependencies?.[mod]);
|
|
261
|
+
} catch {}
|
|
262
|
+
|
|
263
|
+
if (inPkg) {
|
|
264
|
+
return {
|
|
265
|
+
diagnosed: true,
|
|
266
|
+
category: "missing_install",
|
|
267
|
+
summary: `${mod} is in package.json but not installed`,
|
|
268
|
+
fixes: ["npm install"],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
diagnosed: true,
|
|
273
|
+
category: "missing_package",
|
|
274
|
+
summary: `${mod} is not in package.json and not installed`,
|
|
275
|
+
fixes: [`npm install ${mod}`],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Pattern: version/compatibility errors
|
|
281
|
+
if (/version mismatch|incompatible|peer dep|ERESOLVE/i.test(msg)) {
|
|
282
|
+
const peerIssues = checkPeerDeps(cwd);
|
|
283
|
+
return {
|
|
284
|
+
diagnosed: true,
|
|
285
|
+
category: "version_conflict",
|
|
286
|
+
summary: `Dependency version conflict (${peerIssues.length} peer dep issues)`,
|
|
287
|
+
fixes: peerIssues.length > 0
|
|
288
|
+
? peerIssues.map(p => `npm install ${p.package}@latest`).concat(["npm install --legacy-peer-deps"])
|
|
289
|
+
: ["npm install --legacy-peer-deps", "rm -rf node_modules && npm install"],
|
|
290
|
+
details: peerIssues,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Pattern: deprecated/removed API
|
|
295
|
+
if (/is not a function|is not a constructor|has no method|deprecated/i.test(msg)) {
|
|
296
|
+
const outdatedPkgs = outdated(cwd);
|
|
297
|
+
if (outdatedPkgs.length > 0) {
|
|
298
|
+
// Find the package mentioned in the error
|
|
299
|
+
const relevantPkgs = outdatedPkgs.filter(p =>
|
|
300
|
+
msg.includes(p.name.toLowerCase()) || errorMessage.includes(p.name)
|
|
301
|
+
);
|
|
302
|
+
if (relevantPkgs.length > 0) {
|
|
303
|
+
return {
|
|
304
|
+
diagnosed: true,
|
|
305
|
+
category: "outdated_api",
|
|
306
|
+
summary: `API may have changed in newer version of ${relevantPkgs.map(p => p.name).join(", ")}`,
|
|
307
|
+
fixes: relevantPkgs.map(p => `npm install ${p.name}@${p.latest}`),
|
|
308
|
+
details: relevantPkgs,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Pattern: corrupted node_modules
|
|
315
|
+
if (/unexpected token|cannot find module.*node_modules|EISDIR|ENOTDIR/i.test(msg) && msg.includes("node_modules")) {
|
|
316
|
+
return {
|
|
317
|
+
diagnosed: true,
|
|
318
|
+
category: "corrupted_modules",
|
|
319
|
+
summary: "node_modules appears corrupted",
|
|
320
|
+
fixes: ["rm -rf node_modules package-lock.json && npm install"],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { diagnosed: false };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Full dependency health report — used by dashboard and agent context.
|
|
329
|
+
* @param {string} cwd
|
|
330
|
+
* @returns {object}
|
|
331
|
+
*/
|
|
332
|
+
function healthReport(cwd) {
|
|
333
|
+
const auditResult = audit(cwd);
|
|
334
|
+
const outdatedPkgs = outdated(cwd);
|
|
335
|
+
const peerIssues = checkPeerDeps(cwd);
|
|
336
|
+
const unused = findUnused(cwd);
|
|
337
|
+
const lockStatus = checkLockFile(cwd);
|
|
338
|
+
|
|
339
|
+
const score = Math.max(0, 100
|
|
340
|
+
- (auditResult.critical * 20)
|
|
341
|
+
- (auditResult.high * 10)
|
|
342
|
+
- (auditResult.moderate * 3)
|
|
343
|
+
- (peerIssues.length * 5)
|
|
344
|
+
- (lockStatus.healthy ? 0 : 15)
|
|
345
|
+
- Math.min(outdatedPkgs.length * 2, 20)
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
score,
|
|
350
|
+
audit: auditResult,
|
|
351
|
+
outdated: outdatedPkgs,
|
|
352
|
+
peerDeps: peerIssues,
|
|
353
|
+
unused,
|
|
354
|
+
lockFile: lockStatus,
|
|
355
|
+
summary: score >= 80 ? "Healthy" : score >= 50 ? "Needs attention" : "Critical issues",
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Migration Knowledge ──
|
|
360
|
+
|
|
361
|
+
const MIGRATION_PATTERNS = {
|
|
362
|
+
"express": {
|
|
363
|
+
to: "fastify",
|
|
364
|
+
reason: "5.6x faster routing, built-in validation, async-first",
|
|
365
|
+
patterns: [
|
|
366
|
+
{ from: "app.get(path, handler)", to: "fastify.get(path, handler)" },
|
|
367
|
+
{ from: "app.use(middleware)", to: "fastify.addHook('preHandler', middleware)" },
|
|
368
|
+
{ from: "res.json(data)", to: "reply.send(data)" },
|
|
369
|
+
{ from: "res.status(code)", to: "reply.code(code)" },
|
|
370
|
+
{ from: "app.listen(port)", to: "fastify.listen({ port, host: '0.0.0.0' })" },
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
"request": {
|
|
374
|
+
to: "node-fetch or undici",
|
|
375
|
+
reason: "request is deprecated since 2020",
|
|
376
|
+
patterns: [
|
|
377
|
+
{ from: "request(url, callback)", to: "const res = await fetch(url)" },
|
|
378
|
+
{ from: "request.post(url, { body })", to: "await fetch(url, { method: 'POST', body })" },
|
|
379
|
+
],
|
|
380
|
+
},
|
|
381
|
+
"moment": {
|
|
382
|
+
to: "dayjs or date-fns",
|
|
383
|
+
reason: "moment is in maintenance mode, 70KB vs 2KB",
|
|
384
|
+
patterns: [
|
|
385
|
+
{ from: "moment().format('YYYY-MM-DD')", to: "dayjs().format('YYYY-MM-DD')" },
|
|
386
|
+
{ from: "moment(date).diff(other)", to: "dayjs(date).diff(other)" },
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
"body-parser": {
|
|
390
|
+
to: "built-in (Express 4.16+ / Fastify)",
|
|
391
|
+
reason: "Included by default in modern frameworks",
|
|
392
|
+
patterns: [
|
|
393
|
+
{ from: "app.use(bodyParser.json())", to: "app.use(express.json())" },
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
"callback-style": {
|
|
397
|
+
to: "async/await with util.promisify",
|
|
398
|
+
reason: "Cleaner error handling, no callback hell",
|
|
399
|
+
patterns: [
|
|
400
|
+
{ from: "fs.readFile(path, (err, data) => {})", to: "const data = await fs.promises.readFile(path)" },
|
|
401
|
+
{ from: "db.query(sql, (err, rows) => {})", to: "const rows = await db.query(sql)" },
|
|
402
|
+
],
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Check if a package has a known migration path.
|
|
408
|
+
* @param {string} packageName
|
|
409
|
+
* @returns {object|null}
|
|
410
|
+
*/
|
|
411
|
+
function getMigration(packageName) {
|
|
412
|
+
return MIGRATION_PATTERNS[packageName] || null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Skill Metadata ──
|
|
416
|
+
|
|
417
|
+
const SKILL_NAME = "deps";
|
|
418
|
+
const SKILL_DESCRIPTION = "Dependency manager: npm audit, version conflicts, peer deps, unused packages, lock file repair, migration knowledge. Diagnoses dependency-related errors before AI runs, providing actionable fix commands instead of code edits.";
|
|
419
|
+
const SKILL_KEYWORDS = ["dependency", "dependencies", "npm", "package", "install", "audit", "vulnerability", "outdated", "peer", "version", "conflict", "lock", "node_modules", "upgrade", "migrate", "deprecated"];
|
|
420
|
+
const SKILL_USAGE = `// Diagnose a dependency error (returns structured fix)
|
|
421
|
+
const { diagnose } = require("../src/skills/deps");
|
|
422
|
+
const result = diagnose("Cannot find module 'cors'", process.cwd());
|
|
423
|
+
// { diagnosed: true, category: "missing_package", fixes: ["npm install cors"] }
|
|
424
|
+
|
|
425
|
+
// Full health report
|
|
426
|
+
const { healthReport } = require("../src/skills/deps");
|
|
427
|
+
const report = healthReport(process.cwd());
|
|
428
|
+
// { score: 85, audit: {...}, outdated: [...], peerDeps: [...], unused: [...] }
|
|
429
|
+
|
|
430
|
+
// Check migration paths
|
|
431
|
+
const { getMigration } = require("../src/skills/deps");
|
|
432
|
+
const migration = getMigration("moment");
|
|
433
|
+
// { to: "dayjs", reason: "maintenance mode, 70KB vs 2KB", patterns: [...] }`;
|
|
434
|
+
|
|
435
|
+
module.exports = {
|
|
436
|
+
SKILL_NAME,
|
|
437
|
+
SKILL_DESCRIPTION,
|
|
438
|
+
SKILL_KEYWORDS,
|
|
439
|
+
SKILL_USAGE,
|
|
440
|
+
audit,
|
|
441
|
+
outdated,
|
|
442
|
+
checkPeerDeps,
|
|
443
|
+
findUnused,
|
|
444
|
+
checkLockFile,
|
|
445
|
+
diagnose,
|
|
446
|
+
healthReport,
|
|
447
|
+
getMigration,
|
|
448
|
+
MIGRATION_PATTERNS,
|
|
449
|
+
};
|