@frostbridge/imdl 0.1.12 → 0.1.14

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.
Files changed (2) hide show
  1. package/dist/index.js +1846 -132
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/index.ts
10
- import { Command } from "commander";
10
+ import { Command as Command2 } from "commander";
11
11
 
12
12
  // src/commands/scan.ts
13
13
  import pc from "picocolors";
@@ -633,7 +633,7 @@ import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync
633
633
  import { join as join4 } from "path";
634
634
 
635
635
  // src/config/store.ts
636
- import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync4, unlinkSync } from "fs";
636
+ import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync4, unlinkSync, chmodSync } from "fs";
637
637
  import { join as join3 } from "path";
638
638
  import { homedir as homedir2 } from "os";
639
639
  import { randomUUID } from "crypto";
@@ -647,8 +647,10 @@ function getBufferDir() {
647
647
  return BUFFER_DIR;
648
648
  }
649
649
  function ensureImdlDir() {
650
- if (!existsSync4(IMDL_DIR)) mkdirSync(IMDL_DIR, { recursive: true, mode: 448 });
651
- if (!existsSync4(BUFFER_DIR)) mkdirSync(BUFFER_DIR, { recursive: true, mode: 448 });
650
+ mkdirSync(IMDL_DIR, { recursive: true, mode: 448 });
651
+ chmodSync(IMDL_DIR, 448);
652
+ mkdirSync(BUFFER_DIR, { recursive: true, mode: 448 });
653
+ chmodSync(BUFFER_DIR, 448);
652
654
  }
653
655
  function isLocalUrl(url) {
654
656
  try {
@@ -1231,7 +1233,8 @@ var AGENTS = [
1231
1233
  hooksFile: join6(homedir4(), ".claude", "settings.json"),
1232
1234
  hooks: {
1233
1235
  PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "imdl hook pre-tool", timeout: 5 }] }],
1234
- PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "imdl hook post-tool", timeout: 5 }] }]
1236
+ PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "imdl hook post-tool", timeout: 5 }] }],
1237
+ UserPromptSubmit: [{ matcher: "*", hooks: [{ type: "command", command: "imdl hook prompt", timeout: 5 }] }]
1235
1238
  }
1236
1239
  },
1237
1240
  {
@@ -1373,6 +1376,7 @@ async function initCommand(options) {
1373
1376
  }
1374
1377
  }
1375
1378
  installDaemon();
1379
+ setupGateway(detectedAgents);
1376
1380
  let apiConnected = false;
1377
1381
  try {
1378
1382
  const healthRes = await fetch(`${config.apiUrl}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -1398,9 +1402,10 @@ async function initCommand(options) {
1398
1402
  console.log("");
1399
1403
  console.log(pc2.bold(" Useful commands:"));
1400
1404
  console.log(pc2.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1401
- console.log(` ${pc2.cyan("imdl status")} Show connection, agents, and policy summary`);
1402
- console.log(` ${pc2.cyan("imdl scan")} Scan MCP servers for supply chain risks`);
1403
- console.log(` ${pc2.cyan("imdl login")} Authenticate to access your team dashboard`);
1405
+ console.log(` ${pc2.cyan("imdl status")} Show connection, agents, and policy summary`);
1406
+ console.log(` ${pc2.cyan("imdl sync-history")} Import past sessions and detect historical violations`);
1407
+ console.log(` ${pc2.cyan("imdl scan")} Scan MCP servers for supply chain risks`);
1408
+ console.log(` ${pc2.cyan("imdl login")} Authenticate to access your team dashboard`);
1404
1409
  console.log("");
1405
1410
  if (!options.teamToken && config.teamId === "default") {
1406
1411
  console.log(pc2.dim(" Tip: Got an invite token? Run: imdl init --token <token>"));
@@ -1591,6 +1596,126 @@ function installMCPProxy(mcpConfigPath, agentName) {
1591
1596
  writeFileSync4(mcpConfigPath, JSON.stringify(existing, null, 2));
1592
1597
  console.log(pc2.green(` \u2713 MCP proxy registered for ${agentName}`));
1593
1598
  }
1599
+ function setupGateway(detectedAgents) {
1600
+ console.log("");
1601
+ console.log(pc2.bold(" [8/9] AI Gateway (token optimization + cost tracking)"));
1602
+ const IMDL_DIR4 = join6(homedir4(), ".imdl");
1603
+ const gatewayConfig = join6(IMDL_DIR4, "gateway.toml");
1604
+ if (!existsSync7(gatewayConfig)) {
1605
+ if (!existsSync7(IMDL_DIR4)) mkdirSync2(IMDL_DIR4, { recursive: true });
1606
+ writeFileSync4(gatewayConfig, `listen_host = "127.0.0.1"
1607
+ listen_port = 9443
1608
+
1609
+ [secret_scanning]
1610
+ enabled = true
1611
+ mode = "audit"
1612
+
1613
+ [dlp]
1614
+ enabled = true
1615
+ mode = "audit"
1616
+
1617
+ [injection_detection]
1618
+ enabled = true
1619
+ mode = "block"
1620
+
1621
+ [routing]
1622
+ enabled = true
1623
+
1624
+ [budget]
1625
+ enabled = false
1626
+
1627
+ [shadow_ai]
1628
+ enabled = true
1629
+
1630
+ [upstream]
1631
+ anthropic_url = "https://api.anthropic.com"
1632
+ openai_url = "https://api.openai.com"
1633
+
1634
+ [context_optimization]
1635
+ enabled = true
1636
+
1637
+ [optimization]
1638
+ enabled = true
1639
+ effort_modulation = true
1640
+ prompt_caching = true
1641
+
1642
+ [audit]
1643
+ enabled = true
1644
+
1645
+ [reporter]
1646
+ enabled = true
1647
+ api_url = "https://imdl-api-1028078600113.us-central1.run.app"
1648
+ flush_interval_secs = 30
1649
+ `, { mode: 384 });
1650
+ console.log(pc2.green(" \u2713 Gateway config created"));
1651
+ } else {
1652
+ console.log(pc2.dim(" \u2713 Gateway config exists"));
1653
+ }
1654
+ const claudeSettings = join6(homedir4(), ".claude", "settings.json");
1655
+ const hasClaudeCode = detectedAgents.some((a) => a.name === "claude-code");
1656
+ const hasCodex = detectedAgents.some((a) => a.name === "codex");
1657
+ if (hasClaudeCode && existsSync7(claudeSettings)) {
1658
+ try {
1659
+ const settings = JSON.parse(readFileSync4(claudeSettings, "utf-8"));
1660
+ if (!settings.env) settings.env = {};
1661
+ if (!settings.env.ANTHROPIC_BEDROCK_BASE_URL) {
1662
+ settings.env.ANTHROPIC_BEDROCK_BASE_URL = "http://127.0.0.1:9443";
1663
+ writeFileSync4(claudeSettings, JSON.stringify(settings, null, 2) + "\n");
1664
+ console.log(pc2.green(" \u2713 Claude Code \u2192 gateway (ANTHROPIC_BEDROCK_BASE_URL)"));
1665
+ } else {
1666
+ console.log(pc2.dim(" \u2713 Claude Code already routed through gateway"));
1667
+ }
1668
+ } catch {
1669
+ }
1670
+ }
1671
+ const vsCodeSettings = join6(homedir4(), "Library", "Application Support", "Code", "User", "settings.json");
1672
+ if (existsSync7(vsCodeSettings)) {
1673
+ try {
1674
+ let content = readFileSync4(vsCodeSettings, "utf-8");
1675
+ if (!content.includes('"http.proxy"')) {
1676
+ const insertion = ` "http.proxy": "http://127.0.0.1:9443",
1677
+ "http.proxyStrictSSL": false,
1678
+ `;
1679
+ const lastBrace = content.lastIndexOf("}");
1680
+ if (lastBrace > 0) {
1681
+ content = content.slice(0, lastBrace) + insertion + content.slice(lastBrace);
1682
+ writeFileSync4(vsCodeSettings, content);
1683
+ console.log(pc2.green(" \u2713 VS Code http.proxy \u2192 gateway (Copilot optimization)"));
1684
+ }
1685
+ } else {
1686
+ console.log(pc2.dim(" \u2713 VS Code proxy already configured"));
1687
+ }
1688
+ } catch {
1689
+ }
1690
+ }
1691
+ const gatewayCandidates = [
1692
+ join6(IMDL_DIR4, "bin", "imdl-gateway"),
1693
+ join6(process.cwd(), "packages", "ai-gateway", "target", "release", "imdl-gateway")
1694
+ ];
1695
+ const gatewayBin = gatewayCandidates.find((p) => existsSync7(p));
1696
+ if (gatewayBin) {
1697
+ try {
1698
+ const net = __require("net");
1699
+ const sock = new net.Socket();
1700
+ sock.setTimeout(1e3);
1701
+ sock.on("connect", () => {
1702
+ sock.destroy();
1703
+ });
1704
+ sock.on("error", () => {
1705
+ const { spawn: spawn2 } = __require("child_process");
1706
+ const child = spawn2(gatewayBin, [], { detached: true, stdio: "ignore", env: { ...process.env, RUST_LOG: "imdl_ai_gateway=info" } });
1707
+ child.unref();
1708
+ console.log(pc2.green(` \u2713 Gateway started (PID ${child.pid})`));
1709
+ });
1710
+ sock.connect(9443, "127.0.0.1");
1711
+ } catch {
1712
+ console.log(pc2.dim(" \u26A0 Gateway binary found but could not start"));
1713
+ }
1714
+ } else {
1715
+ console.log(pc2.dim(" \u26A0 Gateway binary not found \u2014 run separately: imdl gateway start"));
1716
+ }
1717
+ console.log(pc2.dim(" Dashboard: http://127.0.0.1:9443/dashboard"));
1718
+ }
1594
1719
 
1595
1720
  // src/commands/login.ts
1596
1721
  import pc3 from "picocolors";
@@ -2584,12 +2709,12 @@ function detectAgents() {
2584
2709
  const home = homedir11();
2585
2710
  const claudeDir = join13(home, ".claude");
2586
2711
  if (existsSync14(claudeDir)) {
2587
- const hooksPath = join13(claudeDir, "hooks.json");
2588
2712
  let hooked = false;
2589
- if (existsSync14(hooksPath)) {
2713
+ const settingsPath = join13(claudeDir, "settings.json");
2714
+ if (existsSync14(settingsPath)) {
2590
2715
  try {
2591
- const hooks = JSON.parse(readFileSync10(hooksPath, "utf-8"));
2592
- hooked = JSON.stringify(hooks).includes("imdl hook");
2716
+ const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
2717
+ hooked = JSON.stringify(settings).includes("imdl hook");
2593
2718
  } catch {
2594
2719
  }
2595
2720
  }
@@ -2705,8 +2830,8 @@ function resumeCommand() {
2705
2830
  import pc6 from "picocolors";
2706
2831
 
2707
2832
  // src/adapters/index.ts
2708
- import { existsSync as existsSync18, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
2709
- import { join as join17 } from "path";
2833
+ import { existsSync as existsSync19, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
2834
+ import { join as join19 } from "path";
2710
2835
 
2711
2836
  // src/transport/buffer.ts
2712
2837
  import { appendFileSync, readFileSync as readFileSync11, writeFileSync as writeFileSync5, existsSync as existsSync15, statSync as statSync7, unlinkSync as unlinkSync2 } from "fs";
@@ -2784,29 +2909,75 @@ function releaseLock() {
2784
2909
  }
2785
2910
 
2786
2911
  // src/transport/sender.ts
2912
+ import { writeFileSync as writeFileSync6 } from "fs";
2913
+ import { join as join15 } from "path";
2914
+ import { homedir as homedir12 } from "os";
2787
2915
  var FLUSH_TIMEOUT = 1e4;
2788
- var BATCH_SIZE = 50;
2916
+ var BATCH_SIZE = 25;
2917
+ var DEBUG_LOG = join15(homedir12(), ".imdl", "flush-debug.log");
2918
+ function debugLog(msg) {
2919
+ try {
2920
+ writeFileSync6(DEBUG_LOG, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
2921
+ `, { flag: "a" });
2922
+ } catch {
2923
+ }
2924
+ }
2789
2925
  async function tryFlush() {
2790
2926
  if (!acquireLock()) return;
2791
2927
  try {
2792
2928
  const events = readBuffer();
2793
2929
  if (events.length === 0) return;
2794
2930
  const config = loadConfig();
2931
+ if (!config.apiUrl) {
2932
+ debugLog("No apiUrl configured, skipping flush");
2933
+ return;
2934
+ }
2795
2935
  const grouped = groupBySession(events);
2796
- let allSuccess = true;
2936
+ const failedEvents = [];
2937
+ debugLog(`Flushing ${events.length} events across ${grouped.size} sessions`);
2797
2938
  for (const [sessionId, sessionEvents] of grouped) {
2798
- for (let i = 0; i < sessionEvents.length; i += BATCH_SIZE) {
2799
- const batch = sessionEvents.slice(i, i + BATCH_SIZE);
2939
+ if (!sessionId || sessionId === "unknown" || sessionId === "?") {
2940
+ debugLog(`Skipping invalid session: ${sessionId}`);
2941
+ continue;
2942
+ }
2943
+ const validTypes = /* @__PURE__ */ new Set(["user_prompt", "pre_tool_use", "post_tool_use", "response", "notification", "tool_call", "tool_result", "prompt", "violation", "error", "system"]);
2944
+ const apiEvents = [];
2945
+ for (const e of sessionEvents) {
2946
+ if (e.type === "usage") continue;
2947
+ const copy = { ...e };
2948
+ if (copy.type === "assistant_response") copy.type = "response";
2949
+ if (copy.toolOutput && typeof copy.toolOutput !== "string") {
2950
+ copy.toolOutput = JSON.stringify(copy.toolOutput);
2951
+ }
2952
+ if (copy.toolOutput && copy.toolOutput.length > 5e4) {
2953
+ copy.toolOutput = copy.toolOutput.slice(0, 5e4) + "\u2026[truncated]";
2954
+ }
2955
+ if (copy.prompt && copy.prompt.length > 5e4) {
2956
+ copy.prompt = copy.prompt.slice(0, 5e4) + "\u2026[truncated]";
2957
+ }
2958
+ if (!validTypes.has(copy.type)) continue;
2959
+ apiEvents.push(copy);
2960
+ }
2961
+ let sessionFailed = false;
2962
+ for (let i = 0; i < apiEvents.length; i += BATCH_SIZE) {
2963
+ const batch = apiEvents.slice(i, i + BATCH_SIZE);
2800
2964
  const success = await sendBatch(config.apiUrl, sessionId, batch, config.developerId);
2801
2965
  if (!success) {
2802
- allSuccess = false;
2966
+ failedEvents.push(...sessionEvents.slice(i));
2967
+ sessionFailed = true;
2803
2968
  break;
2804
2969
  }
2805
2970
  }
2806
- if (!allSuccess) break;
2971
+ if (sessionFailed) continue;
2807
2972
  }
2808
- if (allSuccess) {
2809
- clearBuffer();
2973
+ clearBuffer();
2974
+ if (failedEvents.length > 0) {
2975
+ debugLog(`Re-queuing ${failedEvents.length} failed events`);
2976
+ for (const event of failedEvents) {
2977
+ appendEvent(event);
2978
+ }
2979
+ } else {
2980
+ debugLog("All events flushed successfully");
2810
2981
  }
2811
2982
  } finally {
2812
2983
  releaseLock();
@@ -2826,8 +2997,13 @@ async function sendBatch(apiUrl, sessionId, events, developerId) {
2826
2997
  body: JSON.stringify({ events, developerId, ...agentType && { agentType } }),
2827
2998
  signal: AbortSignal.timeout(FLUSH_TIMEOUT)
2828
2999
  });
3000
+ if (!res.ok) {
3001
+ const body = await res.text().catch(() => "");
3002
+ debugLog(`sendBatch FAILED for ${sessionId}: ${res.status} ${body.slice(0, 200)}`);
3003
+ }
2829
3004
  return res.ok;
2830
- } catch {
3005
+ } catch (e) {
3006
+ debugLog(`sendBatch ERROR for ${sessionId}: ${e.message}`);
2831
3007
  return false;
2832
3008
  }
2833
3009
  }
@@ -2959,20 +3135,85 @@ function redactObject(obj) {
2959
3135
 
2960
3136
  // src/hooks/prompt-scanner.ts
2961
3137
  var KNOWN_PREFIX_PATTERNS = [
3138
+ // ─── Cloud Providers ───
2962
3139
  { pattern: /AKIA[0-9A-Z]{16}/g, label: "AWS Access Key", severity: "critical" },
3140
+ { pattern: /(?:ASIA|ABIA|ACCA)[0-9A-Z]{16}/g, label: "AWS Temporary Key", severity: "critical" },
3141
+ { pattern: /aws_secret_access_key\s*[=:]\s*["']?([A-Za-z0-9/+=]{40})["']?/gi, label: "AWS Secret Key", severity: "critical" },
3142
+ { pattern: /AIzaSy[A-Za-z0-9_\-]{30,36}/g, label: "Google API Key", severity: "critical" },
3143
+ { pattern: /[0-9]+-[a-z0-9]{32}\.apps\.googleusercontent\.com/g, label: "Google OAuth Client ID", severity: "high" },
3144
+ { pattern: /ya29\.[A-Za-z0-9_\-]{50,}/g, label: "Google OAuth Token", severity: "critical" },
3145
+ { pattern: /GOCSPX-[A-Za-z0-9_\-]{28}/g, label: "Google OAuth Secret", severity: "critical" },
3146
+ // ─── AI/LLM Providers ───
3147
+ { pattern: /sk-ant-[A-Za-z0-9\-_]{20,}/g, label: "Anthropic Key", severity: "critical" },
3148
+ { pattern: /sk-(?:proj-)?[A-Za-z0-9\-_]{20,}/g, label: "OpenAI Key", severity: "critical" },
3149
+ { pattern: /sess-[A-Za-z0-9]{40,}/g, label: "OpenAI Session Token", severity: "critical" },
3150
+ { pattern: /r8_[A-Za-z0-9]{20,}/g, label: "Replicate Token", severity: "critical" },
3151
+ { pattern: /hf_[A-Za-z0-9]{20,}/g, label: "Hugging Face Token", severity: "critical" },
3152
+ { pattern: /key-[A-Za-z0-9]{32,}/g, label: "Cohere API Key", severity: "critical" },
3153
+ // ─── Code Platforms ───
2963
3154
  { pattern: /gh[pousr]_[A-Za-z0-9_]{36,255}/g, label: "GitHub Token", severity: "critical" },
2964
3155
  { pattern: /github_pat_[A-Za-z0-9_]{22,255}/g, label: "GitHub PAT", severity: "critical" },
3156
+ { pattern: /ghu_[A-Za-z0-9]{36,}/g, label: "GitHub User Token", severity: "critical" },
3157
+ { pattern: /glpat-[A-Za-z0-9\-_]{20,}/g, label: "GitLab PAT", severity: "critical" },
3158
+ { pattern: /glrt-[A-Za-z0-9\-_]{20,}/g, label: "GitLab Runner Token", severity: "critical" },
3159
+ { pattern: /ATATT[A-Za-z0-9\-_+/=]{30,}/g, label: "Atlassian/Bitbucket Token", severity: "critical" },
2965
3160
  { pattern: /npm_[A-Za-z0-9]{36,}/g, label: "NPM Token", severity: "critical" },
3161
+ { pattern: /pypi-[A-Za-z0-9\-_]{100,}/g, label: "PyPI API Token", severity: "critical" },
3162
+ { pattern: /nuget\.org\/v3\/[A-Za-z0-9\-]{30,}/g, label: "NuGet API Key", severity: "critical" },
3163
+ { pattern: /rubygems_[A-Za-z0-9]{48}/g, label: "RubyGems Token", severity: "critical" },
3164
+ // ─── Communication ───
2966
3165
  { pattern: /xox[baprs]-[A-Za-z0-9\-]{10,}/g, label: "Slack Token", severity: "critical" },
3166
+ { pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/g, label: "Slack Webhook", severity: "high" },
3167
+ { pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_\-]+/g, label: "Discord Webhook", severity: "high" },
3168
+ { pattern: /[0-9]{8,}:[A-Za-z0-9_\-]{35}/g, label: "Telegram Bot Token", severity: "critical" },
3169
+ { pattern: /EAA[A-Za-z0-9]{100,}/g, label: "Facebook Access Token", severity: "critical" },
3170
+ // ─── Payment/Financial ───
2967
3171
  { pattern: /[spr]k_(?:live|test)_[A-Za-z0-9]{24,}/g, label: "Stripe Key", severity: "critical" },
3172
+ { pattern: /whsec_[A-Za-z0-9]{32,}/g, label: "Stripe Webhook Secret", severity: "critical" },
3173
+ { pattern: /sq0[a-z]{3}-[A-Za-z0-9\-_]{22,}/g, label: "Square Token", severity: "critical" },
3174
+ { pattern: /access_token\$(?:sandbox|production)\$[a-z0-9]{16}\$[a-f0-9]{32}/g, label: "PayPal Token", severity: "critical" },
3175
+ // ─── Messaging/Email ───
2968
3176
  { pattern: /SG\.[A-Za-z0-9_\-]{22,}\.[A-Za-z0-9_\-]{22,}/g, label: "SendGrid Key", severity: "critical" },
2969
- { pattern: /sk-ant-[A-Za-z0-9\-_]{20,}/g, label: "Anthropic Key", severity: "critical" },
2970
- { pattern: /sk-(?:proj-)?[A-Za-z0-9\-_]{20,}/g, label: "OpenAI Key", severity: "critical" },
2971
- { pattern: /AIzaSy[A-Za-z0-9_\-]{30,36}/g, label: "Google API Key", severity: "critical" },
3177
+ { pattern: /key-[a-f0-9]{32}/g, label: "Mailgun Key", severity: "critical" },
3178
+ { pattern: /re_[A-Za-z0-9]{20,}/g, label: "Resend API Key", severity: "critical" },
3179
+ { pattern: /[a-f0-9]{32}-us[0-9]{1,2}/g, label: "Mailchimp Key", severity: "high" },
3180
+ // ─── Infrastructure ───
3181
+ { pattern: /SK[0-9a-fA-F]{32}/g, label: "Twilio Key", severity: "critical" },
3182
+ { pattern: /AC[a-f0-9]{32}/g, label: "Twilio Account SID", severity: "high" },
3183
+ { pattern: /dop_v1_[a-f0-9]{64}/g, label: "DigitalOcean Token", severity: "critical" },
3184
+ { pattern: /do[po]_v1_[a-f0-9]{64}/g, label: "DigitalOcean PAT", severity: "critical" },
3185
+ { pattern: /FLWSECK_TEST-[a-f0-9]{32}/g, label: "Flutterwave Secret", severity: "critical" },
3186
+ { pattern: /vault:[a-z0-9_\-\/]+#[A-Za-z0-9_\-]{8,}/g, label: "Vault Path", severity: "high" },
3187
+ { pattern: /AgEAAr[A-Za-z0-9+/=]{40,}/g, label: "Doppler Token", severity: "critical" },
3188
+ // ─── CI/CD ───
3189
+ { pattern: /v1\.[a-f0-9]{40}/g, label: "CircleCI Token", severity: "critical" },
3190
+ { pattern: /tfp_[A-Za-z0-9]{40,}/g, label: "Terraform Cloud Token", severity: "critical" },
3191
+ { pattern: /AstraCS:[A-Za-z0-9+/=]{40,}/g, label: "DataStax Token", severity: "critical" },
3192
+ { pattern: /snyk[_-][a-f0-9\-]{36,}/gi, label: "Snyk Token", severity: "critical" },
3193
+ // ─── Observability ───
3194
+ { pattern: /https?:\/\/[a-f0-9]{32}@[^\s"']+\.sentry\.io/g, label: "Sentry DSN", severity: "high" },
3195
+ { pattern: /dd[apt]_[A-Za-z0-9]{32,40}/g, label: "Datadog API Key", severity: "critical" },
3196
+ { pattern: /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}:x-]?[a-f0-9]{8}/g, label: "New Relic Key", severity: "high" },
3197
+ // ─── Database ───
3198
+ { pattern: /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql|cockroachdb):\/\/[^:]+:[^@\s]+@/gi, label: "Database Connection String", severity: "critical" },
3199
+ { pattern: /redis:\/\/default:[^\s@]+@/gi, label: "Redis Password URL", severity: "critical" },
3200
+ // ─── Crypto/Keys ───
2972
3201
  { pattern: /-----BEGIN (?:RSA |EC |DSA |ED25519 |OPENSSH )?PRIVATE KEY-----/g, label: "Private Key", severity: "critical" },
3202
+ { pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g, label: "PGP Private Key", severity: "critical" },
3203
+ { pattern: /age1[a-z0-9]{58}/g, label: "Age Encryption Key", severity: "critical" },
3204
+ // ─── Auth/Session ───
2973
3205
  { pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_\-+/=]{10,}/g, label: "JWT Token", severity: "high" },
2974
- { pattern: /(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^:]+:[^@\s]+@/gi, label: "Database Connection String", severity: "critical" },
2975
- { pattern: /Bearer\s+[A-Za-z0-9_\-./+=]{20,}/g, label: "Bearer Token", severity: "high" }
3206
+ { pattern: /Bearer\s+[A-Za-z0-9_\-./+=]{20,}/g, label: "Bearer Token", severity: "high" },
3207
+ { pattern: /Basic\s+[A-Za-z0-9+/=]{20,}/g, label: "Basic Auth Credential", severity: "high" },
3208
+ { pattern: /SAML[A-Za-z0-9+/=]{50,}/g, label: "SAML Token", severity: "high" },
3209
+ // ─── Cloud-specific secrets ───
3210
+ { pattern: /AccountKey=[A-Za-z0-9+/=]{86}==/g, label: "Azure Storage Key", severity: "critical" },
3211
+ { pattern: /SharedAccessSignature=sv=[^&\s]{50,}/g, label: "Azure SAS Token", severity: "critical" },
3212
+ { pattern: /DefaultEndpointsProtocol=https;AccountName=[^;]+;AccountKey=[^;]+/g, label: "Azure Connection String", severity: "critical" },
3213
+ { pattern: /AKCp[A-Za-z0-9]{50,}/g, label: "Artifactory Token", severity: "critical" },
3214
+ // ─── Clerk (our own auth!) ───
3215
+ { pattern: /sk_(?:live|test)_[A-Za-z0-9]{20,}/g, label: "Clerk Secret Key", severity: "critical" },
3216
+ { pattern: /pk_(?:live|test)_[A-Za-z0-9]{20,}/g, label: "Clerk Publishable Key", severity: "medium" }
2976
3217
  ];
2977
3218
  function shannonEntropy(str) {
2978
3219
  if (str.length === 0) return 0;
@@ -3100,12 +3341,75 @@ function createPromptViolation(findings) {
3100
3341
  reason
3101
3342
  };
3102
3343
  }
3344
+ var LLM_SCAN_PROMPT = `You are a security scanner. Determine if the text below contains a secret, credential, API key, or sensitive token that should NOT be shared.
3345
+
3346
+ Respond ONLY with JSON: {"isSecret": true/false, "confidence": 0.0-1.0, "label": "type if found"}
3347
+
3348
+ Text to analyze:
3349
+ `;
3350
+ async function llmScanAmbiguous(text, config) {
3351
+ if (!config.enabled || !config.apiUrl) {
3352
+ return { isSecret: false, confidence: 0 };
3353
+ }
3354
+ const maxChars = config.maxInputChars || 500;
3355
+ const truncated = text.length > maxChars ? text.slice(0, maxChars) : text;
3356
+ try {
3357
+ const res = await fetch(config.apiUrl, {
3358
+ method: "POST",
3359
+ headers: { "Content-Type": "application/json" },
3360
+ body: JSON.stringify({
3361
+ model: config.model || "claude-haiku-4-5-20251001",
3362
+ max_tokens: 80,
3363
+ messages: [{ role: "user", content: LLM_SCAN_PROMPT + truncated }]
3364
+ }),
3365
+ signal: AbortSignal.timeout(3e3)
3366
+ });
3367
+ if (!res.ok) return { isSecret: false, confidence: 0 };
3368
+ const data = await res.json();
3369
+ const content = data.content?.[0]?.text || data.choices?.[0]?.message?.content || "";
3370
+ const jsonMatch = content.match(/\{[^}]+\}/);
3371
+ if (!jsonMatch) return { isSecret: false, confidence: 0 };
3372
+ const parsed = JSON.parse(jsonMatch[0]);
3373
+ return {
3374
+ isSecret: parsed.isSecret === true,
3375
+ confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0,
3376
+ label: parsed.label,
3377
+ reasoning: parsed.reasoning
3378
+ };
3379
+ } catch {
3380
+ return { isSecret: false, confidence: 0 };
3381
+ }
3382
+ }
3383
+ async function scanWithLlmFallback(prompt, llmConfig) {
3384
+ const regexResult = scanPromptForSecrets(prompt);
3385
+ if (regexResult.hasSecrets || !llmConfig.enabled) {
3386
+ return regexResult;
3387
+ }
3388
+ const hasAssignments = ENV_ASSIGNMENT.test(prompt);
3389
+ ENV_ASSIGNMENT.lastIndex = 0;
3390
+ const hasLongStrings = /[A-Za-z0-9_\-]{30,}/.test(prompt);
3391
+ if (!hasAssignments && !hasLongStrings) {
3392
+ return regexResult;
3393
+ }
3394
+ const llmResult = await llmScanAmbiguous(prompt, llmConfig);
3395
+ if (llmResult.isSecret && llmResult.confidence >= 0.7) {
3396
+ return {
3397
+ hasSecrets: true,
3398
+ findings: [{
3399
+ label: llmResult.label || "LLM-detected secret",
3400
+ severity: llmResult.confidence >= 0.9 ? "critical" : "high",
3401
+ snippet: prompt.slice(0, 20) + "..."
3402
+ }]
3403
+ };
3404
+ }
3405
+ return regexResult;
3406
+ }
3103
3407
 
3104
3408
  // src/adapters/claude-code.ts
3105
3409
  import { createReadStream, existsSync as existsSync16, statSync as statSync8, readdirSync as readdirSync3 } from "fs";
3106
3410
  import { createInterface } from "readline";
3107
- import { join as join15 } from "path";
3108
- import { homedir as homedir12 } from "os";
3411
+ import { join as join16 } from "path";
3412
+ import { homedir as homedir13 } from "os";
3109
3413
  var ClaudeCodeAdapter = class {
3110
3414
  agentType = "claude-code";
3111
3415
  async extractEvents(source) {
@@ -3149,6 +3453,21 @@ var ClaudeCodeAdapter = class {
3149
3453
  cwd: entry.cwd,
3150
3454
  agentType: "claude-code"
3151
3455
  });
3456
+ } else if (Array.isArray(content)) {
3457
+ const textBlock = content.find(
3458
+ (block) => block && block.type === "text" && typeof block.text === "string" && block.text.length > 0
3459
+ );
3460
+ if (textBlock) {
3461
+ if (this.isNoisePrompt(textBlock.text)) continue;
3462
+ events.push({
3463
+ sessionId: entry.sessionId || this.sessionIdFromPath(path),
3464
+ type: "user_prompt",
3465
+ prompt: textBlock.text.slice(0, 1e4),
3466
+ timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
3467
+ cwd: entry.cwd,
3468
+ agentType: "claude-code"
3469
+ });
3470
+ }
3152
3471
  }
3153
3472
  }
3154
3473
  if (entry.type === "assistant" && entry.message?.usage) {
@@ -3178,7 +3497,7 @@ var ClaudeCodeAdapter = class {
3178
3497
  const events = [];
3179
3498
  const files = readdirSync3(dirPath).filter((f) => f.endsWith(".jsonl"));
3180
3499
  for (const file of files) {
3181
- const filePath = join15(dirPath, file);
3500
+ const filePath = join16(dirPath, file);
3182
3501
  if (since) {
3183
3502
  const stat = statSync8(filePath);
3184
3503
  if (stat.mtimeMs < since.getTime()) continue;
@@ -3203,20 +3522,20 @@ var ClaudeCodeAdapter = class {
3203
3522
  }
3204
3523
  static getProjectLogDir(projectPath) {
3205
3524
  const encoded = projectPath.replace(/\//g, "-");
3206
- const logDir = join15(homedir12(), ".claude", "projects", encoded);
3525
+ const logDir = join16(homedir13(), ".claude", "projects", encoded);
3207
3526
  return existsSync16(logDir) ? logDir : null;
3208
3527
  }
3209
3528
  static getAllProjectDirs() {
3210
- const base = join15(homedir12(), ".claude", "projects");
3529
+ const base = join16(homedir13(), ".claude", "projects");
3211
3530
  if (!existsSync16(base)) return [];
3212
- return readdirSync3(base).map((d) => join15(base, d)).filter((p) => statSync8(p).isDirectory());
3531
+ return readdirSync3(base).map((d) => join16(base, d)).filter((p) => statSync8(p).isDirectory());
3213
3532
  }
3214
3533
  };
3215
3534
 
3216
3535
  // src/adapters/cursor.ts
3217
3536
  import { existsSync as existsSync17, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync9 } from "fs";
3218
- import { join as join16 } from "path";
3219
- import { homedir as homedir13 } from "os";
3537
+ import { join as join17 } from "path";
3538
+ import { homedir as homedir14 } from "os";
3220
3539
  import { createRequire } from "module";
3221
3540
  var esmRequire = createRequire(import.meta.url);
3222
3541
  function loadSqlite() {
@@ -3269,7 +3588,7 @@ var CursorAdapter = class _CursorAdapter {
3269
3588
  */
3270
3589
  findVscdbFiles(baseDir) {
3271
3590
  const files = [];
3272
- const directDb = join16(baseDir, "state.vscdb");
3591
+ const directDb = join17(baseDir, "state.vscdb");
3273
3592
  if (existsSync17(directDb)) {
3274
3593
  files.push(directDb);
3275
3594
  return files;
@@ -3277,11 +3596,11 @@ var CursorAdapter = class _CursorAdapter {
3277
3596
  try {
3278
3597
  const entries = readdirSync4(baseDir);
3279
3598
  for (const entry of entries) {
3280
- const entryPath = join16(baseDir, entry);
3599
+ const entryPath = join17(baseDir, entry);
3281
3600
  try {
3282
3601
  const stat = statSync9(entryPath);
3283
3602
  if (stat.isDirectory()) {
3284
- const dbPath = join16(entryPath, "state.vscdb");
3603
+ const dbPath = join17(entryPath, "state.vscdb");
3285
3604
  if (existsSync17(dbPath)) {
3286
3605
  files.push(dbPath);
3287
3606
  }
@@ -3602,14 +3921,14 @@ var CursorAdapter = class _CursorAdapter {
3602
3921
  * Uses offsets to only read new lines since last scan.
3603
3922
  */
3604
3923
  extractFromTranscripts(since, offsets) {
3605
- const projectsDir = join16(homedir13(), ".cursor", "projects");
3924
+ const projectsDir = join17(homedir14(), ".cursor", "projects");
3606
3925
  if (!existsSync17(projectsDir)) return { events: [], newOffsets: offsets };
3607
3926
  const events = [];
3608
3927
  const newOffsets = { ...offsets };
3609
3928
  try {
3610
3929
  const projects = readdirSync4(projectsDir);
3611
3930
  for (const proj of projects) {
3612
- const transcriptsDir = join16(projectsDir, proj, "agent-transcripts");
3931
+ const transcriptsDir = join17(projectsDir, proj, "agent-transcripts");
3613
3932
  if (!existsSync17(transcriptsDir)) continue;
3614
3933
  let sessionDirs;
3615
3934
  try {
@@ -3618,7 +3937,7 @@ var CursorAdapter = class _CursorAdapter {
3618
3937
  continue;
3619
3938
  }
3620
3939
  for (const sessionDir of sessionDirs) {
3621
- const jsonlPath = join16(transcriptsDir, sessionDir, `${sessionDir}.jsonl`);
3940
+ const jsonlPath = join17(transcriptsDir, sessionDir, `${sessionDir}.jsonl`);
3622
3941
  if (!existsSync17(jsonlPath)) continue;
3623
3942
  if (since) {
3624
3943
  try {
@@ -3677,6 +3996,23 @@ var CursorAdapter = class _CursorAdapter {
3677
3996
  });
3678
3997
  }
3679
3998
  }
3999
+ const usage = entry.message?.usage || entry.usage;
4000
+ if (usage && (usage.input_tokens || usage.output_tokens)) {
4001
+ events.push({
4002
+ sessionId: sid,
4003
+ type: "notification",
4004
+ timestamp: now,
4005
+ cwd: projectPath,
4006
+ agentType: "cursor",
4007
+ usage: {
4008
+ model: entry.message?.model || entry.model || "unknown",
4009
+ inputTokens: usage.input_tokens || 0,
4010
+ outputTokens: usage.output_tokens || 0,
4011
+ cacheReadTokens: usage.cache_read_input_tokens || 0,
4012
+ cacheWriteTokens: usage.cache_creation_input_tokens || 0
4013
+ }
4014
+ });
4015
+ }
3680
4016
  }
3681
4017
  }
3682
4018
  } catch {
@@ -3692,8 +4028,8 @@ var CursorAdapter = class _CursorAdapter {
3692
4028
  */
3693
4029
  static getLogPath() {
3694
4030
  const paths = [
3695
- join16(homedir13(), ".cursor", "User", "workspaceStorage"),
3696
- join16(homedir13(), "Library", "Application Support", "Cursor", "User", "workspaceStorage")
4031
+ join17(homedir14(), ".cursor", "User", "workspaceStorage"),
4032
+ join17(homedir14(), "Library", "Application Support", "Cursor", "User", "workspaceStorage")
3697
4033
  ];
3698
4034
  for (const p of paths) {
3699
4035
  if (existsSync17(p)) return p;
@@ -3707,7 +4043,7 @@ var CursorAdapter = class _CursorAdapter {
3707
4043
  const base = _CursorAdapter.getLogPath();
3708
4044
  if (!base) return [];
3709
4045
  try {
3710
- return readdirSync4(base).map((d) => join16(base, d)).filter((p) => {
4046
+ return readdirSync4(base).map((d) => join17(base, d)).filter((p) => {
3711
4047
  try {
3712
4048
  return statSync9(p).isDirectory();
3713
4049
  } catch {
@@ -3722,17 +4058,317 @@ var CursorAdapter = class _CursorAdapter {
3722
4058
  * Returns the Cursor projects directory for transcript scanning.
3723
4059
  */
3724
4060
  static getProjectsDir() {
3725
- const dir = join16(homedir13(), ".cursor", "projects");
4061
+ const dir = join17(homedir14(), ".cursor", "projects");
3726
4062
  return existsSync17(dir) ? dir : null;
3727
4063
  }
3728
4064
  };
3729
4065
 
4066
+ // src/adapters/codex.ts
4067
+ import { existsSync as existsSync18, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync10 } from "fs";
4068
+ import { join as join18 } from "path";
4069
+ import { homedir as homedir15 } from "os";
4070
+ var MAX_FILE_SIZE = 50 * 1024 * 1024;
4071
+ var CodexAdapter = class _CodexAdapter {
4072
+ agentType = "codex";
4073
+ async extractEvents(source) {
4074
+ if (source.kind === "directory") {
4075
+ return this.readSessionsDir(source.path, source.since);
4076
+ }
4077
+ return [];
4078
+ }
4079
+ extractWithOffsets(since, offsets = {}) {
4080
+ const basePath = _CodexAdapter.getSessionsDir();
4081
+ if (!basePath) return { events: [], newOffsets: offsets };
4082
+ const events = [];
4083
+ const newOffsets = { ...offsets };
4084
+ const files = this.findSessionFiles(basePath);
4085
+ for (const filePath of files) {
4086
+ const prevOffset = offsets[filePath] || 0;
4087
+ const { events: fileEvents, newOffset } = this.readSessionFileWithOffset(filePath, prevOffset, since);
4088
+ events.push(...fileEvents);
4089
+ if (newOffset > prevOffset) {
4090
+ newOffsets[filePath] = newOffset;
4091
+ }
4092
+ }
4093
+ return { events, newOffsets };
4094
+ }
4095
+ readSessionsDir(basePath, since) {
4096
+ if (!existsSync18(basePath)) return [];
4097
+ const events = [];
4098
+ const files = this.findSessionFiles(basePath, since);
4099
+ for (const filePath of files) {
4100
+ const fileEvents = this.readSessionFile(filePath, since);
4101
+ events.push(...fileEvents);
4102
+ }
4103
+ return events;
4104
+ }
4105
+ findSessionFiles(basePath, _since) {
4106
+ const files = [];
4107
+ const walk = (dir) => {
4108
+ try {
4109
+ for (const entry of readdirSync5(dir)) {
4110
+ const full = join18(dir, entry);
4111
+ try {
4112
+ const stat = statSync10(full);
4113
+ if (stat.isDirectory()) {
4114
+ walk(full);
4115
+ } else if (entry.endsWith(".jsonl")) {
4116
+ if (stat.size > MAX_FILE_SIZE) continue;
4117
+ files.push(full);
4118
+ }
4119
+ } catch {
4120
+ }
4121
+ }
4122
+ } catch {
4123
+ }
4124
+ };
4125
+ walk(basePath);
4126
+ return files;
4127
+ }
4128
+ readSessionFileWithOffset(filePath, lineOffset, since) {
4129
+ const events = [];
4130
+ let content;
4131
+ try {
4132
+ content = readFileSync13(filePath, "utf-8");
4133
+ } catch {
4134
+ return { events, newOffset: lineOffset };
4135
+ }
4136
+ const lines = content.split("\n").filter((l) => l.trim());
4137
+ const sessionId = this.sessionIdFromPath(filePath);
4138
+ let cwd;
4139
+ for (let i = 0; i < lines.length; i++) {
4140
+ let entry;
4141
+ try {
4142
+ entry = JSON.parse(lines[i]);
4143
+ } catch {
4144
+ continue;
4145
+ }
4146
+ if (entry.type === "turn_context" && entry.payload?.cwd) {
4147
+ cwd = entry.payload.cwd;
4148
+ }
4149
+ if (i < lineOffset) continue;
4150
+ if (since && entry.timestamp) {
4151
+ const entryTime = new Date(entry.timestamp).getTime();
4152
+ if (entryTime <= since.getTime()) continue;
4153
+ }
4154
+ const ptype = entry.payload?.type;
4155
+ if (ptype === "user_message" && entry.payload.message) {
4156
+ const msg = entry.payload.message.trim();
4157
+ if (msg.length > 0) {
4158
+ events.push({
4159
+ sessionId,
4160
+ type: "user_prompt",
4161
+ prompt: msg.slice(0, 1e4),
4162
+ timestamp: entry.timestamp,
4163
+ cwd,
4164
+ agentType: "codex"
4165
+ });
4166
+ }
4167
+ }
4168
+ if (ptype === "function_call" && entry.payload.name) {
4169
+ let toolInput;
4170
+ if (entry.payload.arguments) {
4171
+ try {
4172
+ toolInput = JSON.parse(entry.payload.arguments);
4173
+ } catch {
4174
+ toolInput = { raw: entry.payload.arguments };
4175
+ }
4176
+ }
4177
+ events.push({
4178
+ sessionId,
4179
+ type: "pre_tool_use",
4180
+ toolName: entry.payload.name,
4181
+ toolInput,
4182
+ timestamp: entry.timestamp,
4183
+ cwd,
4184
+ agentType: "codex"
4185
+ });
4186
+ }
4187
+ if (ptype === "function_call_output" && entry.payload.output) {
4188
+ events.push({
4189
+ sessionId,
4190
+ type: "post_tool_use",
4191
+ toolName: entry.payload.call_id || "unknown",
4192
+ toolOutput: entry.payload.output.slice(0, 1e4),
4193
+ timestamp: entry.timestamp,
4194
+ cwd,
4195
+ agentType: "codex"
4196
+ });
4197
+ }
4198
+ if (ptype === "web_search_end" || ptype === "web_search_call") {
4199
+ const action = entry.payload.action;
4200
+ events.push({
4201
+ sessionId,
4202
+ type: "pre_tool_use",
4203
+ toolName: "web_search",
4204
+ toolInput: action ? { query: action.query, type: action.type } : void 0,
4205
+ timestamp: entry.timestamp,
4206
+ cwd,
4207
+ agentType: "codex"
4208
+ });
4209
+ }
4210
+ if (ptype === "agent_message" && entry.payload.message) {
4211
+ events.push({
4212
+ sessionId,
4213
+ type: "response",
4214
+ toolOutput: entry.payload.message.slice(0, 1e4),
4215
+ timestamp: entry.timestamp,
4216
+ cwd,
4217
+ agentType: "codex"
4218
+ });
4219
+ }
4220
+ if (ptype === "token_count") {
4221
+ const p = entry.payload;
4222
+ if (p.input_tokens || p.output_tokens) {
4223
+ events.push({
4224
+ sessionId,
4225
+ type: "usage",
4226
+ timestamp: entry.timestamp,
4227
+ cwd,
4228
+ agentType: "codex",
4229
+ usage: {
4230
+ model: p.model || "unknown",
4231
+ inputTokens: p.input_tokens || 0,
4232
+ outputTokens: p.output_tokens || 0,
4233
+ cacheReadTokens: p.cache_read_tokens || 0,
4234
+ cacheWriteTokens: p.cache_write_tokens || 0
4235
+ }
4236
+ });
4237
+ }
4238
+ }
4239
+ }
4240
+ return { events, newOffset: lines.length };
4241
+ }
4242
+ readSessionFile(filePath, since) {
4243
+ const events = [];
4244
+ let content;
4245
+ try {
4246
+ content = readFileSync13(filePath, "utf-8");
4247
+ } catch {
4248
+ return [];
4249
+ }
4250
+ const lines = content.split("\n").filter((l) => l.trim());
4251
+ const sessionId = this.sessionIdFromPath(filePath);
4252
+ let cwd;
4253
+ for (const line of lines) {
4254
+ let entry;
4255
+ try {
4256
+ entry = JSON.parse(line);
4257
+ } catch {
4258
+ continue;
4259
+ }
4260
+ if (since && entry.timestamp) {
4261
+ const entryTime = new Date(entry.timestamp).getTime();
4262
+ if (entryTime <= since.getTime()) continue;
4263
+ }
4264
+ const ptype = entry.payload?.type;
4265
+ if (entry.type === "turn_context" && entry.payload?.cwd) {
4266
+ cwd = entry.payload.cwd;
4267
+ }
4268
+ if (ptype === "user_message" && entry.payload.message) {
4269
+ const msg = entry.payload.message.trim();
4270
+ if (msg.length > 0) {
4271
+ events.push({
4272
+ sessionId,
4273
+ type: "user_prompt",
4274
+ prompt: msg.slice(0, 1e4),
4275
+ timestamp: entry.timestamp,
4276
+ cwd,
4277
+ agentType: "codex"
4278
+ });
4279
+ }
4280
+ }
4281
+ if (ptype === "function_call" && entry.payload.name) {
4282
+ let toolInput;
4283
+ if (entry.payload.arguments) {
4284
+ try {
4285
+ toolInput = JSON.parse(entry.payload.arguments);
4286
+ } catch {
4287
+ toolInput = { raw: entry.payload.arguments };
4288
+ }
4289
+ }
4290
+ events.push({
4291
+ sessionId,
4292
+ type: "pre_tool_use",
4293
+ toolName: entry.payload.name,
4294
+ toolInput,
4295
+ timestamp: entry.timestamp,
4296
+ cwd,
4297
+ agentType: "codex"
4298
+ });
4299
+ }
4300
+ if (ptype === "function_call_output" && entry.payload.output) {
4301
+ events.push({
4302
+ sessionId,
4303
+ type: "post_tool_use",
4304
+ toolName: entry.payload.call_id || "unknown",
4305
+ toolOutput: entry.payload.output.slice(0, 1e4),
4306
+ timestamp: entry.timestamp,
4307
+ cwd,
4308
+ agentType: "codex"
4309
+ });
4310
+ }
4311
+ if (ptype === "web_search_end" || ptype === "web_search_call") {
4312
+ const action = entry.payload.action;
4313
+ events.push({
4314
+ sessionId,
4315
+ type: "pre_tool_use",
4316
+ toolName: "web_search",
4317
+ toolInput: action ? { query: action.query, type: action.type } : void 0,
4318
+ timestamp: entry.timestamp,
4319
+ cwd,
4320
+ agentType: "codex"
4321
+ });
4322
+ }
4323
+ if (ptype === "agent_message" && entry.payload.message) {
4324
+ events.push({
4325
+ sessionId,
4326
+ type: "response",
4327
+ toolOutput: entry.payload.message.slice(0, 1e4),
4328
+ timestamp: entry.timestamp,
4329
+ cwd,
4330
+ agentType: "codex"
4331
+ });
4332
+ }
4333
+ if (ptype === "token_count") {
4334
+ const p = entry.payload;
4335
+ if (p.input_tokens || p.output_tokens) {
4336
+ events.push({
4337
+ sessionId,
4338
+ type: "usage",
4339
+ timestamp: entry.timestamp,
4340
+ cwd,
4341
+ agentType: "codex",
4342
+ usage: {
4343
+ model: p.model || "unknown",
4344
+ inputTokens: p.input_tokens || 0,
4345
+ outputTokens: p.output_tokens || 0,
4346
+ cacheReadTokens: p.cache_read_tokens || 0,
4347
+ cacheWriteTokens: p.cache_write_tokens || 0
4348
+ }
4349
+ });
4350
+ }
4351
+ }
4352
+ }
4353
+ return events;
4354
+ }
4355
+ sessionIdFromPath(filePath) {
4356
+ const filename = filePath.split("/").pop() || "";
4357
+ const match = filename.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
4358
+ return match ? `codex-${match[1].slice(0, 8)}` : `codex-${filename.replace(".jsonl", "").slice(-8)}`;
4359
+ }
4360
+ static getSessionsDir() {
4361
+ const dir = join18(homedir15(), ".codex", "sessions");
4362
+ return existsSync18(dir) ? dir : null;
4363
+ }
4364
+ };
4365
+
3730
4366
  // src/adapters/index.ts
3731
- var STATE_FILE = join17(getImdlDir(), "adapter-state.json");
4367
+ var STATE_FILE = join19(getImdlDir(), "adapter-state.json");
3732
4368
  function loadState() {
3733
4369
  try {
3734
- if (existsSync18(STATE_FILE)) {
3735
- return JSON.parse(readFileSync13(STATE_FILE, "utf-8"));
4370
+ if (existsSync19(STATE_FILE)) {
4371
+ return JSON.parse(readFileSync14(STATE_FILE, "utf-8"));
3736
4372
  }
3737
4373
  } catch {
3738
4374
  }
@@ -3740,16 +4376,19 @@ function loadState() {
3740
4376
  }
3741
4377
  function saveState(state) {
3742
4378
  ensureImdlDir();
3743
- writeFileSync6(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
4379
+ writeFileSync7(STATE_FILE, JSON.stringify(state, null, 2), { mode: 384 });
3744
4380
  }
3745
4381
  var adapters = [
3746
4382
  new ClaudeCodeAdapter(),
3747
- new CursorAdapter()
4383
+ new CursorAdapter(),
4384
+ new CodexAdapter()
3748
4385
  ];
3749
4386
  async function collectPrompts() {
3750
4387
  const state = loadState();
3751
4388
  const config = loadConfig();
3752
4389
  let collected = 0;
4390
+ const usageHeaders = { "Content-Type": "application/json" };
4391
+ if (config.authToken) usageHeaders["X-IMDL-Key"] = config.authToken;
3753
4392
  const claudeDirs = ClaudeCodeAdapter.getAllProjectDirs();
3754
4393
  const claudeAdapter = new ClaudeCodeAdapter();
3755
4394
  for (const dir of claudeDirs) {
@@ -3767,16 +4406,18 @@ async function collectPrompts() {
3767
4406
  path: dir,
3768
4407
  since: new Date(state.lastRun)
3769
4408
  });
3770
- const usageBySession = /* @__PURE__ */ new Map();
4409
+ const usageBySessionDay = /* @__PURE__ */ new Map();
3771
4410
  for (const event of events) {
3772
4411
  if (event.usage) {
3773
- const existing = usageBySession.get(event.sessionId) || { model: event.usage.model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
4412
+ const date = event.timestamp ? event.timestamp.slice(0, 10) : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4413
+ const key = `${event.sessionId}|${date}`;
4414
+ const existing = usageBySessionDay.get(key) || { model: event.usage.model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, date };
3774
4415
  existing.model = event.usage.model;
3775
4416
  existing.input += event.usage.inputTokens;
3776
4417
  existing.output += event.usage.outputTokens;
3777
4418
  existing.cacheRead += event.usage.cacheReadTokens;
3778
4419
  existing.cacheWrite += event.usage.cacheWriteTokens;
3779
- usageBySession.set(event.sessionId, existing);
4420
+ usageBySessionDay.set(key, existing);
3780
4421
  continue;
3781
4422
  }
3782
4423
  let violation;
@@ -3799,9 +4440,8 @@ async function collectPrompts() {
3799
4440
  appendEvent(buffered);
3800
4441
  collected++;
3801
4442
  }
3802
- const usageHeaders = { "Content-Type": "application/json" };
3803
- if (config.authToken) usageHeaders["X-IMDL-Key"] = config.authToken;
3804
- for (const [sessionId, usage] of usageBySession) {
4443
+ for (const [key, usage] of usageBySessionDay) {
4444
+ const sessionId = key.split("|")[0];
3805
4445
  try {
3806
4446
  await fetch(`${config.apiUrl}/api/sessions/${sessionId}/usage`, {
3807
4447
  method: "POST",
@@ -3814,12 +4454,25 @@ async function collectPrompts() {
3814
4454
  }
3815
4455
  }
3816
4456
  const cursorAdapter = new CursorAdapter();
4457
+ const cursorUsageBySessionDay = /* @__PURE__ */ new Map();
3817
4458
  if (CursorAdapter.getProjectsDir()) {
3818
4459
  try {
3819
4460
  const { events: transcriptEvents, newOffsets } = cursorAdapter.extractFromTranscripts(new Date(state.lastRun), state.offsets);
3820
4461
  state.offsets = newOffsets;
3821
4462
  for (const event of transcriptEvents) {
3822
- let violation;
4463
+ if (event.usage) {
4464
+ const date = event.timestamp ? event.timestamp.slice(0, 10) : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4465
+ const key = `${event.sessionId}|${date}`;
4466
+ const existing = cursorUsageBySessionDay.get(key) || { model: event.usage.model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, date };
4467
+ existing.model = event.usage.model;
4468
+ existing.input += event.usage.inputTokens;
4469
+ existing.output += event.usage.outputTokens;
4470
+ existing.cacheRead += event.usage.cacheReadTokens;
4471
+ existing.cacheWrite += event.usage.cacheWriteTokens;
4472
+ cursorUsageBySessionDay.set(key, existing);
4473
+ continue;
4474
+ }
4475
+ let violation;
3823
4476
  if (event.prompt) {
3824
4477
  const scan = scanPromptForSecrets(event.prompt);
3825
4478
  if (scan.hasSecrets) {
@@ -3878,6 +4531,55 @@ async function collectPrompts() {
3878
4531
  }
3879
4532
  }
3880
4533
  }
4534
+ for (const [key, usage] of cursorUsageBySessionDay) {
4535
+ const sessionId = key.split("|")[0];
4536
+ try {
4537
+ await fetch(`${config.apiUrl}/api/sessions/${sessionId}/usage`, {
4538
+ method: "POST",
4539
+ headers: usageHeaders,
4540
+ body: JSON.stringify(usage),
4541
+ signal: AbortSignal.timeout(3e3)
4542
+ });
4543
+ } catch {
4544
+ }
4545
+ }
4546
+ const codexSessionsDir = CodexAdapter.getSessionsDir();
4547
+ if (codexSessionsDir) {
4548
+ try {
4549
+ const codexAdapter = new CodexAdapter();
4550
+ const { events: codexEvents, newOffsets: codexOffsets } = codexAdapter.extractWithOffsets(
4551
+ new Date(state.lastRun),
4552
+ state.offsets
4553
+ );
4554
+ for (const [k, v] of Object.entries(codexOffsets)) {
4555
+ state.offsets[k] = v;
4556
+ }
4557
+ for (const event of codexEvents) {
4558
+ let violation;
4559
+ if (event.prompt) {
4560
+ const scan = scanPromptForSecrets(event.prompt);
4561
+ if (scan.hasSecrets) {
4562
+ violation = createPromptViolation(scan.findings);
4563
+ }
4564
+ }
4565
+ const buffered = {
4566
+ sessionId: event.sessionId,
4567
+ type: event.type,
4568
+ toolName: event.toolName,
4569
+ toolInput: event.toolInput ? redactObject(event.toolInput) : void 0,
4570
+ toolOutput: event.toolOutput ? String(typeof event.toolOutput === "string" ? redactObject(event.toolOutput) : JSON.stringify(redactObject(event.toolOutput))) : void 0,
4571
+ prompt: event.prompt ? redactObject(event.prompt) : void 0,
4572
+ timestamp: event.timestamp,
4573
+ cwd: event.cwd,
4574
+ agentType: "codex",
4575
+ violation
4576
+ };
4577
+ appendEvent(buffered);
4578
+ collected++;
4579
+ }
4580
+ } catch {
4581
+ }
4582
+ }
3881
4583
  if (collected > 0) {
3882
4584
  try {
3883
4585
  await tryFlush();
@@ -3902,8 +4604,8 @@ async function collectCommand() {
3902
4604
 
3903
4605
  // src/commands/daemon.ts
3904
4606
  import pc7 from "picocolors";
3905
- import { existsSync as existsSync19, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
3906
- import { join as join18 } from "path";
4607
+ import { existsSync as existsSync20, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
4608
+ import { join as join20 } from "path";
3907
4609
  var INTERVAL_MS = 3e4;
3908
4610
  async function daemonCommand() {
3909
4611
  console.log(pc7.cyan("imdl daemon started \u2014 collecting every 30s"));
@@ -3926,9 +4628,9 @@ async function daemonCommand() {
3926
4628
  setInterval(tick, INTERVAL_MS);
3927
4629
  }
3928
4630
  async function ingestAgentEvents() {
3929
- const agentLog = join18(getBufferDir(), "agent-events.ndjson");
3930
- if (!existsSync19(agentLog)) return 0;
3931
- const content = readFileSync14(agentLog, "utf-8").trim();
4631
+ const agentLog = join20(getBufferDir(), "agent-events.ndjson");
4632
+ if (!existsSync20(agentLog)) return 0;
4633
+ const content = readFileSync15(agentLog, "utf-8").trim();
3932
4634
  if (!content) return 0;
3933
4635
  const config = loadConfig();
3934
4636
  const lines = content.split("\n");
@@ -3976,14 +4678,14 @@ async function ingestAgentEvents() {
3976
4678
  }
3977
4679
  }
3978
4680
  if (sent === apiEvents.length) {
3979
- writeFileSync7(agentLog, "", { mode: 384 });
4681
+ writeFileSync8(agentLog, "", { mode: 384 });
3980
4682
  }
3981
4683
  return sent;
3982
4684
  }
3983
4685
  async function ingestShellEvents() {
3984
- const shellLog = join18(getBufferDir(), "shell-events.ndjson");
3985
- if (!existsSync19(shellLog)) return 0;
3986
- const content = readFileSync14(shellLog, "utf-8").trim();
4686
+ const shellLog = join20(getBufferDir(), "shell-events.ndjson");
4687
+ if (!existsSync20(shellLog)) return 0;
4688
+ const content = readFileSync15(shellLog, "utf-8").trim();
3987
4689
  if (!content) return 0;
3988
4690
  const config = loadConfig();
3989
4691
  const lines = content.split("\n");
@@ -4020,7 +4722,7 @@ async function ingestShellEvents() {
4020
4722
  }
4021
4723
  }
4022
4724
  if (sent === apiEvents.length) {
4023
- writeFileSync7(shellLog, "", { mode: 384 });
4725
+ writeFileSync8(shellLog, "", { mode: 384 });
4024
4726
  }
4025
4727
  return sent;
4026
4728
  }
@@ -4088,8 +4790,8 @@ async function requestCommand(options) {
4088
4790
  // src/commands/lock.ts
4089
4791
  import pc9 from "picocolors";
4090
4792
  import { createHash as createHash2 } from "crypto";
4091
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, existsSync as existsSync20 } from "fs";
4092
- import { join as join19, resolve as resolve3 } from "path";
4793
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync9, existsSync as existsSync21 } from "fs";
4794
+ import { join as join21, resolve as resolve3 } from "path";
4093
4795
  var LOCKFILE_NAME = "mcp-lock.json";
4094
4796
  function computeIntegrity(server, identity) {
4095
4797
  const payload = JSON.stringify({
@@ -4104,7 +4806,7 @@ function computeIntegrity(server, identity) {
4104
4806
  }
4105
4807
  function getLockfilePath(customPath) {
4106
4808
  if (customPath) return resolve3(customPath);
4107
- return join19(process.cwd(), LOCKFILE_NAME);
4809
+ return join21(process.cwd(), LOCKFILE_NAME);
4108
4810
  }
4109
4811
  async function lockCommand(options) {
4110
4812
  const detection = await detectMCPConfigs(options.path);
@@ -4151,7 +4853,7 @@ async function lockCommand(options) {
4151
4853
  servers: entries
4152
4854
  };
4153
4855
  const lockPath = getLockfilePath(options.output);
4154
- writeFileSync8(lockPath, JSON.stringify(lockfile, null, 2), { mode: 420 });
4856
+ writeFileSync9(lockPath, JSON.stringify(lockfile, null, 2), { mode: 420 });
4155
4857
  if (options.json) {
4156
4858
  console.log(JSON.stringify(lockfile, null, 2));
4157
4859
  } else {
@@ -4174,7 +4876,7 @@ async function lockCommand(options) {
4174
4876
  }
4175
4877
  async function verifyCommand(options) {
4176
4878
  const lockPath = getLockfilePath(options.path);
4177
- if (!existsSync20(lockPath)) {
4879
+ if (!existsSync21(lockPath)) {
4178
4880
  if (options.json) {
4179
4881
  console.log(JSON.stringify({ error: "No lockfile found. Run `imdl lock` first.", path: lockPath }));
4180
4882
  } else {
@@ -4186,7 +4888,7 @@ async function verifyCommand(options) {
4186
4888
  }
4187
4889
  let lockfile;
4188
4890
  try {
4189
- lockfile = JSON.parse(readFileSync15(lockPath, "utf-8"));
4891
+ lockfile = JSON.parse(readFileSync16(lockPath, "utf-8"));
4190
4892
  } catch {
4191
4893
  console.log(pc9.red("Failed to parse lockfile."));
4192
4894
  process.exit(1);
@@ -4595,12 +5297,12 @@ async function checkCompliance(developerId) {
4595
5297
  }
4596
5298
 
4597
5299
  // src/permissions/fixer.ts
4598
- import { existsSync as existsSync21, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
4599
- import { join as join20 } from "path";
4600
- import { homedir as homedir14 } from "os";
5300
+ import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
5301
+ import { join as join22 } from "path";
5302
+ import { homedir as homedir16 } from "os";
4601
5303
  function buildFixesFromChanges(changes) {
4602
5304
  const fixes = [];
4603
- const home = homedir14();
5305
+ const home = homedir16();
4604
5306
  for (const change of changes) {
4605
5307
  const fix = buildFixForAgent(change, home);
4606
5308
  if (fix) fixes.push(fix);
@@ -4610,10 +5312,10 @@ function buildFixesFromChanges(changes) {
4610
5312
  function buildFixForAgent(change, home) {
4611
5313
  const { agentType, category, action, targetGrant } = change;
4612
5314
  if (agentType === "claude-code") {
4613
- const configPath = join20(home, ".claude", "settings.json");
4614
- if (!existsSync21(configPath)) return null;
5315
+ const configPath = join22(home, ".claude", "settings.json");
5316
+ if (!existsSync22(configPath)) return null;
4615
5317
  try {
4616
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
5318
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4617
5319
  const tools = settings.allowedTools || [];
4618
5320
  if (category === "shell") {
4619
5321
  if (!tools.includes("Bash") && !tools.some((t) => t.startsWith("Bash("))) return null;
@@ -4641,10 +5343,10 @@ function buildFixForAgent(change, home) {
4641
5343
  }
4642
5344
  }
4643
5345
  if (agentType === "cursor") {
4644
- const configPath = join20(home, ".cursor", "settings.json");
4645
- if (!existsSync21(configPath)) return null;
5346
+ const configPath = join22(home, ".cursor", "settings.json");
5347
+ if (!existsSync22(configPath)) return null;
4646
5348
  try {
4647
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
5349
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4648
5350
  if ((category === "agentic_mode" || category === "shell") && settings["cursor.composer.yoloMode"] === true) {
4649
5351
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Disable YOLO mode" };
4650
5352
  }
@@ -4653,10 +5355,10 @@ function buildFixForAgent(change, home) {
4653
5355
  }
4654
5356
  }
4655
5357
  if (agentType === "codex") {
4656
- const configPath = join20(home, ".codex", "config.json");
4657
- if (!existsSync21(configPath)) return null;
5358
+ const configPath = join22(home, ".codex", "config.json");
5359
+ if (!existsSync22(configPath)) return null;
4658
5360
  try {
4659
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
5361
+ const config = JSON.parse(readFileSync17(configPath, "utf-8"));
4660
5362
  if ((category === "agentic_mode" || category === "shell") && config.sandbox_mode === "full_auto") {
4661
5363
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Switch to supervised mode" };
4662
5364
  }
@@ -4665,10 +5367,10 @@ function buildFixForAgent(change, home) {
4665
5367
  }
4666
5368
  }
4667
5369
  if (agentType === "copilot") {
4668
- const configPath = join20(home, "Library", "Application Support", "Code", "User", "settings.json");
4669
- if (!existsSync21(configPath)) return null;
5370
+ const configPath = join22(home, "Library", "Application Support", "Code", "User", "settings.json");
5371
+ if (!existsSync22(configPath)) return null;
4670
5372
  try {
4671
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
5373
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4672
5374
  if ((category === "agentic_mode" || category === "shell") && (settings["github.copilot.chat.agent.autoApprove"] || settings["chat.agent.autoApprove"])) {
4673
5375
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Disable autoApprove" };
4674
5376
  }
@@ -4678,10 +5380,10 @@ function buildFixForAgent(change, home) {
4678
5380
  }
4679
5381
  if (agentType === "windsurf") {
4680
5382
  if (category === "mcp_tools") {
4681
- const configPath = join20(home, ".windsurf", "mcp.json");
4682
- if (!existsSync21(configPath)) return null;
5383
+ const configPath = join22(home, ".windsurf", "mcp.json");
5384
+ if (!existsSync22(configPath)) return null;
4683
5385
  try {
4684
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
5386
+ const config = JSON.parse(readFileSync17(configPath, "utf-8"));
4685
5387
  const servers = Object.keys(config.mcpServers || {});
4686
5388
  if (servers.length === 0) return null;
4687
5389
  return { agentType, category, currentGrant: "unrestricted", newGrant: "denied", configPath, description: `Remove MCP servers (${servers.join(", ")})` };
@@ -4694,7 +5396,7 @@ function buildFixForAgent(change, home) {
4694
5396
  }
4695
5397
  function applyFix(fix) {
4696
5398
  try {
4697
- const content = readFileSync16(fix.configPath, "utf-8");
5399
+ const content = readFileSync17(fix.configPath, "utf-8");
4698
5400
  const config = JSON.parse(content);
4699
5401
  let modified = false;
4700
5402
  if (fix.agentType === "claude-code") {
@@ -4748,7 +5450,7 @@ function applyFix(fix) {
4748
5450
  }
4749
5451
  }
4750
5452
  if (modified) {
4751
- writeFileSync9(fix.configPath, JSON.stringify(config, null, 2) + "\n");
5453
+ writeFileSync10(fix.configPath, JSON.stringify(config, null, 2) + "\n");
4752
5454
  return { fix, applied: true };
4753
5455
  }
4754
5456
  return { fix, applied: false, error: "No changes needed" };
@@ -4985,13 +5687,13 @@ function grantLevel(grant) {
4985
5687
 
4986
5688
  // src/commands/gateway.ts
4987
5689
  import pc12 from "picocolors";
4988
- import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync3 } from "fs";
4989
- import { join as join21 } from "path";
4990
- import { homedir as homedir15 } from "os";
5690
+ import { existsSync as existsSync23, readFileSync as readFileSync18, writeFileSync as writeFileSync11, mkdirSync as mkdirSync3 } from "fs";
5691
+ import { join as join23 } from "path";
5692
+ import { homedir as homedir17 } from "os";
4991
5693
  import { execSync as execSync2, spawn } from "child_process";
4992
- var IMDL_DIR3 = join21(homedir15(), ".imdl");
4993
- var GATEWAY_CONFIG = join21(IMDL_DIR3, "gateway.toml");
4994
- var GATEWAY_PID_FILE = join21(IMDL_DIR3, "gateway.pid");
5694
+ var IMDL_DIR3 = join23(homedir17(), ".imdl");
5695
+ var GATEWAY_CONFIG = join23(IMDL_DIR3, "gateway.toml");
5696
+ var GATEWAY_PID_FILE = join23(IMDL_DIR3, "gateway.pid");
4995
5697
  var DEFAULT_PORT = 9443;
4996
5698
  async function gatewayCommand(opts) {
4997
5699
  const status = await getGatewayStatus();
@@ -5066,8 +5768,8 @@ async function gatewayStartCommand(opts) {
5066
5768
  });
5067
5769
  child.unref();
5068
5770
  if (child.pid) {
5069
- if (!existsSync22(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5070
- writeFileSync10(GATEWAY_PID_FILE, String(child.pid));
5771
+ if (!existsSync23(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5772
+ writeFileSync11(GATEWAY_PID_FILE, String(child.pid));
5071
5773
  console.log(pc12.green(`Gateway started (PID ${child.pid}, port ${port})`));
5072
5774
  console.log(pc12.dim(` Config: ${GATEWAY_CONFIG}`));
5073
5775
  console.log(pc12.dim(` Set ANTHROPIC_BASE_URL=http://127.0.0.1:${port} to intercept traffic`));
@@ -5094,8 +5796,8 @@ async function gatewayStopCommand() {
5094
5796
  console.log(pc12.yellow("Could not stop gateway \u2014 process may have already exited."));
5095
5797
  }
5096
5798
  try {
5097
- const { unlinkSync: unlinkSync3 } = await import("fs");
5098
- unlinkSync3(GATEWAY_PID_FILE);
5799
+ const { unlinkSync: unlinkSync4 } = await import("fs");
5800
+ unlinkSync4(GATEWAY_PID_FILE);
5099
5801
  } catch {
5100
5802
  }
5101
5803
  }
@@ -5126,12 +5828,12 @@ async function gatewayDisableCommand(opts) {
5126
5828
  console.log(pc12.dim("Restart gateway for changes to take effect."));
5127
5829
  }
5128
5830
  async function gatewayAlertsCommand(opts) {
5129
- const logPath = join21(IMDL_DIR3, "gateway-alerts.jsonl");
5130
- if (!existsSync22(logPath)) {
5831
+ const logPath = join23(IMDL_DIR3, "gateway-alerts.jsonl");
5832
+ if (!existsSync23(logPath)) {
5131
5833
  console.log(pc12.dim("No alerts logged yet."));
5132
5834
  return;
5133
5835
  }
5134
- const lines = readFileSync17(logPath, "utf-8").split("\n").filter(Boolean);
5836
+ const lines = readFileSync18(logPath, "utf-8").split("\n").filter(Boolean);
5135
5837
  const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
5136
5838
  const recent = lines.slice(-limit);
5137
5839
  if (opts.json) {
@@ -5165,6 +5867,23 @@ async function gatewayAlertsCommand(opts) {
5165
5867
  }
5166
5868
  }
5167
5869
  }
5870
+ async function gatewayDashboardCommand() {
5871
+ const port = getConfiguredPort();
5872
+ const url = `http://127.0.0.1:${port}/dashboard`;
5873
+ try {
5874
+ const controller = new AbortController();
5875
+ const timeout = setTimeout(() => controller.abort(), 2e3);
5876
+ const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: controller.signal });
5877
+ clearTimeout(timeout);
5878
+ if (!res.ok) throw new Error();
5879
+ } catch {
5880
+ console.log(pc12.red("Gateway is not running. Start it first: `imdl gateway start`"));
5881
+ return;
5882
+ }
5883
+ console.log(pc12.green(`Opening dashboard: ${url}`));
5884
+ const { exec } = await import("child_process");
5885
+ exec(`open "${url}"`);
5886
+ }
5168
5887
  async function gatewayCostCommand(opts) {
5169
5888
  const port = getConfiguredPort();
5170
5889
  let data;
@@ -5233,9 +5952,9 @@ async function getGatewayStatus() {
5233
5952
  return { running: true, pid, port, stats, config };
5234
5953
  }
5235
5954
  function getRunningPid() {
5236
- if (!existsSync22(GATEWAY_PID_FILE)) return null;
5955
+ if (!existsSync23(GATEWAY_PID_FILE)) return null;
5237
5956
  try {
5238
- const pid = parseInt(readFileSync17(GATEWAY_PID_FILE, "utf-8").trim(), 10);
5957
+ const pid = parseInt(readFileSync18(GATEWAY_PID_FILE, "utf-8").trim(), 10);
5239
5958
  return isNaN(pid) ? null : pid;
5240
5959
  } catch {
5241
5960
  return null;
@@ -5250,9 +5969,9 @@ function isProcessAlive(pid) {
5250
5969
  }
5251
5970
  }
5252
5971
  function getConfiguredPort() {
5253
- if (!existsSync22(GATEWAY_CONFIG)) return DEFAULT_PORT;
5972
+ if (!existsSync23(GATEWAY_CONFIG)) return DEFAULT_PORT;
5254
5973
  try {
5255
- const content = readFileSync17(GATEWAY_CONFIG, "utf-8");
5974
+ const content = readFileSync18(GATEWAY_CONFIG, "utf-8");
5256
5975
  const match = content.match(/listen_port\s*=\s*(\d+)/);
5257
5976
  return match ? parseInt(match[1], 10) : DEFAULT_PORT;
5258
5977
  } catch {
@@ -5261,12 +5980,12 @@ function getConfiguredPort() {
5261
5980
  }
5262
5981
  function findGatewayBinary() {
5263
5982
  const candidates = [
5264
- join21(process.cwd(), "target", "release", "imdl-gateway"),
5265
- join21(process.cwd(), "..", "ai-gateway", "target", "release", "imdl-gateway"),
5266
- join21(homedir15(), ".imdl", "bin", "imdl-gateway")
5983
+ join23(process.cwd(), "target", "release", "imdl-gateway"),
5984
+ join23(process.cwd(), "..", "ai-gateway", "target", "release", "imdl-gateway"),
5985
+ join23(homedir17(), ".imdl", "bin", "imdl-gateway")
5267
5986
  ];
5268
5987
  for (const path of candidates) {
5269
- if (existsSync22(path)) return path;
5988
+ if (existsSync23(path)) return path;
5270
5989
  }
5271
5990
  try {
5272
5991
  const result = execSync2("which imdl-gateway", { encoding: "utf-8" }).trim();
@@ -5276,8 +5995,8 @@ function findGatewayBinary() {
5276
5995
  return null;
5277
5996
  }
5278
5997
  function ensureConfig(port) {
5279
- if (existsSync22(GATEWAY_CONFIG)) return;
5280
- if (!existsSync22(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5998
+ if (existsSync23(GATEWAY_CONFIG)) return;
5999
+ if (!existsSync23(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5281
6000
  const defaultToml = `# IMDL AI Gateway configuration
5282
6001
  listen_host = "127.0.0.1"
5283
6002
  listen_port = ${port}
@@ -5304,7 +6023,7 @@ openai_url = "https://api.openai.com"
5304
6023
  [audit]
5305
6024
  enabled = true
5306
6025
  `;
5307
- writeFileSync10(GATEWAY_CONFIG, defaultToml, { mode: 384 });
6026
+ writeFileSync11(GATEWAY_CONFIG, defaultToml, { mode: 384 });
5308
6027
  }
5309
6028
  function normalizeModule(input) {
5310
6029
  const map = {
@@ -5321,13 +6040,13 @@ function normalizeModule(input) {
5321
6040
  return map[input.toLowerCase()] ?? null;
5322
6041
  }
5323
6042
  function loadGatewayToml() {
5324
- if (!existsSync22(GATEWAY_CONFIG)) {
6043
+ if (!existsSync23(GATEWAY_CONFIG)) {
5325
6044
  ensureConfig(DEFAULT_PORT);
5326
6045
  }
5327
- return readFileSync17(GATEWAY_CONFIG, "utf-8");
6046
+ return readFileSync18(GATEWAY_CONFIG, "utf-8");
5328
6047
  }
5329
6048
  function saveGatewayToml(content) {
5330
- writeFileSync10(GATEWAY_CONFIG, content, { mode: 384 });
6049
+ writeFileSync11(GATEWAY_CONFIG, content, { mode: 384 });
5331
6050
  }
5332
6051
  function setModuleEnabled(content, module, enabled) {
5333
6052
  const sectionRegex = new RegExp(`(\\[${module}\\][^\\[]*?)enabled\\s*=\\s*(true|false)`, "s");
@@ -5341,6 +6060,153 @@ function printModule(name, enabled, mode) {
5341
6060
  const modeStr = mode ? pc12.dim(` (${mode})`) : "";
5342
6061
  console.log(` ${icon} ${name}${modeStr}`);
5343
6062
  }
6063
+ async function gatewayActivateCommand(opts = {}) {
6064
+ const port = getConfiguredPort();
6065
+ try {
6066
+ const controller = new AbortController();
6067
+ const timeout = setTimeout(() => controller.abort(), 3e3);
6068
+ const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: controller.signal });
6069
+ clearTimeout(timeout);
6070
+ if (!res.ok) {
6071
+ console.log(pc12.red("Gateway is not responding. Start it first: `imdl gateway start`"));
6072
+ return;
6073
+ }
6074
+ } catch {
6075
+ console.log(pc12.red(`Cannot reach gateway on port ${port}. Start it first: \`imdl gateway start\``));
6076
+ return;
6077
+ }
6078
+ if (opts.copilot) {
6079
+ activateCopilot(port);
6080
+ return;
6081
+ }
6082
+ const claudeSettings = getClaudeSettingsPath();
6083
+ if (!claudeSettings) {
6084
+ console.log(pc12.red("Claude Code settings not found at ~/.claude/settings.json"));
6085
+ return;
6086
+ }
6087
+ let settings;
6088
+ try {
6089
+ settings = JSON.parse(readFileSync18(claudeSettings, "utf-8"));
6090
+ } catch {
6091
+ console.log(pc12.red("Could not parse Claude Code settings."));
6092
+ return;
6093
+ }
6094
+ if (!settings.env) settings.env = {};
6095
+ settings.env.ANTHROPIC_BEDROCK_BASE_URL = `http://127.0.0.1:${port}`;
6096
+ if (opts.codex) {
6097
+ settings.env.OPENAI_BASE_URL = `http://127.0.0.1:${port}`;
6098
+ updateCodexConfig(port, true);
6099
+ }
6100
+ writeFileSync11(claudeSettings, JSON.stringify(settings, null, 2) + "\n");
6101
+ console.log(pc12.green("Gateway activated for Claude Code"));
6102
+ console.log(pc12.dim(` ANTHROPIC_BEDROCK_BASE_URL \u2192 http://127.0.0.1:${port}`));
6103
+ if (opts.codex) {
6104
+ console.log(pc12.dim(` OPENAI_BASE_URL \u2192 http://127.0.0.1:${port}`));
6105
+ console.log(pc12.dim(" Codex WebSocket traffic will also route through the gateway."));
6106
+ }
6107
+ console.log(pc12.dim(" New Claude Code sessions will route through the gateway."));
6108
+ console.log(pc12.dim(" Run `imdl gateway deactivate` to revert immediately."));
6109
+ console.log("");
6110
+ console.log(pc12.yellow(" \u26A0 Current session is unaffected. Only NEW sessions use the gateway."));
6111
+ console.log(pc12.yellow(" \u26A0 If gateway goes down, run `imdl gateway deactivate` to restore direct access."));
6112
+ }
6113
+ function activateCopilot(port) {
6114
+ const caCertPath = join23(homedir17(), ".imdl", "gateway-ca.pem");
6115
+ if (!existsSync23(caCertPath)) {
6116
+ console.log(pc12.red("CA certificate not found at " + caCertPath));
6117
+ console.log(pc12.dim("The gateway generates this automatically on startup."));
6118
+ console.log(pc12.dim("Start the gateway first: `imdl gateway start`"));
6119
+ return;
6120
+ }
6121
+ console.log("");
6122
+ console.log(pc12.bold(pc12.cyan("Copilot Gateway Activation")));
6123
+ console.log(pc12.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
6124
+ console.log("");
6125
+ console.log(pc12.green("CA certificate: ") + caCertPath);
6126
+ console.log("");
6127
+ console.log(pc12.bold(" VS Code Settings (settings.json):"));
6128
+ console.log("");
6129
+ console.log(pc12.cyan(" {"));
6130
+ console.log(pc12.cyan(` "http.proxy": "http://127.0.0.1:${port}",`));
6131
+ console.log(pc12.cyan(' "http.proxyStrictSSL": false'));
6132
+ console.log(pc12.cyan(" }"));
6133
+ console.log("");
6134
+ console.log(pc12.bold(" OR use NODE_EXTRA_CA_CERTS (strict TLS):"));
6135
+ console.log("");
6136
+ console.log(pc12.cyan(` export NODE_EXTRA_CA_CERTS="${caCertPath}"`));
6137
+ console.log("");
6138
+ console.log(pc12.dim(" Then set the proxy in VS Code settings:"));
6139
+ console.log(pc12.cyan(` "http.proxy": "http://127.0.0.1:${port}"`));
6140
+ console.log("");
6141
+ console.log(pc12.bold(" What happens:"));
6142
+ console.log(pc12.dim(" - Copilot traffic (api.githubcopilot.com) is intercepted"));
6143
+ console.log(pc12.dim(" - Context optimization reduces token usage"));
6144
+ console.log(pc12.dim(" - Cost tracking reports Copilot spend"));
6145
+ console.log(pc12.dim(" - All other HTTPS traffic tunnels through transparently"));
6146
+ console.log("");
6147
+ }
6148
+ async function gatewayDeactivateCommand() {
6149
+ const claudeSettings = getClaudeSettingsPath();
6150
+ if (!claudeSettings) {
6151
+ console.log(pc12.red("Claude Code settings not found at ~/.claude/settings.json"));
6152
+ return;
6153
+ }
6154
+ let settings;
6155
+ try {
6156
+ settings = JSON.parse(readFileSync18(claudeSettings, "utf-8"));
6157
+ } catch {
6158
+ console.log(pc12.red("Could not parse Claude Code settings."));
6159
+ return;
6160
+ }
6161
+ if (settings.env) {
6162
+ delete settings.env.ANTHROPIC_BEDROCK_BASE_URL;
6163
+ delete settings.env.ANTHROPIC_BASE_URL;
6164
+ delete settings.env.OPENAI_BASE_URL;
6165
+ }
6166
+ writeFileSync11(claudeSettings, JSON.stringify(settings, null, 2) + "\n");
6167
+ updateCodexConfig(0, false);
6168
+ console.log(pc12.green("Gateway deactivated"));
6169
+ console.log(pc12.dim(" Claude Code will connect directly to Bedrock/Anthropic (no gateway)."));
6170
+ console.log(pc12.dim(" Codex will connect directly to OpenAI (no gateway)."));
6171
+ console.log(pc12.dim(" Existing sessions are unaffected \u2014 only new sessions pick up this change."));
6172
+ }
6173
+ function getClaudeSettingsPath() {
6174
+ const candidates = [
6175
+ join23(homedir17(), ".claude", "settings.json")
6176
+ ];
6177
+ for (const p of candidates) {
6178
+ if (existsSync23(p)) return p;
6179
+ }
6180
+ return null;
6181
+ }
6182
+ function updateCodexConfig(port, enable) {
6183
+ const codexConfig = join23(homedir17(), ".codex", "config.toml");
6184
+ if (!existsSync23(codexConfig)) return;
6185
+ try {
6186
+ let content = readFileSync18(codexConfig, "utf-8");
6187
+ if (enable) {
6188
+ if (content.includes("# openai_base_url")) {
6189
+ content = content.replace(/# *openai_base_url *= *"[^"]*"/, `openai_base_url = "http://127.0.0.1:${port}"`);
6190
+ } else if (content.includes("openai_base_url")) {
6191
+ content = content.replace(/openai_base_url *= *"[^"]*"/, `openai_base_url = "http://127.0.0.1:${port}"`);
6192
+ } else {
6193
+ if (content.includes("[model_providers]")) {
6194
+ content = content.replace("[model_providers]", `openai_base_url = "http://127.0.0.1:${port}"
6195
+
6196
+ [model_providers]`);
6197
+ } else {
6198
+ content += `
6199
+ openai_base_url = "http://127.0.0.1:${port}"
6200
+ `;
6201
+ }
6202
+ }
6203
+ } else {
6204
+ content = content.replace(/^(openai_base_url *= *"[^"]*")/m, "# $1");
6205
+ }
6206
+ writeFileSync11(codexConfig, content);
6207
+ } catch {
6208
+ }
6209
+ }
5344
6210
  function formatUptime(seconds) {
5345
6211
  if (seconds < 60) return `${seconds}s`;
5346
6212
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
@@ -5349,6 +6215,834 @@ function formatUptime(seconds) {
5349
6215
  return `${h}h ${m}m`;
5350
6216
  }
5351
6217
 
6218
+ // src/commands/sync-history.ts
6219
+ import { createReadStream as createReadStream2, existsSync as existsSync24, readdirSync as readdirSync6, statSync as statSync11 } from "fs";
6220
+ import { createInterface as createInterface2 } from "readline";
6221
+ import { join as join24 } from "path";
6222
+ import { homedir as homedir18 } from "os";
6223
+ import pc13 from "picocolors";
6224
+ var BATCH_SIZE2 = 50;
6225
+ var SEND_TIMEOUT = 15e3;
6226
+ async function syncHistoryCommand(opts) {
6227
+ const config = loadConfig();
6228
+ if (!config.apiUrl || !config.developerId) {
6229
+ console.log(pc13.red("Not configured. Run `imdl init` first."));
6230
+ return;
6231
+ }
6232
+ const days = parseInt(opts.days || "30", 10);
6233
+ const since = new Date(Date.now() - days * 864e5);
6234
+ const agentFilter = opts.agent?.toLowerCase();
6235
+ console.log(pc13.bold("Syncing historical session data"));
6236
+ console.log(pc13.dim(` Period: last ${days} days (since ${since.toISOString().split("T")[0]})`));
6237
+ console.log(pc13.dim(` Agent filter: ${agentFilter || "all"}`));
6238
+ if (opts.dryRun) console.log(pc13.yellow(" DRY RUN \u2014 no data will be sent"));
6239
+ console.log("");
6240
+ const stats = {
6241
+ filesScanned: 0,
6242
+ filesSkipped: 0,
6243
+ eventsProcessed: 0,
6244
+ eventsSent: 0,
6245
+ violationsFound: 0,
6246
+ secretsFound: 0,
6247
+ errors: 0,
6248
+ bytesProcessed: 0
6249
+ };
6250
+ const sessionFiles = [];
6251
+ if (!agentFilter || agentFilter === "claude-code" || agentFilter === "claude") {
6252
+ const claudeDirs = ClaudeCodeAdapter.getAllProjectDirs();
6253
+ for (const dir of claudeDirs) {
6254
+ const sessionsDir = join24(dir, "sessions");
6255
+ if (!existsSync24(sessionsDir)) continue;
6256
+ for (const file of safeReaddir(sessionsDir)) {
6257
+ if (!file.endsWith(".jsonl")) continue;
6258
+ const full = join24(sessionsDir, file);
6259
+ const stat = safeStat(full);
6260
+ if (!stat) continue;
6261
+ if (stat.mtimeMs < since.getTime()) continue;
6262
+ sessionFiles.push({ path: full, agent: "claude-code", size: stat.size });
6263
+ }
6264
+ }
6265
+ }
6266
+ if (!agentFilter || agentFilter === "codex") {
6267
+ const codexDir = join24(homedir18(), ".codex", "sessions");
6268
+ if (existsSync24(codexDir)) {
6269
+ const files = findJsonlFiles(codexDir, since);
6270
+ for (const f of files) {
6271
+ const stat = safeStat(f);
6272
+ if (stat) sessionFiles.push({ path: f, agent: "codex", size: stat.size });
6273
+ }
6274
+ }
6275
+ }
6276
+ if (!agentFilter || agentFilter === "cursor") {
6277
+ const cursorProjects = join24(homedir18(), ".cursor", "projects");
6278
+ if (existsSync24(cursorProjects)) {
6279
+ const files = findJsonlFiles(cursorProjects, since);
6280
+ for (const f of files) {
6281
+ const stat = safeStat(f);
6282
+ if (stat) sessionFiles.push({ path: f, agent: "cursor", size: stat.size });
6283
+ }
6284
+ }
6285
+ }
6286
+ const totalSize = sessionFiles.reduce((sum, f) => sum + f.size, 0);
6287
+ console.log(pc13.dim(`Found ${sessionFiles.length} session files (${formatBytes2(totalSize)})`));
6288
+ console.log("");
6289
+ if (sessionFiles.length === 0) {
6290
+ console.log(pc13.dim("No session files found for the specified period."));
6291
+ return;
6292
+ }
6293
+ for (let i = 0; i < sessionFiles.length; i++) {
6294
+ const { path: filePath, agent, size } = sessionFiles[i];
6295
+ const shortPath = filePath.replace(homedir18(), "~");
6296
+ const progress = `[${i + 1}/${sessionFiles.length}]`;
6297
+ process.stdout.write(`${pc13.dim(progress)} ${shortPath} (${formatBytes2(size)})...`);
6298
+ try {
6299
+ const result = await processFile(filePath, agent, since, config, opts.dryRun || false);
6300
+ stats.filesScanned++;
6301
+ stats.eventsProcessed += result.events;
6302
+ stats.eventsSent += result.sent;
6303
+ stats.violationsFound += result.violations;
6304
+ stats.secretsFound += result.secrets;
6305
+ stats.bytesProcessed += size;
6306
+ const parts = [];
6307
+ if (result.events > 0) parts.push(`${result.events} events`);
6308
+ if (result.violations > 0) parts.push(pc13.yellow(`${result.violations} violations`));
6309
+ if (result.secrets > 0) parts.push(pc13.red(`${result.secrets} secrets`));
6310
+ if (parts.length > 0) {
6311
+ console.log(` ${parts.join(", ")}`);
6312
+ } else {
6313
+ console.log(pc13.dim(" (no new events)"));
6314
+ }
6315
+ } catch {
6316
+ stats.errors++;
6317
+ stats.filesSkipped++;
6318
+ console.log(pc13.red(" error"));
6319
+ }
6320
+ }
6321
+ console.log("");
6322
+ console.log(pc13.bold("\u2500\u2500\u2500 Sync Complete \u2500\u2500\u2500"));
6323
+ console.log(` Files processed: ${stats.filesScanned} (${formatBytes2(stats.bytesProcessed)})`);
6324
+ if (stats.filesSkipped > 0) console.log(` Files skipped: ${pc13.yellow(String(stats.filesSkipped))}`);
6325
+ console.log(` Events found: ${stats.eventsProcessed}`);
6326
+ if (!opts.dryRun) console.log(` Events synced: ${pc13.green(String(stats.eventsSent))}`);
6327
+ if (stats.violationsFound > 0) {
6328
+ console.log(` ${pc13.yellow("\u26A0")} Historical violations: ${pc13.yellow(String(stats.violationsFound))}`);
6329
+ console.log(pc13.dim(" These violations occurred before IMDL was monitoring."));
6330
+ console.log(pc13.dim(" They are tagged as historical in the dashboard."));
6331
+ }
6332
+ if (stats.secretsFound > 0) {
6333
+ console.log(` ${pc13.red("\u26A0")} Secrets detected: ${pc13.red(String(stats.secretsFound))}`);
6334
+ console.log(pc13.dim(" Secrets were found in historical prompts/tool calls."));
6335
+ console.log(pc13.dim(" Review in Dashboard \u2192 Violations (filter: historical)."));
6336
+ }
6337
+ if (stats.errors > 0) console.log(` Errors: ${pc13.red(String(stats.errors))}`);
6338
+ }
6339
+ async function processFile(filePath, agent, since, config, dryRun) {
6340
+ const result = { events: 0, sent: 0, violations: 0, secrets: 0 };
6341
+ const batch = [];
6342
+ const sessionId = deriveSessionId(filePath, agent);
6343
+ let cwd;
6344
+ const rl = createInterface2({
6345
+ input: createReadStream2(filePath, { encoding: "utf-8" }),
6346
+ crlfDelay: Infinity
6347
+ });
6348
+ for await (const line of rl) {
6349
+ if (!line.trim()) continue;
6350
+ let entry;
6351
+ try {
6352
+ entry = JSON.parse(line);
6353
+ } catch {
6354
+ continue;
6355
+ }
6356
+ const ts = entry.timestamp || entry.ts;
6357
+ if (ts) {
6358
+ const entryTime = new Date(ts).getTime();
6359
+ if (entryTime < since.getTime()) continue;
6360
+ }
6361
+ const event = parseEntry(entry, agent, sessionId, cwd);
6362
+ if (!event && entry.type === "turn_context" && entry.payload?.cwd) {
6363
+ cwd = entry.payload.cwd;
6364
+ continue;
6365
+ }
6366
+ if (!event) continue;
6367
+ if (event.cwd) cwd = event.cwd;
6368
+ let violation;
6369
+ if (event.prompt) {
6370
+ const scan = scanPromptForSecrets(event.prompt);
6371
+ if (scan.hasSecrets) {
6372
+ violation = createPromptViolation(scan.findings);
6373
+ if (violation) violation.action = "logged";
6374
+ result.violations++;
6375
+ result.secrets += scan.findings.length;
6376
+ }
6377
+ }
6378
+ if (event.toolInput) {
6379
+ const inputStr = JSON.stringify(event.toolInput);
6380
+ const scan = scanPromptForSecrets(inputStr);
6381
+ if (scan.hasSecrets && !violation) {
6382
+ violation = {
6383
+ policyId: "builtin:prompt-secrets",
6384
+ ruleId: "tool-input-secret-scan",
6385
+ action: "logged",
6386
+ reason: `Secret in tool input (historical): ${scan.findings.map((f) => f.label).join(", ")}`
6387
+ };
6388
+ result.violations++;
6389
+ result.secrets += scan.findings.length;
6390
+ }
6391
+ }
6392
+ const buffered = {
6393
+ sessionId: event.sessionId,
6394
+ type: event.type,
6395
+ toolName: event.toolName,
6396
+ toolInput: event.toolInput ? redactObject(event.toolInput) : void 0,
6397
+ toolOutput: event.toolOutput ? redactObject(event.toolOutput) : void 0,
6398
+ prompt: event.prompt ? redactObject(event.prompt) : void 0,
6399
+ timestamp: event.timestamp,
6400
+ cwd: event.cwd || cwd,
6401
+ agentType: agent,
6402
+ violation
6403
+ };
6404
+ batch.push(buffered);
6405
+ result.events++;
6406
+ if (batch.length >= BATCH_SIZE2) {
6407
+ if (!dryRun) {
6408
+ const sent = await sendBatch2(config, sessionId, batch.splice(0, BATCH_SIZE2), agent);
6409
+ if (sent) result.sent += BATCH_SIZE2;
6410
+ } else {
6411
+ batch.splice(0, BATCH_SIZE2);
6412
+ }
6413
+ }
6414
+ }
6415
+ if (batch.length > 0 && !dryRun) {
6416
+ const sent = await sendBatch2(config, sessionId, batch, agent);
6417
+ if (sent) result.sent += batch.length;
6418
+ } else if (batch.length > 0) {
6419
+ }
6420
+ return result;
6421
+ }
6422
+ function parseEntry(entry, agent, sessionId, cwd) {
6423
+ const ts = entry.timestamp || entry.ts || (/* @__PURE__ */ new Date()).toISOString();
6424
+ if (agent === "codex") {
6425
+ const ptype = entry.payload?.type;
6426
+ if (ptype === "user_message" && entry.payload.message?.trim()) {
6427
+ return { sessionId, type: "user_prompt", prompt: entry.payload.message.trim().slice(0, 1e4), timestamp: ts, cwd };
6428
+ }
6429
+ if (ptype === "function_call" && entry.payload.name) {
6430
+ let toolInput;
6431
+ if (entry.payload.arguments) {
6432
+ try {
6433
+ toolInput = JSON.parse(entry.payload.arguments);
6434
+ } catch {
6435
+ toolInput = { raw: entry.payload.arguments };
6436
+ }
6437
+ }
6438
+ return { sessionId, type: "pre_tool_use", toolName: entry.payload.name, toolInput, timestamp: ts, cwd };
6439
+ }
6440
+ if (ptype === "function_call_output" && entry.payload.output) {
6441
+ return { sessionId, type: "post_tool_use", toolName: entry.payload.call_id || "unknown", toolOutput: entry.payload.output.slice(0, 1e4), timestamp: ts, cwd };
6442
+ }
6443
+ return null;
6444
+ }
6445
+ if (agent === "claude-code") {
6446
+ if (entry.type === "human" && entry.message?.content) {
6447
+ const text = Array.isArray(entry.message.content) ? entry.message.content.filter((b) => b.type === "text").map((b) => b.text).join("\n") : typeof entry.message.content === "string" ? entry.message.content : "";
6448
+ if (text.trim()) {
6449
+ return { sessionId, type: "user_prompt", prompt: text.trim().slice(0, 1e4), timestamp: ts, cwd: entry.cwd || cwd };
6450
+ }
6451
+ }
6452
+ if (entry.type === "assistant" && entry.message?.content) {
6453
+ const blocks = Array.isArray(entry.message.content) ? entry.message.content : [];
6454
+ for (const block of blocks) {
6455
+ if (block.type === "tool_use") {
6456
+ return { sessionId, type: "pre_tool_use", toolName: block.name, toolInput: block.input, timestamp: ts, cwd: entry.cwd || cwd };
6457
+ }
6458
+ }
6459
+ }
6460
+ if (entry.type === "tool_result" || entry.type === "result") {
6461
+ const output = typeof entry.content === "string" ? entry.content : JSON.stringify(entry.content || "").slice(0, 1e4);
6462
+ return { sessionId, type: "post_tool_use", toolName: entry.tool_use_id || "unknown", toolOutput: output, timestamp: ts, cwd: entry.cwd || cwd };
6463
+ }
6464
+ return null;
6465
+ }
6466
+ if (agent === "cursor") {
6467
+ if (entry.role === "user" && entry.content) {
6468
+ const text = typeof entry.content === "string" ? entry.content : "";
6469
+ if (text.trim()) {
6470
+ return { sessionId, type: "user_prompt", prompt: text.trim().slice(0, 1e4), timestamp: ts, cwd };
6471
+ }
6472
+ }
6473
+ if (entry.role === "assistant" && entry.tool_calls) {
6474
+ for (const tc of entry.tool_calls) {
6475
+ if (tc.function?.name) {
6476
+ let toolInput;
6477
+ if (tc.function.arguments) {
6478
+ try {
6479
+ toolInput = JSON.parse(tc.function.arguments);
6480
+ } catch {
6481
+ toolInput = { raw: tc.function.arguments };
6482
+ }
6483
+ }
6484
+ return { sessionId, type: "pre_tool_use", toolName: tc.function.name, toolInput, timestamp: ts, cwd };
6485
+ }
6486
+ }
6487
+ }
6488
+ return null;
6489
+ }
6490
+ return null;
6491
+ }
6492
+ async function sendBatch2(config, sessionId, events, agentType) {
6493
+ const headers = { "Content-Type": "application/json" };
6494
+ if (config.authToken) headers["X-IMDL-Key"] = config.authToken;
6495
+ try {
6496
+ const res = await fetch(`${config.apiUrl}/api/sessions/${sessionId}/events`, {
6497
+ method: "POST",
6498
+ headers,
6499
+ body: JSON.stringify({
6500
+ events,
6501
+ developerId: config.developerId,
6502
+ agentType,
6503
+ historical: true
6504
+ }),
6505
+ signal: AbortSignal.timeout(SEND_TIMEOUT)
6506
+ });
6507
+ return res.ok;
6508
+ } catch {
6509
+ return false;
6510
+ }
6511
+ }
6512
+ function deriveSessionId(filePath, agent) {
6513
+ const filename = filePath.split("/").pop() || "";
6514
+ if (agent === "codex") {
6515
+ const match = filename.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
6516
+ return match ? `codex-${match[1].slice(0, 8)}` : `codex-${filename.replace(".jsonl", "").slice(-8)}`;
6517
+ }
6518
+ if (agent === "claude-code") {
6519
+ const parts = filePath.split("/");
6520
+ const sessionsIdx = parts.indexOf("sessions");
6521
+ if (sessionsIdx >= 0 && parts[sessionsIdx + 1]) {
6522
+ return parts[sessionsIdx + 1];
6523
+ }
6524
+ return `cc-${filename.replace(".jsonl", "").slice(-8)}`;
6525
+ }
6526
+ if (agent === "cursor") {
6527
+ const match = filename.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
6528
+ return match ? `cursor-${match[1].slice(0, 8)}` : `cursor-${filename.replace(".jsonl", "").slice(-8)}`;
6529
+ }
6530
+ return `unknown-${filename.slice(-8)}`;
6531
+ }
6532
+ function findJsonlFiles(basePath, since) {
6533
+ const files = [];
6534
+ const walk = (dir) => {
6535
+ try {
6536
+ for (const entry of readdirSync6(dir)) {
6537
+ const full = join24(dir, entry);
6538
+ try {
6539
+ const stat = statSync11(full);
6540
+ if (stat.isDirectory()) walk(full);
6541
+ else if (entry.endsWith(".jsonl") && stat.mtimeMs >= since.getTime()) {
6542
+ files.push(full);
6543
+ }
6544
+ } catch {
6545
+ }
6546
+ }
6547
+ } catch {
6548
+ }
6549
+ };
6550
+ walk(basePath);
6551
+ return files;
6552
+ }
6553
+ function safeReaddir(dir) {
6554
+ try {
6555
+ return readdirSync6(dir);
6556
+ } catch {
6557
+ return [];
6558
+ }
6559
+ }
6560
+ function safeStat(path) {
6561
+ try {
6562
+ return statSync11(path);
6563
+ } catch {
6564
+ return null;
6565
+ }
6566
+ }
6567
+ function formatBytes2(bytes) {
6568
+ if (bytes < 1024) return `${bytes}B`;
6569
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
6570
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
6571
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
6572
+ }
6573
+
6574
+ // src/commands/optimize.ts
6575
+ import pc14 from "picocolors";
6576
+ import { existsSync as existsSync25, readFileSync as readFileSync19 } from "fs";
6577
+ import { join as join25 } from "path";
6578
+ import { homedir as homedir19 } from "os";
6579
+ var FEATURES = [
6580
+ { id: "prompt_cache", name: "Prompt Semantic Cache", category: "optimization" },
6581
+ { id: "tool_call_cache", name: "Tool Call Cache", category: "optimization" },
6582
+ { id: "model_routing", name: "Smart Model Routing", category: "optimization" },
6583
+ { id: "idle_alerts", name: "Idle Session Alerts", category: "optimization" },
6584
+ { id: "session_resume", name: "Session Resume Hints", category: "optimization" },
6585
+ { id: "context_optimization", name: "Context Window Optimizer", category: "optimization" },
6586
+ { id: "budget_enforcement", name: "Budget Enforcement", category: "governance" },
6587
+ { id: "model_governance", name: "Model Governance", category: "governance" }
6588
+ ];
6589
+ async function optimizeCommand(opts) {
6590
+ const config = loadConfig();
6591
+ if (opts.enable) {
6592
+ await toggleFeature(config, opts.enable, true);
6593
+ return;
6594
+ }
6595
+ if (opts.disable) {
6596
+ await toggleFeature(config, opts.disable, false);
6597
+ return;
6598
+ }
6599
+ if (opts.check) {
6600
+ await checkCompliance2(config, opts.json);
6601
+ return;
6602
+ }
6603
+ if (opts.report) {
6604
+ await sendReport(config);
6605
+ return;
6606
+ }
6607
+ await showSummary(config, opts);
6608
+ }
6609
+ async function showSummary(config, opts) {
6610
+ console.log("");
6611
+ console.log(pc14.bold("Token Usage & Optimization"));
6612
+ console.log(pc14.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
6613
+ console.log("");
6614
+ const claudeUsage = readClaudeCodeUsage();
6615
+ const codexUsage = await readCodexUsage();
6616
+ if (claudeUsage) {
6617
+ const models = Object.entries(claudeUsage.modelUsage || {});
6618
+ console.log(pc14.bold(" Claude Code") + pc14.dim(` (${claudeUsage.configuredModel || "default"})`));
6619
+ let totalCost = 0;
6620
+ for (const [model, usage] of models) {
6621
+ const u = usage;
6622
+ const cost = u.costUSD || estimateCost(model, u.inputTokens || 0, u.outputTokens || 0);
6623
+ totalCost += cost;
6624
+ console.log(` ${pc14.dim("\u25CF")} ${shortenModel(model)}`);
6625
+ console.log(` Input: ${formatTokens(u.inputTokens || 0)} ${pc14.dim(`($${((u.inputTokens || 0) * inputRate(model) / 1e6).toFixed(2)})`)}`);
6626
+ console.log(` Output: ${formatTokens(u.outputTokens || 0)} ${pc14.dim(`($${((u.outputTokens || 0) * outputRate(model) / 1e6).toFixed(2)})`)}`);
6627
+ if (u.cacheReadInputTokens) {
6628
+ console.log(` Cache: ${formatTokens(u.cacheReadInputTokens)} reads`);
6629
+ }
6630
+ }
6631
+ console.log(` ${pc14.bold("Total")}: $${totalCost.toFixed(2)}`);
6632
+ if (claudeUsage.totalSessions) console.log(` Sessions: ${claudeUsage.totalSessions} | Messages: ${claudeUsage.totalMessages || 0}`);
6633
+ console.log("");
6634
+ }
6635
+ if (codexUsage) {
6636
+ console.log(pc14.bold(" Codex") + pc14.dim(` (${codexUsage.model || "default"})`));
6637
+ console.log(` Tokens: ${formatTokens(codexUsage.totalTokens)}`);
6638
+ console.log(` Sessions: ${codexUsage.sessionCount}`);
6639
+ if (codexUsage.estimatedCost > 0) {
6640
+ console.log(` Est. cost: $${codexUsage.estimatedCost.toFixed(2)}`);
6641
+ }
6642
+ console.log("");
6643
+ }
6644
+ if (!claudeUsage && !codexUsage) {
6645
+ console.log(pc14.dim(" No local usage data found."));
6646
+ console.log(pc14.dim(" Usage is tracked per-session and visible in the dashboard."));
6647
+ console.log("");
6648
+ }
6649
+ console.log(pc14.bold(" Optimization Features"));
6650
+ console.log(pc14.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
6651
+ let features = null;
6652
+ if (config.apiUrl && config.authToken) {
6653
+ try {
6654
+ const headers = { "Content-Type": "application/json" };
6655
+ if (config.authToken) headers["X-IMDL-Key"] = config.authToken;
6656
+ const res = await fetch(`${config.apiUrl}/api/optimize/my-features`, {
6657
+ headers,
6658
+ signal: AbortSignal.timeout(5e3)
6659
+ });
6660
+ if (res.ok) {
6661
+ const data = await res.json();
6662
+ features = data.features;
6663
+ }
6664
+ } catch {
6665
+ }
6666
+ }
6667
+ if (features) {
6668
+ for (const f of features) {
6669
+ const icon = f.enabled ? pc14.green("\u25CF") : pc14.dim("\u25CB");
6670
+ const locked = f.lockedByAdmin ? pc14.yellow(" (admin)") : "";
6671
+ console.log(` ${icon} ${f.name}${locked}`);
6672
+ }
6673
+ } else {
6674
+ for (const f of FEATURES) {
6675
+ console.log(` ${pc14.dim("\u25CB")} ${f.name} ${pc14.dim("(connect to API for status)")}`);
6676
+ }
6677
+ }
6678
+ console.log("");
6679
+ console.log(pc14.dim(" Toggle: imdl optimize --enable <feature> | --disable <feature>"));
6680
+ console.log(pc14.dim(" Features: prompt_cache, tool_call_cache, model_routing,"));
6681
+ console.log(pc14.dim(" idle_alerts, session_resume, context_optimization"));
6682
+ console.log("");
6683
+ if (opts.json) {
6684
+ const output = {
6685
+ claude_code: claudeUsage,
6686
+ codex: codexUsage,
6687
+ features: features || FEATURES.map((f) => ({ ...f, enabled: false }))
6688
+ };
6689
+ console.log(JSON.stringify(output, null, 2));
6690
+ }
6691
+ }
6692
+ async function toggleFeature(config, feature, enabled) {
6693
+ const valid = FEATURES.find((f) => f.id === feature);
6694
+ if (!valid) {
6695
+ console.log(pc14.red(`Unknown feature: ${feature}`));
6696
+ console.log(pc14.dim(`Available: ${FEATURES.map((f) => f.id).join(", ")}`));
6697
+ return;
6698
+ }
6699
+ if (!config.apiUrl || !config.authToken) {
6700
+ console.log(pc14.red("Not connected to API. Run `imdl init` first."));
6701
+ return;
6702
+ }
6703
+ const headers = { "Content-Type": "application/json" };
6704
+ if (config.authToken) headers["X-IMDL-Key"] = config.authToken;
6705
+ try {
6706
+ const res = await fetch(`${config.apiUrl}/api/optimize/my-features`, {
6707
+ method: "PUT",
6708
+ headers,
6709
+ body: JSON.stringify({ feature, enabled }),
6710
+ signal: AbortSignal.timeout(5e3)
6711
+ });
6712
+ if (res.ok) {
6713
+ const icon = enabled ? pc14.green("\u25CF") : pc14.dim("\u25CB");
6714
+ console.log(`${icon} ${valid.name}: ${enabled ? pc14.green("enabled") : pc14.dim("disabled")}`);
6715
+ } else {
6716
+ const data = await res.json().catch(() => ({}));
6717
+ if (data.error?.includes("locked")) {
6718
+ console.log(pc14.yellow(`Cannot toggle "${valid.name}" \u2014 locked by your admin.`));
6719
+ } else {
6720
+ console.log(pc14.red(`Failed: ${data.error || res.statusText}`));
6721
+ }
6722
+ }
6723
+ } catch {
6724
+ console.log(pc14.red("Failed to reach API."));
6725
+ }
6726
+ }
6727
+ async function checkCompliance2(config, json) {
6728
+ if (!config.apiUrl || !config.authToken) {
6729
+ console.log(pc14.red("Not connected to API. Run `imdl init` first."));
6730
+ return;
6731
+ }
6732
+ const headers = { "Content-Type": "application/json" };
6733
+ if (config.authToken) headers["X-IMDL-Key"] = config.authToken;
6734
+ try {
6735
+ const res = await fetch(`${config.apiUrl}/api/optimize/check`, {
6736
+ headers,
6737
+ signal: AbortSignal.timeout(5e3)
6738
+ });
6739
+ if (!res.ok) {
6740
+ console.log(pc14.dim("No cost policies configured for your team."));
6741
+ return;
6742
+ }
6743
+ const data = await res.json();
6744
+ if (json) {
6745
+ console.log(JSON.stringify(data, null, 2));
6746
+ return;
6747
+ }
6748
+ if (!data.violations || data.violations.length === 0) {
6749
+ console.log(pc14.green("\u2713 All cost policies satisfied."));
6750
+ return;
6751
+ }
6752
+ console.log(pc14.yellow(`\u26A0 ${data.violations.length} cost policy violation(s):`));
6753
+ for (const v of data.violations) {
6754
+ console.log(` ${pc14.yellow("\u25CF")} ${v.detail}`);
6755
+ if (v.costImpact) console.log(pc14.dim(` Excess cost: $${v.costImpact.toFixed(2)}`));
6756
+ }
6757
+ } catch {
6758
+ console.log(pc14.red("Failed to reach API."));
6759
+ }
6760
+ }
6761
+ async function sendReport(config) {
6762
+ if (!config.apiUrl || !config.authToken) {
6763
+ console.log(pc14.red("Not connected to API. Run `imdl init` first."));
6764
+ return;
6765
+ }
6766
+ const claudeUsage = readClaudeCodeUsage();
6767
+ const codexUsage = await readCodexUsage();
6768
+ const report = {
6769
+ developerId: config.developerId,
6770
+ reportedAt: (/* @__PURE__ */ new Date()).toISOString(),
6771
+ agents: []
6772
+ };
6773
+ if (claudeUsage) {
6774
+ report.agents.push({
6775
+ agentType: "claude-code",
6776
+ configuredModel: claudeUsage.configuredModel,
6777
+ aggregate: Object.entries(claudeUsage.modelUsage || {}).map(([model, u]) => ({
6778
+ model,
6779
+ provider: "anthropic",
6780
+ inputTokens: u.inputTokens || 0,
6781
+ outputTokens: u.outputTokens || 0,
6782
+ cacheReadTokens: u.cacheReadInputTokens || 0,
6783
+ cacheWriteTokens: u.cacheCreationInputTokens || 0,
6784
+ estimatedCostUSD: u.costUSD || estimateCost(model, u.inputTokens || 0, u.outputTokens || 0)
6785
+ }))
6786
+ });
6787
+ }
6788
+ if (codexUsage) {
6789
+ report.agents.push({
6790
+ agentType: "codex",
6791
+ configuredModel: codexUsage.model,
6792
+ aggregate: [{
6793
+ model: codexUsage.model || "gpt-5",
6794
+ provider: "openai",
6795
+ inputTokens: codexUsage.totalTokens,
6796
+ outputTokens: 0,
6797
+ estimatedCostUSD: codexUsage.estimatedCost
6798
+ }]
6799
+ });
6800
+ }
6801
+ const headers = { "Content-Type": "application/json" };
6802
+ if (config.authToken) headers["X-IMDL-Key"] = config.authToken;
6803
+ try {
6804
+ const res = await fetch(`${config.apiUrl}/api/optimize/report`, {
6805
+ method: "POST",
6806
+ headers,
6807
+ body: JSON.stringify(report),
6808
+ signal: AbortSignal.timeout(1e4)
6809
+ });
6810
+ if (res.ok) {
6811
+ console.log(pc14.green("\u2713 Usage report sent to dashboard."));
6812
+ } else {
6813
+ console.log(pc14.red("Failed to send report."));
6814
+ }
6815
+ } catch {
6816
+ console.log(pc14.red("Failed to reach API."));
6817
+ }
6818
+ }
6819
+ function readClaudeCodeUsage() {
6820
+ const baseDir = join25(homedir19(), ".claude");
6821
+ const statsPath = join25(baseDir, "stats-cache.json");
6822
+ const settingsPath = join25(baseDir, "settings.json");
6823
+ if (!existsSync25(statsPath)) return null;
6824
+ try {
6825
+ const stats = JSON.parse(readFileSync19(statsPath, "utf-8"));
6826
+ let configuredModel = "unknown";
6827
+ if (existsSync25(settingsPath)) {
6828
+ try {
6829
+ const settings = JSON.parse(readFileSync19(settingsPath, "utf-8"));
6830
+ configuredModel = settings.model || "default";
6831
+ } catch {
6832
+ }
6833
+ }
6834
+ return {
6835
+ configuredModel,
6836
+ modelUsage: stats.modelUsage || {},
6837
+ totalSessions: stats.totalSessions || 0,
6838
+ totalMessages: stats.totalMessages || 0
6839
+ };
6840
+ } catch {
6841
+ return null;
6842
+ }
6843
+ }
6844
+ async function readCodexUsage() {
6845
+ const baseDir = join25(homedir19(), ".codex");
6846
+ const configPath = join25(baseDir, "config.toml");
6847
+ const dbPath = join25(baseDir, "state_5.sqlite");
6848
+ if (!existsSync25(dbPath)) return null;
6849
+ let model = "unknown";
6850
+ if (existsSync25(configPath)) {
6851
+ try {
6852
+ const content = readFileSync19(configPath, "utf-8");
6853
+ const modelMatch = content.match(/^model\s*=\s*"([^"]+)"/m);
6854
+ if (modelMatch) model = modelMatch[1];
6855
+ } catch {
6856
+ }
6857
+ }
6858
+ try {
6859
+ const Database = (await import("better-sqlite3")).default;
6860
+ const db = new Database(dbPath, { readonly: true });
6861
+ const row = db.prepare("SELECT SUM(tokens_used) as total, COUNT(*) as cnt FROM threads WHERE tokens_used > 0").get();
6862
+ db.close();
6863
+ const totalTokens = row?.total || 0;
6864
+ const sessionCount = row?.cnt || 0;
6865
+ const estimatedCost = estimateCost(model, totalTokens * 0.6, totalTokens * 0.4);
6866
+ return { model, totalTokens, sessionCount, estimatedCost };
6867
+ } catch {
6868
+ return null;
6869
+ }
6870
+ }
6871
+ function estimateCost(model, inputTokens, outputTokens) {
6872
+ return inputTokens / 1e6 * inputRate(model) + outputTokens / 1e6 * outputRate(model);
6873
+ }
6874
+ function inputRate(model) {
6875
+ if (model.includes("opus")) return 15;
6876
+ if (model.includes("sonnet")) return 3;
6877
+ if (model.includes("haiku")) return 0.8;
6878
+ if (model.includes("gpt-5")) return 10;
6879
+ if (model.includes("gpt-4o") || model.includes("gpt-4.1")) return 2;
6880
+ return 3;
6881
+ }
6882
+ function outputRate(model) {
6883
+ if (model.includes("opus")) return 75;
6884
+ if (model.includes("sonnet")) return 15;
6885
+ if (model.includes("haiku")) return 4;
6886
+ if (model.includes("gpt-5")) return 30;
6887
+ if (model.includes("gpt-4o") || model.includes("gpt-4.1")) return 8;
6888
+ return 15;
6889
+ }
6890
+ function formatTokens(n) {
6891
+ if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
6892
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
6893
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
6894
+ return String(n);
6895
+ }
6896
+ function shortenModel(model) {
6897
+ if (model.includes("opus")) return "Claude Opus";
6898
+ if (model.includes("sonnet")) return "Claude Sonnet";
6899
+ if (model.includes("haiku")) return "Claude Haiku";
6900
+ if (model.includes("gpt-5")) return "GPT-5";
6901
+ if (model.includes("gpt-4")) return "GPT-4";
6902
+ if (model.length > 25) return model.slice(0, 22) + "...";
6903
+ return model;
6904
+ }
6905
+
6906
+ // src/commands/uninstall.ts
6907
+ import { Command } from "commander";
6908
+ import { existsSync as existsSync26, readFileSync as readFileSync20, rmSync, unlinkSync as unlinkSync3 } from "fs";
6909
+ import { homedir as homedir20 } from "os";
6910
+ import { join as join26 } from "path";
6911
+ import pc15 from "picocolors";
6912
+ var uninstall = new Command("uninstall").description("Remove all IMDL components, hooks, and configuration").option("--keep-config", "Keep ~/.imdl config directory").option("--force", "Skip confirmation prompt").action(async (opts) => {
6913
+ const home = homedir20();
6914
+ const imdlDir = join26(home, ".imdl");
6915
+ console.log("");
6916
+ console.log(pc15.bold(" Frostbridge IMDL \u2014 Uninstaller"));
6917
+ console.log("");
6918
+ const removed = [];
6919
+ const skipped = [];
6920
+ const claudeSettings = join26(home, ".claude", "settings.json");
6921
+ if (existsSync26(claudeSettings)) {
6922
+ try {
6923
+ const raw = readFileSync20(claudeSettings, "utf8");
6924
+ const settings = JSON.parse(raw);
6925
+ let modified = false;
6926
+ for (const hookType of ["PreToolUse", "PostToolUse", "UserPromptSubmit"]) {
6927
+ if (settings.hooks?.[hookType]) {
6928
+ const before = settings.hooks[hookType].length;
6929
+ settings.hooks[hookType] = settings.hooks[hookType].filter(
6930
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("imdl"))
6931
+ );
6932
+ if (settings.hooks[hookType].length < before) modified = true;
6933
+ if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
6934
+ }
6935
+ }
6936
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
6937
+ if (modified) {
6938
+ const { writeFileSync: writeFileSync13 } = await import("fs");
6939
+ writeFileSync13(claudeSettings, JSON.stringify(settings, null, 2));
6940
+ removed.push("Claude Code hooks");
6941
+ }
6942
+ } catch {
6943
+ skipped.push("Claude Code hooks (could not parse settings.json)");
6944
+ }
6945
+ }
6946
+ const cursorSettings = join26(home, ".cursor", "settings.json");
6947
+ if (existsSync26(cursorSettings)) {
6948
+ try {
6949
+ const raw = readFileSync20(cursorSettings, "utf8");
6950
+ const settings = JSON.parse(raw);
6951
+ let modified = false;
6952
+ if (settings.hooks) {
6953
+ for (const hookType of Object.keys(settings.hooks)) {
6954
+ const before = settings.hooks[hookType]?.length || 0;
6955
+ if (Array.isArray(settings.hooks[hookType])) {
6956
+ settings.hooks[hookType] = settings.hooks[hookType].filter(
6957
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("imdl"))
6958
+ );
6959
+ if (settings.hooks[hookType].length < before) modified = true;
6960
+ }
6961
+ }
6962
+ }
6963
+ if (modified) {
6964
+ const { writeFileSync: writeFileSync13 } = await import("fs");
6965
+ writeFileSync13(cursorSettings, JSON.stringify(settings, null, 2));
6966
+ removed.push("Cursor hooks");
6967
+ }
6968
+ } catch {
6969
+ skipped.push("Cursor hooks (could not parse settings.json)");
6970
+ }
6971
+ }
6972
+ for (const rc of [".bashrc", ".zshrc", ".profile"]) {
6973
+ const rcPath = join26(home, rc);
6974
+ if (existsSync26(rcPath)) {
6975
+ try {
6976
+ const content = readFileSync20(rcPath, "utf8");
6977
+ if (content.includes("imdl") || content.includes("frostbridge")) {
6978
+ const lines = content.split("\n");
6979
+ const filtered = lines.filter(
6980
+ (l) => !l.includes("imdl") && !l.includes("frostbridge") && !l.includes("# Added by Frostbridge")
6981
+ );
6982
+ if (filtered.length < lines.length) {
6983
+ const { writeFileSync: writeFileSync13 } = await import("fs");
6984
+ writeFileSync13(rcPath, filtered.join("\n"));
6985
+ removed.push(`Shell rc entries (${rc})`);
6986
+ }
6987
+ }
6988
+ } catch {
6989
+ }
6990
+ }
6991
+ }
6992
+ try {
6993
+ const pidFile = join26(imdlDir, "daemon.pid");
6994
+ if (existsSync26(pidFile)) {
6995
+ const pid = parseInt(readFileSync20(pidFile, "utf8").trim());
6996
+ if (pid > 0) {
6997
+ try {
6998
+ process.kill(pid, "SIGTERM");
6999
+ } catch {
7000
+ }
7001
+ unlinkSync3(pidFile);
7002
+ removed.push("Background daemon (killed)");
7003
+ }
7004
+ }
7005
+ } catch {
7006
+ }
7007
+ if (!opts.keepConfig && existsSync26(imdlDir)) {
7008
+ rmSync(imdlDir, { recursive: true, force: true });
7009
+ removed.push("~/.imdl config directory");
7010
+ } else if (opts.keepConfig) {
7011
+ skipped.push("~/.imdl (--keep-config)");
7012
+ }
7013
+ const bufferDir = join26(home, ".imdl", "buffer");
7014
+ if (existsSync26(bufferDir)) {
7015
+ rmSync(bufferDir, { recursive: true, force: true });
7016
+ removed.push("Event buffer");
7017
+ }
7018
+ console.log(pc15.dim(" Uninstalling npm packages..."));
7019
+ const { execSync: execSync3 } = await import("child_process");
7020
+ const packages = ["@frostbridge/imdl", "@frostbridge/imdl-shell-wrapper", "@frostbridge/imdl-mcp-proxy"];
7021
+ for (const pkg of packages) {
7022
+ try {
7023
+ execSync3(`npm uninstall -g ${pkg} 2>/dev/null`, { stdio: "pipe" });
7024
+ removed.push(`npm: ${pkg}`);
7025
+ } catch {
7026
+ }
7027
+ }
7028
+ console.log("");
7029
+ if (removed.length > 0) {
7030
+ console.log(pc15.green(pc15.bold(" Removed:")));
7031
+ for (const item of removed) {
7032
+ console.log(pc15.green(` \u2713 ${item}`));
7033
+ }
7034
+ }
7035
+ if (skipped.length > 0) {
7036
+ console.log(pc15.yellow(pc15.bold(" Skipped:")));
7037
+ for (const item of skipped) {
7038
+ console.log(pc15.yellow(` \u25CB ${item}`));
7039
+ }
7040
+ }
7041
+ console.log("");
7042
+ console.log(pc15.dim(" IMDL has been completely removed from this machine."));
7043
+ console.log("");
7044
+ });
7045
+
5352
7046
  // src/bifrost/compiler.ts
5353
7047
  function globToRegex(glob) {
5354
7048
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{\{GLOBSTAR\}\}/g, ".*");
@@ -5657,18 +7351,18 @@ function ruleTypeToCategory(type) {
5657
7351
  }
5658
7352
 
5659
7353
  // src/transport/sync.ts
5660
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync11, existsSync as existsSync23 } from "fs";
5661
- import { join as join22 } from "path";
7354
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync12, existsSync as existsSync27 } from "fs";
7355
+ import { join as join27 } from "path";
5662
7356
  var SYNC_INTERVAL = 6e4;
5663
7357
  var SYNC_TIMEOUT = 3e3;
5664
7358
  function getSyncStateFile() {
5665
- return join22(getImdlDir(), "last_sync.json");
7359
+ return join27(getImdlDir(), "last_sync.json");
5666
7360
  }
5667
7361
  function getLastSyncTime() {
5668
7362
  try {
5669
7363
  const file = getSyncStateFile();
5670
- if (existsSync23(file)) {
5671
- const data = JSON.parse(readFileSync18(file, "utf-8"));
7364
+ if (existsSync27(file)) {
7365
+ const data = JSON.parse(readFileSync21(file, "utf-8"));
5672
7366
  return data.lastSync || 0;
5673
7367
  }
5674
7368
  } catch {
@@ -5677,7 +7371,7 @@ function getLastSyncTime() {
5677
7371
  }
5678
7372
  function setLastSyncTime() {
5679
7373
  try {
5680
- writeFileSync11(getSyncStateFile(), JSON.stringify({ lastSync: Date.now() }), { mode: 384 });
7374
+ writeFileSync12(getSyncStateFile(), JSON.stringify({ lastSync: Date.now() }), { mode: 384 });
5681
7375
  } catch {
5682
7376
  }
5683
7377
  }
@@ -5989,7 +7683,19 @@ async function handleHook(type) {
5989
7683
  };
5990
7684
  }
5991
7685
  } else if (hookType === "user_prompt" && event.prompt) {
5992
- const scan = scanPromptForSecrets(event.prompt);
7686
+ const config = loadConfig();
7687
+ const llmConfig = {
7688
+ enabled: config.features?.prompt_llm_scan === true,
7689
+ apiUrl: config.llmScanUrl || (config.apiUrl ? `${config.apiUrl}/api/gateway/scan` : void 0),
7690
+ model: config.llmScanModel || "claude-haiku-4-5-20251001",
7691
+ maxInputChars: 500
7692
+ };
7693
+ let scan;
7694
+ if (llmConfig.enabled) {
7695
+ scan = await scanWithLlmFallback(event.prompt, llmConfig);
7696
+ } else {
7697
+ scan = scanPromptForSecrets(event.prompt);
7698
+ }
5993
7699
  if (scan.hasSecrets) {
5994
7700
  violationData = createPromptViolation(scan.findings);
5995
7701
  }
@@ -6016,13 +7722,14 @@ async function handleHook(type) {
6016
7722
  behaviorRiskScore = computeRiskScore(signals);
6017
7723
  }
6018
7724
  }
7725
+ const toolOutput = event.tool_output || event.tool_response;
6019
7726
  const sessionId = event.session_id || getLastSessionId();
6020
7727
  const buffered = {
6021
7728
  sessionId,
6022
7729
  type: hookType,
6023
7730
  toolName: event.tool_name,
6024
7731
  toolInput: redactObject(event.tool_input),
6025
- toolOutput: event.tool_output ? redactObject(event.tool_output) : void 0,
7732
+ toolOutput: toolOutput ? String(typeof toolOutput === "string" ? redactObject(toolOutput) : JSON.stringify(redactObject(toolOutput))) : void 0,
6026
7733
  prompt: event.prompt ? redactObject(event.prompt) : void 0,
6027
7734
  timestamp: event.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
6028
7735
  violation: violationData,
@@ -6079,8 +7786,8 @@ function getSafeAlternative(toolName, toolInput) {
6079
7786
  }
6080
7787
 
6081
7788
  // src/index.ts
6082
- var program = new Command();
6083
- program.name("imdl").description("IMDL \u2014 Intelligent Mediation & Detection Layer. AI agent security.").version("0.1.0");
7789
+ var program = new Command2();
7790
+ program.name("imdl").description("IMDL \u2014 Intelligent Mediation & Detection Layer. AI agent security.").version("0.1.13");
6084
7791
  program.command("scan").description("Scan MCP server configs for security risks").option("-p, --path <path>", "Path to MCP config file").option("-u, --url <url>", "GitHub URL or org/repo to scan").option("--json", "Output as JSON").option("--no-color", "Disable colored output").option("-q, --quiet", "Only show warnings and errors").option("-d, --deep", "Deep scan: static code analysis + GitHub issue scanning").action(scanCommand);
6085
7792
  program.command("init").description("Detect AI agents and install monitoring hooks").option("-t, --token <token>", "Invite token to join a team").option("--team-token <token>", "Alias for --token").option("--api <url>", "API URL to connect to").option("--non-interactive", "Skip all prompts and use defaults").action((opts) => initCommand({ teamToken: opts.token || opts.teamToken, apiUrl: opts.api, nonInteractive: opts.nonInteractive }));
6086
7793
  program.command("login").description("Authenticate to your IMDL team").action(loginCommand);
@@ -6106,5 +7813,12 @@ gwCmd.command("enable <module>").description("Enable a gateway module (secrets,
6106
7813
  gwCmd.command("disable <module>").description("Disable a gateway module").action((module) => gatewayDisableCommand({ module }));
6107
7814
  gwCmd.command("alerts").description("Show recent gateway security alerts").option("-n, --limit <count>", "Number of alerts to show", "20").option("--json", "Output as JSON").action(gatewayAlertsCommand);
6108
7815
  gwCmd.command("cost").description("Show AI spend and usage breakdown").option("--json", "Output as JSON").action(gatewayCostCommand);
7816
+ gwCmd.command("dashboard").description("Open the local gateway dashboard in your browser").action(gatewayDashboardCommand);
7817
+ gwCmd.command("activate").description("Route Claude Code through the gateway (adds env to settings.json)").option("--codex", "Also route Codex (OpenAI) WebSocket traffic through the gateway").option("--copilot", "Show setup instructions for routing GitHub Copilot through the gateway").action(gatewayActivateCommand);
7818
+ gwCmd.command("deactivate").description("Stop routing Claude Code through gateway (direct to Bedrock/Anthropic)").action(gatewayDeactivateCommand);
7819
+ program.command("sync-history").description("Sync historical session data and retroactively detect violations").option("-d, --days <days>", "Number of days to look back (default: 30)", "30").option("-a, --agent <agent>", "Only sync a specific agent (claude-code, codex, cursor)").option("--dry-run", "Scan and report without sending data to the API").action(syncHistoryCommand);
7820
+ program.command("optimize").description("Token usage, cost policies, and optimization features").option("--enable <feature>", "Enable an optimization feature").option("--disable <feature>", "Disable an optimization feature").option("--report", "Send local usage data to your team dashboard").option("--check", "Check compliance against org cost policies").option("--json", "Output as JSON").action(optimizeCommand);
7821
+ program.addCommand(uninstall);
7822
+ program.command("dashboard").description("Open the IMDL control panel in your browser").action(gatewayDashboardCommand);
6109
7823
  program.command("hook <type>").description("Handle hook events from AI agents (internal)").action(handleHook);
6110
7824
  program.parse();