fullcourtdefense-cli 1.7.5 → 1.7.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.
@@ -972,7 +972,7 @@ function compactScanHtml(args) {
972
972
  .eyebrow { color: #1d4ed8; font-size: 12px; font-weight: 900; letter-spacing: .08em; text-transform: uppercase; }
973
973
  h1 { margin: 6px 0 8px; font-size: 26px; line-height: 1.15; }
974
974
  p { color: #475569; line-height: 1.55; }
975
- .grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
975
+ .grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
976
976
  .steps { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 18px; }
977
977
  .step { border: 1px solid #e2e8f0; background: white; color: #64748b; border-radius: 16px; padding: 12px; font-size: 12px; font-weight: 900; }
978
978
  .step.active { border-color: #2563eb; background: #eff6ff; color: #1d4ed8; }
@@ -1011,6 +1011,17 @@ function compactScanHtml(args) {
1011
1011
  .resultMetric span { font-size: 16px; font-weight: 800; color: #0f172a; }
1012
1012
  .resultMeta { margin-top: 10px; color: #334155; font-size: 12px; line-height: 1.5; }
1013
1013
  .resultMeta a { color: #1d4ed8; }
1014
+ .mcpBanner { margin-top: 10px; border-radius: 14px; padding: 12px; font-size: 13px; line-height: 1.45; }
1015
+ .mcpBanner.ok { border: 1px solid #bbf7d0; background: #f0fdf4; color: #166534; }
1016
+ .mcpBanner.blocked { border: 1px solid #fecaca; background: #fef2f2; color: #991b1b; }
1017
+ .mcpCards { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 10px; }
1018
+ .mcpCard { border: 1px solid #e2e8f0; background: white; border-radius: 12px; padding: 10px; }
1019
+ .mcpCard b { display: block; color: #64748b; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; }
1020
+ .mcpCard span { color: #0f172a; font-size: 13px; font-weight: 750; overflow-wrap: anywhere; }
1021
+ .localTable { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; overflow: hidden; border-radius: 12px; }
1022
+ .localTable th, .localTable td { border-bottom: 1px solid #e2e8f0; padding: 8px; text-align: left; vertical-align: top; }
1023
+ .localTable th { color: #475569; background: #f8fafc; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; }
1024
+ .copyBox { margin-top: 10px; }
1014
1025
  .hidden { display: none; }
1015
1026
  @media (max-width: 800px) { .grid, .two { grid-template-columns: 1fr; } }
1016
1027
  </style>
@@ -1049,7 +1060,8 @@ function compactScanHtml(args) {
1049
1060
  <div class="step" id="step-dot-3">3. Run red-team scan</div>
1050
1061
  </div>
1051
1062
  <div class="grid" id="cards">
1052
- <button class="card active" data-target="endpoint"><b>Endpoint</b><span>Internal agent/chat API</span></button>
1063
+ <button class="card active" data-target="runner"><b>Run MCP Tool</b><span>Direct local call through proxy</span></button>
1064
+ <button class="card" data-target="endpoint"><b>Endpoint</b><span>Internal agent/chat API</span></button>
1053
1065
  <button class="card" data-target="rag"><b>RAG</b><span>Retrieval URL or corpus path</span></button>
1054
1066
  <button class="card" data-target="mcp"><b>MCP</b><span>Tool server via stdio or HTTP</span></button>
1055
1067
  <button class="card" data-target="help"><b>Options</b><span>Same modes as web red-team scan</span></button>
@@ -1064,7 +1076,44 @@ function compactScanHtml(args) {
1064
1076
  <div class="muted">Choose Endpoint, RAG, or MCP. This runs from inside your organization.</div>
1065
1077
  </div>
1066
1078
  </div>
1067
- <div id="endpoint-fields">
1079
+ <div id="runner-fields">
1080
+ <div class="note">Run one MCP tool call from this machine. Use a protected proxy/gateway command or local MCP URL. Results stay local in this browser page.</div>
1081
+ <div class="two">
1082
+ <div>
1083
+ <label>Protected MCP HTTP/SSE URL</label>
1084
+ <input id="runnerMcpUrl" placeholder="http://127.0.0.1:5066/mcp" />
1085
+ </div>
1086
+ <div>
1087
+ <label>Tool name</label>
1088
+ <input id="runnerTool" placeholder="lookup_customer" />
1089
+ </div>
1090
+ </div>
1091
+ <div class="two">
1092
+ <div><label>Or stdio command</label><input id="runnerMcpCommand" placeholder="fullcourtdefense" /></div>
1093
+ <div><label>Command args</label><input id="runnerMcpArgs" placeholder='mcp-gateway --mcp-command node --mcp-args "[&quot;./server.js&quot;]"' /></div>
1094
+ </div>
1095
+ <label>Tool arguments JSON</label>
1096
+ <textarea id="runnerToolArgs" spellcheck="false">{
1097
+ "customerId": "cus_123"
1098
+ }</textarea>
1099
+ <div class="actions">
1100
+ <button class="primary" id="runMcpTool">Run MCP tool</button>
1101
+ <button class="secondary" id="copyMcpResult">Copy result</button>
1102
+ </div>
1103
+ <div id="mcpResultCard" class="resultCard hidden">
1104
+ <div class="resultHead">
1105
+ <div class="resultTitle">MCP tool result</div>
1106
+ <span id="mcpResultStatus" class="statusTag">READY</span>
1107
+ </div>
1108
+ <div id="mcpResultTable"></div>
1109
+ <div class="copyBox">
1110
+ <label>Copyable JSON</label>
1111
+ <textarea id="mcpResultJson" spellcheck="false" readonly></textarea>
1112
+ </div>
1113
+ </div>
1114
+ </div>
1115
+
1116
+ <div id="endpoint-fields" class="hidden">
1068
1117
  <label>Internal endpoint URL</label>
1069
1118
  <input id="endpoint" placeholder="http://127.0.0.1:3000/chat" />
1070
1119
  <div class="two">
@@ -1098,7 +1147,7 @@ function compactScanHtml(args) {
1098
1147
  <div id="help-fields" class="hidden">
1099
1148
  <div class="note">Use Endpoint for internal chat APIs, RAG for retrieval/corpus injection checks, and MCP for tool output and tool-abuse red-team scans. Reports save back to the normal web report history when Shield key is configured.</div>
1100
1149
  </div>
1101
- <div class="actions">
1150
+ <div class="actions" id="scanActions">
1102
1151
  <button class="secondary" id="command">Show CLI command</button>
1103
1152
  <button class="primary" id="connectNext">Continue to scan type</button>
1104
1153
  </div>
@@ -1170,14 +1219,15 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1170
1219
  </section>
1171
1220
  </main>
1172
1221
  <script>
1173
- let target = 'endpoint';
1222
+ let target = 'runner';
1174
1223
  let step = 1;
1175
1224
  const qs = new URLSearchParams(location.search);
1176
- if (['endpoint','rag','mcp'].includes(qs.get('target'))) target = qs.get('target');
1225
+ if (['runner','endpoint','rag','mcp'].includes(qs.get('target'))) target = qs.get('target');
1177
1226
  const byId = id => document.getElementById(id);
1178
1227
  const sync = () => {
1179
1228
  document.querySelectorAll('.card').forEach(c => c.classList.toggle('active', c.dataset.target === target));
1180
- ['endpoint','rag','mcp','help'].forEach(id => byId(id + '-fields').classList.toggle('hidden', id !== target));
1229
+ ['runner','endpoint','rag','mcp','help'].forEach(id => byId(id + '-fields').classList.toggle('hidden', id !== target));
1230
+ byId('scanActions').classList.toggle('hidden', target === 'runner');
1181
1231
  };
1182
1232
  const setStep = next => {
1183
1233
  step = next;
@@ -1195,12 +1245,23 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1195
1245
  byId('mcpUrl').value = qs.get('mcpUrl') || '';
1196
1246
  byId('mcpCommand').value = qs.get('mcpCommand') || '';
1197
1247
  byId('mcpArgs').value = qs.get('mcpArgs') || '';
1248
+ byId('runnerMcpUrl').value = qs.get('mcpUrl') || '';
1249
+ byId('runnerMcpCommand').value = qs.get('mcpCommand') || 'fullcourtdefense';
1250
+ byId('runnerMcpArgs').value = qs.get('mcpArgs') || '';
1251
+ byId('runnerTool').value = qs.get('mcpTool') || '';
1198
1252
  const payload = () => ({
1199
1253
  target, mode: byId('mode').value, format: byId('format').value,
1200
1254
  endpoint: byId('endpoint').value, inputField: byId('inputField').value, outputField: byId('outputField').value,
1201
1255
  ragUrl: byId('ragUrl').value, ragPath: byId('ragPath').value, ragInputField: byId('ragInputField').value, ragOutputField: byId('ragOutputField').value,
1202
1256
  mcpUrl: byId('mcpUrl').value, mcpCommand: byId('mcpCommand').value, mcpArgs: byId('mcpArgs').value, mcpTool: byId('mcpTool').value
1203
1257
  });
1258
+ const mcpRunnerPayload = () => ({
1259
+ mcpUrl: byId('runnerMcpUrl').value,
1260
+ mcpCommand: byId('runnerMcpCommand').value,
1261
+ mcpArgs: byId('runnerMcpArgs').value,
1262
+ toolName: byId('runnerTool').value,
1263
+ toolArgs: byId('runnerToolArgs').value
1264
+ });
1204
1265
  const renderCommand = p => {
1205
1266
  const parts = ['fullcourtdefense scan --local --type', p.target, '--mode', p.mode, '--format', p.format, '--no-open'];
1206
1267
  if (p.target === 'endpoint') parts.push('--endpoint', JSON.stringify(p.endpoint), '--method POST --request-format custom --input-field', p.inputField || 'message', '--output-field', p.outputField || 'response');
@@ -1255,7 +1316,7 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1255
1316
  : 'No saved web report URL found in this output.';
1256
1317
  };
1257
1318
  const resetForNewScan = () => {
1258
- target = 'endpoint';
1319
+ target = 'runner';
1259
1320
  sync();
1260
1321
  setStep(1);
1261
1322
  byId('endpoint').value = '';
@@ -1269,6 +1330,14 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1269
1330
  byId('mcpCommand').value = '';
1270
1331
  byId('mcpArgs').value = '';
1271
1332
  byId('mcpTool').value = 'all';
1333
+ byId('runnerMcpUrl').value = '';
1334
+ byId('runnerMcpCommand').value = 'fullcourtdefense';
1335
+ byId('runnerMcpArgs').value = '';
1336
+ byId('runnerTool').value = '';
1337
+ byId('runnerToolArgs').value = '{\\n "customerId": "cus_123"\\n}';
1338
+ byId('mcpResultCard').classList.add('hidden');
1339
+ byId('mcpResultJson').value = '';
1340
+ byId('mcpResultTable').innerHTML = '';
1272
1341
  byId('mode').value = 'quick';
1273
1342
  byId('format').value = 'report';
1274
1343
  clearScanSummary();
@@ -1331,6 +1400,116 @@ Full uses the full web attack corpus bundled with the CLI.</pre>
1331
1400
  };
1332
1401
  byId('checkBackend').onclick = checkBackend;
1333
1402
  checkBackend().catch(() => {});
1403
+ function mcpRows(value) {
1404
+ if (Array.isArray(value)) return value;
1405
+ const content = value && value.content;
1406
+ if (Array.isArray(content)) {
1407
+ const textItem = content.find(item => item && typeof item.text === 'string');
1408
+ if (textItem) {
1409
+ try {
1410
+ const parsed = JSON.parse(textItem.text);
1411
+ if (Array.isArray(parsed)) return parsed;
1412
+ if (parsed && Array.isArray(parsed.data)) return parsed.data;
1413
+ if (parsed && Array.isArray(parsed.results)) return parsed.results;
1414
+ } catch {}
1415
+ }
1416
+ }
1417
+ if (value && Array.isArray(value.data)) return value.data;
1418
+ if (value && Array.isArray(value.results)) return value.results;
1419
+ return null;
1420
+ }
1421
+ function mcpText(value) {
1422
+ if (value == null) return '';
1423
+ if (typeof value === 'string') return value;
1424
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
1425
+ const content = value.content;
1426
+ if (Array.isArray(content)) {
1427
+ return content
1428
+ .map(item => item && typeof item.text === 'string' ? item.text : '')
1429
+ .filter(Boolean)
1430
+ .join('\\n');
1431
+ }
1432
+ return '';
1433
+ }
1434
+ function mcpObject(value) {
1435
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
1436
+ const contentText = mcpText(value);
1437
+ if (contentText) {
1438
+ try {
1439
+ const parsed = JSON.parse(contentText);
1440
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
1441
+ } catch {}
1442
+ }
1443
+ if (value.data && typeof value.data === 'object' && !Array.isArray(value.data)) return value.data;
1444
+ if (value.result && typeof value.result === 'object' && !Array.isArray(value.result)) return value.result;
1445
+ return value;
1446
+ }
1447
+ function cellValue(value) {
1448
+ if (value == null) return '';
1449
+ if (typeof value === 'object') return JSON.stringify(value);
1450
+ return String(value);
1451
+ }
1452
+ function renderObjectCards(obj) {
1453
+ const preferred = ['id', 'customerId', 'name', 'title', 'status', 'plan', 'email', 'decision', 'matchedPolicy', 'reason', 'ok'];
1454
+ const keys = Array.from(new Set([...preferred.filter(k => Object.prototype.hasOwnProperty.call(obj, k)), ...Object.keys(obj)])).slice(0, 12);
1455
+ return '<div class="mcpCards">' + keys.map(k => '<div class="mcpCard"><b>' + htmlEscape(k) + '</b><span>' + htmlEscape(cellValue(obj[k])) + '</span></div>').join('') + '</div>';
1456
+ }
1457
+ function renderMcpTable(rows) {
1458
+ const preferred = ['id', 'customerId', 'name', 'title', 'status', 'plan', 'email', 'createdAt', 'updatedAt'];
1459
+ const keys = Array.from(new Set([...preferred.filter(k => rows.some(row => row && Object.prototype.hasOwnProperty.call(row, k))), ...rows.flatMap(row => Object.keys(row || {}))])).slice(0, 10);
1460
+ return '<table class="localTable"><thead><tr>' + keys.map(k => '<th>' + htmlEscape(k) + '</th>').join('') + '</tr></thead><tbody>' +
1461
+ rows.slice(0, 50).map(row => '<tr>' + keys.map(k => '<td>' + htmlEscape(cellValue(row[k])) + '</td>').join('') + '</tr>').join('') +
1462
+ '</tbody></table>' +
1463
+ (rows.length > 50 ? '<div class="resultMeta">Showing first 50 rows. Full result is in raw JSON below.</div>' : '');
1464
+ }
1465
+ function renderMcpResult(data) {
1466
+ byId('mcpResultCard').classList.remove('hidden');
1467
+ byId('mcpResultStatus').textContent = data.blocked ? 'BLOCKED' : data.success ? 'ALLOW / RESULT' : 'ERROR';
1468
+ byId('mcpResultStatus').className = data.success ? 'statusTag pass' : 'statusTag fail';
1469
+ byId('mcpResultJson').value = JSON.stringify(data, null, 2);
1470
+ const rows = data.success ? mcpRows(data.result) : null;
1471
+ if (rows && rows.length && rows.every(row => row && typeof row === 'object' && !Array.isArray(row))) {
1472
+ byId('mcpResultTable').innerHTML = '<div class="mcpBanner ok"><b>Allowed.</b> Tool returned ' + rows.length + ' row(s).</div>' + renderMcpTable(rows);
1473
+ } else if (data.success) {
1474
+ const obj = mcpObject(data.result);
1475
+ const text = mcpText(data.result);
1476
+ byId('mcpResultTable').innerHTML = '<div class="mcpBanner ok"><b>Allowed.</b> Tool call completed through the configured MCP server/proxy.</div>' +
1477
+ (obj ? renderObjectCards(obj) : '<div class="resultMeta">' + htmlEscape(text || 'Result is shown as raw JSON below.') + '</div>');
1478
+ } else {
1479
+ const blockedCopy = data.blocked
1480
+ ? 'Blocked because a FullCourtDefense policy or local safety rule is defined for this tool/action.'
1481
+ : 'The MCP call failed before a successful tool result was returned.';
1482
+ byId('mcpResultTable').innerHTML = '<div class="mcpBanner blocked"><b>' + (data.blocked ? 'Blocked by policy.' : 'MCP error.') + '</b> ' + htmlEscape(blockedCopy) + '<br/><span>' + htmlEscape(data.reason || data.error || 'No reason returned.') + '</span></div>';
1483
+ }
1484
+ }
1485
+ byId('copyMcpResult').onclick = async () => {
1486
+ await navigator.clipboard?.writeText(byId('mcpResultJson').value || '').catch(() => undefined);
1487
+ byId('copyMcpResult').textContent = 'Copied';
1488
+ setTimeout(() => { byId('copyMcpResult').textContent = 'Copy result'; }, 1200);
1489
+ };
1490
+ byId('runMcpTool').onclick = async () => {
1491
+ byId('runMcpTool').disabled = true;
1492
+ byId('runMcpTool').textContent = 'Running...';
1493
+ byId('mcpResultCard').classList.add('hidden');
1494
+ setConsole('Calling MCP tool locally through configured proxy/server...\\n', 'Running MCP');
1495
+ try {
1496
+ const res = await fetch('/mcp/run', {
1497
+ method: 'POST',
1498
+ headers: { 'content-type': 'application/json' },
1499
+ body: JSON.stringify(mcpRunnerPayload())
1500
+ });
1501
+ const data = await res.json();
1502
+ renderMcpResult(data);
1503
+ setConsole(JSON.stringify(data, null, 2), data.success ? 'MCP complete' : 'MCP blocked/error');
1504
+ } catch (error) {
1505
+ const data = { success: false, error: error && error.message ? error.message : String(error) };
1506
+ renderMcpResult(data);
1507
+ setConsole(JSON.stringify(data, null, 2), 'MCP error');
1508
+ } finally {
1509
+ byId('runMcpTool').disabled = false;
1510
+ byId('runMcpTool').textContent = 'Run MCP tool';
1511
+ }
1512
+ };
1334
1513
  byId('run').onclick = async () => {
1335
1514
  if (byId('run').disabled) return;
1336
1515
  byId('run').disabled = true;
@@ -1455,6 +1634,107 @@ async function runCliFromBrowser(body, onChunk) {
1455
1634
  });
1456
1635
  });
1457
1636
  }
1637
+ function createMcpClient(options) {
1638
+ if (options.transport === 'sse')
1639
+ return new SseMcpClient(options);
1640
+ if (options.transport === 'http')
1641
+ return new HttpMcpClient(options);
1642
+ return new StdioMcpClient(options.command || '', options.args || []);
1643
+ }
1644
+ function parseMcpErrorResult(result) {
1645
+ if (!result || typeof result !== 'object' || !result.isError)
1646
+ return null;
1647
+ const content = result.content;
1648
+ const text = Array.isArray(content)
1649
+ ? content.map(item => item && typeof item === 'object' && typeof item.text === 'string' ? item.text : '').filter(Boolean).join('\n')
1650
+ : '';
1651
+ if (text) {
1652
+ try {
1653
+ const parsed = JSON.parse(text);
1654
+ if (parsed && typeof parsed === 'object') {
1655
+ return {
1656
+ blocked: parsed.blocked === true || /policy|blocked|approval/i.test(text),
1657
+ reason: typeof parsed.reason === 'string' ? parsed.reason : text,
1658
+ details: parsed,
1659
+ };
1660
+ }
1661
+ }
1662
+ catch {
1663
+ // plain-text MCP error
1664
+ }
1665
+ }
1666
+ return {
1667
+ blocked: /policy|blocked|approval|not allowed|denied/i.test(text),
1668
+ reason: text || 'The MCP server returned an error result.',
1669
+ details: result,
1670
+ };
1671
+ }
1672
+ async function runMcpToolFromBrowser(body) {
1673
+ const rawUrl = typeof body.mcpUrl === 'string' ? body.mcpUrl.trim() : '';
1674
+ const rawCommand = typeof body.mcpCommand === 'string' ? body.mcpCommand.trim() : '';
1675
+ const rawArgs = typeof body.mcpArgs === 'string' ? body.mcpArgs.trim() : '';
1676
+ const toolName = typeof body.toolName === 'string' ? body.toolName.trim() : '';
1677
+ if (!toolName)
1678
+ throw new Error('Tool name is required.');
1679
+ if (!rawUrl && !rawCommand)
1680
+ throw new Error('Provide either a protected MCP URL or stdio command.');
1681
+ const transport = rawUrl
1682
+ ? (/\/sse\/?$/.test(new URL(rawUrl).pathname) ? 'sse' : 'http')
1683
+ : 'stdio';
1684
+ const toolArgs = parseJsonObject(typeof body.toolArgs === 'string' ? body.toolArgs : JSON.stringify(body.toolArgs || {}));
1685
+ const options = {
1686
+ transport,
1687
+ url: rawUrl || undefined,
1688
+ command: rawCommand || undefined,
1689
+ args: parseArgsList(rawArgs),
1690
+ authType: 'none',
1691
+ toolName,
1692
+ toolArgs,
1693
+ };
1694
+ const client = createMcpClient(options);
1695
+ const startedAt = Date.now();
1696
+ try {
1697
+ await client.initialize();
1698
+ const result = await client.callTool(toolName, toolArgs);
1699
+ const errorResult = parseMcpErrorResult(result);
1700
+ if (errorResult) {
1701
+ return {
1702
+ success: false,
1703
+ blocked: errorResult.blocked,
1704
+ toolName,
1705
+ transport,
1706
+ destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1707
+ elapsedMs: Date.now() - startedAt,
1708
+ reason: errorResult.reason,
1709
+ result,
1710
+ details: errorResult.details,
1711
+ };
1712
+ }
1713
+ return {
1714
+ success: true,
1715
+ toolName,
1716
+ transport,
1717
+ destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1718
+ elapsedMs: Date.now() - startedAt,
1719
+ result,
1720
+ };
1721
+ }
1722
+ catch (error) {
1723
+ return {
1724
+ success: false,
1725
+ blocked: /policy|blocked|approval|not allowed|denied/i.test(error instanceof Error ? error.message : String(error)),
1726
+ toolName,
1727
+ transport,
1728
+ destination: rawUrl || `${rawCommand} ${options.args?.join(' ') || ''}`.trim(),
1729
+ elapsedMs: Date.now() - startedAt,
1730
+ reason: error instanceof Error ? error.message : String(error),
1731
+ error: error instanceof Error ? error.message : String(error),
1732
+ };
1733
+ }
1734
+ finally {
1735
+ client.close();
1736
+ }
1737
+ }
1458
1738
  function startCompactWebServer(args) {
1459
1739
  const server = http.createServer(async (req, res) => {
1460
1740
  try {
@@ -1504,6 +1784,13 @@ function startCompactWebServer(args) {
1504
1784
  res.end(JSON.stringify(check));
1505
1785
  return;
1506
1786
  }
1787
+ if (req.method === 'POST' && url.pathname === '/mcp/run') {
1788
+ const body = await readRequestBody(req);
1789
+ const result = await runMcpToolFromBrowser(body);
1790
+ res.writeHead(result.success ? 200 : 400, { 'content-type': 'application/json; charset=utf-8' });
1791
+ res.end(JSON.stringify(result));
1792
+ return;
1793
+ }
1507
1794
  if (req.method === 'POST' && url.pathname === '/run') {
1508
1795
  const body = await readRequestBody(req);
1509
1796
  res.writeHead(200, {
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.7.5"
2
+ "version": "1.7.7"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {