fullcourtdefense-cli 1.3.3 → 1.3.5
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.
|
@@ -6,5 +6,7 @@ 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;
|
|
9
11
|
}
|
|
10
12
|
export declare function configureCommand(args: ConfigureArgs, config: BotGuardConfig): Promise<void>;
|
|
@@ -43,63 +43,107 @@ async function ask(rl, question, fallback) {
|
|
|
43
43
|
const answer = (await rl.question(`${question}${suffix}: `)).trim();
|
|
44
44
|
return answer || fallback || '';
|
|
45
45
|
}
|
|
46
|
+
async function confirm(rl, question, defaultYes = true) {
|
|
47
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
48
|
+
const answer = (await rl.question(`${question} (${hint}): `)).trim().toLowerCase();
|
|
49
|
+
if (!answer)
|
|
50
|
+
return defaultYes;
|
|
51
|
+
return answer === 'y' || answer === 'yes';
|
|
52
|
+
}
|
|
46
53
|
function resolveApiKey(args, config) {
|
|
47
54
|
return args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY || '';
|
|
48
55
|
}
|
|
56
|
+
function defaultsFrom(args, config) {
|
|
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
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function allFlagsProvided(args) {
|
|
66
|
+
return Boolean(args.apiKey && args.organizationId && args.shieldId && args.apiUrl);
|
|
67
|
+
}
|
|
68
|
+
function isValidationUnavailableError(error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
return message.includes('HTML instead of JSON') || message.includes('404');
|
|
71
|
+
}
|
|
49
72
|
async function configureCommand(args, config) {
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
finalApiUrl =
|
|
65
|
-
finalApiKey =
|
|
66
|
-
finalOrgId =
|
|
67
|
-
finalShieldId =
|
|
68
|
-
finalShieldKey =
|
|
73
|
+
const saved = defaultsFrom(args, config);
|
|
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;
|
|
81
|
+
try {
|
|
82
|
+
if (useWizard && rl) {
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log('FullCourtDefense setup — enter each value one at a time.');
|
|
85
|
+
console.log('Press Enter to keep the default shown in parentheses.');
|
|
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('');
|
|
69
93
|
}
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
if (!finalApiKey) {
|
|
95
|
+
throw new Error('Organization API key is required.');
|
|
72
96
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
if (!finalOrgId) {
|
|
98
|
+
throw new Error('Organization ID is required.');
|
|
99
|
+
}
|
|
100
|
+
if (!finalShieldId || finalShieldId === 'sh_...') {
|
|
101
|
+
throw new Error('Shield ID is required.');
|
|
102
|
+
}
|
|
103
|
+
let skipValidate = args.skipValidate === true;
|
|
104
|
+
if (!skipValidate) {
|
|
105
|
+
console.log('Validating organization, Shield ID, and Shield key against FullCourtDefense...');
|
|
106
|
+
const api = new api_1.BotGuardApi(finalApiKey, finalApiUrl);
|
|
107
|
+
try {
|
|
108
|
+
const validated = await api.validateSetup({
|
|
109
|
+
organizationId: finalOrgId,
|
|
110
|
+
shieldId: finalShieldId,
|
|
111
|
+
shieldKey: finalShieldKey || undefined,
|
|
112
|
+
});
|
|
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
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (isValidationUnavailableError(error)) {
|
|
120
|
+
console.log('Server validation is not available yet (API endpoint not deployed).');
|
|
121
|
+
if (useWizard && rl) {
|
|
122
|
+
skipValidate = await confirm(rl, 'Save credentials locally anyway?', true);
|
|
123
|
+
if (!skipValidate)
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)} `
|
|
128
|
+
+ 'Use `fullcourtdefense setup --skip-validate true` or run interactive `fullcourtdefense setup` to save without validation.');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const savedPath = (0, config_1.saveSetupConfig)({
|
|
137
|
+
apiKey: finalApiKey,
|
|
87
138
|
organizationId: finalOrgId,
|
|
88
139
|
shieldId: finalShieldId,
|
|
89
140
|
shieldKey: finalShieldKey || undefined,
|
|
141
|
+
apiUrl: finalApiUrl,
|
|
90
142
|
});
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
143
|
+
console.log(`Saved setup to ${savedPath}`);
|
|
144
|
+
console.log('All other CLI commands (install, discover --upload, scan, hooks) will use these credentials automatically.');
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
rl?.close();
|
|
95
148
|
}
|
|
96
|
-
const savedPath = (0, config_1.saveSetupConfig)({
|
|
97
|
-
apiKey: finalApiKey,
|
|
98
|
-
organizationId: finalOrgId,
|
|
99
|
-
shieldId: finalShieldId,
|
|
100
|
-
shieldKey: finalShieldKey || undefined,
|
|
101
|
-
apiUrl: finalApiUrl,
|
|
102
|
-
});
|
|
103
|
-
console.log(`Saved setup to ${savedPath}`);
|
|
104
|
-
console.log('All other CLI commands (install, discover --upload, scan, hooks) will use these credentials automatically.');
|
|
105
149
|
}
|
|
@@ -5,6 +5,8 @@ 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;
|
|
8
10
|
agentName?: string;
|
|
9
11
|
agentClient?: string;
|
|
10
12
|
developerName?: string;
|
|
@@ -733,18 +733,42 @@ function removeTomlSection(content, tableName) {
|
|
|
733
733
|
}
|
|
734
734
|
return out.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
735
735
|
}
|
|
736
|
-
function writeCodexMcpServer(configPath, serverName, nodeExe, commandArgs) {
|
|
736
|
+
function writeCodexMcpServer(configPath, serverName, nodeExe, commandArgs, options = {}) {
|
|
737
737
|
const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
738
738
|
const cleaned = removeTomlSection(existing, `mcp_servers.${serverName}`);
|
|
739
739
|
const argsToml = commandArgs.map(arg => JSON.stringify(arg)).join(', ');
|
|
740
|
-
const
|
|
740
|
+
const blockLines = [
|
|
741
741
|
`[mcp_servers.${serverName}]`,
|
|
742
742
|
`command = ${JSON.stringify(nodeExe)}`,
|
|
743
743
|
`args = [${argsToml}]`,
|
|
744
|
-
]
|
|
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');
|
|
745
752
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
746
753
|
fs.writeFileSync(configPath, `${cleaned ? `${cleaned}\n\n` : ''}${block}\n`, 'utf8');
|
|
747
754
|
}
|
|
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
|
+
}
|
|
748
772
|
function codexConfigPath(projectScope) {
|
|
749
773
|
return projectScope
|
|
750
774
|
? path.join(process.cwd(), '.codex', 'config.toml')
|
|
@@ -1034,7 +1058,10 @@ async function installCodexMcpGatewayCommand(args, config) {
|
|
|
1034
1058
|
throw new Error('Pass --mcp-command for the real MCP server you want to protect.');
|
|
1035
1059
|
const downstreamArgs = parseArgsList(args.mcpArgs || process.env.FCD_MCP_ARGS);
|
|
1036
1060
|
const commandArgs = buildGatewayCommandArgs(gatewayConfig, downstreamCommand, downstreamArgs, INSTALL_GATEWAY_ARGS);
|
|
1037
|
-
writeCodexMcpServer(file, serverName, nodeExe, commandArgs
|
|
1061
|
+
writeCodexMcpServer(file, serverName, nodeExe, commandArgs, {
|
|
1062
|
+
cwd: resolveMcpCwd(args),
|
|
1063
|
+
startupTimeoutSec: 120,
|
|
1064
|
+
});
|
|
1038
1065
|
console.log(`AgentGuard MCP Gateway installed for Codex (${projectScope ? 'project' : 'global'}).`);
|
|
1039
1066
|
console.log(`Config: ${file}`);
|
|
1040
1067
|
console.log(`Server: ${serverName}`);
|
package/dist/index.js
CHANGED
|
@@ -111,7 +111,7 @@ 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 —
|
|
114
|
+
setup Same as configure — step-by-step wizard; validates before saving.
|
|
115
115
|
install-all One-click: MCP gateway in every AI client + Cursor hooks + optional upload.
|
|
116
116
|
install Alias for install-all.
|
|
117
117
|
scan Runs the scan. Use --local for inside-organization scans.
|
|
@@ -154,9 +154,8 @@ 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, prompts one field at a time):
|
|
158
158
|
fullcourtdefense setup
|
|
159
|
-
Saves to ~/.fullcourtdefense.yml — install, discover --upload, scan, and hooks use it automatically.
|
|
160
159
|
|
|
161
160
|
4. One-click protect every AI client on this machine:
|
|
162
161
|
fullcourtdefense install-all --mcp-command npm --mcp-args "run mcp" --upload
|
|
@@ -296,6 +295,7 @@ async function main() {
|
|
|
296
295
|
serverName: flags['server-name'],
|
|
297
296
|
mcpCommand: flags['mcp-command'],
|
|
298
297
|
mcpArgs: flags['mcp-args'],
|
|
298
|
+
mcpCwd: flags['mcp-cwd'],
|
|
299
299
|
agentName: flags['agent-name'],
|
|
300
300
|
developerName: flags['developer-name'],
|
|
301
301
|
shieldId: flags['shield-id'],
|
|
@@ -315,6 +315,7 @@ async function main() {
|
|
|
315
315
|
const buildGatewayArgs = () => ({
|
|
316
316
|
mcpCommand: flags['mcp-command'],
|
|
317
317
|
mcpArgs: flags['mcp-args'],
|
|
318
|
+
mcpCwd: flags['mcp-cwd'],
|
|
318
319
|
agentName: flags['agent-name'],
|
|
319
320
|
agentClient: flags['agent-client'],
|
|
320
321
|
developerName: flags['developer-name'],
|
|
@@ -409,6 +410,7 @@ async function main() {
|
|
|
409
410
|
shieldKey: flags['shield-key'],
|
|
410
411
|
apiUrl: flags['api-url'],
|
|
411
412
|
skipValidate: flags['skip-validate'] === 'true',
|
|
413
|
+
nonInteractive: flags['non-interactive'] === 'true',
|
|
412
414
|
};
|
|
413
415
|
await (0, configure_1.configureCommand)(args, config);
|
|
414
416
|
break;
|
package/dist/version.json
CHANGED