agentverify 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -4,9 +4,645 @@ import {
4
4
  } from "./chunk-XVA2M2HS.mjs";
5
5
 
6
6
  // src/cli.ts
7
- import { readFileSync, readdirSync, statSync, appendFileSync, writeFileSync } from "fs";
8
- import { join, relative, extname } from "path";
9
- var VERSION = "1.4.0";
7
+ import { readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync2, appendFileSync, writeFileSync } from "fs";
8
+ import { join as join2, relative as relative2, extname as extname2 } from "path";
9
+
10
+ // src/mcp.ts
11
+ import { createInterface } from "readline";
12
+ import { readFileSync as readFileSync2 } from "fs";
13
+ import { resolve } from "path";
14
+
15
+ // src/files.ts
16
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
17
+ import { basename, extname, join, relative, sep } from "path";
18
+ import { homedir, platform } from "os";
19
+ var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
20
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache", ".venv", "venv", "__pycache__", "out", "target"]);
21
+ var SKIP_FILES = /* @__PURE__ */ new Set(["package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "Cargo.lock", "composer.lock", "tsconfig.json"]);
22
+ var MAX_FILES = 60;
23
+ var MAX_TOTAL_BYTES = 5 * 1024 * 1024;
24
+ var MAX_FILE_BYTES = 400 * 1024;
25
+ function filePriority(relPath) {
26
+ const p = relPath.toLowerCase().replace(/\\/g, "/");
27
+ const name = p.split("/").pop() ?? p;
28
+ let score = 0;
29
+ if (/(^|\/)(tests?|__tests__|spec|e2e|fixtures?|examples?)\//.test(p) || /\.(test|spec)\.[a-z]+$/.test(name)) score -= 80;
30
+ if (/(agent|tool|mcp|prompt|crew|graph|server|config|policy|skill)/.test(name)) score += 30;
31
+ if (/(^|\/)(agents?|tools?|mcp|src|app|lib)\//.test(p)) score += 10;
32
+ if (name.endsWith(".md")) score -= 20;
33
+ if (name === "skill.md" || name === "agents.md" || name === "claude.md") score += 40;
34
+ return score - p.split("/").length;
35
+ }
36
+ function walk(dir, out) {
37
+ let entries;
38
+ try {
39
+ entries = readdirSync(dir);
40
+ } catch {
41
+ return;
42
+ }
43
+ for (const entry of entries) {
44
+ if (SKIP_DIRS.has(entry) || SKIP_FILES.has(entry)) continue;
45
+ const full = join(dir, entry);
46
+ let st;
47
+ try {
48
+ st = statSync(full);
49
+ } catch {
50
+ continue;
51
+ }
52
+ if (st.isDirectory()) walk(full, out);
53
+ else if (SCANNABLE_EXTENSIONS.has(extname(entry).toLowerCase())) out.push(full);
54
+ }
55
+ }
56
+ function pickFiles(target) {
57
+ const notes = [];
58
+ let st;
59
+ try {
60
+ st = statSync(target);
61
+ } catch {
62
+ throw new Error(`Path not found: ${target}`);
63
+ }
64
+ if (st.isFile()) {
65
+ if (st.size > MAX_TOTAL_BYTES) throw new Error(`${basename(target)} is larger than 5MB, which is the most one scan can take.`);
66
+ return { files: [{ path: basename(target), content: readFileSync(target, "utf8") }], notes };
67
+ }
68
+ const all = [];
69
+ walk(target, all);
70
+ if (all.length === 0) throw new Error(`No agent files found in ${target}. Agent Verify reads ${[...SCANNABLE_EXTENSIONS].join(" ")} files.`);
71
+ const ranked = all.map((full) => ({ full, rel: relative(target, full).split(sep).join("/") })).sort((a, b) => filePriority(b.rel) - filePriority(a.rel) || a.rel.localeCompare(b.rel));
72
+ const files = [];
73
+ let total = 0;
74
+ let tooBig = 0;
75
+ for (const { full, rel } of ranked) {
76
+ if (files.length >= MAX_FILES) break;
77
+ let size = 0;
78
+ try {
79
+ size = statSync(full).size;
80
+ } catch {
81
+ continue;
82
+ }
83
+ if (size > MAX_FILE_BYTES) {
84
+ tooBig++;
85
+ continue;
86
+ }
87
+ if (total + size > MAX_TOTAL_BYTES) break;
88
+ files.push({ path: rel, content: readFileSync(full, "utf8") });
89
+ total += size;
90
+ }
91
+ const left = ranked.length - files.length - tooBig;
92
+ if (left > 0) notes.push(`${left} lower-priority file(s) were left out to stay within one scan (${MAX_FILES} files / 5MB).`);
93
+ if (tooBig > 0) notes.push(`${tooBig} file(s) over 400KB were skipped.`);
94
+ return { files, notes };
95
+ }
96
+ function combineFiles(files) {
97
+ if (files.length === 1) return files[0].content;
98
+ return files.map((f) => `// ===== FILE: ${f.path} =====
99
+ ${f.content}`).join("\n\n");
100
+ }
101
+ function locateLine(files, combinedLine) {
102
+ if (files.length === 1) return { path: files[0].path, line: combinedLine };
103
+ let cursor = 1;
104
+ for (let i = 0; i < files.length; i++) {
105
+ const start = cursor + 1;
106
+ const lines = files[i].content.split("\n").length;
107
+ if (combinedLine >= start && combinedLine < start + lines) return { path: files[i].path, line: combinedLine - start + 1 };
108
+ cursor = start + lines + 1;
109
+ }
110
+ return null;
111
+ }
112
+ function knownMcpConfigPaths(cwd = process.cwd()) {
113
+ const home = homedir();
114
+ const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
115
+ const claudeDesktop = platform() === "win32" ? join(appData, "Claude", "claude_desktop_config.json") : platform() === "darwin" ? join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json") : join(home, ".config", "Claude", "claude_desktop_config.json");
116
+ return [
117
+ { client: "Claude Code (this project)", path: join(cwd, ".mcp.json") },
118
+ { client: "Claude Code (user)", path: join(home, ".claude.json") },
119
+ { client: "Claude Desktop", path: claudeDesktop },
120
+ { client: "Cursor (this project)", path: join(cwd, ".cursor", "mcp.json") },
121
+ { client: "Cursor (user)", path: join(home, ".cursor", "mcp.json") },
122
+ { client: "VS Code (this project)", path: join(cwd, ".vscode", "mcp.json") },
123
+ { client: "Windsurf", path: join(home, ".codeium", "windsurf", "mcp_config.json") }
124
+ ];
125
+ }
126
+ function extractMcpServers(raw) {
127
+ let data;
128
+ try {
129
+ data = JSON.parse(raw);
130
+ } catch {
131
+ return null;
132
+ }
133
+ if (!data || typeof data !== "object") return null;
134
+ const servers = {};
135
+ const take = (obj, prefix = "") => {
136
+ if (!obj || typeof obj !== "object") return;
137
+ for (const key of ["mcpServers", "servers"]) {
138
+ const block = obj[key];
139
+ if (block && typeof block === "object" && !Array.isArray(block)) {
140
+ for (const [name, def] of Object.entries(block)) servers[prefix + name] = def;
141
+ }
142
+ }
143
+ };
144
+ take(data);
145
+ const projects = data.projects;
146
+ if (projects && typeof projects === "object") {
147
+ for (const [proj, value] of Object.entries(projects)) take(value, `${basename(proj)}/`);
148
+ }
149
+ return Object.keys(servers).length ? servers : null;
150
+ }
151
+ var SECRET_NAME = /(TOKEN|KEY|SECRET|PASSWORD|PASSWD|PAT|AUTH|CREDENTIAL|COOKIE|SESSION)/i;
152
+ var SECRET_VALUE = /^(sk-|sk_|ghp_|gho_|github_pat_|xox[abp]-|AKIA|AIza|hf_|npm_|glpat-|eyJ)/;
153
+ function mask(value) {
154
+ return value.length <= 4 ? "****redacted" : `${value.slice(0, 4)}****redacted`;
155
+ }
156
+ function redactMcpSecrets(servers) {
157
+ const clean = (node, parentKey = "") => {
158
+ if (typeof node === "string") {
159
+ if (/^\$\{|^<|^\$[A-Z_]/.test(node)) return node;
160
+ if (SECRET_NAME.test(parentKey) || SECRET_VALUE.test(node)) return mask(node);
161
+ const flag = node.match(/^(--?[A-Za-z0-9_-]*(?:token|key|secret|password|auth)[A-Za-z0-9_-]*=)(.+)$/i);
162
+ if (flag) return flag[1] + mask(flag[2]);
163
+ return node;
164
+ }
165
+ if (Array.isArray(node)) return node.map((item) => clean(item, parentKey));
166
+ if (node && typeof node === "object") {
167
+ const out = {};
168
+ for (const [k, v] of Object.entries(node)) out[k] = clean(v, k);
169
+ return out;
170
+ }
171
+ return node;
172
+ };
173
+ return clean(servers);
174
+ }
175
+ function existingMcpConfigs(cwd = process.cwd()) {
176
+ return knownMcpConfigPaths(cwd).filter((c2) => existsSync(c2.path));
177
+ }
178
+
179
+ // src/mcp.ts
180
+ var SUPPORTED_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
181
+ var PRICING_URL = "https://aimodularity.com/agentverify/pricing/";
182
+ var DASHBOARD_URL = "https://aimodularity.com/agentverify/dashboard/";
183
+ var TOOLS = [
184
+ {
185
+ name: "scan_path",
186
+ title: "Scan agent files",
187
+ description: "Scan an AI agent for security risks before it is deployed: dangerous tool permissions, prompt injection paths, exposed secrets, risky MCP servers, missing approval gates, SQL/SSRF/code-execution risks, and framework-specific issues (LangChain, CrewAI, AutoGen, OpenAI Agents SDK, Claude Agent SDK). Give a file or a folder; a folder is scanned as one report using the most important agent files (up to 60 files / 5MB). Returns a verdict, a 0-100 security score (higher is safer) and findings with file and line.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: { path: { type: "string", description: "File or folder to scan. Relative paths are resolved from the current project." } },
191
+ required: ["path"],
192
+ additionalProperties: false
193
+ }
194
+ },
195
+ {
196
+ name: "scan_code",
197
+ title: "Scan agent code",
198
+ description: "Scan a snippet of agent code, a tool definition, a system prompt or an MCP config for security risks. Use this for code that is not saved to a file yet. Returns a verdict, a security score and findings.",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {
202
+ content: { type: "string", description: "The code or config to scan." },
203
+ fileName: { type: "string", description: "A file name with the right extension (e.g. agent.py, mcp.json) so the language is detected." }
204
+ },
205
+ required: ["content"],
206
+ additionalProperties: false
207
+ }
208
+ },
209
+ {
210
+ name: "scan_mcp_config",
211
+ title: "Check my MCP servers",
212
+ description: "Check the MCP servers configured on this computer (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf) for risks such as broad filesystem access, unpinned packages, remote servers without auth, auto-approved tools and API keys stored in plain text. Only the server definitions are read, and secret values are masked on this computer before anything is sent.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: { path: { type: "string", description: "Optional: a specific MCP config file. Leave empty to check all known locations." } },
216
+ additionalProperties: false
217
+ }
218
+ }
219
+ ];
220
+ var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
221
+ var SEVERITY_LABEL = { critical: "CRITICAL", high: "HIGH", medium: "MEDIUM", low: "LOW" };
222
+ function verdictLine(result) {
223
+ const verdict = result.verdict === "VERIFIED" ? "VERIFIED" : result.verdict === "NOT_ASSESSED" ? "NOT ASSESSED" : "NOT VERIFIED";
224
+ return `**Verdict: ${verdict}** \u2014 Security score ${result.riskScore}/100 (higher is safer, ${result.riskLevel})`;
225
+ }
226
+ function fixText(f) {
227
+ if (f.fixLocked) return `Fix: included with Agent Verify Pro (${PRICING_URL})`;
228
+ const steps = (f.recommendedFix ?? "").trim();
229
+ const code = (f.quickFix ?? f.fixCode ?? "").trim();
230
+ if (!steps && !code) return null;
231
+ return [steps ? `Fix: ${steps}` : "", code ? "```\n" + code + "\n```" : ""].filter(Boolean).join("\n");
232
+ }
233
+ function formatReport(result, opts) {
234
+ const findings = [...result.findings ?? []].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
235
+ const counts = ["critical", "high", "medium", "low"].map((s) => [s, findings.filter((f) => f.severity === s).length]).filter(([, n]) => n > 0);
236
+ const lines = [`# Agent Verify: ${opts.title}`, "", verdictLine(result)];
237
+ lines.push(findings.length ? `Findings: ${counts.map(([s, n]) => `${n} ${s}`).join(", ")}` : "No findings.");
238
+ if (result.reportInsights?.topBlocker) lines.push(`Fix first: ${result.reportInsights.topBlocker}`);
239
+ for (const note of opts.notes ?? []) lines.push(`Note: ${note}`);
240
+ findings.forEach((f, i) => {
241
+ const where = typeof f.line === "number" && f.line > 0 ? opts.files ? locateLine(opts.files, f.line) : { path: opts.title, line: f.line } : null;
242
+ lines.push("", `## ${i + 1}. [${SEVERITY_LABEL[f.severity] ?? f.severity}] ${f.title} (${f.code})`);
243
+ if (where) lines.push(`Location: ${where.path}:${where.line}`);
244
+ if (f.evidence) lines.push(`Evidence: ${f.evidence}`);
245
+ lines.push(`What is wrong: ${f.whatIsWrong}`);
246
+ if (f.whyItMatters) lines.push(`Why it matters: ${f.whyItMatters}`);
247
+ const fix = fixText(f);
248
+ if (fix) lines.push(fix);
249
+ });
250
+ const locked = findings.filter((f) => f.fixLocked).length;
251
+ if (locked > 0) lines.push("", `Step-by-step fixes and corrected code for ${locked} finding(s) are included with Pro: ${PRICING_URL}`);
252
+ if (result.reportUrl) lines.push("", `Full report: ${result.reportUrl}`);
253
+ lines.push("", "Agent Verify is an automated, evidence-based check, not a guarantee. Re-scan after making changes.");
254
+ return lines.join("\n");
255
+ }
256
+ function textResult(text, isError = false, structured) {
257
+ return { content: [{ type: "text", text }], ...isError ? { isError: true } : {}, ...structured ? { structuredContent: structured } : {} };
258
+ }
259
+ function summary(result) {
260
+ return {
261
+ verdict: result.verdict,
262
+ securityScore: result.riskScore,
263
+ riskLevel: result.riskLevel,
264
+ findings: (result.findings ?? []).map((f) => ({ code: f.code, severity: f.severity, title: f.title, line: f.line ?? null })),
265
+ reportUrl: result.reportUrl ?? null
266
+ };
267
+ }
268
+ async function callTool(name, args, opts) {
269
+ const tool = TOOLS.find((t) => t.name === name);
270
+ if (!tool) return textResult(`Unknown tool: ${name}`, true);
271
+ if (!opts.apiKey) {
272
+ return textResult(
273
+ "Agent Verify needs an API key. Create a free one in the dashboard (API / CLI tab): " + DASHBOARD_URL + "\nThen set AGENTVERIFY_API_KEY in this MCP server's environment and restart it.",
274
+ true
275
+ );
276
+ }
277
+ const doScan = opts.scanFn ?? scan;
278
+ const cwd = opts.cwd ?? process.cwd();
279
+ try {
280
+ if (name === "scan_code") {
281
+ const content2 = typeof args.content === "string" ? args.content : "";
282
+ if (!content2.trim()) return textResult('Nothing to scan: "content" is empty.', true);
283
+ const fileName = typeof args.fileName === "string" && args.fileName.trim() ? args.fileName.trim() : "agent";
284
+ const result2 = await doScan({ content: content2, fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
285
+ return textResult(formatReport(result2, { title: fileName }), false, summary(result2));
286
+ }
287
+ if (name === "scan_path") {
288
+ const raw = typeof args.path === "string" ? args.path.trim() : "";
289
+ if (!raw) return textResult('Give a file or folder "path" to scan.', true);
290
+ const target = resolve(cwd, raw);
291
+ const picked = pickFiles(target);
292
+ const fileName = picked.files.length === 1 ? picked.files[0].path : `${raw.replace(/[\\/]+$/, "") || "project"} (${picked.files.length} files)`;
293
+ const result2 = await doScan({ content: combineFiles(picked.files), fileName, platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
294
+ const notes = [...picked.notes];
295
+ if (picked.files.length > 1) notes.unshift(`Scanned ${picked.files.length} files together as one report.`);
296
+ return textResult(formatReport(result2, { title: fileName, files: picked.files, notes }), false, summary(result2));
297
+ }
298
+ const explicit = typeof args.path === "string" && args.path.trim() ? resolve(cwd, args.path.trim()) : "";
299
+ const configs = explicit ? [{ client: "Config file", path: explicit }] : existingMcpConfigs(cwd);
300
+ if (configs.length === 0) return textResult('No MCP config files were found in the usual places (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf). Pass a "path" to check a specific file.');
301
+ const servers = {};
302
+ const read = [];
303
+ for (const cfg of configs) {
304
+ let rawText = "";
305
+ try {
306
+ rawText = readFileSync2(cfg.path, "utf8");
307
+ } catch {
308
+ continue;
309
+ }
310
+ const found = extractMcpServers(rawText);
311
+ if (!found) continue;
312
+ read.push(`${cfg.client}: ${Object.keys(found).length} server(s)`);
313
+ for (const [server, def] of Object.entries(found)) servers[`${cfg.client} \u203A ${server}`] = def;
314
+ }
315
+ if (Object.keys(servers).length === 0) return textResult(`Found ${configs.length} config file(s), but none of them define any MCP servers.`);
316
+ const content = JSON.stringify({ mcpServers: redactMcpSecrets(servers) }, null, 2);
317
+ const result = await doScan({ content, fileName: "mcp.json", platform: "MCP" }, { apiKey: opts.apiKey, apiUrl: opts.apiUrl });
318
+ return textResult(
319
+ formatReport(result, { title: `MCP servers (${Object.keys(servers).length})`, notes: [`Checked: ${read.join("; ")}`, "Secret values were masked on this computer before scanning."] }),
320
+ false,
321
+ summary(result)
322
+ );
323
+ } catch (err) {
324
+ return textResult(`Scan failed: ${err instanceof Error ? err.message : String(err)}`, true);
325
+ }
326
+ }
327
+ async function handleMessage(msg, opts) {
328
+ const isNotification = msg.id === void 0 || msg.id === null;
329
+ const reply = (result) => isNotification ? null : { jsonrpc: "2.0", id: msg.id, result };
330
+ const fail = (code, message) => isNotification ? null : { jsonrpc: "2.0", id: msg.id ?? null, error: { code, message } };
331
+ switch (msg.method) {
332
+ case "initialize": {
333
+ const requested = typeof msg.params?.protocolVersion === "string" ? msg.params.protocolVersion : "";
334
+ return reply({
335
+ protocolVersion: SUPPORTED_PROTOCOLS.includes(requested) ? requested : SUPPORTED_PROTOCOLS[0],
336
+ capabilities: { tools: { listChanged: false } },
337
+ serverInfo: { name: "agentverify", title: "Agent Verify", version: opts.version },
338
+ instructions: "Use Agent Verify to check AI agent code, tools, prompts and MCP configs for security risks before deploying. Run scan_path on the agent folder after making changes, and fix critical and high findings first."
339
+ });
340
+ }
341
+ case "ping":
342
+ return reply({});
343
+ case "tools/list":
344
+ return reply({ tools: TOOLS });
345
+ case "tools/call": {
346
+ const name = typeof msg.params?.name === "string" ? msg.params.name : "";
347
+ const args = msg.params?.arguments && typeof msg.params.arguments === "object" ? msg.params.arguments : {};
348
+ return reply(await callTool(name, args, opts));
349
+ }
350
+ case "resources/list":
351
+ return reply({ resources: [] });
352
+ case "prompts/list":
353
+ return reply({ prompts: [] });
354
+ default:
355
+ if (typeof msg.method === "string" && msg.method.startsWith("notifications/")) return null;
356
+ return fail(-32601, `Method not found: ${msg.method}`);
357
+ }
358
+ }
359
+ function runMcpServer(opts) {
360
+ return new Promise((done) => {
361
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
362
+ const send = (obj) => process.stdout.write(JSON.stringify(obj) + "\n");
363
+ let pending = 0;
364
+ let closed = false;
365
+ const maybeDone = () => {
366
+ if (closed && pending === 0) done();
367
+ };
368
+ rl.on("line", (line) => {
369
+ const text = line.trim();
370
+ if (!text) return;
371
+ let msg;
372
+ try {
373
+ msg = JSON.parse(text);
374
+ } catch {
375
+ send({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } });
376
+ return;
377
+ }
378
+ pending++;
379
+ handleMessage(msg, opts).then((res) => {
380
+ if (res) send(res);
381
+ }).catch((err) => send({ jsonrpc: "2.0", id: msg.id ?? null, error: { code: -32603, message: err instanceof Error ? err.message : "Internal error" } })).finally(() => {
382
+ pending--;
383
+ maybeDone();
384
+ });
385
+ });
386
+ rl.on("close", () => {
387
+ closed = true;
388
+ maybeDone();
389
+ });
390
+ process.stderr.write(`Agent Verify MCP server ${opts.version} ready${opts.apiKey ? "" : " (no API key set: tools will explain how to add one)"}
391
+ `);
392
+ });
393
+ }
394
+
395
+ // src/profile.ts
396
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
397
+ var SUPPORTED_PROFILES = ["owasp-agentic-skills-2026"];
398
+ var SUPPORTED_ASSESSMENT_SCHEMA = "1.1.0";
399
+ var LOCAL_LIMITS = { maxFiles: 5e3, maxFileBytes: 8 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024, maxDepth: 64 };
400
+ var VENDORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
401
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
402
+ ".png",
403
+ ".jpg",
404
+ ".jpeg",
405
+ ".gif",
406
+ ".webp",
407
+ ".ico",
408
+ ".bmp",
409
+ ".tif",
410
+ ".tiff",
411
+ ".avif",
412
+ ".pdf",
413
+ ".woff",
414
+ ".woff2",
415
+ ".ttf",
416
+ ".otf",
417
+ ".eot",
418
+ ".zip",
419
+ ".gz",
420
+ ".tgz",
421
+ ".tar",
422
+ ".bz2",
423
+ ".xz",
424
+ ".7z",
425
+ ".mp3",
426
+ ".mp4",
427
+ ".mov",
428
+ ".wav",
429
+ ".ogg",
430
+ ".webm",
431
+ ".wasm",
432
+ ".so",
433
+ ".dll",
434
+ ".dylib",
435
+ ".exe",
436
+ ".class",
437
+ ".jar",
438
+ ".pyc",
439
+ ".sqlite",
440
+ ".db"
441
+ ]);
442
+ var PackageError = class extends Error {
443
+ };
444
+ function displayPath(path) {
445
+ return JSON.stringify(safeText(path));
446
+ }
447
+ function safeText(text) {
448
+ let out = "";
449
+ for (const ch of text) {
450
+ const cp = ch.codePointAt(0);
451
+ const hidden = cp < 32 || cp >= 127 && cp <= 159 || cp === 173 || cp === 1564 || cp === 6158 || cp === 65279 || cp >= 8203 && cp <= 8207 || cp >= 8234 && cp <= 8238 || cp >= 8288 && cp <= 8292 || cp >= 8294 && cp <= 8297 || cp >= 917504 && cp <= 917999 || cp >= 55296 && cp <= 57343;
452
+ out += hidden ? `\\u{${cp.toString(16).toUpperCase().padStart(4, "0")}}` : ch;
453
+ }
454
+ return out;
455
+ }
456
+ function nameProblem(name) {
457
+ for (let i = 0; i < name.length; i++) {
458
+ const c2 = name.charCodeAt(i);
459
+ if (c2 >= 55296 && c2 <= 56319) {
460
+ const next = name.charCodeAt(i + 1);
461
+ if (next >= 56320 && next <= 57343) {
462
+ i++;
463
+ continue;
464
+ }
465
+ return "contains an unpaired UTF-16 surrogate";
466
+ }
467
+ if (c2 >= 56320 && c2 <= 57343) return "contains an unpaired UTF-16 surrogate";
468
+ }
469
+ return null;
470
+ }
471
+ var strictUtf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
472
+ function decodeStrict(bytes) {
473
+ return strictUtf8.decode(bytes);
474
+ }
475
+ function listEntries(dir, shown) {
476
+ const entries = [];
477
+ if (process.platform === "win32") {
478
+ for (const d of readdirSync2(dir, { withFileTypes: true })) {
479
+ const problem = nameProblem(d.name);
480
+ if (problem) throw new PackageError(`An entry in ${displayPath(shown)} ${problem}, so it cannot be sent faithfully.`);
481
+ entries.push({ name: d.name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
482
+ }
483
+ } else {
484
+ for (const d of readdirSync2(dir, { withFileTypes: true, encoding: "buffer" })) {
485
+ let name;
486
+ try {
487
+ name = decodeStrict(d.name);
488
+ } catch {
489
+ throw new PackageError(`An entry in ${displayPath(shown)} has a name that is not valid UTF-8, so it cannot be sent faithfully.`);
490
+ }
491
+ entries.push({ name, kind: d.isSymbolicLink() ? "symlink" : d.isDirectory() ? "dir" : d.isFile() ? "file" : "other" });
492
+ }
493
+ }
494
+ return entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
495
+ }
496
+ var symlinkMessage = (rel) => `${displayPath(rel)} is a symbolic link. The CLI does not follow symbolic links, and a package containing one cannot be represented faithfully. Replace it with the file or directory it points to, or remove it.`;
497
+ var specialMessage = (rel) => `${displayPath(rel)} is not a regular file or directory (it is a device, socket or pipe), so it cannot be sent.`;
498
+ function readRegularFile(full, rel, budget) {
499
+ let fd;
500
+ try {
501
+ fd = openSync(full, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
502
+ } catch (e) {
503
+ const code = e.code;
504
+ if (code === "ELOOP") throw new PackageError(symlinkMessage(rel));
505
+ throw new PackageError(`Could not read ${displayPath(rel)}: ${code ?? "unknown error"}.`);
506
+ }
507
+ try {
508
+ const st = fstatSync(fd);
509
+ if (!st.isFile()) throw new PackageError(specialMessage(rel));
510
+ if (st.size > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} is ${st.size} bytes, over the CLI's local read guard of ${LOCAL_LIMITS.maxFileBytes} bytes.`);
511
+ const bytes = readFileSync3(fd);
512
+ if (bytes.length > LOCAL_LIMITS.maxFileBytes) throw new PackageError(`${displayPath(rel)} grew past the CLI's local read guard while it was being read.`);
513
+ budget.total += bytes.length;
514
+ if (budget.total > LOCAL_LIMITS.maxTotalBytes) throw new PackageError(`The package is larger than the CLI's local guard of ${LOCAL_LIMITS.maxTotalBytes} bytes.`);
515
+ return bytes;
516
+ } finally {
517
+ closeSync(fd);
518
+ }
519
+ }
520
+ var extensionOf = (name) => {
521
+ const i = name.lastIndexOf(".");
522
+ return i <= 0 ? "" : name.slice(i).toLowerCase();
523
+ };
524
+ function buildPackage(root) {
525
+ let rootStat;
526
+ try {
527
+ rootStat = lstatSync(root);
528
+ } catch {
529
+ throw new PackageError(`${displayPath(root)} does not exist or cannot be read.`);
530
+ }
531
+ if (rootStat.isSymbolicLink()) throw new PackageError(symlinkMessage(root));
532
+ if (!rootStat.isDirectory()) throw new PackageError(`${displayPath(root)} is not a directory. A profile assessment takes a directory.`);
533
+ const files = [];
534
+ const skipped = [];
535
+ const budget = { total: 0 };
536
+ const walk2 = (dir, prefix, depth) => {
537
+ if (depth > LOCAL_LIMITS.maxDepth) throw new PackageError(`The package is nested deeper than ${LOCAL_LIMITS.maxDepth} directories.`);
538
+ for (const entry of listEntries(dir, prefix || ".")) {
539
+ const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
540
+ const full = `${dir}/${entry.name}`;
541
+ if (entry.kind === "symlink") throw new PackageError(symlinkMessage(rel));
542
+ if (entry.kind === "other") throw new PackageError(specialMessage(rel));
543
+ if (entry.kind === "dir") {
544
+ if (VENDORED_DIRS.has(entry.name)) {
545
+ skipped.push({ path: rel, reason: "vendored directory" });
546
+ continue;
547
+ }
548
+ walk2(full, rel, depth + 1);
549
+ continue;
550
+ }
551
+ if (BINARY_EXTENSIONS.has(extensionOf(entry.name))) {
552
+ skipped.push({ path: rel, reason: "binary asset" });
553
+ continue;
554
+ }
555
+ if (files.length >= LOCAL_LIMITS.maxFiles) throw new PackageError(`The package has more than ${LOCAL_LIMITS.maxFiles} files, over the CLI's local guard.`);
556
+ const bytes = readRegularFile(full, rel, budget);
557
+ let content;
558
+ try {
559
+ content = decodeStrict(bytes);
560
+ } catch {
561
+ throw new PackageError(`${displayPath(rel)} is not valid UTF-8 text, so it cannot be sent faithfully. If it is a binary asset, give it a recognized binary extension or remove it from the package.`);
562
+ }
563
+ files.push({ path: rel, content });
564
+ }
565
+ };
566
+ walk2(root, "", 0);
567
+ if (files.length === 0) throw new PackageError(`${displayPath(root)} contains no files to assess.`);
568
+ return { files, skipped };
569
+ }
570
+ function serializeRequest(profile, files) {
571
+ return JSON.stringify({ profile, files });
572
+ }
573
+ async function postProfile(apiUrl, apiKey, body, timeoutMs) {
574
+ const controller = new AbortController();
575
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
576
+ try {
577
+ const response = await fetch(apiUrl, {
578
+ method: "POST",
579
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "User-Agent": "agentverify-cli/2.0.0" },
580
+ body,
581
+ signal: controller.signal
582
+ });
583
+ const text = await response.text();
584
+ let json = null;
585
+ try {
586
+ json = JSON.parse(text);
587
+ } catch {
588
+ json = null;
589
+ }
590
+ return { kind: "response", status: response.status, text, json };
591
+ } catch (e) {
592
+ if (e instanceof Error && e.name === "AbortError") return { kind: "failure", message: `Request timed out after ${timeoutMs}ms` };
593
+ return { kind: "failure", message: `Request failed: ${e instanceof Error ? e.message : String(e)}` };
594
+ } finally {
595
+ clearTimeout(timer);
596
+ }
597
+ }
598
+ var isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
599
+ var str = (v) => typeof v === "string" ? safeText(v) : typeof v === "number" || typeof v === "boolean" ? String(v) : "";
600
+ function formatAssessment(body, skipped) {
601
+ const a = isObj(body.assessment) ? body.assessment : {};
602
+ const profile = isObj(a.profile) ? a.profile : {};
603
+ const pkg = isObj(a.package) ? a.package : {};
604
+ const lines = [];
605
+ lines.push("Agent Verify profile assessment");
606
+ lines.push(`Profile: ${str(body.profile)}`);
607
+ lines.push(`Framework: ${str(profile.framework)} (upstream ${str(profile.upstreamRepo)} @ ${str(profile.upstreamCommit).slice(0, 12)}, ${str(profile.upstreamStatus)}; profile version ${str(profile.agentverifyProfileVersion)})`);
608
+ lines.push(`Assessment schema: ${str(a.schemaVersion)}`);
609
+ lines.push(`Interpretation: scanner ${str(profile.scannerVersion)}, engine ${str(profile.assessmentEngineVersion)}, risk rubric ${str(profile.riskRubricVersion)}, key allowlist ${str(profile.keyAllowlistVersion)}, normalization ${str(profile.normalizationVersion)}`);
610
+ lines.push(`Package: ${str(pkg.fileCount)} files, digest ${str(pkg.digest)}`);
611
+ lines.push(`Attestation: ${body.attestation === null ? "none (this profile assessment is unsigned)" : "present (not verified by this CLI)"}`);
612
+ lines.push("");
613
+ for (const control of Array.isArray(a.controls) ? a.controls : []) {
614
+ if (!isObj(control)) continue;
615
+ const cov = isObj(control.coverage) ? control.coverage : {};
616
+ lines.push(`${str(control.controlId)} ${str(control.title)}: ${str(control.status)} (confidence ${str(control.confidence)})`);
617
+ lines.push(` ${str(cov.evidenceObserved)} evidence observed, ${str(cov.gapIdentified)} gaps identified, ${str(cov.notAssessed)} not assessed, of ${str(cov.total)} checks`);
618
+ for (const check of Array.isArray(control.checks) ? control.checks : []) {
619
+ if (isObj(check)) lines.push(` ${str(check.checkId)} ${str(check.status)}: ${str(check.title)}`);
620
+ }
621
+ lines.push("");
622
+ }
623
+ const gaps = (Array.isArray(a.evidence) ? a.evidence : []).filter((e) => isObj(e) && e.polarity === "gap");
624
+ lines.push(`Gaps identified (${gaps.length}):`);
625
+ for (const g of gaps) {
626
+ lines.push(` [${str(g.severity)}] ${str(g.kind)}: ${str(g.summary)}`);
627
+ const locations = (Array.isArray(g.locations) ? g.locations : []).filter(isObj).slice(0, 3).map((l) => `${str(l.file)}${l.line !== void 0 ? `:${str(l.line)}` : ""}`);
628
+ if (locations.length) lines.push(` at ${locations.join(", ")}`);
629
+ }
630
+ if (gaps.length === 0) lines.push(" none");
631
+ const notImplemented = Array.isArray(a.notImplementedControls) ? a.notImplementedControls.map(str).join(", ") : "";
632
+ lines.push("");
633
+ lines.push(`Not assessed by this profile version: ${notImplemented || "none"}`);
634
+ for (const note of Array.isArray(a.notes) ? a.notes : []) lines.push(`Note: ${str(note)}`);
635
+ if (skipped.length > 0) {
636
+ lines.push("");
637
+ lines.push(`Skipped from the package (${skipped.length}), not sent:`);
638
+ for (const s of skipped.slice(0, 20)) lines.push(` ${displayPath(s.path)}: ${s.reason}`);
639
+ if (skipped.length > 20) lines.push(` and ${skipped.length - 20} more`);
640
+ }
641
+ return lines;
642
+ }
643
+
644
+ // src/cli.ts
645
+ var VERSION = "2.0.0";
10
646
  var API_URL = process.env.AGENTVERIFY_API_URL ?? "https://agentverify-api.agentverify.workers.dev/v1/scan";
11
647
  var A2SPA_DOCS_URL = "https://aimodularity.com/A2SPA/docs";
12
648
  var EXIT_OK = 0;
@@ -26,20 +662,20 @@ var c = {
26
662
  };
27
663
  var write = (value = "") => process.stdout.write(`${value}
28
664
  `);
29
- var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".ts", ".py", ".json", ".yaml", ".yml", ".md", ".mjs", ".cjs"]);
665
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".py", ".json", ".yaml", ".yml", ".toml", ".md", ".mjs", ".cjs", ".sh"]);
30
666
  var MAX_SCAN_FILE_BYTES = 5 * 1024 * 1024;
31
- var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
667
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "vendor", "coverage", ".cache"]);
32
668
  function collectFiles(dir) {
33
669
  const files = [];
34
670
  try {
35
- const entries = readdirSync(dir);
671
+ const entries = readdirSync3(dir);
36
672
  for (const entry of entries) {
37
- if (SKIP_DIRS.has(entry)) continue;
38
- const fullPath = join(dir, entry);
39
- const stat = statSync(fullPath);
673
+ if (SKIP_DIRS2.has(entry)) continue;
674
+ const fullPath = join2(dir, entry);
675
+ const stat = statSync2(fullPath);
40
676
  if (stat.isDirectory()) {
41
677
  files.push(...collectFiles(fullPath));
42
- } else if (SUPPORTED_EXTENSIONS.has(extname(entry).toLowerCase())) {
678
+ } else if (SUPPORTED_EXTENSIONS.has(extname2(entry).toLowerCase())) {
43
679
  files.push(fullPath);
44
680
  }
45
681
  }
@@ -61,7 +697,8 @@ function parseArgs(argv) {
61
697
  help: false,
62
698
  version: false,
63
699
  policy: "",
64
- summaryFile: ""
700
+ summaryFile: "",
701
+ profiles: []
65
702
  };
66
703
  if (result.command === "--help" || result.command === "-h") {
67
704
  result.command = "";
@@ -81,6 +718,8 @@ function parseArgs(argv) {
81
718
  else if (arg === "--allow-not-assessed") result.allowNotAssessed = true;
82
719
  else if (arg === "--policy") result.policy = args[++i] ?? "";
83
720
  else if (arg === "--summary-file") result.summaryFile = args[++i] ?? "";
721
+ else if (arg === "--profile") result.profiles.push(args[++i] ?? "");
722
+ else if (arg.startsWith("--profile=")) result.profiles.push(arg.slice("--profile=".length));
84
723
  else if (arg === "--help" || arg === "-h") result.help = true;
85
724
  else if (arg === "--version" || arg === "-v") result.version = true;
86
725
  else if (!arg.startsWith("-")) result.dir = arg;
@@ -96,9 +735,12 @@ ${c.gray}Execution trust analysis for AI agents${c.reset}
96
735
  ${c.bold}Usage:${c.reset}
97
736
  agentverify scan [dir] --key <api-key>
98
737
  agentverify scan --file <file> --key <api-key>
738
+ agentverify mcp
99
739
 
100
740
  ${c.bold}Commands:${c.reset}
101
741
  scan Scan agent files in a directory or a single file
742
+ mcp Run as an MCP server so Claude Code, Cursor, VS Code and other AI
743
+ tools can scan agents while you code (reads AGENTVERIFY_API_KEY)
102
744
 
103
745
  ${c.bold}Options:${c.reset}
104
746
  --key, -k API key (or set AGENTVERIFY_API_KEY env var)
@@ -112,6 +754,11 @@ ${c.bold}Options:${c.reset}
112
754
  in addition to the scanner's own verdict (e.g. financial-agent)
113
755
  --summary-file <path>
114
756
  Write results as JSON to this path (in addition to any other output)
757
+ --profile <id>
758
+ (alpha) Assess a directory against an assessment profile instead of
759
+ scanning files individually. Supported: ${SUPPORTED_PROFILES.join(", ")}
760
+ Takes a directory; not combinable with --file, --ci, --policy,
761
+ --allow-not-assessed, --markdown or --summary-file.
115
762
  --version, -v Print version
116
763
  --help, -h Show this help message
117
764
 
@@ -131,12 +778,23 @@ ${c.bold}Examples:${c.reset}
131
778
  ${c.gray}# Markdown summary for pull requests${c.reset}
132
779
  agentverify scan . --key av_your_key --markdown
133
780
 
781
+ ${c.gray}# Let Claude Code scan your agents (one-time setup)${c.reset}
782
+ claude mcp add agentverify -e AGENTVERIFY_API_KEY=av_your_key -- npx -y agentverify mcp
783
+
784
+ ${c.gray}# Assess a skill directory against a profile (alpha); --json prints the service's response${c.reset}
785
+ agentverify scan ./my-skill --profile ${SUPPORTED_PROFILES[0]} --key av_your_key
786
+
134
787
  ${c.bold}Exit codes (--ci):${c.reset}
135
788
  0 All scanned files verified
136
789
  1 NOT VERIFIED \u2014 a security/verification check failed
137
790
  2 NOT ASSESSED \u2014 insufficient evidence for a verdict (see --allow-not-assessed)
138
791
  3 Execution error \u2014 the scan itself could not run (bad key, network, missing file)
139
792
 
793
+ ${c.bold}Exit codes (--profile):${c.reset}
794
+ 0 The assessment completed. Control and check statuses (including GAP_IDENTIFIED)
795
+ do not affect the exit code.
796
+ 3 Usage or execution error \u2014 nothing was assessed
797
+
140
798
  ${c.bold}Get your API key:${c.reset}
141
799
  https://aimodularity.com/agentverify/dashboard
142
800
 
@@ -190,6 +848,8 @@ ${c.bold}Fix first:${c.reset}`);
190
848
  else write(` No blocking fixes detected. Re-scan after any material change.`);
191
849
  if (result.reportInsights?.nextAction) write(`
192
850
  ${c.bold}Next action:${c.reset} ${result.reportInsights.nextAction}`);
851
+ const lockedFixes = (result.findings ?? []).filter((f) => f.fixLocked).length;
852
+ if (lockedFixes > 0) write(`${c.gray}Step-by-step fixes and corrected code for ${lockedFixes} finding${lockedFixes === 1 ? "" : "s"} are included with Pro: https://aimodularity.com/agentverify/pricing/${c.reset}`);
193
853
  const threats = getRelevantThreatCategories(result);
194
854
  if (threats.length) {
195
855
  write(`
@@ -201,8 +861,8 @@ ${c.bold}Threat categories:${c.reset}`);
201
861
  write(`${c.gray}Re-scan:${c.reset} agentverify scan --file ${fileName} --key $AGENTVERIFY_API_KEY
202
862
  `);
203
863
  }
204
- function markdownSummary(results, summary) {
205
- const lines = ["# Agent Verify", "", `Files scanned: ${summary.total}`, `Verified: ${summary.verified}`, `Not verified: ${summary.notVerified}`, `Not assessed: ${summary.notAssessed}`, `Errors: ${summary.errors}`, ""];
864
+ function markdownSummary(results, summary2) {
865
+ const lines = ["# Agent Verify", "", `Files scanned: ${summary2.total}`, `Verified: ${summary2.verified}`, `Not verified: ${summary2.notVerified}`, `Not assessed: ${summary2.notAssessed}`, `Errors: ${summary2.errors}`, ""];
206
866
  for (const item of results) {
207
867
  if (!item.result) {
208
868
  lines.push(`## ${item.file}`, "", `Error: ${item.error ?? "Unknown error"}`, "");
@@ -222,20 +882,20 @@ function markdownSummary(results, summary) {
222
882
  lines.push(`A2SPA docs: ${A2SPA_DOCS_URL}`);
223
883
  return lines.join("\n");
224
884
  }
225
- function writeGithubJobSummary(results, summary) {
885
+ function writeGithubJobSummary(results, summary2) {
226
886
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
227
887
  if (!summaryPath) return;
228
888
  try {
229
- appendFileSync(summaryPath, `${markdownSummary(results, summary)}
889
+ appendFileSync(summaryPath, `${markdownSummary(results, summary2)}
230
890
  `);
231
891
  } catch (e) {
232
892
  console.error(`Warning: could not write GitHub job summary: ${e instanceof Error ? e.message : e}`);
233
893
  }
234
894
  }
235
- function writeSummaryFile(path, results, summary) {
895
+ function writeSummaryFile(path, results, summary2) {
236
896
  if (!path) return;
237
897
  try {
238
- writeFileSync(path, JSON.stringify({ results, summary }, null, 2));
898
+ writeFileSync(path, JSON.stringify({ results, summary: summary2 }, null, 2));
239
899
  } catch (e) {
240
900
  console.error(`Warning: could not write summary file: ${e instanceof Error ? e.message : e}`);
241
901
  }
@@ -278,7 +938,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
278
938
  files = collectFiles(dir);
279
939
  if (files.length === 0) {
280
940
  write(`${c.yellow}No agent files found in ${dir}${c.reset}`);
281
- write(`${c.gray}Supported: .js .ts .py .json .yaml .yml .md${c.reset}
941
+ write(`${c.gray}Supported: .js .jsx .ts .tsx .py .json .yaml .yml .toml .md .sh${c.reset}
282
942
  `);
283
943
  process.exit(0);
284
944
  }
@@ -293,12 +953,12 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
293
953
  const batch = files.slice(i, i + CONCURRENCY);
294
954
  const batchResults = await Promise.allSettled(
295
955
  batch.map(async (filePath) => {
296
- const fileStat = statSync(filePath);
956
+ const fileStat = statSync2(filePath);
297
957
  if (fileStat.size > MAX_SCAN_FILE_BYTES) {
298
958
  throw new Error(`File is ${(fileStat.size / 1024 / 1024).toFixed(1)}MB, over the ${MAX_SCAN_FILE_BYTES / 1024 / 1024}MB scan limit \u2014 skipped`);
299
959
  }
300
- const content = readFileSync(filePath, "utf-8");
301
- const relPath = relative(args.dir === "." ? process.cwd() : args.dir, filePath);
960
+ const content = readFileSync4(filePath, "utf-8");
961
+ const relPath = relative2(args.dir === "." ? process.cwd() : args.dir, filePath);
302
962
  const result = await scan(
303
963
  { content, fileName: relPath, policyId: args.policy || void 0 },
304
964
  { apiKey: args.key, apiUrl: API_URL }
@@ -320,7 +980,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
320
980
  else notVerified++;
321
981
  } else {
322
982
  const filePath = batch[j];
323
- const relPath = relative(args.dir, filePath);
983
+ const relPath = relative2(args.dir, filePath);
324
984
  const errMsg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
325
985
  if (!args.json) {
326
986
  write(` ${c.yellow}\u26A0${c.reset} ${c.yellow}WARNING${c.reset} ${relPath.padEnd(30)} ${c.gray}${errMsg}${c.reset}`);
@@ -330,11 +990,11 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
330
990
  }
331
991
  }
332
992
  }
333
- const summary = { total: files.length, verified, notVerified, notAssessed, errors };
334
- writeGithubJobSummary(jsonResults, summary);
335
- writeSummaryFile(args.summaryFile, jsonResults, summary);
993
+ const summary2 = { total: files.length, verified, notVerified, notAssessed, errors };
994
+ writeGithubJobSummary(jsonResults, summary2);
995
+ writeSummaryFile(args.summaryFile, jsonResults, summary2);
336
996
  if (args.json) {
337
- write(JSON.stringify({ results: jsonResults, summary }, null, 2));
997
+ write(JSON.stringify({ results: jsonResults, summary: summary2 }, null, 2));
338
998
  if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
339
999
  if (args.ci) {
340
1000
  if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
@@ -343,7 +1003,7 @@ ${c.bold}Agent Verify${c.reset} ${c.gray}v${VERSION}${c.reset}`);
343
1003
  return;
344
1004
  }
345
1005
  if (args.markdown) {
346
- write(markdownSummary(jsonResults, summary));
1006
+ write(markdownSummary(jsonResults, summary2));
347
1007
  if (errors > 0) process.exit(EXIT_EXECUTION_ERROR);
348
1008
  if (args.ci) {
349
1009
  if (notVerified > 0) process.exit(EXIT_NOT_VERIFIED);
@@ -373,6 +1033,89 @@ ${c.gray}No dashboard report was saved. Check API key access and retry if you ne
373
1033
  if (notAssessed > 0 && !args.allowNotAssessed) process.exit(EXIT_NOT_ASSESSED);
374
1034
  }
375
1035
  }
1036
+ var PROFILE_TIMEOUT_MS = 6e4;
1037
+ function profileUsageError(message) {
1038
+ console.error(`
1039
+ ${c.red}Error:${c.reset} ${message}`);
1040
+ console.error(`Run ${c.cyan}agentverify --help${c.reset} for usage
1041
+ `);
1042
+ process.exitCode = EXIT_EXECUTION_ERROR;
1043
+ }
1044
+ function profileFailure(json, message, detail = {}) {
1045
+ if (json) write(JSON.stringify({ error: message, ...detail }, null, 2));
1046
+ else console.error(`
1047
+ ${c.red}Error:${c.reset} ${safeText(message)}
1048
+ `);
1049
+ process.exitCode = EXIT_EXECUTION_ERROR;
1050
+ }
1051
+ async function runProfile(args) {
1052
+ if (args.profiles.length > 1) return profileUsageError("--profile was given more than once.");
1053
+ const profile = args.profiles[0];
1054
+ if (profile === "") return profileUsageError(`--profile requires a value. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
1055
+ if (!SUPPORTED_PROFILES.includes(profile)) {
1056
+ return profileUsageError(`Unknown profile ${JSON.stringify(safeText(profile))}. Supported profiles: ${SUPPORTED_PROFILES.join(", ")}`);
1057
+ }
1058
+ const conflicts = [];
1059
+ if (args.file) conflicts.push("--file");
1060
+ if (args.ci) conflicts.push("--ci");
1061
+ if (args.allowNotAssessed) conflicts.push("--allow-not-assessed");
1062
+ if (args.policy) conflicts.push("--policy");
1063
+ if (args.markdown) conflicts.push("--markdown");
1064
+ if (args.summaryFile) conflicts.push("--summary-file");
1065
+ if (conflicts.length > 0) return profileUsageError(`--profile cannot be combined with ${conflicts.join(", ")}. A profile assessment takes a directory and reports its own result.`);
1066
+ if (!args.key) {
1067
+ console.error(`
1068
+ ${c.red}Error:${c.reset} API key required
1069
+ `);
1070
+ console.error(` Set with --key av_your_key or AGENTVERIFY_API_KEY env var.`);
1071
+ console.error(` Get your key at https://aimodularity.com/agentverify/dashboard/
1072
+ `);
1073
+ process.exitCode = EXIT_EXECUTION_ERROR;
1074
+ return;
1075
+ }
1076
+ let built;
1077
+ try {
1078
+ built = buildPackage(args.dir);
1079
+ } catch (e) {
1080
+ return profileFailure(args.json, e instanceof PackageError ? e.message : `Could not read the package: ${e instanceof Error ? e.message : String(e)}`);
1081
+ }
1082
+ if (args.json && built.skipped.length > 0) {
1083
+ console.error(`Note: ${built.skipped.length} entries were skipped from the package and not sent: ${built.skipped.slice(0, 20).map((s) => `${displayPath(s.path)} (${s.reason})`).join(", ")}${built.skipped.length > 20 ? `, and ${built.skipped.length - 20} more` : ""}`);
1084
+ }
1085
+ const result = await postProfile(API_URL, args.key, serializeRequest(profile, built.files), PROFILE_TIMEOUT_MS);
1086
+ if (result.kind === "failure") return profileFailure(args.json, result.message);
1087
+ const body = result.json;
1088
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1089
+ if (result.status !== 200) {
1090
+ if (isObject(body) && typeof body.error === "string") {
1091
+ if (args.json) {
1092
+ write(JSON.stringify(body, null, 2));
1093
+ } else {
1094
+ console.error(`
1095
+ ${c.red}Error:${c.reset} ${safeText(body.error)} (HTTP ${result.status})`);
1096
+ const rejection = body.rejection;
1097
+ if (isObject(rejection)) console.error(` rejection: ${safeText(String(rejection.code ?? ""))}: ${safeText(String(rejection.message ?? ""))}${typeof rejection.path === "string" ? ` (${JSON.stringify(safeText(rejection.path))})` : ""}`);
1098
+ if (Array.isArray(body.supportedProfiles)) console.error(` supported profiles: ${body.supportedProfiles.map((p) => safeText(String(p))).join(", ")}`);
1099
+ console.error("");
1100
+ }
1101
+ process.exitCode = EXIT_EXECUTION_ERROR;
1102
+ return;
1103
+ }
1104
+ return profileFailure(args.json, `Unexpected response from the service (HTTP ${result.status})`, { status: result.status });
1105
+ }
1106
+ if (!isObject(body) || typeof body.profile !== "string" || !isObject(body.assessment)) {
1107
+ return profileFailure(args.json, "The service returned a response that is not a profile assessment", { status: result.status });
1108
+ }
1109
+ if (args.json) {
1110
+ write(JSON.stringify(body, null, 2));
1111
+ return;
1112
+ }
1113
+ const schema = body.assessment.schemaVersion;
1114
+ if (schema !== SUPPORTED_ASSESSMENT_SCHEMA) {
1115
+ return profileFailure(false, `This CLI prints assessment schema ${SUPPORTED_ASSESSMENT_SCHEMA}, but the service returned ${JSON.stringify(safeText(String(schema)))}. Update the CLI, or use --json to receive the raw response.`);
1116
+ }
1117
+ for (const line of formatAssessment(body, built.skipped)) write(line);
1118
+ }
376
1119
  async function main() {
377
1120
  const args = parseArgs(process.argv);
378
1121
  if (args.version) {
@@ -383,8 +1126,13 @@ async function main() {
383
1126
  printHelp();
384
1127
  process.exit(EXIT_OK);
385
1128
  }
1129
+ if (args.command === "mcp") {
1130
+ await runMcpServer({ version: VERSION, apiKey: args.key, apiUrl: API_URL });
1131
+ return;
1132
+ }
386
1133
  if (args.command === "scan") {
387
- await runScan(args);
1134
+ if (args.profiles.length > 0) await runProfile(args);
1135
+ else await runScan(args);
388
1136
  } else {
389
1137
  console.error(`
390
1138
  ${c.red}Unknown command: ${args.command}${c.reset}`);