skalpel 2.0.23 → 3.0.0

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.
Files changed (43) hide show
  1. package/INSTALL.md +103 -0
  2. package/LICENSE +201 -21
  3. package/README.md +12 -174
  4. package/design-tokens.json +51 -0
  5. package/npm-bin/colors.js +125 -0
  6. package/npm-bin/skalpel.js +200 -0
  7. package/npm-bin/skalpeld.js +20 -0
  8. package/package.json +50 -68
  9. package/postinstall/index.js +294 -0
  10. package/postinstall/launchd/com.skalpel.skalpeld.plist.tmpl +41 -0
  11. package/postinstall/lib/detect-prior.js +51 -0
  12. package/postinstall/lib/env-inject.js +121 -0
  13. package/postinstall/lib/launch.js +28 -0
  14. package/postinstall/lib/log.js +31 -0
  15. package/postinstall/lib/paths.js +186 -0
  16. package/postinstall/lib/rc-edit.js +167 -0
  17. package/postinstall/lib/rc-edit.test.js +196 -0
  18. package/postinstall/lib/service-register.js +293 -0
  19. package/postinstall/lib/sign-in.js +98 -0
  20. package/postinstall/lib/template.js +36 -0
  21. package/postinstall/snippets/bash.sh.tmpl +12 -0
  22. package/postinstall/snippets/fish.fish.tmpl +11 -0
  23. package/postinstall/snippets/powershell.ps1.tmpl +12 -0
  24. package/postinstall/snippets/zsh.sh.tmpl +13 -0
  25. package/postinstall/systemd/skalpeld.service.tmpl +33 -0
  26. package/postinstall/windows/Task.xml.tmpl +42 -0
  27. package/postinstall/windows/register-task.ps1.tmpl +45 -0
  28. package/dist/cli/index.js +0 -2899
  29. package/dist/cli/index.js.map +0 -1
  30. package/dist/cli/proxy-runner.js +0 -1649
  31. package/dist/cli/proxy-runner.js.map +0 -1
  32. package/dist/index.cjs +0 -2333
  33. package/dist/index.cjs.map +0 -1
  34. package/dist/index.d.cts +0 -165
  35. package/dist/index.d.ts +0 -165
  36. package/dist/index.js +0 -2287
  37. package/dist/index.js.map +0 -1
  38. package/dist/proxy/index.cjs +0 -1782
  39. package/dist/proxy/index.cjs.map +0 -1
  40. package/dist/proxy/index.d.cts +0 -39
  41. package/dist/proxy/index.d.ts +0 -39
  42. package/dist/proxy/index.js +0 -1748
  43. package/dist/proxy/index.js.map +0 -1
package/dist/cli/index.js DELETED
@@ -1,2899 +0,0 @@
1
- #!/usr/bin/env node
2
- var __getOwnPropNames = Object.getOwnPropertyNames;
3
- var __esm = (fn, res) => function __init() {
4
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
- };
6
-
7
- // src/proxy/codex-oauth.ts
8
- import { readFileSync } from "fs";
9
- import { homedir } from "os";
10
- import { join } from "path";
11
- function authFilePath() {
12
- return join(homedir(), ".codex", "auth.json");
13
- }
14
- function readCodexAuth() {
15
- const path17 = authFilePath();
16
- let raw;
17
- try {
18
- raw = readFileSync(path17, "utf-8");
19
- } catch (err) {
20
- const code = err?.code;
21
- if (code !== "ENOENT") {
22
- process.stderr.write(`skalpel: codex-oauth: cannot read auth file (${code ?? "unknown"})
23
- `);
24
- }
25
- return null;
26
- }
27
- let parsed;
28
- try {
29
- parsed = JSON.parse(raw);
30
- } catch {
31
- process.stderr.write("skalpel: codex-oauth: auth file is not valid JSON\n");
32
- return null;
33
- }
34
- if (parsed === null || typeof parsed !== "object" || typeof parsed.access_token !== "string" || typeof parsed.refresh_token !== "string" || typeof parsed.expires_at !== "string") {
35
- process.stderr.write("skalpel: codex-oauth: auth file missing required fields\n");
36
- return null;
37
- }
38
- const obj = parsed;
39
- const auth = {
40
- access_token: obj.access_token,
41
- refresh_token: obj.refresh_token,
42
- expires_at: obj.expires_at
43
- };
44
- if (typeof obj.account_id === "string") {
45
- auth.account_id = obj.account_id;
46
- }
47
- return auth;
48
- }
49
- var init_codex_oauth = __esm({
50
- "src/proxy/codex-oauth.ts"() {
51
- "use strict";
52
- }
53
- });
54
-
55
- // src/proxy/dispatcher.ts
56
- import { Agent } from "undici";
57
- var skalpelDispatcher;
58
- var init_dispatcher = __esm({
59
- "src/proxy/dispatcher.ts"() {
60
- "use strict";
61
- skalpelDispatcher = new Agent({
62
- keepAliveTimeout: 1e4,
63
- keepAliveMaxTimeout: 6e4,
64
- connections: 100,
65
- pipelining: 1,
66
- allowH2: false
67
- // Force HTTP/1.1 to prevent GCP LB WebSocket downgrade
68
- });
69
- }
70
- });
71
-
72
- // src/proxy/envelope.ts
73
- var init_envelope = __esm({
74
- "src/proxy/envelope.ts"() {
75
- "use strict";
76
- }
77
- });
78
-
79
- // src/proxy/recovery.ts
80
- import { createHash } from "crypto";
81
- var MUTEX_MAX_ENTRIES, LruMutexMap, refreshMutex;
82
- var init_recovery = __esm({
83
- "src/proxy/recovery.ts"() {
84
- "use strict";
85
- MUTEX_MAX_ENTRIES = 1024;
86
- LruMutexMap = class extends Map {
87
- set(key, value) {
88
- if (this.has(key)) {
89
- super.delete(key);
90
- } else if (this.size >= MUTEX_MAX_ENTRIES) {
91
- const oldest = this.keys().next().value;
92
- if (oldest !== void 0) super.delete(oldest);
93
- }
94
- return super.set(key, value);
95
- }
96
- };
97
- refreshMutex = new LruMutexMap();
98
- }
99
- });
100
-
101
- // src/proxy/fetch-error.ts
102
- var init_fetch_error = __esm({
103
- "src/proxy/fetch-error.ts"() {
104
- "use strict";
105
- }
106
- });
107
-
108
- // src/proxy/trace-context.ts
109
- import { randomBytes } from "crypto";
110
- var init_trace_context = __esm({
111
- "src/proxy/trace-context.ts"() {
112
- "use strict";
113
- }
114
- });
115
-
116
- // src/proxy/streaming.ts
117
- var HOP_BY_HOP, STRIP_HEADERS;
118
- var init_streaming = __esm({
119
- "src/proxy/streaming.ts"() {
120
- "use strict";
121
- init_dispatcher();
122
- init_handler();
123
- init_envelope();
124
- init_recovery();
125
- init_fetch_error();
126
- init_trace_context();
127
- HOP_BY_HOP = /* @__PURE__ */ new Set([
128
- "connection",
129
- "keep-alive",
130
- "proxy-authenticate",
131
- "proxy-authorization",
132
- "te",
133
- "trailer",
134
- "transfer-encoding",
135
- "upgrade"
136
- ]);
137
- STRIP_HEADERS = /* @__PURE__ */ new Set([
138
- ...HOP_BY_HOP,
139
- "content-encoding",
140
- "content-length"
141
- ]);
142
- }
143
- });
144
-
145
- // src/proxy/ws-client.ts
146
- import { EventEmitter } from "events";
147
- import https from "https";
148
- import http from "http";
149
- import WebSocket2 from "ws";
150
- var H1_HTTPS_AGENT, H1_HTTP_AGENT;
151
- var init_ws_client = __esm({
152
- "src/proxy/ws-client.ts"() {
153
- "use strict";
154
- init_codex_oauth();
155
- init_trace_context();
156
- H1_HTTPS_AGENT = new https.Agent({
157
- ALPNProtocols: ["http/1.1"],
158
- keepAlive: true
159
- });
160
- H1_HTTP_AGENT = new http.Agent({ keepAlive: true });
161
- }
162
- });
163
-
164
- // src/proxy/handler.ts
165
- var init_handler = __esm({
166
- "src/proxy/handler.ts"() {
167
- "use strict";
168
- init_streaming();
169
- init_dispatcher();
170
- init_envelope();
171
- init_ws_client();
172
- init_codex_oauth();
173
- init_recovery();
174
- init_fetch_error();
175
- init_trace_context();
176
- }
177
- });
178
-
179
- // src/cli/index.ts
180
- import { Command } from "commander";
181
- import { createRequire as createRequire2 } from "module";
182
-
183
- // src/cli/init.ts
184
- import * as readline from "readline";
185
- import * as fs2 from "fs";
186
- import * as path2 from "path";
187
-
188
- // src/cli/utils.ts
189
- init_codex_oauth();
190
- import * as fs from "fs";
191
- import * as path from "path";
192
- function detectProjectType() {
193
- if (fs.existsSync(path.join(process.cwd(), "package.json"))) {
194
- return "node";
195
- }
196
- if (fs.existsSync(path.join(process.cwd(), "requirements.txt")) || fs.existsSync(path.join(process.cwd(), "pyproject.toml"))) {
197
- return "python";
198
- }
199
- return "other";
200
- }
201
- function detectAiSdks(projectType) {
202
- const providers = [];
203
- if (projectType === "node") {
204
- try {
205
- const pkgPath = path.join(process.cwd(), "package.json");
206
- const pkg3 = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
207
- const allDeps = {
208
- ...pkg3.dependencies,
209
- ...pkg3.devDependencies
210
- };
211
- if (allDeps["openai"]) providers.push("openai");
212
- if (allDeps["@anthropic-ai/sdk"]) providers.push("anthropic");
213
- } catch {
214
- }
215
- }
216
- if (projectType === "python") {
217
- try {
218
- const reqPath = path.join(process.cwd(), "requirements.txt");
219
- if (fs.existsSync(reqPath)) {
220
- const content = fs.readFileSync(reqPath, "utf-8");
221
- if (/^openai/m.test(content)) providers.push("openai");
222
- if (/^anthropic/m.test(content)) providers.push("anthropic");
223
- }
224
- } catch {
225
- }
226
- }
227
- return providers;
228
- }
229
- function validateApiKey(key) {
230
- return key.startsWith("sk-skalpel-") && key.length >= 20;
231
- }
232
- function validateCodexOAuth() {
233
- try {
234
- const auth = readCodexAuth();
235
- if (auth !== null) {
236
- return { present: true };
237
- }
238
- } catch (err) {
239
- return { present: false, reason: `other: ${err.message}` };
240
- }
241
- const candidatePath = path.join(
242
- process.env.HOME ?? process.env.USERPROFILE ?? "",
243
- ".codex",
244
- "auth.json"
245
- );
246
- if (!fs.existsSync(candidatePath)) {
247
- return { present: false, reason: "file missing" };
248
- }
249
- return { present: false, reason: "malformed JSON" };
250
- }
251
- function generateCodeSample(config) {
252
- if (config.integrationMethod === "wrapper") {
253
- if (config.providers.includes("openai")) {
254
- return `import OpenAI from 'openai';
255
- import { createSkalpelClient } from 'skalpel';
256
-
257
- const openai = createSkalpelClient(new OpenAI(), {
258
- apiKey: process.env.SKALPEL_API_KEY!,${config.workspace ? `
259
- workspace: '${config.workspace}',` : ""}
260
- });
261
-
262
- const response = await openai.chat.completions.create({
263
- model: 'gpt-4o',
264
- messages: [{ role: 'user', content: 'Hello' }],
265
- });`;
266
- }
267
- if (config.providers.includes("anthropic")) {
268
- return `import Anthropic from '@anthropic-ai/sdk';
269
- import { createSkalpelClient } from 'skalpel';
270
-
271
- const anthropic = createSkalpelClient(new Anthropic(), {
272
- apiKey: process.env.SKALPEL_API_KEY!,${config.workspace ? `
273
- workspace: '${config.workspace}',` : ""}
274
- });
275
-
276
- const response = await anthropic.messages.create({
277
- model: 'claude-sonnet-4-20250514',
278
- max_tokens: 1024,
279
- messages: [{ role: 'user', content: 'Hello' }],
280
- });`;
281
- }
282
- }
283
- if (config.integrationMethod === "url_swap") {
284
- if (config.providers.includes("openai")) {
285
- return `import OpenAI from 'openai';
286
-
287
- const openai = new OpenAI({
288
- baseURL: 'https://api.skalpel.ai/v1',
289
- apiKey: process.env.SKALPEL_API_KEY,
290
- });
291
-
292
- const response = await openai.chat.completions.create({
293
- model: 'gpt-4o',
294
- messages: [{ role: 'user', content: 'Hello' }],
295
- });`;
296
- }
297
- if (config.providers.includes("anthropic")) {
298
- return `import Anthropic from '@anthropic-ai/sdk';
299
-
300
- const anthropic = new Anthropic({
301
- baseURL: 'https://api.skalpel.ai/v1',
302
- apiKey: process.env.SKALPEL_API_KEY,
303
- });
304
-
305
- const response = await anthropic.messages.create({
306
- model: 'claude-sonnet-4-20250514',
307
- max_tokens: 1024,
308
- messages: [{ role: 'user', content: 'Hello' }],
309
- });`;
310
- }
311
- }
312
- return `// Configure your Skalpel API key
313
- // SKALPEL_API_KEY=${config.apiKey}
314
- // See https://docs.skalpel.ai for integration guides`;
315
- }
316
-
317
- // src/cli/init.ts
318
- function print(msg) {
319
- console.log(msg);
320
- }
321
- async function runInit() {
322
- const rl = readline.createInterface({
323
- input: process.stdin,
324
- output: process.stdout
325
- });
326
- function ask(question) {
327
- return new Promise((resolve2) => {
328
- rl.question(question, (answer) => resolve2(answer.trim()));
329
- });
330
- }
331
- try {
332
- print("");
333
- print(" Skalpel AI \u2014 SDK Setup");
334
- print(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
335
- print("");
336
- const projectType = detectProjectType();
337
- print(` Detected project type: ${projectType}`);
338
- const sdks = detectAiSdks(projectType);
339
- if (sdks.length > 0) {
340
- print(` Detected AI SDKs: ${sdks.join(", ")}`);
341
- } else {
342
- print(" No AI SDKs detected");
343
- }
344
- print("");
345
- let apiKey = process.env.SKALPEL_API_KEY ?? "";
346
- if (apiKey && validateApiKey(apiKey)) {
347
- print(` Using API key from SKALPEL_API_KEY env var: ${apiKey.slice(0, 14)}...`);
348
- } else {
349
- apiKey = await ask(" Enter your Skalpel API key (sk-skalpel-...): ");
350
- if (!validateApiKey(apiKey)) {
351
- print(' Error: Invalid API key. Must start with "sk-skalpel-" and be at least 20 characters.');
352
- rl.close();
353
- process.exit(1);
354
- }
355
- print(` API key set: ${apiKey.slice(0, 14)}${"*".repeat(Math.max(0, apiKey.length - 14))}`);
356
- }
357
- print("");
358
- print(" Choose integration method:");
359
- print(" 1) Wrapper pattern \u2014 Wraps your existing SDK client. Adds fallback and metadata.");
360
- print(" 2) URL swap \u2014 Changes the base URL. Simplest, one-line change.");
361
- print("");
362
- const methodChoice = await ask(" Enter choice (1 or 2): ");
363
- const integrationMethod = methodChoice === "2" ? "url_swap" : "wrapper";
364
- print("");
365
- const envPath = path2.join(process.cwd(), ".env");
366
- const envContent = `SKALPEL_API_KEY=${apiKey}
367
- SKALPEL_BASE_URL=https://api.skalpel.ai
368
- `;
369
- if (fs2.existsSync(envPath)) {
370
- fs2.appendFileSync(envPath, `
371
- ${envContent}`);
372
- print(" Appended Skalpel config to existing .env file");
373
- } else {
374
- fs2.writeFileSync(envPath, envContent);
375
- print(" Created .env file with Skalpel config");
376
- }
377
- print("");
378
- const providers = sdks.length > 0 ? sdks : ["openai"];
379
- const config = {
380
- projectType,
381
- integrationMethod,
382
- providers,
383
- apiKey
384
- };
385
- const sample = generateCodeSample(config);
386
- print(" Add this to your code:");
387
- print(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
388
- for (const line of sample.split("\n")) {
389
- print(` ${line}`);
390
- }
391
- print(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
392
- print("");
393
- print(" Setup complete! Your API calls will now be optimized by Skalpel.");
394
- print("");
395
- rl.close();
396
- } catch (err) {
397
- rl.close();
398
- throw err;
399
- }
400
- }
401
-
402
- // src/cli/doctor.ts
403
- import * as fs4 from "fs";
404
- import * as path4 from "path";
405
- import * as os2 from "os";
406
- import net from "net";
407
- import WebSocket from "ws";
408
-
409
- // src/cli/agents/detect.ts
410
- import { execSync } from "child_process";
411
- import fs3 from "fs";
412
- import path3 from "path";
413
- import os from "os";
414
- function whichCommand() {
415
- return process.platform === "win32" ? "where" : "which";
416
- }
417
- function tryExec(cmd) {
418
- try {
419
- return execSync(cmd, { encoding: "utf-8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }).trim();
420
- } catch {
421
- return null;
422
- }
423
- }
424
- function detectClaudeCode() {
425
- const agent = {
426
- name: "claude-code",
427
- installed: false,
428
- version: null,
429
- configPath: null
430
- };
431
- const binaryPath = tryExec(`${whichCommand()} claude`);
432
- const hasBinary = binaryPath !== null && binaryPath.length > 0;
433
- const claudeDir = path3.join(os.homedir(), ".claude");
434
- const hasConfigDir = fs3.existsSync(claudeDir);
435
- agent.installed = hasBinary || hasConfigDir;
436
- if (hasBinary) {
437
- const versionOutput = tryExec("claude --version");
438
- if (versionOutput) {
439
- const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
440
- agent.version = match ? match[1] : versionOutput;
441
- }
442
- }
443
- const settingsPath = path3.join(claudeDir, "settings.json");
444
- if (fs3.existsSync(settingsPath)) {
445
- agent.configPath = settingsPath;
446
- } else if (hasConfigDir) {
447
- agent.configPath = settingsPath;
448
- }
449
- return agent;
450
- }
451
- function detectCodex() {
452
- const agent = {
453
- name: "codex",
454
- installed: false,
455
- version: null,
456
- configPath: null
457
- };
458
- const binaryPath = tryExec(`${whichCommand()} codex`);
459
- const hasBinary = binaryPath !== null && binaryPath.length > 0;
460
- const codexConfigDir = process.platform === "win32" ? path3.join(os.homedir(), "AppData", "Roaming", "codex") : path3.join(os.homedir(), ".codex");
461
- const hasConfigDir = fs3.existsSync(codexConfigDir);
462
- agent.installed = hasBinary || hasConfigDir;
463
- if (hasBinary) {
464
- const versionOutput = tryExec("codex --version");
465
- if (versionOutput) {
466
- const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
467
- agent.version = match ? match[1] : versionOutput;
468
- }
469
- }
470
- const configFile = path3.join(codexConfigDir, "config.toml");
471
- if (fs3.existsSync(configFile)) {
472
- agent.configPath = configFile;
473
- } else if (hasConfigDir) {
474
- agent.configPath = configFile;
475
- }
476
- return agent;
477
- }
478
- function detectCursor() {
479
- const agent = {
480
- name: "cursor",
481
- installed: false,
482
- version: null,
483
- configPath: null
484
- };
485
- const binaryPath = tryExec(`${whichCommand()} cursor`);
486
- const hasBinary = binaryPath !== null && binaryPath.length > 0;
487
- let cursorConfigDir;
488
- if (process.platform === "darwin") {
489
- cursorConfigDir = path3.join(os.homedir(), "Library", "Application Support", "Cursor", "User");
490
- } else if (process.platform === "win32") {
491
- cursorConfigDir = path3.join(process.env.APPDATA ?? path3.join(os.homedir(), "AppData", "Roaming"), "Cursor", "User");
492
- } else {
493
- cursorConfigDir = path3.join(os.homedir(), ".config", "Cursor", "User");
494
- }
495
- const hasConfigDir = fs3.existsSync(cursorConfigDir);
496
- agent.installed = hasBinary || hasConfigDir;
497
- if (hasBinary) {
498
- const versionOutput = tryExec("cursor --version");
499
- if (versionOutput) {
500
- const match = versionOutput.match(/(\d+\.\d+[\w.-]*)/);
501
- agent.version = match ? match[1] : versionOutput;
502
- }
503
- }
504
- const settingsPath = path3.join(cursorConfigDir, "settings.json");
505
- if (fs3.existsSync(settingsPath)) {
506
- agent.configPath = settingsPath;
507
- } else if (hasConfigDir) {
508
- agent.configPath = settingsPath;
509
- }
510
- return agent;
511
- }
512
- function detectAgents() {
513
- return [detectClaudeCode(), detectCodex(), detectCursor()];
514
- }
515
-
516
- // src/cli/doctor.ts
517
- function print2(msg) {
518
- console.log(msg);
519
- }
520
- function codexConfigPath() {
521
- return process.platform === "win32" ? path4.join(os2.homedir(), "AppData", "Roaming", "codex", "config.toml") : path4.join(os2.homedir(), ".codex", "config.toml");
522
- }
523
- function checkCodexConfig(config) {
524
- const cfgPath = codexConfigPath();
525
- if (!fs4.existsSync(cfgPath)) {
526
- return {
527
- name: "Codex config",
528
- status: "warn",
529
- message: `${cfgPath} not found \u2014 run "npx skalpel" to configure Codex`
530
- };
531
- }
532
- let content = "";
533
- try {
534
- content = fs4.readFileSync(cfgPath, "utf-8");
535
- } catch {
536
- return {
537
- name: "Codex config",
538
- status: "warn",
539
- message: `cannot read ${cfgPath}`
540
- };
541
- }
542
- const requiredLines = [
543
- `openai_base_url = "http://localhost:${config.openaiPort}"`,
544
- `model_provider = "skalpel-proxy"`,
545
- `[model_providers.skalpel-proxy]`,
546
- `wire_api = "responses"`,
547
- `base_url = "http://localhost:${config.openaiPort}/v1"`
548
- ];
549
- const missing = requiredLines.filter((line) => !content.includes(line));
550
- if (missing.length === 0) {
551
- return {
552
- name: "Codex config",
553
- status: "ok",
554
- message: `skalpel-proxy provider pinned (wire_api=responses) on port ${config.openaiPort}`
555
- };
556
- }
557
- return {
558
- name: "Codex config",
559
- status: "fail",
560
- message: `missing TOML: ${missing.map((m) => m.split("\n")[0]).join("; ")}`
561
- };
562
- }
563
- async function checkCodexWebSocket(config) {
564
- const tcpOk = await new Promise((resolve2) => {
565
- const sock = net.connect({ host: "127.0.0.1", port: config.openaiPort, timeout: 1e3 });
566
- const done = (ok) => {
567
- sock.removeAllListeners();
568
- try {
569
- sock.destroy();
570
- } catch {
571
- }
572
- resolve2(ok);
573
- };
574
- sock.once("connect", () => done(true));
575
- sock.once("error", () => done(false));
576
- sock.once("timeout", () => done(false));
577
- });
578
- if (!tcpOk) {
579
- return {
580
- name: "Codex WebSocket",
581
- status: "skipped",
582
- message: "WebSocket: SKIPPED (proxy not running)"
583
- };
584
- }
585
- return new Promise((resolve2) => {
586
- const url = `ws://localhost:${config.openaiPort}/v1/responses`;
587
- let settled = false;
588
- const ws = new WebSocket(url, ["skalpel-codex-v1"]);
589
- const settle = (result) => {
590
- if (settled) return;
591
- settled = true;
592
- try {
593
- ws.close();
594
- } catch {
595
- }
596
- resolve2(result);
597
- };
598
- const timeout = setTimeout(() => {
599
- settle({
600
- name: "Codex WebSocket",
601
- status: "fail",
602
- message: "WebSocket: FAIL handshake timeout after 5s"
603
- });
604
- }, 5e3);
605
- ws.once("open", () => {
606
- clearTimeout(timeout);
607
- settle({ name: "Codex WebSocket", status: "ok", message: "WebSocket: OK" });
608
- });
609
- ws.once("error", (err) => {
610
- clearTimeout(timeout);
611
- settle({
612
- name: "Codex WebSocket",
613
- status: "fail",
614
- message: `WebSocket: FAIL ${err.message}`
615
- });
616
- });
617
- ws.once("unexpected-response", (_req, res) => {
618
- clearTimeout(timeout);
619
- settle({
620
- name: "Codex WebSocket",
621
- status: "fail",
622
- message: `WebSocket: FAIL unexpected HTTP ${res.statusCode}`
623
- });
624
- });
625
- });
626
- }
627
- async function checkCodexProxyProbe(config) {
628
- const url = `http://localhost:${config.openaiPort}/v1/responses`;
629
- try {
630
- const res = await fetch(url, {
631
- method: "POST",
632
- headers: { "Content-Type": "application/json", Authorization: "Bearer sk-codex-placeholder-skalpel" },
633
- body: JSON.stringify({ model: "gpt-5-codex", input: "ping", stream: false }),
634
- signal: AbortSignal.timeout(5e3)
635
- });
636
- if (res.status === 405) {
637
- return { name: "Codex proxy probe", status: "error", message: "backend rejected POST \u2014 run the fix in docs/codex-integration-fix.md" };
638
- }
639
- if (res.status === 401 || res.status >= 200 && res.status < 300) {
640
- return { name: "Codex proxy probe", status: "ok", message: `POST /v1/responses returned ${res.status}` };
641
- }
642
- return { name: "Codex proxy probe", status: "warn", message: `unexpected status ${res.status}` };
643
- } catch {
644
- return { name: "Codex proxy probe", status: "warn", message: "proxy not reachable (is it running?)" };
645
- }
646
- }
647
- function loadConfigApiKey() {
648
- try {
649
- const configPath = path4.join(os2.homedir(), ".skalpel", "config.json");
650
- const raw = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
651
- if (typeof raw.apiKey === "string" && raw.apiKey.length > 0) {
652
- return raw.apiKey;
653
- }
654
- } catch {
655
- }
656
- return null;
657
- }
658
- async function runDoctor() {
659
- print2("");
660
- print2(" Skalpel Doctor");
661
- print2(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
662
- print2("");
663
- const checks = [];
664
- const configKey = loadConfigApiKey();
665
- const envKey = process.env.SKALPEL_API_KEY ?? "";
666
- const apiKey = configKey || envKey;
667
- if (apiKey && validateApiKey(apiKey)) {
668
- const source = configKey ? "~/.skalpel/config.json" : "environment";
669
- checks.push({
670
- name: "API Key",
671
- status: "ok",
672
- message: `Valid key from ${source}: ${apiKey.slice(0, 14)}${"*".repeat(Math.max(0, apiKey.length - 14))}`
673
- });
674
- } else if (apiKey) {
675
- checks.push({
676
- name: "API Key",
677
- status: "fail",
678
- message: `Invalid format \u2014 must start with "sk-skalpel-" and be >= 20 chars`
679
- });
680
- } else {
681
- checks.push({
682
- name: "API Key",
683
- status: "fail",
684
- message: 'No API key found. Run "npx skalpel" to set up.'
685
- });
686
- }
687
- const skalpelConfigPath = path4.join(os2.homedir(), ".skalpel", "config.json");
688
- if (fs4.existsSync(skalpelConfigPath)) {
689
- checks.push({ name: "Skalpel config", status: "ok", message: "~/.skalpel/config.json found" });
690
- } else {
691
- checks.push({ name: "Skalpel config", status: "warn", message: 'No ~/.skalpel/config.json \u2014 run "npx skalpel" to set up' });
692
- }
693
- let mode = "proxy";
694
- try {
695
- const raw = JSON.parse(fs4.readFileSync(skalpelConfigPath, "utf-8"));
696
- if (raw.mode === "direct") mode = "direct";
697
- } catch {
698
- }
699
- const modeMessage = mode === "direct" ? "direct (agents point at api.skalpel.ai)" : "proxy (local proxy on ports 18100/18101/18102)";
700
- checks.push({ name: "mode", status: "ok", message: modeMessage });
701
- const baseURL = "https://api.skalpel.ai";
702
- try {
703
- const controller = new AbortController();
704
- const timeout = setTimeout(() => controller.abort(), 5e3);
705
- const response = await fetch(`${baseURL}/health`, { signal: controller.signal });
706
- clearTimeout(timeout);
707
- if (response.ok) {
708
- checks.push({ name: "Skalpel backend", status: "ok", message: `${baseURL} reachable (HTTP ${response.status})` });
709
- } else {
710
- checks.push({ name: "Skalpel backend", status: "warn", message: `${baseURL} responded with HTTP ${response.status}` });
711
- }
712
- } catch (err) {
713
- const msg = err instanceof Error ? err.message : String(err);
714
- checks.push({ name: "Skalpel backend", status: "fail", message: `Cannot reach ${baseURL} \u2014 ${msg}` });
715
- }
716
- let proxyPort = 18100;
717
- try {
718
- const raw = JSON.parse(fs4.readFileSync(skalpelConfigPath, "utf-8"));
719
- proxyPort = raw.anthropicPort ?? 18100;
720
- } catch {
721
- }
722
- try {
723
- const controller = new AbortController();
724
- const timeout = setTimeout(() => controller.abort(), 2e3);
725
- const res = await fetch(`http://localhost:${proxyPort}/health`, { signal: controller.signal });
726
- clearTimeout(timeout);
727
- if (res.ok) {
728
- checks.push({ name: "Local proxy", status: "ok", message: `Running on port ${proxyPort}` });
729
- } else {
730
- checks.push({ name: "Local proxy", status: "warn", message: `Port ${proxyPort} responded with HTTP ${res.status}` });
731
- }
732
- } catch {
733
- checks.push({ name: "Local proxy", status: "warn", message: `Not running on port ${proxyPort}. Run "npx skalpel start" to start.` });
734
- }
735
- let cursorPort = 18102;
736
- try {
737
- const raw = JSON.parse(fs4.readFileSync(skalpelConfigPath, "utf-8"));
738
- cursorPort = raw.cursorPort ?? 18102;
739
- } catch {
740
- }
741
- try {
742
- const controller = new AbortController();
743
- const timeout = setTimeout(() => controller.abort(), 2e3);
744
- const res = await fetch(`http://localhost:${cursorPort}/health`, { signal: controller.signal });
745
- clearTimeout(timeout);
746
- if (res.ok) {
747
- checks.push({ name: "Cursor proxy", status: "ok", message: `Running on port ${cursorPort}` });
748
- } else {
749
- checks.push({ name: "Cursor proxy", status: "warn", message: `Port ${cursorPort} responded with HTTP ${res.status}` });
750
- }
751
- } catch {
752
- checks.push({ name: "Cursor proxy", status: "warn", message: `Not running on port ${cursorPort}. Run "npx skalpel start" to start.` });
753
- }
754
- const agents = detectAgents();
755
- for (const agent of agents) {
756
- if (agent.installed) {
757
- const ver = agent.version ? ` v${agent.version}` : "";
758
- const configured = agent.configPath && fs4.existsSync(agent.configPath) ? " (configured)" : "";
759
- checks.push({ name: agent.name, status: "ok", message: `Installed${ver}${configured}` });
760
- } else {
761
- checks.push({ name: agent.name, status: "warn", message: "Not installed" });
762
- }
763
- }
764
- let openaiPort = 18101;
765
- try {
766
- const raw = JSON.parse(fs4.readFileSync(skalpelConfigPath, "utf-8"));
767
- if (typeof raw.openaiPort === "number") openaiPort = raw.openaiPort;
768
- } catch {
769
- }
770
- const config = { openaiPort };
771
- checks.push(checkCodexConfig(config));
772
- checks.push(await checkCodexProxyProbe(config));
773
- checks.push(await checkCodexWebSocket(config));
774
- const icons = { ok: "+", warn: "!", fail: "x", error: "x", skipped: "-" };
775
- for (const check of checks) {
776
- const icon = icons[check.status];
777
- print2(` [${icon}] ${check.name}: ${check.message}`);
778
- }
779
- const failures = checks.filter((c) => c.status === "fail" || c.status === "error");
780
- const warnings = checks.filter((c) => c.status === "warn");
781
- print2("");
782
- if (failures.length > 0) {
783
- print2(` ${failures.length} issue(s) found. Fix the above errors to use Skalpel.`);
784
- } else if (warnings.length > 0) {
785
- print2(` All critical checks passed. ${warnings.length} warning(s).`);
786
- } else {
787
- print2(" All checks passed. Skalpel is ready.");
788
- }
789
- print2("");
790
- }
791
-
792
- // src/cli/benchmark.ts
793
- function print3(msg) {
794
- console.log(msg);
795
- }
796
- async function timedFetch(url, body, headers) {
797
- const start = performance.now();
798
- const response = await fetch(url, {
799
- method: "POST",
800
- headers: { "Content-Type": "application/json", ...headers },
801
- body: JSON.stringify(body)
802
- });
803
- const latencyMs = performance.now() - start;
804
- let responseBody = null;
805
- try {
806
- responseBody = await response.json();
807
- } catch {
808
- }
809
- return { latencyMs, status: response.status, headers: response.headers, body: responseBody };
810
- }
811
- async function runBenchmark() {
812
- print3("");
813
- print3(" Skalpel Benchmark");
814
- print3(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
815
- print3("");
816
- const apiKey = process.env.SKALPEL_API_KEY ?? "";
817
- if (!validateApiKey(apiKey)) {
818
- print3(' Error: SKALPEL_API_KEY not set or invalid. Run "npx skalpel doctor" to diagnose.');
819
- print3("");
820
- process.exit(1);
821
- }
822
- const baseURL = process.env.SKALPEL_BASE_URL ?? "https://api.skalpel.ai";
823
- const testPrompts = [
824
- { model: "gpt-4o-mini", messages: [{ role: "user", content: "Say hello in one word." }] },
825
- { model: "gpt-4o-mini", messages: [{ role: "user", content: "What is 2+2?" }] },
826
- { model: "gpt-4o-mini", messages: [{ role: "user", content: "Say hello in one word." }] }
827
- ];
828
- print3(` Proxy: ${baseURL}`);
829
- print3(` Running ${testPrompts.length} test requests...`);
830
- print3("");
831
- const results = [];
832
- for (let i = 0; i < testPrompts.length; i++) {
833
- const prompt = testPrompts[i];
834
- print3(` Request ${i + 1}/${testPrompts.length}: ${prompt.model} \u2014 "${prompt.messages[0].content}"`);
835
- let proxyLatencyMs = -1;
836
- let savingsUsd = null;
837
- let cacheHit = false;
838
- try {
839
- const proxyResult = await timedFetch(
840
- `${baseURL}/v1/chat/completions`,
841
- prompt,
842
- { Authorization: `Bearer ${apiKey}` }
843
- );
844
- proxyLatencyMs = Math.round(proxyResult.latencyMs);
845
- const savingsHeader = proxyResult.headers.get("x-skalpel-savings-usd");
846
- if (savingsHeader) savingsUsd = parseFloat(savingsHeader);
847
- cacheHit = proxyResult.headers.get("x-skalpel-cache-hit") === "true";
848
- } catch (err) {
849
- print3(` Proxy request failed: ${err instanceof Error ? err.message : String(err)}`);
850
- }
851
- results.push({
852
- requestIndex: i + 1,
853
- model: prompt.model,
854
- directLatencyMs: 0,
855
- proxyLatencyMs,
856
- overheadMs: 0,
857
- savingsUsd,
858
- cacheHit
859
- });
860
- const cacheStr = cacheHit ? " (cache hit)" : "";
861
- const savingsStr = savingsUsd !== null ? ` | savings: $${savingsUsd.toFixed(4)}` : "";
862
- print3(` Proxy: ${proxyLatencyMs}ms${cacheStr}${savingsStr}`);
863
- }
864
- print3("");
865
- print3(" Summary");
866
- print3(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500");
867
- const validResults = results.filter((r) => r.proxyLatencyMs >= 0);
868
- if (validResults.length === 0) {
869
- print3(" No successful requests. Check your API key and proxy endpoint.");
870
- } else {
871
- const avgProxy = Math.round(validResults.reduce((s, r) => s + r.proxyLatencyMs, 0) / validResults.length);
872
- const cacheHits = validResults.filter((r) => r.cacheHit).length;
873
- const totalSavings = validResults.reduce((s, r) => s + (r.savingsUsd ?? 0), 0);
874
- print3(` Requests: ${validResults.length}`);
875
- print3(` Avg latency: ${avgProxy}ms (proxy)`);
876
- print3(` Cache hits: ${cacheHits}/${validResults.length}`);
877
- if (totalSavings > 0) {
878
- print3(` Savings: $${totalSavings.toFixed(4)}`);
879
- }
880
- }
881
- print3("");
882
- }
883
-
884
- // src/cli/replay.ts
885
- import * as fs5 from "fs";
886
- import * as path5 from "path";
887
- function print4(msg) {
888
- console.log(msg);
889
- }
890
- async function runReplay(filePaths) {
891
- print4("");
892
- print4(" Skalpel Replay");
893
- print4(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
894
- print4("");
895
- if (filePaths.length === 0) {
896
- print4(" Usage: skalpel replay <request-file.json> [request-file2.json ...]");
897
- print4("");
898
- print4(" Replays saved request files through the Skalpel proxy.");
899
- print4(' Each file should be a JSON object with "model" and "messages" fields.');
900
- print4("");
901
- process.exit(1);
902
- }
903
- const apiKey = process.env.SKALPEL_API_KEY ?? "";
904
- if (!validateApiKey(apiKey)) {
905
- print4(' Error: SKALPEL_API_KEY not set or invalid. Run "npx skalpel doctor" to diagnose.');
906
- print4("");
907
- process.exit(1);
908
- }
909
- const baseURL = process.env.SKALPEL_BASE_URL ?? "https://api.skalpel.ai";
910
- print4(` Proxy: ${baseURL}`);
911
- print4(` Replaying ${filePaths.length} request file(s)...`);
912
- print4("");
913
- let successCount = 0;
914
- let failCount = 0;
915
- for (const filePath of filePaths) {
916
- const resolved = path5.resolve(filePath);
917
- print4(` File: ${resolved}`);
918
- if (!fs5.existsSync(resolved)) {
919
- print4(` Error: file not found`);
920
- failCount++;
921
- continue;
922
- }
923
- let requestBody;
924
- try {
925
- const raw = fs5.readFileSync(resolved, "utf-8");
926
- requestBody = JSON.parse(raw);
927
- } catch (err) {
928
- print4(` Error: invalid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`);
929
- failCount++;
930
- continue;
931
- }
932
- if (!requestBody.model || !requestBody.messages) {
933
- print4(' Error: request file must contain "model" and "messages" fields');
934
- failCount++;
935
- continue;
936
- }
937
- const model = requestBody.model;
938
- const messageCount = Array.isArray(requestBody.messages) ? requestBody.messages.length : 0;
939
- print4(` Model: ${model} | Messages: ${messageCount}`);
940
- try {
941
- const start = performance.now();
942
- const response = await fetch(`${baseURL}/v1/chat/completions`, {
943
- method: "POST",
944
- headers: {
945
- "Content-Type": "application/json",
946
- Authorization: `Bearer ${apiKey}`
947
- },
948
- body: JSON.stringify(requestBody)
949
- });
950
- const latencyMs = Math.round(performance.now() - start);
951
- if (!response.ok) {
952
- print4(` Failed: HTTP ${response.status}`);
953
- failCount++;
954
- continue;
955
- }
956
- const body = await response.json();
957
- const content = body?.choices?.[0]?.message?.content ?? body?.content?.[0]?.text ?? "(no content)";
958
- const cacheHit = response.headers.get("x-skalpel-cache-hit") === "true";
959
- const savings = response.headers.get("x-skalpel-savings-usd");
960
- print4(` Status: ${response.status} | Latency: ${latencyMs}ms${cacheHit ? " (cache hit)" : ""}`);
961
- if (savings) print4(` Savings: $${parseFloat(savings).toFixed(4)}`);
962
- print4(` Response: ${content.slice(0, 120)}${content.length > 120 ? "..." : ""}`);
963
- successCount++;
964
- } catch (err) {
965
- print4(` Error: ${err instanceof Error ? err.message : String(err)}`);
966
- failCount++;
967
- }
968
- print4("");
969
- }
970
- print4(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
971
- print4(` Done: ${successCount} succeeded, ${failCount} failed`);
972
- print4("");
973
- }
974
-
975
- // src/cli/start.ts
976
- import { spawn } from "child_process";
977
- import path12 from "path";
978
- import { fileURLToPath as fileURLToPath2 } from "url";
979
-
980
- // src/proxy/config.ts
981
- import fs6 from "fs";
982
- import path6 from "path";
983
- import os3 from "os";
984
- function expandHome(filePath) {
985
- if (filePath.startsWith("~")) {
986
- return path6.join(os3.homedir(), filePath.slice(1));
987
- }
988
- return filePath;
989
- }
990
- var DEFAULTS = {
991
- apiKey: "",
992
- remoteBaseUrl: "https://api.skalpel.ai",
993
- anthropicDirectUrl: "https://api.anthropic.com",
994
- openaiDirectUrl: "https://api.openai.com",
995
- anthropicPort: 18100,
996
- openaiPort: 18101,
997
- cursorPort: 18102,
998
- cursorDirectUrl: "https://api.openai.com",
999
- logLevel: "info",
1000
- logFile: "~/.skalpel/logs/proxy.log",
1001
- pidFile: "~/.skalpel/proxy.pid",
1002
- configFile: "~/.skalpel/config.json",
1003
- mode: "proxy"
1004
- };
1005
- function coerceMode(value) {
1006
- return value === "direct" ? "direct" : "proxy";
1007
- }
1008
- function loadConfig(configPath) {
1009
- const filePath = expandHome(configPath ?? DEFAULTS.configFile);
1010
- let fileConfig = {};
1011
- try {
1012
- const raw = fs6.readFileSync(filePath, "utf-8");
1013
- fileConfig = JSON.parse(raw);
1014
- } catch {
1015
- }
1016
- return {
1017
- apiKey: fileConfig.apiKey ?? DEFAULTS.apiKey,
1018
- remoteBaseUrl: fileConfig.remoteBaseUrl ?? DEFAULTS.remoteBaseUrl,
1019
- anthropicDirectUrl: fileConfig.anthropicDirectUrl ?? DEFAULTS.anthropicDirectUrl,
1020
- openaiDirectUrl: fileConfig.openaiDirectUrl ?? DEFAULTS.openaiDirectUrl,
1021
- anthropicPort: fileConfig.anthropicPort ?? DEFAULTS.anthropicPort,
1022
- openaiPort: fileConfig.openaiPort ?? DEFAULTS.openaiPort,
1023
- cursorPort: fileConfig.cursorPort ?? DEFAULTS.cursorPort,
1024
- cursorDirectUrl: fileConfig.cursorDirectUrl ?? DEFAULTS.cursorDirectUrl,
1025
- logLevel: fileConfig.logLevel ?? DEFAULTS.logLevel,
1026
- logFile: expandHome(fileConfig.logFile ?? DEFAULTS.logFile),
1027
- pidFile: expandHome(fileConfig.pidFile ?? DEFAULTS.pidFile),
1028
- configFile: filePath,
1029
- mode: coerceMode(fileConfig.mode)
1030
- };
1031
- }
1032
- function saveConfig(config) {
1033
- const dir = path6.dirname(config.configFile);
1034
- fs6.mkdirSync(dir, { recursive: true });
1035
- const { mode, ...rest } = config;
1036
- const serializable = { ...rest };
1037
- if (mode === "direct") {
1038
- serializable.mode = mode;
1039
- }
1040
- fs6.writeFileSync(config.configFile, JSON.stringify(serializable, null, 2) + "\n");
1041
- }
1042
-
1043
- // src/proxy/pid.ts
1044
- import fs7 from "fs";
1045
- import path7 from "path";
1046
- import { execSync as execSync2 } from "child_process";
1047
- function readPid(pidFile) {
1048
- try {
1049
- const raw = fs7.readFileSync(pidFile, "utf-8").trim();
1050
- try {
1051
- const parsed = JSON.parse(raw);
1052
- if (parsed && typeof parsed === "object" && typeof parsed.pid === "number" && !isNaN(parsed.pid)) {
1053
- const record = parsed;
1054
- if (record.startTime == null) {
1055
- return isRunning(record.pid) ? record.pid : null;
1056
- }
1057
- return isRunningWithIdentity(record.pid, record.startTime) ? record.pid : null;
1058
- }
1059
- } catch {
1060
- }
1061
- const pid = parseInt(raw, 10);
1062
- if (isNaN(pid)) return null;
1063
- return isRunning(pid) ? pid : null;
1064
- } catch {
1065
- return null;
1066
- }
1067
- }
1068
- function isRunning(pid) {
1069
- try {
1070
- process.kill(pid, 0);
1071
- return true;
1072
- } catch {
1073
- return false;
1074
- }
1075
- }
1076
- function getStartTime(pid) {
1077
- try {
1078
- if (process.platform === "linux") {
1079
- const stat = fs7.readFileSync(`/proc/${pid}/stat`, "utf-8");
1080
- const rparen = stat.lastIndexOf(")");
1081
- if (rparen < 0) return null;
1082
- const fields = stat.slice(rparen + 2).split(" ");
1083
- return fields[19] ?? null;
1084
- }
1085
- if (process.platform === "darwin") {
1086
- const out = execSync2(`ps -p ${pid} -o lstart=`, { timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] });
1087
- const text = out.toString().trim();
1088
- return text || null;
1089
- }
1090
- return null;
1091
- } catch {
1092
- return null;
1093
- }
1094
- }
1095
- function isRunningWithIdentity(pid, expectedStartTime) {
1096
- try {
1097
- if (process.platform !== "linux" && process.platform !== "darwin") {
1098
- return isRunning(pid);
1099
- }
1100
- const current = getStartTime(pid);
1101
- if (current == null) return false;
1102
- return current === expectedStartTime;
1103
- } catch {
1104
- return false;
1105
- }
1106
- }
1107
- function removePid(pidFile) {
1108
- try {
1109
- fs7.unlinkSync(pidFile);
1110
- } catch {
1111
- }
1112
- }
1113
-
1114
- // src/proxy/health-check.ts
1115
- async function isProxyAlive(port, timeoutMs = 2e3) {
1116
- const controller = new AbortController();
1117
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1118
- try {
1119
- const res = await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
1120
- return res.ok;
1121
- } catch {
1122
- return false;
1123
- } finally {
1124
- clearTimeout(timer);
1125
- }
1126
- }
1127
-
1128
- // src/cli/service/install.ts
1129
- import fs8 from "fs";
1130
- import path9 from "path";
1131
- import os6 from "os";
1132
- import { execSync as execSync4 } from "child_process";
1133
- import { fileURLToPath } from "url";
1134
-
1135
- // src/cli/service/detect-os.ts
1136
- import os4 from "os";
1137
- import { execSync as execSync3 } from "child_process";
1138
- function detectShell() {
1139
- if (process.platform === "win32") {
1140
- if (process.env.PSModulePath || process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
1141
- return "powershell";
1142
- }
1143
- return "cmd";
1144
- }
1145
- const shellPath = process.env.SHELL ?? "";
1146
- if (shellPath.includes("zsh")) return "zsh";
1147
- if (shellPath.includes("fish")) return "fish";
1148
- if (shellPath.includes("bash")) return "bash";
1149
- try {
1150
- if (process.platform === "darwin") {
1151
- const result = execSync3(`dscl . -read /Users/${os4.userInfo().username} UserShell`, {
1152
- encoding: "utf-8",
1153
- timeout: 3e3
1154
- }).trim();
1155
- const shell = result.split(":").pop()?.trim() ?? "";
1156
- if (shell.includes("zsh")) return "zsh";
1157
- if (shell.includes("fish")) return "fish";
1158
- if (shell.includes("bash")) return "bash";
1159
- } else {
1160
- const result = execSync3(`getent passwd ${os4.userInfo().username}`, {
1161
- encoding: "utf-8",
1162
- timeout: 3e3
1163
- }).trim();
1164
- const shell = result.split(":").pop() ?? "";
1165
- if (shell.includes("zsh")) return "zsh";
1166
- if (shell.includes("fish")) return "fish";
1167
- if (shell.includes("bash")) return "bash";
1168
- }
1169
- } catch {
1170
- }
1171
- return "bash";
1172
- }
1173
- function detectOS() {
1174
- let platform;
1175
- switch (process.platform) {
1176
- case "darwin":
1177
- platform = "macos";
1178
- break;
1179
- case "win32":
1180
- platform = "windows";
1181
- break;
1182
- default:
1183
- platform = "linux";
1184
- break;
1185
- }
1186
- return {
1187
- platform,
1188
- shell: detectShell(),
1189
- homeDir: os4.homedir()
1190
- };
1191
- }
1192
-
1193
- // src/cli/service/templates.ts
1194
- import os5 from "os";
1195
- import path8 from "path";
1196
- function generateLaunchdPlist(config, proxyRunnerPath) {
1197
- const logDir = path8.join(os5.homedir(), ".skalpel", "logs");
1198
- return `<?xml version="1.0" encoding="UTF-8"?>
1199
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1200
- <plist version="1.0">
1201
- <dict>
1202
- <key>Label</key>
1203
- <string>ai.skalpel.proxy</string>
1204
- <key>ProgramArguments</key>
1205
- <array>
1206
- <string>${process.execPath}</string>
1207
- <string>${proxyRunnerPath}</string>
1208
- </array>
1209
- <key>RunAtLoad</key>
1210
- <true/>
1211
- <key>KeepAlive</key>
1212
- <true/>
1213
- <key>StandardOutPath</key>
1214
- <string>${path8.join(logDir, "proxy-stdout.log")}</string>
1215
- <key>StandardErrorPath</key>
1216
- <string>${path8.join(logDir, "proxy-stderr.log")}</string>
1217
- <key>EnvironmentVariables</key>
1218
- <dict>
1219
- <key>SKALPEL_ANTHROPIC_PORT</key>
1220
- <string>${config.anthropicPort}</string>
1221
- <key>SKALPEL_OPENAI_PORT</key>
1222
- <string>${config.openaiPort}</string>
1223
- <key>SKALPEL_CURSOR_PORT</key>
1224
- <string>${config.cursorPort}</string>
1225
- </dict>
1226
- </dict>
1227
- </plist>`;
1228
- }
1229
- function generateSystemdUnit(config, proxyRunnerPath) {
1230
- return `[Unit]
1231
- Description=Skalpel Proxy
1232
- After=network.target
1233
-
1234
- [Service]
1235
- Type=simple
1236
- ExecStart=${process.execPath} ${proxyRunnerPath}
1237
- Restart=always
1238
- RestartSec=5
1239
- Environment=SKALPEL_ANTHROPIC_PORT=${config.anthropicPort}
1240
- Environment=SKALPEL_OPENAI_PORT=${config.openaiPort}
1241
- Environment=SKALPEL_CURSOR_PORT=${config.cursorPort}
1242
-
1243
- [Install]
1244
- WantedBy=default.target`;
1245
- }
1246
- function generateWindowsTask(config, proxyRunnerPath) {
1247
- return [
1248
- "/create",
1249
- "/tn",
1250
- "SkalpelProxy",
1251
- "/tr",
1252
- `"${process.execPath}" "${proxyRunnerPath}"`,
1253
- "/sc",
1254
- "ONLOGON",
1255
- "/rl",
1256
- "LIMITED",
1257
- "/f"
1258
- ];
1259
- }
1260
-
1261
- // src/cli/service/install.ts
1262
- var __dirname = path9.dirname(fileURLToPath(import.meta.url));
1263
- function resolveProxyRunnerPath() {
1264
- const candidates = [
1265
- path9.join(__dirname, "..", "proxy-runner.js"),
1266
- // dist/cli/proxy-runner.js relative to dist/cli/service/
1267
- path9.join(__dirname, "proxy-runner.js"),
1268
- // same dir
1269
- path9.join(__dirname, "..", "..", "cli", "proxy-runner.js")
1270
- // dist/cli/proxy-runner.js from deeper
1271
- ];
1272
- for (const candidate of candidates) {
1273
- if (fs8.existsSync(candidate)) {
1274
- return path9.resolve(candidate);
1275
- }
1276
- }
1277
- try {
1278
- const npmRoot = execSync4("npm root -g", { encoding: "utf-8" }).trim();
1279
- const globalPath = path9.join(npmRoot, "skalpel", "dist", "cli", "proxy-runner.js");
1280
- if (fs8.existsSync(globalPath)) return globalPath;
1281
- } catch {
1282
- }
1283
- const devPath = path9.resolve(process.cwd(), "dist", "cli", "proxy-runner.js");
1284
- return devPath;
1285
- }
1286
- function getMacOSPlistPath() {
1287
- return path9.join(os6.homedir(), "Library", "LaunchAgents", "ai.skalpel.proxy.plist");
1288
- }
1289
- function getLinuxUnitPath() {
1290
- return path9.join(os6.homedir(), ".config", "systemd", "user", "skalpel-proxy.service");
1291
- }
1292
- function installService(config) {
1293
- const osInfo = detectOS();
1294
- const proxyRunnerPath = resolveProxyRunnerPath();
1295
- const logDir = path9.join(os6.homedir(), ".skalpel", "logs");
1296
- fs8.mkdirSync(logDir, { recursive: true });
1297
- switch (osInfo.platform) {
1298
- case "macos": {
1299
- const plistPath = getMacOSPlistPath();
1300
- const plistDir = path9.dirname(plistPath);
1301
- fs8.mkdirSync(plistDir, { recursive: true });
1302
- const plist = generateLaunchdPlist(config, proxyRunnerPath);
1303
- fs8.writeFileSync(plistPath, plist);
1304
- try {
1305
- execSync4(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
1306
- execSync4(`launchctl load "${plistPath}"`, { stdio: "pipe" });
1307
- } catch (err) {
1308
- const msg = err instanceof Error ? err.message : String(err);
1309
- console.warn(` Warning: Could not register launchd service: ${msg}`);
1310
- console.warn(` You can manually load it: launchctl load "${plistPath}"`);
1311
- }
1312
- break;
1313
- }
1314
- case "linux": {
1315
- const unitPath = getLinuxUnitPath();
1316
- const unitDir = path9.dirname(unitPath);
1317
- fs8.mkdirSync(unitDir, { recursive: true });
1318
- const unit = generateSystemdUnit(config, proxyRunnerPath);
1319
- fs8.writeFileSync(unitPath, unit);
1320
- try {
1321
- execSync4("systemctl --user daemon-reload", { stdio: "pipe" });
1322
- execSync4("systemctl --user enable skalpel-proxy", { stdio: "pipe" });
1323
- execSync4("systemctl --user start skalpel-proxy", { stdio: "pipe" });
1324
- } catch {
1325
- try {
1326
- const autostartDir = path9.join(os6.homedir(), ".config", "autostart");
1327
- fs8.mkdirSync(autostartDir, { recursive: true });
1328
- const desktopEntry = `[Desktop Entry]
1329
- Type=Application
1330
- Name=Skalpel Proxy
1331
- Exec=${process.execPath} ${proxyRunnerPath}
1332
- Hidden=false
1333
- NoDisplay=true
1334
- X-GNOME-Autostart-enabled=true
1335
- `;
1336
- fs8.writeFileSync(path9.join(autostartDir, "skalpel-proxy.desktop"), desktopEntry);
1337
- console.warn(" Warning: systemd --user not available. Created .desktop autostart entry instead.");
1338
- } catch (err2) {
1339
- const msg = err2 instanceof Error ? err2.message : String(err2);
1340
- console.warn(` Warning: Could not register service: ${msg}`);
1341
- console.warn(" You can start the proxy manually: skalpel start");
1342
- }
1343
- }
1344
- break;
1345
- }
1346
- case "windows": {
1347
- const args = generateWindowsTask(config, proxyRunnerPath);
1348
- try {
1349
- execSync4(`schtasks ${args.join(" ")}`, { stdio: "pipe" });
1350
- } catch (err) {
1351
- const msg = err instanceof Error ? err.message : String(err);
1352
- console.warn(` Warning: Could not create scheduled task: ${msg}`);
1353
- console.warn(" You can start the proxy manually: skalpel start");
1354
- }
1355
- break;
1356
- }
1357
- }
1358
- }
1359
- function isServiceInstalled() {
1360
- const osInfo = detectOS();
1361
- switch (osInfo.platform) {
1362
- case "macos": {
1363
- const plistPath = getMacOSPlistPath();
1364
- return fs8.existsSync(plistPath);
1365
- }
1366
- case "linux": {
1367
- const unitPath = getLinuxUnitPath();
1368
- return fs8.existsSync(unitPath);
1369
- }
1370
- case "windows": {
1371
- try {
1372
- execSync4("schtasks /query /tn SkalpelProxy", { stdio: "pipe" });
1373
- return true;
1374
- } catch {
1375
- return false;
1376
- }
1377
- }
1378
- }
1379
- }
1380
- function stopService() {
1381
- const osInfo = detectOS();
1382
- switch (osInfo.platform) {
1383
- case "macos": {
1384
- const plistPath = getMacOSPlistPath();
1385
- if (!fs8.existsSync(plistPath)) return;
1386
- try {
1387
- execSync4(`launchctl unload "${plistPath}"`, { stdio: "pipe" });
1388
- } catch {
1389
- }
1390
- break;
1391
- }
1392
- case "linux": {
1393
- try {
1394
- execSync4("systemctl --user stop skalpel-proxy", { stdio: "pipe" });
1395
- } catch {
1396
- }
1397
- break;
1398
- }
1399
- case "windows": {
1400
- try {
1401
- execSync4("schtasks /end /tn SkalpelProxy", { stdio: "pipe" });
1402
- } catch {
1403
- }
1404
- break;
1405
- }
1406
- }
1407
- }
1408
- function startService() {
1409
- const osInfo = detectOS();
1410
- switch (osInfo.platform) {
1411
- case "macos": {
1412
- const plistPath = getMacOSPlistPath();
1413
- if (!fs8.existsSync(plistPath)) return;
1414
- try {
1415
- execSync4(`launchctl load "${plistPath}"`, { stdio: "pipe" });
1416
- } catch {
1417
- }
1418
- break;
1419
- }
1420
- case "linux": {
1421
- try {
1422
- execSync4("systemctl --user start skalpel-proxy", { stdio: "pipe" });
1423
- } catch {
1424
- }
1425
- break;
1426
- }
1427
- case "windows": {
1428
- try {
1429
- execSync4("schtasks /run /tn SkalpelProxy", { stdio: "pipe" });
1430
- } catch {
1431
- }
1432
- break;
1433
- }
1434
- }
1435
- }
1436
- function uninstallService() {
1437
- const osInfo = detectOS();
1438
- switch (osInfo.platform) {
1439
- case "macos": {
1440
- const plistPath = getMacOSPlistPath();
1441
- try {
1442
- execSync4(`launchctl unload "${plistPath}" 2>/dev/null || true`, { stdio: "pipe" });
1443
- } catch {
1444
- }
1445
- if (fs8.existsSync(plistPath)) fs8.unlinkSync(plistPath);
1446
- break;
1447
- }
1448
- case "linux": {
1449
- try {
1450
- execSync4("systemctl --user stop skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
1451
- execSync4("systemctl --user disable skalpel-proxy 2>/dev/null || true", { stdio: "pipe" });
1452
- } catch {
1453
- }
1454
- const unitPath = getLinuxUnitPath();
1455
- if (fs8.existsSync(unitPath)) fs8.unlinkSync(unitPath);
1456
- const desktopPath = path9.join(os6.homedir(), ".config", "autostart", "skalpel-proxy.desktop");
1457
- if (fs8.existsSync(desktopPath)) fs8.unlinkSync(desktopPath);
1458
- break;
1459
- }
1460
- case "windows": {
1461
- try {
1462
- execSync4("schtasks /delete /tn SkalpelProxy /f", { stdio: "pipe" });
1463
- } catch {
1464
- }
1465
- break;
1466
- }
1467
- }
1468
- }
1469
-
1470
- // src/cli/agents/configure.ts
1471
- import fs9 from "fs";
1472
- import path10 from "path";
1473
- import os7 from "os";
1474
- var CURSOR_API_BASE_URL_KEY = "openai.apiBaseUrl";
1475
- var DIRECT_MODE_BASE_URL = "https://api.skalpel.ai";
1476
- var CODEX_DIRECT_PROVIDER_ID = "skalpel";
1477
- var CODEX_PROXY_PROVIDER_ID = "skalpel-proxy";
1478
- function ensureDir(dir) {
1479
- fs9.mkdirSync(dir, { recursive: true });
1480
- }
1481
- function createBackup(filePath) {
1482
- if (fs9.existsSync(filePath)) {
1483
- fs9.copyFileSync(filePath, `${filePath}.skalpel-backup`);
1484
- }
1485
- }
1486
- function readJsonFile(filePath) {
1487
- try {
1488
- return JSON.parse(fs9.readFileSync(filePath, "utf-8"));
1489
- } catch {
1490
- return null;
1491
- }
1492
- }
1493
- function configureClaudeCode(agent, proxyConfig, direct = false) {
1494
- const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1495
- const configDir = path10.dirname(configPath);
1496
- ensureDir(configDir);
1497
- createBackup(configPath);
1498
- const config = readJsonFile(configPath) ?? {};
1499
- if (!config.env || typeof config.env !== "object") {
1500
- config.env = {};
1501
- }
1502
- const env = config.env;
1503
- if (direct) {
1504
- env.ANTHROPIC_BASE_URL = DIRECT_MODE_BASE_URL;
1505
- env.ANTHROPIC_CUSTOM_HEADERS = `X-Skalpel-API-Key: ${proxyConfig.apiKey}`;
1506
- } else {
1507
- env.ANTHROPIC_BASE_URL = `http://localhost:${proxyConfig.anthropicPort}`;
1508
- delete env.ANTHROPIC_CUSTOM_HEADERS;
1509
- }
1510
- fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1511
- }
1512
- function readTomlFile(filePath) {
1513
- try {
1514
- return fs9.readFileSync(filePath, "utf-8");
1515
- } catch {
1516
- return "";
1517
- }
1518
- }
1519
- function setTomlKey(content, key, value) {
1520
- const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*$`, "m");
1521
- const line = `${key} = "${value}"`;
1522
- if (pattern.test(content)) {
1523
- return content.replace(pattern, line);
1524
- }
1525
- const sectionMatch = content.match(/^\[/m);
1526
- if (sectionMatch && sectionMatch.index !== void 0) {
1527
- return content.slice(0, sectionMatch.index) + line + "\n" + content.slice(sectionMatch.index);
1528
- }
1529
- const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
1530
- return content + separator + line + "\n";
1531
- }
1532
- function removeTomlKey(content, key) {
1533
- const pattern = new RegExp(`^${key.replace(".", "\\.")}\\s*=.*\\n?`, "gm");
1534
- return content.replace(pattern, "");
1535
- }
1536
- function buildCodexDirectProviderBlock(apiKey) {
1537
- return [
1538
- `[model_providers.${CODEX_DIRECT_PROVIDER_ID}]`,
1539
- `name = "Skalpel"`,
1540
- `base_url = "${DIRECT_MODE_BASE_URL}"`,
1541
- `wire_api = "responses"`,
1542
- `http_headers = { "X-Skalpel-API-Key" = "${apiKey}" }`
1543
- ].join("\n");
1544
- }
1545
- function upsertCodexDirectProvider(content, apiKey) {
1546
- const sectionHeader = `[model_providers.${CODEX_DIRECT_PROVIDER_ID}]`;
1547
- const block = buildCodexDirectProviderBlock(apiKey);
1548
- const idx = content.indexOf(sectionHeader);
1549
- if (idx === -1) {
1550
- const separator = content.length > 0 && !content.endsWith("\n") ? "\n\n" : content.length > 0 ? "\n" : "";
1551
- return content + separator + block + "\n";
1552
- }
1553
- const after = content.slice(idx + sectionHeader.length);
1554
- const nextHeaderMatch = after.match(/\n\[[^\]]+\]/);
1555
- const end = nextHeaderMatch && nextHeaderMatch.index !== void 0 ? idx + sectionHeader.length + nextHeaderMatch.index : content.length;
1556
- return content.slice(0, idx) + block + content.slice(end);
1557
- }
1558
- function removeCodexDirectProvider(content) {
1559
- const sectionHeader = `[model_providers.${CODEX_DIRECT_PROVIDER_ID}]`;
1560
- const idx = content.indexOf(sectionHeader);
1561
- if (idx === -1) return content;
1562
- const after = content.slice(idx + sectionHeader.length);
1563
- const nextHeaderMatch = after.match(/\n\[[^\]]+\]/);
1564
- const end = nextHeaderMatch && nextHeaderMatch.index !== void 0 ? idx + sectionHeader.length + nextHeaderMatch.index : content.length;
1565
- const before = content.slice(0, idx).replace(/\n+$/, "");
1566
- const rest = content.slice(end).replace(/^\n+/, "");
1567
- return before.length > 0 && rest.length > 0 ? before + "\n" + rest : before + rest;
1568
- }
1569
- function buildCodexProxyProviderBlock(port) {
1570
- return [
1571
- `[model_providers.skalpel-proxy]`,
1572
- `name = "Skalpel Proxy"`,
1573
- `base_url = "http://localhost:${port}/v1"`,
1574
- `wire_api = "responses"`,
1575
- `env_key = "OPENAI_API_KEY"`
1576
- ].join("\n");
1577
- }
1578
- function upsertCodexProxyProvider(content, port) {
1579
- const sectionHeader = `[model_providers.${CODEX_PROXY_PROVIDER_ID}]`;
1580
- const block = buildCodexProxyProviderBlock(port);
1581
- const idx = content.indexOf(sectionHeader);
1582
- if (idx === -1) {
1583
- const separator = content.length > 0 && !content.endsWith("\n") ? "\n\n" : content.length > 0 ? "\n" : "";
1584
- return content + separator + block + "\n";
1585
- }
1586
- const after = content.slice(idx + sectionHeader.length);
1587
- const nextHeaderMatch = after.match(/\n\[[^\]]+\]/);
1588
- const end = nextHeaderMatch && nextHeaderMatch.index !== void 0 ? idx + sectionHeader.length + nextHeaderMatch.index : content.length;
1589
- return content.slice(0, idx) + block + content.slice(end);
1590
- }
1591
- function removeCodexProxyProvider(content) {
1592
- const sectionHeader = `[model_providers.${CODEX_PROXY_PROVIDER_ID}]`;
1593
- const idx = content.indexOf(sectionHeader);
1594
- if (idx === -1) return content;
1595
- const after = content.slice(idx + sectionHeader.length);
1596
- const nextHeaderMatch = after.match(/\n\[[^\]]+\]/);
1597
- const end = nextHeaderMatch && nextHeaderMatch.index !== void 0 ? idx + sectionHeader.length + nextHeaderMatch.index : content.length;
1598
- const before = content.slice(0, idx).replace(/\n+$/, "");
1599
- const rest = content.slice(end).replace(/^\n+/, "");
1600
- return before.length > 0 && rest.length > 0 ? before + "\n" + rest : before + rest;
1601
- }
1602
- function configureCodex(agent, proxyConfig, direct = false) {
1603
- const configDir = process.platform === "win32" ? path10.join(os7.homedir(), "AppData", "Roaming", "codex") : path10.join(os7.homedir(), ".codex");
1604
- const configPath = agent.configPath ?? path10.join(configDir, "config.toml");
1605
- ensureDir(path10.dirname(configPath));
1606
- createBackup(configPath);
1607
- let content = readTomlFile(configPath);
1608
- if (direct) {
1609
- content = removeTomlKey(content, "openai_base_url");
1610
- content = setTomlKey(content, "model_provider", CODEX_DIRECT_PROVIDER_ID);
1611
- content = upsertCodexDirectProvider(content, proxyConfig.apiKey);
1612
- } else {
1613
- content = setTomlKey(content, "openai_base_url", `http://localhost:${proxyConfig.openaiPort}`);
1614
- content = setTomlKey(content, "model_provider", CODEX_PROXY_PROVIDER_ID);
1615
- content = upsertCodexProxyProvider(content, proxyConfig.openaiPort);
1616
- content = removeCodexDirectProvider(content);
1617
- }
1618
- fs9.writeFileSync(configPath, content);
1619
- const oauth = validateCodexOAuth();
1620
- if (!oauth.present) {
1621
- process.stderr.write("OAuth not configured. Run: codex login\n");
1622
- process.stderr.write(" Then re-run: npx skalpel\n");
1623
- process.stderr.write(" (Skalpel will fall back to OPENAI_API_KEY env var if OAuth missing.)\n");
1624
- }
1625
- }
1626
- function getCursorConfigDir() {
1627
- if (process.platform === "darwin") {
1628
- return path10.join(os7.homedir(), "Library", "Application Support", "Cursor", "User");
1629
- } else if (process.platform === "win32") {
1630
- return path10.join(process.env.APPDATA ?? path10.join(os7.homedir(), "AppData", "Roaming"), "Cursor", "User");
1631
- }
1632
- return path10.join(os7.homedir(), ".config", "Cursor", "User");
1633
- }
1634
- function configureCursor(agent, proxyConfig, direct = false) {
1635
- if (direct) {
1636
- console.warn(` [!] cursor: direct mode not supported (no custom-header injection in Cursor settings). Cursor configuration left unchanged.`);
1637
- return;
1638
- }
1639
- const configDir = getCursorConfigDir();
1640
- const configPath = agent.configPath ?? path10.join(configDir, "settings.json");
1641
- ensureDir(path10.dirname(configPath));
1642
- createBackup(configPath);
1643
- const config = readJsonFile(configPath) ?? {};
1644
- const existingUrl = config[CURSOR_API_BASE_URL_KEY];
1645
- if (typeof existingUrl === "string" && existingUrl.length > 0) {
1646
- proxyConfig.cursorDirectUrl = existingUrl;
1647
- saveConfig(proxyConfig);
1648
- }
1649
- config[CURSOR_API_BASE_URL_KEY] = `http://localhost:${proxyConfig.cursorPort}`;
1650
- fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1651
- }
1652
- function configureAgent(agent, proxyConfig, direct = false) {
1653
- switch (agent.name) {
1654
- case "claude-code":
1655
- configureClaudeCode(agent, proxyConfig, direct);
1656
- break;
1657
- case "codex":
1658
- configureCodex(agent, proxyConfig, direct);
1659
- break;
1660
- case "cursor":
1661
- configureCursor(agent, proxyConfig, direct);
1662
- break;
1663
- }
1664
- }
1665
- function unconfigureClaudeCode(agent) {
1666
- const configPath = agent.configPath ?? path10.join(os7.homedir(), ".claude", "settings.json");
1667
- if (!fs9.existsSync(configPath)) return;
1668
- const config = readJsonFile(configPath);
1669
- if (config === null) {
1670
- console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ANTHROPIC_BASE_URL manually if needed.`);
1671
- return;
1672
- }
1673
- if (config.env && typeof config.env === "object") {
1674
- const env = config.env;
1675
- delete env.ANTHROPIC_BASE_URL;
1676
- delete env.ANTHROPIC_CUSTOM_HEADERS;
1677
- if (Object.keys(env).length === 0) {
1678
- delete config.env;
1679
- }
1680
- }
1681
- fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1682
- const backupPath = `${configPath}.skalpel-backup`;
1683
- if (fs9.existsSync(backupPath)) {
1684
- fs9.unlinkSync(backupPath);
1685
- }
1686
- }
1687
- function unconfigureCodex(agent) {
1688
- const configDir = process.platform === "win32" ? path10.join(os7.homedir(), "AppData", "Roaming", "codex") : path10.join(os7.homedir(), ".codex");
1689
- const configPath = agent.configPath ?? path10.join(configDir, "config.toml");
1690
- if (fs9.existsSync(configPath)) {
1691
- let content = readTomlFile(configPath);
1692
- content = removeTomlKey(content, "openai_base_url");
1693
- content = removeTomlKey(content, "model_provider");
1694
- content = removeCodexDirectProvider(content);
1695
- content = removeCodexProxyProvider(content);
1696
- fs9.writeFileSync(configPath, content);
1697
- }
1698
- const backupPath = `${configPath}.skalpel-backup`;
1699
- if (fs9.existsSync(backupPath)) {
1700
- fs9.unlinkSync(backupPath);
1701
- }
1702
- }
1703
- function unconfigureCursor(agent) {
1704
- const configDir = getCursorConfigDir();
1705
- const configPath = agent.configPath ?? path10.join(configDir, "settings.json");
1706
- if (!fs9.existsSync(configPath)) return;
1707
- const config = readJsonFile(configPath);
1708
- if (config === null) {
1709
- console.warn(` [!] Could not parse ${configPath} \u2014 skipping to avoid data loss. Remove ${CURSOR_API_BASE_URL_KEY} manually if needed.`);
1710
- return;
1711
- }
1712
- delete config[CURSOR_API_BASE_URL_KEY];
1713
- fs9.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
1714
- const backupPath = `${configPath}.skalpel-backup`;
1715
- if (fs9.existsSync(backupPath)) {
1716
- fs9.unlinkSync(backupPath);
1717
- }
1718
- }
1719
- function unconfigureAgent(agent) {
1720
- switch (agent.name) {
1721
- case "claude-code":
1722
- unconfigureClaudeCode(agent);
1723
- break;
1724
- case "codex":
1725
- unconfigureCodex(agent);
1726
- break;
1727
- case "cursor":
1728
- unconfigureCursor(agent);
1729
- break;
1730
- }
1731
- }
1732
-
1733
- // src/cli/agents/shell.ts
1734
- import fs10 from "fs";
1735
- import path11 from "path";
1736
- import os8 from "os";
1737
- import crypto from "crypto";
1738
- var BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
1739
- var END_MARKER = "# END SKALPEL PROXY";
1740
- var PS_BEGIN_MARKER = "# BEGIN SKALPEL PROXY - do not edit manually";
1741
- var PS_END_MARKER = "# END SKALPEL PROXY";
1742
- function getUnixProfilePaths() {
1743
- const home = os8.homedir();
1744
- const candidates = [
1745
- path11.join(home, ".bashrc"),
1746
- path11.join(home, ".zshrc"),
1747
- path11.join(home, ".bash_profile"),
1748
- path11.join(home, ".profile")
1749
- ];
1750
- return candidates.filter((p) => fs10.existsSync(p));
1751
- }
1752
- function getPowerShellProfilePath() {
1753
- if (process.platform !== "win32") return null;
1754
- if (process.env.PROFILE) return process.env.PROFILE;
1755
- const docsDir = path11.join(os8.homedir(), "Documents");
1756
- const psProfile = path11.join(docsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1");
1757
- const wpProfile = path11.join(docsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1");
1758
- if (fs10.existsSync(psProfile)) return psProfile;
1759
- if (fs10.existsSync(wpProfile)) return wpProfile;
1760
- return psProfile;
1761
- }
1762
- function randomPlaceholder() {
1763
- return `sk-codex-skalpel-${crypto.randomBytes(8).toString("hex")}`;
1764
- }
1765
- function generateUnixBlock(proxyConfig) {
1766
- return [
1767
- BEGIN_MARKER,
1768
- `export ANTHROPIC_BASE_URL="http://localhost:${proxyConfig.anthropicPort}"`,
1769
- `export OPENAI_BASE_URL="http://localhost:${proxyConfig.openaiPort}"`,
1770
- // `:-` only evaluates the default when OPENAI_API_KEY is unset or empty,
1771
- // so a user-provided key always wins. Bash, zsh, and POSIX sh all
1772
- // support this syntax.
1773
- `export OPENAI_API_KEY="\${OPENAI_API_KEY:-${randomPlaceholder()}}"`,
1774
- END_MARKER
1775
- ].join("\n");
1776
- }
1777
- function generatePowerShellBlock(proxyConfig) {
1778
- return [
1779
- PS_BEGIN_MARKER,
1780
- `$env:ANTHROPIC_BASE_URL = "http://localhost:${proxyConfig.anthropicPort}"`,
1781
- `$env:OPENAI_BASE_URL = "http://localhost:${proxyConfig.openaiPort}"`,
1782
- // Conditional assignment — only set when the user hasn't already
1783
- // exported OPENAI_API_KEY in their session or parent profile.
1784
- `if (-not $env:OPENAI_API_KEY) { $env:OPENAI_API_KEY = "${randomPlaceholder()}" }`,
1785
- PS_END_MARKER
1786
- ].join("\n");
1787
- }
1788
- function createBackup2(filePath) {
1789
- const backupPath = `${filePath}.skalpel-backup`;
1790
- fs10.copyFileSync(filePath, backupPath);
1791
- }
1792
- function updateProfileFile(filePath, block, beginMarker, endMarker) {
1793
- if (fs10.existsSync(filePath)) {
1794
- createBackup2(filePath);
1795
- }
1796
- let content = fs10.existsSync(filePath) ? fs10.readFileSync(filePath, "utf-8") : "";
1797
- const beginIdx = content.indexOf(beginMarker);
1798
- const endIdx = content.indexOf(endMarker);
1799
- if (beginIdx !== -1 && endIdx !== -1) {
1800
- content = content.slice(0, beginIdx) + block + content.slice(endIdx + endMarker.length);
1801
- } else {
1802
- if (content.length > 0) {
1803
- const trimmed = content.replace(/\n+$/, "");
1804
- content = trimmed + "\n\n" + block + "\n";
1805
- } else {
1806
- content = block + "\n";
1807
- }
1808
- }
1809
- fs10.writeFileSync(filePath, content);
1810
- }
1811
- function configureShellEnvVars(_agents, proxyConfig) {
1812
- const modified = [];
1813
- if (process.platform === "win32") {
1814
- const psProfile = getPowerShellProfilePath();
1815
- if (psProfile) {
1816
- const dir = path11.dirname(psProfile);
1817
- fs10.mkdirSync(dir, { recursive: true });
1818
- const block = generatePowerShellBlock(proxyConfig);
1819
- updateProfileFile(psProfile, block, PS_BEGIN_MARKER, PS_END_MARKER);
1820
- modified.push(psProfile);
1821
- }
1822
- } else {
1823
- const profiles = getUnixProfilePaths();
1824
- const block = generateUnixBlock(proxyConfig);
1825
- for (const profilePath of profiles) {
1826
- updateProfileFile(profilePath, block, BEGIN_MARKER, END_MARKER);
1827
- modified.push(profilePath);
1828
- }
1829
- }
1830
- return modified;
1831
- }
1832
- function removeShellEnvVars() {
1833
- const restored = [];
1834
- const home = os8.homedir();
1835
- const allProfiles = [
1836
- path11.join(home, ".bashrc"),
1837
- path11.join(home, ".zshrc"),
1838
- path11.join(home, ".bash_profile"),
1839
- path11.join(home, ".profile")
1840
- ];
1841
- if (process.platform === "win32") {
1842
- const psProfile = getPowerShellProfilePath();
1843
- if (psProfile) allProfiles.push(psProfile);
1844
- }
1845
- for (const profilePath of allProfiles) {
1846
- if (!fs10.existsSync(profilePath)) continue;
1847
- const content = fs10.readFileSync(profilePath, "utf-8");
1848
- const beginIdx = content.indexOf(BEGIN_MARKER);
1849
- const endIdx = content.indexOf(END_MARKER);
1850
- if (beginIdx === -1 || endIdx === -1) continue;
1851
- const before = content.slice(0, beginIdx);
1852
- const after = content.slice(endIdx + END_MARKER.length);
1853
- const cleaned = (before.replace(/\n+$/, "") + after.replace(/^\n+/, "\n")).trimEnd() + "\n";
1854
- fs10.writeFileSync(profilePath, cleaned);
1855
- const backupPath = `${profilePath}.skalpel-backup`;
1856
- if (fs10.existsSync(backupPath)) {
1857
- fs10.unlinkSync(backupPath);
1858
- }
1859
- restored.push(profilePath);
1860
- }
1861
- return restored;
1862
- }
1863
- function writeShellBlock(proxyConfig) {
1864
- return configureShellEnvVars([], proxyConfig);
1865
- }
1866
- function removeShellBlock() {
1867
- return removeShellEnvVars();
1868
- }
1869
-
1870
- // src/cli/start.ts
1871
- function print5(msg) {
1872
- console.log(msg);
1873
- }
1874
- function reconfigureAgents(config) {
1875
- const direct = config.mode === "direct";
1876
- const agents = detectAgents();
1877
- for (const agent of agents) {
1878
- if (agent.installed) {
1879
- try {
1880
- configureAgent(agent, config, direct);
1881
- } catch {
1882
- }
1883
- }
1884
- }
1885
- try {
1886
- configureShellEnvVars(agents.filter((a) => a.installed), config);
1887
- } catch {
1888
- }
1889
- }
1890
- async function runStart() {
1891
- const config = loadConfig();
1892
- if (!config.apiKey) {
1893
- print5(' Error: No API key configured. Run "skalpel init" or set SKALPEL_API_KEY.');
1894
- process.exit(1);
1895
- }
1896
- const existingPid = readPid(config.pidFile);
1897
- if (existingPid !== null) {
1898
- print5(` Proxy is already running (pid=${existingPid}).`);
1899
- return;
1900
- }
1901
- const alive = await isProxyAlive(config.anthropicPort);
1902
- if (alive) {
1903
- print5(" Proxy is already running (detected via health check).");
1904
- return;
1905
- }
1906
- if (isServiceInstalled()) {
1907
- startService();
1908
- reconfigureAgents(config);
1909
- print5(` Skalpel proxy started via system service on ports ${config.anthropicPort}, ${config.openaiPort}, and ${config.cursorPort}`);
1910
- return;
1911
- }
1912
- const dirname = path12.dirname(fileURLToPath2(import.meta.url));
1913
- const runnerScript = path12.resolve(dirname, "proxy-runner.js");
1914
- const child = spawn(process.execPath, [runnerScript], {
1915
- detached: true,
1916
- stdio: "ignore"
1917
- });
1918
- child.unref();
1919
- reconfigureAgents(config);
1920
- print5(` Skalpel proxy started on ports ${config.anthropicPort}, ${config.openaiPort}, and ${config.cursorPort}`);
1921
- }
1922
-
1923
- // src/cli/stop.ts
1924
- import { execSync as execSync5 } from "child_process";
1925
-
1926
- // src/proxy/server.ts
1927
- init_handler();
1928
- import http2 from "http";
1929
-
1930
- // src/proxy/logger.ts
1931
- import fs11 from "fs";
1932
- import path13 from "path";
1933
- var MAX_SIZE = 5 * 1024 * 1024;
1934
-
1935
- // src/proxy/ws-server.ts
1936
- import { WebSocketServer } from "ws";
1937
- var WS_SUBPROTOCOL = "skalpel-codex-v1";
1938
- var wss = new WebSocketServer({
1939
- noServer: true,
1940
- handleProtocols: (protocols) => protocols.has(WS_SUBPROTOCOL) ? WS_SUBPROTOCOL : false
1941
- });
1942
-
1943
- // src/proxy/server.ts
1944
- init_codex_oauth();
1945
- var proxyStartTime = 0;
1946
- function stopProxy(config) {
1947
- const pid = readPid(config.pidFile);
1948
- if (pid === null) return false;
1949
- try {
1950
- process.kill(pid, "SIGTERM");
1951
- } catch {
1952
- }
1953
- removePid(config.pidFile);
1954
- return true;
1955
- }
1956
- async function getProxyStatus(config) {
1957
- const pid = readPid(config.pidFile);
1958
- if (pid !== null) {
1959
- return {
1960
- running: true,
1961
- pid,
1962
- uptime: proxyStartTime > 0 ? Date.now() - proxyStartTime : 0,
1963
- anthropicPort: config.anthropicPort,
1964
- openaiPort: config.openaiPort,
1965
- cursorPort: config.cursorPort
1966
- };
1967
- }
1968
- const alive = await isProxyAlive(config.anthropicPort);
1969
- return {
1970
- running: alive,
1971
- pid: null,
1972
- uptime: 0,
1973
- anthropicPort: config.anthropicPort,
1974
- openaiPort: config.openaiPort,
1975
- cursorPort: config.cursorPort
1976
- };
1977
- }
1978
-
1979
- // src/cli/stop.ts
1980
- function print6(msg) {
1981
- console.log(msg);
1982
- }
1983
- async function runStop() {
1984
- const config = loadConfig();
1985
- if (isServiceInstalled()) {
1986
- stopService();
1987
- }
1988
- const stopped = stopProxy(config);
1989
- if (stopped) {
1990
- print6(" Skalpel proxy stopped.");
1991
- } else {
1992
- const alive = await isProxyAlive(config.anthropicPort);
1993
- if (alive) {
1994
- let killedViaPort = false;
1995
- if (process.platform === "darwin" || process.platform === "linux") {
1996
- try {
1997
- const pids = execSync5(`lsof -ti :${config.anthropicPort}`, { timeout: 3e3 }).toString().trim().split("\n").filter(Boolean);
1998
- for (const p of pids) {
1999
- const pid = parseInt(p, 10);
2000
- if (Number.isInteger(pid) && pid > 0) {
2001
- try {
2002
- process.kill(pid, "SIGTERM");
2003
- } catch {
2004
- }
2005
- }
2006
- }
2007
- killedViaPort = true;
2008
- } catch {
2009
- }
2010
- }
2011
- if (killedViaPort) {
2012
- print6(" Skalpel proxy stopped (found via port detection).");
2013
- } else {
2014
- print6(" Proxy appears to be running but could not be stopped automatically.");
2015
- print6(` Try: kill $(lsof -ti :${config.anthropicPort})`);
2016
- }
2017
- } else {
2018
- print6(" Proxy is not running.");
2019
- }
2020
- }
2021
- const agents = detectAgents();
2022
- for (const agent of agents) {
2023
- if (agent.installed) {
2024
- try {
2025
- unconfigureAgent(agent);
2026
- } catch {
2027
- }
2028
- }
2029
- }
2030
- try {
2031
- removeShellEnvVars();
2032
- } catch {
2033
- }
2034
- }
2035
-
2036
- // src/cli/status.ts
2037
- function print7(msg) {
2038
- console.log(msg);
2039
- }
2040
- async function runStatus() {
2041
- const config = loadConfig();
2042
- const status = await getProxyStatus(config);
2043
- print7("");
2044
- print7(" Skalpel Proxy Status");
2045
- print7(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2046
- print7(` Status: ${status.running ? "running" : "stopped"}`);
2047
- if (status.pid !== null) {
2048
- print7(` PID: ${status.pid}`);
2049
- }
2050
- print7(` Anthropic: port ${status.anthropicPort}`);
2051
- print7(` OpenAI: port ${status.openaiPort}`);
2052
- print7(` Cursor: port ${status.cursorPort}`);
2053
- print7(` Config: ${config.configFile}`);
2054
- print7("");
2055
- }
2056
-
2057
- // src/cli/logs.ts
2058
- import fs12 from "fs";
2059
- function print8(msg) {
2060
- console.log(msg);
2061
- }
2062
- async function runLogs(options) {
2063
- const config = loadConfig();
2064
- const logFile = config.logFile;
2065
- const lineCount = parseInt(options.lines ?? "50", 10);
2066
- if (!fs12.existsSync(logFile)) {
2067
- print8(` No log file found at ${logFile}`);
2068
- return;
2069
- }
2070
- const content = fs12.readFileSync(logFile, "utf-8");
2071
- const lines = content.trimEnd().split("\n");
2072
- const tail = lines.slice(-lineCount);
2073
- for (const line of tail) {
2074
- print8(line);
2075
- }
2076
- if (options.follow) {
2077
- let position = fs12.statSync(logFile).size;
2078
- fs12.watchFile(logFile, { interval: 500 }, () => {
2079
- try {
2080
- const stat = fs12.statSync(logFile);
2081
- if (stat.size > position) {
2082
- const fd = fs12.openSync(logFile, "r");
2083
- const buf = Buffer.alloc(stat.size - position);
2084
- fs12.readSync(fd, buf, 0, buf.length, position);
2085
- fs12.closeSync(fd);
2086
- process.stdout.write(buf.toString("utf-8"));
2087
- position = stat.size;
2088
- }
2089
- } catch {
2090
- }
2091
- });
2092
- }
2093
- }
2094
-
2095
- // src/cli/config-cmd.ts
2096
- function print9(msg) {
2097
- console.log(msg);
2098
- }
2099
- async function runSetMode(mode, config) {
2100
- if (mode !== "direct" && mode !== "proxy") {
2101
- print9(` Invalid mode: ${mode}. Must be 'direct' or 'proxy'.`);
2102
- process.exit(1);
2103
- }
2104
- if (config.mode === mode) {
2105
- print9(` Already in ${mode} mode. Nothing to do.`);
2106
- return;
2107
- }
2108
- config.mode = mode;
2109
- saveConfig(config);
2110
- await applyModeSwitch(mode, config);
2111
- print9(` Switched to ${mode} mode.`);
2112
- }
2113
- async function applyModeSwitch(mode, config) {
2114
- const direct = mode === "direct";
2115
- const agents = detectAgents();
2116
- for (const agent of agents) {
2117
- if (!agent.installed) continue;
2118
- configureAgent(agent, config, direct);
2119
- }
2120
- if (direct) {
2121
- uninstallService();
2122
- removeShellBlock();
2123
- } else {
2124
- installService(config);
2125
- writeShellBlock(config);
2126
- }
2127
- }
2128
- async function runConfig(subcommand, args) {
2129
- const config = loadConfig();
2130
- if (subcommand === "path") {
2131
- print9(config.configFile);
2132
- return;
2133
- }
2134
- if (subcommand === "set") {
2135
- if (!args || args.length < 2) {
2136
- print9(" Usage: skalpel config set <key> <value>");
2137
- process.exit(1);
2138
- }
2139
- const key = args[0];
2140
- const value = args[1];
2141
- if (key === "mode") {
2142
- if (value !== "direct" && value !== "proxy") {
2143
- print9(` Invalid mode: ${value}. Must be 'direct' or 'proxy'.`);
2144
- process.exit(1);
2145
- }
2146
- await runSetMode(value, config);
2147
- return;
2148
- }
2149
- const validKeys = [
2150
- "apiKey",
2151
- "remoteBaseUrl",
2152
- "anthropicPort",
2153
- "openaiPort",
2154
- "cursorPort",
2155
- "cursorDirectUrl",
2156
- "logLevel",
2157
- "logFile",
2158
- "pidFile"
2159
- ];
2160
- if (!validKeys.includes(key)) {
2161
- print9(` Unknown config key: ${key}`);
2162
- print9(` Valid keys: ${validKeys.join(", ")}`);
2163
- process.exit(1);
2164
- }
2165
- const updated = { ...config };
2166
- if (key === "anthropicPort" || key === "openaiPort" || key === "cursorPort") {
2167
- const parsed = parseInt(value, 10);
2168
- if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
2169
- print9(` Invalid port number: ${value}`);
2170
- process.exit(1);
2171
- }
2172
- updated[key] = parsed;
2173
- } else if (key === "logLevel") {
2174
- const validLevels = ["debug", "info", "warn", "error"];
2175
- if (!validLevels.includes(value)) {
2176
- print9(` Invalid log level: ${value}`);
2177
- print9(` Valid levels: ${validLevels.join(", ")}`);
2178
- process.exit(1);
2179
- }
2180
- updated[key] = value;
2181
- } else {
2182
- updated[key] = value;
2183
- }
2184
- saveConfig(updated);
2185
- print9(` Set ${key} = ${value}`);
2186
- return;
2187
- }
2188
- print9(JSON.stringify(config, null, 2));
2189
- }
2190
-
2191
- // src/cli/update.ts
2192
- import { exec } from "child_process";
2193
- import { createRequire } from "module";
2194
- var require2 = createRequire(import.meta.url);
2195
- var pkg = require2("../../package.json");
2196
- function print10(msg) {
2197
- console.log(msg);
2198
- }
2199
- async function runUpdate() {
2200
- print10(` Current version: ${pkg.version}`);
2201
- print10(" Checking for updates...");
2202
- try {
2203
- const latest = await new Promise((resolve2, reject) => {
2204
- exec("npm view skalpel version", (err, stdout) => {
2205
- if (err) reject(err);
2206
- else resolve2(stdout.trim());
2207
- });
2208
- });
2209
- if (latest === pkg.version) {
2210
- print10(` Already on the latest version (${pkg.version}).`);
2211
- return;
2212
- }
2213
- print10(` Updating to ${latest}...`);
2214
- await new Promise((resolve2, reject) => {
2215
- exec("npm install -g skalpel@latest", (err) => {
2216
- if (err) reject(err);
2217
- else resolve2();
2218
- });
2219
- });
2220
- print10(` Updated to ${latest}.`);
2221
- } catch (err) {
2222
- print10(` Update failed: ${err instanceof Error ? err.message : String(err)}`);
2223
- process.exit(1);
2224
- }
2225
- }
2226
-
2227
- // src/cli/wizard.ts
2228
- import * as readline2 from "readline";
2229
- import * as fs13 from "fs";
2230
- import * as path14 from "path";
2231
- import * as os9 from "os";
2232
- function print11(msg) {
2233
- console.log(msg);
2234
- }
2235
- async function runWizard(options) {
2236
- const isAuto = options?.auto === true;
2237
- let rl;
2238
- let ask;
2239
- if (!isAuto) {
2240
- rl = readline2.createInterface({
2241
- input: process.stdin,
2242
- output: process.stdout
2243
- });
2244
- ask = (question) => {
2245
- return new Promise((resolve2) => {
2246
- rl.question(question, (answer) => resolve2(answer.trim()));
2247
- });
2248
- };
2249
- } else {
2250
- ask = () => Promise.resolve("");
2251
- }
2252
- try {
2253
- print11("");
2254
- print11(" _____ _ _ _ ");
2255
- print11(" / ____| | | | | |");
2256
- print11(" | (___ | | ____ _| |_ __ ___| |");
2257
- print11(" \\___ \\| |/ / _` | | '_ \\ / _ \\ |");
2258
- print11(" ____) | < (_| | | |_) | __/ |");
2259
- print11(" |_____/|_|\\_\\__,_|_| .__/ \\___|_|");
2260
- print11(" | | ");
2261
- print11(" |_| ");
2262
- print11("");
2263
- print11(" Welcome to Skalpel! Let's optimize your coding agent costs.");
2264
- 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");
2265
- print11("");
2266
- const skalpelDir2 = path14.join(os9.homedir(), ".skalpel");
2267
- const configPath = path14.join(skalpelDir2, "config.json");
2268
- let apiKey = "";
2269
- if (isAuto && options?.apiKey) {
2270
- apiKey = options.apiKey;
2271
- if (!validateApiKey(apiKey)) {
2272
- print11(' Error: Invalid API key. Must start with "sk-skalpel-" and be at least 20 characters.');
2273
- process.exit(1);
2274
- }
2275
- print11(` API key set: ${apiKey.slice(0, 14)}${"*".repeat(Math.max(0, apiKey.length - 14))}`);
2276
- } else if (isAuto && !options?.apiKey) {
2277
- print11(" Error: --api-key is required when using --auto mode.");
2278
- process.exit(1);
2279
- } else {
2280
- if (fs13.existsSync(configPath)) {
2281
- try {
2282
- const existing = JSON.parse(fs13.readFileSync(configPath, "utf-8"));
2283
- if (existing.apiKey && validateApiKey(existing.apiKey)) {
2284
- const masked = existing.apiKey.slice(0, 14) + "*".repeat(Math.max(0, existing.apiKey.length - 14));
2285
- const useExisting = await ask(` Found existing API key: ${masked}
2286
- Use this key? (Y/n): `);
2287
- if (useExisting.toLowerCase() !== "n") {
2288
- apiKey = existing.apiKey;
2289
- print11(` Using existing API key.`);
2290
- }
2291
- }
2292
- } catch {
2293
- }
2294
- }
2295
- if (!apiKey) {
2296
- apiKey = await ask(" Paste your Skalpel API key (sk-skalpel-...): ");
2297
- if (!validateApiKey(apiKey)) {
2298
- print11(' Error: Invalid API key. Must start with "sk-skalpel-" and be at least 20 characters.');
2299
- rl.close();
2300
- process.exit(1);
2301
- }
2302
- print11(` API key set: ${apiKey.slice(0, 14)}${"*".repeat(Math.max(0, apiKey.length - 14))}`);
2303
- }
2304
- }
2305
- print11("");
2306
- fs13.mkdirSync(skalpelDir2, { recursive: true });
2307
- const proxyConfig = loadConfig(configPath);
2308
- proxyConfig.apiKey = apiKey;
2309
- saveConfig(proxyConfig);
2310
- print11(" Detecting coding agents...");
2311
- const agents = detectAgents();
2312
- const installedAgents = agents.filter((a) => a.installed);
2313
- const notInstalled = agents.filter((a) => !a.installed);
2314
- if (installedAgents.length > 0) {
2315
- for (const agent of installedAgents) {
2316
- const ver = agent.version ? ` v${agent.version}` : "";
2317
- print11(` [+] Found: ${agent.name}${ver}`);
2318
- }
2319
- }
2320
- if (notInstalled.length > 0) {
2321
- for (const agent of notInstalled) {
2322
- print11(` [ ] Not found: ${agent.name}`);
2323
- }
2324
- }
2325
- if (installedAgents.length === 0) {
2326
- print11(" Warning: No coding agents detected. You can install them later.");
2327
- print11(" The proxy will be configured and ready when agents are installed.");
2328
- }
2329
- print11("");
2330
- let agentsToConfigure = installedAgents.filter((a) => {
2331
- if (options?.skipClaude && a.name === "claude-code") return false;
2332
- if (options?.skipCodex && a.name === "codex") return false;
2333
- if (options?.skipCursor && a.name === "cursor") return false;
2334
- return true;
2335
- });
2336
- if (agentsToConfigure.length > 0 && !isAuto) {
2337
- const agentNames = installedAgents.map((a) => a.name).join(", ");
2338
- const confirm = await ask(` Configure ${agentNames}? (Y/n): `);
2339
- if (confirm.toLowerCase() === "n") {
2340
- agentsToConfigure = [];
2341
- }
2342
- }
2343
- print11("");
2344
- if (agentsToConfigure.length > 0) {
2345
- print11(" Configuring agents...");
2346
- for (const agent of agentsToConfigure) {
2347
- configureAgent(agent, proxyConfig);
2348
- print11(` Configured ${agent.name}${agent.configPath ? ` (${agent.configPath})` : ""}`);
2349
- }
2350
- print11("");
2351
- const codexConfigured = agentsToConfigure.some((a) => a.name === "codex");
2352
- if (codexConfigured) {
2353
- print11(" [!] Codex auth: recommended -> run 'codex login' to enable ChatGPT plan billing.");
2354
- print11(" Without OAuth, Skalpel uses the OPENAI_API_KEY env var; if not already set,");
2355
- print11(" Skalpel exports a placeholder so Codex starts. A real sk-... key is only");
2356
- print11(" needed if you have no ChatGPT plan and want pay-as-you-go API billing.");
2357
- print11("");
2358
- }
2359
- }
2360
- print11(" Installing proxy as system service...");
2361
- try {
2362
- installService(proxyConfig);
2363
- print11(" Service installed successfully.");
2364
- } catch (err) {
2365
- const msg = err instanceof Error ? err.message : String(err);
2366
- print11(` Warning: Could not install service: ${msg}`);
2367
- print11(" You can start the proxy manually with: skalpel start");
2368
- }
2369
- print11("");
2370
- print11(" Verifying proxy...");
2371
- let proxyOk = false;
2372
- try {
2373
- const controller = new AbortController();
2374
- const timeout = setTimeout(() => controller.abort(), 3e3);
2375
- const healthUrl = `http://localhost:${proxyConfig.anthropicPort}/health`;
2376
- const res = await fetch(healthUrl, { signal: controller.signal });
2377
- clearTimeout(timeout);
2378
- if (res.ok) {
2379
- print11(` [+] Anthropic proxy (port ${proxyConfig.anthropicPort}): healthy`);
2380
- proxyOk = true;
2381
- } else {
2382
- print11(` [!] Anthropic proxy (port ${proxyConfig.anthropicPort}): HTTP ${res.status}`);
2383
- }
2384
- } catch {
2385
- print11(` [!] Proxy not responding yet. It may take a moment to start.`);
2386
- print11(' Run "npx skalpel status" to check later, or "npx skalpel start" to start manually.');
2387
- }
2388
- try {
2389
- const controller = new AbortController();
2390
- const timeout = setTimeout(() => controller.abort(), 3e3);
2391
- const healthUrl = `http://localhost:${proxyConfig.openaiPort}/health`;
2392
- const res = await fetch(healthUrl, { signal: controller.signal });
2393
- clearTimeout(timeout);
2394
- if (res.ok) {
2395
- print11(` [+] OpenAI proxy (port ${proxyConfig.openaiPort}): healthy`);
2396
- } else {
2397
- print11(` [!] OpenAI proxy (port ${proxyConfig.openaiPort}): HTTP ${res.status}`);
2398
- }
2399
- } catch {
2400
- }
2401
- try {
2402
- const controller = new AbortController();
2403
- const timeout = setTimeout(() => controller.abort(), 3e3);
2404
- const healthUrl = `http://localhost:${proxyConfig.cursorPort}/health`;
2405
- const res = await fetch(healthUrl, { signal: controller.signal });
2406
- clearTimeout(timeout);
2407
- if (res.ok) {
2408
- print11(` [+] Cursor proxy (port ${proxyConfig.cursorPort}): healthy`);
2409
- } else {
2410
- print11(` [!] Cursor proxy (port ${proxyConfig.cursorPort}): HTTP ${res.status}`);
2411
- }
2412
- } catch {
2413
- }
2414
- print11("");
2415
- 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");
2416
- print11("");
2417
- print11(" You're all set! Your coding agents now route through Skalpel.");
2418
- print11("");
2419
- if (agentsToConfigure.length > 0) {
2420
- print11(" Configured agents: " + agentsToConfigure.map((a) => a.name).join(", "));
2421
- }
2422
- print11(" Proxy ports: Anthropic=" + proxyConfig.anthropicPort + ", OpenAI=" + proxyConfig.openaiPort + ", Cursor=" + proxyConfig.cursorPort);
2423
- print11("");
2424
- print11(' Run "npx skalpel status" to check proxy status');
2425
- print11(' Run "npx skalpel doctor" for a full health check');
2426
- print11(' Run "npx skalpel uninstall" to remove everything');
2427
- print11("");
2428
- if (rl) rl.close();
2429
- } catch (err) {
2430
- if (rl) rl.close();
2431
- throw err;
2432
- }
2433
- }
2434
-
2435
- // src/cli/uninstall.ts
2436
- import * as readline3 from "readline";
2437
- import * as fs14 from "fs";
2438
- import * as path15 from "path";
2439
- import * as os10 from "os";
2440
- function print12(msg) {
2441
- console.log(msg);
2442
- }
2443
- async function runUninstall(options) {
2444
- const force = options?.force ?? false;
2445
- const rl = readline3.createInterface({
2446
- input: process.stdin,
2447
- output: process.stdout
2448
- });
2449
- function ask(question) {
2450
- return new Promise((resolve2) => {
2451
- rl.question(question, (answer) => resolve2(answer.trim()));
2452
- });
2453
- }
2454
- try {
2455
- print12("");
2456
- print12(" Skalpel Uninstall");
2457
- print12(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2458
- print12("");
2459
- if (!force) {
2460
- const confirm = await ask(" This will remove Skalpel proxy, service, and agent configurations. Continue? (y/N): ");
2461
- if (confirm.toLowerCase() !== "y") {
2462
- print12(" Aborted.");
2463
- rl.close();
2464
- return;
2465
- }
2466
- print12("");
2467
- }
2468
- const config = loadConfig();
2469
- const removed = [];
2470
- print12(" Removing system service...");
2471
- try {
2472
- uninstallService();
2473
- print12(" [+] Service removed");
2474
- removed.push("system service");
2475
- } catch (err) {
2476
- const msg = err instanceof Error ? err.message : String(err);
2477
- print12(` [!] Could not remove service: ${msg}`);
2478
- }
2479
- print12(" Stopping proxy...");
2480
- const stopped = stopProxy(config);
2481
- if (stopped) {
2482
- print12(" [+] Proxy stopped");
2483
- removed.push("proxy process");
2484
- } else {
2485
- print12(" [ ] Proxy was not running");
2486
- }
2487
- print12(" Removing shell environment variables...");
2488
- const restoredProfiles = removeShellEnvVars();
2489
- if (restoredProfiles.length > 0) {
2490
- for (const p of restoredProfiles) {
2491
- print12(` [+] Restored: ${p}`);
2492
- }
2493
- removed.push("shell env vars");
2494
- } else {
2495
- print12(" [ ] No shell profiles had Skalpel config");
2496
- }
2497
- print12(" Restoring agent configurations...");
2498
- const agents = detectAgents();
2499
- for (const agent of agents) {
2500
- if (agent.installed) {
2501
- try {
2502
- unconfigureAgent(agent);
2503
- print12(` [+] Restored ${agent.name} config`);
2504
- removed.push(`${agent.name} config`);
2505
- } catch (err) {
2506
- const msg = err instanceof Error ? err.message : String(err);
2507
- print12(` [!] Could not restore ${agent.name}: ${msg}`);
2508
- }
2509
- }
2510
- }
2511
- print12("");
2512
- const skalpelDir2 = path15.join(os10.homedir(), ".skalpel");
2513
- if (fs14.existsSync(skalpelDir2)) {
2514
- let shouldRemove = force;
2515
- if (!force) {
2516
- const removeDir = await ask(" Remove ~/.skalpel/ directory (contains config and logs)? (y/N): ");
2517
- shouldRemove = removeDir.toLowerCase() === "y";
2518
- }
2519
- if (shouldRemove) {
2520
- fs14.rmSync(skalpelDir2, { recursive: true, force: true });
2521
- print12(" [+] Removed ~/.skalpel/");
2522
- removed.push("~/.skalpel/ directory");
2523
- }
2524
- }
2525
- print12(" Clearing npx cache...");
2526
- try {
2527
- clearNpxCache();
2528
- print12(" [+] npx cache cleared");
2529
- removed.push("npx cache");
2530
- } catch {
2531
- print12(" [ ] Could not clear npx cache (not critical)");
2532
- }
2533
- print12("");
2534
- print12(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2535
- if (removed.length > 0) {
2536
- print12(" Removed: " + removed.join(", "));
2537
- } else {
2538
- print12(" Nothing to remove.");
2539
- }
2540
- print12(" Skalpel has been uninstalled.");
2541
- if (restoredProfiles.length > 0) {
2542
- print12(" Restart your shell to apply env var changes.");
2543
- }
2544
- print12("");
2545
- rl.close();
2546
- } catch (err) {
2547
- rl.close();
2548
- throw err;
2549
- }
2550
- }
2551
- function clearNpxCache() {
2552
- const npxCacheDir = path15.join(os10.homedir(), ".npm", "_npx");
2553
- if (!fs14.existsSync(npxCacheDir)) return;
2554
- const entries = fs14.readdirSync(npxCacheDir);
2555
- for (const entry of entries) {
2556
- const pkgJsonPath = path15.join(npxCacheDir, entry, "node_modules", "skalpel", "package.json");
2557
- const pkgJsonAlt = path15.join(npxCacheDir, entry, "node_modules", ".package-lock.json");
2558
- if (fs14.existsSync(pkgJsonPath)) {
2559
- fs14.rmSync(path15.join(npxCacheDir, entry), { recursive: true, force: true });
2560
- continue;
2561
- }
2562
- if (fs14.existsSync(pkgJsonAlt)) {
2563
- try {
2564
- const content = fs14.readFileSync(pkgJsonAlt, "utf-8");
2565
- if (content.includes('"skalpel"')) {
2566
- fs14.rmSync(path15.join(npxCacheDir, entry), { recursive: true, force: true });
2567
- }
2568
- } catch {
2569
- }
2570
- }
2571
- }
2572
- }
2573
-
2574
- // src/cli/auth/callback-server.ts
2575
- import * as http3 from "http";
2576
- import * as net2 from "net";
2577
-
2578
- // src/cli/auth/session-storage.ts
2579
- import * as fs15 from "fs";
2580
- import * as os11 from "os";
2581
- import * as path16 from "path";
2582
- function sessionFilePath() {
2583
- return path16.join(os11.homedir(), ".skalpel", "session.json");
2584
- }
2585
- function skalpelDir() {
2586
- return path16.join(os11.homedir(), ".skalpel");
2587
- }
2588
- function isValidSession(value) {
2589
- if (!value || typeof value !== "object") return false;
2590
- const v = value;
2591
- if (typeof v.accessToken !== "string" || v.accessToken.length === 0) return false;
2592
- if (typeof v.refreshToken !== "string" || v.refreshToken.length === 0) return false;
2593
- if (typeof v.expiresAt !== "number" || !Number.isFinite(v.expiresAt)) return false;
2594
- if (!v.user || typeof v.user !== "object") return false;
2595
- const user = v.user;
2596
- if (typeof user.id !== "string" || user.id.length === 0) return false;
2597
- if (typeof user.email !== "string" || user.email.length === 0) return false;
2598
- return true;
2599
- }
2600
- async function readSession() {
2601
- const file = sessionFilePath();
2602
- try {
2603
- const raw = await fs15.promises.readFile(file, "utf-8");
2604
- const parsed = JSON.parse(raw);
2605
- if (!isValidSession(parsed)) return null;
2606
- return parsed;
2607
- } catch (err) {
2608
- if (err.code === "ENOENT") return null;
2609
- return null;
2610
- }
2611
- }
2612
- async function writeSession(session) {
2613
- if (!isValidSession(session)) {
2614
- throw new Error("writeSession: invalid session shape");
2615
- }
2616
- const dir = skalpelDir();
2617
- await fs15.promises.mkdir(dir, { recursive: true, mode: 448 });
2618
- const file = sessionFilePath();
2619
- const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
2620
- const json = JSON.stringify(session, null, 2);
2621
- await fs15.promises.writeFile(tmp, json, { mode: 384 });
2622
- try {
2623
- await fs15.promises.chmod(tmp, 384);
2624
- } catch {
2625
- }
2626
- await fs15.promises.rename(tmp, file);
2627
- try {
2628
- await fs15.promises.chmod(file, 384);
2629
- } catch {
2630
- }
2631
- }
2632
- async function deleteSession() {
2633
- const file = sessionFilePath();
2634
- try {
2635
- await fs15.promises.unlink(file);
2636
- } catch (err) {
2637
- if (err.code === "ENOENT") return;
2638
- throw err;
2639
- }
2640
- }
2641
-
2642
- // src/cli/auth/callback-server.ts
2643
- var MAX_BODY_BYTES = 16 * 1024;
2644
- var DEFAULT_TIMEOUT_MS = 18e4;
2645
- var DEFAULT_ALLOWED_ORIGINS = [
2646
- "https://app.skalpel.ai",
2647
- "https://skalpel.ai"
2648
- ];
2649
- function allowedOrigins() {
2650
- const extras = [];
2651
- const webappUrl = process.env.SKALPEL_WEBAPP_URL;
2652
- if (webappUrl) {
2653
- try {
2654
- const u = new URL(webappUrl);
2655
- extras.push(`${u.protocol}//${u.host}`);
2656
- } catch {
2657
- }
2658
- }
2659
- return [...DEFAULT_ALLOWED_ORIGINS, ...extras];
2660
- }
2661
- function validatePort(port) {
2662
- if (!Number.isInteger(port) || port < 1024 || port > 65535) {
2663
- throw new Error(`Invalid port: ${port} (must be an integer in 1024-65535)`);
2664
- }
2665
- }
2666
- async function findOpenPort(preferred = 51732) {
2667
- if (preferred !== 0) validatePort(preferred);
2668
- const tryBind = (port) => new Promise((resolve2) => {
2669
- const server = net2.createServer();
2670
- server.once("error", () => {
2671
- server.close(() => resolve2(null));
2672
- });
2673
- server.listen(port, "127.0.0.1", () => {
2674
- const address = server.address();
2675
- const boundPort = address && typeof address === "object" ? address.port : null;
2676
- server.close(() => resolve2(boundPort));
2677
- });
2678
- });
2679
- const preferredResult = await tryBind(preferred);
2680
- if (preferredResult !== null) return preferredResult;
2681
- const fallback = await tryBind(0);
2682
- if (fallback !== null) return fallback;
2683
- throw new Error("findOpenPort: no open port available");
2684
- }
2685
- function buildCorsHeaders(origin) {
2686
- const allowed = allowedOrigins();
2687
- const selected = origin && allowed.includes(origin) ? origin : allowed[0];
2688
- return {
2689
- "Access-Control-Allow-Origin": selected,
2690
- "Access-Control-Allow-Methods": "POST, OPTIONS",
2691
- "Access-Control-Allow-Headers": "content-type",
2692
- "Access-Control-Max-Age": "600",
2693
- Vary: "Origin"
2694
- };
2695
- }
2696
- async function startCallbackServer(port, timeoutMsOrOptions = DEFAULT_TIMEOUT_MS, maxBodyBytesArg) {
2697
- validatePort(port);
2698
- const opts = typeof timeoutMsOrOptions === "number" ? { timeoutMs: timeoutMsOrOptions, maxBodyBytes: maxBodyBytesArg } : timeoutMsOrOptions ?? {};
2699
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2700
- const maxBytes = opts.maxBodyBytes ?? MAX_BODY_BYTES;
2701
- return new Promise((resolve2, reject) => {
2702
- let settled = false;
2703
- let timer;
2704
- const server = http3.createServer((req, res) => {
2705
- const origin = req.headers.origin;
2706
- const corsHeaders = buildCorsHeaders(origin);
2707
- if (req.method === "OPTIONS") {
2708
- res.writeHead(204, corsHeaders);
2709
- res.end();
2710
- return;
2711
- }
2712
- if (req.method === "GET" && (req.url === "/callback" || req.url === "/")) {
2713
- res.writeHead(200, {
2714
- ...corsHeaders,
2715
- "Content-Type": "text/html; charset=utf-8"
2716
- });
2717
- res.end(
2718
- '<!doctype html><meta charset="utf-8"><title>Skalpel CLI</title><p>You can close this tab and return to your terminal.</p>'
2719
- );
2720
- return;
2721
- }
2722
- if (req.method !== "POST" || req.url !== "/callback") {
2723
- res.writeHead(404, corsHeaders);
2724
- res.end();
2725
- return;
2726
- }
2727
- const contentType = (req.headers["content-type"] || "").toLowerCase();
2728
- if (!contentType.includes("application/json")) {
2729
- res.writeHead(415, { ...corsHeaders, "Content-Type": "application/json" });
2730
- res.end(JSON.stringify({ error: "Unsupported Media Type" }));
2731
- return;
2732
- }
2733
- let total = 0;
2734
- const chunks = [];
2735
- let aborted = false;
2736
- req.on("data", (chunk) => {
2737
- if (aborted) return;
2738
- total += chunk.length;
2739
- if (total > maxBytes) {
2740
- aborted = true;
2741
- res.writeHead(413, {
2742
- ...corsHeaders,
2743
- "Content-Type": "application/json",
2744
- Connection: "close"
2745
- });
2746
- res.end(JSON.stringify({ error: "Payload too large" }));
2747
- return;
2748
- }
2749
- chunks.push(chunk);
2750
- });
2751
- req.on("end", () => {
2752
- if (aborted) return;
2753
- const raw = Buffer.concat(chunks).toString("utf-8");
2754
- let parsed;
2755
- try {
2756
- parsed = JSON.parse(raw);
2757
- } catch {
2758
- res.writeHead(400, { ...corsHeaders, "Content-Type": "application/json" });
2759
- res.end(JSON.stringify({ error: "Invalid JSON" }));
2760
- return;
2761
- }
2762
- if (!isValidSession(parsed)) {
2763
- res.writeHead(400, { ...corsHeaders, "Content-Type": "application/json" });
2764
- res.end(JSON.stringify({ error: "Invalid session shape" }));
2765
- if (!settled) {
2766
- settled = true;
2767
- if (timer) clearTimeout(timer);
2768
- server.close(() => reject(new Error("Invalid session received")));
2769
- }
2770
- return;
2771
- }
2772
- res.writeHead(200, { ...corsHeaders, "Content-Type": "application/json" });
2773
- res.end(JSON.stringify({ ok: true }));
2774
- if (!settled) {
2775
- settled = true;
2776
- if (timer) clearTimeout(timer);
2777
- server.close(() => resolve2(parsed));
2778
- }
2779
- });
2780
- req.on("error", () => {
2781
- });
2782
- });
2783
- server.once("error", (err) => {
2784
- if (settled) return;
2785
- settled = true;
2786
- if (timer) clearTimeout(timer);
2787
- reject(err);
2788
- });
2789
- server.listen(port, "127.0.0.1", () => {
2790
- timer = setTimeout(() => {
2791
- if (settled) return;
2792
- settled = true;
2793
- server.close(() => reject(new Error("Login timed out")));
2794
- }, timeoutMs);
2795
- if (timer.unref) timer.unref();
2796
- });
2797
- });
2798
- }
2799
-
2800
- // src/cli/auth/browser.ts
2801
- async function openUrl(url) {
2802
- if (!/^https?:\/\//i.test(url)) {
2803
- throw new Error(`openUrl: refusing to open non-http(s) URL: ${url}`);
2804
- }
2805
- try {
2806
- const mod = await import("open");
2807
- const opener = mod.default;
2808
- if (typeof opener !== "function") {
2809
- throw new Error("open package exports no default function");
2810
- }
2811
- await opener(url);
2812
- return { opened: true, fallback: false };
2813
- } catch {
2814
- console.log("");
2815
- console.log(" Could not open your browser automatically.");
2816
- console.log(" Please open this URL manually to continue:");
2817
- console.log("");
2818
- console.log(` ${url}`);
2819
- console.log("");
2820
- return { opened: false, fallback: true };
2821
- }
2822
- }
2823
-
2824
- // src/cli/login.ts
2825
- var DEFAULT_WEBAPP_URL = "https://app.skalpel.ai";
2826
- var DEFAULT_TIMEOUT_MS2 = 18e4;
2827
- async function runLogin(options = {}) {
2828
- const webappUrl = options.webappUrl ?? process.env.SKALPEL_WEBAPP_URL ?? DEFAULT_WEBAPP_URL;
2829
- const preferredPort = options.preferredPort ?? 51732;
2830
- const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
2831
- const log = options.logger ?? console;
2832
- const _findPort = options.findOpenPort ?? findOpenPort;
2833
- const _startServer = options.startCallbackServer ?? startCallbackServer;
2834
- const _openUrl = options.openUrl ?? openUrl;
2835
- const _writeSession = options.writeSession ?? writeSession;
2836
- const port = await _findPort(preferredPort);
2837
- const authorizeUrl = `${webappUrl.replace(/\/$/, "")}/cli/authorize?port=${port}`;
2838
- log.log("");
2839
- log.log(` Opening browser to ${authorizeUrl}`);
2840
- log.log(" Waiting for authentication (timeout 3 min)...");
2841
- log.log("");
2842
- const serverPromise = _startServer(port, timeoutMs);
2843
- try {
2844
- await _openUrl(authorizeUrl);
2845
- } catch {
2846
- log.log(" Browser launch failed. Please open the URL above manually.");
2847
- }
2848
- let session;
2849
- try {
2850
- session = await serverPromise;
2851
- } catch (err) {
2852
- const msg = err instanceof Error ? err.message : String(err);
2853
- log.error("");
2854
- log.error(` Login failed: ${msg}`);
2855
- log.error("");
2856
- process.exitCode = 1;
2857
- throw err;
2858
- }
2859
- await _writeSession(session);
2860
- log.log("");
2861
- log.log(` \u2713 Logged in as ${session.user.email}`);
2862
- log.log("");
2863
- }
2864
-
2865
- // src/cli/logout.ts
2866
- async function runLogout(options = {}) {
2867
- const log = options.logger ?? console;
2868
- const _readSession = options.readSession ?? readSession;
2869
- const _deleteSession = options.deleteSession ?? deleteSession;
2870
- const existing = await _readSession();
2871
- if (!existing) {
2872
- log.log(" Not logged in.");
2873
- return;
2874
- }
2875
- await _deleteSession();
2876
- log.log(` \u2713 Logged out.`);
2877
- }
2878
-
2879
- // src/cli/index.ts
2880
- var require3 = createRequire2(import.meta.url);
2881
- var pkg2 = require3("../../package.json");
2882
- var program = new Command();
2883
- program.name("skalpel").description("Skalpel AI CLI \u2014 optimize your OpenAI and Anthropic API calls").version(pkg2.version).option("--api-key <key>", "Skalpel API key for non-interactive setup").option("--auto", "Run setup in non-interactive mode").option("--skip-claude", "Skip Claude Code configuration").option("--skip-codex", "Skip Codex configuration").option("--skip-cursor", "Skip Cursor configuration").action((options) => runWizard(options));
2884
- program.command("init").description("Initialize Skalpel in your project").action(runInit);
2885
- program.command("doctor").description("Check Skalpel configuration health").action(runDoctor);
2886
- program.command("benchmark").description("Run performance benchmarks").action(runBenchmark);
2887
- program.command("replay").description("Replay saved request files").argument("<files...>", "JSON request files").action(runReplay);
2888
- program.command("start").description("Start the Skalpel proxy").action(runStart);
2889
- program.command("stop").description("Stop the Skalpel proxy").action(runStop);
2890
- program.command("status").description("Show proxy status").action(runStatus);
2891
- program.command("login").description("Log in to your Skalpel account (opens browser)").action(() => runLogin());
2892
- program.command("logout").description("Log out of your Skalpel account").action(() => runLogout());
2893
- 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);
2894
- program.command("config").description("View or edit proxy configuration").argument("[subcommand]", "path | set").argument("[args...]", "Arguments for subcommand").action(runConfig);
2895
- program.command("update").description("Update Skalpel to the latest version").action(runUpdate);
2896
- program.command("setup").description("Run the Skalpel setup wizard").action(runWizard);
2897
- program.command("uninstall").description("Remove Skalpel proxy and configurations").option("--force", "Skip confirmation prompts and remove everything").action(runUninstall);
2898
- program.parse(process.argv);
2899
- //# sourceMappingURL=index.js.map