fullcourtdefense-cli 1.0.2 → 1.1.0

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.
@@ -0,0 +1,312 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.hookCommand = hookCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
41
+ function readStdin() {
42
+ return new Promise((resolve) => {
43
+ let data = '';
44
+ const stdin = process.stdin;
45
+ if (stdin.isTTY) {
46
+ resolve('');
47
+ return;
48
+ }
49
+ stdin.setEncoding('utf8');
50
+ stdin.on('data', (chunk) => { data += chunk; });
51
+ stdin.on('end', () => resolve(data));
52
+ stdin.on('error', () => resolve(data));
53
+ // Safety: if nothing arrives, don't hang forever.
54
+ setTimeout(() => resolve(data), 2000).unref?.();
55
+ });
56
+ }
57
+ /** Load ~/.fullcourtdefense.yml (machine-wide creds) as a fallback to project config. */
58
+ function loadHomeShield() {
59
+ try {
60
+ const candidates = ['.fullcourtdefense.yml', '.fullcourtdefense.yaml'];
61
+ for (const name of candidates) {
62
+ const p = path.join(os.homedir(), name);
63
+ if (!fs.existsSync(p))
64
+ continue;
65
+ const text = fs.readFileSync(p, 'utf8');
66
+ const pick = (key) => {
67
+ const m = text.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+)\\s*$`, 'm'));
68
+ if (!m)
69
+ return undefined;
70
+ return m[1].replace(/^["']|["']$/g, '').trim() || undefined;
71
+ };
72
+ return { shieldId: pick('shieldId'), shieldKey: pick('shieldKey'), apiUrl: pick('apiUrl') };
73
+ }
74
+ }
75
+ catch { /* ignore */ }
76
+ return {};
77
+ }
78
+ function inferEvent(explicit, payload) {
79
+ const e = (explicit || '').toLowerCase();
80
+ if (e) {
81
+ if (e.startsWith('prompt'))
82
+ return 'prompt';
83
+ if (e.startsWith('shell'))
84
+ return 'shell';
85
+ if (e.startsWith('mcp'))
86
+ return 'mcp';
87
+ if (e.startsWith('file') || e === 'edit')
88
+ return 'file';
89
+ if (e.startsWith('read'))
90
+ return 'read';
91
+ }
92
+ if (typeof payload.command === 'string')
93
+ return 'shell';
94
+ if (payload.tool_name || payload.toolName || payload.tool_input || payload.arguments)
95
+ return 'mcp';
96
+ if (payload.prompt || payload.text || payload.message)
97
+ return 'prompt';
98
+ if (payload.edits || payload.file_path || payload.path)
99
+ return 'file';
100
+ return 'unknown';
101
+ }
102
+ /** Pull the meaningful text to scan out of a Cursor hook payload, per event. */
103
+ function extractText(event, p) {
104
+ const str = (v) => (typeof v === 'string' ? v : '');
105
+ switch (event) {
106
+ case 'prompt': {
107
+ const direct = str(p.prompt) || str(p.text) || str(p.message) || str(p.content);
108
+ if (direct)
109
+ return direct;
110
+ // Some shapes nest the prompt or send a messages array.
111
+ const promptObj = p.prompt;
112
+ if (promptObj && typeof promptObj === 'object') {
113
+ const t = str(promptObj.text) || str(promptObj.content);
114
+ if (t)
115
+ return t;
116
+ }
117
+ const messages = p.messages;
118
+ if (Array.isArray(messages) && messages.length) {
119
+ const last = messages[messages.length - 1];
120
+ return str(last?.content) || str(last?.text);
121
+ }
122
+ return '';
123
+ }
124
+ case 'shell':
125
+ return str(p.command) || str(p.cmd);
126
+ case 'mcp': {
127
+ const name = str(p.tool_name) || str(p.toolName) || str(p.name);
128
+ const input = p.tool_input ?? p.arguments ?? p.input ?? p.params;
129
+ const inputStr = typeof input === 'string' ? input : input ? safeJson(input) : '';
130
+ return [name, inputStr].filter(Boolean).join(': ');
131
+ }
132
+ case 'file': {
133
+ const edits = p.edits;
134
+ if (Array.isArray(edits)) {
135
+ return edits.map((e) => str(e.new_text) || str(e.text) || str(e.content)).filter(Boolean).join('\n');
136
+ }
137
+ return str(p.content) || str(p.new_text) || '';
138
+ }
139
+ case 'read':
140
+ return str(p.file_path) || str(p.path) || '';
141
+ default:
142
+ return str(p.prompt) || str(p.text) || str(p.message) || str(p.command) || '';
143
+ }
144
+ }
145
+ function safeJson(v) {
146
+ try {
147
+ return JSON.stringify(v);
148
+ }
149
+ catch {
150
+ return String(v);
151
+ }
152
+ }
153
+ /** Per-event attribution that maps onto the existing Shield runtime headers. */
154
+ function attributionFor(event, p) {
155
+ const h = {};
156
+ const set = (k, v) => { if (v)
157
+ h[k] = v; };
158
+ switch (event) {
159
+ case 'prompt':
160
+ set('x-input-source', 'user');
161
+ set('x-runtime-event-type', 'user_message');
162
+ set('x-runtime-trust', 'trusted_user');
163
+ break;
164
+ case 'shell':
165
+ set('x-input-source', 'tool');
166
+ set('x-runtime-event-type', 'action_request');
167
+ set('x-action-type', 'shell');
168
+ set('x-runtime-sink', 'execute');
169
+ set('x-runtime-trust', 'untrusted');
170
+ break;
171
+ case 'mcp':
172
+ set('x-input-source', 'tool');
173
+ set('x-runtime-event-type', 'tool_call');
174
+ set('x-tool-name', String(p.tool_name || p.toolName || p.name || 'mcp'));
175
+ set('x-runtime-trust', 'untrusted');
176
+ break;
177
+ case 'file':
178
+ set('x-input-source', 'generated');
179
+ set('x-runtime-event-type', 'memory_write');
180
+ break;
181
+ case 'read':
182
+ set('x-input-source', 'tool');
183
+ set('x-runtime-event-type', 'memory_read');
184
+ break;
185
+ default:
186
+ set('x-input-source', 'user');
187
+ }
188
+ return h;
189
+ }
190
+ /** Stable developer identity, used for per-developer attribution on shield events. */
191
+ function developerId() {
192
+ if (process.env.FULLCOURTDEFENSE_DEVELOPER_ID)
193
+ return process.env.FULLCOURTDEFENSE_DEVELOPER_ID;
194
+ try {
195
+ const u = os.userInfo().username;
196
+ return `${u}@${os.hostname()}`;
197
+ }
198
+ catch {
199
+ return 'unknown-developer';
200
+ }
201
+ }
202
+ /** Cursor conversation/session id when present, else a stable per-machine id. */
203
+ function sessionId(p) {
204
+ const fromPayload = p.conversation_id || p.conversationId || p.session_id || p.sessionId;
205
+ if (typeof fromPayload === 'string' && fromPayload)
206
+ return fromPayload;
207
+ return `cursor-${os.hostname()}`;
208
+ }
209
+ function emit(decision, blocked) {
210
+ process.stdout.write(JSON.stringify(decision));
211
+ // Exit 2 is the universal "block" signal across all hook events.
212
+ process.exit(blocked ? 2 : 0);
213
+ }
214
+ async function hookCommand(args, config) {
215
+ const shadow = args.shadow === 'true';
216
+ const failClosed = args.failClosed === 'true';
217
+ const timeoutMs = Number(args.timeout) > 0 ? Number(args.timeout) : 8000;
218
+ const home = loadHomeShield();
219
+ const shieldId = args.shieldId || process.env.FULLCOURTDEFENSE_SHIELD_ID || config.shieldId || home.shieldId;
220
+ const shieldKey = args.shieldKey || process.env.FULLCOURTDEFENSE_SHIELD_KEY || config.shieldKey || home.shieldKey;
221
+ const apiUrl = (args.apiUrl || process.env.FULLCOURTDEFENSE_API_URL || config.apiUrl || home.apiUrl || DEFAULT_API_URL)
222
+ .replace(/\/$/, '');
223
+ let raw = '';
224
+ try {
225
+ raw = await readStdin();
226
+ }
227
+ catch { /* ignore */ }
228
+ let payload = {};
229
+ if (raw.trim()) {
230
+ try {
231
+ payload = JSON.parse(raw);
232
+ }
233
+ catch {
234
+ payload = {};
235
+ }
236
+ }
237
+ const event = inferEvent(args.event, payload);
238
+ const text = extractText(event, payload).trim();
239
+ // Nothing to scan, or no Shield configured → allow (fail-open by design here:
240
+ // a misconfigured machine must not brick the developer's IDE).
241
+ if (!text)
242
+ emit({ permission: 'allow' }, false);
243
+ if (!shieldId) {
244
+ emit({
245
+ permission: 'allow',
246
+ agent_message: 'FullCourtDefense hook installed but no Shield ID configured (run `fullcourtdefense configure`).',
247
+ }, false);
248
+ }
249
+ try {
250
+ const controller = new AbortController();
251
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
252
+ // Behaves exactly like a normal Shield agent calling the public proxy:
253
+ // Shield key + runtime/developer attribution headers, body { message }.
254
+ const headers = {
255
+ 'Content-Type': 'application/json',
256
+ 'x-runtime-source-id': developerId(),
257
+ 'x-developer-id': developerId(),
258
+ 'x-session-id': sessionId(payload),
259
+ ...attributionFor(event, payload),
260
+ };
261
+ if (shieldKey)
262
+ headers['x-shield-key'] = shieldKey;
263
+ const resp = await fetch(`${apiUrl}/api/shield/proxy/${shieldId}`, {
264
+ method: 'POST',
265
+ headers,
266
+ body: JSON.stringify({ message: text }),
267
+ signal: controller.signal,
268
+ });
269
+ clearTimeout(timer);
270
+ if (!resp.ok) {
271
+ // Backend rejected (quota, bad shield, etc.) — fail open unless told otherwise.
272
+ const blocked = failClosed;
273
+ emit({
274
+ permission: blocked ? 'deny' : 'allow',
275
+ user_message: blocked ? `FullCourtDefense gate unavailable (HTTP ${resp.status}) — blocked by fail-closed policy.` : undefined,
276
+ agent_message: `FullCourtDefense gate returned HTTP ${resp.status}.`,
277
+ }, blocked);
278
+ }
279
+ const data = await resp.json().catch(() => ({}));
280
+ const sh = (data._shield || {});
281
+ const action = String(sh.action || '');
282
+ // Proxy returns 'blocked_input' / 'blocked_output' when it blocks; in monitor
283
+ // mode it returns 'allowed' (never block, even if wouldBlock is set).
284
+ const isBlocked = action.startsWith('blocked');
285
+ const reason = String(sh.reason || 'policy_violation');
286
+ const explanation = String(sh.reason || 'Flagged by FullCourtDefense.');
287
+ if (!isBlocked) {
288
+ emit({ permission: 'allow' }, false);
289
+ }
290
+ // Blocked verdict. In shadow mode we annotate but let it through.
291
+ if (shadow) {
292
+ emit({
293
+ permission: 'allow',
294
+ agent_message: `[FullCourtDefense shadow] would block ${event} (${reason}): ${explanation}`,
295
+ }, false);
296
+ }
297
+ emit({
298
+ permission: 'deny',
299
+ user_message: `Blocked by FullCourtDefense — ${reason}. ${explanation}`,
300
+ agent_message: `FullCourtDefense blocked this ${event} as "${reason}". Do not retry; revise to remove the flagged content.`,
301
+ }, true);
302
+ }
303
+ catch (err) {
304
+ // Network error / timeout.
305
+ const blocked = failClosed;
306
+ emit({
307
+ permission: blocked ? 'deny' : 'allow',
308
+ user_message: blocked ? 'FullCourtDefense gate unreachable — blocked by fail-closed policy.' : undefined,
309
+ agent_message: `FullCourtDefense hook error: ${err instanceof Error ? err.message : String(err)}.`,
310
+ }, blocked);
311
+ }
312
+ }
@@ -68,20 +68,20 @@ async function initCommand() {
68
68
  const categories = categoriesRaw === 'all'
69
69
  ? '[jailbreak, prompt_injection, data_extraction, role_manipulation, encoding_attack, social_engineering]'
70
70
  : `[${categoriesRaw}]`;
71
- const content = `# FullCourtDefense CLI Configuration
72
- # Docs: https://fullcourtdefense.ai/docs/cli
73
-
74
- apiKey: \${BOTGUARD_API_KEY}
75
- apiUrl: https://api.fullcourtdefense.ai
76
- shieldId: ${shieldId}
77
- ${shieldKey ? `shieldKey: ${shieldKey}\n` : ''}
78
-
79
- scan:
80
- endpoint: ${endpoint}
81
- description: "${description}"
82
- categories: ${categories}
83
- failThreshold: ${threshold}
84
- format: table
71
+ const content = `# FullCourtDefense CLI Configuration
72
+ # Docs: https://fullcourtdefense.ai/docs/cli
73
+
74
+ apiKey: \${FULLCOURTDEFENSE_API_KEY}
75
+ apiUrl: https://api.fullcourtdefense.ai
76
+ shieldId: ${shieldId}
77
+ ${shieldKey ? `shieldKey: ${shieldKey}\n` : ''}
78
+
79
+ scan:
80
+ endpoint: ${endpoint}
81
+ description: "${description}"
82
+ categories: ${categories}
83
+ failThreshold: ${threshold}
84
+ format: table
85
85
  `;
86
86
  fs.writeFileSync(target, content, 'utf-8');
87
87
  console.log('');
@@ -0,0 +1,12 @@
1
+ import { BotGuardConfig } from '../config';
2
+ export interface InstallHookArgs {
3
+ project?: string;
4
+ shieldId?: string;
5
+ shieldKey?: string;
6
+ apiUrl?: string;
7
+ shadow?: string;
8
+ failClosed?: string;
9
+ events?: string;
10
+ }
11
+ export declare function installCursorHookCommand(args: InstallHookArgs, config: BotGuardConfig): Promise<void>;
12
+ export declare function uninstallCursorHookCommand(args: InstallHookArgs): Promise<void>;
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.installCursorHookCommand = installCursorHookCommand;
37
+ exports.uninstallCursorHookCommand = uninstallCursorHookCommand;
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const COLOR = {
42
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
43
+ red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
44
+ };
45
+ // Stable marker embedded in every command we write, so uninstall/re-install can
46
+ // find our entries regardless of the install path (npm global vs local dev).
47
+ const MANAGED_MARKER = '--fcd-managed true';
48
+ const MANAGED_TAG = 'fullcourtdefense';
49
+ const EVENT_MAP = {
50
+ prompt: { hookKey: 'beforeSubmitPrompt', flag: 'prompt', failClosedDefault: false },
51
+ shell: { hookKey: 'beforeShellExecution', flag: 'shell', failClosedDefault: true },
52
+ mcp: { hookKey: 'beforeMCPExecution', flag: 'mcp', failClosedDefault: true },
53
+ file: { hookKey: 'afterFileEdit', flag: 'file', failClosedDefault: false },
54
+ read: { hookKey: 'beforeReadFile', flag: 'read', failClosedDefault: false },
55
+ };
56
+ /** Build the absolute, shell-agnostic command that invokes this CLI's `hook`. */
57
+ function buildHookCommand(flag, opts) {
58
+ const nodeExe = process.execPath; // real node binary (handles Windows/Linux/mac)
59
+ const scriptPath = process.argv[1] || path.join(__dirname, '..', 'index.js');
60
+ const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
61
+ let cmd = `${q(nodeExe)} ${q(scriptPath)} hook --event ${flag}`;
62
+ if (opts.shadow)
63
+ cmd += ' --shadow true';
64
+ if (opts.failClosed)
65
+ cmd += ' --fail-closed true';
66
+ cmd += ` ${MANAGED_MARKER}`;
67
+ return cmd;
68
+ }
69
+ function hooksJsonPath(projectScope) {
70
+ return projectScope
71
+ ? path.join(process.cwd(), '.cursor', 'hooks.json')
72
+ : path.join(os.homedir(), '.cursor', 'hooks.json');
73
+ }
74
+ function readHooksJson(file) {
75
+ if (!fs.existsSync(file))
76
+ return { version: 1, hooks: {} };
77
+ try {
78
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
79
+ if (!parsed || typeof parsed !== 'object')
80
+ return { version: 1, hooks: {} };
81
+ parsed.version = parsed.version || 1;
82
+ parsed.hooks = parsed.hooks && typeof parsed.hooks === 'object' ? parsed.hooks : {};
83
+ return parsed;
84
+ }
85
+ catch {
86
+ return { version: 1, hooks: {} };
87
+ }
88
+ }
89
+ function isManaged(entry) {
90
+ return typeof entry?.command === 'string'
91
+ && (entry.command.includes(MANAGED_MARKER) || entry.command.includes(MANAGED_TAG));
92
+ }
93
+ /** Save the home-level Shield credentials so the hook can find them machine-wide. */
94
+ function writeHomeShield(input) {
95
+ if (!input.shieldId && !input.shieldKey && !input.apiUrl)
96
+ return undefined;
97
+ const target = path.join(os.homedir(), '.fullcourtdefense.yml');
98
+ const existing = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : '# FullCourtDefense CLI Configuration\n';
99
+ const lines = existing.split(/\r?\n/);
100
+ const setKey = (key, value) => {
101
+ if (!value)
102
+ return;
103
+ const idx = lines.findIndex((l) => new RegExp(`^\\s*${key}\\s*:`).test(l));
104
+ const next = `${key}: ${value}`;
105
+ if (idx >= 0)
106
+ lines[idx] = next;
107
+ else
108
+ lines.push(next);
109
+ };
110
+ setKey('shieldId', input.shieldId);
111
+ setKey('shieldKey', input.shieldKey);
112
+ setKey('apiUrl', input.apiUrl);
113
+ fs.writeFileSync(target, lines.filter((l, i, a) => !(l === '' && a[i - 1] === '')).join('\n').trim() + '\n', 'utf8');
114
+ return target;
115
+ }
116
+ async function installCursorHookCommand(args, config) {
117
+ const projectScope = args.project === 'true';
118
+ const shadow = args.shadow === 'true';
119
+ const file = hooksJsonPath(projectScope);
120
+ const requested = (args.events || 'prompt,shell,mcp')
121
+ .split(',').map((e) => e.trim().toLowerCase()).filter(Boolean);
122
+ const events = requested.filter((e) => EVENT_MAP[e]);
123
+ if (events.length === 0) {
124
+ console.error(`${COLOR.red}No valid events. Choose from: ${Object.keys(EVENT_MAP).join(', ')}${COLOR.reset}`);
125
+ process.exit(1);
126
+ }
127
+ // Persist creds machine-wide (home) so the runtime hook resolves them.
128
+ const shieldId = args.shieldId || config.shieldId;
129
+ const shieldKey = args.shieldKey || config.shieldKey;
130
+ const apiUrl = args.apiUrl || config.apiUrl;
131
+ const credFile = writeHomeShield({ shieldId, shieldKey, apiUrl });
132
+ const json = readHooksJson(file);
133
+ for (const e of events) {
134
+ const { hookKey, flag, failClosedDefault } = EVENT_MAP[e];
135
+ const failClosed = args.failClosed !== undefined ? args.failClosed === 'true' : failClosedDefault;
136
+ const entry = {
137
+ command: buildHookCommand(flag, { shadow, failClosed }),
138
+ timeout: 10,
139
+ };
140
+ if (failClosed)
141
+ entry.failClosed = true;
142
+ const list = Array.isArray(json.hooks[hookKey]) ? json.hooks[hookKey] : [];
143
+ // Replace any prior FullCourtDefense-managed entry; preserve the user's other hooks.
144
+ const preserved = list.filter((x) => !isManaged(x));
145
+ json.hooks[hookKey] = [...preserved, entry];
146
+ }
147
+ fs.mkdirSync(path.dirname(file), { recursive: true });
148
+ fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\n', 'utf8');
149
+ console.log('');
150
+ console.log(`${COLOR.green}${COLOR.bold}FullCourtDefense Cursor hook installed.${COLOR.reset}`);
151
+ console.log(`${COLOR.gray}Scope:${COLOR.reset} ${projectScope ? 'project (.cursor/hooks.json)' : 'machine-wide (~/.cursor/hooks.json)'}`);
152
+ console.log(`${COLOR.gray}File:${COLOR.reset} ${file}`);
153
+ console.log(`${COLOR.gray}Events:${COLOR.reset} ${events.map((e) => EVENT_MAP[e].hookKey).join(', ')}`);
154
+ if (shadow)
155
+ console.log(`${COLOR.yellow}Mode: SHADOW (monitor only — nothing is blocked).${COLOR.reset}`);
156
+ if (credFile)
157
+ console.log(`${COLOR.gray}Creds:${COLOR.reset} ${credFile}`);
158
+ if (!shieldId) {
159
+ console.log('');
160
+ console.log(`${COLOR.yellow}No Shield ID set.${COLOR.reset} Run ${COLOR.bold}fullcourtdefense configure${COLOR.reset} or pass ${COLOR.bold}--shield-id${COLOR.reset}, then re-run install.`);
161
+ }
162
+ console.log('');
163
+ console.log(`${COLOR.gray}Restart Cursor (or it will hot-reload hooks.json). Verify in Cursor → Settings → Hooks.${COLOR.reset}`);
164
+ console.log(`${COLOR.gray}Remove with:${COLOR.reset} fullcourtdefense uninstall-cursor-hook${projectScope ? ' --project true' : ''}`);
165
+ }
166
+ async function uninstallCursorHookCommand(args) {
167
+ const projectScope = args.project === 'true';
168
+ const file = hooksJsonPath(projectScope);
169
+ if (!fs.existsSync(file)) {
170
+ console.log(`${COLOR.yellow}No hooks.json found at ${file}. Nothing to remove.${COLOR.reset}`);
171
+ return;
172
+ }
173
+ const json = readHooksJson(file);
174
+ let removed = 0;
175
+ for (const key of Object.keys(json.hooks)) {
176
+ const list = Array.isArray(json.hooks[key]) ? json.hooks[key] : [];
177
+ const kept = list.filter((x) => !isManaged(x));
178
+ removed += list.length - kept.length;
179
+ if (kept.length)
180
+ json.hooks[key] = kept;
181
+ else
182
+ delete json.hooks[key];
183
+ }
184
+ fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\n', 'utf8');
185
+ console.log(`${COLOR.green}Removed ${removed} FullCourtDefense hook entr${removed === 1 ? 'y' : 'ies'} from ${file}.${COLOR.reset}`);
186
+ }
@@ -37,5 +37,8 @@ export interface LocalScanArgs {
37
37
  configShieldKey?: string;
38
38
  configApiUrl?: string;
39
39
  apiUrl?: string;
40
+ open?: string;
41
+ noOpen?: string;
42
+ openUi?: string;
40
43
  }
41
44
  export declare function localScanCommand(args: LocalScanArgs): Promise<void>;