@gullabs/claude-cli 0.7.0 → 0.16.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.
package/dist/index.js CHANGED
@@ -1,11 +1,109 @@
1
1
  import { mkdtemp, rm } from 'fs/promises';
2
2
  import { tmpdir } from 'os';
3
3
  import { join } from 'path';
4
- import { zodToStandardSchema, toConfigJsonSchema, createModelRegistry, LlmError } from '@gullabs/core';
4
+ import { zodToStandardSchema, toConfigJsonSchema, toConfigKeys, createModelRegistry, LlmError } from '@gullabs/core';
5
5
  import { spawn } from 'child_process';
6
+ import { StringDecoder } from 'string_decoder';
6
7
  import { z } from 'zod';
7
8
 
8
9
  // src/adapter.ts
10
+ var INHERITED_NAMES = /* @__PURE__ */ new Set([
11
+ // Process basics.
12
+ "PATH",
13
+ "HOME",
14
+ "USER",
15
+ "LOGNAME",
16
+ "LANG",
17
+ "LANGUAGE",
18
+ "TERM",
19
+ "TMPDIR",
20
+ "TEMP",
21
+ "TMP",
22
+ "SHELL",
23
+ "TZ",
24
+ // Windows equivalents of the above.
25
+ "SYSTEMROOT",
26
+ "SYSTEMDRIVE",
27
+ "WINDIR",
28
+ "COMSPEC",
29
+ "PATHEXT",
30
+ "USERPROFILE",
31
+ "APPDATA",
32
+ "LOCALAPPDATA",
33
+ "PROGRAMDATA",
34
+ "HOMEDRIVE",
35
+ "HOMEPATH",
36
+ // Network: proxies and trust roots.
37
+ "HTTPS_PROXY",
38
+ "HTTP_PROXY",
39
+ "ALL_PROXY",
40
+ "NO_PROXY",
41
+ "https_proxy",
42
+ "http_proxy",
43
+ "all_proxy",
44
+ "no_proxy",
45
+ "NODE_EXTRA_CA_CERTS",
46
+ "SSL_CERT_FILE",
47
+ "SSL_CERT_DIR",
48
+ // The CLI's own documented settings that keep the subscription login usable:
49
+ // where its login lives, the long-lived subscription token from
50
+ // `claude setup-token`, and client certificates for an mTLS proxy.
51
+ "CLAUDE_CONFIG_DIR",
52
+ "CLAUDE_CODE_OAUTH_TOKEN",
53
+ "CLAUDE_CODE_CLIENT_CERT",
54
+ "CLAUDE_CODE_CLIENT_KEY",
55
+ "CLAUDE_CODE_CLIENT_KEY_PASSPHRASE",
56
+ "CLAUDE_CODE_PROXY_RESOLVES_HOSTS"
57
+ ]);
58
+ var INHERITED_PREFIXES = ["LC_", "XDG_"];
59
+ function isInherited(name, windows) {
60
+ const key = windows ? name.toUpperCase() : name;
61
+ if (INHERITED_NAMES.has(key)) return true;
62
+ return INHERITED_PREFIXES.some((prefix) => key.startsWith(prefix));
63
+ }
64
+ function buildChildEnv(parent, extra, windows) {
65
+ const env = {};
66
+ for (const [name, value] of Object.entries(parent)) {
67
+ if (value !== void 0 && isInherited(name, windows)) env[name] = value;
68
+ }
69
+ if (extra !== void 0) {
70
+ if (windows) {
71
+ const overridden = new Set(Object.keys(extra).map((name) => name.toUpperCase()));
72
+ for (const name of Object.keys(env)) {
73
+ if (overridden.has(name.toUpperCase())) delete env[name];
74
+ }
75
+ }
76
+ Object.assign(env, extra);
77
+ }
78
+ return env;
79
+ }
80
+ function badEnv(message) {
81
+ return new LlmError(message, {
82
+ kind: "bad_request",
83
+ retryable: false,
84
+ provider: "claude-cli"
85
+ });
86
+ }
87
+ function parseExtraEnv(env) {
88
+ if (env === void 0) return void 0;
89
+ if (env === null || typeof env !== "object" || Array.isArray(env)) {
90
+ throw badEnv("claudeCliAdapter: `env` must be an object of string values");
91
+ }
92
+ const copy = {};
93
+ for (const [name, value] of Object.entries(env)) {
94
+ if (name === "" || name.includes("=") || name.includes("\0")) {
95
+ throw badEnv(`claudeCliAdapter: \`env\` has an invalid name "${name}"`);
96
+ }
97
+ if (typeof value !== "string" || value.includes("\0")) {
98
+ throw badEnv(
99
+ `claudeCliAdapter: \`env.${name}\` must be a string without NUL characters`
100
+ );
101
+ }
102
+ copy[name] = value;
103
+ }
104
+ return Object.freeze(copy);
105
+ }
106
+ var MAX_STDOUT_BYTES = 32 * 1024 * 1024;
9
107
  function buildClaudeCliRunner(claudePath = "claude") {
10
108
  return {
11
109
  run(args, input, opts) {
@@ -18,9 +116,14 @@ function buildClaudeCliRunner(claudePath = "claude") {
18
116
  }
19
117
  const child = spawn(claudePath, args, {
20
118
  cwd: opts.cwd,
21
- stdio: ["pipe", "pipe", "pipe"]
119
+ stdio: ["pipe", "pipe", "pipe"],
120
+ detached: process.platform !== "win32",
121
+ env: buildChildEnv(process.env, opts.env, process.platform === "win32")
22
122
  });
123
+ const stdoutDecoder = new StringDecoder("utf8");
124
+ const stderrDecoder = new StringDecoder("utf8");
23
125
  let stdout = "";
126
+ let stdoutBytes = 0;
24
127
  let stderr = "";
25
128
  let settled = false;
26
129
  let pendingError;
@@ -36,12 +139,31 @@ function buildClaudeCliRunner(claudePath = "claude") {
36
139
  opts.signal.removeEventListener("abort", onAbort);
37
140
  }
38
141
  };
142
+ const signalTree = (signal) => {
143
+ if (process.platform !== "win32" && child.pid !== void 0) {
144
+ try {
145
+ process.kill(-child.pid, signal);
146
+ return;
147
+ } catch {
148
+ }
149
+ }
150
+ child.kill(signal);
151
+ };
39
152
  const killChild = () => {
40
- child.kill("SIGTERM");
153
+ signalTree("SIGTERM");
41
154
  killTimeoutHandle = setTimeout(() => {
42
- child.kill("SIGKILL");
155
+ signalTree("SIGKILL");
156
+ child.stdout.destroy();
157
+ child.stderr.destroy();
43
158
  }, 5e3);
44
159
  };
160
+ const sweepGroup = () => {
161
+ if (process.platform === "win32" || child.pid === void 0) return;
162
+ try {
163
+ process.kill(-child.pid, "SIGKILL");
164
+ } catch {
165
+ }
166
+ };
45
167
  const beginReject = (err) => {
46
168
  if (settled || pendingError !== void 0) return;
47
169
  pendingError = err;
@@ -64,10 +186,22 @@ function buildClaudeCliRunner(claudePath = "claude") {
64
186
  }, opts.timeoutMs);
65
187
  }
66
188
  child.stdout.on("data", (chunk) => {
67
- stdout += chunk.toString("utf8");
189
+ if (pendingError !== void 0) return;
190
+ stdoutBytes += chunk.length;
191
+ if (stdoutBytes > MAX_STDOUT_BYTES) {
192
+ const err = new Error(
193
+ `claude-cli stdout exceeded ${MAX_STDOUT_BYTES} bytes; the process was killed`
194
+ );
195
+ err.name = "OutputLimitError";
196
+ beginReject(err);
197
+ return;
198
+ }
199
+ stdout += stdoutDecoder.write(chunk);
68
200
  });
69
201
  child.stderr.on("data", (chunk) => {
70
- stderr += chunk.toString("utf8");
202
+ stderr = (stderr + stderrDecoder.write(chunk)).slice(-1048576);
203
+ });
204
+ child.stdin.on("error", () => {
71
205
  });
72
206
  child.once("error", (err) => {
73
207
  if (settled) return;
@@ -80,8 +214,11 @@ function buildClaudeCliRunner(claudePath = "claude") {
80
214
  settled = true;
81
215
  cleanup();
82
216
  if (pendingError !== void 0) {
217
+ sweepGroup();
83
218
  reject(pendingError);
84
219
  } else {
220
+ stdout += stdoutDecoder.end();
221
+ stderr = (stderr + stderrDecoder.end()).slice(-1048576);
85
222
  resolve({ stdout, stderr, exitCode });
86
223
  }
87
224
  });
@@ -92,31 +229,52 @@ function buildClaudeCliRunner(claudePath = "claude") {
92
229
  }
93
230
 
94
231
  // src/adapter.ts
232
+ function abortError() {
233
+ const err = new Error("claude-cli call aborted");
234
+ err.name = "AbortError";
235
+ return err;
236
+ }
95
237
  var Semaphore = class {
96
238
  available;
97
- queue = [];
239
+ waiters = [];
98
240
  constructor(max) {
99
241
  this.available = max;
100
242
  }
101
- async acquire() {
243
+ /**
244
+ * Take a slot. A caller that has to wait leaves the queue, and rejects with an
245
+ * `AbortError`, as soon as `signal` fires: a call the engine already gave up
246
+ * on must not hold a place in line.
247
+ */
248
+ acquire(signal) {
249
+ if (signal?.aborted === true) return Promise.reject(abortError());
102
250
  if (this.available > 0) {
103
251
  this.available -= 1;
104
- return () => {
252
+ return Promise.resolve(() => {
105
253
  this.release();
106
- };
254
+ });
107
255
  }
108
- await new Promise((resolve) => {
109
- this.queue.push(resolve);
256
+ return new Promise((resolve, reject) => {
257
+ const grant = () => {
258
+ signal?.removeEventListener("abort", onAbort);
259
+ resolve(() => {
260
+ this.release();
261
+ });
262
+ };
263
+ const onAbort = () => {
264
+ const at = this.waiters.indexOf(grant);
265
+ if (at >= 0) this.waiters.splice(at, 1);
266
+ reject(abortError());
267
+ };
268
+ signal?.addEventListener("abort", onAbort, { once: true });
269
+ this.waiters.push(grant);
110
270
  });
111
- this.available -= 1;
112
- return () => {
113
- this.release();
114
- };
115
271
  }
272
+ // A freed slot goes straight to the next waiter, so a new caller cannot
273
+ // overtake it between the release and the waiter's wake-up.
116
274
  release() {
117
- this.available += 1;
118
- const next = this.queue.shift();
275
+ const next = this.waiters.shift();
119
276
  if (next !== void 0) next();
277
+ else this.available += 1;
120
278
  }
121
279
  };
122
280
  function mapFinishReason(stopReason) {
@@ -133,14 +291,19 @@ function mapFinishReason(stopReason) {
133
291
  }
134
292
  }
135
293
  function mapUsage(usage) {
136
- const inputTokens = usage?.input_tokens ?? 0;
294
+ const cacheRead = usage?.cache_read_input_tokens;
295
+ const cacheWrite = usage?.cache_creation_input_tokens;
296
+ const inputTokens = (usage?.input_tokens ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
137
297
  const outputTokens = usage?.output_tokens ?? 0;
138
- const cachedInputTokens = usage?.cache_read_input_tokens;
298
+ const cachedInputTokens = cacheRead;
299
+ const thinkingTokens = usage?.output_tokens_details?.thinking_tokens;
139
300
  const totalTokens = usage !== void 0 ? inputTokens + outputTokens : void 0;
140
301
  const details = {
141
302
  input: inputTokens,
142
303
  output: outputTokens,
143
304
  ...cachedInputTokens !== void 0 ? { cached: cachedInputTokens } : {},
305
+ ...cacheWrite !== void 0 ? { cacheWrite } : {},
306
+ ...thinkingTokens !== void 0 ? { thinking: thinkingTokens } : {},
144
307
  ...totalTokens !== void 0 ? { total: totalTokens } : {}
145
308
  };
146
309
  const raw = usage !== void 0 ? usage : null;
@@ -150,6 +313,7 @@ function mapUsage(usage) {
150
313
  details,
151
314
  raw,
152
315
  ...cachedInputTokens !== void 0 ? { cachedInputTokens } : {},
316
+ ...thinkingTokens !== void 0 ? { thinkingTokens } : {},
153
317
  ...totalTokens !== void 0 ? { totalTokens } : {}
154
318
  };
155
319
  }
@@ -179,11 +343,13 @@ function buildPrompt(messages) {
179
343
  ${messageText(msg)}`;
180
344
  }).join("\n\n");
181
345
  }
346
+ var AUTH_FAILURE = /\b(?:log ?in|auth|oauth|authentication|unauthori[sz]ed|401)\b/i;
347
+ var RATE_LIMITED = /\brate[ -]?limit(?:ed|s|ing)?\b|\btoo many requests\b|(?<![\w.:/-])429(?![\w-]|\.\d)/i;
182
348
  function looksAuthy(text) {
183
- return /login|auth|unauthorized/i.test(text);
349
+ return AUTH_FAILURE.test(text);
184
350
  }
185
351
  function looksRateLimited(text) {
186
- return /rate limit|429/i.test(text);
352
+ return RATE_LIMITED.test(text);
187
353
  }
188
354
  function assertServedModel(envelope, requestedModel) {
189
355
  const modelUsage = envelope.modelUsage;
@@ -218,12 +384,6 @@ function classifyRunFailure(envelope, result) {
218
384
  });
219
385
  }
220
386
  const combinedText = `${envelope?.subtype ?? ""} ${result.stderr}`;
221
- if (looksAuthy(combinedText)) {
222
- return new LlmError(
223
- `claude CLI reported an authentication failure: ${result.stderr.slice(-500)}`,
224
- { kind: "invalid_auth", retryable: false, provider: "claude-cli" }
225
- );
226
- }
227
387
  if (looksRateLimited(combinedText)) {
228
388
  return new LlmError(
229
389
  `claude CLI reported rate limiting: ${result.stderr.slice(-500)}`,
@@ -234,6 +394,12 @@ function classifyRunFailure(envelope, result) {
234
394
  }
235
395
  );
236
396
  }
397
+ if (looksAuthy(combinedText)) {
398
+ return new LlmError(
399
+ `claude CLI reported an authentication failure: ${result.stderr.slice(-500)}`,
400
+ { kind: "invalid_auth", retryable: false, provider: "claude-cli" }
401
+ );
402
+ }
237
403
  if (envelope?.is_error === true) {
238
404
  return new LlmError(
239
405
  `claude CLI reported an error (subtype: ${envelope.subtype ?? "unknown"}): ${result.stderr.slice(-500)}`,
@@ -249,6 +415,7 @@ function classifyRunFailure(envelope, result) {
249
415
  function claudeCliAdapter(opts) {
250
416
  const runner = opts?.runner ?? buildClaudeCliRunner(opts?.claudePath);
251
417
  const semaphore = new Semaphore(opts?.maxConcurrency ?? 2);
418
+ const env = parseExtraEnv(opts?.env);
252
419
  return {
253
420
  id: "claude-cli",
254
421
  async run(req, ctx) {
@@ -320,14 +487,29 @@ function claudeCliAdapter(opts) {
320
487
  args.push("--json-schema", JSON.stringify(req.outputJsonSchema));
321
488
  }
322
489
  const timeoutMs = req.attemptTimeoutMs ?? config.timeoutMs;
323
- const cwd = await mkdtemp(join(tmpdir(), "claude-cli-"));
324
- const release = await semaphore.acquire();
490
+ let release;
491
+ try {
492
+ release = await semaphore.acquire(ctx.signal);
493
+ } catch (waitErr) {
494
+ throw new LlmError(
495
+ waitErr instanceof Error ? waitErr.message : "claude-cli call aborted",
496
+ { kind: "aborted", retryable: false, provider: "claude-cli" }
497
+ );
498
+ }
499
+ let cwd;
500
+ try {
501
+ cwd = await mkdtemp(join(tmpdir(), "claude-cli-"));
502
+ } catch (mkdirErr) {
503
+ release();
504
+ throw mkdirErr;
505
+ }
325
506
  let result;
326
507
  try {
327
508
  result = await runner.run(args, prompt, {
328
509
  cwd,
329
510
  ...timeoutMs !== void 0 ? { timeoutMs } : {},
330
- ...ctx.signal !== void 0 ? { signal: ctx.signal } : {}
511
+ ...ctx.signal !== void 0 ? { signal: ctx.signal } : {},
512
+ ...env !== void 0 ? { env } : {}
331
513
  });
332
514
  } catch (rawErr) {
333
515
  release();
@@ -392,6 +574,10 @@ function claudeCliAdapter(opts) {
392
574
  };
393
575
  const adapterResult = {
394
576
  model,
577
+ message: {
578
+ role: "assistant",
579
+ parts: text.length > 0 ? [{ kind: "text", text }] : []
580
+ },
395
581
  usage,
396
582
  warnings,
397
583
  ...text.length > 0 ? { text } : {},
@@ -451,21 +637,30 @@ var CONFIG_SCHEMAS = {
451
637
  "claude-sonnet-5": ClaudeSonnet5ConfigSchema,
452
638
  "claude-haiku-4-5-20251001": ClaudeHaiku45ConfigSchema
453
639
  };
640
+ var CLAUDE_CLI_LIMITS = {
641
+ "claude-fable-5-1": { contextWindow: 1e6, maxOutputTokens: 64e3 },
642
+ "claude-opus-5-5": { contextWindow: 1e6, maxOutputTokens: 128e3 },
643
+ "claude-sonnet-5": { contextWindow: 1e6, maxOutputTokens: 128e3 },
644
+ "claude-haiku-4-5-20251001": { contextWindow: 2e5, maxOutputTokens: 32e3 }
645
+ };
454
646
  var claudeCliModelDescriptors = CLAUDE_CLI_MODEL_IDS.map(
455
647
  (id) => {
456
648
  const configSchema = CONFIG_SCHEMAS[id];
457
649
  return {
458
650
  model: id,
459
651
  provider: "claude-cli",
652
+ limits: CLAUDE_CLI_LIMITS[id],
460
653
  capabilities: {
654
+ // The CLI runs text-only: the adapter rejects every non-text part.
655
+ inputMimeTypes: [],
461
656
  structuredOutput: true,
462
657
  nativeStructuredOutput: true,
463
658
  ...id !== "claude-haiku-4-5-20251001" ? { reasoningApi: "level" } : {},
464
659
  admittedReasoningEfforts: id === "claude-haiku-4-5-20251001" ? [] : CLAUDE_CLI_EFFORTS,
465
- sampling: "fixed",
466
- vision: false
660
+ sampling: "fixed"
467
661
  },
468
662
  configSchema,
663
+ configKeys: toConfigKeys(configSchema),
469
664
  configJsonSchema: toConfigJsonSchema(configSchema),
470
665
  validateConfig: zodToStandardSchema(configSchema)
471
666
  };