@leo000001/opencode-quota-sidebar 3.0.9 → 4.0.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/README.md +157 -42
  3. package/README.zh-CN.md +157 -42
  4. package/SECURITY.md +1 -1
  5. package/dist/cli.d.ts +18 -0
  6. package/dist/cli.js +354 -0
  7. package/dist/cli_render.d.ts +17 -0
  8. package/dist/cli_render.js +292 -0
  9. package/dist/events.d.ts +1 -1
  10. package/dist/events.js +2 -2
  11. package/dist/format.d.ts +4 -0
  12. package/dist/format.js +302 -41
  13. package/dist/history_messages.d.ts +8 -0
  14. package/dist/history_messages.js +157 -0
  15. package/dist/history_usage.d.ts +93 -0
  16. package/dist/history_usage.js +251 -0
  17. package/dist/index.js +29 -4
  18. package/dist/period.d.ts +29 -1
  19. package/dist/period.js +187 -9
  20. package/dist/provider_catalog.d.ts +8 -0
  21. package/dist/provider_catalog.js +68 -0
  22. package/dist/providers/core/anthropic.d.ts +1 -1
  23. package/dist/providers/core/anthropic.js +69 -45
  24. package/dist/providers/core/openai.js +101 -8
  25. package/dist/providers/index.d.ts +1 -2
  26. package/dist/providers/index.js +1 -3
  27. package/dist/quota.d.ts +4 -2
  28. package/dist/quota.js +18 -21
  29. package/dist/quota_render.d.ts +1 -1
  30. package/dist/quota_render.js +23 -24
  31. package/dist/quota_service.d.ts +1 -0
  32. package/dist/quota_service.js +151 -19
  33. package/dist/storage.d.ts +1 -1
  34. package/dist/storage.js +4 -4
  35. package/dist/storage_dates.d.ts +1 -1
  36. package/dist/storage_dates.js +8 -5
  37. package/dist/storage_parse.js +23 -1
  38. package/dist/supported_quota.d.ts +4 -0
  39. package/dist/supported_quota.js +36 -0
  40. package/dist/title.js +18 -8
  41. package/dist/tools.d.ts +14 -3
  42. package/dist/tools.js +54 -2
  43. package/dist/tui.tsx +17 -6
  44. package/dist/tui_helpers.js +11 -6
  45. package/dist/types.d.ts +8 -0
  46. package/dist/usage.d.ts +18 -0
  47. package/dist/usage.js +93 -9
  48. package/dist/usage_service.d.ts +4 -1
  49. package/dist/usage_service.js +193 -189
  50. package/package.json +4 -1
  51. package/quota-sidebar.config.example.json +36 -45
  52. package/dist/providers/third_party/xyai.d.ts +0 -2
  53. package/dist/providers/third_party/xyai.js +0 -348
@@ -3,15 +3,27 @@ import { TtlValueCache } from './cache.js';
3
3
  import { isRecord, swallow } from './helpers.js';
4
4
  import { listDefaultQuotaProviderIDs, loadAuthMap, quotaSort } from './quota.js';
5
5
  export function createQuotaService(deps) {
6
- const ERROR_CACHE_TTL_MS = 30_000;
6
+ const ERROR_CACHE_TTL_MS = 15_000;
7
7
  const ZERO_QUOTA_CACHE_TTL_MS = 15_000;
8
+ const CRITICAL_QUOTA_CACHE_TTL_MS = 20_000;
8
9
  const LOW_QUOTA_CACHE_TTL_MS = 30_000;
10
+ const MODERATE_QUOTA_CACHE_TTL_MS = 45_000;
9
11
  const SOON_RESET_CACHE_TTL_MS = 15_000;
10
12
  const SOON_RESET_WINDOW_MS = 2 * 60 * 1000;
13
+ const MIN_STALE_MAX_AGE_MS = 5 * 60 * 1000;
14
+ const MAX_STALE_MAX_AGE_MS = 15 * 60 * 1000;
15
+ const RESET_PASSED_STALE_GRACE_MS = 60_000;
11
16
  const authCache = new TtlValueCache();
12
17
  const providerOptionsCache = new TtlValueCache();
13
18
  const inFlight = new Map();
19
+ const invalidatedCacheKeys = new Map();
20
+ const latestFetchGeneration = new Map();
21
+ let requestGeneration = 0;
14
22
  let lastSuccessfulProviderOptionsMap = {};
23
+ const nextRequestGeneration = () => {
24
+ requestGeneration += 1;
25
+ return requestGeneration;
26
+ };
15
27
  const authFingerprint = (auth) => {
16
28
  if (!auth || typeof auth !== 'object')
17
29
  return undefined;
@@ -78,7 +90,8 @@ export function createQuotaService(deps) {
78
90
  })
79
91
  .catch(swallow('getProviderOptionsMap:providerList'));
80
92
  }
81
- const data = isRecord(response) && Object.prototype.hasOwnProperty.call(response, 'data')
93
+ const data = isRecord(response) &&
94
+ Object.prototype.hasOwnProperty.call(response, 'data')
82
95
  ? response.data
83
96
  : undefined;
84
97
  if (!response || data === undefined) {
@@ -89,7 +102,8 @@ export function createQuotaService(deps) {
89
102
  throwOnError: true,
90
103
  })
91
104
  .catch(swallow('getProviderOptionsMap:providerListNoDataFallback'));
92
- const fallbackData = isRecord(response) && Object.prototype.hasOwnProperty.call(response, 'data')
105
+ const fallbackData = isRecord(response) &&
106
+ Object.prototype.hasOwnProperty.call(response, 'data')
93
107
  ? response.data
94
108
  : undefined;
95
109
  const fallbackRecord = isRecord(fallbackData) ? fallbackData : undefined;
@@ -110,7 +124,9 @@ export function createQuotaService(deps) {
110
124
  const key = record.key;
111
125
  if (typeof id !== 'string')
112
126
  return acc;
113
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
127
+ if (!options ||
128
+ typeof options !== 'object' ||
129
+ Array.isArray(options)) {
114
130
  acc[id] =
115
131
  typeof key === 'string' && key ? { apiKey: key } : {};
116
132
  return acc;
@@ -118,7 +134,9 @@ export function createQuotaService(deps) {
118
134
  const optionsRecord = options;
119
135
  acc[id] = {
120
136
  ...optionsRecord,
121
- ...(typeof key === 'string' && key && optionsRecord.apiKey === undefined
137
+ ...(typeof key === 'string' &&
138
+ key &&
139
+ optionsRecord.apiKey === undefined
122
140
  ? { apiKey: key }
123
141
  : {}),
124
142
  };
@@ -149,7 +167,8 @@ export function createQuotaService(deps) {
149
167
  throwOnError: true,
150
168
  })
151
169
  .catch(swallow('getProviderOptionsMap:providerListFallback'));
152
- const fallbackData = isRecord(response) && Object.prototype.hasOwnProperty.call(response, 'data')
170
+ const fallbackData = isRecord(response) &&
171
+ Object.prototype.hasOwnProperty.call(response, 'data')
153
172
  ? response.data
154
173
  : undefined;
155
174
  const fallbackRecord = isRecord(fallbackData) ? fallbackData : undefined;
@@ -170,14 +189,18 @@ export function createQuotaService(deps) {
170
189
  const key = record.key;
171
190
  if (typeof id !== 'string')
172
191
  return acc;
173
- if (!options || typeof options !== 'object' || Array.isArray(options)) {
192
+ if (!options ||
193
+ typeof options !== 'object' ||
194
+ Array.isArray(options)) {
174
195
  acc[id] = typeof key === 'string' && key ? { apiKey: key } : {};
175
196
  return acc;
176
197
  }
177
198
  const optionsRecord = options;
178
199
  acc[id] = {
179
200
  ...optionsRecord,
180
- ...(typeof key === 'string' && key && optionsRecord.apiKey === undefined
201
+ ...(typeof key === 'string' &&
202
+ key &&
203
+ optionsRecord.apiKey === undefined
181
204
  ? { apiKey: key }
182
205
  : {}),
183
206
  };
@@ -214,7 +237,9 @@ export function createQuotaService(deps) {
214
237
  const optionsRecord = options;
215
238
  acc[id] = {
216
239
  ...optionsRecord,
217
- ...(typeof key === 'string' && key && optionsRecord.apiKey === undefined
240
+ ...(typeof key === 'string' &&
241
+ key &&
242
+ optionsRecord.apiKey === undefined
218
243
  ? { apiKey: key }
219
244
  : {}),
220
245
  };
@@ -283,8 +308,59 @@ export function createQuotaService(deps) {
283
308
  }
284
309
  return values;
285
310
  };
311
+ const staleMaxAgeMs = () => Math.min(MAX_STALE_MAX_AGE_MS, Math.max(MIN_STALE_MAX_AGE_MS, deps.config.quota.refreshMs * 2));
312
+ const staleReason = (snapshot) => {
313
+ if (snapshot.status !== 'error')
314
+ return undefined;
315
+ const note = snapshot.note?.trim();
316
+ if (!note)
317
+ return undefined;
318
+ const normalized = note.toLowerCase();
319
+ if (normalized === 'timeout') {
320
+ return { kind: 'timeout', text: note };
321
+ }
322
+ if (normalized.includes('network request failed')) {
323
+ return { kind: 'network', text: note };
324
+ }
325
+ if (/^http\s+5\d\d\b/i.test(note)) {
326
+ return { kind: 'http_5xx', text: note };
327
+ }
328
+ if (/^http\s+(408|429)\b/i.test(note)) {
329
+ return { kind: 'http_transient', text: note };
330
+ }
331
+ if (normalized.includes('invalid response') ||
332
+ normalized.includes('missing quota fields')) {
333
+ return { kind: 'invalid_response', text: note };
334
+ }
335
+ return undefined;
336
+ };
337
+ const canReuseStaleQuota = (snapshot, now = Date.now()) => {
338
+ if (snapshot.status !== 'ok')
339
+ return false;
340
+ if (now - snapshot.checkedAt > staleMaxAgeMs())
341
+ return false;
342
+ const resetTimes = snapshotResetTimes(snapshot);
343
+ if (resetTimes.some((resetAt) => resetAt + RESET_PASSED_STALE_GRACE_MS <= now)) {
344
+ return false;
345
+ }
346
+ return true;
347
+ };
348
+ const withStaleQuota = (snapshot, failure, now = Date.now()) => {
349
+ const reason = staleReason(failure);
350
+ return {
351
+ ...snapshot,
352
+ stale: {
353
+ staleAt: now,
354
+ staleReason: reason?.text || failure.note || 'error',
355
+ staleReasonKind: reason?.kind || 'unknown',
356
+ },
357
+ };
358
+ };
286
359
  const effectiveQuotaCacheTtl = (snapshot, now = Date.now()) => {
287
360
  let ttlMs = deps.config.quota.refreshMs;
361
+ if (snapshot.stale) {
362
+ ttlMs = Math.min(ttlMs, ERROR_CACHE_TTL_MS);
363
+ }
288
364
  if (snapshot.status !== 'ok') {
289
365
  ttlMs = Math.min(ttlMs, ERROR_CACHE_TTL_MS);
290
366
  }
@@ -292,9 +368,15 @@ export function createQuotaService(deps) {
292
368
  if (remainingPercents.some((value) => value <= 0)) {
293
369
  ttlMs = Math.min(ttlMs, ZERO_QUOTA_CACHE_TTL_MS);
294
370
  }
295
- else if (remainingPercents.some((value) => value <= 1)) {
371
+ else if (remainingPercents.some((value) => value <= 5)) {
372
+ ttlMs = Math.min(ttlMs, CRITICAL_QUOTA_CACHE_TTL_MS);
373
+ }
374
+ else if (remainingPercents.some((value) => value <= 15)) {
296
375
  ttlMs = Math.min(ttlMs, LOW_QUOTA_CACHE_TTL_MS);
297
376
  }
377
+ else if (remainingPercents.some((value) => value <= 30)) {
378
+ ttlMs = Math.min(ttlMs, MODERATE_QUOTA_CACHE_TTL_MS);
379
+ }
298
380
  const resetTimes = snapshotResetTimes(snapshot);
299
381
  if (resetTimes.some((resetAt) => resetAt <= now))
300
382
  return 0;
@@ -391,18 +473,29 @@ export function createQuotaService(deps) {
391
473
  const fetchSnapshot = (providerID, providerOptions) => {
392
474
  const baseKey = deps.quotaRuntime.quotaCacheKey(providerID, providerOptions);
393
475
  const cacheKey = `${baseKey}#${authScopeFor(providerID, providerOptions)}`;
476
+ const normalizedProviderID = deps.quotaRuntime.normalizeProviderID(providerID);
394
477
  const cached = deps.state.quotaCache[cacheKey];
395
478
  const now = Date.now();
479
+ const invalidatedAt = invalidatedCacheKeys.get(cacheKey);
396
480
  const cacheTtl = cached ? effectiveQuotaCacheTtl(cached, now) : 0;
397
- if (cached && cacheTtl > 0 && now - cached.checkedAt <= cacheTtl) {
481
+ const freshnessAt = cached?.stale?.staleAt ?? cached?.checkedAt ?? 0;
482
+ if (cached &&
483
+ invalidatedAt === undefined &&
484
+ cacheTtl > 0 &&
485
+ now - freshnessAt <= cacheTtl) {
398
486
  if (isValidQuotaCache(cached))
399
487
  return Promise.resolve(cached);
400
488
  delete deps.state.quotaCache[cacheKey];
401
489
  cacheChanged = true;
402
490
  }
403
491
  const existing = inFlight.get(cacheKey);
404
- if (existing)
405
- return existing;
492
+ if (existing &&
493
+ (invalidatedAt === undefined || existing.generation >= invalidatedAt)) {
494
+ return existing.promise;
495
+ }
496
+ if (invalidatedAt !== undefined)
497
+ invalidatedCacheKeys.delete(cacheKey);
498
+ const generation = nextRequestGeneration();
406
499
  const promise = deps.quotaRuntime
407
500
  .fetchQuotaSnapshot(providerID, authMap, deps.config, async (id, next) => {
408
501
  await deps.client.auth
@@ -421,16 +514,36 @@ export function createQuotaService(deps) {
421
514
  .then((latest) => {
422
515
  if (!latest)
423
516
  return undefined;
424
- deps.state.quotaCache[cacheKey] = latest;
425
- cacheChanged = true;
426
- return latest;
517
+ const cachedOk = cached &&
518
+ cached.status === 'ok' &&
519
+ canReuseStaleQuota(cached, Date.now())
520
+ ? cached
521
+ : undefined;
522
+ const next = cachedOk && staleReason(latest)
523
+ ? withStaleQuota(cachedOk, latest, Date.now())
524
+ : latest.stale
525
+ ? { ...latest, stale: undefined }
526
+ : latest;
527
+ const invalidatedGeneration = invalidatedCacheKeys.get(cacheKey) || 0;
528
+ if (generation >= (latestFetchGeneration.get(cacheKey) || 0) &&
529
+ generation >= invalidatedGeneration) {
530
+ deps.state.quotaCache[cacheKey] = next;
531
+ cacheChanged = true;
532
+ }
533
+ return next;
427
534
  })
428
535
  .finally(() => {
429
- if (inFlight.get(cacheKey) === promise) {
536
+ if (inFlight.get(cacheKey)?.promise === promise) {
430
537
  inFlight.delete(cacheKey);
431
538
  }
432
539
  });
433
- inFlight.set(cacheKey, promise);
540
+ latestFetchGeneration.set(cacheKey, generation);
541
+ inFlight.set(cacheKey, {
542
+ generation,
543
+ providerID,
544
+ normalizedProviderID,
545
+ promise,
546
+ });
434
547
  return promise;
435
548
  };
436
549
  const fetched = await Promise.all(dedupedCandidates.map(({ providerID, providerOptions }) => fetchSnapshot(providerID, providerOptions)));
@@ -440,5 +553,24 @@ export function createQuotaService(deps) {
440
553
  deps.scheduleSave();
441
554
  return snapshots;
442
555
  };
443
- return { getQuotaSnapshots };
556
+ const invalidateForProvider = (providerID) => {
557
+ const normalized = deps.quotaRuntime.normalizeProviderID(providerID);
558
+ for (const [cacheKey, snapshot] of Object.entries(deps.state.quotaCache)) {
559
+ if (snapshot.providerID === providerID) {
560
+ invalidatedCacheKeys.set(cacheKey, nextRequestGeneration());
561
+ continue;
562
+ }
563
+ if (deps.quotaRuntime.normalizeProviderID(snapshot.providerID) ===
564
+ normalized) {
565
+ invalidatedCacheKeys.set(cacheKey, nextRequestGeneration());
566
+ }
567
+ }
568
+ for (const [cacheKey, entry] of inFlight.entries()) {
569
+ if (entry.providerID === providerID ||
570
+ entry.normalizedProviderID === normalized) {
571
+ invalidatedCacheKeys.set(cacheKey, nextRequestGeneration());
572
+ }
573
+ }
574
+ };
575
+ return { getQuotaSnapshots, invalidateForProvider };
444
576
  }
package/dist/storage.d.ts CHANGED
@@ -26,7 +26,7 @@ export declare function evictOldSessions(state: QuotaSidebarState, retentionDays
26
26
  * M9 fix: scan from in-memory state first, only read disk for date keys
27
27
  * not represented in memory.
28
28
  */
29
- export declare function scanSessionsByCreatedRange(statePath: string, startAt: number, endAt?: number, memoryState?: QuotaSidebarState): Promise<{
29
+ export declare function scanSessionsByCreatedRange(statePath: string, startAt: number, endAt?: number, memoryState?: QuotaSidebarState, retentionDays?: number): Promise<{
30
30
  sessionID: string;
31
31
  dateKey: string;
32
32
  state: SessionState;
package/dist/storage.js CHANGED
@@ -38,13 +38,13 @@ export const defaultConfig = {
38
38
  },
39
39
  },
40
40
  quota: {
41
- refreshMs: 5 * 60 * 1000,
41
+ refreshMs: 1 * 60 * 1000,
42
42
  includeOpenAI: true,
43
43
  includeCopilot: true,
44
44
  includeAnthropic: true,
45
45
  providers: {},
46
46
  refreshAccessToken: false,
47
- requestTimeoutMs: 8_000,
47
+ requestTimeoutMs: 12_000,
48
48
  },
49
49
  toast: {
50
50
  durationMs: 12_000,
@@ -363,10 +363,10 @@ export function evictOldSessions(state, retentionDays) {
363
363
  * M9 fix: scan from in-memory state first, only read disk for date keys
364
364
  * not represented in memory.
365
365
  */
366
- export async function scanSessionsByCreatedRange(statePath, startAt, endAt = Date.now(), memoryState) {
366
+ export async function scanSessionsByCreatedRange(statePath, startAt, endAt = Date.now(), memoryState, retentionDays = 730) {
367
367
  const rootPath = chunkRootPathFromStateFile(statePath);
368
368
  const deletedSessionIDs = new Set(Object.keys(memoryState?.deletedSessionDateMap || {}));
369
- const dateKeys = dateKeysInRange(startAt, endAt);
369
+ const dateKeys = dateKeysInRange(startAt, endAt, retentionDays + 1);
370
370
  if (!dateKeys.length) {
371
371
  return [];
372
372
  }
@@ -6,4 +6,4 @@ export declare function isDateKey(value: string): boolean;
6
6
  */
7
7
  export declare function dateKeyFromTimestamp(timestampMs: number): string;
8
8
  export declare function dateStartFromKey(dateKey: string): number;
9
- export declare function dateKeysInRange(startAt: number, endAt: number): string[];
9
+ export declare function dateKeysInRange(startAt: number, endAt: number, maxDays?: number): string[];
@@ -65,9 +65,12 @@ export function dateStartFromKey(dateKey) {
65
65
  const [yearText, monthText, dayText] = dateKey.split('-');
66
66
  return new Date(Number(yearText), Number(monthText) - 1, Number(dayText)).getTime();
67
67
  }
68
- /** M7 fix: cap iteration at 400 days (~13 months). */
69
- const MAX_DATE_RANGE_DAYS = 400;
70
- export function dateKeysInRange(startAt, endAt) {
68
+ /**
69
+ * M7 fix: cap iteration at 731 calendar dates so a rolling 730-day retention
70
+ * window still covers both boundary days.
71
+ */
72
+ const DEFAULT_MAX_DATE_RANGE_DAYS = 731;
73
+ export function dateKeysInRange(startAt, endAt, maxDays = DEFAULT_MAX_DATE_RANGE_DAYS) {
71
74
  const startDate = new Date(startAt);
72
75
  if (Number.isNaN(startDate.getTime()))
73
76
  return [];
@@ -77,9 +80,9 @@ export function dateKeysInRange(startAt, endAt) {
77
80
  const cursor = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
78
81
  const endDay = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
79
82
  const keys = [];
83
+ const safeMaxDays = Math.max(1, Math.floor(maxDays));
80
84
  let iterations = 0;
81
- while (cursor.getTime() <= endDay.getTime() &&
82
- iterations < MAX_DATE_RANGE_DAYS) {
85
+ while (cursor.getTime() <= endDay.getTime() && iterations < safeMaxDays) {
83
86
  keys.push(dateKeyFromTimestamp(cursor.getTime()));
84
87
  cursor.setDate(cursor.getDate() + 1);
85
88
  iterations++;
@@ -1,4 +1,5 @@
1
1
  import { asNumber, isRecord } from './helpers.js';
2
+ import { isSupportedQuotaSnapshot } from './supported_quota.js';
2
3
  import { normalizeTimestampMs } from './storage_dates.js';
3
4
  function parseSessionTitleState(value) {
4
5
  if (!isRecord(value))
@@ -152,7 +153,26 @@ function parseQuotaSnapshot(value) {
152
153
  }))
153
154
  .filter((window) => window.label || window.remainingPercent !== undefined)
154
155
  : undefined;
155
- return {
156
+ const stale = isRecord(value.stale)
157
+ ? (() => {
158
+ const staleReasonKind = value.stale.staleReasonKind === 'timeout' ||
159
+ value.stale.staleReasonKind === 'network' ||
160
+ value.stale.staleReasonKind === 'http_5xx' ||
161
+ value.stale.staleReasonKind === 'http_transient' ||
162
+ value.stale.staleReasonKind === 'invalid_response' ||
163
+ value.stale.staleReasonKind === 'unknown'
164
+ ? value.stale.staleReasonKind
165
+ : 'unknown';
166
+ return {
167
+ staleAt: typeof value.stale.staleAt === 'number' ? value.stale.staleAt : 0,
168
+ staleReason: typeof value.stale.staleReason === 'string'
169
+ ? value.stale.staleReason
170
+ : '',
171
+ staleReasonKind,
172
+ };
173
+ })()
174
+ : undefined;
175
+ const parsed = {
156
176
  providerID: typeof value.providerID === 'string' ? value.providerID : label,
157
177
  adapterID,
158
178
  label,
@@ -169,7 +189,9 @@ function parseQuotaSnapshot(value) {
169
189
  balance,
170
190
  note: typeof value.note === 'string' ? value.note : undefined,
171
191
  windows,
192
+ stale: stale && stale.staleAt > 0 && stale.staleReason ? stale : undefined,
172
193
  };
194
+ return isSupportedQuotaSnapshot(parsed) ? parsed : undefined;
173
195
  }
174
196
  function parseQuotaSnapshots(value) {
175
197
  if (!Array.isArray(value))
@@ -0,0 +1,4 @@
1
+ import type { QuotaSnapshot } from './types.js';
2
+ export declare function isSupportedQuotaProviderID(providerID: string): boolean;
3
+ export declare function isSupportedQuotaSnapshot(quota: Pick<QuotaSnapshot, 'providerID' | 'adapterID'>): boolean;
4
+ export declare function isSupportedQuotaTitleLabel(label: string): boolean;
@@ -0,0 +1,36 @@
1
+ const SUPPORTED_QUOTA_PROVIDER_IDS = new Set([
2
+ 'openai',
3
+ 'github-copilot',
4
+ 'anthropic',
5
+ 'kimi-for-coding',
6
+ 'zhipuai-coding-plan',
7
+ 'minimax-cn-coding-plan',
8
+ 'rightcode',
9
+ ]);
10
+ const SUPPORTED_QUOTA_TITLE_LABELS = new Set([
11
+ 'OAI',
12
+ 'Cop',
13
+ 'Ant',
14
+ 'Kimi',
15
+ 'Zhipu',
16
+ 'MiniMax',
17
+ 'RC',
18
+ ]);
19
+ export function isSupportedQuotaProviderID(providerID) {
20
+ if (providerID.startsWith('github-copilot'))
21
+ return true;
22
+ if (providerID.startsWith('rightcode-'))
23
+ return true;
24
+ return SUPPORTED_QUOTA_PROVIDER_IDS.has(providerID);
25
+ }
26
+ export function isSupportedQuotaSnapshot(quota) {
27
+ if (typeof quota.adapterID === 'string' && quota.adapterID) {
28
+ return isSupportedQuotaProviderID(quota.adapterID);
29
+ }
30
+ return isSupportedQuotaProviderID(quota.providerID);
31
+ }
32
+ export function isSupportedQuotaTitleLabel(label) {
33
+ if (SUPPORTED_QUOTA_TITLE_LABELS.has(label))
34
+ return true;
35
+ return /^RC-[^\s]+$/.test(label);
36
+ }
package/dist/title.js CHANGED
@@ -59,28 +59,38 @@ function isQuotaDecoratedDetail(line) {
59
59
  return false;
60
60
  const resetValue = '[-:\\dhmD]+';
61
61
  const compactResetValue = '\\d[\\d:.\\-hmD]*';
62
- if (/^(OAI|Cop|Ant|Kimi|XYAI|RC(?:-[^\s]+)?)(?:\s+(?:\?|unsupported|unavailable|error|(?:\d+h|D|W|M)\d{1,3}|D[\d.,]+\/[\d.,]+|B(?:[¥$-])?[\d.,]+))+$/i.test(line)) {
62
+ const legacyProvider = '[A-Z][A-Z0-9-]{1,15}';
63
+ if (/^(OAI|Cop|Ant|Kimi|RC(?:-[^\s]+)?)(?:\s+(?:\?|unsupported|unavailable|error|stale|St|(?:\d+h|D|W|M)\d{1,3}|(?:Sk5h|SkW)\d{1,3}|D[\d.,]+\/[\d.,]+|B(?:[¥$-])?[\d.,]+))+$/i.test(line)) {
63
64
  return true;
64
65
  }
65
- if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\s]+)?)\s*$/.test(line)) {
66
+ if (/^(OpenAI|Copilot|Anthropic|Kimi|RC(?:-[^\s]+)?)\s*$/.test(line)) {
66
67
  return true;
67
68
  }
68
- if (new RegExp(`^(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|Balance\\s+\\$[\\d.,]+|Remaining\\s+\\?|(?:error|unsupported|unavailable))$`).test(line)) {
69
+ if (new RegExp(`^(?:(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|Balance\\s+\\$[\\d.,]+|Remaining\\s+\\?)(?:\\s+stale)?|(?:error|unsupported|unavailable|stale))$`).test(line)) {
69
70
  return true;
70
71
  }
71
- if (new RegExp(`^(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3})(?:\\s+(?:R|E\\+?)${compactResetValue})?$`).test(line)) {
72
+ if (new RegExp(`^(?:Spark\\s+(?:5h|Weekly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?)(?:\\s+stale)?$`).test(line)) {
72
73
  return true;
73
74
  }
74
- if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\\s]+)?)(?:\\s+(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|(?:error|unsupported|unavailable)))$`).test(line)) {
75
+ if (new RegExp(`^(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St)(?:\\s+(?:R|E\\+?)${compactResetValue})?$`).test(line)) {
75
76
  return true;
76
77
  }
77
- if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\\s]+)?)(?:\\s+(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3})(?:\\s+(?:R|E\\+?)${compactResetValue})?)$`).test(line)) {
78
+ if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|RC(?:-[^\\s]+)?)(?:\\s+(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?)(?:\\s+stale)?|(?:error|unsupported|unavailable|stale))$`).test(line)) {
78
79
  return true;
79
80
  }
80
- if (new RegExp(`^(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3}|(?:R|E\\+?)${compactResetValue}))(?:\\s+(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3}|(?:R|E\\+?)${compactResetValue})))*$`).test(line)) {
81
+ if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|RC(?:-[^\\s]+)?)(?:\\s+(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St)(?:\\s+(?:R|E\\+?)${compactResetValue})?)$`).test(line)) {
81
82
  return true;
82
83
  }
83
- if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|XYAI|RC(?:-[^\\s]+)?)(?:\\s+(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3}|(?:R|E\\+?)${compactResetValue})))*$`).test(line)) {
84
+ if (new RegExp(`^(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St|(?:R|E\\+?)${compactResetValue}))(?:\\s+(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St|(?:R|E\\+?)${compactResetValue})))*$`).test(line)) {
85
+ return true;
86
+ }
87
+ if (new RegExp(`^(OpenAI|Copilot|Anthropic|Kimi|RC(?:-[^\\s]+)?)(?:\\s+(?:(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d|Sk5h|SkW)\\d{1,3}|St|(?:R|E\\+?)${compactResetValue})))*$`).test(line)) {
88
+ return true;
89
+ }
90
+ if (new RegExp(`^${legacyProvider}(?:\\s+(?:(?:Daily\\s+\\$[\\d.,]+\\/\\$[\\d.,]+|\\$[\\d.,]+\\/\\$[\\d.,]+)(?:\\s+(?:Rst|Exp\\+?)\\s+${resetValue})?|(?:\\d+[hdw]|Weekly|Monthly)\\s+\\d{1,3}%(?:\\s+Rst\\s+${resetValue})?|(?:error|unsupported|unavailable|stale)))$`).test(line)) {
91
+ return true;
92
+ }
93
+ if (new RegExp(`^${legacyProvider}(?:\\s+(?:D\\$?[\\d.,]+\\/\\$?[\\d.,]+|B(?:[¥$-])?[\\d.,]+|(?:\\d+[hdw]|[DWM])\\d{1,3}|(?:S7d|O7d|OA7d|Co7d)\\d{1,3}|St|(?:R|E\\+?)${compactResetValue}))*$`).test(line)) {
84
94
  return true;
85
95
  }
86
96
  return false;
package/dist/tools.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import * as z from 'zod';
2
2
  import type { QuotaSnapshot } from './types.js';
3
3
  import type { UsageSummary } from './usage.js';
4
+ import type { HistoryPeriod } from './period.js';
5
+ import type { HistoryUsageResult } from './usage_service.js';
4
6
  type ToolContext = {
5
7
  sessionID: string;
6
8
  };
@@ -19,6 +21,8 @@ export declare function createQuotaSidebarTools(deps: {
19
21
  restoreSessionTitle?: (sessionID: string) => Promise<boolean>;
20
22
  showToast: (period: 'session' | 'day' | 'week' | 'month' | 'toggle', message: string) => Promise<void>;
21
23
  summarizeForTool: (period: 'session' | 'day' | 'week' | 'month', sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
24
+ summarizeHistoryForTool: (period: HistoryPeriod, since: string) => Promise<HistoryUsageResult>;
25
+ listCurrentProviderIDs?: () => Promise<Set<string>>;
22
26
  getQuotaSnapshots: (providerIDs: string[], options?: {
23
27
  allowDefault?: boolean;
24
28
  }) => Promise<QuotaSnapshot[]>;
@@ -29,6 +33,9 @@ export declare function createQuotaSidebarTools(deps: {
29
33
  showCost?: boolean;
30
34
  width?: number;
31
35
  }) => string;
36
+ renderHistoryMarkdownReport: (result: HistoryUsageResult, quotas: QuotaSnapshot[], options?: {
37
+ showCost?: boolean;
38
+ }) => string;
32
39
  config: {
33
40
  sidebar: {
34
41
  showCost: boolean;
@@ -42,16 +49,20 @@ export declare function createQuotaSidebarTools(deps: {
42
49
  description: string;
43
50
  args: {
44
51
  period: z.ZodOptional<z.ZodEnum<{
45
- day: "day";
46
- week: "week";
47
52
  month: "month";
53
+ day: "day";
48
54
  session: "session";
55
+ week: "week";
49
56
  }>>;
57
+ since: z.ZodOptional<z.ZodString>;
58
+ last: z.ZodOptional<z.ZodNumber>;
50
59
  toast: z.ZodOptional<z.ZodBoolean>;
51
60
  includeChildren: z.ZodOptional<z.ZodBoolean>;
52
61
  };
53
62
  execute: (args: {
54
- period?: "day" | "week" | "month" | "session" | undefined;
63
+ period?: "month" | "day" | "session" | "week" | undefined;
64
+ since?: string | undefined;
65
+ last?: number | undefined;
55
66
  toast?: boolean | undefined;
56
67
  includeChildren?: boolean | undefined;
57
68
  }, context: ToolContext) => Promise<string>;
package/dist/tools.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import * as z from 'zod';
2
+ import { sinceFromLast } from './period.js';
3
+ import { filterHistoryProvidersForDisplay, filterUsageProvidersForDisplay, } from './provider_catalog.js';
2
4
  function tool(input) {
3
5
  return input;
4
6
  }
@@ -16,6 +18,16 @@ export function createQuotaSidebarTools(deps) {
16
18
  description: 'Show usage and quota summary for session/day/week/month. Returns the full markdown report with totals, highlights, provider table, and subscription quota so callers can present the report directly to the user.',
17
19
  args: {
18
20
  period: z.enum(['session', 'day', 'week', 'month']).optional(),
21
+ since: z
22
+ .string()
23
+ .optional()
24
+ .describe('Historical start date: `YYYY-MM` or `YYYY-MM-DD`.'),
25
+ last: z
26
+ .number()
27
+ .int()
28
+ .positive()
29
+ .optional()
30
+ .describe('Relative history length. Examples: `period=day,last=7`, `period=week,last=8`, `period=month,last=6`.'),
19
31
  toast: z.boolean().optional(),
20
32
  includeChildren: z
21
33
  .boolean()
@@ -23,11 +35,51 @@ export function createQuotaSidebarTools(deps) {
23
35
  .describe('For period=session, include descendant subagent sessions in usage aggregation.'),
24
36
  },
25
37
  execute: async (args, context) => {
26
- const period = args.period || 'session';
38
+ const period = args.period || (args.since || args.last ? 'month' : 'session');
39
+ const since = args.since?.trim();
40
+ const last = args.last;
41
+ if (since && last !== undefined) {
42
+ throw new Error('`since` and `last` cannot be used together');
43
+ }
44
+ if (period === 'session' && since) {
45
+ throw new Error('`since` is not supported when `period=session`');
46
+ }
47
+ if (period === 'session' && last !== undefined) {
48
+ throw new Error('`last` is not supported when `period=session`');
49
+ }
50
+ const resolvedSince = since ||
51
+ (period !== 'session' && last !== undefined
52
+ ? sinceFromLast(period, last)
53
+ : undefined);
54
+ const allowedProviderIDs = await deps
55
+ .listCurrentProviderIDs?.()
56
+ .catch(() => new Set());
57
+ if (period !== 'session' && resolvedSince) {
58
+ const historyRaw = await deps.summarizeHistoryForTool(period, resolvedSince);
59
+ const history = allowedProviderIDs
60
+ ? filterHistoryProvidersForDisplay(historyRaw, allowedProviderIDs)
61
+ : historyRaw;
62
+ const quotas = await deps.getQuotaSnapshots([], {
63
+ allowDefault: true,
64
+ });
65
+ const markdown = deps.renderHistoryMarkdownReport(history, quotas, {
66
+ showCost: deps.config.sidebar.showCost,
67
+ });
68
+ if (args.toast === true) {
69
+ await deps.showToast(period, deps.renderToastMessage(period, history.total, quotas, {
70
+ showCost: deps.config.sidebar.showCost,
71
+ width: Math.max(44, deps.config.sidebar.width + 18),
72
+ }));
73
+ }
74
+ return markdown;
75
+ }
27
76
  const includeChildren = period === 'session'
28
77
  ? (args.includeChildren ?? deps.config.sidebar.includeChildren)
29
78
  : false;
30
- const usage = await deps.summarizeForTool(period, context.sessionID, includeChildren);
79
+ const usageRaw = await deps.summarizeForTool(period, context.sessionID, includeChildren);
80
+ const usage = allowedProviderIDs
81
+ ? filterUsageProvidersForDisplay(usageRaw, allowedProviderIDs)
82
+ : usageRaw;
31
83
  // For quota_summary, always show all subscription quota balances,
32
84
  // regardless of which providers were used in the session.
33
85
  const quotas = await deps.getQuotaSnapshots([], { allowDefault: true });