@securityreviewai/securityreview-kit 0.1.47 → 0.1.49

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 (79) 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 +37 -0
  12. package/dist/scaffold/codex.js +35 -0
  13. package/dist/scaffold/cursor.js +39 -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 +165 -0
  18. package/dist/scaffold/vibreview.js +24 -0
  19. package/dist/scaffold/vscode.js +22 -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 -48
  46. package/src/generators/mcp/vscode.test.js +0 -21
  47. package/src/generators/mcp/windsurf.js +0 -27
  48. package/src/generators/rules/antigravity.js +0 -22
  49. package/src/generators/rules/claude.js +0 -87
  50. package/src/generators/rules/claude.test.js +0 -60
  51. package/src/generators/rules/codex.js +0 -141
  52. package/src/generators/rules/codex.test.js +0 -59
  53. package/src/generators/rules/content.js +0 -110
  54. package/src/generators/rules/content.md +0 -57
  55. package/src/generators/rules/cursor.js +0 -128
  56. package/src/generators/rules/gemini.js +0 -13
  57. package/src/generators/rules/guardrails-init-profile.md +0 -56
  58. package/src/generators/rules/guardrails-profiler/SKILL.md +0 -130
  59. package/src/generators/rules/guardrails-profiler/references/signal-registry.json +0 -514
  60. package/src/generators/rules/guardrails-selection/SKILL.md +0 -187
  61. package/src/generators/rules/guardrails-selection/references/category-threat-map.md +0 -232
  62. package/src/generators/rules/guardrails_rule.md +0 -94
  63. package/src/generators/rules/hooks.json +0 -11
  64. package/src/generators/rules/skill.md +0 -256
  65. package/src/generators/rules/srai-profile.md +0 -32
  66. package/src/generators/rules/vibereview-sync/SKILL.md +0 -378
  67. package/src/generators/rules/vscode.js +0 -101
  68. package/src/generators/rules/vscode.test.js +0 -54
  69. package/src/generators/rules/windsurf.js +0 -13
  70. package/src/utils/constants.js +0 -95
  71. package/src/utils/cursor-agent-path.js +0 -67
  72. package/src/utils/cursor-cli-permissions.js +0 -28
  73. package/src/utils/detect.js +0 -27
  74. package/src/utils/fs-helpers.js +0 -82
  75. package/src/utils/guardrails-profiler-bundle.js +0 -84
  76. package/src/utils/ide-cli-install.js +0 -138
  77. package/src/utils/profiler-agent.js +0 -446
  78. package/src/utils/profiler-agent.test.js +0 -81
  79. package/src/utils/srai.js +0 -252
@@ -1,138 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { augmentPathEnv, resolveCursorAgentExecutable } from './cursor-agent-path.js';
3
-
4
- const AGENT_CLI_TARGETS = new Set(['cursor', 'claude', 'vscode', 'codex']);
5
-
6
- function commandOk(cmd, args = ['--version'], env = process.env) {
7
- const r = spawnSync(cmd, args, { stdio: 'ignore', env });
8
- return r.status === 0;
9
- }
10
-
11
- function runShell(script) {
12
- return spawnSync(script, { shell: true, stdio: 'inherit' });
13
- }
14
-
15
- /**
16
- * Ensure Cursor / Claude Code / Copilot / Codex CLIs are present when those targets are selected.
17
- * Installation uses vendor scripts or npm where appropriate.
18
- */
19
- export function ensureIdeCliForTarget(target, options = {}) {
20
- const { skipInstall = false } = options;
21
-
22
- if (!AGENT_CLI_TARGETS.has(target)) {
23
- return { target, ok: true, skipped: true };
24
- }
25
-
26
- if (target === 'cursor') {
27
- if (resolveCursorAgentExecutable()) {
28
- return { target, ok: true, already: true };
29
- }
30
- if (skipInstall) {
31
- return {
32
- target,
33
- ok: false,
34
- message:
35
- 'Cursor Agent CLI not found (`agent` or `cursor-agent`). Install from https://cursor.com/docs/cli/installation.',
36
- };
37
- }
38
- if (process.platform === 'win32') {
39
- return {
40
- target,
41
- ok: false,
42
- message: 'Install Cursor CLI manually: https://cursor.com/cli',
43
- };
44
- }
45
- const r = runShell('curl -fsSL https://cursor.com/install | bash');
46
- if (r.status !== 0) {
47
- return { target, ok: false, message: 'Cursor CLI install script failed' };
48
- }
49
- if (!resolveCursorAgentExecutable()) {
50
- return {
51
- target,
52
- ok: false,
53
- message:
54
- 'Cursor Agent CLI (`agent` / `cursor-agent`) not found after install; open a new shell or ensure ~/.local/bin exists and re-run init',
55
- };
56
- }
57
- return { target, ok: true };
58
- }
59
-
60
- if (target === 'claude') {
61
- if (commandOk('claude', ['--version'], augmentPathEnv())) {
62
- return { target, ok: true, already: true };
63
- }
64
- if (skipInstall) {
65
- return { target, ok: false, message: 'claude not found on PATH' };
66
- }
67
- if (process.platform === 'win32') {
68
- const r = runShell('powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://claude.ai/install.ps1 | iex"');
69
- return r.status === 0
70
- ? { target, ok: true }
71
- : { target, ok: false, message: 'Claude Code install failed' };
72
- }
73
- const r = runShell('curl -fsSL https://claude.ai/install.sh | bash');
74
- return r.status === 0
75
- ? { target, ok: true }
76
- : { target, ok: false, message: 'Claude Code install failed' };
77
- }
78
-
79
- if (target === 'codex') {
80
- if (commandOk('codex', ['--version'], augmentPathEnv())) {
81
- return { target, ok: true, already: true };
82
- }
83
- if (skipInstall) {
84
- return { target, ok: false, message: 'codex not found on PATH' };
85
- }
86
- const r = runShell('npm install -g @openai/codex');
87
- return r.status === 0
88
- ? { target, ok: true }
89
- : { target, ok: false, message: 'Codex CLI npm install failed' };
90
- }
91
-
92
- if (target === 'vscode') {
93
- if (commandOk('copilot', ['--version'], augmentPathEnv())) {
94
- return { target, ok: true, already: true };
95
- }
96
- if (skipInstall) {
97
- return {
98
- target,
99
- ok: false,
100
- message:
101
- 'GitHub Copilot CLI not found (`copilot`). Install from https://docs.github.com/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli.',
102
- };
103
- }
104
- if (process.platform === 'win32') {
105
- const r = runShell('winget install GitHub.Copilot');
106
- if (r.status !== 0) {
107
- return { target, ok: false, message: 'GitHub Copilot CLI winget install failed' };
108
- }
109
- return commandOk('copilot', ['--version'], augmentPathEnv())
110
- ? { target, ok: true }
111
- : {
112
- target,
113
- ok: false,
114
- message:
115
- 'GitHub Copilot CLI (`copilot`) not found after install; open a new shell or ensure it is on PATH and re-run init',
116
- };
117
- }
118
- const r = runShell('curl -fsSL https://gh.io/copilot-install | bash');
119
- if (r.status !== 0) {
120
- return { target, ok: false, message: 'GitHub Copilot CLI install script failed' };
121
- }
122
- if (!commandOk('copilot', ['--version'], augmentPathEnv())) {
123
- return {
124
- target,
125
- ok: false,
126
- message:
127
- 'GitHub Copilot CLI (`copilot`) not found after install; open a new shell or ensure the install directory is on PATH and re-run init',
128
- };
129
- }
130
- return { target, ok: true };
131
- }
132
-
133
- return { target, ok: true, skipped: true };
134
- }
135
-
136
- export function ensureIdeClisForTargets(targets, options = {}) {
137
- return targets.map((t) => ensureIdeCliForTarget(t, options));
138
- }
@@ -1,446 +0,0 @@
1
- import { spawnSync } from 'node:child_process';
2
- import { join } from 'node:path';
3
- import { GUARDRAILS_PROFILER_SKILL_REL_DIR, MCP_SERVER_NAME, MCP_SERVER_PACKAGE } from './constants.js';
4
- import { augmentPathEnv, resolveCursorAgentExecutable } from './cursor-agent-path.js';
5
- import { readJson, readText, writeText } from './fs-helpers.js';
6
-
7
- const PREFERRED_ORDER = ['cursor', 'vscode', 'claude', 'codex'];
8
-
9
- function commandOk(cmd, args = ['--version'], env = process.env) {
10
- const r = spawnSync(cmd, args, { stdio: 'ignore', env });
11
- return r.status === 0;
12
- }
13
-
14
- export function getProfilerLogPath(cwd, target) {
15
- return join(cwd, '.guardrails', 'logs', `profiler-${target}.log`);
16
- }
17
-
18
- function buildProfilerEnv(target, streamProgress, extraEnv = {}) {
19
- const env = augmentPathEnv(process.env);
20
- for (const [key, value] of Object.entries(extraEnv || {})) {
21
- if (value == null || value === '') {
22
- delete env[key];
23
- } else {
24
- env[key] = value;
25
- }
26
- }
27
- if (target === 'codex' && streamProgress && !env.RUST_LOG) {
28
- env.RUST_LOG = 'info';
29
- }
30
- return env;
31
- }
32
-
33
- function persistProfilerLog(cwd, target, result) {
34
- if (!result || (result.stdout == null && result.stderr == null)) {
35
- return null;
36
- }
37
-
38
- const stdout = String(result.stdout || '');
39
- const stderr = String(result.stderr || '');
40
- const body = [
41
- `target=${target}`,
42
- `status=${result.status ?? ''}`,
43
- `signal=${result.signal ?? ''}`,
44
- '',
45
- '--- stdout ---',
46
- stdout,
47
- '',
48
- '--- stderr ---',
49
- stderr,
50
- '',
51
- ].join('\n');
52
-
53
- const logPath = getProfilerLogPath(cwd, target);
54
- writeText(logPath, body);
55
- return logPath;
56
- }
57
-
58
- export function buildProfilerAgentPrompt(projectName, agentTarget = 'cursor') {
59
- const p = String(projectName || '').trim() || '<SRAI_PROJECT_NAME>';
60
- const skillRoot =
61
- GUARDRAILS_PROFILER_SKILL_REL_DIR[agentTarget] ||
62
- GUARDRAILS_PROFILER_SKILL_REL_DIR.cursor;
63
- return [
64
- 'Security Review Kit: run guardrails initialization profiling for this workspace.',
65
- `SRAI project name: "${p}".`,
66
- `Open and follow every step in ${skillRoot}/SKILL.md.`,
67
- `Use ${skillRoot}/references/signal-registry.json as the signal registry.`,
68
- 'Write .guardrails/profile.json as defined in the skill.',
69
- `Resolve project_id with find_project_by_name for "${p}", then call update_vibe_profile and write_default_pack via security-review-mcp.`,
70
- 'Do not invent stack details, compliance, or user groups; ground everything in the repository.',
71
- ].join('\n');
72
- }
73
-
74
- /**
75
- * Run Cursor Agent OAuth/login in the current terminal (stdio inherited).
76
- * Call this from init before profiling so the user does not leave the kit flow.
77
- */
78
- export function runCursorAgentLogin(cwd) {
79
- const bin = resolveCursorAgentExecutable();
80
- if (!bin) {
81
- return {
82
- ok: false,
83
- status: null,
84
- message:
85
- 'Cursor Agent CLI not found (`agent` or `cursor-agent`). Install from https://cursor.com/docs/cli/installation (add ~/.local/bin to PATH), or re-run init without --skip-ide-cli-install.',
86
- };
87
- }
88
- const env = augmentPathEnv(process.env);
89
- const r = spawnSync(bin, ['login'], { cwd, stdio: 'inherit', env });
90
- const spawnErr = r.error ? r.error.message : null;
91
- if (r.status === null && spawnErr) {
92
- return { ok: false, status: null, message: spawnErr };
93
- }
94
- return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
95
- }
96
-
97
- /**
98
- * Run GitHub Copilot CLI OAuth login in the current terminal (stdio inherited).
99
- * `copilot login` exits after the device-flow succeeds, so init can continue into profiling.
100
- */
101
- export function runCopilotLogin(cwd) {
102
- const env = augmentPathEnv(process.env);
103
- if (!commandOk('copilot', ['--version'], env)) {
104
- return {
105
- ok: false,
106
- status: null,
107
- message:
108
- 'GitHub Copilot CLI not found (`copilot`). Install from https://docs.github.com/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli.',
109
- };
110
- }
111
- const r = spawnSync('copilot', ['login'], { cwd, stdio: 'inherit', env });
112
- const spawnErr = r.error ? r.error.message : null;
113
- if (r.status === null && spawnErr) {
114
- return { ok: false, status: null, message: spawnErr };
115
- }
116
- return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
117
- }
118
-
119
- /**
120
- * Run Claude Code login in the current terminal.
121
- */
122
- export function runClaudeLogin(cwd) {
123
- return runClaudeAuthLogin(cwd, { mode: 'claudeai' });
124
- }
125
-
126
- /**
127
- * Run Claude Code auth login in the current terminal.
128
- */
129
- export function runClaudeAuthLogin(cwd, options = {}) {
130
- const env = augmentPathEnv(process.env);
131
- if (!commandOk('claude', ['--version'], env)) {
132
- return {
133
- ok: false,
134
- status: null,
135
- message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
136
- };
137
- }
138
- const mode = options.mode === 'console' ? '--console' : '--claudeai';
139
- const r = spawnSync('claude', ['auth', 'login', mode], { cwd, stdio: 'inherit', env });
140
- const spawnErr = r.error ? r.error.message : null;
141
- if (r.status === null && spawnErr) {
142
- return { ok: false, status: null, message: spawnErr };
143
- }
144
- return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
145
- }
146
-
147
- /**
148
- * Run Claude Code long-lived token setup in the current terminal.
149
- */
150
- export function runClaudeSetupToken(cwd) {
151
- const env = augmentPathEnv(process.env);
152
- if (!commandOk('claude', ['--version'], env)) {
153
- return {
154
- ok: false,
155
- status: null,
156
- message: 'Claude Code CLI not found (`claude`). Install from https://claude.ai/code.',
157
- };
158
- }
159
- const r = spawnSync('claude', ['setup-token'], { cwd, stdio: 'inherit', env });
160
- const spawnErr = r.error ? r.error.message : null;
161
- if (r.status === null && spawnErr) {
162
- return { ok: false, status: null, message: spawnErr };
163
- }
164
- return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
165
- }
166
-
167
- /**
168
- * Run Codex login in the current terminal. Device auth keeps the flow in-terminal.
169
- */
170
- export function runCodexLogin(cwd) {
171
- const env = augmentPathEnv(process.env);
172
- if (!commandOk('codex', ['--version'], env)) {
173
- return {
174
- ok: false,
175
- status: null,
176
- message: 'Codex CLI not found (`codex`). Install with `npm install -g @openai/codex`.',
177
- };
178
- }
179
- const r = spawnSync('codex', ['login', '--device-auth'], { cwd, stdio: 'inherit', env });
180
- const spawnErr = r.error ? r.error.message : null;
181
- if (r.status === null && spawnErr) {
182
- return { ok: false, status: null, message: spawnErr };
183
- }
184
- return { ok: r.status === 0, status: r.status, message: r.status !== 0 ? spawnErr : undefined };
185
- }
186
-
187
- function escapeTomlString(value) {
188
- return String(value || '').replaceAll('\\', '\\\\').replaceAll('"', '\\"');
189
- }
190
-
191
- function readCodexMcpEnv(cwd) {
192
- const config = readText(join(cwd, '.codex', 'config.toml'));
193
- const apiUrl = config.match(/^\s*SECURITY_REVIEW_API_URL\s*=\s*"([^"]*)"/m)?.[1] || '';
194
- const apiToken = config.match(/^\s*SECURITY_REVIEW_API_TOKEN\s*=\s*"([^"]*)"/m)?.[1] || '';
195
- return { apiUrl, apiToken };
196
- }
197
-
198
- export function buildCodexConfigOverrides(cwd, overrides = {}) {
199
- const fileValues = readCodexMcpEnv(cwd);
200
- const apiUrl = String(overrides.apiUrl || fileValues.apiUrl || '').trim();
201
- const apiToken = String(overrides.apiToken || fileValues.apiToken || '').trim();
202
-
203
- if (!apiUrl || !apiToken) {
204
- return null;
205
- }
206
-
207
- return [
208
- 'features.codex_hooks=true',
209
- `mcp_servers.${MCP_SERVER_NAME}.command="npx"`,
210
- `mcp_servers.${MCP_SERVER_NAME}.args=["-y","${MCP_SERVER_PACKAGE}@latest"]`,
211
- `mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_URL="${escapeTomlString(apiUrl)}"`,
212
- `mcp_servers.${MCP_SERVER_NAME}.env.SECURITY_REVIEW_API_TOKEN="${escapeTomlString(apiToken)}"`,
213
- ];
214
- }
215
-
216
- export function buildClaudeAdditionalMcpConfig(cwd) {
217
- const projectMcp = readJson(join(cwd, '.mcp.json'));
218
- const sourceServer = projectMcp?.mcpServers?.[MCP_SERVER_NAME];
219
-
220
- if (!sourceServer || typeof sourceServer !== 'object') {
221
- return null;
222
- }
223
-
224
- return JSON.stringify({
225
- mcpServers: {
226
- [MCP_SERVER_NAME]: sourceServer,
227
- },
228
- });
229
- }
230
-
231
- export function pickProfilerAgentTarget(targets) {
232
- for (const t of PREFERRED_ORDER) {
233
- if (targets.includes(t)) {
234
- return t;
235
- }
236
- }
237
- return null;
238
- }
239
-
240
- /**
241
- * Spawn the IDE agent CLI to execute the profiler skill (user must be logged in where required).
242
- *
243
- * @param {object} opts
244
- * @param {boolean} [opts.cursorTrust=true] When true, passes `--trust` and `--approve-mcps` so headless init is not blocked by
245
- * workspace trust or MCP approval (user confirmed profiling in the kit). Set false with `--profiler-no-trust`
246
- * if you need an interactive trust/login/MCP flow in the same terminal.
247
- * @param {boolean} [opts.streamProgress=false] When true, pass each CLI’s streaming / verbose flags.
248
- * @param {boolean} [opts.showOutput=false] When true, inherit stdio from the child process.
249
- * Keep both false for init-time profiling so the agent does not flood the terminal with JSON/progress logs.
250
- */
251
- export function runProfilerAgent(
252
- cwd,
253
- {
254
- target,
255
- projectName,
256
- apiUrl,
257
- apiToken,
258
- modelOverride,
259
- extraEnv,
260
- cursorTrust = true,
261
- streamProgress = false,
262
- showOutput = streamProgress,
263
- },
264
- ) {
265
- const prompt = buildProfilerAgentPrompt(projectName, target);
266
- const env = buildProfilerEnv(target, streamProgress, extraEnv);
267
- const opts = showOutput
268
- ? { cwd, stdio: 'inherit', env }
269
- : { cwd, stdio: ['ignore', 'pipe', 'pipe'], env, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 };
270
-
271
- if (streamProgress) {
272
- console.error(
273
- '\n[securityreview-kit] Profiler live output: you should see streaming progress below ' +
274
- '(JSON lines are normal). Omit --profiler-verbose for minimal output.\n',
275
- );
276
- }
277
-
278
- if (target === 'cursor') {
279
- const bin = resolveCursorAgentExecutable();
280
- if (!bin) {
281
- return {
282
- ok: false,
283
- message:
284
- 'Cursor Agent CLI not found (`agent` or `cursor-agent`). Install from https://cursor.com/docs/cli/installation and ensure ~/.local/bin exists; run init Step 1b without --skip-ide-cli-install (Node subprocesses get an augmented PATH, but the binary must be installed).',
285
- };
286
- }
287
- const args = ['-p', prompt];
288
- if (streamProgress) {
289
- args.push('--output-format', 'stream-json', '--stream-partial-output');
290
- }
291
- if (cursorTrust) {
292
- args.push('--trust', '--approve-mcps');
293
- }
294
- const r = spawnSync(bin, args, opts);
295
- if (r.error) {
296
- return { ok: false, message: r.error.message };
297
- }
298
- return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
299
- }
300
-
301
- if (target === 'claude') {
302
- if (!commandOk('claude', ['--version'], env)) {
303
- return { ok: false, message: 'claude not on PATH' };
304
- }
305
- const settingsPath = join(cwd, '.claude', 'settings.json');
306
- const mcpConfig = buildClaudeAdditionalMcpConfig(cwd);
307
- if (!mcpConfig) {
308
- return {
309
- ok: false,
310
- message:
311
- 'Claude profiling needs the SRAI MCP server in .mcp.json. Re-run init with MCP installation enabled.',
312
- };
313
- }
314
- const args = [
315
- '-p',
316
- '--settings',
317
- settingsPath,
318
- '--mcp-config',
319
- mcpConfig,
320
- '--strict-mcp-config',
321
- '--permission-mode',
322
- 'bypassPermissions',
323
- '--model',
324
- modelOverride || 'haiku',
325
- ];
326
- if (streamProgress) {
327
- args.push('--output-format', 'stream-json', '--include-partial-messages', '--include-hook-events', '--verbose');
328
- }
329
- args.push(prompt);
330
- const r = spawnSync('claude', args, opts);
331
- return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
332
- }
333
-
334
- if (target === 'codex') {
335
- if (!commandOk('codex', ['--version'], env)) {
336
- return { ok: false, message: 'codex not on PATH' };
337
- }
338
- const configOverrides = buildCodexConfigOverrides(cwd, { apiUrl, apiToken });
339
- if (!configOverrides) {
340
- return {
341
- ok: false,
342
- message:
343
- 'Codex profiling needs SRAI MCP credentials. Re-run init with Codex selected and MCP installation enabled.',
344
- };
345
- }
346
- const args = ['exec', '--full-auto'];
347
- for (const override of configOverrides) {
348
- args.push('-c', override);
349
- }
350
- if (streamProgress) {
351
- args.push('--json');
352
- }
353
- args.push(prompt);
354
- const r = spawnSync('codex', args, opts);
355
- return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
356
- }
357
-
358
- if (target === 'vscode') {
359
- if (!commandOk('copilot', ['--version'], env)) {
360
- return { ok: false, message: 'copilot not on PATH' };
361
- }
362
-
363
- const mcpConfig = buildCopilotAdditionalMcpConfig(cwd);
364
- if (!mcpConfig) {
365
- return {
366
- ok: false,
367
- message:
368
- 'VS Code MCP config not found for security-review-mcp. Re-run init with MCP installation enabled.',
369
- };
370
- }
371
-
372
- const args = [
373
- '-p',
374
- prompt,
375
- '--additional-mcp-config',
376
- mcpConfig,
377
- '--allow-all',
378
- ];
379
- if (streamProgress) {
380
- args.push('--output-format', 'json');
381
- }
382
- const r = spawnSync('copilot', args, opts);
383
- return buildProfilerResult(r, { logPath: showOutput ? null : persistProfilerLog(cwd, target, r) });
384
- }
385
-
386
- return { ok: false, message: 'unsupported agent target' };
387
- }
388
-
389
- export function buildCopilotAdditionalMcpConfig(cwd) {
390
- const vscodeMcp = readJson(join(cwd, '.vscode', 'mcp.json'));
391
- const sourceServer =
392
- vscodeMcp?.mcpServers?.[MCP_SERVER_NAME] || vscodeMcp?.servers?.[MCP_SERVER_NAME];
393
-
394
- if (!sourceServer || typeof sourceServer !== 'object') {
395
- return null;
396
- }
397
-
398
- const server = { ...sourceServer };
399
- if (!server.type) {
400
- server.type = 'stdio';
401
- }
402
- if (!Array.isArray(server.tools)) {
403
- server.tools = ['*'];
404
- }
405
-
406
- return JSON.stringify({
407
- mcpServers: {
408
- [MCP_SERVER_NAME]: server,
409
- },
410
- });
411
- }
412
-
413
- function buildProfilerResult(result, extras = {}) {
414
- if (result.error) {
415
- return { ok: false, status: result.status, message: result.error.message, ...extras };
416
- }
417
-
418
- if (result.status === 0) {
419
- return { ok: true, status: result.status, ...extras };
420
- }
421
-
422
- const outputTail = summarizeProfilerOutput(result.stderr || result.stdout || '');
423
- const exitDetail =
424
- typeof result.status === 'number'
425
- ? `exit status ${result.status}`
426
- : result.signal
427
- ? `signal ${result.signal}`
428
- : 'unknown error';
429
-
430
- return {
431
- ok: false,
432
- status: result.status,
433
- message: outputTail ? `${exitDetail}; last output: ${outputTail}` : exitDetail,
434
- ...extras,
435
- };
436
- }
437
-
438
- function summarizeProfilerOutput(output) {
439
- return String(output || '')
440
- .replace(/\u001b\[[0-9;]*m/g, '')
441
- .split(/\r?\n/)
442
- .map((line) => line.trim())
443
- .filter(Boolean)
444
- .slice(-3)
445
- .join(' | ');
446
- }
@@ -1,81 +0,0 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { test } from 'node:test';
5
- import assert from 'node:assert/strict';
6
- import {
7
- buildCodexConfigOverrides,
8
- buildCopilotAdditionalMcpConfig,
9
- buildProfilerAgentPrompt,
10
- pickProfilerAgentTarget,
11
- } from './profiler-agent.js';
12
-
13
- test('pickProfilerAgentTarget supports VS Code Copilot', () => {
14
- assert.equal(pickProfilerAgentTarget(['vscode']), 'vscode');
15
- assert.equal(pickProfilerAgentTarget(['codex', 'vscode']), 'vscode');
16
- });
17
-
18
- test('buildCopilotAdditionalMcpConfig converts VS Code MCP config', () => {
19
- const cwd = join(tmpdir(), `securityreview-kit-${Date.now()}`);
20
- mkdirSync(join(cwd, '.vscode'), { recursive: true });
21
- writeFileSync(
22
- join(cwd, '.vscode', 'mcp.json'),
23
- JSON.stringify({
24
- servers: {
25
- 'security-review-mcp': {
26
- type: 'stdio',
27
- command: 'npx',
28
- args: ['-y', '@securityreviewai/security-review-mcp@latest'],
29
- env: {
30
- SECURITY_REVIEW_API_URL: 'https://api.example.test',
31
- SECURITY_REVIEW_API_TOKEN: 'token',
32
- },
33
- },
34
- },
35
- }),
36
- );
37
-
38
- const converted = JSON.parse(buildCopilotAdditionalMcpConfig(cwd));
39
-
40
- assert.deepEqual(Object.keys(converted.mcpServers), ['security-review-mcp']);
41
- assert.equal(converted.mcpServers['security-review-mcp'].command, 'npx');
42
- assert.deepEqual(converted.mcpServers['security-review-mcp'].tools, ['*']);
43
- });
44
-
45
- test('buildCodexConfigOverrides builds inline MCP config for profiling', () => {
46
- const cwd = join(tmpdir(), `securityreview-kit-codex-${Date.now()}`);
47
- mkdirSync(join(cwd, '.codex'), { recursive: true });
48
- writeFileSync(
49
- join(cwd, '.codex', 'config.toml'),
50
- [
51
- '[features]',
52
- 'codex_hooks = true',
53
- '',
54
- '[mcp_servers.security-review-mcp]',
55
- 'command = "npx"',
56
- 'args = ["-y", "@securityreviewai/security-review-mcp@latest"]',
57
- '',
58
- '[mcp_servers.security-review-mcp.env]',
59
- 'SECURITY_REVIEW_API_URL = "https://api.example.test"',
60
- 'SECURITY_REVIEW_API_TOKEN = "token"',
61
- '',
62
- ].join('\n'),
63
- );
64
-
65
- const overrides = buildCodexConfigOverrides(cwd);
66
-
67
- assert.deepEqual(overrides, [
68
- 'features.codex_hooks=true',
69
- 'mcp_servers.security-review-mcp.command="npx"',
70
- 'mcp_servers.security-review-mcp.args=["-y","@securityreviewai/security-review-mcp@latest"]',
71
- 'mcp_servers.security-review-mcp.env.SECURITY_REVIEW_API_URL="https://api.example.test"',
72
- 'mcp_servers.security-review-mcp.env.SECURITY_REVIEW_API_TOKEN="token"',
73
- ]);
74
- });
75
-
76
- test('buildProfilerAgentPrompt only instructs writing the guardrails profile under .guardrails', () => {
77
- const prompt = buildProfilerAgentPrompt('demo-project', 'codex');
78
-
79
- assert.match(prompt, /Write \.guardrails\/profile\.json as defined in the skill\./);
80
- assert.doesNotMatch(prompt, /profile\.json at the repository root/);
81
- });