@adecore/agents 0.39.0 → 0.40.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 (168) hide show
  1. package/dist/async.d.ts +5 -0
  2. package/dist/async.js +13 -5
  3. package/dist/chat/acp-protocol.js +3 -5
  4. package/dist/chat/chat-core.js +0 -2
  5. package/dist/chat/chat-process.d.ts +1 -0
  6. package/dist/chat/chat-process.js +1 -1
  7. package/dist/chat/chat-session.js +6 -8
  8. package/dist/chat/claude-protocol.js +1 -6
  9. package/dist/chat/claude-transcript.js +1 -3
  10. package/dist/chat/codex-protocol.js +1 -6
  11. package/dist/chat/projector.js +1 -3
  12. package/dist/chat/request-summary.js +1 -3
  13. package/dist/coded-error.js +1 -5
  14. package/dist/context/argv.js +1 -2
  15. package/dist/context/verb.js +1 -3
  16. package/dist/error-text.d.ts +1 -0
  17. package/dist/error-text.js +4 -0
  18. package/dist/fields.d.ts +3 -0
  19. package/dist/fields.js +12 -0
  20. package/dist/fs.d.ts +1 -0
  21. package/dist/fs.js +8 -4
  22. package/dist/lineage.js +11 -5
  23. package/dist/messages/notice-store.js +3 -4
  24. package/dist/providers/accounts/acp-account.js +3 -20
  25. package/dist/providers/accounts/service.d.ts +1 -2
  26. package/dist/providers/accounts/service.js +5 -9
  27. package/dist/providers/accounts/status.js +1 -3
  28. package/dist/providers/acp-agent-state.js +1 -3
  29. package/dist/providers/claude.js +0 -1
  30. package/dist/providers/codex.js +0 -1
  31. package/dist/providers/detect.js +1 -1
  32. package/dist/providers/shell-commands.js +1 -2
  33. package/dist/run-process.js +1 -3
  34. package/dist/skills.d.ts +1 -1
  35. package/dist/skills.js +5 -8
  36. package/dist/tasks/background-limit.js +3 -2
  37. package/dist/tasks/task-coordinator.js +11 -17
  38. package/dist/testing/watch.js +0 -1
  39. package/dist/usage/limits/monitor.js +1 -2
  40. package/dist/usage/readers/claude.js +3 -10
  41. package/dist/usage/readers/codex.js +2 -9
  42. package/dist/usage/readers/grok.js +3 -10
  43. package/dist/usage/readers/host-log.js +3 -10
  44. package/dist/usage/record.d.ts +2 -0
  45. package/dist/usage/record.js +13 -0
  46. package/package.json +6 -6
  47. package/src/async.ts +23 -5
  48. package/src/chat/acp-backend.ts +121 -0
  49. package/src/chat/acp-protocol.ts +172 -6
  50. package/src/chat/attachment-store.ts +21 -0
  51. package/src/chat/backend.ts +0 -2
  52. package/src/chat/bookmark-store.ts +23 -0
  53. package/src/chat/chat-core.ts +208 -7
  54. package/src/chat/chat-log.ts +19 -0
  55. package/src/chat/chat-process.ts +52 -1
  56. package/src/chat/chat-session.ts +401 -10
  57. package/src/chat/chat-store.ts +50 -0
  58. package/src/chat/chat-title.ts +9 -0
  59. package/src/chat/claude-backend.ts +50 -0
  60. package/src/chat/claude-protocol.ts +155 -8
  61. package/src/chat/claude-title.ts +10 -0
  62. package/src/chat/claude-transcript.ts +47 -4
  63. package/src/chat/codex-backend.ts +131 -0
  64. package/src/chat/codex-protocol.ts +180 -8
  65. package/src/chat/codex-thread.ts +23 -0
  66. package/src/chat/composer-preferences.ts +6 -0
  67. package/src/chat/context-breakdown.ts +11 -0
  68. package/src/chat/delta-coalescer.ts +8 -0
  69. package/src/chat/external-sessions.ts +28 -0
  70. package/src/chat/generated-image.ts +25 -0
  71. package/src/chat/index.ts +5 -0
  72. package/src/chat/input.ts +13 -0
  73. package/src/chat/json-rpc-transport.ts +28 -0
  74. package/src/chat/limit-resume.ts +13 -0
  75. package/src/chat/projector.ts +160 -4
  76. package/src/chat/readable-text.ts +3 -0
  77. package/src/chat/request-summary.ts +18 -4
  78. package/src/chat/subagent-projection.ts +3 -0
  79. package/src/chat/subagent-reader.ts +76 -0
  80. package/src/chat/subagent-settlement.ts +24 -0
  81. package/src/chat/thread-search.ts +13 -0
  82. package/src/chat/thread.ts +31 -0
  83. package/src/chat/ui-queries.ts +122 -0
  84. package/src/chat/ui-replies.ts +34 -0
  85. package/src/chat/visual-store.ts +49 -0
  86. package/src/chat/wake-chat.ts +3 -0
  87. package/src/client-sinks.ts +2 -0
  88. package/src/coded-error.ts +1 -5
  89. package/src/context/argv.ts +18 -2
  90. package/src/context/index.ts +1 -0
  91. package/src/context/refusal.ts +3 -0
  92. package/src/context/verb.ts +32 -4
  93. package/src/coordination/index.ts +10 -0
  94. package/src/error-text.ts +7 -0
  95. package/src/fields.ts +14 -0
  96. package/src/fs.ts +20 -4
  97. package/src/host/agent-host.ts +20 -0
  98. package/src/host/environment.ts +6 -0
  99. package/src/host/handlers.ts +18 -0
  100. package/src/host/memory-port.ts +4 -0
  101. package/src/host/wiring.ts +10 -0
  102. package/src/index.ts +1 -0
  103. package/src/lineage.ts +24 -5
  104. package/src/messages/deliver-message.ts +6 -0
  105. package/src/messages/deliver-notice.ts +13 -0
  106. package/src/messages/notice-store.ts +16 -4
  107. package/src/modes.ts +1 -0
  108. package/src/outbox/outbox-worker.ts +31 -0
  109. package/src/outbox/outbox.ts +5 -0
  110. package/src/providers/accounts/accounts.ts +36 -0
  111. package/src/providers/accounts/acp-account.ts +22 -27
  112. package/src/providers/accounts/launch.ts +4 -0
  113. package/src/providers/accounts/service.ts +110 -12
  114. package/src/providers/accounts/shadow-home.ts +10 -0
  115. package/src/providers/accounts/status.ts +13 -4
  116. package/src/providers/accounts/store.ts +25 -0
  117. package/src/providers/accounts/variables.ts +11 -1
  118. package/src/providers/acp-agent-state.ts +24 -3
  119. package/src/providers/acp-provider.ts +5 -0
  120. package/src/providers/acp-usage-log.ts +2 -0
  121. package/src/providers/catalog.ts +17 -0
  122. package/src/providers/claude.ts +10 -1
  123. package/src/providers/codex-rules.ts +3 -0
  124. package/src/providers/codex.ts +2 -1
  125. package/src/providers/detect.ts +6 -1
  126. package/src/providers/provider.ts +1 -1
  127. package/src/providers/registry.ts +18 -2
  128. package/src/providers/shell-commands.ts +37 -2
  129. package/src/record-directory.ts +19 -0
  130. package/src/run-process.ts +24 -3
  131. package/src/serializer.ts +3 -0
  132. package/src/skills.ts +73 -10
  133. package/src/tasks/background-limit.ts +9 -2
  134. package/src/tasks/end-children.ts +9 -0
  135. package/src/tasks/give-task.ts +8 -0
  136. package/src/tasks/task-coordinator.ts +81 -17
  137. package/src/tasks/task-store.ts +28 -0
  138. package/src/tasks/waiting-child.ts +21 -0
  139. package/src/tasks/wake-parent.ts +26 -0
  140. package/src/tasks/wiring.ts +22 -0
  141. package/src/testing/accounts.ts +2 -0
  142. package/src/testing/fake-acp-capture.ts +16 -0
  143. package/src/testing/fake-acp.ts +66 -0
  144. package/src/testing/fake-claude.ts +177 -0
  145. package/src/testing/fake-cli.ts +29 -0
  146. package/src/testing/fake-codex.ts +122 -0
  147. package/src/testing/manual-clock.ts +2 -0
  148. package/src/testing/watch.ts +7 -1
  149. package/src/title-file.ts +12 -0
  150. package/src/usage/accounts.ts +6 -0
  151. package/src/usage/aggregate.ts +19 -0
  152. package/src/usage/exchange.ts +12 -0
  153. package/src/usage/index-file.ts +27 -0
  154. package/src/usage/limits/monitor.ts +31 -3
  155. package/src/usage/limits/normalize.ts +30 -0
  156. package/src/usage/limits/probe.ts +17 -0
  157. package/src/usage/pricing.ts +35 -0
  158. package/src/usage/projects.ts +8 -0
  159. package/src/usage/readers/claude.ts +12 -9
  160. package/src/usage/readers/codex.ts +40 -8
  161. package/src/usage/readers/grok.ts +17 -9
  162. package/src/usage/readers/host-log.ts +9 -9
  163. package/src/usage/record.ts +22 -0
  164. package/src/usage/roots.ts +6 -0
  165. package/src/usage/scanner.ts +61 -0
  166. package/src/usage/usage-service.ts +14 -0
  167. package/src/watch-seam.ts +8 -0
  168. package/src/windows-command.ts +12 -0
@@ -26,6 +26,7 @@ function timeoutOf<T>(work: Promise<T>, ms: number, what: string): Promise<T> {
26
26
  */
27
27
  export async function probeClaude(command: readonly string[], env: Record<string, string>, spawn: SpawnChatProcess = spawnChatProcess): Promise<ProbeResult> {
28
28
  let child: ChatProcess;
29
+
29
30
  try {
30
31
  child = spawn({
31
32
  command: [...command, '-p', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose'],
@@ -36,45 +37,58 @@ export async function probeClaude(command: readonly string[], env: Record<string
36
37
  } catch (error) {
37
38
  return failure(errorText(error));
38
39
  }
40
+
39
41
  const write = (frame: unknown): void => {
40
42
  child.stdin.write(`${JSON.stringify(frame)}\n`);
41
43
  child.stdin.flush();
42
44
  };
45
+
43
46
  const read = async (): Promise<ProbeResult> => {
44
47
  write({ type: 'control_request', request_id: 'probe-init', request: { subtype: 'initialize', hooks: {} } });
45
48
  let asked = false;
46
49
  let buffer = '';
47
50
  const decoder = new TextDecoder();
51
+
48
52
  for await (const chunk of child.stdout as unknown as AsyncIterable<Uint8Array>) {
49
53
  buffer += decoder.decode(chunk, { stream: true });
50
54
  let at = buffer.indexOf('\n');
55
+
51
56
  for (; at !== -1; at = buffer.indexOf('\n')) {
52
57
  const line = buffer.slice(0, at);
53
58
  buffer = buffer.slice(at + 1);
54
59
  let frame: unknown;
60
+
55
61
  try {
56
62
  frame = JSON.parse(line);
57
63
  } catch {
58
64
  continue;
59
65
  }
66
+
60
67
  if (typeof frame !== 'object' || frame === null) {
61
68
  continue;
62
69
  }
70
+
63
71
  const envelope = frame as { type?: unknown; request_id?: unknown; response?: { subtype?: unknown; response?: unknown } };
72
+
64
73
  if (envelope.type !== 'control_response') {
65
74
  continue;
66
75
  }
76
+
67
77
  if (!asked) {
68
78
  asked = true;
69
79
  write({ type: 'control_request', request_id: 'probe-usage', request: { subtype: 'get_usage' } });
80
+
70
81
  continue;
71
82
  }
83
+
72
84
  if (envelope.response?.subtype === 'error') {
73
85
  return failure('Claude Code refused to report its usage');
74
86
  }
87
+
75
88
  return readClaudeUsage(envelope.response?.response ?? envelope.response);
76
89
  }
77
90
  }
91
+
78
92
  return failure('Claude Code closed without reporting its usage');
79
93
  };
80
94
 
@@ -95,6 +109,7 @@ export async function probeCodex(
95
109
  client: CodexClientInfo = DEFAULT_CODEX_CLIENT
96
110
  ): Promise<ProbeResult> {
97
111
  let transport: CodexTransport | null = null;
112
+
98
113
  try {
99
114
  transport = new CodexTransport({
100
115
  command: [...command],
@@ -103,12 +118,14 @@ export async function probeCodex(
103
118
  onFrame: () => undefined,
104
119
  onExit: () => undefined
105
120
  });
121
+
106
122
  const open = transport;
107
123
  await timeoutOf(open.request('initialize', { clientInfo: client, capabilities: { experimentalApi: true } }), PROBE_TIMEOUT_MS, 'Codex');
108
124
  open.notify('initialized', {});
109
125
  const answer = await timeoutOf(open.request('account/rateLimits/read', undefined), CODEX_READ_TIMEOUT_MS, 'Codex');
110
126
  const snapshot = typeof answer === 'object' && answer !== null ? (answer as { rateLimits?: unknown }).rateLimits : null;
111
127
  const reading = readCodexLimits(snapshot);
128
+
112
129
  // An account on an API key has no plan windows; Codex answers, with nothing in it.
113
130
  return reading ?? { unavailable: { reason: 'no-subscription', message: null } };
114
131
  } catch (error) {
@@ -61,24 +61,31 @@ function rate(value: unknown): number | null {
61
61
  */
62
62
  export function parsePriceTable(document: unknown): PriceTable {
63
63
  const table: PriceTable = new Map();
64
+
64
65
  if (typeof document !== 'object' || document === null) {
65
66
  return table;
66
67
  }
68
+
67
69
  for (const [name, value] of Object.entries(document as Record<string, LiteLlmEntry>)) {
68
70
  if (typeof value !== 'object' || value === null) {
69
71
  continue;
70
72
  }
73
+
71
74
  if (typeof value.litellm_provider !== 'string' || !PROVIDERS.has(value.litellm_provider)) {
72
75
  continue;
73
76
  }
77
+
74
78
  if (typeof value.mode !== 'string' || !MODES.has(value.mode)) {
75
79
  continue;
76
80
  }
81
+
77
82
  const input = rate(value.input_cost_per_token);
78
83
  const output = rate(value.output_cost_per_token);
84
+
79
85
  if (input === null || output === null) {
80
86
  continue;
81
87
  }
88
+
82
89
  const price: ModelPrice = {
83
90
  input,
84
91
  output,
@@ -86,7 +93,9 @@ export function parsePriceTable(document: unknown): PriceTable {
86
93
  cacheWrite: rate(value.cache_creation_input_token_cost) ?? input * CACHE_WRITE_SHARE,
87
94
  cacheWrite1h: input * CACHE_WRITE_1H_SHARE
88
95
  };
96
+
89
97
  const tiers: Partial<Record<string, ModelPrice>> = {};
98
+
90
99
  for (const [tier, multiplier] of [
91
100
  ['priority', 2],
92
101
  ['flex', 0.5],
@@ -100,22 +109,27 @@ export function parsePriceTable(document: unknown): PriceTable {
100
109
  cacheWrite1h: price.cacheWrite1h * multiplier
101
110
  };
102
111
  }
112
+
103
113
  if (value.litellm_provider === 'openai') {
104
114
  price.tiers = tiers;
105
115
  }
116
+
106
117
  const key = name.trim().toLowerCase();
107
118
  table.set(key, price);
108
119
  const bare = key.slice(key.lastIndexOf('/') + 1);
120
+
109
121
  if (bare !== key && !table.has(bare)) {
110
122
  table.set(bare, price);
111
123
  }
112
124
  }
125
+
113
126
  return table;
114
127
  }
115
128
 
116
129
  /* `claude-opus-4-5-20251101` is the same model as `claude-opus-4-5`, dated. */
117
130
  function withoutDate(key: string): string | null {
118
131
  const match = /^(.*)-\d{6,8}$/.exec(key);
132
+
119
133
  return match === null ? null : match[1]!;
120
134
  }
121
135
 
@@ -134,37 +148,48 @@ const REVIEW_MODELS = [
134
148
  export function lookupPrice(table: PriceTable, model: string, serviceTier?: string, timestampMs?: number): PriceLookup {
135
149
  const found = lookupBasePrice(table, model, timestampMs);
136
150
  const tier = serviceTier === 'fast' ? 'priority' : serviceTier;
151
+
137
152
  return { ...found, price: (tier === undefined ? undefined : found.price?.tiers?.[tier]) ?? found.price };
138
153
  }
139
154
 
140
155
  function lookupBasePrice(table: PriceTable, model: string, timestampMs?: number): PriceLookup {
141
156
  const key = model.trim().toLowerCase().split('[')[0]!;
142
157
  const bare = key.slice(key.lastIndexOf('/') + 1);
158
+
143
159
  if (bare === '' || AMBIGUOUS.has(bare)) {
144
160
  return { price: null, basis: 'unknown', pricedAs: null };
145
161
  }
162
+
146
163
  const exact = table.get(key) ?? table.get(bare);
164
+
147
165
  if (exact !== undefined) {
148
166
  return { price: exact, basis: 'exact', pricedAs: null };
149
167
  }
168
+
150
169
  if (bare === 'codex-auto-review' && timestampMs !== undefined && Number.isFinite(timestampMs)) {
151
170
  const date = new Date(timestampMs).toISOString().slice(0, 10);
152
171
  const fallback = REVIEW_MODELS.find(([released]) => date >= released)?.[1] ?? 'gpt-5';
153
172
  const found = lookupBasePrice(table, fallback);
173
+
154
174
  return { price: found.price, basis: found.price === null ? 'unknown' : 'family', pricedAs: found.price === null ? null : fallback };
155
175
  }
176
+
156
177
  const dated = withoutDate(bare);
157
178
  const undated = dated === null ? undefined : table.get(dated);
179
+
158
180
  if (undated !== undefined && dated !== null) {
159
181
  return { price: undated, basis: 'exact', pricedAs: dated };
160
182
  }
183
+
161
184
  for (let family = dated ?? bare; family.includes('-');) {
162
185
  family = family.slice(0, family.lastIndexOf('-'));
163
186
  const found = AMBIGUOUS.has(family) ? undefined : table.get(family);
187
+
164
188
  if (found !== undefined) {
165
189
  return { price: found, basis: 'family', pricedAs: family };
166
190
  }
167
191
  }
192
+
168
193
  return { price: null, basis: 'unknown', pricedAs: null };
169
194
  }
170
195
 
@@ -222,6 +247,7 @@ export class PriceBook {
222
247
  /* Reads the table an earlier run stored, once; asking LiteLLM is left to `ensure`. */
223
248
  load(): Promise<void> {
224
249
  this.stored ??= this.readStored();
250
+
225
251
  return this.stored;
226
252
  }
227
253
 
@@ -232,6 +258,7 @@ export class PriceBook {
232
258
  .finally(() => {
233
259
  this.inFlight = null;
234
260
  });
261
+
235
262
  return this.inFlight;
236
263
  }
237
264
 
@@ -243,6 +270,7 @@ export class PriceBook {
243
270
  try {
244
271
  const snapshot = JSON.parse(await readFile(this.file, 'utf8')) as Snapshot;
245
272
  const table = parsePriceTable(snapshot?.document);
273
+
246
274
  if (table.size > 0 && typeof snapshot.fetchedAt === 'number') {
247
275
  this.table = table;
248
276
  this.source = 'litellm';
@@ -259,17 +287,23 @@ export class PriceBook {
259
287
  if (!this.allowFetch || (this.fetchedAt !== null && Date.now() - this.fetchedAt < TTL_MS)) {
260
288
  return false;
261
289
  }
290
+
262
291
  let changed = false;
292
+
263
293
  try {
264
294
  const response = await fetch(LITELLM_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
295
+
265
296
  if (!response.ok) {
266
297
  throw new Error(`LiteLLM answered ${response.status}`);
267
298
  }
299
+
268
300
  const document: unknown = await response.json();
269
301
  const table = parsePriceTable(document);
302
+
270
303
  if (table.size === 0) {
271
304
  throw new Error('The LiteLLM table held no model this app prices');
272
305
  }
306
+
273
307
  this.table = table;
274
308
  this.source = 'litellm';
275
309
  this.fetchedAt = Date.now();
@@ -280,6 +314,7 @@ export class PriceBook {
280
314
  // Whatever is loaded keeps pricing; a table that could not be refreshed is not a failure.
281
315
  console.warn('Could not refresh the price table:', errorText(e));
282
316
  }
317
+
283
318
  return changed;
284
319
  }
285
320
  }
@@ -22,6 +22,7 @@ async function mainCheckoutOf(gitPath: string): Promise<string | null> {
22
22
  const match = /^gitdir:\s*(.+)$/m.exec(text);
23
23
  const gitDir = match?.[1]?.trim();
24
24
  const at = gitDir?.indexOf('/.git/worktrees/') ?? -1;
25
+
25
26
  return gitDir !== undefined && at > 0 ? gitDir.slice(0, at) : null;
26
27
  }
27
28
 
@@ -41,25 +42,32 @@ export class ProjectResolver {
41
42
  async resolve(cwd: string): Promise<ResolvedProject> {
42
43
  const folder = cwd === '' ? '' : await this.rootOf(cwd);
43
44
  const project = this.known.get(folder);
45
+
44
46
  return { folder, name: project?.name ?? (folder === '' ? 'Elsewhere' : basename(folder)), projectId: project?.projectId ?? null };
45
47
  }
46
48
 
47
49
  private async rootOf(cwd: string): Promise<string> {
48
50
  const cached = this.roots.get(cwd);
51
+
49
52
  if (cached !== undefined) {
50
53
  return cached;
51
54
  }
55
+
52
56
  let root = cwd;
57
+
53
58
  for (let at = cwd; at !== dirname(at); at = dirname(at)) {
54
59
  try {
55
60
  const info = await stat(join(at, '.git'));
56
61
  root = (info.isFile() ? await mainCheckoutOf(join(at, '.git')) : null) ?? at;
62
+
57
63
  break;
58
64
  } catch {
59
65
  // Not a checkout at this level; the one above it may be.
60
66
  }
61
67
  }
68
+
62
69
  this.roots.set(cwd, root);
70
+
63
71
  return root;
64
72
  }
65
73
  }
@@ -1,4 +1,4 @@
1
- import { asObject, asString, int, type UsageRecord } from '../record.ts';
1
+ import { asObject, asString, countsTokens, int, parseObject, type UsageRecord } from '../record.ts';
2
2
 
3
3
  /* Only a line with this in it can carry counts, which keeps `JSON.parse` off about two thirds of them. */
4
4
  export function claudeMightCarryUsage(line: string): boolean {
@@ -11,31 +11,32 @@ export function claudeMightCarryUsage(line: string): boolean {
11
11
  * Anthropic's, so `input_tokens` already excludes what was read from or written to the cache.
12
12
  */
13
13
  export function parseClaudeLine(line: string): UsageRecord | null {
14
- let parsed: unknown;
15
- try {
16
- parsed = JSON.parse(line);
17
- } catch {
18
- return null;
19
- }
20
- const record = asObject(parsed);
14
+ const record = parseObject(line);
15
+
21
16
  if (record === null || record.type !== 'assistant') {
22
17
  return null;
23
18
  }
19
+
24
20
  const message = asObject(record.message);
25
21
  const usage = message === null ? null : asObject(message.usage);
22
+
26
23
  if (message === null || usage === null) {
27
24
  return null;
28
25
  }
26
+
29
27
  const model = asString(message.model);
30
28
  const timestampMs = Date.parse(asString(record.timestamp));
29
+
31
30
  // `<synthetic>` is what Claude Code writes for a message it made up itself, an error or an
32
31
  // interruption. It never reached the API, and its counts are zero, so it is not a call.
33
32
  if (model === '' || model === '<synthetic>' || Number.isNaN(timestampMs)) {
34
33
  return null;
35
34
  }
35
+
36
36
  const creation = asObject(usage.cache_creation);
37
37
  const outputDetails = asObject(usage.output_tokens_details);
38
38
  const messageId = asString(message.id);
39
+
39
40
  const totals = {
40
41
  calls: 1,
41
42
  input: int(usage.input_tokens),
@@ -45,9 +46,11 @@ export function parseClaudeLine(line: string): UsageRecord | null {
45
46
  output: int(usage.output_tokens),
46
47
  reasoning: outputDetails === null ? 0 : int(outputDetails.thinking_tokens)
47
48
  };
48
- if (totals.input + totals.cacheRead + totals.cacheWrite + totals.output === 0) {
49
+
50
+ if (!countsTokens(totals)) {
49
51
  return null;
50
52
  }
53
+
51
54
  return {
52
55
  provider: 'claude',
53
56
  timestampMs,
@@ -1,4 +1,4 @@
1
- import { asObject, asString, int, type UsageRecord } from '../record.ts';
1
+ import { asObject, asString, int, parseObject, type UsageRecord } from '../record.ts';
2
2
 
3
3
  export function codexMightCarryUsage(line: string): boolean {
4
4
  return ['token_count', 'turn_context', 'session_meta', 'thread_settings_applied', 'token_usage_record', 'compacted'].some((type) => line.includes(type));
@@ -76,6 +76,7 @@ function deltaOf(total: CodexCounts, previous: CodexCounts | null): CodexCounts
76
76
  if (previous === null) {
77
77
  return total;
78
78
  }
79
+
79
80
  const delta: CodexCounts = {
80
81
  input: total.input - previous.input,
81
82
  cached: total.cached - previous.cached,
@@ -83,6 +84,7 @@ function deltaOf(total: CodexCounts, previous: CodexCounts | null): CodexCounts
83
84
  output: total.output - previous.output,
84
85
  reasoning: total.reasoning - previous.reasoning
85
86
  };
87
+
86
88
  // A compaction or a resume restarts the count, so anything falling is not a delta at all.
87
89
  return delta.input < 0 || delta.cached < 0 || delta.cacheWrite < 0 || delta.output < 0 || delta.reasoning < 0 ? null : delta;
88
90
  }
@@ -92,21 +94,18 @@ export function isForkOrSubagent(payload: Record<string, unknown>): boolean {
92
94
  if (typeof payload.forked_from_id === 'string' || typeof payload.parent_thread_id === 'string') {
93
95
  return true;
94
96
  }
97
+
95
98
  const spawn = asObject(asObject(payload.source)?.subagent)?.thread_spawn;
99
+
96
100
  return typeof asObject(spawn)?.parent_thread_id === 'string';
97
101
  }
98
102
 
99
103
  /* Codex includes cache reads and writes in input_tokens. Compaction can appear separately from
100
104
  cumulative token_count events, so response identities prevent charging the same request twice. */
101
105
  export function parseCodexLine(line: string, state: CodexParserState): UsageRecord | null {
102
- let parsed: unknown;
103
- try {
104
- parsed = JSON.parse(line);
105
- } catch {
106
- return null;
107
- }
108
- const record = asObject(parsed);
106
+ const record = parseObject(line);
109
107
  const payload = record === null ? null : asObject(record.payload);
108
+
110
109
  if (record === null || payload === null) {
111
110
  return null;
112
111
  }
@@ -116,33 +115,42 @@ export function parseCodexLine(line: string, state: CodexParserState): UsageReco
116
115
  if (state.sawSessionMeta) {
117
116
  return null;
118
117
  }
118
+
119
119
  state.sawSessionMeta = true;
120
120
  state.sessionId = asString(payload.id) || asString(payload.session_id);
121
121
  state.cwd = asString(payload.cwd);
122
+
122
123
  if (isForkOrSubagent(payload)) {
123
124
  state.suppressing = true;
124
125
  state.anchorMs = Date.parse(asString(record.timestamp)) || 0;
125
126
  }
127
+
126
128
  return null;
127
129
  }
128
130
 
129
131
  if (record.type === 'turn_context') {
130
132
  const model = asString(payload.model);
133
+
131
134
  if (model !== '') {
132
135
  state.model = model;
133
136
  }
137
+
134
138
  const cwd = asString(payload.cwd);
139
+
135
140
  if (cwd !== '') {
136
141
  state.cwd = cwd;
137
142
  }
143
+
138
144
  return null;
139
145
  }
140
146
 
141
147
  if (payload.type === 'thread_settings_applied') {
142
148
  const settings = asObject(payload.thread_settings);
149
+
143
150
  if (settings !== null && typeof settings.service_tier === 'string') {
144
151
  state.serviceTier = settings.service_tier;
145
152
  }
153
+
146
154
  return null;
147
155
  }
148
156
 
@@ -153,26 +161,33 @@ export function parseCodexLine(line: string, state: CodexParserState): UsageReco
153
161
  if (payload.type !== 'token_count') {
154
162
  return null;
155
163
  }
164
+
156
165
  const info = asObject(payload.info);
157
166
  const total = info === null ? null : asObject(info.total_token_usage);
167
+
158
168
  if (info === null || total === null || state.model === null) {
159
169
  return null;
160
170
  }
171
+
161
172
  const timestampMs = Date.parse(asString(record.timestamp));
173
+
162
174
  if (Number.isNaN(timestampMs)) {
163
175
  return null;
164
176
  }
165
177
 
166
178
  const signature = JSON.stringify(info.total_token_usage);
179
+
167
180
  if (signature === state.lastSignature) {
168
181
  return null;
169
182
  }
183
+
170
184
  state.lastSignature = signature;
171
185
 
172
186
  const cumulative = countsOf(total);
173
187
  const last = asObject(info.last_token_usage);
174
188
  const counts = deltaOf(cumulative, state.total) ?? (last === null ? null : countsOf(last));
175
189
  state.total = cumulative;
190
+
176
191
  if (counts === null) {
177
192
  return null;
178
193
  }
@@ -180,19 +195,23 @@ export function parseCodexLine(line: string, state: CodexParserState): UsageReco
180
195
  if (state.suppressing) {
181
196
  if (timestampMs - state.anchorMs <= FORK_COPY_MAX_GAP_MS) {
182
197
  state.anchorMs = timestampMs;
198
+
183
199
  return null;
184
200
  }
201
+
185
202
  // The first event that is not part of the replayed burst is this thread's own work.
186
203
  state.suppressing = false;
187
204
  }
188
205
 
189
206
  let dedupeKey: string | null = null;
190
207
  const pending = state.latestResponse === null ? undefined : state.pendingCompactions[state.latestResponse];
208
+
191
209
  if (pending !== undefined && (sameCounts(pending.counts, counts) || sameCounts(pending.threadTotal, cumulative))) {
192
210
  dedupeKey = `codex:response:${state.latestResponse}`;
193
211
  delete state.pendingCompactions[state.latestResponse!];
194
212
  state.handledResponses.push(state.latestResponse!);
195
213
  }
214
+
196
215
  return usageRecord(counts, timestampMs, state.model, state, dedupeKey);
197
216
  }
198
217
 
@@ -209,9 +228,11 @@ function sameCounts(first: CodexCounts | null, second: CodexCounts): boolean {
209
228
 
210
229
  function parseCompaction(record: Record<string, unknown>, payload: Record<string, unknown>, state: CodexParserState): UsageRecord | null {
211
230
  const id = asString(record.type === 'compacted' ? payload.compaction_response_id : payload.response_id);
231
+
212
232
  if (id === '' || state.handledResponses.includes(id)) {
213
233
  return null;
214
234
  }
235
+
215
236
  if (record.type === 'compacted') {
216
237
  if (!state.compacted.includes(id)) {
217
238
  state.compacted.push(id);
@@ -220,29 +241,38 @@ function parseCompaction(record: Record<string, unknown>, payload: Record<string
220
241
  const usage = asObject(payload.usage);
221
242
  const model = asString(payload.model) || state.model;
222
243
  const timestampMs = Date.parse(asString(record.timestamp));
244
+
223
245
  if (usage === null || model === null || !Number.isFinite(timestampMs)) {
224
246
  return null;
225
247
  }
248
+
226
249
  const counts = countsOf(usage);
227
250
  const parsed = usageRecord(counts, timestampMs, model, state, `codex:response:${id}`);
251
+
228
252
  if (parsed === null) {
229
253
  return null;
230
254
  }
255
+
231
256
  state.latestResponse = id;
232
257
  const threadTotal = asObject(payload.thread_token_usage);
233
258
  state.pendingCompactions[id] ??= { record: parsed, counts, threadTotal: threadTotal === null ? null : countsOf(threadTotal) };
234
259
  }
260
+
235
261
  const pending = state.pendingCompactions[id];
262
+
236
263
  if (!state.compacted.includes(id) || pending === undefined) {
237
264
  return null;
238
265
  }
266
+
239
267
  state.handledResponses.push(id);
240
268
  delete state.pendingCompactions[id];
269
+
241
270
  return pending.record;
242
271
  }
243
272
 
244
273
  function usageRecord(counts: CodexCounts, timestampMs: number, model: string, state: CodexParserState, dedupeKey: string | null): UsageRecord | null {
245
274
  const uncached = Math.max(0, counts.input - counts.cached - counts.cacheWrite);
275
+
246
276
  const totals = {
247
277
  calls: 1,
248
278
  input: uncached,
@@ -252,9 +282,11 @@ function usageRecord(counts: CodexCounts, timestampMs: number, model: string, st
252
282
  output: counts.output,
253
283
  reasoning: Math.min(counts.output, counts.reasoning)
254
284
  };
285
+
255
286
  if (uncached + counts.cached + counts.cacheWrite + counts.output === 0) {
256
287
  return null;
257
288
  }
289
+
258
290
  return {
259
291
  provider: 'codex',
260
292
  timestampMs,
@@ -1,5 +1,5 @@
1
1
  import { basename, dirname } from 'node:path';
2
- import { asObject, asString, int, type UsageRecord } from '../record.ts';
2
+ import { asObject, asString, countsTokens, int, parseObject, type UsageRecord } from '../record.ts';
3
3
 
4
4
  /* The file Grok keeps the counts of a session in, beside its other files in the session's folder. */
5
5
  export const GROK_USAGE_FILE = 'usage.json';
@@ -19,6 +19,7 @@ function cwdOf(path: string): string {
19
19
  function recordOf(counts: Record<string, unknown>, model: string, base: Omit<UsageRecord, 'model' | 'totals' | 'dedupeKey'>, turn: string): UsageRecord | null {
20
20
  const cacheRead = int(counts.cachedReadTokens);
21
21
  const cacheWrite = int(counts.cacheCreationTokens);
22
+
22
23
  const totals = {
23
24
  calls: int(counts.modelCalls),
24
25
  // Grok's input holds both cache kinds: its total is input plus output.
@@ -29,10 +30,13 @@ function recordOf(counts: Record<string, unknown>, model: string, base: Omit<Usa
29
30
  output: int(counts.outputTokens),
30
31
  reasoning: int(counts.reasoningTokens)
31
32
  };
32
- if (totals.input + totals.cacheRead + totals.cacheWrite + totals.output === 0) {
33
+
34
+ if (!countsTokens(totals)) {
33
35
  return null;
34
36
  }
37
+
35
38
  const ticks = int(counts.costUsdTicks);
39
+
36
40
  return {
37
41
  ...base,
38
42
  model,
@@ -49,41 +53,45 @@ function recordOf(counts: Record<string, unknown>, model: string, base: Omit<Usa
49
53
  * read as one, which leaves what the scanner held of it.
50
54
  */
51
55
  export function parseGrokUsage(text: string, path: string): UsageRecord[] | null {
52
- let parsed: unknown;
53
- try {
54
- parsed = JSON.parse(text);
55
- } catch {
56
- return null;
57
- }
58
- const file = asObject(parsed);
56
+ const file = parseObject(text);
57
+
59
58
  if (file === null || !Array.isArray(file.turns)) {
60
59
  return null;
61
60
  }
61
+
62
62
  const sessionId = asString(file.sessionId) || basename(dirname(path));
63
63
  const cwd = cwdOf(path);
64
64
  const records: UsageRecord[] = [];
65
+
65
66
  for (const entry of file.turns) {
66
67
  const turn = asObject(entry);
67
68
  const timestampMs = turn === null ? Number.NaN : Date.parse(asString(turn.endedAt));
69
+
68
70
  if (turn === null || Number.isNaN(timestampMs)) {
69
71
  continue;
70
72
  }
73
+
71
74
  const number = typeof turn.turnNumber === 'number' ? String(turn.turnNumber) : asString(turn.endedAt);
72
75
  const base = { provider: 'grok' as const, timestampMs, sessionId, cwd };
73
76
  const byModel = asObject(turn.modelUsage);
77
+
74
78
  const models: Array<[string, Record<string, unknown>]> =
75
79
  byModel === null
76
80
  ? [[asString(turn.primaryModelId), turn]]
77
81
  : Object.entries(byModel).flatMap(([model, counts]) => {
78
82
  const object = asObject(counts);
83
+
79
84
  return object === null ? [] : [[model, object] as [string, Record<string, unknown>]];
80
85
  });
86
+
81
87
  for (const [model, counts] of models) {
82
88
  const record = model === '' ? null : recordOf(counts, model, base, number);
89
+
83
90
  if (record !== null) {
84
91
  records.push(record);
85
92
  }
86
93
  }
87
94
  }
95
+
88
96
  return records;
89
97
  }