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 CHANGED
@@ -204,79 +204,176 @@ ${envContent}`);
204
204
  }
205
205
 
206
206
  // src/cli/doctor.ts
207
- import * as fs3 from "fs";
208
- import * as path3 from "path";
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 apiKey = process.env.SKALPEL_API_KEY ?? "";
219
- if (!apiKey) {
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: "fail",
223
- message: "SKALPEL_API_KEY not set in environment"
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 (!validateApiKey(apiKey)) {
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 (got "${apiKey.slice(0, 10)}...")`
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: "ok",
235
- message: `Valid key: ${apiKey.slice(0, 14)}${"*".repeat(Math.max(0, apiKey.length - 14))}`
324
+ status: "fail",
325
+ message: 'No API key found. Run "npx skalpel" to set up.'
236
326
  });
237
327
  }
238
- const envPath = path3.join(process.cwd(), ".env");
239
- if (fs3.existsSync(envPath)) {
240
- const content = fs3.readFileSync(envPath, "utf-8");
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: ".env file", status: "warn", message: "No .env file found in current directory" });
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 = process.env.SKALPEL_BASE_URL ?? "https://api.skalpel.ai";
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: "Proxy endpoint", status: "ok", message: `${baseURL} reachable (HTTP ${response.status})` });
341
+ checks.push({ name: "Skalpel backend", status: "ok", message: `${baseURL} reachable (HTTP ${response.status})` });
257
342
  } else {
258
- checks.push({ name: "Proxy endpoint", status: "warn", message: `${baseURL} responded with HTTP ${response.status}` });
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: "Proxy endpoint", status: "fail", message: `Cannot reach ${baseURL} \u2014 ${msg}` });
347
+ checks.push({ name: "Skalpel backend", status: "fail", message: `Cannot reach ${baseURL} \u2014 ${msg}` });
263
348
  }
264
- const skalpelDir = path3.join(process.cwd(), ".skalpel");
265
- if (fs3.existsSync(path3.join(skalpelDir, "config.json"))) {
266
- checks.push({ name: "Workspace config", status: "ok", message: ".skalpel/config.json found" });
267
- } else {
268
- checks.push({ name: "Workspace config", status: "warn", message: 'No .skalpel/config.json \u2014 run "skalpel init" first' });
269
- }
270
- const hasPackageJson = fs3.existsSync(path3.join(process.cwd(), "package.json"));
271
- const hasPyProject = fs3.existsSync(path3.join(process.cwd(), "pyproject.toml"));
272
- const hasRequirements = fs3.existsSync(path3.join(process.cwd(), "requirements.txt"));
273
- if (hasPackageJson || hasPyProject || hasRequirements) {
274
- const types = [];
275
- if (hasPackageJson) types.push("Node.js");
276
- if (hasPyProject || hasRequirements) types.push("Python");
277
- checks.push({ name: "Project detected", status: "ok", message: types.join(", ") });
278
- } else {
279
- checks.push({ name: "Project detected", status: "warn", message: "No package.json or Python config found" });
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 fs4 from "fs";
393
- import * as path4 from "path";
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 = path4.resolve(filePath);
520
+ const resolved = path5.resolve(filePath);
424
521
  print4(` File: ${resolved}`);
425
- if (!fs4.existsSync(resolved)) {
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 = fs4.readFileSync(resolved, "utf-8");
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 path9 from "path";
581
+ import path10 from "path";
485
582
  import { fileURLToPath as fileURLToPath2 } from "url";
486
583
 
487
584
  // src/proxy/config.ts
488
- import fs5 from "fs";
489
- import path5 from "path";
490
- import os from "os";
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 path5.join(os.homedir(), filePath.slice(1));
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 = fs5.readFileSync(filePath, "utf-8");
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 = path5.dirname(config.configFile);
530
- fs5.mkdirSync(dir, { recursive: true });
531
- fs5.writeFileSync(config.configFile, JSON.stringify(config, null, 2) + "\n");
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 fs6 from "fs";
536
- import path6 from "path";
632
+ import fs7 from "fs";
633
+ import path7 from "path";
537
634
  function readPid(pidFile) {
538
635
  try {
539
- const raw = fs6.readFileSync(pidFile, "utf-8").trim();
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
- fs6.unlinkSync(pidFile);
654
+ fs7.unlinkSync(pidFile);
558
655
  } catch {
559
656
  }
560
657
  }
561
658
 
562
659
  // src/cli/service/install.ts
563
- import fs7 from "fs";
564
- import path8 from "path";
565
- import os4 from "os";
566
- import { execSync as execSync2 } from "child_process";
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 os2 from "os";
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 = execSync(`dscl . -read /Users/${os2.userInfo().username} UserShell`, {
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 = execSync(`getent passwd ${os2.userInfo().username}`, {
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: os2.homedir()
720
+ homeDir: os4.homedir()
624
721
  };
625
722
  }
626
723
 
627
724
  // src/cli/service/templates.ts
628
- import os3 from "os";
629
- import path7 from "path";
725
+ import os5 from "os";
726
+ import path8 from "path";
630
727
  function generateLaunchdPlist(config, proxyRunnerPath) {
631
- const logDir = path7.join(os3.homedir(), ".skalpel", "logs");
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>${path7.join(logDir, "proxy-stdout.log")}</string>
745
+ <string>${path8.join(logDir, "proxy-stdout.log")}</string>
649
746
  <key>StandardErrorPath</key>
650
- <string>${path7.join(logDir, "proxy-stderr.log")}</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 = path8.dirname(fileURLToPath(import.meta.url));
790
+ var __dirname = path9.dirname(fileURLToPath(import.meta.url));
694
791
  function resolveProxyRunnerPath() {
695
792
  const candidates = [
696
- path8.join(__dirname, "..", "proxy-runner.js"),
793
+ path9.join(__dirname, "..", "proxy-runner.js"),
697
794
  // dist/cli/proxy-runner.js relative to dist/cli/service/
698
- path8.join(__dirname, "proxy-runner.js"),
795
+ path9.join(__dirname, "proxy-runner.js"),
699
796
  // same dir
700
- path8.join(__dirname, "..", "..", "cli", "proxy-runner.js")
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 (fs7.existsSync(candidate)) {
705
- return path8.resolve(candidate);
801
+ if (fs8.existsSync(candidate)) {
802
+ return path9.resolve(candidate);
706
803
  }
707
804
  }
708
805
  try {
709
- const npmRoot = execSync2("npm root -g", { encoding: "utf-8" }).trim();
710
- const globalPath = path8.join(npmRoot, "skalpel", "dist", "cli", "proxy-runner.js");
711
- if (fs7.existsSync(globalPath)) return globalPath;
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 = path8.resolve(process.cwd(), "dist", "cli", "proxy-runner.js");
811
+ const devPath = path9.resolve(process.cwd(), "dist", "cli", "proxy-runner.js");
715
812
  return devPath;
716
813
  }
717
814
  function getMacOSPlistPath() {
718
- return path8.join(os4.homedir(), "Library", "LaunchAgents", "ai.skalpel.proxy.plist");
815
+ return path9.join(os6.homedir(), "Library", "LaunchAgents", "ai.skalpel.proxy.plist");
719
816
  }
720
817
  function getLinuxUnitPath() {
721
- return path8.join(os4.homedir(), ".config", "systemd", "user", "skalpel-proxy.service");
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 = path8.join(os4.homedir(), ".skalpel", "logs");
727
- fs7.mkdirSync(logDir, { recursive: true });
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 = path8.dirname(plistPath);
732
- fs7.mkdirSync(plistDir, { recursive: true });
828
+ const plistDir = path9.dirname(plistPath);
829
+ fs8.mkdirSync(plistDir, { recursive: true });
733
830
  const plist = generateLaunchdPlist(config, proxyRunnerPath);
734
- fs7.writeFileSync(plistPath, plist);
831
+ fs8.writeFileSync(plistPath, plist);
735
832
  try {
736
- execSync2(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
737
- execSync2(`launchctl load "${plistPath}"`, { stdio: "pipe" });
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 = path8.dirname(unitPath);
748
- fs7.mkdirSync(unitDir, { recursive: true });
844
+ const unitDir = path9.dirname(unitPath);
845
+ fs8.mkdirSync(unitDir, { recursive: true });
749
846
  const unit = generateSystemdUnit(config, proxyRunnerPath);
750
- fs7.writeFileSync(unitPath, unit);
847
+ fs8.writeFileSync(unitPath, unit);
751
848
  try {
752
- execSync2("systemctl --user daemon-reload", { stdio: "pipe" });
753
- execSync2("systemctl --user enable skalpel-proxy", { stdio: "pipe" });
754
- execSync2("systemctl --user start skalpel-proxy", { stdio: "pipe" });
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 = path8.join(os4.homedir(), ".config", "autostart");
758
- fs7.mkdirSync(autostartDir, { recursive: true });
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
- fs7.writeFileSync(path8.join(autostartDir, "skalpel-proxy.desktop"), desktopEntry);
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
- execSync2(`schtasks ${args.join(" ")}`, { stdio: "pipe" });
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 fs7.existsSync(plistPath);
892
+ return fs8.existsSync(plistPath);
796
893
  }
797
894
  case "linux": {
798
895
  const unitPath = getLinuxUnitPath();
799
- return fs7.existsSync(unitPath);
896
+ return fs8.existsSync(unitPath);
800
897
  }
801
898
  case "windows": {
802
899
  try {
803
- execSync2("schtasks /query /tn SkalpelProxy", { stdio: "pipe" });
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 (!fs7.existsSync(plistPath)) return;
913
+ if (!fs8.existsSync(plistPath)) return;
817
914
  try {
818
- execSync2(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
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
- execSync2("systemctl --user stop skalpel-proxy", { stdio: "pipe" });
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
- execSync2("schtasks /end /tn SkalpelProxy", { stdio: "pipe" });
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 (!fs7.existsSync(plistPath)) return;
941
+ if (!fs8.existsSync(plistPath)) return;
845
942
  try {
846
- execSync2(`launchctl load "${plistPath}"`, { stdio: "pipe" });
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
- execSync2("systemctl --user start skalpel-proxy", { stdio: "pipe" });
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
- execSync2("schtasks /run /tn SkalpelProxy", { stdio: "pipe" });
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
- execSync2(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
970
+ execSync3(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
874
971
  } catch {
875
972
  }
876
- if (fs7.existsSync(plistPath)) fs7.unlinkSync(plistPath);
973
+ if (fs8.existsSync(plistPath)) fs8.unlinkSync(plistPath);
877
974
  break;
878
975
  }
879
976
  case "linux": {
880
977
  try {
881
- execSync2("systemctl --user stop skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
882
- execSync2("systemctl --user disable skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
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 (fs7.existsSync(unitPath)) fs7.unlinkSync(unitPath);
887
- const desktopPath = path8.join(os4.homedir(), ".config", "autostart", "skalpel-proxy.desktop");
888
- if (fs7.existsSync(desktopPath)) fs7.unlinkSync(desktopPath);
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
- execSync2("schtasks /delete /tn SkalpelProxy /f", { stdio: "pipe" });
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 = path9.dirname(fileURLToPath2(import.meta.url));
922
- const runnerScript = path9.resolve(dirname, "proxy-runner.js");
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 fs8 from "fs";
936
- import path10 from "path";
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 fs9 from "fs";
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 (!fs9.existsSync(logFile)) {
1105
+ if (!fs10.existsSync(logFile)) {
1009
1106
  print8(` No log file found at ${logFile}`);
1010
1107
  return;
1011
1108
  }
1012
- const content = fs9.readFileSync(logFile, "utf-8");
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 = fs9.statSync(logFile).size;
1020
- fs9.watchFile(logFile, { interval: 500 }, () => {
1116
+ let position = fs10.statSync(logFile).size;
1117
+ fs10.watchFile(logFile, { interval: 500 }, () => {
1021
1118
  try {
1022
- const stat = fs9.statSync(logFile);
1119
+ const stat = fs10.statSync(logFile);
1023
1120
  if (stat.size > position) {
1024
- const fd = fs9.openSync(logFile, "r");
1121
+ const fd = fs10.openSync(logFile, "r");
1025
1122
  const buf = Buffer.alloc(stat.size - position);
1026
- fs9.readSync(fd, buf, 0, buf.length, position);
1027
- fs9.closeSync(fd);
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 os7 from "os";
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 os6 from "os";
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(os6.homedir(), ".claude", "settings.json");
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(os6.homedir(), "AppData", "Roaming", "codex") : path12.join(os6.homedir(), ".codex");
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(os6.homedir(), ".claude", "settings.json");
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(os6.homedir(), "AppData", "Roaming", "codex") : path12.join(os6.homedir(), ".codex");
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(os7.homedir(), ".skalpel");
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 os9 from "os";
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 os8 from "os";
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(os8.homedir(), "Documents");
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 = os8.homedir();
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(os9.homedir(), ".skalpel");
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") {