@rafter-security/cli 0.5.3 → 0.5.9

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 (40) hide show
  1. package/README.md +15 -3
  2. package/dist/commands/agent/audit-skill.js +2 -2
  3. package/dist/commands/agent/audit.js +96 -0
  4. package/dist/commands/agent/baseline.js +213 -0
  5. package/dist/commands/agent/exec.js +1 -1
  6. package/dist/commands/agent/index.js +4 -0
  7. package/dist/commands/agent/init.js +371 -29
  8. package/dist/commands/agent/install-hook.js +41 -47
  9. package/dist/commands/agent/scan.js +196 -23
  10. package/dist/commands/agent/status.js +65 -4
  11. package/dist/commands/agent/update-gitleaks.js +40 -0
  12. package/dist/commands/agent/verify.js +18 -4
  13. package/dist/commands/backend/run.js +69 -61
  14. package/dist/commands/ci/init.js +10 -3
  15. package/dist/commands/completion.js +320 -110
  16. package/dist/commands/hook/posttool.js +21 -7
  17. package/dist/commands/hook/pretool.js +50 -13
  18. package/dist/commands/issues/dedup.js +39 -0
  19. package/dist/commands/issues/from-scan.js +143 -0
  20. package/dist/commands/issues/from-text.js +185 -0
  21. package/dist/commands/issues/github-client.js +85 -0
  22. package/dist/commands/issues/index.js +25 -0
  23. package/dist/commands/issues/issue-builder.js +101 -0
  24. package/dist/commands/policy/export.js +7 -2
  25. package/dist/commands/scan/index.js +44 -0
  26. package/dist/core/audit-logger.js +41 -0
  27. package/dist/core/config-defaults.js +28 -0
  28. package/dist/core/config-manager.js +19 -2
  29. package/dist/core/pattern-engine.js +26 -1
  30. package/dist/core/risk-rules.js +5 -3
  31. package/dist/index.js +8 -2
  32. package/dist/scanners/gitleaks.js +5 -5
  33. package/dist/scanners/regex-scanner.js +12 -1
  34. package/dist/scanners/secret-patterns.js +3 -3
  35. package/dist/utils/binary-manager.js +59 -20
  36. package/dist/utils/skill-manager.js +5 -3
  37. package/package.json +2 -1
  38. package/resources/pre-commit-hook.sh +2 -2
  39. package/resources/pre-push-hook.sh +60 -0
  40. package/resources/rafter-security-skill.md +7 -11
@@ -2,10 +2,36 @@ import { Command } from "commander";
2
2
  import { RegexScanner } from "../../scanners/regex-scanner.js";
3
3
  import { GitleaksScanner } from "../../scanners/gitleaks.js";
4
4
  import { ConfigManager } from "../../core/config-manager.js";
5
+ import { AuditLogger } from "../../core/audit-logger.js";
5
6
  import { execSync, execFileSync } from "child_process";
6
7
  import fs from "fs";
8
+ import os from "os";
7
9
  import path from "path";
8
10
  import { fmt } from "../../utils/formatter.js";
11
+ function loadBaselineEntries() {
12
+ const baselinePath = path.join(os.homedir(), ".rafter", "baseline.json");
13
+ if (!fs.existsSync(baselinePath))
14
+ return [];
15
+ try {
16
+ const data = JSON.parse(fs.readFileSync(baselinePath, "utf-8"));
17
+ return data.entries || [];
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ }
23
+ function applyBaseline(results, entries) {
24
+ if (entries.length === 0)
25
+ return results;
26
+ return results
27
+ .map((r) => ({
28
+ ...r,
29
+ matches: r.matches.filter((m) => !entries.some((e) => e.file === r.file &&
30
+ e.pattern === m.pattern.name &&
31
+ (e.line == null || e.line === (m.line ?? null)))),
32
+ }))
33
+ .filter((r) => r.matches.length > 0);
34
+ }
9
35
  export function createScanCommand() {
10
36
  return new Command("scan")
11
37
  .description("Scan files or directories for secrets")
@@ -16,19 +42,42 @@ export function createScanCommand() {
16
42
  .option("--staged", "Scan only git staged files")
17
43
  .option("--diff <ref>", "Scan files changed since a git ref")
18
44
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
45
+ .option("--baseline", "Filter findings present in the saved baseline")
46
+ .option("--watch", "Watch for file changes and re-scan on change")
19
47
  .action(async (scanPath, opts) => {
48
+ // Validate flags before doing any work
49
+ const validEngines = ["auto", "gitleaks", "patterns"];
50
+ const engineValue = opts.engine || "auto";
51
+ if (!validEngines.includes(engineValue)) {
52
+ console.error(`Invalid engine: ${engineValue}. Valid values: ${validEngines.join(", ")}`);
53
+ process.exit(2);
54
+ }
55
+ const format = opts.format ?? (opts.json ? "json" : "text");
56
+ const validFormats = ["text", "json", "sarif"];
57
+ if (!validFormats.includes(format)) {
58
+ console.error(`Invalid format: ${format}. Valid values: ${validFormats.join(", ")}`);
59
+ process.exit(2);
60
+ }
61
+ // Deprecation notice — only when invoked as `rafter agent scan`, not as `rafter scan local`
62
+ const argv = process.argv;
63
+ const isAgentScan = argv.includes("agent") && argv.includes("scan") &&
64
+ argv.indexOf("agent") < argv.indexOf("scan");
65
+ if (isAgentScan) {
66
+ process.stderr.write("Warning: rafter agent scan is deprecated and will be removed in a future major version. Use rafter scan local instead.\n");
67
+ }
20
68
  // Load policy-merged config for excludePaths/customPatterns
21
69
  const manager = new ConfigManager();
22
70
  const cfg = manager.loadWithPolicy();
23
71
  const scanCfg = cfg.agent?.scan;
72
+ const baselineEntries = opts.baseline ? loadBaselineEntries() : [];
24
73
  // Handle --diff flag
25
74
  if (opts.diff) {
26
- await scanDiffFiles(opts.diff, opts, scanCfg);
75
+ await scanDiffFiles(opts.diff, opts, scanCfg, baselineEntries);
27
76
  return;
28
77
  }
29
78
  // Handle --staged flag
30
79
  if (opts.staged) {
31
- await scanStagedFiles(opts, scanCfg);
80
+ await scanStagedFiles(opts, scanCfg, baselineEntries);
32
81
  return;
33
82
  }
34
83
  const resolvedPath = path.resolve(scanPath);
@@ -37,6 +86,11 @@ export function createScanCommand() {
37
86
  console.error(`Error: Path not found: ${resolvedPath}`);
38
87
  process.exit(2);
39
88
  }
89
+ // Handle --watch flag
90
+ if (opts.watch) {
91
+ await watchAndScan(resolvedPath, opts, scanCfg);
92
+ return;
93
+ }
40
94
  // Determine scan engine
41
95
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
42
96
  // Determine if path is file or directory
@@ -54,7 +108,7 @@ export function createScanCommand() {
54
108
  }
55
109
  results = await scanFile(resolvedPath, engine, scanCfg);
56
110
  }
57
- outputScanResults(results, opts);
111
+ outputScanResults(applyBaseline(results, baselineEntries), opts);
58
112
  });
59
113
  }
60
114
  /**
@@ -89,13 +143,14 @@ function outputSarif(results) {
89
143
  }
90
144
  }
91
145
  const sarif = {
92
- $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
146
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
93
147
  version: "2.1.0",
94
148
  runs: [
95
149
  {
96
150
  tool: {
97
151
  driver: {
98
152
  name: "rafter",
153
+ version: "0.5.7",
99
154
  informationUri: "https://rafter.so",
100
155
  rules: Array.from(rules.values()),
101
156
  },
@@ -110,8 +165,12 @@ function outputSarif(results) {
110
165
  /**
111
166
  * Shared output logic for scan results
112
167
  */
113
- function outputScanResults(results, opts, context) {
168
+ function outputScanResults(results, opts, context, exitOnFindings = true) {
114
169
  const format = opts.format ?? (opts.json ? "json" : "text");
170
+ if (!["text", "json", "sarif"].includes(format)) {
171
+ console.error(`Invalid format: ${format}. Valid values: text, json, sarif`);
172
+ process.exit(2);
173
+ }
115
174
  if (format === "sarif") {
116
175
  outputSarif(results);
117
176
  return;
@@ -127,14 +186,18 @@ function outputScanResults(results, opts, context) {
127
186
  })),
128
187
  }));
129
188
  console.log(JSON.stringify(out, null, 2));
130
- process.exit(results.length > 0 ? 1 : 0);
189
+ if (exitOnFindings)
190
+ process.exit(results.length > 0 ? 1 : 0);
191
+ return;
131
192
  }
132
193
  if (results.length === 0) {
133
194
  if (!opts.quiet) {
134
195
  const msg = context ? `No secrets detected in ${context}` : "No secrets detected";
135
196
  console.log(`\n${fmt.success(msg)}\n`);
136
197
  }
137
- process.exit(0);
198
+ if (exitOnFindings)
199
+ process.exit(0);
200
+ return;
138
201
  }
139
202
  console.log(`\n${fmt.warning(`Found secrets in ${results.length} file(s):`)}\n`);
140
203
  let totalMatches = 0;
@@ -155,34 +218,37 @@ function outputScanResults(results, opts, context) {
155
218
  if (context === "staged files") {
156
219
  console.log(`${fmt.error("Commit blocked. Remove secrets before committing.")}\n`);
157
220
  }
158
- else {
221
+ else if (exitOnFindings) {
159
222
  console.log(`Run 'rafter agent audit' to see the security log.\n`);
160
223
  }
161
- process.exit(1);
224
+ if (exitOnFindings)
225
+ process.exit(1);
162
226
  }
163
227
  /**
164
228
  * Scan files changed since a git ref
165
229
  */
166
- async function scanDiffFiles(ref, opts, scanCfg) {
230
+ async function scanDiffFiles(ref, opts, scanCfg, baselineEntries = []) {
167
231
  try {
168
232
  const diffOutput = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACM", ref], {
169
233
  encoding: "utf-8",
170
234
  stdio: ["pipe", "pipe", "ignore"],
171
235
  }).trim();
172
236
  if (!diffOutput) {
173
- if (!opts.quiet) {
174
- console.log(fmt.success(`No files changed since ${ref}`));
175
- }
176
- process.exit(0);
237
+ outputScanResults([], opts, `files changed since ${ref}`);
238
+ return;
177
239
  }
178
240
  const changedFiles = diffOutput.split("\n").map(f => f.trim()).filter(f => f);
179
241
  if (!opts.quiet) {
180
242
  console.error(`Scanning ${changedFiles.length} file(s) changed since ${ref}...`);
181
243
  }
244
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
245
+ encoding: "utf-8",
246
+ stdio: ["pipe", "pipe", "ignore"],
247
+ }).trim();
182
248
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
183
249
  const allResults = [];
184
250
  for (const file of changedFiles) {
185
- const filePath = path.resolve(file);
251
+ const filePath = path.resolve(repoRoot, file);
186
252
  if (!fs.existsSync(filePath))
187
253
  continue;
188
254
  const stats = fs.statSync(filePath);
@@ -191,7 +257,7 @@ async function scanDiffFiles(ref, opts, scanCfg) {
191
257
  const results = await scanFile(filePath, engine, scanCfg);
192
258
  allResults.push(...results);
193
259
  }
194
- outputScanResults(allResults, opts, `files changed since ${ref}`);
260
+ outputScanResults(applyBaseline(allResults, baselineEntries), opts, `files changed since ${ref}`);
195
261
  }
196
262
  catch (error) {
197
263
  if (error.status === 128) {
@@ -204,26 +270,28 @@ async function scanDiffFiles(ref, opts, scanCfg) {
204
270
  /**
205
271
  * Scan git staged files for secrets
206
272
  */
207
- async function scanStagedFiles(opts, scanCfg) {
273
+ async function scanStagedFiles(opts, scanCfg, baselineEntries = []) {
208
274
  try {
209
275
  const stagedFilesOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
210
276
  encoding: "utf-8",
211
277
  stdio: ["pipe", "pipe", "ignore"]
212
278
  }).trim();
213
279
  if (!stagedFilesOutput) {
214
- if (!opts.quiet) {
215
- console.log(fmt.success("No files staged for commit"));
216
- }
217
- process.exit(0);
280
+ outputScanResults([], opts, "staged files");
281
+ return;
218
282
  }
219
283
  const stagedFiles = stagedFilesOutput.split("\n").map(f => f.trim()).filter(f => f);
220
284
  if (!opts.quiet) {
221
285
  console.error(`Scanning ${stagedFiles.length} staged file(s)...`);
222
286
  }
287
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
288
+ encoding: "utf-8",
289
+ stdio: ["pipe", "pipe", "ignore"],
290
+ }).trim();
223
291
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
224
292
  const allResults = [];
225
293
  for (const file of stagedFiles) {
226
- const filePath = path.resolve(file);
294
+ const filePath = path.resolve(repoRoot, file);
227
295
  if (!fs.existsSync(filePath))
228
296
  continue;
229
297
  const stats = fs.statSync(filePath);
@@ -232,7 +300,7 @@ async function scanStagedFiles(opts, scanCfg) {
232
300
  const results = await scanFile(filePath, engine, scanCfg);
233
301
  allResults.push(...results);
234
302
  }
235
- outputScanResults(allResults, opts, "staged files");
303
+ outputScanResults(applyBaseline(allResults, baselineEntries), opts, "staged files");
236
304
  }
237
305
  catch (error) {
238
306
  if (error.status === 128) {
@@ -260,6 +328,10 @@ async function selectEngine(preference, quiet) {
260
328
  }
261
329
  return "gitleaks";
262
330
  }
331
+ if (preference !== "auto") {
332
+ console.error(`Invalid engine: ${preference}. Valid values: auto, gitleaks, patterns`);
333
+ process.exit(2);
334
+ }
263
335
  // Auto mode: try Gitleaks, fall back to patterns
264
336
  const gitleaks = new GitleaksScanner();
265
337
  const available = await gitleaks.isAvailable();
@@ -308,3 +380,104 @@ async function scanDirectory(dirPath, engine, scanCfg) {
308
380
  return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
309
381
  }
310
382
  }
383
+ /**
384
+ * Watch a path for changes and re-scan on each change
385
+ */
386
+ async function watchAndScan(watchPath, opts, scanCfg) {
387
+ const { watch } = await import("chokidar");
388
+ const logger = new AuditLogger();
389
+ const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
390
+ if (!opts.quiet) {
391
+ console.error(fmt.info(`Watching ${watchPath} for changes (${engine}). Press Ctrl+C to exit.`));
392
+ }
393
+ // Do an initial scan
394
+ const stats = fs.statSync(watchPath);
395
+ const initialResults = stats.isDirectory()
396
+ ? await scanDirectory(watchPath, engine, scanCfg)
397
+ : await scanFile(watchPath, engine, scanCfg);
398
+ if (initialResults.length > 0) {
399
+ console.log(fmt.warning(`\n[Initial scan] Found secrets:`));
400
+ outputScanResults(initialResults, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
401
+ logWatchFindings(logger, initialResults);
402
+ }
403
+ else if (!opts.quiet) {
404
+ console.log(fmt.success(`[Initial scan] No secrets detected`));
405
+ }
406
+ const watcher = watch(watchPath, {
407
+ ignoreInitial: true,
408
+ persistent: true,
409
+ ignored: /(^|[/\\])\../,
410
+ });
411
+ watcher.on("change", async (filePath) => {
412
+ const timestamp = new Date().toLocaleTimeString();
413
+ if (!opts.quiet) {
414
+ console.error(`\n[${timestamp}] Changed: ${filePath}`);
415
+ }
416
+ if (!fs.existsSync(filePath))
417
+ return;
418
+ const fileStats = fs.statSync(filePath);
419
+ if (!fileStats.isFile())
420
+ return;
421
+ const results = await scanFile(filePath, engine, scanCfg);
422
+ if (results.length > 0) {
423
+ outputScanResults(results, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
424
+ logWatchFindings(logger, results);
425
+ }
426
+ else if (!opts.quiet) {
427
+ console.log(fmt.success(` No secrets detected`));
428
+ }
429
+ });
430
+ watcher.on("add", async (filePath) => {
431
+ const timestamp = new Date().toLocaleTimeString();
432
+ if (!opts.quiet) {
433
+ console.error(`\n[${timestamp}] Added: ${filePath}`);
434
+ }
435
+ const fileStats = fs.statSync(filePath);
436
+ if (!fileStats.isFile())
437
+ return;
438
+ const results = await scanFile(filePath, engine, scanCfg);
439
+ if (results.length > 0) {
440
+ outputScanResults(results, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
441
+ logWatchFindings(logger, results);
442
+ }
443
+ else if (!opts.quiet) {
444
+ console.log(fmt.success(` No secrets detected`));
445
+ }
446
+ });
447
+ // Keep process alive until Ctrl+C
448
+ await new Promise((resolve) => {
449
+ process.on("SIGINT", () => {
450
+ if (!opts.quiet) {
451
+ console.log(fmt.info("\nWatch mode stopped."));
452
+ }
453
+ watcher.close();
454
+ resolve();
455
+ });
456
+ });
457
+ }
458
+ /**
459
+ * Log watch findings to audit log
460
+ */
461
+ function logWatchFindings(logger, results) {
462
+ for (const result of results) {
463
+ for (const match of result.matches) {
464
+ logger.log({
465
+ eventType: "secret_detected",
466
+ securityCheck: {
467
+ passed: false,
468
+ reason: `${match.pattern.name} detected in ${result.file}`,
469
+ details: {
470
+ file: result.file,
471
+ line: match.line,
472
+ pattern: match.pattern.name,
473
+ severity: match.pattern.severity,
474
+ watchMode: true,
475
+ },
476
+ },
477
+ resolution: {
478
+ actionTaken: "allowed",
479
+ },
480
+ });
481
+ }
482
+ }
483
+ }
@@ -32,7 +32,7 @@ export function createStatusCommand() {
32
32
  }
33
33
  // --- Gitleaks ---
34
34
  const localGitleaks = path.join(getBinDir(), "gitleaks");
35
- let gitleaksStatus = "not found — run: rafter agent init";
35
+ let gitleaksStatus = "not found — run: rafter agent init --with-gitleaks";
36
36
  try {
37
37
  const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8" }).trim();
38
38
  gitleaksStatus = `${ver} (PATH)`;
@@ -74,8 +74,8 @@ export function createStatusCommand() {
74
74
  // unreadable settings
75
75
  }
76
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"}`);
77
+ console.log(`PreToolUse: ${pretoolOk ? "installed" : "not installed — run: rafter agent init --with-claude-code"}`);
78
+ console.log(`PostToolUse: ${posttoolOk ? "installed" : "not installed — run: rafter agent init --with-claude-code"}`);
79
79
  // --- OpenClaw skill ---
80
80
  const skillPath = path.join(home, ".openclaw", "skills", "rafter-security.md");
81
81
  const openclawDir = path.join(home, ".openclaw");
@@ -83,11 +83,72 @@ export function createStatusCommand() {
83
83
  console.log(`OpenClaw: skill installed (${skillPath})`);
84
84
  }
85
85
  else if (fs.existsSync(openclawDir)) {
86
- console.log("OpenClaw: detected but skill missing — run: rafter agent init");
86
+ console.log("OpenClaw: detected but skill missing — run: rafter agent init --with-openclaw");
87
87
  }
88
88
  else {
89
89
  console.log("OpenClaw: not detected (optional)");
90
90
  }
91
+ // --- Codex CLI skills ---
92
+ const codexDir = path.join(home, ".codex");
93
+ const codexSkillPath = path.join(home, ".agents", "skills", "rafter", "SKILL.md");
94
+ if (fs.existsSync(codexSkillPath)) {
95
+ console.log(`Codex CLI: skills installed (${path.join(home, ".agents", "skills")})`);
96
+ }
97
+ else if (fs.existsSync(codexDir)) {
98
+ console.log("Codex CLI: detected but skills missing — run: rafter agent init --with-codex");
99
+ }
100
+ else {
101
+ console.log("Codex CLI: not detected (optional)");
102
+ }
103
+ // --- MCP-native AI engine integrations ---
104
+ const mcpAgents = [
105
+ { name: "Gemini CLI", flag: "--with-gemini", configDir: path.join(home, ".gemini"), configFile: path.join(home, ".gemini", "settings.json"), needle: "rafter" },
106
+ { name: "Cursor", flag: "--with-cursor", configDir: path.join(home, ".cursor"), configFile: path.join(home, ".cursor", "mcp.json"), needle: "rafter" },
107
+ { name: "Windsurf", flag: "--with-windsurf", configDir: path.join(home, ".codeium", "windsurf"), configFile: path.join(home, ".codeium", "windsurf", "mcp_config.json"), needle: "rafter" },
108
+ { name: "Continue.dev", flag: "--with-continue", configDir: path.join(home, ".continue"), configFile: path.join(home, ".continue", "config.json"), needle: "rafter" },
109
+ ];
110
+ for (const agent of mcpAgents) {
111
+ const label = `${agent.name}:`.padEnd(14);
112
+ if (fs.existsSync(agent.configFile)) {
113
+ try {
114
+ const content = fs.readFileSync(agent.configFile, "utf-8");
115
+ if (content.includes(agent.needle)) {
116
+ console.log(`${label}MCP installed (${agent.configFile})`);
117
+ }
118
+ else {
119
+ console.log(`${label}detected but MCP missing — run: rafter agent init ${agent.flag}`);
120
+ }
121
+ }
122
+ catch {
123
+ console.log(`${label}config unreadable (${agent.configFile})`);
124
+ }
125
+ }
126
+ else if (fs.existsSync(agent.configDir)) {
127
+ console.log(`${label}detected but MCP missing — run: rafter agent init ${agent.flag}`);
128
+ }
129
+ else {
130
+ console.log(`${label}not detected (optional)`);
131
+ }
132
+ }
133
+ // --- Aider ---
134
+ const aiderConfig = path.join(home, ".aider.conf.yml");
135
+ if (fs.existsSync(aiderConfig)) {
136
+ try {
137
+ const content = fs.readFileSync(aiderConfig, "utf-8");
138
+ if (content.includes("rafter mcp serve")) {
139
+ console.log(`Aider: MCP installed (${aiderConfig})`);
140
+ }
141
+ else {
142
+ console.log("Aider: detected but MCP missing — run: rafter agent init --with-aider");
143
+ }
144
+ }
145
+ catch {
146
+ console.log(`Aider: config unreadable (${aiderConfig})`);
147
+ }
148
+ }
149
+ else {
150
+ console.log("Aider: not detected (optional)");
151
+ }
91
152
  // --- Audit log summary ---
92
153
  console.log(`\nAudit log: ${auditPath}`);
93
154
  if (fs.existsSync(auditPath)) {
@@ -0,0 +1,40 @@
1
+ import { Command } from "commander";
2
+ import { BinaryManager, GITLEAKS_VERSION } from "../../utils/binary-manager.js";
3
+ import { fmt } from "../../utils/formatter.js";
4
+ export function createUpdateGitleaksCommand() {
5
+ return new Command("update-gitleaks")
6
+ .description("Update (or reinstall) the managed gitleaks binary")
7
+ .option("--version <version>", "Gitleaks version to install", GITLEAKS_VERSION)
8
+ .action(async (opts) => {
9
+ const bm = new BinaryManager();
10
+ if (!bm.isPlatformSupported()) {
11
+ const { platform, arch } = bm.getPlatformInfo();
12
+ console.error(fmt.error(`Gitleaks not available for ${platform}/${arch}`));
13
+ process.exit(1);
14
+ }
15
+ // Show current version if installed
16
+ if (bm.isGitleaksInstalled()) {
17
+ const current = await bm.getGitleaksVersion();
18
+ console.log(fmt.info(`Current: ${current}`));
19
+ }
20
+ else {
21
+ console.log(fmt.info("Gitleaks not currently installed (managed binary)"));
22
+ }
23
+ console.log(fmt.info(`Installing gitleaks v${opts.version}...`));
24
+ console.log();
25
+ try {
26
+ await bm.downloadGitleaks((msg) => console.log(` ${msg}`), opts.version);
27
+ console.log();
28
+ const installed = await bm.getGitleaksVersion();
29
+ console.log(fmt.success(`Gitleaks updated: ${installed}`));
30
+ console.log(fmt.info(` Binary: ${bm.getGitleaksPath()}`));
31
+ }
32
+ catch (e) {
33
+ console.log();
34
+ console.error(fmt.error(`Update failed: ${e}`));
35
+ console.log(fmt.info("To fix: install gitleaks manually (https://github.com/gitleaks/gitleaks/releases) " +
36
+ "and ensure it is on PATH."));
37
+ process.exit(1);
38
+ }
39
+ });
40
+ }
@@ -43,7 +43,7 @@ function checkClaudeCode() {
43
43
  // optional: warn if absent but don't fail exit code
44
44
  const claudeDir = path.join(homeDir, ".claude");
45
45
  if (!fs.existsSync(claudeDir)) {
46
- return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --claude-code' to enable` };
46
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-claude-code' to enable` };
47
47
  }
48
48
  const settingsPath = path.join(claudeDir, "settings.json");
49
49
  if (!fs.existsSync(settingsPath)) {
@@ -54,7 +54,7 @@ function checkClaudeCode() {
54
54
  const hooks = settings?.hooks?.PreToolUse || [];
55
55
  const hasRafterHook = hooks.some((entry) => (entry.hooks || []).some((h) => h.command === "rafter hook pretool"));
56
56
  if (!hasRafterHook) {
57
- return { name, passed: false, optional: true, detail: "Rafter hooks not installed — run 'rafter agent init --claude-code'" };
57
+ return { name, passed: false, optional: true, detail: "Rafter hooks not installed — run 'rafter agent init --with-claude-code'" };
58
58
  }
59
59
  return { name, passed: true, detail: "Hooks installed" };
60
60
  }
@@ -66,14 +66,27 @@ function checkOpenClaw() {
66
66
  const name = "OpenClaw";
67
67
  const skillManager = new SkillManager();
68
68
  if (!skillManager.isOpenClawInstalled()) {
69
- return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init' to enable` };
69
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-openclaw' to enable` };
70
70
  }
71
71
  if (!skillManager.isRafterSkillInstalled()) {
72
- return { name, passed: false, optional: true, detail: `Rafter skill not installed — run 'rafter agent init'` };
72
+ return { name, passed: false, optional: true, detail: `Rafter skill not installed — run 'rafter agent init --with-openclaw'` };
73
73
  }
74
74
  const version = skillManager.getInstalledVersion();
75
75
  return { name, passed: true, detail: `Rafter skill installed${version ? ` (v${version})` : ""}` };
76
76
  }
77
+ function checkCodex() {
78
+ const name = "Codex CLI";
79
+ const homeDir = os.homedir();
80
+ const codexDir = path.join(homeDir, ".codex");
81
+ if (!fs.existsSync(codexDir)) {
82
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-codex' to enable` };
83
+ }
84
+ const skillPath = path.join(homeDir, ".agents", "skills", "rafter", "SKILL.md");
85
+ if (!fs.existsSync(skillPath)) {
86
+ return { name, passed: false, optional: true, detail: `Rafter skills not installed — run 'rafter agent init --with-codex'` };
87
+ }
88
+ return { name, passed: true, detail: `Skills installed (${path.join(homeDir, ".agents", "skills")})` };
89
+ }
77
90
  export function createVerifyCommand() {
78
91
  return new Command("verify")
79
92
  .description("Check agent security integration status")
@@ -86,6 +99,7 @@ export function createVerifyCommand() {
86
99
  await checkGitleaks(),
87
100
  checkClaudeCode(),
88
101
  checkOpenClaw(),
102
+ checkCodex(),
89
103
  ];
90
104
  for (const r of results) {
91
105
  if (r.passed) {