fullcourtdefense-cli 1.1.17 → 1.2.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.
package/README.md CHANGED
@@ -53,8 +53,41 @@ fullcourtdefense init
53
53
  - `fullcourtdefense install-claude-code-mcp-gateway ...` — registers the protected MCP gateway in Claude Code.
54
54
  - `fullcourtdefense install-claude-desktop-mcp-gateway ...` — registers the protected MCP gateway in Claude Desktop.
55
55
  - `fullcourtdefense credits` — checks hosted scan credits for CI/CD API-key scans.
56
+ - `fullcourtdefense discover` — scans local MCP client configs and reports risk; `--upload` sends desktop inventory to AI Inventory.
56
57
  - `fullcourtdefense init` — creates a starter config file.
57
58
 
59
+ ## Shadow AI / Desktop Discovery
60
+
61
+ Find MCP servers configured on developer machines without reading chat history or spawning stdio servers by default:
62
+
63
+ ```bash
64
+ # Scan this machine and print results
65
+ fullcourtdefense discover
66
+
67
+ # JSON output (includes host fingerprint + servers)
68
+ fullcourtdefense discover --json
69
+
70
+ # Upload to AI Inventory (requires CI/CD API key with audit:write)
71
+ fullcourtdefense discover --upload --api-key YOUR_KEY
72
+
73
+ # Live-probe HTTP/SSE MCP servers for tools/list (stdio stays config-only)
74
+ fullcourtdefense discover --deep
75
+
76
+ # Fleet / MDM script — silent upload with user attribution
77
+ fullcourtdefense discover --upload --silent --user-email you@company.com --api-key YOUR_KEY
78
+
79
+ # Daily automatic scan on this laptop (uses API key from env or ~/.fullcourtdefense.yml)
80
+ fullcourtdefense discover --schedule daily --user-email you@company.com
81
+
82
+ # Remove daily schedule
83
+ fullcourtdefense discover --unschedule true
84
+
85
+ # Scan an extra config path
86
+ fullcourtdefense discover --path /path/to/mcp.json
87
+ ```
88
+
89
+ Each upload includes host metadata (`machineId`, `hostname`, `platform`, `user`) so the same MCP server on different laptops appears as separate desktop inventory rows. The CLI is also available as `fcd` or `botguard` after global install.
90
+
58
91
  ## Config File
59
92
 
60
93
  Create a `.fullcourtdefense.yml` to avoid passing flags every time:
@@ -7,5 +7,19 @@ export interface DiscoverArgs {
7
7
  upload?: string;
8
8
  connectorName?: string;
9
9
  extraPath?: string;
10
+ deep?: string;
11
+ silent?: string;
12
+ userEmail?: string;
13
+ schedule?: string;
14
+ unschedule?: string;
15
+ scheduleHour?: string;
16
+ }
17
+ export interface DesktopDiscoveryHost {
18
+ machineId: string;
19
+ hostname: string;
20
+ platform: string;
21
+ user: string;
22
+ scannedAt: string;
23
+ probeMode: 'config' | 'deep';
10
24
  }
11
25
  export declare function discoverCommand(args: DiscoverArgs, config: BotGuardConfig): Promise<void>;
@@ -34,12 +34,30 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.discoverCommand = discoverCommand;
37
+ const crypto = __importStar(require("crypto"));
37
38
  const fs = __importStar(require("fs"));
38
39
  const os = __importStar(require("os"));
39
40
  const path = __importStar(require("path"));
41
+ const discoverProxy_1 = require("./discoverProxy");
42
+ const discoverSchedule_1 = require("./discoverSchedule");
40
43
  const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
41
44
  const SECRET_VALUE = /sk-[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{20,}/;
42
45
  const SECRET_KEY = /key|token|secret|password|credential|api[_-]?key/i;
46
+ function machineFingerprint() {
47
+ const seed = [os.hostname(), os.userInfo().username, os.platform(), os.arch()].join('|');
48
+ return crypto.createHash('sha256').update(seed).digest('hex').slice(0, 16);
49
+ }
50
+ function buildHostMetadata(userEmail, probeMode = 'config') {
51
+ const info = os.userInfo();
52
+ return {
53
+ machineId: machineFingerprint(),
54
+ hostname: os.hostname(),
55
+ platform: os.platform(),
56
+ user: userEmail || info.username,
57
+ scannedAt: new Date().toISOString(),
58
+ probeMode,
59
+ };
60
+ }
43
61
  /** Where MCP client configs live, by client + OS. */
44
62
  function candidateConfigPaths(cwd, extra) {
45
63
  const home = os.homedir();
@@ -50,10 +68,25 @@ function candidateConfigPaths(cwd, extra) {
50
68
  // Cursor
51
69
  { path: path.join(home, '.cursor', 'mcp.json'), source: 'Cursor (global)' },
52
70
  { path: path.join(cwd, '.cursor', 'mcp.json'), source: 'Cursor (project)' },
71
+ // Cursor managed (enterprise)
72
+ { path: path.join(appData, 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, win)' },
73
+ { path: path.join(home, 'Library', 'Application Support', 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, mac)' },
74
+ { path: '/etc/cursor/managed-mcp.json', source: 'Cursor (managed, linux)' },
53
75
  // Claude Desktop
54
76
  { path: path.join(appData, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (win)' },
55
77
  { path: path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (mac)' },
56
78
  { path: path.join(xdg, 'Claude', 'claude_desktop_config.json'), source: 'Claude Desktop (linux)' },
79
+ // Claude Code
80
+ { path: path.join(home, '.claude.json'), source: 'Claude Code' },
81
+ { path: path.join(home, '.claude', 'settings.json'), source: 'Claude Code (global)' },
82
+ { path: path.join(cwd, '.claude', 'settings.json'), source: 'Claude Code (project)' },
83
+ { path: path.join(cwd, '.mcp.json'), source: 'Claude Code (.mcp.json project)' },
84
+ // Gemini CLI
85
+ { path: path.join(home, '.gemini', 'settings.json'), source: 'Gemini CLI' },
86
+ { path: path.join(xdg, 'gemini', 'settings.json'), source: 'Gemini CLI (xdg)' },
87
+ // OpenAI Codex
88
+ { path: path.join(home, '.codex', 'mcp.json'), source: 'Codex' },
89
+ { path: path.join(xdg, 'codex', 'mcp.json'), source: 'Codex (xdg)' },
57
90
  // VS Code (project + user settings)
58
91
  { path: path.join(cwd, '.vscode', 'mcp.json'), source: 'VS Code (project)' },
59
92
  { path: path.join(appData, 'Code', 'User', 'settings.json'), source: 'VS Code (user, win)' },
@@ -61,11 +94,14 @@ function candidateConfigPaths(cwd, extra) {
61
94
  { path: path.join(xdg, 'Code', 'User', 'settings.json'), source: 'VS Code (user, linux)' },
62
95
  // Windsurf
63
96
  { path: path.join(home, '.codeium', 'windsurf', 'mcp_config.json'), source: 'Windsurf' },
97
+ // Kiro / generic
98
+ { path: path.join(home, '.kiro', 'mcp.json'), source: 'Kiro' },
64
99
  // Generic / repo-root
65
100
  { path: path.join(cwd, '.mcp.json'), source: 'Repo (.mcp.json)' },
66
101
  { path: path.join(cwd, 'mcp.json'), source: 'Repo (mcp.json)' },
67
102
  ];
68
103
  if (process.platform === 'win32') {
104
+ list.push({ path: path.join(process.env.ProgramData || 'C:\\ProgramData', 'Cursor', 'managed-mcp.json'), source: 'Cursor (managed, program data)' });
69
105
  const packagesDir = path.join(localAppData, 'Packages');
70
106
  try {
71
107
  for (const packageName of fs.readdirSync(packagesDir)) {
@@ -137,7 +173,7 @@ function parseConfigFile(filePath, source) {
137
173
  parsed = JSON.parse(raw);
138
174
  }
139
175
  catch {
140
- return []; // settings.json with comments/JSONC — best-effort skip
176
+ return [];
141
177
  }
142
178
  const servers = extractServerMap(parsed);
143
179
  const out = [];
@@ -152,7 +188,15 @@ function parseConfigFile(filePath, source) {
152
188
  ? (/\/sse(\b|$)/.test(url) ? 'sse' : 'http')
153
189
  : command ? 'stdio' : 'unknown';
154
190
  const tools = Array.isArray(cfg.tools)
155
- ? cfg.tools.map((t) => (typeof t === 'string' ? t : t?.name)).filter(Boolean).map(String)
191
+ ? cfg.tools
192
+ .map((t) => {
193
+ if (typeof t === 'string')
194
+ return { name: t };
195
+ if (t && typeof t.name === 'string')
196
+ return { name: t.name, description: typeof t.description === 'string' ? t.description : undefined };
197
+ return null;
198
+ })
199
+ .filter(Boolean)
156
200
  : [];
157
201
  const { level, tags } = scoreRisk({ serverName: name, command, args, url });
158
202
  const warnings = [];
@@ -178,10 +222,97 @@ function parseConfigFile(filePath, source) {
178
222
  riskLevel: level,
179
223
  riskTags: tags,
180
224
  warnings,
225
+ probeMode: 'config',
226
+ proxyStatus: 'unknown',
181
227
  });
182
228
  }
183
229
  return out;
184
230
  }
231
+ async function mcpJsonRpc(endpoint, method, params, timeoutMs, sessionId) {
232
+ const headers = {
233
+ 'Content-Type': 'application/json',
234
+ Accept: 'application/json, text/event-stream',
235
+ };
236
+ if (sessionId)
237
+ headers['Mcp-Session-Id'] = sessionId;
238
+ const controller = new AbortController();
239
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
240
+ try {
241
+ const resp = await fetch(endpoint, {
242
+ method: 'POST',
243
+ headers,
244
+ body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
245
+ signal: controller.signal,
246
+ });
247
+ const nextSession = resp.headers.get('mcp-session-id') || sessionId;
248
+ const ct = resp.headers.get('content-type') || '';
249
+ if (ct.includes('text/event-stream')) {
250
+ const text = await resp.text();
251
+ const dataLine = text.split('\n').find(line => line.startsWith('data: '));
252
+ return { json: dataLine ? JSON.parse(dataLine.slice(6)) : null, sessionId: nextSession || undefined };
253
+ }
254
+ return { json: await resp.json(), sessionId: nextSession || undefined };
255
+ }
256
+ finally {
257
+ clearTimeout(timer);
258
+ }
259
+ }
260
+ /** Best-effort live tools/list for HTTP/SSE MCP servers (stdio skipped — would spawn processes). */
261
+ async function probeHttpMcpTools(url, timeoutMs = 8000) {
262
+ const normalized = url.replace(/\/$/, '');
263
+ const candidates = [...new Set([
264
+ normalized,
265
+ normalized.replace(/\/sse(\?.*)?$/i, ''),
266
+ `${normalized}/mcp`,
267
+ normalized.replace(/\/sse(\?.*)?$/i, '/mcp'),
268
+ ])];
269
+ for (const endpoint of candidates) {
270
+ try {
271
+ const init = await mcpJsonRpc(endpoint, 'initialize', {
272
+ protocolVersion: '2024-11-05',
273
+ capabilities: {},
274
+ clientInfo: { name: 'fullcourtdefense-discover', version: '1.0.0' },
275
+ }, timeoutMs);
276
+ const sid = init.sessionId;
277
+ if (init.json?.error)
278
+ continue;
279
+ await mcpJsonRpc(endpoint, 'notifications/initialized', {}, timeoutMs, sid);
280
+ const listed = await mcpJsonRpc(endpoint, 'tools/list', {}, timeoutMs, sid);
281
+ const tools = listed.json?.result?.tools;
282
+ if (Array.isArray(tools) && tools.length > 0) {
283
+ return tools
284
+ .map((t) => (t && typeof t.name === 'string' ? { name: t.name, description: typeof t.description === 'string' ? t.description : undefined } : null))
285
+ .filter(Boolean);
286
+ }
287
+ }
288
+ catch {
289
+ // try next endpoint variant
290
+ }
291
+ }
292
+ return [];
293
+ }
294
+ async function enrichWithDeepProbe(servers, silent) {
295
+ for (const server of servers) {
296
+ if (server.transport !== 'http' && server.transport !== 'sse') {
297
+ if (!silent && server.transport === 'stdio') {
298
+ server.warnings.push('Deep probe skipped for stdio server (config-only inventory).');
299
+ }
300
+ continue;
301
+ }
302
+ if (!server.url)
303
+ continue;
304
+ const liveTools = await probeHttpMcpTools(server.url);
305
+ if (liveTools.length === 0) {
306
+ server.warnings.push('Deep probe could not list tools (server may require auth or be offline).');
307
+ continue;
308
+ }
309
+ const byName = new Map(server.tools.map(t => [t.name, t]));
310
+ for (const tool of liveTools)
311
+ byName.set(tool.name, tool);
312
+ server.tools = [...byName.values()];
313
+ server.probeMode = 'deep';
314
+ }
315
+ }
185
316
  /** Merge servers found in multiple files; keep richest record, union sources. */
186
317
  function dedupe(servers) {
187
318
  const byName = new Map();
@@ -195,8 +326,13 @@ function dedupe(servers) {
195
326
  existing.source += `, ${s.source}`;
196
327
  existing.command = existing.command || s.command;
197
328
  existing.url = existing.url || s.url;
198
- existing.tools = Array.from(new Set([...existing.tools, ...s.tools]));
329
+ const toolMap = new Map(existing.tools.map(t => [t.name, t]));
330
+ for (const t of s.tools)
331
+ toolMap.set(t.name, t);
332
+ existing.tools = [...toolMap.values()];
199
333
  existing.warnings = Array.from(new Set([...existing.warnings, ...s.warnings]));
334
+ if (s.probeMode === 'deep')
335
+ existing.probeMode = 'deep';
200
336
  }
201
337
  return [...byName.values()];
202
338
  }
@@ -207,14 +343,47 @@ const COLOR = {
207
343
  function riskColor(level) {
208
344
  return level === 'critical' || level === 'high' ? COLOR.red : level === 'medium' ? COLOR.yellow : COLOR.green;
209
345
  }
210
- async function upload(servers, apiUrl, apiKey, connectorName) {
346
+ function applyProxyClassification(servers, scanned, cwd) {
347
+ const wrapsByConfig = (0, discoverProxy_1.buildGatewayWrapIndex)(servers);
348
+ for (const server of servers) {
349
+ server.proxyStatus = (0, discoverProxy_1.classifyProxyStatus)(server, wrapsByConfig);
350
+ if (server.proxyStatus === 'fcd_gateway') {
351
+ server.wrapsServerName = (0, discoverProxy_1.extractGatewayDownstream)(server)?.label;
352
+ }
353
+ if (server.proxyStatus === 'direct' && server.riskLevel !== 'low') {
354
+ server.warnings.push('Direct MCP — not proxied through Full Court Defense gateway.');
355
+ }
356
+ }
357
+ return (0, discoverProxy_1.buildClientCoverage)(scanned, servers, cwd);
358
+ }
359
+ function proxyStatusLabel(status) {
360
+ if (status === 'fcd_gateway')
361
+ return 'FCD gateway';
362
+ if (status === 'proxied')
363
+ return 'Proxied';
364
+ if (status === 'direct')
365
+ return 'Direct — needs gateway';
366
+ return 'Unknown';
367
+ }
368
+ async function upload(servers, host, clientCoverage, apiUrl, apiKey, connectorName) {
211
369
  const payload = {
212
- connectorName: connectorName || 'Local MCP discovery (CLI)',
370
+ connectorName: connectorName || `Desktop MCP · ${host.hostname}`,
371
+ host,
372
+ clientCoverage,
213
373
  servers: servers.map(s => ({
214
374
  serverName: s.serverName,
215
375
  command: s.command,
216
376
  url: s.url,
217
- tools: s.tools.map(name => ({ name })),
377
+ transport: s.transport,
378
+ clientSource: s.source,
379
+ configPath: s.configPath,
380
+ riskLevel: s.riskLevel,
381
+ riskTags: s.riskTags,
382
+ warnings: s.warnings,
383
+ probeMode: s.probeMode,
384
+ proxyStatus: s.proxyStatus,
385
+ wrapsServerName: s.wrapsServerName,
386
+ tools: s.tools,
218
387
  })),
219
388
  };
220
389
  const resp = await fetch(`${apiUrl.replace(/\/$/, '')}/api/cicd/discovery/mcp`, {
@@ -227,12 +396,30 @@ async function upload(servers, apiUrl, apiKey, connectorName) {
227
396
  throw new Error(data.error || `Upload failed (${resp.status})`);
228
397
  }
229
398
  const ingested = data.data?.ingested ?? servers.length;
230
- console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) to your AI Inventory.${COLOR.reset}`);
399
+ console.log(`${COLOR.green}Uploaded ${ingested} MCP server(s) from ${host.hostname} to your AI Inventory.${COLOR.reset}`);
231
400
  }
232
401
  async function discoverCommand(args, config) {
233
402
  const type = (args.type || 'mcp').toLowerCase();
403
+ const silent = args.silent === 'true';
404
+ const deep = args.deep === 'true';
405
+ if (args.unschedule === 'true') {
406
+ (0, discoverSchedule_1.uninstallDailyDiscoverSchedule)();
407
+ if (!silent)
408
+ console.log('Removed daily desktop discovery schedule.');
409
+ return;
410
+ }
411
+ if (args.schedule === 'daily') {
412
+ const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
413
+ (0, discoverSchedule_1.installDailyDiscoverSchedule)({ userEmail: args.userEmail, hour: Number.isFinite(hour) ? hour : 9 });
414
+ if (!silent) {
415
+ console.log(`Daily desktop discovery scheduled (runs discover --upload --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
416
+ console.log('Requires FULLCOURTDEFENSE_API_KEY or apiKey in ~/.fullcourtdefense.yml');
417
+ }
418
+ return;
419
+ }
234
420
  if (type !== 'mcp') {
235
- console.error(`Unsupported discover type "${type}". Supported: mcp`);
421
+ if (!silent)
422
+ console.error(`Unsupported discover type "${type}". Supported: mcp`);
236
423
  process.exit(1);
237
424
  }
238
425
  const cwd = process.cwd();
@@ -242,24 +429,49 @@ async function discoverCommand(args, config) {
242
429
  for (const c of candidates) {
243
430
  if (!fs.existsSync(c.path))
244
431
  continue;
245
- scanned.push(`${c.source} → ${c.path}`);
432
+ scanned.push({ path: c.path, source: c.source });
246
433
  found.push(...parseConfigFile(c.path, c.source));
247
434
  }
248
435
  found = dedupe(found);
436
+ const clientCoverage = applyProxyClassification(found, scanned, cwd);
437
+ if (deep && found.length > 0) {
438
+ if (!silent)
439
+ console.log(`${COLOR.gray}Running deep probe (HTTP/SSE tools/list)…${COLOR.reset}`);
440
+ await enrichWithDeepProbe(found, silent);
441
+ }
442
+ const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
249
443
  if (args.json === 'true') {
250
- console.log(JSON.stringify({ scannedFiles: scanned, servers: found }, null, 2));
444
+ console.log(JSON.stringify({
445
+ host,
446
+ scannedFiles: scanned.map(s => `${s.source} → ${s.path}`),
447
+ clientCoverage,
448
+ servers: found,
449
+ }, null, 2));
450
+ return;
451
+ }
452
+ if (silent) {
453
+ if (args.upload === 'true') {
454
+ const apiKey = args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY;
455
+ const apiUrl = args.apiUrl || config.apiUrl || DEFAULT_API_URL;
456
+ if (!apiKey)
457
+ process.exit(1);
458
+ if (found.length === 0)
459
+ return;
460
+ await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
461
+ }
251
462
  return;
252
463
  }
253
464
  console.log(`\n${COLOR.bold}${COLOR.cyan}MCP server discovery${COLOR.reset}`);
465
+ console.log(`${COLOR.gray}Host ${host.hostname} · ${host.platform} · machine ${host.machineId}${COLOR.reset}`);
254
466
  console.log(`${COLOR.gray}Scanned ${candidates.length} known config locations on this machine.${COLOR.reset}\n`);
255
467
  if (scanned.length === 0) {
256
468
  console.log(`${COLOR.yellow}No MCP config files found.${COLOR.reset}`);
257
- console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop, VS Code, Windsurf, and repo-root configs.${COLOR.reset}`);
469
+ console.log(`${COLOR.gray}Looked in Cursor, Claude Desktop/Code, VS Code, Windsurf, Codex, Gemini CLI, and repo-root configs.${COLOR.reset}`);
258
470
  return;
259
471
  }
260
472
  console.log(`${COLOR.bold}Config files found:${COLOR.reset}`);
261
473
  for (const s of scanned)
262
- console.log(` ${COLOR.dim}•${COLOR.reset} ${s}`);
474
+ console.log(` ${COLOR.dim}•${COLOR.reset} ${s.source} → ${s.path}`);
263
475
  console.log('');
264
476
  if (found.length === 0) {
265
477
  console.log(`${COLOR.yellow}Config files exist but declare no MCP servers.${COLOR.reset}`);
@@ -267,16 +479,30 @@ async function discoverCommand(args, config) {
267
479
  }
268
480
  const order = { critical: 0, high: 1, medium: 2, low: 3 };
269
481
  found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
482
+ if (clientCoverage.length > 0) {
483
+ console.log(`${COLOR.bold}Client coverage:${COLOR.reset}`);
484
+ for (const row of clientCoverage) {
485
+ const gateway = row.mcpGatewayInstalled ? `${COLOR.green}gateway ✓${COLOR.reset}` : `${COLOR.red}no gateway${COLOR.reset}`;
486
+ const hooks = row.clientKey === 'cursor'
487
+ ? (row.cursorHooksInstalled ? `${COLOR.green}hooks ✓ (${row.cursorHookEvents.join(', ')})${COLOR.reset}` : `${COLOR.yellow}no hooks${COLOR.reset}`)
488
+ : '';
489
+ console.log(` ${COLOR.dim}•${COLOR.reset} ${row.client} — ${row.mcpServerCount} MCP · ${gateway}${hooks ? ` · ${hooks}` : ''}`);
490
+ }
491
+ console.log('');
492
+ }
270
493
  console.log(`${COLOR.bold}${found.length} MCP server(s) discovered:${COLOR.reset}\n`);
271
494
  for (const s of found) {
272
495
  const col = riskColor(s.riskLevel);
273
496
  const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
274
- console.log(` ${col}${COLOR.bold}[${s.riskLevel.toUpperCase()}]${COLOR.reset} ${COLOR.bold}${s.serverName}${COLOR.reset} ${COLOR.gray}(${s.transport})${COLOR.reset}`);
497
+ const proxyCol = s.proxyStatus === 'direct' ? COLOR.red : s.proxyStatus === 'proxied' || s.proxyStatus === 'fcd_gateway' ? COLOR.green : COLOR.yellow;
498
+ console.log(` ${col}${COLOR.bold}[${s.riskLevel.toUpperCase()}]${COLOR.reset} ${COLOR.bold}${s.serverName}${COLOR.reset} ${COLOR.gray}(${s.transport}${s.probeMode === 'deep' ? ', deep' : ''})${COLOR.reset} ${proxyCol}${proxyStatusLabel(s.proxyStatus)}${COLOR.reset}`);
275
499
  console.log(` ${COLOR.gray}target:${COLOR.reset} ${target}`);
276
500
  if (s.riskTags.length)
277
501
  console.log(` ${COLOR.gray}tags:${COLOR.reset} ${s.riskTags.join(', ')}`);
278
- if (s.tools.length)
279
- console.log(` ${COLOR.gray}tools:${COLOR.reset} ${s.tools.slice(0, 12).join(', ')}${s.tools.length > 12 ? ' …' : ''}`);
502
+ if (s.tools.length) {
503
+ const names = s.tools.map(t => t.name);
504
+ console.log(` ${COLOR.gray}tools:${COLOR.reset} ${names.slice(0, 12).join(', ')}${names.length > 12 ? ' …' : ''}`);
505
+ }
280
506
  console.log(` ${COLOR.gray}from:${COLOR.reset} ${s.source}`);
281
507
  for (const w of s.warnings)
282
508
  console.log(` ${COLOR.red}⚠ ${w}${COLOR.reset}`);
@@ -292,9 +518,12 @@ async function discoverCommand(args, config) {
292
518
  process.exit(1);
293
519
  }
294
520
  console.log('');
295
- await upload(found, apiUrl, apiKey, args.connectorName);
521
+ await upload(found, host, clientCoverage, apiUrl, apiKey, args.connectorName);
296
522
  }
297
523
  else {
298
524
  console.log(`${COLOR.gray}Run with --upload to push these into your AI Inventory.${COLOR.reset}`);
525
+ if (!deep)
526
+ console.log(`${COLOR.gray}Add --deep to live-probe HTTP/SSE servers for tools/list.${COLOR.reset}`);
527
+ console.log(`${COLOR.gray}Add --schedule daily for automatic daily upload (MDM/fleet).${COLOR.reset}`);
299
528
  }
300
529
  }
@@ -0,0 +1,42 @@
1
+ export type DesktopProxyStatus = 'fcd_gateway' | 'proxied' | 'direct' | 'unknown';
2
+ export declare const FCD_GATEWAY_SERVER_NAMES: Set<string>;
3
+ export declare const FCD_HOOK_MARKER = "--fcd-managed true";
4
+ export declare const FCD_HOOK_TAG = "fullcourtdefense";
5
+ export interface ClientCoverageRow {
6
+ client: string;
7
+ clientKey: string;
8
+ configPath: string;
9
+ configPresent: boolean;
10
+ mcpServerCount: number;
11
+ mcpGatewayInstalled: boolean;
12
+ cursorHooksInstalled: boolean;
13
+ cursorHookEvents: string[];
14
+ cursorHookShadow: boolean;
15
+ }
16
+ export interface ProxyClassifiableServer {
17
+ serverName: string;
18
+ command?: string;
19
+ args?: string[];
20
+ url?: string;
21
+ source: string;
22
+ configPath: string;
23
+ }
24
+ export declare function isFcdGatewayServer(server: ProxyClassifiableServer): boolean;
25
+ export declare function extractGatewayDownstream(server: ProxyClassifiableServer): {
26
+ command?: string;
27
+ args?: string[];
28
+ label?: string;
29
+ } | null;
30
+ /** Per-client downstream targets wrapped by FCD gateway entries in the same config file. */
31
+ export declare function buildGatewayWrapIndex(servers: ProxyClassifiableServer[]): Map<string, Set<string>>;
32
+ export declare function classifyProxyStatus(server: ProxyClassifiableServer, wrapsByConfig: Map<string, Set<string>>): DesktopProxyStatus;
33
+ export declare function scanCursorHooks(projectPath?: string): {
34
+ installed: boolean;
35
+ events: string[];
36
+ shadow: boolean;
37
+ };
38
+ export declare function clientKeyFromSource(source: string): string;
39
+ export declare function buildClientCoverage(scanned: Array<{
40
+ path: string;
41
+ source: string;
42
+ }>, servers: ProxyClassifiableServer[], cwd: string): ClientCoverageRow[];
@@ -0,0 +1,256 @@
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.FCD_HOOK_TAG = exports.FCD_HOOK_MARKER = exports.FCD_GATEWAY_SERVER_NAMES = void 0;
37
+ exports.isFcdGatewayServer = isFcdGatewayServer;
38
+ exports.extractGatewayDownstream = extractGatewayDownstream;
39
+ exports.buildGatewayWrapIndex = buildGatewayWrapIndex;
40
+ exports.classifyProxyStatus = classifyProxyStatus;
41
+ exports.scanCursorHooks = scanCursorHooks;
42
+ exports.clientKeyFromSource = clientKeyFromSource;
43
+ exports.buildClientCoverage = buildClientCoverage;
44
+ const fs = __importStar(require("fs"));
45
+ const os = __importStar(require("os"));
46
+ const path = __importStar(require("path"));
47
+ exports.FCD_GATEWAY_SERVER_NAMES = new Set([
48
+ 'agentguard-gateway',
49
+ 'fullcourtdefense-gateway',
50
+ 'fcd-gateway',
51
+ ]);
52
+ exports.FCD_HOOK_MARKER = '--fcd-managed true';
53
+ exports.FCD_HOOK_TAG = 'fullcourtdefense';
54
+ function commandBlob(server) {
55
+ return `${server.serverName} ${server.command || ''} ${(server.args || []).join(' ')} ${server.url || ''}`.toLowerCase();
56
+ }
57
+ function isFcdGatewayServer(server) {
58
+ const name = server.serverName.toLowerCase();
59
+ if (exports.FCD_GATEWAY_SERVER_NAMES.has(name))
60
+ return true;
61
+ const blob = commandBlob(server);
62
+ return blob.includes('mcp-gateway')
63
+ && (blob.includes('fullcourtdefense') || blob.includes('agentguard') || blob.includes('botguard') || blob.includes('fcd-managed'));
64
+ }
65
+ function extractGatewayDownstream(server) {
66
+ const args = server.args || [];
67
+ const cmdIdx = args.indexOf('--mcp-command');
68
+ if (cmdIdx < 0 || !args[cmdIdx + 1])
69
+ return null;
70
+ let downstreamArgs = [];
71
+ const argsIdx = args.indexOf('--mcp-args');
72
+ if (argsIdx >= 0 && args[argsIdx + 1]) {
73
+ try {
74
+ downstreamArgs = JSON.parse(args[argsIdx + 1]);
75
+ }
76
+ catch {
77
+ downstreamArgs = [args[argsIdx + 1]];
78
+ }
79
+ }
80
+ const command = args[cmdIdx + 1];
81
+ const label = inferDownstreamLabel(command, downstreamArgs);
82
+ return { command, args: downstreamArgs, label };
83
+ }
84
+ function inferDownstreamLabel(command, args) {
85
+ const hay = `${command || ''} ${(args || []).join(' ')}`.toLowerCase();
86
+ const pkgMatch = hay.match(/@([a-z0-9._-]+\/mcp[a-z0-9._-]*|[a-z0-9._-]+-mcp[a-z0-9._-]*)/i);
87
+ if (pkgMatch)
88
+ return pkgMatch[1].replace(/^@/, '').split('@')[0];
89
+ const fileMatch = hay.match(/([^/\\]+\.(js|ts|py|mjs))(?:\s|$)/i);
90
+ if (fileMatch)
91
+ return fileMatch[1].replace(/\.(js|ts|py|mjs)$/i, '');
92
+ if (command)
93
+ return path.basename(command).replace(/\.(cmd|exe|js)$/i, '');
94
+ return undefined;
95
+ }
96
+ function downstreamFingerprint(command, args) {
97
+ return `${command || ''}|${(args || []).join(' ')}`.trim().toLowerCase();
98
+ }
99
+ function serverFingerprints(server) {
100
+ const out = new Set();
101
+ out.add(server.serverName.toLowerCase());
102
+ const blob = commandBlob(server);
103
+ out.add(blob);
104
+ if (server.command)
105
+ out.add(downstreamFingerprint(server.command, server.args));
106
+ const label = inferDownstreamLabel(server.command, server.args);
107
+ if (label)
108
+ out.add(label.toLowerCase());
109
+ return [...out];
110
+ }
111
+ /** Per-client downstream targets wrapped by FCD gateway entries in the same config file. */
112
+ function buildGatewayWrapIndex(servers) {
113
+ const byConfig = new Map();
114
+ for (const server of servers) {
115
+ const key = server.configPath.toLowerCase();
116
+ const list = byConfig.get(key) || [];
117
+ list.push(server);
118
+ byConfig.set(key, list);
119
+ }
120
+ const wrapsByConfig = new Map();
121
+ for (const [configPath, list] of byConfig.entries()) {
122
+ const wrapped = new Set();
123
+ for (const server of list) {
124
+ if (!isFcdGatewayServer(server))
125
+ continue;
126
+ const downstream = extractGatewayDownstream(server);
127
+ if (downstream?.label)
128
+ wrapped.add(downstream.label.toLowerCase());
129
+ if (downstream?.command)
130
+ wrapped.add(downstreamFingerprint(downstream.command, downstream.args));
131
+ wrapped.add(server.serverName.toLowerCase());
132
+ }
133
+ wrapsByConfig.set(configPath, wrapped);
134
+ }
135
+ return wrapsByConfig;
136
+ }
137
+ function classifyProxyStatus(server, wrapsByConfig) {
138
+ if (isFcdGatewayServer(server))
139
+ return 'fcd_gateway';
140
+ const wrapped = wrapsByConfig.get(server.configPath.toLowerCase());
141
+ if (!wrapped || wrapped.size === 0)
142
+ return 'direct';
143
+ for (const fp of serverFingerprints(server)) {
144
+ if (wrapped.has(fp))
145
+ return 'proxied';
146
+ for (const w of wrapped) {
147
+ if (fp.includes(w) || w.includes(fp))
148
+ return 'proxied';
149
+ }
150
+ }
151
+ return 'direct';
152
+ }
153
+ const CURSOR_HOOK_EVENTS = {
154
+ beforeSubmitPrompt: 'prompt',
155
+ beforeShellExecution: 'shell',
156
+ beforeMCPExecution: 'mcp',
157
+ afterFileEdit: 'file',
158
+ beforeReadFile: 'read',
159
+ };
160
+ function readHooksJson(file) {
161
+ if (!fs.existsSync(file))
162
+ return { hooks: {} };
163
+ try {
164
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
165
+ return { hooks: parsed?.hooks && typeof parsed.hooks === 'object' ? parsed.hooks : {} };
166
+ }
167
+ catch {
168
+ return { hooks: {} };
169
+ }
170
+ }
171
+ function scanCursorHooks(projectPath) {
172
+ const globalFile = path.join(os.homedir(), '.cursor', 'hooks.json');
173
+ const projectFile = projectPath ? path.join(projectPath, '.cursor', 'hooks.json') : '';
174
+ const files = [globalFile, projectFile].filter(Boolean);
175
+ const events = new Set();
176
+ let shadow = false;
177
+ let installed = false;
178
+ for (const file of files) {
179
+ const { hooks } = readHooksJson(file);
180
+ for (const [hookKey, entries] of Object.entries(hooks)) {
181
+ for (const entry of entries || []) {
182
+ const cmd = entry?.command || '';
183
+ if (!cmd.includes(exports.FCD_HOOK_MARKER) && !cmd.includes(exports.FCD_HOOK_TAG))
184
+ continue;
185
+ installed = true;
186
+ const mapped = CURSOR_HOOK_EVENTS[hookKey] || hookKey;
187
+ events.add(mapped);
188
+ if (cmd.includes('--shadow true'))
189
+ shadow = true;
190
+ }
191
+ }
192
+ }
193
+ return { installed, events: [...events].sort(), shadow };
194
+ }
195
+ function clientKeyFromSource(source) {
196
+ const s = source.toLowerCase();
197
+ if (s.includes('cursor'))
198
+ return 'cursor';
199
+ if (s.includes('claude desktop'))
200
+ return 'claude_desktop';
201
+ if (s.includes('claude code'))
202
+ return 'claude_code';
203
+ if (s.includes('vs code'))
204
+ return 'vscode';
205
+ if (s.includes('windsurf'))
206
+ return 'windsurf';
207
+ if (s.includes('gemini'))
208
+ return 'gemini_cli';
209
+ if (s.includes('codex'))
210
+ return 'codex';
211
+ if (s.includes('kiro'))
212
+ return 'kiro';
213
+ return 'other';
214
+ }
215
+ function buildClientCoverage(scanned, servers, cwd) {
216
+ const hooks = scanCursorHooks(cwd);
217
+ const serversByConfig = new Map();
218
+ for (const server of servers) {
219
+ const key = server.configPath.toLowerCase();
220
+ const list = serversByConfig.get(key) || [];
221
+ list.push(server);
222
+ serversByConfig.set(key, list);
223
+ }
224
+ const rows = [];
225
+ for (const item of scanned) {
226
+ const list = serversByConfig.get(item.path.toLowerCase()) || [];
227
+ const clientKey = clientKeyFromSource(item.source);
228
+ const mcpGatewayInstalled = list.some(isFcdGatewayServer);
229
+ rows.push({
230
+ client: item.source,
231
+ clientKey,
232
+ configPath: item.path,
233
+ configPresent: true,
234
+ mcpServerCount: list.filter(s => !isFcdGatewayServer(s)).length,
235
+ mcpGatewayInstalled,
236
+ cursorHooksInstalled: clientKey === 'cursor' ? hooks.installed : false,
237
+ cursorHookEvents: clientKey === 'cursor' ? hooks.events : [],
238
+ cursorHookShadow: clientKey === 'cursor' ? hooks.shadow : false,
239
+ });
240
+ }
241
+ // Cursor hooks apply globally even if only project mcp.json was scanned
242
+ if (!rows.some(r => r.clientKey === 'cursor') && hooks.installed) {
243
+ rows.push({
244
+ client: 'Cursor (hooks)',
245
+ clientKey: 'cursor',
246
+ configPath: path.join(os.homedir(), '.cursor', 'hooks.json'),
247
+ configPresent: fs.existsSync(path.join(os.homedir(), '.cursor', 'hooks.json')),
248
+ mcpServerCount: 0,
249
+ mcpGatewayInstalled: false,
250
+ cursorHooksInstalled: true,
251
+ cursorHookEvents: hooks.events,
252
+ cursorHookShadow: hooks.shadow,
253
+ });
254
+ }
255
+ return rows.sort((a, b) => a.client.localeCompare(b.client));
256
+ }
@@ -0,0 +1,6 @@
1
+ export interface ScheduleArgs {
2
+ userEmail?: string;
3
+ hour?: number;
4
+ }
5
+ export declare function installDailyDiscoverSchedule(args?: ScheduleArgs): void;
6
+ export declare function uninstallDailyDiscoverSchedule(): 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.installDailyDiscoverSchedule = installDailyDiscoverSchedule;
37
+ exports.uninstallDailyDiscoverSchedule = uninstallDailyDiscoverSchedule;
38
+ const child_process_1 = require("child_process");
39
+ const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const TASK_NAME = 'FullCourtDefenseDesktopDiscover';
43
+ function discoverCommandLine(userEmail) {
44
+ const node = process.execPath;
45
+ const script = path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
46
+ const q = (value) => (/\s/.test(value) ? `"${value}"` : value);
47
+ const parts = [q(node), q(script), 'discover', '--upload', '--silent'];
48
+ if (userEmail)
49
+ parts.push('--user-email', q(userEmail));
50
+ return parts.join(' ');
51
+ }
52
+ function installWindowsSchedule(hour, userEmail) {
53
+ const tr = discoverCommandLine(userEmail);
54
+ const st = `${String(hour).padStart(2, '0')}:00`;
55
+ try {
56
+ (0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'ignore' });
57
+ }
58
+ catch {
59
+ // task may not exist
60
+ }
61
+ (0, child_process_1.execSync)(`schtasks /Create /F /TN "${TASK_NAME}" /SC DAILY /ST ${st} /TR "${tr.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
62
+ }
63
+ function installMacSchedule(hour, userEmail) {
64
+ const label = 'ai.fullcourtdefense.discover';
65
+ const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
66
+ const plistPath = path.join(plistDir, `${label}.plist`);
67
+ fs.mkdirSync(plistDir, { recursive: true });
68
+ const cmd = discoverCommandLine(userEmail).split(/\s+/);
69
+ const program = cmd.shift() || process.execPath;
70
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
71
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
72
+ <plist version="1.0">
73
+ <dict>
74
+ <key>Label</key><string>${label}</string>
75
+ <key>ProgramArguments</key>
76
+ <array>${[program, ...cmd].map(part => `<string>${part.replace(/&/g, '&amp;').replace(/</g, '&lt;')}</string>`).join('')}</array>
77
+ <key>StartCalendarInterval</key>
78
+ <dict><key>Hour</key><integer>${hour}</integer><key>Minute</key><integer>0</integer></dict>
79
+ <key>StandardOutPath</key><string>${path.join(os.homedir(), '.fullcourtdefense-discover.log')}</string>
80
+ <key>StandardErrorPath</key><string>${path.join(os.homedir(), '.fullcourtdefense-discover.err.log')}</string>
81
+ </dict>
82
+ </plist>`;
83
+ fs.writeFileSync(plistPath, plist, 'utf8');
84
+ (0, child_process_1.spawnSync)('launchctl', ['unload', plistPath], { stdio: 'ignore' });
85
+ (0, child_process_1.spawnSync)('launchctl', ['load', plistPath], { stdio: 'inherit' });
86
+ }
87
+ function installLinuxCron(hour, userEmail) {
88
+ const line = `0 ${hour} * * * ${discoverCommandLine(userEmail)} >> ${path.join(os.homedir(), '.fullcourtdefense-discover.log')} 2>&1`;
89
+ const marker = '# fullcourtdefense-discover';
90
+ let crontab = '';
91
+ try {
92
+ crontab = (0, child_process_1.execSync)('crontab -l', { encoding: 'utf8' });
93
+ }
94
+ catch {
95
+ crontab = '';
96
+ }
97
+ const filtered = crontab.split('\n').filter(l => !l.includes(marker) && !l.includes('discover --upload --silent'));
98
+ filtered.push(`${marker}`);
99
+ filtered.push(line);
100
+ (0, child_process_1.execSync)(`echo "${filtered.join('\\n')}" | crontab -`, { stdio: 'inherit', shell: '/bin/bash' });
101
+ }
102
+ function installDailyDiscoverSchedule(args = {}) {
103
+ const hour = Number.isFinite(args.hour) ? Math.min(23, Math.max(0, args.hour)) : 9;
104
+ if (process.platform === 'win32')
105
+ installWindowsSchedule(hour, args.userEmail);
106
+ else if (process.platform === 'darwin')
107
+ installMacSchedule(hour, args.userEmail);
108
+ else
109
+ installLinuxCron(hour, args.userEmail);
110
+ }
111
+ function uninstallDailyDiscoverSchedule() {
112
+ if (process.platform === 'win32') {
113
+ (0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'inherit' });
114
+ return;
115
+ }
116
+ if (process.platform === 'darwin') {
117
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'ai.fullcourtdefense.discover.plist');
118
+ (0, child_process_1.spawnSync)('launchctl', ['unload', plistPath], { stdio: 'ignore' });
119
+ if (fs.existsSync(plistPath))
120
+ fs.unlinkSync(plistPath);
121
+ return;
122
+ }
123
+ let crontab = '';
124
+ try {
125
+ crontab = (0, child_process_1.execSync)('crontab -l', { encoding: 'utf8' });
126
+ }
127
+ catch {
128
+ return;
129
+ }
130
+ const filtered = crontab.split('\n').filter(l => !l.includes('fullcourtdefense-discover') && !l.includes('discover --upload --silent'));
131
+ (0, child_process_1.execSync)(`echo "${filtered.join('\\n')}" | crontab -`, { stdio: 'inherit', shell: '/bin/bash' });
132
+ }
package/dist/index.js CHANGED
@@ -66,8 +66,9 @@ function printHelp() {
66
66
  configure Saves Shield ID, Shield key, and API URL to .fullcourtdefense.yml.
67
67
  scan Runs the scan. Use --local for inside-organization scans.
68
68
  discover Finds MCP servers configured on this machine (Cursor, Claude,
69
- VS Code, Windsurf) and reports their risk. Use --upload to add
70
- them to your AI Inventory.
69
+ VS Code, Windsurf, Codex, Gemini CLI) and reports their risk.
70
+ Use --upload to add them to your AI Inventory. Use --deep for
71
+ live HTTP/SSE tools/list. Use --silent for fleet/MDM scripts.
71
72
  install-cursor-hook
72
73
  Installs a Cursor hook so every AI prompt + agent action
73
74
  (shell, MCP) on this machine is scanned by your Shield.
@@ -199,6 +200,7 @@ function printHelp() {
199
200
  $ fullcourtdefense scan --local --type mcp --mcp-command node --mcp-args ./server.js --mcp-tool all --mode full --format report
200
201
  $ fullcourtdefense discover
201
202
  $ fullcourtdefense discover --upload
203
+ $ fullcourtdefense discover --deep --upload --silent --user-email you@company.com
202
204
  $ fullcourtdefense discover --type mcp --json
203
205
  $ fullcourtdefense install-cursor-hook --shield-id <id> --shield-key <key>
204
206
  $ fullcourtdefense install-cursor-hook --shadow true # monitor only
@@ -320,6 +322,12 @@ async function main() {
320
322
  upload: flags.upload,
321
323
  connectorName: flags['connector-name'],
322
324
  extraPath: flags.path,
325
+ deep: flags.deep,
326
+ silent: flags.silent,
327
+ userEmail: flags['user-email'],
328
+ schedule: flags.schedule,
329
+ unschedule: flags.unschedule,
330
+ scheduleHour: flags['schedule-hour'],
323
331
  };
324
332
  await (0, discover_1.discoverCommand)(args, config);
325
333
  break;
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.1.17",
3
+ "version": "1.2.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": {
7
7
  "fullcourtdefense": "dist/index.js",
8
+ "fcd": "dist/index.js",
8
9
  "botguard": "dist/index.js"
9
10
  },
10
11
  "files": [