codex-usage-dashboard 0.1.0 → 0.1.2
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 +11 -3
- package/bin/codex-usage.js +341 -60
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Multi-user Codex usage tracking with a Vercel-hosted dashboard, Supabase Auth, a
|
|
|
7
7
|
- Vite + TanStack Router + React Query
|
|
8
8
|
- Supabase Auth + Postgres
|
|
9
9
|
- Vercel Functions for pairing and sync ingest
|
|
10
|
-
- Local Codex access through `
|
|
10
|
+
- Local Codex access through a bundled `@openai/codex` `app-server`
|
|
11
11
|
|
|
12
12
|
## What changed
|
|
13
13
|
|
|
@@ -47,12 +47,20 @@ The Vite dev server now serves the local pairing and sync API routes under `/api
|
|
|
47
47
|
|
|
48
48
|
## Direct connect flow
|
|
49
49
|
|
|
50
|
-
Run this on any machine
|
|
50
|
+
Run this on any machine:
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
53
|
npx codex-usage-dashboard@latest connect --site "https://your-site.vercel.app"
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
The package now brings a compatible `@openai/codex` CLI dependency with it, so `connect`, `pair`, and `sync` do not require a separate global Codex install.
|
|
57
|
+
|
|
58
|
+
If the machine has never logged into Codex before, authenticate once with:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx @openai/codex@latest login
|
|
62
|
+
```
|
|
63
|
+
|
|
56
64
|
That command:
|
|
57
65
|
|
|
58
66
|
- starts a local `codex app-server`
|
|
@@ -68,7 +76,7 @@ If the same machine is already connected, rerun the same command to reopen the d
|
|
|
68
76
|
|
|
69
77
|
1. Sign in on the website.
|
|
70
78
|
2. Click `Create pairing command`.
|
|
71
|
-
3. Run the generated command on the machine
|
|
79
|
+
3. Run the generated command on the machine you want to pair.
|
|
72
80
|
|
|
73
81
|
The generated command looks like:
|
|
74
82
|
|
package/bin/codex-usage.js
CHANGED
|
@@ -3,19 +3,28 @@
|
|
|
3
3
|
import { spawn } from 'node:child_process'
|
|
4
4
|
import { existsSync } from 'node:fs'
|
|
5
5
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
6
7
|
import os from 'node:os'
|
|
7
8
|
import path from 'node:path'
|
|
8
9
|
import process from 'node:process'
|
|
9
10
|
|
|
10
11
|
const DEFAULT_POLL_MS = 60_000
|
|
12
|
+
const CODEX_HELP_TIMEOUT_MS = 15_000
|
|
11
13
|
const CONFIG_FILE_NAME = 'codex-usage-sync.json'
|
|
12
14
|
const NPX_COMMAND = 'npx codex-usage-dashboard@latest'
|
|
13
15
|
|
|
16
|
+
const require = createRequire(import.meta.url)
|
|
17
|
+
|
|
18
|
+
let codexAppServerSupportPromise = null
|
|
19
|
+
let resolvedCodexExecutable = null
|
|
20
|
+
|
|
14
21
|
class StdioCodexClient {
|
|
15
22
|
constructor({ codexHome }) {
|
|
16
23
|
this.codexHome = codexHome
|
|
17
24
|
this.child = null
|
|
18
25
|
this.buffer = ''
|
|
26
|
+
this.isClosing = false
|
|
27
|
+
this.lastStderrMessage = ''
|
|
19
28
|
this.pending = new Map()
|
|
20
29
|
this.requestId = 0
|
|
21
30
|
this.notificationHandler = null
|
|
@@ -26,17 +35,9 @@ class StdioCodexClient {
|
|
|
26
35
|
return
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
this.buffer = ''
|
|
39
|
+
this.lastStderrMessage = ''
|
|
29
40
|
this.child = await this.spawnAppServer()
|
|
30
|
-
this.child.stdout.setEncoding('utf8')
|
|
31
|
-
this.child.stdout.on('data', (chunk) => {
|
|
32
|
-
this.consumeStdout(chunk)
|
|
33
|
-
})
|
|
34
|
-
this.child.stderr.on('data', (chunk) => {
|
|
35
|
-
const message = chunk.toString().trim()
|
|
36
|
-
if (message) {
|
|
37
|
-
console.error(`[codex] ${message}`)
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
41
|
|
|
41
42
|
await this.request('initialize', {
|
|
42
43
|
capabilities: {},
|
|
@@ -54,13 +55,12 @@ class StdioCodexClient {
|
|
|
54
55
|
return
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
this.child
|
|
58
|
+
const child = this.child
|
|
58
59
|
this.child = null
|
|
60
|
+
this.isClosing = true
|
|
61
|
+
child.kill('SIGTERM')
|
|
59
62
|
|
|
60
|
-
|
|
61
|
-
pending.reject(new Error('Codex app-server closed.'))
|
|
62
|
-
}
|
|
63
|
-
this.pending.clear()
|
|
63
|
+
this.rejectPending(new Error('Codex app-server closed.'))
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
onNotification(handler) {
|
|
@@ -73,17 +73,25 @@ class StdioCodexClient {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const id = ++this.requestId
|
|
76
|
-
this.child.stdin.write(
|
|
77
|
-
`${JSON.stringify({
|
|
78
|
-
id,
|
|
79
|
-
jsonrpc: '2.0',
|
|
80
|
-
method,
|
|
81
|
-
...(params ? { params } : {}),
|
|
82
|
-
})}\n`,
|
|
83
|
-
)
|
|
84
|
-
|
|
85
76
|
return new Promise((resolve, reject) => {
|
|
86
77
|
this.pending.set(id, { reject, resolve })
|
|
78
|
+
|
|
79
|
+
this.child.stdin.write(
|
|
80
|
+
`${JSON.stringify({
|
|
81
|
+
id,
|
|
82
|
+
jsonrpc: '2.0',
|
|
83
|
+
method,
|
|
84
|
+
...(params ? { params } : {}),
|
|
85
|
+
})}\n`,
|
|
86
|
+
(error) => {
|
|
87
|
+
if (!error) {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.pending.delete(id)
|
|
92
|
+
reject(error)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
87
95
|
})
|
|
88
96
|
}
|
|
89
97
|
|
|
@@ -151,32 +159,78 @@ class StdioCodexClient {
|
|
|
151
159
|
}
|
|
152
160
|
}
|
|
153
161
|
|
|
154
|
-
|
|
162
|
+
rejectPending(error) {
|
|
163
|
+
for (const pending of this.pending.values()) {
|
|
164
|
+
pending.reject(error)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.pending.clear()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async spawnAppServer() {
|
|
171
|
+
await ensureCodexAppServerSupport()
|
|
172
|
+
const codexExecutable = resolveCodexExecutable()
|
|
173
|
+
|
|
155
174
|
return new Promise((resolve, reject) => {
|
|
156
|
-
const child = spawn(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
175
|
+
const child = spawn(
|
|
176
|
+
codexExecutable.command,
|
|
177
|
+
[...codexExecutable.argsPrefix, 'app-server', '--listen', 'stdio://'],
|
|
178
|
+
{
|
|
179
|
+
env: {
|
|
180
|
+
...process.env,
|
|
181
|
+
...(this.codexHome ? { CODEX_HOME: this.codexHome } : {}),
|
|
182
|
+
},
|
|
183
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
160
184
|
},
|
|
161
|
-
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
child.stdout.setEncoding('utf8')
|
|
188
|
+
child.stdout.on('data', (chunk) => {
|
|
189
|
+
this.consumeStdout(chunk)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
child.stderr.on('data', (chunk) => {
|
|
193
|
+
const message = chunk.toString().trim()
|
|
194
|
+
if (!message) {
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const lines = message.split(/\r?\n/)
|
|
199
|
+
this.lastStderrMessage = lines[lines.length - 1] ?? message
|
|
200
|
+
console.error(`[codex] ${message}`)
|
|
162
201
|
})
|
|
163
202
|
|
|
164
203
|
child.once('error', (error) => {
|
|
165
204
|
reject(
|
|
166
205
|
new Error(
|
|
167
206
|
error.code === 'ENOENT'
|
|
168
|
-
? '
|
|
207
|
+
? 'Codex CLI is not available. Reinstall `codex-usage-dashboard` or install it globally with `npm install -g @openai/codex`.'
|
|
169
208
|
: error.message,
|
|
170
209
|
),
|
|
171
210
|
)
|
|
172
211
|
})
|
|
173
212
|
|
|
213
|
+
child.once('exit', (code, signal) => {
|
|
214
|
+
if (this.child === child) {
|
|
215
|
+
this.child = null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const expectedShutdown = this.isClosing
|
|
219
|
+
this.isClosing = false
|
|
220
|
+
|
|
221
|
+
if (expectedShutdown) {
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const error = buildCodexAppServerExitError(
|
|
226
|
+
code,
|
|
227
|
+
signal,
|
|
228
|
+
this.lastStderrMessage,
|
|
229
|
+
)
|
|
230
|
+
this.rejectPending(error)
|
|
231
|
+
})
|
|
232
|
+
|
|
174
233
|
child.once('spawn', () => {
|
|
175
|
-
child.on('exit', (code) => {
|
|
176
|
-
if (code !== 0 && code !== null) {
|
|
177
|
-
console.error(`[codex] app-server exited with code ${code}`)
|
|
178
|
-
}
|
|
179
|
-
})
|
|
180
234
|
resolve(child)
|
|
181
235
|
})
|
|
182
236
|
})
|
|
@@ -236,20 +290,20 @@ async function runPairCommand(args) {
|
|
|
236
290
|
}),
|
|
237
291
|
})
|
|
238
292
|
|
|
239
|
-
const payload = await
|
|
293
|
+
const payload = await parseResponseBody(response)
|
|
240
294
|
if (!response.ok) {
|
|
241
|
-
throw new Error(payload
|
|
295
|
+
throw new Error(buildHttpErrorMessage(response, payload, 'Pairing failed.'))
|
|
242
296
|
}
|
|
243
297
|
|
|
244
298
|
const config = {
|
|
245
299
|
authMode: 'website-paired',
|
|
246
300
|
codexHome,
|
|
247
|
-
deviceId: payload.deviceId,
|
|
248
|
-
deviceToken: payload.deviceToken,
|
|
301
|
+
deviceId: payload.data.deviceId,
|
|
302
|
+
deviceToken: payload.data.deviceToken,
|
|
249
303
|
dashboardOrigin: new URL(pairUrl).origin,
|
|
250
304
|
label: device.label,
|
|
251
|
-
pollMs: payload.pollMs ?? DEFAULT_POLL_MS,
|
|
252
|
-
syncUrl: payload.syncUrl,
|
|
305
|
+
pollMs: payload.data.pollMs ?? DEFAULT_POLL_MS,
|
|
306
|
+
syncUrl: payload.data.syncUrl,
|
|
253
307
|
}
|
|
254
308
|
|
|
255
309
|
await writeConfig(codexHome, config)
|
|
@@ -313,24 +367,26 @@ async function runConnectCommand(args) {
|
|
|
313
367
|
}),
|
|
314
368
|
})
|
|
315
369
|
|
|
316
|
-
const payload = await
|
|
370
|
+
const payload = await parseResponseBody(response)
|
|
317
371
|
if (!response.ok) {
|
|
318
|
-
throw new Error(
|
|
372
|
+
throw new Error(
|
|
373
|
+
buildHttpErrorMessage(response, payload, 'Unable to connect this machine.'),
|
|
374
|
+
)
|
|
319
375
|
}
|
|
320
376
|
|
|
321
377
|
const config = {
|
|
322
378
|
authMode: 'guest-link',
|
|
323
379
|
codexHome,
|
|
324
380
|
dashboardOrigin: siteOrigin,
|
|
325
|
-
deviceId: payload.deviceId,
|
|
326
|
-
deviceToken: payload.deviceToken,
|
|
381
|
+
deviceId: payload.data.deviceId,
|
|
382
|
+
deviceToken: payload.data.deviceToken,
|
|
327
383
|
label: device.label,
|
|
328
|
-
pollMs: payload.pollMs ?? DEFAULT_POLL_MS,
|
|
329
|
-
syncUrl: payload.syncUrl,
|
|
384
|
+
pollMs: payload.data.pollMs ?? DEFAULT_POLL_MS,
|
|
385
|
+
syncUrl: payload.data.syncUrl,
|
|
330
386
|
}
|
|
331
387
|
|
|
332
388
|
await writeConfig(codexHome, config)
|
|
333
|
-
await openDashboard(payload.dashboardUrl)
|
|
389
|
+
await openDashboard(payload.data.dashboardUrl)
|
|
334
390
|
console.log('Dashboard opened.')
|
|
335
391
|
console.log(`Config saved to ${resolveConfigPath(codexHome)}`)
|
|
336
392
|
console.log(
|
|
@@ -447,9 +503,9 @@ async function syncOnce(client, config, args) {
|
|
|
447
503
|
}),
|
|
448
504
|
})
|
|
449
505
|
|
|
450
|
-
const payload = await
|
|
506
|
+
const payload = await parseResponseBody(response)
|
|
451
507
|
if (!response.ok) {
|
|
452
|
-
throw new Error(payload
|
|
508
|
+
throw new Error(buildHttpErrorMessage(response, payload, 'Sync failed.'))
|
|
453
509
|
}
|
|
454
510
|
}
|
|
455
511
|
|
|
@@ -465,12 +521,14 @@ async function resolveExistingDashboardUrl(config, args) {
|
|
|
465
521
|
}),
|
|
466
522
|
})
|
|
467
523
|
|
|
468
|
-
const payload = await
|
|
524
|
+
const payload = await parseResponseBody(response)
|
|
469
525
|
if (!response.ok) {
|
|
470
|
-
throw new Error(
|
|
526
|
+
throw new Error(
|
|
527
|
+
buildHttpErrorMessage(response, payload, 'Unable to open the dashboard.'),
|
|
528
|
+
)
|
|
471
529
|
}
|
|
472
530
|
|
|
473
|
-
return payload.dashboardUrl ?? null
|
|
531
|
+
return payload.data.dashboardUrl ?? null
|
|
474
532
|
}
|
|
475
533
|
|
|
476
534
|
const siteOrigin =
|
|
@@ -489,7 +547,7 @@ async function readSnapshot(client, failWhenLoggedOut) {
|
|
|
489
547
|
if (!accountState.account) {
|
|
490
548
|
if (failWhenLoggedOut) {
|
|
491
549
|
throw new Error(
|
|
492
|
-
'No logged-in Codex account was found. Run `codex login` and try again.',
|
|
550
|
+
'No logged-in Codex account was found. Run `npx @openai/codex@latest login` (or `codex login` if installed globally) and try again.',
|
|
493
551
|
)
|
|
494
552
|
}
|
|
495
553
|
|
|
@@ -598,6 +656,161 @@ function openInBrowser(url) {
|
|
|
598
656
|
})
|
|
599
657
|
}
|
|
600
658
|
|
|
659
|
+
async function ensureCodexAppServerSupport() {
|
|
660
|
+
if (!codexAppServerSupportPromise) {
|
|
661
|
+
codexAppServerSupportPromise = inspectCodexCliForAppServer().catch(
|
|
662
|
+
(error) => {
|
|
663
|
+
codexAppServerSupportPromise = null
|
|
664
|
+
throw error
|
|
665
|
+
},
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
await codexAppServerSupportPromise
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async function inspectCodexCliForAppServer() {
|
|
673
|
+
const codexExecutable = resolveCodexExecutable()
|
|
674
|
+
const help = await runCommandCapture(
|
|
675
|
+
codexExecutable.command,
|
|
676
|
+
[...codexExecutable.argsPrefix, '--help'],
|
|
677
|
+
{ timeoutMs: CODEX_HELP_TIMEOUT_MS },
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
const helpText = `${help.stdout}\n${help.stderr}`
|
|
681
|
+
if (help.code === 0 && /\bapp-server\b/.test(helpText)) {
|
|
682
|
+
return
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const version = await runCommandCapture(
|
|
686
|
+
codexExecutable.command,
|
|
687
|
+
[...codexExecutable.argsPrefix, '--version'],
|
|
688
|
+
{ timeoutMs: CODEX_HELP_TIMEOUT_MS },
|
|
689
|
+
)
|
|
690
|
+
const versionText = normalizeCodexVersion(version.stdout || version.stderr)
|
|
691
|
+
throw new Error(
|
|
692
|
+
`Resolved Codex CLI ${versionText} (${codexExecutable.label}) does not support \`codex app-server\`. Reinstall \`codex-usage-dashboard\` or update Codex with \`npm install -g @openai/codex\`, then rerun this command.`,
|
|
693
|
+
)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function resolveCodexExecutable() {
|
|
697
|
+
if (resolvedCodexExecutable) {
|
|
698
|
+
return resolvedCodexExecutable
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const bundledBinPath = resolveBundledCodexBin()
|
|
702
|
+
if (bundledBinPath) {
|
|
703
|
+
resolvedCodexExecutable = {
|
|
704
|
+
argsPrefix: [bundledBinPath],
|
|
705
|
+
command: process.execPath,
|
|
706
|
+
label: 'bundled @openai/codex',
|
|
707
|
+
}
|
|
708
|
+
return resolvedCodexExecutable
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
resolvedCodexExecutable = {
|
|
712
|
+
argsPrefix: [],
|
|
713
|
+
command: 'codex',
|
|
714
|
+
label: 'global codex',
|
|
715
|
+
}
|
|
716
|
+
return resolvedCodexExecutable
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function resolveBundledCodexBin() {
|
|
720
|
+
try {
|
|
721
|
+
return require.resolve('@openai/codex/bin/codex.js')
|
|
722
|
+
} catch {
|
|
723
|
+
return null
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function runCommandCapture(command, args, { timeoutMs }) {
|
|
728
|
+
return new Promise((resolve, reject) => {
|
|
729
|
+
let stdout = ''
|
|
730
|
+
let stderr = ''
|
|
731
|
+
let settled = false
|
|
732
|
+
|
|
733
|
+
const child = spawn(command, args, {
|
|
734
|
+
env: process.env,
|
|
735
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
const timeout = setTimeout(() => {
|
|
739
|
+
if (settled) {
|
|
740
|
+
return
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
settled = true
|
|
744
|
+
child.kill('SIGTERM')
|
|
745
|
+
reject(
|
|
746
|
+
new Error(
|
|
747
|
+
`Timed out while probing \`${command} ${args.join(' ')}\`.`,
|
|
748
|
+
),
|
|
749
|
+
)
|
|
750
|
+
}, timeoutMs)
|
|
751
|
+
|
|
752
|
+
child.stdout.on('data', (chunk) => {
|
|
753
|
+
stdout += chunk.toString()
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
child.stderr.on('data', (chunk) => {
|
|
757
|
+
stderr += chunk.toString()
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
child.once('error', (error) => {
|
|
761
|
+
if (settled) {
|
|
762
|
+
return
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
settled = true
|
|
766
|
+
clearTimeout(timeout)
|
|
767
|
+
reject(
|
|
768
|
+
new Error(
|
|
769
|
+
error.code === 'ENOENT'
|
|
770
|
+
? 'The `codex` command is not installed on this machine.'
|
|
771
|
+
: error.message,
|
|
772
|
+
),
|
|
773
|
+
)
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
child.once('close', (code, signal) => {
|
|
777
|
+
if (settled) {
|
|
778
|
+
return
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
settled = true
|
|
782
|
+
clearTimeout(timeout)
|
|
783
|
+
resolve({
|
|
784
|
+
code,
|
|
785
|
+
signal,
|
|
786
|
+
stderr: stderr.trim(),
|
|
787
|
+
stdout: stdout.trim(),
|
|
788
|
+
})
|
|
789
|
+
})
|
|
790
|
+
})
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function normalizeCodexVersion(versionText) {
|
|
794
|
+
const normalized = versionText.trim().replace(/^codex-cli\s+/i, '')
|
|
795
|
+
return normalized || 'unknown'
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function buildCodexAppServerExitError(code, signal, stderrMessage) {
|
|
799
|
+
const exitDetail =
|
|
800
|
+
typeof code === 'number'
|
|
801
|
+
? `exit code ${code}`
|
|
802
|
+
: signal
|
|
803
|
+
? `signal ${signal}`
|
|
804
|
+
: 'no exit code'
|
|
805
|
+
|
|
806
|
+
const message = [`Codex app-server exited unexpectedly (${exitDetail}).`]
|
|
807
|
+
if (stderrMessage) {
|
|
808
|
+
message.push(stderrMessage)
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return new Error(message.join(' '))
|
|
812
|
+
}
|
|
813
|
+
|
|
601
814
|
async function writeConfig(codexHome, config) {
|
|
602
815
|
await mkdir(codexHome, { recursive: true })
|
|
603
816
|
await writeFile(
|
|
@@ -616,13 +829,81 @@ async function readConfig(codexHome) {
|
|
|
616
829
|
return JSON.parse(await readFile(configPath, 'utf8'))
|
|
617
830
|
}
|
|
618
831
|
|
|
619
|
-
async function
|
|
620
|
-
const
|
|
621
|
-
if (!
|
|
622
|
-
return {}
|
|
832
|
+
async function parseResponseBody(response) {
|
|
833
|
+
const text = (await response.text().catch(() => '')).trim()
|
|
834
|
+
if (!text) {
|
|
835
|
+
return { data: {}, text: '' }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
const parsed = JSON.parse(text)
|
|
840
|
+
if (parsed && typeof parsed === 'object') {
|
|
841
|
+
return { data: parsed, text }
|
|
842
|
+
}
|
|
843
|
+
} catch {
|
|
844
|
+
// Fall back to the raw text when the response body is not JSON.
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
return { data: {}, text }
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function buildHttpErrorMessage(response, payload, fallbackMessage) {
|
|
851
|
+
const bodyError =
|
|
852
|
+
typeof payload.data?.error === 'string' && payload.data.error.trim()
|
|
853
|
+
? payload.data.error.trim()
|
|
854
|
+
: null
|
|
855
|
+
const plainText =
|
|
856
|
+
payload.text && !looksLikeHtml(payload.text)
|
|
857
|
+
? payload.text.replace(/\s+/g, ' ').trim()
|
|
858
|
+
: ''
|
|
859
|
+
const statusLabel = `${response.status}${response.statusText ? ` ${response.statusText}` : ''}`
|
|
860
|
+
const vercelError = response.headers.get('x-vercel-error')
|
|
861
|
+
const vercelId = response.headers.get('x-vercel-id')
|
|
862
|
+
|
|
863
|
+
const detail = bodyError ?? truncateText(plainText, 240)
|
|
864
|
+
let message = detail
|
|
865
|
+
? `${fallbackMessage} ${detail}`
|
|
866
|
+
: `${fallbackMessage} HTTP ${statusLabel}.`
|
|
867
|
+
|
|
868
|
+
if (!detail) {
|
|
869
|
+
return appendHttpContext(message, vercelError, vercelId)
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (!bodyError) {
|
|
873
|
+
message = `${message} (HTTP ${statusLabel})`
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return appendHttpContext(message, vercelError, vercelId)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function appendHttpContext(message, vercelError, vercelId) {
|
|
880
|
+
const context = []
|
|
881
|
+
|
|
882
|
+
if (vercelError) {
|
|
883
|
+
context.push(`Vercel error: ${vercelError}`)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (vercelId) {
|
|
887
|
+
context.push(`request id: ${vercelId}`)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (!context.length) {
|
|
891
|
+
return message
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return `${message} [${context.join('; ')}]`
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function looksLikeHtml(text) {
|
|
898
|
+
return /^<!doctype html>|^<html[\s>]/i.test(text)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function truncateText(text, maxLength) {
|
|
902
|
+
if (text.length <= maxLength) {
|
|
903
|
+
return text
|
|
623
904
|
}
|
|
624
905
|
|
|
625
|
-
return
|
|
906
|
+
return `${text.slice(0, maxLength - 1)}…`
|
|
626
907
|
}
|
|
627
908
|
|
|
628
909
|
function waitForTermination(cleanup) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-usage-dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.2",
|
|
5
5
|
"files": [
|
|
6
6
|
"README.md",
|
|
7
7
|
"bin"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@fontsource-variable/geist": "^5.2.8",
|
|
30
|
+
"@openai/codex": "^0.118.0",
|
|
30
31
|
"@radix-ui/react-slot": "^1.2.4",
|
|
31
32
|
"@supabase/supabase-js": "^2.101.1",
|
|
32
33
|
"@tanstack/react-query": "^5.96.2",
|