sidekick-shared 0.24.5 → 0.26.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 (157) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +205 -52
  3. package/dist/accountChangeSignal.d.ts +3 -0
  4. package/dist/accountChangeSignal.js +19 -0
  5. package/dist/accountChanges.d.ts +21 -0
  6. package/dist/accountChanges.js +170 -0
  7. package/dist/accountRegistry.d.ts +25 -2
  8. package/dist/accountRegistry.js +41 -13
  9. package/dist/accountStatus.d.ts +9 -1
  10. package/dist/accountStatus.js +3 -3
  11. package/dist/accounts.d.ts +3 -1
  12. package/dist/accounts.js +84 -64
  13. package/dist/aggregation/EventAggregator.d.ts +15 -2
  14. package/dist/aggregation/EventAggregator.js +54 -9
  15. package/dist/aggregation/costProvenance.d.ts +14 -0
  16. package/dist/aggregation/costProvenance.js +35 -0
  17. package/dist/aggregation/snapshot.d.ts +13 -2
  18. package/dist/aggregation/snapshot.js +57 -6
  19. package/dist/aggregation/types.d.ts +30 -1
  20. package/dist/analytics/compactionLedger.js +15 -5
  21. package/dist/analytics/qualityScore.js +2 -1
  22. package/dist/browser.d.ts +19 -4
  23. package/dist/browser.js +43 -1
  24. package/dist/codexProfiles.d.ts +3 -1
  25. package/dist/codexProfiles.js +4 -2
  26. package/dist/codexQuota.d.ts +2 -0
  27. package/dist/codexQuota.js +11 -66
  28. package/dist/codexQuotaWatcher.d.ts +6 -0
  29. package/dist/codexQuotaWatcher.js +101 -47
  30. package/dist/failingTools.d.ts +37 -0
  31. package/dist/failingTools.js +70 -0
  32. package/dist/formatters/sessionDump.js +13 -7
  33. package/dist/formatting.d.ts +13 -0
  34. package/dist/formatting.js +35 -0
  35. package/dist/historicalStore.d.ts +47 -0
  36. package/dist/historicalStore.js +312 -0
  37. package/dist/index.d.ts +67 -23
  38. package/dist/index.js +138 -26
  39. package/dist/modelAliases.d.ts +13 -0
  40. package/dist/modelAliases.js +58 -0
  41. package/dist/modelCatalog.d.ts +32 -0
  42. package/dist/modelCatalog.js +145 -0
  43. package/dist/modelContext.d.ts +36 -3
  44. package/dist/modelContext.js +109 -34
  45. package/dist/modelInfo.d.ts +30 -1
  46. package/dist/modelInfo.js +116 -26
  47. package/dist/multiProviderQuotaService.d.ts +8 -0
  48. package/dist/multiProviderQuotaService.js +64 -3
  49. package/dist/node.d.ts +23 -4
  50. package/dist/node.js +50 -1
  51. package/dist/observedContextWindows.d.ts +7 -0
  52. package/dist/observedContextWindows.js +17 -2
  53. package/dist/observedSessionCollector.d.ts +72 -6
  54. package/dist/observedSessionCollector.js +440 -47
  55. package/dist/parsers/codexHistory.d.ts +4 -2
  56. package/dist/parsers/codexHistory.js +9 -2
  57. package/dist/parsers/sessionActivityDetector.d.ts +28 -0
  58. package/dist/parsers/sessionActivityDetector.js +110 -15
  59. package/dist/parsers/sessionPathResolver.d.ts +11 -0
  60. package/dist/parsers/sessionPathResolver.js +13 -10
  61. package/dist/peakHours.d.ts +34 -3
  62. package/dist/peakHours.js +92 -7
  63. package/dist/pricingCatalog.d.ts +7 -0
  64. package/dist/pricingCatalog.js +8 -5
  65. package/dist/providerIds.d.ts +11 -0
  66. package/dist/providerIds.js +9 -0
  67. package/dist/providerQuota.d.ts +3 -1
  68. package/dist/providerQuota.js +3 -0
  69. package/dist/providers/claudeCode.d.ts +10 -1
  70. package/dist/providers/claudeCode.js +211 -131
  71. package/dist/providers/codex.d.ts +20 -1
  72. package/dist/providers/codex.js +308 -244
  73. package/dist/providers/codexDatabase.d.ts +9 -0
  74. package/dist/providers/codexDatabase.js +107 -2
  75. package/dist/providers/detect.d.ts +2 -0
  76. package/dist/providers/detect.js +29 -24
  77. package/dist/providers/diagnostics.d.ts +13 -0
  78. package/dist/providers/diagnostics.js +88 -0
  79. package/dist/providers/factory.d.ts +20 -0
  80. package/dist/providers/factory.js +57 -0
  81. package/dist/providers/openCode.d.ts +22 -1
  82. package/dist/providers/openCode.js +460 -126
  83. package/dist/providers/openCodeDatabase.d.ts +10 -0
  84. package/dist/providers/openCodeDatabase.js +69 -0
  85. package/dist/providers/rolloutWalker.d.ts +53 -0
  86. package/dist/providers/rolloutWalker.js +209 -0
  87. package/dist/providers/types.d.ts +82 -2
  88. package/dist/providers/types.js +4 -0
  89. package/dist/quota.d.ts +34 -4
  90. package/dist/quota.js +37 -9
  91. package/dist/quotaHistory.d.ts +3 -2
  92. package/dist/quotaHistory.js +36 -35
  93. package/dist/quotaPoller.d.ts +13 -1
  94. package/dist/quotaPoller.js +65 -3
  95. package/dist/quotaResolver.d.ts +81 -0
  96. package/dist/quotaResolver.js +213 -0
  97. package/dist/quotaSnapshots.d.ts +15 -2
  98. package/dist/quotaSnapshots.js +25 -3
  99. package/dist/quotaThresholds.d.ts +60 -0
  100. package/dist/quotaThresholds.js +90 -0
  101. package/dist/report/htmlReportGenerator.js +10 -4
  102. package/dist/report/sessionReportInputs.d.ts +23 -0
  103. package/dist/report/sessionReportInputs.js +34 -0
  104. package/dist/schemas/accountManager.d.ts +2 -2
  105. package/dist/schemas/accountManager.js +2 -4
  106. package/dist/schemas/accountStatus.d.ts +3 -0
  107. package/dist/schemas/accountStatus.js +1 -0
  108. package/dist/schemas/index.d.ts +3 -0
  109. package/dist/schemas/index.js +16 -2
  110. package/dist/schemas/observedSessionV1.d.ts +27 -26
  111. package/dist/schemas/observedSessionV1.js +6 -4
  112. package/dist/schemas/quota.d.ts +141 -2
  113. package/dist/schemas/quota.js +20 -8
  114. package/dist/schemas/quotaHistory.d.ts +3 -2
  115. package/dist/schemas/quotaHistory.js +1 -1
  116. package/dist/schemas/sessionEvent.d.ts +6 -0
  117. package/dist/schemas/sessionEvent.js +1 -0
  118. package/dist/schemas/stateFile.d.ts +198 -0
  119. package/dist/schemas/stateFile.js +77 -0
  120. package/dist/schemas/tokenEstimation.d.ts +1 -1
  121. package/dist/schemas/transcript.js +2 -0
  122. package/dist/sessionFingerprint.d.ts +25 -0
  123. package/dist/sessionFingerprint.js +46 -0
  124. package/dist/sessionMonitor.d.ts +15 -0
  125. package/dist/sessionMonitor.js +142 -0
  126. package/dist/sessionPreviews.d.ts +33 -2
  127. package/dist/sessionPreviews.js +333 -6
  128. package/dist/sessionStats.d.ts +51 -0
  129. package/dist/sessionStats.js +166 -0
  130. package/dist/stateFile.d.ts +113 -0
  131. package/dist/stateFile.js +160 -0
  132. package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
  133. package/dist/statusline/claudeStatuslinePayload.js +152 -0
  134. package/dist/statusline/formatter.d.ts +13 -0
  135. package/dist/statusline/formatter.js +48 -6
  136. package/dist/statusline/index.d.ts +10 -2
  137. package/dist/statusline/index.js +19 -1
  138. package/dist/tokenSummary.d.ts +62 -0
  139. package/dist/tokenSummary.js +72 -0
  140. package/dist/transcript.d.ts +5 -0
  141. package/dist/transcript.js +2 -0
  142. package/dist/types/observedSessionV1.d.ts +8 -2
  143. package/dist/types/observedSessionV1.js +88 -25
  144. package/dist/types/sessionEvent.d.ts +2 -0
  145. package/dist/usage/billingBlocks.d.ts +85 -0
  146. package/dist/usage/billingBlocks.js +153 -0
  147. package/dist/usage/importSessionHistory.d.ts +53 -0
  148. package/dist/usage/importSessionHistory.js +88 -0
  149. package/dist/usage/usageEvents.d.ts +92 -0
  150. package/dist/usage/usageEvents.js +322 -0
  151. package/dist/usage/usageReports.d.ts +76 -0
  152. package/dist/usage/usageReports.js +208 -0
  153. package/dist/watchers/factory.d.ts +6 -0
  154. package/dist/watchers/factory.js +30 -25
  155. package/dist/zaiQuotaApi.d.ts +2 -0
  156. package/dist/zaiQuotaApi.js +2 -0
  157. package/package.json +22 -1
@@ -17,12 +17,22 @@ export declare class OpenCodeDatabase {
17
17
  open(): boolean;
18
18
  close(): void;
19
19
  private query;
20
+ private queryAsync;
20
21
  private queryOne;
21
22
  findProjectByWorktree(workspacePath: string): DbProject | null;
22
23
  findProjectBySessionDirectory(workspacePath: string): DbProject | null;
23
24
  getAllProjects(): DbProject[];
24
25
  hasProject(projectId: string): boolean;
25
26
  getSessionsForProject(projectId: string): DbSession[];
27
+ /** Get every root session in one query for global preview enumeration. */
28
+ getAllSessions(): DbSession[];
29
+ /** Enumerate root sessions with one non-blocking sqlite3 subprocess. */
30
+ getAllSessionsAsync(): Promise<DbSession[]>;
31
+ /**
32
+ * Resolve many sessions with non-blocking sqlite3 subprocesses — one per
33
+ * chunk of ids, so the inlined query stays clear of argv length limits.
34
+ */
35
+ getSessionsByIdsAsync(sessionIds: readonly string[]): Promise<DbSession[]>;
26
36
  /** Get the most recently updated session for a project (excludes subagent child sessions). */
27
37
  getMostRecentSession(projectId: string): DbSession | null;
28
38
  /** Get child sessions (subagents) whose parent_id matches the given session ID. */
@@ -41,6 +41,11 @@ exports.OpenCodeDatabase = void 0;
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const child_process_1 = require("child_process");
44
+ /**
45
+ * Ids per inlined `IN (…)` query. Values are passed inside a single argv
46
+ * element to sqlite3, so an unbounded list would hit E2BIG on large stores.
47
+ */
48
+ const SQL_ID_CHUNK_SIZE = 400;
44
49
  class OpenCodeDatabase {
45
50
  dbPath;
46
51
  runtimeStatus = null;
@@ -127,6 +132,23 @@ class OpenCodeDatabase {
127
132
  return [];
128
133
  }
129
134
  }
135
+ async queryAsync(sql, params = []) {
136
+ if (!this.isAvailable()) {
137
+ this.runtimeStatus = { available: false, kind: 'db_missing' };
138
+ return [];
139
+ }
140
+ const query = bindSqlParams(sql, params);
141
+ try {
142
+ const result = await execSqlite(this.dbPath, query);
143
+ this.runtimeStatus = { available: true, kind: 'available' };
144
+ const trimmed = result.trim();
145
+ return trimmed ? JSON.parse(trimmed) : [];
146
+ }
147
+ catch (error) {
148
+ this.runtimeStatus = toRuntimeStatus(error, 'query_failed');
149
+ return [];
150
+ }
151
+ }
130
152
  queryOne(sql, params = []) {
131
153
  const results = this.query(sql, params);
132
154
  return results[0] ?? null;
@@ -181,6 +203,27 @@ class OpenCodeDatabase {
181
203
  getSessionsForProject(projectId) {
182
204
  return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC', [projectId]);
183
205
  }
206
+ /** Get every root session in one query for global preview enumeration. */
207
+ getAllSessions() {
208
+ return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
209
+ }
210
+ /** Enumerate root sessions with one non-blocking sqlite3 subprocess. */
211
+ getAllSessionsAsync() {
212
+ return this.queryAsync('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
213
+ }
214
+ /**
215
+ * Resolve many sessions with non-blocking sqlite3 subprocesses — one per
216
+ * chunk of ids, so the inlined query stays clear of argv length limits.
217
+ */
218
+ async getSessionsByIdsAsync(sessionIds) {
219
+ const results = [];
220
+ for (let i = 0; i < sessionIds.length; i += SQL_ID_CHUNK_SIZE) {
221
+ const chunk = sessionIds.slice(i, i + SQL_ID_CHUNK_SIZE);
222
+ const placeholders = chunk.map(() => '?').join(', ');
223
+ results.push(...(await this.queryAsync(`SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE id IN (${placeholders})`, [...chunk])));
224
+ }
225
+ return results;
226
+ }
184
227
  /** Get the most recently updated session for a project (excludes subagent child sessions). */
185
228
  getMostRecentSession(projectId) {
186
229
  return this.queryOne('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC LIMIT 1', [projectId]);
@@ -315,6 +358,32 @@ class OpenCodeDatabase {
315
358
  }
316
359
  }
317
360
  exports.OpenCodeDatabase = OpenCodeDatabase;
361
+ function bindSqlParams(sql, params) {
362
+ let paramIndex = 0;
363
+ return sql.replace(/\?/g, () => {
364
+ if (paramIndex >= params.length)
365
+ return '?';
366
+ const param = params[paramIndex++];
367
+ if (typeof param === 'number')
368
+ return Number.isFinite(param) ? String(param) : '0';
369
+ return `'${String(param).replace(/'/g, "''")}'`;
370
+ });
371
+ }
372
+ function execSqlite(dbPath, query) {
373
+ return new Promise((resolve, reject) => {
374
+ (0, child_process_1.execFile)('sqlite3', ['-json', '-readonly', dbPath, query], {
375
+ encoding: 'utf8',
376
+ timeout: 4_000,
377
+ killSignal: 'SIGKILL',
378
+ maxBuffer: 50 * 1024 * 1024,
379
+ }, (error, stdout) => {
380
+ if (error)
381
+ reject(error);
382
+ else
383
+ resolve(stdout);
384
+ });
385
+ });
386
+ }
318
387
  function normalizePath(input) {
319
388
  try {
320
389
  return fs.realpathSync(input);
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One capped walker for Codex rollout files.
3
+ *
4
+ * Codex writes `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`.
5
+ * Six recursive, uncapped walkers used to exist across the provider and the
6
+ * quota resolver; this module replaces them with a single walk that caps
7
+ * depth and file count, visits the newest-dated directories first so a
8
+ * `limit` can stop early, stats each file exactly once, and returns results
9
+ * newest first with the size and session id already attached.
10
+ *
11
+ * With `limit`, the early exit is by directory date: a rollout that was
12
+ * created days ago but is still being written lands in its creation day's
13
+ * directory, so a very small limit can miss it in favour of newer-dated,
14
+ * older-modified files. Callers that need exact mtime order over the whole
15
+ * history omit `limit` (the `maxFiles` cap still applies).
16
+ *
17
+ * @module providers/rolloutWalker
18
+ */
19
+ export interface RolloutFileInfo {
20
+ path: string;
21
+ mtime: Date;
22
+ sizeBytes: number;
23
+ /** Session id parsed from the file name. */
24
+ sessionId: string;
25
+ }
26
+ export interface WalkRolloutFilesOptions {
27
+ /** Directory depth below each root to descend into (default 6; the dated tree is 3). */
28
+ maxDepth?: number;
29
+ /** Upper bound on files returned across all roots (default 20 000). */
30
+ maxFiles?: number;
31
+ /** Stop once this many files are collected, visiting newest-dated directories first. */
32
+ limit?: number;
33
+ /** Only files whose name carries this session id (case-insensitive). */
34
+ sessionId?: string;
35
+ /** Include zero-byte files (default false). */
36
+ includeEmpty?: boolean;
37
+ }
38
+ export declare const DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
39
+ export declare const DEFAULT_ROLLOUT_WALK_MAX_FILES = 20000;
40
+ /** Test if a filename is a Codex rollout file. */
41
+ export declare function isRolloutFile(filename: string): boolean;
42
+ /**
43
+ * Extract the session UUID from a rollout filename.
44
+ * Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
45
+ */
46
+ export declare function extractRolloutSessionId(filename: string): string;
47
+ /**
48
+ * Walk one or more `sessions` roots for rollout files, newest first.
49
+ * Missing or unreadable directories are skipped; paths are deduplicated.
50
+ */
51
+ export declare function walkRolloutFiles(roots: readonly string[], options?: WalkRolloutFilesOptions): RolloutFileInfo[];
52
+ /** Async twin of {@link walkRolloutFiles} that yields between directories. */
53
+ export declare function walkRolloutFilesAsync(roots: readonly string[], options?: WalkRolloutFilesOptions): Promise<RolloutFileInfo[]>;
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ /**
3
+ * One capped walker for Codex rollout files.
4
+ *
5
+ * Codex writes `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl`.
6
+ * Six recursive, uncapped walkers used to exist across the provider and the
7
+ * quota resolver; this module replaces them with a single walk that caps
8
+ * depth and file count, visits the newest-dated directories first so a
9
+ * `limit` can stop early, stats each file exactly once, and returns results
10
+ * newest first with the size and session id already attached.
11
+ *
12
+ * With `limit`, the early exit is by directory date: a rollout that was
13
+ * created days ago but is still being written lands in its creation day's
14
+ * directory, so a very small limit can miss it in favour of newer-dated,
15
+ * older-modified files. Callers that need exact mtime order over the whole
16
+ * history omit `limit` (the `maxFiles` cap still applies).
17
+ *
18
+ * @module providers/rolloutWalker
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.DEFAULT_ROLLOUT_WALK_MAX_FILES = exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH = void 0;
55
+ exports.isRolloutFile = isRolloutFile;
56
+ exports.extractRolloutSessionId = extractRolloutSessionId;
57
+ exports.walkRolloutFiles = walkRolloutFiles;
58
+ exports.walkRolloutFilesAsync = walkRolloutFilesAsync;
59
+ const fs = __importStar(require("fs"));
60
+ const path = __importStar(require("path"));
61
+ exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH = 6;
62
+ exports.DEFAULT_ROLLOUT_WALK_MAX_FILES = 20_000;
63
+ /** Test if a filename is a Codex rollout file. */
64
+ function isRolloutFile(filename) {
65
+ return filename.startsWith('rollout-') && filename.endsWith('.jsonl');
66
+ }
67
+ /**
68
+ * Extract the session UUID from a rollout filename.
69
+ * Format: rollout-<timestamp>-<uuid>.jsonl -> <uuid>
70
+ */
71
+ function extractRolloutSessionId(filename) {
72
+ const base = path.basename(filename, '.jsonl');
73
+ // rollout-YYYYMMDD-HHMMSS-<uuid> or rollout-<timestamp>-<uuid>
74
+ const parts = base.split('-');
75
+ // The UUID is typically the last 5 segments (8-4-4-4-12)
76
+ if (parts.length >= 6) {
77
+ const possibleUuid = parts.slice(-5).join('-');
78
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(possibleUuid)) {
79
+ return possibleUuid;
80
+ }
81
+ }
82
+ // Fallback: use everything after "rollout-"
83
+ return base.replace(/^rollout-/, '');
84
+ }
85
+ function walkState(options) {
86
+ const maxFiles = Math.max(0, options.maxFiles ?? exports.DEFAULT_ROLLOUT_WALK_MAX_FILES);
87
+ const limit = options.limit === undefined ? Infinity : Math.max(0, options.limit);
88
+ return {
89
+ results: [],
90
+ seen: new Set(),
91
+ cap: Math.min(maxFiles, limit),
92
+ maxDepth: options.maxDepth ?? exports.DEFAULT_ROLLOUT_WALK_MAX_DEPTH,
93
+ sessionId: options.sessionId ? options.sessionId.trim().toLowerCase() : null,
94
+ includeEmpty: options.includeEmpty ?? false,
95
+ };
96
+ }
97
+ /** Newest-dated first: directory and file names sort descending. */
98
+ function byNameDescending(a, b) {
99
+ return b.name.localeCompare(a.name);
100
+ }
101
+ function matchesFilter(state, name) {
102
+ if (!isRolloutFile(name))
103
+ return false;
104
+ return (state.sessionId === null || extractRolloutSessionId(name).toLowerCase() === state.sessionId);
105
+ }
106
+ function record(state, fullPath, name, stat) {
107
+ if (!state.includeEmpty && stat.size <= 0)
108
+ return;
109
+ if (state.seen.has(fullPath))
110
+ return;
111
+ state.seen.add(fullPath);
112
+ state.results.push({
113
+ path: fullPath,
114
+ mtime: stat.mtime,
115
+ sizeBytes: stat.size,
116
+ sessionId: extractRolloutSessionId(name),
117
+ });
118
+ }
119
+ function finish(state) {
120
+ return state.results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
121
+ }
122
+ function visitSync(state, dir, depth) {
123
+ if (depth > state.maxDepth || state.results.length >= state.cap)
124
+ return;
125
+ let entries;
126
+ try {
127
+ entries = fs.readdirSync(dir, { withFileTypes: true });
128
+ }
129
+ catch {
130
+ return;
131
+ }
132
+ entries.sort(byNameDescending);
133
+ for (const entry of entries) {
134
+ if (state.results.length >= state.cap)
135
+ return;
136
+ if (!entry.isFile() || !matchesFilter(state, entry.name))
137
+ continue;
138
+ const fullPath = path.join(dir, entry.name);
139
+ try {
140
+ record(state, fullPath, entry.name, fs.statSync(fullPath));
141
+ }
142
+ catch {
143
+ // Skip files that vanish between readdir and stat.
144
+ }
145
+ }
146
+ for (const entry of entries) {
147
+ if (state.results.length >= state.cap)
148
+ return;
149
+ if (entry.isDirectory())
150
+ visitSync(state, path.join(dir, entry.name), depth + 1);
151
+ }
152
+ }
153
+ /**
154
+ * Walk one or more `sessions` roots for rollout files, newest first.
155
+ * Missing or unreadable directories are skipped; paths are deduplicated.
156
+ */
157
+ function walkRolloutFiles(roots, options = {}) {
158
+ const state = walkState(options);
159
+ for (const root of roots)
160
+ visitSync(state, root, 0);
161
+ return finish(state);
162
+ }
163
+ function yieldToEventLoop() {
164
+ return new Promise((resolve) => setImmediate(resolve));
165
+ }
166
+ async function visitAsync(state, dir, depth) {
167
+ if (depth > state.maxDepth || state.results.length >= state.cap)
168
+ return;
169
+ let entries;
170
+ try {
171
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
172
+ }
173
+ catch {
174
+ return;
175
+ }
176
+ entries.sort(byNameDescending);
177
+ let sinceYield = 0;
178
+ for (const entry of entries) {
179
+ if (state.results.length >= state.cap)
180
+ return;
181
+ if (!entry.isFile() || !matchesFilter(state, entry.name))
182
+ continue;
183
+ const fullPath = path.join(dir, entry.name);
184
+ try {
185
+ record(state, fullPath, entry.name, await fs.promises.stat(fullPath));
186
+ }
187
+ catch {
188
+ // Skip files that vanish between readdir and stat.
189
+ }
190
+ if (++sinceYield >= 200) {
191
+ sinceYield = 0;
192
+ await yieldToEventLoop();
193
+ }
194
+ }
195
+ await yieldToEventLoop();
196
+ for (const entry of entries) {
197
+ if (state.results.length >= state.cap)
198
+ return;
199
+ if (entry.isDirectory())
200
+ await visitAsync(state, path.join(dir, entry.name), depth + 1);
201
+ }
202
+ }
203
+ /** Async twin of {@link walkRolloutFiles} that yields between directories. */
204
+ async function walkRolloutFilesAsync(roots, options = {}) {
205
+ const state = walkState(options);
206
+ for (const root of roots)
207
+ await visitAsync(state, root, 0);
208
+ return finish(state);
209
+ }
@@ -10,7 +10,32 @@
10
10
  */
11
11
  import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
12
12
  import type { SessionEvent, SubagentStats, TokenUsage, ContextAttribution } from '../types/sessionEvent';
13
- export type ProviderId = 'claude-code' | 'opencode' | 'codex';
13
+ import { SESSION_PROVIDER_IDS } from '../providerIds';
14
+ import type { AggregatedCostProvenance } from '../aggregation/types';
15
+ /** Stable runtime list for consumers that must enumerate session providers. */
16
+ export { SESSION_PROVIDER_IDS } from '../providerIds';
17
+ export type ProviderId = (typeof SESSION_PROVIDER_IDS)[number];
18
+ export type SessionProviderDiagnosticSeverity = 'info' | 'warning' | 'error';
19
+ export type SessionProviderDiagnosticPhase = 'construct' | 'initialize' | 'enumerate' | 'query' | 'read' | 'watch';
20
+ export type SessionProviderDiagnosticKind = 'construction_failed' | 'home_unavailable' | 'config_unreadable' | 'db_missing' | 'sqlite_missing' | 'sqlite_blocked' | 'query_failed' | 'read_failed' | 'watch_failed';
21
+ /** Content-safe provider degradation emitted at construction or first use. */
22
+ export interface SessionProviderDiagnostic {
23
+ providerId: ProviderId;
24
+ kind: SessionProviderDiagnosticKind;
25
+ severity: SessionProviderDiagnosticSeverity;
26
+ phase: SessionProviderDiagnosticPhase;
27
+ message: string;
28
+ }
29
+ export interface ProviderOperationStatus {
30
+ operation: string;
31
+ usable: boolean;
32
+ degraded: boolean;
33
+ runtimeStatus: ProviderRuntimeStatus;
34
+ diagnostics: readonly SessionProviderDiagnostic[];
35
+ }
36
+ export interface SessionProviderOptions {
37
+ onDiagnostic?: (diagnostic: SessionProviderDiagnostic) => void;
38
+ }
14
39
  export interface SearchHit {
15
40
  sessionPath: string;
16
41
  line: string;
@@ -25,7 +50,21 @@ export interface SessionFileInfo {
25
50
  mtime: Date;
26
51
  /** Optional human-readable label (e.g., first user prompt) */
27
52
  label?: string;
53
+ /** Optional size supplied by synthetic/DB-backed enumerators. */
54
+ sizeBytes?: number;
55
+ /** Optional provider-native session id. */
56
+ sessionId?: string;
57
+ /** Optional provider-recorded workspace path. */
58
+ workspacePath?: string;
59
+ /** Optional provider-recorded creation timestamp. */
60
+ createdAt?: Date;
28
61
  }
62
+ /** How much of a session `readSessionStats()` could read. */
63
+ export type SessionFileStatsAvailability = 'full' | 'partial' | 'unavailable';
64
+ /**
65
+ * Aggregated stats for one session file, computed by `computeSessionFileStats()`
66
+ * over the shared `EventAggregator` for every provider.
67
+ */
29
68
  export interface SessionFileStats {
30
69
  providerId: ProviderId;
31
70
  sessionId: string;
@@ -34,19 +73,42 @@ export interface SessionFileStats {
34
73
  startTime: string;
35
74
  endTime: string;
36
75
  messageCount: number;
76
+ /** Token buckets; `summarizeTokens()` turns them into the cache-inclusive total. */
37
77
  tokens: {
38
78
  input: number;
39
79
  output: number;
40
80
  cacheWrite: number;
41
81
  cacheRead: number;
42
82
  };
83
+ /**
84
+ * Per-model calls, cache-inclusive token total (the `summarizeTokens().total`
85
+ * vocabulary), priced cost, and whether every call could be priced.
86
+ */
43
87
  modelUsage: Record<string, {
44
88
  calls: number;
45
89
  tokens: number;
90
+ costUsd: number;
91
+ priced: boolean;
46
92
  }>;
93
+ /** Calls per tool name: successful, failed, and still pending. */
47
94
  toolUsage: Record<string, number>;
95
+ /** Failed calls per tool name; a subset of `toolUsage`. */
96
+ toolFailures: Record<string, number>;
48
97
  compactionEstimate: number;
49
98
  truncationCount: number;
99
+ /** Cost of the priced portion of the session; read `costProvenance` before labelling it. */
100
+ costUsd: number;
101
+ costProvenance: AggregatedCostProvenance;
102
+ /** Usage events with neither a provider-reported cost nor a catalog price. */
103
+ unpricedCalls: number;
104
+ /**
105
+ * `full` when every event was read, `partial` when the reader reported a
106
+ * truncated source, `unavailable` when the session could not be read at all
107
+ * (`unavailableReason` says why and every number is zero).
108
+ */
109
+ availability: SessionFileStatsAvailability;
110
+ unavailableReason?: string;
111
+ /** @deprecated Same value as `costUsd`; use it together with `costProvenance`. */
50
112
  reportedCost: number;
51
113
  }
52
114
  export interface ProjectFolderInfo {
@@ -58,7 +120,7 @@ export interface ProjectFolderInfo {
58
120
  }
59
121
  export interface ProviderRuntimeStatus {
60
122
  available: boolean;
61
- kind: 'available' | 'db_missing' | 'sqlite_missing' | 'sqlite_blocked' | 'query_failed';
123
+ kind: 'available' | 'home_unavailable' | 'config_unreadable' | 'db_missing' | 'sqlite_missing' | 'sqlite_blocked' | 'query_failed';
62
124
  message?: string;
63
125
  }
64
126
  /**
@@ -105,6 +167,12 @@ export interface SessionProviderBase {
105
167
  findActiveSession(workspacePath: string): string | null;
106
168
  /** Finds all session files for a workspace, sorted by mtime (most recent first). */
107
169
  findAllSessions(workspacePath: string): string[];
170
+ /**
171
+ * Finds one session without reading every transcript. Wrong ids return null.
172
+ * Optional so pre-0.25 external implementations keep compiling; when absent,
173
+ * callers fall back to scanning findAllSessions().
174
+ */
175
+ findSessionById?(workspacePath: string, sessionId: string): string | null;
108
176
  /** Finds all session files in a specific directory. */
109
177
  findSessionsInDirectory(dir: string): string[];
110
178
  /** Gets all project folders with session data. */
@@ -131,6 +199,12 @@ export interface SessionProviderBase {
131
199
  getSessionMetadata?(sessionPath: string): {
132
200
  mtime: Date;
133
201
  } | null;
202
+ /** Event-loop-safe metadata refresh for synthetic/DB-backed session paths. */
203
+ getSessionMetadataAsync?(sessionPath: string): Promise<{
204
+ mtime: Date;
205
+ } | null>;
206
+ /** Roots whose changes can invalidate this provider's session fingerprints. */
207
+ getWatchRoots?(): string[];
134
208
  /** Gets the context window token limit for a model. Returns 200K by default. */
135
209
  getContextWindowLimit?(modelId?: string): number;
136
210
  /** Computes context window size from token usage. Provider-specific formula. */
@@ -143,11 +217,17 @@ export interface SessionProviderBase {
143
217
  readSessionContextSnapshot?(sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;
144
218
  /** Reports provider runtime readiness for DB-backed providers. */
145
219
  getRuntimeStatus?(): ProviderRuntimeStatus;
220
+ /** Status for the most recent provider operation, including fallback diagnostics. */
221
+ getLastOperationStatus?(): ProviderOperationStatus;
146
222
  /**
147
223
  * Lists every session file this provider knows about, across all workspaces.
148
224
  * Stat-only — implementations must not read file contents.
149
225
  */
150
226
  listAllSessionFiles?(): SessionFileInfo[];
227
+ /** Event-loop-safe enumeration used by async preview and subscription APIs. */
228
+ listSessionFilesAsync?(workspacePath?: string): Promise<SessionFileInfo[]>;
229
+ /** Batch label extraction. DB-backed implementations use at most one query. */
230
+ extractSessionLabelsAsync?(sessionPaths: readonly string[]): Promise<Map<string, string | null>>;
151
231
  /** Tests whether a provider can monitor a directory path, including synthetic DB-backed paths. */
152
232
  canMonitorDirectory?(dir: string): boolean;
153
233
  /** Release resources. */
@@ -10,3 +10,7 @@
10
10
  * compatibility with existing CLI code.
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.SESSION_PROVIDER_IDS = void 0;
14
+ /** Stable runtime list for consumers that must enumerate session providers. */
15
+ var providerIds_1 = require("../providerIds");
16
+ Object.defineProperty(exports, "SESSION_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.SESSION_PROVIDER_IDS; } });
package/dist/quota.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import type { QuotaProviderId } from './providerIds';
2
+ export { QUOTA_PROVIDER_IDS } from './providerIds';
3
+ export type { QuotaProviderId } from './providerIds';
1
4
  /**
2
5
  * Stateless quota fetcher for Claude Max subscription usage.
3
6
  *
@@ -42,13 +45,27 @@ export interface QuotaState {
42
45
  /** Projected 7-day utilization at reset (percentage, capped at 200) */
43
46
  projectedSevenDay?: number;
44
47
  /** Provider that produced this quota sample */
45
- providerId?: 'claude-code' | 'codex' | 'zai';
46
- /** Source of the sample */
47
- source?: 'api' | 'session' | 'cache';
48
+ providerId?: QuotaProviderId;
49
+ /**
50
+ * Source of the sample. `statusline` is the official rate-limit block Claude
51
+ * Code hands to its status-line command; `session` is inferred from session
52
+ * logs; `api` is a live usage-endpoint fetch; `cache` is a persisted snapshot.
53
+ */
54
+ source?: 'api' | 'session' | 'cache' | 'statusline';
55
+ /**
56
+ * Where a cached sample originally came from. `readQuotaSnapshot()` forces
57
+ * `source` to `cache`; this keeps the origin (`statusline`, `session`, `api`)
58
+ * so a reader can rank an official status-line sample above a session guess.
59
+ */
60
+ capturedSource?: 'api' | 'session' | 'statusline';
48
61
  /** ISO timestamp when the sample was captured */
49
62
  capturedAt?: string;
50
63
  /** Whether the sample is stale cached data */
51
64
  stale?: boolean;
65
+ /** Milliseconds between `capturedAt` and the read, when read from a snapshot. */
66
+ ageMs?: number;
67
+ /** Freshness tier derived from `ageMs`; see `classifyQuotaFreshness`. */
68
+ freshness?: QuotaFreshness;
52
69
  /** Provider-specific display label for the first window */
53
70
  fiveHourLabel?: string;
54
71
  /** Provider-specific display label for the second window */
@@ -68,6 +85,16 @@ export interface QuotaState {
68
85
  }
69
86
  export declare const FIVE_HOUR_WINDOW_MS: number;
70
87
  export declare const SEVEN_DAY_WINDOW_MS: number;
88
+ /** How much to trust a cached quota sample, by age. */
89
+ export type QuotaFreshness = 'fresh' | 'aging' | 'stale';
90
+ /** A snapshot younger than this is as good as live. */
91
+ export declare const QUOTA_FRESH_MAX_AGE_MS: number;
92
+ /** A snapshot older than this should be labelled as stale wherever it is shown. */
93
+ export declare const QUOTA_AGING_MAX_AGE_MS: number;
94
+ /** Classify a cached sample's age. Non-finite or negative ages count as stale. */
95
+ export declare function classifyQuotaFreshness(ageMs: number | undefined): QuotaFreshness;
96
+ /** Compact age label for status lines and table footers: `just now`, `3m ago`, `2h ago`, `3d ago`. */
97
+ export declare function formatQuotaAge(ageMs: number | undefined): string;
71
98
  export interface QuotaProjectionInput {
72
99
  utilization: number;
73
100
  resetsAt: string;
@@ -92,6 +119,9 @@ export declare function withQuotaProjections<T extends QuotaState>(state: T, opt
92
119
  * it in a polling loop, VS Code EventEmitter, or CLI interval as needed.
93
120
  *
94
121
  * @param accessToken - OAuth access token from `readClaudeMaxCredentials()`
122
+ * @param options.fetchImpl - Fetch implementation (defaults to the global `fetch`; tests inject a stub)
95
123
  * @returns QuotaState with utilization data and elapsed-time projections
96
124
  */
97
- export declare function fetchQuota(accessToken: string): Promise<QuotaState>;
125
+ export declare function fetchQuota(accessToken: string, options?: {
126
+ fetchImpl?: typeof fetch;
127
+ }): Promise<QuotaState>;
package/dist/quota.js CHANGED
@@ -1,20 +1,46 @@
1
1
  "use strict";
2
- /**
3
- * Stateless quota fetcher for Claude Max subscription usage.
4
- *
5
- * Calls the Anthropic OAuth usage endpoint and returns utilization data.
6
- * Polling / eventing is the caller's responsibility.
7
- */
8
2
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.SEVEN_DAY_WINDOW_MS = exports.FIVE_HOUR_WINDOW_MS = void 0;
3
+ exports.QUOTA_AGING_MAX_AGE_MS = exports.QUOTA_FRESH_MAX_AGE_MS = exports.SEVEN_DAY_WINDOW_MS = exports.FIVE_HOUR_WINDOW_MS = exports.QUOTA_PROVIDER_IDS = void 0;
4
+ exports.classifyQuotaFreshness = classifyQuotaFreshness;
5
+ exports.formatQuotaAge = formatQuotaAge;
10
6
  exports.projectQuotaWindow = projectQuotaWindow;
11
7
  exports.withQuotaProjections = withQuotaProjections;
12
8
  exports.fetchQuota = fetchQuota;
9
+ var providerIds_1 = require("./providerIds");
10
+ Object.defineProperty(exports, "QUOTA_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.QUOTA_PROVIDER_IDS; } });
13
11
  const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
14
12
  const BETA_HEADER = 'oauth-2025-04-20';
15
13
  const FETCH_TIMEOUT_MS = 10_000;
16
14
  exports.FIVE_HOUR_WINDOW_MS = 5 * 3_600_000;
17
15
  exports.SEVEN_DAY_WINDOW_MS = 7 * 86_400_000;
16
+ /** A snapshot younger than this is as good as live. */
17
+ exports.QUOTA_FRESH_MAX_AGE_MS = 5 * 60_000;
18
+ /** A snapshot older than this should be labelled as stale wherever it is shown. */
19
+ exports.QUOTA_AGING_MAX_AGE_MS = 60 * 60_000;
20
+ /** Classify a cached sample's age. Non-finite or negative ages count as stale. */
21
+ function classifyQuotaFreshness(ageMs) {
22
+ if (typeof ageMs !== 'number' || !Number.isFinite(ageMs) || ageMs < 0)
23
+ return 'stale';
24
+ if (ageMs < exports.QUOTA_FRESH_MAX_AGE_MS)
25
+ return 'fresh';
26
+ if (ageMs < exports.QUOTA_AGING_MAX_AGE_MS)
27
+ return 'aging';
28
+ return 'stale';
29
+ }
30
+ /** Compact age label for status lines and table footers: `just now`, `3m ago`, `2h ago`, `3d ago`. */
31
+ function formatQuotaAge(ageMs) {
32
+ if (typeof ageMs !== 'number' || !Number.isFinite(ageMs) || ageMs < 0)
33
+ return 'age unknown';
34
+ const minutes = Math.floor(ageMs / 60_000);
35
+ if (minutes < 1)
36
+ return 'just now';
37
+ if (minutes < 60)
38
+ return `${minutes}m ago`;
39
+ const hours = Math.floor(minutes / 60);
40
+ if (hours < 48)
41
+ return `${hours}h ago`;
42
+ return `${Math.floor(hours / 24)}d ago`;
43
+ }
18
44
  /**
19
45
  * Projects utilization at end of window based on elapsed time.
20
46
  *
@@ -92,14 +118,16 @@ function parseRetryAfterMs(retryAfter) {
92
118
  * it in a polling loop, VS Code EventEmitter, or CLI interval as needed.
93
119
  *
94
120
  * @param accessToken - OAuth access token from `readClaudeMaxCredentials()`
121
+ * @param options.fetchImpl - Fetch implementation (defaults to the global `fetch`; tests inject a stub)
95
122
  * @returns QuotaState with utilization data and elapsed-time projections
96
123
  */
97
- async function fetchQuota(accessToken) {
124
+ async function fetchQuota(accessToken, options = {}) {
98
125
  const controller = new AbortController();
99
126
  const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
100
127
  timeout.unref();
101
128
  try {
102
- const res = await fetch(USAGE_URL, {
129
+ const fetchImpl = options.fetchImpl ?? fetch;
130
+ const res = await fetchImpl(USAGE_URL, {
103
131
  method: 'GET',
104
132
  signal: controller.signal,
105
133
  headers: {