sidekick-shared 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,8 +22,8 @@ npm install sidekick-shared
22
22
  | **Readers** | Read tasks, decisions, notes, history, handoff, and plans from `~/.config/sidekick/` |
23
23
  | **Providers** | Session provider abstraction with Claude Code, OpenCode, and Codex implementations; auto-detection via filesystem |
24
24
  | **Parsers** | JSONL event parsing, OpenCode/Codex format normalization, subagent scanning, session path resolution, debug log parsing |
25
- | **Watchers** | Live session file watching with event bridging |
26
- | **Formatters** | Tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
25
+ | **Watchers** | Live session file watching with event bridging, plus `createJsonlTail()` for raw incremental JSONL consumers |
26
+ | **Formatters** | Display helpers (`formatTokenCount()`, `formatDurationMs()`), tool summary, noise classification, session dump (text/markdown/JSON), event highlighting |
27
27
  | **Search** | Cross-session full-text search, advanced filtering (substring, fuzzy, regex, date) |
28
28
  | **Aggregation** | Event aggregation, frequency tracking, activity heatmaps, pattern extraction |
29
29
  | **Report** | Self-contained HTML session report generation |
@@ -52,6 +52,7 @@ npm install sidekick-shared
52
52
  | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
53
53
  | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
54
54
  | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
55
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
55
56
 
56
57
  ### Browser / webview runtimes
57
58
 
@@ -64,6 +65,8 @@ import {
64
65
  parseModelId,
65
66
  calculateCost,
66
67
  formatCost,
68
+ formatTokenCount,
69
+ formatDurationMs,
67
70
  } from 'sidekick-shared/browser';
68
71
  ```
69
72
 
@@ -177,6 +180,16 @@ const tools = extractToolCalls(event); // ToolCall[] — assistant
177
180
  const toolFromEvent = extractToolCall(event); // ToolCall | null — top-level `tool_use` events
178
181
  ```
179
182
 
183
+ ### Format shared dashboard values
184
+
185
+ ```typescript
186
+ import { formatTokenCount, formatDurationMs, formatCost } from 'sidekick-shared';
187
+
188
+ console.log(formatTokenCount(15_000)); // "15.0k"
189
+ console.log(formatDurationMs(330_000)); // "5m 30s"
190
+ console.log(formatCost(0.0045)); // "$0.0045"
191
+ ```
192
+
180
193
  ### Validate JSONL events with Zod schemas
181
194
 
182
195
  ```typescript
@@ -186,7 +199,25 @@ const parser = new JsonlParser(
186
199
  { onEvent: (e) => console.log(e), onError: (e) => console.warn(e) },
187
200
  { schema: sessionEventSchema },
188
201
  );
189
- parser.addChunk(rawData);
202
+ parser.processChunk(rawData);
203
+ ```
204
+
205
+ ### Tail raw JSONL events incrementally
206
+
207
+ Use `createJsonlTail()` when a consumer needs raw parsed events and owns its own aggregation lifecycle. `onBatchComplete` fires once after each drained byte chunk, which lets callers defer expensive UI or metrics updates until parsing for that chunk is complete.
208
+
209
+ ```typescript
210
+ import { createJsonlTail, sessionEventSchema } from 'sidekick-shared';
211
+
212
+ const tail = createJsonlTail({
213
+ path: '/path/to/session.jsonl',
214
+ schema: sessionEventSchema,
215
+ onEvent: event => aggregator.processEvent(event),
216
+ onBatchComplete: () => renderMetrics(aggregator.getMetrics()),
217
+ onError: error => console.warn(error.message),
218
+ });
219
+
220
+ tail.start();
190
221
  ```
191
222
 
192
223
  ### Poll quota with backoff
@@ -220,6 +251,7 @@ service.onUpdate(({ claude, codex }) => {
220
251
 
221
252
  service.startPolling();
222
253
  // service.setPollingMode('active'); // tighter cadence while a session is live
254
+ // service.updateProviderQuota('codex', codexQuota); // externally push Codex quota snapshots
223
255
  // service.dispose();
224
256
  ```
225
257
 
@@ -264,6 +296,10 @@ const totalSource = mergeCostSources(a.source, b.source); // 'unpriced' wins (le
264
296
  console.log(formatCost(total), totalSource);
265
297
  ```
266
298
 
299
+ ### Deferred Contextful adoption note
300
+
301
+ `sidekick-shared@0.18.x` already exposes the quota primitives Contextful needs: `MultiProviderQuotaService`, `ProviderQuotaMap`, `ProviderQuotaState`, and `CodexQuotaWatcher`. Contextful should keep its local integration unchanged until a newer `sidekick-shared` release is published to npm, then migrate thin wrappers to these public APIs plus `formatTokenCount()`, `formatDurationMs()`, and `createJsonlTail()`.
302
+
267
303
  ## Building
268
304
 
269
305
  ```bash
package/dist/browser.d.ts CHANGED
@@ -8,5 +8,7 @@
8
8
  * For pricing hydration (LiteLLM catalog refresh), use `sidekick-shared/node`.
9
9
  */
10
10
  export { getModelContextWindowSize, DEFAULT_CONTEXT_WINDOW } from './modelContext';
11
+ export { formatDurationMs, formatTokenCount } from './formatting';
12
+ export type { FormatDurationMsOptions, FormatTokenCountOptions } from './formatting';
11
13
  export { parseModelId, getModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, } from './modelInfo';
12
14
  export type { ModelPricing, CostTokenUsage, CostSource, CostProvenanceInput, CostWithProvenance, ModelProvider, ParsedModelId, ModelInfo, ModelDisplayInfo, } from './modelInfo';
package/dist/browser.js CHANGED
@@ -9,10 +9,13 @@
9
9
  * For pricing hydration (LiteLLM catalog refresh), use `sidekick-shared/node`.
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = void 0;
12
+ exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.formatTokenCount = exports.formatDurationMs = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = void 0;
13
13
  var modelContext_1 = require("./modelContext");
14
14
  Object.defineProperty(exports, "getModelContextWindowSize", { enumerable: true, get: function () { return modelContext_1.getModelContextWindowSize; } });
15
15
  Object.defineProperty(exports, "DEFAULT_CONTEXT_WINDOW", { enumerable: true, get: function () { return modelContext_1.DEFAULT_CONTEXT_WINDOW; } });
16
+ var formatting_1 = require("./formatting");
17
+ Object.defineProperty(exports, "formatDurationMs", { enumerable: true, get: function () { return formatting_1.formatDurationMs; } });
18
+ Object.defineProperty(exports, "formatTokenCount", { enumerable: true, get: function () { return formatting_1.formatTokenCount; } });
16
19
  var modelInfo_1 = require("./modelInfo");
17
20
  Object.defineProperty(exports, "parseModelId", { enumerable: true, get: function () { return modelInfo_1.parseModelId; } });
18
21
  Object.defineProperty(exports, "getModelPricing", { enumerable: true, get: function () { return modelInfo_1.getModelPricing; } });
@@ -1,3 +1,43 @@
1
1
  import type { QuotaState } from './quota';
2
+ import { CodexProvider } from './providers/codex';
3
+ import type { ProviderQuotaState } from './providerQuota';
4
+ import type { SavedAccountProfile } from './accountRegistry';
2
5
  import type { CodexRateLimits } from './types/codex';
3
- export declare function quotaFromCodexRateLimits(rateLimits: CodexRateLimits | null | undefined, source?: 'session' | 'cache', capturedAt?: string): QuotaState | null;
6
+ type SnapshotReader = (providerId: 'codex', accountId: string) => QuotaState | null;
7
+ type SnapshotWriter = (providerId: 'codex', accountId: string, quota: QuotaState) => void;
8
+ export type CodexQuotaResolveSource = 'local' | 'api' | 'auto';
9
+ export interface CodexQuotaCreditsSnapshot {
10
+ hasCredits?: boolean;
11
+ unlimited?: boolean;
12
+ balance?: string;
13
+ }
14
+ export interface CodexQuotaResolveOptions {
15
+ workspacePath?: string;
16
+ source?: CodexQuotaResolveSource;
17
+ codexHome?: string;
18
+ provider?: CodexProvider;
19
+ activeAccount?: SavedAccountProfile | null;
20
+ readSnapshot?: SnapshotReader;
21
+ writeSnapshot?: SnapshotWriter;
22
+ maxTailBytes?: number;
23
+ maxSessionFiles?: number;
24
+ fetchImpl?: typeof fetch;
25
+ accessToken?: string;
26
+ }
27
+ export interface CodexQuotaApiOptions {
28
+ codexHome?: string;
29
+ accessToken?: string;
30
+ fetchImpl?: typeof fetch;
31
+ usageUrl?: string;
32
+ capturedAt?: string;
33
+ }
34
+ export declare function quotaFromCodexRateLimits(rateLimits: CodexRateLimits | null | undefined, source?: 'api' | 'session' | 'cache', capturedAt?: string): QuotaState | null;
35
+ export declare function readLatestCodexQuotaFromRollouts(sessionPaths: string[], options?: {
36
+ source?: 'session' | 'cache';
37
+ maxTailBytes?: number;
38
+ maxSessionFiles?: number;
39
+ }): QuotaState | null;
40
+ export declare function resolveCodexQuotaFromLocalSources(options?: CodexQuotaResolveOptions): ProviderQuotaState<'codex'> | null;
41
+ export declare function resolveCodexQuota(options?: CodexQuotaResolveOptions): Promise<ProviderQuotaState<'codex'>>;
42
+ export declare function fetchCodexQuotaFromApi(options?: CodexQuotaApiOptions): Promise<QuotaState>;
43
+ export {};
@@ -1,6 +1,153 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.quotaFromCodexRateLimits = quotaFromCodexRateLimits;
37
+ exports.readLatestCodexQuotaFromRollouts = readLatestCodexQuotaFromRollouts;
38
+ exports.resolveCodexQuotaFromLocalSources = resolveCodexQuotaFromLocalSources;
39
+ exports.resolveCodexQuota = resolveCodexQuota;
40
+ exports.fetchCodexQuotaFromApi = fetchCodexQuotaFromApi;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const codexProfiles_1 = require("./codexProfiles");
44
+ const quotaSnapshots_1 = require("./quotaSnapshots");
45
+ const codex_1 = require("./providers/codex");
46
+ const DEFAULT_TAIL_BYTES = 2 * 1024 * 1024;
47
+ const DEFAULT_MAX_SESSION_FILES = 50;
48
+ const CHATGPT_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
49
+ function normalizePercent(value) {
50
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
51
+ }
52
+ function timestampToIso(seconds) {
53
+ if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0) {
54
+ return '';
55
+ }
56
+ return new Date(seconds * 1000).toISOString();
57
+ }
58
+ function accountEmail(account) {
59
+ return account?.email ?? account?.metadata?.email;
60
+ }
61
+ function enrichCodexQuota(state, account) {
62
+ return {
63
+ ...state,
64
+ runtimeProvider: 'codex',
65
+ providerId: 'codex',
66
+ accountLabel: account?.label,
67
+ accountDetail: accountEmail(account),
68
+ };
69
+ }
70
+ function unavailableCodexQuota(error, account, meta = {}) {
71
+ return enrichCodexQuota({
72
+ fiveHour: { utilization: 0, resetsAt: '' },
73
+ sevenDay: { utilization: 0, resetsAt: '' },
74
+ available: false,
75
+ error,
76
+ providerId: 'codex',
77
+ fiveHourLabel: 'Primary',
78
+ sevenDayLabel: 'Secondary',
79
+ ...meta,
80
+ }, account);
81
+ }
82
+ function parseRetryAfterMs(retryAfter) {
83
+ if (!retryAfter)
84
+ return undefined;
85
+ const seconds = Number(retryAfter);
86
+ if (Number.isFinite(seconds) && seconds >= 0) {
87
+ return Math.round(seconds * 1000);
88
+ }
89
+ const retryAt = Date.parse(retryAfter);
90
+ if (Number.isNaN(retryAt))
91
+ return undefined;
92
+ return Math.max(retryAt - Date.now(), 0);
93
+ }
94
+ function firstString(value) {
95
+ return typeof value === 'string' && value.trim() ? value : undefined;
96
+ }
97
+ function normalizeCredits(credits) {
98
+ if (!credits)
99
+ return undefined;
100
+ return {
101
+ hasCredits: credits.has_credits ?? credits.hasCredits,
102
+ unlimited: credits.unlimited,
103
+ balance: credits.balance ?? undefined,
104
+ };
105
+ }
106
+ function normalizeRateLimitReachedType(value) {
107
+ if (typeof value === 'string')
108
+ return value;
109
+ return firstString(value?.kind);
110
+ }
111
+ function normalizeApiWindow(window) {
112
+ if (!window)
113
+ return undefined;
114
+ const windowMinutes = typeof window.window_minutes === 'number' || window.window_minutes === null
115
+ ? window.window_minutes
116
+ : typeof window.limit_window_seconds === 'number'
117
+ ? Math.round(window.limit_window_seconds / 60)
118
+ : undefined;
119
+ const resetsAt = typeof window.resets_at === 'number' || window.resets_at === null
120
+ ? window.resets_at
121
+ : window.reset_at;
122
+ return {
123
+ used_percent: normalizePercent(window.used_percent),
124
+ window_minutes: windowMinutes,
125
+ resets_at: resetsAt,
126
+ };
127
+ }
128
+ function rateLimitsFromUsagePayload(payload) {
129
+ if (payload.primary || payload.secondary) {
130
+ return {
131
+ limit_id: payload.limit_id ?? 'codex',
132
+ limit_name: payload.limit_name ?? null,
133
+ primary: payload.primary,
134
+ secondary: payload.secondary,
135
+ credits: normalizeCredits(payload.credits),
136
+ plan_type: payload.plan_type ?? undefined,
137
+ rate_limit_reached_type: normalizeRateLimitReachedType(payload.rate_limit_reached_type),
138
+ };
139
+ }
140
+ const preferred = payload.rate_limit;
141
+ return {
142
+ limit_id: 'codex',
143
+ limit_name: null,
144
+ primary: normalizeApiWindow(preferred?.primary_window),
145
+ secondary: normalizeApiWindow(preferred?.secondary_window),
146
+ credits: normalizeCredits(payload.credits),
147
+ plan_type: payload.plan_type ?? undefined,
148
+ rate_limit_reached_type: normalizeRateLimitReachedType(payload.rate_limit_reached_type),
149
+ };
150
+ }
4
151
  function quotaFromCodexRateLimits(rateLimits, source = 'session', capturedAt = new Date().toISOString()) {
5
152
  const primary = rateLimits?.primary;
6
153
  const secondary = rateLimits?.secondary;
@@ -8,10 +155,10 @@ function quotaFromCodexRateLimits(rateLimits, source = 'session', capturedAt = n
8
155
  return null;
9
156
  return {
10
157
  fiveHour: primary
11
- ? { utilization: primary.used_percent, resetsAt: new Date(primary.resets_at * 1000).toISOString() }
158
+ ? { utilization: normalizePercent(primary.used_percent), resetsAt: timestampToIso(primary.resets_at) }
12
159
  : { utilization: 0, resetsAt: '' },
13
160
  sevenDay: secondary
14
- ? { utilization: secondary.used_percent, resetsAt: new Date(secondary.resets_at * 1000).toISOString() }
161
+ ? { utilization: normalizePercent(secondary.used_percent), resetsAt: timestampToIso(secondary.resets_at) }
15
162
  : { utilization: 0, resetsAt: '' },
16
163
  available: true,
17
164
  providerId: 'codex',
@@ -20,5 +167,267 @@ function quotaFromCodexRateLimits(rateLimits, source = 'session', capturedAt = n
20
167
  stale: source === 'cache',
21
168
  fiveHourLabel: 'Primary',
22
169
  sevenDayLabel: 'Secondary',
170
+ limitId: rateLimits?.limit_id,
171
+ limitName: rateLimits?.limit_name ?? undefined,
172
+ credits: rateLimits?.credits,
173
+ planType: rateLimits?.plan_type,
174
+ rateLimitReachedType: rateLimits?.rate_limit_reached_type,
23
175
  };
24
176
  }
177
+ function readLatestCodexQuotaFromRollouts(sessionPaths, options = {}) {
178
+ const maxSessionFiles = options.maxSessionFiles ?? DEFAULT_MAX_SESSION_FILES;
179
+ const maxTailBytes = options.maxTailBytes ?? DEFAULT_TAIL_BYTES;
180
+ for (const sessionPath of sessionPaths.slice(0, maxSessionFiles)) {
181
+ const hit = readLatestQuotaFromRollout(sessionPath, maxTailBytes, options.source ?? 'session');
182
+ if (hit)
183
+ return hit.quota;
184
+ }
185
+ return null;
186
+ }
187
+ function resolveCodexQuotaFromLocalSources(options = {}) {
188
+ const account = options.activeAccount !== undefined ? options.activeAccount : (0, codexProfiles_1.getActiveCodexAccount)();
189
+ const readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
190
+ const writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
191
+ const maxTailBytes = options.maxTailBytes ?? DEFAULT_TAIL_BYTES;
192
+ const maxSessionFiles = options.maxSessionFiles ?? DEFAULT_MAX_SESSION_FILES;
193
+ let ownProvider = false;
194
+ const provider = options.provider ?? (() => {
195
+ ownProvider = true;
196
+ return new codex_1.CodexProvider();
197
+ })();
198
+ try {
199
+ const workspaceSessions = options.workspacePath ? provider.findAllSessions(options.workspacePath) : [];
200
+ const workspaceQuota = readLatestCodexQuotaFromRollouts(workspaceSessions, { maxTailBytes, maxSessionFiles });
201
+ if (workspaceQuota) {
202
+ if (account)
203
+ writeSnapshot('codex', account.id, workspaceQuota);
204
+ return enrichCodexQuota(workspaceQuota, account);
205
+ }
206
+ const codexHome = options.codexHome ?? (0, codexProfiles_1.resolveSidekickCodexHome)();
207
+ const accountSessions = findRolloutFiles(path.join(codexHome, 'sessions'));
208
+ const accountQuota = readLatestCodexQuotaFromRollouts(accountSessions, { maxTailBytes, maxSessionFiles });
209
+ if (accountQuota) {
210
+ if (account)
211
+ writeSnapshot('codex', account.id, accountQuota);
212
+ return enrichCodexQuota(accountQuota, account);
213
+ }
214
+ const cached = account ? readSnapshot('codex', account.id) : null;
215
+ if (cached) {
216
+ return enrichCodexQuota({
217
+ ...cached,
218
+ providerId: 'codex',
219
+ source: 'cache',
220
+ stale: true,
221
+ fiveHourLabel: cached.fiveHourLabel ?? 'Primary',
222
+ sevenDayLabel: cached.sevenDayLabel ?? 'Secondary',
223
+ }, account);
224
+ }
225
+ }
226
+ finally {
227
+ if (ownProvider)
228
+ provider.dispose();
229
+ }
230
+ return null;
231
+ }
232
+ async function resolveCodexQuota(options = {}) {
233
+ const source = options.source ?? 'local';
234
+ const account = options.activeAccount !== undefined ? options.activeAccount : (0, codexProfiles_1.getActiveCodexAccount)();
235
+ const writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
236
+ if (source === 'api') {
237
+ const apiQuota = await fetchCodexQuotaFromApi(options);
238
+ if (apiQuota.available) {
239
+ if (account)
240
+ writeSnapshot('codex', account.id, apiQuota);
241
+ return enrichCodexQuota(apiQuota, account);
242
+ }
243
+ const fallback = resolveCodexQuotaFromLocalSources(options);
244
+ return fallback ?? enrichCodexQuota(apiQuota, account);
245
+ }
246
+ const local = resolveCodexQuotaFromLocalSources(options);
247
+ if (local)
248
+ return local;
249
+ if (source === 'auto') {
250
+ const apiQuota = await fetchCodexQuotaFromApi(options);
251
+ if (apiQuota.available && account) {
252
+ writeSnapshot('codex', account.id, apiQuota);
253
+ }
254
+ return enrichCodexQuota(apiQuota, account);
255
+ }
256
+ return unavailableCodexQuota(account
257
+ ? `No Codex rate-limit data is available for "${account.label ?? account.id}".`
258
+ : 'No Codex rate-limit data is available.', account, { source: 'session' });
259
+ }
260
+ async function fetchCodexQuotaFromApi(options = {}) {
261
+ const capturedAt = options.capturedAt ?? new Date().toISOString();
262
+ const accessToken = options.accessToken ?? readCodexAccessToken(options.codexHome ?? (0, codexProfiles_1.resolveSidekickCodexHome)());
263
+ if (!accessToken) {
264
+ return {
265
+ fiveHour: { utilization: 0, resetsAt: '' },
266
+ sevenDay: { utilization: 0, resetsAt: '' },
267
+ available: false,
268
+ error: 'Codex API refresh requires a ChatGPT login.',
269
+ failureKind: 'auth',
270
+ providerId: 'codex',
271
+ source: 'api',
272
+ capturedAt,
273
+ fiveHourLabel: 'Primary',
274
+ sevenDayLabel: 'Secondary',
275
+ };
276
+ }
277
+ try {
278
+ const fetchImpl = options.fetchImpl ?? fetch;
279
+ const response = await fetchImpl(options.usageUrl ?? CHATGPT_USAGE_URL, {
280
+ method: 'GET',
281
+ headers: {
282
+ Authorization: `Bearer ${accessToken}`,
283
+ Accept: 'application/json',
284
+ },
285
+ });
286
+ if (!response.ok) {
287
+ return {
288
+ fiveHour: { utilization: 0, resetsAt: '' },
289
+ sevenDay: { utilization: 0, resetsAt: '' },
290
+ available: false,
291
+ error: `Codex usage API error: ${response.status}`,
292
+ failureKind: response.status === 401 || response.status === 403
293
+ ? 'auth'
294
+ : response.status === 429
295
+ ? 'rate_limit'
296
+ : response.status >= 500 && response.status <= 599
297
+ ? 'server'
298
+ : 'unknown',
299
+ httpStatus: response.status,
300
+ retryAfterMs: response.status === 429 ? parseRetryAfterMs(response.headers.get('retry-after')) : undefined,
301
+ providerId: 'codex',
302
+ source: 'api',
303
+ capturedAt,
304
+ fiveHourLabel: 'Primary',
305
+ sevenDayLabel: 'Secondary',
306
+ };
307
+ }
308
+ const payload = await response.json();
309
+ const quota = quotaFromCodexRateLimits(rateLimitsFromUsagePayload(payload), 'api', capturedAt);
310
+ if (!quota) {
311
+ return {
312
+ fiveHour: { utilization: 0, resetsAt: '' },
313
+ sevenDay: { utilization: 0, resetsAt: '' },
314
+ available: false,
315
+ error: 'Codex usage API returned no rate-limit windows.',
316
+ failureKind: 'unknown',
317
+ providerId: 'codex',
318
+ source: 'api',
319
+ capturedAt,
320
+ fiveHourLabel: 'Primary',
321
+ sevenDayLabel: 'Secondary',
322
+ };
323
+ }
324
+ return quota;
325
+ }
326
+ catch {
327
+ return {
328
+ fiveHour: { utilization: 0, resetsAt: '' },
329
+ sevenDay: { utilization: 0, resetsAt: '' },
330
+ available: false,
331
+ error: 'Codex usage API network error',
332
+ failureKind: 'network',
333
+ providerId: 'codex',
334
+ source: 'api',
335
+ capturedAt,
336
+ fiveHourLabel: 'Primary',
337
+ sevenDayLabel: 'Secondary',
338
+ };
339
+ }
340
+ }
341
+ function readCodexAccessToken(codexHome) {
342
+ try {
343
+ const parsed = JSON.parse(fs.readFileSync(path.join(codexHome, 'auth.json'), 'utf8'));
344
+ if (parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key')
345
+ return null;
346
+ return parsed.tokens?.access_token || null;
347
+ }
348
+ catch {
349
+ return null;
350
+ }
351
+ }
352
+ function readLatestQuotaFromRollout(sessionPath, maxTailBytes, source) {
353
+ let fd = null;
354
+ try {
355
+ const stat = fs.statSync(sessionPath);
356
+ if (!stat.isFile() || stat.size <= 0)
357
+ return null;
358
+ const start = Math.max(0, stat.size - maxTailBytes);
359
+ const bytesToRead = stat.size - start;
360
+ const buffer = Buffer.alloc(bytesToRead);
361
+ fd = fs.openSync(sessionPath, 'r');
362
+ const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, start);
363
+ fs.closeSync(fd);
364
+ fd = null;
365
+ let text = buffer.toString('utf8', 0, bytesRead);
366
+ if (start > 0) {
367
+ const firstNewline = text.indexOf('\n');
368
+ text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
369
+ }
370
+ const lines = text.split('\n');
371
+ for (let i = lines.length - 1; i >= 0; i--) {
372
+ const line = lines[i].trim();
373
+ if (!line || !line.includes('rate_limits'))
374
+ continue;
375
+ try {
376
+ const parsed = JSON.parse(line);
377
+ if (parsed.type !== 'event_msg' || parsed.payload?.type !== 'token_count')
378
+ continue;
379
+ const quota = quotaFromCodexRateLimits(parsed.payload.rate_limits, source, parsed.timestamp ?? new Date(stat.mtime).toISOString());
380
+ if (quota)
381
+ return { quota, filePath: sessionPath };
382
+ }
383
+ catch {
384
+ // Ignore malformed or partial lines.
385
+ }
386
+ }
387
+ }
388
+ catch {
389
+ return null;
390
+ }
391
+ finally {
392
+ if (fd !== null) {
393
+ try {
394
+ fs.closeSync(fd);
395
+ }
396
+ catch { /* ignore */ }
397
+ }
398
+ }
399
+ return null;
400
+ }
401
+ function findRolloutFiles(sessionsDir) {
402
+ const results = [];
403
+ function visit(dir) {
404
+ let entries;
405
+ try {
406
+ entries = fs.readdirSync(dir, { withFileTypes: true });
407
+ }
408
+ catch {
409
+ return;
410
+ }
411
+ for (const entry of entries) {
412
+ const fullPath = path.join(dir, entry.name);
413
+ if (entry.isDirectory()) {
414
+ visit(fullPath);
415
+ continue;
416
+ }
417
+ if (!entry.isFile() || !entry.name.startsWith('rollout-') || !entry.name.endsWith('.jsonl'))
418
+ continue;
419
+ try {
420
+ const stat = fs.statSync(fullPath);
421
+ if (stat.size > 0) {
422
+ results.push({ path: fullPath, mtime: stat.mtime.getTime() });
423
+ }
424
+ }
425
+ catch {
426
+ // Skip inaccessible files.
427
+ }
428
+ }
429
+ }
430
+ visit(sessionsDir);
431
+ results.sort((a, b) => b.mtime - a.mtime);
432
+ return results.map(item => item.path);
433
+ }
@@ -11,6 +11,8 @@ type SnapshotWriter = (providerId: 'codex', accountId: string, quota: QuotaState
11
11
  type WatchFile = (filename: fs.PathLike, listener: fs.WatchListener<string>) => FSWatcher;
12
12
  export interface CodexQuotaWatcherOptions {
13
13
  discoveryPollIntervalMs?: number;
14
+ maxTailBytes?: number;
15
+ maxSessionFiles?: number;
14
16
  providerFactory?: () => CodexProvider;
15
17
  getActiveAccount?: CodexAccountReader;
16
18
  readSnapshot?: SnapshotReader;
@@ -29,6 +31,8 @@ export declare class CodexQuotaWatcher implements Disposable {
29
31
  private readonly readSnapshot;
30
32
  private readonly writeSnapshot;
31
33
  private readonly watchFile;
34
+ private readonly maxTailBytes;
35
+ private readonly maxSessionFiles;
32
36
  private readonly listeners;
33
37
  private discoveryTimer;
34
38
  private provider;
@@ -78,6 +78,8 @@ class CodexQuotaWatcher {
78
78
  readSnapshot;
79
79
  writeSnapshot;
80
80
  watchFile;
81
+ maxTailBytes;
82
+ maxSessionFiles;
81
83
  listeners = [];
82
84
  discoveryTimer;
83
85
  provider = null;
@@ -94,6 +96,8 @@ class CodexQuotaWatcher {
94
96
  this.readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
95
97
  this.writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
96
98
  this.watchFile = options.watchFile ?? fs.watch;
99
+ this.maxTailBytes = options.maxTailBytes;
100
+ this.maxSessionFiles = options.maxSessionFiles;
97
101
  }
98
102
  start() {
99
103
  if (this.running)
@@ -201,6 +205,29 @@ class CodexQuotaWatcher {
201
205
  }
202
206
  emitCachedOrUnavailable() {
203
207
  const account = this.getActiveAccount();
208
+ let localProvider = null;
209
+ try {
210
+ localProvider = this.providerFactory();
211
+ const local = (0, codexQuota_1.resolveCodexQuotaFromLocalSources)({
212
+ workspacePath: this.workspacePath,
213
+ activeAccount: account,
214
+ readSnapshot: this.readSnapshot,
215
+ writeSnapshot: this.writeSnapshot,
216
+ provider: localProvider,
217
+ maxTailBytes: this.maxTailBytes,
218
+ maxSessionFiles: this.maxSessionFiles,
219
+ });
220
+ if (local) {
221
+ this.emitState(local);
222
+ return;
223
+ }
224
+ }
225
+ catch {
226
+ // Fall through to account-scoped cache or unavailable state.
227
+ }
228
+ finally {
229
+ localProvider?.dispose();
230
+ }
204
231
  const cached = account ? this.readSnapshot('codex', account.id) : null;
205
232
  if (cached) {
206
233
  this.emitState(enrichQuotaState({
@@ -15,6 +15,7 @@ exports.fmtTokens = fmtTokens;
15
15
  exports.fmtCost = fmtCost;
16
16
  exports.formatTimestamp = formatTimestamp;
17
17
  exports.formatDuration = formatDuration;
18
+ const formatting_1 = require("../formatting");
18
19
  // ── JSON output ──
19
20
  /**
20
21
  * Serialize aggregated metrics as pretty-printed JSON.
@@ -238,11 +239,7 @@ function formatTokenSummary(metrics) {
238
239
  }
239
240
  /** @internal Exported for tests. */
240
241
  function fmtTokens(n) {
241
- if (n >= 1_000_000)
242
- return `${(n / 1_000_000).toFixed(1)}M`;
243
- if (n >= 1_000)
244
- return `${(n / 1_000).toFixed(1)}k`;
245
- return String(n);
242
+ return (0, formatting_1.formatTokenCount)(n);
246
243
  }
247
244
  /** @internal Exported for tests. */
248
245
  function fmtCost(cost) {
@@ -273,17 +270,7 @@ function formatDuration(start, end) {
273
270
  return 'N/A';
274
271
  try {
275
272
  const ms = new Date(end).getTime() - new Date(start).getTime();
276
- if (ms < 0)
277
- return 'N/A';
278
- const totalSec = Math.floor(ms / 1000);
279
- const hours = Math.floor(totalSec / 3600);
280
- const minutes = Math.floor((totalSec % 3600) / 60);
281
- const seconds = totalSec % 60;
282
- if (hours > 0)
283
- return `${hours}h ${minutes}m ${seconds}s`;
284
- if (minutes > 0)
285
- return `${minutes}m ${seconds}s`;
286
- return `${seconds}s`;
273
+ return (0, formatting_1.formatDurationMs)(ms);
287
274
  }
288
275
  catch {
289
276
  return 'N/A';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Pure display formatting helpers shared by CLI, extension host, webviews,
3
+ * and downstream apps.
4
+ *
5
+ * This module intentionally has no Node-only imports.
6
+ *
7
+ * @module formatting
8
+ */
9
+ export interface FormatTokenCountOptions {
10
+ /** Suffix casing for thousands. Millions always use `M`. Defaults to `lower`. */
11
+ suffixCase?: 'lower' | 'upper';
12
+ }
13
+ export interface FormatDurationMsOptions {
14
+ /** `spaced`: `5m 30s`; `compact`: `5m30s`. Defaults to `spaced`. */
15
+ style?: 'spaced' | 'compact';
16
+ /** Render sub-second durations as `123ms` instead of `0.1s`. Defaults to false. */
17
+ includeMilliseconds?: boolean;
18
+ /** Decimal places for durations under one minute. Defaults to 0. */
19
+ secondsFractionDigits?: number;
20
+ /** Value to return for negative, non-finite, or otherwise invalid durations. */
21
+ invalid?: string;
22
+ }
23
+ /** Format a token count with compact `k` / `M` suffixes. */
24
+ export declare function formatTokenCount(value: number, options?: FormatTokenCountOptions): string;
25
+ /** Format an elapsed duration in milliseconds. */
26
+ export declare function formatDurationMs(ms: number, options?: FormatDurationMsOptions): string;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * Pure display formatting helpers shared by CLI, extension host, webviews,
4
+ * and downstream apps.
5
+ *
6
+ * This module intentionally has no Node-only imports.
7
+ *
8
+ * @module formatting
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.formatTokenCount = formatTokenCount;
12
+ exports.formatDurationMs = formatDurationMs;
13
+ /** Format a token count with compact `k` / `M` suffixes. */
14
+ function formatTokenCount(value, options = {}) {
15
+ if (!Number.isFinite(value))
16
+ return '0';
17
+ const n = Math.trunc(value);
18
+ const sign = n < 0 ? '-' : '';
19
+ const abs = Math.abs(n);
20
+ const thousandsSuffix = options.suffixCase === 'upper' ? 'K' : 'k';
21
+ if (abs >= 1_000_000)
22
+ return `${sign}${(abs / 1_000_000).toFixed(1)}M`;
23
+ if (abs >= 1_000)
24
+ return `${sign}${(abs / 1_000).toFixed(1)}${thousandsSuffix}`;
25
+ return `${n}`;
26
+ }
27
+ /** Format an elapsed duration in milliseconds. */
28
+ function formatDurationMs(ms, options = {}) {
29
+ const invalid = options.invalid ?? 'N/A';
30
+ if (!Number.isFinite(ms) || ms < 0)
31
+ return invalid;
32
+ const style = options.style ?? 'spaced';
33
+ const separator = style === 'compact' ? '' : ' ';
34
+ const secondsFractionDigits = options.secondsFractionDigits ?? 0;
35
+ if (ms < 1000) {
36
+ if (options.includeMilliseconds)
37
+ return `${Math.round(ms)}ms`;
38
+ return `${(ms / 1000).toFixed(Math.max(1, secondsFractionDigits))}s`;
39
+ }
40
+ const seconds = ms / 1000;
41
+ if (seconds < 60) {
42
+ if (secondsFractionDigits > 0)
43
+ return `${seconds.toFixed(secondsFractionDigits)}s`;
44
+ return `${Math.floor(seconds)}s`;
45
+ }
46
+ const totalSeconds = Math.floor(seconds);
47
+ const hours = Math.floor(totalSeconds / 3600);
48
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
49
+ const remainingSeconds = totalSeconds % 60;
50
+ if (hours > 0) {
51
+ return `${hours}h${separator}${minutes}m${separator}${remainingSeconds}s`;
52
+ }
53
+ return `${minutes}m${separator}${remainingSeconds}s`;
54
+ }
package/dist/index.d.ts CHANGED
@@ -58,6 +58,10 @@ export type { FollowEvent, FollowEventType, SessionWatcher, SessionWatcherCallba
58
58
  export { createWatcher } from './watchers/factory';
59
59
  export type { CreateWatcherOptions } from './watchers/factory';
60
60
  export { toFollowEvents } from './watchers/eventBridge';
61
+ export { createJsonlTail } from './watchers/jsonlTail';
62
+ export type { JsonlTail, JsonlTailBatch, JsonlTailOptions } from './watchers/jsonlTail';
63
+ export { formatDurationMs, formatTokenCount } from './formatting';
64
+ export type { FormatDurationMsOptions, FormatTokenCountOptions } from './formatting';
61
65
  export { formatToolSummary } from './formatters/toolSummary';
62
66
  export { isHardNoise, isHardNoiseFollowEvent, getSoftNoiseReason, classifyMessage, classifyFollowEvent, shouldMergeWithPrevious, classifyNoise, } from './formatters/noiseClassifier';
63
67
  export type { MessageClassification, NoiseResult } from './formatters/noiseClassifier';
@@ -100,7 +104,8 @@ export type { QuotaFailureDescriptor } from './quotaPresentation';
100
104
  export { QuotaPoller } from './quotaPoller';
101
105
  export type { QuotaPollerOptions } from './quotaPoller';
102
106
  export { readQuotaSnapshot, writeQuotaSnapshot } from './quotaSnapshots';
103
- export { quotaFromCodexRateLimits } from './codexQuota';
107
+ export { fetchCodexQuotaFromApi, quotaFromCodexRateLimits, readLatestCodexQuotaFromRollouts, resolveCodexQuota, resolveCodexQuotaFromLocalSources, } from './codexQuota';
108
+ export type { CodexQuotaApiOptions, CodexQuotaCreditsSnapshot, CodexQuotaResolveOptions, CodexQuotaResolveSource, } from './codexQuota';
104
109
  export { CodexQuotaWatcher } from './codexQuotaWatcher';
105
110
  export type { CodexQuotaWatcherOptions } from './codexQuotaWatcher';
106
111
  export { MultiProviderQuotaService } from './multiProviderQuotaService';
package/dist/index.js CHANGED
@@ -4,9 +4,9 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.findActiveClaudeSession = exports.discoverSessionDirectory = exports.getClaudeSessionDirectory = exports.encodeClaudeWorkspacePath = exports.detectSessionActivity = exports.extractTaskInfo = exports.scanSubagentDir = exports.normalizeCodexToolInput = exports.normalizeCodexToolName = exports.extractPatchFilePaths = exports.CodexRolloutParser = exports.parseDbPartData = exports.parseDbMessageData = exports.convertOpenCodeMessage = exports.detectPlanModeFromText = exports.normalizeToolInput = exports.normalizeToolName = exports.TRUNCATION_PATTERNS = exports.JsonlParser = exports.CodexProvider = exports.OpenCodeProvider = exports.ClaudeCodeProvider = exports.getAllDetectedProviders = exports.detectProvider = exports.readClaudeCodePlanFiles = exports.getPlanAnalytics = exports.writePlans = exports.getLatestPlan = exports.readPlans = exports.readLatestHandoff = exports.readHistory = exports.readNotes = exports.readDecisions = exports.readTasks = exports.getProjectSlugRaw = exports.getProjectSlug = exports.encodeWorkspacePath = exports.getGlobalDataPath = exports.getProjectDataPath = exports.getConfigDir = exports.MAX_PLANS_PER_PROJECT = exports.PLAN_SCHEMA_VERSION = exports.createEmptyTokenTotals = exports.HISTORICAL_DATA_SCHEMA_VERSION = exports.STALENESS_THRESHOLDS = exports.IMPORTANCE_DECAY_FACTORS = exports.KNOWLEDGE_NOTE_SCHEMA_VERSION = exports.DECISION_LOG_SCHEMA_VERSION = exports.normalizeTaskStatus = exports.TASK_PERSISTENCE_SCHEMA_VERSION = void 0;
7
- exports.FrequencyTracker = exports.getSnapshotPath = exports.isSnapshotValid = exports.deleteSnapshot = exports.loadSnapshot = exports.saveSnapshot = exports.parseTodoDependencies = exports.EventAggregator = exports.getRandomPhrase = exports.PHRASE_CATEGORIES = exports.ALL_PHRASES = exports.HIGHLIGHT_CSS = exports.clearHighlightCache = exports.highlightEvent = exports.formatSessionJson = exports.formatSessionMarkdown = exports.formatSessionText = exports.classifyNoise = exports.shouldMergeWithPrevious = exports.classifyFollowEvent = exports.classifyMessage = exports.getSoftNoiseReason = exports.isHardNoiseFollowEvent = exports.isHardNoise = exports.formatToolSummary = exports.toFollowEvents = exports.createWatcher = exports.parseChangelog = exports.extractProposedPlanShared = exports.parsePlanMarkdownShared = exports.PlanExtractor = exports.composeContext = exports.FilterEngine = exports.searchSessions = exports.CodexDatabase = exports.OpenCodeDatabase = exports.discoverDebugLogs = exports.collapseDuplicates = exports.filterByLevel = exports.parseDebugLog = exports.scanSubagentTraces = exports.findAllSessionsWithWorktrees = exports.discoverWorktreeSiblings = exports.resolveWorktreeMainRepo = exports.getAllClaudeProjectFolders = exports.decodeEncodedPath = exports.getMostRecentlyActiveSessionDir = exports.findSubdirectorySessionDirs = exports.findSessionsInDirectory = exports.findAllClaudeSessions = void 0;
8
- exports.getModelContextWindowSize = exports.MultiProviderQuotaService = exports.CodexQuotaWatcher = exports.quotaFromCodexRateLimits = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.QuotaPoller = exports.describeQuotaFailure = exports.fetchQuota = exports.removeCodexAccount = exports.switchToCodexAccount = exports.finalizeCodexAccount = exports.prepareCodexAccount = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.getActiveCodexAccount = exports.listCodexAccounts = exports.getSystemCodexHome = exports.getCodexMonitoringHomes = exports.getCodexProfileHome = exports.getCodexProfilesDir = exports.getActiveAccountStatus = exports.removeSavedAccountProfile = exports.replaceSavedAccountProfiles = exports.setActiveSavedAccount = exports.upsertSavedAccountProfile = exports.getActiveSavedAccount = exports.listSavedAccountProfiles = exports.writeSavedAccountRegistry = exports.readSavedAccountRegistry = exports.getAccountsDir = exports.isMultiAccountEnabled = exports.getActiveAccount = exports.listAccounts = exports.removeAccount = exports.switchToAccount = exports.addCurrentAccount = exports.readActiveClaudeAccount = exports.writeAccountRegistry = exports.readAccountRegistry = exports.ensureDefaultAccounts = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.writeActiveCredentials = exports.readActiveCredentials = exports.openInBrowser = exports.parseTranscript = exports.generateHtmlReport = exports.PatternExtractor = exports.HeatmapTracker = void 0;
9
- exports.fetchPeakHoursStatus = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.extractToolCalls = exports.extractToolCall = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = void 0;
7
+ exports.deleteSnapshot = exports.loadSnapshot = exports.saveSnapshot = exports.parseTodoDependencies = exports.EventAggregator = exports.getRandomPhrase = exports.PHRASE_CATEGORIES = exports.ALL_PHRASES = exports.HIGHLIGHT_CSS = exports.clearHighlightCache = exports.highlightEvent = exports.formatSessionJson = exports.formatSessionMarkdown = exports.formatSessionText = exports.classifyNoise = exports.shouldMergeWithPrevious = exports.classifyFollowEvent = exports.classifyMessage = exports.getSoftNoiseReason = exports.isHardNoiseFollowEvent = exports.isHardNoise = exports.formatToolSummary = exports.formatTokenCount = exports.formatDurationMs = exports.createJsonlTail = exports.toFollowEvents = exports.createWatcher = exports.parseChangelog = exports.extractProposedPlanShared = exports.parsePlanMarkdownShared = exports.PlanExtractor = exports.composeContext = exports.FilterEngine = exports.searchSessions = exports.CodexDatabase = exports.OpenCodeDatabase = exports.discoverDebugLogs = exports.collapseDuplicates = exports.filterByLevel = exports.parseDebugLog = exports.scanSubagentTraces = exports.findAllSessionsWithWorktrees = exports.discoverWorktreeSiblings = exports.resolveWorktreeMainRepo = exports.getAllClaudeProjectFolders = exports.decodeEncodedPath = exports.getMostRecentlyActiveSessionDir = exports.findSubdirectorySessionDirs = exports.findSessionsInDirectory = exports.findAllClaudeSessions = void 0;
8
+ exports.fetchCodexQuotaFromApi = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.QuotaPoller = exports.describeQuotaFailure = exports.fetchQuota = exports.removeCodexAccount = exports.switchToCodexAccount = exports.finalizeCodexAccount = exports.prepareCodexAccount = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.getActiveCodexAccount = exports.listCodexAccounts = exports.getSystemCodexHome = exports.getCodexMonitoringHomes = exports.getCodexProfileHome = exports.getCodexProfilesDir = exports.getActiveAccountStatus = exports.removeSavedAccountProfile = exports.replaceSavedAccountProfiles = exports.setActiveSavedAccount = exports.upsertSavedAccountProfile = exports.getActiveSavedAccount = exports.listSavedAccountProfiles = exports.writeSavedAccountRegistry = exports.readSavedAccountRegistry = exports.getAccountsDir = exports.isMultiAccountEnabled = exports.getActiveAccount = exports.listAccounts = exports.removeAccount = exports.switchToAccount = exports.addCurrentAccount = exports.readActiveClaudeAccount = exports.writeAccountRegistry = exports.readAccountRegistry = exports.ensureDefaultAccounts = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.writeActiveCredentials = exports.readActiveCredentials = exports.openInBrowser = exports.parseTranscript = exports.generateHtmlReport = exports.PatternExtractor = exports.HeatmapTracker = exports.FrequencyTracker = exports.getSnapshotPath = exports.isSnapshotValid = void 0;
9
+ exports.fetchPeakHoursStatus = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.extractToolCalls = exports.extractToolCall = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = exports.MultiProviderQuotaService = exports.CodexQuotaWatcher = exports.resolveCodexQuotaFromLocalSources = exports.resolveCodexQuota = exports.readLatestCodexQuotaFromRollouts = exports.quotaFromCodexRateLimits = void 0;
10
10
  var taskPersistence_1 = require("./types/taskPersistence");
11
11
  Object.defineProperty(exports, "TASK_PERSISTENCE_SCHEMA_VERSION", { enumerable: true, get: function () { return taskPersistence_1.TASK_PERSISTENCE_SCHEMA_VERSION; } });
12
12
  Object.defineProperty(exports, "normalizeTaskStatus", { enumerable: true, get: function () { return taskPersistence_1.normalizeTaskStatus; } });
@@ -130,7 +130,12 @@ var factory_1 = require("./watchers/factory");
130
130
  Object.defineProperty(exports, "createWatcher", { enumerable: true, get: function () { return factory_1.createWatcher; } });
131
131
  var eventBridge_1 = require("./watchers/eventBridge");
132
132
  Object.defineProperty(exports, "toFollowEvents", { enumerable: true, get: function () { return eventBridge_1.toFollowEvents; } });
133
+ var jsonlTail_1 = require("./watchers/jsonlTail");
134
+ Object.defineProperty(exports, "createJsonlTail", { enumerable: true, get: function () { return jsonlTail_1.createJsonlTail; } });
133
135
  // Formatters
136
+ var formatting_1 = require("./formatting");
137
+ Object.defineProperty(exports, "formatDurationMs", { enumerable: true, get: function () { return formatting_1.formatDurationMs; } });
138
+ Object.defineProperty(exports, "formatTokenCount", { enumerable: true, get: function () { return formatting_1.formatTokenCount; } });
134
139
  var toolSummary_1 = require("./formatters/toolSummary");
135
140
  Object.defineProperty(exports, "formatToolSummary", { enumerable: true, get: function () { return toolSummary_1.formatToolSummary; } });
136
141
  var noiseClassifier_1 = require("./formatters/noiseClassifier");
@@ -233,7 +238,11 @@ var quotaSnapshots_1 = require("./quotaSnapshots");
233
238
  Object.defineProperty(exports, "readQuotaSnapshot", { enumerable: true, get: function () { return quotaSnapshots_1.readQuotaSnapshot; } });
234
239
  Object.defineProperty(exports, "writeQuotaSnapshot", { enumerable: true, get: function () { return quotaSnapshots_1.writeQuotaSnapshot; } });
235
240
  var codexQuota_1 = require("./codexQuota");
241
+ Object.defineProperty(exports, "fetchCodexQuotaFromApi", { enumerable: true, get: function () { return codexQuota_1.fetchCodexQuotaFromApi; } });
236
242
  Object.defineProperty(exports, "quotaFromCodexRateLimits", { enumerable: true, get: function () { return codexQuota_1.quotaFromCodexRateLimits; } });
243
+ Object.defineProperty(exports, "readLatestCodexQuotaFromRollouts", { enumerable: true, get: function () { return codexQuota_1.readLatestCodexQuotaFromRollouts; } });
244
+ Object.defineProperty(exports, "resolveCodexQuota", { enumerable: true, get: function () { return codexQuota_1.resolveCodexQuota; } });
245
+ Object.defineProperty(exports, "resolveCodexQuotaFromLocalSources", { enumerable: true, get: function () { return codexQuota_1.resolveCodexQuotaFromLocalSources; } });
237
246
  var codexQuotaWatcher_1 = require("./codexQuotaWatcher");
238
247
  Object.defineProperty(exports, "CodexQuotaWatcher", { enumerable: true, get: function () { return codexQuotaWatcher_1.CodexQuotaWatcher; } });
239
248
  var multiProviderQuotaService_1 = require("./multiProviderQuotaService");
@@ -45,7 +45,7 @@ class CodexDatabase {
45
45
  dbPath;
46
46
  sqlite3Available = null;
47
47
  constructor(codexHome) {
48
- this.dbPath = path.join(codexHome, 'state.sqlite');
48
+ this.dbPath = findLatestStateDatabase(codexHome) ?? path.join(codexHome, 'state.sqlite');
49
49
  }
50
50
  isAvailable() {
51
51
  try {
@@ -138,6 +138,31 @@ class CodexDatabase {
138
138
  }
139
139
  }
140
140
  exports.CodexDatabase = CodexDatabase;
141
+ function findLatestStateDatabase(codexHome) {
142
+ try {
143
+ const entries = fs.readdirSync(codexHome, { withFileTypes: true });
144
+ const candidates = [];
145
+ for (const entry of entries) {
146
+ if (!entry.isFile() || !/^state(?:_\d+)?\.sqlite$/.test(entry.name))
147
+ continue;
148
+ const dbPath = path.join(codexHome, entry.name);
149
+ try {
150
+ const stat = fs.statSync(dbPath);
151
+ if (stat.size > 0) {
152
+ candidates.push({ path: dbPath, mtime: stat.mtime.getTime() });
153
+ }
154
+ }
155
+ catch {
156
+ // Skip inaccessible candidates.
157
+ }
158
+ }
159
+ candidates.sort((a, b) => b.mtime - a.mtime);
160
+ return candidates[0]?.path ?? null;
161
+ }
162
+ catch {
163
+ return null;
164
+ }
165
+ }
141
166
  function normalizePath(input) {
142
167
  try {
143
168
  return fs.realpathSync(input);
@@ -58,6 +58,35 @@ function getOpenCodeDataDir() {
58
58
  function getCodexHomes() {
59
59
  return (0, codexProfiles_1.getCodexMonitoringHomes)();
60
60
  }
61
+ function hasCodexStateDb(codexHome) {
62
+ try {
63
+ return fs.readdirSync(codexHome).some(entry => /^state(?:_\d+)?\.sqlite$/.test(entry));
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function getCodexStateDbMtime(codexHome) {
70
+ try {
71
+ let latest = 0;
72
+ for (const entry of fs.readdirSync(codexHome)) {
73
+ if (!/^state(?:_\d+)?\.sqlite$/.test(entry))
74
+ continue;
75
+ try {
76
+ const mtime = fs.statSync(path.join(codexHome, entry)).mtime.getTime();
77
+ if (mtime > latest)
78
+ latest = mtime;
79
+ }
80
+ catch {
81
+ // Skip inaccessible files.
82
+ }
83
+ }
84
+ return latest;
85
+ }
86
+ catch {
87
+ return 0;
88
+ }
89
+ }
61
90
  function getMostRecentMtime(dir) {
62
91
  try {
63
92
  if (!fs.existsSync(dir))
@@ -97,13 +126,9 @@ function getOpenCodeActivityMtime() {
97
126
  function getCodexActivityMtime() {
98
127
  let latest = 0;
99
128
  for (const codexHome of getCodexHomes()) {
100
- const dbPath = path.join(codexHome, 'state.sqlite');
101
- try {
102
- const dbMtime = fs.statSync(dbPath).mtime.getTime();
103
- if (dbMtime > latest)
104
- latest = dbMtime;
105
- }
106
- catch { /* no DB */ }
129
+ const dbMtime = getCodexStateDbMtime(codexHome);
130
+ if (dbMtime > latest)
131
+ latest = dbMtime;
107
132
  const sessionsMtime = getMostRecentMtime(path.join(codexHome, 'sessions'));
108
133
  if (sessionsMtime > latest)
109
134
  latest = sessionsMtime;
@@ -129,7 +154,7 @@ function getAllDetectedProviders() {
129
154
  const { claudeBase, openCodeDbPath, openCodeStorageDir, codexHomes } = getProviderPaths();
130
155
  const hasClaude = fs.existsSync(claudeBase);
131
156
  const hasOpenCode = fs.existsSync(openCodeStorageDir) || fs.existsSync(openCodeDbPath);
132
- const hasCodex = codexHomes.some(codexHome => fs.existsSync(path.join(codexHome, 'sessions')) || fs.existsSync(path.join(codexHome, 'state.sqlite')));
157
+ const hasCodex = codexHomes.some(codexHome => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
133
158
  const available = [];
134
159
  if (hasClaude)
135
160
  available.push({ id: 'claude-code', mtime: getMostRecentMtime(claudeBase) });
@@ -150,7 +175,7 @@ function detectProvider(override) {
150
175
  const { claudeBase, openCodeDbPath, openCodeStorageDir, codexHomes } = getProviderPaths();
151
176
  const hasClaude = fs.existsSync(claudeBase);
152
177
  const hasOpenCode = fs.existsSync(openCodeStorageDir) || fs.existsSync(openCodeDbPath);
153
- const hasCodex = codexHomes.some(codexHome => fs.existsSync(path.join(codexHome, 'sessions')) || fs.existsSync(path.join(codexHome, 'state.sqlite')));
178
+ const hasCodex = codexHomes.some(codexHome => fs.existsSync(path.join(codexHome, 'sessions')) || hasCodexStateDb(codexHome));
154
179
  const available = [];
155
180
  if (hasClaude) {
156
181
  available.push({ id: 'claude-code', mtime: getMostRecentMtime(claudeBase) });
package/dist/quota.d.ts CHANGED
@@ -39,6 +39,16 @@ export interface QuotaState {
39
39
  fiveHourLabel?: string;
40
40
  /** Provider-specific display label for the second window */
41
41
  sevenDayLabel?: string;
42
+ /** Provider-specific rate-limit identifier */
43
+ limitId?: string;
44
+ /** Provider-specific rate-limit display name */
45
+ limitName?: string;
46
+ /** Provider-specific credits snapshot */
47
+ credits?: unknown;
48
+ /** Provider-specific plan type */
49
+ planType?: string;
50
+ /** Provider-specific rate-limit reached reason */
51
+ rateLimitReachedType?: string;
42
52
  }
43
53
  /**
44
54
  * Fetch current quota utilization from the Anthropic OAuth usage API.
@@ -180,14 +180,17 @@ export interface CodexRateLimits {
180
180
  limit_name?: string | null;
181
181
  primary?: {
182
182
  used_percent: number;
183
- window_minutes: number;
184
- resets_at: number;
183
+ window_minutes?: number | null;
184
+ resets_at?: number | null;
185
185
  };
186
186
  secondary?: {
187
187
  used_percent: number;
188
- window_minutes: number;
189
- resets_at: number;
188
+ window_minutes?: number | null;
189
+ resets_at?: number | null;
190
190
  };
191
+ credits?: unknown;
192
+ plan_type?: string;
193
+ rate_limit_reached_type?: string;
191
194
  }
192
195
  export interface CodexTokenUsage {
193
196
  input_tokens: number;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Low-level JSONL tail reader for consumers that need raw parsed events plus
3
+ * their own aggregation lifecycle.
4
+ */
5
+ import type { ZodType } from 'zod';
6
+ export interface JsonlTailBatch {
7
+ bytesRead: number;
8
+ eventsRead: number;
9
+ offset: number;
10
+ }
11
+ export interface JsonlTailOptions<T> {
12
+ path: string;
13
+ schema?: ZodType<T>;
14
+ startOffset?: number;
15
+ startAtEnd?: boolean;
16
+ debounceMs?: number;
17
+ catchupIntervalMs?: number;
18
+ onEvent: (event: T) => void;
19
+ onBatchComplete?: (batch: JsonlTailBatch) => void;
20
+ onError?: (error: Error, line?: string) => void;
21
+ }
22
+ export interface JsonlTail {
23
+ readonly isActive: boolean;
24
+ start(): void;
25
+ stop(): void;
26
+ dispose(): void;
27
+ readNow(): void;
28
+ getOffset(): number;
29
+ seekTo(offset: number): void;
30
+ }
31
+ export declare function createJsonlTail<T>(options: JsonlTailOptions<T>): JsonlTail;
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ /**
3
+ * Low-level JSONL tail reader for consumers that need raw parsed events plus
4
+ * their own aggregation lifecycle.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.createJsonlTail = createJsonlTail;
41
+ const fs = __importStar(require("fs"));
42
+ const jsonl_1 = require("../parsers/jsonl");
43
+ const DEFAULT_DEBOUNCE_MS = 100;
44
+ const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
45
+ function createJsonlTail(options) {
46
+ return new JsonlTailReader(options);
47
+ }
48
+ class JsonlTailReader {
49
+ options;
50
+ active = false;
51
+ offset;
52
+ fsWatcher = null;
53
+ debounceTimer = null;
54
+ catchupTimer = null;
55
+ eventsInCurrentBatch = 0;
56
+ parser;
57
+ constructor(options) {
58
+ this.options = options;
59
+ this.offset = options.startOffset ?? 0;
60
+ this.parser = new jsonl_1.JsonlParser({
61
+ onEvent: (event) => {
62
+ this.eventsInCurrentBatch += 1;
63
+ this.options.onEvent(event);
64
+ },
65
+ onError: (error, line) => this.options.onError?.(error, line),
66
+ }, { schema: options.schema });
67
+ }
68
+ get isActive() {
69
+ return this.active;
70
+ }
71
+ start() {
72
+ if (this.active)
73
+ return;
74
+ this.active = true;
75
+ if (this.options.startAtEnd && this.options.startOffset === undefined) {
76
+ try {
77
+ this.offset = fs.statSync(this.options.path).size;
78
+ }
79
+ catch {
80
+ this.offset = 0;
81
+ }
82
+ }
83
+ this.readNow();
84
+ this.watchFile();
85
+ const catchupIntervalMs = this.options.catchupIntervalMs ?? DEFAULT_CATCHUP_INTERVAL_MS;
86
+ if (catchupIntervalMs > 0) {
87
+ this.catchupTimer = setInterval(() => this.readNow(), catchupIntervalMs);
88
+ }
89
+ }
90
+ stop() {
91
+ if (!this.active)
92
+ return;
93
+ this.active = false;
94
+ if (this.debounceTimer) {
95
+ clearTimeout(this.debounceTimer);
96
+ this.debounceTimer = null;
97
+ }
98
+ if (this.fsWatcher) {
99
+ this.fsWatcher.close();
100
+ this.fsWatcher = null;
101
+ }
102
+ if (this.catchupTimer) {
103
+ clearInterval(this.catchupTimer);
104
+ this.catchupTimer = null;
105
+ }
106
+ this.parser.flush();
107
+ }
108
+ dispose() {
109
+ this.stop();
110
+ }
111
+ readNow() {
112
+ let fd = null;
113
+ try {
114
+ const stat = fs.statSync(this.options.path);
115
+ if (stat.size < this.offset) {
116
+ this.offset = 0;
117
+ this.parser.reset();
118
+ }
119
+ if (stat.size <= this.offset)
120
+ return;
121
+ const bytesToRead = stat.size - this.offset;
122
+ const buffer = Buffer.alloc(bytesToRead);
123
+ fd = fs.openSync(this.options.path, 'r');
124
+ const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, this.offset);
125
+ fs.closeSync(fd);
126
+ fd = null;
127
+ if (bytesRead <= 0)
128
+ return;
129
+ this.offset += bytesRead;
130
+ this.eventsInCurrentBatch = 0;
131
+ this.parser.processChunk(buffer.toString('utf-8', 0, bytesRead));
132
+ this.options.onBatchComplete?.({
133
+ bytesRead,
134
+ eventsRead: this.eventsInCurrentBatch,
135
+ offset: this.offset,
136
+ });
137
+ }
138
+ catch (error) {
139
+ if (fd !== null) {
140
+ try {
141
+ fs.closeSync(fd);
142
+ }
143
+ catch { /* ignore close errors */ }
144
+ }
145
+ this.options.onError?.(error instanceof Error ? error : new Error(String(error)));
146
+ }
147
+ }
148
+ getOffset() {
149
+ return this.offset;
150
+ }
151
+ seekTo(offset) {
152
+ this.offset = Math.max(0, offset);
153
+ this.parser.reset();
154
+ }
155
+ watchFile() {
156
+ try {
157
+ this.fsWatcher = fs.watch(this.options.path, { persistent: false }, () => {
158
+ this.debouncedRead();
159
+ });
160
+ this.fsWatcher.on('error', (error) => this.options.onError?.(error));
161
+ }
162
+ catch {
163
+ // Polling still covers filesystems where fs.watch is unavailable.
164
+ }
165
+ }
166
+ debouncedRead() {
167
+ if (this.debounceTimer)
168
+ clearTimeout(this.debounceTimer);
169
+ const debounceMs = this.options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
170
+ this.debounceTimer = setTimeout(() => this.readNow(), debounceMs);
171
+ }
172
+ }
@@ -398,13 +398,17 @@ class JsonlSessionWatcher {
398
398
  if (evtType === 'token_count') {
399
399
  const info = payload?.info;
400
400
  const usage = (info?.last_token_usage || info?.total_token_usage);
401
- if (usage) {
402
- const rl = payload?.rate_limits;
403
- const rateLimits = rl ? extractRateLimits(rl) : undefined;
401
+ const rl = payload?.rate_limits;
402
+ const rateLimits = rl ? extractRateLimits(rl) : undefined;
403
+ if (usage || rateLimits) {
404
404
  events.push({
405
405
  providerId: 'codex', type: 'system', timestamp: ts,
406
- summary: `Tokens: ${usage.input_tokens ?? 0} in / ${usage.output_tokens ?? 0} out`,
407
- tokens: { input: usage.input_tokens || 0, output: usage.output_tokens || 0 },
406
+ summary: usage
407
+ ? `Tokens: ${usage.input_tokens ?? 0} in / ${usage.output_tokens ?? 0} out`
408
+ : 'Rate limits updated',
409
+ tokens: usage
410
+ ? { input: usage.input_tokens || 0, output: usage.output_tokens || 0 }
411
+ : undefined,
408
412
  rateLimits,
409
413
  raw,
410
414
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,6 +29,10 @@
29
29
  "types": "./dist/modelInfo.d.ts",
30
30
  "default": "./dist/modelInfo.js"
31
31
  },
32
+ "./formatting": {
33
+ "types": "./dist/formatting.d.ts",
34
+ "default": "./dist/formatting.js"
35
+ },
32
36
  "./dist/*": {
33
37
  "types": "./dist/*.d.ts",
34
38
  "default": "./dist/*.js"