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 +547 -330
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/proxy-runner.js +437 -91
- package/dist/cli/proxy-runner.js.map +1 -1
- package/dist/index.cjs +584 -137
- 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 +584 -137
- package/dist/index.js.map +1 -1
- package/dist/proxy/index.cjs +468 -93
- package/dist/proxy/index.cjs.map +1 -1
- package/dist/proxy/index.d.cts +7 -0
- package/dist/proxy/index.d.ts +7 -0
- package/dist/proxy/index.js +468 -93
- package/dist/proxy/index.js.map +1 -1
- package/package.json +6 -13
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
834
|
-
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
979
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
1309
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
1004
1310
|
}
|
|
1005
1311
|
function createBackup(filePath) {
|
|
1006
|
-
if (
|
|
1007
|
-
|
|
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(
|
|
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 ??
|
|
1019
|
-
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);
|
|
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
|
-
|
|
1027
|
-
|
|
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
|
|
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
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
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 ??
|
|
1074
|
-
if (!
|
|
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
|
-
|
|
1082
|
-
|
|
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
|
-
|
|
1471
|
+
fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1087
1472
|
const backupPath = `${configPath}.skalpel-backup`;
|
|
1088
|
-
if (
|
|
1089
|
-
|
|
1473
|
+
if (fs11.existsSync(backupPath)) {
|
|
1474
|
+
fs11.unlinkSync(backupPath);
|
|
1090
1475
|
}
|
|
1091
1476
|
}
|
|
1092
1477
|
function unconfigureCodex(agent) {
|
|
1093
|
-
const configDir = process.platform === "win32" ?
|
|
1094
|
-
const configPath = agent.configPath ??
|
|
1095
|
-
if (
|
|
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
|
-
|
|
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 (
|
|
1102
|
-
|
|
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
|
|
1118
|
-
import
|
|
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
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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) =>
|
|
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 =
|
|
1138
|
-
const psProfile =
|
|
1139
|
-
const wpProfile =
|
|
1140
|
-
if (
|
|
1141
|
-
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;
|
|
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
|
-
|
|
1568
|
+
fs12.copyFileSync(filePath, backupPath);
|
|
1163
1569
|
}
|
|
1164
1570
|
function updateProfileFile(filePath, block, beginMarker, endMarker) {
|
|
1165
|
-
if (
|
|
1571
|
+
if (fs12.existsSync(filePath)) {
|
|
1166
1572
|
createBackup2(filePath);
|
|
1167
1573
|
}
|
|
1168
|
-
let content =
|
|
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
|
-
|
|
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 =
|
|
1189
|
-
|
|
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
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
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 (!
|
|
1219
|
-
const content =
|
|
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
|
-
|
|
1632
|
+
fs12.writeFileSync(profilePath, cleaned);
|
|
1227
1633
|
const backupPath = `${profilePath}.skalpel-backup`;
|
|
1228
|
-
if (
|
|
1229
|
-
|
|
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/
|
|
1237
|
-
function
|
|
1648
|
+
// src/cli/config-cmd.ts
|
|
1649
|
+
function print9(msg) {
|
|
1238
1650
|
console.log(msg);
|
|
1239
1651
|
}
|
|
1240
|
-
async function
|
|
1241
|
-
|
|
1242
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|