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.
- package/README.md +3 -3
- package/README.zh_cn.md +3 -3
- package/THIRD_PARTY_NOTICES.md +17 -1
- package/backend/acp/packaged-codex-acp.js +13 -0
- package/backend/acp-runtime.js +413 -24
- package/backend/agent-manager.js +572 -557
- package/backend/agent-provider-session.js +15 -1
- package/backend/agent-runtime-binding.js +9 -55
- package/backend/chat-runtime.js +28 -0
- package/backend/codex-rollout-follower.js +83 -0
- package/backend/codex-session-archive.js +12 -1
- package/backend/codex-session-history.js +94 -0
- package/backend/codex-transcript.js +2 -1
- package/backend/config-manager.js +25 -17
- package/backend/control-api.js +3 -1
- package/backend/farming-app-cli.js +21 -0
- package/backend/farming-session-store.js +2 -1
- package/backend/native-pty-host-client.js +59 -2
- package/backend/npm-update-helper.js +65 -7
- package/backend/provider-adapters.js +123 -38
- package/backend/provider-session-service.js +138 -34
- package/backend/server.js +94 -105
- package/backend/storage-layout.js +5 -16
- package/backend/update-service.js +94 -5
- package/backend/usage-history-client.js +305 -0
- package/backend/usage-history-scanner.generated.js +2250 -0
- package/backend/usage-history-scanner.ts +2718 -0
- package/backend/usage-history-smoke.js +64 -0
- package/backend/usage-history-worker.js +34 -0
- package/backend/usage-monitor.js +188 -588
- package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
- package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
- package/backend/vendor/usage-parsers/VENDOR.json +45 -0
- package/dist/acp/LICENSE.codex-acp +190 -0
- package/dist/acp/codex-acp-1.1.4.js +30718 -0
- package/dist/assets/App-6yWdwzj8.js +210 -0
- package/dist/assets/{FileEditorMarkdownPreview-UlVd0O4D.js → FileEditorMarkdownPreview-R0jwwLME.js} +4 -4
- package/dist/assets/FileEditorPane-DNy3LAqS.js +2 -0
- package/dist/assets/{IconGlyphs-AU22gPDY.js → IconGlyphs-BsUfqpKN.js} +1 -1
- package/dist/assets/ProjectFilesSection-BhDEUVHA.js +12 -0
- package/dist/assets/{ReviewPage-CeDKDyZh.js → ReviewPage-ChtByBe3.js} +1 -1
- package/dist/assets/{abnfDiagram-VRR7QNED-BgawzDQT.js → abnfDiagram-VRR7QNED-nnglrXFi.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-DUShwPg0.js → architectureDiagram-ZJ3FMSHR-BEXMaFfY.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-CeGZviXR.js → blockDiagram-677ZJIJ3-1mCezSBk.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-CLhYht6S.js → c4Diagram-LMCZKHZV-DhTiI6Uv.js} +1 -1
- package/dist/assets/channel-BVWNatUB.js +1 -0
- package/dist/assets/{chunk-32BRIVSS-LFYpT-Cu.js → chunk-32BRIVSS-CCdsKd73.js} +1 -1
- package/dist/assets/{chunk-52WLFC77-BH6jDw9J.js → chunk-52WLFC77-Gncjk4LF.js} +1 -1
- package/dist/assets/{chunk-C7G6YPKG-B_mIUlb7.js → chunk-C7G6YPKG-CiDVUP82.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-C63hVuAJ.js → chunk-EX3LRPZG-z6gXP2_N.js} +1 -1
- package/dist/assets/{chunk-FWX5IMBZ-CboX-WF_.js → chunk-FWX5IMBZ-DMZBccQa.js} +2 -2
- package/dist/assets/{chunk-HOUHSVGY-BOgSd6Un.js → chunk-HOUHSVGY-D8tIeoPk.js} +1 -1
- package/dist/assets/{chunk-ICXQ74PX-C0K-pgZt.js → chunk-ICXQ74PX-b6oQ2Q17.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-CVMfwN_7.js → chunk-MOJQB5TN-DAVH_p2a.js} +1 -1
- package/dist/assets/{chunk-OGEWGWER-DLT3pxyw.js → chunk-OGEWGWER-DxoiMCI-.js} +1 -1
- package/dist/assets/{chunk-PUDLZKDR-Cd079yIQ.js → chunk-PUDLZKDR-NsOWh7HM.js} +1 -1
- package/dist/assets/{chunk-Q4XR5HBZ-D0jHa6SO.js → chunk-Q4XR5HBZ-Di4H7L21.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-CVDtR75H.js → chunk-V7JOEXUC-DttpZg9Y.js} +1 -1
- package/dist/assets/{chunk-VAUOI2AC-B-W2_9Ot.js → chunk-VAUOI2AC-UHNr_yhX.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-FD3kWh32.js → chunk-VR4S4FIN-D-Fmtd4Z.js} +1 -1
- package/dist/assets/{chunk-WYO6CB5R-C2bGaS5r.js → chunk-WYO6CB5R-CUxMtfK2.js} +4 -4
- package/dist/assets/{chunk-ZGVPDNZ5-ymgts0zo.js → chunk-ZGVPDNZ5-CoV2QEl0.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Db0ib5iE.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-Bkf69XF9.js +1 -0
- package/dist/assets/code-dark-5esj0bRu.css +1 -0
- package/dist/assets/{cynefinDiagram-TSTJHNR4-CwpobXIl.js → cynefinDiagram-TSTJHNR4-0xPI92Lg.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-Dkhlfjna.js → dagre-VKFMJZFB-DfuMG4gU.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-hA8dicp6.js → diagram-FQU43EPY-CT00bLZ6.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-BTY6JTzO.js → diagram-G47NLZAW-Xbh7zXUx.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-xkaHHX9M.js → diagram-NH7WQ7WH-SRDVPfn7.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-gk_fg9UU.js → diagram-OA4YK3LP-CQRli4Np.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-BvC8xd6T.js → diagram-WEI45ONY-DMf17WQi.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CR7RaNat.js → ebnfDiagram-CCIWWBDH-DhpFkgJ5.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-_M-JFPbj.js → erDiagram-Q63AITRT-1IwxWmJo.js} +1 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +1 -0
- package/dist/assets/flowDiagram-23GEKE2U-DYFo2xmB.js +1 -0
- package/dist/assets/{ganttDiagram-NO4QXBWP-Bl8P1yf_.js → ganttDiagram-NO4QXBWP-BNCdfyr1.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-DoHa5ysz.js → gitGraphDiagram-IHSO6WYX-pbyWZuiX.js} +1 -1
- package/dist/assets/{index-BA-iH2Kg.js → index-C8ZFKye1.js} +2 -2
- package/dist/assets/{infoDiagram-FWYZ7A6U-DSFcSuUs.js → infoDiagram-FWYZ7A6U-BcTOgDbe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-DdkqullI.js → ishikawaDiagram-FXEZZL3T-DCdvGKrn.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-B4hRnQJr.js → journeyDiagram-5HDEW3XC-BZ-1q_IT.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tm2FZB2E.js → kanban-definition-HUTT4EX6-BIgejCjj.js} +1 -1
- package/dist/assets/{line-CmsjK4kn.js → line-CfptFMmX.js} +1 -1
- package/dist/assets/main-DSO3i_Gs.css +1 -0
- package/dist/assets/{mermaid.core-C7UT_EEL.js → mermaid.core-gSpX30pV.js} +3 -3
- package/dist/assets/{mindmap-definition-LN4V7U3C-DplfjGms.js → mindmap-definition-LN4V7U3C-BKBaI4LW.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-lu3SdkQs.js → pegDiagram-2B236MQR-sBvDY3VX.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P-3fHbmDf9.js → pieDiagram-ENE6RG2P-DWlWcu08.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-a5qoWz-X.js → quadrantDiagram-ABIIQ3AL-DZutUd3J.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-D2TOuXW9.js → railroadDiagram-RFXS5EU6-DQb3lXGM.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-LQBWpUvd.js → requirementDiagram-TGXJPOKE-DqFGXu2e.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-B1ohYoyK.js → sankeyDiagram-HTMAVEWB-d7ECahnO.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-C3vM-hMq.js → sequenceDiagram-DBY2YBRQ-DdsbKqct.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-BvFhOryI.js → stateDiagram-2N3HPSRC-P6iSrgwR.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DIYSf_qh.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-DKF_b-2s.js → swimlanes-5IMT3BWC-BGnZJx1M.js} +1 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BIkrlKrZ.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-vAs11ux_.js → timeline-definition-FHXFAJF6-rA-r3aUR.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-BybTX_9B.js → vennDiagram-L72KCM5P-DiVKUjA9.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-CicUmMBR.js → wardleyDiagram-EHGQE667-Cfxh7UOn.js} +1 -1
- package/dist/assets/workspace-editor-model-D0Im34S5.js +1 -0
- package/dist/assets/{workspace-editor-monaco-BICJESG0.js → workspace-editor-monaco-BIpqcWf1.js} +1 -1
- package/dist/assets/workspace-editor-monaco-DLlws5Ig.js +1 -0
- package/dist/assets/{workspace-view-state-DvYG_9PH.js → workspace-view-state-BpPINu84.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-BhWDyCX-.js → xychartDiagram-FW5EYKEG--hmryQeL.js} +1 -1
- package/dist/crt-mermaid-renderer.js +313 -313
- package/dist/index.html +1 -1
- package/frontend/session-bridge.js +24 -2
- package/frontend/skins/crt/app.js +203 -85
- package/frontend/skins/crt/index.html +2 -11
- package/frontend/skins/crt/styles/effects.css +2 -34
- package/package.json +30 -12
- package/shared/backend-connection-status.d.ts +25 -0
- package/shared/backend-connection-status.js +40 -0
- package/shared/browser-protocol.js +3 -3
- package/backend/app-server-api.js +0 -917
- package/backend/codex-app-server-home.js +0 -39
- package/backend/codex-app-server-runtime.js +0 -901
- package/dist/assets/App-Bumi772v.js +0 -206
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +0 -2
- package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +0 -12
- package/dist/assets/channel-Uw90AO0b.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +0 -1
- package/dist/assets/code-dark-DPiuyeSp.css +0 -1
- package/dist/assets/code-mobile-3C2Rr-CN.css +0 -1
- package/dist/assets/file-icons-Bw2qd5iT.js +0 -1
- package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +0 -1
- package/dist/assets/main-Bqtb5kuz.css +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +0 -8
- package/dist/assets/workspace-editor-model-BQol4qbA.js +0 -1
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +0 -1
- package/scripts/sync-ghostty-vendor.js +0 -66
- /package/dist/assets/{core-D0LFJkDt.js → core-B9MwRjDE.js} +0 -0
package/backend/usage-monitor.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
const fsp = require('fs/promises');
|
|
2
1
|
const os = require('os');
|
|
3
2
|
const path = require('path');
|
|
4
|
-
const
|
|
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
|
|
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
|
-
|
|
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:
|
|
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 [
|
|
1003
|
-
|
|
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:
|
|
1020
|
-
claude:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1119
|
-
options
|
|
1120
|
-
|
|
1121
|
-
options.
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
const
|
|
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
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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:
|
|
1164
|
-
reason: 'No
|
|
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
|
-
|
|
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
|
|
1192
|
-
options
|
|
1193
|
-
|
|
1194
|
-
options.
|
|
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
|
-
|
|
1202
|
-
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
-
|
|
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({
|
|
1404
|
-
|
|
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
|
};
|