dsh-claude-subscription 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/lib/index.js ADDED
@@ -0,0 +1,724 @@
1
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
+ import { LlmError } from "@deepseek-ai/dsh-llm";
3
+ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
4
+ import { settingsNamespace } from "@deepseek-ai/dsh-settings";
5
+ import z from "@deepseek-ai/schemastery";
6
+ import { spawn } from "node:child_process";
7
+ import { anthropicProvider as createAnthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
8
+ import { createModels } from "@earendil-works/pi-ai";
9
+ //#region src/credential-store.js
10
+ const PROVIDER$1 = "anthropic";
11
+ const abortIfNeeded = (options) => options?.signal?.throwIfAborted();
12
+ const clone = (value) => value === void 0 ? void 0 : structuredClone(value);
13
+ function assertProvider(providerId) {
14
+ if (providerId !== PROVIDER$1) throw new Error(`Claude credential store does not own provider ${JSON.stringify(providerId)}`);
15
+ }
16
+ function assertOAuthCredential(value) {
17
+ if (value === void 0) return void 0;
18
+ if (value === null || typeof value !== "object" || value.type !== "oauth" || typeof value.access !== "string" || value.access.length === 0 || typeof value.refresh !== "string" || value.refresh.length === 0 || typeof value.expires !== "number" || !Number.isFinite(value.expires)) throw new Error("Claude credential store received a malformed OAuth credential");
19
+ return clone(value);
20
+ }
21
+ function parseOAuthCredential(value) {
22
+ try {
23
+ return assertOAuthCredential(JSON.parse(value));
24
+ } catch (error) {
25
+ if (error?.message === "Claude credential store received a malformed OAuth credential") throw error;
26
+ throw new Error("Claude credential store contains malformed OAuth JSON", { cause: error });
27
+ }
28
+ }
29
+ /**
30
+ * Adapt DSH's managed string credential service to pi-ai's typed OAuth store.
31
+ * Refresh/login/logout operations are serialized so an older refresh response
32
+ * cannot overwrite a newer rotated token.
33
+ */
34
+ var DshOAuthCredentialStore = class {
35
+ #chains = /* @__PURE__ */ new Map();
36
+ constructor(credentials, ref, legacyRefs = []) {
37
+ if (credentials === void 0 || credentials === null) throw new Error("Claude OAuth requires the DSH credentials service");
38
+ this.credentials = credentials;
39
+ this.ref = ref;
40
+ this.legacyRefs = Object.freeze([...legacyRefs]);
41
+ }
42
+ #enqueue(providerId, operation, options) {
43
+ assertProvider(providerId);
44
+ const current = (this.#chains.get(providerId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
45
+ abortIfNeeded(options);
46
+ return operation();
47
+ });
48
+ const tail = current.catch(() => void 0);
49
+ this.#chains.set(providerId, tail);
50
+ tail.finally(() => {
51
+ if (this.#chains.get(providerId) === tail) this.#chains.delete(providerId);
52
+ });
53
+ return current;
54
+ }
55
+ async read(providerId, options) {
56
+ assertProvider(providerId);
57
+ abortIfNeeded(options);
58
+ let hit = await this.credentials.resolve(this.ref);
59
+ if (hit?.value === void 0 || hit.value === "") for (const legacyRef of this.legacyRefs) {
60
+ const legacy = await this.credentials.resolve(legacyRef);
61
+ if (legacy?.value === void 0 || legacy.value === "") continue;
62
+ const migrated = parseOAuthCredential(legacy.value);
63
+ await this.credentials.set(this.ref, JSON.stringify(migrated));
64
+ await this.credentials.unset(legacyRef);
65
+ hit = { value: JSON.stringify(migrated) };
66
+ break;
67
+ }
68
+ abortIfNeeded(options);
69
+ if (hit?.value === void 0 || hit.value === "") return void 0;
70
+ return parseOAuthCredential(hit.value);
71
+ }
72
+ async list(options) {
73
+ abortIfNeeded(options);
74
+ return await this.read(PROVIDER$1, options) === void 0 ? [] : [{
75
+ providerId: PROVIDER$1,
76
+ type: "oauth"
77
+ }];
78
+ }
79
+ modify(providerId, update, options) {
80
+ return this.#enqueue(providerId, async () => {
81
+ const current = await this.read(providerId, options);
82
+ const next = await update(clone(current));
83
+ abortIfNeeded(options);
84
+ if (next === void 0) return current;
85
+ const validated = assertOAuthCredential(next);
86
+ await this.credentials.set(this.ref, JSON.stringify(validated));
87
+ for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
88
+ abortIfNeeded(options);
89
+ return clone(validated);
90
+ }, options);
91
+ }
92
+ delete(providerId, options) {
93
+ return this.#enqueue(providerId, async () => {
94
+ await this.credentials.unset(this.ref);
95
+ for (const legacyRef of this.legacyRefs) await this.credentials.unset(legacyRef);
96
+ abortIfNeeded(options);
97
+ }, options);
98
+ }
99
+ };
100
+ /** Return only account state that is safe to expose to the browser client. */
101
+ function createClaudeAuthService(models, store) {
102
+ return Object.freeze({
103
+ async status(options) {
104
+ const current = await store.read(PROVIDER$1, options);
105
+ if (current === void 0) return {
106
+ authenticated: false,
107
+ provider: PROVIDER$1
108
+ };
109
+ return {
110
+ authenticated: true,
111
+ provider: PROVIDER$1,
112
+ type: "oauth",
113
+ expiresAt: current.expires
114
+ };
115
+ },
116
+ login(interaction) {
117
+ return models.login(PROVIDER$1, "oauth", interaction);
118
+ },
119
+ logout(options) {
120
+ return models.logout(PROVIDER$1, options);
121
+ }
122
+ });
123
+ }
124
+ //#endregion
125
+ //#region src/external-url.js
126
+ const CLAUDE_AUTH_ORIGIN = "https://claude.ai";
127
+ /** Validate the only external origin this plugin may launch. */
128
+ function assertClaudeAuthUrl(value) {
129
+ let url;
130
+ try {
131
+ url = new URL(value);
132
+ } catch {
133
+ throw new Error("Claude auth URL is invalid");
134
+ }
135
+ if (url.protocol !== "https:") throw new Error("Claude auth URL must use HTTPS");
136
+ if (url.origin !== CLAUDE_AUTH_ORIGIN || url.username !== "" || url.password !== "") throw new Error("Claude auth URL must use the claude.ai origin");
137
+ return url.href;
138
+ }
139
+ /** Return a shell-free native opener command for the current desktop. */
140
+ function commandForClaudeAuthUrl(value, platform = process.platform) {
141
+ const url = assertClaudeAuthUrl(value);
142
+ if (platform === "win32") return {
143
+ file: "rundll32.exe",
144
+ args: ["url.dll,FileProtocolHandler", url],
145
+ shell: false
146
+ };
147
+ if (platform === "darwin") return {
148
+ file: "open",
149
+ args: [url],
150
+ shell: false
151
+ };
152
+ if (platform === "linux") return {
153
+ file: "xdg-open",
154
+ args: [url],
155
+ shell: false
156
+ };
157
+ throw new Error(`Claude auth URL opener is unsupported on ${platform}`);
158
+ }
159
+ function openClaudeAuthUrl(value, options = {}) {
160
+ const command = commandForClaudeAuthUrl(value, options.platform);
161
+ const spawnProcess = options.spawn ?? spawn;
162
+ return new Promise((resolve, reject) => {
163
+ const child = spawnProcess(command.file, command.args, {
164
+ detached: true,
165
+ stdio: "ignore",
166
+ windowsHide: true,
167
+ shell: command.shell
168
+ });
169
+ child.once("error", reject);
170
+ child.once("spawn", () => {
171
+ child.unref();
172
+ resolve();
173
+ });
174
+ });
175
+ }
176
+ //#endregion
177
+ //#region src/login-coordinator.js
178
+ const LOGIN_METHODS = /* @__PURE__ */ new Set(["browser"]);
179
+ const TERMINAL_PHASES = /* @__PURE__ */ new Set([
180
+ "authenticated",
181
+ "failed",
182
+ "cancelled"
183
+ ]);
184
+ const publicClone = (value) => structuredClone(value);
185
+ const asObject = (value) => value !== null && typeof value === "object" ? value : {};
186
+ const ok = (value) => ({
187
+ ok: true,
188
+ value
189
+ });
190
+ const badRequest = (message) => ({
191
+ ok: false,
192
+ error: {
193
+ code: "bad-request",
194
+ message,
195
+ details: { issues: [] }
196
+ }
197
+ });
198
+ const deferred = () => {
199
+ let resolve;
200
+ let reject;
201
+ return {
202
+ promise: new Promise((onResolve, onReject) => {
203
+ resolve = onResolve;
204
+ reject = onReject;
205
+ }),
206
+ resolve,
207
+ reject
208
+ };
209
+ };
210
+ const publicPrompt = (prompt) => ({
211
+ type: prompt.type,
212
+ message: String(prompt.message ?? ""),
213
+ ...typeof prompt.placeholder === "string" ? { placeholder: prompt.placeholder } : {}
214
+ });
215
+ /** Own one host-side login without exposing tokens to the browser client. */
216
+ var ClaudeLoginCoordinator = class {
217
+ #sessions = /* @__PURE__ */ new Map();
218
+ #activeId;
219
+ constructor(auth, options = {}) {
220
+ this.auth = auth;
221
+ this.createId = options.createId ?? (() => crypto.randomUUID());
222
+ }
223
+ async accountStatus(options) {
224
+ return publicClone(await this.auth.status(options));
225
+ }
226
+ async start({ method }) {
227
+ if (!LOGIN_METHODS.has(method)) throw new Error(`unsupported Claude login method: ${String(method)}`);
228
+ const active = this.#activeId === void 0 ? void 0 : this.#sessions.get(this.#activeId);
229
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) throw new Error("a Claude login is already active");
230
+ if (active !== void 0) this.#sessions.delete(active.view.id);
231
+ const id = this.createId();
232
+ const ready = deferred();
233
+ const controller = new AbortController();
234
+ const session = {
235
+ controller,
236
+ prompt: void 0,
237
+ ready,
238
+ view: {
239
+ id,
240
+ provider: "anthropic",
241
+ method,
242
+ phase: "starting",
243
+ authenticated: false
244
+ }
245
+ };
246
+ this.#sessions.set(id, session);
247
+ this.#activeId = id;
248
+ const publishReady = () => ready.resolve(this.read(id));
249
+ const interaction = {
250
+ signal: controller.signal,
251
+ prompt: async (prompt) => {
252
+ controller.signal.throwIfAborted();
253
+ if (prompt.type === "select") return method;
254
+ if (![
255
+ "manual_code",
256
+ "text",
257
+ "secret"
258
+ ].includes(prompt.type)) throw new Error(`unsupported Claude auth prompt: ${String(prompt.type)}`);
259
+ const answer = deferred();
260
+ session.prompt = answer;
261
+ session.view = {
262
+ ...session.view,
263
+ phase: "waiting_input",
264
+ prompt: publicPrompt(prompt)
265
+ };
266
+ const abortPrompt = () => answer.reject(controller.signal.reason ?? /* @__PURE__ */ new Error("login cancelled"));
267
+ controller.signal.addEventListener("abort", abortPrompt, { once: true });
268
+ prompt.signal?.addEventListener("abort", abortPrompt, { once: true });
269
+ publishReady();
270
+ try {
271
+ return await answer.promise;
272
+ } finally {
273
+ controller.signal.removeEventListener("abort", abortPrompt);
274
+ prompt.signal?.removeEventListener("abort", abortPrompt);
275
+ if (session.prompt === answer) session.prompt = void 0;
276
+ }
277
+ },
278
+ notify: (event) => {
279
+ if (controller.signal.aborted) return;
280
+ if (event.type === "auth_url") session.view = {
281
+ ...session.view,
282
+ phase: "waiting_browser",
283
+ authUrl: assertClaudeAuthUrl(event.url),
284
+ ...typeof event.instructions === "string" ? { instructions: event.instructions } : {}
285
+ };
286
+ else session.view = {
287
+ ...session.view,
288
+ message: String(event.message ?? "")
289
+ };
290
+ publishReady();
291
+ }
292
+ };
293
+ session.run = Promise.resolve().then(() => this.auth.login(interaction)).then(async () => {
294
+ if (controller.signal.aborted) return;
295
+ const status = await this.auth.status();
296
+ session.view = {
297
+ id,
298
+ provider: "anthropic",
299
+ method,
300
+ phase: "authenticated",
301
+ authenticated: status.authenticated === true,
302
+ ...typeof status.expiresAt === "number" ? { expiresAt: status.expiresAt } : {}
303
+ };
304
+ }).catch((error) => {
305
+ if (controller.signal.aborted) {
306
+ session.view = {
307
+ id,
308
+ provider: "anthropic",
309
+ method,
310
+ phase: "cancelled",
311
+ authenticated: false
312
+ };
313
+ return;
314
+ }
315
+ session.view = {
316
+ id,
317
+ provider: "anthropic",
318
+ method,
319
+ phase: "failed",
320
+ authenticated: false,
321
+ error: "Claude login failed"
322
+ };
323
+ session.hostError = error;
324
+ }).finally(publishReady);
325
+ return ready.promise;
326
+ }
327
+ read(id) {
328
+ const session = this.#sessions.get(id);
329
+ if (session === void 0) throw new Error("unknown Claude login");
330
+ return publicClone(session.view);
331
+ }
332
+ async submit({ id, value }) {
333
+ const session = this.#sessions.get(id);
334
+ if (session === void 0) throw new Error("unknown Claude login");
335
+ if (session.prompt === void 0 || session.view.phase !== "waiting_input") throw new Error("Claude login is not waiting for input");
336
+ if (typeof value !== "string" || value.trim() === "") throw new Error("Claude login input is empty");
337
+ const answer = session.prompt;
338
+ session.prompt = void 0;
339
+ session.view = {
340
+ ...session.view,
341
+ phase: session.view.authUrl === void 0 ? "starting" : "waiting_browser",
342
+ prompt: void 0
343
+ };
344
+ answer.resolve(value);
345
+ return this.read(id);
346
+ }
347
+ async cancel(id) {
348
+ const session = this.#sessions.get(id);
349
+ if (session === void 0) throw new Error("unknown Claude login");
350
+ if (!TERMINAL_PHASES.has(session.view.phase)) {
351
+ session.view = {
352
+ id,
353
+ provider: "anthropic",
354
+ method: session.view.method,
355
+ phase: "cancelled",
356
+ authenticated: false
357
+ };
358
+ session.controller.abort(/* @__PURE__ */ new Error("Claude login cancelled"));
359
+ }
360
+ await Promise.resolve(session.run).catch(() => void 0);
361
+ return this.read(id);
362
+ }
363
+ async logout(options) {
364
+ if (this.#activeId !== void 0) {
365
+ const active = this.#sessions.get(this.#activeId);
366
+ if (active !== void 0 && !TERMINAL_PHASES.has(active.view.phase)) await this.cancel(active.view.id);
367
+ }
368
+ await this.auth.logout(options);
369
+ return this.accountStatus(options);
370
+ }
371
+ };
372
+ /** Map the loopback-only DSH Connection channel onto the coordinator. */
373
+ function createClaudeRpcHandler(coordinator, options = {}) {
374
+ const openExternal = options.openExternal;
375
+ return async (endpoint, payload, signal) => {
376
+ try {
377
+ signal.throwIfAborted();
378
+ const input = asObject(payload);
379
+ if (endpoint === "status") return ok(await coordinator.accountStatus({ signal }));
380
+ if (endpoint === "login/start") {
381
+ const started = await coordinator.start({ method: input.method });
382
+ if (input.openExternal !== true) return ok(started);
383
+ const url = started.authUrl;
384
+ if (typeof url !== "string" || openExternal === void 0) return ok({
385
+ ...started,
386
+ externalOpened: false
387
+ });
388
+ try {
389
+ await openExternal(url);
390
+ return ok({
391
+ ...started,
392
+ externalOpened: true
393
+ });
394
+ } catch {
395
+ return ok({
396
+ ...started,
397
+ externalOpened: false
398
+ });
399
+ }
400
+ }
401
+ if (endpoint === "login/status") return ok(coordinator.read(input.id));
402
+ if (endpoint === "login/submit") return ok(await coordinator.submit({
403
+ id: input.id,
404
+ value: input.value
405
+ }));
406
+ if (endpoint === "login/cancel") return ok(await coordinator.cancel(input.id));
407
+ if (endpoint === "logout") return ok(await coordinator.logout({ signal }));
408
+ return badRequest(`unknown Claude auth endpoint: ${endpoint}`);
409
+ } catch (error) {
410
+ if (signal.aborted) throw error;
411
+ const message = error instanceof Error && /^(unknown|unsupported|a Claude|Claude login)/.test(error.message) ? error.message : "Claude request failed";
412
+ return badRequest(message);
413
+ }
414
+ };
415
+ }
416
+ //#endregion
417
+ //#region src/pi-ai-runtime.js
418
+ /**
419
+ * Preserve pi-ai's native Anthropic OAuth provider (Claude Pro/Max) while
420
+ * allowing DSH's generic PiAiAdapter to pass the access token resolved by the
421
+ * host credential store.
422
+ *
423
+ * PiAiAdapter owns a request-local Models collection without a credential
424
+ * store. A pure OAuth provider ignores its `apiKey` request override and fails
425
+ * before dispatch with "Provider is not configured". This non-interactive
426
+ * bridge teaches that collection how to consume only the already-refreshed
427
+ * token for this request; login, refresh, persistence, headers, transport, and
428
+ * model behavior remain owned by the original provider.
429
+ */
430
+ function anthropicSubscriptionProvider() {
431
+ const provider = createAnthropicProvider();
432
+ const requestToken = Object.freeze({
433
+ name: "DSH-managed Anthropic OAuth request token",
434
+ async resolve({ credential }) {
435
+ const token = credential?.type === "api_key" ? credential.key : void 0;
436
+ if (typeof token !== "string" || token.length === 0) return void 0;
437
+ return {
438
+ auth: { apiKey: token },
439
+ source: "DSH-managed OAuth request"
440
+ };
441
+ }
442
+ });
443
+ return Object.freeze({
444
+ ...provider,
445
+ auth: Object.freeze({
446
+ ...provider.auth,
447
+ apiKey: requestToken
448
+ })
449
+ });
450
+ }
451
+ const PI_AI_RUNTIME_VERSION = "0.82.1";
452
+ //#endregion
453
+ //#region src/settings-contract.js
454
+ const SETTINGS_NAMESPACE = "claude-subscription";
455
+ const QUICK_QUOTA_FIELD = "quickQuotaVisible";
456
+ Object.freeze({ [QUICK_QUOTA_FIELD]: false });
457
+ //#endregion
458
+ //#region src/usage.js
459
+ const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
460
+ const DEFAULT_TTL_MS = 6e4;
461
+ const DEFAULT_TIMEOUT_MS = 15e3;
462
+ const OAUTH_BETA = "oauth-2025-04-20";
463
+ const WINDOW_IDS = Object.freeze([
464
+ "five_hour",
465
+ "seven_day",
466
+ "seven_day_opus",
467
+ "seven_day_sonnet"
468
+ ]);
469
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
470
+ function epochSecondsOf(value) {
471
+ if (typeof value !== "string" || value.length === 0) return void 0;
472
+ const parsed = Date.parse(value);
473
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : void 0;
474
+ }
475
+ function windowOf(id, value) {
476
+ if (value === void 0 || value === null) return void 0;
477
+ if (!record(value)) throw new Error("Claude returned a malformed usage window");
478
+ const utilization = value.utilization;
479
+ if (!Number.isFinite(utilization) || utilization < 0 || utilization > 100) throw new Error("Claude returned an invalid utilization percentage");
480
+ const resetsAt = epochSecondsOf(value.resets_at);
481
+ return {
482
+ id,
483
+ remainingPercent: 100 - utilization,
484
+ ...resetsAt === void 0 ? {} : { resetsAt }
485
+ };
486
+ }
487
+ function decimalOf(value, label) {
488
+ if (value === void 0 || value === null) return void 0;
489
+ if (!Number.isFinite(value) || value < 0) throw new Error(`Claude returned an invalid ${label}`);
490
+ return value;
491
+ }
492
+ function extraUsageOf(value) {
493
+ if (value === void 0 || value === null) return void 0;
494
+ if (!record(value)) throw new Error("Claude returned malformed extra usage details");
495
+ if (typeof value.is_enabled !== "boolean") return void 0;
496
+ const monthlyLimit = decimalOf(value.monthly_limit, "monthly credit limit");
497
+ const usedCredits = decimalOf(value.used_credits, "used credit amount");
498
+ const utilization = decimalOf(value.utilization, "extra usage utilization");
499
+ return {
500
+ enabled: value.is_enabled,
501
+ ...monthlyLimit === void 0 ? {} : { monthlyLimit },
502
+ ...usedCredits === void 0 ? {} : { usedCredits },
503
+ ...utilization === void 0 ? {} : { utilization }
504
+ };
505
+ }
506
+ /** Reduce the provider payload to a browser-safe quota projection. */
507
+ function parseClaudeUsage(value) {
508
+ if (!record(value)) throw new Error("Claude returned a malformed usage response");
509
+ const windows = WINDOW_IDS.map((id) => windowOf(id, value[id])).filter((entry) => entry !== void 0);
510
+ const extraUsage = extraUsageOf(value.extra_usage);
511
+ return {
512
+ windows,
513
+ ...extraUsage === void 0 ? {} : { extraUsage }
514
+ };
515
+ }
516
+ const requestSignal = (signal, timeoutMs) => {
517
+ const timeout = AbortSignal.timeout(timeoutMs);
518
+ return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
519
+ };
520
+ function messageForStatus(status) {
521
+ if (status === 401 || status === 403) return "Claude sign-in needs to be renewed";
522
+ if (status === 429) return "Claude usage requests are rate limited. Try again later";
523
+ return `Claude usage request failed (HTTP ${status})`;
524
+ }
525
+ /**
526
+ * Read quota through the same refreshable OAuth lifecycle used by model turns.
527
+ * The browser receives only a parsed quota projection; the bearer token is a
528
+ * request-local host value. Concurrent polls share one request.
529
+ */
530
+ function createClaudeUsageReader(options) {
531
+ const getAuth = options.getAuth;
532
+ const fetchUsage = options.fetch ?? fetch;
533
+ const now = options.now ?? Date.now;
534
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
535
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
536
+ const userAgent = options.userAgent ?? "dsh-claude-subscription/0.2.0";
537
+ let cached;
538
+ let inFlight;
539
+ const load = async (signal) => {
540
+ const access = (await getAuth({ signal }))?.auth?.apiKey;
541
+ if (typeof access !== "string" || access.length === 0) throw new Error("Claude subscription is not signed in");
542
+ const response = await fetchUsage(CLAUDE_USAGE_URL, {
543
+ method: "GET",
544
+ redirect: "error",
545
+ headers: {
546
+ authorization: `Bearer ${access}`,
547
+ "anthropic-beta": OAUTH_BETA,
548
+ accept: "application/json",
549
+ "cache-control": "no-store",
550
+ "user-agent": userAgent
551
+ },
552
+ signal: requestSignal(signal, timeoutMs)
553
+ });
554
+ if (!response.ok) throw new Error(messageForStatus(response.status));
555
+ let value;
556
+ try {
557
+ value = await response.json();
558
+ } catch {
559
+ throw new Error("Claude returned an unreadable usage response");
560
+ }
561
+ return {
562
+ ...parseClaudeUsage(value),
563
+ fetchedAt: now()
564
+ };
565
+ };
566
+ return Object.freeze({
567
+ read({ force = false, signal } = {}) {
568
+ if (!force && cached !== void 0 && now() - cached.fetchedAt < ttlMs) return Promise.resolve(structuredClone(cached));
569
+ if (inFlight !== void 0) return inFlight.then(structuredClone);
570
+ const current = load(signal).then((value) => {
571
+ cached = structuredClone(value);
572
+ return structuredClone(value);
573
+ }).finally(() => {
574
+ if (inFlight === current) inFlight = void 0;
575
+ });
576
+ inFlight = current;
577
+ return current;
578
+ },
579
+ clear() {
580
+ cached = void 0;
581
+ }
582
+ });
583
+ }
584
+ //#endregion
585
+ //#region src/composer-quota.js
586
+ const SHARED_WINDOW_IDS = Object.freeze(["five_hour", "seven_day"]);
587
+ const MODEL_WINDOW_IDS = Object.freeze({
588
+ opus: "seven_day_opus",
589
+ sonnet: "seven_day_sonnet"
590
+ });
591
+ const isDisplayableWindow = (window) => Number.isFinite(window?.remainingPercent) && window.remainingPercent >= 0 && window.remainingPercent <= 100;
592
+ const normalized = (value) => String(value ?? "").toLocaleLowerCase("en-US");
593
+ function applicableIds(model) {
594
+ const name = normalized(model);
595
+ const family = Object.entries(MODEL_WINDOW_IDS).find(([key]) => name.includes(key));
596
+ return family === void 0 ? SHARED_WINDOW_IDS : [...SHARED_WINDOW_IDS, family[1]];
597
+ }
598
+ /** Pick the tightest quota window that constrains the given model. */
599
+ function selectModelQuota(usage, model) {
600
+ const ids = applicableIds(model);
601
+ const windows = Array.isArray(usage?.windows) ? usage.windows.filter((window) => ids.includes(window?.id) && isDisplayableWindow(window)) : [];
602
+ if (windows.length === 0) return void 0;
603
+ const selected = windows.reduce((lowest, candidate) => candidate.remainingPercent < lowest.remainingPercent ? candidate : lowest);
604
+ return {
605
+ id: selected.id,
606
+ remainingPercent: selected.remainingPercent,
607
+ ...Number.isSafeInteger(selected.resetsAt) ? { resetsAt: selected.resetsAt } : {}
608
+ };
609
+ }
610
+ //#endregion
611
+ //#region src/index.js
612
+ const name = "claude-subscription";
613
+ const inject = [
614
+ "llm",
615
+ "credentials",
616
+ "connection",
617
+ "settings"
618
+ ];
619
+ const PROVIDER = "anthropic";
620
+ const CREDENTIAL_REF = credentialRef("ANTHROPIC_SUBSCRIPTION_OAUTH");
621
+ const CHANNEL = "/claude-subscription";
622
+ const publicError = (code, message) => ({
623
+ ok: false,
624
+ error: {
625
+ code,
626
+ message,
627
+ details: { issues: [] }
628
+ }
629
+ });
630
+ const KNOWN_USAGE_ERRORS = /* @__PURE__ */ new Set([
631
+ "Claude subscription is not signed in",
632
+ "Claude sign-in needs to be renewed",
633
+ "Claude usage requests are rate limited. Try again later"
634
+ ]);
635
+ /**
636
+ * Extend the account RPC with the preference toggle and the quota projection.
637
+ * Unknown endpoints stay owned by the login coordinator.
638
+ */
639
+ function createSubscriptionRpcHandler({ authHandler, usageReader, preferences }) {
640
+ return async (endpoint, payload, signal) => {
641
+ if (endpoint === "preferences/status" || endpoint === "preferences/update") try {
642
+ signal.throwIfAborted();
643
+ if (endpoint === "preferences/update") {
644
+ if (!Object.hasOwn(payload ?? {}, "quickQuotaVisible")) return publicError("internal", "Invalid preference update");
645
+ if (typeof payload["quickQuotaVisible"] !== "boolean") return publicError("internal", "Invalid quick quota preference");
646
+ await preferences.update({ [QUICK_QUOTA_FIELD]: payload[QUICK_QUOTA_FIELD] });
647
+ }
648
+ return {
649
+ ok: true,
650
+ value: preferences.status()
651
+ };
652
+ } catch (error) {
653
+ if (signal.aborted) throw error;
654
+ return publicError("internal", "Could not update preferences");
655
+ }
656
+ if (endpoint === "usage") try {
657
+ signal.throwIfAborted();
658
+ return {
659
+ ok: true,
660
+ value: await usageReader.read({
661
+ force: payload?.force === true,
662
+ signal
663
+ })
664
+ };
665
+ } catch (error) {
666
+ if (signal.aborted) throw error;
667
+ const message = error instanceof Error && KNOWN_USAGE_ERRORS.has(error.message) ? error.message : "Could not read Claude usage";
668
+ return publicError("internal", message);
669
+ }
670
+ const result = await authHandler(endpoint, payload, signal);
671
+ if (endpoint === "logout" && result.ok === true) usageReader.clear();
672
+ return result;
673
+ };
674
+ }
675
+ function apply(ctx) {
676
+ const settings = ctx.settings.register(settingsNamespace(SETTINGS_NAMESPACE), z.object({ [QUICK_QUOTA_FIELD]: z.boolean().default(false) }));
677
+ const preferences = {
678
+ status: () => ({
679
+ [QUICK_QUOTA_FIELD]: settings.get()[QUICK_QUOTA_FIELD],
680
+ writable: ctx.settings.writable
681
+ }),
682
+ update: (patch) => settings.update(patch)
683
+ };
684
+ const store = new DshOAuthCredentialStore(ctx.credentials, CREDENTIAL_REF);
685
+ const provider = anthropicSubscriptionProvider();
686
+ const authModels = createModels({ credentials: store });
687
+ authModels.setProvider(provider);
688
+ const profile = Object.freeze({
689
+ provider: PROVIDER,
690
+ displayName: "Claude subscription",
691
+ piProvider: provider,
692
+ configuredMaxTokens: /* @__PURE__ */ new Map(),
693
+ streamIdleTimeoutMs: 600 * 1e3,
694
+ cacheRetention: "short",
695
+ transport: "sse"
696
+ });
697
+ const profiles = /* @__PURE__ */ new Map([[PROVIDER, profile]]);
698
+ const resolveAuth = () => authModels.getAuth(PROVIDER);
699
+ const adapter = new PiAiAdapter({
700
+ profiles: () => profiles,
701
+ resolveApiKey: async () => {
702
+ let resolved;
703
+ try {
704
+ resolved = await resolveAuth();
705
+ } catch {
706
+ throw new LlmError("Claude subscription authorization failed", "AUTH_FAILED");
707
+ }
708
+ if (typeof resolved?.auth.apiKey !== "string" || resolved.auth.apiKey.length === 0) throw new LlmError("Claude subscription is not signed in", "MISSING_CREDENTIAL");
709
+ return resolved.auth.apiKey;
710
+ },
711
+ resolveAttachments: () => void 0
712
+ });
713
+ ctx.llm.registerAdapter([PROVIDER], adapter);
714
+ const coordinator = new ClaudeLoginCoordinator(createClaudeAuthService(authModels, store));
715
+ const usageReader = createClaudeUsageReader({ getAuth: resolveAuth });
716
+ const handler = createSubscriptionRpcHandler({
717
+ authHandler: createClaudeRpcHandler(coordinator, { openExternal: openClaudeAuthUrl }),
718
+ usageReader,
719
+ preferences
720
+ });
721
+ ctx.effect(() => ctx.connection.rpc.handle(CHANNEL, handler, { authority: "loopback" }), "claude-subscription: loopback account RPC");
722
+ }
723
+ //#endregion
724
+ export { CLAUDE_USAGE_URL, ClaudeLoginCoordinator, DshOAuthCredentialStore, PI_AI_RUNTIME_VERSION, anthropicSubscriptionProvider, apply, assertClaudeAuthUrl, createClaudeAuthService, createClaudeRpcHandler, createClaudeUsageReader, createSubscriptionRpcHandler, inject, name, openClaudeAuthUrl, parseClaudeUsage, selectModelQuota };