skalpel 2.0.2 → 2.0.5
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 +241 -217
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/proxy-runner.js +21 -9
- package/dist/cli/proxy-runner.js.map +1 -1
- package/dist/index.cjs +21 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +21 -9
- package/dist/index.js.map +1 -1
- package/dist/proxy/index.cjs +21 -9
- package/dist/proxy/index.cjs.map +1 -1
- package/dist/proxy/index.js +21 -9
- package/dist/proxy/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -204,79 +204,176 @@ ${envContent}`);
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
// src/cli/doctor.ts
|
|
207
|
-
import * as
|
|
208
|
-
import * as
|
|
207
|
+
import * as fs4 from "fs";
|
|
208
|
+
import * as path4 from "path";
|
|
209
|
+
import * as os2 from "os";
|
|
210
|
+
|
|
211
|
+
// src/cli/agents/detect.ts
|
|
212
|
+
import { execSync } from "child_process";
|
|
213
|
+
import fs3 from "fs";
|
|
214
|
+
import path3 from "path";
|
|
215
|
+
import os from "os";
|
|
216
|
+
function whichCommand() {
|
|
217
|
+
return process.platform === "win32" ? "where" : "which";
|
|
218
|
+
}
|
|
219
|
+
function tryExec(cmd) {
|
|
220
|
+
try {
|
|
221
|
+
return execSync(cmd, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
222
|
+
} catch {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function detectClaudeCode() {
|
|
227
|
+
const agent = {
|
|
228
|
+
name: "claude-code",
|
|
229
|
+
installed: false,
|
|
230
|
+
version: null,
|
|
231
|
+
configPath: null
|
|
232
|
+
};
|
|
233
|
+
const binaryPath = tryExec(`${whichCommand()} claude`);
|
|
234
|
+
const hasBinary = binaryPath !== null && binaryPath.length > 0;
|
|
235
|
+
const claudeDir = path3.join(os.homedir(), ".claude");
|
|
236
|
+
const hasConfigDir = fs3.existsSync(claudeDir);
|
|
237
|
+
agent.installed = hasBinary || hasConfigDir;
|
|
238
|
+
if (hasBinary) {
|
|
239
|
+
const versionOutput = tryExec("claude --version");
|
|
240
|
+
if (versionOutput) {
|
|
241
|
+
const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
|
|
242
|
+
agent.version = match ? match[1] : versionOutput;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const settingsPath = path3.join(claudeDir, "settings.json");
|
|
246
|
+
if (fs3.existsSync(settingsPath)) {
|
|
247
|
+
agent.configPath = settingsPath;
|
|
248
|
+
} else if (hasConfigDir) {
|
|
249
|
+
agent.configPath = settingsPath;
|
|
250
|
+
}
|
|
251
|
+
return agent;
|
|
252
|
+
}
|
|
253
|
+
function detectCodex() {
|
|
254
|
+
const agent = {
|
|
255
|
+
name: "codex",
|
|
256
|
+
installed: false,
|
|
257
|
+
version: null,
|
|
258
|
+
configPath: null
|
|
259
|
+
};
|
|
260
|
+
const binaryPath = tryExec(`${whichCommand()} codex`);
|
|
261
|
+
const hasBinary = binaryPath !== null && binaryPath.length > 0;
|
|
262
|
+
const codexConfigDir = process.platform === "win32" ? path3.join(os.homedir(), "AppData", "Roaming", "codex") : path3.join(os.homedir(), ".codex");
|
|
263
|
+
const hasConfigDir = fs3.existsSync(codexConfigDir);
|
|
264
|
+
agent.installed = hasBinary || hasConfigDir;
|
|
265
|
+
if (hasBinary) {
|
|
266
|
+
const versionOutput = tryExec("codex --version");
|
|
267
|
+
if (versionOutput) {
|
|
268
|
+
const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
|
|
269
|
+
agent.version = match ? match[1] : versionOutput;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const configFile = path3.join(codexConfigDir, "config.toml");
|
|
273
|
+
if (fs3.existsSync(configFile)) {
|
|
274
|
+
agent.configPath = configFile;
|
|
275
|
+
} else if (hasConfigDir) {
|
|
276
|
+
agent.configPath = configFile;
|
|
277
|
+
}
|
|
278
|
+
return agent;
|
|
279
|
+
}
|
|
280
|
+
function detectAgents() {
|
|
281
|
+
return [detectClaudeCode(), detectCodex()];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/cli/doctor.ts
|
|
209
285
|
function print2(msg) {
|
|
210
286
|
console.log(msg);
|
|
211
287
|
}
|
|
288
|
+
function loadConfigApiKey() {
|
|
289
|
+
try {
|
|
290
|
+
const configPath = path4.join(os2.homedir(), ".skalpel", "config.json");
|
|
291
|
+
const raw = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
292
|
+
if (typeof raw.apiKey === "string" && raw.apiKey.length > 0) {
|
|
293
|
+
return raw.apiKey;
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
212
299
|
async function runDoctor() {
|
|
213
300
|
print2("");
|
|
214
301
|
print2(" Skalpel Doctor");
|
|
215
302
|
print2(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
216
303
|
print2("");
|
|
217
304
|
const checks = [];
|
|
218
|
-
const
|
|
219
|
-
|
|
305
|
+
const configKey = loadConfigApiKey();
|
|
306
|
+
const envKey = process.env.SKALPEL_API_KEY ?? "";
|
|
307
|
+
const apiKey = configKey || envKey;
|
|
308
|
+
if (apiKey && validateApiKey(apiKey)) {
|
|
309
|
+
const source = configKey ? "~/.skalpel/config.json" : "environment";
|
|
220
310
|
checks.push({
|
|
221
311
|
name: "API Key",
|
|
222
|
-
status: "
|
|
223
|
-
message:
|
|
312
|
+
status: "ok",
|
|
313
|
+
message: `Valid key from ${source}: ${apiKey.slice(0, 14)}${"*".repeat(Math.max(0, apiKey.length - 14))}`
|
|
224
314
|
});
|
|
225
|
-
} else if (
|
|
315
|
+
} else if (apiKey) {
|
|
226
316
|
checks.push({
|
|
227
317
|
name: "API Key",
|
|
228
318
|
status: "fail",
|
|
229
|
-
message: `Invalid format \u2014 must start with "sk-skalpel-" and be >= 20 chars
|
|
319
|
+
message: `Invalid format \u2014 must start with "sk-skalpel-" and be >= 20 chars`
|
|
230
320
|
});
|
|
231
321
|
} else {
|
|
232
322
|
checks.push({
|
|
233
323
|
name: "API Key",
|
|
234
|
-
status: "
|
|
235
|
-
message:
|
|
324
|
+
status: "fail",
|
|
325
|
+
message: 'No API key found. Run "npx skalpel" to set up.'
|
|
236
326
|
});
|
|
237
327
|
}
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
if (content.includes("SKALPEL_API_KEY")) {
|
|
242
|
-
checks.push({ name: ".env file", status: "ok", message: "Found SKALPEL_API_KEY in .env" });
|
|
243
|
-
} else {
|
|
244
|
-
checks.push({ name: ".env file", status: "warn", message: ".env exists but no SKALPEL_API_KEY entry" });
|
|
245
|
-
}
|
|
328
|
+
const skalpelConfigPath = path4.join(os2.homedir(), ".skalpel", "config.json");
|
|
329
|
+
if (fs4.existsSync(skalpelConfigPath)) {
|
|
330
|
+
checks.push({ name: "Skalpel config", status: "ok", message: "~/.skalpel/config.json found" });
|
|
246
331
|
} else {
|
|
247
|
-
checks.push({ name: "
|
|
332
|
+
checks.push({ name: "Skalpel config", status: "warn", message: 'No ~/.skalpel/config.json \u2014 run "npx skalpel" to set up' });
|
|
248
333
|
}
|
|
249
|
-
const baseURL =
|
|
334
|
+
const baseURL = "https://api.skalpel.ai";
|
|
250
335
|
try {
|
|
251
336
|
const controller = new AbortController();
|
|
252
337
|
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
253
338
|
const response = await fetch(`${baseURL}/health`, { signal: controller.signal });
|
|
254
339
|
clearTimeout(timeout);
|
|
255
340
|
if (response.ok) {
|
|
256
|
-
checks.push({ name: "
|
|
341
|
+
checks.push({ name: "Skalpel backend", status: "ok", message: `${baseURL} reachable (HTTP ${response.status})` });
|
|
257
342
|
} else {
|
|
258
|
-
checks.push({ name: "
|
|
343
|
+
checks.push({ name: "Skalpel backend", status: "warn", message: `${baseURL} responded with HTTP ${response.status}` });
|
|
259
344
|
}
|
|
260
345
|
} catch (err) {
|
|
261
346
|
const msg = err instanceof Error ? err.message : String(err);
|
|
262
|
-
checks.push({ name: "
|
|
347
|
+
checks.push({ name: "Skalpel backend", status: "fail", message: `Cannot reach ${baseURL} \u2014 ${msg}` });
|
|
263
348
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
349
|
+
let proxyPort = 18100;
|
|
350
|
+
try {
|
|
351
|
+
const raw = JSON.parse(fs4.readFileSync(skalpelConfigPath, "utf-8"));
|
|
352
|
+
proxyPort = raw.anthropicPort ?? 18100;
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
const controller = new AbortController();
|
|
357
|
+
const timeout = setTimeout(() => controller.abort(), 2e3);
|
|
358
|
+
const res = await fetch(`http://localhost:${proxyPort}/health`, { signal: controller.signal });
|
|
359
|
+
clearTimeout(timeout);
|
|
360
|
+
if (res.ok) {
|
|
361
|
+
checks.push({ name: "Local proxy", status: "ok", message: `Running on port ${proxyPort}` });
|
|
362
|
+
} else {
|
|
363
|
+
checks.push({ name: "Local proxy", status: "warn", message: `Port ${proxyPort} responded with HTTP ${res.status}` });
|
|
364
|
+
}
|
|
365
|
+
} catch {
|
|
366
|
+
checks.push({ name: "Local proxy", status: "warn", message: `Not running on port ${proxyPort}. Run "npx skalpel start" to start.` });
|
|
367
|
+
}
|
|
368
|
+
const agents = detectAgents();
|
|
369
|
+
for (const agent of agents) {
|
|
370
|
+
if (agent.installed) {
|
|
371
|
+
const ver = agent.version ? ` v${agent.version}` : "";
|
|
372
|
+
const configured = agent.configPath && fs4.existsSync(agent.configPath) ? " (configured)" : "";
|
|
373
|
+
checks.push({ name: agent.name, status: "ok", message: `Installed${ver}${configured}` });
|
|
374
|
+
} else {
|
|
375
|
+
checks.push({ name: agent.name, status: "warn", message: "Not installed" });
|
|
376
|
+
}
|
|
280
377
|
}
|
|
281
378
|
const icons = { ok: "+", warn: "!", fail: "x" };
|
|
282
379
|
for (const check of checks) {
|
|
@@ -389,8 +486,8 @@ async function runBenchmark() {
|
|
|
389
486
|
}
|
|
390
487
|
|
|
391
488
|
// src/cli/replay.ts
|
|
392
|
-
import * as
|
|
393
|
-
import * as
|
|
489
|
+
import * as fs5 from "fs";
|
|
490
|
+
import * as path5 from "path";
|
|
394
491
|
function print4(msg) {
|
|
395
492
|
console.log(msg);
|
|
396
493
|
}
|
|
@@ -420,16 +517,16 @@ async function runReplay(filePaths) {
|
|
|
420
517
|
let successCount = 0;
|
|
421
518
|
let failCount = 0;
|
|
422
519
|
for (const filePath of filePaths) {
|
|
423
|
-
const resolved =
|
|
520
|
+
const resolved = path5.resolve(filePath);
|
|
424
521
|
print4(` File: ${resolved}`);
|
|
425
|
-
if (!
|
|
522
|
+
if (!fs5.existsSync(resolved)) {
|
|
426
523
|
print4(` Error: file not found`);
|
|
427
524
|
failCount++;
|
|
428
525
|
continue;
|
|
429
526
|
}
|
|
430
527
|
let requestBody;
|
|
431
528
|
try {
|
|
432
|
-
const raw =
|
|
529
|
+
const raw = fs5.readFileSync(resolved, "utf-8");
|
|
433
530
|
requestBody = JSON.parse(raw);
|
|
434
531
|
} catch (err) {
|
|
435
532
|
print4(` Error: invalid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -481,16 +578,16 @@ async function runReplay(filePaths) {
|
|
|
481
578
|
|
|
482
579
|
// src/cli/start.ts
|
|
483
580
|
import { spawn } from "child_process";
|
|
484
|
-
import
|
|
581
|
+
import path10 from "path";
|
|
485
582
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
486
583
|
|
|
487
584
|
// src/proxy/config.ts
|
|
488
|
-
import
|
|
489
|
-
import
|
|
490
|
-
import
|
|
585
|
+
import fs6 from "fs";
|
|
586
|
+
import path6 from "path";
|
|
587
|
+
import os3 from "os";
|
|
491
588
|
function expandHome(filePath) {
|
|
492
589
|
if (filePath.startsWith("~")) {
|
|
493
|
-
return
|
|
590
|
+
return path6.join(os3.homedir(), filePath.slice(1));
|
|
494
591
|
}
|
|
495
592
|
return filePath;
|
|
496
593
|
}
|
|
@@ -509,7 +606,7 @@ function loadConfig(configPath) {
|
|
|
509
606
|
const filePath = expandHome(configPath ?? DEFAULTS.configFile);
|
|
510
607
|
let fileConfig = {};
|
|
511
608
|
try {
|
|
512
|
-
const raw =
|
|
609
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
513
610
|
fileConfig = JSON.parse(raw);
|
|
514
611
|
} catch {
|
|
515
612
|
}
|
|
@@ -526,17 +623,17 @@ function loadConfig(configPath) {
|
|
|
526
623
|
};
|
|
527
624
|
}
|
|
528
625
|
function saveConfig(config) {
|
|
529
|
-
const dir =
|
|
530
|
-
|
|
531
|
-
|
|
626
|
+
const dir = path6.dirname(config.configFile);
|
|
627
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
628
|
+
fs6.writeFileSync(config.configFile, JSON.stringify(config, null, 2) + "\n");
|
|
532
629
|
}
|
|
533
630
|
|
|
534
631
|
// src/proxy/pid.ts
|
|
535
|
-
import
|
|
536
|
-
import
|
|
632
|
+
import fs7 from "fs";
|
|
633
|
+
import path7 from "path";
|
|
537
634
|
function readPid(pidFile) {
|
|
538
635
|
try {
|
|
539
|
-
const raw =
|
|
636
|
+
const raw = fs7.readFileSync(pidFile, "utf-8").trim();
|
|
540
637
|
const pid = parseInt(raw, 10);
|
|
541
638
|
if (isNaN(pid)) return null;
|
|
542
639
|
return isRunning(pid) ? pid : null;
|
|
@@ -554,21 +651,21 @@ function isRunning(pid) {
|
|
|
554
651
|
}
|
|
555
652
|
function removePid(pidFile) {
|
|
556
653
|
try {
|
|
557
|
-
|
|
654
|
+
fs7.unlinkSync(pidFile);
|
|
558
655
|
} catch {
|
|
559
656
|
}
|
|
560
657
|
}
|
|
561
658
|
|
|
562
659
|
// src/cli/service/install.ts
|
|
563
|
-
import
|
|
564
|
-
import
|
|
565
|
-
import
|
|
566
|
-
import { execSync as
|
|
660
|
+
import fs8 from "fs";
|
|
661
|
+
import path9 from "path";
|
|
662
|
+
import os6 from "os";
|
|
663
|
+
import { execSync as execSync3 } from "child_process";
|
|
567
664
|
import { fileURLToPath } from "url";
|
|
568
665
|
|
|
569
666
|
// src/cli/service/detect-os.ts
|
|
570
|
-
import
|
|
571
|
-
import { execSync } from "child_process";
|
|
667
|
+
import os4 from "os";
|
|
668
|
+
import { execSync as execSync2 } from "child_process";
|
|
572
669
|
function detectShell() {
|
|
573
670
|
if (process.platform === "win32") {
|
|
574
671
|
if (process.env.PSModulePath || process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
|
|
@@ -582,7 +679,7 @@ function detectShell() {
|
|
|
582
679
|
if (shellPath.includes("bash")) return "bash";
|
|
583
680
|
try {
|
|
584
681
|
if (process.platform === "darwin") {
|
|
585
|
-
const result =
|
|
682
|
+
const result = execSync2(`dscl . -read /Users/${os4.userInfo().username} UserShell`, {
|
|
586
683
|
encoding: "utf-8",
|
|
587
684
|
timeout: 3e3
|
|
588
685
|
}).trim();
|
|
@@ -591,7 +688,7 @@ function detectShell() {
|
|
|
591
688
|
if (shell.includes("fish")) return "fish";
|
|
592
689
|
if (shell.includes("bash")) return "bash";
|
|
593
690
|
} else {
|
|
594
|
-
const result =
|
|
691
|
+
const result = execSync2(`getent passwd ${os4.userInfo().username}`, {
|
|
595
692
|
encoding: "utf-8",
|
|
596
693
|
timeout: 3e3
|
|
597
694
|
}).trim();
|
|
@@ -620,15 +717,15 @@ function detectOS() {
|
|
|
620
717
|
return {
|
|
621
718
|
platform,
|
|
622
719
|
shell: detectShell(),
|
|
623
|
-
homeDir:
|
|
720
|
+
homeDir: os4.homedir()
|
|
624
721
|
};
|
|
625
722
|
}
|
|
626
723
|
|
|
627
724
|
// src/cli/service/templates.ts
|
|
628
|
-
import
|
|
629
|
-
import
|
|
725
|
+
import os5 from "os";
|
|
726
|
+
import path8 from "path";
|
|
630
727
|
function generateLaunchdPlist(config, proxyRunnerPath) {
|
|
631
|
-
const logDir =
|
|
728
|
+
const logDir = path8.join(os5.homedir(), ".skalpel", "logs");
|
|
632
729
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
633
730
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
634
731
|
<plist version="1.0">
|
|
@@ -645,9 +742,9 @@ function generateLaunchdPlist(config, proxyRunnerPath) {
|
|
|
645
742
|
<key>KeepAlive</key>
|
|
646
743
|
<true/>
|
|
647
744
|
<key>StandardOutPath</key>
|
|
648
|
-
<string>${
|
|
745
|
+
<string>${path8.join(logDir, "proxy-stdout.log")}</string>
|
|
649
746
|
<key>StandardErrorPath</key>
|
|
650
|
-
<string>${
|
|
747
|
+
<string>${path8.join(logDir, "proxy-stderr.log")}</string>
|
|
651
748
|
<key>EnvironmentVariables</key>
|
|
652
749
|
<dict>
|
|
653
750
|
<key>SKALPEL_ANTHROPIC_PORT</key>
|
|
@@ -690,51 +787,51 @@ function generateWindowsTask(config, proxyRunnerPath) {
|
|
|
690
787
|
}
|
|
691
788
|
|
|
692
789
|
// src/cli/service/install.ts
|
|
693
|
-
var __dirname =
|
|
790
|
+
var __dirname = path9.dirname(fileURLToPath(import.meta.url));
|
|
694
791
|
function resolveProxyRunnerPath() {
|
|
695
792
|
const candidates = [
|
|
696
|
-
|
|
793
|
+
path9.join(__dirname, "..", "proxy-runner.js"),
|
|
697
794
|
// dist/cli/proxy-runner.js relative to dist/cli/service/
|
|
698
|
-
|
|
795
|
+
path9.join(__dirname, "proxy-runner.js"),
|
|
699
796
|
// same dir
|
|
700
|
-
|
|
797
|
+
path9.join(__dirname, "..", "..", "cli", "proxy-runner.js")
|
|
701
798
|
// dist/cli/proxy-runner.js from deeper
|
|
702
799
|
];
|
|
703
800
|
for (const candidate of candidates) {
|
|
704
|
-
if (
|
|
705
|
-
return
|
|
801
|
+
if (fs8.existsSync(candidate)) {
|
|
802
|
+
return path9.resolve(candidate);
|
|
706
803
|
}
|
|
707
804
|
}
|
|
708
805
|
try {
|
|
709
|
-
const npmRoot =
|
|
710
|
-
const globalPath =
|
|
711
|
-
if (
|
|
806
|
+
const npmRoot = execSync3("npm root -g", { encoding: "utf-8" }).trim();
|
|
807
|
+
const globalPath = path9.join(npmRoot, "skalpel", "dist", "cli", "proxy-runner.js");
|
|
808
|
+
if (fs8.existsSync(globalPath)) return globalPath;
|
|
712
809
|
} catch {
|
|
713
810
|
}
|
|
714
|
-
const devPath =
|
|
811
|
+
const devPath = path9.resolve(process.cwd(), "dist", "cli", "proxy-runner.js");
|
|
715
812
|
return devPath;
|
|
716
813
|
}
|
|
717
814
|
function getMacOSPlistPath() {
|
|
718
|
-
return
|
|
815
|
+
return path9.join(os6.homedir(), "Library", "LaunchAgents", "ai.skalpel.proxy.plist");
|
|
719
816
|
}
|
|
720
817
|
function getLinuxUnitPath() {
|
|
721
|
-
return
|
|
818
|
+
return path9.join(os6.homedir(), ".config", "systemd", "user", "skalpel-proxy.service");
|
|
722
819
|
}
|
|
723
820
|
function installService(config) {
|
|
724
821
|
const osInfo = detectOS();
|
|
725
822
|
const proxyRunnerPath = resolveProxyRunnerPath();
|
|
726
|
-
const logDir =
|
|
727
|
-
|
|
823
|
+
const logDir = path9.join(os6.homedir(), ".skalpel", "logs");
|
|
824
|
+
fs8.mkdirSync(logDir, { recursive: true });
|
|
728
825
|
switch (osInfo.platform) {
|
|
729
826
|
case "macos": {
|
|
730
827
|
const plistPath = getMacOSPlistPath();
|
|
731
|
-
const plistDir =
|
|
732
|
-
|
|
828
|
+
const plistDir = path9.dirname(plistPath);
|
|
829
|
+
fs8.mkdirSync(plistDir, { recursive: true });
|
|
733
830
|
const plist = generateLaunchdPlist(config, proxyRunnerPath);
|
|
734
|
-
|
|
831
|
+
fs8.writeFileSync(plistPath, plist);
|
|
735
832
|
try {
|
|
736
|
-
|
|
737
|
-
|
|
833
|
+
execSync3(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
|
|
834
|
+
execSync3(`launchctl load "${plistPath}"`, { stdio: "pipe" });
|
|
738
835
|
} catch (err) {
|
|
739
836
|
const msg = err instanceof Error ? err.message : String(err);
|
|
740
837
|
console.warn(` Warning: Could not register launchd service: ${msg}`);
|
|
@@ -744,18 +841,18 @@ function installService(config) {
|
|
|
744
841
|
}
|
|
745
842
|
case "linux": {
|
|
746
843
|
const unitPath = getLinuxUnitPath();
|
|
747
|
-
const unitDir =
|
|
748
|
-
|
|
844
|
+
const unitDir = path9.dirname(unitPath);
|
|
845
|
+
fs8.mkdirSync(unitDir, { recursive: true });
|
|
749
846
|
const unit = generateSystemdUnit(config, proxyRunnerPath);
|
|
750
|
-
|
|
847
|
+
fs8.writeFileSync(unitPath, unit);
|
|
751
848
|
try {
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
849
|
+
execSync3("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
850
|
+
execSync3("systemctl --user enable skalpel-proxy", { stdio: "pipe" });
|
|
851
|
+
execSync3("systemctl --user start skalpel-proxy", { stdio: "pipe" });
|
|
755
852
|
} catch {
|
|
756
853
|
try {
|
|
757
|
-
const autostartDir =
|
|
758
|
-
|
|
854
|
+
const autostartDir = path9.join(os6.homedir(), ".config", "autostart");
|
|
855
|
+
fs8.mkdirSync(autostartDir, { recursive: true });
|
|
759
856
|
const desktopEntry = `[Desktop Entry]
|
|
760
857
|
Type=Application
|
|
761
858
|
Name=Skalpel Proxy
|
|
@@ -764,7 +861,7 @@ Hidden=false
|
|
|
764
861
|
NoDisplay=true
|
|
765
862
|
X-GNOME-Autostart-enabled=true
|
|
766
863
|
`;
|
|
767
|
-
|
|
864
|
+
fs8.writeFileSync(path9.join(autostartDir, "skalpel-proxy.desktop"), desktopEntry);
|
|
768
865
|
console.warn(" Warning: systemd --user not available. Created .desktop autostart entry instead.");
|
|
769
866
|
} catch (err2) {
|
|
770
867
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
@@ -777,7 +874,7 @@ X-GNOME-Autostart-enabled=true
|
|
|
777
874
|
case "windows": {
|
|
778
875
|
const args = generateWindowsTask(config, proxyRunnerPath);
|
|
779
876
|
try {
|
|
780
|
-
|
|
877
|
+
execSync3(`schtasks ${args.join(" ")}`, { stdio: "pipe" });
|
|
781
878
|
} catch (err) {
|
|
782
879
|
const msg = err instanceof Error ? err.message : String(err);
|
|
783
880
|
console.warn(` Warning: Could not create scheduled task: ${msg}`);
|
|
@@ -792,15 +889,15 @@ function isServiceInstalled() {
|
|
|
792
889
|
switch (osInfo.platform) {
|
|
793
890
|
case "macos": {
|
|
794
891
|
const plistPath = getMacOSPlistPath();
|
|
795
|
-
return
|
|
892
|
+
return fs8.existsSync(plistPath);
|
|
796
893
|
}
|
|
797
894
|
case "linux": {
|
|
798
895
|
const unitPath = getLinuxUnitPath();
|
|
799
|
-
return
|
|
896
|
+
return fs8.existsSync(unitPath);
|
|
800
897
|
}
|
|
801
898
|
case "windows": {
|
|
802
899
|
try {
|
|
803
|
-
|
|
900
|
+
execSync3("schtasks /query /tn SkalpelProxy", { stdio: "pipe" });
|
|
804
901
|
return true;
|
|
805
902
|
} catch {
|
|
806
903
|
return false;
|
|
@@ -813,23 +910,23 @@ function stopService() {
|
|
|
813
910
|
switch (osInfo.platform) {
|
|
814
911
|
case "macos": {
|
|
815
912
|
const plistPath = getMacOSPlistPath();
|
|
816
|
-
if (!
|
|
913
|
+
if (!fs8.existsSync(plistPath)) return;
|
|
817
914
|
try {
|
|
818
|
-
|
|
915
|
+
execSync3(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
|
|
819
916
|
} catch {
|
|
820
917
|
}
|
|
821
918
|
break;
|
|
822
919
|
}
|
|
823
920
|
case "linux": {
|
|
824
921
|
try {
|
|
825
|
-
|
|
922
|
+
execSync3("systemctl --user stop skalpel-proxy", { stdio: "pipe" });
|
|
826
923
|
} catch {
|
|
827
924
|
}
|
|
828
925
|
break;
|
|
829
926
|
}
|
|
830
927
|
case "windows": {
|
|
831
928
|
try {
|
|
832
|
-
|
|
929
|
+
execSync3("schtasks /end /tn SkalpelProxy", { stdio: "pipe" });
|
|
833
930
|
} catch {
|
|
834
931
|
}
|
|
835
932
|
break;
|
|
@@ -841,23 +938,23 @@ function startService() {
|
|
|
841
938
|
switch (osInfo.platform) {
|
|
842
939
|
case "macos": {
|
|
843
940
|
const plistPath = getMacOSPlistPath();
|
|
844
|
-
if (!
|
|
941
|
+
if (!fs8.existsSync(plistPath)) return;
|
|
845
942
|
try {
|
|
846
|
-
|
|
943
|
+
execSync3(`launchctl load "${plistPath}"`, { stdio: "pipe" });
|
|
847
944
|
} catch {
|
|
848
945
|
}
|
|
849
946
|
break;
|
|
850
947
|
}
|
|
851
948
|
case "linux": {
|
|
852
949
|
try {
|
|
853
|
-
|
|
950
|
+
execSync3("systemctl --user start skalpel-proxy", { stdio: "pipe" });
|
|
854
951
|
} catch {
|
|
855
952
|
}
|
|
856
953
|
break;
|
|
857
954
|
}
|
|
858
955
|
case "windows": {
|
|
859
956
|
try {
|
|
860
|
-
|
|
957
|
+
execSync3("schtasks /run /tn SkalpelProxy", { stdio: "pipe" });
|
|
861
958
|
} catch {
|
|
862
959
|
}
|
|
863
960
|
break;
|
|
@@ -870,27 +967,27 @@ function uninstallService() {
|
|
|
870
967
|
case "macos": {
|
|
871
968
|
const plistPath = getMacOSPlistPath();
|
|
872
969
|
try {
|
|
873
|
-
|
|
970
|
+
execSync3(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
|
|
874
971
|
} catch {
|
|
875
972
|
}
|
|
876
|
-
if (
|
|
973
|
+
if (fs8.existsSync(plistPath)) fs8.unlinkSync(plistPath);
|
|
877
974
|
break;
|
|
878
975
|
}
|
|
879
976
|
case "linux": {
|
|
880
977
|
try {
|
|
881
|
-
|
|
882
|
-
|
|
978
|
+
execSync3("systemctl --user stop skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
|
|
979
|
+
execSync3("systemctl --user disable skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
|
|
883
980
|
} catch {
|
|
884
981
|
}
|
|
885
982
|
const unitPath = getLinuxUnitPath();
|
|
886
|
-
if (
|
|
887
|
-
const desktopPath =
|
|
888
|
-
if (
|
|
983
|
+
if (fs8.existsSync(unitPath)) fs8.unlinkSync(unitPath);
|
|
984
|
+
const desktopPath = path9.join(os6.homedir(), ".config", "autostart", "skalpel-proxy.desktop");
|
|
985
|
+
if (fs8.existsSync(desktopPath)) fs8.unlinkSync(desktopPath);
|
|
889
986
|
break;
|
|
890
987
|
}
|
|
891
988
|
case "windows": {
|
|
892
989
|
try {
|
|
893
|
-
|
|
990
|
+
execSync3("schtasks /delete /tn SkalpelProxy /f", { stdio: "pipe" });
|
|
894
991
|
} catch {
|
|
895
992
|
}
|
|
896
993
|
break;
|
|
@@ -918,8 +1015,8 @@ async function runStart() {
|
|
|
918
1015
|
print5(` Skalpel proxy started via system service on ports ${config.anthropicPort} and ${config.openaiPort}`);
|
|
919
1016
|
return;
|
|
920
1017
|
}
|
|
921
|
-
const dirname =
|
|
922
|
-
const runnerScript =
|
|
1018
|
+
const dirname = path10.dirname(fileURLToPath2(import.meta.url));
|
|
1019
|
+
const runnerScript = path10.resolve(dirname, "proxy-runner.js");
|
|
923
1020
|
const child = spawn(process.execPath, [runnerScript], {
|
|
924
1021
|
detached: true,
|
|
925
1022
|
stdio: "ignore"
|
|
@@ -932,8 +1029,8 @@ async function runStart() {
|
|
|
932
1029
|
import http from "http";
|
|
933
1030
|
|
|
934
1031
|
// src/proxy/logger.ts
|
|
935
|
-
import
|
|
936
|
-
import
|
|
1032
|
+
import fs9 from "fs";
|
|
1033
|
+
import path11 from "path";
|
|
937
1034
|
var MAX_SIZE = 5 * 1024 * 1024;
|
|
938
1035
|
|
|
939
1036
|
// src/proxy/server.ts
|
|
@@ -997,7 +1094,7 @@ async function runStatus() {
|
|
|
997
1094
|
}
|
|
998
1095
|
|
|
999
1096
|
// src/cli/logs.ts
|
|
1000
|
-
import
|
|
1097
|
+
import fs10 from "fs";
|
|
1001
1098
|
function print8(msg) {
|
|
1002
1099
|
console.log(msg);
|
|
1003
1100
|
}
|
|
@@ -1005,26 +1102,26 @@ async function runLogs(options) {
|
|
|
1005
1102
|
const config = loadConfig();
|
|
1006
1103
|
const logFile = config.logFile;
|
|
1007
1104
|
const lineCount = parseInt(options.lines ?? "50", 10);
|
|
1008
|
-
if (!
|
|
1105
|
+
if (!fs10.existsSync(logFile)) {
|
|
1009
1106
|
print8(` No log file found at ${logFile}`);
|
|
1010
1107
|
return;
|
|
1011
1108
|
}
|
|
1012
|
-
const content =
|
|
1109
|
+
const content = fs10.readFileSync(logFile, "utf-8");
|
|
1013
1110
|
const lines = content.trimEnd().split("\n");
|
|
1014
1111
|
const tail = lines.slice(-lineCount);
|
|
1015
1112
|
for (const line of tail) {
|
|
1016
1113
|
print8(line);
|
|
1017
1114
|
}
|
|
1018
1115
|
if (options.follow) {
|
|
1019
|
-
let position =
|
|
1020
|
-
|
|
1116
|
+
let position = fs10.statSync(logFile).size;
|
|
1117
|
+
fs10.watchFile(logFile, { interval: 500 }, () => {
|
|
1021
1118
|
try {
|
|
1022
|
-
const stat =
|
|
1119
|
+
const stat = fs10.statSync(logFile);
|
|
1023
1120
|
if (stat.size > position) {
|
|
1024
|
-
const fd =
|
|
1121
|
+
const fd = fs10.openSync(logFile, "r");
|
|
1025
1122
|
const buf = Buffer.alloc(stat.size - position);
|
|
1026
|
-
|
|
1027
|
-
|
|
1123
|
+
fs10.readSync(fd, buf, 0, buf.length, position);
|
|
1124
|
+
fs10.closeSync(fd);
|
|
1028
1125
|
process.stdout.write(buf.toString("utf-8"));
|
|
1029
1126
|
position = stat.size;
|
|
1030
1127
|
}
|
|
@@ -1131,85 +1228,12 @@ async function runUpdate() {
|
|
|
1131
1228
|
import * as readline2 from "readline";
|
|
1132
1229
|
import * as fs12 from "fs";
|
|
1133
1230
|
import * as path13 from "path";
|
|
1134
|
-
import * as
|
|
1135
|
-
|
|
1136
|
-
// src/cli/agents/detect.ts
|
|
1137
|
-
import { execSync as execSync3 } from "child_process";
|
|
1138
|
-
import fs10 from "fs";
|
|
1139
|
-
import path11 from "path";
|
|
1140
|
-
import os5 from "os";
|
|
1141
|
-
function whichCommand() {
|
|
1142
|
-
return process.platform === "win32" ? "where" : "which";
|
|
1143
|
-
}
|
|
1144
|
-
function tryExec(cmd) {
|
|
1145
|
-
try {
|
|
1146
|
-
return execSync3(cmd, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1147
|
-
} catch {
|
|
1148
|
-
return null;
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
function detectClaudeCode() {
|
|
1152
|
-
const agent = {
|
|
1153
|
-
name: "claude-code",
|
|
1154
|
-
installed: false,
|
|
1155
|
-
version: null,
|
|
1156
|
-
configPath: null
|
|
1157
|
-
};
|
|
1158
|
-
const binaryPath = tryExec(`${whichCommand()} claude`);
|
|
1159
|
-
const hasBinary = binaryPath !== null && binaryPath.length > 0;
|
|
1160
|
-
const claudeDir = path11.join(os5.homedir(), ".claude");
|
|
1161
|
-
const hasConfigDir = fs10.existsSync(claudeDir);
|
|
1162
|
-
agent.installed = hasBinary || hasConfigDir;
|
|
1163
|
-
if (hasBinary) {
|
|
1164
|
-
const versionOutput = tryExec("claude --version");
|
|
1165
|
-
if (versionOutput) {
|
|
1166
|
-
const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
|
|
1167
|
-
agent.version = match ? match[1] : versionOutput;
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
const settingsPath = path11.join(claudeDir, "settings.json");
|
|
1171
|
-
if (fs10.existsSync(settingsPath)) {
|
|
1172
|
-
agent.configPath = settingsPath;
|
|
1173
|
-
} else if (hasConfigDir) {
|
|
1174
|
-
agent.configPath = settingsPath;
|
|
1175
|
-
}
|
|
1176
|
-
return agent;
|
|
1177
|
-
}
|
|
1178
|
-
function detectCodex() {
|
|
1179
|
-
const agent = {
|
|
1180
|
-
name: "codex",
|
|
1181
|
-
installed: false,
|
|
1182
|
-
version: null,
|
|
1183
|
-
configPath: null
|
|
1184
|
-
};
|
|
1185
|
-
const binaryPath = tryExec(`${whichCommand()} codex`);
|
|
1186
|
-
const hasBinary = binaryPath !== null && binaryPath.length > 0;
|
|
1187
|
-
const codexConfigDir = process.platform === "win32" ? path11.join(os5.homedir(), "AppData", "Roaming", "codex") : path11.join(os5.homedir(), ".codex");
|
|
1188
|
-
const hasConfigDir = fs10.existsSync(codexConfigDir);
|
|
1189
|
-
agent.installed = hasBinary || hasConfigDir;
|
|
1190
|
-
if (hasBinary) {
|
|
1191
|
-
const versionOutput = tryExec("codex --version");
|
|
1192
|
-
if (versionOutput) {
|
|
1193
|
-
const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
|
|
1194
|
-
agent.version = match ? match[1] : versionOutput;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
const configFile = path11.join(codexConfigDir, "config.toml");
|
|
1198
|
-
if (fs10.existsSync(configFile)) {
|
|
1199
|
-
agent.configPath = configFile;
|
|
1200
|
-
} else if (hasConfigDir) {
|
|
1201
|
-
agent.configPath = configFile;
|
|
1202
|
-
}
|
|
1203
|
-
return agent;
|
|
1204
|
-
}
|
|
1205
|
-
function detectAgents() {
|
|
1206
|
-
return [detectClaudeCode(), detectCodex()];
|
|
1207
|
-
}
|
|
1231
|
+
import * as os8 from "os";
|
|
1208
1232
|
|
|
1209
1233
|
// src/cli/agents/configure.ts
|
|
1210
1234
|
import fs11 from "fs";
|
|
1211
1235
|
import path12 from "path";
|
|
1212
|
-
import
|
|
1236
|
+
import os7 from "os";
|
|
1213
1237
|
function ensureDir(dir) {
|
|
1214
1238
|
fs11.mkdirSync(dir, { recursive: true });
|
|
1215
1239
|
}
|
|
@@ -1226,7 +1250,7 @@ function readJsonFile(filePath) {
|
|
|
1226
1250
|
}
|
|
1227
1251
|
}
|
|
1228
1252
|
function configureClaudeCode(agent, proxyConfig) {
|
|
1229
|
-
const configPath = agent.configPath ?? path12.join(
|
|
1253
|
+
const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
|
|
1230
1254
|
const configDir = path12.dirname(configPath);
|
|
1231
1255
|
ensureDir(configDir);
|
|
1232
1256
|
createBackup(configPath);
|
|
@@ -1262,7 +1286,7 @@ function removeTomlKey(content, key) {
|
|
|
1262
1286
|
return content.replace(pattern, "");
|
|
1263
1287
|
}
|
|
1264
1288
|
function configureCodex(agent, proxyConfig) {
|
|
1265
|
-
const configDir = process.platform === "win32" ? path12.join(
|
|
1289
|
+
const configDir = process.platform === "win32" ? path12.join(os7.homedir(), "AppData", "Roaming", "codex") : path12.join(os7.homedir(), ".codex");
|
|
1266
1290
|
const configPath = agent.configPath ?? path12.join(configDir, "config.toml");
|
|
1267
1291
|
ensureDir(path12.dirname(configPath));
|
|
1268
1292
|
createBackup(configPath);
|
|
@@ -1281,7 +1305,7 @@ function configureAgent(agent, proxyConfig) {
|
|
|
1281
1305
|
}
|
|
1282
1306
|
}
|
|
1283
1307
|
function unconfigureClaudeCode(agent) {
|
|
1284
|
-
const configPath = agent.configPath ?? path12.join(
|
|
1308
|
+
const configPath = agent.configPath ?? path12.join(os7.homedir(), ".claude", "settings.json");
|
|
1285
1309
|
const backupPath = `${configPath}.skalpel-backup`;
|
|
1286
1310
|
if (fs11.existsSync(backupPath)) {
|
|
1287
1311
|
fs11.copyFileSync(backupPath, configPath);
|
|
@@ -1299,7 +1323,7 @@ function unconfigureClaudeCode(agent) {
|
|
|
1299
1323
|
fs11.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1300
1324
|
}
|
|
1301
1325
|
function unconfigureCodex(agent) {
|
|
1302
|
-
const configDir = process.platform === "win32" ? path12.join(
|
|
1326
|
+
const configDir = process.platform === "win32" ? path12.join(os7.homedir(), "AppData", "Roaming", "codex") : path12.join(os7.homedir(), ".codex");
|
|
1303
1327
|
const configPath = agent.configPath ?? path12.join(configDir, "config.toml");
|
|
1304
1328
|
const backupPath = `${configPath}.skalpel-backup`;
|
|
1305
1329
|
if (fs11.existsSync(backupPath)) {
|
|
@@ -1358,7 +1382,7 @@ async function runWizard(options) {
|
|
|
1358
1382
|
print11(" Welcome to Skalpel! Let's optimize your coding agent costs.");
|
|
1359
1383
|
print11(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1360
1384
|
print11("");
|
|
1361
|
-
const skalpelDir = path13.join(
|
|
1385
|
+
const skalpelDir = path13.join(os8.homedir(), ".skalpel");
|
|
1362
1386
|
const configPath = path13.join(skalpelDir, "config.json");
|
|
1363
1387
|
let apiKey = "";
|
|
1364
1388
|
if (isAuto && options?.apiKey) {
|
|
@@ -1509,18 +1533,18 @@ async function runWizard(options) {
|
|
|
1509
1533
|
import * as readline3 from "readline";
|
|
1510
1534
|
import * as fs14 from "fs";
|
|
1511
1535
|
import * as path15 from "path";
|
|
1512
|
-
import * as
|
|
1536
|
+
import * as os10 from "os";
|
|
1513
1537
|
|
|
1514
1538
|
// src/cli/agents/shell.ts
|
|
1515
1539
|
import fs13 from "fs";
|
|
1516
1540
|
import path14 from "path";
|
|
1517
|
-
import
|
|
1541
|
+
import os9 from "os";
|
|
1518
1542
|
var BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
|
|
1519
1543
|
var END_MARKER = "# END SKALPEL PROXY";
|
|
1520
1544
|
function getPowerShellProfilePath() {
|
|
1521
1545
|
if (process.platform !== "win32") return null;
|
|
1522
1546
|
if (process.env.PROFILE) return process.env.PROFILE;
|
|
1523
|
-
const docsDir = path14.join(
|
|
1547
|
+
const docsDir = path14.join(os9.homedir(), "Documents");
|
|
1524
1548
|
const psProfile = path14.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
1525
1549
|
const wpProfile = path14.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
1526
1550
|
if (fs13.existsSync(psProfile)) return psProfile;
|
|
@@ -1529,7 +1553,7 @@ function getPowerShellProfilePath() {
|
|
|
1529
1553
|
}
|
|
1530
1554
|
function removeShellEnvVars() {
|
|
1531
1555
|
const restored = [];
|
|
1532
|
-
const home =
|
|
1556
|
+
const home = os9.homedir();
|
|
1533
1557
|
const allProfiles = [
|
|
1534
1558
|
path14.join(home, ".bashrc"),
|
|
1535
1559
|
path14.join(home, ".zshrc"),
|
|
@@ -1631,7 +1655,7 @@ async function runUninstall() {
|
|
|
1631
1655
|
}
|
|
1632
1656
|
}
|
|
1633
1657
|
print12("");
|
|
1634
|
-
const skalpelDir = path15.join(
|
|
1658
|
+
const skalpelDir = path15.join(os10.homedir(), ".skalpel");
|
|
1635
1659
|
if (fs14.existsSync(skalpelDir)) {
|
|
1636
1660
|
const removeDir = await ask(" Remove ~/.skalpel/ directory (contains config and logs)? (y/N): ");
|
|
1637
1661
|
if (removeDir.toLowerCase() === "y") {
|