teamshare-bridge 0.26.20 → 0.26.21
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/dist/bridge/daemon.js +60 -0
- package/dist/bridge/daemon.js.map +1 -1
- package/dist/bridge/local-server.js +29 -11
- package/dist/bridge/local-server.js.map +1 -1
- package/dist/bridge/session-relay.d.ts +14 -0
- package/dist/bridge/session-relay.js +53 -2
- package/dist/bridge/session-relay.js.map +1 -1
- package/dist/cli/commands/research.d.ts +7 -3
- package/dist/cli/commands/research.js +67 -15
- package/dist/cli/commands/research.js.map +1 -1
- package/dist/cli/commands/usage.js +85 -32
- package/dist/cli/commands/usage.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/lib/brief.js +3 -2
- package/dist/lib/brief.js.map +1 -1
- package/dist/lib/browser/batch.d.ts +23 -0
- package/dist/lib/browser/batch.js +33 -0
- package/dist/lib/browser/batch.js.map +1 -1
- package/dist/lib/browser/manager.d.ts +1 -0
- package/dist/lib/browser/manager.js +1 -0
- package/dist/lib/browser/manager.js.map +1 -1
- package/dist/lib/browser/network-log.d.ts +28 -3
- package/dist/lib/browser/network-log.js +74 -2
- package/dist/lib/browser/network-log.js.map +1 -1
- package/dist/lib/browser/profile-sweep.d.ts +15 -0
- package/dist/lib/browser/profile-sweep.js +69 -0
- package/dist/lib/browser/profile-sweep.js.map +1 -0
- package/dist/lib/browser/screen-change.d.ts +5 -0
- package/dist/lib/browser/screen-change.js +22 -1
- package/dist/lib/browser/screen-change.js.map +1 -1
- package/dist/lib/browser/session-pool.d.ts +40 -0
- package/dist/lib/browser/session-pool.js +62 -1
- package/dist/lib/browser/session-pool.js.map +1 -1
- package/dist/lib/browser/session.d.ts +51 -8
- package/dist/lib/browser/session.js +144 -15
- package/dist/lib/browser/session.js.map +1 -1
- package/dist/lib/llm/usage-reader.d.ts +91 -4
- package/dist/lib/llm/usage-reader.js +750 -111
- package/dist/lib/llm/usage-reader.js.map +1 -1
- package/dist/lib/llm/usage.d.ts +9 -9
- package/dist/lib/llm/usage.js +9 -8
- package/dist/lib/llm/usage.js.map +1 -1
- package/dist/lib/orchestrator/handlers/browser.js +342 -56
- package/dist/lib/orchestrator/handlers/browser.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,40 +1,141 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUPPORTED_USAGE_HARNESSES = exports.USAGE_HARNESSES = void 0;
|
|
4
|
+
exports.todayLocalDay = todayLocalDay;
|
|
5
|
+
exports.dayBounds = dayBounds;
|
|
6
|
+
exports.estimateModelCost = estimateModelCost;
|
|
7
|
+
exports.readOpencodeSessions = readOpencodeSessions;
|
|
8
|
+
exports.readClaudeSessions = readClaudeSessions;
|
|
9
|
+
exports.readCodexSessions = readCodexSessions;
|
|
3
10
|
exports.readUsageReport = readUsageReport;
|
|
4
11
|
/**
|
|
5
|
-
* Usage report reader — reads cost data directly from harness
|
|
12
|
+
* Usage report reader — reads cost data directly from harness-local stores.
|
|
6
13
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - claude
|
|
14
|
+
* Each supported harness keeps its own ledger on this machine:
|
|
15
|
+
* - opencode: ~/.local/share/opencode/opencode.db (`session_v2` table;
|
|
16
|
+
* the legacy `session` table stopped at V2 and is only a fallback).
|
|
17
|
+
* Cost + all token counts are recorded per session.
|
|
18
|
+
* - claude: ~/.claude/projects (jsonl transcripts holding `message.usage`
|
|
19
|
+
* per assistant message). Tokens are real; cost is ESTIMATED from
|
|
20
|
+
* catalogue (`~/.cache/opencode/models.json`).
|
|
21
|
+
* - codex: ~/.codex/sessions (rollout-*.jsonl files holding `token_count`
|
|
22
|
+
* events with `last_token_usage`). Tokens are real; cost is ESTIMATED the
|
|
23
|
+
* same way. Only `last_token_usage` deltas are summed — `total_token_usage`
|
|
24
|
+
* is cumulative per session and summing it would massively overcount.
|
|
25
|
+
*
|
|
26
|
+
* Every other harness the bridge knows (gemini, antigravity, copilot, cursor,
|
|
27
|
+
* goose, openhands, aider, cline, openrouter) keeps no local token/cost data,
|
|
28
|
+
* so it is listed as unavailable with the reason instead of a silent zero.
|
|
12
29
|
*
|
|
13
30
|
* The report is fully offline, needs neither the backend nor the daemon.
|
|
14
31
|
*/
|
|
15
32
|
const node_fs_1 = require("node:fs");
|
|
16
33
|
const node_os_1 = require("node:os");
|
|
17
34
|
const node_path_1 = require("node:path");
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
exports.USAGE_HARNESSES = [
|
|
36
|
+
{
|
|
37
|
+
id: 'opencode',
|
|
38
|
+
label: 'Opencode',
|
|
39
|
+
supported: true,
|
|
40
|
+
costSource: 'recorded',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'claude',
|
|
44
|
+
label: 'Claude Code',
|
|
45
|
+
supported: true,
|
|
46
|
+
costSource: 'estimated',
|
|
47
|
+
},
|
|
48
|
+
{ id: 'codex', label: 'Codex', supported: true, costSource: 'estimated' },
|
|
49
|
+
{
|
|
50
|
+
id: 'gemini',
|
|
51
|
+
label: 'Gemini CLI',
|
|
52
|
+
supported: false,
|
|
53
|
+
costSource: 'unavailable',
|
|
54
|
+
unavailableReason: 'Gemini CLI keeps chat logs only — no token or cost data is stored locally',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'antigravity',
|
|
58
|
+
label: 'Antigravity',
|
|
59
|
+
supported: false,
|
|
60
|
+
costSource: 'unavailable',
|
|
61
|
+
unavailableReason: 'Antigravity transcripts record steps only — no token or cost data',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'copilot',
|
|
65
|
+
label: 'Copilot CLI',
|
|
66
|
+
supported: false,
|
|
67
|
+
costSource: 'unavailable',
|
|
68
|
+
unavailableReason: 'Copilot CLI has no local usage store',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'cursor',
|
|
72
|
+
label: 'Cursor',
|
|
73
|
+
supported: false,
|
|
74
|
+
costSource: 'unavailable',
|
|
75
|
+
unavailableReason: 'Cursor usage lives in the Cursor dashboard — no local store',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'goose',
|
|
79
|
+
label: 'Goose',
|
|
80
|
+
supported: false,
|
|
81
|
+
costSource: 'unavailable',
|
|
82
|
+
unavailableReason: 'Goose has no local usage store on this machine',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'openhands',
|
|
86
|
+
label: 'OpenHands',
|
|
87
|
+
supported: false,
|
|
88
|
+
costSource: 'unavailable',
|
|
89
|
+
unavailableReason: 'OpenHands has no local usage store on this machine',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'aider',
|
|
93
|
+
label: 'Aider',
|
|
94
|
+
supported: false,
|
|
95
|
+
costSource: 'unavailable',
|
|
96
|
+
unavailableReason: 'Aider has no local usage store',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'cline',
|
|
100
|
+
label: 'Cline',
|
|
101
|
+
supported: false,
|
|
102
|
+
costSource: 'unavailable',
|
|
103
|
+
unavailableReason: 'Cline session files record provider and model but no token counts',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'openrouter',
|
|
107
|
+
label: 'OpenRouter',
|
|
108
|
+
supported: false,
|
|
109
|
+
costSource: 'unavailable',
|
|
110
|
+
unavailableReason: 'OpenRouter is a hosted gateway — usage lives on its dashboard',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
exports.SUPPORTED_USAGE_HARNESSES = exports.USAGE_HARNESSES.filter((h) => h.supported).map((h) => h.id);
|
|
114
|
+
function harnessDef(id) {
|
|
115
|
+
return exports.USAGE_HARNESSES.find((h) => h.id === id);
|
|
116
|
+
}
|
|
117
|
+
// ── Paths ───────────────────────────────────────────────────────────────────
|
|
118
|
+
function opencodeDbPath(home) {
|
|
119
|
+
return (0, node_path_1.join)(home, '.local', 'share', 'opencode', 'opencode.db');
|
|
120
|
+
}
|
|
121
|
+
function claudeProjectsPath(home) {
|
|
122
|
+
return (0, node_path_1.join)(home, '.claude', 'projects');
|
|
34
123
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
124
|
+
function codexSessionsPath(home) {
|
|
125
|
+
return (0, node_path_1.join)(home, '.codex', 'sessions');
|
|
126
|
+
}
|
|
127
|
+
function codexIndexPath(home) {
|
|
128
|
+
return (0, node_path_1.join)(home, '.codex', 'session_index.jsonl');
|
|
129
|
+
}
|
|
130
|
+
function modelsCatalogPath(home) {
|
|
131
|
+
return (0, node_path_1.join)(home, '.cache', 'opencode', 'models.json');
|
|
132
|
+
}
|
|
133
|
+
// ── Day bounds (local midnight ms-epoch) ────────────────────────────────────
|
|
134
|
+
function todayLocalDay() {
|
|
135
|
+
const now = new Date();
|
|
136
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
137
|
+
const date = String(now.getDate()).padStart(2, '0');
|
|
138
|
+
return `${now.getFullYear()}-${month}-${date}`;
|
|
38
139
|
}
|
|
39
140
|
function dayBounds(day) {
|
|
40
141
|
const d = new Date(`${day}T00:00:00`);
|
|
@@ -43,6 +144,113 @@ function dayBounds(day) {
|
|
|
43
144
|
const end = new Date(start.getTime() + 86_400_000);
|
|
44
145
|
return { startMs: start.getTime(), endMs: end.getTime() };
|
|
45
146
|
}
|
|
147
|
+
let priceCache = null;
|
|
148
|
+
function stripDateSuffix(id) {
|
|
149
|
+
return id.replace(/-20\d{6}(-\d+)?$/, '').replace(/-\d{4}-\d{2}-\d{2}$/, '');
|
|
150
|
+
}
|
|
151
|
+
function toPrice(raw, fallbackOutput) {
|
|
152
|
+
if (!raw || typeof raw !== 'object')
|
|
153
|
+
return null;
|
|
154
|
+
const c = raw;
|
|
155
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
156
|
+
// Catalogue rates are per MTok USD.
|
|
157
|
+
return {
|
|
158
|
+
input: num(c.input),
|
|
159
|
+
output: num(c.output) || fallbackOutput,
|
|
160
|
+
cacheRead: num(c.cache_read ?? c.cacheRead),
|
|
161
|
+
cacheWrite: num(c.cache_write ?? c.cacheWrite),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function loadPricing(home) {
|
|
165
|
+
const path = modelsCatalogPath(home);
|
|
166
|
+
let mtimeMs = 0;
|
|
167
|
+
try {
|
|
168
|
+
mtimeMs = (0, node_fs_1.statSync)(path).mtimeMs;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
priceCache = { home, mtimeMs: -1, map: new Map() };
|
|
172
|
+
return priceCache.map;
|
|
173
|
+
}
|
|
174
|
+
if (priceCache &&
|
|
175
|
+
priceCache.home === home &&
|
|
176
|
+
priceCache.mtimeMs === mtimeMs) {
|
|
177
|
+
return priceCache.map;
|
|
178
|
+
}
|
|
179
|
+
const map = new Map();
|
|
180
|
+
try {
|
|
181
|
+
const raw = JSON.parse((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
182
|
+
for (const provider of Object.values(raw)) {
|
|
183
|
+
if (!provider || typeof provider !== 'object')
|
|
184
|
+
continue;
|
|
185
|
+
const models = provider.models;
|
|
186
|
+
if (!models || typeof models !== 'object')
|
|
187
|
+
continue;
|
|
188
|
+
for (const [id, entry] of Object.entries(models)) {
|
|
189
|
+
if (map.has(id))
|
|
190
|
+
continue;
|
|
191
|
+
const price = toPrice(entry?.cost ?? entry, 0);
|
|
192
|
+
if (price)
|
|
193
|
+
map.set(id, price);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* corrupt catalogue reads as empty — tokens still reported */
|
|
199
|
+
}
|
|
200
|
+
priceCache = { home, mtimeMs, map };
|
|
201
|
+
return map;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Estimate a session/message cost from the catalogue. Returns cost 0 with
|
|
205
|
+
* known=false when the model has no price — the caller still reports tokens.
|
|
206
|
+
* `style` decides how cached input is charged: claude reports cache tokens
|
|
207
|
+
* separately from input; codex `input_tokens` already include the cached
|
|
208
|
+
* ones, so only the fresh remainder is charged at the input rate.
|
|
209
|
+
*/
|
|
210
|
+
function estimateModelCost(modelId, tokens, style, home = (0, node_os_1.homedir)()) {
|
|
211
|
+
if (!modelId || modelId === '<synthetic>')
|
|
212
|
+
return { cost: 0, known: false };
|
|
213
|
+
const map = loadPricing(home);
|
|
214
|
+
const price = map.get(modelId) ??
|
|
215
|
+
map.get(stripDateSuffix(modelId)) ??
|
|
216
|
+
map.get(modelId.toLowerCase());
|
|
217
|
+
if (!price)
|
|
218
|
+
return { cost: 0, known: false };
|
|
219
|
+
const freshInput = style === 'codex'
|
|
220
|
+
? Math.max(0, tokens.input - tokens.cacheRead)
|
|
221
|
+
: tokens.input;
|
|
222
|
+
const cost = (freshInput * price.input +
|
|
223
|
+
tokens.cacheRead * (price.cacheRead || 0) +
|
|
224
|
+
tokens.cacheWrite * (price.cacheWrite || price.output) +
|
|
225
|
+
tokens.output * price.output) /
|
|
226
|
+
1_000_000;
|
|
227
|
+
return { cost: round6(cost), known: true };
|
|
228
|
+
}
|
|
229
|
+
// ── Attribution ─────────────────────────────────────────────────────────────
|
|
230
|
+
/**
|
|
231
|
+
* The exact title shapes the bridge passes as `--title` (case-sensitive):
|
|
232
|
+
* `TeamShare task <taskId> (agent <agentId>)` (run.ts), `TeamShare chat/doc …`,
|
|
233
|
+
* `Tool run <key> (<runId>)` (research.ts), `Plan task <taskId>` (plan.ts).
|
|
234
|
+
* A loose prefix would mislabel a human's own window that merely mentions
|
|
235
|
+
* TeamShare (e.g. "Teamshare quota display …"), so the shape is matched.
|
|
236
|
+
*/
|
|
237
|
+
const TEAMSHARE_TITLE_RE = /^(TeamShare (task|chat|doc) |Tool run [\w-]+ \(|Plan task )/;
|
|
238
|
+
function isTeamshareWork(title, directory) {
|
|
239
|
+
if (directory.includes('.teamshare-runs') ||
|
|
240
|
+
directory.includes('.teamshare-worktrees')) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return TEAMSHARE_TITLE_RE.test(title);
|
|
244
|
+
}
|
|
245
|
+
function attributeSession(title, directory) {
|
|
246
|
+
// NOTE: opencode's `agent` column is the subagent lane (build/explore/plan)
|
|
247
|
+
// and is set on interactive windows too, so it can never discriminate.
|
|
248
|
+
// The only reliable join key is where the session ran / what it is titled.
|
|
249
|
+
if (isTeamshareWork(title, directory))
|
|
250
|
+
return { attribution: 'agent', teamshare: true };
|
|
251
|
+
return { attribution: 'user', teamshare: false };
|
|
252
|
+
}
|
|
253
|
+
// ── Model parsing ───────────────────────────────────────────────────────────
|
|
46
254
|
function parseModel(modelJson) {
|
|
47
255
|
if (!modelJson)
|
|
48
256
|
return {};
|
|
@@ -54,31 +262,54 @@ function parseModel(modelJson) {
|
|
|
54
262
|
return {};
|
|
55
263
|
}
|
|
56
264
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
265
|
+
function pickOpencodeTable(db) {
|
|
266
|
+
try {
|
|
267
|
+
const tables = new Set(db
|
|
268
|
+
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
269
|
+
.all().map((t) => t.name));
|
|
270
|
+
// V2 is the live table; legacy `session` stopped in 2026-10. Never read
|
|
271
|
+
// both — every legacy id also exists in session_v2, so a union would
|
|
272
|
+
// double-count.
|
|
273
|
+
if (tables.has('session_v2'))
|
|
274
|
+
return 'session_v2';
|
|
275
|
+
if (tables.has('session'))
|
|
276
|
+
return 'session';
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
/* fall through */
|
|
280
|
+
}
|
|
281
|
+
throw new Error('opencode.db has neither session_v2 nor session table');
|
|
282
|
+
}
|
|
283
|
+
function hasColumn(db, table, col) {
|
|
284
|
+
try {
|
|
285
|
+
const cols = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
286
|
+
return cols.some((c) => c.name === col);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function readOpencodeSessions(day, agentFilter, home = (0, node_os_1.homedir)()) {
|
|
293
|
+
const dbPath = opencodeDbPath(home);
|
|
294
|
+
if (!(0, node_fs_1.existsSync)(dbPath)) {
|
|
295
|
+
throw new Error(`opencode db not found at ${dbPath} — has opencode ever run?`);
|
|
296
|
+
}
|
|
297
|
+
// Lazy-import better-sqlite3 so jsonl-only harnesses never load it.
|
|
70
298
|
const Database = require('better-sqlite3');
|
|
71
299
|
const db = new Database(dbPath, {
|
|
72
300
|
readonly: true,
|
|
73
301
|
fileMustExist: true,
|
|
74
302
|
});
|
|
75
303
|
try {
|
|
304
|
+
const table = pickOpencodeTable(db);
|
|
305
|
+
const wantDir = hasColumn(db, table, 'directory');
|
|
306
|
+
const wantAgent = hasColumn(db, table, 'agent');
|
|
76
307
|
const { startMs, endMs } = dayBounds(day);
|
|
77
308
|
const rows = db
|
|
78
309
|
.prepare(`SELECT title, model, cost, tokens_input, tokens_output,
|
|
79
310
|
tokens_reasoning, tokens_cache_read, tokens_cache_write,
|
|
80
|
-
time_created, time_updated
|
|
81
|
-
FROM
|
|
311
|
+
time_created, time_updated${wantDir ? ', directory' : ''}${wantAgent ? ', agent' : ''}
|
|
312
|
+
FROM ${table} WHERE time_created >= ? AND time_created < ?
|
|
82
313
|
ORDER BY time_created`)
|
|
83
314
|
.all(startMs, endMs);
|
|
84
315
|
const sessions = [];
|
|
@@ -97,7 +328,8 @@ function readOpencode(dbPath, day, agentFilter) {
|
|
|
97
328
|
continue;
|
|
98
329
|
const model = parseModel(row.model);
|
|
99
330
|
const providerId = model.providerID ?? '';
|
|
100
|
-
const
|
|
331
|
+
const directory = row.directory ?? '';
|
|
332
|
+
const { attribution, teamshare } = attributeSession(title, directory);
|
|
101
333
|
// Agent filter
|
|
102
334
|
if (agentFilter &&
|
|
103
335
|
!title.toLowerCase().includes(`agent ${agentFilter}`.toLowerCase()))
|
|
@@ -106,7 +338,10 @@ function readOpencode(dbPath, day, agentFilter) {
|
|
|
106
338
|
title,
|
|
107
339
|
model: model.id ?? '',
|
|
108
340
|
providerID: providerId,
|
|
341
|
+
harness: 'opencode',
|
|
109
342
|
cost: round6(cost),
|
|
343
|
+
costEstimated: false,
|
|
344
|
+
costKnown: true,
|
|
110
345
|
tokensInput: tIn,
|
|
111
346
|
tokensOutput: tOut,
|
|
112
347
|
tokensReasoning: tReason,
|
|
@@ -119,108 +354,512 @@ function readOpencode(dbPath, day, agentFilter) {
|
|
|
119
354
|
teamshare,
|
|
120
355
|
});
|
|
121
356
|
}
|
|
122
|
-
return
|
|
357
|
+
return { sessions, source: dbPath };
|
|
123
358
|
}
|
|
124
359
|
finally {
|
|
125
360
|
db.close();
|
|
126
361
|
}
|
|
127
362
|
}
|
|
128
|
-
// ──
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
fileMustExist: true,
|
|
134
|
-
});
|
|
363
|
+
// ── jsonl helpers ───────────────────────────────────────────────────────────
|
|
364
|
+
function walkJsonl(root, cap, newerThanMs, out = []) {
|
|
365
|
+
if (out.length >= cap)
|
|
366
|
+
return out;
|
|
367
|
+
let entries;
|
|
135
368
|
try {
|
|
136
|
-
|
|
137
|
-
// Return empty report — ready for when codex adds cost data.
|
|
138
|
-
return buildReport(day, dbPath, 'codex', []);
|
|
369
|
+
entries = (0, node_fs_1.readdirSync)(root, { withFileTypes: true });
|
|
139
370
|
}
|
|
140
|
-
|
|
141
|
-
|
|
371
|
+
catch {
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
for (const entry of entries) {
|
|
375
|
+
if (out.length >= cap)
|
|
376
|
+
break;
|
|
377
|
+
const full = (0, node_path_1.join)(root, entry.name);
|
|
378
|
+
try {
|
|
379
|
+
if (entry.isDirectory()) {
|
|
380
|
+
walkJsonl(full, cap, newerThanMs, out);
|
|
381
|
+
}
|
|
382
|
+
else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
383
|
+
try {
|
|
384
|
+
if ((0, node_fs_1.statSync)(full).mtimeMs >= newerThanMs)
|
|
385
|
+
out.push(full);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
/* unreadable — skip */
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
/* best-effort walk */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
function parseTimeMs(value) {
|
|
399
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
400
|
+
return value > 1e12 ? value : value * 1000;
|
|
401
|
+
}
|
|
402
|
+
if (typeof value === 'string' && value) {
|
|
403
|
+
const ms = new Date(value).getTime();
|
|
404
|
+
return Number.isFinite(ms) ? ms : null;
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
function readClaudeSessions(day, agentFilter, home = (0, node_os_1.homedir)()) {
|
|
409
|
+
const root = claudeProjectsPath(home);
|
|
410
|
+
if (!(0, node_fs_1.existsSync)(root)) {
|
|
411
|
+
throw new Error(`no Claude projects at ${root} — has Claude Code ever run here?`);
|
|
412
|
+
}
|
|
413
|
+
const { startMs, endMs } = dayBounds(day);
|
|
414
|
+
const files = walkJsonl(root, 2000, startMs);
|
|
415
|
+
const buckets = new Map();
|
|
416
|
+
for (const file of files) {
|
|
417
|
+
let text;
|
|
418
|
+
try {
|
|
419
|
+
text = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
for (const line of text.split('\n')) {
|
|
425
|
+
const trimmed = line.trim();
|
|
426
|
+
if (!trimmed)
|
|
427
|
+
continue;
|
|
428
|
+
let obj;
|
|
429
|
+
try {
|
|
430
|
+
obj = JSON.parse(trimmed);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (obj.type !== 'assistant')
|
|
436
|
+
continue;
|
|
437
|
+
const message = obj.message;
|
|
438
|
+
const usage = message?.usage;
|
|
439
|
+
if (!usage)
|
|
440
|
+
continue;
|
|
441
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
442
|
+
const tIn = num(usage.input_tokens);
|
|
443
|
+
const tOut = num(usage.output_tokens);
|
|
444
|
+
const tCr = num(usage.cache_read_input_tokens);
|
|
445
|
+
const tCw = num(usage.cache_creation_input_tokens ?? usage.cache_creation_tokens);
|
|
446
|
+
if (!tIn && !tOut && !tCr && !tCw)
|
|
447
|
+
continue;
|
|
448
|
+
const at = parseTimeMs(obj.timestamp);
|
|
449
|
+
if (at === null || at < startMs || at >= endMs)
|
|
450
|
+
continue;
|
|
451
|
+
const sessionId = typeof obj.sessionId === 'string' && obj.sessionId
|
|
452
|
+
? obj.sessionId
|
|
453
|
+
: file;
|
|
454
|
+
const cwd = typeof obj.cwd === 'string' ? obj.cwd : '';
|
|
455
|
+
const model = typeof message?.model === 'string' && message.model
|
|
456
|
+
? message.model
|
|
457
|
+
: '';
|
|
458
|
+
let bucket = buckets.get(sessionId);
|
|
459
|
+
if (!bucket) {
|
|
460
|
+
bucket = {
|
|
461
|
+
sessionId,
|
|
462
|
+
cwd,
|
|
463
|
+
model,
|
|
464
|
+
firstMs: at,
|
|
465
|
+
lastMs: at,
|
|
466
|
+
input: 0,
|
|
467
|
+
output: 0,
|
|
468
|
+
cacheRead: 0,
|
|
469
|
+
cacheWrite: 0,
|
|
470
|
+
cost: 0,
|
|
471
|
+
known: false,
|
|
472
|
+
};
|
|
473
|
+
buckets.set(sessionId, bucket);
|
|
474
|
+
}
|
|
475
|
+
if (model && model !== '<synthetic>')
|
|
476
|
+
bucket.model = model;
|
|
477
|
+
if (cwd && !bucket.cwd)
|
|
478
|
+
bucket.cwd = cwd;
|
|
479
|
+
bucket.firstMs = Math.min(bucket.firstMs, at);
|
|
480
|
+
bucket.lastMs = Math.max(bucket.lastMs, at);
|
|
481
|
+
bucket.input += tIn;
|
|
482
|
+
bucket.output += tOut;
|
|
483
|
+
bucket.cacheRead += tCr;
|
|
484
|
+
bucket.cacheWrite += tCw;
|
|
485
|
+
if (model) {
|
|
486
|
+
const est = estimateModelCost(model, { input: tIn, output: tOut, cacheRead: tCr, cacheWrite: tCw }, 'claude', home);
|
|
487
|
+
bucket.cost = round6(bucket.cost + est.cost);
|
|
488
|
+
if (est.known)
|
|
489
|
+
bucket.known = true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const sessions = [];
|
|
494
|
+
for (const bucket of buckets.values()) {
|
|
495
|
+
const shortCwd = bucket.cwd ? (0, node_path_1.basename)(bucket.cwd) : '';
|
|
496
|
+
const title = shortCwd
|
|
497
|
+
? `Claude · ${shortCwd}`
|
|
498
|
+
: `Claude ${bucket.sessionId.slice(0, 8)}`;
|
|
499
|
+
if (agentFilter &&
|
|
500
|
+
!title.toLowerCase().includes(`agent ${agentFilter}`.toLowerCase()) &&
|
|
501
|
+
!bucket.cwd.toLowerCase().includes(agentFilter.toLowerCase())) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const { attribution, teamshare } = attributeSession(title, bucket.cwd);
|
|
505
|
+
sessions.push({
|
|
506
|
+
title,
|
|
507
|
+
model: bucket.model,
|
|
508
|
+
providerID: 'anthropic',
|
|
509
|
+
harness: 'claude',
|
|
510
|
+
cost: bucket.cost,
|
|
511
|
+
costEstimated: true,
|
|
512
|
+
costKnown: bucket.known,
|
|
513
|
+
tokensInput: bucket.input,
|
|
514
|
+
tokensOutput: bucket.output,
|
|
515
|
+
tokensReasoning: 0,
|
|
516
|
+
tokensCacheRead: bucket.cacheRead,
|
|
517
|
+
tokensCacheWrite: bucket.cacheWrite,
|
|
518
|
+
timeCreated: bucket.firstMs,
|
|
519
|
+
timeUpdated: bucket.lastMs,
|
|
520
|
+
durationMin: round1(Math.max(0, bucket.lastMs - bucket.firstMs) / 60_000),
|
|
521
|
+
attribution,
|
|
522
|
+
teamshare,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
sessions.sort((a, b) => a.timeCreated - b.timeCreated);
|
|
526
|
+
return { sessions, source: root };
|
|
527
|
+
}
|
|
528
|
+
// ── codex reader ────────────────────────────────────────────────────────────
|
|
529
|
+
function readCodexSessions(day, agentFilter, home = (0, node_os_1.homedir)()) {
|
|
530
|
+
const root = codexSessionsPath(home);
|
|
531
|
+
if (!(0, node_fs_1.existsSync)(root)) {
|
|
532
|
+
throw new Error(`no Codex sessions at ${root} — has Codex ever run here?`);
|
|
533
|
+
}
|
|
534
|
+
const { startMs, endMs } = dayBounds(day);
|
|
535
|
+
// Cross-midnight sessions live in the previous day's directory, so the
|
|
536
|
+
// mtime gate opens a day early and each event is filtered by its own time.
|
|
537
|
+
const files = walkJsonl(root, 500, startMs - 86_400_000);
|
|
538
|
+
// Thread names live beside the sessions, not in them.
|
|
539
|
+
const threadNames = new Map();
|
|
540
|
+
try {
|
|
541
|
+
const indexText = (0, node_fs_1.readFileSync)(codexIndexPath(home), 'utf8');
|
|
542
|
+
for (const line of indexText.split('\n')) {
|
|
543
|
+
const trimmed = line.trim();
|
|
544
|
+
if (!trimmed)
|
|
545
|
+
continue;
|
|
546
|
+
try {
|
|
547
|
+
const row = JSON.parse(trimmed);
|
|
548
|
+
if (typeof row.id === 'string' && typeof row.thread_name === 'string') {
|
|
549
|
+
threadNames.set(row.id, row.thread_name);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
/* malformed index line — skip */
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
/* no index — titles fall back to the session id */
|
|
559
|
+
}
|
|
560
|
+
const buckets = new Map();
|
|
561
|
+
for (const file of files) {
|
|
562
|
+
let text;
|
|
563
|
+
try {
|
|
564
|
+
text = (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const base = (0, node_path_1.basename)(file, '.jsonl');
|
|
570
|
+
const fileSessionId = base.startsWith('rollout-')
|
|
571
|
+
? base.slice('rollout-'.length)
|
|
572
|
+
: base;
|
|
573
|
+
const ensure = () => {
|
|
574
|
+
let b = buckets.get(file);
|
|
575
|
+
if (!b) {
|
|
576
|
+
b = {
|
|
577
|
+
key: file,
|
|
578
|
+
sessionId: fileSessionId,
|
|
579
|
+
cwd: '',
|
|
580
|
+
model: '',
|
|
581
|
+
firstMs: Number.MAX_SAFE_INTEGER,
|
|
582
|
+
lastMs: 0,
|
|
583
|
+
input: 0,
|
|
584
|
+
cached: 0,
|
|
585
|
+
output: 0,
|
|
586
|
+
reasoning: 0,
|
|
587
|
+
};
|
|
588
|
+
buckets.set(file, b);
|
|
589
|
+
}
|
|
590
|
+
return b;
|
|
591
|
+
};
|
|
592
|
+
for (const line of text.split('\n')) {
|
|
593
|
+
const trimmed = line.trim();
|
|
594
|
+
if (!trimmed)
|
|
595
|
+
continue;
|
|
596
|
+
let obj;
|
|
597
|
+
try {
|
|
598
|
+
obj = JSON.parse(trimmed);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const payload = obj.payload;
|
|
604
|
+
if (obj.type === 'session_meta' && payload) {
|
|
605
|
+
const b = ensure();
|
|
606
|
+
if (typeof payload.id === 'string' && payload.id) {
|
|
607
|
+
b.sessionId = payload.id;
|
|
608
|
+
}
|
|
609
|
+
if (typeof payload.cwd === 'string')
|
|
610
|
+
b.cwd = payload.cwd;
|
|
611
|
+
if (typeof payload.model === 'string' && payload.model) {
|
|
612
|
+
b.model = payload.model;
|
|
613
|
+
}
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (obj.type === 'turn_context' && payload) {
|
|
617
|
+
const model = payload.model;
|
|
618
|
+
if (typeof model === 'string' && model)
|
|
619
|
+
ensure().model = model;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (obj.type !== 'event_msg' || !payload)
|
|
623
|
+
continue;
|
|
624
|
+
if (payload.type !== 'token_count')
|
|
625
|
+
continue;
|
|
626
|
+
const info = payload.info;
|
|
627
|
+
const last = info?.last_token_usage;
|
|
628
|
+
if (!last)
|
|
629
|
+
continue;
|
|
630
|
+
const at = parseTimeMs(obj.timestamp);
|
|
631
|
+
if (at === null || at < startMs || at >= endMs)
|
|
632
|
+
continue;
|
|
633
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : 0;
|
|
634
|
+
const b = ensure();
|
|
635
|
+
b.firstMs = Math.min(b.firstMs, at);
|
|
636
|
+
b.lastMs = Math.max(b.lastMs, at);
|
|
637
|
+
b.input += num(last.input_tokens);
|
|
638
|
+
b.cached += num(last.cached_input_tokens);
|
|
639
|
+
b.output += num(last.output_tokens);
|
|
640
|
+
b.reasoning += num(last.reasoning_output_tokens);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const sessions = [];
|
|
644
|
+
for (const bucket of buckets.values()) {
|
|
645
|
+
if (bucket.lastMs === 0)
|
|
646
|
+
continue;
|
|
647
|
+
const title = threadNames.get(bucket.sessionId) ??
|
|
648
|
+
`Codex ${bucket.sessionId.slice(0, 8)}`;
|
|
649
|
+
if (agentFilter &&
|
|
650
|
+
!title.toLowerCase().includes(`agent ${agentFilter}`.toLowerCase()) &&
|
|
651
|
+
!bucket.cwd.toLowerCase().includes(agentFilter.toLowerCase())) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const est = estimateModelCost(bucket.model, {
|
|
655
|
+
input: bucket.input,
|
|
656
|
+
output: bucket.output,
|
|
657
|
+
cacheRead: bucket.cached,
|
|
658
|
+
cacheWrite: 0,
|
|
659
|
+
}, 'codex', home);
|
|
660
|
+
const { attribution, teamshare } = attributeSession(title, bucket.cwd);
|
|
661
|
+
sessions.push({
|
|
662
|
+
title,
|
|
663
|
+
model: bucket.model,
|
|
664
|
+
providerID: 'openai',
|
|
665
|
+
harness: 'codex',
|
|
666
|
+
cost: est.cost,
|
|
667
|
+
costEstimated: true,
|
|
668
|
+
costKnown: est.known,
|
|
669
|
+
tokensInput: bucket.input,
|
|
670
|
+
tokensOutput: bucket.output,
|
|
671
|
+
// Reasoning tokens ride inside output_tokens on the Responses API —
|
|
672
|
+
// recorded for display, never charged twice.
|
|
673
|
+
tokensReasoning: bucket.reasoning,
|
|
674
|
+
tokensCacheRead: bucket.cached,
|
|
675
|
+
tokensCacheWrite: 0,
|
|
676
|
+
timeCreated: bucket.firstMs,
|
|
677
|
+
timeUpdated: bucket.lastMs,
|
|
678
|
+
durationMin: round1(Math.max(0, bucket.lastMs - bucket.firstMs) / 60_000),
|
|
679
|
+
attribution,
|
|
680
|
+
teamshare,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
sessions.sort((a, b) => a.timeCreated - b.timeCreated);
|
|
684
|
+
return { sessions, source: root };
|
|
685
|
+
}
|
|
686
|
+
// ── Availability probe ──────────────────────────────────────────────────────
|
|
687
|
+
function probeHarness(def, home) {
|
|
688
|
+
if (!def.supported) {
|
|
689
|
+
return { available: false, reason: def.unavailableReason };
|
|
690
|
+
}
|
|
691
|
+
if (def.id === 'opencode') {
|
|
692
|
+
const p = opencodeDbPath(home);
|
|
693
|
+
return (0, node_fs_1.existsSync)(p)
|
|
694
|
+
? { available: true, source: p }
|
|
695
|
+
: {
|
|
696
|
+
available: false,
|
|
697
|
+
reason: 'opencode has not run on this machine yet (no opencode.db)',
|
|
698
|
+
};
|
|
142
699
|
}
|
|
700
|
+
if (def.id === 'claude') {
|
|
701
|
+
const p = claudeProjectsPath(home);
|
|
702
|
+
return (0, node_fs_1.existsSync)(p)
|
|
703
|
+
? { available: true, source: p }
|
|
704
|
+
: {
|
|
705
|
+
available: false,
|
|
706
|
+
reason: 'no Claude Code history on this machine yet',
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const p = codexSessionsPath(home);
|
|
710
|
+
return (0, node_fs_1.existsSync)(p)
|
|
711
|
+
? { available: true, source: p }
|
|
712
|
+
: { available: false, reason: 'no Codex history on this machine yet' };
|
|
713
|
+
}
|
|
714
|
+
// ── Report builder ──────────────────────────────────────────────────────────
|
|
715
|
+
function emptyTotals() {
|
|
716
|
+
return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 };
|
|
143
717
|
}
|
|
144
|
-
|
|
145
|
-
function buildReport(day, dbPath, provider, sessions) {
|
|
718
|
+
function buildReport(day, harness, sessions, harnesses, totalsByHarness, sources, dbPath, notice) {
|
|
146
719
|
const totalCost = round6(sessions.reduce((s, x) => s + x.cost, 0));
|
|
147
720
|
const userCost = round6(sessions
|
|
148
721
|
.filter((s) => s.attribution === 'user')
|
|
149
722
|
.reduce((s, x) => s + x.cost, 0));
|
|
150
723
|
const agentCost = round6(totalCost - userCost);
|
|
151
724
|
const teamshareCost = round6(sessions.filter((s) => s.teamshare).reduce((s, x) => s + x.cost, 0));
|
|
725
|
+
const tokens = emptyTotals();
|
|
726
|
+
for (const s of sessions) {
|
|
727
|
+
tokens.input += s.tokensInput;
|
|
728
|
+
tokens.output += s.tokensOutput;
|
|
729
|
+
tokens.reasoning += s.tokensReasoning;
|
|
730
|
+
tokens.cacheRead += s.tokensCacheRead;
|
|
731
|
+
tokens.cacheWrite += s.tokensCacheWrite;
|
|
732
|
+
}
|
|
152
733
|
return {
|
|
153
734
|
day,
|
|
735
|
+
harness,
|
|
154
736
|
dbPath,
|
|
155
|
-
|
|
737
|
+
sources,
|
|
738
|
+
provider: harness,
|
|
156
739
|
totalCost,
|
|
157
740
|
sessionCount: sessions.length,
|
|
158
741
|
userCost,
|
|
159
742
|
agentCost,
|
|
160
743
|
teamshareCost,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
cacheWrite: sessions.reduce((s, x) => s + x.tokensCacheWrite, 0),
|
|
167
|
-
},
|
|
744
|
+
unpricedSessions: sessions.filter((s) => !s.costKnown).length,
|
|
745
|
+
...(notice ? { notice } : {}),
|
|
746
|
+
harnesses,
|
|
747
|
+
totalsByHarness,
|
|
748
|
+
tokens,
|
|
168
749
|
sessions,
|
|
169
750
|
};
|
|
170
751
|
}
|
|
171
|
-
// ──
|
|
172
|
-
function round6(n) {
|
|
173
|
-
return Math.round(n * 1_000_000) / 1_000_000;
|
|
174
|
-
}
|
|
175
|
-
function round1(n) {
|
|
176
|
-
return Math.round(n * 10) / 10;
|
|
177
|
-
}
|
|
178
|
-
// ── Public API ────────────────────────────────────────────────────────────
|
|
752
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
179
753
|
/**
|
|
180
|
-
* Read
|
|
181
|
-
*
|
|
182
|
-
*
|
|
754
|
+
* Read one harness (or 'all' supported harnesses) for a local day.
|
|
755
|
+
* Each harness is its own report slice: sessions carry `harness`,
|
|
756
|
+
* `costEstimated` and `costKnown`, and the UI switches between them.
|
|
757
|
+
* Throws when no local store exists for an explicitly requested harness.
|
|
183
758
|
*/
|
|
184
759
|
function readUsageReport(opts = {}) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
760
|
+
const home = opts.homeDir ?? (0, node_os_1.homedir)();
|
|
761
|
+
const day = opts.day ?? todayLocalDay();
|
|
762
|
+
const requested = opts.harness ?? (opts.provider === 'codex' ? 'codex' : 'opencode');
|
|
763
|
+
const agentFilter = opts.agent ?? null;
|
|
764
|
+
const infos = new Map();
|
|
765
|
+
const totals = {};
|
|
766
|
+
const sources = {};
|
|
767
|
+
const allSessions = [];
|
|
768
|
+
const readOne = (id) => {
|
|
769
|
+
const def = harnessDef(id);
|
|
770
|
+
if (!def) {
|
|
771
|
+
throw new Error(`unknown harness "${id}" — supported: ${exports.SUPPORTED_USAGE_HARNESSES.join(', ')}`);
|
|
772
|
+
}
|
|
773
|
+
if (!def.supported) {
|
|
774
|
+
// The honest answer carries the reason, not a silent zero.
|
|
775
|
+
throw new Error(def.unavailableReason ?? `usage is not supported for harness "${id}"`);
|
|
776
|
+
}
|
|
777
|
+
const probe = probeHarness(def, home);
|
|
778
|
+
if (!probe.available) {
|
|
779
|
+
throw new Error(probe.reason ?? `no local data for ${id}`);
|
|
780
|
+
}
|
|
781
|
+
const { sessions, source } = id === 'opencode'
|
|
782
|
+
? readOpencodeSessions(day, agentFilter, home)
|
|
783
|
+
: id === 'claude'
|
|
784
|
+
? readClaudeSessions(day, agentFilter, home)
|
|
785
|
+
: readCodexSessions(day, agentFilter, home);
|
|
786
|
+
sources[id] = source;
|
|
787
|
+
const cost = round6(sessions.reduce((s, x) => s + x.cost, 0));
|
|
788
|
+
const info = {
|
|
789
|
+
id,
|
|
790
|
+
label: def.label,
|
|
791
|
+
available: true,
|
|
792
|
+
costSource: def.costSource,
|
|
793
|
+
sessions: sessions.length,
|
|
794
|
+
cost,
|
|
210
795
|
tokens: {
|
|
211
|
-
input: 0,
|
|
212
|
-
output: 0,
|
|
213
|
-
reasoning: 0,
|
|
214
|
-
cacheRead: 0,
|
|
215
|
-
cacheWrite: 0,
|
|
796
|
+
input: sessions.reduce((s, x) => s + x.tokensInput, 0),
|
|
797
|
+
output: sessions.reduce((s, x) => s + x.tokensOutput, 0),
|
|
216
798
|
},
|
|
217
|
-
sessions: [],
|
|
218
799
|
};
|
|
800
|
+
infos.set(id, info);
|
|
801
|
+
totals[id] = {
|
|
802
|
+
sessions: sessions.length,
|
|
803
|
+
cost,
|
|
804
|
+
costEstimated: def.costSource === 'estimated',
|
|
805
|
+
unpricedSessions: sessions.filter((s) => !s.costKnown).length,
|
|
806
|
+
};
|
|
807
|
+
allSessions.push(...sessions);
|
|
808
|
+
};
|
|
809
|
+
if (requested === 'all') {
|
|
810
|
+
for (const id of exports.SUPPORTED_USAGE_HARNESSES) {
|
|
811
|
+
try {
|
|
812
|
+
readOne(id);
|
|
813
|
+
}
|
|
814
|
+
catch {
|
|
815
|
+
// A harness with no local store contributes nothing — its
|
|
816
|
+
// unavailability is still listed below.
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
readOne(requested);
|
|
822
|
+
}
|
|
823
|
+
// Every known harness is listed so the UI can offer exactly the
|
|
824
|
+
// supported ones and explain the rest.
|
|
825
|
+
const harnesses = exports.USAGE_HARNESSES.map((def) => {
|
|
826
|
+
const read = infos.get(def.id);
|
|
827
|
+
if (read)
|
|
828
|
+
return read;
|
|
829
|
+
const probe = probeHarness(def, home);
|
|
830
|
+
return {
|
|
831
|
+
id: def.id,
|
|
832
|
+
label: def.label,
|
|
833
|
+
available: false,
|
|
834
|
+
...(probe.reason ? { reason: probe.reason } : {}),
|
|
835
|
+
costSource: def.costSource,
|
|
836
|
+
sessions: 0,
|
|
837
|
+
cost: 0,
|
|
838
|
+
tokens: { input: 0, output: 0 },
|
|
839
|
+
};
|
|
840
|
+
});
|
|
841
|
+
for (const def of exports.USAGE_HARNESSES) {
|
|
842
|
+
if (!totals[def.id]) {
|
|
843
|
+
totals[def.id] = {
|
|
844
|
+
sessions: 0,
|
|
845
|
+
cost: 0,
|
|
846
|
+
costEstimated: def.costSource === 'estimated',
|
|
847
|
+
unpricedSessions: 0,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
219
850
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
851
|
+
allSessions.sort((a, b) => a.timeCreated - b.timeCreated);
|
|
852
|
+
const dbPath = sources[requested] ??
|
|
853
|
+
sources.opencode ??
|
|
854
|
+
sources[Object.keys(sources)[0] ?? ''] ??
|
|
855
|
+
'(no provider database found)';
|
|
856
|
+
return buildReport(day, requested, allSessions, harnesses, totals, sources, dbPath);
|
|
857
|
+
}
|
|
858
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
859
|
+
function round6(n) {
|
|
860
|
+
return Math.round(n * 1_000_000) / 1_000_000;
|
|
861
|
+
}
|
|
862
|
+
function round1(n) {
|
|
863
|
+
return Math.round(n * 10) / 10;
|
|
225
864
|
}
|
|
226
865
|
//# sourceMappingURL=usage-reader.js.map
|