@rafter-security/cli 0.5.1 → 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.
package/README.md CHANGED
@@ -566,7 +566,7 @@ Agent security settings are stored in `~/.rafter/config.json`. Key settings:
566
566
 
567
567
  **File Locations:**
568
568
  - Config: `~/.rafter/config.json`
569
- - Audit log: `~/.rafter/audit.log`
569
+ - Audit log: `~/.rafter/audit.jsonl`
570
570
  - Binaries: `~/.rafter/bin/`
571
571
  - Patterns: `~/.rafter/patterns/`
572
572
 
@@ -48,6 +48,9 @@ async function auditSkill(skillPath, opts) {
48
48
  rafterSkillInstalled
49
49
  };
50
50
  console.log(JSON.stringify(result, null, 2));
51
+ if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
52
+ process.exit(1);
53
+ }
51
54
  return;
52
55
  }
53
56
  // Check if we can use OpenClaw
@@ -85,6 +88,9 @@ async function auditSkill(skillPath, opts) {
85
88
  console.log("─".repeat(60));
86
89
  }
87
90
  console.log();
91
+ if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
92
+ process.exit(1);
93
+ }
88
94
  }
89
95
  async function runQuickScan(content) {
90
96
  // 1. Scan for secrets
@@ -6,6 +6,8 @@ import { createConfigCommand } from "./config.js";
6
6
  import { createExecCommand } from "./exec.js";
7
7
  import { createAuditSkillCommand } from "./audit-skill.js";
8
8
  import { createInstallHookCommand } from "./install-hook.js";
9
+ import { createVerifyCommand } from "./verify.js";
10
+ import { createStatusCommand } from "./status.js";
9
11
  export function createAgentCommand() {
10
12
  const agent = new Command("agent")
11
13
  .description("Agent security features");
@@ -17,5 +19,7 @@ export function createAgentCommand() {
17
19
  agent.addCommand(createAuditCommand());
18
20
  agent.addCommand(createAuditSkillCommand());
19
21
  agent.addCommand(createInstallHookCommand());
22
+ agent.addCommand(createVerifyCommand());
23
+ agent.addCommand(createStatusCommand());
20
24
  return agent;
21
25
  }
@@ -32,16 +32,25 @@ function installClaudeCodeHooks() {
32
32
  settings.hooks = {};
33
33
  if (!settings.hooks.PreToolUse)
34
34
  settings.hooks.PreToolUse = [];
35
- const rafterHook = { type: "command", command: "rafter hook pretool" };
35
+ if (!settings.hooks.PostToolUse)
36
+ settings.hooks.PostToolUse = [];
37
+ const preHook = { type: "command", command: "rafter hook pretool" };
38
+ const postHook = { type: "command", command: "rafter hook posttool" };
36
39
  // Remove any existing Rafter hooks to avoid duplicates
37
40
  settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) => {
38
41
  const hooks = entry.hooks || [];
39
42
  return !hooks.some((h) => h.command === "rafter hook pretool");
40
43
  });
44
+ settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => {
45
+ const hooks = entry.hooks || [];
46
+ return !hooks.some((h) => h.command === "rafter hook posttool");
47
+ });
41
48
  // Add Rafter hooks
42
- settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [rafterHook] }, { matcher: "Write|Edit", hooks: [rafterHook] });
49
+ settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] }, { matcher: "Write|Edit", hooks: [preHook] });
50
+ settings.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
43
51
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
44
52
  console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
53
+ console.log(fmt.success(`Installed PostToolUse hooks to ${settingsPath}`));
45
54
  }
46
55
  async function installClaudeCodeSkills() {
47
56
  const homeDir = os.homedir();
@@ -125,49 +134,89 @@ export function createInitCommand() {
125
134
  }
126
135
  manager.set("agent.riskLevel", opts.riskLevel);
127
136
  console.log(fmt.success(`Set risk level: ${opts.riskLevel}`));
128
- // Download Gitleaks binary (optional)
137
+ // Check / download Gitleaks binary (optional)
129
138
  if (!opts.skipGitleaks) {
130
139
  const binaryManager = new BinaryManager();
131
140
  const platformInfo = binaryManager.getPlatformInfo();
132
- if (!platformInfo.supported) {
133
- console.log(fmt.info(`Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`));
134
- console.log(fmt.success("Using pattern-based scanning (21 patterns)"));
135
- }
136
- else if (binaryManager.isGitleaksInstalled()) {
137
- const version = await binaryManager.getGitleaksVersion();
138
- console.log(fmt.success(`Gitleaks already installed (${version})`));
141
+ // Helper: show diagnostics for a failing binary (mirrors Python's agent init)
142
+ const showDiagnostics = async (binaryPath, verResult) => {
143
+ if (verResult.stderr) {
144
+ console.log(fmt.info(` stderr: ${verResult.stderr}`));
145
+ }
146
+ const diag = await binaryManager.collectBinaryDiagnostics(binaryPath);
147
+ if (diag) {
148
+ console.log(fmt.info("Diagnostics:"));
149
+ console.log(diag);
150
+ }
151
+ console.log(fmt.info("To fix: install gitleaks (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
152
+ console.log();
153
+ };
154
+ if (binaryManager.isGitleaksInstalled()) {
155
+ // Local binary exists — verify it actually works
156
+ const verResult = await binaryManager.verifyGitleaksVerbose();
157
+ if (verResult.ok) {
158
+ console.log(fmt.success(`Gitleaks already installed (${verResult.stdout})`));
159
+ }
160
+ else {
161
+ console.log(fmt.warning("Gitleaks binary found locally but failed to execute."));
162
+ console.log(fmt.info(` Binary: ${binaryManager.getGitleaksPath()}`));
163
+ await showDiagnostics(binaryManager.getGitleaksPath(), verResult);
164
+ }
139
165
  }
140
166
  else {
141
- console.log();
142
- console.log(fmt.info("Downloading Gitleaks (enhanced secret detection)..."));
143
- try {
144
- await binaryManager.downloadGitleaks((msg) => {
145
- console.log(` ${msg}`);
146
- });
147
- console.log();
167
+ // Not installed locally — check PATH (mirrors Python's shutil.which)
168
+ const pathBinary = binaryManager.findGitleaksOnPath();
169
+ if (pathBinary) {
170
+ const verResult = await binaryManager.verifyGitleaksVerbose(pathBinary);
171
+ if (verResult.ok) {
172
+ console.log(fmt.success(`Gitleaks available on PATH (${verResult.stdout})`));
173
+ }
174
+ else {
175
+ console.log(fmt.warning("Gitleaks found on PATH but failed to execute."));
176
+ console.log(fmt.info(` Binary: ${pathBinary}`));
177
+ await showDiagnostics(pathBinary, verResult);
178
+ }
179
+ }
180
+ else if (!platformInfo.supported) {
181
+ console.log(fmt.info(`Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`));
182
+ console.log(fmt.success("Using pattern-based scanning (21 patterns)"));
148
183
  }
149
- catch (e) {
150
- console.log(fmt.warning(`Failed to download Gitleaks: ${e}`));
151
- console.log(fmt.success("Falling back to pattern-based scanning"));
184
+ else {
185
+ // Not on PATH, not installed locally — download
152
186
  console.log();
187
+ console.log(fmt.info("Downloading Gitleaks (enhanced secret detection)..."));
188
+ try {
189
+ await binaryManager.downloadGitleaks((msg) => {
190
+ console.log(` ${msg}`);
191
+ });
192
+ console.log();
193
+ }
194
+ catch (e) {
195
+ console.log();
196
+ console.log(fmt.error(`Gitleaks setup failed — pattern-based scanning will be used instead.`));
197
+ console.log(fmt.warning(String(e)));
198
+ console.log();
199
+ console.log(fmt.info("To fix: install gitleaks manually (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
200
+ console.log();
201
+ }
153
202
  }
154
203
  }
155
204
  }
156
205
  // Install OpenClaw skill if applicable
157
206
  if (hasOpenClaw && !opts.skipOpenclaw) {
158
- try {
159
- const skillManager = new SkillManager();
160
- const installed = await skillManager.installRafterSkill();
161
- if (installed) {
162
- console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
163
- manager.set("agent.environments.openclaw.enabled", true);
164
- }
165
- else {
166
- console.log(fmt.warning("Failed to install Rafter Security skill"));
167
- }
207
+ const skillManager = new SkillManager();
208
+ const result = await skillManager.installRafterSkillVerbose();
209
+ if (result.ok) {
210
+ console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
211
+ manager.set("agent.environments.openclaw.enabled", true);
168
212
  }
169
- catch (e) {
170
- console.error(fmt.error(`Failed to install OpenClaw skill: ${e}`));
213
+ else {
214
+ console.log(fmt.error("Failed to install Rafter Security skill"));
215
+ console.log(fmt.warning(` Source: ${result.sourcePath}`));
216
+ console.log(fmt.warning(` Destination: ${result.destPath}`));
217
+ if (result.error) {
218
+ console.log(fmt.warning(` Error: ${result.error}`));
219
+ }
171
220
  }
172
221
  }
173
222
  // Install Claude Code skills + hooks if applicable
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import fs from "fs";
3
+ import os from "os";
3
4
  import path from "path";
4
5
  import { execSync } from "child_process";
5
6
  import { fileURLToPath } from 'url';
@@ -81,7 +82,7 @@ async function installLocalHook() {
81
82
  */
82
83
  async function installGlobalHook() {
83
84
  // Create global hooks directory
84
- const homeDir = process.env.HOME || process.env.USERPROFILE;
85
+ const homeDir = os.homedir();
85
86
  if (!homeDir) {
86
87
  console.error("❌ Error: Could not determine home directory");
87
88
  process.exit(1);
@@ -12,6 +12,7 @@ export function createScanCommand() {
12
12
  .argument("[path]", "File or directory to scan", ".")
13
13
  .option("-q, --quiet", "Only output if secrets found")
14
14
  .option("--json", "Output as JSON")
15
+ .option("--format <format>", "Output format: text, json, sarif", "text")
15
16
  .option("--staged", "Scan only git staged files")
16
17
  .option("--diff <ref>", "Scan files changed since a git ref")
17
18
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
@@ -34,7 +35,7 @@ export function createScanCommand() {
34
35
  // Check if path exists
35
36
  if (!fs.existsSync(resolvedPath)) {
36
37
  console.error(`Error: Path not found: ${resolvedPath}`);
37
- process.exit(1);
38
+ process.exit(2);
38
39
  }
39
40
  // Determine scan engine
40
41
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
@@ -56,12 +57,76 @@ export function createScanCommand() {
56
57
  outputScanResults(results, opts);
57
58
  });
58
59
  }
60
+ /**
61
+ * Emit SARIF 2.1.0 JSON for GitHub/GitLab security tab integration
62
+ */
63
+ function outputSarif(results) {
64
+ const rules = new Map();
65
+ const sarifResults = [];
66
+ for (const r of results) {
67
+ for (const m of r.matches) {
68
+ const ruleId = m.pattern.name.toLowerCase().replace(/\s+/g, "-");
69
+ if (!rules.has(ruleId)) {
70
+ rules.set(ruleId, {
71
+ id: ruleId,
72
+ name: m.pattern.name,
73
+ shortDescription: m.pattern.description || m.pattern.name,
74
+ });
75
+ }
76
+ sarifResults.push({
77
+ ruleId,
78
+ level: m.pattern.severity === "critical" || m.pattern.severity === "high" ? "error" : "warning",
79
+ message: { text: `${m.pattern.name} detected` },
80
+ locations: [
81
+ {
82
+ physicalLocation: {
83
+ artifactLocation: { uri: r.file.replace(/\\/g, "/"), uriBaseId: "%SRCROOT%" },
84
+ region: m.line ? { startLine: m.line, startColumn: m.column ?? 1 } : undefined,
85
+ },
86
+ },
87
+ ],
88
+ });
89
+ }
90
+ }
91
+ const sarif = {
92
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
93
+ version: "2.1.0",
94
+ runs: [
95
+ {
96
+ tool: {
97
+ driver: {
98
+ name: "rafter",
99
+ informationUri: "https://rafter.so",
100
+ rules: Array.from(rules.values()),
101
+ },
102
+ },
103
+ results: sarifResults,
104
+ },
105
+ ],
106
+ };
107
+ console.log(JSON.stringify(sarif, null, 2));
108
+ process.exit(results.length > 0 ? 1 : 0);
109
+ }
59
110
  /**
60
111
  * Shared output logic for scan results
61
112
  */
62
113
  function outputScanResults(results, opts, context) {
63
- if (opts.json) {
64
- console.log(JSON.stringify(results, null, 2));
114
+ const format = opts.format ?? (opts.json ? "json" : "text");
115
+ if (format === "sarif") {
116
+ outputSarif(results);
117
+ return;
118
+ }
119
+ if (format === "json" || opts.json) {
120
+ const out = results.map((r) => ({
121
+ file: r.file,
122
+ matches: r.matches.map((m) => ({
123
+ pattern: { name: m.pattern.name, severity: m.pattern.severity, description: m.pattern.description || "" },
124
+ line: m.line ?? null,
125
+ column: m.column ?? null,
126
+ redacted: m.redacted || "",
127
+ })),
128
+ }));
129
+ console.log(JSON.stringify(out, null, 2));
65
130
  process.exit(results.length > 0 ? 1 : 0);
66
131
  }
67
132
  if (results.length === 0) {
@@ -131,7 +196,7 @@ async function scanDiffFiles(ref, opts, scanCfg) {
131
196
  catch (error) {
132
197
  if (error.status === 128) {
133
198
  console.error("Error: Not in a git repository or invalid ref");
134
- process.exit(1);
199
+ process.exit(2);
135
200
  }
136
201
  throw error;
137
202
  }
@@ -172,7 +237,7 @@ async function scanStagedFiles(opts, scanCfg) {
172
237
  catch (error) {
173
238
  if (error.status === 128) {
174
239
  console.error("Error: Not in a git repository");
175
- process.exit(1);
240
+ process.exit(2);
176
241
  }
177
242
  throw error;
178
243
  }
@@ -0,0 +1,115 @@
1
+ import { Command } from "commander";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ import { execSync } from "child_process";
6
+ import { getRafterDir, getAuditLogPath, getBinDir } from "../../core/config-defaults.js";
7
+ import { AuditLogger } from "../../core/audit-logger.js";
8
+ import { ConfigManager } from "../../core/config-manager.js";
9
+ export function createStatusCommand() {
10
+ return new Command("status")
11
+ .description("Show agent security status dashboard")
12
+ .action(async () => {
13
+ const rafterDir = getRafterDir();
14
+ const auditPath = getAuditLogPath();
15
+ const home = os.homedir();
16
+ console.log("Rafter Agent Status");
17
+ console.log("=".repeat(50));
18
+ // --- Config ---
19
+ const configPath = path.join(rafterDir, "config.json");
20
+ if (fs.existsSync(configPath)) {
21
+ try {
22
+ const cfg = new ConfigManager().load();
23
+ console.log(`\nConfig: ${configPath}`);
24
+ console.log(`Risk level: ${cfg.agent?.riskLevel ?? "moderate"}`);
25
+ }
26
+ catch {
27
+ console.log(`\nConfig: ${configPath} (parse error)`);
28
+ }
29
+ }
30
+ else {
31
+ console.log(`\nConfig: not found — run: rafter agent init`);
32
+ }
33
+ // --- Gitleaks ---
34
+ const localGitleaks = path.join(getBinDir(), "gitleaks");
35
+ let gitleaksStatus = "not found — run: rafter agent init";
36
+ try {
37
+ const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8" }).trim();
38
+ gitleaksStatus = `${ver} (PATH)`;
39
+ }
40
+ catch {
41
+ if (fs.existsSync(localGitleaks)) {
42
+ try {
43
+ const ver = execSync(`"${localGitleaks}" version`, { timeout: 5000, encoding: "utf-8" }).trim();
44
+ gitleaksStatus = `${ver} (local)`;
45
+ }
46
+ catch {
47
+ gitleaksStatus = `${localGitleaks} (binary error)`;
48
+ }
49
+ }
50
+ }
51
+ console.log(`Gitleaks: ${gitleaksStatus}`);
52
+ // --- Claude Code hooks ---
53
+ const settingsPath = path.join(home, ".claude", "settings.json");
54
+ let pretoolOk = false;
55
+ let posttoolOk = false;
56
+ if (fs.existsSync(settingsPath)) {
57
+ try {
58
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
59
+ const hooks = settings.hooks ?? {};
60
+ for (const entry of hooks.PreToolUse ?? []) {
61
+ for (const h of entry.hooks ?? []) {
62
+ if (String(h.command ?? "").includes("rafter hook pretool"))
63
+ pretoolOk = true;
64
+ }
65
+ }
66
+ for (const entry of hooks.PostToolUse ?? []) {
67
+ for (const h of entry.hooks ?? []) {
68
+ if (String(h.command ?? "").includes("rafter hook posttool"))
69
+ posttoolOk = true;
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // unreadable settings
75
+ }
76
+ }
77
+ console.log(`PreToolUse: ${pretoolOk ? "installed" : "not installed — run: rafter agent init"}`);
78
+ console.log(`PostToolUse: ${posttoolOk ? "installed" : "not installed — run: rafter agent init"}`);
79
+ // --- OpenClaw skill ---
80
+ const skillPath = path.join(home, ".openclaw", "skills", "rafter-security.md");
81
+ const openclawDir = path.join(home, ".openclaw");
82
+ if (fs.existsSync(skillPath)) {
83
+ console.log(`OpenClaw: skill installed (${skillPath})`);
84
+ }
85
+ else if (fs.existsSync(openclawDir)) {
86
+ console.log("OpenClaw: detected but skill missing — run: rafter agent init");
87
+ }
88
+ else {
89
+ console.log("OpenClaw: not detected (optional)");
90
+ }
91
+ // --- Audit log summary ---
92
+ console.log(`\nAudit log: ${auditPath}`);
93
+ if (fs.existsSync(auditPath)) {
94
+ const logger = new AuditLogger();
95
+ const allEntries = logger.read();
96
+ const total = allEntries.length;
97
+ const secrets = allEntries.filter((e) => e.eventType === "secret_detected").length;
98
+ const blocked = allEntries.filter((e) => e.eventType === "command_intercepted" && e.resolution?.actionTaken === "blocked").length;
99
+ console.log(`Total events: ${total} | Secrets detected: ${secrets} | Commands blocked: ${blocked}`);
100
+ const recent = logger.read({ limit: 5 });
101
+ if (recent.length > 0) {
102
+ console.log("\nRecent events:");
103
+ for (const e of [...recent].reverse()) {
104
+ const ts = (e.timestamp ?? "").slice(0, 19).replace("T", " ");
105
+ const action = e.resolution?.actionTaken ?? "";
106
+ console.log(` ${ts} ${e.eventType} [${action}]`);
107
+ }
108
+ }
109
+ }
110
+ else {
111
+ console.log("No events logged yet.");
112
+ }
113
+ console.log();
114
+ });
115
+ }
@@ -0,0 +1,117 @@
1
+ import { Command } from "commander";
2
+ import { BinaryManager } from "../../utils/binary-manager.js";
3
+ import { SkillManager } from "../../utils/skill-manager.js";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import os from "os";
7
+ import { fmt } from "../../utils/formatter.js";
8
+ async function checkGitleaks() {
9
+ const binaryManager = new BinaryManager();
10
+ const name = "Gitleaks";
11
+ // Check PATH first (e.g. Homebrew), then fall back to ~/.rafter/bin
12
+ const pathBinary = binaryManager.findGitleaksOnPath();
13
+ const hasBinary = pathBinary !== null || binaryManager.isGitleaksInstalled();
14
+ if (!hasBinary) {
15
+ return { name, passed: false, detail: `Not found on PATH or at ${binaryManager.getGitleaksPath()}` };
16
+ }
17
+ const binaryPath = pathBinary ?? binaryManager.getGitleaksPath();
18
+ const { ok, stdout, stderr } = await binaryManager.verifyGitleaksVerbose(binaryPath);
19
+ if (!ok) {
20
+ const diag = await binaryManager.collectBinaryDiagnostics(binaryPath);
21
+ return { name, passed: false, detail: `Binary found at ${binaryPath} but failed to execute\n${stdout ? ` stdout: ${stdout}\n` : ""}${stderr ? ` stderr: ${stderr}\n` : ""}${diag}` };
22
+ }
23
+ return { name, passed: true, detail: `${stdout} (${binaryPath})` };
24
+ }
25
+ function checkConfig() {
26
+ const name = "Config";
27
+ const configPath = path.join(os.homedir(), ".rafter", "config.json");
28
+ if (!fs.existsSync(configPath)) {
29
+ return { name, passed: false, detail: `Not found: ${configPath}` };
30
+ }
31
+ try {
32
+ const content = fs.readFileSync(configPath, "utf-8");
33
+ JSON.parse(content);
34
+ return { name, passed: true, detail: configPath };
35
+ }
36
+ catch (e) {
37
+ return { name, passed: false, detail: `Invalid JSON: ${configPath} — ${e}` };
38
+ }
39
+ }
40
+ function checkClaudeCode() {
41
+ const name = "Claude Code";
42
+ const homeDir = os.homedir();
43
+ // optional: warn if absent but don't fail exit code
44
+ const claudeDir = path.join(homeDir, ".claude");
45
+ if (!fs.existsSync(claudeDir)) {
46
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --claude-code' to enable` };
47
+ }
48
+ const settingsPath = path.join(claudeDir, "settings.json");
49
+ if (!fs.existsSync(settingsPath)) {
50
+ return { name, passed: false, optional: true, detail: `Settings file not found: ${settingsPath}` };
51
+ }
52
+ try {
53
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
54
+ const hooks = settings?.hooks?.PreToolUse || [];
55
+ const hasRafterHook = hooks.some((entry) => (entry.hooks || []).some((h) => h.command === "rafter hook pretool"));
56
+ if (!hasRafterHook) {
57
+ return { name, passed: false, optional: true, detail: "Rafter hooks not installed — run 'rafter agent init --claude-code'" };
58
+ }
59
+ return { name, passed: true, detail: "Hooks installed" };
60
+ }
61
+ catch (e) {
62
+ return { name, passed: false, optional: true, detail: `Cannot read settings: ${e}` };
63
+ }
64
+ }
65
+ function checkOpenClaw() {
66
+ const name = "OpenClaw";
67
+ const skillManager = new SkillManager();
68
+ if (!skillManager.isOpenClawInstalled()) {
69
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init' to enable` };
70
+ }
71
+ if (!skillManager.isRafterSkillInstalled()) {
72
+ return { name, passed: false, optional: true, detail: `Rafter skill not installed — run 'rafter agent init'` };
73
+ }
74
+ const version = skillManager.getInstalledVersion();
75
+ return { name, passed: true, detail: `Rafter skill installed${version ? ` (v${version})` : ""}` };
76
+ }
77
+ export function createVerifyCommand() {
78
+ return new Command("verify")
79
+ .description("Check agent security integration status")
80
+ .action(async () => {
81
+ console.log(fmt.header("Rafter Agent Verify"));
82
+ console.log(fmt.divider());
83
+ console.log();
84
+ const results = [
85
+ checkConfig(),
86
+ await checkGitleaks(),
87
+ checkClaudeCode(),
88
+ checkOpenClaw(),
89
+ ];
90
+ for (const r of results) {
91
+ if (r.passed) {
92
+ console.log(fmt.success(`${r.name}: ${r.detail}`));
93
+ }
94
+ else if (r.optional) {
95
+ console.log(fmt.warning(`${r.name}: ${r.detail}`));
96
+ }
97
+ else {
98
+ console.log(fmt.error(`${r.name}: FAIL — ${r.detail}`));
99
+ }
100
+ }
101
+ console.log();
102
+ const hardFailed = results.filter((r) => !r.passed && !r.optional);
103
+ const warned = results.filter((r) => !r.passed && r.optional);
104
+ const passed = results.filter((r) => r.passed);
105
+ if (hardFailed.length === 0) {
106
+ const warnNote = warned.length > 0 ? ` (${warned.length} optional check${warned.length > 1 ? "s" : ""} not configured)` : "";
107
+ console.log(fmt.success(`${passed.length}/${results.length} core checks passed${warnNote}`));
108
+ }
109
+ else {
110
+ console.log(fmt.error(`${hardFailed.length} check${hardFailed.length > 1 ? "s" : ""} failed`));
111
+ }
112
+ console.log();
113
+ if (hardFailed.length > 0) {
114
+ process.exit(1);
115
+ }
116
+ });
117
+ }