@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.
- package/LICENSE +77 -0
- package/README.md +120 -0
- package/bin/vibedefend.js +19 -0
- package/dist/auth/auth-store.js +170 -0
- package/dist/auth/auth-store.js.map +1 -0
- package/dist/auth/auth.js +125 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/callback-server.js +216 -0
- package/dist/auth/callback-server.js.map +1 -0
- package/dist/auth/pkce.js +31 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/token-exchange.js +83 -0
- package/dist/auth/token-exchange.js.map +1 -0
- package/dist/clients/claude-code.js +170 -0
- package/dist/clients/claude-code.js.map +1 -0
- package/dist/clients/codex.js +378 -0
- package/dist/clients/codex.js.map +1 -0
- package/dist/clients/cursor-guards-rules.js +94 -0
- package/dist/clients/cursor-guards-rules.js.map +1 -0
- package/dist/clients/cursor.js +172 -0
- package/dist/clients/cursor.js.map +1 -0
- package/dist/clients/detect.js +86 -0
- package/dist/clients/detect.js.map +1 -0
- package/dist/clients/registry.js +41 -0
- package/dist/clients/registry.js.map +1 -0
- package/dist/clients/types.js +2 -0
- package/dist/clients/types.js.map +1 -0
- package/dist/clients/vscode.js +187 -0
- package/dist/clients/vscode.js.map +1 -0
- package/dist/clients/windsurf.js +151 -0
- package/dist/clients/windsurf.js.map +1 -0
- package/dist/config.js +32 -0
- package/dist/config.js.map +1 -0
- package/dist/custom-regions.js +112 -0
- package/dist/custom-regions.js.map +1 -0
- package/dist/diagnostics.js +122 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/doctor.js +125 -0
- package/dist/doctor.js.map +1 -0
- package/dist/guards-evaluator/bucketing.js +83 -0
- package/dist/guards-evaluator/bucketing.js.map +1 -0
- package/dist/guards-evaluator/evaluate.js +272 -0
- package/dist/guards-evaluator/evaluate.js.map +1 -0
- package/dist/guards-evaluator/glob.js +148 -0
- package/dist/guards-evaluator/glob.js.map +1 -0
- package/dist/guards-evaluator/index.js +9 -0
- package/dist/guards-evaluator/index.js.map +1 -0
- package/dist/guards-evaluator/preprocess.js +174 -0
- package/dist/guards-evaluator/preprocess.js.map +1 -0
- package/dist/guards-evaluator/redact.js +111 -0
- package/dist/guards-evaluator/redact.js.map +1 -0
- package/dist/guards-evaluator/regex.js +125 -0
- package/dist/guards-evaluator/regex.js.map +1 -0
- package/dist/guards-evaluator/types.js +2 -0
- package/dist/guards-evaluator/types.js.map +1 -0
- package/dist/guards-evaluator/validation.js +115 -0
- package/dist/guards-evaluator/validation.js.map +1 -0
- package/dist/hook-runner.js +6680 -0
- package/dist/hooks/install.js +169 -0
- package/dist/hooks/install.js.map +1 -0
- package/dist/hooks/runtime/api.js +167 -0
- package/dist/hooks/runtime/api.js.map +1 -0
- package/dist/hooks/runtime/config.js +60 -0
- package/dist/hooks/runtime/config.js.map +1 -0
- package/dist/hooks/runtime/emit.js +45 -0
- package/dist/hooks/runtime/emit.js.map +1 -0
- package/dist/hooks/runtime/fetch-rules.js +154 -0
- package/dist/hooks/runtime/fetch-rules.js.map +1 -0
- package/dist/hooks/runtime/guard-rules-cache.js +217 -0
- package/dist/hooks/runtime/guard-rules-cache.js.map +1 -0
- package/dist/hooks/runtime/guard-violations-buffer.js +105 -0
- package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -0
- package/dist/hooks/runtime/pre-compact.js +41 -0
- package/dist/hooks/runtime/pre-compact.js.map +1 -0
- package/dist/hooks/runtime/resolve.js +206 -0
- package/dist/hooks/runtime/resolve.js.map +1 -0
- package/dist/hooks/runtime/session-review.js +198 -0
- package/dist/hooks/runtime/session-review.js.map +1 -0
- package/dist/hooks/runtime/session-start.js +101 -0
- package/dist/hooks/runtime/session-start.js.map +1 -0
- package/dist/hooks/runtime/sniff.js +112 -0
- package/dist/hooks/runtime/sniff.js.map +1 -0
- package/dist/hooks/runtime/types.js +22 -0
- package/dist/hooks/runtime/types.js.map +1 -0
- package/dist/hooks/runtime/user-prompt-submit.js +154 -0
- package/dist/hooks/runtime/user-prompt-submit.js.map +1 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/install.js +183 -0
- package/dist/install.js.map +1 -0
- package/dist/login.js +335 -0
- package/dist/login.js.map +1 -0
- package/dist/prompts.js +134 -0
- package/dist/prompts.js.map +1 -0
- package/dist/self-update.js +177 -0
- package/dist/self-update.js.map +1 -0
- package/dist/status.js +58 -0
- package/dist/status.js.map +1 -0
- package/dist/utils.js +84 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- 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
|