fullcourtdefense-cli 1.21.12 → 1.21.13

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,8 @@
1
+ /** Whether the Cursor IDE appears installed (app binaries or app data, not our hook file). */
2
+ export declare function isCursorAppInstalled(): boolean;
3
+ /**
4
+ * Whether any client of the Claude-format hook family appears installed:
5
+ * Claude Code, VS Code (Copilot agent mode), or GitHub Copilot CLI — one
6
+ * `~/.claude/settings.json` hook file covers all three.
7
+ */
8
+ export declare function isClaudeFamilyAppInstalled(): boolean;
@@ -0,0 +1,107 @@
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.isCursorAppInstalled = isCursorAppInstalled;
37
+ exports.isClaudeFamilyAppInstalled = isClaudeFamilyAppInstalled;
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ /**
42
+ * Best-effort detection of whether the AI clients we hook are actually
43
+ * installed on this machine. Used to decide whether a missing hook is a
44
+ * protection FAILURE (app present, hook gone) or merely informational
45
+ * (nothing to hook — e.g. a VS Code-only machine without Claude).
46
+ *
47
+ * Deliberately does NOT use our own config artifacts (`~/.cursor/hooks.json`,
48
+ * `~/.claude/settings.json`) as evidence — the hook installers create those
49
+ * directories themselves, which would make every machine look like it has
50
+ * every app.
51
+ */
52
+ function anyExists(candidates) {
53
+ for (const candidate of candidates) {
54
+ try {
55
+ if (fs.existsSync(candidate))
56
+ return true;
57
+ }
58
+ catch { /* unreadable path — treat as absent */ }
59
+ }
60
+ return false;
61
+ }
62
+ /** Whether the Cursor IDE appears installed (app binaries or app data, not our hook file). */
63
+ function isCursorAppInstalled() {
64
+ const home = os.homedir();
65
+ const candidates = [
66
+ // Populated by the app itself (extensions, chats, state) — not by our installer.
67
+ path.join(home, '.cursor', 'extensions'),
68
+ ];
69
+ if (process.platform === 'win32') {
70
+ if (process.env.LOCALAPPDATA)
71
+ candidates.push(path.join(process.env.LOCALAPPDATA, 'Programs', 'cursor'));
72
+ if (process.env.APPDATA)
73
+ candidates.push(path.join(process.env.APPDATA, 'Cursor'));
74
+ }
75
+ else if (process.platform === 'darwin') {
76
+ candidates.push('/Applications/Cursor.app');
77
+ candidates.push(path.join(home, 'Library', 'Application Support', 'Cursor'));
78
+ }
79
+ else {
80
+ candidates.push(path.join(home, '.config', 'Cursor'));
81
+ }
82
+ return anyExists(candidates);
83
+ }
84
+ /**
85
+ * Whether any client of the Claude-format hook family appears installed:
86
+ * Claude Code, VS Code (Copilot agent mode), or GitHub Copilot CLI — one
87
+ * `~/.claude/settings.json` hook file covers all three.
88
+ */
89
+ function isClaudeFamilyAppInstalled() {
90
+ const home = os.homedir();
91
+ const candidates = [
92
+ path.join(home, '.claude.json'), // Claude Code state file
93
+ path.join(home, '.claude', 'projects'), // Claude Code session data
94
+ path.join(home, '.copilot'), // GitHub Copilot CLI
95
+ ];
96
+ if (process.platform === 'win32') {
97
+ if (process.env.APPDATA)
98
+ candidates.push(path.join(process.env.APPDATA, 'Code'));
99
+ }
100
+ else if (process.platform === 'darwin') {
101
+ candidates.push(path.join(home, 'Library', 'Application Support', 'Code'));
102
+ }
103
+ else {
104
+ candidates.push(path.join(home, '.config', 'Code'));
105
+ }
106
+ return anyExists(candidates);
107
+ }
@@ -13,7 +13,7 @@ async function doctorCommand(args, config) {
13
13
  const pingUrl = `${apiUrl}/api/health/ping`;
14
14
  const rootUrl = `${apiUrl}/`;
15
15
  console.log('');
16
- console.log('BotGuard outbound diagnostic');
16
+ console.log('FullCourtDefense outbound diagnostic');
17
17
  console.log(`Target: ${apiUrl}`);
18
18
  console.log('');
19
19
  const started = Date.now();
@@ -46,6 +46,13 @@ const localSafetySnapshot_1 = require("../localSafetySnapshot");
46
46
  const taintLedger_1 = require("./taintLedger");
47
47
  const notify_1 = require("../notify");
48
48
  const DEBUG_LOG = path.join(os.homedir(), '.fullcourtdefense-hook.log');
49
+ /**
50
+ * Verbose diagnostics (FCD_HOOK_DEBUG=1) additionally log content previews
51
+ * (raw payload + prompt text snippets). Default logging keeps operational
52
+ * metadata only — user prompt content must never sit in a plaintext file on
53
+ * a customer laptop unless support explicitly opts in.
54
+ */
55
+ const HOOK_DEBUG_VERBOSE = process.env.FCD_HOOK_DEBUG === '1';
49
56
  /** Append a diagnostic line so we can see exactly what Cursor invoked + the verdict. */
50
57
  function dbg(obj) {
51
58
  try {
@@ -552,7 +559,7 @@ async function hookCommand(args, config) {
552
559
  event = inferEvent(args.event, payload);
553
560
  }
554
561
  dbg({ phase: 'invoke', event, argEvent: args.event, format: hookFormat, client, pid: process.pid,
555
- isTTY, rawLen: raw.length, rawPreview: raw.slice(0, 300), stdinErr, parseErr,
562
+ isTTY, rawLen: raw.length, ...(HOOK_DEBUG_VERBOSE ? { rawPreview: raw.slice(0, 300) } : {}), stdinErr, parseErr,
556
563
  argv: process.argv.slice(2), payloadKeys: Object.keys(payload) });
557
564
  // Emit the verdict in the shape THIS client expects, then exit.
558
565
  // Cursor: beforeSubmitPrompt blocks via { continue: false }; execution hooks
@@ -644,7 +651,7 @@ async function hookCommand(args, config) {
644
651
  // --- Shield text analysis for prompts (developer chat) ---
645
652
  if (event === 'prompt') {
646
653
  const text = extractPromptText(payload).trim();
647
- dbg({ phase: 'prompt_text', textLen: text.length, textPreview: text.slice(0, 300) });
654
+ dbg({ phase: 'prompt_text', textLen: text.length, ...(HOOK_DEBUG_VERBOSE ? { textPreview: text.slice(0, 300) } : {}) });
648
655
  if (!text)
649
656
  respond(false);
650
657
  // Record the developer's prompt so taint-tracking can later tell whether a
@@ -50,6 +50,7 @@ const posixShellGuard_1 = require("./posixShellGuard");
50
50
  const notify_1 = require("../notify");
51
51
  const daemon_1 = require("./daemon");
52
52
  const machineIdentity_1 = require("../machineIdentity");
53
+ const appDetection_1 = require("../appDetection");
53
54
  const integrity_1 = require("../integrity");
54
55
  const telemetry_1 = require("../telemetry");
55
56
  const onboardingJournal_1 = require("./onboardingJournal");
@@ -123,12 +124,24 @@ function checkCursorHooks() {
123
124
  const hooksFile = path.join(os.homedir(), '.cursor', 'hooks.json');
124
125
  const text = readTextSafe(hooksFile);
125
126
  const ok = text.includes('fcd-managed') || /fullcourtdefense|botguard/i.test(text);
127
+ if (!ok && !(0, appDetection_1.isCursorAppInstalled)()) {
128
+ // Nothing to hook — a machine without Cursor must not fail onboarding.
129
+ return { label: 'Cursor runtime hooks', ok: true, optional: true, detail: 'Cursor is not installed on this machine — hooks install automatically when it is' };
130
+ }
126
131
  return { label: 'Cursor runtime hooks', ok, detail: ok ? hooksFile : 'not installed (install-cursor-hook)' };
127
132
  }
128
133
  function checkClaudeHooks() {
129
134
  const settingsFile = path.join(os.homedir(), '.claude', 'settings.json');
130
135
  const text = readTextSafe(settingsFile);
131
136
  const ok = /fullcourtdefense|fcd-managed|botguard/i.test(text);
137
+ if (!ok && !(0, appDetection_1.isClaudeFamilyAppInstalled)()) {
138
+ return {
139
+ label: 'Claude Code / VS Code / Copilot CLI hooks',
140
+ ok: true,
141
+ optional: true,
142
+ detail: 'no Claude Code / VS Code / Copilot CLI found on this machine — hooks install automatically when one is',
143
+ };
144
+ }
132
145
  return {
133
146
  label: 'Claude Code / VS Code / Copilot CLI hooks',
134
147
  ok,
@@ -213,6 +226,17 @@ async function onboardCommand(args, config) {
213
226
  };
214
227
  console.log(`\n${BOLD}\x1b[36mFullCourtDefense — machine onboarding${RESET}`);
215
228
  console.log(`${DIM}Connectivity -> enrollment -> protection -> verification. One command.${RESET}\n`);
229
+ // Onboarding is a persisted transaction: completed steps are reused on any
230
+ // re-run, so `--resume` is an explicit alias that also surfaces the journal.
231
+ const resume = args.resume === 'true' || args.resume === '';
232
+ if (resume) {
233
+ const previouslyCompleted = Object.entries(journal.steps)
234
+ .filter(([, step]) => step.status === 'completed')
235
+ .map(([name]) => name);
236
+ console.log(previouslyCompleted.length > 0
237
+ ? `${DIM}Resuming onboarding — reusing completed steps: ${previouslyCompleted.join(', ')} (journal: ${journalFile}).${RESET}\n`
238
+ : `${DIM}Resuming onboarding — no completed steps found in the journal; running from the start.${RESET}\n`);
239
+ }
216
240
  const creds = (0, config_1.resolveCliCredentials)(config, { apiUrl: args.apiUrl });
217
241
  const apiUrl = creds.apiUrl;
218
242
  const token = (args.token || process.env.FCD_ENROLL_TOKEN || '').trim();
@@ -419,7 +443,7 @@ async function onboardCommand(args, config) {
419
443
  const dashboard = (0, notify_1.consoleUrl)('/agent-security/users?view=desktop');
420
444
  console.log('');
421
445
  if (requiredFailures.length === 0) {
422
- console.log(`${GREEN}${BOLD}This machine is protected.${RESET} It reports to your org's AI Fleet and enforces your policies (dangerous actions are blocked). An admin can switch it to monitor-only from the dashboard.`);
446
+ console.log(`${GREEN}${BOLD}This machine is protected.${RESET} It reports to your org's AI Fleet and enforces your policies in blocking mode — dangerous actions are stopped, not just logged.`);
423
447
  console.log(`${DIM}Fleet view: ${dashboard}${RESET}`);
424
448
  console.log(`${DIM}Restart your AI clients (Cursor, Claude, VS Code, …) so they pick up the wrapped configs.${RESET}`);
425
449
  mark('verification', 'completed');
@@ -0,0 +1,18 @@
1
+ import { BotGuardConfig } from '../config';
2
+ import { ProtectAllArgs } from './mcpGateway';
3
+ export interface UninstallAllArgs extends ProtectAllArgs {
4
+ /** 'true' => also delete local state/config files (~/.fullcourtdefense*). */
5
+ purge?: string;
6
+ }
7
+ /**
8
+ * `fullcourtdefense uninstall` — ONE command that reverses `onboard`/`install-all`:
9
+ * daemon autostart, self-heal schedule, MCP gateway wraps, IDE hooks, terminal
10
+ * guards, and the daily discovery schedule. Each step is best-effort so a
11
+ * single failure never leaves the rest of the machine half-uninstalled.
12
+ *
13
+ * Machine credentials (~/.fullcourtdefense.yml) and local state are KEPT by
14
+ * default so the machine can re-onboard without a new enrollment token; pass
15
+ * `--purge true` to delete them too. The fleet record in the dashboard is not
16
+ * touched — deprovision it from the console (Machines → machine → Deprovision).
17
+ */
18
+ export declare function uninstallAllCommand(args: UninstallAllArgs, config: BotGuardConfig): Promise<void>;
@@ -0,0 +1,132 @@
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.uninstallAllCommand = uninstallAllCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const daemon_1 = require("./daemon");
41
+ const autoProtect_1 = require("./autoProtect");
42
+ const mcpGateway_1 = require("./mcpGateway");
43
+ const discoverSchedule_1 = require("./discoverSchedule");
44
+ const installCursorHook_1 = require("./installCursorHook");
45
+ const installClaudeHook_1 = require("./installClaudeHook");
46
+ const shellGuard_1 = require("./shellGuard");
47
+ const posixShellGuard_1 = require("./posixShellGuard");
48
+ const cmdGuard_1 = require("./cmdGuard");
49
+ const BOLD = '\x1b[1m';
50
+ const GREEN = '\x1b[32m';
51
+ const YELLOW = '\x1b[33m';
52
+ const DIM = '\x1b[2m';
53
+ const RESET = '\x1b[0m';
54
+ /**
55
+ * `fullcourtdefense uninstall` — ONE command that reverses `onboard`/`install-all`:
56
+ * daemon autostart, self-heal schedule, MCP gateway wraps, IDE hooks, terminal
57
+ * guards, and the daily discovery schedule. Each step is best-effort so a
58
+ * single failure never leaves the rest of the machine half-uninstalled.
59
+ *
60
+ * Machine credentials (~/.fullcourtdefense.yml) and local state are KEPT by
61
+ * default so the machine can re-onboard without a new enrollment token; pass
62
+ * `--purge true` to delete them too. The fleet record in the dashboard is not
63
+ * touched — deprovision it from the console (Machines → machine → Deprovision).
64
+ */
65
+ async function uninstallAllCommand(args, config) {
66
+ console.log(`\n${BOLD}\x1b[36mFullCourtDefense — uninstall local protection${RESET}`);
67
+ console.log(`${DIM}Daemon -> self-heal schedule -> MCP gateways -> IDE hooks -> terminal guards -> discovery schedule.${RESET}\n`);
68
+ const steps = [
69
+ { label: 'Resident daemon + autostart', run: () => (0, daemon_1.daemonCommand)({ ...args, uninstall: 'true', quiet: 'true' }, config) },
70
+ { label: 'Self-healing protect schedule', run: () => (0, autoProtect_1.autoProtectCommand)({ ...args, uninstall: 'true' }, config) },
71
+ { label: 'MCP gateway wraps (restore original server commands)', run: () => (0, mcpGateway_1.unprotectAllCommand)({ ...args, clients: args.clients || 'all' }, config) },
72
+ { label: 'Cursor runtime hooks', run: () => (0, installCursorHook_1.uninstallCursorHookCommand)({}) },
73
+ { label: 'Claude Code / VS Code / Copilot CLI hooks', run: () => (0, installClaudeHook_1.uninstallClaudeHookCommand)({}) },
74
+ ...(process.platform === 'win32'
75
+ ? [
76
+ { label: 'PowerShell shell guard', run: () => (0, shellGuard_1.uninstallShellGuardCommand)() },
77
+ { label: 'cmd.exe shell guard', run: () => (0, cmdGuard_1.uninstallCmdGuardCommand)() },
78
+ ]
79
+ : [
80
+ { label: 'bash/zsh shell guard', run: () => (0, posixShellGuard_1.uninstallPosixShellGuardCommand)() },
81
+ ]),
82
+ { label: 'Daily discovery schedule', run: () => (0, discoverSchedule_1.uninstallDailyDiscoverSchedule)() },
83
+ ];
84
+ const failures = [];
85
+ for (const step of steps) {
86
+ console.log(`${BOLD}› ${step.label}${RESET}`);
87
+ try {
88
+ await step.run();
89
+ }
90
+ catch (error) {
91
+ failures.push(step.label);
92
+ console.log(`${YELLOW} ⚠ ${step.label} failed: ${error instanceof Error ? error.message : String(error)} — continuing.${RESET}`);
93
+ }
94
+ console.log('');
95
+ }
96
+ if (args.purge === 'true') {
97
+ console.log(`${BOLD}› Local state files${RESET}`);
98
+ const home = os.homedir();
99
+ const targets = [
100
+ path.join(home, '.fullcourtdefense.yml'),
101
+ path.join(home, '.fullcourtdefense-spool.jsonl'),
102
+ path.join(home, '.fullcourtdefense-runtime.json'),
103
+ path.join(home, '.fullcourtdefense-hook.log'),
104
+ path.join(home, '.fullcourtdefense'),
105
+ ];
106
+ for (const target of targets) {
107
+ try {
108
+ if (!fs.existsSync(target))
109
+ continue;
110
+ fs.rmSync(target, { recursive: true, force: true });
111
+ console.log(`${DIM} removed ${target}${RESET}`);
112
+ }
113
+ catch (error) {
114
+ failures.push(target);
115
+ console.log(`${YELLOW} ⚠ could not remove ${target}: ${error instanceof Error ? error.message : String(error)}${RESET}`);
116
+ }
117
+ }
118
+ console.log('');
119
+ }
120
+ if (failures.length === 0) {
121
+ console.log(`${GREEN}${BOLD}Local protection removed.${RESET}`);
122
+ if (args.purge !== 'true') {
123
+ console.log(`${DIM}Machine credentials were kept (re-run \`fullcourtdefense onboard\` to re-protect). Use --purge true to delete all local state.${RESET}`);
124
+ }
125
+ console.log(`${DIM}The machine still appears in the fleet console until an admin deprovisions it (Machines → machine → Deprovision).${RESET}`);
126
+ console.log(`${DIM}Restart your AI clients (Cursor, Claude, VS Code, …) so they reload the restored configs.${RESET}`);
127
+ }
128
+ else {
129
+ console.log(`${YELLOW}${BOLD}Uninstall finished with ${failures.length} step(s) needing attention:${RESET} ${failures.join(', ')}`);
130
+ process.exitCode = 1;
131
+ }
132
+ }
package/dist/index.js CHANGED
@@ -50,6 +50,7 @@ const installClaudeHook_1 = require("./commands/installClaudeHook");
50
50
  const mcpGateway_1 = require("./commands/mcpGateway");
51
51
  const installAll_1 = require("./commands/installAll");
52
52
  const onboard_1 = require("./commands/onboard");
53
+ const uninstallAll_1 = require("./commands/uninstallAll");
53
54
  const autoProtect_1 = require("./commands/autoProtect");
54
55
  const daemon_1 = require("./commands/daemon");
55
56
  const desktopChatGuard_1 = require("./commands/desktopChatGuard");
@@ -129,6 +130,14 @@ function printHelp() {
129
130
  connectivity check -> enroll (--token <fleet-token> or
130
131
  FCD_ENROLL_TOKEN) -> install-all -> verification checklist.
131
132
  Exits non-zero if a required protection surface failed.
133
+ Safe to re-run: completed steps are reused automatically
134
+ (--repair true re-runs them; --dry-run true previews;
135
+ --json true prints the transaction as JSON).
136
+ uninstall ONE command that reverses onboard/install-all on this machine:
137
+ daemon, self-heal schedule, MCP gateway wraps, IDE hooks,
138
+ terminal guards, and the daily discovery schedule. Keeps
139
+ machine credentials for easy re-onboarding; --purge true
140
+ deletes all local state too.
132
141
  doctor First step. Checks outbound HTTPS access to FullCourtDefense.
133
142
  login Enrolls this machine with a fleet enrollment token and saves
134
143
  per-machine Shield credentials to ~/.fullcourtdefense.yml.
@@ -203,10 +212,15 @@ function printHelp() {
203
212
  install-vscode-mcp-gateway
204
213
  VS Code only — .vscode/mcp.json or user settings.
205
214
  mcp-gateway
206
- Runs a local MCP gateway that checks AgentGuard runtime/action
215
+ Runs a local MCP gateway that checks FullCourtDefense runtime/action
207
216
  policies before forwarding tool calls to a real MCP server.
208
217
  hook Internal: invoked by Cursor per agent event (reads stdin JSON,
209
218
  returns an allow/deny verdict). Not run by hand.
219
+ desktop-chat-guard
220
+ Windows-only advisory guard for Claude Desktop's native chat
221
+ (clipboard/secret warnings). Usually started by the daemon.
222
+ flush-spool Manually drain locally spooled telemetry events to the backend
223
+ (they flush automatically; useful after long offline periods).
210
224
  credits Shows hosted scan credits for CI/CD API-key scans.
211
225
  init Creates a starter .fullcourtdefense.yml config file.
212
226
 
@@ -300,7 +314,7 @@ function printHelp() {
300
314
  --fail-on <severity> agent-ci fail threshold: critical, high, medium, low
301
315
  --require-gateway agent-ci requires risky MCP servers to use FCD gateway
302
316
  --require-approval-for agent-ci risky controls list, e.g. payment,delete,shell,secrets
303
- --require-upload agent-ci fails if evidence cannot upload to AgentGuard UI
317
+ --require-upload agent-ci fails if evidence cannot upload to the FullCourtDefense UI
304
318
  --changed-only agent-ci limits findings to changed files
305
319
  --changed-files <path> file or comma/newline list of changed files for agent-ci
306
320
  --method <GET|POST> Local endpoint method
@@ -362,6 +376,8 @@ function printHelp() {
362
376
  $ fullcourtdefense install-all --auto-protect true --upload
363
377
  $ fullcourtdefense onboard --token <fleet-enrollment-token> --resume
364
378
  $ fullcourtdefense onboard --dry-run true --json true
379
+ $ fullcourtdefense uninstall
380
+ $ fullcourtdefense uninstall --purge true
365
381
  $ fullcourtdefense discover --surface all --upload --schedule logon
366
382
  $ fullcourtdefense install-mcp-gateway --clients cursor,codex --mcp-command npm --mcp-args "run mcp"
367
383
  $ fullcourtdefense install-codex-mcp-gateway --mcp-command npm --mcp-args "run mcp"
@@ -645,6 +661,17 @@ async function main() {
645
661
  await (0, installAll_1.installAllCommand)(args, config);
646
662
  break;
647
663
  }
664
+ case 'uninstall':
665
+ case 'uninstall-all': {
666
+ const args = {
667
+ ...buildGatewayArgs(),
668
+ clients: flags.clients,
669
+ config: flags.config,
670
+ purge: flags.purge,
671
+ };
672
+ await (0, uninstallAll_1.uninstallAllCommand)(args, config);
673
+ break;
674
+ }
648
675
  case 'onboard': {
649
676
  const args = {
650
677
  ...buildInstallArgs(),
package/dist/integrity.js CHANGED
@@ -41,6 +41,7 @@ const path = __importStar(require("path"));
41
41
  const installClaudeHook_1 = require("./commands/installClaudeHook");
42
42
  const installCursorHook_1 = require("./commands/installCursorHook");
43
43
  const mcpGateway_1 = require("./commands/mcpGateway");
44
+ const appDetection_1 = require("./appDetection");
44
45
  function readText(file) {
45
46
  try {
46
47
  return fs.readFileSync(file, 'utf8');
@@ -93,14 +94,22 @@ function getLocalIntegrityReport(options = {}) {
93
94
  }
94
95
  if (protectedMcpConfigs < discoveredMcpConfigs)
95
96
  reasons.push('mcp_gateway_missing');
97
+ // A missing hook only counts as damage when the app it protects is actually
98
+ // installed — a VS Code-only machine without Cursor (or vice versa) must not
99
+ // report broken integrity forever. Stale-path entries remain failures either
100
+ // way: the hook file exists but points at a dead CLI path.
96
101
  const cursorHooks = (0, installCursorHook_1.inspectCursorManagedHooks)();
97
- if (!cursorHooks.existed || cursorHooks.managedEntries === 0)
98
- reasons.push('cursor_hook_missing');
102
+ if (!cursorHooks.existed || cursorHooks.managedEntries === 0) {
103
+ if ((0, appDetection_1.isCursorAppInstalled)())
104
+ reasons.push('cursor_hook_missing');
105
+ }
99
106
  else if (cursorHooks.staleEntries > 0)
100
107
  reasons.push('cursor_hook_stale_path');
101
108
  const claudeHooks = (0, installClaudeHook_1.inspectClaudeManagedHooks)();
102
- if (!claudeHooks.existed || claudeHooks.managedEntries === 0)
103
- reasons.push('claude_hook_missing');
109
+ if (!claudeHooks.existed || claudeHooks.managedEntries === 0) {
110
+ if ((0, appDetection_1.isClaudeFamilyAppInstalled)())
111
+ reasons.push('claude_hook_missing');
112
+ }
104
113
  else if (claudeHooks.staleEntries > 0)
105
114
  reasons.push('claude_hook_stale_path');
106
115
  const daemonRunning = isDaemonRunning();
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.21.12"
2
+ "version": "1.21.13"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.21.12",
3
+ "version": "1.21.13",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {