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 +588 -247
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/proxy-runner.js +592 -76
- package/dist/cli/proxy-runner.js.map +1 -1
- package/dist/index.cjs +1580 -1045
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1570 -1028
- package/dist/index.js.map +1 -1
- package/dist/proxy/index.cjs +628 -93
- package/dist/proxy/index.cjs.map +1 -1
- package/dist/proxy/index.d.cts +1 -1
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.js +621 -79
- package/dist/proxy/index.js.map +1 -1
- package/package.json +4 -2
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
|
-
|
|
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
|
|
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
|
|
1303
|
-
import
|
|
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
|
-
|
|
1388
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
1310
1389
|
}
|
|
1311
1390
|
function createBackup(filePath) {
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
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(
|
|
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 ??
|
|
1325
|
-
const configDir =
|
|
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
|
-
|
|
1419
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1341
1420
|
}
|
|
1342
1421
|
function readTomlFile(filePath) {
|
|
1343
1422
|
try {
|
|
1344
|
-
return
|
|
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" ?
|
|
1401
|
-
const configPath = agent.configPath ??
|
|
1402
|
-
ensureDir(
|
|
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 =
|
|
1523
|
+
content = setTomlKey(content, "model_provider", CODEX_PROXY_PROVIDER_ID);
|
|
1524
|
+
content = upsertCodexProxyProvider(content, proxyConfig.openaiPort);
|
|
1412
1525
|
content = removeCodexDirectProvider(content);
|
|
1413
1526
|
}
|
|
1414
|
-
|
|
1527
|
+
fs9.writeFileSync(configPath, content);
|
|
1415
1528
|
}
|
|
1416
1529
|
function getCursorConfigDir() {
|
|
1417
1530
|
if (process.platform === "darwin") {
|
|
1418
|
-
return
|
|
1531
|
+
return path10.join(os7.homedir(), "Library", "Application Support", "Cursor", "User");
|
|
1419
1532
|
} else if (process.platform === "win32") {
|
|
1420
|
-
return
|
|
1533
|
+
return path10.join(process.env.APPDATA ?? path10.join(os7.homedir(), "AppData", "Roaming"), "Cursor", "User");
|
|
1421
1534
|
}
|
|
1422
|
-
return
|
|
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 ??
|
|
1431
|
-
ensureDir(
|
|
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
|
-
|
|
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 ??
|
|
1457
|
-
if (!
|
|
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
|
-
|
|
1584
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1472
1585
|
const backupPath = `${configPath}.skalpel-backup`;
|
|
1473
|
-
if (
|
|
1474
|
-
|
|
1586
|
+
if (fs9.existsSync(backupPath)) {
|
|
1587
|
+
fs9.unlinkSync(backupPath);
|
|
1475
1588
|
}
|
|
1476
1589
|
}
|
|
1477
1590
|
function unconfigureCodex(agent) {
|
|
1478
|
-
const configDir = process.platform === "win32" ?
|
|
1479
|
-
const configPath = agent.configPath ??
|
|
1480
|
-
if (
|
|
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
|
-
|
|
1598
|
+
content = removeCodexProxyProvider(content);
|
|
1599
|
+
fs9.writeFileSync(configPath, content);
|
|
1486
1600
|
}
|
|
1487
1601
|
const backupPath = `${configPath}.skalpel-backup`;
|
|
1488
|
-
if (
|
|
1489
|
-
|
|
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 ??
|
|
1495
|
-
if (!
|
|
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
|
-
|
|
1616
|
+
fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1503
1617
|
const backupPath = `${configPath}.skalpel-backup`;
|
|
1504
|
-
if (
|
|
1505
|
-
|
|
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
|
|
1524
|
-
import
|
|
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
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
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) =>
|
|
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 =
|
|
1544
|
-
const psProfile =
|
|
1545
|
-
const wpProfile =
|
|
1546
|
-
if (
|
|
1547
|
-
if (
|
|
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
|
-
|
|
1682
|
+
fs10.copyFileSync(filePath, backupPath);
|
|
1569
1683
|
}
|
|
1570
1684
|
function updateProfileFile(filePath, block, beginMarker, endMarker) {
|
|
1571
|
-
if (
|
|
1685
|
+
if (fs10.existsSync(filePath)) {
|
|
1572
1686
|
createBackup2(filePath);
|
|
1573
1687
|
}
|
|
1574
|
-
let content =
|
|
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
|
-
|
|
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 =
|
|
1595
|
-
|
|
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
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
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 (!
|
|
1625
|
-
const content =
|
|
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
|
-
|
|
1746
|
+
fs10.writeFileSync(profilePath, cleaned);
|
|
1633
1747
|
const backupPath = `${profilePath}.skalpel-backup`;
|
|
1634
|
-
if (
|
|
1635
|
-
|
|
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 {
|