@demicodes/provider-claude-code 0.10.3 → 0.11.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/README.md CHANGED
@@ -5,8 +5,38 @@ and `listClaudeCodeModels()`.
5
5
 
6
6
  ```ts
7
7
  import { createClaudeCodeProvider, listClaudeCodeModels } from '@demicodes/provider-claude-code'
8
+
9
+ const provider = createClaudeCodeProvider()
10
+ // provider.auth, provider.quota, provider.credentials (multi-account pool by default)
8
11
  ```
9
12
 
13
+ ## Auth and credentials
14
+
15
+ - Default material: `CLAUDE_CODE_OAUTH_TOKEN` or macOS Keychain (`Claude Code-credentials`).
16
+ - Multi-credential pool under `$DEMI_HOME/credentials/claude-code/`; active token is
17
+ injected into the CLI child as `CLAUDE_CODE_OAUTH_TOKEN`.
18
+ - Lifecycle: `beginLogin` → `claude auth login` → `importDefault` / `add` → `setActive`.
19
+ - Changing active credential forces a cold restart of a long-lived CLI process.
20
+
21
+ See [docs/provider-global-credentials.md](../../docs/provider-global-credentials.md).
22
+
23
+ ## Quota
24
+
25
+ - **probe** (cost: `free`): `GET /api/oauth/usage` with the active OAuth token.
26
+ - **observe**: stream-json `rate_limits` bodies (and unified rate-limit headers when present).
27
+
28
+ See [docs/provider-quota.md](../../docs/provider-quota.md).
29
+
30
+ ## Tool-call batches
31
+
32
+ Claude Code's SDK-MCP control channel can hold later `tools/call` callbacks until
33
+ the preceding callback receives a result, even when the model emitted several
34
+ `tool_use` blocks in one response. The provider preserves the model's original
35
+ batch and tool-use IDs for the host scheduler, then matches the completed
36
+ results to SDK-MCP callbacks as the CLI releases them.
37
+
38
+ See [docs/tool-call-concurrency.md](../../docs/tool-call-concurrency.md).
39
+
10
40
  > Diagnostics: the transport writes a raw request/response wire log (including
11
41
  > prompts) to `$TMPDIR/demi-claude-wire` by default. Disable with
12
42
  > `DEMI_CLAUDE_WIRE_LOG=0`. See [SECURITY](../../SECURITY.md).
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { ModelPolicy, Provider, ProviderModelList, ProviderQuota, ProviderQuotaProbeResult } from "@demicodes/provider";
2
-
1
+ import { ModelPolicy, Provider, ProviderAuthState, ProviderCredentials, ProviderModelList, ProviderQuota, ProviderQuotaProbeResult } from "@demicodes/provider";
2
+ import { FileCredentialPool } from "@demicodes/provider/credentials-pool";
3
3
  //#region src/models.d.ts
4
4
  interface ClaudeCodeModelCatalogOptions {
5
5
  fetch?: ModelCatalogFetch;
@@ -10,12 +10,74 @@ interface ClaudeCodeModelCatalogOptions {
10
10
  type ModelCatalogFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
11
11
  declare function listClaudeCodeModels(options?: ClaudeCodeModelCatalogOptions): Promise<ProviderModelList>;
12
12
  //#endregion
13
+ //#region src/oauth.d.ts
14
+ /**
15
+ * Where a resolved token came from. Callers that inject the token into a
16
+ * spawned CLI must skip `keychain`: that token is the CLI's own short-lived
17
+ * credential, and injecting it via CLAUDE_CODE_OAUTH_TOKEN disables the CLI's
18
+ * refresh flow — the run starts 401ing as soon as the token expires. Owned
19
+ * sources (static/file/env) are the caller's responsibility and inject as-is.
20
+ */
21
+ type ClaudeCodeOAuthSource = 'static' | 'file' | 'env' | 'keychain';
22
+ interface ClaudeCodeOAuthAccess {
23
+ accessToken: string;
24
+ source: ClaudeCodeOAuthSource;
25
+ subscriptionType?: string | null;
26
+ rateLimitTier?: string | null;
27
+ }
28
+ /**
29
+ * Resolve Claude Code consumer OAuth access for quota APIs.
30
+ * Order: options env CLAUDE_CODE_OAUTH_TOKEN, then macOS Keychain "Claude Code-credentials".
31
+ * Prefer injecting {@link ClaudeCodeAuthStore} when multi-credential is enabled.
32
+ */
33
+ declare function resolveClaudeCodeOAuthAccess(): Promise<ClaudeCodeOAuthAccess | null>;
34
+ //#endregion
35
+ //#region src/auth.d.ts
36
+ interface ClaudeCodeAuthStore {
37
+ status(): Promise<ProviderAuthState>;
38
+ resolveAccess(options?: {
39
+ forceRefresh?: boolean;
40
+ }): Promise<ClaudeCodeOAuthAccess>;
41
+ }
42
+ /** Renews a pool oauth secret (wired to `refreshClaudeCodeSecret`; injectable in tests). */
43
+ type ClaudeCodeSecretRefresh = (secret: Record<string, unknown>) => Promise<Record<string, unknown>>;
44
+ interface FileClaudeCodeAuthStoreOptions {
45
+ /** Optional path to oauth.json (pool entry). */
46
+ oauthFile?: string;
47
+ /** Prefer this token over env/keychain when set (tests / static). */
48
+ accessToken?: string | null;
49
+ /** Renews the oauth file when its access token nears expiry. */
50
+ refresh?: ClaudeCodeSecretRefresh;
51
+ }
52
+ /**
53
+ * Resolves Claude OAuth: explicit token → oauth file → CLAUDE_CODE_OAUTH_TOKEN → keychain.
54
+ */
55
+ declare class FileClaudeCodeAuthStore implements ClaudeCodeAuthStore {
56
+ private readonly oauthFile;
57
+ private readonly accessToken;
58
+ private readonly refresh;
59
+ constructor(options?: FileClaudeCodeAuthStoreOptions);
60
+ status(): Promise<ProviderAuthState>;
61
+ resolveAccess(): Promise<ClaudeCodeOAuthAccess>;
62
+ }
63
+ declare class StaticClaudeCodeAuthStore implements ClaudeCodeAuthStore {
64
+ private readonly access;
65
+ constructor(access: ClaudeCodeOAuthAccess);
66
+ status(): Promise<ProviderAuthState>;
67
+ resolveAccess(): Promise<ClaudeCodeOAuthAccess>;
68
+ }
69
+ //#endregion
13
70
  //#region src/provider.d.ts
14
71
  interface ClaudeCodeProviderOptions {
15
72
  id?: string;
16
73
  displayName?: string;
17
74
  claudePath?: string;
18
75
  models?: ModelPolicy;
76
+ /** Demi state root for credential pool (`$DEMI_HOME` / `~/.demi`). */
77
+ stateDir?: string;
78
+ /** When true (default), attach multi-credential pool + global switch. */
79
+ credentials?: boolean;
80
+ authStore?: ClaudeCodeAuthStore;
19
81
  }
20
82
  declare function createClaudeCodeProvider(options?: ClaudeCodeProviderOptions): Provider;
21
83
  //#endregion
@@ -27,18 +89,6 @@ declare function createClaudeCodeProvider(options?: ClaudeCodeProviderOptions):
27
89
  */
28
90
  declare function resolveWireLogDir(): string | null;
29
91
  //#endregion
30
- //#region src/oauth.d.ts
31
- interface ClaudeCodeOAuthAccess {
32
- accessToken: string;
33
- subscriptionType?: string | null;
34
- rateLimitTier?: string | null;
35
- }
36
- /**
37
- * Resolve Claude Code consumer OAuth access for quota APIs.
38
- * Order: options env CLAUDE_CODE_OAUTH_TOKEN, then macOS Keychain "Claude Code-credentials".
39
- */
40
- declare function resolveClaudeCodeOAuthAccess(): Promise<ClaudeCodeOAuthAccess | null>;
41
- //#endregion
42
92
  //#region src/quota.d.ts
43
93
  interface ClaudeCodeQuotaOptions {
44
94
  providerId?: string;
@@ -58,4 +108,56 @@ declare function mapClaudeUsagePayload(payload: unknown, access?: ClaudeCodeOAut
58
108
  /** Map anthropic-ratelimit-unified-* headers into a coarse snapshot. */
59
109
  declare function observeClaudeRateLimitHeaders(headers: Headers | undefined): ProviderQuotaProbeResult | null;
60
110
  //#endregion
61
- export { type ClaudeCodeModelCatalogOptions, type ClaudeCodeOAuthAccess, type ClaudeCodeProviderOptions, type ClaudeCodeQuotaOptions, createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, resolveClaudeCodeOAuthAccess, resolveWireLogDir };
111
+ //#region src/credentials.d.ts
112
+ declare class PoolAwareClaudeCodeAuthStore implements ClaudeCodeAuthStore {
113
+ private readonly pool;
114
+ constructor(pool: FileCredentialPool);
115
+ status(): Promise<import("@demicodes/provider").ProviderAuthState>;
116
+ resolveAccess(): Promise<ClaudeCodeOAuthAccess>;
117
+ private currentStore;
118
+ }
119
+ declare function openClaudeCodeCredentialPool(options?: {
120
+ stateDir?: string;
121
+ }): FileCredentialPool;
122
+ declare function createClaudeCodeCredentials(pool: FileCredentialPool, authStore: ClaudeCodeAuthStore, options?: {
123
+ quota?: ProviderQuota | null;
124
+ onActiveChange?: () => void;
125
+ /** Injectable fetch for the OAuth login flow (tests). */
126
+ loginFetch?: typeof fetch;
127
+ }): ProviderCredentials;
128
+ //#endregion
129
+ //#region src/login.d.ts
130
+ /** Pool-entry oauth.json shape (demi-owned; refreshable when refreshToken present). */
131
+ interface ClaudeCodeOAuthSecret {
132
+ accessToken: string;
133
+ refreshToken?: string | null;
134
+ /** ISO-8601 access token expiry. */
135
+ expiresAt?: string | null;
136
+ scopes?: string[] | null;
137
+ subscriptionType?: string | null;
138
+ rateLimitTier?: string | null;
139
+ emailAddress?: string | null;
140
+ [key: string]: unknown;
141
+ }
142
+ interface ClaudeCodeLoginOptions {
143
+ signal?: AbortSignal;
144
+ /** Fires once with the authorize URL; the flow then waits on promptForCode. */
145
+ onPending?: (pending: {
146
+ verificationUrl: string;
147
+ requiresCodeInput: true;
148
+ }) => void;
149
+ /** Collects the "code#state" string the vendor page shows after approval. */
150
+ promptForCode: () => Promise<string>;
151
+ fetch?: typeof fetch;
152
+ consoleBase?: string;
153
+ }
154
+ /** Runs the copy-back OAuth flow and returns a refreshable pool secret. */
155
+ declare function runClaudeCodeLogin(options: ClaudeCodeLoginOptions): Promise<ClaudeCodeOAuthSecret>;
156
+ /** Refreshes a pool secret in place; returns the renewed secret. */
157
+ declare function refreshClaudeCodeSecret(secret: ClaudeCodeOAuthSecret, options?: {
158
+ fetch?: typeof fetch;
159
+ consoleBase?: string;
160
+ signal?: AbortSignal;
161
+ }): Promise<ClaudeCodeOAuthSecret>;
162
+ //#endregion
163
+ export { type ClaudeCodeAuthStore, type ClaudeCodeLoginOptions, type ClaudeCodeModelCatalogOptions, type ClaudeCodeOAuthAccess, type ClaudeCodeOAuthSecret, type ClaudeCodeProviderOptions, type ClaudeCodeQuotaOptions, FileClaudeCodeAuthStore, PoolAwareClaudeCodeAuthStore, StaticClaudeCodeAuthStore, createClaudeCodeCredentials, createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, openClaudeCodeCredentialPool, refreshClaudeCodeSecret, resolveClaudeCodeOAuthAccess, resolveWireLogDir, runClaudeCodeLogin };
package/dist/index.mjs CHANGED
@@ -1,10 +1,12 @@
1
- import { errorMessage, isRecord, nonEmptyString, numberOrNull, stringOrNull } from "@demicodes/utils";
2
- import { randomUUID } from "node:crypto";
3
- import { applyModelPolicy, clampUsedPercent, createProviderQuota, defineProvider, severityFromUsedPercent, unixSecondsToIso } from "@demicodes/provider";
4
- import { Buffer as Buffer$1 } from "node:buffer";
1
+ import { abortable, errorMessage, isRecord, nonEmptyString, numberOrNull, stringOrNull } from "@demicodes/utils";
2
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
3
+ import { applyModelPolicy, clampUsedPercent, createProviderQuota, defineProvider, numberHeader, severityFromUsedPercent, toolResultContentToText, unixSecondsToIso } from "@demicodes/provider";
5
4
  import { execFile, spawn } from "node:child_process";
5
+ import { readFile, writeFile } from "node:fs/promises";
6
6
  import { promisify } from "node:util";
7
7
  import process$1 from "node:process";
8
+ import { FileCredentialPool, credentialIdFromIdentity } from "@demicodes/provider/credentials-pool";
9
+ import { Buffer as Buffer$1 } from "node:buffer";
8
10
  import { appendFileSync, mkdirSync, statSync } from "node:fs";
9
11
  import { createInterface } from "node:readline";
10
12
  import { tmpdir } from "node:os";
@@ -12,7 +14,7 @@ import { join } from "node:path";
12
14
  //#region src/models.ts
13
15
  const DEFAULT_MODELS_DEV_URL = "https://models.dev/api.json";
14
16
  const DEFAULT_MINIMUM_MODEL_VERSION = "4.6";
15
- const MODELS_DEV_CACHE_TTL_MS = 1440 * 60 * 1e3;
17
+ const MODELS_DEV_CACHE_TTL_MS = 864e5;
16
18
  let memoryCache = null;
17
19
  async function listClaudeCodeModels(options = {}) {
18
20
  const fetchImpl = options.fetch ?? fetch;
@@ -95,7 +97,8 @@ const CLAUDE_FAMILY_RANK = {
95
97
  haiku: 2
96
98
  };
97
99
  function claudeFamilyRank(id) {
98
- return CLAUDE_FAMILY_RANK[id.slice(7).split("-")[0] ?? ""] ?? 3;
100
+ const family = id.slice(7).split("-")[0] ?? "";
101
+ return CLAUDE_FAMILY_RANK[family] ?? 3;
99
102
  }
100
103
  /** Canonical catalog order: flagship family first (Opus > Sonnet > Haiku > others), newest version first. */
101
104
  function compareClaudeModels(a, b) {
@@ -197,6 +200,378 @@ function reasoningEfforts(value) {
197
200
  return efforts.length > 0 ? efforts : [];
198
201
  }
199
202
  //#endregion
203
+ //#region src/auth.ts
204
+ const execFileAsync = promisify(execFile);
205
+ const OAUTH_EXPIRY_SKEW_MS = 3e5;
206
+ /**
207
+ * Resolves Claude OAuth: explicit token → oauth file → CLAUDE_CODE_OAUTH_TOKEN → keychain.
208
+ */
209
+ var FileClaudeCodeAuthStore = class {
210
+ oauthFile;
211
+ accessToken;
212
+ refresh;
213
+ constructor(options = {}) {
214
+ this.oauthFile = options.oauthFile ?? null;
215
+ this.accessToken = nonEmptyString(options.accessToken) ?? null;
216
+ this.refresh = options.refresh ?? null;
217
+ }
218
+ async status() {
219
+ try {
220
+ const access = await this.resolveAccess();
221
+ return {
222
+ status: "authenticated",
223
+ accountLabel: nonEmptyString(access.subscriptionType) ?? "Claude Code"
224
+ };
225
+ } catch (error) {
226
+ if (error instanceof ClaudeCodeAuthError && error.code === "auth_missing") return {
227
+ status: "unauthenticated",
228
+ message: error.message
229
+ };
230
+ return {
231
+ status: "error",
232
+ message: error instanceof Error ? error.message : String(error)
233
+ };
234
+ }
235
+ }
236
+ async resolveAccess() {
237
+ if (this.accessToken) return {
238
+ accessToken: this.accessToken,
239
+ source: "static"
240
+ };
241
+ if (this.oauthFile) try {
242
+ let raw = JSON.parse(await readFile(this.oauthFile, "utf8"));
243
+ if (!isRecord(raw)) throw new ClaudeCodeAuthError("auth_invalid", `Invalid OAuth file: ${this.oauthFile}`);
244
+ const expiresAt = nonEmptyString(raw.expiresAt);
245
+ if (expiresAt !== void 0 && Date.parse(expiresAt) - Date.now() < OAUTH_EXPIRY_SKEW_MS && nonEmptyString(raw.refreshToken) && this.refresh) {
246
+ raw = await this.refresh(raw);
247
+ await writeFile(this.oauthFile, `${JSON.stringify(raw, null, 2)}\n`);
248
+ }
249
+ if (!isRecord(raw)) throw new ClaudeCodeAuthError("auth_invalid", `Invalid OAuth file: ${this.oauthFile}`);
250
+ const accessToken = nonEmptyString(raw.accessToken) ?? nonEmptyString(raw.access_token);
251
+ if (!accessToken) throw new ClaudeCodeAuthError("auth_missing", `No accessToken in ${this.oauthFile}`);
252
+ return {
253
+ accessToken,
254
+ source: "file",
255
+ subscriptionType: nonEmptyString(raw.subscriptionType) ?? null,
256
+ rateLimitTier: nonEmptyString(raw.rateLimitTier) ?? null
257
+ };
258
+ } catch (error) {
259
+ if (error instanceof ClaudeCodeAuthError) throw error;
260
+ throw new ClaudeCodeAuthError("auth_missing", `Failed to read Claude OAuth file ${this.oauthFile}: ${error instanceof Error ? error.message : String(error)}`);
261
+ }
262
+ const fromEnv = nonEmptyString(process$1.env.CLAUDE_CODE_OAUTH_TOKEN);
263
+ if (fromEnv) return {
264
+ accessToken: fromEnv,
265
+ source: "env"
266
+ };
267
+ if (process$1.platform === "darwin") try {
268
+ const { stdout } = await execFileAsync("security", [
269
+ "find-generic-password",
270
+ "-s",
271
+ "Claude Code-credentials",
272
+ "-w"
273
+ ], {
274
+ encoding: "utf8",
275
+ timeout: 5e3
276
+ });
277
+ const parsed = JSON.parse(stdout.trim());
278
+ if (!isRecord(parsed)) throw new ClaudeCodeAuthError("auth_missing", "Claude Code keychain item is not a JSON object");
279
+ const oauth = isRecord(parsed.claudeAiOauth) ? parsed.claudeAiOauth : null;
280
+ if (!oauth) throw new ClaudeCodeAuthError("auth_missing", "Claude Code keychain missing claudeAiOauth");
281
+ const accessToken = nonEmptyString(oauth.accessToken);
282
+ if (!accessToken) throw new ClaudeCodeAuthError("auth_missing", "Claude Code keychain missing accessToken");
283
+ return {
284
+ accessToken,
285
+ source: "keychain",
286
+ subscriptionType: nonEmptyString(oauth.subscriptionType) ?? null,
287
+ rateLimitTier: nonEmptyString(oauth.rateLimitTier) ?? null
288
+ };
289
+ } catch (error) {
290
+ if (error instanceof ClaudeCodeAuthError) throw error;
291
+ }
292
+ throw new ClaudeCodeAuthError("auth_missing", "Claude Code OAuth access token not found (set CLAUDE_CODE_OAUTH_TOKEN or log in with Claude Code)");
293
+ }
294
+ };
295
+ var StaticClaudeCodeAuthStore = class {
296
+ access;
297
+ constructor(access) {
298
+ this.access = access;
299
+ }
300
+ async status() {
301
+ return {
302
+ status: "authenticated",
303
+ accountLabel: nonEmptyString(this.access.subscriptionType) ?? "Claude Code"
304
+ };
305
+ }
306
+ async resolveAccess() {
307
+ return this.access;
308
+ }
309
+ };
310
+ var ClaudeCodeAuthError = class extends Error {
311
+ code;
312
+ constructor(code, message) {
313
+ super(message);
314
+ this.code = code;
315
+ this.name = "ClaudeCodeAuthError";
316
+ }
317
+ };
318
+ //#endregion
319
+ //#region src/login.ts
320
+ const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
321
+ const CLAUDE_CONSOLE_BASE = "https://console.anthropic.com";
322
+ const CLAUDE_CODE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
323
+ const CLAUDE_LOGIN_SCOPE = "org:create_api_key user:profile user:inference";
324
+ function tokenEndpoint(consoleBase) {
325
+ return `${consoleBase.replace(/\/+$/, "")}/v1/oauth/token`;
326
+ }
327
+ async function requestTokens(fetchImpl, consoleBase, body, signal) {
328
+ const response = await fetchImpl(tokenEndpoint(consoleBase), {
329
+ method: "POST",
330
+ headers: { "content-type": "application/json" },
331
+ body: JSON.stringify(body),
332
+ signal
333
+ });
334
+ if (!response.ok) throw new ClaudeCodeAuthError("auth_invalid", `Claude OAuth token request failed with HTTP ${response.status}`);
335
+ const parsed = await response.json().catch(() => null);
336
+ if (!isRecord(parsed)) throw new ClaudeCodeAuthError("auth_invalid", "Claude OAuth token response is not a JSON object");
337
+ const accessToken = nonEmptyString(parsed.access_token);
338
+ if (!accessToken) throw new ClaudeCodeAuthError("auth_invalid", "Claude OAuth token response is missing access_token");
339
+ const expiresIn = Number(parsed.expires_in);
340
+ const account = isRecord(parsed.account) ? parsed.account : {};
341
+ return {
342
+ accessToken,
343
+ refreshToken: nonEmptyString(parsed.refresh_token) ?? null,
344
+ expiresAt: Number.isFinite(expiresIn) && expiresIn > 0 ? new Date(Date.now() + expiresIn * 1e3).toISOString() : null,
345
+ scopes: typeof parsed.scope === "string" ? parsed.scope.split(" ").filter(Boolean) : null,
346
+ subscriptionType: nonEmptyString(parsed.subscription_type) ?? nonEmptyString(account.subscription_type) ?? null,
347
+ ...nonEmptyString(account.email_address) ? { emailAddress: nonEmptyString(account.email_address) } : {}
348
+ };
349
+ }
350
+ /** Runs the copy-back OAuth flow and returns a refreshable pool secret. */
351
+ async function runClaudeCodeLogin(options) {
352
+ const fetchImpl = options.fetch ?? fetch;
353
+ const consoleBase = options.consoleBase ?? CLAUDE_CONSOLE_BASE;
354
+ const verifier = randomBytes(32).toString("base64url");
355
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
356
+ const state = randomBytes(32).toString("base64url");
357
+ const redirectUri = `${consoleBase.replace(/\/+$/, "")}/oauth/code/callback`;
358
+ const params = new URLSearchParams({
359
+ code: "true",
360
+ client_id: CLAUDE_CODE_CLIENT_ID,
361
+ response_type: "code",
362
+ redirect_uri: redirectUri,
363
+ scope: CLAUDE_LOGIN_SCOPE,
364
+ code_challenge: challenge,
365
+ code_challenge_method: "S256",
366
+ state
367
+ });
368
+ options.onPending?.({
369
+ verificationUrl: `${CLAUDE_AUTHORIZE_URL}?${params.toString()}`,
370
+ requiresCodeInput: true
371
+ });
372
+ const pasted = (await options.promptForCode()).trim();
373
+ if (!pasted) throw new ClaudeCodeAuthError("auth_invalid", "Empty authorization code");
374
+ const [code, returnedState] = pasted.split("#");
375
+ if (!nonEmptyString(code)) throw new ClaudeCodeAuthError("auth_invalid", "Authorization code is missing the code part");
376
+ if (returnedState && returnedState !== state) throw new ClaudeCodeAuthError("auth_invalid", "Authorization code state mismatch — copy the full string from the callback page");
377
+ return requestTokens(fetchImpl, consoleBase, {
378
+ grant_type: "authorization_code",
379
+ code,
380
+ state: returnedState ?? state,
381
+ client_id: CLAUDE_CODE_CLIENT_ID,
382
+ redirect_uri: redirectUri,
383
+ code_verifier: verifier
384
+ }, options.signal);
385
+ }
386
+ /** Refreshes a pool secret in place; returns the renewed secret. */
387
+ async function refreshClaudeCodeSecret(secret, options = {}) {
388
+ const refreshToken = nonEmptyString(secret.refreshToken);
389
+ if (!refreshToken) throw new ClaudeCodeAuthError("auth_missing", "Claude OAuth secret has no refreshToken to renew with");
390
+ const renewed = await requestTokens(options.fetch ?? fetch, options.consoleBase ?? CLAUDE_CONSOLE_BASE, {
391
+ grant_type: "refresh_token",
392
+ refresh_token: refreshToken,
393
+ client_id: CLAUDE_CODE_CLIENT_ID
394
+ }, options.signal);
395
+ return {
396
+ ...secret,
397
+ accessToken: renewed.accessToken,
398
+ refreshToken: renewed.refreshToken ?? refreshToken,
399
+ expiresAt: renewed.expiresAt ?? secret.expiresAt ?? null,
400
+ scopes: renewed.scopes ?? secret.scopes ?? null,
401
+ subscriptionType: renewed.subscriptionType ?? secret.subscriptionType ?? null,
402
+ ...nonEmptyString(renewed.emailAddress) ? { emailAddress: renewed.emailAddress } : {}
403
+ };
404
+ }
405
+ //#endregion
406
+ //#region src/credentials.ts
407
+ var PoolAwareClaudeCodeAuthStore = class {
408
+ pool;
409
+ constructor(pool) {
410
+ this.pool = pool;
411
+ }
412
+ async status() {
413
+ return this.currentStore().then((s) => s.status());
414
+ }
415
+ async resolveAccess() {
416
+ return this.currentStore().then((s) => s.resolveAccess());
417
+ }
418
+ async currentStore() {
419
+ await this.pool.ensureActivePointer();
420
+ const activeId = await this.pool.getActiveId();
421
+ if (activeId) return new FileClaudeCodeAuthStore({
422
+ oauthFile: this.pool.secretPath(activeId),
423
+ refresh: (secret) => refreshClaudeCodeSecret(secret)
424
+ });
425
+ return new FileClaudeCodeAuthStore();
426
+ }
427
+ };
428
+ function openClaudeCodeCredentialPool(options = {}) {
429
+ return new FileCredentialPool({
430
+ stateDir: options.stateDir,
431
+ providerKey: "claude-code",
432
+ secretFileName: "oauth.json"
433
+ });
434
+ }
435
+ function createClaudeCodeCredentials(pool, authStore, options = {}) {
436
+ const capability = () => ({
437
+ mode: "supported",
438
+ canBeginLogin: true,
439
+ canImportDefault: true,
440
+ canAdd: true,
441
+ multi: true
442
+ });
443
+ const getActive = async () => {
444
+ await pool.ensureActivePointer();
445
+ return {
446
+ credentialId: await pool.getActiveId(),
447
+ status: await authStore.status()
448
+ };
449
+ };
450
+ const setActive = async (credentialId) => {
451
+ await pool.setActiveId(credentialId);
452
+ options.quota?.clearLatest?.();
453
+ options.onActiveChange?.();
454
+ return getActive();
455
+ };
456
+ const importAccess = async (access, source) => {
457
+ const token = nonEmptyString(access.accessToken);
458
+ if (!token) throw new ClaudeCodeAuthError("auth_missing", "No Claude access token to import");
459
+ const identityKey = nonEmptyString(access.subscriptionType) != null ? `token:${createHash("sha256").update(token).digest("hex").slice(0, 16)}:${access.subscriptionType}` : `token:${createHash("sha256").update(token).digest("hex").slice(0, 16)}`;
460
+ const label = nonEmptyString(access.subscriptionType) ?? `claude-${identityKey.slice(-8)}`;
461
+ const id = (await pool.findByIdentityKey(identityKey))?.id ?? credentialIdFromIdentity(identityKey, label);
462
+ const meta = {
463
+ id,
464
+ label,
465
+ detail: nonEmptyString(access.rateLimitTier) ?? null,
466
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
467
+ source,
468
+ identityKey
469
+ };
470
+ const secret = {
471
+ accessToken: token,
472
+ subscriptionType: access.subscriptionType ?? null,
473
+ rateLimitTier: access.rateLimitTier ?? null
474
+ };
475
+ await pool.writeEntry(meta, `${JSON.stringify(secret, null, 2)}\n`);
476
+ if (!await pool.getActiveId()) await pool.setActiveId(id);
477
+ options.quota?.clearLatest?.();
478
+ options.onActiveChange?.();
479
+ return {
480
+ id: meta.id,
481
+ label: meta.label,
482
+ detail: meta.detail,
483
+ updatedAt: meta.updatedAt
484
+ };
485
+ };
486
+ const importSecret = async (secret, source) => {
487
+ const email = nonEmptyString(secret.emailAddress);
488
+ const identityKey = email ? `email:${email}` : `token:${createHash("sha256").update(secret.accessToken).digest("hex").slice(0, 16)}`;
489
+ const label = email ?? nonEmptyString(secret.subscriptionType) ?? `claude-${identityKey.slice(-8)}`;
490
+ const id = (await pool.findByIdentityKey(identityKey))?.id ?? credentialIdFromIdentity(identityKey, label);
491
+ const meta = {
492
+ id,
493
+ label,
494
+ detail: nonEmptyString(secret.subscriptionType) ?? nonEmptyString(secret.rateLimitTier) ?? null,
495
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
496
+ source,
497
+ identityKey
498
+ };
499
+ await pool.writeEntry(meta, `${JSON.stringify(secret, null, 2)}\n`);
500
+ if (!await pool.getActiveId()) await pool.setActiveId(id);
501
+ options.quota?.clearLatest?.();
502
+ options.onActiveChange?.();
503
+ return {
504
+ id: meta.id,
505
+ label: meta.label,
506
+ detail: meta.detail,
507
+ updatedAt: meta.updatedAt
508
+ };
509
+ };
510
+ return {
511
+ capability,
512
+ list: () => pool.list(),
513
+ getActive,
514
+ setActive,
515
+ beginLogin: async (loginOptions) => {
516
+ if (!loginOptions?.promptForCode) return {
517
+ status: "unavailable",
518
+ message: "Claude login requires promptForCode to collect the pasted authorization code"
519
+ };
520
+ try {
521
+ const secret = await runClaudeCodeLogin({
522
+ signal: loginOptions.signal,
523
+ onPending: loginOptions.onPending,
524
+ promptForCode: loginOptions.promptForCode,
525
+ fetch: options.loginFetch
526
+ });
527
+ return {
528
+ status: "completed",
529
+ credentialId: (await importSecret(secret, "login:oauth")).id
530
+ };
531
+ } catch (error) {
532
+ if (loginOptions.signal?.aborted) return { status: "cancelled" };
533
+ return {
534
+ status: "failed",
535
+ message: errorMessage(error)
536
+ };
537
+ }
538
+ },
539
+ importDefault: async () => {
540
+ const vendor = new FileClaudeCodeAuthStore();
541
+ let access;
542
+ try {
543
+ access = await vendor.resolveAccess();
544
+ } catch {
545
+ throw new ClaudeCodeAuthError("auth_missing", "No Claude Code OAuth to import. Run claude auth login or beginLogin first.");
546
+ }
547
+ return importAccess(access, "vendor:default");
548
+ },
549
+ add: async (input) => {
550
+ if (typeof input.accessToken === "string") return importAccess({
551
+ accessToken: input.accessToken,
552
+ source: "static",
553
+ subscriptionType: typeof input.subscriptionType === "string" ? input.subscriptionType : null,
554
+ rateLimitTier: typeof input.rateLimitTier === "string" ? input.rateLimitTier : null
555
+ }, "add:accessToken");
556
+ if (isRecord(input.oauth) && typeof input.oauth.accessToken === "string") {
557
+ const oauth = input.oauth;
558
+ return importAccess({
559
+ accessToken: oauth.accessToken,
560
+ source: "static",
561
+ subscriptionType: typeof oauth.subscriptionType === "string" ? oauth.subscriptionType : null,
562
+ rateLimitTier: typeof oauth.rateLimitTier === "string" ? oauth.rateLimitTier : null
563
+ }, "add:oauth");
564
+ }
565
+ throw new Error("Claude credentials.add expects accessToken or oauth.accessToken");
566
+ },
567
+ remove: async (credentialId) => {
568
+ await pool.remove(credentialId);
569
+ options.quota?.clearLatest?.();
570
+ options.onActiveChange?.();
571
+ }
572
+ };
573
+ }
574
+ //#endregion
200
575
  //#region src/jsonl.ts
201
576
  /**
202
577
  * Builds the input messages used to prime a *fresh* Claude CLI process with prior
@@ -260,12 +635,10 @@ function coldStartInputMessages(items) {
260
635
  text: renderToolUseText(item.toolName, item.input)
261
636
  }]);
262
637
  break;
263
- case "tool_result":
264
- append("assistant", [{
265
- type: "text",
266
- text: renderToolResultText(toolNames.get(item.toolUseId), item)
267
- }]);
268
- break;
638
+ case "tool_result": append("assistant", [{
639
+ type: "text",
640
+ text: renderToolResultText(toolNames.get(item.toolUseId), item)
641
+ }]);
269
642
  }
270
643
  flush();
271
644
  return messages;
@@ -274,7 +647,7 @@ function renderToolUseText(toolName, input) {
274
647
  return `[Earlier in this conversation I called the tool ${toolName} with input: ${safeJson(input)}.`;
275
648
  }
276
649
  function renderToolResultText(toolName, item) {
277
- const body = toolResultToText(item.output);
650
+ const body = toolResultContentToText(item.output);
278
651
  const suffix = toolName ? ` from ${toolName}` : "";
279
652
  return item.isError ? `It returned an error${suffix}: ${body}]` : `It returned${suffix}: ${body}]`;
280
653
  }
@@ -339,6 +712,10 @@ function userContentToClaude(content) {
339
712
  type: "image",
340
713
  source: imageSourceToClaude(block.source)
341
714
  };
715
+ if (block.type === "video") return {
716
+ type: "text",
717
+ text: "[video]"
718
+ };
342
719
  if (block.type === "document") return documentSourceToClaude(block.source);
343
720
  return {
344
721
  type: "text",
@@ -371,11 +748,30 @@ function assistantItemToClaudeContent(item) {
371
748
  };
372
749
  }
373
750
  function toolResultToClaudeContent(item) {
751
+ const hasImage = item.output.some((block) => block.type !== "text");
374
752
  return {
375
753
  type: "tool_result",
376
754
  tool_use_id: item.toolUseId,
377
755
  is_error: item.isError,
378
- content: toolResultToText(item.output)
756
+ content: hasImage ? item.output.map(toolResultBlockToClaude) : toolResultContentToText(item.output)
757
+ };
758
+ }
759
+ function toolResultBlockToClaude(block) {
760
+ if (block.type === "text") return {
761
+ type: "text",
762
+ text: block.text
763
+ };
764
+ if (block.type === "video") return {
765
+ type: "text",
766
+ text: `[video:${block.source.mediaType}]`
767
+ };
768
+ return {
769
+ type: "image",
770
+ source: {
771
+ type: "base64",
772
+ media_type: block.source.mediaType,
773
+ data: block.source.data
774
+ }
379
775
  };
380
776
  }
381
777
  function imageSourceToClaude(source) {
@@ -400,9 +796,6 @@ function documentSourceToClaude(source) {
400
796
  title: source.fileName
401
797
  };
402
798
  }
403
- function toolResultToText(output) {
404
- return output.map((block) => block.type === "text" ? block.text : `[image:${block.source.mediaType}]`).join("\n");
405
- }
406
799
  function bytesToBase64(data) {
407
800
  return Buffer$1.from(data.buffer, data.byteOffset, data.byteLength).toString("base64");
408
801
  }
@@ -411,6 +804,28 @@ function toolNameToClaude(name) {
411
804
  return `mcp__main__${name}`;
412
805
  }
413
806
  //#endregion
807
+ //#region src/oauth.ts
808
+ /**
809
+ * The token to inject into a spawned CLI as CLAUDE_CODE_OAUTH_TOKEN, or null
810
+ * to let the CLI authenticate itself. Keychain-sourced tokens are never
811
+ * injected — see {@link ClaudeCodeOAuthSource}.
812
+ */
813
+ function injectableCliToken(access) {
814
+ return access.source === "keychain" ? null : access.accessToken;
815
+ }
816
+ /**
817
+ * Resolve Claude Code consumer OAuth access for quota APIs.
818
+ * Order: options env CLAUDE_CODE_OAUTH_TOKEN, then macOS Keychain "Claude Code-credentials".
819
+ * Prefer injecting {@link ClaudeCodeAuthStore} when multi-credential is enabled.
820
+ */
821
+ async function resolveClaudeCodeOAuthAccess() {
822
+ try {
823
+ return await new FileClaudeCodeAuthStore().resolveAccess();
824
+ } catch {
825
+ return null;
826
+ }
827
+ }
828
+ //#endregion
414
829
  //#region src/output.ts
415
830
  function mapClaudeStdoutMessage(message, options = {}) {
416
831
  const events = [];
@@ -418,7 +833,7 @@ function mapClaudeStdoutMessage(message, options = {}) {
418
833
  events,
419
834
  terminal: false
420
835
  };
421
- if (message.type === "assistant" && isRecord(message.message) && !options.ignoreAssistantContent) events.push(...mapContentArray(message.message.content, options));
836
+ if (message.type === "assistant" && isRecord(message.message)) events.push(...mapContentArray(message.message.content, options));
422
837
  if (message.type === "stream_event" && isRecord(message.event)) events.push(...mapStreamEvent(message.event));
423
838
  if (message.type === "control_request") {
424
839
  const request = parseControlRequest(message);
@@ -439,7 +854,7 @@ function mapClaudeStdoutMessage(message, options = {}) {
439
854
  }
440
855
  events.push({
441
856
  type: "response",
442
- usage: mapUsage(message.usage)
857
+ usage: mapResultUsage(message.usage)
443
858
  });
444
859
  return {
445
860
  events,
@@ -476,11 +891,11 @@ function mapContentArray(content, options) {
476
891
  const events = [];
477
892
  for (const block of content) {
478
893
  if (!isRecord(block)) continue;
479
- if (block.type === "text") events.push({
894
+ if (block.type === "text" && !options.ignoreAssistantContent) events.push({
480
895
  type: "text_delta",
481
896
  text: String(block.text ?? "")
482
897
  });
483
- else if (block.type === "thinking") {
898
+ else if (block.type === "thinking" && !options.ignoreAssistantContent) {
484
899
  events.push({ type: "thinking_start" });
485
900
  events.push({
486
901
  type: "thinking_delta",
@@ -490,7 +905,7 @@ function mapContentArray(content, options) {
490
905
  type: "thinking_signature",
491
906
  signature: block.signature
492
907
  });
493
- } else if (block.type === "redacted_thinking") events.push({
908
+ } else if (block.type === "redacted_thinking" && !options.ignoreAssistantContent) events.push({
494
909
  type: "redacted_thinking",
495
910
  data: String(block.data ?? "")
496
911
  });
@@ -554,6 +969,7 @@ function parseControlRequest(message) {
554
969
  outerRequestId: message.request_id,
555
970
  serverName: request.server_name,
556
971
  id,
972
+ toolUseId: sdkMcpToolUseId(inner.params),
557
973
  method,
558
974
  params: inner.params
559
975
  };
@@ -568,6 +984,26 @@ function parseControlRequest(message) {
568
984
  params: message.params
569
985
  };
570
986
  }
987
+ function sdkMcpToolUseId(params) {
988
+ if (!isRecord(params) || !isRecord(params._meta)) return void 0;
989
+ const value = params._meta["claudecode/toolUseId"];
990
+ return typeof value === "string" && value.length > 0 ? value : void 0;
991
+ }
992
+ /**
993
+ * Usage for the response event from a `result` message. The top-level
994
+ * `result.usage` sums every API call the CLI made within the turn (initial call
995
+ * plus one per tool result), so its input/cache counts can exceed the context
996
+ * window itself and must not be reported as request usage. `usage.iterations`
997
+ * lists the per-call usage; the last entry is the final request — the one the
998
+ * response contract requires.
999
+ */
1000
+ function mapResultUsage(usage) {
1001
+ if (isRecord(usage) && Array.isArray(usage.iterations)) {
1002
+ const last = usage.iterations[usage.iterations.length - 1];
1003
+ if (isRecord(last)) return mapUsage(last);
1004
+ }
1005
+ return mapUsage(usage);
1006
+ }
571
1007
  function mapUsage(usage) {
572
1008
  if (!isRecord(usage)) return {
573
1009
  inputTokens: 0,
@@ -605,42 +1041,6 @@ function stripMcpToolPrefix(name) {
605
1041
  return /^mcp__[^_]+__(.+)$/.exec(name)?.[1] ?? name;
606
1042
  }
607
1043
  //#endregion
608
- //#region src/oauth.ts
609
- const execFileAsync = promisify(execFile);
610
- /**
611
- * Resolve Claude Code consumer OAuth access for quota APIs.
612
- * Order: options env CLAUDE_CODE_OAUTH_TOKEN, then macOS Keychain "Claude Code-credentials".
613
- */
614
- async function resolveClaudeCodeOAuthAccess() {
615
- const fromEnv = nonEmptyString(process$1.env.CLAUDE_CODE_OAUTH_TOKEN);
616
- if (fromEnv) return { accessToken: fromEnv };
617
- if (process$1.platform === "darwin") try {
618
- const { stdout } = await execFileAsync("security", [
619
- "find-generic-password",
620
- "-s",
621
- "Claude Code-credentials",
622
- "-w"
623
- ], {
624
- encoding: "utf8",
625
- timeout: 5e3
626
- });
627
- const parsed = JSON.parse(stdout.trim());
628
- if (!isRecord(parsed)) return null;
629
- const oauth = isRecord(parsed.claudeAiOauth) ? parsed.claudeAiOauth : null;
630
- if (!oauth) return null;
631
- const accessToken = nonEmptyString(oauth.accessToken);
632
- if (!accessToken) return null;
633
- return {
634
- accessToken,
635
- subscriptionType: nonEmptyString(oauth.subscriptionType) ?? null,
636
- rateLimitTier: nonEmptyString(oauth.rateLimitTier) ?? null
637
- };
638
- } catch {
639
- return null;
640
- }
641
- return null;
642
- }
643
- //#endregion
644
1044
  //#region src/quota.ts
645
1045
  const DEFAULT_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
646
1046
  const DEFAULT_OAUTH_BETA = "oauth-2025-04-20";
@@ -662,14 +1062,15 @@ function createClaudeCodeQuota(options = {}) {
662
1062
  probe: async ({ signal } = {}) => {
663
1063
  const access = await resolveAccess();
664
1064
  if (!access?.accessToken) throw new Error("Claude Code OAuth access token not found (set CLAUDE_CODE_OAUTH_TOKEN or log in with Claude Code)");
1065
+ const headers = new Headers({
1066
+ authorization: `Bearer ${access.accessToken}`,
1067
+ "anthropic-beta": DEFAULT_OAUTH_BETA,
1068
+ accept: "application/json",
1069
+ "user-agent": "demi-provider-claude-code"
1070
+ });
665
1071
  const response = await fetchImpl(usageUrl, {
666
1072
  method: "GET",
667
- headers: new Headers({
668
- authorization: `Bearer ${access.accessToken}`,
669
- "anthropic-beta": DEFAULT_OAUTH_BETA,
670
- accept: "application/json",
671
- "user-agent": "demi-provider-claude-code"
672
- }),
1073
+ headers,
673
1074
  signal
674
1075
  });
675
1076
  if (!response.ok) {
@@ -746,7 +1147,7 @@ function observeClaudeRateLimitHeaders(headers) {
746
1147
  const claim = headers.get("anthropic-ratelimit-unified-representative-claim");
747
1148
  const overageUtil = headers.get("anthropic-ratelimit-unified-overage-period-channel-utilization");
748
1149
  if (!status && !reset && !claim && !overageUtil) return null;
749
- const usedPercent = clampUsedPercent(overageUtil != null ? Number(overageUtil) : null);
1150
+ const usedPercent = clampUsedPercent(numberHeader(headers, "anthropic-ratelimit-unified-overage-period-channel-utilization"));
750
1151
  return {
751
1152
  windows: [{
752
1153
  id: "unified",
@@ -803,13 +1204,15 @@ function buildClaudeArgs(params) {
803
1204
  if (params.thinkingEffort) args.push("--effort", params.thinkingEffort);
804
1205
  return args;
805
1206
  }
806
- function buildClaudeEnv(base = process.env) {
1207
+ function buildClaudeEnv(base = process.env, options = {}) {
807
1208
  const env = {
808
1209
  ...base,
809
1210
  DISABLE_AUTO_COMPACT: "1",
810
1211
  MAX_MCP_OUTPUT_TOKENS: "1000000"
811
1212
  };
812
1213
  delete env.CLAUDECODE;
1214
+ const token = options.oauthAccessToken?.trim();
1215
+ if (token) env.CLAUDE_CODE_OAUTH_TOKEN = token;
813
1216
  return env;
814
1217
  }
815
1218
  //#endregion
@@ -835,7 +1238,8 @@ function createClaudeWireLog(sessionId) {
835
1238
  } catch {
836
1239
  return NULL_WIRE_LOG;
837
1240
  }
838
- const path = join(dir, `claude-${sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") || "session"}.jsonl`);
1241
+ const safeSession = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_") || "session";
1242
+ const path = join(dir, `claude-${safeSession}.jsonl`);
839
1243
  return {
840
1244
  path,
841
1245
  record(direction, data) {
@@ -856,13 +1260,19 @@ function resolveSpawnCwd(cwd) {
856
1260
  try {
857
1261
  if (statSync(cwd).isDirectory()) return cwd;
858
1262
  } catch {}
859
- return process.cwd();
1263
+ return process$1.cwd();
860
1264
  }
861
1265
  var ClaudeCliTransportFactory = class {
862
1266
  claudePath;
1267
+ resolveOAuthAccessToken;
863
1268
  constructor(options = {}) {
864
- if (typeof options === "string") this.claudePath = options;
865
- else this.claudePath = options.claudePath ?? "claude";
1269
+ if (typeof options === "string") {
1270
+ this.claudePath = options;
1271
+ this.resolveOAuthAccessToken = null;
1272
+ } else {
1273
+ this.claudePath = options.claudePath ?? "claude";
1274
+ this.resolveOAuthAccessToken = options.resolveOAuthAccessToken ?? null;
1275
+ }
866
1276
  }
867
1277
  async start(request) {
868
1278
  const args = buildClaudeArgsForRequest(request);
@@ -874,9 +1284,10 @@ var ClaudeCliTransportFactory = class {
874
1284
  cwd: request.cwd,
875
1285
  args
876
1286
  });
1287
+ const oauthAccessToken = this.resolveOAuthAccessToken ? await this.resolveOAuthAccessToken() : null;
877
1288
  return new ChildProcessClaudeTransport(spawn(this.claudePath, args, {
878
1289
  cwd: resolveSpawnCwd(request.cwd),
879
- env: buildClaudeEnv(),
1290
+ env: buildClaudeEnv(process$1.env, { oauthAccessToken }),
880
1291
  stdio: [
881
1292
  "pipe",
882
1293
  "pipe",
@@ -962,13 +1373,32 @@ function thinkingEffort(thinking) {
962
1373
  }
963
1374
  //#endregion
964
1375
  //#region src/provider.ts
965
- var ClaudeCodeProvider = class {
1376
+ var ClaudeCodeProvider = class ClaudeCodeProvider {
1377
+ cloneOptions;
966
1378
  transportFactory;
967
1379
  quota;
1380
+ getActiveCredentialId;
968
1381
  active = null;
969
1382
  constructor(options = {}) {
970
- this.transportFactory = options.transportFactory ?? new ClaudeCliTransportFactory({ claudePath: options.claudePath });
1383
+ this.cloneOptions = options;
1384
+ this.transportFactory = options.transportFactory ?? new ClaudeCliTransportFactory({
1385
+ claudePath: options.claudePath,
1386
+ resolveOAuthAccessToken: options.authStore ? async () => {
1387
+ try {
1388
+ return injectableCliToken(await options.authStore.resolveAccess());
1389
+ } catch {
1390
+ return null;
1391
+ }
1392
+ } : void 0
1393
+ });
971
1394
  this.quota = options.quota ?? null;
1395
+ this.getActiveCredentialId = options.getActiveCredentialId ?? null;
1396
+ }
1397
+ clone() {
1398
+ return new ClaudeCodeProvider({
1399
+ ...this.cloneOptions,
1400
+ transportFactory: this.transportFactory
1401
+ });
972
1402
  }
973
1403
  observeQuotaFromMessage(message) {
974
1404
  try {
@@ -1018,21 +1448,50 @@ var ClaudeCodeProvider = class {
1018
1448
  }
1019
1449
  const raw = next.value;
1020
1450
  this.observeQuotaFromMessage(raw);
1021
- const mapped = mapClaudeStdoutMessage(raw, {
1022
- ignoreAssistantContent: active.hasStreamed && isMessageType(raw, "assistant"),
1023
- ignoreAssistantToolUse: active.sdkMcpEnabled
1024
- });
1451
+ const mapped = mapClaudeStdoutMessage(raw, { ignoreAssistantContent: active.hasStreamed && isMessageType(raw, "assistant") });
1025
1452
  if (isMessageType(raw, "stream_event")) active.hasStreamed = true;
1026
1453
  if (mapped.controlRequest) {
1027
- if (await this.handleControlRequest(active, mapped.controlRequest, request) === "tool-call") {
1454
+ const handled = await this.handleControlRequest(active, mapped.controlRequest, request);
1455
+ if (handled === "tool-call") {
1028
1456
  const event = active.pendingControlRequest ? controlRequestToToolCall(active.pendingControlRequest) : null;
1029
1457
  keepActiveForContinuation = true;
1030
1458
  if (event) yield event;
1031
1459
  return;
1032
1460
  }
1461
+ if (handled === "sdk-tool-call") {
1462
+ const pending = active.pendingSdkControlRequests.get(mapped.controlRequest.toolUseId ?? "");
1463
+ const event = pending ? controlRequestToToolCall(pending) : null;
1464
+ if (event?.type === "tool_call_requested") {
1465
+ active.pendingSdkToolCalls = [event];
1466
+ keepActiveForContinuation = true;
1467
+ yield event;
1468
+ }
1469
+ return;
1470
+ }
1033
1471
  continue;
1034
1472
  }
1035
1473
  const toolUseIds = mapped.events.filter(isToolCallRequested).map((event) => event.toolUseId);
1474
+ if (active.sdkMcpEnabled) {
1475
+ for (const event of mapped.events) {
1476
+ if (event.type === "tool_call_requested") {
1477
+ active.collectingSdkToolCalls.set(event.toolUseId, event);
1478
+ continue;
1479
+ }
1480
+ yield event;
1481
+ }
1482
+ if (isStreamMessageStop(raw) && active.collectingSdkToolCalls.size > 0) {
1483
+ active.pendingSdkToolCalls = [...active.collectingSdkToolCalls.values()];
1484
+ active.collectingSdkToolCalls.clear();
1485
+ keepActiveForContinuation = true;
1486
+ for (const event of active.pendingSdkToolCalls) yield event;
1487
+ return;
1488
+ }
1489
+ if (mapped.terminal) {
1490
+ keepActiveForContinuation = true;
1491
+ return;
1492
+ }
1493
+ continue;
1494
+ }
1036
1495
  if (toolUseIds.length > 0) active.pendingToolUseIds = toolUseIds;
1037
1496
  for (const event of mapped.events) {
1038
1497
  if (event.type === "tool_call_requested") keepActiveForContinuation = true;
@@ -1075,22 +1534,26 @@ var ClaudeCodeProvider = class {
1075
1534
  * the session changed, or the transcript was rewritten underneath us (compaction).
1076
1535
  */
1077
1536
  async ensureActiveForRequest(request) {
1537
+ const credentialId = this.getActiveCredentialId ? await this.getActiveCredentialId() : null;
1078
1538
  const existing = this.active;
1079
- if (existing && existing.sessionId === request.sessionId && existing.modelId === request.modelId && existing.thinkingSig === thinkingSignature(request)) {
1080
- if (existing.pendingControlRequest !== null || existing.pendingToolUseIds.length > 0 || !itemsDiverged(existing, request.items)) {
1539
+ if (existing && existing.sessionId === request.sessionId && existing.modelId === request.modelId && existing.thinkingSig === thinkingSignature(request) && existing.credentialId === credentialId) {
1540
+ if (existing.pendingControlRequest !== null || existing.pendingSdkToolCalls.length > 0 || existing.pendingToolUseIds.length > 0 || !itemsDiverged(existing, request.items)) {
1081
1541
  await this.sendContinuation(existing, request);
1082
1542
  return existing;
1083
1543
  }
1084
1544
  }
1085
1545
  if (existing) await this.disposeActive(existing);
1086
- return this.coldStart(request);
1546
+ return this.coldStart(request, credentialId);
1087
1547
  }
1088
- async coldStart(request) {
1548
+ async coldStart(request, credentialId) {
1089
1549
  const transport = await this.transportFactory.start(request);
1090
1550
  const active = {
1091
1551
  transport,
1092
1552
  iterator: transport.messages()[Symbol.asyncIterator](),
1093
1553
  pendingControlRequest: null,
1554
+ pendingSdkControlRequests: /* @__PURE__ */ new Map(),
1555
+ pendingSdkToolCalls: [],
1556
+ collectingSdkToolCalls: /* @__PURE__ */ new Map(),
1094
1557
  pendingToolUseIds: [],
1095
1558
  bufferedMessages: [],
1096
1559
  sdkMcpEnabled: request.tools.length > 0,
@@ -1098,6 +1561,7 @@ var ClaudeCodeProvider = class {
1098
1561
  sessionId: request.sessionId,
1099
1562
  modelId: request.modelId,
1100
1563
  thinkingSig: thinkingSignature(request),
1564
+ credentialId,
1101
1565
  sentUserMessageCount: 0,
1102
1566
  firstUserSig: null
1103
1567
  };
@@ -1114,6 +1578,10 @@ var ClaudeCodeProvider = class {
1114
1578
  await this.writeToolResults(request, active.pendingControlRequest);
1115
1579
  active.pendingControlRequest = null;
1116
1580
  }
1581
+ if (active.pendingSdkToolCalls.length > 0) {
1582
+ await this.writeSdkMcpToolResults(active, request);
1583
+ active.pendingSdkToolCalls = [];
1584
+ }
1117
1585
  if (active.pendingToolUseIds.length > 0) {
1118
1586
  await this.writeToolResultMessages(request, active.pendingToolUseIds);
1119
1587
  active.pendingToolUseIds = [];
@@ -1167,6 +1635,15 @@ var ClaudeCodeProvider = class {
1167
1635
  await this.writeControlError(active, request, "Invalid tools/call request");
1168
1636
  return "handled";
1169
1637
  }
1638
+ if (request.protocol === "sdk-mcp") {
1639
+ request.toolUseId ??= `mcp-control-${randomUUID()}`;
1640
+ const normalized = {
1641
+ ...request,
1642
+ toolUseId: request.toolUseId
1643
+ };
1644
+ active.pendingSdkControlRequests.set(normalized.toolUseId, normalized);
1645
+ return active.hasStreamed ? "handled" : "sdk-tool-call";
1646
+ }
1170
1647
  active.pendingControlRequest = {
1171
1648
  ...request,
1172
1649
  toolUseId: `mcp-control-${randomUUID()}`
@@ -1176,6 +1653,38 @@ var ClaudeCodeProvider = class {
1176
1653
  await this.writeControlError(active, request, `Unsupported method: ${request.method}`);
1177
1654
  return "handled";
1178
1655
  }
1656
+ async writeSdkMcpToolResults(active, request) {
1657
+ const results = new Map(request.items.filter((item) => item.type === "tool_result").map((item) => [item.toolUseId, item]));
1658
+ const expected = active.pendingSdkToolCalls.map((event) => event.toolUseId);
1659
+ const missing = expected.filter((toolUseId) => !results.has(toolUseId));
1660
+ if (missing.length > 0) throw new Error(`Claude Code provider missing tool_result for SDK MCP tool_use ${missing.join(", ")}`);
1661
+ const remaining = new Set(expected);
1662
+ while (remaining.size > 0) {
1663
+ let responded = false;
1664
+ for (const toolUseId of remaining) {
1665
+ const controlRequest = active.pendingSdkControlRequests.get(toolUseId);
1666
+ if (!controlRequest) continue;
1667
+ await this.writeToolResults(request, controlRequest);
1668
+ active.pendingSdkControlRequests.delete(toolUseId);
1669
+ remaining.delete(toolUseId);
1670
+ responded = true;
1671
+ }
1672
+ if (remaining.size === 0) return;
1673
+ if (responded) continue;
1674
+ const next = await abortable(active.iterator.next(), request.cancel);
1675
+ if (next.done) throw new Error(`Claude Code exited before requesting SDK MCP tool result for ${[...remaining].join(", ")}`);
1676
+ this.observeQuotaFromMessage(next.value);
1677
+ const mapped = mapClaudeStdoutMessage(next.value, {
1678
+ ignoreAssistantContent: true,
1679
+ ignoreAssistantToolUse: true
1680
+ });
1681
+ if (mapped.controlRequest) {
1682
+ if (await this.handleControlRequest(active, mapped.controlRequest, request) === "tool-call") throw new Error("Claude Code emitted a legacy tool call while awaiting SDK MCP results");
1683
+ continue;
1684
+ }
1685
+ active.bufferedMessages.push(next.value);
1686
+ }
1687
+ }
1179
1688
  async writeToolResults(request, controlRequest) {
1180
1689
  if (!this.active) throw new Error("No active Claude transport");
1181
1690
  const expectedToolUseId = controlRequest.toolUseId ?? String(controlRequest.id);
@@ -1276,25 +1785,39 @@ var ClaudeCodeProvider = class {
1276
1785
  function createClaudeCodeProvider(options = {}) {
1277
1786
  const id = options.id ?? "claude-code";
1278
1787
  const displayName = options.displayName ?? "Claude Code";
1279
- const quota = createClaudeCodeQuota({ providerId: id });
1788
+ const enableCredentials = options.credentials ?? options.authStore === void 0;
1789
+ const pool = !options.authStore && enableCredentials ? openClaudeCodeCredentialPool({ stateDir: options.stateDir }) : null;
1790
+ const authStore = options.authStore ?? (pool ? new PoolAwareClaudeCodeAuthStore(pool) : new FileClaudeCodeAuthStore());
1791
+ const quota = createClaudeCodeQuota({
1792
+ providerId: id,
1793
+ resolveAccess: async () => {
1794
+ try {
1795
+ return await authStore.resolveAccess();
1796
+ } catch {
1797
+ return null;
1798
+ }
1799
+ }
1800
+ });
1801
+ const credentialsApi = pool ? createClaudeCodeCredentials(pool, authStore, { quota }) : void 0;
1280
1802
  const runtimeOptions = {
1281
1803
  claudePath: options.claudePath,
1282
- quota
1804
+ quota,
1805
+ authStore,
1806
+ getActiveCredentialId: pool ? () => pool.getActiveId() : void 0
1283
1807
  };
1284
1808
  return defineProvider({
1285
1809
  id,
1286
1810
  displayName,
1287
- auth: { status: () => ({
1288
- status: "unknown",
1289
- message: "Auth is checked when a Claude Code request runs"
1290
- }) },
1811
+ auth: { status: () => authStore.status() },
1291
1812
  quota,
1813
+ ...credentialsApi ? { credentials: credentialsApi } : {},
1292
1814
  state: () => ({
1293
1815
  status: "unknown",
1294
1816
  message: "Runtime is checked when a Claude Code request runs"
1295
1817
  }),
1296
1818
  listModels: async () => {
1297
- return applyModelPolicy(await listClaudeCodeModels(), id, options.models);
1819
+ const catalog = await listClaudeCodeModels();
1820
+ return applyModelPolicy(catalog, id, options.models);
1298
1821
  },
1299
1822
  createRuntime: () => new ClaudeCodeProvider(runtimeOptions)
1300
1823
  });
@@ -1306,18 +1829,19 @@ function isControlResponseFor(value, requestId) {
1306
1829
  if (!isRecord(value) || value.type !== "control_response" || !isRecord(value.response)) return false;
1307
1830
  return value.response.request_id === requestId && value.response.subtype === "success";
1308
1831
  }
1309
- function toolResultContentToText(output) {
1310
- return output.map((block) => block.type === "text" ? block.text : `[image:${block.source.mediaType}]`).join("\n");
1311
- }
1312
1832
  function toolResultContentToMcp(output) {
1313
1833
  return output.map((block) => {
1314
1834
  if (block.type === "text") return {
1315
1835
  type: "text",
1316
1836
  text: block.text
1317
1837
  };
1838
+ if (block.type === "video") return {
1839
+ type: "text",
1840
+ text: `[video:${block.source.mediaType}]`
1841
+ };
1318
1842
  return {
1319
1843
  type: "image",
1320
- data: Buffer.from(block.source.data).toString("base64"),
1844
+ data: block.source.data,
1321
1845
  mimeType: block.source.mediaType
1322
1846
  };
1323
1847
  });
@@ -1325,6 +1849,9 @@ function toolResultContentToMcp(output) {
1325
1849
  function isMessageType(value, type) {
1326
1850
  return isRecord(value) && value.type === type;
1327
1851
  }
1852
+ function isStreamMessageStop(value) {
1853
+ return isRecord(value) && value.type === "stream_event" && isRecord(value.event) && value.event.type === "message_stop";
1854
+ }
1328
1855
  function thinkingSignature(request) {
1329
1856
  return JSON.stringify(request.thinking ?? null);
1330
1857
  }
@@ -1349,4 +1876,4 @@ function itemsDiverged(active, items) {
1349
1876
  return false;
1350
1877
  }
1351
1878
  //#endregion
1352
- export { createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, resolveClaudeCodeOAuthAccess, resolveWireLogDir };
1879
+ export { FileClaudeCodeAuthStore, PoolAwareClaudeCodeAuthStore, StaticClaudeCodeAuthStore, createClaudeCodeCredentials, createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, openClaudeCodeCredentialPool, refreshClaudeCodeSecret, resolveClaudeCodeOAuthAccess, resolveWireLogDir, runClaudeCodeLogin };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@demicodes/provider-claude-code",
3
3
  "description": "Claude Code provider adapter for Demi.",
4
- "version": "0.10.3",
4
+ "version": "0.11.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -11,13 +11,13 @@
11
11
  }
12
12
  },
13
13
  "dependencies": {
14
- "@demicodes/core": "^0.10.3",
15
- "@demicodes/provider": "^0.10.3",
16
- "@demicodes/utils": "^0.10.3"
14
+ "@demicodes/core": "^0.11.0",
15
+ "@demicodes/provider": "^0.11.0",
16
+ "@demicodes/utils": "^0.11.0"
17
17
  },
18
18
  "devDependencies": {
19
- "@demicodes/agent": "^0.10.3",
20
- "@demicodes/shell": "^0.10.3"
19
+ "@demicodes/agent": "^0.11.0",
20
+ "@demicodes/shell": "^0.11.0"
21
21
  },
22
22
  "license": "Apache-2.0",
23
23
  "main": "./dist/index.mjs",