farming-code 2.2.14 → 2.2.17

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 (136) hide show
  1. package/README.md +3 -3
  2. package/README.zh_cn.md +3 -3
  3. package/THIRD_PARTY_NOTICES.md +17 -1
  4. package/backend/acp/packaged-codex-acp.js +13 -0
  5. package/backend/acp-runtime.js +413 -24
  6. package/backend/agent-manager.js +572 -557
  7. package/backend/agent-provider-session.js +15 -1
  8. package/backend/agent-runtime-binding.js +9 -55
  9. package/backend/chat-runtime.js +28 -0
  10. package/backend/codex-rollout-follower.js +83 -0
  11. package/backend/codex-session-archive.js +12 -1
  12. package/backend/codex-session-history.js +94 -0
  13. package/backend/codex-transcript.js +2 -1
  14. package/backend/config-manager.js +25 -17
  15. package/backend/control-api.js +3 -1
  16. package/backend/farming-app-cli.js +21 -0
  17. package/backend/farming-session-store.js +2 -1
  18. package/backend/native-pty-host-client.js +59 -2
  19. package/backend/npm-update-helper.js +65 -7
  20. package/backend/provider-adapters.js +123 -38
  21. package/backend/provider-session-service.js +138 -34
  22. package/backend/server.js +94 -105
  23. package/backend/storage-layout.js +5 -16
  24. package/backend/update-service.js +94 -5
  25. package/backend/usage-history-client.js +305 -0
  26. package/backend/usage-history-scanner.generated.js +2250 -0
  27. package/backend/usage-history-scanner.ts +2718 -0
  28. package/backend/usage-history-smoke.js +64 -0
  29. package/backend/usage-history-worker.js +34 -0
  30. package/backend/usage-monitor.js +188 -588
  31. package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
  32. package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
  33. package/backend/vendor/usage-parsers/VENDOR.json +45 -0
  34. package/dist/acp/LICENSE.codex-acp +190 -0
  35. package/dist/acp/codex-acp-1.1.4.js +30718 -0
  36. package/dist/assets/App-6yWdwzj8.js +210 -0
  37. package/dist/assets/{FileEditorMarkdownPreview-UlVd0O4D.js → FileEditorMarkdownPreview-R0jwwLME.js} +4 -4
  38. package/dist/assets/FileEditorPane-DNy3LAqS.js +2 -0
  39. package/dist/assets/{IconGlyphs-AU22gPDY.js → IconGlyphs-BsUfqpKN.js} +1 -1
  40. package/dist/assets/ProjectFilesSection-BhDEUVHA.js +12 -0
  41. package/dist/assets/{ReviewPage-CeDKDyZh.js → ReviewPage-ChtByBe3.js} +1 -1
  42. package/dist/assets/{abnfDiagram-VRR7QNED-BgawzDQT.js → abnfDiagram-VRR7QNED-nnglrXFi.js} +1 -1
  43. package/dist/assets/{architectureDiagram-ZJ3FMSHR-DUShwPg0.js → architectureDiagram-ZJ3FMSHR-BEXMaFfY.js} +1 -1
  44. package/dist/assets/{blockDiagram-677ZJIJ3-CeGZviXR.js → blockDiagram-677ZJIJ3-1mCezSBk.js} +1 -1
  45. package/dist/assets/{c4Diagram-LMCZKHZV-CLhYht6S.js → c4Diagram-LMCZKHZV-DhTiI6Uv.js} +1 -1
  46. package/dist/assets/channel-BVWNatUB.js +1 -0
  47. package/dist/assets/{chunk-32BRIVSS-LFYpT-Cu.js → chunk-32BRIVSS-CCdsKd73.js} +1 -1
  48. package/dist/assets/{chunk-52WLFC77-BH6jDw9J.js → chunk-52WLFC77-Gncjk4LF.js} +1 -1
  49. package/dist/assets/{chunk-C7G6YPKG-B_mIUlb7.js → chunk-C7G6YPKG-CiDVUP82.js} +1 -1
  50. package/dist/assets/{chunk-EX3LRPZG-C63hVuAJ.js → chunk-EX3LRPZG-z6gXP2_N.js} +1 -1
  51. package/dist/assets/{chunk-FWX5IMBZ-CboX-WF_.js → chunk-FWX5IMBZ-DMZBccQa.js} +2 -2
  52. package/dist/assets/{chunk-HOUHSVGY-BOgSd6Un.js → chunk-HOUHSVGY-D8tIeoPk.js} +1 -1
  53. package/dist/assets/{chunk-ICXQ74PX-C0K-pgZt.js → chunk-ICXQ74PX-b6oQ2Q17.js} +1 -1
  54. package/dist/assets/{chunk-MOJQB5TN-CVMfwN_7.js → chunk-MOJQB5TN-DAVH_p2a.js} +1 -1
  55. package/dist/assets/{chunk-OGEWGWER-DLT3pxyw.js → chunk-OGEWGWER-DxoiMCI-.js} +1 -1
  56. package/dist/assets/{chunk-PUDLZKDR-Cd079yIQ.js → chunk-PUDLZKDR-NsOWh7HM.js} +1 -1
  57. package/dist/assets/{chunk-Q4XR5HBZ-D0jHa6SO.js → chunk-Q4XR5HBZ-Di4H7L21.js} +1 -1
  58. package/dist/assets/{chunk-V7JOEXUC-CVDtR75H.js → chunk-V7JOEXUC-DttpZg9Y.js} +1 -1
  59. package/dist/assets/{chunk-VAUOI2AC-B-W2_9Ot.js → chunk-VAUOI2AC-UHNr_yhX.js} +1 -1
  60. package/dist/assets/{chunk-VR4S4FIN-FD3kWh32.js → chunk-VR4S4FIN-D-Fmtd4Z.js} +1 -1
  61. package/dist/assets/{chunk-WYO6CB5R-C2bGaS5r.js → chunk-WYO6CB5R-CUxMtfK2.js} +4 -4
  62. package/dist/assets/{chunk-ZGVPDNZ5-ymgts0zo.js → chunk-ZGVPDNZ5-CoV2QEl0.js} +1 -1
  63. package/dist/assets/classDiagram-OUVF2IWQ-Db0ib5iE.js +1 -0
  64. package/dist/assets/classDiagram-v2-EOCWNBFH-Bkf69XF9.js +1 -0
  65. package/dist/assets/code-dark-5esj0bRu.css +1 -0
  66. package/dist/assets/{cynefinDiagram-TSTJHNR4-CwpobXIl.js → cynefinDiagram-TSTJHNR4-0xPI92Lg.js} +1 -1
  67. package/dist/assets/{dagre-VKFMJZFB-Dkhlfjna.js → dagre-VKFMJZFB-DfuMG4gU.js} +1 -1
  68. package/dist/assets/{diagram-FQU43EPY-hA8dicp6.js → diagram-FQU43EPY-CT00bLZ6.js} +1 -1
  69. package/dist/assets/{diagram-G47NLZAW-BTY6JTzO.js → diagram-G47NLZAW-Xbh7zXUx.js} +1 -1
  70. package/dist/assets/{diagram-NH7WQ7WH-xkaHHX9M.js → diagram-NH7WQ7WH-SRDVPfn7.js} +1 -1
  71. package/dist/assets/{diagram-OA4YK3LP-gk_fg9UU.js → diagram-OA4YK3LP-CQRli4Np.js} +1 -1
  72. package/dist/assets/{diagram-WEI45ONY-BvC8xd6T.js → diagram-WEI45ONY-DMf17WQi.js} +1 -1
  73. package/dist/assets/{ebnfDiagram-CCIWWBDH-CR7RaNat.js → ebnfDiagram-CCIWWBDH-DhpFkgJ5.js} +1 -1
  74. package/dist/assets/{erDiagram-Q63AITRT-_M-JFPbj.js → erDiagram-Q63AITRT-1IwxWmJo.js} +1 -1
  75. package/dist/assets/file-icons-DpbrD5ZP.js +1 -0
  76. package/dist/assets/flowDiagram-23GEKE2U-DYFo2xmB.js +1 -0
  77. package/dist/assets/{ganttDiagram-NO4QXBWP-Bl8P1yf_.js → ganttDiagram-NO4QXBWP-BNCdfyr1.js} +1 -1
  78. package/dist/assets/{gitGraphDiagram-IHSO6WYX-DoHa5ysz.js → gitGraphDiagram-IHSO6WYX-pbyWZuiX.js} +1 -1
  79. package/dist/assets/{index-BA-iH2Kg.js → index-C8ZFKye1.js} +2 -2
  80. package/dist/assets/{infoDiagram-FWYZ7A6U-DSFcSuUs.js → infoDiagram-FWYZ7A6U-BcTOgDbe.js} +1 -1
  81. package/dist/assets/{ishikawaDiagram-FXEZZL3T-DdkqullI.js → ishikawaDiagram-FXEZZL3T-DCdvGKrn.js} +1 -1
  82. package/dist/assets/{journeyDiagram-5HDEW3XC-B4hRnQJr.js → journeyDiagram-5HDEW3XC-BZ-1q_IT.js} +1 -1
  83. package/dist/assets/{kanban-definition-HUTT4EX6-tm2FZB2E.js → kanban-definition-HUTT4EX6-BIgejCjj.js} +1 -1
  84. package/dist/assets/{line-CmsjK4kn.js → line-CfptFMmX.js} +1 -1
  85. package/dist/assets/main-DSO3i_Gs.css +1 -0
  86. package/dist/assets/{mermaid.core-C7UT_EEL.js → mermaid.core-gSpX30pV.js} +3 -3
  87. package/dist/assets/{mindmap-definition-LN4V7U3C-DplfjGms.js → mindmap-definition-LN4V7U3C-BKBaI4LW.js} +1 -1
  88. package/dist/assets/{pegDiagram-2B236MQR-lu3SdkQs.js → pegDiagram-2B236MQR-sBvDY3VX.js} +1 -1
  89. package/dist/assets/{pieDiagram-ENE6RG2P-3fHbmDf9.js → pieDiagram-ENE6RG2P-DWlWcu08.js} +1 -1
  90. package/dist/assets/{quadrantDiagram-ABIIQ3AL-a5qoWz-X.js → quadrantDiagram-ABIIQ3AL-DZutUd3J.js} +1 -1
  91. package/dist/assets/{railroadDiagram-RFXS5EU6-D2TOuXW9.js → railroadDiagram-RFXS5EU6-DQb3lXGM.js} +1 -1
  92. package/dist/assets/{requirementDiagram-TGXJPOKE-LQBWpUvd.js → requirementDiagram-TGXJPOKE-DqFGXu2e.js} +1 -1
  93. package/dist/assets/{sankeyDiagram-HTMAVEWB-B1ohYoyK.js → sankeyDiagram-HTMAVEWB-d7ECahnO.js} +1 -1
  94. package/dist/assets/{sequenceDiagram-DBY2YBRQ-C3vM-hMq.js → sequenceDiagram-DBY2YBRQ-DdsbKqct.js} +1 -1
  95. package/dist/assets/{stateDiagram-2N3HPSRC-BvFhOryI.js → stateDiagram-2N3HPSRC-P6iSrgwR.js} +1 -1
  96. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIYSf_qh.js +1 -0
  97. package/dist/assets/{swimlanes-5IMT3BWC-DKF_b-2s.js → swimlanes-5IMT3BWC-BGnZJx1M.js} +1 -1
  98. package/dist/assets/swimlanesDiagram-G3AALYLV-BIkrlKrZ.js +8 -0
  99. package/dist/assets/{timeline-definition-FHXFAJF6-vAs11ux_.js → timeline-definition-FHXFAJF6-rA-r3aUR.js} +1 -1
  100. package/dist/assets/{vennDiagram-L72KCM5P-BybTX_9B.js → vennDiagram-L72KCM5P-DiVKUjA9.js} +1 -1
  101. package/dist/assets/{wardleyDiagram-EHGQE667-CicUmMBR.js → wardleyDiagram-EHGQE667-Cfxh7UOn.js} +1 -1
  102. package/dist/assets/workspace-editor-model-D0Im34S5.js +1 -0
  103. package/dist/assets/{workspace-editor-monaco-BICJESG0.js → workspace-editor-monaco-BIpqcWf1.js} +1 -1
  104. package/dist/assets/workspace-editor-monaco-DLlws5Ig.js +1 -0
  105. package/dist/assets/{workspace-view-state-DvYG_9PH.js → workspace-view-state-BpPINu84.js} +1 -1
  106. package/dist/assets/{xychartDiagram-FW5EYKEG-BhWDyCX-.js → xychartDiagram-FW5EYKEG--hmryQeL.js} +1 -1
  107. package/dist/crt-mermaid-renderer.js +313 -313
  108. package/dist/index.html +1 -1
  109. package/frontend/session-bridge.js +24 -2
  110. package/frontend/skins/crt/app.js +203 -85
  111. package/frontend/skins/crt/index.html +2 -11
  112. package/frontend/skins/crt/styles/effects.css +2 -34
  113. package/package.json +30 -12
  114. package/shared/backend-connection-status.d.ts +25 -0
  115. package/shared/backend-connection-status.js +40 -0
  116. package/shared/browser-protocol.js +3 -3
  117. package/backend/app-server-api.js +0 -917
  118. package/backend/codex-app-server-home.js +0 -39
  119. package/backend/codex-app-server-runtime.js +0 -901
  120. package/dist/assets/App-Bumi772v.js +0 -206
  121. package/dist/assets/FileEditorPane-Dxx4rKOg.js +0 -2
  122. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +0 -12
  123. package/dist/assets/channel-Uw90AO0b.js +0 -1
  124. package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +0 -1
  125. package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +0 -1
  126. package/dist/assets/code-dark-DPiuyeSp.css +0 -1
  127. package/dist/assets/code-mobile-3C2Rr-CN.css +0 -1
  128. package/dist/assets/file-icons-Bw2qd5iT.js +0 -1
  129. package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +0 -1
  130. package/dist/assets/main-Bqtb5kuz.css +0 -1
  131. package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +0 -1
  132. package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +0 -8
  133. package/dist/assets/workspace-editor-model-BQol4qbA.js +0 -1
  134. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +0 -1
  135. package/scripts/sync-ghostty-vendor.js +0 -66
  136. /package/dist/assets/{core-D0LFJkDt.js → core-B9MwRjDE.js} +0 -0
@@ -1,9 +1,8 @@
1
- const fsp = require('fs/promises');
2
1
  const os = require('os');
3
2
  const path = require('path');
4
- const readline = require('readline');
5
- const { execFile, spawn } = require('child_process');
3
+ const { execFile } = require('child_process');
6
4
  const { promisify } = require('util');
5
+ const { UsageHistoryClient } = require('./usage-history-client');
7
6
  const { attachQuotaForecasts } = require('./usage-forecast');
8
7
 
9
8
  const execFileAsync = promisify(execFile);
@@ -14,18 +13,12 @@ const USAGE_TIMELINE_BUCKET_COUNT = 24;
14
13
  const USAGE_DAILY_DAYS = 52 * 7;
15
14
  const USAGE_DAILY_CACHE_MS = 5 * 60 * 1000;
16
15
  const USAGE_LIVE_DAY_CACHE_MS = 5 * 1000;
17
- const JSONL_FILE_LIMIT = 60;
18
- const JSONL_SCAN_LIMIT = 2000;
19
- const DAILY_JSONL_FILE_LIMIT = 5000;
20
- const DAILY_JSONL_SCAN_LIMIT = 20_000;
21
- const JSONL_TAIL_BYTES = 2 * 1024 * 1024;
22
16
  const COMMAND_TIMEOUT_MS = 2500;
23
17
  const OPENCODE_COMMAND_TIMEOUT_MS = 20_000;
24
18
  const OPENCODE_EXPORT_CONCURRENCY = 4;
25
19
  const OPENCODE_SESSION_LIMIT = 5000;
26
- const dailyFileEventCache = new Map();
20
+ const OPENCODE_SESSION_CACHE_LIMIT = 5000;
27
21
  const openCodeSessionEventCache = new Map();
28
- let nativeRipgrepPathPromise = null;
29
22
 
30
23
  function numberOrNull(value) {
31
24
  const numberValue = Number(value);
@@ -50,18 +43,6 @@ function parseTimestampMs(value) {
50
43
  return Number.isFinite(parsed) ? parsed : null;
51
44
  }
52
45
 
53
- function tokenTotalFromUsage(usage, fields) {
54
- if (!usage || typeof usage !== 'object') return 0;
55
-
56
- const explicitTotal = numberOrNull(usage.total_tokens);
57
- if (explicitTotal !== null) return Math.max(0, explicitTotal);
58
-
59
- return fields.reduce((sum, field) => {
60
- const value = numberOrNull(usage[field]);
61
- return sum + Math.max(0, value ?? 0);
62
- }, 0);
63
- }
64
-
65
46
  function tokenUsageSummary({ totalTokens, eventCount, source, windowMs, sampledAt }) {
66
47
  const windowMinutes = Math.max(1, windowMs / 60_000);
67
48
  return {
@@ -75,10 +56,6 @@ function tokenUsageSummary({ totalTokens, eventCount, source, windowMs, sampledA
75
56
  };
76
57
  }
77
58
 
78
- function codexTokenTotalFromInfo(info, field) {
79
- return tokenTotalFromUsage(info?.[field], ['input_tokens', 'output_tokens']);
80
- }
81
-
82
59
  function emptyTokenBreakdown() {
83
60
  return {
84
61
  totalTokens: 0,
@@ -90,30 +67,6 @@ function emptyTokenBreakdown() {
90
67
  };
91
68
  }
92
69
 
93
- function tokenBreakdownFromUsage(usage, provider) {
94
- if (!usage || typeof usage !== 'object') return emptyTokenBreakdown();
95
- const rawInput = Math.max(0, numberOrNull(usage.input_tokens) ?? 0);
96
- const outputTokens = Math.max(0, numberOrNull(usage.output_tokens) ?? 0);
97
- const cacheReadTokens = Math.max(0, numberOrNull(
98
- usage.cached_input_tokens ?? usage.cache_read_input_tokens,
99
- ) ?? 0);
100
- const cacheWriteTokens = Math.max(0, numberOrNull(usage.cache_creation_input_tokens) ?? 0);
101
- const inputTokens = provider === 'codex'
102
- ? Math.max(0, rawInput - cacheReadTokens)
103
- : rawInput;
104
- const componentTotal = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
105
- const explicitTotal = numberOrNull(usage.total_tokens);
106
- const totalTokens = Math.max(0, explicitTotal ?? componentTotal);
107
- return {
108
- totalTokens,
109
- inputTokens,
110
- outputTokens,
111
- cacheReadTokens,
112
- cacheWriteTokens,
113
- unattributedTokens: Math.max(0, totalTokens - componentTotal),
114
- };
115
- }
116
-
117
70
  function tokenBreakdownFromOpenCode(tokens) {
118
71
  if (!tokens || typeof tokens !== 'object') return emptyTokenBreakdown();
119
72
  const inputTokens = Math.max(0, numberOrNull(tokens.input) ?? 0);
@@ -145,6 +98,73 @@ function providerHomePaths(providerHomes, provider, fallbackPath) {
145
98
  }).filter(Boolean)));
146
99
  }
147
100
 
101
+ function ccStatisticsRoots(options = {}) {
102
+ const codexHomes = providerHomePaths(
103
+ options.providerHomes,
104
+ 'codex',
105
+ options.codexHome || path.join(os.homedir(), '.codex'),
106
+ );
107
+ const claudeHomes = providerHomePaths(
108
+ options.providerHomes,
109
+ 'claude',
110
+ options.claudeHome || path.join(os.homedir(), '.claude'),
111
+ );
112
+ return {
113
+ codexHomes,
114
+ claudeHomes,
115
+ codexRoots: codexHomes.flatMap(home => [
116
+ path.join(home, 'sessions'),
117
+ path.join(home, 'archived_sessions'),
118
+ ]),
119
+ claudeRoots: claudeHomes.map(home => path.join(home, 'projects')),
120
+ };
121
+ }
122
+
123
+ function ccStatisticsClient(options = {}) {
124
+ return options.usageHistoryClient || options.ccStatisticsClient || new UsageHistoryClient({
125
+ configDir: options.configDir || path.join(os.homedir(), '.farming'),
126
+ });
127
+ }
128
+
129
+ function unavailableCCStatisticsResult(error, now) {
130
+ const reason = commandUnavailable(error);
131
+ return {
132
+ schemaVersion: 1,
133
+ source: 'local usage history unavailable',
134
+ sampledAt: now,
135
+ providers: {
136
+ codex: { events: [], quotaCandidates: [], available: false, reason, fileCount: 0 },
137
+ claude: { events: [], quotaCandidates: [], available: false, reason, fileCount: 0 },
138
+ },
139
+ cache: { errors: 1 },
140
+ };
141
+ }
142
+
143
+ async function collectCCStatistics(options = {}) {
144
+ const now = options.now ?? Date.now();
145
+ const roots = ccStatisticsRoots(options);
146
+ try {
147
+ const result = await ccStatisticsClient(options).collect({
148
+ now,
149
+ retentionDays: options.retentionDays ?? USAGE_DAILY_DAYS,
150
+ codexRoots: roots.codexRoots,
151
+ claudeRoots: roots.claudeRoots,
152
+ fresh: options.fresh,
153
+ });
154
+ return { result, roots };
155
+ } catch (error) {
156
+ return { result: unavailableCCStatisticsResult(error, now), roots };
157
+ }
158
+ }
159
+
160
+ function ccStatisticsProviderEvents(result, provider) {
161
+ return (result?.providers?.[provider]?.events || []).map(event => attributeUsageEvent(
162
+ event,
163
+ provider,
164
+ String(event.sessionId || 'unattributed'),
165
+ ));
166
+ }
167
+
148
168
  function addTokenBreakdown(target, source) {
149
169
  for (const field of Object.keys(emptyTokenBreakdown())) {
150
170
  target[field] += Math.max(0, numberOrNull(source?.[field]) ?? 0);
@@ -152,12 +172,6 @@ function addTokenBreakdown(target, source) {
152
172
  return target;
153
173
  }
154
174
 
155
- function usageAgentIdFromFilePath(filePath) {
156
- const basename = path.basename(String(filePath || ''), path.extname(String(filePath || '')));
157
- const uuid = basename.match(/([0-9a-f]{8}-[0-9a-f-]{27})$/i);
158
- return uuid?.[1] || basename || 'unattributed';
159
- }
160
-
161
175
  function usageAgentLabel(provider, agentId) {
162
176
  const providerName = {
163
177
  codex: 'Codex',
@@ -178,117 +192,6 @@ function attributeUsageEvent(event, provider, agentId) {
178
192
  };
179
193
  }
180
194
 
181
- function subtractTokenBreakdown(current, previous) {
182
- const result = emptyTokenBreakdown();
183
- for (const field of Object.keys(result)) {
184
- result[field] = Math.max(0, current[field] - previous[field]);
185
- }
186
- return result;
187
- }
188
-
189
- function normalizeDeltaBreakdown(current, previous, last) {
190
- const deltaTotal = Math.max(0, current.totalTokens - previous.totalTokens);
191
- const delta = subtractTokenBreakdown(current, previous);
192
- const componentTotal = delta.inputTokens + delta.outputTokens + delta.cacheReadTokens
193
- + delta.cacheWriteTokens + delta.unattributedTokens;
194
- if (componentTotal === deltaTotal) return delta;
195
- if (last.totalTokens === deltaTotal) return last;
196
- return { ...emptyTokenBreakdown(), totalTokens: deltaTotal, unattributedTokens: deltaTotal };
197
- }
198
-
199
- function usageObjectSignature(usage) {
200
- if (!usage || typeof usage !== 'object') return '';
201
- return JSON.stringify(Object.keys(usage).sort().map(key => [key, usage[key]]));
202
- }
203
-
204
- function createCodexDeltaState() {
205
- return {
206
- previousTotal: null,
207
- previousBreakdown: emptyTokenBreakdown(),
208
- seenFallbackUsages: new Set(),
209
- };
210
- }
211
-
212
- function codexTokenEventFromRecord(record, state) {
213
- if (record?.type !== 'event_msg' || record.payload?.type !== 'token_count') return null;
214
- const timestamp = parseTimestampMs(record.timestamp);
215
- if (!timestamp) return null;
216
- const info = record.payload?.info;
217
- const cumulativeTotal = codexTokenTotalFromInfo(info, 'total_token_usage');
218
- const lastTokenTotal = codexTokenTotalFromInfo(info, 'last_token_usage');
219
- const cumulativeBreakdown = tokenBreakdownFromUsage(info?.total_token_usage, 'codex');
220
- const lastBreakdown = tokenBreakdownFromUsage(info?.last_token_usage, 'codex');
221
-
222
- if (cumulativeTotal > 0) {
223
- let delta = 0;
224
- let deltaBreakdown = emptyTokenBreakdown();
225
- if (state.previousTotal !== null && cumulativeTotal >= state.previousTotal) {
226
- delta = cumulativeTotal - state.previousTotal;
227
- deltaBreakdown = normalizeDeltaBreakdown(
228
- cumulativeBreakdown,
229
- state.previousBreakdown,
230
- lastBreakdown,
231
- );
232
- } else if (state.previousTotal === null) {
233
- delta = lastTokenTotal > 0 ? lastTokenTotal : cumulativeTotal;
234
- deltaBreakdown = lastTokenTotal > 0 ? lastBreakdown : cumulativeBreakdown;
235
- } else if (lastTokenTotal > 0) {
236
- delta = lastTokenTotal;
237
- deltaBreakdown = lastBreakdown;
238
- }
239
- state.previousTotal = cumulativeTotal;
240
- state.previousBreakdown = cumulativeBreakdown;
241
- return delta > 0 ? { timestamp, ...deltaBreakdown, totalTokens: delta } : null;
242
- }
243
-
244
- if (lastTokenTotal <= 0) return null;
245
- const signature = usageObjectSignature(info?.last_token_usage);
246
- if (signature && state.seenFallbackUsages.has(signature)) return null;
247
- if (signature) state.seenFallbackUsages.add(signature);
248
- return { timestamp, ...lastBreakdown, totalTokens: lastTokenTotal };
249
- }
250
-
251
- function collectCodexTokenDeltas(records, {
252
- now,
253
- windowMs,
254
- historyWindowMs = windowMs,
255
- includeAllEvents = false,
256
- }) {
257
- const windowStart = now - windowMs;
258
- const historyWindowStart = now - historyWindowMs;
259
- const sortedRecords = records
260
- .map((record, index) => ({
261
- record,
262
- index,
263
- timestamp: parseTimestampMs(record.timestamp),
264
- }))
265
- .filter(entry => entry.record.type === 'event_msg' && entry.record.payload?.type === 'token_count')
266
- .sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0) || a.index - b.index);
267
-
268
- let totalTokens = 0;
269
- let eventCount = 0;
270
- const tokenEvents = [];
271
- const deltaState = createCodexDeltaState();
272
-
273
- for (const entry of sortedRecords) {
274
- const { record } = entry;
275
- const event = codexTokenEventFromRecord(record, deltaState);
276
- if (!event) continue;
277
- const { timestamp } = event;
278
- const inWindow = Boolean(timestamp && timestamp >= windowStart && timestamp <= now + 60_000);
279
- const inHistoryWindow = Boolean(timestamp && (
280
- includeAllEvents || (timestamp >= historyWindowStart && timestamp <= now + 60_000)
281
- ));
282
- if (inWindow) {
283
- totalTokens += event.totalTokens;
284
- eventCount += 1;
285
- }
286
- if (inHistoryWindow) tokenEvents.push(event);
287
- }
288
-
289
- return { totalTokens, eventCount, tokenEvents };
290
- }
291
-
292
195
  function buildUsageTimeline(providerEvents, options = {}) {
293
196
  const now = options.now ?? Date.now();
294
197
  const windowMs = Math.max(60_000, options.windowMs ?? USAGE_TIMELINE_WINDOW_MS);
@@ -443,162 +346,6 @@ async function readClaudeAuthStatus(commandRunner = defaultCommandRunner) {
443
346
  }
444
347
  }
445
348
 
446
- async function readJsonlTail(filePath, maxBytes = JSONL_TAIL_BYTES) {
447
- const stat = await fsp.stat(filePath);
448
- const size = stat.size;
449
- if (size <= 0) return '';
450
-
451
- const bytesToRead = Math.min(size, maxBytes);
452
- const start = size - bytesToRead;
453
- const buffer = Buffer.alloc(bytesToRead);
454
- const handle = await fsp.open(filePath, 'r');
455
- try {
456
- await handle.read(buffer, 0, bytesToRead, start);
457
- } finally {
458
- await handle.close();
459
- }
460
-
461
- let text = buffer.toString('utf8');
462
- if (start > 0) {
463
- const newlineIndex = text.indexOf('\n');
464
- text = newlineIndex === -1 ? '' : text.slice(newlineIndex + 1);
465
- }
466
- return text;
467
- }
468
-
469
- async function findRecentJsonlFiles(roots, options = {}) {
470
- const limit = options.limit ?? JSONL_FILE_LIMIT;
471
- const maxDepth = options.maxDepth ?? 6;
472
- const scanLimit = options.scanLimit ?? JSONL_SCAN_LIMIT;
473
- const files = [];
474
- let scanned = 0;
475
-
476
- async function walk(dir, depth) {
477
- if (depth > maxDepth || scanned >= scanLimit) return;
478
-
479
- let entries;
480
- try {
481
- entries = await fsp.readdir(dir, { withFileTypes: true });
482
- } catch {
483
- return;
484
- }
485
-
486
- for (const entry of entries) {
487
- if (scanned >= scanLimit) return;
488
- const entryPath = path.join(dir, entry.name);
489
- if (entry.isDirectory()) {
490
- await walk(entryPath, depth + 1);
491
- continue;
492
- }
493
- if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
494
-
495
- scanned += 1;
496
- try {
497
- const stat = await fsp.stat(entryPath);
498
- files.push({ filePath: entryPath, mtimeMs: stat.mtimeMs });
499
- } catch {
500
- // Ignore files that disappear while we scan.
501
- }
502
- }
503
- }
504
-
505
- for (const root of roots) {
506
- await walk(root, 0);
507
- }
508
-
509
- return files
510
- .sort((a, b) => b.mtimeMs - a.mtimeMs)
511
- .slice(0, limit)
512
- .map(file => file.filePath);
513
- }
514
-
515
- async function readJsonlRecords(filePath) {
516
- const text = await readJsonlTail(filePath);
517
- return text
518
- .split(/\r?\n/)
519
- .filter(Boolean)
520
- .map(line => {
521
- try {
522
- return JSON.parse(line);
523
- } catch {
524
- return null;
525
- }
526
- })
527
- .filter(Boolean);
528
- }
529
-
530
- async function isNativeExecutable(filePath) {
531
- try {
532
- const resolved = await fsp.realpath(filePath);
533
- const stat = await fsp.stat(resolved);
534
- if (!stat.isFile()) return false;
535
- const handle = await fsp.open(resolved, 'r');
536
- try {
537
- const header = Buffer.alloc(2);
538
- const { bytesRead } = await handle.read(header, 0, header.length, 0);
539
- return bytesRead === header.length && header.toString('utf8') !== '#!';
540
- } finally {
541
- await handle.close();
542
- }
543
- } catch {
544
- return false;
545
- }
546
- }
547
-
548
- async function codexBundledRipgrepCandidates() {
549
- let codexPackagePath;
550
- try {
551
- codexPackagePath = require.resolve('@openai/codex/package.json');
552
- } catch {
553
- return [];
554
- }
555
- const scopeDir = path.dirname(path.dirname(codexPackagePath));
556
- let packages;
557
- try {
558
- packages = await fsp.readdir(scopeDir, { withFileTypes: true });
559
- } catch {
560
- return [];
561
- }
562
- const executableName = process.platform === 'win32' ? 'rg.exe' : 'rg';
563
- const candidates = [];
564
- for (const entry of packages) {
565
- if (!entry.isDirectory() || !entry.name.startsWith('codex-')) continue;
566
- const vendorDir = path.join(scopeDir, entry.name, 'vendor');
567
- let triples;
568
- try {
569
- triples = await fsp.readdir(vendorDir, { withFileTypes: true });
570
- } catch {
571
- continue;
572
- }
573
- for (const triple of triples) {
574
- if (!triple.isDirectory()) continue;
575
- candidates.push(path.join(vendorDir, triple.name, 'codex-path', executableName));
576
- }
577
- }
578
- return candidates;
579
- }
580
-
581
- async function resolveNativeRipgrepPath() {
582
- if (process.env.FARMING_RG_BIN) return process.env.FARMING_RG_BIN;
583
- if (nativeRipgrepPathPromise) return nativeRipgrepPathPromise;
584
- nativeRipgrepPathPromise = (async () => {
585
- const executableName = process.platform === 'win32' ? 'rg.exe' : 'rg';
586
- const pathCandidates = String(process.env.PATH || '')
587
- .split(path.delimiter)
588
- .filter(Boolean)
589
- .map(dir => path.join(dir, executableName));
590
- const candidates = [
591
- ...pathCandidates,
592
- ...await codexBundledRipgrepCandidates(),
593
- ];
594
- for (const candidate of Array.from(new Set(candidates))) {
595
- if (await isNativeExecutable(candidate)) return candidate;
596
- }
597
- return null;
598
- })();
599
- return nativeRipgrepPathPromise;
600
- }
601
-
602
349
  function localDateKey(timestamp) {
603
350
  const date = new Date(timestamp);
604
351
  if (!Number.isFinite(date.getTime())) return '';
@@ -730,140 +477,6 @@ function buildUsageDayDetail(providerEvents, options = {}) {
730
477
  };
731
478
  }
732
479
 
733
- async function readDailyFileEvents(filePath, provider, minimumMtimeMs = 0) {
734
- const stat = await fsp.stat(filePath);
735
- if (stat.mtimeMs < minimumMtimeMs) return { events: [], mtimeMs: stat.mtimeMs };
736
- const cacheKey = `${provider}:${filePath}`;
737
- const signature = `${stat.size}:${stat.mtimeMs}`;
738
- const cached = dailyFileEventCache.get(cacheKey);
739
- if (cached?.signature === signature) {
740
- return { events: cached.events, mtimeMs: stat.mtimeMs, truncated: cached.truncated };
741
- }
742
-
743
- const records = await readJsonlRecords(filePath);
744
- const events = [];
745
- if (provider === 'codex') {
746
- events.push(...collectCodexTokenDeltas(records, {
747
- now: Date.now(),
748
- windowMs: 1,
749
- includeAllEvents: true,
750
- }).tokenEvents);
751
- } else {
752
- for (const record of records) {
753
- const timestamp = parseTimestampMs(record.timestamp);
754
- if (!timestamp) continue;
755
- for (const usage of claudeUsageObjectsFromRecord(record)) {
756
- const breakdown = tokenBreakdownFromUsage(usage, 'claude');
757
- if (breakdown.totalTokens > 0) events.push({ timestamp, ...breakdown });
758
- }
759
- }
760
- }
761
-
762
- const agentId = usageAgentIdFromFilePath(filePath);
763
- const attributedEvents = events.map(event => attributeUsageEvent(event, provider, agentId));
764
- const truncated = stat.size > JSONL_TAIL_BYTES;
765
- dailyFileEventCache.set(cacheKey, { signature, events: attributedEvents, truncated });
766
- return { events: attributedEvents, mtimeMs: stat.mtimeMs, truncated };
767
- }
768
-
769
- async function collectCodexDailyEventsWithRipgrep(roots, options = {}) {
770
- const existingRoots = [];
771
- for (const root of roots) {
772
- try {
773
- await fsp.access(root);
774
- existingRoots.push(root);
775
- } catch {
776
- // Ignore provider history roots that do not exist yet.
777
- }
778
- }
779
- if (existingRoots.length === 0) return { events: [], partial: false };
780
-
781
- const now = options.now ?? Date.now();
782
- const cutoffMs = options.cutoffMs ?? 0;
783
- const ripgrepPath = options.ripgrepPath || await resolveNativeRipgrepPath();
784
- if (!ripgrepPath) return null;
785
- return new Promise((resolve) => {
786
- const child = spawn(ripgrepPath, [
787
- '-F',
788
- '"type":"token_count"',
789
- '--with-filename',
790
- '--no-heading',
791
- '--no-line-number',
792
- '-g',
793
- '*.jsonl',
794
- ...existingRoots,
795
- ], { stdio: ['ignore', 'pipe', 'pipe'] });
796
- const events = [];
797
- const states = new Map();
798
- let unavailable = false;
799
- child.once('error', () => {
800
- unavailable = true;
801
- });
802
- child.stderr.resume();
803
- const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
804
- lines.on('line', line => {
805
- const separator = line.indexOf(':{');
806
- if (separator <= 0) return;
807
- const filePath = line.slice(0, separator);
808
- let record;
809
- try {
810
- record = JSON.parse(line.slice(separator + 1));
811
- } catch {
812
- return;
813
- }
814
- let state = states.get(filePath);
815
- if (!state) {
816
- state = createCodexDeltaState();
817
- states.set(filePath, state);
818
- }
819
- const event = codexTokenEventFromRecord(record, state);
820
- if (event && event.timestamp >= cutoffMs && event.timestamp <= now + 60_000) {
821
- events.push(attributeUsageEvent(event, 'codex', usageAgentIdFromFilePath(filePath)));
822
- }
823
- });
824
- child.once('close', code => {
825
- if (unavailable || (code !== 0 && code !== 1)) {
826
- resolve(null);
827
- return;
828
- }
829
- resolve({ events, partial: false, source: 'ripgrep token_count scan' });
830
- });
831
- });
832
- }
833
-
834
- async function collectProviderDailyEvents(provider, roots, options = {}) {
835
- const now = options.now ?? Date.now();
836
- const days = Math.max(1, Math.floor(options.days ?? USAGE_DAILY_DAYS));
837
- const cutoff = new Date(now);
838
- cutoff.setHours(0, 0, 0, 0);
839
- cutoff.setDate(cutoff.getDate() - days);
840
- if (provider === 'codex') {
841
- const ripgrepResult = await collectCodexDailyEventsWithRipgrep(roots, {
842
- now,
843
- cutoffMs: cutoff.getTime(),
844
- });
845
- if (ripgrepResult) return ripgrepResult;
846
- }
847
- const fileLimit = options.fileLimit ?? DAILY_JSONL_FILE_LIMIT;
848
- const files = await findRecentJsonlFiles(roots, {
849
- limit: fileLimit,
850
- maxDepth: options.maxDepth ?? 6,
851
- scanLimit: options.scanLimit ?? DAILY_JSONL_SCAN_LIMIT,
852
- });
853
- const events = [];
854
- let truncated = false;
855
- for (const filePath of files) {
856
- const file = await readDailyFileEvents(filePath, provider, cutoff.getTime()).catch(() => null);
857
- if (!file) continue;
858
- events.push(...file.events);
859
- truncated = truncated || file.truncated === true;
860
- }
861
- return {
862
- events,
863
- partial: truncated || files.length >= fileLimit,
864
- };
865
- }
866
-
867
480
  async function defaultOpenCodeCommandRunner(args, options = {}) {
868
481
  const env = { ...process.env };
869
482
  if (options.openCodeHome) env.OPENCODE_CONFIG_DIR = options.openCodeHome;
@@ -895,6 +508,16 @@ function openCodeTokenEventsFromExport(sessionExport, options = {}) {
895
508
  return events;
896
509
  }
897
510
 
511
+ function cacheOpenCodeSessionEvents(cacheKey, value) {
512
+ openCodeSessionEventCache.delete(cacheKey);
513
+ openCodeSessionEventCache.set(cacheKey, value);
514
+ while (openCodeSessionEventCache.size > OPENCODE_SESSION_CACHE_LIMIT) {
515
+ const oldestKey = openCodeSessionEventCache.keys().next().value;
516
+ if (oldestKey === undefined) break;
517
+ openCodeSessionEventCache.delete(oldestKey);
518
+ }
519
+ }
520
+
898
521
  async function collectOpenCodeDailyEvents(homePaths, options = {}) {
899
522
  const now = options.now ?? Date.now();
900
523
  const cutoffMs = options.cutoffMs ?? 0;
@@ -914,6 +537,11 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
914
537
  const listed = JSON.parse(String(result?.stdout || '[]'));
915
538
  if (!Array.isArray(listed)) throw new Error('OpenCode session list was not an array');
916
539
  successfulHomes += 1;
540
+ if (listed.length >= OPENCODE_SESSION_LIMIT) {
541
+ partial = true;
542
+ reason = reason
543
+ || `OpenCode returned the ${OPENCODE_SESSION_LIMIT.toLocaleString('en-US')} session limit; older sessions may be omitted.`;
544
+ }
917
545
  for (const session of listed) {
918
546
  const id = typeof session?.id === 'string' ? session.id.trim() : '';
919
547
  const updatedAt = parseTimestampMs(session?.updated ?? session?.created);
@@ -936,6 +564,7 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
936
564
  const cacheKey = `${session.openCodeHome}:${session.id}`;
937
565
  const cached = openCodeSessionEventCache.get(cacheKey);
938
566
  if (cached?.updatedAt === session.updatedAt) {
567
+ cacheOpenCodeSessionEvents(cacheKey, cached);
939
568
  successfulExports += 1;
940
569
  events.push(...cached.events);
941
570
  continue;
@@ -949,7 +578,10 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
949
578
  const sessionEvents = openCodeTokenEventsFromExport(exported, { cutoffMs, now })
950
579
  .map(event => attributeUsageEvent(event, 'opencode', session.id));
951
580
  successfulExports += 1;
952
- openCodeSessionEventCache.set(cacheKey, { updatedAt: session.updatedAt, events: sessionEvents });
581
+ cacheOpenCodeSessionEvents(
582
+ cacheKey,
583
+ { updatedAt: session.updatedAt, events: sessionEvents },
584
+ );
953
585
  events.push(...sessionEvents);
954
586
  } catch (error) {
955
587
  partial = true;
@@ -965,7 +597,8 @@ async function collectOpenCodeDailyEvents(homePaths, options = {}) {
965
597
  return {
966
598
  events,
967
599
  partial,
968
- available: successfulHomes === homePaths.length
600
+ available: !partial
601
+ && successfulHomes === homePaths.length
969
602
  && (sessions.length === 0 || successfulExports === sessions.length),
970
603
  reason,
971
604
  sessionCount: sessions.length,
@@ -979,16 +612,6 @@ async function collectUsageHistory(options = {}) {
979
612
  const cutoff = new Date(now);
980
613
  cutoff.setHours(0, 0, 0, 0);
981
614
  cutoff.setDate(cutoff.getDate() - days);
982
- const codexHomes = providerHomePaths(
983
- options.providerHomes,
984
- 'codex',
985
- options.codexHome || path.join(os.homedir(), '.codex'),
986
- );
987
- const claudeHomes = providerHomePaths(
988
- options.providerHomes,
989
- 'claude',
990
- options.claudeHome || path.join(os.homedir(), '.claude'),
991
- );
992
615
  const openCodeHomes = providerHomePaths(
993
616
  options.providerHomes,
994
617
  'opencode',
@@ -999,41 +622,41 @@ async function collectUsageHistory(options = {}) {
999
622
  'qoder',
1000
623
  options.qoderHome || path.join(os.homedir(), '.qoder'),
1001
624
  );
1002
- const [codex, claude, openCode] = await Promise.all([
1003
- collectProviderDailyEvents('codex', codexHomes.flatMap(home => [
1004
- path.join(home, 'sessions'),
1005
- path.join(home, 'archived_sessions'),
1006
- ]), { now, days, maxDepth: 6 }),
1007
- collectProviderDailyEvents('claude', claudeHomes.map(home => path.join(home, 'projects')), {
1008
- now,
1009
- days,
1010
- maxDepth: 4,
1011
- }),
625
+ const [ccStatistics, openCode] = await Promise.all([
626
+ collectCCStatistics({ ...options, now, days }),
1012
627
  collectOpenCodeDailyEvents(openCodeHomes, {
1013
628
  now,
1014
629
  cutoffMs: cutoff.getTime(),
1015
630
  openCodeCommandRunner: options.openCodeCommandRunner,
1016
631
  }),
1017
632
  ]);
633
+ const codex = ccStatistics.result.providers.codex;
634
+ const claude = ccStatistics.result.providers.claude;
635
+ const codexEvents = ccStatisticsProviderEvents(ccStatistics.result, 'codex');
636
+ const claudeEvents = ccStatisticsProviderEvents(ccStatistics.result, 'claude');
1018
637
  const providerEvents = {
1019
- codex: codex.events,
1020
- claude: claude.events,
638
+ codex: codexEvents,
639
+ claude: claudeEvents,
1021
640
  opencode: openCode.events,
1022
641
  };
1023
642
  const coverage = [
1024
643
  {
1025
644
  provider: 'codex',
1026
645
  providerName: 'Codex',
1027
- available: true,
1028
- homeCount: codexHomes.length,
1029
- source: codex.source || 'local Codex token_count events',
646
+ available: codex.available === true,
647
+ homeCount: ccStatistics.roots.codexHomes.length,
648
+ fileCount: codex.fileCount,
649
+ source: codex.source || ccStatistics.result.source,
650
+ ...(codex.reason ? { reason: codex.reason } : {}),
1030
651
  },
1031
652
  {
1032
653
  provider: 'claude',
1033
654
  providerName: 'Claude',
1034
- available: true,
1035
- homeCount: claudeHomes.length,
1036
- source: 'local Claude usage fields',
655
+ available: claude.available === true,
656
+ homeCount: ccStatistics.roots.claudeHomes.length,
657
+ fileCount: claude.fileCount,
658
+ source: claude.source || ccStatistics.result.source,
659
+ ...(claude.reason ? { reason: claude.reason } : {}),
1037
660
  },
1038
661
  {
1039
662
  provider: 'opencode',
@@ -1058,8 +681,10 @@ async function collectUsageHistory(options = {}) {
1058
681
  return {
1059
682
  daily: {
1060
683
  ...buildDailyUsage(providerEvents, { now, days }),
1061
- partial: codex.partial || claude.partial || openCode.partial,
684
+ partial: codex.available !== true || claude.available !== true || openCode.partial,
685
+ syncing: ccStatistics.result.cache?.scan_complete === false,
1062
686
  coverage,
687
+ ccStatisticsCache: ccStatistics.result.cache,
1063
688
  },
1064
689
  providerEvents,
1065
690
  coverage,
@@ -1115,115 +740,74 @@ async function collectCodexUsage(options = {}) {
1115
740
  const now = options.now ?? Date.now();
1116
741
  const windowMs = options.windowMs ?? USAGE_WINDOW_MS;
1117
742
  const historyWindowMs = options.historyWindowMs ?? USAGE_TIMELINE_WINDOW_MS;
1118
- const codexHomes = providerHomePaths(
1119
- options.providerHomes,
1120
- 'codex',
1121
- options.codexHome || path.join(os.homedir(), '.codex'),
1122
- );
1123
- const roots = codexHomes.flatMap(home => [
1124
- path.join(home, 'sessions'),
1125
- path.join(home, 'archived_sessions'),
1126
- ]);
1127
- const files = await findRecentJsonlFiles(roots, {
1128
- limit: options.fileLimit ?? JSONL_FILE_LIMIT,
1129
- maxDepth: 6,
1130
- scanLimit: options.scanLimit ?? JSONL_SCAN_LIMIT,
743
+ const ccStatistics = await collectCCStatistics({
744
+ ...options,
745
+ now,
746
+ days: options.days ?? USAGE_DAILY_DAYS,
1131
747
  });
1132
- let totalTokens = 0;
1133
- let eventCount = 0;
1134
- const tokenEvents = [];
748
+ const provider = ccStatistics.result.providers.codex;
749
+ const providerSource = provider.source || ccStatistics.result.source;
750
+ const events = ccStatisticsProviderEvents(ccStatistics.result, 'codex');
1135
751
  let latestQuota = null;
1136
752
  let latestQuotaAt = 0;
1137
753
  let latestOverallQuota = null;
1138
754
  let latestOverallQuotaAt = 0;
1139
755
 
1140
- for (const filePath of files) {
1141
- const records = await readJsonlRecords(filePath).catch(() => []);
1142
- for (const record of records) {
1143
- if (record.type !== 'event_msg' || record.payload?.type !== 'token_count') continue;
1144
- const timestamp = parseTimestampMs(record.timestamp);
1145
- const rateLimits = parseCodexRateLimits(record.payload.rate_limits);
1146
- if (rateLimits && (!timestamp || timestamp >= latestQuotaAt)) {
1147
- latestQuota = rateLimits;
1148
- latestQuotaAt = timestamp ?? latestQuotaAt;
1149
- }
1150
- if (rateLimits && isCodexOverallRateLimit(rateLimits) && (!timestamp || timestamp >= latestOverallQuotaAt)) {
1151
- latestOverallQuota = rateLimits;
1152
- latestOverallQuotaAt = timestamp ?? latestOverallQuotaAt;
1153
- }
1154
-
756
+ for (const candidate of provider.quotaCandidates || []) {
757
+ const timestamp = parseTimestampMs(candidate.timestamp);
758
+ const rateLimits = parseCodexRateLimits(candidate.rateLimits);
759
+ if (rateLimits && (!timestamp || timestamp >= latestQuotaAt)) {
760
+ latestQuota = rateLimits;
761
+ latestQuotaAt = timestamp ?? latestQuotaAt;
762
+ }
763
+ if (rateLimits && isCodexOverallRateLimit(rateLimits) && (!timestamp || timestamp >= latestOverallQuotaAt)) {
764
+ latestOverallQuota = rateLimits;
765
+ latestOverallQuotaAt = timestamp ?? latestOverallQuotaAt;
1155
766
  }
1156
- const fileUsage = collectCodexTokenDeltas(records, { now, windowMs, historyWindowMs });
1157
- totalTokens += fileUsage.totalTokens;
1158
- eventCount += fileUsage.eventCount;
1159
- tokenEvents.push(...fileUsage.tokenEvents);
1160
767
  }
1161
768
  const quota = latestOverallQuota || latestQuota || {
1162
769
  available: false,
1163
- source: 'codex token_count events',
1164
- reason: 'No recent Codex token_count event with rate limits was found.',
770
+ source: providerSource,
771
+ reason: provider.reason || 'No Codex token_count event with rate limits was found.',
1165
772
  };
773
+ const usage = providerUsageFromEvents(events, {
774
+ now,
775
+ windowMs,
776
+ historyWindowMs,
777
+ source: providerSource,
778
+ });
779
+ if (provider.available !== true) {
780
+ usage.tokenUsage.available = false;
781
+ usage.tokenUsage.reason = provider.reason || 'Local usage history is unavailable.';
782
+ }
1166
783
 
1167
784
  return {
1168
785
  quota: attachQuotaForecasts(quota, { now }),
1169
- tokenUsage: tokenUsageSummary({
1170
- totalTokens,
1171
- eventCount,
1172
- source: 'codex cumulative token_count deltas',
1173
- windowMs,
1174
- sampledAt: now,
1175
- }),
1176
- tokenEvents,
786
+ ...usage,
1177
787
  };
1178
788
  }
1179
789
 
1180
- function claudeUsageObjectsFromRecord(record) {
1181
- const objects = [];
1182
- if (record?.message?.usage) objects.push(record.message.usage);
1183
- if (record?.toolUseResult?.usage) objects.push(record.toolUseResult.usage);
1184
- return objects;
1185
- }
1186
-
1187
790
  async function collectClaudeUsage(options = {}) {
1188
791
  const now = options.now ?? Date.now();
1189
792
  const windowMs = options.windowMs ?? USAGE_WINDOW_MS;
1190
793
  const historyWindowMs = options.historyWindowMs ?? USAGE_TIMELINE_WINDOW_MS;
1191
- const claudeHomes = providerHomePaths(
1192
- options.providerHomes,
1193
- 'claude',
1194
- options.claudeHome || path.join(os.homedir(), '.claude'),
1195
- );
1196
- const files = await findRecentJsonlFiles(claudeHomes.map(home => path.join(home, 'projects')), {
1197
- limit: options.fileLimit ?? JSONL_FILE_LIMIT,
1198
- maxDepth: 4,
1199
- scanLimit: options.scanLimit ?? JSONL_SCAN_LIMIT,
794
+ const ccStatistics = await collectCCStatistics({
795
+ ...options,
796
+ now,
797
+ days: options.days ?? USAGE_DAILY_DAYS,
1200
798
  });
1201
- let totalTokens = 0;
1202
- let eventCount = 0;
1203
- const tokenEvents = [];
1204
-
1205
- for (const filePath of files) {
1206
- const records = await readJsonlRecords(filePath).catch(() => []);
1207
- for (const record of records) {
1208
- const timestamp = parseTimestampMs(record.timestamp);
1209
- if (!timestamp || timestamp < now - historyWindowMs || timestamp > now + 60_000) continue;
1210
-
1211
- for (const usage of claudeUsageObjectsFromRecord(record)) {
1212
- const tokenTotal = tokenTotalFromUsage(usage, [
1213
- 'input_tokens',
1214
- 'cache_creation_input_tokens',
1215
- 'cache_read_input_tokens',
1216
- 'output_tokens',
1217
- ]);
1218
- if (tokenTotal > 0) {
1219
- tokenEvents.push({ timestamp, totalTokens: tokenTotal });
1220
- if (timestamp >= now - windowMs) {
1221
- totalTokens += tokenTotal;
1222
- eventCount += 1;
1223
- }
1224
- }
1225
- }
1226
- }
799
+ const provider = ccStatistics.result.providers.claude;
800
+ const providerSource = provider.source || ccStatistics.result.source;
801
+ const events = ccStatisticsProviderEvents(ccStatistics.result, 'claude');
802
+ const usage = providerUsageFromEvents(events, {
803
+ now,
804
+ windowMs,
805
+ historyWindowMs,
806
+ source: providerSource,
807
+ });
808
+ if (provider.available !== true) {
809
+ usage.tokenUsage.available = false;
810
+ usage.tokenUsage.reason = provider.reason || 'Local usage history is unavailable.';
1227
811
  }
1228
812
 
1229
813
  return {
@@ -1232,14 +816,7 @@ async function collectClaudeUsage(options = {}) {
1232
816
  source: 'claude auth status',
1233
817
  reason: 'Claude Code auth/status output does not expose usage remaining.',
1234
818
  },
1235
- tokenUsage: tokenUsageSummary({
1236
- totalTokens,
1237
- eventCount,
1238
- source: 'claude local usage fields',
1239
- windowMs,
1240
- sampledAt: now,
1241
- }),
1242
- tokenEvents,
819
+ ...usage,
1243
820
  };
1244
821
  }
1245
822
 
@@ -1254,6 +831,10 @@ class UsageMonitor {
1254
831
  this.qoderHome = options.qoderHome;
1255
832
  this.getProviderHomes = options.getProviderHomes || null;
1256
833
  this.openCodeCommandRunner = options.openCodeCommandRunner;
834
+ this.configDir = options.configDir || path.join(os.homedir(), '.farming');
835
+ this.ccStatisticsClient = options.usageHistoryClient
836
+ || options.ccStatisticsClient
837
+ || new UsageHistoryClient({ configDir: this.configDir });
1257
838
  this.windowMs = options.windowMs ?? USAGE_WINDOW_MS;
1258
839
  this.dailyDays = options.dailyDays ?? USAGE_DAILY_DAYS;
1259
840
  this.dailyCacheMs = options.dailyCacheMs ?? USAGE_DAILY_CACHE_MS;
@@ -1312,6 +893,8 @@ class UsageMonitor {
1312
893
  qoderHome: this.qoderHome,
1313
894
  providerHomes,
1314
895
  openCodeCommandRunner: this.openCodeCommandRunner,
896
+ configDir: this.configDir,
897
+ ccStatisticsClient: this.ccStatisticsClient,
1315
898
  now,
1316
899
  days: this.dailyDays,
1317
900
  }).then(value => {
@@ -1362,6 +945,8 @@ class UsageMonitor {
1362
945
  qoderHome: this.qoderHome,
1363
946
  providerHomes: this.getProviderHomes ? this.getProviderHomes() : undefined,
1364
947
  openCodeCommandRunner: this.openCodeCommandRunner,
948
+ configDir: this.configDir,
949
+ ccStatisticsClient: this.ccStatisticsClient,
1365
950
  now,
1366
951
  days: 1,
1367
952
  }).then(history => {
@@ -1400,8 +985,26 @@ class UsageMonitor {
1400
985
  ] = await Promise.all([
1401
986
  readCodexAuthStatus(this.commandRunner),
1402
987
  readClaudeAuthStatus(this.commandRunner),
1403
- collectCodexUsage({ codexHome: this.codexHome, providerHomes, now, windowMs, historyWindowMs }),
1404
- collectClaudeUsage({ claudeHome: this.claudeHome, providerHomes, now, windowMs, historyWindowMs }),
988
+ collectCodexUsage({
989
+ codexHome: this.codexHome,
990
+ claudeHome: this.claudeHome,
991
+ providerHomes,
992
+ configDir: this.configDir,
993
+ ccStatisticsClient: this.ccStatisticsClient,
994
+ now,
995
+ windowMs,
996
+ historyWindowMs,
997
+ }),
998
+ collectClaudeUsage({
999
+ codexHome: this.codexHome,
1000
+ claudeHome: this.claudeHome,
1001
+ providerHomes,
1002
+ configDir: this.configDir,
1003
+ ccStatisticsClient: this.ccStatisticsClient,
1004
+ now,
1005
+ windowMs,
1006
+ historyWindowMs,
1007
+ }),
1405
1008
  this.getDailyUsage({ now, force: options.fresh === true }),
1406
1009
  this.systemMonitor?.getSystemStats ? this.systemMonitor.getSystemStats().catch(() => null) : Promise.resolve(null),
1407
1010
  ]);
@@ -1507,10 +1110,7 @@ module.exports = {
1507
1110
  collectClaudeUsage,
1508
1111
  collectCodexUsage,
1509
1112
  collectOpenCodeDailyEvents,
1510
- findRecentJsonlFiles,
1511
1113
  openCodeTokenEventsFromExport,
1512
1114
  readClaudeAuthStatus,
1513
1115
  readCodexAuthStatus,
1514
- collectCodexTokenDeltas,
1515
- tokenTotalFromUsage,
1516
1116
  };