better-codex 0.1.0 → 0.1.3
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 +64 -11
- package/bin/better-codex.cjs +200 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,26 +1,79 @@
|
|
|
1
1
|
# better-codex
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
better-codex is a Codex session hub: a web UI plus a local backend that supervises `codex app-server` to manage multiple accounts, sessions, reviews, and analytics in one place.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Features
|
|
6
|
+
- Multi-account hub with session list, threads, and model switching.
|
|
7
|
+
- Reviews view backed by Codex review mode events.
|
|
8
|
+
- Analytics + search (local SQLite indexes).
|
|
9
|
+
- Zero-config `better-codex` launcher that boots backend + UI.
|
|
6
10
|
|
|
11
|
+
## Project Layout
|
|
12
|
+
- `apps/backend` Bun + Elysia hub server, Codex app-server supervisor.
|
|
13
|
+
- `apps/web` React UI (Vite).
|
|
14
|
+
- `scripts/` launcher scripts for local dev.
|
|
15
|
+
- `bin/` CLI wrappers (`better-codex`).
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
- Bun: https://bun.sh
|
|
19
|
+
- Codex CLI (`codex`) available in PATH.
|
|
20
|
+
- Node 18+ for the npm package wrapper.
|
|
21
|
+
|
|
22
|
+
## Install (npm)
|
|
7
23
|
```bash
|
|
8
24
|
npm i -g better-codex
|
|
9
25
|
```
|
|
10
26
|
|
|
11
|
-
|
|
27
|
+
## Run (zero-config)
|
|
28
|
+
```bash
|
|
29
|
+
better-codex
|
|
30
|
+
```
|
|
12
31
|
|
|
13
|
-
|
|
32
|
+
Optional:
|
|
33
|
+
```bash
|
|
34
|
+
better-codex web --open
|
|
35
|
+
```
|
|
14
36
|
|
|
37
|
+
## Run from Source
|
|
15
38
|
```bash
|
|
39
|
+
export PATH="$(pwd)/bin:$PATH"
|
|
16
40
|
better-codex
|
|
17
41
|
```
|
|
18
42
|
|
|
19
|
-
|
|
43
|
+
## Manual Dev (backend + UI)
|
|
44
|
+
```bash
|
|
45
|
+
cd apps/backend
|
|
46
|
+
bun install
|
|
47
|
+
bun run dev
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cd apps/web
|
|
52
|
+
VITE_CODEX_HUB_URL=http://127.0.0.1:7711 \
|
|
53
|
+
VITE_CODEX_HUB_TOKEN=<token from backend log> \
|
|
54
|
+
bun install
|
|
55
|
+
bun run dev
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
### Backend env vars
|
|
60
|
+
- `CODEX_HUB_HOST` (default `127.0.0.1`)
|
|
61
|
+
- `CODEX_HUB_PORT` (default `7711`)
|
|
62
|
+
- `CODEX_HUB_TOKEN` (auto-generated by launcher)
|
|
63
|
+
- `CODEX_BIN` (default `codex`)
|
|
64
|
+
- `CODEX_FLAGS`, `CODEX_FLAGS_JSON`
|
|
65
|
+
- `CODEX_APP_SERVER_FLAGS`, `CODEX_APP_SERVER_FLAGS_JSON`
|
|
66
|
+
- `CODEX_HUB_DEFAULT_CWD` (default repo root)
|
|
67
|
+
- `CODEX_HUB_DATA_DIR` (default `~/.codex-hub`)
|
|
68
|
+
- `CODEX_HUB_PROFILES_DIR` (default `~/.codex/profiles`)
|
|
69
|
+
- `CODEX_HUB_DEFAULT_CODEX_HOME` (default `~/.codex`)
|
|
70
|
+
|
|
71
|
+
### Web env vars
|
|
72
|
+
- `VITE_CODEX_HUB_URL` (default `http://127.0.0.1:7711`)
|
|
73
|
+
- `VITE_CODEX_HUB_TOKEN`
|
|
20
74
|
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
- `--backend-port
|
|
25
|
-
-
|
|
26
|
-
- `--open` open the UI in your browser after startup.
|
|
75
|
+
## Troubleshooting
|
|
76
|
+
- `bun: command not found`: install Bun and reopen your terminal.
|
|
77
|
+
- `codex: command not found`: install Codex CLI and ensure it is in PATH.
|
|
78
|
+
- Port already in use: pass `--backend-port` or `--web-port`.
|
|
79
|
+
- UI can’t connect: token mismatch or backend not running; restart and copy the token.
|
package/bin/better-codex.cjs
CHANGED
|
@@ -5,6 +5,73 @@ const { existsSync } = require('node:fs');
|
|
|
5
5
|
const { dirname, join, resolve } = require('node:path');
|
|
6
6
|
const { randomUUID } = require('node:crypto');
|
|
7
7
|
|
|
8
|
+
const c = {
|
|
9
|
+
reset: '\x1b[0m',
|
|
10
|
+
bold: '\x1b[1m',
|
|
11
|
+
dim: '\x1b[2m',
|
|
12
|
+
italic: '\x1b[3m',
|
|
13
|
+
underline: '\x1b[4m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
green: '\x1b[32m',
|
|
16
|
+
yellow: '\x1b[33m',
|
|
17
|
+
red: '\x1b[31m',
|
|
18
|
+
magenta: '\x1b[35m',
|
|
19
|
+
blue: '\x1b[34m',
|
|
20
|
+
white: '\x1b[37m',
|
|
21
|
+
gray: '\x1b[90m',
|
|
22
|
+
brightCyan: '\x1b[96m',
|
|
23
|
+
brightGreen: '\x1b[92m',
|
|
24
|
+
brightYellow: '\x1b[93m',
|
|
25
|
+
brightMagenta: '\x1b[95m',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const symbols = {
|
|
29
|
+
tick: '✔',
|
|
30
|
+
cross: '✖',
|
|
31
|
+
pointer: '▶',
|
|
32
|
+
dot: '●',
|
|
33
|
+
line: '─',
|
|
34
|
+
arrow: '→',
|
|
35
|
+
rocket: '◆',
|
|
36
|
+
sparkles: '✦',
|
|
37
|
+
globe: '◎',
|
|
38
|
+
server: '⚡',
|
|
39
|
+
info: 'ℹ',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const BANNER = `
|
|
43
|
+
${c.brightCyan}${c.bold} ____ __ __ ______ __
|
|
44
|
+
/ __ )___ / /_/ /____ _____ / ____/___ ____/ /__ _ __
|
|
45
|
+
/ __ / _ \\/ __/ __/ _ \\/ ___/ / / / __ \\/ __ / _ \\| |/_/
|
|
46
|
+
/ /_/ / __/ /_/ /_/ __/ / / /___/ /_/ / /_/ / __/> <
|
|
47
|
+
/_____/\\___/\\__/\\__/\\___/_/ \\____/\\____/\\__,_/\\___/_/|_|
|
|
48
|
+
${c.reset}
|
|
49
|
+
${c.dim}${c.italic} A modern web interface for Codex${c.reset}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
const log = {
|
|
53
|
+
info: (msg) => console.log(`${c.cyan}${symbols.info}${c.reset} ${msg}`),
|
|
54
|
+
success: (msg) => console.log(`${c.green}${symbols.tick}${c.reset} ${msg}`),
|
|
55
|
+
warn: (msg) => console.log(`${c.yellow}⚠${c.reset} ${msg}`),
|
|
56
|
+
error: (msg) => console.log(`${c.red}${symbols.cross}${c.reset} ${msg}`),
|
|
57
|
+
step: (msg) => console.log(`${c.magenta}${symbols.pointer}${c.reset} ${msg}`),
|
|
58
|
+
dim: (msg) => console.log(`${c.dim} ${msg}${c.reset}`),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const box = (lines, color = c.cyan) => {
|
|
62
|
+
// Calculate display width (ANSI codes = 0 width)
|
|
63
|
+
const displayWidth = (str) => str.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
64
|
+
|
|
65
|
+
const maxLen = Math.max(...lines.map(displayWidth));
|
|
66
|
+
const top = `${color}╭${'─'.repeat(maxLen + 2)}╮${c.reset}`;
|
|
67
|
+
const bottom = `${color}╰${'─'.repeat(maxLen + 2)}╯${c.reset}`;
|
|
68
|
+
const padded = lines.map((l) => {
|
|
69
|
+
const width = displayWidth(l);
|
|
70
|
+
return `${color}│${c.reset} ${l}${' '.repeat(maxLen - width)} ${color}│${c.reset}`;
|
|
71
|
+
});
|
|
72
|
+
return [top, ...padded, bottom].join('\n');
|
|
73
|
+
};
|
|
74
|
+
|
|
8
75
|
const DEFAULTS = {
|
|
9
76
|
host: '127.0.0.1',
|
|
10
77
|
backendPort: 7711,
|
|
@@ -13,18 +80,39 @@ const DEFAULTS = {
|
|
|
13
80
|
};
|
|
14
81
|
|
|
15
82
|
const printHelp = () => {
|
|
16
|
-
console.log(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
`);
|
|
83
|
+
console.log(BANNER);
|
|
84
|
+
console.log(`${c.bold}USAGE${c.reset}`);
|
|
85
|
+
console.log(` ${c.cyan}better-codex${c.reset} ${c.dim}[command]${c.reset} ${c.dim}[options]${c.reset}`);
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(`${c.bold}COMMANDS${c.reset}`);
|
|
88
|
+
console.log(` ${c.green}web${c.reset} Start the web interface ${c.dim}(default)${c.reset}`);
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(`${c.bold}OPTIONS${c.reset}`);
|
|
91
|
+
console.log(` ${c.yellow}--root${c.reset} ${c.dim}<path>${c.reset} Path to project root`);
|
|
92
|
+
console.log(` ${c.yellow}--host${c.reset} ${c.dim}<host>${c.reset} Host to bind ${c.dim}(default: 127.0.0.1)${c.reset}`);
|
|
93
|
+
console.log(` ${c.yellow}--backend-port${c.reset} ${c.dim}<n>${c.reset} Backend port ${c.dim}(default: 7711)${c.reset}`);
|
|
94
|
+
console.log(` ${c.yellow}--web-port${c.reset} ${c.dim}<n>${c.reset} Web UI port ${c.dim}(default: 5173)${c.reset}`);
|
|
95
|
+
console.log(` ${c.yellow}--open${c.reset} Open browser automatically`);
|
|
96
|
+
console.log(` ${c.yellow}--help, -h${c.reset} Show this help message`);
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log(`${c.bold}EXAMPLES${c.reset}`);
|
|
99
|
+
console.log(` ${c.dim}$${c.reset} better-codex`);
|
|
100
|
+
console.log(` ${c.dim}$${c.reset} better-codex web --open`);
|
|
101
|
+
console.log(` ${c.dim}$${c.reset} better-codex --host 0.0.0.0 --web-port 3000`);
|
|
102
|
+
console.log('');
|
|
21
103
|
};
|
|
22
104
|
|
|
23
105
|
const parseArgs = () => {
|
|
24
106
|
const args = process.argv.slice(2);
|
|
107
|
+
|
|
108
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
109
|
+
printHelp();
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
25
113
|
const options = { ...DEFAULTS, root: undefined };
|
|
26
114
|
const command = args[0] && !args[0].startsWith('-') ? args[0] : 'web';
|
|
27
|
-
const flagArgs = command ===
|
|
115
|
+
const flagArgs = command === args[0] ? args.slice(1) : args;
|
|
28
116
|
|
|
29
117
|
for (let i = 0; i < flagArgs.length; i += 1) {
|
|
30
118
|
const arg = flagArgs[i];
|
|
@@ -52,55 +140,85 @@ const parseArgs = () => {
|
|
|
52
140
|
options.open = true;
|
|
53
141
|
continue;
|
|
54
142
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
59
|
-
console.error(`Unknown option: ${arg}`);
|
|
60
|
-
printHelp();
|
|
143
|
+
log.error(`Unknown option: ${c.yellow}${arg}${c.reset}`);
|
|
144
|
+
console.log(`Run ${c.cyan}better-codex --help${c.reset} for usage.`);
|
|
61
145
|
process.exit(1);
|
|
62
146
|
}
|
|
63
147
|
|
|
64
148
|
return { command, options };
|
|
65
149
|
};
|
|
66
150
|
|
|
151
|
+
const isRoot = (dir) =>
|
|
152
|
+
existsSync(join(dir, 'apps', 'backend', 'package.json')) &&
|
|
153
|
+
existsSync(join(dir, 'apps', 'web', 'package.json'));
|
|
154
|
+
|
|
67
155
|
const findRoot = (explicit) => {
|
|
68
|
-
|
|
156
|
+
if (explicit) {
|
|
157
|
+
const resolved = resolve(explicit);
|
|
158
|
+
if (isRoot(resolved)) {
|
|
159
|
+
return resolved;
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Specified root does not contain apps/: ${explicit}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let current = resolve(process.cwd());
|
|
69
165
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
70
166
|
if (isRoot(current)) {
|
|
71
167
|
return current;
|
|
72
168
|
}
|
|
73
169
|
const parent = dirname(current);
|
|
74
|
-
if (parent === current)
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
170
|
+
if (parent === current) break;
|
|
77
171
|
current = parent;
|
|
78
172
|
}
|
|
173
|
+
|
|
174
|
+
const bundledRoot = resolve(dirname(__filename), '..');
|
|
175
|
+
if (isRoot(bundledRoot)) {
|
|
176
|
+
return bundledRoot;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const npmGlobalRoot = resolve(dirname(__filename), '..', '..');
|
|
180
|
+
const npmPackageRoot = join(npmGlobalRoot, 'better-codex');
|
|
181
|
+
if (isRoot(npmPackageRoot)) {
|
|
182
|
+
return npmPackageRoot;
|
|
183
|
+
}
|
|
184
|
+
|
|
79
185
|
throw new Error(
|
|
80
|
-
|
|
186
|
+
`Could not locate Better Codex apps.\n` +
|
|
187
|
+
`The bundled apps may be missing. Try reinstalling:\n` +
|
|
188
|
+
` ${c.cyan}npm install -g better-codex${c.reset}`
|
|
81
189
|
);
|
|
82
190
|
};
|
|
83
191
|
|
|
84
|
-
const isRoot = (dir) =>
|
|
85
|
-
existsSync(join(dir, 'apps', 'backend', 'package.json')) &&
|
|
86
|
-
existsSync(join(dir, 'apps', 'web', 'package.json'));
|
|
87
|
-
|
|
88
192
|
const ensureBun = () => {
|
|
89
|
-
const result = spawnSync('bun', ['--version'], { stdio: '
|
|
193
|
+
const result = spawnSync('bun', ['--version'], { stdio: 'pipe' });
|
|
90
194
|
if (result.status !== 0) {
|
|
91
|
-
|
|
195
|
+
console.log('');
|
|
196
|
+
log.error(`${c.bold}Bun is required but not installed${c.reset}`);
|
|
197
|
+
console.log('');
|
|
198
|
+
console.log(` Install Bun: ${c.cyan}${c.underline}https://bun.sh${c.reset}`);
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(` ${c.dim}# Quick install:${c.reset}`);
|
|
201
|
+
console.log(` ${c.dim}$${c.reset} curl -fsSL https://bun.sh/install | bash`);
|
|
202
|
+
console.log('');
|
|
203
|
+
process.exit(1);
|
|
92
204
|
}
|
|
205
|
+
const version = result.stdout?.toString().trim() || 'unknown';
|
|
206
|
+
return version;
|
|
93
207
|
};
|
|
94
208
|
|
|
95
|
-
const ensureDeps = (cwd) => {
|
|
209
|
+
const ensureDeps = (cwd, name) => {
|
|
96
210
|
const nodeModules = join(cwd, 'node_modules');
|
|
97
211
|
if (existsSync(nodeModules)) {
|
|
98
|
-
return;
|
|
212
|
+
return true;
|
|
99
213
|
}
|
|
100
|
-
|
|
214
|
+
log.step(`Installing dependencies for ${c.cyan}${name}${c.reset}...`);
|
|
215
|
+
const result = spawnSync('bun', ['install'], { cwd, stdio: 'ignore' });
|
|
101
216
|
if (result.status !== 0) {
|
|
102
|
-
|
|
217
|
+
log.error(`Failed to install dependencies in ${cwd}`);
|
|
218
|
+
process.exit(1);
|
|
103
219
|
}
|
|
220
|
+
log.success(`Dependencies installed for ${c.cyan}${name}${c.reset}`);
|
|
221
|
+
return true;
|
|
104
222
|
};
|
|
105
223
|
|
|
106
224
|
const openUrl = (url) => {
|
|
@@ -115,16 +233,28 @@ const openUrl = (url) => {
|
|
|
115
233
|
};
|
|
116
234
|
|
|
117
235
|
const runWeb = (options) => {
|
|
118
|
-
|
|
236
|
+
console.log(BANNER);
|
|
237
|
+
|
|
238
|
+
const bunVersion = ensureBun();
|
|
239
|
+
log.success(`Bun ${c.dim}v${bunVersion}${c.reset}`);
|
|
240
|
+
|
|
119
241
|
const root = findRoot(options.root);
|
|
242
|
+
log.success(`Project root: ${c.dim}${root}${c.reset}`);
|
|
243
|
+
|
|
120
244
|
const backendDir = join(root, 'apps', 'backend');
|
|
121
245
|
const webDir = join(root, 'apps', 'web');
|
|
122
246
|
const token = randomUUID();
|
|
123
247
|
const backendUrl = `http://${options.host}:${options.backendPort}`;
|
|
124
248
|
const webUrl = `http://${options.host}:${options.webPort}`;
|
|
125
249
|
|
|
126
|
-
|
|
127
|
-
|
|
250
|
+
console.log('');
|
|
251
|
+
|
|
252
|
+
ensureDeps(backendDir, 'backend');
|
|
253
|
+
ensureDeps(webDir, 'web');
|
|
254
|
+
|
|
255
|
+
console.log('');
|
|
256
|
+
log.step(`Starting servers...`);
|
|
257
|
+
console.log('');
|
|
128
258
|
|
|
129
259
|
const backend = spawn('bun', ['run', 'dev'], {
|
|
130
260
|
cwd: backendDir,
|
|
@@ -133,9 +263,10 @@ const runWeb = (options) => {
|
|
|
133
263
|
CODEX_HUB_HOST: options.host,
|
|
134
264
|
CODEX_HUB_PORT: String(options.backendPort),
|
|
135
265
|
CODEX_HUB_TOKEN: token,
|
|
136
|
-
CODEX_HUB_DEFAULT_CWD:
|
|
266
|
+
CODEX_HUB_DEFAULT_CWD: process.cwd(),
|
|
137
267
|
},
|
|
138
|
-
stdio: '
|
|
268
|
+
stdio: 'pipe',
|
|
269
|
+
detached: false,
|
|
139
270
|
});
|
|
140
271
|
|
|
141
272
|
const web = spawn(
|
|
@@ -148,27 +279,50 @@ const runWeb = (options) => {
|
|
|
148
279
|
VITE_CODEX_HUB_URL: backendUrl,
|
|
149
280
|
VITE_CODEX_HUB_TOKEN: token,
|
|
150
281
|
},
|
|
151
|
-
stdio: '
|
|
282
|
+
stdio: 'pipe',
|
|
283
|
+
detached: false,
|
|
152
284
|
}
|
|
153
285
|
);
|
|
154
286
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
console.log('');
|
|
287
|
+
backend.stdout?.resume();
|
|
288
|
+
backend.stderr?.resume();
|
|
289
|
+
web.stdout?.resume();
|
|
290
|
+
web.stderr?.resume();
|
|
160
291
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
292
|
+
setTimeout(() => {
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log(
|
|
295
|
+
box(
|
|
296
|
+
[
|
|
297
|
+
`${c.bold}Better Codex is running${c.reset}`,
|
|
298
|
+
'',
|
|
299
|
+
` Web UI ${c.cyan}${c.underline}${webUrl}${c.reset}`,
|
|
300
|
+
` Backend ${c.dim}${backendUrl}${c.reset}`,
|
|
301
|
+
'',
|
|
302
|
+
` ${c.dim}Press ${c.reset}${c.bold}Ctrl+C${c.reset}${c.dim} to stop${c.reset}`,
|
|
303
|
+
],
|
|
304
|
+
c.green
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
console.log('');
|
|
308
|
+
|
|
309
|
+
if (options.open) {
|
|
310
|
+
log.info(`Opening browser...`);
|
|
311
|
+
openUrl(webUrl);
|
|
312
|
+
}
|
|
313
|
+
}, 1500);
|
|
164
314
|
|
|
165
315
|
const shutdown = () => {
|
|
316
|
+
console.log('');
|
|
317
|
+
log.info('Shutting down...');
|
|
166
318
|
backend.kill();
|
|
167
319
|
web.kill();
|
|
168
320
|
};
|
|
169
321
|
|
|
170
322
|
process.on('SIGINT', () => {
|
|
171
323
|
shutdown();
|
|
324
|
+
log.success('Goodbye! 👋');
|
|
325
|
+
console.log('');
|
|
172
326
|
process.exit(0);
|
|
173
327
|
});
|
|
174
328
|
|
|
@@ -178,13 +332,15 @@ const runWeb = (options) => {
|
|
|
178
332
|
});
|
|
179
333
|
|
|
180
334
|
const onExit = (name) => (code) => {
|
|
181
|
-
|
|
335
|
+
if (code !== 0 && code !== null) {
|
|
336
|
+
log.error(`${name} exited with code ${code}`);
|
|
337
|
+
}
|
|
182
338
|
shutdown();
|
|
183
339
|
process.exit(code ?? 0);
|
|
184
340
|
};
|
|
185
341
|
|
|
186
|
-
backend.on('exit', onExit('
|
|
187
|
-
web.on('exit', onExit('
|
|
342
|
+
backend.on('exit', onExit('Backend'));
|
|
343
|
+
web.on('exit', onExit('Web'));
|
|
188
344
|
};
|
|
189
345
|
|
|
190
346
|
const main = () => {
|