@rafter-security/cli 0.6.4 → 0.6.6
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 +22 -22
- package/dist/commands/agent/index.js +2 -0
- package/dist/commands/agent/init-project.js +164 -0
- package/dist/commands/agent/init.js +270 -15
- package/dist/commands/agent/instruction-block.js +63 -0
- package/dist/commands/agent/verify.js +72 -0
- package/dist/commands/brief.js +526 -0
- package/dist/commands/ci/init.js +2 -2
- package/dist/commands/completion.js +30 -1
- package/dist/commands/hook/posttool.js +95 -10
- package/dist/commands/hook/pretool.js +105 -10
- package/dist/commands/mcp/server.js +5 -5
- package/dist/commands/notify.js +278 -0
- package/dist/commands/report.js +273 -0
- package/dist/core/command-interceptor.js +2 -2
- package/dist/core/risk-rules.js +4 -2
- package/dist/index.js +10 -1
- package/dist/scanners/gitleaks.js +8 -2
- package/dist/scanners/secret-patterns.js +1 -1
- package/package.json +4 -3
- package/resources/pre-commit-hook.sh +0 -5
- package/resources/rafter-security-skill.md +20 -1
- package/resources/skills/rafter/SKILL.md +28 -9
- package/resources/skills/rafter-agent-security/SKILL.md +45 -35
|
@@ -8,9 +8,45 @@ import os from "os";
|
|
|
8
8
|
import { execSync } from "child_process";
|
|
9
9
|
import { fileURLToPath } from "url";
|
|
10
10
|
import { createRequire } from "module";
|
|
11
|
+
import { createInterface } from "readline";
|
|
11
12
|
import { fmt } from "../../utils/formatter.js";
|
|
13
|
+
import { injectInstructionFile } from "./instruction-block.js";
|
|
12
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
13
15
|
const __dirname = path.dirname(__filename);
|
|
16
|
+
/**
|
|
17
|
+
* Install global instruction files for platforms that support them.
|
|
18
|
+
*
|
|
19
|
+
* Only Claude Code (~/.claude/CLAUDE.md) and Cursor (~/.cursor/rules/*.mdc)
|
|
20
|
+
* have confirmed global instruction file paths. Other platforms (Codex, Gemini,
|
|
21
|
+
* Windsurf, Continue.dev, Aider) only support project-level instruction files
|
|
22
|
+
* (AGENTS.md, GEMINI.md, .windsurfrules, .continuerules, .aider/conventions.md)
|
|
23
|
+
* which are handled by `rafter agent init-project` (not yet implemented).
|
|
24
|
+
*/
|
|
25
|
+
function installGlobalInstructions(platforms) {
|
|
26
|
+
const homeDir = os.homedir();
|
|
27
|
+
// Claude Code — ~/.claude/CLAUDE.md (confirmed global instruction file)
|
|
28
|
+
if (platforms.claudeCode) {
|
|
29
|
+
try {
|
|
30
|
+
const filePath = path.join(homeDir, ".claude", "CLAUDE.md");
|
|
31
|
+
injectInstructionFile(filePath);
|
|
32
|
+
console.log(fmt.success(`Installed Rafter instructions to ${filePath}`));
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.log(fmt.warning(`Failed to write Claude Code instructions: ${e}`));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Cursor — ~/.cursor/rules/rafter-security.mdc (global rules directory, markdown format)
|
|
39
|
+
if (platforms.cursor) {
|
|
40
|
+
try {
|
|
41
|
+
const filePath = path.join(homeDir, ".cursor", "rules", "rafter-security.mdc");
|
|
42
|
+
injectInstructionFile(filePath);
|
|
43
|
+
console.log(fmt.success(`Installed Rafter instructions to ${filePath}`));
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
console.log(fmt.warning(`Failed to write Cursor instructions: ${e}`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
14
50
|
function installClaudeCodeHooks() {
|
|
15
51
|
const homeDir = os.homedir();
|
|
16
52
|
const settingsPath = path.join(homeDir, ".claude", "settings.json");
|
|
@@ -54,6 +90,175 @@ function installClaudeCodeHooks() {
|
|
|
54
90
|
console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
|
|
55
91
|
console.log(fmt.success(`Installed PostToolUse hooks to ${settingsPath}`));
|
|
56
92
|
}
|
|
93
|
+
function installCodexHooks() {
|
|
94
|
+
const homeDir = os.homedir();
|
|
95
|
+
const codexDir = path.join(homeDir, ".codex");
|
|
96
|
+
if (!fs.existsSync(codexDir)) {
|
|
97
|
+
fs.mkdirSync(codexDir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
const hooksPath = path.join(codexDir, "hooks.json");
|
|
100
|
+
let config = {};
|
|
101
|
+
if (fs.existsSync(hooksPath)) {
|
|
102
|
+
try {
|
|
103
|
+
config = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
console.log(fmt.warning("Existing Codex hooks.json was unreadable, creating new one"));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!config.hooks)
|
|
110
|
+
config.hooks = {};
|
|
111
|
+
if (!config.hooks.PreToolUse)
|
|
112
|
+
config.hooks.PreToolUse = [];
|
|
113
|
+
if (!config.hooks.PostToolUse)
|
|
114
|
+
config.hooks.PostToolUse = [];
|
|
115
|
+
// Codex uses the same hookSpecificOutput protocol as Claude Code (format=claude)
|
|
116
|
+
const preHook = { type: "command", command: "rafter hook pretool" };
|
|
117
|
+
const postHook = { type: "command", command: "rafter hook posttool" };
|
|
118
|
+
// Remove existing rafter hooks
|
|
119
|
+
config.hooks.PreToolUse = config.hooks.PreToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook pretool")));
|
|
120
|
+
config.hooks.PostToolUse = config.hooks.PostToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook posttool")));
|
|
121
|
+
config.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] });
|
|
122
|
+
config.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
|
|
123
|
+
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
|
|
124
|
+
console.log(fmt.success(`Installed hooks to ${hooksPath}`));
|
|
125
|
+
}
|
|
126
|
+
function installCursorHooks() {
|
|
127
|
+
const homeDir = os.homedir();
|
|
128
|
+
const cursorDir = path.join(homeDir, ".cursor");
|
|
129
|
+
if (!fs.existsSync(cursorDir)) {
|
|
130
|
+
fs.mkdirSync(cursorDir, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
const hooksPath = path.join(cursorDir, "hooks.json");
|
|
133
|
+
let config = {};
|
|
134
|
+
if (fs.existsSync(hooksPath)) {
|
|
135
|
+
try {
|
|
136
|
+
config = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
console.log(fmt.warning("Existing Cursor hooks.json was unreadable, creating new one"));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!config.version)
|
|
143
|
+
config.version = 1;
|
|
144
|
+
if (!config.hooks)
|
|
145
|
+
config.hooks = {};
|
|
146
|
+
if (!config.hooks.beforeShellExecution)
|
|
147
|
+
config.hooks.beforeShellExecution = [];
|
|
148
|
+
// Remove existing rafter hooks
|
|
149
|
+
config.hooks.beforeShellExecution = config.hooks.beforeShellExecution.filter((entry) => !entry.command?.includes("rafter hook pretool"));
|
|
150
|
+
config.hooks.beforeShellExecution.push({
|
|
151
|
+
command: "rafter hook pretool --format cursor",
|
|
152
|
+
type: "command",
|
|
153
|
+
timeout: 5000,
|
|
154
|
+
});
|
|
155
|
+
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
|
|
156
|
+
console.log(fmt.success(`Installed hooks to ${hooksPath}`));
|
|
157
|
+
}
|
|
158
|
+
function installGeminiHooks() {
|
|
159
|
+
const homeDir = os.homedir();
|
|
160
|
+
const geminiDir = path.join(homeDir, ".gemini");
|
|
161
|
+
if (!fs.existsSync(geminiDir)) {
|
|
162
|
+
fs.mkdirSync(geminiDir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
const settingsPath = path.join(geminiDir, "settings.json");
|
|
165
|
+
let settings = {};
|
|
166
|
+
if (fs.existsSync(settingsPath)) {
|
|
167
|
+
try {
|
|
168
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
console.log(fmt.warning("Existing Gemini settings.json was unreadable, creating new one"));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!settings.hooks)
|
|
175
|
+
settings.hooks = {};
|
|
176
|
+
if (!settings.hooks.BeforeTool)
|
|
177
|
+
settings.hooks.BeforeTool = [];
|
|
178
|
+
if (!settings.hooks.AfterTool)
|
|
179
|
+
settings.hooks.AfterTool = [];
|
|
180
|
+
// Remove existing rafter hooks
|
|
181
|
+
settings.hooks.BeforeTool = settings.hooks.BeforeTool.filter((entry) => !(entry.hooks || []).some((h) => h.command?.includes("rafter hook pretool")));
|
|
182
|
+
settings.hooks.AfterTool = settings.hooks.AfterTool.filter((entry) => !(entry.hooks || []).some((h) => h.command?.includes("rafter hook posttool")));
|
|
183
|
+
settings.hooks.BeforeTool.push({
|
|
184
|
+
matcher: "shell|write_file",
|
|
185
|
+
hooks: [{ type: "command", command: "rafter hook pretool --format gemini", timeout: 5000 }],
|
|
186
|
+
});
|
|
187
|
+
settings.hooks.AfterTool.push({
|
|
188
|
+
matcher: ".*",
|
|
189
|
+
hooks: [{ type: "command", command: "rafter hook posttool --format gemini", timeout: 5000 }],
|
|
190
|
+
});
|
|
191
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
192
|
+
console.log(fmt.success(`Installed hooks to ${settingsPath}`));
|
|
193
|
+
}
|
|
194
|
+
function installWindsurfHooks() {
|
|
195
|
+
const homeDir = os.homedir();
|
|
196
|
+
const windsurfDir = path.join(homeDir, ".windsurf");
|
|
197
|
+
if (!fs.existsSync(windsurfDir)) {
|
|
198
|
+
fs.mkdirSync(windsurfDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
const hooksPath = path.join(windsurfDir, "hooks.json");
|
|
201
|
+
let config = {};
|
|
202
|
+
if (fs.existsSync(hooksPath)) {
|
|
203
|
+
try {
|
|
204
|
+
config = JSON.parse(fs.readFileSync(hooksPath, "utf-8"));
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
console.log(fmt.warning("Existing Windsurf hooks.json was unreadable, creating new one"));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!config.hooks)
|
|
211
|
+
config.hooks = {};
|
|
212
|
+
if (!config.hooks.pre_run_command)
|
|
213
|
+
config.hooks.pre_run_command = [];
|
|
214
|
+
if (!config.hooks.pre_write_code)
|
|
215
|
+
config.hooks.pre_write_code = [];
|
|
216
|
+
// Remove existing rafter hooks
|
|
217
|
+
config.hooks.pre_run_command = config.hooks.pre_run_command.filter((entry) => !entry.command?.includes("rafter hook pretool"));
|
|
218
|
+
config.hooks.pre_write_code = config.hooks.pre_write_code.filter((entry) => !entry.command?.includes("rafter hook pretool"));
|
|
219
|
+
config.hooks.pre_run_command.push({
|
|
220
|
+
command: "rafter hook pretool --format windsurf",
|
|
221
|
+
show_output: true,
|
|
222
|
+
});
|
|
223
|
+
config.hooks.pre_write_code.push({
|
|
224
|
+
command: "rafter hook pretool --format windsurf",
|
|
225
|
+
show_output: true,
|
|
226
|
+
});
|
|
227
|
+
fs.writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
|
|
228
|
+
console.log(fmt.success(`Installed hooks to ${hooksPath}`));
|
|
229
|
+
}
|
|
230
|
+
function installContinueDevHooks() {
|
|
231
|
+
const homeDir = os.homedir();
|
|
232
|
+
const continueDir = path.join(homeDir, ".continue");
|
|
233
|
+
if (!fs.existsSync(continueDir)) {
|
|
234
|
+
fs.mkdirSync(continueDir, { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
const settingsPath = path.join(continueDir, "settings.json");
|
|
237
|
+
let settings = {};
|
|
238
|
+
if (fs.existsSync(settingsPath)) {
|
|
239
|
+
try {
|
|
240
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
console.log(fmt.warning("Existing Continue.dev settings.json was unreadable, creating new one"));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!settings.hooks)
|
|
247
|
+
settings.hooks = {};
|
|
248
|
+
if (!settings.hooks.PreToolUse)
|
|
249
|
+
settings.hooks.PreToolUse = [];
|
|
250
|
+
if (!settings.hooks.PostToolUse)
|
|
251
|
+
settings.hooks.PostToolUse = [];
|
|
252
|
+
// Continue.dev uses the same protocol as Claude Code
|
|
253
|
+
const preHook = { type: "command", command: "rafter hook pretool" };
|
|
254
|
+
const postHook = { type: "command", command: "rafter hook posttool" };
|
|
255
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook pretool")));
|
|
256
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => !(entry.hooks || []).some((h) => h.command?.startsWith("rafter hook posttool")));
|
|
257
|
+
settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] }, { matcher: "Write|Edit", hooks: [preHook] });
|
|
258
|
+
settings.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
|
|
259
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
260
|
+
console.log(fmt.success(`Installed hooks to ${settingsPath}`));
|
|
261
|
+
}
|
|
57
262
|
/** MCP server entry for rafter — shared across MCP-native clients */
|
|
58
263
|
const RAFTER_MCP_ENTRY = {
|
|
59
264
|
command: "rafter",
|
|
@@ -266,6 +471,20 @@ function installCodexSkills() {
|
|
|
266
471
|
console.log(fmt.warning(`Agent Security skill template not found at ${agentTemplatePath}`));
|
|
267
472
|
}
|
|
268
473
|
}
|
|
474
|
+
async function askYesNo(question, defaultYes = true) {
|
|
475
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
476
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
477
|
+
return new Promise((resolve) => {
|
|
478
|
+
rl.question(` ${question} ${suffix} `, (answer) => {
|
|
479
|
+
rl.close();
|
|
480
|
+
const trimmed = answer.trim().toLowerCase();
|
|
481
|
+
if (trimmed === "")
|
|
482
|
+
resolve(defaultYes);
|
|
483
|
+
else
|
|
484
|
+
resolve(trimmed === "y" || trimmed === "yes");
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
}
|
|
269
488
|
export function createInitCommand() {
|
|
270
489
|
return new Command("init")
|
|
271
490
|
.description("Initialize agent security system")
|
|
@@ -280,6 +499,7 @@ export function createInitCommand() {
|
|
|
280
499
|
.option("--with-continue", "Install Continue.dev integration")
|
|
281
500
|
.option("--with-gitleaks", "Download and install Gitleaks binary")
|
|
282
501
|
.option("--all", "Install all detected integrations and download Gitleaks")
|
|
502
|
+
.option("-i, --interactive", "Guided setup — prompts for each detected integration")
|
|
283
503
|
.option("--update", "Re-download gitleaks and reinstall integrations without resetting config")
|
|
284
504
|
.action(async (opts) => {
|
|
285
505
|
console.log(fmt.header("Rafter Agent Security Setup"));
|
|
@@ -295,16 +515,41 @@ export function createInitCommand() {
|
|
|
295
515
|
const hasWindsurf = fs.existsSync(path.join(os.homedir(), ".codeium", "windsurf"));
|
|
296
516
|
const hasContinueDev = fs.existsSync(path.join(os.homedir(), ".continue"));
|
|
297
517
|
const hasAider = fs.existsSync(path.join(os.homedir(), ".aider.conf.yml"));
|
|
298
|
-
// Resolve opt-in flags (--all enables all detected)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
518
|
+
// Resolve opt-in flags (--all enables all detected, --interactive prompts)
|
|
519
|
+
let wantOpenClaw = opts.withOpenclaw || opts.all;
|
|
520
|
+
let wantClaudeCode = opts.withClaudeCode || opts.all;
|
|
521
|
+
let wantCodex = opts.withCodex || opts.all;
|
|
522
|
+
let wantGemini = opts.withGemini || opts.all;
|
|
523
|
+
let wantCursor = opts.withCursor || opts.all;
|
|
524
|
+
let wantWindsurf = opts.withWindsurf || opts.all;
|
|
525
|
+
let wantContinue = opts.withContinue || opts.all;
|
|
526
|
+
let wantAider = opts.withAider || opts.all;
|
|
527
|
+
let wantGitleaks = opts.withGitleaks || opts.all;
|
|
528
|
+
// Interactive mode: prompt for each detected integration
|
|
529
|
+
if (opts.interactive && !opts.all) {
|
|
530
|
+
console.log();
|
|
531
|
+
console.log(fmt.info("Select integrations to install:"));
|
|
532
|
+
console.log();
|
|
533
|
+
if (hasClaudeCode && !wantClaudeCode)
|
|
534
|
+
wantClaudeCode = await askYesNo("Install Claude Code hooks + skills?");
|
|
535
|
+
if (hasCodex && !wantCodex)
|
|
536
|
+
wantCodex = await askYesNo("Install Codex CLI skills + hooks?");
|
|
537
|
+
if (hasOpenClaw && !wantOpenClaw)
|
|
538
|
+
wantOpenClaw = await askYesNo("Install OpenClaw skill?");
|
|
539
|
+
if (hasGemini && !wantGemini)
|
|
540
|
+
wantGemini = await askYesNo("Install Gemini CLI MCP + hooks?");
|
|
541
|
+
if (hasCursor && !wantCursor)
|
|
542
|
+
wantCursor = await askYesNo("Install Cursor MCP + hooks?");
|
|
543
|
+
if (hasWindsurf && !wantWindsurf)
|
|
544
|
+
wantWindsurf = await askYesNo("Install Windsurf MCP + hooks?");
|
|
545
|
+
if (hasContinueDev && !wantContinue)
|
|
546
|
+
wantContinue = await askYesNo("Install Continue.dev MCP + hooks?");
|
|
547
|
+
if (hasAider && !wantAider)
|
|
548
|
+
wantAider = await askYesNo("Install Aider MCP server?");
|
|
549
|
+
if (!wantGitleaks)
|
|
550
|
+
wantGitleaks = await askYesNo("Download Gitleaks binary (enhanced scanning)?");
|
|
551
|
+
console.log();
|
|
552
|
+
}
|
|
308
553
|
// Show detected environments with opt-in hints
|
|
309
554
|
const detected = [];
|
|
310
555
|
if (hasOpenClaw)
|
|
@@ -465,11 +710,12 @@ export function createInitCommand() {
|
|
|
465
710
|
console.error(fmt.error(`Failed to install Claude Code integration: ${e}`));
|
|
466
711
|
}
|
|
467
712
|
}
|
|
468
|
-
// Install Codex CLI skills if opted in
|
|
713
|
+
// Install Codex CLI skills + hooks if opted in
|
|
469
714
|
let codexOk = false;
|
|
470
715
|
if (hasCodex && wantCodex) {
|
|
471
716
|
try {
|
|
472
717
|
installCodexSkills();
|
|
718
|
+
installCodexHooks();
|
|
473
719
|
manager.set("agent.environments.codex.enabled", true);
|
|
474
720
|
codexOk = true;
|
|
475
721
|
}
|
|
@@ -477,11 +723,12 @@ export function createInitCommand() {
|
|
|
477
723
|
console.error(fmt.error(`Failed to install Codex CLI integration: ${e}`));
|
|
478
724
|
}
|
|
479
725
|
}
|
|
480
|
-
// Install Gemini CLI MCP if opted in
|
|
726
|
+
// Install Gemini CLI MCP + hooks if opted in
|
|
481
727
|
let geminiOk = false;
|
|
482
728
|
if (hasGemini && wantGemini) {
|
|
483
729
|
try {
|
|
484
730
|
geminiOk = installGeminiMcp();
|
|
731
|
+
installGeminiHooks();
|
|
485
732
|
if (geminiOk)
|
|
486
733
|
manager.set("agent.environments.gemini.enabled", true);
|
|
487
734
|
}
|
|
@@ -489,11 +736,12 @@ export function createInitCommand() {
|
|
|
489
736
|
console.error(fmt.error(`Failed to install Gemini CLI integration: ${e}`));
|
|
490
737
|
}
|
|
491
738
|
}
|
|
492
|
-
// Install Cursor MCP if opted in
|
|
739
|
+
// Install Cursor MCP + hooks if opted in
|
|
493
740
|
let cursorOk = false;
|
|
494
741
|
if (hasCursor && wantCursor) {
|
|
495
742
|
try {
|
|
496
743
|
cursorOk = installCursorMcp();
|
|
744
|
+
installCursorHooks();
|
|
497
745
|
if (cursorOk)
|
|
498
746
|
manager.set("agent.environments.cursor.enabled", true);
|
|
499
747
|
}
|
|
@@ -501,11 +749,12 @@ export function createInitCommand() {
|
|
|
501
749
|
console.error(fmt.error(`Failed to install Cursor integration: ${e}`));
|
|
502
750
|
}
|
|
503
751
|
}
|
|
504
|
-
// Install Windsurf MCP if opted in
|
|
752
|
+
// Install Windsurf MCP + hooks if opted in
|
|
505
753
|
let windsurfOk = false;
|
|
506
754
|
if (hasWindsurf && wantWindsurf) {
|
|
507
755
|
try {
|
|
508
756
|
windsurfOk = installWindsurfMcp();
|
|
757
|
+
installWindsurfHooks();
|
|
509
758
|
if (windsurfOk)
|
|
510
759
|
manager.set("agent.environments.windsurf.enabled", true);
|
|
511
760
|
}
|
|
@@ -513,11 +762,12 @@ export function createInitCommand() {
|
|
|
513
762
|
console.error(fmt.error(`Failed to install Windsurf integration: ${e}`));
|
|
514
763
|
}
|
|
515
764
|
}
|
|
516
|
-
// Install Continue.dev MCP if opted in
|
|
765
|
+
// Install Continue.dev MCP + hooks if opted in
|
|
517
766
|
let continueOk = false;
|
|
518
767
|
if (hasContinueDev && wantContinue) {
|
|
519
768
|
try {
|
|
520
769
|
continueOk = installContinueDevMcp();
|
|
770
|
+
installContinueDevHooks();
|
|
521
771
|
if (continueOk)
|
|
522
772
|
manager.set("agent.environments.continueDev.enabled", true);
|
|
523
773
|
}
|
|
@@ -537,6 +787,11 @@ export function createInitCommand() {
|
|
|
537
787
|
console.error(fmt.error(`Failed to install Aider integration: ${e}`));
|
|
538
788
|
}
|
|
539
789
|
}
|
|
790
|
+
// Install global instruction files for platforms that support them
|
|
791
|
+
installGlobalInstructions({
|
|
792
|
+
claudeCode: claudeCodeOk,
|
|
793
|
+
cursor: cursorOk,
|
|
794
|
+
});
|
|
540
795
|
console.log();
|
|
541
796
|
console.log(fmt.success("Agent security initialized!"));
|
|
542
797
|
console.log();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export const RAFTER_MARKER_START = "<!-- rafter:start -->";
|
|
4
|
+
export const RAFTER_MARKER_END = "<!-- rafter:end -->";
|
|
5
|
+
/** Rafter security context block for agent instruction files (CLAUDE.md, AGENTS.md, etc.) */
|
|
6
|
+
export const RAFTER_INSTRUCTION_BLOCK = `${RAFTER_MARKER_START}
|
|
7
|
+
## Security: Rafter
|
|
8
|
+
|
|
9
|
+
This environment has Rafter available — the security toolkit built for AI workflows.
|
|
10
|
+
|
|
11
|
+
**Local scanning** (always available, no API key needed):
|
|
12
|
+
- \`rafter scan local .\` — scan for leaked credentials
|
|
13
|
+
- \`rafter scan local --staged\` — scan staged changes only
|
|
14
|
+
|
|
15
|
+
**Remote analysis** (requires RAFTER_API_KEY):
|
|
16
|
+
- \`rafter run\` — deterministic SAST, secret detection, dependency checks
|
|
17
|
+
- \`rafter run --mode plus\` — agentic deep-dive with additional analysis passes
|
|
18
|
+
|
|
19
|
+
Consider scanning before pushing, shipping features, or merging PRs.
|
|
20
|
+
If RAFTER_API_KEY is not set, local scanning still works — use what's available.
|
|
21
|
+
|
|
22
|
+
**Project integration:**
|
|
23
|
+
- \`rafter agent install-hook\` — pre-commit secret scanning
|
|
24
|
+
- \`rafter ci init\` — add scanning to CI/CD pipeline
|
|
25
|
+
- \`.rafter.yml\` — project-specific security policy
|
|
26
|
+
|
|
27
|
+
**More:**
|
|
28
|
+
- \`rafter agent audit-skill <path>\` — audit a skill before installing
|
|
29
|
+
- \`rafter agent audit --last 5\` — recent security events
|
|
30
|
+
- \`rafter brief commands\` — full CLI reference
|
|
31
|
+
${RAFTER_MARKER_END}`;
|
|
32
|
+
/**
|
|
33
|
+
* Write a Rafter instruction block into an instruction file.
|
|
34
|
+
* Uses marker comments for idempotent updates — existing rafter blocks are replaced,
|
|
35
|
+
* non-rafter content is preserved.
|
|
36
|
+
*/
|
|
37
|
+
export function injectInstructionFile(filePath) {
|
|
38
|
+
const dir = path.dirname(filePath);
|
|
39
|
+
if (!fs.existsSync(dir)) {
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
const block = RAFTER_INSTRUCTION_BLOCK;
|
|
43
|
+
const startMarker = RAFTER_MARKER_START;
|
|
44
|
+
const endMarker = RAFTER_MARKER_END;
|
|
45
|
+
let content = "";
|
|
46
|
+
if (fs.existsSync(filePath)) {
|
|
47
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
48
|
+
}
|
|
49
|
+
// Replace existing block or append
|
|
50
|
+
const startIdx = content.indexOf(startMarker);
|
|
51
|
+
const endIdx = content.indexOf(endMarker);
|
|
52
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
53
|
+
content = content.slice(0, startIdx) + block + content.slice(endIdx + endMarker.length);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
if (content.length > 0 && !content.endsWith("\n\n")) {
|
|
57
|
+
content += content.endsWith("\n") ? "\n" : "\n\n";
|
|
58
|
+
}
|
|
59
|
+
content += block + "\n";
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
@@ -87,6 +87,75 @@ function checkCodex() {
|
|
|
87
87
|
}
|
|
88
88
|
return { name, passed: true, detail: `Skills installed (${path.join(homeDir, ".agents", "skills")})` };
|
|
89
89
|
}
|
|
90
|
+
function checkGemini() {
|
|
91
|
+
const name = "Gemini CLI";
|
|
92
|
+
const homeDir = os.homedir();
|
|
93
|
+
const geminiDir = path.join(homeDir, ".gemini");
|
|
94
|
+
if (!fs.existsSync(geminiDir)) {
|
|
95
|
+
return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-gemini' to enable` };
|
|
96
|
+
}
|
|
97
|
+
const settingsPath = path.join(geminiDir, "settings.json");
|
|
98
|
+
if (!fs.existsSync(settingsPath)) {
|
|
99
|
+
return { name, passed: false, optional: true, detail: `Settings file not found: ${settingsPath} — run 'rafter agent init --with-gemini'` };
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
103
|
+
const hasRafterMcp = settings?.mcpServers?.rafter != null;
|
|
104
|
+
if (!hasRafterMcp) {
|
|
105
|
+
return { name, passed: false, optional: true, detail: "Rafter MCP server not configured — run 'rafter agent init --with-gemini'" };
|
|
106
|
+
}
|
|
107
|
+
return { name, passed: true, detail: "MCP server configured" };
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return { name, passed: false, optional: true, detail: `Cannot read settings: ${e}` };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function checkCursor() {
|
|
114
|
+
const name = "Cursor";
|
|
115
|
+
const homeDir = os.homedir();
|
|
116
|
+
const cursorDir = path.join(homeDir, ".cursor");
|
|
117
|
+
if (!fs.existsSync(cursorDir)) {
|
|
118
|
+
return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-cursor' to enable` };
|
|
119
|
+
}
|
|
120
|
+
const mcpPath = path.join(cursorDir, "mcp.json");
|
|
121
|
+
if (!fs.existsSync(mcpPath)) {
|
|
122
|
+
return { name, passed: false, optional: true, detail: `MCP config not found: ${mcpPath} — run 'rafter agent init --with-cursor'` };
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
|
|
126
|
+
const hasRafterMcp = config?.mcpServers?.rafter != null;
|
|
127
|
+
if (!hasRafterMcp) {
|
|
128
|
+
return { name, passed: false, optional: true, detail: "Rafter MCP server not configured — run 'rafter agent init --with-cursor'" };
|
|
129
|
+
}
|
|
130
|
+
return { name, passed: true, detail: "MCP server configured" };
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
return { name, passed: false, optional: true, detail: `Cannot read config: ${e}` };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function checkWindsurf() {
|
|
137
|
+
const name = "Windsurf";
|
|
138
|
+
const homeDir = os.homedir();
|
|
139
|
+
const windsurfDir = path.join(homeDir, ".codeium", "windsurf");
|
|
140
|
+
if (!fs.existsSync(windsurfDir)) {
|
|
141
|
+
return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-windsurf' to enable` };
|
|
142
|
+
}
|
|
143
|
+
const mcpPath = path.join(windsurfDir, "mcp_config.json");
|
|
144
|
+
if (!fs.existsSync(mcpPath)) {
|
|
145
|
+
return { name, passed: false, optional: true, detail: `MCP config not found: ${mcpPath} — run 'rafter agent init --with-windsurf'` };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const config = JSON.parse(fs.readFileSync(mcpPath, "utf-8"));
|
|
149
|
+
const hasRafterMcp = config?.mcpServers?.rafter != null;
|
|
150
|
+
if (!hasRafterMcp) {
|
|
151
|
+
return { name, passed: false, optional: true, detail: "Rafter MCP server not configured — run 'rafter agent init --with-windsurf'" };
|
|
152
|
+
}
|
|
153
|
+
return { name, passed: true, detail: "MCP server configured" };
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
return { name, passed: false, optional: true, detail: `Cannot read config: ${e}` };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
90
159
|
export function createVerifyCommand() {
|
|
91
160
|
return new Command("verify")
|
|
92
161
|
.description("Check agent security integration status")
|
|
@@ -100,6 +169,9 @@ export function createVerifyCommand() {
|
|
|
100
169
|
checkClaudeCode(),
|
|
101
170
|
checkOpenClaw(),
|
|
102
171
|
checkCodex(),
|
|
172
|
+
checkGemini(),
|
|
173
|
+
checkCursor(),
|
|
174
|
+
checkWindsurf(),
|
|
103
175
|
];
|
|
104
176
|
for (const r of results) {
|
|
105
177
|
if (r.passed) {
|