@rafter-security/cli 0.7.7 → 0.7.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.
@@ -1,6 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import os from "os";
4
+ import yaml from "js-yaml";
4
5
  import { RAFTER_MARKER_START, RAFTER_MARKER_END, injectInstructionFile, } from "./instruction-block.js";
5
6
  import { ConfigManager } from "../../core/config-manager.js";
6
7
  import { fileURLToPath } from "url";
@@ -259,7 +260,8 @@ function codexHooks() {
259
260
  const post = { type: "command", command: "rafter hook posttool" };
260
261
  cfg.hooks.PreToolUse = filterOutRafter(cfg.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
261
262
  cfg.hooks.PostToolUse = filterOutRafter(cfg.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
262
- cfg.hooks.PreToolUse.push({ matcher: "Bash", hooks: [pre] });
263
+ // Bash + apply_patch per Codex hook docs (rf-ovql verification).
264
+ cfg.hooks.PreToolUse.push({ matcher: "Bash|apply_patch", hooks: [pre] });
263
265
  cfg.hooks.PostToolUse.push({ matcher: ".*", hooks: [post] });
264
266
  writeJson(hooksPath, cfg);
265
267
  },
@@ -323,6 +325,12 @@ function claudeCodeMcp() {
323
325
  },
324
326
  };
325
327
  }
328
+ /** Cursor hook events covered by rafter (rf-svn3). */
329
+ const CURSOR_HOOK_EVENTS = [
330
+ { event: "preToolUse", command: "rafter hook pretool --format cursor" },
331
+ { event: "postToolUse", command: "rafter hook posttool --format cursor" },
332
+ { event: "beforeShellExecution", command: "rafter hook pretool --format cursor" },
333
+ ];
326
334
  function cursorHooks() {
327
335
  const home = os.homedir();
328
336
  const hooksPath = path.join(home, ".cursor", "hooks.json");
@@ -330,16 +338,18 @@ function cursorHooks() {
330
338
  id: "cursor.hooks",
331
339
  platform: "cursor",
332
340
  kind: "hooks",
333
- description: "Cursor hooks (~/.cursor/hooks.json)",
341
+ description: "Cursor hooks: preToolUse + postToolUse + beforeShellExecution (~/.cursor/hooks.json)",
334
342
  detectDir: path.join(home, ".cursor"),
335
343
  path: hooksPath,
336
344
  isInstalled: () => {
337
345
  if (!fs.existsSync(hooksPath))
338
346
  return false;
339
347
  const cfg = readJson(hooksPath);
340
- for (const entry of cfg.hooks?.beforeShellExecution ?? []) {
341
- if (String(entry?.command ?? "").includes("rafter hook pretool"))
342
- return true;
348
+ for (const { event } of CURSOR_HOOK_EVENTS) {
349
+ for (const entry of cfg.hooks?.[event] ?? []) {
350
+ if (String(entry?.command ?? "").includes("rafter hook"))
351
+ return true;
352
+ }
343
353
  }
344
354
  return false;
345
355
  },
@@ -351,46 +361,123 @@ function cursorHooks() {
351
361
  const cfg = fs.existsSync(hooksPath) ? readJson(hooksPath) : {};
352
362
  cfg.version ?? (cfg.version = 1);
353
363
  cfg.hooks ?? (cfg.hooks = {});
354
- (_a = cfg.hooks).beforeShellExecution ?? (_a.beforeShellExecution = []);
355
- cfg.hooks.beforeShellExecution = filterOutRafter(cfg.hooks.beforeShellExecution, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
356
- cfg.hooks.beforeShellExecution.push({
357
- command: "rafter hook pretool --format cursor",
358
- type: "command",
359
- timeout: 5000,
360
- });
364
+ for (const { event, command } of CURSOR_HOOK_EVENTS) {
365
+ (_a = cfg.hooks)[event] ?? (_a[event] = []);
366
+ cfg.hooks[event] = filterOutRafter(cfg.hooks[event], (e) => String(e?.command ?? "").includes("rafter hook"));
367
+ cfg.hooks[event].push({ command, type: "command", timeout: 5000 });
368
+ }
361
369
  writeJson(hooksPath, cfg);
362
370
  },
363
371
  uninstall: () => {
364
372
  if (!fs.existsSync(hooksPath))
365
373
  return;
366
374
  const cfg = readJson(hooksPath);
367
- if (cfg.hooks?.beforeShellExecution) {
368
- cfg.hooks.beforeShellExecution = filterOutRafter(cfg.hooks.beforeShellExecution, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
375
+ for (const { event } of CURSOR_HOOK_EVENTS) {
376
+ if (cfg.hooks?.[event]) {
377
+ cfg.hooks[event] = filterOutRafter(cfg.hooks[event], (e) => String(e?.command ?? "").includes("rafter hook"));
378
+ }
369
379
  }
370
380
  writeJson(hooksPath, cfg);
371
381
  },
372
382
  };
373
383
  }
384
+ const CURSOR_RULE_SKILLS = [
385
+ "rafter",
386
+ "rafter-secure-design",
387
+ "rafter-code-review",
388
+ "rafter-skill-review",
389
+ ];
390
+ function cursorRuleSourceDir() {
391
+ // After build: dist/commands/agent/components.js -> ../../../resources/cursor-rules
392
+ const candidates = [
393
+ path.resolve(__dirname, "..", "..", "..", "resources", "cursor-rules"),
394
+ path.resolve(__dirname, "..", "..", "resources", "cursor-rules"),
395
+ ];
396
+ return candidates.find((p) => fs.existsSync(p)) ?? null;
397
+ }
398
+ function cursorAgentSourceFile() {
399
+ const candidates = [
400
+ path.resolve(__dirname, "..", "..", "..", "resources", "agents", "rafter.md"),
401
+ path.resolve(__dirname, "..", "..", "resources", "agents", "rafter.md"),
402
+ ];
403
+ return candidates.find((p) => fs.existsSync(p)) ?? null;
404
+ }
405
+ /**
406
+ * Cursor instructions = per-skill rules under .cursor/rules/ + the rafter
407
+ * sub-agent at .cursor/agents/rafter.md (rf-svn3). The legacy consolidated
408
+ * rafter-security.mdc was retired.
409
+ *
410
+ * `path` reports the rules dir for diagnostics; install/uninstall manage
411
+ * both rules and the sub-agent file together.
412
+ */
374
413
  function cursorInstructions() {
375
414
  const home = os.homedir();
376
- const filePath = path.join(home, ".cursor", "rules", "rafter-security.mdc");
415
+ const rulesDir = path.join(home, ".cursor", "rules");
416
+ const agentPath = path.join(home, ".cursor", "agents", "rafter.md");
417
+ const legacyPath = path.join(rulesDir, "rafter-security.mdc");
377
418
  return {
378
419
  id: "cursor.instructions",
379
420
  platform: "cursor",
380
421
  kind: "instructions",
381
- description: "Cursor global rule block (~/.cursor/rules/rafter-security.mdc)",
422
+ description: "Cursor per-skill rules + rafter sub-agent (~/.cursor/rules/, ~/.cursor/agents/rafter.md)",
382
423
  detectDir: path.join(home, ".cursor"),
383
- path: filePath,
384
- isInstalled: () => hasMarkerBlock(filePath),
385
- install: () => injectInstructionFile(filePath),
424
+ path: rulesDir,
425
+ isInstalled: () => {
426
+ const rulesPresent = CURSOR_RULE_SKILLS.every((n) => fs.existsSync(path.join(rulesDir, `${n}.mdc`)));
427
+ return rulesPresent && fs.existsSync(agentPath);
428
+ },
429
+ install: () => {
430
+ fs.mkdirSync(rulesDir, { recursive: true });
431
+ const ruleSrc = cursorRuleSourceDir();
432
+ if (ruleSrc) {
433
+ for (const name of CURSOR_RULE_SKILLS) {
434
+ const src = path.join(ruleSrc, `${name}.mdc`);
435
+ if (fs.existsSync(src)) {
436
+ fs.copyFileSync(src, path.join(rulesDir, `${name}.mdc`));
437
+ }
438
+ }
439
+ }
440
+ // Migrate away from the legacy consolidated rule.
441
+ if (fs.existsSync(legacyPath)) {
442
+ try {
443
+ fs.unlinkSync(legacyPath);
444
+ }
445
+ catch { /* best-effort */ }
446
+ }
447
+ const agentSrc = cursorAgentSourceFile();
448
+ if (agentSrc) {
449
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
450
+ const raw = fs.readFileSync(agentSrc, "utf-8");
451
+ const cursored = stripFrontmatterField(raw, "tools");
452
+ fs.writeFileSync(agentPath, cursored, "utf-8");
453
+ }
454
+ },
386
455
  uninstall: () => {
387
- if (!fs.existsSync(filePath))
388
- return;
389
- // This file is ours — delete it rather than editing around the block.
390
- fs.rmSync(filePath, { force: true });
456
+ for (const name of CURSOR_RULE_SKILLS) {
457
+ const p = path.join(rulesDir, `${name}.mdc`);
458
+ if (fs.existsSync(p))
459
+ fs.rmSync(p, { force: true });
460
+ }
461
+ if (fs.existsSync(legacyPath))
462
+ fs.rmSync(legacyPath, { force: true });
463
+ if (fs.existsSync(agentPath))
464
+ fs.rmSync(agentPath, { force: true });
391
465
  },
392
466
  };
393
467
  }
468
+ /** Strip a single-line frontmatter field from a markdown file's frontmatter. */
469
+ function stripFrontmatterField(content, field) {
470
+ if (!content.startsWith("---\n"))
471
+ return content;
472
+ const fmEnd = content.indexOf("\n---", 4);
473
+ if (fmEnd === -1)
474
+ return content;
475
+ const frontmatter = content.slice(4, fmEnd);
476
+ const body = content.slice(fmEnd);
477
+ const re = new RegExp(`^${field}:\\s.*$`, "m");
478
+ const cleaned = frontmatter.replace(re, "").replace(/\n\n+/g, "\n").replace(/^\n/, "");
479
+ return `---\n${cleaned}${body}`;
480
+ }
394
481
  function cursorMcp() {
395
482
  const home = os.homedir();
396
483
  const mcpPath = path.join(home, ".cursor", "mcp.json");
@@ -456,8 +543,10 @@ function geminiHooks() {
456
543
  (_b = s.hooks).AfterTool ?? (_b.AfterTool = []);
457
544
  s.hooks.BeforeTool = filterOutRafter(s.hooks.BeforeTool, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
458
545
  s.hooks.AfterTool = filterOutRafter(s.hooks.AfterTool, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
546
+ // Explicit Gemini built-in tool names per geminicli.com/docs/hooks/reference
547
+ // (rf-044o verification).
459
548
  s.hooks.BeforeTool.push({
460
- matcher: "shell|write_file",
549
+ matcher: "run_shell_command|write_file|replace|edit",
461
550
  hooks: [{ type: "command", command: "rafter hook pretool --format gemini", timeout: 5000 }],
462
551
  });
463
552
  s.hooks.AfterTool.push({
@@ -514,58 +603,60 @@ function geminiMcp() {
514
603
  },
515
604
  };
516
605
  }
517
- function windsurfHooks() {
606
+ /** Skills shipped as Windsurf rules at .windsurf/rules/<skill>.md (rf-0vr3). */
607
+ const WINDSURF_RULE_SKILLS = [
608
+ "rafter",
609
+ "rafter-secure-design",
610
+ "rafter-code-review",
611
+ "rafter-skill-review",
612
+ ];
613
+ function windsurfRuleSourceDir() {
614
+ const candidates = [
615
+ path.resolve(__dirname, "..", "..", "..", "resources", "windsurf-rules"),
616
+ path.resolve(__dirname, "..", "..", "resources", "windsurf-rules"),
617
+ ];
618
+ return candidates.find((p) => fs.existsSync(p)) ?? null;
619
+ }
620
+ /**
621
+ * Windsurf rules component: per-skill files at .windsurf/rules/<skill>.md.
622
+ *
623
+ * Project/workspace-scope by design — Windsurf reads workspace rules from
624
+ * .windsurf/rules/ (12KB cap per file). The cwd at the time install runs is
625
+ * what gets the rules. Shown in the registry as resolved to the current
626
+ * working directory.
627
+ *
628
+ * Replaces the prior `windsurf.hooks` component, pruned in rf-0vr3 because
629
+ * Windsurf has no documented hook surface.
630
+ */
631
+ function windsurfRules() {
518
632
  const home = os.homedir();
519
- const hooksPath = path.join(home, ".windsurf", "hooks.json");
633
+ const rulesDir = path.join(process.cwd(), ".windsurf", "rules");
520
634
  return {
521
- id: "windsurf.hooks",
635
+ id: "windsurf.rules",
522
636
  platform: "windsurf",
523
- kind: "hooks",
524
- description: "Windsurf hooks (~/.windsurf/hooks.json)",
637
+ kind: "instructions",
638
+ description: "Windsurf per-skill rules (.windsurf/rules/*.md, workspace-scope)",
525
639
  detectDir: path.join(home, ".codeium", "windsurf"),
526
- path: hooksPath,
527
- isInstalled: () => {
528
- if (!fs.existsSync(hooksPath))
529
- return false;
530
- const cfg = readJson(hooksPath);
531
- for (const entry of cfg.hooks?.pre_run_command ?? []) {
532
- if (String(entry?.command ?? "").includes("rafter hook pretool"))
533
- return true;
534
- }
535
- return false;
536
- },
640
+ path: rulesDir,
641
+ isInstalled: () => WINDSURF_RULE_SKILLS.every((n) => fs.existsSync(path.join(rulesDir, `${n}.md`))),
537
642
  install: () => {
538
- var _a, _b;
539
- const dir = path.join(home, ".windsurf");
540
- if (!fs.existsSync(dir))
541
- fs.mkdirSync(dir, { recursive: true });
542
- const cfg = fs.existsSync(hooksPath) ? readJson(hooksPath) : {};
543
- cfg.hooks ?? (cfg.hooks = {});
544
- (_a = cfg.hooks).pre_run_command ?? (_a.pre_run_command = []);
545
- (_b = cfg.hooks).pre_write_code ?? (_b.pre_write_code = []);
546
- cfg.hooks.pre_run_command = filterOutRafter(cfg.hooks.pre_run_command, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
547
- cfg.hooks.pre_write_code = filterOutRafter(cfg.hooks.pre_write_code, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
548
- cfg.hooks.pre_run_command.push({
549
- command: "rafter hook pretool --format windsurf",
550
- show_output: true,
551
- });
552
- cfg.hooks.pre_write_code.push({
553
- command: "rafter hook pretool --format windsurf",
554
- show_output: true,
555
- });
556
- writeJson(hooksPath, cfg);
557
- },
558
- uninstall: () => {
559
- if (!fs.existsSync(hooksPath))
643
+ fs.mkdirSync(rulesDir, { recursive: true });
644
+ const src = windsurfRuleSourceDir();
645
+ if (!src)
560
646
  return;
561
- const cfg = readJson(hooksPath);
562
- if (cfg.hooks?.pre_run_command) {
563
- cfg.hooks.pre_run_command = filterOutRafter(cfg.hooks.pre_run_command, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
647
+ for (const name of WINDSURF_RULE_SKILLS) {
648
+ const from = path.join(src, `${name}.md`);
649
+ if (fs.existsSync(from)) {
650
+ fs.copyFileSync(from, path.join(rulesDir, `${name}.md`));
651
+ }
564
652
  }
565
- if (cfg.hooks?.pre_write_code) {
566
- cfg.hooks.pre_write_code = filterOutRafter(cfg.hooks.pre_write_code, (e) => String(e?.command ?? "").includes("rafter hook pretool"));
653
+ },
654
+ uninstall: () => {
655
+ for (const name of WINDSURF_RULE_SKILLS) {
656
+ const p = path.join(rulesDir, `${name}.md`);
657
+ if (fs.existsSync(p))
658
+ fs.rmSync(p, { force: true });
567
659
  }
568
- writeJson(hooksPath, cfg);
569
660
  },
570
661
  };
571
662
  }
@@ -603,54 +694,50 @@ function windsurfMcp() {
603
694
  },
604
695
  };
605
696
  }
606
- function continueHooks() {
697
+ /** Skills shipped as Continue.dev rules at .continue/rules/<skill>.md (rf-acz0). */
698
+ const CONTINUE_RULE_SKILLS = [
699
+ "rafter",
700
+ "rafter-secure-design",
701
+ "rafter-code-review",
702
+ "rafter-skill-review",
703
+ ];
704
+ function continueRuleSourceDir() {
705
+ const candidates = [
706
+ path.resolve(__dirname, "..", "..", "..", "resources", "continue-rules"),
707
+ path.resolve(__dirname, "..", "..", "resources", "continue-rules"),
708
+ ];
709
+ return candidates.find((p) => fs.existsSync(p)) ?? null;
710
+ }
711
+ /** Continue.dev rules component: .continue/rules/<skill>.md, workspace-scope (rf-acz0). */
712
+ function continueRules() {
607
713
  const home = os.homedir();
608
- const settingsPath = path.join(home, ".continue", "settings.json");
714
+ const rulesDir = path.join(process.cwd(), ".continue", "rules");
609
715
  return {
610
- id: "continue.hooks",
716
+ id: "continue.rules",
611
717
  platform: "continue",
612
- kind: "hooks",
613
- description: "Continue.dev PreToolUse + PostToolUse hooks",
718
+ kind: "instructions",
719
+ description: "Continue.dev per-skill rules (.continue/rules/*.md, workspace-scope)",
614
720
  detectDir: path.join(home, ".continue"),
615
- path: settingsPath,
616
- isInstalled: () => {
617
- if (!fs.existsSync(settingsPath))
618
- return false;
619
- const s = readJson(settingsPath);
620
- for (const entry of s.hooks?.PreToolUse ?? []) {
621
- if (hookEntryMatchesRafter(entry, "rafter hook pretool"))
622
- return true;
623
- }
624
- return false;
625
- },
721
+ path: rulesDir,
722
+ isInstalled: () => CONTINUE_RULE_SKILLS.every((n) => fs.existsSync(path.join(rulesDir, `${n}.md`))),
626
723
  install: () => {
627
- var _a, _b;
628
- const dir = path.join(home, ".continue");
629
- if (!fs.existsSync(dir))
630
- fs.mkdirSync(dir, { recursive: true });
631
- const s = fs.existsSync(settingsPath) ? readJson(settingsPath) : {};
632
- s.hooks ?? (s.hooks = {});
633
- (_a = s.hooks).PreToolUse ?? (_a.PreToolUse = []);
634
- (_b = s.hooks).PostToolUse ?? (_b.PostToolUse = []);
635
- const pre = { type: "command", command: "rafter hook pretool" };
636
- const post = { type: "command", command: "rafter hook posttool" };
637
- s.hooks.PreToolUse = filterOutRafter(s.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
638
- s.hooks.PostToolUse = filterOutRafter(s.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
639
- s.hooks.PreToolUse.push({ matcher: "Bash", hooks: [pre] }, { matcher: "Write|Edit", hooks: [pre] });
640
- s.hooks.PostToolUse.push({ matcher: ".*", hooks: [post] });
641
- writeJson(settingsPath, s);
642
- },
643
- uninstall: () => {
644
- if (!fs.existsSync(settingsPath))
724
+ fs.mkdirSync(rulesDir, { recursive: true });
725
+ const src = continueRuleSourceDir();
726
+ if (!src)
645
727
  return;
646
- const s = readJson(settingsPath);
647
- if (s.hooks?.PreToolUse) {
648
- s.hooks.PreToolUse = filterOutRafter(s.hooks.PreToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook pretool"));
728
+ for (const name of CONTINUE_RULE_SKILLS) {
729
+ const from = path.join(src, `${name}.md`);
730
+ if (fs.existsSync(from)) {
731
+ fs.copyFileSync(from, path.join(rulesDir, `${name}.md`));
732
+ }
649
733
  }
650
- if (s.hooks?.PostToolUse) {
651
- s.hooks.PostToolUse = filterOutRafter(s.hooks.PostToolUse, (e) => hookEntryMatchesRafter(e, "rafter hook posttool"));
734
+ },
735
+ uninstall: () => {
736
+ for (const name of CONTINUE_RULE_SKILLS) {
737
+ const p = path.join(rulesDir, `${name}.md`);
738
+ if (fs.existsSync(p))
739
+ fs.rmSync(p, { force: true });
652
740
  }
653
- writeJson(settingsPath, s);
654
741
  },
655
742
  };
656
743
  }
@@ -708,46 +795,103 @@ function continueMcp() {
708
795
  },
709
796
  };
710
797
  }
711
- function aiderMcp() {
798
+ /**
799
+ * Aider read-only context: writes RAFTER.md and adds it to .aider.conf.yml `read:`.
800
+ *
801
+ * Replaces the prior `aider.mcp` component, pruned in rf-du2o because Aider
802
+ * has no native MCP support — the legacy `mcp-server-command: rafter mcp serve`
803
+ * line was a silent no-op (Aider ignores unknown YAML keys per its docs).
804
+ *
805
+ * Project-scope by design — RAFTER.md and the read entry land in cwd.
806
+ */
807
+ function aiderRead() {
712
808
  const home = os.homedir();
713
- const configPath = path.join(home, ".aider.conf.yml");
714
- const mcpLineHeader = "# Rafter security MCP server";
809
+ const cwd = process.cwd();
810
+ const configPath = path.join(cwd, ".aider.conf.yml");
811
+ const rafterMdPath = path.join(cwd, "RAFTER.md");
812
+ const READ_ENTRY = "RAFTER.md";
715
813
  return {
716
- id: "aider.mcp",
814
+ id: "aider.read",
717
815
  platform: "aider",
718
- kind: "mcp",
719
- description: "Aider MCP server entry (~/.aider.conf.yml)",
720
- // Aider has no config dir — its presence is the file itself. Point detectDir
721
- // at $HOME so the platform is always considered "present enough to install into".
816
+ kind: "instructions",
817
+ description: "Aider read-only context (RAFTER.md + .aider.conf.yml read:)",
722
818
  detectDir: home,
723
- path: configPath,
819
+ path: rafterMdPath,
724
820
  isInstalled: () => {
821
+ if (!fs.existsSync(rafterMdPath))
822
+ return false;
725
823
  if (!fs.existsSync(configPath))
726
824
  return false;
727
- return fs.readFileSync(configPath, "utf-8").includes("rafter mcp serve");
825
+ const raw = fs.readFileSync(configPath, "utf-8");
826
+ try {
827
+ const parsed = yaml.load(raw);
828
+ const reads = Array.isArray(parsed?.read)
829
+ ? parsed.read.map(String)
830
+ : typeof parsed?.read === "string" ? [parsed.read] : [];
831
+ return reads.includes(READ_ENTRY);
832
+ }
833
+ catch {
834
+ return raw.includes(READ_ENTRY);
835
+ }
728
836
  },
729
837
  install: () => {
730
- const content = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf-8") : "";
731
- if (content.includes("rafter mcp serve"))
732
- return;
733
- const block = `\n${mcpLineHeader}\nmcp-server-command: rafter mcp serve\n`;
734
- fs.writeFileSync(configPath, content + block, "utf-8");
838
+ injectInstructionFile(rafterMdPath);
839
+ let raw = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf-8") : "";
840
+ // Strip legacy mcp-server-command silent-no-op (rf-du2o migration).
841
+ raw = raw.replace(/\n?#\s*Rafter security MCP server\s*\nmcp-server-command:\s*rafter\s+mcp\s+serve\s*\n?/g, "\n");
842
+ raw = raw.replace(/^mcp-server-command:\s*rafter\s+mcp\s+serve\s*\n?/gm, "");
843
+ let parsed = {};
844
+ if (raw.trim().length > 0) {
845
+ try {
846
+ const loaded = yaml.load(raw);
847
+ if (loaded && typeof loaded === "object" && !Array.isArray(loaded)) {
848
+ parsed = loaded;
849
+ }
850
+ }
851
+ catch {
852
+ // Unparseable YAML — append safely without touching existing content.
853
+ if (!new RegExp(`\\b${READ_ENTRY}\\b`).test(raw)) {
854
+ const sep = raw.length > 0 && !raw.endsWith("\n") ? "\n" : "";
855
+ fs.writeFileSync(configPath, `${raw}${sep}read:\n - ${READ_ENTRY}\n`, "utf-8");
856
+ }
857
+ return;
858
+ }
859
+ }
860
+ let reads = [];
861
+ if (Array.isArray(parsed.read))
862
+ reads = parsed.read.map(String);
863
+ else if (typeof parsed.read === "string")
864
+ reads = [parsed.read];
865
+ if (!reads.includes(READ_ENTRY))
866
+ reads.push(READ_ENTRY);
867
+ parsed.read = reads;
868
+ fs.writeFileSync(configPath, yaml.dump(parsed), "utf-8");
735
869
  },
736
870
  uninstall: () => {
871
+ if (fs.existsSync(rafterMdPath)) {
872
+ try {
873
+ fs.rmSync(rafterMdPath, { force: true });
874
+ }
875
+ catch { /* best-effort */ }
876
+ }
737
877
  if (!fs.existsSync(configPath))
738
878
  return;
739
- const content = fs.readFileSync(configPath, "utf-8");
740
- // Remove both the comment marker and the command line; preserve everything else.
741
- const lines = content.split("\n");
742
- const next = lines.filter((l) => {
743
- const t = l.trim();
744
- if (t === mcpLineHeader)
745
- return false;
746
- if (t.startsWith("mcp-server-command:") && t.includes("rafter mcp serve"))
747
- return false;
748
- return true;
749
- });
750
- fs.writeFileSync(configPath, next.join("\n"), "utf-8");
879
+ const raw = fs.readFileSync(configPath, "utf-8");
880
+ try {
881
+ const parsed = yaml.load(raw);
882
+ if (parsed && Array.isArray(parsed.read)) {
883
+ parsed.read = parsed.read.filter((p) => String(p) !== READ_ENTRY);
884
+ if (parsed.read.length === 0)
885
+ delete parsed.read;
886
+ }
887
+ else if (parsed && parsed.read === READ_ENTRY) {
888
+ delete parsed.read;
889
+ }
890
+ fs.writeFileSync(configPath, yaml.dump(parsed ?? {}), "utf-8");
891
+ }
892
+ catch {
893
+ /* preserve unparseable file */
894
+ }
751
895
  },
752
896
  };
753
897
  }
@@ -793,11 +937,11 @@ export function getComponentRegistry() {
793
937
  cursorMcp(),
794
938
  geminiHooks(),
795
939
  geminiMcp(),
796
- windsurfHooks(),
940
+ windsurfRules(),
797
941
  windsurfMcp(),
798
- continueHooks(),
942
+ continueRules(),
799
943
  continueMcp(),
800
- aiderMcp(),
944
+ aiderRead(),
801
945
  openclawSkill(),
802
946
  ];
803
947
  }