@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.
- 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
|
@@ -11,6 +11,7 @@ import { createRequire } from "module";
|
|
|
11
11
|
import { createInterface } from "readline";
|
|
12
12
|
import { fmt } from "../../utils/formatter.js";
|
|
13
13
|
import { injectInstructionFile } from "./instruction-block.js";
|
|
14
|
+
import yaml from "js-yaml";
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
const __dirname = path.dirname(__filename);
|
|
16
17
|
/**
|
|
@@ -54,17 +55,23 @@ function installGlobalInstructions(platforms, root, scope) {
|
|
|
54
55
|
console.log(fmt.warning(`Failed to write Claude Code instructions: ${e}`));
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
//
|
|
58
|
-
|
|
58
|
+
// AGENTS.md — read natively by Codex AND Windsurf. Codex at user scope keeps
|
|
59
|
+
// its own copy at ~/.codex/AGENTS.md; everything else (project scope, or any
|
|
60
|
+
// scope where Windsurf is in play) writes <root>/AGENTS.md once.
|
|
61
|
+
if (platforms.codex || platforms.windsurf) {
|
|
59
62
|
try {
|
|
60
|
-
const
|
|
63
|
+
const codexUser = scope === "user" && platforms.codex && !platforms.windsurf;
|
|
64
|
+
const filePath = codexUser
|
|
61
65
|
? path.join(root, ".codex", "AGENTS.md")
|
|
62
66
|
: path.join(root, "AGENTS.md");
|
|
63
67
|
injectInstructionFile(filePath);
|
|
64
|
-
|
|
68
|
+
const readers = [platforms.codex && "Codex", platforms.windsurf && "Windsurf"]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join(" + ");
|
|
71
|
+
console.log(fmt.success(`Installed Rafter instructions for ${readers} to ${filePath}`));
|
|
65
72
|
}
|
|
66
73
|
catch (e) {
|
|
67
|
-
console.log(fmt.warning(`Failed to write
|
|
74
|
+
console.log(fmt.warning(`Failed to write AGENTS.md: ${e}`));
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
77
|
// Gemini — ~/.gemini/GEMINI.md (user) or <cwd>/GEMINI.md (project)
|
|
@@ -80,17 +87,10 @@ function installGlobalInstructions(platforms, root, scope) {
|
|
|
80
87
|
console.log(fmt.warning(`Failed to write Gemini instructions: ${e}`));
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
|
-
// Cursor
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
injectInstructionFile(filePath);
|
|
88
|
-
console.log(fmt.success(`Installed Rafter instructions to ${filePath}`));
|
|
89
|
-
}
|
|
90
|
-
catch (e) {
|
|
91
|
-
console.log(fmt.warning(`Failed to write Cursor instructions: ${e}`));
|
|
92
|
-
}
|
|
93
|
-
}
|
|
90
|
+
// Cursor uses per-skill rules at <root>/.cursor/rules/<skill>.mdc and the
|
|
91
|
+
// rafter sub-agent at <root>/.cursor/agents/rafter.md (installed in the
|
|
92
|
+
// Cursor branch above). The consolidated rafter-security.mdc was retired
|
|
93
|
+
// in rf-svn3 in favor of per-skill rules with trigger-first descriptions.
|
|
94
94
|
}
|
|
95
95
|
function installClaudeCodeHooks(root) {
|
|
96
96
|
const settingsPath = path.join(root, ".claude", "settings.json");
|
|
@@ -170,11 +170,30 @@ function installCodexHooks(root) {
|
|
|
170
170
|
// Remove existing rafter hooks
|
|
171
171
|
config.hooks.PreToolUse = config.hooks.PreToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook pretool")));
|
|
172
172
|
config.hooks.PostToolUse = config.hooks.PostToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook posttool")));
|
|
173
|
-
|
|
173
|
+
// PreToolUse intercepts the tools Codex documents support for: Bash and
|
|
174
|
+
// apply_patch (file edits). Per developers.openai.com/codex/hooks PreToolUse
|
|
175
|
+
// also covers MCP tool calls via patterns like `mcp__<server>__<tool>` —
|
|
176
|
+
// when an MCP server is wired up, install a separate matcher for it.
|
|
177
|
+
// (rf-ovql verification 2026-05-03.)
|
|
178
|
+
config.hooks.PreToolUse.push({ matcher: "Bash|apply_patch", hooks: [preHook] });
|
|
179
|
+
// PostToolUse fires for the same tool surface; .* keeps all events in the
|
|
180
|
+
// audit log without filtering.
|
|
174
181
|
config.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
|
|
175
182
|
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
|
|
176
183
|
console.log(fmt.success(`Installed hooks to ${hooksPath}`));
|
|
177
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Install Cursor hooks at <root>/.cursor/hooks.json.
|
|
187
|
+
*
|
|
188
|
+
* Covers the full pre/post-tool lifecycle plus shell-specific gating:
|
|
189
|
+
* - preToolUse — rafter classifies every tool call
|
|
190
|
+
* - postToolUse — rafter post-hook (audit, telemetry)
|
|
191
|
+
* - beforeShellExecution — narrower complement; some Cursor versions
|
|
192
|
+
* fire this without firing preToolUse for shell.
|
|
193
|
+
*
|
|
194
|
+
* Idempotent — repeated installs do not duplicate rafter entries.
|
|
195
|
+
* Non-rafter entries (other tools' hooks, unrelated events) are preserved.
|
|
196
|
+
*/
|
|
178
197
|
function installCursorHooks(root) {
|
|
179
198
|
const cursorDir = path.join(root, ".cursor");
|
|
180
199
|
if (!fs.existsSync(cursorDir)) {
|
|
@@ -194,18 +213,116 @@ function installCursorHooks(root) {
|
|
|
194
213
|
config.version = 1;
|
|
195
214
|
if (!config.hooks)
|
|
196
215
|
config.hooks = {};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
const events = [
|
|
217
|
+
{ event: "preToolUse", command: "rafter hook pretool --format cursor" },
|
|
218
|
+
{ event: "postToolUse", command: "rafter hook posttool --format cursor" },
|
|
219
|
+
{ event: "beforeShellExecution", command: "rafter hook pretool --format cursor" },
|
|
220
|
+
];
|
|
221
|
+
for (const { event, command } of events) {
|
|
222
|
+
if (!Array.isArray(config.hooks[event]))
|
|
223
|
+
config.hooks[event] = [];
|
|
224
|
+
config.hooks[event] = config.hooks[event].filter((entry) => !entry?.command?.includes("rafter hook"));
|
|
225
|
+
config.hooks[event].push({ command, type: "command", timeout: 5000 });
|
|
226
|
+
}
|
|
206
227
|
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
|
|
207
228
|
console.log(fmt.success(`Installed hooks to ${hooksPath}`));
|
|
208
229
|
}
|
|
230
|
+
/** Skills shipped as both Cursor rules and (Claude Code / Codex / Gemini) skills. */
|
|
231
|
+
const CURSOR_RULE_SKILLS = [
|
|
232
|
+
"rafter",
|
|
233
|
+
"rafter-secure-design",
|
|
234
|
+
"rafter-code-review",
|
|
235
|
+
"rafter-skill-review",
|
|
236
|
+
];
|
|
237
|
+
/**
|
|
238
|
+
* Install per-skill Cursor rules at <root>/.cursor/rules/<skill>.mdc.
|
|
239
|
+
*
|
|
240
|
+
* One file per shipped skill. Each rule's frontmatter description is reused
|
|
241
|
+
* verbatim from the skill's SKILL.md frontmatter (trigger-first phrasing per
|
|
242
|
+
* rf-4ei / rf-8po) so Cursor surfaces it on the same triggers as Claude Code.
|
|
243
|
+
*
|
|
244
|
+
* Replaces the legacy consolidated `.cursor/rules/rafter-security.mdc` — that
|
|
245
|
+
* single file is no longer written by the Cursor install path.
|
|
246
|
+
*/
|
|
247
|
+
function installCursorRules(root) {
|
|
248
|
+
const rulesDir = path.join(root, ".cursor", "rules");
|
|
249
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
250
|
+
// Resolve resources/cursor-rules relative to this module.
|
|
251
|
+
// After build: dist/commands/agent/init.js -> ../../../resources/cursor-rules
|
|
252
|
+
const candidates = [
|
|
253
|
+
path.resolve(__dirname, "..", "..", "..", "resources", "cursor-rules"),
|
|
254
|
+
path.resolve(__dirname, "..", "..", "resources", "cursor-rules"),
|
|
255
|
+
];
|
|
256
|
+
const sourceDir = candidates.find((p) => fs.existsSync(p));
|
|
257
|
+
if (!sourceDir) {
|
|
258
|
+
console.log(fmt.warning(`Cursor rule templates not found in resources/cursor-rules`));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
for (const name of CURSOR_RULE_SKILLS) {
|
|
262
|
+
const src = path.join(sourceDir, `${name}.mdc`);
|
|
263
|
+
const dest = path.join(rulesDir, `${name}.mdc`);
|
|
264
|
+
if (!fs.existsSync(src)) {
|
|
265
|
+
console.log(fmt.warning(`Cursor rule template missing: ${src}`));
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
fs.copyFileSync(src, dest);
|
|
269
|
+
console.log(fmt.success(`Installed Cursor rule to ${dest}`));
|
|
270
|
+
}
|
|
271
|
+
// Remove the legacy consolidated rule if present, so reinstall on top of
|
|
272
|
+
// an old layout migrates cleanly.
|
|
273
|
+
const legacy = path.join(rulesDir, "rafter-security.mdc");
|
|
274
|
+
if (fs.existsSync(legacy)) {
|
|
275
|
+
try {
|
|
276
|
+
fs.unlinkSync(legacy);
|
|
277
|
+
console.log(fmt.info(`Removed legacy ${legacy} (superseded by per-skill rules)`));
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
/* best-effort */
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Install Cursor sub-agent at <root>/.cursor/agents/rafter.md.
|
|
286
|
+
*
|
|
287
|
+
* Reuses the Claude-Code sub-agent body (rf-q7j) with one shape difference:
|
|
288
|
+
* Cursor's frontmatter has no `tools:` field — tools inherit from the parent
|
|
289
|
+
* agent. The hard rules in the body (no code modification, no commits) still
|
|
290
|
+
* apply, since Cursor relies on prompt-level constraints rather than
|
|
291
|
+
* structural restriction.
|
|
292
|
+
*/
|
|
293
|
+
function installCursorSubAgents(root) {
|
|
294
|
+
const agentsDir = path.join(root, ".cursor", "agents");
|
|
295
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
296
|
+
const candidates = [
|
|
297
|
+
path.resolve(__dirname, "..", "..", "..", "resources", "agents", "rafter.md"),
|
|
298
|
+
path.resolve(__dirname, "..", "..", "resources", "agents", "rafter.md"),
|
|
299
|
+
];
|
|
300
|
+
const src = candidates.find((p) => fs.existsSync(p));
|
|
301
|
+
if (!src) {
|
|
302
|
+
console.log(fmt.warning(`Rafter sub-agent template not found in resources/agents`));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const raw = fs.readFileSync(src, "utf-8");
|
|
306
|
+
const cursored = stripToolsFromFrontmatter(raw);
|
|
307
|
+
const dest = path.join(agentsDir, "rafter.md");
|
|
308
|
+
fs.writeFileSync(dest, cursored, "utf-8");
|
|
309
|
+
console.log(fmt.success(`Installed Cursor sub-agent to ${dest}`));
|
|
310
|
+
}
|
|
311
|
+
/** Strip the Claude-Code `tools:` line from sub-agent frontmatter — Cursor doesn't have it. */
|
|
312
|
+
function stripToolsFromFrontmatter(content) {
|
|
313
|
+
if (!content.startsWith("---\n"))
|
|
314
|
+
return content;
|
|
315
|
+
const fmEnd = content.indexOf("\n---", 4);
|
|
316
|
+
if (fmEnd === -1)
|
|
317
|
+
return content;
|
|
318
|
+
const frontmatter = content.slice(4, fmEnd);
|
|
319
|
+
const body = content.slice(fmEnd);
|
|
320
|
+
const cleaned = frontmatter
|
|
321
|
+
.split("\n")
|
|
322
|
+
.filter((line) => !/^tools:\s/.test(line))
|
|
323
|
+
.join("\n");
|
|
324
|
+
return `---\n${cleaned}${body}`;
|
|
325
|
+
}
|
|
209
326
|
function installGeminiHooks(root) {
|
|
210
327
|
const geminiDir = path.join(root, ".gemini");
|
|
211
328
|
if (!fs.existsSync(geminiDir)) {
|
|
@@ -230,8 +347,12 @@ function installGeminiHooks(root) {
|
|
|
230
347
|
// Remove existing rafter hooks
|
|
231
348
|
settings.hooks.BeforeTool = settings.hooks.BeforeTool.filter((entry) => !(entry.hooks || []).some((h) => h.command?.includes("rafter hook pretool")));
|
|
232
349
|
settings.hooks.AfterTool = settings.hooks.AfterTool.filter((entry) => !(entry.hooks || []).some((h) => h.command?.includes("rafter hook posttool")));
|
|
350
|
+
// Gemini matchers are regexes against built-in tool names per
|
|
351
|
+
// geminicli.com/docs/hooks/reference. Match the mutating tools by name
|
|
352
|
+
// explicitly: run_shell_command, write_file, replace, edit. (rf-044o
|
|
353
|
+
// verification 2026-05-03 — schema confirmed against current Gemini docs.)
|
|
233
354
|
settings.hooks.BeforeTool.push({
|
|
234
|
-
matcher: "
|
|
355
|
+
matcher: "run_shell_command|write_file|replace|edit",
|
|
235
356
|
hooks: [{ type: "command", command: "rafter hook pretool --format gemini", timeout: 5000 }],
|
|
236
357
|
});
|
|
237
358
|
settings.hooks.AfterTool.push({
|
|
@@ -241,71 +362,47 @@ function installGeminiHooks(root) {
|
|
|
241
362
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
242
363
|
console.log(fmt.success(`Installed hooks to ${settingsPath}`));
|
|
243
364
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
show_output: true,
|
|
275
|
-
});
|
|
276
|
-
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
|
|
277
|
-
console.log(fmt.success(`Installed hooks to ${hooksPath}`));
|
|
278
|
-
}
|
|
279
|
-
function installContinueDevHooks(root) {
|
|
280
|
-
const continueDir = path.join(root, ".continue");
|
|
281
|
-
if (!fs.existsSync(continueDir)) {
|
|
282
|
-
fs.mkdirSync(continueDir, { recursive: true });
|
|
365
|
+
/** Skills shipped as Windsurf per-workspace rules at .windsurf/rules/<skill>.md (rf-0vr3). */
|
|
366
|
+
const WINDSURF_RULE_SKILLS = [
|
|
367
|
+
"rafter",
|
|
368
|
+
"rafter-secure-design",
|
|
369
|
+
"rafter-code-review",
|
|
370
|
+
"rafter-skill-review",
|
|
371
|
+
];
|
|
372
|
+
/**
|
|
373
|
+
* Install per-skill Windsurf rules at <root>/.windsurf/rules/<skill>.md.
|
|
374
|
+
*
|
|
375
|
+
* Windsurf reads workspace rules from .windsurf/rules/*.md (12KB cap per file
|
|
376
|
+
* per docs). Each file uses Windsurf YAML frontmatter (`trigger: model_decision`
|
|
377
|
+
* + `description:`) so the agent fetches the rule when its description matches
|
|
378
|
+
* the task. Body content mirrors the Cursor pointer-rule pattern.
|
|
379
|
+
*
|
|
380
|
+
* Replaces the prior `~/.windsurf/hooks.json` install, which was a silent
|
|
381
|
+
* no-op — Windsurf has no documented hook surface as of v1.x (research bead
|
|
382
|
+
* rf-s1n3, gap reports rf-p1ri / rf-vayl).
|
|
383
|
+
*/
|
|
384
|
+
function installWindsurfRules(root) {
|
|
385
|
+
const rulesDir = path.join(root, ".windsurf", "rules");
|
|
386
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
387
|
+
const candidates = [
|
|
388
|
+
path.resolve(__dirname, "..", "..", "..", "resources", "windsurf-rules"),
|
|
389
|
+
path.resolve(__dirname, "..", "..", "resources", "windsurf-rules"),
|
|
390
|
+
];
|
|
391
|
+
const sourceDir = candidates.find((p) => fs.existsSync(p));
|
|
392
|
+
if (!sourceDir) {
|
|
393
|
+
console.log(fmt.warning(`Windsurf rule templates not found in resources/windsurf-rules`));
|
|
394
|
+
return;
|
|
283
395
|
}
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
catch {
|
|
291
|
-
console.log(fmt.warning("Existing Continue.dev settings.json was unreadable, creating new one"));
|
|
396
|
+
for (const name of WINDSURF_RULE_SKILLS) {
|
|
397
|
+
const src = path.join(sourceDir, `${name}.md`);
|
|
398
|
+
const dest = path.join(rulesDir, `${name}.md`);
|
|
399
|
+
if (!fs.existsSync(src)) {
|
|
400
|
+
console.log(fmt.warning(`Windsurf rule template missing: ${src}`));
|
|
401
|
+
continue;
|
|
292
402
|
}
|
|
403
|
+
fs.copyFileSync(src, dest);
|
|
404
|
+
console.log(fmt.success(`Installed Windsurf rule to ${dest}`));
|
|
293
405
|
}
|
|
294
|
-
if (!settings.hooks)
|
|
295
|
-
settings.hooks = {};
|
|
296
|
-
if (!settings.hooks.PreToolUse)
|
|
297
|
-
settings.hooks.PreToolUse = [];
|
|
298
|
-
if (!settings.hooks.PostToolUse)
|
|
299
|
-
settings.hooks.PostToolUse = [];
|
|
300
|
-
// Continue.dev uses the same protocol as Claude Code
|
|
301
|
-
const preHook = { type: "command", command: "rafter hook pretool" };
|
|
302
|
-
const postHook = { type: "command", command: "rafter hook posttool" };
|
|
303
|
-
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook pretool")));
|
|
304
|
-
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook posttool")));
|
|
305
|
-
settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] }, { matcher: "Write|Edit", hooks: [preHook] });
|
|
306
|
-
settings.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
|
|
307
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
308
|
-
console.log(fmt.success(`Installed hooks to ${settingsPath}`));
|
|
309
406
|
}
|
|
310
407
|
/** MCP server entry for rafter — shared across MCP-native clients */
|
|
311
408
|
const RAFTER_MCP_ENTRY = {
|
|
@@ -409,6 +506,46 @@ function installWindsurfMcp(root) {
|
|
|
409
506
|
console.log(fmt.success(`Installed Rafter MCP server to ${mcpPath}`));
|
|
410
507
|
return true;
|
|
411
508
|
}
|
|
509
|
+
/** Skills shipped as Continue.dev rules at .continue/rules/<skill>.md (rf-acz0). */
|
|
510
|
+
const CONTINUE_RULE_SKILLS = [
|
|
511
|
+
"rafter",
|
|
512
|
+
"rafter-secure-design",
|
|
513
|
+
"rafter-code-review",
|
|
514
|
+
"rafter-skill-review",
|
|
515
|
+
];
|
|
516
|
+
/**
|
|
517
|
+
* Install per-skill Continue.dev rules at <root>/.continue/rules/<skill>.md.
|
|
518
|
+
*
|
|
519
|
+
* Continue.dev reads workspace rules from .continue/rules/*.md (per-rule files,
|
|
520
|
+
* lexicographic load order). Frontmatter: `name`, `description`, `alwaysApply`.
|
|
521
|
+
* Each rule body mirrors the Cursor / Windsurf pointer-rule pattern.
|
|
522
|
+
*
|
|
523
|
+
* Continue.dev has no documented hook surface (the prior hooks install was
|
|
524
|
+
* pruned in rf-cia phase b). Rules + MCP are the only intercepts.
|
|
525
|
+
*/
|
|
526
|
+
function installContinueDevRules(root) {
|
|
527
|
+
const rulesDir = path.join(root, ".continue", "rules");
|
|
528
|
+
fs.mkdirSync(rulesDir, { recursive: true });
|
|
529
|
+
const candidates = [
|
|
530
|
+
path.resolve(__dirname, "..", "..", "..", "resources", "continue-rules"),
|
|
531
|
+
path.resolve(__dirname, "..", "..", "resources", "continue-rules"),
|
|
532
|
+
];
|
|
533
|
+
const sourceDir = candidates.find((p) => fs.existsSync(p));
|
|
534
|
+
if (!sourceDir) {
|
|
535
|
+
console.log(fmt.warning(`Continue.dev rule templates not found in resources/continue-rules`));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
for (const name of CONTINUE_RULE_SKILLS) {
|
|
539
|
+
const src = path.join(sourceDir, `${name}.md`);
|
|
540
|
+
const dest = path.join(rulesDir, `${name}.md`);
|
|
541
|
+
if (!fs.existsSync(src)) {
|
|
542
|
+
console.log(fmt.warning(`Continue.dev rule template missing: ${src}`));
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
fs.copyFileSync(src, dest);
|
|
546
|
+
console.log(fmt.success(`Installed Continue.dev rule to ${dest}`));
|
|
547
|
+
}
|
|
548
|
+
}
|
|
412
549
|
/**
|
|
413
550
|
* Install MCP server config for Continue.dev (~/.continue/config.json)
|
|
414
551
|
*/
|
|
@@ -447,50 +584,129 @@ function installContinueDevMcp(root) {
|
|
|
447
584
|
return true;
|
|
448
585
|
}
|
|
449
586
|
/**
|
|
450
|
-
* Install
|
|
451
|
-
*
|
|
587
|
+
* Install Rafter context for Aider (rf-du2o).
|
|
588
|
+
*
|
|
589
|
+
* Aider has no native MCP support and no plugin/hook surface. Its only
|
|
590
|
+
* intercept-friendly persistent-context primitive is the `read:` flag in
|
|
591
|
+
* `.aider.conf.yml`, which injects read-only files into every session.
|
|
592
|
+
*
|
|
593
|
+
* Behavior:
|
|
594
|
+
* 1. Write `<root>/RAFTER.md` with the rafter instruction block.
|
|
595
|
+
* 2. Update `<root>/.aider.conf.yml` so `read:` includes `RAFTER.md`
|
|
596
|
+
* (preserves any pre-existing `read:` entries; preserves other YAML keys).
|
|
597
|
+
* 3. Strip the legacy `mcp-server-command: rafter mcp serve` line if present
|
|
598
|
+
* — it was a silent no-op in earlier rafter versions (Aider has no MCP).
|
|
599
|
+
*
|
|
600
|
+
* Returns true on success.
|
|
452
601
|
*/
|
|
453
|
-
function
|
|
602
|
+
function installAiderRead(root) {
|
|
603
|
+
const rafterMdPath = path.join(root, "RAFTER.md");
|
|
454
604
|
const configPath = path.join(root, ".aider.conf.yml");
|
|
455
|
-
|
|
456
|
-
|
|
605
|
+
const RAFTER_READ_ENTRY = "RAFTER.md";
|
|
606
|
+
// 1. Write RAFTER.md (idempotent — the marker block is replaced in place).
|
|
607
|
+
injectInstructionFile(rafterMdPath);
|
|
608
|
+
// 2. Update .aider.conf.yml read: list.
|
|
609
|
+
let raw = "";
|
|
457
610
|
if (fs.existsSync(configPath)) {
|
|
458
|
-
|
|
611
|
+
raw = fs.readFileSync(configPath, "utf-8");
|
|
459
612
|
}
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
613
|
+
// 2a. Strip the legacy mcp-server-command line(s). Match a contiguous block
|
|
614
|
+
// that may include the preceding `# Rafter security MCP server` comment.
|
|
615
|
+
raw = raw.replace(/\n?#\s*Rafter security MCP server\s*\nmcp-server-command:\s*rafter\s+mcp\s+serve\s*\n?/g, "\n");
|
|
616
|
+
raw = raw.replace(/^mcp-server-command:\s*rafter\s+mcp\s+serve\s*\n?/gm, "");
|
|
617
|
+
// 2b. Parse remaining YAML, normalize read:.
|
|
618
|
+
let parsed = {};
|
|
619
|
+
if (raw.trim().length > 0) {
|
|
620
|
+
try {
|
|
621
|
+
const loaded = yaml.load(raw);
|
|
622
|
+
if (loaded && typeof loaded === "object" && !Array.isArray(loaded)) {
|
|
623
|
+
parsed = loaded;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
console.log(fmt.warning(`Existing ${configPath} was not valid YAML — preserving raw content and appending read: entry`));
|
|
628
|
+
// Append the read: line at the bottom rather than rewriting an
|
|
629
|
+
// unparseable file. We still need to make sure RAFTER.md ends up in it.
|
|
630
|
+
if (!new RegExp(`^read:\\s*\\[?[^\\n]*\\b${RAFTER_READ_ENTRY}\\b`, "m").test(raw)) {
|
|
631
|
+
const sep = raw.length > 0 && !raw.endsWith("\n") ? "\n" : "";
|
|
632
|
+
raw = `${raw}${sep}read:\n - ${RAFTER_READ_ENTRY}\n`;
|
|
633
|
+
fs.writeFileSync(configPath, raw, "utf-8");
|
|
634
|
+
}
|
|
635
|
+
console.log(fmt.success(`Installed Rafter read-only context to ${configPath}`));
|
|
636
|
+
return true;
|
|
637
|
+
}
|
|
464
638
|
}
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
639
|
+
// Normalize `read:` to a string array.
|
|
640
|
+
let reads = [];
|
|
641
|
+
if (Array.isArray(parsed.read)) {
|
|
642
|
+
reads = parsed.read.map(String);
|
|
643
|
+
}
|
|
644
|
+
else if (typeof parsed.read === "string") {
|
|
645
|
+
reads = [parsed.read];
|
|
646
|
+
}
|
|
647
|
+
if (!reads.includes(RAFTER_READ_ENTRY)) {
|
|
648
|
+
reads.push(RAFTER_READ_ENTRY);
|
|
649
|
+
}
|
|
650
|
+
parsed.read = reads;
|
|
651
|
+
fs.writeFileSync(configPath, yaml.dump(parsed), "utf-8");
|
|
652
|
+
console.log(fmt.success(`Installed Rafter read-only context to ${rafterMdPath} + ${configPath}`));
|
|
469
653
|
return true;
|
|
470
654
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
655
|
+
// Copies an entire skill source directory (SKILL.md + any subfolders like docs/)
|
|
656
|
+
// into the destination. Without this, `docs/` reference material referenced from
|
|
657
|
+
// SKILL.md would never reach users — only SKILL.md would.
|
|
658
|
+
function installSkillDir(sourceSkillDir, destSkillDir, label) {
|
|
659
|
+
const sourceSkillFile = path.join(sourceSkillDir, "SKILL.md");
|
|
660
|
+
if (!fs.existsSync(sourceSkillFile)) {
|
|
661
|
+
console.log(fmt.warning(`${label} skill template not found at ${sourceSkillFile}`));
|
|
662
|
+
return;
|
|
474
663
|
}
|
|
664
|
+
fs.mkdirSync(destSkillDir, { recursive: true });
|
|
665
|
+
fs.cpSync(sourceSkillDir, destSkillDir, { recursive: true });
|
|
666
|
+
console.log(fmt.success(`Installed ${label} skill to ${destSkillDir}`));
|
|
667
|
+
}
|
|
668
|
+
function skillResourceDir(name) {
|
|
669
|
+
return path.join(__dirname, "..", "..", "..", "resources", "skills", name);
|
|
670
|
+
}
|
|
671
|
+
function installSkillsTo(skillsDir) {
|
|
672
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
475
673
|
for (const skill of AGENT_SKILLS) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
674
|
+
installSkillDir(skillResourceDir(skill.name), path.join(skillsDir, skill.name), skill.description);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
async function installClaudeCodeSkills(root) {
|
|
678
|
+
installSkillsTo(path.join(root, ".claude", "skills"));
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Sub-agents shipped by `rafter agent init --with-claude-code`.
|
|
682
|
+
*
|
|
683
|
+
* These land in <root>/.claude/agents/<name>.md and become first-class
|
|
684
|
+
* delegation targets (Agent(subagent_type='<name>')) in the calling Claude
|
|
685
|
+
* Code session — distinct from skills, which only surface in the activation
|
|
686
|
+
* prompt. Source files live in `resources/agents/<name>.md`.
|
|
687
|
+
*
|
|
688
|
+
* Keep this list in sync with the Python installer.
|
|
689
|
+
*/
|
|
690
|
+
const CLAUDE_CODE_SUBAGENTS = [
|
|
691
|
+
{ name: "rafter", description: "Rafter Security" },
|
|
692
|
+
];
|
|
693
|
+
function installClaudeCodeSubAgents(root) {
|
|
694
|
+
const agentsDir = path.join(root, ".claude", "agents");
|
|
695
|
+
if (!fs.existsSync(agentsDir)) {
|
|
696
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
697
|
+
}
|
|
698
|
+
for (const sub of CLAUDE_CODE_SUBAGENTS) {
|
|
699
|
+
const destPath = path.join(agentsDir, `${sub.name}.md`);
|
|
700
|
+
const srcPath = path.join(__dirname, "..", "..", "..", "resources", "agents", `${sub.name}.md`);
|
|
482
701
|
if (fs.existsSync(srcPath)) {
|
|
483
702
|
fs.copyFileSync(srcPath, destPath);
|
|
484
|
-
console.log(fmt.success(`Installed ${
|
|
703
|
+
console.log(fmt.success(`Installed ${sub.description} sub-agent to ${destPath}`));
|
|
485
704
|
}
|
|
486
705
|
else {
|
|
487
|
-
console.log(fmt.warning(`${
|
|
706
|
+
console.log(fmt.warning(`${sub.description} sub-agent template not found at ${srcPath}`));
|
|
488
707
|
}
|
|
489
708
|
}
|
|
490
709
|
}
|
|
491
|
-
async function installClaudeCodeSkills(root) {
|
|
492
|
-
installSkillsTo(path.join(root, ".claude", "skills"));
|
|
493
|
-
}
|
|
494
710
|
function installCodexSkills(root) {
|
|
495
711
|
installSkillsTo(path.join(root, ".agents", "skills"));
|
|
496
712
|
}
|
|
@@ -598,14 +814,25 @@ export function createInitCommand() {
|
|
|
598
814
|
// Resolve opt-in flags (--all enables all detected, --interactive prompts).
|
|
599
815
|
// In --local scope, --all is restricted to platforms that have a project-local
|
|
600
816
|
// config story (claudeCode, codex, gemini, cursor). The rest require user scope.
|
|
817
|
+
// OpenClaw returns to --all in rf-zgwj — the integration was rebuilt
|
|
818
|
+
// to ship a ClawHub-shaped skill at the canonical workspace path
|
|
819
|
+
// (~/.openclaw/workspace/skills/rafter-security/SKILL.md), so OpenClaw
|
|
820
|
+
// actually auto-discovers it now. (User-scope only; --local doesn't
|
|
821
|
+
// apply since the platform is user-config-driven.)
|
|
601
822
|
let wantOpenClaw = opts.withOpenclaw || (opts.all && !opts.local);
|
|
602
823
|
let wantClaudeCode = opts.withClaudeCode || opts.all;
|
|
603
824
|
let wantCodex = opts.withCodex || opts.all;
|
|
604
825
|
let wantGemini = opts.withGemini || opts.all;
|
|
605
826
|
let wantCursor = opts.withCursor || opts.all;
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
let
|
|
827
|
+
// Windsurf can install at --local scope (project rules + AGENTS.md)
|
|
828
|
+
// since rf-0vr3. User scope still also installs the MCP entry.
|
|
829
|
+
let wantWindsurf = opts.withWindsurf || opts.all;
|
|
830
|
+
// Continue.dev can install at --local scope (project rules) since rf-acz0.
|
|
831
|
+
// User scope additionally registers the MCP entry.
|
|
832
|
+
let wantContinue = opts.withContinue || opts.all;
|
|
833
|
+
// Aider can install at --local scope (writes RAFTER.md + .aider.conf.yml
|
|
834
|
+
// in cwd) since rf-du2o.
|
|
835
|
+
let wantAider = opts.withAider || opts.all;
|
|
609
836
|
let wantGitleaks = opts.withGitleaks || (opts.all && !opts.local);
|
|
610
837
|
// Interactive mode: prompt for each detected integration
|
|
611
838
|
if (opts.interactive && !opts.all) {
|
|
@@ -625,7 +852,7 @@ export function createInitCommand() {
|
|
|
625
852
|
if (hasWindsurf && !wantWindsurf)
|
|
626
853
|
wantWindsurf = await askYesNo("Install Windsurf MCP + hooks?");
|
|
627
854
|
if (hasContinueDev && !wantContinue)
|
|
628
|
-
wantContinue = await askYesNo("Install Continue.dev MCP
|
|
855
|
+
wantContinue = await askYesNo("Install Continue.dev MCP server?");
|
|
629
856
|
if (hasAider && !wantAider)
|
|
630
857
|
wantAider = await askYesNo("Install Aider MCP server?");
|
|
631
858
|
if (!wantGitleaks)
|
|
@@ -793,6 +1020,7 @@ export function createInitCommand() {
|
|
|
793
1020
|
if ((hasClaudeCode || opts.withClaudeCode || (opts.local && wantClaudeCode)) && wantClaudeCode) {
|
|
794
1021
|
try {
|
|
795
1022
|
await installClaudeCodeSkills(root);
|
|
1023
|
+
installClaudeCodeSubAgents(root);
|
|
796
1024
|
installClaudeCodeHooks(root);
|
|
797
1025
|
if (scope === "project") {
|
|
798
1026
|
const components = (manager.get("agent.components") ?? {});
|
|
@@ -842,12 +1070,14 @@ export function createInitCommand() {
|
|
|
842
1070
|
console.error(fmt.error(`Failed to install Gemini CLI integration: ${e}`));
|
|
843
1071
|
}
|
|
844
1072
|
}
|
|
845
|
-
// Install Cursor MCP + hooks if opted in
|
|
1073
|
+
// Install Cursor MCP + hooks + per-skill rules + sub-agent if opted in
|
|
846
1074
|
let cursorOk = false;
|
|
847
1075
|
if ((hasCursor || (opts.local && wantCursor)) && wantCursor) {
|
|
848
1076
|
try {
|
|
849
1077
|
cursorOk = installCursorMcp(root);
|
|
850
1078
|
installCursorHooks(root);
|
|
1079
|
+
installCursorRules(root);
|
|
1080
|
+
installCursorSubAgents(root);
|
|
851
1081
|
if (cursorOk && scope === "user")
|
|
852
1082
|
manager.set("agent.environments.cursor.enabled", true);
|
|
853
1083
|
}
|
|
@@ -855,59 +1085,78 @@ export function createInitCommand() {
|
|
|
855
1085
|
console.error(fmt.error(`Failed to install Cursor integration: ${e}`));
|
|
856
1086
|
}
|
|
857
1087
|
}
|
|
858
|
-
// Install Windsurf
|
|
1088
|
+
// Install Windsurf integration if opted in.
|
|
1089
|
+
// - User scope: MCP entry under ~/.codeium/windsurf/ + per-skill rules
|
|
1090
|
+
// at .windsurf/rules/ (workspace) + AGENTS.md (workspace, written by
|
|
1091
|
+
// installGlobalInstructions below).
|
|
1092
|
+
// - Project scope (--local): per-skill rules + AGENTS.md only (no
|
|
1093
|
+
// user-scope MCP entry written from a project init).
|
|
1094
|
+
// The previous ~/.windsurf/hooks.json install was pruned: Windsurf has
|
|
1095
|
+
// no documented hook surface (rf-0vr3).
|
|
859
1096
|
let windsurfOk = false;
|
|
860
|
-
if (
|
|
1097
|
+
if (wantWindsurf && (hasWindsurf || opts.local)) {
|
|
861
1098
|
try {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1099
|
+
if (hasWindsurf) {
|
|
1100
|
+
windsurfOk = installWindsurfMcp(root);
|
|
1101
|
+
if (windsurfOk)
|
|
1102
|
+
manager.set("agent.environments.windsurf.enabled", true);
|
|
1103
|
+
}
|
|
1104
|
+
installWindsurfRules(root);
|
|
1105
|
+
// AGENTS.md is written below in installGlobalInstructions when
|
|
1106
|
+
// platforms.windsurf is true.
|
|
1107
|
+
if (!hasWindsurf)
|
|
1108
|
+
windsurfOk = true; // local-scope success: rules + AGENTS.md
|
|
866
1109
|
}
|
|
867
1110
|
catch (e) {
|
|
868
1111
|
console.error(fmt.error(`Failed to install Windsurf integration: ${e}`));
|
|
869
1112
|
}
|
|
870
1113
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
//
|
|
1114
|
+
// Install Continue.dev integration if opted in (rf-acz0).
|
|
1115
|
+
// - User scope: per-skill rules (.continue/rules/) + MCP entry under
|
|
1116
|
+
// ~/.continue/config.json.
|
|
1117
|
+
// - Project scope (--local): rules only.
|
|
1118
|
+
// Continue.dev has no hook surface — the prior hooks install was pruned
|
|
1119
|
+
// in rf-cia phase b.
|
|
875
1120
|
let continueOk = false;
|
|
876
|
-
if (
|
|
1121
|
+
if (wantContinue && (hasContinueDev || opts.local)) {
|
|
877
1122
|
try {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1123
|
+
if (hasContinueDev) {
|
|
1124
|
+
continueOk = installContinueDevMcp(root);
|
|
1125
|
+
if (continueOk)
|
|
1126
|
+
manager.set("agent.environments.continueDev.enabled", true);
|
|
1127
|
+
}
|
|
1128
|
+
installContinueDevRules(root);
|
|
1129
|
+
if (!hasContinueDev)
|
|
1130
|
+
continueOk = true; // local-scope success: rules only
|
|
882
1131
|
}
|
|
883
1132
|
catch (e) {
|
|
884
1133
|
console.error(fmt.error(`Failed to install Continue.dev integration: ${e}`));
|
|
885
1134
|
}
|
|
886
1135
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
//
|
|
1136
|
+
// Install Aider integration if opted in (rf-du2o).
|
|
1137
|
+
// Aider has no MCP and no hook surface — its only intercept is the
|
|
1138
|
+
// `read:` flag in .aider.conf.yml. We write RAFTER.md and ensure
|
|
1139
|
+
// `read:` includes it. The legacy mcp-server-command YAML line (a
|
|
1140
|
+
// silent no-op) is stripped on reinstall.
|
|
891
1141
|
let aiderOk = false;
|
|
892
|
-
if (
|
|
1142
|
+
if (wantAider && (hasAider || opts.local)) {
|
|
893
1143
|
try {
|
|
894
|
-
aiderOk =
|
|
895
|
-
if (aiderOk)
|
|
1144
|
+
aiderOk = installAiderRead(root);
|
|
1145
|
+
if (aiderOk && hasAider)
|
|
896
1146
|
manager.set("agent.environments.aider.enabled", true);
|
|
897
1147
|
}
|
|
898
1148
|
catch (e) {
|
|
899
1149
|
console.error(fmt.error(`Failed to install Aider integration: ${e}`));
|
|
900
1150
|
}
|
|
901
1151
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
// Install global instruction files for platforms that support them
|
|
1152
|
+
// Install global instruction files for platforms that support them.
|
|
1153
|
+
// Cursor is intentionally absent — Cursor uses per-skill rules + the
|
|
1154
|
+
// rafter sub-agent installed in the Cursor branch above (rf-svn3).
|
|
906
1155
|
installGlobalInstructions({
|
|
907
1156
|
claudeCode: claudeCodeOk,
|
|
908
1157
|
codex: codexOk,
|
|
909
1158
|
gemini: geminiOk,
|
|
910
|
-
|
|
1159
|
+
windsurf: windsurfOk,
|
|
911
1160
|
}, root, scope);
|
|
912
1161
|
console.log();
|
|
913
1162
|
console.log(fmt.success("Agent security initialized!"));
|
|
@@ -930,7 +1179,7 @@ export function createInitCommand() {
|
|
|
930
1179
|
if (continueOk)
|
|
931
1180
|
console.log(" - Restart Continue.dev to load MCP server");
|
|
932
1181
|
if (aiderOk)
|
|
933
|
-
console.log(" - Restart Aider to load
|
|
1182
|
+
console.log(" - Restart Aider to load RAFTER.md from .aider.conf.yml read:");
|
|
934
1183
|
}
|
|
935
1184
|
else if (scope === "project") {
|
|
936
1185
|
console.log("No integrations were installed. In --local mode, pass one or more opt-in flags:");
|