@securityreviewai/securityreview-kit 0.1.22 → 0.1.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@securityreviewai/securityreview-kit",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "description": "Bootstrap security-review-mcp for AI IDEs and CLI tools",
5
5
  "author": "Debarshi Das <debarshi.das@we45.com>",
6
6
  "license": "UNLICENSED",
package/src/cli.js CHANGED
@@ -26,6 +26,9 @@ export function run() {
26
26
  .option('--switch-project', 'Fetch projects and only update mapped workspace rules')
27
27
  .option('--skip-mcp', 'Skip MCP server config installation')
28
28
  .option('--skip-rules', 'Skip workspace rule installation')
29
+ .option('--skip-ide-cli-install', 'Do not install Cursor / Claude Code / Codex CLIs when those targets are selected')
30
+ .option('--profile-repo', 'After init, run the guardrails profiler agent (non-interactive; needs cursor, claude, or codex target)')
31
+ .option('--no-profile-repo', 'Skip the optional profiler agent step after init')
29
32
  .action(async (options) => {
30
33
  try {
31
34
  if (options.switchProject) {
@@ -2,6 +2,9 @@ import chalk from 'chalk';
2
2
  import { input, checkbox, confirm, select } from '@inquirer/prompts';
3
3
  import { TARGETS, TARGET_NAMES } from '../utils/constants.js';
4
4
  import { detectTargets } from '../utils/detect.js';
5
+ import { ensureIdeClisForTargets } from '../utils/ide-cli-install.js';
6
+ import { writeGuardrailsProfilerBundle } from '../utils/guardrails-profiler-bundle.js';
7
+ import { pickProfilerAgentTarget, runProfilerAgent } from '../utils/profiler-agent.js';
5
8
  import { fetchVibeReviewProjectNames, getStoredCredentials, normalizeApiUrl } from '../utils/srai.js';
6
9
 
7
10
  // Dynamic imports for generators (avoids loading all at startup)
@@ -220,18 +223,34 @@ export async function initCommand(options) {
220
223
  }
221
224
 
222
225
  // Step 1: Resolve targets
223
- console.log(chalk.bold.white(' Step 1 of 4: Select Targets'));
226
+ console.log(chalk.bold.white(' Step 1 of 5: Select Targets'));
224
227
  console.log(chalk.dim(' ─────────────────────────────────'));
225
228
  const targets = await resolveTargets(options, interactive, cwd);
226
229
  console.log(chalk.green(` ✓ Targets: ${targets.map((t) => TARGETS[t].name).join(', ')}`));
227
230
  console.log('');
231
+ console.log(chalk.bold.white(' Step 1b: IDE / agent CLIs'));
232
+ console.log(chalk.dim(' ─────────────────────────────────'));
233
+ const cliResults = ensureIdeClisForTargets(targets, { skipInstall: options.skipIdeCliInstall });
234
+ for (const r of cliResults) {
235
+ if (r.skipped) continue;
236
+ const label = TARGETS[r.target]?.name || r.target;
237
+ if (r.already) {
238
+ console.log(chalk.dim(` • ${label} CLI already available`));
239
+ } else if (r.ok) {
240
+ console.log(chalk.green(` \u2713 ${label} CLI install attempted`));
241
+ } else {
242
+ console.log(chalk.yellow(` \u26a0 ${label} CLI: ${r.message || 'install skipped or failed'}`));
243
+ }
244
+ }
245
+ console.log('');
246
+
228
247
 
229
248
  // Step 2: What to install (rules question first, then MCP)
230
249
  let installMcp = !options.skipMcp;
231
250
  let installRules = !options.skipRules;
232
251
 
233
252
  if (interactive) {
234
- console.log(chalk.bold.white(' Step 2 of 4: Installation Options'));
253
+ console.log(chalk.bold.white(' Step 2 of 5: Installation Options'));
235
254
  console.log(chalk.dim(' ─────────────────────────────────'));
236
255
  installRules = await confirm({
237
256
  message: '📋 Install workspace security rules?',
@@ -252,19 +271,19 @@ export async function initCommand(options) {
252
271
  // Step 3: Resolve credentials (only needed when MCP is being installed)
253
272
  let envVars = { apiUrl: '', apiToken: '' };
254
273
  if (installMcp) {
255
- console.log(chalk.bold.white(' Step 3 of 4: SRAI Credentials'));
274
+ console.log(chalk.bold.white(' Step 3 of 5: SRAI Credentials'));
256
275
  console.log(chalk.dim(' ─────────────────────────────────'));
257
276
  envVars = await resolveEnvVars(options, interactive, cwd);
258
277
  console.log(chalk.green(' ✓ Credentials configured'));
259
278
  } else {
260
- console.log(chalk.dim(' Step 3 of 4: Skipping SRAI credentials (MCP not selected).'));
279
+ console.log(chalk.dim(' Step 3 of 5: Skipping SRAI credentials (MCP not selected).'));
261
280
  }
262
281
  console.log('');
263
282
 
264
283
  // Step 4: Resolve project name (only needed when MCP is being installed)
265
284
  let projectName = options.projectName || process.env.SECURITY_REVIEW_PROJECT_NAME || '';
266
285
  if (installMcp) {
267
- console.log(chalk.bold.white(' Step 4 of 4: SRAI Project Mapping'));
286
+ console.log(chalk.bold.white(' Step 4 of 5: SRAI Project Mapping'));
268
287
  console.log(chalk.dim(' ─────────────────────────────────'));
269
288
  projectName = await resolveProjectName(options, interactive, envVars.apiUrl, envVars.apiToken);
270
289
  if (projectName) {
@@ -273,11 +292,14 @@ export async function initCommand(options) {
273
292
  console.log(chalk.dim(' No project name provided. Rules will use a generic project lookup instruction.'));
274
293
  }
275
294
  } else {
276
- console.log(chalk.dim(' Step 4 of 4: Skipping SRAI project mapping (MCP not selected).'));
295
+ console.log(chalk.dim(' Step 4 of 5: Skipping SRAI project mapping (MCP not selected).'));
277
296
  }
278
297
  console.log('');
279
298
 
280
- console.log(chalk.bold.white(' Installing...'));
299
+ const projectNameForSkill =
300
+ (projectName || options.projectName || process.env.SECURITY_REVIEW_PROJECT_NAME || '').trim();
301
+
302
+ console.log(chalk.bold.white(' Step 5 of 5: Installing workspace files'));
281
303
  console.log(chalk.dim(' ─────────────────────────────────'));
282
304
 
283
305
  const results = [];
@@ -322,6 +344,17 @@ export async function initCommand(options) {
322
344
  }
323
345
  }
324
346
 
347
+ if (installRules) {
348
+ try {
349
+ const bundlePath = writeGuardrailsProfilerBundle(cwd, { projectName: projectNameForSkill });
350
+ console.log(chalk.green(` \u2713 Guardrails profiler bundle → ${bundlePath}`));
351
+ results.push({ target: 'kit', type: 'bundle', status: 'ok', path: bundlePath });
352
+ } catch (err) {
353
+ console.log(chalk.red(` \u2717 Guardrails profiler bundle failed: ${err.message}`));
354
+ results.push({ target: 'kit', type: 'bundle', status: 'error', error: err.message });
355
+ }
356
+ }
357
+
325
358
  // Summary
326
359
  const ok = results.filter((r) => r.status === 'ok').length;
327
360
  const errors = results.filter((r) => r.status === 'error').length;
@@ -335,6 +368,52 @@ export async function initCommand(options) {
335
368
  chalk.bold.yellow(` ⚠ Done with ${errors} error(s). ${ok} configuration(s) installed.`),
336
369
  );
337
370
  }
371
+ console.log('');
372
+
373
+ const profilerEligible =
374
+ installMcp && installRules && projectNameForSkill && options.profileRepo !== false;
375
+
376
+ let shouldProfile = false;
377
+ if (profilerEligible) {
378
+ if (interactive) {
379
+ shouldProfile = await confirm({
380
+ message:
381
+ 'Profile this repository and push the default guardrail pack to SecurityReview.ai (runs your IDE agent CLI)?',
382
+ default: true,
383
+ });
384
+ } else {
385
+ shouldProfile = Boolean(options.profileRepo);
386
+ }
387
+ }
388
+
389
+ if (shouldProfile) {
390
+ const agentTarget = pickProfilerAgentTarget(targets);
391
+ if (!agentTarget) {
392
+ console.log(
393
+ chalk.yellow(
394
+ ' \u26a0 Profiling needs Cursor, Claude Code, or Codex in your targets. Add one and re-run, or run the guardrails-init-profile command in your IDE.',
395
+ ),
396
+ );
397
+ } else {
398
+ console.log('');
399
+ console.log(chalk.bold.white(` Starting profiler via ${TARGETS[agentTarget].name} CLI…`));
400
+ console.log(chalk.dim(' (Sign-in or approvals may be required in your terminal.)\n'));
401
+ const pr = runProfilerAgent(cwd, { target: agentTarget, projectName: projectNameForSkill });
402
+ if (pr.ok) {
403
+ console.log(chalk.green(' \u2713 Profiler agent finished.'));
404
+ } else {
405
+ const detail =
406
+ pr.message ||
407
+ (typeof pr.status === 'number' ? `exit status ${pr.status}` : 'unknown error');
408
+ console.log(
409
+ chalk.yellow(
410
+ ` \u26a0 Profiler agent exited with an error: ${detail}. You can run the guardrails-init-profile workflow manually.`,
411
+ ),
412
+ );
413
+ }
414
+ }
415
+ }
416
+
338
417
  console.log('');
339
418
  console.log(chalk.dim(' Run `securityreview-kit status` to verify your setup.'));
340
419
  console.log('');
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
- import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
3
- import { getRuleContent } from './content.js';
2
+ import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent, getGuardrailsInitProfileContent } from './content.js';
4
4
 
5
5
  /**
6
6
  * Generate Claude Code workspace rule — appends to CLAUDE.md
@@ -9,5 +9,13 @@ export function generate(cwd, options = {}) {
9
9
  const filePath = join(cwd, 'CLAUDE.md');
10
10
  const content = getRuleContent(options);
11
11
  const action = upsertSentinelBlock(filePath, content);
12
- return { filePath, action };
12
+
13
+ const guardrailsInitPath = join(cwd, '.claude', 'commands', 'guardrails-init-profile.md');
14
+ const guardrailsInitContent = getGuardrailsInitProfileContent(options);
15
+ writeText(guardrailsInitPath, guardrailsInitContent);
16
+
17
+ return [
18
+ { filePath, action, kind: 'rule' },
19
+ { filePath: guardrailsInitPath, action: 'created', kind: 'command' },
20
+ ];
13
21
  }
@@ -1,6 +1,6 @@
1
1
  import { join } from 'node:path';
2
- import { upsertSentinelBlock } from '../../utils/fs-helpers.js';
3
- import { getRuleContent } from './content.js';
2
+ import { upsertSentinelBlock, writeText } from '../../utils/fs-helpers.js';
3
+ import { getRuleContent, getGuardrailsInitProfileContent } from './content.js';
4
4
 
5
5
  /**
6
6
  * Generate Codex workspace rule — appends to AGENTS.md
@@ -9,5 +9,13 @@ export function generate(cwd, options = {}) {
9
9
  const filePath = join(cwd, 'AGENTS.md');
10
10
  const content = getRuleContent(options);
11
11
  const action = upsertSentinelBlock(filePath, content);
12
- return { filePath, action };
12
+
13
+ const guardrailsInitPath = join(cwd, '.codex', 'commands', 'guardrails-init-profile.md');
14
+ const guardrailsInitContent = getGuardrailsInitProfileContent(options);
15
+ writeText(guardrailsInitPath, guardrailsInitContent);
16
+
17
+ return [
18
+ { filePath, action, kind: 'rule' },
19
+ { filePath: guardrailsInitPath, action: 'created', kind: 'command' },
20
+ ];
13
21
  }
@@ -71,6 +71,13 @@ export function getGuardrailsRuleContent(options = {}) {
71
71
  return readTemplate('guardrails_rule.md', options);
72
72
  }
73
73
 
74
+ /**
75
+ * Guardrails init profile command (repo scan + profile.json + SRAI upload).
76
+ */
77
+ export function getGuardrailsInitProfileContent(options = {}) {
78
+ return readTemplate('guardrails-init-profile.md', options);
79
+ }
80
+
74
81
  /**
75
82
  * Returns the hooks.json content for Cursor session hooks.
76
83
  */
@@ -119,4 +119,4 @@ When running `ctm_sync` (dedicated agent/command where available, or the same st
119
119
  | **Analysis** | `get_threat_scenarios`, `get_countermeasures`, `get_components`, `get_data_dictionaries`, `get_security_objectives`, `get_findings`, `get_security_test_cases` |
120
120
  | **Integrations** | `fetch_jira_issue`, `fetch_confluence_page`, `search_confluence_pages`, `fetch_and_link_to_srai` |
121
121
  | **AI IDE CTM** | `create_ai_ide_workflow`, `create_ai_ide_event` (and any `list_*` AI IDE workflow tools exposed by the server) |
122
- | **Profile Update** | `update_vibe_project_profile` |
122
+ | **Vibe profile & default packs** | `update_vibe_profile`, `write_default_pack` (used by the guardrails profiler / init flow — not part of `ctm_sync`) |
@@ -34,14 +34,7 @@ When invoked:
34
34
  6. **Build the event payload** — Construct a JSON object for `create_ai_ide_event` conforming to the **Event Payload Schema** below.
35
35
  7. **Upload the payload** using `security-review-mcp`:
36
36
  - Call `create_ai_ide_event` with the JSON payload.
37
- 8. **Push a project profile update** After the event is created, derive project profile data from the current threat model context and call `update_vibe_project_profile` with `project_id` and the following fields:
38
- - `description`: a concise summary of the system/project derived from the threat model context (what it does, what data it handles, its overall purpose)
39
- - `architecture_notes`: a list of architecture observations extracted from the threat model — deployment topology, trust boundaries, data flows, integration points
40
- - `tech_categories`: a list of technology category labels identified during threat modeling (e.g. `"backend"`, `"frontend"`, `"database"`, `"cloud"`, `"mobile"`)
41
- - `user_groups`: a list of user roles and groups surfaced in the threat model (e.g. `"admin"`, `"end user"`, `"service account"`)
42
- - `compliance_requirements`: a list of compliance requirements or standards referenced in the threat model or conversation (e.g. `"PCI-DSS"`, `"HIPAA"`, `"SOC 2"`)
43
- - `language_stacks`: a list of programming languages, runtimes, and major frameworks identified in the codebase or threat model context (e.g. `"Python/FastAPI"`, `"TypeScript/React"`, `"Java/Spring"`)
44
- - Omit any field for which no data is available in context — do **not** invent values. Pass an empty list `[]` only if the field is reasonably expected but simply unpopulated.
37
+ - **Stop here.** Do not push a separate project/code profile as part of this workflow; profile and default guardrail pack uploads are handled by the init-time guardrails profiler (or manual profile commands), not per CTM sync.
45
38
 
46
39
  ---
47
40
 
@@ -155,8 +148,6 @@ Use the following IDs and names exactly when populating `owasp_top_10_2025_mappi
155
148
  - Never skip upload when a threat model exists.
156
149
  - Never invent missing values; use empty strings/arrays if data is unavailable.
157
150
  - Never omit `chat_session_id` from the payload.
158
- - Never skip the `update_vibe_project_profile` call when profile-relevant data (architecture, tech, users, compliance, languages, or description) can be derived from context.
159
151
  - Return a compact confirmation after upload including:
160
152
  - Whether an existing workflow was reused or a new named workflow was created
161
- - Confirmation that the project profile was updated
162
153
  - Count of guardrails applied (existing vs IDE-generated)
@@ -9,6 +9,7 @@ import {
9
9
  getCreateIdeWorkflowCommandContent,
10
10
  getThreatModellingSkillContent,
11
11
  getGuardrailsRuleContent,
12
+ getGuardrailsInitProfileContent,
12
13
  getHooksContent,
13
14
  } from './content.js';
14
15
 
@@ -44,6 +45,7 @@ export function generate(cwd, options = {}) {
44
45
  const ctmSyncAgentPath = join(cwd, '.cursor', 'agents', 'ctm_sync.md');
45
46
  const createIdeWorkflowCommandPath = join(cwd, '.cursor', 'commands', 'create-ide-workflow.md');
46
47
  const profileCommandPath = join(cwd, '.cursor', 'commands', 'srai-profile.md');
48
+ const guardrailsInitProfileCommandPath = join(cwd, '.cursor', 'commands', 'guardrails-init-profile.md');
47
49
  const skillPath = join(cwd, '.cursor', 'skills', 'threat-modelling', 'SKILL.md');
48
50
  const hooksPath = join(cwd, '.cursor', 'hooks.json');
49
51
 
@@ -53,6 +55,7 @@ export function generate(cwd, options = {}) {
53
55
  const ctmSyncWorkflowContent = getCtmSyncWorkflowContent(options);
54
56
  const createIdeWorkflowCommandContent = getCreateIdeWorkflowCommandContent(options);
55
57
  const profileCommandContent = getProfileCommandContent(options);
58
+ const guardrailsInitProfileCommandContent = getGuardrailsInitProfileContent(options);
56
59
  const skillContent = getThreatModellingSkillContent(options);
57
60
 
58
61
  const baseRule = writeCursorRule(
@@ -72,6 +75,10 @@ export function generate(cwd, options = {}) {
72
75
  const createIdeWorkflowCommand = writeCursorCommand(createIdeWorkflowCommandPath, createIdeWorkflowCommandContent);
73
76
 
74
77
  const profileCommand = writeCursorCommand(profileCommandPath, profileCommandContent);
78
+ const guardrailsInitProfileCommand = writeCursorCommand(
79
+ guardrailsInitProfileCommandPath,
80
+ guardrailsInitProfileCommandContent,
81
+ );
75
82
  const skillAction = existsSync(skillPath) ? 'updated' : 'created';
76
83
  writeText(skillPath, skillContent);
77
84
 
@@ -87,6 +94,7 @@ export function generate(cwd, options = {}) {
87
94
  { ...ctmSyncAgent, kind: 'agent' },
88
95
  { ...createIdeWorkflowCommand, kind: 'command' },
89
96
  { ...profileCommand, kind: 'command' },
97
+ { ...guardrailsInitProfileCommand, kind: 'command' },
90
98
  { filePath: skillPath, action: skillAction, kind: 'skill' },
91
99
  { filePath: hooksPath, action: hooksAction, kind: 'hooks' },
92
100
  ];
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: guardrails-init-profile
3
+ description: Run the Security Review Kit guardrails profiler — scan the repo, write profile.json, push profile and default guardrail pack to SecurityReview.ai via MCP.
4
+ ---
5
+
6
+ # Guardrails init profile
7
+
8
+ Execute the workflow defined in **`.securityreview-kit/guardrails-profiler/SKILL.md`** end-to-end in this workspace.
9
+
10
+ Configured SRAI project name: `<SRAI_PROJECT_NAME>`
11
+
12
+ **You must:**
13
+
14
+ 1. Read `.securityreview-kit/guardrails-profiler/SKILL.md` and follow every step (use the signal registry at `.securityreview-kit/guardrails-profiler/references/signal-registry.json`).
15
+ 2. Write `.guardrails/profile.json` and **`profile.json`** at the project root as specified.
16
+ 3. Call **`update_vibe_profile`** and **`write_default_pack`** on `security-review-mcp` after resolving `project_id` for `<SRAI_PROJECT_NAME>`.
17
+
18
+ Do not skip MCP upload when credentials and MCP are available.
@@ -0,0 +1,163 @@
1
+ ---
2
+ name: guardrails-profiler
3
+ description: Profile a codebase to detect its technology stack and generate a guardrails profile for security-aware AI code generation, then publish the profile and default guardrail pack to SecurityReview.ai via security-review-mcp. Use when Security Review Kit init runs profiling, when `.guardrails/profile.json` is missing, or when the developer asks to profile or re-profile the project.
4
+ ---
5
+
6
+ # Guardrails Profiler
7
+
8
+ Profile a codebase's technology stack, write `.guardrails/profile.json`, write a combined **`profile.json` in the project root**, and upload to SRAI using `update_vibe_profile` and `write_default_pack`.
9
+
10
+ Configured SRAI project name: `<SRAI_PROJECT_NAME>`
11
+
12
+ ## Canonical paths
13
+
14
+ - **Signal registry (read-only):** `.securityreview-kit/guardrails-profiler/references/signal-registry.json`
15
+ - **Local guardrails file:** `.guardrails/profile.json`
16
+ - **Combined manifest (project root):** `profile.json` — includes guardrails profile, vibe profile fields for MCP, and default pack payload
17
+
18
+ ## When This Runs
19
+
20
+ 1. **Kit init**: User opted in to profile the repo and push the default pack.
21
+ 2. **First-run / missing profile**: No `.guardrails/profile.json` and guardrails are needed before threat modeling.
22
+ 3. **Explicit re-profile**: Developer asks to refresh the profile.
23
+
24
+ ## Quick Check: Should I Profile?
25
+
26
+ Before profiling, check if a profile already exists:
27
+
28
+ - If `.guardrails/profile.json` exists and has a valid `schema_version`: **SKIP** unless the developer asked to re-profile.
29
+ - If missing: **PROCEED**.
30
+
31
+ ## Profiling Procedure
32
+
33
+ Follow these steps in order.
34
+
35
+ ### Step 1: Locate the Project Root
36
+
37
+ The project root is the current working directory. Confirm with markers such as `.git/`, `package.json`, `go.mod`, etc.
38
+
39
+ ### Step 2: Read the Signal Registry
40
+
41
+ Read `.securityreview-kit/guardrails-profiler/references/signal-registry.json`. Use its categories (`universal`, `languages`, `frameworks`, `auth_identity`, `ai_agent`, `infrastructure`, `ci_cd`, `cloud_compute`, `databases`, `messaging`, `api_protocols`, etc.) to map detected signals to guardrail pack IDs.
42
+
43
+ ### Step 3: Scan for Signals
44
+
45
+ Same methodology as the upstream guardrails-profiler skill:
46
+
47
+ #### 3a. Manifest and Config File Detection
48
+
49
+ List files in the project root (1–2 levels deep). Detect manifests (`package.json`, `pyproject.toml`, `go.mod`, `pom.xml`, `Dockerfile`, `.github/workflows/`, `next.config.*`, etc.) per the registry.
50
+
51
+ #### 3b. Dependency Parsing
52
+
53
+ For each manifest found, read dependencies and match names against `dependency_signals` in the registry. Prefer manifests over extension-only guesses.
54
+
55
+ #### 3c. Content Signals (targeted only)
56
+
57
+ When needed, grep specific files (e.g. Terraform providers, K8s `apiVersion`, CloudFormation) — do not read the entire repository.
58
+
59
+ #### 3d. File Extension Fallback
60
+
61
+ If no manifest exists for a language, use dominant extensions as a last resort.
62
+
63
+ ### Step 4: Assemble the Guardrails Profile Object
64
+
65
+ Build the object for `.guardrails/profile.json`:
66
+
67
+ ```json
68
+ {
69
+ "schema_version": "1.0",
70
+ "project_name": "<directory name>",
71
+ "profiled_at": "<ISO 8601 timestamp>",
72
+ "profiled_by": "<ide or cli id, e.g. cursor-agent, claude, codex>",
73
+ "detection_summary": {
74
+ "languages": [],
75
+ "frameworks": [],
76
+ "infrastructure": [],
77
+ "databases": [],
78
+ "auth": [],
79
+ "ai_agent": [],
80
+ "ci_cd": [],
81
+ "cloud_compute": [],
82
+ "messaging": [],
83
+ "api_protocols": [],
84
+ "mobile": false
85
+ },
86
+ "guardrail_packs": [],
87
+ "pack_count": 0
88
+ }
89
+ ```
90
+
91
+ Rules for `guardrail_packs`:
92
+
93
+ 1. Always include `owasp-asvs`.
94
+ 2. Include `owasp-masvs` if mobile stacks are detected (flutter, react-native, swift, objective-c, kotlin per registry).
95
+ 3. Add language, framework, auth, AI, infra, CI/CD, cloud, DB, messaging, and API packs per registry matches.
96
+ 4. Deduplicate and set `pack_count`.
97
+
98
+ Do **not** invent packs or signals; if the repo is empty, use universal baseline only and empty category arrays where appropriate.
99
+
100
+ ### Step 5: Write `.guardrails/profile.json`
101
+
102
+ Create `.guardrails/` if needed and write the profile file.
103
+
104
+ ### Step 6: Build `profile.json` (project root)
105
+
106
+ Write **`profile.json`** at the project root with this structure (all parts required; use empty strings or `[]` when unknown — never fabricate compliance or user groups):
107
+
108
+ ```json
109
+ {
110
+ "schema_version": "2.0",
111
+ "srai_project_name": "<SRAI_PROJECT_NAME>",
112
+ "guardrails_profile": {},
113
+ "vibe_profile": {
114
+ "description": "<short summary of what the repo appears to be, from detected stack only>",
115
+ "architecture_notes": [],
116
+ "tech_categories": [],
117
+ "user_groups": [],
118
+ "compliance_requirements": [],
119
+ "language_stacks": []
120
+ },
121
+ "default_guardrail_pack": {
122
+ "guardrail_packs": [],
123
+ "pack_count": 0
124
+ }
125
+ }
126
+ ```
127
+
128
+ Populate `guardrails_profile` with the **same object** written to `.guardrails/profile.json`.
129
+
130
+ Populate `default_guardrail_pack.guardrail_packs` with the deduplicated pack id list (same as `guardrails_profile.guardrail_packs`) and `pack_count`.
131
+
132
+ Derive `vibe_profile` fields **only** from what you observed:
133
+
134
+ - `tech_categories`: e.g. `backend`, `frontend`, `database`, `cloud`, `mobile` when supported by files/deps.
135
+ - `language_stacks`: strings like `TypeScript/Node`, `Python/FastAPI` from detection.
136
+ - `architecture_notes`: short bullets (e.g. "Dockerfile present", "GitHub Actions CI") — not speculative threat narratives.
137
+ - `user_groups` / `compliance_requirements`: only if explicit in repo (e.g. compliance docs); else `[]`.
138
+
139
+ ### Step 7: Upload to SecurityReview.ai (security-review-mcp)
140
+
141
+ 1. Resolve `project_id`: `find_project_by_name` with `name="<SRAI_PROJECT_NAME>"`. If missing, follow existing kit rules (`list_projects`, `create_project`).
142
+
143
+ 2. Call **`update_vibe_profile`** with `project_id` and the fields from `profile.json.vibe_profile` (map parameter names to the MCP tool’s expected shape; pass only fields the tool accepts).
144
+
145
+ 3. Call **`write_default_pack`** with `project_id` and the default pack payload from `profile.json.default_guardrail_pack` (match the MCP tool’s schema — typically the pack id list and metadata the server expects).
146
+
147
+ 4. Confirm success to the user: paths written (`profile.json`, `.guardrails/profile.json`) and that both MCP calls completed or the exact error.
148
+
149
+ ### Step 8: Report
150
+
151
+ Give a concise summary of detected stack, pack count, and upload status.
152
+
153
+ ## Empty / New Repository Handling
154
+
155
+ If there are no signals:
156
+
157
+ 1. Optionally read `.git/config` for hints.
158
+ 2. Emit minimal profile: `owasp-asvs` only, empty summaries where appropriate.
159
+ 3. Still write `profile.json` and attempt MCP calls with honest minimal `vibe_profile` data.
160
+
161
+ ## IDE-Specific Notes
162
+
163
+ When run from Cursor Agent CLI, Claude Code, or Codex CLI, set `profiled_by` to a stable id (`cursor-agent`, `claude`, `codex`).
@@ -0,0 +1,514 @@
1
+ {
2
+ "_doc": "Maps filesystem signals to guardrail pack IDs. Each entry defines what to look for and which pack to activate.",
3
+
4
+ "universal": {
5
+ "_doc": "Always included regardless of detection",
6
+ "packs": ["owasp-asvs"]
7
+ },
8
+
9
+ "mobile_universal": {
10
+ "_doc": "Included when any mobile platform is detected",
11
+ "trigger_packs": ["flutter", "react-native", "swift", "objective-c", "kotlin"],
12
+ "packs": ["owasp-masvs"]
13
+ },
14
+
15
+ "languages": {
16
+ "typescript": {
17
+ "manifest_files": ["tsconfig.json", "tsconfig.base.json"],
18
+ "file_extensions": [".ts", ".tsx"],
19
+ "pack": "typescript"
20
+ },
21
+ "nodejs": {
22
+ "manifest_files": ["package.json"],
23
+ "file_extensions": [".js", ".mjs", ".cjs"],
24
+ "pack": "nodejs"
25
+ },
26
+ "python": {
27
+ "manifest_files": ["requirements.txt", "pyproject.toml", "setup.py", "setup.cfg", "Pipfile", "poetry.lock"],
28
+ "file_extensions": [".py"],
29
+ "pack": null,
30
+ "_note": "Python has no standalone pack; frameworks (django, flask, fastapi) cover it"
31
+ },
32
+ "go": {
33
+ "manifest_files": ["go.mod", "go.sum"],
34
+ "file_extensions": [".go"],
35
+ "pack": "go"
36
+ },
37
+ "java": {
38
+ "manifest_files": ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"],
39
+ "file_extensions": [".java"],
40
+ "pack": null,
41
+ "_note": "Java framework packs (spring-boot, java-ee) are more specific"
42
+ },
43
+ "kotlin": {
44
+ "manifest_files": ["build.gradle.kts"],
45
+ "file_extensions": [".kt", ".kts"],
46
+ "dependency_signals": { "build.gradle.kts": ["kotlin"] },
47
+ "pack": "kotlin"
48
+ },
49
+ "php": {
50
+ "manifest_files": ["composer.json", "composer.lock"],
51
+ "file_extensions": [".php"],
52
+ "pack": "php"
53
+ },
54
+ "ruby": {
55
+ "manifest_files": ["Gemfile", "Gemfile.lock"],
56
+ "file_extensions": [".rb"],
57
+ "pack": null,
58
+ "_note": "Ruby framework pack (ruby-on-rails) is more specific"
59
+ },
60
+ "rust": {
61
+ "manifest_files": ["Cargo.toml", "Cargo.lock"],
62
+ "file_extensions": [".rs"],
63
+ "pack": "rust"
64
+ },
65
+ "c": {
66
+ "file_extensions": [".c", ".h"],
67
+ "manifest_files": ["CMakeLists.txt", "Makefile"],
68
+ "pack": "c"
69
+ },
70
+ "cpp": {
71
+ "file_extensions": [".cpp", ".cxx", ".cc", ".hpp", ".hxx"],
72
+ "manifest_files": ["CMakeLists.txt"],
73
+ "pack": "cpp"
74
+ },
75
+ "swift": {
76
+ "manifest_files": ["Package.swift"],
77
+ "file_extensions": [".swift"],
78
+ "config_files": ["*.xcodeproj", "*.xcworkspace"],
79
+ "pack": "swift"
80
+ },
81
+ "objective-c": {
82
+ "file_extensions": [".m", ".mm"],
83
+ "pack": "objective-c"
84
+ },
85
+ "groovy": {
86
+ "file_extensions": [".groovy"],
87
+ "pack": "groovy"
88
+ },
89
+ "csharp": {
90
+ "manifest_files": ["*.csproj", "*.sln"],
91
+ "file_extensions": [".cs"],
92
+ "pack": null,
93
+ "_note": "C# framework pack (dotnet-aspnet-core) is more specific"
94
+ }
95
+ },
96
+
97
+ "frameworks": {
98
+ "express": {
99
+ "ecosystem": "nodejs",
100
+ "dependency_signals": { "package.json": ["express"] },
101
+ "pack": "express"
102
+ },
103
+ "fastify": {
104
+ "ecosystem": "nodejs",
105
+ "dependency_signals": { "package.json": ["fastify"] },
106
+ "pack": "fastify"
107
+ },
108
+ "honojs": {
109
+ "ecosystem": "nodejs",
110
+ "dependency_signals": { "package.json": ["hono"] },
111
+ "pack": "honojs"
112
+ },
113
+ "nestjs": {
114
+ "ecosystem": "nodejs",
115
+ "dependency_signals": { "package.json": ["@nestjs/core"] },
116
+ "config_files": ["nest-cli.json"],
117
+ "pack": "nestjs"
118
+ },
119
+ "nextjs": {
120
+ "ecosystem": "nodejs",
121
+ "dependency_signals": { "package.json": ["next"] },
122
+ "config_files": ["next.config.js", "next.config.mjs", "next.config.ts"],
123
+ "pack": "nextjs"
124
+ },
125
+ "react": {
126
+ "ecosystem": "nodejs",
127
+ "dependency_signals": { "package.json": ["react", "react-dom"] },
128
+ "pack": "react"
129
+ },
130
+ "angular": {
131
+ "ecosystem": "nodejs",
132
+ "dependency_signals": { "package.json": ["@angular/core"] },
133
+ "config_files": ["angular.json"],
134
+ "pack": "angular"
135
+ },
136
+ "vuejs": {
137
+ "ecosystem": "nodejs",
138
+ "dependency_signals": { "package.json": ["vue"] },
139
+ "config_files": ["vue.config.js", "vite.config.ts", "nuxt.config.ts"],
140
+ "pack": "vuejs"
141
+ },
142
+ "electron": {
143
+ "ecosystem": "nodejs",
144
+ "dependency_signals": { "package.json": ["electron"] },
145
+ "pack": "electron"
146
+ },
147
+ "react-native": {
148
+ "ecosystem": "nodejs",
149
+ "dependency_signals": { "package.json": ["react-native"] },
150
+ "pack": "react-native"
151
+ },
152
+ "django": {
153
+ "ecosystem": "python",
154
+ "dependency_signals": {
155
+ "requirements.txt": ["django", "Django"],
156
+ "pyproject.toml": ["django", "Django"],
157
+ "Pipfile": ["django", "Django"]
158
+ },
159
+ "config_files": ["manage.py"],
160
+ "directory_signals": ["*/settings.py", "*/wsgi.py"],
161
+ "pack": "django"
162
+ },
163
+ "flask": {
164
+ "ecosystem": "python",
165
+ "dependency_signals": {
166
+ "requirements.txt": ["flask", "Flask"],
167
+ "pyproject.toml": ["flask", "Flask"]
168
+ },
169
+ "pack": "flask"
170
+ },
171
+ "fastapi": {
172
+ "ecosystem": "python",
173
+ "dependency_signals": {
174
+ "requirements.txt": ["fastapi"],
175
+ "pyproject.toml": ["fastapi"]
176
+ },
177
+ "pack": "fastapi"
178
+ },
179
+ "java-spring-boot": {
180
+ "ecosystem": "java",
181
+ "dependency_signals": {
182
+ "pom.xml": ["spring-boot-starter"],
183
+ "build.gradle": ["org.springframework.boot"]
184
+ },
185
+ "pack": "java-spring-boot"
186
+ },
187
+ "spring-security": {
188
+ "ecosystem": "java",
189
+ "dependency_signals": {
190
+ "pom.xml": ["spring-boot-starter-security", "spring-security"],
191
+ "build.gradle": ["spring-boot-starter-security", "spring-security"]
192
+ },
193
+ "pack": "spring-security"
194
+ },
195
+ "java-ee": {
196
+ "ecosystem": "java",
197
+ "dependency_signals": {
198
+ "pom.xml": ["javax.servlet", "jakarta.servlet", "javax.enterprise", "jakarta.enterprise"],
199
+ "build.gradle": ["javax.servlet", "jakarta.servlet"]
200
+ },
201
+ "config_files": ["web.xml"],
202
+ "pack": "java-ee"
203
+ },
204
+ "ruby-on-rails": {
205
+ "ecosystem": "ruby",
206
+ "dependency_signals": { "Gemfile": ["rails"] },
207
+ "config_files": ["config/routes.rb", "config/application.rb"],
208
+ "pack": "ruby-on-rails"
209
+ },
210
+ "laravel": {
211
+ "ecosystem": "php",
212
+ "dependency_signals": { "composer.json": ["laravel/framework"] },
213
+ "config_files": ["artisan"],
214
+ "directory_signals": ["app/Http/Controllers"],
215
+ "pack": "laravel"
216
+ },
217
+ "symfony": {
218
+ "ecosystem": "php",
219
+ "dependency_signals": { "composer.json": ["symfony/framework-bundle"] },
220
+ "config_files": ["config/bundles.php", "symfony.lock"],
221
+ "pack": "symfony"
222
+ },
223
+ "dotnet-aspnet-core": {
224
+ "ecosystem": "csharp",
225
+ "dependency_signals": { "*.csproj": ["Microsoft.AspNetCore"] },
226
+ "config_files": ["Program.cs", "Startup.cs", "appsettings.json"],
227
+ "pack": "dotnet-aspnet-core"
228
+ },
229
+ "flutter": {
230
+ "ecosystem": "dart",
231
+ "manifest_files": ["pubspec.yaml"],
232
+ "dependency_signals": { "pubspec.yaml": ["flutter"] },
233
+ "pack": "flutter"
234
+ },
235
+ "apollo-graphql": {
236
+ "ecosystem": "nodejs",
237
+ "dependency_signals": { "package.json": ["@apollo/server", "apollo-server", "apollo-server-express"] },
238
+ "pack": "apollo-graphql"
239
+ }
240
+ },
241
+
242
+ "auth_identity": {
243
+ "jwt": {
244
+ "dependency_signals": {
245
+ "package.json": ["jsonwebtoken", "jose", "@auth/core"],
246
+ "requirements.txt": ["pyjwt", "python-jose"],
247
+ "pyproject.toml": ["pyjwt", "python-jose"],
248
+ "Gemfile": ["jwt"],
249
+ "pom.xml": ["jjwt", "nimbus-jose-jwt"]
250
+ },
251
+ "pack": "jwt"
252
+ },
253
+ "oauth-oidc": {
254
+ "dependency_signals": {
255
+ "package.json": ["openid-client", "passport-oauth2", "oauth4webapi"],
256
+ "requirements.txt": ["authlib", "oauthlib"],
257
+ "pyproject.toml": ["authlib", "oauthlib"]
258
+ },
259
+ "pack": "oauth-oidc"
260
+ },
261
+ "auth0": {
262
+ "dependency_signals": {
263
+ "package.json": ["@auth0/nextjs-auth0", "auth0", "express-openid-connect"],
264
+ "requirements.txt": ["auth0-python"],
265
+ "pyproject.toml": ["auth0-python"]
266
+ },
267
+ "pack": "auth0"
268
+ },
269
+ "okta": {
270
+ "dependency_signals": {
271
+ "package.json": ["@okta/okta-sdk-nodejs", "@okta/oidc-middleware"],
272
+ "pom.xml": ["okta-spring-boot-starter"]
273
+ },
274
+ "pack": "okta"
275
+ },
276
+ "keycloak": {
277
+ "dependency_signals": {
278
+ "package.json": ["keycloak-connect", "keycloak-js"],
279
+ "pom.xml": ["keycloak-spring-boot-starter"]
280
+ },
281
+ "config_files": ["keycloak.json"],
282
+ "pack": "keycloak"
283
+ }
284
+ },
285
+
286
+ "ai_agent": {
287
+ "langchain": {
288
+ "dependency_signals": {
289
+ "requirements.txt": ["langchain"],
290
+ "pyproject.toml": ["langchain"],
291
+ "package.json": ["langchain"]
292
+ },
293
+ "pack": "langchain"
294
+ },
295
+ "langgraph": {
296
+ "dependency_signals": {
297
+ "requirements.txt": ["langgraph"],
298
+ "pyproject.toml": ["langgraph"]
299
+ },
300
+ "pack": "langgraph"
301
+ },
302
+ "llamaindex": {
303
+ "dependency_signals": {
304
+ "requirements.txt": ["llama-index", "llama_index"],
305
+ "pyproject.toml": ["llama-index", "llama_index"]
306
+ },
307
+ "pack": "llamaindex"
308
+ },
309
+ "crewai": {
310
+ "dependency_signals": {
311
+ "requirements.txt": ["crewai"],
312
+ "pyproject.toml": ["crewai"]
313
+ },
314
+ "pack": "crewai"
315
+ },
316
+ "mastra": {
317
+ "dependency_signals": {
318
+ "package.json": ["@mastra/core", "mastra"]
319
+ },
320
+ "pack": "mastra"
321
+ },
322
+ "ai-sdk": {
323
+ "dependency_signals": {
324
+ "package.json": ["ai", "@ai-sdk/openai", "@ai-sdk/anthropic"]
325
+ },
326
+ "pack": "ai-sdk"
327
+ },
328
+ "claude-agent-sdk": {
329
+ "dependency_signals": {
330
+ "package.json": ["@anthropic-ai/sdk"],
331
+ "requirements.txt": ["anthropic"],
332
+ "pyproject.toml": ["anthropic"]
333
+ },
334
+ "pack": "claude-agent-sdk"
335
+ },
336
+ "openai-agents-sdk": {
337
+ "dependency_signals": {
338
+ "package.json": ["openai"],
339
+ "requirements.txt": ["openai"],
340
+ "pyproject.toml": ["openai"]
341
+ },
342
+ "pack": "openai-agents-sdk"
343
+ },
344
+ "mcp": {
345
+ "dependency_signals": {
346
+ "package.json": ["@modelcontextprotocol/sdk"],
347
+ "requirements.txt": ["mcp"],
348
+ "pyproject.toml": ["mcp"]
349
+ },
350
+ "pack": "mcp"
351
+ }
352
+ },
353
+
354
+ "infrastructure": {
355
+ "docker": {
356
+ "config_files": ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", ".dockerignore"],
357
+ "pack": "docker"
358
+ },
359
+ "kubernetes": {
360
+ "config_files": ["helmfile.yaml", "Chart.yaml", "kustomization.yaml"],
361
+ "directory_signals": ["k8s/", "kubernetes/", "helm/"],
362
+ "content_signals": { "*.yaml": ["apiVersion", "kind: Deployment", "kind: Service", "kind: Pod"] },
363
+ "pack": "kubernetes"
364
+ },
365
+ "terraform-aws": {
366
+ "file_extensions": [".tf"],
367
+ "content_signals": { "*.tf": ["provider \"aws\"", "aws_"] },
368
+ "pack": "terraform-aws"
369
+ },
370
+ "terraform-azure": {
371
+ "file_extensions": [".tf"],
372
+ "content_signals": { "*.tf": ["provider \"azurerm\"", "azurerm_"] },
373
+ "pack": "terraform-azure"
374
+ },
375
+ "terraform-gcp": {
376
+ "file_extensions": [".tf"],
377
+ "content_signals": { "*.tf": ["provider \"google\"", "google_"] },
378
+ "pack": "terraform-gcp"
379
+ },
380
+ "cloudformation": {
381
+ "content_signals": { "*.yaml": ["AWSTemplateFormatVersion"], "*.json": ["AWSTemplateFormatVersion"] },
382
+ "config_files": ["template.yaml", "template.json", "samconfig.toml"],
383
+ "pack": "cloudformation"
384
+ },
385
+ "nginx": {
386
+ "config_files": ["nginx.conf"],
387
+ "directory_signals": ["nginx/"],
388
+ "pack": "nginx"
389
+ }
390
+ },
391
+
392
+ "ci_cd": {
393
+ "github-actions": {
394
+ "directory_signals": [".github/workflows/"],
395
+ "pack": "github-actions"
396
+ },
397
+ "jenkins-pipeline": {
398
+ "config_files": ["Jenkinsfile"],
399
+ "pack": "jenkins-pipeline"
400
+ },
401
+ "argocd": {
402
+ "config_files": ["argocd-cm.yaml"],
403
+ "content_signals": { "*.yaml": ["argoproj.io"] },
404
+ "pack": "argocd"
405
+ }
406
+ },
407
+
408
+ "cloud_compute": {
409
+ "aws-lambda": {
410
+ "config_files": ["serverless.yml", "serverless.yaml", "serverless.ts", "sam.yaml", "samconfig.toml"],
411
+ "content_signals": {
412
+ "serverless.yml": ["provider:", "aws"],
413
+ "template.yaml": ["AWS::Serverless::Function", "AWS::Lambda::Function"]
414
+ },
415
+ "pack": "aws-lambda"
416
+ },
417
+ "azure-functions": {
418
+ "config_files": ["host.json", "local.settings.json"],
419
+ "directory_signals": ["function_app.py"],
420
+ "pack": "azure-functions"
421
+ },
422
+ "gcp-cloud-run": {
423
+ "config_files": ["app.yaml", "cloudbuild.yaml"],
424
+ "content_signals": { "*.yaml": ["gcr.io", "run.googleapis.com"] },
425
+ "pack": "gcp-cloud-run"
426
+ }
427
+ },
428
+
429
+ "databases": {
430
+ "mongodb": {
431
+ "dependency_signals": {
432
+ "package.json": ["mongoose", "mongodb"],
433
+ "requirements.txt": ["pymongo", "motor", "mongoengine"],
434
+ "pyproject.toml": ["pymongo", "motor"],
435
+ "Gemfile": ["mongoid"]
436
+ },
437
+ "pack": "mongodb"
438
+ },
439
+ "postgresql": {
440
+ "dependency_signals": {
441
+ "package.json": ["pg", "knex", "prisma", "typeorm", "sequelize"],
442
+ "requirements.txt": ["psycopg2", "asyncpg", "sqlalchemy"],
443
+ "pyproject.toml": ["psycopg2", "asyncpg", "sqlalchemy"],
444
+ "Gemfile": ["pg"],
445
+ "pom.xml": ["postgresql"]
446
+ },
447
+ "pack": "postgresql"
448
+ },
449
+ "mysql-mariadb": {
450
+ "dependency_signals": {
451
+ "package.json": ["mysql2", "mysql"],
452
+ "requirements.txt": ["mysqlclient", "pymysql", "aiomysql"],
453
+ "pyproject.toml": ["mysqlclient", "pymysql"],
454
+ "pom.xml": ["mysql-connector-java"]
455
+ },
456
+ "pack": "mysql-mariadb"
457
+ },
458
+ "redis": {
459
+ "dependency_signals": {
460
+ "package.json": ["redis", "ioredis"],
461
+ "requirements.txt": ["redis", "aioredis"],
462
+ "pyproject.toml": ["redis"],
463
+ "Gemfile": ["redis"]
464
+ },
465
+ "pack": "redis"
466
+ }
467
+ },
468
+
469
+ "messaging": {
470
+ "kafka": {
471
+ "dependency_signals": {
472
+ "package.json": ["kafkajs"],
473
+ "requirements.txt": ["confluent-kafka", "aiokafka"],
474
+ "pyproject.toml": ["confluent-kafka"],
475
+ "pom.xml": ["kafka-clients", "spring-kafka"]
476
+ },
477
+ "pack": "kafka"
478
+ },
479
+ "rabbitmq": {
480
+ "dependency_signals": {
481
+ "package.json": ["amqplib"],
482
+ "requirements.txt": ["pika", "aio-pika"],
483
+ "pyproject.toml": ["pika"],
484
+ "pom.xml": ["spring-boot-starter-amqp"]
485
+ },
486
+ "pack": "rabbitmq"
487
+ }
488
+ },
489
+
490
+ "api_protocols": {
491
+ "grpc": {
492
+ "dependency_signals": {
493
+ "package.json": ["@grpc/grpc-js"],
494
+ "requirements.txt": ["grpcio"],
495
+ "pyproject.toml": ["grpcio"],
496
+ "pom.xml": ["grpc-netty"]
497
+ },
498
+ "file_extensions": [".proto"],
499
+ "pack": "grpc"
500
+ },
501
+ "websockets": {
502
+ "dependency_signals": {
503
+ "package.json": ["ws", "socket.io"],
504
+ "requirements.txt": ["websockets"],
505
+ "pyproject.toml": ["websockets"]
506
+ },
507
+ "pack": "websockets"
508
+ },
509
+ "openapi-rest-api": {
510
+ "config_files": ["openapi.yaml", "openapi.json", "swagger.yaml", "swagger.json"],
511
+ "pack": "openapi-rest-api"
512
+ }
513
+ }
514
+ }
@@ -0,0 +1,33 @@
1
+ import { copyFileSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { ensureDir, writeText } from './fs-helpers.js';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ function sanitizeProjectName(value) {
9
+ return String(value || '').replace(/[\r\n`]/g, ' ').trim();
10
+ }
11
+
12
+ function injectProjectName(content, projectName) {
13
+ const resolved = sanitizeProjectName(projectName) || '<SRAI_PROJECT_NAME>';
14
+ return content.replaceAll('{{SRAI_PROJECT_NAME}}', resolved).replaceAll('<SRAI_PROJECT_NAME>', resolved);
15
+ }
16
+
17
+ const BUNDLE_ROOT = join(__dirname, '..', 'generators', 'rules', 'guardrails-profiler');
18
+
19
+ /**
20
+ * Writes `.securityreview-kit/guardrails-profiler/` (SKILL + signal registry) into the target workspace.
21
+ */
22
+ export function writeGuardrailsProfilerBundle(cwd, options = {}) {
23
+ const destBase = join(cwd, '.securityreview-kit', 'guardrails-profiler');
24
+ const destRefs = join(destBase, 'references');
25
+ ensureDir(destRefs);
26
+
27
+ const skillTemplate = readFileSync(join(BUNDLE_ROOT, 'SKILL.md'), 'utf-8');
28
+ writeText(join(destBase, 'SKILL.md'), injectProjectName(skillTemplate, options.projectName));
29
+
30
+ copyFileSync(join(BUNDLE_ROOT, 'references', 'signal-registry.json'), join(destRefs, 'signal-registry.json'));
31
+
32
+ return destBase;
33
+ }
@@ -0,0 +1,82 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ const AGENT_CLI_TARGETS = new Set(['cursor', 'claude', 'codex']);
4
+
5
+ function commandOk(cmd, args = ['--version']) {
6
+ const r = spawnSync(cmd, args, { stdio: 'ignore' });
7
+ return r.status === 0;
8
+ }
9
+
10
+ function runShell(script) {
11
+ return spawnSync(script, { shell: true, stdio: 'inherit' });
12
+ }
13
+
14
+ /**
15
+ * Ensure Cursor / Claude Code / Codex CLIs are present when those targets are selected.
16
+ * Installation uses vendor scripts or npm where appropriate.
17
+ */
18
+ export function ensureIdeCliForTarget(target, options = {}) {
19
+ const { skipInstall = false } = options;
20
+
21
+ if (!AGENT_CLI_TARGETS.has(target)) {
22
+ return { target, ok: true, skipped: true };
23
+ }
24
+
25
+ if (target === 'cursor') {
26
+ if (commandOk('cursor-agent', ['--version'])) {
27
+ return { target, ok: true, already: true };
28
+ }
29
+ if (skipInstall) {
30
+ return { target, ok: false, message: 'cursor-agent not found on PATH' };
31
+ }
32
+ if (process.platform === 'win32') {
33
+ return {
34
+ target,
35
+ ok: false,
36
+ message: 'Install Cursor CLI manually: https://cursor.com/cli',
37
+ };
38
+ }
39
+ const r = runShell('curl -fsSL https://cursor.com/install | bash');
40
+ return r.status === 0
41
+ ? { target, ok: true }
42
+ : { target, ok: false, message: 'Cursor CLI install script failed' };
43
+ }
44
+
45
+ if (target === 'claude') {
46
+ if (commandOk('claude', ['--version'])) {
47
+ return { target, ok: true, already: true };
48
+ }
49
+ if (skipInstall) {
50
+ return { target, ok: false, message: 'claude not found on PATH' };
51
+ }
52
+ if (process.platform === 'win32') {
53
+ const r = runShell('powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://claude.ai/install.ps1 | iex"');
54
+ return r.status === 0
55
+ ? { target, ok: true }
56
+ : { target, ok: false, message: 'Claude Code install failed' };
57
+ }
58
+ const r = runShell('curl -fsSL https://claude.ai/install.sh | bash');
59
+ return r.status === 0
60
+ ? { target, ok: true }
61
+ : { target, ok: false, message: 'Claude Code install failed' };
62
+ }
63
+
64
+ if (target === 'codex') {
65
+ if (commandOk('codex', ['--version'])) {
66
+ return { target, ok: true, already: true };
67
+ }
68
+ if (skipInstall) {
69
+ return { target, ok: false, message: 'codex not found on PATH' };
70
+ }
71
+ const r = runShell('npm install -g @openai/codex');
72
+ return r.status === 0
73
+ ? { target, ok: true }
74
+ : { target, ok: false, message: 'Codex CLI npm install failed' };
75
+ }
76
+
77
+ return { target, ok: true, skipped: true };
78
+ }
79
+
80
+ export function ensureIdeClisForTargets(targets, options = {}) {
81
+ return targets.map((t) => ensureIdeCliForTarget(t, options));
82
+ }
@@ -0,0 +1,64 @@
1
+ import { spawnSync } from 'node:child_process';
2
+
3
+ const PREFERRED_ORDER = ['cursor', 'claude', 'codex'];
4
+
5
+ function commandOk(cmd, args = ['--version']) {
6
+ const r = spawnSync(cmd, args, { stdio: 'ignore' });
7
+ return r.status === 0;
8
+ }
9
+
10
+ export function buildProfilerAgentPrompt(projectName) {
11
+ const p = String(projectName || '').trim() || '<SRAI_PROJECT_NAME>';
12
+ return [
13
+ 'Security Review Kit: run guardrails initialization profiling for this workspace.',
14
+ `SRAI project name: "${p}".`,
15
+ 'Open and follow every step in .securityreview-kit/guardrails-profiler/SKILL.md.',
16
+ 'Use .securityreview-kit/guardrails-profiler/references/signal-registry.json as the signal registry.',
17
+ 'Write .guardrails/profile.json and profile.json at the repository root as defined in the skill.',
18
+ `Resolve project_id with find_project_by_name for "${p}", then call update_vibe_profile and write_default_pack via security-review-mcp.`,
19
+ 'Do not invent stack details, compliance, or user groups; ground everything in the repository.',
20
+ ].join('\n');
21
+ }
22
+
23
+ export function pickProfilerAgentTarget(targets) {
24
+ for (const t of PREFERRED_ORDER) {
25
+ if (targets.includes(t)) {
26
+ return t;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * Spawn the IDE agent CLI to execute the profiler skill (user must be logged in where required).
34
+ */
35
+ export function runProfilerAgent(cwd, { target, projectName }) {
36
+ const prompt = buildProfilerAgentPrompt(projectName);
37
+ const opts = { cwd, stdio: 'inherit', env: { ...process.env } };
38
+
39
+ if (target === 'cursor') {
40
+ if (!commandOk('cursor-agent', ['--version'])) {
41
+ return { ok: false, message: 'cursor-agent not on PATH' };
42
+ }
43
+ const r = spawnSync('cursor-agent', ['-p', prompt], opts);
44
+ return { ok: r.status === 0, status: r.status };
45
+ }
46
+
47
+ if (target === 'claude') {
48
+ if (!commandOk('claude', ['--version'])) {
49
+ return { ok: false, message: 'claude not on PATH' };
50
+ }
51
+ const r = spawnSync('claude', ['-p', prompt], opts);
52
+ return { ok: r.status === 0, status: r.status };
53
+ }
54
+
55
+ if (target === 'codex') {
56
+ if (!commandOk('codex', ['--version'])) {
57
+ return { ok: false, message: 'codex not on PATH' };
58
+ }
59
+ const r = spawnSync('codex', ['exec', prompt], opts);
60
+ return { ok: r.status === 0, status: r.status };
61
+ }
62
+
63
+ return { ok: false, message: 'unsupported agent target' };
64
+ }