fullcourtdefense-cli 1.14.14 → 1.14.15

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.
@@ -122,6 +122,20 @@ function buildHostMetadata(userEmail, probeMode = 'config') {
122
122
  function candidateConfigPaths(cwd, extra) {
123
123
  return (0, discoverPaths_1.discoverScanTargets)(cwd, extra);
124
124
  }
125
+ /** Dot-dirs that hold a client's project config; the real project root is their parent. */
126
+ const CONFIG_DOT_DIRS = new Set(['.cursor', '.claude', '.codex', '.vscode', '.gemini', '.kiro']);
127
+ /** Map discovered project config file paths back to their owning project folders. */
128
+ function deriveProjectRoots(candidates) {
129
+ const roots = new Set();
130
+ for (const candidate of candidates) {
131
+ let dir = path.dirname(candidate.path);
132
+ if (CONFIG_DOT_DIRS.has(path.basename(dir).toLowerCase())) {
133
+ dir = path.dirname(dir);
134
+ }
135
+ roots.add(path.resolve(dir));
136
+ }
137
+ return [...roots];
138
+ }
125
139
  /** MCP servers can be declared under several keys across clients. */
126
140
  function extractServerMap(parsed) {
127
141
  if (!parsed || typeof parsed !== 'object')
@@ -758,16 +772,23 @@ async function discoverCommand(args, config) {
758
772
  }
759
773
  if (args.schedule === 'daily' || args.schedule === 'logon') {
760
774
  const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
761
- const surface = surfaces.size === 4 ? 'all' : Array.from(surfaces).join(',');
775
+ // Scheduled runs default to the FULL posture (mcp + secrets + agent-files +
776
+ // blast radius) unless the user explicitly narrowed the surface — a daily
777
+ // job should cover everything, not just MCP configs.
778
+ const explicitSurface = Boolean(args.surface || args.type);
779
+ const surface = !explicitSurface ? 'all' : (surfaces.size === 4 ? 'all' : Array.from(surfaces).join(','));
762
780
  (0, discoverSchedule_1.installDailyDiscoverSchedule)({
763
781
  userEmail: args.userEmail,
764
782
  hour: Number.isFinite(hour) ? hour : 9,
765
783
  surface,
784
+ scanRoot: args.scanRoot,
785
+ deepRoots: args.deepRoots === 'true',
766
786
  trigger: args.schedule,
767
787
  });
768
788
  if (!silent) {
769
789
  const when = args.schedule === 'logon' ? 'at logon' : `daily at ${String(Number.isFinite(hour) ? hour : 9).padStart(2, '0')}:00`;
770
- console.log(`Desktop discovery scheduled ${when} (runs discover --surface ${surface} --upload --deep --silent${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
790
+ const rootNote = args.scanRoot ? ` --scan-root ${args.scanRoot}` : '';
791
+ console.log(`Desktop discovery scheduled ${when} (runs discover --surface ${surface} --upload --deep --silent${rootNote}${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
771
792
  console.log('Uses login Shield credentials or an explicit org API key.');
772
793
  }
773
794
  return;
@@ -777,29 +798,28 @@ async function discoverCommand(args, config) {
777
798
  const scanned = [];
778
799
  let found = [];
779
800
  let clientCoverage = [];
801
+ // Optional --scan-root sweep: find project-scoped MCP configs in OTHER repos
802
+ // under the given dev roots (bounded walk — see scanRootsForProjectConfigs).
803
+ // Computed once and shared across MCP, secrets, and agent-file scans so the
804
+ // whole posture references every mapped folder. Without the flag it is empty
805
+ // and behavior is exactly the known-locations scan.
806
+ const scanRoots = (0, discoverPaths_1.resolveScanRoots)(args.scanRoot);
807
+ const sweep = scanRoots.length > 0 ? (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots) : { candidates: [], scannedDirs: 0 };
808
+ const sweepProjectRoots = deriveProjectRoots(sweep.candidates);
809
+ if (scanRoots.length > 0 && !silent) {
810
+ console.log(`${COLOR.gray}Scan-root sweep: checked ${sweep.scannedDirs} folder(s) under ${scanRoots.length} root(s) — found ${sweep.candidates.length} project config(s) across ${sweepProjectRoots.length} project folder(s).${COLOR.reset}`);
811
+ }
780
812
  const scanRootConfigPaths = new Set();
781
813
  if (runMcp) {
782
814
  const candidates = candidateConfigPaths(cwd, args.extraPath);
783
- // Optional --scan-root sweep: find project-scoped MCP configs in OTHER repos
784
- // under the given dev roots (bounded walk see scanRootsForProjectConfigs).
785
- // Without the flag, behavior is exactly the known-locations scan.
786
- const scanRoots = (0, discoverPaths_1.resolveScanRoots)(args.scanRoot);
787
- if (scanRoots.length > 0) {
788
- const sweep = (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots);
789
- const known = new Set(candidates.map(c => path.resolve(c.path).toLowerCase()));
790
- let added = 0;
791
- for (const candidate of sweep.candidates) {
792
- const key = path.resolve(candidate.path).toLowerCase();
793
- if (known.has(key))
794
- continue;
795
- known.add(key);
796
- candidates.push(candidate);
797
- scanRootConfigPaths.add(key);
798
- added += 1;
799
- }
800
- if (!silent) {
801
- console.log(`${COLOR.gray}Scan-root sweep: checked ${sweep.scannedDirs} folder(s) under ${scanRoots.length} root(s) — found ${added} additional project config(s).${COLOR.reset}`);
802
- }
815
+ const known = new Set(candidates.map(c => path.resolve(c.path).toLowerCase()));
816
+ for (const candidate of sweep.candidates) {
817
+ const key = path.resolve(candidate.path).toLowerCase();
818
+ if (known.has(key))
819
+ continue;
820
+ known.add(key);
821
+ candidates.push(candidate);
822
+ scanRootConfigPaths.add(key);
803
823
  }
804
824
  // Self-heal BEFORE parsing: configs written by old CLI versions can carry a stale
805
825
  // generic `--agent-name` that breaks dashboard policy matching. Fix them in place so
@@ -829,10 +849,10 @@ async function discoverCommand(args, config) {
829
849
  }
830
850
  }
831
851
  const secrets = surfaces.has('secrets') || surfaces.has('posture')
832
- ? (0, discoverSecrets_1.scanSecrets)({ cwd })
852
+ ? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots })
833
853
  : undefined;
834
854
  const agentFiles = surfaces.has('agent-files') || surfaces.has('posture')
835
- ? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd })
855
+ ? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd, extraRoots: sweepProjectRoots })
836
856
  : undefined;
837
857
  const posture = surfaces.has('posture')
838
858
  ? (0, discoverBlastRadius_1.computeBlastRadius)({
@@ -12,4 +12,5 @@ export interface AgentFileFinding {
12
12
  }
13
13
  export declare function scanAgentFiles(options?: {
14
14
  cwd?: string;
15
+ extraRoots?: string[];
15
16
  }): AgentFileFinding[];
@@ -182,27 +182,67 @@ function scanNamedFiles(roots, names, kind, titleFor, out, seen) {
182
182
  }
183
183
  }
184
184
  }
185
+ /** Scan a single project root for agent instruction/rule/skill/hook files. */
186
+ function scanProjectRootAgentFiles(root, clientLabel, out, seen) {
187
+ scanNamedFiles([{ root, client: clientLabel }], [
188
+ 'AGENTS.md',
189
+ 'CLAUDE.md',
190
+ 'GEMINI.md',
191
+ '.cursorrules',
192
+ '.windsurfrules',
193
+ ], 'instructions', name => `Agent instructions (${name})`, out, seen);
194
+ scanNamedFiles([{ root: path.join(root, '.github'), client: 'github' }], [
195
+ 'copilot-instructions.md',
196
+ ], 'instructions', () => 'GitHub Copilot instructions', out, seen);
197
+ for (const base of [
198
+ { dir: path.join(root, '.cursor', 'rules'), client: `Cursor (${clientLabel})`, kind: 'rules' },
199
+ { dir: path.join(root, '.agents', 'skills'), client: `Agents (${clientLabel})`, kind: 'skills' },
200
+ ]) {
201
+ if (!fs.existsSync(base.dir))
202
+ continue;
203
+ walkDir(base.dir, out, seen, (filePath, name) => name.endsWith('.md') || name.endsWith('.mdc') || name === 'SKILL.md', filePath => ({
204
+ kind: base.kind,
205
+ client: base.client,
206
+ filePath,
207
+ title: path.basename(filePath),
208
+ sizeBytes: 0,
209
+ lineCount: 0,
210
+ }));
211
+ }
212
+ const hooksPath = path.join(root, '.cursor', 'hooks.json');
213
+ if (fs.existsSync(hooksPath)) {
214
+ const parsed = readAgentFile(hooksPath);
215
+ if (parsed) {
216
+ pushFinding(out, seen, {
217
+ kind: 'hooks',
218
+ client: 'Cursor',
219
+ filePath: hooksPath,
220
+ title: 'Cursor hooks config',
221
+ sizeBytes: parsed.sizeBytes,
222
+ lineCount: parsed.lineCount,
223
+ content: parsed.content,
224
+ riskTags: parsed.content.includes('fullcourtdefense') ? [] : ['no_runtime_hook'],
225
+ });
226
+ }
227
+ }
228
+ }
185
229
  function scanAgentFiles(options = {}) {
186
230
  const home = os.homedir();
187
231
  const cwd = options.cwd || process.cwd();
188
232
  const out = [];
189
233
  const seen = new Set();
190
- const projectRoots = [{ root: cwd, client: 'project' }, { root: home, client: 'user' }];
191
- scanNamedFiles(projectRoots, [
234
+ // Current project + user-global locations.
235
+ scanProjectRootAgentFiles(cwd, 'project', out, seen);
236
+ scanNamedFiles([{ root: home, client: 'user' }], [
192
237
  'AGENTS.md',
193
238
  'CLAUDE.md',
194
239
  'GEMINI.md',
195
240
  '.cursorrules',
196
241
  '.windsurfrules',
197
242
  ], 'instructions', name => `Agent instructions (${name})`, out, seen);
198
- scanNamedFiles([{ root: path.join(cwd, '.github'), client: 'github' }], [
199
- 'copilot-instructions.md',
200
- ], 'instructions', () => 'GitHub Copilot instructions', out, seen);
201
243
  for (const base of [
202
244
  { dir: path.join(home, '.cursor', 'rules'), client: 'Cursor', kind: 'rules' },
203
- { dir: path.join(cwd, '.cursor', 'rules'), client: 'Cursor (project)', kind: 'rules' },
204
245
  { dir: path.join(home, '.agents', 'skills'), client: 'Agents', kind: 'skills' },
205
- { dir: path.join(cwd, '.agents', 'skills'), client: 'Agents (project)', kind: 'skills' },
206
246
  ]) {
207
247
  if (!fs.existsSync(base.dir))
208
248
  continue;
@@ -215,25 +255,31 @@ function scanAgentFiles(options = {}) {
215
255
  lineCount: 0,
216
256
  }));
217
257
  }
218
- for (const hooksPath of [
219
- path.join(home, '.cursor', 'hooks.json'),
220
- path.join(cwd, '.cursor', 'hooks.json'),
221
- ]) {
222
- if (!fs.existsSync(hooksPath))
258
+ const homeHooks = path.join(home, '.cursor', 'hooks.json');
259
+ if (fs.existsSync(homeHooks)) {
260
+ const parsed = readAgentFile(homeHooks);
261
+ if (parsed) {
262
+ pushFinding(out, seen, {
263
+ kind: 'hooks',
264
+ client: 'Cursor',
265
+ filePath: homeHooks,
266
+ title: 'Cursor hooks config',
267
+ sizeBytes: parsed.sizeBytes,
268
+ lineCount: parsed.lineCount,
269
+ content: parsed.content,
270
+ riskTags: parsed.content.includes('fullcourtdefense') ? [] : ['no_runtime_hook'],
271
+ });
272
+ }
273
+ }
274
+ // Additional project roots (e.g. from `discover --scan-root`) — reference agent
275
+ // files in every mapped project, not just the current folder.
276
+ const cwdKey = path.resolve(cwd).toLowerCase();
277
+ for (const extra of options.extraRoots ?? []) {
278
+ if (path.resolve(extra).toLowerCase() === cwdKey)
223
279
  continue;
224
- const parsed = readAgentFile(hooksPath);
225
- if (!parsed)
280
+ if (!fs.existsSync(extra))
226
281
  continue;
227
- pushFinding(out, seen, {
228
- kind: 'hooks',
229
- client: 'Cursor',
230
- filePath: hooksPath,
231
- title: 'Cursor hooks config',
232
- sizeBytes: parsed.sizeBytes,
233
- lineCount: parsed.lineCount,
234
- content: parsed.content,
235
- riskTags: parsed.content.includes('fullcourtdefense') ? [] : ['no_runtime_hook'],
236
- });
282
+ scanProjectRootAgentFiles(extra, 'scanned project', out, seen);
237
283
  }
238
284
  out.sort((a, b) => b.riskTags.length - a.riskTags.length || b.sizeBytes - a.sizeBytes);
239
285
  return out;
@@ -2,6 +2,8 @@ export interface ScheduleArgs {
2
2
  userEmail?: string;
3
3
  hour?: number;
4
4
  surface?: string;
5
+ scanRoot?: string;
6
+ deepRoots?: boolean;
5
7
  trigger?: 'daily' | 'logon';
6
8
  }
7
9
  export declare function installDailyDiscoverSchedule(args?: ScheduleArgs): void;
@@ -45,19 +45,23 @@ function windowsStartupLauncherPath() {
45
45
  const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
46
46
  return path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', STARTUP_LAUNCHER);
47
47
  }
48
- function discoverCommandLine(userEmail, surface) {
48
+ function discoverCommandLine(opts = {}) {
49
49
  const node = process.execPath;
50
50
  const script = path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
51
51
  const q = (value) => (/\s/.test(value) ? `"${value}"` : value);
52
52
  const parts = [q(node), q(script), 'discover', '--upload', '--deep', '--silent'];
53
- if (surface)
54
- parts.push('--surface', q(surface));
55
- if (userEmail)
56
- parts.push('--user-email', q(userEmail));
53
+ if (opts.surface)
54
+ parts.push('--surface', q(opts.surface));
55
+ if (opts.scanRoot)
56
+ parts.push('--scan-root', q(opts.scanRoot));
57
+ if (opts.deepRoots)
58
+ parts.push('--deep-roots', 'true');
59
+ if (opts.userEmail)
60
+ parts.push('--user-email', q(opts.userEmail));
57
61
  return parts.join(' ');
58
62
  }
59
- function installWindowsSchedule(hour, userEmail, surface, trigger = 'daily') {
60
- const tr = discoverCommandLine(userEmail, surface);
63
+ function installWindowsSchedule(hour, opts, trigger = 'daily') {
64
+ const tr = discoverCommandLine(opts);
61
65
  try {
62
66
  (0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'ignore' });
63
67
  }
@@ -79,12 +83,12 @@ function installWindowsSchedule(hour, userEmail, surface, trigger = 'daily') {
79
83
  console.log(`Task Scheduler denied access; installed user logon launcher instead: ${launcher}`);
80
84
  }
81
85
  }
82
- function installMacSchedule(hour, userEmail, surface, trigger = 'daily') {
86
+ function installMacSchedule(hour, opts, trigger = 'daily') {
83
87
  const label = 'ai.fullcourtdefense.discover';
84
88
  const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
85
89
  const plistPath = path.join(plistDir, `${label}.plist`);
86
90
  fs.mkdirSync(plistDir, { recursive: true });
87
- const cmd = discoverCommandLine(userEmail, surface).split(/\s+/);
91
+ const cmd = discoverCommandLine(opts).split(/\s+/);
88
92
  const program = cmd.shift() || process.execPath;
89
93
  const triggerXml = trigger === 'logon'
90
94
  ? '<key>RunAtLoad</key><true/>'
@@ -106,9 +110,9 @@ function installMacSchedule(hour, userEmail, surface, trigger = 'daily') {
106
110
  (0, child_process_1.spawnSync)('launchctl', ['unload', plistPath], { stdio: 'ignore' });
107
111
  (0, child_process_1.spawnSync)('launchctl', ['load', plistPath], { stdio: 'inherit' });
108
112
  }
109
- function installLinuxCron(hour, userEmail, surface, trigger = 'daily') {
113
+ function installLinuxCron(hour, opts, trigger = 'daily') {
110
114
  const schedule = trigger === 'logon' ? '@reboot' : `0 ${hour} * * *`;
111
- const line = `${schedule} ${discoverCommandLine(userEmail, surface)} >> ${path.join(os.homedir(), '.fullcourtdefense-discover.log')} 2>&1`;
115
+ const line = `${schedule} ${discoverCommandLine(opts)} >> ${path.join(os.homedir(), '.fullcourtdefense-discover.log')} 2>&1`;
112
116
  const marker = '# fullcourtdefense-discover';
113
117
  let crontab = '';
114
118
  try {
@@ -126,11 +130,11 @@ function installDailyDiscoverSchedule(args = {}) {
126
130
  const hour = Number.isFinite(args.hour) ? Math.min(23, Math.max(0, args.hour)) : 9;
127
131
  const trigger = args.trigger || 'daily';
128
132
  if (process.platform === 'win32')
129
- installWindowsSchedule(hour, args.userEmail, args.surface, trigger);
133
+ installWindowsSchedule(hour, args, trigger);
130
134
  else if (process.platform === 'darwin')
131
- installMacSchedule(hour, args.userEmail, args.surface, trigger);
135
+ installMacSchedule(hour, args, trigger);
132
136
  else
133
- installLinuxCron(hour, args.userEmail, args.surface, trigger);
137
+ installLinuxCron(hour, args, trigger);
134
138
  }
135
139
  function uninstallDailyDiscoverSchedule() {
136
140
  if (process.platform === 'win32') {
@@ -21,4 +21,5 @@ export interface SecretsScanResult {
21
21
  export declare function scanSecrets(options?: {
22
22
  cwd?: string;
23
23
  maxEnvDepth?: number;
24
+ extraRoots?: string[];
24
25
  }): SecretsScanResult;
@@ -402,6 +402,7 @@ function scanSecrets(options = {}) {
402
402
  const home = os.homedir();
403
403
  const cwd = options.cwd || process.cwd();
404
404
  const maxDepth = options.maxEnvDepth ?? 4;
405
+ const extraRoots = options.extraRoots ?? [];
405
406
  const findings = [];
406
407
  const seen = new Set();
407
408
  let scannedFiles = 0;
@@ -430,6 +431,7 @@ function scanSecrets(options = {}) {
430
431
  const envRoots = Array.from(new Set([
431
432
  home,
432
433
  cwd,
434
+ ...extraRoots,
433
435
  path.join(home, 'dev'),
434
436
  path.join(home, 'repos'),
435
437
  path.join(home, 'projects'),
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.14.14"
2
+ "version": "1.14.15"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.14.14",
3
+ "version": "1.14.15",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {