opencode-usage 0.3.1 → 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/dist/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var __require = import.meta.require;
4
3
 
5
4
  // src/cli.ts
6
5
  import { parseArgs as nodeParseArgs } from "util";
@@ -47,10 +46,8 @@ function parseArgs() {
47
46
  json: { type: "boolean", short: "j" },
48
47
  monthly: { type: "boolean", short: "m" },
49
48
  watch: { type: "boolean", short: "w" },
50
- dashboard: { type: "boolean", short: "D" },
51
- "codex-token": { type: "string" },
49
+ stats: { type: "boolean", short: "S" },
52
50
  config: { type: "string" },
53
- token: { type: "string" },
54
51
  help: { type: "boolean", short: "h" }
55
52
  },
56
53
  strict: true
@@ -67,10 +64,8 @@ function parseArgs() {
67
64
  json: values.json,
68
65
  monthly: values.monthly,
69
66
  watch: values.watch,
70
- dashboard: values.dashboard,
71
- codexToken: values["codex-token"],
72
- config: values.config,
73
- configToken: values.token
67
+ stats: values.stats,
68
+ config: values.config
74
69
  };
75
70
  } catch (error) {
76
71
  if (error instanceof Error && error.message.includes("Unknown option")) {
@@ -88,236 +83,125 @@ opencode-usage - Track OpenCode AI coding assistant usage and costs
88
83
  Usage:
89
84
  bunx opencode-usage [options]
90
85
 
86
+ Modes:
87
+ (default) Interactive dashboard (Bun only)
88
+ -S, --stats Stats table mode (works with Node.js too)
89
+
91
90
  Options:
92
91
  -p, --provider <name> Filter by provider (anthropic, openai, google, opencode)
93
92
  -d, --days <n> Show only last N days
94
93
  -s, --since <date> Start date (YYYYMMDD, YYYY-MM-DD, or 7d/1w/1m)
95
94
  -u, --until <date> End date (YYYYMMDD, YYYY-MM-DD, or 7d/1w/1m)
96
- -j, --json Output as JSON
97
- -m, --monthly Aggregate by month instead of day
98
- -w, --watch Watch mode - refresh every 5 minutes
99
- -D, --dashboard Dashboard mode - unified multi-source view (Bun only)
100
- --codex-token <t> Codex API token for quota display in dashboard
101
- --config <cmd> Config commands: set-codex-token, show
95
+ -j, --json Output as JSON (stats mode only)
96
+ -m, --monthly Aggregate by month (stats mode only)
97
+ -w, --watch Watch mode - refresh every 5 minutes (stats mode only)
98
+ --config show Show current configuration
102
99
  -h, --help Show this help message
103
100
 
104
- Config Commands:
105
- --config show Show current configuration
106
- --config set-codex-token --token <tok> Save Codex API token to config
101
+ Codex Quota:
102
+ Dashboard auto-reads Codex auth from ~/.codex/auth.json.
103
+ Run 'codex login' to authenticate.
107
104
 
108
105
  Examples:
109
106
  bunx opencode-usage
110
- bunx opencode-usage --provider anthropic
111
- bunx opencode-usage -p openai -d 30
112
- bunx opencode-usage --since 20251201 --until 20251231
113
- bunx opencode-usage --since 7d
114
- bunx opencode-usage --monthly --json
115
- bunx opencode-usage --watch
116
- bunx opencode-usage -w -d 1
117
- bunx opencode-usage --dashboard
118
- bunx opencode-usage --dashboard --codex-token <token>
107
+ bunx opencode-usage --stats
108
+ bunx opencode-usage --stats --provider anthropic
109
+ bunx opencode-usage --stats -p openai -d 30
110
+ bunx opencode-usage --stats --since 7d --monthly --json
111
+ bunx opencode-usage --stats -w -d 1
119
112
  bunx opencode-usage --config show
120
- bunx opencode-usage --config set-codex-token --token sk-...
121
-
122
- How to get Codex token:
123
- 1. Open chatgpt.com in browser
124
- 2. Open DevTools (F12 or Cmd+Option+I)
125
- 3. Go to Network tab and reload page
126
- 4. Find request to 'backend-api/wham/usage'
127
- 5. Copy 'Authorization' header value (starts with 'Bearer ')
128
113
  `);
129
114
  }
130
115
 
131
116
  // src/loader.ts
132
- import { readdir, stat, readFile } from "fs/promises";
117
+ import { Database } from "bun:sqlite";
133
118
  import { homedir } from "os";
134
119
  import { join } from "path";
135
- var isBun = typeof globalThis.Bun !== "undefined";
136
- var BATCH_SIZE = 1e4;
137
120
  function getOpenCodeStoragePath() {
138
121
  const xdgDataHome = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
139
- return join(xdgDataHome, "opencode", "storage");
122
+ return join(xdgDataHome, "opencode");
140
123
  }
141
- async function readJsonFile(filePath) {
142
- const content = isBun ? await Bun.file(filePath).text() : await readFile(filePath, "utf-8");
143
- return JSON.parse(content);
144
- }
145
- async function collectFilePaths(messagesDir) {
146
- const sessionDirs = await readdir(messagesDir);
147
- const pathArrays = await Promise.all(sessionDirs.map(async (sessionDir) => {
148
- const sessionPath = join(messagesDir, sessionDir);
149
- const st = await stat(sessionPath);
150
- if (!st.isDirectory())
151
- return [];
152
- const files = await readdir(sessionPath);
153
- return files.filter((f) => f.endsWith(".json")).map((f) => join(sessionPath, f));
154
- }));
155
- return pathArrays.flat();
124
+ function openDb(dataPath) {
125
+ return new Database(join(dataPath, "opencode.db"), { readonly: true });
156
126
  }
157
- async function processInBatches(items, fn, batchSize) {
158
- const results = [];
159
- for (let i = 0;i < items.length; i += batchSize) {
160
- const batch = items.slice(i, i + batchSize);
161
- const batchResults = await Promise.all(batch.map(fn));
162
- 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;
163
144
  }
164
- return results;
145
+ return true;
165
146
  }
166
147
  async function loadRecentMessages(storagePath, hoursBack = 24, providerFilter) {
167
- const messagesDir = join(storagePath, "message");
168
148
  const cutoffTime = Date.now() - hoursBack * 60 * 60 * 1000;
169
149
  try {
170
- const sessionDirs = await readdir(messagesDir);
171
- const recentFiles = [];
172
- for (const sessionDir of sessionDirs) {
173
- const sessionPath = join(messagesDir, sessionDir);
174
- try {
175
- const sessionStat = await stat(sessionPath);
176
- if (!sessionStat.isDirectory())
177
- continue;
178
- if (sessionStat.mtimeMs < cutoffTime) {
179
- continue;
180
- }
181
- } catch {
182
- continue;
183
- }
184
- const files = await readdir(sessionPath);
185
- const fileStats = await Promise.all(files.map(async (file) => {
186
- if (!file.endsWith(".json"))
187
- return null;
188
- const filePath = join(sessionPath, file);
189
- try {
190
- const fileStat = await stat(filePath);
191
- return { filePath, mtime: fileStat.mtimeMs };
192
- } catch {
193
- return null;
194
- }
195
- }));
196
- for (const fileInfo of fileStats) {
197
- if (fileInfo && fileInfo.mtime >= cutoffTime) {
198
- recentFiles.push(fileInfo.filePath);
199
- }
200
- }
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();
201
156
  }
202
- const results = await processInBatches(recentFiles, async (filePath) => {
203
- try {
204
- return await readJsonFile(filePath);
205
- } catch {
206
- return null;
207
- }
208
- }, BATCH_SIZE);
209
- return results.filter((msg) => {
210
- if (!msg)
211
- return false;
212
- if (msg.role === "user")
213
- return false;
214
- if (!msg.tokens)
215
- return false;
216
- if (providerFilter) {
217
- const providerId = msg.model?.providerID ?? msg.providerID ?? "unknown";
218
- if (providerId.toLowerCase() !== providerFilter)
219
- return false;
220
- }
221
- return true;
222
- });
223
157
  } catch (err) {
224
158
  console.error(`Error reading recent messages: ${err}`);
225
159
  return [];
226
160
  }
227
161
  }
228
162
  async function loadMessages(storagePath, providerFilter) {
229
- const messagesDir = join(storagePath, "message");
230
163
  try {
231
- const filePaths = await collectFilePaths(messagesDir);
232
- const results = await processInBatches(filePaths, async (filePath) => {
233
- try {
234
- return await readJsonFile(filePath);
235
- } catch {
236
- return null;
237
- }
238
- }, BATCH_SIZE);
239
- return results.filter((msg) => {
240
- if (!msg)
241
- return false;
242
- if (msg.role === "user")
243
- return false;
244
- if (!msg.tokens)
245
- return false;
246
- if (providerFilter) {
247
- const providerId = msg.model?.providerID ?? msg.providerID ?? "unknown";
248
- if (providerId.toLowerCase() !== providerFilter)
249
- return false;
250
- }
251
- return true;
252
- });
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
+ }
253
171
  } catch (err) {
254
- console.error(`Error reading messages directory: ${err}`);
172
+ console.error(`Error reading messages from database: ${err}`);
255
173
  return [];
256
174
  }
257
175
  }
258
176
  function createCursor() {
259
- return {
260
- knownSessions: new Set,
261
- fileCountPerSession: new Map,
262
- lastTimestamp: 0
263
- };
177
+ return { lastTimestamp: 0 };
264
178
  }
265
179
  async function loadMessagesIncremental(storagePath, cursor, providerFilter) {
266
- const messagesDir = join(storagePath, "message");
267
- const newMessages = [];
268
- const newCursor = {
269
- knownSessions: new Set(cursor.knownSessions),
270
- fileCountPerSession: new Map(cursor.fileCountPerSession),
271
- lastTimestamp: cursor.lastTimestamp
272
- };
273
180
  try {
274
- const sessionDirs = await readdir(messagesDir);
275
- for (const sessionDir of sessionDirs) {
276
- const sessionPath = join(messagesDir, sessionDir);
277
- const st = await stat(sessionPath);
278
- if (!st.isDirectory())
279
- continue;
280
- const files = await readdir(sessionPath);
281
- const jsonFiles = files.filter((f) => f.endsWith(".json"));
282
- const previousCount = cursor.fileCountPerSession.get(sessionDir) ?? 0;
283
- if (jsonFiles.length > previousCount) {
284
- const sortedFiles = jsonFiles.sort();
285
- const newFiles = sortedFiles.slice(previousCount);
286
- for (const file of newFiles) {
287
- const filePath = join(sessionPath, file);
288
- try {
289
- const msg = await readJsonFile(filePath);
290
- if (isValidMessage(msg, providerFilter)) {
291
- newMessages.push(msg);
292
- if (msg.time?.created && msg.time.created > newCursor.lastTimestamp) {
293
- newCursor.lastTimestamp = msg.time.created;
294
- }
295
- }
296
- } 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;
297
189
  }
298
190
  }
299
- newCursor.knownSessions.add(sessionDir);
300
- newCursor.fileCountPerSession.set(sessionDir, jsonFiles.length);
191
+ return {
192
+ messages,
193
+ cursor: { lastTimestamp: maxTimestamp }
194
+ };
195
+ } finally {
196
+ db.close();
301
197
  }
302
- return { messages: newMessages, cursor: newCursor };
303
198
  } catch (err) {
304
199
  console.error(`Error in incremental load: ${err}`);
305
- return { messages: [], cursor: newCursor };
306
- }
307
- }
308
- function isValidMessage(msg, providerFilter) {
309
- if (!msg)
310
- return false;
311
- if (msg.role === "user")
312
- return false;
313
- if (!msg.tokens)
314
- return false;
315
- if (providerFilter) {
316
- const providerId = msg.model?.providerID ?? msg.providerID ?? "unknown";
317
- if (providerId.toLowerCase() !== providerFilter)
318
- return false;
200
+ return {
201
+ messages: [],
202
+ cursor: { lastTimestamp: cursor.lastTimestamp }
203
+ };
319
204
  }
320
- return true;
321
205
  }
322
206
 
323
207
  // src/pricing.ts
@@ -11251,8 +11135,8 @@ function convertToDebugSymbols(symbols) {
11251
11135
  const p99Width = Math.max(p99Header.length, ...allStats.map((s) => s.p99.toFixed(2).length));
11252
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)}`);
11253
11137
  lines.push(`${"-".repeat(nameWidth)}-+-${"-".repeat(countWidth)}-+-${"-".repeat(totalWidth)}-+-${"-".repeat(avgWidth)}-+-${"-".repeat(minWidth)}-+-${"-".repeat(maxWidth)}-+-${"-".repeat(medianWidth)}-+-${"-".repeat(p90Width)}-+-${"-".repeat(p99Width)}`);
11254
- allStats.forEach((stat2) => {
11255
- 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)}`);
11256
11140
  });
11257
11141
  }
11258
11142
  lines.push("-------------------------------------------------------------------------------------------------------------------------");
@@ -28182,13 +28066,13 @@ var render = async (node, rendererOrConfig = {}) => {
28182
28066
  };
28183
28067
 
28184
28068
  // src/quota-loader.ts
28185
- import { readFile as readFile2 } from "fs/promises";
28069
+ import { readFile } from "fs/promises";
28186
28070
  import { homedir as homedir2 } from "os";
28187
28071
  import { join as join3 } from "path";
28188
- var isBun2 = typeof globalThis.Bun !== "undefined";
28189
- async function readJsonFile2(filePath) {
28072
+ var isBun = typeof globalThis.Bun !== "undefined";
28073
+ async function readJsonFile(filePath) {
28190
28074
  try {
28191
- 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");
28192
28076
  return JSON.parse(content);
28193
28077
  } catch {
28194
28078
  return null;
@@ -28196,7 +28080,7 @@ async function readJsonFile2(filePath) {
28196
28080
  }
28197
28081
  async function loadMultiAccountQuota() {
28198
28082
  const path2 = join3(homedir2(), ".local/share/opencode/multi-account-state.json");
28199
- const state = await readJsonFile2(path2);
28083
+ const state = await readJsonFile(path2);
28200
28084
  if (!state?.usage) {
28201
28085
  return [
28202
28086
  {
@@ -28247,7 +28131,7 @@ async function loadMultiAccountQuota() {
28247
28131
  }
28248
28132
  async function loadAntigravityQuota() {
28249
28133
  const path2 = join3(homedir2(), ".config/opencode/antigravity-accounts.json");
28250
- const data = await readJsonFile2(path2);
28134
+ const data = await readJsonFile(path2);
28251
28135
  if (!data?.accounts?.length) {
28252
28136
  return [
28253
28137
  {
@@ -28302,15 +28186,38 @@ async function loadAntigravityQuota() {
28302
28186
  }
28303
28187
 
28304
28188
  // src/codex-client.ts
28189
+ import { readFile as readFile2 } from "fs/promises";
28190
+ import { homedir as homedir3 } from "os";
28191
+ import { join as join4 } from "path";
28192
+ var isBun2 = typeof globalThis.Bun !== "undefined";
28305
28193
  var CODEX_API_URL = "https://chatgpt.com/backend-api/wham/usage";
28194
+ var CODEX_AUTH_PATH = join4(homedir3(), ".codex", "auth.json");
28195
+ async function readCodexAuthToken() {
28196
+ try {
28197
+ const content = isBun2 ? await Bun.file(CODEX_AUTH_PATH).text() : await readFile2(CODEX_AUTH_PATH, "utf-8");
28198
+ const auth = JSON.parse(content);
28199
+ if (auth.OPENAI_API_KEY) {
28200
+ return auth.OPENAI_API_KEY;
28201
+ }
28202
+ return auth.tokens?.access_token ?? undefined;
28203
+ } catch {
28204
+ return;
28205
+ }
28206
+ }
28207
+ async function resolveCodexToken(explicitToken) {
28208
+ if (explicitToken)
28209
+ return explicitToken;
28210
+ return readCodexAuthToken();
28211
+ }
28306
28212
  async function loadCodexQuota(token) {
28307
- if (!token) {
28213
+ const resolvedToken = await resolveCodexToken(token);
28214
+ if (!resolvedToken) {
28308
28215
  return [
28309
28216
  {
28310
28217
  source: "codex",
28311
28218
  label: "Codex",
28312
28219
  used: 0,
28313
- error: "No --codex-token provided"
28220
+ error: "Not logged in. Run: codex login"
28314
28221
  }
28315
28222
  ];
28316
28223
  }
@@ -28318,17 +28225,18 @@ async function loadCodexQuota(token) {
28318
28225
  const response = await fetch(CODEX_API_URL, {
28319
28226
  method: "GET",
28320
28227
  headers: {
28321
- Authorization: `Bearer ${token}`,
28228
+ Authorization: `Bearer ${resolvedToken}`,
28322
28229
  "Content-Type": "application/json"
28323
28230
  }
28324
28231
  });
28325
28232
  if (!response.ok) {
28233
+ const hint = response.status === 401 ? " (token expired? Run: codex login)" : "";
28326
28234
  return [
28327
28235
  {
28328
28236
  source: "codex",
28329
28237
  label: "Codex",
28330
28238
  used: 0,
28331
- error: `API error: ${response.status}`
28239
+ error: `API error: ${response.status}${hint}`
28332
28240
  }
28333
28241
  ];
28334
28242
  }
@@ -28463,9 +28371,9 @@ function UsageTable(props) {
28463
28371
  const windowedData = getWindowedData();
28464
28372
  let totalTokens = 0;
28465
28373
  let totalCost = 0;
28466
- windowedData.forEach(([_2, stat2]) => {
28467
- totalTokens += stat2.input + stat2.output;
28468
- totalCost += stat2.cost;
28374
+ windowedData.forEach(([_2, stat]) => {
28375
+ totalTokens += stat.input + stat.output;
28376
+ totalCost += stat.cost;
28469
28377
  });
28470
28378
  return {
28471
28379
  tokens: totalTokens,
@@ -28543,10 +28451,10 @@ function UsageTable(props) {
28543
28451
  get each() {
28544
28452
  return statsArray();
28545
28453
  },
28546
- children: ([dateKey, stat2], index) => {
28454
+ children: ([dateKey, stat], index) => {
28547
28455
  const isLast = index() === statsArray().length - 1;
28548
28456
  const isEven = index() % 2 === 0;
28549
- const providers = Array.from(stat2.providerStats.entries());
28457
+ const providers = Array.from(stat.providerStats.entries());
28550
28458
  return [(() => {
28551
28459
  var _el$26 = createElement("box"), _el$27 = createElement("text"), _el$28 = createElement("span"), _el$29 = createElement("span"), _el$30 = createElement("span");
28552
28460
  insertNode(_el$26, _el$27);
@@ -28557,8 +28465,8 @@ function UsageTable(props) {
28557
28465
  setProp(_el$27, "overflow", "hidden");
28558
28466
  setProp(_el$27, "wrapMode", "none");
28559
28467
  insert(_el$28, () => padRight2(dateKey, 18));
28560
- insert(_el$29, () => padLeft2(formatNum(stat2.input + stat2.output), 13));
28561
- 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));
28562
28470
  effect((_p$) => {
28563
28471
  var _v$1 = providers.length > 0 ? 0.25 : 0.5, _v$10 = isEven ? COLORS.bg.secondary : COLORS.bg.accent, _v$11 = {
28564
28472
  fg: COLORS.text.primary,
@@ -29052,18 +28960,16 @@ function Dashboard(props) {
29052
28960
  error: `Load error: ${err}`
29053
28961
  });
29054
28962
  }
29055
- if (props.codexToken) {
29056
- try {
29057
- const codex = await loadCodexQuota(props.codexToken);
29058
- results.push(...codex);
29059
- } catch (err) {
29060
- results.push({
29061
- source: "codex",
29062
- label: "Codex",
29063
- used: 0,
29064
- error: `Load error: ${err}`
29065
- });
29066
- }
28963
+ try {
28964
+ const codex = await loadCodexQuota();
28965
+ results.push(...codex);
28966
+ } catch (err) {
28967
+ results.push({
28968
+ source: "codex",
28969
+ label: "Codex",
28970
+ used: 0,
28971
+ error: `Load error: ${err}`
28972
+ });
29067
28973
  }
29068
28974
  setQuotas(results);
29069
28975
  };
@@ -29199,9 +29105,6 @@ function Dashboard(props) {
29199
29105
  }
29200
29106
  async function runSolidDashboard(options) {
29201
29107
  await render(() => createComponent2(Dashboard, {
29202
- get codexToken() {
29203
- return options.codexToken;
29204
- },
29205
29108
  get providerFilter() {
29206
29109
  return options.providerFilter;
29207
29110
  },
@@ -29218,57 +29121,32 @@ async function runSolidDashboard(options) {
29218
29121
  });
29219
29122
  }
29220
29123
 
29221
- // src/config.ts
29124
+ // src/config-commands.ts
29222
29125
  import { readFile as readFile3 } from "fs/promises";
29223
- import { homedir as homedir3 } from "os";
29224
- import { join as join4 } from "path";
29225
- var isBun3 = typeof globalThis.Bun !== "undefined";
29126
+ import { homedir as homedir5 } from "os";
29127
+ import { join as join6 } from "path";
29128
+
29129
+ // src/config.ts
29130
+ import { homedir as homedir4 } from "os";
29131
+ import { join as join5 } from "path";
29226
29132
  function getConfigPath() {
29227
- const configDir = process.env.XDG_CONFIG_HOME ?? join4(homedir3(), ".config");
29228
- return join4(configDir, "opencode-usage", "config.json");
29229
- }
29230
- async function loadConfig() {
29231
- try {
29232
- const configPath = getConfigPath();
29233
- const content = isBun3 ? await Bun.file(configPath).text() : await readFile3(configPath, "utf-8");
29234
- return JSON.parse(content);
29235
- } catch {
29236
- return {};
29237
- }
29238
- }
29239
- async function saveConfig(config) {
29240
- const configPath = getConfigPath();
29241
- const configDir = join4(configPath, "..");
29242
- const { mkdir, writeFile } = await import("fs/promises");
29243
- await mkdir(configDir, { recursive: true });
29244
- const content = JSON.stringify(config, null, 2) + `
29245
- `;
29246
- if (isBun3) {
29247
- await Bun.write(configPath, content);
29248
- } else {
29249
- await writeFile(configPath, content, "utf-8");
29250
- }
29133
+ const configDir = process.env.XDG_CONFIG_HOME ?? join5(homedir4(), ".config");
29134
+ return join5(configDir, "opencode-usage", "config.json");
29251
29135
  }
29252
29136
 
29253
29137
  // src/config-commands.ts
29254
- async function setCodexToken(token) {
29255
- const config = await loadConfig();
29256
- config.codexToken = token;
29257
- await saveConfig(config);
29258
- console.log(`\u2713 Codex token saved to ${getConfigPath()}`);
29259
- }
29138
+ var CODEX_AUTH_PATH2 = join6(homedir5(), ".codex", "auth.json");
29260
29139
  async function showConfig() {
29261
- const config = await loadConfig();
29262
29140
  const configPath = getConfigPath();
29263
29141
  console.log(`
29264
- Configuration file: ${configPath}
29265
- `);
29266
- if (config.codexToken) {
29267
- const maskedToken = config.codexToken.substring(0, 8) + "..." + config.codexToken.substring(config.codexToken.length - 4);
29268
- console.log(` codexToken: ${maskedToken}`);
29269
- } else {
29270
- console.log(` codexToken: (not set)`);
29271
- }
29142
+ Configuration file: ${configPath}`);
29143
+ let hasCodexAuth = false;
29144
+ try {
29145
+ const content = await readFile3(CODEX_AUTH_PATH2, "utf-8");
29146
+ const auth = JSON.parse(content);
29147
+ hasCodexAuth = !!auth.tokens?.access_token;
29148
+ } catch {}
29149
+ console.log(` Codex auth: ${hasCodexAuth ? "~/.codex/auth.json (auto)" : "(not found \u2014 run: codex login)"}`);
29272
29150
  console.log();
29273
29151
  }
29274
29152
 
@@ -29323,37 +29201,13 @@ Loading OpenCode usage data from: ${options.storagePath}`);
29323
29201
  }
29324
29202
  }
29325
29203
  async function main2() {
29326
- const {
29327
- provider,
29328
- days,
29329
- since,
29330
- until,
29331
- json,
29332
- monthly,
29333
- watch,
29334
- dashboard,
29335
- codexToken,
29336
- config,
29337
- configToken
29338
- } = parseArgs();
29204
+ const { provider, days, since, until, json, monthly, watch, stats, config } = parseArgs();
29339
29205
  if (config === "show") {
29340
29206
  await showConfig();
29341
29207
  return;
29342
29208
  }
29343
- if (config === "set-codex-token") {
29344
- if (!configToken) {
29345
- console.error("Error: --config set-codex-token requires --token <value>");
29346
- console.error("Usage: opencode-usage --config set-codex-token --token <token>");
29347
- process.exit(1);
29348
- }
29349
- await setCodexToken(configToken);
29350
- return;
29351
- }
29352
- const configData = await loadConfig();
29353
- const effectiveCodexToken = codexToken ?? configData.codexToken;
29354
- if (dashboard) {
29209
+ if (!stats) {
29355
29210
  await runSolidDashboard({
29356
- codexToken: effectiveCodexToken,
29357
29211
  refreshInterval: 300,
29358
29212
  providerFilter: provider,
29359
29213
  initialDays: days
@@ -29391,4 +29245,4 @@ async function main2() {
29391
29245
  }
29392
29246
  main2().catch(console.error);
29393
29247
 
29394
- //# debugId=2D8D06802834C82864756E2164756E21
29248
+ //# debugId=71CC1B06755E934764756E2164756E21