@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/LICENSE +201 -0
- package/README.md +15 -0
- package/dist/index.d.mts +30 -0
- package/dist/index.mjs +1166 -0
- package/package.json +43 -0
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 };
|