agentaudit 3.10.10 → 3.12.1
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/cli.mjs +1200 -132
- package/index.mjs +795 -659
- package/package.json +5 -1
- package/scan-tool-poisoning.mjs +297 -0
- package/tool-poisoning-detector.mjs +913 -0
package/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Usage: agentaudit <command> [options]
|
|
6
6
|
*
|
|
7
7
|
* Commands:
|
|
8
|
-
* discover Find MCP servers
|
|
8
|
+
* discover Find MCP servers across all AI tools
|
|
9
9
|
* scan <url> [url...] Quick static scan (regex)
|
|
10
10
|
* audit <url> [url...] Deep LLM-powered security audit
|
|
11
11
|
* lookup <name> Look up package in registry
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* model [name|reset] Configure LLM provider + model
|
|
18
18
|
* setup Log in to agentaudit.dev (for report uploads)
|
|
19
19
|
* status Show current config + auth status
|
|
20
|
+
* profile Your profile — rank, points, audit stats
|
|
20
21
|
* help [command] Show help
|
|
21
22
|
*
|
|
22
23
|
* Flags: --json, --quiet, --no-color, --no-upload, --model, --export, --debug
|
|
@@ -554,7 +555,7 @@ function banner() {
|
|
|
554
555
|
const ptsStr = `${fmtNum(cache.total_points)}pts`;
|
|
555
556
|
const auditsStr = `${fmtNum(cache.total_reports)} audits`;
|
|
556
557
|
const profile = [cache.agent_name, rankStr, ptsStr, auditsStr].filter(Boolean).join(' \u00b7 ');
|
|
557
|
-
console.log(` ${c.bold}${c.cyan}\
|
|
558
|
+
console.log(` ${c.bold}${c.cyan}\u25c6 AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset} ${c.dim}\u2502${c.reset} ${profile}`);
|
|
558
559
|
} else {
|
|
559
560
|
console.log(` ${c.bold}${c.cyan}AgentAudit${c.reset} ${c.dim}v${getVersion()}${c.reset}`);
|
|
560
561
|
console.log(` ${c.dim}Security scanner for AI packages${c.reset}`);
|
|
@@ -575,10 +576,14 @@ function elapsed(startMs) {
|
|
|
575
576
|
}
|
|
576
577
|
|
|
577
578
|
function riskBadge(score) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
579
|
+
const badge = score === 0 ? `${c.bgGreen}${c.bold}${c.white} SAFE ${c.reset}`
|
|
580
|
+
: score <= 10 ? `${c.bgGreen}${c.white} LOW ${c.reset}`
|
|
581
|
+
: score <= 30 ? `${c.bgYellow}${c.bold} CAUTION ${c.reset}`
|
|
582
|
+
: `${c.bgRed}${c.bold}${c.white} UNSAFE ${c.reset}`;
|
|
583
|
+
const filled = Math.min(Math.round(score / 20), 5);
|
|
584
|
+
const gaugeColor = score <= 10 ? c.green : score <= 30 ? c.yellow : c.red;
|
|
585
|
+
const gauge = `${gaugeColor}${'▰'.repeat(filled)}${c.dim}${'▱'.repeat(5 - filled)}${c.reset}`;
|
|
586
|
+
return `${badge} ${gauge} ${score}/100`;
|
|
582
587
|
}
|
|
583
588
|
|
|
584
589
|
function severityColor(sev) {
|
|
@@ -638,15 +643,17 @@ function padLeft(str, len) {
|
|
|
638
643
|
|
|
639
644
|
function drawBox(title, contentLines, width) {
|
|
640
645
|
const inner = width - 4; // 2 for "│ " + 2 for " │"
|
|
646
|
+
const totalDash = inner + 2; // total horizontal line chars between corners
|
|
641
647
|
const lines = [];
|
|
642
648
|
const titleStr = title ? ` ${title} ` : '';
|
|
643
649
|
const titleLen = visLen(titleStr);
|
|
644
|
-
|
|
645
|
-
|
|
650
|
+
// Top: ╭─ Title ────────────╮ (1 dash before title + title + remaining dashes)
|
|
651
|
+
const topDash = BOX.h.repeat(Math.max(1, totalDash - 1 - titleLen));
|
|
652
|
+
lines.push(` ${BOX.tl}${c.dim}${BOX.h}${c.reset}${c.bold}${titleStr}${c.reset}${c.dim}${topDash}${c.reset}${BOX.tr}`);
|
|
646
653
|
for (const line of contentLines) {
|
|
647
654
|
lines.push(` ${BOX.v} ${padRight(line, inner + 1)}${BOX.v}`);
|
|
648
655
|
}
|
|
649
|
-
lines.push(` ${BOX.bl}${c.dim}${BOX.h.repeat(
|
|
656
|
+
lines.push(` ${BOX.bl}${c.dim}${BOX.h.repeat(totalDash)}${c.reset}${BOX.br}`);
|
|
650
657
|
return lines;
|
|
651
658
|
}
|
|
652
659
|
|
|
@@ -695,6 +702,47 @@ function sparkline(values) {
|
|
|
695
702
|
}).join('');
|
|
696
703
|
}
|
|
697
704
|
|
|
705
|
+
// ─── Section Header ─────────── labeled divider
|
|
706
|
+
function sectionHeader(title, width = 60) {
|
|
707
|
+
const dashAfter = Math.max(3, width - 5 - title.length);
|
|
708
|
+
return ` ${c.dim}───${c.reset} ${c.bold}${title}${c.reset} ${c.dim}${'─'.repeat(dashAfter)}${c.reset}`;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// █████░░░░░░░░░░░░░░ coverage bar
|
|
712
|
+
function coverageBar(filled, total, width = 20) {
|
|
713
|
+
if (total === 0) return `${c.dim}${'░'.repeat(width)}${c.reset} 0/0`;
|
|
714
|
+
const barFilled = Math.max(filled > 0 ? 1 : 0, Math.round((filled / total) * width));
|
|
715
|
+
const barEmpty = width - barFilled;
|
|
716
|
+
const pct = Math.round((filled / total) * 100);
|
|
717
|
+
const color = pct >= 80 ? c.green : pct >= 50 ? c.yellow : c.red;
|
|
718
|
+
return `${color}${'█'.repeat(barFilled)}${c.dim}${'░'.repeat(barEmpty)}${c.reset} ${filled}/${total} (${pct}%)`;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Severity histogram for findings
|
|
722
|
+
function severityHistogram(findings) {
|
|
723
|
+
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
724
|
+
for (const f of findings) {
|
|
725
|
+
const sev = (f.severity || '').toLowerCase();
|
|
726
|
+
if (counts[sev] !== undefined) counts[sev]++;
|
|
727
|
+
}
|
|
728
|
+
const max = Math.max(...Object.values(counts), 1);
|
|
729
|
+
const lines = [];
|
|
730
|
+
for (const sev of ['critical', 'high', 'medium', 'low']) {
|
|
731
|
+
const count = counts[sev];
|
|
732
|
+
if (count === 0) continue;
|
|
733
|
+
const barLen = Math.max(1, Math.round((count / max) * 24));
|
|
734
|
+
const sc = severityColor(sev);
|
|
735
|
+
const label = sev.toUpperCase().padEnd(10);
|
|
736
|
+
lines.push(` ${sc}${label}${c.reset} ${sc}${'█'.repeat(barLen)}${c.reset}${' '.repeat(24 - barLen)} ${count}`);
|
|
737
|
+
}
|
|
738
|
+
return lines;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ▰▰▰▱ step progress indicator
|
|
742
|
+
function stepProgress(current, total) {
|
|
743
|
+
return `${c.cyan}${'▰'.repeat(current)}${c.dim}${'▱'.repeat(total - current)}${c.reset}`;
|
|
744
|
+
}
|
|
745
|
+
|
|
698
746
|
function fmtNum(n) {
|
|
699
747
|
if (n == null) return '0';
|
|
700
748
|
return n.toLocaleString('en-US');
|
|
@@ -709,7 +757,7 @@ function dashboardBanner() {
|
|
|
709
757
|
const ver = getVersion();
|
|
710
758
|
return [
|
|
711
759
|
` ${BOX.tl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.tr}`,
|
|
712
|
-
` ${BOX.v} ${c.bold}${c.cyan}
|
|
760
|
+
` ${BOX.v} ${c.bold}${c.cyan}◆ AgentAudit${c.reset} ${c.dim}v${ver}${c.reset}${' '.repeat(Math.max(0, 19 - ver.length))}${BOX.v}`,
|
|
713
761
|
` ${BOX.v} ${c.dim}Security Registry for AI Agents${c.reset} ${BOX.v}`,
|
|
714
762
|
` ${BOX.bl}${c.dim}${BOX.h.repeat(35)}${c.reset}${BOX.br}`,
|
|
715
763
|
];
|
|
@@ -883,24 +931,38 @@ function detectPackageInfo(repoPath, files) {
|
|
|
883
931
|
info.type = 'library';
|
|
884
932
|
}
|
|
885
933
|
|
|
886
|
-
// Extract MCP tools
|
|
934
|
+
// Extract MCP tools — only from files that reference MCP SDK
|
|
935
|
+
const mcpKeywords = ['modelcontextprotocol', 'FastMCP', 'mcp.server', 'mcp_server', '@mcp.tool', '@server.tool', '.tool(', 'ListTools', 'CallTool'];
|
|
936
|
+
const mcpFiles = files.filter(f => mcpKeywords.some(kw => f.content.includes(kw)));
|
|
937
|
+
// Fallback: if no MCP-specific files found, try entrypoint files
|
|
938
|
+
if (mcpFiles.length === 0) {
|
|
939
|
+
const entryNames = ['index.js', 'index.ts', 'index.mjs', 'main.py', 'server.py', 'app.py', 'src/index.ts', 'src/main.ts', 'src/index.js'];
|
|
940
|
+
for (const f of files) {
|
|
941
|
+
if (entryNames.includes(f.path)) mcpFiles.push(f);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
887
945
|
const toolPatterns = [
|
|
888
|
-
// JS/TS
|
|
889
|
-
|
|
890
|
-
// Python: @mcp.tool()
|
|
891
|
-
|
|
892
|
-
//
|
|
893
|
-
/
|
|
946
|
+
// JS/TS MCP SDK: server.tool('name', ...) or .setTool('name', ...)
|
|
947
|
+
/\.tool\s*\(\s*['"]([a-z_][a-z0-9_]*)['"]/gi,
|
|
948
|
+
// Python: @mcp.tool() / @server.tool() followed by def name
|
|
949
|
+
/@(?:mcp|server)\.tool\s*\(.*?\)[\s\S]*?def\s+([a-z_][a-z0-9_]*)/gi,
|
|
950
|
+
// Python: Tool(name="xxx")
|
|
951
|
+
/Tool\s*\(\s*name\s*=\s*['"]([a-z_][a-z0-9_]*)['"]/gi,
|
|
952
|
+
// ListTools handler: { name: "tool_name", description: ... }
|
|
953
|
+
/{\s*(?:['"]?)name(?:['"]?)\s*:\s*['"]([a-z_][a-z0-9_]*)['"]\s*,\s*(?:['"]?)description(?:['"]?)\s*:/gi,
|
|
894
954
|
];
|
|
895
|
-
|
|
955
|
+
|
|
956
|
+
const toolBlacklist = new Set(['type', 'name', 'string', 'object', 'number', 'boolean', 'array', 'required', 'description', 'default', 'null', 'true', 'false', 'none', 'test', 'self', 'args', 'kwargs', 'input', 'output', 'result', 'data', 'error', 'value', 'index', 'item', 'list', 'dict', 'set', 'map', 'key', 'url', 'env', 'config', 'options']);
|
|
957
|
+
|
|
896
958
|
const toolSet = new Set();
|
|
897
|
-
for (const file of
|
|
959
|
+
for (const file of mcpFiles) {
|
|
898
960
|
for (const pattern of toolPatterns) {
|
|
899
961
|
pattern.lastIndex = 0;
|
|
900
962
|
let m;
|
|
901
963
|
while ((m = pattern.exec(file.content)) !== null) {
|
|
902
964
|
const name = m[1] || m[2];
|
|
903
|
-
if (name && name.length > 2 && name.length < 50 && !
|
|
965
|
+
if (name && name.length > 2 && name.length < 50 && !toolBlacklist.has(name)) {
|
|
904
966
|
toolSet.add(name);
|
|
905
967
|
}
|
|
906
968
|
}
|
|
@@ -1120,31 +1182,38 @@ function printScanResult(url, info, files, findings, registryData, duration) {
|
|
|
1120
1182
|
console.log(`${icons.pipe} ${c.dim}(no tools or prompts detected)${c.reset}`);
|
|
1121
1183
|
}
|
|
1122
1184
|
|
|
1123
|
-
// Findings
|
|
1185
|
+
// Findings with severity stripe
|
|
1124
1186
|
if (findings.length > 0) {
|
|
1125
|
-
console.log(
|
|
1126
|
-
console.log(
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
const branch = isLast ? icons.treeLast : icons.tree;
|
|
1131
|
-
const pipeOrSpace = isLast ? ' ' : `${icons.pipe} `;
|
|
1187
|
+
console.log();
|
|
1188
|
+
console.log(sectionHeader(`Findings (${findings.length})`));
|
|
1189
|
+
console.log(` ${c.dim}static analysis — may include false positives${c.reset}`);
|
|
1190
|
+
console.log();
|
|
1191
|
+
for (const f of findings) {
|
|
1132
1192
|
const sc = severityColor(f.severity);
|
|
1133
|
-
console.log(
|
|
1134
|
-
console.log(
|
|
1193
|
+
console.log(` ${sc}┃${c.reset} ${sc}${f.severity.toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
|
|
1194
|
+
console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}:${f.line}${c.reset} ${c.dim}${f.snippet || ''}${c.reset}`);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// Severity histogram
|
|
1198
|
+
const histLines = severityHistogram(findings);
|
|
1199
|
+
if (histLines.length > 1) {
|
|
1200
|
+
console.log();
|
|
1201
|
+
console.log(sectionHeader('Severity'));
|
|
1202
|
+
for (const line of histLines) console.log(line);
|
|
1135
1203
|
}
|
|
1136
1204
|
}
|
|
1137
|
-
|
|
1205
|
+
|
|
1138
1206
|
// Registry status
|
|
1139
|
-
console.log(
|
|
1207
|
+
console.log();
|
|
1208
|
+
console.log(sectionHeader('Registry'));
|
|
1140
1209
|
if (registryData) {
|
|
1141
1210
|
const rd = registryData;
|
|
1142
1211
|
const riskScore = rd.risk_score ?? rd.latest_risk_score ?? 0;
|
|
1143
|
-
console.log(
|
|
1212
|
+
console.log(` ${riskBadge(riskScore)} ${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
|
|
1144
1213
|
} else {
|
|
1145
|
-
console.log(
|
|
1214
|
+
console.log(` ${c.dim}not audited yet${c.reset}`);
|
|
1146
1215
|
}
|
|
1147
|
-
|
|
1216
|
+
|
|
1148
1217
|
console.log();
|
|
1149
1218
|
}
|
|
1150
1219
|
|
|
@@ -1153,27 +1222,23 @@ function printSummary(results) {
|
|
|
1153
1222
|
const safe = results.filter(r => r.findings.length === 0).length;
|
|
1154
1223
|
const withFindings = total - safe;
|
|
1155
1224
|
const totalFindings = results.reduce((sum, r) => sum + r.findings.length, 0);
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
console.log(`
|
|
1225
|
+
const allFindings = results.flatMap(r => r.findings);
|
|
1226
|
+
|
|
1227
|
+
console.log(sectionHeader(`Summary — ${total} packages scanned`));
|
|
1159
1228
|
console.log();
|
|
1160
1229
|
if (safe > 0) console.log(` ${icons.safe} ${c.green}${safe} clean${c.reset}`);
|
|
1161
1230
|
if (withFindings > 0) console.log(` ${icons.caution} ${c.yellow}${withFindings} with findings${c.reset} (${totalFindings} total)`);
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (Object.keys(bySev).length > 0) {
|
|
1231
|
+
console.log();
|
|
1232
|
+
console.log(` ${coverageBar(safe, total)}`);
|
|
1233
|
+
|
|
1234
|
+
// Severity histogram
|
|
1235
|
+
const histLines = severityHistogram(allFindings);
|
|
1236
|
+
if (histLines.length > 0) {
|
|
1169
1237
|
console.log();
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
console.log(` ${severityIcon(sev)} ${bySev[sev]}× ${severityColor(sev)}${sev}${c.reset}`);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1238
|
+
console.log(sectionHeader('Severity'));
|
|
1239
|
+
for (const line of histLines) console.log(line);
|
|
1175
1240
|
}
|
|
1176
|
-
|
|
1241
|
+
|
|
1177
1242
|
console.log();
|
|
1178
1243
|
}
|
|
1179
1244
|
|
|
@@ -1230,51 +1295,522 @@ async function scanRepo(url) {
|
|
|
1230
1295
|
|
|
1231
1296
|
// ── Discover local MCP configs ──────────────────────────
|
|
1232
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Minimal YAML parser — extracts MCP server list entries from
|
|
1300
|
+
* Continue.dev (mcpServers: list) and Goose (extensions: list).
|
|
1301
|
+
* Zero dependencies. Only handles the subset of YAML used by these tools.
|
|
1302
|
+
*/
|
|
1303
|
+
function parseSimpleYaml(text, rootKey) {
|
|
1304
|
+
const result = { mcpServers: {} };
|
|
1305
|
+
const lines = text.split('\n');
|
|
1306
|
+
let inSection = false;
|
|
1307
|
+
let currentName = null;
|
|
1308
|
+
let currentServer = {};
|
|
1309
|
+
let collectingArgs = false;
|
|
1310
|
+
let argsIndent = -1;
|
|
1311
|
+
|
|
1312
|
+
for (const line of lines) {
|
|
1313
|
+
const trimmed = line.trimEnd();
|
|
1314
|
+
if (trimmed === '' || /^\s*#/.test(trimmed)) continue;
|
|
1315
|
+
const indent = line.search(/\S/);
|
|
1316
|
+
|
|
1317
|
+
if (indent === 0 && trimmed === rootKey + ':') { inSection = true; continue; }
|
|
1318
|
+
if (indent === 0 && inSection && /^\w/.test(trimmed)) {
|
|
1319
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
if (!inSection) continue;
|
|
1323
|
+
|
|
1324
|
+
const nameMatch = trimmed.match(/^\s*-\s+name:\s*(.+)/);
|
|
1325
|
+
if (nameMatch) {
|
|
1326
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1327
|
+
currentName = nameMatch[1].replace(/^["']|["']$/g, '');
|
|
1328
|
+
currentServer = {};
|
|
1329
|
+
collectingArgs = false;
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
if (collectingArgs && indent > argsIndent) {
|
|
1334
|
+
const argVal = trimmed.match(/^\s*-\s+(.+)/);
|
|
1335
|
+
if (argVal) {
|
|
1336
|
+
if (!currentServer.args) currentServer.args = [];
|
|
1337
|
+
currentServer.args.push(argVal[1].replace(/^["']|["']$/g, ''));
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (collectingArgs && indent <= argsIndent) collectingArgs = false;
|
|
1342
|
+
if (!currentName) continue;
|
|
1343
|
+
|
|
1344
|
+
const kvMatch = trimmed.match(/^\s+(command|cmd|type|url):\s*(.+)/);
|
|
1345
|
+
if (kvMatch) {
|
|
1346
|
+
collectingArgs = false;
|
|
1347
|
+
const key = kvMatch[1] === 'cmd' ? 'command' : kvMatch[1];
|
|
1348
|
+
currentServer[key] = kvMatch[2].replace(/^["']|["']$/g, '');
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const argsMatch = trimmed.match(/^\s+(args):\s*(.*)/);
|
|
1353
|
+
if (argsMatch) {
|
|
1354
|
+
const inlineArr = argsMatch[2].match(/^\[(.+)\]$/);
|
|
1355
|
+
if (inlineArr) {
|
|
1356
|
+
currentServer.args = inlineArr[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
1357
|
+
collectingArgs = false;
|
|
1358
|
+
} else {
|
|
1359
|
+
collectingArgs = true;
|
|
1360
|
+
argsIndent = indent;
|
|
1361
|
+
currentServer.args = [];
|
|
1362
|
+
}
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1367
|
+
return result;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
/**
|
|
1371
|
+
* Minimal TOML parser — extracts [mcp_servers.xxx] sections
|
|
1372
|
+
* from OpenAI Codex CLI config. Zero dependencies.
|
|
1373
|
+
*/
|
|
1374
|
+
function parseSimpleToml(text) {
|
|
1375
|
+
const result = { mcpServers: {} };
|
|
1376
|
+
const lines = text.split('\n');
|
|
1377
|
+
let currentName = null;
|
|
1378
|
+
let currentServer = {};
|
|
1379
|
+
|
|
1380
|
+
for (const line of lines) {
|
|
1381
|
+
const trimmed = line.trim();
|
|
1382
|
+
if (trimmed === '' || trimmed.startsWith('#')) continue;
|
|
1383
|
+
|
|
1384
|
+
const sectionMatch = trimmed.match(/^\[mcp_servers\.(.+)\]$/);
|
|
1385
|
+
if (sectionMatch) {
|
|
1386
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1387
|
+
currentName = sectionMatch[1];
|
|
1388
|
+
currentServer = {};
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
if (trimmed.startsWith('[') && !trimmed.startsWith('[mcp_servers.')) {
|
|
1392
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1393
|
+
currentName = null;
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (!currentName) continue;
|
|
1397
|
+
|
|
1398
|
+
const strMatch = trimmed.match(/^(command|url)\s*=\s*"(.+?)"/);
|
|
1399
|
+
if (strMatch) { currentServer[strMatch[1]] = strMatch[2]; continue; }
|
|
1400
|
+
|
|
1401
|
+
const argsMatch = trimmed.match(/^args\s*=\s*\[(.+)\]/);
|
|
1402
|
+
if (argsMatch) {
|
|
1403
|
+
currentServer.args = argsMatch[1].split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const boolMatch = trimmed.match(/^enabled\s*=\s*(true|false)/);
|
|
1408
|
+
if (boolMatch && boolMatch[1] === 'false') currentServer.disabled = true;
|
|
1409
|
+
}
|
|
1410
|
+
if (currentName) result.mcpServers[currentName] = currentServer;
|
|
1411
|
+
return result;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/**
|
|
1415
|
+
* Comprehensive MCP config discovery across all major AI editors & tools.
|
|
1416
|
+
*
|
|
1417
|
+
* Supports: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code,
|
|
1418
|
+
* Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose,
|
|
1419
|
+
* OpenAI Codex CLI, Visual Studio — global + project-level configs.
|
|
1420
|
+
*/
|
|
1233
1421
|
function findMcpConfigs() {
|
|
1234
1422
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1235
1423
|
const platform = process.platform;
|
|
1236
|
-
|
|
1237
|
-
|
|
1424
|
+
const cwd = process.cwd();
|
|
1425
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
1426
|
+
|
|
1427
|
+
// Platform-specific app data directory
|
|
1428
|
+
// macOS: ~/Library/Application Support, Windows: ~/AppData/Roaming, Linux: ~/.config
|
|
1429
|
+
const appData = platform === 'darwin'
|
|
1430
|
+
? path.join(home, 'Library', 'Application Support')
|
|
1431
|
+
: platform === 'win32'
|
|
1432
|
+
? path.join(home, 'AppData', 'Roaming')
|
|
1433
|
+
: xdgConfig;
|
|
1434
|
+
|
|
1435
|
+
// Each candidate: { name, path, format: 'json'|'yaml'|'toml', key: top-level key }
|
|
1238
1436
|
const candidates = [
|
|
1239
|
-
// Claude Desktop
|
|
1240
|
-
{ name: 'Claude Desktop', path: path.join(home, '
|
|
1241
|
-
{ name: 'Claude Desktop', path: path.join(home, '
|
|
1242
|
-
{ name: 'Claude Desktop', path: path.join(
|
|
1243
|
-
|
|
1244
|
-
//
|
|
1245
|
-
{ name: '
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
//
|
|
1249
|
-
{ name: '
|
|
1250
|
-
|
|
1251
|
-
|
|
1437
|
+
// ── Claude Desktop ──
|
|
1438
|
+
...(platform === 'darwin' ? [{ name: 'Claude Desktop', path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1439
|
+
...(platform === 'win32' ? [{ name: 'Claude Desktop', path: path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1440
|
+
...(platform === 'linux' ? [{ name: 'Claude Desktop', path: path.join(xdgConfig, 'Claude', 'claude_desktop_config.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1441
|
+
|
|
1442
|
+
// ── Claude Code ──
|
|
1443
|
+
{ name: 'Claude Code', path: path.join(home, '.claude.json'), format: 'json', key: 'mcpServers' },
|
|
1444
|
+
{ name: 'Claude Code', path: path.join(home, '.claude', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1445
|
+
|
|
1446
|
+
// ── Cursor (global) ──
|
|
1447
|
+
{ name: 'Cursor', path: path.join(home, '.cursor', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1448
|
+
|
|
1449
|
+
// ── Windsurf / Codeium ──
|
|
1450
|
+
{ name: 'Windsurf', path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), format: 'json', key: 'mcpServers' },
|
|
1451
|
+
|
|
1452
|
+
// ── VS Code (global mcp.json — uses 'servers' key) ──
|
|
1453
|
+
...(platform === 'darwin' ? [{ name: 'VS Code', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
|
|
1454
|
+
...(platform === 'win32' ? [{ name: 'VS Code', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
|
|
1455
|
+
...(platform === 'linux' ? [{ name: 'VS Code', path: path.join(xdgConfig, 'Code', 'User', 'mcp.json'), format: 'json', key: 'servers' }] : []),
|
|
1456
|
+
|
|
1457
|
+
// ── VS Code settings.json (mcp.servers nested key) ──
|
|
1458
|
+
...(platform === 'darwin' ? [{ name: 'VS Code (settings)', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
|
|
1459
|
+
...(platform === 'win32' ? [{ name: 'VS Code (settings)', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
|
|
1460
|
+
...(platform === 'linux' ? [{ name: 'VS Code (settings)', path: path.join(xdgConfig, 'Code', 'User', 'settings.json'), format: 'json', key: 'mcp.servers' }] : []),
|
|
1461
|
+
|
|
1462
|
+
// ── Cline (VS Code extension) ──
|
|
1463
|
+
...(platform === 'darwin' ? [{ name: 'Cline', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1464
|
+
...(platform === 'win32' ? [{ name: 'Cline', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1465
|
+
...(platform === 'linux' ? [{ name: 'Cline', path: path.join(xdgConfig, 'Code', 'User', 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1466
|
+
|
|
1467
|
+
// ── Roo Code (VS Code extension) ──
|
|
1468
|
+
...(platform === 'darwin' ? [{ name: 'Roo Code', path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1469
|
+
...(platform === 'win32' ? [{ name: 'Roo Code', path: path.join(home, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1470
|
+
...(platform === 'linux' ? [{ name: 'Roo Code', path: path.join(xdgConfig, 'Code', 'User', 'globalStorage', 'rooveterinaryinc.roo-cline', 'settings', 'mcp_settings.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1471
|
+
|
|
1472
|
+
// ── Amazon Q Developer ──
|
|
1473
|
+
{ name: 'Amazon Q', path: path.join(home, '.aws', 'amazonq', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1474
|
+
{ name: 'Amazon Q (IDE)', path: path.join(home, '.aws', 'amazonq', 'default.json'), format: 'json', key: 'mcpServers' },
|
|
1475
|
+
|
|
1476
|
+
// ── Gemini CLI ──
|
|
1477
|
+
{ name: 'Gemini CLI', path: path.join(home, '.gemini', 'settings.json'), format: 'json', key: 'mcpServers' },
|
|
1478
|
+
|
|
1479
|
+
// ── Zed (macOS + Linux only, uses 'context_servers' key) ──
|
|
1480
|
+
...(platform === 'darwin' ? [{ name: 'Zed', path: path.join(home, '.zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
|
|
1481
|
+
...(platform === 'linux' ? [{ name: 'Zed', path: path.join(xdgConfig, 'zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
|
|
1482
|
+
|
|
1483
|
+
// ── Continue.dev ──
|
|
1484
|
+
{ name: 'Continue', path: path.join(home, '.continue', 'config.json'), format: 'json', key: 'mcpServers' },
|
|
1485
|
+
{ name: 'Continue', path: path.join(home, '.continue', 'config.yaml'), format: 'yaml', key: 'mcpServers' },
|
|
1486
|
+
|
|
1487
|
+
// ── Goose (Block/Square) ──
|
|
1488
|
+
{ name: 'Goose', path: path.join(xdgConfig, 'goose', 'config.yaml'), format: 'yaml', key: 'extensions' },
|
|
1489
|
+
|
|
1490
|
+
// ── OpenAI Codex CLI ──
|
|
1491
|
+
{ name: 'Codex CLI', path: path.join(home, '.codex', 'config.toml'), format: 'toml', key: 'mcp_servers' },
|
|
1492
|
+
|
|
1493
|
+
// ── Visual Studio (Windows only) ──
|
|
1494
|
+
...(platform === 'win32' ? [{ name: 'Visual Studio', path: path.join(home, '.mcp.json'), format: 'json', key: 'mcpServers' }] : []),
|
|
1495
|
+
|
|
1496
|
+
// ── Project-level configs (cwd) ──
|
|
1497
|
+
{ name: 'Claude Code (project)', path: path.join(cwd, '.mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1498
|
+
{ name: 'Cursor (project)', path: path.join(cwd, '.cursor', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1499
|
+
{ name: 'VS Code (project)', path: path.join(cwd, '.vscode', 'mcp.json'), format: 'json', key: 'servers' },
|
|
1500
|
+
{ name: 'Roo Code (project)', path: path.join(cwd, '.roo', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1501
|
+
{ name: 'Amazon Q (project)', path: path.join(cwd, '.amazonq', 'mcp.json'), format: 'json', key: 'mcpServers' },
|
|
1502
|
+
{ name: 'Gemini CLI (project)', path: path.join(cwd, '.gemini', 'settings.json'), format: 'json', key: 'mcpServers' },
|
|
1503
|
+
...(platform !== 'win32' ? [{ name: 'Zed (project)', path: path.join(cwd, '.zed', 'settings.json'), format: 'json', key: 'context_servers' }] : []),
|
|
1504
|
+
{ name: 'Codex CLI (project)', path: path.join(cwd, '.codex', 'config.toml'), format: 'toml', key: 'mcp_servers' },
|
|
1252
1505
|
];
|
|
1253
|
-
|
|
1254
|
-
//
|
|
1506
|
+
|
|
1507
|
+
// Test config override
|
|
1255
1508
|
if (process.env.AGENTAUDIT_TEST_CONFIG) {
|
|
1256
|
-
candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG });
|
|
1509
|
+
candidates.push({ name: 'Test Config', path: process.env.AGENTAUDIT_TEST_CONFIG, format: 'json', key: 'mcpServers' });
|
|
1257
1510
|
}
|
|
1258
|
-
|
|
1259
|
-
//
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1511
|
+
|
|
1512
|
+
// Continue.dev mcpServers drop-in directory (individual JSON files)
|
|
1513
|
+
const continueDropIn = path.join(home, '.continue', 'mcpServers');
|
|
1514
|
+
try {
|
|
1515
|
+
if (fs.existsSync(continueDropIn)) {
|
|
1516
|
+
for (const f of fs.readdirSync(continueDropIn)) {
|
|
1517
|
+
if (f.endsWith('.json')) {
|
|
1518
|
+
candidates.push({ name: 'Continue (drop-in)', path: path.join(continueDropIn, f), format: 'json', key: 'mcpServers' });
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
} catch {}
|
|
1523
|
+
|
|
1524
|
+
// Project-level Continue.dev drop-ins
|
|
1525
|
+
const cwdContinueDropIn = path.join(cwd, '.continue', 'mcpServers');
|
|
1526
|
+
try {
|
|
1527
|
+
if (fs.existsSync(cwdContinueDropIn)) {
|
|
1528
|
+
for (const f of fs.readdirSync(cwdContinueDropIn)) {
|
|
1529
|
+
if (f.endsWith('.json')) {
|
|
1530
|
+
candidates.push({ name: 'Continue (project drop-in)', path: path.join(cwdContinueDropIn, f), format: 'json', key: 'mcpServers' });
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
} catch {}
|
|
1535
|
+
|
|
1266
1536
|
const found = [];
|
|
1537
|
+
const seenPaths = new Set();
|
|
1538
|
+
|
|
1267
1539
|
for (const c of candidates) {
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1540
|
+
const resolved = path.resolve(c.path);
|
|
1541
|
+
if (seenPaths.has(resolved)) continue;
|
|
1542
|
+
if (!fs.existsSync(c.path)) continue;
|
|
1543
|
+
seenPaths.add(resolved);
|
|
1544
|
+
|
|
1545
|
+
try {
|
|
1546
|
+
const raw = fs.readFileSync(c.path, 'utf8');
|
|
1547
|
+
let content;
|
|
1548
|
+
|
|
1549
|
+
if (c.format === 'yaml') {
|
|
1550
|
+
content = parseSimpleYaml(raw, c.key);
|
|
1551
|
+
} else if (c.format === 'toml') {
|
|
1552
|
+
content = parseSimpleToml(raw);
|
|
1553
|
+
} else {
|
|
1554
|
+
content = JSON.parse(raw);
|
|
1555
|
+
// Normalize different JSON key structures to mcpServers
|
|
1556
|
+
if (c.key === 'mcp.servers' && content.mcp?.servers) {
|
|
1557
|
+
content = { mcpServers: content.mcp.servers };
|
|
1558
|
+
} else if (c.key === 'context_servers' && content.context_servers) {
|
|
1559
|
+
// Zed: normalize nested { command: { path, args } } → { command, args }
|
|
1560
|
+
const normalized = {};
|
|
1561
|
+
for (const [name, cfg] of Object.entries(content.context_servers)) {
|
|
1562
|
+
if (cfg.command && typeof cfg.command === 'object') {
|
|
1563
|
+
normalized[name] = { command: cfg.command.path || cfg.command.command, args: cfg.command.args || [], env: cfg.command.env || {} };
|
|
1564
|
+
} else {
|
|
1565
|
+
normalized[name] = cfg;
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
content = { mcpServers: normalized };
|
|
1569
|
+
} else if (c.key === 'servers' && content.servers && !content.mcpServers) {
|
|
1570
|
+
content = { mcpServers: content.servers };
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// Only include configs that actually have servers
|
|
1575
|
+
const servers = content?.mcpServers || content?.servers || {};
|
|
1576
|
+
if (Object.keys(servers).length > 0) {
|
|
1577
|
+
found.push({ name: c.name, path: c.path, content });
|
|
1578
|
+
}
|
|
1579
|
+
} catch {}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
return found;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// ── Skill Discovery & Validation ─────────────────────────
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Parse YAML frontmatter from a SKILL.md file.
|
|
1589
|
+
* Returns { meta: {...}, body: string, errors: string[] }
|
|
1590
|
+
*/
|
|
1591
|
+
function parseSkillFrontmatter(content) {
|
|
1592
|
+
const errors = [];
|
|
1593
|
+
const lines = content.split('\n');
|
|
1594
|
+
|
|
1595
|
+
// Must start with ---
|
|
1596
|
+
if (lines[0].trim() !== '---') {
|
|
1597
|
+
return { meta: null, body: content, errors: ['Missing YAML frontmatter (file must start with ---)'] };
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// Find closing ---
|
|
1601
|
+
let endIdx = -1;
|
|
1602
|
+
for (let i = 1; i < lines.length; i++) {
|
|
1603
|
+
if (lines[i].trim() === '---') { endIdx = i; break; }
|
|
1604
|
+
}
|
|
1605
|
+
if (endIdx === -1) {
|
|
1606
|
+
return { meta: null, body: content, errors: ['Unclosed frontmatter (missing closing ---)'] };
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Parse YAML-like key: value pairs
|
|
1610
|
+
const meta = {};
|
|
1611
|
+
const yamlLines = lines.slice(1, endIdx);
|
|
1612
|
+
for (let i = 0; i < yamlLines.length; i++) {
|
|
1613
|
+
const line = yamlLines[i];
|
|
1614
|
+
if (line.trim() === '' || line.trim().startsWith('#')) continue;
|
|
1615
|
+
|
|
1616
|
+
// Check for tabs
|
|
1617
|
+
if (line.includes('\t')) {
|
|
1618
|
+
errors.push(`Line ${i + 2}: Tab character found (use spaces)`);
|
|
1273
1619
|
}
|
|
1620
|
+
|
|
1621
|
+
const match = line.match(/^([a-z][a-z0-9_-]*):\s*(.*)/i);
|
|
1622
|
+
if (!match) {
|
|
1623
|
+
// Could be a continuation line (YAML multiline)
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
const key = match[1].toLowerCase();
|
|
1627
|
+
let value = match[2].trim();
|
|
1628
|
+
|
|
1629
|
+
// Handle YAML lists on next lines
|
|
1630
|
+
if (value === '' && i + 1 < yamlLines.length && yamlLines[i + 1].match(/^\s+-\s/)) {
|
|
1631
|
+
const items = [];
|
|
1632
|
+
let j = i + 1;
|
|
1633
|
+
while (j < yamlLines.length && yamlLines[j].match(/^\s+-\s/)) {
|
|
1634
|
+
items.push(yamlLines[j].replace(/^\s+-\s*/, '').trim());
|
|
1635
|
+
j++;
|
|
1636
|
+
}
|
|
1637
|
+
value = items;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Strip surrounding quotes
|
|
1641
|
+
if (typeof value === 'string' && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))) {
|
|
1642
|
+
value = value.slice(1, -1);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
meta[key] = value;
|
|
1274
1646
|
}
|
|
1647
|
+
|
|
1648
|
+
const body = lines.slice(endIdx + 1).join('\n').trim();
|
|
1649
|
+
return { meta, body, errors };
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* Validate a parsed skill against the Claude Code SKILL.md spec.
|
|
1654
|
+
* Returns { errors: [...], warnings: [...], info: {...} }
|
|
1655
|
+
*/
|
|
1656
|
+
function validateSkill(parsed) {
|
|
1657
|
+
const { meta, body, errors: parseErrors } = parsed;
|
|
1658
|
+
const errors = [...parseErrors];
|
|
1659
|
+
const warnings = [];
|
|
1660
|
+
const info = {};
|
|
1661
|
+
|
|
1662
|
+
if (!meta) return { errors, warnings, info };
|
|
1663
|
+
|
|
1664
|
+
// Known fields
|
|
1665
|
+
const knownFields = new Set([
|
|
1666
|
+
'name', 'description', 'allowed-tools', 'user-invocable', 'user-invokable',
|
|
1667
|
+
'disable-model-invocation', 'license', 'metadata', 'argument-hint',
|
|
1668
|
+
'compatibility', 'version', 'author',
|
|
1669
|
+
]);
|
|
1670
|
+
|
|
1671
|
+
// Check for unknown fields
|
|
1672
|
+
for (const key of Object.keys(meta)) {
|
|
1673
|
+
if (!knownFields.has(key)) {
|
|
1674
|
+
warnings.push(`Unknown frontmatter field: "${key}"`);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// Required: name
|
|
1679
|
+
if (!meta.name) {
|
|
1680
|
+
errors.push('Missing required field: name');
|
|
1681
|
+
} else {
|
|
1682
|
+
info.name = meta.name;
|
|
1683
|
+
if (meta.name.length > 64) errors.push(`name exceeds 64 chars (${meta.name.length})`);
|
|
1684
|
+
if (/<[^>]+>/.test(meta.name)) errors.push('name contains XML/HTML tags');
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// Required: description
|
|
1688
|
+
if (!meta.description) {
|
|
1689
|
+
errors.push('Missing required field: description');
|
|
1690
|
+
} else {
|
|
1691
|
+
info.description = typeof meta.description === 'string' ? meta.description.slice(0, 120) : String(meta.description).slice(0, 120);
|
|
1692
|
+
if (typeof meta.description === 'string' && meta.description.length > 1024) {
|
|
1693
|
+
warnings.push(`description is ${meta.description.length} chars (recommended max: 1024)`);
|
|
1694
|
+
}
|
|
1695
|
+
if (/<[^>]+>/.test(meta.description)) warnings.push('description contains XML/HTML tags');
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// Security: allowed-tools
|
|
1699
|
+
if (!meta['allowed-tools']) {
|
|
1700
|
+
warnings.push('No allowed-tools set — skill has access to ALL tools (security risk)');
|
|
1701
|
+
info.allowedTools = null;
|
|
1702
|
+
} else {
|
|
1703
|
+
const tools = typeof meta['allowed-tools'] === 'string'
|
|
1704
|
+
? meta['allowed-tools'].split(',').map(t => t.trim()).filter(Boolean)
|
|
1705
|
+
: Array.isArray(meta['allowed-tools']) ? meta['allowed-tools'] : [];
|
|
1706
|
+
info.allowedTools = tools;
|
|
1707
|
+
// Check for wildcard/dangerous patterns
|
|
1708
|
+
if (tools.some(t => t === '*' || t === 'Bash' || t === 'Bash(*)')) {
|
|
1709
|
+
warnings.push('allowed-tools includes unrestricted Bash access');
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Boolean fields
|
|
1714
|
+
for (const boolField of ['user-invocable', 'user-invokable', 'disable-model-invocation']) {
|
|
1715
|
+
if (meta[boolField] !== undefined) {
|
|
1716
|
+
const val = String(meta[boolField]).toLowerCase();
|
|
1717
|
+
if (!['true', 'false'].includes(val)) {
|
|
1718
|
+
errors.push(`${boolField} must be true or false (got: "${meta[boolField]}")`);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Typo detection
|
|
1724
|
+
if (meta['user-invokable'] && !meta['user-invocable']) {
|
|
1725
|
+
warnings.push('Using "user-invokable" (known typo variant) — both spellings work');
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Body checks
|
|
1729
|
+
if (body) {
|
|
1730
|
+
const bodyLines = body.split('\n').length;
|
|
1731
|
+
info.bodyLines = bodyLines;
|
|
1732
|
+
if (bodyLines > 500) warnings.push(`Body is ${bodyLines} lines (recommended max: 500)`);
|
|
1733
|
+
|
|
1734
|
+
// Check for potential prompt injection patterns in body
|
|
1735
|
+
const injectionPatterns = [
|
|
1736
|
+
{ pattern: /ignore\s+(all\s+)?previous\s+(instructions|rules)/i, label: 'Prompt injection pattern' },
|
|
1737
|
+
{ pattern: /<IMPORTANT>/i, label: 'Suspicious <IMPORTANT> tag' },
|
|
1738
|
+
{ pattern: /system\s*:\s*you\s+are/i, label: 'System prompt override attempt' },
|
|
1739
|
+
];
|
|
1740
|
+
for (const { pattern, label } of injectionPatterns) {
|
|
1741
|
+
if (pattern.test(body)) {
|
|
1742
|
+
warnings.push(`${label} detected in body`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Extract MCP tool references
|
|
1748
|
+
const mcpRefs = [];
|
|
1749
|
+
const mcpPattern = /mcp__([a-z0-9_-]+)__([a-z0-9_]+)/gi;
|
|
1750
|
+
const fullText = (meta.description || '') + ' ' + (typeof meta['allowed-tools'] === 'string' ? meta['allowed-tools'] : '') + ' ' + (body || '');
|
|
1751
|
+
let mcpMatch;
|
|
1752
|
+
while ((mcpMatch = mcpPattern.exec(fullText)) !== null) {
|
|
1753
|
+
mcpRefs.push({ server: mcpMatch[1], tool: mcpMatch[2] });
|
|
1754
|
+
}
|
|
1755
|
+
info.mcpRefs = mcpRefs;
|
|
1756
|
+
|
|
1757
|
+
// Deduplicate MCP server names
|
|
1758
|
+
info.mcpServers = [...new Set(mcpRefs.map(r => r.server))];
|
|
1759
|
+
|
|
1760
|
+
return { errors, warnings, info };
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Find all SKILL.md files in known skill directories.
|
|
1765
|
+
*/
|
|
1766
|
+
function findSkills() {
|
|
1767
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
1768
|
+
const cwd = process.cwd();
|
|
1769
|
+
const found = [];
|
|
1770
|
+
|
|
1771
|
+
const skillDirs = [
|
|
1772
|
+
// Global skill dirs
|
|
1773
|
+
{ name: 'Claude Code (global)', base: path.join(home, '.claude', 'skills') },
|
|
1774
|
+
{ name: 'Cursor (global)', base: path.join(home, '.cursor', 'skills') },
|
|
1775
|
+
{ name: 'Antigravity (global)', base: path.join(home, '.agent', 'skills') },
|
|
1776
|
+
// Project-level skill dirs
|
|
1777
|
+
{ name: 'Claude Code (project)', base: path.join(cwd, '.claude', 'skills') },
|
|
1778
|
+
{ name: 'Cursor (project)', base: path.join(cwd, '.cursor', 'skills') },
|
|
1779
|
+
{ name: 'GitHub Skills (project)', base: path.join(cwd, '.github', 'skills') },
|
|
1780
|
+
{ name: 'Antigravity (project)', base: path.join(cwd, '.agent', 'skills') },
|
|
1781
|
+
];
|
|
1782
|
+
|
|
1783
|
+
for (const dir of skillDirs) {
|
|
1784
|
+
if (!fs.existsSync(dir.base)) continue;
|
|
1785
|
+
try {
|
|
1786
|
+
const entries = fs.readdirSync(dir.base, { withFileTypes: true });
|
|
1787
|
+
for (const entry of entries) {
|
|
1788
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
1789
|
+
const skillPath = path.join(dir.base, entry.name, 'SKILL.md');
|
|
1790
|
+
if (!fs.existsSync(skillPath)) continue;
|
|
1791
|
+
try {
|
|
1792
|
+
const content = fs.readFileSync(skillPath, 'utf8');
|
|
1793
|
+
const parsed = parseSkillFrontmatter(content);
|
|
1794
|
+
const validation = validateSkill(parsed);
|
|
1795
|
+
found.push({
|
|
1796
|
+
source: dir.name,
|
|
1797
|
+
dir: path.join(dir.base, entry.name),
|
|
1798
|
+
path: skillPath,
|
|
1799
|
+
dirName: entry.name,
|
|
1800
|
+
parsed,
|
|
1801
|
+
validation,
|
|
1802
|
+
isSymlink: entry.isSymbolicLink(),
|
|
1803
|
+
});
|
|
1804
|
+
} catch {}
|
|
1805
|
+
}
|
|
1806
|
+
} catch {}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1275
1809
|
return found;
|
|
1276
1810
|
}
|
|
1277
1811
|
|
|
1812
|
+
// ── Server Config Extraction ─────────────────────────────
|
|
1813
|
+
|
|
1278
1814
|
function extractServersFromConfig(config) {
|
|
1279
1815
|
// Handle both { mcpServers: {...} } and { servers: {...} } formats
|
|
1280
1816
|
const servers = config.mcpServers || config.servers || {};
|
|
@@ -1336,7 +1872,10 @@ function extractServersFromConfig(config) {
|
|
|
1336
1872
|
}
|
|
1337
1873
|
} catch {}
|
|
1338
1874
|
}
|
|
1339
|
-
|
|
1875
|
+
|
|
1876
|
+
// Resolve local installation directory
|
|
1877
|
+
info.localDir = resolveLocalDir(info);
|
|
1878
|
+
|
|
1340
1879
|
result.push(info);
|
|
1341
1880
|
}
|
|
1342
1881
|
return result;
|
|
@@ -1349,6 +1888,196 @@ function serverSlug(server) {
|
|
|
1349
1888
|
return server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
|
|
1350
1889
|
}
|
|
1351
1890
|
|
|
1891
|
+
/**
|
|
1892
|
+
* Resolve the local installation directory for a discovered MCP server.
|
|
1893
|
+
* Returns an absolute path or null if not found.
|
|
1894
|
+
*/
|
|
1895
|
+
function resolveLocalDir(server) {
|
|
1896
|
+
const home = os.homedir();
|
|
1897
|
+
const isWin = process.platform === 'win32';
|
|
1898
|
+
|
|
1899
|
+
// node /path/to/file → walk up to project root (package.json or .git)
|
|
1900
|
+
const allArgs = [server.command, ...server.args].filter(Boolean).join(' ');
|
|
1901
|
+
const nodePathMatch = allArgs.match(/node\s+["']?([^"'\s]+)/);
|
|
1902
|
+
if (nodePathMatch) {
|
|
1903
|
+
let dir = path.dirname(path.resolve(nodePathMatch[1]));
|
|
1904
|
+
for (let i = 0; i < 5; i++) {
|
|
1905
|
+
if (fs.existsSync(path.join(dir, 'package.json')) || fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
1906
|
+
const parent = path.dirname(dir);
|
|
1907
|
+
if (parent === dir) break;
|
|
1908
|
+
dir = parent;
|
|
1909
|
+
}
|
|
1910
|
+
// Fallback: use the script's directory
|
|
1911
|
+
return path.dirname(path.resolve(nodePathMatch[1]));
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// python /path/to/file → same approach
|
|
1915
|
+
const pyPathMatch = allArgs.match(/python[3]?\s+["']?([^"'\s]+\.py)/);
|
|
1916
|
+
if (pyPathMatch) {
|
|
1917
|
+
let dir = path.dirname(path.resolve(pyPathMatch[1]));
|
|
1918
|
+
for (let i = 0; i < 5; i++) {
|
|
1919
|
+
if (fs.existsSync(path.join(dir, 'pyproject.toml')) || fs.existsSync(path.join(dir, 'setup.py')) || fs.existsSync(path.join(dir, '.git'))) return dir;
|
|
1920
|
+
const parent = path.dirname(dir);
|
|
1921
|
+
if (parent === dir) break;
|
|
1922
|
+
dir = parent;
|
|
1923
|
+
}
|
|
1924
|
+
return path.dirname(path.resolve(pyPathMatch[1]));
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// npm/npx package → check global node_modules
|
|
1928
|
+
if (server.npmPackage) {
|
|
1929
|
+
const pkgName = server.npmPackage.replace(/@latest$/, '').replace(/@[\d.]+$/, '');
|
|
1930
|
+
const candidates = [];
|
|
1931
|
+
// Global npm
|
|
1932
|
+
try {
|
|
1933
|
+
const globalRoot = execFileSync('npm', ['root', '-g'], { timeout: 5000, stdio: 'pipe' }).toString().trim();
|
|
1934
|
+
candidates.push(path.join(globalRoot, pkgName));
|
|
1935
|
+
} catch {}
|
|
1936
|
+
// Local node_modules (cwd)
|
|
1937
|
+
candidates.push(path.join(process.cwd(), 'node_modules', pkgName));
|
|
1938
|
+
for (const dir of candidates) {
|
|
1939
|
+
if (fs.existsSync(dir)) return dir;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
// uvx/pip package → check uv tools cache and site-packages
|
|
1944
|
+
if (server.pyPackage) {
|
|
1945
|
+
const pkgName = server.pyPackage.replace(/@latest$/, '').replace(/@[\d.]+$/, '');
|
|
1946
|
+
const candidates = [];
|
|
1947
|
+
if (isWin) {
|
|
1948
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
1949
|
+
candidates.push(path.join(localAppData, 'uv', 'tools', pkgName));
|
|
1950
|
+
} else {
|
|
1951
|
+
candidates.push(path.join(home, '.local', 'share', 'uv', 'tools', pkgName));
|
|
1952
|
+
}
|
|
1953
|
+
// Also try pip show
|
|
1954
|
+
try {
|
|
1955
|
+
const pipOut = execFileSync('pip', ['show', pkgName, '-f'], { timeout: 5000, stdio: 'pipe' }).toString();
|
|
1956
|
+
const locMatch = pipOut.match(/Location:\s*(.+)/);
|
|
1957
|
+
if (locMatch) {
|
|
1958
|
+
const normalized = pkgName.replace(/-/g, '_');
|
|
1959
|
+
const pkgDir = path.join(locMatch[1].trim(), normalized);
|
|
1960
|
+
if (fs.existsSync(pkgDir)) candidates.push(pkgDir);
|
|
1961
|
+
}
|
|
1962
|
+
} catch {}
|
|
1963
|
+
for (const dir of candidates) {
|
|
1964
|
+
if (fs.existsSync(dir)) return dir;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
return null;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/**
|
|
1972
|
+
* Scan a local directory (like scanRepo but without cloning).
|
|
1973
|
+
*/
|
|
1974
|
+
async function scanLocalDir(localDir, serverName) {
|
|
1975
|
+
const start = Date.now();
|
|
1976
|
+
const slug = serverName.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
|
|
1977
|
+
|
|
1978
|
+
if (!jsonMode) process.stdout.write(`${icons.scan} Scanning ${c.bold}${slug}${c.reset} ${c.dim}(local)${c.reset} ${c.dim}...${c.reset}`);
|
|
1979
|
+
|
|
1980
|
+
// Collect files from local dir
|
|
1981
|
+
const files = collectFiles(localDir);
|
|
1982
|
+
if (files.length === 0) {
|
|
1983
|
+
if (!jsonMode) process.stdout.write(` ${c.yellow}no scannable files found${c.reset}\n`);
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// Detect info
|
|
1988
|
+
const info = detectPackageInfo(localDir, files);
|
|
1989
|
+
|
|
1990
|
+
// Quick checks
|
|
1991
|
+
const findings = quickChecks(files);
|
|
1992
|
+
|
|
1993
|
+
// Registry lookup
|
|
1994
|
+
const registryData = await checkRegistry(slug);
|
|
1995
|
+
|
|
1996
|
+
const duration = elapsed(start);
|
|
1997
|
+
|
|
1998
|
+
if (!jsonMode) {
|
|
1999
|
+
process.stdout.write('\r\x1b[K');
|
|
2000
|
+
printScanResult(`local://${localDir}`, info, files, findings, registryData, duration);
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
return { slug, url: `local://${localDir}`, info, files: files.length, findings, registryData, duration };
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
/**
|
|
2007
|
+
* Download package source from PyPI or npm to a temp dir and scan it.
|
|
2008
|
+
* Used as last resort when git clone fails and no local install exists.
|
|
2009
|
+
*/
|
|
2010
|
+
async function downloadAndScan(server) {
|
|
2011
|
+
const start = Date.now();
|
|
2012
|
+
const slug = server.name.toLowerCase().replace(/[^a-z0-9-]/gi, '-');
|
|
2013
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-pkg-'));
|
|
2014
|
+
|
|
2015
|
+
try {
|
|
2016
|
+
if (server.pyPackage) {
|
|
2017
|
+
const pkgName = server.pyPackage.replace(/@latest$/, '').replace(/@[\d.]+$/, '');
|
|
2018
|
+
if (!jsonMode) process.stdout.write(`${icons.scan} Downloading ${c.bold}${pkgName}${c.reset} ${c.dim}from PyPI...${c.reset}`);
|
|
2019
|
+
// Download sdist/wheel without installing
|
|
2020
|
+
execFileSync('pip', ['download', '--no-deps', '-d', tmpDir, pkgName], { timeout: 30000, stdio: 'pipe' });
|
|
2021
|
+
// Extract any .tar.gz or .whl (zip) files
|
|
2022
|
+
const downloaded = fs.readdirSync(tmpDir);
|
|
2023
|
+
const extractDir = path.join(tmpDir, 'src');
|
|
2024
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
2025
|
+
for (const f of downloaded) {
|
|
2026
|
+
const fp = path.join(tmpDir, f);
|
|
2027
|
+
if (f.endsWith('.whl') || f.endsWith('.zip')) {
|
|
2028
|
+
execFileSync('python', ['-m', 'zipfile', '-e', fp, extractDir], { timeout: 10000, stdio: 'pipe' });
|
|
2029
|
+
} else if (f.endsWith('.tar.gz') || f.endsWith('.tgz')) {
|
|
2030
|
+
execFileSync('tar', ['xzf', fp, '-C', extractDir], { timeout: 10000, stdio: 'pipe' });
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
const files = collectFiles(extractDir);
|
|
2034
|
+
if (files.length === 0) return null;
|
|
2035
|
+
const info = detectPackageInfo(extractDir, files);
|
|
2036
|
+
const findings = quickChecks(files);
|
|
2037
|
+
const registryData = await checkRegistry(slug);
|
|
2038
|
+
const duration = elapsed(start);
|
|
2039
|
+
if (!jsonMode) {
|
|
2040
|
+
process.stdout.write('\r\x1b[K');
|
|
2041
|
+
printScanResult(`pypi://${pkgName}`, info, files, findings, registryData, duration);
|
|
2042
|
+
}
|
|
2043
|
+
return { slug, url: `pypi://${pkgName}`, info, files: files.length, findings, registryData, duration };
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
if (server.npmPackage) {
|
|
2047
|
+
const pkgName = server.npmPackage.replace(/@latest$/, '').replace(/@[\d.]+$/, '');
|
|
2048
|
+
if (!jsonMode) process.stdout.write(`${icons.scan} Downloading ${c.bold}${pkgName}${c.reset} ${c.dim}from npm...${c.reset}`);
|
|
2049
|
+
// npm pack downloads tarball without installing
|
|
2050
|
+
execFileSync('npm', ['pack', pkgName, '--pack-destination', tmpDir], { timeout: 30000, stdio: 'pipe' });
|
|
2051
|
+
const tarballs = fs.readdirSync(tmpDir).filter(f => f.endsWith('.tgz'));
|
|
2052
|
+
if (tarballs.length === 0) return null;
|
|
2053
|
+
const extractDir = path.join(tmpDir, 'src');
|
|
2054
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
2055
|
+
execFileSync('tar', ['xzf', path.join(tmpDir, tarballs[0]), '-C', extractDir], { timeout: 10000, stdio: 'pipe' });
|
|
2056
|
+
const files = collectFiles(extractDir);
|
|
2057
|
+
if (files.length === 0) return null;
|
|
2058
|
+
const info = detectPackageInfo(extractDir, files);
|
|
2059
|
+
const findings = quickChecks(files);
|
|
2060
|
+
const registryData = await checkRegistry(slug);
|
|
2061
|
+
const duration = elapsed(start);
|
|
2062
|
+
if (!jsonMode) {
|
|
2063
|
+
process.stdout.write('\r\x1b[K');
|
|
2064
|
+
printScanResult(`npm://${pkgName}`, info, files, findings, registryData, duration);
|
|
2065
|
+
}
|
|
2066
|
+
return { slug, url: `npm://${pkgName}`, info, files: files.length, findings, registryData, duration };
|
|
2067
|
+
}
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
if (!jsonMode) {
|
|
2070
|
+
process.stdout.write('\r\x1b[K');
|
|
2071
|
+
process.stdout.write(`${icons.scan} ${c.bold}${slug}${c.reset} ${c.yellow}download failed${c.reset}\n`);
|
|
2072
|
+
const msg = err.stderr?.toString().trim().split('\n')[0] || err.message?.split('\n')[0] || '';
|
|
2073
|
+
if (msg) console.log(` ${c.dim}${msg}${c.reset}`);
|
|
2074
|
+
}
|
|
2075
|
+
} finally {
|
|
2076
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
2077
|
+
}
|
|
2078
|
+
return null;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
1352
2081
|
async function searchGitHub(query) {
|
|
1353
2082
|
try {
|
|
1354
2083
|
const res = await fetch(`https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&per_page=1`, {
|
|
@@ -1450,7 +2179,7 @@ async function discoverCommand(options = {}) {
|
|
|
1450
2179
|
const interactiveAudit = options.audit || false;
|
|
1451
2180
|
|
|
1452
2181
|
if (!jsonMode) {
|
|
1453
|
-
console.log(` ${c.bold}Discovering MCP servers
|
|
2182
|
+
console.log(` ${c.bold}Discovering MCP servers across all AI tools...${c.reset}`);
|
|
1454
2183
|
console.log();
|
|
1455
2184
|
}
|
|
1456
2185
|
|
|
@@ -1458,13 +2187,16 @@ async function discoverCommand(options = {}) {
|
|
|
1458
2187
|
|
|
1459
2188
|
if (configs.length === 0) {
|
|
1460
2189
|
console.log(` ${c.yellow}No MCP configurations found.${c.reset}`);
|
|
1461
|
-
console.log(` ${c.dim}Searched: Claude Desktop, Cursor, Windsurf, VS Code
|
|
2190
|
+
console.log(` ${c.dim}Searched 15+ tools: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code,${c.reset}`);
|
|
2191
|
+
console.log(` ${c.dim}Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose, Codex CLI${c.reset}`);
|
|
1462
2192
|
console.log();
|
|
1463
|
-
console.log(` ${c.dim}MCP config locations:${c.reset}`);
|
|
1464
|
-
console.log(` ${c.dim} Claude:
|
|
1465
|
-
console.log(` ${c.dim}
|
|
1466
|
-
console.log(` ${c.dim}
|
|
1467
|
-
console.log(` ${c.dim}
|
|
2193
|
+
console.log(` ${c.dim}Common MCP config locations:${c.reset}`);
|
|
2194
|
+
console.log(` ${c.dim} Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json${c.reset}`);
|
|
2195
|
+
console.log(` ${c.dim} Claude Code: ~/.claude.json${c.reset}`);
|
|
2196
|
+
console.log(` ${c.dim} Cursor: ~/.cursor/mcp.json${c.reset}`);
|
|
2197
|
+
console.log(` ${c.dim} Windsurf: ~/.codeium/windsurf/mcp_config.json${c.reset}`);
|
|
2198
|
+
console.log(` ${c.dim} VS Code: (platform)/Code/User/mcp.json${c.reset}`);
|
|
2199
|
+
console.log(` ${c.dim} Project-level: .mcp.json / .cursor/mcp.json / .vscode/mcp.json${c.reset}`);
|
|
1468
2200
|
console.log();
|
|
1469
2201
|
return;
|
|
1470
2202
|
}
|
|
@@ -1527,15 +2259,18 @@ async function discoverCommand(options = {}) {
|
|
|
1527
2259
|
const riskScore = regData.risk_score ?? regData.latest_risk_score ?? 0;
|
|
1528
2260
|
const hasOfficial = regData.has_official_audit;
|
|
1529
2261
|
console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
|
|
1530
|
-
console.log(`${pipe} ${riskBadge(riskScore)}
|
|
1531
|
-
if (resolvedUrl) allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: true, regData });
|
|
2262
|
+
console.log(`${pipe} ${riskBadge(riskScore)} ${hasOfficial ? `${c.green}✔ official${c.reset} ` : ''}${c.dim}${REGISTRY_URL}/packages/${slug}${c.reset}`);
|
|
2263
|
+
if (resolvedUrl || server.localDir || server.pyPackage || server.npmPackage) allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, localDir: server.localDir, pyPackage: server.pyPackage, npmPackage: server.npmPackage, hasAudit: true, regData });
|
|
1532
2264
|
} else {
|
|
1533
2265
|
unauditedServers++;
|
|
1534
2266
|
console.log(`${branch} ${c.bold}${server.name}${c.reset} ${sourceLabel}`);
|
|
1535
2267
|
if (resolvedUrl) {
|
|
1536
2268
|
console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Run: ${c.cyan}agentaudit audit ${resolvedUrl}${c.reset}`);
|
|
1537
2269
|
unauditedWithUrls.push({ name: server.name, sourceUrl: resolvedUrl });
|
|
1538
|
-
allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, hasAudit: false });
|
|
2270
|
+
allServersWithUrls.push({ name: server.name, sourceUrl: resolvedUrl, localDir: server.localDir, pyPackage: server.pyPackage, npmPackage: server.npmPackage, hasAudit: false });
|
|
2271
|
+
} else if (server.localDir || server.pyPackage || server.npmPackage) {
|
|
2272
|
+
console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}${server.localDir ? 'local install found' : 'package registry available'} — will scan${c.reset}`);
|
|
2273
|
+
allServersWithUrls.push({ name: server.name, sourceUrl: null, localDir: server.localDir, pyPackage: server.pyPackage, npmPackage: server.npmPackage, hasAudit: false });
|
|
1539
2274
|
} else {
|
|
1540
2275
|
console.log(`${pipe} ${c.yellow}⚠ not audited${c.reset} ${c.dim}Source URL unknown — check the package's GitHub/npm page${c.reset}`);
|
|
1541
2276
|
}
|
|
@@ -1550,43 +2285,131 @@ async function discoverCommand(options = {}) {
|
|
|
1550
2285
|
}
|
|
1551
2286
|
|
|
1552
2287
|
// Summary
|
|
1553
|
-
console.log(
|
|
1554
|
-
console.log(` ${c.bold}Summary${c.reset} ${totalServers} server${totalServers !== 1 ? 's' : ''} across ${configs.length} config${configs.length !== 1 ? 's' : ''}`);
|
|
2288
|
+
console.log(sectionHeader(`Summary — ${totalServers} server${totalServers !== 1 ? 's' : ''} across ${configs.length} config${configs.length !== 1 ? 's' : ''}`));
|
|
1555
2289
|
console.log();
|
|
1556
2290
|
if (auditedServers > 0) console.log(` ${icons.safe} ${c.green}${auditedServers} audited${c.reset}`);
|
|
1557
2291
|
if (unauditedServers > 0) console.log(` ${icons.caution} ${c.yellow}${unauditedServers} not audited${c.reset}`);
|
|
2292
|
+
if (totalServers > 0) {
|
|
2293
|
+
console.log();
|
|
2294
|
+
console.log(` ${coverageBar(auditedServers, totalServers)}`);
|
|
2295
|
+
}
|
|
1558
2296
|
console.log();
|
|
1559
2297
|
|
|
1560
|
-
//
|
|
2298
|
+
// ── Skill Discovery ──────────────────────────────────
|
|
2299
|
+
const skills = findSkills();
|
|
2300
|
+
if (skills.length > 0) {
|
|
2301
|
+
console.log(sectionHeader(`Skills — ${skills.length} found`));
|
|
2302
|
+
console.log();
|
|
2303
|
+
|
|
2304
|
+
// Group by source
|
|
2305
|
+
const bySource = {};
|
|
2306
|
+
for (const skill of skills) {
|
|
2307
|
+
(bySource[skill.source] || (bySource[skill.source] = [])).push(skill);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
for (const [source, sourceSkills] of Object.entries(bySource)) {
|
|
2311
|
+
console.log(`${icons.bullet} ${c.bold}${source}${c.reset}`);
|
|
2312
|
+
console.log();
|
|
2313
|
+
|
|
2314
|
+
for (let i = 0; i < sourceSkills.length; i++) {
|
|
2315
|
+
const skill = sourceSkills[i];
|
|
2316
|
+
const isLast = i === sourceSkills.length - 1;
|
|
2317
|
+
const branch = isLast ? icons.treeLast : icons.tree;
|
|
2318
|
+
const pipe = isLast ? ' ' : `${icons.pipe} `;
|
|
2319
|
+
const { errors, warnings, info } = skill.validation;
|
|
2320
|
+
const name = info.name || skill.dirName;
|
|
2321
|
+
const hasErrors = errors.length > 0;
|
|
2322
|
+
const hasWarnings = warnings.length > 0;
|
|
2323
|
+
|
|
2324
|
+
// Status indicator
|
|
2325
|
+
let status;
|
|
2326
|
+
if (hasErrors) status = `${c.red}✖ ${errors.length} error${errors.length !== 1 ? 's' : ''}${c.reset}`;
|
|
2327
|
+
else if (hasWarnings) status = `${c.yellow}⚠ ${warnings.length} warning${warnings.length !== 1 ? 's' : ''}${c.reset}`;
|
|
2328
|
+
else status = `${c.green}✔ valid${c.reset}`;
|
|
2329
|
+
|
|
2330
|
+
console.log(`${branch} ${c.bold}${name}${c.reset} ${status}`);
|
|
2331
|
+
|
|
2332
|
+
// Description (truncated)
|
|
2333
|
+
if (info.description) {
|
|
2334
|
+
const desc = info.description.length > 70 ? info.description.slice(0, 67) + '...' : info.description;
|
|
2335
|
+
console.log(`${pipe} ${c.dim}${desc}${c.reset}`);
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
// MCP tool references
|
|
2339
|
+
if (info.mcpServers && info.mcpServers.length > 0) {
|
|
2340
|
+
const serverList = info.mcpServers.map(s => `${c.cyan}${s}${c.reset}`).join(', ');
|
|
2341
|
+
console.log(`${pipe} ${c.dim}uses MCP:${c.reset} ${serverList}`);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
// Allowed tools summary
|
|
2345
|
+
if (info.allowedTools === null) {
|
|
2346
|
+
console.log(`${pipe} ${c.yellow}⚠ no allowed-tools — unrestricted access${c.reset}`);
|
|
2347
|
+
} else if (info.allowedTools && info.allowedTools.length > 0) {
|
|
2348
|
+
const toolCount = info.allowedTools.length;
|
|
2349
|
+
console.log(`${pipe} ${c.dim}${toolCount} allowed tool${toolCount !== 1 ? 's' : ''}${c.reset}`);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// Show errors/warnings inline
|
|
2353
|
+
if (hasErrors) {
|
|
2354
|
+
for (const err of errors.slice(0, 3)) {
|
|
2355
|
+
console.log(`${pipe} ${c.red} ✖ ${err}${c.reset}`);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (hasWarnings && !hasErrors) {
|
|
2359
|
+
for (const warn of warnings.slice(0, 2)) {
|
|
2360
|
+
console.log(`${pipe} ${c.yellow} ⚠ ${warn}${c.reset}`);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
console.log();
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// --scan: automatically scan all servers (git clone + local fallback)
|
|
1561
2369
|
if (autoScan) {
|
|
1562
2370
|
const isCloneable = (url) => /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org)\//i.test(url);
|
|
1563
|
-
|
|
1564
|
-
|
|
2371
|
+
// Include servers that are cloneable OR have a local dir OR a known package
|
|
2372
|
+
const scanTargets = allServersWithUrls.filter(s =>
|
|
2373
|
+
(s.sourceUrl && isCloneable(s.sourceUrl)) || s.localDir || s.pyPackage || s.npmPackage
|
|
2374
|
+
);
|
|
2375
|
+
// Deduplicate by sourceUrl or localDir
|
|
1565
2376
|
const seen = new Set();
|
|
1566
2377
|
const dedupedTargets = scanTargets.filter(s => {
|
|
1567
|
-
|
|
1568
|
-
seen.
|
|
2378
|
+
const key = (s.sourceUrl && isCloneable(s.sourceUrl)) ? s.sourceUrl : s.localDir;
|
|
2379
|
+
if (!key || seen.has(key)) return false;
|
|
2380
|
+
seen.add(key);
|
|
1569
2381
|
return true;
|
|
1570
2382
|
});
|
|
1571
|
-
const
|
|
2383
|
+
const skippedCount = allServersWithUrls.length - scanTargets.length;
|
|
1572
2384
|
if (dedupedTargets.length > 0) {
|
|
1573
|
-
console.log(
|
|
1574
|
-
console.log(` ${c.bold}${icons.scan}
|
|
1575
|
-
if (
|
|
1576
|
-
console.log(` ${c.dim}(${
|
|
2385
|
+
console.log(sectionHeader(`Auto-scanning ${dedupedTargets.length} server${dedupedTargets.length !== 1 ? 's' : ''}`));
|
|
2386
|
+
console.log(` ${c.bold}${icons.scan} Starting scans...${c.reset}`);
|
|
2387
|
+
if (skippedCount > 0) {
|
|
2388
|
+
console.log(` ${c.dim}(${skippedCount} skipped — remote-only, no local source)${c.reset}`);
|
|
1577
2389
|
}
|
|
1578
2390
|
console.log();
|
|
1579
|
-
|
|
2391
|
+
|
|
1580
2392
|
const scanResults = [];
|
|
1581
2393
|
for (const target of dedupedTargets) {
|
|
1582
|
-
|
|
2394
|
+
let result = null;
|
|
2395
|
+
// Try git clone first if URL is cloneable
|
|
2396
|
+
if (target.sourceUrl && isCloneable(target.sourceUrl)) {
|
|
2397
|
+
result = await scanRepo(target.sourceUrl);
|
|
2398
|
+
}
|
|
2399
|
+
// Fallback 1: scan local installation
|
|
2400
|
+
if (!result && target.localDir) {
|
|
2401
|
+
result = await scanLocalDir(target.localDir, target.name);
|
|
2402
|
+
}
|
|
2403
|
+
// Fallback 2: download from PyPI/npm and scan
|
|
2404
|
+
if (!result && (target.pyPackage || target.npmPackage)) {
|
|
2405
|
+
result = await downloadAndScan(target);
|
|
2406
|
+
}
|
|
1583
2407
|
if (result) scanResults.push({ ...result, serverName: target.name });
|
|
1584
2408
|
}
|
|
1585
2409
|
|
|
1586
2410
|
if (scanResults.length > 1) {
|
|
1587
2411
|
// Print combined scan summary
|
|
1588
|
-
console.log(
|
|
1589
|
-
console.log(` ${c.bold}Scan Summary${c.reset} ${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} scanned`);
|
|
2412
|
+
console.log(sectionHeader(`Scan Summary — ${scanResults.length} server${scanResults.length !== 1 ? 's' : ''} scanned`));
|
|
1590
2413
|
console.log();
|
|
1591
2414
|
|
|
1592
2415
|
let totalFindings = 0;
|
|
@@ -1673,7 +2496,7 @@ async function discoverCommand(options = {}) {
|
|
|
1673
2496
|
}
|
|
1674
2497
|
|
|
1675
2498
|
if (!autoScan && !interactiveAudit && !jsonMode) {
|
|
1676
|
-
console.log(` ${c.dim}
|
|
2499
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit discover --quick${c.dim} to auto-scan all servers${c.reset}`);
|
|
1677
2500
|
console.log();
|
|
1678
2501
|
}
|
|
1679
2502
|
}
|
|
@@ -1695,7 +2518,7 @@ async function auditRepo(url) {
|
|
|
1695
2518
|
console.log();
|
|
1696
2519
|
|
|
1697
2520
|
// Step 1: Clone
|
|
1698
|
-
process.stdout.write(` ${
|
|
2521
|
+
process.stdout.write(` ${stepProgress(1, 4)} Cloning repository...`);
|
|
1699
2522
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentaudit-'));
|
|
1700
2523
|
const repoPath = path.join(tmpDir, 'repo');
|
|
1701
2524
|
try {
|
|
@@ -1710,12 +2533,12 @@ async function auditRepo(url) {
|
|
|
1710
2533
|
}
|
|
1711
2534
|
|
|
1712
2535
|
// Step 2: Collect files
|
|
1713
|
-
process.stdout.write(` ${
|
|
2536
|
+
process.stdout.write(` ${stepProgress(2, 4)} Collecting source files...`);
|
|
1714
2537
|
const files = collectFiles(repoPath);
|
|
1715
2538
|
console.log(` ${c.green}${files.length} files${c.reset}`);
|
|
1716
2539
|
|
|
1717
2540
|
// Step 3: Build audit payload
|
|
1718
|
-
process.stdout.write(` ${
|
|
2541
|
+
process.stdout.write(` ${stepProgress(3, 4)} Preparing audit payload...`);
|
|
1719
2542
|
const auditPrompt = loadAuditPrompt();
|
|
1720
2543
|
|
|
1721
2544
|
let codeBlock = '';
|
|
@@ -1788,7 +2611,7 @@ async function auditRepo(url) {
|
|
|
1788
2611
|
|
|
1789
2612
|
// We have an API key — run LLM audit
|
|
1790
2613
|
const modelLabel = modelOverride ? `${activeProvider} → ${activeLlm.model}` : activeProvider;
|
|
1791
|
-
process.stdout.write(` ${
|
|
2614
|
+
process.stdout.write(` ${stepProgress(4, 4)} Running LLM analysis ${c.dim}(${modelLabel})${c.reset}...`);
|
|
1792
2615
|
|
|
1793
2616
|
const systemPrompt = auditPrompt || 'You are a security auditor. Analyze the code and report findings as JSON.';
|
|
1794
2617
|
const userMessage = [
|
|
@@ -2002,17 +2825,26 @@ async function auditRepo(url) {
|
|
|
2002
2825
|
// Display results
|
|
2003
2826
|
console.log();
|
|
2004
2827
|
const riskScore = report.risk_score || 0;
|
|
2005
|
-
console.log(
|
|
2828
|
+
console.log(sectionHeader('Result'));
|
|
2829
|
+
console.log(` ${riskBadge(riskScore)}`);
|
|
2006
2830
|
console.log();
|
|
2007
|
-
|
|
2831
|
+
|
|
2008
2832
|
if (report.findings && report.findings.length > 0) {
|
|
2009
|
-
console.log(`
|
|
2833
|
+
console.log(sectionHeader(`Findings (${report.findings.length})`));
|
|
2010
2834
|
console.log();
|
|
2011
2835
|
for (const f of report.findings) {
|
|
2012
2836
|
const sc = severityColor(f.severity);
|
|
2013
|
-
console.log(` ${
|
|
2014
|
-
if (f.file) console.log(`
|
|
2015
|
-
if (f.description) console.log(`
|
|
2837
|
+
console.log(` ${sc}┃${c.reset} ${sc}${(f.severity || '').toUpperCase().padEnd(8)}${c.reset} ${c.bold}${f.title}${c.reset}`);
|
|
2838
|
+
if (f.file) console.log(` ${sc}┃${c.reset} ${c.dim}${f.file}${f.line ? ':' + f.line : ''}${c.reset}`);
|
|
2839
|
+
if (f.description) console.log(` ${sc}┃${c.reset} ${c.dim}${f.description.slice(0, 120)}${c.reset}`);
|
|
2840
|
+
console.log();
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
// Severity histogram
|
|
2844
|
+
const histLines = severityHistogram(report.findings);
|
|
2845
|
+
if (histLines.length > 1) {
|
|
2846
|
+
console.log(sectionHeader('Severity'));
|
|
2847
|
+
for (const line of histLines) console.log(line);
|
|
2016
2848
|
console.log();
|
|
2017
2849
|
}
|
|
2018
2850
|
} else {
|
|
@@ -2247,7 +3079,7 @@ function renderLeaderboardTab(data, width, opts = {}) {
|
|
|
2247
3079
|
}
|
|
2248
3080
|
|
|
2249
3081
|
const maxPts = leaderboard[0]?.total_points || 1;
|
|
2250
|
-
const medals = [
|
|
3082
|
+
const medals = [`${c.yellow}★${c.reset}`, `${c.cyan}★${c.reset}`, `${c.magenta}★${c.reset}`];
|
|
2251
3083
|
|
|
2252
3084
|
for (let i = 0; i < leaderboard.length; i++) {
|
|
2253
3085
|
const entry = leaderboard[i];
|
|
@@ -2289,20 +3121,22 @@ function renderBenchmarkTab(data, width) {
|
|
|
2289
3121
|
lines.push(` ${c.bold}${fmtNum(benchmark.models.length)}${c.reset} models ${c.dim}│${c.reset} ${c.bold}${fmtNum(overview.total_reports || 0)}${c.reset} audits ${c.dim}│${c.reset} ${c.bold}${fmtNum(overview.total_findings || 0)}${c.reset} findings`);
|
|
2290
3122
|
lines.push('');
|
|
2291
3123
|
|
|
2292
|
-
// Header
|
|
2293
|
-
const nameW =
|
|
2294
|
-
const
|
|
2295
|
-
|
|
3124
|
+
// Header — fixed column widths for alignment
|
|
3125
|
+
const nameW = 30;
|
|
3126
|
+
const auditsW = 6;
|
|
3127
|
+
const riskW = 5;
|
|
3128
|
+
const hdr = ` ${padRight('Model', nameW)} ${padLeft('Audits', auditsW)} ${padLeft('Risk', riskW)} ${'Detection'.padEnd(14)} Severity`;
|
|
3129
|
+
lines.push(` ${c.bold}${stripAnsi(hdr).trim()}${c.reset}`);
|
|
2296
3130
|
lines.push(` ${c.dim}${'─'.repeat(Math.min(width - 4, 86))}${c.reset}`);
|
|
2297
3131
|
|
|
2298
3132
|
for (const m of benchmark.models) {
|
|
2299
3133
|
const name = (m.audit_model || 'unknown').slice(0, nameW - 2);
|
|
2300
|
-
const audits = padLeft(fmtNum(m.total_audits),
|
|
3134
|
+
const audits = padLeft(fmtNum(m.total_audits), auditsW);
|
|
2301
3135
|
const riskVal = parseFloat(m.avg_risk_score) || 0;
|
|
2302
3136
|
const riskColor = riskVal <= 20 ? c.green : riskVal <= 40 ? c.yellow : c.red;
|
|
2303
|
-
const risk = `${riskColor}${padLeft(String(Math.round(riskVal)),
|
|
3137
|
+
const risk = `${riskColor}${padLeft(String(Math.round(riskVal)), riskW)}${c.reset}`;
|
|
2304
3138
|
const detection = renderGauge(m.detection_rate || 0, 100, 10);
|
|
2305
|
-
// Severity as compact text
|
|
3139
|
+
// Severity as compact text
|
|
2306
3140
|
const sev = m.severity_breakdown || {};
|
|
2307
3141
|
const sevParts = [];
|
|
2308
3142
|
if (sev.critical) sevParts.push(`${c.red}${sev.critical}C${c.reset}`);
|
|
@@ -2310,7 +3144,7 @@ function renderBenchmarkTab(data, width) {
|
|
|
2310
3144
|
if (sev.medium) sevParts.push(`${c.yellow}${sev.medium}M${c.reset}`);
|
|
2311
3145
|
if (sev.low) sevParts.push(`${c.blue}${sev.low}L${c.reset}`);
|
|
2312
3146
|
const sevStr = sevParts.length > 0 ? sevParts.join(' ') : `${c.dim}—${c.reset}`;
|
|
2313
|
-
lines.push(` ${padRight(name, nameW)}
|
|
3147
|
+
lines.push(` ${padRight(name, nameW)} ${audits} ${risk} ${detection} ${sevStr}`);
|
|
2314
3148
|
}
|
|
2315
3149
|
|
|
2316
3150
|
// Vulnerability landscape
|
|
@@ -2651,7 +3485,7 @@ async function leaderboardCommand(args) {
|
|
|
2651
3485
|
}
|
|
2652
3486
|
|
|
2653
3487
|
const maxPts = data[0]?.total_points || 1;
|
|
2654
|
-
const medals = [
|
|
3488
|
+
const medals = [`${c.yellow}★${c.reset}`, `${c.cyan}★${c.reset}`, `${c.magenta}★${c.reset}`];
|
|
2655
3489
|
const barW = 24;
|
|
2656
3490
|
|
|
2657
3491
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -2997,7 +3831,9 @@ async function main() {
|
|
|
2997
3831
|
discover: [
|
|
2998
3832
|
`${c.bold}agentaudit discover${c.reset} [options]`,
|
|
2999
3833
|
``,
|
|
3000
|
-
`Find MCP servers
|
|
3834
|
+
`Find MCP servers across 15+ AI tools (Claude Desktop, Claude Code, Cursor, VS Code,`,
|
|
3835
|
+
`Windsurf, Cline, Roo Code, Amazon Q, Gemini CLI, Zed, Continue.dev, Goose, Codex CLI).`,
|
|
3836
|
+
`Checks global + project-level configs on macOS, Windows, and Linux.`,
|
|
3001
3837
|
``,
|
|
3002
3838
|
`${c.bold}Options:${c.reset}`,
|
|
3003
3839
|
` --quick, -s Auto-scan all discovered servers (regex-based)`,
|
|
@@ -3185,6 +4021,21 @@ async function main() {
|
|
|
3185
4021
|
` agentaudit find fastmcp`,
|
|
3186
4022
|
],
|
|
3187
4023
|
find: null, // alias → search
|
|
4024
|
+
profile: [
|
|
4025
|
+
`${c.bold}agentaudit profile${c.reset}`,
|
|
4026
|
+
``,
|
|
4027
|
+
`Show your AgentAudit profile — rank, points, audit stats,`,
|
|
4028
|
+
`and a link to your public profile on agentaudit.dev.`,
|
|
4029
|
+
``,
|
|
4030
|
+
`Requires being logged in (run ${c.cyan}agentaudit setup${c.reset} first).`,
|
|
4031
|
+
``,
|
|
4032
|
+
`${c.bold}Options:${c.reset}`,
|
|
4033
|
+
` --json Machine-readable JSON output`,
|
|
4034
|
+
``,
|
|
4035
|
+
`${c.bold}Examples:${c.reset}`,
|
|
4036
|
+
` agentaudit profile`,
|
|
4037
|
+
` agentaudit profile --json`,
|
|
4038
|
+
],
|
|
3188
4039
|
};
|
|
3189
4040
|
|
|
3190
4041
|
// Show subcommand help: `agentaudit help <cmd>` or `agentaudit <cmd> --help`
|
|
@@ -3231,9 +4082,10 @@ async function main() {
|
|
|
3231
4082
|
console.log(` agentaudit <command> [options]`);
|
|
3232
4083
|
console.log();
|
|
3233
4084
|
console.log(` ${c.bold}SCAN & AUDIT${c.reset}`);
|
|
3234
|
-
console.log(` ${c.cyan}discover${c.reset} Find MCP servers in your AI
|
|
4085
|
+
console.log(` ${c.cyan}discover${c.reset} Find MCP servers & skills in your AI tools`);
|
|
3235
4086
|
console.log(` ${c.cyan}scan${c.reset} <url> [url...] Quick static scan (regex, ~2s)`);
|
|
3236
4087
|
console.log(` ${c.cyan}audit${c.reset} <url> [url...] Deep LLM-powered security audit (~30s)`);
|
|
4088
|
+
console.log(` ${c.cyan}validate${c.reset} [path] Validate SKILL.md format & security`);
|
|
3237
4089
|
console.log(` ${c.cyan}lookup${c.reset} <name> Look up package in registry`);
|
|
3238
4090
|
console.log();
|
|
3239
4091
|
console.log(` ${c.bold}COMMUNITY${c.reset}`);
|
|
@@ -3247,6 +4099,7 @@ async function main() {
|
|
|
3247
4099
|
console.log(` ${c.cyan}model${c.reset} Configure LLM provider + model`);
|
|
3248
4100
|
console.log(` ${c.cyan}setup${c.reset} Log in to agentaudit.dev (for report uploads)`);
|
|
3249
4101
|
console.log(` ${c.cyan}status${c.reset} Show current config + auth status`);
|
|
4102
|
+
console.log(` ${c.cyan}profile${c.reset} Your profile — rank, points, audit stats`);
|
|
3250
4103
|
console.log();
|
|
3251
4104
|
console.log(` ${c.bold}FLAGS${c.reset}`);
|
|
3252
4105
|
console.log(` ${c.dim}--json Machine-readable JSON output${c.reset}`);
|
|
@@ -3405,6 +4258,103 @@ async function main() {
|
|
|
3405
4258
|
return;
|
|
3406
4259
|
}
|
|
3407
4260
|
|
|
4261
|
+
if (command === 'profile') {
|
|
4262
|
+
const creds = loadCredentials();
|
|
4263
|
+
if (!creds) {
|
|
4264
|
+
console.log(` ${c.yellow}Not logged in.${c.reset}`);
|
|
4265
|
+
console.log(` ${c.dim}Run ${c.cyan}agentaudit setup${c.dim} to link your API key.${c.reset}`);
|
|
4266
|
+
console.log();
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
const agentName = creds.agent_name || 'unknown';
|
|
4271
|
+
console.log(` ${c.bold}Profile${c.reset} ${c.cyan}${agentName}${c.reset}`);
|
|
4272
|
+
console.log();
|
|
4273
|
+
|
|
4274
|
+
try {
|
|
4275
|
+
process.stdout.write(` ${c.dim}Fetching profile data...${c.reset}`);
|
|
4276
|
+
const [agentRes, lbRes] = await Promise.all([
|
|
4277
|
+
fetch(`${REGISTRY_URL}/api/agents/${encodeURIComponent(agentName)}`, {
|
|
4278
|
+
headers: { 'Authorization': `Bearer ${creds.api_key}` },
|
|
4279
|
+
signal: AbortSignal.timeout(10_000),
|
|
4280
|
+
}).then(r => r.ok ? r.json() : null),
|
|
4281
|
+
fetch(`${REGISTRY_URL}/api/leaderboard?limit=100`, {
|
|
4282
|
+
signal: AbortSignal.timeout(10_000),
|
|
4283
|
+
}).then(r => r.ok ? r.json() : null),
|
|
4284
|
+
]);
|
|
4285
|
+
process.stdout.write('\r\x1b[K');
|
|
4286
|
+
|
|
4287
|
+
if (!agentRes) {
|
|
4288
|
+
console.log(` ${c.yellow}Could not fetch profile data.${c.reset}`);
|
|
4289
|
+
console.log(` ${c.dim}Your account may not have submitted any audits yet.${c.reset}`);
|
|
4290
|
+
console.log();
|
|
4291
|
+
console.log(` ${c.dim}Web profile: ${c.cyan}${REGISTRY_URL}/profile${c.reset}`);
|
|
4292
|
+
console.log();
|
|
4293
|
+
return;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
let rank = null;
|
|
4297
|
+
if (Array.isArray(lbRes)) {
|
|
4298
|
+
const idx = lbRes.findIndex(e => e.agent_name === agentName);
|
|
4299
|
+
if (idx >= 0) rank = idx + 1;
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
// Update cache
|
|
4303
|
+
saveProfileCache({
|
|
4304
|
+
agent_name: agentName,
|
|
4305
|
+
rank,
|
|
4306
|
+
total_points: agentRes.total_points || 0,
|
|
4307
|
+
total_reports: agentRes.total_reports || 0,
|
|
4308
|
+
});
|
|
4309
|
+
|
|
4310
|
+
if (jsonMode) {
|
|
4311
|
+
console.log(JSON.stringify({
|
|
4312
|
+
agent_name: agentName,
|
|
4313
|
+
rank: rank ? { position: rank, total: lbRes?.length || 0 } : null,
|
|
4314
|
+
total_points: agentRes.total_points || 0,
|
|
4315
|
+
total_reports: agentRes.total_reports || 0,
|
|
4316
|
+
total_findings_submitted: agentRes.total_findings_submitted || 0,
|
|
4317
|
+
total_findings_confirmed: agentRes.total_findings_confirmed || 0,
|
|
4318
|
+
profile_url: `${REGISTRY_URL}/profile`,
|
|
4319
|
+
}, null, 2));
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4323
|
+
const boxW = 44;
|
|
4324
|
+
const rankStr = rank ? `#${rank} of ${lbRes.length}` : '—';
|
|
4325
|
+
const pts = agentRes.total_points || 0;
|
|
4326
|
+
const audits = agentRes.total_reports || 0;
|
|
4327
|
+
const findingsSubmitted = agentRes.total_findings_submitted || 0;
|
|
4328
|
+
const findingsConfirmed = agentRes.total_findings_confirmed || 0;
|
|
4329
|
+
const ptsBar = renderBar(pts, Math.max(pts, 1000), boxW - 16);
|
|
4330
|
+
|
|
4331
|
+
const contentLines = [
|
|
4332
|
+
'',
|
|
4333
|
+
`${c.bold}${c.cyan}${agentName}${c.reset}${' '.repeat(Math.max(1, boxW - 6 - agentName.length - rankStr.length))}${c.bold}${rankStr}${c.reset}`,
|
|
4334
|
+
'',
|
|
4335
|
+
`Points ${c.bold}${padLeft(fmtNum(pts), 8)}${c.reset}`,
|
|
4336
|
+
`Audits ${c.bold}${padLeft(fmtNum(audits), 8)}${c.reset}`,
|
|
4337
|
+
`Findings ${c.bold}${padLeft(fmtNum(findingsSubmitted), 8)}${c.reset} ${c.dim}(${fmtNum(findingsConfirmed)} confirmed)${c.reset}`,
|
|
4338
|
+
'',
|
|
4339
|
+
ptsBar,
|
|
4340
|
+
'',
|
|
4341
|
+
`${c.dim}${REGISTRY_URL}/profile${c.reset}`,
|
|
4342
|
+
`${c.dim}Key: ${creds.api_key.slice(0, 12)}...${c.reset}`,
|
|
4343
|
+
'',
|
|
4344
|
+
];
|
|
4345
|
+
const boxLines = drawBox('Profile', contentLines, boxW + 4);
|
|
4346
|
+
for (const line of boxLines) console.log(line);
|
|
4347
|
+
console.log();
|
|
4348
|
+
} catch (err) {
|
|
4349
|
+
process.stdout.write('\r\x1b[K');
|
|
4350
|
+
console.log(` ${c.yellow}Failed to fetch profile: ${err.message}${c.reset}`);
|
|
4351
|
+
console.log();
|
|
4352
|
+
console.log(` ${c.dim}Web profile: ${c.cyan}${REGISTRY_URL}/profile${c.reset}`);
|
|
4353
|
+
console.log();
|
|
4354
|
+
}
|
|
4355
|
+
return;
|
|
4356
|
+
}
|
|
4357
|
+
|
|
3408
4358
|
if (command === 'model') {
|
|
3409
4359
|
const newModel = targets.filter(t => !t.startsWith('--'))[0];
|
|
3410
4360
|
const config = loadLlmConfig();
|
|
@@ -3568,6 +4518,124 @@ async function main() {
|
|
|
3568
4518
|
return;
|
|
3569
4519
|
}
|
|
3570
4520
|
|
|
4521
|
+
if (command === 'validate') {
|
|
4522
|
+
const paths = targets.filter(t => !t.startsWith('--'));
|
|
4523
|
+
|
|
4524
|
+
// If no path given, find all skills and validate them
|
|
4525
|
+
if (paths.length === 0) {
|
|
4526
|
+
const skills = findSkills();
|
|
4527
|
+
if (skills.length === 0) {
|
|
4528
|
+
console.log(` ${c.yellow}No SKILL.md files found${c.reset}`);
|
|
4529
|
+
console.log(` ${c.dim}Searched: ~/.claude/skills/, ~/.cursor/skills/, .claude/skills/, .cursor/skills/${c.reset}`);
|
|
4530
|
+
console.log();
|
|
4531
|
+
console.log(` ${c.dim}Usage: ${c.cyan}agentaudit validate [path/to/SKILL.md]${c.reset}`);
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
console.log(` ${c.bold}Validating ${skills.length} skill${skills.length !== 1 ? 's' : ''}${c.reset}`);
|
|
4536
|
+
console.log();
|
|
4537
|
+
|
|
4538
|
+
let totalErrors = 0;
|
|
4539
|
+
let totalWarnings = 0;
|
|
4540
|
+
|
|
4541
|
+
for (const skill of skills) {
|
|
4542
|
+
const { errors, warnings, info } = skill.validation;
|
|
4543
|
+
totalErrors += errors.length;
|
|
4544
|
+
totalWarnings += warnings.length;
|
|
4545
|
+
|
|
4546
|
+
const name = info.name || skill.dirName;
|
|
4547
|
+
const hasErrors = errors.length > 0;
|
|
4548
|
+
const hasWarnings = warnings.length > 0;
|
|
4549
|
+
|
|
4550
|
+
if (hasErrors) {
|
|
4551
|
+
console.log(` ${c.red}✖${c.reset} ${c.bold}${name}${c.reset} ${c.dim}${skill.path}${c.reset}`);
|
|
4552
|
+
for (const err of errors) console.log(` ${c.red}✖ ${err}${c.reset}`);
|
|
4553
|
+
for (const warn of warnings) console.log(` ${c.yellow}⚠ ${warn}${c.reset}`);
|
|
4554
|
+
} else if (hasWarnings) {
|
|
4555
|
+
console.log(` ${c.yellow}⚠${c.reset} ${c.bold}${name}${c.reset} ${c.dim}${skill.path}${c.reset}`);
|
|
4556
|
+
for (const warn of warnings) console.log(` ${c.yellow}⚠ ${warn}${c.reset}`);
|
|
4557
|
+
} else {
|
|
4558
|
+
console.log(` ${c.green}✔${c.reset} ${c.bold}${name}${c.reset} ${c.dim}${skill.path}${c.reset}`);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
// Show MCP references
|
|
4562
|
+
if (info.mcpServers && info.mcpServers.length > 0) {
|
|
4563
|
+
console.log(` ${c.dim}MCP servers: ${info.mcpServers.join(', ')}${c.reset}`);
|
|
4564
|
+
}
|
|
4565
|
+
if (info.allowedTools === null) {
|
|
4566
|
+
console.log(` ${c.yellow}⚠ no allowed-tools — unrestricted tool access${c.reset}`);
|
|
4567
|
+
}
|
|
4568
|
+
console.log();
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
// Summary
|
|
4572
|
+
console.log(sectionHeader('Validation Summary'));
|
|
4573
|
+
console.log();
|
|
4574
|
+
if (totalErrors === 0 && totalWarnings === 0) {
|
|
4575
|
+
console.log(` ${c.green}✔ All ${skills.length} skills valid${c.reset}`);
|
|
4576
|
+
} else {
|
|
4577
|
+
if (totalErrors > 0) console.log(` ${c.red}✖ ${totalErrors} error${totalErrors !== 1 ? 's' : ''}${c.reset}`);
|
|
4578
|
+
if (totalWarnings > 0) console.log(` ${c.yellow}⚠ ${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}${c.reset}`);
|
|
4579
|
+
}
|
|
4580
|
+
console.log();
|
|
4581
|
+
|
|
4582
|
+
if (jsonMode) {
|
|
4583
|
+
console.log(JSON.stringify(skills.map(s => ({
|
|
4584
|
+
name: s.validation.info.name || s.dirName,
|
|
4585
|
+
path: s.path,
|
|
4586
|
+
source: s.source,
|
|
4587
|
+
errors: s.validation.errors,
|
|
4588
|
+
warnings: s.validation.warnings,
|
|
4589
|
+
mcpServers: s.validation.info.mcpServers,
|
|
4590
|
+
allowedTools: s.validation.info.allowedTools,
|
|
4591
|
+
})), null, 2));
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
process.exitCode = totalErrors > 0 ? 1 : 0;
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
|
|
4598
|
+
// Validate specific file(s)
|
|
4599
|
+
for (const p of paths) {
|
|
4600
|
+
const resolved = path.resolve(p);
|
|
4601
|
+
if (!fs.existsSync(resolved)) {
|
|
4602
|
+
console.log(` ${c.red}✖ File not found: ${p}${c.reset}`);
|
|
4603
|
+
process.exitCode = 1;
|
|
4604
|
+
continue;
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4607
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
4608
|
+
const parsed = parseSkillFrontmatter(content);
|
|
4609
|
+
const { errors, warnings, info } = validateSkill(parsed);
|
|
4610
|
+
const name = info.name || path.basename(path.dirname(resolved));
|
|
4611
|
+
|
|
4612
|
+
console.log(` ${c.bold}${name}${c.reset} ${c.dim}${resolved}${c.reset}`);
|
|
4613
|
+
console.log();
|
|
4614
|
+
|
|
4615
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
4616
|
+
console.log(` ${c.green}✔ Valid skill format${c.reset}`);
|
|
4617
|
+
}
|
|
4618
|
+
|
|
4619
|
+
for (const err of errors) console.log(` ${c.red}✖ ${err}${c.reset}`);
|
|
4620
|
+
for (const warn of warnings) console.log(` ${c.yellow}⚠ ${warn}${c.reset}`);
|
|
4621
|
+
|
|
4622
|
+
if (info.name) console.log(` ${c.dim}name:${c.reset} ${info.name}`);
|
|
4623
|
+
if (info.description) console.log(` ${c.dim}description:${c.reset} ${info.description.slice(0, 80)}${info.description.length > 80 ? '...' : ''}`);
|
|
4624
|
+
if (info.allowedTools) console.log(` ${c.dim}allowed-tools:${c.reset} ${info.allowedTools.join(', ')}`);
|
|
4625
|
+
else if (info.allowedTools === null) console.log(` ${c.yellow}allowed-tools: none (unrestricted)${c.reset}`);
|
|
4626
|
+
if (info.mcpServers?.length > 0) console.log(` ${c.dim}MCP servers:${c.reset} ${info.mcpServers.join(', ')}`);
|
|
4627
|
+
if (info.bodyLines) console.log(` ${c.dim}body:${c.reset} ${info.bodyLines} lines`);
|
|
4628
|
+
console.log();
|
|
4629
|
+
|
|
4630
|
+
if (jsonMode) {
|
|
4631
|
+
console.log(JSON.stringify({ name: info.name, path: resolved, errors, warnings, info }, null, 2));
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
process.exitCode = errors.length > 0 ? 1 : 0;
|
|
4635
|
+
}
|
|
4636
|
+
return;
|
|
4637
|
+
}
|
|
4638
|
+
|
|
3571
4639
|
if (command === 'discover') {
|
|
3572
4640
|
const scanFlag = targets.includes('--quick') || targets.includes('--scan') || targets.includes('-s');
|
|
3573
4641
|
const auditFlag = targets.includes('--deep') || targets.includes('--audit') || targets.includes('-a');
|