fullcourtdefense-cli 1.5.3 → 1.6.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,10 @@
1
+ import { BotGuardConfig } from '../config';
2
+ import { ProtectAllArgs } from './mcpGateway';
3
+ export interface AutoProtectArgs extends ProtectAllArgs {
4
+ install?: string;
5
+ uninstall?: string;
6
+ status?: string;
7
+ intervalMinutes?: string;
8
+ onLogon?: string;
9
+ }
10
+ export declare function autoProtectCommand(args: AutoProtectArgs, config: BotGuardConfig): Promise<void>;
@@ -0,0 +1,150 @@
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.autoProtectCommand = autoProtectCommand;
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const child_process_1 = require("child_process");
41
+ const mcpGateway_1 = require("./mcpGateway");
42
+ const COLOR = {
43
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
44
+ red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
45
+ };
46
+ const TASK_NAME = 'FullCourtDefense Auto-Protect';
47
+ const CRON_MARKER = '# FCD_AUTO_PROTECT';
48
+ function cliEntry() {
49
+ return path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
50
+ }
51
+ function launcherDir() {
52
+ const base = process.platform === 'win32'
53
+ ? (process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'))
54
+ : path.join(os.homedir(), '.fullcourtdefense');
55
+ return path.join(base, 'FullCourtDefense');
56
+ }
57
+ /** On Windows, write a hidden VBS launcher so the scheduled run doesn't flash a console window. */
58
+ function ensureWindowsLauncher() {
59
+ const dir = launcherDir();
60
+ fs.mkdirSync(dir, { recursive: true });
61
+ const vbs = path.join(dir, 'auto-protect.vbs');
62
+ const node = process.execPath;
63
+ const cli = cliEntry();
64
+ // WScript.Shell.Run with window style 0 = hidden, wait = false.
65
+ const inner = `"${node}" "${cli}" protect-all`;
66
+ const content = `CreateObject("WScript.Shell").Run "cmd /c ${inner.replace(/"/g, '""')}", 0, False\n`;
67
+ fs.writeFileSync(vbs, content, 'utf8');
68
+ return vbs;
69
+ }
70
+ function installWindows(intervalMinutes, onLogon) {
71
+ const vbs = ensureWindowsLauncher();
72
+ const tr = `wscript.exe "${vbs}"`;
73
+ const baseArgs = ['/Create', '/TN', TASK_NAME, '/TR', tr, '/F', '/RL', 'LIMITED'];
74
+ const schedArgs = onLogon
75
+ ? ['/SC', 'ONLOGON']
76
+ : ['/SC', 'MINUTE', '/MO', String(intervalMinutes)];
77
+ const result = (0, child_process_1.spawnSync)('schtasks', [...baseArgs, ...schedArgs], { stdio: 'inherit' });
78
+ return result.status === 0;
79
+ }
80
+ function uninstallWindows() {
81
+ const result = (0, child_process_1.spawnSync)('schtasks', ['/Delete', '/TN', TASK_NAME, '/F'], { stdio: 'inherit' });
82
+ const vbs = path.join(launcherDir(), 'auto-protect.vbs');
83
+ try {
84
+ if (fs.existsSync(vbs))
85
+ fs.unlinkSync(vbs);
86
+ }
87
+ catch { /* ignore */ }
88
+ return result.status === 0;
89
+ }
90
+ function statusWindows() {
91
+ (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', TASK_NAME, '/V', '/FO', 'LIST'], { stdio: 'inherit' });
92
+ }
93
+ function readCrontab() {
94
+ const result = (0, child_process_1.spawnSync)('crontab', ['-l'], { encoding: 'utf8' });
95
+ return result.status === 0 ? (result.stdout || '') : '';
96
+ }
97
+ function writeCrontab(content) {
98
+ const result = (0, child_process_1.spawnSync)('crontab', ['-'], { input: content.endsWith('\n') ? content : `${content}\n`, encoding: 'utf8' });
99
+ return result.status === 0;
100
+ }
101
+ function installUnix(intervalMinutes, onLogon) {
102
+ const node = process.execPath;
103
+ const cli = cliEntry();
104
+ const command = `"${node}" "${cli}" protect-all >/dev/null 2>&1`;
105
+ const schedule = onLogon ? '@reboot' : `*/${Math.max(1, intervalMinutes)} * * * *`;
106
+ const line = `${schedule} ${command} ${CRON_MARKER}`;
107
+ const existing = readCrontab().split('\n').filter(l => l && !l.includes(CRON_MARKER));
108
+ return writeCrontab([...existing, line].join('\n'));
109
+ }
110
+ function uninstallUnix() {
111
+ const existing = readCrontab().split('\n').filter(l => l && !l.includes(CRON_MARKER));
112
+ return writeCrontab(existing.join('\n'));
113
+ }
114
+ async function autoProtectCommand(args, config) {
115
+ const intervalMinutes = Number(args.intervalMinutes) > 0 ? Math.floor(Number(args.intervalMinutes)) : 60;
116
+ const onLogon = args.onLogon === 'true';
117
+ if (args.uninstall === 'true') {
118
+ const ok = process.platform === 'win32' ? uninstallWindows() : uninstallUnix();
119
+ console.log(ok
120
+ ? `${COLOR.green}Removed the FullCourtDefense auto-protect schedule.${COLOR.reset}`
121
+ : `${COLOR.yellow}No auto-protect schedule found (or removal failed).${COLOR.reset}`);
122
+ return;
123
+ }
124
+ if (args.status === 'true') {
125
+ if (process.platform === 'win32')
126
+ statusWindows();
127
+ else
128
+ console.log(readCrontab().split('\n').filter(l => l.includes(CRON_MARKER)).join('\n') || 'No auto-protect cron entry found.');
129
+ return;
130
+ }
131
+ // Default action is install.
132
+ console.log(`\n${COLOR.bold}${COLOR.cyan}FullCourtDefense — schedule self-healing auto-protect${COLOR.reset}`);
133
+ console.log(`${COLOR.gray}Re-runs \`protect-all\` ${onLogon ? 'at every logon' : `every ${intervalMinutes} min`} so any server that loses the proxy is automatically re-wrapped.${COLOR.reset}\n`);
134
+ // Protect everything once right now, then register the recurring task.
135
+ await (0, mcpGateway_1.protectAllCommand)({ ...args, install: undefined, uninstall: undefined, status: undefined }, config);
136
+ const ok = process.platform === 'win32'
137
+ ? installWindows(intervalMinutes, onLogon)
138
+ : installUnix(intervalMinutes, onLogon);
139
+ console.log('');
140
+ if (ok) {
141
+ console.log(`${COLOR.green}${COLOR.bold}Auto-protect scheduled.${COLOR.reset}`);
142
+ console.log(`${COLOR.gray}Trigger:${COLOR.reset} ${onLogon ? 'at logon' : `every ${intervalMinutes} minutes`}`);
143
+ console.log(`${COLOR.gray}Task:${COLOR.reset} ${process.platform === 'win32' ? `Scheduled Task "${TASK_NAME}"` : 'crontab entry'}`);
144
+ console.log(`${COLOR.gray}Status:${COLOR.reset} fullcourtdefense auto-protect --status true`);
145
+ console.log(`${COLOR.gray}Remove:${COLOR.reset} fullcourtdefense auto-protect --uninstall true`);
146
+ }
147
+ else {
148
+ console.log(`${COLOR.red}Could not register the schedule.${COLOR.reset} On Windows you may need to run the terminal as Administrator, or use --on-logon true.`);
149
+ }
150
+ }
@@ -1279,33 +1279,48 @@ async function protectAllCommand(args, config) {
1279
1279
  : 'No MCP client config files found on this machine. Configure an MCP server in any client, then re-run.');
1280
1280
  return;
1281
1281
  }
1282
+ console.log(`\x1b[2mAuto-searching every known MCP client on this machine — ${files.length} config file(s) found.\x1b[0m\n`);
1282
1283
  let totalWrapped = 0;
1283
1284
  let totalManaged = 0;
1284
1285
  let totalRemote = 0;
1286
+ let totalEmpty = 0;
1287
+ const presentClientKeys = new Set();
1285
1288
  for (const file of files) {
1289
+ presentClientKeys.add(file.clientKey);
1286
1290
  const agentClient = CLIENT_KEY_TO_AGENT[file.clientKey] || 'cursor';
1287
1291
  const isToml = /\.toml$/i.test(file.path);
1288
1292
  const stats = isToml
1289
1293
  ? transformCodexToml(file.path, 'wrap', gatewayConfig, dryRun)
1290
1294
  : wrapJsonConfigFile(file.path, gatewayConfig, agentClient, dryRun);
1291
- if (!stats)
1292
- continue;
1293
- if (stats.wrapped.length === 0 && stats.skippedManaged.length === 0 && stats.skippedRemote.length === 0)
1295
+ console.log(`\x1b[1m${file.source}\x1b[0m \x1b[2m${file.path}\x1b[0m`);
1296
+ if (!stats) {
1297
+ console.log(' \x1b[33m• could not parse left unchanged\x1b[0m\n');
1294
1298
  continue;
1299
+ }
1300
+ const found = stats.wrapped.length + stats.skippedManaged.length + stats.skippedRemote.length;
1295
1301
  totalWrapped += stats.wrapped.length;
1296
1302
  totalManaged += stats.skippedManaged.length;
1297
1303
  totalRemote += stats.skippedRemote.length;
1298
- console.log(`\x1b[1m${file.source}\x1b[0m \x1b[2m${file.path}\x1b[0m`);
1299
1304
  if (stats.wrapped.length)
1300
- console.log(` \x1b[32m✓ wrapped:\x1b[0m ${stats.wrapped.join(', ')}`);
1305
+ console.log(` \x1b[32m✓ ${dryRun ? 'would update' : 'updated'} → now protected:\x1b[0m ${stats.wrapped.join(', ')}`);
1301
1306
  if (stats.skippedManaged.length)
1302
- console.log(` \x1b[2m• already protected:\x1b[0m ${stats.skippedManaged.join(', ')}`);
1307
+ console.log(` \x1b[2m• already protected (unchanged):\x1b[0m ${stats.skippedManaged.join(', ')}`);
1303
1308
  if (stats.skippedRemote.length)
1304
1309
  console.log(` \x1b[33m• skipped (remote/no command):\x1b[0m ${stats.skippedRemote.join(', ')}`);
1310
+ if (found === 0) {
1311
+ totalEmpty++;
1312
+ console.log(' \x1b[2m• no MCP servers configured — nothing to protect\x1b[0m');
1313
+ }
1305
1314
  console.log('');
1306
1315
  }
1316
+ // Show leading tools that have NO config file at all, so it's clear they were searched.
1317
+ const consideredClients = exports.ALL_GATEWAY_CLIENTS.filter(c => !allowedClientKeys || (AGENT_TO_CLIENT_KEYS[c] || []).some(k => allowedClientKeys.has(k)));
1318
+ const notConfigured = consideredClients.filter(c => !(AGENT_TO_CLIENT_KEYS[c] || []).some(k => presentClientKeys.has(k)));
1319
+ if (notConfigured.length) {
1320
+ console.log(`\x1b[2mNot configured (no config file found): ${notConfigured.map(c => CLIENT_LABELS[c]).join(', ')}\x1b[0m\n`);
1321
+ }
1307
1322
  console.log('\x1b[1mSummary\x1b[0m');
1308
- console.log(` ${dryRun ? 'would wrap' : 'wrapped'}: ${totalWrapped} already protected: ${totalManaged} skipped remote: ${totalRemote}`);
1323
+ console.log(` ${dryRun ? 'would update' : 'updated'}: ${totalWrapped} already protected: ${totalManaged} no servers: ${totalEmpty} skipped remote: ${totalRemote}`);
1309
1324
  console.log(` Each protected server enforces your org Action Policies (allow / block / wait-for-human-approval) on its tool calls.`);
1310
1325
  if (!dryRun && totalWrapped > 0)
1311
1326
  console.log(' Backups saved next to each edited file (*.fcd-backup-*). Restart each client to load the protected servers.');
package/dist/index.js CHANGED
@@ -45,6 +45,7 @@ const hook_1 = require("./commands/hook");
45
45
  const installCursorHook_1 = require("./commands/installCursorHook");
46
46
  const mcpGateway_1 = require("./commands/mcpGateway");
47
47
  const installAll_1 = require("./commands/installAll");
48
+ const autoProtect_1 = require("./commands/autoProtect");
48
49
  const fs = __importStar(require("fs"));
49
50
  const path = __importStar(require("path"));
50
51
  function readCliVersion() {
@@ -133,6 +134,10 @@ function printHelp() {
133
134
  unprotect-all
134
135
  Reverse protect-all: restore every server to its original command.
135
136
  Also supports --clients to target one tool, and --dry-run.
137
+ auto-protect
138
+ Self-healing: schedule protect-all to re-run automatically so any
139
+ server that loses the proxy is re-wrapped. --interval-minutes 60
140
+ (default) or --on-logon true. --uninstall true / --status true.
136
141
  install-mcp-gateway
137
142
  Install MCP gateway into selected clients (--clients all for every tool).
138
143
  install-cursor-mcp-gateway
@@ -530,6 +535,21 @@ async function main() {
530
535
  await (0, mcpGateway_1.unprotectAllCommand)(args, config);
531
536
  break;
532
537
  }
538
+ case 'auto-protect': {
539
+ const args = {
540
+ ...buildGatewayArgs(),
541
+ clients: flags.clients,
542
+ config: flags.config,
543
+ dryRun: flags['dry-run'],
544
+ install: flags.install,
545
+ uninstall: flags.uninstall,
546
+ status: flags.status,
547
+ intervalMinutes: flags['interval-minutes'],
548
+ onLogon: flags['on-logon'],
549
+ };
550
+ await (0, autoProtect_1.autoProtectCommand)(args, config);
551
+ break;
552
+ }
533
553
  case 'install-cursor-mcp-gateway': {
534
554
  const args = {
535
555
  ...buildGatewayArgs(),
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.5.3"
2
+ "version": "1.6.0"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {