opencode-usage 0.3.3 → 0.4.1

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 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/storage/`
93
- - macOS: `~/.local/share/opencode/storage/`
94
- - Windows: `%LOCALAPPDATA%/opencode/storage/`
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 { readdir, stat, readFile } from "fs/promises";
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", "storage");
122
+ return join(xdgDataHome, "opencode");
125
123
  }
126
- async function readJsonFile(filePath) {
127
- const content = isBun ? await Bun.file(filePath).text() : await readFile(filePath, "utf-8");
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
- async function processInBatches(items, fn, batchSize) {
143
- const results = [];
144
- for (let i = 0;i < items.length; i += batchSize) {
145
- const batch = items.slice(i, i + batchSize);
146
- const batchResults = await Promise.all(batch.map(fn));
147
- results.push(...batchResults);
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 results;
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 sessionDirs = await readdir(messagesDir);
156
- const recentFiles = [];
157
- for (const sessionDir of sessionDirs) {
158
- const sessionPath = join(messagesDir, sessionDir);
159
- try {
160
- const sessionStat = await stat(sessionPath);
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 filePaths = await collectFilePaths(messagesDir);
217
- const results = await processInBatches(filePaths, async (filePath) => {
218
- try {
219
- return await readJsonFile(filePath);
220
- } catch {
221
- return null;
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 directory: ${err}`);
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 sessionDirs = await readdir(messagesDir);
260
- for (const sessionDir of sessionDirs) {
261
- const sessionPath = join(messagesDir, sessionDir);
262
- const st = await stat(sessionPath);
263
- if (!st.isDirectory())
264
- continue;
265
- const files = await readdir(sessionPath);
266
- const jsonFiles = files.filter((f) => f.endsWith(".json"));
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
- newCursor.knownSessions.add(sessionDir);
285
- newCursor.fileCountPerSession.set(sessionDir, jsonFiles.length);
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 { messages: [], cursor: newCursor };
291
- }
292
- }
293
- function isValidMessage(msg, providerFilter) {
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((stat2) => {
11240
- lines.push(`${stat2.name.padEnd(nameWidth)} | ${String(stat2.count).padStart(countWidth)} | ${stat2.total.toFixed(2).padStart(totalWidth)} | ${stat2.average.toFixed(2).padStart(avgWidth)} | ${stat2.min.toFixed(2).padStart(minWidth)} | ${stat2.max.toFixed(2).padStart(maxWidth)} | ${stat2.median.toFixed(2).padStart(medianWidth)} | ${stat2.p90.toFixed(2).padStart(p90Width)} | ${stat2.p99.toFixed(2).padStart(p99Width)}`);
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 as readFile2 } from "fs/promises";
28069
+ import { readFile } from "fs/promises";
28171
28070
  import { homedir as homedir2 } from "os";
28172
28071
  import { join as join3 } from "path";
28173
- var isBun2 = typeof globalThis.Bun !== "undefined";
28174
- async function readJsonFile2(filePath) {
28072
+ var isBun = typeof globalThis.Bun !== "undefined";
28073
+ async function readJsonFile(filePath) {
28175
28074
  try {
28176
- const content = isBun2 ? await Bun.file(filePath).text() : await readFile2(filePath, "utf-8");
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 readJsonFile2(path2);
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 readJsonFile2(path2);
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 readFile3 } from "fs/promises";
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 isBun3 = typeof globalThis.Bun !== "undefined";
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 = isBun3 ? await Bun.file(CODEX_AUTH_PATH).text() : await readFile3(CODEX_AUTH_PATH, "utf-8");
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, stat2]) => {
28476
- totalTokens += stat2.input + stat2.output;
28477
- totalCost += stat2.cost;
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, stat2], index) => {
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(stat2.providerStats.entries());
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(stat2.input + stat2.output), 13));
28570
- insert(_el$30, () => padLeft2(formatCost2(stat2.cost), 10));
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 readFile4 } from "fs/promises";
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 readFile4(CODEX_AUTH_PATH2, "utf-8");
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=4F3AFA8CCC597E5564756E2164756E21
29248
+ //# debugId=71CC1B06755E934764756E2164756E21