@safetnsr/vet 0.6.0 → 1.1.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 +2 -1
- package/dist/categories.d.ts +9 -0
- package/dist/categories.js +79 -0
- package/dist/checks/config.js +8 -8
- package/dist/checks/debt.js +9 -9
- package/dist/checks/deps.js +11 -9
- package/dist/checks/diff.js +4 -4
- package/dist/checks/history.js +4 -4
- package/dist/checks/integrity.d.ts +2 -0
- package/dist/checks/integrity.js +369 -0
- package/dist/checks/map.d.ts +25 -0
- package/dist/checks/map.js +256 -0
- package/dist/checks/memory.d.ts +2 -0
- package/dist/checks/memory.js +275 -0
- package/dist/checks/models.js +11 -6
- package/dist/checks/owasp-checks.d.ts +51 -0
- package/dist/checks/owasp-checks.js +670 -0
- package/dist/checks/owasp.d.ts +2 -0
- package/dist/checks/owasp.js +57 -0
- package/dist/checks/ready.js +7 -7
- package/dist/checks/receipt.js +5 -5
- package/dist/checks/scan.js +8 -36
- package/dist/checks/secrets.js +4 -4
- package/dist/cli.js +79 -47
- package/dist/reporter.d.ts +1 -0
- package/dist/reporter.js +61 -26
- package/dist/scorer.d.ts +7 -1
- package/dist/scorer.js +4 -14
- package/dist/types.d.ts +11 -1
- package/dist/util.d.ts +8 -1
- package/dist/util.js +40 -9
- package/package.json +2 -2
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
export declare const AGENT_CONFIG_FILES: string[];
|
|
3
|
+
export declare function parseAgentConfigs(cwd: string): string[];
|
|
4
|
+
export declare function extractRefs(content: string, cwd: string): string[];
|
|
5
|
+
export type VisibilityTier = 'config' | 'visible' | 'invisible';
|
|
6
|
+
export interface ClassifiedFile {
|
|
7
|
+
path: string;
|
|
8
|
+
tier: VisibilityTier;
|
|
9
|
+
}
|
|
10
|
+
export declare function classifyFiles(cwd: string, configPaths: string[], refs: string[]): ClassifiedFile[];
|
|
11
|
+
export interface MapResult {
|
|
12
|
+
config: string[];
|
|
13
|
+
visible: string[];
|
|
14
|
+
invisible: string[];
|
|
15
|
+
stats: {
|
|
16
|
+
total: number;
|
|
17
|
+
visible_pct: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export declare function checkMap(cwd: string): Promise<CheckResult & {
|
|
21
|
+
mapData: MapResult;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function renderMapReport(result: CheckResult & {
|
|
24
|
+
mapData: MapResult;
|
|
25
|
+
}, asJson: boolean): string;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { walkFiles, readFile, c } from '../util.js';
|
|
4
|
+
// ── Agent config filenames to discover ───────────────────────────────────────
|
|
5
|
+
export const AGENT_CONFIG_FILES = [
|
|
6
|
+
'CLAUDE.md',
|
|
7
|
+
'AGENTS.md',
|
|
8
|
+
'.cursorrules',
|
|
9
|
+
'codex.md',
|
|
10
|
+
'.github/copilot-instructions.md',
|
|
11
|
+
'cursor.json',
|
|
12
|
+
'.cursor/rules',
|
|
13
|
+
'copilot-instructions.md',
|
|
14
|
+
];
|
|
15
|
+
// ── Parse all agent config files present in cwd ──────────────────────────────
|
|
16
|
+
export function parseAgentConfigs(cwd) {
|
|
17
|
+
const found = [];
|
|
18
|
+
for (const name of AGENT_CONFIG_FILES) {
|
|
19
|
+
const full = join(cwd, name);
|
|
20
|
+
if (existsSync(full)) {
|
|
21
|
+
found.push(name);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
// ── Extract file/dir references from config file content ─────────────────────
|
|
27
|
+
export function extractRefs(content, cwd) {
|
|
28
|
+
const refs = new Set();
|
|
29
|
+
// Patterns to extract:
|
|
30
|
+
// 1. Backtick paths: `path/to/file.ts` or `./path`
|
|
31
|
+
const backtickPat = /`([^`\s]+)`/g;
|
|
32
|
+
// 2. Inline code in markdown: single-line code with path-like content
|
|
33
|
+
const codePat = /`([./][^`\s]+)`/g;
|
|
34
|
+
// 3. Explicit path patterns in text: word/word or ./word or ~/word
|
|
35
|
+
const pathPat = /(?:^|\s)((?:\.\/|\.\.\/|~\/)?(?:[a-zA-Z0-9_-]+\/)+[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])/gm;
|
|
36
|
+
// 4. Absolute paths starting with /
|
|
37
|
+
const absPat = /(?:^|\s)(\/(?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+)/gm;
|
|
38
|
+
const extractFromPattern = (pat) => {
|
|
39
|
+
let match;
|
|
40
|
+
pat.lastIndex = 0;
|
|
41
|
+
while ((match = pat.exec(content)) !== null) {
|
|
42
|
+
const raw = match[1].trim();
|
|
43
|
+
// Skip URLs
|
|
44
|
+
if (raw.startsWith('http://') || raw.startsWith('https://') || raw.includes('://'))
|
|
45
|
+
continue;
|
|
46
|
+
// Skip if looks like a domain
|
|
47
|
+
if (/^[a-z]+\.[a-z]{2,}/.test(raw) && !raw.includes('/'))
|
|
48
|
+
continue;
|
|
49
|
+
refs.add(raw);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
extractFromPattern(backtickPat);
|
|
53
|
+
extractFromPattern(codePat);
|
|
54
|
+
extractFromPattern(pathPat);
|
|
55
|
+
extractFromPattern(absPat);
|
|
56
|
+
// Filter to only refs that actually exist on disk (relative to cwd)
|
|
57
|
+
const resolved = [];
|
|
58
|
+
for (const ref of refs) {
|
|
59
|
+
let resolvedPath;
|
|
60
|
+
if (ref.startsWith('/')) {
|
|
61
|
+
resolvedPath = ref;
|
|
62
|
+
}
|
|
63
|
+
else if (ref.startsWith('~/')) {
|
|
64
|
+
resolvedPath = join(process.env.HOME || '/root', ref.slice(2));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
resolvedPath = join(cwd, ref);
|
|
68
|
+
}
|
|
69
|
+
if (existsSync(resolvedPath)) {
|
|
70
|
+
// Store as relative to cwd
|
|
71
|
+
const rel = ref.startsWith('/') ? ref : ref;
|
|
72
|
+
resolved.push(rel.replace(/^\.\//, ''));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...new Set(resolved)];
|
|
76
|
+
}
|
|
77
|
+
// ── Classify all codebase files ───────────────────────────────────────────────
|
|
78
|
+
export function classifyFiles(cwd, configPaths, refs) {
|
|
79
|
+
const allFiles = walkFiles(cwd);
|
|
80
|
+
const configSet = new Set(configPaths);
|
|
81
|
+
// Build a set of ref prefixes for directory matching
|
|
82
|
+
const refSet = new Set(refs);
|
|
83
|
+
// Also include files whose parent directory is referenced
|
|
84
|
+
function isReferencedByRef(file) {
|
|
85
|
+
if (refSet.has(file))
|
|
86
|
+
return true;
|
|
87
|
+
// Check if any ref is a directory prefix of this file
|
|
88
|
+
for (const ref of refSet) {
|
|
89
|
+
if (file.startsWith(ref + '/') || file.startsWith(ref.replace(/\/$/, '') + '/')) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const classified = [];
|
|
96
|
+
for (const file of allFiles) {
|
|
97
|
+
let tier;
|
|
98
|
+
if (configSet.has(file)) {
|
|
99
|
+
tier = 'config';
|
|
100
|
+
}
|
|
101
|
+
else if (isReferencedByRef(file)) {
|
|
102
|
+
tier = 'visible';
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
tier = 'invisible';
|
|
106
|
+
}
|
|
107
|
+
classified.push({ path: file, tier });
|
|
108
|
+
}
|
|
109
|
+
// Also add config files that walkFiles might have missed (e.g. .github/copilot-instructions.md)
|
|
110
|
+
for (const cp of configPaths) {
|
|
111
|
+
if (!classified.find(f => f.path === cp)) {
|
|
112
|
+
classified.push({ path: cp, tier: 'config' });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return classified;
|
|
116
|
+
}
|
|
117
|
+
// ── Main check ───────────────────────────────────────────────────────────────
|
|
118
|
+
export async function checkMap(cwd) {
|
|
119
|
+
const issues = [];
|
|
120
|
+
// Discover agent configs
|
|
121
|
+
const configPaths = parseAgentConfigs(cwd);
|
|
122
|
+
// Extract all refs from all config files
|
|
123
|
+
const allRefs = [];
|
|
124
|
+
for (const cp of configPaths) {
|
|
125
|
+
const content = readFile(join(cwd, cp));
|
|
126
|
+
if (content) {
|
|
127
|
+
const refs = extractRefs(content, cwd);
|
|
128
|
+
allRefs.push(...refs);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const uniqueRefs = [...new Set(allRefs)];
|
|
132
|
+
// Classify files
|
|
133
|
+
const classified = classifyFiles(cwd, configPaths, uniqueRefs);
|
|
134
|
+
const configFiles = classified.filter(f => f.tier === 'config').map(f => f.path);
|
|
135
|
+
const visibleFiles = classified.filter(f => f.tier === 'visible').map(f => f.path);
|
|
136
|
+
const invisibleFiles = classified.filter(f => f.tier === 'invisible').map(f => f.path);
|
|
137
|
+
const total = classified.length;
|
|
138
|
+
const visible = visibleFiles.length + configFiles.length;
|
|
139
|
+
const visible_pct = total > 0 ? Math.round((visible / total) * 100) : 0;
|
|
140
|
+
// Issues
|
|
141
|
+
if (configPaths.length === 0) {
|
|
142
|
+
issues.push({
|
|
143
|
+
severity: 'warning',
|
|
144
|
+
message: 'no agent config files found (CLAUDE.md, .cursorrules, etc.) — agent has no guided context',
|
|
145
|
+
fixable: true,
|
|
146
|
+
fixHint: 'run: npx @safetnsr/vet init',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (visible_pct < 20 && total > 0) {
|
|
150
|
+
issues.push({
|
|
151
|
+
severity: 'warning',
|
|
152
|
+
message: `agent is mostly blind: only ${visible_pct}% of codebase is referenced in agent configs`,
|
|
153
|
+
fixable: false,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// Surface top invisible dirs as info
|
|
157
|
+
const invisibleDirs = new Set();
|
|
158
|
+
for (const f of invisibleFiles) {
|
|
159
|
+
const parts = f.split('/');
|
|
160
|
+
if (parts.length > 1)
|
|
161
|
+
invisibleDirs.add(parts[0]);
|
|
162
|
+
}
|
|
163
|
+
const topInvisibleDirs = [...invisibleDirs].slice(0, 5);
|
|
164
|
+
if (topInvisibleDirs.length > 0 && invisibleFiles.length > 0) {
|
|
165
|
+
issues.push({
|
|
166
|
+
severity: 'info',
|
|
167
|
+
message: `top invisible directories: ${topInvisibleDirs.join(', ')}`,
|
|
168
|
+
fixable: false,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const mapData = {
|
|
172
|
+
config: configFiles,
|
|
173
|
+
visible: visibleFiles,
|
|
174
|
+
invisible: invisibleFiles,
|
|
175
|
+
stats: { total, visible_pct },
|
|
176
|
+
};
|
|
177
|
+
const summary = configPaths.length === 0
|
|
178
|
+
? `no agent config files — all ${total} files invisible`
|
|
179
|
+
: `${visible_pct}% visible to agent (${visible}/${total} files)`;
|
|
180
|
+
return {
|
|
181
|
+
name: 'map',
|
|
182
|
+
score: visible_pct,
|
|
183
|
+
maxScore: 100,
|
|
184
|
+
issues,
|
|
185
|
+
summary,
|
|
186
|
+
mapData,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
// ── Terminal renderer ─────────────────────────────────────────────────────────
|
|
190
|
+
export function renderMapReport(result, asJson) {
|
|
191
|
+
const { mapData } = result;
|
|
192
|
+
if (asJson) {
|
|
193
|
+
return JSON.stringify({
|
|
194
|
+
config: mapData.config,
|
|
195
|
+
visible: mapData.visible,
|
|
196
|
+
invisible: mapData.invisible,
|
|
197
|
+
stats: mapData.stats,
|
|
198
|
+
}, null, 2);
|
|
199
|
+
}
|
|
200
|
+
const lines = [];
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push(` ${c.bold}vet map${c.reset} — agent visibility\n`);
|
|
203
|
+
lines.push(` ${c.dim}score:${c.reset} ${c.bold}${mapData.stats.visible_pct}%${c.reset} visible to agent`);
|
|
204
|
+
lines.push(` ${c.dim}total:${c.reset} ${mapData.stats.total} files`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
// Config files tier
|
|
207
|
+
if (mapData.config.length > 0) {
|
|
208
|
+
lines.push(` ${c.yellow}${c.bold}config files${c.reset} ${c.dim}(${mapData.config.length})${c.reset}`);
|
|
209
|
+
for (const f of mapData.config.slice(0, 10)) {
|
|
210
|
+
lines.push(` ${c.yellow}●${c.reset} ${c.dim}${f}${c.reset}`);
|
|
211
|
+
}
|
|
212
|
+
if (mapData.config.length > 10) {
|
|
213
|
+
lines.push(` ${c.dim} ... and ${mapData.config.length - 10} more${c.reset}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push('');
|
|
216
|
+
}
|
|
217
|
+
// Visible files tier
|
|
218
|
+
if (mapData.visible.length > 0) {
|
|
219
|
+
lines.push(` ${c.green}${c.bold}visible to agent${c.reset} ${c.dim}(${mapData.visible.length})${c.reset}`);
|
|
220
|
+
for (const f of mapData.visible.slice(0, 15)) {
|
|
221
|
+
lines.push(` ${c.green}●${c.reset} ${f}`);
|
|
222
|
+
}
|
|
223
|
+
if (mapData.visible.length > 15) {
|
|
224
|
+
lines.push(` ${c.dim} ... and ${mapData.visible.length - 15} more${c.reset}`);
|
|
225
|
+
}
|
|
226
|
+
lines.push('');
|
|
227
|
+
}
|
|
228
|
+
// Invisible dirs summary
|
|
229
|
+
if (mapData.invisible.length > 0) {
|
|
230
|
+
const invisibleDirs = new Map();
|
|
231
|
+
for (const f of mapData.invisible) {
|
|
232
|
+
const parts = f.split('/');
|
|
233
|
+
const dir = parts.length > 1 ? parts[0] : '(root)';
|
|
234
|
+
invisibleDirs.set(dir, (invisibleDirs.get(dir) || 0) + 1);
|
|
235
|
+
}
|
|
236
|
+
const sortedDirs = [...invisibleDirs.entries()].sort((a, b) => b[1] - a[1]);
|
|
237
|
+
lines.push(` ${c.dim}${c.bold}invisible to agent${c.reset} ${c.dim}(${mapData.invisible.length} files)${c.reset}`);
|
|
238
|
+
for (const [dir, count] of sortedDirs.slice(0, 8)) {
|
|
239
|
+
lines.push(` ${c.dim}○ ${dir}/ (${count} files)${c.reset}`);
|
|
240
|
+
}
|
|
241
|
+
if (sortedDirs.length > 8) {
|
|
242
|
+
lines.push(` ${c.dim} ... and ${sortedDirs.length - 8} more directories${c.reset}`);
|
|
243
|
+
}
|
|
244
|
+
lines.push('');
|
|
245
|
+
}
|
|
246
|
+
// Issues
|
|
247
|
+
for (const issue of result.issues) {
|
|
248
|
+
const icon = issue.severity === 'warning' ? c.yellow + '⚠' : c.dim + 'ℹ';
|
|
249
|
+
lines.push(` ${icon}${c.reset} ${issue.message}`);
|
|
250
|
+
if (issue.fixHint)
|
|
251
|
+
lines.push(` ${c.dim}→ ${issue.fixHint}${c.reset}`);
|
|
252
|
+
}
|
|
253
|
+
if (result.issues.length > 0)
|
|
254
|
+
lines.push('');
|
|
255
|
+
return lines.join('\n');
|
|
256
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
// ── Memory file targets ──────────────────────────────────────────────────────
|
|
4
|
+
const ROOT_FILES = ['CLAUDE.md', 'AGENTS.md', 'SOUL.md', '.cursorrules', 'codex.md'];
|
|
5
|
+
const MEMORY_DIR = 'memory';
|
|
6
|
+
const DAILY_DIR = join(MEMORY_DIR, 'daily');
|
|
7
|
+
const MAX_DAILY_FILES = 30;
|
|
8
|
+
// ── Tool categories for contradiction detection ─────────────────────────────
|
|
9
|
+
const TOOL_CATEGORIES = {
|
|
10
|
+
'test framework': [/\bvitest\b/i, /\bjest\b/i, /\bmocha\b/i, /\bnode:test\b/i, /\bava\b/i],
|
|
11
|
+
'package manager': [/\bnpm\b/, /\byarn\b/, /\bpnpm\b/, /\bbun\b/],
|
|
12
|
+
};
|
|
13
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
14
|
+
function safeRead(path) {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(path, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function collectMemoryFiles(cwd) {
|
|
23
|
+
const files = [];
|
|
24
|
+
// Root-level memory files
|
|
25
|
+
for (const name of ROOT_FILES) {
|
|
26
|
+
const full = join(cwd, name);
|
|
27
|
+
if (existsSync(full))
|
|
28
|
+
files.push(full);
|
|
29
|
+
}
|
|
30
|
+
// memory/*.md
|
|
31
|
+
const memDir = join(cwd, MEMORY_DIR);
|
|
32
|
+
if (existsSync(memDir) && statSync(memDir).isDirectory()) {
|
|
33
|
+
try {
|
|
34
|
+
for (const entry of readdirSync(memDir)) {
|
|
35
|
+
if (!entry.endsWith('.md'))
|
|
36
|
+
continue;
|
|
37
|
+
const full = join(memDir, entry);
|
|
38
|
+
try {
|
|
39
|
+
if (statSync(full).isFile())
|
|
40
|
+
files.push(full);
|
|
41
|
+
}
|
|
42
|
+
catch { /* skip */ }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch { /* skip */ }
|
|
46
|
+
}
|
|
47
|
+
// memory/daily/*.md (capped)
|
|
48
|
+
const dailyDir = join(cwd, DAILY_DIR);
|
|
49
|
+
if (existsSync(dailyDir) && statSync(dailyDir).isDirectory()) {
|
|
50
|
+
try {
|
|
51
|
+
const entries = readdirSync(dailyDir).filter(e => e.endsWith('.md')).sort().reverse();
|
|
52
|
+
for (const entry of entries.slice(0, MAX_DAILY_FILES)) {
|
|
53
|
+
const full = join(dailyDir, entry);
|
|
54
|
+
try {
|
|
55
|
+
if (statSync(full).isFile())
|
|
56
|
+
files.push(full);
|
|
57
|
+
}
|
|
58
|
+
catch { /* skip */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch { /* skip */ }
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
/** Extract @scope/package references */
|
|
66
|
+
function extractScopedPackages(content) {
|
|
67
|
+
const results = [];
|
|
68
|
+
const lines = content.split('\n');
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
const matches = lines[i].matchAll(/@[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+/g);
|
|
71
|
+
for (const m of matches) {
|
|
72
|
+
results.push({ pkg: m[0], line: i + 1 });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results;
|
|
76
|
+
}
|
|
77
|
+
/** Extract file/dir path references */
|
|
78
|
+
function extractPaths(content) {
|
|
79
|
+
const results = [];
|
|
80
|
+
const lines = content.split('\n');
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const line = lines[i];
|
|
83
|
+
// Skip lines that are purely URLs
|
|
84
|
+
// Match absolute paths
|
|
85
|
+
const absMatches = line.matchAll(/(?:^|\s|["`'(])(\/((?:var|home|usr|etc|opt|tmp|root|srv|mnt)[^\s"'`),;]*))(?=[\s"'`),;]|$)/g);
|
|
86
|
+
for (const m of absMatches) {
|
|
87
|
+
const p = m[1].replace(/[.,:;)]+$/, '');
|
|
88
|
+
if (p.startsWith('//') || p.includes('://'))
|
|
89
|
+
continue;
|
|
90
|
+
if (p.length < 4)
|
|
91
|
+
continue;
|
|
92
|
+
results.push({ path: p, line: i + 1 });
|
|
93
|
+
}
|
|
94
|
+
// Match relative paths starting with ./ or ../
|
|
95
|
+
const relMatches = line.matchAll(/(?:^|\s|["`'(])(\.\.?\/[^\s"'`),;]+)/g);
|
|
96
|
+
for (const m of relMatches) {
|
|
97
|
+
const p = m[1].replace(/[.,:;)]+$/, '');
|
|
98
|
+
if (p.includes('://'))
|
|
99
|
+
continue;
|
|
100
|
+
results.push({ path: p, line: i + 1 });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return results;
|
|
104
|
+
}
|
|
105
|
+
/** Extract tool mentions per category */
|
|
106
|
+
function extractToolMentions(content, fileName) {
|
|
107
|
+
const mentions = new Map();
|
|
108
|
+
const lines = content.split('\n');
|
|
109
|
+
for (const [category, patterns] of Object.entries(TOOL_CATEGORIES)) {
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
for (const regex of patterns) {
|
|
112
|
+
if (regex.test(lines[i])) {
|
|
113
|
+
const toolMatch = lines[i].match(regex);
|
|
114
|
+
if (toolMatch) {
|
|
115
|
+
const existing = mentions.get(category) || [];
|
|
116
|
+
existing.push({ tool: toolMatch[0].toLowerCase(), file: fileName, line: i + 1 });
|
|
117
|
+
mentions.set(category, existing);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return mentions;
|
|
124
|
+
}
|
|
125
|
+
/** Count meaningful fact claims in a file */
|
|
126
|
+
function countFacts(content) {
|
|
127
|
+
let count = 0;
|
|
128
|
+
const lines = content.split('\n');
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
const trimmed = line.trim();
|
|
131
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('---') || trimmed.startsWith('```'))
|
|
132
|
+
continue;
|
|
133
|
+
// A "fact" is a line with at least some substantive content
|
|
134
|
+
if (trimmed.length > 15 && /[a-zA-Z]/.test(trimmed)) {
|
|
135
|
+
// Contains a keyword-like pattern (assignment, instruction, reference)
|
|
136
|
+
if (/[:=→\->]|use |stack|requires?|install|run |npm |config|version|path|file|dir|tool/i.test(trimmed)) {
|
|
137
|
+
count++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return count;
|
|
142
|
+
}
|
|
143
|
+
// ── Main check ───────────────────────────────────────────────────────────────
|
|
144
|
+
export function checkMemory(cwd) {
|
|
145
|
+
const memoryFiles = collectMemoryFiles(cwd);
|
|
146
|
+
const issues = [];
|
|
147
|
+
let deductions = 0;
|
|
148
|
+
if (memoryFiles.length === 0) {
|
|
149
|
+
return {
|
|
150
|
+
name: 'memory',
|
|
151
|
+
score: 100,
|
|
152
|
+
maxScore: 100,
|
|
153
|
+
issues: [],
|
|
154
|
+
summary: 'no agent memory files found',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Load package.json deps
|
|
158
|
+
const pkgPath = join(cwd, 'package.json');
|
|
159
|
+
const allDeps = new Set();
|
|
160
|
+
if (existsSync(pkgPath)) {
|
|
161
|
+
try {
|
|
162
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
163
|
+
for (const key of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
164
|
+
if (pkg[key]) {
|
|
165
|
+
for (const name of Object.keys(pkg[key])) {
|
|
166
|
+
allDeps.add(name);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch { /* skip */ }
|
|
172
|
+
}
|
|
173
|
+
// Collect all tool mentions across files for contradiction detection
|
|
174
|
+
const globalToolMentions = new Map();
|
|
175
|
+
for (const filePath of memoryFiles) {
|
|
176
|
+
const content = safeRead(filePath);
|
|
177
|
+
if (!content)
|
|
178
|
+
continue;
|
|
179
|
+
const relPath = filePath.startsWith(cwd) ? filePath.slice(cwd.length + 1) : filePath;
|
|
180
|
+
// 1. Stale package references
|
|
181
|
+
if (allDeps.size > 0) {
|
|
182
|
+
const pkgRefs = extractScopedPackages(content);
|
|
183
|
+
for (const { pkg, line } of pkgRefs) {
|
|
184
|
+
if (!allDeps.has(pkg)) {
|
|
185
|
+
issues.push({
|
|
186
|
+
severity: 'warning',
|
|
187
|
+
message: `Stale package: ${pkg} not in package.json`,
|
|
188
|
+
file: relPath,
|
|
189
|
+
line,
|
|
190
|
+
fixable: false,
|
|
191
|
+
fixHint: 'Remove or update this reference',
|
|
192
|
+
});
|
|
193
|
+
deductions += 10;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// 2. Broken path references
|
|
198
|
+
const pathRefs = extractPaths(content);
|
|
199
|
+
for (const { path: p, line } of pathRefs) {
|
|
200
|
+
const resolved = p.startsWith('/') ? p : resolve(cwd, p);
|
|
201
|
+
if (!existsSync(resolved)) {
|
|
202
|
+
issues.push({
|
|
203
|
+
severity: 'error',
|
|
204
|
+
message: `Broken path reference: ${p}`,
|
|
205
|
+
file: relPath,
|
|
206
|
+
line,
|
|
207
|
+
fixable: false,
|
|
208
|
+
fixHint: 'Remove or update this path reference',
|
|
209
|
+
});
|
|
210
|
+
deductions += 15;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 3. Collect tool mentions for contradiction check
|
|
214
|
+
const toolMentions = extractToolMentions(content, relPath);
|
|
215
|
+
for (const [category, mentions] of toolMentions) {
|
|
216
|
+
const existing = globalToolMentions.get(category) || [];
|
|
217
|
+
existing.push(...mentions);
|
|
218
|
+
globalToolMentions.set(category, existing);
|
|
219
|
+
}
|
|
220
|
+
// 4. Bloat check
|
|
221
|
+
if (content.length > 5000) {
|
|
222
|
+
const factCount = countFacts(content);
|
|
223
|
+
if (factCount < 3) {
|
|
224
|
+
issues.push({
|
|
225
|
+
severity: 'info',
|
|
226
|
+
message: `Bloated memory file: ${content.length} chars but only ${factCount} fact claims`,
|
|
227
|
+
file: relPath,
|
|
228
|
+
line: 1,
|
|
229
|
+
fixable: false,
|
|
230
|
+
fixHint: 'Trim this file to only essential facts',
|
|
231
|
+
});
|
|
232
|
+
deductions += 5;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// 3. Contradiction detection
|
|
237
|
+
for (const [category, mentions] of globalToolMentions) {
|
|
238
|
+
const uniqueTools = new Map();
|
|
239
|
+
for (const m of mentions) {
|
|
240
|
+
if (!uniqueTools.has(m.tool)) {
|
|
241
|
+
uniqueTools.set(m.tool, { file: m.file, line: m.line });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (uniqueTools.size > 1) {
|
|
245
|
+
const tools = [...uniqueTools.entries()];
|
|
246
|
+
for (let i = 0; i < tools.length; i++) {
|
|
247
|
+
for (let j = i + 1; j < tools.length; j++) {
|
|
248
|
+
// Only flag if they're in different files
|
|
249
|
+
if (tools[i][1].file !== tools[j][1].file) {
|
|
250
|
+
issues.push({
|
|
251
|
+
severity: 'warning',
|
|
252
|
+
message: `Contradiction in ${category}: "${tools[i][0]}" in ${tools[i][1].file} vs "${tools[j][0]}" in ${tools[j][1].file}`,
|
|
253
|
+
file: tools[i][1].file,
|
|
254
|
+
line: tools[i][1].line,
|
|
255
|
+
fixable: false,
|
|
256
|
+
fixHint: `Standardize on one ${category} across memory files`,
|
|
257
|
+
});
|
|
258
|
+
deductions += 10;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const finalScore = Math.max(0, 100 - deductions);
|
|
265
|
+
const issueCount = issues.length;
|
|
266
|
+
return {
|
|
267
|
+
name: 'memory',
|
|
268
|
+
score: finalScore,
|
|
269
|
+
maxScore: 100,
|
|
270
|
+
issues,
|
|
271
|
+
summary: issueCount === 0
|
|
272
|
+
? `${memoryFiles.length} memory file${memoryFiles.length !== 1 ? 's' : ''} scanned, clean`
|
|
273
|
+
: `${issueCount} stale fact${issueCount !== 1 ? 's' : ''} found in agent memory files`,
|
|
274
|
+
};
|
|
275
|
+
}
|
package/dist/checks/models.js
CHANGED
|
@@ -8,9 +8,14 @@ async function tryModelGraveyard(cwd) {
|
|
|
8
8
|
return null;
|
|
9
9
|
const report = await mod.scan(cwd);
|
|
10
10
|
const issues = [];
|
|
11
|
+
// Files that define deprecated model registries should not be flagged
|
|
12
|
+
const SELF_FILES = ['models.ts', 'models.js', 'model-graveyard', 'model-registry', 'sunset', 'fix/models'];
|
|
11
13
|
for (const match of report.matches) {
|
|
12
14
|
if (!match.model)
|
|
13
15
|
continue;
|
|
16
|
+
// Skip self-referencing files (model definition/fix files)
|
|
17
|
+
if (match.file && SELF_FILES.some(s => match.file.toLowerCase().includes(s)))
|
|
18
|
+
continue;
|
|
14
19
|
if (match.model.status === 'deprecated' || match.model.status === 'eol') {
|
|
15
20
|
issues.push({
|
|
16
21
|
severity: 'error',
|
|
@@ -22,11 +27,11 @@ async function tryModelGraveyard(cwd) {
|
|
|
22
27
|
});
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
|
-
const score = Math.max(0,
|
|
30
|
+
const score = Math.max(0, 100 - issues.length * 20);
|
|
26
31
|
return {
|
|
27
32
|
name: 'models',
|
|
28
|
-
score: Math.min(
|
|
29
|
-
maxScore:
|
|
33
|
+
score: Math.min(100, score),
|
|
34
|
+
maxScore: 100,
|
|
30
35
|
issues,
|
|
31
36
|
summary: issues.length === 0
|
|
32
37
|
? `${report.filesScanned} files scanned (via model-graveyard) — all current`
|
|
@@ -111,11 +116,11 @@ function builtinModels(cwd, ignore) {
|
|
|
111
116
|
fixHint: `replace "${model}" with "${info.replacement}"`,
|
|
112
117
|
});
|
|
113
118
|
}
|
|
114
|
-
const score = Math.max(0,
|
|
119
|
+
const score = Math.max(0, 100 - issues.length * 20);
|
|
115
120
|
return {
|
|
116
121
|
name: 'models',
|
|
117
|
-
score: Math.min(
|
|
118
|
-
maxScore:
|
|
122
|
+
score: Math.min(100, score),
|
|
123
|
+
maxScore: 100,
|
|
119
124
|
issues,
|
|
120
125
|
summary: issues.length === 0 ? 'all model references current' : `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} found`,
|
|
121
126
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export declare function collectAgentConfigFiles(cwd: string): string[];
|
|
2
|
+
export declare function collectMcpConfigFiles(cwd: string): string[];
|
|
3
|
+
export declare function readTextFile(filePath: string): string | null;
|
|
4
|
+
export interface OwaspFinding {
|
|
5
|
+
asiId: string;
|
|
6
|
+
severity: 'error' | 'warning' | 'info';
|
|
7
|
+
message: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
line?: number;
|
|
10
|
+
fixHint?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function checkASI01(cwd: string, configFiles: string[]): {
|
|
13
|
+
findings: OwaspFinding[];
|
|
14
|
+
deduction: number;
|
|
15
|
+
};
|
|
16
|
+
export declare function checkASI02(cwd: string, mcpFiles: string[]): {
|
|
17
|
+
findings: OwaspFinding[];
|
|
18
|
+
deduction: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function checkASI03(cwd: string, configFiles: string[]): {
|
|
21
|
+
findings: OwaspFinding[];
|
|
22
|
+
deduction: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function checkASI04(cwd: string, mcpFiles: string[], agentConfigFiles: string[]): {
|
|
25
|
+
findings: OwaspFinding[];
|
|
26
|
+
deduction: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function checkASI05(cwd: string, configFiles: string[]): {
|
|
29
|
+
findings: OwaspFinding[];
|
|
30
|
+
deduction: number;
|
|
31
|
+
};
|
|
32
|
+
export declare function checkASI06(cwd: string): {
|
|
33
|
+
findings: OwaspFinding[];
|
|
34
|
+
deduction: number;
|
|
35
|
+
};
|
|
36
|
+
export declare function checkASI07(cwd: string, configFiles: string[]): {
|
|
37
|
+
findings: OwaspFinding[];
|
|
38
|
+
deduction: number;
|
|
39
|
+
};
|
|
40
|
+
export declare function checkASI08(cwd: string, configFiles: string[]): {
|
|
41
|
+
findings: OwaspFinding[];
|
|
42
|
+
deduction: number;
|
|
43
|
+
};
|
|
44
|
+
export declare function checkASI09(cwd: string, configFiles: string[]): {
|
|
45
|
+
findings: OwaspFinding[];
|
|
46
|
+
deduction: number;
|
|
47
|
+
};
|
|
48
|
+
export declare function checkASI10(cwd: string, configFiles: string[]): {
|
|
49
|
+
findings: OwaspFinding[];
|
|
50
|
+
deduction: number;
|
|
51
|
+
};
|