skalpel 2.0.13 → 2.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,4 +1,109 @@
1
1
  #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+
7
+ // src/proxy/dispatcher.ts
8
+ import { Agent } from "undici";
9
+ var skalpelDispatcher;
10
+ var init_dispatcher = __esm({
11
+ "src/proxy/dispatcher.ts"() {
12
+ "use strict";
13
+ skalpelDispatcher = new Agent({
14
+ keepAliveTimeout: 1e4,
15
+ keepAliveMaxTimeout: 6e4,
16
+ connections: 100,
17
+ pipelining: 1
18
+ });
19
+ }
20
+ });
21
+
22
+ // src/proxy/envelope.ts
23
+ var init_envelope = __esm({
24
+ "src/proxy/envelope.ts"() {
25
+ "use strict";
26
+ }
27
+ });
28
+
29
+ // src/proxy/recovery.ts
30
+ import { createHash } from "crypto";
31
+ var MUTEX_MAX_ENTRIES, LruMutexMap, refreshMutex;
32
+ var init_recovery = __esm({
33
+ "src/proxy/recovery.ts"() {
34
+ "use strict";
35
+ MUTEX_MAX_ENTRIES = 1024;
36
+ LruMutexMap = class extends Map {
37
+ set(key, value) {
38
+ if (this.has(key)) {
39
+ super.delete(key);
40
+ } else if (this.size >= MUTEX_MAX_ENTRIES) {
41
+ const oldest = this.keys().next().value;
42
+ if (oldest !== void 0) super.delete(oldest);
43
+ }
44
+ return super.set(key, value);
45
+ }
46
+ };
47
+ refreshMutex = new LruMutexMap();
48
+ }
49
+ });
50
+
51
+ // src/proxy/fetch-error.ts
52
+ var init_fetch_error = __esm({
53
+ "src/proxy/fetch-error.ts"() {
54
+ "use strict";
55
+ }
56
+ });
57
+
58
+ // src/proxy/streaming.ts
59
+ var HOP_BY_HOP, STRIP_HEADERS;
60
+ var init_streaming = __esm({
61
+ "src/proxy/streaming.ts"() {
62
+ "use strict";
63
+ init_dispatcher();
64
+ init_handler();
65
+ init_envelope();
66
+ init_recovery();
67
+ init_fetch_error();
68
+ HOP_BY_HOP = /* @__PURE__ */ new Set([
69
+ "connection",
70
+ "keep-alive",
71
+ "proxy-authenticate",
72
+ "proxy-authorization",
73
+ "te",
74
+ "trailer",
75
+ "transfer-encoding",
76
+ "upgrade"
77
+ ]);
78
+ STRIP_HEADERS = /* @__PURE__ */ new Set([
79
+ ...HOP_BY_HOP,
80
+ "content-encoding",
81
+ "content-length"
82
+ ]);
83
+ }
84
+ });
85
+
86
+ // src/proxy/ws-client.ts
87
+ import { EventEmitter } from "events";
88
+ import WebSocket2 from "ws";
89
+ var init_ws_client = __esm({
90
+ "src/proxy/ws-client.ts"() {
91
+ "use strict";
92
+ }
93
+ });
94
+
95
+ // src/proxy/handler.ts
96
+ var init_handler = __esm({
97
+ "src/proxy/handler.ts"() {
98
+ "use strict";
99
+ init_streaming();
100
+ init_dispatcher();
101
+ init_envelope();
102
+ init_ws_client();
103
+ init_recovery();
104
+ init_fetch_error();
105
+ }
106
+ });
2
107
 
3
108
  // src/cli/index.ts
4
109
  import { Command } from "commander";
@@ -207,6 +312,8 @@ ${envContent}`);
207
312
  import * as fs4 from "fs";
208
313
  import * as path4 from "path";
209
314
  import * as os2 from "os";
315
+ import net from "net";
316
+ import WebSocket from "ws";
210
317
 
211
318
  // src/cli/agents/detect.ts
212
319
  import { execSync } from "child_process";
@@ -319,6 +426,133 @@ function detectAgents() {
319
426
  function print2(msg) {
320
427
  console.log(msg);
321
428
  }
429
+ function codexConfigPath() {
430
+ return process.platform === "win32" ? path4.join(os2.homedir(), "AppData", "Roaming", "codex", "config.toml") : path4.join(os2.homedir(), ".codex", "config.toml");
431
+ }
432
+ function checkCodexConfig(config) {
433
+ const cfgPath = codexConfigPath();
434
+ if (!fs4.existsSync(cfgPath)) {
435
+ return {
436
+ name: "Codex config",
437
+ status: "warn",
438
+ message: `${cfgPath} not found \u2014 run "npx skalpel" to configure Codex`
439
+ };
440
+ }
441
+ let content = "";
442
+ try {
443
+ content = fs4.readFileSync(cfgPath, "utf-8");
444
+ } catch {
445
+ return {
446
+ name: "Codex config",
447
+ status: "warn",
448
+ message: `cannot read ${cfgPath}`
449
+ };
450
+ }
451
+ const requiredLines = [
452
+ `openai_base_url = "http://localhost:${config.openaiPort}"`,
453
+ `model_provider = "skalpel-proxy"`,
454
+ `[model_providers.skalpel-proxy]`,
455
+ `wire_api = "responses"`,
456
+ `base_url = "http://localhost:${config.openaiPort}/v1"`
457
+ ];
458
+ const missing = requiredLines.filter((line) => !content.includes(line));
459
+ if (missing.length === 0) {
460
+ return {
461
+ name: "Codex config",
462
+ status: "ok",
463
+ message: `skalpel-proxy provider pinned (wire_api=responses) on port ${config.openaiPort}`
464
+ };
465
+ }
466
+ return {
467
+ name: "Codex config",
468
+ status: "fail",
469
+ message: `missing TOML: ${missing.map((m) => m.split("\n")[0]).join("; ")}`
470
+ };
471
+ }
472
+ async function checkCodexWebSocket(config) {
473
+ const tcpOk = await new Promise((resolve2) => {
474
+ const sock = net.connect({ host: "127.0.0.1", port: config.openaiPort, timeout: 1e3 });
475
+ const done = (ok) => {
476
+ sock.removeAllListeners();
477
+ try {
478
+ sock.destroy();
479
+ } catch {
480
+ }
481
+ resolve2(ok);
482
+ };
483
+ sock.once("connect", () => done(true));
484
+ sock.once("error", () => done(false));
485
+ sock.once("timeout", () => done(false));
486
+ });
487
+ if (!tcpOk) {
488
+ return {
489
+ name: "Codex WebSocket",
490
+ status: "skipped",
491
+ message: "WebSocket: SKIPPED (proxy not running)"
492
+ };
493
+ }
494
+ return new Promise((resolve2) => {
495
+ const url = `ws://localhost:${config.openaiPort}/v1/responses`;
496
+ let settled = false;
497
+ const ws = new WebSocket(url, ["skalpel-codex-v1"]);
498
+ const settle = (result) => {
499
+ if (settled) return;
500
+ settled = true;
501
+ try {
502
+ ws.close();
503
+ } catch {
504
+ }
505
+ resolve2(result);
506
+ };
507
+ const timeout = setTimeout(() => {
508
+ settle({
509
+ name: "Codex WebSocket",
510
+ status: "fail",
511
+ message: "WebSocket: FAIL handshake timeout after 5s"
512
+ });
513
+ }, 5e3);
514
+ ws.once("open", () => {
515
+ clearTimeout(timeout);
516
+ settle({ name: "Codex WebSocket", status: "ok", message: "WebSocket: OK" });
517
+ });
518
+ ws.once("error", (err) => {
519
+ clearTimeout(timeout);
520
+ settle({
521
+ name: "Codex WebSocket",
522
+ status: "fail",
523
+ message: `WebSocket: FAIL ${err.message}`
524
+ });
525
+ });
526
+ ws.once("unexpected-response", (_req, res) => {
527
+ clearTimeout(timeout);
528
+ settle({
529
+ name: "Codex WebSocket",
530
+ status: "fail",
531
+ message: `WebSocket: FAIL unexpected HTTP ${res.statusCode}`
532
+ });
533
+ });
534
+ });
535
+ }
536
+ async function checkCodexProxyProbe(config) {
537
+ const url = `http://localhost:${config.openaiPort}/v1/responses`;
538
+ try {
539
+ const res = await fetch(url, {
540
+ method: "POST",
541
+ headers: { "Content-Type": "application/json", Authorization: "Bearer sk-codex-placeholder-skalpel" },
542
+ body: JSON.stringify({ model: "gpt-5-codex", input: "ping", stream: false }),
543
+ signal: AbortSignal.timeout(5e3)
544
+ });
545
+ if (res.status === 405) {
546
+ return { name: "Codex proxy probe", status: "error", message: "backend rejected POST \u2014 run the fix in docs/codex-integration-fix.md" };
547
+ }
548
+ if (res.status === 401 || res.status >= 200 && res.status < 300) {
549
+ return { name: "Codex proxy probe", status: "ok", message: `POST /v1/responses returned ${res.status}` };
550
+ }
551
+ return { name: "Codex proxy probe", status: "warn", message: `unexpected status ${res.status}` };
552
+ } catch {
553
+ return { name: "Codex proxy probe", status: "warn", message: "proxy not reachable (is it running?)" };
554
+ }
555
+ }
322
556
  function loadConfigApiKey() {
323
557
  try {
324
558
  const configPath = path4.join(os2.homedir(), ".skalpel", "config.json");
@@ -436,12 +670,22 @@ async function runDoctor() {
436
670
  checks.push({ name: agent.name, status: "warn", message: "Not installed" });
437
671
  }
438
672
  }
439
- const icons = { ok: "+", warn: "!", fail: "x" };
673
+ let openaiPort = 18101;
674
+ try {
675
+ const raw = JSON.parse(fs4.readFileSync(skalpelConfigPath, "utf-8"));
676
+ if (typeof raw.openaiPort === "number") openaiPort = raw.openaiPort;
677
+ } catch {
678
+ }
679
+ const config = { openaiPort };
680
+ checks.push(checkCodexConfig(config));
681
+ checks.push(await checkCodexProxyProbe(config));
682
+ checks.push(await checkCodexWebSocket(config));
683
+ const icons = { ok: "+", warn: "!", fail: "x", error: "x", skipped: "-" };
440
684
  for (const check of checks) {
441
685
  const icon = icons[check.status];
442
686
  print2(` [${icon}] ${check.name}: ${check.message}`);
443
687
  }
444
- const failures = checks.filter((c) => c.status === "fail");
688
+ const failures = checks.filter((c) => c.status === "fail" || c.status === "error");
445
689
  const warnings = checks.filter((c) => c.status === "warn");
446
690
  print2("");
447
691
  if (failures.length > 0) {
@@ -639,7 +883,7 @@ async function runReplay(filePaths) {
639
883
 
640
884
  // src/cli/start.ts
641
885
  import { spawn } from "child_process";
642
- import path10 from "path";
886
+ import path12 from "path";
643
887
  import { fileURLToPath as fileURLToPath2 } from "url";
644
888
 
645
889
  // src/proxy/config.ts
@@ -776,6 +1020,20 @@ function removePid(pidFile) {
776
1020
  }
777
1021
  }
778
1022
 
1023
+ // src/proxy/health-check.ts
1024
+ async function isProxyAlive(port, timeoutMs = 2e3) {
1025
+ const controller = new AbortController();
1026
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1027
+ try {
1028
+ const res = await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
1029
+ return res.ok;
1030
+ } catch {
1031
+ return false;
1032
+ } finally {
1033
+ clearTimeout(timer);
1034
+ }
1035
+ }
1036
+
779
1037
  // src/cli/service/install.ts
780
1038
  import fs8 from "fs";
781
1039
  import path9 from "path";
@@ -1118,211 +1376,32 @@ function uninstallService() {
1118
1376
  }
1119
1377
  }
1120
1378
 
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
1197
- import fs9 from "fs";
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
1379
  // src/cli/agents/configure.ts
1302
- import fs11 from "fs";
1303
- import path12 from "path";
1380
+ import fs9 from "fs";
1381
+ import path10 from "path";
1304
1382
  import os7 from "os";
1305
1383
  var CURSOR_API_BASE_URL_KEY = "openai.apiBaseUrl";
1306
1384
  var DIRECT_MODE_BASE_URL = "https://api.skalpel.ai";
1307
1385
  var CODEX_DIRECT_PROVIDER_ID = "skalpel";
1386
+ var CODEX_PROXY_PROVIDER_ID = "skalpel-proxy";
1308
1387
  function ensureDir(dir) {
1309
- fs11.mkdirSync(dir, { recursive: true });
1388
+ fs9.mkdirSync(dir, { recursive: true });
1310
1389
  }
1311
1390
  function createBackup(filePath) {
1312
- if (fs11.existsSync(filePath)) {
1313
- fs11.copyFileSync(filePath, `${filePath}.skalpel-backup`);
1391
+ if (fs9.existsSync(filePath)) {
1392
+ fs9.copyFileSync(filePath, `${filePath}.skalpel-backup`);
1314
1393
  }
1315
1394
  }
1316
1395
  function readJsonFile(filePath) {
1317
1396
  try {
1318
- return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
1397
+ return JSON.parse(fs9.readFileSync(filePath, "utf-8"));
1319
1398
  } catch {
1320
1399
  return null;
1321
1400
  }
1322
1401
  }
1323
1402
  function configureClaudeCode(agent, proxyConfig, direct = false) {
1324
- const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
1325
- const configDir = path12.dirname(configPath);
1403
+ const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1404
+ const configDir = path10.dirname(configPath);
1326
1405
  ensureDir(configDir);
1327
1406
  createBackup(configPath);
1328
1407
  const config = readJsonFile(configPath) ?? {};
@@ -1337,11 +1416,11 @@ function configureClaudeCode(agent, proxyConfig, direct = false) {
1337
1416
  env.ANTHROPIC_BASE_URL = `http://localhost:${proxyConfig.anthropicPort}`;
1338
1417
  delete env.ANTHROPIC_CUSTOM_HEADERS;
1339
1418
  }
1340
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1419
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1341
1420
  }
1342
1421
  function readTomlFile(filePath) {
1343
1422
  try {
1344
- return fs11.readFileSync(filePath, "utf-8");
1423
+ return fs9.readFileSync(filePath, "utf-8");
1345
1424
  } catch {
1346
1425
  return "";
1347
1426
  }
@@ -1396,10 +1475,43 @@ function removeCodexDirectProvider(content) {
1396
1475
  const rest = content.slice(end).replace(/^\n+/, "");
1397
1476
  return before.length > 0 && rest.length > 0 ? before + "\n" + rest : before + rest;
1398
1477
  }
1478
+ function buildCodexProxyProviderBlock(port) {
1479
+ return [
1480
+ `[model_providers.skalpel-proxy]`,
1481
+ `name = "Skalpel Proxy"`,
1482
+ `base_url = "http://localhost:${port}/v1"`,
1483
+ `wire_api = "responses"`,
1484
+ `env_key = "OPENAI_API_KEY"`
1485
+ ].join("\n");
1486
+ }
1487
+ function upsertCodexProxyProvider(content, port) {
1488
+ const sectionHeader = `[model_providers.${CODEX_PROXY_PROVIDER_ID}]`;
1489
+ const block = buildCodexProxyProviderBlock(port);
1490
+ const idx = content.indexOf(sectionHeader);
1491
+ if (idx === -1) {
1492
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n\n" : content.length > 0 ? "\n" : "";
1493
+ return content + separator + block + "\n";
1494
+ }
1495
+ const after = content.slice(idx + sectionHeader.length);
1496
+ const nextHeaderMatch = after.match(/\n\[[^\]]+\]/);
1497
+ const end = nextHeaderMatch && nextHeaderMatch.index !== void 0 ? idx + sectionHeader.length + nextHeaderMatch.index : content.length;
1498
+ return content.slice(0, idx) + block + content.slice(end);
1499
+ }
1500
+ function removeCodexProxyProvider(content) {
1501
+ const sectionHeader = `[model_providers.${CODEX_PROXY_PROVIDER_ID}]`;
1502
+ const idx = content.indexOf(sectionHeader);
1503
+ if (idx === -1) return content;
1504
+ const after = content.slice(idx + sectionHeader.length);
1505
+ const nextHeaderMatch = after.match(/\n\[[^\]]+\]/);
1506
+ const end = nextHeaderMatch && nextHeaderMatch.index !== void 0 ? idx + sectionHeader.length + nextHeaderMatch.index : content.length;
1507
+ const before = content.slice(0, idx).replace(/\n+$/, "");
1508
+ const rest = content.slice(end).replace(/^\n+/, "");
1509
+ return before.length > 0 && rest.length > 0 ? before + "\n" + rest : before + rest;
1510
+ }
1399
1511
  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));
1512
+ const configDir = process.platform === "win32" ? path10.join(os7.homedir(), "AppData", "Roaming", "codex") : path10.join(os7.homedir(), ".codex");
1513
+ const configPath = agent.configPath ?? path10.join(configDir, "config.toml");
1514
+ ensureDir(path10.dirname(configPath));
1403
1515
  createBackup(configPath);
1404
1516
  let content = readTomlFile(configPath);
1405
1517
  if (direct) {
@@ -1408,18 +1520,19 @@ function configureCodex(agent, proxyConfig, direct = false) {
1408
1520
  content = upsertCodexDirectProvider(content, proxyConfig.apiKey);
1409
1521
  } else {
1410
1522
  content = setTomlKey(content, "openai_base_url", `http://localhost:${proxyConfig.openaiPort}`);
1411
- content = removeTomlKey(content, "model_provider");
1523
+ content = setTomlKey(content, "model_provider", CODEX_PROXY_PROVIDER_ID);
1524
+ content = upsertCodexProxyProvider(content, proxyConfig.openaiPort);
1412
1525
  content = removeCodexDirectProvider(content);
1413
1526
  }
1414
- fs11.writeFileSync(configPath, content);
1527
+ fs9.writeFileSync(configPath, content);
1415
1528
  }
1416
1529
  function getCursorConfigDir() {
1417
1530
  if (process.platform === "darwin") {
1418
- return path12.join(os7.homedir(), "Library", "Application Support", "Cursor", "User");
1531
+ return path10.join(os7.homedir(), "Library", "Application Support", "Cursor", "User");
1419
1532
  } else if (process.platform === "win32") {
1420
- return path12.join(process.env.APPDATA ?? path12.join(os7.homedir(), "AppData", "Roaming"), "Cursor", "User");
1533
+ return path10.join(process.env.APPDATA ?? path10.join(os7.homedir(), "AppData", "Roaming"), "Cursor", "User");
1421
1534
  }
1422
- return path12.join(os7.homedir(), ".config", "Cursor", "User");
1535
+ return path10.join(os7.homedir(), ".config", "Cursor", "User");
1423
1536
  }
1424
1537
  function configureCursor(agent, proxyConfig, direct = false) {
1425
1538
  if (direct) {
@@ -1427,8 +1540,8 @@ function configureCursor(agent, proxyConfig, direct = false) {
1427
1540
  return;
1428
1541
  }
1429
1542
  const configDir = getCursorConfigDir();
1430
- const configPath = agent.configPath ?? path12.join(configDir, "settings.json");
1431
- ensureDir(path12.dirname(configPath));
1543
+ const configPath = agent.configPath ?? path10.join(configDir, "settings.json");
1544
+ ensureDir(path10.dirname(configPath));
1432
1545
  createBackup(configPath);
1433
1546
  const config = readJsonFile(configPath) ?? {};
1434
1547
  const existingUrl = config[CURSOR_API_BASE_URL_KEY];
@@ -1437,7 +1550,7 @@ function configureCursor(agent, proxyConfig, direct = false) {
1437
1550
  saveConfig(proxyConfig);
1438
1551
  }
1439
1552
  config[CURSOR_API_BASE_URL_KEY] = `http://localhost:${proxyConfig.cursorPort}`;
1440
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1553
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1441
1554
  }
1442
1555
  function configureAgent(agent, proxyConfig, direct = false) {
1443
1556
  switch (agent.name) {
@@ -1453,8 +1566,8 @@ function configureAgent(agent, proxyConfig, direct = false) {
1453
1566
  }
1454
1567
  }
1455
1568
  function unconfigureClaudeCode(agent) {
1456
- const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
1457
- if (!fs11.existsSync(configPath)) return;
1569
+ const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1570
+ if (!fs9.existsSync(configPath)) return;
1458
1571
  const config = readJsonFile(configPath);
1459
1572
  if (config === null) {
1460
1573
  console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ANTHROPIC_BASE_URL manually if needed.`);
@@ -1468,41 +1581,42 @@ function unconfigureClaudeCode(agent) {
1468
1581
  delete config.env;
1469
1582
  }
1470
1583
  }
1471
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1584
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1472
1585
  const backupPath = `${configPath}.skalpel-backup`;
1473
- if (fs11.existsSync(backupPath)) {
1474
- fs11.unlinkSync(backupPath);
1586
+ if (fs9.existsSync(backupPath)) {
1587
+ fs9.unlinkSync(backupPath);
1475
1588
  }
1476
1589
  }
1477
1590
  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)) {
1591
+ const configDir = process.platform === "win32" ? path10.join(os7.homedir(), "AppData", "Roaming", "codex") : path10.join(os7.homedir(), ".codex");
1592
+ const configPath = agent.configPath ?? path10.join(configDir, "config.toml");
1593
+ if (fs9.existsSync(configPath)) {
1481
1594
  let content = readTomlFile(configPath);
1482
1595
  content = removeTomlKey(content, "openai_base_url");
1483
1596
  content = removeTomlKey(content, "model_provider");
1484
1597
  content = removeCodexDirectProvider(content);
1485
- fs11.writeFileSync(configPath, content);
1598
+ content = removeCodexProxyProvider(content);
1599
+ fs9.writeFileSync(configPath, content);
1486
1600
  }
1487
1601
  const backupPath = `${configPath}.skalpel-backup`;
1488
- if (fs11.existsSync(backupPath)) {
1489
- fs11.unlinkSync(backupPath);
1602
+ if (fs9.existsSync(backupPath)) {
1603
+ fs9.unlinkSync(backupPath);
1490
1604
  }
1491
1605
  }
1492
1606
  function unconfigureCursor(agent) {
1493
1607
  const configDir = getCursorConfigDir();
1494
- const configPath = agent.configPath ?? path12.join(configDir, "settings.json");
1495
- if (!fs11.existsSync(configPath)) return;
1608
+ const configPath = agent.configPath ?? path10.join(configDir, "settings.json");
1609
+ if (!fs9.existsSync(configPath)) return;
1496
1610
  const config = readJsonFile(configPath);
1497
1611
  if (config === null) {
1498
1612
  console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ${CURSOR_API_BASE_URL_KEY} manually if needed.`);
1499
1613
  return;
1500
1614
  }
1501
1615
  delete config[CURSOR_API_BASE_URL_KEY];
1502
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1616
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1503
1617
  const backupPath = `${configPath}.skalpel-backup`;
1504
- if (fs11.existsSync(backupPath)) {
1505
- fs11.unlinkSync(backupPath);
1618
+ if (fs9.existsSync(backupPath)) {
1619
+ fs9.unlinkSync(backupPath);
1506
1620
  }
1507
1621
  }
1508
1622
  function unconfigureAgent(agent) {
@@ -1520,8 +1634,8 @@ function unconfigureAgent(agent) {
1520
1634
  }
1521
1635
 
1522
1636
  // src/cli/agents/shell.ts
1523
- import fs12 from "fs";
1524
- import path13 from "path";
1637
+ import fs10 from "fs";
1638
+ import path11 from "path";
1525
1639
  import os8 from "os";
1526
1640
  var BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
1527
1641
  var END_MARKER = "# END SKALPEL PROXY";
@@ -1530,21 +1644,21 @@ var PS_END_MARKER = "# END SKALPEL PROXY";
1530
1644
  function getUnixProfilePaths() {
1531
1645
  const home = os8.homedir();
1532
1646
  const candidates = [
1533
- path13.join(home, ".bashrc"),
1534
- path13.join(home, ".zshrc"),
1535
- path13.join(home, ".bash_profile"),
1536
- path13.join(home, ".profile")
1647
+ path11.join(home, ".bashrc"),
1648
+ path11.join(home, ".zshrc"),
1649
+ path11.join(home, ".bash_profile"),
1650
+ path11.join(home, ".profile")
1537
1651
  ];
1538
- return candidates.filter((p) => fs12.existsSync(p));
1652
+ return candidates.filter((p) => fs10.existsSync(p));
1539
1653
  }
1540
1654
  function getPowerShellProfilePath() {
1541
1655
  if (process.platform !== "win32") return null;
1542
1656
  if (process.env.PROFILE) return process.env.PROFILE;
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;
1657
+ const docsDir = path11.join(os8.homedir(), "Documents");
1658
+ const psProfile = path11.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
1659
+ const wpProfile = path11.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
1660
+ if (fs10.existsSync(psProfile)) return psProfile;
1661
+ if (fs10.existsSync(wpProfile)) return wpProfile;
1548
1662
  return psProfile;
1549
1663
  }
1550
1664
  function generateUnixBlock(proxyConfig) {
@@ -1565,13 +1679,13 @@ function generatePowerShellBlock(proxyConfig) {
1565
1679
  }
1566
1680
  function createBackup2(filePath) {
1567
1681
  const backupPath = `${filePath}.skalpel-backup`;
1568
- fs12.copyFileSync(filePath, backupPath);
1682
+ fs10.copyFileSync(filePath, backupPath);
1569
1683
  }
1570
1684
  function updateProfileFile(filePath, block, beginMarker, endMarker) {
1571
- if (fs12.existsSync(filePath)) {
1685
+ if (fs10.existsSync(filePath)) {
1572
1686
  createBackup2(filePath);
1573
1687
  }
1574
- let content = fs12.existsSync(filePath) ? fs12.readFileSync(filePath, "utf-8") : "";
1688
+ let content = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf-8") : "";
1575
1689
  const beginIdx = content.indexOf(beginMarker);
1576
1690
  const endIdx = content.indexOf(endMarker);
1577
1691
  if (beginIdx !== -1 && endIdx !== -1) {
@@ -1584,15 +1698,15 @@ function updateProfileFile(filePath, block, beginMarker, endMarker) {
1584
1698
  content = block + "\n";
1585
1699
  }
1586
1700
  }
1587
- fs12.writeFileSync(filePath, content);
1701
+ fs10.writeFileSync(filePath, content);
1588
1702
  }
1589
1703
  function configureShellEnvVars(_agents, proxyConfig) {
1590
1704
  const modified = [];
1591
1705
  if (process.platform === "win32") {
1592
1706
  const psProfile = getPowerShellProfilePath();
1593
1707
  if (psProfile) {
1594
- const dir = path13.dirname(psProfile);
1595
- fs12.mkdirSync(dir, { recursive: true });
1708
+ const dir = path11.dirname(psProfile);
1709
+ fs10.mkdirSync(dir, { recursive: true });
1596
1710
  const block = generatePowerShellBlock(proxyConfig);
1597
1711
  updateProfileFile(psProfile, block, PS_BEGIN_MARKER, PS_END_MARKER);
1598
1712
  modified.push(psProfile);
@@ -1611,28 +1725,28 @@ function removeShellEnvVars() {
1611
1725
  const restored = [];
1612
1726
  const home = os8.homedir();
1613
1727
  const allProfiles = [
1614
- path13.join(home, ".bashrc"),
1615
- path13.join(home, ".zshrc"),
1616
- path13.join(home, ".bash_profile"),
1617
- path13.join(home, ".profile")
1728
+ path11.join(home, ".bashrc"),
1729
+ path11.join(home, ".zshrc"),
1730
+ path11.join(home, ".bash_profile"),
1731
+ path11.join(home, ".profile")
1618
1732
  ];
1619
1733
  if (process.platform === "win32") {
1620
1734
  const psProfile = getPowerShellProfilePath();
1621
1735
  if (psProfile) allProfiles.push(psProfile);
1622
1736
  }
1623
1737
  for (const profilePath of allProfiles) {
1624
- if (!fs12.existsSync(profilePath)) continue;
1625
- const content = fs12.readFileSync(profilePath, "utf-8");
1738
+ if (!fs10.existsSync(profilePath)) continue;
1739
+ const content = fs10.readFileSync(profilePath, "utf-8");
1626
1740
  const beginIdx = content.indexOf(BEGIN_MARKER);
1627
1741
  const endIdx = content.indexOf(END_MARKER);
1628
1742
  if (beginIdx === -1 || endIdx === -1) continue;
1629
1743
  const before = content.slice(0, beginIdx);
1630
1744
  const after = content.slice(endIdx + END_MARKER.length);
1631
1745
  const cleaned = (before.replace(/\n+$/, "") + after.replace(/^\n+/, "\n")).trimEnd() + "\n";
1632
- fs12.writeFileSync(profilePath, cleaned);
1746
+ fs10.writeFileSync(profilePath, cleaned);
1633
1747
  const backupPath = `${profilePath}.skalpel-backup`;
1634
- if (fs12.existsSync(backupPath)) {
1635
- fs12.unlinkSync(backupPath);
1748
+ if (fs10.existsSync(backupPath)) {
1749
+ fs10.unlinkSync(backupPath);
1636
1750
  }
1637
1751
  restored.push(profilePath);
1638
1752
  }
@@ -1645,6 +1759,226 @@ function removeShellBlock() {
1645
1759
  return removeShellEnvVars();
1646
1760
  }
1647
1761
 
1762
+ // src/cli/start.ts
1763
+ function print5(msg) {
1764
+ console.log(msg);
1765
+ }
1766
+ function reconfigureAgents(config) {
1767
+ const direct = config.mode === "direct";
1768
+ const agents = detectAgents();
1769
+ for (const agent of agents) {
1770
+ if (agent.installed) {
1771
+ try {
1772
+ configureAgent(agent, config, direct);
1773
+ } catch {
1774
+ }
1775
+ }
1776
+ }
1777
+ try {
1778
+ configureShellEnvVars(agents.filter((a) => a.installed), config);
1779
+ } catch {
1780
+ }
1781
+ }
1782
+ async function runStart() {
1783
+ const config = loadConfig();
1784
+ if (!config.apiKey) {
1785
+ print5(' Error: No API key configured. Run "skalpel init" or set SKALPEL_API_KEY.');
1786
+ process.exit(1);
1787
+ }
1788
+ const existingPid = readPid(config.pidFile);
1789
+ if (existingPid !== null) {
1790
+ print5(` Proxy is already running (pid=${existingPid}).`);
1791
+ return;
1792
+ }
1793
+ const alive = await isProxyAlive(config.anthropicPort);
1794
+ if (alive) {
1795
+ print5(" Proxy is already running (detected via health check).");
1796
+ return;
1797
+ }
1798
+ if (isServiceInstalled()) {
1799
+ startService();
1800
+ reconfigureAgents(config);
1801
+ print5(` Skalpel proxy started via system service on ports ${config.anthropicPort}, ${config.openaiPort}, and ${config.cursorPort}`);
1802
+ return;
1803
+ }
1804
+ const dirname = path12.dirname(fileURLToPath2(import.meta.url));
1805
+ const runnerScript = path12.resolve(dirname, "proxy-runner.js");
1806
+ const child = spawn(process.execPath, [runnerScript], {
1807
+ detached: true,
1808
+ stdio: "ignore"
1809
+ });
1810
+ child.unref();
1811
+ reconfigureAgents(config);
1812
+ print5(` Skalpel proxy started on ports ${config.anthropicPort}, ${config.openaiPort}, and ${config.cursorPort}`);
1813
+ }
1814
+
1815
+ // src/cli/stop.ts
1816
+ import { execSync as execSync5 } from "child_process";
1817
+
1818
+ // src/proxy/server.ts
1819
+ init_handler();
1820
+ import http from "http";
1821
+
1822
+ // src/proxy/logger.ts
1823
+ import fs11 from "fs";
1824
+ import path13 from "path";
1825
+ var MAX_SIZE = 5 * 1024 * 1024;
1826
+
1827
+ // src/proxy/ws-server.ts
1828
+ import { WebSocketServer } from "ws";
1829
+ var wss = new WebSocketServer({ noServer: true });
1830
+
1831
+ // src/proxy/server.ts
1832
+ var proxyStartTime = 0;
1833
+ function stopProxy(config) {
1834
+ const pid = readPid(config.pidFile);
1835
+ if (pid === null) return false;
1836
+ try {
1837
+ process.kill(pid, "SIGTERM");
1838
+ } catch {
1839
+ }
1840
+ removePid(config.pidFile);
1841
+ return true;
1842
+ }
1843
+ async function getProxyStatus(config) {
1844
+ const pid = readPid(config.pidFile);
1845
+ if (pid !== null) {
1846
+ return {
1847
+ running: true,
1848
+ pid,
1849
+ uptime: proxyStartTime > 0 ? Date.now() - proxyStartTime : 0,
1850
+ anthropicPort: config.anthropicPort,
1851
+ openaiPort: config.openaiPort,
1852
+ cursorPort: config.cursorPort
1853
+ };
1854
+ }
1855
+ const alive = await isProxyAlive(config.anthropicPort);
1856
+ return {
1857
+ running: alive,
1858
+ pid: null,
1859
+ uptime: 0,
1860
+ anthropicPort: config.anthropicPort,
1861
+ openaiPort: config.openaiPort,
1862
+ cursorPort: config.cursorPort
1863
+ };
1864
+ }
1865
+
1866
+ // src/cli/stop.ts
1867
+ function print6(msg) {
1868
+ console.log(msg);
1869
+ }
1870
+ async function runStop() {
1871
+ const config = loadConfig();
1872
+ if (isServiceInstalled()) {
1873
+ stopService();
1874
+ }
1875
+ const stopped = stopProxy(config);
1876
+ if (stopped) {
1877
+ print6(" Skalpel proxy stopped.");
1878
+ } else {
1879
+ const alive = await isProxyAlive(config.anthropicPort);
1880
+ if (alive) {
1881
+ let killedViaPort = false;
1882
+ if (process.platform === "darwin" || process.platform === "linux") {
1883
+ try {
1884
+ const pids = execSync5(`lsof -ti :${config.anthropicPort}`, { timeout: 3e3 }).toString().trim().split("\n").filter(Boolean);
1885
+ for (const p of pids) {
1886
+ const pid = parseInt(p, 10);
1887
+ if (Number.isInteger(pid) && pid > 0) {
1888
+ try {
1889
+ process.kill(pid, "SIGTERM");
1890
+ } catch {
1891
+ }
1892
+ }
1893
+ }
1894
+ killedViaPort = true;
1895
+ } catch {
1896
+ }
1897
+ }
1898
+ if (killedViaPort) {
1899
+ print6(" Skalpel proxy stopped (found via port detection).");
1900
+ } else {
1901
+ print6(" Proxy appears to be running but could not be stopped automatically.");
1902
+ print6(` Try: kill $(lsof -ti :${config.anthropicPort})`);
1903
+ }
1904
+ } else {
1905
+ print6(" Proxy is not running.");
1906
+ }
1907
+ }
1908
+ const agents = detectAgents();
1909
+ for (const agent of agents) {
1910
+ if (agent.installed) {
1911
+ try {
1912
+ unconfigureAgent(agent);
1913
+ } catch {
1914
+ }
1915
+ }
1916
+ }
1917
+ try {
1918
+ removeShellEnvVars();
1919
+ } catch {
1920
+ }
1921
+ }
1922
+
1923
+ // src/cli/status.ts
1924
+ function print7(msg) {
1925
+ console.log(msg);
1926
+ }
1927
+ async function runStatus() {
1928
+ const config = loadConfig();
1929
+ const status = await getProxyStatus(config);
1930
+ print7("");
1931
+ print7(" Skalpel Proxy Status");
1932
+ print7(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1933
+ print7(` Status: ${status.running ? "running" : "stopped"}`);
1934
+ if (status.pid !== null) {
1935
+ print7(` PID: ${status.pid}`);
1936
+ }
1937
+ print7(` Anthropic: port ${status.anthropicPort}`);
1938
+ print7(` OpenAI: port ${status.openaiPort}`);
1939
+ print7(` Cursor: port ${status.cursorPort}`);
1940
+ print7(` Config: ${config.configFile}`);
1941
+ print7("");
1942
+ }
1943
+
1944
+ // src/cli/logs.ts
1945
+ import fs12 from "fs";
1946
+ function print8(msg) {
1947
+ console.log(msg);
1948
+ }
1949
+ async function runLogs(options) {
1950
+ const config = loadConfig();
1951
+ const logFile = config.logFile;
1952
+ const lineCount = parseInt(options.lines ?? "50", 10);
1953
+ if (!fs12.existsSync(logFile)) {
1954
+ print8(` No log file found at ${logFile}`);
1955
+ return;
1956
+ }
1957
+ const content = fs12.readFileSync(logFile, "utf-8");
1958
+ const lines = content.trimEnd().split("\n");
1959
+ const tail = lines.slice(-lineCount);
1960
+ for (const line of tail) {
1961
+ print8(line);
1962
+ }
1963
+ if (options.follow) {
1964
+ let position = fs12.statSync(logFile).size;
1965
+ fs12.watchFile(logFile, { interval: 500 }, () => {
1966
+ try {
1967
+ const stat = fs12.statSync(logFile);
1968
+ if (stat.size > position) {
1969
+ const fd = fs12.openSync(logFile, "r");
1970
+ const buf = Buffer.alloc(stat.size - position);
1971
+ fs12.readSync(fd, buf, 0, buf.length, position);
1972
+ fs12.closeSync(fd);
1973
+ process.stdout.write(buf.toString("utf-8"));
1974
+ position = stat.size;
1975
+ }
1976
+ } catch {
1977
+ }
1978
+ });
1979
+ }
1980
+ }
1981
+
1648
1982
  // src/cli/config-cmd.ts
1649
1983
  function print9(msg) {
1650
1984
  console.log(msg);
@@ -1901,6 +2235,13 @@ async function runWizard(options) {
1901
2235
  print11(` Configured ${agent.name}${agent.configPath ? ` (${agent.configPath})` : ""}`);
1902
2236
  }
1903
2237
  print11("");
2238
+ const codexConfigured = agentsToConfigure.some((a) => a.name === "codex");
2239
+ if (codexConfigured && !process.env.OPENAI_API_KEY) {
2240
+ print11(" [!] Codex expects OPENAI_API_KEY to be set. The Skalpel proxy ignores the value,");
2241
+ print11(" so any non-empty string works, e.g.:");
2242
+ print11(" export OPENAI_API_KEY=sk-codex-placeholder-skalpel");
2243
+ print11("");
2244
+ }
1904
2245
  }
1905
2246
  print11(" Installing proxy as system service...");
1906
2247
  try {