sidekick-shared 0.18.3 → 0.18.4

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.
@@ -46,6 +46,36 @@ const codex_1 = require("./providers/codex");
46
46
  const DEFAULT_TAIL_BYTES = 2 * 1024 * 1024;
47
47
  const DEFAULT_MAX_SESSION_FILES = 50;
48
48
  const CHATGPT_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
49
+ function timestampMs(value, fallbackMs = 0) {
50
+ if (!value)
51
+ return fallbackMs;
52
+ const ms = Date.parse(value);
53
+ return Number.isFinite(ms) ? ms : fallbackMs;
54
+ }
55
+ // Codex can report slightly different percentages for the same reset window
56
+ // across recent sessions. Prefer newer reset windows, then the highest observed
57
+ // same-window utilization so local fallbacks do not under-report quota usage.
58
+ function isPreferredQuotaHit(candidate, current) {
59
+ if (!current)
60
+ return true;
61
+ const candidatePrimaryReset = timestampMs(candidate.quota.fiveHour.resetsAt);
62
+ const currentPrimaryReset = timestampMs(current.quota.fiveHour.resetsAt);
63
+ if (candidatePrimaryReset !== currentPrimaryReset)
64
+ return candidatePrimaryReset > currentPrimaryReset;
65
+ const candidateSecondaryReset = timestampMs(candidate.quota.sevenDay.resetsAt);
66
+ const currentSecondaryReset = timestampMs(current.quota.sevenDay.resetsAt);
67
+ if (candidateSecondaryReset !== currentSecondaryReset)
68
+ return candidateSecondaryReset > currentSecondaryReset;
69
+ const candidateUtilization = candidate.quota.fiveHour.utilization + candidate.quota.sevenDay.utilization;
70
+ const currentUtilization = current.quota.fiveHour.utilization + current.quota.sevenDay.utilization;
71
+ if (candidateUtilization !== currentUtilization)
72
+ return candidateUtilization > currentUtilization;
73
+ const candidateMs = timestampMs(candidate.quota.capturedAt, candidate.mtimeMs);
74
+ const currentMs = timestampMs(current.quota.capturedAt, current.mtimeMs);
75
+ if (candidateMs !== currentMs)
76
+ return candidateMs > currentMs;
77
+ return candidate.mtimeMs > current.mtimeMs;
78
+ }
49
79
  function normalizePercent(value) {
50
80
  return typeof value === 'number' && Number.isFinite(value) ? value : 0;
51
81
  }
@@ -175,14 +205,53 @@ function quotaFromCodexRateLimits(rateLimits, source = 'session', capturedAt = n
175
205
  };
176
206
  }
177
207
  function readLatestCodexQuotaFromRollouts(sessionPaths, options = {}) {
208
+ return readLatestCodexQuotaHitFromRollouts(sessionPaths, options)?.quota ?? null;
209
+ }
210
+ function readLatestCodexQuotaHitFromRollouts(sessionPaths, options = {}) {
178
211
  const maxSessionFiles = options.maxSessionFiles ?? DEFAULT_MAX_SESSION_FILES;
179
212
  const maxTailBytes = options.maxTailBytes ?? DEFAULT_TAIL_BYTES;
213
+ let latest = null;
180
214
  for (const sessionPath of sessionPaths.slice(0, maxSessionFiles)) {
181
215
  const hit = readLatestQuotaFromRollout(sessionPath, maxTailBytes, options.source ?? 'session');
182
- if (hit)
183
- return hit.quota;
216
+ if (hit && isPreferredQuotaHit(hit, latest)) {
217
+ latest = hit;
218
+ }
184
219
  }
185
- return null;
220
+ return latest;
221
+ }
222
+ function dedupePaths(paths) {
223
+ const seen = new Set();
224
+ const unique = [];
225
+ for (const filePath of paths) {
226
+ if (seen.has(filePath))
227
+ continue;
228
+ seen.add(filePath);
229
+ unique.push(filePath);
230
+ }
231
+ return unique;
232
+ }
233
+ function sortPathsByMtimeDesc(paths) {
234
+ return [...paths].sort((a, b) => {
235
+ const aMtime = safeMtimeMs(a);
236
+ const bMtime = safeMtimeMs(b);
237
+ return bMtime - aMtime;
238
+ });
239
+ }
240
+ function safeMtimeMs(filePath) {
241
+ try {
242
+ return fs.statSync(filePath).mtime.getTime();
243
+ }
244
+ catch {
245
+ return 0;
246
+ }
247
+ }
248
+ function findAccountRolloutFiles(codexHome) {
249
+ const homes = codexHome ? [codexHome] : (0, codexProfiles_1.getCodexMonitoringHomes)();
250
+ const files = [];
251
+ for (const home of homes) {
252
+ files.push(...findRolloutFiles(path.join(home, 'sessions')));
253
+ }
254
+ return sortPathsByMtimeDesc(dedupePaths(files));
186
255
  }
187
256
  function resolveCodexQuotaFromLocalSources(options = {}) {
188
257
  const account = options.activeAccount !== undefined ? options.activeAccount : (0, codexProfiles_1.getActiveCodexAccount)();
@@ -196,20 +265,22 @@ function resolveCodexQuotaFromLocalSources(options = {}) {
196
265
  return new codex_1.CodexProvider();
197
266
  })();
198
267
  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);
268
+ const candidates = [];
269
+ if (options.workspacePath) {
270
+ const workspaceSessions = provider.findAllSessions(options.workspacePath);
271
+ const workspaceHit = readLatestCodexQuotaHitFromRollouts(workspaceSessions, { maxTailBytes, maxSessionFiles });
272
+ if (workspaceHit)
273
+ candidates.push(workspaceHit);
205
274
  }
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) {
275
+ const accountSessions = findAccountRolloutFiles(options.codexHome);
276
+ const accountHit = readLatestCodexQuotaHitFromRollouts(accountSessions, { maxTailBytes, maxSessionFiles });
277
+ if (accountHit)
278
+ candidates.push(accountHit);
279
+ const latestHit = candidates.reduce((latest, candidate) => isPreferredQuotaHit(candidate, latest) ? candidate : latest, null);
280
+ if (latestHit) {
210
281
  if (account)
211
- writeSnapshot('codex', account.id, accountQuota);
212
- return enrichCodexQuota(accountQuota, account);
282
+ writeSnapshot('codex', account.id, latestHit.quota);
283
+ return enrichCodexQuota(latestHit.quota, account);
213
284
  }
214
285
  const cached = account ? readSnapshot('codex', account.id) : null;
215
286
  if (cached) {
@@ -378,7 +449,7 @@ function readLatestQuotaFromRollout(sessionPath, maxTailBytes, source) {
378
449
  continue;
379
450
  const quota = quotaFromCodexRateLimits(parsed.payload.rate_limits, source, parsed.timestamp ?? new Date(stat.mtime).toISOString());
380
451
  if (quota)
381
- return { quota, filePath: sessionPath };
452
+ return { quota, filePath: sessionPath, mtimeMs: stat.mtime.getTime() };
382
453
  }
383
454
  catch {
384
455
  // Ignore malformed or partial lines.
package/dist/index.d.ts CHANGED
@@ -123,5 +123,5 @@ export { extractToolCall, extractToolCalls } from './extractors/toolCall';
123
123
  export { messageUsageSchema, sessionMessageSchema, sessionEventSchema, permissionModeSchema, } from './schemas/sessionEvent';
124
124
  export { fetchProviderStatus, fetchOpenAIStatus } from './providerStatus';
125
125
  export type { ProviderStatusState } from './providerStatus';
126
- export { fetchPeakHoursStatus } from './peakHours';
126
+ export { createPeakHoursNotApplicableState, fetchPeakHoursStatus, isClaudeCodeSessionProvider, scopePeakHoursToSessionProvider, } from './peakHours';
127
127
  export type { PeakHoursState } from './peakHours';
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ 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
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
8
  exports.appendQuotaHistorySample = 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 = exports.fetchCodexQuotaFromApi = exports.getWorkspaceIdFromPath = exports.pruneQuotaHistory = exports.readQuotaHistoryDailyBuckets = exports.readQuotaHistoryRange = void 0;
9
+ exports.scopePeakHoursToSessionProvider = exports.isClaudeCodeSessionProvider = exports.fetchPeakHoursStatus = exports.createPeakHoursNotApplicableState = 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 = exports.fetchCodexQuotaFromApi = exports.getWorkspaceIdFromPath = exports.pruneQuotaHistory = exports.readQuotaHistoryDailyBuckets = exports.readQuotaHistoryRange = 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; } });
@@ -295,4 +295,7 @@ Object.defineProperty(exports, "fetchProviderStatus", { enumerable: true, get: f
295
295
  Object.defineProperty(exports, "fetchOpenAIStatus", { enumerable: true, get: function () { return providerStatus_1.fetchOpenAIStatus; } });
296
296
  // Peak Hours (PromoClock — third-party)
297
297
  var peakHours_1 = require("./peakHours");
298
+ Object.defineProperty(exports, "createPeakHoursNotApplicableState", { enumerable: true, get: function () { return peakHours_1.createPeakHoursNotApplicableState; } });
298
299
  Object.defineProperty(exports, "fetchPeakHoursStatus", { enumerable: true, get: function () { return peakHours_1.fetchPeakHoursStatus; } });
300
+ Object.defineProperty(exports, "isClaudeCodeSessionProvider", { enumerable: true, get: function () { return peakHours_1.isClaudeCodeSessionProvider; } });
301
+ Object.defineProperty(exports, "scopePeakHoursToSessionProvider", { enumerable: true, get: function () { return peakHours_1.scopePeakHoursToSessionProvider; } });
@@ -9,6 +9,7 @@
9
9
  *
10
10
  * Polling / eventing is the caller's responsibility.
11
11
  */
12
+ import type { ProviderId } from './providers/types';
12
13
  export interface PeakHoursState {
13
14
  status: 'peak' | 'off_peak' | 'unknown';
14
15
  isPeak: boolean;
@@ -20,7 +21,11 @@ export interface PeakHoursState {
20
21
  note: string;
21
22
  updatedAt: string;
22
23
  unavailable: boolean;
24
+ notApplicable?: boolean;
23
25
  }
26
+ export declare function isClaudeCodeSessionProvider(providerId: ProviderId): boolean;
27
+ export declare function createPeakHoursNotApplicableState(providerId: ProviderId): PeakHoursState;
28
+ export declare function scopePeakHoursToSessionProvider(providerId: ProviderId, status: PeakHoursState | null | undefined): PeakHoursState | null;
24
29
  /**
25
30
  * Fetch current Claude peak-hours state from promoclock.co.
26
31
  *
package/dist/peakHours.js CHANGED
@@ -11,6 +11,9 @@
11
11
  * Polling / eventing is the caller's responsibility.
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.isClaudeCodeSessionProvider = isClaudeCodeSessionProvider;
15
+ exports.createPeakHoursNotApplicableState = createPeakHoursNotApplicableState;
16
+ exports.scopePeakHoursToSessionProvider = scopePeakHoursToSessionProvider;
14
17
  exports.fetchPeakHoursStatus = fetchPeakHoursStatus;
15
18
  const PROMOCLOCK_ENDPOINT = 'https://promoclock.co/api/status';
16
19
  function unavailableState() {
@@ -27,6 +30,35 @@ function unavailableState() {
27
30
  unavailable: true,
28
31
  };
29
32
  }
33
+ const PROVIDER_DISPLAY_NAMES = {
34
+ 'claude-code': 'Claude Code',
35
+ opencode: 'OpenCode',
36
+ codex: 'Codex CLI',
37
+ };
38
+ function isClaudeCodeSessionProvider(providerId) {
39
+ return providerId === 'claude-code';
40
+ }
41
+ function createPeakHoursNotApplicableState(providerId) {
42
+ const providerName = PROVIDER_DISPLAY_NAMES[providerId] ?? providerId;
43
+ return {
44
+ status: 'unknown',
45
+ isPeak: false,
46
+ sessionLimitSpeed: 'unknown',
47
+ label: 'Claude peak hours not applicable',
48
+ peakHoursDescription: '',
49
+ nextChange: null,
50
+ minutesUntilChange: null,
51
+ note: `Claude peak hours apply only to Claude Code sessions, not ${providerName}.`,
52
+ updatedAt: new Date().toISOString(),
53
+ unavailable: true,
54
+ notApplicable: true,
55
+ };
56
+ }
57
+ function scopePeakHoursToSessionProvider(providerId, status) {
58
+ if (!isClaudeCodeSessionProvider(providerId))
59
+ return null;
60
+ return status ?? null;
61
+ }
30
62
  function normalizeStatus(raw) {
31
63
  if (raw === 'peak' || raw === 'off_peak')
32
64
  return raw;
@@ -83,6 +83,31 @@ function writeStore(store) {
83
83
  ensureConfigDir();
84
84
  atomicWriteJson(getQuotaSnapshotPath(), store);
85
85
  }
86
+ function snapshotTimeMs(quota) {
87
+ const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : NaN;
88
+ return Number.isFinite(capturedAt) ? capturedAt : 0;
89
+ }
90
+ function windowResetMs(value) {
91
+ const ms = Date.parse(value);
92
+ return Number.isFinite(ms) ? ms : 0;
93
+ }
94
+ // Preserve the best-known same-window snapshot while still allowing lower
95
+ // utilization after Codex advances to a newer reset window.
96
+ function shouldKeepExistingSnapshot(existing, next) {
97
+ const existingPrimaryReset = windowResetMs(existing.fiveHour.resetsAt);
98
+ const nextPrimaryReset = windowResetMs(next.fiveHour.resetsAt);
99
+ if (existingPrimaryReset !== nextPrimaryReset)
100
+ return existingPrimaryReset > nextPrimaryReset;
101
+ const existingSecondaryReset = windowResetMs(existing.sevenDay.resetsAt);
102
+ const nextSecondaryReset = windowResetMs(next.sevenDay.resetsAt);
103
+ if (existingSecondaryReset !== nextSecondaryReset)
104
+ return existingSecondaryReset > nextSecondaryReset;
105
+ const existingUtilization = existing.fiveHour.utilization + existing.sevenDay.utilization;
106
+ const nextUtilization = next.fiveHour.utilization + next.sevenDay.utilization;
107
+ if (existingUtilization !== nextUtilization)
108
+ return existingUtilization > nextUtilization;
109
+ return snapshotTimeMs(existing) > snapshotTimeMs(next);
110
+ }
86
111
  function writeQuotaSnapshot(providerId, accountId, quota) {
87
112
  const store = readStore();
88
113
  const snapshot = {
@@ -93,6 +118,9 @@ function writeQuotaSnapshot(providerId, accountId, quota) {
93
118
  stale: false,
94
119
  };
95
120
  const index = store.snapshots.findIndex(item => item.providerId === providerId && item.accountId === accountId);
121
+ if (index >= 0 && shouldKeepExistingSnapshot(store.snapshots[index].quota, snapshot)) {
122
+ return;
123
+ }
96
124
  const record = {
97
125
  providerId,
98
126
  accountId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.18.3",
3
+ "version": "0.18.4",
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",