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 apiUrl = args.apiUrl || config.apiUrl || 'https://api.fullcourtdefense.ai';
51
- const apiKey = resolveApiKey(args, config);
52
- const organizationId = args.organizationId || config.organizationId;
53
- const shieldId = args.shieldId || config.shieldId;
54
- const shieldKey = args.shieldKey ?? config.shieldKey;
55
- const hasAllRequired = Boolean(apiKey && organizationId && shieldId);
56
- let finalApiUrl = apiUrl;
57
- let finalApiKey = apiKey;
58
- let finalOrgId = organizationId;
59
- let finalShieldId = shieldId;
60
- let finalShieldKey = shieldKey;
61
- if (!hasAllRequired) {
62
- const rl = readline.createInterface({ input: process_1.stdin, output: process_1.stdout });
63
- try {
64
- finalApiUrl = args.apiUrl || await ask(rl, 'FullCourtDefense API URL', config.apiUrl || 'https://api.fullcourtdefense.ai');
65
- finalApiKey = resolveApiKey(args, config) || await ask(rl, 'Organization API key (from Settings → API Keys)', '');
66
- finalOrgId = args.organizationId || await ask(rl, 'Organization ID (from Settings → Organization)', config.organizationId || '');
67
- finalShieldId = args.shieldId || await ask(rl, 'Shield ID', config.shieldId || 'sh_...');
68
- finalShieldKey = args.shieldKey ?? await ask(rl, 'Shield key if locked, blank if not required', config.shieldKey || '');
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
- finally {
71
- rl.close();
94
+ if (!finalApiKey) {
95
+ throw new Error('Organization API key is required.');
72
96
  }
73
- }
74
- if (!finalApiKey) {
75
- throw new Error('Organization API key is required to validate your setup.');
76
- }
77
- if (!finalOrgId) {
78
- throw new Error('Organization ID is required.');
79
- }
80
- if (!finalShieldId || finalShieldId === 'sh_...') {
81
- throw new Error('Shield ID is required.');
82
- }
83
- if (args.skipValidate !== true) {
84
- console.log('Validating organization, Shield ID, and Shield key against FullCourtDefense...');
85
- const api = new api_1.BotGuardApi(finalApiKey, finalApiUrl);
86
- const validated = await api.validateSetup({
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
- if (validated.organizationId !== finalOrgId) {
92
- throw new Error(`Organization mismatch: API key belongs to "${validated.organizationName}" (${validated.organizationId}), not "${finalOrgId}".`);
93
- }
94
- console.log(`Verified: ${validated.organizationName} → Shield "${validated.shieldName}" (${validated.shieldId})`);
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 block = [
740
+ const blockLines = [
741
741
  `[mcp_servers.${serverName}]`,
742
742
  `command = ${JSON.stringify(nodeExe)}`,
743
743
  `args = [${argsToml}]`,
744
- ].join('\n');
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 — validates credentials against the server before saving.
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.3.3"
2
+ "version": "1.3.5"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {