protect-mcp 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,10 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ProtectGateway,
4
+ formatSimulation,
4
5
  initSigning,
5
6
  loadPolicy,
7
+ parseLogFile,
8
+ simulate,
6
9
  validateCredentials
7
- } from "./chunk-3WCA7O4D.mjs";
10
+ } from "./chunk-GV7N53QE.mjs";
8
11
 
9
12
  // src/cli.ts
10
13
  function printHelp() {
@@ -13,9 +16,16 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
13
16
 
14
17
  Usage:
15
18
  protect-mcp [options] -- <command> [args...]
19
+ protect-mcp quickstart
16
20
  protect-mcp init [--dir <path>]
17
21
  protect-mcp demo
22
+ protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
18
23
  protect-mcp status [--dir <path>]
24
+ protect-mcp digest [--today] [--dir <path>]
25
+ protect-mcp receipts [--last <n>] [--dir <path>]
26
+ protect-mcp bundle [--output <path>] [--dir <path>]
27
+ protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]
28
+ protect-mcp report [--period <days>d] [--format md|json] [--output <path>] [--dir <path>]
19
29
 
20
30
  Options:
21
31
  --policy <path> Policy/config JSON file (default: allow-all)
@@ -25,17 +35,26 @@ Options:
25
35
  --help Show this help
26
36
 
27
37
  Commands:
38
+ quickstart Zero-config onboarding: init + demo + show receipts in one command
28
39
  init Generate config template, Ed25519 keypair, and sample policy
29
40
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
41
+ trace <id> Visualize the receipt DAG from a given receipt_id (ASCII tree)
30
42
  status Show tool call statistics from the local decision log
43
+ digest Generate a human-readable summary of agent activity
44
+ receipts Show recent persisted signed receipts
45
+ bundle Export an offline-verifiable audit bundle
31
46
 
32
47
  Examples:
48
+ protect-mcp quickstart
33
49
  protect-mcp -- node my-server.js
34
50
  protect-mcp --policy protect-mcp.json -- node my-server.js
35
- protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
36
51
  protect-mcp init
37
52
  protect-mcp demo
53
+ protect-mcp trace sha256:abc123 --depth 5
38
54
  protect-mcp status
55
+ protect-mcp digest --today
56
+ protect-mcp receipts --last 10
57
+ protect-mcp bundle --output audit.json
39
58
 
40
59
  `);
41
60
  }
@@ -96,10 +115,7 @@ async function handleInit(argv) {
96
115
  process.exit(1);
97
116
  }
98
117
  let keypair;
99
- try {
100
- const artifacts = await import("@veritasacta/artifacts");
101
- keypair = artifacts.generateKeypair();
102
- } catch {
118
+ {
103
119
  const { randomBytes } = await import("crypto");
104
120
  const { ed25519 } = await import("./ed25519-EDO4K4EP.mjs");
105
121
  const { bytesToHex } = await import("./utils-IDWBSHJU.mjs");
@@ -196,11 +212,10 @@ Add --enforce when ready to block policy violations.
196
212
  }
197
213
  async function handleDemo() {
198
214
  const { existsSync } = await import("fs");
199
- const { join, dirname } = await import("path");
200
- const { fileURLToPath } = await import("url");
201
- const __filename = fileURLToPath(import.meta.url);
202
- const __dirname = dirname(__filename);
203
- const demoServerPath = join(__dirname, "demo-server.js");
215
+ const { join, dirname, resolve } = await import("path");
216
+ const cliPath = resolve(process.argv[1] || "dist/cli.js");
217
+ const cliDir = dirname(cliPath);
218
+ const demoServerPath = join(cliDir, "demo-server.js");
204
219
  const configPath = join(process.cwd(), "protect-mcp.json");
205
220
  const hasConfig = existsSync(configPath);
206
221
  if (!hasConfig) {
@@ -385,6 +400,27 @@ ${bold("protect-mcp status")}
385
400
  } catch {
386
401
  }
387
402
  }
403
+ const keyPath = join(dir, "keys", "gateway.json");
404
+ if (existsSync(keyPath)) {
405
+ try {
406
+ const keyData = JSON.parse(readFileSync(keyPath, "utf-8"));
407
+ if (keyData.publicKey) {
408
+ const fingerprint = keyData.publicKey.slice(0, 16) + "...";
409
+ process.stdout.write(`
410
+ ${bold("\u{1F6E1}\uFE0F Passport identity:")}
411
+ `);
412
+ process.stdout.write(` Public key: ${fingerprint}
413
+ `);
414
+ if (keyData.kid) process.stdout.write(` Key ID: ${keyData.kid}
415
+ `);
416
+ process.stdout.write(` Issuer: ${keyData.issuer || "protect-mcp"}
417
+ `);
418
+ process.stdout.write(` Verify: ${dim("npx @veritasacta/verify <receipt.json>")}
419
+ `);
420
+ }
421
+ } catch {
422
+ }
423
+ }
388
424
  process.stdout.write(`
389
425
  Log file: ${dim(logPath)}
390
426
 
@@ -405,12 +441,502 @@ function red(s) {
405
441
  function yellow(s) {
406
442
  return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
407
443
  }
444
+ async function handleDigest(argv) {
445
+ const { readFileSync, existsSync } = await import("fs");
446
+ const { join } = await import("path");
447
+ let dir = process.cwd();
448
+ const dirIdx = argv.indexOf("--dir");
449
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
450
+ const today = argv.includes("--today");
451
+ const logPath = join(dir, ".protect-mcp-log.jsonl");
452
+ if (!existsSync(logPath)) {
453
+ process.stderr.write(`${bold("protect-mcp digest")}
454
+
455
+ No log file found. Run protect-mcp first.
456
+ `);
457
+ process.exit(0);
458
+ }
459
+ const raw = readFileSync(logPath, "utf-8");
460
+ const lines = raw.trim().split("\n").filter(Boolean);
461
+ let entries = [];
462
+ for (const line of lines) {
463
+ try {
464
+ entries.push(JSON.parse(line));
465
+ } catch {
466
+ }
467
+ }
468
+ if (today) {
469
+ const todayStart = /* @__PURE__ */ new Date();
470
+ todayStart.setHours(0, 0, 0, 0);
471
+ entries = entries.filter((e) => e.timestamp >= todayStart.getTime());
472
+ }
473
+ if (entries.length === 0) {
474
+ process.stdout.write(`
475
+ ${bold("\u{1F6E1}\uFE0F Agent Digest")}
476
+
477
+ No activity${today ? " today" : ""}.
478
+
479
+ `);
480
+ process.exit(0);
481
+ }
482
+ const allowed = entries.filter((e) => e.decision === "allow").length;
483
+ const denied = entries.filter((e) => e.decision === "deny").length;
484
+ const approvalRequired = entries.filter((e) => e.decision === "require_approval").length;
485
+ const toolUsage = /* @__PURE__ */ new Map();
486
+ for (const e of entries) {
487
+ toolUsage.set(e.tool, (toolUsage.get(e.tool) || 0) + 1);
488
+ }
489
+ const sortedTools = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
490
+ const currentTier = entries[entries.length - 1]?.tier || "unknown";
491
+ const firstTime = new Date(Math.min(...entries.map((e) => e.timestamp)));
492
+ const lastTime = new Date(Math.max(...entries.map((e) => e.timestamp)));
493
+ const durationMs = lastTime.getTime() - firstTime.getTime();
494
+ const durationStr = durationMs < 6e4 ? `${Math.round(durationMs / 1e3)}s` : durationMs < 36e5 ? `${Math.round(durationMs / 6e4)}m` : `${(durationMs / 36e5).toFixed(1)}h`;
495
+ process.stdout.write(`
496
+ ${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
497
+
498
+ `);
499
+ process.stdout.write(` \u{1F4CA} ${bold(String(entries.length))} actions | `);
500
+ process.stdout.write(`${green("\u2713 " + allowed)} allowed | `);
501
+ process.stdout.write(`${red("\u2717 " + denied)} blocked`);
502
+ if (approvalRequired > 0) process.stdout.write(` | ${yellow("\u23F3 " + approvalRequired)} awaiting approval`);
503
+ process.stdout.write(`
504
+ `);
505
+ process.stdout.write(` \u{1F3C5} Trust tier: ${bold(currentTier)} | \u23F1 Active: ${durationStr}
506
+
507
+ `);
508
+ process.stdout.write(` ${bold("Tools used:")}
509
+ `);
510
+ for (const [tool, count] of sortedTools.slice(0, 8)) {
511
+ process.stdout.write(` ${tool.padEnd(22)} ${count}x
512
+ `);
513
+ }
514
+ if (denied > 0) {
515
+ const deniedTools = entries.filter((e) => e.decision === "deny");
516
+ const deniedToolNames = [...new Set(deniedTools.map((e) => e.tool))];
517
+ process.stdout.write(`
518
+ ${bold(red("Blocked tools:"))}
519
+ `);
520
+ for (const tool of deniedToolNames) {
521
+ const reason = deniedTools.find((e) => e.tool === tool)?.reason_code || "policy";
522
+ process.stdout.write(` ${red("\u2717")} ${tool} (${reason})
523
+ `);
524
+ }
525
+ }
526
+ process.stdout.write(`
527
+ ${dim("Latest receipt: curl -s http://127.0.0.1:9876/receipts/latest | jq -r .receipt > receipt.json")}
528
+ `);
529
+ process.stdout.write(` ${dim("Verify: npx @veritasacta/verify receipt.json --key <public-key-hex>")}
530
+ `);
531
+ process.stdout.write(` ${dim("Export: npx protect-mcp bundle --output audit.json")}
532
+
533
+ `);
534
+ }
535
+ async function handleReceipts(argv) {
536
+ const { readFileSync, existsSync } = await import("fs");
537
+ const { join } = await import("path");
538
+ let dir = process.cwd();
539
+ const dirIdx = argv.indexOf("--dir");
540
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
541
+ const lastIdx = argv.indexOf("--last");
542
+ const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
543
+ const receiptsPath = join(dir, ".protect-mcp-receipts.jsonl");
544
+ if (!existsSync(receiptsPath)) {
545
+ process.stderr.write(`${bold("protect-mcp receipts")}
546
+
547
+ No signed receipt file found. Run protect-mcp with signing enabled first.
548
+ `);
549
+ process.exit(0);
550
+ }
551
+ const raw = readFileSync(receiptsPath, "utf-8");
552
+ const lines = raw.trim().split("\n").filter(Boolean);
553
+ const recent = lines.slice(-count);
554
+ process.stdout.write(`
555
+ ${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
556
+
557
+ `);
558
+ for (const line of recent) {
559
+ try {
560
+ const entry = JSON.parse(line);
561
+ const payload = entry.payload || {};
562
+ const time = typeof entry.issued_at === "string" ? new Date(entry.issued_at).toLocaleTimeString() : "unknown";
563
+ const decision = payload.decision || "unknown";
564
+ const icon = decision === "allow" ? green("\u2713") : decision === "require_approval" ? yellow("\u23F3") : red("\u2717");
565
+ process.stdout.write(` ${dim(time)} ${icon} ${String(payload.tool || "unknown").padEnd(22)} ${String(entry.type || "receipt").padEnd(18)} ${dim(String(payload.reason_code || "signed"))}
566
+ `);
567
+ } catch {
568
+ }
569
+ }
570
+ process.stdout.write(`
571
+ `);
572
+ }
573
+ async function handleBundle(argv) {
574
+ const { readFileSync, writeFileSync, existsSync } = await import("fs");
575
+ const { join } = await import("path");
576
+ const { createAuditBundle } = await import("./bundle-TXOTFJIJ.mjs");
577
+ let dir = process.cwd();
578
+ const dirIdx = argv.indexOf("--dir");
579
+ if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
580
+ const outputIdx = argv.indexOf("--output");
581
+ const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join(dir, "audit-bundle.json");
582
+ const receiptsPath = join(dir, ".protect-mcp-receipts.jsonl");
583
+ const keyPath = join(dir, "keys", "gateway.json");
584
+ if (!existsSync(receiptsPath)) {
585
+ process.stderr.write(`${bold("protect-mcp bundle")}
586
+
587
+ No signed receipt file found. Run protect-mcp with signing enabled first.
588
+ `);
589
+ process.exit(0);
590
+ }
591
+ if (!existsSync(keyPath)) {
592
+ process.stderr.write(`${bold("protect-mcp bundle")}
593
+
594
+ No key file found at ${keyPath}
595
+ `);
596
+ process.exit(1);
597
+ }
598
+ const receipts = readFileSync(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
599
+ const keyData = JSON.parse(readFileSync(keyPath, "utf-8"));
600
+ const bundle = createAuditBundle({
601
+ tenant: keyData.issuer || "protect-mcp",
602
+ receipts,
603
+ signingKeys: [{
604
+ kty: "OKP",
605
+ crv: "Ed25519",
606
+ kid: keyData.kid || "unknown",
607
+ x: Buffer.from(keyData.publicKey, "hex").toString("base64url"),
608
+ use: "sig"
609
+ }]
610
+ });
611
+ writeFileSync(outputPath, JSON.stringify(bundle, null, 2) + "\n");
612
+ process.stdout.write(`
613
+ ${bold("protect-mcp bundle")}
614
+
615
+ `);
616
+ process.stdout.write(` Receipts: ${receipts.length}
617
+ `);
618
+ process.stdout.write(` Output: ${outputPath}
619
+ `);
620
+ process.stdout.write(` Verify: npx @veritasacta/verify ${outputPath} --bundle
621
+
622
+ `);
623
+ }
624
+ async function handleQuickstart() {
625
+ const { mkdtempSync, writeFileSync, existsSync, mkdirSync, readFileSync } = await import("fs");
626
+ const { join } = await import("path");
627
+ const { tmpdir } = await import("os");
628
+ const dir = mkdtempSync(join(tmpdir(), "protect-mcp-quickstart-"));
629
+ process.stdout.write(`
630
+ ${bold("protect-mcp quickstart")}
631
+ `);
632
+ process.stdout.write(`${"\u2500".repeat(50)}
633
+
634
+ `);
635
+ process.stdout.write(` This will:
636
+ `);
637
+ process.stdout.write(` 1. Generate an Ed25519 signing keypair
638
+ `);
639
+ process.stdout.write(` 2. Create a shadow-mode policy
640
+ `);
641
+ process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
642
+ `);
643
+ process.stdout.write(` 4. Log signed receipts for every tool call
644
+
645
+ `);
646
+ process.stdout.write(` Working dir: ${dir}
647
+
648
+ `);
649
+ const keysDir = join(dir, "keys");
650
+ mkdirSync(keysDir, { recursive: true });
651
+ const { randomBytes } = await import("crypto");
652
+ let keypair;
653
+ try {
654
+ const { ed25519 } = await import("./ed25519-EDO4K4EP.mjs");
655
+ const { bytesToHex } = await import("./utils-IDWBSHJU.mjs");
656
+ const privateKey = randomBytes(32);
657
+ const publicKey = ed25519.getPublicKey(privateKey);
658
+ keypair = {
659
+ privateKey: bytesToHex(privateKey),
660
+ publicKey: bytesToHex(publicKey),
661
+ kid: `quickstart-${Date.now()}`
662
+ };
663
+ } catch {
664
+ keypair = {
665
+ privateKey: randomBytes(32).toString("hex"),
666
+ publicKey: randomBytes(32).toString("hex"),
667
+ kid: `quickstart-${Date.now()}`
668
+ };
669
+ }
670
+ writeFileSync(join(keysDir, "gateway.json"), JSON.stringify({
671
+ privateKey: keypair.privateKey,
672
+ publicKey: keypair.publicKey,
673
+ kid: keypair.kid,
674
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
675
+ }, null, 2) + "\n");
676
+ const configPath = join(dir, "protect-mcp.json");
677
+ const config = {
678
+ tools: {
679
+ "*": { rate_limit: "100/hour" },
680
+ "delete_file": { block: true }
681
+ },
682
+ default_tier: "unknown",
683
+ signing: {
684
+ key_path: join(keysDir, "gateway.json"),
685
+ issuer: "protect-mcp-quickstart",
686
+ enabled: true
687
+ }
688
+ };
689
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
690
+ process.stdout.write(` \u2713 Keypair generated (kid: ${keypair.kid})
691
+ `);
692
+ process.stdout.write(` \u2713 Policy created (shadow mode, all tools logged)
693
+ `);
694
+ process.stdout.write(` \u2713 Signing enabled (Ed25519)
695
+
696
+ `);
697
+ process.stdout.write(`${bold("Starting demo server...")}
698
+
699
+ `);
700
+ process.stdout.write(` Every tool call will produce a signed receipt.
701
+ `);
702
+ process.stdout.write(` Try it with Claude Desktop or any MCP client.
703
+
704
+ `);
705
+ process.stdout.write(` ${bold("To use in production:")}
706
+ `);
707
+ process.stdout.write(` 1. Copy ${configPath} to your project
708
+ `);
709
+ process.stdout.write(` 2. Edit tool policies to match your server
710
+ `);
711
+ process.stdout.write(` 3. Run: protect-mcp --policy protect-mcp.json -- node your-server.js
712
+
713
+ `);
714
+ process.stdout.write(`${"\u2500".repeat(50)}
715
+
716
+ `);
717
+ process.env.PROTECT_MCP_CONFIG = configPath;
718
+ await handleDemo();
719
+ }
720
+ async function handleTrace(argv) {
721
+ const receiptId = argv[0];
722
+ if (!receiptId) {
723
+ process.stderr.write("[PROTECT_MCP] Usage: protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]\n");
724
+ process.exit(1);
725
+ }
726
+ let endpoint = "https://evidence-indexer.tomjwxf.workers.dev";
727
+ let depth = 3;
728
+ for (let i = 1; i < argv.length; i++) {
729
+ if (argv[i] === "--endpoint" && argv[i + 1]) {
730
+ endpoint = argv[++i];
731
+ } else if (argv[i] === "--depth" && argv[i + 1]) {
732
+ depth = Math.min(10, Math.max(1, parseInt(argv[++i], 10) || 3));
733
+ }
734
+ }
735
+ process.stdout.write(`
736
+ ${bold("protect-mcp trace")}
737
+ `);
738
+ process.stdout.write(`${"\u2500".repeat(60)}
739
+
740
+ `);
741
+ process.stdout.write(` Root: ${receiptId}
742
+ `);
743
+ process.stdout.write(` Endpoint: ${endpoint}
744
+ `);
745
+ process.stdout.write(` Depth: ${depth}
746
+
747
+ `);
748
+ const url = `${endpoint}/evidence/graph/${encodeURIComponent(receiptId)}?depth=${depth}&direction=both&max=50`;
749
+ let graphData;
750
+ try {
751
+ const resp = await fetch(url);
752
+ if (!resp.ok) {
753
+ const body = await resp.text();
754
+ process.stderr.write(`[PROTECT_MCP] Error fetching graph: ${resp.status} ${body}
755
+ `);
756
+ process.exit(1);
757
+ }
758
+ graphData = await resp.json();
759
+ } catch (err) {
760
+ process.stderr.write(`[PROTECT_MCP] Could not reach evidence indexer at ${endpoint}
761
+ `);
762
+ process.stderr.write(`[PROTECT_MCP] Trying local receipts...
763
+
764
+ `);
765
+ await traceLocal(receiptId);
766
+ return;
767
+ }
768
+ if (!graphData.nodes || graphData.nodes.length === 0) {
769
+ process.stdout.write(` No receipts found for ${receiptId}
770
+
771
+ `);
772
+ return;
773
+ }
774
+ process.stdout.write(` ${bold("Evidence DAG")} (${graphData.node_count} nodes, ${graphData.edge_count} edges)
775
+
776
+ `);
777
+ const nodeMap = /* @__PURE__ */ new Map();
778
+ for (const node of graphData.nodes) {
779
+ nodeMap.set(node.receipt_id, node);
780
+ }
781
+ const childMap = /* @__PURE__ */ new Map();
782
+ for (const edge of graphData.edges) {
783
+ if (!childMap.has(edge.from)) childMap.set(edge.from, []);
784
+ childMap.get(edge.from).push({ to: edge.to, relation: edge.relation });
785
+ }
786
+ const rendered = /* @__PURE__ */ new Set();
787
+ function renderNode(id, prefix, isLast) {
788
+ const node = nodeMap.get(id);
789
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
790
+ const childPrefix = isLast ? " " : "\u2502 ";
791
+ const typeEmoji = getTypeEmoji(node?.receipt_type || "unknown");
792
+ const shortId = id.length > 16 ? id.slice(0, 12) + "\u2026" : id;
793
+ const time = node?.event_time ? new Date(node.event_time).toLocaleTimeString() : "?";
794
+ const type = node?.receipt_type?.replace("acta:", "") || "unknown";
795
+ process.stdout.write(`${prefix}${connector}${typeEmoji} ${bold(type)} ${dim(shortId)} ${dim(time)}
796
+ `);
797
+ if (rendered.has(id)) {
798
+ process.stdout.write(`${prefix}${childPrefix}${dim("(cycle \u2014 already rendered)")}
799
+ `);
800
+ return;
801
+ }
802
+ rendered.add(id);
803
+ const children = childMap.get(id) || [];
804
+ for (let i = 0; i < children.length; i++) {
805
+ const child = children[i];
806
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
807
+ process.stdout.write(`${prefix}${childPrefix}${edgeLabel}
808
+ `);
809
+ renderNode(child.to, prefix + childPrefix, i === children.length - 1);
810
+ }
811
+ }
812
+ const rootNode = nodeMap.get(receiptId);
813
+ if (rootNode) {
814
+ const typeEmoji = getTypeEmoji(rootNode.receipt_type);
815
+ const type = rootNode.receipt_type?.replace("acta:", "") || "unknown";
816
+ const time = rootNode.event_time ? new Date(rootNode.event_time).toLocaleTimeString() : "?";
817
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(receiptId.slice(0, 16) + "\u2026")} ${dim(time)} ${bold("(root)")}
818
+ `);
819
+ rendered.add(receiptId);
820
+ const children = childMap.get(receiptId) || [];
821
+ for (let i = 0; i < children.length; i++) {
822
+ const child = children[i];
823
+ const edgeLabel = dim(`\u2500\u2500[${child.relation}]\u2500\u2500\u25B6`);
824
+ process.stdout.write(` ${edgeLabel}
825
+ `);
826
+ renderNode(child.to, " ", i === children.length - 1);
827
+ }
828
+ const incomingEdges = (graphData.edges || []).filter((e) => e.to === receiptId);
829
+ if (incomingEdges.length > 0) {
830
+ process.stdout.write(`
831
+ ${bold("Incoming edges:")}
832
+ `);
833
+ for (const edge of incomingEdges) {
834
+ const fromNode = nodeMap.get(edge.from);
835
+ const fromType = fromNode?.receipt_type?.replace("acta:", "") || "unknown";
836
+ process.stdout.write(` \u25C0\u2500\u2500[${edge.relation}]\u2500\u2500 ${getTypeEmoji(fromNode?.receipt_type)} ${fromType} ${dim(edge.from.slice(0, 16) + "\u2026")}
837
+ `);
838
+ }
839
+ }
840
+ } else {
841
+ for (const node of graphData.nodes) {
842
+ const typeEmoji = getTypeEmoji(node.receipt_type);
843
+ const type = node.receipt_type?.replace("acta:", "") || "unknown";
844
+ process.stdout.write(` ${typeEmoji} ${bold(type)} ${dim(node.receipt_id.slice(0, 16) + "\u2026")}
845
+ `);
846
+ }
847
+ }
848
+ process.stdout.write(`
849
+ ${"\u2500".repeat(60)}
850
+ `);
851
+ process.stdout.write(` ${dim(`Fetched from ${endpoint}`)}
852
+
853
+ `);
854
+ }
855
+ async function traceLocal(receiptId) {
856
+ const { readFileSync, existsSync } = await import("fs");
857
+ const { join } = await import("path");
858
+ const dir = process.cwd();
859
+ const receiptsDir = join(dir, ".protect-mcp", "receipts");
860
+ if (!existsSync(receiptsDir)) {
861
+ process.stdout.write(` No local receipts found in ${receiptsDir}
862
+
863
+ `);
864
+ return;
865
+ }
866
+ const { readdirSync } = await import("fs");
867
+ const files = readdirSync(receiptsDir).filter((f) => f.endsWith(".json"));
868
+ process.stdout.write(` Scanning ${files.length} local receipts...
869
+
870
+ `);
871
+ const receipts = [];
872
+ for (const file of files) {
873
+ try {
874
+ const content = readFileSync(join(receiptsDir, file), "utf-8");
875
+ const receipt = JSON.parse(content);
876
+ receipts.push(receipt);
877
+ } catch {
878
+ }
879
+ }
880
+ const match = receipts.find(
881
+ (r) => r.signed_claims?.claims?.receipt_id === receiptId || r.receipt_id === receiptId
882
+ );
883
+ if (match) {
884
+ const claims = match.signed_claims?.claims || match;
885
+ process.stdout.write(` Found: ${getTypeEmoji(claims.receipt_type)} ${bold(claims.receipt_type?.replace("acta:", "") || "unknown")}
886
+ `);
887
+ process.stdout.write(` Event: ${claims.event_id || "?"}
888
+ `);
889
+ process.stdout.write(` Issuer: ${claims.issuer_id || "?"}
890
+ `);
891
+ process.stdout.write(` Time: ${claims.event_time || "?"}
892
+ `);
893
+ if (claims.edges && claims.edges.length > 0) {
894
+ process.stdout.write(`
895
+ ${bold("Edges:")}
896
+ `);
897
+ for (const edge of claims.edges) {
898
+ process.stdout.write(` \u2500\u2500[${edge.relation}]\u2500\u2500\u25B6 ${dim(edge.receipt_id?.slice(0, 16) + "\u2026")}
899
+ `);
900
+ }
901
+ }
902
+ } else {
903
+ process.stdout.write(` Receipt ${receiptId} not found locally.
904
+ `);
905
+ }
906
+ process.stdout.write("\n");
907
+ }
908
+ function getTypeEmoji(type) {
909
+ switch (type) {
910
+ case "acta:observation":
911
+ return "\u{1F441} ";
912
+ case "acta:policy-load":
913
+ return "\u{1F4CB}";
914
+ case "acta:approval":
915
+ return "\u2705";
916
+ case "acta:decision":
917
+ return "\u2696\uFE0F ";
918
+ case "acta:execution":
919
+ return "\u26A1";
920
+ case "acta:outcome":
921
+ return "\u{1F4E6}";
922
+ case "acta:delegation":
923
+ return "\u{1F91D}";
924
+ case "acta:capability-attestation":
925
+ return "\u{1F3C5}";
926
+ default:
927
+ return "\u{1F4C4}";
928
+ }
929
+ }
408
930
  async function main() {
409
931
  const args = process.argv.slice(2);
410
932
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
411
933
  printHelp();
412
934
  process.exit(0);
413
935
  }
936
+ if (args[0] === "quickstart") {
937
+ await handleQuickstart();
938
+ return;
939
+ }
414
940
  if (args[0] === "init") {
415
941
  await handleInit(args.slice(1));
416
942
  process.exit(0);
@@ -423,6 +949,30 @@ async function main() {
423
949
  await handleStatus(args.slice(1));
424
950
  process.exit(0);
425
951
  }
952
+ if (args[0] === "digest") {
953
+ await handleDigest(args.slice(1));
954
+ process.exit(0);
955
+ }
956
+ if (args[0] === "receipts") {
957
+ await handleReceipts(args.slice(1));
958
+ process.exit(0);
959
+ }
960
+ if (args[0] === "bundle") {
961
+ await handleBundle(args.slice(1));
962
+ process.exit(0);
963
+ }
964
+ if (args[0] === "trace") {
965
+ await handleTrace(args.slice(1));
966
+ process.exit(0);
967
+ }
968
+ if (args[0] === "simulate") {
969
+ await handleSimulate(args.slice(1));
970
+ process.exit(0);
971
+ }
972
+ if (args[0] === "report") {
973
+ await handleReport(args.slice(1));
974
+ process.exit(0);
975
+ }
426
976
  const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
427
977
  let policy = null;
428
978
  let policyDigest = "none";
@@ -473,6 +1023,85 @@ async function main() {
473
1023
  const gateway = new ProtectGateway(config);
474
1024
  await gateway.start();
475
1025
  }
1026
+ async function handleSimulate(args) {
1027
+ let policyPath = "";
1028
+ let logPath = ".protect-mcp-log.jsonl";
1029
+ let tier = "unknown";
1030
+ let jsonOutput = false;
1031
+ for (let i = 0; i < args.length; i++) {
1032
+ if (args[i] === "--policy" && args[i + 1]) {
1033
+ policyPath = args[++i];
1034
+ } else if (args[i] === "--log" && args[i + 1]) {
1035
+ logPath = args[++i];
1036
+ } else if (args[i] === "--tier" && args[i + 1]) {
1037
+ tier = args[++i];
1038
+ } else if (args[i] === "--json") {
1039
+ jsonOutput = true;
1040
+ }
1041
+ }
1042
+ if (!policyPath) {
1043
+ process.stderr.write("Usage: protect-mcp simulate --policy <path> [--log <path>] [--tier <tier>] [--json]\n");
1044
+ process.exit(1);
1045
+ }
1046
+ const { existsSync } = await import("fs");
1047
+ if (!existsSync(logPath)) {
1048
+ process.stderr.write(`Log file not found: ${logPath}
1049
+ `);
1050
+ process.stderr.write("Run protect-mcp in shadow mode first to generate a log file.\n");
1051
+ process.exit(1);
1052
+ }
1053
+ const { policy } = loadPolicy(policyPath);
1054
+ const entries = parseLogFile(logPath);
1055
+ if (entries.length === 0) {
1056
+ process.stderr.write("No tool call entries found in log file.\n");
1057
+ process.exit(1);
1058
+ }
1059
+ const summary = simulate(entries, policy, tier);
1060
+ summary.policy_file = policyPath;
1061
+ summary.log_file = logPath;
1062
+ if (jsonOutput) {
1063
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
1064
+ } else {
1065
+ process.stdout.write(formatSimulation(summary) + "\n");
1066
+ }
1067
+ }
1068
+ async function handleReport(args) {
1069
+ let period = 30;
1070
+ let format = "json";
1071
+ let outputPath = "";
1072
+ let dir = process.cwd();
1073
+ for (let i = 0; i < args.length; i++) {
1074
+ if (args[i] === "--period" && args[i + 1]) {
1075
+ const match = args[++i].match(/^(\d+)d$/);
1076
+ if (match) period = parseInt(match[1], 10);
1077
+ } else if (args[i] === "--format" && args[i + 1]) {
1078
+ format = args[++i];
1079
+ } else if (args[i] === "--output" && args[i + 1]) {
1080
+ outputPath = args[++i];
1081
+ } else if (args[i] === "--dir" && args[i + 1]) {
1082
+ dir = args[++i];
1083
+ }
1084
+ }
1085
+ const { generateReport, formatReportMarkdown } = await import("./report-ENQ3KUI2.mjs");
1086
+ const { join } = await import("path");
1087
+ const logPath = join(dir, ".protect-mcp-log.jsonl");
1088
+ const receiptPath = join(dir, ".protect-mcp-receipts.jsonl");
1089
+ const report = generateReport(logPath, receiptPath, period);
1090
+ let output;
1091
+ if (format === "md") {
1092
+ output = formatReportMarkdown(report);
1093
+ } else {
1094
+ output = JSON.stringify(report, null, 2);
1095
+ }
1096
+ if (outputPath) {
1097
+ const { writeFileSync } = await import("fs");
1098
+ writeFileSync(outputPath, output, "utf-8");
1099
+ process.stderr.write(`Report written to ${outputPath}
1100
+ `);
1101
+ } else {
1102
+ process.stdout.write(output + "\n");
1103
+ }
1104
+ }
476
1105
  main().catch((err) => {
477
1106
  process.stderr.write(`[PROTECT_MCP] Fatal error: ${err instanceof Error ? err.message : err}
478
1107
  `);