@demicodes/provider-claude-code 0.1.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.mjs ADDED
@@ -0,0 +1,1166 @@
1
+ import { errorMessage, isRecord, nonEmptyString, numberOrNull, stringOrNull } from "@demicodes/utils";
2
+ import { randomUUID } from "node:crypto";
3
+ import { applyModelPolicy, defineProvider } from "@demicodes/provider";
4
+ import { Buffer as Buffer$1 } from "node:buffer";
5
+ import { spawn } from "node:child_process";
6
+ import { appendFileSync, mkdirSync, statSync } from "node:fs";
7
+ import { createInterface } from "node:readline";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import process$1 from "node:process";
11
+ //#region src/models.ts
12
+ const DEFAULT_MODELS_DEV_URL = "https://models.dev/api.json";
13
+ const DEFAULT_MINIMUM_MODEL_VERSION = "4.6";
14
+ const MODELS_DEV_CACHE_TTL_MS = 1440 * 60 * 1e3;
15
+ let memoryCache = null;
16
+ async function listClaudeCodeModels(options = {}) {
17
+ const fetchImpl = options.fetch ?? fetch;
18
+ const nowDate = (options.now ?? (() => /* @__PURE__ */ new Date()))();
19
+ const minimum = parseMinimumModelVersion(options.minimumModelVersion ?? DEFAULT_MINIMUM_MODEL_VERSION);
20
+ const modelsDevUrl = options.modelsDevUrl ?? DEFAULT_MODELS_DEV_URL;
21
+ const cacheKey = claudeCatalogCacheKey(modelsDevUrl, minimum);
22
+ const headers = new Headers({ accept: "application/json" });
23
+ if (memoryCache?.cacheKey === cacheKey && nowDate.getTime() - memoryCache.fetchedAtMs < MODELS_DEV_CACHE_TTL_MS) return cloneModelList(markModelListCache(memoryCache.list, false));
24
+ if (memoryCache?.cacheKey === cacheKey && memoryCache.etag) headers.set("if-none-match", memoryCache.etag);
25
+ if (memoryCache?.cacheKey === cacheKey && memoryCache.lastModified) headers.set("if-modified-since", memoryCache.lastModified);
26
+ try {
27
+ const response = await fetchImpl(modelsDevUrl, { headers });
28
+ if (response.status === 304 && memoryCache?.cacheKey === cacheKey) {
29
+ memoryCache = {
30
+ ...memoryCache,
31
+ fetchedAtMs: nowDate.getTime()
32
+ };
33
+ return cloneModelList(markModelListCache(memoryCache.list, false));
34
+ }
35
+ if (!response.ok) throw new Error(`models.dev catalog request failed with HTTP ${response.status}`);
36
+ const list = modelsDevAnthropicCatalogToModelList(await response.json(), {
37
+ minimumModelVersion: minimum,
38
+ sourceFetchedAt: nowDate.toISOString(),
39
+ stale: false,
40
+ warnings: []
41
+ });
42
+ memoryCache = {
43
+ cacheKey,
44
+ etag: response.headers.get("etag"),
45
+ lastModified: response.headers.get("last-modified"),
46
+ fetchedAtMs: nowDate.getTime(),
47
+ list
48
+ };
49
+ return cloneModelList(list);
50
+ } catch (error) {
51
+ if (!memoryCache || memoryCache.cacheKey !== cacheKey) throw error;
52
+ const list = markModelListCache(memoryCache.list, true);
53
+ return {
54
+ ...cloneModelList(list),
55
+ warnings: [...list.warnings, `Using stale models.dev catalog: ${errorMessage(error)}`]
56
+ };
57
+ }
58
+ }
59
+ function modelsDevAnthropicCatalogToModelList(value, options = {}) {
60
+ const sourceFetchedAt = options.sourceFetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
61
+ const minimum = typeof options.minimumModelVersion === "string" ? parseMinimumModelVersion(options.minimumModelVersion) : options.minimumModelVersion ?? parseMinimumModelVersion(DEFAULT_MINIMUM_MODEL_VERSION);
62
+ const warnings = [...options.warnings ?? []];
63
+ const anthropic = isRecord(value) && isRecord(value.anthropic) ? value.anthropic : null;
64
+ const rawModels = anthropic && isRecord(anthropic.models) ? anthropic.models : null;
65
+ if (!rawModels) throw new Error("models.dev response does not contain anthropic.models");
66
+ const models = [];
67
+ for (const [id, rawModel] of Object.entries(rawModels)) {
68
+ if (!id.startsWith("claude-")) continue;
69
+ const version = parseClaudeModelVersion(id);
70
+ if (!version) {
71
+ warnings.push(`Skipped Claude model with unparseable version: ${id}`);
72
+ continue;
73
+ }
74
+ if (!versionGte(version, minimum)) continue;
75
+ if (!isRecord(rawModel)) {
76
+ warnings.push(`Skipped Claude model with invalid metadata: ${id}`);
77
+ continue;
78
+ }
79
+ models.push(modelFromModelsDevEntry(id, rawModel, sourceFetchedAt, options.stale === true));
80
+ }
81
+ models.sort(compareClaudeModels);
82
+ return {
83
+ providerId: "claude-code",
84
+ models,
85
+ defaultModelId: null,
86
+ warnings,
87
+ sourceFetchedAt,
88
+ stale: options.stale === true
89
+ };
90
+ }
91
+ const CLAUDE_FAMILY_RANK = {
92
+ opus: 0,
93
+ sonnet: 1,
94
+ haiku: 2
95
+ };
96
+ function claudeFamilyRank(id) {
97
+ return CLAUDE_FAMILY_RANK[id.slice(7).split("-")[0] ?? ""] ?? 3;
98
+ }
99
+ /** Canonical catalog order: flagship family first (Opus > Sonnet > Haiku > others), newest version first. */
100
+ function compareClaudeModels(a, b) {
101
+ const familyDelta = claudeFamilyRank(a.id) - claudeFamilyRank(b.id);
102
+ if (familyDelta !== 0) return familyDelta;
103
+ const versionA = parseClaudeModelVersion(a.id);
104
+ const versionB = parseClaudeModelVersion(b.id);
105
+ if (versionA && versionB) {
106
+ if (versionA.major !== versionB.major) return versionB.major - versionA.major;
107
+ if (versionA.minor !== versionB.minor) return versionB.minor - versionA.minor;
108
+ }
109
+ return a.id.localeCompare(b.id);
110
+ }
111
+ function parseClaudeModelVersion(id) {
112
+ if (!id.startsWith("claude-")) return null;
113
+ const parts = id.slice(7).split("-");
114
+ const isInteger = (value) => value !== void 0 && /^\d+$/.test(value);
115
+ const isDate = (value) => value !== void 0 && /^\d{8}$/.test(value);
116
+ if (isInteger(parts[0])) return {
117
+ major: Number(parts[0]),
118
+ minor: isInteger(parts[1]) ? Number(parts[1]) : 0
119
+ };
120
+ if (!isInteger(parts[1])) return null;
121
+ return {
122
+ major: Number(parts[1]),
123
+ minor: isInteger(parts[2]) && !isDate(parts[2]) ? Number(parts[2]) : 0
124
+ };
125
+ }
126
+ function modelFromModelsDevEntry(id, raw, sourceFetchedAt, stale) {
127
+ const limit = isRecord(raw.limit) ? raw.limit : null;
128
+ const cost = isRecord(raw.cost) ? raw.cost : null;
129
+ return {
130
+ providerId: "claude-code",
131
+ id,
132
+ displayName: nonEmptyString(raw.name) ?? id,
133
+ description: nonEmptyString(raw.description),
134
+ contextWindow: numberOrNull(limit?.context),
135
+ outputLimit: numberOrNull(limit?.output),
136
+ supportsTools: booleanOrNull(isRecord(raw.tool) ? raw.tool.call : raw.tool_call),
137
+ supportsAttachments: booleanOrNull(raw.attachment),
138
+ supportsReasoning: booleanOrNull(raw.reasoning),
139
+ supportedThinkingEfforts: reasoningEfforts(raw.reasoning_options),
140
+ defaultThinkingEffort: null,
141
+ canDisableThinking: false,
142
+ ...cost ? { cost: {
143
+ input: numberOrNull(cost.input),
144
+ output: numberOrNull(cost.output),
145
+ cacheRead: numberOrNull(cost.cache_read),
146
+ cacheWrite: numberOrNull(cost.cache_write)
147
+ } } : {},
148
+ sourceFetchedAt,
149
+ stale
150
+ };
151
+ }
152
+ function parseMinimumModelVersion(value) {
153
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(value);
154
+ if (!match) throw new Error(`Invalid minimum Claude model version: ${value}`);
155
+ return {
156
+ major: Number(match[1]),
157
+ minor: match[2] ? Number(match[2]) : 0
158
+ };
159
+ }
160
+ function versionGte(version, minimum) {
161
+ return version.major > minimum.major || version.major === minimum.major && version.minor >= minimum.minor;
162
+ }
163
+ function claudeCatalogCacheKey(modelsDevUrl, minimum) {
164
+ return `${modelsDevUrl}\0${minimum.major}.${minimum.minor}`;
165
+ }
166
+ function markModelListCache(list, stale) {
167
+ return {
168
+ ...list,
169
+ sourceFetchedAt: list.sourceFetchedAt,
170
+ stale,
171
+ models: list.models.map((model) => ({
172
+ ...model,
173
+ stale
174
+ }))
175
+ };
176
+ }
177
+ function cloneModelList(list) {
178
+ return {
179
+ ...list,
180
+ warnings: [...list.warnings],
181
+ models: list.models.map((model) => ({
182
+ ...model,
183
+ ...model.cost ? { cost: { ...model.cost } } : {},
184
+ supportedThinkingEfforts: model.supportedThinkingEfforts ? [...model.supportedThinkingEfforts] : null
185
+ }))
186
+ };
187
+ }
188
+ function booleanOrNull(value) {
189
+ return typeof value === "boolean" ? value : null;
190
+ }
191
+ function reasoningEfforts(value) {
192
+ if (!Array.isArray(value)) return null;
193
+ const effortOption = value.find((option) => isRecord(option) && option.type === "effort");
194
+ if (!isRecord(effortOption) || !Array.isArray(effortOption.values)) return null;
195
+ const efforts = effortOption.values.map((effort) => nonEmptyString(effort)).filter((effort) => effort !== void 0);
196
+ return efforts.length > 0 ? efforts : [];
197
+ }
198
+ //#endregion
199
+ //#region src/jsonl.ts
200
+ /**
201
+ * Builds the input messages used to prime a *fresh* Claude CLI process with prior
202
+ * conversation history — i.e. on a cold start: the first turn, a resume after the process
203
+ * died, or a restart forced by compaction.
204
+ *
205
+ * Two invariants:
206
+ *
207
+ * 1. Prior tool calls/results are rendered as plain TEXT, never structured `tool_use` /
208
+ * `tool_result` blocks. Replaying a structured MCP `tool_use` into a freshly-initialized
209
+ * SDK-MCP session is exactly what makes Claude reject the request with
210
+ * `API Error: 400 ... tool use concurrency`.
211
+ * 2. A `user` message contains ONLY real user input. Both the tool call and its result are
212
+ * folded into the *assistant* narrative ("I ran X, it returned Y"), so we never synthesize
213
+ * a user turn the human did not type, and never merge a tool result into the next real
214
+ * prompt. This keeps the conversation cleanly alternating without fabricated user messages.
215
+ *
216
+ * The live, in-process path never calls this: there the CLI keeps the structured tool history
217
+ * in its own native context, so no replay happens at all.
218
+ */
219
+ function coldStartInputMessages(items) {
220
+ const messages = [];
221
+ let pending = null;
222
+ const toolNames = /* @__PURE__ */ new Map();
223
+ const flush = () => {
224
+ if (pending && pending.content.length > 0) messages.push({
225
+ type: pending.role,
226
+ message: {
227
+ role: pending.role,
228
+ content: pending.content
229
+ }
230
+ });
231
+ pending = null;
232
+ };
233
+ const append = (role, blocks) => {
234
+ if (blocks.length === 0) return;
235
+ if (!pending || pending.role !== role) {
236
+ flush();
237
+ pending = {
238
+ role,
239
+ content: []
240
+ };
241
+ }
242
+ pending.content.push(...blocks);
243
+ };
244
+ for (const item of items) switch (item.type) {
245
+ case "user_message":
246
+ case "user_steer":
247
+ append("user", userContentToClaude(item.content));
248
+ break;
249
+ case "assistant_text":
250
+ append("assistant", item.text ? [{
251
+ type: "text",
252
+ text: item.text
253
+ }] : []);
254
+ break;
255
+ case "tool_use":
256
+ toolNames.set(item.toolUseId, item.toolName);
257
+ append("assistant", [{
258
+ type: "text",
259
+ text: renderToolUseText(item.toolName, item.input)
260
+ }]);
261
+ break;
262
+ case "tool_result":
263
+ append("assistant", [{
264
+ type: "text",
265
+ text: renderToolResultText(toolNames.get(item.toolUseId), item)
266
+ }]);
267
+ break;
268
+ }
269
+ flush();
270
+ return messages;
271
+ }
272
+ function renderToolUseText(toolName, input) {
273
+ return `[Earlier in this conversation I called the tool ${toolName} with input: ${safeJson(input)}.`;
274
+ }
275
+ function renderToolResultText(toolName, item) {
276
+ const body = toolResultToText(item.output);
277
+ const suffix = toolName ? ` from ${toolName}` : "";
278
+ return item.isError ? `It returned an error${suffix}: ${body}]` : `It returned${suffix}: ${body}]`;
279
+ }
280
+ function safeJson(value) {
281
+ try {
282
+ return JSON.stringify(value) ?? String(value);
283
+ } catch {
284
+ return String(value);
285
+ }
286
+ }
287
+ function inferenceItemToClaudeMessage(item) {
288
+ switch (item.type) {
289
+ case "user_message":
290
+ case "user_steer": return {
291
+ type: "user",
292
+ message: {
293
+ role: "user",
294
+ content: userContentToClaude(item.content)
295
+ }
296
+ };
297
+ case "assistant_text":
298
+ case "assistant_thinking":
299
+ case "assistant_redacted_thinking":
300
+ case "tool_use": {
301
+ const content = assistantItemToClaudeContent(item);
302
+ if (content === null) return null;
303
+ return {
304
+ type: "assistant",
305
+ message: {
306
+ role: "assistant",
307
+ content: [content]
308
+ }
309
+ };
310
+ }
311
+ case "tool_result": return toolResultsToClaudeMessage([item]);
312
+ }
313
+ return null;
314
+ }
315
+ function toolResultsToClaudeMessage(results) {
316
+ return {
317
+ type: "user",
318
+ message: {
319
+ role: "user",
320
+ content: results.map(toolResultToClaudeContent)
321
+ }
322
+ };
323
+ }
324
+ function controlResponse(id, response) {
325
+ return {
326
+ type: "control_response",
327
+ id,
328
+ response
329
+ };
330
+ }
331
+ function userContentToClaude(content) {
332
+ return content.map((block) => {
333
+ if (block.type === "text") return {
334
+ type: "text",
335
+ text: block.text
336
+ };
337
+ if (block.type === "image") return {
338
+ type: "image",
339
+ source: imageSourceToClaude(block.source)
340
+ };
341
+ if (block.type === "document") return documentSourceToClaude(block.source);
342
+ return {
343
+ type: "text",
344
+ text: block.reference
345
+ };
346
+ });
347
+ }
348
+ function assistantItemToClaudeContent(item) {
349
+ if (item.type === "assistant_text") return {
350
+ type: "text",
351
+ text: item.text
352
+ };
353
+ if (item.type === "assistant_thinking") {
354
+ if (!item.signature) return null;
355
+ return {
356
+ type: "thinking",
357
+ thinking: item.text,
358
+ signature: item.signature
359
+ };
360
+ }
361
+ if (item.type === "assistant_redacted_thinking") return {
362
+ type: "redacted_thinking",
363
+ data: item.data
364
+ };
365
+ return {
366
+ type: "tool_use",
367
+ id: item.toolUseId,
368
+ name: toolNameToClaude(item.toolName),
369
+ input: item.input
370
+ };
371
+ }
372
+ function toolResultToClaudeContent(item) {
373
+ return {
374
+ type: "tool_result",
375
+ tool_use_id: item.toolUseId,
376
+ is_error: item.isError,
377
+ content: toolResultToText(item.output)
378
+ };
379
+ }
380
+ function imageSourceToClaude(source) {
381
+ if (source.type === "url") return {
382
+ type: "url",
383
+ url: source.url
384
+ };
385
+ return {
386
+ type: "base64",
387
+ media_type: source.mediaType,
388
+ data: bytesToBase64(source.data)
389
+ };
390
+ }
391
+ function documentSourceToClaude(source) {
392
+ return {
393
+ type: "document",
394
+ source: {
395
+ type: "base64",
396
+ media_type: source.mediaType,
397
+ data: bytesToBase64(source.data)
398
+ },
399
+ title: source.fileName
400
+ };
401
+ }
402
+ function toolResultToText(output) {
403
+ return output.map((block) => block.type === "text" ? block.text : `[image:${block.source.mediaType}]`).join("\n");
404
+ }
405
+ function bytesToBase64(data) {
406
+ return Buffer$1.from(data.buffer, data.byteOffset, data.byteLength).toString("base64");
407
+ }
408
+ function toolNameToClaude(name) {
409
+ if (/^mcp__[^_]+__.+$/.test(name)) return name;
410
+ return `mcp__main__${name}`;
411
+ }
412
+ //#endregion
413
+ //#region src/output.ts
414
+ function mapClaudeStdoutMessage(message, options = {}) {
415
+ const events = [];
416
+ if (!isRecord(message)) return {
417
+ events,
418
+ terminal: false
419
+ };
420
+ if (message.type === "assistant" && isRecord(message.message) && !options.ignoreAssistantContent) events.push(...mapContentArray(message.message.content, options));
421
+ if (message.type === "stream_event" && isRecord(message.event)) events.push(...mapStreamEvent(message.event));
422
+ if (message.type === "control_request") {
423
+ const request = parseControlRequest(message);
424
+ if (request) return {
425
+ events,
426
+ controlRequest: request,
427
+ terminal: false
428
+ };
429
+ }
430
+ if (message.type === "result") {
431
+ if (message.is_error === true) {
432
+ const errorMessage = resultErrorMessage(message);
433
+ events.push({
434
+ type: "error",
435
+ message: errorMessage,
436
+ code: classifyProviderError(errorMessage)
437
+ });
438
+ }
439
+ events.push({
440
+ type: "response",
441
+ usage: mapUsage(message.usage)
442
+ });
443
+ return {
444
+ events,
445
+ terminal: true
446
+ };
447
+ }
448
+ if (message.type === "error") {
449
+ const errorMessage = String(message.message ?? "Claude Code error");
450
+ events.push({
451
+ type: "error",
452
+ message: errorMessage,
453
+ code: stringOrNull(message.code) ?? classifyProviderError(errorMessage)
454
+ });
455
+ }
456
+ return {
457
+ events,
458
+ terminal: false
459
+ };
460
+ }
461
+ function controlRequestToToolCall(request) {
462
+ if (request.method !== "tools/call") return null;
463
+ if (!isRecord(request.params)) return null;
464
+ const name = typeof request.params.name === "string" ? request.params.name : null;
465
+ if (!name) return null;
466
+ return {
467
+ type: "tool_call_requested",
468
+ toolUseId: request.toolUseId ?? String(request.id),
469
+ toolName: stripMcpToolPrefix(name),
470
+ input: request.params.arguments ?? request.params.input ?? {}
471
+ };
472
+ }
473
+ function mapContentArray(content, options) {
474
+ if (!Array.isArray(content)) return [];
475
+ const events = [];
476
+ for (const block of content) {
477
+ if (!isRecord(block)) continue;
478
+ if (block.type === "text") events.push({
479
+ type: "text_delta",
480
+ text: String(block.text ?? "")
481
+ });
482
+ else if (block.type === "thinking") {
483
+ events.push({ type: "thinking_start" });
484
+ events.push({
485
+ type: "thinking_delta",
486
+ text: String(block.thinking ?? block.text ?? "")
487
+ });
488
+ if (typeof block.signature === "string") events.push({
489
+ type: "thinking_signature",
490
+ signature: block.signature
491
+ });
492
+ } else if (block.type === "redacted_thinking") events.push({
493
+ type: "redacted_thinking",
494
+ data: String(block.data ?? "")
495
+ });
496
+ else if (block.type === "tool_use" && !options.ignoreAssistantToolUse) events.push(mapToolUseBlock(block));
497
+ }
498
+ return events;
499
+ }
500
+ function mapToolUseBlock(block) {
501
+ const id = typeof block.id === "string" || typeof block.id === "number" ? String(block.id) : null;
502
+ const name = typeof block.name === "string" && block.name.length > 0 ? block.name : null;
503
+ if (!id || !name) return {
504
+ type: "error",
505
+ message: "Invalid tool_use block from Claude Code",
506
+ code: null
507
+ };
508
+ return {
509
+ type: "tool_call_requested",
510
+ toolUseId: id,
511
+ toolName: stripMcpToolPrefix(name),
512
+ input: block.input ?? {}
513
+ };
514
+ }
515
+ function mapStreamEvent(event) {
516
+ if (event.type === "content_block_start" && isRecord(event.content_block)) {
517
+ const block = event.content_block;
518
+ if (block.type === "thinking") return [{ type: "thinking_start" }];
519
+ if (block.type === "text" && typeof block.text === "string") return [{
520
+ type: "text_delta",
521
+ text: block.text
522
+ }];
523
+ }
524
+ if (event.type === "content_block_delta" && isRecord(event.delta)) {
525
+ const delta = event.delta;
526
+ if (delta.type === "text_delta") return [{
527
+ type: "text_delta",
528
+ text: String(delta.text ?? "")
529
+ }];
530
+ if (delta.type === "thinking_delta") return [{
531
+ type: "thinking_delta",
532
+ text: String(delta.thinking ?? "")
533
+ }];
534
+ if (delta.type === "signature_delta") return [{
535
+ type: "thinking_signature",
536
+ signature: String(delta.signature ?? "")
537
+ }];
538
+ }
539
+ return [];
540
+ }
541
+ function parseControlRequest(message) {
542
+ if (typeof message.request_id === "string" && isRecord(message.request)) {
543
+ const request = message.request;
544
+ if (request.subtype !== "mcp_message") return null;
545
+ if (request.server_name !== "main") return null;
546
+ if (!isRecord(request.message)) return null;
547
+ const inner = request.message;
548
+ const id = typeof inner.id === "string" || typeof inner.id === "number" ? inner.id : 0;
549
+ const method = typeof inner.method === "string" ? inner.method : void 0;
550
+ if (!method) return null;
551
+ return {
552
+ protocol: "sdk-mcp",
553
+ outerRequestId: message.request_id,
554
+ serverName: request.server_name,
555
+ id,
556
+ method,
557
+ params: inner.params
558
+ };
559
+ }
560
+ const id = typeof message.id === "string" || typeof message.id === "number" ? message.id : void 0;
561
+ const method = typeof message.method === "string" ? message.method : void 0;
562
+ if (id === void 0 || !method) return null;
563
+ return {
564
+ protocol: "legacy",
565
+ id,
566
+ method,
567
+ params: message.params
568
+ };
569
+ }
570
+ function mapUsage(usage) {
571
+ if (!isRecord(usage)) return {
572
+ inputTokens: 0,
573
+ outputTokens: 0,
574
+ cacheReadTokens: 0,
575
+ cacheWriteTokens: 0
576
+ };
577
+ return {
578
+ inputTokens: numberValue(usage.input_tokens ?? usage.inputTokens),
579
+ outputTokens: numberValue(usage.output_tokens ?? usage.outputTokens),
580
+ cacheReadTokens: numberValue(usage.cache_read_input_tokens ?? usage.cacheReadTokens),
581
+ cacheWriteTokens: numberValue(usage.cache_creation_input_tokens ?? usage.cacheWriteTokens)
582
+ };
583
+ }
584
+ function numberValue(value) {
585
+ return typeof value === "number" ? value : 0;
586
+ }
587
+ function resultErrorMessage(message) {
588
+ const parts = [];
589
+ if (typeof message.result === "string" && message.result.trim()) parts.push(message.result.trim());
590
+ if (Array.isArray(message.errors)) for (const error of message.errors) {
591
+ const text = String(error).trim();
592
+ if (text) parts.push(text);
593
+ }
594
+ return parts.join("\n") || "Claude Code returned an error";
595
+ }
596
+ function classifyProviderError(message) {
597
+ const lower = message.toLowerCase();
598
+ if (lower.includes("context_length_exceeded") || lower.includes("context window") || lower.includes("context length") || lower.includes("maximum context") || lower.includes("input is too long")) return "context_length_exceeded";
599
+ if (lower.includes("rate_limit") || lower.includes("rate limit") || lower.includes("rate-limit") || lower.includes("rate limited") || lower.includes("too many requests") || /\b429\b/.test(lower)) return "rate_limit";
600
+ if (lower.includes("auth_expired") || lower.includes("auth expired") || lower.includes("authentication expired") || lower.includes("auth failed") || lower.includes("authentication failed") || lower.includes("not logged in") || lower.includes("login required") || lower.includes("unauthorized")) return "auth_expired";
601
+ return null;
602
+ }
603
+ function stripMcpToolPrefix(name) {
604
+ return /^mcp__[^_]+__(.+)$/.exec(name)?.[1] ?? name;
605
+ }
606
+ //#endregion
607
+ //#region src/cli.ts
608
+ function buildClaudeArgs(params) {
609
+ const args = [
610
+ "--print",
611
+ "--output-format",
612
+ "stream-json",
613
+ "--verbose",
614
+ "--input-format",
615
+ "stream-json",
616
+ "--include-partial-messages",
617
+ "--no-session-persistence",
618
+ "--safe-mode",
619
+ "--disable-slash-commands",
620
+ "--tools",
621
+ "",
622
+ "--permission-mode",
623
+ "bypassPermissions",
624
+ "--allow-dangerously-skip-permissions",
625
+ "--model",
626
+ params.modelId,
627
+ "--system-prompt",
628
+ params.systemPrompt
629
+ ];
630
+ if (params.thinkingEffort) args.push("--effort", params.thinkingEffort);
631
+ return args;
632
+ }
633
+ function buildClaudeEnv(base = process.env) {
634
+ const env = {
635
+ ...base,
636
+ DISABLE_AUTO_COMPACT: "1",
637
+ MAX_MCP_OUTPUT_TOKENS: "1000000"
638
+ };
639
+ delete env.CLAUDECODE;
640
+ return env;
641
+ }
642
+ //#endregion
643
+ //#region src/wire-log.ts
644
+ const NULL_WIRE_LOG = {
645
+ path: null,
646
+ record() {}
647
+ };
648
+ /**
649
+ * Resolves the directory the claude-code wire log is written to. Default-on so the raw
650
+ * provider request/response stream is always retained for diagnostics; set
651
+ * `DEMI_CLAUDE_WIRE_LOG=0` to disable, or `DEMI_CLAUDE_WIRE_LOG_DIR` to relocate.
652
+ */
653
+ function resolveWireLogDir() {
654
+ if (process$1.env.DEMI_CLAUDE_WIRE_LOG === "0") return null;
655
+ return process$1.env.DEMI_CLAUDE_WIRE_LOG_DIR ?? join(tmpdir(), "demi-claude-wire");
656
+ }
657
+ function createClaudeWireLog(sessionId) {
658
+ const dir = resolveWireLogDir();
659
+ if (!dir) return NULL_WIRE_LOG;
660
+ try {
661
+ mkdirSync(dir, { recursive: true });
662
+ } catch {
663
+ return NULL_WIRE_LOG;
664
+ }
665
+ const path = join(dir, `claude-${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") || "session"}.jsonl`);
666
+ return {
667
+ path,
668
+ record(direction, data) {
669
+ const entry = {
670
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
671
+ dir: direction,
672
+ data
673
+ };
674
+ try {
675
+ appendFileSync(path, `${JSON.stringify(entry)}\n`);
676
+ } catch {}
677
+ }
678
+ };
679
+ }
680
+ //#endregion
681
+ //#region src/transport.ts
682
+ function resolveSpawnCwd(cwd) {
683
+ try {
684
+ if (statSync(cwd).isDirectory()) return cwd;
685
+ } catch {}
686
+ return process.cwd();
687
+ }
688
+ var ClaudeCliTransportFactory = class {
689
+ claudePath;
690
+ constructor(options = {}) {
691
+ if (typeof options === "string") this.claudePath = options;
692
+ else this.claudePath = options.claudePath ?? "claude";
693
+ }
694
+ async start(request) {
695
+ const args = buildClaudeArgsForRequest(request);
696
+ const wireLog = createClaudeWireLog(request.sessionId);
697
+ wireLog.record("spawn", {
698
+ requestId: request.requestId,
699
+ turnId: request.turnId,
700
+ model: request.modelId,
701
+ cwd: request.cwd,
702
+ args
703
+ });
704
+ return new ChildProcessClaudeTransport(spawn(this.claudePath, args, {
705
+ cwd: resolveSpawnCwd(request.cwd),
706
+ env: buildClaudeEnv(),
707
+ stdio: [
708
+ "pipe",
709
+ "pipe",
710
+ "pipe"
711
+ ]
712
+ }), wireLog);
713
+ }
714
+ };
715
+ function buildClaudeArgsForRequest(request) {
716
+ return buildClaudeArgs({
717
+ modelId: request.modelId,
718
+ systemPrompt: request.systemPrompt,
719
+ thinkingEffort: thinkingEffort(request.thinking)
720
+ });
721
+ }
722
+ var ChildProcessClaudeTransport = class {
723
+ child;
724
+ wireLog;
725
+ stderr = "";
726
+ waitPromise;
727
+ constructor(child, wireLog) {
728
+ this.child = child;
729
+ this.wireLog = wireLog;
730
+ this.waitPromise = new Promise((resolve) => {
731
+ child.once("close", (exitCode, signal) => {
732
+ this.wireLog.record("exit", {
733
+ exitCode,
734
+ signal: signal ?? null
735
+ });
736
+ resolve({
737
+ exitCode,
738
+ signal: signal ?? void 0
739
+ });
740
+ });
741
+ child.once("error", (error) => {
742
+ this.wireLog.record("exit", { error: error.message });
743
+ resolve({ exitCode: null });
744
+ });
745
+ });
746
+ this.collectStderr();
747
+ }
748
+ async writeJson(value) {
749
+ this.wireLog.record("in", value);
750
+ const line = `${JSON.stringify(value)}\n`;
751
+ await new Promise((resolve, reject) => {
752
+ this.child.stdin.write(line, (error) => {
753
+ if (error) reject(error);
754
+ else resolve();
755
+ });
756
+ });
757
+ }
758
+ async *messages() {
759
+ const rl = createInterface({ input: this.child.stdout });
760
+ for await (const line of rl) {
761
+ if (String(line).trim() === "") continue;
762
+ const parsed = JSON.parse(String(line));
763
+ this.wireLog.record("out", parsed);
764
+ yield parsed;
765
+ }
766
+ }
767
+ async kill() {
768
+ if (!this.child.killed) this.child.kill("SIGTERM");
769
+ }
770
+ wait() {
771
+ return this.waitPromise;
772
+ }
773
+ stderrText() {
774
+ return this.stderr;
775
+ }
776
+ async collectStderr() {
777
+ for await (const chunk of this.child.stderr) {
778
+ const text = Buffer.from(chunk).toString("utf8");
779
+ this.stderr += text;
780
+ this.wireLog.record("err", text);
781
+ }
782
+ }
783
+ };
784
+ function thinkingEffort(thinking) {
785
+ if (!thinking) return null;
786
+ if (thinking.type === "adaptive") return thinking.effort;
787
+ if (thinking.type === "effort") return thinking.effort;
788
+ return null;
789
+ }
790
+ //#endregion
791
+ //#region src/provider.ts
792
+ var ClaudeCodeProvider = class {
793
+ transportFactory;
794
+ active = null;
795
+ constructor(options = {}) {
796
+ this.transportFactory = options.transportFactory ?? new ClaudeCliTransportFactory({ claudePath: options.claudePath });
797
+ }
798
+ async *run(request) {
799
+ let active = null;
800
+ let keepActiveForContinuation = false;
801
+ const signal = request.cancel;
802
+ let abortListener = null;
803
+ try {
804
+ active = await this.ensureActiveForRequest(request);
805
+ const run = active;
806
+ const onAbort = async () => {
807
+ await run.transport.kill();
808
+ if (this.active === run) this.active = null;
809
+ };
810
+ const abort = new Promise((resolve) => {
811
+ if (signal.aborted) {
812
+ onAbort().then(() => resolve({
813
+ done: true,
814
+ value: void 0
815
+ }));
816
+ return;
817
+ }
818
+ abortListener = () => {
819
+ onAbort().finally(() => resolve({
820
+ done: true,
821
+ value: void 0
822
+ }));
823
+ };
824
+ signal.addEventListener("abort", abortListener, { once: true });
825
+ });
826
+ while (true) {
827
+ const next = await Promise.race([this.nextMessage(run), abort]);
828
+ if (next.done) {
829
+ const wasAborted = signal.aborted;
830
+ const exit = await active.transport.wait();
831
+ if (this.active === active) this.active = null;
832
+ if (!wasAborted && exit.exitCode !== 0) yield {
833
+ type: "error",
834
+ message: active.transport.stderrText() || `Claude Code exited with code ${exit.exitCode}`,
835
+ code: exit.signal ?? null
836
+ };
837
+ return;
838
+ }
839
+ const raw = next.value;
840
+ const mapped = mapClaudeStdoutMessage(raw, {
841
+ ignoreAssistantContent: active.hasStreamed && isMessageType(raw, "assistant"),
842
+ ignoreAssistantToolUse: active.sdkMcpEnabled
843
+ });
844
+ if (isMessageType(raw, "stream_event")) active.hasStreamed = true;
845
+ if (mapped.controlRequest) {
846
+ if (await this.handleControlRequest(active, mapped.controlRequest, request) === "tool-call") {
847
+ const event = active.pendingControlRequest ? controlRequestToToolCall(active.pendingControlRequest) : null;
848
+ keepActiveForContinuation = true;
849
+ if (event) yield event;
850
+ return;
851
+ }
852
+ continue;
853
+ }
854
+ const toolUseIds = mapped.events.filter(isToolCallRequested).map((event) => event.toolUseId);
855
+ if (toolUseIds.length > 0) active.pendingToolUseIds = toolUseIds;
856
+ for (const event of mapped.events) {
857
+ if (event.type === "tool_call_requested") keepActiveForContinuation = true;
858
+ yield event;
859
+ }
860
+ if (toolUseIds.length > 0) return;
861
+ if (mapped.terminal) {
862
+ keepActiveForContinuation = true;
863
+ return;
864
+ }
865
+ }
866
+ } catch (error) {
867
+ const cleanup = active ?? this.active;
868
+ if (cleanup) {
869
+ if (this.active === cleanup) this.active = null;
870
+ await cleanup.transport.kill();
871
+ await cleanup.transport.wait();
872
+ }
873
+ throw error;
874
+ } finally {
875
+ if (abortListener) signal.removeEventListener("abort", abortListener);
876
+ if (!keepActiveForContinuation && active && this.active === active) {
877
+ this.active = null;
878
+ await active.transport.kill();
879
+ await active.transport.wait();
880
+ }
881
+ }
882
+ }
883
+ async dispose() {
884
+ const active = this.active;
885
+ if (!active) return;
886
+ this.active = null;
887
+ await active.transport.kill();
888
+ await active.transport.wait();
889
+ }
890
+ /**
891
+ * Returns the live process for this request, reusing the one kept alive from the previous
892
+ * turn whenever possible. Reuse delivers only the *new* input (a pending tool result and/or
893
+ * a freshly appended user message); a cold start is taken only when there is no live process,
894
+ * the session changed, or the transcript was rewritten underneath us (compaction).
895
+ */
896
+ async ensureActiveForRequest(request) {
897
+ const existing = this.active;
898
+ if (existing && existing.sessionId === request.sessionId && existing.modelId === request.modelId && existing.thinkingSig === thinkingSignature(request)) {
899
+ if (existing.pendingControlRequest !== null || existing.pendingToolUseIds.length > 0 || !itemsDiverged(existing, request.items)) {
900
+ await this.sendContinuation(existing, request);
901
+ return existing;
902
+ }
903
+ }
904
+ if (existing) await this.disposeActive(existing);
905
+ return this.coldStart(request);
906
+ }
907
+ async coldStart(request) {
908
+ const transport = await this.transportFactory.start(request);
909
+ const active = {
910
+ transport,
911
+ iterator: transport.messages()[Symbol.asyncIterator](),
912
+ pendingControlRequest: null,
913
+ pendingToolUseIds: [],
914
+ bufferedMessages: [],
915
+ sdkMcpEnabled: request.tools.length > 0,
916
+ hasStreamed: false,
917
+ sessionId: request.sessionId,
918
+ modelId: request.modelId,
919
+ thinkingSig: thinkingSignature(request),
920
+ sentUserMessageCount: 0,
921
+ firstUserSig: null
922
+ };
923
+ this.active = active;
924
+ if (request.tools.length > 0) await this.initializeSdkMcp(active, request.systemPrompt);
925
+ for (const message of coldStartInputMessages(request.items)) await transport.writeJson(message);
926
+ active.sentUserMessageCount = countUserMessages(request.items);
927
+ active.firstUserSig = firstUserSignature(request.items);
928
+ return active;
929
+ }
930
+ /** Feeds a reused process only the input it has not seen: pending tool results, then any new user turns. */
931
+ async sendContinuation(active, request) {
932
+ if (active.pendingControlRequest) {
933
+ await this.writeToolResults(request, active.pendingControlRequest);
934
+ active.pendingControlRequest = null;
935
+ }
936
+ if (active.pendingToolUseIds.length > 0) {
937
+ await this.writeToolResultMessages(request, active.pendingToolUseIds);
938
+ active.pendingToolUseIds = [];
939
+ }
940
+ const userCount = countUserMessages(request.items);
941
+ if (userCount > active.sentUserMessageCount) {
942
+ const userItems = request.items.filter((item) => item.type === "user_message" || item.type === "user_steer");
943
+ for (const item of userItems.slice(active.sentUserMessageCount)) {
944
+ const message = inferenceItemToClaudeMessage(item);
945
+ if (message) await active.transport.writeJson(message);
946
+ }
947
+ active.sentUserMessageCount = userCount;
948
+ if (active.firstUserSig === null) active.firstUserSig = firstUserSignature(request.items);
949
+ }
950
+ }
951
+ async disposeActive(active) {
952
+ if (this.active === active) this.active = null;
953
+ await active.transport.kill();
954
+ await active.transport.wait();
955
+ }
956
+ async handleControlRequest(active, request, inference) {
957
+ if (request.method === "initialize") {
958
+ await this.writeControlResponse(active, request, {
959
+ protocolVersion: "2024-11-05",
960
+ capabilities: { tools: {} },
961
+ serverInfo: {
962
+ name: "demi",
963
+ version: "0.0.0"
964
+ }
965
+ });
966
+ return "handled";
967
+ }
968
+ if (request.method === "ping") {
969
+ await this.writeControlResponse(active, request, {});
970
+ return "handled";
971
+ }
972
+ if (request.method === "notifications/initialized") {
973
+ await this.writeControlResponse(active, request, {});
974
+ return "handled";
975
+ }
976
+ if (request.method === "tools/list") {
977
+ await this.writeControlResponse(active, request, { tools: inference.tools.map((tool) => ({
978
+ name: tool.name,
979
+ description: tool.description,
980
+ inputSchema: tool.inputSchema
981
+ })) });
982
+ return "handled";
983
+ }
984
+ if (request.method === "tools/call") {
985
+ if (!controlRequestToToolCall(request)) {
986
+ await this.writeControlError(active, request, "Invalid tools/call request");
987
+ return "handled";
988
+ }
989
+ active.pendingControlRequest = {
990
+ ...request,
991
+ toolUseId: `mcp-control-${randomUUID()}`
992
+ };
993
+ return "tool-call";
994
+ }
995
+ await this.writeControlError(active, request, `Unsupported method: ${request.method}`);
996
+ return "handled";
997
+ }
998
+ async writeToolResults(request, controlRequest) {
999
+ if (!this.active) throw new Error("No active Claude transport");
1000
+ const expectedToolUseId = controlRequest.toolUseId ?? String(controlRequest.id);
1001
+ const results = request.items.filter((item) => {
1002
+ return item.type === "tool_result" && item.toolUseId === expectedToolUseId;
1003
+ });
1004
+ if (results.length === 0) throw new Error(`Claude Code provider missing tool_result for control_request ${String(controlRequest.id)}`);
1005
+ const latest = results[results.length - 1];
1006
+ if (controlRequest.protocol === "sdk-mcp") {
1007
+ await this.writeControlResponse(this.active, controlRequest, {
1008
+ content: toolResultContentToMcp(latest.output),
1009
+ isError: latest.isError
1010
+ });
1011
+ return;
1012
+ }
1013
+ await this.writeControlResponse(this.active, controlRequest, {
1014
+ content: toolResultContentToText(latest.output),
1015
+ isError: latest.isError
1016
+ });
1017
+ }
1018
+ async writeToolResultMessages(request, toolUseIds) {
1019
+ if (!this.active) throw new Error("No active Claude transport");
1020
+ const pending = new Set(toolUseIds);
1021
+ const results = request.items.filter((item) => {
1022
+ return item.type === "tool_result" && pending.has(item.toolUseId);
1023
+ });
1024
+ const found = new Set(results.map((item) => item.toolUseId));
1025
+ const missing = toolUseIds.filter((id) => !found.has(id));
1026
+ if (missing.length > 0) throw new Error(`Claude Code provider missing tool_result for tool_use ${missing.join(", ")}`);
1027
+ await this.active.transport.writeJson(toolResultsToClaudeMessage(results));
1028
+ }
1029
+ async initializeSdkMcp(active, systemPrompt) {
1030
+ const requestId = randomUUID();
1031
+ await active.transport.writeJson({
1032
+ type: "control_request",
1033
+ request_id: requestId,
1034
+ request: {
1035
+ subtype: "initialize",
1036
+ sdkMcpServers: ["main"],
1037
+ systemPrompt
1038
+ }
1039
+ });
1040
+ while (true) {
1041
+ const next = await active.iterator.next();
1042
+ if (next.done) throw new Error("Claude Code exited before SDK MCP initialization completed");
1043
+ if (isControlResponseFor(next.value, requestId)) return;
1044
+ active.bufferedMessages.push(next.value);
1045
+ }
1046
+ }
1047
+ nextMessage(active) {
1048
+ const value = active.bufferedMessages.shift();
1049
+ if (value !== void 0) return Promise.resolve({
1050
+ done: false,
1051
+ value
1052
+ });
1053
+ return active.iterator.next();
1054
+ }
1055
+ async writeControlResponse(active, request, result) {
1056
+ if (request.protocol === "sdk-mcp") {
1057
+ await active.transport.writeJson({
1058
+ type: "control_response",
1059
+ response: {
1060
+ subtype: "success",
1061
+ request_id: request.outerRequestId,
1062
+ response: { mcp_response: {
1063
+ jsonrpc: "2.0",
1064
+ id: request.id,
1065
+ result
1066
+ } }
1067
+ }
1068
+ });
1069
+ return;
1070
+ }
1071
+ await active.transport.writeJson(controlResponse(request.id, result));
1072
+ }
1073
+ async writeControlError(active, request, message) {
1074
+ if (request.protocol === "sdk-mcp") {
1075
+ await active.transport.writeJson({
1076
+ type: "control_response",
1077
+ response: {
1078
+ subtype: "success",
1079
+ request_id: request.outerRequestId,
1080
+ response: { mcp_response: {
1081
+ jsonrpc: "2.0",
1082
+ id: request.id,
1083
+ error: {
1084
+ code: -32601,
1085
+ message
1086
+ }
1087
+ } }
1088
+ }
1089
+ });
1090
+ return;
1091
+ }
1092
+ await active.transport.writeJson(controlResponse(request.id, { error: { message } }));
1093
+ }
1094
+ };
1095
+ function createClaudeCodeProvider(options = {}) {
1096
+ const id = options.id ?? "claude-code";
1097
+ const displayName = options.displayName ?? "Claude Code";
1098
+ const runtimeOptions = { claudePath: options.claudePath };
1099
+ return defineProvider({
1100
+ id,
1101
+ displayName,
1102
+ auth: { status: () => ({
1103
+ status: "unknown",
1104
+ message: "Auth is checked when a Claude Code request runs"
1105
+ }) },
1106
+ state: () => ({
1107
+ status: "unknown",
1108
+ message: "Runtime is checked when a Claude Code request runs"
1109
+ }),
1110
+ listModels: async () => {
1111
+ return applyModelPolicy(await listClaudeCodeModels(), id, options.models);
1112
+ },
1113
+ createRuntime: () => new ClaudeCodeProvider(runtimeOptions)
1114
+ });
1115
+ }
1116
+ function isToolCallRequested(event) {
1117
+ return event.type === "tool_call_requested";
1118
+ }
1119
+ function isControlResponseFor(value, requestId) {
1120
+ if (!isRecord(value) || value.type !== "control_response" || !isRecord(value.response)) return false;
1121
+ return value.response.request_id === requestId && value.response.subtype === "success";
1122
+ }
1123
+ function toolResultContentToText(output) {
1124
+ return output.map((block) => block.type === "text" ? block.text : `[image:${block.source.mediaType}]`).join("\n");
1125
+ }
1126
+ function toolResultContentToMcp(output) {
1127
+ return output.map((block) => {
1128
+ if (block.type === "text") return {
1129
+ type: "text",
1130
+ text: block.text
1131
+ };
1132
+ return {
1133
+ type: "image",
1134
+ data: Buffer.from(block.source.data).toString("base64"),
1135
+ mimeType: block.source.mediaType
1136
+ };
1137
+ });
1138
+ }
1139
+ function isMessageType(value, type) {
1140
+ return isRecord(value) && value.type === type;
1141
+ }
1142
+ function thinkingSignature(request) {
1143
+ return JSON.stringify(request.thinking ?? null);
1144
+ }
1145
+ function countUserMessages(items) {
1146
+ let count = 0;
1147
+ for (const item of items) if (item.type === "user_message" || item.type === "user_steer") count += 1;
1148
+ return count;
1149
+ }
1150
+ function firstUserSignature(items) {
1151
+ const first = items.find((item) => item.type === "user_message");
1152
+ if (!first || first.type !== "user_message") return null;
1153
+ return first.content.map((block) => block.type === "text" ? `t:${block.text}` : block.type).join("|");
1154
+ }
1155
+ /**
1156
+ * True when the transcript no longer extends what the live process has already consumed —
1157
+ * i.e. user turns were removed or the leading user message changed (compaction rewrote the
1158
+ * history). In that case the process must be cold-restarted from the rewritten transcript.
1159
+ */
1160
+ function itemsDiverged(active, items) {
1161
+ if (countUserMessages(items) < active.sentUserMessageCount) return true;
1162
+ if (active.firstUserSig !== null && firstUserSignature(items) !== active.firstUserSig) return true;
1163
+ return false;
1164
+ }
1165
+ //#endregion
1166
+ export { createClaudeCodeProvider, listClaudeCodeModels, resolveWireLogDir };