@frostbridge/imdl 0.1.11 → 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 +494 -75
  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"));
@@ -3912,9 +4134,10 @@ async function daemonCommand() {
3912
4134
  try {
3913
4135
  const count = await collectPrompts();
3914
4136
  const shellCount = await ingestShellEvents();
3915
- const total = count + shellCount;
4137
+ const agentCount = await ingestAgentEvents();
4138
+ const total = count + shellCount + agentCount;
3916
4139
  if (total > 0) {
3917
- console.log(pc7.green(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Collected ${total} event${total !== 1 ? "s" : ""} (prompts: ${count}, shell: ${shellCount})`));
4140
+ console.log(pc7.green(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Collected ${total} event${total !== 1 ? "s" : ""} (prompts: ${count}, shell: ${shellCount}, os-agent: ${agentCount})`));
3918
4141
  }
3919
4142
  await tryFlush();
3920
4143
  } catch (err) {
@@ -3924,10 +4147,65 @@ async function daemonCommand() {
3924
4147
  await tick();
3925
4148
  setInterval(tick, INTERVAL_MS);
3926
4149
  }
4150
+ async function ingestAgentEvents() {
4151
+ const agentLog = join19(getBufferDir(), "agent-events.ndjson");
4152
+ if (!existsSync20(agentLog)) return 0;
4153
+ const content = readFileSync15(agentLog, "utf-8").trim();
4154
+ if (!content) return 0;
4155
+ const config = loadConfig();
4156
+ const lines = content.split("\n");
4157
+ const events = lines.map((line) => {
4158
+ try {
4159
+ return JSON.parse(line);
4160
+ } catch {
4161
+ return null;
4162
+ }
4163
+ }).filter(Boolean);
4164
+ if (events.length === 0) return 0;
4165
+ const headers = { "Content-Type": "application/json" };
4166
+ if (config.authToken) headers["X-IMDL-Key"] = config.authToken;
4167
+ const sessionId = `os_${config.developerId}_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
4168
+ const apiEvents = events.map((e) => ({
4169
+ type: e.event_type === "exec" ? "pre_tool_use" : "tool_call",
4170
+ toolName: `os_${e.event_type}`,
4171
+ toolInput: {
4172
+ process: e.process,
4173
+ path: e.path,
4174
+ address: e.address,
4175
+ pid: e.pid,
4176
+ agent: e.agent
4177
+ },
4178
+ timestamp: e.timestamp,
4179
+ violation: e.decision === "block" ? {
4180
+ action: "block",
4181
+ reason: e.reason,
4182
+ policyName: e.policy_name
4183
+ } : void 0,
4184
+ source: "os_agent"
4185
+ }));
4186
+ let sent = 0;
4187
+ for (let i = 0; i < apiEvents.length; i += 50) {
4188
+ const batch = apiEvents.slice(i, i + 50);
4189
+ try {
4190
+ const res = await fetch(`${config.apiUrl}/api/sessions/${sessionId}/events`, {
4191
+ method: "POST",
4192
+ headers,
4193
+ body: JSON.stringify({ events: batch, developerId: config.developerId, source: "os_agent" }),
4194
+ signal: AbortSignal.timeout(1e4)
4195
+ });
4196
+ if (res.ok) sent += batch.length;
4197
+ } catch {
4198
+ }
4199
+ }
4200
+ if (sent === apiEvents.length) {
4201
+ writeFileSync7(agentLog, "", { mode: 384 });
4202
+ }
4203
+ return sent;
4204
+ }
3927
4205
  async function ingestShellEvents() {
3928
- const shellLog = join18(getBufferDir(), "shell-events.ndjson");
3929
- if (!existsSync19(shellLog)) return 0;
3930
- 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();
3931
4209
  if (!content) return 0;
3932
4210
  const config = loadConfig();
3933
4211
  const lines = content.split("\n");
@@ -4032,8 +4310,8 @@ async function requestCommand(options) {
4032
4310
  // src/commands/lock.ts
4033
4311
  import pc9 from "picocolors";
4034
4312
  import { createHash as createHash2 } from "crypto";
4035
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, existsSync as existsSync20 } from "fs";
4036
- 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";
4037
4315
  var LOCKFILE_NAME = "mcp-lock.json";
4038
4316
  function computeIntegrity(server, identity) {
4039
4317
  const payload = JSON.stringify({
@@ -4048,7 +4326,7 @@ function computeIntegrity(server, identity) {
4048
4326
  }
4049
4327
  function getLockfilePath(customPath) {
4050
4328
  if (customPath) return resolve3(customPath);
4051
- return join19(process.cwd(), LOCKFILE_NAME);
4329
+ return join20(process.cwd(), LOCKFILE_NAME);
4052
4330
  }
4053
4331
  async function lockCommand(options) {
4054
4332
  const detection = await detectMCPConfigs(options.path);
@@ -4118,7 +4396,7 @@ async function lockCommand(options) {
4118
4396
  }
4119
4397
  async function verifyCommand(options) {
4120
4398
  const lockPath = getLockfilePath(options.path);
4121
- if (!existsSync20(lockPath)) {
4399
+ if (!existsSync21(lockPath)) {
4122
4400
  if (options.json) {
4123
4401
  console.log(JSON.stringify({ error: "No lockfile found. Run `imdl lock` first.", path: lockPath }));
4124
4402
  } else {
@@ -4130,7 +4408,7 @@ async function verifyCommand(options) {
4130
4408
  }
4131
4409
  let lockfile;
4132
4410
  try {
4133
- lockfile = JSON.parse(readFileSync15(lockPath, "utf-8"));
4411
+ lockfile = JSON.parse(readFileSync16(lockPath, "utf-8"));
4134
4412
  } catch {
4135
4413
  console.log(pc9.red("Failed to parse lockfile."));
4136
4414
  process.exit(1);
@@ -4539,12 +4817,12 @@ async function checkCompliance(developerId) {
4539
4817
  }
4540
4818
 
4541
4819
  // src/permissions/fixer.ts
4542
- import { existsSync as existsSync21, readFileSync as readFileSync16, writeFileSync as writeFileSync9 } from "fs";
4543
- import { join as join20 } from "path";
4544
- 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";
4545
4823
  function buildFixesFromChanges(changes) {
4546
4824
  const fixes = [];
4547
- const home = homedir14();
4825
+ const home = homedir15();
4548
4826
  for (const change of changes) {
4549
4827
  const fix = buildFixForAgent(change, home);
4550
4828
  if (fix) fixes.push(fix);
@@ -4554,10 +4832,10 @@ function buildFixesFromChanges(changes) {
4554
4832
  function buildFixForAgent(change, home) {
4555
4833
  const { agentType, category, action, targetGrant } = change;
4556
4834
  if (agentType === "claude-code") {
4557
- const configPath = join20(home, ".claude", "settings.json");
4558
- if (!existsSync21(configPath)) return null;
4835
+ const configPath = join21(home, ".claude", "settings.json");
4836
+ if (!existsSync22(configPath)) return null;
4559
4837
  try {
4560
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
4838
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4561
4839
  const tools = settings.allowedTools || [];
4562
4840
  if (category === "shell") {
4563
4841
  if (!tools.includes("Bash") && !tools.some((t) => t.startsWith("Bash("))) return null;
@@ -4585,10 +4863,10 @@ function buildFixForAgent(change, home) {
4585
4863
  }
4586
4864
  }
4587
4865
  if (agentType === "cursor") {
4588
- const configPath = join20(home, ".cursor", "settings.json");
4589
- if (!existsSync21(configPath)) return null;
4866
+ const configPath = join21(home, ".cursor", "settings.json");
4867
+ if (!existsSync22(configPath)) return null;
4590
4868
  try {
4591
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
4869
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4592
4870
  if ((category === "agentic_mode" || category === "shell") && settings["cursor.composer.yoloMode"] === true) {
4593
4871
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Disable YOLO mode" };
4594
4872
  }
@@ -4597,10 +4875,10 @@ function buildFixForAgent(change, home) {
4597
4875
  }
4598
4876
  }
4599
4877
  if (agentType === "codex") {
4600
- const configPath = join20(home, ".codex", "config.json");
4601
- if (!existsSync21(configPath)) return null;
4878
+ const configPath = join21(home, ".codex", "config.json");
4879
+ if (!existsSync22(configPath)) return null;
4602
4880
  try {
4603
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
4881
+ const config = JSON.parse(readFileSync17(configPath, "utf-8"));
4604
4882
  if ((category === "agentic_mode" || category === "shell") && config.sandbox_mode === "full_auto") {
4605
4883
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Switch to supervised mode" };
4606
4884
  }
@@ -4609,10 +4887,10 @@ function buildFixForAgent(change, home) {
4609
4887
  }
4610
4888
  }
4611
4889
  if (agentType === "copilot") {
4612
- const configPath = join20(home, "Library", "Application Support", "Code", "User", "settings.json");
4613
- if (!existsSync21(configPath)) return null;
4890
+ const configPath = join21(home, "Library", "Application Support", "Code", "User", "settings.json");
4891
+ if (!existsSync22(configPath)) return null;
4614
4892
  try {
4615
- const settings = JSON.parse(readFileSync16(configPath, "utf-8"));
4893
+ const settings = JSON.parse(readFileSync17(configPath, "utf-8"));
4616
4894
  if ((category === "agentic_mode" || category === "shell") && (settings["github.copilot.chat.agent.autoApprove"] || settings["chat.agent.autoApprove"])) {
4617
4895
  return { agentType, category: "agentic_mode", currentGrant: "unrestricted", newGrant: "confirmation", configPath, description: "Disable autoApprove" };
4618
4896
  }
@@ -4622,10 +4900,10 @@ function buildFixForAgent(change, home) {
4622
4900
  }
4623
4901
  if (agentType === "windsurf") {
4624
4902
  if (category === "mcp_tools") {
4625
- const configPath = join20(home, ".windsurf", "mcp.json");
4626
- if (!existsSync21(configPath)) return null;
4903
+ const configPath = join21(home, ".windsurf", "mcp.json");
4904
+ if (!existsSync22(configPath)) return null;
4627
4905
  try {
4628
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
4906
+ const config = JSON.parse(readFileSync17(configPath, "utf-8"));
4629
4907
  const servers = Object.keys(config.mcpServers || {});
4630
4908
  if (servers.length === 0) return null;
4631
4909
  return { agentType, category, currentGrant: "unrestricted", newGrant: "denied", configPath, description: `Remove MCP servers (${servers.join(", ")})` };
@@ -4638,7 +4916,7 @@ function buildFixForAgent(change, home) {
4638
4916
  }
4639
4917
  function applyFix(fix) {
4640
4918
  try {
4641
- const content = readFileSync16(fix.configPath, "utf-8");
4919
+ const content = readFileSync17(fix.configPath, "utf-8");
4642
4920
  const config = JSON.parse(content);
4643
4921
  let modified = false;
4644
4922
  if (fix.agentType === "claude-code") {
@@ -4929,13 +5207,13 @@ function grantLevel(grant) {
4929
5207
 
4930
5208
  // src/commands/gateway.ts
4931
5209
  import pc12 from "picocolors";
4932
- import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync3 } from "fs";
4933
- import { join as join21 } from "path";
4934
- 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";
4935
5213
  import { execSync as execSync2, spawn } from "child_process";
4936
- var IMDL_DIR3 = join21(homedir15(), ".imdl");
4937
- var GATEWAY_CONFIG = join21(IMDL_DIR3, "gateway.toml");
4938
- 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");
4939
5217
  var DEFAULT_PORT = 9443;
4940
5218
  async function gatewayCommand(opts) {
4941
5219
  const status = await getGatewayStatus();
@@ -5010,7 +5288,7 @@ async function gatewayStartCommand(opts) {
5010
5288
  });
5011
5289
  child.unref();
5012
5290
  if (child.pid) {
5013
- if (!existsSync22(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5291
+ if (!existsSync23(IMDL_DIR3)) mkdirSync3(IMDL_DIR3, { recursive: true });
5014
5292
  writeFileSync10(GATEWAY_PID_FILE, String(child.pid));
5015
5293
  console.log(pc12.green(`Gateway started (PID ${child.pid}, port ${port})`));
5016
5294
  console.log(pc12.dim(` Config: ${GATEWAY_CONFIG}`));
@@ -5038,8 +5316,8 @@ async function gatewayStopCommand() {
5038
5316
  console.log(pc12.yellow("Could not stop gateway \u2014 process may have already exited."));
5039
5317
  }
5040
5318
  try {
5041
- const { unlinkSync: unlinkSync3 } = await import("fs");
5042
- unlinkSync3(GATEWAY_PID_FILE);
5319
+ const { unlinkSync: unlinkSync4 } = await import("fs");
5320
+ unlinkSync4(GATEWAY_PID_FILE);
5043
5321
  } catch {
5044
5322
  }
5045
5323
  }
@@ -5070,12 +5348,12 @@ async function gatewayDisableCommand(opts) {
5070
5348
  console.log(pc12.dim("Restart gateway for changes to take effect."));
5071
5349
  }
5072
5350
  async function gatewayAlertsCommand(opts) {
5073
- const logPath = join21(IMDL_DIR3, "gateway-alerts.jsonl");
5074
- if (!existsSync22(logPath)) {
5351
+ const logPath = join22(IMDL_DIR3, "gateway-alerts.jsonl");
5352
+ if (!existsSync23(logPath)) {
5075
5353
  console.log(pc12.dim("No alerts logged yet."));
5076
5354
  return;
5077
5355
  }
5078
- const lines = readFileSync17(logPath, "utf-8").split("\n").filter(Boolean);
5356
+ const lines = readFileSync18(logPath, "utf-8").split("\n").filter(Boolean);
5079
5357
  const limit = opts.limit ? parseInt(opts.limit, 10) : 20;
5080
5358
  const recent = lines.slice(-limit);
5081
5359
  if (opts.json) {
@@ -5177,9 +5455,9 @@ async function getGatewayStatus() {
5177
5455
  return { running: true, pid, port, stats, config };
5178
5456
  }
5179
5457
  function getRunningPid() {
5180
- if (!existsSync22(GATEWAY_PID_FILE)) return null;
5458
+ if (!existsSync23(GATEWAY_PID_FILE)) return null;
5181
5459
  try {
5182
- const pid = parseInt(readFileSync17(GATEWAY_PID_FILE, "utf-8").trim(), 10);
5460
+ const pid = parseInt(readFileSync18(GATEWAY_PID_FILE, "utf-8").trim(), 10);
5183
5461
  return isNaN(pid) ? null : pid;
5184
5462
  } catch {
5185
5463
  return null;
@@ -5194,9 +5472,9 @@ function isProcessAlive(pid) {
5194
5472
  }
5195
5473
  }
5196
5474
  function getConfiguredPort() {
5197
- if (!existsSync22(GATEWAY_CONFIG)) return DEFAULT_PORT;
5475
+ if (!existsSync23(GATEWAY_CONFIG)) return DEFAULT_PORT;
5198
5476
  try {
5199
- const content = readFileSync17(GATEWAY_CONFIG, "utf-8");
5477
+ const content = readFileSync18(GATEWAY_CONFIG, "utf-8");
5200
5478
  const match = content.match(/listen_port\s*=\s*(\d+)/);
5201
5479
  return match ? parseInt(match[1], 10) : DEFAULT_PORT;
5202
5480
  } catch {
@@ -5205,12 +5483,12 @@ function getConfiguredPort() {
5205
5483
  }
5206
5484
  function findGatewayBinary() {
5207
5485
  const candidates = [
5208
- join21(process.cwd(), "target", "release", "imdl-gateway"),
5209
- join21(process.cwd(), "..", "ai-gateway", "target", "release", "imdl-gateway"),
5210
- 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")
5211
5489
  ];
5212
5490
  for (const path of candidates) {
5213
- if (existsSync22(path)) return path;
5491
+ if (existsSync23(path)) return path;
5214
5492
  }
5215
5493
  try {
5216
5494
  const result = execSync2("which imdl-gateway", { encoding: "utf-8" }).trim();
@@ -5220,8 +5498,8 @@ function findGatewayBinary() {
5220
5498
  return null;
5221
5499
  }
5222
5500
  function ensureConfig(port) {
5223
- if (existsSync22(GATEWAY_CONFIG)) return;
5224
- 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 });
5225
5503
  const defaultToml = `# IMDL AI Gateway configuration
5226
5504
  listen_host = "127.0.0.1"
5227
5505
  listen_port = ${port}
@@ -5265,10 +5543,10 @@ function normalizeModule(input) {
5265
5543
  return map[input.toLowerCase()] ?? null;
5266
5544
  }
5267
5545
  function loadGatewayToml() {
5268
- if (!existsSync22(GATEWAY_CONFIG)) {
5546
+ if (!existsSync23(GATEWAY_CONFIG)) {
5269
5547
  ensureConfig(DEFAULT_PORT);
5270
5548
  }
5271
- return readFileSync17(GATEWAY_CONFIG, "utf-8");
5549
+ return readFileSync18(GATEWAY_CONFIG, "utf-8");
5272
5550
  }
5273
5551
  function saveGatewayToml(content) {
5274
5552
  writeFileSync10(GATEWAY_CONFIG, content, { mode: 384 });
@@ -5293,6 +5571,146 @@ function formatUptime(seconds) {
5293
5571
  return `${h}h ${m}m`;
5294
5572
  }
5295
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
+
5296
5714
  // src/bifrost/compiler.ts
5297
5715
  function globToRegex(glob) {
5298
5716
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, ".").replace(/\{\{GLOBSTAR\}\}/g, ".*");
@@ -5601,18 +6019,18 @@ function ruleTypeToCategory(type) {
5601
6019
  }
5602
6020
 
5603
6021
  // src/transport/sync.ts
5604
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync11, existsSync as existsSync23 } from "fs";
5605
- 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";
5606
6024
  var SYNC_INTERVAL = 6e4;
5607
6025
  var SYNC_TIMEOUT = 3e3;
5608
6026
  function getSyncStateFile() {
5609
- return join22(getImdlDir(), "last_sync.json");
6027
+ return join24(getImdlDir(), "last_sync.json");
5610
6028
  }
5611
6029
  function getLastSyncTime() {
5612
6030
  try {
5613
6031
  const file = getSyncStateFile();
5614
- if (existsSync23(file)) {
5615
- const data = JSON.parse(readFileSync18(file, "utf-8"));
6032
+ if (existsSync25(file)) {
6033
+ const data = JSON.parse(readFileSync20(file, "utf-8"));
5616
6034
  return data.lastSync || 0;
5617
6035
  }
5618
6036
  } catch {
@@ -6023,7 +6441,7 @@ function getSafeAlternative(toolName, toolInput) {
6023
6441
  }
6024
6442
 
6025
6443
  // src/index.ts
6026
- var program = new Command();
6444
+ var program = new Command2();
6027
6445
  program.name("imdl").description("IMDL \u2014 Intelligent Mediation & Detection Layer. AI agent security.").version("0.1.0");
6028
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);
6029
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 }));
@@ -6050,5 +6468,6 @@ gwCmd.command("enable <module>").description("Enable a gateway module (secrets,
6050
6468
  gwCmd.command("disable <module>").description("Disable a gateway module").action((module) => gatewayDisableCommand({ module }));
6051
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);
6052
6470
  gwCmd.command("cost").description("Show AI spend and usage breakdown").option("--json", "Output as JSON").action(gatewayCostCommand);
6471
+ program.addCommand(uninstall);
6053
6472
  program.command("hook <type>").description("Handle hook events from AI agents (internal)").action(handleHook);
6054
6473
  program.parse();
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "restricted"
5
5
  },
6
- "version": "0.1.11",
6
+ "version": "0.1.13",
7
7
  "description": "IMDL — Intelligent Mediation & Detection Layer. AI agent security CLI.",
8
8
  "files": [
9
9
  "dist"