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
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ /**
3
+ * Locates the session record that issued each of a set of tool calls, by id.
4
+ *
5
+ * Given Claude Code `tool_use` ids (`toolu_…`) and/or Codex `call_id`s, finds
6
+ * the session file, session id and time of each call, plus the ordinal of the
7
+ * human prompt the call answered, so a downstream record (an edit, a commit)
8
+ * can be tied back to the prompt that caused it. Scope, listing, session keys
9
+ * and ordinals are the ones `collectPromptHistory()` uses, so results join
10
+ * directly with prompt-history entries.
11
+ *
12
+ * Cost: one listing of the workspace-scoped session files (`since` prunes by
13
+ * modification time), then a streaming pass over each candidate file in which
14
+ * a line is only parsed when it contains one of the wanted ids. Claude Code
15
+ * subagent transcripts of each session are searched too. Stops as soon as
16
+ * every id is found. Prompt ordinals are read only for files that matched.
17
+ *
18
+ * Node-only: reads session files.
19
+ *
20
+ * @module toolCallOrigins
21
+ */
22
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
23
+ if (k2 === undefined) k2 = k;
24
+ var desc = Object.getOwnPropertyDescriptor(m, k);
25
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
26
+ desc = { enumerable: true, get: function() { return m[k]; } };
27
+ }
28
+ Object.defineProperty(o, k2, desc);
29
+ }) : (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ o[k2] = m[k];
32
+ }));
33
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
34
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
35
+ }) : function(o, v) {
36
+ o["default"] = v;
37
+ });
38
+ var __importStar = (this && this.__importStar) || (function () {
39
+ var ownKeys = function(o) {
40
+ ownKeys = Object.getOwnPropertyNames || function (o) {
41
+ var ar = [];
42
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
43
+ return ar;
44
+ };
45
+ return ownKeys(o);
46
+ };
47
+ return function (mod) {
48
+ if (mod && mod.__esModule) return mod;
49
+ var result = {};
50
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
51
+ __setModuleDefault(result, mod);
52
+ return result;
53
+ };
54
+ })();
55
+ Object.defineProperty(exports, "__esModule", { value: true });
56
+ exports.findToolCallOrigins = findToolCallOrigins;
57
+ const fs = __importStar(require("fs"));
58
+ const path = __importStar(require("path"));
59
+ const readline = __importStar(require("readline"));
60
+ const promptHistory_1 = require("./promptHistory");
61
+ const codexRolloutOrigin_1 = require("./parsers/codexRolloutOrigin");
62
+ const workspaceRoots_1 = require("./workspaceRoots");
63
+ /**
64
+ * Finds where each tool call was issued. Ids that are not found are absent
65
+ * from the result. When an id appears more than once (a forked or resumed
66
+ * session can replay history), the earliest record wins, and a session's own
67
+ * record wins over a fork's copy at the same instant.
68
+ */
69
+ async function findToolCallOrigins(options) {
70
+ const wanted = new Set([...options.ids].filter((id) => typeof id === 'string' && id.length > 0));
71
+ const found = new Map();
72
+ if (wanted.size === 0)
73
+ return found;
74
+ throwIfAborted(options.signal);
75
+ const realpaths = new Map();
76
+ const { roots, listingRoots } = (0, promptHistory_1.resolveRoots)(options.workspacePaths, options.includeWorktrees !== false, realpaths);
77
+ if (roots.length === 0)
78
+ return found;
79
+ const providers = options.providers ?? ['claude-code', 'codex'];
80
+ const sinceMs = options.since ? options.since.getTime() : null;
81
+ const candidates = await (0, promptHistory_1.listCandidates)(providers, listingRoots, sinceMs);
82
+ const byProviderSession = new Map();
83
+ for (const candidate of candidates) {
84
+ byProviderSession.set(`${candidate.provider}:${candidate.sessionId}`, candidate);
85
+ }
86
+ const marksCache = new Map();
87
+ const marksFor = (candidate) => {
88
+ let marks = marksCache.get(candidate.key);
89
+ if (!marks) {
90
+ marks = (0, promptHistory_1.readPromptOrdinalMarks)(candidate.provider, candidate.sessionId, candidate.filePath).catch(() => []);
91
+ marksCache.set(candidate.key, marks);
92
+ }
93
+ return marks;
94
+ };
95
+ const remaining = new Set(wanted);
96
+ // Best timestamp seen per id, to prefer the earliest record across files.
97
+ const bestMs = new Map();
98
+ for (const candidate of candidates) {
99
+ if (remaining.size === 0)
100
+ break;
101
+ throwIfAborted(options.signal);
102
+ const targets = [{ candidate, filePath: candidate.filePath }];
103
+ if (candidate.provider === 'claude-code') {
104
+ for (const sub of claudeSubagentTranscripts(candidate.filePath, candidate.sessionId)) {
105
+ targets.push({ candidate, filePath: sub.filePath, agentId: sub.agentId });
106
+ }
107
+ }
108
+ // A Codex fork or spawned subagent answers its parent's prompts.
109
+ let codexParent;
110
+ if (candidate.provider === 'codex') {
111
+ const origin = (0, codexRolloutOrigin_1.codexRolloutOrigin)(await readFirstRecord(candidate.filePath));
112
+ if (origin.parentId)
113
+ codexParent = { id: origin.parentId, fork: !origin.isSubagent };
114
+ }
115
+ for (const target of targets) {
116
+ const hits = await scanForIds(target.filePath, candidate.provider, remaining, roots, realpaths, options.signal);
117
+ for (const hit of hits) {
118
+ const isFork = codexParent?.fork === true;
119
+ const previous = bestMs.get(hit.id);
120
+ if (previous &&
121
+ (previous.ms < hit.timestampMs ||
122
+ (previous.ms === hit.timestampMs && (!previous.fork || isFork)))) {
123
+ continue;
124
+ }
125
+ bestMs.set(hit.id, { ms: hit.timestampMs, fork: isFork });
126
+ let subagent;
127
+ if (target.agentId) {
128
+ subagent = { agentId: target.agentId, parentSessionId: candidate.sessionId };
129
+ }
130
+ else if (codexParent && !codexParent.fork) {
131
+ subagent = { agentId: candidate.sessionId, parentSessionId: codexParent.id };
132
+ }
133
+ // Ordinals come from the session whose prompts the call answered: a
134
+ // Codex fork or subagent has none of its own, so its parent's count.
135
+ const parentSession = codexParent
136
+ ? byProviderSession.get(`codex:${codexParent.id}`)
137
+ : undefined;
138
+ const session = parentSession ?? candidate;
139
+ const afterOrdinal = !codexParent || parentSession
140
+ ? ordinalAt(await marksFor(session), hit.timestampMs)
141
+ : null;
142
+ found.set(hit.id, {
143
+ provider: candidate.provider,
144
+ sessionId: session.sessionId,
145
+ sessionKey: session.key,
146
+ filePath: target.filePath,
147
+ timestamp: hit.timestamp,
148
+ afterOrdinal,
149
+ ...(hit.toolName ? { toolName: hit.toolName } : {}),
150
+ ...(subagent ? { subagent } : {}),
151
+ });
152
+ // A fork's copy might be replaced by the original later; keep looking.
153
+ if (!isFork)
154
+ remaining.delete(hit.id);
155
+ }
156
+ if (remaining.size === 0)
157
+ break;
158
+ }
159
+ }
160
+ return found;
161
+ }
162
+ /** Highest ordinal among prompts at or before `ms`; marks are in file order. */
163
+ function ordinalAt(marks, ms) {
164
+ let best = null;
165
+ for (const mark of marks) {
166
+ if (Number.isFinite(mark.timestampMs) && mark.timestampMs <= ms) {
167
+ if (best === null || mark.ordinal > best)
168
+ best = mark.ordinal;
169
+ }
170
+ }
171
+ return best;
172
+ }
173
+ function claudeSubagentTranscripts(sessionFile, sessionId) {
174
+ const dir = path.join(path.dirname(sessionFile), sessionId, 'subagents');
175
+ let names;
176
+ try {
177
+ names = fs.readdirSync(dir);
178
+ }
179
+ catch {
180
+ return [];
181
+ }
182
+ return names
183
+ .filter((name) => name.endsWith('.jsonl'))
184
+ .sort()
185
+ .map((name) => ({
186
+ filePath: path.join(dir, name),
187
+ agentId: path.basename(name, '.jsonl').replace(/^agent-/, ''),
188
+ }));
189
+ }
190
+ async function readFirstRecord(filePath) {
191
+ let handle;
192
+ try {
193
+ handle = await fs.promises.open(filePath, 'r');
194
+ const buffer = Buffer.alloc(256 * 1024);
195
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
196
+ const text = buffer.subarray(0, bytesRead).toString('utf8');
197
+ const newline = text.indexOf('\n');
198
+ return JSON.parse(newline === -1 ? text : text.slice(0, newline));
199
+ }
200
+ catch {
201
+ return undefined;
202
+ }
203
+ finally {
204
+ await handle?.close();
205
+ }
206
+ }
207
+ /** Fail-closed scope check on a recorded working directory. */
208
+ function inScope(cwd, roots, realpaths) {
209
+ if (typeof cwd !== 'string' || cwd.length === 0)
210
+ return false;
211
+ const real = (0, workspaceRoots_1.realpathOrNull)(cwd, realpaths);
212
+ if (!real)
213
+ return false;
214
+ return roots.some((root) => {
215
+ if (real === root)
216
+ return true;
217
+ const prefix = root.endsWith(path.sep) ? root : root + path.sep;
218
+ return real.startsWith(prefix);
219
+ });
220
+ }
221
+ const ID_PATTERN = /"(?:id|call_id)"\s*:\s*"([^"\\]{1,256})"/g;
222
+ async function scanForIds(filePath, provider, wanted, roots, realpaths, signal) {
223
+ const hits = [];
224
+ let stream;
225
+ try {
226
+ stream = fs.createReadStream(filePath, { encoding: 'utf8' });
227
+ }
228
+ catch {
229
+ return hits;
230
+ }
231
+ const lines = readline.createInterface({ input: stream, crlfDelay: Infinity });
232
+ // Codex records a working directory per session/turn, not per line.
233
+ let codexCwd;
234
+ try {
235
+ for await (const line of lines) {
236
+ throwIfAborted(signal);
237
+ if (provider === 'codex' && line.includes('"cwd"')) {
238
+ const cwd = codexCwdOf(line);
239
+ if (cwd !== undefined)
240
+ codexCwd = cwd;
241
+ }
242
+ let match = false;
243
+ for (const found of line.matchAll(ID_PATTERN)) {
244
+ if (wanted.has(found[1])) {
245
+ match = true;
246
+ break;
247
+ }
248
+ }
249
+ if (!match)
250
+ continue;
251
+ let record;
252
+ try {
253
+ record = JSON.parse(line);
254
+ }
255
+ catch {
256
+ continue;
257
+ }
258
+ const lineHits = provider === 'claude-code' ? claudeToolUses(record, wanted) : codexToolCall(record, wanted);
259
+ if (lineHits.length === 0)
260
+ continue;
261
+ const cwd = provider === 'claude-code' ? record.cwd : codexCwd;
262
+ if (!inScope(cwd, roots, realpaths))
263
+ continue;
264
+ hits.push(...lineHits);
265
+ }
266
+ }
267
+ catch (error) {
268
+ if (signal?.aborted)
269
+ throw error;
270
+ // Unreadable or vanished file: keep what was found.
271
+ }
272
+ finally {
273
+ lines.close();
274
+ stream.destroy();
275
+ }
276
+ return hits;
277
+ }
278
+ function codexCwdOf(line) {
279
+ try {
280
+ const record = JSON.parse(line);
281
+ if (record.type === 'session_meta' || record.type === 'turn_context') {
282
+ return record.payload?.cwd;
283
+ }
284
+ }
285
+ catch {
286
+ // Not a complete record.
287
+ }
288
+ return undefined;
289
+ }
290
+ function timeOf(record) {
291
+ const timestamp = record.timestamp;
292
+ if (typeof timestamp !== 'string')
293
+ return null;
294
+ const timestampMs = Date.parse(timestamp);
295
+ return Number.isFinite(timestampMs) ? { timestamp, timestampMs } : null;
296
+ }
297
+ function claudeToolUses(record, wanted) {
298
+ const row = record;
299
+ if (row?.type !== 'assistant' || !Array.isArray(row.message?.content))
300
+ return [];
301
+ const time = timeOf(record);
302
+ if (!time)
303
+ return [];
304
+ const hits = [];
305
+ for (const block of row.message.content) {
306
+ const item = block;
307
+ if ((item?.type === 'tool_use' || item?.type === 'server_tool_use') &&
308
+ typeof item.id === 'string' &&
309
+ wanted.has(item.id)) {
310
+ hits.push({
311
+ id: item.id,
312
+ ...time,
313
+ ...(typeof item.name === 'string' ? { toolName: item.name } : {}),
314
+ });
315
+ }
316
+ }
317
+ return hits;
318
+ }
319
+ function codexToolCall(record, wanted) {
320
+ const row = record;
321
+ const payloadType = row?.payload?.type;
322
+ if (row?.type !== 'response_item' ||
323
+ typeof payloadType !== 'string' ||
324
+ !payloadType.endsWith('_call')) {
325
+ return [];
326
+ }
327
+ const callId = row.payload?.call_id;
328
+ if (typeof callId !== 'string' || !wanted.has(callId))
329
+ return [];
330
+ const time = timeOf(record);
331
+ if (!time)
332
+ return [];
333
+ const name = row.payload?.name;
334
+ return [
335
+ {
336
+ id: callId,
337
+ ...time,
338
+ toolName: typeof name === 'string' ? name : payloadType.replace(/_call$/, ''),
339
+ },
340
+ ];
341
+ }
342
+ function throwIfAborted(signal) {
343
+ if (signal?.aborted) {
344
+ throw (signal.reason ?? Object.assign(new Error('The operation was aborted'), { name: 'AbortError' }));
345
+ }
346
+ }
@@ -32,6 +32,13 @@ export interface CanonicalTranscriptBlock {
32
32
  command?: string;
33
33
  truncated?: boolean;
34
34
  raw?: unknown;
35
+ /**
36
+ * The provider's own tool name before normalization (for example Codex
37
+ * `exec_command` behind `Bash`), when the parser recorded one.
38
+ */
39
+ rawToolName?: string;
40
+ /** The MCP server that served this tool call, when the parser recorded one. */
41
+ mcpServerName?: string;
35
42
  }
36
43
  export interface CanonicalTranscriptMessage {
37
44
  id?: string;
@@ -51,10 +58,15 @@ export interface CanonicalToolCall {
51
58
  id?: string;
52
59
  name: string;
53
60
  timestamp: string;
61
+ /** Tool input without the parsers' internal `_sidekick*` bookkeeping keys. */
54
62
  input?: unknown;
55
63
  output?: unknown;
56
64
  isError?: boolean;
57
65
  command?: string;
66
+ /** The provider's own tool name before normalization, when recorded. */
67
+ rawToolName?: string;
68
+ /** The MCP server that served this tool call, when recorded. */
69
+ mcpServerName?: string;
58
70
  source: TranscriptSourceProvenance;
59
71
  }
60
72
  export interface CanonicalTranscriptUsageTotals {
@@ -66,10 +66,12 @@ function projectSessionTranscript(events, options = {}) {
66
66
  blocks.push({ type: 'text', ...boundedText(event.summary, fidelity, maxContentChars) });
67
67
  }
68
68
  if (event.type === 'tool_use') {
69
+ const projected = projectToolInput(event.tool.input);
69
70
  blocks.push({
70
71
  type: 'tool_use',
71
72
  toolName: event.tool.name,
72
- input: boundedValue(event.tool.input, fidelity, maxContentChars),
73
+ input: boundedValue(projected.input, fidelity, maxContentChars),
74
+ ...toolIdentity(projected),
73
75
  });
74
76
  }
75
77
  if (event.type === 'tool_result') {
@@ -90,6 +92,7 @@ function projectSessionTranscript(events, options = {}) {
90
92
  timestamp: event.timestamp,
91
93
  input: block.input,
92
94
  command,
95
+ ...toolIdentity(block),
93
96
  source,
94
97
  };
95
98
  tools.push(call);
@@ -186,12 +189,14 @@ function contentBlocks(content, fidelity, maxChars, includeUnknown = false) {
186
189
  });
187
190
  }
188
191
  else if (block.type === 'tool_use' && typeof block.name === 'string') {
192
+ const projected = projectToolInput(block.input);
189
193
  result.push({
190
194
  type: 'tool_use',
191
195
  toolName: block.name,
192
196
  toolUseId: typeof block.id === 'string' ? block.id : undefined,
193
- input: boundedValue(block.input, fidelity, maxChars),
194
- command: commandFromTool(block.name, block.input),
197
+ input: boundedValue(projected.input, fidelity, maxChars),
198
+ command: commandFromTool(block.name, projected.input),
199
+ ...toolIdentity(projected),
195
200
  });
196
201
  }
197
202
  else if (block.type === 'tool_result') {
@@ -215,6 +220,40 @@ function contentBlocks(content, fidelity, maxChars, includeUnknown = false) {
215
220
  }
216
221
  return result;
217
222
  }
223
+ /** Prefix of the bookkeeping keys session parsers stash inside tool inputs. */
224
+ const SIDEKICK_INPUT_KEY_PREFIX = '_sidekick';
225
+ /**
226
+ * Lifts the parsers' `_sidekickRawToolName` / `_sidekickMcpServerName` out of
227
+ * a tool input and drops every other top-level `_sidekick*` key. Returns a
228
+ * copy when anything is removed; the source event is never modified.
229
+ */
230
+ function projectToolInput(input) {
231
+ if (!input || typeof input !== 'object' || Array.isArray(input))
232
+ return { input };
233
+ const source = input;
234
+ const keys = Object.keys(source);
235
+ if (!keys.some((key) => key.startsWith(SIDEKICK_INPUT_KEY_PREFIX)))
236
+ return { input };
237
+ const clean = {};
238
+ for (const key of keys) {
239
+ if (!key.startsWith(SIDEKICK_INPUT_KEY_PREFIX))
240
+ clean[key] = source[key];
241
+ }
242
+ return {
243
+ input: clean,
244
+ rawToolName: nonEmptyString(source._sidekickRawToolName),
245
+ mcpServerName: nonEmptyString(source._sidekickMcpServerName),
246
+ };
247
+ }
248
+ function toolIdentity(value) {
249
+ return {
250
+ ...(value.rawToolName ? { rawToolName: value.rawToolName } : {}),
251
+ ...(value.mcpServerName ? { mcpServerName: value.mcpServerName } : {}),
252
+ };
253
+ }
254
+ function nonEmptyString(value) {
255
+ return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
256
+ }
218
257
  function boundedText(text, fidelity, maxChars) {
219
258
  if (fidelity === 'full' || text.length <= maxChars)
220
259
  return { text, truncated: false };
@@ -42,6 +42,8 @@ export interface OpenCodeMessage {
42
42
  cacheRead?: number;
43
43
  cacheWrite?: number;
44
44
  reasoning?: number;
45
+ /** Provider-reported total; tells OpenCode's token layouts apart (see `normalizeOpenCodeUsage`). */
46
+ total?: number;
45
47
  };
46
48
  time: {
47
49
  created: string | number;
@@ -11,9 +11,19 @@
11
11
  * every hit and pruned least-recently-used, once per collection.
12
12
  *
13
13
  * A Claude Code session's subagents write their own transcripts under
14
- * `<sessionId>/subagents/`; their usage is attributed to the parent session
15
- * and their sizes and mtimes join the fingerprint. (Codex spawned subagents
16
- * are top-level rollouts, already listed as sessions of their own.)
14
+ * `<sessionId>/subagents/`; their usage is attributed to the parent session,
15
+ * tagged with the subagent, and their sizes and mtimes join the fingerprint.
16
+ * (Codex spawned subagents and forks are top-level rollouts, already listed
17
+ * as sessions of their own; their events are tagged with the parent read
18
+ * from the rollout's first line.)
19
+ *
20
+ * Each event also carries the ordinal of the human prompt it answered, from
21
+ * the same scanner as `collectPromptHistory()`, so usage joins prompt history
22
+ * on `(provider, sessionId, ordinal)`.
23
+ *
24
+ * Cache files are named per cache version (`<provider>--<session>.v3.json`)
25
+ * so readers of different versions sharing the directory never overwrite each
26
+ * other's files; pruning ages out every version's files alike.
17
27
  *
18
28
  * Node-only: reads session files and the cache directory.
19
29
  *
@@ -25,9 +35,11 @@ import type { PricingProvenance } from '../usageNormalization';
25
35
  /**
26
36
  * Bump when the cached record shape or the counting rules change; older files
27
37
  * are re-read. v2: Claude split lines and duplicate Codex `token_count` events
28
- * are counted once, and Claude subagent transcripts are included.
38
+ * are counted once, and Claude subagent transcripts are included. v3: events
39
+ * keep reasoning tokens, prompt ordinal, message id, and subagent, and files
40
+ * move to a versioned name (`<provider>--<session>.v3.json`).
29
41
  */
30
- export declare const USAGE_CACHE_VERSION = 2;
42
+ export declare const USAGE_CACHE_VERSION = 3;
31
43
  /**
32
44
  * Most-recently-used cache files kept on disk. Sized for a busy month of
33
45
  * sessions (files are a few KB each); a collection larger than this still
@@ -47,11 +59,43 @@ export interface UsageEventRecord {
47
59
  /** Four buckets plus the provider-semantics-aware total. */
48
60
  tokens: TokenTotals & {
49
61
  totalTokens: number;
62
+ /**
63
+ * Reasoning tokens the provider reported, when any. Informational, never
64
+ * added to a total: Codex counts them inside `outputTokens`; OpenCode
65
+ * reports them apart, inside `totalTokens` but not `outputTokens`.
66
+ */
67
+ reasoningTokens?: number;
50
68
  };
51
69
  costUsd: number | null;
52
70
  costProvenance: PricingProvenance;
53
71
  /** Session file the event came from; tells apart two files with one session id. */
54
72
  filePath?: string;
73
+ /**
74
+ * Ordinal of the human prompt this call followed: the highest ordinal among
75
+ * the session's prompts timestamped at or before the event, as
76
+ * `collectPromptHistory()` numbers them with default bounds. Claude Code
77
+ * subagent calls map to the parent session's prompts. `null` when no prompt
78
+ * precedes the call, or the session has none (a Codex subagent or fork is a
79
+ * session of its own without human prompts). Absent when
80
+ * unknown: a provider without prompt history (OpenCode), or a cache entry
81
+ * written before prompt ordinals were recorded.
82
+ */
83
+ promptOrdinal?: number | null;
84
+ /**
85
+ * The provider's message id for the call (Claude Code, OpenCode). Absent for
86
+ * Codex, whose `token_count` lines carry no request id (the reader's
87
+ * timestamp-derived placeholder is not passed through).
88
+ */
89
+ requestId?: string;
90
+ /**
91
+ * Set when a subagent made the call. Claude Code: the agent id from its
92
+ * transcript name (`agent-<id>.jsonl`), parent = this session. Codex: the
93
+ * forked or spawned rollout's own session id and the session it came from.
94
+ */
95
+ subagent?: {
96
+ agentId: string;
97
+ parentSessionId: string;
98
+ };
55
99
  }
56
100
  /** One session that contributed events. */
57
101
  export interface UsageSessionRecord {
@@ -111,7 +155,11 @@ export interface CollectUsageEventsResult {
111
155
  roots?: string[];
112
156
  }
113
157
  export declare function getUsageCacheDir(): string;
114
- /** Deletes all but the `keep` most recently used cache files (by mtime). Best-effort. */
158
+ /**
159
+ * Deletes all but the `keep` most recently used cache files (by mtime), of
160
+ * every cache version alike, so files no reader touches any more age out.
161
+ * Best-effort.
162
+ */
115
163
  export declare function pruneUsageCache(keep?: number, dir?: string): number;
116
164
  /**
117
165
  * Collect priced usage events for every session touched in the window.