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 +318 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/proxy-runner.js +122 -88
- package/dist/cli/proxy-runner.js.map +1 -1
- package/dist/index.cjs +122 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +122 -88
- package/dist/index.js.map +1 -1
- package/dist/proxy/index.cjs +122 -88
- package/dist/proxy/index.cjs.map +1 -1
- package/dist/proxy/index.js +122 -88
- package/dist/proxy/index.js.map +1 -1
- package/package.json +2 -1
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
|
|
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
|
|
2154
|
-
const configPath = path14.join(
|
|
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(
|
|
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
|
|
2399
|
-
if (fs14.existsSync(
|
|
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(
|
|
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);
|