fullcourtdefense-cli 1.14.13 → 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.
- package/dist/commands/discover.d.ts +4 -0
- package/dist/commands/discover.js +58 -6
- package/dist/commands/discoverAgentFiles.d.ts +1 -0
- package/dist/commands/discoverAgentFiles.js +70 -24
- package/dist/commands/discoverPaths.d.ts +17 -0
- package/dist/commands/discoverPaths.js +119 -0
- package/dist/commands/discoverSchedule.d.ts +2 -0
- package/dist/commands/discoverSchedule.js +18 -14
- package/dist/commands/discoverSecrets.d.ts +1 -0
- package/dist/commands/discoverSecrets.js +2 -0
- package/dist/index.js +4 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -12,6 +12,10 @@ export interface DiscoverArgs {
|
|
|
12
12
|
agentCiGate?: AgentCiGateUpload;
|
|
13
13
|
extraPath?: string;
|
|
14
14
|
deep?: string;
|
|
15
|
+
/** Optional comma-separated dev roots (or "auto") to sweep for project MCP configs in OTHER repos. */
|
|
16
|
+
scanRoot?: string;
|
|
17
|
+
/** Allow deep stdio probing (process spawn) for servers found via --scan-root. Default: off. */
|
|
18
|
+
deepRoots?: string;
|
|
15
19
|
silent?: string;
|
|
16
20
|
userEmail?: string;
|
|
17
21
|
schedule?: string;
|
|
@@ -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')
|
|
@@ -482,10 +496,18 @@ async function probeHttpMcpTools(url, timeoutMs = 8000) {
|
|
|
482
496
|
}
|
|
483
497
|
return [];
|
|
484
498
|
}
|
|
485
|
-
async function enrichWithDeepProbe(servers, silent) {
|
|
499
|
+
async function enrichWithDeepProbe(servers, silent, options = {}) {
|
|
486
500
|
for (const server of servers) {
|
|
487
501
|
if (server.transport !== 'http' && server.transport !== 'sse') {
|
|
488
502
|
if (server.transport === 'stdio' && server.command) {
|
|
503
|
+
// Safety: never auto-spawn stdio server processes discovered in OTHER
|
|
504
|
+
// projects via --scan-root; inventory them without live tools unless
|
|
505
|
+
// the user opts in with --deep-roots. (HTTP probing below is fine —
|
|
506
|
+
// it's a network call, not a process spawn.)
|
|
507
|
+
if (options.skipStdioForConfigPaths?.has(path.resolve(server.configPath).toLowerCase())) {
|
|
508
|
+
server.warnings.push('Deep stdio probe skipped for config outside the current project (--scan-root). Pass --deep-roots true to probe these too.');
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
489
511
|
const downstream = (0, discoverProxy_1.isFcdGatewayServer)(server) ? (0, discoverProxy_1.extractGatewayDownstream)(server) : null;
|
|
490
512
|
const command = downstream?.command || server.command;
|
|
491
513
|
const args = downstream?.args || server.args || [];
|
|
@@ -750,16 +772,23 @@ async function discoverCommand(args, config) {
|
|
|
750
772
|
}
|
|
751
773
|
if (args.schedule === 'daily' || args.schedule === 'logon') {
|
|
752
774
|
const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
|
|
753
|
-
|
|
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(','));
|
|
754
780
|
(0, discoverSchedule_1.installDailyDiscoverSchedule)({
|
|
755
781
|
userEmail: args.userEmail,
|
|
756
782
|
hour: Number.isFinite(hour) ? hour : 9,
|
|
757
783
|
surface,
|
|
784
|
+
scanRoot: args.scanRoot,
|
|
785
|
+
deepRoots: args.deepRoots === 'true',
|
|
758
786
|
trigger: args.schedule,
|
|
759
787
|
});
|
|
760
788
|
if (!silent) {
|
|
761
789
|
const when = args.schedule === 'logon' ? 'at logon' : `daily at ${String(Number.isFinite(hour) ? hour : 9).padStart(2, '0')}:00`;
|
|
762
|
-
|
|
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}` : ''}).`);
|
|
763
792
|
console.log('Uses login Shield credentials or an explicit org API key.');
|
|
764
793
|
}
|
|
765
794
|
return;
|
|
@@ -769,8 +798,29 @@ async function discoverCommand(args, config) {
|
|
|
769
798
|
const scanned = [];
|
|
770
799
|
let found = [];
|
|
771
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
|
+
}
|
|
812
|
+
const scanRootConfigPaths = new Set();
|
|
772
813
|
if (runMcp) {
|
|
773
814
|
const candidates = candidateConfigPaths(cwd, args.extraPath);
|
|
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);
|
|
823
|
+
}
|
|
774
824
|
// Self-heal BEFORE parsing: configs written by old CLI versions can carry a stale
|
|
775
825
|
// generic `--agent-name` that breaks dashboard policy matching. Fix them in place so
|
|
776
826
|
// every discovery run converges the fleet to the per-server identity contract.
|
|
@@ -793,14 +843,16 @@ async function discoverCommand(args, config) {
|
|
|
793
843
|
if (deep && found.length > 0) {
|
|
794
844
|
if (!silent)
|
|
795
845
|
console.log(`${COLOR.gray}Running deep probe (HTTP/SSE/stdio tools/list)…${COLOR.reset}`);
|
|
796
|
-
await enrichWithDeepProbe(found, silent
|
|
846
|
+
await enrichWithDeepProbe(found, silent, {
|
|
847
|
+
skipStdioForConfigPaths: args.deepRoots === 'true' ? undefined : scanRootConfigPaths,
|
|
848
|
+
});
|
|
797
849
|
}
|
|
798
850
|
}
|
|
799
851
|
const secrets = surfaces.has('secrets') || surfaces.has('posture')
|
|
800
|
-
? (0, discoverSecrets_1.scanSecrets)({ cwd })
|
|
852
|
+
? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots })
|
|
801
853
|
: undefined;
|
|
802
854
|
const agentFiles = surfaces.has('agent-files') || surfaces.has('posture')
|
|
803
|
-
? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd })
|
|
855
|
+
? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd, extraRoots: sweepProjectRoots })
|
|
804
856
|
: undefined;
|
|
805
857
|
const posture = surfaces.has('posture')
|
|
806
858
|
? (0, discoverBlastRadius_1.computeBlastRadius)({
|
|
@@ -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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
if (!parsed)
|
|
280
|
+
if (!fs.existsSync(extra))
|
|
226
281
|
continue;
|
|
227
|
-
|
|
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;
|
|
@@ -14,5 +14,22 @@ export declare function claudeDesktopLikelyInstalled(): boolean;
|
|
|
14
14
|
export declare function claudeDesktopInstallTargets(configPath?: string): ClaudeDesktopConfigCandidate[];
|
|
15
15
|
/** All MCP client config locations discover / install should check. */
|
|
16
16
|
export declare function candidateConfigPaths(cwd: string, extra?: string): ConfigPathCandidate[];
|
|
17
|
+
/** Common places developers keep code, used by `--scan-root auto`. */
|
|
18
|
+
export declare function defaultScanRoots(): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Bounded walk of developer folders looking for project-scoped MCP configs
|
|
21
|
+
* (`--scan-root`). Depth- and volume-capped so it stays fast and predictable —
|
|
22
|
+
* this is a repo-root sweep, not a disk scan. Hidden/dependency/build folders
|
|
23
|
+
* are pruned; only the known project config filenames are checked.
|
|
24
|
+
*/
|
|
25
|
+
export declare function scanRootsForProjectConfigs(roots: string[], options?: {
|
|
26
|
+
maxDepth?: number;
|
|
27
|
+
maxDirs?: number;
|
|
28
|
+
}): {
|
|
29
|
+
candidates: ConfigPathCandidate[];
|
|
30
|
+
scannedDirs: number;
|
|
31
|
+
};
|
|
32
|
+
/** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
|
|
33
|
+
export declare function resolveScanRoots(flag: string | undefined): string[];
|
|
17
34
|
/** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
|
|
18
35
|
export declare function discoverScanTargets(cwd: string, extra?: string): ConfigPathCandidate[];
|
|
@@ -38,6 +38,9 @@ exports.claudeDesktopConfigCandidates = claudeDesktopConfigCandidates;
|
|
|
38
38
|
exports.claudeDesktopLikelyInstalled = claudeDesktopLikelyInstalled;
|
|
39
39
|
exports.claudeDesktopInstallTargets = claudeDesktopInstallTargets;
|
|
40
40
|
exports.candidateConfigPaths = candidateConfigPaths;
|
|
41
|
+
exports.defaultScanRoots = defaultScanRoots;
|
|
42
|
+
exports.scanRootsForProjectConfigs = scanRootsForProjectConfigs;
|
|
43
|
+
exports.resolveScanRoots = resolveScanRoots;
|
|
41
44
|
exports.discoverScanTargets = discoverScanTargets;
|
|
42
45
|
const fs = __importStar(require("fs"));
|
|
43
46
|
const os = __importStar(require("os"));
|
|
@@ -176,6 +179,122 @@ function candidateConfigPaths(cwd, extra) {
|
|
|
176
179
|
return true;
|
|
177
180
|
});
|
|
178
181
|
}
|
|
182
|
+
/** Project-scoped MCP config files that can sit at any repo root. */
|
|
183
|
+
const PROJECT_CONFIG_FILES = [
|
|
184
|
+
{ rel: path.join('.cursor', 'mcp.json'), source: 'Cursor (project)', clientKey: 'cursor' },
|
|
185
|
+
{ rel: '.mcp.json', source: 'Claude Code (.mcp.json project)', clientKey: 'claude_code' },
|
|
186
|
+
{ rel: path.join('.claude', 'settings.json'), source: 'Claude Code (project)', clientKey: 'claude_code' },
|
|
187
|
+
{ rel: path.join('.vscode', 'mcp.json'), source: 'VS Code (project)', clientKey: 'vscode' },
|
|
188
|
+
{ rel: path.join('.codex', 'config.toml'), source: 'Codex (project)', clientKey: 'codex' },
|
|
189
|
+
{ rel: path.join('.gemini', 'settings.json'), source: 'Gemini CLI (project)', clientKey: 'gemini_cli' },
|
|
190
|
+
{ rel: 'mcp.json', source: 'Repo (mcp.json)', clientKey: 'other' },
|
|
191
|
+
];
|
|
192
|
+
/** Dependency/build/VCS folders that never contain project MCP configs. */
|
|
193
|
+
const SCAN_SKIP_DIRS = new Set([
|
|
194
|
+
'node_modules', 'dist', 'build', 'out', 'vendor', 'venv', 'env', '__pycache__',
|
|
195
|
+
'target', 'coverage', 'tmp', 'temp', 'obj', 'bin', 'packages', 'bower_components',
|
|
196
|
+
'site-packages', 'deps', '.terraform',
|
|
197
|
+
]);
|
|
198
|
+
/** Common places developers keep code, used by `--scan-root auto`. */
|
|
199
|
+
function defaultScanRoots() {
|
|
200
|
+
const home = os.homedir();
|
|
201
|
+
const roots = [
|
|
202
|
+
path.join(home, 'repos'),
|
|
203
|
+
path.join(home, 'projects'),
|
|
204
|
+
path.join(home, 'code'),
|
|
205
|
+
path.join(home, 'dev'),
|
|
206
|
+
path.join(home, 'src'),
|
|
207
|
+
path.join(home, 'work'),
|
|
208
|
+
path.join(home, 'Documents', 'GitHub'),
|
|
209
|
+
path.join(home, 'Documents', 'repos'),
|
|
210
|
+
path.join(home, 'Documents', 'projects'),
|
|
211
|
+
];
|
|
212
|
+
if (process.platform === 'win32') {
|
|
213
|
+
const drive = (process.env.SystemDrive || 'C:') + path.sep;
|
|
214
|
+
for (const name of ['dev', 'src', 'repos', 'code', 'projects', 'work', 'git']) {
|
|
215
|
+
roots.push(path.join(drive, name));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return roots.filter(root => {
|
|
219
|
+
try {
|
|
220
|
+
return fs.statSync(root).isDirectory();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Bounded walk of developer folders looking for project-scoped MCP configs
|
|
229
|
+
* (`--scan-root`). Depth- and volume-capped so it stays fast and predictable —
|
|
230
|
+
* this is a repo-root sweep, not a disk scan. Hidden/dependency/build folders
|
|
231
|
+
* are pruned; only the known project config filenames are checked.
|
|
232
|
+
*/
|
|
233
|
+
function scanRootsForProjectConfigs(roots, options = {}) {
|
|
234
|
+
const maxDepth = options.maxDepth ?? 4;
|
|
235
|
+
const maxDirs = options.maxDirs ?? 25000;
|
|
236
|
+
const candidates = [];
|
|
237
|
+
const visited = new Set();
|
|
238
|
+
let scannedDirs = 0;
|
|
239
|
+
const queue = [];
|
|
240
|
+
for (const root of roots) {
|
|
241
|
+
try {
|
|
242
|
+
const resolved = path.resolve(root);
|
|
243
|
+
if (fs.statSync(resolved).isDirectory())
|
|
244
|
+
queue.push({ dir: resolved, depth: 0 });
|
|
245
|
+
}
|
|
246
|
+
catch { /* missing root — skip */ }
|
|
247
|
+
}
|
|
248
|
+
while (queue.length > 0 && scannedDirs < maxDirs) {
|
|
249
|
+
const { dir, depth } = queue.shift();
|
|
250
|
+
const key = dir.toLowerCase();
|
|
251
|
+
if (visited.has(key))
|
|
252
|
+
continue;
|
|
253
|
+
visited.add(key);
|
|
254
|
+
scannedDirs += 1;
|
|
255
|
+
for (const config of PROJECT_CONFIG_FILES) {
|
|
256
|
+
const file = path.join(dir, config.rel);
|
|
257
|
+
try {
|
|
258
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
259
|
+
candidates.push({ path: file, source: config.source, clientKey: config.clientKey });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch { /* unreadable — skip */ }
|
|
263
|
+
}
|
|
264
|
+
if (depth >= maxDepth)
|
|
265
|
+
continue;
|
|
266
|
+
let entries;
|
|
267
|
+
try {
|
|
268
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
continue; // permission denied etc.
|
|
272
|
+
}
|
|
273
|
+
for (const entry of entries) {
|
|
274
|
+
if (!entry.isDirectory())
|
|
275
|
+
continue;
|
|
276
|
+
const name = entry.name;
|
|
277
|
+
if (name.startsWith('.') || SCAN_SKIP_DIRS.has(name.toLowerCase()))
|
|
278
|
+
continue;
|
|
279
|
+
queue.push({ dir: path.join(dir, name), depth: depth + 1 });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { candidates, scannedDirs };
|
|
283
|
+
}
|
|
284
|
+
/** Resolve the --scan-root flag: comma-separated paths, or "auto" for common dev folders. */
|
|
285
|
+
function resolveScanRoots(flag) {
|
|
286
|
+
if (!flag?.trim())
|
|
287
|
+
return [];
|
|
288
|
+
const parts = flag.split(',').map(part => part.trim()).filter(Boolean);
|
|
289
|
+
const roots = [];
|
|
290
|
+
for (const part of parts) {
|
|
291
|
+
if (part.toLowerCase() === 'auto')
|
|
292
|
+
roots.push(...defaultScanRoots());
|
|
293
|
+
else
|
|
294
|
+
roots.push(path.resolve(part));
|
|
295
|
+
}
|
|
296
|
+
return [...new Set(roots.map(root => root))];
|
|
297
|
+
}
|
|
179
298
|
/** Include config paths for apps that exist on disk even when the MCP file is not created yet. */
|
|
180
299
|
function discoverScanTargets(cwd, extra) {
|
|
181
300
|
const candidates = candidateConfigPaths(cwd, extra);
|
|
@@ -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(
|
|
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 (
|
|
56
|
-
parts.push('--
|
|
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,
|
|
60
|
-
const tr = discoverCommandLine(
|
|
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,
|
|
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(
|
|
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,
|
|
113
|
+
function installLinuxCron(hour, opts, trigger = 'daily') {
|
|
110
114
|
const schedule = trigger === 'logon' ? '@reboot' : `0 ${hour} * * *`;
|
|
111
|
-
const line = `${schedule} ${discoverCommandLine(
|
|
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
|
|
133
|
+
installWindowsSchedule(hour, args, trigger);
|
|
130
134
|
else if (process.platform === 'darwin')
|
|
131
|
-
installMacSchedule(hour, args
|
|
135
|
+
installMacSchedule(hour, args, trigger);
|
|
132
136
|
else
|
|
133
|
-
installLinuxCron(hour, args
|
|
137
|
+
installLinuxCron(hour, args, trigger);
|
|
134
138
|
}
|
|
135
139
|
function uninstallDailyDiscoverSchedule() {
|
|
136
140
|
if (process.platform === 'win32') {
|
|
@@ -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/index.js
CHANGED
|
@@ -327,6 +327,8 @@ function printHelp() {
|
|
|
327
327
|
$ fullcourtdefense discover --surface secrets
|
|
328
328
|
$ fullcourtdefense discover --surface agent-files
|
|
329
329
|
$ fullcourtdefense discover --deep --upload --silent --user-email you@company.com
|
|
330
|
+
$ fullcourtdefense discover --surface mcp --scan-root auto --upload
|
|
331
|
+
$ fullcourtdefense discover --surface mcp --scan-root "C:\dev,D:\work" --upload
|
|
330
332
|
$ fullcourtdefense discover --type mcp --json
|
|
331
333
|
$ fullcourtdefense install-cursor-hook
|
|
332
334
|
$ fullcourtdefense install-cursor-hook --shadow true # monitor only
|
|
@@ -526,6 +528,8 @@ async function main() {
|
|
|
526
528
|
connectorName: flags['connector-name'],
|
|
527
529
|
extraPath: flags.path,
|
|
528
530
|
deep: flags.deep,
|
|
531
|
+
scanRoot: flags['scan-root'],
|
|
532
|
+
deepRoots: flags['deep-roots'],
|
|
529
533
|
silent: flags.silent,
|
|
530
534
|
userEmail: flags['user-email'],
|
|
531
535
|
schedule: flags.schedule,
|
package/dist/version.json
CHANGED