@securityreviewai/securityreview-kit 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/api.js +44 -0
  2. package/dist/commands/guardrails.js +13 -0
  3. package/dist/commands/init.js +88 -0
  4. package/dist/commands/profile.js +14 -0
  5. package/dist/commands/status.js +27 -0
  6. package/dist/commands/sync.js +6 -0
  7. package/dist/config.js +18 -0
  8. package/dist/fs.js +43 -0
  9. package/dist/index.js +44 -0
  10. package/dist/profile.js +113 -0
  11. package/dist/scaffold/claude-code.js +43 -0
  12. package/dist/scaffold/codex.js +41 -0
  13. package/dist/scaffold/cursor.js +45 -0
  14. package/dist/scaffold/gemini.js +10 -0
  15. package/dist/scaffold/index.js +22 -0
  16. package/dist/scaffold/mcp.js +15 -0
  17. package/dist/scaffold/rules.js +191 -0
  18. package/dist/scaffold/vibreview.js +30 -0
  19. package/dist/scaffold/vscode.js +28 -0
  20. package/dist/scaffold/windsurf.js +10 -0
  21. package/dist/sync/index.js +34 -0
  22. package/dist/sync/payload.js +23 -0
  23. package/dist/sync/state.js +12 -0
  24. package/dist/types.js +1 -0
  25. package/package.json +24 -30
  26. package/templates/claude/CLAUDE.md +13 -0
  27. package/templates/claude/agents/guardrail_profiler.md +12 -0
  28. package/templates/claude/agents/threat_modeler.md +5 -0
  29. package/templates/claude/skills/vibreview/SKILL.md +21 -0
  30. package/templates/claude/skills/vibreview/guardrail_patterns.md +12 -0
  31. package/templates/cursor/rules/vibreview-security.mdc +8 -0
  32. package/README.md +0 -105
  33. package/bin/securityreview-kit.js +0 -5
  34. package/src/cli.js +0 -109
  35. package/src/commands/init.js +0 -851
  36. package/src/commands/status.js +0 -99
  37. package/src/commands/switch-project.js +0 -207
  38. package/src/generators/mcp/claude.js +0 -85
  39. package/src/generators/mcp/claude.test.js +0 -64
  40. package/src/generators/mcp/codex.js +0 -70
  41. package/src/generators/mcp/codex.test.js +0 -43
  42. package/src/generators/mcp/cursor.js +0 -29
  43. package/src/generators/mcp/cursor.test.js +0 -50
  44. package/src/generators/mcp/gemini.js +0 -28
  45. package/src/generators/mcp/vscode.js +0 -29
  46. package/src/generators/mcp/windsurf.js +0 -27
  47. package/src/generators/rules/antigravity.js +0 -22
  48. package/src/generators/rules/claude.js +0 -87
  49. package/src/generators/rules/claude.test.js +0 -60
  50. package/src/generators/rules/codex.js +0 -141
  51. package/src/generators/rules/codex.test.js +0 -59
  52. package/src/generators/rules/content.js +0 -110
  53. package/src/generators/rules/cursor.js +0 -128
  54. package/src/generators/rules/gemini.js +0 -13
  55. package/src/generators/rules/guardrails-init-profile.md +0 -56
  56. package/src/generators/rules/guardrails-profiler/SKILL.md +0 -130
  57. package/src/generators/rules/guardrails-profiler/references/signal-registry.json +0 -514
  58. package/src/generators/rules/guardrails-selection/references/category-threat-map.md +0 -232
  59. package/src/generators/rules/guardrails_rule.md +0 -94
  60. package/src/generators/rules/hooks.json +0 -11
  61. package/src/generators/rules/srai-profile.md +0 -32
  62. package/src/generators/rules/vscode.js +0 -101
  63. package/src/generators/rules/vscode.test.js +0 -54
  64. package/src/generators/rules/windsurf.js +0 -13
  65. package/src/utils/constants.js +0 -95
  66. package/src/utils/cursor-agent-path.js +0 -67
  67. package/src/utils/cursor-cli-permissions.js +0 -28
  68. package/src/utils/detect.js +0 -27
  69. package/src/utils/fs-helpers.js +0 -82
  70. package/src/utils/guardrails-profiler-bundle.js +0 -84
  71. package/src/utils/ide-cli-install.js +0 -138
  72. package/src/utils/profiler-agent.js +0 -446
  73. package/src/utils/profiler-agent.test.js +0 -81
  74. package/src/utils/srai.js +0 -252
  75. /package/{src/generators/rules → templates/shared}/content.md +0 -0
  76. /package/{src/generators/rules/guardrails-selection/SKILL.md → templates/shared/guardrails-selection.md} +0 -0
  77. /package/{src/generators/rules/skill.md → templates/shared/threat-modelling.md} +0 -0
  78. /package/{src/generators/rules → templates/shared}/vibereview-sync/SKILL.md +0 -0
package/dist/api.js ADDED
@@ -0,0 +1,44 @@
1
+ import { apiBaseUrl } from "./config.js";
2
+ export class VibeReviewApiClient {
3
+ baseUrl;
4
+ apiKey;
5
+ constructor(config) {
6
+ this.baseUrl = apiBaseUrl(config).replace(/\/$/, "");
7
+ this.apiKey = config.api_key;
8
+ }
9
+ async listProjects() {
10
+ return this.request("/v1/projects");
11
+ }
12
+ async getAuthContext() {
13
+ return this.request("/v1/mcp/auth-context");
14
+ }
15
+ async getProject(projectSlug) {
16
+ return this.request(`/v1/projects/${encodeURIComponent(projectSlug)}`);
17
+ }
18
+ async getGuardrails(projectSlug) {
19
+ return this.request(`/v1/projects/${encodeURIComponent(projectSlug)}/guardrails`);
20
+ }
21
+ async syncCTMMarkdown(projectSlug, payload) {
22
+ return this.request(`/v1/mcp/projects/${encodeURIComponent(projectSlug)}/ctm-sync-markdown`, {
23
+ method: "POST",
24
+ body: payload,
25
+ });
26
+ }
27
+ async request(path, options = {}) {
28
+ if (!this.apiKey) {
29
+ throw new Error("Missing VibeReview API key in .vibreview/config.json");
30
+ }
31
+ const response = await fetch(`${this.baseUrl}${path}`, {
32
+ method: options.method ?? "GET",
33
+ headers: {
34
+ Authorization: `Bearer ${this.apiKey}`,
35
+ ...(options.body ? { "Content-Type": "application/json" } : {}),
36
+ },
37
+ body: options.body ? JSON.stringify(options.body) : undefined,
38
+ });
39
+ if (!response.ok) {
40
+ throw new Error(`VibeReview API request failed (${response.status}): ${await response.text()}`);
41
+ }
42
+ return response.json();
43
+ }
44
+ }
@@ -0,0 +1,13 @@
1
+ import chalk from "chalk";
2
+ import { VibeReviewApiClient } from "../api.js";
3
+ import { readConfig } from "../config.js";
4
+ export async function guardrailsCommand(options = {}) {
5
+ const config = await readConfig(options.cwd ?? process.cwd());
6
+ if (!config)
7
+ throw new Error("VibeReview is not initialized. Run `securityreview-kit init` first.");
8
+ const guardrails = await new VibeReviewApiClient(config).getGuardrails(config.project_slug);
9
+ console.log(chalk.bold(`${guardrails.length} guardrail(s)`));
10
+ for (const guardrail of guardrails) {
11
+ console.log(`${guardrail.type.toUpperCase()} [${guardrail.category}] ${guardrail.title}`);
12
+ }
13
+ }
@@ -0,0 +1,88 @@
1
+ import inquirer from "inquirer";
2
+ import chalk from "chalk";
3
+ import { VibeReviewApiClient } from "../api.js";
4
+ import { scaffoldProject } from "../scaffold/index.js";
5
+ export async function initCommand(options = {}) {
6
+ const cwd = options.cwd ?? process.cwd();
7
+ const answers = await promptForMissing(options);
8
+ const client = new VibeReviewApiClient({
9
+ server_url: answers.serverUrl,
10
+ api_base_url: answers.apiBaseUrl,
11
+ api_key: answers.apiKey,
12
+ });
13
+ const authContext = await client.getAuthContext();
14
+ const projects = await loadProjects(client);
15
+ const selected = await selectProject(projects, authContext, answers.projectSlug, answers.projectId, answers.yes);
16
+ const config = {
17
+ version: 1,
18
+ server_url: answers.serverUrl,
19
+ api_base_url: answers.apiBaseUrl,
20
+ api_key: answers.apiKey,
21
+ tenant_id: authContext.tenant_id,
22
+ project_slug: selected.slug,
23
+ project_id: selected.id,
24
+ project_name: selected.name,
25
+ ide: answers.ide,
26
+ };
27
+ await scaffoldProject(cwd, config);
28
+ console.log(chalk.green(`VibeReview initialized for project ${selected.slug}.`));
29
+ console.log(chalk.dim(`Tenant: ${authContext.tenant_id}`));
30
+ console.log(chalk.dim(`MCP: ${answers.serverUrl.replace(/\/$/, "")}/mcp`));
31
+ }
32
+ async function promptForMissing(options) {
33
+ const questions = [];
34
+ if (!options.ide) {
35
+ questions.push({
36
+ type: "checkbox",
37
+ name: "ide",
38
+ message: "Which IDEs do you use?",
39
+ choices: [
40
+ { name: "Claude Code", value: "claude_code" },
41
+ { name: "Cursor", value: "cursor" },
42
+ { name: "VSCode Copilot", value: "vscode_copilot" },
43
+ { name: "Codex", value: "codex" },
44
+ { name: "Gemini", value: "gemini" },
45
+ { name: "Windsurf", value: "windsurf" },
46
+ ],
47
+ default: ["claude_code"],
48
+ });
49
+ }
50
+ if (!options.serverUrl)
51
+ questions.push({ type: "input", name: "serverUrl", message: "VibeReview MCP server URL", default: "http://localhost:3000" });
52
+ if (!options.apiBaseUrl)
53
+ questions.push({ type: "input", name: "apiBaseUrl", message: "VibeReview API URL", default: "http://localhost:8000" });
54
+ if (!options.apiKey)
55
+ questions.push({ type: "password", name: "apiKey", message: "VibeReview API key", mask: "*" });
56
+ const answers = questions.length ? await inquirer.prompt(questions) : {};
57
+ return {
58
+ serverUrl: (options.serverUrl ?? answers.serverUrl),
59
+ apiBaseUrl: (options.apiBaseUrl ?? answers.apiBaseUrl),
60
+ apiKey: (options.apiKey ?? answers.apiKey),
61
+ projectSlug: options.projectSlug,
62
+ projectId: options.projectId,
63
+ ide: (options.ide ?? answers.ide ?? ["claude_code"]),
64
+ yes: options.yes ?? false,
65
+ };
66
+ }
67
+ async function loadProjects(client) {
68
+ return client.listProjects();
69
+ }
70
+ async function selectProject(projects, authContext, slug, projectId, yes) {
71
+ const requested = projects.find((project) => project.id === projectId || project.slug === slug);
72
+ if (requested)
73
+ return requested;
74
+ const scopedProject = authContext.project_id ? projects.find((project) => project.id === authContext.project_id) : undefined;
75
+ if (scopedProject)
76
+ return scopedProject;
77
+ if (slug && yes)
78
+ return { id: projectId ?? "", name: slug, slug };
79
+ if (projects.length === 0) {
80
+ throw new Error("No VibeReview projects were visible for this API key. Create a project in the SaaS app first.");
81
+ }
82
+ if (yes)
83
+ return projects[0];
84
+ const answer = await inquirer.prompt([
85
+ { type: "list", name: "project", message: "Select a VibeReview project", choices: projects.map((project) => ({ name: `${project.name} (${project.slug})`, value: project.slug })) },
86
+ ]);
87
+ return projects.find((project) => project.slug === answer.project) ?? projects[0];
88
+ }
@@ -0,0 +1,14 @@
1
+ import chalk from "chalk";
2
+ import { readConfig } from "../config.js";
3
+ export async function profileCommand(options = {}) {
4
+ const cwd = options.cwd ?? process.cwd();
5
+ const config = await readConfig(cwd);
6
+ if (!config) {
7
+ console.log(chalk.yellow("VibeReview is not initialized. Run `securityreview-kit init` first."));
8
+ return;
9
+ }
10
+ console.log(chalk.yellow("Local profiling is deprecated for VibeReview SaaS."));
11
+ console.log(`Project ${config.project_slug} is profiled server-side after project creation, and guardrails are served through the VibeReview MCP/KV path.`);
12
+ if (options.push)
13
+ console.log(chalk.dim("--push was ignored."));
14
+ }
@@ -0,0 +1,27 @@
1
+ import chalk from "chalk";
2
+ import { VibeReviewApiClient } from "../api.js";
3
+ import { readConfig } from "../config.js";
4
+ export async function statusCommand(options = {}) {
5
+ const cwd = options.cwd ?? process.cwd();
6
+ const config = await readConfig(cwd);
7
+ if (!config) {
8
+ console.log(chalk.yellow("VibeReview is not initialized. Run `securityreview-kit init`."));
9
+ return;
10
+ }
11
+ console.log(chalk.bold("VibeReview status"));
12
+ console.log(`Project: ${config.project_slug}`);
13
+ if (config.project_id)
14
+ console.log(`Project ID: ${config.project_id}`);
15
+ if (config.tenant_id)
16
+ console.log(`Tenant ID: ${config.tenant_id}`);
17
+ console.log(`MCP server: ${config.server_url}`);
18
+ console.log(`IDEs: ${config.ide.join(", ")}`);
19
+ console.log(`Last synced: ${config.last_synced ?? "never"}`);
20
+ try {
21
+ const guardrails = await new VibeReviewApiClient(config).getGuardrails(config.project_slug);
22
+ console.log(`Guardrails: ${guardrails.length}`);
23
+ }
24
+ catch (error) {
25
+ console.log(chalk.yellow(`Remote check skipped: ${error instanceof Error ? error.message : String(error)}`));
26
+ }
27
+ }
@@ -0,0 +1,6 @@
1
+ import chalk from "chalk";
2
+ import { syncTelemetry } from "../sync/index.js";
3
+ export async function syncCommand(options = {}) {
4
+ const result = await syncTelemetry(options.cwd ?? process.cwd(), { force: options.force });
5
+ console.log(chalk.green(`Synced ${result.synced} artifact(s), skipped ${result.skipped} unchanged artifact(s).`));
6
+ }
package/dist/config.js ADDED
@@ -0,0 +1,18 @@
1
+ import { join } from "node:path";
2
+ import { readJson, writeJson } from "./fs.js";
3
+ export const CONFIG_DIR = ".vibreview";
4
+ export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
5
+ export const SYNC_STATE_PATH = join(CONFIG_DIR, "sync-state.json");
6
+ export const SCANS_DIR = join(CONFIG_DIR, "scans");
7
+ export function resolveConfigPath(cwd) {
8
+ return join(cwd, CONFIG_PATH);
9
+ }
10
+ export async function readConfig(cwd) {
11
+ return readJson(resolveConfigPath(cwd));
12
+ }
13
+ export async function writeConfig(cwd, config) {
14
+ await writeJson(resolveConfigPath(cwd), config);
15
+ }
16
+ export function apiBaseUrl(config) {
17
+ return config.api_base_url ?? config.server_url.replace(/\/mcp\/?$/, "").replace(/:3000$/, ":8000");
18
+ }
package/dist/fs.js ADDED
@@ -0,0 +1,43 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ export async function ensureDir(path) {
4
+ await mkdir(path, { recursive: true });
5
+ }
6
+ export async function readText(path) {
7
+ try {
8
+ return await readFile(path, "utf8");
9
+ }
10
+ catch {
11
+ return "";
12
+ }
13
+ }
14
+ export async function writeText(path, content) {
15
+ await ensureDir(dirname(path));
16
+ await writeFile(path, content, "utf8");
17
+ }
18
+ export async function readJson(path) {
19
+ try {
20
+ return JSON.parse(await readFile(path, "utf8"));
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ export async function writeJson(path, data) {
27
+ await writeText(path, `${JSON.stringify(data, null, 2)}\n`);
28
+ }
29
+ export async function upsertBlock(path, start, end, content) {
30
+ const block = `${start}\n${content.trim()}\n${end}`;
31
+ const existing = await readText(path);
32
+ if (!existing) {
33
+ await writeText(path, `${block}\n`);
34
+ return;
35
+ }
36
+ const startIndex = existing.indexOf(start);
37
+ const endIndex = existing.indexOf(end);
38
+ if (startIndex >= 0 && endIndex >= 0) {
39
+ await writeText(path, `${existing.slice(0, startIndex)}${block}${existing.slice(endIndex + end.length)}`);
40
+ return;
41
+ }
42
+ await writeText(path, `${existing.trimEnd()}\n\n${block}\n`);
43
+ }
package/dist/index.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { guardrailsCommand } from "./commands/guardrails.js";
4
+ import { initCommand } from "./commands/init.js";
5
+ import { profileCommand } from "./commands/profile.js";
6
+ import { statusCommand } from "./commands/status.js";
7
+ import { syncCommand } from "./commands/sync.js";
8
+ const program = new Command();
9
+ program.name("securityreview-kit").description("VibeReview IDE integration CLI").version("0.1.0");
10
+ program
11
+ .command("init")
12
+ .description("Initialize VibeReview in this repository")
13
+ .option("--server-url <url>", "VibeReview MCP server URL")
14
+ .option("--api-base-url <url>", "VibeReview API URL")
15
+ .option("--api-key <key>", "VibeReview API key")
16
+ .option("--project-slug <slug>", "VibeReview project slug")
17
+ .option("--project-id <id>", "VibeReview project ID")
18
+ .option("--ide <ide...>", "IDE targets")
19
+ .option("-y, --yes", "Use provided values without project selection prompt")
20
+ .action((options) => initCommand({
21
+ serverUrl: options.serverUrl,
22
+ apiBaseUrl: options.apiBaseUrl,
23
+ apiKey: options.apiKey,
24
+ projectSlug: options.projectSlug,
25
+ projectId: options.projectId,
26
+ ide: options.ide,
27
+ yes: options.yes,
28
+ }));
29
+ program
30
+ .command("profile")
31
+ .description("Explain server-side VibeReview profiling")
32
+ .option("--push", "Deprecated; profiling runs server-side")
33
+ .action((options) => profileCommand({ push: options.push }));
34
+ program
35
+ .command("sync")
36
+ .description("Sync local scan telemetry")
37
+ .option("--force", "Re-sync unchanged artifacts")
38
+ .action((options) => syncCommand({ force: options.force }));
39
+ program.command("status").description("Show VibeReview link status").action(() => statusCommand());
40
+ program.command("guardrails").description("List project guardrails").action(() => guardrailsCommand());
41
+ program.parseAsync().catch((error) => {
42
+ console.error(error instanceof Error ? error.message : String(error));
43
+ process.exitCode = 1;
44
+ });
@@ -0,0 +1,113 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { extname, join } from "node:path";
3
+ const LANGUAGE_BY_EXTENSION = {
4
+ ".py": "python",
5
+ ".ts": "typescript",
6
+ ".tsx": "typescript",
7
+ ".js": "javascript",
8
+ ".jsx": "javascript",
9
+ ".go": "go",
10
+ ".java": "java",
11
+ ".rb": "ruby",
12
+ ".php": "php",
13
+ ".cs": "csharp",
14
+ ".rs": "rust",
15
+ ".kt": "kotlin",
16
+ ".swift": "swift",
17
+ };
18
+ const SKIP_DIRS = new Set([".git", "node_modules", ".venv", "venv", "dist", "build", ".turbo", "__pycache__"]);
19
+ export async function profileRepository(cwd) {
20
+ const files = await collectFiles(cwd);
21
+ const languageCounts = {};
22
+ const frameworks = new Set();
23
+ const databases = new Set();
24
+ const auth = new Set();
25
+ const infrastructure = new Set();
26
+ const apiProtocols = new Set();
27
+ for (const file of files) {
28
+ const language = LANGUAGE_BY_EXTENSION[extname(file)];
29
+ if (language)
30
+ languageCounts[language] = (languageCounts[language] ?? 0) + 1;
31
+ const lower = file.toLowerCase();
32
+ if (lower.endsWith("package.json")) {
33
+ const pkg = await readJsonSafe(file);
34
+ const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
35
+ if (deps.react)
36
+ frameworks.add("react");
37
+ if (deps.next)
38
+ frameworks.add("nextjs");
39
+ if (deps["@nestjs/core"])
40
+ frameworks.add("nestjs");
41
+ if (deps.express)
42
+ frameworks.add("express");
43
+ if (deps.hono)
44
+ frameworks.add("honojs");
45
+ if (deps["@apollo/server"])
46
+ apiProtocols.add("graphql");
47
+ }
48
+ if (lower.endsWith("pyproject.toml") || lower.endsWith("requirements.txt")) {
49
+ const text = await readFile(file, "utf8").catch(() => "");
50
+ if (/fastapi/i.test(text))
51
+ frameworks.add("fastapi");
52
+ if (/flask/i.test(text))
53
+ frameworks.add("flask");
54
+ if (/django/i.test(text))
55
+ frameworks.add("django");
56
+ if (/sqlalchemy/i.test(text))
57
+ frameworks.add("sqlalchemy");
58
+ }
59
+ if (lower.includes("dockerfile") || lower.endsWith("docker-compose.yml"))
60
+ infrastructure.add("docker");
61
+ if (lower.includes(".github/workflows"))
62
+ infrastructure.add("github-actions");
63
+ if (lower.includes("kubernetes") || lower.endsWith(".helm.yaml"))
64
+ infrastructure.add("kubernetes");
65
+ if (/postgres|postgresql/.test(lower))
66
+ databases.add("postgresql");
67
+ if (/redis/.test(lower))
68
+ databases.add("redis");
69
+ if (/mongo/.test(lower))
70
+ databases.add("mongodb");
71
+ if (/jwt|oauth|oidc|auth0|zitadel|okta|keycloak/.test(lower))
72
+ auth.add(RegExp.lastMatch.toLowerCase());
73
+ if (/openapi|swagger|graphql|grpc/.test(lower))
74
+ apiProtocols.add(RegExp.lastMatch.toLowerCase());
75
+ }
76
+ return {
77
+ languages: percentages(languageCounts),
78
+ frameworks: [...frameworks].sort(),
79
+ databases: [...databases].sort(),
80
+ auth: [...auth].sort(),
81
+ infrastructure: [...infrastructure].sort(),
82
+ api_protocols: [...apiProtocols].sort(),
83
+ generated_at: new Date().toISOString(),
84
+ };
85
+ }
86
+ async function collectFiles(root, current = root, output = []) {
87
+ for (const entry of await readdir(current, { withFileTypes: true }).catch(() => [])) {
88
+ if (entry.isDirectory()) {
89
+ if (!SKIP_DIRS.has(entry.name))
90
+ await collectFiles(root, join(current, entry.name), output);
91
+ continue;
92
+ }
93
+ const path = join(current, entry.name);
94
+ const info = await stat(path).catch(() => null);
95
+ if (info && info.size < 1_000_000)
96
+ output.push(path);
97
+ }
98
+ return output;
99
+ }
100
+ async function readJsonSafe(path) {
101
+ try {
102
+ return JSON.parse(await readFile(path, "utf8"));
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ function percentages(counts) {
109
+ const total = Object.values(counts).reduce((sum, value) => sum + value, 0);
110
+ if (total === 0)
111
+ return {};
112
+ return Object.fromEntries(Object.entries(counts).map(([key, value]) => [key, Math.round((value / total) * 100)]));
113
+ }
@@ -0,0 +1,43 @@
1
+ import { join } from "node:path";
2
+ import { readJson, writeJson, writeText, upsertBlock } from "../fs.js";
3
+ import { mcpRemoteServerWithType } from "./mcp.js";
4
+ import { SENTINEL_END, SENTINEL_START, claudeRuleContent, guardrailsSelectionSkillContent, syncSkillContent, threatModelSkillContent } from "./rules.js";
5
+ export async function scaffoldClaudeCode(cwd, config) {
6
+ await writeMcpConfig(cwd, config);
7
+ await writeClaudeSettings(cwd);
8
+ await writeClaudeRules(cwd, config);
9
+ }
10
+ async function writeMcpConfig(cwd, config) {
11
+ const path = join(cwd, ".mcp.json");
12
+ const existing = (await readJson(path)) ?? {};
13
+ const mcpServers = existing.mcpServers ?? {};
14
+ mcpServers.vibreview = mcpRemoteServerWithType(config);
15
+ await writeJson(path, { ...existing, mcpServers });
16
+ }
17
+ async function writeClaudeSettings(cwd) {
18
+ const path = join(cwd, ".claude", "settings.json");
19
+ const existing = (await readJson(path)) ?? {};
20
+ const enabled = Array.isArray(existing.enabledMcpjsonServers) ? existing.enabledMcpjsonServers : [];
21
+ const allow = Array.isArray(existing.permissions?.allow)
22
+ ? existing.permissions.allow.filter((item) => typeof item === "string")
23
+ : [];
24
+ await writeJson(path, {
25
+ ...existing,
26
+ enabledMcpjsonServers: enabled.includes("vibreview") ? enabled : [...enabled, "vibreview"],
27
+ permissions: {
28
+ ...(typeof existing.permissions === "object" && existing.permissions ? existing.permissions : {}),
29
+ allow: allow.includes("mcp__vibreview") ? allow : [...allow, "mcp__vibreview"],
30
+ },
31
+ });
32
+ }
33
+ async function writeClaudeRules(cwd, config) {
34
+ const paths = {
35
+ guardrailsSelectionSkillDir: ".claude/skills/guardrails-selection",
36
+ threatModellingSkillDir: ".claude/skills/threat-modelling",
37
+ vibereviewSyncSkillDir: ".claude/skills/vibereview-sync",
38
+ };
39
+ await upsertBlock(join(cwd, "CLAUDE.md"), SENTINEL_START, SENTINEL_END, claudeRuleContent(config));
40
+ await writeText(join(cwd, ".claude", "skills", "guardrails-selection", "SKILL.md"), `${guardrailsSelectionSkillContent(config, paths)}\n`);
41
+ await writeText(join(cwd, ".claude", "skills", "threat-modelling", "SKILL.md"), `${threatModelSkillContent(config, paths)}\n`);
42
+ await writeText(join(cwd, ".claude", "skills", "vibereview-sync", "SKILL.md"), `${syncSkillContent(config)}\n`);
43
+ }
@@ -0,0 +1,41 @@
1
+ import { join } from "node:path";
2
+ import { readText, upsertBlock, writeText } from "../fs.js";
3
+ import { SENTINEL_END, SENTINEL_START, codexHooksContent, codexRuleContent, guardrailsSelectionSkillContent, syncSkillContent, threatModelSkillContent } from "./rules.js";
4
+ export async function scaffoldCodex(cwd, config) {
5
+ const paths = {
6
+ guardrailsSelectionSkillDir: ".codex/skills/guardrails-selection",
7
+ threatModellingSkillDir: ".codex/skills/threat-modelling",
8
+ vibereviewSyncSkillDir: ".codex/skills/vibereview-sync",
9
+ };
10
+ await writeCodexConfig(cwd, config);
11
+ await upsertBlock(join(cwd, ".codex", "AGENTS.md"), SENTINEL_START, SENTINEL_END, codexRuleContent(config));
12
+ await writeText(join(cwd, ".codex", "hooks.json"), codexHooksContent(config));
13
+ await writeText(join(cwd, ".codex", "skills", "guardrails-selection", "SKILL.md"), `${guardrailsSelectionSkillContent(config, paths)}\n`);
14
+ await writeText(join(cwd, ".codex", "skills", "threat-modelling", "SKILL.md"), `${threatModelSkillContent(config, paths)}\n`);
15
+ await writeText(join(cwd, ".codex", "skills", "vibereview-sync", "SKILL.md"), `${syncSkillContent(config)}\n`);
16
+ }
17
+ async function writeCodexConfig(cwd, config) {
18
+ const path = join(cwd, ".codex", "config.toml");
19
+ const existing = await readText(path);
20
+ const serverBlock = `
21
+ [mcp_servers.vibreview]
22
+ command = "npx"
23
+ args = ["-y", "mcp-remote@latest", "${config.server_url.replace(/\/$/, "")}/mcp", "--header", "Authorization:\${AUTH_HEADER}"]
24
+ default_tools_approval_mode = "approve"
25
+
26
+ [mcp_servers.vibreview.env]
27
+ AUTH_HEADER = "Bearer ${escapeTomlString(config.api_key ?? "")}"
28
+ `.trim();
29
+ const updated = replaceTomlBlock(existing, serverBlock);
30
+ await writeText(path, `${updated.trim()}\n`);
31
+ }
32
+ function replaceTomlBlock(existing, serverBlock) {
33
+ if (!existing.trim())
34
+ return serverBlock;
35
+ if (!existing.includes("[mcp_servers.vibreview]"))
36
+ return `${existing.trimEnd()}\n\n${serverBlock}`;
37
+ return existing.replace(/\[mcp_servers\.vibreview][\s\S]*?(?=\n\[[^\]]+]|\s*$)/, serverBlock);
38
+ }
39
+ function escapeTomlString(value) {
40
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
41
+ }
@@ -0,0 +1,45 @@
1
+ import { join } from "node:path";
2
+ import { readJson, writeJson, writeText } from "../fs.js";
3
+ import { mcpRemoteServerWithType } from "./mcp.js";
4
+ import { cursorHooksContent, cursorRuleContent, guardrailsSelectionSkillContent, syncSkillContent, threatModelSkillContent } from "./rules.js";
5
+ export async function scaffoldCursor(cwd, config) {
6
+ const paths = {
7
+ guardrailsSelectionSkillDir: ".cursor/skills/guardrails-selection",
8
+ threatModellingSkillDir: ".cursor/skills/threat-modelling",
9
+ vibereviewSyncSkillDir: ".cursor/skills/vibereview-sync",
10
+ };
11
+ const mcpPath = join(cwd, ".cursor", "mcp.json");
12
+ const existing = (await readJson(mcpPath)) ?? {};
13
+ const mcpServers = existing.mcpServers ?? {};
14
+ mcpServers.vibreview = mcpRemoteServerWithType(config);
15
+ await writeJson(mcpPath, { ...existing, mcpServers });
16
+ await writeText(join(cwd, ".cursor", "rules", "vibreview-security.mdc"), cursorRuleFile(config));
17
+ await writeText(join(cwd, ".cursor", "hooks.json"), cursorHooksContent(config));
18
+ await writeText(join(cwd, ".cursor", "skills", "guardrails-selection", "SKILL.md"), `${guardrailsSelectionSkillContent(config, paths)}\n`);
19
+ await writeText(join(cwd, ".cursor", "skills", "threat-modelling", "SKILL.md"), `${threatModelSkillContent(config, paths)}\n`);
20
+ await writeText(join(cwd, ".cursor", "skills", "vibereview-sync", "SKILL.md"), `${syncSkillContent(config)}\n`);
21
+ await updateCursorAllowlist(cwd);
22
+ }
23
+ async function updateCursorAllowlist(cwd) {
24
+ const path = join(cwd, ".cursor", "cli.json");
25
+ const existing = (await readJson(path)) ?? {};
26
+ const permissions = existing.permissions ?? {};
27
+ const allow = Array.isArray(permissions.allow) ? permissions.allow.filter((item) => typeof item === "string") : [];
28
+ const entry = "mcp(vibreview)";
29
+ await writeJson(path, {
30
+ ...existing,
31
+ permissions: {
32
+ ...permissions,
33
+ allow: allow.includes(entry) ? allow : [...allow, entry],
34
+ },
35
+ });
36
+ }
37
+ function cursorRuleFile(config) {
38
+ return `---
39
+ description: VibeReview security workflow
40
+ alwaysApply: true
41
+ ---
42
+
43
+ ${cursorRuleContent(config)}
44
+ `;
45
+ }
@@ -0,0 +1,10 @@
1
+ import { join } from "node:path";
2
+ import { readJson, writeJson } from "../fs.js";
3
+ import { mcpRemoteServer } from "./mcp.js";
4
+ export async function scaffoldGemini(cwd, config) {
5
+ const path = join(cwd, ".gemini", "settings.json");
6
+ const existing = (await readJson(path)) ?? {};
7
+ const mcpServers = existing.mcpServers ?? {};
8
+ mcpServers.vibreview = mcpRemoteServer(config);
9
+ await writeJson(path, { ...existing, mcpServers });
10
+ }
@@ -0,0 +1,22 @@
1
+ import { scaffoldClaudeCode } from "./claude-code.js";
2
+ import { scaffoldCodex } from "./codex.js";
3
+ import { scaffoldCursor } from "./cursor.js";
4
+ import { scaffoldGemini } from "./gemini.js";
5
+ import { scaffoldVibeReview } from "./vibreview.js";
6
+ import { scaffoldVSCode } from "./vscode.js";
7
+ import { scaffoldWindsurf } from "./windsurf.js";
8
+ export async function scaffoldProject(cwd, config) {
9
+ await scaffoldVibeReview(cwd, config);
10
+ if (config.ide.includes("claude_code"))
11
+ await scaffoldClaudeCode(cwd, config);
12
+ if (config.ide.includes("cursor"))
13
+ await scaffoldCursor(cwd, config);
14
+ if (config.ide.includes("vscode_copilot"))
15
+ await scaffoldVSCode(cwd, config);
16
+ if (config.ide.includes("codex"))
17
+ await scaffoldCodex(cwd, config);
18
+ if (config.ide.includes("gemini") || config.ide.includes("antigravity"))
19
+ await scaffoldGemini(cwd, config);
20
+ if (config.ide.includes("windsurf"))
21
+ await scaffoldWindsurf(cwd, config);
22
+ }
@@ -0,0 +1,15 @@
1
+ export function mcpRemoteServer(config) {
2
+ return {
3
+ command: "npx",
4
+ args: ["-y", "mcp-remote@latest", `${config.server_url.replace(/\/$/, "")}/mcp`, "--header", "Authorization:${AUTH_HEADER}"],
5
+ env: {
6
+ AUTH_HEADER: `Bearer ${config.api_key ?? ""}`,
7
+ },
8
+ };
9
+ }
10
+ export function mcpRemoteServerWithType(config) {
11
+ return {
12
+ type: "stdio",
13
+ ...mcpRemoteServer(config),
14
+ };
15
+ }