fullcourtdefense-cli 1.3.5 → 1.3.6
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/api.js +1 -17
- package/dist/commands/configure.d.ts +1 -3
- package/dist/commands/configure.js +60 -78
- package/dist/commands/discover.js +93 -1
- package/dist/commands/discoverPaths.js +6 -0
- package/dist/commands/discoverProxy.js +12 -2
- package/dist/commands/installAll.d.ts +3 -2
- package/dist/commands/installAll.js +6 -5
- package/dist/commands/mcpGateway.d.ts +0 -2
- package/dist/commands/mcpGateway.js +12 -33
- package/dist/config.js +8 -1
- package/dist/index.js +10 -8
- package/dist/version.json +1 -1
- package/package.json +42 -42
package/dist/api.js
CHANGED
|
@@ -19,23 +19,7 @@ class BotGuardApi {
|
|
|
19
19
|
},
|
|
20
20
|
body: body ? JSON.stringify(body) : undefined,
|
|
21
21
|
});
|
|
22
|
-
const
|
|
23
|
-
const raw = await resp.text();
|
|
24
|
-
let data;
|
|
25
|
-
try {
|
|
26
|
-
data = raw ? JSON.parse(raw) : {};
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
if (raw.trimStart().startsWith('<!DOCTYPE') || raw.trimStart().startsWith('<html')) {
|
|
30
|
-
throw new Error(`API returned HTML instead of JSON (${resp.status} ${path}). `
|
|
31
|
-
+ 'The endpoint may not be deployed yet — retry with `fullcourtdefense setup --skip-validate true` '
|
|
32
|
-
+ 'or confirm https://api.fullcourtdefense.ai is reachable.');
|
|
33
|
-
}
|
|
34
|
-
throw new Error(`Invalid API response (${resp.status} ${path}): ${raw.slice(0, 200)}`);
|
|
35
|
-
}
|
|
36
|
-
if (!contentType.includes('json') && !data?.success && !resp.ok) {
|
|
37
|
-
// Some gateways omit content-type; JSON parse already succeeded.
|
|
38
|
-
}
|
|
22
|
+
const data = await resp.json();
|
|
39
23
|
if (!resp.ok || data.success === false) {
|
|
40
24
|
const err = data;
|
|
41
25
|
const msg = err.code ? `${err.error} (${err.code})` : (err.error || `API error (${resp.status})`);
|
|
@@ -6,7 +6,5 @@ export interface ConfigureArgs {
|
|
|
6
6
|
shieldKey?: string;
|
|
7
7
|
apiUrl?: string;
|
|
8
8
|
skipValidate?: boolean;
|
|
9
|
-
/** When true, use only flags/env/config — no prompts (for scripts). */
|
|
10
|
-
nonInteractive?: boolean;
|
|
11
9
|
}
|
|
12
|
-
export declare function configureCommand(args: ConfigureArgs,
|
|
10
|
+
export declare function configureCommand(args: ConfigureArgs, _config: BotGuardConfig): Promise<void>;
|
|
@@ -43,107 +43,89 @@ async function ask(rl, question, fallback) {
|
|
|
43
43
|
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
44
44
|
return answer || fallback || '';
|
|
45
45
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const answer = (await rl.question(`${question} (${hint}): `)).trim().toLowerCase();
|
|
49
|
-
if (!answer)
|
|
50
|
-
return defaultYes;
|
|
51
|
-
return answer === 'y' || answer === 'yes';
|
|
46
|
+
function looksLikeOrgId(value) {
|
|
47
|
+
return /^[A-Za-z0-9_-]{8,80}$/.test(value);
|
|
52
48
|
}
|
|
53
|
-
function
|
|
54
|
-
return
|
|
49
|
+
function looksLikeShieldId(value) {
|
|
50
|
+
return /^sh_[A-Za-z0-9]{12,80}$/.test(value);
|
|
55
51
|
}
|
|
56
|
-
function
|
|
57
|
-
return {
|
|
58
|
-
apiUrl: args.apiUrl || config.apiUrl || 'https://api.fullcourtdefense.ai',
|
|
59
|
-
apiKey: resolveApiKey(args, config),
|
|
60
|
-
organizationId: args.organizationId || config.organizationId || '',
|
|
61
|
-
shieldId: args.shieldId || config.shieldId || '',
|
|
62
|
-
shieldKey: args.shieldKey ?? config.shieldKey ?? '',
|
|
63
|
-
};
|
|
52
|
+
function looksLikeShieldKey(value) {
|
|
53
|
+
return /^shsk_[A-Za-z0-9]{24,160}$/.test(value);
|
|
64
54
|
}
|
|
65
|
-
function
|
|
66
|
-
return
|
|
55
|
+
function looksLikeApiKey(value) {
|
|
56
|
+
return /^ag_[A-Za-z0-9]{24,160}$/.test(value);
|
|
67
57
|
}
|
|
68
|
-
function
|
|
69
|
-
const
|
|
70
|
-
|
|
58
|
+
function validateLocalShape(input) {
|
|
59
|
+
const problems = [];
|
|
60
|
+
if (!looksLikeApiKey(input.apiKey))
|
|
61
|
+
problems.push('Organization API key must start with ag_ and look like a FullCourtDefense API key.');
|
|
62
|
+
if (!looksLikeOrgId(input.organizationId))
|
|
63
|
+
problems.push('Organization ID format is invalid.');
|
|
64
|
+
if (!looksLikeShieldId(input.shieldId))
|
|
65
|
+
problems.push('Shield ID must start with sh_ and look like sh_...');
|
|
66
|
+
if (input.shieldKey && !looksLikeShieldKey(input.shieldKey))
|
|
67
|
+
problems.push('Shield key must start with shsk_ and look like shsk_...');
|
|
68
|
+
if (problems.length > 0) {
|
|
69
|
+
throw new Error(`Setup values are not valid:\n- ${problems.join('\n- ')}\nNothing was saved.`);
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
|
-
async function configureCommand(args,
|
|
73
|
-
const
|
|
74
|
-
const useWizard = !args.nonInteractive && !allFlagsProvided(args);
|
|
75
|
-
let finalApiUrl = saved.apiUrl;
|
|
76
|
-
let finalApiKey = saved.apiKey;
|
|
77
|
-
let finalOrgId = saved.organizationId;
|
|
78
|
-
let finalShieldId = saved.shieldId;
|
|
79
|
-
let finalShieldKey = saved.shieldKey;
|
|
80
|
-
const rl = useWizard ? readline.createInterface({ input: process_1.stdin, output: process_1.stdout }) : null;
|
|
72
|
+
async function configureCommand(args, _config) {
|
|
73
|
+
const rl = readline.createInterface({ input: process_1.stdin, output: process_1.stdout });
|
|
81
74
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.log('Find these in the app: Settings → API Keys / Organization, Shield → Integrate.\n');
|
|
87
|
-
finalApiUrl = await ask(rl, '1/5 API URL', saved.apiUrl);
|
|
88
|
-
finalApiKey = await ask(rl, '2/5 Organization API key', saved.apiKey || undefined);
|
|
89
|
-
finalOrgId = await ask(rl, '3/5 Organization ID', saved.organizationId || undefined);
|
|
90
|
-
finalShieldId = await ask(rl, '4/5 Shield ID (sh_...)', saved.shieldId || undefined);
|
|
91
|
-
finalShieldKey = await ask(rl, '5/5 Shield key (blank if not required)', saved.shieldKey || undefined);
|
|
92
|
-
console.log('');
|
|
93
|
-
}
|
|
94
|
-
if (!finalApiKey) {
|
|
95
|
-
throw new Error('Organization API key is required.');
|
|
75
|
+
const apiUrl = args.apiUrl || await ask(rl, 'FullCourtDefense API URL', 'https://api.fullcourtdefense.ai');
|
|
76
|
+
const apiKey = args.apiKey || process.env.FULLCOURTDEFENSE_API_KEY || await ask(rl, 'Organization API key (starts with ag_)');
|
|
77
|
+
if (!apiKey) {
|
|
78
|
+
throw new Error('Organization API key is required to validate your setup.');
|
|
96
79
|
}
|
|
97
|
-
|
|
80
|
+
const organizationId = args.organizationId || await ask(rl, 'Organization ID');
|
|
81
|
+
if (!organizationId) {
|
|
98
82
|
throw new Error('Organization ID is required.');
|
|
99
83
|
}
|
|
100
|
-
|
|
84
|
+
const shieldId = args.shieldId || await ask(rl, 'Shield ID (starts with sh_)');
|
|
85
|
+
if (!shieldId) {
|
|
101
86
|
throw new Error('Shield ID is required.');
|
|
102
87
|
}
|
|
103
|
-
|
|
104
|
-
|
|
88
|
+
const shieldKey = args.shieldKey ?? await ask(rl, 'Shield key (starts with shsk_; blank only if Shield is public)');
|
|
89
|
+
validateLocalShape({ apiKey, organizationId, shieldId, shieldKey: shieldKey || undefined });
|
|
90
|
+
if (args.skipValidate !== true) {
|
|
105
91
|
console.log('Validating organization, Shield ID, and Shield key against FullCourtDefense...');
|
|
106
|
-
const api = new api_1.BotGuardApi(
|
|
92
|
+
const api = new api_1.BotGuardApi(apiKey, apiUrl);
|
|
93
|
+
let validated;
|
|
107
94
|
try {
|
|
108
|
-
|
|
109
|
-
organizationId
|
|
110
|
-
shieldId
|
|
111
|
-
shieldKey:
|
|
95
|
+
validated = await api.validateSetup({
|
|
96
|
+
organizationId,
|
|
97
|
+
shieldId,
|
|
98
|
+
shieldKey: shieldKey || undefined,
|
|
112
99
|
});
|
|
113
|
-
if (validated.organizationId !== finalOrgId) {
|
|
114
|
-
throw new Error(`Organization mismatch: API key belongs to "${validated.organizationName}" (${validated.organizationId}), not "${finalOrgId}".`);
|
|
115
|
-
}
|
|
116
|
-
console.log(`Verified: ${validated.organizationName} → Shield "${validated.shieldName}" (${validated.shieldId})`);
|
|
117
100
|
}
|
|
118
101
|
catch (error) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
throw error;
|
|
133
|
-
}
|
|
102
|
+
throw new Error(`Setup values do not match FullCourtDefense. Nothing was saved.\n`
|
|
103
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
if (validated.organizationId !== organizationId) {
|
|
106
|
+
throw new Error(`Setup values do not match. Nothing was saved.\n`
|
|
107
|
+
+ `Organization mismatch: API key belongs to "${validated.organizationName}" (${validated.organizationId}), not "${organizationId}".`);
|
|
108
|
+
}
|
|
109
|
+
if (validated.shieldId !== shieldId) {
|
|
110
|
+
throw new Error(`Setup values do not match. Nothing was saved.\n`
|
|
111
|
+
+ `Shield mismatch: API returned "${validated.shieldId}", not "${shieldId}".`);
|
|
112
|
+
}
|
|
113
|
+
if (validated.shieldKeyRequired && !shieldKey) {
|
|
114
|
+
throw new Error('Shield key is required for this Shield. Nothing was saved.');
|
|
134
115
|
}
|
|
116
|
+
console.log(`Verified: ${validated.organizationName} → Shield "${validated.shieldName}" (${validated.shieldId})`);
|
|
135
117
|
}
|
|
136
118
|
const savedPath = (0, config_1.saveSetupConfig)({
|
|
137
|
-
apiKey
|
|
138
|
-
organizationId
|
|
139
|
-
shieldId
|
|
140
|
-
shieldKey:
|
|
141
|
-
apiUrl
|
|
119
|
+
apiKey,
|
|
120
|
+
organizationId,
|
|
121
|
+
shieldId,
|
|
122
|
+
shieldKey: shieldKey || undefined,
|
|
123
|
+
apiUrl,
|
|
142
124
|
});
|
|
143
125
|
console.log(`Saved setup to ${savedPath}`);
|
|
144
126
|
console.log('All other CLI commands (install, discover --upload, scan, hooks) will use these credentials automatically.');
|
|
145
127
|
}
|
|
146
128
|
finally {
|
|
147
|
-
rl
|
|
129
|
+
rl.close();
|
|
148
130
|
}
|
|
149
131
|
}
|
|
@@ -38,6 +38,7 @@ exports.runDiscoverUpload = runDiscoverUpload;
|
|
|
38
38
|
const crypto = __importStar(require("crypto"));
|
|
39
39
|
const fs = __importStar(require("fs"));
|
|
40
40
|
const os = __importStar(require("os"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
41
42
|
const config_1 = require("../config");
|
|
42
43
|
const discoverProxy_1 = require("./discoverProxy");
|
|
43
44
|
const discoverSchedule_1 = require("./discoverSchedule");
|
|
@@ -96,6 +97,93 @@ function extractServerMap(parsed) {
|
|
|
96
97
|
return parsed['mcp.servers'];
|
|
97
98
|
return {};
|
|
98
99
|
}
|
|
100
|
+
function parseTomlString(value) {
|
|
101
|
+
const trimmed = value.trim().replace(/,$/, '');
|
|
102
|
+
if (!trimmed)
|
|
103
|
+
return undefined;
|
|
104
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
105
|
+
try {
|
|
106
|
+
return trimmed.startsWith('"')
|
|
107
|
+
? JSON.parse(trimmed)
|
|
108
|
+
: trimmed.slice(1, -1);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return trimmed.slice(1, -1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return trimmed;
|
|
115
|
+
}
|
|
116
|
+
function parseTomlStringArray(value) {
|
|
117
|
+
const trimmed = value.trim();
|
|
118
|
+
if (!trimmed.startsWith('[') || !trimmed.endsWith(']'))
|
|
119
|
+
return undefined;
|
|
120
|
+
try {
|
|
121
|
+
const parsed = JSON.parse(trimmed);
|
|
122
|
+
if (Array.isArray(parsed))
|
|
123
|
+
return parsed.map(String);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// Codex configs may use TOML single-quoted strings; parse those below.
|
|
127
|
+
}
|
|
128
|
+
const out = [];
|
|
129
|
+
const re = /"((?:\\.|[^"\\])*)"|'([^']*)'/g;
|
|
130
|
+
let match;
|
|
131
|
+
while ((match = re.exec(trimmed))) {
|
|
132
|
+
if (match[1] !== undefined) {
|
|
133
|
+
try {
|
|
134
|
+
out.push(JSON.parse(`"${match[1]}"`));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
out.push(match[1]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
out.push(match[2]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out.length > 0 ? out : undefined;
|
|
145
|
+
}
|
|
146
|
+
function parseCodexTomlServers(raw) {
|
|
147
|
+
const servers = {};
|
|
148
|
+
let currentName;
|
|
149
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
152
|
+
continue;
|
|
153
|
+
const section = trimmed.match(/^\[mcp_servers\.([^\]]+)\]$/);
|
|
154
|
+
if (section) {
|
|
155
|
+
const sectionName = section[1].replace(/^"|"$/g, '').replace(/^'|'$/g, '');
|
|
156
|
+
if (sectionName.includes('.')) {
|
|
157
|
+
currentName = undefined;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
currentName = sectionName;
|
|
161
|
+
servers[currentName] = servers[currentName] || {};
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (trimmed.startsWith('[')) {
|
|
165
|
+
currentName = undefined;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (!currentName)
|
|
169
|
+
continue;
|
|
170
|
+
const eq = trimmed.indexOf('=');
|
|
171
|
+
if (eq < 0)
|
|
172
|
+
continue;
|
|
173
|
+
const key = trimmed.slice(0, eq).trim();
|
|
174
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
175
|
+
if (key === 'command') {
|
|
176
|
+
servers[currentName].command = parseTomlString(value);
|
|
177
|
+
}
|
|
178
|
+
else if (key === 'args') {
|
|
179
|
+
servers[currentName].args = parseTomlStringArray(value);
|
|
180
|
+
}
|
|
181
|
+
else if (key === 'url' || key === 'serverUrl') {
|
|
182
|
+
servers[currentName][key] = parseTomlString(value);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return servers;
|
|
186
|
+
}
|
|
99
187
|
const RISK_RULES = [
|
|
100
188
|
{ test: /shell|bash|\bexec\b|command[-_]?runner|terminal|subprocess|run[-_]?admin/i, level: 'critical', tag: 'shell/exec' },
|
|
101
189
|
{ test: /postgres|mysql|mongo|sqlite|\bsql\b|database|\bdb\b|redis|snowflake|bigquery/i, level: 'critical', tag: 'database' },
|
|
@@ -183,7 +271,11 @@ function parseConfigFile(filePath, source) {
|
|
|
183
271
|
parsed = JSON.parse(raw);
|
|
184
272
|
}
|
|
185
273
|
catch {
|
|
186
|
-
|
|
274
|
+
parsed = path.extname(filePath).toLowerCase() === '.toml' || /codex/i.test(source)
|
|
275
|
+
? { mcpServers: parseCodexTomlServers(raw) }
|
|
276
|
+
: null;
|
|
277
|
+
if (!parsed)
|
|
278
|
+
return [];
|
|
187
279
|
}
|
|
188
280
|
const out = [];
|
|
189
281
|
for (const [name, cfgRaw] of Object.entries(extractServerMap(parsed))) {
|
|
@@ -141,6 +141,8 @@ function candidateConfigPaths(cwd, extra) {
|
|
|
141
141
|
{ path: path.join(cwd, '.mcp.json'), source: 'Claude Code (.mcp.json project)', clientKey: 'claude_code' },
|
|
142
142
|
{ path: path.join(home, '.gemini', 'settings.json'), source: 'Gemini CLI', clientKey: 'gemini_cli' },
|
|
143
143
|
{ path: path.join(xdg, 'gemini', 'settings.json'), source: 'Gemini CLI (xdg)', clientKey: 'gemini_cli' },
|
|
144
|
+
{ path: path.join(home, '.codex', 'config.toml'), source: 'Codex', clientKey: 'codex' },
|
|
145
|
+
{ path: path.join(cwd, '.codex', 'config.toml'), source: 'Codex (project)', clientKey: 'codex' },
|
|
144
146
|
{ path: path.join(home, '.codex', 'mcp.json'), source: 'Codex', clientKey: 'codex' },
|
|
145
147
|
{ path: path.join(xdg, 'codex', 'mcp.json'), source: 'Codex (xdg)', clientKey: 'codex' },
|
|
146
148
|
{ path: path.join(cwd, '.vscode', 'mcp.json'), source: 'VS Code (project)', clientKey: 'vscode' },
|
|
@@ -190,5 +192,9 @@ function discoverScanTargets(cwd, extra) {
|
|
|
190
192
|
if (fs.existsSync(path.join(os.homedir(), '.cursor')) && !existing.some(item => path.resolve(item.path).toLowerCase() === cursorGlobal.toLowerCase())) {
|
|
191
193
|
implicit.push({ path: cursorGlobal, source: 'Cursor (global)', clientKey: 'cursor' });
|
|
192
194
|
}
|
|
195
|
+
const codexGlobal = path.join(os.homedir(), '.codex', 'config.toml');
|
|
196
|
+
if (fs.existsSync(path.join(os.homedir(), '.codex')) && !existing.some(item => path.resolve(item.path).toLowerCase() === codexGlobal.toLowerCase())) {
|
|
197
|
+
implicit.push({ path: codexGlobal, source: 'Codex', clientKey: 'codex' });
|
|
198
|
+
}
|
|
193
199
|
return [...existing, ...implicit];
|
|
194
200
|
}
|
|
@@ -74,7 +74,17 @@ function extractGatewayDownstream(server) {
|
|
|
74
74
|
downstreamArgs = JSON.parse(args[argsIdx + 1]);
|
|
75
75
|
}
|
|
76
76
|
catch {
|
|
77
|
-
|
|
77
|
+
const raw = args[argsIdx + 1].trim();
|
|
78
|
+
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
79
|
+
downstreamArgs = raw
|
|
80
|
+
.slice(1, -1)
|
|
81
|
+
.split(',')
|
|
82
|
+
.map(part => part.trim().replace(/^"|"$/g, '').replace(/^'|'$/g, ''))
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
downstreamArgs = [args[argsIdx + 1]];
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
const command = args[cmdIdx + 1];
|
|
@@ -230,7 +240,7 @@ function buildClientCoverage(scanned, servers, cwd) {
|
|
|
230
240
|
client: item.source,
|
|
231
241
|
clientKey,
|
|
232
242
|
configPath: item.path,
|
|
233
|
-
configPresent:
|
|
243
|
+
configPresent: fs.existsSync(item.path),
|
|
234
244
|
mcpServerCount: list.filter(s => !isFcdGatewayServer(s)).length,
|
|
235
245
|
mcpGatewayInstalled,
|
|
236
246
|
cursorHooksInstalled: clientKey === 'cursor' ? hooks.installed : false,
|
|
@@ -5,7 +5,8 @@ export interface InstallAllArgs extends InstallMcpGatewayArgs {
|
|
|
5
5
|
discover?: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
* One-click install: MCP gateway into every supported AI client, optional
|
|
9
|
-
* optional fleet upload.
|
|
8
|
+
* One-click install: MCP gateway into every supported AI client, optional
|
|
9
|
+
* Cursor hooks, optional fleet upload. Hooks are opt-in because Action Policies
|
|
10
|
+
* should be the default enforcement path for MCP tools.
|
|
10
11
|
*/
|
|
11
12
|
export declare function installAllCommand(args: InstallAllArgs, config: BotGuardConfig): Promise<void>;
|
|
@@ -6,8 +6,9 @@ const discover_1 = require("./discover");
|
|
|
6
6
|
const installCursorHook_1 = require("./installCursorHook");
|
|
7
7
|
const mcpGateway_1 = require("./mcpGateway");
|
|
8
8
|
/**
|
|
9
|
-
* One-click install: MCP gateway into every supported AI client, optional
|
|
10
|
-
* optional fleet upload.
|
|
9
|
+
* One-click install: MCP gateway into every supported AI client, optional
|
|
10
|
+
* Cursor hooks, optional fleet upload. Hooks are opt-in because Action Policies
|
|
11
|
+
* should be the default enforcement path for MCP tools.
|
|
11
12
|
*/
|
|
12
13
|
async function installAllCommand(args, config) {
|
|
13
14
|
const creds = (0, config_1.requireCliSetup)(config, {
|
|
@@ -22,9 +23,9 @@ async function installAllCommand(args, config) {
|
|
|
22
23
|
clients: args.clients || 'all',
|
|
23
24
|
};
|
|
24
25
|
console.log('\n\x1b[1m\x1b[36mFullCourtDefense — install all AI clients\x1b[0m');
|
|
25
|
-
console.log('\x1b[
|
|
26
|
+
console.log('\x1b[2mMCP gateway + optional fleet upload. Cursor hooks are opt-in with --hooks true.\x1b[0m\n');
|
|
26
27
|
await (0, mcpGateway_1.installMcpGatewayCommand)(gatewayArgs, config);
|
|
27
|
-
if (args.hooks
|
|
28
|
+
if (args.hooks === 'true') {
|
|
28
29
|
console.log('\n\x1b[1mInstalling Cursor runtime hooks…\x1b[0m');
|
|
29
30
|
const hookArgs = {
|
|
30
31
|
shieldId: creds.shieldId,
|
|
@@ -51,5 +52,5 @@ async function installAllCommand(args, config) {
|
|
|
51
52
|
};
|
|
52
53
|
await (0, discover_1.discoverCommand)(discoverArgs, config);
|
|
53
54
|
}
|
|
54
|
-
console.log('\n\x1b[32mDone.\x1b[0m Restart each AI client you use, then run \x1b[1mfullcourtdefense discover --surface
|
|
55
|
+
console.log('\n\x1b[32mDone.\x1b[0m Restart each AI client you use, then run \x1b[1mfullcourtdefense discover --surface mcp\x1b[0m to verify gateways.');
|
|
55
56
|
}
|
|
@@ -5,8 +5,6 @@ export declare const ALL_GATEWAY_CLIENTS: GatewayClient[];
|
|
|
5
5
|
export interface McpGatewayArgs {
|
|
6
6
|
mcpCommand?: string;
|
|
7
7
|
mcpArgs?: string;
|
|
8
|
-
/** Working directory for the downstream MCP process (Codex config.toml cwd). */
|
|
9
|
-
mcpCwd?: string;
|
|
10
8
|
agentName?: string;
|
|
11
9
|
agentClient?: string;
|
|
12
10
|
developerName?: string;
|
|
@@ -52,7 +52,7 @@ const discoverPaths_1 = require("./discoverPaths");
|
|
|
52
52
|
const discover_1 = require("./discover");
|
|
53
53
|
const DEFAULT_API_URL = 'https://api.fullcourtdefense.ai';
|
|
54
54
|
const MANAGED_SERVER_NAME = 'agentguard-gateway';
|
|
55
|
-
const INSTALL_GATEWAY_ARGS = {
|
|
55
|
+
const INSTALL_GATEWAY_ARGS = { includeShieldKey: false };
|
|
56
56
|
exports.ALL_GATEWAY_CLIENTS = [
|
|
57
57
|
'cursor',
|
|
58
58
|
'claude-code',
|
|
@@ -87,6 +87,12 @@ function parseArgsList(value) {
|
|
|
87
87
|
const inner = trimmed.replace(/^\[/, '').replace(/\]$/, '').trim();
|
|
88
88
|
if (inner && !inner.includes(','))
|
|
89
89
|
return [inner.replace(/^"|"$/g, '')];
|
|
90
|
+
if (inner) {
|
|
91
|
+
return inner
|
|
92
|
+
.split(',')
|
|
93
|
+
.map(part => part.trim().replace(/^"|"$/g, '').replace(/^'|'$/g, ''))
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
}
|
|
90
96
|
throw error;
|
|
91
97
|
}
|
|
92
98
|
}
|
|
@@ -733,42 +739,18 @@ function removeTomlSection(content, tableName) {
|
|
|
733
739
|
}
|
|
734
740
|
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
735
741
|
}
|
|
736
|
-
function writeCodexMcpServer(configPath, serverName, nodeExe, commandArgs
|
|
742
|
+
function writeCodexMcpServer(configPath, serverName, nodeExe, commandArgs) {
|
|
737
743
|
const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
738
744
|
const cleaned = removeTomlSection(existing, `mcp_servers.${serverName}`);
|
|
739
745
|
const argsToml = commandArgs.map(arg => JSON.stringify(arg)).join(', ');
|
|
740
|
-
const
|
|
746
|
+
const block = [
|
|
741
747
|
`[mcp_servers.${serverName}]`,
|
|
742
748
|
`command = ${JSON.stringify(nodeExe)}`,
|
|
743
749
|
`args = [${argsToml}]`,
|
|
744
|
-
];
|
|
745
|
-
if (options.cwd) {
|
|
746
|
-
blockLines.push(`cwd = ${JSON.stringify(options.cwd.replace(/\//g, path.sep))}`);
|
|
747
|
-
}
|
|
748
|
-
if (options.startupTimeoutSec && options.startupTimeoutSec > 0) {
|
|
749
|
-
blockLines.push(`startup_timeout_sec = ${options.startupTimeoutSec}`);
|
|
750
|
-
}
|
|
751
|
-
const block = blockLines.join('\n');
|
|
750
|
+
].join('\n');
|
|
752
751
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
753
752
|
fs.writeFileSync(configPath, `${cleaned ? `${cleaned}\n\n` : ''}${block}\n`, 'utf8');
|
|
754
753
|
}
|
|
755
|
-
function resolveMcpCwd(args) {
|
|
756
|
-
const explicit = args.mcpCwd || process.env.FCD_MCP_CWD;
|
|
757
|
-
if (explicit)
|
|
758
|
-
return path.resolve(explicit);
|
|
759
|
-
const pkg = path.join(process.cwd(), 'package.json');
|
|
760
|
-
if (fs.existsSync(pkg)) {
|
|
761
|
-
try {
|
|
762
|
-
const scripts = JSON.parse(fs.readFileSync(pkg, 'utf8')).scripts || {};
|
|
763
|
-
const mcpArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
764
|
-
if (args.mcpCommand === 'npm' && mcpArgs[0] === 'run' && scripts[mcpArgs[1] || 'mcp']) {
|
|
765
|
-
return process.cwd();
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
catch { /* ignore */ }
|
|
769
|
-
}
|
|
770
|
-
return undefined;
|
|
771
|
-
}
|
|
772
754
|
function codexConfigPath(projectScope) {
|
|
773
755
|
return projectScope
|
|
774
756
|
? path.join(process.cwd(), '.codex', 'config.toml')
|
|
@@ -971,7 +953,7 @@ async function installClaudeCodeMcpGatewayCommand(args, config) {
|
|
|
971
953
|
if (!downstreamCommand)
|
|
972
954
|
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
973
955
|
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
974
|
-
const includeShieldKey =
|
|
956
|
+
const includeShieldKey = false;
|
|
975
957
|
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, {
|
|
976
958
|
...INSTALL_GATEWAY_ARGS,
|
|
977
959
|
includeShieldKey,
|
|
@@ -1058,10 +1040,7 @@ async function installCodexMcpGatewayCommand(args, config) {
|
|
|
1058
1040
|
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1059
1041
|
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1060
1042
|
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1061
|
-
writeCodexMcpServer(file, serverName, nodeExe, commandArgs
|
|
1062
|
-
cwd: resolveMcpCwd(args),
|
|
1063
|
-
startupTimeoutSec: 120,
|
|
1064
|
-
});
|
|
1043
|
+
writeCodexMcpServer(file, serverName, nodeExe, commandArgs);
|
|
1065
1044
|
console.log(`AgentGuard MCP Gateway installed for Codex (${projectScope ? 'project' : 'global'}).`);
|
|
1066
1045
|
console.log(`Config: ${file}`);
|
|
1067
1046
|
console.log(`Server: ${serverName}`);
|
package/dist/config.js
CHANGED
|
@@ -177,7 +177,14 @@ function loadConfig(configPath) {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
|
|
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.
|
|
185
|
+
return configPath
|
|
186
|
+
? mergeConfig(homeConfig, projectConfig)
|
|
187
|
+
: mergeConfig(projectConfig, homeConfig);
|
|
181
188
|
}
|
|
182
189
|
function getDefaultConfigPath() {
|
|
183
190
|
return path.resolve(process.cwd(), '.fullcourtdefense.yml');
|
package/dist/index.js
CHANGED
|
@@ -111,8 +111,8 @@ function printHelp() {
|
|
|
111
111
|
doctor First step. Checks outbound HTTPS access to FullCourtDefense.
|
|
112
112
|
configure Saves org API key, Organization ID, Shield ID, and Shield key to ~/.fullcourtdefense.yml.
|
|
113
113
|
After setup, other commands read credentials automatically — no --shield-id/--api-key needed.
|
|
114
|
-
setup Same as configure —
|
|
115
|
-
install-all One-click: MCP gateway in every AI client
|
|
114
|
+
setup Same as configure — validates credentials against the server before saving.
|
|
115
|
+
install-all One-click: MCP gateway in every AI client. Cursor hooks are opt-in.
|
|
116
116
|
install Alias for install-all.
|
|
117
117
|
scan Runs the scan. Use --local for inside-organization scans.
|
|
118
118
|
discover Finds MCP servers, local secrets, agent rules/skills, and machine
|
|
@@ -154,11 +154,14 @@ function printHelp() {
|
|
|
154
154
|
fullcourtdefense doctor
|
|
155
155
|
This confirms the machine can reach https://api.fullcourtdefense.ai over HTTPS.
|
|
156
156
|
|
|
157
|
-
3. Save org + Shield credentials locally (one time
|
|
157
|
+
3. Save org + Shield credentials locally (one time):
|
|
158
158
|
fullcourtdefense setup
|
|
159
|
+
Saves to ~/.fullcourtdefense.yml — install, discover --upload, scan, gateway, and hooks use it automatically.
|
|
159
160
|
|
|
160
|
-
4. One-click protect every AI client on this machine:
|
|
161
|
-
fullcourtdefense install-all --mcp-command npm --mcp-args "run mcp"
|
|
161
|
+
4. One-click protect every AI client on this machine with MCP Action Policies:
|
|
162
|
+
fullcourtdefense install-all --mcp-command npm --mcp-args "run mcp"
|
|
163
|
+
Verify installs:
|
|
164
|
+
fullcourtdefense discover --surface mcp
|
|
162
165
|
Or install one client: fullcourtdefense install-cursor-mcp-gateway ...
|
|
163
166
|
|
|
164
167
|
5. Run a guided local scan:
|
|
@@ -257,6 +260,7 @@ function printHelp() {
|
|
|
257
260
|
$ fullcourtdefense scan --local --type mcp --mcp-transport sse --mcp-url https://internal.company.com/sse --mcp-tool all --mode full
|
|
258
261
|
$ fullcourtdefense scan --local --type mcp --mcp-command node --mcp-args ./server.js --mcp-tool all --mode full --format report
|
|
259
262
|
$ fullcourtdefense discover
|
|
263
|
+
$ fullcourtdefense discover --surface mcp
|
|
260
264
|
$ fullcourtdefense discover --surface all --upload
|
|
261
265
|
$ fullcourtdefense discover --surface secrets
|
|
262
266
|
$ fullcourtdefense discover --surface agent-files
|
|
@@ -275,6 +279,7 @@ function printHelp() {
|
|
|
275
279
|
$ fullcourtdefense install-claude-desktop-mcp-gateway --mcp-command npm --mcp-args "run mcp"
|
|
276
280
|
$ fullcourtdefense mcp-gateway --mcp-command npm --mcp-args "run mcp"
|
|
277
281
|
$ fullcourtdefense setup
|
|
282
|
+
$ fullcourtdefense install-all --mcp-command npm --mcp-args "run mcp"
|
|
278
283
|
$ fullcourtdefense install-all --mcp-command npm --mcp-args "run mcp" --upload
|
|
279
284
|
$ fullcourtdefense install-mcp-gateway --clients cursor,codex --mcp-command npm --mcp-args "run mcp"
|
|
280
285
|
$ fullcourtdefense install-codex-mcp-gateway --mcp-command npm --mcp-args "run mcp"
|
|
@@ -295,7 +300,6 @@ async function main() {
|
|
|
295
300
|
serverName: flags['server-name'],
|
|
296
301
|
mcpCommand: flags['mcp-command'],
|
|
297
302
|
mcpArgs: flags['mcp-args'],
|
|
298
|
-
mcpCwd: flags['mcp-cwd'],
|
|
299
303
|
agentName: flags['agent-name'],
|
|
300
304
|
developerName: flags['developer-name'],
|
|
301
305
|
shieldId: flags['shield-id'],
|
|
@@ -315,7 +319,6 @@ async function main() {
|
|
|
315
319
|
const buildGatewayArgs = () => ({
|
|
316
320
|
mcpCommand: flags['mcp-command'],
|
|
317
321
|
mcpArgs: flags['mcp-args'],
|
|
318
|
-
mcpCwd: flags['mcp-cwd'],
|
|
319
322
|
agentName: flags['agent-name'],
|
|
320
323
|
agentClient: flags['agent-client'],
|
|
321
324
|
developerName: flags['developer-name'],
|
|
@@ -410,7 +413,6 @@ async function main() {
|
|
|
410
413
|
shieldKey: flags['shield-key'],
|
|
411
414
|
apiUrl: flags['api-url'],
|
|
412
415
|
skipValidate: flags['skip-validate'] === 'true',
|
|
413
|
-
nonInteractive: flags['non-interactive'] === 'true',
|
|
414
416
|
};
|
|
415
417
|
await (0, configure_1.configureCommand)(args, config);
|
|
416
418
|
break;
|
package/dist/version.json
CHANGED
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "fullcourtdefense-cli",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"fullcourtdefense": "dist/index.js",
|
|
8
|
-
"fcd": "dist/index.js",
|
|
9
|
-
"botguard": "dist/index.js"
|
|
10
|
-
},
|
|
11
|
-
"files": [
|
|
12
|
-
"dist",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsc && node scripts/copy-attack-corpus.js",
|
|
17
|
-
"prepublishOnly": "npm run build"
|
|
18
|
-
},
|
|
19
|
-
"keywords": [
|
|
20
|
-
"llm",
|
|
21
|
-
"security",
|
|
22
|
-
"guardrails",
|
|
23
|
-
"ai-safety",
|
|
24
|
-
"prompt-injection",
|
|
25
|
-
"cli",
|
|
26
|
-
"red-teaming",
|
|
27
|
-
"owasp",
|
|
28
|
-
"mcp",
|
|
29
|
-
"rag"
|
|
30
|
-
],
|
|
31
|
-
"license": "MIT",
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"@types/node": "^25.3.0",
|
|
34
|
-
"typescript": "^5.3.0"
|
|
35
|
-
},
|
|
36
|
-
"engines": {
|
|
37
|
-
"node": ">=18.0.0"
|
|
38
|
-
},
|
|
39
|
-
"dependencies": {
|
|
40
|
-
"yaml": "^2.8.4"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "fullcourtdefense-cli",
|
|
3
|
+
"version": "1.3.6",
|
|
4
|
+
"description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fullcourtdefense": "dist/index.js",
|
|
8
|
+
"fcd": "dist/index.js",
|
|
9
|
+
"botguard": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc && node scripts/copy-attack-corpus.js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"llm",
|
|
21
|
+
"security",
|
|
22
|
+
"guardrails",
|
|
23
|
+
"ai-safety",
|
|
24
|
+
"prompt-injection",
|
|
25
|
+
"cli",
|
|
26
|
+
"red-teaming",
|
|
27
|
+
"owasp",
|
|
28
|
+
"mcp",
|
|
29
|
+
"rag"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.3.0",
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"yaml": "^2.8.4"
|
|
41
|
+
}
|
|
42
|
+
}
|