@rafter-security/cli 0.7.0 → 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 (56) hide show
  1. package/README.md +20 -1
  2. package/dist/commands/agent/audit-skill.js +2 -1
  3. package/dist/commands/agent/audit.js +27 -0
  4. package/dist/commands/agent/components.js +800 -0
  5. package/dist/commands/agent/disable.js +47 -0
  6. package/dist/commands/agent/enable.js +50 -0
  7. package/dist/commands/agent/index.js +6 -0
  8. package/dist/commands/agent/init.js +162 -164
  9. package/dist/commands/agent/list.js +72 -0
  10. package/dist/commands/brief.js +20 -0
  11. package/dist/commands/docs/index.js +18 -0
  12. package/dist/commands/docs/list.js +37 -0
  13. package/dist/commands/docs/show.js +64 -0
  14. package/dist/commands/mcp/server.js +84 -0
  15. package/dist/commands/skill/index.js +14 -0
  16. package/dist/commands/skill/install.js +89 -0
  17. package/dist/commands/skill/list.js +79 -0
  18. package/dist/commands/skill/registry.js +273 -0
  19. package/dist/commands/skill/remote.js +333 -0
  20. package/dist/commands/skill/review.js +975 -0
  21. package/dist/commands/skill/uninstall.js +65 -0
  22. package/dist/core/audit-logger.js +262 -21
  23. package/dist/core/config-manager.js +3 -0
  24. package/dist/core/docs-loader.js +148 -0
  25. package/dist/core/policy-loader.js +72 -1
  26. package/dist/index.js +6 -0
  27. package/package.json +1 -1
  28. package/resources/skills/rafter/SKILL.md +76 -96
  29. package/resources/skills/rafter/docs/backend.md +106 -0
  30. package/resources/skills/rafter/docs/cli-reference.md +199 -0
  31. package/resources/skills/rafter/docs/finding-triage.md +79 -0
  32. package/resources/skills/rafter/docs/guardrails.md +91 -0
  33. package/resources/skills/rafter/docs/shift-left.md +64 -0
  34. package/resources/skills/rafter-code-review/SKILL.md +91 -0
  35. package/resources/skills/rafter-code-review/docs/api.md +90 -0
  36. package/resources/skills/rafter-code-review/docs/asvs.md +120 -0
  37. package/resources/skills/rafter-code-review/docs/cwe-top25.md +78 -0
  38. package/resources/skills/rafter-code-review/docs/investigation-playbook.md +101 -0
  39. package/resources/skills/rafter-code-review/docs/llm.md +87 -0
  40. package/resources/skills/rafter-code-review/docs/web-app.md +84 -0
  41. package/resources/skills/rafter-secure-design/SKILL.md +103 -0
  42. package/resources/skills/rafter-secure-design/docs/api-design.md +97 -0
  43. package/resources/skills/rafter-secure-design/docs/auth.md +67 -0
  44. package/resources/skills/rafter-secure-design/docs/data-storage.md +90 -0
  45. package/resources/skills/rafter-secure-design/docs/dependencies.md +101 -0
  46. package/resources/skills/rafter-secure-design/docs/deployment.md +104 -0
  47. package/resources/skills/rafter-secure-design/docs/ingestion.md +98 -0
  48. package/resources/skills/rafter-secure-design/docs/standards-pointers.md +102 -0
  49. package/resources/skills/rafter-secure-design/docs/threat-modeling.md +128 -0
  50. package/resources/skills/rafter-skill-review/SKILL.md +106 -0
  51. package/resources/skills/rafter-skill-review/docs/authorship-provenance.md +82 -0
  52. package/resources/skills/rafter-skill-review/docs/changelog-review.md +99 -0
  53. package/resources/skills/rafter-skill-review/docs/data-practices.md +88 -0
  54. package/resources/skills/rafter-skill-review/docs/malware-indicators.md +79 -0
  55. package/resources/skills/rafter-skill-review/docs/prompt-injection.md +85 -0
  56. package/resources/skills/rafter-skill-review/docs/telemetry.md +78 -0
@@ -0,0 +1,800 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { RAFTER_MARKER_START, RAFTER_MARKER_END, injectInstructionFile, } from "./instruction-block.js";
5
+ import { ConfigManager } from "../../core/config-manager.js";
6
+ import { fileURLToPath } from "url";
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const RAFTER_MCP_ENTRY = {
10
+ command: "rafter",
11
+ args: ["mcp", "serve"],
12
+ };
13
+ // ── helpers ────────────────────────────────────────────────────────────
14
+ function readJson(p) {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(p, "utf-8"));
17
+ }
18
+ catch {
19
+ return {};
20
+ }
21
+ }
22
+ function writeJson(p, obj) {
23
+ const dir = path.dirname(p);
24
+ if (!fs.existsSync(dir))
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ fs.writeFileSync(p, JSON.stringify(obj, null, 2), "utf-8");
27
+ }
28
+ function filterOutRafter(arr, pred) {
29
+ if (!Array.isArray(arr))
30
+ return [];
31
+ return arr.filter((entry) => !pred(entry));
32
+ }
33
+ /** Remove a key from an object and return whether it existed. */
34
+ function removeKey(obj, key) {
35
+ if (!obj || !(key in obj))
36
+ return false;
37
+ delete obj[key];
38
+ return true;
39
+ }
40
+ /** True when a hook entry (matcher + hooks array) contains a rafter command matching a prefix. */
41
+ function hookEntryMatchesRafter(entry, prefix) {
42
+ const hooks = entry?.hooks;
43
+ if (!Array.isArray(hooks))
44
+ return false;
45
+ return hooks.some((h) => String(h?.command ?? "").startsWith(prefix));
46
+ }
47
+ /** Strip the rafter marker block from a text file, writing only if the file actually changed. */
48
+ function stripMarkerBlock(filePath) {
49
+ if (!fs.existsSync(filePath))
50
+ return false;
51
+ const content = fs.readFileSync(filePath, "utf-8");
52
+ const startIdx = content.indexOf(RAFTER_MARKER_START);
53
+ const endIdx = content.indexOf(RAFTER_MARKER_END);
54
+ if (startIdx === -1 || endIdx === -1)
55
+ return false;
56
+ const before = content.slice(0, startIdx).trimEnd();
57
+ const after = content.slice(endIdx + RAFTER_MARKER_END.length);
58
+ const trailing = after.replace(/^\s*\n+/, "");
59
+ const next = (before ? before + "\n" : "") + trailing;
60
+ fs.writeFileSync(filePath, next, "utf-8");
61
+ return true;
62
+ }
63
+ /** True when a file exists and contains the rafter marker block. */
64
+ function hasMarkerBlock(filePath) {
65
+ if (!fs.existsSync(filePath))
66
+ return false;
67
+ const content = fs.readFileSync(filePath, "utf-8");
68
+ return content.includes(RAFTER_MARKER_START) && content.includes(RAFTER_MARKER_END);
69
+ }
70
+ // ── per-component implementations ──────────────────────────────────────
71
+ function claudeCodeHooks() {
72
+ const home = os.homedir();
73
+ const settingsPath = path.join(home, ".claude", "settings.json");
74
+ return {
75
+ id: "claude-code.hooks",
76
+ platform: "claude-code",
77
+ kind: "hooks",
78
+ description: "Claude Code PreToolUse + PostToolUse hooks",
79
+ detectDir: path.join(home, ".claude"),
80
+ path: settingsPath,
81
+ isInstalled: () => {
82
+ if (!fs.existsSync(settingsPath))
83
+ return false;
84
+ const s = readJson(settingsPath);
85
+ const pre = s.hooks?.PreToolUse ?? [];
86
+ for (const entry of pre) {
87
+ if (hookEntryMatchesRafter(entry, "rafter hook pretool"))
88
+ return true;
89
+ }
90
+ return false;
91
+ },
92
+ install: () => {
93
+ var _a, _b;
94
+ if (!fs.existsSync(path.join(home, ".claude"))) {
95
+ fs.mkdirSync(path.join(home, ".claude"), { recursive: true });
96
+ }
97
+ const settings = fs.existsSync(settingsPath)
98
+ ? readJson(settingsPath)
99
+ : {};
100
+ settings.hooks ?? (settings.hooks = {});
101
+ (_a = settings.hooks).PreToolUse ?? (_a.PreToolUse = []);
102
+ (_b = settings.hooks).PostToolUse ?? (_b.PostToolUse = []);
103
+ const pre = { type: "command", command: "rafter hook pretool" };
104
+ const post = { type: "command", command: "rafter hook posttool" };
105
+ settings.hooks.PreToolUse = filterOutRafter(settings.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
106
+ settings.hooks.PostToolUse = filterOutRafter(settings.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
107
+ settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [pre] }, { matcher: "Write|Edit", hooks: [pre] });
108
+ settings.hooks.PostToolUse.push({ matcher: ".*", hooks: [post] });
109
+ writeJson(settingsPath, settings);
110
+ },
111
+ uninstall: () => {
112
+ if (!fs.existsSync(settingsPath))
113
+ return;
114
+ const settings = readJson(settingsPath);
115
+ if (settings.hooks?.PreToolUse) {
116
+ settings.hooks.PreToolUse = filterOutRafter(settings.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
117
+ }
118
+ if (settings.hooks?.PostToolUse) {
119
+ settings.hooks.PostToolUse = filterOutRafter(settings.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
120
+ }
121
+ writeJson(settingsPath, settings);
122
+ },
123
+ };
124
+ }
125
+ function claudeCodeInstructions() {
126
+ const home = os.homedir();
127
+ const filePath = path.join(home, ".claude", "CLAUDE.md");
128
+ return {
129
+ id: "claude-code.instructions",
130
+ platform: "claude-code",
131
+ kind: "instructions",
132
+ description: "Claude Code global instruction block (~/.claude/CLAUDE.md)",
133
+ detectDir: path.join(home, ".claude"),
134
+ path: filePath,
135
+ isInstalled: () => hasMarkerBlock(filePath),
136
+ install: () => {
137
+ injectInstructionFile(filePath);
138
+ },
139
+ uninstall: () => {
140
+ stripMarkerBlock(filePath);
141
+ },
142
+ };
143
+ }
144
+ function skillTemplatePath(name) {
145
+ return path.join(__dirname, "..", "..", "..", "resources", "skills", name, "SKILL.md");
146
+ }
147
+ function skillsDirComponent(opts) {
148
+ const backendDest = path.join(opts.skillsBaseDir, "rafter", "SKILL.md");
149
+ const agentDest = path.join(opts.skillsBaseDir, "rafter-agent-security", "SKILL.md");
150
+ return {
151
+ id: opts.id,
152
+ platform: opts.platform,
153
+ kind: "skills",
154
+ description: opts.description,
155
+ detectDir: opts.detectDir,
156
+ path: opts.skillsBaseDir,
157
+ isInstalled: () => fs.existsSync(backendDest) || fs.existsSync(agentDest),
158
+ install: () => {
159
+ const pairs = [
160
+ [skillTemplatePath("rafter"), backendDest],
161
+ [skillTemplatePath("rafter-agent-security"), agentDest],
162
+ ];
163
+ for (const [src, dst] of pairs) {
164
+ if (!fs.existsSync(src))
165
+ continue;
166
+ const dir = path.dirname(dst);
167
+ if (!fs.existsSync(dir))
168
+ fs.mkdirSync(dir, { recursive: true });
169
+ fs.copyFileSync(src, dst);
170
+ }
171
+ },
172
+ uninstall: () => {
173
+ for (const p of [backendDest, agentDest]) {
174
+ if (fs.existsSync(p)) {
175
+ fs.rmSync(p, { force: true });
176
+ const dir = path.dirname(p);
177
+ try {
178
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) {
179
+ fs.rmdirSync(dir);
180
+ }
181
+ }
182
+ catch {
183
+ // non-empty or races — leave it
184
+ }
185
+ }
186
+ }
187
+ },
188
+ };
189
+ }
190
+ function claudeCodeSkills() {
191
+ const home = os.homedir();
192
+ return skillsDirComponent({
193
+ id: "claude-code.skills",
194
+ platform: "claude-code",
195
+ description: "Claude Code skills (rafter + rafter-agent-security)",
196
+ detectDir: path.join(home, ".claude"),
197
+ skillsBaseDir: path.join(home, ".claude", "skills"),
198
+ });
199
+ }
200
+ function codexSkills() {
201
+ const home = os.homedir();
202
+ return skillsDirComponent({
203
+ id: "codex.skills",
204
+ platform: "codex",
205
+ description: "Codex CLI skills (~/.agents/skills/rafter*)",
206
+ detectDir: path.join(home, ".codex"),
207
+ skillsBaseDir: path.join(home, ".agents", "skills"),
208
+ });
209
+ }
210
+ function codexHooks() {
211
+ const home = os.homedir();
212
+ const hooksPath = path.join(home, ".codex", "hooks.json");
213
+ return {
214
+ id: "codex.hooks",
215
+ platform: "codex",
216
+ kind: "hooks",
217
+ description: "Codex CLI hooks (~/.codex/hooks.json)",
218
+ detectDir: path.join(home, ".codex"),
219
+ path: hooksPath,
220
+ isInstalled: () => {
221
+ if (!fs.existsSync(hooksPath))
222
+ return false;
223
+ const cfg = readJson(hooksPath);
224
+ for (const entry of cfg.hooks?.PreToolUse ?? []) {
225
+ if (hookEntryMatchesRafter(entry, "rafter hook pretool"))
226
+ return true;
227
+ }
228
+ return false;
229
+ },
230
+ install: () => {
231
+ var _a, _b;
232
+ const dir = path.join(home, ".codex");
233
+ if (!fs.existsSync(dir))
234
+ fs.mkdirSync(dir, { recursive: true });
235
+ const cfg = fs.existsSync(hooksPath) ? readJson(hooksPath) : {};
236
+ cfg.hooks ?? (cfg.hooks = {});
237
+ (_a = cfg.hooks).PreToolUse ?? (_a.PreToolUse = []);
238
+ (_b = cfg.hooks).PostToolUse ?? (_b.PostToolUse = []);
239
+ const pre = { type: "command", command: "rafter hook pretool" };
240
+ const post = { type: "command", command: "rafter hook posttool" };
241
+ cfg.hooks.PreToolUse = filterOutRafter(cfg.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
242
+ cfg.hooks.PostToolUse = filterOutRafter(cfg.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
243
+ cfg.hooks.PreToolUse.push({ matcher: "Bash", hooks: [pre] });
244
+ cfg.hooks.PostToolUse.push({ matcher: ".*", hooks: [post] });
245
+ writeJson(hooksPath, cfg);
246
+ },
247
+ uninstall: () => {
248
+ if (!fs.existsSync(hooksPath))
249
+ return;
250
+ const cfg = readJson(hooksPath);
251
+ if (cfg.hooks?.PreToolUse) {
252
+ cfg.hooks.PreToolUse = filterOutRafter(cfg.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
253
+ }
254
+ if (cfg.hooks?.PostToolUse) {
255
+ cfg.hooks.PostToolUse = filterOutRafter(cfg.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
256
+ }
257
+ writeJson(hooksPath, cfg);
258
+ },
259
+ };
260
+ }
261
+ function cursorHooks() {
262
+ const home = os.homedir();
263
+ const hooksPath = path.join(home, ".cursor", "hooks.json");
264
+ return {
265
+ id: "cursor.hooks",
266
+ platform: "cursor",
267
+ kind: "hooks",
268
+ description: "Cursor hooks (~/.cursor/hooks.json)",
269
+ detectDir: path.join(home, ".cursor"),
270
+ path: hooksPath,
271
+ isInstalled: () => {
272
+ if (!fs.existsSync(hooksPath))
273
+ return false;
274
+ const cfg = readJson(hooksPath);
275
+ for (const entry of cfg.hooks?.beforeShellExecution ?? []) {
276
+ if (String(entry?.command ?? "").includes("rafter hook pretool"))
277
+ return true;
278
+ }
279
+ return false;
280
+ },
281
+ install: () => {
282
+ var _a;
283
+ const dir = path.join(home, ".cursor");
284
+ if (!fs.existsSync(dir))
285
+ fs.mkdirSync(dir, { recursive: true });
286
+ const cfg = fs.existsSync(hooksPath) ? readJson(hooksPath) : {};
287
+ cfg.version ?? (cfg.version = 1);
288
+ cfg.hooks ?? (cfg.hooks = {});
289
+ (_a = cfg.hooks).beforeShellExecution ?? (_a.beforeShellExecution = []);
290
+ cfg.hooks.beforeShellExecution = filterOutRafter(cfg.hooks.beforeShellExecution, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
291
+ cfg.hooks.beforeShellExecution.push({
292
+ command: "rafter hook pretool --format cursor",
293
+ type: "command",
294
+ timeout: 5000,
295
+ });
296
+ writeJson(hooksPath, cfg);
297
+ },
298
+ uninstall: () => {
299
+ if (!fs.existsSync(hooksPath))
300
+ return;
301
+ const cfg = readJson(hooksPath);
302
+ if (cfg.hooks?.beforeShellExecution) {
303
+ cfg.hooks.beforeShellExecution = filterOutRafter(cfg.hooks.beforeShellExecution, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
304
+ }
305
+ writeJson(hooksPath, cfg);
306
+ },
307
+ };
308
+ }
309
+ function cursorInstructions() {
310
+ const home = os.homedir();
311
+ const filePath = path.join(home, ".cursor", "rules", "rafter-security.mdc");
312
+ return {
313
+ id: "cursor.instructions",
314
+ platform: "cursor",
315
+ kind: "instructions",
316
+ description: "Cursor global rule block (~/.cursor/rules/rafter-security.mdc)",
317
+ detectDir: path.join(home, ".cursor"),
318
+ path: filePath,
319
+ isInstalled: () => hasMarkerBlock(filePath),
320
+ install: () => injectInstructionFile(filePath),
321
+ uninstall: () => {
322
+ if (!fs.existsSync(filePath))
323
+ return;
324
+ // This file is ours — delete it rather than editing around the block.
325
+ fs.rmSync(filePath, { force: true });
326
+ },
327
+ };
328
+ }
329
+ function cursorMcp() {
330
+ const home = os.homedir();
331
+ const mcpPath = path.join(home, ".cursor", "mcp.json");
332
+ return {
333
+ id: "cursor.mcp",
334
+ platform: "cursor",
335
+ kind: "mcp",
336
+ description: "Cursor MCP server entry (~/.cursor/mcp.json)",
337
+ detectDir: path.join(home, ".cursor"),
338
+ path: mcpPath,
339
+ isInstalled: () => {
340
+ if (!fs.existsSync(mcpPath))
341
+ return false;
342
+ const cfg = readJson(mcpPath);
343
+ return !!cfg.mcpServers?.rafter;
344
+ },
345
+ install: () => {
346
+ const dir = path.join(home, ".cursor");
347
+ if (!fs.existsSync(dir))
348
+ fs.mkdirSync(dir, { recursive: true });
349
+ const cfg = fs.existsSync(mcpPath) ? readJson(mcpPath) : {};
350
+ cfg.mcpServers ?? (cfg.mcpServers = {});
351
+ cfg.mcpServers.rafter = { ...RAFTER_MCP_ENTRY };
352
+ writeJson(mcpPath, cfg);
353
+ },
354
+ uninstall: () => {
355
+ if (!fs.existsSync(mcpPath))
356
+ return;
357
+ const cfg = readJson(mcpPath);
358
+ if (removeKey(cfg.mcpServers, "rafter"))
359
+ writeJson(mcpPath, cfg);
360
+ },
361
+ };
362
+ }
363
+ function geminiHooks() {
364
+ const home = os.homedir();
365
+ const settingsPath = path.join(home, ".gemini", "settings.json");
366
+ return {
367
+ id: "gemini.hooks",
368
+ platform: "gemini",
369
+ kind: "hooks",
370
+ description: "Gemini CLI BeforeTool + AfterTool hooks",
371
+ detectDir: path.join(home, ".gemini"),
372
+ path: settingsPath,
373
+ isInstalled: () => {
374
+ if (!fs.existsSync(settingsPath))
375
+ return false;
376
+ const s = readJson(settingsPath);
377
+ for (const entry of s.hooks?.BeforeTool ?? []) {
378
+ if (hookEntryMatchesRafter(entry, "rafter hook pretool"))
379
+ return true;
380
+ }
381
+ return false;
382
+ },
383
+ install: () => {
384
+ var _a, _b;
385
+ const dir = path.join(home, ".gemini");
386
+ if (!fs.existsSync(dir))
387
+ fs.mkdirSync(dir, { recursive: true });
388
+ const s = fs.existsSync(settingsPath) ? readJson(settingsPath) : {};
389
+ s.hooks ?? (s.hooks = {});
390
+ (_a = s.hooks).BeforeTool ?? (_a.BeforeTool = []);
391
+ (_b = s.hooks).AfterTool ?? (_b.AfterTool = []);
392
+ s.hooks.BeforeTool = filterOutRafter(s.hooks.BeforeTool, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
393
+ s.hooks.AfterTool = filterOutRafter(s.hooks.AfterTool, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
394
+ s.hooks.BeforeTool.push({
395
+ matcher: "shell|write_file",
396
+ hooks: [{ type: "command", command: "rafter hook pretool --format gemini", timeout: 5000 }],
397
+ });
398
+ s.hooks.AfterTool.push({
399
+ matcher: ".*",
400
+ hooks: [{ type: "command", command: "rafter hook posttool --format gemini", timeout: 5000 }],
401
+ });
402
+ writeJson(settingsPath, s);
403
+ },
404
+ uninstall: () => {
405
+ if (!fs.existsSync(settingsPath))
406
+ return;
407
+ const s = readJson(settingsPath);
408
+ if (s.hooks?.BeforeTool) {
409
+ s.hooks.BeforeTool = filterOutRafter(s.hooks.BeforeTool, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
410
+ }
411
+ if (s.hooks?.AfterTool) {
412
+ s.hooks.AfterTool = filterOutRafter(s.hooks.AfterTool, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
413
+ }
414
+ writeJson(settingsPath, s);
415
+ },
416
+ };
417
+ }
418
+ function geminiMcp() {
419
+ const home = os.homedir();
420
+ const settingsPath = path.join(home, ".gemini", "settings.json");
421
+ return {
422
+ id: "gemini.mcp",
423
+ platform: "gemini",
424
+ kind: "mcp",
425
+ description: "Gemini CLI MCP server entry (~/.gemini/settings.json)",
426
+ detectDir: path.join(home, ".gemini"),
427
+ path: settingsPath,
428
+ isInstalled: () => {
429
+ if (!fs.existsSync(settingsPath))
430
+ return false;
431
+ const s = readJson(settingsPath);
432
+ return !!s.mcpServers?.rafter;
433
+ },
434
+ install: () => {
435
+ const dir = path.join(home, ".gemini");
436
+ if (!fs.existsSync(dir))
437
+ fs.mkdirSync(dir, { recursive: true });
438
+ const s = fs.existsSync(settingsPath) ? readJson(settingsPath) : {};
439
+ s.mcpServers ?? (s.mcpServers = {});
440
+ s.mcpServers.rafter = { ...RAFTER_MCP_ENTRY };
441
+ writeJson(settingsPath, s);
442
+ },
443
+ uninstall: () => {
444
+ if (!fs.existsSync(settingsPath))
445
+ return;
446
+ const s = readJson(settingsPath);
447
+ if (removeKey(s.mcpServers, "rafter"))
448
+ writeJson(settingsPath, s);
449
+ },
450
+ };
451
+ }
452
+ function windsurfHooks() {
453
+ const home = os.homedir();
454
+ const hooksPath = path.join(home, ".windsurf", "hooks.json");
455
+ return {
456
+ id: "windsurf.hooks",
457
+ platform: "windsurf",
458
+ kind: "hooks",
459
+ description: "Windsurf hooks (~/.windsurf/hooks.json)",
460
+ detectDir: path.join(home, ".codeium", "windsurf"),
461
+ path: hooksPath,
462
+ isInstalled: () => {
463
+ if (!fs.existsSync(hooksPath))
464
+ return false;
465
+ const cfg = readJson(hooksPath);
466
+ for (const entry of cfg.hooks?.pre_run_command ?? []) {
467
+ if (String(entry?.command ?? "").includes("rafter hook pretool"))
468
+ return true;
469
+ }
470
+ return false;
471
+ },
472
+ install: () => {
473
+ var _a, _b;
474
+ const dir = path.join(home, ".windsurf");
475
+ if (!fs.existsSync(dir))
476
+ fs.mkdirSync(dir, { recursive: true });
477
+ const cfg = fs.existsSync(hooksPath) ? readJson(hooksPath) : {};
478
+ cfg.hooks ?? (cfg.hooks = {});
479
+ (_a = cfg.hooks).pre_run_command ?? (_a.pre_run_command = []);
480
+ (_b = cfg.hooks).pre_write_code ?? (_b.pre_write_code = []);
481
+ cfg.hooks.pre_run_command = filterOutRafter(cfg.hooks.pre_run_command, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
482
+ cfg.hooks.pre_write_code = filterOutRafter(cfg.hooks.pre_write_code, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
483
+ cfg.hooks.pre_run_command.push({
484
+ command: "rafter hook pretool --format windsurf",
485
+ show_output: true,
486
+ });
487
+ cfg.hooks.pre_write_code.push({
488
+ command: "rafter hook pretool --format windsurf",
489
+ show_output: true,
490
+ });
491
+ writeJson(hooksPath, cfg);
492
+ },
493
+ uninstall: () => {
494
+ if (!fs.existsSync(hooksPath))
495
+ return;
496
+ const cfg = readJson(hooksPath);
497
+ if (cfg.hooks?.pre_run_command) {
498
+ cfg.hooks.pre_run_command = filterOutRafter(cfg.hooks.pre_run_command, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
499
+ }
500
+ if (cfg.hooks?.pre_write_code) {
501
+ cfg.hooks.pre_write_code = filterOutRafter(cfg.hooks.pre_write_code, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
502
+ }
503
+ writeJson(hooksPath, cfg);
504
+ },
505
+ };
506
+ }
507
+ function windsurfMcp() {
508
+ const home = os.homedir();
509
+ const mcpPath = path.join(home, ".codeium", "windsurf", "mcp_config.json");
510
+ return {
511
+ id: "windsurf.mcp",
512
+ platform: "windsurf",
513
+ kind: "mcp",
514
+ description: "Windsurf MCP server entry",
515
+ detectDir: path.join(home, ".codeium", "windsurf"),
516
+ path: mcpPath,
517
+ isInstalled: () => {
518
+ if (!fs.existsSync(mcpPath))
519
+ return false;
520
+ const cfg = readJson(mcpPath);
521
+ return !!cfg.mcpServers?.rafter;
522
+ },
523
+ install: () => {
524
+ const dir = path.join(home, ".codeium", "windsurf");
525
+ if (!fs.existsSync(dir))
526
+ fs.mkdirSync(dir, { recursive: true });
527
+ const cfg = fs.existsSync(mcpPath) ? readJson(mcpPath) : {};
528
+ cfg.mcpServers ?? (cfg.mcpServers = {});
529
+ cfg.mcpServers.rafter = { ...RAFTER_MCP_ENTRY };
530
+ writeJson(mcpPath, cfg);
531
+ },
532
+ uninstall: () => {
533
+ if (!fs.existsSync(mcpPath))
534
+ return;
535
+ const cfg = readJson(mcpPath);
536
+ if (removeKey(cfg.mcpServers, "rafter"))
537
+ writeJson(mcpPath, cfg);
538
+ },
539
+ };
540
+ }
541
+ function continueHooks() {
542
+ const home = os.homedir();
543
+ const settingsPath = path.join(home, ".continue", "settings.json");
544
+ return {
545
+ id: "continue.hooks",
546
+ platform: "continue",
547
+ kind: "hooks",
548
+ description: "Continue.dev PreToolUse + PostToolUse hooks",
549
+ detectDir: path.join(home, ".continue"),
550
+ path: settingsPath,
551
+ isInstalled: () => {
552
+ if (!fs.existsSync(settingsPath))
553
+ return false;
554
+ const s = readJson(settingsPath);
555
+ for (const entry of s.hooks?.PreToolUse ?? []) {
556
+ if (hookEntryMatchesRafter(entry, "rafter hook pretool"))
557
+ return true;
558
+ }
559
+ return false;
560
+ },
561
+ install: () => {
562
+ var _a, _b;
563
+ const dir = path.join(home, ".continue");
564
+ if (!fs.existsSync(dir))
565
+ fs.mkdirSync(dir, { recursive: true });
566
+ const s = fs.existsSync(settingsPath) ? readJson(settingsPath) : {};
567
+ s.hooks ?? (s.hooks = {});
568
+ (_a = s.hooks).PreToolUse ?? (_a.PreToolUse = []);
569
+ (_b = s.hooks).PostToolUse ?? (_b.PostToolUse = []);
570
+ const pre = { type: "command", command: "rafter hook pretool" };
571
+ const post = { type: "command", command: "rafter hook posttool" };
572
+ s.hooks.PreToolUse = filterOutRafter(s.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
573
+ s.hooks.PostToolUse = filterOutRafter(s.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
574
+ s.hooks.PreToolUse.push({ matcher: "Bash", hooks: [pre] }, { matcher: "Write|Edit", hooks: [pre] });
575
+ s.hooks.PostToolUse.push({ matcher: ".*", hooks: [post] });
576
+ writeJson(settingsPath, s);
577
+ },
578
+ uninstall: () => {
579
+ if (!fs.existsSync(settingsPath))
580
+ return;
581
+ const s = readJson(settingsPath);
582
+ if (s.hooks?.PreToolUse) {
583
+ s.hooks.PreToolUse = filterOutRafter(s.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
584
+ }
585
+ if (s.hooks?.PostToolUse) {
586
+ s.hooks.PostToolUse = filterOutRafter(s.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
587
+ }
588
+ writeJson(settingsPath, s);
589
+ },
590
+ };
591
+ }
592
+ function continueMcp() {
593
+ const home = os.homedir();
594
+ const configPath = path.join(home, ".continue", "config.json");
595
+ return {
596
+ id: "continue.mcp",
597
+ platform: "continue",
598
+ kind: "mcp",
599
+ description: "Continue.dev MCP server entry (~/.continue/config.json)",
600
+ detectDir: path.join(home, ".continue"),
601
+ path: configPath,
602
+ isInstalled: () => {
603
+ if (!fs.existsSync(configPath))
604
+ return false;
605
+ const cfg = readJson(configPath);
606
+ const servers = cfg.mcpServers;
607
+ if (Array.isArray(servers))
608
+ return servers.some((s) => s?.name === "rafter");
609
+ if (servers && typeof servers === "object")
610
+ return !!servers.rafter;
611
+ return false;
612
+ },
613
+ install: () => {
614
+ const dir = path.join(home, ".continue");
615
+ if (!fs.existsSync(dir))
616
+ fs.mkdirSync(dir, { recursive: true });
617
+ const cfg = fs.existsSync(configPath) ? readJson(configPath) : {};
618
+ if (Array.isArray(cfg.mcpServers)) {
619
+ cfg.mcpServers = cfg.mcpServers.filter((s) => s?.name !== "rafter");
620
+ cfg.mcpServers.push({ name: "rafter", ...RAFTER_MCP_ENTRY });
621
+ }
622
+ else {
623
+ cfg.mcpServers ?? (cfg.mcpServers = {});
624
+ cfg.mcpServers.rafter = { ...RAFTER_MCP_ENTRY };
625
+ }
626
+ writeJson(configPath, cfg);
627
+ },
628
+ uninstall: () => {
629
+ if (!fs.existsSync(configPath))
630
+ return;
631
+ const cfg = readJson(configPath);
632
+ let changed = false;
633
+ if (Array.isArray(cfg.mcpServers)) {
634
+ const before = cfg.mcpServers.length;
635
+ cfg.mcpServers = cfg.mcpServers.filter((s) => s?.name !== "rafter");
636
+ changed = cfg.mcpServers.length !== before;
637
+ }
638
+ else if (cfg.mcpServers && typeof cfg.mcpServers === "object") {
639
+ changed = removeKey(cfg.mcpServers, "rafter");
640
+ }
641
+ if (changed)
642
+ writeJson(configPath, cfg);
643
+ },
644
+ };
645
+ }
646
+ function aiderMcp() {
647
+ const home = os.homedir();
648
+ const configPath = path.join(home, ".aider.conf.yml");
649
+ const mcpLineHeader = "# Rafter security MCP server";
650
+ return {
651
+ id: "aider.mcp",
652
+ platform: "aider",
653
+ kind: "mcp",
654
+ description: "Aider MCP server entry (~/.aider.conf.yml)",
655
+ // Aider has no config dir — its presence is the file itself. Point detectDir
656
+ // at $HOME so the platform is always considered "present enough to install into".
657
+ detectDir: home,
658
+ path: configPath,
659
+ isInstalled: () => {
660
+ if (!fs.existsSync(configPath))
661
+ return false;
662
+ return fs.readFileSync(configPath, "utf-8").includes("rafter mcp serve");
663
+ },
664
+ install: () => {
665
+ const content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf-8") : "";
666
+ if (content.includes("rafter mcp serve"))
667
+ return;
668
+ const block = `\n${mcpLineHeader}\nmcp-server-command: rafter mcp serve\n`;
669
+ fs.writeFileSync(configPath, content + block, "utf-8");
670
+ },
671
+ uninstall: () => {
672
+ if (!fs.existsSync(configPath))
673
+ return;
674
+ const content = fs.readFileSync(configPath, "utf-8");
675
+ // Remove both the comment marker and the command line; preserve everything else.
676
+ const lines = content.split("\n");
677
+ const next = lines.filter((l) => {
678
+ const t = l.trim();
679
+ if (t === mcpLineHeader)
680
+ return false;
681
+ if (t.startsWith("mcp-server-command:") && t.includes("rafter mcp serve"))
682
+ return false;
683
+ return true;
684
+ });
685
+ fs.writeFileSync(configPath, next.join("\n"), "utf-8");
686
+ },
687
+ };
688
+ }
689
+ function openclawSkill() {
690
+ const home = os.homedir();
691
+ const skillPath = path.join(home, ".openclaw", "skills", "rafter-security.md");
692
+ return {
693
+ id: "openclaw.skills",
694
+ platform: "openclaw",
695
+ kind: "skills",
696
+ description: "OpenClaw rafter-security skill",
697
+ detectDir: path.join(home, ".openclaw"),
698
+ path: skillPath,
699
+ isInstalled: () => fs.existsSync(skillPath),
700
+ install: () => {
701
+ const dir = path.dirname(skillPath);
702
+ if (!fs.existsSync(dir))
703
+ fs.mkdirSync(dir, { recursive: true });
704
+ const template = skillTemplatePath("rafter");
705
+ if (fs.existsSync(template)) {
706
+ fs.copyFileSync(template, skillPath);
707
+ }
708
+ },
709
+ uninstall: () => {
710
+ if (fs.existsSync(skillPath))
711
+ fs.rmSync(skillPath, { force: true });
712
+ },
713
+ };
714
+ }
715
+ // ── registry ───────────────────────────────────────────────────────────
716
+ let _registry = null;
717
+ export function getComponentRegistry() {
718
+ if (!_registry) {
719
+ _registry = [
720
+ claudeCodeHooks(),
721
+ claudeCodeInstructions(),
722
+ claudeCodeSkills(),
723
+ codexHooks(),
724
+ codexSkills(),
725
+ cursorHooks(),
726
+ cursorInstructions(),
727
+ cursorMcp(),
728
+ geminiHooks(),
729
+ geminiMcp(),
730
+ windsurfHooks(),
731
+ windsurfMcp(),
732
+ continueHooks(),
733
+ continueMcp(),
734
+ aiderMcp(),
735
+ openclawSkill(),
736
+ ];
737
+ }
738
+ return _registry;
739
+ }
740
+ /** Reset cached registry — tests use this when HOME changes. */
741
+ export function resetComponentRegistryCache() {
742
+ _registry = null;
743
+ }
744
+ export function resolveComponent(id) {
745
+ const normalized = id.trim().toLowerCase();
746
+ // Allow short aliases: "claude" for "claude-code", "continuedev" for "continue".
747
+ const aliased = normalized
748
+ .replace(/^claude\./, "claude-code.")
749
+ .replace(/^continuedev\./, "continue.");
750
+ return getComponentRegistry().find((c) => c.id === aliased);
751
+ }
752
+ /** Produce a structured status snapshot for every registered component. */
753
+ export function snapshotComponents() {
754
+ const registry = getComponentRegistry();
755
+ const cm = new ConfigManager();
756
+ let cfg = {};
757
+ try {
758
+ cfg = cm.load();
759
+ }
760
+ catch {
761
+ cfg = {};
762
+ }
763
+ const components = cfg.agent?.components ?? {};
764
+ return registry.map((c) => {
765
+ const detected = fs.existsSync(c.detectDir);
766
+ const installed = c.isInstalled();
767
+ const configEntry = components[c.id];
768
+ const configEnabled = configEntry?.enabled ?? installed;
769
+ const state = installed
770
+ ? "installed"
771
+ : detected
772
+ ? "not-installed"
773
+ : "not-detected";
774
+ return {
775
+ id: c.id,
776
+ platform: c.platform,
777
+ kind: c.kind,
778
+ description: c.description,
779
+ path: c.path,
780
+ state,
781
+ installed,
782
+ detected,
783
+ configEnabled,
784
+ };
785
+ });
786
+ }
787
+ /**
788
+ * Update ~/.rafter/config.json to record that `id` was just installed (enabled=true)
789
+ * or uninstalled (enabled=false). Safe to call multiple times; idempotent.
790
+ *
791
+ * Writes the full `agent.components` map in one shot rather than using dot-notation
792
+ * `set("agent.components.<id>", ...)` — component IDs contain dots which the
793
+ * dot-path setter would otherwise split into nested keys.
794
+ */
795
+ export function recordComponentState(id, enabled) {
796
+ const cm = new ConfigManager();
797
+ const existing = (cm.get("agent.components") ?? {});
798
+ existing[id] = { enabled, updatedAt: new Date().toISOString() };
799
+ cm.set("agent.components", existing);
800
+ }