@magnusekdahl/parallix 1.2.0 → 1.3.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 (127) hide show
  1. package/index.js +190 -185
  2. package/lib/agents/agents.js +737 -838
  3. package/lib/agents/agents.ts +930 -0
  4. package/lib/agents/claude-telemetry.js +149 -154
  5. package/lib/agents/claude-telemetry.ts +233 -0
  6. package/lib/agents/claude.js +121 -131
  7. package/lib/agents/claude.ts +156 -0
  8. package/lib/agents/codex-telemetry.js +176 -164
  9. package/lib/agents/codex-telemetry.ts +205 -0
  10. package/lib/agents/codex.js +168 -201
  11. package/lib/agents/codex.ts +236 -0
  12. package/lib/agents/limit-hit.js +216 -226
  13. package/lib/agents/limit-hit.ts +262 -0
  14. package/lib/agents/mistral-telemetry.js +7 -10
  15. package/lib/agents/mistral-telemetry.ts +44 -0
  16. package/lib/agents/mistral.js +40 -58
  17. package/lib/agents/mistral.ts +84 -0
  18. package/lib/agents/opencode-export.js +126 -127
  19. package/lib/agents/opencode-export.ts +160 -0
  20. package/lib/agents/opencode-telemetry.js +282 -261
  21. package/lib/agents/opencode-telemetry.ts +350 -0
  22. package/lib/agents/opencode.js +263 -305
  23. package/lib/agents/opencode.ts +370 -0
  24. package/lib/agents/stage-telemetry.js +23 -24
  25. package/lib/agents/stage-telemetry.ts +47 -0
  26. package/lib/commands/active.js +502 -579
  27. package/lib/commands/active.ts +665 -0
  28. package/lib/commands/checkpoint.js +98 -74
  29. package/lib/commands/checkpoint.ts +79 -0
  30. package/lib/commands/config.js +61 -31
  31. package/lib/commands/config.ts +47 -0
  32. package/lib/commands/coverage-gate.js +271 -246
  33. package/lib/commands/coverage-gate.ts +362 -0
  34. package/lib/commands/diff.js +140 -119
  35. package/lib/commands/diff.ts +125 -0
  36. package/lib/commands/draft.js +770 -923
  37. package/lib/commands/draft.ts +1021 -0
  38. package/lib/commands/handoff.js +588 -608
  39. package/lib/commands/handoff.ts +694 -0
  40. package/lib/commands/integrate.js +1332 -1463
  41. package/lib/commands/integrate.ts +1688 -0
  42. package/lib/commands/mission-start.js +279 -237
  43. package/lib/commands/mission-start.ts +263 -0
  44. package/lib/commands/rebase.js +606 -582
  45. package/lib/commands/rebase.ts +635 -0
  46. package/lib/commands/repair-handoff.js +181 -179
  47. package/lib/commands/repair-handoff.ts +228 -0
  48. package/lib/commands/resolve-conflict.js +121 -102
  49. package/lib/commands/resolve-conflict.ts +112 -0
  50. package/lib/commands/review.js +3 -4
  51. package/lib/commands/review.ts +14 -0
  52. package/lib/commands/setup-review.js +4 -5
  53. package/lib/commands/setup-review.ts +14 -0
  54. package/lib/commands/setup.js +3 -3
  55. package/lib/commands/setup.ts +2 -0
  56. package/lib/commands/stats-backfill.js +394 -359
  57. package/lib/commands/stats-backfill.ts +418 -0
  58. package/lib/commands/stats.js +1424 -1665
  59. package/lib/commands/stats.ts +2186 -0
  60. package/lib/commands/status.js +219 -200
  61. package/lib/commands/status.ts +219 -0
  62. package/lib/commands/verify.js +3 -1
  63. package/lib/commands/verify.ts +2 -0
  64. package/lib/core/fmt.js +140 -211
  65. package/lib/core/fmt.ts +189 -0
  66. package/lib/core/git.js +121 -102
  67. package/lib/core/git.ts +146 -0
  68. package/lib/core/gitignore.js +59 -106
  69. package/lib/core/gitignore.ts +103 -0
  70. package/lib/core/mission-utils.js +833 -875
  71. package/lib/core/mission-utils.ts +998 -0
  72. package/lib/core/nels.js +112 -127
  73. package/lib/core/nels.ts +189 -0
  74. package/lib/core/persistent-data-migration.js +247 -205
  75. package/lib/core/persistent-data-migration.ts +255 -0
  76. package/lib/core/product-config.js +400 -476
  77. package/lib/core/product-config.ts +518 -0
  78. package/lib/core/runtime-matrix.js +41 -87
  79. package/lib/core/runtime-matrix.ts +84 -0
  80. package/lib/core/spawn-tee.js +152 -192
  81. package/lib/core/spawn-tee.ts +202 -0
  82. package/lib/core/state-map.js +92 -92
  83. package/lib/core/state-map.ts +115 -0
  84. package/lib/core/storage.js +101 -129
  85. package/lib/core/storage.ts +182 -0
  86. package/lib/core/subagent-limit.js +15 -16
  87. package/lib/core/subagent-limit.ts +25 -0
  88. package/lib/core/verification.js +148 -138
  89. package/lib/core/verification.ts +200 -0
  90. package/lib/index.js +142 -59
  91. package/lib/index.ts +134 -0
  92. package/lib/review/rebase.js +142 -145
  93. package/lib/review/rebase.ts +188 -0
  94. package/lib/review/review-adapter.js +99 -87
  95. package/lib/review/review-adapter.ts +198 -0
  96. package/lib/review/review-artifacts.js +486 -629
  97. package/lib/review/review-artifacts.ts +622 -0
  98. package/lib/review/review-commands.js +1220 -1373
  99. package/lib/review/review-commands.ts +1579 -0
  100. package/lib/review/review-events.js +544 -886
  101. package/lib/review/review-events.ts +954 -0
  102. package/lib/review/review-loop.js +945 -1020
  103. package/lib/review/review-loop.ts +1174 -0
  104. package/lib/review/review-polling.js +86 -115
  105. package/lib/review/review-polling.ts +194 -0
  106. package/lib/review/review-prompts.js +154 -189
  107. package/lib/review/review-prompts.ts +182 -0
  108. package/lib/review/review-state.js +246 -225
  109. package/lib/review/review-state.ts +353 -0
  110. package/lib/review/review.js +63 -84
  111. package/lib/review/review.ts +138 -0
  112. package/lib/tools/backlog.js +694 -680
  113. package/lib/tools/backlog.ts +835 -0
  114. package/lib/tools/forgejo.js +1270 -1405
  115. package/lib/tools/forgejo.ts +1793 -0
  116. package/lib/tools/gatekeeper.js +106 -96
  117. package/lib/tools/gatekeeper.ts +124 -0
  118. package/lib/tools/redgreen.js +200 -181
  119. package/lib/tools/redgreen.ts +221 -0
  120. package/lib/tools/sessions.js +58 -52
  121. package/lib/tools/sessions.ts +81 -0
  122. package/lib/tools/setup-review.js +902 -975
  123. package/lib/tools/setup-review.ts +1152 -0
  124. package/package.json +7 -5
  125. package/prompts/draft.md +1 -0
  126. package/prompts/portfolio.md +1 -1
  127. package/px.js +226 -210
@@ -1,5 +1,9 @@
1
1
  'use strict';
2
-
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractOpencodeTelemetryFromExport = extractOpencodeTelemetryFromExport;
4
+ exports.parseOpencodeExport = extractOpencodeTelemetryFromExport;
5
+ exports.extractOpencodeTelemetry = extractOpencodeTelemetry;
6
+ exports.getOpencodeProviderModel = getOpencodeProviderModel;
3
7
  /**
4
8
  * Opencode Telemetry Parser
5
9
  *
@@ -15,228 +19,260 @@
15
19
  *
16
20
  * See task-1285 for the full telemetry credibility design.
17
21
  */
18
-
19
22
  const PROVIDER = 'opencode';
20
23
  const MODEL = 'custom';
21
-
22
- /** @param {*} value */
23
24
  function num(value) {
24
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
25
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
25
26
  }
26
-
27
27
  /**
28
28
  * @param {{[key: string]: any}} obj
29
29
  * @returns {{input_tokens?: number, output_tokens?: number, cached_input_tokens?: number, total_tokens?: number}|null}
30
30
  */
31
31
  function findTokenUsage(obj) {
32
- if (!obj || typeof obj !== 'object') {return null;}
33
-
34
- // Direct token usage fields at top level
35
- if ('input_tokens' in obj || 'output_tokens' in obj) {
36
- return {
37
- input_tokens: /** @type {number} */(obj.input_tokens),
38
- output_tokens: /** @type {number} */(obj.output_tokens),
39
- cached_input_tokens: /** @type {number} */(obj.cached_input_tokens || obj.cached_tokens || 0),
40
- total_tokens: /** @type {number} */(obj.total_tokens || 0),
41
- };
42
- }
43
-
44
- // Nested under total_token_usage (Codex-compatible shape)
45
- if (obj.total_token_usage && typeof obj.total_token_usage === 'object') {
46
- return {
47
- input_tokens: /** @type {number} */(obj.total_token_usage.input_tokens || 0),
48
- output_tokens: /** @type {number} */(obj.total_token_usage.output_tokens || 0),
49
- cached_input_tokens: /** @type {number} */(obj.total_token_usage.cached_input_tokens || obj.total_token_usage.cached_tokens || 0),
50
- total_tokens: /** @type {number} */(obj.total_token_usage.total_tokens || 0),
51
- };
52
- }
53
-
54
- // Nested under token_usage
55
- if (obj.token_usage && typeof obj.token_usage === 'object') {
56
- return {
57
- input_tokens: /** @type {number} */(obj.token_usage.input_tokens || 0),
58
- output_tokens: /** @type {number} */(obj.token_usage.output_tokens || 0),
59
- cached_input_tokens: /** @type {number} */(obj.token_usage.cached_input_tokens || obj.token_usage.cached_tokens || 0),
60
- total_tokens: /** @type {number} */(obj.token_usage.total_tokens || 0),
61
- };
62
- }
63
-
64
- // Nested under usage
65
- if (obj.usage && typeof obj.usage === 'object') {
66
- return {
67
- input_tokens: /** @type {number} */(obj.usage.input_tokens || 0),
68
- output_tokens: /** @type {number} */(obj.usage.output_tokens || 0),
69
- cached_input_tokens: /** @type {number} */(obj.usage.cached_input_tokens || obj.usage.cached_tokens || 0),
70
- total_tokens: /** @type {number} */(obj.usage.total_tokens || 0),
71
- };
72
- }
73
-
74
- // Nested under meta or metadata
75
- const metaOrMeta = obj.meta || obj.metadata;
76
- if (metaOrMeta && typeof metaOrMeta === 'object') {
77
- if ('input_tokens' in metaOrMeta || 'output_tokens' in metaOrMeta) {
78
- return {
79
- input_tokens: /** @type {number} */(metaOrMeta.input_tokens || 0),
80
- output_tokens: /** @type {number} */(metaOrMeta.output_tokens || 0),
81
- cached_input_tokens: /** @type {number} */(metaOrMeta.cached_input_tokens || metaOrMeta.cached_tokens || 0),
82
- total_tokens: /** @type {number} */(metaOrMeta.total_tokens || 0),
83
- };
84
- }
85
- if (metaOrMeta.total_token_usage && typeof metaOrMeta.total_token_usage === 'object') {
86
- return {
87
- input_tokens: /** @type {number} */(metaOrMeta.total_token_usage.input_tokens || 0),
88
- output_tokens: /** @type {number} */(metaOrMeta.total_token_usage.output_tokens || 0),
89
- cached_input_tokens: /** @type {number} */(metaOrMeta.total_token_usage.cached_input_tokens || metaOrMeta.total_token_usage.cached_tokens || 0),
90
- total_tokens: /** @type {number} */(metaOrMeta.total_token_usage.total_tokens || 0),
91
- };
92
- }
93
- }
94
-
95
- // Nested under info (opencode v2.x export format)
96
- // Shape: { info: { tokens: { input, output, cache: { read, write }, reasoning } } }
97
- if (obj.info && typeof obj.info === 'object' && obj.info.tokens && typeof obj.info.tokens === 'object') {
98
- const t = obj.info.tokens;
99
- if ('input' in t || 'output' in t) {
100
- return {
101
- input_tokens: /** @type {number} */(t.input || 0),
102
- output_tokens: /** @type {number} */(t.output || 0),
103
- cached_input_tokens: (t.cache && typeof t.cache === 'object') ? /** @type {number} */(t.cache.read || 0) : 0,
104
- total_tokens: /** @type {number} */(t.input + t.output || 0),
105
- };
106
- }
107
- }
108
-
109
- // Search recursively through array elements (e.g., events array)
110
- for (const key of Object.keys(obj)) {
111
- const val = obj[key];
112
- if (Array.isArray(val)) {
113
- for (const item of val) {
114
- if (item && typeof item === 'object') {
115
- const found = findTokenUsage(/** @type {{[key: string]: any}} */(item));
116
- if (found) {return found;}
32
+ if (!obj || typeof obj !== 'object') {
33
+ return null;
34
+ }
35
+ // Direct token usage fields at top level
36
+ if ('input_tokens' in obj || 'output_tokens' in obj) {
37
+ return {
38
+ input_tokens: /** @type {number} */ (obj.input_tokens),
39
+ output_tokens: /** @type {number} */ (obj.output_tokens),
40
+ cached_input_tokens: /** @type {number} */ (obj.cached_input_tokens || obj.cached_tokens || 0),
41
+ total_tokens: /** @type {number} */ (obj.total_tokens || 0),
42
+ };
43
+ }
44
+ // Nested under total_token_usage (Codex-compatible shape)
45
+ if (obj.total_token_usage && typeof obj.total_token_usage === 'object') {
46
+ return {
47
+ input_tokens: /** @type {number} */ (obj.total_token_usage.input_tokens || 0),
48
+ output_tokens: /** @type {number} */ (obj.total_token_usage.output_tokens || 0),
49
+ cached_input_tokens: /** @type {number} */ (obj.total_token_usage.cached_input_tokens || obj.total_token_usage.cached_tokens || 0),
50
+ total_tokens: /** @type {number} */ (obj.total_token_usage.total_tokens || 0),
51
+ };
52
+ }
53
+ // Nested under token_usage
54
+ if (obj.token_usage && typeof obj.token_usage === 'object') {
55
+ return {
56
+ input_tokens: /** @type {number} */ (obj.token_usage.input_tokens || 0),
57
+ output_tokens: /** @type {number} */ (obj.token_usage.output_tokens || 0),
58
+ cached_input_tokens: /** @type {number} */ (obj.token_usage.cached_input_tokens || obj.token_usage.cached_tokens || 0),
59
+ total_tokens: /** @type {number} */ (obj.token_usage.total_tokens || 0),
60
+ };
61
+ }
62
+ // Nested under usage
63
+ if (obj.usage && typeof obj.usage === 'object') {
64
+ return {
65
+ input_tokens: /** @type {number} */ (obj.usage.input_tokens || 0),
66
+ output_tokens: /** @type {number} */ (obj.usage.output_tokens || 0),
67
+ cached_input_tokens: /** @type {number} */ (obj.usage.cached_input_tokens || obj.usage.cached_tokens || 0),
68
+ total_tokens: /** @type {number} */ (obj.usage.total_tokens || 0),
69
+ };
70
+ }
71
+ // Nested under meta or metadata
72
+ const metaOrMeta = obj.meta || obj.metadata;
73
+ if (metaOrMeta && typeof metaOrMeta === 'object') {
74
+ if ('input_tokens' in metaOrMeta || 'output_tokens' in metaOrMeta) {
75
+ return {
76
+ input_tokens: /** @type {number} */ (metaOrMeta.input_tokens || 0),
77
+ output_tokens: /** @type {number} */ (metaOrMeta.output_tokens || 0),
78
+ cached_input_tokens: /** @type {number} */ (metaOrMeta.cached_input_tokens || metaOrMeta.cached_tokens || 0),
79
+ total_tokens: /** @type {number} */ (metaOrMeta.total_tokens || 0),
80
+ };
81
+ }
82
+ if (metaOrMeta.total_token_usage && typeof metaOrMeta.total_token_usage === 'object') {
83
+ return {
84
+ input_tokens: /** @type {number} */ (metaOrMeta.total_token_usage.input_tokens || 0),
85
+ output_tokens: /** @type {number} */ (metaOrMeta.total_token_usage.output_tokens || 0),
86
+ cached_input_tokens: /** @type {number} */ (metaOrMeta.total_token_usage.cached_input_tokens || metaOrMeta.total_token_usage.cached_tokens || 0),
87
+ total_tokens: /** @type {number} */ (metaOrMeta.total_token_usage.total_tokens || 0),
88
+ };
89
+ }
90
+ }
91
+ // Nested under info (opencode v2.x export format)
92
+ // Shape: { info: { tokens: { input, output, cache: { read, write }, reasoning } } }
93
+ if (obj.info && typeof obj.info === 'object' && obj.info.tokens && typeof obj.info.tokens === 'object') {
94
+ const t = obj.info.tokens;
95
+ if ('input' in t || 'output' in t) {
96
+ return {
97
+ input_tokens: /** @type {number} */ (t.input || 0),
98
+ output_tokens: /** @type {number} */ (t.output || 0),
99
+ cached_input_tokens: (t.cache && typeof t.cache === 'object') ? /** @type {number} */ (t.cache.read || 0) : 0,
100
+ total_tokens: /** @type {number} */ (t.input + t.output || 0),
101
+ };
102
+ }
103
+ }
104
+ // Search recursively through array elements (e.g., events array)
105
+ for (const key of Object.keys(obj)) {
106
+ const val = obj[key];
107
+ if (Array.isArray(val)) {
108
+ for (const item of val) {
109
+ if (item && typeof item === 'object') {
110
+ const found = findTokenUsage(/** @type {{[key: string]: any}} */ (item));
111
+ if (found) {
112
+ return found;
113
+ }
114
+ }
115
+ }
117
116
  }
118
- }
119
117
  }
120
- }
121
-
122
- return null;
118
+ return null;
123
119
  }
124
-
125
120
  /**
126
121
  * @param {{[key: string]: any}} parsed
127
122
  * @returns {string|null}
128
123
  */
129
124
  function extractSessionId(parsed) {
130
- if (!parsed || typeof parsed !== 'object') {return null;}
131
-
132
- // Direct session_id
133
- if (parsed.session_id) {return String(parsed.session_id);}
134
- if (parsed.sessionId) {return String(parsed.sessionId);}
135
-
136
- // Nested under info (opencode v2.x export format)
137
- if (parsed.info && typeof parsed.info === 'object') {
138
- if (parsed.info.id) {return String(parsed.info.id);}
139
- }
140
-
141
- // Nested in metadata/meta/session
142
- if (parsed.metadata && typeof parsed.metadata === 'object') {
143
- if (parsed.metadata.session_id) {return String(parsed.metadata.session_id);}
144
- if (parsed.metadata.sessionId) {return String(parsed.metadata.sessionId);}
145
- }
146
- if (parsed.meta && typeof parsed.meta === 'object') {
147
- if (parsed.meta.session_id) {return String(parsed.meta.session_id);}
148
- if (parsed.meta.sessionId) {return String(parsed.meta.sessionId);}
149
- }
150
- if (parsed.session && typeof parsed.session === 'object') {
151
- if (parsed.session.id) {return String(parsed.session.id);}
152
- if (parsed.session.session_id) {return String(parsed.session.session_id);}
153
- }
154
-
155
- return null;
125
+ if (!parsed || typeof parsed !== 'object') {
126
+ return null;
127
+ }
128
+ // Direct session_id
129
+ if (parsed.session_id) {
130
+ return String(parsed.session_id);
131
+ }
132
+ if (parsed.sessionId) {
133
+ return String(parsed.sessionId);
134
+ }
135
+ // Nested under info (opencode v2.x export format)
136
+ if (parsed.info && typeof parsed.info === 'object') {
137
+ if (parsed.info.id) {
138
+ return String(parsed.info.id);
139
+ }
140
+ }
141
+ // Nested in metadata/meta/session
142
+ if (parsed.metadata && typeof parsed.metadata === 'object') {
143
+ if (parsed.metadata.session_id) {
144
+ return String(parsed.metadata.session_id);
145
+ }
146
+ if (parsed.metadata.sessionId) {
147
+ return String(parsed.metadata.sessionId);
148
+ }
149
+ }
150
+ if (parsed.meta && typeof parsed.meta === 'object') {
151
+ if (parsed.meta.session_id) {
152
+ return String(parsed.meta.session_id);
153
+ }
154
+ if (parsed.meta.sessionId) {
155
+ return String(parsed.meta.sessionId);
156
+ }
157
+ }
158
+ if (parsed.session && typeof parsed.session === 'object') {
159
+ if (parsed.session.id) {
160
+ return String(parsed.session.id);
161
+ }
162
+ if (parsed.session.session_id) {
163
+ return String(parsed.session.session_id);
164
+ }
165
+ }
166
+ return null;
156
167
  }
157
-
158
168
  /**
159
169
  * @param {{[key: string]: any}} parsed
160
170
  * @returns {string|null}
161
171
  */
162
172
  function extractModelName(parsed) {
163
- if (!parsed || typeof parsed !== 'object') {return null;}
164
-
165
- // Direct model fields
166
- if (parsed.model) {return String(parsed.model);}
167
- if (parsed.model_name) {return String(parsed.model_name);}
168
-
169
- // Nested under info.model (opencode v2.x format)
170
- if (parsed.info && typeof parsed.info === 'object' && parsed.info.model && typeof parsed.info.model === 'object') {
171
- if (parsed.info.model.id) {return String(parsed.info.model.id);}
172
- if (parsed.info.model.name) {return String(parsed.info.model.name);}
173
- }
174
-
175
- // Nested in metadata/meta/session
176
- if (parsed.metadata && typeof parsed.metadata === 'object') {
177
- if (parsed.metadata.model) {return String(parsed.metadata.model);}
178
- if (parsed.metadata.model_name) {return String(parsed.metadata.model_name);}
179
- }
180
- if (parsed.meta && typeof parsed.meta === 'object') {
181
- if (parsed.meta.model) {return String(parsed.meta.model);}
182
- }
183
- if (parsed.session && typeof parsed.session === 'object') {
184
- if (parsed.session.model) {return String(parsed.session.model);}
185
- }
186
-
187
- return null;
173
+ if (!parsed || typeof parsed !== 'object') {
174
+ return null;
175
+ }
176
+ // Direct model fields
177
+ if (parsed.model) {
178
+ return String(parsed.model);
179
+ }
180
+ if (parsed.model_name) {
181
+ return String(parsed.model_name);
182
+ }
183
+ // Nested under info.model (opencode v2.x format)
184
+ if (parsed.info && typeof parsed.info === 'object' && parsed.info.model && typeof parsed.info.model === 'object') {
185
+ if (parsed.info.model.id) {
186
+ return String(parsed.info.model.id);
187
+ }
188
+ if (parsed.info.model.name) {
189
+ return String(parsed.info.model.name);
190
+ }
191
+ }
192
+ // Nested in metadata/meta/session
193
+ if (parsed.metadata && typeof parsed.metadata === 'object') {
194
+ if (parsed.metadata.model) {
195
+ return String(parsed.metadata.model);
196
+ }
197
+ if (parsed.metadata.model_name) {
198
+ return String(parsed.metadata.model_name);
199
+ }
200
+ }
201
+ if (parsed.meta && typeof parsed.meta === 'object') {
202
+ if (parsed.meta.model) {
203
+ return String(parsed.meta.model);
204
+ }
205
+ }
206
+ if (parsed.session && typeof parsed.session === 'object') {
207
+ if (parsed.session.model) {
208
+ return String(parsed.session.model);
209
+ }
210
+ }
211
+ return null;
188
212
  }
189
-
190
213
  /**
191
214
  * @param {{[key: string]: any}} parsed
192
215
  * @returns {number}
193
216
  */
194
217
  function countToolCalls(parsed) {
195
- if (!parsed || typeof parsed !== 'object') {return 0;}
196
-
197
- let count = 0;
198
-
199
- // Direct tool_calls field
200
- if (typeof parsed.tool_calls === 'number') {count += parsed.tool_calls;}
201
- if (typeof parsed.toolCalls === 'number') {count += parsed.toolCalls;}
202
- if (typeof parsed.usage?.tool_calls === 'number') {count += parsed.usage.tool_calls;}
203
-
204
- // Real opencode v2.x export schema: tool calls live in
205
- // messages[].parts[] where part.type === 'tool'.
206
- if (Array.isArray(parsed.messages)) {
207
- for (const msg of parsed.messages) {
208
- if (msg && typeof msg === 'object' && Array.isArray(msg.parts)) {
209
- for (const part of msg.parts) {
210
- if (part && typeof part === 'object' && part.type === 'tool') {count += 1;}
218
+ if (!parsed || typeof parsed !== 'object') {
219
+ return 0;
220
+ }
221
+ let count = 0;
222
+ // Direct tool_calls field
223
+ if (typeof parsed.tool_calls === 'number') {
224
+ count += parsed.tool_calls;
225
+ }
226
+ if (typeof parsed.toolCalls === 'number') {
227
+ count += parsed.toolCalls;
228
+ }
229
+ if (typeof parsed.usage?.tool_calls === 'number') {
230
+ count += parsed.usage.tool_calls;
231
+ }
232
+ // Real opencode v2.x export schema: tool calls live in
233
+ // messages[].parts[] where part.type === 'tool'.
234
+ if (Array.isArray(parsed.messages)) {
235
+ for (const msg of parsed.messages) {
236
+ if (msg && typeof msg === 'object' && Array.isArray(msg.parts)) {
237
+ for (const part of msg.parts) {
238
+ if (part && typeof part === 'object' && part.type === 'tool') {
239
+ count += 1;
240
+ }
241
+ }
242
+ }
211
243
  }
212
- }
213
- }
214
- }
215
-
216
- // Nested arrays of tool calls
217
- if (Array.isArray(parsed.events)) {
218
- for (const evt of parsed.events) {
219
- if (evt && typeof evt === 'object') {
220
- if (evt.type === 'tool_use') {count += 1;}
221
- if (evt.type === 'tool_call') {count += 1;}
222
- if (evt.type === 'function_call') {count += 1;}
223
- if (Array.isArray(evt.tool_calls)) {count += evt.tool_calls.length;}
224
- if (Array.isArray(evt.content_block_start)) {
225
- for (const cb of evt.content_block_start) {
226
- if (cb && cb.type === 'tool_use') {count += 1;}
227
- }
244
+ }
245
+ // Nested arrays of tool calls
246
+ if (Array.isArray(parsed.events)) {
247
+ for (const evt of parsed.events) {
248
+ if (evt && typeof evt === 'object') {
249
+ if (evt.type === 'tool_use') {
250
+ count += 1;
251
+ }
252
+ if (evt.type === 'tool_call') {
253
+ count += 1;
254
+ }
255
+ if (evt.type === 'function_call') {
256
+ count += 1;
257
+ }
258
+ if (Array.isArray(evt.tool_calls)) {
259
+ count += evt.tool_calls.length;
260
+ }
261
+ if (Array.isArray(evt.content_block_start)) {
262
+ for (const cb of evt.content_block_start) {
263
+ if (cb && cb.type === 'tool_use') {
264
+ count += 1;
265
+ }
266
+ }
267
+ }
268
+ }
228
269
  }
229
- }
230
- }
231
- }
232
-
233
- if (Array.isArray(parsed.tool_use_events)) {
234
- count += parsed.tool_use_events.length;
235
- }
236
-
237
- return count;
270
+ }
271
+ if (Array.isArray(parsed.tool_use_events)) {
272
+ count += parsed.tool_use_events.length;
273
+ }
274
+ return count;
238
275
  }
239
-
240
276
  /**
241
277
  * Parse an `opencode export` JSON string and return a normalized telemetry
242
278
  * object compatible with `stats.telemetryToStatsFields()`.
@@ -256,81 +292,73 @@ function countToolCalls(parsed) {
256
292
  * @returns {object|null} Normalized telemetry object or null
257
293
  */
258
294
  function extractOpencodeTelemetryFromExport(jsonString, fallbackModel) {
259
- if (!jsonString || typeof jsonString !== 'string' || !jsonString.trim()) {
260
- return null;
261
- }
262
-
263
- let parsed;
264
- try {
265
- parsed = JSON.parse(jsonString);
266
- } catch (_) {
267
- return null;
268
- }
269
-
270
- if (!parsed || typeof parsed !== 'object') {
271
- return null;
272
- }
273
-
274
- const tokenUsage = findTokenUsage(parsed);
275
- if (!tokenUsage) {
276
- return null;
277
- }
278
-
279
- // Verify there is at least some non-zero token data
280
- const inputTokens = num(tokenUsage.input_tokens);
281
- const outputTokens = num(tokenUsage.output_tokens);
282
- const cachedTokens = num(tokenUsage.cached_input_tokens);
283
-
284
- // If all tokens are zero, treat as no signal (unless total_tokens explicitly exists and is non-zero)
285
- if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0) {
286
- if (num(tokenUsage.total_tokens) === 0) {
287
- return null;
288
- }
289
- // total_tokens is set but individual fields are zero — treat total_tokens as the real signal
290
- // and derive input/output from it conservatively
291
- const totalTokens = num(tokenUsage.total_tokens);
292
- return {
295
+ if (!jsonString || typeof jsonString !== 'string' || !jsonString.trim()) {
296
+ return null;
297
+ }
298
+ let parsed;
299
+ try {
300
+ parsed = JSON.parse(jsonString);
301
+ }
302
+ catch (_) {
303
+ return null;
304
+ }
305
+ if (!parsed || typeof parsed !== 'object') {
306
+ return null;
307
+ }
308
+ const tokenUsage = findTokenUsage(parsed);
309
+ if (!tokenUsage) {
310
+ return null;
311
+ }
312
+ // Verify there is at least some non-zero token data
313
+ const inputTokens = num(tokenUsage.input_tokens);
314
+ const outputTokens = num(tokenUsage.output_tokens);
315
+ const cachedTokens = num(tokenUsage.cached_input_tokens);
316
+ // If all tokens are zero, treat as no signal (unless total_tokens explicitly exists and is non-zero)
317
+ if (inputTokens === 0 && outputTokens === 0 && cachedTokens === 0) {
318
+ if (num(tokenUsage.total_tokens) === 0) {
319
+ return null;
320
+ }
321
+ // total_tokens is set but individual fields are zero — treat total_tokens as the real signal
322
+ // and derive input/output from it conservatively
323
+ const totalTokens = num(tokenUsage.total_tokens);
324
+ return {
325
+ sessionId: extractSessionId(parsed),
326
+ provider: PROVIDER,
327
+ model: extractModelName(parsed) || fallbackModel || MODEL,
328
+ inputTokens: 0,
329
+ outputTokens: 0,
330
+ cachedTokens: 0,
331
+ totalTokens,
332
+ toolCalls: countToolCalls(parsed),
333
+ usagePercent: null,
334
+ };
335
+ }
336
+ const totalTokens = num(tokenUsage.total_tokens) || (inputTokens + outputTokens);
337
+ return {
293
338
  sessionId: extractSessionId(parsed),
294
339
  provider: PROVIDER,
295
340
  model: extractModelName(parsed) || fallbackModel || MODEL,
296
- inputTokens: 0,
297
- outputTokens: 0,
298
- cachedTokens: 0,
341
+ inputTokens,
342
+ outputTokens,
343
+ cachedTokens,
299
344
  totalTokens,
300
345
  toolCalls: countToolCalls(parsed),
301
346
  usagePercent: null,
302
- };
303
- }
304
-
305
- const totalTokens = num(tokenUsage.total_tokens) || (inputTokens + outputTokens);
306
-
307
- return {
308
- sessionId: extractSessionId(parsed),
309
- provider: PROVIDER,
310
- model: extractModelName(parsed) || fallbackModel || MODEL,
311
- inputTokens,
312
- outputTokens,
313
- cachedTokens,
314
- totalTokens,
315
- toolCalls: countToolCalls(parsed),
316
- usagePercent: null,
317
- };
347
+ };
318
348
  }
319
-
320
349
  /**
321
350
  * @param {{exportJson?: string, telemetry?: object}} result
322
351
  * @returns {object|null}
323
352
  */
324
353
  function extractOpencodeTelemetry(result) {
325
- if (result && result.exportJson && typeof result.exportJson === 'string') {
326
- return extractOpencodeTelemetryFromExport(result.exportJson);
327
- }
328
- if (result && result.telemetry) {
329
- return result.telemetry;
330
- }
331
- return null;
354
+ if (result && result.exportJson && typeof result.exportJson === 'string') {
355
+ return extractOpencodeTelemetryFromExport(result.exportJson);
356
+ }
357
+ if (result && result.telemetry) {
358
+ return result.telemetry;
359
+ }
360
+ return null;
332
361
  }
333
-
334
362
  /**
335
363
  * Return the provider/model pair for opencode tasks.
336
364
  * Used as fallback when telemetry is null.
@@ -340,12 +368,5 @@ function extractOpencodeTelemetry(result) {
340
368
  * @returns {{provider: string, model: string}}
341
369
  */
342
370
  function getOpencodeProviderModel(defaultModel) {
343
- return { provider: PROVIDER, model: defaultModel || MODEL };
371
+ return { provider: PROVIDER, model: defaultModel || MODEL };
344
372
  }
345
-
346
- module.exports = {
347
- extractOpencodeTelemetryFromExport,
348
- extractOpencodeTelemetry,
349
- getOpencodeProviderModel,
350
- parseOpencodeExport: extractOpencodeTelemetryFromExport,
351
- };