@rafter-security/cli 0.6.5 → 0.7.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.
@@ -1,6 +1,7 @@
1
1
  import axios from "axios";
2
2
  import ora from "ora";
3
3
  import { API, writePayload, EXIT_GENERAL_ERROR, EXIT_SCAN_NOT_FOUND } from "../../utils/api.js";
4
+ import { fmt as output } from "../../utils/formatter.js";
4
5
  export async function handleScanStatus(scan_id, headers, fmt, quiet) {
5
6
  // First poll
6
7
  let poll;
@@ -9,10 +10,10 @@ export async function handleScanStatus(scan_id, headers, fmt, quiet) {
9
10
  }
10
11
  catch (e) {
11
12
  if (e.response?.status === 404) {
12
- console.error(`Scan '${scan_id}' not found`);
13
+ console.error(output.error(`Scan '${scan_id}' not found`));
13
14
  return EXIT_SCAN_NOT_FOUND;
14
15
  }
15
- console.error(`Error: ${e.response?.data || e.message}`);
16
+ console.error(output.error(`${e.response?.data || e.message}`));
16
17
  return EXIT_GENERAL_ERROR;
17
18
  }
18
19
  let status = poll.data.status;
@@ -54,7 +54,7 @@ function buildTopics() {
54
54
  render: () => loadSkill("rafter-agent-security"),
55
55
  },
56
56
  scanning: {
57
- description: "Remote SAST/SCA code analysis via backend API",
57
+ description: "Remote SAST/SCA code analysis via Rafter API",
58
58
  render: () => loadSkill("rafter"),
59
59
  },
60
60
  commands: {
@@ -78,7 +78,7 @@ function buildTopics() {
78
78
  return [
79
79
  "# Rafter Command Reference",
80
80
  "",
81
- "## Backend (Remote Code Analysis)",
81
+ "## Remote Code Analysis",
82
82
  "",
83
83
  backCmds,
84
84
  "",
@@ -128,6 +128,43 @@ function buildTopics() {
128
128
  description: "Setup instructions for unsupported / generic agents",
129
129
  render: () => renderPlatformSetup("generic"),
130
130
  },
131
+ pricing: {
132
+ description: "What's free, what's paid, and the philosophy behind it",
133
+ render: () => [
134
+ "# Rafter Pricing",
135
+ "",
136
+ "**Free forever for individuals and open source. No account required. No telemetry.**",
137
+ "",
138
+ "## What's Free",
139
+ "",
140
+ "All local agent security features are free with no limits:",
141
+ "",
142
+ "- Secret scanning (21+ patterns, Gitleaks integration)",
143
+ "- Pre-commit hooks (local and global)",
144
+ "- Command interception with risk-tiered approval",
145
+ "- Skill/extension auditing",
146
+ "- Audit logging",
147
+ "- MCP server for tool integration",
148
+ "- CI/CD pipeline generation",
149
+ "- All supported agent integrations (Claude Code, Codex, Gemini, Cursor, Windsurf, Aider, OpenClaw, Continue.dev)",
150
+ "",
151
+ "No API key. No sign-up. No telemetry. No data collection. No network access required.",
152
+ "Everything runs locally on your machine. MIT licensed.",
153
+ "",
154
+ "## Remote Code Analysis (API)",
155
+ "",
156
+ "Remote SAST/SCA scanning via the Rafter API has a free tier.",
157
+ "Sign up at rafter.so for an API key. Enterprise plans offer higher",
158
+ "limits, dashboards, policy management, and compliance reporting.",
159
+ "",
160
+ "## Philosophy",
161
+ "",
162
+ "Security tooling should be free for the people writing code.",
163
+ "Generous free tiers drive bottom-up adoption. Enterprise value",
164
+ "comes from dashboards, policy, and compliance — not from gating",
165
+ "the tools developers use every day.",
166
+ ].join("\n"),
167
+ },
131
168
  all: {
132
169
  description: "Everything — full security + scanning + setup briefing",
133
170
  render: () => {
@@ -1,13 +1,14 @@
1
1
  import { Command } from "commander";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import { fmt } from "../../utils/formatter.js";
4
+ import { fmt, isAgentMode } from "../../utils/formatter.js";
5
5
  export function createCiInitCommand() {
6
6
  return new Command("init")
7
7
  .description("Generate CI/CD pipeline config for secret scanning")
8
8
  .option("--platform <platform>", "CI platform: github, gitlab, circleci (default: auto-detect)")
9
9
  .option("--output <path>", "Output file path (default: platform-specific)")
10
- .option("--with-backend", "Include backend security audit job (requires RAFTER_API_KEY)")
10
+ .option("--with-remote", "Include remote security audit job (requires RAFTER_API_KEY)")
11
+ .option("--with-backend", "Deprecated: use --with-remote")
11
12
  .action((opts) => {
12
13
  const platform = opts.platform || detectPlatform();
13
14
  if (!platform) {
@@ -21,7 +22,8 @@ export function createCiInitCommand() {
21
22
  console.error(`Valid options: ${validPlatforms.join(", ")}`);
22
23
  process.exit(1);
23
24
  }
24
- const { content, defaultPath } = generateTemplate(platform, !!opts.withBackend);
25
+ const includeRemote = !!(opts.includeRemote || opts.withBackend);
26
+ const { content, defaultPath } = generateTemplate(platform, includeRemote);
25
27
  const outputPath = opts.output || defaultPath;
26
28
  const outputDir = path.dirname(outputPath);
27
29
  if (!fs.existsSync(outputDir)) {
@@ -29,28 +31,30 @@ export function createCiInitCommand() {
29
31
  }
30
32
  fs.writeFileSync(outputPath, content, "utf-8");
31
33
  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");
34
+ if (!isAgentMode()) {
35
+ console.log();
36
+ console.log("Next steps:");
37
+ console.log(` 1. Review the generated file: ${outputPath}`);
38
+ if (includeRemote) {
39
+ if (platform === "github") {
40
+ console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
41
+ }
42
+ else if (platform === "gitlab") {
43
+ console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
44
+ }
45
+ else {
46
+ console.log(" 2. Add RAFTER_API_KEY to project environment variables");
47
+ }
41
48
  }
42
- else {
43
- console.log(" 2. Add RAFTER_API_KEY to project environment variables");
49
+ console.log(` ${includeRemote ? "3" : "2"}. Commit and push to trigger the pipeline`);
50
+ if (platform === "github") {
51
+ console.log();
52
+ console.log("Alternatives:");
53
+ console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
54
+ console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
44
55
  }
45
- }
46
- console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
47
- if (platform === "github") {
48
56
  console.log();
49
- console.log("Alternatives:");
50
- console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v0");
51
- console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
52
57
  }
53
- console.log();
54
58
  });
55
59
  }
56
60
  function detectPlatform() {
@@ -74,7 +78,7 @@ function generateTemplate(platform, withBackend) {
74
78
  }
75
79
  function githubTemplate(withBackend) {
76
80
  let yaml = `# Generated by: rafter ci init
77
- # Alternative: uses: Raftersecurity/rafter-cli@v0
81
+ # Alternative: uses: Raftersecurity/rafter-cli@v1
78
82
  name: Rafter Security
79
83
 
80
84
  on:
@@ -68,7 +68,7 @@ _rafter_completions() {
68
68
  if [[ "\${COMP_WORDS[1]}" == "agent" ]]; then
69
69
  COMPREPLY=( $(compgen -W "--risk-level --with-openclaw --with-claude-code --with-codex --with-gemini --with-aider --with-cursor --with-windsurf --with-continue --with-gitleaks --all --help" -- "\${cur}") )
70
70
  elif [[ "\${COMP_WORDS[1]}" == "ci" ]]; then
71
- COMPREPLY=( $(compgen -W "--platform --output --with-backend --help" -- "\${cur}") )
71
+ COMPREPLY=( $(compgen -W "--platform --output --with-remote --with-backend --help" -- "\${cur}") )
72
72
  fi
73
73
  return 0
74
74
  ;;
@@ -81,7 +81,7 @@ const ZSH_COMPLETION = `#compdef rafter
81
81
  _rafter() {
82
82
  local -a commands
83
83
  commands=(
84
- 'run:Submit a security scan to the Rafter backend'
84
+ 'run:Submit a remote security scan'
85
85
  'scan:Alias for run'
86
86
  'get:Retrieve scan results'
87
87
  'usage:Check API usage quota'
@@ -369,7 +369,8 @@ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcom
369
369
  complete -c rafter -n '__fish_seen_subcommand_from ci' -a init -d 'Initialize CI pipeline'
370
370
  complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l platform -d 'CI platform' -ra 'github gitlab circleci'
371
371
  complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l output -d 'Output path' -r
372
- complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-backend -d 'Include backend audit'
372
+ complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-remote -d 'Include remote audit'
373
+ complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-backend -d 'Deprecated: use --with-remote'
373
374
 
374
375
  # hook subcommands
375
376
  complete -c rafter -n '__fish_seen_subcommand_from hook' -a pretool -d 'PreToolUse hook handler'
@@ -4,31 +4,71 @@ import { AuditLogger } from "../../core/audit-logger.js";
4
4
  export function createHookPosttoolCommand() {
5
5
  return new Command("posttool")
6
6
  .description("PostToolUse hook handler (reads stdin, redacts secrets in output, writes JSON to stdout)")
7
- .action(async () => {
7
+ .option("--format <format>", "Output format: claude (default, also Codex/Continue), cursor, gemini, windsurf", "claude")
8
+ .action(async (opts) => {
9
+ const format = (opts.format || "claude");
8
10
  try {
9
11
  const input = await readStdin();
10
- let payload;
12
+ let raw;
11
13
  try {
12
- payload = JSON.parse(input);
14
+ raw = JSON.parse(input);
13
15
  }
14
16
  catch {
15
- writeOutput({ action: "continue" });
17
+ writeOutput({ action: "continue" }, format);
16
18
  return;
17
19
  }
18
20
  // Validate payload is an object with expected shape
19
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
20
- writeOutput({ action: "continue" });
21
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
22
+ writeOutput({ action: "continue" }, format);
21
23
  return;
22
24
  }
25
+ const payload = normalizePostInput(raw, format);
23
26
  const output = evaluateToolResponse(payload);
24
- writeOutput(output);
27
+ writeOutput(output, format);
25
28
  }
26
29
  catch {
27
30
  // Any unexpected error → fail open
28
- writeOutput({ action: "continue" });
31
+ writeOutput({ action: "continue" }, format);
29
32
  }
30
33
  });
31
34
  }
35
+ /**
36
+ * Normalize platform-specific PostToolUse stdin into common shape.
37
+ * Windsurf sends { tool_info: { stdout, stderr } }, Cursor sends { output, ... }.
38
+ */
39
+ function normalizePostInput(raw, format) {
40
+ if (format === "windsurf") {
41
+ const toolInfo = raw.tool_info || {};
42
+ return {
43
+ session_id: raw.trajectory_id,
44
+ tool_name: raw.agent_action_name?.includes("run_command") ? "Bash" : (toolInfo.mcp_tool_name || "unknown"),
45
+ tool_input: {},
46
+ tool_response: {
47
+ output: toolInfo.stdout || toolInfo.output || "",
48
+ error: toolInfo.stderr || "",
49
+ },
50
+ };
51
+ }
52
+ if (format === "cursor") {
53
+ return {
54
+ session_id: raw.conversation_id,
55
+ tool_name: raw.hook_event_name === "afterShellExecution" ? "Bash" : (raw.tool_name || "unknown"),
56
+ tool_input: raw.tool_input || {},
57
+ tool_response: {
58
+ output: raw.output || raw.tool_response?.output || "",
59
+ content: raw.content || raw.tool_response?.content || "",
60
+ error: raw.error || raw.tool_response?.error || "",
61
+ },
62
+ };
63
+ }
64
+ // Claude, Codex, Continue, Gemini — same shape
65
+ return {
66
+ session_id: raw.session_id,
67
+ tool_name: raw.tool_name || "",
68
+ tool_input: raw.tool_input || {},
69
+ tool_response: raw.tool_response,
70
+ };
71
+ }
32
72
  function evaluateToolResponse(payload) {
33
73
  const { tool_response } = payload;
34
74
  // No response body — pass through
@@ -82,6 +122,51 @@ function readStdin() {
82
122
  process.stdin.resume();
83
123
  });
84
124
  }
85
- function writeOutput(output) {
86
- process.stdout.write(JSON.stringify(output) + "\n");
125
+ function writeOutput(output, format) {
126
+ const isModify = output.action === "modify" && output.tool_response;
127
+ switch (format) {
128
+ case "cursor": {
129
+ // Cursor: { agentMessage?: string } for post-tool notifications
130
+ if (isModify) {
131
+ process.stdout.write(JSON.stringify({
132
+ agentMessage: "Rafter redacted secrets from tool output",
133
+ }) + "\n");
134
+ }
135
+ // No output for continue (noop)
136
+ break;
137
+ }
138
+ case "gemini": {
139
+ // Gemini AfterTool: { systemMessage?: string } or {}
140
+ if (isModify) {
141
+ process.stdout.write(JSON.stringify({
142
+ systemMessage: "Rafter redacted secrets from tool output",
143
+ }) + "\n");
144
+ }
145
+ else {
146
+ process.stdout.write("{}\n");
147
+ }
148
+ break;
149
+ }
150
+ case "windsurf": {
151
+ // Windsurf: exit 0 for continue, stderr for notification
152
+ if (isModify) {
153
+ process.stderr.write("Rafter: secrets redacted from tool output\n");
154
+ }
155
+ // Always exit 0 for post-tool (never block after execution)
156
+ break;
157
+ }
158
+ default: {
159
+ // Claude Code / Codex / Continue.dev: hookSpecificOutput envelope
160
+ const hookOutput = {
161
+ hookSpecificOutput: {
162
+ hookEventName: "PostToolUse",
163
+ },
164
+ };
165
+ if (isModify) {
166
+ hookOutput.hookSpecificOutput.modifiedToolResult = output.tool_response;
167
+ }
168
+ process.stdout.write(JSON.stringify(hookOutput) + "\n");
169
+ break;
170
+ }
171
+ }
87
172
  }
@@ -29,32 +29,81 @@ function formatApprovalMessage(command, evaluation) {
29
29
  export function createHookPretoolCommand() {
30
30
  return new Command("pretool")
31
31
  .description("PreToolUse hook handler (reads stdin, writes JSON decision to stdout)")
32
- .action(async () => {
32
+ .option("--format <format>", "Output format: claude (default, also Codex/Continue), cursor, gemini, windsurf", "claude")
33
+ .action(async (opts) => {
34
+ const format = (opts.format || "claude");
33
35
  try {
34
36
  const input = await readStdin();
35
- let payload;
37
+ let raw;
36
38
  try {
37
- payload = JSON.parse(input);
39
+ raw = JSON.parse(input);
38
40
  }
39
41
  catch {
40
42
  // Can't parse → fail open
41
- writeDecision({ decision: "allow" });
43
+ writeDecision({ decision: "allow" }, format);
42
44
  return;
43
45
  }
44
46
  // Validate payload is an object with expected shape
45
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
46
- writeDecision({ decision: "allow" });
47
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
48
+ writeDecision({ decision: "allow" }, format);
47
49
  return;
48
50
  }
51
+ const payload = normalizeInput(raw, format);
49
52
  const decision = evaluateToolCall(payload);
50
- writeDecision(decision);
53
+ writeDecision(decision, format);
51
54
  }
52
55
  catch {
53
56
  // Any unexpected error → fail open
54
- writeDecision({ decision: "allow" });
57
+ writeDecision({ decision: "allow" }, format);
55
58
  }
56
59
  });
57
60
  }
61
+ /**
62
+ * Normalize platform-specific stdin JSON into a common HookInput shape.
63
+ *
64
+ * Claude/Codex/Continue: { tool_name, tool_input: { command } }
65
+ * Cursor: { hook_event_name, command, cwd }
66
+ * Gemini: { tool_name, tool_input: { command } } (same as Claude)
67
+ * Windsurf: { agent_action_name, tool_info: { command_line, cwd } }
68
+ */
69
+ function normalizeInput(raw, format) {
70
+ if (format === "cursor") {
71
+ // Cursor sends { command, cwd, hook_event_name, ... }
72
+ const command = raw.command || "";
73
+ const eventName = raw.hook_event_name || "";
74
+ // beforeShellExecution → Bash, beforeMCPExecution → tool name from payload
75
+ const toolName = eventName === "beforeShellExecution" ? "Bash"
76
+ : eventName === "beforeReadFile" ? "Read"
77
+ : eventName === "afterFileEdit" ? "Write"
78
+ : raw.tool_name || "unknown";
79
+ return {
80
+ session_id: raw.conversation_id,
81
+ tool_name: toolName,
82
+ tool_input: eventName === "beforeShellExecution" ? { command } : (raw.tool_input || {}),
83
+ };
84
+ }
85
+ if (format === "windsurf") {
86
+ // Windsurf sends { agent_action_name, tool_info: { command_line, cwd } }
87
+ const toolInfo = raw.tool_info || {};
88
+ const actionName = raw.agent_action_name || "";
89
+ const toolName = actionName.includes("run_command") ? "Bash"
90
+ : actionName.includes("write_code") ? "Write"
91
+ : actionName.includes("read_code") ? "Read"
92
+ : actionName.includes("mcp_tool_use") ? (toolInfo.mcp_tool_name || "unknown")
93
+ : "unknown";
94
+ return {
95
+ session_id: raw.trajectory_id,
96
+ tool_name: toolName,
97
+ tool_input: toolName === "Bash" ? { command: toolInfo.command_line || "" } : toolInfo,
98
+ };
99
+ }
100
+ // Claude, Codex, Continue, Gemini — all use { tool_name, tool_input }
101
+ return {
102
+ session_id: raw.session_id,
103
+ tool_name: raw.tool_name || "",
104
+ tool_input: raw.tool_input || {},
105
+ };
106
+ }
58
107
  function evaluateToolCall(payload) {
59
108
  const { tool_name, tool_input } = payload;
60
109
  if (tool_name === "Bash") {
@@ -154,6 +203,52 @@ function readStdin() {
154
203
  process.stdin.resume();
155
204
  });
156
205
  }
157
- function writeDecision(decision) {
158
- process.stdout.write(JSON.stringify(decision) + "\n");
206
+ function writeDecision(decision, format) {
207
+ const isDeny = decision.decision === "deny";
208
+ const reason = decision.reason ?? "";
209
+ switch (format) {
210
+ case "cursor": {
211
+ // Cursor: { permission: "allow"|"deny"|"ask", agentMessage?, userMessage? }
212
+ const output = {
213
+ permission: isDeny ? "deny" : "allow",
214
+ };
215
+ if (isDeny && reason) {
216
+ output.agentMessage = reason;
217
+ output.userMessage = reason;
218
+ }
219
+ process.stdout.write(JSON.stringify(output) + "\n");
220
+ break;
221
+ }
222
+ case "gemini": {
223
+ // Gemini: {} for allow, { decision: "deny", reason: "..." } for deny
224
+ if (isDeny) {
225
+ process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
226
+ }
227
+ else {
228
+ process.stdout.write("{}\n");
229
+ }
230
+ break;
231
+ }
232
+ case "windsurf": {
233
+ // Windsurf: exit 0 for allow, exit 2 + stderr for deny
234
+ if (isDeny) {
235
+ process.stderr.write(reason + "\n");
236
+ process.exit(2);
237
+ }
238
+ // Allow: exit 0 (no output needed)
239
+ break;
240
+ }
241
+ default: {
242
+ // Claude Code / Codex / Continue.dev: hookSpecificOutput envelope
243
+ const output = {
244
+ hookSpecificOutput: {
245
+ hookEventName: "PreToolUse",
246
+ permissionDecision: isDeny ? "deny" : "allow",
247
+ permissionDecisionReason: reason,
248
+ },
249
+ };
250
+ process.stdout.write(JSON.stringify(output) + "\n");
251
+ break;
252
+ }
253
+ }
159
254
  }
@@ -27,14 +27,14 @@ function textResult(data) {
27
27
  function errorResult(message) {
28
28
  return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
29
29
  }
30
- function createServer() {
30
+ export function createServer() {
31
31
  const server = new Server({ name: "rafter", version: CLI_VERSION }, { capabilities: { tools: {}, resources: {} } });
32
32
  // ── Tools ───────────────────────────────────────────────────────────
33
33
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
34
34
  tools: [
35
35
  {
36
36
  name: "scan_secrets",
37
- description: "Scan files or directories for hardcoded secrets and credentials",
37
+ description: "Scan files or directories for leaked secrets, API keys, tokens, passwords, and credentials. Use before pushing code, when handling config files, or when asked 'is this safe to commit?' or 'check for leaked keys'.",
38
38
  inputSchema: {
39
39
  type: "object",
40
40
  properties: {
@@ -50,7 +50,7 @@ function createServer() {
50
50
  },
51
51
  {
52
52
  name: "evaluate_command",
53
- description: "Evaluate whether a shell command is allowed by Rafter security policy",
53
+ description: "Check if a shell command is safe to run per security policy. Use when asked 'is this command safe?' or before running destructive or privileged operations.",
54
54
  inputSchema: {
55
55
  type: "object",
56
56
  properties: {
@@ -61,7 +61,7 @@ function createServer() {
61
61
  },
62
62
  {
63
63
  name: "read_audit_log",
64
- description: "Read Rafter audit log entries with optional filtering",
64
+ description: "Read security event history blocked commands, detected secrets, policy overrides. Use when asked 'what happened?' or 'show security events'.",
65
65
  inputSchema: {
66
66
  type: "object",
67
67
  properties: {
@@ -76,7 +76,7 @@ function createServer() {
76
76
  },
77
77
  {
78
78
  name: "get_config",
79
- description: "Read Rafter configuration (full config or a specific key)",
79
+ description: "Read Rafter security policy and configuration. Use to understand what protections are active and what risk level is configured.",
80
80
  inputSchema: {
81
81
  type: "object",
82
82
  properties: {