skalpel 2.0.15 → 2.0.17

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
@@ -1826,7 +1826,11 @@ var MAX_SIZE = 5 * 1024 * 1024;
1826
1826
 
1827
1827
  // src/proxy/ws-server.ts
1828
1828
  import { WebSocketServer } from "ws";
1829
- var wss = new WebSocketServer({ noServer: true });
1829
+ var WS_SUBPROTOCOL = "skalpel-codex-v1";
1830
+ var wss = new WebSocketServer({
1831
+ noServer: true,
1832
+ handleProtocols: (protocols) => protocols.has(WS_SUBPROTOCOL) ? WS_SUBPROTOCOL : false
1833
+ });
1830
1834
 
1831
1835
  // src/proxy/server.ts
1832
1836
  var proxyStartTime = 0;
@@ -2150,8 +2154,8 @@ async function runWizard(options) {
2150
2154
  print11(" Welcome to Skalpel! Let's optimize your coding agent costs.");
2151
2155
  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");
2152
2156
  print11("");
2153
- const skalpelDir = path14.join(os9.homedir(), ".skalpel");
2154
- const configPath = path14.join(skalpelDir, "config.json");
2157
+ const skalpelDir2 = path14.join(os9.homedir(), ".skalpel");
2158
+ const configPath = path14.join(skalpelDir2, "config.json");
2155
2159
  let apiKey = "";
2156
2160
  if (isAuto && options?.apiKey) {
2157
2161
  apiKey = options.apiKey;
@@ -2190,7 +2194,7 @@ async function runWizard(options) {
2190
2194
  }
2191
2195
  }
2192
2196
  print11("");
2193
- fs13.mkdirSync(skalpelDir, { recursive: true });
2197
+ fs13.mkdirSync(skalpelDir2, { recursive: true });
2194
2198
  const proxyConfig = loadConfig(configPath);
2195
2199
  proxyConfig.apiKey = apiKey;
2196
2200
  saveConfig(proxyConfig);
@@ -2395,15 +2399,15 @@ async function runUninstall(options) {
2395
2399
  }
2396
2400
  }
2397
2401
  print12("");
2398
- const skalpelDir = path15.join(os10.homedir(), ".skalpel");
2399
- if (fs14.existsSync(skalpelDir)) {
2402
+ const skalpelDir2 = path15.join(os10.homedir(), ".skalpel");
2403
+ if (fs14.existsSync(skalpelDir2)) {
2400
2404
  let shouldRemove = force;
2401
2405
  if (!force) {
2402
2406
  const removeDir = await ask(" Remove ~/.skalpel/ directory (contains config and logs)? (y/N): ");
2403
2407
  shouldRemove = removeDir.toLowerCase() === "y";
2404
2408
  }
2405
2409
  if (shouldRemove) {
2406
- fs14.rmSync(skalpelDir, { recursive: true, force: true });
2410
+ fs14.rmSync(skalpelDir2, { recursive: true, force: true });
2407
2411
  print12(" [+] Removed ~/.skalpel/");
2408
2412
  removed.push("~/.skalpel/ directory");
2409
2413
  }
@@ -2457,6 +2461,311 @@ function clearNpxCache() {
2457
2461
  }
2458
2462
  }
2459
2463
 
2464
+ // src/cli/auth/callback-server.ts
2465
+ import * as http2 from "http";
2466
+ import * as net2 from "net";
2467
+
2468
+ // src/cli/auth/session-storage.ts
2469
+ import * as fs15 from "fs";
2470
+ import * as os11 from "os";
2471
+ import * as path16 from "path";
2472
+ function sessionFilePath() {
2473
+ return path16.join(os11.homedir(), ".skalpel", "session.json");
2474
+ }
2475
+ function skalpelDir() {
2476
+ return path16.join(os11.homedir(), ".skalpel");
2477
+ }
2478
+ function isValidSession(value) {
2479
+ if (!value || typeof value !== "object") return false;
2480
+ const v = value;
2481
+ if (typeof v.accessToken !== "string" || v.accessToken.length === 0) return false;
2482
+ if (typeof v.refreshToken !== "string" || v.refreshToken.length === 0) return false;
2483
+ if (typeof v.expiresAt !== "number" || !Number.isFinite(v.expiresAt)) return false;
2484
+ if (!v.user || typeof v.user !== "object") return false;
2485
+ const user = v.user;
2486
+ if (typeof user.id !== "string" || user.id.length === 0) return false;
2487
+ if (typeof user.email !== "string" || user.email.length === 0) return false;
2488
+ return true;
2489
+ }
2490
+ async function readSession() {
2491
+ const file = sessionFilePath();
2492
+ try {
2493
+ const raw = await fs15.promises.readFile(file, "utf-8");
2494
+ const parsed = JSON.parse(raw);
2495
+ if (!isValidSession(parsed)) return null;
2496
+ return parsed;
2497
+ } catch (err) {
2498
+ if (err.code === "ENOENT") return null;
2499
+ return null;
2500
+ }
2501
+ }
2502
+ async function writeSession(session) {
2503
+ if (!isValidSession(session)) {
2504
+ throw new Error("writeSession: invalid session shape");
2505
+ }
2506
+ const dir = skalpelDir();
2507
+ await fs15.promises.mkdir(dir, { recursive: true, mode: 448 });
2508
+ const file = sessionFilePath();
2509
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
2510
+ const json = JSON.stringify(session, null, 2);
2511
+ await fs15.promises.writeFile(tmp, json, { mode: 384 });
2512
+ try {
2513
+ await fs15.promises.chmod(tmp, 384);
2514
+ } catch {
2515
+ }
2516
+ await fs15.promises.rename(tmp, file);
2517
+ try {
2518
+ await fs15.promises.chmod(file, 384);
2519
+ } catch {
2520
+ }
2521
+ }
2522
+ async function deleteSession() {
2523
+ const file = sessionFilePath();
2524
+ try {
2525
+ await fs15.promises.unlink(file);
2526
+ } catch (err) {
2527
+ if (err.code === "ENOENT") return;
2528
+ throw err;
2529
+ }
2530
+ }
2531
+
2532
+ // src/cli/auth/callback-server.ts
2533
+ var MAX_BODY_BYTES = 16 * 1024;
2534
+ var DEFAULT_TIMEOUT_MS = 18e4;
2535
+ var DEFAULT_ALLOWED_ORIGINS = [
2536
+ "https://app.skalpel.ai",
2537
+ "https://skalpel.ai"
2538
+ ];
2539
+ function allowedOrigins() {
2540
+ const extras = [];
2541
+ const webappUrl = process.env.SKALPEL_WEBAPP_URL;
2542
+ if (webappUrl) {
2543
+ try {
2544
+ const u = new URL(webappUrl);
2545
+ extras.push(`${u.protocol}//${u.host}`);
2546
+ } catch {
2547
+ }
2548
+ }
2549
+ return [...DEFAULT_ALLOWED_ORIGINS, ...extras];
2550
+ }
2551
+ function validatePort(port) {
2552
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
2553
+ throw new Error(`Invalid port: ${port} (must be an integer in 1024-65535)`);
2554
+ }
2555
+ }
2556
+ async function findOpenPort(preferred = 51732) {
2557
+ if (preferred !== 0) validatePort(preferred);
2558
+ const tryBind = (port) => new Promise((resolve2) => {
2559
+ const server = net2.createServer();
2560
+ server.once("error", () => {
2561
+ server.close(() => resolve2(null));
2562
+ });
2563
+ server.listen(port, "127.0.0.1", () => {
2564
+ const address = server.address();
2565
+ const boundPort = address && typeof address === "object" ? address.port : null;
2566
+ server.close(() => resolve2(boundPort));
2567
+ });
2568
+ });
2569
+ const preferredResult = await tryBind(preferred);
2570
+ if (preferredResult !== null) return preferredResult;
2571
+ const fallback = await tryBind(0);
2572
+ if (fallback !== null) return fallback;
2573
+ throw new Error("findOpenPort: no open port available");
2574
+ }
2575
+ function buildCorsHeaders(origin) {
2576
+ const allowed = allowedOrigins();
2577
+ const selected = origin && allowed.includes(origin) ? origin : allowed[0];
2578
+ return {
2579
+ "Access-Control-Allow-Origin": selected,
2580
+ "Access-Control-Allow-Methods": "POST, OPTIONS",
2581
+ "Access-Control-Allow-Headers": "content-type",
2582
+ "Access-Control-Max-Age": "600",
2583
+ Vary: "Origin"
2584
+ };
2585
+ }
2586
+ async function startCallbackServer(port, timeoutMsOrOptions = DEFAULT_TIMEOUT_MS, maxBodyBytesArg) {
2587
+ validatePort(port);
2588
+ const opts = typeof timeoutMsOrOptions === "number" ? { timeoutMs: timeoutMsOrOptions, maxBodyBytes: maxBodyBytesArg } : timeoutMsOrOptions ?? {};
2589
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2590
+ const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES;
2591
+ return new Promise((resolve2, reject) => {
2592
+ let settled = false;
2593
+ let timer;
2594
+ const server = http2.createServer((req, res) => {
2595
+ const origin = req.headers.origin;
2596
+ const corsHeaders = buildCorsHeaders(origin);
2597
+ if (req.method === "OPTIONS") {
2598
+ res.writeHead(204, corsHeaders);
2599
+ res.end();
2600
+ return;
2601
+ }
2602
+ if (req.method === "GET" && (req.url === "/callback" || req.url === "/")) {
2603
+ res.writeHead(200, {
2604
+ ...corsHeaders,
2605
+ "Content-Type": "text/html; charset=utf-8"
2606
+ });
2607
+ res.end(
2608
+ '<!doctype html><meta charset="utf-8"><title>Skalpel CLI</title><p>You can close this tab and return to your terminal.</p>'
2609
+ );
2610
+ return;
2611
+ }
2612
+ if (req.method !== "POST" || req.url !== "/callback") {
2613
+ res.writeHead(404, corsHeaders);
2614
+ res.end();
2615
+ return;
2616
+ }
2617
+ const contentType = (req.headers["content-type"] || "").toLowerCase();
2618
+ if (!contentType.includes("application/json")) {
2619
+ res.writeHead(415, { ...corsHeaders, "Content-Type": "application/json" });
2620
+ res.end(JSON.stringify({ error: "Unsupported Media Type" }));
2621
+ return;
2622
+ }
2623
+ let total = 0;
2624
+ const chunks = [];
2625
+ let aborted = false;
2626
+ req.on("data", (chunk) => {
2627
+ if (aborted) return;
2628
+ total += chunk.length;
2629
+ if (total > maxBytes) {
2630
+ aborted = true;
2631
+ res.writeHead(413, {
2632
+ ...corsHeaders,
2633
+ "Content-Type": "application/json",
2634
+ Connection: "close"
2635
+ });
2636
+ res.end(JSON.stringify({ error: "Payload too large" }));
2637
+ return;
2638
+ }
2639
+ chunks.push(chunk);
2640
+ });
2641
+ req.on("end", () => {
2642
+ if (aborted) return;
2643
+ const raw = Buffer.concat(chunks).toString("utf-8");
2644
+ let parsed;
2645
+ try {
2646
+ parsed = JSON.parse(raw);
2647
+ } catch {
2648
+ res.writeHead(400, { ...corsHeaders, "Content-Type": "application/json" });
2649
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
2650
+ return;
2651
+ }
2652
+ if (!isValidSession(parsed)) {
2653
+ res.writeHead(400, { ...corsHeaders, "Content-Type": "application/json" });
2654
+ res.end(JSON.stringify({ error: "Invalid session shape" }));
2655
+ if (!settled) {
2656
+ settled = true;
2657
+ if (timer) clearTimeout(timer);
2658
+ server.close(() => reject(new Error("Invalid session received")));
2659
+ }
2660
+ return;
2661
+ }
2662
+ res.writeHead(200, { ...corsHeaders, "Content-Type": "application/json" });
2663
+ res.end(JSON.stringify({ ok: true }));
2664
+ if (!settled) {
2665
+ settled = true;
2666
+ if (timer) clearTimeout(timer);
2667
+ server.close(() => resolve2(parsed));
2668
+ }
2669
+ });
2670
+ req.on("error", () => {
2671
+ });
2672
+ });
2673
+ server.once("error", (err) => {
2674
+ if (settled) return;
2675
+ settled = true;
2676
+ if (timer) clearTimeout(timer);
2677
+ reject(err);
2678
+ });
2679
+ server.listen(port, "127.0.0.1", () => {
2680
+ timer = setTimeout(() => {
2681
+ if (settled) return;
2682
+ settled = true;
2683
+ server.close(() => reject(new Error("Login timed out")));
2684
+ }, timeoutMs);
2685
+ if (timer.unref) timer.unref();
2686
+ });
2687
+ });
2688
+ }
2689
+
2690
+ // src/cli/auth/browser.ts
2691
+ async function openUrl(url) {
2692
+ if (!/^https?:\/\//i.test(url)) {
2693
+ throw new Error(`openUrl: refusing to open non-http(s) URL: ${url}`);
2694
+ }
2695
+ try {
2696
+ const mod = await import("open");
2697
+ const opener = mod.default;
2698
+ if (typeof opener !== "function") {
2699
+ throw new Error("open package exports no default function");
2700
+ }
2701
+ await opener(url);
2702
+ return { opened: true, fallback: false };
2703
+ } catch {
2704
+ console.log("");
2705
+ console.log(" Could not open your browser automatically.");
2706
+ console.log(" Please open this URL manually to continue:");
2707
+ console.log("");
2708
+ console.log(` ${url}`);
2709
+ console.log("");
2710
+ return { opened: false, fallback: true };
2711
+ }
2712
+ }
2713
+
2714
+ // src/cli/login.ts
2715
+ var DEFAULT_WEBAPP_URL = "https://app.skalpel.ai";
2716
+ var DEFAULT_TIMEOUT_MS2 = 18e4;
2717
+ async function runLogin(options = {}) {
2718
+ const webappUrl = options.webappUrl ?? process.env.SKALPEL_WEBAPP_URL ?? DEFAULT_WEBAPP_URL;
2719
+ const preferredPort = options.preferredPort ?? 51732;
2720
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
2721
+ const log = options.logger ?? console;
2722
+ const _findPort = options.findOpenPort ?? findOpenPort;
2723
+ const _startServer = options.startCallbackServer ?? startCallbackServer;
2724
+ const _openUrl = options.openUrl ?? openUrl;
2725
+ const _writeSession = options.writeSession ?? writeSession;
2726
+ const port = await _findPort(preferredPort);
2727
+ const authorizeUrl = `${webappUrl.replace(/\/$/, "")}/cli/authorize?port=${port}`;
2728
+ log.log("");
2729
+ log.log(` Opening browser to ${authorizeUrl}`);
2730
+ log.log(" Waiting for authentication (timeout 3 min)...");
2731
+ log.log("");
2732
+ const serverPromise = _startServer(port, timeoutMs);
2733
+ try {
2734
+ await _openUrl(authorizeUrl);
2735
+ } catch {
2736
+ log.log(" Browser launch failed. Please open the URL above manually.");
2737
+ }
2738
+ let session;
2739
+ try {
2740
+ session = await serverPromise;
2741
+ } catch (err) {
2742
+ const msg = err instanceof Error ? err.message : String(err);
2743
+ log.error("");
2744
+ log.error(` Login failed: ${msg}`);
2745
+ log.error("");
2746
+ process.exitCode = 1;
2747
+ throw err;
2748
+ }
2749
+ await _writeSession(session);
2750
+ log.log("");
2751
+ log.log(` \u2713 Logged in as ${session.user.email}`);
2752
+ log.log("");
2753
+ }
2754
+
2755
+ // src/cli/logout.ts
2756
+ async function runLogout(options = {}) {
2757
+ const log = options.logger ?? console;
2758
+ const _readSession = options.readSession ?? readSession;
2759
+ const _deleteSession = options.deleteSession ?? deleteSession;
2760
+ const existing = await _readSession();
2761
+ if (!existing) {
2762
+ log.log(" Not logged in.");
2763
+ return;
2764
+ }
2765
+ await _deleteSession();
2766
+ log.log(` \u2713 Logged out.`);
2767
+ }
2768
+
2460
2769
  // src/cli/index.ts
2461
2770
  var require3 = createRequire2(import.meta.url);
2462
2771
  var pkg2 = require3("../../package.json");
@@ -2469,6 +2778,8 @@ program.command("replay").description("Replay saved request files").argument("<f
2469
2778
  program.command("start").description("Start the Skalpel proxy").action(runStart);
2470
2779
  program.command("stop").description("Stop the Skalpel proxy").action(runStop);
2471
2780
  program.command("status").description("Show proxy status").action(runStatus);
2781
+ program.command("login").description("Log in to your Skalpel account (opens browser)").action(() => runLogin());
2782
+ program.command("logout").description("Log out of your Skalpel account").action(() => runLogout());
2472
2783
  program.command("logs").description("View proxy logs").option("-n, --lines <count>", "Number of lines to show", "50").option("-f, --follow", "Follow log output").action(runLogs);
2473
2784
  program.command("config").description("View or edit proxy configuration").argument("[subcommand]", "path | set").argument("[args...]", "Arguments for subcommand").action(runConfig);
2474
2785
  program.command("update").description("Update Skalpel to the latest version").action(runUpdate);