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 CHANGED
@@ -1,26 +1,79 @@
1
1
  # better-codex
2
2
 
3
- Consumer-friendly launcher for the Codex Hub web UI.
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
- ## Install
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
- Requires Bun: https://bun.sh
27
+ ## Run (zero-config)
28
+ ```bash
29
+ better-codex
30
+ ```
12
31
 
13
- ## Run
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
- `better-codex web` works too (same command).
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
- Options:
22
- - `--root PATH` set the repo root (defaults to current dir or parent lookup).
23
- - `--host 127.0.0.1` host for backend + UI.
24
- - `--backend-port 7711` backend port.
25
- - `--web-port 5173` UI port.
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.
@@ -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(`better-codex
17
-
18
- Usage:
19
- better-codex web [--root PATH] [--host 127.0.0.1] [--backend-port 7711] [--web-port 5173] [--open]
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 === 'web' ? args.slice(1) : args;
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
- if (arg === '--help' || arg === '-h') {
56
- printHelp();
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
- let current = resolve(explicit ?? process.cwd());
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
- 'Could not locate. Run from the repo root or pass --root.'
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: 'ignore' });
193
+ const result = spawnSync('bun', ['--version'], { stdio: 'pipe' });
90
194
  if (result.status !== 0) {
91
- throw new Error('bun is required. Install it first: https://bun.sh');
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
- const result = spawnSync('bun', ['install'], { cwd, stdio: 'inherit' });
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
- throw new Error(`bun install failed in ${cwd}`);
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
- ensureBun();
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
- ensureDeps(backendDir);
127
- ensureDeps(webDir);
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: root,
266
+ CODEX_HUB_DEFAULT_CWD: process.cwd(),
137
267
  },
138
- stdio: 'inherit',
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: 'inherit',
282
+ stdio: 'pipe',
283
+ detached: false,
152
284
  }
153
285
  );
154
286
 
155
- console.log('');
156
- console.log('Codex Web running:');
157
- console.log(` UI: ${webUrl}`);
158
- console.log(` Backend: ${backendUrl}`);
159
- console.log('');
287
+ backend.stdout?.resume();
288
+ backend.stderr?.resume();
289
+ web.stdout?.resume();
290
+ web.stderr?.resume();
160
291
 
161
- if (options.open) {
162
- openUrl(webUrl);
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
- console.error(`${name} exited with code ${code}`);
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('backend'));
187
- web.on('exit', onExit('web'));
342
+ backend.on('exit', onExit('Backend'));
343
+ web.on('exit', onExit('Web'));
188
344
  };
189
345
 
190
346
  const main = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-codex",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Web launcher for Codex Hub",
5
5
  "bin": {
6
6
  "better-codex": "bin/better-codex.cjs"