claudish 1.2.1 → 1.3.1

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 (3) hide show
  1. package/README.md +43 -27
  2. package/dist/index.js +653 -52
  3. package/package.json +5 -4
package/README.md CHANGED
@@ -6,6 +6,9 @@
6
6
 
7
7
  ## Features
8
8
 
9
+ - ✅ **Cross-platform** - Works with both Node.js and Bun (v1.3.0+)
10
+ - ✅ **Universal compatibility** - Use with `npx` or `bunx` - no installation required
11
+ - ✅ **Interactive setup** - Prompts for API key and model if not provided (zero config!)
9
12
  - ✅ **Monitor mode** - Proxy to real Anthropic API and log all traffic (for debugging)
10
13
  - ✅ **Protocol compliance** - 1:1 compatibility with Claude Code communication protocol
11
14
  - ✅ **Snapshot testing** - Comprehensive test suite with 13/13 passing tests
@@ -22,63 +25,74 @@
22
25
 
23
26
  ### Prerequisites
24
27
 
25
- - [Bun](https://bun.sh) - JavaScript runtime
28
+ - **Node.js 18+** or **Bun 1.0+** - JavaScript runtime (either works!)
26
29
  - [Claude Code](https://claude.com/claude-code) - Claude CLI must be installed
27
30
  - [OpenRouter API Key](https://openrouter.ai/keys) - Free tier available
28
31
 
29
32
  ### Install Claudish
30
33
 
31
- **IMPORTANT: Claudish requires Bun runtime for optimal performance.**
34
+ **✨ NEW in v1.3.0: Universal compatibility! Works with both Node.js and Bun.**
32
35
 
33
- **Option 1: Install from npm (recommended)**
36
+ **Option 1: Use without installing (recommended)**
34
37
 
35
38
  ```bash
36
- # Install globally (requires Bun in PATH)
39
+ # With Node.js (works everywhere)
40
+ npx claudish@latest --model x-ai/grok-code-fast-1 "your prompt"
41
+
42
+ # With Bun (faster execution)
43
+ bunx claudish@latest --model openai/gpt-5-codex "your prompt"
44
+ ```
45
+
46
+ **Option 2: Install globally**
47
+
48
+ ```bash
49
+ # With npm (Node.js)
37
50
  npm install -g claudish
38
51
 
39
- # Or use bunx (recommended - always uses Bun)
40
- bunx claudish --version
52
+ # With Bun (faster)
53
+ bun install -g claudish
41
54
  ```
42
55
 
43
- **Option 2: Install from source**
56
+ **Option 3: Install from source**
44
57
 
45
58
  ```bash
46
59
  cd mcp/claudish
47
- bun install
48
- bun run build
49
- bun link
60
+ bun install # or: npm install
61
+ bun run build # or: npm run build
62
+ bun link # or: npm link
50
63
  ```
51
64
 
52
- **Why Bun?** Claudish is built with Bun and runs 10x faster than Node.js for proxy operations. The shebang `#!/usr/bin/env bun` ensures it always runs with Bun, but Bun must be installed on your system.
65
+ **Performance Note:** While Claudish works with both runtimes, Bun offers faster startup times. Both provide identical functionality.
53
66
 
54
67
  ## Quick Start
55
68
 
56
- ### 1. Set up environment
69
+ ### Option 1: Interactive Mode (Easiest)
57
70
 
58
71
  ```bash
59
- # Copy example env file
60
- cp .env.example .env
72
+ # Just run it - will prompt for API key and model
73
+ claudish
61
74
 
62
- # Add your OpenRouter API key
63
- export OPENROUTER_API_KEY=sk-or-v1-...
64
-
65
- # Recommended: Set placeholder to avoid Claude Code's API key prompt
66
- export ANTHROPIC_API_KEY=sk-ant-api03-placeholder
75
+ # Enter your OpenRouter API key when prompted
76
+ # Select a model from the list
77
+ # Start coding!
67
78
  ```
68
79
 
69
- ### 2. Run claudish
80
+ ### Option 2: With Environment Variables
70
81
 
71
82
  ```bash
72
- # Basic usage (auto-approve enabled by default)
83
+ # Set up environment
84
+ export OPENROUTER_API_KEY=sk-or-v1-...
85
+ export ANTHROPIC_API_KEY=sk-ant-api03-placeholder
86
+
87
+ # Run with specific task
73
88
  claudish "implement user authentication"
74
89
 
75
- # Use specific model
90
+ # Or with specific model
76
91
  claudish --model openai/gpt-5-codex "add tests"
77
-
78
- # Fully autonomous mode (auto-approve + dangerous)
79
- claudish --dangerous "refactor codebase"
80
92
  ```
81
93
 
94
+ **Note:** In interactive mode, if `OPENROUTER_API_KEY` is not set, you'll be prompted to enter it. This makes first-time usage super simple!
95
+
82
96
  ## Usage
83
97
 
84
98
  ### Basic Syntax
@@ -107,13 +121,15 @@ claudish [OPTIONS] <claude-args...>
107
121
 
108
122
  | Variable | Description | Required |
109
123
  |----------|-------------|----------|
110
- | `OPENROUTER_API_KEY` | Your OpenRouter API key | Yes |
124
+ | `OPENROUTER_API_KEY` | Your OpenRouter API key | **Optional in interactive mode** (will prompt if not set)<br>✅ **Required in non-interactive mode** |
111
125
  | `ANTHROPIC_API_KEY` | Placeholder to prevent Claude Code dialog (not used for auth) | ✅ **Required** |
112
126
  | `CLAUDISH_MODEL` | Default model to use | ❌ No |
113
127
  | `CLAUDISH_PORT` | Default proxy port | ❌ No |
114
128
  | `CLAUDISH_ACTIVE_MODEL_NAME` | Automatically set by claudish to show active model in status line (read-only) | ❌ No |
115
129
 
116
- **Important:** You MUST set `ANTHROPIC_API_KEY=sk-ant-api03-placeholder` (or any value). Without it, Claude Code will show a dialog, and if you select "No", it will bypass the proxy and use real Anthropic API. Claudish now enforces this requirement.
130
+ **Important Notes:**
131
+ - **NEW in v1.3.0:** In interactive mode, if `OPENROUTER_API_KEY` is not set, you'll be prompted to enter it
132
+ - You MUST set `ANTHROPIC_API_KEY=sk-ant-api03-placeholder` (or any value). Without it, Claude Code will show a dialog
117
133
 
118
134
  ## Available Models
119
135
 
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- #!/usr/bin/env bun
2
- // @bun
1
+ #!/usr/bin/env node
3
2
 
4
3
  // src/claude-runner.ts
5
- import { writeFileSync, unlinkSync } from "fs";
6
- import { tmpdir } from "os";
7
- import { join } from "path";
4
+ import { spawn } from "node:child_process";
5
+ import { writeFileSync, unlinkSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
8
 
9
9
  // src/config.ts
10
10
  var DEFAULT_PORT_RANGE = { start: 3000, end: 9000 };
@@ -84,7 +84,7 @@ function createTempSettingsFile(modelDisplay, port) {
84
84
  const settings = {
85
85
  statusLine: {
86
86
  type: "command",
87
- command: `JSON=$(cat) && DIR=$(basename "$(pwd)") && [ \${#DIR} -gt 15 ] && DIR="\${DIR:0:12}..." || true && COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2) && [ -z "$COST" ] && COST="0" || true && if [ -f "${tokenFilePath}" ]; then TOKENS=$(cat "${tokenFilePath}" 2>/dev/null) && INPUT=$(echo "$TOKENS" | grep -o '"input_tokens":[0-9]*' | grep -o '[0-9]*') && OUTPUT=$(echo "$TOKENS" | grep -o '"output_tokens":[0-9]*' | grep -o '[0-9]*') && TOTAL=$((INPUT + OUTPUT)) && CTX=$(echo "scale=0; (${maxTokens} - $TOTAL) * 100 / ${maxTokens}" | bc 2>/dev/null); else INPUT=0 && OUTPUT=0 && CTX=100; fi && [ -z "$CTX" ] && CTX="100" || true && printf "${CYAN}${BOLD}%s${RESET} ${DIM}\u2022${RESET} ${YELLOW}%s${RESET} ${DIM}\u2022${RESET} ${GREEN}\\$%.3f${RESET} ${DIM}\u2022${RESET} ${MAGENTA}%s%%${RESET}\\n" "$DIR" "$CLAUDISH_ACTIVE_MODEL_NAME" "$COST" "$CTX"`,
87
+ command: `JSON=$(cat) && DIR=$(basename "$(pwd)") && [ \${#DIR} -gt 15 ] && DIR="\${DIR:0:12}..." || true && COST=$(echo "$JSON" | grep -o '"total_cost_usd":[0-9.]*' | cut -d: -f2) && [ -z "$COST" ] && COST="0" || true && if [ -f "${tokenFilePath}" ]; then TOKENS=$(cat "${tokenFilePath}" 2>/dev/null) && INPUT=$(echo "$TOKENS" | grep -o '"input_tokens":[0-9]*' | grep -o '[0-9]*') && OUTPUT=$(echo "$TOKENS" | grep -o '"output_tokens":[0-9]*' | grep -o '[0-9]*') && TOTAL=$((INPUT + OUTPUT)) && CTX=$(echo "scale=0; (${maxTokens} - $TOTAL) * 100 / ${maxTokens}" | bc 2>/dev/null); else INPUT=0 && OUTPUT=0 && CTX=100; fi && [ -z "$CTX" ] && CTX="100" || true && printf "${CYAN}${BOLD}%s${RESET} ${DIM}•${RESET} ${YELLOW}%s${RESET} ${DIM}•${RESET} ${GREEN}\\$%.3f${RESET} ${DIM}•${RESET} ${MAGENTA}%s%%${RESET}\\n" "$DIR" "$CLAUDISH_ACTIVE_MODEL_NAME" "$COST" "$CTX"`,
88
88
  padding: 0
89
89
  }
90
90
  };
@@ -143,14 +143,16 @@ async function runClaudeWithProxy(config, proxyUrl) {
143
143
  log(`[claudish] Arguments: ${claudeArgs.join(" ")}
144
144
  `);
145
145
  }
146
- const proc = Bun.spawn(["claude", ...claudeArgs], {
146
+ const proc = spawn("claude", claudeArgs, {
147
147
  env,
148
- stdout: "inherit",
149
- stderr: "inherit",
150
- stdin: "inherit"
148
+ stdio: "inherit"
151
149
  });
152
150
  setupSignalHandlers(proc, tempSettingsPath, config.quiet);
153
- const exitCode = await proc.exited;
151
+ const exitCode = await new Promise((resolve) => {
152
+ proc.on("exit", (code) => {
153
+ resolve(code ?? 1);
154
+ });
155
+ });
154
156
  try {
155
157
  unlinkSync(tempSettingsPath);
156
158
  } catch (error) {}
@@ -174,11 +176,14 @@ function setupSignalHandlers(proc, tempSettingsPath, quiet) {
174
176
  }
175
177
  async function checkClaudeInstalled() {
176
178
  try {
177
- const proc = Bun.spawn(["which", "claude"], {
178
- stdout: "pipe",
179
- stderr: "pipe"
179
+ const proc = spawn("which", ["claude"], {
180
+ stdio: "ignore"
181
+ });
182
+ const exitCode = await new Promise((resolve) => {
183
+ proc.on("exit", (code) => {
184
+ resolve(code ?? 1);
185
+ });
180
186
  });
181
- const exitCode = await proc.exited;
182
187
  return exitCode === 0;
183
188
  } catch {
184
189
  return false;
@@ -294,6 +299,9 @@ function parseArgs(args) {
294
299
  }
295
300
  i++;
296
301
  }
302
+ if ((!config.claudeArgs || config.claudeArgs.length === 0) && !config.stdin) {
303
+ config.interactive = true;
304
+ }
297
305
  if (config.monitor) {
298
306
  if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.includes("placeholder")) {
299
307
  delete process.env.ANTHROPIC_API_KEY;
@@ -309,27 +317,19 @@ function parseArgs(args) {
309
317
  } else {
310
318
  const apiKey = process.env[ENV.OPENROUTER_API_KEY];
311
319
  if (!apiKey) {
312
- console.error("Error: OPENROUTER_API_KEY environment variable is required");
313
- console.error("Get your API key from: https://openrouter.ai/keys");
314
- process.exit(1);
315
- }
316
- config.openrouterApiKey = apiKey;
317
- if (!process.env.ANTHROPIC_API_KEY) {
318
- console.error(`
319
- Error: ANTHROPIC_API_KEY is not set`);
320
- console.error("This placeholder key is required to prevent Claude Code from prompting.");
321
- console.error("");
322
- console.error("Set it now:");
323
- console.error(" export ANTHROPIC_API_KEY='sk-ant-api03-placeholder'");
324
- console.error("");
325
- console.error("Or add it to your shell profile (~/.zshrc or ~/.bashrc) to set permanently.");
326
- console.error("");
327
- console.error("Note: This key is NOT used for auth - claudish uses OPENROUTER_API_KEY");
328
- process.exit(1);
320
+ if (!config.interactive) {
321
+ console.error("Error: OPENROUTER_API_KEY environment variable is required");
322
+ console.error("Get your API key from: https://openrouter.ai/keys");
323
+ console.error("");
324
+ console.error("Set it now:");
325
+ console.error(" export OPENROUTER_API_KEY='sk-or-v1-...'");
326
+ process.exit(1);
327
+ }
328
+ config.openrouterApiKey = undefined;
329
+ } else {
330
+ config.openrouterApiKey = apiKey;
329
331
  }
330
- }
331
- if (!config.claudeArgs || config.claudeArgs.length === 0) {
332
- config.interactive = true;
332
+ config.anthropicApiKey = process.env.ANTHROPIC_API_KEY;
333
333
  }
334
334
  if (config.quiet === undefined) {
335
335
  config.quiet = !config.interactive;
@@ -340,7 +340,7 @@ Error: ANTHROPIC_API_KEY is not set`);
340
340
  return config;
341
341
  }
342
342
  function printVersion() {
343
- console.log("claudish version 1.2.1");
343
+ console.log("claudish version 1.3.0");
344
344
  }
345
345
  function printHelp() {
346
346
  console.log(`
@@ -371,14 +371,14 @@ OPTIONS:
371
371
  -h, --help Show this help message
372
372
 
373
373
  MODES:
374
- \u2022 Interactive mode (default): Shows model selector, starts persistent session
375
- \u2022 Single-shot mode: Runs one task in headless mode and exits (requires --model)
374
+ Interactive mode (default): Shows model selector, starts persistent session
375
+ Single-shot mode: Runs one task in headless mode and exits (requires --model)
376
376
 
377
377
  NOTES:
378
- \u2022 Permission prompts are SKIPPED by default (--dangerously-skip-permissions)
379
- \u2022 Use --no-auto-approve to enable permission prompts
380
- \u2022 Model selector appears ONLY in interactive mode when --model not specified
381
- \u2022 Use --dangerous to disable sandbox (use with extreme caution!)
378
+ Permission prompts are SKIPPED by default (--dangerously-skip-permissions)
379
+ Use --no-auto-approve to enable permission prompts
380
+ Model selector appears ONLY in interactive mode when --model not specified
381
+ Use --dangerous to disable sandbox (use with extreme caution!)
382
382
 
383
383
  ENVIRONMENT VARIABLES:
384
384
  OPENROUTER_API_KEY Required: Your OpenRouter API key
@@ -451,6 +451,55 @@ Available OpenRouter Models (in priority order):
451
451
 
452
452
  // src/simple-selector.ts
453
453
  import { createInterface } from "readline";
454
+ async function promptForApiKey() {
455
+ return new Promise((resolve) => {
456
+ console.log(`
457
+ \x1B[1m\x1B[36mOpenRouter API Key Required\x1B[0m
458
+ `);
459
+ console.log(`\x1B[2mGet your free API key from: https://openrouter.ai/keys\x1B[0m
460
+ `);
461
+ console.log("Enter your OpenRouter API key:");
462
+ console.log(`\x1B[2m(it will not be saved, only used for this session)\x1B[0m
463
+ `);
464
+ const rl = createInterface({
465
+ input: process.stdin,
466
+ output: process.stdout,
467
+ terminal: false
468
+ });
469
+ let apiKey = null;
470
+ rl.on("line", (input) => {
471
+ const trimmed = input.trim();
472
+ if (!trimmed) {
473
+ console.log("\x1B[31mError: API key cannot be empty\x1B[0m");
474
+ return;
475
+ }
476
+ if (!trimmed.startsWith("sk-or-v1-")) {
477
+ console.log("\x1B[33mWarning: OpenRouter API keys usually start with 'sk-or-v1-'\x1B[0m");
478
+ console.log("\x1B[2mContinuing anyway...\x1B[0m");
479
+ }
480
+ apiKey = trimmed;
481
+ rl.close();
482
+ });
483
+ rl.on("close", () => {
484
+ if (apiKey) {
485
+ process.stdin.pause();
486
+ process.stdin.removeAllListeners("data");
487
+ process.stdin.removeAllListeners("end");
488
+ process.stdin.removeAllListeners("error");
489
+ process.stdin.removeAllListeners("readable");
490
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
491
+ process.stdin.setRawMode(false);
492
+ }
493
+ setTimeout(() => {
494
+ resolve(apiKey);
495
+ }, 200);
496
+ } else {
497
+ console.error("\x1B[31mError: API key is required\x1B[0m");
498
+ process.exit(1);
499
+ }
500
+ });
501
+ });
502
+ }
454
503
  async function selectModelInteractively() {
455
504
  return new Promise((resolve) => {
456
505
  console.log(`
@@ -659,7 +708,7 @@ function logStructured(label, data) {
659
708
  }
660
709
 
661
710
  // src/port-manager.ts
662
- import { createServer } from "net";
711
+ import { createServer } from "node:net";
663
712
  async function findAvailablePort(startPort = 3000, endPort = 9000) {
664
713
  const randomPort = Math.floor(Math.random() * (endPort - startPort + 1)) + startPort;
665
714
  if (await isPortAvailable(randomPort)) {
@@ -2285,8 +2334,550 @@ var cors = (options) => {
2285
2334
  };
2286
2335
  };
2287
2336
 
2337
+ // node_modules/@hono/node-server/dist/index.mjs
2338
+ import { createServer as createServerHTTP } from "http";
2339
+ import { Http2ServerRequest as Http2ServerRequest2 } from "http2";
2340
+ import { Http2ServerRequest } from "http2";
2341
+ import { Readable } from "stream";
2342
+ import crypto from "crypto";
2343
+ var RequestError = class extends Error {
2344
+ constructor(message, options) {
2345
+ super(message, options);
2346
+ this.name = "RequestError";
2347
+ }
2348
+ };
2349
+ var toRequestError = (e) => {
2350
+ if (e instanceof RequestError) {
2351
+ return e;
2352
+ }
2353
+ return new RequestError(e.message, { cause: e });
2354
+ };
2355
+ var GlobalRequest = global.Request;
2356
+ var Request2 = class extends GlobalRequest {
2357
+ constructor(input, options) {
2358
+ if (typeof input === "object" && getRequestCache in input) {
2359
+ input = input[getRequestCache]();
2360
+ }
2361
+ if (typeof options?.body?.getReader !== "undefined") {
2362
+ options.duplex ??= "half";
2363
+ }
2364
+ super(input, options);
2365
+ }
2366
+ };
2367
+ var newHeadersFromIncoming = (incoming) => {
2368
+ const headerRecord = [];
2369
+ const rawHeaders = incoming.rawHeaders;
2370
+ for (let i = 0;i < rawHeaders.length; i += 2) {
2371
+ const { [i]: key, [i + 1]: value } = rawHeaders;
2372
+ if (key.charCodeAt(0) !== 58) {
2373
+ headerRecord.push([key, value]);
2374
+ }
2375
+ }
2376
+ return new Headers(headerRecord);
2377
+ };
2378
+ var wrapBodyStream = Symbol("wrapBodyStream");
2379
+ var newRequestFromIncoming = (method, url, headers, incoming, abortController) => {
2380
+ const init = {
2381
+ method,
2382
+ headers,
2383
+ signal: abortController.signal
2384
+ };
2385
+ if (method === "TRACE") {
2386
+ init.method = "GET";
2387
+ const req = new Request2(url, init);
2388
+ Object.defineProperty(req, "method", {
2389
+ get() {
2390
+ return "TRACE";
2391
+ }
2392
+ });
2393
+ return req;
2394
+ }
2395
+ if (!(method === "GET" || method === "HEAD")) {
2396
+ if ("rawBody" in incoming && incoming.rawBody instanceof Buffer) {
2397
+ init.body = new ReadableStream({
2398
+ start(controller) {
2399
+ controller.enqueue(incoming.rawBody);
2400
+ controller.close();
2401
+ }
2402
+ });
2403
+ } else if (incoming[wrapBodyStream]) {
2404
+ let reader;
2405
+ init.body = new ReadableStream({
2406
+ async pull(controller) {
2407
+ try {
2408
+ reader ||= Readable.toWeb(incoming).getReader();
2409
+ const { done, value } = await reader.read();
2410
+ if (done) {
2411
+ controller.close();
2412
+ } else {
2413
+ controller.enqueue(value);
2414
+ }
2415
+ } catch (error) {
2416
+ controller.error(error);
2417
+ }
2418
+ }
2419
+ });
2420
+ } else {
2421
+ init.body = Readable.toWeb(incoming);
2422
+ }
2423
+ }
2424
+ return new Request2(url, init);
2425
+ };
2426
+ var getRequestCache = Symbol("getRequestCache");
2427
+ var requestCache = Symbol("requestCache");
2428
+ var incomingKey = Symbol("incomingKey");
2429
+ var urlKey = Symbol("urlKey");
2430
+ var headersKey = Symbol("headersKey");
2431
+ var abortControllerKey = Symbol("abortControllerKey");
2432
+ var getAbortController = Symbol("getAbortController");
2433
+ var requestPrototype = {
2434
+ get method() {
2435
+ return this[incomingKey].method || "GET";
2436
+ },
2437
+ get url() {
2438
+ return this[urlKey];
2439
+ },
2440
+ get headers() {
2441
+ return this[headersKey] ||= newHeadersFromIncoming(this[incomingKey]);
2442
+ },
2443
+ [getAbortController]() {
2444
+ this[getRequestCache]();
2445
+ return this[abortControllerKey];
2446
+ },
2447
+ [getRequestCache]() {
2448
+ this[abortControllerKey] ||= new AbortController;
2449
+ return this[requestCache] ||= newRequestFromIncoming(this.method, this[urlKey], this.headers, this[incomingKey], this[abortControllerKey]);
2450
+ }
2451
+ };
2452
+ [
2453
+ "body",
2454
+ "bodyUsed",
2455
+ "cache",
2456
+ "credentials",
2457
+ "destination",
2458
+ "integrity",
2459
+ "mode",
2460
+ "redirect",
2461
+ "referrer",
2462
+ "referrerPolicy",
2463
+ "signal",
2464
+ "keepalive"
2465
+ ].forEach((k) => {
2466
+ Object.defineProperty(requestPrototype, k, {
2467
+ get() {
2468
+ return this[getRequestCache]()[k];
2469
+ }
2470
+ });
2471
+ });
2472
+ ["arrayBuffer", "blob", "clone", "formData", "json", "text"].forEach((k) => {
2473
+ Object.defineProperty(requestPrototype, k, {
2474
+ value: function() {
2475
+ return this[getRequestCache]()[k]();
2476
+ }
2477
+ });
2478
+ });
2479
+ Object.setPrototypeOf(requestPrototype, Request2.prototype);
2480
+ var newRequest = (incoming, defaultHostname) => {
2481
+ const req = Object.create(requestPrototype);
2482
+ req[incomingKey] = incoming;
2483
+ const incomingUrl = incoming.url || "";
2484
+ if (incomingUrl[0] !== "/" && (incomingUrl.startsWith("http://") || incomingUrl.startsWith("https://"))) {
2485
+ if (incoming instanceof Http2ServerRequest) {
2486
+ throw new RequestError("Absolute URL for :path is not allowed in HTTP/2");
2487
+ }
2488
+ try {
2489
+ const url2 = new URL(incomingUrl);
2490
+ req[urlKey] = url2.href;
2491
+ } catch (e) {
2492
+ throw new RequestError("Invalid absolute URL", { cause: e });
2493
+ }
2494
+ return req;
2495
+ }
2496
+ const host = (incoming instanceof Http2ServerRequest ? incoming.authority : incoming.headers.host) || defaultHostname;
2497
+ if (!host) {
2498
+ throw new RequestError("Missing host header");
2499
+ }
2500
+ let scheme;
2501
+ if (incoming instanceof Http2ServerRequest) {
2502
+ scheme = incoming.scheme;
2503
+ if (!(scheme === "http" || scheme === "https")) {
2504
+ throw new RequestError("Unsupported scheme");
2505
+ }
2506
+ } else {
2507
+ scheme = incoming.socket && incoming.socket.encrypted ? "https" : "http";
2508
+ }
2509
+ const url = new URL(`${scheme}://${host}${incomingUrl}`);
2510
+ if (url.hostname.length !== host.length && url.hostname !== host.replace(/:\d+$/, "")) {
2511
+ throw new RequestError("Invalid host header");
2512
+ }
2513
+ req[urlKey] = url.href;
2514
+ return req;
2515
+ };
2516
+ var responseCache = Symbol("responseCache");
2517
+ var getResponseCache = Symbol("getResponseCache");
2518
+ var cacheKey = Symbol("cache");
2519
+ var GlobalResponse = global.Response;
2520
+ var Response2 = class _Response {
2521
+ #body;
2522
+ #init;
2523
+ [getResponseCache]() {
2524
+ delete this[cacheKey];
2525
+ return this[responseCache] ||= new GlobalResponse(this.#body, this.#init);
2526
+ }
2527
+ constructor(body, init) {
2528
+ let headers;
2529
+ this.#body = body;
2530
+ if (init instanceof _Response) {
2531
+ const cachedGlobalResponse = init[responseCache];
2532
+ if (cachedGlobalResponse) {
2533
+ this.#init = cachedGlobalResponse;
2534
+ this[getResponseCache]();
2535
+ return;
2536
+ } else {
2537
+ this.#init = init.#init;
2538
+ headers = new Headers(init.#init.headers);
2539
+ }
2540
+ } else {
2541
+ this.#init = init;
2542
+ }
2543
+ if (typeof body === "string" || typeof body?.getReader !== "undefined" || body instanceof Blob || body instanceof Uint8Array) {
2544
+ headers ||= init?.headers || { "content-type": "text/plain; charset=UTF-8" };
2545
+ this[cacheKey] = [init?.status || 200, body, headers];
2546
+ }
2547
+ }
2548
+ get headers() {
2549
+ const cache = this[cacheKey];
2550
+ if (cache) {
2551
+ if (!(cache[2] instanceof Headers)) {
2552
+ cache[2] = new Headers(cache[2]);
2553
+ }
2554
+ return cache[2];
2555
+ }
2556
+ return this[getResponseCache]().headers;
2557
+ }
2558
+ get status() {
2559
+ return this[cacheKey]?.[0] ?? this[getResponseCache]().status;
2560
+ }
2561
+ get ok() {
2562
+ const status = this.status;
2563
+ return status >= 200 && status < 300;
2564
+ }
2565
+ };
2566
+ ["body", "bodyUsed", "redirected", "statusText", "trailers", "type", "url"].forEach((k) => {
2567
+ Object.defineProperty(Response2.prototype, k, {
2568
+ get() {
2569
+ return this[getResponseCache]()[k];
2570
+ }
2571
+ });
2572
+ });
2573
+ ["arrayBuffer", "blob", "clone", "formData", "json", "text"].forEach((k) => {
2574
+ Object.defineProperty(Response2.prototype, k, {
2575
+ value: function() {
2576
+ return this[getResponseCache]()[k]();
2577
+ }
2578
+ });
2579
+ });
2580
+ Object.setPrototypeOf(Response2, GlobalResponse);
2581
+ Object.setPrototypeOf(Response2.prototype, GlobalResponse.prototype);
2582
+ async function readWithoutBlocking(readPromise) {
2583
+ return Promise.race([readPromise, Promise.resolve().then(() => Promise.resolve(undefined))]);
2584
+ }
2585
+ function writeFromReadableStreamDefaultReader(reader, writable, currentReadPromise) {
2586
+ const cancel = (error) => {
2587
+ reader.cancel(error).catch(() => {});
2588
+ };
2589
+ writable.on("close", cancel);
2590
+ writable.on("error", cancel);
2591
+ (currentReadPromise ?? reader.read()).then(flow, handleStreamError);
2592
+ return reader.closed.finally(() => {
2593
+ writable.off("close", cancel);
2594
+ writable.off("error", cancel);
2595
+ });
2596
+ function handleStreamError(error) {
2597
+ if (error) {
2598
+ writable.destroy(error);
2599
+ }
2600
+ }
2601
+ function onDrain() {
2602
+ reader.read().then(flow, handleStreamError);
2603
+ }
2604
+ function flow({ done, value }) {
2605
+ try {
2606
+ if (done) {
2607
+ writable.end();
2608
+ } else if (!writable.write(value)) {
2609
+ writable.once("drain", onDrain);
2610
+ } else {
2611
+ return reader.read().then(flow, handleStreamError);
2612
+ }
2613
+ } catch (e) {
2614
+ handleStreamError(e);
2615
+ }
2616
+ }
2617
+ }
2618
+ function writeFromReadableStream(stream, writable) {
2619
+ if (stream.locked) {
2620
+ throw new TypeError("ReadableStream is locked.");
2621
+ } else if (writable.destroyed) {
2622
+ return;
2623
+ }
2624
+ return writeFromReadableStreamDefaultReader(stream.getReader(), writable);
2625
+ }
2626
+ var buildOutgoingHttpHeaders = (headers) => {
2627
+ const res = {};
2628
+ if (!(headers instanceof Headers)) {
2629
+ headers = new Headers(headers ?? undefined);
2630
+ }
2631
+ const cookies = [];
2632
+ for (const [k, v] of headers) {
2633
+ if (k === "set-cookie") {
2634
+ cookies.push(v);
2635
+ } else {
2636
+ res[k] = v;
2637
+ }
2638
+ }
2639
+ if (cookies.length > 0) {
2640
+ res["set-cookie"] = cookies;
2641
+ }
2642
+ res["content-type"] ??= "text/plain; charset=UTF-8";
2643
+ return res;
2644
+ };
2645
+ var X_ALREADY_SENT = "x-hono-already-sent";
2646
+ var webFetch = global.fetch;
2647
+ if (typeof global.crypto === "undefined") {
2648
+ global.crypto = crypto;
2649
+ }
2650
+ global.fetch = (info, init) => {
2651
+ init = {
2652
+ compress: false,
2653
+ ...init
2654
+ };
2655
+ return webFetch(info, init);
2656
+ };
2657
+ var outgoingEnded = Symbol("outgoingEnded");
2658
+ var handleRequestError = () => new Response(null, {
2659
+ status: 400
2660
+ });
2661
+ var handleFetchError = (e) => new Response(null, {
2662
+ status: e instanceof Error && (e.name === "TimeoutError" || e.constructor.name === "TimeoutError") ? 504 : 500
2663
+ });
2664
+ var handleResponseError = (e, outgoing) => {
2665
+ const err = e instanceof Error ? e : new Error("unknown error", { cause: e });
2666
+ if (err.code === "ERR_STREAM_PREMATURE_CLOSE") {
2667
+ console.info("The user aborted a request.");
2668
+ } else {
2669
+ console.error(e);
2670
+ if (!outgoing.headersSent) {
2671
+ outgoing.writeHead(500, { "Content-Type": "text/plain" });
2672
+ }
2673
+ outgoing.end(`Error: ${err.message}`);
2674
+ outgoing.destroy(err);
2675
+ }
2676
+ };
2677
+ var flushHeaders = (outgoing) => {
2678
+ if ("flushHeaders" in outgoing && outgoing.writable) {
2679
+ outgoing.flushHeaders();
2680
+ }
2681
+ };
2682
+ var responseViaCache = async (res, outgoing) => {
2683
+ let [status, body, header] = res[cacheKey];
2684
+ if (header instanceof Headers) {
2685
+ header = buildOutgoingHttpHeaders(header);
2686
+ }
2687
+ if (typeof body === "string") {
2688
+ header["Content-Length"] = Buffer.byteLength(body);
2689
+ } else if (body instanceof Uint8Array) {
2690
+ header["Content-Length"] = body.byteLength;
2691
+ } else if (body instanceof Blob) {
2692
+ header["Content-Length"] = body.size;
2693
+ }
2694
+ outgoing.writeHead(status, header);
2695
+ if (typeof body === "string" || body instanceof Uint8Array) {
2696
+ outgoing.end(body);
2697
+ } else if (body instanceof Blob) {
2698
+ outgoing.end(new Uint8Array(await body.arrayBuffer()));
2699
+ } else {
2700
+ flushHeaders(outgoing);
2701
+ await writeFromReadableStream(body, outgoing)?.catch((e) => handleResponseError(e, outgoing));
2702
+ }
2703
+ outgoing[outgoingEnded]?.();
2704
+ };
2705
+ var isPromise = (res) => typeof res.then === "function";
2706
+ var responseViaResponseObject = async (res, outgoing, options = {}) => {
2707
+ if (isPromise(res)) {
2708
+ if (options.errorHandler) {
2709
+ try {
2710
+ res = await res;
2711
+ } catch (err) {
2712
+ const errRes = await options.errorHandler(err);
2713
+ if (!errRes) {
2714
+ return;
2715
+ }
2716
+ res = errRes;
2717
+ }
2718
+ } else {
2719
+ res = await res.catch(handleFetchError);
2720
+ }
2721
+ }
2722
+ if (cacheKey in res) {
2723
+ return responseViaCache(res, outgoing);
2724
+ }
2725
+ const resHeaderRecord = buildOutgoingHttpHeaders(res.headers);
2726
+ if (res.body) {
2727
+ const reader = res.body.getReader();
2728
+ const values = [];
2729
+ let done = false;
2730
+ let currentReadPromise = undefined;
2731
+ if (resHeaderRecord["transfer-encoding"] !== "chunked") {
2732
+ let maxReadCount = 2;
2733
+ for (let i = 0;i < maxReadCount; i++) {
2734
+ currentReadPromise ||= reader.read();
2735
+ const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => {
2736
+ console.error(e);
2737
+ done = true;
2738
+ });
2739
+ if (!chunk) {
2740
+ if (i === 1) {
2741
+ await new Promise((resolve) => setTimeout(resolve));
2742
+ maxReadCount = 3;
2743
+ continue;
2744
+ }
2745
+ break;
2746
+ }
2747
+ currentReadPromise = undefined;
2748
+ if (chunk.value) {
2749
+ values.push(chunk.value);
2750
+ }
2751
+ if (chunk.done) {
2752
+ done = true;
2753
+ break;
2754
+ }
2755
+ }
2756
+ if (done && !("content-length" in resHeaderRecord)) {
2757
+ resHeaderRecord["content-length"] = values.reduce((acc, value) => acc + value.length, 0);
2758
+ }
2759
+ }
2760
+ outgoing.writeHead(res.status, resHeaderRecord);
2761
+ values.forEach((value) => {
2762
+ outgoing.write(value);
2763
+ });
2764
+ if (done) {
2765
+ outgoing.end();
2766
+ } else {
2767
+ if (values.length === 0) {
2768
+ flushHeaders(outgoing);
2769
+ }
2770
+ await writeFromReadableStreamDefaultReader(reader, outgoing, currentReadPromise);
2771
+ }
2772
+ } else if (resHeaderRecord[X_ALREADY_SENT]) {} else {
2773
+ outgoing.writeHead(res.status, resHeaderRecord);
2774
+ outgoing.end();
2775
+ }
2776
+ outgoing[outgoingEnded]?.();
2777
+ };
2778
+ var getRequestListener = (fetchCallback, options = {}) => {
2779
+ const autoCleanupIncoming = options.autoCleanupIncoming ?? true;
2780
+ if (options.overrideGlobalObjects !== false && global.Request !== Request2) {
2781
+ Object.defineProperty(global, "Request", {
2782
+ value: Request2
2783
+ });
2784
+ Object.defineProperty(global, "Response", {
2785
+ value: Response2
2786
+ });
2787
+ }
2788
+ return async (incoming, outgoing) => {
2789
+ let res, req;
2790
+ try {
2791
+ req = newRequest(incoming, options.hostname);
2792
+ let incomingEnded = !autoCleanupIncoming || incoming.method === "GET" || incoming.method === "HEAD";
2793
+ if (!incomingEnded) {
2794
+ incoming[wrapBodyStream] = true;
2795
+ incoming.on("end", () => {
2796
+ incomingEnded = true;
2797
+ });
2798
+ if (incoming instanceof Http2ServerRequest2) {
2799
+ outgoing[outgoingEnded] = () => {
2800
+ if (!incomingEnded) {
2801
+ setTimeout(() => {
2802
+ if (!incomingEnded) {
2803
+ setTimeout(() => {
2804
+ incoming.destroy();
2805
+ outgoing.destroy();
2806
+ });
2807
+ }
2808
+ });
2809
+ }
2810
+ };
2811
+ }
2812
+ }
2813
+ outgoing.on("close", () => {
2814
+ const abortController = req[abortControllerKey];
2815
+ if (abortController) {
2816
+ if (incoming.errored) {
2817
+ req[abortControllerKey].abort(incoming.errored.toString());
2818
+ } else if (!outgoing.writableFinished) {
2819
+ req[abortControllerKey].abort("Client connection prematurely closed.");
2820
+ }
2821
+ }
2822
+ if (!incomingEnded) {
2823
+ setTimeout(() => {
2824
+ if (!incomingEnded) {
2825
+ setTimeout(() => {
2826
+ incoming.destroy();
2827
+ });
2828
+ }
2829
+ });
2830
+ }
2831
+ });
2832
+ res = fetchCallback(req, { incoming, outgoing });
2833
+ if (cacheKey in res) {
2834
+ return responseViaCache(res, outgoing);
2835
+ }
2836
+ } catch (e) {
2837
+ if (!res) {
2838
+ if (options.errorHandler) {
2839
+ res = await options.errorHandler(req ? e : toRequestError(e));
2840
+ if (!res) {
2841
+ return;
2842
+ }
2843
+ } else if (!req) {
2844
+ res = handleRequestError();
2845
+ } else {
2846
+ res = handleFetchError(e);
2847
+ }
2848
+ } else {
2849
+ return handleResponseError(e, outgoing);
2850
+ }
2851
+ }
2852
+ try {
2853
+ return await responseViaResponseObject(res, outgoing, options);
2854
+ } catch (e) {
2855
+ return handleResponseError(e, outgoing);
2856
+ }
2857
+ };
2858
+ };
2859
+ var createAdaptorServer = (options) => {
2860
+ const fetchCallback = options.fetch;
2861
+ const requestListener = getRequestListener(fetchCallback, {
2862
+ hostname: options.hostname,
2863
+ overrideGlobalObjects: options.overrideGlobalObjects,
2864
+ autoCleanupIncoming: options.autoCleanupIncoming
2865
+ });
2866
+ const createServer2 = options.createServer || createServerHTTP;
2867
+ const server = createServer2(options.serverOptions || {}, requestListener);
2868
+ return server;
2869
+ };
2870
+ var serve = (options, listeningListener) => {
2871
+ const server = createAdaptorServer(options);
2872
+ server.listen(options?.port ?? 3000, options.hostname, () => {
2873
+ const serverInfo = server.address();
2874
+ listeningListener && listeningListener(serverInfo);
2875
+ });
2876
+ return server;
2877
+ };
2878
+
2288
2879
  // src/proxy-server.ts
2289
- import { writeFileSync as writeFileSync3 } from "fs";
2880
+ import { writeFileSync as writeFileSync3 } from "node:fs";
2290
2881
 
2291
2882
  // src/transform.ts
2292
2883
  function removeUriFormat(schema) {
@@ -2577,7 +3168,7 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
2577
3168
  const originalHeaders = c.req.header();
2578
3169
  const extractedApiKey = originalHeaders["x-api-key"] || originalHeaders["authorization"] || anthropicApiKey;
2579
3170
  log(`
2580
- === [MONITOR] Claude Code \u2192 Anthropic API Request ===`);
3171
+ === [MONITOR] Claude Code Anthropic API Request ===`);
2581
3172
  log(`Headers received: ${JSON.stringify(originalHeaders, null, 2)}`);
2582
3173
  if (!extractedApiKey) {
2583
3174
  log("[Monitor] WARNING: No API key found in headers!");
@@ -2625,7 +3216,7 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
2625
3216
  let buffer = "";
2626
3217
  let eventLog = "";
2627
3218
  log(`
2628
- === [MONITOR] Anthropic API \u2192 Claude Code Response (Streaming) ===`);
3219
+ === [MONITOR] Anthropic API Claude Code Response (Streaming) ===`);
2629
3220
  try {
2630
3221
  while (true) {
2631
3222
  const { done, value } = await reader.read();
@@ -2667,7 +3258,7 @@ async function createProxyServer(port, openrouterApiKey, model, monitorMode = fa
2667
3258
  }
2668
3259
  const responseData = await anthropicResponse.json();
2669
3260
  log(`
2670
- === [MONITOR] Anthropic API \u2192 Claude Code Response (JSON) ===`);
3261
+ === [MONITOR] Anthropic API Claude Code Response (JSON) ===`);
2671
3262
  log(JSON.stringify(responseData, null, 2));
2672
3263
  log(`=== End Response ===
2673
3264
  `);
@@ -3484,11 +4075,10 @@ data: ${JSON.stringify(data)}
3484
4075
  }, 400);
3485
4076
  }
3486
4077
  });
3487
- const server = Bun.serve({
4078
+ const server = serve({
4079
+ fetch: app.fetch,
3488
4080
  port,
3489
- hostname: "127.0.0.1",
3490
- idleTimeout: 255,
3491
- fetch: app.fetch
4081
+ hostname: "127.0.0.1"
3492
4082
  });
3493
4083
  if (monitorMode) {
3494
4084
  log(`[Monitor] Server started on http://127.0.0.1:${port}`);
@@ -3502,7 +4092,14 @@ data: ${JSON.stringify(data)}
3502
4092
  port,
3503
4093
  url: `http://127.0.0.1:${port}`,
3504
4094
  shutdown: async () => {
3505
- server.stop();
4095
+ await new Promise((resolve, reject) => {
4096
+ server.close((err) => {
4097
+ if (err)
4098
+ reject(err);
4099
+ else
4100
+ resolve();
4101
+ });
4102
+ });
3506
4103
  log("[Proxy] Server stopped");
3507
4104
  }
3508
4105
  };
@@ -3560,6 +4157,10 @@ async function main() {
3560
4157
  console.error("Install it from: https://claude.com/claude-code");
3561
4158
  process.exit(1);
3562
4159
  }
4160
+ if (config.interactive && !config.monitor && !config.openrouterApiKey) {
4161
+ config.openrouterApiKey = await promptForApiKey();
4162
+ console.log("");
4163
+ }
3563
4164
  if (config.interactive && !config.monitor && !config.model) {
3564
4165
  config.model = await selectModelInteractively();
3565
4166
  console.log("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudish",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "CLI tool to run Claude Code with any OpenRouter model (Grok, GPT-5, MiniMax, etc.) via local Anthropic API-compatible proxy",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "dev:grok": "bun run src/index.ts --interactive --model x-ai/grok-code-fast-1",
13
13
  "dev:grok:debug": "bun run src/index.ts --interactive --debug --log-level info --model x-ai/grok-code-fast-1",
14
14
  "dev:info": "bun run src/index.ts --interactive --monitor",
15
- "build": "bun build src/index.ts --outdir dist --target bun && chmod +x dist/index.js",
15
+ "build": "bun build src/index.ts --outdir dist --target node && chmod +x dist/index.js",
16
16
  "link": "npm link",
17
17
  "unlink": "npm unlink -g claudish",
18
18
  "install-global": "bun run build && npm link",
@@ -25,12 +25,12 @@
25
25
  "postinstall": "node scripts/postinstall.cjs"
26
26
  },
27
27
  "dependencies": {
28
- "hono": "^4.9.0"
28
+ "hono": "^4.9.0",
29
+ "@hono/node-server": "^1.13.7"
29
30
  },
30
31
  "devDependencies": {
31
32
  "@biomejs/biome": "^1.9.4",
32
33
  "@types/bun": "latest",
33
- "@types/react": "^19.2.2",
34
34
  "typescript": "^5.7.0"
35
35
  },
36
36
  "files": [
@@ -38,6 +38,7 @@
38
38
  "scripts/"
39
39
  ],
40
40
  "engines": {
41
+ "node": ">=18.0.0",
41
42
  "bun": ">=1.0.0"
42
43
  },
43
44
  "preferGlobal": true,