skalpel 2.0.14 → 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";
@@ -362,6 +469,70 @@ function checkCodexConfig(config) {
362
469
  message: `missing TOML: ${missing.map((m) => m.split("\n")[0]).join("; ")}`
363
470
  };
364
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
+ }
365
536
  async function checkCodexProxyProbe(config) {
366
537
  const url = `http://localhost:${config.openaiPort}/v1/responses`;
367
538
  try {
@@ -508,7 +679,8 @@ async function runDoctor() {
508
679
  const config = { openaiPort };
509
680
  checks.push(checkCodexConfig(config));
510
681
  checks.push(await checkCodexProxyProbe(config));
511
- const icons = { ok: "+", warn: "!", fail: "x", error: "x" };
682
+ checks.push(await checkCodexWebSocket(config));
683
+ const icons = { ok: "+", warn: "!", fail: "x", error: "x", skipped: "-" };
512
684
  for (const check of checks) {
513
685
  const icon = icons[check.status];
514
686
  print2(` [${icon}] ${check.name}: ${check.message}`);
@@ -711,7 +883,7 @@ async function runReplay(filePaths) {
711
883
 
712
884
  // src/cli/start.ts
713
885
  import { spawn } from "child_process";
714
- import path10 from "path";
886
+ import path12 from "path";
715
887
  import { fileURLToPath as fileURLToPath2 } from "url";
716
888
 
717
889
  // src/proxy/config.ts
@@ -848,6 +1020,20 @@ function removePid(pidFile) {
848
1020
  }
849
1021
  }
850
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
+
851
1037
  // src/cli/service/install.ts
852
1038
  import fs8 from "fs";
853
1039
  import path9 from "path";
@@ -1190,212 +1376,32 @@ function uninstallService() {
1190
1376
  }
1191
1377
  }
1192
1378
 
1193
- // src/cli/start.ts
1194
- function print5(msg) {
1195
- console.log(msg);
1196
- }
1197
- async function runStart() {
1198
- const config = loadConfig();
1199
- if (!config.apiKey) {
1200
- print5(' Error: No API key configured. Run "skalpel init" or set SKALPEL_API_KEY.');
1201
- process.exit(1);
1202
- }
1203
- const existingPid = readPid(config.pidFile);
1204
- if (existingPid !== null) {
1205
- print5(` Proxy is already running (pid=${existingPid}).`);
1206
- return;
1207
- }
1208
- if (isServiceInstalled()) {
1209
- startService();
1210
- print5(` Skalpel proxy started via system service on ports ${config.anthropicPort}, ${config.openaiPort}, and ${config.cursorPort}`);
1211
- return;
1212
- }
1213
- const dirname = path10.dirname(fileURLToPath2(import.meta.url));
1214
- const runnerScript = path10.resolve(dirname, "proxy-runner.js");
1215
- const child = spawn(process.execPath, [runnerScript], {
1216
- detached: true,
1217
- stdio: "ignore"
1218
- });
1219
- child.unref();
1220
- print5(` Skalpel proxy started on ports ${config.anthropicPort}, ${config.openaiPort}, and ${config.cursorPort}`);
1221
- }
1222
-
1223
- // src/proxy/server.ts
1224
- import http from "http";
1225
-
1226
- // src/proxy/dispatcher.ts
1227
- import { Agent } from "undici";
1228
- var skalpelDispatcher = new Agent({
1229
- keepAliveTimeout: 1e4,
1230
- keepAliveMaxTimeout: 6e4,
1231
- connections: 100,
1232
- pipelining: 1
1233
- });
1234
-
1235
- // src/proxy/recovery.ts
1236
- import { createHash } from "crypto";
1237
- var MUTEX_MAX_ENTRIES = 1024;
1238
- var LruMutexMap = class extends Map {
1239
- set(key, value) {
1240
- if (this.has(key)) {
1241
- super.delete(key);
1242
- } else if (this.size >= MUTEX_MAX_ENTRIES) {
1243
- const oldest = this.keys().next().value;
1244
- if (oldest !== void 0) super.delete(oldest);
1245
- }
1246
- return super.set(key, value);
1247
- }
1248
- };
1249
- var refreshMutex = new LruMutexMap();
1250
-
1251
- // src/proxy/streaming.ts
1252
- var HOP_BY_HOP = /* @__PURE__ */ new Set([
1253
- "connection",
1254
- "keep-alive",
1255
- "proxy-authenticate",
1256
- "proxy-authorization",
1257
- "te",
1258
- "trailer",
1259
- "transfer-encoding",
1260
- "upgrade"
1261
- ]);
1262
- var STRIP_HEADERS = /* @__PURE__ */ new Set([
1263
- ...HOP_BY_HOP,
1264
- "content-encoding",
1265
- "content-length"
1266
- ]);
1267
-
1268
- // src/proxy/logger.ts
1269
- import fs9 from "fs";
1270
- import path11 from "path";
1271
- var MAX_SIZE = 5 * 1024 * 1024;
1272
-
1273
- // src/proxy/server.ts
1274
- var proxyStartTime = 0;
1275
- function stopProxy(config) {
1276
- const pid = readPid(config.pidFile);
1277
- if (pid === null) return false;
1278
- try {
1279
- process.kill(pid, "SIGTERM");
1280
- } catch {
1281
- }
1282
- removePid(config.pidFile);
1283
- return true;
1284
- }
1285
- function getProxyStatus(config) {
1286
- const pid = readPid(config.pidFile);
1287
- return {
1288
- running: pid !== null,
1289
- pid,
1290
- uptime: proxyStartTime > 0 ? Date.now() - proxyStartTime : 0,
1291
- anthropicPort: config.anthropicPort,
1292
- openaiPort: config.openaiPort,
1293
- cursorPort: config.cursorPort
1294
- };
1295
- }
1296
-
1297
- // src/cli/stop.ts
1298
- function print6(msg) {
1299
- console.log(msg);
1300
- }
1301
- async function runStop() {
1302
- const config = loadConfig();
1303
- if (isServiceInstalled()) {
1304
- stopService();
1305
- }
1306
- const stopped = stopProxy(config);
1307
- if (stopped) {
1308
- print6(" Skalpel proxy stopped.");
1309
- } else {
1310
- print6(" Proxy is not running.");
1311
- }
1312
- }
1313
-
1314
- // src/cli/status.ts
1315
- function print7(msg) {
1316
- console.log(msg);
1317
- }
1318
- async function runStatus() {
1319
- const config = loadConfig();
1320
- const status = getProxyStatus(config);
1321
- print7("");
1322
- print7(" Skalpel Proxy Status");
1323
- print7(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1324
- print7(` Status: ${status.running ? "running" : "stopped"}`);
1325
- if (status.pid !== null) {
1326
- print7(` PID: ${status.pid}`);
1327
- }
1328
- print7(` Anthropic: port ${status.anthropicPort}`);
1329
- print7(` OpenAI: port ${status.openaiPort}`);
1330
- print7(` Cursor: port ${status.cursorPort}`);
1331
- print7(` Config: ${config.configFile}`);
1332
- print7("");
1333
- }
1334
-
1335
- // src/cli/logs.ts
1336
- import fs10 from "fs";
1337
- function print8(msg) {
1338
- console.log(msg);
1339
- }
1340
- async function runLogs(options) {
1341
- const config = loadConfig();
1342
- const logFile = config.logFile;
1343
- const lineCount = parseInt(options.lines ?? "50", 10);
1344
- if (!fs10.existsSync(logFile)) {
1345
- print8(` No log file found at ${logFile}`);
1346
- return;
1347
- }
1348
- const content = fs10.readFileSync(logFile, "utf-8");
1349
- const lines = content.trimEnd().split("\n");
1350
- const tail = lines.slice(-lineCount);
1351
- for (const line of tail) {
1352
- print8(line);
1353
- }
1354
- if (options.follow) {
1355
- let position = fs10.statSync(logFile).size;
1356
- fs10.watchFile(logFile, { interval: 500 }, () => {
1357
- try {
1358
- const stat = fs10.statSync(logFile);
1359
- if (stat.size > position) {
1360
- const fd = fs10.openSync(logFile, "r");
1361
- const buf = Buffer.alloc(stat.size - position);
1362
- fs10.readSync(fd, buf, 0, buf.length, position);
1363
- fs10.closeSync(fd);
1364
- process.stdout.write(buf.toString("utf-8"));
1365
- position = stat.size;
1366
- }
1367
- } catch {
1368
- }
1369
- });
1370
- }
1371
- }
1372
-
1373
1379
  // src/cli/agents/configure.ts
1374
- import fs11 from "fs";
1375
- import path12 from "path";
1380
+ import fs9 from "fs";
1381
+ import path10 from "path";
1376
1382
  import os7 from "os";
1377
1383
  var CURSOR_API_BASE_URL_KEY = "openai.apiBaseUrl";
1378
1384
  var DIRECT_MODE_BASE_URL = "https://api.skalpel.ai";
1379
1385
  var CODEX_DIRECT_PROVIDER_ID = "skalpel";
1380
1386
  var CODEX_PROXY_PROVIDER_ID = "skalpel-proxy";
1381
1387
  function ensureDir(dir) {
1382
- fs11.mkdirSync(dir, { recursive: true });
1388
+ fs9.mkdirSync(dir, { recursive: true });
1383
1389
  }
1384
1390
  function createBackup(filePath) {
1385
- if (fs11.existsSync(filePath)) {
1386
- fs11.copyFileSync(filePath, `${filePath}.skalpel-backup`);
1391
+ if (fs9.existsSync(filePath)) {
1392
+ fs9.copyFileSync(filePath, `${filePath}.skalpel-backup`);
1387
1393
  }
1388
1394
  }
1389
1395
  function readJsonFile(filePath) {
1390
1396
  try {
1391
- return JSON.parse(fs11.readFileSync(filePath, "utf-8"));
1397
+ return JSON.parse(fs9.readFileSync(filePath, "utf-8"));
1392
1398
  } catch {
1393
1399
  return null;
1394
1400
  }
1395
1401
  }
1396
1402
  function configureClaudeCode(agent, proxyConfig, direct = false) {
1397
- const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
1398
- const configDir = path12.dirname(configPath);
1403
+ const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1404
+ const configDir = path10.dirname(configPath);
1399
1405
  ensureDir(configDir);
1400
1406
  createBackup(configPath);
1401
1407
  const config = readJsonFile(configPath) ?? {};
@@ -1410,11 +1416,11 @@ function configureClaudeCode(agent, proxyConfig, direct = false) {
1410
1416
  env.ANTHROPIC_BASE_URL = `http://localhost:${proxyConfig.anthropicPort}`;
1411
1417
  delete env.ANTHROPIC_CUSTOM_HEADERS;
1412
1418
  }
1413
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1419
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1414
1420
  }
1415
1421
  function readTomlFile(filePath) {
1416
1422
  try {
1417
- return fs11.readFileSync(filePath, "utf-8");
1423
+ return fs9.readFileSync(filePath, "utf-8");
1418
1424
  } catch {
1419
1425
  return "";
1420
1426
  }
@@ -1503,9 +1509,9 @@ function removeCodexProxyProvider(content) {
1503
1509
  return before.length > 0 && rest.length > 0 ? before + "\n" + rest : before + rest;
1504
1510
  }
1505
1511
  function configureCodex(agent, proxyConfig, direct = false) {
1506
- const configDir = process.platform === "win32" ? path12.join(os7.homedir(), "AppData", "Roaming", "codex") : path12.join(os7.homedir(), ".codex");
1507
- const configPath = agent.configPath ?? path12.join(configDir, "config.toml");
1508
- 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));
1509
1515
  createBackup(configPath);
1510
1516
  let content = readTomlFile(configPath);
1511
1517
  if (direct) {
@@ -1518,15 +1524,15 @@ function configureCodex(agent, proxyConfig, direct = false) {
1518
1524
  content = upsertCodexProxyProvider(content, proxyConfig.openaiPort);
1519
1525
  content = removeCodexDirectProvider(content);
1520
1526
  }
1521
- fs11.writeFileSync(configPath, content);
1527
+ fs9.writeFileSync(configPath, content);
1522
1528
  }
1523
1529
  function getCursorConfigDir() {
1524
1530
  if (process.platform === "darwin") {
1525
- return path12.join(os7.homedir(), "Library", "Application Support", "Cursor", "User");
1531
+ return path10.join(os7.homedir(), "Library", "Application Support", "Cursor", "User");
1526
1532
  } else if (process.platform === "win32") {
1527
- 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");
1528
1534
  }
1529
- return path12.join(os7.homedir(), ".config", "Cursor", "User");
1535
+ return path10.join(os7.homedir(), ".config", "Cursor", "User");
1530
1536
  }
1531
1537
  function configureCursor(agent, proxyConfig, direct = false) {
1532
1538
  if (direct) {
@@ -1534,8 +1540,8 @@ function configureCursor(agent, proxyConfig, direct = false) {
1534
1540
  return;
1535
1541
  }
1536
1542
  const configDir = getCursorConfigDir();
1537
- const configPath = agent.configPath ?? path12.join(configDir, "settings.json");
1538
- ensureDir(path12.dirname(configPath));
1543
+ const configPath = agent.configPath ?? path10.join(configDir, "settings.json");
1544
+ ensureDir(path10.dirname(configPath));
1539
1545
  createBackup(configPath);
1540
1546
  const config = readJsonFile(configPath) ?? {};
1541
1547
  const existingUrl = config[CURSOR_API_BASE_URL_KEY];
@@ -1544,7 +1550,7 @@ function configureCursor(agent, proxyConfig, direct = false) {
1544
1550
  saveConfig(proxyConfig);
1545
1551
  }
1546
1552
  config[CURSOR_API_BASE_URL_KEY] = `http://localhost:${proxyConfig.cursorPort}`;
1547
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1553
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1548
1554
  }
1549
1555
  function configureAgent(agent, proxyConfig, direct = false) {
1550
1556
  switch (agent.name) {
@@ -1560,8 +1566,8 @@ function configureAgent(agent, proxyConfig, direct = false) {
1560
1566
  }
1561
1567
  }
1562
1568
  function unconfigureClaudeCode(agent) {
1563
- const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
1564
- if (!fs11.existsSync(configPath)) return;
1569
+ const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1570
+ if (!fs9.existsSync(configPath)) return;
1565
1571
  const config = readJsonFile(configPath);
1566
1572
  if (config === null) {
1567
1573
  console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ANTHROPIC_BASE_URL manually if needed.`);
@@ -1575,42 +1581,42 @@ function unconfigureClaudeCode(agent) {
1575
1581
  delete config.env;
1576
1582
  }
1577
1583
  }
1578
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1584
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1579
1585
  const backupPath = `${configPath}.skalpel-backup`;
1580
- if (fs11.existsSync(backupPath)) {
1581
- fs11.unlinkSync(backupPath);
1586
+ if (fs9.existsSync(backupPath)) {
1587
+ fs9.unlinkSync(backupPath);
1582
1588
  }
1583
1589
  }
1584
1590
  function unconfigureCodex(agent) {
1585
- const configDir = process.platform === "win32" ? path12.join(os7.homedir(), "AppData", "Roaming", "codex") : path12.join(os7.homedir(), ".codex");
1586
- const configPath = agent.configPath ?? path12.join(configDir, "config.toml");
1587
- 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)) {
1588
1594
  let content = readTomlFile(configPath);
1589
1595
  content = removeTomlKey(content, "openai_base_url");
1590
1596
  content = removeTomlKey(content, "model_provider");
1591
1597
  content = removeCodexDirectProvider(content);
1592
1598
  content = removeCodexProxyProvider(content);
1593
- fs11.writeFileSync(configPath, content);
1599
+ fs9.writeFileSync(configPath, content);
1594
1600
  }
1595
1601
  const backupPath = `${configPath}.skalpel-backup`;
1596
- if (fs11.existsSync(backupPath)) {
1597
- fs11.unlinkSync(backupPath);
1602
+ if (fs9.existsSync(backupPath)) {
1603
+ fs9.unlinkSync(backupPath);
1598
1604
  }
1599
1605
  }
1600
1606
  function unconfigureCursor(agent) {
1601
1607
  const configDir = getCursorConfigDir();
1602
- const configPath = agent.configPath ?? path12.join(configDir, "settings.json");
1603
- if (!fs11.existsSync(configPath)) return;
1608
+ const configPath = agent.configPath ?? path10.join(configDir, "settings.json");
1609
+ if (!fs9.existsSync(configPath)) return;
1604
1610
  const config = readJsonFile(configPath);
1605
1611
  if (config === null) {
1606
1612
  console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ${CURSOR_API_BASE_URL_KEY} manually if needed.`);
1607
1613
  return;
1608
1614
  }
1609
1615
  delete config[CURSOR_API_BASE_URL_KEY];
1610
- fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1616
+ fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1611
1617
  const backupPath = `${configPath}.skalpel-backup`;
1612
- if (fs11.existsSync(backupPath)) {
1613
- fs11.unlinkSync(backupPath);
1618
+ if (fs9.existsSync(backupPath)) {
1619
+ fs9.unlinkSync(backupPath);
1614
1620
  }
1615
1621
  }
1616
1622
  function unconfigureAgent(agent) {
@@ -1628,8 +1634,8 @@ function unconfigureAgent(agent) {
1628
1634
  }
1629
1635
 
1630
1636
  // src/cli/agents/shell.ts
1631
- import fs12 from "fs";
1632
- import path13 from "path";
1637
+ import fs10 from "fs";
1638
+ import path11 from "path";
1633
1639
  import os8 from "os";
1634
1640
  var BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
1635
1641
  var END_MARKER = "# END SKALPEL PROXY";
@@ -1638,21 +1644,21 @@ var PS_END_MARKER = "# END SKALPEL PROXY";
1638
1644
  function getUnixProfilePaths() {
1639
1645
  const home = os8.homedir();
1640
1646
  const candidates = [
1641
- path13.join(home, ".bashrc"),
1642
- path13.join(home, ".zshrc"),
1643
- path13.join(home, ".bash_profile"),
1644
- 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")
1645
1651
  ];
1646
- return candidates.filter((p) => fs12.existsSync(p));
1652
+ return candidates.filter((p) => fs10.existsSync(p));
1647
1653
  }
1648
1654
  function getPowerShellProfilePath() {
1649
1655
  if (process.platform !== "win32") return null;
1650
1656
  if (process.env.PROFILE) return process.env.PROFILE;
1651
- const docsDir = path13.join(os8.homedir(), "Documents");
1652
- const psProfile = path13.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
1653
- const wpProfile = path13.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
1654
- if (fs12.existsSync(psProfile)) return psProfile;
1655
- 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;
1656
1662
  return psProfile;
1657
1663
  }
1658
1664
  function generateUnixBlock(proxyConfig) {
@@ -1673,13 +1679,13 @@ function generatePowerShellBlock(proxyConfig) {
1673
1679
  }
1674
1680
  function createBackup2(filePath) {
1675
1681
  const backupPath = `${filePath}.skalpel-backup`;
1676
- fs12.copyFileSync(filePath, backupPath);
1682
+ fs10.copyFileSync(filePath, backupPath);
1677
1683
  }
1678
1684
  function updateProfileFile(filePath, block, beginMarker, endMarker) {
1679
- if (fs12.existsSync(filePath)) {
1685
+ if (fs10.existsSync(filePath)) {
1680
1686
  createBackup2(filePath);
1681
1687
  }
1682
- let content = fs12.existsSync(filePath) ? fs12.readFileSync(filePath, "utf-8") : "";
1688
+ let content = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf-8") : "";
1683
1689
  const beginIdx = content.indexOf(beginMarker);
1684
1690
  const endIdx = content.indexOf(endMarker);
1685
1691
  if (beginIdx !== -1 && endIdx !== -1) {
@@ -1692,15 +1698,15 @@ function updateProfileFile(filePath, block, beginMarker, endMarker) {
1692
1698
  content = block + "\n";
1693
1699
  }
1694
1700
  }
1695
- fs12.writeFileSync(filePath, content);
1701
+ fs10.writeFileSync(filePath, content);
1696
1702
  }
1697
1703
  function configureShellEnvVars(_agents, proxyConfig) {
1698
1704
  const modified = [];
1699
1705
  if (process.platform === "win32") {
1700
1706
  const psProfile = getPowerShellProfilePath();
1701
1707
  if (psProfile) {
1702
- const dir = path13.dirname(psProfile);
1703
- fs12.mkdirSync(dir, { recursive: true });
1708
+ const dir = path11.dirname(psProfile);
1709
+ fs10.mkdirSync(dir, { recursive: true });
1704
1710
  const block = generatePowerShellBlock(proxyConfig);
1705
1711
  updateProfileFile(psProfile, block, PS_BEGIN_MARKER, PS_END_MARKER);
1706
1712
  modified.push(psProfile);
@@ -1719,28 +1725,28 @@ function removeShellEnvVars() {
1719
1725
  const restored = [];
1720
1726
  const home = os8.homedir();
1721
1727
  const allProfiles = [
1722
- path13.join(home, ".bashrc"),
1723
- path13.join(home, ".zshrc"),
1724
- path13.join(home, ".bash_profile"),
1725
- 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")
1726
1732
  ];
1727
1733
  if (process.platform === "win32") {
1728
1734
  const psProfile = getPowerShellProfilePath();
1729
1735
  if (psProfile) allProfiles.push(psProfile);
1730
1736
  }
1731
1737
  for (const profilePath of allProfiles) {
1732
- if (!fs12.existsSync(profilePath)) continue;
1733
- const content = fs12.readFileSync(profilePath, "utf-8");
1738
+ if (!fs10.existsSync(profilePath)) continue;
1739
+ const content = fs10.readFileSync(profilePath, "utf-8");
1734
1740
  const beginIdx = content.indexOf(BEGIN_MARKER);
1735
1741
  const endIdx = content.indexOf(END_MARKER);
1736
1742
  if (beginIdx === -1 || endIdx === -1) continue;
1737
1743
  const before = content.slice(0, beginIdx);
1738
1744
  const after = content.slice(endIdx + END_MARKER.length);
1739
1745
  const cleaned = (before.replace(/\n+$/, "") + after.replace(/^\n+/, "\n")).trimEnd() + "\n";
1740
- fs12.writeFileSync(profilePath, cleaned);
1746
+ fs10.writeFileSync(profilePath, cleaned);
1741
1747
  const backupPath = `${profilePath}.skalpel-backup`;
1742
- if (fs12.existsSync(backupPath)) {
1743
- fs12.unlinkSync(backupPath);
1748
+ if (fs10.existsSync(backupPath)) {
1749
+ fs10.unlinkSync(backupPath);
1744
1750
  }
1745
1751
  restored.push(profilePath);
1746
1752
  }
@@ -1753,6 +1759,226 @@ function removeShellBlock() {
1753
1759
  return removeShellEnvVars();
1754
1760
  }
1755
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
+
1756
1982
  // src/cli/config-cmd.ts
1757
1983
  function print9(msg) {
1758
1984
  console.log(msg);