@rafter-security/cli 0.4.2 → 0.5.3

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.
Files changed (36) hide show
  1. package/README.md +101 -1
  2. package/dist/commands/agent/audit-skill.js +6 -0
  3. package/dist/commands/agent/audit.js +15 -3
  4. package/dist/commands/agent/exec.js +9 -8
  5. package/dist/commands/agent/index.js +4 -0
  6. package/dist/commands/agent/init.js +132 -47
  7. package/dist/commands/agent/install-hook.js +2 -1
  8. package/dist/commands/agent/scan.js +180 -103
  9. package/dist/commands/agent/status.js +115 -0
  10. package/dist/commands/agent/verify.js +117 -0
  11. package/dist/commands/ci/index.js +8 -0
  12. package/dist/commands/ci/init.js +191 -0
  13. package/dist/commands/completion.js +170 -0
  14. package/dist/commands/hook/index.js +10 -0
  15. package/dist/commands/hook/posttool.js +73 -0
  16. package/dist/commands/hook/pretool.js +122 -0
  17. package/dist/commands/mcp/index.js +8 -0
  18. package/dist/commands/mcp/server.js +205 -0
  19. package/dist/commands/policy/export.js +81 -0
  20. package/dist/commands/policy/index.js +8 -0
  21. package/dist/core/audit-logger.js +2 -33
  22. package/dist/core/command-interceptor.js +6 -50
  23. package/dist/core/config-defaults.js +4 -15
  24. package/dist/core/config-manager.js +68 -0
  25. package/dist/core/custom-patterns.js +157 -0
  26. package/dist/core/policy-loader.js +167 -0
  27. package/dist/core/risk-rules.js +72 -0
  28. package/dist/index.js +26 -2
  29. package/dist/scanners/gitleaks.js +7 -6
  30. package/dist/scanners/regex-scanner.js +28 -12
  31. package/dist/utils/binary-manager.js +100 -7
  32. package/dist/utils/formatter.js +52 -0
  33. package/dist/utils/skill-manager.js +22 -9
  34. package/package.json +7 -3
  35. package/resources/pre-commit-hook.sh +45 -0
  36. package/resources/rafter-security-skill.md +323 -0
@@ -0,0 +1,191 @@
1
+ import { Command } from "commander";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fmt } from "../../utils/formatter.js";
5
+ export function createCiInitCommand() {
6
+ return new Command("init")
7
+ .description("Generate CI/CD pipeline config for secret scanning")
8
+ .option("--platform <platform>", "CI platform: github, gitlab, circleci (default: auto-detect)")
9
+ .option("--output <path>", "Output file path (default: platform-specific)")
10
+ .option("--with-backend", "Include backend security audit job (requires RAFTER_API_KEY)")
11
+ .action((opts) => {
12
+ const platform = opts.platform || detectPlatform();
13
+ if (!platform) {
14
+ console.error(fmt.error("Could not auto-detect CI platform."));
15
+ console.error("Specify one with --platform github|gitlab|circleci");
16
+ process.exit(1);
17
+ }
18
+ const validPlatforms = ["github", "gitlab", "circleci"];
19
+ if (!validPlatforms.includes(platform)) {
20
+ console.error(fmt.error(`Unknown platform: ${platform}`));
21
+ console.error(`Valid options: ${validPlatforms.join(", ")}`);
22
+ process.exit(1);
23
+ }
24
+ const { content, defaultPath } = generateTemplate(platform, !!opts.withBackend);
25
+ const outputPath = opts.output || defaultPath;
26
+ const outputDir = path.dirname(outputPath);
27
+ if (!fs.existsSync(outputDir)) {
28
+ fs.mkdirSync(outputDir, { recursive: true });
29
+ }
30
+ fs.writeFileSync(outputPath, content, "utf-8");
31
+ console.log(fmt.success(`Generated ${platform} CI config at ${outputPath}`));
32
+ console.log();
33
+ console.log("Next steps:");
34
+ console.log(` 1. Review the generated file: ${outputPath}`);
35
+ if (opts.withBackend) {
36
+ if (platform === "github") {
37
+ console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
38
+ }
39
+ else if (platform === "gitlab") {
40
+ console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
41
+ }
42
+ else {
43
+ console.log(" 2. Add RAFTER_API_KEY to project environment variables");
44
+ }
45
+ }
46
+ console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
47
+ console.log();
48
+ });
49
+ }
50
+ function detectPlatform() {
51
+ if (fs.existsSync(".github"))
52
+ return "github";
53
+ if (fs.existsSync(".gitlab-ci.yml"))
54
+ return "gitlab";
55
+ if (fs.existsSync(".circleci"))
56
+ return "circleci";
57
+ return null;
58
+ }
59
+ function generateTemplate(platform, withBackend) {
60
+ switch (platform) {
61
+ case "github":
62
+ return { content: githubTemplate(withBackend), defaultPath: ".github/workflows/rafter-security.yml" };
63
+ case "gitlab":
64
+ return { content: gitlabTemplate(withBackend), defaultPath: ".gitlab-ci-rafter.yml" };
65
+ case "circleci":
66
+ return { content: circleciTemplate(withBackend), defaultPath: ".circleci/rafter-security.yml" };
67
+ }
68
+ }
69
+ function githubTemplate(withBackend) {
70
+ let yaml = `# Generated by: rafter ci init
71
+ name: Rafter Security
72
+
73
+ on:
74
+ push:
75
+ branches: [main]
76
+ pull_request:
77
+ branches: [main]
78
+
79
+ permissions:
80
+ contents: read
81
+
82
+ jobs:
83
+ secret-scan:
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - uses: actions/checkout@v4
87
+
88
+ - name: Install Rafter CLI
89
+ run: npm install -g @rafter-security/cli
90
+
91
+ - name: Scan for secrets
92
+ run: rafter agent scan . --quiet
93
+ `;
94
+ if (withBackend) {
95
+ yaml += `
96
+ security-audit:
97
+ runs-on: ubuntu-latest
98
+ needs: secret-scan
99
+ steps:
100
+ - uses: actions/checkout@v4
101
+
102
+ - name: Install Rafter CLI
103
+ run: npm install -g @rafter-security/cli
104
+
105
+ - name: Run security audit
106
+ env:
107
+ RAFTER_API_KEY: \${{ secrets.RAFTER_API_KEY }}
108
+ run: rafter run --format json --quiet
109
+ `;
110
+ }
111
+ return yaml;
112
+ }
113
+ function gitlabTemplate(withBackend) {
114
+ let yaml = `# Generated by: rafter ci init
115
+ stages:
116
+ - security
117
+
118
+ secret-scan:
119
+ stage: security
120
+ image: node:20
121
+ script:
122
+ - npm install -g @rafter-security/cli
123
+ - rafter agent scan . --quiet
124
+ rules:
125
+ - if: $CI_PIPELINE_SOURCE == "push"
126
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
127
+ `;
128
+ if (withBackend) {
129
+ yaml += `
130
+ security-audit:
131
+ stage: security
132
+ image: node:20
133
+ needs: [secret-scan]
134
+ script:
135
+ - npm install -g @rafter-security/cli
136
+ - rafter run --format json --quiet
137
+ variables:
138
+ RAFTER_API_KEY: $RAFTER_API_KEY
139
+ rules:
140
+ - if: $CI_PIPELINE_SOURCE == "push"
141
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
142
+ `;
143
+ }
144
+ return yaml;
145
+ }
146
+ function circleciTemplate(withBackend) {
147
+ let yaml = `# Generated by: rafter ci init
148
+ version: 2.1
149
+
150
+ jobs:
151
+ secret-scan:
152
+ docker:
153
+ - image: cimg/node:20.0
154
+ steps:
155
+ - checkout
156
+ - run:
157
+ name: Install Rafter CLI
158
+ command: npm install -g @rafter-security/cli
159
+ - run:
160
+ name: Scan for secrets
161
+ command: rafter agent scan . --quiet
162
+ `;
163
+ if (withBackend) {
164
+ yaml += `
165
+ security-audit:
166
+ docker:
167
+ - image: cimg/node:20.0
168
+ steps:
169
+ - checkout
170
+ - run:
171
+ name: Install Rafter CLI
172
+ command: npm install -g @rafter-security/cli
173
+ - run:
174
+ name: Run security audit
175
+ command: rafter run --format json --quiet
176
+ `;
177
+ }
178
+ yaml += `
179
+ workflows:
180
+ security:
181
+ jobs:
182
+ - secret-scan`;
183
+ if (withBackend) {
184
+ yaml += `
185
+ - security-audit:
186
+ requires:
187
+ - secret-scan`;
188
+ }
189
+ yaml += "\n";
190
+ return yaml;
191
+ }
@@ -0,0 +1,170 @@
1
+ import { Command } from "commander";
2
+ const BASH_COMPLETION = `
3
+ # rafter bash completion
4
+ # Add to ~/.bashrc: eval "$(rafter completion bash)"
5
+ _rafter_completion() {
6
+ local cur prev words
7
+ COMPREPLY=()
8
+ cur="\${COMP_WORDS[COMP_CWORD]}"
9
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
10
+ words="\${COMP_WORDS[*]}"
11
+
12
+ local top_cmds="run scan get usage agent ci hook mcp policy completion --help --version"
13
+ local agent_cmds="init scan exec config audit audit-skill install-hook verify status"
14
+ local ci_cmds="init"
15
+ local hook_cmds="pretool posttool"
16
+ local policy_cmds="export"
17
+
18
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
19
+ COMPREPLY=( \$(compgen -W "\${top_cmds}" -- "\${cur}") )
20
+ return 0
21
+ fi
22
+
23
+ case "\${COMP_WORDS[1]}" in
24
+ agent)
25
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
26
+ COMPREPLY=( \$(compgen -W "\${agent_cmds}" -- "\${cur}") )
27
+ fi
28
+ case "\${COMP_WORDS[2]}" in
29
+ scan) COMPREPLY=( \$(compgen -W "--quiet --json --format --staged --diff --engine" -- "\${cur}") ) ;;
30
+ init) COMPREPLY=( \$(compgen -W "--risk-level --skip-gitleaks --skip-openclaw --skip-claude-code --force" -- "\${cur}") ) ;;
31
+ verify) COMPREPLY=() ;;
32
+ status) COMPREPLY=() ;;
33
+ audit-skill) COMPREPLY=( \$(compgen -W "--skip-openclaw --json" -- "\${cur}") ) ;;
34
+ install-hook) COMPREPLY=( \$(compgen -W "--global" -- "\${cur}") ) ;;
35
+ config) COMPREPLY=( \$(compgen -W "show get set" -- "\${cur}") ) ;;
36
+ audit) COMPREPLY=( \$(compgen -W "--last --event --agent --since" -- "\${cur}") ) ;;
37
+ esac
38
+ ;;
39
+ hook)
40
+ COMPREPLY=( \$(compgen -W "\${hook_cmds}" -- "\${cur}") )
41
+ ;;
42
+ ci)
43
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
44
+ COMPREPLY=( \$(compgen -W "\${ci_cmds}" -- "\${cur}") )
45
+ fi
46
+ ;;
47
+ policy)
48
+ COMPREPLY=( \$(compgen -W "\${policy_cmds}" -- "\${cur}") )
49
+ ;;
50
+ run|scan)
51
+ COMPREPLY=( \$(compgen -W "--api-key --format --quiet" -- "\${cur}") )
52
+ ;;
53
+ completion)
54
+ COMPREPLY=( \$(compgen -W "bash zsh fish" -- "\${cur}") )
55
+ ;;
56
+ esac
57
+ }
58
+ complete -F _rafter_completion rafter
59
+ `;
60
+ const ZSH_COMPLETION = `
61
+ # rafter zsh completion
62
+ # Add to ~/.zshrc: eval "$(rafter completion zsh)"
63
+ #compdef rafter
64
+
65
+ _rafter() {
66
+ local state
67
+ typeset -A opt_args
68
+
69
+ _arguments \\
70
+ '1: :->cmd' \\
71
+ '*: :->args'
72
+
73
+ case \$state in
74
+ cmd)
75
+ _values 'command' \\
76
+ 'run[Run a security scan via backend]' \\
77
+ 'scan[Alias for run]' \\
78
+ 'agent[Agent security features]' \\
79
+ 'ci[CI/CD integration]' \\
80
+ 'hook[Hook handlers]' \\
81
+ 'mcp[MCP server]' \\
82
+ 'policy[Policy management]' \\
83
+ 'completion[Shell completion scripts]'
84
+ ;;
85
+ args)
86
+ case \$words[2] in
87
+ agent)
88
+ _values 'subcommand' \\
89
+ 'init[Initialize agent security]' \\
90
+ 'scan[Scan for secrets]' \\
91
+ 'exec[Execute command with security validation]' \\
92
+ 'config[Manage configuration]' \\
93
+ 'audit[View audit logs]' \\
94
+ 'audit-skill[Audit a skill file]' \\
95
+ 'install-hook[Install git pre-commit hook]' \\
96
+ 'verify[Health check]' \\
97
+ 'status[Status dashboard]'
98
+ ;;
99
+ hook)
100
+ _values 'subcommand' 'pretool[PreToolUse handler]' 'posttool[PostToolUse handler]'
101
+ ;;
102
+ completion)
103
+ _values 'shell' 'bash' 'zsh' 'fish'
104
+ ;;
105
+ esac
106
+ ;;
107
+ esac
108
+ }
109
+
110
+ _rafter
111
+ `;
112
+ const FISH_COMPLETION = `
113
+ # rafter fish completion
114
+ # Save to ~/.config/fish/completions/rafter.fish
115
+ # Or: rafter completion fish > ~/.config/fish/completions/rafter.fish
116
+
117
+ complete -c rafter -f
118
+ complete -c rafter -n '__fish_use_subcommand' -a 'run' -d 'Run a security scan via backend'
119
+ complete -c rafter -n '__fish_use_subcommand' -a 'agent' -d 'Agent security features'
120
+ complete -c rafter -n '__fish_use_subcommand' -a 'ci' -d 'CI/CD integration'
121
+ complete -c rafter -n '__fish_use_subcommand' -a 'hook' -d 'Hook handlers'
122
+ complete -c rafter -n '__fish_use_subcommand' -a 'mcp' -d 'MCP server'
123
+ complete -c rafter -n '__fish_use_subcommand' -a 'completion' -d 'Shell completion scripts'
124
+
125
+ # agent subcommands
126
+ complete -c rafter -n '__fish_seen_subcommand_from agent' -a 'init scan exec config audit audit-skill install-hook verify status'
127
+ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l quiet -s q -d 'Only output if secrets found'
128
+ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l json -d 'JSON output'
129
+ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l format -d 'Output format: text, json, sarif'
130
+ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l staged -d 'Scan staged files'
131
+ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l engine -d 'Engine: gitleaks, patterns, auto'
132
+
133
+ # hook subcommands
134
+ complete -c rafter -n '__fish_seen_subcommand_from hook' -a 'pretool posttool'
135
+
136
+ # completion subcommands
137
+ complete -c rafter -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
138
+ `;
139
+ export function createCompletionCommand() {
140
+ return new Command("completion")
141
+ .description("Generate shell completion scripts")
142
+ .argument("<shell>", "Shell type: bash, zsh, or fish")
143
+ .addHelpText("after", `
144
+ Examples:
145
+ # bash — add to ~/.bashrc
146
+ eval "$(rafter completion bash)"
147
+
148
+ # zsh — add to ~/.zshrc
149
+ eval "$(rafter completion zsh)"
150
+
151
+ # fish — save to completions dir
152
+ rafter completion fish > ~/.config/fish/completions/rafter.fish
153
+ `)
154
+ .action((shell) => {
155
+ switch (shell.toLowerCase()) {
156
+ case "bash":
157
+ process.stdout.write(BASH_COMPLETION.trimStart());
158
+ break;
159
+ case "zsh":
160
+ process.stdout.write(ZSH_COMPLETION.trimStart());
161
+ break;
162
+ case "fish":
163
+ process.stdout.write(FISH_COMPLETION.trimStart());
164
+ break;
165
+ default:
166
+ console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
167
+ process.exit(1);
168
+ }
169
+ });
170
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ import { createHookPretoolCommand } from "./pretool.js";
3
+ import { createHookPosttoolCommand } from "./posttool.js";
4
+ export function createHookCommand() {
5
+ const hook = new Command("hook")
6
+ .description("Hook handlers for agent platform integration");
7
+ hook.addCommand(createHookPretoolCommand());
8
+ hook.addCommand(createHookPosttoolCommand());
9
+ return hook;
10
+ }
@@ -0,0 +1,73 @@
1
+ import { Command } from "commander";
2
+ import { RegexScanner } from "../../scanners/regex-scanner.js";
3
+ import { AuditLogger } from "../../core/audit-logger.js";
4
+ export function createHookPosttoolCommand() {
5
+ return new Command("posttool")
6
+ .description("PostToolUse hook handler (reads stdin, redacts secrets in output, writes JSON to stdout)")
7
+ .action(async () => {
8
+ const input = await readStdin();
9
+ let payload;
10
+ try {
11
+ payload = JSON.parse(input);
12
+ }
13
+ catch {
14
+ writeOutput({ action: "continue" });
15
+ return;
16
+ }
17
+ const output = evaluateToolResponse(payload);
18
+ writeOutput(output);
19
+ });
20
+ }
21
+ function evaluateToolResponse(payload) {
22
+ const { tool_response } = payload;
23
+ // No response body — pass through
24
+ if (!tool_response) {
25
+ return { action: "continue" };
26
+ }
27
+ const scanner = new RegexScanner();
28
+ let modified = false;
29
+ const redacted = { ...tool_response };
30
+ // Scan and redact output
31
+ if (typeof tool_response.output === "string" && tool_response.output) {
32
+ if (scanner.hasSecrets(tool_response.output)) {
33
+ redacted.output = scanner.redact(tool_response.output);
34
+ modified = true;
35
+ }
36
+ }
37
+ // Scan and redact content (used by some tools)
38
+ if (typeof tool_response.content === "string" && tool_response.content) {
39
+ if (scanner.hasSecrets(tool_response.content)) {
40
+ redacted.content = scanner.redact(tool_response.content);
41
+ modified = true;
42
+ }
43
+ }
44
+ if (modified) {
45
+ const audit = new AuditLogger();
46
+ const matchCount = countMatches(scanner, tool_response);
47
+ audit.logContentSanitized(`${payload.tool_name} tool response`, matchCount);
48
+ return { action: "modify", tool_response: redacted };
49
+ }
50
+ return { action: "continue" };
51
+ }
52
+ function countMatches(scanner, tool_response) {
53
+ let count = 0;
54
+ if (typeof tool_response?.output === "string" && tool_response.output) {
55
+ count += scanner.scanText(tool_response.output).length;
56
+ }
57
+ if (typeof tool_response?.content === "string" && tool_response.content) {
58
+ count += scanner.scanText(tool_response.content).length;
59
+ }
60
+ return count;
61
+ }
62
+ function readStdin() {
63
+ return new Promise((resolve) => {
64
+ let data = "";
65
+ process.stdin.setEncoding("utf-8");
66
+ process.stdin.on("data", (chunk) => { data += chunk; });
67
+ process.stdin.on("end", () => { resolve(data); });
68
+ process.stdin.resume();
69
+ });
70
+ }
71
+ function writeOutput(output) {
72
+ process.stdout.write(JSON.stringify(output) + "\n");
73
+ }
@@ -0,0 +1,122 @@
1
+ import { Command } from "commander";
2
+ import { CommandInterceptor } from "../../core/command-interceptor.js";
3
+ import { RegexScanner } from "../../scanners/regex-scanner.js";
4
+ import { AuditLogger } from "../../core/audit-logger.js";
5
+ import { execSync } from "child_process";
6
+ export function createHookPretoolCommand() {
7
+ return new Command("pretool")
8
+ .description("PreToolUse hook handler (reads stdin, writes JSON decision to stdout)")
9
+ .action(async () => {
10
+ const input = await readStdin();
11
+ let payload;
12
+ try {
13
+ payload = JSON.parse(input);
14
+ }
15
+ catch {
16
+ // Can't parse → fail open
17
+ writeDecision({ decision: "allow" });
18
+ return;
19
+ }
20
+ const decision = evaluateToolCall(payload);
21
+ writeDecision(decision);
22
+ });
23
+ }
24
+ function evaluateToolCall(payload) {
25
+ const { tool_name, tool_input } = payload;
26
+ if (tool_name === "Bash") {
27
+ return evaluateBash(tool_input.command || "");
28
+ }
29
+ if (tool_name === "Write" || tool_name === "Edit") {
30
+ return evaluateWrite(tool_input);
31
+ }
32
+ return { decision: "allow" };
33
+ }
34
+ function evaluateBash(command) {
35
+ const interceptor = new CommandInterceptor();
36
+ const audit = new AuditLogger();
37
+ const evaluation = interceptor.evaluate(command);
38
+ // Blocked — hard deny
39
+ if (!evaluation.allowed && !evaluation.requiresApproval) {
40
+ audit.logCommandIntercepted(command, false, "blocked", evaluation.reason);
41
+ return {
42
+ decision: "deny",
43
+ reason: `Blocked by Rafter policy: ${evaluation.reason}`,
44
+ };
45
+ }
46
+ // Requires approval — deny (agent can't provide interactive approval)
47
+ if (evaluation.requiresApproval) {
48
+ audit.logCommandIntercepted(command, false, "blocked", evaluation.reason);
49
+ return {
50
+ decision: "deny",
51
+ reason: `Rafter policy requires approval: ${evaluation.reason}`,
52
+ };
53
+ }
54
+ // Git commit/push — scan staged files for secrets
55
+ const trimmed = command.trim();
56
+ if (trimmed.startsWith("git commit") || trimmed.startsWith("git push")) {
57
+ const scanResult = scanStagedFiles();
58
+ if (scanResult.secretsFound) {
59
+ audit.logSecretDetected("staged files", `${scanResult.count} secret(s)`, "blocked");
60
+ return {
61
+ decision: "deny",
62
+ reason: `${scanResult.count} secret(s) detected in ${scanResult.files} staged file(s). Run 'rafter agent scan --staged' for details.`,
63
+ };
64
+ }
65
+ }
66
+ audit.logCommandIntercepted(command, true, "allowed");
67
+ return { decision: "allow" };
68
+ }
69
+ function evaluateWrite(toolInput) {
70
+ // Write uses "content", Edit uses "new_string"
71
+ const content = toolInput.content || toolInput.new_string || "";
72
+ if (!content) {
73
+ return { decision: "allow" };
74
+ }
75
+ const scanner = new RegexScanner();
76
+ if (scanner.hasSecrets(content)) {
77
+ const matches = scanner.scanText(content);
78
+ const names = [...new Set(matches.map(m => m.pattern.name))];
79
+ const audit = new AuditLogger();
80
+ audit.logSecretDetected(toolInput.file_path || "file content", names.join(", "), "blocked");
81
+ return {
82
+ decision: "deny",
83
+ reason: `Secret detected in file content: ${names.join(", ")}`,
84
+ };
85
+ }
86
+ return { decision: "allow" };
87
+ }
88
+ function scanStagedFiles() {
89
+ try {
90
+ const stagedOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
91
+ encoding: "utf-8",
92
+ stdio: ["pipe", "pipe", "ignore"],
93
+ }).trim();
94
+ if (!stagedOutput) {
95
+ return { secretsFound: false, count: 0, files: 0 };
96
+ }
97
+ const stagedFiles = stagedOutput.split("\n").filter(f => f.trim());
98
+ const scanner = new RegexScanner();
99
+ const results = scanner.scanFiles(stagedFiles);
100
+ const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
101
+ return {
102
+ secretsFound: results.length > 0,
103
+ count: totalMatches,
104
+ files: results.length,
105
+ };
106
+ }
107
+ catch {
108
+ return { secretsFound: false, count: 0, files: 0 };
109
+ }
110
+ }
111
+ function readStdin() {
112
+ return new Promise((resolve) => {
113
+ let data = "";
114
+ process.stdin.setEncoding("utf-8");
115
+ process.stdin.on("data", (chunk) => { data += chunk; });
116
+ process.stdin.on("end", () => { resolve(data); });
117
+ process.stdin.resume();
118
+ });
119
+ }
120
+ function writeDecision(decision) {
121
+ process.stdout.write(JSON.stringify(decision) + "\n");
122
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ import { createMcpServeCommand } from "./server.js";
3
+ export function createMcpCommand() {
4
+ const mcp = new Command("mcp")
5
+ .description("MCP server for cross-platform security tools");
6
+ mcp.addCommand(createMcpServeCommand());
7
+ return mcp;
8
+ }