@leo000001/opencode-quota-sidebar 1.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 (55) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/CONTRIBUTING.md +102 -0
  3. package/LICENSE +21 -0
  4. package/README.md +216 -0
  5. package/SECURITY.md +26 -0
  6. package/dist/cache.d.ts +6 -0
  7. package/dist/cache.js +22 -0
  8. package/dist/cost.d.ts +13 -0
  9. package/dist/cost.js +76 -0
  10. package/dist/format.d.ts +21 -0
  11. package/dist/format.js +426 -0
  12. package/dist/helpers.d.ts +14 -0
  13. package/dist/helpers.js +50 -0
  14. package/dist/index.d.ts +5 -0
  15. package/dist/index.js +699 -0
  16. package/dist/period.d.ts +1 -0
  17. package/dist/period.js +14 -0
  18. package/dist/providers/common.d.ts +24 -0
  19. package/dist/providers/common.js +114 -0
  20. package/dist/providers/core/anthropic.d.ts +2 -0
  21. package/dist/providers/core/anthropic.js +46 -0
  22. package/dist/providers/core/copilot.d.ts +2 -0
  23. package/dist/providers/core/copilot.js +117 -0
  24. package/dist/providers/core/openai.d.ts +2 -0
  25. package/dist/providers/core/openai.js +159 -0
  26. package/dist/providers/index.d.ts +8 -0
  27. package/dist/providers/index.js +14 -0
  28. package/dist/providers/registry.d.ts +9 -0
  29. package/dist/providers/registry.js +38 -0
  30. package/dist/providers/third_party/rightcode.d.ts +2 -0
  31. package/dist/providers/third_party/rightcode.js +230 -0
  32. package/dist/providers/types.d.ts +58 -0
  33. package/dist/providers/types.js +1 -0
  34. package/dist/quota.d.ts +49 -0
  35. package/dist/quota.js +116 -0
  36. package/dist/quota_render.d.ts +5 -0
  37. package/dist/quota_render.js +85 -0
  38. package/dist/storage.d.ts +32 -0
  39. package/dist/storage.js +328 -0
  40. package/dist/storage_chunks.d.ts +9 -0
  41. package/dist/storage_chunks.js +147 -0
  42. package/dist/storage_dates.d.ts +9 -0
  43. package/dist/storage_dates.js +88 -0
  44. package/dist/storage_parse.d.ts +4 -0
  45. package/dist/storage_parse.js +149 -0
  46. package/dist/storage_paths.d.ts +14 -0
  47. package/dist/storage_paths.js +31 -0
  48. package/dist/title.d.ts +8 -0
  49. package/dist/title.js +38 -0
  50. package/dist/types.d.ts +116 -0
  51. package/dist/types.js +1 -0
  52. package/dist/usage.d.ts +51 -0
  53. package/dist/usage.js +243 -0
  54. package/package.json +68 -0
  55. package/quota-sidebar.config.example.json +25 -0
package/dist/format.js ADDED
@@ -0,0 +1,426 @@
1
+ import { canonicalProviderID, collapseQuotaSnapshots, displayShortLabel, quotaDisplayLabel, } from './quota_render.js';
2
+ import { stripAnsi } from './title.js';
3
+ /** M6 fix: handle negative, NaN, Infinity gracefully. */
4
+ function shortNumber(value, decimals = 1) {
5
+ if (!Number.isFinite(value) || value < 0)
6
+ return '0';
7
+ if (value >= 1_000_000)
8
+ return `${(value / 1_000_000).toFixed(decimals)}m`;
9
+ if (value >= 1000) {
10
+ const k = value / 1000;
11
+ // Avoid "1000.0k" — promote to "m" when rounding pushes past 999
12
+ if (Number(k.toFixed(decimals)) >= 1000)
13
+ return `${(value / 1_000_000).toFixed(decimals)}m`;
14
+ return `${k.toFixed(decimals)}k`;
15
+ }
16
+ return `${Math.round(value)}`;
17
+ }
18
+ /** Sidebar token display: adaptive short unit (k/m) with one decimal. */
19
+ function sidebarNumber(value) {
20
+ return shortNumber(value, 1);
21
+ }
22
+ /**
23
+ * Truncate `value` to at most `width` visible characters.
24
+ * Keep plain text only (no ANSI) to avoid renderer corruption.
25
+ */
26
+ function fitLine(value, width) {
27
+ if (width <= 0)
28
+ return '';
29
+ if (value.length > width) {
30
+ return width <= 1 ? value.slice(0, width) : `${value.slice(0, width - 1)}~`;
31
+ }
32
+ return value;
33
+ }
34
+ function formatApiCostValue(value) {
35
+ const safe = Number.isFinite(value) && value > 0 ? value : 0;
36
+ return `$${safe.toFixed(2)}`;
37
+ }
38
+ function formatApiCostLine(value) {
39
+ return `${formatApiCostValue(value)} as API cost`;
40
+ }
41
+ function alignPairs(pairs, indent = ' ') {
42
+ if (pairs.length === 0)
43
+ return [];
44
+ const labelWidth = Math.max(...pairs.map((pair) => pair.label.length), 0);
45
+ return pairs.map((pair) => {
46
+ if (!pair.label) {
47
+ return `${indent}${' '.repeat(labelWidth)} ${pair.value}`;
48
+ }
49
+ return `${indent}${pair.label.padEnd(labelWidth)} ${pair.value}`;
50
+ });
51
+ }
52
+ /**
53
+ * Render sidebar title with multi-line token breakdown.
54
+ *
55
+ * Layout:
56
+ * Session title
57
+ * Input 18.9k Output 53
58
+ * Cache Read 1.5k (only if read > 0)
59
+ * Cache Write 200 (only if write > 0)
60
+ * $3.81 as API cost (only if showCost=true)
61
+ * OpenAI Remaining 78% (only if quota available)
62
+ */
63
+ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
64
+ const width = Math.max(8, Math.floor(config.sidebar.width || 36));
65
+ const lines = [];
66
+ const safeBaseTitle = stripAnsi(baseTitle || 'Session').split(/\r?\n/, 1)[0] || 'Session';
67
+ lines.push(fitLine(safeBaseTitle, width));
68
+ lines.push('');
69
+ // Input / Output line
70
+ const io = `Input ${sidebarNumber(usage.input)} Output ${sidebarNumber(usage.output)}`;
71
+ lines.push(fitLine(io, width));
72
+ // Cache lines (provider-compatible across OpenAI/Anthropic/Gemini/Copilot)
73
+ if (usage.cacheRead > 0) {
74
+ lines.push(fitLine(`Cache Read ${sidebarNumber(usage.cacheRead)}`, width));
75
+ }
76
+ if (usage.cacheWrite > 0) {
77
+ lines.push(fitLine(`Cache Write ${sidebarNumber(usage.cacheWrite)}`, width));
78
+ }
79
+ if (config.sidebar.showCost && usage.apiCost > 0) {
80
+ lines.push(fitLine(formatApiCostLine(usage.apiCost), width));
81
+ }
82
+ // Quota lines (one provider per line for stable wrapping)
83
+ if (config.sidebar.showQuota) {
84
+ const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
85
+ const labelWidth = visibleQuotas.reduce((max, item) => {
86
+ const label = quotaDisplayLabel(item);
87
+ return Math.max(max, label.length);
88
+ }, 0);
89
+ const quotaItems = visibleQuotas
90
+ .flatMap((item) => compactQuotaWide(item, labelWidth))
91
+ .filter((s) => Boolean(s));
92
+ if (quotaItems.length > 0) {
93
+ lines.push('');
94
+ }
95
+ for (const line of quotaItems) {
96
+ lines.push(fitLine(line, width));
97
+ }
98
+ }
99
+ return lines.join('\n');
100
+ }
101
+ /**
102
+ * Multi-window quota format for sidebar.
103
+ * Single window: "OpenAI 5h 80% Rst 16:20"
104
+ * Multi window: "OpenAI 5h 80% Rst 16:20" + indented next line
105
+ * Copilot: "Copilot Monthly 70% Rst 03-01"
106
+ */
107
+ function compactQuotaWide(quota, labelWidth = 0) {
108
+ const label = quotaDisplayLabel(quota);
109
+ const labelPadding = ' '.repeat(Math.max(0, labelWidth - label.length));
110
+ const withLabel = (content) => `${label}${labelPadding} ${content}`;
111
+ if (quota.status === 'error')
112
+ return [withLabel('Remaining ?')];
113
+ if (quota.status === 'unsupported')
114
+ return [withLabel('unsupported')];
115
+ if (quota.status === 'unavailable')
116
+ return [withLabel('unavailable')];
117
+ if (quota.status !== 'ok')
118
+ return [];
119
+ const balanceText = quota.balance
120
+ ? `Balance ${quota.balance.currency}${quota.balance.amount.toFixed(2)}`
121
+ : undefined;
122
+ const renderWindow = (win) => {
123
+ const showPercent = win.showPercent !== false;
124
+ const pct = win.remainingPercent === undefined
125
+ ? '?'
126
+ : `${Math.round(win.remainingPercent)}%`;
127
+ const parts = win.label
128
+ ? showPercent
129
+ ? [win.label, pct]
130
+ : [win.label]
131
+ : [pct];
132
+ const reset = compactReset(win.resetAt);
133
+ if (reset) {
134
+ parts.push(`${win.resetLabel || 'Rst'} ${reset}`);
135
+ }
136
+ return parts.join(' ');
137
+ };
138
+ // Multi-window rendering
139
+ if (quota.windows && quota.windows.length > 0) {
140
+ const parts = quota.windows.map(renderWindow);
141
+ if (parts.length === 1) {
142
+ const first = withLabel(parts[0]);
143
+ if (balanceText && !parts[0].includes('Balance ')) {
144
+ const indent = ' '.repeat(labelWidth + 1);
145
+ return [first, `${indent}${balanceText}`];
146
+ }
147
+ return [first];
148
+ }
149
+ const indent = ' '.repeat(labelWidth + 1);
150
+ const lines = [
151
+ withLabel(parts[0]),
152
+ ...parts.slice(1).map((part) => `${indent}${part}`),
153
+ ];
154
+ const alreadyHasBalance = parts.some((part) => part.includes('Balance '));
155
+ if (balanceText && !alreadyHasBalance) {
156
+ lines.push(`${indent}${balanceText}`);
157
+ }
158
+ return lines;
159
+ }
160
+ if (balanceText) {
161
+ return [withLabel(balanceText)];
162
+ }
163
+ // Fallback: single value from top-level remainingPercent
164
+ const percent = quota.remainingPercent === undefined
165
+ ? '?'
166
+ : `${Math.round(quota.remainingPercent)}%`;
167
+ const reset = compactReset(quota.resetAt);
168
+ return [withLabel(`Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`)];
169
+ }
170
+ function compactReset(iso) {
171
+ if (!iso)
172
+ return undefined;
173
+ const timestamp = Date.parse(iso);
174
+ if (Number.isNaN(timestamp))
175
+ return undefined;
176
+ const value = new Date(timestamp);
177
+ const now = new Date();
178
+ const sameDay = value.getFullYear() === now.getFullYear() &&
179
+ value.getMonth() === now.getMonth() &&
180
+ value.getDate() === now.getDate();
181
+ const two = (num) => `${num}`.padStart(2, '0');
182
+ if (sameDay) {
183
+ return `${two(value.getHours())}:${two(value.getMinutes())}`;
184
+ }
185
+ return `${two(value.getMonth() + 1)}-${two(value.getDate())}`;
186
+ }
187
+ function dateLine(iso) {
188
+ if (!iso)
189
+ return '-';
190
+ const time = Date.parse(iso);
191
+ if (Number.isNaN(time))
192
+ return iso;
193
+ return new Date(time).toLocaleString();
194
+ }
195
+ function periodLabel(period) {
196
+ if (period === 'day')
197
+ return 'Today';
198
+ if (period === 'week')
199
+ return 'This Week';
200
+ if (period === 'month')
201
+ return 'This Month';
202
+ return 'Current Session';
203
+ }
204
+ export function renderMarkdownReport(period, usage, quotas, options) {
205
+ const showCost = options?.showCost !== false;
206
+ const rightCodeSubscriptionProviderIDs = new Set(collapseQuotaSnapshots(quotas)
207
+ .filter((quota) => quota.adapterID === 'rightcode')
208
+ .filter((quota) => quota.status === 'ok')
209
+ .filter((quota) => Array.isArray(quota.windows) && quota.windows.length)
210
+ .filter((quota) => quota.windows[0].label.startsWith('Daily $'))
211
+ .map((quota) => quota.providerID));
212
+ const measuredCostCell = (providerID, cost) => {
213
+ const canonical = canonicalProviderID(providerID);
214
+ const isSubscription = canonical === 'openai' ||
215
+ canonical === 'github-copilot' ||
216
+ rightCodeSubscriptionProviderIDs.has(providerID);
217
+ if (isSubscription)
218
+ return '-';
219
+ return `$${cost.toFixed(3)}`;
220
+ };
221
+ const isSubscriptionMeasuredProvider = (providerID) => {
222
+ const canonical = canonicalProviderID(providerID);
223
+ return (canonical === 'openai' ||
224
+ canonical === 'github-copilot' ||
225
+ rightCodeSubscriptionProviderIDs.has(providerID));
226
+ };
227
+ const apiCostCell = (providerID, apiCost) => {
228
+ const canonical = canonicalProviderID(providerID);
229
+ if (canonical === 'github-copilot')
230
+ return '-';
231
+ if (!Number.isFinite(apiCost) || apiCost <= 0)
232
+ return '$0.00';
233
+ return `$${apiCost.toFixed(2)}`;
234
+ };
235
+ const measuredCostSummaryValue = () => {
236
+ const providers = Object.values(usage.providers);
237
+ if (providers.length === 0)
238
+ return `$${usage.cost.toFixed(4)}`;
239
+ const hasNonSubscription = providers.some((provider) => !isSubscriptionMeasuredProvider(provider.providerID));
240
+ if (!hasNonSubscription)
241
+ return '-';
242
+ return `$${usage.cost.toFixed(4)}`;
243
+ };
244
+ const apiCostSummaryValue = () => {
245
+ const providers = Object.values(usage.providers);
246
+ if (providers.length === 0)
247
+ return formatApiCostValue(usage.apiCost);
248
+ const hasNonCopilot = providers.some((provider) => canonicalProviderID(provider.providerID) !== 'github-copilot');
249
+ if (!hasNonCopilot)
250
+ return '-';
251
+ return formatApiCostValue(usage.apiCost);
252
+ };
253
+ const providerRows = Object.values(usage.providers)
254
+ .sort((a, b) => b.total - a.total)
255
+ .map((provider) => showCost
256
+ ? `| ${provider.providerID} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} | ${measuredCostCell(provider.providerID, provider.cost)} | ${apiCostCell(provider.providerID, provider.apiCost)} |`
257
+ : `| ${provider.providerID} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} |`);
258
+ const quotaLines = collapseQuotaSnapshots(quotas).flatMap((quota) => {
259
+ // Multi-window detail
260
+ if (quota.windows && quota.windows.length > 0 && quota.status === 'ok') {
261
+ return quota.windows.map((win) => {
262
+ if (win.showPercent === false) {
263
+ const winLabel = win.label ? ` (${win.label})` : '';
264
+ return `- ${quota.label}${winLabel}: ${quota.status} | reset ${dateLine(win.resetAt)}`;
265
+ }
266
+ const remaining = win.remainingPercent === undefined
267
+ ? '-'
268
+ : `${win.remainingPercent.toFixed(1)}%`;
269
+ const winLabel = win.label ? ` (${win.label})` : '';
270
+ return `- ${quota.label}${winLabel}: ${quota.status} | remaining ${remaining} | reset ${dateLine(win.resetAt)}`;
271
+ });
272
+ }
273
+ if (quota.status === 'ok' && quota.balance) {
274
+ return [
275
+ `- ${quota.label}: ${quota.status} | balance ${quota.balance.currency}${quota.balance.amount.toFixed(2)}`,
276
+ ];
277
+ }
278
+ const remaining = quota.remainingPercent === undefined
279
+ ? '-'
280
+ : `${quota.remainingPercent.toFixed(1)}%`;
281
+ return [
282
+ `- ${quota.label}: ${quota.status} | remaining ${remaining} | reset ${dateLine(quota.resetAt)}${quota.note ? ` | ${quota.note}` : ''}`,
283
+ ];
284
+ });
285
+ return [
286
+ `## Quota Report - ${periodLabel(period)}`,
287
+ '',
288
+ `- Sessions: ${usage.sessionCount}`,
289
+ `- Assistant messages: ${usage.assistantMessages}`,
290
+ `- Tokens: input ${usage.input}, output ${usage.output}, cache_read ${usage.cacheRead}, cache_write ${usage.cacheWrite}, total ${usage.total}`,
291
+ ...(showCost
292
+ ? [
293
+ `- Measured cost: ${measuredCostSummaryValue()}`,
294
+ `- API cost: ${apiCostSummaryValue()}`,
295
+ ]
296
+ : []),
297
+ '',
298
+ '### Usage by Provider',
299
+ showCost
300
+ ? '| Provider | Input | Output | Cache | Total | Measured Cost | API Cost |'
301
+ : '| Provider | Input | Output | Cache | Total |',
302
+ showCost
303
+ ? '|---|---:|---:|---:|---:|---:|---:|'
304
+ : '|---|---:|---:|---:|---:|',
305
+ ...(providerRows.length
306
+ ? providerRows
307
+ : [showCost ? '| - | - | - | - | - | - | - |' : '| - | - | - | - | - |']),
308
+ '',
309
+ '### Subscription Quota',
310
+ ...(quotaLines.length
311
+ ? quotaLines
312
+ : ['- no provider quota data available']),
313
+ ].join('\n');
314
+ }
315
+ export function renderToastMessage(period, usage, quotas, options) {
316
+ const width = Math.max(24, Math.floor(options?.width || 56));
317
+ const showCost = options?.showCost !== false;
318
+ const lines = [];
319
+ lines.push(fitLine(`${periodLabel(period)} - Total ${shortNumber(usage.total)}`, width));
320
+ lines.push('');
321
+ lines.push(fitLine('Token Usage', width));
322
+ const tokenPairs = [
323
+ { label: 'Input', value: shortNumber(usage.input) },
324
+ { label: 'Output', value: shortNumber(usage.output) },
325
+ ];
326
+ if (usage.cacheRead > 0) {
327
+ tokenPairs.push({
328
+ label: 'Cache Read',
329
+ value: shortNumber(usage.cacheRead),
330
+ });
331
+ }
332
+ if (usage.cacheWrite > 0) {
333
+ tokenPairs.push({
334
+ label: 'Cache Write',
335
+ value: shortNumber(usage.cacheWrite),
336
+ });
337
+ }
338
+ if (showCost) {
339
+ if (usage.apiCost > 0) {
340
+ tokenPairs.push({
341
+ label: 'API Cost',
342
+ value: formatApiCostValue(usage.apiCost),
343
+ });
344
+ }
345
+ }
346
+ lines.push(...alignPairs(tokenPairs).map((line) => fitLine(line, width)));
347
+ if (showCost) {
348
+ const costPairs = Object.values(usage.providers)
349
+ .filter((provider) => canonicalProviderID(provider.providerID) !== 'github-copilot')
350
+ .filter((provider) => provider.apiCost > 0)
351
+ .sort((left, right) => right.apiCost - left.apiCost)
352
+ .map((provider) => ({
353
+ label: displayShortLabel(provider.providerID),
354
+ value: `$${provider.apiCost.toFixed(2)}`,
355
+ }));
356
+ lines.push('');
357
+ lines.push(fitLine('Cost as API', width));
358
+ if (costPairs.length > 0) {
359
+ lines.push(...alignPairs(costPairs).map((line) => fitLine(line, width)));
360
+ }
361
+ else {
362
+ const hasAnyUsage = Object.keys(usage.providers).length > 0;
363
+ lines.push(fitLine(hasAnyUsage ? ' N/A (Copilot)' : ' -', width));
364
+ }
365
+ }
366
+ const quotaPairs = collapseQuotaSnapshots(quotas).flatMap((item) => {
367
+ if (item.status === 'ok') {
368
+ if (item.windows && item.windows.length > 0) {
369
+ const pairs = item.windows.map((win, idx) => {
370
+ const showPercent = win.showPercent !== false;
371
+ const pct = win.remainingPercent === undefined
372
+ ? '-'
373
+ : `${win.remainingPercent.toFixed(1)}%`;
374
+ const reset = compactReset(win.resetAt);
375
+ const parts = [win.label];
376
+ if (showPercent)
377
+ parts.push(pct);
378
+ if (reset)
379
+ parts.push(`${win.resetLabel || 'Rst'} ${reset}`);
380
+ return {
381
+ label: idx === 0 ? quotaDisplayLabel(item) : '',
382
+ value: parts.filter(Boolean).join(' '),
383
+ };
384
+ });
385
+ if (item.balance) {
386
+ pairs.push({
387
+ label: '',
388
+ value: `Balance ${item.balance.currency}${item.balance.amount.toFixed(2)}`,
389
+ });
390
+ }
391
+ return pairs;
392
+ }
393
+ if (item.balance) {
394
+ return [
395
+ {
396
+ label: quotaDisplayLabel(item),
397
+ value: `Balance ${item.balance.currency}${item.balance.amount.toFixed(2)}`,
398
+ },
399
+ ];
400
+ }
401
+ const percent = item.remainingPercent === undefined
402
+ ? '-'
403
+ : `${item.remainingPercent.toFixed(1)}%`;
404
+ const reset = compactReset(item.resetAt);
405
+ return [
406
+ {
407
+ label: quotaDisplayLabel(item),
408
+ value: `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`,
409
+ },
410
+ ];
411
+ }
412
+ if (item.status === 'unsupported') {
413
+ return [{ label: quotaDisplayLabel(item), value: 'unsupported' }];
414
+ }
415
+ if (item.status === 'unavailable') {
416
+ return [{ label: quotaDisplayLabel(item), value: 'unavailable' }];
417
+ }
418
+ return [{ label: quotaDisplayLabel(item), value: 'Remaining ?' }];
419
+ });
420
+ if (quotaPairs.length > 0) {
421
+ lines.push('');
422
+ lines.push(fitLine('Quota', width));
423
+ lines.push(...alignPairs(quotaPairs).map((line) => fitLine(line, width)));
424
+ }
425
+ return lines.join('\n');
426
+ }
@@ -0,0 +1,14 @@
1
+ /** Shared type guards, utilities, and debug logging. */
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ export declare function asNumber(value: unknown, fallback: number): number;
4
+ export declare function asNumber(value: unknown): number | undefined;
5
+ export declare function asBoolean(value: unknown, fallback: boolean): boolean;
6
+ export declare function debug(message: string, ...args: unknown[]): void;
7
+ export declare function debugError(context: string, error: unknown): void;
8
+ /** Returns a `.catch()` handler that logs in debug mode and returns undefined. */
9
+ export declare function swallow(context: string): (error: unknown) => undefined;
10
+ /**
11
+ * Run up to `limit` async tasks concurrently from `items`.
12
+ * Returns results in original order.
13
+ */
14
+ export declare function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
@@ -0,0 +1,50 @@
1
+ /** Shared type guards, utilities, and debug logging. */
2
+ export function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ export function asNumber(value, fallback) {
6
+ if (typeof value !== 'number' || !Number.isFinite(value))
7
+ return fallback;
8
+ return value;
9
+ }
10
+ export function asBoolean(value, fallback) {
11
+ if (typeof value !== 'boolean')
12
+ return fallback;
13
+ return value;
14
+ }
15
+ const DEBUG = typeof process !== 'undefined' && process.env.OPENCODE_QUOTA_DEBUG === '1';
16
+ export function debug(message, ...args) {
17
+ if (!DEBUG)
18
+ return;
19
+ console.error(`[quota-sidebar] ${message}`, ...args);
20
+ }
21
+ export function debugError(context, error) {
22
+ if (!DEBUG)
23
+ return;
24
+ const msg = error instanceof Error ? error.message : String(error);
25
+ console.error(`[quota-sidebar] ${context}: ${msg}`);
26
+ }
27
+ /** Returns a `.catch()` handler that logs in debug mode and returns undefined. */
28
+ export function swallow(context) {
29
+ return (error) => {
30
+ debugError(context, error);
31
+ return undefined;
32
+ };
33
+ }
34
+ /**
35
+ * Run up to `limit` async tasks concurrently from `items`.
36
+ * Returns results in original order.
37
+ */
38
+ export async function mapConcurrent(items, limit, fn) {
39
+ const results = new Array(items.length);
40
+ let next = 0;
41
+ async function worker() {
42
+ while (next < items.length) {
43
+ const idx = next++;
44
+ results[idx] = await fn(items[idx], idx);
45
+ }
46
+ }
47
+ const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
48
+ await Promise.all(workers);
49
+ return results;
50
+ }
@@ -0,0 +1,5 @@
1
+ import { type Hooks, type PluginInput } from '@opencode-ai/plugin';
2
+ export declare function QuotaSidebarPlugin(input: PluginInput): Promise<Hooks>;
3
+ export default QuotaSidebarPlugin;
4
+ export type { QuotaSidebarConfig, QuotaSidebarState, QuotaSnapshot, QuotaStatus, SessionState, CachedSessionUsage, CachedProviderUsage, IncrementalCursor, } from './types.js';
5
+ export type { UsageSummary } from './usage.js';