@cortexkit/anthropic-auth-core 1.21.0 → 1.22.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.
@@ -0,0 +1,529 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { userInfo } from 'node:os';
3
+ import { SubcCallError, SubcClient, } from '@cortexkit/subc-client';
4
+ import { logger } from './logger';
5
+ export function getDefaultClaustrumConnectionPath() {
6
+ const uid = process.getuid?.() ?? userInfo().uid;
7
+ return `/run/user/${uid}/subc-connection.json`;
8
+ }
9
+ function isRecord(value) {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ }
12
+ function isEndpoint(value) {
13
+ return (isRecord(value) &&
14
+ typeof value.host === 'string' &&
15
+ value.host.trim().length > 0 &&
16
+ typeof value.port === 'number' &&
17
+ Number.isInteger(value.port) &&
18
+ value.port > 0 &&
19
+ value.port <= 65_535);
20
+ }
21
+ export async function detectClaustrumConnection(path = getDefaultClaustrumConnectionPath()) {
22
+ let raw;
23
+ try {
24
+ raw = await readFile(path, 'utf8');
25
+ }
26
+ catch (error) {
27
+ const code = error.code;
28
+ if (code === 'ENOENT')
29
+ return { status: 'absent', path };
30
+ return {
31
+ status: 'malformed',
32
+ path,
33
+ reason: `unreadable (${code ?? 'unknown'})`,
34
+ };
35
+ }
36
+ let value;
37
+ try {
38
+ value = JSON.parse(raw);
39
+ }
40
+ catch {
41
+ return {
42
+ status: 'malformed',
43
+ path,
44
+ reason: 'invalid JSON',
45
+ };
46
+ }
47
+ if (!isRecord(value) ||
48
+ typeof value.schema !== 'number' ||
49
+ !Number.isFinite(value.schema) ||
50
+ typeof value.wire_version !== 'number' ||
51
+ !Number.isFinite(value.wire_version) ||
52
+ !Array.isArray(value.endpoints) ||
53
+ value.endpoints.length === 0 ||
54
+ !value.endpoints.every(isEndpoint)) {
55
+ return {
56
+ status: 'malformed',
57
+ path,
58
+ reason: 'connection file has an invalid schema, wire_version, or endpoints',
59
+ };
60
+ }
61
+ return {
62
+ status: 'available',
63
+ schema: value.schema,
64
+ wireVersion: value.wire_version,
65
+ endpoints: value.endpoints.map((endpoint) => ({
66
+ host: endpoint.host,
67
+ port: endpoint.port,
68
+ })),
69
+ };
70
+ }
71
+ export const CUSTODY_TOMBSTONE_PREFIX = 'claustrum-tombstone:v1:';
72
+ export function custodyTombstoneKey(provider) {
73
+ return `${CUSTODY_TOMBSTONE_PREFIX}${provider}`;
74
+ }
75
+ export function isCustodyTombstoneValue(value) {
76
+ return typeof value === 'string' && value.startsWith(CUSTODY_TOMBSTONE_PREFIX);
77
+ }
78
+ export function isCustodyTombstoneOAuth(auth, provider) {
79
+ if (!isRecord(auth) || auth.type !== 'oauth')
80
+ return false;
81
+ const key = custodyTombstoneKey(provider);
82
+ return auth.refresh === key && auth.access === key;
83
+ }
84
+ export class CustodyTombstoneRefreshError extends Error {
85
+ provider;
86
+ code = 'custody_tombstone_refresh';
87
+ constructor(provider) {
88
+ super(`${provider} main slot is vault-custodied; local refresh is forbidden — the vault-served main path is not yet implemented`);
89
+ this.provider = provider;
90
+ this.name = 'CustodyTombstoneRefreshError';
91
+ }
92
+ }
93
+ export function assertNotCustodyTombstone(refreshToken, provider) {
94
+ if (isCustodyTombstoneValue(refreshToken)) {
95
+ throw new CustodyTombstoneRefreshError(provider);
96
+ }
97
+ }
98
+ const CUSTODY_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
99
+ // The suffix is base64url for 256 CSPRNG bits; its charset is not fixture-derived.
100
+ const CUSTODY_HANDLE_PATTERN = /^ckh_[A-Za-z0-9_-]{43}$/;
101
+ const RESERVED_CUSTODY_IDS = new Set(['__proto__', 'constructor', 'prototype']);
102
+ function isCustodyId(value) {
103
+ return (typeof value === 'string' &&
104
+ CUSTODY_ID_PATTERN.test(value) &&
105
+ !RESERVED_CUSTODY_IDS.has(value));
106
+ }
107
+ export function isValidCustodyHandle(value) {
108
+ return typeof value === 'string' && CUSTODY_HANDLE_PATTERN.test(value);
109
+ }
110
+ export function readCustodyHandles(json, provider) {
111
+ if (!isCustodyId(provider)) {
112
+ throw new Error(`Invalid custody provider id ${provider}`);
113
+ }
114
+ const providers = isRecord(json) && Array.isArray(json.providers) ? json.providers : [];
115
+ const source = providers.find((entry) => isRecord(entry) &&
116
+ Object.hasOwn(entry, 'provider') &&
117
+ entry.provider === provider);
118
+ if (!source || !Array.isArray(source.accounts)) {
119
+ throw new Error(`Missing custody handles for provider ${provider}`);
120
+ }
121
+ return {
122
+ provider: String(source.provider),
123
+ serve: String(source.serve),
124
+ shape: String(source.shape),
125
+ accounts: source.accounts.flatMap((entry) => {
126
+ if (!isRecord(entry))
127
+ return [];
128
+ if (!Object.hasOwn(entry, 'label') ||
129
+ !Object.hasOwn(entry, 'handle') ||
130
+ !Object.hasOwn(entry, 'credential_id') ||
131
+ typeof entry.label !== 'string' ||
132
+ typeof entry.handle !== 'string' ||
133
+ !isCustodyId(entry.label) ||
134
+ !isValidCustodyHandle(entry.handle) ||
135
+ typeof entry.credential_id !== 'string')
136
+ return [];
137
+ return [
138
+ {
139
+ label: entry.label,
140
+ handle: entry.handle,
141
+ credentialId: entry.credential_id,
142
+ },
143
+ ];
144
+ }),
145
+ };
146
+ }
147
+ export class ClaustrumClient {
148
+ #client;
149
+ #connector;
150
+ #connectionFile;
151
+ #handshakeTimeoutMs;
152
+ #reconnecting = null;
153
+ #nextReconnectAt = 0;
154
+ #closed = false;
155
+ constructor(client, connector, connectionFile, handshakeTimeoutMs) {
156
+ this.#client = client;
157
+ this.#connector = connector;
158
+ this.#connectionFile = connectionFile;
159
+ this.#handshakeTimeoutMs = handshakeTimeoutMs;
160
+ }
161
+ static async connect(options = {}) {
162
+ const connectionFile = options.connectionFile ?? getDefaultClaustrumConnectionPath();
163
+ const connector = options.connector ??
164
+ ((connectOptions) => SubcClient.connect(connectOptions));
165
+ const client = await connector({
166
+ connectionFile,
167
+ handshakeTimeoutMs: options.handshakeTimeoutMs,
168
+ });
169
+ return new ClaustrumClient(client, connector, connectionFile, options.handshakeTimeoutMs);
170
+ }
171
+ async catalogList(moduleId) {
172
+ return this.#client.catalogList(moduleId);
173
+ }
174
+ async routeOpen(target, identity, options = {}) {
175
+ // The host process inherits aft's supervised-spawn environment; null is the
176
+ // library contract that omits that identity instead of impersonating aft.
177
+ return this.#client.routeOpen(target, identity, {
178
+ ...options,
179
+ consumerIdentity: null,
180
+ });
181
+ }
182
+ async request(handle, body, options = {}) {
183
+ return this.#client.request(handle, body, options);
184
+ }
185
+ async call(moduleId, method, params, options = {}) {
186
+ try {
187
+ return await this.#client.call(moduleId, method, params, {
188
+ ...options,
189
+ consumerIdentity: null,
190
+ });
191
+ }
192
+ catch (error) {
193
+ if (!this.#shouldReconnect(error))
194
+ throw error;
195
+ await this.#reconnect();
196
+ return this.#client.call(moduleId, method, params, {
197
+ ...options,
198
+ consumerIdentity: null,
199
+ });
200
+ }
201
+ }
202
+ subscribe(handle, body, onEvent, options = {}) {
203
+ return this.#client.subscribe(handle, body, onEvent, options);
204
+ }
205
+ async closeRoute(handle, options = {}) {
206
+ return this.#client.closeRoute(handle, options);
207
+ }
208
+ async closeManagedRoute(target, identity, options = {}) {
209
+ return this.#client.closeManagedRoute(target, identity, {
210
+ ...options,
211
+ consumerIdentity: null,
212
+ });
213
+ }
214
+ close() {
215
+ this.#closed = true;
216
+ this.#client.close();
217
+ }
218
+ #shouldReconnect(error) {
219
+ return (!this.#closed &&
220
+ error instanceof SubcCallError &&
221
+ error.kind === 'terminal' &&
222
+ error.code !== 'missing_identity' &&
223
+ error.code !== 'invalid_control_body');
224
+ }
225
+ async #reconnect() {
226
+ if (this.#closed)
227
+ throw new Error('Claustrum client is closed');
228
+ if (this.#reconnecting) {
229
+ await this.#reconnecting;
230
+ return;
231
+ }
232
+ const now = Date.now();
233
+ if (now < this.#nextReconnectAt) {
234
+ throw new Error('Claustrum client reconnect is backed off');
235
+ }
236
+ this.#nextReconnectAt = now + CLAUSTRUM_CREDENTIAL_REFRESH_BACKOFF_MS;
237
+ this.#reconnecting = this.#connector({
238
+ connectionFile: this.#connectionFile,
239
+ handshakeTimeoutMs: this.#handshakeTimeoutMs,
240
+ })
241
+ .then((client) => {
242
+ if (this.#closed) {
243
+ client.close();
244
+ throw new Error('Claustrum client is closed');
245
+ }
246
+ const previous = this.#client;
247
+ this.#client = client;
248
+ previous.close();
249
+ })
250
+ .finally(() => {
251
+ this.#reconnecting = null;
252
+ });
253
+ await this.#reconnecting;
254
+ }
255
+ }
256
+ export function connectClaustrumClient(options = {}) {
257
+ return ClaustrumClient.connect(options);
258
+ }
259
+ export const CLAUSTRUM_MODULE_ID = 'claustrum';
260
+ export const DEFAULT_CLAUSTRUM_CREDENTIAL_MIN_TTL_MS = 120_000;
261
+ const CLAUSTRUM_CREDENTIAL_REFRESH_BACKOFF_MS = 60_000;
262
+ export const ERROR_CLASS_WIRE_SET = [
263
+ 'transient',
264
+ 'permanent',
265
+ 'auth_required',
266
+ 'context_overflow',
267
+ ];
268
+ export class ClaustrumCredentialError extends Error {
269
+ code;
270
+ action;
271
+ errorClass;
272
+ constructor(message, code, errorClass, action) {
273
+ super(message);
274
+ this.code = code;
275
+ this.action = action;
276
+ this.name = 'ClaustrumCredentialError';
277
+ this.errorClass = errorClass;
278
+ }
279
+ }
280
+ function credentialErrorAction(errorClass) {
281
+ switch (errorClass) {
282
+ case 'permanent':
283
+ return 'gone';
284
+ case 'auth_required':
285
+ return 'reauth';
286
+ case 'context_overflow':
287
+ return 'reduce_and_retry';
288
+ case 'transient':
289
+ return 'retry';
290
+ }
291
+ }
292
+ function asCredentialError(response, fallbackCode = 'invalid_response') {
293
+ const result = isRecord(response) && isRecord(response.result)
294
+ ? response.result
295
+ : undefined;
296
+ const error = result && isRecord(result.error) ? result.error : undefined;
297
+ const rawErrorClass = error?.class;
298
+ const errorClass = typeof rawErrorClass === 'string' &&
299
+ ERROR_CLASS_WIRE_SET.includes(rawErrorClass)
300
+ ? rawErrorClass
301
+ : 'transient';
302
+ if (typeof rawErrorClass !== 'string' ||
303
+ !ERROR_CLASS_WIRE_SET.includes(rawErrorClass)) {
304
+ logger.warn('claustrum', 'unrecognised credential error class', {
305
+ errorClass: rawErrorClass ?? null,
306
+ });
307
+ }
308
+ const code = error && typeof error.code === 'string' ? error.code : fallbackCode;
309
+ return new ClaustrumCredentialError(`Claustrum credential request failed: ${code}`, code, errorClass, credentialErrorAction(errorClass));
310
+ }
311
+ function asCredentialCallError(error) {
312
+ if (error instanceof ClaustrumCredentialError) {
313
+ return error;
314
+ }
315
+ if (error instanceof SubcCallError && error.kind === 'terminal') {
316
+ logger.warn('claustrum', 'terminal credential call failed', {
317
+ code: error.code ?? null,
318
+ message: error.message,
319
+ });
320
+ return new ClaustrumCredentialError(`Claustrum credential request failed: ${error.message}`, error.code ?? 'terminal_error', 'transient', 'retry');
321
+ }
322
+ const message = error instanceof Error ? error.message : String(error);
323
+ const code = isRecord(error) && typeof error.code === 'string'
324
+ ? error.code
325
+ : 'transport_error';
326
+ return new ClaustrumCredentialError(`Claustrum credential request failed: ${message}`, code, 'transient', 'retry');
327
+ }
328
+ function decodeCredentialGetResponse(response) {
329
+ const result = isRecord(response) && isRecord(response.result)
330
+ ? response.result
331
+ : undefined;
332
+ if (result && isRecord(result.error)) {
333
+ throw asCredentialError(response);
334
+ }
335
+ const payload = result?.payload;
336
+ if (!Array.isArray(payload) ||
337
+ payload.length === 0 ||
338
+ !payload.every((value) => typeof value === 'number' &&
339
+ Number.isInteger(value) &&
340
+ value >= 0 &&
341
+ value <= 255)) {
342
+ throw asCredentialError(response);
343
+ }
344
+ const recordVersion = result?.record_version;
345
+ if (typeof recordVersion !== 'number' ||
346
+ !Number.isSafeInteger(recordVersion) ||
347
+ recordVersion < 0) {
348
+ throw asCredentialError(response, 'invalid_record_version');
349
+ }
350
+ const rawExpiresAtMs = result?.expires_at_ms;
351
+ const expiresAtMs = rawExpiresAtMs === null || rawExpiresAtMs === undefined
352
+ ? null
353
+ : typeof rawExpiresAtMs === 'number' && Number.isFinite(rawExpiresAtMs)
354
+ ? rawExpiresAtMs
355
+ : undefined;
356
+ if (expiresAtMs === undefined) {
357
+ throw asCredentialError(response, 'invalid_expiry');
358
+ }
359
+ const decoded = new TextDecoder().decode(Uint8Array.from(payload));
360
+ return {
361
+ payload: decoded,
362
+ expiresAtMs,
363
+ recordVersion,
364
+ ...(typeof result?.project_id === 'string' && {
365
+ projectId: result.project_id,
366
+ }),
367
+ ...(typeof result?.account_id === 'string' && {
368
+ accountId: result.account_id,
369
+ }),
370
+ };
371
+ }
372
+ export class ClaustrumCredentialCache {
373
+ #cache = new Map();
374
+ #inFlight = new Map();
375
+ #client;
376
+ #identity;
377
+ #now;
378
+ #refreshBackoffUntil = new Map();
379
+ #minTtlMs;
380
+ constructor(client, options = {}) {
381
+ this.#client = client;
382
+ this.#identity = options.identity;
383
+ this.#now = options.now ?? Date.now;
384
+ this.#minTtlMs = options.minTtlMs ?? DEFAULT_CLAUSTRUM_CREDENTIAL_MIN_TTL_MS;
385
+ if (!Number.isSafeInteger(this.#minTtlMs) || this.#minTtlMs < 0) {
386
+ throw new RangeError('minTtlMs must be a non-negative safe integer');
387
+ }
388
+ }
389
+ async get(handle, minTtlMs = this.#minTtlMs) {
390
+ if (!Number.isSafeInteger(minTtlMs) || minTtlMs < 0) {
391
+ throw new RangeError('minTtlMs must be a non-negative safe integer');
392
+ }
393
+ const now = this.#now();
394
+ const cached = this.#cache.get(handle);
395
+ if (cached && cached.expiresAtMs !== null && cached.expiresAtMs > now) {
396
+ if (cached.expiresAtMs - now <= minTtlMs) {
397
+ this.#refreshIfApproachingExpiry(handle, now, minTtlMs);
398
+ }
399
+ return cached;
400
+ }
401
+ if (cached) {
402
+ this.#cache.delete(handle);
403
+ this.#refreshBackoffUntil.delete(handle);
404
+ }
405
+ const pending = this.#inFlight.get(handle);
406
+ if (pending)
407
+ return pending;
408
+ const load = this.#load(handle, minTtlMs);
409
+ this.#inFlight.set(handle, load);
410
+ try {
411
+ return await load;
412
+ }
413
+ finally {
414
+ if (this.#inFlight.get(handle) === load)
415
+ this.#inFlight.delete(handle);
416
+ }
417
+ }
418
+ peek(handle) {
419
+ return this.#cache.get(handle);
420
+ }
421
+ seedForTest(handle, credential) {
422
+ this.#cache.set(handle, credential);
423
+ }
424
+ reduceMinTtlMs() {
425
+ this.#minTtlMs = Math.floor(this.#minTtlMs / 2);
426
+ return this.#minTtlMs;
427
+ }
428
+ async reportAuthFailure(handle, providerStatusOrServedCredential, servedCredentialOrStatus, reporterSource) {
429
+ const providerStatus = typeof providerStatusOrServedCredential === 'number'
430
+ ? providerStatusOrServedCredential
431
+ : typeof servedCredentialOrStatus === 'number'
432
+ ? servedCredentialOrStatus
433
+ : 401;
434
+ const servedCredential = typeof providerStatusOrServedCredential === 'number'
435
+ ? typeof servedCredentialOrStatus === 'object' &&
436
+ servedCredentialOrStatus !== null
437
+ ? servedCredentialOrStatus
438
+ : undefined
439
+ : providerStatusOrServedCredential;
440
+ const recordVersion = servedCredential?.recordVersion;
441
+ if (typeof recordVersion !== 'number' ||
442
+ !Number.isSafeInteger(recordVersion) ||
443
+ recordVersion < 0) {
444
+ throw new TypeError('record_version is required from the credential served to the provider');
445
+ }
446
+ try {
447
+ let response;
448
+ try {
449
+ response = await this.#client.call(CLAUSTRUM_MODULE_ID, 'credential.report_auth_failure', {
450
+ handle,
451
+ provider_status: providerStatus,
452
+ record_version: recordVersion,
453
+ ...(reporterSource ? { reporter_source: reporterSource } : {}),
454
+ }, { identity: this.#identity });
455
+ }
456
+ catch (error) {
457
+ throw asCredentialCallError(error);
458
+ }
459
+ const result = isRecord(response) && isRecord(response.result)
460
+ ? response.result
461
+ : undefined;
462
+ if (result && isRecord(result.error)) {
463
+ throw asCredentialError(response);
464
+ }
465
+ }
466
+ finally {
467
+ this.invalidate(handle, recordVersion);
468
+ }
469
+ }
470
+ invalidate(handle, recordVersion) {
471
+ const cached = this.#cache.get(handle);
472
+ if (cached &&
473
+ (recordVersion === undefined || cached.recordVersion === recordVersion)) {
474
+ this.#cache.delete(handle);
475
+ }
476
+ }
477
+ close() {
478
+ this.#client.close();
479
+ }
480
+ #refreshIfApproachingExpiry(handle, now, minTtlMs) {
481
+ if (this.#inFlight.has(handle))
482
+ return;
483
+ const retryAt = this.#refreshBackoffUntil.get(handle);
484
+ if (retryAt !== undefined && retryAt > now)
485
+ return;
486
+ this.#refreshBackoffUntil.set(handle, now + CLAUSTRUM_CREDENTIAL_REFRESH_BACKOFF_MS);
487
+ const load = this.#load(handle, minTtlMs);
488
+ this.#inFlight.set(handle, load);
489
+ void load
490
+ .catch(() => { })
491
+ .finally(() => {
492
+ if (this.#inFlight.get(handle) === load)
493
+ this.#inFlight.delete(handle);
494
+ });
495
+ }
496
+ async #load(handle, minTtlMs) {
497
+ let response;
498
+ try {
499
+ response = await this.#client.call(CLAUSTRUM_MODULE_ID, 'credential.get', {
500
+ handle,
501
+ force_refresh: false,
502
+ min_ttl_ms: minTtlMs,
503
+ }, { identity: this.#identity });
504
+ }
505
+ catch (error) {
506
+ throw asCredentialCallError(error);
507
+ }
508
+ const result = decodeCredentialGetResponse(response);
509
+ const credential = {
510
+ payload: result.payload,
511
+ expiresAtMs: result.expiresAtMs,
512
+ recordVersion: result.recordVersion,
513
+ ...(result.projectId !== undefined && { projectId: result.projectId }),
514
+ ...(result.accountId !== undefined && { accountId: result.accountId }),
515
+ };
516
+ if (credential.expiresAtMs !== null &&
517
+ credential.expiresAtMs > this.#now()) {
518
+ this.#cache.set(handle, credential);
519
+ }
520
+ return credential;
521
+ }
522
+ }
523
+ export async function connectClaustrumCredentialCache(options = {}) {
524
+ if (options.enabled !== true)
525
+ return null;
526
+ const { enabled: _enabled, identity, now, minTtlMs, ...clientOptions } = options;
527
+ const client = await connectClaustrumClient(clientOptions);
528
+ return new ClaustrumCredentialCache(client, { identity, now, minTtlMs });
529
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AccountStorage } from '../accounts.ts';
2
+ import type { ClaustrumDetection } from '../claustrum.ts';
2
3
  export declare const CLAUDE_ACCOUNT_COMMAND_NAME = "claude-account";
3
4
  export type AccountCommandAction = {
4
5
  type: 'status';
@@ -17,6 +18,8 @@ export type AccountCommandAction = {
17
18
  } | {
18
19
  type: 'move-down';
19
20
  id: string;
21
+ } | {
22
+ type: 'reset-backoff';
20
23
  } | {
21
24
  type: 'add-apikey';
22
25
  apiKey: string;
@@ -45,11 +48,12 @@ export declare function buildAccountList(storage: AccountStorage): AccountListIt
45
48
  export declare function executeAccountCommand(input: {
46
49
  argumentsText: string;
47
50
  storage: AccountStorage;
51
+ claustrum?: ClaustrumDetection;
48
52
  }): {
49
53
  text: string;
50
54
  updated?: {
51
55
  id: string;
52
- action: 'enable' | 'disable' | 'remove' | 'reorder';
56
+ action: 'enable' | 'disable' | 'remove' | 'reorder' | 'reset-backoff';
53
57
  enabled?: boolean;
54
58
  previousOrder?: string[];
55
59
  newOrder?: string[];
@@ -1,3 +1,4 @@
1
+ import { isClaustrumEnabledForAccount } from '../accounts.js';
1
2
  import { formatOAuthAccountTier } from '../oauth-profile.js';
2
3
  export const CLAUDE_ACCOUNT_COMMAND_NAME = 'claude-account';
3
4
  export function parseAccountCommandAction(argumentsText) {
@@ -16,6 +17,8 @@ export function parseAccountCommandAction(argumentsText) {
16
17
  return { type: 'move-up', id: rest };
17
18
  if (action === 'move-down' && rest)
18
19
  return { type: 'move-down', id: rest };
20
+ if (action === 'reset-backoff' && !rest)
21
+ return { type: 'reset-backoff' };
19
22
  if (action === 'add-apikey' && rest) {
20
23
  let remaining = rest;
21
24
  let baseURL;
@@ -115,6 +118,7 @@ const USAGE_TEXT = [
115
118
  ' /claude-account remove <id> Remove a fallback account',
116
119
  ' /claude-account move-up <id> Move a fallback account up',
117
120
  ' /claude-account move-down <id> Move a fallback account down',
121
+ ' /claude-account reset-backoff Clear main OAuth refresh and quota backoff',
118
122
  ' /claude-account add-apikey <key> Add an API key fallback account',
119
123
  ' /claude-account add-oauth-start Start OAuth device flow',
120
124
  ' /claude-account add-oauth-finish <code> Complete OAuth flow',
@@ -125,12 +129,16 @@ export function executeAccountCommand(input) {
125
129
  const mainId = 'main';
126
130
  if (action.type === 'status') {
127
131
  const list = buildAccountList(input.storage);
128
- const lines = ['## Claude Accounts', ''];
132
+ const detection = input.claustrum?.status ?? 'unknown';
133
+ const lines = ['## Claude Accounts', '', `- Claustrum: ${detection}`, ''];
129
134
  for (const a of list) {
130
135
  const pct = a.quotaPercent != null ? ` ${Math.round(a.quotaPercent)}%` : '';
131
136
  const status = !a.enabled ? ' (disabled)' : '';
132
137
  const tier = a.tierLabel ? ` · ${a.tierLabel}` : '';
133
- lines.push(`- **${a.label}** [${a.role}]${tier}${status}${pct}`);
138
+ const gate = a.id === mainId
139
+ ? ' · gate n/a (OpenCode managed)'
140
+ : ` · gate ${isClaustrumEnabledForAccount(input.storage, a.id) ? 'on' : 'off'}`;
141
+ lines.push(`- **${a.label}** [${a.role}]${tier}${status}${pct}${gate}`);
134
142
  }
135
143
  lines.push('', USAGE_TEXT);
136
144
  return { text: lines.join('\n') };
@@ -147,6 +155,12 @@ export function executeAccountCommand(input) {
147
155
  if (action.type === 'add-oauth-finish') {
148
156
  return { text: 'add-oauth-finish' };
149
157
  }
158
+ if (action.type === 'reset-backoff') {
159
+ return {
160
+ text: 'Main OAuth refresh and quota backoff cleared.',
161
+ updated: { id: 'main', action: 'reset-backoff' },
162
+ };
163
+ }
150
164
  const id = action.id;
151
165
  if (id === mainId) {
152
166
  return {
@@ -18,12 +18,11 @@ export declare const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's of
18
18
  export declare const PARALLEL_TOOL_CALLS_SYSTEM_PROMPT: string;
19
19
  export declare const CCH_SALT = "59cf53e54c78";
20
20
  export declare const CCH_POSITIONS: number[];
21
- export declare const CLAUDE_CODE_VERSION = "2.1.177";
22
- export declare const CLAUDE_CODE_BUILD_HASH = "3bf";
21
+ export declare const CLAUDE_CODE_VERSION = "2.1.258";
23
22
  export declare const CLAUDE_CODE_ENTRYPOINT = "cli";
24
- export declare const CLAUDE_CODE_STAINLESS_PACKAGE_VERSION = "0.94.0";
25
- export declare const CLAUDE_CODE_STAINLESS_RUNTIME_VERSION = "v24.3.0";
26
- export declare const USER_AGENT = "claude-cli/2.1.177 (external, cli)";
23
+ export declare const CLAUDE_CODE_STAINLESS_PACKAGE_VERSION = "0.112.1";
24
+ export declare const CLAUDE_CODE_STAINLESS_RUNTIME_VERSION = "v26.3.0";
25
+ export declare const USER_AGENT = "claude-cli/2.1.258 (external, cli)";
27
26
  export declare const CACHE_1H_MODES: readonly ['explicit', 'automatic', 'hybrid'];
28
27
  export type Cache1hMode = (typeof CACHE_1H_MODES)[number];
29
28
  export declare const DEFAULT_CACHE_1H_MODE: Cache1hMode;
package/dist/constants.js CHANGED
@@ -46,12 +46,11 @@ export const PARALLEL_TOOL_CALLS_SYSTEM_PROMPT = [
46
46
  ].join('\n');
47
47
  export const CCH_SALT = '59cf53e54c78';
48
48
  export const CCH_POSITIONS = [4, 7, 20];
49
- export const CLAUDE_CODE_VERSION = '2.1.177';
50
- export const CLAUDE_CODE_BUILD_HASH = '3bf';
49
+ export const CLAUDE_CODE_VERSION = '2.1.258';
51
50
  export const CLAUDE_CODE_ENTRYPOINT = 'cli';
52
- export const CLAUDE_CODE_STAINLESS_PACKAGE_VERSION = '0.94.0';
53
- export const CLAUDE_CODE_STAINLESS_RUNTIME_VERSION = 'v24.3.0';
54
- export const USER_AGENT = 'claude-cli/2.1.177 (external, cli)';
51
+ export const CLAUDE_CODE_STAINLESS_PACKAGE_VERSION = '0.112.1';
52
+ export const CLAUDE_CODE_STAINLESS_RUNTIME_VERSION = 'v26.3.0';
53
+ export const USER_AGENT = 'claude-cli/2.1.258 (external, cli)';
55
54
  export const CACHE_1H_MODES = ['explicit', 'automatic', 'hybrid'];
56
55
  export const DEFAULT_CACHE_1H_MODE = 'explicit';
57
56
  /**
package/dist/dump.js CHANGED
@@ -3,6 +3,7 @@ import { lstat, mkdir, readdir, readFile, rename, unlink, writeFile, } from 'nod
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
5
  import { extractBillingHeaderCCH } from './cch.js';
6
+ import { parseJsonRedacted } from './json.js';
6
7
  import { isSecretKey, logger, relayLog } from './logger.js';
7
8
  export const CLAUDE_DUMP_COMMAND_NAME = 'claude-dump';
8
9
  const DUMP_STATUS_TITLE = '## Claude Dump Status';
@@ -425,7 +426,7 @@ function diffSummary(previousBodyText, bodyText) {
425
426
  }
426
427
  function parseBody(bodyText) {
427
428
  try {
428
- const parsed = JSON.parse(bodyText);
429
+ const parsed = parseJsonRedacted(bodyText);
429
430
  return parsed != null &&
430
431
  typeof parsed === 'object' &&
431
432
  !Array.isArray(parsed)
package/dist/index.d.ts CHANGED
@@ -5,13 +5,16 @@ export * from './cachekeep.ts';
5
5
  export * from './cachekeep-registry.ts';
6
6
  export * from './cch.ts';
7
7
  export * from './claude-code.ts';
8
+ export * from './claustrum.ts';
8
9
  export * from './commands/account.ts';
9
10
  export * from './constants.ts';
10
11
  export * from './dump.ts';
11
12
  export * from './fast.ts';
13
+ export * from './json.ts';
12
14
  export * from './killswitch.ts';
13
15
  export * from './logger.ts';
14
16
  export * from './logging.ts';
17
+ export * from './mid-conversation-output-config.ts';
15
18
  export * from './models.ts';
16
19
  export * from './oauth-profile.ts';
17
20
  export * from './pkce.ts';
@@ -25,3 +28,4 @@ export * from './relay.ts';
25
28
  export * from './routing.ts';
26
29
  export * from './start.ts';
27
30
  export * from './sticky-routing.ts';
31
+ export * from './thinking-binding.ts';