fullcourtdefense-cli 1.15.8 → 1.15.10

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.
@@ -135,7 +135,9 @@ function logFile() {
135
135
  /** Spawn `discover --upload --surface all --silent` and wait for it to finish. */
136
136
  function runDiscoverSweep(timeoutMs = 300_000) {
137
137
  return new Promise((resolve, reject) => {
138
- const child = (0, child_process_1.spawn)(process.execPath, [cliEntry(), 'discover', '--upload', '--surface', 'all', '--silent'], {
138
+ // --scan-root auto sweeps the default dev roots; admin-selected folders from
139
+ // the console are merged in by discover itself (cached runtime bundle).
140
+ const child = (0, child_process_1.spawn)(process.execPath, [cliEntry(), 'discover', '--upload', '--surface', 'all', '--silent', '--scan-root', 'auto'], {
139
141
  windowsHide: true,
140
142
  stdio: 'ignore',
141
143
  // Daemon/scheduled-task cwd is C:\WINDOWS\system32 — scan from the user's
@@ -45,6 +45,7 @@ const discoverProxy_1 = require("./discoverProxy");
45
45
  const discoverSchedule_1 = require("./discoverSchedule");
46
46
  const mcpGateway_1 = require("./mcpGateway");
47
47
  const discoverPaths_1 = require("./discoverPaths");
48
+ const runtimeConfig_1 = require("../runtimeConfig");
48
49
  const knownMcpServers_1 = require("./knownMcpServers");
49
50
  const discoverAgentFiles_1 = require("./discoverAgentFiles");
50
51
  const discoverBlastRadius_1 = require("./discoverBlastRadius");
@@ -821,9 +822,18 @@ async function discoverCommand(args, config) {
821
822
  // Computed once and shared across MCP, secrets, and agent-file scans so the
822
823
  // whole posture references every mapped folder. Without the flag it is empty
823
824
  // and behavior is exactly the known-locations scan.
824
- const scanRoots = (0, discoverPaths_1.resolveScanRoots)(args.scanRoot);
825
+ // Admin-chosen folders (picked in the console per machine, delivered via the
826
+ // runtime bundle) are always merged in, so every scheduled/daemon/manual scan
827
+ // covers them without extra flags. Default folders the admin explicitly
828
+ // removed in the console are dropped from the sweep.
829
+ const { extraScanRoots: adminScanRoots, disabledScanRoots } = (0, runtimeConfig_1.getCachedScanRootOverrides)();
830
+ const disabledRootSet = new Set(disabledScanRoots.map(root => path.resolve(root).toLowerCase()));
831
+ const isDisabledRoot = (root) => disabledRootSet.has(path.resolve(root).toLowerCase());
832
+ const scanRoots = [...new Set([...(0, discoverPaths_1.resolveScanRoots)(args.scanRoot), ...adminScanRoots])]
833
+ .filter(root => !isDisabledRoot(root));
825
834
  const sweep = scanRoots.length > 0 ? (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots) : { candidates: [], scannedDirs: 0 };
826
- const sweepProjectRoots = deriveProjectRoots(sweep.candidates);
835
+ // Admin roots are scanned for env/secrets even when no MCP config lives there.
836
+ const sweepProjectRoots = [...new Set([...deriveProjectRoots(sweep.candidates), ...adminScanRoots])];
827
837
  const knownConfigCandidates = candidateConfigPaths(cwd, args.extraPath);
828
838
  const secretConfigFiles = [...knownConfigCandidates, ...sweep.candidates]
829
839
  .map(candidate => path.resolve(candidate.path))
@@ -871,7 +881,7 @@ async function discoverCommand(args, config) {
871
881
  }
872
882
  }
873
883
  const secrets = surfaces.has('secrets') || surfaces.has('posture')
874
- ? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots, configFiles: secretConfigFiles })
884
+ ? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots, disabledRoots: disabledScanRoots, configFiles: secretConfigFiles })
875
885
  : undefined;
876
886
  const agentFiles = surfaces.has('agent-files') || surfaces.has('posture')
877
887
  ? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd, extraRoots: sweepProjectRoots })
@@ -886,6 +896,23 @@ async function discoverCommand(args, config) {
886
896
  : undefined;
887
897
  const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
888
898
  const home = os.homedir();
899
+ // Effective roots the posture scan covered — proof of coverage for the console.
900
+ // Admin-removed defaults are excluded here too, so the console mirrors reality.
901
+ const defaultEnvRoots = [home, cwd, path.join(home, 'dev'), path.join(home, 'repos'), path.join(home, 'projects'), path.join(home, 'Documents'), path.join(home, 'code')]
902
+ .map(root => path.resolve(root));
903
+ const adminRootSet = new Set(adminScanRoots.map(root => path.resolve(root).toLowerCase()));
904
+ const scannedRoots = [
905
+ ...[...new Set(defaultEnvRoots)].filter(root => fs.existsSync(root) && !isDisabledRoot(root)).map(root => ({ path: root, source: 'default' })),
906
+ ...adminScanRoots.map(root => path.resolve(root)).filter(root => fs.existsSync(root)).map(root => ({ path: root, source: 'admin' })),
907
+ ...(0, discoverPaths_1.resolveScanRoots)(args.scanRoot)
908
+ .map(root => path.resolve(root))
909
+ .filter(root => fs.existsSync(root) && !adminRootSet.has(root.toLowerCase()) && !isDisabledRoot(root))
910
+ .map(root => ({ path: root, source: 'default' })),
911
+ ].filter((entry, index, list) => list.findIndex(other => other.path.toLowerCase() === entry.path.toLowerCase()) === index);
912
+ // Shallow folder tree for the console folder picker (names only, no contents).
913
+ const folderCatalog = surfaces.has('posture') || surfaces.has('secrets')
914
+ ? (0, discoverPaths_1.buildFolderCatalog)({ extraRoots: adminScanRoots })
915
+ : undefined;
889
916
  const uploadExtras = {
890
917
  secrets,
891
918
  agentFiles,
@@ -895,6 +922,8 @@ async function discoverCommand(args, config) {
895
922
  surfaces: [...surfaces],
896
923
  workingDirectory: cwd,
897
924
  mcpConfigPaths: scanned.map(s => ({ ...s, exists: fs.existsSync(s.path) })),
925
+ scannedRoots,
926
+ folderCatalog,
898
927
  included: [
899
928
  cwd === rawCwd
900
929
  ? `Current command folder: ${cwd}`
@@ -903,13 +932,14 @@ async function discoverCommand(args, config) {
903
932
  `User-level AI rules, skills, hooks, and instruction files under ${home}`,
904
933
  `Credential stores and shell history under ${home}`,
905
934
  `Environment files under ${cwd}, ${path.join(home, 'dev')}, ${path.join(home, 'repos')}, ${path.join(home, 'projects')}, ${path.join(home, 'Documents')}, and ${path.join(home, 'code')}`,
935
+ ...adminScanRoots.map(root => `Admin-selected folder: ${root} (and subfolders)`),
906
936
  ],
907
937
  excluded: [
908
938
  'Unrelated project folders outside the listed roots',
909
939
  'Large/generated dependency and build folders such as node_modules, .git, dist, build, vendor, caches, and temp folders',
910
940
  'Full disk contents, browser profiles, email/chat archives, binary files, and operating-system folders',
911
941
  ],
912
- note: 'Desktop posture is a targeted AI-development exposure scan, not a full-device forensic scan. Run the command from the project folder you want represented.',
942
+ note: 'Desktop posture is a targeted AI-development exposure scan, not a full-device forensic scan. Add folders from the machine page to expand coverage.',
913
943
  },
914
944
  };
915
945
  async function maybeUpload() {
@@ -29,6 +29,25 @@ export declare function scanRootsForProjectConfigs(roots: string[], options?: {
29
29
  candidates: ConfigPathCandidate[];
30
30
  scannedDirs: number;
31
31
  };
32
+ export interface FolderCatalogEntry {
33
+ path: string;
34
+ /** Depth relative to its walk root (home or a dev root) — lets the UI indent. */
35
+ depth: number;
36
+ /** True when the folder has at least one visible subfolder (UI shows an expander). */
37
+ hasChildren: boolean;
38
+ }
39
+ /**
40
+ * Shallow, bounded folder tree of the machine for the console folder picker:
41
+ * the user home plus every default dev root, walked a few levels deep, names
42
+ * only. The admin expands this tree in the dashboard and picks folders to add
43
+ * to the posture scan — so the picker always reflects the machine's REAL
44
+ * structure without uploading anything sensitive (no files, no contents).
45
+ */
46
+ export declare function buildFolderCatalog(options?: {
47
+ maxDepth?: number;
48
+ maxEntries?: number;
49
+ extraRoots?: string[];
50
+ }): FolderCatalogEntry[];
32
51
  /** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
33
52
  export declare function resolveScanRoots(flag: string | undefined): string[];
34
53
  /** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
@@ -40,6 +40,7 @@ exports.claudeDesktopInstallTargets = claudeDesktopInstallTargets;
40
40
  exports.candidateConfigPaths = candidateConfigPaths;
41
41
  exports.defaultScanRoots = defaultScanRoots;
42
42
  exports.scanRootsForProjectConfigs = scanRootsForProjectConfigs;
43
+ exports.buildFolderCatalog = buildFolderCatalog;
43
44
  exports.resolveScanRoots = resolveScanRoots;
44
45
  exports.discoverScanTargets = discoverScanTargets;
45
46
  const fs = __importStar(require("fs"));
@@ -281,6 +282,79 @@ function scanRootsForProjectConfigs(roots, options = {}) {
281
282
  }
282
283
  return { candidates, scannedDirs };
283
284
  }
285
+ /** Non-dev home folders that only add noise to the console folder picker. */
286
+ const CATALOG_SKIP_HOME_DIRS = new Set([
287
+ 'appdata', 'application data', 'local settings', 'cookies', 'nethood', 'printhood',
288
+ 'recent', 'sendto', 'start menu', 'templates', 'searches', 'links', 'saved games',
289
+ '3d objects', 'contacts', 'favorites', 'music', 'videos', 'pictures', 'library',
290
+ 'movies', 'public',
291
+ ]);
292
+ /**
293
+ * Shallow, bounded folder tree of the machine for the console folder picker:
294
+ * the user home plus every default dev root, walked a few levels deep, names
295
+ * only. The admin expands this tree in the dashboard and picks folders to add
296
+ * to the posture scan — so the picker always reflects the machine's REAL
297
+ * structure without uploading anything sensitive (no files, no contents).
298
+ */
299
+ function buildFolderCatalog(options = {}) {
300
+ const maxDepth = options.maxDepth ?? 3;
301
+ const maxEntries = options.maxEntries ?? 600;
302
+ const home = os.homedir();
303
+ const entries = [];
304
+ const seen = new Set();
305
+ const listVisibleSubdirs = (dir, atHomeLevel) => {
306
+ let dirents;
307
+ try {
308
+ dirents = fs.readdirSync(dir, { withFileTypes: true });
309
+ }
310
+ catch {
311
+ return [];
312
+ }
313
+ return dirents
314
+ .filter(entry => {
315
+ if (!entry.isDirectory())
316
+ return false;
317
+ const name = entry.name.toLowerCase();
318
+ if (entry.name.startsWith('.') || entry.name.startsWith('$'))
319
+ return false;
320
+ if (SCAN_SKIP_DIRS.has(name))
321
+ return false;
322
+ if (atHomeLevel && CATALOG_SKIP_HOME_DIRS.has(name))
323
+ return false;
324
+ return true;
325
+ })
326
+ .map(entry => path.join(dir, entry.name));
327
+ };
328
+ const walk = (root) => {
329
+ const queue = [{ dir: root, depth: 0 }];
330
+ while (queue.length > 0 && entries.length < maxEntries) {
331
+ const { dir, depth } = queue.shift();
332
+ const key = path.resolve(dir).toLowerCase();
333
+ if (seen.has(key))
334
+ continue;
335
+ seen.add(key);
336
+ const children = listVisibleSubdirs(dir, key === home.toLowerCase());
337
+ entries.push({ path: path.resolve(dir), depth, hasChildren: children.length > 0 });
338
+ if (depth < maxDepth) {
339
+ for (const child of children)
340
+ queue.push({ dir: child, depth: depth + 1 });
341
+ }
342
+ }
343
+ };
344
+ walk(home);
345
+ for (const root of defaultScanRoots())
346
+ walk(root);
347
+ // Admin-chosen folders get catalogued too, so the picker tree keeps expanding
348
+ // beneath paths the admin already selected.
349
+ for (const root of options.extraRoots || []) {
350
+ try {
351
+ if (fs.statSync(root).isDirectory())
352
+ walk(root);
353
+ }
354
+ catch { /* folder removed since selection — skip */ }
355
+ }
356
+ return entries;
357
+ }
284
358
  /** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
285
359
  function resolveScanRoots(flag) {
286
360
  if (!flag?.trim())
@@ -22,5 +22,6 @@ export declare function scanSecrets(options?: {
22
22
  cwd?: string;
23
23
  maxEnvDepth?: number;
24
24
  extraRoots?: string[];
25
+ disabledRoots?: string[];
25
26
  configFiles?: string[];
26
27
  }): SecretsScanResult;
@@ -199,6 +199,28 @@ function scanCredentialStorePosture(filePath, content, category, out, seen) {
199
199
  detail: '.netrc contains machine login/password material used by CLI tools.',
200
200
  });
201
201
  }
202
+ if (normalized.endsWith('/.git-credentials') && /https?:\/\/[^:\s/]+:[^@\s]+@/i.test(content)) {
203
+ pushStoreFinding(out, seen, {
204
+ filePath,
205
+ category,
206
+ provider: 'git',
207
+ severity: 'high',
208
+ title: 'Git credentials stored in plaintext',
209
+ detail: '.git-credentials stores repository usernames/tokens unencrypted — anything on this machine (including AI agents) can read and reuse them.',
210
+ remediation: 'Switch to `git config --global credential.helper manager` (Windows/mac keychain) or `cache`, then delete ~/.git-credentials and rotate the exposed tokens.',
211
+ });
212
+ }
213
+ if (normalized.endsWith('/.gitconfig') && /helper\s*=\s*store\b/i.test(content)) {
214
+ pushStoreFinding(out, seen, {
215
+ filePath,
216
+ category,
217
+ provider: 'git',
218
+ severity: 'medium',
219
+ title: 'Git credential helper set to plaintext "store"',
220
+ detail: 'credential.helper=store writes every git password/token to ~/.git-credentials in plaintext.',
221
+ remediation: 'Use the OS keychain helper instead: `git config --global credential.helper manager` (Windows) / `osxkeychain` (mac) / `libsecret` (Linux).',
222
+ });
223
+ }
202
224
  if (normalized.endsWith('/.pypirc') && /\b(?:password|token)\s*=\s*\S+/i.test(content)) {
203
225
  pushStoreFinding(out, seen, {
204
226
  filePath,
@@ -317,6 +339,14 @@ function credentialStores(home) {
317
339
  const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
318
340
  return [
319
341
  { path: path.join(home, '.aws', 'credentials'), category: 'credential_store', title: 'AWS credentials file' },
342
+ { path: path.join(home, '.git-credentials'), category: 'credential_store', title: 'Git plaintext credentials' },
343
+ { path: path.join(home, '.gitconfig'), category: 'credential_store', title: 'Git config' },
344
+ { path: path.join(appData, 'Code', 'User', 'settings.json'), category: 'credential_store', title: 'VS Code user settings' },
345
+ { path: path.join(appData, 'Cursor', 'User', 'settings.json'), category: 'credential_store', title: 'Cursor user settings' },
346
+ { path: path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'), category: 'credential_store', title: 'VS Code user settings (mac)' },
347
+ { path: path.join(home, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'), category: 'credential_store', title: 'Cursor user settings (mac)' },
348
+ { path: path.join(xdg, 'Code', 'User', 'settings.json'), category: 'credential_store', title: 'VS Code user settings (linux)' },
349
+ { path: path.join(xdg, 'Cursor', 'User', 'settings.json'), category: 'credential_store', title: 'Cursor user settings (linux)' },
320
350
  { path: path.join(home, '.docker', 'config.json'), category: 'credential_store', title: 'Docker registry auth' },
321
351
  { path: path.join(home, '.npmrc'), category: 'credential_store', title: 'npm auth token' },
322
352
  { path: path.join(home, '.pypirc'), category: 'credential_store', title: 'PyPI auth token' },
@@ -448,7 +478,8 @@ function scanSecrets(options = {}) {
448
478
  const home = os.homedir();
449
479
  const cwd = options.cwd || process.cwd();
450
480
  const maxDepth = options.maxEnvDepth ?? 4;
451
- const extraRoots = options.extraRoots ?? [];
481
+ const disabledRoots = new Set((options.disabledRoots ?? []).map(root => path.resolve(root).toLowerCase()));
482
+ const extraRoots = (options.extraRoots ?? []).filter(root => !disabledRoots.has(path.resolve(root).toLowerCase()));
452
483
  const findings = [];
453
484
  const seen = new Set();
454
485
  let scannedFiles = 0;
@@ -483,7 +514,7 @@ function scanSecrets(options = {}) {
483
514
  path.join(home, 'projects'),
484
515
  path.join(home, 'Documents'),
485
516
  path.join(home, 'code'),
486
- ].filter(root => fs.existsSync(root))));
517
+ ].filter(root => fs.existsSync(root) && !disabledRoots.has(path.resolve(root).toLowerCase()))));
487
518
  for (const envFile of collectEnvFiles(envRoots, maxDepth)) {
488
519
  if (findings.length >= MAX_FINDINGS)
489
520
  break;
@@ -24,6 +24,10 @@ export interface RuntimeBundle {
24
24
  failClosed?: boolean;
25
25
  /** Admin kill-switch: while true, hooks and the gateway deny ALL actions. */
26
26
  suspended?: boolean;
27
+ /** Admin-chosen extra posture scan folders for this machine (from the console). */
28
+ extraScanRoots?: string[];
29
+ /** Default scan folders the admin explicitly removed from this machine's posture sweep. */
30
+ disabledScanRoots?: string[];
27
31
  /** One constrained, auditable action queued for the resident daemon. */
28
32
  machineAction?: {
29
33
  id: string;
@@ -55,3 +59,15 @@ export interface EffectiveBundle extends RuntimeBundle {
55
59
  * or a 'default' marker so the caller can apply its local fallback.
56
60
  */
57
61
  export declare function getRuntimeBundle(input: FetchBundleInput): Promise<EffectiveBundle>;
62
+ /**
63
+ * Admin scan-folder choices from the most recent cached bundle (any shield):
64
+ * extra folders to add and default folders the admin removed. Read-only and
65
+ * offline — used by `discover` so scheduled/daemon scans honor the console
66
+ * selection without needing credentials plumbed in.
67
+ */
68
+ export declare function getCachedScanRootOverrides(): {
69
+ extraScanRoots: string[];
70
+ disabledScanRoots: string[];
71
+ };
72
+ /** @deprecated Use getCachedScanRootOverrides(). */
73
+ export declare function getCachedExtraScanRoots(): string[];
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getRuntimeBundle = getRuntimeBundle;
37
+ exports.getCachedScanRootOverrides = getCachedScanRootOverrides;
38
+ exports.getCachedExtraScanRoots = getCachedExtraScanRoots;
37
39
  const fs = __importStar(require("fs"));
38
40
  const os = __importStar(require("os"));
39
41
  const path = __importStar(require("path"));
@@ -70,7 +72,7 @@ async function getRuntimeBundle(input) {
70
72
  const cached = cache[input.shieldId];
71
73
  const fresh = cached && Date.now() - cached.fetchedAt < ttl;
72
74
  if (cached && fresh && !input.force) {
73
- return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, machineAction: cached.machineAction, source: 'cache' };
75
+ return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, machineAction: cached.machineAction, source: 'cache' };
74
76
  }
75
77
  try {
76
78
  const headers = { 'Content-Type': 'application/json' };
@@ -90,7 +92,7 @@ async function getRuntimeBundle(input) {
90
92
  if (resp.status === 304 && cached) {
91
93
  cache[input.shieldId] = { ...cached, fetchedAt: Date.now() };
92
94
  writeCacheFile(cache);
93
- return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, machineAction: cached.machineAction, source: 'cache' };
95
+ return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, machineAction: cached.machineAction, source: 'cache' };
94
96
  }
95
97
  if (resp.ok) {
96
98
  const body = await resp.json().catch(() => ({}));
@@ -103,6 +105,12 @@ async function getRuntimeBundle(input) {
103
105
  pollIntervalMs: body.data.pollIntervalMs,
104
106
  failClosed: typeof body.data.failClosed === 'boolean' ? body.data.failClosed : undefined,
105
107
  suspended: body.data.suspended === true,
108
+ extraScanRoots: Array.isArray(body.data.extraScanRoots)
109
+ ? body.data.extraScanRoots.filter((root) => typeof root === 'string').slice(0, 20)
110
+ : undefined,
111
+ disabledScanRoots: Array.isArray(body.data.disabledScanRoots)
112
+ ? body.data.disabledScanRoots.filter((root) => typeof root === 'string').slice(0, 20)
113
+ : undefined,
106
114
  machineAction: body.data.machineAction,
107
115
  fetchedAt: Date.now(),
108
116
  };
@@ -114,7 +122,33 @@ async function getRuntimeBundle(input) {
114
122
  }
115
123
  catch { /* fall through to cache / default */ }
116
124
  if (cached) {
117
- return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, machineAction: cached.machineAction, source: 'cache' };
125
+ return { mode: cached.mode, version: cached.version, policyHash: cached.policyHash, policyCount: cached.policyCount, pollIntervalMs: cached.pollIntervalMs, failClosed: cached.failClosed, suspended: cached.suspended, extraScanRoots: cached.extraScanRoots, machineAction: cached.machineAction, source: 'cache' };
118
126
  }
119
127
  return { mode: 'block', version: '', source: 'default' };
120
128
  }
129
+ /**
130
+ * Admin scan-folder choices from the most recent cached bundle (any shield):
131
+ * extra folders to add and default folders the admin removed. Read-only and
132
+ * offline — used by `discover` so scheduled/daemon scans honor the console
133
+ * selection without needing credentials plumbed in.
134
+ */
135
+ function getCachedScanRootOverrides() {
136
+ const cache = readCacheFile();
137
+ const extra = new Set();
138
+ const disabled = new Set();
139
+ for (const entry of Object.values(cache)) {
140
+ for (const root of entry.extraScanRoots || []) {
141
+ if (typeof root === 'string' && root.trim())
142
+ extra.add(root.trim());
143
+ }
144
+ for (const root of entry.disabledScanRoots || []) {
145
+ if (typeof root === 'string' && root.trim())
146
+ disabled.add(root.trim());
147
+ }
148
+ }
149
+ return { extraScanRoots: [...extra], disabledScanRoots: [...disabled] };
150
+ }
151
+ /** @deprecated Use getCachedScanRootOverrides(). */
152
+ function getCachedExtraScanRoots() {
153
+ return getCachedScanRootOverrides().extraScanRoots;
154
+ }
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.15.8"
2
+ "version": "1.15.10"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.15.8",
3
+ "version": "1.15.10",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {