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
@@ -23,6 +23,20 @@ export interface StateFileQuotaWindow {
23
23
  utilization: number;
24
24
  /** ISO timestamp when the window resets; empty when unknown. */
25
25
  resetsAt: string;
26
+ /**
27
+ * Present (true) only when the plan has no such window — e.g. Claude Ultra
28
+ * has no five-hour limit. `utilization`/`resetsAt` are then 0/''.
29
+ */
30
+ absent?: boolean;
31
+ }
32
+ /** An extra usage meter beyond the two headline windows, e.g. a Fable weekly cap. */
33
+ export interface StateFileQuotaLimit {
34
+ /** Server meter kind, e.g. `weekly_scoped`. */
35
+ kind: string;
36
+ /** Display label, e.g. `7-Day (Fable)`. */
37
+ label: string;
38
+ utilization: number;
39
+ resetsAt: string;
26
40
  }
27
41
  export interface StateFileQuota {
28
42
  fiveHour: StateFileQuotaWindow;
@@ -34,6 +48,10 @@ export interface StateFileQuota {
34
48
  capturedAt: string | null;
35
49
  ageMs: number | null;
36
50
  freshness: 'fresh' | 'aging' | 'stale' | null;
51
+ /** Subscription plan name when known, e.g. `Ultra`. Omitted when unknown. */
52
+ planLabel?: string;
53
+ /** Extra meters (per-model weekly caps and similar). Omitted when there are none. */
54
+ extraLimits?: StateFileQuotaLimit[];
37
55
  }
38
56
  export interface StateFileAccount {
39
57
  providerId: 'claude-code' | 'codex';
package/dist/stateFile.js CHANGED
@@ -131,14 +131,29 @@ function quotaToStateFile(quota, now = Date.now()) {
131
131
  freshness ??= (0, quota_1.classifyQuotaFreshness)(ageMs);
132
132
  }
133
133
  }
134
+ const extraLimits = (0, quota_1.extraQuotaRows)(quota).map((row) => ({
135
+ kind: row.kind,
136
+ label: row.label,
137
+ utilization: row.utilization,
138
+ resetsAt: row.resetsAt,
139
+ }));
134
140
  return {
135
- fiveHour: { utilization: quota.fiveHour.utilization, resetsAt: quota.fiveHour.resetsAt },
136
- sevenDay: { utilization: quota.sevenDay.utilization, resetsAt: quota.sevenDay.resetsAt },
141
+ fiveHour: stateFileWindow(quota.fiveHour),
142
+ sevenDay: stateFileWindow(quota.sevenDay),
137
143
  source: quota.source ?? null,
138
144
  capturedSource: quota.capturedSource ?? null,
139
145
  capturedAt: quota.capturedAt ?? null,
140
146
  ageMs,
141
147
  freshness,
148
+ ...(quota.planLabel ? { planLabel: quota.planLabel } : {}),
149
+ ...(extraLimits.length > 0 ? { extraLimits } : {}),
150
+ };
151
+ }
152
+ function stateFileWindow(window) {
153
+ return {
154
+ utilization: window.utilization,
155
+ resetsAt: window.resetsAt,
156
+ ...(window.absent ? { absent: true } : {}),
142
157
  };
143
158
  }
144
159
  /** Project a billing block onto the state-file shape (totals only). */
@@ -74,9 +74,10 @@ export interface QuotaFromStatuslineOptions {
74
74
  /** Capture time; defaults to now. */
75
75
  now?: Date;
76
76
  /**
77
- * A previously known quota (typically the persisted snapshot). Windows the
78
- * payload omits are carried over from it, since Claude Code drops a window
79
- * from the document once its reset time has passed.
77
+ * A previously known quota (typically the persisted snapshot). A window the
78
+ * payload omits is carried over from it while the fallback window has not
79
+ * reset yet, or when the fallback marks it `absent` (the plan has no such
80
+ * window, e.g. Ultra's missing five-hour limit).
80
81
  */
81
82
  fallback?: QuotaState | null;
82
83
  }
@@ -119,14 +119,26 @@ function parseClaudeStatuslinePayload(text) {
119
119
  raw,
120
120
  };
121
121
  }
122
- function toWindow(live, fallback) {
122
+ /**
123
+ * Claude Code leaves a window out of the document once its reset has passed,
124
+ * and always for a window the plan does not have (Ultra has no five-hour
125
+ * limit). A fallback is carried over only while it can still be current: an
126
+ * `absent` marker from an earlier API read, or a window that has not reset yet.
127
+ * An expired fallback would otherwise be re-saved as a fresh sample forever.
128
+ */
129
+ function toWindow(live, fallback, nowMs) {
123
130
  if (live) {
124
131
  return {
125
132
  utilization: Math.max(0, live.usedPercentage),
126
133
  resetsAt: new Date(live.resetsAt * 1000).toISOString(),
127
134
  };
128
135
  }
129
- return fallback ?? { utilization: 0, resetsAt: '' };
136
+ if (fallback?.absent)
137
+ return { utilization: 0, resetsAt: '', absent: true };
138
+ const fallbackResetMs = fallback ? Date.parse(fallback.resetsAt) : NaN;
139
+ if (fallback && Number.isFinite(fallbackResetMs) && fallbackResetMs > nowMs)
140
+ return fallback;
141
+ return { utilization: 0, resetsAt: '' };
130
142
  }
131
143
  /**
132
144
  * Build a `QuotaState` from the official rate-limit block. Returns `null`
@@ -141,8 +153,8 @@ function quotaFromStatuslinePayload(payload, options = {}) {
141
153
  const now = options.now ?? new Date();
142
154
  const fallback = options.fallback ?? undefined;
143
155
  return {
144
- fiveHour: toWindow(limits.fiveHour, fallback?.fiveHour),
145
- sevenDay: toWindow(limits.sevenDay, fallback?.sevenDay),
156
+ fiveHour: toWindow(limits.fiveHour, fallback?.fiveHour, now.getTime()),
157
+ sevenDay: toWindow(limits.sevenDay, fallback?.sevenDay, now.getTime()),
146
158
  available: true,
147
159
  providerId: 'claude-code',
148
160
  source: 'statusline',
@@ -23,6 +23,11 @@ export declare function selectStatuslineAccount(input: StatuslineInput): Statusl
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
  */
@@ -27,6 +27,11 @@ function selectStatuslineAccount(input) {
27
27
  * One-line status. Segments, in order, each omitted when unknown:
28
28
  * `acct:<label> · 5h 42% resets 14:00 · ~1h20m left · 7d 61% · (2h ago) · ctx 37% · $0.42 · cache 92%`
29
29
  *
30
+ * Plans without a five-hour window (Claude Ultra) lead with the weekly window
31
+ * instead: `acct:<label> · 7d 17% resets Fri 01:00 · ~2d3h left · …`. Window
32
+ * labels follow the reported window length, so a Codex weekly primary window
33
+ * reads `7d`, not `5h`.
34
+ *
30
35
  * The age segment appears only for cached quota older than five minutes, so
31
36
  * a snapshot from yesterday can no longer read like a live figure.
32
37
  */
@@ -41,20 +46,35 @@ function formatStatusline(input) {
41
46
  if (!quota?.available) {
42
47
  return joinSegments([`acct:${account}`, 'quota unavailable', ...liveSegments]);
43
48
  }
44
- const utilization = clampPercent(quota.fiveHour.utilization);
45
- const reset = formatReset(quota.fiveHour.resetsAt, now);
46
- const eta = estimateWindowEta(quota, now);
47
- const segments = [`acct:${account}`, `5h ${Math.round(utilization)}% resets ${reset}`];
49
+ const first = describeWindow(quota.fiveHour, '5h', quota_1.FIVE_HOUR_WINDOW_MS);
50
+ const second = describeWindow(quota.sevenDay, '7d', quota_1.SEVEN_DAY_WINDOW_MS);
51
+ // Lead with the first window whenever the plan has it (even idle); fall back
52
+ // to the second window when the first does not exist for this plan.
53
+ const leadWithFirst = (0, quota_1.quotaWindowApplies)(quota.fiveHour) || !(0, quota_1.quotaWindowHasData)(quota.sevenDay);
54
+ const lead = leadWithFirst ? first : second;
55
+ const segments = [`acct:${account}`, formatLeadSegment(lead, now)];
56
+ const eta = estimateWindowEta(lead, quota.capturedAt, now);
48
57
  if (eta != null)
49
58
  segments.push(`~${formatMinutes(eta)} left`);
50
- if (quota.sevenDay.resetsAt && quota.sevenDay.utilization > 0) {
51
- segments.push(`7d ${Math.round(clampPercent(quota.sevenDay.utilization))}%`);
59
+ if (leadWithFirst && (0, quota_1.quotaWindowHasData)(quota.sevenDay) && quota.sevenDay.utilization > 0) {
60
+ segments.push(`${second.label} ${Math.round(clampPercent(quota.sevenDay.utilization))}%`);
52
61
  }
53
62
  if (quota.freshness && quota.freshness !== 'fresh') {
54
63
  segments.push(`(${(0, quota_1.formatQuotaAge)(quota.ageMs)})`);
55
64
  }
56
65
  return joinSegments([...segments, ...liveSegments]);
57
66
  }
67
+ function describeWindow(window, label, defaultMs) {
68
+ const windowMs = typeof window.windowMs === 'number' && Number.isFinite(window.windowMs) && window.windowMs > 0
69
+ ? window.windowMs
70
+ : defaultMs;
71
+ return { label: (0, quota_1.quotaWindowShortLabel)(window.windowMs, label), window, windowMs };
72
+ }
73
+ function formatLeadSegment(lead, now) {
74
+ const percent = `${lead.label} ${Math.round(clampPercent(lead.window.utilization))}%`;
75
+ const reset = formatReset(lead.window.resetsAt, now);
76
+ return reset ? `${percent} resets ${reset}` : percent;
77
+ }
58
78
  function formatLiveSegments(live) {
59
79
  if (!live)
60
80
  return [];
@@ -80,22 +100,25 @@ function formatCost(value) {
80
100
  return value.toFixed(1);
81
101
  return value.toFixed(2);
82
102
  }
83
- function estimateWindowEta(quota, now) {
84
- const resetAt = Date.parse(quota.fiveHour.resetsAt);
103
+ function estimateWindowEta(lead, capturedAtIso, now) {
104
+ const { window, windowMs } = lead;
105
+ if (window.absent)
106
+ return null;
107
+ const resetAt = Date.parse(window.resetsAt);
85
108
  if (!Number.isFinite(resetAt))
86
109
  return null;
87
110
  const nowMs = now.getTime();
88
111
  if (resetAt <= nowMs)
89
112
  return null;
90
- const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : now.getTime();
113
+ const capturedAt = capturedAtIso ? Date.parse(capturedAtIso) : now.getTime();
91
114
  const sampleAt = Number.isFinite(capturedAt) ? capturedAt : now.getTime();
92
115
  if (sampleAt >= resetAt)
93
116
  return null;
94
- const elapsedMinutes = (sampleAt - (resetAt - quota_1.FIVE_HOUR_WINDOW_MS)) / 60_000;
95
- if (elapsedMinutes <= 0 || quota.fiveHour.utilization <= 0)
117
+ const elapsedMinutes = (sampleAt - (resetAt - windowMs)) / 60_000;
118
+ if (elapsedMinutes <= 0 || window.utilization <= 0)
96
119
  return null;
97
- const burnRate = quota.fiveHour.utilization / elapsedMinutes;
98
- const eta = (0, BurnRateCalculator_1.estimateTimeToQuota)(quota.fiveHour.utilization, 100, burnRate);
120
+ const burnRate = window.utilization / elapsedMinutes;
121
+ const eta = (0, BurnRateCalculator_1.estimateTimeToQuota)(window.utilization, 100, burnRate);
99
122
  const minutesUntilReset = (resetAt - nowMs) / 60_000;
100
123
  return eta !== null && eta < minutesUntilReset ? eta : null;
101
124
  }
@@ -107,20 +130,36 @@ function compactLabel(value) {
107
130
  function clampPercent(value) {
108
131
  return Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : 0;
109
132
  }
110
- function formatReset(value, fallback) {
133
+ /**
134
+ * Reset time as `HH:MM`, or `Fri 01:00` when it is more than a day away.
135
+ * Returns '' for an empty or unparseable value — never the current time.
136
+ */
137
+ function formatReset(value, now) {
138
+ if (!value)
139
+ return '';
111
140
  const reset = new Date(value);
112
- const date = Number.isNaN(reset.getTime()) ? fallback : reset;
113
- return new Intl.DateTimeFormat(undefined, {
141
+ if (Number.isNaN(reset.getTime()))
142
+ return '';
143
+ const time = new Intl.DateTimeFormat(undefined, {
114
144
  hour: '2-digit',
115
145
  minute: '2-digit',
116
146
  hour12: false,
117
- }).format(date);
147
+ }).format(reset);
148
+ if (reset.getTime() - now.getTime() <= 86_400_000)
149
+ return time;
150
+ const weekday = new Intl.DateTimeFormat(undefined, { weekday: 'short' }).format(reset);
151
+ return `${weekday} ${time}`;
118
152
  }
119
153
  function formatMinutes(value) {
120
154
  const minutes = Math.max(0, Math.round(value));
121
155
  if (minutes < 60)
122
156
  return `${minutes}min`;
123
157
  const hours = Math.floor(minutes / 60);
158
+ if (hours >= 48) {
159
+ const days = Math.floor(hours / 24);
160
+ const remainderHours = hours % 24;
161
+ return remainderHours ? `${days}d${remainderHours}h` : `${days}d`;
162
+ }
124
163
  const remainder = minutes % 60;
125
164
  return remainder ? `${hours}h${remainder}m` : `${hours}h`;
126
165
  }
@@ -2,14 +2,14 @@ export { getActiveAccountStatus } from '../accountStatus';
2
2
  export type { ActiveAccountStatus, ActiveAccountStatusOptions, ActiveProviderAccountStatus, } from '../accountStatus';
3
3
  export { readQuotaSnapshot, writeQuotaSnapshot } from '../quotaSnapshots';
4
4
  export type { QuotaSnapshotProviderId } from '../quotaSnapshots';
5
- export { appendQuotaHistorySample, getWorkspaceIdFromPath } from '../quotaHistory';
5
+ export { appendQuotaHistorySample, getWorkspaceIdFromPath, quotaHistoryWindow, } from '../quotaHistory';
6
6
  export type { QuotaHistorySample } from '../quotaHistory';
7
- export { classifyQuotaFreshness, formatQuotaAge } from '../quota';
8
- export type { QuotaFreshness, QuotaState } from '../quota';
7
+ export { classifyQuotaFreshness, formatQuotaAge, quotaWindowApplies, quotaWindowHasData, quotaWindowShortLabel, } from '../quota';
8
+ export type { QuotaFreshness, QuotaState, QuotaWindow } from '../quota';
9
9
  export { BurnRateCalculator, estimateTimeToQuota } from './BurnRateCalculator';
10
10
  export { formatStatusline, selectStatuslineAccount } from './formatter';
11
11
  export type { StatuslineInput, StatuslineSelection } from './formatter';
12
12
  export { parseClaudeStatuslinePayload, quotaFromStatuslinePayload, } from './claudeStatuslinePayload';
13
13
  export type { ClaudeStatuslinePayload, ClaudeStatuslineRateLimitWindow, QuotaFromStatuslineOptions, } from './claudeStatuslinePayload';
14
14
  export { STATE_FILE_NAME, STATE_FILE_SCHEMA_VERSION, billingBlockToStateFile, getStateFilePath, quotaToStateFile, readStateFile, writeStateFile, } from '../stateFile';
15
- export type { SidekickStateFile, SidekickStateInput, StateFileAccount, StateFileBillingBlock, StateFileContext, StateFileQuota, StateFileQuotaWindow, StateFileSession, StateFileWriter, WriteStateFileOptions, } from '../stateFile';
15
+ export type { SidekickStateFile, SidekickStateInput, StateFileAccount, StateFileBillingBlock, StateFileContext, StateFileQuota, StateFileQuotaLimit, StateFileQuotaWindow, StateFileSession, StateFileWriter, WriteStateFileOptions, } from '../stateFile';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.writeStateFile = exports.readStateFile = exports.quotaToStateFile = exports.getStateFilePath = exports.billingBlockToStateFile = exports.STATE_FILE_SCHEMA_VERSION = exports.STATE_FILE_NAME = exports.quotaFromStatuslinePayload = exports.parseClaudeStatuslinePayload = exports.selectStatuslineAccount = exports.formatStatusline = exports.estimateTimeToQuota = exports.BurnRateCalculator = exports.formatQuotaAge = exports.classifyQuotaFreshness = exports.getWorkspaceIdFromPath = exports.appendQuotaHistorySample = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.getActiveAccountStatus = void 0;
3
+ exports.writeStateFile = exports.readStateFile = exports.quotaToStateFile = exports.getStateFilePath = exports.billingBlockToStateFile = exports.STATE_FILE_SCHEMA_VERSION = exports.STATE_FILE_NAME = exports.quotaFromStatuslinePayload = exports.parseClaudeStatuslinePayload = exports.selectStatuslineAccount = exports.formatStatusline = exports.estimateTimeToQuota = exports.BurnRateCalculator = exports.quotaWindowShortLabel = exports.quotaWindowHasData = exports.quotaWindowApplies = exports.formatQuotaAge = exports.classifyQuotaFreshness = exports.quotaHistoryWindow = exports.getWorkspaceIdFromPath = exports.appendQuotaHistorySample = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.getActiveAccountStatus = void 0;
4
4
  var accountStatus_1 = require("../accountStatus");
5
5
  Object.defineProperty(exports, "getActiveAccountStatus", { enumerable: true, get: function () { return accountStatus_1.getActiveAccountStatus; } });
6
6
  var quotaSnapshots_1 = require("../quotaSnapshots");
@@ -9,9 +9,13 @@ Object.defineProperty(exports, "writeQuotaSnapshot", { enumerable: true, get: fu
9
9
  var quotaHistory_1 = require("../quotaHistory");
10
10
  Object.defineProperty(exports, "appendQuotaHistorySample", { enumerable: true, get: function () { return quotaHistory_1.appendQuotaHistorySample; } });
11
11
  Object.defineProperty(exports, "getWorkspaceIdFromPath", { enumerable: true, get: function () { return quotaHistory_1.getWorkspaceIdFromPath; } });
12
+ Object.defineProperty(exports, "quotaHistoryWindow", { enumerable: true, get: function () { return quotaHistory_1.quotaHistoryWindow; } });
12
13
  var quota_1 = require("../quota");
13
14
  Object.defineProperty(exports, "classifyQuotaFreshness", { enumerable: true, get: function () { return quota_1.classifyQuotaFreshness; } });
14
15
  Object.defineProperty(exports, "formatQuotaAge", { enumerable: true, get: function () { return quota_1.formatQuotaAge; } });
16
+ Object.defineProperty(exports, "quotaWindowApplies", { enumerable: true, get: function () { return quota_1.quotaWindowApplies; } });
17
+ Object.defineProperty(exports, "quotaWindowHasData", { enumerable: true, get: function () { return quota_1.quotaWindowHasData; } });
18
+ Object.defineProperty(exports, "quotaWindowShortLabel", { enumerable: true, get: function () { return quota_1.quotaWindowShortLabel; } });
15
19
  var BurnRateCalculator_1 = require("./BurnRateCalculator");
16
20
  Object.defineProperty(exports, "BurnRateCalculator", { enumerable: true, get: function () { return BurnRateCalculator_1.BurnRateCalculator; } });
17
21
  Object.defineProperty(exports, "estimateTimeToQuota", { enumerable: true, get: function () { return BurnRateCalculator_1.estimateTimeToQuota; } });
@@ -0,0 +1,10 @@
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';
10
+ export type { ClaudeStatuslinePayload, ClaudeStatuslineRateLimitWindow, QuotaFromStatuslineOptions, } from './claudeStatuslinePayload';
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * `sidekick-shared/statusline/payload` — the pure Claude Code status-line
4
+ * stdin parser on its own, for hosts with a tight bundle budget that render
5
+ * their own line. Loads no account, credential, quota-snapshot, or network
6
+ * code (the full `sidekick-shared/statusline` entry does).
7
+ *
8
+ * @module statusline/payload
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.quotaFromStatuslinePayload = exports.parseClaudeStatuslinePayload = void 0;
12
+ var claudeStatuslinePayload_1 = require("./claudeStatuslinePayload");
13
+ Object.defineProperty(exports, "parseClaudeStatuslinePayload", { enumerable: true, get: function () { return claudeStatuslinePayload_1.parseClaudeStatuslinePayload; } });
14
+ Object.defineProperty(exports, "quotaFromStatuslinePayload", { enumerable: true, get: function () { return claudeStatuslinePayload_1.quotaFromStatuslinePayload; } });
@@ -15,6 +15,20 @@ export interface CredentialGuardOptions {
15
15
  blockedBinaries?: string[];
16
16
  /** Extra real paths to protect, besides the Claude, Codex, and Sidekick homes. */
17
17
  protectedPaths?: string[];
18
+ /**
19
+ * Home overrides to keep instead of unsetting, for a suite that points one
20
+ * at its own fixture before the guard installs. Accepts only the variables
21
+ * the guard unsets: `CLAUDE_CONFIG_DIR`, `CLAUDE_SECURESTORAGE_CONFIG_DIR`,
22
+ * `CODEX_HOME`, and `SIDEKICK_CONFIG_DIR`.
23
+ *
24
+ * Install throws, before changing anything, for any other name, or when a
25
+ * preserved value is, contains, or lies inside a real protected location:
26
+ * the real home's `.claude`, `.claude.json`, and `.codex`, the default
27
+ * Sidekick config dir, or a `protectedPaths` entry. Symlinks are resolved
28
+ * first. The guard cannot tell a real store kept elsewhere from a fixture,
29
+ * so preserve only paths the suite created. Blank values are kept unchecked.
30
+ */
31
+ preserveEnv?: readonly string[];
18
32
  }
19
33
  /**
20
34
  * Installs the guard once per process and returns it; later calls return the
@@ -46,7 +46,8 @@ exports.installCredentialGuard = installCredentialGuard;
46
46
  * - `HOME` (and the Windows equivalents) point at a per-process temp dir,
47
47
  * removed on exit, and the home overrides (`CLAUDE_CONFIG_DIR`,
48
48
  * `CLAUDE_SECURESTORAGE_CONFIG_DIR`, `CODEX_HOME`, `SIDEKICK_CONFIG_DIR`)
49
- * are unset, so every `os.homedir()`-derived path is isolated structurally;
49
+ * are unset (unless named in `preserveEnv` and pointed at a fixture), so
50
+ * every `os.homedir()`-derived path is isolated structurally;
50
51
  * - the real `child_process` module object is patched, which reaches both ESM
51
52
  * named imports (after `syncBuiltinESMExports`) and CommonJS code loaded
52
53
  * through native `require`; per-file `vi.mock('child_process')` factories
@@ -64,6 +65,13 @@ const os = __importStar(require("node:os"));
64
65
  const path = __importStar(require("node:path"));
65
66
  const GUARD_KEY = '__sidekickCredentialGuard';
66
67
  const DEFAULT_BLOCKED_BINARIES = ['security', 'claude', 'codex'];
68
+ /** Home overrides the guard unsets; only these can be preserved. */
69
+ const SCRUBBED_ENV = [
70
+ 'CLAUDE_CONFIG_DIR',
71
+ 'CLAUDE_SECURESTORAGE_CONFIG_DIR',
72
+ 'CODEX_HOME',
73
+ 'SIDEKICK_CONFIG_DIR',
74
+ ];
67
75
  const CHILD_PROCESS_FUNCTIONS = [
68
76
  'spawn',
69
77
  'spawnSync',
@@ -111,6 +119,8 @@ function installCredentialGuard(options = {}) {
111
119
  // os.userInfo() reads the passwd entry, so it still names the real home
112
120
  // after HOME is redirected below.
113
121
  const realHome = os.userInfo().homedir;
122
+ // Validated before any side effect, so a refused install changes nothing.
123
+ const preserved = checkPreservedEnv(options, realHome, realFs);
114
124
  const testHome = realFs.mkdtempSync(path.join(os.tmpdir(), 'sidekick-test-home-'));
115
125
  const removeTestHome = realFs.rmSync.bind(realFs);
116
126
  process.once('exit', () => {
@@ -139,10 +149,9 @@ function installCredentialGuard(options = {}) {
139
149
  process.env.USERPROFILE = testHome;
140
150
  process.env.APPDATA = path.join(testHome, 'AppData', 'Roaming');
141
151
  process.env.LOCALAPPDATA = path.join(testHome, 'AppData', 'Local');
142
- delete process.env.CLAUDE_CONFIG_DIR;
143
- delete process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
144
- delete process.env.CODEX_HOME;
145
- delete process.env.SIDEKICK_CONFIG_DIR;
152
+ for (const name of SCRUBBED_ENV)
153
+ if (!preserved.has(name))
154
+ delete process.env[name];
146
155
  const protectedRoots = path.resolve(realHome) === path.resolve(testHome)
147
156
  ? []
148
157
  : [
@@ -244,6 +253,79 @@ function installCredentialGuard(options = {}) {
244
253
  globalThis[GUARD_KEY] = state;
245
254
  return state;
246
255
  }
256
+ /** The `preserveEnv` names, after refusing unknown names and unsafe values. */
257
+ function checkPreservedEnv(options, realHome, realFs) {
258
+ const preserved = new Set();
259
+ for (const name of options.preserveEnv ?? []) {
260
+ if (!SCRUBBED_ENV.includes(name)) {
261
+ throw new Error(`installCredentialGuard: preserveEnv accepts only ${SCRUBBED_ENV.join(', ')}; got ${JSON.stringify(name)}.`);
262
+ }
263
+ preserved.add(name);
264
+ }
265
+ if (preserved.size === 0)
266
+ return preserved;
267
+ const roots = [
268
+ path.join(realHome, '.claude'),
269
+ path.join(realHome, '.claude.json'),
270
+ path.join(realHome, '.codex'),
271
+ path.join(realHome, '.config', 'sidekick'),
272
+ // The Windows default config dir, read before HOME and APPDATA move.
273
+ ...(process.platform === 'win32' && process.env.APPDATA
274
+ ? [path.join(process.env.APPDATA, 'sidekick')]
275
+ : []),
276
+ ...(options.protectedPaths ?? []),
277
+ ];
278
+ for (const name of preserved) {
279
+ const value = process.env[name]?.trim();
280
+ if (!value)
281
+ continue;
282
+ const hit = protectedLocation(value, roots, realFs);
283
+ if (hit) {
284
+ throw new Error(`installCredentialGuard: preserveEnv ${name}=${value} reaches the real protected location ${hit}. Point it at a temporary fixture instead.`);
285
+ }
286
+ }
287
+ return preserved;
288
+ }
289
+ /** The root `target` equals, contains, or lies inside, comparing plain and symlink-resolved paths. */
290
+ function protectedLocation(target, roots, realFs) {
291
+ // Case-insensitive by default on macOS and Windows; folding can only over-refuse.
292
+ const fold = process.platform === 'darwin' || process.platform === 'win32';
293
+ const normal = (value) => {
294
+ const resolved = path.resolve(value);
295
+ return fold ? resolved.toLowerCase() : resolved;
296
+ };
297
+ const within = (parent, child) => child === parent || child.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
298
+ const related = (a, b) => within(a, b) || within(b, a);
299
+ // Plain paths first, so an obviously unsafe value never reaches the filesystem.
300
+ const plain = normal(target);
301
+ for (const root of roots)
302
+ if (related(normal(root), plain))
303
+ return root;
304
+ const real = normal(realpathOfNearest(target, realFs));
305
+ for (const root of roots) {
306
+ const realRoot = normal(realpathOfNearest(root, realFs));
307
+ if (related(realRoot, real) || related(normal(root), real) || related(realRoot, plain))
308
+ return root;
309
+ }
310
+ return null;
311
+ }
312
+ /** `realpath` of the nearest existing ancestor, with the missing tail re-appended. */
313
+ function realpathOfNearest(target, realFs) {
314
+ let current = path.resolve(target);
315
+ let tail = [];
316
+ for (;;) {
317
+ try {
318
+ return path.join(realFs.realpathSync.native(current), ...tail);
319
+ }
320
+ catch {
321
+ const parent = path.dirname(current);
322
+ if (parent === current)
323
+ return path.resolve(target);
324
+ tail = [path.basename(current), ...tail];
325
+ current = parent;
326
+ }
327
+ }
328
+ }
247
329
  /**
248
330
  * The real module object of a Node built-in: `process.getBuiltinModule`
249
331
  * (Node 20.16+), or `require` in CommonJS on older Node.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Locates the session record that issued each of a set of tool calls, by id.
3
+ *
4
+ * Given Claude Code `tool_use` ids (`toolu_…`) and/or Codex `call_id`s, finds
5
+ * the session file, session id and time of each call, plus the ordinal of the
6
+ * human prompt the call answered, so a downstream record (an edit, a commit)
7
+ * can be tied back to the prompt that caused it. Scope, listing, session keys
8
+ * and ordinals are the ones `collectPromptHistory()` uses, so results join
9
+ * directly with prompt-history entries.
10
+ *
11
+ * Cost: one listing of the workspace-scoped session files (`since` prunes by
12
+ * modification time), then a streaming pass over each candidate file in which
13
+ * a line is only parsed when it contains one of the wanted ids. Claude Code
14
+ * subagent transcripts of each session are searched too. Stops as soon as
15
+ * every id is found. Prompt ordinals are read only for files that matched.
16
+ *
17
+ * Node-only: reads session files.
18
+ *
19
+ * @module toolCallOrigins
20
+ */
21
+ import { type PromptHistoryProvider } from './promptHistory';
22
+ export interface FindToolCallOriginsOptions {
23
+ /** Tool-call ids to locate: Claude `tool_use` ids and/or Codex `call_id`s. */
24
+ ids: Iterable<string>;
25
+ /** Workspace roots that scope the search (as for `collectPromptHistory`). */
26
+ workspacePaths: string[];
27
+ /** Also search the roots' git worktree siblings. Default true. */
28
+ includeWorktrees?: boolean;
29
+ /** Only session files modified at or after this instant. */
30
+ since?: Date;
31
+ /** Default both. */
32
+ providers?: PromptHistoryProvider[];
33
+ /** Stops between files and lines; the promise rejects with the signal's reason. */
34
+ signal?: AbortSignal;
35
+ }
36
+ export interface ToolCallOrigin {
37
+ provider: PromptHistoryProvider;
38
+ /**
39
+ * Session whose prompt history `afterOrdinal` counts in: the session itself,
40
+ * or its parent for a subagent or fork (Claude subagent transcripts live
41
+ * under the parent; a Codex subagent or fork has its own rollout, named in
42
+ * `subagent.agentId` / `filePath`, and no prompts of its own).
43
+ */
44
+ sessionId: string;
45
+ /** Same key `collectPromptHistory()` reports for that session's file. */
46
+ sessionKey: string;
47
+ /** File the call was recorded in (a subagent transcript for subagent calls). */
48
+ filePath: string;
49
+ /** ISO timestamp of the record that issued the call. */
50
+ timestamp: string;
51
+ /**
52
+ * Ordinal of the last human prompt at or before the call, in the session's
53
+ * prompt history; null when the call precedes every prompt or the session
54
+ * has none (e.g. a Codex subagent whose parent was not found in scope).
55
+ */
56
+ afterOrdinal: number | null;
57
+ toolName?: string;
58
+ /** Set when a subagent issued the call. */
59
+ subagent?: {
60
+ agentId: string;
61
+ parentSessionId: string;
62
+ };
63
+ }
64
+ /**
65
+ * Finds where each tool call was issued. Ids that are not found are absent
66
+ * from the result. When an id appears more than once (a forked or resumed
67
+ * session can replay history), the earliest record wins, and a session's own
68
+ * record wins over a fork's copy at the same instant.
69
+ */
70
+ export declare function findToolCallOrigins(options: FindToolCallOriginsOptions): Promise<Map<string, ToolCallOrigin>>;