fullcourtdefense-cli 1.7.2 → 1.7.4

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.
@@ -64,6 +64,30 @@ function parseSurfaces(args) {
64
64
  }
65
65
  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,}/;
66
66
  const SECRET_KEY = /key|token|secret|password|credential|api[_-]?key/i;
67
+ const SECRET_ARG_NAME = /^(?:--?(?:shield-key|api-key|token|mcp-token|mcp-api-key|key|secret|password|credential)|(?:.*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL).*))$/i;
68
+ function redactCommandArgs(args) {
69
+ const out = [];
70
+ let redactNext = false;
71
+ for (const arg of args || []) {
72
+ if (redactNext) {
73
+ out.push('[redacted]');
74
+ redactNext = false;
75
+ continue;
76
+ }
77
+ const eq = arg.indexOf('=');
78
+ if (eq > 0 && SECRET_ARG_NAME.test(arg.slice(0, eq))) {
79
+ out.push(`${arg.slice(0, eq + 1)}[redacted]`);
80
+ continue;
81
+ }
82
+ if (SECRET_ARG_NAME.test(arg)) {
83
+ out.push(arg);
84
+ redactNext = true;
85
+ continue;
86
+ }
87
+ out.push(arg.replace(SECRET_VALUE, '[redacted]'));
88
+ }
89
+ return out;
90
+ }
67
91
  function machineFingerprint() {
68
92
  const seed = [os.hostname(), os.userInfo().username, os.platform(), os.arch()].join('|');
69
93
  return crypto.createHash('sha256').update(seed).digest('hex').slice(0, 16);
@@ -505,10 +529,11 @@ async function upload(servers, host, clientCoverage, apiUrl, auth, connectorName
505
529
  tools: s.tools,
506
530
  })),
507
531
  };
508
- // Prefer the org API key path; otherwise fall back to the Shield-key endpoint so
509
- // `discover --upload` works straight from `login` credentials with no org key.
532
+ // Prefer the enrolled Shield-key endpoint when available so the zero-paste
533
+ // `login --token` flow works even if an older org API key remains in config.
534
+ // Passing --api-key is the explicit opt-in to the org API key path.
510
535
  const base = apiUrl.replace(/\/$/, '');
511
- const useShieldKey = !auth.apiKey && !!(auth.shieldId && auth.shieldKey);
536
+ const useShieldKey = !auth.preferApiKey && !!(auth.shieldId && auth.shieldKey);
512
537
  const url = useShieldKey ? `${base}/api/cli/discovery` : `${base}/api/cicd/discovery/mcp`;
513
538
  const headers = { 'Content-Type': 'application/json' };
514
539
  if (useShieldKey) {
@@ -601,12 +626,19 @@ async function discoverCommand(args, config) {
601
626
  console.log('Removed daily desktop discovery schedule.');
602
627
  return;
603
628
  }
604
- if (args.schedule === 'daily') {
629
+ if (args.schedule === 'daily' || args.schedule === 'logon') {
605
630
  const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
606
- (0, discoverSchedule_1.installDailyDiscoverSchedule)({ userEmail: args.userEmail, hour: Number.isFinite(hour) ? hour : 9 });
631
+ const surface = surfaces.size === 4 ? 'all' : Array.from(surfaces).join(',');
632
+ (0, discoverSchedule_1.installDailyDiscoverSchedule)({
633
+ userEmail: args.userEmail,
634
+ hour: Number.isFinite(hour) ? hour : 9,
635
+ surface,
636
+ trigger: args.schedule,
637
+ });
607
638
  if (!silent) {
608
- console.log(`Daily desktop discovery scheduled (runs discover --upload --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
609
- console.log('Requires FULLCOURTDEFENSE_API_KEY or apiKey in ~/.fullcourtdefense.yml');
639
+ const when = args.schedule === 'logon' ? 'at logon' : `daily at ${String(Number.isFinite(hour) ? hour : 9).padStart(2, '0')}:00`;
640
+ console.log(`Desktop discovery scheduled ${when} (runs discover --surface ${surface} --upload --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
641
+ console.log('Uses login Shield credentials or an explicit org API key.');
610
642
  }
611
643
  return;
612
644
  }
@@ -668,11 +700,11 @@ async function discoverCommand(args, config) {
668
700
  const hasShieldCreds = !!(creds.shieldId && creds.shieldKey);
669
701
  if (!creds.apiKey && !hasShieldCreds) {
670
702
  if (!silent) {
671
- console.error(`\n${COLOR.red}--upload needs credentials.${COLOR.reset} Run \`fullcourtdefense login --token <fleet-token>\` (or \`fullcourtdefense setup\`) first.`);
703
+ console.error(`\n${COLOR.red}--upload needs credentials.${COLOR.reset} Run \`fullcourtdefense login --token <fleet-enrollment-token>\` first.`);
672
704
  }
673
705
  process.exit(1);
674
706
  }
675
- await upload(found, host, clientCoverage, creds.apiUrl, { apiKey: creds.apiKey, shieldId: creds.shieldId, shieldKey: creds.shieldKey }, args.connectorName, uploadExtras);
707
+ await upload(found, host, clientCoverage, creds.apiUrl, { apiKey: creds.apiKey, shieldId: creds.shieldId, shieldKey: creds.shieldKey, preferApiKey: !!args.apiKey }, args.connectorName, uploadExtras);
676
708
  }
677
709
  if (silent) {
678
710
  if (args.upload === 'true') {
@@ -714,7 +746,7 @@ async function discoverCommand(args, config) {
714
746
  found.sort((a, b) => order[a.riskLevel] - order[b.riskLevel]);
715
747
  for (const s of found) {
716
748
  const col = riskColor(s.riskLevel);
717
- const target = s.command ? `${s.command} ${(s.args || []).join(' ')}`.trim() : (s.url || 'unknown');
749
+ const target = s.command ? `${s.command} ${redactCommandArgs(s.args).join(' ')}`.trim() : (s.url || 'unknown');
718
750
  console.log(` ${col}[${s.riskLevel.toUpperCase()}]${COLOR.reset} ${s.serverName} — ${target} (${proxyStatusLabel(s.proxyStatus)})`);
719
751
  }
720
752
  console.log('');
@@ -1,6 +1,8 @@
1
1
  export interface ScheduleArgs {
2
2
  userEmail?: string;
3
3
  hour?: number;
4
+ surface?: string;
5
+ trigger?: 'daily' | 'logon';
4
6
  }
5
7
  export declare function installDailyDiscoverSchedule(args?: ScheduleArgs): void;
6
8
  export declare function uninstallDailyDiscoverSchedule(): void;
@@ -40,33 +40,56 @@ const fs = __importStar(require("fs"));
40
40
  const os = __importStar(require("os"));
41
41
  const path = __importStar(require("path"));
42
42
  const TASK_NAME = 'FullCourtDefenseDesktopDiscover';
43
- function discoverCommandLine(userEmail) {
43
+ const STARTUP_LAUNCHER = 'FullCourtDefenseDesktopDiscover.cmd';
44
+ function windowsStartupLauncherPath() {
45
+ const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
46
+ return path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', STARTUP_LAUNCHER);
47
+ }
48
+ function discoverCommandLine(userEmail, surface) {
44
49
  const node = process.execPath;
45
50
  const script = path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
46
51
  const q = (value) => (/\s/.test(value) ? `"${value}"` : value);
47
52
  const parts = [q(node), q(script), 'discover', '--upload', '--silent'];
53
+ if (surface)
54
+ parts.push('--surface', q(surface));
48
55
  if (userEmail)
49
56
  parts.push('--user-email', q(userEmail));
50
57
  return parts.join(' ');
51
58
  }
52
- function installWindowsSchedule(hour, userEmail) {
53
- const tr = discoverCommandLine(userEmail);
54
- const st = `${String(hour).padStart(2, '0')}:00`;
59
+ function installWindowsSchedule(hour, userEmail, surface, trigger = 'daily') {
60
+ const tr = discoverCommandLine(userEmail, surface);
55
61
  try {
56
62
  (0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'ignore' });
57
63
  }
58
64
  catch {
59
65
  // task may not exist
60
66
  }
61
- (0, child_process_1.execSync)(`schtasks /Create /F /TN "${TASK_NAME}" /SC DAILY /ST ${st} /TR "${tr.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
67
+ const schedule = trigger === 'logon'
68
+ ? '/SC ONLOGON'
69
+ : `/SC DAILY /ST ${String(hour).padStart(2, '0')}:00`;
70
+ try {
71
+ (0, child_process_1.execSync)(`schtasks /Create /F /TN "${TASK_NAME}" ${schedule} /RL LIMITED /TR "${tr.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
72
+ }
73
+ catch (error) {
74
+ if (trigger !== 'logon')
75
+ throw error;
76
+ const launcher = windowsStartupLauncherPath();
77
+ fs.mkdirSync(path.dirname(launcher), { recursive: true });
78
+ fs.writeFileSync(launcher, `@echo off\r\n${tr} > "%USERPROFILE%\\.fullcourtdefense-discover.log" 2>&1\r\n`, 'utf8');
79
+ console.log(`Task Scheduler denied access; installed user logon launcher instead: ${launcher}`);
80
+ }
62
81
  }
63
- function installMacSchedule(hour, userEmail) {
82
+ function installMacSchedule(hour, userEmail, surface, trigger = 'daily') {
64
83
  const label = 'ai.fullcourtdefense.discover';
65
84
  const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
66
85
  const plistPath = path.join(plistDir, `${label}.plist`);
67
86
  fs.mkdirSync(plistDir, { recursive: true });
68
- const cmd = discoverCommandLine(userEmail).split(/\s+/);
87
+ const cmd = discoverCommandLine(userEmail, surface).split(/\s+/);
69
88
  const program = cmd.shift() || process.execPath;
89
+ const triggerXml = trigger === 'logon'
90
+ ? '<key>RunAtLoad</key><true/>'
91
+ : `<key>StartCalendarInterval</key>
92
+ <dict><key>Hour</key><integer>${hour}</integer><key>Minute</key><integer>0</integer></dict>`;
70
93
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
71
94
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
72
95
  <plist version="1.0">
@@ -74,8 +97,7 @@ function installMacSchedule(hour, userEmail) {
74
97
  <key>Label</key><string>${label}</string>
75
98
  <key>ProgramArguments</key>
76
99
  <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>
100
+ ${triggerXml}
79
101
  <key>StandardOutPath</key><string>${path.join(os.homedir(), '.fullcourtdefense-discover.log')}</string>
80
102
  <key>StandardErrorPath</key><string>${path.join(os.homedir(), '.fullcourtdefense-discover.err.log')}</string>
81
103
  </dict>
@@ -84,8 +106,9 @@ function installMacSchedule(hour, userEmail) {
84
106
  (0, child_process_1.spawnSync)('launchctl', ['unload', plistPath], { stdio: 'ignore' });
85
107
  (0, child_process_1.spawnSync)('launchctl', ['load', plistPath], { stdio: 'inherit' });
86
108
  }
87
- function installLinuxCron(hour, userEmail) {
88
- const line = `0 ${hour} * * * ${discoverCommandLine(userEmail)} >> ${path.join(os.homedir(), '.fullcourtdefense-discover.log')} 2>&1`;
109
+ function installLinuxCron(hour, userEmail, surface, trigger = 'daily') {
110
+ const schedule = trigger === 'logon' ? '@reboot' : `0 ${hour} * * *`;
111
+ const line = `${schedule} ${discoverCommandLine(userEmail, surface)} >> ${path.join(os.homedir(), '.fullcourtdefense-discover.log')} 2>&1`;
89
112
  const marker = '# fullcourtdefense-discover';
90
113
  let crontab = '';
91
114
  try {
@@ -101,16 +124,30 @@ function installLinuxCron(hour, userEmail) {
101
124
  }
102
125
  function installDailyDiscoverSchedule(args = {}) {
103
126
  const hour = Number.isFinite(args.hour) ? Math.min(23, Math.max(0, args.hour)) : 9;
127
+ const trigger = args.trigger || 'daily';
104
128
  if (process.platform === 'win32')
105
- installWindowsSchedule(hour, args.userEmail);
129
+ installWindowsSchedule(hour, args.userEmail, args.surface, trigger);
106
130
  else if (process.platform === 'darwin')
107
- installMacSchedule(hour, args.userEmail);
131
+ installMacSchedule(hour, args.userEmail, args.surface, trigger);
108
132
  else
109
- installLinuxCron(hour, args.userEmail);
133
+ installLinuxCron(hour, args.userEmail, args.surface, trigger);
110
134
  }
111
135
  function uninstallDailyDiscoverSchedule() {
112
136
  if (process.platform === 'win32') {
113
- (0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'inherit' });
137
+ try {
138
+ (0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'inherit' });
139
+ }
140
+ catch {
141
+ // Scheduled Task may not exist or may be inaccessible.
142
+ }
143
+ try {
144
+ const launcher = windowsStartupLauncherPath();
145
+ if (fs.existsSync(launcher))
146
+ fs.unlinkSync(launcher);
147
+ }
148
+ catch {
149
+ // ignore
150
+ }
114
151
  return;
115
152
  }
116
153
  if (process.platform === 'darwin') {
@@ -409,7 +409,7 @@ async function hookCommand(args, config) {
409
409
  // No Shield configured → allow (fail-open by design: a misconfigured machine
410
410
  // must not brick the developer's IDE).
411
411
  if (!shieldId) {
412
- respond(false, undefined, 'FullCourtDefense hook installed but no Shield ID configured (run `fullcourtdefense setup`).');
412
+ respond(false, undefined, 'FullCourtDefense hook installed but no Shield ID configured (run `fullcourtdefense login --token <fleet-enrollment-token>`).');
413
413
  }
414
414
  // --- Action Policy enforcement for actions (shell / mcp / file / read) ---
415
415
  if (event === 'shell' || event === 'mcp' || event === 'file' || event === 'read') {
@@ -168,7 +168,7 @@ async function installCursorHookCommand(args, config) {
168
168
  console.log(`${COLOR.gray}Creds:${COLOR.reset} ${credFile}`);
169
169
  if (!creds.shieldId) {
170
170
  console.log('');
171
- console.log(`${COLOR.yellow}No Shield ID set.${COLOR.reset} Run ${COLOR.bold}fullcourtdefense setup${COLOR.reset} first, then re-run install.`);
171
+ console.log(`${COLOR.yellow}No Shield ID set.${COLOR.reset} Run ${COLOR.bold}fullcourtdefense login --token <fleet-enrollment-token>${COLOR.reset} first, then re-run install.`);
172
172
  }
173
173
  console.log('');
174
174
  (0, restartNotice_1.printRestartNotice)('Cursor');
@@ -172,7 +172,7 @@ function resolveGatewayConfig(args, config, defaults = {}) {
172
172
  apiUrl: args.apiUrl,
173
173
  });
174
174
  if (!creds.shieldId) {
175
- throw new Error('Missing Shield ID. Run `fullcourtdefense setup` once (saves org, API key, Shield ID, and Shield key to ~/.fullcourtdefense.yml).');
175
+ throw new Error('Missing Shield ID. Run `fullcourtdefense login --token <fleet-enrollment-token>` once (saves per-machine Shield credentials to ~/.fullcourtdefense.yml).');
176
176
  }
177
177
  const agentClient = normalizeAgentClient(args.agentClient || process.env.FCD_AGENT_CLIENT || detectRuntimeAgentClient(), defaults.agentClient || 'cursor');
178
178
  const developerName = detectedDeveloperName(args);
package/dist/config.js CHANGED
@@ -177,11 +177,11 @@ function loadConfig(configPath) {
177
177
  }
178
178
  }
179
179
  }
180
- // Plain CLI usage is intentionally global: `fullcourtdefense setup` writes
181
- // ~/.fullcourtdefense.yml, then `install-all` should use that same verified
182
- // Shield from any repo. Project config is still supported for scan defaults
183
- // and explicit `--config` workflows, but it must not silently override the
184
- // user's one-time setup credentials.
180
+ // Plain CLI usage is intentionally global: `fullcourtdefense login --token ...`
181
+ // writes per-machine Shield credentials to ~/.fullcourtdefense.yml, then
182
+ // `install-all` should use that same enrolled Shield from any repo. Project
183
+ // config is still supported for scan defaults and explicit `--config`
184
+ // workflows, but it must not silently override the user's machine enrollment.
185
185
  return configPath
186
186
  ? mergeConfig(homeConfig, projectConfig)
187
187
  : mergeConfig(projectConfig, homeConfig);
@@ -237,7 +237,7 @@ function requireCliSetup(config, overrides = {}, options = {}) {
237
237
  if (requireOrganizationId && !creds.organizationId)
238
238
  missing.push('organizationId');
239
239
  if (missing.length > 0) {
240
- throw new Error(`Missing ${missing.join(', ')}. Run \`fullcourtdefense setup\` once — it saves org, API key, Shield ID, and Shield key to ~/.fullcourtdefense.yml so you never pass them again.`);
240
+ throw new Error(`Missing ${missing.join(', ')}. Run \`fullcourtdefense login --token <fleet-enrollment-token>\` once — it saves per-machine Shield credentials to ~/.fullcourtdefense.yml so you never pass them again.`);
241
241
  }
242
242
  return creds;
243
243
  }
package/dist/index.js CHANGED
@@ -112,9 +112,11 @@ function printHelp() {
112
112
  \x1b[1mCommands:\x1b[0m
113
113
  help Show this onboarding guide and command reference.
114
114
  doctor First step. Checks outbound HTTPS access to FullCourtDefense.
115
- configure Saves org API key, Organization ID, Shield ID, and Shield key to ~/.fullcourtdefense.yml.
116
- After setup, other commands read credentials automatically — no --shield-id/--api-key needed.
117
- setup Same as configure validates credentials against the server before saving.
115
+ login Enrolls this machine with a fleet enrollment token and saves
116
+ per-machine Shield credentials to ~/.fullcourtdefense.yml.
117
+ configure Legacy/manual setup: saves org API key, Organization ID,
118
+ Shield ID, and Shield key to ~/.fullcourtdefense.yml.
119
+ setup Same as configure — manual fallback when not using fleet login.
118
120
  install-all One-click: protect existing MCP servers in every AI client and
119
121
  install Cursor hooks for built-in Cursor actions. No folder path
120
122
  or --mcp-command needed. Use --hooks false to skip Cursor hooks.
@@ -122,6 +124,7 @@ function printHelp() {
122
124
  scan Runs the scan. Use --local for inside-organization scans.
123
125
  discover Finds MCP servers, local secrets, agent rules/skills, and machine
124
126
  blast radius on this laptop. Use --surface all --upload for AI Fleet.
127
+ Use --schedule logon to upload inventory/posture at machine login.
125
128
  install-cursor-hook
126
129
  Installs a Cursor hook so EVERY agent action on this machine is
127
130
  checked against your org's Action Policies — in any repo/folder.
@@ -167,21 +170,23 @@ function printHelp() {
167
170
  init Creates a starter .fullcourtdefense.yml config file.
168
171
 
169
172
  \x1b[1mNew User Process:\x1b[0m
170
- 1. In the web app, create a Shield.
171
- Copy the Shield ID and Shield key from the Shield Integrate tab.
173
+ 1. In the web app, an org admin creates a fleet enrollment token:
174
+ AI Fleet Settings Fleet enrollment token.
172
175
 
173
176
  2. Check outbound connectivity from the customer machine:
174
177
  fullcourtdefense doctor
175
178
  This confirms the machine can reach https://api.fullcourtdefense.ai over HTTPS.
176
179
 
177
- 3. Save org + Shield credentials locally (one time):
178
- fullcourtdefense setup
179
- Saves to ~/.fullcourtdefense.yml — install, discover --upload, scan, gateway, and hooks use it automatically.
180
+ 3. Enroll this developer machine (one time):
181
+ fullcourtdefense login --token <fleet-enrollment-token>
182
+ Or set FCD_ENROLL_TOKEN and run: fullcourtdefense login
183
+ Saves per-machine Shield credentials to ~/.fullcourtdefense.yml —
184
+ install, discover --upload, scan, gateway, and hooks use it automatically.
180
185
 
181
186
  4. One-click protect every AI client on this machine with Action Policies:
182
187
  fullcourtdefense install-all
183
188
  Verify installs:
184
- fullcourtdefense discover --surface mcp
189
+ fullcourtdefense protect-all --dry-run true
185
190
  Or install one client: fullcourtdefense install-cursor-mcp-gateway ...
186
191
 
187
192
  5. Run a guided local scan:
@@ -298,9 +303,10 @@ function printHelp() {
298
303
  $ fullcourtdefense install-claude-code-mcp-gateway --scope project --mcp-command npm --mcp-args "run mcp"
299
304
  $ fullcourtdefense install-claude-desktop-mcp-gateway --mcp-command npm --mcp-args "run mcp"
300
305
  $ fullcourtdefense mcp-gateway --mcp-command npm --mcp-args "run mcp"
301
- $ fullcourtdefense setup
306
+ $ fullcourtdefense login --token <fleet-enrollment-token>
302
307
  $ fullcourtdefense install-all
303
308
  $ fullcourtdefense install-all --auto-protect true --upload
309
+ $ fullcourtdefense discover --surface all --upload --schedule logon
304
310
  $ fullcourtdefense install-mcp-gateway --clients cursor,codex --mcp-command npm --mcp-args "run mcp"
305
311
  $ fullcourtdefense install-codex-mcp-gateway --mcp-command npm --mcp-args "run mcp"
306
312
  $ fullcourtdefense scan --system-prompt ./prompts/system.md --fail-threshold 80
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.7.2"
2
+ "version": "1.7.4"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.7.2",
3
+ "version": "1.7.4",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {