@frostbridge/imdl 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +439 -76
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/index.ts
10
- import { Command } from "commander";
10
+ import { Command as Command2 } from "commander";
11
11
 
12
12
  // src/commands/scan.ts
13
13
  import pc from "picocolors";
@@ -633,7 +633,7 @@ import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync
633
633
  import { join as join4 } from "path";
634
634
 
635
635
  // src/config/store.ts
636
- import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync4, unlinkSync } from "fs";
636
+ import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync4, unlinkSync, chmodSync } from "fs";
637
637
  import { join as join3 } from "path";
638
638
  import { homedir as homedir2 } from "os";
639
639
  import { randomUUID } from "crypto";
@@ -647,8 +647,10 @@ function getBufferDir() {
647
647
  return BUFFER_DIR;
648
648
  }
649
649
  function ensureImdlDir() {
650
- if (!existsSync4(IMDL_DIR)) mkdirSync(IMDL_DIR, { recursive: true, mode: 448 });
651
- if (!existsSync4(BUFFER_DIR)) mkdirSync(BUFFER_DIR, { recursive: true, mode: 448 });
650
+ mkdirSync(IMDL_DIR, { recursive: true, mode: 448 });
651
+ chmodSync(IMDL_DIR, 448);
652
+ mkdirSync(BUFFER_DIR, { recursive: true, mode: 448 });
653
+ chmodSync(BUFFER_DIR, 448);
652
654
  }
653
655
  function isLocalUrl(url) {
654
656
  try {
@@ -1231,7 +1233,8 @@ var AGENTS = [
1231
1233
  hooksFile: join6(homedir4(), ".claude", "settings.json"),
1232
1234
  hooks: {
1233
1235
  PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "imdl hook pre-tool", timeout: 5 }] }],
1234
- PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "imdl hook post-tool", timeout: 5 }] }]
1236
+ PostToolUse: [{ matcher: "*", hooks: [{ type: "command", command: "imdl hook post-tool", timeout: 5 }] }],
1237
+ UserPromptSubmit: [{ matcher: "*", hooks: [{ type: "command", command: "imdl hook prompt", timeout: 5 }] }]
1235
1238
  }
1236
1239
  },
1237
1240
  {
@@ -2705,8 +2708,8 @@ function resumeCommand() {
2705
2708
  import pc6 from "picocolors";
2706
2709
 
2707
2710
  // src/adapters/index.ts
2708
- import { existsSync as existsSync18, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
2709
- import { join as join17 } from "path";
2711
+ import { existsSync as existsSync19, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
2712
+ import { join as join18 } from "path";
2710
2713
 
2711
2714
  // src/transport/buffer.ts
2712
2715
  import { appendFileSync, readFileSync as readFileSync11, writeFileSync as writeFileSync5, existsSync as existsSync15, statSync as statSync7, unlinkSync as unlinkSync2 } from "fs";
@@ -3149,6 +3152,21 @@ var ClaudeCodeAdapter = class {
3149
3152
  cwd: entry.cwd,
3150
3153
  agentType: "claude-code"
3151
3154
  });
3155
+ } else if (Array.isArray(content)) {
3156
+ const textBlock = content.find(
3157
+ (block) => block && block.type === "text" && typeof block.text === "string" && block.text.length > 0
3158
+ );
3159
+ if (textBlock) {
3160
+ if (this.isNoisePrompt(textBlock.text)) continue;
3161
+ events.push({
3162
+ sessionId: entry.sessionId || this.sessionIdFromPath(path),
3163
+ type: "user_prompt",
3164
+ prompt: textBlock.text.slice(0, 1e4),
3165
+ timestamp: entry.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
3166
+ cwd: entry.cwd,
3167
+ agentType: "claude-code"
3168
+ });
3169
+ }
3152
3170
  }
3153
3171
  }
3154
3172
  if (entry.type === "assistant" && entry.message?.usage) {
@@ -3677,6 +3695,23 @@ var CursorAdapter = class _CursorAdapter {
3677
3695
  });
3678
3696
  }
3679
3697
  }
3698
+ const usage = entry.message?.usage || entry.usage;
3699
+ if (usage && (usage.input_tokens || usage.output_tokens)) {
3700
+ events.push({
3701
+ sessionId: sid,
3702
+ type: "notification",
3703
+ timestamp: now,
3704
+ cwd: projectPath,
3705
+ agentType: "cursor",
3706
+ usage: {
3707
+ model: entry.message?.model || entry.model || "unknown",
3708
+ inputTokens: usage.input_tokens || 0,
3709
+ outputTokens: usage.output_tokens || 0,
3710
+ cacheReadTokens: usage.cache_read_input_tokens || 0,
3711
+ cacheWriteTokens: usage.cache_creation_input_tokens || 0
3712
+ }
3713
+ });
3714
+ }
3680
3715
  }
3681
3716
  }
3682
3717
  } catch {
@@ -3727,12 +3762,141 @@ var CursorAdapter = class _CursorAdapter {
3727
3762
  }
3728
3763
  };
3729
3764
 
3765
+ // src/adapters/codex.ts
3766
+ import { existsSync as existsSync18, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync10 } from "fs";
3767
+ import { join as join17 } from "path";
3768
+ import { homedir as homedir14 } from "os";
3769
+ var CodexAdapter = class {
3770
+ agentType = "codex";
3771
+ async extractEvents(source) {
3772
+ if (source.kind === "directory") {
3773
+ return this.readSessionsDir(source.path, source.since);
3774
+ }
3775
+ return [];
3776
+ }
3777
+ readSessionsDir(basePath, since) {
3778
+ if (!existsSync18(basePath)) return [];
3779
+ const events = [];
3780
+ const files = this.findSessionFiles(basePath, since);
3781
+ for (const filePath of files) {
3782
+ const fileEvents = this.readSessionFile(filePath, since);
3783
+ events.push(...fileEvents);
3784
+ }
3785
+ return events;
3786
+ }
3787
+ findSessionFiles(basePath, since) {
3788
+ const files = [];
3789
+ const walk = (dir) => {
3790
+ try {
3791
+ for (const entry of readdirSync5(dir)) {
3792
+ const full = join17(dir, entry);
3793
+ try {
3794
+ const stat = statSync10(full);
3795
+ if (stat.isDirectory()) {
3796
+ walk(full);
3797
+ } else if (entry.endsWith(".jsonl")) {
3798
+ if (!since || stat.mtimeMs >= since.getTime()) {
3799
+ files.push(full);
3800
+ }
3801
+ }
3802
+ } catch {
3803
+ }
3804
+ }
3805
+ } catch {
3806
+ }
3807
+ };
3808
+ walk(basePath);
3809
+ return files;
3810
+ }
3811
+ readSessionFile(filePath, since) {
3812
+ const events = [];
3813
+ let content;
3814
+ try {
3815
+ content = readFileSync13(filePath, "utf-8");
3816
+ } catch {
3817
+ return [];
3818
+ }
3819
+ const lines = content.split("\n").filter((l) => l.trim());
3820
+ const sessionId = this.sessionIdFromPath(filePath);
3821
+ let cwd;
3822
+ for (const line of lines) {
3823
+ let entry;
3824
+ try {
3825
+ entry = JSON.parse(line);
3826
+ } catch {
3827
+ continue;
3828
+ }
3829
+ if (since && entry.timestamp) {
3830
+ const entryTime = new Date(entry.timestamp).getTime();
3831
+ if (entryTime <= since.getTime()) continue;
3832
+ }
3833
+ const ptype = entry.payload?.type;
3834
+ if (ptype === "turn_context" && entry.payload.cwd) {
3835
+ cwd = entry.payload.cwd;
3836
+ }
3837
+ if (ptype === "user_message" && entry.payload.message) {
3838
+ const msg = entry.payload.message.trim();
3839
+ if (msg.length > 0) {
3840
+ events.push({
3841
+ sessionId,
3842
+ type: "user_prompt",
3843
+ prompt: msg.slice(0, 1e4),
3844
+ timestamp: entry.timestamp,
3845
+ cwd,
3846
+ agentType: "codex"
3847
+ });
3848
+ }
3849
+ }
3850
+ if (ptype === "function_call" && entry.payload.name) {
3851
+ let toolInput;
3852
+ if (entry.payload.arguments) {
3853
+ try {
3854
+ toolInput = JSON.parse(entry.payload.arguments);
3855
+ } catch {
3856
+ toolInput = { raw: entry.payload.arguments };
3857
+ }
3858
+ }
3859
+ events.push({
3860
+ sessionId,
3861
+ type: "pre_tool_use",
3862
+ toolName: entry.payload.name,
3863
+ toolInput,
3864
+ timestamp: entry.timestamp,
3865
+ cwd,
3866
+ agentType: "codex"
3867
+ });
3868
+ }
3869
+ if (ptype === "function_call_output" && entry.payload.output) {
3870
+ events.push({
3871
+ sessionId,
3872
+ type: "post_tool_use",
3873
+ toolName: entry.payload.call_id || "unknown",
3874
+ toolOutput: entry.payload.output.slice(0, 1e4),
3875
+ timestamp: entry.timestamp,
3876
+ cwd,
3877
+ agentType: "codex"
3878
+ });
3879
+ }
3880
+ }
3881
+ return events;
3882
+ }
3883
+ sessionIdFromPath(filePath) {
3884
+ const filename = filePath.split("/").pop() || "";
3885
+ const match = filename.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
3886
+ return match ? `codex-${match[1].slice(0, 8)}` : `codex-${filename.replace(".jsonl", "").slice(-8)}`;
3887
+ }
3888
+ static getSessionsDir() {
3889
+ const dir = join17(homedir14(), ".codex", "sessions");
3890
+ return existsSync18(dir) ? dir : null;
3891
+ }
3892
+ };
3893
+
3730
3894
  // src/adapters/index.ts
3731
- var STATE_FILE = join17(getImdlDir(), "adapter-state.json");
3895
+ var STATE_FILE = join18(getImdlDir(), "adapter-state.json");
3732
3896
  function loadState() {
3733
3897
  try {
3734
- if (existsSync18(STATE_FILE)) {
3735
- return JSON.parse(readFileSync13(STATE_FILE, "utf-8"));
3898
+ if (existsSync19(STATE_FILE)) {
3899
+ return JSON.parse(readFileSync14(STATE_FILE, "utf-8"));
3736
3900
  }
3737
3901
  } catch {
3738
3902
  }
@@ -3744,12 +3908,15 @@ function saveState(state) {
3744
3908
  }
3745
3909
  var adapters = [
3746
3910
  new ClaudeCodeAdapter(),
3747
- new CursorAdapter()
3911
+ new CursorAdapter(),
3912
+ new CodexAdapter()
3748
3913
  ];
3749
3914
  async function collectPrompts() {
3750
3915
  const state = loadState();
3751
3916
  const config = loadConfig();
3752
3917
  let collected = 0;
3918
+ const usageHeaders = { "Content-Type": "application/json" };
3919
+ if (config.authToken) usageHeaders["X-IMDL-Key"] = config.authToken;
3753
3920
  const claudeDirs = ClaudeCodeAdapter.getAllProjectDirs();
3754
3921
  const claudeAdapter = new ClaudeCodeAdapter();
3755
3922
  for (const dir of claudeDirs) {
@@ -3799,8 +3966,6 @@ async function collectPrompts() {
3799
3966
  appendEvent(buffered);
3800
3967
  collected++;
3801
3968
  }
3802
- const usageHeaders = { "Content-Type": "application/json" };
3803
- if (config.authToken) usageHeaders["X-IMDL-Key"] = config.authToken;
3804
3969
  for (const [sessionId, usage] of usageBySession) {
3805
3970
  try {
3806
3971
  await fetch(`${config.apiUrl}/api/sessions/${sessionId}/usage`, {
@@ -3814,11 +3979,22 @@ async function collectPrompts() {
3814
3979
  }
3815
3980
  }
3816
3981
  const cursorAdapter = new CursorAdapter();
3982
+ const cursorUsageBySession = /* @__PURE__ */ new Map();
3817
3983
  if (CursorAdapter.getProjectsDir()) {
3818
3984
  try {
3819
3985
  const { events: transcriptEvents, newOffsets } = cursorAdapter.extractFromTranscripts(new Date(state.lastRun), state.offsets);
3820
3986
  state.offsets = newOffsets;
3821
3987
  for (const event of transcriptEvents) {
3988
+ if (event.usage) {
3989
+ const existing = cursorUsageBySession.get(event.sessionId) || { model: event.usage.model, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
3990
+ existing.model = event.usage.model;
3991
+ existing.input += event.usage.inputTokens;
3992
+ existing.output += event.usage.outputTokens;
3993
+ existing.cacheRead += event.usage.cacheReadTokens;
3994
+ existing.cacheWrite += event.usage.cacheWriteTokens;
3995
+ cursorUsageBySession.set(event.sessionId, existing);
3996
+ continue;
3997
+ }
3822
3998
  let violation;
3823
3999
  if (event.prompt) {
3824
4000
  const scan = scanPromptForSecrets(event.prompt);
@@ -3878,6 +4054,52 @@ async function collectPrompts() {
3878
4054
  }
3879
4055
  }
3880
4056
  }
4057
+ for (const [sessionId, usage] of cursorUsageBySession) {
4058
+ try {
4059
+ await fetch(`${config.apiUrl}/api/sessions/${sessionId}/usage`, {
4060
+ method: "POST",
4061
+ headers: usageHeaders,
4062
+ body: JSON.stringify(usage),
4063
+ signal: AbortSignal.timeout(3e3)
4064
+ });
4065
+ } catch {
4066
+ }
4067
+ }
4068
+ const codexSessionsDir = CodexAdapter.getSessionsDir();
4069
+ if (codexSessionsDir) {
4070
+ try {
4071
+ const codexAdapter = new CodexAdapter();
4072
+ const codexEvents = await codexAdapter.extractEvents({
4073
+ kind: "directory",
4074
+ path: codexSessionsDir,
4075
+ since: new Date(state.lastRun)
4076
+ });
4077
+ for (const event of codexEvents) {
4078
+ let violation;
4079
+ if (event.prompt) {
4080
+ const scan = scanPromptForSecrets(event.prompt);
4081
+ if (scan.hasSecrets) {
4082
+ violation = createPromptViolation(scan.findings);
4083
+ }
4084
+ }
4085
+ const buffered = {
4086
+ sessionId: event.sessionId,
4087
+ type: event.type,
4088
+ toolName: event.toolName,
4089
+ toolInput: event.toolInput ? redactObject(event.toolInput) : void 0,
4090
+ toolOutput: event.toolOutput ? redactObject(event.toolOutput) : void 0,
4091
+ prompt: event.prompt ? redactObject(event.prompt) : void 0,
4092
+ timestamp: event.timestamp,
4093
+ cwd: event.cwd,
4094
+ agentType: "codex",
4095
+ violation
4096
+ };
4097
+ appendEvent(buffered);
4098
+ collected++;
4099
+ }
4100
+ } catch {
4101
+ }
4102
+ }
3881
4103
  if (collected > 0) {
3882
4104
  try {
3883
4105
  await tryFlush();
@@ -3902,8 +4124,8 @@ async function collectCommand() {
3902
4124
 
3903
4125
  // src/commands/daemon.ts
3904
4126
  import pc7 from "picocolors";
3905
- import { existsSync as existsSync19, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
3906
- import { join as join18 } from "path";
4127
+ import { existsSync as existsSync20, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
4128
+ import { join as join19 } from "path";
3907
4129
  var INTERVAL_MS = 3e4;
3908
4130
  async function daemonCommand() {
3909
4131
  console.log(pc7.cyan("imdl daemon started \u2014 collecting every 30s"));
@@ -3926,9 +4148,9 @@ async function daemonCommand() {
3926
4148
  setInterval(tick, INTERVAL_MS);
3927
4149
  }
3928
4150
  async function ingestAgentEvents() {
3929
- const agentLog = join18(getBufferDir(), "agent-events.ndjson");
3930
- if (!existsSync19(agentLog)) return 0;
3931
- const content = readFileSync14(agentLog, "utf-8").trim();
4151
+ const agentLog = join19(getBufferDir(), "agent-events.ndjson");
4152
+ if (!existsSync20(agentLog)) return 0;
4153
+ const content = readFileSync15(agentLog, "utf-8").trim();
3932
4154
  if (!content) return 0;
3933
4155
  const config = loadConfig();
3934
4156
  const lines = content.split("\n");
@@ -3981,9 +4203,9 @@ async function ingestAgentEvents() {
3981
4203
  return sent;
3982
4204
  }
3983
4205
  async function ingestShellEvents() {
3984
- const shellLog = join18(getBufferDir(), "shell-events.ndjson");
3985
- if (!existsSync19(shellLog)) return 0;
3986
- const content = readFileSync14(shellLog, "utf-8").trim();
4206
+ const shellLog = join19(getBufferDir(), "shell-events.ndjson");
4207
+ if (!existsSync20(shellLog)) return 0;
4208
+ const content = readFileSync15(shellLog, "utf-8").trim();
3987
4209
  if (!content) return 0;
3988
4210
  const config = loadConfig();
3989
4211
  const lines = content.split("\n");
@@ -4088,8 +4310,8 @@ async function requestCommand(options) {
4088
4310
  // src/commands/lock.ts
4089
4311
  import pc9 from "picocolors";
4090
4312
  import { createHash as createHash2 } from "crypto";
4091
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, existsSync as existsSync20 } from "fs";
4092
- import { join as join19, resolve as resolve3 } from "path";
4313
+ import { readFileSync as readFileSync16, writeFileSync as writeFileSync8, existsSync as existsSync21 } from "fs";
4314
+ import { join as join20, resolve as resolve3 } from "path";
4093
4315
  var LOCKFILE_NAME = "mcp-lock.json";
4094
4316
  function computeIntegrity(server, identity) {
4095
4317
  const payload = JSON.stringify({
@@ -4104,7 +4326,7 @@ function computeIntegrity(server, identity) {
4104
4326
  }
4105
4327
  function getLockfilePath(customPath) {
4106
4328
  if (customPath) return resolve3(customPath);
4107
- return join19(process.cwd(), LOCKFILE_NAME);
4329
+ return join20(process.cwd(), LOCKFILE_NAME);
4108
4330
  }
4109
4331
  async function lockCommand(options) {
4110
4332
  const detection = await detectMCPConfigs(options.path);
@@ -4174,7 +4396,7 @@ async function lockCommand(options) {
4174
4396
  }
4175
4397
  async function verifyCommand(options) {
4176
4398
  const lockPath = getLockfilePath(options.path);
4177
- if (!existsSync20(lockPath)) {
4399
+ if (!existsSync21(lockPath)) {
4178
4400
  if (options.json) {
4179
4401
  console.log(JSON.stringify({ error: "No lockfile found. Run `imdl lock` first.", path: lockPath }));
4180
4402
  } else {
@@ -4186,7 +4408,7 @@ async function verifyCommand(options) {
4186
4408
  }
4187
4409
  let lockfile;
4188
4410
  try {
4189
- lockfile = JSON.parse(readFileSync15(lockPath, "utf-8"));
4411
+ lockfile = JSON.parse(readFileSync16(lockPath, "utf-8"));
4190
4412
  } catch {
4191
4413
  console.log(pc9.red("Failed to parse lockfile."));
4192
4414
  process.exit(1);
@@ -4595,12 +4817,12 @@ async function checkCompliance(developerId) {
4595
4817
  }
4596
4818
 
4597
4819
  // src/permissions/fixer.ts
4598
- import { existsSync as existsSync21, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
4599
- import { join as join20 } from "path";
4600
- import { homedir as homedir14 } from "os";
4820
+ import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
4821
+ import { join as join21 } from "path";
4822
+ import { homedir as homedir15 } from "os";
4601
4823
  function buildFixesFromChanges(changes) {
4602
4824
  const fixes = [];
4603
- const home = homedir14();
4825
+ const home = homedir15();
4604
4826
  for (const change of changes) {
4605
4827
  const fix = buildFixForAgent(change, home);
4606
4828
  if (fix) fixes.push(fix);
@@ -4610,10 +4832,10 @@ function buildFixesFromChanges(changes) {
4610
4832
  function buildFixForAgent(change, home) {
4611
4833
  const { agentType, category, action, targetGrant } = change;
4612
4834
  if (agentType === "claude-code") {
4613
- const configPath = join20(home, ".claude", "settings.json");
4614
- if (!existsSync21(configPath)) return null;
4835
+ const configPath = join21(home, ".claude", "settings.json");
4836
+ if (!existsSync22(configPath)) return null;
4615
4837
  try {
4616
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
4838
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4617
4839
  const tools = settings.allowedTools || [];
4618
4840
  if (category === "shell") {
4619
4841
  if (!tools.includes("Bash") && !tools.some((t) => t.startsWith("Bash("))) return null;
@@ -4641,10 +4863,10 @@ function buildFixForAgent(change, home) {
4641
4863
  }
4642
4864
  }
4643
4865
  if (agentType === "cursor") {
4644
- const configPath = join20(home, ".cursor", "settings.json");
4645
- if (!existsSync21(configPath)) return null;
4866
+ const configPath = join21(home, ".cursor", "settings.json");
4867
+ if (!existsSync22(configPath)) return null;
4646
4868
  try {
4647
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
4869
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4648
4870
  if ((category === "agentic_mode" || category === "shell") && settings["cursor.composer.yoloMode"] === true) {
4649
4871
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Disable YOLO mode" };
4650
4872
  }
@@ -4653,10 +4875,10 @@ function buildFixForAgent(change, home) {
4653
4875
  }
4654
4876
  }
4655
4877
  if (agentType === "codex") {
4656
- const configPath = join20(home, ".codex", "config.json");
4657
- if (!existsSync21(configPath)) return null;
4878
+ const configPath = join21(home, ".codex", "config.json");
4879
+ if (!existsSync22(configPath)) return null;
4658
4880
  try {
4659
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
4881
+ const config = JSON.parse(readFileSync17(configPath, "utf-8"));
4660
4882
  if ((category === "agentic_mode" || category === "shell") && config.sandbox_mode === "full_auto") {
4661
4883
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Switch to supervised mode" };
4662
4884
  }
@@ -4665,10 +4887,10 @@ function buildFixForAgent(change, home) {
4665
4887
  }
4666
4888
  }
4667
4889
  if (agentType === "copilot") {
4668
- const configPath = join20(home, "Library", "Application Support", "Code", "User", "settings.json");
4669
- if (!existsSync21(configPath)) return null;
4890
+ const configPath = join21(home, "Library", "Application Support", "Code", "User", "settings.json");
4891
+ if (!existsSync22(configPath)) return null;
4670
4892
  try {
4671
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
4893
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4672
4894
  if ((category === "agentic_mode" || category === "shell") && (settings["github.copilot.chat.agent.autoApprove"] || settings["chat.agent.autoApprove"])) {
4673
4895
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Disable autoApprove" };
4674
4896
  }
@@ -4678,10 +4900,10 @@ function buildFixForAgent(change, home) {
4678
4900
  }
4679
4901
  if (agentType === "windsurf") {
4680
4902
  if (category === "mcp_tools") {
4681
- const configPath = join20(home, ".windsurf", "mcp.json");
4682
- if (!existsSync21(configPath)) return null;
4903
+ const configPath = join21(home, ".windsurf", "mcp.json");
4904
+ if (!existsSync22(configPath)) return null;
4683
4905
  try {
4684
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
4906
+ const config = JSON.parse(readFileSync17(configPath, "utf-8"));
4685
4907
  const servers = Object.keys(config.mcpServers || {});
4686
4908
  if (servers.length === 0) return null;
4687
4909
  return { agentType, category, currentGrant: "unrestricted", newGrant: "denied", configPath, description: `Remove MCP servers (${servers.join(", ")})` };
@@ -4694,7 +4916,7 @@ function buildFixForAgent(change, home) {
4694
4916
  }
4695
4917
  function applyFix(fix) {
4696
4918
  try {
4697
- const content = readFileSync16(fix.configPath, "utf-8");
4919
+ const content = readFileSync17(fix.configPath, "utf-8");
4698
4920
  const config = JSON.parse(content);
4699
4921
  let modified = false;
4700
4922
  if (fix.agentType === "claude-code") {
@@ -4985,13 +5207,13 @@ function grantLevel(grant) {
4985
5207
 
4986
5208
  // src/commands/gateway.ts
4987
5209
  import pc12 from "picocolors";
4988
- import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync3 } from "fs";
4989
- import { join as join21 } from "path";
4990
- import { homedir as homedir15 } from "os";
5210
+ import { existsSync as existsSync23, readFileSync as readFileSync18, writeFileSync as writeFileSync10, mkdirSync as mkdirSync3 } from "fs";
5211
+ import { join as join22 } from "path";
5212
+ import { homedir as homedir16 } from "os";
4991
5213
  import { execSync as execSync2, spawn } from "child_process";
4992
- var IMDL_DIR3 = join21(homedir15(), ".imdl");
4993
- var GATEWAY_CONFIG = join21(IMDL_DIR3, "gateway.toml");
4994
- var GATEWAY_PID_FILE = join21(IMDL_DIR3, "gateway.pid");
5214
+ var IMDL_DIR3 = join22(homedir16(), ".imdl");
5215
+ var GATEWAY_CONFIG = join22(IMDL_DIR3, "gateway.toml");
5216
+ var GATEWAY_PID_FILE = join22(IMDL_DIR3, "gateway.pid");
4995
5217
  var DEFAULT_PORT = 9443;
4996
5218
  async function gatewayCommand(opts) {
4997
5219
  const status = await getGatewayStatus();
@@ -5066,7 +5288,7 @@ async function gatewayStartCommand(opts) {
5066
5288
  });
5067
5289
  child.unref();
5068
5290
  if (child.pid) {
5069
- if (!existsSync22(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5291
+ if (!existsSync23(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5070
5292
  writeFileSync10(GATEWAY_PID_FILE, String(child.pid));
5071
5293
  console.log(pc12.green(`Gateway started (PID ${child.pid}, port ${port})`));
5072
5294
  console.log(pc12.dim(` Config: ${GATEWAY_CONFIG}`));
@@ -5094,8 +5316,8 @@ async function gatewayStopCommand() {
5094
5316
  console.log(pc12.yellow("Could not stop gateway \u2014 process may have already exited."));
5095
5317
  }
5096
5318
  try {
5097
- const { unlinkSync: unlinkSync3 } = await import("fs");
5098
- unlinkSync3(GATEWAY_PID_FILE);
5319
+ const { unlinkSync: unlinkSync4 } = await import("fs");
5320
+ unlinkSync4(GATEWAY_PID_FILE);
5099
5321
  } catch {
5100
5322
  }
5101
5323
  }
@@ -5126,12 +5348,12 @@ async function gatewayDisableCommand(opts) {
5126
5348
  console.log(pc12.dim("Restart gateway for changes to take effect."));
5127
5349
  }
5128
5350
  async function gatewayAlertsCommand(opts) {
5129
- const logPath = join21(IMDL_DIR3, "gateway-alerts.jsonl");
5130
- if (!existsSync22(logPath)) {
5351
+ const logPath = join22(IMDL_DIR3, "gateway-alerts.jsonl");
5352
+ if (!existsSync23(logPath)) {
5131
5353
  console.log(pc12.dim("No alerts logged yet."));
5132
5354
  return;
5133
5355
  }
5134
- const lines = readFileSync17(logPath, "utf-8").split("\n").filter(Boolean);
5356
+ const lines = readFileSync18(logPath, "utf-8").split("\n").filter(Boolean);
5135
5357
  const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
5136
5358
  const recent = lines.slice(-limit);
5137
5359
  if (opts.json) {
@@ -5233,9 +5455,9 @@ async function getGatewayStatus() {
5233
5455
  return { running: true, pid, port, stats, config };
5234
5456
  }
5235
5457
  function getRunningPid() {
5236
- if (!existsSync22(GATEWAY_PID_FILE)) return null;
5458
+ if (!existsSync23(GATEWAY_PID_FILE)) return null;
5237
5459
  try {
5238
- const pid = parseInt(readFileSync17(GATEWAY_PID_FILE, "utf-8").trim(), 10);
5460
+ const pid = parseInt(readFileSync18(GATEWAY_PID_FILE, "utf-8").trim(), 10);
5239
5461
  return isNaN(pid) ? null : pid;
5240
5462
  } catch {
5241
5463
  return null;
@@ -5250,9 +5472,9 @@ function isProcessAlive(pid) {
5250
5472
  }
5251
5473
  }
5252
5474
  function getConfiguredPort() {
5253
- if (!existsSync22(GATEWAY_CONFIG)) return DEFAULT_PORT;
5475
+ if (!existsSync23(GATEWAY_CONFIG)) return DEFAULT_PORT;
5254
5476
  try {
5255
- const content = readFileSync17(GATEWAY_CONFIG, "utf-8");
5477
+ const content = readFileSync18(GATEWAY_CONFIG, "utf-8");
5256
5478
  const match = content.match(/listen_port\s*=\s*(\d+)/);
5257
5479
  return match ? parseInt(match[1], 10) : DEFAULT_PORT;
5258
5480
  } catch {
@@ -5261,12 +5483,12 @@ function getConfiguredPort() {
5261
5483
  }
5262
5484
  function findGatewayBinary() {
5263
5485
  const candidates = [
5264
- join21(process.cwd(), "target", "release", "imdl-gateway"),
5265
- join21(process.cwd(), "..", "ai-gateway", "target", "release", "imdl-gateway"),
5266
- join21(homedir15(), ".imdl", "bin", "imdl-gateway")
5486
+ join22(process.cwd(), "target", "release", "imdl-gateway"),
5487
+ join22(process.cwd(), "..", "ai-gateway", "target", "release", "imdl-gateway"),
5488
+ join22(homedir16(), ".imdl", "bin", "imdl-gateway")
5267
5489
  ];
5268
5490
  for (const path of candidates) {
5269
- if (existsSync22(path)) return path;
5491
+ if (existsSync23(path)) return path;
5270
5492
  }
5271
5493
  try {
5272
5494
  const result = execSync2("which imdl-gateway", { encoding: "utf-8" }).trim();
@@ -5276,8 +5498,8 @@ function findGatewayBinary() {
5276
5498
  return null;
5277
5499
  }
5278
5500
  function ensureConfig(port) {
5279
- if (existsSync22(GATEWAY_CONFIG)) return;
5280
- if (!existsSync22(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5501
+ if (existsSync23(GATEWAY_CONFIG)) return;
5502
+ if (!existsSync23(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5281
5503
  const defaultToml = `# IMDL AI Gateway configuration
5282
5504
  listen_host = "127.0.0.1"
5283
5505
  listen_port = ${port}
@@ -5321,10 +5543,10 @@ function normalizeModule(input) {
5321
5543
  return map[input.toLowerCase()] ?? null;
5322
5544
  }
5323
5545
  function loadGatewayToml() {
5324
- if (!existsSync22(GATEWAY_CONFIG)) {
5546
+ if (!existsSync23(GATEWAY_CONFIG)) {
5325
5547
  ensureConfig(DEFAULT_PORT);
5326
5548
  }
5327
- return readFileSync17(GATEWAY_CONFIG, "utf-8");
5549
+ return readFileSync18(GATEWAY_CONFIG, "utf-8");
5328
5550
  }
5329
5551
  function saveGatewayToml(content) {
5330
5552
  writeFileSync10(GATEWAY_CONFIG, content, { mode: 384 });
@@ -5349,6 +5571,146 @@ function formatUptime(seconds) {
5349
5571
  return `${h}h ${m}m`;
5350
5572
  }
5351
5573
 
5574
+ // src/commands/uninstall.ts
5575
+ import { Command } from "commander";
5576
+ import { existsSync as existsSync24, readFileSync as readFileSync19, rmSync, unlinkSync as unlinkSync3 } from "fs";
5577
+ import { homedir as homedir17 } from "os";
5578
+ import { join as join23 } from "path";
5579
+ import pc13 from "picocolors";
5580
+ 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) => {
5581
+ const home = homedir17();
5582
+ const imdlDir = join23(home, ".imdl");
5583
+ console.log("");
5584
+ console.log(pc13.bold(" Frostbridge IMDL \u2014 Uninstaller"));
5585
+ console.log("");
5586
+ const removed = [];
5587
+ const skipped = [];
5588
+ const claudeSettings = join23(home, ".claude", "settings.json");
5589
+ if (existsSync24(claudeSettings)) {
5590
+ try {
5591
+ const raw = readFileSync19(claudeSettings, "utf8");
5592
+ const settings = JSON.parse(raw);
5593
+ let modified = false;
5594
+ for (const hookType of ["PreToolUse", "PostToolUse", "UserPromptSubmit"]) {
5595
+ if (settings.hooks?.[hookType]) {
5596
+ const before = settings.hooks[hookType].length;
5597
+ settings.hooks[hookType] = settings.hooks[hookType].filter(
5598
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("imdl"))
5599
+ );
5600
+ if (settings.hooks[hookType].length < before) modified = true;
5601
+ if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
5602
+ }
5603
+ }
5604
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
5605
+ if (modified) {
5606
+ const { writeFileSync: writeFileSync12 } = await import("fs");
5607
+ writeFileSync12(claudeSettings, JSON.stringify(settings, null, 2));
5608
+ removed.push("Claude Code hooks");
5609
+ }
5610
+ } catch {
5611
+ skipped.push("Claude Code hooks (could not parse settings.json)");
5612
+ }
5613
+ }
5614
+ const cursorSettings = join23(home, ".cursor", "settings.json");
5615
+ if (existsSync24(cursorSettings)) {
5616
+ try {
5617
+ const raw = readFileSync19(cursorSettings, "utf8");
5618
+ const settings = JSON.parse(raw);
5619
+ let modified = false;
5620
+ if (settings.hooks) {
5621
+ for (const hookType of Object.keys(settings.hooks)) {
5622
+ const before = settings.hooks[hookType]?.length || 0;
5623
+ if (Array.isArray(settings.hooks[hookType])) {
5624
+ settings.hooks[hookType] = settings.hooks[hookType].filter(
5625
+ (h) => !h.hooks?.some((hh) => hh.command?.includes("imdl"))
5626
+ );
5627
+ if (settings.hooks[hookType].length < before) modified = true;
5628
+ }
5629
+ }
5630
+ }
5631
+ if (modified) {
5632
+ const { writeFileSync: writeFileSync12 } = await import("fs");
5633
+ writeFileSync12(cursorSettings, JSON.stringify(settings, null, 2));
5634
+ removed.push("Cursor hooks");
5635
+ }
5636
+ } catch {
5637
+ skipped.push("Cursor hooks (could not parse settings.json)");
5638
+ }
5639
+ }
5640
+ for (const rc of [".bashrc", ".zshrc", ".profile"]) {
5641
+ const rcPath = join23(home, rc);
5642
+ if (existsSync24(rcPath)) {
5643
+ try {
5644
+ const content = readFileSync19(rcPath, "utf8");
5645
+ if (content.includes("imdl") || content.includes("frostbridge")) {
5646
+ const lines = content.split("\n");
5647
+ const filtered = lines.filter(
5648
+ (l) => !l.includes("imdl") && !l.includes("frostbridge") && !l.includes("# Added by Frostbridge")
5649
+ );
5650
+ if (filtered.length < lines.length) {
5651
+ const { writeFileSync: writeFileSync12 } = await import("fs");
5652
+ writeFileSync12(rcPath, filtered.join("\n"));
5653
+ removed.push(`Shell rc entries (${rc})`);
5654
+ }
5655
+ }
5656
+ } catch {
5657
+ }
5658
+ }
5659
+ }
5660
+ try {
5661
+ const pidFile = join23(imdlDir, "daemon.pid");
5662
+ if (existsSync24(pidFile)) {
5663
+ const pid = parseInt(readFileSync19(pidFile, "utf8").trim());
5664
+ if (pid > 0) {
5665
+ try {
5666
+ process.kill(pid, "SIGTERM");
5667
+ } catch {
5668
+ }
5669
+ unlinkSync3(pidFile);
5670
+ removed.push("Background daemon (killed)");
5671
+ }
5672
+ }
5673
+ } catch {
5674
+ }
5675
+ if (!opts.keepConfig && existsSync24(imdlDir)) {
5676
+ rmSync(imdlDir, { recursive: true, force: true });
5677
+ removed.push("~/.imdl config directory");
5678
+ } else if (opts.keepConfig) {
5679
+ skipped.push("~/.imdl (--keep-config)");
5680
+ }
5681
+ const bufferDir = join23(home, ".imdl", "buffer");
5682
+ if (existsSync24(bufferDir)) {
5683
+ rmSync(bufferDir, { recursive: true, force: true });
5684
+ removed.push("Event buffer");
5685
+ }
5686
+ console.log(pc13.dim(" Uninstalling npm packages..."));
5687
+ const { execSync: execSync3 } = await import("child_process");
5688
+ const packages = ["@frostbridge/imdl", "@frostbridge/imdl-shell-wrapper", "@frostbridge/imdl-mcp-proxy"];
5689
+ for (const pkg of packages) {
5690
+ try {
5691
+ execSync3(`npm uninstall -g ${pkg} 2>/dev/null`, { stdio: "pipe" });
5692
+ removed.push(`npm: ${pkg}`);
5693
+ } catch {
5694
+ }
5695
+ }
5696
+ console.log("");
5697
+ if (removed.length > 0) {
5698
+ console.log(pc13.green(pc13.bold(" Removed:")));
5699
+ for (const item of removed) {
5700
+ console.log(pc13.green(` \u2713 ${item}`));
5701
+ }
5702
+ }
5703
+ if (skipped.length > 0) {
5704
+ console.log(pc13.yellow(pc13.bold(" Skipped:")));
5705
+ for (const item of skipped) {
5706
+ console.log(pc13.yellow(` \u25CB ${item}`));
5707
+ }
5708
+ }
5709
+ console.log("");
5710
+ console.log(pc13.dim(" IMDL has been completely removed from this machine."));
5711
+ console.log("");
5712
+ });
5713
+
5352
5714
  // src/bifrost/compiler.ts
5353
5715
  function globToRegex(glob) {
5354
5716
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{\{GLOBSTAR\}\}/g, ".*");
@@ -5657,18 +6019,18 @@ function ruleTypeToCategory(type) {
5657
6019
  }
5658
6020
 
5659
6021
  // src/transport/sync.ts
5660
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync11, existsSync as existsSync23 } from "fs";
5661
- import { join as join22 } from "path";
6022
+ import { readFileSync as readFileSync20, writeFileSync as writeFileSync11, existsSync as existsSync25 } from "fs";
6023
+ import { join as join24 } from "path";
5662
6024
  var SYNC_INTERVAL = 6e4;
5663
6025
  var SYNC_TIMEOUT = 3e3;
5664
6026
  function getSyncStateFile() {
5665
- return join22(getImdlDir(), "last_sync.json");
6027
+ return join24(getImdlDir(), "last_sync.json");
5666
6028
  }
5667
6029
  function getLastSyncTime() {
5668
6030
  try {
5669
6031
  const file = getSyncStateFile();
5670
- if (existsSync23(file)) {
5671
- const data = JSON.parse(readFileSync18(file, "utf-8"));
6032
+ if (existsSync25(file)) {
6033
+ const data = JSON.parse(readFileSync20(file, "utf-8"));
5672
6034
  return data.lastSync || 0;
5673
6035
  }
5674
6036
  } catch {
@@ -6079,7 +6441,7 @@ function getSafeAlternative(toolName, toolInput) {
6079
6441
  }
6080
6442
 
6081
6443
  // src/index.ts
6082
- var program = new Command();
6444
+ var program = new Command2();
6083
6445
  program.name("imdl").description("IMDL \u2014 Intelligent Mediation & Detection Layer. AI agent security.").version("0.1.0");
6084
6446
  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
6447
  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 }));
@@ -6106,5 +6468,6 @@ gwCmd.command("enable <module>").description("Enable a gateway module (secrets,
6106
6468
  gwCmd.command("disable <module>").description("Disable a gateway module").action((module) => gatewayDisableCommand({ module }));
6107
6469
  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
6470
  gwCmd.command("cost").description("Show AI spend and usage breakdown").option("--json", "Output as JSON").action(gatewayCostCommand);
6471
+ program.addCommand(uninstall);
6109
6472
  program.command("hook <type>").description("Handle hook events from AI agents (internal)").action(handleHook);
6110
6473
  program.parse();
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "restricted"
5
5
  },
6
- "version": "0.1.12",
6
+ "version": "0.1.13",
7
7
  "description": "IMDL — Intelligent Mediation & Detection Layer. AI agent security CLI.",
8
8
  "files": [
9
9
  "dist"