@rafter-security/cli 0.6.6 → 0.7.1

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 (70) hide show
  1. package/README.md +29 -10
  2. package/dist/commands/agent/audit-skill.js +22 -20
  3. package/dist/commands/agent/audit.js +27 -0
  4. package/dist/commands/agent/components.js +800 -0
  5. package/dist/commands/agent/config.js +2 -1
  6. package/dist/commands/agent/disable.js +47 -0
  7. package/dist/commands/agent/enable.js +50 -0
  8. package/dist/commands/agent/exec.js +2 -0
  9. package/dist/commands/agent/index.js +6 -0
  10. package/dist/commands/agent/init.js +162 -163
  11. package/dist/commands/agent/install-hook.js +15 -14
  12. package/dist/commands/agent/list.js +72 -0
  13. package/dist/commands/agent/scan.js +4 -3
  14. package/dist/commands/agent/verify.js +1 -1
  15. package/dist/commands/backend/run.js +12 -3
  16. package/dist/commands/backend/scan-status.js +3 -2
  17. package/dist/commands/brief.js +22 -2
  18. package/dist/commands/ci/init.js +25 -21
  19. package/dist/commands/completion.js +4 -3
  20. package/dist/commands/docs/index.js +18 -0
  21. package/dist/commands/docs/list.js +37 -0
  22. package/dist/commands/docs/show.js +64 -0
  23. package/dist/commands/mcp/server.js +84 -0
  24. package/dist/commands/report.js +42 -41
  25. package/dist/commands/scan/index.js +7 -5
  26. package/dist/commands/skill/index.js +14 -0
  27. package/dist/commands/skill/install.js +89 -0
  28. package/dist/commands/skill/list.js +79 -0
  29. package/dist/commands/skill/registry.js +273 -0
  30. package/dist/commands/skill/remote.js +333 -0
  31. package/dist/commands/skill/review.js +975 -0
  32. package/dist/commands/skill/uninstall.js +65 -0
  33. package/dist/core/audit-logger.js +262 -21
  34. package/dist/core/config-manager.js +3 -0
  35. package/dist/core/docs-loader.js +148 -0
  36. package/dist/core/policy-loader.js +72 -1
  37. package/dist/core/risk-rules.js +16 -3
  38. package/dist/index.js +19 -9
  39. package/dist/scanners/gitleaks.js +6 -2
  40. package/package.json +1 -1
  41. package/resources/skills/rafter/SKILL.md +77 -97
  42. package/resources/skills/rafter/docs/backend.md +106 -0
  43. package/resources/skills/rafter/docs/cli-reference.md +199 -0
  44. package/resources/skills/rafter/docs/finding-triage.md +79 -0
  45. package/resources/skills/rafter/docs/guardrails.md +91 -0
  46. package/resources/skills/rafter/docs/shift-left.md +64 -0
  47. package/resources/skills/rafter-agent-security/SKILL.md +1 -1
  48. package/resources/skills/rafter-code-review/SKILL.md +91 -0
  49. package/resources/skills/rafter-code-review/docs/api.md +90 -0
  50. package/resources/skills/rafter-code-review/docs/asvs.md +120 -0
  51. package/resources/skills/rafter-code-review/docs/cwe-top25.md +78 -0
  52. package/resources/skills/rafter-code-review/docs/investigation-playbook.md +101 -0
  53. package/resources/skills/rafter-code-review/docs/llm.md +87 -0
  54. package/resources/skills/rafter-code-review/docs/web-app.md +84 -0
  55. package/resources/skills/rafter-secure-design/SKILL.md +103 -0
  56. package/resources/skills/rafter-secure-design/docs/api-design.md +97 -0
  57. package/resources/skills/rafter-secure-design/docs/auth.md +67 -0
  58. package/resources/skills/rafter-secure-design/docs/data-storage.md +90 -0
  59. package/resources/skills/rafter-secure-design/docs/dependencies.md +101 -0
  60. package/resources/skills/rafter-secure-design/docs/deployment.md +104 -0
  61. package/resources/skills/rafter-secure-design/docs/ingestion.md +98 -0
  62. package/resources/skills/rafter-secure-design/docs/standards-pointers.md +102 -0
  63. package/resources/skills/rafter-secure-design/docs/threat-modeling.md +128 -0
  64. package/resources/skills/rafter-skill-review/SKILL.md +106 -0
  65. package/resources/skills/rafter-skill-review/docs/authorship-provenance.md +82 -0
  66. package/resources/skills/rafter-skill-review/docs/changelog-review.md +99 -0
  67. package/resources/skills/rafter-skill-review/docs/data-practices.md +88 -0
  68. package/resources/skills/rafter-skill-review/docs/malware-indicators.md +79 -0
  69. package/resources/skills/rafter-skill-review/docs/prompt-injection.md +85 -0
  70. package/resources/skills/rafter-skill-review/docs/telemetry.md +78 -0
@@ -0,0 +1,72 @@
1
+ import { Command } from "commander";
2
+ import { snapshotComponents } from "./components.js";
3
+ import { fmt } from "../../utils/formatter.js";
4
+ /**
5
+ * `rafter agent list` — machine-readable inventory of everything rafter has touched
6
+ * (or could touch) on this machine. One row per (platform, component).
7
+ *
8
+ * Exit codes: 0 on success.
9
+ */
10
+ export function createListCommand() {
11
+ return new Command("list")
12
+ .description("List agent integration components and their state")
13
+ .option("--json", "Output machine-readable JSON")
14
+ .option("--installed", "Only show components that are currently installed")
15
+ .option("--detected", "Only show components whose platform is detected")
16
+ .action((opts) => {
17
+ let rows = snapshotComponents();
18
+ if (opts.installed)
19
+ rows = rows.filter((r) => r.installed);
20
+ if (opts.detected)
21
+ rows = rows.filter((r) => r.detected);
22
+ if (opts.json) {
23
+ const payload = {
24
+ components: rows.map((r) => ({
25
+ id: r.id,
26
+ platform: r.platform,
27
+ kind: r.kind,
28
+ description: r.description,
29
+ path: r.path,
30
+ state: r.state,
31
+ installed: r.installed,
32
+ detected: r.detected,
33
+ configEnabled: r.configEnabled,
34
+ })),
35
+ };
36
+ console.log(JSON.stringify(payload, null, 2));
37
+ return;
38
+ }
39
+ const byPlatform = new Map();
40
+ for (const r of rows) {
41
+ const arr = byPlatform.get(r.platform) ?? [];
42
+ arr.push(r);
43
+ byPlatform.set(r.platform, arr);
44
+ }
45
+ console.log(fmt.header("Rafter agent components"));
46
+ console.log(fmt.divider());
47
+ for (const [platform, list] of byPlatform) {
48
+ const detected = list[0]?.detected ?? false;
49
+ const suffix = detected ? "" : " (not detected)";
50
+ console.log(`\n${platform}${suffix}`);
51
+ for (const r of list) {
52
+ const label = r.id.padEnd(28);
53
+ let marker;
54
+ switch (r.state) {
55
+ case "installed":
56
+ marker = "● installed";
57
+ break;
58
+ case "not-installed":
59
+ marker = "○ not installed";
60
+ break;
61
+ case "not-detected":
62
+ default:
63
+ marker = "· platform not detected";
64
+ break;
65
+ }
66
+ console.log(` ${label} ${marker}`);
67
+ }
68
+ }
69
+ console.log();
70
+ console.log(fmt.info("Use `rafter agent enable <id>` or `rafter agent disable <id>` to toggle individual components."));
71
+ });
72
+ }
@@ -47,6 +47,7 @@ export function createScanCommand() {
47
47
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
48
48
  .option("--baseline", "Filter findings present in the saved baseline")
49
49
  .option("--watch", "Watch for file changes and re-scan on change")
50
+ .option("--history", "Scan git history for secrets (requires gitleaks engine)")
50
51
  .action(async (scanPath, opts) => {
51
52
  // Validate flags before doing any work
52
53
  const validEngines = ["auto", "gitleaks", "patterns"];
@@ -103,7 +104,7 @@ export function createScanCommand() {
103
104
  if (!opts.quiet) {
104
105
  console.error(`Scanning directory: ${resolvedPath} (${engine})`);
105
106
  }
106
- results = await scanDirectory(resolvedPath, engine, scanCfg);
107
+ results = await scanDirectory(resolvedPath, engine, scanCfg, opts.history);
107
108
  }
108
109
  else {
109
110
  if (!opts.quiet) {
@@ -372,11 +373,11 @@ async function scanFile(filePath, engine, scanCfg) {
372
373
  /**
373
374
  * Scan a directory with selected engine
374
375
  */
375
- async function scanDirectory(dirPath, engine, scanCfg) {
376
+ async function scanDirectory(dirPath, engine, scanCfg, history) {
376
377
  if (engine === "gitleaks") {
377
378
  try {
378
379
  const gitleaks = new GitleaksScanner();
379
- return await gitleaks.scanDirectory(dirPath);
380
+ return await gitleaks.scanDirectory(dirPath, { useGit: history ?? false });
380
381
  }
381
382
  catch (e) {
382
383
  console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
@@ -193,7 +193,7 @@ export function createVerifyCommand() {
193
193
  console.log(fmt.success(`${passed.length}/${results.length} core checks passed${warnNote}`));
194
194
  }
195
195
  else {
196
- console.log(fmt.error(`${hardFailed.length} check${hardFailed.length > 1 ? "s" : ""} failed`));
196
+ console.log(fmt.error(`${passed.length}/${results.length} checks passed ${hardFailed.length} failed`));
197
197
  }
198
198
  console.log();
199
199
  if (hardFailed.length > 0) {
@@ -9,6 +9,7 @@ import { handleScanStatus } from "./scan-status.js";
9
9
  */
10
10
  export async function runRemoteScan(opts) {
11
11
  const key = resolveKey(opts.apiKey);
12
+ const ghToken = opts.githubToken || process.env.RAFTER_GITHUB_TOKEN;
12
13
  let repo, branch;
13
14
  try {
14
15
  ({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
@@ -22,10 +23,17 @@ export async function runRemoteScan(opts) {
22
23
  }
23
24
  process.exit(EXIT_GENERAL_ERROR);
24
25
  }
26
+ const body = {
27
+ repository_name: repo,
28
+ branch_name: branch,
29
+ scan_mode: opts.mode ?? "fast",
30
+ };
31
+ if (ghToken)
32
+ body.github_token = ghToken;
25
33
  if (!opts.quiet) {
26
34
  const spinner = ora("Submitting scan").start();
27
35
  try {
28
- const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch, scan_mode: opts.mode ?? "fast" }, { headers: { "x-api-key": key } });
36
+ const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
29
37
  spinner.succeed(`Scan ID: ${data.scan_id}`);
30
38
  if (opts.skipInteractive)
31
39
  return;
@@ -56,7 +64,7 @@ export async function runRemoteScan(opts) {
56
64
  }
57
65
  else {
58
66
  try {
59
- const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch, scan_mode: opts.mode ?? "fast" }, { headers: { "x-api-key": key } });
67
+ const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
60
68
  if (opts.skipInteractive)
61
69
  return;
62
70
  const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format ?? "md", opts.quiet);
@@ -90,12 +98,13 @@ function addRunOptions(cmd) {
90
98
  .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
91
99
  .option("-f, --format <format>", "json | md", "md")
92
100
  .option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
101
+ .option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
93
102
  .option("--skip-interactive", "do not wait for scan to complete")
94
103
  .option("--quiet", "suppress status messages");
95
104
  }
96
105
  export function createRunCommand() {
97
106
  return addRunOptions(new Command("run")
98
- .description("Trigger a remote backend security scan")).action(async (opts) => {
107
+ .description("Trigger a remote security scan")).action(async (opts) => {
99
108
  await runRemoteScan(opts);
100
109
  });
101
110
  }
@@ -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;
@@ -9,6 +9,16 @@ function loadSkill(name) {
9
9
  // Strip YAML frontmatter
10
10
  return raw.replace(/^---[\s\S]*?---\n*/, "").trim();
11
11
  }
12
+ function loadSkillDoc(skill, doc) {
13
+ return readFileSync(join(RESOURCES_DIR, skill, "docs", `${doc}.md`), "utf-8").trim();
14
+ }
15
+ const RAFTER_SUBDOCS = [
16
+ { slug: "cli-reference", desc: "Full rafter CLI tree by category" },
17
+ { slug: "guardrails", desc: "PreToolUse hooks, risk tiers, overrides" },
18
+ { slug: "backend", desc: "Remote fast vs plus, setup, cost/latency" },
19
+ { slug: "shift-left", desc: "Pointers to secure-design & code-review skills" },
20
+ { slug: "finding-triage", desc: "How to read a finding and decide next steps" },
21
+ ];
12
22
  function extractSections(content, headings) {
13
23
  const lines = content.split("\n");
14
24
  const sections = [];
@@ -54,7 +64,7 @@ function buildTopics() {
54
64
  render: () => loadSkill("rafter-agent-security"),
55
65
  },
56
66
  scanning: {
57
- description: "Remote SAST/SCA code analysis via backend API",
67
+ description: "Remote SAST/SCA code analysis via Rafter API",
58
68
  render: () => loadSkill("rafter"),
59
69
  },
60
70
  commands: {
@@ -78,7 +88,7 @@ function buildTopics() {
78
88
  return [
79
89
  "# Rafter Command Reference",
80
90
  "",
81
- "## Backend (Remote Code Analysis)",
91
+ "## Remote Code Analysis",
82
92
  "",
83
93
  backCmds,
84
94
  "",
@@ -165,6 +175,13 @@ function buildTopics() {
165
175
  "the tools developers use every day.",
166
176
  ].join("\n"),
167
177
  },
178
+ ...Object.fromEntries(RAFTER_SUBDOCS.map(({ slug, desc }) => [
179
+ slug,
180
+ {
181
+ description: desc,
182
+ render: () => loadSkillDoc("rafter", slug),
183
+ },
184
+ ])),
168
185
  all: {
169
186
  description: "Everything — full security + scanning + setup briefing",
170
187
  render: () => {
@@ -503,6 +520,9 @@ function renderTopicList(topics) {
503
520
  lines.push(" rafter brief commands # full command reference");
504
521
  lines.push(" rafter brief setup/claude-code # Claude Code setup guide");
505
522
  lines.push(" rafter brief setup/generic # setup for any agent");
523
+ lines.push(" rafter brief cli-reference # full CLI tree");
524
+ lines.push(" rafter brief guardrails # hooks + risk tiers");
525
+ lines.push(" rafter brief finding-triage # interpret findings");
506
526
  lines.push(" rafter brief all # everything");
507
527
  return lines.join("\n");
508
528
  }
@@ -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@v1");
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() {
@@ -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'
@@ -0,0 +1,18 @@
1
+ import { Command } from "commander";
2
+ import { createDocsListCommand } from "./list.js";
3
+ import { createDocsShowCommand } from "./show.js";
4
+ export function createDocsCommand() {
5
+ const docs = new Command("docs")
6
+ .description("Repo-specific security docs declared in .rafter.yml")
7
+ .addHelpText("after", [
8
+ "",
9
+ "Examples:",
10
+ " $ rafter docs list",
11
+ " $ rafter docs list --tag owasp",
12
+ " $ rafter docs show secure-coding",
13
+ " $ rafter docs show secure-coding --refresh",
14
+ ].join("\n"));
15
+ docs.addCommand(createDocsListCommand());
16
+ docs.addCommand(createDocsShowCommand());
17
+ return docs;
18
+ }
@@ -0,0 +1,37 @@
1
+ import { Command } from "commander";
2
+ import { listDocs } from "../../core/docs-loader.js";
3
+ export function createDocsListCommand() {
4
+ return new Command("list")
5
+ .description("List security docs declared in .rafter.yml")
6
+ .option("--tag <tag>", "Filter to docs matching this tag")
7
+ .option("--json", "Output as JSON")
8
+ .action((opts) => {
9
+ const entries = listDocs().filter(d => !opts.tag || (Array.isArray(d.tags) && d.tags.includes(opts.tag)));
10
+ if (entries.length === 0) {
11
+ if (opts.json) {
12
+ process.stdout.write("[]\n");
13
+ }
14
+ else {
15
+ process.stderr.write("No docs configured in .rafter.yml\n");
16
+ }
17
+ process.exit(3);
18
+ }
19
+ if (opts.json) {
20
+ process.stdout.write(JSON.stringify(entries.map(e => ({
21
+ id: e.id,
22
+ source: e.source,
23
+ source_kind: e.sourceKind,
24
+ description: e.description || "",
25
+ tags: e.tags || [],
26
+ cache_status: e.cacheStatus,
27
+ })), null, 2) + "\n");
28
+ return;
29
+ }
30
+ for (const e of entries) {
31
+ const tags = (e.tags && e.tags.length) ? ` [${e.tags.join(", ")}]` : "";
32
+ const cache = e.sourceKind === "url" ? ` (${e.cacheStatus})` : "";
33
+ const desc = e.description ? ` — ${e.description}` : "";
34
+ process.stdout.write(`${e.id} ${e.source}${cache}${tags}${desc}\n`);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,64 @@
1
+ import { Command } from "commander";
2
+ import { resolveDocSelector, fetchDoc } from "../../core/docs-loader.js";
3
+ export function createDocsShowCommand() {
4
+ return new Command("show")
5
+ .description("Print the content of a doc by id or tag")
6
+ .argument("<id-or-tag>", "Doc id or tag selector")
7
+ .option("--refresh", "Force re-fetch for URL-backed docs (bypass cache)")
8
+ .option("--json", "Output as JSON (array of { id, source, content })")
9
+ .action(async (selector, opts) => {
10
+ const entries = resolveDocSelector(selector);
11
+ if (entries.length === 0) {
12
+ const { loadPolicy } = await import("../../core/policy-loader.js");
13
+ const policy = loadPolicy();
14
+ if (!policy || !policy.docs || policy.docs.length === 0) {
15
+ process.stderr.write("No docs configured in .rafter.yml\n");
16
+ process.exit(3);
17
+ }
18
+ process.stderr.write(`No doc matched id or tag: ${selector}\n`);
19
+ process.exit(2);
20
+ }
21
+ const results = [];
22
+ let anyError = false;
23
+ for (const entry of entries) {
24
+ try {
25
+ const fetched = await fetchDoc(entry, { refresh: opts.refresh });
26
+ results.push({
27
+ id: entry.id,
28
+ source: fetched.source,
29
+ source_kind: fetched.sourceKind,
30
+ stale: fetched.stale,
31
+ content: fetched.content,
32
+ });
33
+ if (fetched.stale) {
34
+ process.stderr.write(`Warning: ${entry.id} served from stale cache (fetch failed)\n`);
35
+ }
36
+ }
37
+ catch (err) {
38
+ anyError = true;
39
+ process.stderr.write(`Error: failed to fetch ${entry.id}: ${err.message || err}\n`);
40
+ }
41
+ }
42
+ if (results.length === 0) {
43
+ process.exit(1);
44
+ }
45
+ if (opts.json) {
46
+ process.stdout.write(JSON.stringify(results, null, 2) + "\n");
47
+ }
48
+ else if (results.length === 1) {
49
+ process.stdout.write(results[0].content);
50
+ if (!results[0].content.endsWith("\n"))
51
+ process.stdout.write("\n");
52
+ }
53
+ else {
54
+ for (const r of results) {
55
+ process.stdout.write(`\n===== ${r.id} (${r.source}) =====\n`);
56
+ process.stdout.write(r.content);
57
+ if (!r.content.endsWith("\n"))
58
+ process.stdout.write("\n");
59
+ }
60
+ }
61
+ if (anyError)
62
+ process.exit(1);
63
+ });
64
+ }
@@ -7,6 +7,7 @@ import { GitleaksScanner } from "../../scanners/gitleaks.js";
7
7
  import { CommandInterceptor } from "../../core/command-interceptor.js";
8
8
  import { AuditLogger } from "../../core/audit-logger.js";
9
9
  import { ConfigManager } from "../../core/config-manager.js";
10
+ import { listDocs, resolveDocSelector, fetchDoc } from "../../core/docs-loader.js";
10
11
  import { createRequire } from "module";
11
12
  const _require = createRequire(import.meta.url);
12
13
  const { version: CLI_VERSION } = _require("../../../package.json");
@@ -87,6 +88,28 @@ export function createServer() {
87
88
  },
88
89
  },
89
90
  },
91
+ {
92
+ name: "list_docs",
93
+ description: "List repo-specific security docs declared in .rafter.yml. Call this early in any security-relevant task to discover project-specific rules, threat models, or compliance policies the user expects agents to follow.",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ tag: { type: "string", description: "Filter to docs whose tags include this value" },
98
+ },
99
+ },
100
+ },
101
+ {
102
+ name: "get_doc",
103
+ description: "Return the content of a repo-specific security doc by id or tag. Use after list_docs to read a specific document.",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ id_or_tag: { type: "string", description: "Doc id or tag selector" },
108
+ refresh: { type: "boolean", description: "Force re-fetch for URL-backed docs (bypass cache)" },
109
+ },
110
+ required: ["id_or_tag"],
111
+ },
112
+ },
90
113
  ],
91
114
  }));
92
115
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -149,6 +172,44 @@ export function createServer() {
149
172
  const value = key ? manager.get(key) : manager.load();
150
173
  return textResult(value);
151
174
  }
175
+ case "list_docs": {
176
+ const tag = args?.tag;
177
+ const entries = listDocs().filter(d => !tag || (Array.isArray(d.tags) && d.tags.includes(tag)));
178
+ return textResult(entries.map(e => ({
179
+ id: e.id,
180
+ source: e.source,
181
+ source_kind: e.sourceKind,
182
+ description: e.description || "",
183
+ tags: e.tags || [],
184
+ cache_status: e.cacheStatus,
185
+ })));
186
+ }
187
+ case "get_doc": {
188
+ const selector = args?.id_or_tag;
189
+ if (!selector)
190
+ return errorResult("id_or_tag is required");
191
+ const matches = resolveDocSelector(selector);
192
+ if (matches.length === 0)
193
+ return errorResult(`No doc matched id or tag: ${selector}`);
194
+ const refresh = Boolean(args?.refresh);
195
+ const results = [];
196
+ for (const entry of matches) {
197
+ try {
198
+ const fetched = await fetchDoc(entry, { refresh });
199
+ results.push({
200
+ id: entry.id,
201
+ source: fetched.source,
202
+ source_kind: fetched.sourceKind,
203
+ stale: fetched.stale,
204
+ content: fetched.content,
205
+ });
206
+ }
207
+ catch (err) {
208
+ return errorResult(`Failed to fetch ${entry.id}: ${err.message || err}`);
209
+ }
210
+ }
211
+ return textResult(results);
212
+ }
152
213
  default:
153
214
  return errorResult(`Unknown tool: ${name}`);
154
215
  }
@@ -168,6 +229,12 @@ export function createServer() {
168
229
  description: "Active security policy (merged .rafter.yml + config)",
169
230
  mimeType: "application/json",
170
231
  },
232
+ {
233
+ uri: "rafter://docs",
234
+ name: "Rafter Docs",
235
+ description: "Repo-specific security docs declared in .rafter.yml (metadata only, no content)",
236
+ mimeType: "application/json",
237
+ },
171
238
  ],
172
239
  }));
173
240
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
@@ -190,6 +257,23 @@ export function createServer() {
190
257
  text: JSON.stringify(manager.loadWithPolicy(), null, 2),
191
258
  }],
192
259
  };
260
+ case "rafter://docs": {
261
+ const entries = listDocs().map(e => ({
262
+ id: e.id,
263
+ source: e.source,
264
+ source_kind: e.sourceKind,
265
+ description: e.description || "",
266
+ tags: e.tags || [],
267
+ cache_status: e.cacheStatus,
268
+ }));
269
+ return {
270
+ contents: [{
271
+ uri: "rafter://docs",
272
+ mimeType: "application/json",
273
+ text: JSON.stringify(entries, null, 2),
274
+ }],
275
+ };
276
+ }
193
277
  default:
194
278
  throw new Error(`Unknown resource: ${uri}`);
195
279
  }