sidekick-shared 0.28.2 → 0.29.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 (142) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +176 -30
  3. package/dist/authStatus.d.ts +54 -0
  4. package/dist/authStatus.js +91 -0
  5. package/dist/browser.d.ts +14 -6
  6. package/dist/browser.js +26 -1
  7. package/dist/codex.d.ts +23 -0
  8. package/dist/codex.js +44 -0
  9. package/dist/codexProfiles.d.ts +1 -0
  10. package/dist/codexProfiles.js +8 -11
  11. package/dist/codexQuota.js +17 -4
  12. package/dist/esm/authStatus.js +87 -0
  13. package/dist/esm/browser.js +6 -2
  14. package/dist/esm/codex.js +18 -0
  15. package/dist/esm/codexProfiles.js +8 -11
  16. package/dist/esm/codexQuota.js +18 -5
  17. package/dist/esm/formatting.js +10 -1
  18. package/dist/esm/index.js +12 -9
  19. package/dist/esm/modelCatalog.js +2 -1
  20. package/dist/esm/modelInfo.js +422 -45
  21. package/dist/esm/multiProviderQuotaService.js +1 -1
  22. package/dist/esm/node.js +9 -3
  23. package/dist/esm/parsers/codexParser.js +175 -54
  24. package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
  25. package/dist/esm/parsers/openCodeParser.js +83 -21
  26. package/dist/esm/pricing.js +18 -0
  27. package/dist/esm/pricingCatalog.js +14 -7
  28. package/dist/esm/promptHistory.js +66 -4
  29. package/dist/esm/providerFailure.js +124 -3
  30. package/dist/esm/providers/codex.js +2 -24
  31. package/dist/esm/providers/codexDatabase.js +3 -19
  32. package/dist/esm/providers/codexRollouts.js +24 -1
  33. package/dist/esm/providers/openCode.js +9 -17
  34. package/dist/esm/quota.js +356 -24
  35. package/dist/esm/quotaHistory.js +33 -8
  36. package/dist/esm/quotaPoller.js +5 -2
  37. package/dist/esm/quotaResolver.js +31 -6
  38. package/dist/esm/quotaSnapshots.js +65 -10
  39. package/dist/esm/quotaThresholds.js +7 -2
  40. package/dist/esm/report/htmlReportGenerator.js +42 -5
  41. package/dist/esm/report/transcriptParser.js +13 -11
  42. package/dist/esm/schemas/index.js +2 -2
  43. package/dist/esm/schemas/providerFailure.js +3 -1
  44. package/dist/esm/schemas/quota.js +30 -0
  45. package/dist/esm/schemas/quotaHistory.js +10 -2
  46. package/dist/esm/schemas/stateFile.js +9 -0
  47. package/dist/esm/schemas/transcript.js +4 -0
  48. package/dist/esm/sessionPromptHistory.js +2 -2
  49. package/dist/esm/stateFile.js +18 -3
  50. package/dist/esm/statusline/claudeStatuslinePayload.js +16 -4
  51. package/dist/esm/statusline/formatter.js +57 -18
  52. package/dist/esm/statusline/index.js +2 -2
  53. package/dist/esm/statusline/payload.js +9 -0
  54. package/dist/esm/testing/credentialGuard.js +87 -5
  55. package/dist/esm/toolCallOrigins.js +310 -0
  56. package/dist/esm/transcript.js +42 -3
  57. package/dist/esm/usage/usageEvents.js +146 -17
  58. package/dist/esm/usage/usageReports.js +39 -6
  59. package/dist/esm/watchers/jsonlTail.js +378 -59
  60. package/dist/esm/zaiQuotaApi.js +4 -2
  61. package/dist/formatting.d.ts +26 -1
  62. package/dist/formatting.js +11 -1
  63. package/dist/index.d.ts +22 -16
  64. package/dist/index.js +48 -12
  65. package/dist/modelCatalog.js +2 -1
  66. package/dist/modelInfo.d.ts +185 -16
  67. package/dist/modelInfo.js +431 -47
  68. package/dist/multiProviderQuotaService.js +1 -1
  69. package/dist/node.d.ts +16 -5
  70. package/dist/node.js +31 -2
  71. package/dist/parsers/codexParser.d.ts +55 -0
  72. package/dist/parsers/codexParser.js +175 -54
  73. package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
  74. package/dist/parsers/codexRolloutOrigin.js +111 -0
  75. package/dist/parsers/openCodeParser.d.ts +38 -0
  76. package/dist/parsers/openCodeParser.js +86 -21
  77. package/dist/pricing.d.ts +22 -0
  78. package/dist/pricing.js +50 -0
  79. package/dist/pricingCatalog.d.ts +10 -0
  80. package/dist/pricingCatalog.js +13 -6
  81. package/dist/promptHistory.d.ts +65 -4
  82. package/dist/promptHistory.js +68 -5
  83. package/dist/providerFailure.d.ts +44 -0
  84. package/dist/providerFailure.js +126 -4
  85. package/dist/providers/codex.d.ts +3 -22
  86. package/dist/providers/codex.js +3 -26
  87. package/dist/providers/codexDatabase.d.ts +3 -2
  88. package/dist/providers/codexDatabase.js +4 -22
  89. package/dist/providers/codexRollouts.d.ts +22 -0
  90. package/dist/providers/codexRollouts.js +24 -0
  91. package/dist/providers/openCode.js +8 -16
  92. package/dist/quota.d.ts +131 -1
  93. package/dist/quota.js +366 -24
  94. package/dist/quotaHistory.d.ts +10 -0
  95. package/dist/quotaHistory.js +34 -8
  96. package/dist/quotaPoller.d.ts +11 -3
  97. package/dist/quotaPoller.js +5 -2
  98. package/dist/quotaResolver.js +30 -5
  99. package/dist/quotaSnapshots.js +64 -9
  100. package/dist/quotaThresholds.js +7 -2
  101. package/dist/report/htmlReportGenerator.js +42 -5
  102. package/dist/report/transcriptParser.d.ts +10 -1
  103. package/dist/report/transcriptParser.js +13 -11
  104. package/dist/schemas/index.d.ts +3 -3
  105. package/dist/schemas/index.js +5 -2
  106. package/dist/schemas/providerFailure.d.ts +39 -0
  107. package/dist/schemas/providerFailure.js +2 -0
  108. package/dist/schemas/quota.d.ts +258 -0
  109. package/dist/schemas/quota.js +31 -1
  110. package/dist/schemas/quotaHistory.d.ts +2 -0
  111. package/dist/schemas/quotaHistory.js +10 -2
  112. package/dist/schemas/stateFile.d.ts +34 -0
  113. package/dist/schemas/stateFile.js +10 -1
  114. package/dist/schemas/transcript.d.ts +2 -0
  115. package/dist/schemas/transcript.js +4 -0
  116. package/dist/sessionPromptHistory.d.ts +7 -1
  117. package/dist/sessionPromptHistory.js +1 -1
  118. package/dist/stateFile.d.ts +18 -0
  119. package/dist/stateFile.js +17 -2
  120. package/dist/statusline/claudeStatuslinePayload.d.ts +4 -3
  121. package/dist/statusline/claudeStatuslinePayload.js +16 -4
  122. package/dist/statusline/formatter.d.ts +5 -0
  123. package/dist/statusline/formatter.js +56 -17
  124. package/dist/statusline/index.d.ts +4 -4
  125. package/dist/statusline/index.js +5 -1
  126. package/dist/statusline/payload.d.ts +10 -0
  127. package/dist/statusline/payload.js +14 -0
  128. package/dist/testing/credentialGuard.d.ts +14 -0
  129. package/dist/testing/credentialGuard.js +87 -5
  130. package/dist/toolCallOrigins.d.ts +70 -0
  131. package/dist/toolCallOrigins.js +346 -0
  132. package/dist/transcript.d.ts +12 -0
  133. package/dist/transcript.js +42 -3
  134. package/dist/types/opencode.d.ts +2 -0
  135. package/dist/usage/usageEvents.d.ts +54 -6
  136. package/dist/usage/usageEvents.js +146 -17
  137. package/dist/usage/usageReports.d.ts +35 -4
  138. package/dist/usage/usageReports.js +39 -6
  139. package/dist/watchers/jsonlTail.d.ts +101 -4
  140. package/dist/watchers/jsonlTail.js +380 -60
  141. package/dist/zaiQuotaApi.js +3 -1
  142. package/package.json +41 -1
@@ -2,7 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { getConfigDir } from './paths';
4
4
  import { atomicWriteJsonSync, withFileLockSync } from './writers/atomic';
5
- import { classifyQuotaFreshness } from './quota';
5
+ import { classifyQuotaFreshness, extraQuotaRows } from './quota';
6
6
  import { isAggregateCodexLimit } from './types/codex';
7
7
  function getQuotaSnapshotPath() {
8
8
  return path.join(getConfigDir(), 'quota-snapshots.json');
@@ -46,6 +46,27 @@ function windowResetMs(value) {
46
46
  const ms = Date.parse(value);
47
47
  return Number.isFinite(ms) ? ms : 0;
48
48
  }
49
+ /**
50
+ * Rank two samples by one window's reset time. A later reset means a newer
51
+ * window, so it wins — except that an existing window which had already reset
52
+ * by the time the newer sample was captured is obsolete and always yields.
53
+ * Without that exception a pre-reset sample outranks every later sample whose
54
+ * window has not started yet (empty reset), and the snapshot freezes.
55
+ * Returns `undefined` when the reset times tie.
56
+ */
57
+ function compareWindowResets(existing, next, asOfMs) {
58
+ const existingReset = windowResetMs(existing.resetsAt);
59
+ const nextReset = windowResetMs(next.resetsAt);
60
+ if (existingReset === nextReset)
61
+ return undefined;
62
+ if (existingReset > 0 && asOfMs > 0 && existingReset <= asOfMs)
63
+ return false;
64
+ return existingReset > nextReset;
65
+ }
66
+ function windowPresenceChanged(existing, next) {
67
+ return (Boolean(existing.fiveHour.absent) !== Boolean(next.fiveHour.absent) ||
68
+ Boolean(existing.sevenDay.absent) !== Boolean(next.sevenDay.absent));
69
+ }
49
70
  // Preserve the best-known same-window snapshot while still allowing lower
50
71
  // utilization after Codex advances to a newer reset window.
51
72
  function shouldKeepExistingSnapshot(existing, next) {
@@ -58,20 +79,50 @@ function shouldKeepExistingSnapshot(existing, next) {
58
79
  const nextAggregate = isAggregateCodexLimit(next.limitId);
59
80
  if (existingAggregate !== nextAggregate)
60
81
  return existingAggregate;
61
- const existingPrimaryReset = windowResetMs(existing.fiveHour.resetsAt);
62
- const nextPrimaryReset = windowResetMs(next.fiveHour.resetsAt);
63
- if (existingPrimaryReset !== nextPrimaryReset)
64
- return existingPrimaryReset > nextPrimaryReset;
65
- const existingSecondaryReset = windowResetMs(existing.sevenDay.resetsAt);
66
- const nextSecondaryReset = windowResetMs(next.sevenDay.resetsAt);
67
- if (existingSecondaryReset !== nextSecondaryReset)
68
- return existingSecondaryReset > nextSecondaryReset;
82
+ // The plan's window layout changed (a window appeared or disappeared): the
83
+ // reset times are not comparable, so the newer capture wins.
84
+ if (windowPresenceChanged(existing, next)) {
85
+ return snapshotTimeMs(existing) > snapshotTimeMs(next);
86
+ }
87
+ const asOfMs = snapshotTimeMs(next);
88
+ const primary = compareWindowResets(existing.fiveHour, next.fiveHour, asOfMs);
89
+ if (primary !== undefined)
90
+ return primary;
91
+ const secondary = compareWindowResets(existing.sevenDay, next.sevenDay, asOfMs);
92
+ if (secondary !== undefined)
93
+ return secondary;
69
94
  const existingUtilization = existing.fiveHour.utilization + existing.sevenDay.utilization;
70
95
  const nextUtilization = next.fiveHour.utilization + next.sevenDay.utilization;
71
96
  if (existingUtilization !== nextUtilization)
72
97
  return existingUtilization > nextUtilization;
73
98
  return snapshotTimeMs(existing) > snapshotTimeMs(next);
74
99
  }
100
+ /**
101
+ * Status-line and history samples only know the two headline windows. Keep
102
+ * the plan label, extra usage, and still-running extra meters (e.g. a Fable
103
+ * weekly cap) an earlier API sample recorded, so a fresh status-line write
104
+ * does not erase them from every snapshot reader.
105
+ */
106
+ function carryApiOnlyFields(existing, next) {
107
+ const merged = { ...next };
108
+ if (merged.planLabel === undefined && existing.planLabel !== undefined) {
109
+ merged.planLabel = existing.planLabel;
110
+ }
111
+ if (merged.planType === undefined && existing.planType !== undefined) {
112
+ merged.planType = existing.planType;
113
+ }
114
+ if (merged.extraUsage === undefined && existing.extraUsage !== undefined) {
115
+ merged.extraUsage = existing.extraUsage;
116
+ }
117
+ if (merged.limits === undefined && existing.limits !== undefined) {
118
+ const asOfMs = snapshotTimeMs(next);
119
+ // Headline meters duplicate the fresher windows on `next`; carry only the extras.
120
+ const running = extraQuotaRows(existing).filter((row) => windowResetMs(row.resetsAt) > asOfMs);
121
+ if (running.length > 0)
122
+ merged.limits = running;
123
+ }
124
+ return merged;
125
+ }
75
126
  export function writeQuotaSnapshot(providerId, accountId, quota) {
76
127
  withSnapshotStoreLock(() => {
77
128
  const store = readStore();
@@ -93,7 +144,11 @@ export function writeQuotaSnapshot(providerId, accountId, quota) {
93
144
  };
94
145
  if (index >= 0 && shouldKeepExistingSnapshot(store.snapshots[index].quota, snapshot))
95
146
  return;
96
- const record = { providerId, accountId, quota: snapshot };
147
+ const record = {
148
+ providerId,
149
+ accountId,
150
+ quota: existingQuota ? carryApiOnlyFields(existingQuota, snapshot) : snapshot,
151
+ };
97
152
  if (index >= 0)
98
153
  store.snapshots[index] = record;
99
154
  else
@@ -50,8 +50,13 @@ function evaluateWindow(window, quota, thresholds, previous) {
50
50
  export function evaluateQuotaThresholds(quota, config = DEFAULT_QUOTA_THRESHOLDS, memory = {}) {
51
51
  if (!quota.available)
52
52
  return { alerts: [], memory };
53
- const fiveHour = evaluateWindow('fiveHour', quota.fiveHour, config.fiveHour, memory.fiveHour);
54
- const sevenDay = evaluateWindow('sevenDay', quota.sevenDay, config.sevenDay, memory.sevenDay);
53
+ // A window the plan does not have (Ultra's missing five-hour limit) never alerts.
54
+ const fiveHour = quota.fiveHour.absent
55
+ ? { alert: null, memory: memory.fiveHour ?? { resetsAt: '', level: 0 } }
56
+ : evaluateWindow('fiveHour', quota.fiveHour, config.fiveHour, memory.fiveHour);
57
+ const sevenDay = quota.sevenDay.absent
58
+ ? { alert: null, memory: memory.sevenDay ?? { resetsAt: '', level: 0 } }
59
+ : evaluateWindow('sevenDay', quota.sevenDay, config.sevenDay, memory.sevenDay);
55
60
  return {
56
61
  alerts: [fiveHour.alert, sevenDay.alert].filter((alert) => alert !== null),
57
62
  memory: { fiveHour: fiveHour.memory, sevenDay: sevenDay.memory },
@@ -596,14 +596,51 @@ function renderContentBlock(block, includeThinking, includeToolDetail) {
596
596
  return '';
597
597
  }
598
598
  }
599
- function renderToolUse(block, includeDetail) {
599
+ /** Prefix of the bookkeeping keys session parsers stash inside tool inputs. */
600
+ const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
601
+ /**
602
+ * The tool's display identity and user-facing input. Entries built outside
603
+ * the canonical projection may still carry the parsers' `_sidekick*` keys in
604
+ * `toolInput`; those are hidden here and read as the raw identity.
605
+ */
606
+ function toolDisplay(block) {
600
607
  const name = block.toolName || 'unknown';
608
+ const raw = block.toolInput;
609
+ let input = raw;
610
+ let rawToolName = block.rawToolName;
611
+ let mcpServerName = block.mcpServerName;
612
+ if (raw && Object.keys(raw).some((key) => key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))) {
613
+ const clean = {};
614
+ for (const [key, value] of Object.entries(raw)) {
615
+ if (!key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
616
+ clean[key] = value;
617
+ }
618
+ input = clean;
619
+ if (!rawToolName && typeof raw._sidekickRawToolName === 'string') {
620
+ rawToolName = raw._sidekickRawToolName;
621
+ }
622
+ if (!mcpServerName && typeof raw._sidekickMcpServerName === 'string') {
623
+ mcpServerName = raw._sidekickMcpServerName;
624
+ }
625
+ }
626
+ const parts = [];
627
+ if (rawToolName && rawToolName.trim() && rawToolName !== name)
628
+ parts.push(rawToolName);
629
+ if (mcpServerName && mcpServerName.trim())
630
+ parts.push(`MCP: ${mcpServerName}`);
631
+ const hint = parts.length
632
+ ? ` <span class="tool-raw-name" style="color:var(--text-muted);font-size:11px">${escapeHtml(parts.join(' · '))}</span>`
633
+ : '';
634
+ return { name, hint, input };
635
+ }
636
+ function renderToolUse(block, includeDetail) {
637
+ const { name, hint, input } = toolDisplay(block);
601
638
  if (!includeDetail) {
602
- return `<div class="tool-block" data-block-type="tool"><div style="padding:8px 12px;background:rgba(245,166,35,0.08);color:var(--accent-orange);font-family:monospace;font-size:13px;">${escapeHtml(name)}</div></div>`;
639
+ return `<div class="tool-block" data-block-type="tool"><div style="padding:8px 12px;background:rgba(245,166,35,0.08);color:var(--accent-orange);font-family:monospace;font-size:13px;">${escapeHtml(name)}${hint}</div></div>`;
603
640
  }
604
641
  let inputHtml = '';
605
- if (block.toolInput && Object.keys(block.toolInput).length > 0) {
606
- const inputJson = JSON.stringify(block.toolInput, null, 2);
642
+ if (input && Object.keys(input).length > 0) {
643
+ const inputJson = JSON.stringify(input, null, 2);
607
644
  const highlighted = highlightCodeBlock(inputJson, 'json');
608
645
  inputHtml = `<div class="tool-io">
609
646
  <div class="tool-label">Input</div>
@@ -611,7 +648,7 @@ function renderToolUse(block, includeDetail) {
611
648
  </div>`;
612
649
  }
613
650
  return `<details class="tool-block" data-block-type="tool">
614
- <summary>${escapeHtml(name)}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${escapeHtml(block.toolUseId)}</span>` : ''}</summary>
651
+ <summary>${escapeHtml(name)}${hint}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${escapeHtml(block.toolUseId)}</span>` : ''}</summary>
615
652
  <div class="tool-content">
616
653
  ${inputHtml}
617
654
  </div>
@@ -42,17 +42,19 @@ function toReportBlock(block) {
42
42
  return block.text ? [{ type: 'text', text: block.text }] : [];
43
43
  case 'thinking':
44
44
  return block.text ? [{ type: 'thinking', text: block.text }] : [];
45
- case 'tool_use':
46
- return [
47
- {
48
- type: 'tool_use',
49
- toolName: block.toolName ?? 'unknown',
50
- toolInput: block.input && typeof block.input === 'object'
51
- ? block.input
52
- : {},
53
- toolUseId: block.toolUseId,
54
- },
55
- ];
45
+ case 'tool_use': {
46
+ const toolBlock = {
47
+ type: 'tool_use',
48
+ toolName: block.toolName ?? 'unknown',
49
+ toolInput: block.input && typeof block.input === 'object'
50
+ ? block.input
51
+ : {},
52
+ toolUseId: block.toolUseId,
53
+ ...(block.rawToolName ? { rawToolName: block.rawToolName } : {}),
54
+ ...(block.mcpServerName ? { mcpServerName: block.mcpServerName } : {}),
55
+ };
56
+ return [toolBlock];
57
+ }
56
58
  case 'tool_result':
57
59
  return [
58
60
  {
@@ -17,9 +17,9 @@ export { tokenEstimateSchema } from './tokenEstimation';
17
17
  export { providerAuthenticationEvidenceSchema, providerFailureEvidenceSchema, providerRetryAfterSchema, providerFailureDiagnosisSchema, } from './providerFailure';
18
18
  export { transcriptSourceProvenanceSchema, canonicalTranscriptBlockSchema, canonicalToolCallSchema, canonicalTranscriptMessageSchema, canonicalSessionTranscriptSchema, } from './transcript';
19
19
  export { observationProvenanceV1Schema, observedAgentSessionV1Schema, observedValueV1Schema, pendingUserRequestV1Schema, providerCapabilitiesV1Schema, providerSessionAdapterV1Schema, sessionEvidenceRefV1Schema, } from './observedSessionV1';
20
- export { quotaWindowSchema, quotaStateSchema, quotaFailureKindSchema, quotaProviderIdSchema, quotaSourceSchema, codexResetCreditSchema, codexResetCreditsSnapshotSchema, peakHoursStateSchema, quotaFailureDescriptorSchema, runtimeQuotaProviderSchema, providerQuotaStateSchema, claudeProviderQuotaStateSchema, codexProviderQuotaStateSchema, zaiProviderQuotaStateSchema, providerQuotaMapSchema, } from './quota';
20
+ export { quotaWindowSchema, quotaStateSchema, quotaLimitRowSchema, quotaExtraUsageSchema, quotaFailureKindSchema, quotaProviderIdSchema, quotaSourceSchema, codexResetCreditSchema, codexResetCreditsSnapshotSchema, peakHoursStateSchema, quotaFailureDescriptorSchema, runtimeQuotaProviderSchema, providerQuotaStateSchema, claudeProviderQuotaStateSchema, codexProviderQuotaStateSchema, zaiProviderQuotaStateSchema, providerQuotaMapSchema, } from './quota';
21
21
  export { quotaHistoryRuntimeProviderSchema, quotaHistorySampleSchema, quotaHistoryDailyBucketSchema, } from './quotaHistory';
22
22
  export { activeProviderAccountStatusSchema, activeAccountStatusSchema } from './accountStatus';
23
23
  export { accountProviderIdSchema, accountManagerResultSchema, beginAccountLoginResultSchema, accountLoginStatusSchema, accountEntrySchema, savedAccountProfileSchema, listAllAccountsResultSchema, accountHealthStateSchema, accountHealthSourceSchema, accountHealthSidecarSchema, accountHealthSchema, accountViewSchema, runningProcessSchema, accountConsumerKindSchema, runningAccountConsumerSchema, switchAccountResultSchema, lastSwitchRecordSchema, providerSyncReportSchema, syncReportSchema, accountLaunchEnvSchema, } from './accountManager';
24
24
  export { assistantTurnEventSchema, assistantTurnEventTypeSchema, assistantTurnNarrationStepSchema, assistantTurnProcessSchema, assistantTurnProcessStepSchema, assistantTurnProjectionSchema, assistantTurnReasoningTimelineItemSchema, assistantTurnSubagentSchema, assistantTurnSubagentStatusSchema, assistantTurnTimelineItemSchema, assistantTurnToolGroupStepSchema, assistantTurnToolRefSchema, } from './assistantTurn';
25
- export { sidekickStateFileSchema, stateFileAccountSchema, stateFileBillingBlockSchema, stateFileContextSchema, stateFileQuotaSchema, stateFileQuotaWindowSchema, stateFileSessionSchema, stateFileWriterSchema, } from './stateFile';
25
+ export { sidekickStateFileSchema, stateFileAccountSchema, stateFileBillingBlockSchema, stateFileContextSchema, stateFileQuotaSchema, stateFileQuotaWindowSchema, stateFileQuotaLimitSchema, stateFileSessionSchema, stateFileWriterSchema, } from './stateFile';
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { z } from 'zod';
11
11
  import { ACCOUNT_PROVIDER_IDS } from '../providerIds';
12
- import { PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, } from '../providerFailure';
12
+ import { CODEX_ERROR_INFO_KINDS, PROVIDER_AUTHENTICATION_SOURCES, PROVIDER_AUTHENTICATION_STATES, PROVIDER_CREDENTIAL_KINDS, PROVIDER_FAILURE_CODES, PROVIDER_FAILURE_EVIDENCE_SOURCES, PROVIDER_RECOVERY_ACTIONS, } from '../providerFailure';
13
13
  export const providerAuthenticationEvidenceSchema = z.object({
14
14
  credentialKind: z.enum(PROVIDER_CREDENTIAL_KINDS),
15
15
  state: z.enum(PROVIDER_AUTHENTICATION_STATES),
@@ -20,6 +20,7 @@ export const providerFailureEvidenceSchema = z.object({
20
20
  source: z.enum(PROVIDER_FAILURE_EVIDENCE_SOURCES),
21
21
  rule: z.enum(PROVIDER_FAILURE_CODES),
22
22
  checkedAt: z.string().optional(),
23
+ codexErrorInfo: z.enum(CODEX_ERROR_INFO_KINDS).optional(),
23
24
  });
24
25
  export const providerRetryAfterSchema = z.discriminatedUnion('kind', [
25
26
  z.object({ kind: z.literal('delay'), delayMs: z.number() }),
@@ -30,6 +31,7 @@ export const providerFailureDiagnosisSchema = z.object({
30
31
  credentialKind: z.enum(PROVIDER_CREDENTIAL_KINDS),
31
32
  diagnosis: z.enum(PROVIDER_FAILURE_CODES),
32
33
  recovery: z.enum(PROVIDER_RECOVERY_ACTIONS),
34
+ retryable: z.boolean().optional(),
33
35
  evidence: z.array(providerFailureEvidenceSchema),
34
36
  authentication: providerAuthenticationEvidenceSchema.optional(),
35
37
  httpStatus: z.number().optional(),
@@ -14,6 +14,33 @@ import { QUOTA_PROVIDER_IDS, RUNTIME_QUOTA_PROVIDER_IDS } from '../providerIds';
14
14
  export const quotaWindowSchema = z.object({
15
15
  utilization: z.number(),
16
16
  resetsAt: z.string(),
17
+ absent: z.boolean().optional(),
18
+ windowMs: z.number().optional(),
19
+ });
20
+ // ── QuotaLimitRow / QuotaExtraUsage ──
21
+ export const quotaLimitRowSchema = z.object({
22
+ kind: z.string(),
23
+ group: z.string(),
24
+ label: z.string(),
25
+ utilization: z.number(),
26
+ resetsAt: z.string(),
27
+ windowMs: z.number().optional(),
28
+ projected: z.number().optional(),
29
+ scopeModel: z.string().optional(),
30
+ scopeSurface: z.string().optional(),
31
+ severity: z.string().optional(),
32
+ isActive: z.boolean().optional(),
33
+ allowancePercentOfWeekly: z.number().optional(),
34
+ usedPercentOfWeekly: z.number().optional(),
35
+ });
36
+ export const quotaExtraUsageSchema = z.object({
37
+ enabled: z.boolean(),
38
+ monthlyLimit: z.number().nullable(),
39
+ usedCredits: z.number().nullable(),
40
+ utilization: z.number().nullable(),
41
+ currency: z.string().optional(),
42
+ decimalPlaces: z.number().optional(),
43
+ disabledReason: z.string().optional(),
17
44
  });
18
45
  // ── QuotaState ──
19
46
  export const quotaFailureKindSchema = z.enum([
@@ -74,7 +101,10 @@ export const quotaStateSchema = z.object({
74
101
  credits: z.unknown().optional(),
75
102
  resetCredits: codexResetCreditsSnapshotSchema.optional(),
76
103
  planType: z.string().optional(),
104
+ planLabel: z.string().optional(),
77
105
  rateLimitReachedType: z.string().optional(),
106
+ limits: z.array(quotaLimitRowSchema).optional(),
107
+ extraUsage: quotaExtraUsageSchema.optional(),
78
108
  });
79
109
  // ── PeakHoursState ──
80
110
  export const peakHoursStateSchema = z.object({
@@ -21,8 +21,16 @@ export const quotaHistorySampleSchema = z.object({
21
21
  runtimeProvider: quotaHistoryRuntimeProviderSchema,
22
22
  providerId: z.string(),
23
23
  workspaceId: z.string(),
24
- fiveHour: z.object({ utilization: z.number(), resetsAt: z.string() }),
25
- sevenDay: z.object({ utilization: z.number(), resetsAt: z.string() }),
24
+ fiveHour: z.object({
25
+ utilization: z.number(),
26
+ resetsAt: z.string(),
27
+ absent: z.boolean().optional(),
28
+ }),
29
+ sevenDay: z.object({
30
+ utilization: z.number(),
31
+ resetsAt: z.string(),
32
+ absent: z.boolean().optional(),
33
+ }),
26
34
  available: z.boolean(),
27
35
  error: z.string().optional(),
28
36
  source: z.enum(['session', 'cache', 'api', 'statusline']).optional(),
@@ -16,6 +16,13 @@ export const stateFileWriterSchema = z.enum([
16
16
  export const stateFileQuotaWindowSchema = z.object({
17
17
  utilization: z.number(),
18
18
  resetsAt: z.string(),
19
+ absent: z.boolean().optional(),
20
+ });
21
+ export const stateFileQuotaLimitSchema = z.object({
22
+ kind: z.string(),
23
+ label: z.string(),
24
+ utilization: z.number(),
25
+ resetsAt: z.string(),
19
26
  });
20
27
  export const stateFileQuotaSchema = z.object({
21
28
  fiveHour: stateFileQuotaWindowSchema,
@@ -25,6 +32,8 @@ export const stateFileQuotaSchema = z.object({
25
32
  capturedAt: z.string().nullable(),
26
33
  ageMs: z.number().nullable(),
27
34
  freshness: z.enum(['fresh', 'aging', 'stale']).nullable(),
35
+ planLabel: z.string().optional(),
36
+ extraLimits: z.array(stateFileQuotaLimitSchema).optional(),
28
37
  });
29
38
  export const stateFileAccountSchema = z.object({
30
39
  providerId: z.enum(['claude-code', 'codex']),
@@ -17,6 +17,8 @@ export const canonicalTranscriptBlockSchema = z.object({
17
17
  type: z.enum(['text', 'thinking', 'tool_use', 'tool_result', 'image', 'unknown']),
18
18
  text: z.string().optional(),
19
19
  toolName: z.string().optional(),
20
+ rawToolName: z.string().optional(),
21
+ mcpServerName: z.string().optional(),
20
22
  toolUseId: z.string().optional(),
21
23
  input: z.unknown().optional(),
22
24
  output: z.unknown().optional(),
@@ -28,6 +30,8 @@ export const canonicalTranscriptBlockSchema = z.object({
28
30
  export const canonicalToolCallSchema = z.object({
29
31
  id: z.string().optional(),
30
32
  name: z.string(),
33
+ rawToolName: z.string().optional(),
34
+ mcpServerName: z.string().optional(),
31
35
  timestamp: z.string(),
32
36
  input: z.unknown().optional(),
33
37
  output: z.unknown().optional(),
@@ -14,13 +14,13 @@
14
14
  * and replies or signals that belong to an out-of-scope prompt are dropped.
15
15
  */
16
16
  import * as fs from 'fs';
17
- import { DEFAULT_BOUNDS, compareStrings, emptyStats, listCandidates, resolveRoots, scanFile, } from './promptHistory';
17
+ import { DEFAULT_BOUNDS, PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES, compareStrings, emptyStats, listCandidates, resolveRoots, scanFile, } from './promptHistory';
18
18
  import { createSessionRecordExtractor, } from './sessionPromptSignals';
19
19
  const DEFAULT_SESSION_BOUNDS = {
20
20
  maxSessions: 1000,
21
21
  maxSessionBytes: 256 * 1024 * 1024,
22
22
  maxTotalBytes: 256 * 1024 * 1024,
23
- maxRecordBytes: DEFAULT_BOUNDS.maxRecordBytes,
23
+ maxRecordBytes: PROMPT_HISTORY_ORDINAL_MAX_RECORD_BYTES,
24
24
  deadlineMs: 60_000,
25
25
  };
26
26
  const MAX_EXCLUSIONS = 1000;
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import * as fs from 'fs';
17
17
  import { getGlobalDataPath } from './paths';
18
- import { classifyQuotaFreshness } from './quota';
18
+ import { classifyQuotaFreshness, extraQuotaRows } from './quota';
19
19
  import { atomicWriteJsonSync } from './writers/atomic';
20
20
  export const STATE_FILE_SCHEMA_VERSION = 1;
21
21
  export const STATE_FILE_NAME = 'state.json';
@@ -90,14 +90,29 @@ export function quotaToStateFile(quota, now = Date.now()) {
90
90
  freshness ??= classifyQuotaFreshness(ageMs);
91
91
  }
92
92
  }
93
+ const extraLimits = extraQuotaRows(quota).map((row) => ({
94
+ kind: row.kind,
95
+ label: row.label,
96
+ utilization: row.utilization,
97
+ resetsAt: row.resetsAt,
98
+ }));
93
99
  return {
94
- fiveHour: { utilization: quota.fiveHour.utilization, resetsAt: quota.fiveHour.resetsAt },
95
- sevenDay: { utilization: quota.sevenDay.utilization, resetsAt: quota.sevenDay.resetsAt },
100
+ fiveHour: stateFileWindow(quota.fiveHour),
101
+ sevenDay: stateFileWindow(quota.sevenDay),
96
102
  source: quota.source ?? null,
97
103
  capturedSource: quota.capturedSource ?? null,
98
104
  capturedAt: quota.capturedAt ?? null,
99
105
  ageMs,
100
106
  freshness,
107
+ ...(quota.planLabel ? { planLabel: quota.planLabel } : {}),
108
+ ...(extraLimits.length > 0 ? { extraLimits } : {}),
109
+ };
110
+ }
111
+ function stateFileWindow(window) {
112
+ return {
113
+ utilization: window.utilization,
114
+ resetsAt: window.resetsAt,
115
+ ...(window.absent ? { absent: true } : {}),
101
116
  };
102
117
  }
103
118
  /** Project a billing block onto the state-file shape (totals only). */
@@ -115,14 +115,26 @@ export function parseClaudeStatuslinePayload(text) {
115
115
  raw,
116
116
  };
117
117
  }
118
- function toWindow(live, fallback) {
118
+ /**
119
+ * Claude Code leaves a window out of the document once its reset has passed,
120
+ * and always for a window the plan does not have (Ultra has no five-hour
121
+ * limit). A fallback is carried over only while it can still be current: an
122
+ * `absent` marker from an earlier API read, or a window that has not reset yet.
123
+ * An expired fallback would otherwise be re-saved as a fresh sample forever.
124
+ */
125
+ function toWindow(live, fallback, nowMs) {
119
126
  if (live) {
120
127
  return {
121
128
  utilization: Math.max(0, live.usedPercentage),
122
129
  resetsAt: new Date(live.resetsAt * 1000).toISOString(),
123
130
  };
124
131
  }
125
- return fallback ?? { utilization: 0, resetsAt: '' };
132
+ if (fallback?.absent)
133
+ return { utilization: 0, resetsAt: '', absent: true };
134
+ const fallbackResetMs = fallback ? Date.parse(fallback.resetsAt) : NaN;
135
+ if (fallback && Number.isFinite(fallbackResetMs) && fallbackResetMs > nowMs)
136
+ return fallback;
137
+ return { utilization: 0, resetsAt: '' };
126
138
  }
127
139
  /**
128
140
  * Build a `QuotaState` from the official rate-limit block. Returns `null`
@@ -137,8 +149,8 @@ export function quotaFromStatuslinePayload(payload, options = {}) {
137
149
  const now = options.now ?? new Date();
138
150
  const fallback = options.fallback ?? undefined;
139
151
  return {
140
- fiveHour: toWindow(limits.fiveHour, fallback?.fiveHour),
141
- sevenDay: toWindow(limits.sevenDay, fallback?.sevenDay),
152
+ fiveHour: toWindow(limits.fiveHour, fallback?.fiveHour, now.getTime()),
153
+ sevenDay: toWindow(limits.sevenDay, fallback?.sevenDay, now.getTime()),
142
154
  available: true,
143
155
  providerId: 'claude-code',
144
156
  source: 'statusline',
@@ -1,4 +1,4 @@
1
- import { FIVE_HOUR_WINDOW_MS, formatQuotaAge } from '../quota';
1
+ import { FIVE_HOUR_WINDOW_MS, SEVEN_DAY_WINDOW_MS, formatQuotaAge, quotaWindowApplies, quotaWindowHasData, quotaWindowShortLabel, } from '../quota';
2
2
  import { estimateTimeToQuota } from './BurnRateCalculator';
3
3
  export function selectStatuslineAccount(input) {
4
4
  if (input.accounts.claude.present) {
@@ -23,6 +23,11 @@ export function selectStatuslineAccount(input) {
23
23
  * One-line status. Segments, in order, each omitted when unknown:
24
24
  * `acct:<label> · 5h 42% resets 14:00 · ~1h20m left · 7d 61% · (2h ago) · ctx 37% · $0.42 · cache 92%`
25
25
  *
26
+ * Plans without a five-hour window (Claude Ultra) lead with the weekly window
27
+ * instead: `acct:<label> · 7d 17% resets Fri 01:00 · ~2d3h left · …`. Window
28
+ * labels follow the reported window length, so a Codex weekly primary window
29
+ * reads `7d`, not `5h`.
30
+ *
26
31
  * The age segment appears only for cached quota older than five minutes, so
27
32
  * a snapshot from yesterday can no longer read like a live figure.
28
33
  */
@@ -37,20 +42,35 @@ export function formatStatusline(input) {
37
42
  if (!quota?.available) {
38
43
  return joinSegments([`acct:${account}`, 'quota unavailable', ...liveSegments]);
39
44
  }
40
- const utilization = clampPercent(quota.fiveHour.utilization);
41
- const reset = formatReset(quota.fiveHour.resetsAt, now);
42
- const eta = estimateWindowEta(quota, now);
43
- const segments = [`acct:${account}`, `5h ${Math.round(utilization)}% resets ${reset}`];
45
+ const first = describeWindow(quota.fiveHour, '5h', FIVE_HOUR_WINDOW_MS);
46
+ const second = describeWindow(quota.sevenDay, '7d', SEVEN_DAY_WINDOW_MS);
47
+ // Lead with the first window whenever the plan has it (even idle); fall back
48
+ // to the second window when the first does not exist for this plan.
49
+ const leadWithFirst = quotaWindowApplies(quota.fiveHour) || !quotaWindowHasData(quota.sevenDay);
50
+ const lead = leadWithFirst ? first : second;
51
+ const segments = [`acct:${account}`, formatLeadSegment(lead, now)];
52
+ const eta = estimateWindowEta(lead, quota.capturedAt, now);
44
53
  if (eta != null)
45
54
  segments.push(`~${formatMinutes(eta)} left`);
46
- if (quota.sevenDay.resetsAt && quota.sevenDay.utilization > 0) {
47
- segments.push(`7d ${Math.round(clampPercent(quota.sevenDay.utilization))}%`);
55
+ if (leadWithFirst && quotaWindowHasData(quota.sevenDay) && quota.sevenDay.utilization > 0) {
56
+ segments.push(`${second.label} ${Math.round(clampPercent(quota.sevenDay.utilization))}%`);
48
57
  }
49
58
  if (quota.freshness && quota.freshness !== 'fresh') {
50
59
  segments.push(`(${formatQuotaAge(quota.ageMs)})`);
51
60
  }
52
61
  return joinSegments([...segments, ...liveSegments]);
53
62
  }
63
+ function describeWindow(window, label, defaultMs) {
64
+ const windowMs = typeof window.windowMs === 'number' && Number.isFinite(window.windowMs) && window.windowMs > 0
65
+ ? window.windowMs
66
+ : defaultMs;
67
+ return { label: quotaWindowShortLabel(window.windowMs, label), window, windowMs };
68
+ }
69
+ function formatLeadSegment(lead, now) {
70
+ const percent = `${lead.label} ${Math.round(clampPercent(lead.window.utilization))}%`;
71
+ const reset = formatReset(lead.window.resetsAt, now);
72
+ return reset ? `${percent} resets ${reset}` : percent;
73
+ }
54
74
  function formatLiveSegments(live) {
55
75
  if (!live)
56
76
  return [];
@@ -76,22 +96,25 @@ function formatCost(value) {
76
96
  return value.toFixed(1);
77
97
  return value.toFixed(2);
78
98
  }
79
- function estimateWindowEta(quota, now) {
80
- const resetAt = Date.parse(quota.fiveHour.resetsAt);
99
+ function estimateWindowEta(lead, capturedAtIso, now) {
100
+ const { window, windowMs } = lead;
101
+ if (window.absent)
102
+ return null;
103
+ const resetAt = Date.parse(window.resetsAt);
81
104
  if (!Number.isFinite(resetAt))
82
105
  return null;
83
106
  const nowMs = now.getTime();
84
107
  if (resetAt <= nowMs)
85
108
  return null;
86
- const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : now.getTime();
109
+ const capturedAt = capturedAtIso ? Date.parse(capturedAtIso) : now.getTime();
87
110
  const sampleAt = Number.isFinite(capturedAt) ? capturedAt : now.getTime();
88
111
  if (sampleAt >= resetAt)
89
112
  return null;
90
- const elapsedMinutes = (sampleAt - (resetAt - FIVE_HOUR_WINDOW_MS)) / 60_000;
91
- if (elapsedMinutes <= 0 || quota.fiveHour.utilization <= 0)
113
+ const elapsedMinutes = (sampleAt - (resetAt - windowMs)) / 60_000;
114
+ if (elapsedMinutes <= 0 || window.utilization <= 0)
92
115
  return null;
93
- const burnRate = quota.fiveHour.utilization / elapsedMinutes;
94
- const eta = estimateTimeToQuota(quota.fiveHour.utilization, 100, burnRate);
116
+ const burnRate = window.utilization / elapsedMinutes;
117
+ const eta = estimateTimeToQuota(window.utilization, 100, burnRate);
95
118
  const minutesUntilReset = (resetAt - nowMs) / 60_000;
96
119
  return eta !== null && eta < minutesUntilReset ? eta : null;
97
120
  }
@@ -103,20 +126,36 @@ function compactLabel(value) {
103
126
  function clampPercent(value) {
104
127
  return Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : 0;
105
128
  }
106
- function formatReset(value, fallback) {
129
+ /**
130
+ * Reset time as `HH:MM`, or `Fri 01:00` when it is more than a day away.
131
+ * Returns '' for an empty or unparseable value — never the current time.
132
+ */
133
+ function formatReset(value, now) {
134
+ if (!value)
135
+ return '';
107
136
  const reset = new Date(value);
108
- const date = Number.isNaN(reset.getTime()) ? fallback : reset;
109
- return new Intl.DateTimeFormat(undefined, {
137
+ if (Number.isNaN(reset.getTime()))
138
+ return '';
139
+ const time = new Intl.DateTimeFormat(undefined, {
110
140
  hour: '2-digit',
111
141
  minute: '2-digit',
112
142
  hour12: false,
113
- }).format(date);
143
+ }).format(reset);
144
+ if (reset.getTime() - now.getTime() <= 86_400_000)
145
+ return time;
146
+ const weekday = new Intl.DateTimeFormat(undefined, { weekday: 'short' }).format(reset);
147
+ return `${weekday} ${time}`;
114
148
  }
115
149
  function formatMinutes(value) {
116
150
  const minutes = Math.max(0, Math.round(value));
117
151
  if (minutes < 60)
118
152
  return `${minutes}min`;
119
153
  const hours = Math.floor(minutes / 60);
154
+ if (hours >= 48) {
155
+ const days = Math.floor(hours / 24);
156
+ const remainderHours = hours % 24;
157
+ return remainderHours ? `${days}d${remainderHours}h` : `${days}d`;
158
+ }
120
159
  const remainder = minutes % 60;
121
160
  return remainder ? `${hours}h${remainder}m` : `${hours}h`;
122
161
  }
@@ -1,7 +1,7 @@
1
1
  export { getActiveAccountStatus } from '../accountStatus';
2
2
  export { readQuotaSnapshot, writeQuotaSnapshot } from '../quotaSnapshots';
3
- export { appendQuotaHistorySample, getWorkspaceIdFromPath } from '../quotaHistory';
4
- export { classifyQuotaFreshness, formatQuotaAge } from '../quota';
3
+ export { appendQuotaHistorySample, getWorkspaceIdFromPath, quotaHistoryWindow, } from '../quotaHistory';
4
+ export { classifyQuotaFreshness, formatQuotaAge, quotaWindowApplies, quotaWindowHasData, quotaWindowShortLabel, } from '../quota';
5
5
  export { BurnRateCalculator, estimateTimeToQuota } from './BurnRateCalculator';
6
6
  export { formatStatusline, selectStatuslineAccount } from './formatter';
7
7
  export { parseClaudeStatuslinePayload, quotaFromStatuslinePayload, } from './claudeStatuslinePayload';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `sidekick-shared/statusline/payload` — the pure Claude Code status-line
3
+ * stdin parser on its own, for hosts with a tight bundle budget that render
4
+ * their own line. Loads no account, credential, quota-snapshot, or network
5
+ * code (the full `sidekick-shared/statusline` entry does).
6
+ *
7
+ * @module statusline/payload
8
+ */
9
+ export { parseClaudeStatuslinePayload, quotaFromStatuslinePayload, } from './claudeStatuslinePayload';