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
@@ -44,15 +44,22 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  };
45
45
  })();
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.SESSION_ACTIVITY_STALENESS_MS = exports.SESSION_ACTIVITY_GRACE_PERIOD_MS = void 0;
47
48
  exports.detectSessionActivity = detectSessionActivity;
49
+ exports.classifySessionActivity = classifySessionActivity;
50
+ exports.refreshSessionActivityState = refreshSessionActivityState;
48
51
  const fs = __importStar(require("fs"));
49
52
  // ── Constants ──
50
53
  /** How many bytes to read from the end of the file for analysis */
51
54
  const TAIL_BYTES = 32 * 1024;
52
55
  /** Grace period to prevent spinner flicker (ms) */
53
- const GRACE_PERIOD_MS = 5_000;
56
+ exports.SESSION_ACTIVITY_GRACE_PERIOD_MS = 5_000;
57
+ const GRACE_PERIOD_MS = exports.SESSION_ACTIVITY_GRACE_PERIOD_MS;
54
58
  /** If mtime is older than this, session is stale regardless of content */
55
- const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
59
+ exports.SESSION_ACTIVITY_STALENESS_MS = 5 * 60 * 1000;
60
+ const STALENESS_THRESHOLD_MS = exports.SESSION_ACTIVITY_STALENESS_MS;
61
+ /** How many trailing events the in-memory classifier inspects (about a 32 KiB tail). */
62
+ const TAIL_EVENTS = 200;
56
63
  // ── Event Classification Patterns ──
57
64
  /** Patterns indicating the AI is actively working */
58
65
  const AI_ACTIVITY_PATTERNS = [
@@ -105,7 +112,9 @@ function detectSessionActivity(sessionPath) {
105
112
  if (lines.length === 0) {
106
113
  return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'no-events' };
107
114
  }
108
- // Find the last line that matches each category
115
+ return classifyFromSignals(signalsFromLines(lines), stat.mtimeMs, mtimeAge);
116
+ }
117
+ function signalsFromLines(lines) {
109
118
  let lastAiActivityIndex = -1;
110
119
  let lastEndingIndex = -1;
111
120
  let hasTerminal = false;
@@ -135,31 +144,117 @@ function detectSessionActivity(sessionPath) {
135
144
  }
136
145
  }
137
146
  }
147
+ return { lastAiActivityIndex, lastEndingIndex, hasTerminal };
148
+ }
149
+ function hasToolResultBlock(event) {
150
+ const content = event.message?.content;
151
+ return (Array.isArray(content) &&
152
+ content.some((block) => typeof block === 'object' &&
153
+ block !== null &&
154
+ block.type === 'tool_result'));
155
+ }
156
+ /**
157
+ * The same signals the tail scanner derives from raw JSONL lines, read from
158
+ * canonical events instead: assistant, tool-use, and tool-result events (and
159
+ * user events carrying tool results) are AI activity; an assistant turn that
160
+ * stopped with `end_turn` or a user prompt is an ending event.
161
+ */
162
+ function signalsFromEvents(events) {
163
+ let lastAiActivityIndex = -1;
164
+ let lastEndingIndex = -1;
165
+ for (let i = 0; i < events.length; i++) {
166
+ const event = events[i];
167
+ const carriesToolResult = event.type === 'user' && hasToolResultBlock(event);
168
+ if (event.type === 'assistant' ||
169
+ event.type === 'tool_use' ||
170
+ event.type === 'tool_result' ||
171
+ carriesToolResult) {
172
+ lastAiActivityIndex = i;
173
+ }
174
+ if ((event.type === 'assistant' && event.message?.stop_reason === 'end_turn') ||
175
+ (event.type === 'user' && !carriesToolResult)) {
176
+ lastEndingIndex = i;
177
+ }
178
+ }
179
+ return { lastAiActivityIndex, lastEndingIndex, hasTerminal: false };
180
+ }
181
+ function classifyFromSignals(signals, mtimeMs, mtimeAge) {
182
+ const lastActivityTime = new Date(mtimeMs);
183
+ const { lastAiActivityIndex, lastEndingIndex, hasTerminal } = signals;
138
184
  // Terminal patterns mean the session is definitely done
139
185
  if (hasTerminal) {
140
- return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'terminal-event' };
186
+ return { state: 'ended', lastActivityTime, reason: 'terminal-event' };
141
187
  }
142
- // AI activity after the last ending event → ongoing
188
+ // AI activity after the last ending event -> ongoing
143
189
  if (lastAiActivityIndex > lastEndingIndex) {
144
- return {
145
- state: 'ongoing',
146
- lastActivityTime: new Date(stat.mtimeMs),
147
- reason: 'ai-activity-after-ending',
148
- };
190
+ return { state: 'ongoing', lastActivityTime, reason: 'ai-activity-after-ending' };
149
191
  }
150
192
  // Ending event with no subsequent AI activity
151
193
  if (lastEndingIndex >= 0) {
152
194
  // Apply grace period to prevent flicker
153
195
  if (mtimeAge < GRACE_PERIOD_MS) {
154
- return { state: 'ongoing', lastActivityTime: new Date(stat.mtimeMs), reason: 'grace-period' };
196
+ return { state: 'ongoing', lastActivityTime, reason: 'grace-period' };
155
197
  }
156
- return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'ending-event' };
198
+ return { state: 'ended', lastActivityTime, reason: 'ending-event' };
157
199
  }
158
- // No clear signal — if recently modified, assume ongoing
200
+ // No clear signal: if recently modified, assume ongoing
159
201
  if (mtimeAge < GRACE_PERIOD_MS) {
160
- return { state: 'ongoing', lastActivityTime: new Date(stat.mtimeMs), reason: 'recent-mtime' };
202
+ return { state: 'ongoing', lastActivityTime, reason: 'recent-mtime' };
203
+ }
204
+ return { state: 'ended', lastActivityTime, reason: 'no-activity-signal' };
205
+ }
206
+ /**
207
+ * Classify a session from events already in memory, with the same states,
208
+ * reasons, grace period, and staleness rule as {@link detectSessionActivity},
209
+ * but without opening the file again. Works for every provider (the raw
210
+ * tail patterns only ever matched Claude Code JSONL) and for database-backed
211
+ * sessions that have no file to tail.
212
+ */
213
+ function classifySessionActivity(options) {
214
+ const now = options.now ?? Date.now();
215
+ const events = options.events;
216
+ const lastEventMs = events.length > 0 ? Date.parse(events[events.length - 1].timestamp) : NaN;
217
+ const mtimeMs = typeof options.mtimeMs === 'number' && Number.isFinite(options.mtimeMs)
218
+ ? options.mtimeMs
219
+ : Number.isFinite(lastEventMs)
220
+ ? lastEventMs
221
+ : null;
222
+ if (mtimeMs === null) {
223
+ return { state: 'ended', lastActivityTime: null, reason: 'no-events' };
224
+ }
225
+ const mtimeAge = now - mtimeMs;
226
+ if (mtimeAge > STALENESS_THRESHOLD_MS) {
227
+ return { state: 'stale', lastActivityTime: new Date(mtimeMs), reason: 'mtime-stale' };
228
+ }
229
+ if (events.length === 0) {
230
+ return { state: 'ended', lastActivityTime: new Date(mtimeMs), reason: 'no-events' };
231
+ }
232
+ const tail = events.length > TAIL_EVENTS ? events.slice(-TAIL_EVENTS) : events;
233
+ return classifyFromSignals(signalsFromEvents(tail), mtimeMs, mtimeAge);
234
+ }
235
+ /**
236
+ * Refresh a cached classification from its unchanged fingerprint without
237
+ * re-reading the transcript. An active file remains active until it crosses
238
+ * the same five-minute staleness boundary used by detectSessionActivity,
239
+ * unless it was active only by grace period (`previousReason`).
240
+ */
241
+ function refreshSessionActivityState(previous, mtimeMs, nowMs = Date.now(), previousReason) {
242
+ if (!Number.isFinite(mtimeMs))
243
+ return previous;
244
+ if (nowMs - mtimeMs > STALENESS_THRESHOLD_MS)
245
+ return 'idle';
246
+ if (previous === 'ended')
247
+ return 'ended';
248
+ if (previous === 'idle')
249
+ return 'idle';
250
+ // A session that was only active thanks to the grace period ends once the
251
+ // grace period lapses, exactly as a fresh classification would say.
252
+ if (previous === 'active' &&
253
+ (previousReason === 'grace-period' || previousReason === 'recent-mtime') &&
254
+ nowMs - mtimeMs >= GRACE_PERIOD_MS) {
255
+ return 'ended';
161
256
  }
162
- return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'no-activity-signal' };
257
+ return previous === 'unknown' ? 'unknown' : 'active';
163
258
  }
164
259
  // ── Helpers ──
165
260
  function readTail(filePath, fileSize) {
@@ -205,6 +205,17 @@ export declare function getAllProjectFolders(workspacePath?: string): ProjectFol
205
205
  * ```
206
206
  */
207
207
  export declare function findSessionsInDirectory(sessionDir: string): string[];
208
+ export interface SessionFileStat {
209
+ path: string;
210
+ mtime: Date;
211
+ sizeBytes: number;
212
+ }
213
+ /**
214
+ * The non-empty `.jsonl` files in a session directory with the stat each one
215
+ * already cost, newest first, so callers that need the mtime do not stat the
216
+ * same files a second time.
217
+ */
218
+ export declare function findSessionFilesWithStats(sessionDir: string): SessionFileStat[];
208
219
  /**
209
220
  * Detects if a workspace is a git worktree and resolves the main repo path.
210
221
  *
@@ -54,6 +54,7 @@ exports.findAllSessions = findAllSessions;
54
54
  exports.decodeEncodedPath = decodeEncodedPath;
55
55
  exports.getAllProjectFolders = getAllProjectFolders;
56
56
  exports.findSessionsInDirectory = findSessionsInDirectory;
57
+ exports.findSessionFilesWithStats = findSessionFilesWithStats;
57
58
  exports.resolveWorktreeMainRepo = resolveWorktreeMainRepo;
58
59
  exports.discoverWorktreeSiblings = discoverWorktreeSiblings;
59
60
  exports.findAllSessionsWithWorktrees = findAllSessionsWithWorktrees;
@@ -590,31 +591,33 @@ function getAllProjectFolders(workspacePath) {
590
591
  * ```
591
592
  */
592
593
  function findSessionsInDirectory(sessionDir) {
594
+ return findSessionFilesWithStats(sessionDir).map((file) => file.path);
595
+ }
596
+ /**
597
+ * The non-empty `.jsonl` files in a session directory with the stat each one
598
+ * already cost, newest first, so callers that need the mtime do not stat the
599
+ * same files a second time.
600
+ */
601
+ function findSessionFilesWithStats(sessionDir) {
593
602
  try {
594
603
  if (!fs.existsSync(sessionDir)) {
595
604
  return [];
596
605
  }
597
- const files = fs
606
+ return fs
598
607
  .readdirSync(sessionDir)
599
608
  .filter((file) => file.endsWith('.jsonl'))
600
609
  .map((file) => {
601
610
  const fullPath = path.join(sessionDir, file);
602
611
  try {
603
612
  const stats = fs.statSync(fullPath);
604
- return {
605
- path: fullPath,
606
- mtime: stats.mtime.getTime(),
607
- size: stats.size,
608
- };
613
+ return { path: fullPath, mtime: stats.mtime, sizeBytes: stats.size };
609
614
  }
610
615
  catch {
611
616
  return null;
612
617
  }
613
618
  })
614
- .filter((f) => f !== null && f.size > 0)
615
- .sort((a, b) => b.mtime - a.mtime)
616
- .map((f) => f.path);
617
- return files;
619
+ .filter((f) => f !== null && f.sizeBytes > 0)
620
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
618
621
  }
619
622
  catch {
620
623
  return [];
@@ -22,9 +22,34 @@ export interface PeakHoursState {
22
22
  updatedAt: string;
23
23
  unavailable: boolean;
24
24
  notApplicable?: boolean;
25
+ /**
26
+ * `promoclock` when the state came from promoclock.co, `schedule` when it
27
+ * was computed locally from the published weekday window. Absent on the
28
+ * not-applicable and unavailable placeholder states.
29
+ */
30
+ source?: 'promoclock' | 'schedule';
25
31
  }
26
- /** Default budget for the promoclock.co request, in milliseconds. */
27
- export declare const DEFAULT_PEAK_HOURS_TIMEOUT_MS = 10000;
32
+ /**
33
+ * Default budget for the promoclock.co request, in milliseconds. The schedule
34
+ * fallback answers the question anyway, so a slow third-party host should not
35
+ * hold up a `sidekick status` or `sidekick quota` run for long.
36
+ */
37
+ export declare const DEFAULT_PEAK_HOURS_TIMEOUT_MS = 4000;
38
+ /** A promoclock.co answer is reused for this long before the host is asked again. */
39
+ export declare const PEAK_HOURS_CACHE_MS: number;
40
+ /** Published peak window: weekdays 13:00–19:00 UTC. */
41
+ export declare const PEAK_WINDOW_START_UTC_HOUR = 13;
42
+ export declare const PEAK_WINDOW_END_UTC_HOUR = 19;
43
+ export declare const PEAK_HOURS_DESCRIPTION = "Weekdays 13:00\u201319:00 UTC";
44
+ /** Forget the cached promoclock.co answer. Test-only. */
45
+ export declare function _resetPeakHoursCache(): void;
46
+ /**
47
+ * Peak-hours state computed from the published schedule alone, with no
48
+ * network access. Used directly by cache-only commands (`sidekick today`) and
49
+ * as the fallback when promoclock.co cannot be reached, so every surface
50
+ * agrees on whether it is currently peak.
51
+ */
52
+ export declare function getScheduledPeakHoursState(now?: Date): PeakHoursState;
28
53
  /** Options for {@link fetchPeakHoursStatus}. */
29
54
  export interface FetchPeakHoursOptions {
30
55
  /**
@@ -32,6 +57,8 @@ export interface FetchPeakHoursOptions {
32
57
  * Defaults to {@link DEFAULT_PEAK_HOURS_TIMEOUT_MS}.
33
58
  */
34
59
  timeoutMs?: number;
60
+ /** Bypass the in-process cache and ask promoclock.co again. */
61
+ force?: boolean;
35
62
  }
36
63
  export declare function isClaudeCodeSessionProvider(providerId: ProviderId): boolean;
37
64
  export declare function createPeakHoursNotApplicableState(providerId: ProviderId): PeakHoursState;
@@ -40,7 +67,11 @@ export declare function scopePeakHoursToSessionProvider(providerId: ProviderId,
40
67
  * Fetch current Claude peak-hours state from promoclock.co.
41
68
  *
42
69
  * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
43
- * Returns `unavailable: true` on any network, timeout, or parse error.
70
+ * A successful answer is cached in-process for {@link PEAK_HOURS_CACHE_MS}.
71
+ * On any network, timeout, or parse error the state is computed from the
72
+ * published schedule instead (`source: 'schedule'`), so callers always get a
73
+ * usable answer and every surface agrees on it; {@link unavailableState} is
74
+ * only returned when the schedule itself cannot be evaluated.
44
75
  *
45
76
  * promoclock.co is a third-party host, so the request is always bounded by
46
77
  * an abort timeout. Without one a hung host would stall the caller — and
package/dist/peakHours.js CHANGED
@@ -11,14 +11,74 @@
11
11
  * Polling / eventing is the caller's responsibility.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = void 0;
14
+ exports.PEAK_HOURS_DESCRIPTION = exports.PEAK_WINDOW_END_UTC_HOUR = exports.PEAK_WINDOW_START_UTC_HOUR = exports.PEAK_HOURS_CACHE_MS = exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = void 0;
15
+ exports._resetPeakHoursCache = _resetPeakHoursCache;
16
+ exports.getScheduledPeakHoursState = getScheduledPeakHoursState;
15
17
  exports.isClaudeCodeSessionProvider = isClaudeCodeSessionProvider;
16
18
  exports.createPeakHoursNotApplicableState = createPeakHoursNotApplicableState;
17
19
  exports.scopePeakHoursToSessionProvider = scopePeakHoursToSessionProvider;
18
20
  exports.fetchPeakHoursStatus = fetchPeakHoursStatus;
19
21
  const PROMOCLOCK_ENDPOINT = 'https://promoclock.co/api/status';
20
- /** Default budget for the promoclock.co request, in milliseconds. */
21
- exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = 10_000;
22
+ /**
23
+ * Default budget for the promoclock.co request, in milliseconds. The schedule
24
+ * fallback answers the question anyway, so a slow third-party host should not
25
+ * hold up a `sidekick status` or `sidekick quota` run for long.
26
+ */
27
+ exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = 4_000;
28
+ /** A promoclock.co answer is reused for this long before the host is asked again. */
29
+ exports.PEAK_HOURS_CACHE_MS = 10 * 60_000;
30
+ /** Published peak window: weekdays 13:00–19:00 UTC. */
31
+ exports.PEAK_WINDOW_START_UTC_HOUR = 13;
32
+ exports.PEAK_WINDOW_END_UTC_HOUR = 19;
33
+ exports.PEAK_HOURS_DESCRIPTION = 'Weekdays 13:00–19:00 UTC';
34
+ let cachedNetworkState = null;
35
+ /** Forget the cached promoclock.co answer. Test-only. */
36
+ function _resetPeakHoursCache() {
37
+ cachedNetworkState = null;
38
+ }
39
+ function isWeekdayUtc(date) {
40
+ const day = date.getUTCDay();
41
+ return day >= 1 && day <= 5;
42
+ }
43
+ function utcDateAtHour(date, hour, dayOffset = 0) {
44
+ return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + dayOffset, hour));
45
+ }
46
+ /**
47
+ * Peak-hours state computed from the published schedule alone, with no
48
+ * network access. Used directly by cache-only commands (`sidekick today`) and
49
+ * as the fallback when promoclock.co cannot be reached, so every surface
50
+ * agrees on whether it is currently peak.
51
+ */
52
+ function getScheduledPeakHoursState(now = new Date()) {
53
+ const hour = now.getUTCHours() + now.getUTCMinutes() / 60;
54
+ const isPeak = isWeekdayUtc(now) && hour >= exports.PEAK_WINDOW_START_UTC_HOUR && hour < exports.PEAK_WINDOW_END_UTC_HOUR;
55
+ let nextChange;
56
+ if (isPeak) {
57
+ nextChange = utcDateAtHour(now, exports.PEAK_WINDOW_END_UTC_HOUR);
58
+ }
59
+ else if (isWeekdayUtc(now) && hour < exports.PEAK_WINDOW_START_UTC_HOUR) {
60
+ nextChange = utcDateAtHour(now, exports.PEAK_WINDOW_START_UTC_HOUR);
61
+ }
62
+ else {
63
+ let offset = 1;
64
+ while (!isWeekdayUtc(utcDateAtHour(now, 0, offset)))
65
+ offset += 1;
66
+ nextChange = utcDateAtHour(now, exports.PEAK_WINDOW_START_UTC_HOUR, offset);
67
+ }
68
+ return {
69
+ status: isPeak ? 'peak' : 'off_peak',
70
+ isPeak,
71
+ sessionLimitSpeed: isPeak ? 'faster' : 'normal',
72
+ label: isPeak ? 'Peak Hours' : 'Off-Peak',
73
+ peakHoursDescription: exports.PEAK_HOURS_DESCRIPTION,
74
+ nextChange: nextChange.toISOString(),
75
+ minutesUntilChange: Math.max(0, Math.round((nextChange.getTime() - now.getTime()) / 60_000)),
76
+ note: 'Computed from the published weekday schedule.',
77
+ updatedAt: now.toISOString(),
78
+ unavailable: false,
79
+ source: 'schedule',
80
+ };
81
+ }
22
82
  function unavailableState() {
23
83
  return {
24
84
  status: 'unknown',
@@ -76,13 +136,23 @@ function normalizeSpeed(raw) {
76
136
  * Fetch current Claude peak-hours state from promoclock.co.
77
137
  *
78
138
  * Single-shot — caller wraps in polling loop, EventEmitter, or interval.
79
- * Returns `unavailable: true` on any network, timeout, or parse error.
139
+ * A successful answer is cached in-process for {@link PEAK_HOURS_CACHE_MS}.
140
+ * On any network, timeout, or parse error the state is computed from the
141
+ * published schedule instead (`source: 'schedule'`), so callers always get a
142
+ * usable answer and every surface agrees on it; {@link unavailableState} is
143
+ * only returned when the schedule itself cannot be evaluated.
80
144
  *
81
145
  * promoclock.co is a third-party host, so the request is always bounded by
82
146
  * an abort timeout. Without one a hung host would stall the caller — and
83
147
  * this is polled, so the stall would repeat.
84
148
  */
85
149
  async function fetchPeakHoursStatus(options = {}) {
150
+ const now = Date.now();
151
+ if (!options.force &&
152
+ cachedNetworkState &&
153
+ now - cachedNetworkState.fetchedAt < exports.PEAK_HOURS_CACHE_MS) {
154
+ return cachedNetworkState.state;
155
+ }
86
156
  const timeoutMs = options.timeoutMs ?? exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS;
87
157
  const controller = new AbortController();
88
158
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -90,12 +160,12 @@ async function fetchPeakHoursStatus(options = {}) {
90
160
  try {
91
161
  const res = await fetch(PROMOCLOCK_ENDPOINT, { signal: controller.signal });
92
162
  if (!res.ok)
93
- return unavailableState();
163
+ return scheduleFallback();
94
164
  const data = await res.json();
95
165
  const status = normalizeStatus(data.status);
96
166
  const sessionLimitSpeed = normalizeSpeed(data.sessionLimitSpeed);
97
167
  const isPeak = typeof data.isPeak === 'boolean' ? data.isPeak : status === 'peak';
98
- return {
168
+ const state = {
99
169
  status,
100
170
  isPeak,
101
171
  sessionLimitSpeed,
@@ -106,13 +176,28 @@ async function fetchPeakHoursStatus(options = {}) {
106
176
  note: data.note ?? '',
107
177
  updatedAt: data.timestamp ?? new Date().toISOString(),
108
178
  unavailable: false,
179
+ source: 'promoclock',
109
180
  };
181
+ cachedNetworkState = { state, fetchedAt: now };
182
+ return state;
110
183
  }
111
184
  catch {
112
185
  // Covers AbortError from the timeout above alongside network/parse failures.
113
- return unavailableState();
186
+ return scheduleFallback();
114
187
  }
115
188
  finally {
116
189
  clearTimeout(timeout);
117
190
  }
118
191
  }
192
+ function scheduleFallback() {
193
+ try {
194
+ const state = getScheduledPeakHoursState();
195
+ return {
196
+ ...state,
197
+ note: 'promoclock.co unreachable; computed from the published weekday schedule.',
198
+ };
199
+ }
200
+ catch {
201
+ return unavailableState();
202
+ }
203
+ }
@@ -34,6 +34,13 @@ export interface HydrateOptions {
34
34
  url?: string;
35
35
  /** Optional logger (useful for diagnostics without pulling a logger dep). */
36
36
  logger?: (msg: string) => void;
37
+ /**
38
+ * Never touch the network: apply the on-disk cache regardless of its age, or
39
+ * stay on the static tables when there is none. Callers that must produce
40
+ * the same figures on every run (scripted reports, tests, air-gapped hosts)
41
+ * set this; `SIDEKICK_OFFLINE=1` does the same for the CLI.
42
+ */
43
+ offline?: boolean;
37
44
  }
38
45
  export interface HydrateResult {
39
46
  /** Where the loaded pricing came from. */
@@ -60,6 +60,7 @@ const path = __importStar(require("node:path"));
60
60
  const modelContext_1 = require("./modelContext");
61
61
  const modelInfo_1 = require("./modelInfo");
62
62
  const paths_1 = require("./paths");
63
+ const atomic_1 = require("./writers/atomic");
63
64
  // ── Constants ──
64
65
  exports.LITELLM_CATALOG_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
65
66
  const CACHE_FILE_NAME = 'pricing-catalog.json';
@@ -77,16 +78,16 @@ const DEFAULT_TIMEOUT_MS = 3_000;
77
78
  * Always resolves — never throws. Offline-safe.
78
79
  */
79
80
  async function hydratePricingCatalog(options = {}) {
80
- const { cacheDir = (0, paths_1.getConfigDir)(), fetchImpl = globalThis.fetch, ttlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, url = exports.LITELLM_CATALOG_URL, logger, } = options;
81
+ const { cacheDir = (0, paths_1.getConfigDir)(), fetchImpl = globalThis.fetch, ttlMs = DEFAULT_TTL_MS, timeoutMs = DEFAULT_TIMEOUT_MS, url = exports.LITELLM_CATALOG_URL, logger, offline = false, } = options;
81
82
  const cachePath = path.join(cacheDir, CACHE_FILE_NAME);
82
83
  const now = Date.now();
83
- // 1. Fresh cache?
84
+ // 1. Fresh cache? (Offline callers take any cache, however old.)
84
85
  const cached = await readCache(cachePath);
85
- if (cached && now - Date.parse(cached.fetchedAt) < ttlMs) {
86
+ if (cached && (offline || now - Date.parse(cached.fetchedAt) < ttlMs)) {
86
87
  return applyCacheFile(cached, 'cache');
87
88
  }
88
89
  // 2. Try network.
89
- if (fetchImpl) {
90
+ if (fetchImpl && !offline) {
90
91
  const fetched = await fetchCatalog(url, fetchImpl, timeoutMs, logger);
91
92
  if (fetched) {
92
93
  const payload = {
@@ -256,7 +257,9 @@ async function readCache(cachePath) {
256
257
  async function writeCache(cachePath, payload, logger) {
257
258
  try {
258
259
  await node_fs_1.promises.mkdir(path.dirname(cachePath), { recursive: true });
259
- await node_fs_1.promises.writeFile(cachePath, JSON.stringify(payload, null, 2), 'utf8');
260
+ // The CLI and the extension host can both refresh the catalog at once;
261
+ // the atomic writer keeps a reader from ever seeing a half-written file.
262
+ await (0, atomic_1.atomicWriteJson)(cachePath, payload);
260
263
  }
261
264
  catch (err) {
262
265
  logger?.(`pricingCatalog: failed to write cache: ${String(err)}`);
@@ -0,0 +1,11 @@
1
+ /** Browser-safe provider ID catalogs. Types are derived from these values. */
2
+ export declare const SESSION_PROVIDER_IDS: readonly ["claude-code", "opencode", "codex"];
3
+ export type SessionProviderId = (typeof SESSION_PROVIDER_IDS)[number];
4
+ export declare const ACCOUNT_PROVIDER_IDS: readonly ["claude-code", "codex"];
5
+ export type AccountProviderId = (typeof ACCOUNT_PROVIDER_IDS)[number];
6
+ export declare const QUOTA_PROVIDER_IDS: readonly ["claude-code", "codex", "zai"];
7
+ export type QuotaProviderId = (typeof QUOTA_PROVIDER_IDS)[number];
8
+ export declare const RUNTIME_QUOTA_PROVIDER_IDS: readonly ["claude", "codex", "zai"];
9
+ export type RuntimeQuotaProviderId = (typeof RUNTIME_QUOTA_PROVIDER_IDS)[number];
10
+ export declare const MODEL_PROVIDER_IDS: readonly ["anthropic", "openai", "google", "unknown"];
11
+ export type ModelProviderId = (typeof MODEL_PROVIDER_IDS)[number];
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MODEL_PROVIDER_IDS = exports.RUNTIME_QUOTA_PROVIDER_IDS = exports.QUOTA_PROVIDER_IDS = exports.ACCOUNT_PROVIDER_IDS = exports.SESSION_PROVIDER_IDS = void 0;
4
+ /** Browser-safe provider ID catalogs. Types are derived from these values. */
5
+ exports.SESSION_PROVIDER_IDS = ['claude-code', 'opencode', 'codex'];
6
+ exports.ACCOUNT_PROVIDER_IDS = ['claude-code', 'codex'];
7
+ exports.QUOTA_PROVIDER_IDS = ['claude-code', 'codex', 'zai'];
8
+ exports.RUNTIME_QUOTA_PROVIDER_IDS = ['claude', 'codex', 'zai'];
9
+ exports.MODEL_PROVIDER_IDS = ['anthropic', 'openai', 'google', 'unknown'];
@@ -1,7 +1,9 @@
1
1
  import type { PeakHoursState } from './peakHours';
2
2
  import type { QuotaState } from './quota';
3
3
  import type { QuotaFailureDescriptor } from './quotaPresentation';
4
- export type RuntimeQuotaProvider = 'claude' | 'codex' | 'zai';
4
+ import type { RuntimeQuotaProviderId } from './providerIds';
5
+ export { RUNTIME_QUOTA_PROVIDER_IDS } from './providerIds';
6
+ export type RuntimeQuotaProvider = RuntimeQuotaProviderId;
5
7
  export interface ProviderQuotaState<TProvider extends RuntimeQuotaProvider = RuntimeQuotaProvider> extends QuotaState {
6
8
  runtimeProvider: TProvider;
7
9
  accountLabel?: string;
@@ -1,2 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RUNTIME_QUOTA_PROVIDER_IDS = void 0;
4
+ var providerIds_1 = require("./providerIds");
5
+ Object.defineProperty(exports, "RUNTIME_QUOTA_PROVIDER_IDS", { enumerable: true, get: function () { return providerIds_1.RUNTIME_QUOTA_PROVIDER_IDS; } });
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import type { ReadSessionContextSnapshotOptions, SessionContextSnapshot } from '../context/sessionContext';
11
11
  import type { SubagentStats, TokenUsage } from '../types/sessionEvent';
12
- import type { SessionProviderBase, SessionReader, SessionFileInfo, SessionFileStats, SearchHit, ProjectFolderInfo, ProviderId } from './types';
12
+ import type { SessionProviderBase, SessionReader, SessionFileInfo, SessionFileStats, SearchHit, ProjectFolderInfo, ProviderId, ProviderOperationStatus, ProviderRuntimeStatus, SessionProviderOptions } from './types';
13
13
  /**
14
14
  * Session provider for Claude Code CLI.
15
15
  *
@@ -20,27 +20,35 @@ import type { SessionProviderBase, SessionReader, SessionFileInfo, SessionFileSt
20
20
  export declare class ClaudeCodeProvider implements SessionProviderBase {
21
21
  readonly id: ProviderId;
22
22
  readonly displayName = "Claude Code";
23
+ private readonly diagnostics;
23
24
  /** Runtime-reported context window limit (overrides static map when set). */
24
25
  private dynamicContextWindowLimit;
26
+ constructor(options?: SessionProviderOptions);
25
27
  getSessionDirectory(workspacePath: string): string;
26
28
  discoverSessionDirectory(workspacePath: string): string | null;
27
29
  findActiveSession(workspacePath: string): string | null;
28
30
  findAllSessions(workspacePath: string): string[];
31
+ findSessionById(workspacePath: string, sessionId: string): string | null;
29
32
  /** Backward-compatible alias for findAllSessions. */
30
33
  findSessionFiles(workspacePath: string): string[];
31
34
  findSessionsInDirectory(dir: string): string[];
32
35
  getAllProjectFolders(workspacePath?: string): ProjectFolderInfo[];
33
36
  listAllSessionFiles(): SessionFileInfo[];
37
+ listSessionFilesAsync(workspacePath?: string): Promise<SessionFileInfo[]>;
34
38
  isSessionFile(filename: string): boolean;
35
39
  getSessionId(sessionPath: string): string;
36
40
  encodeWorkspacePath(workspacePath: string): string;
37
41
  extractSessionLabel(sessionPath: string): string | null;
42
+ extractSessionLabelsAsync(sessionPaths: readonly string[]): Promise<Map<string, string | null>>;
38
43
  createReader(sessionPath: string): SessionReader;
39
44
  scanSubagents(sessionDir: string, sessionId: string): SubagentStats[];
40
45
  searchInSession(sessionPath: string, query: string, maxResults: number): SearchHit[];
41
46
  getProjectsBaseDir(): string;
47
+ getWatchRoots(): string[];
42
48
  readSessionStats(sessionPath: string): SessionFileStats;
43
49
  readSessionContextSnapshot(sessionPath: string, options?: ReadSessionContextSnapshotOptions): SessionContextSnapshot;
50
+ getRuntimeStatus(): ProviderRuntimeStatus;
51
+ getLastOperationStatus(): ProviderOperationStatus;
44
52
  getContextWindowLimit(modelId?: string): number;
45
53
  /** Set a runtime-reported context window limit (overrides static map). */
46
54
  setDynamicContextWindowLimit(limit: number): void;
@@ -52,4 +60,5 @@ export declare class ClaudeCodeProvider implements SessionProviderBase {
52
60
  */
53
61
  getCurrentUsageSnapshot(sessionPath: string): TokenUsage | null;
54
62
  dispose(): void;
63
+ private recordHomeStatus;
55
64
  }