skalpel 2.0.12 → 2.0.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.
package/dist/cli/index.js CHANGED
@@ -250,8 +250,69 @@ function detectClaudeCode() {
250
250
  }
251
251
  return agent;
252
252
  }
253
+ function detectCodex() {
254
+ const agent = {
255
+ name: "codex",
256
+ installed: false,
257
+ version: null,
258
+ configPath: null
259
+ };
260
+ const binaryPath = tryExec(`${whichCommand()} codex`);
261
+ const hasBinary = binaryPath !== null && binaryPath.length > 0;
262
+ const codexConfigDir = process.platform === "win32" ? path3.join(os.homedir(), "AppData", "Roaming", "codex") : path3.join(os.homedir(), ".codex");
263
+ const hasConfigDir = fs3.existsSync(codexConfigDir);
264
+ agent.installed = hasBinary || hasConfigDir;
265
+ if (hasBinary) {
266
+ const versionOutput = tryExec("codex --version");
267
+ if (versionOutput) {
268
+ const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
269
+ agent.version = match ? match[1] : versionOutput;
270
+ }
271
+ }
272
+ const configFile = path3.join(codexConfigDir, "config.toml");
273
+ if (fs3.existsSync(configFile)) {
274
+ agent.configPath = configFile;
275
+ } else if (hasConfigDir) {
276
+ agent.configPath = configFile;
277
+ }
278
+ return agent;
279
+ }
280
+ function detectCursor() {
281
+ const agent = {
282
+ name: "cursor",
283
+ installed: false,
284
+ version: null,
285
+ configPath: null
286
+ };
287
+ const binaryPath = tryExec(`${whichCommand()} cursor`);
288
+ const hasBinary = binaryPath !== null && binaryPath.length > 0;
289
+ let cursorConfigDir;
290
+ if (process.platform === "darwin") {
291
+ cursorConfigDir = path3.join(os.homedir(), "Library", "Application Support", "Cursor", "User");
292
+ } else if (process.platform === "win32") {
293
+ cursorConfigDir = path3.join(process.env.APPDATA ?? path3.join(os.homedir(), "AppData", "Roaming"), "Cursor", "User");
294
+ } else {
295
+ cursorConfigDir = path3.join(os.homedir(), ".config", "Cursor", "User");
296
+ }
297
+ const hasConfigDir = fs3.existsSync(cursorConfigDir);
298
+ agent.installed = hasBinary || hasConfigDir;
299
+ if (hasBinary) {
300
+ const versionOutput = tryExec("cursor --version");
301
+ if (versionOutput) {
302
+ const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
303
+ agent.version = match ? match[1] : versionOutput;
304
+ }
305
+ }
306
+ const settingsPath = path3.join(cursorConfigDir, "settings.json");
307
+ if (fs3.existsSync(settingsPath)) {
308
+ agent.configPath = settingsPath;
309
+ } else if (hasConfigDir) {
310
+ agent.configPath = settingsPath;
311
+ }
312
+ return agent;
313
+ }
253
314
  function detectAgents() {
254
- return [detectClaudeCode()];
315
+ return [detectClaudeCode(), detectCodex(), detectCursor()];
255
316
  }
256
317
 
257
318
  // src/cli/doctor.ts
@@ -304,6 +365,14 @@ async function runDoctor() {
304
365
  } else {
305
366
  checks.push({ name: "Skalpel config", status: "warn", message: 'No ~/.skalpel/config.json \u2014 run "npx skalpel" to set up' });
306
367
  }
368
+ let mode = "proxy";
369
+ try {
370
+ const raw = JSON.parse(fs4.readFileSync(skalpelConfigPath, "utf-8"));
371
+ if (raw.mode === "direct") mode = "direct";
372
+ } catch {
373
+ }
374
+ const modeMessage = mode === "direct" ? "direct (agents point at api.skalpel.ai)" : "proxy (local proxy on ports 18100/18101/18102)";
375
+ checks.push({ name: "mode", status: "ok", message: modeMessage });
307
376
  const baseURL = "https://api.skalpel.ai";
308
377
  try {
309
378
  const controller = new AbortController();
@@ -338,6 +407,25 @@ async function runDoctor() {
338
407
  } catch {
339
408
  checks.push({ name: "Local proxy", status: "warn", message: `Not running on port ${proxyPort}. Run "npx skalpel start" to start.` });
340
409
  }
410
+ let cursorPort = 18102;
411
+ try {
412
+ const raw = JSON.parse(fs4.readFileSync(skalpelConfigPath, "utf-8"));
413
+ cursorPort = raw.cursorPort ?? 18102;
414
+ } catch {
415
+ }
416
+ try {
417
+ const controller = new AbortController();
418
+ const timeout = setTimeout(() => controller.abort(), 2e3);
419
+ const res = await fetch(`http://localhost:${cursorPort}/health`, { signal: controller.signal });
420
+ clearTimeout(timeout);
421
+ if (res.ok) {
422
+ checks.push({ name: "Cursor proxy", status: "ok", message: `Running on port ${cursorPort}` });
423
+ } else {
424
+ checks.push({ name: "Cursor proxy", status: "warn", message: `Port ${cursorPort} responded with HTTP ${res.status}` });
425
+ }
426
+ } catch {
427
+ checks.push({ name: "Cursor proxy", status: "warn", message: `Not running on port ${cursorPort}. Run "npx skalpel start" to start.` });
428
+ }
341
429
  const agents = detectAgents();
342
430
  for (const agent of agents) {
343
431
  if (agent.installed) {
@@ -551,7 +639,7 @@ async function runReplay(filePaths) {
551
639
 
552
640
  // src/cli/start.ts
553
641
  import { spawn } from "child_process";
554
- import path12 from "path";
642
+ import path10 from "path";
555
643
  import { fileURLToPath as fileURLToPath2 } from "url";
556
644
 
557
645
  // src/proxy/config.ts
@@ -568,12 +656,20 @@ var DEFAULTS = {
568
656
  apiKey: "",
569
657
  remoteBaseUrl: "https://api.skalpel.ai",
570
658
  anthropicDirectUrl: "https://api.anthropic.com",
659
+ openaiDirectUrl: "https://api.openai.com",
571
660
  anthropicPort: 18100,
661
+ openaiPort: 18101,
662
+ cursorPort: 18102,
663
+ cursorDirectUrl: "https://api.openai.com",
572
664
  logLevel: "info",
573
665
  logFile: "~/.skalpel/logs/proxy.log",
574
666
  pidFile: "~/.skalpel/proxy.pid",
575
- configFile: "~/.skalpel/config.json"
667
+ configFile: "~/.skalpel/config.json",
668
+ mode: "proxy"
576
669
  };
670
+ function coerceMode(value) {
671
+ return value === "direct" ? "direct" : "proxy";
672
+ }
577
673
  function loadConfig(configPath) {
578
674
  const filePath = expandHome(configPath ?? DEFAULTS.configFile);
579
675
  let fileConfig = {};
@@ -586,25 +682,47 @@ function loadConfig(configPath) {
586
682
  apiKey: fileConfig.apiKey ?? DEFAULTS.apiKey,
587
683
  remoteBaseUrl: fileConfig.remoteBaseUrl ?? DEFAULTS.remoteBaseUrl,
588
684
  anthropicDirectUrl: fileConfig.anthropicDirectUrl ?? DEFAULTS.anthropicDirectUrl,
685
+ openaiDirectUrl: fileConfig.openaiDirectUrl ?? DEFAULTS.openaiDirectUrl,
589
686
  anthropicPort: fileConfig.anthropicPort ?? DEFAULTS.anthropicPort,
687
+ openaiPort: fileConfig.openaiPort ?? DEFAULTS.openaiPort,
688
+ cursorPort: fileConfig.cursorPort ?? DEFAULTS.cursorPort,
689
+ cursorDirectUrl: fileConfig.cursorDirectUrl ?? DEFAULTS.cursorDirectUrl,
590
690
  logLevel: fileConfig.logLevel ?? DEFAULTS.logLevel,
591
691
  logFile: expandHome(fileConfig.logFile ?? DEFAULTS.logFile),
592
692
  pidFile: expandHome(fileConfig.pidFile ?? DEFAULTS.pidFile),
593
- configFile: filePath
693
+ configFile: filePath,
694
+ mode: coerceMode(fileConfig.mode)
594
695
  };
595
696
  }
596
697
  function saveConfig(config) {
597
698
  const dir = path6.dirname(config.configFile);
598
699
  fs6.mkdirSync(dir, { recursive: true });
599
- fs6.writeFileSync(config.configFile, JSON.stringify(config, null, 2) + "\n");
700
+ const { mode, ...rest } = config;
701
+ const serializable = { ...rest };
702
+ if (mode === "direct") {
703
+ serializable.mode = mode;
704
+ }
705
+ fs6.writeFileSync(config.configFile, JSON.stringify(serializable, null, 2) + "\n");
600
706
  }
601
707
 
602
708
  // src/proxy/pid.ts
603
709
  import fs7 from "fs";
604
710
  import path7 from "path";
711
+ import { execSync as execSync2 } from "child_process";
605
712
  function readPid(pidFile) {
606
713
  try {
607
714
  const raw = fs7.readFileSync(pidFile, "utf-8").trim();
715
+ try {
716
+ const parsed = JSON.parse(raw);
717
+ if (parsed && typeof parsed === "object" && typeof parsed.pid === "number" && !isNaN(parsed.pid)) {
718
+ const record = parsed;
719
+ if (record.startTime == null) {
720
+ return isRunning(record.pid) ? record.pid : null;
721
+ }
722
+ return isRunningWithIdentity(record.pid, record.startTime) ? record.pid : null;
723
+ }
724
+ } catch {
725
+ }
608
726
  const pid = parseInt(raw, 10);
609
727
  if (isNaN(pid)) return null;
610
728
  return isRunning(pid) ? pid : null;
@@ -620,6 +738,37 @@ function isRunning(pid) {
620
738
  return false;
621
739
  }
622
740
  }
741
+ function getStartTime(pid) {
742
+ try {
743
+ if (process.platform === "linux") {
744
+ const stat = fs7.readFileSync(`/proc/${pid}/stat`, "utf-8");
745
+ const rparen = stat.lastIndexOf(")");
746
+ if (rparen < 0) return null;
747
+ const fields = stat.slice(rparen + 2).split(" ");
748
+ return fields[19] ?? null;
749
+ }
750
+ if (process.platform === "darwin") {
751
+ const out = execSync2(`ps -p ${pid} -o lstart=`, { timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] });
752
+ const text = out.toString().trim();
753
+ return text || null;
754
+ }
755
+ return null;
756
+ } catch {
757
+ return null;
758
+ }
759
+ }
760
+ function isRunningWithIdentity(pid, expectedStartTime) {
761
+ try {
762
+ if (process.platform !== "linux" && process.platform !== "darwin") {
763
+ return isRunning(pid);
764
+ }
765
+ const current = getStartTime(pid);
766
+ if (current == null) return false;
767
+ return current === expectedStartTime;
768
+ } catch {
769
+ return false;
770
+ }
771
+ }
623
772
  function removePid(pidFile) {
624
773
  try {
625
774
  fs7.unlinkSync(pidFile);
@@ -631,12 +780,12 @@ function removePid(pidFile) {
631
780
  import fs8 from "fs";
632
781
  import path9 from "path";
633
782
  import os6 from "os";
634
- import { execSync as execSync3 } from "child_process";
783
+ import { execSync as execSync4 } from "child_process";
635
784
  import { fileURLToPath } from "url";
636
785
 
637
786
  // src/cli/service/detect-os.ts
638
787
  import os4 from "os";
639
- import { execSync as execSync2 } from "child_process";
788
+ import { execSync as execSync3 } from "child_process";
640
789
  function detectShell() {
641
790
  if (process.platform === "win32") {
642
791
  if (process.env.PSModulePath || process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
@@ -650,7 +799,7 @@ function detectShell() {
650
799
  if (shellPath.includes("bash")) return "bash";
651
800
  try {
652
801
  if (process.platform === "darwin") {
653
- const result = execSync2(`dscl . -read /Users/${os4.userInfo().username} UserShell`, {
802
+ const result = execSync3(`dscl . -read /Users/${os4.userInfo().username} UserShell`, {
654
803
  encoding: "utf-8",
655
804
  timeout: 3e3
656
805
  }).trim();
@@ -659,7 +808,7 @@ function detectShell() {
659
808
  if (shell.includes("fish")) return "fish";
660
809
  if (shell.includes("bash")) return "bash";
661
810
  } else {
662
- const result = execSync2(`getent passwd ${os4.userInfo().username}`, {
811
+ const result = execSync3(`getent passwd ${os4.userInfo().username}`, {
663
812
  encoding: "utf-8",
664
813
  timeout: 3e3
665
814
  }).trim();
@@ -720,6 +869,10 @@ function generateLaunchdPlist(config, proxyRunnerPath) {
720
869
  <dict>
721
870
  <key>SKALPEL_ANTHROPIC_PORT</key>
722
871
  <string>${config.anthropicPort}</string>
872
+ <key>SKALPEL_OPENAI_PORT</key>
873
+ <string>${config.openaiPort}</string>
874
+ <key>SKALPEL_CURSOR_PORT</key>
875
+ <string>${config.cursorPort}</string>
723
876
  </dict>
724
877
  </dict>
725
878
  </plist>`;
@@ -735,6 +888,8 @@ ExecStart=${process.execPath} ${proxyRunnerPath}
735
888
  Restart=always
736
889
  RestartSec=5
737
890
  Environment=SKALPEL_ANTHROPIC_PORT=${config.anthropicPort}
891
+ Environment=SKALPEL_OPENAI_PORT=${config.openaiPort}
892
+ Environment=SKALPEL_CURSOR_PORT=${config.cursorPort}
738
893
 
739
894
  [Install]
740
895
  WantedBy=default.target`;
@@ -771,7 +926,7 @@ function resolveProxyRunnerPath() {
771
926
  }
772
927
  }
773
928
  try {
774
- const npmRoot = execSync3("npm root -g", { encoding: "utf-8" }).trim();
929
+ const npmRoot = execSync4("npm root -g", { encoding: "utf-8" }).trim();
775
930
  const globalPath = path9.join(npmRoot, "skalpel", "dist", "cli", "proxy-runner.js");
776
931
  if (fs8.existsSync(globalPath)) return globalPath;
777
932
  } catch {
@@ -798,8 +953,8 @@ function installService(config) {
798
953
  const plist = generateLaunchdPlist(config, proxyRunnerPath);
799
954
  fs8.writeFileSync(plistPath, plist);
800
955
  try {
801
- execSync3(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
802
- execSync3(`launchctl load "${plistPath}"`, { stdio: "pipe" });
956
+ execSync4(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
957
+ execSync4(`launchctl load "${plistPath}"`, { stdio: "pipe" });
803
958
  } catch (err) {
804
959
  const msg = err instanceof Error ? err.message : String(err);
805
960
  console.warn(` Warning: Could not register launchd service: ${msg}`);
@@ -814,9 +969,9 @@ function installService(config) {
814
969
  const unit = generateSystemdUnit(config, proxyRunnerPath);
815
970
  fs8.writeFileSync(unitPath, unit);
816
971
  try {
817
- execSync3("systemctl --user daemon-reload", { stdio: "pipe" });
818
- execSync3("systemctl --user enable skalpel-proxy", { stdio: "pipe" });
819
- execSync3("systemctl --user start skalpel-proxy", { stdio: "pipe" });
972
+ execSync4("systemctl --user daemon-reload", { stdio: "pipe" });
973
+ execSync4("systemctl --user enable skalpel-proxy", { stdio: "pipe" });
974
+ execSync4("systemctl --user start skalpel-proxy", { stdio: "pipe" });
820
975
  } catch {
821
976
  try {
822
977
  const autostartDir = path9.join(os6.homedir(), ".config", "autostart");
@@ -842,7 +997,7 @@ X-GNOME-Autostart-enabled=true
842
997
  case "windows": {
843
998
  const args = generateWindowsTask(config, proxyRunnerPath);
844
999
  try {
845
- execSync3(`schtasks ${args.join(" ")}`, { stdio: "pipe" });
1000
+ execSync4(`schtasks ${args.join(" ")}`, { stdio: "pipe" });
846
1001
  } catch (err) {
847
1002
  const msg = err instanceof Error ? err.message : String(err);
848
1003
  console.warn(` Warning: Could not create scheduled task: ${msg}`);
@@ -865,7 +1020,7 @@ function isServiceInstalled() {
865
1020
  }
866
1021
  case "windows": {
867
1022
  try {
868
- execSync3("schtasks /query /tn SkalpelProxy", { stdio: "pipe" });
1023
+ execSync4("schtasks /query /tn SkalpelProxy", { stdio: "pipe" });
869
1024
  return true;
870
1025
  } catch {
871
1026
  return false;
@@ -880,21 +1035,21 @@ function stopService() {
880
1035
  const plistPath = getMacOSPlistPath();
881
1036
  if (!fs8.existsSync(plistPath)) return;
882
1037
  try {
883
- execSync3(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
1038
+ execSync4(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
884
1039
  } catch {
885
1040
  }
886
1041
  break;
887
1042
  }
888
1043
  case "linux": {
889
1044
  try {
890
- execSync3("systemctl --user stop skalpel-proxy", { stdio: "pipe" });
1045
+ execSync4("systemctl --user stop skalpel-proxy", { stdio: "pipe" });
891
1046
  } catch {
892
1047
  }
893
1048
  break;
894
1049
  }
895
1050
  case "windows": {
896
1051
  try {
897
- execSync3("schtasks /end /tn SkalpelProxy", { stdio: "pipe" });
1052
+ execSync4("schtasks /end /tn SkalpelProxy", { stdio: "pipe" });
898
1053
  } catch {
899
1054
  }
900
1055
  break;
@@ -908,21 +1063,21 @@ function startService() {
908
1063
  const plistPath = getMacOSPlistPath();
909
1064
  if (!fs8.existsSync(plistPath)) return;
910
1065
  try {
911
- execSync3(`launchctl load "${plistPath}"`, { stdio: "pipe" });
1066
+ execSync4(`launchctl load "${plistPath}"`, { stdio: "pipe" });
912
1067
  } catch {
913
1068
  }
914
1069
  break;
915
1070
  }
916
1071
  case "linux": {
917
1072
  try {
918
- execSync3("systemctl --user start skalpel-proxy", { stdio: "pipe" });
1073
+ execSync4("systemctl --user start skalpel-proxy", { stdio: "pipe" });
919
1074
  } catch {
920
1075
  }
921
1076
  break;
922
1077
  }
923
1078
  case "windows": {
924
1079
  try {
925
- execSync3("schtasks /run /tn SkalpelProxy", { stdio: "pipe" });
1080
+ execSync4("schtasks /run /tn SkalpelProxy", { stdio: "pipe" });
926
1081
  } catch {
927
1082
  }
928
1083
  break;
@@ -935,7 +1090,7 @@ function uninstallService() {
935
1090
  case "macos": {
936
1091
  const plistPath = getMacOSPlistPath();
937
1092
  try {
938
- execSync3(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
1093
+ execSync4(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
939
1094
  } catch {
940
1095
  }
941
1096
  if (fs8.existsSync(plistPath)) fs8.unlinkSync(plistPath);
@@ -943,8 +1098,8 @@ function uninstallService() {
943
1098
  }
944
1099
  case "linux": {
945
1100
  try {
946
- execSync3("systemctl --user stop skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
947
- execSync3("systemctl --user disable skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
1101
+ execSync4("systemctl --user stop skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
1102
+ execSync4("systemctl --user disable skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
948
1103
  } catch {
949
1104
  }
950
1105
  const unitPath = getLinuxUnitPath();
@@ -955,7 +1110,7 @@ function uninstallService() {
955
1110
  }
956
1111
  case "windows": {
957
1112
  try {
958
- execSync3("schtasks /delete /tn SkalpelProxy /f", { stdio: "pipe" });
1113
+ execSync4("schtasks /delete /tn SkalpelProxy /f", { stdio: "pipe" });
959
1114
  } catch {
960
1115
  }
961
1116
  break;
@@ -963,62 +1118,391 @@ function uninstallService() {
963
1118
  }
964
1119
  }
965
1120
 
966
- // src/cli/agents/configure.ts
1121
+ // src/cli/start.ts
1122
+ function print5(msg) {
1123
+ console.log(msg);
1124
+ }
1125
+ async function runStart() {
1126
+ const config = loadConfig();
1127
+ if (!config.apiKey) {
1128
+ print5(' Error: No API key configured. Run "skalpel init" or set SKALPEL_API_KEY.');
1129
+ process.exit(1);
1130
+ }
1131
+ const existingPid = readPid(config.pidFile);
1132
+ if (existingPid !== null) {
1133
+ print5(` Proxy is already running (pid=${existingPid}).`);
1134
+ return;
1135
+ }
1136
+ if (isServiceInstalled()) {
1137
+ startService();
1138
+ print5(` Skalpel proxy started via system service on ports ${config.anthropicPort}, ${config.openaiPort}, and ${config.cursorPort}`);
1139
+ return;
1140
+ }
1141
+ const dirname = path10.dirname(fileURLToPath2(import.meta.url));
1142
+ const runnerScript = path10.resolve(dirname, "proxy-runner.js");
1143
+ const child = spawn(process.execPath, [runnerScript], {
1144
+ detached: true,
1145
+ stdio: "ignore"
1146
+ });
1147
+ child.unref();
1148
+ print5(` Skalpel proxy started on ports ${config.anthropicPort}, ${config.openaiPort}, and ${config.cursorPort}`);
1149
+ }
1150
+
1151
+ // src/proxy/server.ts
1152
+ import http from "http";
1153
+
1154
+ // src/proxy/dispatcher.ts
1155
+ import { Agent } from "undici";
1156
+ var skalpelDispatcher = new Agent({
1157
+ keepAliveTimeout: 1e4,
1158
+ keepAliveMaxTimeout: 6e4,
1159
+ connections: 100,
1160
+ pipelining: 1
1161
+ });
1162
+
1163
+ // src/proxy/recovery.ts
1164
+ import { createHash } from "crypto";
1165
+ var MUTEX_MAX_ENTRIES = 1024;
1166
+ var LruMutexMap = class extends Map {
1167
+ set(key, value) {
1168
+ if (this.has(key)) {
1169
+ super.delete(key);
1170
+ } else if (this.size >= MUTEX_MAX_ENTRIES) {
1171
+ const oldest = this.keys().next().value;
1172
+ if (oldest !== void 0) super.delete(oldest);
1173
+ }
1174
+ return super.set(key, value);
1175
+ }
1176
+ };
1177
+ var refreshMutex = new LruMutexMap();
1178
+
1179
+ // src/proxy/streaming.ts
1180
+ var HOP_BY_HOP = /* @__PURE__ */ new Set([
1181
+ "connection",
1182
+ "keep-alive",
1183
+ "proxy-authenticate",
1184
+ "proxy-authorization",
1185
+ "te",
1186
+ "trailer",
1187
+ "transfer-encoding",
1188
+ "upgrade"
1189
+ ]);
1190
+ var STRIP_HEADERS = /* @__PURE__ */ new Set([
1191
+ ...HOP_BY_HOP,
1192
+ "content-encoding",
1193
+ "content-length"
1194
+ ]);
1195
+
1196
+ // src/proxy/logger.ts
967
1197
  import fs9 from "fs";
968
- import path10 from "path";
1198
+ import path11 from "path";
1199
+ var MAX_SIZE = 5 * 1024 * 1024;
1200
+
1201
+ // src/proxy/server.ts
1202
+ var proxyStartTime = 0;
1203
+ function stopProxy(config) {
1204
+ const pid = readPid(config.pidFile);
1205
+ if (pid === null) return false;
1206
+ try {
1207
+ process.kill(pid, "SIGTERM");
1208
+ } catch {
1209
+ }
1210
+ removePid(config.pidFile);
1211
+ return true;
1212
+ }
1213
+ function getProxyStatus(config) {
1214
+ const pid = readPid(config.pidFile);
1215
+ return {
1216
+ running: pid !== null,
1217
+ pid,
1218
+ uptime: proxyStartTime > 0 ? Date.now() - proxyStartTime : 0,
1219
+ anthropicPort: config.anthropicPort,
1220
+ openaiPort: config.openaiPort,
1221
+ cursorPort: config.cursorPort
1222
+ };
1223
+ }
1224
+
1225
+ // src/cli/stop.ts
1226
+ function print6(msg) {
1227
+ console.log(msg);
1228
+ }
1229
+ async function runStop() {
1230
+ const config = loadConfig();
1231
+ if (isServiceInstalled()) {
1232
+ stopService();
1233
+ }
1234
+ const stopped = stopProxy(config);
1235
+ if (stopped) {
1236
+ print6(" Skalpel proxy stopped.");
1237
+ } else {
1238
+ print6(" Proxy is not running.");
1239
+ }
1240
+ }
1241
+
1242
+ // src/cli/status.ts
1243
+ function print7(msg) {
1244
+ console.log(msg);
1245
+ }
1246
+ async function runStatus() {
1247
+ const config = loadConfig();
1248
+ const status = getProxyStatus(config);
1249
+ print7("");
1250
+ print7(" Skalpel Proxy Status");
1251
+ print7(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1252
+ print7(` Status: ${status.running ? "running" : "stopped"}`);
1253
+ if (status.pid !== null) {
1254
+ print7(` PID: ${status.pid}`);
1255
+ }
1256
+ print7(` Anthropic: port ${status.anthropicPort}`);
1257
+ print7(` OpenAI: port ${status.openaiPort}`);
1258
+ print7(` Cursor: port ${status.cursorPort}`);
1259
+ print7(` Config: ${config.configFile}`);
1260
+ print7("");
1261
+ }
1262
+
1263
+ // src/cli/logs.ts
1264
+ import fs10 from "fs";
1265
+ function print8(msg) {
1266
+ console.log(msg);
1267
+ }
1268
+ async function runLogs(options) {
1269
+ const config = loadConfig();
1270
+ const logFile = config.logFile;
1271
+ const lineCount = parseInt(options.lines ?? "50", 10);
1272
+ if (!fs10.existsSync(logFile)) {
1273
+ print8(` No log file found at ${logFile}`);
1274
+ return;
1275
+ }
1276
+ const content = fs10.readFileSync(logFile, "utf-8");
1277
+ const lines = content.trimEnd().split("\n");
1278
+ const tail = lines.slice(-lineCount);
1279
+ for (const line of tail) {
1280
+ print8(line);
1281
+ }
1282
+ if (options.follow) {
1283
+ let position = fs10.statSync(logFile).size;
1284
+ fs10.watchFile(logFile, { interval: 500 }, () => {
1285
+ try {
1286
+ const stat = fs10.statSync(logFile);
1287
+ if (stat.size > position) {
1288
+ const fd = fs10.openSync(logFile, "r");
1289
+ const buf = Buffer.alloc(stat.size - position);
1290
+ fs10.readSync(fd, buf, 0, buf.length, position);
1291
+ fs10.closeSync(fd);
1292
+ process.stdout.write(buf.toString("utf-8"));
1293
+ position = stat.size;
1294
+ }
1295
+ } catch {
1296
+ }
1297
+ });
1298
+ }
1299
+ }
1300
+
1301
+ // src/cli/agents/configure.ts
1302
+ import fs11 from "fs";
1303
+ import path12 from "path";
969
1304
  import os7 from "os";
1305
+ var CURSOR_API_BASE_URL_KEY = "openai.apiBaseUrl";
1306
+ var DIRECT_MODE_BASE_URL = "https://api.skalpel.ai";
1307
+ var CODEX_DIRECT_PROVIDER_ID = "skalpel";
970
1308
  function ensureDir(dir) {
971
- fs9.mkdirSync(dir, { recursive: true });
1309
+ fs11.mkdirSync(dir, { recursive: true });
972
1310
  }
973
1311
  function createBackup(filePath) {
974
- if (fs9.existsSync(filePath)) {
975
- fs9.copyFileSync(filePath, `${filePath}.skalpel-backup`);
1312
+ if (fs11.existsSync(filePath)) {
1313
+ fs11.copyFileSync(filePath, `${filePath}.skalpel-backup`);
976
1314
  }
977
1315
  }
978
1316
  function readJsonFile(filePath) {
979
1317
  try {
980
- return JSON.parse(fs9.readFileSync(filePath, "utf-8"));
1318
+ return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
981
1319
  } catch {
982
1320
  return null;
983
1321
  }
984
1322
  }
985
- function configureClaudeCode(agent, proxyConfig) {
986
- const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
987
- const configDir = path10.dirname(configPath);
1323
+ function configureClaudeCode(agent, proxyConfig, direct = false) {
1324
+ const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
1325
+ const configDir = path12.dirname(configPath);
988
1326
  ensureDir(configDir);
989
1327
  createBackup(configPath);
990
1328
  const config = readJsonFile(configPath) ?? {};
991
1329
  if (!config.env || typeof config.env !== "object") {
992
1330
  config.env = {};
993
1331
  }
994
- config.env.ANTHROPIC_BASE_URL = `http://localhost:${proxyConfig.anthropicPort}`;
995
- fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1332
+ const env = config.env;
1333
+ if (direct) {
1334
+ env.ANTHROPIC_BASE_URL = DIRECT_MODE_BASE_URL;
1335
+ env.ANTHROPIC_CUSTOM_HEADERS = `X-Skalpel-API-Key: ${proxyConfig.apiKey}`;
1336
+ } else {
1337
+ env.ANTHROPIC_BASE_URL = `http://localhost:${proxyConfig.anthropicPort}`;
1338
+ delete env.ANTHROPIC_CUSTOM_HEADERS;
1339
+ }
1340
+ fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
996
1341
  }
997
- function configureAgent(agent, proxyConfig) {
1342
+ function readTomlFile(filePath) {
1343
+ try {
1344
+ return fs11.readFileSync(filePath, "utf-8");
1345
+ } catch {
1346
+ return "";
1347
+ }
1348
+ }
1349
+ function setTomlKey(content, key, value) {
1350
+ const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*$`, "m");
1351
+ const line = `${key} = "${value}"`;
1352
+ if (pattern.test(content)) {
1353
+ return content.replace(pattern, line);
1354
+ }
1355
+ const sectionMatch = content.match(/^\[/m);
1356
+ if (sectionMatch && sectionMatch.index !== void 0) {
1357
+ return content.slice(0, sectionMatch.index) + line + "\n" + content.slice(sectionMatch.index);
1358
+ }
1359
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
1360
+ return content + separator + line + "\n";
1361
+ }
1362
+ function removeTomlKey(content, key) {
1363
+ const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*\\n?`, "gm");
1364
+ return content.replace(pattern, "");
1365
+ }
1366
+ function buildCodexDirectProviderBlock(apiKey) {
1367
+ return [
1368
+ `[model_providers.${CODEX_DIRECT_PROVIDER_ID}]`,
1369
+ `name = "Skalpel"`,
1370
+ `base_url = "${DIRECT_MODE_BASE_URL}"`,
1371
+ `wire_api = "responses"`,
1372
+ `http_headers = { "X-Skalpel-API-Key" = "${apiKey}" }`
1373
+ ].join("\n");
1374
+ }
1375
+ function upsertCodexDirectProvider(content, apiKey) {
1376
+ const sectionHeader = `[model_providers.${CODEX_DIRECT_PROVIDER_ID}]`;
1377
+ const block = buildCodexDirectProviderBlock(apiKey);
1378
+ const idx = content.indexOf(sectionHeader);
1379
+ if (idx === -1) {
1380
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n\n" : content.length > 0 ? "\n" : "";
1381
+ return content + separator + block + "\n";
1382
+ }
1383
+ const after = content.slice(idx + sectionHeader.length);
1384
+ const nextHeaderMatch = after.match(/\n\[[^\]]+\]/);
1385
+ const end = nextHeaderMatch && nextHeaderMatch.index !== void 0 ? idx + sectionHeader.length + nextHeaderMatch.index : content.length;
1386
+ return content.slice(0, idx) + block + content.slice(end);
1387
+ }
1388
+ function removeCodexDirectProvider(content) {
1389
+ const sectionHeader = `[model_providers.${CODEX_DIRECT_PROVIDER_ID}]`;
1390
+ const idx = content.indexOf(sectionHeader);
1391
+ if (idx === -1) return content;
1392
+ const after = content.slice(idx + sectionHeader.length);
1393
+ const nextHeaderMatch = after.match(/\n\[[^\]]+\]/);
1394
+ const end = nextHeaderMatch && nextHeaderMatch.index !== void 0 ? idx + sectionHeader.length + nextHeaderMatch.index : content.length;
1395
+ const before = content.slice(0, idx).replace(/\n+$/, "");
1396
+ const rest = content.slice(end).replace(/^\n+/, "");
1397
+ return before.length > 0 && rest.length > 0 ? before + "\n" + rest : before + rest;
1398
+ }
1399
+ function configureCodex(agent, proxyConfig, direct = false) {
1400
+ const configDir = process.platform === "win32" ? path12.join(os7.homedir(), "AppData", "Roaming", "codex") : path12.join(os7.homedir(), ".codex");
1401
+ const configPath = agent.configPath ?? path12.join(configDir, "config.toml");
1402
+ ensureDir(path12.dirname(configPath));
1403
+ createBackup(configPath);
1404
+ let content = readTomlFile(configPath);
1405
+ if (direct) {
1406
+ content = removeTomlKey(content, "openai_base_url");
1407
+ content = setTomlKey(content, "model_provider", CODEX_DIRECT_PROVIDER_ID);
1408
+ content = upsertCodexDirectProvider(content, proxyConfig.apiKey);
1409
+ } else {
1410
+ content = setTomlKey(content, "openai_base_url", `http://localhost:${proxyConfig.openaiPort}`);
1411
+ content = removeTomlKey(content, "model_provider");
1412
+ content = removeCodexDirectProvider(content);
1413
+ }
1414
+ fs11.writeFileSync(configPath, content);
1415
+ }
1416
+ function getCursorConfigDir() {
1417
+ if (process.platform === "darwin") {
1418
+ return path12.join(os7.homedir(), "Library", "Application Support", "Cursor", "User");
1419
+ } else if (process.platform === "win32") {
1420
+ return path12.join(process.env.APPDATA ?? path12.join(os7.homedir(), "AppData", "Roaming"), "Cursor", "User");
1421
+ }
1422
+ return path12.join(os7.homedir(), ".config", "Cursor", "User");
1423
+ }
1424
+ function configureCursor(agent, proxyConfig, direct = false) {
1425
+ if (direct) {
1426
+ console.warn(` [!] cursor: direct mode not supported (no custom-header injection in Cursor settings). Cursor configuration left unchanged.`);
1427
+ return;
1428
+ }
1429
+ const configDir = getCursorConfigDir();
1430
+ const configPath = agent.configPath ?? path12.join(configDir, "settings.json");
1431
+ ensureDir(path12.dirname(configPath));
1432
+ createBackup(configPath);
1433
+ const config = readJsonFile(configPath) ?? {};
1434
+ const existingUrl = config[CURSOR_API_BASE_URL_KEY];
1435
+ if (typeof existingUrl === "string" && existingUrl.length > 0) {
1436
+ proxyConfig.cursorDirectUrl = existingUrl;
1437
+ saveConfig(proxyConfig);
1438
+ }
1439
+ config[CURSOR_API_BASE_URL_KEY] = `http://localhost:${proxyConfig.cursorPort}`;
1440
+ fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1441
+ }
1442
+ function configureAgent(agent, proxyConfig, direct = false) {
998
1443
  switch (agent.name) {
999
1444
  case "claude-code":
1000
- configureClaudeCode(agent, proxyConfig);
1445
+ configureClaudeCode(agent, proxyConfig, direct);
1446
+ break;
1447
+ case "codex":
1448
+ configureCodex(agent, proxyConfig, direct);
1449
+ break;
1450
+ case "cursor":
1451
+ configureCursor(agent, proxyConfig, direct);
1001
1452
  break;
1002
1453
  }
1003
1454
  }
1004
1455
  function unconfigureClaudeCode(agent) {
1005
- const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1006
- if (!fs9.existsSync(configPath)) return;
1456
+ const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
1457
+ if (!fs11.existsSync(configPath)) return;
1007
1458
  const config = readJsonFile(configPath);
1008
1459
  if (config === null) {
1009
1460
  console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ANTHROPIC_BASE_URL manually if needed.`);
1010
1461
  return;
1011
1462
  }
1012
1463
  if (config.env && typeof config.env === "object") {
1013
- delete config.env.ANTHROPIC_BASE_URL;
1014
- if (Object.keys(config.env).length === 0) {
1464
+ const env = config.env;
1465
+ delete env.ANTHROPIC_BASE_URL;
1466
+ delete env.ANTHROPIC_CUSTOM_HEADERS;
1467
+ if (Object.keys(env).length === 0) {
1015
1468
  delete config.env;
1016
1469
  }
1017
1470
  }
1018
- fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1471
+ fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1472
+ const backupPath = `${configPath}.skalpel-backup`;
1473
+ if (fs11.existsSync(backupPath)) {
1474
+ fs11.unlinkSync(backupPath);
1475
+ }
1476
+ }
1477
+ function unconfigureCodex(agent) {
1478
+ const configDir = process.platform === "win32" ? path12.join(os7.homedir(), "AppData", "Roaming", "codex") : path12.join(os7.homedir(), ".codex");
1479
+ const configPath = agent.configPath ?? path12.join(configDir, "config.toml");
1480
+ if (fs11.existsSync(configPath)) {
1481
+ let content = readTomlFile(configPath);
1482
+ content = removeTomlKey(content, "openai_base_url");
1483
+ content = removeTomlKey(content, "model_provider");
1484
+ content = removeCodexDirectProvider(content);
1485
+ fs11.writeFileSync(configPath, content);
1486
+ }
1019
1487
  const backupPath = `${configPath}.skalpel-backup`;
1020
- if (fs9.existsSync(backupPath)) {
1021
- fs9.unlinkSync(backupPath);
1488
+ if (fs11.existsSync(backupPath)) {
1489
+ fs11.unlinkSync(backupPath);
1490
+ }
1491
+ }
1492
+ function unconfigureCursor(agent) {
1493
+ const configDir = getCursorConfigDir();
1494
+ const configPath = agent.configPath ?? path12.join(configDir, "settings.json");
1495
+ if (!fs11.existsSync(configPath)) return;
1496
+ const config = readJsonFile(configPath);
1497
+ if (config === null) {
1498
+ console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ${CURSOR_API_BASE_URL_KEY} manually if needed.`);
1499
+ return;
1500
+ }
1501
+ delete config[CURSOR_API_BASE_URL_KEY];
1502
+ fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1503
+ const backupPath = `${configPath}.skalpel-backup`;
1504
+ if (fs11.existsSync(backupPath)) {
1505
+ fs11.unlinkSync(backupPath);
1022
1506
  }
1023
1507
  }
1024
1508
  function unconfigureAgent(agent) {
@@ -1026,12 +1510,18 @@ function unconfigureAgent(agent) {
1026
1510
  case "claude-code":
1027
1511
  unconfigureClaudeCode(agent);
1028
1512
  break;
1513
+ case "codex":
1514
+ unconfigureCodex(agent);
1515
+ break;
1516
+ case "cursor":
1517
+ unconfigureCursor(agent);
1518
+ break;
1029
1519
  }
1030
1520
  }
1031
1521
 
1032
1522
  // src/cli/agents/shell.ts
1033
- import fs10 from "fs";
1034
- import path11 from "path";
1523
+ import fs12 from "fs";
1524
+ import path13 from "path";
1035
1525
  import os8 from "os";
1036
1526
  var BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
1037
1527
  var END_MARKER = "# END SKALPEL PROXY";
@@ -1040,27 +1530,28 @@ var PS_END_MARKER = "# END SKALPEL PROXY";
1040
1530
  function getUnixProfilePaths() {
1041
1531
  const home = os8.homedir();
1042
1532
  const candidates = [
1043
- path11.join(home, ".bashrc"),
1044
- path11.join(home, ".zshrc"),
1045
- path11.join(home, ".bash_profile"),
1046
- path11.join(home, ".profile")
1533
+ path13.join(home, ".bashrc"),
1534
+ path13.join(home, ".zshrc"),
1535
+ path13.join(home, ".bash_profile"),
1536
+ path13.join(home, ".profile")
1047
1537
  ];
1048
- return candidates.filter((p) => fs10.existsSync(p));
1538
+ return candidates.filter((p) => fs12.existsSync(p));
1049
1539
  }
1050
1540
  function getPowerShellProfilePath() {
1051
1541
  if (process.platform !== "win32") return null;
1052
1542
  if (process.env.PROFILE) return process.env.PROFILE;
1053
- const docsDir = path11.join(os8.homedir(), "Documents");
1054
- const psProfile = path11.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
1055
- const wpProfile = path11.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
1056
- if (fs10.existsSync(psProfile)) return psProfile;
1057
- if (fs10.existsSync(wpProfile)) return wpProfile;
1543
+ const docsDir = path13.join(os8.homedir(), "Documents");
1544
+ const psProfile = path13.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
1545
+ const wpProfile = path13.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
1546
+ if (fs12.existsSync(psProfile)) return psProfile;
1547
+ if (fs12.existsSync(wpProfile)) return wpProfile;
1058
1548
  return psProfile;
1059
1549
  }
1060
1550
  function generateUnixBlock(proxyConfig) {
1061
1551
  return [
1062
1552
  BEGIN_MARKER,
1063
1553
  `export ANTHROPIC_BASE_URL="http://localhost:${proxyConfig.anthropicPort}"`,
1554
+ `export OPENAI_BASE_URL="http://localhost:${proxyConfig.openaiPort}"`,
1064
1555
  END_MARKER
1065
1556
  ].join("\n");
1066
1557
  }
@@ -1068,18 +1559,19 @@ function generatePowerShellBlock(proxyConfig) {
1068
1559
  return [
1069
1560
  PS_BEGIN_MARKER,
1070
1561
  `$env:ANTHROPIC_BASE_URL = "http://localhost:${proxyConfig.anthropicPort}"`,
1562
+ `$env:OPENAI_BASE_URL = "http://localhost:${proxyConfig.openaiPort}"`,
1071
1563
  PS_END_MARKER
1072
1564
  ].join("\n");
1073
1565
  }
1074
1566
  function createBackup2(filePath) {
1075
1567
  const backupPath = `${filePath}.skalpel-backup`;
1076
- fs10.copyFileSync(filePath, backupPath);
1568
+ fs12.copyFileSync(filePath, backupPath);
1077
1569
  }
1078
1570
  function updateProfileFile(filePath, block, beginMarker, endMarker) {
1079
- if (fs10.existsSync(filePath)) {
1571
+ if (fs12.existsSync(filePath)) {
1080
1572
  createBackup2(filePath);
1081
1573
  }
1082
- let content = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf-8") : "";
1574
+ let content = fs12.existsSync(filePath) ? fs12.readFileSync(filePath, "utf-8") : "";
1083
1575
  const beginIdx = content.indexOf(beginMarker);
1084
1576
  const endIdx = content.indexOf(endMarker);
1085
1577
  if (beginIdx !== -1 && endIdx !== -1) {
@@ -1092,15 +1584,15 @@ function updateProfileFile(filePath, block, beginMarker, endMarker) {
1092
1584
  content = block + "\n";
1093
1585
  }
1094
1586
  }
1095
- fs10.writeFileSync(filePath, content);
1587
+ fs12.writeFileSync(filePath, content);
1096
1588
  }
1097
1589
  function configureShellEnvVars(_agents, proxyConfig) {
1098
1590
  const modified = [];
1099
1591
  if (process.platform === "win32") {
1100
1592
  const psProfile = getPowerShellProfilePath();
1101
1593
  if (psProfile) {
1102
- const dir = path11.dirname(psProfile);
1103
- fs10.mkdirSync(dir, { recursive: true });
1594
+ const dir = path13.dirname(psProfile);
1595
+ fs12.mkdirSync(dir, { recursive: true });
1104
1596
  const block = generatePowerShellBlock(proxyConfig);
1105
1597
  updateProfileFile(psProfile, block, PS_BEGIN_MARKER, PS_END_MARKER);
1106
1598
  modified.push(psProfile);
@@ -1119,284 +1611,73 @@ function removeShellEnvVars() {
1119
1611
  const restored = [];
1120
1612
  const home = os8.homedir();
1121
1613
  const allProfiles = [
1122
- path11.join(home, ".bashrc"),
1123
- path11.join(home, ".zshrc"),
1124
- path11.join(home, ".bash_profile"),
1125
- path11.join(home, ".profile")
1614
+ path13.join(home, ".bashrc"),
1615
+ path13.join(home, ".zshrc"),
1616
+ path13.join(home, ".bash_profile"),
1617
+ path13.join(home, ".profile")
1126
1618
  ];
1127
1619
  if (process.platform === "win32") {
1128
1620
  const psProfile = getPowerShellProfilePath();
1129
1621
  if (psProfile) allProfiles.push(psProfile);
1130
1622
  }
1131
1623
  for (const profilePath of allProfiles) {
1132
- if (!fs10.existsSync(profilePath)) continue;
1133
- const content = fs10.readFileSync(profilePath, "utf-8");
1624
+ if (!fs12.existsSync(profilePath)) continue;
1625
+ const content = fs12.readFileSync(profilePath, "utf-8");
1134
1626
  const beginIdx = content.indexOf(BEGIN_MARKER);
1135
1627
  const endIdx = content.indexOf(END_MARKER);
1136
1628
  if (beginIdx === -1 || endIdx === -1) continue;
1137
1629
  const before = content.slice(0, beginIdx);
1138
1630
  const after = content.slice(endIdx + END_MARKER.length);
1139
1631
  const cleaned = (before.replace(/\n+$/, "") + after.replace(/^\n+/, "\n")).trimEnd() + "\n";
1140
- fs10.writeFileSync(profilePath, cleaned);
1632
+ fs12.writeFileSync(profilePath, cleaned);
1141
1633
  const backupPath = `${profilePath}.skalpel-backup`;
1142
- if (fs10.existsSync(backupPath)) {
1143
- fs10.unlinkSync(backupPath);
1634
+ if (fs12.existsSync(backupPath)) {
1635
+ fs12.unlinkSync(backupPath);
1144
1636
  }
1145
1637
  restored.push(profilePath);
1146
1638
  }
1147
1639
  return restored;
1148
1640
  }
1641
+ function writeShellBlock(proxyConfig) {
1642
+ return configureShellEnvVars([], proxyConfig);
1643
+ }
1644
+ function removeShellBlock() {
1645
+ return removeShellEnvVars();
1646
+ }
1149
1647
 
1150
- // src/cli/start.ts
1151
- function print5(msg) {
1648
+ // src/cli/config-cmd.ts
1649
+ function print9(msg) {
1152
1650
  console.log(msg);
1153
1651
  }
1154
- async function setNormalMode(config) {
1155
- try {
1156
- const res = await fetch(`http://localhost:${config.anthropicPort}/admin/mode`, {
1157
- method: "POST",
1158
- headers: { "Content-Type": "application/json" },
1159
- body: JSON.stringify({ mode: "normal" })
1160
- });
1161
- return res.ok;
1162
- } catch {
1163
- return false;
1164
- }
1165
- }
1166
- async function runStart() {
1167
- const config = loadConfig();
1168
- if (!config.apiKey) {
1169
- print5(' Error: No API key configured. Run "skalpel init" or set SKALPEL_API_KEY.');
1652
+ async function runSetMode(mode, config) {
1653
+ if (mode !== "direct" && mode !== "proxy") {
1654
+ print9(` Invalid mode: ${mode}. Must be 'direct' or 'proxy'.`);
1170
1655
  process.exit(1);
1171
1656
  }
1172
- const existingPid = readPid(config.pidFile);
1173
- if (existingPid !== null) {
1174
- const switched = await setNormalMode(config);
1175
- if (switched) {
1176
- print5(" Skalpel optimization re-enabled.");
1177
- } else {
1178
- print5(` Proxy is already running (pid=${existingPid}).`);
1179
- }
1180
- configureAgentsForProxy(config);
1657
+ if (config.mode === mode) {
1658
+ print9(` Already in ${mode} mode. Nothing to do.`);
1181
1659
  return;
1182
1660
  }
1183
- if (isServiceInstalled()) {
1184
- startService();
1185
- print5(` Skalpel proxy started via system service on port ${config.anthropicPort}`);
1186
- configureAgentsForProxy(config);
1187
- return;
1188
- }
1189
- const dirname = path12.dirname(fileURLToPath2(import.meta.url));
1190
- const runnerScript = path12.resolve(dirname, "proxy-runner.js");
1191
- const child = spawn(process.execPath, [runnerScript], {
1192
- detached: true,
1193
- stdio: "ignore"
1194
- });
1195
- child.unref();
1196
- print5(` Skalpel proxy started on port ${config.anthropicPort}`);
1197
- configureAgentsForProxy(config);
1661
+ config.mode = mode;
1662
+ saveConfig(config);
1663
+ await applyModeSwitch(mode, config);
1664
+ print9(` Switched to ${mode} mode.`);
1198
1665
  }
1199
- function configureAgentsForProxy(config) {
1666
+ async function applyModeSwitch(mode, config) {
1667
+ const direct = mode === "direct";
1200
1668
  const agents = detectAgents();
1201
1669
  for (const agent of agents) {
1202
- if (agent.installed) {
1203
- try {
1204
- configureAgent(agent, config);
1205
- print5(` Configured ${agent.name}`);
1206
- } catch (err) {
1207
- const msg = err instanceof Error ? err.message : String(err);
1208
- print5(` Could not configure ${agent.name}: ${msg}`);
1209
- }
1210
- }
1211
- }
1212
- configureShellEnvVars(agents, config);
1213
- }
1214
-
1215
- // src/proxy/server.ts
1216
- import http from "http";
1217
-
1218
- // src/proxy/streaming.ts
1219
- var HOP_BY_HOP = /* @__PURE__ */ new Set([
1220
- "connection",
1221
- "keep-alive",
1222
- "proxy-authenticate",
1223
- "proxy-authorization",
1224
- "te",
1225
- "trailer",
1226
- "transfer-encoding",
1227
- "upgrade"
1228
- ]);
1229
- var STRIP_HEADERS = /* @__PURE__ */ new Set([
1230
- ...HOP_BY_HOP,
1231
- "content-encoding",
1232
- "content-length"
1233
- ]);
1234
-
1235
- // src/proxy/logger.ts
1236
- import fs11 from "fs";
1237
- import path13 from "path";
1238
- var MAX_SIZE = 5 * 1024 * 1024;
1239
-
1240
- // src/proxy/server.ts
1241
- var proxyStartTime = 0;
1242
- function stopProxy(config) {
1243
- const pid = readPid(config.pidFile);
1244
- if (pid === null) return false;
1245
- try {
1246
- process.kill(pid, "SIGTERM");
1247
- } catch {
1248
- }
1249
- removePid(config.pidFile);
1250
- return true;
1251
- }
1252
- function getProxyStatus(config) {
1253
- const pid = readPid(config.pidFile);
1254
- return {
1255
- running: pid !== null,
1256
- pid,
1257
- uptime: proxyStartTime > 0 ? Date.now() - proxyStartTime : 0,
1258
- anthropicPort: config.anthropicPort
1259
- };
1260
- }
1261
-
1262
- // src/cli/stop.ts
1263
- function print6(msg) {
1264
- console.log(msg);
1265
- }
1266
- async function setPassthroughMode(config) {
1267
- try {
1268
- const res = await fetch(`http://localhost:${config.anthropicPort}/admin/mode`, {
1269
- method: "POST",
1270
- headers: { "Content-Type": "application/json" },
1271
- body: JSON.stringify({ mode: "passthrough" })
1272
- });
1273
- return res.ok;
1274
- } catch {
1275
- return false;
1276
- }
1277
- }
1278
- async function runStop() {
1279
- const config = loadConfig();
1280
- const pid = readPid(config.pidFile);
1281
- if (pid === null) {
1282
- print6(" Proxy is not running.");
1283
- return;
1284
- }
1285
- const switched = await setPassthroughMode(config);
1286
- if (switched) {
1287
- print6(" Skalpel optimization paused (proxy running in passthrough mode).");
1288
- print6(" Requests now go directly to provider APIs.");
1289
- print6(' Run "skalpel start" to re-enable optimization.');
1290
- return;
1670
+ if (!agent.installed) continue;
1671
+ configureAgent(agent, config, direct);
1291
1672
  }
1292
- print6(" Could not reach proxy \u2014 performing full shutdown...");
1293
- if (isServiceInstalled()) {
1294
- stopService();
1295
- }
1296
- const stopped = stopProxy(config);
1297
- if (stopped) {
1298
- print6(" Skalpel proxy stopped.");
1673
+ if (direct) {
1674
+ uninstallService();
1675
+ removeShellBlock();
1299
1676
  } else {
1300
- print6(" Proxy is not running.");
1301
- }
1302
- const restoredProfiles = removeShellEnvVars();
1303
- if (restoredProfiles.length > 0) {
1304
- for (const p of restoredProfiles) {
1305
- print6(` Restored shell profile: ${p}`);
1306
- }
1307
- }
1308
- const agents = detectAgents();
1309
- for (const agent of agents) {
1310
- if (agent.installed) {
1311
- try {
1312
- unconfigureAgent(agent);
1313
- print6(` Restored ${agent.name} config`);
1314
- } catch (err) {
1315
- const msg = err instanceof Error ? err.message : String(err);
1316
- print6(` Could not restore ${agent.name}: ${msg}`);
1317
- }
1318
- }
1319
- }
1320
- }
1321
-
1322
- // src/cli/status.ts
1323
- function print7(msg) {
1324
- console.log(msg);
1325
- }
1326
- async function fetchProxyMode(port) {
1327
- try {
1328
- const res = await fetch(`http://localhost:${port}/health`);
1329
- if (res.ok) {
1330
- const data = await res.json();
1331
- return data.mode ?? null;
1332
- }
1333
- } catch {
1334
- }
1335
- return null;
1336
- }
1337
- async function runStatus() {
1338
- const config = loadConfig();
1339
- const status = getProxyStatus(config);
1340
- print7("");
1341
- print7(" Skalpel Proxy Status");
1342
- print7(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1343
- print7(` Status: ${status.running ? "running" : "stopped"}`);
1344
- if (status.pid !== null) {
1345
- print7(` PID: ${status.pid}`);
1346
- }
1347
- if (status.running) {
1348
- const mode = await fetchProxyMode(config.anthropicPort);
1349
- if (mode) {
1350
- print7(` Mode: ${mode}`);
1351
- }
1352
- }
1353
- print7(` Anthropic: port ${status.anthropicPort}`);
1354
- print7(` Config: ${config.configFile}`);
1355
- print7("");
1356
- }
1357
-
1358
- // src/cli/logs.ts
1359
- import fs12 from "fs";
1360
- function print8(msg) {
1361
- console.log(msg);
1362
- }
1363
- async function runLogs(options) {
1364
- const config = loadConfig();
1365
- const logFile = config.logFile;
1366
- const lineCount = parseInt(options.lines ?? "50", 10);
1367
- if (!fs12.existsSync(logFile)) {
1368
- print8(` No log file found at ${logFile}`);
1369
- return;
1370
- }
1371
- const content = fs12.readFileSync(logFile, "utf-8");
1372
- const lines = content.trimEnd().split("\n");
1373
- const tail = lines.slice(-lineCount);
1374
- for (const line of tail) {
1375
- print8(line);
1376
- }
1377
- if (options.follow) {
1378
- let position = fs12.statSync(logFile).size;
1379
- fs12.watchFile(logFile, { interval: 500 }, () => {
1380
- try {
1381
- const stat = fs12.statSync(logFile);
1382
- if (stat.size > position) {
1383
- const fd = fs12.openSync(logFile, "r");
1384
- const buf = Buffer.alloc(stat.size - position);
1385
- fs12.readSync(fd, buf, 0, buf.length, position);
1386
- fs12.closeSync(fd);
1387
- process.stdout.write(buf.toString("utf-8"));
1388
- position = stat.size;
1389
- }
1390
- } catch {
1391
- }
1392
- });
1677
+ installService(config);
1678
+ writeShellBlock(config);
1393
1679
  }
1394
1680
  }
1395
-
1396
- // src/cli/config-cmd.ts
1397
- function print9(msg) {
1398
- console.log(msg);
1399
- }
1400
1681
  async function runConfig(subcommand, args) {
1401
1682
  const config = loadConfig();
1402
1683
  if (subcommand === "path") {
@@ -1410,10 +1691,21 @@ async function runConfig(subcommand, args) {
1410
1691
  }
1411
1692
  const key = args[0];
1412
1693
  const value = args[1];
1694
+ if (key === "mode") {
1695
+ if (value !== "direct" && value !== "proxy") {
1696
+ print9(` Invalid mode: ${value}. Must be 'direct' or 'proxy'.`);
1697
+ process.exit(1);
1698
+ }
1699
+ await runSetMode(value, config);
1700
+ return;
1701
+ }
1413
1702
  const validKeys = [
1414
1703
  "apiKey",
1415
1704
  "remoteBaseUrl",
1416
1705
  "anthropicPort",
1706
+ "openaiPort",
1707
+ "cursorPort",
1708
+ "cursorDirectUrl",
1417
1709
  "logLevel",
1418
1710
  "logFile",
1419
1711
  "pidFile"
@@ -1424,7 +1716,7 @@ async function runConfig(subcommand, args) {
1424
1716
  process.exit(1);
1425
1717
  }
1426
1718
  const updated = { ...config };
1427
- if (key === "anthropicPort") {
1719
+ if (key === "anthropicPort" || key === "openaiPort" || key === "cursorPort") {
1428
1720
  const parsed = parseInt(value, 10);
1429
1721
  if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
1430
1722
  print9(` Invalid port number: ${value}`);
@@ -1590,6 +1882,8 @@ async function runWizard(options) {
1590
1882
  print11("");
1591
1883
  let agentsToConfigure = installedAgents.filter((a) => {
1592
1884
  if (options?.skipClaude && a.name === "claude-code") return false;
1885
+ if (options?.skipCodex && a.name === "codex") return false;
1886
+ if (options?.skipCursor && a.name === "cursor") return false;
1593
1887
  return true;
1594
1888
  });
1595
1889
  if (agentsToConfigure.length > 0 && !isAuto) {
@@ -1636,6 +1930,32 @@ async function runWizard(options) {
1636
1930
  print11(` [!] Proxy not responding yet. It may take a moment to start.`);
1637
1931
  print11(' Run "npx skalpel status" to check later, or "npx skalpel start" to start manually.');
1638
1932
  }
1933
+ try {
1934
+ const controller = new AbortController();
1935
+ const timeout = setTimeout(() => controller.abort(), 3e3);
1936
+ const healthUrl = `http://localhost:${proxyConfig.openaiPort}/health`;
1937
+ const res = await fetch(healthUrl, { signal: controller.signal });
1938
+ clearTimeout(timeout);
1939
+ if (res.ok) {
1940
+ print11(` [+] OpenAI proxy (port ${proxyConfig.openaiPort}): healthy`);
1941
+ } else {
1942
+ print11(` [!] OpenAI proxy (port ${proxyConfig.openaiPort}): HTTP ${res.status}`);
1943
+ }
1944
+ } catch {
1945
+ }
1946
+ try {
1947
+ const controller = new AbortController();
1948
+ const timeout = setTimeout(() => controller.abort(), 3e3);
1949
+ const healthUrl = `http://localhost:${proxyConfig.cursorPort}/health`;
1950
+ const res = await fetch(healthUrl, { signal: controller.signal });
1951
+ clearTimeout(timeout);
1952
+ if (res.ok) {
1953
+ print11(` [+] Cursor proxy (port ${proxyConfig.cursorPort}): healthy`);
1954
+ } else {
1955
+ print11(` [!] Cursor proxy (port ${proxyConfig.cursorPort}): HTTP ${res.status}`);
1956
+ }
1957
+ } catch {
1958
+ }
1639
1959
  print11("");
1640
1960
  print11(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1641
1961
  print11("");
@@ -1644,7 +1964,7 @@ async function runWizard(options) {
1644
1964
  if (agentsToConfigure.length > 0) {
1645
1965
  print11(" Configured agents: " + agentsToConfigure.map((a) => a.name).join(", "));
1646
1966
  }
1647
- print11(" Proxy port: Anthropic=" + proxyConfig.anthropicPort);
1967
+ print11(" Proxy ports: Anthropic=" + proxyConfig.anthropicPort + ", OpenAI=" + proxyConfig.openaiPort + ", Cursor=" + proxyConfig.cursorPort);
1648
1968
  print11("");
1649
1969
  print11(' Run "npx skalpel status" to check proxy status');
1650
1970
  print11(' Run "npx skalpel doctor" for a full health check');
@@ -1800,7 +2120,7 @@ function clearNpxCache() {
1800
2120
  var require3 = createRequire2(import.meta.url);
1801
2121
  var pkg2 = require3("../../package.json");
1802
2122
  var program = new Command();
1803
- program.name("skalpel").description("Skalpel AI CLI \u2014 optimize your OpenAI and Anthropic API calls").version(pkg2.version).option("--api-key <key>", "Skalpel API key for non-interactive setup").option("--auto", "Run setup in non-interactive mode").option("--skip-claude", "Skip Claude Code configuration").action((options) => runWizard(options));
2123
+ program.name("skalpel").description("Skalpel AI CLI \u2014 optimize your OpenAI and Anthropic API calls").version(pkg2.version).option("--api-key <key>", "Skalpel API key for non-interactive setup").option("--auto", "Run setup in non-interactive mode").option("--skip-claude", "Skip Claude Code configuration").option("--skip-codex", "Skip Codex configuration").option("--skip-cursor", "Skip Cursor configuration").action((options) => runWizard(options));
1804
2124
  program.command("init").description("Initialize Skalpel in your project").action(runInit);
1805
2125
  program.command("doctor").description("Check Skalpel configuration health").action(runDoctor);
1806
2126
  program.command("benchmark").description("Run performance benchmarks").action(runBenchmark);