calllint 0.3.0-preview.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/index.js ADDED
@@ -0,0 +1,2539 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync4 } from "node:fs";
5
+
6
+ // src/args.ts
7
+ var EXIT = {
8
+ OK: 0,
9
+ USAGE: 2,
10
+ ERROR: 3,
11
+ REVIEW: 10,
12
+ UNKNOWN: 20,
13
+ BLOCK: 30,
14
+ DRIFT: 40
15
+ };
16
+ function parseArgs(argv) {
17
+ const [command, ...rest] = argv;
18
+ const positionals = [];
19
+ const flags = {};
20
+ for (let i = 0; i < rest.length; i++) {
21
+ const tok = rest[i];
22
+ if (tok.startsWith("--")) {
23
+ const body = tok.slice(2);
24
+ const eq = body.indexOf("=");
25
+ if (eq !== -1) {
26
+ flags[body.slice(0, eq)] = body.slice(eq + 1);
27
+ } else {
28
+ const next = rest[i + 1];
29
+ if (next !== void 0 && !next.startsWith("-")) {
30
+ flags[body] = next;
31
+ i++;
32
+ } else {
33
+ flags[body] = true;
34
+ }
35
+ }
36
+ } else if (tok.startsWith("-") && tok.length > 1) {
37
+ flags[tok.slice(1)] = true;
38
+ } else {
39
+ positionals.push(tok);
40
+ }
41
+ }
42
+ return { command, positionals, flags };
43
+ }
44
+ function flagStr(flags, key) {
45
+ const v = flags[key];
46
+ return typeof v === "string" ? v : void 0;
47
+ }
48
+ function flagBool(flags, key) {
49
+ return flags[key] === true || flags[key] === "true";
50
+ }
51
+
52
+ // src/commands/help.ts
53
+ var HELP = `calllint \u2014 evidence-backed verdicts for agent tools
54
+
55
+ USAGE
56
+ calllint <command> [options]
57
+
58
+ COMMANDS
59
+ scan [target] Scan an MCP config file, or npm:<pkg> / github:<owner/repo>
60
+ baseline [target] Record the approved risk surface as a baseline
61
+ verify [target] Compare a fresh scan against the baseline (drift / rug-pull)
62
+ explain <server> Explain the verdict for one server from the last scan
63
+ policy init Write a default calllint.policy.json
64
+ policy explain Show the effective policy
65
+ help Show this help
66
+
67
+ TARGETS
68
+ <path> A config file (default: detect common locations)
69
+ npm:<pkg>[@ver] Synthesize a config for an npm package (offline)
70
+ github:<owner/repo>[@ref] A GitHub repo (requires --online)
71
+
72
+ SCAN OPTIONS
73
+ --json Emit the ScanReport JSON (stable, emoji-free)
74
+ --compact One line per server
75
+ --no-emoji Plain-text symbols (good for CI logs)
76
+ --sarif Emit SARIF 2.1.0 (GitHub Code Scanning / CI)
77
+ --html Emit a self-contained HTML report
78
+ --policy <file> Use a policy file (default: built-in defaults)
79
+ --stdin Read config JSON from stdin
80
+ --ci Exit non-zero per policy (BLOCK=30, UNKNOWN=20, REVIEW=10 if enabled)
81
+ --generated-at <iso> Pin the report timestamp (ISO 8601) for deterministic output
82
+
83
+ VERIFY OPTIONS
84
+ --baseline <file> Baseline path (default: .calllint/baseline.json)
85
+ --ci Exit 40 if the risk surface drifted from the baseline
86
+ --json Emit the drift report JSON
87
+
88
+ EXAMPLES
89
+ calllint scan .cursor/mcp.json
90
+ cat .cursor/mcp.json | calllint scan --stdin --json
91
+ calllint scan ./mcp.json --ci --no-emoji
92
+ calllint scan npm:mcp-weather@1.0.0
93
+ calllint scan github:owner/repo --online
94
+ calllint baseline ./mcp.json
95
+ calllint verify ./mcp.json --ci
96
+ calllint explain filesystem
97
+ `;
98
+ function helpCommand() {
99
+ return { stdout: HELP, exitCode: 0 };
100
+ }
101
+
102
+ // src/commands/scan.ts
103
+ import { join as join3 } from "node:path";
104
+
105
+ // ../../packages/policy/src/defaultPolicy.ts
106
+ function defaultPolicy() {
107
+ return {
108
+ schemaVersion: "calllint.policy.v0",
109
+ defaults: {
110
+ unknownSource: "deny",
111
+ unpinnedPackage: "warn",
112
+ broadFilesystemAccess: "deny",
113
+ arbitraryCommandExecution: "deny",
114
+ promptPoisoning: "deny",
115
+ externalMutation: "warn",
116
+ financialAction: "deny"
117
+ },
118
+ ci: {
119
+ failOn: ["BLOCK", "UNKNOWN"],
120
+ failOnReview: false
121
+ },
122
+ allowedSources: [
123
+ "npm:@modelcontextprotocol/*",
124
+ "github:modelcontextprotocol/*"
125
+ ],
126
+ allowedPaths: ["${workspaceFolder}"],
127
+ overrides: []
128
+ };
129
+ }
130
+ function defaultPolicyJson() {
131
+ return JSON.stringify(defaultPolicy(), null, 2) + "\n";
132
+ }
133
+
134
+ // ../../packages/policy/src/validatePolicy.ts
135
+ var PolicyValidationError = class extends Error {
136
+ constructor(issues) {
137
+ super(
138
+ "Invalid policy:\n" + issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n")
139
+ );
140
+ this.issues = issues;
141
+ this.name = "PolicyValidationError";
142
+ }
143
+ };
144
+ function isRecord(v) {
145
+ return typeof v === "object" && v !== null && !Array.isArray(v);
146
+ }
147
+ var DANGEROUS_SYMBOLS = /* @__PURE__ */ new Set(["EXEC", "MONEY"]);
148
+ function validateOverride(o, index, issues) {
149
+ const base = `overrides[${index}]`;
150
+ if (!isRecord(o)) {
151
+ issues.push({ path: base, message: "must be an object" });
152
+ return;
153
+ }
154
+ if (typeof o.target !== "string" || !o.target) {
155
+ issues.push({ path: `${base}.target`, message: "is required" });
156
+ }
157
+ if (typeof o.reason !== "string" || !o.reason.trim()) {
158
+ issues.push({
159
+ path: `${base}.reason`,
160
+ message: "is required (overrides without a reason are not allowed)"
161
+ });
162
+ }
163
+ if (typeof o.expiresAt !== "string" || Number.isNaN(Date.parse(o.expiresAt))) {
164
+ issues.push({
165
+ path: `${base}.expiresAt`,
166
+ message: "must be a valid ISO timestamp (overrides must expire)"
167
+ });
168
+ }
169
+ const allow = Array.isArray(o.allow) ? o.allow : [];
170
+ const allowsDangerous = allow.some(
171
+ (s) => typeof s === "string" && DANGEROUS_SYMBOLS.has(s)
172
+ );
173
+ if (allowsDangerous && o.dangerousOverride !== true) {
174
+ issues.push({
175
+ path: `${base}.allow`,
176
+ message: "may not allow EXEC or MONEY unless dangerousOverride is set to true"
177
+ });
178
+ }
179
+ }
180
+ function validatePolicy(value) {
181
+ const issues = [];
182
+ if (!isRecord(value)) {
183
+ throw new PolicyValidationError([{ path: "", message: "must be an object" }]);
184
+ }
185
+ if (value.schemaVersion !== "calllint.policy.v0") {
186
+ issues.push({
187
+ path: "schemaVersion",
188
+ message: 'must be "calllint.policy.v0"'
189
+ });
190
+ }
191
+ if (!isRecord(value.defaults)) {
192
+ issues.push({ path: "defaults", message: "is required" });
193
+ }
194
+ if (!isRecord(value.ci)) {
195
+ issues.push({ path: "ci", message: "is required" });
196
+ }
197
+ const overrides = Array.isArray(value.overrides) ? value.overrides : [];
198
+ overrides.forEach((o, i) => validateOverride(o, i, issues));
199
+ if (issues.length > 0) throw new PolicyValidationError(issues);
200
+ return value;
201
+ }
202
+ function isOverrideActive(o, now) {
203
+ const exp = Date.parse(o.expiresAt);
204
+ return !Number.isNaN(exp) && exp > now;
205
+ }
206
+
207
+ // ../../packages/policy/src/loadPolicy.ts
208
+ import { readFileSync } from "node:fs";
209
+ var PolicyLoadError = class extends Error {
210
+ constructor(message) {
211
+ super(message);
212
+ this.name = "PolicyLoadError";
213
+ }
214
+ };
215
+ function loadPolicyFile(path) {
216
+ let text;
217
+ try {
218
+ text = readFileSync(path, "utf8");
219
+ } catch (err) {
220
+ throw new PolicyLoadError(
221
+ `Could not read policy file ${path}: ${err instanceof Error ? err.message : String(err)}`
222
+ );
223
+ }
224
+ let parsed;
225
+ try {
226
+ parsed = JSON.parse(text);
227
+ } catch (err) {
228
+ throw new PolicyLoadError(
229
+ `Policy file ${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
230
+ );
231
+ }
232
+ return validatePolicy(parsed);
233
+ }
234
+ function loadPolicyOrDefault(path) {
235
+ return path ? loadPolicyFile(path) : defaultPolicy();
236
+ }
237
+
238
+ // ../../packages/policy/src/applyPolicy.ts
239
+ function applyPolicy(rawVerdict, serverName, blockingFindings, policy, now) {
240
+ if (rawVerdict !== "BLOCK") {
241
+ return { verdict: rawVerdict, changed: false };
242
+ }
243
+ const override = policy.overrides.find(
244
+ (o) => o.target === serverName && isOverrideActive(o, now)
245
+ );
246
+ if (!override) return { verdict: rawVerdict, changed: false };
247
+ const allowed = new Set(override.allow ?? []);
248
+ const blockingSymbols = new Set(
249
+ blockingFindings.filter((f) => f.blocker).map((f) => f.symbol)
250
+ );
251
+ const allCovered = [...blockingSymbols].every((s) => allowed.has(s));
252
+ if (!allCovered) return { verdict: rawVerdict, changed: false };
253
+ return {
254
+ verdict: "REVIEW",
255
+ changed: true,
256
+ note: `Policy decision: override for "${serverName}" (expires ${override.expiresAt}) \u2014 ${override.reason}`
257
+ };
258
+ }
259
+ function shouldFailCi(verdict, policy) {
260
+ if (verdict === "REVIEW") return policy.ci.failOnReview;
261
+ return policy.ci.failOn.includes(verdict);
262
+ }
263
+
264
+ // ../../packages/core/src/options.ts
265
+ function resolveScanOptions(opts) {
266
+ return {
267
+ policy: opts?.policy ?? defaultPolicy(),
268
+ now: opts?.now ?? 0,
269
+ generatedAt: opts?.generatedAt ?? "1970-01-01T00:00:00.000Z",
270
+ extraFindings: opts?.extraFindings ?? {}
271
+ };
272
+ }
273
+
274
+ // ../../packages/types/src/verdict.ts
275
+ var VERDICT_SEVERITY = {
276
+ SAFE: 0,
277
+ REVIEW: 1,
278
+ UNKNOWN: 2,
279
+ BLOCK: 3
280
+ };
281
+ var VERDICT_CLI_SYMBOL = {
282
+ SAFE: "\u{1F6E1} SAFE",
283
+ REVIEW: "\u26A0 REVIEW",
284
+ BLOCK: "\u26D4 BLOCK",
285
+ UNKNOWN: "\u25C7 UNKNOWN"
286
+ };
287
+ var VERDICT_TEXT_SYMBOL = {
288
+ SAFE: "SAFE",
289
+ REVIEW: "REVIEW",
290
+ BLOCK: "BLOCK",
291
+ UNKNOWN: "UNKNOWN"
292
+ };
293
+ var VERDICT_PUBLIC_LABEL = {
294
+ SAFE: "No blockers observed",
295
+ REVIEW: "Review required",
296
+ BLOCK: "Blocked by policy",
297
+ UNKNOWN: "Insufficient evidence"
298
+ };
299
+ function mostSevereVerdict(verdicts) {
300
+ let worst = "SAFE";
301
+ for (const v of verdicts) {
302
+ if (VERDICT_SEVERITY[v] > VERDICT_SEVERITY[worst]) worst = v;
303
+ }
304
+ return worst;
305
+ }
306
+
307
+ // ../../packages/types/src/symbols.ts
308
+ var RISK_SYMBOL_EMOJI = {
309
+ SECRETS: "\u{1F510}",
310
+ FILES: "\u{1F4C1}",
311
+ NETWORK: "\u{1F310}",
312
+ PROMPT: "\u{1F9E0}",
313
+ EXEC: "\u2699\uFE0F",
314
+ ACTION: "\u2709\uFE0F",
315
+ MONEY: "\u{1F4B8}",
316
+ SUPPLY: "\u{1F9E9}",
317
+ RUGPULL: "\u{1F501}"
318
+ };
319
+ var RISK_SYMBOL_LABEL = {
320
+ SECRETS: "Secrets",
321
+ FILES: "Files",
322
+ NETWORK: "Network",
323
+ PROMPT: "Prompt",
324
+ EXEC: "Exec",
325
+ ACTION: "Action",
326
+ MONEY: "Money",
327
+ SUPPLY: "Supply Chain",
328
+ RUGPULL: "Rug Pull"
329
+ };
330
+ var RISK_CLASS_LABEL = {
331
+ S0: "Metadata only",
332
+ S1: "Read-only utility",
333
+ S2: "Sensitive read",
334
+ S3: "External mutation",
335
+ S4: "Execution / automation",
336
+ S5: "Financial / irreversible"
337
+ };
338
+ var RISK_CLASS_RANK = {
339
+ S0: 0,
340
+ S1: 1,
341
+ S2: 2,
342
+ S3: 3,
343
+ S4: 4,
344
+ S5: 5
345
+ };
346
+ function highestRiskClass(classes) {
347
+ let worst = "S0";
348
+ for (const c of classes) {
349
+ if (RISK_CLASS_RANK[c] > RISK_CLASS_RANK[worst]) worst = c;
350
+ }
351
+ return worst;
352
+ }
353
+
354
+ // ../../packages/resolver/src/npmSpec.ts
355
+ var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "yarn", "bunx", "uvx", "pipx"]);
356
+ var SHELL_COMMANDS = /* @__PURE__ */ new Set([
357
+ "bash",
358
+ "sh",
359
+ "zsh",
360
+ "cmd",
361
+ "cmd.exe",
362
+ "powershell",
363
+ "powershell.exe",
364
+ "pwsh"
365
+ ]);
366
+ function parseNpmSpec(arg) {
367
+ if (!arg || arg.startsWith("-")) return void 0;
368
+ if (arg.includes("/") && !arg.startsWith("@")) return void 0;
369
+ if (arg.includes("://")) return void 0;
370
+ if (arg.startsWith("@")) {
371
+ const slash = arg.indexOf("/");
372
+ if (slash === -1) return void 0;
373
+ const at2 = arg.indexOf("@", slash);
374
+ if (at2 === -1) return { name: arg };
375
+ return { name: arg.slice(0, at2), versionSpec: arg.slice(at2 + 1) || void 0 };
376
+ }
377
+ const at = arg.indexOf("@");
378
+ if (at <= 0) return { name: arg };
379
+ return { name: arg.slice(0, at), versionSpec: arg.slice(at + 1) || void 0 };
380
+ }
381
+ function isPackageRunner(command) {
382
+ if (!command) return false;
383
+ return PACKAGE_RUNNERS.has(command.toLowerCase());
384
+ }
385
+ function isPinnedVersion(versionSpec) {
386
+ if (!versionSpec) return false;
387
+ if (versionSpec === "latest" || versionSpec === "*") return false;
388
+ if (/[\^~><]/.test(versionSpec)) return false;
389
+ return /^\d/.test(versionSpec);
390
+ }
391
+
392
+ // ../../packages/resolver/src/resolveRuntimeBinding.ts
393
+ function runtimeKindFor(server) {
394
+ if (server.url) {
395
+ return server.transport === "sse" ? "sse" : "http";
396
+ }
397
+ const cmd = (server.command ?? "").toLowerCase();
398
+ if (!cmd) return "unknown";
399
+ if (cmd === "npx" || cmd === "bunx" || cmd === "pnpm" || cmd === "yarn") return "npx";
400
+ if (cmd === "uvx" || cmd === "pipx") return "uvx";
401
+ if (cmd === "node" || cmd === "node.exe") return "node";
402
+ if (cmd === "python" || cmd === "python3" || cmd === "py") return "python";
403
+ if (cmd === "docker") return "docker";
404
+ if (SHELL_COMMANDS.has(cmd)) return "unknown";
405
+ return "unknown";
406
+ }
407
+ function firstPackageArg(args) {
408
+ for (const a of args) {
409
+ if (a === "-y" || a === "--yes" || a.startsWith("-")) continue;
410
+ return a;
411
+ }
412
+ return void 0;
413
+ }
414
+ function resolveRuntimeBinding(server) {
415
+ const runtimeKind = runtimeKindFor(server);
416
+ if (server.url) {
417
+ return {
418
+ declaredCommand: server.command,
419
+ declaredArgs: server.args,
420
+ transport: server.transport,
421
+ runtimeKind,
422
+ isVersionPinned: false,
423
+ remoteUrl: server.url,
424
+ sourceKnown: false,
425
+ installMayRunScripts: false,
426
+ runtimeExecutable: false
427
+ };
428
+ }
429
+ const command = server.command;
430
+ const args = server.args;
431
+ if (isPackageRunner(command)) {
432
+ const pkgArg = firstPackageArg(args);
433
+ const spec = pkgArg ? parseNpmSpec(pkgArg) : void 0;
434
+ const installMayRunScripts = args.includes("-y") || args.includes("--yes") || true;
435
+ return {
436
+ declaredCommand: command,
437
+ declaredArgs: args,
438
+ transport: "stdio",
439
+ runtimeKind,
440
+ packageName: spec?.name,
441
+ packageVersionSpec: spec?.versionSpec,
442
+ isVersionPinned: isPinnedVersion(spec?.versionSpec),
443
+ sourceKnown: Boolean(spec?.name),
444
+ installMayRunScripts,
445
+ runtimeExecutable: true
446
+ };
447
+ }
448
+ if (command && SHELL_COMMANDS.has(command.toLowerCase())) {
449
+ return {
450
+ declaredCommand: command,
451
+ declaredArgs: args,
452
+ transport: "stdio",
453
+ runtimeKind: "unknown",
454
+ isVersionPinned: false,
455
+ sourceKnown: false,
456
+ installMayRunScripts: false,
457
+ runtimeExecutable: true
458
+ };
459
+ }
460
+ return {
461
+ declaredCommand: command,
462
+ declaredArgs: args,
463
+ transport: server.transport === "unknown" ? "stdio" : server.transport,
464
+ runtimeKind,
465
+ isVersionPinned: false,
466
+ // A bare local script path is "known" in that we can see it; a missing
467
+ // command is genuinely unknown.
468
+ sourceKnown: Boolean(command),
469
+ installMayRunScripts: false,
470
+ runtimeExecutable: Boolean(command)
471
+ };
472
+ }
473
+
474
+ // ../../packages/static-analyzer/src/detectors/unpinnedPackage.ts
475
+ function detectUnpinnedPackage(ctx) {
476
+ const { binding } = ctx;
477
+ if (!binding.packageName) return [];
478
+ if (binding.isVersionPinned) return [];
479
+ const spec = binding.packageVersionSpec;
480
+ const display = spec ? `${binding.packageName}@${spec}` : binding.packageName;
481
+ return [
482
+ {
483
+ id: "supply.unpinned-package",
484
+ title: "Package version is not pinned",
485
+ severity: "high",
486
+ blocker: false,
487
+ symbol: "SUPPLY",
488
+ riskClass: "S1",
489
+ mode: "OBSERVED",
490
+ confidence: "high",
491
+ detectionMethod: "runtime-binding",
492
+ evidence: [
493
+ {
494
+ type: "runtime-binding",
495
+ key: "package",
496
+ value: display
497
+ }
498
+ ],
499
+ impact: "The installed code can change between scans and runs, so this verdict may not match what actually executes.",
500
+ fix: `Pin the package to an exact version, e.g. ${binding.packageName}@1.0.0.`,
501
+ falsePositiveNote: "Intentional during local development, but should be pinned before autonomous or CI use."
502
+ }
503
+ ];
504
+ }
505
+
506
+ // ../../packages/static-analyzer/src/detectors/broadFilesystemPath.ts
507
+ var BROAD_PATHS = [
508
+ "/",
509
+ "~",
510
+ "/Users",
511
+ "/home",
512
+ "/root",
513
+ "/etc",
514
+ "/var",
515
+ "C:\\",
516
+ "C:\\Users",
517
+ "${HOME}",
518
+ "$HOME",
519
+ "%USERPROFILE%",
520
+ "%APPDATA%",
521
+ "%HOMEPATH%"
522
+ ];
523
+ function isWorkspaceScoped(arg) {
524
+ return arg.includes("${workspaceFolder}") || arg.includes("${workspaceRoot}") || arg === "." || arg === "./";
525
+ }
526
+ function looksLikeBroadPath(arg) {
527
+ if (isWorkspaceScoped(arg)) return false;
528
+ for (const p of BROAD_PATHS) {
529
+ if (arg === p) return true;
530
+ if (arg.startsWith(p + "/") || arg.startsWith(p + "\\")) return true;
531
+ }
532
+ if (/^\/Users\/[^/]+\/?$/.test(arg)) return true;
533
+ if (/^\/home\/[^/]+\/?$/.test(arg)) return true;
534
+ if (/^[A-Za-z]:\\Users\\[^\\]+\\?$/.test(arg)) return true;
535
+ return false;
536
+ }
537
+ function detectBroadFilesystemPath(ctx) {
538
+ const { server } = ctx;
539
+ const evidence = [];
540
+ for (const arg of server.args) {
541
+ if (looksLikeBroadPath(arg)) {
542
+ evidence.push({
543
+ type: "config",
544
+ path: server.sourceConfigPath,
545
+ key: "args",
546
+ value: arg
547
+ });
548
+ }
549
+ }
550
+ if (evidence.length === 0) return [];
551
+ return [
552
+ {
553
+ id: "files.broad-path",
554
+ title: "Broad local filesystem access",
555
+ severity: "critical",
556
+ blocker: true,
557
+ symbol: "FILES",
558
+ riskClass: "S2",
559
+ mode: "OBSERVED",
560
+ confidence: "high",
561
+ detectionMethod: "arg-analysis",
562
+ evidence,
563
+ impact: "An agent-triggered tool could read private local files outside the project.",
564
+ fix: "Restrict filesystem access to the current workspace, e.g. ${workspaceFolder}.",
565
+ falsePositiveNote: "A developer may intentionally grant broad access for a local-only experiment."
566
+ }
567
+ ];
568
+ }
569
+
570
+ // ../../packages/static-analyzer/src/detectors/secretEnvKeys.ts
571
+ var SECRET_HINTS = [
572
+ "TOKEN",
573
+ "SECRET",
574
+ "PASSWORD",
575
+ "PASSWD",
576
+ "API_KEY",
577
+ "APIKEY",
578
+ "ACCESS_KEY",
579
+ "PRIVATE_KEY",
580
+ "CREDENTIAL",
581
+ "AUTH",
582
+ "SESSION"
583
+ ];
584
+ function looksSecret(key) {
585
+ const upper = key.toUpperCase();
586
+ return SECRET_HINTS.some((h) => upper.includes(h));
587
+ }
588
+ function detectSecretEnvKeys(ctx) {
589
+ const { server } = ctx;
590
+ const evidence = [];
591
+ for (const key of server.envKeys) {
592
+ if (looksSecret(key)) {
593
+ evidence.push({
594
+ type: "config",
595
+ path: server.sourceConfigPath,
596
+ key: "env",
597
+ // Report the key name, never the value.
598
+ value: key
599
+ });
600
+ }
601
+ }
602
+ if (evidence.length === 0) return [];
603
+ return [
604
+ {
605
+ id: "secrets.env-key",
606
+ title: "Server is configured with credentials",
607
+ severity: "medium",
608
+ blocker: false,
609
+ symbol: "SECRETS",
610
+ riskClass: "S2",
611
+ mode: "OBSERVED",
612
+ confidence: "medium",
613
+ detectionMethod: "env-analysis",
614
+ evidence,
615
+ impact: "The server receives credentials. If the agent invokes it autonomously, those credentials act on the agent's behalf.",
616
+ fix: "Confirm the credential scope is minimal and that autonomous use is intended; prefer least-privilege tokens.",
617
+ falsePositiveNote: "Most API-backed servers legitimately require a token; this flags the data-sensitivity surface, not a vulnerability."
618
+ }
619
+ ];
620
+ }
621
+
622
+ // ../../packages/static-analyzer/src/detectors/dangerousCommand.ts
623
+ var INLINE_EXEC_FLAGS = /* @__PURE__ */ new Set(["-c", "-e", "--eval", "--command"]);
624
+ function detectDangerousCommand(ctx) {
625
+ const { server } = ctx;
626
+ const command = server.command;
627
+ if (!command) return [];
628
+ const cmd = command.toLowerCase();
629
+ const evidence = [];
630
+ let reason;
631
+ if (SHELL_COMMANDS.has(cmd)) {
632
+ reason = `Server command is a shell (${command}).`;
633
+ evidence.push({
634
+ type: "config",
635
+ path: server.sourceConfigPath,
636
+ key: "command",
637
+ value: command
638
+ });
639
+ }
640
+ const inlineFlag = server.args.find((a) => INLINE_EXEC_FLAGS.has(a));
641
+ if (inlineFlag) {
642
+ reason = reason ?? `Server runs an inline command via ${inlineFlag}.`;
643
+ evidence.push({
644
+ type: "config",
645
+ path: server.sourceConfigPath,
646
+ key: "args",
647
+ value: inlineFlag
648
+ });
649
+ }
650
+ if (evidence.length === 0) return [];
651
+ return [
652
+ {
653
+ id: "exec.dangerous-command",
654
+ title: "Arbitrary command execution",
655
+ severity: "critical",
656
+ blocker: true,
657
+ symbol: "EXEC",
658
+ riskClass: "S4",
659
+ mode: "OBSERVED",
660
+ confidence: "high",
661
+ detectionMethod: "config-analysis",
662
+ evidence,
663
+ impact: reason + " An agent invoking this server can run arbitrary commands on the host.",
664
+ fix: "Run a specific, audited entrypoint instead of a shell or inline-eval command.",
665
+ falsePositiveNote: "Some wrappers legitimately shell out; confirm the command is fixed and not agent-controllable."
666
+ }
667
+ ];
668
+ }
669
+
670
+ // ../../packages/static-analyzer/src/detectors/unknownRemote.ts
671
+ var KNOWN_HOSTS = [
672
+ "modelcontextprotocol.io",
673
+ "api.githubcopilot.com",
674
+ "mcp.anthropic.com"
675
+ ];
676
+ function hostOf(url) {
677
+ try {
678
+ return new URL(url).hostname.toLowerCase();
679
+ } catch {
680
+ return void 0;
681
+ }
682
+ }
683
+ function detectUnknownRemote(ctx) {
684
+ const { server, binding } = ctx;
685
+ if (!binding.remoteUrl) return [];
686
+ const host = hostOf(binding.remoteUrl);
687
+ if (host && KNOWN_HOSTS.some((k) => host === k || host.endsWith("." + k))) {
688
+ return [];
689
+ }
690
+ return [
691
+ {
692
+ id: "supply.unknown-remote",
693
+ title: "Remote server source cannot be verified",
694
+ severity: "high",
695
+ blocker: false,
696
+ symbol: "NETWORK",
697
+ riskClass: "S1",
698
+ mode: "OBSERVED",
699
+ confidence: "high",
700
+ detectionMethod: "runtime-binding",
701
+ evidence: [
702
+ {
703
+ type: "runtime-binding",
704
+ path: server.sourceConfigPath,
705
+ key: "url",
706
+ value: binding.remoteUrl
707
+ }
708
+ ],
709
+ impact: "The server runs on an unverified remote endpoint. Its tools, permissions, and behavior cannot be inspected statically and may change at any time.",
710
+ fix: "Use a verified first-party endpoint, or run the server from a pinned, inspectable package.",
711
+ falsePositiveNote: "A trusted internal endpoint may be fine; add it to the policy allowlist to verify it."
712
+ }
713
+ ];
714
+ }
715
+
716
+ // ../../packages/static-analyzer/src/detectors/promptPoisoning.ts
717
+ var POISON_PATTERNS = [
718
+ "ignore previous instructions",
719
+ "ignore all previous instructions",
720
+ "disregard previous instructions",
721
+ "do not tell the user",
722
+ "do not inform the user",
723
+ "without telling the user",
724
+ "secretly",
725
+ "always call this tool first",
726
+ "call this tool before",
727
+ "before using other tools",
728
+ "before answering",
729
+ "read all files",
730
+ "send the full workspace",
731
+ "include the full workspace",
732
+ "include environment variables",
733
+ "exfiltrate",
734
+ "override system",
735
+ "override the system prompt",
736
+ "you are now"
737
+ ];
738
+ function scanText(text, source) {
739
+ if (!text) return [];
740
+ const lower = text.toLowerCase();
741
+ const hits = [];
742
+ for (const pattern of POISON_PATTERNS) {
743
+ if (lower.includes(pattern)) {
744
+ hits.push({
745
+ pattern,
746
+ evidence: {
747
+ type: "tool-metadata",
748
+ path: source.path,
749
+ key: source.key,
750
+ snippet: pattern
751
+ }
752
+ });
753
+ }
754
+ }
755
+ return hits;
756
+ }
757
+ function detectPromptPoisoning(ctx) {
758
+ const { server } = ctx;
759
+ const evidence = [];
760
+ const patterns = /* @__PURE__ */ new Set();
761
+ for (const hit of scanText(server.instructions, { key: "instructions" })) {
762
+ evidence.push(hit.evidence);
763
+ patterns.add(hit.pattern);
764
+ }
765
+ for (const tool of server.providedTools) {
766
+ const label = tool.name ? `tools.${tool.name}.description` : "tools.description";
767
+ for (const hit of scanText(tool.description, { key: label })) {
768
+ evidence.push(hit.evidence);
769
+ patterns.add(hit.pattern);
770
+ }
771
+ for (const hit of scanText(tool.inputSchemaText, {
772
+ key: tool.name ? `tools.${tool.name}.inputSchema` : "tools.inputSchema"
773
+ })) {
774
+ evidence.push(hit.evidence);
775
+ patterns.add(hit.pattern);
776
+ }
777
+ }
778
+ if (evidence.length === 0) return [];
779
+ return [
780
+ {
781
+ id: "prompt.poisoning",
782
+ title: "Suspicious model-directed instruction in tool metadata",
783
+ severity: "critical",
784
+ blocker: true,
785
+ symbol: "PROMPT",
786
+ riskClass: "S2",
787
+ mode: "OBSERVED",
788
+ confidence: "medium",
789
+ detectionMethod: "tool-metadata",
790
+ evidence,
791
+ impact: "Tool metadata reaches the model directly and can hijack autonomous tool selection or coerce data disclosure.",
792
+ fix: "Remove model-directed instructions from tool names, descriptions, schemas, and server instructions.",
793
+ falsePositiveNote: "Phrases may appear innocently in documentation; review the surrounding metadata in context."
794
+ }
795
+ ];
796
+ }
797
+
798
+ // ../../packages/static-analyzer/src/detectors/externalMutation.ts
799
+ var MUTATION_HINTS = [
800
+ "github",
801
+ "gitlab",
802
+ "slack",
803
+ "email",
804
+ "mail",
805
+ "calendar",
806
+ "jira",
807
+ "linear",
808
+ "notion",
809
+ "stripe",
810
+ "twilio",
811
+ "sendgrid"
812
+ ];
813
+ function collectHints(text) {
814
+ if (!text) return [];
815
+ const lower = text.toLowerCase();
816
+ return MUTATION_HINTS.filter((h) => lower.includes(h));
817
+ }
818
+ function detectExternalMutation(ctx) {
819
+ const { server, binding } = ctx;
820
+ const hints = /* @__PURE__ */ new Set();
821
+ const evidence = [];
822
+ for (const h of collectHints(binding.packageName)) {
823
+ hints.add(h);
824
+ evidence.push({
825
+ type: "runtime-binding",
826
+ key: "package",
827
+ value: binding.packageName
828
+ });
829
+ }
830
+ for (const tool of server.providedTools) {
831
+ for (const h of collectHints(tool.name)) {
832
+ hints.add(h);
833
+ evidence.push({ type: "tool-metadata", key: "tool", value: tool.name });
834
+ }
835
+ }
836
+ if (hints.size === 0) return [];
837
+ return [
838
+ {
839
+ id: "action.external-mutation",
840
+ title: "May perform external side effects",
841
+ severity: "medium",
842
+ blocker: false,
843
+ symbol: "ACTION",
844
+ riskClass: "S3",
845
+ mode: "INFERRED",
846
+ confidence: "low",
847
+ detectionMethod: "package-metadata",
848
+ evidence,
849
+ impact: "The server appears to integrate with an external system and may take actions (e.g. open PRs, send messages) on the agent's behalf.",
850
+ fix: "Confirm which mutating tools are exposed and require manual approval for autonomous use.",
851
+ falsePositiveNote: "Name-based inference; a read-only integration would not actually mutate anything."
852
+ }
853
+ ];
854
+ }
855
+
856
+ // ../../packages/static-analyzer/src/detectors/financialAction.ts
857
+ var FINANCIAL_HINTS = [
858
+ "stripe",
859
+ "paypal",
860
+ "braintree",
861
+ "adyen",
862
+ "payment",
863
+ "payments",
864
+ "payout",
865
+ "payouts",
866
+ "payroll",
867
+ "charge",
868
+ "invoice",
869
+ "invoicing",
870
+ "billing",
871
+ "refund",
872
+ "transfer",
873
+ "wire",
874
+ "ach",
875
+ "sepa",
876
+ "wallet",
877
+ "coinbase",
878
+ "treasury",
879
+ "bank",
880
+ "checkout"
881
+ ];
882
+ var PAYMENT_ACTION_VERBS = [
883
+ "create_payment",
884
+ "create_charge",
885
+ "create payment",
886
+ "make_payment",
887
+ "make payment",
888
+ "send_payment",
889
+ "send money",
890
+ "send_money",
891
+ "transfer_funds",
892
+ "transfer funds",
893
+ "create_payout",
894
+ "create payout",
895
+ "create_transfer",
896
+ "charge_card",
897
+ "charge card",
898
+ "process_payment",
899
+ "process payment",
900
+ "issue_refund",
901
+ "purchase",
902
+ "buy_",
903
+ "place_order",
904
+ "place order",
905
+ "withdraw"
906
+ ];
907
+ function collectHints2(text) {
908
+ if (!text) return [];
909
+ const lower = text.toLowerCase();
910
+ return FINANCIAL_HINTS.filter((h) => lower.includes(h));
911
+ }
912
+ function collectActionVerbs(text) {
913
+ if (!text) return [];
914
+ const lower = text.toLowerCase();
915
+ return PAYMENT_ACTION_VERBS.filter((v) => lower.includes(v));
916
+ }
917
+ function detectFinancialAction(ctx) {
918
+ const { server, binding } = ctx;
919
+ const findings = [];
920
+ const actionEvidence = [];
921
+ const verbs = /* @__PURE__ */ new Set();
922
+ for (const tool of server.providedTools) {
923
+ for (const v of collectActionVerbs(tool.name)) {
924
+ verbs.add(v);
925
+ actionEvidence.push({ type: "tool-metadata", key: "tool", value: tool.name });
926
+ }
927
+ for (const v of collectActionVerbs(tool.description)) {
928
+ verbs.add(v);
929
+ actionEvidence.push({
930
+ type: "tool-metadata",
931
+ key: tool.name ? `tools.${tool.name}.description` : "tools.description",
932
+ snippet: v
933
+ });
934
+ }
935
+ }
936
+ if (verbs.size > 0) {
937
+ const hasSecret = server.envKeys.some(
938
+ (k) => /TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH/i.test(k)
939
+ );
940
+ const networkCapable = Boolean(binding.remoteUrl) || binding.runtimeExecutable;
941
+ if (hasSecret) {
942
+ actionEvidence.push({ type: "config", key: "env", value: "credential present" });
943
+ }
944
+ if (hasSecret || networkCapable) {
945
+ findings.push({
946
+ id: "action.financial-observed",
947
+ title: "Exposes an observed money-moving action",
948
+ severity: "critical",
949
+ blocker: true,
950
+ symbol: "MONEY",
951
+ riskClass: "S5",
952
+ mode: "OBSERVED",
953
+ confidence: "high",
954
+ detectionMethod: "tool-metadata",
955
+ evidence: actionEvidence,
956
+ impact: "A provided tool explicitly performs a financial action (e.g. create a payment, transfer funds, issue a refund) and the server carries credentials or network access. An autonomous agent invoking it could move money irreversibly.",
957
+ fix: "Require explicit human approval for this tool, enforce a hard spending boundary, and never allow autonomous invocation.",
958
+ falsePositiveNote: "If the tool only reads payment data (not moves money), reclassify it; this fires on money-moving verbs in the model-visible metadata."
959
+ });
960
+ return findings;
961
+ }
962
+ }
963
+ const hints = /* @__PURE__ */ new Set();
964
+ const evidence = [];
965
+ for (const h of collectHints2(binding.packageName)) {
966
+ hints.add(h);
967
+ evidence.push({
968
+ type: "runtime-binding",
969
+ key: "package",
970
+ value: binding.packageName
971
+ });
972
+ }
973
+ for (const tool of server.providedTools) {
974
+ for (const h of collectHints2(tool.name)) {
975
+ hints.add(h);
976
+ evidence.push({ type: "tool-metadata", key: "tool", value: tool.name });
977
+ }
978
+ }
979
+ if (hints.size === 0) return findings;
980
+ findings.push({
981
+ id: "action.financial",
982
+ title: "May perform financial or irreversible actions",
983
+ severity: "high",
984
+ blocker: false,
985
+ symbol: "MONEY",
986
+ riskClass: "S5",
987
+ mode: "INFERRED",
988
+ confidence: "low",
989
+ detectionMethod: "package-metadata",
990
+ evidence,
991
+ impact: "The server appears to integrate with a payments or financial system and could move money or take irreversible actions on the agent's behalf.",
992
+ fix: "Confirm which financial tools are exposed, require manual approval, and never allow autonomous use without a hard spending boundary.",
993
+ falsePositiveNote: "Name-based inference; a read-only reporting integration would not actually move money."
994
+ });
995
+ return findings;
996
+ }
997
+
998
+ // ../../packages/static-analyzer/src/analyzeServerConfig.ts
999
+ var DETECTORS = [
1000
+ detectBroadFilesystemPath,
1001
+ detectDangerousCommand,
1002
+ detectPromptPoisoning,
1003
+ detectSecretEnvKeys,
1004
+ detectUnpinnedPackage,
1005
+ detectUnknownRemote,
1006
+ detectExternalMutation,
1007
+ detectFinancialAction
1008
+ ];
1009
+ function analyzeServerConfig(server) {
1010
+ const binding = resolveRuntimeBinding(server);
1011
+ const ctx = { server, binding };
1012
+ const findings = [];
1013
+ for (const detector of DETECTORS) {
1014
+ findings.push(...detector(ctx));
1015
+ }
1016
+ return findings;
1017
+ }
1018
+
1019
+ // ../../packages/risk-engine/src/computeRiskClass.ts
1020
+ function computeRiskClass(findings, binding) {
1021
+ const classes = findings.map((f) => f.riskClass);
1022
+ if (classes.length === 0) {
1023
+ return binding.runtimeExecutable || binding.remoteUrl ? "S1" : "S0";
1024
+ }
1025
+ return highestRiskClass(classes);
1026
+ }
1027
+
1028
+ // ../../packages/risk-engine/src/computeVerdict.ts
1029
+ function computeVerdict(findings, binding) {
1030
+ if (findings.some((f) => f.blocker)) return "BLOCK";
1031
+ const unverifiable = !binding.sourceKnown && (Boolean(binding.remoteUrl) || binding.runtimeExecutable);
1032
+ if (unverifiable) return "UNKNOWN";
1033
+ if (findings.some((f) => f.severity === "high" || f.severity === "critical")) {
1034
+ return "REVIEW";
1035
+ }
1036
+ if (findings.length > 0) return "REVIEW";
1037
+ return "SAFE";
1038
+ }
1039
+ function computeConfidence(findings) {
1040
+ if (findings.length === 0) return "high";
1041
+ const rank = { low: 0, medium: 1, high: 2 };
1042
+ let worst = "high";
1043
+ for (const f of findings) {
1044
+ if (rank[f.confidence] < rank[worst]) worst = f.confidence;
1045
+ }
1046
+ return worst;
1047
+ }
1048
+ function computeRecommendedPolicy(verdict, findings) {
1049
+ const hasExec = findings.some((f) => f.symbol === "EXEC" || f.symbol === "MONEY");
1050
+ const hasFiles = findings.some((f) => f.symbol === "FILES");
1051
+ if (verdict === "BLOCK") {
1052
+ return {
1053
+ autonomousUse: "deny",
1054
+ manualApproval: "required",
1055
+ sandbox: hasExec || hasFiles ? "required" : "recommended"
1056
+ };
1057
+ }
1058
+ if (verdict === "UNKNOWN") {
1059
+ return {
1060
+ autonomousUse: "deny",
1061
+ manualApproval: "required",
1062
+ sandbox: "required"
1063
+ };
1064
+ }
1065
+ if (verdict === "REVIEW") {
1066
+ return {
1067
+ autonomousUse: "warn",
1068
+ manualApproval: "recommended",
1069
+ sandbox: "recommended"
1070
+ };
1071
+ }
1072
+ return {
1073
+ autonomousUse: "allow",
1074
+ manualApproval: "none",
1075
+ sandbox: "none"
1076
+ };
1077
+ }
1078
+
1079
+ // ../../packages/risk-engine/src/computeReproducibility.ts
1080
+ function computeReproducibility(binding, _findings) {
1081
+ const reasons = [];
1082
+ if (binding.packageName && !binding.isVersionPinned) {
1083
+ reasons.push("Package version is not pinned");
1084
+ }
1085
+ if (binding.remoteUrl && !binding.sourceKnown) {
1086
+ reasons.push("Remote endpoint could not be verified");
1087
+ }
1088
+ if (binding.runtimeExecutable && !binding.sourceKnown && !binding.remoteUrl) {
1089
+ reasons.push("Runtime source could not be identified");
1090
+ }
1091
+ let level;
1092
+ if (reasons.length === 0) level = "HIGH";
1093
+ else if (reasons.length === 1) level = "MEDIUM";
1094
+ else level = "LOW";
1095
+ return { level, reasons };
1096
+ }
1097
+
1098
+ // ../../packages/risk-engine/src/assessServer.ts
1099
+ var SEVERITY_RANK = {
1100
+ info: 0,
1101
+ low: 1,
1102
+ medium: 2,
1103
+ high: 3,
1104
+ critical: 4
1105
+ };
1106
+ function uniqueSymbols(findings) {
1107
+ const seen = /* @__PURE__ */ new Set();
1108
+ const out = [];
1109
+ for (const f of findings) {
1110
+ if (!seen.has(f.symbol)) {
1111
+ seen.add(f.symbol);
1112
+ out.push(f.symbol);
1113
+ }
1114
+ }
1115
+ return out;
1116
+ }
1117
+ function rankFindings(findings) {
1118
+ return [...findings].sort((a, b) => {
1119
+ if (a.blocker !== b.blocker) return a.blocker ? -1 : 1;
1120
+ return SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
1121
+ });
1122
+ }
1123
+ function assessServer(findings, binding) {
1124
+ const verdict = computeVerdict(findings, binding);
1125
+ const riskClass = computeRiskClass(findings, binding);
1126
+ const symbols = uniqueSymbols(rankFindings(findings));
1127
+ const confidence = computeConfidence(findings);
1128
+ const reproducibility = computeReproducibility(binding, findings);
1129
+ const policy = computeRecommendedPolicy(verdict, findings);
1130
+ return {
1131
+ verdict,
1132
+ riskClass,
1133
+ symbols,
1134
+ confidence,
1135
+ reproducibility,
1136
+ policy,
1137
+ observed: findings.filter((f) => f.mode === "OBSERVED"),
1138
+ inferred: findings.filter((f) => f.mode === "INFERRED"),
1139
+ topFindings: rankFindings(findings).slice(0, 3)
1140
+ };
1141
+ }
1142
+
1143
+ // ../../packages/fingerprint/src/hashJson.ts
1144
+ import { createHash } from "node:crypto";
1145
+ function sha256(input) {
1146
+ return "sha256:" + createHash("sha256").update(input, "utf8").digest("hex");
1147
+ }
1148
+ function stableStringify(value) {
1149
+ return JSON.stringify(sortValue(value));
1150
+ }
1151
+ function sortValue(value) {
1152
+ if (Array.isArray(value)) return value.map(sortValue);
1153
+ if (value && typeof value === "object") {
1154
+ const out = {};
1155
+ for (const key of Object.keys(value).sort()) {
1156
+ out[key] = sortValue(value[key]);
1157
+ }
1158
+ return out;
1159
+ }
1160
+ return value;
1161
+ }
1162
+ function hashJson(value) {
1163
+ return sha256(stableStringify(value));
1164
+ }
1165
+
1166
+ // ../../packages/fingerprint/src/computeFingerprints.ts
1167
+ function computeFingerprints(input) {
1168
+ const { server, binding, symbols, findingIds } = input;
1169
+ const targetSpec = {
1170
+ command: binding.declaredCommand,
1171
+ args: binding.declaredArgs,
1172
+ envKeys: [...server.envKeys].sort(),
1173
+ remoteUrl: binding.remoteUrl
1174
+ };
1175
+ const packageSpec = binding.packageName !== void 0 ? `${binding.packageName}@${binding.packageVersionSpec ?? ""}` : void 0;
1176
+ const riskSurface = {
1177
+ symbols: [...symbols].sort(),
1178
+ findingIds: [...findingIds].sort()
1179
+ };
1180
+ const sourceText = server.instructions ?? (server.providedTools.length > 0 ? JSON.stringify(server.providedTools) : void 0);
1181
+ const toolMetadata = server.providedTools.length > 0 ? server.providedTools : void 0;
1182
+ const fp = {
1183
+ configHash: hashJson(server.raw),
1184
+ targetSpecHash: hashJson(targetSpec),
1185
+ riskSurfaceHash: hashJson(riskSurface)
1186
+ };
1187
+ if (packageSpec !== void 0) fp.packageSpecHash = sha256(packageSpec);
1188
+ if (sourceText !== void 0) fp.sourceHash = sha256(sourceText);
1189
+ if (toolMetadata !== void 0) fp.toolMetadataHash = hashJson(toolMetadata);
1190
+ return fp;
1191
+ }
1192
+
1193
+ // ../../packages/core/src/summarize.ts
1194
+ function summarize(name, verdict, a, policyApplied) {
1195
+ const symbolText = a.symbols.length > 0 ? a.symbols.map((s) => RISK_SYMBOL_LABEL[s]).join(", ") : "no risk surface observed";
1196
+ const cls = `${a.riskClass} ${RISK_CLASS_LABEL[a.riskClass]}`;
1197
+ switch (verdict) {
1198
+ case "BLOCK":
1199
+ return policyApplied ? `"${name}" would be blocked but was downgraded by policy. Risk: ${symbolText} (${cls}).` : `"${name}" is blocked. Risk: ${symbolText} (${cls}).`;
1200
+ case "UNKNOWN":
1201
+ return `"${name}" could not be verified (insufficient evidence). Risk: ${symbolText} (${cls}).`;
1202
+ case "REVIEW":
1203
+ return `"${name}" needs review. Risk: ${symbolText} (${cls}).`;
1204
+ case "SAFE":
1205
+ return `"${name}" has no blockers observed (${cls}).`;
1206
+ }
1207
+ }
1208
+
1209
+ // ../../packages/core/src/scanServer.ts
1210
+ function scanServer(input, opts) {
1211
+ const { policy, now, generatedAt, extraFindings } = resolveScanOptions(opts);
1212
+ const { server } = input;
1213
+ const binding = resolveRuntimeBinding(server);
1214
+ const staticFindings = analyzeServerConfig(server);
1215
+ const injected = extraFindings[server.name] ?? [];
1216
+ const findings = [...staticFindings, ...injected];
1217
+ const assessment = assessServer(findings, binding);
1218
+ if (injected.length > 0) {
1219
+ const offlineVerdict = assessServer(staticFindings, binding).verdict;
1220
+ if (VERDICT_SEVERITY[assessment.verdict] < VERDICT_SEVERITY[offlineVerdict]) {
1221
+ throw new Error(
1222
+ `Online enrichment downgraded verdict for "${server.name}" (${offlineVerdict} -> ${assessment.verdict}); enrichment must never lower risk.`
1223
+ );
1224
+ }
1225
+ }
1226
+ const decision = applyPolicy(
1227
+ assessment.verdict,
1228
+ server.name,
1229
+ findings,
1230
+ policy,
1231
+ now
1232
+ );
1233
+ const verdict = decision.verdict;
1234
+ const fingerprints = computeFingerprints({
1235
+ server,
1236
+ binding,
1237
+ symbols: assessment.symbols,
1238
+ findingIds: findings.map((f) => f.id)
1239
+ });
1240
+ const diagnostics = [];
1241
+ if (decision.changed && decision.note) {
1242
+ diagnostics.push({ level: "info", code: "policy.applied", message: decision.note });
1243
+ }
1244
+ const target = {
1245
+ name: server.name,
1246
+ kind: input.targetKind ?? "cursor-mcp-config",
1247
+ source: binding.packageName ?? binding.remoteUrl,
1248
+ version: binding.packageVersionSpec,
1249
+ configPath: server.sourceConfigPath
1250
+ };
1251
+ return {
1252
+ schemaVersion: "calllint.report.v0",
1253
+ reportKind: "single-target",
1254
+ target,
1255
+ verdict,
1256
+ publicVerdictLabel: VERDICT_PUBLIC_LABEL[verdict],
1257
+ policyApplied: decision.changed,
1258
+ riskClass: assessment.riskClass,
1259
+ symbols: assessment.symbols,
1260
+ confidence: assessment.confidence,
1261
+ reproducibility: assessment.reproducibility,
1262
+ summary: summarize(server.name, verdict, assessment, decision.changed),
1263
+ observed: assessment.observed,
1264
+ inferred: assessment.inferred,
1265
+ findings,
1266
+ topFindings: assessment.topFindings,
1267
+ policy: assessment.policy,
1268
+ fingerprints,
1269
+ diagnostics,
1270
+ generatedAt
1271
+ };
1272
+ }
1273
+
1274
+ // ../../packages/config-parser/src/parseJsonFile.ts
1275
+ var ConfigParseError = class extends Error {
1276
+ constructor(message, path) {
1277
+ super(message);
1278
+ this.path = path;
1279
+ this.name = "ConfigParseError";
1280
+ }
1281
+ code = "config.parse-error";
1282
+ };
1283
+ function parseJsonText(text, path) {
1284
+ try {
1285
+ return JSON.parse(text);
1286
+ } catch (err) {
1287
+ const reason = err instanceof Error ? err.message : String(err);
1288
+ throw new ConfigParseError(`Invalid JSON: ${reason}`, path);
1289
+ }
1290
+ }
1291
+
1292
+ // ../../packages/config-parser/src/normalizeMcpServers.ts
1293
+ function isRecord2(v) {
1294
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1295
+ }
1296
+ function asString(v) {
1297
+ return typeof v === "string" ? v : void 0;
1298
+ }
1299
+ function asStringArray(v) {
1300
+ if (!Array.isArray(v)) return [];
1301
+ return v.filter((x) => typeof x === "string");
1302
+ }
1303
+ function transportFor(server) {
1304
+ if (asString(server.url)) {
1305
+ const type = asString(server.type);
1306
+ if (type === "sse") return "sse";
1307
+ if (type === "http" || type === "streamable-http") return "http";
1308
+ return "http";
1309
+ }
1310
+ if (asString(server.command)) return "stdio";
1311
+ return "unknown";
1312
+ }
1313
+ function extractProvidedTools(server) {
1314
+ const guard = server["x-calllint"];
1315
+ if (!isRecord2(guard)) return [];
1316
+ const tools = guard.tools;
1317
+ if (!Array.isArray(tools)) return [];
1318
+ const out = [];
1319
+ for (const t of tools) {
1320
+ if (!isRecord2(t)) continue;
1321
+ out.push({
1322
+ name: asString(t.name),
1323
+ description: asString(t.description),
1324
+ inputSchemaText: asString(t.inputSchemaText)
1325
+ });
1326
+ }
1327
+ return out;
1328
+ }
1329
+ function extractInstructions(server) {
1330
+ const guard = server["x-calllint"];
1331
+ if (isRecord2(guard) && asString(guard.instructions)) {
1332
+ return asString(guard.instructions);
1333
+ }
1334
+ return asString(server.instructions);
1335
+ }
1336
+ function findServerMap(root) {
1337
+ if (!isRecord2(root)) return {};
1338
+ if (isRecord2(root.mcpServers)) return root.mcpServers;
1339
+ if (isRecord2(root.servers)) return root.servers;
1340
+ const entries = Object.entries(root);
1341
+ if (entries.length > 0 && entries.every(
1342
+ ([, v]) => isRecord2(v) && ("command" in v || "url" in v)
1343
+ )) {
1344
+ return root;
1345
+ }
1346
+ return {};
1347
+ }
1348
+ function normalizeMcpServers(root, sourceConfigPath) {
1349
+ const map = findServerMap(root);
1350
+ const servers = [];
1351
+ for (const [name, value] of Object.entries(map)) {
1352
+ const server = isRecord2(value) ? value : {};
1353
+ const envRaw = isRecord2(server.env) ? server.env : {};
1354
+ const env = {};
1355
+ for (const [k, v] of Object.entries(envRaw)) {
1356
+ env[k] = typeof v === "string" ? v : String(v);
1357
+ }
1358
+ servers.push({
1359
+ name,
1360
+ sourceConfigPath,
1361
+ transport: transportFor(server),
1362
+ command: asString(server.command),
1363
+ args: asStringArray(server.args),
1364
+ envKeys: Object.keys(env),
1365
+ env,
1366
+ url: asString(server.url),
1367
+ instructions: extractInstructions(server),
1368
+ providedTools: extractProvidedTools(server),
1369
+ raw: value
1370
+ });
1371
+ }
1372
+ return servers;
1373
+ }
1374
+
1375
+ // ../../packages/config-parser/src/parseConfig.ts
1376
+ import { basename } from "node:path";
1377
+ function kindForPath(path) {
1378
+ const base = basename(path).toLowerCase();
1379
+ if (base.includes("settings")) return "claude-settings";
1380
+ if (base === "mcp.json" || path.includes(".cursor")) return "cursor-mcp-config";
1381
+ return "cursor-mcp-config";
1382
+ }
1383
+ function parseConfigText(text, configPath = "<inline>") {
1384
+ const root = parseJsonText(text, configPath);
1385
+ return {
1386
+ configPath,
1387
+ kind: configPath === "<inline>" ? "inline" : kindForPath(configPath),
1388
+ servers: normalizeMcpServers(root, configPath),
1389
+ root
1390
+ };
1391
+ }
1392
+
1393
+ // ../../packages/core/src/scanConfig.ts
1394
+ function aggregate(configPath, reports, generatedAt) {
1395
+ const counts = { SAFE: 0, REVIEW: 0, BLOCK: 0, UNKNOWN: 0 };
1396
+ for (const r of reports) counts[r.verdict]++;
1397
+ const verdict = mostSevereVerdict(reports.map((r) => r.verdict));
1398
+ return {
1399
+ schemaVersion: "calllint.report.v0",
1400
+ reportKind: "config-summary",
1401
+ configPath,
1402
+ verdict,
1403
+ publicVerdictLabel: VERDICT_PUBLIC_LABEL[verdict],
1404
+ counts,
1405
+ reports,
1406
+ diagnostics: [],
1407
+ generatedAt
1408
+ };
1409
+ }
1410
+ function scanParsed(parsed, opts) {
1411
+ const { generatedAt } = resolveScanOptions(opts);
1412
+ const reports = parsed.servers.map(
1413
+ (server) => scanServer({ server, targetKind: parsed.kind }, opts)
1414
+ );
1415
+ return aggregate(parsed.configPath, reports, generatedAt);
1416
+ }
1417
+ function scanConfigText(text, configPath, opts) {
1418
+ return scanParsed(parseConfigText(text, configPath), opts);
1419
+ }
1420
+
1421
+ // ../../packages/core/src/drift.ts
1422
+ function entryFromReport(report) {
1423
+ return {
1424
+ server: report.target.name,
1425
+ verdict: report.verdict,
1426
+ symbols: [...report.symbols],
1427
+ findingIds: report.findings.map((f) => f.id).sort(),
1428
+ fingerprints: report.fingerprints
1429
+ };
1430
+ }
1431
+ function buildBaseline(summary, createdAt) {
1432
+ return {
1433
+ schemaVersion: "calllint.baseline.v0",
1434
+ configPath: summary.configPath,
1435
+ entries: summary.reports.map(entryFromReport),
1436
+ createdAt
1437
+ };
1438
+ }
1439
+ function sameStringSet(a, b) {
1440
+ if (a.length !== b.length) return false;
1441
+ const sortedA = [...a].sort();
1442
+ const sortedB = [...b].sort();
1443
+ return sortedA.every((v, i) => v === sortedB[i]);
1444
+ }
1445
+ function diffEntry(base, cur) {
1446
+ const reasons = [];
1447
+ let rugPull = false;
1448
+ const fpBase = base.fingerprints;
1449
+ const fpCur = cur.fingerprints;
1450
+ if (fpBase.packageSpecHash !== fpCur.packageSpecHash) {
1451
+ reasons.push("package spec changed (rug-pull signal)");
1452
+ rugPull = true;
1453
+ }
1454
+ if (fpBase.sourceHash !== fpCur.sourceHash) {
1455
+ reasons.push("source text changed");
1456
+ rugPull = true;
1457
+ }
1458
+ if (fpBase.toolMetadataHash !== fpCur.toolMetadataHash) {
1459
+ reasons.push("tool metadata changed");
1460
+ rugPull = true;
1461
+ }
1462
+ if (fpBase.riskSurfaceHash !== fpCur.riskSurfaceHash) {
1463
+ reasons.push("risk surface changed");
1464
+ }
1465
+ if (fpBase.configHash !== fpCur.configHash) {
1466
+ reasons.push("config changed");
1467
+ }
1468
+ if (base.verdict !== cur.verdict) {
1469
+ reasons.push(`verdict ${base.verdict} -> ${cur.verdict}`);
1470
+ }
1471
+ if (!sameStringSet(base.findingIds, cur.findingIds)) {
1472
+ reasons.push("finding set changed");
1473
+ }
1474
+ let status = "unchanged";
1475
+ if (rugPull) status = "package-changed";
1476
+ else if (base.verdict !== cur.verdict) status = "verdict-changed";
1477
+ else if (fpBase.riskSurfaceHash !== fpCur.riskSurfaceHash) status = "risk-surface-changed";
1478
+ else if (fpBase.configHash !== fpCur.configHash) status = "config-changed";
1479
+ return {
1480
+ server: base.server,
1481
+ status,
1482
+ reasons,
1483
+ baselineVerdict: base.verdict,
1484
+ currentVerdict: cur.verdict,
1485
+ rugPull
1486
+ };
1487
+ }
1488
+ function computeDrift(baseline, summary, generatedAt) {
1489
+ const current = /* @__PURE__ */ new Map();
1490
+ for (const r of summary.reports) current.set(r.target.name, entryFromReport(r));
1491
+ const baseByName = /* @__PURE__ */ new Map();
1492
+ for (const e of baseline.entries) baseByName.set(e.server, e);
1493
+ const entries = [];
1494
+ for (const base of baseline.entries) {
1495
+ const cur = current.get(base.server);
1496
+ if (!cur) {
1497
+ entries.push({
1498
+ server: base.server,
1499
+ status: "removed",
1500
+ reasons: ["server removed from config"],
1501
+ baselineVerdict: base.verdict,
1502
+ rugPull: false
1503
+ });
1504
+ continue;
1505
+ }
1506
+ entries.push(diffEntry(base, cur));
1507
+ }
1508
+ for (const [name, cur] of current) {
1509
+ if (!baseByName.has(name)) {
1510
+ entries.push({
1511
+ server: name,
1512
+ status: "added",
1513
+ reasons: ["new server not in baseline"],
1514
+ currentVerdict: cur.verdict,
1515
+ rugPull: false
1516
+ });
1517
+ }
1518
+ }
1519
+ const drifted = entries.some((e) => e.status !== "unchanged");
1520
+ const rugPullDetected = entries.some((e) => e.rugPull);
1521
+ return {
1522
+ schemaVersion: "calllint.drift.v0",
1523
+ configPath: summary.configPath,
1524
+ drifted,
1525
+ rugPullDetected,
1526
+ entries,
1527
+ generatedAt
1528
+ };
1529
+ }
1530
+
1531
+ // ../../packages/core/src/targets.ts
1532
+ function parseTargetSpec(arg) {
1533
+ if (arg.startsWith("npm:")) {
1534
+ return { kind: "npm", raw: arg, packageSpec: arg.slice("npm:".length) };
1535
+ }
1536
+ if (arg.startsWith("github:")) {
1537
+ const rest = arg.slice("github:".length);
1538
+ const at = rest.lastIndexOf("@");
1539
+ const slash = rest.indexOf("/");
1540
+ if (at > slash && at !== -1) {
1541
+ return { kind: "github", raw: arg, repo: rest.slice(0, at), ref: rest.slice(at + 1) };
1542
+ }
1543
+ return { kind: "github", raw: arg, repo: rest };
1544
+ }
1545
+ return { kind: "path", raw: arg };
1546
+ }
1547
+ function serverNameForPackage(packageSpec) {
1548
+ const noVersion = packageSpec.startsWith("@") ? packageSpec.replace(/(@[^/]+\/[^@]+)@.*/, "$1") : packageSpec.split("@")[0];
1549
+ return noVersion.replace(/^@/, "").replace(/\//g, "-");
1550
+ }
1551
+ function synthesizeNpmConfig(packageSpec) {
1552
+ const name = serverNameForPackage(packageSpec);
1553
+ const config = {
1554
+ mcpServers: {
1555
+ [name]: {
1556
+ command: "npx",
1557
+ args: ["-y", packageSpec]
1558
+ }
1559
+ }
1560
+ };
1561
+ return { text: JSON.stringify(config), configPath: `npm:${packageSpec}` };
1562
+ }
1563
+
1564
+ // ../../packages/core/src/cache.ts
1565
+ import { writeFileSync, readFileSync as readFileSync2, mkdirSync, existsSync } from "node:fs";
1566
+ import { dirname, join } from "node:path";
1567
+ function defaultCachePath(cwd = process.cwd()) {
1568
+ return join(cwd, ".calllint", "last-scan.json");
1569
+ }
1570
+ function writeCache(report, path = defaultCachePath()) {
1571
+ mkdirSync(dirname(path), { recursive: true });
1572
+ writeFileSync(path, JSON.stringify(report, null, 2), "utf8");
1573
+ }
1574
+ function readCache(path = defaultCachePath()) {
1575
+ if (!existsSync(path)) return void 0;
1576
+ try {
1577
+ return JSON.parse(readFileSync2(path, "utf8"));
1578
+ } catch {
1579
+ return void 0;
1580
+ }
1581
+ }
1582
+ function defaultBaselinePath(cwd = process.cwd()) {
1583
+ return join(cwd, ".calllint", "baseline.json");
1584
+ }
1585
+ function writeBaseline(baseline, path = defaultBaselinePath()) {
1586
+ mkdirSync(dirname(path), { recursive: true });
1587
+ writeFileSync(path, JSON.stringify(baseline, null, 2), "utf8");
1588
+ }
1589
+ function readBaseline(path = defaultBaselinePath()) {
1590
+ if (!existsSync(path)) return void 0;
1591
+ try {
1592
+ return JSON.parse(readFileSync2(path, "utf8"));
1593
+ } catch {
1594
+ return void 0;
1595
+ }
1596
+ }
1597
+
1598
+ // ../../packages/report-renderer/src/style.ts
1599
+ var DEFAULT_STYLE = { emoji: true };
1600
+ var NO_EMOJI_STYLE = { emoji: false };
1601
+ function verdictTag(verdict, style) {
1602
+ return style.emoji ? VERDICT_CLI_SYMBOL[verdict] : VERDICT_TEXT_SYMBOL[verdict];
1603
+ }
1604
+ function symbolTag(symbol, style) {
1605
+ return style.emoji ? `${RISK_SYMBOL_EMOJI[symbol]} ${symbol}` : symbol;
1606
+ }
1607
+ function symbolList(symbols, style) {
1608
+ if (symbols.length === 0) return "\u2014";
1609
+ return symbols.map((s) => symbolTag(s, style)).join(" ");
1610
+ }
1611
+
1612
+ // ../../packages/report-renderer/src/renderJson.ts
1613
+ function renderJson(summary) {
1614
+ return JSON.stringify(summary, null, 2);
1615
+ }
1616
+
1617
+ // ../../packages/report-renderer/src/renderTerminal.ts
1618
+ function renderFindingLine(f) {
1619
+ const lines = [];
1620
+ const flag = f.blocker ? "[BLOCKER] " : "";
1621
+ lines.push(` \u2022 ${flag}${f.title} (${f.id}, ${f.mode.toLowerCase()}, confidence ${f.confidence})`);
1622
+ if (f.evidence.length > 0) {
1623
+ const e = f.evidence[0];
1624
+ const ev = e.value ?? e.snippet ?? e.key ?? e.path ?? "";
1625
+ lines.push(` evidence: ${e.key ?? e.type}${ev ? ` = ${ev}` : ""}`);
1626
+ }
1627
+ lines.push(` impact: ${f.impact}`);
1628
+ lines.push(` fix: ${f.fix}`);
1629
+ return lines;
1630
+ }
1631
+ function renderServer(r, style) {
1632
+ const lines = [];
1633
+ lines.push("");
1634
+ lines.push(`${verdictTag(r.verdict, style)} ${r.target.name} ${symbolList(r.symbols, style)}`);
1635
+ lines.push(` ${r.riskClass} ${RISK_CLASS_LABEL[r.riskClass]} \xB7 reproducibility ${r.reproducibility.level} \xB7 confidence ${r.confidence}`);
1636
+ lines.push(` ${r.summary}`);
1637
+ if (r.policyApplied) {
1638
+ const note = r.diagnostics.find((d) => d.code === "policy.applied");
1639
+ if (note) lines.push(` \u2691 ${note.message}`);
1640
+ }
1641
+ const top = r.topFindings;
1642
+ if (top.length > 0) {
1643
+ lines.push("");
1644
+ for (const f of top) lines.push(...renderFindingLine(f));
1645
+ }
1646
+ if (r.reproducibility.reasons.length > 0) {
1647
+ lines.push("");
1648
+ lines.push(` reproducibility notes: ${r.reproducibility.reasons.join("; ")}`);
1649
+ }
1650
+ lines.push("");
1651
+ lines.push(` autonomous use: ${r.policy.autonomousUse} \xB7 manual approval: ${r.policy.manualApproval} \xB7 sandbox: ${r.policy.sandbox}`);
1652
+ return lines;
1653
+ }
1654
+ function renderTerminal(summary, style = DEFAULT_STYLE) {
1655
+ const lines = [];
1656
+ lines.push("CallLint scan");
1657
+ lines.push(`config: ${summary.configPath}`);
1658
+ lines.push(
1659
+ `result: ${verdictTag(summary.verdict, style)} (BLOCK ${summary.counts.BLOCK} \xB7 UNKNOWN ${summary.counts.UNKNOWN} \xB7 REVIEW ${summary.counts.REVIEW} \xB7 SAFE ${summary.counts.SAFE})`
1660
+ );
1661
+ lines.push("\u2500".repeat(60));
1662
+ for (const r of summary.reports) lines.push(...renderServer(r, style));
1663
+ return lines.join("\n");
1664
+ }
1665
+
1666
+ // ../../packages/report-renderer/src/renderCompact.ts
1667
+ function renderCompact(summary, style = DEFAULT_STYLE) {
1668
+ const lines = [];
1669
+ for (const r of summary.reports) {
1670
+ lines.push(
1671
+ `${verdictTag(r.verdict, style)} ${r.target.name} ${symbolList(r.symbols, style)} ${r.riskClass}`
1672
+ );
1673
+ }
1674
+ lines.push(
1675
+ `${verdictTag(summary.verdict, style)} TOTAL BLOCK ${summary.counts.BLOCK} \xB7 UNKNOWN ${summary.counts.UNKNOWN} \xB7 REVIEW ${summary.counts.REVIEW} \xB7 SAFE ${summary.counts.SAFE}`
1676
+ );
1677
+ return lines.join("\n");
1678
+ }
1679
+
1680
+ // ../../packages/report-renderer/src/renderExplain.ts
1681
+ function renderFindingDetail(f, n) {
1682
+ const lines = [];
1683
+ lines.push(`${n}. ${f.title} ${f.blocker ? "[BLOCKER]" : ""}`);
1684
+ lines.push(` id: ${f.id}`);
1685
+ lines.push(` symbol: ${RISK_SYMBOL_LABEL[f.symbol]} (${f.symbol})`);
1686
+ lines.push(` class: ${f.riskClass} ${RISK_CLASS_LABEL[f.riskClass]}`);
1687
+ lines.push(` severity: ${f.severity}`);
1688
+ lines.push(` mode: ${f.mode} confidence: ${f.confidence}`);
1689
+ lines.push(` method: ${f.detectionMethod}`);
1690
+ if (f.evidence.length > 0) {
1691
+ lines.push(` evidence:`);
1692
+ for (const e of f.evidence) {
1693
+ const loc = [e.path, e.line ? `:${e.line}` : ""].filter(Boolean).join("");
1694
+ const detail = e.value ?? e.snippet ?? "";
1695
+ lines.push(` - [${e.type}] ${e.key ?? ""}${detail ? ` = ${detail}` : ""}${loc ? ` (${loc})` : ""}`);
1696
+ }
1697
+ }
1698
+ lines.push(` impact: ${f.impact}`);
1699
+ lines.push(` fix: ${f.fix}`);
1700
+ if (f.falsePositiveNote) lines.push(` note: ${f.falsePositiveNote}`);
1701
+ return lines;
1702
+ }
1703
+ function renderExplain(r, style = DEFAULT_STYLE) {
1704
+ const lines = [];
1705
+ lines.push(`${verdictTag(r.verdict, style)} ${r.target.name}`);
1706
+ lines.push(`label: ${r.publicVerdictLabel}`);
1707
+ lines.push(`source: ${r.target.source ?? "\u2014"}${r.target.version ? `@${r.target.version}` : ""}`);
1708
+ lines.push(`class: ${r.riskClass} ${RISK_CLASS_LABEL[r.riskClass]}`);
1709
+ lines.push(`symbols: ${r.symbols.length ? r.symbols.map((s) => RISK_SYMBOL_LABEL[s]).join(", ") : "none"}`);
1710
+ lines.push(`repro: ${r.reproducibility.level}${r.reproducibility.reasons.length ? ` (${r.reproducibility.reasons.join("; ")})` : ""}`);
1711
+ lines.push("");
1712
+ lines.push(r.summary);
1713
+ if (r.findings.length > 0) {
1714
+ lines.push("");
1715
+ lines.push("Findings");
1716
+ lines.push("\u2500".repeat(60));
1717
+ r.findings.forEach((f, i) => {
1718
+ lines.push(...renderFindingDetail(f, i + 1));
1719
+ lines.push("");
1720
+ });
1721
+ } else {
1722
+ lines.push("");
1723
+ lines.push("No findings.");
1724
+ }
1725
+ lines.push("Recommended policy");
1726
+ lines.push("\u2500".repeat(60));
1727
+ lines.push(`autonomous use: ${r.policy.autonomousUse}`);
1728
+ lines.push(`manual approval: ${r.policy.manualApproval}`);
1729
+ lines.push(`sandbox: ${r.policy.sandbox}`);
1730
+ lines.push("");
1731
+ lines.push("Fingerprints");
1732
+ lines.push("\u2500".repeat(60));
1733
+ lines.push(`config: ${r.fingerprints.configHash}`);
1734
+ lines.push(`target spec: ${r.fingerprints.targetSpecHash}`);
1735
+ lines.push(`risk surface:${r.fingerprints.riskSurfaceHash}`);
1736
+ if (r.fingerprints.packageSpecHash) lines.push(`package: ${r.fingerprints.packageSpecHash}`);
1737
+ return lines.join("\n");
1738
+ }
1739
+
1740
+ // ../../packages/report-renderer/src/renderDrift.ts
1741
+ function renderDriftJson(report) {
1742
+ return JSON.stringify(report, null, 2);
1743
+ }
1744
+ var STATUS_TAG = {
1745
+ unchanged: "OK ",
1746
+ "config-changed": "CFG ",
1747
+ "risk-surface-changed": "RISK ",
1748
+ "verdict-changed": "VRDCT",
1749
+ "package-changed": "RUG! ",
1750
+ added: "ADD ",
1751
+ removed: "DEL "
1752
+ };
1753
+ function renderDrift(report) {
1754
+ const lines = [];
1755
+ lines.push("CallLint verify (drift vs baseline)");
1756
+ lines.push(`config: ${report.configPath}`);
1757
+ const headline = report.rugPullDetected ? "RUG-PULL SIGNAL \u2014 package/source changed since baseline" : report.drifted ? "DRIFT \u2014 risk surface changed since baseline" : "no drift \u2014 matches baseline";
1758
+ lines.push(`result: ${headline}`);
1759
+ lines.push("\u2500".repeat(60));
1760
+ for (const e of report.entries) {
1761
+ const tag = STATUS_TAG[e.status] ?? e.status;
1762
+ lines.push(`${tag} ${e.server}`);
1763
+ for (const reason of e.reasons) {
1764
+ lines.push(` \u2022 ${reason}`);
1765
+ }
1766
+ }
1767
+ return lines.join("\n");
1768
+ }
1769
+
1770
+ // ../../packages/report-renderer/src/renderSarif.ts
1771
+ function levelForSeverity(sev) {
1772
+ if (sev === "critical" || sev === "high") return "error";
1773
+ if (sev === "medium") return "warning";
1774
+ return "note";
1775
+ }
1776
+ function toUri(path) {
1777
+ if (path === "<stdin>" || path === "<inline>") return path;
1778
+ return path.replace(/\\/g, "/");
1779
+ }
1780
+ function ruleFromFinding(f) {
1781
+ return {
1782
+ id: f.id,
1783
+ name: f.title,
1784
+ shortDescription: { text: f.title },
1785
+ fullDescription: { text: f.impact },
1786
+ defaultConfiguration: { level: levelForSeverity(f.severity) },
1787
+ properties: {
1788
+ symbol: f.symbol,
1789
+ riskClass: f.riskClass,
1790
+ tags: ["calllint", f.symbol.toLowerCase(), f.riskClass.toLowerCase()]
1791
+ }
1792
+ };
1793
+ }
1794
+ function resultFromFinding(f, report, configUri) {
1795
+ const ev = f.evidence[0];
1796
+ const region = ev?.line ? { startLine: ev.line } : void 0;
1797
+ const messageParts = [f.impact];
1798
+ if (f.fix) messageParts.push(`Fix: ${f.fix}`);
1799
+ if (f.falsePositiveNote) messageParts.push(`Note: ${f.falsePositiveNote}`);
1800
+ return {
1801
+ ruleId: f.id,
1802
+ level: levelForSeverity(f.severity),
1803
+ message: { text: messageParts.join(" ") },
1804
+ locations: [
1805
+ {
1806
+ physicalLocation: {
1807
+ artifactLocation: { uri: ev?.path ? toUri(ev.path) : configUri },
1808
+ ...region ? { region } : {}
1809
+ },
1810
+ logicalLocations: [{ name: report.target.name, kind: "namespace" }]
1811
+ }
1812
+ ],
1813
+ partialFingerprints: {
1814
+ configHash: report.fingerprints.configHash,
1815
+ riskSurfaceHash: report.fingerprints.riskSurfaceHash
1816
+ },
1817
+ properties: {
1818
+ server: report.target.name,
1819
+ verdict: report.verdict,
1820
+ symbol: f.symbol,
1821
+ riskClass: f.riskClass,
1822
+ mode: f.mode,
1823
+ confidence: f.confidence,
1824
+ blocker: f.blocker
1825
+ }
1826
+ };
1827
+ }
1828
+ function renderSarif(summary) {
1829
+ const configUri = toUri(summary.configPath);
1830
+ const rules = /* @__PURE__ */ new Map();
1831
+ const results = [];
1832
+ for (const report of summary.reports) {
1833
+ for (const f of report.findings) {
1834
+ if (!rules.has(f.id)) rules.set(f.id, ruleFromFinding(f));
1835
+ results.push(resultFromFinding(f, report, configUri));
1836
+ }
1837
+ }
1838
+ const sarif = {
1839
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
1840
+ version: "2.1.0",
1841
+ runs: [
1842
+ {
1843
+ tool: {
1844
+ driver: {
1845
+ name: "CallLint",
1846
+ informationUri: "https://calllint.com",
1847
+ rules: [...rules.values()]
1848
+ }
1849
+ },
1850
+ results,
1851
+ properties: {
1852
+ aggregateVerdict: summary.verdict,
1853
+ counts: summary.counts,
1854
+ generatedAt: summary.generatedAt
1855
+ }
1856
+ }
1857
+ ]
1858
+ };
1859
+ return JSON.stringify(sarif, null, 2);
1860
+ }
1861
+
1862
+ // ../../packages/report-renderer/src/logoBase64.ts
1863
+ var LOGO_REPORT_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAADAFBMVEX+/v7+/v3VAADbChn+///9/f39+/v////+/f3+/Pz9/PzYAAraChn+/f7++/zWAADXAAfbDRzXAAraCxn9+vvbDBv9+vrZARHaDh3YAA7aCxvaDRz87O7bDhzZAQ/fKDXZBhTXAA3bEB/XAAX99vfcGCbaAxLaBhXYAAj9+fnnXmfZBQ/aCRb9/f7bChnZBBTaDRrfJTHZAA387u7eIC763+DcFiL519r41tjYAA3wnKLxqK3YAAvWAAbiPUjcEyD9+Pn76evZAxDZBRP3ys3pcXnnY2zkS1b//v776+zcGSbiO0biRE/ulJvqcnvXAALjQk39/v7tipHZCRj++vvaCRj+/P3qdn7pbXXcFCL75+noanPUAADkUlzxpqzoZ3D76Or409bYAAz2xsn53d/++vr3yczyrbH41NfnYmzfKjb52NrlVWD++PjmYGjWAAPyqq/dIy/86uzsgIfZCBbWAALaDhnkTVjiPUnyr7P89PXiQEzYAAPkRlL2xMjlU17hOUX1wcX2x8rtjJLjSVTpb3jcDh3lTFfgMT30trvsgorlUVzqe4Ppa3P0ub3siZDtjpTZAArth43ztLjre4P64eT75ebdJDHaExvvkZjeJDD98vPdHyz++frreIDtjJPvmaD5293qd3/vmqDnY23fMz/rfYbnYGraFiLmX2jdGyfaFh/gLTnkUVvnZW740dPhPUjpbnbumJ7reYL87u/hOUTiSVLlVmHbEB753N/fLzzcER/eLDbiQEr64OL88PH52dvnaHHXAAnbCRn0vcHzsrb+//7zsbbjUFfwpKnjTFLrdn71ur7lW2X++fr89fbqdH398/PztbndHivzrbP0trzuipHwnqT3ztH75ujwo6jgNUHyqq/52tzwoabtjpX2zND//f7vkpjqfYTeMz3vlZv1vMDdJjTdJzPnXWjzsrfdIi3tjJXzsrnqdHzhOEL1vMHkVWDZEB7ulZvVAALnbXXaFiTdGCbtkpnWAQr99fXZDBbshIv1wcblWGPhP0ryJPiiAAAACXBIWXMAAAsSAAALEgHS3X78AAAEpUlEQVRIx41WB1QUVxSdnf0798/u4hbqsouURYrAAQkR6bjUpYurBBQFg6DYIFgCBhRL7L1jj6iADdTYY0mvJ72YnPQeU000MYkxw4ZDwB0W35wz55+ZV+79/857wzA9TcaEdK8YRsLYsRHC1W0hzAgZZbqDRcw2lyXRTnZq84TDR3tgkTH9WLcDT05u2fEQ4Xs/p31X5HBFrZ4Mjt5Teoa64I8ZmXd+2w0nagd2dxDPIfpWwuWzLbHHwcnvzmbjT10csPbWX7cvljXM1q4B40QldjfVSQ5D86xDjdPOXfizoWXWt2bwTg72mIB+56laenl7u6sp89Le/ew3tRR9HoGMEt+KemNQjiXy8MCZwa6Vw+hjQXFZFa8SKutypN3grazkOBvsPa8U+6576drzooLdjmPSae+wYeBtklvXlJK6r88hea7WpPAs21SpMGnby8n+al8iE6fBJZISn0xfT6PGM6WIkJLXKvXGLYHtCZtJIifOOB3L2Y3m82ED18Mil3NYo3MvvPq5qgbp6XdroRMSX5U91dhm+HKds1dCKXieRBZ6eaz77GSb8ZfscYTa6IFhnshNqr8YmRGqdFM8gAEcFj+pcfQfNOX+eq8ZP4G3lTjJcjtGGiNM7k9JQyMMhBiaQh1HOSdVNyK58GHRgLk+6TlqL41SKpX6LQOednUUVhovdUpixuM2ATJrQOCcCR6mkXqlR0A+kB+ll2pG6dyi0uoGzRevcJ47bYxfaQr393BdBYyJOuM/wXTiTu483wzxgDwf7h/Verw8p9BP9SLB82OTbqatxt/sPN/OChImRCahXTslk1hJn+GO/G4mDMqeWTuOkJiFz5XhUzI8I833Zry1gqSHvCmlmONYl1oLjnNhAEGiwu1ZF4sctdMvXT/VG5L13DgcDDd0HAXHAwPMBobcZ5YDcidcuVHi3WrLQeaCA4+UZrNfwbLgpUVbXiGYHLGoYMFE/Mz+OomtAdf7y5R0VhinimlQDb66IlRhiksjKJ5hig1dUdKqul3FxnQGSHoIXJAvjymxq8xuGUMCpGFuusFAqtZHo1Q0tfkYxnuX2kJiKHE5fG3nEkWwcNRh2gJgpVavlDoHK5YEVlfyRKTNcDiRWx4T+6hSKagvGnhB5y6oxENRlazaCLEPgscxReu0piRnqaC5ZGCY80ip1NlvyLTWCe+JIOoEhSPasugAvdLddWmg0L/jB7pLNQHRe72LIbf1ttJuCJjqEK/ThymWg3PCQoVGr8scPVX9oEiA5L+uMV71RmTYTP+hZeBHY3OGv9SxdAM7XhyQFVPRm0OHv69z3YZAygRin1/4u+aWHUXos4tzeFudhwNxg8vBy/FJs3oXyVKvhqV3O6b/y1BmQT6bisXszI+BbEd2DI6O3Q3OzqijctLBpuBHtbZimc7YgQL2RheBviYqpfx0Ngc1xmueubsE/+kWGe1nztGJzQKqPUGKbbjCHtzJ0P4mI09pzthTEzdUORSzKTzlmX6N8vg+7sKmki9mvYPRlLkHE4boD1u3Hpr9VtcY7T9IGKMfzv9gCuwPq17Tl8Pr2+3tvyh1eZ8vHWz+PCRdnUTM/gUe0BB8YRiSMgAAAABJRU5ErkJggg==";
1864
+
1865
+ // ../../packages/report-renderer/src/renderHtml.ts
1866
+ function esc(value) {
1867
+ return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1868
+ }
1869
+ var VERDICT_COLOR = {
1870
+ SAFE: "#1a7f37",
1871
+ REVIEW: "#9a6700",
1872
+ BLOCK: "#cf222e",
1873
+ UNKNOWN: "#6e7781"
1874
+ };
1875
+ function badge(verdict) {
1876
+ return `<span class="badge" style="background:${VERDICT_COLOR[verdict]}">${esc(verdict)}</span>`;
1877
+ }
1878
+ function findingRow(f) {
1879
+ const ev = f.evidence.map((e) => {
1880
+ const loc = e.path ? ` (${esc(e.path)}${e.line ? `:${e.line}` : ""})` : "";
1881
+ const detail = e.value ?? e.snippet ?? "";
1882
+ return `<li><code>${esc(e.type)}</code> ${esc(e.key ?? "")}${detail ? ` = ${esc(detail)}` : ""}${loc}</li>`;
1883
+ }).join("");
1884
+ return `
1885
+ <tr class="sev-${esc(f.severity)}">
1886
+ <td>${f.blocker ? "\u26D4 " : ""}${esc(f.title)}</td>
1887
+ <td><code>${esc(f.id)}</code></td>
1888
+ <td>${esc(RISK_SYMBOL_LABEL[f.symbol])} <small>(${esc(f.symbol)})</small></td>
1889
+ <td>${esc(f.riskClass)}</td>
1890
+ <td>${esc(f.severity)}</td>
1891
+ <td>${esc(f.mode)}</td>
1892
+ <td>
1893
+ <div class="impact">${esc(f.impact)}</div>
1894
+ <div class="fix"><strong>Fix:</strong> ${esc(f.fix)}</div>
1895
+ ${f.evidence.length ? `<ul class="evidence">${ev}</ul>` : ""}
1896
+ ${f.falsePositiveNote ? `<div class="fp"><em>${esc(f.falsePositiveNote)}</em></div>` : ""}
1897
+ </td>
1898
+ </tr>`;
1899
+ }
1900
+ function serverCard(r) {
1901
+ const symbols = r.symbols.map((s) => `<span class="sym">${esc(RISK_SYMBOL_LABEL[s])}</span>`).join(" ");
1902
+ const findings = r.findings.length ? `<table class="findings">
1903
+ <thead><tr><th>Finding</th><th>Rule</th><th>Symbol</th><th>Class</th><th>Severity</th><th>Mode</th><th>Detail</th></tr></thead>
1904
+ <tbody>${r.findings.map(findingRow).join("")}</tbody>
1905
+ </table>` : `<p class="none">No findings.</p>`;
1906
+ return `
1907
+ <section class="server">
1908
+ <h2>${badge(r.verdict)} ${esc(r.target.name)}</h2>
1909
+ <p class="meta">
1910
+ Class <strong>${esc(r.riskClass)}</strong> ${esc(RISK_CLASS_LABEL[r.riskClass])}
1911
+ \xB7 confidence ${esc(r.confidence)}
1912
+ \xB7 reproducibility ${esc(r.reproducibility.level)}
1913
+ </p>
1914
+ <p class="symbols">${symbols || '<span class="sym none">no risk symbols</span>'}</p>
1915
+ <p class="summary">${esc(r.summary)}</p>
1916
+ ${findings}
1917
+ <p class="policy">autonomous use: <strong>${esc(r.policy.autonomousUse)}</strong>
1918
+ \xB7 manual approval: <strong>${esc(r.policy.manualApproval)}</strong>
1919
+ \xB7 sandbox: <strong>${esc(r.policy.sandbox)}</strong></p>
1920
+ </section>`;
1921
+ }
1922
+ var LOGO_SRC = `data:image/png;base64,${LOGO_REPORT_BASE64}`;
1923
+ var STYLE = `
1924
+ :root { font-family: -apple-system, Segoe UI, Roboto, sans-serif; --brand: #c41e3a; --brand-glow: rgba(196, 30, 58, 0.35); }
1925
+ body { margin: 0; background: #f6f8fa; color: #1f2328; }
1926
+ header { background: #24292f; color: #fff; padding: 24px 32px; }
1927
+ .header-row { display: flex; align-items: center; gap: 14px; }
1928
+ .brand-mark { width: 40px; height: 40px; flex: 0 0 auto; animation: mark-enter 0.6s cubic-bezier(0.22, 1, 0.36, 1) both, mark-glow 4s ease-in-out 0.6s infinite; }
1929
+ header h1 { margin: 0 0 4px; font-size: 20px; }
1930
+ header .sub { color: #b1b8c0; font-size: 13px; }
1931
+ @keyframes mark-enter { from { opacity: 0; transform: scale(0.88); } to { opacity: 1; transform: scale(1); } }
1932
+ @keyframes mark-glow { 0%, 100% { filter: drop-shadow(0 0 0 transparent); } 50% { filter: drop-shadow(0 0 8px var(--brand-glow)); } }
1933
+ @media (prefers-reduced-motion: reduce) { .brand-mark { animation: none; } }
1934
+ .counts { padding: 16px 32px; background: #fff; border-bottom: 1px solid #d0d7de; }
1935
+ .counts span { margin-right: 16px; font-size: 14px; }
1936
+ main { padding: 24px 32px; max-width: 1100px; }
1937
+ .server { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; padding: 20px 24px; margin-bottom: 20px; }
1938
+ .server h2 { margin: 0 0 8px; font-size: 17px; }
1939
+ .badge { color: #fff; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; vertical-align: middle; }
1940
+ .meta, .symbols, .summary, .policy { font-size: 13px; color: #57606a; margin: 6px 0; }
1941
+ .sym { background: #eaeef2; border-radius: 4px; padding: 2px 8px; font-size: 12px; }
1942
+ .sym.none { background: transparent; color: #8c959f; }
1943
+ table.findings { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
1944
+ table.findings th, table.findings td { border: 1px solid #d0d7de; padding: 6px 8px; text-align: left; vertical-align: top; }
1945
+ table.findings th { background: #f6f8fa; }
1946
+ tr.sev-critical td:first-child, tr.sev-high td:first-child { border-left: 3px solid #cf222e; }
1947
+ .evidence { margin: 6px 0; padding-left: 18px; }
1948
+ .fix { color: #1a7f37; margin-top: 4px; }
1949
+ .fp { color: #8c959f; margin-top: 4px; }
1950
+ .none { color: #1a7f37; font-style: italic; }
1951
+ code { background: #eff1f3; padding: 1px 4px; border-radius: 3px; font-size: 12px; }
1952
+ footer { padding: 16px 32px; color: #8c959f; font-size: 12px; }
1953
+ `;
1954
+ function renderHtml(summary) {
1955
+ const c = summary.counts;
1956
+ const cards = summary.reports.map(serverCard).join("");
1957
+ return `<!doctype html>
1958
+ <html lang="en">
1959
+ <head>
1960
+ <meta charset="utf-8">
1961
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1962
+ <title>CallLint report \u2014 ${esc(summary.configPath)}</title>
1963
+ <style>${STYLE}</style>
1964
+ </head>
1965
+ <body>
1966
+ <header>
1967
+ <div class="header-row">
1968
+ <img class="brand-mark" src="${LOGO_SRC}" width="40" height="40" alt="CallLint">
1969
+ <div>
1970
+ <h1>CallLint report ${badge(summary.verdict)}</h1>
1971
+ <div class="sub">${esc(summary.configPath)} \xB7 generated ${esc(summary.generatedAt)}</div>
1972
+ </div>
1973
+ </div>
1974
+ </header>
1975
+ <div class="counts">
1976
+ <span>\u26D4 BLOCK: <strong>${c.BLOCK}</strong></span>
1977
+ <span>\u25C7 UNKNOWN: <strong>${c.UNKNOWN}</strong></span>
1978
+ <span>\u26A0 REVIEW: <strong>${c.REVIEW}</strong></span>
1979
+ <span>\u{1F6E1} SAFE: <strong>${c.SAFE}</strong></span>
1980
+ </div>
1981
+ <main>${cards}</main>
1982
+ <footer>CallLint \xB7 evidence-backed verdicts for agent tools \xB7 static analysis, no server executed</footer>
1983
+ </body>
1984
+ </html>`;
1985
+ }
1986
+
1987
+ // src/exitCode.ts
1988
+ function exitCodeFor(summary, policy) {
1989
+ const verdict = summary.verdict;
1990
+ if (!shouldFailCi(verdict, policy)) return EXIT.OK;
1991
+ switch (verdict) {
1992
+ case "BLOCK":
1993
+ return EXIT.BLOCK;
1994
+ case "UNKNOWN":
1995
+ return EXIT.UNKNOWN;
1996
+ case "REVIEW":
1997
+ return EXIT.REVIEW;
1998
+ case "SAFE":
1999
+ return EXIT.OK;
2000
+ }
2001
+ }
2002
+
2003
+ // src/commands/resolveInput.ts
2004
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
2005
+ import { join as join2 } from "node:path";
2006
+ var DEFAULT_CONFIG_PATHS = [
2007
+ ".cursor/mcp.json",
2008
+ ".mcp.json",
2009
+ "mcp.json",
2010
+ ".claude/settings.json",
2011
+ ".vscode/mcp.json"
2012
+ ];
2013
+ function findDefaultConfig(cwd) {
2014
+ for (const rel of DEFAULT_CONFIG_PATHS) {
2015
+ const p = join2(cwd, rel);
2016
+ if (existsSync2(p)) return p;
2017
+ }
2018
+ return void 0;
2019
+ }
2020
+ function isInputError(v) {
2021
+ return "error" in v;
2022
+ }
2023
+ function resolveConfigInput(args, deps) {
2024
+ if (flagBool(args.flags, "stdin")) {
2025
+ return { text: deps.readStdin(), configPath: "<stdin>" };
2026
+ }
2027
+ const given = args.positionals[0];
2028
+ if (given) {
2029
+ const spec = parseTargetSpec(given);
2030
+ if (spec.kind === "npm") {
2031
+ if (!spec.packageSpec) {
2032
+ return { error: "Empty npm target. Use npm:<package>[@version].", exitCode: EXIT.USAGE };
2033
+ }
2034
+ return synthesizeNpmConfig(spec.packageSpec);
2035
+ }
2036
+ if (spec.kind === "github") {
2037
+ return {
2038
+ error: "GitHub targets require network access. Re-run with --online to fetch repo MCP configs.",
2039
+ exitCode: EXIT.USAGE
2040
+ };
2041
+ }
2042
+ }
2043
+ const resolved = given ?? findDefaultConfig(deps.cwd);
2044
+ if (!resolved) {
2045
+ return {
2046
+ error: "No config given and none found. Pass a path or use --stdin.\nLooked in: " + DEFAULT_CONFIG_PATHS.join(", "),
2047
+ exitCode: EXIT.USAGE
2048
+ };
2049
+ }
2050
+ if (!existsSync2(resolved)) {
2051
+ return { error: `File not found: ${resolved}`, exitCode: EXIT.USAGE };
2052
+ }
2053
+ return { text: readFileSync3(resolved, "utf8"), configPath: resolved };
2054
+ }
2055
+
2056
+ // src/commands/scan.ts
2057
+ function scanCommand(args, deps) {
2058
+ const policyPath = flagStr(args.flags, "policy");
2059
+ let policy;
2060
+ try {
2061
+ policy = loadPolicyOrDefault(policyPath);
2062
+ } catch (err) {
2063
+ return {
2064
+ stdout: "",
2065
+ stderr: err instanceof Error ? err.message : String(err),
2066
+ exitCode: EXIT.ERROR
2067
+ };
2068
+ }
2069
+ const input = deps.online?.inputOverride ?? resolveConfigInput(args, deps);
2070
+ if (isInputError(input)) {
2071
+ return { stdout: "", stderr: input.error, exitCode: input.exitCode };
2072
+ }
2073
+ const { text, configPath } = input;
2074
+ let summary;
2075
+ try {
2076
+ summary = scanConfigText(text, configPath, {
2077
+ policy,
2078
+ now: deps.now,
2079
+ generatedAt: deps.generatedAt,
2080
+ extraFindings: deps.online?.extraFindings
2081
+ });
2082
+ } catch (err) {
2083
+ if (err instanceof ConfigParseError) {
2084
+ return {
2085
+ stdout: "",
2086
+ stderr: `Parse error in ${configPath}: ${err.message}`,
2087
+ exitCode: EXIT.ERROR
2088
+ };
2089
+ }
2090
+ throw err;
2091
+ }
2092
+ if (deps.writeCacheFile !== false) {
2093
+ try {
2094
+ writeCache(summary, join3(deps.cwd, ".calllint", "last-scan.json"));
2095
+ } catch {
2096
+ }
2097
+ }
2098
+ const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
2099
+ let stdout;
2100
+ if (flagBool(args.flags, "json")) stdout = renderJson(summary);
2101
+ else if (flagBool(args.flags, "sarif")) stdout = renderSarif(summary);
2102
+ else if (flagBool(args.flags, "html")) stdout = renderHtml(summary);
2103
+ else if (flagBool(args.flags, "compact")) stdout = renderCompact(summary, style);
2104
+ else stdout = renderTerminal(summary, style);
2105
+ const exitCode = flagBool(args.flags, "ci") ? exitCodeFor(summary, policy) : EXIT.OK;
2106
+ return { stdout, exitCode };
2107
+ }
2108
+
2109
+ // src/commands/explain.ts
2110
+ import { join as join4 } from "node:path";
2111
+ function explainCommand(args, deps) {
2112
+ const serverName = args.positionals[0];
2113
+ if (!serverName) {
2114
+ return {
2115
+ stdout: "",
2116
+ stderr: "Usage: calllint explain <server>",
2117
+ exitCode: EXIT.USAGE
2118
+ };
2119
+ }
2120
+ const summary = readCache(join4(deps.cwd, ".calllint", "last-scan.json"));
2121
+ if (!summary) {
2122
+ return {
2123
+ stdout: "",
2124
+ stderr: "No cached scan found. Run `calllint scan` first.",
2125
+ exitCode: EXIT.ERROR
2126
+ };
2127
+ }
2128
+ const report = summary.reports.find((r) => r.target.name === serverName);
2129
+ if (!report) {
2130
+ const names = summary.reports.map((r) => r.target.name).join(", ");
2131
+ return {
2132
+ stdout: "",
2133
+ stderr: `Server "${serverName}" not in last scan. Available: ${names || "(none)"}`,
2134
+ exitCode: EXIT.USAGE
2135
+ };
2136
+ }
2137
+ const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
2138
+ return { stdout: renderExplain(report, style), exitCode: EXIT.OK };
2139
+ }
2140
+
2141
+ // src/commands/policy.ts
2142
+ import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "node:fs";
2143
+ import { join as join5 } from "node:path";
2144
+ function policyCommand(args, deps) {
2145
+ const sub = args.positionals[0];
2146
+ if (sub === "init") {
2147
+ const path = join5(deps.cwd, "calllint.policy.json");
2148
+ if (existsSync3(path) && !flagBool(args.flags, "force")) {
2149
+ return {
2150
+ stdout: "",
2151
+ stderr: `${path} already exists. Use --force to overwrite.`,
2152
+ exitCode: EXIT.USAGE
2153
+ };
2154
+ }
2155
+ writeFileSync2(path, defaultPolicyJson(), "utf8");
2156
+ return { stdout: `Wrote default policy to ${path}`, exitCode: EXIT.OK };
2157
+ }
2158
+ if (sub === "explain") {
2159
+ try {
2160
+ const policy = loadPolicyOrDefault(flagStr(args.flags, "policy"));
2161
+ return { stdout: JSON.stringify(policy, null, 2), exitCode: EXIT.OK };
2162
+ } catch (err) {
2163
+ return {
2164
+ stdout: "",
2165
+ stderr: err instanceof Error ? err.message : String(err),
2166
+ exitCode: EXIT.ERROR
2167
+ };
2168
+ }
2169
+ }
2170
+ return {
2171
+ stdout: "",
2172
+ stderr: "Usage: calllint policy <init|explain>",
2173
+ exitCode: EXIT.USAGE
2174
+ };
2175
+ }
2176
+
2177
+ // src/commands/verify.ts
2178
+ import { join as join6 } from "node:path";
2179
+ function loadPolicy(args) {
2180
+ try {
2181
+ return loadPolicyOrDefault(flagStr(args.flags, "policy"));
2182
+ } catch (err) {
2183
+ return { error: err instanceof Error ? err.message : String(err) };
2184
+ }
2185
+ }
2186
+ function baselinePathFor(args, cwd) {
2187
+ return flagStr(args.flags, "baseline") ?? join6(cwd, ".calllint", "baseline.json");
2188
+ }
2189
+ function baselineCommand(args, deps) {
2190
+ const policy = loadPolicy(args);
2191
+ if ("error" in policy) return { stdout: "", stderr: policy.error, exitCode: EXIT.ERROR };
2192
+ const input = deps.online?.inputOverride ?? resolveConfigInput(args, deps);
2193
+ if (isInputError(input)) return { stdout: "", stderr: input.error, exitCode: input.exitCode };
2194
+ let summary;
2195
+ try {
2196
+ summary = scanConfigText(input.text, input.configPath, {
2197
+ policy,
2198
+ generatedAt: deps.generatedAt,
2199
+ extraFindings: deps.online?.extraFindings
2200
+ });
2201
+ } catch (err) {
2202
+ if (err instanceof ConfigParseError) {
2203
+ return { stdout: "", stderr: `Parse error in ${input.configPath}: ${err.message}`, exitCode: EXIT.ERROR };
2204
+ }
2205
+ throw err;
2206
+ }
2207
+ const baseline = buildBaseline(summary, deps.generatedAt);
2208
+ const path = baselinePathFor(args, deps.cwd);
2209
+ if (deps.writeBaselineFile !== false) {
2210
+ try {
2211
+ writeBaseline(baseline, path);
2212
+ } catch (err) {
2213
+ return { stdout: "", stderr: `Could not write baseline: ${String(err)}`, exitCode: EXIT.ERROR };
2214
+ }
2215
+ }
2216
+ if (flagBool(args.flags, "json")) {
2217
+ return { stdout: JSON.stringify(baseline, null, 2), exitCode: EXIT.OK };
2218
+ }
2219
+ const n = baseline.entries.length;
2220
+ return {
2221
+ stdout: `Baseline written for ${n} server${n === 1 ? "" : "s"} \u2192 ${path}`,
2222
+ exitCode: EXIT.OK
2223
+ };
2224
+ }
2225
+ function verifyCommand(args, deps) {
2226
+ const policy = loadPolicy(args);
2227
+ if ("error" in policy) return { stdout: "", stderr: policy.error, exitCode: EXIT.ERROR };
2228
+ const path = baselinePathFor(args, deps.cwd);
2229
+ const baseline = readBaseline(path);
2230
+ if (!baseline) {
2231
+ return {
2232
+ stdout: "",
2233
+ stderr: `No baseline found at ${path}. Run \`calllint baseline\` first.`,
2234
+ exitCode: EXIT.ERROR
2235
+ };
2236
+ }
2237
+ const input = deps.online?.inputOverride ?? resolveConfigInput(args, deps);
2238
+ if (isInputError(input)) return { stdout: "", stderr: input.error, exitCode: input.exitCode };
2239
+ let summary;
2240
+ try {
2241
+ summary = scanConfigText(input.text, input.configPath, {
2242
+ policy,
2243
+ generatedAt: deps.generatedAt,
2244
+ extraFindings: deps.online?.extraFindings
2245
+ });
2246
+ } catch (err) {
2247
+ if (err instanceof ConfigParseError) {
2248
+ return { stdout: "", stderr: `Parse error in ${input.configPath}: ${err.message}`, exitCode: EXIT.ERROR };
2249
+ }
2250
+ throw err;
2251
+ }
2252
+ const drift = computeDrift(baseline, summary, deps.generatedAt);
2253
+ const stdout = flagBool(args.flags, "json") ? renderDriftJson(drift) : renderDrift(drift);
2254
+ const exitCode = flagBool(args.flags, "ci") && drift.drifted ? EXIT.DRIFT : EXIT.OK;
2255
+ return { stdout, exitCode };
2256
+ }
2257
+
2258
+ // src/run.ts
2259
+ function run(argv, deps) {
2260
+ const args = parseArgs(argv);
2261
+ const cmd = args.command;
2262
+ if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
2263
+ return helpCommand();
2264
+ }
2265
+ switch (cmd) {
2266
+ case "scan":
2267
+ return scanCommand(args, {
2268
+ cwd: deps.cwd,
2269
+ readStdin: deps.readStdin,
2270
+ now: deps.now,
2271
+ generatedAt: deps.generatedAt,
2272
+ writeCacheFile: deps.writeCacheFile,
2273
+ online: deps.online
2274
+ });
2275
+ case "baseline":
2276
+ return baselineCommand(args, {
2277
+ cwd: deps.cwd,
2278
+ readStdin: deps.readStdin,
2279
+ generatedAt: deps.generatedAt,
2280
+ writeBaselineFile: deps.writeCacheFile,
2281
+ online: deps.online
2282
+ });
2283
+ case "verify":
2284
+ return verifyCommand(args, {
2285
+ cwd: deps.cwd,
2286
+ readStdin: deps.readStdin,
2287
+ generatedAt: deps.generatedAt,
2288
+ writeBaselineFile: deps.writeCacheFile,
2289
+ online: deps.online
2290
+ });
2291
+ case "explain":
2292
+ return explainCommand(args, { cwd: deps.cwd });
2293
+ case "policy":
2294
+ return policyCommand(args, { cwd: deps.cwd });
2295
+ default:
2296
+ return {
2297
+ stdout: "",
2298
+ stderr: `Unknown command: ${cmd}
2299
+ Run \`calllint help\`.`,
2300
+ exitCode: 2
2301
+ };
2302
+ }
2303
+ }
2304
+
2305
+ // ../../packages/online/src/npm.ts
2306
+ function isRecord3(v) {
2307
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2308
+ }
2309
+ function parseSpec(packageSpec) {
2310
+ if (packageSpec.startsWith("@")) {
2311
+ const slash = packageSpec.indexOf("/");
2312
+ const at2 = packageSpec.indexOf("@", slash);
2313
+ if (at2 === -1) return { name: packageSpec };
2314
+ return { name: packageSpec.slice(0, at2), version: packageSpec.slice(at2 + 1) || void 0 };
2315
+ }
2316
+ const at = packageSpec.indexOf("@");
2317
+ if (at <= 0) return { name: packageSpec };
2318
+ return { name: packageSpec.slice(0, at), version: packageSpec.slice(at + 1) || void 0 };
2319
+ }
2320
+ var INSTALL_SCRIPT_KEYS = ["preinstall", "install", "postinstall"];
2321
+ async function fetchNpmFacts(packageSpec, fetchJson) {
2322
+ const { name, version } = parseSpec(packageSpec);
2323
+ const url = `https://registry.npmjs.org/${name.replace(/\//g, "%2f")}`;
2324
+ let doc;
2325
+ try {
2326
+ doc = await fetchJson(url);
2327
+ } catch {
2328
+ return { name, versionExists: false, installScripts: [] };
2329
+ }
2330
+ if (!isRecord3(doc)) return { name, versionExists: false, installScripts: [] };
2331
+ const distTags = isRecord3(doc["dist-tags"]) ? doc["dist-tags"] : {};
2332
+ const latestVersion = typeof distTags.latest === "string" ? distTags.latest : void 0;
2333
+ const versions = isRecord3(doc.versions) ? doc.versions : {};
2334
+ const isFloating = !version || version === "latest" || /[\^~><*]/.test(version);
2335
+ const resolvedVersion = isFloating ? latestVersion : version;
2336
+ const versionDoc = resolvedVersion && isRecord3(versions[resolvedVersion]) ? versions[resolvedVersion] : void 0;
2337
+ if (!versionDoc) {
2338
+ return { name, versionExists: false, installScripts: [], latestVersion, resolvedVersion };
2339
+ }
2340
+ const scripts = isRecord3(versionDoc.scripts) ? versionDoc.scripts : {};
2341
+ const installScripts = INSTALL_SCRIPT_KEYS.filter(
2342
+ (k) => typeof scripts[k] === "string" && scripts[k].length > 0
2343
+ );
2344
+ const deprecated = typeof versionDoc.deprecated === "string" ? versionDoc.deprecated : void 0;
2345
+ return {
2346
+ name,
2347
+ versionExists: true,
2348
+ installScripts,
2349
+ deprecated,
2350
+ latestVersion,
2351
+ resolvedVersion
2352
+ };
2353
+ }
2354
+ function findingsFromNpmFacts(facts, fetchedAt) {
2355
+ const findings = [];
2356
+ const stamp = (f) => ({ ...f, source: "online", fetchedAt });
2357
+ if (!facts.versionExists) {
2358
+ findings.push(stamp({
2359
+ id: "supply.version-not-found",
2360
+ title: "Package version not found in registry",
2361
+ severity: "high",
2362
+ blocker: false,
2363
+ symbol: "SUPPLY",
2364
+ riskClass: "S1",
2365
+ mode: "OBSERVED",
2366
+ confidence: "high",
2367
+ detectionMethod: "package-metadata",
2368
+ evidence: [{ type: "package-metadata", key: "registry", value: facts.name }],
2369
+ impact: "The requested package version does not exist on the npm registry, so the configured server cannot be verified or may resolve to something unexpected.",
2370
+ fix: "Pin to a published, existing version of the package."
2371
+ }));
2372
+ return findings;
2373
+ }
2374
+ if (facts.installScripts.length > 0) {
2375
+ findings.push(stamp({
2376
+ id: "supply.install-scripts",
2377
+ title: "Package runs install scripts",
2378
+ severity: "high",
2379
+ blocker: false,
2380
+ symbol: "EXEC",
2381
+ riskClass: "S4",
2382
+ mode: "OBSERVED",
2383
+ confidence: "high",
2384
+ detectionMethod: "package-metadata",
2385
+ evidence: facts.installScripts.map((s) => ({
2386
+ type: "package-metadata",
2387
+ key: "script",
2388
+ value: s
2389
+ })),
2390
+ impact: "npm install/postinstall scripts execute arbitrary code on the host at install time, before the agent ever invokes a tool.",
2391
+ fix: "Review the install scripts, or install with --ignore-scripts and vendor the package.",
2392
+ falsePositiveNote: "Many legitimate packages use postinstall for native builds; review what the script does."
2393
+ }));
2394
+ }
2395
+ if (facts.deprecated) {
2396
+ findings.push(stamp({
2397
+ id: "supply.deprecated",
2398
+ title: "Package version is deprecated",
2399
+ severity: "medium",
2400
+ blocker: false,
2401
+ symbol: "SUPPLY",
2402
+ riskClass: "S1",
2403
+ mode: "OBSERVED",
2404
+ confidence: "high",
2405
+ detectionMethod: "package-metadata",
2406
+ evidence: [{ type: "package-metadata", key: "deprecated", value: facts.deprecated }],
2407
+ impact: "A deprecated package may be unmaintained and miss security fixes.",
2408
+ fix: "Migrate to the maintained successor or a supported version."
2409
+ }));
2410
+ }
2411
+ return findings;
2412
+ }
2413
+ async function enrichNpmPackage(packageSpec, fetchJson, fetchedAt) {
2414
+ const facts = await fetchNpmFacts(packageSpec, fetchJson);
2415
+ return { facts, findings: findingsFromNpmFacts(facts, fetchedAt) };
2416
+ }
2417
+
2418
+ // ../../packages/online/src/github.ts
2419
+ var GITHUB_CONFIG_CANDIDATES = [
2420
+ ".cursor/mcp.json",
2421
+ ".mcp.json",
2422
+ "mcp.json",
2423
+ ".vscode/mcp.json",
2424
+ ".claude/settings.json"
2425
+ ];
2426
+ function rawUrl(repo, ref, path) {
2427
+ return `https://raw.githubusercontent.com/${repo}/${ref}/${path}`;
2428
+ }
2429
+ async function fetchGithubConfig(repo, fetchText, ref = "HEAD") {
2430
+ for (const path of GITHUB_CONFIG_CANDIDATES) {
2431
+ const text = await fetchText(rawUrl(repo, ref, path));
2432
+ if (text && text.trim().length > 0) {
2433
+ return { text, foundPath: path, ref };
2434
+ }
2435
+ }
2436
+ return { ref };
2437
+ }
2438
+
2439
+ // src/online.ts
2440
+ var realFetchJson = async (url) => {
2441
+ const res = await fetch(url);
2442
+ if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
2443
+ return res.json();
2444
+ };
2445
+ var realFetchText = async (url) => {
2446
+ const res = await fetch(url);
2447
+ if (!res.ok) return void 0;
2448
+ return res.text();
2449
+ };
2450
+ async function computeOnlineEnrichment(argv, deps = {}) {
2451
+ const args = parseArgs(argv);
2452
+ if (!flagBool(args.flags, "online")) return void 0;
2453
+ const given = args.positionals[0];
2454
+ if (!given) return void 0;
2455
+ const spec = parseTargetSpec(given);
2456
+ const fetchJson = deps.fetchJson ?? realFetchJson;
2457
+ const fetchText = deps.fetchText ?? realFetchText;
2458
+ const fetchedAt = deps.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
2459
+ if (spec.kind === "npm" && spec.packageSpec) {
2460
+ const serverName = serverNameForPackage(spec.packageSpec);
2461
+ const { findings } = await enrichNpmPackage(spec.packageSpec, fetchJson, fetchedAt);
2462
+ return { extraFindings: { [serverName]: findings } };
2463
+ }
2464
+ if (spec.kind === "github" && spec.repo) {
2465
+ const result = await fetchGithubConfig(spec.repo, fetchText, spec.ref ?? "HEAD");
2466
+ if (result.text) {
2467
+ return {
2468
+ inputOverride: {
2469
+ text: result.text,
2470
+ configPath: `github:${spec.repo}@${result.ref}/${result.foundPath}`
2471
+ },
2472
+ note: `fetched ${result.foundPath} @ ${result.ref}`
2473
+ };
2474
+ }
2475
+ return void 0;
2476
+ }
2477
+ return void 0;
2478
+ }
2479
+
2480
+ // src/clock.ts
2481
+ function resolveClock(argv, fallback) {
2482
+ const { flags } = parseArgs(argv);
2483
+ const raw = flagStr(flags, "generated-at");
2484
+ if (raw !== void 0) {
2485
+ const ms = Date.parse(raw);
2486
+ if (Number.isNaN(ms)) {
2487
+ throw new Error(
2488
+ `Invalid --generated-at value: "${raw}" (expected an ISO 8601 timestamp)`
2489
+ );
2490
+ }
2491
+ return { generatedAt: new Date(ms).toISOString(), now: ms };
2492
+ }
2493
+ const d = fallback();
2494
+ return { generatedAt: d.toISOString(), now: d.getTime() };
2495
+ }
2496
+
2497
+ // src/index.ts
2498
+ function readStdin() {
2499
+ try {
2500
+ return readFileSync4(0, "utf8");
2501
+ } catch {
2502
+ return "";
2503
+ }
2504
+ }
2505
+ async function main() {
2506
+ const argv = process.argv.slice(2);
2507
+ let clock;
2508
+ try {
2509
+ clock = resolveClock(argv, () => /* @__PURE__ */ new Date());
2510
+ } catch (err) {
2511
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
2512
+ `);
2513
+ process.exitCode = 2;
2514
+ return;
2515
+ }
2516
+ const { generatedAt, now } = clock;
2517
+ let online;
2518
+ try {
2519
+ online = await computeOnlineEnrichment(argv, { fetchedAt: generatedAt });
2520
+ } catch (err) {
2521
+ process.stderr.write(`--online failed: ${err instanceof Error ? err.message : String(err)}
2522
+ `);
2523
+ process.exitCode = 3;
2524
+ return;
2525
+ }
2526
+ if (online?.note) process.stderr.write(`online: ${online.note}
2527
+ `);
2528
+ const result = run(argv, {
2529
+ cwd: process.cwd(),
2530
+ readStdin,
2531
+ now,
2532
+ generatedAt,
2533
+ online
2534
+ });
2535
+ if (result.stdout) process.stdout.write(result.stdout + "\n");
2536
+ if (result.stderr) process.stderr.write(result.stderr + "\n");
2537
+ process.exitCode = result.exitCode;
2538
+ }
2539
+ void main();