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.
- package/dist/lib/usage-reader.d.ts +46 -0
- package/dist/lib/usage-reader.js +206 -0
- package/dist/lib/usage-reader.js.map +1 -0
- package/dist/lib/usage.d.ts +13 -47
- package/dist/lib/usage.js +11 -50
- package/dist/lib/usage.js.map +1 -1
- package/package.json +3 -1
- package/tools/usage_report.py +0 -223
|
@@ -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":"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"}
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -1,52 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
50
|
-
*
|
|
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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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
|
|
38
|
-
*
|
|
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
|
-
|
|
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(`
|
|
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
|
package/dist/lib/usage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../../src/lib/usage.ts"],"names":[],"mappings":";;;
|
|
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.
|
|
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": {
|
package/tools/usage_report.py
DELETED
|
@@ -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())
|