opencode-usage 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/index.js +76 -177
- package/dist/index.js.map +3 -3
- package/dist/loader.d.ts +0 -7
- package/dist/types.d.ts +1 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,13 +87,13 @@ opencode-usage --provider anthropic --since 7d --json
|
|
|
87
87
|
|
|
88
88
|
## How It Works
|
|
89
89
|
|
|
90
|
-
This tool reads OpenCode session data from:
|
|
90
|
+
This tool reads OpenCode session data from the SQLite database (`opencode.db`):
|
|
91
91
|
|
|
92
|
-
- Linux: `~/.local/share/opencode/
|
|
93
|
-
- macOS: `~/.local/share/opencode/
|
|
94
|
-
- Windows: `%LOCALAPPDATA%/opencode/
|
|
92
|
+
- Linux: `~/.local/share/opencode/opencode.db`
|
|
93
|
+
- macOS: `~/.local/share/opencode/opencode.db`
|
|
94
|
+
- Windows: `%LOCALAPPDATA%/opencode/opencode.db`
|
|
95
95
|
|
|
96
|
-
It aggregates token usage by day and calculates estimated costs based on current API pricing.
|
|
96
|
+
Requires OpenCode v1.2.0+ (SQLite storage). It aggregates token usage by day and calculates estimated costs based on current API pricing.
|
|
97
97
|
|
|
98
98
|
## Note on Costs
|
|
99
99
|
|
package/dist/index.js
CHANGED
|
@@ -114,195 +114,94 @@ Examples:
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
// src/loader.ts
|
|
117
|
-
import {
|
|
117
|
+
import { Database } from "bun:sqlite";
|
|
118
118
|
import { homedir } from "os";
|
|
119
119
|
import { join } from "path";
|
|
120
|
-
var isBun = typeof globalThis.Bun !== "undefined";
|
|
121
|
-
var BATCH_SIZE = 1e4;
|
|
122
120
|
function getOpenCodeStoragePath() {
|
|
123
121
|
const xdgDataHome = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
|
|
124
|
-
return join(xdgDataHome, "opencode"
|
|
122
|
+
return join(xdgDataHome, "opencode");
|
|
125
123
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return JSON.parse(content);
|
|
129
|
-
}
|
|
130
|
-
async function collectFilePaths(messagesDir) {
|
|
131
|
-
const sessionDirs = await readdir(messagesDir);
|
|
132
|
-
const pathArrays = await Promise.all(sessionDirs.map(async (sessionDir) => {
|
|
133
|
-
const sessionPath = join(messagesDir, sessionDir);
|
|
134
|
-
const st = await stat(sessionPath);
|
|
135
|
-
if (!st.isDirectory())
|
|
136
|
-
return [];
|
|
137
|
-
const files = await readdir(sessionPath);
|
|
138
|
-
return files.filter((f) => f.endsWith(".json")).map((f) => join(sessionPath, f));
|
|
139
|
-
}));
|
|
140
|
-
return pathArrays.flat();
|
|
124
|
+
function openDb(dataPath) {
|
|
125
|
+
return new Database(join(dataPath, "opencode.db"), { readonly: true });
|
|
141
126
|
}
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
127
|
+
function rowToMessage(row) {
|
|
128
|
+
const data = JSON.parse(row.data);
|
|
129
|
+
return {
|
|
130
|
+
id: row.id,
|
|
131
|
+
sessionID: row.session_id,
|
|
132
|
+
...data
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function isValidMessage(msg, providerFilter) {
|
|
136
|
+
if (msg.role === "user")
|
|
137
|
+
return false;
|
|
138
|
+
if (!msg.tokens)
|
|
139
|
+
return false;
|
|
140
|
+
if (providerFilter) {
|
|
141
|
+
const providerId = msg.model?.providerID ?? msg.providerID ?? "unknown";
|
|
142
|
+
if (providerId.toLowerCase() !== providerFilter)
|
|
143
|
+
return false;
|
|
148
144
|
}
|
|
149
|
-
return
|
|
145
|
+
return true;
|
|
150
146
|
}
|
|
151
147
|
async function loadRecentMessages(storagePath, hoursBack = 24, providerFilter) {
|
|
152
|
-
const messagesDir = join(storagePath, "message");
|
|
153
148
|
const cutoffTime = Date.now() - hoursBack * 60 * 60 * 1000;
|
|
154
149
|
try {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (!sessionStat.isDirectory())
|
|
162
|
-
continue;
|
|
163
|
-
if (sessionStat.mtimeMs < cutoffTime) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
} catch {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
const files = await readdir(sessionPath);
|
|
170
|
-
const fileStats = await Promise.all(files.map(async (file) => {
|
|
171
|
-
if (!file.endsWith(".json"))
|
|
172
|
-
return null;
|
|
173
|
-
const filePath = join(sessionPath, file);
|
|
174
|
-
try {
|
|
175
|
-
const fileStat = await stat(filePath);
|
|
176
|
-
return { filePath, mtime: fileStat.mtimeMs };
|
|
177
|
-
} catch {
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
}));
|
|
181
|
-
for (const fileInfo of fileStats) {
|
|
182
|
-
if (fileInfo && fileInfo.mtime >= cutoffTime) {
|
|
183
|
-
recentFiles.push(fileInfo.filePath);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
150
|
+
const db = openDb(storagePath);
|
|
151
|
+
try {
|
|
152
|
+
const rows = db.query(`SELECT id, session_id, time_created, data FROM message WHERE time_created >= ?`).all(cutoffTime);
|
|
153
|
+
return rows.map(rowToMessage).filter((msg) => isValidMessage(msg, providerFilter));
|
|
154
|
+
} finally {
|
|
155
|
+
db.close();
|
|
186
156
|
}
|
|
187
|
-
const results = await processInBatches(recentFiles, async (filePath) => {
|
|
188
|
-
try {
|
|
189
|
-
return await readJsonFile(filePath);
|
|
190
|
-
} catch {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}, BATCH_SIZE);
|
|
194
|
-
return results.filter((msg) => {
|
|
195
|
-
if (!msg)
|
|
196
|
-
return false;
|
|
197
|
-
if (msg.role === "user")
|
|
198
|
-
return false;
|
|
199
|
-
if (!msg.tokens)
|
|
200
|
-
return false;
|
|
201
|
-
if (providerFilter) {
|
|
202
|
-
const providerId = msg.model?.providerID ?? msg.providerID ?? "unknown";
|
|
203
|
-
if (providerId.toLowerCase() !== providerFilter)
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
return true;
|
|
207
|
-
});
|
|
208
157
|
} catch (err) {
|
|
209
158
|
console.error(`Error reading recent messages: ${err}`);
|
|
210
159
|
return [];
|
|
211
160
|
}
|
|
212
161
|
}
|
|
213
162
|
async function loadMessages(storagePath, providerFilter) {
|
|
214
|
-
const messagesDir = join(storagePath, "message");
|
|
215
163
|
try {
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}, BATCH_SIZE);
|
|
224
|
-
return results.filter((msg) => {
|
|
225
|
-
if (!msg)
|
|
226
|
-
return false;
|
|
227
|
-
if (msg.role === "user")
|
|
228
|
-
return false;
|
|
229
|
-
if (!msg.tokens)
|
|
230
|
-
return false;
|
|
231
|
-
if (providerFilter) {
|
|
232
|
-
const providerId = msg.model?.providerID ?? msg.providerID ?? "unknown";
|
|
233
|
-
if (providerId.toLowerCase() !== providerFilter)
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
return true;
|
|
237
|
-
});
|
|
164
|
+
const db = openDb(storagePath);
|
|
165
|
+
try {
|
|
166
|
+
const rows = db.query(`SELECT id, session_id, time_created, data FROM message`).all();
|
|
167
|
+
return rows.map(rowToMessage).filter((msg) => isValidMessage(msg, providerFilter));
|
|
168
|
+
} finally {
|
|
169
|
+
db.close();
|
|
170
|
+
}
|
|
238
171
|
} catch (err) {
|
|
239
|
-
console.error(`Error reading messages
|
|
172
|
+
console.error(`Error reading messages from database: ${err}`);
|
|
240
173
|
return [];
|
|
241
174
|
}
|
|
242
175
|
}
|
|
243
176
|
function createCursor() {
|
|
244
|
-
return {
|
|
245
|
-
knownSessions: new Set,
|
|
246
|
-
fileCountPerSession: new Map,
|
|
247
|
-
lastTimestamp: 0
|
|
248
|
-
};
|
|
177
|
+
return { lastTimestamp: 0 };
|
|
249
178
|
}
|
|
250
179
|
async function loadMessagesIncremental(storagePath, cursor, providerFilter) {
|
|
251
|
-
const messagesDir = join(storagePath, "message");
|
|
252
|
-
const newMessages = [];
|
|
253
|
-
const newCursor = {
|
|
254
|
-
knownSessions: new Set(cursor.knownSessions),
|
|
255
|
-
fileCountPerSession: new Map(cursor.fileCountPerSession),
|
|
256
|
-
lastTimestamp: cursor.lastTimestamp
|
|
257
|
-
};
|
|
258
180
|
try {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const previousCount = cursor.fileCountPerSession.get(sessionDir) ?? 0;
|
|
268
|
-
if (jsonFiles.length > previousCount) {
|
|
269
|
-
const sortedFiles = jsonFiles.sort();
|
|
270
|
-
const newFiles = sortedFiles.slice(previousCount);
|
|
271
|
-
for (const file of newFiles) {
|
|
272
|
-
const filePath = join(sessionPath, file);
|
|
273
|
-
try {
|
|
274
|
-
const msg = await readJsonFile(filePath);
|
|
275
|
-
if (isValidMessage(msg, providerFilter)) {
|
|
276
|
-
newMessages.push(msg);
|
|
277
|
-
if (msg.time?.created && msg.time.created > newCursor.lastTimestamp) {
|
|
278
|
-
newCursor.lastTimestamp = msg.time.created;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
} catch {}
|
|
181
|
+
const db = openDb(storagePath);
|
|
182
|
+
try {
|
|
183
|
+
const rows = db.query(`SELECT id, session_id, time_created, data FROM message WHERE time_created > ?`).all(cursor.lastTimestamp);
|
|
184
|
+
const messages = rows.map(rowToMessage).filter((msg) => isValidMessage(msg, providerFilter));
|
|
185
|
+
let maxTimestamp = cursor.lastTimestamp;
|
|
186
|
+
for (const row of rows) {
|
|
187
|
+
if (row.time_created > maxTimestamp) {
|
|
188
|
+
maxTimestamp = row.time_created;
|
|
282
189
|
}
|
|
283
190
|
}
|
|
284
|
-
|
|
285
|
-
|
|
191
|
+
return {
|
|
192
|
+
messages,
|
|
193
|
+
cursor: { lastTimestamp: maxTimestamp }
|
|
194
|
+
};
|
|
195
|
+
} finally {
|
|
196
|
+
db.close();
|
|
286
197
|
}
|
|
287
|
-
return { messages: newMessages, cursor: newCursor };
|
|
288
198
|
} catch (err) {
|
|
289
199
|
console.error(`Error in incremental load: ${err}`);
|
|
290
|
-
return {
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (!msg)
|
|
295
|
-
return false;
|
|
296
|
-
if (msg.role === "user")
|
|
297
|
-
return false;
|
|
298
|
-
if (!msg.tokens)
|
|
299
|
-
return false;
|
|
300
|
-
if (providerFilter) {
|
|
301
|
-
const providerId = msg.model?.providerID ?? msg.providerID ?? "unknown";
|
|
302
|
-
if (providerId.toLowerCase() !== providerFilter)
|
|
303
|
-
return false;
|
|
200
|
+
return {
|
|
201
|
+
messages: [],
|
|
202
|
+
cursor: { lastTimestamp: cursor.lastTimestamp }
|
|
203
|
+
};
|
|
304
204
|
}
|
|
305
|
-
return true;
|
|
306
205
|
}
|
|
307
206
|
|
|
308
207
|
// src/pricing.ts
|
|
@@ -11236,8 +11135,8 @@ function convertToDebugSymbols(symbols) {
|
|
|
11236
11135
|
const p99Width = Math.max(p99Header.length, ...allStats.map((s) => s.p99.toFixed(2).length));
|
|
11237
11136
|
lines.push(`${nameHeader.padEnd(nameWidth)} | ${callsHeader.padStart(countWidth)} | ${totalHeader.padStart(totalWidth)} | ${avgHeader.padStart(avgWidth)} | ${minHeader.padStart(minWidth)} | ${maxHeader.padStart(maxWidth)} | ${medHeader.padStart(medianWidth)} | ${p90Header.padStart(p90Width)} | ${p99Header.padStart(p99Width)}`);
|
|
11238
11137
|
lines.push(`${"-".repeat(nameWidth)}-+-${"-".repeat(countWidth)}-+-${"-".repeat(totalWidth)}-+-${"-".repeat(avgWidth)}-+-${"-".repeat(minWidth)}-+-${"-".repeat(maxWidth)}-+-${"-".repeat(medianWidth)}-+-${"-".repeat(p90Width)}-+-${"-".repeat(p99Width)}`);
|
|
11239
|
-
allStats.forEach((
|
|
11240
|
-
lines.push(`${
|
|
11138
|
+
allStats.forEach((stat) => {
|
|
11139
|
+
lines.push(`${stat.name.padEnd(nameWidth)} | ${String(stat.count).padStart(countWidth)} | ${stat.total.toFixed(2).padStart(totalWidth)} | ${stat.average.toFixed(2).padStart(avgWidth)} | ${stat.min.toFixed(2).padStart(minWidth)} | ${stat.max.toFixed(2).padStart(maxWidth)} | ${stat.median.toFixed(2).padStart(medianWidth)} | ${stat.p90.toFixed(2).padStart(p90Width)} | ${stat.p99.toFixed(2).padStart(p99Width)}`);
|
|
11241
11140
|
});
|
|
11242
11141
|
}
|
|
11243
11142
|
lines.push("-------------------------------------------------------------------------------------------------------------------------");
|
|
@@ -28167,13 +28066,13 @@ var render = async (node, rendererOrConfig = {}) => {
|
|
|
28167
28066
|
};
|
|
28168
28067
|
|
|
28169
28068
|
// src/quota-loader.ts
|
|
28170
|
-
import { readFile
|
|
28069
|
+
import { readFile } from "fs/promises";
|
|
28171
28070
|
import { homedir as homedir2 } from "os";
|
|
28172
28071
|
import { join as join3 } from "path";
|
|
28173
|
-
var
|
|
28174
|
-
async function
|
|
28072
|
+
var isBun = typeof globalThis.Bun !== "undefined";
|
|
28073
|
+
async function readJsonFile(filePath) {
|
|
28175
28074
|
try {
|
|
28176
|
-
const content =
|
|
28075
|
+
const content = isBun ? await Bun.file(filePath).text() : await readFile(filePath, "utf-8");
|
|
28177
28076
|
return JSON.parse(content);
|
|
28178
28077
|
} catch {
|
|
28179
28078
|
return null;
|
|
@@ -28181,7 +28080,7 @@ async function readJsonFile2(filePath) {
|
|
|
28181
28080
|
}
|
|
28182
28081
|
async function loadMultiAccountQuota() {
|
|
28183
28082
|
const path2 = join3(homedir2(), ".local/share/opencode/multi-account-state.json");
|
|
28184
|
-
const state = await
|
|
28083
|
+
const state = await readJsonFile(path2);
|
|
28185
28084
|
if (!state?.usage) {
|
|
28186
28085
|
return [
|
|
28187
28086
|
{
|
|
@@ -28232,7 +28131,7 @@ async function loadMultiAccountQuota() {
|
|
|
28232
28131
|
}
|
|
28233
28132
|
async function loadAntigravityQuota() {
|
|
28234
28133
|
const path2 = join3(homedir2(), ".config/opencode/antigravity-accounts.json");
|
|
28235
|
-
const data = await
|
|
28134
|
+
const data = await readJsonFile(path2);
|
|
28236
28135
|
if (!data?.accounts?.length) {
|
|
28237
28136
|
return [
|
|
28238
28137
|
{
|
|
@@ -28287,15 +28186,15 @@ async function loadAntigravityQuota() {
|
|
|
28287
28186
|
}
|
|
28288
28187
|
|
|
28289
28188
|
// src/codex-client.ts
|
|
28290
|
-
import { readFile as
|
|
28189
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
28291
28190
|
import { homedir as homedir3 } from "os";
|
|
28292
28191
|
import { join as join4 } from "path";
|
|
28293
|
-
var
|
|
28192
|
+
var isBun2 = typeof globalThis.Bun !== "undefined";
|
|
28294
28193
|
var CODEX_API_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
28295
28194
|
var CODEX_AUTH_PATH = join4(homedir3(), ".codex", "auth.json");
|
|
28296
28195
|
async function readCodexAuthToken() {
|
|
28297
28196
|
try {
|
|
28298
|
-
const content =
|
|
28197
|
+
const content = isBun2 ? await Bun.file(CODEX_AUTH_PATH).text() : await readFile2(CODEX_AUTH_PATH, "utf-8");
|
|
28299
28198
|
const auth = JSON.parse(content);
|
|
28300
28199
|
if (auth.OPENAI_API_KEY) {
|
|
28301
28200
|
return auth.OPENAI_API_KEY;
|
|
@@ -28472,9 +28371,9 @@ function UsageTable(props) {
|
|
|
28472
28371
|
const windowedData = getWindowedData();
|
|
28473
28372
|
let totalTokens = 0;
|
|
28474
28373
|
let totalCost = 0;
|
|
28475
|
-
windowedData.forEach(([_2,
|
|
28476
|
-
totalTokens +=
|
|
28477
|
-
totalCost +=
|
|
28374
|
+
windowedData.forEach(([_2, stat]) => {
|
|
28375
|
+
totalTokens += stat.input + stat.output;
|
|
28376
|
+
totalCost += stat.cost;
|
|
28478
28377
|
});
|
|
28479
28378
|
return {
|
|
28480
28379
|
tokens: totalTokens,
|
|
@@ -28552,10 +28451,10 @@ function UsageTable(props) {
|
|
|
28552
28451
|
get each() {
|
|
28553
28452
|
return statsArray();
|
|
28554
28453
|
},
|
|
28555
|
-
children: ([dateKey,
|
|
28454
|
+
children: ([dateKey, stat], index) => {
|
|
28556
28455
|
const isLast = index() === statsArray().length - 1;
|
|
28557
28456
|
const isEven = index() % 2 === 0;
|
|
28558
|
-
const providers = Array.from(
|
|
28457
|
+
const providers = Array.from(stat.providerStats.entries());
|
|
28559
28458
|
return [(() => {
|
|
28560
28459
|
var _el$26 = createElement("box"), _el$27 = createElement("text"), _el$28 = createElement("span"), _el$29 = createElement("span"), _el$30 = createElement("span");
|
|
28561
28460
|
insertNode(_el$26, _el$27);
|
|
@@ -28566,8 +28465,8 @@ function UsageTable(props) {
|
|
|
28566
28465
|
setProp(_el$27, "overflow", "hidden");
|
|
28567
28466
|
setProp(_el$27, "wrapMode", "none");
|
|
28568
28467
|
insert(_el$28, () => padRight2(dateKey, 18));
|
|
28569
|
-
insert(_el$29, () => padLeft2(formatNum(
|
|
28570
|
-
insert(_el$30, () => padLeft2(formatCost2(
|
|
28468
|
+
insert(_el$29, () => padLeft2(formatNum(stat.input + stat.output), 13));
|
|
28469
|
+
insert(_el$30, () => padLeft2(formatCost2(stat.cost), 10));
|
|
28571
28470
|
effect((_p$) => {
|
|
28572
28471
|
var _v$1 = providers.length > 0 ? 0.25 : 0.5, _v$10 = isEven ? COLORS.bg.secondary : COLORS.bg.accent, _v$11 = {
|
|
28573
28472
|
fg: COLORS.text.primary,
|
|
@@ -29223,7 +29122,7 @@ async function runSolidDashboard(options) {
|
|
|
29223
29122
|
}
|
|
29224
29123
|
|
|
29225
29124
|
// src/config-commands.ts
|
|
29226
|
-
import { readFile as
|
|
29125
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
29227
29126
|
import { homedir as homedir5 } from "os";
|
|
29228
29127
|
import { join as join6 } from "path";
|
|
29229
29128
|
|
|
@@ -29243,7 +29142,7 @@ async function showConfig() {
|
|
|
29243
29142
|
Configuration file: ${configPath}`);
|
|
29244
29143
|
let hasCodexAuth = false;
|
|
29245
29144
|
try {
|
|
29246
|
-
const content = await
|
|
29145
|
+
const content = await readFile3(CODEX_AUTH_PATH2, "utf-8");
|
|
29247
29146
|
const auth = JSON.parse(content);
|
|
29248
29147
|
hasCodexAuth = !!auth.tokens?.access_token;
|
|
29249
29148
|
} catch {}
|
|
@@ -29346,4 +29245,4 @@ async function main2() {
|
|
|
29346
29245
|
}
|
|
29347
29246
|
main2().catch(console.error);
|
|
29348
29247
|
|
|
29349
|
-
//# debugId=
|
|
29248
|
+
//# debugId=71CC1B06755E934764756E2164756E21
|