skalpel 2.0.8 → 2.0.11
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 +356 -193
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/proxy-runner.js +48 -34
- package/dist/cli/proxy-runner.js.map +1 -1
- package/dist/index.cjs +48 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +48 -34
- package/dist/index.js.map +1 -1
- package/dist/proxy/index.cjs +48 -34
- package/dist/proxy/index.cjs.map +1 -1
- package/dist/proxy/index.js +48 -34
- package/dist/proxy/index.js.map +1 -1
- package/package.json +14 -5
package/dist/cli/index.js
CHANGED
|
@@ -578,7 +578,7 @@ async function runReplay(filePaths) {
|
|
|
578
578
|
|
|
579
579
|
// src/cli/start.ts
|
|
580
580
|
import { spawn } from "child_process";
|
|
581
|
-
import
|
|
581
|
+
import path12 from "path";
|
|
582
582
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
583
583
|
|
|
584
584
|
// src/proxy/config.ts
|
|
@@ -995,10 +995,260 @@ function uninstallService() {
|
|
|
995
995
|
}
|
|
996
996
|
}
|
|
997
997
|
|
|
998
|
+
// src/cli/agents/configure.ts
|
|
999
|
+
import fs9 from "fs";
|
|
1000
|
+
import path10 from "path";
|
|
1001
|
+
import os7 from "os";
|
|
1002
|
+
function ensureDir(dir) {
|
|
1003
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
1004
|
+
}
|
|
1005
|
+
function createBackup(filePath) {
|
|
1006
|
+
if (fs9.existsSync(filePath)) {
|
|
1007
|
+
fs9.copyFileSync(filePath, `${filePath}.skalpel-backup`);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function readJsonFile(filePath) {
|
|
1011
|
+
try {
|
|
1012
|
+
return JSON.parse(fs9.readFileSync(filePath, "utf-8"));
|
|
1013
|
+
} catch {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function configureClaudeCode(agent, proxyConfig) {
|
|
1018
|
+
const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
|
|
1019
|
+
const configDir = path10.dirname(configPath);
|
|
1020
|
+
ensureDir(configDir);
|
|
1021
|
+
createBackup(configPath);
|
|
1022
|
+
const config = readJsonFile(configPath) ?? {};
|
|
1023
|
+
if (!config.env || typeof config.env !== "object") {
|
|
1024
|
+
config.env = {};
|
|
1025
|
+
}
|
|
1026
|
+
config.env.ANTHROPIC_BASE_URL = `http://localhost:${proxyConfig.anthropicPort}`;
|
|
1027
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1028
|
+
}
|
|
1029
|
+
function readTomlFile(filePath) {
|
|
1030
|
+
try {
|
|
1031
|
+
return fs9.readFileSync(filePath, "utf-8");
|
|
1032
|
+
} catch {
|
|
1033
|
+
return "";
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function setTomlKey(content, key, value) {
|
|
1037
|
+
const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*$`, "m");
|
|
1038
|
+
const line = `${key} = "${value}"`;
|
|
1039
|
+
if (pattern.test(content)) {
|
|
1040
|
+
return content.replace(pattern, line);
|
|
1041
|
+
}
|
|
1042
|
+
const sectionMatch = content.match(/^\[/m);
|
|
1043
|
+
if (sectionMatch && sectionMatch.index !== void 0) {
|
|
1044
|
+
return content.slice(0, sectionMatch.index) + line + "\n" + content.slice(sectionMatch.index);
|
|
1045
|
+
}
|
|
1046
|
+
const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
1047
|
+
return content + separator + line + "\n";
|
|
1048
|
+
}
|
|
1049
|
+
function removeTomlKey(content, key) {
|
|
1050
|
+
const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*\\n?`, "gm");
|
|
1051
|
+
return content.replace(pattern, "");
|
|
1052
|
+
}
|
|
1053
|
+
function configureCodex(agent, proxyConfig) {
|
|
1054
|
+
const configDir = process.platform === "win32" ? path10.join(os7.homedir(), "AppData", "Roaming", "codex") : path10.join(os7.homedir(), ".codex");
|
|
1055
|
+
const configPath = agent.configPath ?? path10.join(configDir, "config.toml");
|
|
1056
|
+
ensureDir(path10.dirname(configPath));
|
|
1057
|
+
createBackup(configPath);
|
|
1058
|
+
let content = readTomlFile(configPath);
|
|
1059
|
+
content = setTomlKey(content, "openai_base_url", `http://localhost:${proxyConfig.openaiPort}`);
|
|
1060
|
+
fs9.writeFileSync(configPath, content);
|
|
1061
|
+
}
|
|
1062
|
+
function configureAgent(agent, proxyConfig) {
|
|
1063
|
+
switch (agent.name) {
|
|
1064
|
+
case "claude-code":
|
|
1065
|
+
configureClaudeCode(agent, proxyConfig);
|
|
1066
|
+
break;
|
|
1067
|
+
case "codex":
|
|
1068
|
+
configureCodex(agent, proxyConfig);
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function unconfigureClaudeCode(agent) {
|
|
1073
|
+
const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
|
|
1074
|
+
if (!fs9.existsSync(configPath)) return;
|
|
1075
|
+
const config = readJsonFile(configPath);
|
|
1076
|
+
if (config === null) {
|
|
1077
|
+
console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ANTHROPIC_BASE_URL manually if needed.`);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (config.env && typeof config.env === "object") {
|
|
1081
|
+
delete config.env.ANTHROPIC_BASE_URL;
|
|
1082
|
+
if (Object.keys(config.env).length === 0) {
|
|
1083
|
+
delete config.env;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1087
|
+
const backupPath = `${configPath}.skalpel-backup`;
|
|
1088
|
+
if (fs9.existsSync(backupPath)) {
|
|
1089
|
+
fs9.unlinkSync(backupPath);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function unconfigureCodex(agent) {
|
|
1093
|
+
const configDir = process.platform === "win32" ? path10.join(os7.homedir(), "AppData", "Roaming", "codex") : path10.join(os7.homedir(), ".codex");
|
|
1094
|
+
const configPath = agent.configPath ?? path10.join(configDir, "config.toml");
|
|
1095
|
+
if (fs9.existsSync(configPath)) {
|
|
1096
|
+
let content = readTomlFile(configPath);
|
|
1097
|
+
content = removeTomlKey(content, "openai_base_url");
|
|
1098
|
+
fs9.writeFileSync(configPath, content);
|
|
1099
|
+
}
|
|
1100
|
+
const backupPath = `${configPath}.skalpel-backup`;
|
|
1101
|
+
if (fs9.existsSync(backupPath)) {
|
|
1102
|
+
fs9.unlinkSync(backupPath);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function unconfigureAgent(agent) {
|
|
1106
|
+
switch (agent.name) {
|
|
1107
|
+
case "claude-code":
|
|
1108
|
+
unconfigureClaudeCode(agent);
|
|
1109
|
+
break;
|
|
1110
|
+
case "codex":
|
|
1111
|
+
unconfigureCodex(agent);
|
|
1112
|
+
break;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/cli/agents/shell.ts
|
|
1117
|
+
import fs10 from "fs";
|
|
1118
|
+
import path11 from "path";
|
|
1119
|
+
import os8 from "os";
|
|
1120
|
+
var BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
|
|
1121
|
+
var END_MARKER = "# END SKALPEL PROXY";
|
|
1122
|
+
var PS_BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
|
|
1123
|
+
var PS_END_MARKER = "# END SKALPEL PROXY";
|
|
1124
|
+
function getUnixProfilePaths() {
|
|
1125
|
+
const home = os8.homedir();
|
|
1126
|
+
const candidates = [
|
|
1127
|
+
path11.join(home, ".bashrc"),
|
|
1128
|
+
path11.join(home, ".zshrc"),
|
|
1129
|
+
path11.join(home, ".bash_profile"),
|
|
1130
|
+
path11.join(home, ".profile")
|
|
1131
|
+
];
|
|
1132
|
+
return candidates.filter((p) => fs10.existsSync(p));
|
|
1133
|
+
}
|
|
1134
|
+
function getPowerShellProfilePath() {
|
|
1135
|
+
if (process.platform !== "win32") return null;
|
|
1136
|
+
if (process.env.PROFILE) return process.env.PROFILE;
|
|
1137
|
+
const docsDir = path11.join(os8.homedir(), "Documents");
|
|
1138
|
+
const psProfile = path11.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
1139
|
+
const wpProfile = path11.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
1140
|
+
if (fs10.existsSync(psProfile)) return psProfile;
|
|
1141
|
+
if (fs10.existsSync(wpProfile)) return wpProfile;
|
|
1142
|
+
return psProfile;
|
|
1143
|
+
}
|
|
1144
|
+
function generateUnixBlock(proxyConfig) {
|
|
1145
|
+
return [
|
|
1146
|
+
BEGIN_MARKER,
|
|
1147
|
+
`export ANTHROPIC_BASE_URL="http://localhost:${proxyConfig.anthropicPort}"`,
|
|
1148
|
+
`export OPENAI_BASE_URL="http://localhost:${proxyConfig.openaiPort}"`,
|
|
1149
|
+
END_MARKER
|
|
1150
|
+
].join("\n");
|
|
1151
|
+
}
|
|
1152
|
+
function generatePowerShellBlock(proxyConfig) {
|
|
1153
|
+
return [
|
|
1154
|
+
PS_BEGIN_MARKER,
|
|
1155
|
+
`$env:ANTHROPIC_BASE_URL = "http://localhost:${proxyConfig.anthropicPort}"`,
|
|
1156
|
+
`$env:OPENAI_BASE_URL = "http://localhost:${proxyConfig.openaiPort}"`,
|
|
1157
|
+
PS_END_MARKER
|
|
1158
|
+
].join("\n");
|
|
1159
|
+
}
|
|
1160
|
+
function createBackup2(filePath) {
|
|
1161
|
+
const backupPath = `${filePath}.skalpel-backup`;
|
|
1162
|
+
fs10.copyFileSync(filePath, backupPath);
|
|
1163
|
+
}
|
|
1164
|
+
function updateProfileFile(filePath, block, beginMarker, endMarker) {
|
|
1165
|
+
if (fs10.existsSync(filePath)) {
|
|
1166
|
+
createBackup2(filePath);
|
|
1167
|
+
}
|
|
1168
|
+
let content = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf-8") : "";
|
|
1169
|
+
const beginIdx = content.indexOf(beginMarker);
|
|
1170
|
+
const endIdx = content.indexOf(endMarker);
|
|
1171
|
+
if (beginIdx !== -1 && endIdx !== -1) {
|
|
1172
|
+
content = content.slice(0, beginIdx) + block + content.slice(endIdx + endMarker.length);
|
|
1173
|
+
} else {
|
|
1174
|
+
if (content.length > 0) {
|
|
1175
|
+
const trimmed = content.replace(/\n+$/, "");
|
|
1176
|
+
content = trimmed + "\n\n" + block + "\n";
|
|
1177
|
+
} else {
|
|
1178
|
+
content = block + "\n";
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
fs10.writeFileSync(filePath, content);
|
|
1182
|
+
}
|
|
1183
|
+
function configureShellEnvVars(_agents, proxyConfig) {
|
|
1184
|
+
const modified = [];
|
|
1185
|
+
if (process.platform === "win32") {
|
|
1186
|
+
const psProfile = getPowerShellProfilePath();
|
|
1187
|
+
if (psProfile) {
|
|
1188
|
+
const dir = path11.dirname(psProfile);
|
|
1189
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
1190
|
+
const block = generatePowerShellBlock(proxyConfig);
|
|
1191
|
+
updateProfileFile(psProfile, block, PS_BEGIN_MARKER, PS_END_MARKER);
|
|
1192
|
+
modified.push(psProfile);
|
|
1193
|
+
}
|
|
1194
|
+
} else {
|
|
1195
|
+
const profiles = getUnixProfilePaths();
|
|
1196
|
+
const block = generateUnixBlock(proxyConfig);
|
|
1197
|
+
for (const profilePath of profiles) {
|
|
1198
|
+
updateProfileFile(profilePath, block, BEGIN_MARKER, END_MARKER);
|
|
1199
|
+
modified.push(profilePath);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return modified;
|
|
1203
|
+
}
|
|
1204
|
+
function removeShellEnvVars() {
|
|
1205
|
+
const restored = [];
|
|
1206
|
+
const home = os8.homedir();
|
|
1207
|
+
const allProfiles = [
|
|
1208
|
+
path11.join(home, ".bashrc"),
|
|
1209
|
+
path11.join(home, ".zshrc"),
|
|
1210
|
+
path11.join(home, ".bash_profile"),
|
|
1211
|
+
path11.join(home, ".profile")
|
|
1212
|
+
];
|
|
1213
|
+
if (process.platform === "win32") {
|
|
1214
|
+
const psProfile = getPowerShellProfilePath();
|
|
1215
|
+
if (psProfile) allProfiles.push(psProfile);
|
|
1216
|
+
}
|
|
1217
|
+
for (const profilePath of allProfiles) {
|
|
1218
|
+
if (!fs10.existsSync(profilePath)) continue;
|
|
1219
|
+
const content = fs10.readFileSync(profilePath, "utf-8");
|
|
1220
|
+
const beginIdx = content.indexOf(BEGIN_MARKER);
|
|
1221
|
+
const endIdx = content.indexOf(END_MARKER);
|
|
1222
|
+
if (beginIdx === -1 || endIdx === -1) continue;
|
|
1223
|
+
const before = content.slice(0, beginIdx);
|
|
1224
|
+
const after = content.slice(endIdx + END_MARKER.length);
|
|
1225
|
+
const cleaned = (before.replace(/\n+$/, "") + after.replace(/^\n+/, "\n")).trimEnd() + "\n";
|
|
1226
|
+
fs10.writeFileSync(profilePath, cleaned);
|
|
1227
|
+
const backupPath = `${profilePath}.skalpel-backup`;
|
|
1228
|
+
if (fs10.existsSync(backupPath)) {
|
|
1229
|
+
fs10.unlinkSync(backupPath);
|
|
1230
|
+
}
|
|
1231
|
+
restored.push(profilePath);
|
|
1232
|
+
}
|
|
1233
|
+
return restored;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
998
1236
|
// src/cli/start.ts
|
|
999
1237
|
function print5(msg) {
|
|
1000
1238
|
console.log(msg);
|
|
1001
1239
|
}
|
|
1240
|
+
async function setNormalMode(config) {
|
|
1241
|
+
try {
|
|
1242
|
+
const res = await fetch(`http://localhost:${config.anthropicPort}/admin/mode`, {
|
|
1243
|
+
method: "POST",
|
|
1244
|
+
headers: { "Content-Type": "application/json" },
|
|
1245
|
+
body: JSON.stringify({ mode: "normal" })
|
|
1246
|
+
});
|
|
1247
|
+
return res.ok;
|
|
1248
|
+
} catch {
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1002
1252
|
async function runStart() {
|
|
1003
1253
|
const config = loadConfig();
|
|
1004
1254
|
if (!config.apiKey) {
|
|
@@ -1007,22 +1257,45 @@ async function runStart() {
|
|
|
1007
1257
|
}
|
|
1008
1258
|
const existingPid = readPid(config.pidFile);
|
|
1009
1259
|
if (existingPid !== null) {
|
|
1010
|
-
|
|
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);
|
|
1011
1267
|
return;
|
|
1012
1268
|
}
|
|
1013
1269
|
if (isServiceInstalled()) {
|
|
1014
1270
|
startService();
|
|
1015
1271
|
print5(` Skalpel proxy started via system service on ports ${config.anthropicPort} and ${config.openaiPort}`);
|
|
1272
|
+
configureAgentsForProxy(config);
|
|
1016
1273
|
return;
|
|
1017
1274
|
}
|
|
1018
|
-
const dirname =
|
|
1019
|
-
const runnerScript =
|
|
1275
|
+
const dirname = path12.dirname(fileURLToPath2(import.meta.url));
|
|
1276
|
+
const runnerScript = path12.resolve(dirname, "proxy-runner.js");
|
|
1020
1277
|
const child = spawn(process.execPath, [runnerScript], {
|
|
1021
1278
|
detached: true,
|
|
1022
1279
|
stdio: "ignore"
|
|
1023
1280
|
});
|
|
1024
1281
|
child.unref();
|
|
1025
1282
|
print5(` Skalpel proxy started on ports ${config.anthropicPort} and ${config.openaiPort}`);
|
|
1283
|
+
configureAgentsForProxy(config);
|
|
1284
|
+
}
|
|
1285
|
+
function configureAgentsForProxy(config) {
|
|
1286
|
+
const agents = detectAgents();
|
|
1287
|
+
for (const agent of agents) {
|
|
1288
|
+
if (agent.installed) {
|
|
1289
|
+
try {
|
|
1290
|
+
configureAgent(agent, config);
|
|
1291
|
+
print5(` Configured ${agent.name}`);
|
|
1292
|
+
} catch (err) {
|
|
1293
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1294
|
+
print5(` Could not configure ${agent.name}: ${msg}`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
configureShellEnvVars(agents, config);
|
|
1026
1299
|
}
|
|
1027
1300
|
|
|
1028
1301
|
// src/proxy/server.ts
|
|
@@ -1046,8 +1319,8 @@ var STRIP_HEADERS = /* @__PURE__ */ new Set([
|
|
|
1046
1319
|
]);
|
|
1047
1320
|
|
|
1048
1321
|
// src/proxy/logger.ts
|
|
1049
|
-
import
|
|
1050
|
-
import
|
|
1322
|
+
import fs11 from "fs";
|
|
1323
|
+
import path13 from "path";
|
|
1051
1324
|
var MAX_SIZE = 5 * 1024 * 1024;
|
|
1052
1325
|
|
|
1053
1326
|
// src/proxy/server.ts
|
|
@@ -1077,8 +1350,33 @@ function getProxyStatus(config) {
|
|
|
1077
1350
|
function print6(msg) {
|
|
1078
1351
|
console.log(msg);
|
|
1079
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
|
+
}
|
|
1080
1365
|
async function runStop() {
|
|
1081
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...");
|
|
1082
1380
|
if (isServiceInstalled()) {
|
|
1083
1381
|
stopService();
|
|
1084
1382
|
}
|
|
@@ -1088,12 +1386,41 @@ async function runStop() {
|
|
|
1088
1386
|
} else {
|
|
1089
1387
|
print6(" Proxy is not running.");
|
|
1090
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
|
+
}
|
|
1091
1407
|
}
|
|
1092
1408
|
|
|
1093
1409
|
// src/cli/status.ts
|
|
1094
1410
|
function print7(msg) {
|
|
1095
1411
|
console.log(msg);
|
|
1096
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
|
+
}
|
|
1097
1424
|
async function runStatus() {
|
|
1098
1425
|
const config = loadConfig();
|
|
1099
1426
|
const status = getProxyStatus(config);
|
|
@@ -1104,6 +1431,12 @@ async function runStatus() {
|
|
|
1104
1431
|
if (status.pid !== null) {
|
|
1105
1432
|
print7(` PID: ${status.pid}`);
|
|
1106
1433
|
}
|
|
1434
|
+
if (status.running) {
|
|
1435
|
+
const mode = await fetchProxyMode(config.anthropicPort);
|
|
1436
|
+
if (mode) {
|
|
1437
|
+
print7(` Mode: ${mode}`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1107
1440
|
print7(` Anthropic: port ${status.anthropicPort}`);
|
|
1108
1441
|
print7(` OpenAI: port ${status.openaiPort}`);
|
|
1109
1442
|
print7(` Config: ${config.configFile}`);
|
|
@@ -1111,7 +1444,7 @@ async function runStatus() {
|
|
|
1111
1444
|
}
|
|
1112
1445
|
|
|
1113
1446
|
// src/cli/logs.ts
|
|
1114
|
-
import
|
|
1447
|
+
import fs12 from "fs";
|
|
1115
1448
|
function print8(msg) {
|
|
1116
1449
|
console.log(msg);
|
|
1117
1450
|
}
|
|
@@ -1119,26 +1452,26 @@ async function runLogs(options) {
|
|
|
1119
1452
|
const config = loadConfig();
|
|
1120
1453
|
const logFile = config.logFile;
|
|
1121
1454
|
const lineCount = parseInt(options.lines ?? "50", 10);
|
|
1122
|
-
if (!
|
|
1455
|
+
if (!fs12.existsSync(logFile)) {
|
|
1123
1456
|
print8(` No log file found at ${logFile}`);
|
|
1124
1457
|
return;
|
|
1125
1458
|
}
|
|
1126
|
-
const content =
|
|
1459
|
+
const content = fs12.readFileSync(logFile, "utf-8");
|
|
1127
1460
|
const lines = content.trimEnd().split("\n");
|
|
1128
1461
|
const tail = lines.slice(-lineCount);
|
|
1129
1462
|
for (const line of tail) {
|
|
1130
1463
|
print8(line);
|
|
1131
1464
|
}
|
|
1132
1465
|
if (options.follow) {
|
|
1133
|
-
let position =
|
|
1134
|
-
|
|
1466
|
+
let position = fs12.statSync(logFile).size;
|
|
1467
|
+
fs12.watchFile(logFile, { interval: 500 }, () => {
|
|
1135
1468
|
try {
|
|
1136
|
-
const stat =
|
|
1469
|
+
const stat = fs12.statSync(logFile);
|
|
1137
1470
|
if (stat.size > position) {
|
|
1138
|
-
const fd =
|
|
1471
|
+
const fd = fs12.openSync(logFile, "r");
|
|
1139
1472
|
const buf = Buffer.alloc(stat.size - position);
|
|
1140
|
-
|
|
1141
|
-
|
|
1473
|
+
fs12.readSync(fd, buf, 0, buf.length, position);
|
|
1474
|
+
fs12.closeSync(fd);
|
|
1142
1475
|
process.stdout.write(buf.toString("utf-8"));
|
|
1143
1476
|
position = stat.size;
|
|
1144
1477
|
}
|
|
@@ -1243,129 +1576,9 @@ async function runUpdate() {
|
|
|
1243
1576
|
|
|
1244
1577
|
// src/cli/wizard.ts
|
|
1245
1578
|
import * as readline2 from "readline";
|
|
1246
|
-
import * as
|
|
1247
|
-
import * as
|
|
1248
|
-
import * as
|
|
1249
|
-
|
|
1250
|
-
// src/cli/agents/configure.ts
|
|
1251
|
-
import fs11 from "fs";
|
|
1252
|
-
import path12 from "path";
|
|
1253
|
-
import os7 from "os";
|
|
1254
|
-
function ensureDir(dir) {
|
|
1255
|
-
fs11.mkdirSync(dir, { recursive: true });
|
|
1256
|
-
}
|
|
1257
|
-
function createBackup(filePath) {
|
|
1258
|
-
if (fs11.existsSync(filePath)) {
|
|
1259
|
-
fs11.copyFileSync(filePath, `${filePath}.skalpel-backup`);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
function readJsonFile(filePath) {
|
|
1263
|
-
try {
|
|
1264
|
-
return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
|
|
1265
|
-
} catch {
|
|
1266
|
-
return null;
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
function configureClaudeCode(agent, proxyConfig) {
|
|
1270
|
-
const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
|
|
1271
|
-
const configDir = path12.dirname(configPath);
|
|
1272
|
-
ensureDir(configDir);
|
|
1273
|
-
createBackup(configPath);
|
|
1274
|
-
const config = readJsonFile(configPath) ?? {};
|
|
1275
|
-
if (!config.env || typeof config.env !== "object") {
|
|
1276
|
-
config.env = {};
|
|
1277
|
-
}
|
|
1278
|
-
config.env.ANTHROPIC_BASE_URL = `http://localhost:${proxyConfig.anthropicPort}`;
|
|
1279
|
-
fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1280
|
-
}
|
|
1281
|
-
function readTomlFile(filePath) {
|
|
1282
|
-
try {
|
|
1283
|
-
return fs11.readFileSync(filePath, "utf-8");
|
|
1284
|
-
} catch {
|
|
1285
|
-
return "";
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
function setTomlKey(content, key, value) {
|
|
1289
|
-
const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*$`, "m");
|
|
1290
|
-
const line = `${key} = "${value}"`;
|
|
1291
|
-
if (pattern.test(content)) {
|
|
1292
|
-
return content.replace(pattern, line);
|
|
1293
|
-
}
|
|
1294
|
-
const sectionMatch = content.match(/^\[/m);
|
|
1295
|
-
if (sectionMatch && sectionMatch.index !== void 0) {
|
|
1296
|
-
return content.slice(0, sectionMatch.index) + line + "\n" + content.slice(sectionMatch.index);
|
|
1297
|
-
}
|
|
1298
|
-
const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
1299
|
-
return content + separator + line + "\n";
|
|
1300
|
-
}
|
|
1301
|
-
function removeTomlKey(content, key) {
|
|
1302
|
-
const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*\\n?`, "gm");
|
|
1303
|
-
return content.replace(pattern, "");
|
|
1304
|
-
}
|
|
1305
|
-
function configureCodex(agent, proxyConfig) {
|
|
1306
|
-
const configDir = process.platform === "win32" ? path12.join(os7.homedir(), "AppData", "Roaming", "codex") : path12.join(os7.homedir(), ".codex");
|
|
1307
|
-
const configPath = agent.configPath ?? path12.join(configDir, "config.toml");
|
|
1308
|
-
ensureDir(path12.dirname(configPath));
|
|
1309
|
-
createBackup(configPath);
|
|
1310
|
-
let content = readTomlFile(configPath);
|
|
1311
|
-
content = setTomlKey(content, "openai_base_url", `http://localhost:${proxyConfig.openaiPort}`);
|
|
1312
|
-
fs11.writeFileSync(configPath, content);
|
|
1313
|
-
}
|
|
1314
|
-
function configureAgent(agent, proxyConfig) {
|
|
1315
|
-
switch (agent.name) {
|
|
1316
|
-
case "claude-code":
|
|
1317
|
-
configureClaudeCode(agent, proxyConfig);
|
|
1318
|
-
break;
|
|
1319
|
-
case "codex":
|
|
1320
|
-
configureCodex(agent, proxyConfig);
|
|
1321
|
-
break;
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
function unconfigureClaudeCode(agent) {
|
|
1325
|
-
const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
|
|
1326
|
-
if (!fs11.existsSync(configPath)) return;
|
|
1327
|
-
const config = readJsonFile(configPath);
|
|
1328
|
-
if (config === null) {
|
|
1329
|
-
console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ANTHROPIC_BASE_URL manually if needed.`);
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
if (config.env && typeof config.env === "object") {
|
|
1333
|
-
delete config.env.ANTHROPIC_BASE_URL;
|
|
1334
|
-
if (Object.keys(config.env).length === 0) {
|
|
1335
|
-
delete config.env;
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1339
|
-
const backupPath = `${configPath}.skalpel-backup`;
|
|
1340
|
-
if (fs11.existsSync(backupPath)) {
|
|
1341
|
-
fs11.unlinkSync(backupPath);
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
function unconfigureCodex(agent) {
|
|
1345
|
-
const configDir = process.platform === "win32" ? path12.join(os7.homedir(), "AppData", "Roaming", "codex") : path12.join(os7.homedir(), ".codex");
|
|
1346
|
-
const configPath = agent.configPath ?? path12.join(configDir, "config.toml");
|
|
1347
|
-
if (fs11.existsSync(configPath)) {
|
|
1348
|
-
let content = readTomlFile(configPath);
|
|
1349
|
-
content = removeTomlKey(content, "openai_base_url");
|
|
1350
|
-
fs11.writeFileSync(configPath, content);
|
|
1351
|
-
}
|
|
1352
|
-
const backupPath = `${configPath}.skalpel-backup`;
|
|
1353
|
-
if (fs11.existsSync(backupPath)) {
|
|
1354
|
-
fs11.unlinkSync(backupPath);
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
function unconfigureAgent(agent) {
|
|
1358
|
-
switch (agent.name) {
|
|
1359
|
-
case "claude-code":
|
|
1360
|
-
unconfigureClaudeCode(agent);
|
|
1361
|
-
break;
|
|
1362
|
-
case "codex":
|
|
1363
|
-
unconfigureCodex(agent);
|
|
1364
|
-
break;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
// src/cli/wizard.ts
|
|
1579
|
+
import * as fs13 from "fs";
|
|
1580
|
+
import * as path14 from "path";
|
|
1581
|
+
import * as os9 from "os";
|
|
1369
1582
|
function print11(msg) {
|
|
1370
1583
|
console.log(msg);
|
|
1371
1584
|
}
|
|
@@ -1400,8 +1613,8 @@ async function runWizard(options) {
|
|
|
1400
1613
|
print11(" Welcome to Skalpel! Let's optimize your coding agent costs.");
|
|
1401
1614
|
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");
|
|
1402
1615
|
print11("");
|
|
1403
|
-
const skalpelDir =
|
|
1404
|
-
const configPath =
|
|
1616
|
+
const skalpelDir = path14.join(os9.homedir(), ".skalpel");
|
|
1617
|
+
const configPath = path14.join(skalpelDir, "config.json");
|
|
1405
1618
|
let apiKey = "";
|
|
1406
1619
|
if (isAuto && options?.apiKey) {
|
|
1407
1620
|
apiKey = options.apiKey;
|
|
@@ -1414,9 +1627,9 @@ async function runWizard(options) {
|
|
|
1414
1627
|
print11(" Error: --api-key is required when using --auto mode.");
|
|
1415
1628
|
process.exit(1);
|
|
1416
1629
|
} else {
|
|
1417
|
-
if (
|
|
1630
|
+
if (fs13.existsSync(configPath)) {
|
|
1418
1631
|
try {
|
|
1419
|
-
const existing = JSON.parse(
|
|
1632
|
+
const existing = JSON.parse(fs13.readFileSync(configPath, "utf-8"));
|
|
1420
1633
|
if (existing.apiKey && validateApiKey(existing.apiKey)) {
|
|
1421
1634
|
const masked = existing.apiKey.slice(0, 14) + "*".repeat(Math.max(0, existing.apiKey.length - 14));
|
|
1422
1635
|
const useExisting = await ask(` Found existing API key: ${masked}
|
|
@@ -1440,7 +1653,7 @@ async function runWizard(options) {
|
|
|
1440
1653
|
}
|
|
1441
1654
|
}
|
|
1442
1655
|
print11("");
|
|
1443
|
-
|
|
1656
|
+
fs13.mkdirSync(skalpelDir, { recursive: true });
|
|
1444
1657
|
const proxyConfig = loadConfig(configPath);
|
|
1445
1658
|
proxyConfig.apiKey = apiKey;
|
|
1446
1659
|
saveConfig(proxyConfig);
|
|
@@ -1552,56 +1765,6 @@ import * as readline3 from "readline";
|
|
|
1552
1765
|
import * as fs14 from "fs";
|
|
1553
1766
|
import * as path15 from "path";
|
|
1554
1767
|
import * as os10 from "os";
|
|
1555
|
-
|
|
1556
|
-
// src/cli/agents/shell.ts
|
|
1557
|
-
import fs13 from "fs";
|
|
1558
|
-
import path14 from "path";
|
|
1559
|
-
import os9 from "os";
|
|
1560
|
-
var BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
|
|
1561
|
-
var END_MARKER = "# END SKALPEL PROXY";
|
|
1562
|
-
function getPowerShellProfilePath() {
|
|
1563
|
-
if (process.platform !== "win32") return null;
|
|
1564
|
-
if (process.env.PROFILE) return process.env.PROFILE;
|
|
1565
|
-
const docsDir = path14.join(os9.homedir(), "Documents");
|
|
1566
|
-
const psProfile = path14.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
1567
|
-
const wpProfile = path14.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
1568
|
-
if (fs13.existsSync(psProfile)) return psProfile;
|
|
1569
|
-
if (fs13.existsSync(wpProfile)) return wpProfile;
|
|
1570
|
-
return psProfile;
|
|
1571
|
-
}
|
|
1572
|
-
function removeShellEnvVars() {
|
|
1573
|
-
const restored = [];
|
|
1574
|
-
const home = os9.homedir();
|
|
1575
|
-
const allProfiles = [
|
|
1576
|
-
path14.join(home, ".bashrc"),
|
|
1577
|
-
path14.join(home, ".zshrc"),
|
|
1578
|
-
path14.join(home, ".bash_profile"),
|
|
1579
|
-
path14.join(home, ".profile")
|
|
1580
|
-
];
|
|
1581
|
-
if (process.platform === "win32") {
|
|
1582
|
-
const psProfile = getPowerShellProfilePath();
|
|
1583
|
-
if (psProfile) allProfiles.push(psProfile);
|
|
1584
|
-
}
|
|
1585
|
-
for (const profilePath of allProfiles) {
|
|
1586
|
-
if (!fs13.existsSync(profilePath)) continue;
|
|
1587
|
-
const content = fs13.readFileSync(profilePath, "utf-8");
|
|
1588
|
-
const beginIdx = content.indexOf(BEGIN_MARKER);
|
|
1589
|
-
const endIdx = content.indexOf(END_MARKER);
|
|
1590
|
-
if (beginIdx === -1 || endIdx === -1) continue;
|
|
1591
|
-
const before = content.slice(0, beginIdx);
|
|
1592
|
-
const after = content.slice(endIdx + END_MARKER.length);
|
|
1593
|
-
const cleaned = (before.replace(/\n+$/, "") + after.replace(/^\n+/, "\n")).trimEnd() + "\n";
|
|
1594
|
-
fs13.writeFileSync(profilePath, cleaned);
|
|
1595
|
-
const backupPath = `${profilePath}.skalpel-backup`;
|
|
1596
|
-
if (fs13.existsSync(backupPath)) {
|
|
1597
|
-
fs13.unlinkSync(backupPath);
|
|
1598
|
-
}
|
|
1599
|
-
restored.push(profilePath);
|
|
1600
|
-
}
|
|
1601
|
-
return restored;
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
// src/cli/uninstall.ts
|
|
1605
1768
|
function print12(msg) {
|
|
1606
1769
|
console.log(msg);
|
|
1607
1770
|
}
|