@rafter-security/cli 0.7.6 → 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.
- package/README.md +27 -681
- package/dist/commands/agent/components.js +282 -138
- package/dist/commands/agent/init.js +399 -150
- package/dist/commands/agent/scan.js +52 -23
- package/dist/commands/agent/verify.js +211 -21
- package/dist/commands/brief.js +13 -45
- package/dist/commands/issues/from-scan.js +4 -1
- package/dist/core/config-manager.js +6 -0
- package/dist/core/custom-patterns.js +86 -4
- package/dist/core/policy-loader.js +60 -1
- package/dist/scanners/regex-scanner.js +4 -5
- package/dist/utils/skill-manager.js +96 -16
- package/package.json +1 -1
- package/resources/agents/rafter.md +81 -0
- package/resources/continue-rules/rafter-code-review.md +15 -0
- package/resources/continue-rules/rafter-secure-design.md +15 -0
- package/resources/continue-rules/rafter-skill-review.md +15 -0
- package/resources/continue-rules/rafter.md +16 -0
- package/resources/cursor-rules/rafter-code-review.mdc +14 -0
- package/resources/cursor-rules/rafter-secure-design.mdc +14 -0
- package/resources/cursor-rules/rafter-skill-review.mdc +14 -0
- package/resources/cursor-rules/rafter.mdc +15 -0
- package/resources/rafter-security-skill.md +17 -9
- package/resources/windsurf-rules/rafter-code-review.md +14 -0
- package/resources/windsurf-rules/rafter-secure-design.md +14 -0
- package/resources/windsurf-rules/rafter-skill-review.md +14 -0
- package/resources/windsurf-rules/rafter.md +15 -0
|
@@ -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
|
-
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
(
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
command: "
|
|
358
|
-
|
|
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
|
-
|
|
368
|
-
|
|
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
|
|
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
|
|
422
|
+
description: "Cursor per-skill rules + rafter sub-agent (~/.cursor/rules/, ~/.cursor/agents/rafter.md)",
|
|
382
423
|
detectDir: path.join(home, ".cursor"),
|
|
383
|
-
path:
|
|
384
|
-
isInstalled: () =>
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
|
633
|
+
const rulesDir = path.join(process.cwd(), ".windsurf", "rules");
|
|
520
634
|
return {
|
|
521
|
-
id: "windsurf.
|
|
635
|
+
id: "windsurf.rules",
|
|
522
636
|
platform: "windsurf",
|
|
523
|
-
kind: "
|
|
524
|
-
description: "Windsurf
|
|
637
|
+
kind: "instructions",
|
|
638
|
+
description: "Windsurf per-skill rules (.windsurf/rules/*.md, workspace-scope)",
|
|
525
639
|
detectDir: path.join(home, ".codeium", "windsurf"),
|
|
526
|
-
path:
|
|
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
|
-
|
|
539
|
-
const
|
|
540
|
-
if (!
|
|
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
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
|
714
|
+
const rulesDir = path.join(process.cwd(), ".continue", "rules");
|
|
609
715
|
return {
|
|
610
|
-
id: "continue.
|
|
716
|
+
id: "continue.rules",
|
|
611
717
|
platform: "continue",
|
|
612
|
-
kind: "
|
|
613
|
-
description: "Continue.dev
|
|
718
|
+
kind: "instructions",
|
|
719
|
+
description: "Continue.dev per-skill rules (.continue/rules/*.md, workspace-scope)",
|
|
614
720
|
detectDir: path.join(home, ".continue"),
|
|
615
|
-
path:
|
|
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
|
-
|
|
628
|
-
const
|
|
629
|
-
if (!
|
|
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
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
651
|
-
|
|
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
|
-
|
|
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
|
|
714
|
-
const
|
|
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.
|
|
814
|
+
id: "aider.read",
|
|
717
815
|
platform: "aider",
|
|
718
|
-
kind: "
|
|
719
|
-
description: "Aider
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
940
|
+
windsurfRules(),
|
|
797
941
|
windsurfMcp(),
|
|
798
|
-
|
|
942
|
+
continueRules(),
|
|
799
943
|
continueMcp(),
|
|
800
|
-
|
|
944
|
+
aiderRead(),
|
|
801
945
|
openclawSkill(),
|
|
802
946
|
];
|
|
803
947
|
}
|