codex-usage-dashboard 0.1.0 → 0.1.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 +11 -3
- package/bin/codex-usage.js +246 -37
- 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
|
})
|
|
@@ -489,7 +543,7 @@ async function readSnapshot(client, failWhenLoggedOut) {
|
|
|
489
543
|
if (!accountState.account) {
|
|
490
544
|
if (failWhenLoggedOut) {
|
|
491
545
|
throw new Error(
|
|
492
|
-
'No logged-in Codex account was found. Run `codex login` and try again.',
|
|
546
|
+
'No logged-in Codex account was found. Run `npx @openai/codex@latest login` (or `codex login` if installed globally) and try again.',
|
|
493
547
|
)
|
|
494
548
|
}
|
|
495
549
|
|
|
@@ -598,6 +652,161 @@ function openInBrowser(url) {
|
|
|
598
652
|
})
|
|
599
653
|
}
|
|
600
654
|
|
|
655
|
+
async function ensureCodexAppServerSupport() {
|
|
656
|
+
if (!codexAppServerSupportPromise) {
|
|
657
|
+
codexAppServerSupportPromise = inspectCodexCliForAppServer().catch(
|
|
658
|
+
(error) => {
|
|
659
|
+
codexAppServerSupportPromise = null
|
|
660
|
+
throw error
|
|
661
|
+
},
|
|
662
|
+
)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
await codexAppServerSupportPromise
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function inspectCodexCliForAppServer() {
|
|
669
|
+
const codexExecutable = resolveCodexExecutable()
|
|
670
|
+
const help = await runCommandCapture(
|
|
671
|
+
codexExecutable.command,
|
|
672
|
+
[...codexExecutable.argsPrefix, '--help'],
|
|
673
|
+
{ timeoutMs: CODEX_HELP_TIMEOUT_MS },
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
const helpText = `${help.stdout}\n${help.stderr}`
|
|
677
|
+
if (help.code === 0 && /\bapp-server\b/.test(helpText)) {
|
|
678
|
+
return
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const version = await runCommandCapture(
|
|
682
|
+
codexExecutable.command,
|
|
683
|
+
[...codexExecutable.argsPrefix, '--version'],
|
|
684
|
+
{ timeoutMs: CODEX_HELP_TIMEOUT_MS },
|
|
685
|
+
)
|
|
686
|
+
const versionText = normalizeCodexVersion(version.stdout || version.stderr)
|
|
687
|
+
throw new Error(
|
|
688
|
+
`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.`,
|
|
689
|
+
)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function resolveCodexExecutable() {
|
|
693
|
+
if (resolvedCodexExecutable) {
|
|
694
|
+
return resolvedCodexExecutable
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const bundledBinPath = resolveBundledCodexBin()
|
|
698
|
+
if (bundledBinPath) {
|
|
699
|
+
resolvedCodexExecutable = {
|
|
700
|
+
argsPrefix: [bundledBinPath],
|
|
701
|
+
command: process.execPath,
|
|
702
|
+
label: 'bundled @openai/codex',
|
|
703
|
+
}
|
|
704
|
+
return resolvedCodexExecutable
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
resolvedCodexExecutable = {
|
|
708
|
+
argsPrefix: [],
|
|
709
|
+
command: 'codex',
|
|
710
|
+
label: 'global codex',
|
|
711
|
+
}
|
|
712
|
+
return resolvedCodexExecutable
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function resolveBundledCodexBin() {
|
|
716
|
+
try {
|
|
717
|
+
return require.resolve('@openai/codex/bin/codex.js')
|
|
718
|
+
} catch {
|
|
719
|
+
return null
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function runCommandCapture(command, args, { timeoutMs }) {
|
|
724
|
+
return new Promise((resolve, reject) => {
|
|
725
|
+
let stdout = ''
|
|
726
|
+
let stderr = ''
|
|
727
|
+
let settled = false
|
|
728
|
+
|
|
729
|
+
const child = spawn(command, args, {
|
|
730
|
+
env: process.env,
|
|
731
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
const timeout = setTimeout(() => {
|
|
735
|
+
if (settled) {
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
settled = true
|
|
740
|
+
child.kill('SIGTERM')
|
|
741
|
+
reject(
|
|
742
|
+
new Error(
|
|
743
|
+
`Timed out while probing \`${command} ${args.join(' ')}\`.`,
|
|
744
|
+
),
|
|
745
|
+
)
|
|
746
|
+
}, timeoutMs)
|
|
747
|
+
|
|
748
|
+
child.stdout.on('data', (chunk) => {
|
|
749
|
+
stdout += chunk.toString()
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
child.stderr.on('data', (chunk) => {
|
|
753
|
+
stderr += chunk.toString()
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
child.once('error', (error) => {
|
|
757
|
+
if (settled) {
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
settled = true
|
|
762
|
+
clearTimeout(timeout)
|
|
763
|
+
reject(
|
|
764
|
+
new Error(
|
|
765
|
+
error.code === 'ENOENT'
|
|
766
|
+
? 'The `codex` command is not installed on this machine.'
|
|
767
|
+
: error.message,
|
|
768
|
+
),
|
|
769
|
+
)
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
child.once('close', (code, signal) => {
|
|
773
|
+
if (settled) {
|
|
774
|
+
return
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
settled = true
|
|
778
|
+
clearTimeout(timeout)
|
|
779
|
+
resolve({
|
|
780
|
+
code,
|
|
781
|
+
signal,
|
|
782
|
+
stderr: stderr.trim(),
|
|
783
|
+
stdout: stdout.trim(),
|
|
784
|
+
})
|
|
785
|
+
})
|
|
786
|
+
})
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function normalizeCodexVersion(versionText) {
|
|
790
|
+
const normalized = versionText.trim().replace(/^codex-cli\s+/i, '')
|
|
791
|
+
return normalized || 'unknown'
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function buildCodexAppServerExitError(code, signal, stderrMessage) {
|
|
795
|
+
const exitDetail =
|
|
796
|
+
typeof code === 'number'
|
|
797
|
+
? `exit code ${code}`
|
|
798
|
+
: signal
|
|
799
|
+
? `signal ${signal}`
|
|
800
|
+
: 'no exit code'
|
|
801
|
+
|
|
802
|
+
const message = [`Codex app-server exited unexpectedly (${exitDetail}).`]
|
|
803
|
+
if (stderrMessage) {
|
|
804
|
+
message.push(stderrMessage)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
return new Error(message.join(' '))
|
|
808
|
+
}
|
|
809
|
+
|
|
601
810
|
async function writeConfig(codexHome, config) {
|
|
602
811
|
await mkdir(codexHome, { recursive: true })
|
|
603
812
|
await writeFile(
|
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.1",
|
|
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",
|