@magnusekdahl/parallix 1.2.0 → 1.2.1

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 (51) hide show
  1. package/lib/agents/agents.js +737 -838
  2. package/lib/agents/agents.ts +930 -0
  3. package/lib/agents/claude-telemetry.js +149 -154
  4. package/lib/agents/claude-telemetry.ts +233 -0
  5. package/lib/agents/claude.js +121 -131
  6. package/lib/agents/claude.ts +156 -0
  7. package/lib/agents/codex-telemetry.js +176 -164
  8. package/lib/agents/codex-telemetry.ts +205 -0
  9. package/lib/agents/codex.js +168 -201
  10. package/lib/agents/codex.ts +236 -0
  11. package/lib/agents/limit-hit.js +216 -226
  12. package/lib/agents/limit-hit.ts +262 -0
  13. package/lib/agents/mistral-telemetry.js +7 -10
  14. package/lib/agents/mistral-telemetry.ts +44 -0
  15. package/lib/agents/mistral.js +40 -58
  16. package/lib/agents/mistral.ts +84 -0
  17. package/lib/agents/opencode-export.js +126 -127
  18. package/lib/agents/opencode-export.ts +160 -0
  19. package/lib/agents/opencode-telemetry.js +282 -261
  20. package/lib/agents/opencode-telemetry.ts +350 -0
  21. package/lib/agents/opencode.js +263 -305
  22. package/lib/agents/opencode.ts +370 -0
  23. package/lib/agents/stage-telemetry.js +23 -24
  24. package/lib/agents/stage-telemetry.ts +47 -0
  25. package/lib/commands/integrate.js +9 -3
  26. package/lib/core/fmt.js +140 -211
  27. package/lib/core/fmt.ts +189 -0
  28. package/lib/core/git.js +121 -102
  29. package/lib/core/git.ts +146 -0
  30. package/lib/core/gitignore.js +59 -106
  31. package/lib/core/gitignore.ts +103 -0
  32. package/lib/core/mission-utils.js +833 -875
  33. package/lib/core/mission-utils.ts +998 -0
  34. package/lib/core/persistent-data-migration.js +247 -205
  35. package/lib/core/persistent-data-migration.ts +255 -0
  36. package/lib/core/product-config.js +400 -476
  37. package/lib/core/product-config.ts +518 -0
  38. package/lib/core/runtime-matrix.js +41 -87
  39. package/lib/core/runtime-matrix.ts +84 -0
  40. package/lib/core/spawn-tee.js +152 -192
  41. package/lib/core/spawn-tee.ts +202 -0
  42. package/lib/core/state-map.js +92 -92
  43. package/lib/core/state-map.ts +115 -0
  44. package/lib/core/storage.js +101 -129
  45. package/lib/core/storage.ts +182 -0
  46. package/lib/core/verification.js +148 -138
  47. package/lib/core/verification.ts +200 -0
  48. package/package.json +4 -2
  49. package/prompts/draft.md +1 -0
  50. package/prompts/portfolio.md +1 -1
  51. package/px.js +0 -2
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
-
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseClaudeStreamJson = parseClaudeStreamJson;
4
+ exports.extractClaudeTelemetryFromStdout = extractClaudeTelemetryFromStdout;
3
5
  // Claude telemetry is read from the Claude CLI's `--output-format stream-json`
4
6
  // stdout stream (see claude.js `buildClaudeInvocation`). Unlike Codex — which
5
7
  // writes a per-session rollout JSONL to disk — the Claude CLI emits its usage
@@ -33,27 +35,21 @@
33
35
  // the stage's total generated output is the SUM of each turn's final value.
34
36
  // - `cache_read_input_tokens` / `cache_creation_input_tokens` appear on
35
37
  // message_start.usage when prompt caching is active; summed across turns.
36
-
37
38
  const PROVIDER = 'anthropic';
38
-
39
- /** @param {*} value */
40
39
  function num(value) {
41
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
40
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
42
41
  }
43
-
44
42
  /**
45
43
  * Unwrap a parsed JSONL line into its underlying Anthropic SSE event. The CLI's
46
44
  * `--include-partial-messages` form nests the real event under `stream_event`;
47
45
  * the bare form has it at the top level. Returns the inner event object.
48
46
  */
49
- /** @param {any} evt */
50
47
  function unwrapEvent(evt) {
51
- if (evt && evt.type === 'stream_event' && evt.event && typeof evt.event === 'object') {
52
- return evt.event;
53
- }
54
- return evt;
48
+ if (evt && evt.type === 'stream_event' && evt.event && typeof evt.event === 'object') {
49
+ return evt.event;
50
+ }
51
+ return evt;
55
52
  }
56
-
57
53
  /**
58
54
  * Parse a Claude `stream-json` stdout string into a normalized structure of the
59
55
  * usage-bearing events. Returns null when the content yields no usable signal
@@ -68,97 +64,106 @@ function unwrapEvent(evt) {
68
64
  * resultUsage: { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens } | null
69
65
  * }
70
66
  */
71
- /** @param {string} content */
72
67
  function parseClaudeStreamJson(content) {
73
- if (!content) {return null;}
74
-
75
- let sessionId = null;
76
- let model = null;
77
- let toolCalls = 0;
78
- let resultUsage = null;
79
- let resultCostUsd = null;
80
- const messageStarts = [];
81
- const messageDeltas = [];
82
-
83
- for (const line of String(content).split('\n')) {
84
- const trimmed = line.trim();
85
- if (!trimmed.startsWith('{')) {continue;}
86
- let outer;
87
- try {
88
- outer = JSON.parse(trimmed);
89
- } catch (_) {
90
- continue;
68
+ if (!content) {
69
+ return null;
91
70
  }
92
- if (!outer || typeof outer !== 'object') {continue;}
93
-
94
- // Top-level CLI envelope fields (present on system/result and sometimes the
95
- // wrapper) carry session/model metadata.
96
- if (outer.session_id) {sessionId = outer.session_id;}
97
- if (outer.type === 'system' && outer.model) {model = outer.model;}
98
-
99
- // The final `result` event carries `total_cost_usd` (direct from the CLI)
100
- // and an aggregate usage object (fallback when partial events were evicted).
101
- if (outer.type === 'result') {
102
- if (outer.usage && typeof outer.usage === 'object') {
103
- resultUsage = {
104
- inputTokens: num(outer.usage.input_tokens),
105
- outputTokens: num(outer.usage.output_tokens),
106
- cacheReadTokens: num(outer.usage.cache_read_input_tokens),
107
- cacheCreationTokens: num(outer.usage.cache_creation_input_tokens),
108
- };
109
- }
110
- if (typeof outer.total_cost_usd === 'number' && Number.isFinite(outer.total_cost_usd)) {
111
- resultCostUsd = outer.total_cost_usd;
112
- }
71
+ let sessionId = null;
72
+ let model = null;
73
+ let toolCalls = 0;
74
+ let resultUsage = null;
75
+ let resultCostUsd = null;
76
+ const messageStarts = [];
77
+ const messageDeltas = [];
78
+ for (const line of String(content).split('\n')) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed.startsWith('{')) {
81
+ continue;
82
+ }
83
+ let outer;
84
+ try {
85
+ outer = JSON.parse(trimmed);
86
+ }
87
+ catch (_) {
88
+ continue;
89
+ }
90
+ if (!outer || typeof outer !== 'object') {
91
+ continue;
92
+ }
93
+ // Top-level CLI envelope fields (present on system/result and sometimes the
94
+ // wrapper) carry session/model metadata.
95
+ if (outer.session_id) {
96
+ sessionId = outer.session_id;
97
+ }
98
+ if (outer.type === 'system' && outer.model) {
99
+ model = outer.model;
100
+ }
101
+ // The final `result` event carries `total_cost_usd` (direct from the CLI)
102
+ // and an aggregate usage object (fallback when partial events were evicted).
103
+ if (outer.type === 'result') {
104
+ if (outer.usage && typeof outer.usage === 'object') {
105
+ resultUsage = {
106
+ inputTokens: num(outer.usage.input_tokens),
107
+ outputTokens: num(outer.usage.output_tokens),
108
+ cacheReadTokens: num(outer.usage.cache_read_input_tokens),
109
+ cacheCreationTokens: num(outer.usage.cache_creation_input_tokens),
110
+ };
111
+ }
112
+ if (typeof outer.total_cost_usd === 'number' && Number.isFinite(outer.total_cost_usd)) {
113
+ resultCostUsd = outer.total_cost_usd;
114
+ }
115
+ }
116
+ const evt = unwrapEvent(outer);
117
+ if (!evt || typeof evt !== 'object') {
118
+ continue;
119
+ }
120
+ switch (evt.type) {
121
+ case 'message_start': {
122
+ const message = evt.message || {};
123
+ const usage = message.usage || {};
124
+ if (message.model) {
125
+ model = message.model;
126
+ }
127
+ messageStarts.push({
128
+ inputTokens: num(usage.input_tokens),
129
+ outputTokens: num(usage.output_tokens),
130
+ cacheReadTokens: num(usage.cache_read_input_tokens),
131
+ cacheCreationTokens: num(usage.cache_creation_input_tokens),
132
+ model: message.model || null,
133
+ });
134
+ break;
135
+ }
136
+ case 'message_delta': {
137
+ const usage = evt.usage || {};
138
+ messageDeltas.push({ outputTokens: num(usage.output_tokens) });
139
+ break;
140
+ }
141
+ case 'content_block_start': {
142
+ const block = evt.content_block || {};
143
+ if (block.type === 'tool_use') {
144
+ toolCalls += 1;
145
+ }
146
+ break;
147
+ }
148
+ default:
149
+ break;
150
+ }
113
151
  }
114
-
115
- const evt = unwrapEvent(outer);
116
- if (!evt || typeof evt !== 'object') {continue;}
117
-
118
- switch (evt.type) {
119
- case 'message_start': {
120
- const message = evt.message || {};
121
- const usage = message.usage || {};
122
- if (message.model) {model = message.model;}
123
- messageStarts.push({
124
- inputTokens: num(usage.input_tokens),
125
- outputTokens: num(usage.output_tokens),
126
- cacheReadTokens: num(usage.cache_read_input_tokens),
127
- cacheCreationTokens: num(usage.cache_creation_input_tokens),
128
- model: message.model || null,
129
- });
130
- break;
131
- }
132
- case 'message_delta': {
133
- const usage = evt.usage || {};
134
- messageDeltas.push({ outputTokens: num(usage.output_tokens) });
135
- break;
136
- }
137
- case 'content_block_start': {
138
- const block = evt.content_block || {};
139
- if (block.type === 'tool_use') {toolCalls += 1;}
140
- break;
141
- }
142
- default:
143
- break;
152
+ const hasSignal = messageStarts.length > 0 || messageDeltas.length > 0 || resultUsage || model || sessionId;
153
+ if (!hasSignal) {
154
+ return null;
144
155
  }
145
- }
146
-
147
- const hasSignal = messageStarts.length > 0 || messageDeltas.length > 0 || resultUsage || model || sessionId;
148
- if (!hasSignal) {return null;}
149
-
150
- return {
151
- sessionId,
152
- model: model || null,
153
- provider: PROVIDER,
154
- messageStarts,
155
- messageDeltas,
156
- toolCalls,
157
- resultUsage,
158
- resultCostUsd,
159
- };
156
+ return {
157
+ sessionId,
158
+ model: model || null,
159
+ provider: PROVIDER,
160
+ messageStarts,
161
+ messageDeltas,
162
+ toolCalls,
163
+ resultUsage,
164
+ resultCostUsd,
165
+ };
160
166
  }
161
-
162
167
  /**
163
168
  * Aggregate token counts from a Claude `stream-json` stdout stream into a
164
169
  * telemetry object compatible with `stats.telemetryToStatsFields()`.
@@ -177,61 +182,51 @@ function parseClaudeStreamJson(content) {
177
182
  *
178
183
  * Returns null when the stream yields no usable token signal.
179
184
  */
180
- /** @param {string} stdout */
181
185
  function extractClaudeTelemetryFromStdout(stdout) {
182
- const parsed = parseClaudeStreamJson(stdout);
183
- if (!parsed) {return null;}
184
-
185
- const { messageStarts, messageDeltas, resultUsage, resultCostUsd } = parsed;
186
-
187
- let inputTokens = messageStarts.length > 0 ? messageStarts[0].inputTokens : 0;
188
- let outputTokens = messageDeltas.reduce((sum, d) => sum + d.outputTokens, 0);
189
- let cachedTokens = messageStarts.reduce((sum, s) => sum + s.cacheReadTokens, 0);
190
- let cacheCreationTokens = messageStarts.reduce((sum, s) => sum + s.cacheCreationTokens, 0);
191
-
192
- // If partial events were truncated out of the captured tail, fall back to the
193
- // final `result` event's aggregate usage so we still record real numbers.
194
- if (inputTokens === 0 && outputTokens === 0 && resultUsage) {
195
- inputTokens = resultUsage.inputTokens;
196
- outputTokens = resultUsage.outputTokens;
197
- cachedTokens = resultUsage.cacheReadTokens;
198
- cacheCreationTokens = resultUsage.cacheCreationTokens;
199
- }
200
-
201
- // NOTE: a small `inputTokens` (even 1) alongside a large `cachedTokens` is NOT
202
- // an artifact — it is normal prompt caching. Anthropic streaming reports only
203
- // the *uncached* prompt delta as `message_start.usage.input_tokens`; the bulk
204
- // of the re-sent context is billed under `cache_read_input_tokens`. We keep
205
- // the parsed value truthful and surface caching honestly in the stats render
206
- // rather than rewriting telemetry here (see lib/commands/stats.js render).
207
-
208
- // No token signal at all (only metadata events): not worth a telemetry row.
209
- if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && cacheCreationTokens === 0) {
210
- return null;
211
- }
212
-
213
- return {
214
- sessionId: parsed.sessionId,
215
- provider: parsed.provider,
216
- model: parsed.model,
217
- effort: null,
218
- inputTokens,
219
- outputTokens,
220
- cachedTokens,
221
- cacheCreationTokens,
222
- reasoningTokens: 0,
223
- totalTokens: inputTokens + outputTokens,
224
- contextWindow: 0,
225
- toolCalls: parsed.toolCalls,
226
- // Claude's CLI exposes no rate-limit percentage (unlike Codex); leave null
227
- // so telemetryToStatsFields records openai_usage_after as 0.
228
- usagePercent: null,
229
- // total_cost_usd is emitted directly by the Claude CLI in the result event.
230
- cost_usd: typeof resultCostUsd === 'number' ? resultCostUsd : 0,
231
- };
186
+ const parsed = parseClaudeStreamJson(stdout);
187
+ if (!parsed) {
188
+ return null;
189
+ }
190
+ const { messageStarts, messageDeltas, resultUsage, resultCostUsd } = parsed;
191
+ let inputTokens = messageStarts.length > 0 ? messageStarts[0].inputTokens : 0;
192
+ let outputTokens = messageDeltas.reduce((sum, d) => sum + d.outputTokens, 0);
193
+ let cachedTokens = messageStarts.reduce((sum, s) => sum + s.cacheReadTokens, 0);
194
+ let cacheCreationTokens = messageStarts.reduce((sum, s) => sum + s.cacheCreationTokens, 0);
195
+ // If partial events were truncated out of the captured tail, fall back to the
196
+ // final `result` event's aggregate usage so we still record real numbers.
197
+ if (inputTokens === 0 && outputTokens === 0 && resultUsage) {
198
+ inputTokens = resultUsage.inputTokens;
199
+ outputTokens = resultUsage.outputTokens;
200
+ cachedTokens = resultUsage.cacheReadTokens;
201
+ cacheCreationTokens = resultUsage.cacheCreationTokens;
202
+ }
203
+ // NOTE: a small `inputTokens` (even 1) alongside a large `cachedTokens` is NOT
204
+ // an artifact — it is normal prompt caching. Anthropic streaming reports only
205
+ // the *uncached* prompt delta as `message_start.usage.input_tokens`; the bulk
206
+ // of the re-sent context is billed under `cache_read_input_tokens`. We keep
207
+ // the parsed value truthful and surface caching honestly in the stats render
208
+ // rather than rewriting telemetry here (see lib/commands/stats.js render).
209
+ // No token signal at all (only metadata events): not worth a telemetry row.
210
+ if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && cacheCreationTokens === 0) {
211
+ return null;
212
+ }
213
+ return {
214
+ sessionId: parsed.sessionId,
215
+ provider: parsed.provider,
216
+ model: parsed.model,
217
+ effort: null,
218
+ inputTokens,
219
+ outputTokens,
220
+ cachedTokens,
221
+ cacheCreationTokens,
222
+ reasoningTokens: 0,
223
+ totalTokens: inputTokens + outputTokens,
224
+ contextWindow: 0,
225
+ toolCalls: parsed.toolCalls,
226
+ // Claude's CLI exposes no rate-limit percentage (unlike Codex); leave null
227
+ // so telemetryToStatsFields records openai_usage_after as 0.
228
+ usagePercent: null,
229
+ // total_cost_usd is emitted directly by the Claude CLI in the result event.
230
+ cost_usd: typeof resultCostUsd === 'number' ? resultCostUsd : 0,
231
+ };
232
232
  }
233
-
234
- module.exports = {
235
- parseClaudeStreamJson,
236
- extractClaudeTelemetryFromStdout,
237
- };
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ // Claude telemetry is read from the Claude CLI's `--output-format stream-json`
4
+ // stdout stream (see claude.js `buildClaudeInvocation`). Unlike Codex — which
5
+ // writes a per-session rollout JSONL to disk — the Claude CLI emits its usage
6
+ // signal inline on stdout as SSE-style JSONL events, so we parse the captured
7
+ // stdout tail directly (no on-disk transcript to read).
8
+ //
9
+ // The CLI runs with `--include-partial-messages`, so the Anthropic SSE events
10
+ // are forwarded as JSONL. Two envelope shapes are handled:
11
+ //
12
+ // 1. Wrapped (real CLI form): each partial event is nested under a
13
+ // `stream_event` wrapper:
14
+ // {"type":"stream_event","event":{"type":"message_start","message":{"usage":{...}}}}
15
+ // {"type":"stream_event","event":{"type":"message_delta","usage":{"output_tokens":N}}}
16
+ // {"type":"stream_event","event":{"type":"content_block_start","content_block":{"type":"tool_use"}}}
17
+ // 2. Raw (bare SSE form): the Anthropic event sits at the top level:
18
+ // {"type":"message_start","message":{"usage":{...}}}
19
+ // {"type":"message_delta","usage":{"output_tokens":N}}
20
+ //
21
+ // The CLI also emits top-level `system` (init), `assistant`, and `result`
22
+ // events; `system`/`result` carry `session_id` and `model`, and `result`
23
+ // carries a final aggregate `usage` used as a fallback when partial events were
24
+ // truncated out of the captured stdout tail.
25
+ //
26
+ // Token semantics (Anthropic streaming):
27
+ // - `message_start.usage.input_tokens` is the prompt size for that turn. A
28
+ // multi-turn (tool-use) invocation re-sends the growing context each turn,
29
+ // so summing every turn's input would multi-count the shared prompt. We take
30
+ // the FIRST message_start's input_tokens as the representative prompt size.
31
+ // - `message_delta.usage.output_tokens` is the FINAL cumulative output count
32
+ // for that one message. Across turns these are independent generations, so
33
+ // the stage's total generated output is the SUM of each turn's final value.
34
+ // - `cache_read_input_tokens` / `cache_creation_input_tokens` appear on
35
+ // message_start.usage when prompt caching is active; summed across turns.
36
+
37
+ const PROVIDER = 'anthropic';
38
+
39
+ function num(value: any) {
40
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
41
+ }
42
+
43
+ /**
44
+ * Unwrap a parsed JSONL line into its underlying Anthropic SSE event. The CLI's
45
+ * `--include-partial-messages` form nests the real event under `stream_event`;
46
+ * the bare form has it at the top level. Returns the inner event object.
47
+ */
48
+ function unwrapEvent(evt: any) {
49
+ if (evt && evt.type === 'stream_event' && evt.event && typeof evt.event === 'object') {
50
+ return evt.event;
51
+ }
52
+ return evt;
53
+ }
54
+
55
+ /**
56
+ * Parse a Claude `stream-json` stdout string into a normalized structure of the
57
+ * usage-bearing events. Returns null when the content yields no usable signal
58
+ * (e.g. empty, garbage, or a stream with no usage events at all).
59
+ *
60
+ * Shape:
61
+ * {
62
+ * sessionId, model, provider: 'anthropic',
63
+ * messageStarts: [{ inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens, model }],
64
+ * messageDeltas: [{ outputTokens }],
65
+ * toolCalls,
66
+ * resultUsage: { inputTokens, outputTokens, cacheReadTokens, cacheCreationTokens } | null
67
+ * }
68
+ */
69
+ function parseClaudeStreamJson(content: string) {
70
+ if (!content) {return null;}
71
+
72
+ let sessionId = null;
73
+ let model = null;
74
+ let toolCalls = 0;
75
+ let resultUsage = null;
76
+ let resultCostUsd = null;
77
+ const messageStarts = [];
78
+ const messageDeltas = [];
79
+
80
+ for (const line of String(content).split('\n')) {
81
+ const trimmed = line.trim();
82
+ if (!trimmed.startsWith('{')) {continue;}
83
+ let outer;
84
+ try {
85
+ outer = JSON.parse(trimmed);
86
+ } catch (_) {
87
+ continue;
88
+ }
89
+ if (!outer || typeof outer !== 'object') {continue;}
90
+
91
+ // Top-level CLI envelope fields (present on system/result and sometimes the
92
+ // wrapper) carry session/model metadata.
93
+ if (outer.session_id) {sessionId = outer.session_id;}
94
+ if (outer.type === 'system' && outer.model) {model = outer.model;}
95
+
96
+ // The final `result` event carries `total_cost_usd` (direct from the CLI)
97
+ // and an aggregate usage object (fallback when partial events were evicted).
98
+ if (outer.type === 'result') {
99
+ if (outer.usage && typeof outer.usage === 'object') {
100
+ resultUsage = {
101
+ inputTokens: num(outer.usage.input_tokens),
102
+ outputTokens: num(outer.usage.output_tokens),
103
+ cacheReadTokens: num(outer.usage.cache_read_input_tokens),
104
+ cacheCreationTokens: num(outer.usage.cache_creation_input_tokens),
105
+ };
106
+ }
107
+ if (typeof outer.total_cost_usd === 'number' && Number.isFinite(outer.total_cost_usd)) {
108
+ resultCostUsd = outer.total_cost_usd;
109
+ }
110
+ }
111
+
112
+ const evt = unwrapEvent(outer);
113
+ if (!evt || typeof evt !== 'object') {continue;}
114
+
115
+ switch (evt.type) {
116
+ case 'message_start': {
117
+ const message = evt.message || {};
118
+ const usage = message.usage || {};
119
+ if (message.model) {model = message.model;}
120
+ messageStarts.push({
121
+ inputTokens: num(usage.input_tokens),
122
+ outputTokens: num(usage.output_tokens),
123
+ cacheReadTokens: num(usage.cache_read_input_tokens),
124
+ cacheCreationTokens: num(usage.cache_creation_input_tokens),
125
+ model: message.model || null,
126
+ });
127
+ break;
128
+ }
129
+ case 'message_delta': {
130
+ const usage = evt.usage || {};
131
+ messageDeltas.push({ outputTokens: num(usage.output_tokens) });
132
+ break;
133
+ }
134
+ case 'content_block_start': {
135
+ const block = evt.content_block || {};
136
+ if (block.type === 'tool_use') {toolCalls += 1;}
137
+ break;
138
+ }
139
+ default:
140
+ break;
141
+ }
142
+ }
143
+
144
+ const hasSignal = messageStarts.length > 0 || messageDeltas.length > 0 || resultUsage || model || sessionId;
145
+ if (!hasSignal) {return null;}
146
+
147
+ return {
148
+ sessionId,
149
+ model: model || null,
150
+ provider: PROVIDER,
151
+ messageStarts,
152
+ messageDeltas,
153
+ toolCalls,
154
+ resultUsage,
155
+ resultCostUsd,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Aggregate token counts from a Claude `stream-json` stdout stream into a
161
+ * telemetry object compatible with `stats.telemetryToStatsFields()`.
162
+ *
163
+ * Aggregation rules (see token-semantics note at top of file):
164
+ * - inputTokens = first message_start's input_tokens (representative prompt
165
+ * size; avoids multi-counting the re-sent context across
166
+ * tool-use turns).
167
+ * - outputTokens = sum of every message_delta's output_tokens (total
168
+ * generation across all turns).
169
+ * - cachedTokens = sum of message_start cache_read_input_tokens across turns.
170
+ * - totalTokens = inputTokens + outputTokens.
171
+ *
172
+ * When partial events are absent (e.g. truncated stdout tail) but a `result`
173
+ * event survived, its aggregate usage is used as a fallback.
174
+ *
175
+ * Returns null when the stream yields no usable token signal.
176
+ */
177
+ function extractClaudeTelemetryFromStdout(stdout: string) {
178
+ const parsed = parseClaudeStreamJson(stdout);
179
+ if (!parsed) {return null;}
180
+
181
+ const { messageStarts, messageDeltas, resultUsage, resultCostUsd } = parsed;
182
+
183
+ let inputTokens = messageStarts.length > 0 ? messageStarts[0].inputTokens : 0;
184
+ let outputTokens = messageDeltas.reduce((sum, d) => sum + d.outputTokens, 0);
185
+ let cachedTokens = messageStarts.reduce((sum, s) => sum + s.cacheReadTokens, 0);
186
+ let cacheCreationTokens = messageStarts.reduce((sum, s) => sum + s.cacheCreationTokens, 0);
187
+
188
+ // If partial events were truncated out of the captured tail, fall back to the
189
+ // final `result` event's aggregate usage so we still record real numbers.
190
+ if (inputTokens === 0 && outputTokens === 0 && resultUsage) {
191
+ inputTokens = resultUsage.inputTokens;
192
+ outputTokens = resultUsage.outputTokens;
193
+ cachedTokens = resultUsage.cacheReadTokens;
194
+ cacheCreationTokens = resultUsage.cacheCreationTokens;
195
+ }
196
+
197
+ // NOTE: a small `inputTokens` (even 1) alongside a large `cachedTokens` is NOT
198
+ // an artifact — it is normal prompt caching. Anthropic streaming reports only
199
+ // the *uncached* prompt delta as `message_start.usage.input_tokens`; the bulk
200
+ // of the re-sent context is billed under `cache_read_input_tokens`. We keep
201
+ // the parsed value truthful and surface caching honestly in the stats render
202
+ // rather than rewriting telemetry here (see lib/commands/stats.js render).
203
+
204
+ // No token signal at all (only metadata events): not worth a telemetry row.
205
+ if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0 && cacheCreationTokens === 0) {
206
+ return null;
207
+ }
208
+
209
+ return {
210
+ sessionId: parsed.sessionId,
211
+ provider: parsed.provider,
212
+ model: parsed.model,
213
+ effort: null,
214
+ inputTokens,
215
+ outputTokens,
216
+ cachedTokens,
217
+ cacheCreationTokens,
218
+ reasoningTokens: 0,
219
+ totalTokens: inputTokens + outputTokens,
220
+ contextWindow: 0,
221
+ toolCalls: parsed.toolCalls,
222
+ // Claude's CLI exposes no rate-limit percentage (unlike Codex); leave null
223
+ // so telemetryToStatsFields records openai_usage_after as 0.
224
+ usagePercent: null,
225
+ // total_cost_usd is emitted directly by the Claude CLI in the result event.
226
+ cost_usd: typeof resultCostUsd === 'number' ? resultCostUsd : 0,
227
+ };
228
+ }
229
+
230
+ export {
231
+ parseClaudeStreamJson,
232
+ extractClaudeTelemetryFromStdout,
233
+ };