teamshare-bridge 0.14.17 → 0.14.18

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.
@@ -0,0 +1,46 @@
1
+ export interface UsageSession {
2
+ title: string;
3
+ model: string;
4
+ providerID: string;
5
+ cost: number;
6
+ tokensInput: number;
7
+ tokensOutput: number;
8
+ tokensReasoning: number;
9
+ tokensCacheRead: number;
10
+ tokensCacheWrite: number;
11
+ timeCreated: number;
12
+ timeUpdated: number;
13
+ durationMin: number;
14
+ attribution: 'user' | 'agent';
15
+ teamshare: boolean;
16
+ }
17
+ export interface UsageReport {
18
+ day: string;
19
+ dbPath: string;
20
+ provider: string;
21
+ totalCost: number;
22
+ sessionCount: number;
23
+ userCost: number;
24
+ agentCost: number;
25
+ teamshareCost: number;
26
+ tokens: {
27
+ input: number;
28
+ output: number;
29
+ reasoning: number;
30
+ cacheRead: number;
31
+ cacheWrite: number;
32
+ };
33
+ sessions: UsageSession[];
34
+ }
35
+ export interface UsageOptions {
36
+ day?: string;
37
+ agent?: string;
38
+ /** Force a specific provider instead of auto-detecting. */
39
+ provider?: 'opencode' | 'codex';
40
+ }
41
+ /**
42
+ * Read the usage report directly from the harness database.
43
+ * Auto-detects the provider (opencode > codex) or uses the explicit option.
44
+ * Throws if no provider database is found.
45
+ */
46
+ export declare function readUsageReport(opts?: UsageOptions): UsageReport;
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readUsageReport = readUsageReport;
4
+ /**
5
+ * Usage report reader — reads cost data directly from harness databases.
6
+ *
7
+ * Replaces the Python tools/usage_report.py with in-process Node reads via
8
+ * better-sqlite3. Supports multiple providers:
9
+ * - opencode: ~/.local/share/opencode/opencode.db (session table)
10
+ * - codex: ~/.codex/logs_2.sqlite (logs table, no cost data yet)
11
+ * - claude/gemini: no local cost database (returns empty)
12
+ *
13
+ * The report is fully offline, needs neither the backend nor the daemon.
14
+ */
15
+ const node_fs_1 = require("node:fs");
16
+ const node_os_1 = require("node:os");
17
+ const node_path_1 = require("node:path");
18
+ // ── Provider-specific database paths ──────────────────────────────────────
19
+ function opencodeDbPath() {
20
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), '.local', 'share', 'opencode', 'opencode.db');
21
+ }
22
+ function codexDbPath() {
23
+ return (0, node_path_1.join)((0, node_os_1.homedir)(), '.codex', 'logs_2.sqlite');
24
+ }
25
+ /** Returns the first available provider path, or null if none found. */
26
+ function detectProvider() {
27
+ const opencodePath = opencodeDbPath();
28
+ if ((0, node_fs_1.existsSync)(opencodePath))
29
+ return { name: 'opencode', dbPath: opencodePath };
30
+ const codexPath = codexDbPath();
31
+ if ((0, node_fs_1.existsSync)(codexPath))
32
+ return { name: 'codex', dbPath: codexPath };
33
+ return null;
34
+ }
35
+ // ── Day bounds (local midnight ms-epoch) ─────────────────────────────────
36
+ function today() {
37
+ return new Date().toISOString().slice(0, 10);
38
+ }
39
+ function dayBounds(day) {
40
+ const d = new Date(`${day}T00:00:00`);
41
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
42
+ const start = new Date(d.toLocaleString('en-US', { timeZone: tz }));
43
+ const end = new Date(start.getTime() + 86_400_000);
44
+ return { startMs: start.getTime(), endMs: end.getTime() };
45
+ }
46
+ function parseModel(modelJson) {
47
+ if (!modelJson)
48
+ return {};
49
+ try {
50
+ const parsed = JSON.parse(modelJson);
51
+ return typeof parsed === 'object' && parsed !== null ? parsed : {};
52
+ }
53
+ catch {
54
+ return {};
55
+ }
56
+ }
57
+ // ── Attribution ───────────────────────────────────────────────────────────
58
+ function attributeSession(title, providerId) {
59
+ if (title?.startsWith('TeamShare '))
60
+ return { attribution: 'agent', teamshare: true };
61
+ // opencode-go / opencode: providerID == "opencode" or "opencode-go" = user sessions
62
+ if (providerId === 'opencode' || providerId === 'opencode-go')
63
+ return { attribution: 'user', teamshare: false };
64
+ // claude, codex, gemini, etc. — treat as agent sessions
65
+ return { attribution: 'agent', teamshare: false };
66
+ }
67
+ // ── opencode reader ──────────────────────────────────────────────────────
68
+ function readOpencode(dbPath, day, agentFilter) {
69
+ // Lazy-import better-sqlite3 so non-opencode providers don't need it loaded.
70
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
71
+ const Database = require('better-sqlite3');
72
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
73
+ try {
74
+ const { startMs, endMs } = dayBounds(day);
75
+ const rows = db.prepare(`SELECT title, model, cost, tokens_input, tokens_output,
76
+ tokens_reasoning, tokens_cache_read, tokens_cache_write,
77
+ time_created, time_updated
78
+ FROM session WHERE time_created >= ? AND time_created < ?
79
+ ORDER BY time_created`).all(startMs, endMs);
80
+ const sessions = [];
81
+ for (const row of rows) {
82
+ const title = row.title ?? '(untitled)';
83
+ // Skip opencode's "New session - ..." placeholders
84
+ if (title.startsWith('New session'))
85
+ continue;
86
+ const cost = row.cost ?? 0;
87
+ const tIn = row.tokens_input ?? 0;
88
+ const tOut = row.tokens_output ?? 0;
89
+ const tReason = row.tokens_reasoning ?? 0;
90
+ const tCr = row.tokens_cache_read ?? 0;
91
+ const tCw = row.tokens_cache_write ?? 0;
92
+ if (cost <= 0 && !tIn && !tOut && !tReason && !tCr && !tCw)
93
+ continue;
94
+ const model = parseModel(row.model);
95
+ const providerId = model.providerID ?? '';
96
+ const { attribution, teamshare } = attributeSession(title, providerId);
97
+ // Agent filter
98
+ if (agentFilter && !title.toLowerCase().includes(`agent ${agentFilter}`.toLowerCase()))
99
+ continue;
100
+ sessions.push({
101
+ title,
102
+ model: model.id ?? '',
103
+ providerID: providerId,
104
+ cost: round6(cost),
105
+ tokensInput: tIn,
106
+ tokensOutput: tOut,
107
+ tokensReasoning: tReason,
108
+ tokensCacheRead: tCr,
109
+ tokensCacheWrite: tCw,
110
+ timeCreated: row.time_created,
111
+ timeUpdated: row.time_updated,
112
+ durationMin: round1(Math.max(0, (row.time_updated || row.time_created) - row.time_created) / 60_000),
113
+ attribution,
114
+ teamshare,
115
+ });
116
+ }
117
+ return buildReport(day, dbPath, 'opencode', sessions);
118
+ }
119
+ finally {
120
+ db.close();
121
+ }
122
+ }
123
+ // ── Codex reader (placeholder — no cost data yet) ────────────────────────
124
+ function readCodex(dbPath, day) {
125
+ const Database = require('better-sqlite3');
126
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
127
+ try {
128
+ // Codex logs_2.sqlite has a logs table but no cost/token columns yet.
129
+ // Return empty report — ready for when codex adds cost data.
130
+ return buildReport(day, dbPath, 'codex', []);
131
+ }
132
+ finally {
133
+ db.close();
134
+ }
135
+ }
136
+ // ── Report builder ────────────────────────────────────────────────────────
137
+ function buildReport(day, dbPath, provider, sessions) {
138
+ const totalCost = round6(sessions.reduce((s, x) => s + x.cost, 0));
139
+ const userCost = round6(sessions.filter((s) => s.attribution === 'user').reduce((s, x) => s + x.cost, 0));
140
+ const agentCost = round6(totalCost - userCost);
141
+ const teamshareCost = round6(sessions.filter((s) => s.teamshare).reduce((s, x) => s + x.cost, 0));
142
+ return {
143
+ day,
144
+ dbPath,
145
+ provider,
146
+ totalCost,
147
+ sessionCount: sessions.length,
148
+ userCost,
149
+ agentCost,
150
+ teamshareCost,
151
+ tokens: {
152
+ input: sessions.reduce((s, x) => s + x.tokensInput, 0),
153
+ output: sessions.reduce((s, x) => s + x.tokensOutput, 0),
154
+ reasoning: sessions.reduce((s, x) => s + x.tokensReasoning, 0),
155
+ cacheRead: sessions.reduce((s, x) => s + x.tokensCacheRead, 0),
156
+ cacheWrite: sessions.reduce((s, x) => s + x.tokensCacheWrite, 0),
157
+ },
158
+ sessions,
159
+ };
160
+ }
161
+ // ── Helpers ───────────────────────────────────────────────────────────────
162
+ function round6(n) { return Math.round(n * 1_000_000) / 1_000_000; }
163
+ function round1(n) { return Math.round(n * 10) / 10; }
164
+ // ── Public API ────────────────────────────────────────────────────────────
165
+ /**
166
+ * Read the usage report directly from the harness database.
167
+ * Auto-detects the provider (opencode > codex) or uses the explicit option.
168
+ * Throws if no provider database is found.
169
+ */
170
+ function readUsageReport(opts = {}) {
171
+ const day = opts.day ?? today();
172
+ if (opts.provider === 'opencode') {
173
+ const dbPath = opencodeDbPath();
174
+ if (!(0, node_fs_1.existsSync)(dbPath))
175
+ throw new Error(`opencode db not found at ${dbPath} — has opencode ever run?`);
176
+ return readOpencode(dbPath, day, opts.agent ?? null);
177
+ }
178
+ if (opts.provider === 'codex') {
179
+ const dbPath = codexDbPath();
180
+ if (!(0, node_fs_1.existsSync)(dbPath))
181
+ throw new Error(`codex db not found at ${dbPath} — has codex ever run?`);
182
+ return readCodex(dbPath, day);
183
+ }
184
+ // Auto-detect
185
+ const detected = detectProvider();
186
+ if (!detected) {
187
+ return {
188
+ day,
189
+ dbPath: '(no provider database found)',
190
+ provider: 'none',
191
+ totalCost: 0,
192
+ sessionCount: 0,
193
+ userCost: 0,
194
+ agentCost: 0,
195
+ teamshareCost: 0,
196
+ tokens: { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0 },
197
+ sessions: [],
198
+ };
199
+ }
200
+ if (detected.name === 'opencode')
201
+ return readOpencode(detected.dbPath, day, opts.agent ?? null);
202
+ if (detected.name === 'codex')
203
+ return readCodex(detected.dbPath, day);
204
+ return buildReport(day, '(unknown)', 'none', []);
205
+ }
206
+ //# sourceMappingURL=usage-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage-reader.js","sourceRoot":"","sources":["../../src/lib/usage-reader.ts"],"names":[],"mappings":";;AAyPA,0CAmCC;AA5RD;;;;;;;;;;GAUG;AACH,qCAAqC;AACrC,qCAAkC;AAClC,yCAAiC;AA8CjC,6EAA6E;AAE7E,SAAS,cAAc;IACrB,OAAO,IAAA,gBAAI,EAAC,IAAA,iBAAO,GAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,WAAW;IAClB,OAAO,IAAA,gBAAI,EAAC,IAAA,iBAAO,GAAE,EAAE,QAAQ,EAAE,eAAe,CAAC,CAAC;AACpD,CAAC;AAED,wEAAwE;AACxE,SAAS,cAAc;IACrB,MAAM,YAAY,GAAG,cAAc,EAAE,CAAC;IACtC,IAAI,IAAA,oBAAU,EAAC,YAAY,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAEhF,MAAM,SAAS,GAAG,WAAW,EAAE,CAAC;IAChC,IAAI,IAAA,oBAAU,EAAC,SAAS,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAEvE,OAAO,IAAI,CAAC;AACd,CAAC;AAED,4EAA4E;AAE5E,SAAS,KAAK;IACZ,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,SAAS,CAAC,GAAW;IAC5B,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,GAAG,WAAW,CAAC,CAAC;IACtC,MAAM,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ,CAAC;IAC5D,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IACpE,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,UAAU,CAAC,CAAC;IACnD,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;AAC5D,CAAC;AASD,SAAS,UAAU,CAAC,SAAoC;IACtD,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACrC,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,6EAA6E;AAE7E,SAAS,gBAAgB,CAAC,KAAoB,EAAE,UAAkB;IAChE,IAAI,KAAK,EAAE,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IACtF,oFAAoF;IACpF,IAAI,UAAU,KAAK,UAAU,IAAI,UAAU,KAAK,aAAa;QAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAChH,wDAAwD;IACxD,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AACpD,CAAC;AAED,4EAA4E;AAE5E,SAAS,YAAY,CAAC,MAAc,EAAE,GAAW,EAAE,WAA0B;IAC3E,6EAA6E;IAC7E,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAoC,CAAC;IAC9E,MAAM,EAAE,GAAsB,IAAI,QAAQ,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAS,CAAC,CAAC;IAEnG,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAE1C,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CACrB;;;;6BAIuB,CACxB,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAWlB,CAAC;QAEH,MAAM,QAAQ,GAAmB,EAAE,CAAC;QACpC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,IAAI,YAAY,CAAC;YACxC,mDAAmD;YACnD,IAAI,KAAK,CAAC,UAAU,CAAC,aAAa,CAAC;gBAAE,SAAS;YAC9C,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC;YAC3B,MAAM,GAAG,GAAG,GAAG,CAAC,YAAY,IAAI,CAAC,CAAC;YAClC,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,IAAI,CAAC,CAAC;YACpC,MAAM,OAAO,GAAG,GAAG,CAAC,gBAAgB,IAAI,CAAC,CAAC;YAC1C,MAAM,GAAG,GAAG,GAAG,CAAC,iBAAiB,IAAI,CAAC,CAAC;YACvC,MAAM,GAAG,GAAG,GAAG,CAAC,kBAAkB,IAAI,CAAC,CAAC;YACxC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG;gBAAE,SAAS;YAErE,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACpC,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;YAC1C,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,gBAAgB,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;YAEvE,eAAe;YACf,IAAI,WAAW,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,SAAS,WAAW,EAAE,CAAC,WAAW,EAAE,CAAC;gBAAE,SAAS;YAEjG,QAAQ,CAAC,IAAI,CAAC;gBACZ,KAAK;gBACL,KAAK,EAAE,KAAK,CAAC,EAAE,IAAI,EAAE;gBACrB,UAAU,EAAE,UAAU;gBACtB,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC;gBAClB,WAAW,EAAE,GAAG;gBAChB,YAAY,EAAE,IAAI;gBAClB,eAAe,EAAE,OAAO;gBACxB,eAAe,EAAE,GAAG;gBACpB,gBAAgB,EAAE,GAAG;gBACrB,WAAW,EAAE,GAAG,CAAC,YAAY;gBAC7B,WAAW,EAAE,GAAG,CAAC,YAAY;gBAC7B,WAAW,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,YAAY,IAAI,GAAG,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,MAAM,CAAC;gBACpG,WAAW;gBACX,SAAS;aACV,CAAC,CAAC;QACL,CAAC;QAED,OAAO,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC;IACxD,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC;AAED,4EAA4E;AAE5E,SAAS,SAAS,CAAC,MAAc,EAAE,GAAW;IAC5C,MAAM,QAAQ,GAAG,OAAO,CAAC,gBAAgB,CAAoC,CAAC;IAC9E,MAAM,EAAE,GAAsB,IAAI,QAAQ,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAS,CAAC,CAAC;IACnG,IAAI,CAAC;QACH,sEAAsE;QACtE,6DAA6D;QAC7D,OAAO,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;IAC/C,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC;AAED,6EAA6E;AAE7E,SAAS,WAAW,CAAC,GAAW,EAAE,MAAc,EAAE,QAAgB,EAAE,QAAwB;IAC1F,MAAM,SAAS,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC1G,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,GAAG,QAAQ,CAAC,CAAC;IAC/C,MAAM,aAAa,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAElG,OAAO;QACL,GAAG;QACH,MAAM;QACN,QAAQ;QACR,SAAS;QACT,YAAY,EAAE,QAAQ,CAAC,MAAM;QAC7B,QAAQ;QACR,SAAS;QACT,aAAa;QACb,MAAM,EAAE;YACN,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;YACtD,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC;YACxD,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;YAC9D,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;YAC9D,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC;SACjE;QACD,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,6EAA6E;AAE7E,SAAS,MAAM,CAAC,CAAS,IAAY,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC;AACpF,SAAS,MAAM,CAAC,CAAS,IAAY,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AAEtE,6EAA6E;AAE7E;;;;GAIG;AACH,SAAgB,eAAe,CAAC,OAAqB,EAAE;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,KAAK,EAAE,CAAC;IAEhC,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;QACjC,MAAM,MAAM,GAAG,cAAc,EAAE,CAAC;QAChC,IAAI,CAAC,IAAA,oBAAU,EAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,4BAA4B,MAAM,2BAA2B,CAAC,CAAC;QACxG,OAAO,YAAY,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,WAAW,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAA,oBAAU,EAAC,MAAM,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,MAAM,wBAAwB,CAAC,CAAC;QAClG,OAAO,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,CAAC;IAED,cAAc;IACd,MAAM,QAAQ,GAAG,cAAc,EAAE,CAAC;IAClC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO;YACL,GAAG;YACH,MAAM,EAAE,8BAA8B;YACtC,QAAQ,EAAE,MAAM;YAChB,SAAS,EAAE,CAAC;YACZ,YAAY,EAAE,CAAC;YACf,QAAQ,EAAE,CAAC;YACX,SAAS,EAAE,CAAC;YACZ,aAAa,EAAE,CAAC;YAChB,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;YAC1E,QAAQ,EAAE,EAAE;SACb,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,IAAI,KAAK,UAAU;QAAE,OAAO,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;IAChG,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAEtE,OAAO,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;AACnD,CAAC"}
@@ -1,52 +1,18 @@
1
- export interface UsageSession {
2
- title: string;
3
- model: string;
4
- providerID: string;
5
- cost: number;
6
- tokensInput: number;
7
- tokensOutput: number;
8
- tokensReasoning: number;
9
- tokensCacheRead: number;
10
- tokensCacheWrite: number;
11
- timeCreated: number;
12
- timeUpdated: number;
13
- durationMin: number;
14
- /** 'user' = the user's own opencode windows; 'agent' = agent sessions. */
15
- attribution: 'user' | 'agent';
16
- /** True when the title starts with "TeamShare " (bridge/TeamShare work). */
17
- teamshare: boolean;
18
- }
19
- export interface UsageReport {
20
- day: string;
21
- dbPath: string;
22
- totalCost: number;
23
- sessionCount: number;
24
- userCost: number;
25
- agentCost: number;
26
- teamshareCost: number;
27
- tokens: {
28
- input: number;
29
- output: number;
30
- reasoning: number;
31
- cacheRead: number;
32
- cacheWrite: number;
33
- };
34
- sessions: UsageSession[];
35
- }
36
- export interface UsageOptions {
37
- /** YYYY-MM-DD (default: today, local time). */
38
- day?: string;
39
- /** Filter to sessions whose title carries `agent <id>`. */
40
- agent?: string;
41
- }
42
- /** Resolves the python binary: TEAMSHARE_PYTHON > known 3.14 > PATH python. */
43
- export declare function pythonBinary(): string;
44
- /** Absolute path to tools/usage_report.py (package root, whatever the cwd). */
45
- export declare function usageScriptPath(): string;
1
+ /**
2
+ * Agent spend reporting — reads cost data directly from harness databases
3
+ * via better-sqlite3 (replaces the Python tools/usage_report.py).
4
+ *
5
+ * Supports opencode (SQLite session table with cost data) and codex
6
+ * (placeholder — no cost data yet). Falls back to empty report when
7
+ * no provider database is found. Fully offline; no backend/daemon needed.
8
+ */
9
+ import { type UsageReport, type UsageOptions, type UsageSession } from './usage-reader';
10
+ export type { UsageReport, UsageSession, UsageOptions };
46
11
  export declare class UsageError extends Error {
47
12
  }
48
13
  /**
49
- * Runs the read-only spend report. Throws UsageError when the script or the
50
- * db is unavailable (the caller decides how loudly to fail).
14
+ * Runs the read-only spend report directly from the harness database.
15
+ * Throws UsageError when the database is unavailable (the caller decides
16
+ * how loudly to fail).
51
17
  */
52
18
  export declare function runUsageReport(opts?: UsageOptions): UsageReport;
package/dist/lib/usage.js CHANGED
@@ -1,69 +1,30 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UsageError = void 0;
4
- exports.pythonBinary = pythonBinary;
5
- exports.usageScriptPath = usageScriptPath;
6
4
  exports.runUsageReport = runUsageReport;
7
5
  /**
8
- * Agent spend reporting - typed wrapper around tools/usage_report.py.
6
+ * Agent spend reporting reads cost data directly from harness databases
7
+ * via better-sqlite3 (replaces the Python tools/usage_report.py).
9
8
  *
10
- * Node has no built-in sqlite, so the CLI (and the local-server /usage
11
- * endpoint) shells out to the Python script, which opens opencode.db with a
12
- * READ-ONLY sqlite uri. Never writes, never copies - the db is the source
13
- * of truth. Works fully offline; needs neither the backend nor the daemon.
9
+ * Supports opencode (SQLite session table with cost data) and codex
10
+ * (placeholder no cost data yet). Falls back to empty report when
11
+ * no provider database is found. Fully offline; no backend/daemon needed.
14
12
  */
15
- const node_fs_1 = require("node:fs");
16
- const node_child_process_1 = require("node:child_process");
17
- const node_path_1 = require("node:path");
18
- /** Python 3.14 on this machine - has sqlite3 in stdlib and pip (unlike the
19
- * undocumented PATH python 3.11). env TEAMSHARE_PYTHON overrides. */
20
- const KNOWN_PYTHON = 'C:\\Users\\Dominion Banjo\\AppData\\Local\\Python\\bin\\python.exe';
21
- /** Resolves the python binary: TEAMSHARE_PYTHON > known 3.14 > PATH python. */
22
- function pythonBinary() {
23
- if (process.env.TEAMSHARE_PYTHON)
24
- return process.env.TEAMSHARE_PYTHON;
25
- if ((0, node_fs_1.existsSync)(KNOWN_PYTHON))
26
- return KNOWN_PYTHON;
27
- return 'python';
28
- }
29
- /** Absolute path to tools/usage_report.py (package root, whatever the cwd). */
30
- function usageScriptPath() {
31
- return (0, node_path_1.join)(__dirname, '..', '..', 'tools', 'usage_report.py');
32
- }
13
+ const usage_reader_1 = require("./usage-reader");
33
14
  class UsageError extends Error {
34
15
  }
35
16
  exports.UsageError = UsageError;
36
17
  /**
37
- * Runs the read-only spend report. Throws UsageError when the script or the
38
- * db is unavailable (the caller decides how loudly to fail).
18
+ * Runs the read-only spend report directly from the harness database.
19
+ * Throws UsageError when the database is unavailable (the caller decides
20
+ * how loudly to fail).
39
21
  */
40
22
  function runUsageReport(opts = {}) {
41
- const script = usageScriptPath();
42
- if (!(0, node_fs_1.existsSync)(script)) {
43
- throw new UsageError(`usage script not found: ${script} (reinstall teamshare-bridge)`);
44
- }
45
- const args = [script, '--json'];
46
- if (opts.day)
47
- args.push('--day', opts.day);
48
- if (opts.agent)
49
- args.push('--agent', opts.agent);
50
- const res = (0, node_child_process_1.spawnSync)(pythonBinary(), args, { encoding: 'utf8' });
51
- if (res.error) {
52
- throw new UsageError(`could not run ${pythonBinary()} - is Python installed? (${res.error.message})`);
53
- }
54
- if (res.status !== 0) {
55
- const detail = (res.stderr || res.stdout || '').trim();
56
- throw new UsageError(detail || `usage report failed with exit code ${res.status}`);
57
- }
58
23
  try {
59
- const parsed = JSON.parse(res.stdout);
60
- if (typeof parsed.totalCost !== 'number' || !Array.isArray(parsed.sessions)) {
61
- throw new Error('unexpected report shape');
62
- }
63
- return parsed;
24
+ return (0, usage_reader_1.readUsageReport)(opts);
64
25
  }
65
26
  catch (err) {
66
- throw new UsageError(`could not parse usage report output: ${err instanceof Error ? err.message : String(err)}`);
27
+ throw new UsageError(`Could not read usage data: ${err instanceof Error ? err.message : String(err)}`);
67
28
  }
68
29
  }
69
30
  //# sourceMappingURL=usage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/lib/usage.ts"],"names":[],"mappings":";;;AA6DA,oCAIC;AAGD,0CAEC;AAQD,wCA6BC;AA3GD;;;;;;;GAOG;AACH,qCAAqC;AACrC,2DAA+C;AAC/C,yCAAiC;AA8CjC;qEACqE;AACrE,MAAM,YAAY,GAAG,oEAAoE,CAAC;AAE1F,+EAA+E;AAC/E,SAAgB,YAAY;IAC1B,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB;QAAE,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IACtE,IAAI,IAAA,oBAAU,EAAC,YAAY,CAAC;QAAE,OAAO,YAAY,CAAC;IAClD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,+EAA+E;AAC/E,SAAgB,eAAe;IAC7B,OAAO,IAAA,gBAAI,EAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,CAAC,CAAC;AACjE,CAAC;AAED,MAAa,UAAW,SAAQ,KAAK;CAAG;AAAxC,gCAAwC;AAExC;;;GAGG;AACH,SAAgB,cAAc,CAAC,OAAqB,EAAE;IACpD,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IACjC,IAAI,CAAC,IAAA,oBAAU,EAAC,MAAM,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,UAAU,CAAC,2BAA2B,MAAM,+BAA+B,CAAC,CAAC;IACzF,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,GAAG;QAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,IAAI,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,IAAA,8BAAS,EAAC,YAAY,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAClE,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;QACd,MAAM,IAAI,UAAU,CAClB,iBAAiB,YAAY,EAAE,4BAA4B,GAAG,CAAC,KAAK,CAAC,OAAO,GAAG,CAChF,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvD,MAAM,IAAI,UAAU,CAAC,MAAM,IAAI,sCAAsC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IACrF,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAgB,CAAC;QACrD,IAAI,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,UAAU,CAClB,wCAAwC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC3F,CAAC;IACJ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/lib/usage.ts"],"names":[],"mappings":";;;AAmBA,wCAQC;AA3BD;;;;;;;GAOG;AACH,iDAAyG;AAIzG,MAAa,UAAW,SAAQ,KAAK;CAAG;AAAxC,gCAAwC;AAExC;;;;GAIG;AACH,SAAgB,cAAc,CAAC,OAAqB,EAAE;IACpD,IAAI,CAAC;QACH,OAAO,IAAA,8BAAe,EAAC,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,UAAU,CAClB,8BAA8B,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACjF,CAAC;IACJ,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamshare-bridge",
3
- "version": "0.14.17",
3
+ "version": "0.14.18",
4
4
  "description": "Local bridge for TeamShare agents - CLI session runner, auto-wake WebSocket daemon, and teamshare:// protocol handler.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",
@@ -39,6 +39,8 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@napi-rs/keyring": "^1.3.0",
42
+ "@types/better-sqlite3": "^9.6.0",
43
+ "better-sqlite3": "^13.0.3",
42
44
  "socket.io-client": "^4.8.3"
43
45
  },
44
46
  "devDependencies": {
@@ -1,223 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- usage_report.py - read-only agent spend report from the local opencode db.
4
-
5
- Queries ~/.local/share/opencode/opencode.db (SQLite, WAL mode, ~3.5 GB) with a
6
- READ-ONLY uri - never writes, never copies. Node has no built-in sqlite, so
7
- the teamshare-agent CLI shells out to this script (offline, no deps, stdlib
8
- only: sqlite3/argparse/json/datetime).
9
-
10
- Attribution rules (source of truth: the `session` table):
11
- - title starting "TeamShare " -> TeamShare bridge agent session
12
- - providerID == "opencode" -> the user's own opencode windows
13
- - everything else -> other agent work (opencode-go, ...)
14
-
15
- Baseline (2026-08-16): $1.0445 across 14 cost-bearing sessions.
16
-
17
- Usage:
18
- python usage_report.py [--day YYYY-MM-DD] [--agent <id>] [--json]
19
- """
20
- import argparse
21
- import datetime
22
- import json
23
- import os
24
- import sqlite3
25
- import sys
26
-
27
- DB_PATH = os.path.join(
28
- os.path.expanduser("~"), ".local", "share", "opencode", "opencode.db"
29
- )
30
-
31
-
32
- def db_uri() -> str:
33
- """Read-only sqlite uri - NEVER open opencode.db for write (corruption
34
- while opencode runs); NEVER copy the 3.5 GB file. Query in place."""
35
- return "file:" + DB_PATH.replace("\\", "/") + "?mode=ro"
36
-
37
-
38
- def day_bounds(day: str) -> tuple[int, int]:
39
- """Local-midnight ms-epoch bounds for a YYYY-MM-DD day."""
40
- d = datetime.datetime.strptime(day, "%Y-%m-%d")
41
- start = d.replace(tzinfo=datetime.datetime.now().astimezone().tzinfo)
42
- end = start + datetime.timedelta(days=1)
43
- return int(start.timestamp() * 1000), int(end.timestamp() * 1000)
44
-
45
-
46
- def parse_model(model_json: str | None) -> dict:
47
- """`model` is a JSON string like {"id":..., "providerID":...}."""
48
- if not model_json:
49
- return {}
50
- try:
51
- parsed = json.loads(model_json)
52
- return parsed if isinstance(parsed, dict) else {}
53
- except (json.JSONDecodeError, TypeError):
54
- return {}
55
-
56
-
57
- def attribution(title: str | None, provider_id: str | None) -> tuple[str, bool]:
58
- """(label, is_teamshare): 'user' for the user's own windows, 'agent'
59
- otherwise; TeamShare-titled sessions are flagged for the bridge split."""
60
- if title and title.startswith("TeamShare "):
61
- return "agent", True
62
- if provider_id == "opencode":
63
- return "user", False
64
- return "agent", False
65
-
66
-
67
- def fmt_duration(ms: int) -> str:
68
- seconds = max(0, ms) // 1000
69
- if seconds < 60:
70
- return f"{seconds}s"
71
- minutes, sec = divmod(seconds, 60)
72
- return f"{minutes}m {sec}s" if minutes < 60 else f"{minutes // 60}h {minutes % 60}m"
73
-
74
-
75
- def fmt_time(ms: int) -> str:
76
- return datetime.datetime.fromtimestamp(ms / 1000).strftime("%H:%M")
77
-
78
-
79
- def run(day: str, agent: str | None, as_json: bool) -> int:
80
- if not os.path.exists(DB_PATH):
81
- print(
82
- f"usage: opencode db not found at {DB_PATH} (has opencode ever run?)",
83
- file=sys.stderr,
84
- )
85
- return 1
86
- try:
87
- con = sqlite3.connect(db_uri(), uri=True)
88
- except sqlite3.Error as err:
89
- print(f"usage: cannot open opencode db read-only: {err}", file=sys.stderr)
90
- return 1
91
-
92
- start_ms, end_ms = day_bounds(day)
93
- try:
94
- rows = con.execute(
95
- "SELECT title, model, cost, tokens_input, tokens_output,"
96
- " tokens_reasoning, tokens_cache_read, tokens_cache_write,"
97
- " time_created, time_updated"
98
- " FROM session WHERE time_created >= ? AND time_created < ?"
99
- " ORDER BY time_created",
100
- (start_ms, end_ms),
101
- ).fetchall()
102
- except sqlite3.Error as err:
103
- print(f"usage: query failed: {err}", file=sys.stderr)
104
- con.close()
105
- return 1
106
- con.close()
107
-
108
- sessions = []
109
- for (title, model_json, cost, t_in, t_out, t_reason, t_cr, t_cw,
110
- created, updated) in rows:
111
- cost = cost or 0.0
112
- title = title or "(untitled)"
113
- # Exclude opencode's "New session - ..." placeholders (empty shells
114
- # that never carry a title; one even holds 547k cached tokens at
115
- # $0). The baseline counts the remaining cost-bearing sessions, but
116
- # REAL sessions on free models also matter for attribution - they
117
- # show up at $0.0000 when they used tokens.
118
- if title.startswith("New session"):
119
- continue
120
- if cost <= 0 and not (t_in or t_out or t_reason or t_cr or t_cw):
121
- continue
122
- model = parse_model(model_json)
123
- provider_id = model.get("providerID") or ""
124
- label, teamshare = attribution(title, provider_id)
125
- sessions.append(
126
- {
127
- "title": title,
128
- "model": model.get("id") or "",
129
- "providerID": provider_id,
130
- "cost": round(cost, 6),
131
- "tokensInput": t_in or 0,
132
- "tokensOutput": t_out or 0,
133
- "tokensReasoning": t_reason or 0,
134
- "tokensCacheRead": t_cr or 0,
135
- "tokensCacheWrite": t_cw or 0,
136
- "timeCreated": created,
137
- "timeUpdated": updated,
138
- "durationMin": round(max(0, (updated or created) - created) / 60000, 1),
139
- "attribution": label,
140
- "teamshare": teamshare,
141
- }
142
- )
143
-
144
- total_cost = round(sum(s["cost"] for s in sessions), 6)
145
- user_cost = round(sum(s["cost"] for s in sessions if s["attribution"] == "user"), 6)
146
- agent_cost = round(total_cost - user_cost, 6)
147
- teamshare_cost = round(sum(s["cost"] for s in sessions if s["teamshare"]), 6)
148
-
149
- # --agent <id> filters to sessions whose title carries `agent <id>`
150
- # (the format every bridge session now records: "TeamShare task <taskId>
151
- # (agent <agentId>)").
152
- if agent:
153
- needle = f"agent {agent}".lower()
154
- sessions = [s for s in sessions if needle in s["title"].lower()]
155
- total_cost = round(sum(s["cost"] for s in sessions), 6)
156
- user_cost = round(sum(s["cost"] for s in sessions if s["attribution"] == "user"), 6)
157
- agent_cost = round(total_cost - user_cost, 6)
158
- teamshare_cost = round(sum(s["cost"] for s in sessions if s["teamshare"]), 6)
159
-
160
- tokens = {
161
- "input": sum(s["tokensInput"] for s in sessions),
162
- "output": sum(s["tokensOutput"] for s in sessions),
163
- "reasoning": sum(s["tokensReasoning"] for s in sessions),
164
- "cacheRead": sum(s["tokensCacheRead"] for s in sessions),
165
- "cacheWrite": sum(s["tokensCacheWrite"] for s in sessions),
166
- }
167
-
168
- if as_json:
169
- print(
170
- json.dumps(
171
- {
172
- "day": day,
173
- "dbPath": DB_PATH,
174
- "totalCost": total_cost,
175
- "sessionCount": len(sessions),
176
- "userCost": user_cost,
177
- "agentCost": agent_cost,
178
- "teamshareCost": teamshare_cost,
179
- "tokens": tokens,
180
- "sessions": sessions,
181
- },
182
- indent=2,
183
- )
184
- )
185
- return 0
186
-
187
- # Human table.
188
- print(f"Agent spend report - {day} (local time)")
189
- print(f"Source: {DB_PATH} (read-only)")
190
- print(f"Total: ${total_cost:.4f} across {len(sessions)} sessions")
191
- print(f" user windows : ${user_cost:.4f}")
192
- print(f" agent sessions : ${agent_cost:.4f} (of which TeamShare ${teamshare_cost:.4f})")
193
- print(f"Tokens: {tokens['input']:,} in / {tokens['output']:,} out / "
194
- f"{tokens['reasoning']:,} reasoning / "
195
- f"{tokens['cacheRead']:,} cache-read / {tokens['cacheWrite']:,} cache-write")
196
- print()
197
- print(f"{'time':<13} {'dur':<8} {'cost':<8} {'src':<8} {'model':<34} title")
198
- print("-" * 110)
199
- for s in sessions:
200
- src = "user" if s["attribution"] == "user" else ("teamshare" if s["teamshare"] else "agent")
201
- span = f"{fmt_time(s['timeCreated'])}-{fmt_time(s['timeUpdated'])}"
202
- print(
203
- f"{span:<13} {s['durationMin']:>4}m {s['cost']:>7.4f} {src:<8} "
204
- f"{(s['providerID'] + '/' + s['model'])[:34]:<34} {s['title'][:60]}"
205
- )
206
- return 0
207
-
208
-
209
- def main() -> int:
210
- parser = argparse.ArgumentParser(description="Read-only opencode spend report")
211
- parser.add_argument(
212
- "--day",
213
- default=datetime.date.today().isoformat(),
214
- help="YYYY-MM-DD (default: today, local time)",
215
- )
216
- parser.add_argument("--agent", default=None, help="filter sessions for one agent id")
217
- parser.add_argument("--json", action="store_true", help="machine-readable output")
218
- args = parser.parse_args()
219
- return run(args.day, args.agent, args.json)
220
-
221
-
222
- if __name__ == "__main__":
223
- sys.exit(main())