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/README.md +5 -5
- package/dist/cli.d.ts +2 -4
- package/dist/codex-client.d.ts +6 -2
- package/dist/config-commands.d.ts +0 -4
- package/dist/config.d.ts +0 -8
- package/dist/dashboard-solid.d.ts +0 -1
- package/dist/dashboard.d.ts +0 -1
- package/dist/index.d.ts +4 -7
- package/dist/index.js +148 -294
- package/dist/index.js.map +10 -10
- package/dist/loader.d.ts +0 -7
- package/dist/types.d.ts +1 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
98
|
-
-w, --watch Watch mode - refresh every 5 minutes
|
|
99
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 --
|
|
111
|
-
bunx opencode-usage
|
|
112
|
-
bunx opencode-usage --
|
|
113
|
-
bunx opencode-usage --since 7d
|
|
114
|
-
bunx opencode-usage --
|
|
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 {
|
|
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"
|
|
122
|
+
return join(xdgDataHome, "opencode");
|
|
140
123
|
}
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
300
|
-
|
|
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 {
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
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((
|
|
11255
|
-
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)}`);
|
|
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
|
|
28069
|
+
import { readFile } from "fs/promises";
|
|
28186
28070
|
import { homedir as homedir2 } from "os";
|
|
28187
28071
|
import { join as join3 } from "path";
|
|
28188
|
-
var
|
|
28189
|
-
async function
|
|
28072
|
+
var isBun = typeof globalThis.Bun !== "undefined";
|
|
28073
|
+
async function readJsonFile(filePath) {
|
|
28190
28074
|
try {
|
|
28191
|
-
const content =
|
|
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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 ${
|
|
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,
|
|
28467
|
-
totalTokens +=
|
|
28468
|
-
totalCost +=
|
|
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,
|
|
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(
|
|
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(
|
|
28561
|
-
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));
|
|
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
|
-
|
|
29056
|
-
|
|
29057
|
-
|
|
29058
|
-
|
|
29059
|
-
|
|
29060
|
-
|
|
29061
|
-
|
|
29062
|
-
|
|
29063
|
-
|
|
29064
|
-
|
|
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
|
|
29224
|
-
import { join as
|
|
29225
|
-
|
|
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 ??
|
|
29228
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
29267
|
-
const
|
|
29268
|
-
|
|
29269
|
-
|
|
29270
|
-
|
|
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 (
|
|
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=
|
|
29248
|
+
//# debugId=71CC1B06755E934764756E2164756E21
|