codex-devtools 0.1.11 → 0.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.
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  const fs = require("node:fs");
3
+ const node_crypto = require("node:crypto");
3
4
  const path = require("node:path");
4
5
  const node_buffer = require("node:buffer");
5
6
  const os = require("node:os");
@@ -34,6 +35,12 @@ const createLogger = (scope) => {
34
35
  debug: (message, ...args) => console.debug(prefix, message, ...args)
35
36
  };
36
37
  };
38
+ const DEFAULT_CODEX_DEVTOOLS_CONFIG_PATH = path__namespace.join(
39
+ os__namespace.homedir(),
40
+ ".config",
41
+ "codex-devtools",
42
+ "config.json"
43
+ );
37
44
  const DEFAULT_CONFIG = {
38
45
  general: {
39
46
  launchAtLogin: false,
@@ -66,7 +73,7 @@ function deepMerge(target, source) {
66
73
  return output;
67
74
  }
68
75
  class ConfigManager {
69
- constructor(configPath = path__namespace.join(os__namespace.homedir(), ".config", "codex-devtools", "config.json")) {
76
+ constructor(configPath = DEFAULT_CODEX_DEVTOOLS_CONFIG_PATH) {
70
77
  this.configPath = configPath;
71
78
  this.config = this.loadConfig();
72
79
  }
@@ -117,68 +124,68 @@ class ConfigManager {
117
124
  fs__namespace.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf8");
118
125
  }
119
126
  }
120
- function isRecord(value) {
127
+ function isRecord$2(value) {
121
128
  return typeof value === "object" && value !== null;
122
129
  }
123
- function isString(value) {
130
+ function isString$2(value) {
124
131
  return typeof value === "string";
125
132
  }
126
133
  function hasString(obj, key) {
127
- return isString(obj[key]);
134
+ return isString$2(obj[key]);
128
135
  }
129
136
  function hasOptionalString(obj, key) {
130
- return !(key in obj) || isString(obj[key]);
137
+ return !(key in obj) || isString$2(obj[key]);
131
138
  }
132
139
  function hasOptionalRecord(obj, key) {
133
- return !(key in obj) || isRecord(obj[key]);
140
+ return !(key in obj) || isRecord$2(obj[key]);
134
141
  }
135
142
  function getContentBlockText(block) {
136
143
  if (!("text" in block)) {
137
144
  return "";
138
145
  }
139
- return isString(block.text) ? block.text : "";
146
+ return isString$2(block.text) ? block.text : "";
140
147
  }
141
148
  function isReasoningSummaryText(value) {
142
- return isRecord(value) && hasString(value, "text");
149
+ return isRecord$2(value) && hasString(value, "text");
143
150
  }
144
151
  function reasoningSummaryItemToText(item) {
145
- return isString(item) ? item : item.text;
152
+ return isString$2(item) ? item : item.text;
146
153
  }
147
154
  function reasoningSummaryToText(summary) {
148
155
  return summary.map(reasoningSummaryItemToText);
149
156
  }
150
157
  function isContentBlockInputText(value) {
151
- if (!isRecord(value)) {
158
+ if (!isRecord$2(value)) {
152
159
  return false;
153
160
  }
154
161
  return value.type === "input_text" && hasString(value, "text");
155
162
  }
156
163
  function isContentBlockOutputText(value) {
157
- if (!isRecord(value)) {
164
+ if (!isRecord$2(value)) {
158
165
  return false;
159
166
  }
160
167
  return value.type === "output_text" && hasString(value, "text");
161
168
  }
162
169
  function isContentBlockInputImage(value) {
163
- if (!isRecord(value)) {
170
+ if (!isRecord$2(value)) {
164
171
  return false;
165
172
  }
166
173
  return value.type === "input_image" && hasString(value, "image_url");
167
174
  }
168
175
  function isContentBlockUnknown(value) {
169
- if (!isRecord(value) || !hasString(value, "type")) {
176
+ if (!isRecord$2(value) || !hasString(value, "type")) {
170
177
  return false;
171
178
  }
172
179
  if (value.type === "input_text" || value.type === "output_text" || value.type === "input_image") {
173
180
  return false;
174
181
  }
175
- return !("text" in value) || isString(value.text);
182
+ return !("text" in value) || isString$2(value.text);
176
183
  }
177
184
  function isContentBlock(value) {
178
185
  return isContentBlockInputText(value) || isContentBlockOutputText(value) || isContentBlockInputImage(value) || isContentBlockUnknown(value);
179
186
  }
180
187
  function isMessagePayload(value) {
181
- if (!isRecord(value)) {
188
+ if (!isRecord$2(value)) {
182
189
  return false;
183
190
  }
184
191
  if (value.type !== "message" || value.role !== "developer" && value.role !== "user" && value.role !== "assistant") {
@@ -190,70 +197,70 @@ function isMessagePayload(value) {
190
197
  return value.content.every(isContentBlock);
191
198
  }
192
199
  function isFunctionCallPayload(value) {
193
- if (!isRecord(value)) {
200
+ if (!isRecord$2(value)) {
194
201
  return false;
195
202
  }
196
203
  return value.type === "function_call" && hasString(value, "name") && hasString(value, "arguments") && hasString(value, "call_id");
197
204
  }
198
205
  function isFunctionCallOutputPayload(value) {
199
- if (!isRecord(value)) {
206
+ if (!isRecord$2(value)) {
200
207
  return false;
201
208
  }
202
209
  return value.type === "function_call_output" && hasString(value, "call_id") && hasString(value, "output");
203
210
  }
204
211
  function isReasoningPayload(value) {
205
- if (!isRecord(value)) {
212
+ if (!isRecord$2(value)) {
206
213
  return false;
207
214
  }
208
215
  if (value.type !== "reasoning" || !Array.isArray(value.summary)) {
209
216
  return false;
210
217
  }
211
- if (!value.summary.every((item) => isString(item) || isReasoningSummaryText(item))) {
218
+ if (!value.summary.every((item) => isString$2(item) || isReasoningSummaryText(item))) {
212
219
  return false;
213
220
  }
214
- return !("encrypted_content" in value) || isString(value.encrypted_content) || value.encrypted_content === null;
221
+ return !("encrypted_content" in value) || isString$2(value.encrypted_content) || value.encrypted_content === null;
215
222
  }
216
223
  function isResponseItemPayload(value) {
217
224
  return isMessagePayload(value) || isFunctionCallPayload(value) || isFunctionCallOutputPayload(value) || isReasoningPayload(value);
218
225
  }
219
226
  function isTokenUsage(value) {
220
- if (!isRecord(value)) {
227
+ if (!isRecord$2(value)) {
221
228
  return false;
222
229
  }
223
230
  return typeof value.input_tokens === "number" && typeof value.cached_input_tokens === "number" && typeof value.output_tokens === "number" && typeof value.reasoning_output_tokens === "number" && typeof value.total_tokens === "number";
224
231
  }
225
232
  function isTokenCountPayload(value) {
226
- if (!isRecord(value) || value.type !== "token_count") {
233
+ if (!isRecord$2(value) || value.type !== "token_count") {
227
234
  return false;
228
235
  }
229
236
  if (value.info === null) {
230
237
  return true;
231
238
  }
232
- if (!isRecord(value.info)) {
239
+ if (!isRecord$2(value.info)) {
233
240
  return false;
234
241
  }
235
242
  return isTokenUsage(value.info.total_token_usage) && isTokenUsage(value.info.last_token_usage) && typeof value.info.model_context_window === "number";
236
243
  }
237
244
  function isAgentReasoningPayload(value) {
238
- if (!isRecord(value)) {
245
+ if (!isRecord$2(value)) {
239
246
  return false;
240
247
  }
241
248
  return value.type === "agent_reasoning" && hasString(value, "text");
242
249
  }
243
250
  function isAgentMessagePayload(value) {
244
- if (!isRecord(value)) {
251
+ if (!isRecord$2(value)) {
245
252
  return false;
246
253
  }
247
254
  return value.type === "agent_message" && hasString(value, "message");
248
255
  }
249
256
  function isUserMessagePayload(value) {
250
- if (!isRecord(value)) {
257
+ if (!isRecord$2(value)) {
251
258
  return false;
252
259
  }
253
260
  return value.type === "user_message" && hasString(value, "message");
254
261
  }
255
262
  function isContextCompactedPayload(value) {
256
- if (!isRecord(value)) {
263
+ if (!isRecord$2(value)) {
257
264
  return false;
258
265
  }
259
266
  return value.type === "context_compacted";
@@ -262,10 +269,10 @@ function isEventMsgPayload(value) {
262
269
  return isTokenCountPayload(value) || isAgentReasoningPayload(value) || isAgentMessagePayload(value) || isUserMessagePayload(value) || isContextCompactedPayload(value);
263
270
  }
264
271
  function isSessionMetaEntry(value) {
265
- if (!isRecord(value) || value.type !== "session_meta" || !hasString(value, "timestamp")) {
272
+ if (!isRecord$2(value) || value.type !== "session_meta" || !hasString(value, "timestamp")) {
266
273
  return false;
267
274
  }
268
- if (!isRecord(value.payload)) {
275
+ if (!isRecord$2(value.payload)) {
269
276
  return false;
270
277
  }
271
278
  if (!hasOptionalString(value.payload, "id") || !hasOptionalString(value.payload, "cwd") || !hasOptionalString(value.payload, "originator") || !hasOptionalString(value.payload, "cli_version") || !hasOptionalString(value.payload, "model_provider") || !hasOptionalString(value.payload, "model")) {
@@ -273,14 +280,14 @@ function isSessionMetaEntry(value) {
273
280
  }
274
281
  if ("base_instructions" in value.payload) {
275
282
  const baseInstructions = value.payload.base_instructions;
276
- if (!isString(baseInstructions) && !isRecord(baseInstructions)) {
283
+ if (!isString$2(baseInstructions) && !isRecord$2(baseInstructions)) {
277
284
  return false;
278
285
  }
279
286
  }
280
287
  if (!hasOptionalRecord(value.payload, "git")) {
281
288
  return false;
282
289
  }
283
- if (isRecord(value.payload.git)) {
290
+ if (isRecord$2(value.payload.git)) {
284
291
  if (!hasOptionalString(value.payload.git, "commit_hash") || !hasOptionalString(value.payload.git, "branch") || !hasOptionalString(value.payload.git, "repository_url")) {
285
292
  return false;
286
293
  }
@@ -288,55 +295,55 @@ function isSessionMetaEntry(value) {
288
295
  return hasString(value.payload, "id") || hasString(value.payload, "cwd");
289
296
  }
290
297
  function isResponseItemEntry(value) {
291
- if (!isRecord(value) || value.type !== "response_item" || !hasString(value, "timestamp")) {
298
+ if (!isRecord$2(value) || value.type !== "response_item" || !hasString(value, "timestamp")) {
292
299
  return false;
293
300
  }
294
301
  return isResponseItemPayload(value.payload);
295
302
  }
296
303
  function isTurnContextEntry(value) {
297
- if (!isRecord(value) || value.type !== "turn_context" || !hasString(value, "timestamp")) {
304
+ if (!isRecord$2(value) || value.type !== "turn_context" || !hasString(value, "timestamp")) {
298
305
  return false;
299
306
  }
300
- if (!isRecord(value.payload)) {
307
+ if (!isRecord$2(value.payload)) {
301
308
  return false;
302
309
  }
303
310
  if (!hasOptionalString(value.payload, "turn_id") || !hasOptionalString(value.payload, "cwd") || !hasOptionalString(value.payload, "approval_policy") || !hasOptionalString(value.payload, "model") || !hasOptionalString(value.payload, "personality") || !hasOptionalString(value.payload, "effort") || !hasOptionalString(value.payload, "summary") || !hasOptionalString(value.payload, "user_instructions")) {
304
311
  return false;
305
312
  }
306
313
  const sandboxPolicy = value.payload.sandbox_policy;
307
- if ("sandbox_policy" in value.payload && !(isString(sandboxPolicy) || isRecord(sandboxPolicy))) {
314
+ if ("sandbox_policy" in value.payload && !(isString$2(sandboxPolicy) || isRecord$2(sandboxPolicy))) {
308
315
  return false;
309
316
  }
310
317
  const collaborationMode = value.payload.collaboration_mode;
311
- if ("collaboration_mode" in value.payload && !(isString(collaborationMode) || isRecord(collaborationMode))) {
318
+ if ("collaboration_mode" in value.payload && !(isString$2(collaborationMode) || isRecord$2(collaborationMode))) {
312
319
  return false;
313
320
  }
314
321
  const truncationPolicy = value.payload.truncation_policy;
315
- if ("truncation_policy" in value.payload && !(isString(truncationPolicy) || isRecord(truncationPolicy))) {
322
+ if ("truncation_policy" in value.payload && !(isString$2(truncationPolicy) || isRecord$2(truncationPolicy))) {
316
323
  return false;
317
324
  }
318
325
  return hasString(value.payload, "cwd") || hasString(value.payload, "model");
319
326
  }
320
327
  function isEventMsgEntry(value) {
321
- if (!isRecord(value) || value.type !== "event_msg" || !hasString(value, "timestamp")) {
328
+ if (!isRecord$2(value) || value.type !== "event_msg" || !hasString(value, "timestamp")) {
322
329
  return false;
323
330
  }
324
331
  return isEventMsgPayload(value.payload);
325
332
  }
326
333
  function isCompactedEntry(value) {
327
- if (!isRecord(value) || value.type !== "compacted" || !hasString(value, "timestamp")) {
334
+ if (!isRecord$2(value) || value.type !== "compacted" || !hasString(value, "timestamp")) {
328
335
  return false;
329
336
  }
330
- return isRecord(value.payload);
337
+ return isRecord$2(value.payload);
331
338
  }
332
339
  function isCompactionEntry(value) {
333
- if (!isRecord(value) || value.type !== "compaction" || !hasString(value, "timestamp")) {
340
+ if (!isRecord$2(value) || value.type !== "compaction" || !hasString(value, "timestamp")) {
334
341
  return false;
335
342
  }
336
- if ("payload" in value && !isRecord(value.payload)) {
343
+ if ("payload" in value && !isRecord$2(value.payload)) {
337
344
  return false;
338
345
  }
339
- if ("encrypted_content" in value && !(isString(value.encrypted_content) || value.encrypted_content === null)) {
346
+ if ("encrypted_content" in value && !(isString$2(value.encrypted_content) || value.encrypted_content === null)) {
340
347
  return false;
341
348
  }
342
349
  return true;
@@ -354,6 +361,40 @@ const EMPTY_CODEX_SESSION_METRICS = {
354
361
  toolCallCount: 0,
355
362
  duration: 0
356
363
  };
364
+ function diffTokenUsage(previous, current) {
365
+ const delta = {
366
+ input_tokens: current.input_tokens - previous.input_tokens,
367
+ cached_input_tokens: current.cached_input_tokens - previous.cached_input_tokens,
368
+ output_tokens: current.output_tokens - previous.output_tokens,
369
+ reasoning_output_tokens: current.reasoning_output_tokens - previous.reasoning_output_tokens,
370
+ total_tokens: current.total_tokens - previous.total_tokens
371
+ };
372
+ const normalized = {
373
+ input_tokens: Math.max(delta.input_tokens, 0),
374
+ cached_input_tokens: Math.max(delta.cached_input_tokens, 0),
375
+ output_tokens: Math.max(delta.output_tokens, 0),
376
+ reasoning_output_tokens: Math.max(delta.reasoning_output_tokens, 0),
377
+ total_tokens: Math.max(delta.total_tokens, 0)
378
+ };
379
+ const hasAnyPositive = normalized.input_tokens > 0 || normalized.cached_input_tokens > 0 || normalized.output_tokens > 0 || normalized.reasoning_output_tokens > 0 || normalized.total_tokens > 0;
380
+ return hasAnyPositive ? normalized : null;
381
+ }
382
+ function isSameTokenUsage(left, right) {
383
+ return left.input_tokens === right.input_tokens && left.cached_input_tokens === right.cached_input_tokens && left.output_tokens === right.output_tokens && left.reasoning_output_tokens === right.reasoning_output_tokens && left.total_tokens === right.total_tokens;
384
+ }
385
+ function resolveTokenUsage(previousTotalUsage, currentTotalUsage, fallbackUsage) {
386
+ if (!previousTotalUsage) {
387
+ return fallbackUsage;
388
+ }
389
+ const delta = diffTokenUsage(previousTotalUsage, currentTotalUsage);
390
+ if (delta) {
391
+ return delta;
392
+ }
393
+ if (isSameTokenUsage(previousTotalUsage, currentTotalUsage)) {
394
+ return null;
395
+ }
396
+ return fallbackUsage;
397
+ }
357
398
  const AGENTS_HEADING_PATTERN = /^#?\s*AGENTS\.md instructions\b/i;
358
399
  const AGENTS_INSTRUCTIONS_BLOCK_PATTERN = /<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>/i;
359
400
  const ENVIRONMENT_CONTEXT_WRAPPER_PATTERN = /^<environment_context>[\s\S]*<\/environment_context>$/i;
@@ -737,10 +778,10 @@ function pickPreferredEquivalentUserContent(pending, incoming) {
737
778
  }
738
779
  return pending;
739
780
  }
740
- function normalizeModel$2(model) {
781
+ function normalizeModel$3(model) {
741
782
  return model?.trim() ?? "";
742
783
  }
743
- function normalizeReasoningEffort$2(effort) {
784
+ function normalizeReasoningEffort$3(effort) {
744
785
  const value = effort?.trim();
745
786
  return value ? value : "unknown";
746
787
  }
@@ -997,6 +1038,7 @@ class CodexChunkBuilder {
997
1038
  let pendingUser = null;
998
1039
  let lastSeenModelUsage = null;
999
1040
  let lastSeenCollaborationMode = "";
1041
+ let previousTotalUsage = null;
1000
1042
  const flushAIChunk = () => {
1001
1043
  if (!currentAI) {
1002
1044
  return;
@@ -1077,11 +1119,11 @@ class CodexChunkBuilder {
1077
1119
  continue;
1078
1120
  }
1079
1121
  if (isTurnContextEntry(entry)) {
1080
- const model = normalizeModel$2(entry.payload.model);
1122
+ const model = normalizeModel$3(entry.payload.model);
1081
1123
  if (model) {
1082
1124
  const usage = {
1083
1125
  model,
1084
- reasoningEffort: normalizeReasoningEffort$2(entry.payload.effort)
1126
+ reasoningEffort: normalizeReasoningEffort$3(entry.payload.effort)
1085
1127
  };
1086
1128
  if (lastSeenModelUsage === null) {
1087
1129
  lastSeenModelUsage = usage;
@@ -1251,30 +1293,33 @@ class CodexChunkBuilder {
1251
1293
  addReasoningSection(ai, "event", [entry.payload.text]);
1252
1294
  continue;
1253
1295
  }
1254
- if (isEventMsgEntry(entry) && isTokenCountPayload(entry.payload)) {
1255
- this.accumulateMetricsFromTokenEvent(ai.metrics, entry);
1256
- this.assignTokenUsageToPendingTool(ai, entry);
1296
+ if (isEventMsgEntry(entry) && isTokenCountPayload(entry.payload) && entry.payload.info) {
1297
+ const currentTotalUsage = entry.payload.info.total_token_usage;
1298
+ const usage = resolveTokenUsage(
1299
+ previousTotalUsage,
1300
+ currentTotalUsage,
1301
+ entry.payload.info.last_token_usage
1302
+ );
1303
+ previousTotalUsage = currentTotalUsage;
1304
+ if (!usage) {
1305
+ continue;
1306
+ }
1307
+ this.accumulateMetricsFromTokenUsage(ai.metrics, usage);
1308
+ this.assignTokenUsageToPendingTool(ai, usage);
1257
1309
  }
1258
1310
  }
1259
1311
  flushPendingEventUser();
1260
1312
  flushAIChunk();
1261
1313
  return chunks;
1262
1314
  }
1263
- accumulateMetricsFromTokenEvent(target, entry) {
1264
- if (!isTokenCountPayload(entry.payload) || !entry.payload.info) {
1265
- return;
1266
- }
1267
- const usage = entry.payload.info.last_token_usage;
1315
+ accumulateMetricsFromTokenUsage(target, usage) {
1268
1316
  target.inputTokens = (target.inputTokens ?? 0) + usage.input_tokens;
1269
1317
  target.cachedTokens = (target.cachedTokens ?? 0) + usage.cached_input_tokens;
1270
1318
  target.outputTokens = (target.outputTokens ?? 0) + usage.output_tokens;
1271
1319
  target.reasoningTokens = (target.reasoningTokens ?? 0) + usage.reasoning_output_tokens;
1272
1320
  target.totalTokens = (target.totalTokens ?? 0) + usage.total_tokens;
1273
1321
  }
1274
- assignTokenUsageToPendingTool(ai, entry) {
1275
- if (!isTokenCountPayload(entry.payload) || !entry.payload.info) {
1276
- return;
1277
- }
1322
+ assignTokenUsageToPendingTool(ai, usage) {
1278
1323
  if (ai.pendingUsageToolIndex === null) {
1279
1324
  return;
1280
1325
  }
@@ -1283,20 +1328,399 @@ class CodexChunkBuilder {
1283
1328
  ai.pendingUsageToolIndex = null;
1284
1329
  return;
1285
1330
  }
1286
- const usage = entry.payload.info.last_token_usage;
1287
1331
  if (tool.tokenUsage) {
1288
1332
  tool.tokenUsage.inputTokens += usage.input_tokens;
1333
+ tool.tokenUsage.cachedInputTokens += usage.cached_input_tokens;
1289
1334
  tool.tokenUsage.outputTokens += usage.output_tokens;
1290
1335
  } else {
1291
1336
  tool.tokenUsage = {
1292
1337
  inputTokens: usage.input_tokens,
1338
+ cachedInputTokens: usage.cached_input_tokens,
1293
1339
  outputTokens: usage.output_tokens
1294
1340
  };
1295
1341
  }
1296
1342
  ai.pendingUsageToolIndex = null;
1297
1343
  }
1298
1344
  }
1299
- const logger$2 = createLogger("Main:jsonl");
1345
+ function normalizeRateKey(model) {
1346
+ return model.trim().toLowerCase();
1347
+ }
1348
+ function createZeroTokenTotals() {
1349
+ return {
1350
+ totalTokens: 0,
1351
+ inputTokens: 0,
1352
+ outputTokens: 0,
1353
+ cachedTokens: 0,
1354
+ reasoningTokens: 0
1355
+ };
1356
+ }
1357
+ function addTokenTotals(target, source) {
1358
+ target.totalTokens += source.totalTokens;
1359
+ target.inputTokens += source.inputTokens;
1360
+ target.outputTokens += source.outputTokens;
1361
+ target.cachedTokens += source.cachedTokens;
1362
+ target.reasoningTokens += source.reasoningTokens;
1363
+ }
1364
+ function tokenUsageToTotals(usage) {
1365
+ return {
1366
+ totalTokens: usage.total_tokens,
1367
+ inputTokens: usage.input_tokens,
1368
+ outputTokens: usage.output_tokens,
1369
+ cachedTokens: usage.cached_input_tokens,
1370
+ reasoningTokens: usage.reasoning_output_tokens
1371
+ };
1372
+ }
1373
+ function parseTimestamp(timestamp) {
1374
+ const value = new Date(timestamp);
1375
+ return Number.isNaN(value.getTime()) ? null : value;
1376
+ }
1377
+ function localDateKey(date) {
1378
+ const year = date.getFullYear();
1379
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1380
+ const day = String(date.getDate()).padStart(2, "0");
1381
+ return `${year}-${month}-${day}`;
1382
+ }
1383
+ function localHour(date) {
1384
+ return date.getHours();
1385
+ }
1386
+ function ensureDailyBucket(buckets, date) {
1387
+ const existing = buckets.get(date);
1388
+ if (existing) {
1389
+ return existing;
1390
+ }
1391
+ const next = {
1392
+ date,
1393
+ eventCount: 0,
1394
+ ...createZeroTokenTotals()
1395
+ };
1396
+ buckets.set(date, next);
1397
+ return next;
1398
+ }
1399
+ function ensureHourlyBucket(buckets, hour) {
1400
+ const existing = buckets.get(hour);
1401
+ if (existing) {
1402
+ return existing;
1403
+ }
1404
+ const next = {
1405
+ hour,
1406
+ eventCount: 0,
1407
+ ...createZeroTokenTotals()
1408
+ };
1409
+ buckets.set(hour, next);
1410
+ return next;
1411
+ }
1412
+ function ensureModelBucket(buckets, model, reasoningEffort) {
1413
+ const key = `${normalizeRateKey(model)}::${normalizeReasoningEffort$2(reasoningEffort)}`;
1414
+ const existing = buckets.get(key);
1415
+ if (existing) {
1416
+ return existing;
1417
+ }
1418
+ const next = {
1419
+ model,
1420
+ reasoningEffort,
1421
+ ...createZeroTokenTotals()
1422
+ };
1423
+ buckets.set(key, next);
1424
+ return next;
1425
+ }
1426
+ function estimateUsageCostUsd(tokens, rate) {
1427
+ const nonReasoningOutput = Math.max(tokens.outputTokens - tokens.reasoningTokens, 0);
1428
+ const outputCost = nonReasoningOutput / 1e6 * rate.outputUsdPer1M;
1429
+ const reasoningCost = tokens.reasoningTokens / 1e6 * rate.reasoningOutputUsdPer1M;
1430
+ return tokens.inputTokens / 1e6 * rate.inputUsdPer1M + tokens.cachedTokens / 1e6 * rate.cachedInputUsdPer1M + outputCost + reasoningCost;
1431
+ }
1432
+ function buildRateLookup(rateCard) {
1433
+ const lookup = /* @__PURE__ */ new Map();
1434
+ for (const rate of rateCard.models) {
1435
+ lookup.set(normalizeRateKey(rate.model), rate);
1436
+ }
1437
+ return lookup;
1438
+ }
1439
+ function buildHourlyResult(hourly) {
1440
+ const result = [];
1441
+ for (let hour = 0; hour <= 23; hour += 1) {
1442
+ const current = hourly.get(hour);
1443
+ if (!current) {
1444
+ result.push({
1445
+ hour,
1446
+ eventCount: 0,
1447
+ sessionCount: 0,
1448
+ ...createZeroTokenTotals()
1449
+ });
1450
+ continue;
1451
+ }
1452
+ result.push({
1453
+ hour,
1454
+ eventCount: current.eventCount,
1455
+ sessionCount: current.sessions.size,
1456
+ totalTokens: current.totalTokens,
1457
+ inputTokens: current.inputTokens,
1458
+ outputTokens: current.outputTokens,
1459
+ cachedTokens: current.cachedTokens,
1460
+ reasoningTokens: current.reasoningTokens
1461
+ });
1462
+ }
1463
+ return result;
1464
+ }
1465
+ function normalizeModel$2(model) {
1466
+ const trimmed = model?.trim();
1467
+ return trimmed && trimmed.length > 0 ? trimmed : "unknown-model";
1468
+ }
1469
+ function normalizeReasoningEffort$2(reasoningEffort) {
1470
+ const trimmed = reasoningEffort?.trim();
1471
+ return trimmed && trimmed.length > 0 ? trimmed : "unknown";
1472
+ }
1473
+ function buildSessionStatsRecord(parsed, revision, nowIso) {
1474
+ const dailyBuckets = /* @__PURE__ */ new Map();
1475
+ const hourlyBuckets = /* @__PURE__ */ new Map();
1476
+ const modelBuckets = /* @__PURE__ */ new Map();
1477
+ const tokens = createZeroTokenTotals();
1478
+ let eventCount = 0;
1479
+ let currentModel = normalizeModel$2(parsed.session.model || parsed.sessionMeta?.payload.model);
1480
+ let currentReasoningEffort = "unknown";
1481
+ let previousTotalUsage = null;
1482
+ for (const modelUsage of parsed.session.modelUsages) {
1483
+ if (modelUsage.model) {
1484
+ currentModel = normalizeModel$2(modelUsage.model);
1485
+ currentReasoningEffort = normalizeReasoningEffort$2(modelUsage.reasoningEffort);
1486
+ break;
1487
+ }
1488
+ }
1489
+ for (const entry of parsed.entries) {
1490
+ if (isTurnContextEntry(entry)) {
1491
+ if (entry.payload.model) {
1492
+ currentModel = normalizeModel$2(entry.payload.model);
1493
+ }
1494
+ currentReasoningEffort = normalizeReasoningEffort$2(entry.payload.effort);
1495
+ continue;
1496
+ }
1497
+ if (!isEventMsgEntry(entry) || !isTokenCountPayload(entry.payload) || !entry.payload.info) {
1498
+ continue;
1499
+ }
1500
+ const timestamp = parseTimestamp(entry.timestamp);
1501
+ const currentTotal = entry.payload.info.total_token_usage;
1502
+ const usage = resolveTokenUsage(previousTotalUsage, currentTotal, entry.payload.info.last_token_usage);
1503
+ previousTotalUsage = currentTotal;
1504
+ if (!usage) {
1505
+ continue;
1506
+ }
1507
+ const usageTotals = tokenUsageToTotals(usage);
1508
+ addTokenTotals(tokens, usageTotals);
1509
+ const modelTotals = ensureModelBucket(modelBuckets, currentModel, currentReasoningEffort);
1510
+ addTokenTotals(modelTotals, usageTotals);
1511
+ if (!timestamp) {
1512
+ continue;
1513
+ }
1514
+ const dateBucket = ensureDailyBucket(dailyBuckets, localDateKey(timestamp));
1515
+ addTokenTotals(dateBucket, usageTotals);
1516
+ const hourBucket = ensureHourlyBucket(hourlyBuckets, localHour(timestamp));
1517
+ addTokenTotals(hourBucket, usageTotals);
1518
+ }
1519
+ for (const classifiedEntry of parsed.classifiedEntries) {
1520
+ const timestamp = parseTimestamp(classifiedEntry.entry.timestamp);
1521
+ if (!timestamp) {
1522
+ continue;
1523
+ }
1524
+ eventCount += 1;
1525
+ const dateBucket = ensureDailyBucket(dailyBuckets, localDateKey(timestamp));
1526
+ dateBucket.eventCount += 1;
1527
+ const hourBucket = ensureHourlyBucket(hourlyBuckets, localHour(timestamp));
1528
+ hourBucket.eventCount += 1;
1529
+ }
1530
+ const lastActivity = parsed.entries[parsed.entries.length - 1]?.timestamp ?? parsed.session.startTime;
1531
+ return {
1532
+ tokenComputationVersion: 4,
1533
+ sessionId: parsed.session.id,
1534
+ filePath: parsed.session.filePath,
1535
+ revision,
1536
+ archived: false,
1537
+ cwd: parsed.session.cwd,
1538
+ startTime: parsed.session.startTime,
1539
+ lastActivity,
1540
+ modelUsages: parsed.session.modelUsages,
1541
+ eventCount,
1542
+ turnCount: parsed.metrics.turnCount,
1543
+ toolCallCount: parsed.metrics.toolCallCount,
1544
+ durationMs: parsed.metrics.duration,
1545
+ tokens,
1546
+ modelTokenTotals: Array.from(modelBuckets.values()).sort((a, b) => b.totalTokens - a.totalTokens),
1547
+ dailyBuckets: Array.from(dailyBuckets.values()).sort((a, b) => a.date.localeCompare(b.date)),
1548
+ hourlyBuckets: Array.from(hourlyBuckets.values()).sort((a, b) => a.hour - b.hour),
1549
+ lastSeenAt: nowIso
1550
+ };
1551
+ }
1552
+ function aggregateStatsSummary(records, scope, rateCard) {
1553
+ const scopedRecords = records.filter((record) => {
1554
+ if (scope.type === "all") {
1555
+ return true;
1556
+ }
1557
+ return record.cwd === scope.cwd;
1558
+ });
1559
+ const daily = /* @__PURE__ */ new Map();
1560
+ const hourly = /* @__PURE__ */ new Map();
1561
+ const modelBreakdown = /* @__PURE__ */ new Map();
1562
+ const rateLookup = buildRateLookup(rateCard);
1563
+ const unpricedModels = /* @__PURE__ */ new Set();
1564
+ const totals = {
1565
+ sessions: 0,
1566
+ archivedSessions: 0,
1567
+ eventCount: 0,
1568
+ durationMs: 0,
1569
+ estimatedCostUsd: 0,
1570
+ ...createZeroTokenTotals()
1571
+ };
1572
+ const costCoverage = {
1573
+ pricedTokens: 0,
1574
+ unpricedTokens: 0
1575
+ };
1576
+ for (const record of scopedRecords) {
1577
+ totals.sessions += 1;
1578
+ if (record.archived) {
1579
+ totals.archivedSessions += 1;
1580
+ }
1581
+ totals.eventCount += record.eventCount;
1582
+ totals.durationMs += record.durationMs;
1583
+ addTokenTotals(totals, record.tokens);
1584
+ for (const bucket of record.dailyBuckets) {
1585
+ const existing = daily.get(bucket.date) ?? {
1586
+ date: bucket.date,
1587
+ eventCount: 0,
1588
+ sessions: /* @__PURE__ */ new Set(),
1589
+ ...createZeroTokenTotals()
1590
+ };
1591
+ existing.eventCount += bucket.eventCount;
1592
+ addTokenTotals(existing, bucket);
1593
+ existing.sessions.add(record.sessionId);
1594
+ daily.set(bucket.date, existing);
1595
+ }
1596
+ for (const bucket of record.hourlyBuckets) {
1597
+ const existing = hourly.get(bucket.hour) ?? {
1598
+ hour: bucket.hour,
1599
+ eventCount: 0,
1600
+ sessions: /* @__PURE__ */ new Set(),
1601
+ ...createZeroTokenTotals()
1602
+ };
1603
+ existing.eventCount += bucket.eventCount;
1604
+ addTokenTotals(existing, bucket);
1605
+ existing.sessions.add(record.sessionId);
1606
+ hourly.set(bucket.hour, existing);
1607
+ }
1608
+ for (const modelTotals of record.modelTokenTotals) {
1609
+ const rate = rateLookup.get(normalizeRateKey(modelTotals.model));
1610
+ const modelKey = `${normalizeRateKey(modelTotals.model)}::${normalizeReasoningEffort$2(modelTotals.reasoningEffort)}`;
1611
+ const existing = modelBreakdown.get(modelKey) ?? {
1612
+ model: modelTotals.model,
1613
+ reasoningEffort: modelTotals.reasoningEffort,
1614
+ estimatedCostUsd: 0,
1615
+ sessions: /* @__PURE__ */ new Set(),
1616
+ archivedSessions: /* @__PURE__ */ new Set(),
1617
+ ...createZeroTokenTotals()
1618
+ };
1619
+ addTokenTotals(existing, modelTotals);
1620
+ existing.sessions.add(record.sessionId);
1621
+ if (record.archived) {
1622
+ existing.archivedSessions.add(record.sessionId);
1623
+ }
1624
+ if (rate) {
1625
+ const usageCost = estimateUsageCostUsd(modelTotals, rate);
1626
+ existing.estimatedCostUsd += usageCost;
1627
+ totals.estimatedCostUsd += usageCost;
1628
+ costCoverage.pricedTokens += modelTotals.totalTokens;
1629
+ } else {
1630
+ costCoverage.unpricedTokens += modelTotals.totalTokens;
1631
+ unpricedModels.add(modelTotals.model);
1632
+ }
1633
+ modelBreakdown.set(modelKey, existing);
1634
+ }
1635
+ }
1636
+ const dailyPoints = Array.from(daily.values()).map((bucket) => ({
1637
+ date: bucket.date,
1638
+ eventCount: bucket.eventCount,
1639
+ sessionCount: bucket.sessions.size,
1640
+ totalTokens: bucket.totalTokens,
1641
+ inputTokens: bucket.inputTokens,
1642
+ outputTokens: bucket.outputTokens,
1643
+ cachedTokens: bucket.cachedTokens,
1644
+ reasoningTokens: bucket.reasoningTokens
1645
+ })).sort((a, b) => a.date.localeCompare(b.date));
1646
+ const hourlyPoints = buildHourlyResult(hourly);
1647
+ const models = Array.from(modelBreakdown.values()).map((model) => ({
1648
+ model: model.model,
1649
+ reasoningEffort: model.reasoningEffort,
1650
+ sessionCount: model.sessions.size,
1651
+ archivedSessionCount: model.archivedSessions.size,
1652
+ estimatedCostUsd: Number(model.estimatedCostUsd.toFixed(6)),
1653
+ totalTokens: model.totalTokens,
1654
+ inputTokens: model.inputTokens,
1655
+ outputTokens: model.outputTokens,
1656
+ cachedTokens: model.cachedTokens,
1657
+ reasoningTokens: model.reasoningTokens
1658
+ })).sort((a, b) => b.totalTokens - a.totalTokens);
1659
+ const reasoningMap = /* @__PURE__ */ new Map();
1660
+ for (const model of modelBreakdown.values()) {
1661
+ const existing = reasoningMap.get(model.reasoningEffort) ?? {
1662
+ reasoningEffort: model.reasoningEffort,
1663
+ estimatedCostUsd: 0,
1664
+ sessions: /* @__PURE__ */ new Set(),
1665
+ ...createZeroTokenTotals()
1666
+ };
1667
+ for (const sessionId of model.sessions) {
1668
+ existing.sessions.add(sessionId);
1669
+ }
1670
+ existing.estimatedCostUsd += model.estimatedCostUsd;
1671
+ addTokenTotals(existing, model);
1672
+ reasoningMap.set(model.reasoningEffort, existing);
1673
+ }
1674
+ const reasoningEfforts = Array.from(reasoningMap.values()).map((reasoning) => ({
1675
+ reasoningEffort: reasoning.reasoningEffort,
1676
+ sessionCount: reasoning.sessions.size,
1677
+ estimatedCostUsd: Number(reasoning.estimatedCostUsd.toFixed(6)),
1678
+ totalTokens: reasoning.totalTokens,
1679
+ inputTokens: reasoning.inputTokens,
1680
+ outputTokens: reasoning.outputTokens,
1681
+ cachedTokens: reasoning.cachedTokens,
1682
+ reasoningTokens: reasoning.reasoningTokens
1683
+ })).sort((a, b) => b.totalTokens - a.totalTokens);
1684
+ const topDays = [...dailyPoints].sort((a, b) => b.eventCount - a.eventCount || b.totalTokens - a.totalTokens).slice(0, 5).map((day) => ({
1685
+ date: day.date,
1686
+ eventCount: day.eventCount,
1687
+ sessionCount: day.sessionCount,
1688
+ totalTokens: day.totalTokens,
1689
+ outputTokens: day.outputTokens
1690
+ }));
1691
+ const topHours = [...hourlyPoints].sort((a, b) => b.eventCount - a.eventCount || b.totalTokens - a.totalTokens).slice(0, 5).map((hour) => ({
1692
+ hour: hour.hour,
1693
+ eventCount: hour.eventCount,
1694
+ sessionCount: hour.sessionCount,
1695
+ totalTokens: hour.totalTokens,
1696
+ outputTokens: hour.outputTokens
1697
+ }));
1698
+ return {
1699
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1700
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "local",
1701
+ scope,
1702
+ totals: {
1703
+ ...totals,
1704
+ estimatedCostUsd: Number(totals.estimatedCostUsd.toFixed(6))
1705
+ },
1706
+ daily: dailyPoints,
1707
+ hourly: hourlyPoints,
1708
+ topDays,
1709
+ topHours,
1710
+ models,
1711
+ reasoningEfforts,
1712
+ costCoverage: {
1713
+ pricedTokens: costCoverage.pricedTokens,
1714
+ unpricedTokens: costCoverage.unpricedTokens,
1715
+ unpricedModels: Array.from(unpricedModels.values()).sort((a, b) => a.localeCompare(b))
1716
+ },
1717
+ rates: {
1718
+ updatedAt: rateCard.updatedAt,
1719
+ source: rateCard.source
1720
+ }
1721
+ };
1722
+ }
1723
+ const logger$4 = createLogger("Main:jsonl");
1300
1724
  const defaultParser = (value) => value;
1301
1725
  function waitForStreamClose(stream) {
1302
1726
  if (stream.closed) {
@@ -1343,7 +1767,7 @@ async function streamJsonlFile(filePath, options) {
1343
1767
  } catch (error) {
1344
1768
  options.onError?.(error, line, lineNumber);
1345
1769
  if (!options.onError) {
1346
- logger$2.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
1770
+ logger$4.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
1347
1771
  }
1348
1772
  }
1349
1773
  }
@@ -1384,7 +1808,7 @@ async function readFirstJsonlEntry(filePath, parser) {
1384
1808
  break;
1385
1809
  }
1386
1810
  } catch (error) {
1387
- logger$2.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
1811
+ logger$4.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
1388
1812
  }
1389
1813
  }
1390
1814
  } finally {
@@ -1392,7 +1816,7 @@ async function readFirstJsonlEntry(filePath, parser) {
1392
1816
  }
1393
1817
  return firstEntry;
1394
1818
  }
1395
- const logger$1 = createLogger("Service:CodexSessionScanner");
1819
+ const logger$3 = createLogger("Service:CodexSessionScanner");
1396
1820
  function isWithinDateRange(timestamp, options) {
1397
1821
  const current = new Date(timestamp).getTime();
1398
1822
  if (Number.isNaN(current)) {
@@ -1438,7 +1862,7 @@ class CodexSessionScanner {
1438
1862
  (value) => isSessionMetaEntry(value) ? value : null
1439
1863
  );
1440
1864
  if (!metaEntry) {
1441
- logger$1.warn(`Skipping session file without valid session_meta: ${filePath}`);
1865
+ logger$3.warn(`Skipping session file without valid session_meta: ${filePath}`);
1442
1866
  continue;
1443
1867
  }
1444
1868
  if (!isWithinDateRange(metaEntry.timestamp, options)) {
@@ -1575,6 +1999,7 @@ class CodexSessionParser {
1575
1999
  let firstTimestamp = null;
1576
2000
  let lastTimestamp = null;
1577
2001
  let firstTurnContextModel = null;
2002
+ let previousTotalUsage = null;
1578
2003
  const modelUsages = [];
1579
2004
  const modelUsageKeys = /* @__PURE__ */ new Set();
1580
2005
  for (const entry of entries) {
@@ -1615,7 +2040,12 @@ class CodexSessionParser {
1615
2040
  if (isEventMsgEntry(entry)) {
1616
2041
  eventMessages.push(entry);
1617
2042
  if (isTokenCountPayload(entry.payload) && entry.payload.info) {
1618
- const usage = entry.payload.info.last_token_usage;
2043
+ const currentTotal = entry.payload.info.total_token_usage;
2044
+ const usage = resolveTokenUsage(previousTotalUsage, currentTotal, entry.payload.info.last_token_usage);
2045
+ previousTotalUsage = currentTotal;
2046
+ if (!usage) {
2047
+ continue;
2048
+ }
1619
2049
  metrics.inputTokens += usage.input_tokens;
1620
2050
  metrics.cachedTokens += usage.cached_input_tokens;
1621
2051
  metrics.outputTokens += usage.output_tokens;
@@ -1713,7 +2143,7 @@ class DataCache {
1713
2143
  return `${cwdHash}/${sessionId}`;
1714
2144
  }
1715
2145
  }
1716
- const logger = createLogger("Service:FileWatcher");
2146
+ const logger$2 = createLogger("Service:FileWatcher");
1717
2147
  const DEBOUNCE_MS = 100;
1718
2148
  class FileWatcher extends node_events.EventEmitter {
1719
2149
  constructor(sessionsPath = path__namespace.join(os__namespace.homedir(), ".codex", "sessions")) {
@@ -1739,7 +2169,7 @@ class FileWatcher extends node_events.EventEmitter {
1739
2169
  }
1740
2170
  );
1741
2171
  } catch (error) {
1742
- logger.error(`Failed to start watcher for ${this.sessionsPath}`, error);
2172
+ logger$2.error(`Failed to start watcher for ${this.sessionsPath}`, error);
1743
2173
  }
1744
2174
  }
1745
2175
  stop() {
@@ -1787,10 +2217,179 @@ class FileWatcher extends node_events.EventEmitter {
1787
2217
  return fs__namespace.existsSync(targetPath) ? "created" : "deleted";
1788
2218
  }
1789
2219
  }
1790
- const DETAIL_CACHE_PREFIX = "detail-v2";
1791
- const CHUNKS_CACHE_PREFIX = "chunks-v2";
2220
+ const logger$1 = createLogger("Service:ModelRatesStore");
2221
+ const RATES_VERSION = 1;
2222
+ const BUNDLED_DEFAULT_RATES = [
2223
+ { model: "gpt-5.2", inputUsdPer1M: 1.75, cachedInputUsdPer1M: 0.175, outputUsdPer1M: 14, reasoningOutputUsdPer1M: 14 },
2224
+ { model: "gpt-5.1", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
2225
+ { model: "gpt-5", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
2226
+ { model: "gpt-5-mini", inputUsdPer1M: 0.25, cachedInputUsdPer1M: 0.025, outputUsdPer1M: 2, reasoningOutputUsdPer1M: 2 },
2227
+ { model: "gpt-5-nano", inputUsdPer1M: 0.05, cachedInputUsdPer1M: 5e-3, outputUsdPer1M: 0.4, reasoningOutputUsdPer1M: 0.4 },
2228
+ { model: "gpt-5.2-codex", inputUsdPer1M: 1.75, cachedInputUsdPer1M: 0.175, outputUsdPer1M: 14, reasoningOutputUsdPer1M: 14 },
2229
+ { model: "gpt-5.1-codex-max", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
2230
+ { model: "gpt-5.1-codex", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
2231
+ { model: "gpt-5-codex", inputUsdPer1M: 1.25, cachedInputUsdPer1M: 0.125, outputUsdPer1M: 10, reasoningOutputUsdPer1M: 10 },
2232
+ { model: "gpt-5.1-codex-mini", inputUsdPer1M: 0.25, cachedInputUsdPer1M: 0.025, outputUsdPer1M: 2, reasoningOutputUsdPer1M: 2 },
2233
+ { model: "codex-mini-latest", inputUsdPer1M: 1.5, cachedInputUsdPer1M: 0.375, outputUsdPer1M: 6, reasoningOutputUsdPer1M: 6 }
2234
+ ];
2235
+ const DEFAULT_RATE_CARD = {
2236
+ updatedAt: null,
2237
+ source: "bundled-defaults",
2238
+ models: BUNDLED_DEFAULT_RATES,
2239
+ warnings: ["Bundled rates may be outdated. Pricing refresh is currently disabled."]
2240
+ };
2241
+ function isRecord$1(value) {
2242
+ return typeof value === "object" && value !== null;
2243
+ }
2244
+ function isString$1(value) {
2245
+ return typeof value === "string";
2246
+ }
2247
+ function isFiniteNumber$1(value) {
2248
+ return typeof value === "number" && Number.isFinite(value);
2249
+ }
2250
+ function isCodexModelRate(value) {
2251
+ return isRecord$1(value) && isString$1(value.model) && isFiniteNumber$1(value.inputUsdPer1M) && isFiniteNumber$1(value.cachedInputUsdPer1M) && isFiniteNumber$1(value.outputUsdPer1M) && isFiniteNumber$1(value.reasoningOutputUsdPer1M);
2252
+ }
2253
+ function isRatesFile(value) {
2254
+ return isRecord$1(value) && isFiniteNumber$1(value.version) && (value.updatedAt === null || isString$1(value.updatedAt)) && (value.source === null || isString$1(value.source)) && Array.isArray(value.models) && value.models.every(isCodexModelRate) && Array.isArray(value.warnings) && value.warnings.every(isString$1);
2255
+ }
2256
+ function cloneRateCard(card) {
2257
+ return structuredClone(card);
2258
+ }
2259
+ class ModelRatesStore {
2260
+ constructor(filePath) {
2261
+ this.filePath = filePath;
2262
+ }
2263
+ async getRateCard() {
2264
+ const file = await this.readRateFile();
2265
+ return {
2266
+ updatedAt: file.updatedAt,
2267
+ source: file.source,
2268
+ models: file.models,
2269
+ warnings: file.warnings
2270
+ };
2271
+ }
2272
+ async refreshFromPricingPage() {
2273
+ const current = await this.getRateCard();
2274
+ return {
2275
+ ...current,
2276
+ warnings: [...current.warnings, "Pricing refresh is disabled in this build."],
2277
+ refreshed: false
2278
+ };
2279
+ }
2280
+ async readRateFile() {
2281
+ try {
2282
+ const raw = await fs.promises.readFile(this.filePath, "utf8");
2283
+ const parsed = JSON.parse(raw);
2284
+ if (!isRatesFile(parsed) || parsed.version !== RATES_VERSION) {
2285
+ return {
2286
+ version: RATES_VERSION,
2287
+ updatedAt: DEFAULT_RATE_CARD.updatedAt,
2288
+ source: DEFAULT_RATE_CARD.source,
2289
+ models: cloneRateCard(DEFAULT_RATE_CARD).models,
2290
+ warnings: [...DEFAULT_RATE_CARD.warnings]
2291
+ };
2292
+ }
2293
+ return parsed;
2294
+ } catch (error) {
2295
+ if (error.code !== "ENOENT") {
2296
+ logger$1.warn("Failed to load model rates, falling back to bundled defaults", error);
2297
+ }
2298
+ return {
2299
+ version: RATES_VERSION,
2300
+ updatedAt: DEFAULT_RATE_CARD.updatedAt,
2301
+ source: DEFAULT_RATE_CARD.source,
2302
+ models: cloneRateCard(DEFAULT_RATE_CARD).models,
2303
+ warnings: [...DEFAULT_RATE_CARD.warnings]
2304
+ };
2305
+ }
2306
+ }
2307
+ }
2308
+ const logger = createLogger("Service:StatsSnapshotStore");
2309
+ const SNAPSHOT_VERSION = 1;
2310
+ function isRecord(value) {
2311
+ return typeof value === "object" && value !== null;
2312
+ }
2313
+ function isFiniteNumber(value) {
2314
+ return typeof value === "number" && Number.isFinite(value);
2315
+ }
2316
+ function isString(value) {
2317
+ return typeof value === "string";
2318
+ }
2319
+ function isTokenTotals(value) {
2320
+ return isRecord(value) && isFiniteNumber(value.totalTokens) && isFiniteNumber(value.inputTokens) && isFiniteNumber(value.outputTokens) && isFiniteNumber(value.cachedTokens) && isFiniteNumber(value.reasoningTokens);
2321
+ }
2322
+ function isDailyBucket(value) {
2323
+ return isRecord(value) && isString(value.date) && isFiniteNumber(value.eventCount) && isTokenTotals(value);
2324
+ }
2325
+ function isHourlyBucket(value) {
2326
+ return isRecord(value) && isFiniteNumber(value.hour) && isFiniteNumber(value.eventCount) && isTokenTotals(value);
2327
+ }
2328
+ function isModelTotals(value) {
2329
+ return isRecord(value) && isString(value.model) && isString(value.reasoningEffort) && isTokenTotals(value);
2330
+ }
2331
+ function isSessionModelUsage(value) {
2332
+ return isRecord(value) && isString(value.model) && isString(value.reasoningEffort);
2333
+ }
2334
+ function isSessionRecord(value) {
2335
+ return isRecord(value) && (!("tokenComputationVersion" in value) || isFiniteNumber(value.tokenComputationVersion)) && isString(value.sessionId) && isString(value.filePath) && isString(value.revision) && typeof value.archived === "boolean" && isString(value.cwd) && isString(value.startTime) && isString(value.lastActivity) && isFiniteNumber(value.eventCount) && isFiniteNumber(value.turnCount) && isFiniteNumber(value.toolCallCount) && isFiniteNumber(value.durationMs) && Array.isArray(value.modelUsages) && value.modelUsages.every(isSessionModelUsage) && isTokenTotals(value.tokens) && Array.isArray(value.modelTokenTotals) && value.modelTokenTotals.every(isModelTotals) && Array.isArray(value.dailyBuckets) && value.dailyBuckets.every(isDailyBucket) && Array.isArray(value.hourlyBuckets) && value.hourlyBuckets.every(isHourlyBucket) && isString(value.lastSeenAt);
2336
+ }
2337
+ function isSnapshotFile(value) {
2338
+ return isRecord(value) && isFiniteNumber(value.version) && Array.isArray(value.sessions) && value.sessions.every(isSessionRecord);
2339
+ }
2340
+ function cloneSessions(rows) {
2341
+ return structuredClone(rows);
2342
+ }
2343
+ class StatsSnapshotStore {
2344
+ constructor(filePath) {
2345
+ this.filePath = filePath;
2346
+ }
2347
+ async getSessions() {
2348
+ const snapshot = await this.readSnapshot();
2349
+ return cloneSessions(snapshot.sessions);
2350
+ }
2351
+ async saveSessions(rows) {
2352
+ const snapshot = {
2353
+ version: SNAPSHOT_VERSION,
2354
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2355
+ sessions: cloneSessions(rows)
2356
+ };
2357
+ const directory = path__namespace.dirname(this.filePath);
2358
+ await fs.promises.mkdir(directory, { recursive: true });
2359
+ const tempPath = `${this.filePath}.tmp-${process.pid}-${Date.now()}`;
2360
+ await fs.promises.writeFile(tempPath, JSON.stringify(snapshot, null, 2), "utf8");
2361
+ await fs.promises.rename(tempPath, this.filePath);
2362
+ }
2363
+ async readSnapshot() {
2364
+ try {
2365
+ const raw = await fs.promises.readFile(this.filePath, "utf8");
2366
+ const parsed = JSON.parse(raw);
2367
+ if (!isSnapshotFile(parsed) || parsed.version !== SNAPSHOT_VERSION) {
2368
+ return {
2369
+ version: SNAPSHOT_VERSION,
2370
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
2371
+ sessions: []
2372
+ };
2373
+ }
2374
+ return parsed;
2375
+ } catch (error) {
2376
+ if (error.code !== "ENOENT") {
2377
+ logger.warn("Failed to read stats snapshot, using empty snapshot", error);
2378
+ }
2379
+ return {
2380
+ version: SNAPSHOT_VERSION,
2381
+ updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
2382
+ sessions: []
2383
+ };
2384
+ }
2385
+ }
2386
+ }
2387
+ const DETAIL_CACHE_PREFIX = "detail-v3";
2388
+ const CHUNKS_CACHE_PREFIX = "chunks-v4";
1792
2389
  const SESSIONS_CACHE_PREFIX = "sessions";
2390
+ const STATS_CACHE_PREFIX = "stats-v3";
1793
2391
  const UNKNOWN_REVISION = "unknown-revision";
2392
+ const TOKEN_COMPUTATION_VERSION = 4;
1794
2393
  function hasCompactionSignals(entries) {
1795
2394
  return entries.some(
1796
2395
  (entry) => isCompactedEntry(entry) || isCompactionEntry(entry) || isEventMsgEntry(entry) && isContextCompactedPayload(entry.payload)
@@ -1857,15 +2456,53 @@ function buildProjectsFromSessions(sessions) {
1857
2456
  (a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
1858
2457
  );
1859
2458
  }
2459
+ function normalizeStatsScope(scope) {
2460
+ if (!scope || scope.type === "all") {
2461
+ return { type: "all" };
2462
+ }
2463
+ const cwd = scope.cwd?.trim();
2464
+ if (!cwd) {
2465
+ return { type: "all" };
2466
+ }
2467
+ return {
2468
+ type: "project",
2469
+ cwd
2470
+ };
2471
+ }
2472
+ function serializeStatsScope(scope) {
2473
+ if (scope.type === "all") {
2474
+ return "all";
2475
+ }
2476
+ return `project:${scope.cwd}`;
2477
+ }
2478
+ function buildStatsSessionSignature(sessions, revisionByFilePath) {
2479
+ const hash = node_crypto.createHash("sha1");
2480
+ const sortedSessions = [...sessions].sort(
2481
+ (left, right) => left.filePath.localeCompare(right.filePath) || left.id.localeCompare(right.id)
2482
+ );
2483
+ for (const session of sortedSessions) {
2484
+ hash.update(session.id);
2485
+ hash.update("\0");
2486
+ hash.update(session.filePath);
2487
+ hash.update("\0");
2488
+ hash.update(revisionByFilePath.get(session.filePath) ?? UNKNOWN_REVISION);
2489
+ hash.update("\n");
2490
+ }
2491
+ return hash.digest("hex");
2492
+ }
1860
2493
  class CodexServiceContext {
1861
2494
  constructor(options = {}) {
1862
2495
  const sessionsPath = options.sessionsPath ?? process.env.CODEX_SESSIONS_PATH;
2496
+ const configPath = options.configPath ?? DEFAULT_CODEX_DEVTOOLS_CONFIG_PATH;
1863
2497
  this.scanner = new CodexSessionScanner(sessionsPath);
1864
2498
  this.parser = new CodexSessionParser();
1865
2499
  this.chunkBuilder = new CodexChunkBuilder();
1866
2500
  this.dataCache = new DataCache(options.cacheSize ?? 200, options.cacheTtlMinutes ?? 10);
1867
2501
  this.watcher = new FileWatcher(sessionsPath);
1868
- this.configManager = new ConfigManager(options.configPath);
2502
+ this.configManager = new ConfigManager(configPath);
2503
+ const metadataDirectory = path__namespace.dirname(configPath);
2504
+ this.statsSnapshotStore = new StatsSnapshotStore(path__namespace.join(metadataDirectory, "stats-snapshot.json"));
2505
+ this.modelRatesStore = new ModelRatesStore(path__namespace.join(metadataDirectory, "stats-rates.json"));
1869
2506
  this.removeFileChangeListener = this.watcher.onFileChange(() => {
1870
2507
  this.dataCache.clear();
1871
2508
  });
@@ -1983,6 +2620,33 @@ class CodexServiceContext {
1983
2620
  results
1984
2621
  };
1985
2622
  }
2623
+ async getModelRates() {
2624
+ return this.modelRatesStore.getRateCard();
2625
+ }
2626
+ async refreshModelRates() {
2627
+ const refreshed = await this.modelRatesStore.refreshFromPricingPage();
2628
+ this.dataCache.clear();
2629
+ return refreshed;
2630
+ }
2631
+ async getStats(scope = { type: "all" }) {
2632
+ const normalizedScope = normalizeStatsScope(scope);
2633
+ const rates = await this.getModelRates();
2634
+ const sessions = await this.scanner.scanSessions();
2635
+ const revisionByFilePath = await this.getSessionRevisionLookup(sessions);
2636
+ const sessionSignature = buildStatsSessionSignature(sessions, revisionByFilePath);
2637
+ const cacheKey = DataCache.buildKey(
2638
+ STATS_CACHE_PREFIX,
2639
+ `${serializeStatsScope(normalizedScope)}:${rates.updatedAt ?? "never"}:${sessionSignature}`
2640
+ );
2641
+ const cached = this.dataCache.get(cacheKey);
2642
+ if (cached) {
2643
+ return cached;
2644
+ }
2645
+ const reconciledRecords = await this.reconcileStatsSnapshot(sessions, revisionByFilePath);
2646
+ const summary = aggregateStatsSummary(reconciledRecords, normalizedScope, rates);
2647
+ this.dataCache.set(cacheKey, summary);
2648
+ return summary;
2649
+ }
1986
2650
  getConfig() {
1987
2651
  return this.configManager.getConfig();
1988
2652
  }
@@ -1999,6 +2663,75 @@ class CodexServiceContext {
1999
2663
  value
2000
2664
  );
2001
2665
  }
2666
+ async reconcileStatsSnapshot(currentSessions, revisionByFilePath) {
2667
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
2668
+ const existingRecords = await this.statsSnapshotStore.getSessions();
2669
+ const existingBySessionId = new Map(existingRecords.map((record) => [record.sessionId, record]));
2670
+ const nextRecords = [];
2671
+ const activeSessionIds = /* @__PURE__ */ new Set();
2672
+ for (const session of currentSessions) {
2673
+ activeSessionIds.add(session.id);
2674
+ const revision = revisionByFilePath.get(session.filePath) ?? UNKNOWN_REVISION;
2675
+ const existing = existingBySessionId.get(session.id);
2676
+ if (existing && existing.revision === revision && existing.tokenComputationVersion === TOKEN_COMPUTATION_VERSION) {
2677
+ nextRecords.push({
2678
+ ...existing,
2679
+ archived: false,
2680
+ revision,
2681
+ filePath: session.filePath,
2682
+ cwd: session.cwd,
2683
+ startTime: session.startTime,
2684
+ modelUsages: session.modelUsages.length > 0 ? session.modelUsages : existing.modelUsages,
2685
+ lastSeenAt: nowIso
2686
+ });
2687
+ continue;
2688
+ }
2689
+ let detail = null;
2690
+ try {
2691
+ detail = await this.getSessionDetail(session.id);
2692
+ } catch {
2693
+ detail = null;
2694
+ }
2695
+ if (!detail) {
2696
+ if (existing) {
2697
+ nextRecords.push({
2698
+ ...existing,
2699
+ archived: false,
2700
+ revision,
2701
+ filePath: session.filePath,
2702
+ cwd: session.cwd,
2703
+ startTime: session.startTime,
2704
+ lastSeenAt: nowIso
2705
+ });
2706
+ }
2707
+ continue;
2708
+ }
2709
+ const nextRecord = buildSessionStatsRecord(detail, revision, nowIso);
2710
+ nextRecord.archived = false;
2711
+ nextRecords.push(nextRecord);
2712
+ }
2713
+ for (const record of existingRecords) {
2714
+ if (activeSessionIds.has(record.sessionId)) {
2715
+ continue;
2716
+ }
2717
+ nextRecords.push({
2718
+ ...record,
2719
+ archived: true
2720
+ });
2721
+ }
2722
+ nextRecords.sort((a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime());
2723
+ await this.statsSnapshotStore.saveSessions(nextRecords);
2724
+ return nextRecords;
2725
+ }
2726
+ async getSessionRevisionLookup(sessions) {
2727
+ const revisions = await Promise.all(
2728
+ sessions.map(async (session) => {
2729
+ const revision = await this.getSessionFileRevision(session.filePath);
2730
+ return [session.filePath, revision];
2731
+ })
2732
+ );
2733
+ return new Map(revisions);
2734
+ }
2002
2735
  async findSessionById(sessionId) {
2003
2736
  const sessions = await this.getAllSessions();
2004
2737
  const match = sessions.find((session) => session.id === sessionId);