@cybedefend/vibedefend 1.1.1

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 (103) hide show
  1. package/LICENSE +77 -0
  2. package/README.md +120 -0
  3. package/bin/vibedefend.js +19 -0
  4. package/dist/auth/auth-store.js +170 -0
  5. package/dist/auth/auth-store.js.map +1 -0
  6. package/dist/auth/auth.js +125 -0
  7. package/dist/auth/auth.js.map +1 -0
  8. package/dist/auth/callback-server.js +216 -0
  9. package/dist/auth/callback-server.js.map +1 -0
  10. package/dist/auth/pkce.js +31 -0
  11. package/dist/auth/pkce.js.map +1 -0
  12. package/dist/auth/token-exchange.js +83 -0
  13. package/dist/auth/token-exchange.js.map +1 -0
  14. package/dist/clients/claude-code.js +170 -0
  15. package/dist/clients/claude-code.js.map +1 -0
  16. package/dist/clients/codex.js +378 -0
  17. package/dist/clients/codex.js.map +1 -0
  18. package/dist/clients/cursor-guards-rules.js +94 -0
  19. package/dist/clients/cursor-guards-rules.js.map +1 -0
  20. package/dist/clients/cursor.js +172 -0
  21. package/dist/clients/cursor.js.map +1 -0
  22. package/dist/clients/detect.js +86 -0
  23. package/dist/clients/detect.js.map +1 -0
  24. package/dist/clients/registry.js +41 -0
  25. package/dist/clients/registry.js.map +1 -0
  26. package/dist/clients/types.js +2 -0
  27. package/dist/clients/types.js.map +1 -0
  28. package/dist/clients/vscode.js +187 -0
  29. package/dist/clients/vscode.js.map +1 -0
  30. package/dist/clients/windsurf.js +151 -0
  31. package/dist/clients/windsurf.js.map +1 -0
  32. package/dist/config.js +32 -0
  33. package/dist/config.js.map +1 -0
  34. package/dist/custom-regions.js +112 -0
  35. package/dist/custom-regions.js.map +1 -0
  36. package/dist/diagnostics.js +122 -0
  37. package/dist/diagnostics.js.map +1 -0
  38. package/dist/doctor.js +125 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/guards-evaluator/bucketing.js +83 -0
  41. package/dist/guards-evaluator/bucketing.js.map +1 -0
  42. package/dist/guards-evaluator/evaluate.js +272 -0
  43. package/dist/guards-evaluator/evaluate.js.map +1 -0
  44. package/dist/guards-evaluator/glob.js +148 -0
  45. package/dist/guards-evaluator/glob.js.map +1 -0
  46. package/dist/guards-evaluator/index.js +9 -0
  47. package/dist/guards-evaluator/index.js.map +1 -0
  48. package/dist/guards-evaluator/preprocess.js +174 -0
  49. package/dist/guards-evaluator/preprocess.js.map +1 -0
  50. package/dist/guards-evaluator/redact.js +111 -0
  51. package/dist/guards-evaluator/redact.js.map +1 -0
  52. package/dist/guards-evaluator/regex.js +125 -0
  53. package/dist/guards-evaluator/regex.js.map +1 -0
  54. package/dist/guards-evaluator/types.js +2 -0
  55. package/dist/guards-evaluator/types.js.map +1 -0
  56. package/dist/guards-evaluator/validation.js +115 -0
  57. package/dist/guards-evaluator/validation.js.map +1 -0
  58. package/dist/hook-runner.js +6680 -0
  59. package/dist/hooks/install.js +169 -0
  60. package/dist/hooks/install.js.map +1 -0
  61. package/dist/hooks/runtime/api.js +167 -0
  62. package/dist/hooks/runtime/api.js.map +1 -0
  63. package/dist/hooks/runtime/config.js +60 -0
  64. package/dist/hooks/runtime/config.js.map +1 -0
  65. package/dist/hooks/runtime/emit.js +45 -0
  66. package/dist/hooks/runtime/emit.js.map +1 -0
  67. package/dist/hooks/runtime/fetch-rules.js +154 -0
  68. package/dist/hooks/runtime/fetch-rules.js.map +1 -0
  69. package/dist/hooks/runtime/guard-rules-cache.js +217 -0
  70. package/dist/hooks/runtime/guard-rules-cache.js.map +1 -0
  71. package/dist/hooks/runtime/guard-violations-buffer.js +105 -0
  72. package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -0
  73. package/dist/hooks/runtime/pre-compact.js +41 -0
  74. package/dist/hooks/runtime/pre-compact.js.map +1 -0
  75. package/dist/hooks/runtime/resolve.js +206 -0
  76. package/dist/hooks/runtime/resolve.js.map +1 -0
  77. package/dist/hooks/runtime/session-review.js +198 -0
  78. package/dist/hooks/runtime/session-review.js.map +1 -0
  79. package/dist/hooks/runtime/session-start.js +101 -0
  80. package/dist/hooks/runtime/session-start.js.map +1 -0
  81. package/dist/hooks/runtime/sniff.js +112 -0
  82. package/dist/hooks/runtime/sniff.js.map +1 -0
  83. package/dist/hooks/runtime/types.js +22 -0
  84. package/dist/hooks/runtime/types.js.map +1 -0
  85. package/dist/hooks/runtime/user-prompt-submit.js +154 -0
  86. package/dist/hooks/runtime/user-prompt-submit.js.map +1 -0
  87. package/dist/index.js +129 -0
  88. package/dist/index.js.map +1 -0
  89. package/dist/install.js +183 -0
  90. package/dist/install.js.map +1 -0
  91. package/dist/login.js +335 -0
  92. package/dist/login.js.map +1 -0
  93. package/dist/prompts.js +134 -0
  94. package/dist/prompts.js.map +1 -0
  95. package/dist/self-update.js +177 -0
  96. package/dist/self-update.js.map +1 -0
  97. package/dist/status.js +58 -0
  98. package/dist/status.js.map +1 -0
  99. package/dist/utils.js +84 -0
  100. package/dist/utils.js.map +1 -0
  101. package/dist/version.js +23 -0
  102. package/dist/version.js.map +1 -0
  103. package/package.json +73 -0
@@ -0,0 +1,378 @@
1
+ /**
2
+ * OpenAI Codex adapter.
3
+ *
4
+ * Two-step wiring (per https://developers.openai.com/codex/hooks):
5
+ * 1. Write hook entries to `~/.codex/hooks.json` (JSON format — the doc
6
+ * also accepts TOML in `config.toml`, but JSON is the same shape we
7
+ * use elsewhere and avoids needing a TOML AST writer).
8
+ * 2. Auto-enable the `codex_hooks` feature flag in `~/.codex/config.toml`
9
+ * (per decision 3 = A: silently flip the flag rather than printing a
10
+ * "edit this file manually" message).
11
+ *
12
+ * Feature-flag handling is intentionally conservative. We:
13
+ * - Read `config.toml` as text.
14
+ * - If `codex_hooks = true` already appears, leave the file alone.
15
+ * - If `[features]` section exists but `codex_hooks` is `false` or
16
+ * missing, append the line under that section.
17
+ * - If `[features]` doesn't exist, append a fresh `\n[features]\n
18
+ * codex_hooks = true\n` block at the end.
19
+ *
20
+ * Yes, this is regex-based and not a real TOML parser. Reason: a full
21
+ * TOML rewriter (e.g. `@iarna/toml`) strips comments + reorders keys,
22
+ * which is hostile to a config file the user may also edit. Appending
23
+ * is safer than rewriting. Doc-tested against the shapes Codex actually
24
+ * ships with.
25
+ *
26
+ * Tool matcher uses Codex's regex syntax (per its docs) — `^(Edit|Write|apply_patch)$`.
27
+ */
28
+ import { execFileSync } from 'node:child_process';
29
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
30
+ import { dirname } from 'node:path';
31
+ import { home, log, readJson, writeJson, which } from '../utils.js';
32
+ import { hookCommand, VIBEDEFEND_PATH_MARKER, isVibedefendOwnedHookCommand, } from '../hooks/install.js';
33
+ import { configDirExists, probeBinaryVersion, } from './detect.js';
34
+ const CODEX_HOOKS = home('.codex', 'hooks.json');
35
+ const CODEX_CONFIG_TOML = home('.codex', 'config.toml');
36
+ function detect() {
37
+ const version = probeBinaryVersion('codex') ?? undefined;
38
+ const hasConfigDir = configDirExists(home('.codex'));
39
+ const installed = version !== undefined || hasConfigDir;
40
+ if (!installed) {
41
+ return {
42
+ installed: false,
43
+ supportsHooks: false,
44
+ reason: '`codex` CLI not on PATH and no ~/.codex/ directory.',
45
+ };
46
+ }
47
+ return {
48
+ installed: true,
49
+ version,
50
+ supportsHooks: true,
51
+ // No version gate documented — hooks are behind a feature flag we
52
+ // auto-enable below. If a very old codex predates hooks, the flag
53
+ // is harmless (ignored).
54
+ };
55
+ }
56
+ function supports(event) {
57
+ // Codex lists SessionStart / PreToolUse / Stop. No PreCompact event.
58
+ return event !== 'pre-compact';
59
+ }
60
+ /**
61
+ * Drop hook entries owned by vibedefend, in BOTH the current nested shape
62
+ * and the legacy flat shape V1 used to write (kept for migration: re-installing
63
+ * on top of a V1 hooks.json must scrub the old broken entries before adding
64
+ * the new well-formed ones). Third-party entries are never matched and pass
65
+ * through untouched.
66
+ */
67
+ function dropOurs(entries) {
68
+ return entries.filter((e) => {
69
+ // New nested shape: { matcher?, hooks: [{ type, command }] }
70
+ if (Array.isArray(e.hooks)) {
71
+ const handlers = e.hooks;
72
+ return !handlers.some((h) => typeof h.command === 'string' &&
73
+ isVibedefendOwnedHookCommand(h.command));
74
+ }
75
+ // Legacy flat shape: { command, matchers? } — from V1 vibedefend-cli
76
+ // before the schema fix; also drops V0 `cybedefend-claude-hooks/` paths
77
+ // if they ever land in a Codex hooks.json.
78
+ const flatCommand = e.command;
79
+ if (typeof flatCommand === 'string') {
80
+ return !isVibedefendOwnedHookCommand(flatCommand);
81
+ }
82
+ // Unknown shape — leave it alone.
83
+ return true;
84
+ });
85
+ }
86
+ /**
87
+ * Ensure `hooks = true` is set under `[features]` in `~/.codex/config.toml`.
88
+ * Returns `true` if the file was modified, `false` if it was already correct.
89
+ *
90
+ * Codex 0.131 deprecated `[features].codex_hooks` (the original V1 flag) in
91
+ * favour of `[features].hooks`. The deprecated key is no longer honored — it
92
+ * prints a startup warning AND leaves the hooks subsystem off entirely. The
93
+ * symptom: Codex's `/hooks` panel shows `Installed 0 / Active 0` for every
94
+ * event, and `hooks.json` is never read.
95
+ *
96
+ * This function therefore also MIGRATES any legacy `codex_hooks = …` line
97
+ * out of the file (deleting it) so Codex stops printing the deprecation
98
+ * warning. It does NOT preserve any value the user may have set on the
99
+ * legacy key — `vibedefend install` is the only writer for both keys, so
100
+ * losing information here is acceptable.
101
+ *
102
+ * Exported for tests — `writeSettings` calls it as part of the wire-up.
103
+ */
104
+ export function ensureCodexFeatureFlag(configPath = CODEX_CONFIG_TOML) {
105
+ const originalContent = existsSync(configPath)
106
+ ? readFileSync(configPath, 'utf8')
107
+ : '';
108
+ let content = originalContent;
109
+ // ── 1. Migration: drop any legacy `codex_hooks = …` line ────────────
110
+ // We always remove it regardless of value, so the deprecation warning
111
+ // Codex prints on startup goes away. We do it BEFORE the canonical-key
112
+ // check so a file with both keys still ends up clean.
113
+ if (/^\s*codex_hooks\s*=/m.test(content)) {
114
+ // Match the whole line including its trailing newline so we don't
115
+ // leave an empty line behind.
116
+ content = content.replace(/^\s*codex_hooks\s*=\s*\S+\s*\r?\n?/m, '');
117
+ }
118
+ // ── 2. Ensure `hooks = true` is set ──────────────────────────────────
119
+ const canonicalAlreadyTrue = /^hooks\s*=\s*true\s*$/m.test(content);
120
+ if (canonicalAlreadyTrue) {
121
+ // Idempotent — but if step 1 changed anything (legacy key removed),
122
+ // we still need to persist; only return early when content matches
123
+ // the original byte-for-byte.
124
+ if (content === originalContent)
125
+ return false;
126
+ mkdirSync(dirname(configPath), { recursive: true });
127
+ writeFileSync(configPath, content);
128
+ return true;
129
+ }
130
+ const featuresHeader = /^\[features\]\s*$/m;
131
+ if (featuresHeader.test(content)) {
132
+ if (/^\s*hooks\s*=/m.test(content)) {
133
+ // hooks = <something other than true> — flip it.
134
+ content = content.replace(/^\s*hooks\s*=\s*\S+\s*$/m, 'hooks = true');
135
+ }
136
+ else {
137
+ // Insert under the [features] header.
138
+ content = content.replace(featuresHeader, '[features]\nhooks = true');
139
+ }
140
+ }
141
+ else {
142
+ // No [features] section — append a fresh one. Newline padding so we
143
+ // don't merge with a preceding stanza.
144
+ if (content.length > 0 && !content.endsWith('\n'))
145
+ content += '\n';
146
+ content += '\n[features]\nhooks = true\n';
147
+ }
148
+ if (content === originalContent)
149
+ return false;
150
+ mkdirSync(dirname(configPath), { recursive: true });
151
+ writeFileSync(configPath, content);
152
+ return true;
153
+ }
154
+ /**
155
+ * Register the CybeDefend MCP server under `[mcp_servers.<name>]` in
156
+ * `~/.codex/config.toml`. Idempotent — if the same header already exists,
157
+ * we replace the `url` value (keeps the entry consistent if the region
158
+ * changed); otherwise we append a fresh block at the end.
159
+ *
160
+ * We use `indexOf`-based block extraction rather than a single regex
161
+ * because JS regex doesn't support the `\Z` (end-of-string) anchor and
162
+ * faking it with `$(?![\s\S])` reads worse than the explicit slicing.
163
+ *
164
+ * Exported for tests.
165
+ */
166
+ export function ensureCodexMcpServer(configPath, region) {
167
+ let content = existsSync(configPath) ? readFileSync(configPath, 'utf8') : '';
168
+ const headerLine = `[mcp_servers.${region.mcpName}]`;
169
+ // Locate the header line at the START of a line (avoids accidental match
170
+ // inside a comment or inside another key's value).
171
+ const headerSearchRe = new RegExp(`^\\[mcp_servers\\.${region.mcpName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]\\s*$`, 'm');
172
+ const headerMatch = content.match(headerSearchRe);
173
+ if (headerMatch === null || headerMatch.index === undefined) {
174
+ // Not found — append a fresh block.
175
+ if (content.length > 0 && !content.endsWith('\n'))
176
+ content += '\n';
177
+ content += `\n${headerLine}\nurl = "${region.mcpUrl}"\n`;
178
+ mkdirSync(dirname(configPath), { recursive: true });
179
+ writeFileSync(configPath, content);
180
+ return { added: true, updated: false };
181
+ }
182
+ // Slice out the existing block: from the header to either the next
183
+ // `\n[` (start of another table) or end of file.
184
+ const headerStart = headerMatch.index;
185
+ const headerEnd = headerStart + headerMatch[0].length;
186
+ const nextHeaderRelative = content.slice(headerEnd).search(/\n\[/);
187
+ const blockEnd = nextHeaderRelative === -1 ? content.length : headerEnd + nextHeaderRelative;
188
+ const body = content.slice(headerEnd, blockEnd);
189
+ // Replace the `url = ...` line if present; append one otherwise.
190
+ //
191
+ // Critical: use `[ \t]*` (not `\s*`) for the in-line whitespace because
192
+ // `\s` includes `\n` — a `\s*` after `^` greedily eats the newline that
193
+ // separates the header from the body, and the replace then drops it,
194
+ // resulting in `[mcp_servers.X]url = "..."` (no newline). Same applies
195
+ // to the gap between `url` and `=`.
196
+ let newBody;
197
+ if (/^url[ \t]*=/m.test(body)) {
198
+ newBody = body.replace(/^url[ \t]*=[ \t]*\S.*$/m, `url = "${region.mcpUrl}"`);
199
+ }
200
+ else {
201
+ newBody = '\n' + `url = "${region.mcpUrl}"` + body;
202
+ }
203
+ if (newBody === body) {
204
+ return { added: false, updated: false };
205
+ }
206
+ const newContent = content.slice(0, headerEnd) + newBody + content.slice(blockEnd);
207
+ mkdirSync(dirname(configPath), { recursive: true });
208
+ writeFileSync(configPath, newContent);
209
+ return { added: false, updated: true };
210
+ }
211
+ function registerMcp(opts) {
212
+ log.step(`Registering MCP "${opts.region.mcpName}" in Codex`);
213
+ log.hint(`URL: ${opts.region.mcpUrl}`);
214
+ const { added, updated } = ensureCodexMcpServer(CODEX_CONFIG_TOML, opts.region);
215
+ if (added) {
216
+ log.ok(`Added [mcp_servers.${opts.region.mcpName}] to ${CODEX_CONFIG_TOML}`);
217
+ }
218
+ else if (updated) {
219
+ log.ok(`Updated [mcp_servers.${opts.region.mcpName}] url in ${CODEX_CONFIG_TOML}`);
220
+ }
221
+ else {
222
+ log.hint(`[mcp_servers.${opts.region.mcpName}] already configured in ${CODEX_CONFIG_TOML}`);
223
+ }
224
+ return true;
225
+ }
226
+ /**
227
+ * Codex-specific post-registration step. After the `[mcp_servers.<name>]`
228
+ * block is written to `config.toml`, Codex still requires an explicit
229
+ * `codex mcp login <name>` before the first MCP tool call succeeds.
230
+ * We run it for the user.
231
+ *
232
+ * Never throws — if the `codex` CLI is missing or login fails, we log a
233
+ * warning telling the user to run it manually and the install continues.
234
+ */
235
+ function postRegister(opts) {
236
+ // Codex is "detected" if EITHER the `codex` CLI is on PATH OR the
237
+ // ~/.codex/ config dir exists (the IDE extension). `codex mcp login` is a
238
+ // CLI command — when only the extension is present there is no binary to
239
+ // run, so skip with an informational hint instead of an ENOENT warning.
240
+ const cliOnPath = opts.cliOnPath ?? (() => which('codex') !== null);
241
+ if (!cliOnPath()) {
242
+ log.hint(`Codex CLI not on PATH — skipping \`codex mcp login\`. Codex was ` +
243
+ `detected via its ~/.codex/ config (the IDE extension), which ` +
244
+ `authenticates the MCP on first use. If you also use the Codex ` +
245
+ `CLI, run \`codex mcp login ${opts.region.mcpName}\` after ` +
246
+ `installing it.`);
247
+ return;
248
+ }
249
+ const spawn = opts.spawn ??
250
+ ((cmd, args) => {
251
+ execFileSync(cmd, args, { stdio: 'inherit' });
252
+ });
253
+ log.step(`Authenticating Codex MCP "${opts.region.mcpName}" (codex mcp login)`);
254
+ try {
255
+ spawn('codex', ['mcp', 'login', opts.region.mcpName]);
256
+ log.ok('Codex MCP login complete.');
257
+ }
258
+ catch (err) {
259
+ const msg = err instanceof Error ? err.message : String(err);
260
+ log.warn(`\`codex mcp login ${opts.region.mcpName}\` failed (${msg}). ` +
261
+ 'Run it manually if Codex MCP tool calls fail.');
262
+ }
263
+ }
264
+ function writeSettings(opts) {
265
+ // Ensure parent directory exists.
266
+ mkdirSync(dirname(CODEX_HOOKS), { recursive: true });
267
+ const file = readJson(CODEX_HOOKS, {});
268
+ file.hooks ??= {};
269
+ file.hooks.PreToolUse ??= [];
270
+ file.hooks.SessionStart ??= [];
271
+ file.hooks.Stop ??= [];
272
+ file.hooks.PreToolUse = dropOurs(file.hooks.PreToolUse);
273
+ file.hooks.SessionStart = dropOurs(file.hooks.SessionStart);
274
+ file.hooks.Stop = dropOurs(file.hooks.Stop);
275
+ // PreToolUse: fire on apply_patch / Edit / Write (matcher is a single
276
+ // regex string per Codex docs; the three values are matcher-aliases Codex
277
+ // accepts for its apply_patch tool).
278
+ file.hooks.PreToolUse.push({
279
+ matcher: '^(Edit|Write|apply_patch)$',
280
+ hooks: [
281
+ {
282
+ type: 'command',
283
+ command: hookCommand('fetch-rules'),
284
+ statusMessage: 'CybeDefend: fetching relevant rules',
285
+ },
286
+ ],
287
+ });
288
+ // Action Guards: no matcher → fires for every tool call. Exit code 2
289
+ // signals Codex (via its hook subsystem) to block the tool call.
290
+ file.hooks.PreToolUse.push({
291
+ hooks: [
292
+ {
293
+ type: 'command',
294
+ command: hookCommand('guard-check'),
295
+ statusMessage: 'CybeDefend: checking action guards',
296
+ },
297
+ ],
298
+ });
299
+ // SessionStart: no matcher → fires on every source (startup/resume/clear).
300
+ file.hooks.SessionStart.push({
301
+ hooks: [
302
+ {
303
+ type: 'command',
304
+ command: hookCommand('session-start'),
305
+ statusMessage: 'CybeDefend: loading project context',
306
+ },
307
+ ],
308
+ });
309
+ // Stop has no matcher per Codex docs.
310
+ if (opts.enableSessionReview) {
311
+ file.hooks.Stop.push({
312
+ hooks: [
313
+ {
314
+ type: 'command',
315
+ command: hookCommand('session-review'),
316
+ statusMessage: 'CybeDefend: session review',
317
+ timeout: 60,
318
+ },
319
+ ],
320
+ });
321
+ }
322
+ writeJson(CODEX_HOOKS, file);
323
+ const flagChanged = ensureCodexFeatureFlag();
324
+ if (flagChanged) {
325
+ log.ok(`Enabled [features].hooks in ${CODEX_CONFIG_TOML}`);
326
+ }
327
+ else {
328
+ log.hint(`[features].hooks already set in ${CODEX_CONFIG_TOML}`);
329
+ }
330
+ log.ok(`Codex hooks wired into ${CODEX_HOOKS}`);
331
+ // Codex 0.131+ requires per-hook trust review before any hook fires.
332
+ // Until the user opens Codex and trusts each entry, `/hooks` shows
333
+ // `Installed 0 / Active 0` and the SessionStart projectId block is
334
+ // silently dropped — the symptom the user observed before this fix.
335
+ log.warn('Codex requires manual hook approval. Open Codex and run `/hooks` to ' +
336
+ 'review and trust the cybedefend hooks before they fire (until then ' +
337
+ 'they show as Installed > 0 but Active = 0).');
338
+ log.hint('Note: Codex has no PreCompact event, so long-session gap analysis only fires at Stop.');
339
+ }
340
+ /**
341
+ * Pure introspection — never throws. `hooksWired` from `~/.codex/hooks.json`,
342
+ * `mcpRegistered` by testing for the `[mcp_servers.<mcpName>]` table header
343
+ * in `~/.codex/config.toml` (the same anchor `ensureCodexMcpServer` writes).
344
+ * The header is matched at line-start so it doesn't false-positive inside a
345
+ * comment or another key's value.
346
+ */
347
+ function inspect(region) {
348
+ let hooksWired = false;
349
+ try {
350
+ if (existsSync(CODEX_HOOKS)) {
351
+ hooksWired = readFileSync(CODEX_HOOKS, 'utf8').includes(VIBEDEFEND_PATH_MARKER);
352
+ }
353
+ }
354
+ catch {
355
+ /* missing/unreadable → false */
356
+ }
357
+ let mcpRegistered = false;
358
+ try {
359
+ if (existsSync(CODEX_CONFIG_TOML)) {
360
+ mcpRegistered = new RegExp(`^\\[mcp_servers\\.${region.mcpName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]\\s*$`, 'm').test(readFileSync(CODEX_CONFIG_TOML, 'utf8'));
361
+ }
362
+ }
363
+ catch {
364
+ /* → false */
365
+ }
366
+ return { hooksWired, mcpRegistered };
367
+ }
368
+ export const codexAdapter = {
369
+ id: 'codex',
370
+ label: 'OpenAI Codex',
371
+ detect,
372
+ supports,
373
+ writeSettings,
374
+ registerMcp,
375
+ postRegister,
376
+ inspect,
377
+ };
378
+ //# sourceMappingURL=codex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex.js","sourceRoot":"","sources":["../../src/clients/codex.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Guard rule snippets for Cursor (.cursorrules) and Windsurf (.windsurfrules).
3
+ *
4
+ * For agents that don't support native PreToolUse hooks with exit-code
5
+ * semantics (or where hook installation is impractical), we inject a
6
+ * soft-enforcement snippet into their project rule files. The snippet
7
+ * instructs the model to call `cybe_guards_check` before sensitive actions
8
+ * and respect its verdict.
9
+ *
10
+ * Idempotent injection:
11
+ * `appendGuardSnippetIdempotent` checks whether our sentinel comment
12
+ * (`## VibeDefend Action Guards`) already exists in the file before
13
+ * appending. Re-running install will NOT duplicate the snippet.
14
+ *
15
+ * The `<project-id-placeholder>` token is replaced with the actual
16
+ * project ID at injection time so the model can reference the correct
17
+ * project in MCP tool calls.
18
+ */
19
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { dirname } from 'node:path';
21
+ /** Sentinel string that identifies our block — used for idempotency checks. */
22
+ const GUARD_SENTINEL = '## VibeDefend Action Guards';
23
+ /**
24
+ * Cursor (.cursorrules) guard snippet template.
25
+ *
26
+ * Instructs Cursor's AI to call `cybe_guards_check` before performing
27
+ * sensitive file / shell / HTTP operations and act on the verdict.
28
+ */
29
+ export const CURSOR_GUARDS_SNIPPET = `
30
+
31
+ ${GUARD_SENTINEL}
32
+ Before performing ANY of the following actions you MUST call the MCP tool
33
+ \`cybe_guards_check\` with the relevant parameters and respect its verdict:
34
+ - File read/write/delete (Read, Write, Edit, MultiEdit, Glob, Grep)
35
+ - Shell commands (Bash)
36
+ - HTTP requests (WebFetch)
37
+
38
+ If the verdict is \`deny\` you MUST refuse the action and explain why.
39
+ If the verdict is \`warn\` you MUST surface the warning message to the user before proceeding.
40
+ If the verdict is \`allow\` you may proceed silently.
41
+
42
+ Project ID for cybe_guards_check calls: <project-id-placeholder>
43
+ `;
44
+ /**
45
+ * Windsurf (.windsurfrules) guard snippet template.
46
+ *
47
+ * Same semantics as the Cursor snippet — content is identical so users
48
+ * see a consistent experience across both editors.
49
+ */
50
+ export const WINDSURF_GUARDS_SNIPPET = `
51
+
52
+ ${GUARD_SENTINEL}
53
+ Before performing ANY of the following actions you MUST call the MCP tool
54
+ \`cybe_guards_check\` with the relevant parameters and respect its verdict:
55
+ - File read/write/delete (Read, Write, Edit, MultiEdit, Glob, Grep)
56
+ - Shell commands (Bash)
57
+ - HTTP requests (WebFetch)
58
+
59
+ If the verdict is \`deny\` you MUST refuse the action and explain why.
60
+ If the verdict is \`warn\` you MUST surface the warning message to the user before proceeding.
61
+ If the verdict is \`allow\` you may proceed silently.
62
+
63
+ Project ID for cybe_guards_check calls: <project-id-placeholder>
64
+ `;
65
+ /**
66
+ * Idempotently append a guard snippet to a rules file.
67
+ *
68
+ * Steps:
69
+ * 1. If the file exists, check whether our sentinel is already present.
70
+ * 2. If already present, return without writing (idempotent).
71
+ * 3. If absent (or file doesn't exist), append the snippet with the
72
+ * project ID substituted for the placeholder.
73
+ * 4. Create parent directories if needed.
74
+ *
75
+ * Preserves any existing content.
76
+ */
77
+ export function appendGuardSnippetIdempotent(filePath, snippet, projectId) {
78
+ const existing = existsSync(filePath)
79
+ ? readFileSync(filePath, 'utf8')
80
+ : '';
81
+ // Idempotency check — sentinel already present
82
+ if (existing.includes(GUARD_SENTINEL))
83
+ return;
84
+ // Substitute the project ID placeholder
85
+ const finalSnippet = snippet.replace('<project-id-placeholder>', projectId);
86
+ // Ensure parent directory exists
87
+ const dir = dirname(filePath);
88
+ if (!existsSync(dir)) {
89
+ mkdirSync(dir, { recursive: true });
90
+ }
91
+ // Append (existing content + snippet)
92
+ writeFileSync(filePath, existing + finalSnippet, 'utf8');
93
+ }
94
+ //# sourceMappingURL=cursor-guards-rules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor-guards-rules.js","sourceRoot":"","sources":["../../src/clients/cursor-guards-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,+EAA+E;AAC/E,MAAM,cAAc,GAAG,6BAA6B,CAAC;AAErD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;EAEnC,cAAc;;;;;;;;;;;;CAYf,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG;;EAErC,cAAc;;;;;;;;;;;;CAYf,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,4BAA4B,CAC1C,QAAgB,EAChB,OAAe,EACf,SAAiB;IAEjB,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAC;QACnC,CAAC,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC;QAChC,CAAC,CAAC,EAAE,CAAC;IAEP,+CAA+C;IAC/C,IAAI,QAAQ,CAAC,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO;IAE9C,wCAAwC;IACxC,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAClC,0BAA0B,EAC1B,SAAS,CACV,CAAC;IAEF,iCAAiC;IACjC,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IAED,sCAAsC;IACtC,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,YAAY,EAAE,MAAM,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Cursor adapter.
3
+ *
4
+ * Wires our universal hook scripts into `~/.cursor/hooks.json`. Cursor's
5
+ * hooks format (per https://cursor.com/docs/hooks) uses a top-level
6
+ * `version` field + a `hooks` object mapping camelCase event names
7
+ * (`preToolUse`, `sessionStart`, `stop`, `preCompact`) to arrays of
8
+ * `{ command, matcher?, failClosed? }` objects.
9
+ *
10
+ * Cursor supports per-event matchers (unlike VS Code which ignores them);
11
+ * we set the same `Edit|Write|MultiEdit` matcher on `preToolUse` for
12
+ * consistency with Claude Code's behaviour.
13
+ *
14
+ * Min version: 1.7 (per Cursor's release notes — hooks were added in
15
+ * the 1.7 stable release). Earlier versions silently ignore `hooks.json`.
16
+ *
17
+ * Hook semantics (verified May 2026 against cursor.com/docs/agent/hooks):
18
+ * Exit code 2 → hard-blocks the tool call (same as Claude Code/Codex).
19
+ * Other non-zero → hook failure, action proceeds (fail-open by default).
20
+ * `failClosed: true` can override fail-open for other non-zero codes.
21
+ *
22
+ * Because exit code 2 hard-blocks, Cursor's Action Guards enforcement is
23
+ * equivalent to Claude Code's. The guard-check hook always exits 2 on
24
+ * `deny`, so Cursor is classified as "Hard enforcement" alongside
25
+ * Claude Code and Codex.
26
+ */
27
+ import { existsSync, readFileSync } from 'node:fs';
28
+ import { home, log, readJson, writeJson } from '../utils.js';
29
+ import { hookCommand, VIBEDEFEND_PATH_MARKER, isVibedefendOwnedHookCommand, } from '../hooks/install.js';
30
+ import { configDirExists, probeBinaryVersion, versionAtLeast, } from './detect.js';
31
+ const CURSOR_HOOKS = home('.cursor', 'hooks.json');
32
+ const CURSOR_MCP = home('.cursor', 'mcp.json');
33
+ const MIN_VERSION = '1.7.0';
34
+ function detect() {
35
+ // Cursor on macOS often isn't on PATH (lives in /Applications/Cursor.app).
36
+ // Two probes: (a) `cursor --version` if the user added the CLI shim,
37
+ // (b) presence of `~/.cursor/` config dir as a fallback signal.
38
+ const version = probeBinaryVersion('cursor') ?? undefined;
39
+ const hasConfigDir = configDirExists(home('.cursor'));
40
+ const installed = version !== undefined || hasConfigDir;
41
+ if (!installed) {
42
+ return {
43
+ installed: false,
44
+ supportsHooks: false,
45
+ reason: '`cursor` CLI not on PATH and no ~/.cursor/ directory.',
46
+ };
47
+ }
48
+ if (version === undefined) {
49
+ // Can't probe version (no CLI shim), but the user obviously has Cursor.
50
+ // Soft-pass with a warning the user can act on.
51
+ return {
52
+ installed: true,
53
+ supportsHooks: true,
54
+ reason: 'Could not check Cursor version (CLI shim not installed). Hooks need Cursor >= 1.7.',
55
+ };
56
+ }
57
+ if (!versionAtLeast(version, MIN_VERSION)) {
58
+ return {
59
+ installed: true,
60
+ version,
61
+ supportsHooks: false,
62
+ reason: `Cursor ${version} is below the minimum ${MIN_VERSION} required for hooks. Update Cursor.`,
63
+ };
64
+ }
65
+ return { installed: true, version, supportsHooks: true };
66
+ }
67
+ function supports(event) {
68
+ // Cursor exposes all four lifecycle moments per its docs.
69
+ return ['fetch-rules', 'session-start', 'stop', 'pre-compact'].includes(event);
70
+ }
71
+ /**
72
+ * Drop any prior entry owned by vibedefend (V0 manual template, V1 bash
73
+ * hooks, current V2 Node runner). See `isVibedefendOwnedHookCommand` for
74
+ * the exact match set. Third-party hooks pass through untouched.
75
+ */
76
+ function dropOurs(entries) {
77
+ return entries.filter((e) => !isVibedefendOwnedHookCommand(e.command));
78
+ }
79
+ function writeSettings(opts) {
80
+ const file = readJson(CURSOR_HOOKS, {});
81
+ file.version ??= '1';
82
+ file.hooks ??= {};
83
+ file.hooks.preToolUse ??= [];
84
+ file.hooks.sessionStart ??= [];
85
+ file.hooks.stop ??= [];
86
+ file.hooks.preCompact ??= [];
87
+ file.hooks.preToolUse = dropOurs(file.hooks.preToolUse);
88
+ file.hooks.sessionStart = dropOurs(file.hooks.sessionStart);
89
+ file.hooks.stop = dropOurs(file.hooks.stop);
90
+ file.hooks.preCompact = dropOurs(file.hooks.preCompact);
91
+ file.hooks.preToolUse.push({
92
+ command: hookCommand('fetch-rules'),
93
+ matcher: 'Edit|Write|MultiEdit',
94
+ // failClosed: false (default) — if our hook crashes, edit still
95
+ // proceeds. We never block the user on a transient gateway issue.
96
+ });
97
+ // Action Guards: hard-block hook. No matcher so it fires for every tool
98
+ // call. failClosed intentionally absent (false) — a hook crash never
99
+ // blocks the user's action, only a deliberate deny verdict does.
100
+ file.hooks.preToolUse.push({
101
+ command: hookCommand('guard-check'),
102
+ });
103
+ file.hooks.sessionStart.push({ command: hookCommand('session-start') });
104
+ if (opts.enableSessionReview) {
105
+ file.hooks.stop.push({ command: hookCommand('session-review') });
106
+ file.hooks.preCompact.push({ command: hookCommand('pre-compact') });
107
+ }
108
+ writeJson(CURSOR_HOOKS, file);
109
+ log.ok(`Cursor hooks wired into ${CURSOR_HOOKS}`);
110
+ // NOTE: We do NOT inject a guard snippet into .cursorrules because Cursor's
111
+ // hooks.preToolUse hard-blocks on exit code 2 (same semantics as Claude Code).
112
+ // The hook is the enforcement primitive; the LLM should NOT be told to call
113
+ // cybe_guards_check preemptively — that would dilute the "real interception"
114
+ // UX. Windsurf still gets a .windsurfrules snippet because its pre_write_code
115
+ // hook only covers writes.
116
+ }
117
+ /**
118
+ * Register the CybeDefend MCP server in `~/.cursor/mcp.json`.
119
+ *
120
+ * Cursor's MCP config is a separate file from its hooks config
121
+ * (`hooks.json`). Schema (per cursor.com/docs/mcp):
122
+ * { "mcpServers": { "<name>": { "url": "<streamable-http url>" } } }
123
+ *
124
+ * Idempotent — re-running install replaces the entry with the current
125
+ * region's URL (handles the case where the user switches region).
126
+ */
127
+ function registerMcp(opts) {
128
+ log.step(`Registering MCP "${opts.region.mcpName}" in Cursor`);
129
+ log.hint(`URL: ${opts.region.mcpUrl}`);
130
+ const file = readJson(CURSOR_MCP, {});
131
+ file.mcpServers ??= {};
132
+ file.mcpServers[opts.region.mcpName] = { url: opts.region.mcpUrl };
133
+ writeJson(CURSOR_MCP, file);
134
+ log.ok(`MCP "${opts.region.mcpName}" registered in ${CURSOR_MCP}`);
135
+ return true;
136
+ }
137
+ /**
138
+ * Pure introspection — never throws. `hooksWired` from `~/.cursor/hooks.json`,
139
+ * `mcpRegistered` from `~/.cursor/mcp.json`. A raw-text substring check is
140
+ * sufficient and keeps the never-throw guarantee (no JSON parse to fail on).
141
+ */
142
+ function inspect(region) {
143
+ let hooksWired = false;
144
+ try {
145
+ if (existsSync(CURSOR_HOOKS)) {
146
+ hooksWired = readFileSync(CURSOR_HOOKS, 'utf8').includes(VIBEDEFEND_PATH_MARKER);
147
+ }
148
+ }
149
+ catch {
150
+ /* missing/unreadable → false */
151
+ }
152
+ let mcpRegistered = false;
153
+ try {
154
+ if (existsSync(CURSOR_MCP)) {
155
+ mcpRegistered = readFileSync(CURSOR_MCP, 'utf8').includes(region.mcpName);
156
+ }
157
+ }
158
+ catch {
159
+ /* → false */
160
+ }
161
+ return { hooksWired, mcpRegistered };
162
+ }
163
+ export const cursorAdapter = {
164
+ id: 'cursor',
165
+ label: 'Cursor',
166
+ detect,
167
+ supports,
168
+ writeSettings,
169
+ registerMcp,
170
+ inspect,
171
+ };
172
+ //# sourceMappingURL=cursor.js.map