skalpel 2.0.11 → 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
@@ -277,8 +277,42 @@ function detectCodex() {
277
277
  }
278
278
  return agent;
279
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
+ }
280
314
  function detectAgents() {
281
- return [detectClaudeCode(), detectCodex()];
315
+ return [detectClaudeCode(), detectCodex(), detectCursor()];
282
316
  }
283
317
 
284
318
  // src/cli/doctor.ts
@@ -331,6 +365,14 @@ async function runDoctor() {
331
365
  } else {
332
366
  checks.push({ name: "Skalpel config", status: "warn", message: 'No ~/.skalpel/config.json \u2014 run "npx skalpel" to set up' });
333
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 });
334
376
  const baseURL = "https://api.skalpel.ai";
335
377
  try {
336
378
  const controller = new AbortController();
@@ -365,6 +407,25 @@ async function runDoctor() {
365
407
  } catch {
366
408
  checks.push({ name: "Local proxy", status: "warn", message: `Not running on port ${proxyPort}. Run "npx skalpel start" to start.` });
367
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
+ }
368
429
  const agents = detectAgents();
369
430
  for (const agent of agents) {
370
431
  if (agent.installed) {
@@ -578,7 +639,7 @@ async function runReplay(filePaths) {
578
639
 
579
640
  // src/cli/start.ts
580
641
  import { spawn } from "child_process";
581
- import path12 from "path";
642
+ import path10 from "path";
582
643
  import { fileURLToPath as fileURLToPath2 } from "url";
583
644
 
584
645
  // src/proxy/config.ts
@@ -595,13 +656,20 @@ var DEFAULTS = {
595
656
  apiKey: "",
596
657
  remoteBaseUrl: "https://api.skalpel.ai",
597
658
  anthropicDirectUrl: "https://api.anthropic.com",
659
+ openaiDirectUrl: "https://api.openai.com",
598
660
  anthropicPort: 18100,
599
661
  openaiPort: 18101,
662
+ cursorPort: 18102,
663
+ cursorDirectUrl: "https://api.openai.com",
600
664
  logLevel: "info",
601
665
  logFile: "~/.skalpel/logs/proxy.log",
602
666
  pidFile: "~/.skalpel/proxy.pid",
603
- configFile: "~/.skalpel/config.json"
667
+ configFile: "~/.skalpel/config.json",
668
+ mode: "proxy"
604
669
  };
670
+ function coerceMode(value) {
671
+ return value === "direct" ? "direct" : "proxy";
672
+ }
605
673
  function loadConfig(configPath) {
606
674
  const filePath = expandHome(configPath ?? DEFAULTS.configFile);
607
675
  let fileConfig = {};
@@ -614,26 +682,47 @@ function loadConfig(configPath) {
614
682
  apiKey: fileConfig.apiKey ?? DEFAULTS.apiKey,
615
683
  remoteBaseUrl: fileConfig.remoteBaseUrl ?? DEFAULTS.remoteBaseUrl,
616
684
  anthropicDirectUrl: fileConfig.anthropicDirectUrl ?? DEFAULTS.anthropicDirectUrl,
685
+ openaiDirectUrl: fileConfig.openaiDirectUrl ?? DEFAULTS.openaiDirectUrl,
617
686
  anthropicPort: fileConfig.anthropicPort ?? DEFAULTS.anthropicPort,
618
687
  openaiPort: fileConfig.openaiPort ?? DEFAULTS.openaiPort,
688
+ cursorPort: fileConfig.cursorPort ?? DEFAULTS.cursorPort,
689
+ cursorDirectUrl: fileConfig.cursorDirectUrl ?? DEFAULTS.cursorDirectUrl,
619
690
  logLevel: fileConfig.logLevel ?? DEFAULTS.logLevel,
620
691
  logFile: expandHome(fileConfig.logFile ?? DEFAULTS.logFile),
621
692
  pidFile: expandHome(fileConfig.pidFile ?? DEFAULTS.pidFile),
622
- configFile: filePath
693
+ configFile: filePath,
694
+ mode: coerceMode(fileConfig.mode)
623
695
  };
624
696
  }
625
697
  function saveConfig(config) {
626
698
  const dir = path6.dirname(config.configFile);
627
699
  fs6.mkdirSync(dir, { recursive: true });
628
- 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");
629
706
  }
630
707
 
631
708
  // src/proxy/pid.ts
632
709
  import fs7 from "fs";
633
710
  import path7 from "path";
711
+ import { execSync as execSync2 } from "child_process";
634
712
  function readPid(pidFile) {
635
713
  try {
636
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
+ }
637
726
  const pid = parseInt(raw, 10);
638
727
  if (isNaN(pid)) return null;
639
728
  return isRunning(pid) ? pid : null;
@@ -649,6 +738,37 @@ function isRunning(pid) {
649
738
  return false;
650
739
  }
651
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
+ }
652
772
  function removePid(pidFile) {
653
773
  try {
654
774
  fs7.unlinkSync(pidFile);
@@ -660,12 +780,12 @@ function removePid(pidFile) {
660
780
  import fs8 from "fs";
661
781
  import path9 from "path";
662
782
  import os6 from "os";
663
- import { execSync as execSync3 } from "child_process";
783
+ import { execSync as execSync4 } from "child_process";
664
784
  import { fileURLToPath } from "url";
665
785
 
666
786
  // src/cli/service/detect-os.ts
667
787
  import os4 from "os";
668
- import { execSync as execSync2 } from "child_process";
788
+ import { execSync as execSync3 } from "child_process";
669
789
  function detectShell() {
670
790
  if (process.platform === "win32") {
671
791
  if (process.env.PSModulePath || process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
@@ -679,7 +799,7 @@ function detectShell() {
679
799
  if (shellPath.includes("bash")) return "bash";
680
800
  try {
681
801
  if (process.platform === "darwin") {
682
- const result = execSync2(`dscl . -read /Users/${os4.userInfo().username} UserShell`, {
802
+ const result = execSync3(`dscl . -read /Users/${os4.userInfo().username} UserShell`, {
683
803
  encoding: "utf-8",
684
804
  timeout: 3e3
685
805
  }).trim();
@@ -688,7 +808,7 @@ function detectShell() {
688
808
  if (shell.includes("fish")) return "fish";
689
809
  if (shell.includes("bash")) return "bash";
690
810
  } else {
691
- const result = execSync2(`getent passwd ${os4.userInfo().username}`, {
811
+ const result = execSync3(`getent passwd ${os4.userInfo().username}`, {
692
812
  encoding: "utf-8",
693
813
  timeout: 3e3
694
814
  }).trim();
@@ -751,6 +871,8 @@ function generateLaunchdPlist(config, proxyRunnerPath) {
751
871
  <string>${config.anthropicPort}</string>
752
872
  <key>SKALPEL_OPENAI_PORT</key>
753
873
  <string>${config.openaiPort}</string>
874
+ <key>SKALPEL_CURSOR_PORT</key>
875
+ <string>${config.cursorPort}</string>
754
876
  </dict>
755
877
  </dict>
756
878
  </plist>`;
@@ -767,6 +889,7 @@ Restart=always
767
889
  RestartSec=5
768
890
  Environment=SKALPEL_ANTHROPIC_PORT=${config.anthropicPort}
769
891
  Environment=SKALPEL_OPENAI_PORT=${config.openaiPort}
892
+ Environment=SKALPEL_CURSOR_PORT=${config.cursorPort}
770
893
 
771
894
  [Install]
772
895
  WantedBy=default.target`;
@@ -803,7 +926,7 @@ function resolveProxyRunnerPath() {
803
926
  }
804
927
  }
805
928
  try {
806
- const npmRoot = execSync3("npm root -g", { encoding: "utf-8" }).trim();
929
+ const npmRoot = execSync4("npm root -g", { encoding: "utf-8" }).trim();
807
930
  const globalPath = path9.join(npmRoot, "skalpel", "dist", "cli", "proxy-runner.js");
808
931
  if (fs8.existsSync(globalPath)) return globalPath;
809
932
  } catch {
@@ -830,8 +953,8 @@ function installService(config) {
830
953
  const plist = generateLaunchdPlist(config, proxyRunnerPath);
831
954
  fs8.writeFileSync(plistPath, plist);
832
955
  try {
833
- execSync3(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
834
- execSync3(`launchctl load "${plistPath}"`, { stdio: "pipe" });
956
+ execSync4(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
957
+ execSync4(`launchctl load "${plistPath}"`, { stdio: "pipe" });
835
958
  } catch (err) {
836
959
  const msg = err instanceof Error ? err.message : String(err);
837
960
  console.warn(` Warning: Could not register launchd service: ${msg}`);
@@ -846,9 +969,9 @@ function installService(config) {
846
969
  const unit = generateSystemdUnit(config, proxyRunnerPath);
847
970
  fs8.writeFileSync(unitPath, unit);
848
971
  try {
849
- execSync3("systemctl --user daemon-reload", { stdio: "pipe" });
850
- execSync3("systemctl --user enable skalpel-proxy", { stdio: "pipe" });
851
- 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" });
852
975
  } catch {
853
976
  try {
854
977
  const autostartDir = path9.join(os6.homedir(), ".config", "autostart");
@@ -874,7 +997,7 @@ X-GNOME-Autostart-enabled=true
874
997
  case "windows": {
875
998
  const args = generateWindowsTask(config, proxyRunnerPath);
876
999
  try {
877
- execSync3(`schtasks ${args.join(" ")}`, { stdio: "pipe" });
1000
+ execSync4(`schtasks ${args.join(" ")}`, { stdio: "pipe" });
878
1001
  } catch (err) {
879
1002
  const msg = err instanceof Error ? err.message : String(err);
880
1003
  console.warn(` Warning: Could not create scheduled task: ${msg}`);
@@ -897,7 +1020,7 @@ function isServiceInstalled() {
897
1020
  }
898
1021
  case "windows": {
899
1022
  try {
900
- execSync3("schtasks /query /tn SkalpelProxy", { stdio: "pipe" });
1023
+ execSync4("schtasks /query /tn SkalpelProxy", { stdio: "pipe" });
901
1024
  return true;
902
1025
  } catch {
903
1026
  return false;
@@ -912,21 +1035,21 @@ function stopService() {
912
1035
  const plistPath = getMacOSPlistPath();
913
1036
  if (!fs8.existsSync(plistPath)) return;
914
1037
  try {
915
- execSync3(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
1038
+ execSync4(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
916
1039
  } catch {
917
1040
  }
918
1041
  break;
919
1042
  }
920
1043
  case "linux": {
921
1044
  try {
922
- execSync3("systemctl --user stop skalpel-proxy", { stdio: "pipe" });
1045
+ execSync4("systemctl --user stop skalpel-proxy", { stdio: "pipe" });
923
1046
  } catch {
924
1047
  }
925
1048
  break;
926
1049
  }
927
1050
  case "windows": {
928
1051
  try {
929
- execSync3("schtasks /end /tn SkalpelProxy", { stdio: "pipe" });
1052
+ execSync4("schtasks /end /tn SkalpelProxy", { stdio: "pipe" });
930
1053
  } catch {
931
1054
  }
932
1055
  break;
@@ -940,21 +1063,21 @@ function startService() {
940
1063
  const plistPath = getMacOSPlistPath();
941
1064
  if (!fs8.existsSync(plistPath)) return;
942
1065
  try {
943
- execSync3(`launchctl load "${plistPath}"`, { stdio: "pipe" });
1066
+ execSync4(`launchctl load "${plistPath}"`, { stdio: "pipe" });
944
1067
  } catch {
945
1068
  }
946
1069
  break;
947
1070
  }
948
1071
  case "linux": {
949
1072
  try {
950
- execSync3("systemctl --user start skalpel-proxy", { stdio: "pipe" });
1073
+ execSync4("systemctl --user start skalpel-proxy", { stdio: "pipe" });
951
1074
  } catch {
952
1075
  }
953
1076
  break;
954
1077
  }
955
1078
  case "windows": {
956
1079
  try {
957
- execSync3("schtasks /run /tn SkalpelProxy", { stdio: "pipe" });
1080
+ execSync4("schtasks /run /tn SkalpelProxy", { stdio: "pipe" });
958
1081
  } catch {
959
1082
  }
960
1083
  break;
@@ -967,7 +1090,7 @@ function uninstallService() {
967
1090
  case "macos": {
968
1091
  const plistPath = getMacOSPlistPath();
969
1092
  try {
970
- execSync3(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
1093
+ execSync4(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
971
1094
  } catch {
972
1095
  }
973
1096
  if (fs8.existsSync(plistPath)) fs8.unlinkSync(plistPath);
@@ -975,8 +1098,8 @@ function uninstallService() {
975
1098
  }
976
1099
  case "linux": {
977
1100
  try {
978
- execSync3("systemctl --user stop skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
979
- 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" });
980
1103
  } catch {
981
1104
  }
982
1105
  const unitPath = getLinuxUnitPath();
@@ -987,7 +1110,7 @@ function uninstallService() {
987
1110
  }
988
1111
  case "windows": {
989
1112
  try {
990
- execSync3("schtasks /delete /tn SkalpelProxy /f", { stdio: "pipe" });
1113
+ execSync4("schtasks /delete /tn SkalpelProxy /f", { stdio: "pipe" });
991
1114
  } catch {
992
1115
  }
993
1116
  break;
@@ -995,40 +1118,230 @@ function uninstallService() {
995
1118
  }
996
1119
  }
997
1120
 
998
- // 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
999
1197
  import fs9 from "fs";
1000
- 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";
1001
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";
1002
1308
  function ensureDir(dir) {
1003
- fs9.mkdirSync(dir, { recursive: true });
1309
+ fs11.mkdirSync(dir, { recursive: true });
1004
1310
  }
1005
1311
  function createBackup(filePath) {
1006
- if (fs9.existsSync(filePath)) {
1007
- fs9.copyFileSync(filePath, `${filePath}.skalpel-backup`);
1312
+ if (fs11.existsSync(filePath)) {
1313
+ fs11.copyFileSync(filePath, `${filePath}.skalpel-backup`);
1008
1314
  }
1009
1315
  }
1010
1316
  function readJsonFile(filePath) {
1011
1317
  try {
1012
- return JSON.parse(fs9.readFileSync(filePath, "utf-8"));
1318
+ return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
1013
1319
  } catch {
1014
1320
  return null;
1015
1321
  }
1016
1322
  }
1017
- function configureClaudeCode(agent, proxyConfig) {
1018
- const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1019
- 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);
1020
1326
  ensureDir(configDir);
1021
1327
  createBackup(configPath);
1022
1328
  const config = readJsonFile(configPath) ?? {};
1023
1329
  if (!config.env || typeof config.env !== "object") {
1024
1330
  config.env = {};
1025
1331
  }
1026
- config.env.ANTHROPIC_BASE_URL = `http://localhost:${proxyConfig.anthropicPort}`;
1027
- 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");
1028
1341
  }
1029
1342
  function readTomlFile(filePath) {
1030
1343
  try {
1031
- return fs9.readFileSync(filePath, "utf-8");
1344
+ return fs11.readFileSync(filePath, "utf-8");
1032
1345
  } catch {
1033
1346
  return "";
1034
1347
  }
@@ -1050,56 +1363,146 @@ function removeTomlKey(content, key) {
1050
1363
  const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*\\n?`, "gm");
1051
1364
  return content.replace(pattern, "");
1052
1365
  }
1053
- function configureCodex(agent, proxyConfig) {
1054
- const configDir = process.platform === "win32" ? path10.join(os7.homedir(), "AppData", "Roaming", "codex") : path10.join(os7.homedir(), ".codex");
1055
- const configPath = agent.configPath ?? path10.join(configDir, "config.toml");
1056
- ensureDir(path10.dirname(configPath));
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));
1057
1403
  createBackup(configPath);
1058
1404
  let content = readTomlFile(configPath);
1059
- content = setTomlKey(content, "openai_base_url", `http://localhost:${proxyConfig.openaiPort}`);
1060
- fs9.writeFileSync(configPath, content);
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");
1061
1441
  }
1062
- function configureAgent(agent, proxyConfig) {
1442
+ function configureAgent(agent, proxyConfig, direct = false) {
1063
1443
  switch (agent.name) {
1064
1444
  case "claude-code":
1065
- configureClaudeCode(agent, proxyConfig);
1445
+ configureClaudeCode(agent, proxyConfig, direct);
1066
1446
  break;
1067
1447
  case "codex":
1068
- configureCodex(agent, proxyConfig);
1448
+ configureCodex(agent, proxyConfig, direct);
1449
+ break;
1450
+ case "cursor":
1451
+ configureCursor(agent, proxyConfig, direct);
1069
1452
  break;
1070
1453
  }
1071
1454
  }
1072
1455
  function unconfigureClaudeCode(agent) {
1073
- const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1074
- if (!fs9.existsSync(configPath)) return;
1456
+ const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
1457
+ if (!fs11.existsSync(configPath)) return;
1075
1458
  const config = readJsonFile(configPath);
1076
1459
  if (config === null) {
1077
1460
  console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ANTHROPIC_BASE_URL manually if needed.`);
1078
1461
  return;
1079
1462
  }
1080
1463
  if (config.env && typeof config.env === "object") {
1081
- delete config.env.ANTHROPIC_BASE_URL;
1082
- 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) {
1083
1468
  delete config.env;
1084
1469
  }
1085
1470
  }
1086
- fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1471
+ fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1087
1472
  const backupPath = `${configPath}.skalpel-backup`;
1088
- if (fs9.existsSync(backupPath)) {
1089
- fs9.unlinkSync(backupPath);
1473
+ if (fs11.existsSync(backupPath)) {
1474
+ fs11.unlinkSync(backupPath);
1090
1475
  }
1091
1476
  }
1092
1477
  function unconfigureCodex(agent) {
1093
- const configDir = process.platform === "win32" ? path10.join(os7.homedir(), "AppData", "Roaming", "codex") : path10.join(os7.homedir(), ".codex");
1094
- const configPath = agent.configPath ?? path10.join(configDir, "config.toml");
1095
- if (fs9.existsSync(configPath)) {
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)) {
1096
1481
  let content = readTomlFile(configPath);
1097
1482
  content = removeTomlKey(content, "openai_base_url");
1098
- fs9.writeFileSync(configPath, content);
1483
+ content = removeTomlKey(content, "model_provider");
1484
+ content = removeCodexDirectProvider(content);
1485
+ fs11.writeFileSync(configPath, content);
1486
+ }
1487
+ const backupPath = `${configPath}.skalpel-backup`;
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;
1099
1500
  }
1501
+ delete config[CURSOR_API_BASE_URL_KEY];
1502
+ fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1100
1503
  const backupPath = `${configPath}.skalpel-backup`;
1101
- if (fs9.existsSync(backupPath)) {
1102
- fs9.unlinkSync(backupPath);
1504
+ if (fs11.existsSync(backupPath)) {
1505
+ fs11.unlinkSync(backupPath);
1103
1506
  }
1104
1507
  }
1105
1508
  function unconfigureAgent(agent) {
@@ -1110,12 +1513,15 @@ function unconfigureAgent(agent) {
1110
1513
  case "codex":
1111
1514
  unconfigureCodex(agent);
1112
1515
  break;
1516
+ case "cursor":
1517
+ unconfigureCursor(agent);
1518
+ break;
1113
1519
  }
1114
1520
  }
1115
1521
 
1116
1522
  // src/cli/agents/shell.ts
1117
- import fs10 from "fs";
1118
- import path11 from "path";
1523
+ import fs12 from "fs";
1524
+ import path13 from "path";
1119
1525
  import os8 from "os";
1120
1526
  var BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
1121
1527
  var END_MARKER = "# END SKALPEL PROXY";
@@ -1124,21 +1530,21 @@ var PS_END_MARKER = "# END SKALPEL PROXY";
1124
1530
  function getUnixProfilePaths() {
1125
1531
  const home = os8.homedir();
1126
1532
  const candidates = [
1127
- path11.join(home, ".bashrc"),
1128
- path11.join(home, ".zshrc"),
1129
- path11.join(home, ".bash_profile"),
1130
- 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")
1131
1537
  ];
1132
- return candidates.filter((p) => fs10.existsSync(p));
1538
+ return candidates.filter((p) => fs12.existsSync(p));
1133
1539
  }
1134
1540
  function getPowerShellProfilePath() {
1135
1541
  if (process.platform !== "win32") return null;
1136
1542
  if (process.env.PROFILE) return process.env.PROFILE;
1137
- const docsDir = path11.join(os8.homedir(), "Documents");
1138
- const psProfile = path11.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
1139
- const wpProfile = path11.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
1140
- if (fs10.existsSync(psProfile)) return psProfile;
1141
- 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;
1142
1548
  return psProfile;
1143
1549
  }
1144
1550
  function generateUnixBlock(proxyConfig) {
@@ -1159,13 +1565,13 @@ function generatePowerShellBlock(proxyConfig) {
1159
1565
  }
1160
1566
  function createBackup2(filePath) {
1161
1567
  const backupPath = `${filePath}.skalpel-backup`;
1162
- fs10.copyFileSync(filePath, backupPath);
1568
+ fs12.copyFileSync(filePath, backupPath);
1163
1569
  }
1164
1570
  function updateProfileFile(filePath, block, beginMarker, endMarker) {
1165
- if (fs10.existsSync(filePath)) {
1571
+ if (fs12.existsSync(filePath)) {
1166
1572
  createBackup2(filePath);
1167
1573
  }
1168
- let content = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf-8") : "";
1574
+ let content = fs12.existsSync(filePath) ? fs12.readFileSync(filePath, "utf-8") : "";
1169
1575
  const beginIdx = content.indexOf(beginMarker);
1170
1576
  const endIdx = content.indexOf(endMarker);
1171
1577
  if (beginIdx !== -1 && endIdx !== -1) {
@@ -1178,15 +1584,15 @@ function updateProfileFile(filePath, block, beginMarker, endMarker) {
1178
1584
  content = block + "\n";
1179
1585
  }
1180
1586
  }
1181
- fs10.writeFileSync(filePath, content);
1587
+ fs12.writeFileSync(filePath, content);
1182
1588
  }
1183
1589
  function configureShellEnvVars(_agents, proxyConfig) {
1184
1590
  const modified = [];
1185
1591
  if (process.platform === "win32") {
1186
1592
  const psProfile = getPowerShellProfilePath();
1187
1593
  if (psProfile) {
1188
- const dir = path11.dirname(psProfile);
1189
- fs10.mkdirSync(dir, { recursive: true });
1594
+ const dir = path13.dirname(psProfile);
1595
+ fs12.mkdirSync(dir, { recursive: true });
1190
1596
  const block = generatePowerShellBlock(proxyConfig);
1191
1597
  updateProfileFile(psProfile, block, PS_BEGIN_MARKER, PS_END_MARKER);
1192
1598
  modified.push(psProfile);
@@ -1205,285 +1611,72 @@ function removeShellEnvVars() {
1205
1611
  const restored = [];
1206
1612
  const home = os8.homedir();
1207
1613
  const allProfiles = [
1208
- path11.join(home, ".bashrc"),
1209
- path11.join(home, ".zshrc"),
1210
- path11.join(home, ".bash_profile"),
1211
- 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")
1212
1618
  ];
1213
1619
  if (process.platform === "win32") {
1214
1620
  const psProfile = getPowerShellProfilePath();
1215
1621
  if (psProfile) allProfiles.push(psProfile);
1216
1622
  }
1217
1623
  for (const profilePath of allProfiles) {
1218
- if (!fs10.existsSync(profilePath)) continue;
1219
- const content = fs10.readFileSync(profilePath, "utf-8");
1624
+ if (!fs12.existsSync(profilePath)) continue;
1625
+ const content = fs12.readFileSync(profilePath, "utf-8");
1220
1626
  const beginIdx = content.indexOf(BEGIN_MARKER);
1221
1627
  const endIdx = content.indexOf(END_MARKER);
1222
1628
  if (beginIdx === -1 || endIdx === -1) continue;
1223
1629
  const before = content.slice(0, beginIdx);
1224
1630
  const after = content.slice(endIdx + END_MARKER.length);
1225
1631
  const cleaned = (before.replace(/\n+$/, "") + after.replace(/^\n+/, "\n")).trimEnd() + "\n";
1226
- fs10.writeFileSync(profilePath, cleaned);
1632
+ fs12.writeFileSync(profilePath, cleaned);
1227
1633
  const backupPath = `${profilePath}.skalpel-backup`;
1228
- if (fs10.existsSync(backupPath)) {
1229
- fs10.unlinkSync(backupPath);
1634
+ if (fs12.existsSync(backupPath)) {
1635
+ fs12.unlinkSync(backupPath);
1230
1636
  }
1231
1637
  restored.push(profilePath);
1232
1638
  }
1233
1639
  return restored;
1234
1640
  }
1641
+ function writeShellBlock(proxyConfig) {
1642
+ return configureShellEnvVars([], proxyConfig);
1643
+ }
1644
+ function removeShellBlock() {
1645
+ return removeShellEnvVars();
1646
+ }
1235
1647
 
1236
- // src/cli/start.ts
1237
- function print5(msg) {
1648
+ // src/cli/config-cmd.ts
1649
+ function print9(msg) {
1238
1650
  console.log(msg);
1239
1651
  }
1240
- async function setNormalMode(config) {
1241
- try {
1242
- const res = await fetch(`http://localhost:${config.anthropicPort}/admin/mode`, {
1243
- method: "POST",
1244
- headers: { "Content-Type": "application/json" },
1245
- body: JSON.stringify({ mode: "normal" })
1246
- });
1247
- return res.ok;
1248
- } catch {
1249
- return false;
1250
- }
1251
- }
1252
- async function runStart() {
1253
- const config = loadConfig();
1254
- if (!config.apiKey) {
1255
- 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'.`);
1256
1655
  process.exit(1);
1257
1656
  }
1258
- const existingPid = readPid(config.pidFile);
1259
- if (existingPid !== null) {
1260
- const switched = await setNormalMode(config);
1261
- if (switched) {
1262
- print5(" Skalpel optimization re-enabled.");
1263
- } else {
1264
- print5(` Proxy is already running (pid=${existingPid}).`);
1265
- }
1266
- configureAgentsForProxy(config);
1267
- return;
1268
- }
1269
- if (isServiceInstalled()) {
1270
- startService();
1271
- print5(` Skalpel proxy started via system service on ports ${config.anthropicPort} and ${config.openaiPort}`);
1272
- configureAgentsForProxy(config);
1657
+ if (config.mode === mode) {
1658
+ print9(` Already in ${mode} mode. Nothing to do.`);
1273
1659
  return;
1274
1660
  }
1275
- const dirname = path12.dirname(fileURLToPath2(import.meta.url));
1276
- const runnerScript = path12.resolve(dirname, "proxy-runner.js");
1277
- const child = spawn(process.execPath, [runnerScript], {
1278
- detached: true,
1279
- stdio: "ignore"
1280
- });
1281
- child.unref();
1282
- print5(` Skalpel proxy started on ports ${config.anthropicPort} and ${config.openaiPort}`);
1283
- configureAgentsForProxy(config);
1661
+ config.mode = mode;
1662
+ saveConfig(config);
1663
+ await applyModeSwitch(mode, config);
1664
+ print9(` Switched to ${mode} mode.`);
1284
1665
  }
1285
- function configureAgentsForProxy(config) {
1666
+ async function applyModeSwitch(mode, config) {
1667
+ const direct = mode === "direct";
1286
1668
  const agents = detectAgents();
1287
1669
  for (const agent of agents) {
1288
- if (agent.installed) {
1289
- try {
1290
- configureAgent(agent, config);
1291
- print5(` Configured ${agent.name}`);
1292
- } catch (err) {
1293
- const msg = err instanceof Error ? err.message : String(err);
1294
- print5(` Could not configure ${agent.name}: ${msg}`);
1295
- }
1296
- }
1670
+ if (!agent.installed) continue;
1671
+ configureAgent(agent, config, direct);
1297
1672
  }
1298
- configureShellEnvVars(agents, config);
1299
- }
1300
-
1301
- // src/proxy/server.ts
1302
- import http from "http";
1303
-
1304
- // src/proxy/streaming.ts
1305
- var HOP_BY_HOP = /* @__PURE__ */ new Set([
1306
- "connection",
1307
- "keep-alive",
1308
- "proxy-authenticate",
1309
- "proxy-authorization",
1310
- "te",
1311
- "trailer",
1312
- "transfer-encoding",
1313
- "upgrade"
1314
- ]);
1315
- var STRIP_HEADERS = /* @__PURE__ */ new Set([
1316
- ...HOP_BY_HOP,
1317
- "content-encoding",
1318
- "content-length"
1319
- ]);
1320
-
1321
- // src/proxy/logger.ts
1322
- import fs11 from "fs";
1323
- import path13 from "path";
1324
- var MAX_SIZE = 5 * 1024 * 1024;
1325
-
1326
- // src/proxy/server.ts
1327
- var proxyStartTime = 0;
1328
- function stopProxy(config) {
1329
- const pid = readPid(config.pidFile);
1330
- if (pid === null) return false;
1331
- try {
1332
- process.kill(pid, "SIGTERM");
1333
- } catch {
1334
- }
1335
- removePid(config.pidFile);
1336
- return true;
1337
- }
1338
- function getProxyStatus(config) {
1339
- const pid = readPid(config.pidFile);
1340
- return {
1341
- running: pid !== null,
1342
- pid,
1343
- uptime: proxyStartTime > 0 ? Date.now() - proxyStartTime : 0,
1344
- anthropicPort: config.anthropicPort,
1345
- openaiPort: config.openaiPort
1346
- };
1347
- }
1348
-
1349
- // src/cli/stop.ts
1350
- function print6(msg) {
1351
- console.log(msg);
1352
- }
1353
- async function setPassthroughMode(config) {
1354
- try {
1355
- const res = await fetch(`http://localhost:${config.anthropicPort}/admin/mode`, {
1356
- method: "POST",
1357
- headers: { "Content-Type": "application/json" },
1358
- body: JSON.stringify({ mode: "passthrough" })
1359
- });
1360
- return res.ok;
1361
- } catch {
1362
- return false;
1363
- }
1364
- }
1365
- async function runStop() {
1366
- const config = loadConfig();
1367
- const pid = readPid(config.pidFile);
1368
- if (pid === null) {
1369
- print6(" Proxy is not running.");
1370
- return;
1371
- }
1372
- const switched = await setPassthroughMode(config);
1373
- if (switched) {
1374
- print6(" Skalpel optimization paused (proxy running in passthrough mode).");
1375
- print6(" Requests now go directly to provider APIs.");
1376
- print6(' Run "skalpel start" to re-enable optimization.');
1377
- return;
1378
- }
1379
- print6(" Could not reach proxy \u2014 performing full shutdown...");
1380
- if (isServiceInstalled()) {
1381
- stopService();
1382
- }
1383
- const stopped = stopProxy(config);
1384
- if (stopped) {
1385
- print6(" Skalpel proxy stopped.");
1673
+ if (direct) {
1674
+ uninstallService();
1675
+ removeShellBlock();
1386
1676
  } else {
1387
- print6(" Proxy is not running.");
1388
- }
1389
- const restoredProfiles = removeShellEnvVars();
1390
- if (restoredProfiles.length > 0) {
1391
- for (const p of restoredProfiles) {
1392
- print6(` Restored shell profile: ${p}`);
1393
- }
1394
- }
1395
- const agents = detectAgents();
1396
- for (const agent of agents) {
1397
- if (agent.installed) {
1398
- try {
1399
- unconfigureAgent(agent);
1400
- print6(` Restored ${agent.name} config`);
1401
- } catch (err) {
1402
- const msg = err instanceof Error ? err.message : String(err);
1403
- print6(` Could not restore ${agent.name}: ${msg}`);
1404
- }
1405
- }
1406
- }
1407
- }
1408
-
1409
- // src/cli/status.ts
1410
- function print7(msg) {
1411
- console.log(msg);
1412
- }
1413
- async function fetchProxyMode(port) {
1414
- try {
1415
- const res = await fetch(`http://localhost:${port}/health`);
1416
- if (res.ok) {
1417
- const data = await res.json();
1418
- return data.mode ?? null;
1419
- }
1420
- } catch {
1421
- }
1422
- return null;
1423
- }
1424
- async function runStatus() {
1425
- const config = loadConfig();
1426
- const status = getProxyStatus(config);
1427
- print7("");
1428
- print7(" Skalpel Proxy Status");
1429
- print7(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1430
- print7(` Status: ${status.running ? "running" : "stopped"}`);
1431
- if (status.pid !== null) {
1432
- print7(` PID: ${status.pid}`);
1677
+ installService(config);
1678
+ writeShellBlock(config);
1433
1679
  }
1434
- if (status.running) {
1435
- const mode = await fetchProxyMode(config.anthropicPort);
1436
- if (mode) {
1437
- print7(` Mode: ${mode}`);
1438
- }
1439
- }
1440
- print7(` Anthropic: port ${status.anthropicPort}`);
1441
- print7(` OpenAI: port ${status.openaiPort}`);
1442
- print7(` Config: ${config.configFile}`);
1443
- print7("");
1444
- }
1445
-
1446
- // src/cli/logs.ts
1447
- import fs12 from "fs";
1448
- function print8(msg) {
1449
- console.log(msg);
1450
- }
1451
- async function runLogs(options) {
1452
- const config = loadConfig();
1453
- const logFile = config.logFile;
1454
- const lineCount = parseInt(options.lines ?? "50", 10);
1455
- if (!fs12.existsSync(logFile)) {
1456
- print8(` No log file found at ${logFile}`);
1457
- return;
1458
- }
1459
- const content = fs12.readFileSync(logFile, "utf-8");
1460
- const lines = content.trimEnd().split("\n");
1461
- const tail = lines.slice(-lineCount);
1462
- for (const line of tail) {
1463
- print8(line);
1464
- }
1465
- if (options.follow) {
1466
- let position = fs12.statSync(logFile).size;
1467
- fs12.watchFile(logFile, { interval: 500 }, () => {
1468
- try {
1469
- const stat = fs12.statSync(logFile);
1470
- if (stat.size > position) {
1471
- const fd = fs12.openSync(logFile, "r");
1472
- const buf = Buffer.alloc(stat.size - position);
1473
- fs12.readSync(fd, buf, 0, buf.length, position);
1474
- fs12.closeSync(fd);
1475
- process.stdout.write(buf.toString("utf-8"));
1476
- position = stat.size;
1477
- }
1478
- } catch {
1479
- }
1480
- });
1481
- }
1482
- }
1483
-
1484
- // src/cli/config-cmd.ts
1485
- function print9(msg) {
1486
- console.log(msg);
1487
1680
  }
1488
1681
  async function runConfig(subcommand, args) {
1489
1682
  const config = loadConfig();
@@ -1498,11 +1691,21 @@ async function runConfig(subcommand, args) {
1498
1691
  }
1499
1692
  const key = args[0];
1500
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
+ }
1501
1702
  const validKeys = [
1502
1703
  "apiKey",
1503
1704
  "remoteBaseUrl",
1504
1705
  "anthropicPort",
1505
1706
  "openaiPort",
1707
+ "cursorPort",
1708
+ "cursorDirectUrl",
1506
1709
  "logLevel",
1507
1710
  "logFile",
1508
1711
  "pidFile"
@@ -1513,7 +1716,7 @@ async function runConfig(subcommand, args) {
1513
1716
  process.exit(1);
1514
1717
  }
1515
1718
  const updated = { ...config };
1516
- if (key === "anthropicPort" || key === "openaiPort") {
1719
+ if (key === "anthropicPort" || key === "openaiPort" || key === "cursorPort") {
1517
1720
  const parsed = parseInt(value, 10);
1518
1721
  if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
1519
1722
  print9(` Invalid port number: ${value}`);
@@ -1680,6 +1883,7 @@ async function runWizard(options) {
1680
1883
  let agentsToConfigure = installedAgents.filter((a) => {
1681
1884
  if (options?.skipClaude && a.name === "claude-code") return false;
1682
1885
  if (options?.skipCodex && a.name === "codex") return false;
1886
+ if (options?.skipCursor && a.name === "cursor") return false;
1683
1887
  return true;
1684
1888
  });
1685
1889
  if (agentsToConfigure.length > 0 && !isAuto) {
@@ -1739,6 +1943,19 @@ async function runWizard(options) {
1739
1943
  }
1740
1944
  } catch {
1741
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
+ }
1742
1959
  print11("");
1743
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");
1744
1961
  print11("");
@@ -1747,7 +1964,7 @@ async function runWizard(options) {
1747
1964
  if (agentsToConfigure.length > 0) {
1748
1965
  print11(" Configured agents: " + agentsToConfigure.map((a) => a.name).join(", "));
1749
1966
  }
1750
- print11(" Proxy ports: Anthropic=" + proxyConfig.anthropicPort + ", OpenAI=" + proxyConfig.openaiPort);
1967
+ print11(" Proxy ports: Anthropic=" + proxyConfig.anthropicPort + ", OpenAI=" + proxyConfig.openaiPort + ", Cursor=" + proxyConfig.cursorPort);
1751
1968
  print11("");
1752
1969
  print11(' Run "npx skalpel status" to check proxy status');
1753
1970
  print11(' Run "npx skalpel doctor" for a full health check');
@@ -1903,7 +2120,7 @@ function clearNpxCache() {
1903
2120
  var require3 = createRequire2(import.meta.url);
1904
2121
  var pkg2 = require3("../../package.json");
1905
2122
  var program = new Command();
1906
- 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").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));
1907
2124
  program.command("init").description("Initialize Skalpel in your project").action(runInit);
1908
2125
  program.command("doctor").description("Check Skalpel configuration health").action(runDoctor);
1909
2126
  program.command("benchmark").description("Run performance benchmarks").action(runBenchmark);