@safetnsr/vet 0.3.0 → 0.5.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 +8 -35
- package/dist/checks/deps.d.ts +6 -0
- package/dist/checks/deps.js +276 -0
- package/dist/cli.js +9 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vet
|
|
2
2
|
|
|
3
|
-
vet your AI-generated code. one command,
|
|
3
|
+
vet your AI-generated code. one command, eight checks, zero config.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @safetnsr/vet
|
|
@@ -15,13 +15,11 @@ works with Claude Code, Cursor, Copilot, Codex, Aider, Windsurf, Cline — anyth
|
|
|
15
15
|
| **ready** | is your codebase AI-friendly? | scans structure, docs, types, tests |
|
|
16
16
|
| **diff** | did the AI leave anti-patterns? | AI-specific patterns: wholesale rewrites, orphaned imports, catch-alls, over-commenting, plus secrets & stubs |
|
|
17
17
|
| **models** | using deprecated AI models? | scans code for sunset model strings across OpenAI, Anthropic, Google, Cohere |
|
|
18
|
-
| **links** | broken markdown links? | validates relative links and wikilinks |
|
|
19
18
|
| **config** | agent configs in place? | deep analysis of CLAUDE.md, .cursorrules, copilot-instructions — checks completeness, consistency, and specificity against your actual codebase |
|
|
20
19
|
| **history** | git patterns healthy? | analyzes commit churn, AI attribution, large changes |
|
|
21
20
|
| **scan** | malicious patterns in agent configs? | scans .claude/, .cursorrules, CLAUDE.md, .mcp/ for prompt injection, shell injection, exfiltration endpoints |
|
|
22
21
|
| **secrets** | leaked secrets in build output? | scans dist/, build/, .next/ + .env files for API keys, tokens, connection strings using pattern + entropy analysis |
|
|
23
22
|
| **receipt** | what did the last agent session do? | parses ~/.claude/projects/ JSONL session logs — files changed, commands run, packages installed, SHA256 integrity hash |
|
|
24
|
-
| **edge** | how replaceable is your git history? | classifies commits by human-edge score: architecture (90) → debugging (85) → integration (80) → feature (60) → boilerplate (20) → cosmetic (10) |
|
|
25
23
|
|
|
26
24
|
## usage
|
|
27
25
|
|
|
@@ -53,23 +51,21 @@ npx @safetnsr/vet init
|
|
|
53
51
|
# show last agent session receipt (ASCII or JSON)
|
|
54
52
|
npx @safetnsr/vet receipt
|
|
55
53
|
npx @safetnsr/vet receipt --json
|
|
56
|
-
|
|
57
|
-
# show human-edge score for git history
|
|
58
|
-
npx @safetnsr/vet edge
|
|
59
|
-
npx @safetnsr/vet edge --explain
|
|
60
54
|
```
|
|
61
55
|
|
|
62
56
|
## output
|
|
63
57
|
|
|
64
58
|
```
|
|
65
|
-
my-project
|
|
59
|
+
my-project 7.5/10
|
|
66
60
|
|
|
67
61
|
ready ████░░░░░░ 4 3 readiness issues
|
|
68
62
|
diff ████████░░ 8 3 issues (2 AI-specific) in 5 files
|
|
69
63
|
models ██████████ 10 all models current
|
|
70
|
-
links ██████░░░░ 6 3 broken links in docs/
|
|
71
64
|
config ███░░░░░░░ 3 Cursor — needs work (3/10)
|
|
72
65
|
history █████████░ 9 41 commits (~15% AI-attributed)
|
|
66
|
+
scan ██████████ 10 no malicious patterns found
|
|
67
|
+
secrets ██████████ 10 no leaked secrets
|
|
68
|
+
receipt ██████████ 10 last session: 3 files, 2 commands
|
|
73
69
|
|
|
74
70
|
✗ no README — AI agents have no project context
|
|
75
71
|
✗ no tests — AI agents produce better code when tests exist
|
|
@@ -81,7 +77,7 @@ npx @safetnsr/vet edge --explain
|
|
|
81
77
|
|
|
82
78
|
## --fix
|
|
83
79
|
|
|
84
|
-
`vet --fix`
|
|
80
|
+
`vet --fix` analyzes your codebase and generates project-specific configs:
|
|
85
81
|
|
|
86
82
|
```bash
|
|
87
83
|
$ npx @safetnsr/vet --fix
|
|
@@ -95,12 +91,10 @@ $ npx @safetnsr/vet --fix
|
|
|
95
91
|
fixed 3 issues
|
|
96
92
|
```
|
|
97
93
|
|
|
98
|
-
the generated CLAUDE.md includes your actual stack, directory structure, and framework-specific rules
|
|
94
|
+
the generated CLAUDE.md includes your actual stack, directory structure, and framework-specific rules.
|
|
99
95
|
|
|
100
96
|
## AI-specific diff patterns
|
|
101
97
|
|
|
102
|
-
vet catches things that are specific to AI-generated code:
|
|
103
|
-
|
|
104
98
|
| pattern | what it catches |
|
|
105
99
|
|---------|----------------|
|
|
106
100
|
| `[ai] wholesale rewrite` | AI rewrote an entire function when a small edit would suffice |
|
|
@@ -144,34 +138,13 @@ Shows a receipt for the last Claude Code agent session — what files it touched
|
|
|
144
138
|
╚══════════════════════════════════════════════╝
|
|
145
139
|
```
|
|
146
140
|
|
|
147
|
-
### `vet edge`
|
|
148
|
-
|
|
149
|
-
Analyzes git history and scores how AI-replaceable your contributions are:
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
Human Edge Report 72/100
|
|
153
|
-
|
|
154
|
-
🏗️ Architecture 12 (28%) ████████████
|
|
155
|
-
🔍 Debugging 8 (19%) ████████
|
|
156
|
-
🔗 Integration 6 (14%) ██████
|
|
157
|
-
⚡ Feature 10 (24%) ██████████
|
|
158
|
-
📋 Boilerplate 5 (12%) █████
|
|
159
|
-
🎨 Cosmetic 1 (2%) █
|
|
160
|
-
|
|
161
|
-
Top commits
|
|
162
|
-
90 a3f2b1c refactor: extract auth middleware across all routes
|
|
163
|
-
85 7e8d9f1 fix: resolve race condition in session cleanup
|
|
164
|
-
|
|
165
|
-
→ Strong position. Your work is deeply contextual and hard to automate.
|
|
166
|
-
```
|
|
167
|
-
|
|
168
141
|
## config
|
|
169
142
|
|
|
170
143
|
create `.vetrc` in your project root (optional):
|
|
171
144
|
|
|
172
145
|
```json
|
|
173
146
|
{
|
|
174
|
-
"checks": ["ready", "diff", "models", "
|
|
147
|
+
"checks": ["ready", "diff", "models", "config", "history", "scan", "secrets", "receipt"],
|
|
175
148
|
"ignore": ["vendor/", "generated/"],
|
|
176
149
|
"thresholds": { "min": 6 }
|
|
177
150
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
export declare function levenshtein(a: string, b: string): number;
|
|
3
|
+
export declare function extractImports(source: string): string[];
|
|
4
|
+
export declare function extractPackageName(specifier: string): string | null;
|
|
5
|
+
export declare function isBuiltin(specifier: string): boolean;
|
|
6
|
+
export declare function checkDeps(cwd: string): Promise<CheckResult>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { walkFiles, readFile } from '../util.js';
|
|
4
|
+
// ── Top packages list (~150 popular npm packages) ────────────────────────────
|
|
5
|
+
const TOP_PACKAGES = [
|
|
6
|
+
'react', 'react-dom', 'next', 'vue', 'angular', 'express', 'koa', 'fastify', 'hono',
|
|
7
|
+
'axios', 'node-fetch', 'chalk', 'commander', 'yargs', 'inquirer', 'lodash', 'underscore',
|
|
8
|
+
'ramda', 'moment', 'dayjs', 'date-fns', 'uuid', 'nanoid', 'dotenv', 'cors', 'helmet',
|
|
9
|
+
'morgan', 'winston', 'pino', 'debug', 'zod', 'joi', 'yup', 'ajv', 'prettier', 'eslint',
|
|
10
|
+
'typescript', 'webpack', 'vite', 'rollup', 'esbuild', 'swc', 'babel', 'jest', 'vitest',
|
|
11
|
+
'mocha', 'chai', 'sinon', 'supertest', 'playwright', 'puppeteer', 'cypress', 'mongoose',
|
|
12
|
+
'prisma', 'drizzle-orm', 'knex', 'sequelize', 'pg', 'mysql2', 'better-sqlite3', 'redis',
|
|
13
|
+
'ioredis', 'bullmq', 'sharp', 'jimp', 'multer', 'formidable', 'nodemailer', 'socket.io',
|
|
14
|
+
'ws', 'mqtt', 'graphql', 'apollo-server', 'trpc', 'stripe', 'aws-sdk', 'firebase',
|
|
15
|
+
'supabase', 'openai', 'langchain', 'oclif', 'glob', 'minimatch', 'micromatch', 'semver',
|
|
16
|
+
'minimist', 'cross-env', 'concurrently', 'tsx', 'ts-node', 'rimraf', 'mkdirp', 'fs-extra',
|
|
17
|
+
'chokidar', 'ora', 'listr2', 'boxen', 'figlet', 'gradient-string', 'conf', 'cosmiconfig',
|
|
18
|
+
'execa', 'got', 'ky', 'undici', 'cheerio', 'jsdom', 'marked', 'gray-matter', 'unified',
|
|
19
|
+
'rehype', 'remark', 'mdast', 'hast', 'three', 'd3', 'chart.js', 'tailwindcss', 'postcss',
|
|
20
|
+
'sass', 'less', 'styled-components', 'emotion',
|
|
21
|
+
];
|
|
22
|
+
// ── Node.js builtins ─────────────────────────────────────────────────────────
|
|
23
|
+
const NODE_BUILTINS = new Set([
|
|
24
|
+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants',
|
|
25
|
+
'crypto', 'dgram', 'diagnostics_channel', 'dns', 'domain', 'events', 'fs', 'http',
|
|
26
|
+
'http2', 'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks',
|
|
27
|
+
'process', 'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder',
|
|
28
|
+
'sys', 'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'wasi',
|
|
29
|
+
'worker_threads', 'zlib', 'test',
|
|
30
|
+
// also with node: prefix variants handled separately
|
|
31
|
+
'fs/promises', 'stream/promises', 'timers/promises', 'dns/promises',
|
|
32
|
+
'stream/web', 'stream/consumers', 'readline/promises', 'util/types',
|
|
33
|
+
]);
|
|
34
|
+
// ── Levenshtein distance ─────────────────────────────────────────────────────
|
|
35
|
+
export function levenshtein(a, b) {
|
|
36
|
+
const m = a.length;
|
|
37
|
+
const n = b.length;
|
|
38
|
+
if (m === 0)
|
|
39
|
+
return n;
|
|
40
|
+
if (n === 0)
|
|
41
|
+
return m;
|
|
42
|
+
const dp = [];
|
|
43
|
+
for (let i = 0; i <= m; i++) {
|
|
44
|
+
dp[i] = [i];
|
|
45
|
+
}
|
|
46
|
+
for (let j = 1; j <= n; j++) {
|
|
47
|
+
dp[0][j] = j;
|
|
48
|
+
}
|
|
49
|
+
for (let i = 1; i <= m; i++) {
|
|
50
|
+
for (let j = 1; j <= n; j++) {
|
|
51
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
52
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return dp[m][n];
|
|
56
|
+
}
|
|
57
|
+
// ── Import extraction ────────────────────────────────────────────────────────
|
|
58
|
+
export function extractImports(source) {
|
|
59
|
+
const imports = new Set();
|
|
60
|
+
// import ... from 'pkg'
|
|
61
|
+
const importFrom = /import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = importFrom.exec(source)) !== null) {
|
|
64
|
+
imports.add(match[1]);
|
|
65
|
+
}
|
|
66
|
+
// require('pkg')
|
|
67
|
+
const requirePat = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
68
|
+
while ((match = requirePat.exec(source)) !== null) {
|
|
69
|
+
imports.add(match[1]);
|
|
70
|
+
}
|
|
71
|
+
// import('pkg')
|
|
72
|
+
const dynamicImport = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
73
|
+
while ((match = dynamicImport.exec(source)) !== null) {
|
|
74
|
+
imports.add(match[1]);
|
|
75
|
+
}
|
|
76
|
+
return [...imports];
|
|
77
|
+
}
|
|
78
|
+
// ── Package name extraction ──────────────────────────────────────────────────
|
|
79
|
+
export function extractPackageName(specifier) {
|
|
80
|
+
// Skip relative imports
|
|
81
|
+
if (specifier.startsWith('.') || specifier.startsWith('/'))
|
|
82
|
+
return null;
|
|
83
|
+
// Skip node: builtins
|
|
84
|
+
if (specifier.startsWith('node:'))
|
|
85
|
+
return null;
|
|
86
|
+
// Scoped packages: @scope/name or @scope/name/sub
|
|
87
|
+
if (specifier.startsWith('@')) {
|
|
88
|
+
const parts = specifier.split('/');
|
|
89
|
+
if (parts.length < 2)
|
|
90
|
+
return null;
|
|
91
|
+
return `${parts[0]}/${parts[1]}`;
|
|
92
|
+
}
|
|
93
|
+
// Regular package: name or name/sub
|
|
94
|
+
return specifier.split('/')[0];
|
|
95
|
+
}
|
|
96
|
+
// ── Builtin check ────────────────────────────────────────────────────────────
|
|
97
|
+
export function isBuiltin(specifier) {
|
|
98
|
+
if (specifier.startsWith('node:'))
|
|
99
|
+
return true;
|
|
100
|
+
const name = specifier.split('/')[0];
|
|
101
|
+
if (NODE_BUILTINS.has(name))
|
|
102
|
+
return true;
|
|
103
|
+
// Also check full specifier for subpath builtins
|
|
104
|
+
if (NODE_BUILTINS.has(specifier))
|
|
105
|
+
return true;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
// ── Registry check with concurrency limit ────────────────────────────────────
|
|
109
|
+
async function checkRegistry(packages) {
|
|
110
|
+
const results = new Map();
|
|
111
|
+
const queue = [...packages];
|
|
112
|
+
let networkError = false;
|
|
113
|
+
async function checkOne(pkg) {
|
|
114
|
+
try {
|
|
115
|
+
const controller = new AbortController();
|
|
116
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
117
|
+
const res = await fetch(`https://registry.npmjs.org/${pkg}`, {
|
|
118
|
+
method: 'HEAD',
|
|
119
|
+
signal: controller.signal,
|
|
120
|
+
});
|
|
121
|
+
clearTimeout(timeout);
|
|
122
|
+
results.set(pkg, res.status !== 404);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
networkError = true;
|
|
126
|
+
results.set(pkg, true); // assume exists on error
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Process in batches of 5
|
|
130
|
+
const concurrency = 5;
|
|
131
|
+
for (let i = 0; i < queue.length; i += concurrency) {
|
|
132
|
+
const batch = queue.slice(i, i + concurrency);
|
|
133
|
+
await Promise.all(batch.map(checkOne));
|
|
134
|
+
}
|
|
135
|
+
if (networkError) {
|
|
136
|
+
results.set('__network_error__', true);
|
|
137
|
+
}
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
// ── Main check ───────────────────────────────────────────────────────────────
|
|
141
|
+
export async function checkDeps(cwd) {
|
|
142
|
+
const issues = [];
|
|
143
|
+
// Read package.json
|
|
144
|
+
let declaredDeps = {};
|
|
145
|
+
let hasPkgJson = false;
|
|
146
|
+
try {
|
|
147
|
+
const pkgRaw = readFile(join(cwd, 'package.json'));
|
|
148
|
+
if (pkgRaw) {
|
|
149
|
+
const pkg = JSON.parse(pkgRaw);
|
|
150
|
+
hasPkgJson = true;
|
|
151
|
+
declaredDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch { /* skip */ }
|
|
155
|
+
if (!hasPkgJson) {
|
|
156
|
+
return {
|
|
157
|
+
name: 'deps',
|
|
158
|
+
score: 10,
|
|
159
|
+
maxScore: 10,
|
|
160
|
+
issues: [],
|
|
161
|
+
summary: 'no package.json found',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const declaredNames = Object.keys(declaredDeps);
|
|
165
|
+
// ── 1. Registry check (nonexistent packages) ──────────────────────────────
|
|
166
|
+
const registryResults = await checkRegistry(declaredNames);
|
|
167
|
+
if (registryResults.get('__network_error__')) {
|
|
168
|
+
issues.push({
|
|
169
|
+
severity: 'info',
|
|
170
|
+
message: 'could not reach npm registry — skipping existence checks',
|
|
171
|
+
fixable: false,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
for (const pkg of declaredNames) {
|
|
175
|
+
if (registryResults.get(pkg) === false) {
|
|
176
|
+
issues.push({
|
|
177
|
+
severity: 'error',
|
|
178
|
+
message: `phantom dependency: "${pkg}" does not exist on npm`,
|
|
179
|
+
file: 'package.json',
|
|
180
|
+
fixable: true,
|
|
181
|
+
fixHint: 'remove from package.json',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// ── 2. Typosquat detection ─────────────────────────────────────────────────
|
|
186
|
+
const topSet = new Set(TOP_PACKAGES);
|
|
187
|
+
for (const pkg of declaredNames) {
|
|
188
|
+
if (topSet.has(pkg))
|
|
189
|
+
continue; // it IS the popular package
|
|
190
|
+
for (const top of TOP_PACKAGES) {
|
|
191
|
+
const dist = levenshtein(pkg, top);
|
|
192
|
+
if (dist >= 1 && dist <= 2) {
|
|
193
|
+
issues.push({
|
|
194
|
+
severity: 'error',
|
|
195
|
+
message: `possible typosquat: "${pkg}" is ${dist} edit${dist > 1 ? 's' : ''} from "${top}"`,
|
|
196
|
+
file: 'package.json',
|
|
197
|
+
fixable: true,
|
|
198
|
+
fixHint: `did you mean "${top}"?`,
|
|
199
|
+
});
|
|
200
|
+
break; // one match is enough
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// ── 3 & 4. Dead deps + phantom imports ─────────────────────────────────────
|
|
205
|
+
const sourceExts = new Set(['.ts', '.js', '.tsx', '.jsx', '.mts', '.mjs', '.cts', '.cjs']);
|
|
206
|
+
const allFiles = walkFiles(cwd);
|
|
207
|
+
const sourceFiles = allFiles.filter(f => {
|
|
208
|
+
const ext = f.substring(f.lastIndexOf('.'));
|
|
209
|
+
return sourceExts.has(ext);
|
|
210
|
+
});
|
|
211
|
+
const importedPackages = new Set();
|
|
212
|
+
for (const file of sourceFiles) {
|
|
213
|
+
try {
|
|
214
|
+
const content = readFileSync(join(cwd, file), 'utf-8');
|
|
215
|
+
const rawImports = extractImports(content);
|
|
216
|
+
for (const imp of rawImports) {
|
|
217
|
+
if (isBuiltin(imp))
|
|
218
|
+
continue;
|
|
219
|
+
const pkg = extractPackageName(imp);
|
|
220
|
+
if (pkg)
|
|
221
|
+
importedPackages.add(pkg);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch { /* skip unreadable files */ }
|
|
225
|
+
}
|
|
226
|
+
// Dead deps: declared but never imported
|
|
227
|
+
const declaredSet = new Set(declaredNames);
|
|
228
|
+
for (const pkg of declaredNames) {
|
|
229
|
+
if (!importedPackages.has(pkg)) {
|
|
230
|
+
// Check if it's a CLI tool / plugin / type package (common false positives)
|
|
231
|
+
// Still flag it, but as info
|
|
232
|
+
issues.push({
|
|
233
|
+
severity: 'info',
|
|
234
|
+
message: `unused dependency: "${pkg}" is declared but never imported`,
|
|
235
|
+
file: 'package.json',
|
|
236
|
+
fixable: true,
|
|
237
|
+
fixHint: 'remove from package.json',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Phantom imports: imported but not declared
|
|
242
|
+
for (const pkg of importedPackages) {
|
|
243
|
+
if (!declaredSet.has(pkg)) {
|
|
244
|
+
issues.push({
|
|
245
|
+
severity: 'warning',
|
|
246
|
+
message: `phantom import: "${pkg}" is imported but not in package.json`,
|
|
247
|
+
fixable: true,
|
|
248
|
+
fixHint: `run: npm install ${pkg}`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// ── Scoring ────────────────────────────────────────────────────────────────
|
|
253
|
+
const errors = issues.filter(i => i.severity === 'error').length;
|
|
254
|
+
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
255
|
+
const rawScore = 10 - (errors * 3) - (warnings * 1);
|
|
256
|
+
const finalScore = Math.max(0, Math.min(10, rawScore));
|
|
257
|
+
// ── Summary ────────────────────────────────────────────────────────────────
|
|
258
|
+
const parts = [];
|
|
259
|
+
if (errors > 0)
|
|
260
|
+
parts.push(`${errors} error${errors !== 1 ? 's' : ''}`);
|
|
261
|
+
if (warnings > 0)
|
|
262
|
+
parts.push(`${warnings} warning${warnings !== 1 ? 's' : ''}`);
|
|
263
|
+
const infos = issues.filter(i => i.severity === 'info').length;
|
|
264
|
+
if (infos > 0)
|
|
265
|
+
parts.push(`${infos} info`);
|
|
266
|
+
const summary = parts.length === 0
|
|
267
|
+
? `${declaredNames.length} dependencies checked, all clean`
|
|
268
|
+
: `${declaredNames.length} dependencies: ${parts.join(', ')}`;
|
|
269
|
+
return {
|
|
270
|
+
name: 'deps',
|
|
271
|
+
score: finalScore,
|
|
272
|
+
maxScore: 10,
|
|
273
|
+
issues,
|
|
274
|
+
summary,
|
|
275
|
+
};
|
|
276
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -5,13 +5,12 @@ import { isGitRepo, readFile, c } from './util.js';
|
|
|
5
5
|
import { checkReady } from './checks/ready.js';
|
|
6
6
|
import { checkDiff } from './checks/diff.js';
|
|
7
7
|
import { checkModels } from './checks/models.js';
|
|
8
|
-
import { checkLinks } from './checks/links.js';
|
|
9
8
|
import { checkConfig } from './checks/config.js';
|
|
10
9
|
import { checkHistory } from './checks/history.js';
|
|
11
10
|
import { checkScan } from './checks/scan.js';
|
|
12
11
|
import { checkSecrets } from './checks/secrets.js';
|
|
12
|
+
import { checkDeps } from './checks/deps.js';
|
|
13
13
|
import { checkReceipt, runReceiptCommand } from './checks/receipt.js';
|
|
14
|
-
import { checkEdge, runEdgeCommand } from './checks/edge.js';
|
|
15
14
|
import { score } from './scorer.js';
|
|
16
15
|
import { reportPretty, reportJSON } from './reporter.js';
|
|
17
16
|
const args = process.argv.slice(2);
|
|
@@ -40,28 +39,25 @@ if (flags.has('--help') || flags.has('-h')) {
|
|
|
40
39
|
npx @safetnsr/vet --watch live monitoring during AI sessions
|
|
41
40
|
npx @safetnsr/vet init generate configs + hooks
|
|
42
41
|
npx @safetnsr/vet receipt show last agent session receipt
|
|
43
|
-
npx @safetnsr/vet edge show human-edge score for git history
|
|
44
42
|
|
|
45
43
|
${c.dim}checks:${c.reset}
|
|
46
44
|
ready codebase readiness for AI agents
|
|
47
45
|
diff AI-specific anti-patterns in recent changes
|
|
48
46
|
models deprecated/risky model usage
|
|
49
|
-
links dead markdown links
|
|
50
47
|
config agent config hygiene
|
|
51
48
|
history git history quality
|
|
52
49
|
scan malicious patterns in agent config files
|
|
53
50
|
secrets leaked secrets in build output and .env files
|
|
54
51
|
receipt last agent session audit (informational)
|
|
55
|
-
|
|
52
|
+
deps phantom/hallucinated dependency detection
|
|
56
53
|
|
|
57
54
|
${c.dim}options:${c.reset}
|
|
58
55
|
--ci CI mode (exit 1 if score < threshold)
|
|
59
|
-
--fix auto-fix configs, models
|
|
56
|
+
--fix auto-fix configs, models
|
|
60
57
|
--since REF diff against specific commit/range
|
|
61
58
|
--watch re-run on file changes
|
|
62
59
|
--json JSON output
|
|
63
60
|
--pretty force pretty output (even in pipes)
|
|
64
|
-
--explain show detailed reasoning (edge subcommand)
|
|
65
61
|
-h, --help show this help
|
|
66
62
|
-v, --version show version
|
|
67
63
|
`);
|
|
@@ -77,7 +73,7 @@ if (flags.has('--version') || flags.has('-v')) {
|
|
|
77
73
|
}
|
|
78
74
|
process.exit(0);
|
|
79
75
|
}
|
|
80
|
-
const COMMANDS = ['init', 'receipt'
|
|
76
|
+
const COMMANDS = ['init', 'receipt'];
|
|
81
77
|
const command = COMMANDS.includes(positional[0]) ? positional[0] : undefined;
|
|
82
78
|
const cwd = resolve(positional.find(p => !COMMANDS.includes(p)) || '.');
|
|
83
79
|
const isCI = flags.has('--ci');
|
|
@@ -105,11 +101,6 @@ if (command === 'receipt') {
|
|
|
105
101
|
await runReceiptCommand(format);
|
|
106
102
|
process.exit(0);
|
|
107
103
|
}
|
|
108
|
-
if (command === 'edge') {
|
|
109
|
-
const explain = flags.has('--explain');
|
|
110
|
-
runEdgeCommand(cwd, explain);
|
|
111
|
-
process.exit(0);
|
|
112
|
-
}
|
|
113
104
|
if (!isGitRepo(cwd)) {
|
|
114
105
|
console.error(`${c.red}not a git repository${c.reset}. vet operates on git repos.`);
|
|
115
106
|
process.exit(1);
|
|
@@ -119,12 +110,10 @@ if (isFix) {
|
|
|
119
110
|
console.log(`\n ${c.bold}vet --fix${c.reset}\n`);
|
|
120
111
|
const { fixConfig } = await import('./fix/config.js');
|
|
121
112
|
const { fixModels } = await import('./fix/models.js');
|
|
122
|
-
const { fixLinks } = await import('./fix/links.js');
|
|
123
113
|
const configResult = fixConfig(cwd);
|
|
124
114
|
const modelsResult = fixModels(cwd, ignore);
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const totalFixed = configResult.fixed + modelsResult.fixed + linksResult.fixed;
|
|
115
|
+
const allMessages = [...configResult.messages, ...modelsResult.messages];
|
|
116
|
+
const totalFixed = configResult.fixed + modelsResult.fixed;
|
|
128
117
|
if (allMessages.length > 0) {
|
|
129
118
|
for (const msg of allMessages)
|
|
130
119
|
console.log(msg);
|
|
@@ -133,7 +122,7 @@ if (isFix) {
|
|
|
133
122
|
process.exit(0);
|
|
134
123
|
}
|
|
135
124
|
async function runChecks() {
|
|
136
|
-
const allChecks = ['ready', 'diff', 'models', '
|
|
125
|
+
const allChecks = ['ready', 'diff', 'models', 'config', 'history', 'scan', 'secrets', 'receipt', 'deps'];
|
|
137
126
|
const enabledChecks = config.checks || allChecks;
|
|
138
127
|
const results = [];
|
|
139
128
|
// ready and models are async (try rich subpackages first, fallback to built-in)
|
|
@@ -143,8 +132,6 @@ async function runChecks() {
|
|
|
143
132
|
results.push(checkDiff(cwd, { since }));
|
|
144
133
|
if (enabledChecks.includes('models'))
|
|
145
134
|
results.push(await checkModels(cwd, ignore));
|
|
146
|
-
if (enabledChecks.includes('links'))
|
|
147
|
-
results.push(checkLinks(cwd, ignore));
|
|
148
135
|
if (enabledChecks.includes('config'))
|
|
149
136
|
results.push(checkConfig(cwd, ignore));
|
|
150
137
|
if (enabledChecks.includes('history'))
|
|
@@ -155,8 +142,8 @@ async function runChecks() {
|
|
|
155
142
|
results.push(await checkSecrets(cwd));
|
|
156
143
|
if (enabledChecks.includes('receipt'))
|
|
157
144
|
results.push(await checkReceipt(cwd));
|
|
158
|
-
if (enabledChecks.includes('
|
|
159
|
-
results.push(
|
|
145
|
+
if (enabledChecks.includes('deps'))
|
|
146
|
+
results.push(await checkDeps(cwd));
|
|
160
147
|
return score(cwd, results);
|
|
161
148
|
}
|
|
162
149
|
// --watch mode
|