fullcourtdefense-cli 1.3.5 → 1.3.7

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 CHANGED
@@ -19,23 +19,7 @@ class BotGuardApi {
19
19
  },
20
20
  body: body ? JSON.stringify(body) : undefined,
21
21
  });
22
- const contentType = resp.headers.get('content-type') || '';
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, config: BotGuardConfig): Promise<void>;
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
- 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';
46
+ function looksLikeOrgId(value) {
47
+ return /^[A-Za-z0-9_-]{8,80}$/.test(value);
52
48
  }
53
- function resolveApiKey(args, config) {
54
- return args.apiKey || config.apiKey || process.env.FULLCOURTDEFENSE_API_KEY || '';
49
+ function looksLikeShieldId(value) {
50
+ return /^sh_[A-Za-z0-9]{12,80}$/.test(value);
55
51
  }
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
- };
52
+ function looksLikeShieldKey(value) {
53
+ return /^shsk_[A-Za-z0-9]{24,160}$/.test(value);
64
54
  }
65
- function allFlagsProvided(args) {
66
- return Boolean(args.apiKey && args.organizationId && args.shieldId && args.apiUrl);
55
+ function looksLikeApiKey(value) {
56
+ return /^ag_[A-Za-z0-9_-]{24,200}$/.test(value);
67
57
  }
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');
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, config) {
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;
72
+ async function configureCommand(args, _config) {
73
+ const rl = readline.createInterface({ input: process_1.stdin, output: process_1.stdout });
81
74
  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('');
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
- if (!finalOrgId) {
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
- if (!finalShieldId || finalShieldId === 'sh_...') {
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
- let skipValidate = args.skipValidate === true;
104
- if (!skipValidate) {
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(finalApiKey, finalApiUrl);
92
+ const api = new api_1.BotGuardApi(apiKey, apiUrl);
93
+ let validated;
107
94
  try {
108
- const validated = await api.validateSetup({
109
- organizationId: finalOrgId,
110
- shieldId: finalShieldId,
111
- shieldKey: finalShieldKey || undefined,
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
- 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
- }
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: finalApiKey,
138
- organizationId: finalOrgId,
139
- shieldId: finalShieldId,
140
- shieldKey: finalShieldKey || undefined,
141
- apiUrl: finalApiUrl,
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?.close();
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
- return [];
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
- downstreamArgs = [args[argsIdx + 1]];
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: true,
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 Cursor hooks,
9
- * optional fleet upload. Use per-client commands when you only need one tool.
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 Cursor hooks,
10
- * optional fleet upload. Use per-client commands when you only need one tool.
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[2mGateway + optional Cursor hooks + optional fleet upload\x1b[0m\n');
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 !== 'false') {
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 all\x1b[0m to verify.');
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 = { omitSavedCredentials: true };
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, options = {}) {
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 blockLines = [
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 = scope !== 'project' || Boolean(args.shieldKey);
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
- return mergeConfig(homeConfig, projectConfig);
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 — step-by-step wizard; validates before saving.
115
- install-all One-click: MCP gateway in every AI client + Cursor hooks + optional upload.
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, prompts one field at a 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" --upload
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.3.5"
2
+ "version": "1.3.7"
3
3
  }
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
- {
2
- "name": "fullcourtdefense-cli",
3
- "version": "1.3.5",
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.7",
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
+ }