sidekick-shared 0.27.6 → 0.28.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.
- package/CHANGELOG.md +28 -0
- package/README.md +58 -18
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +2 -9
- package/dist/claudeProfiles.js +10 -25
- package/dist/codexPaths.d.ts +22 -0
- package/dist/codexPaths.js +67 -0
- package/dist/codexProfiles.d.ts +1 -4
- package/dist/codexProfiles.js +5 -43
- package/dist/doctor.js +2 -2
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -15
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { getActiveCodexAccount, getCodexMonitoringHomes, resolveSidekickCodexHome, } from './codexProfiles';
|
|
4
|
+
import { readQuotaSnapshot, writeQuotaSnapshot } from './quotaSnapshots';
|
|
5
|
+
import { FIVE_HOUR_WINDOW_MS, SEVEN_DAY_WINDOW_MS, withQuotaProjections } from './quota';
|
|
6
|
+
import { CodexProvider } from './providers/codex';
|
|
7
|
+
import { walkRolloutFiles } from './providers/rolloutWalker';
|
|
8
|
+
import { isAggregateCodexLimit } from './types/codex';
|
|
9
|
+
const DEFAULT_TAIL_BYTES = 2 * 1024 * 1024;
|
|
10
|
+
const DEFAULT_MAX_SESSION_FILES = 50;
|
|
11
|
+
const CHATGPT_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
12
|
+
const CHATGPT_RESET_CREDITS_URL = 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits';
|
|
13
|
+
function timestampMs(value, fallbackMs = 0) {
|
|
14
|
+
if (!value)
|
|
15
|
+
return fallbackMs;
|
|
16
|
+
const ms = Date.parse(value);
|
|
17
|
+
return Number.isFinite(ms) ? ms : fallbackMs;
|
|
18
|
+
}
|
|
19
|
+
// Codex can report slightly different percentages for the same reset window
|
|
20
|
+
// across recent sessions. Prefer the aggregate plan family ("codex") over
|
|
21
|
+
// model/feature-specific families (which can read 0% while the plan is busy),
|
|
22
|
+
// then newer reset windows, then the highest observed same-window utilization so
|
|
23
|
+
// local fallbacks do not under-report quota usage.
|
|
24
|
+
function isPreferredQuotaHit(candidate, current) {
|
|
25
|
+
if (!current)
|
|
26
|
+
return true;
|
|
27
|
+
// Family rank first: an aggregate hit always outranks a model-specific one, so a
|
|
28
|
+
// freshly-used per-model family (e.g. codex_bengalfox at 0%) can never mask the plan quota.
|
|
29
|
+
const candidateAggregate = isAggregateCodexLimit(candidate.quota.limitId);
|
|
30
|
+
const currentAggregate = isAggregateCodexLimit(current.quota.limitId);
|
|
31
|
+
if (candidateAggregate !== currentAggregate)
|
|
32
|
+
return candidateAggregate;
|
|
33
|
+
const candidatePrimaryReset = timestampMs(candidate.quota.fiveHour.resetsAt);
|
|
34
|
+
const currentPrimaryReset = timestampMs(current.quota.fiveHour.resetsAt);
|
|
35
|
+
if (candidatePrimaryReset !== currentPrimaryReset)
|
|
36
|
+
return candidatePrimaryReset > currentPrimaryReset;
|
|
37
|
+
const candidateSecondaryReset = timestampMs(candidate.quota.sevenDay.resetsAt);
|
|
38
|
+
const currentSecondaryReset = timestampMs(current.quota.sevenDay.resetsAt);
|
|
39
|
+
if (candidateSecondaryReset !== currentSecondaryReset)
|
|
40
|
+
return candidateSecondaryReset > currentSecondaryReset;
|
|
41
|
+
const candidateUtilization = candidate.quota.fiveHour.utilization + candidate.quota.sevenDay.utilization;
|
|
42
|
+
const currentUtilization = current.quota.fiveHour.utilization + current.quota.sevenDay.utilization;
|
|
43
|
+
if (candidateUtilization !== currentUtilization)
|
|
44
|
+
return candidateUtilization > currentUtilization;
|
|
45
|
+
const candidateMs = timestampMs(candidate.quota.capturedAt, candidate.mtimeMs);
|
|
46
|
+
const currentMs = timestampMs(current.quota.capturedAt, current.mtimeMs);
|
|
47
|
+
if (candidateMs !== currentMs)
|
|
48
|
+
return candidateMs > currentMs;
|
|
49
|
+
return candidate.mtimeMs > current.mtimeMs;
|
|
50
|
+
}
|
|
51
|
+
function normalizePercent(value) {
|
|
52
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
53
|
+
}
|
|
54
|
+
function windowMinutesToMs(value, fallbackMs) {
|
|
55
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
56
|
+
? value * 60_000
|
|
57
|
+
: fallbackMs;
|
|
58
|
+
}
|
|
59
|
+
function timestampToIso(seconds) {
|
|
60
|
+
if (typeof seconds !== 'number' || !Number.isFinite(seconds) || seconds <= 0) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
return new Date(seconds * 1000).toISOString();
|
|
64
|
+
}
|
|
65
|
+
function accountEmail(account) {
|
|
66
|
+
return account?.email ?? account?.metadata?.email;
|
|
67
|
+
}
|
|
68
|
+
/** Stamp a Codex quota sample with its runtime provider, projections, and account identity. */
|
|
69
|
+
export function enrichCodexQuota(state, account) {
|
|
70
|
+
const withProjections = withQuotaProjections(state);
|
|
71
|
+
return {
|
|
72
|
+
...withProjections,
|
|
73
|
+
runtimeProvider: 'codex',
|
|
74
|
+
providerId: 'codex',
|
|
75
|
+
accountLabel: account?.label,
|
|
76
|
+
accountDetail: accountEmail(account),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function unavailableCodexQuota(error, account, meta = {}) {
|
|
80
|
+
return enrichCodexQuota({
|
|
81
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
82
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
83
|
+
available: false,
|
|
84
|
+
error,
|
|
85
|
+
providerId: 'codex',
|
|
86
|
+
fiveHourLabel: 'Primary',
|
|
87
|
+
sevenDayLabel: 'Secondary',
|
|
88
|
+
...meta,
|
|
89
|
+
}, account);
|
|
90
|
+
}
|
|
91
|
+
function parseRetryAfterMs(retryAfter) {
|
|
92
|
+
if (!retryAfter)
|
|
93
|
+
return undefined;
|
|
94
|
+
const seconds = Number(retryAfter);
|
|
95
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
96
|
+
return Math.round(seconds * 1000);
|
|
97
|
+
}
|
|
98
|
+
const retryAt = Date.parse(retryAfter);
|
|
99
|
+
if (Number.isNaN(retryAt))
|
|
100
|
+
return undefined;
|
|
101
|
+
return Math.max(retryAt - Date.now(), 0);
|
|
102
|
+
}
|
|
103
|
+
function firstString(value) {
|
|
104
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
105
|
+
}
|
|
106
|
+
function normalizeCredits(credits) {
|
|
107
|
+
if (!credits)
|
|
108
|
+
return undefined;
|
|
109
|
+
return {
|
|
110
|
+
hasCredits: credits.has_credits ?? credits.hasCredits,
|
|
111
|
+
unlimited: credits.unlimited,
|
|
112
|
+
balance: credits.balance ?? undefined,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function normalizeCount(value) {
|
|
116
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
117
|
+
}
|
|
118
|
+
function normalizeResetCreditsPayload(payload, capturedAt) {
|
|
119
|
+
const credits = Array.isArray(payload.credits)
|
|
120
|
+
? payload.credits.flatMap((credit) => {
|
|
121
|
+
const status = firstString(credit.status);
|
|
122
|
+
const expiresAt = firstString(credit.expires_at);
|
|
123
|
+
if (!status || !expiresAt)
|
|
124
|
+
return [];
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
title: firstString(credit.title),
|
|
128
|
+
status,
|
|
129
|
+
resetType: firstString(credit.reset_type),
|
|
130
|
+
expiresAt,
|
|
131
|
+
grantedAt: firstString(credit.granted_at),
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
})
|
|
135
|
+
: [];
|
|
136
|
+
return {
|
|
137
|
+
availableCount: normalizeCount(payload.available_count),
|
|
138
|
+
totalEarnedCount: typeof payload.total_earned_count === 'number' && Number.isFinite(payload.total_earned_count)
|
|
139
|
+
? payload.total_earned_count
|
|
140
|
+
: undefined,
|
|
141
|
+
credits,
|
|
142
|
+
source: 'api',
|
|
143
|
+
capturedAt,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function normalizeRateLimitReachedType(value) {
|
|
147
|
+
if (typeof value === 'string')
|
|
148
|
+
return value;
|
|
149
|
+
return firstString(value?.kind);
|
|
150
|
+
}
|
|
151
|
+
function normalizeApiWindow(window) {
|
|
152
|
+
if (!window)
|
|
153
|
+
return undefined;
|
|
154
|
+
const windowMinutes = typeof window.window_minutes === 'number' || window.window_minutes === null
|
|
155
|
+
? window.window_minutes
|
|
156
|
+
: typeof window.limit_window_seconds === 'number'
|
|
157
|
+
? Math.round(window.limit_window_seconds / 60)
|
|
158
|
+
: undefined;
|
|
159
|
+
const resetsAt = typeof window.resets_at === 'number' || window.resets_at === null
|
|
160
|
+
? window.resets_at
|
|
161
|
+
: window.reset_at;
|
|
162
|
+
return {
|
|
163
|
+
used_percent: normalizePercent(window.used_percent),
|
|
164
|
+
window_minutes: windowMinutes,
|
|
165
|
+
resets_at: resetsAt,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function rateLimitsFromUsagePayload(payload) {
|
|
169
|
+
if (payload.primary || payload.secondary) {
|
|
170
|
+
return {
|
|
171
|
+
limit_id: payload.limit_id ?? 'codex',
|
|
172
|
+
limit_name: payload.limit_name ?? null,
|
|
173
|
+
primary: payload.primary,
|
|
174
|
+
secondary: payload.secondary,
|
|
175
|
+
credits: normalizeCredits(payload.credits),
|
|
176
|
+
plan_type: payload.plan_type ?? undefined,
|
|
177
|
+
rate_limit_reached_type: normalizeRateLimitReachedType(payload.rate_limit_reached_type),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const preferred = payload.rate_limit;
|
|
181
|
+
return {
|
|
182
|
+
limit_id: 'codex',
|
|
183
|
+
limit_name: null,
|
|
184
|
+
primary: normalizeApiWindow(preferred?.primary_window),
|
|
185
|
+
secondary: normalizeApiWindow(preferred?.secondary_window),
|
|
186
|
+
credits: normalizeCredits(payload.credits),
|
|
187
|
+
plan_type: payload.plan_type ?? undefined,
|
|
188
|
+
rate_limit_reached_type: normalizeRateLimitReachedType(payload.rate_limit_reached_type),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
export function quotaFromCodexRateLimits(rateLimits, source = 'session', capturedAt = new Date().toISOString()) {
|
|
192
|
+
const primary = rateLimits?.primary;
|
|
193
|
+
const secondary = rateLimits?.secondary;
|
|
194
|
+
if (!primary && !secondary)
|
|
195
|
+
return null;
|
|
196
|
+
return withQuotaProjections({
|
|
197
|
+
fiveHour: primary
|
|
198
|
+
? {
|
|
199
|
+
utilization: normalizePercent(primary.used_percent),
|
|
200
|
+
resetsAt: timestampToIso(primary.resets_at),
|
|
201
|
+
}
|
|
202
|
+
: { utilization: 0, resetsAt: '' },
|
|
203
|
+
sevenDay: secondary
|
|
204
|
+
? {
|
|
205
|
+
utilization: normalizePercent(secondary.used_percent),
|
|
206
|
+
resetsAt: timestampToIso(secondary.resets_at),
|
|
207
|
+
}
|
|
208
|
+
: { utilization: 0, resetsAt: '' },
|
|
209
|
+
available: true,
|
|
210
|
+
providerId: 'codex',
|
|
211
|
+
source,
|
|
212
|
+
capturedAt,
|
|
213
|
+
stale: source === 'cache',
|
|
214
|
+
fiveHourLabel: 'Primary',
|
|
215
|
+
sevenDayLabel: 'Secondary',
|
|
216
|
+
limitId: rateLimits?.limit_id,
|
|
217
|
+
limitName: rateLimits?.limit_name ?? undefined,
|
|
218
|
+
credits: rateLimits?.credits,
|
|
219
|
+
planType: rateLimits?.plan_type,
|
|
220
|
+
rateLimitReachedType: rateLimits?.rate_limit_reached_type,
|
|
221
|
+
}, {
|
|
222
|
+
fiveHourWindowMs: windowMinutesToMs(primary?.window_minutes, FIVE_HOUR_WINDOW_MS),
|
|
223
|
+
sevenDayWindowMs: windowMinutesToMs(secondary?.window_minutes, SEVEN_DAY_WINDOW_MS),
|
|
224
|
+
capturedAt,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
export function readLatestCodexQuotaFromRollouts(sessionPaths, options = {}) {
|
|
228
|
+
return readLatestCodexQuotaHitFromRollouts(sessionPaths, options)?.quota ?? null;
|
|
229
|
+
}
|
|
230
|
+
function readLatestCodexQuotaHitFromRollouts(sessionPaths, options = {}) {
|
|
231
|
+
const maxSessionFiles = options.maxSessionFiles ?? DEFAULT_MAX_SESSION_FILES;
|
|
232
|
+
const maxTailBytes = options.maxTailBytes ?? DEFAULT_TAIL_BYTES;
|
|
233
|
+
let latest = null;
|
|
234
|
+
for (const sessionPath of sessionPaths.slice(0, maxSessionFiles)) {
|
|
235
|
+
const hit = readLatestQuotaFromRollout(sessionPath, maxTailBytes, options.source ?? 'session');
|
|
236
|
+
if (hit && isPreferredQuotaHit(hit, latest)) {
|
|
237
|
+
latest = hit;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return latest;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The newest account-level rollouts, one capped walk across the monitored
|
|
244
|
+
* homes. `limit` bounds the walk itself, so a large history is never fully
|
|
245
|
+
* enumerated or stat'ed just to tail a handful of files.
|
|
246
|
+
*/
|
|
247
|
+
function findAccountRolloutFiles(codexHome, limit) {
|
|
248
|
+
const homes = codexHome ? [codexHome] : getCodexMonitoringHomes();
|
|
249
|
+
return walkRolloutFiles(homes.map((home) => path.join(home, 'sessions')), { limit }).map((file) => file.path);
|
|
250
|
+
}
|
|
251
|
+
export function resolveCodexQuotaFromLocalSources(options = {}) {
|
|
252
|
+
const account = options.activeAccount !== undefined ? options.activeAccount : getActiveCodexAccount();
|
|
253
|
+
const readSnapshot = options.readSnapshot ?? readQuotaSnapshot;
|
|
254
|
+
const writeSnapshot = options.writeSnapshot ?? writeQuotaSnapshot;
|
|
255
|
+
const maxTailBytes = options.maxTailBytes ?? DEFAULT_TAIL_BYTES;
|
|
256
|
+
const maxSessionFiles = options.maxSessionFiles ?? DEFAULT_MAX_SESSION_FILES;
|
|
257
|
+
let ownProvider = false;
|
|
258
|
+
const provider = options.provider ??
|
|
259
|
+
(() => {
|
|
260
|
+
ownProvider = true;
|
|
261
|
+
return new CodexProvider();
|
|
262
|
+
})();
|
|
263
|
+
try {
|
|
264
|
+
const candidates = [];
|
|
265
|
+
if (options.workspacePath) {
|
|
266
|
+
const workspaceSessions = provider.findAllSessions(options.workspacePath);
|
|
267
|
+
const workspaceHit = readLatestCodexQuotaHitFromRollouts(workspaceSessions, {
|
|
268
|
+
maxTailBytes,
|
|
269
|
+
maxSessionFiles,
|
|
270
|
+
});
|
|
271
|
+
if (workspaceHit)
|
|
272
|
+
candidates.push(workspaceHit);
|
|
273
|
+
}
|
|
274
|
+
const accountSessions = findAccountRolloutFiles(options.codexHome, maxSessionFiles);
|
|
275
|
+
const accountHit = readLatestCodexQuotaHitFromRollouts(accountSessions, {
|
|
276
|
+
maxTailBytes,
|
|
277
|
+
maxSessionFiles,
|
|
278
|
+
});
|
|
279
|
+
if (accountHit)
|
|
280
|
+
candidates.push(accountHit);
|
|
281
|
+
const latestHit = candidates.reduce((latest, candidate) => (isPreferredQuotaHit(candidate, latest) ? candidate : latest), null);
|
|
282
|
+
if (latestHit) {
|
|
283
|
+
if (account)
|
|
284
|
+
writeSnapshot('codex', account.id, latestHit.quota);
|
|
285
|
+
return enrichCodexQuota(latestHit.quota, account);
|
|
286
|
+
}
|
|
287
|
+
const cached = account ? readSnapshot('codex', account.id) : null;
|
|
288
|
+
if (cached) {
|
|
289
|
+
return enrichCodexQuota({
|
|
290
|
+
...cached,
|
|
291
|
+
providerId: 'codex',
|
|
292
|
+
source: 'cache',
|
|
293
|
+
stale: true,
|
|
294
|
+
fiveHourLabel: cached.fiveHourLabel ?? 'Primary',
|
|
295
|
+
sevenDayLabel: cached.sevenDayLabel ?? 'Secondary',
|
|
296
|
+
}, account);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
if (ownProvider)
|
|
301
|
+
provider.dispose();
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
export async function resolveCodexQuota(options = {}) {
|
|
306
|
+
const source = options.source ?? 'local';
|
|
307
|
+
const account = options.activeAccount !== undefined ? options.activeAccount : getActiveCodexAccount();
|
|
308
|
+
const writeSnapshot = options.writeSnapshot ?? writeQuotaSnapshot;
|
|
309
|
+
if (source === 'api') {
|
|
310
|
+
const apiQuota = await fetchCodexQuotaFromApi(options);
|
|
311
|
+
if (apiQuota.available) {
|
|
312
|
+
if (account)
|
|
313
|
+
writeSnapshot('codex', account.id, apiQuota);
|
|
314
|
+
return enrichCodexQuota(apiQuota, account);
|
|
315
|
+
}
|
|
316
|
+
const fallback = resolveCodexQuotaFromLocalSources(options);
|
|
317
|
+
return fallback ?? enrichCodexQuota(apiQuota, account);
|
|
318
|
+
}
|
|
319
|
+
const local = resolveCodexQuotaFromLocalSources(options);
|
|
320
|
+
if (local)
|
|
321
|
+
return local;
|
|
322
|
+
if (source === 'auto') {
|
|
323
|
+
const apiQuota = await fetchCodexQuotaFromApi(options);
|
|
324
|
+
if (apiQuota.available && account) {
|
|
325
|
+
writeSnapshot('codex', account.id, apiQuota);
|
|
326
|
+
}
|
|
327
|
+
return enrichCodexQuota(apiQuota, account);
|
|
328
|
+
}
|
|
329
|
+
return unavailableCodexQuota(account
|
|
330
|
+
? `No Codex rate-limit data is available for "${account.label ?? account.id}".`
|
|
331
|
+
: 'No Codex rate-limit data is available.', account, { source: 'session' });
|
|
332
|
+
}
|
|
333
|
+
export async function fetchCodexQuotaFromApi(options = {}) {
|
|
334
|
+
const capturedAt = options.capturedAt ?? new Date().toISOString();
|
|
335
|
+
const auth = options.accessToken != null
|
|
336
|
+
? { accessToken: options.accessToken, accountId: options.accountId }
|
|
337
|
+
: readCodexAuth(options.codexHome ?? resolveSidekickCodexHome());
|
|
338
|
+
const accessToken = auth?.accessToken;
|
|
339
|
+
if (!accessToken) {
|
|
340
|
+
return {
|
|
341
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
342
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
343
|
+
available: false,
|
|
344
|
+
error: 'Codex API refresh requires a ChatGPT login.',
|
|
345
|
+
failureKind: 'auth',
|
|
346
|
+
providerId: 'codex',
|
|
347
|
+
source: 'api',
|
|
348
|
+
capturedAt,
|
|
349
|
+
fiveHourLabel: 'Primary',
|
|
350
|
+
sevenDayLabel: 'Secondary',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const controller = new AbortController();
|
|
354
|
+
const onAbort = () => controller.abort();
|
|
355
|
+
if (options.signal?.aborted)
|
|
356
|
+
controller.abort();
|
|
357
|
+
else
|
|
358
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
359
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
|
|
360
|
+
timeout.unref();
|
|
361
|
+
try {
|
|
362
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
363
|
+
const response = await fetchImpl(options.usageUrl ?? CHATGPT_USAGE_URL, {
|
|
364
|
+
method: 'GET',
|
|
365
|
+
signal: controller.signal,
|
|
366
|
+
headers: {
|
|
367
|
+
Authorization: `Bearer ${accessToken}`,
|
|
368
|
+
Accept: 'application/json',
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
if (!response.ok) {
|
|
372
|
+
return {
|
|
373
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
374
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
375
|
+
available: false,
|
|
376
|
+
error: `Codex usage API error: ${response.status}`,
|
|
377
|
+
failureKind: response.status === 401 || response.status === 403
|
|
378
|
+
? 'auth'
|
|
379
|
+
: response.status === 429
|
|
380
|
+
? 'rate_limit'
|
|
381
|
+
: response.status >= 500 && response.status <= 599
|
|
382
|
+
? 'server'
|
|
383
|
+
: 'unknown',
|
|
384
|
+
httpStatus: response.status,
|
|
385
|
+
retryAfterMs: response.status === 429
|
|
386
|
+
? parseRetryAfterMs(response.headers.get('retry-after'))
|
|
387
|
+
: undefined,
|
|
388
|
+
providerId: 'codex',
|
|
389
|
+
source: 'api',
|
|
390
|
+
capturedAt,
|
|
391
|
+
fiveHourLabel: 'Primary',
|
|
392
|
+
sevenDayLabel: 'Secondary',
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const payload = (await response.json());
|
|
396
|
+
const quota = quotaFromCodexRateLimits(rateLimitsFromUsagePayload(payload), 'api', capturedAt);
|
|
397
|
+
if (!quota) {
|
|
398
|
+
return {
|
|
399
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
400
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
401
|
+
available: false,
|
|
402
|
+
error: 'Codex usage API returned no rate-limit windows.',
|
|
403
|
+
failureKind: 'unknown',
|
|
404
|
+
providerId: 'codex',
|
|
405
|
+
source: 'api',
|
|
406
|
+
capturedAt,
|
|
407
|
+
fiveHourLabel: 'Primary',
|
|
408
|
+
sevenDayLabel: 'Secondary',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
const resetCredits = await fetchCodexResetCreditsFromApi({
|
|
412
|
+
...options,
|
|
413
|
+
accessToken,
|
|
414
|
+
accountId: auth?.accountId,
|
|
415
|
+
capturedAt,
|
|
416
|
+
signal: controller.signal,
|
|
417
|
+
});
|
|
418
|
+
if (resetCredits) {
|
|
419
|
+
quota.resetCredits = resetCredits;
|
|
420
|
+
}
|
|
421
|
+
return quota;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return {
|
|
425
|
+
fiveHour: { utilization: 0, resetsAt: '' },
|
|
426
|
+
sevenDay: { utilization: 0, resetsAt: '' },
|
|
427
|
+
available: false,
|
|
428
|
+
error: 'Codex usage API network error',
|
|
429
|
+
failureKind: 'network',
|
|
430
|
+
providerId: 'codex',
|
|
431
|
+
source: 'api',
|
|
432
|
+
capturedAt,
|
|
433
|
+
fiveHourLabel: 'Primary',
|
|
434
|
+
sevenDayLabel: 'Secondary',
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
clearTimeout(timeout);
|
|
439
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
export async function fetchCodexResetCreditsFromApi(options = {}) {
|
|
443
|
+
const capturedAt = options.capturedAt ?? new Date().toISOString();
|
|
444
|
+
const auth = options.accessToken != null
|
|
445
|
+
? { accessToken: options.accessToken, accountId: options.accountId }
|
|
446
|
+
: readCodexAuth(options.codexHome ?? resolveSidekickCodexHome());
|
|
447
|
+
const accessToken = auth?.accessToken;
|
|
448
|
+
if (!accessToken)
|
|
449
|
+
return undefined;
|
|
450
|
+
const controller = new AbortController();
|
|
451
|
+
const onAbort = () => controller.abort();
|
|
452
|
+
if (options.signal?.aborted)
|
|
453
|
+
controller.abort();
|
|
454
|
+
else
|
|
455
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
456
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
|
|
457
|
+
timeout.unref();
|
|
458
|
+
try {
|
|
459
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
460
|
+
const headers = {
|
|
461
|
+
Authorization: `Bearer ${accessToken}`,
|
|
462
|
+
originator: 'Codex Desktop',
|
|
463
|
+
'OAI-Product-Sku': 'CODEX',
|
|
464
|
+
Accept: 'application/json',
|
|
465
|
+
};
|
|
466
|
+
if (auth?.accountId) {
|
|
467
|
+
headers['ChatGPT-Account-ID'] = auth.accountId;
|
|
468
|
+
}
|
|
469
|
+
const response = await fetchImpl(options.resetCreditsUrl ?? CHATGPT_RESET_CREDITS_URL, {
|
|
470
|
+
method: 'GET',
|
|
471
|
+
headers,
|
|
472
|
+
signal: controller.signal,
|
|
473
|
+
});
|
|
474
|
+
if (!response.ok)
|
|
475
|
+
return undefined;
|
|
476
|
+
return normalizeResetCreditsPayload((await response.json()), capturedAt);
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
clearTimeout(timeout);
|
|
483
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
function readCodexAuth(codexHome) {
|
|
487
|
+
try {
|
|
488
|
+
const parsed = JSON.parse(fs.readFileSync(path.join(codexHome, 'auth.json'), 'utf8'));
|
|
489
|
+
if (parsed.OPENAI_API_KEY || parsed.auth_mode === 'api_key')
|
|
490
|
+
return null;
|
|
491
|
+
const accessToken = parsed.tokens?.access_token;
|
|
492
|
+
if (!accessToken)
|
|
493
|
+
return null;
|
|
494
|
+
return {
|
|
495
|
+
accessToken,
|
|
496
|
+
accountId: parsed.tokens?.account_id,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function readLatestQuotaFromRollout(sessionPath, maxTailBytes, source) {
|
|
504
|
+
let fd = null;
|
|
505
|
+
try {
|
|
506
|
+
const stat = fs.statSync(sessionPath);
|
|
507
|
+
if (!stat.isFile() || stat.size <= 0)
|
|
508
|
+
return null;
|
|
509
|
+
const start = Math.max(0, stat.size - maxTailBytes);
|
|
510
|
+
const bytesToRead = stat.size - start;
|
|
511
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
512
|
+
fd = fs.openSync(sessionPath, 'r');
|
|
513
|
+
const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, start);
|
|
514
|
+
fs.closeSync(fd);
|
|
515
|
+
fd = null;
|
|
516
|
+
let text = buffer.toString('utf8', 0, bytesRead);
|
|
517
|
+
if (start > 0) {
|
|
518
|
+
const firstNewline = text.indexOf('\n');
|
|
519
|
+
text = firstNewline >= 0 ? text.slice(firstNewline + 1) : '';
|
|
520
|
+
}
|
|
521
|
+
// Scan from the end for the latest aggregate ("codex") rate-limit sample, which
|
|
522
|
+
// is what the plan quota represents. Keep the latest sample of any family as a
|
|
523
|
+
// fallback so a model-only session still surfaces something.
|
|
524
|
+
const lines = text.split('\n');
|
|
525
|
+
let fallback = null;
|
|
526
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
527
|
+
const line = lines[i].trim();
|
|
528
|
+
if (!line || !line.includes('rate_limits'))
|
|
529
|
+
continue;
|
|
530
|
+
try {
|
|
531
|
+
const parsed = JSON.parse(line);
|
|
532
|
+
if (parsed.type !== 'event_msg' || parsed.payload?.type !== 'token_count')
|
|
533
|
+
continue;
|
|
534
|
+
const quota = quotaFromCodexRateLimits(parsed.payload.rate_limits, source, parsed.timestamp ?? new Date(stat.mtime).toISOString());
|
|
535
|
+
if (!quota)
|
|
536
|
+
continue;
|
|
537
|
+
const hit = { quota, filePath: sessionPath, mtimeMs: stat.mtime.getTime() };
|
|
538
|
+
if (isAggregateCodexLimit(quota.limitId))
|
|
539
|
+
return hit;
|
|
540
|
+
if (!fallback)
|
|
541
|
+
fallback = hit;
|
|
542
|
+
}
|
|
543
|
+
catch {
|
|
544
|
+
// Ignore malformed or partial lines.
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (fallback)
|
|
548
|
+
return fallback;
|
|
549
|
+
}
|
|
550
|
+
catch {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
if (fd !== null) {
|
|
555
|
+
try {
|
|
556
|
+
fs.closeSync(fd);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
/* ignore */
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
}
|