vibeusage 0.2.8 โ 0.2.10
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 +12 -0
- package/README.zh-CN.md +12 -0
- package/package.json +3 -1
- package/src/commands/init.js +6 -6
- package/src/lib/opencode-config.js +3 -1
- package/src/lib/opencode-usage-audit.js +202 -0
- package/src/lib/rollout.js +97 -11
package/README.md
CHANGED
|
@@ -91,6 +91,8 @@ npx --yes vibeusage status
|
|
|
91
91
|
|
|
92
92
|
- `VIBESCORE_HTTP_TIMEOUT_MS`: CLI HTTP timeout in ms (default `20000`, `0` disables, clamped to `1000..120000`).
|
|
93
93
|
- `VITE_VIBESCORE_HTTP_TIMEOUT_MS`: Dashboard request timeout in ms (default `15000`, `0` disables, clamped to `1000..30000`).
|
|
94
|
+
- `VIBEUSAGE_ROLLUP_ENABLED`: Currently ignored; rollup aggregation is disabled in code until the daily rollup table is deployed.
|
|
95
|
+
- `VIBESCORE_ROLLUP_ENABLED`: Legacy alias for `VIBEUSAGE_ROLLUP_ENABLED` (ignored).
|
|
94
96
|
- `GEMINI_HOME`: Override Gemini CLI home (defaults to `~/.gemini`).
|
|
95
97
|
|
|
96
98
|
## ๐งฐ Troubleshooting
|
|
@@ -170,6 +172,16 @@ npm run validate:copy
|
|
|
170
172
|
npm run smoke
|
|
171
173
|
```
|
|
172
174
|
|
|
175
|
+
### Architecture Canvas Focus
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# Generate a focused canvas for a top-level module
|
|
179
|
+
node scripts/ops/architecture-canvas.cjs --focus src
|
|
180
|
+
|
|
181
|
+
# Alias: --module
|
|
182
|
+
node scripts/ops/architecture-canvas.cjs --module dashboard
|
|
183
|
+
```
|
|
184
|
+
|
|
173
185
|
## ๐ License
|
|
174
186
|
|
|
175
187
|
This project is licensed under the [MIT License](LICENSE).
|
package/README.zh-CN.md
CHANGED
|
@@ -91,6 +91,8 @@ npx --yes vibeusage status
|
|
|
91
91
|
|
|
92
92
|
- `VIBESCORE_HTTP_TIMEOUT_MS`๏ผCLI ่ฏทๆฑ่ถ
ๆถ๏ผๆฏซ็ง๏ผ้ป่ฎค `20000`๏ผ`0` ่กจ็คบๅ
ณ้ญ๏ผ่ๅด `1000..120000`๏ผใ
|
|
93
93
|
- `VITE_VIBESCORE_HTTP_TIMEOUT_MS`๏ผDashboard ่ฏทๆฑ่ถ
ๆถ๏ผๆฏซ็ง๏ผ้ป่ฎค `15000`๏ผ`0` ่กจ็คบๅ
ณ้ญ๏ผ่ๅด `1000..30000`๏ผใ
|
|
94
|
+
- `VIBEUSAGE_ROLLUP_ENABLED`๏ผๅฝๅ่ขซๅฟฝ็ฅ๏ผrollup ่ๅๅจไปฃ็ ๅฑ็ฆ็จ๏ผ็ญๅพ
rollup ่กจ้จ็ฝฒๅฎๆๅๅๆขๅคใ
|
|
95
|
+
- `VIBESCORE_ROLLUP_ENABLED`๏ผ`VIBEUSAGE_ROLLUP_ENABLED` ็ๅ
ผๅฎนๅซๅ๏ผๅๆ ทๆ ๆ๏ผใ
|
|
94
96
|
- `GEMINI_HOME`๏ผ่ฆ็ Gemini CLI ็ home๏ผ้ป่ฎค `~/.gemini`๏ผใ
|
|
95
97
|
|
|
96
98
|
## ๐งฐ ๅธธ่ง้ฎ้ข
|
|
@@ -170,6 +172,16 @@ npm run validate:copy
|
|
|
170
172
|
npm run smoke
|
|
171
173
|
```
|
|
172
174
|
|
|
175
|
+
### ๆถๆ็ปๅธ่็ฆ
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
# ็ๆๆๅฎ้กถๅฑๆจกๅ็่็ฆ็ปๅธ
|
|
179
|
+
node scripts/ops/architecture-canvas.cjs --focus src
|
|
180
|
+
|
|
181
|
+
# ๅซๅ๏ผ--module
|
|
182
|
+
node scripts/ops/architecture-canvas.cjs --module dashboard
|
|
183
|
+
```
|
|
184
|
+
|
|
173
185
|
## ๐ ๅผๆบๅ่ฎฎ
|
|
174
186
|
|
|
175
187
|
ๆฌ้กน็ฎๅบไบ [MIT](LICENSE) ๅ่ฎฎๅผๆบใ
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeusage",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
"copy:pull": "node scripts/copy-sync.cjs pull",
|
|
21
21
|
"copy:push": "node scripts/copy-sync.cjs push",
|
|
22
22
|
"architecture:canvas": "node scripts/ops/architecture-canvas.cjs",
|
|
23
|
+
"architecture:canvas:focus": "node scripts/ops/architecture-canvas.cjs --focus",
|
|
24
|
+
"architecture:canvas:list-modules": "node scripts/ops/architecture-canvas.cjs --list-modules",
|
|
23
25
|
"validate:guardrails": "node scripts/validate-architecture-guardrails.cjs"
|
|
24
26
|
},
|
|
25
27
|
"bin": {
|
package/src/commands/init.js
CHANGED
|
@@ -42,12 +42,12 @@ const {
|
|
|
42
42
|
const { renderLocalReport, renderAuthTransition, renderSuccessBox } = require('../lib/init-flow');
|
|
43
43
|
|
|
44
44
|
const ASCII_LOGO = [
|
|
45
|
-
'โโโ โโโโโโโโโโโโโ
|
|
46
|
-
'โโโ
|
|
47
|
-
'โโโ โโโโโโโโโโโโโโโโโโโโ
|
|
48
|
-
'โโโโ โโโโโโโโโโโโโโโโโโโโโ
|
|
49
|
-
' โโโโโโโ
|
|
50
|
-
' โโโโโ โโโโโโโโโโ
|
|
45
|
+
'โโโ โโโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโ โโโโโโ โโโโโโโ โโโโโโโโ',
|
|
46
|
+
'โโโ โโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโ',
|
|
47
|
+
'โโโ โโโโโโโโโโโโโโโโโโโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโ',
|
|
48
|
+
'โโโโ โโโโโโโโโโโโโโโโโโโโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโ',
|
|
49
|
+
' โโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ',
|
|
50
|
+
' โโโโโ โโโโโโโโโโ โโโโโโโโ โโโโโโโ โโโโโโโโโโโ โโโ โโโโโโโ โโโโโโโโ'
|
|
51
51
|
].join('\n');
|
|
52
52
|
|
|
53
53
|
const DIVIDER = '----------------------------------------------';
|
|
@@ -6,7 +6,7 @@ const { ensureDir } = require('./fs');
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_PLUGIN_NAME = 'vibeusage-tracker.js';
|
|
8
8
|
const PLUGIN_MARKER = 'VIBEUSAGE_TRACKER_PLUGIN';
|
|
9
|
-
const DEFAULT_EVENT = 'session.
|
|
9
|
+
const DEFAULT_EVENT = 'session.updated';
|
|
10
10
|
|
|
11
11
|
function resolveOpencodeConfigDir({ home = os.homedir(), env = process.env } = {}) {
|
|
12
12
|
const explicit = typeof env.OPENCODE_CONFIG_DIR === 'string' ? env.OPENCODE_CONFIG_DIR.trim() : '';
|
|
@@ -88,7 +88,9 @@ function hasPluginMarker(text) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
module.exports = {
|
|
91
|
+
DEFAULT_EVENT,
|
|
91
92
|
DEFAULT_PLUGIN_NAME,
|
|
93
|
+
PLUGIN_MARKER,
|
|
92
94
|
resolveOpencodeConfigDir,
|
|
93
95
|
resolveOpencodePluginDir,
|
|
94
96
|
buildOpencodePlugin,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { listOpencodeMessageFiles, parseOpencodeIncremental } = require('./rollout');
|
|
6
|
+
|
|
7
|
+
const BUCKET_SEPARATOR = '|';
|
|
8
|
+
const DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
9
|
+
|
|
10
|
+
function formatHourKey(date) {
|
|
11
|
+
return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(
|
|
12
|
+
date.getUTCDate()
|
|
13
|
+
).padStart(2, '0')}T${String(date.getUTCHours()).padStart(2, '0')}:${String(
|
|
14
|
+
date.getUTCMinutes() >= 30 ? 30 : 0
|
|
15
|
+
).padStart(2, '0')}:00`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toBig(value) {
|
|
19
|
+
if (typeof value === 'bigint') return value;
|
|
20
|
+
if (typeof value === 'number') return BigInt(value);
|
|
21
|
+
if (typeof value === 'string' && value.trim()) return BigInt(value);
|
|
22
|
+
return 0n;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addTotals(target, delta) {
|
|
26
|
+
target.input_tokens += toBig(delta.input_tokens);
|
|
27
|
+
target.cached_input_tokens += toBig(delta.cached_input_tokens);
|
|
28
|
+
target.output_tokens += toBig(delta.output_tokens);
|
|
29
|
+
target.reasoning_output_tokens += toBig(delta.reasoning_output_tokens);
|
|
30
|
+
target.total_tokens += toBig(delta.total_tokens);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function buildLocalHourlyTotals({ storageDir, source = 'opencode' }) {
|
|
34
|
+
const messageFiles = await listOpencodeMessageFiles(storageDir);
|
|
35
|
+
const queuePath = path.join(os.tmpdir(), `vibeusage-opencode-audit-${process.pid}-${Date.now()}.jsonl`);
|
|
36
|
+
const cursors = { version: 1, files: {}, hourly: null, opencode: null };
|
|
37
|
+
|
|
38
|
+
await parseOpencodeIncremental({ messageFiles, cursors, queuePath, source });
|
|
39
|
+
await fs.rm(queuePath, { force: true }).catch(() => {});
|
|
40
|
+
|
|
41
|
+
const byHour = new Map();
|
|
42
|
+
let minDay = null;
|
|
43
|
+
let maxDay = null;
|
|
44
|
+
|
|
45
|
+
for (const [key, bucket] of Object.entries(cursors.hourly?.buckets || {})) {
|
|
46
|
+
const [bucketSource, , hourStart] = String(key).split(BUCKET_SEPARATOR);
|
|
47
|
+
if (bucketSource !== source || !hourStart) continue;
|
|
48
|
+
const dt = new Date(hourStart);
|
|
49
|
+
if (!Number.isFinite(dt.getTime())) continue;
|
|
50
|
+
const hourKey = formatHourKey(dt);
|
|
51
|
+
const dayKey = hourKey.slice(0, 10);
|
|
52
|
+
|
|
53
|
+
if (!minDay || dayKey < minDay) minDay = dayKey;
|
|
54
|
+
if (!maxDay || dayKey > maxDay) maxDay = dayKey;
|
|
55
|
+
|
|
56
|
+
const totals = byHour.get(hourKey) || {
|
|
57
|
+
input_tokens: 0n,
|
|
58
|
+
cached_input_tokens: 0n,
|
|
59
|
+
output_tokens: 0n,
|
|
60
|
+
reasoning_output_tokens: 0n,
|
|
61
|
+
total_tokens: 0n
|
|
62
|
+
};
|
|
63
|
+
addTotals(totals, bucket.totals || {});
|
|
64
|
+
byHour.set(hourKey, totals);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { byHour, minDay, maxDay };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeServerRows(rows) {
|
|
71
|
+
const map = new Map();
|
|
72
|
+
for (const row of rows || []) {
|
|
73
|
+
if (!row || !row.hour) continue;
|
|
74
|
+
map.set(row.hour, {
|
|
75
|
+
missing: Boolean(row.missing),
|
|
76
|
+
totals: {
|
|
77
|
+
input_tokens: toBig(row.input_tokens),
|
|
78
|
+
cached_input_tokens: toBig(row.cached_input_tokens),
|
|
79
|
+
output_tokens: toBig(row.output_tokens),
|
|
80
|
+
reasoning_output_tokens: toBig(row.reasoning_output_tokens),
|
|
81
|
+
total_tokens: toBig(row.total_tokens)
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return map;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function diffTotals(local, server) {
|
|
89
|
+
return {
|
|
90
|
+
input_tokens: local.input_tokens - server.input_tokens,
|
|
91
|
+
cached_input_tokens: local.cached_input_tokens - server.cached_input_tokens,
|
|
92
|
+
output_tokens: local.output_tokens - server.output_tokens,
|
|
93
|
+
reasoning_output_tokens: local.reasoning_output_tokens - server.reasoning_output_tokens,
|
|
94
|
+
total_tokens: local.total_tokens - server.total_tokens
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function maxAbsDelta(delta) {
|
|
99
|
+
return [
|
|
100
|
+
delta.input_tokens,
|
|
101
|
+
delta.cached_input_tokens,
|
|
102
|
+
delta.output_tokens,
|
|
103
|
+
delta.reasoning_output_tokens,
|
|
104
|
+
delta.total_tokens
|
|
105
|
+
].reduce((acc, value) => {
|
|
106
|
+
const abs = value < 0n ? -value : value;
|
|
107
|
+
return abs > acc ? abs : acc;
|
|
108
|
+
}, 0n);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isValidDay(value) {
|
|
112
|
+
if (!DAY_RE.test(value)) return false;
|
|
113
|
+
const dt = new Date(`${value}T00:00:00.000Z`);
|
|
114
|
+
return Number.isFinite(dt.getTime());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function listDays(from, to) {
|
|
118
|
+
if (!isValidDay(from) || !isValidDay(to)) return [];
|
|
119
|
+
if (from > to) return [];
|
|
120
|
+
const out = [];
|
|
121
|
+
const start = new Date(`${from}T00:00:00.000Z`);
|
|
122
|
+
const end = new Date(`${to}T00:00:00.000Z`);
|
|
123
|
+
for (let dt = start; dt <= end; dt = new Date(dt.getTime() + 24 * 60 * 60 * 1000)) {
|
|
124
|
+
const day = `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, '0')}-${String(
|
|
125
|
+
dt.getUTCDate()
|
|
126
|
+
).padStart(2, '0')}`;
|
|
127
|
+
out.push(day);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function auditOpencodeUsage({ storageDir, from, to, fetchHourly, includeMissing = false }) {
|
|
133
|
+
const local = await buildLocalHourlyTotals({ storageDir, source: 'opencode' });
|
|
134
|
+
if (!local.minDay || !local.maxDay) {
|
|
135
|
+
throw new Error('No local opencode data found');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fromDay = from || local.minDay;
|
|
139
|
+
const toDay = to || local.maxDay;
|
|
140
|
+
const days = listDays(fromDay, toDay);
|
|
141
|
+
if (days.length === 0) {
|
|
142
|
+
throw new Error('Invalid date range for audit');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const diffs = [];
|
|
146
|
+
let matched = 0;
|
|
147
|
+
let mismatched = 0;
|
|
148
|
+
let incomplete = 0;
|
|
149
|
+
let maxDelta = 0n;
|
|
150
|
+
|
|
151
|
+
for (const day of days) {
|
|
152
|
+
const server = await fetchHourly(day);
|
|
153
|
+
const serverByHour = normalizeServerRows(server?.data || []);
|
|
154
|
+
for (let h = 0; h < 24; h++) {
|
|
155
|
+
for (const m of [0, 30]) {
|
|
156
|
+
const hourKey = `${day}T${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:00`;
|
|
157
|
+
const localTotals = local.byHour.get(hourKey) || {
|
|
158
|
+
input_tokens: 0n,
|
|
159
|
+
cached_input_tokens: 0n,
|
|
160
|
+
output_tokens: 0n,
|
|
161
|
+
reasoning_output_tokens: 0n,
|
|
162
|
+
total_tokens: 0n
|
|
163
|
+
};
|
|
164
|
+
const serverEntry = serverByHour.get(hourKey) || null;
|
|
165
|
+
if (serverEntry?.missing && !includeMissing) {
|
|
166
|
+
incomplete += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const serverTotals = serverEntry?.totals || {
|
|
170
|
+
input_tokens: 0n,
|
|
171
|
+
cached_input_tokens: 0n,
|
|
172
|
+
output_tokens: 0n,
|
|
173
|
+
reasoning_output_tokens: 0n,
|
|
174
|
+
total_tokens: 0n
|
|
175
|
+
};
|
|
176
|
+
const delta = diffTotals(localTotals, serverTotals);
|
|
177
|
+
const deltaMax = maxAbsDelta(delta);
|
|
178
|
+
if (deltaMax === 0n) {
|
|
179
|
+
matched += 1;
|
|
180
|
+
} else {
|
|
181
|
+
mismatched += 1;
|
|
182
|
+
if (deltaMax > maxDelta) maxDelta = deltaMax;
|
|
183
|
+
diffs.push({ hour: hourKey, local: localTotals, server: serverTotals, delta });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
summary: {
|
|
191
|
+
days: days.length,
|
|
192
|
+
slots: days.length * 48,
|
|
193
|
+
matched,
|
|
194
|
+
mismatched,
|
|
195
|
+
incomplete,
|
|
196
|
+
maxDelta
|
|
197
|
+
},
|
|
198
|
+
diffs
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { auditOpencodeUsage, buildLocalHourlyTotals };
|
package/src/lib/rollout.js
CHANGED
|
@@ -287,6 +287,8 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
287
287
|
const files = Array.isArray(messageFiles) ? messageFiles : [];
|
|
288
288
|
const totalFiles = files.length;
|
|
289
289
|
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
290
|
+
const opencodeState = normalizeOpencodeState(cursors?.opencode);
|
|
291
|
+
const messageIndex = opencodeState.messages;
|
|
290
292
|
const touchedBuckets = new Set();
|
|
291
293
|
const defaultSource = normalizeSourceInput(source) || 'opencode';
|
|
292
294
|
|
|
@@ -324,10 +326,14 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
324
326
|
continue;
|
|
325
327
|
}
|
|
326
328
|
|
|
327
|
-
const
|
|
329
|
+
const fallbackTotals = prev && typeof prev.lastTotals === 'object' ? prev.lastTotals : null;
|
|
330
|
+
const fallbackMessageKey =
|
|
331
|
+
prev && typeof prev.messageKey === 'string' && prev.messageKey.trim() ? prev.messageKey.trim() : null;
|
|
328
332
|
const result = await parseOpencodeMessageFile({
|
|
329
333
|
filePath,
|
|
330
|
-
|
|
334
|
+
messageIndex,
|
|
335
|
+
fallbackTotals,
|
|
336
|
+
fallbackMessageKey,
|
|
331
337
|
hourlyState,
|
|
332
338
|
touchedBuckets,
|
|
333
339
|
source: fileSource
|
|
@@ -338,12 +344,20 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
338
344
|
size,
|
|
339
345
|
mtimeMs,
|
|
340
346
|
lastTotals: result.lastTotals,
|
|
347
|
+
messageKey: result.messageKey || null,
|
|
341
348
|
updatedAt: new Date().toISOString()
|
|
342
349
|
};
|
|
343
350
|
|
|
344
351
|
filesProcessed += 1;
|
|
345
352
|
eventsAggregated += result.eventsAggregated;
|
|
346
353
|
|
|
354
|
+
if (result.messageKey && result.shouldUpdate) {
|
|
355
|
+
messageIndex[result.messageKey] = {
|
|
356
|
+
lastTotals: result.lastTotals,
|
|
357
|
+
updatedAt: new Date().toISOString()
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
347
361
|
if (cb) {
|
|
348
362
|
cb({
|
|
349
363
|
index: idx + 1,
|
|
@@ -359,6 +373,8 @@ async function parseOpencodeIncremental({ messageFiles, cursors, queuePath, onPr
|
|
|
359
373
|
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
360
374
|
hourlyState.updatedAt = new Date().toISOString();
|
|
361
375
|
cursors.hourly = hourlyState;
|
|
376
|
+
opencodeState.updatedAt = new Date().toISOString();
|
|
377
|
+
cursors.opencode = opencodeState;
|
|
362
378
|
|
|
363
379
|
return { filesProcessed, eventsAggregated, bucketsQueued };
|
|
364
380
|
}
|
|
@@ -550,35 +566,84 @@ async function parseGeminiFile({
|
|
|
550
566
|
};
|
|
551
567
|
}
|
|
552
568
|
|
|
553
|
-
async function parseOpencodeMessageFile({
|
|
569
|
+
async function parseOpencodeMessageFile({
|
|
570
|
+
filePath,
|
|
571
|
+
messageIndex,
|
|
572
|
+
fallbackTotals,
|
|
573
|
+
fallbackMessageKey,
|
|
574
|
+
hourlyState,
|
|
575
|
+
touchedBuckets,
|
|
576
|
+
source
|
|
577
|
+
}) {
|
|
578
|
+
const fallbackKey =
|
|
579
|
+
typeof fallbackMessageKey === 'string' && fallbackMessageKey.trim() ? fallbackMessageKey.trim() : null;
|
|
580
|
+
const legacyTotals = fallbackTotals && typeof fallbackTotals === 'object' ? fallbackTotals : null;
|
|
581
|
+
const fallbackEntry = messageIndex && fallbackKey ? messageIndex[fallbackKey] : null;
|
|
582
|
+
const fallbackLastTotals =
|
|
583
|
+
fallbackEntry && typeof fallbackEntry.lastTotals === 'object' ? fallbackEntry.lastTotals : legacyTotals;
|
|
584
|
+
|
|
554
585
|
const raw = await fs.readFile(filePath, 'utf8').catch(() => '');
|
|
555
|
-
if (!raw.trim())
|
|
586
|
+
if (!raw.trim()) {
|
|
587
|
+
return {
|
|
588
|
+
messageKey: fallbackKey,
|
|
589
|
+
lastTotals: fallbackLastTotals,
|
|
590
|
+
eventsAggregated: 0,
|
|
591
|
+
shouldUpdate: false
|
|
592
|
+
};
|
|
593
|
+
}
|
|
556
594
|
|
|
557
595
|
let msg;
|
|
558
596
|
try {
|
|
559
597
|
msg = JSON.parse(raw);
|
|
560
598
|
} catch (_e) {
|
|
561
|
-
return {
|
|
599
|
+
return {
|
|
600
|
+
messageKey: fallbackKey,
|
|
601
|
+
lastTotals: fallbackLastTotals,
|
|
602
|
+
eventsAggregated: 0,
|
|
603
|
+
shouldUpdate: false
|
|
604
|
+
};
|
|
562
605
|
}
|
|
563
606
|
|
|
607
|
+
const messageKey = deriveOpencodeMessageKey(msg, filePath);
|
|
608
|
+
const prev = messageIndex && messageKey ? messageIndex[messageKey] : null;
|
|
609
|
+
const lastTotals = prev && typeof prev.lastTotals === 'object' ? prev.lastTotals : legacyTotals;
|
|
610
|
+
|
|
564
611
|
const currentTotals = normalizeOpencodeTokens(msg?.tokens);
|
|
565
|
-
if (!currentTotals)
|
|
612
|
+
if (!currentTotals) {
|
|
613
|
+
return { messageKey, lastTotals, eventsAggregated: 0, shouldUpdate: false };
|
|
614
|
+
}
|
|
566
615
|
|
|
567
616
|
const delta = diffGeminiTotals(currentTotals, lastTotals);
|
|
568
|
-
if (!delta || isAllZeroUsage(delta))
|
|
617
|
+
if (!delta || isAllZeroUsage(delta)) {
|
|
618
|
+
return { messageKey, lastTotals: currentTotals, eventsAggregated: 0, shouldUpdate: true };
|
|
619
|
+
}
|
|
569
620
|
|
|
570
621
|
const timestampMs = coerceEpochMs(msg?.time?.completed) || coerceEpochMs(msg?.time?.created);
|
|
571
|
-
if (!timestampMs)
|
|
622
|
+
if (!timestampMs) {
|
|
623
|
+
return {
|
|
624
|
+
messageKey,
|
|
625
|
+
lastTotals,
|
|
626
|
+
eventsAggregated: 0,
|
|
627
|
+
shouldUpdate: Boolean(lastTotals)
|
|
628
|
+
};
|
|
629
|
+
}
|
|
572
630
|
|
|
573
631
|
const tsIso = new Date(timestampMs).toISOString();
|
|
574
632
|
const bucketStart = toUtcHalfHourStart(tsIso);
|
|
575
|
-
if (!bucketStart)
|
|
633
|
+
if (!bucketStart) {
|
|
634
|
+
return {
|
|
635
|
+
messageKey,
|
|
636
|
+
lastTotals,
|
|
637
|
+
eventsAggregated: 0,
|
|
638
|
+
shouldUpdate: Boolean(lastTotals)
|
|
639
|
+
};
|
|
640
|
+
}
|
|
576
641
|
|
|
577
|
-
const model = normalizeModelInput(msg?.modelID) || DEFAULT_MODEL;
|
|
642
|
+
const model = normalizeModelInput(msg?.modelID || msg?.model || msg?.modelId) || DEFAULT_MODEL;
|
|
578
643
|
const bucket = getHourlyBucket(hourlyState, source, model, bucketStart);
|
|
579
644
|
addTotals(bucket.totals, delta);
|
|
580
645
|
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
581
|
-
return { lastTotals: currentTotals, eventsAggregated: 1 };
|
|
646
|
+
return { messageKey, lastTotals: currentTotals, eventsAggregated: 1, shouldUpdate: true };
|
|
582
647
|
}
|
|
583
648
|
|
|
584
649
|
async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets }) {
|
|
@@ -982,6 +1047,27 @@ function normalizeHourlyState(raw) {
|
|
|
982
1047
|
};
|
|
983
1048
|
}
|
|
984
1049
|
|
|
1050
|
+
function normalizeOpencodeState(raw) {
|
|
1051
|
+
const state = raw && typeof raw === 'object' ? raw : {};
|
|
1052
|
+
const messages = state.messages && typeof state.messages === 'object' ? state.messages : {};
|
|
1053
|
+
return {
|
|
1054
|
+
messages,
|
|
1055
|
+
updatedAt: typeof state.updatedAt === 'string' ? state.updatedAt : null
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function normalizeMessageKeyPart(value) {
|
|
1060
|
+
if (typeof value !== 'string') return '';
|
|
1061
|
+
return value.trim();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function deriveOpencodeMessageKey(msg, fallback) {
|
|
1065
|
+
const sessionId = normalizeMessageKeyPart(msg?.sessionID || msg?.sessionId || msg?.session_id);
|
|
1066
|
+
const messageId = normalizeMessageKeyPart(msg?.id || msg?.messageID || msg?.messageId);
|
|
1067
|
+
if (sessionId && messageId) return `${sessionId}|${messageId}`;
|
|
1068
|
+
return fallback;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
985
1071
|
function getHourlyBucket(state, source, model, hourStart) {
|
|
986
1072
|
const buckets = state.buckets;
|
|
987
1073
|
const normalizedSource = normalizeSourceInput(source) || DEFAULT_SOURCE;
|