multicorn-shield 0.10.0 → 0.12.0

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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from 'fs';
3
- import { mkdir, writeFile, readFile, copyFile, unlink } from 'fs/promises';
3
+ import { mkdir, writeFile, readFile, copyFile, chmod, unlink } from 'fs/promises';
4
4
  import { join, dirname } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { fileURLToPath } from 'url';
@@ -473,19 +473,86 @@ async function installWindsurfNativeHooks() {
473
473
  await mkdir(hooksDir, { recursive: true });
474
474
  await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
475
475
  }
476
- var PLATFORM_LABELS = ["OpenClaw", "Claude Code", "Cursor", "Windsurf", "Local MCP / Other"];
476
+ function getClineHooksInstallDir() {
477
+ return join(homedir(), ".multicorn", "cline-hooks");
478
+ }
479
+ function getClineGlobalHooksDir() {
480
+ return join(homedir(), "Documents", "Cline", "Hooks");
481
+ }
482
+ async function installClineNativeHooks() {
483
+ const root = multicornShieldPackageRoot();
484
+ const srcPre = join(root, "plugins", "cline", "hooks", "scripts", "pre-tool-use.cjs");
485
+ const srcPost = join(root, "plugins", "cline", "hooks", "scripts", "post-tool-use.cjs");
486
+ const srcShared = join(root, "plugins", "cline", "hooks", "scripts", "shared.cjs");
487
+ if (!existsSync(srcPre) || !existsSync(srcPost) || !existsSync(srcShared)) {
488
+ throw new Error(
489
+ `Could not find Shield Cline hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
490
+ );
491
+ }
492
+ const installDir = getClineHooksInstallDir();
493
+ await mkdir(installDir, { recursive: true });
494
+ const destPre = join(installDir, "pre-tool-use.cjs");
495
+ const destPost = join(installDir, "post-tool-use.cjs");
496
+ const destShared = join(installDir, "shared.cjs");
497
+ await copyFile(srcPre, destPre);
498
+ await copyFile(srcPost, destPost);
499
+ await copyFile(srcShared, destShared);
500
+ const hookScriptMode = 493;
501
+ await chmod(destPre, hookScriptMode);
502
+ await chmod(destPost, hookScriptMode);
503
+ await chmod(destShared, hookScriptMode);
504
+ const hooksDir = getClineGlobalHooksDir();
505
+ await mkdir(hooksDir, { recursive: true });
506
+ const preWrapper = join(hooksDir, "PreToolUse");
507
+ const postWrapper = join(hooksDir, "PostToolUse");
508
+ const preContent = `#!/usr/bin/env node
509
+ require(${JSON.stringify(destPre)});
510
+ `;
511
+ const postContent = `#!/usr/bin/env node
512
+ require(${JSON.stringify(destPost)});
513
+ `;
514
+ await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
515
+ await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
516
+ }
517
+ async function promptClineIntegrationMode(ask) {
518
+ process.stderr.write("\n" + style.bold("Cline integration") + "\n");
519
+ process.stderr.write(
520
+ " " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
521
+ );
522
+ process.stderr.write(
523
+ " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
524
+ );
525
+ let choice = 0;
526
+ while (choice === 0) {
527
+ const input = await ask("Choose integration (1-2): ");
528
+ const num = parseInt(input.trim(), 10);
529
+ if (num === 1) choice = 1;
530
+ if (num === 2) choice = 2;
531
+ }
532
+ return choice === 1 ? "native" : "hosted";
533
+ }
534
+ var PLATFORM_LABELS = [
535
+ "OpenClaw",
536
+ "Claude Code",
537
+ "Cursor",
538
+ "Windsurf",
539
+ "Cline",
540
+ "Local MCP / Other"
541
+ ];
477
542
  var PLATFORM_BY_SELECTION = {
478
543
  1: "openclaw",
479
544
  2: "claude-code",
480
545
  3: "cursor",
481
546
  4: "windsurf",
482
- 5: "other-mcp"
547
+ 5: "cline",
548
+ 6: "other-mcp"
483
549
  };
484
550
  var DEFAULT_AGENT_NAMES = {
485
551
  openclaw: "my-openclaw-agent",
486
552
  "claude-code": "my-claude-code-agent",
487
553
  cursor: "my-cursor-agent",
488
- windsurf: "my-windsurf-agent"
554
+ windsurf: "my-windsurf-agent",
555
+ cline: "my-cline-agent"
489
556
  };
490
557
  async function promptPlatformSelection(ask) {
491
558
  process.stderr.write(
@@ -505,13 +572,13 @@ async function promptPlatformSelection(ask) {
505
572
  );
506
573
  }
507
574
  process.stderr.write(
508
- style.dim(" Pick 5 if you want to wrap a local MCP server with multicorn-proxy --wrap.") + "\n"
575
+ style.dim(" Pick 6 if you want to wrap a local MCP server with multicorn-proxy --wrap.") + "\n"
509
576
  );
510
577
  let selection = 0;
511
578
  while (selection === 0) {
512
- const input = await ask("Select (1-5): ");
579
+ const input = await ask("Select (1-6): ");
513
580
  const num = parseInt(input.trim(), 10);
514
- if (num >= 1 && num <= 5) {
581
+ if (num >= 1 && num <= 6) {
515
582
  selection = num;
516
583
  }
517
584
  }
@@ -632,7 +699,7 @@ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverNa
632
699
  return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
633
700
  }
634
701
  function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
635
- const usesInlineKey = platform === "cursor" || platform === "windsurf";
702
+ const usesInlineKey = platform === "cursor" || platform === "windsurf" || platform === "cline";
636
703
  const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
637
704
  const urlKey = platform === "windsurf" ? "serverUrl" : "url";
638
705
  const mcpSnippet = JSON.stringify(
@@ -657,6 +724,23 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
657
724
  process.stderr.write(
658
725
  "\n" + style.dim("Add this to ~/.codeium/windsurf/mcp_config.json:") + "\n\n"
659
726
  );
727
+ } else if (platform === "cline") {
728
+ process.stderr.write("\n" + style.dim("Add this to your Cline MCP settings file:") + "\n");
729
+ process.stderr.write(
730
+ style.dim(
731
+ " macOS: ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
732
+ ) + "\n"
733
+ );
734
+ process.stderr.write(
735
+ style.dim(
736
+ " Windows: %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json"
737
+ ) + "\n"
738
+ );
739
+ process.stderr.write(
740
+ style.dim(
741
+ " Linux: ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
742
+ ) + "\n\n"
743
+ );
660
744
  } else {
661
745
  process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
662
746
  }
@@ -680,6 +764,13 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
680
764
  ) + "\n"
681
765
  );
682
766
  }
767
+ if (platform === "cline") {
768
+ process.stderr.write(
769
+ style.dim(
770
+ "After pasting, restart Cline or reload the VS Code window. Cline will discover the Shield tools automatically."
771
+ ) + "\n"
772
+ );
773
+ }
683
774
  if (platform === "windsurf") {
684
775
  process.stderr.write(style.dim("Then restart Windsurf (Cmd/Ctrl+Q, then reopen).") + "\n");
685
776
  process.stderr.write(
@@ -777,7 +868,7 @@ async function runInit(explicitBaseUrl) {
777
868
  const selection = await promptPlatformSelection(ask);
778
869
  const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
779
870
  const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
780
- if (selection === 5) {
871
+ if (selection === 6) {
781
872
  const raw = existing !== null ? { ...existing } : {};
782
873
  raw["apiKey"] = apiKey;
783
874
  raw["baseUrl"] = resolvedBaseUrl;
@@ -1001,6 +1092,71 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
1001
1092
  setupSucceeded = true;
1002
1093
  }
1003
1094
  }
1095
+ } else if (selection === 5) {
1096
+ const clineMode = await promptClineIntegrationMode(ask);
1097
+ if (clineMode === "native") {
1098
+ try {
1099
+ await installClineNativeHooks();
1100
+ process.stderr.write("\n" + style.bold("Shield Cline hooks installed") + "\n\n");
1101
+ process.stderr.write(
1102
+ style.dim(
1103
+ "The Shield hook runs with your user permissions. It intercepts Cline tool calls to check permissions and log activity. Review the scripts under "
1104
+ ) + style.cyan("~/.multicorn/cline-hooks") + style.dim(" if that is a concern.") + "\n"
1105
+ );
1106
+ configuredAgents.push({
1107
+ selection,
1108
+ platform: selectedPlatform,
1109
+ platformLabel: selectedLabel,
1110
+ agentName,
1111
+ clineIntegration: "native"
1112
+ });
1113
+ setupSucceeded = true;
1114
+ } catch (error) {
1115
+ const detail = error instanceof Error ? error.message : String(error);
1116
+ process.stderr.write(style.red("\u2717 ") + detail + "\n");
1117
+ }
1118
+ } else {
1119
+ const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
1120
+ let proxyUrl = "";
1121
+ let created = false;
1122
+ while (!created) {
1123
+ const spinner = withSpinner("Creating proxy config...");
1124
+ try {
1125
+ proxyUrl = await createProxyConfig(
1126
+ resolvedBaseUrl,
1127
+ apiKey,
1128
+ agentName,
1129
+ targetUrl,
1130
+ shortName,
1131
+ selectedPlatform
1132
+ );
1133
+ spinner.stop(true, "Proxy config created!");
1134
+ created = true;
1135
+ } catch (error) {
1136
+ const detail = error instanceof Error ? error.message : String(error);
1137
+ spinner.stop(false, detail);
1138
+ const retry = await ask("Try again? (Y/n) ");
1139
+ if (retry.trim().toLowerCase() === "n") {
1140
+ break;
1141
+ }
1142
+ }
1143
+ }
1144
+ if (created && proxyUrl.length > 0) {
1145
+ process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
1146
+ process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
1147
+ printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
1148
+ configuredAgents.push({
1149
+ selection,
1150
+ platform: selectedPlatform,
1151
+ platformLabel: selectedLabel,
1152
+ agentName,
1153
+ shortName,
1154
+ proxyUrl,
1155
+ clineIntegration: "hosted"
1156
+ });
1157
+ setupSucceeded = true;
1158
+ }
1159
+ }
1004
1160
  } else {
1005
1161
  const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
1006
1162
  let proxyUrl = "";
@@ -1119,6 +1275,22 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
1119
1275
  "\n" + style.bold("To complete your Windsurf hosted-proxy setup:") + "\n 1. If you don't have Windsurf yet, download it from " + style.cyan("https://windsurf.com/download") + "\n 2. Open " + style.cyan("~/.codeium/windsurf/mcp_config.json") + " and paste the config snippet shown above\n 3. Restart Windsurf (or launch it for the first time) to load the new MCP server\n"
1120
1276
  );
1121
1277
  }
1278
+ const clineNativeConfigured = configuredAgents.some(
1279
+ (a) => a.platform === "cline" && a.clineIntegration === "native"
1280
+ );
1281
+ const clineHostedConfigured = configuredAgents.some(
1282
+ (a) => a.platform === "cline" && a.clineIntegration === "hosted"
1283
+ );
1284
+ if (clineNativeConfigured) {
1285
+ blocks.push(
1286
+ "\n" + style.bold("To complete native Cline (Shield) setup:") + "\n 1. Enable Hooks in Cline: open VS Code, click the Cline sidebar icon, click the gear icon,\n scroll down to the Advanced section, and toggle Hooks on.\n 2. Reload the VS Code window (Cmd+Shift+P > Reload Window)\n 3. Trigger any tool call to verify Shield is intercepting\n"
1287
+ );
1288
+ }
1289
+ if (clineHostedConfigured) {
1290
+ blocks.push(
1291
+ "\n" + style.bold("To complete your Cline hosted-proxy setup:") + "\n 1. If you don't have Cline yet, install it from the VS Code marketplace\n 2. Open your Cline MCP settings file and paste the config snippet shown above\n 3. Restart Cline or reload the VS Code window\n"
1292
+ );
1293
+ }
1122
1294
  if (blocks.length > 0) {
1123
1295
  process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
1124
1296
  process.stderr.write(blocks.join("") + "\n");
@@ -377,7 +377,6 @@ ${url}
377
377
  }
378
378
  async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
379
379
  const dashboardUrl = deriveDashboardUrl(baseUrl);
380
- console.error("[SHIELD] buildConsentUrl baseUrl:", baseUrl);
381
380
  const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
382
381
  process.stderr.write(
383
382
  `[multicorn-shield] Opening consent page...
@@ -315,14 +315,6 @@ async function fetchGrantedScopes(agentId, apiKey, baseUrl, logger) {
315
315
  }
316
316
  async function checkActionPermission(payload, apiKey, baseUrl, logger) {
317
317
  try {
318
- const requestBody = {
319
- agent: payload.agent,
320
- service: payload.service,
321
- actionType: payload.actionType,
322
- status: payload.status,
323
- metadata: payload.metadata
324
- };
325
- console.error("[SHIELD-CLIENT] POST /api/v1/actions request: " + JSON.stringify(requestBody));
326
318
  const response = await fetch(`${baseUrl}/api/v1/actions`, {
327
319
  method: "POST",
328
320
  headers: {
@@ -333,22 +325,18 @@ async function checkActionPermission(payload, apiKey, baseUrl, logger) {
333
325
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
334
326
  });
335
327
  if (response.status === 201) {
336
- console.error(
337
- "[SHIELD-CLIENT] response status=201, returning approved (body not read - backend may have failed approval creation)"
338
- );
328
+ console.error("[SHIELD-CLIENT] POST /api/v1/actions: 201 approved");
339
329
  return { status: "approved" };
340
330
  }
341
331
  if (response.status === 202) {
342
332
  const body = await response.json();
343
333
  const data = isApiSuccess(body) ? body.data : null;
344
- console.error("[SHIELD-CLIENT] response status=202 body=" + JSON.stringify(data ?? body));
334
+ console.error("[SHIELD-CLIENT] POST /api/v1/actions: 202 pending");
345
335
  if (!isApiSuccess(body) || data === null) {
346
336
  return { status: "blocked" };
347
337
  }
348
338
  const approvalId = typeof data["approval_id"] === "string" ? data["approval_id"] : void 0;
349
- console.error(
350
- "[SHIELD-CLIENT] extracted: status=" + String(data["status"]) + " approval_id=" + (approvalId ?? "undefined")
351
- );
339
+ console.error("[SHIELD-CLIENT] extracted: approval_id=" + (approvalId ?? "undefined"));
352
340
  if (approvalId === void 0) {
353
341
  return { status: "blocked" };
354
342
  }
@@ -439,7 +427,6 @@ ${url}
439
427
  }
440
428
  async function waitForConsent(agentId, agentName, apiKey, baseUrl, scope, logger) {
441
429
  const dashboardUrl = deriveDashboardUrl(baseUrl);
442
- console.error("[SHIELD] buildConsentUrl baseUrl:", baseUrl);
443
430
  const consentUrl = buildConsentUrl(agentName, dashboardUrl, scope);
444
431
  process.stderr.write(
445
432
  `[multicorn-shield] Opening consent page...
@@ -509,7 +496,14 @@ function readConfig() {
509
496
  const resolvedBaseUrl = asString(cachedMulticornConfig?.baseUrl) ?? asString(process.env["MULTICORN_BASE_URL"]) ?? "https://api.multicorn.ai";
510
497
  const agentName = asString(pc["agentName"]) ?? process.env["MULTICORN_AGENT_NAME"] ?? agentNameFromOpenclawPlatform(cachedMulticornConfig) ?? asString(cachedMulticornConfig?.agentName) ?? null;
511
498
  const failMode = "closed";
512
- return { apiKey: resolvedApiKey, baseUrl: resolvedBaseUrl, agentName, failMode };
499
+ let apiKey = resolvedApiKey;
500
+ if (apiKey.length > 0 && (!apiKey.startsWith("mcs_") || apiKey.length < 16)) {
501
+ pluginLogger?.error(
502
+ "Invalid API key format. Key must start with mcs_ and be at least 16 characters."
503
+ );
504
+ apiKey = "";
505
+ }
506
+ return { apiKey, baseUrl: resolvedBaseUrl, agentName, failMode };
513
507
  }
514
508
  function asString(value) {
515
509
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -884,7 +878,9 @@ var plugin = {
884
878
  if (config.agentName !== null) {
885
879
  pinnedAgentName = config.agentName;
886
880
  }
887
- console.error("[SHIELD-DIAG] cachedMulticornConfig: " + JSON.stringify(cachedMulticornConfig));
881
+ api.logger.info(
882
+ `Multicorn Shield config loaded: hasApiKey=${String((cachedMulticornConfig?.apiKey ?? "").length > 0)} baseUrl=${cachedMulticornConfig?.baseUrl ?? "default"} agentName=${cachedMulticornConfig?.agentName ?? "unset"} defaultAgent=${cachedMulticornConfig?.defaultAgent ?? "unset"} agents=${String(cachedMulticornConfig?.agents?.length ?? 0)}`
883
+ );
888
884
  api.on("before_tool_call", beforeToolCall, { priority: 10 });
889
885
  api.on("after_tool_call", afterToolCall);
890
886
  api.logger.info("Multicorn Shield plugin registered.");
@@ -8,7 +8,9 @@
8
8
  "additionalProperties": false,
9
9
  "properties": {
10
10
  "apiKey": {
11
- "type": "string"
11
+ "type": "string",
12
+ "pattern": "^mcs_.{12,}$",
13
+ "minLength": 16
12
14
  },
13
15
  "baseUrl": {
14
16
  "type": "string"
package/dist/proxy.cjs CHANGED
@@ -437,12 +437,186 @@ function mapMcpToolToScope(toolName) {
437
437
  return { service: head, permissionLevel, actionType };
438
438
  }
439
439
 
440
+ // src/logger/action-logger.ts
441
+ function createActionLogger(config) {
442
+ if (!config.apiKey || config.apiKey.trim().length === 0) {
443
+ throw new Error(
444
+ "[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
445
+ );
446
+ }
447
+ const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
448
+ const timeout = config.timeout ?? 5e3;
449
+ if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
450
+ throw new Error(
451
+ `[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
452
+ );
453
+ }
454
+ const endpoint = `${baseUrl}/api/v1/actions`;
455
+ const batchEnabled = config.batchMode?.enabled ?? false;
456
+ const maxBatchSize = config.batchMode?.maxSize ?? 10;
457
+ const flushInterval = config.batchMode?.flushIntervalMs ?? 5e3;
458
+ const queue = [];
459
+ let flushTimer;
460
+ let isShutdown = false;
461
+ async function sendActions(actions) {
462
+ if (actions.length === 0) return;
463
+ const convertAction = (action) => ({
464
+ agent: action.agent,
465
+ service: action.service,
466
+ actionType: action.actionType,
467
+ status: action.status,
468
+ ...action.cost !== void 0 ? { cost: action.cost } : {},
469
+ ...action.metadata !== void 0 ? { metadata: action.metadata } : {}
470
+ });
471
+ const convertedActions = actions.map(convertAction);
472
+ const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
473
+ let lastError;
474
+ for (let attempt = 0; attempt < 2; attempt++) {
475
+ try {
476
+ const controller = new AbortController();
477
+ const timeoutId = setTimeout(() => {
478
+ controller.abort();
479
+ }, timeout);
480
+ const response = await fetch(endpoint, {
481
+ method: "POST",
482
+ headers: {
483
+ "Content-Type": "application/json",
484
+ "X-Multicorn-Key": config.apiKey
485
+ },
486
+ body: JSON.stringify(payload),
487
+ signal: controller.signal
488
+ });
489
+ clearTimeout(timeoutId);
490
+ if (response.ok) {
491
+ return;
492
+ }
493
+ if (response.status >= 400 && response.status < 500) {
494
+ const body = await response.text().catch(() => "");
495
+ throw new Error(
496
+ `[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
497
+ );
498
+ }
499
+ if (response.status >= 500 && attempt === 0) {
500
+ lastError = new Error(
501
+ `[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
502
+ );
503
+ await sleep(100 * Math.pow(2, attempt));
504
+ continue;
505
+ }
506
+ throw new Error(
507
+ `[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
508
+ );
509
+ } catch (error) {
510
+ if (error instanceof Error) {
511
+ if (error.name === "AbortError") {
512
+ lastError = new Error(
513
+ `[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
514
+ );
515
+ } else if (error.message.includes("Client error") || error.message.includes("Server error")) {
516
+ lastError = error;
517
+ } else {
518
+ lastError = new Error(
519
+ `[ActionLogger] Network error: ${error.message}. Check your network connection and API endpoint.`
520
+ );
521
+ }
522
+ } else {
523
+ lastError = new Error(`[ActionLogger] Unknown error: ${String(error)}`);
524
+ }
525
+ if (attempt === 0 && !lastError.message.includes("Client error")) {
526
+ await sleep(100 * Math.pow(2, attempt));
527
+ continue;
528
+ }
529
+ break;
530
+ }
531
+ }
532
+ if (lastError) {
533
+ if (config.onError) {
534
+ config.onError(lastError);
535
+ }
536
+ }
537
+ }
538
+ async function flushQueue() {
539
+ if (queue.length === 0) return;
540
+ const actions = queue.map((item) => item.payload);
541
+ queue.length = 0;
542
+ await sendActions(actions);
543
+ }
544
+ function startFlushTimer() {
545
+ if (flushTimer !== void 0) return;
546
+ flushTimer = setInterval(() => {
547
+ flushQueue().catch(() => {
548
+ });
549
+ }, flushInterval);
550
+ const timer = flushTimer;
551
+ if (typeof timer.unref === "function") {
552
+ timer.unref();
553
+ }
554
+ }
555
+ function stopFlushTimer() {
556
+ if (flushTimer) {
557
+ clearInterval(flushTimer);
558
+ flushTimer = void 0;
559
+ }
560
+ }
561
+ if (batchEnabled) {
562
+ startFlushTimer();
563
+ }
564
+ return {
565
+ logAction(action) {
566
+ if (isShutdown) {
567
+ throw new Error(
568
+ "[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
569
+ );
570
+ }
571
+ if (action.agent.trim().length === 0) {
572
+ throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
573
+ }
574
+ if (action.service.trim().length === 0) {
575
+ throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
576
+ }
577
+ if (action.actionType.trim().length === 0) {
578
+ throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
579
+ }
580
+ if (action.status.trim().length === 0) {
581
+ throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
582
+ }
583
+ if (batchEnabled) {
584
+ queue.push({ payload: action, timestamp: Date.now() });
585
+ if (queue.length >= maxBatchSize) {
586
+ flushQueue().catch(() => {
587
+ });
588
+ }
589
+ } else {
590
+ sendActions([action]).catch(() => {
591
+ });
592
+ }
593
+ return Promise.resolve();
594
+ },
595
+ async flush() {
596
+ if (!batchEnabled) return;
597
+ await flushQueue();
598
+ },
599
+ async shutdown() {
600
+ if (isShutdown) return;
601
+ isShutdown = true;
602
+ stopFlushTimer();
603
+ if (batchEnabled) {
604
+ await flushQueue();
605
+ }
606
+ }
607
+ };
608
+ }
609
+ function sleep(ms) {
610
+ return new Promise((resolve) => setTimeout(resolve, ms));
611
+ }
612
+
440
613
  exports.ShieldAuthError = ShieldAuthError;
441
614
  exports.buildAuthErrorResponse = buildAuthErrorResponse;
442
615
  exports.buildBlockedResponse = buildBlockedResponse;
443
616
  exports.buildInternalErrorResponse = buildInternalErrorResponse;
444
617
  exports.buildServiceUnreachableResponse = buildServiceUnreachableResponse;
445
618
  exports.buildSpendingBlockedResponse = buildSpendingBlockedResponse;
619
+ exports.createActionLogger = createActionLogger;
446
620
  exports.createLogger = createLogger;
447
621
  exports.deriveDashboardUrl = deriveDashboardUrl;
448
622
  exports.extractActionFromToolName = extractActionFromToolName;