@demicodes/provider-claude-code 0.1.0 → 0.2.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,28 @@ 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
+
10
30
  > Diagnostics: the transport writes a raw request/response wire log (including
11
31
  > prompts) to `$TMPDIR/demi-claude-wire` by default. Disable with
12
32
  > `DEMI_CLAUDE_WIRE_LOG=0`. See [SECURITY](../../SECURITY.md).
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { ModelPolicy, Provider, ProviderModelList } 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,60 @@ 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
+ interface ClaudeCodeOAuthAccess {
15
+ accessToken: string;
16
+ subscriptionType?: string | null;
17
+ rateLimitTier?: string | null;
18
+ }
19
+ /**
20
+ * Resolve Claude Code consumer OAuth access for quota APIs.
21
+ * Order: options env CLAUDE_CODE_OAUTH_TOKEN, then macOS Keychain "Claude Code-credentials".
22
+ * Prefer injecting {@link ClaudeCodeAuthStore} when multi-credential is enabled.
23
+ */
24
+ declare function resolveClaudeCodeOAuthAccess(): Promise<ClaudeCodeOAuthAccess | null>;
25
+ //#endregion
26
+ //#region src/auth.d.ts
27
+ interface ClaudeCodeAuthStore {
28
+ status(): Promise<ProviderAuthState>;
29
+ resolveAccess(options?: {
30
+ forceRefresh?: boolean;
31
+ }): Promise<ClaudeCodeOAuthAccess>;
32
+ }
33
+ interface FileClaudeCodeAuthStoreOptions {
34
+ /** Optional path to oauth.json (pool entry). */
35
+ oauthFile?: string;
36
+ /** Prefer this token over env/keychain when set (tests / static). */
37
+ accessToken?: string | null;
38
+ }
39
+ /**
40
+ * Resolves Claude OAuth: explicit token → oauth file → CLAUDE_CODE_OAUTH_TOKEN → keychain.
41
+ */
42
+ declare class FileClaudeCodeAuthStore implements ClaudeCodeAuthStore {
43
+ private readonly oauthFile;
44
+ private readonly accessToken;
45
+ constructor(options?: FileClaudeCodeAuthStoreOptions);
46
+ status(): Promise<ProviderAuthState>;
47
+ resolveAccess(): Promise<ClaudeCodeOAuthAccess>;
48
+ }
49
+ declare class StaticClaudeCodeAuthStore implements ClaudeCodeAuthStore {
50
+ private readonly access;
51
+ constructor(access: ClaudeCodeOAuthAccess);
52
+ status(): Promise<ProviderAuthState>;
53
+ resolveAccess(): Promise<ClaudeCodeOAuthAccess>;
54
+ }
55
+ //#endregion
13
56
  //#region src/provider.d.ts
14
57
  interface ClaudeCodeProviderOptions {
15
58
  id?: string;
16
59
  displayName?: string;
17
60
  claudePath?: string;
18
61
  models?: ModelPolicy;
62
+ /** Demi state root for credential pool (`$DEMI_HOME` / `~/.demi`). */
63
+ stateDir?: string;
64
+ /** When true (default), attach multi-credential pool + global switch. */
65
+ credentials?: boolean;
66
+ authStore?: ClaudeCodeAuthStore;
19
67
  }
20
68
  declare function createClaudeCodeProvider(options?: ClaudeCodeProviderOptions): Provider;
21
69
  //#endregion
@@ -27,4 +75,41 @@ declare function createClaudeCodeProvider(options?: ClaudeCodeProviderOptions):
27
75
  */
28
76
  declare function resolveWireLogDir(): string | null;
29
77
  //#endregion
30
- export { type ClaudeCodeModelCatalogOptions, type ClaudeCodeProviderOptions, createClaudeCodeProvider, listClaudeCodeModels, resolveWireLogDir };
78
+ //#region src/quota.d.ts
79
+ interface ClaudeCodeQuotaOptions {
80
+ providerId?: string;
81
+ /** Override token resolution (tests / custom stores). */
82
+ resolveAccess?: () => Promise<ClaudeCodeOAuthAccess | null>;
83
+ fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
84
+ usageUrl?: string;
85
+ }
86
+ /**
87
+ * Active probe: GET /api/oauth/usage (Claude.ai consumer plan windows).
88
+ * Observation: anthropic-ratelimit-unified-* response headers.
89
+ */
90
+ declare function createClaudeCodeQuota(options?: ClaudeCodeQuotaOptions): ProviderQuota;
91
+ /** Claude CLI stream-json / status envelopes that embed `rate_limits`. */
92
+ declare function observeClaudeStreamBody(body: unknown): ProviderQuotaProbeResult | null;
93
+ declare function mapClaudeUsagePayload(payload: unknown, access?: ClaudeCodeOAuthAccess | null): ProviderQuotaProbeResult;
94
+ /** Map anthropic-ratelimit-unified-* headers into a coarse snapshot. */
95
+ declare function observeClaudeRateLimitHeaders(headers: Headers | undefined): ProviderQuotaProbeResult | null;
96
+ //#endregion
97
+ //#region src/credentials.d.ts
98
+ declare class PoolAwareClaudeCodeAuthStore implements ClaudeCodeAuthStore {
99
+ private readonly pool;
100
+ constructor(pool: FileCredentialPool);
101
+ status(): Promise<import("@demicodes/provider").ProviderAuthState>;
102
+ resolveAccess(): Promise<ClaudeCodeOAuthAccess>;
103
+ private currentStore;
104
+ }
105
+ declare function openClaudeCodeCredentialPool(options?: {
106
+ stateDir?: string;
107
+ }): FileCredentialPool;
108
+ declare function createClaudeCodeCredentials(pool: FileCredentialPool, authStore: ClaudeCodeAuthStore, options?: {
109
+ loginCommand?: string;
110
+ loginArgs?: string[];
111
+ quota?: ProviderQuota | null;
112
+ onActiveChange?: () => void;
113
+ }): ProviderCredentials;
114
+ //#endregion
115
+ export { type ClaudeCodeAuthStore, type ClaudeCodeModelCatalogOptions, type ClaudeCodeOAuthAccess, type ClaudeCodeProviderOptions, type ClaudeCodeQuotaOptions, FileClaudeCodeAuthStore, PoolAwareClaudeCodeAuthStore, StaticClaudeCodeAuthStore, createClaudeCodeCredentials, createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, openClaudeCodeCredentialPool, resolveClaudeCodeOAuthAccess, resolveWireLogDir };
package/dist/index.mjs CHANGED
@@ -1,13 +1,16 @@
1
1
  import { errorMessage, isRecord, nonEmptyString, numberOrNull, stringOrNull } from "@demicodes/utils";
2
- import { randomUUID } from "node:crypto";
3
- import { applyModelPolicy, defineProvider } from "@demicodes/provider";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { applyModelPolicy, clampUsedPercent, createProviderQuota, defineProvider, numberHeader, severityFromUsedPercent, toolResultContentToText, unixSecondsToIso } from "@demicodes/provider";
4
+ import { execFile, spawn } from "node:child_process";
5
+ import { readFile } from "node:fs/promises";
6
+ import { promisify } from "node:util";
7
+ import process$1 from "node:process";
8
+ import { FileCredentialPool, credentialIdFromIdentity, runVendorLoginCommand } from "@demicodes/provider/credentials-pool";
4
9
  import { Buffer as Buffer$1 } from "node:buffer";
5
- import { spawn } from "node:child_process";
6
10
  import { appendFileSync, mkdirSync, statSync } from "node:fs";
7
11
  import { createInterface } from "node:readline";
8
12
  import { tmpdir } from "node:os";
9
13
  import { join } from "node:path";
10
- import process$1 from "node:process";
11
14
  //#region src/models.ts
12
15
  const DEFAULT_MODELS_DEV_URL = "https://models.dev/api.json";
13
16
  const DEFAULT_MINIMUM_MODEL_VERSION = "4.6";
@@ -94,7 +97,8 @@ const CLAUDE_FAMILY_RANK = {
94
97
  haiku: 2
95
98
  };
96
99
  function claudeFamilyRank(id) {
97
- 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;
98
102
  }
99
103
  /** Canonical catalog order: flagship family first (Opus > Sonnet > Haiku > others), newest version first. */
100
104
  function compareClaudeModels(a, b) {
@@ -196,6 +200,235 @@ function reasoningEfforts(value) {
196
200
  return efforts.length > 0 ? efforts : [];
197
201
  }
198
202
  //#endregion
203
+ //#region src/auth.ts
204
+ const execFileAsync = promisify(execFile);
205
+ /**
206
+ * Resolves Claude OAuth: explicit token → oauth file → CLAUDE_CODE_OAUTH_TOKEN → keychain.
207
+ */
208
+ var FileClaudeCodeAuthStore = class {
209
+ oauthFile;
210
+ accessToken;
211
+ constructor(options = {}) {
212
+ this.oauthFile = options.oauthFile ?? null;
213
+ this.accessToken = nonEmptyString(options.accessToken) ?? null;
214
+ }
215
+ async status() {
216
+ try {
217
+ return {
218
+ status: "authenticated",
219
+ accountLabel: nonEmptyString((await this.resolveAccess()).subscriptionType) ?? "Claude Code"
220
+ };
221
+ } catch (error) {
222
+ if (error instanceof ClaudeCodeAuthError && error.code === "auth_missing") return {
223
+ status: "unauthenticated",
224
+ message: error.message
225
+ };
226
+ return {
227
+ status: "error",
228
+ message: error instanceof Error ? error.message : String(error)
229
+ };
230
+ }
231
+ }
232
+ async resolveAccess() {
233
+ if (this.accessToken) return { accessToken: this.accessToken };
234
+ if (this.oauthFile) try {
235
+ const raw = JSON.parse(await readFile(this.oauthFile, "utf8"));
236
+ if (!isRecord(raw)) throw new ClaudeCodeAuthError("auth_invalid", `Invalid OAuth file: ${this.oauthFile}`);
237
+ const accessToken = nonEmptyString(raw.accessToken) ?? nonEmptyString(raw.access_token);
238
+ if (!accessToken) throw new ClaudeCodeAuthError("auth_missing", `No accessToken in ${this.oauthFile}`);
239
+ return {
240
+ accessToken,
241
+ subscriptionType: nonEmptyString(raw.subscriptionType) ?? null,
242
+ rateLimitTier: nonEmptyString(raw.rateLimitTier) ?? null
243
+ };
244
+ } catch (error) {
245
+ if (error instanceof ClaudeCodeAuthError) throw error;
246
+ throw new ClaudeCodeAuthError("auth_missing", `Failed to read Claude OAuth file ${this.oauthFile}: ${error instanceof Error ? error.message : String(error)}`);
247
+ }
248
+ const fromEnv = nonEmptyString(process$1.env.CLAUDE_CODE_OAUTH_TOKEN);
249
+ if (fromEnv) return { accessToken: fromEnv };
250
+ if (process$1.platform === "darwin") try {
251
+ const { stdout } = await execFileAsync("security", [
252
+ "find-generic-password",
253
+ "-s",
254
+ "Claude Code-credentials",
255
+ "-w"
256
+ ], {
257
+ encoding: "utf8",
258
+ timeout: 5e3
259
+ });
260
+ const parsed = JSON.parse(stdout.trim());
261
+ if (!isRecord(parsed)) throw new ClaudeCodeAuthError("auth_missing", "Claude Code keychain item is not a JSON object");
262
+ const oauth = isRecord(parsed.claudeAiOauth) ? parsed.claudeAiOauth : null;
263
+ if (!oauth) throw new ClaudeCodeAuthError("auth_missing", "Claude Code keychain missing claudeAiOauth");
264
+ const accessToken = nonEmptyString(oauth.accessToken);
265
+ if (!accessToken) throw new ClaudeCodeAuthError("auth_missing", "Claude Code keychain missing accessToken");
266
+ return {
267
+ accessToken,
268
+ subscriptionType: nonEmptyString(oauth.subscriptionType) ?? null,
269
+ rateLimitTier: nonEmptyString(oauth.rateLimitTier) ?? null
270
+ };
271
+ } catch (error) {
272
+ if (error instanceof ClaudeCodeAuthError) throw error;
273
+ }
274
+ throw new ClaudeCodeAuthError("auth_missing", "Claude Code OAuth access token not found (set CLAUDE_CODE_OAUTH_TOKEN or log in with Claude Code)");
275
+ }
276
+ };
277
+ var StaticClaudeCodeAuthStore = class {
278
+ access;
279
+ constructor(access) {
280
+ this.access = access;
281
+ }
282
+ async status() {
283
+ return {
284
+ status: "authenticated",
285
+ accountLabel: nonEmptyString(this.access.subscriptionType) ?? "Claude Code"
286
+ };
287
+ }
288
+ async resolveAccess() {
289
+ return this.access;
290
+ }
291
+ };
292
+ var ClaudeCodeAuthError = class extends Error {
293
+ code;
294
+ constructor(code, message) {
295
+ super(message);
296
+ this.code = code;
297
+ this.name = "ClaudeCodeAuthError";
298
+ }
299
+ };
300
+ //#endregion
301
+ //#region src/credentials.ts
302
+ var PoolAwareClaudeCodeAuthStore = class {
303
+ pool;
304
+ constructor(pool) {
305
+ this.pool = pool;
306
+ }
307
+ async status() {
308
+ return this.currentStore().then((s) => s.status());
309
+ }
310
+ async resolveAccess() {
311
+ return this.currentStore().then((s) => s.resolveAccess());
312
+ }
313
+ async currentStore() {
314
+ await this.pool.ensureActivePointer();
315
+ const activeId = await this.pool.getActiveId();
316
+ if (activeId) return new FileClaudeCodeAuthStore({ oauthFile: this.pool.secretPath(activeId) });
317
+ return new FileClaudeCodeAuthStore();
318
+ }
319
+ };
320
+ function openClaudeCodeCredentialPool(options = {}) {
321
+ return new FileCredentialPool({
322
+ stateDir: options.stateDir,
323
+ providerKey: "claude-code",
324
+ secretFileName: "oauth.json"
325
+ });
326
+ }
327
+ function createClaudeCodeCredentials(pool, authStore, options = {}) {
328
+ const loginCommand = options.loginCommand ?? "claude";
329
+ const loginArgs = options.loginArgs ?? ["auth", "login"];
330
+ const capability = () => ({
331
+ mode: "supported",
332
+ canBeginLogin: true,
333
+ canImportDefault: true,
334
+ canAdd: true,
335
+ multi: true
336
+ });
337
+ const getActive = async () => {
338
+ await pool.ensureActivePointer();
339
+ return {
340
+ credentialId: await pool.getActiveId(),
341
+ status: await authStore.status()
342
+ };
343
+ };
344
+ const setActive = async (credentialId) => {
345
+ await pool.setActiveId(credentialId);
346
+ options.quota?.clearLatest?.();
347
+ options.onActiveChange?.();
348
+ return getActive();
349
+ };
350
+ const importAccess = async (access, source) => {
351
+ const token = nonEmptyString(access.accessToken);
352
+ if (!token) throw new ClaudeCodeAuthError("auth_missing", "No Claude access token to import");
353
+ 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)}`;
354
+ const label = nonEmptyString(access.subscriptionType) ?? `claude-${identityKey.slice(-8)}`;
355
+ const id = (await pool.findByIdentityKey(identityKey))?.id ?? credentialIdFromIdentity(identityKey, label);
356
+ const meta = {
357
+ id,
358
+ label,
359
+ detail: nonEmptyString(access.rateLimitTier) ?? null,
360
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
361
+ source,
362
+ identityKey
363
+ };
364
+ const secret = {
365
+ accessToken: token,
366
+ subscriptionType: access.subscriptionType ?? null,
367
+ rateLimitTier: access.rateLimitTier ?? null
368
+ };
369
+ await pool.writeEntry(meta, `${JSON.stringify(secret, null, 2)}\n`);
370
+ if (!await pool.getActiveId()) await pool.setActiveId(id);
371
+ options.quota?.clearLatest?.();
372
+ options.onActiveChange?.();
373
+ return {
374
+ id: meta.id,
375
+ label: meta.label,
376
+ detail: meta.detail,
377
+ updatedAt: meta.updatedAt
378
+ };
379
+ };
380
+ return {
381
+ capability,
382
+ list: () => pool.list(),
383
+ getActive,
384
+ setActive,
385
+ beginLogin: async (loginOptions) => {
386
+ const result = await runVendorLoginCommand(loginCommand, loginArgs, { signal: loginOptions?.signal });
387
+ if (result.status === "completed") return { status: "completed" };
388
+ if (result.status === "cancelled") return { status: "cancelled" };
389
+ if (result.status === "unavailable") return {
390
+ status: "unavailable",
391
+ message: result.message ?? "Login unavailable"
392
+ };
393
+ return {
394
+ status: "failed",
395
+ message: result.message ?? "Login failed"
396
+ };
397
+ },
398
+ importDefault: async () => {
399
+ const vendor = new FileClaudeCodeAuthStore();
400
+ let access;
401
+ try {
402
+ access = await vendor.resolveAccess();
403
+ } catch {
404
+ throw new ClaudeCodeAuthError("auth_missing", "No Claude Code OAuth to import. Run claude auth login or beginLogin first.");
405
+ }
406
+ return importAccess(access, "vendor:default");
407
+ },
408
+ add: async (input) => {
409
+ if (typeof input.accessToken === "string") return importAccess({
410
+ accessToken: input.accessToken,
411
+ subscriptionType: typeof input.subscriptionType === "string" ? input.subscriptionType : null,
412
+ rateLimitTier: typeof input.rateLimitTier === "string" ? input.rateLimitTier : null
413
+ }, "add:accessToken");
414
+ if (isRecord(input.oauth) && typeof input.oauth.accessToken === "string") {
415
+ const oauth = input.oauth;
416
+ return importAccess({
417
+ accessToken: oauth.accessToken,
418
+ subscriptionType: typeof oauth.subscriptionType === "string" ? oauth.subscriptionType : null,
419
+ rateLimitTier: typeof oauth.rateLimitTier === "string" ? oauth.rateLimitTier : null
420
+ }, "add:oauth");
421
+ }
422
+ throw new Error("Claude credentials.add expects accessToken or oauth.accessToken");
423
+ },
424
+ remove: async (credentialId) => {
425
+ await pool.remove(credentialId);
426
+ options.quota?.clearLatest?.();
427
+ options.onActiveChange?.();
428
+ }
429
+ };
430
+ }
431
+ //#endregion
199
432
  //#region src/jsonl.ts
200
433
  /**
201
434
  * Builds the input messages used to prime a *fresh* Claude CLI process with prior
@@ -273,7 +506,7 @@ function renderToolUseText(toolName, input) {
273
506
  return `[Earlier in this conversation I called the tool ${toolName} with input: ${safeJson(input)}.`;
274
507
  }
275
508
  function renderToolResultText(toolName, item) {
276
- const body = toolResultToText(item.output);
509
+ const body = toolResultContentToText(item.output);
277
510
  const suffix = toolName ? ` from ${toolName}` : "";
278
511
  return item.isError ? `It returned an error${suffix}: ${body}]` : `It returned${suffix}: ${body}]`;
279
512
  }
@@ -338,6 +571,10 @@ function userContentToClaude(content) {
338
571
  type: "image",
339
572
  source: imageSourceToClaude(block.source)
340
573
  };
574
+ if (block.type === "video") return {
575
+ type: "text",
576
+ text: "[video]"
577
+ };
341
578
  if (block.type === "document") return documentSourceToClaude(block.source);
342
579
  return {
343
580
  type: "text",
@@ -370,11 +607,30 @@ function assistantItemToClaudeContent(item) {
370
607
  };
371
608
  }
372
609
  function toolResultToClaudeContent(item) {
610
+ const hasImage = item.output.some((block) => block.type !== "text");
373
611
  return {
374
612
  type: "tool_result",
375
613
  tool_use_id: item.toolUseId,
376
614
  is_error: item.isError,
377
- content: toolResultToText(item.output)
615
+ content: hasImage ? item.output.map(toolResultBlockToClaude) : toolResultContentToText(item.output)
616
+ };
617
+ }
618
+ function toolResultBlockToClaude(block) {
619
+ if (block.type === "text") return {
620
+ type: "text",
621
+ text: block.text
622
+ };
623
+ if (block.type === "video") return {
624
+ type: "text",
625
+ text: `[video:${block.source.mediaType}]`
626
+ };
627
+ return {
628
+ type: "image",
629
+ source: {
630
+ type: "base64",
631
+ media_type: block.source.mediaType,
632
+ data: block.source.data
633
+ }
378
634
  };
379
635
  }
380
636
  function imageSourceToClaude(source) {
@@ -399,9 +655,6 @@ function documentSourceToClaude(source) {
399
655
  title: source.fileName
400
656
  };
401
657
  }
402
- function toolResultToText(output) {
403
- return output.map((block) => block.type === "text" ? block.text : `[image:${block.source.mediaType}]`).join("\n");
404
- }
405
658
  function bytesToBase64(data) {
406
659
  return Buffer$1.from(data.buffer, data.byteOffset, data.byteLength).toString("base64");
407
660
  }
@@ -604,6 +857,157 @@ function stripMcpToolPrefix(name) {
604
857
  return /^mcp__[^_]+__(.+)$/.exec(name)?.[1] ?? name;
605
858
  }
606
859
  //#endregion
860
+ //#region src/oauth.ts
861
+ /**
862
+ * Resolve Claude Code consumer OAuth access for quota APIs.
863
+ * Order: options env CLAUDE_CODE_OAUTH_TOKEN, then macOS Keychain "Claude Code-credentials".
864
+ * Prefer injecting {@link ClaudeCodeAuthStore} when multi-credential is enabled.
865
+ */
866
+ async function resolveClaudeCodeOAuthAccess() {
867
+ try {
868
+ return await new FileClaudeCodeAuthStore().resolveAccess();
869
+ } catch {
870
+ return null;
871
+ }
872
+ }
873
+ //#endregion
874
+ //#region src/quota.ts
875
+ const DEFAULT_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
876
+ const DEFAULT_OAUTH_BETA = "oauth-2025-04-20";
877
+ /**
878
+ * Active probe: GET /api/oauth/usage (Claude.ai consumer plan windows).
879
+ * Observation: anthropic-ratelimit-unified-* response headers.
880
+ */
881
+ function createClaudeCodeQuota(options = {}) {
882
+ const providerId = options.providerId ?? "claude-code";
883
+ const fetchImpl = options.fetch ?? fetch;
884
+ const usageUrl = options.usageUrl ?? DEFAULT_USAGE_URL;
885
+ const resolveAccess = options.resolveAccess ?? resolveClaudeCodeOAuthAccess;
886
+ return createProviderQuota({
887
+ providerId,
888
+ canProbe: true,
889
+ canObserve: true,
890
+ probeCost: "free",
891
+ staleAfterMs: 6e4,
892
+ probe: async ({ signal } = {}) => {
893
+ const access = await resolveAccess();
894
+ if (!access?.accessToken) throw new Error("Claude Code OAuth access token not found (set CLAUDE_CODE_OAUTH_TOKEN or log in with Claude Code)");
895
+ const headers = new Headers({
896
+ authorization: `Bearer ${access.accessToken}`,
897
+ "anthropic-beta": DEFAULT_OAUTH_BETA,
898
+ accept: "application/json",
899
+ "user-agent": "demi-provider-claude-code"
900
+ });
901
+ const response = await fetchImpl(usageUrl, {
902
+ method: "GET",
903
+ headers,
904
+ signal
905
+ });
906
+ if (!response.ok) {
907
+ const body = await response.text().catch(() => "");
908
+ throw new Error(`Claude usage request failed (${response.status}): ${body.slice(0, 200)}`);
909
+ }
910
+ return mapClaudeUsagePayload(await response.json(), access);
911
+ },
912
+ observe: ({ headers, body }) => {
913
+ if (headers) {
914
+ const fromHeaders = observeClaudeRateLimitHeaders(headers);
915
+ if (fromHeaders) return fromHeaders;
916
+ }
917
+ if (body !== void 0) return observeClaudeStreamBody(body);
918
+ return null;
919
+ }
920
+ });
921
+ }
922
+ /** Claude CLI stream-json / status envelopes that embed `rate_limits`. */
923
+ function observeClaudeStreamBody(body) {
924
+ if (!isRecord(body)) return null;
925
+ const rateLimits = isRecord(body.rate_limits) ? body.rate_limits : isRecord(body.message) && isRecord(body.message.rate_limits) ? body.message.rate_limits : null;
926
+ if (!rateLimits) return null;
927
+ const partial = mapClaudeUsagePayload(rateLimits);
928
+ return partial.windows.length > 0 ? {
929
+ windows: partial.windows,
930
+ raw: rateLimits
931
+ } : null;
932
+ }
933
+ function mapClaudeUsagePayload(payload, access) {
934
+ const record = isRecord(payload) ? payload : {};
935
+ const windows = [];
936
+ pushWindow(windows, "five_hour", "5h session", record.five_hour);
937
+ pushWindow(windows, "seven_day", "7d all models", record.seven_day);
938
+ pushWindow(windows, "seven_day_sonnet", "7d Sonnet", record.seven_day_sonnet);
939
+ pushWindow(windows, "seven_day_opus", "7d Opus", record.seven_day_opus);
940
+ if (Array.isArray(record.limits)) for (const item of record.limits) {
941
+ if (!isRecord(item)) continue;
942
+ const kind = typeof item.kind === "string" ? item.kind : null;
943
+ if (!kind) continue;
944
+ if (kind === "session" || kind === "weekly_all") continue;
945
+ const percent = clampUsedPercent(typeof item.percent === "number" ? item.percent : null);
946
+ const scopeLabel = isRecord(item.scope) && isRecord(item.scope.model) && typeof item.scope.model.display_name === "string" ? item.scope.model.display_name : void 0;
947
+ windows.push({
948
+ id: `limit:${kind}${scopeLabel ? `:${scopeLabel}` : ""}`,
949
+ label: scopeLabel ? `${kind} (${scopeLabel})` : kind,
950
+ usedPercent: percent,
951
+ unit: "percent",
952
+ resetsAt: unixSecondsToIso(item.resets_at) ?? stringOrNull(item.resets_at),
953
+ severity: item.severity === "critical" || item.severity === "warning" || item.severity === "normal" ? item.severity : severityFromUsedPercent(percent),
954
+ scope: scopeLabel ? {
955
+ kind: "model",
956
+ label: scopeLabel
957
+ } : { kind }
958
+ });
959
+ }
960
+ const planId = access?.subscriptionType ?? null;
961
+ return {
962
+ plan: planId ? {
963
+ id: planId,
964
+ label: planId,
965
+ raw: access?.rateLimitTier ?? planId
966
+ } : null,
967
+ accountLabel: null,
968
+ windows,
969
+ raw: payload
970
+ };
971
+ }
972
+ /** Map anthropic-ratelimit-unified-* headers into a coarse snapshot. */
973
+ function observeClaudeRateLimitHeaders(headers) {
974
+ if (!headers) return null;
975
+ const status = headers.get("anthropic-ratelimit-unified-status");
976
+ const reset = headers.get("anthropic-ratelimit-unified-reset");
977
+ const claim = headers.get("anthropic-ratelimit-unified-representative-claim");
978
+ const overageUtil = headers.get("anthropic-ratelimit-unified-overage-period-channel-utilization");
979
+ if (!status && !reset && !claim && !overageUtil) return null;
980
+ const usedPercent = clampUsedPercent(numberHeader(headers, "anthropic-ratelimit-unified-overage-period-channel-utilization"));
981
+ return {
982
+ windows: [{
983
+ id: "unified",
984
+ label: claim ?? "Unified rate limit",
985
+ usedPercent,
986
+ unit: "percent",
987
+ resetsAt: unixSecondsToIso(reset),
988
+ severity: status === "rejected" || status === "allowed_warning" ? status === "rejected" ? "critical" : "warning" : severityFromUsedPercent(usedPercent)
989
+ }],
990
+ raw: {
991
+ status,
992
+ reset,
993
+ claim,
994
+ overageUtil
995
+ }
996
+ };
997
+ }
998
+ function pushWindow(windows, id, label, value) {
999
+ if (!isRecord(value)) return;
1000
+ const usedPercent = clampUsedPercent(typeof value.utilization === "number" ? value.utilization : typeof value.used_percentage === "number" ? value.used_percentage : null);
1001
+ windows.push({
1002
+ id,
1003
+ label,
1004
+ usedPercent,
1005
+ unit: "percent",
1006
+ resetsAt: unixSecondsToIso(value.resets_at) ?? stringOrNull(value.resets_at),
1007
+ severity: severityFromUsedPercent(usedPercent)
1008
+ });
1009
+ }
1010
+ //#endregion
607
1011
  //#region src/cli.ts
608
1012
  function buildClaudeArgs(params) {
609
1013
  const args = [
@@ -630,13 +1034,15 @@ function buildClaudeArgs(params) {
630
1034
  if (params.thinkingEffort) args.push("--effort", params.thinkingEffort);
631
1035
  return args;
632
1036
  }
633
- function buildClaudeEnv(base = process.env) {
1037
+ function buildClaudeEnv(base = process.env, options = {}) {
634
1038
  const env = {
635
1039
  ...base,
636
1040
  DISABLE_AUTO_COMPACT: "1",
637
1041
  MAX_MCP_OUTPUT_TOKENS: "1000000"
638
1042
  };
639
1043
  delete env.CLAUDECODE;
1044
+ const token = options.oauthAccessToken?.trim();
1045
+ if (token) env.CLAUDE_CODE_OAUTH_TOKEN = token;
640
1046
  return env;
641
1047
  }
642
1048
  //#endregion
@@ -683,13 +1089,19 @@ function resolveSpawnCwd(cwd) {
683
1089
  try {
684
1090
  if (statSync(cwd).isDirectory()) return cwd;
685
1091
  } catch {}
686
- return process.cwd();
1092
+ return process$1.cwd();
687
1093
  }
688
1094
  var ClaudeCliTransportFactory = class {
689
1095
  claudePath;
1096
+ resolveOAuthAccessToken;
690
1097
  constructor(options = {}) {
691
- if (typeof options === "string") this.claudePath = options;
692
- else this.claudePath = options.claudePath ?? "claude";
1098
+ if (typeof options === "string") {
1099
+ this.claudePath = options;
1100
+ this.resolveOAuthAccessToken = null;
1101
+ } else {
1102
+ this.claudePath = options.claudePath ?? "claude";
1103
+ this.resolveOAuthAccessToken = options.resolveOAuthAccessToken ?? null;
1104
+ }
693
1105
  }
694
1106
  async start(request) {
695
1107
  const args = buildClaudeArgsForRequest(request);
@@ -701,9 +1113,10 @@ var ClaudeCliTransportFactory = class {
701
1113
  cwd: request.cwd,
702
1114
  args
703
1115
  });
1116
+ const oauthAccessToken = this.resolveOAuthAccessToken ? await this.resolveOAuthAccessToken() : null;
704
1117
  return new ChildProcessClaudeTransport(spawn(this.claudePath, args, {
705
1118
  cwd: resolveSpawnCwd(request.cwd),
706
- env: buildClaudeEnv(),
1119
+ env: buildClaudeEnv(process$1.env, { oauthAccessToken }),
707
1120
  stdio: [
708
1121
  "pipe",
709
1122
  "pipe",
@@ -791,9 +1204,27 @@ function thinkingEffort(thinking) {
791
1204
  //#region src/provider.ts
792
1205
  var ClaudeCodeProvider = class {
793
1206
  transportFactory;
1207
+ quota;
1208
+ getActiveCredentialId;
794
1209
  active = null;
795
1210
  constructor(options = {}) {
796
- this.transportFactory = options.transportFactory ?? new ClaudeCliTransportFactory({ claudePath: options.claudePath });
1211
+ this.transportFactory = options.transportFactory ?? new ClaudeCliTransportFactory({
1212
+ claudePath: options.claudePath,
1213
+ resolveOAuthAccessToken: options.authStore ? async () => {
1214
+ try {
1215
+ return (await options.authStore.resolveAccess()).accessToken;
1216
+ } catch {
1217
+ return null;
1218
+ }
1219
+ } : void 0
1220
+ });
1221
+ this.quota = options.quota ?? null;
1222
+ this.getActiveCredentialId = options.getActiveCredentialId ?? null;
1223
+ }
1224
+ observeQuotaFromMessage(message) {
1225
+ try {
1226
+ this.quota?.observeResponse?.({ body: message });
1227
+ } catch {}
797
1228
  }
798
1229
  async *run(request) {
799
1230
  let active = null;
@@ -837,6 +1268,7 @@ var ClaudeCodeProvider = class {
837
1268
  return;
838
1269
  }
839
1270
  const raw = next.value;
1271
+ this.observeQuotaFromMessage(raw);
840
1272
  const mapped = mapClaudeStdoutMessage(raw, {
841
1273
  ignoreAssistantContent: active.hasStreamed && isMessageType(raw, "assistant"),
842
1274
  ignoreAssistantToolUse: active.sdkMcpEnabled
@@ -894,17 +1326,18 @@ var ClaudeCodeProvider = class {
894
1326
  * the session changed, or the transcript was rewritten underneath us (compaction).
895
1327
  */
896
1328
  async ensureActiveForRequest(request) {
1329
+ const credentialId = this.getActiveCredentialId ? await this.getActiveCredentialId() : null;
897
1330
  const existing = this.active;
898
- if (existing && existing.sessionId === request.sessionId && existing.modelId === request.modelId && existing.thinkingSig === thinkingSignature(request)) {
1331
+ if (existing && existing.sessionId === request.sessionId && existing.modelId === request.modelId && existing.thinkingSig === thinkingSignature(request) && existing.credentialId === credentialId) {
899
1332
  if (existing.pendingControlRequest !== null || existing.pendingToolUseIds.length > 0 || !itemsDiverged(existing, request.items)) {
900
1333
  await this.sendContinuation(existing, request);
901
1334
  return existing;
902
1335
  }
903
1336
  }
904
1337
  if (existing) await this.disposeActive(existing);
905
- return this.coldStart(request);
1338
+ return this.coldStart(request, credentialId);
906
1339
  }
907
- async coldStart(request) {
1340
+ async coldStart(request, credentialId) {
908
1341
  const transport = await this.transportFactory.start(request);
909
1342
  const active = {
910
1343
  transport,
@@ -917,6 +1350,7 @@ var ClaudeCodeProvider = class {
917
1350
  sessionId: request.sessionId,
918
1351
  modelId: request.modelId,
919
1352
  thinkingSig: thinkingSignature(request),
1353
+ credentialId,
920
1354
  sentUserMessageCount: 0,
921
1355
  firstUserSig: null
922
1356
  };
@@ -1095,14 +1529,32 @@ var ClaudeCodeProvider = class {
1095
1529
  function createClaudeCodeProvider(options = {}) {
1096
1530
  const id = options.id ?? "claude-code";
1097
1531
  const displayName = options.displayName ?? "Claude Code";
1098
- const runtimeOptions = { claudePath: options.claudePath };
1532
+ const enableCredentials = options.credentials ?? options.authStore === void 0;
1533
+ const pool = !options.authStore && enableCredentials ? openClaudeCodeCredentialPool({ stateDir: options.stateDir }) : null;
1534
+ const authStore = options.authStore ?? (pool ? new PoolAwareClaudeCodeAuthStore(pool) : new FileClaudeCodeAuthStore());
1535
+ const quota = createClaudeCodeQuota({
1536
+ providerId: id,
1537
+ resolveAccess: async () => {
1538
+ try {
1539
+ return await authStore.resolveAccess();
1540
+ } catch {
1541
+ return null;
1542
+ }
1543
+ }
1544
+ });
1545
+ const credentialsApi = pool ? createClaudeCodeCredentials(pool, authStore, { quota }) : void 0;
1546
+ const runtimeOptions = {
1547
+ claudePath: options.claudePath,
1548
+ quota,
1549
+ authStore,
1550
+ getActiveCredentialId: pool ? () => pool.getActiveId() : void 0
1551
+ };
1099
1552
  return defineProvider({
1100
1553
  id,
1101
1554
  displayName,
1102
- auth: { status: () => ({
1103
- status: "unknown",
1104
- message: "Auth is checked when a Claude Code request runs"
1105
- }) },
1555
+ auth: { status: () => authStore.status() },
1556
+ quota,
1557
+ ...credentialsApi ? { credentials: credentialsApi } : {},
1106
1558
  state: () => ({
1107
1559
  status: "unknown",
1108
1560
  message: "Runtime is checked when a Claude Code request runs"
@@ -1120,18 +1572,19 @@ function isControlResponseFor(value, requestId) {
1120
1572
  if (!isRecord(value) || value.type !== "control_response" || !isRecord(value.response)) return false;
1121
1573
  return value.response.request_id === requestId && value.response.subtype === "success";
1122
1574
  }
1123
- function toolResultContentToText(output) {
1124
- return output.map((block) => block.type === "text" ? block.text : `[image:${block.source.mediaType}]`).join("\n");
1125
- }
1126
1575
  function toolResultContentToMcp(output) {
1127
1576
  return output.map((block) => {
1128
1577
  if (block.type === "text") return {
1129
1578
  type: "text",
1130
1579
  text: block.text
1131
1580
  };
1581
+ if (block.type === "video") return {
1582
+ type: "text",
1583
+ text: `[video:${block.source.mediaType}]`
1584
+ };
1132
1585
  return {
1133
1586
  type: "image",
1134
- data: Buffer.from(block.source.data).toString("base64"),
1587
+ data: block.source.data,
1135
1588
  mimeType: block.source.mediaType
1136
1589
  };
1137
1590
  });
@@ -1163,4 +1616,4 @@ function itemsDiverged(active, items) {
1163
1616
  return false;
1164
1617
  }
1165
1618
  //#endregion
1166
- export { createClaudeCodeProvider, listClaudeCodeModels, resolveWireLogDir };
1619
+ export { FileClaudeCodeAuthStore, PoolAwareClaudeCodeAuthStore, StaticClaudeCodeAuthStore, createClaudeCodeCredentials, createClaudeCodeProvider, createClaudeCodeQuota, listClaudeCodeModels, mapClaudeUsagePayload, observeClaudeRateLimitHeaders, observeClaudeStreamBody, openClaudeCodeCredentialPool, resolveClaudeCodeOAuthAccess, resolveWireLogDir };
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.1.0",
4
+ "version": "0.2.0",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "exports": {
@@ -12,13 +12,13 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@demicodes/core": "workspace:*",
16
- "@demicodes/provider": "workspace:*",
17
- "@demicodes/utils": "workspace:*"
15
+ "@demicodes/core": "0.2.0",
16
+ "@demicodes/provider": "0.2.0",
17
+ "@demicodes/utils": "0.2.0"
18
18
  },
19
19
  "devDependencies": {
20
- "@demicodes/agent": "workspace:*",
21
- "@demicodes/shell": "workspace:*"
20
+ "@demicodes/agent": "0.2.0",
21
+ "@demicodes/shell": "0.2.0"
22
22
  },
23
23
  "license": "Apache-2.0",
24
24
  "main": "./dist/index.mjs",