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/README.md +7 -2
- package/dist/cli/index.js +634 -314
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/proxy-runner.js +460 -86
- package/dist/cli/proxy-runner.js.map +1 -1
- package/dist/index.cjs +608 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +608 -132
- package/dist/index.js.map +1 -1
- package/dist/proxy/index.cjs +492 -88
- package/dist/proxy/index.cjs.map +1 -1
- package/dist/proxy/index.d.cts +10 -0
- package/dist/proxy/index.d.ts +10 -0
- package/dist/proxy/index.js +492 -88
- package/dist/proxy/index.js.map +1 -1
- package/package.json +6 -13
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
802
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
1309
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
972
1310
|
}
|
|
973
1311
|
function createBackup(filePath) {
|
|
974
|
-
if (
|
|
975
|
-
|
|
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(
|
|
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 ??
|
|
987
|
-
const configDir =
|
|
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
|
-
|
|
995
|
-
|
|
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
|
|
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 ??
|
|
1006
|
-
if (!
|
|
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
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
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 (
|
|
1021
|
-
|
|
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
|
|
1034
|
-
import
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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) =>
|
|
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 =
|
|
1054
|
-
const psProfile =
|
|
1055
|
-
const wpProfile =
|
|
1056
|
-
if (
|
|
1057
|
-
if (
|
|
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
|
-
|
|
1568
|
+
fs12.copyFileSync(filePath, backupPath);
|
|
1077
1569
|
}
|
|
1078
1570
|
function updateProfileFile(filePath, block, beginMarker, endMarker) {
|
|
1079
|
-
if (
|
|
1571
|
+
if (fs12.existsSync(filePath)) {
|
|
1080
1572
|
createBackup2(filePath);
|
|
1081
1573
|
}
|
|
1082
|
-
let content =
|
|
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
|
-
|
|
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 =
|
|
1103
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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 (!
|
|
1133
|
-
const content =
|
|
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
|
-
|
|
1632
|
+
fs12.writeFileSync(profilePath, cleaned);
|
|
1141
1633
|
const backupPath = `${profilePath}.skalpel-backup`;
|
|
1142
|
-
if (
|
|
1143
|
-
|
|
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/
|
|
1151
|
-
function
|
|
1648
|
+
// src/cli/config-cmd.ts
|
|
1649
|
+
function print9(msg) {
|
|
1152
1650
|
console.log(msg);
|
|
1153
1651
|
}
|
|
1154
|
-
async function
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
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
|
|
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);
|