@cortexkit/anthropic-auth-core 1.15.1 → 1.17.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,21 @@
1
+ export declare const CACHE_KEEP_REGISTRY_LEASE_MS: number;
2
+ export type CacheKeepTrackedSession = {
3
+ id: string;
4
+ cacheExpiresAt: number;
5
+ nextPrewarmAt: number;
6
+ };
7
+ export declare function getDefaultCacheKeepRegistryDirectory(scope: 'opencode' | 'pi'): string;
8
+ export declare class CacheKeepSessionRegistry {
9
+ private readonly options;
10
+ private readonly filePath;
11
+ private writeChain;
12
+ constructor(options: {
13
+ directory: string;
14
+ now?: () => number;
15
+ leaseMs?: number;
16
+ instanceId?: string;
17
+ });
18
+ publish(sessions: readonly CacheKeepTrackedSession[]): Promise<void>;
19
+ list(localSessions?: readonly CacheKeepTrackedSession[]): Promise<CacheKeepTrackedSession[]>;
20
+ dispose(): Promise<void>;
21
+ }
@@ -0,0 +1,124 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readdir, readFile, rename, rm, writeFile, } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ export const CACHE_KEEP_REGISTRY_LEASE_MS = 3 * 60_000;
6
+ function normalizeSession(value) {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value))
8
+ return;
9
+ const record = value;
10
+ if (typeof record.id !== 'string' ||
11
+ record.id.length === 0 ||
12
+ typeof record.cacheExpiresAt !== 'number' ||
13
+ !Number.isFinite(record.cacheExpiresAt) ||
14
+ typeof record.nextPrewarmAt !== 'number' ||
15
+ !Number.isFinite(record.nextPrewarmAt)) {
16
+ return;
17
+ }
18
+ return {
19
+ id: record.id,
20
+ cacheExpiresAt: record.cacheExpiresAt,
21
+ nextPrewarmAt: record.nextPrewarmAt,
22
+ };
23
+ }
24
+ function normalizeRecord(value) {
25
+ if (!value || typeof value !== 'object' || Array.isArray(value))
26
+ return;
27
+ const record = value;
28
+ if (record.version !== 1 ||
29
+ typeof record.updatedAt !== 'number' ||
30
+ !Number.isFinite(record.updatedAt) ||
31
+ !Array.isArray(record.sessions)) {
32
+ return;
33
+ }
34
+ return {
35
+ version: 1,
36
+ updatedAt: record.updatedAt,
37
+ sessions: record.sessions.flatMap((session) => {
38
+ const normalized = normalizeSession(session);
39
+ return normalized ? [normalized] : [];
40
+ }),
41
+ };
42
+ }
43
+ export function getDefaultCacheKeepRegistryDirectory(scope) {
44
+ return join(tmpdir(), 'opencode-anthropic-auth', 'cachekeep-sessions', scope);
45
+ }
46
+ export class CacheKeepSessionRegistry {
47
+ options;
48
+ filePath;
49
+ writeChain = Promise.resolve();
50
+ constructor(options) {
51
+ this.options = options;
52
+ const instanceId = options.instanceId ?? `${process.pid}-${randomUUID()}`;
53
+ this.filePath = join(options.directory, `${instanceId}.json`);
54
+ }
55
+ publish(sessions) {
56
+ const snapshot = sessions.map((session) => ({ ...session }));
57
+ this.writeChain = this.writeChain
58
+ .catch(() => { })
59
+ .then(async () => {
60
+ if (snapshot.length === 0) {
61
+ await rm(this.filePath, { force: true });
62
+ return;
63
+ }
64
+ await mkdir(this.options.directory, { recursive: true, mode: 0o700 });
65
+ const tempPath = `${this.filePath}.${randomUUID()}.tmp`;
66
+ const record = {
67
+ version: 1,
68
+ updatedAt: this.options.now?.() ?? Date.now(),
69
+ sessions: snapshot,
70
+ };
71
+ await writeFile(tempPath, `${JSON.stringify(record)}\n`, {
72
+ mode: 0o600,
73
+ });
74
+ await rename(tempPath, this.filePath);
75
+ });
76
+ return this.writeChain;
77
+ }
78
+ async list(localSessions = []) {
79
+ await this.writeChain.catch(() => { });
80
+ const now = this.options.now?.() ?? Date.now();
81
+ const leaseMs = this.options.leaseMs ?? CACHE_KEEP_REGISTRY_LEASE_MS;
82
+ let names;
83
+ try {
84
+ names = (await readdir(this.options.directory)).filter((name) => name.endsWith('.json'));
85
+ }
86
+ catch {
87
+ names = [];
88
+ }
89
+ const records = await Promise.all(names.map(async (name) => {
90
+ try {
91
+ const raw = await readFile(join(this.options.directory, name), 'utf8');
92
+ return normalizeRecord(JSON.parse(raw));
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ }));
98
+ const sessions = new Map();
99
+ for (const record of records) {
100
+ if (!record ||
101
+ now - record.updatedAt > leaseMs ||
102
+ record.updatedAt > now) {
103
+ continue;
104
+ }
105
+ for (const session of record.sessions) {
106
+ const existing = sessions.get(session.id);
107
+ if (!existing || session.cacheExpiresAt > existing.cacheExpiresAt) {
108
+ sessions.set(session.id, session);
109
+ }
110
+ }
111
+ }
112
+ for (const session of localSessions) {
113
+ const existing = sessions.get(session.id);
114
+ if (!existing || session.cacheExpiresAt > existing.cacheExpiresAt) {
115
+ sessions.set(session.id, { ...session });
116
+ }
117
+ }
118
+ return [...sessions.values()].sort((left, right) => left.id.localeCompare(right.id));
119
+ }
120
+ async dispose() {
121
+ await this.writeChain.catch(() => { });
122
+ await rm(this.filePath, { force: true });
123
+ }
124
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AccountStorage } from './accounts.ts';
2
+ import type { CacheKeepTrackedSession } from './cachekeep-registry.ts';
2
3
  export declare const CLAUDE_CACHE_KEEP_COMMAND_NAME = "claude-cachekeep";
3
4
  export declare const CACHE_KEEP_TTL_MS: number;
4
5
  export declare const CACHE_KEEP_PREWARM_LEAD_MS: number;
@@ -16,6 +17,8 @@ export type CacheKeepCommandAction = {
16
17
  type: 'status';
17
18
  } | {
18
19
  type: 'disable';
20
+ } | {
21
+ type: 'always';
19
22
  } | {
20
23
  type: 'window';
21
24
  startHour: number;
@@ -28,23 +31,29 @@ export type CacheKeepCommandAction = {
28
31
  };
29
32
  export type CacheKeepStatus = {
30
33
  enabled: boolean;
34
+ always?: boolean;
31
35
  window?: CacheKeepWindow;
32
36
  trackedSessions?: number;
37
+ trackedSessionDetails?: CacheKeepTrackedSession[];
33
38
  nextPrewarmAt?: number;
34
39
  hybridActive?: boolean;
35
40
  };
36
41
  export declare function parseCacheKeepCommandAction(input: string): CacheKeepCommandAction;
37
42
  export declare function getCacheKeepWindow(storage: AccountStorage | null): CacheKeepWindow | undefined;
43
+ export declare function isCacheKeepAlways(storage: AccountStorage | null): boolean;
38
44
  export declare function isCacheKeepPersistentlyEnabled(storage: AccountStorage | null): boolean;
39
45
  export declare function isCacheKeepHybridActive(storage: AccountStorage | null): boolean;
40
46
  export declare function isWithinCacheKeepWindow(window: CacheKeepWindow | undefined, now?: Date): boolean;
47
+ export declare function isCacheKeepActiveNow(storage: AccountStorage | null, now?: Date): boolean;
41
48
  export declare function localDayKey(now?: Date): string;
42
49
  export declare function buildCacheKeepStatusSummary(status: CacheKeepStatus): string;
43
50
  export declare function executeCacheKeepCommand(input: {
44
51
  argumentsText: string;
45
52
  enabled?: boolean;
53
+ always?: boolean;
46
54
  window?: CacheKeepWindow;
47
55
  trackedSessions?: number;
56
+ trackedSessionDetails?: CacheKeepTrackedSession[];
48
57
  nextPrewarmAt?: number;
49
58
  hybridActive?: boolean;
50
59
  }): string;
@@ -86,14 +95,20 @@ export declare class CacheKeepManager {
86
95
  now?: () => number;
87
96
  prewarmTimeoutMs?: number;
88
97
  prepareHeaders?: (headers: Headers, target: CacheKeepTarget) => Promise<Headers> | Headers;
98
+ onTrackedSessionsChanged?: (sessions: readonly CacheKeepTrackedSession[]) => Promise<void> | void;
89
99
  });
90
100
  start(): void;
91
101
  stop(): void;
92
- stats(window?: CacheKeepWindow, now?: number): {
102
+ stats(window?: CacheKeepWindow, now?: number, always?: boolean): {
93
103
  trackedSessions: number;
94
104
  nextPrewarmAt: number | undefined;
95
105
  };
106
+ trackedSessions(): CacheKeepTrackedSession[];
107
+ trackedOAuthRoute(sessionId: string): {
108
+ oauthAccountId?: string;
109
+ } | undefined;
96
110
  trackedCount(): number;
111
+ private publishTrackedSessions;
97
112
  private totalBodyBytes;
98
113
  private evictOldestTarget;
99
114
  private pruneTargets;
package/dist/cachekeep.js CHANGED
@@ -14,7 +14,7 @@ const STATUS_TITLE = '## Claude Cache Keep Status';
14
14
  const ENABLED_TITLE = '## Claude Cache Keep Enabled';
15
15
  const DISABLED_TITLE = '## Claude Cache Keep Disabled';
16
16
  const USAGE_TITLE = '## Claude Cache Keep Usage';
17
- const USAGE = 'Usage: `/claude-cachekeep`, `/claude-cachekeep off`, or `/claude-cachekeep HH-HH`.';
17
+ const USAGE = 'Usage: `/claude-cachekeep`, `/claude-cachekeep always`, `/claude-cachekeep off`, or `/claude-cachekeep HH-HH`.';
18
18
  function padHour(hour) {
19
19
  return String(hour).padStart(2, '0');
20
20
  }
@@ -24,6 +24,8 @@ export function parseCacheKeepCommandAction(input) {
24
24
  return { type: 'status' };
25
25
  if (trimmed === 'off')
26
26
  return { type: 'disable' };
27
+ if (trimmed === 'always')
28
+ return { type: 'always' };
27
29
  if (trimmed === 'subagents on')
28
30
  return { type: 'subagents', enabled: true };
29
31
  if (trimmed === 'subagents off')
@@ -58,8 +60,12 @@ export function getCacheKeepWindow(storage) {
58
60
  }
59
61
  return { startHour, endHour };
60
62
  }
63
+ export function isCacheKeepAlways(storage) {
64
+ return (storage?.cacheKeep?.enabled === true && storage.cacheKeep.always === true);
65
+ }
61
66
  export function isCacheKeepPersistentlyEnabled(storage) {
62
- return storage?.cacheKeep?.enabled === true && !!getCacheKeepWindow(storage);
67
+ return (storage?.cacheKeep?.enabled === true &&
68
+ (isCacheKeepAlways(storage) || !!getCacheKeepWindow(storage)));
63
69
  }
64
70
  export function isCacheKeepHybridActive(storage) {
65
71
  return (isCacheKeepPersistentlyEnabled(storage) &&
@@ -75,6 +81,12 @@ export function isWithinCacheKeepWindow(window, now = new Date()) {
75
81
  }
76
82
  return hour >= window.startHour || hour < window.endHour;
77
83
  }
84
+ export function isCacheKeepActiveNow(storage, now = new Date()) {
85
+ if (storage?.cacheKeep?.enabled !== true)
86
+ return false;
87
+ return (isCacheKeepAlways(storage) ||
88
+ isWithinCacheKeepWindow(getCacheKeepWindow(storage), now));
89
+ }
78
90
  export function localDayKey(now = new Date()) {
79
91
  return `${now.getFullYear()}-${padHour(now.getMonth() + 1)}-${padHour(now.getDate())}`;
80
92
  }
@@ -88,13 +100,18 @@ function localWindowKey(window, now = new Date()) {
88
100
  }
89
101
  return localDayKey(now);
90
102
  }
103
+ function cacheKeepPeriodKey(storage, window, now = new Date()) {
104
+ return isCacheKeepAlways(storage) ? 'always' : localWindowKey(window, now);
105
+ }
91
106
  export function buildCacheKeepStatusSummary(status) {
92
- const window = status.window
93
- ? `${padHour(status.window.startHour)}-${padHour(status.window.endHour)}`
94
- : 'not configured';
107
+ const schedule = status.always
108
+ ? 'always (while this process is running)'
109
+ : status.window
110
+ ? `${padHour(status.window.startHour)}-${padHour(status.window.endHour)}`
111
+ : 'not configured';
95
112
  const lines = [
96
113
  `Cache keep: ${status.enabled ? 'enabled' : 'disabled'}`,
97
- `Window: ${window}`,
114
+ `Schedule: ${schedule}`,
98
115
  'Mode requirement: `/claude-cache mode hybrid` must be active.',
99
116
  ];
100
117
  if (typeof status.hybridActive === 'boolean') {
@@ -103,6 +120,12 @@ export function buildCacheKeepStatusSummary(status) {
103
120
  if (typeof status.trackedSessions === 'number') {
104
121
  lines.push(`Tracked sessions: ${status.trackedSessions}`);
105
122
  }
123
+ if (status.trackedSessionDetails?.length) {
124
+ lines.push('Sessions:');
125
+ for (const session of status.trackedSessionDetails) {
126
+ lines.push(`- ${session.id}`);
127
+ }
128
+ }
106
129
  if (status.nextPrewarmAt) {
107
130
  lines.push(`Next prewarm: ${new Date(status.nextPrewarmAt).toLocaleString()}`);
108
131
  }
@@ -112,8 +135,10 @@ export function executeCacheKeepCommand(input) {
112
135
  const action = parseCacheKeepCommandAction(input.argumentsText);
113
136
  const status = {
114
137
  enabled: input.enabled ?? false,
138
+ always: input.always,
115
139
  window: input.window,
116
140
  trackedSessions: input.trackedSessions,
141
+ trackedSessionDetails: input.trackedSessionDetails,
117
142
  nextPrewarmAt: input.nextPrewarmAt,
118
143
  hybridActive: input.hybridActive,
119
144
  };
@@ -127,6 +152,18 @@ export function executeCacheKeepCommand(input) {
127
152
  buildCacheKeepStatusSummary({ ...status, enabled: false }),
128
153
  ].join('\n');
129
154
  }
155
+ if (action.type === 'always') {
156
+ return [
157
+ ENABLED_TITLE,
158
+ '',
159
+ buildCacheKeepStatusSummary({
160
+ ...status,
161
+ enabled: true,
162
+ always: true,
163
+ window: undefined,
164
+ }),
165
+ ].join('\n');
166
+ }
130
167
  if (action.type === 'window') {
131
168
  return [
132
169
  ENABLED_TITLE,
@@ -134,6 +171,7 @@ export function executeCacheKeepCommand(input) {
134
171
  buildCacheKeepStatusSummary({
135
172
  ...status,
136
173
  enabled: true,
174
+ always: false,
137
175
  window: { startHour: action.startHour, endHour: action.endHour },
138
176
  }),
139
177
  ].join('\n');
@@ -218,17 +256,47 @@ export class CacheKeepManager {
218
256
  clearInterval(this.timer);
219
257
  this.timer = null;
220
258
  }
221
- stats(window, now = this.options.now?.() ?? Date.now()) {
222
- const today = localWindowKey(window, new Date(now));
259
+ stats(window, now = this.options.now?.() ?? Date.now(), always = false) {
260
+ const today = always ? 'always' : localWindowKey(window, new Date(now));
223
261
  const targets = [...this.targets.values()].filter((target) => target.dayKey === today);
224
262
  const nextPrewarmAt = targets.length
225
263
  ? Math.min(...targets.map((target) => target.cacheExpiresAt - CACHE_KEEP_PREWARM_LEAD_MS))
226
264
  : undefined;
227
265
  return { trackedSessions: targets.length, nextPrewarmAt };
228
266
  }
267
+ trackedSessions() {
268
+ return [...this.targets.values()]
269
+ .map((target) => ({
270
+ id: target.id,
271
+ cacheExpiresAt: target.cacheExpiresAt,
272
+ nextPrewarmAt: target.cacheExpiresAt - CACHE_KEEP_PREWARM_LEAD_MS,
273
+ }))
274
+ .sort((left, right) => left.id.localeCompare(right.id));
275
+ }
276
+ trackedOAuthRoute(sessionId) {
277
+ const target = this.targets.get(sessionId);
278
+ return target ? { oauthAccountId: target.oauthAccountId } : undefined;
279
+ }
229
280
  trackedCount() {
230
281
  return this.targets.size;
231
282
  }
283
+ publishTrackedSessions() {
284
+ const callback = this.options.onTrackedSessionsChanged;
285
+ if (!callback)
286
+ return;
287
+ try {
288
+ void Promise.resolve(callback(this.trackedSessions())).catch((error) => {
289
+ logger.warn('cachekeep', 'failed to publish tracked sessions', {
290
+ error: error instanceof Error ? error.message : String(error),
291
+ });
292
+ });
293
+ }
294
+ catch (error) {
295
+ logger.warn('cachekeep', 'failed to publish tracked sessions', {
296
+ error: error instanceof Error ? error.message : String(error),
297
+ });
298
+ }
299
+ }
232
300
  totalBodyBytes() {
233
301
  return [...this.targets.values()].reduce((sum, target) => sum + target.bodyText.length, 0);
234
302
  }
@@ -270,10 +338,10 @@ export class CacheKeepManager {
270
338
  }
271
339
  const now = this.options.now?.() ?? Date.now();
272
340
  const window = getCacheKeepWindow(input.storage);
273
- if (!isWithinCacheKeepWindow(window, new Date(now))) {
274
- return { tracked: false, reason: 'outside configured window' };
341
+ if (!isCacheKeepActiveNow(input.storage, new Date(now))) {
342
+ return { tracked: false, reason: 'outside configured schedule' };
275
343
  }
276
- const today = localWindowKey(window, new Date(now));
344
+ const today = cacheKeepPeriodKey(input.storage, window, new Date(now));
277
345
  this.pruneTargets(now, today);
278
346
  if (this.targets.has(input.sessionId))
279
347
  this.targets.delete(input.sessionId);
@@ -291,6 +359,7 @@ export class CacheKeepManager {
291
359
  oauthAccountId: input.oauthAccountId,
292
360
  });
293
361
  this.pruneTargets(now, today);
362
+ this.publishTrackedSessions();
294
363
  this.start();
295
364
  return { tracked: true };
296
365
  }
@@ -314,16 +383,25 @@ export class CacheKeepManager {
314
383
  const storage = await this.options.loadStorage();
315
384
  const window = getCacheKeepWindow(storage);
316
385
  const now = this.options.now?.() ?? Date.now();
317
- const today = localWindowKey(window, new Date(now));
386
+ const today = cacheKeepPeriodKey(storage, window, new Date(now));
387
+ if (isCacheKeepAlways(storage)) {
388
+ for (const target of this.targets.values())
389
+ target.dayKey = 'always';
390
+ }
318
391
  this.pruneTargets(now, today);
319
392
  if (!this.targets.size) {
393
+ this.publishTrackedSessions();
320
394
  this.stop();
321
395
  return;
322
396
  }
323
- if (!isCacheKeepHybridActive(storage))
397
+ if (!isCacheKeepHybridActive(storage)) {
398
+ this.publishTrackedSessions();
324
399
  return;
325
- if (!isWithinCacheKeepWindow(window, new Date(now)))
400
+ }
401
+ if (!isCacheKeepActiveNow(storage, new Date(now))) {
402
+ this.publishTrackedSessions();
326
403
  return;
404
+ }
327
405
  logger.debug('cachekeep', 'fired', { targets: this.targets.size });
328
406
  const dueAt = now + CACHE_KEEP_PREWARM_LEAD_MS;
329
407
  for (const target of this.targets.values()) {
@@ -331,6 +409,7 @@ export class CacheKeepManager {
331
409
  continue;
332
410
  await this.prewarm(target, now);
333
411
  }
412
+ this.publishTrackedSessions();
334
413
  }
335
414
  async sendPrewarm(target) {
336
415
  const prewarm = await buildCacheKeepPrewarmBody(target.bodyText);
@@ -39,6 +39,7 @@ export interface AccountListItem {
39
39
  role: 'main' | 'fallback';
40
40
  enabled: boolean;
41
41
  quotaPercent: number | null;
42
+ tierLabel?: string;
42
43
  }
43
44
  export declare function buildAccountList(storage: AccountStorage): AccountListItem[];
44
45
  export declare function executeAccountCommand(input: {
@@ -1,3 +1,4 @@
1
+ import { formatOAuthAccountTier } from "../oauth-profile.js";
1
2
  export const CLAUDE_ACCOUNT_COMMAND_NAME = 'claude-account';
2
3
  export function parseAccountCommandAction(argumentsText) {
3
4
  const parts = argumentsText.trim().split(/\s+/).filter(Boolean);
@@ -89,6 +90,7 @@ export function buildAccountList(storage) {
89
90
  role: 'main',
90
91
  enabled: true,
91
92
  quotaPercent: fiveHour?.usedPercent ?? null,
93
+ tierLabel: formatOAuthAccountTier(storage.main?.profile),
92
94
  });
93
95
  for (const account of storage.accounts) {
94
96
  const fiveHourQuota = account.quota?.five_hour;
@@ -98,6 +100,9 @@ export function buildAccountList(storage) {
98
100
  role: 'fallback',
99
101
  enabled: account.enabled !== false,
100
102
  quotaPercent: fiveHourQuota?.usedPercent ?? null,
103
+ tierLabel: account.type === 'oauth'
104
+ ? formatOAuthAccountTier(account.profile)
105
+ : undefined,
101
106
  });
102
107
  }
103
108
  return list;
@@ -124,7 +129,8 @@ export function executeAccountCommand(input) {
124
129
  for (const a of list) {
125
130
  const pct = a.quotaPercent != null ? ` ${Math.round(a.quotaPercent)}%` : '';
126
131
  const status = !a.enabled ? ' (disabled)' : '';
127
- lines.push(`- **${a.label}** [${a.role}]${status}${pct}`);
132
+ const tier = a.tierLabel ? ` · ${a.tierLabel}` : '';
133
+ lines.push(`- **${a.label}** [${a.role}]${tier}${status}${pct}`);
128
134
  }
129
135
  lines.push('', USAGE_TEXT);
130
136
  return { text: lines.join('\n') };
package/dist/dump.d.ts CHANGED
@@ -13,6 +13,25 @@ export declare function isDumpEnabled(): boolean;
13
13
  export declare function setDumpEnabled(enabled: boolean): void;
14
14
  export declare function resetDumpState(): void;
15
15
  export declare function getDumpDirectory(): string;
16
+ export declare function writeDumpFile(path: string, contents: string): Promise<boolean>;
17
+ /**
18
+ * Caps completed dump artifacts in the configured directory. A zero-byte cap
19
+ * disables sweeping. Custom directories may contain unrelated files: only
20
+ * recognized final artifact names and stale partials are eligible. Symlinks,
21
+ * fresh partial writes, and files younger than the newness floor remain
22
+ * protected.
23
+ */
24
+ export declare function sweepDumpDirectory(options: {
25
+ dumpDir?: string;
26
+ maxBytes?: number;
27
+ protectedPaths?: readonly string[];
28
+ now?: number;
29
+ minAgeMs?: number;
30
+ partialStaleMs?: number;
31
+ }): Promise<{
32
+ removed: number;
33
+ freedBytes: number;
34
+ }>;
16
35
  export declare function parseDumpCommandAction(argumentsText: string): DumpCommandAction;
17
36
  export declare function buildDumpStatusSummary(input?: {
18
37
  enabled?: boolean;