@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.
- package/dist/index.js +1846 -132
- 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
|
-
|
|
651
|
-
|
|
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")}
|
|
1402
|
-
console.log(` ${pc2.cyan("imdl
|
|
1403
|
-
console.log(` ${pc2.cyan("imdl
|
|
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
|
-
|
|
2713
|
+
const settingsPath = join13(claudeDir, "settings.json");
|
|
2714
|
+
if (existsSync14(settingsPath)) {
|
|
2590
2715
|
try {
|
|
2591
|
-
const
|
|
2592
|
-
hooked = JSON.stringify(
|
|
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
|
|
2709
|
-
import { join as
|
|
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 =
|
|
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
|
-
|
|
2936
|
+
const failedEvents = [];
|
|
2937
|
+
debugLog(`Flushing ${events.length} events across ${grouped.size} sessions`);
|
|
2797
2938
|
for (const [sessionId, sessionEvents] of grouped) {
|
|
2798
|
-
|
|
2799
|
-
|
|
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
|
-
|
|
2966
|
+
failedEvents.push(...sessionEvents.slice(i));
|
|
2967
|
+
sessionFailed = true;
|
|
2803
2968
|
break;
|
|
2804
2969
|
}
|
|
2805
2970
|
}
|
|
2806
|
-
if (
|
|
2971
|
+
if (sessionFailed) continue;
|
|
2807
2972
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
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: /
|
|
2970
|
-
{ pattern: /
|
|
2971
|
-
{ pattern: /
|
|
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: /
|
|
2975
|
-
{ pattern: /
|
|
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
|
|
3108
|
-
import { homedir as
|
|
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 =
|
|
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 =
|
|
3525
|
+
const logDir = join16(homedir13(), ".claude", "projects", encoded);
|
|
3207
3526
|
return existsSync16(logDir) ? logDir : null;
|
|
3208
3527
|
}
|
|
3209
3528
|
static getAllProjectDirs() {
|
|
3210
|
-
const base =
|
|
3529
|
+
const base = join16(homedir13(), ".claude", "projects");
|
|
3211
3530
|
if (!existsSync16(base)) return [];
|
|
3212
|
-
return readdirSync3(base).map((d) =>
|
|
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
|
|
3219
|
-
import { homedir as
|
|
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 =
|
|
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 =
|
|
3599
|
+
const entryPath = join17(baseDir, entry);
|
|
3281
3600
|
try {
|
|
3282
3601
|
const stat = statSync9(entryPath);
|
|
3283
3602
|
if (stat.isDirectory()) {
|
|
3284
|
-
const dbPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
3696
|
-
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
4367
|
+
var STATE_FILE = join19(getImdlDir(), "adapter-state.json");
|
|
3732
4368
|
function loadState() {
|
|
3733
4369
|
try {
|
|
3734
|
-
if (
|
|
3735
|
-
return JSON.parse(
|
|
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
|
-
|
|
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
|
|
4409
|
+
const usageBySessionDay = /* @__PURE__ */ new Map();
|
|
3771
4410
|
for (const event of events) {
|
|
3772
4411
|
if (event.usage) {
|
|
3773
|
-
const
|
|
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
|
-
|
|
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
|
|
3803
|
-
|
|
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
|
-
|
|
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
|
|
3906
|
-
import { join as
|
|
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 =
|
|
3930
|
-
if (!
|
|
3931
|
-
const content =
|
|
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
|
-
|
|
4681
|
+
writeFileSync8(agentLog, "", { mode: 384 });
|
|
3980
4682
|
}
|
|
3981
4683
|
return sent;
|
|
3982
4684
|
}
|
|
3983
4685
|
async function ingestShellEvents() {
|
|
3984
|
-
const shellLog =
|
|
3985
|
-
if (!
|
|
3986
|
-
const content =
|
|
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
|
-
|
|
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
|
|
4092
|
-
import { join as
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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
|
|
4599
|
-
import { join as
|
|
4600
|
-
import { homedir as
|
|
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 =
|
|
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 =
|
|
4614
|
-
if (!
|
|
5315
|
+
const configPath = join22(home, ".claude", "settings.json");
|
|
5316
|
+
if (!existsSync22(configPath)) return null;
|
|
4615
5317
|
try {
|
|
4616
|
-
const settings = JSON.parse(
|
|
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 =
|
|
4645
|
-
if (!
|
|
5346
|
+
const configPath = join22(home, ".cursor", "settings.json");
|
|
5347
|
+
if (!existsSync22(configPath)) return null;
|
|
4646
5348
|
try {
|
|
4647
|
-
const settings = JSON.parse(
|
|
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 =
|
|
4657
|
-
if (!
|
|
5358
|
+
const configPath = join22(home, ".codex", "config.json");
|
|
5359
|
+
if (!existsSync22(configPath)) return null;
|
|
4658
5360
|
try {
|
|
4659
|
-
const config = JSON.parse(
|
|
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 =
|
|
4669
|
-
if (!
|
|
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(
|
|
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 =
|
|
4682
|
-
if (!
|
|
5383
|
+
const configPath = join22(home, ".windsurf", "mcp.json");
|
|
5384
|
+
if (!existsSync22(configPath)) return null;
|
|
4683
5385
|
try {
|
|
4684
|
-
const config = JSON.parse(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
4989
|
-
import { join as
|
|
4990
|
-
import { homedir as
|
|
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 =
|
|
4993
|
-
var GATEWAY_CONFIG =
|
|
4994
|
-
var GATEWAY_PID_FILE =
|
|
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 (!
|
|
5070
|
-
|
|
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:
|
|
5098
|
-
|
|
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 =
|
|
5130
|
-
if (!
|
|
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 =
|
|
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 (!
|
|
5955
|
+
if (!existsSync23(GATEWAY_PID_FILE)) return null;
|
|
5237
5956
|
try {
|
|
5238
|
-
const pid = parseInt(
|
|
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 (!
|
|
5972
|
+
if (!existsSync23(GATEWAY_CONFIG)) return DEFAULT_PORT;
|
|
5254
5973
|
try {
|
|
5255
|
-
const content =
|
|
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
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
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 (
|
|
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 (
|
|
5280
|
-
if (!
|
|
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
|
-
|
|
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 (!
|
|
6043
|
+
if (!existsSync23(GATEWAY_CONFIG)) {
|
|
5325
6044
|
ensureConfig(DEFAULT_PORT);
|
|
5326
6045
|
}
|
|
5327
|
-
return
|
|
6046
|
+
return readFileSync18(GATEWAY_CONFIG, "utf-8");
|
|
5328
6047
|
}
|
|
5329
6048
|
function saveGatewayToml(content) {
|
|
5330
|
-
|
|
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
|
|
5661
|
-
import { join as
|
|
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
|
|
7359
|
+
return join27(getImdlDir(), "last_sync.json");
|
|
5666
7360
|
}
|
|
5667
7361
|
function getLastSyncTime() {
|
|
5668
7362
|
try {
|
|
5669
7363
|
const file = getSyncStateFile();
|
|
5670
|
-
if (
|
|
5671
|
-
const data = JSON.parse(
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
6083
|
-
program.name("imdl").description("IMDL \u2014 Intelligent Mediation & Detection Layer. AI agent security.").version("0.1.
|
|
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();
|