@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/NOTICE +6 -0
- package/README.md +73 -7
- package/dist/index.cjs +227 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -14
- package/dist/index.d.ts +34 -14
- package/dist/index.js +228 -33
- package/dist/index.js.map +1 -1
- package/package.json +18 -8
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
|
-
|
|
153
|
+
signalTree("SIGTERM");
|
|
41
154
|
killTimeoutHandle = setTimeout(() => {
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
239
|
+
waiters = [];
|
|
98
240
|
constructor(max) {
|
|
99
241
|
this.available = max;
|
|
100
242
|
}
|
|
101
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
|
349
|
+
return AUTH_FAILURE.test(text);
|
|
184
350
|
}
|
|
185
351
|
function looksRateLimited(text) {
|
|
186
|
-
return
|
|
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
|
-
|
|
324
|
-
|
|
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
|
};
|