codex-relay 1.0.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 ADDED
@@ -0,0 +1,139 @@
1
+ # Codex Relay CLI
2
+
3
+ Codex Relay runs a local bridge server for the Codex Relay mobile app. Keep Codex on your computer, then use your phone to pair with that local session, send prompts, watch streamed output, and respond to approval requests.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 22.14 or newer
8
+ - Codex CLI installed and signed in on the computer running the relay
9
+ - The Codex Relay mobile app on the same network, Tailscale network, or another route that can reach your computer
10
+
11
+ ## Start the Relay
12
+
13
+ Run the server from the workspace you want Codex to use:
14
+
15
+ ```sh
16
+ npx codex-relay
17
+ ```
18
+
19
+ The CLI prints a QR code, a mobile URL, and a `codex-relay://pair...` pairing payload. Scan the QR code from the mobile app. If scanning is not available, paste the full pairing payload into the app.
20
+
21
+ When the app shows an approval code, approve it on the computer:
22
+
23
+ ```sh
24
+ npx codex-relay approve XXXX-XXXX
25
+ ```
26
+
27
+ After approval, the phone can list Codex threads, start new work, stream messages, and handle approval prompts from the local Codex runtime.
28
+
29
+ ## Background Mode
30
+
31
+ To keep the relay running after the command returns:
32
+
33
+ ```sh
34
+ npx codex-relay --bg
35
+ ```
36
+
37
+ Background mode writes runtime files under `.codex-relay/` in the current directory:
38
+
39
+ - `.codex-relay/server.log`
40
+ - `.codex-relay/server.pid`
41
+ - `.codex-relay/server-state.json`
42
+ - `.codex-relay/auth.db`
43
+
44
+ Print the current pairing QR again:
45
+
46
+ ```sh
47
+ npx codex-relay qr
48
+ ```
49
+
50
+ Stop a background server with the printed process id:
51
+
52
+ ```sh
53
+ kill -TERM <pid>
54
+ ```
55
+
56
+ ## Commands
57
+
58
+ ```sh
59
+ npx codex-relay
60
+ ```
61
+
62
+ Start the relay in the foreground.
63
+
64
+ ```sh
65
+ npx codex-relay --bg
66
+ ```
67
+
68
+ Start the relay in the background.
69
+
70
+ ```sh
71
+ npx codex-relay qr
72
+ ```
73
+
74
+ Print the latest pairing QR for an already running relay.
75
+
76
+ ```sh
77
+ npx codex-relay approve XXXX-XXXX
78
+ ```
79
+
80
+ Approve a pending mobile pairing request.
81
+
82
+ ## Configuration
83
+
84
+ The relay listens on `0.0.0.0:8787` by default. Configure it with environment variables:
85
+
86
+ | Variable | Purpose |
87
+ | ----------------------------- | ------------------------------------------------------------------------------------------- |
88
+ | `PORT` | Server port. Defaults to `8787`. |
89
+ | `HOST` | Listen host. Defaults to `0.0.0.0`. |
90
+ | `CODEX_RELAY_PUBLIC_URL` | URL printed into the pairing QR, for example a Tailscale or tunnel URL. |
91
+ | `CODEX_RELAY_WORKSPACE_PATH` | Workspace path Codex should use. Defaults to the directory where you run `npx codex-relay`. |
92
+ | `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`. |
93
+ | `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically. |
94
+ | `CODEX_HOME` | Codex home directory, used when reading Codex session metadata. |
95
+ | `CODEX_BIN` | Codex CLI executable path. |
96
+
97
+ Examples:
98
+
99
+ ```sh
100
+ PORT=8788 npx codex-relay
101
+ ```
102
+
103
+ ```sh
104
+ CODEX_RELAY_PUBLIC_URL=http://100.64.0.10:8787 npx codex-relay
105
+ ```
106
+
107
+ ```sh
108
+ CODEX_RELAY_WORKSPACE_PATH=/path/to/project npx codex-relay
109
+ ```
110
+
111
+ ## Network Notes
112
+
113
+ The phone must be able to reach the URL printed as `Mobile:` by the relay.
114
+
115
+ - On the same Wi-Fi network, the relay usually prints a local network address.
116
+ - On Tailscale, the relay prefers your Tailscale address when it can detect one.
117
+ - If the printed URL is not reachable from the phone, set `CODEX_RELAY_PUBLIC_URL` to a reachable HTTP URL.
118
+
119
+ ## Troubleshooting
120
+
121
+ If `npx codex-relay qr` cannot find a server, start one first:
122
+
123
+ ```sh
124
+ npx codex-relay
125
+ ```
126
+
127
+ If the relay says another process is using the local pairing database, use the existing server:
128
+
129
+ ```sh
130
+ npx codex-relay qr
131
+ ```
132
+
133
+ Or stop the background process shown by the CLI:
134
+
135
+ ```sh
136
+ kill -TERM <pid>
137
+ ```
138
+
139
+ If the mobile app cannot connect, confirm that the phone can reach the printed `Mobile:` URL and that the chosen port is not blocked by a firewall.
package/dist/cli.js ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import { w as apiPaths } from "./src.js";
3
+ import { Command } from "@commander-js/extra-typings";
4
+ import qrcode from "qrcode-terminal";
5
+ import { spawn } from "node:child_process";
6
+ import { closeSync, openSync } from "node:fs";
7
+ import { mkdir, readFile, unlink } from "node:fs/promises";
8
+ import { dirname, resolve } from "node:path";
9
+ import { setTimeout } from "node:timers/promises";
10
+ import { fileURLToPath } from "node:url";
11
+ //#region src/cli.ts
12
+ const program = new Command().name("codex-relay").description("Run and approve the codex-relay local CLI bridge.").option("--bg", "run the Codex Relay server in the background").action(async (options) => {
13
+ if (options.bg) {
14
+ await startBackgroundServer();
15
+ return;
16
+ }
17
+ await import("./src2.js").catch(handleServerStartError);
18
+ });
19
+ program.command("qr").description("Print the current pairing QR for an already running server.").action(async () => {
20
+ await printPairingQr();
21
+ });
22
+ program.command("approve").description("Approve a pending mobile pairing request.").argument("<approval-code>", "approval code shown in the mobile app").action(async (approvalCode) => {
23
+ await approvePairing(approvalCode);
24
+ });
25
+ await program.parseAsync();
26
+ async function startBackgroundServer() {
27
+ const logPath = resolve(process.cwd(), ".codex-relay/server.log");
28
+ const pidPath = resolve(process.cwd(), ".codex-relay/server.pid");
29
+ await mkdir(dirname(logPath), { recursive: true });
30
+ const existingPid = await readRunningPid(pidPath);
31
+ if (existingPid) {
32
+ console.log(`codex-relay is already running in the background (pid ${existingPid}).`);
33
+ console.log(`Logs: ${logPath}`);
34
+ console.log("Print the current pairing QR with: npx codex-relay qr");
35
+ return;
36
+ }
37
+ await unlink(pidPath).catch(() => void 0);
38
+ const output = openSync(logPath, "a", 384);
39
+ const cliPath = fileURLToPath(import.meta.url);
40
+ const child = spawn(process.execPath, [
41
+ ...process.execArgv,
42
+ cliPath,
43
+ ...backgroundArgs()
44
+ ], {
45
+ cwd: process.cwd(),
46
+ detached: true,
47
+ env: {
48
+ ...process.env,
49
+ CODEX_RELAY_BACKGROUND: "1",
50
+ CODEX_RELAY_PID_PATH: pidPath
51
+ },
52
+ stdio: [
53
+ "ignore",
54
+ output,
55
+ output
56
+ ]
57
+ });
58
+ child.unref();
59
+ closeSync(output);
60
+ const startedPid = await waitForBackgroundPid(child, pidPath);
61
+ if (!startedPid) {
62
+ console.error("codex-relay failed to start in the background.");
63
+ console.error(`Logs: ${logPath}`);
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+ console.log(`Started codex-relay in the background (pid ${startedPid}).`);
68
+ console.log(`Logs: ${logPath}`);
69
+ console.log("Print the pairing QR later with: npx codex-relay qr");
70
+ }
71
+ async function readRunningPid(pidPath) {
72
+ const value = await readFile(pidPath, "utf8").catch(() => void 0);
73
+ const pid = value ? Number(value.trim()) : NaN;
74
+ if (!Number.isInteger(pid) || pid <= 0) return;
75
+ try {
76
+ process.kill(pid, 0);
77
+ return pid;
78
+ } catch {
79
+ return;
80
+ }
81
+ }
82
+ function backgroundArgs() {
83
+ return process.argv.slice(2).filter((arg) => arg !== "--bg");
84
+ }
85
+ async function waitForBackgroundPid(child, pidPath) {
86
+ let childExited = false;
87
+ child.once("exit", () => {
88
+ childExited = true;
89
+ });
90
+ for (let attempt = 0; attempt < 50; attempt += 1) {
91
+ const pid = await readRunningPid(pidPath);
92
+ if (pid) return pid;
93
+ if (childExited) return;
94
+ await setTimeout(100);
95
+ }
96
+ }
97
+ async function approvePairing(rawCode) {
98
+ const approvalCode = normalizeApprovalCode(rawCode ?? "");
99
+ if (!approvalCode) {
100
+ console.error("Usage: codex-relay approve XXXX-XXXX");
101
+ process.exitCode = 1;
102
+ return;
103
+ }
104
+ const endpoint = await getApprovalEndpoint();
105
+ const secret = await readApprovalSecret();
106
+ const response = await fetch(`${endpoint}${apiPaths.pairApprove}`, {
107
+ method: "POST",
108
+ headers: {
109
+ accept: "application/json",
110
+ "content-type": "application/json",
111
+ "x-codex-relay-approve-secret": secret
112
+ },
113
+ body: JSON.stringify({ approvalCode })
114
+ });
115
+ const payload = await response.json().catch(() => void 0);
116
+ if (!response.ok) {
117
+ const message = payload && typeof payload === "object" && "error" in payload && payload.error && typeof payload.error === "object" && "message" in payload.error ? String(payload.error.message) : `Codex Relay server returned ${response.status}`;
118
+ console.error(message);
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ console.log("Approved Codex Relay pairing request.");
123
+ }
124
+ async function printPairingQr() {
125
+ const storedState = await readServerState();
126
+ const state = storedState?.pairingPayload ? storedState : await readServerLogState();
127
+ if (!state?.pairingPayload) {
128
+ console.error("No running Codex Relay server state was found.");
129
+ console.error("Start the server first with: npx codex-relay");
130
+ console.error("Or run it in the background with: npx codex-relay --bg");
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+ console.log("");
135
+ qrcode.generate(state.pairingPayload, { small: true });
136
+ console.log("");
137
+ if (state.connectUrl) console.log(`Mobile: ${state.connectUrl}`);
138
+ if (state.listenUrl) console.log(`Server: ${state.listenUrl}`);
139
+ console.log("");
140
+ console.log(`Pairing: ${state.pairingPayload}`);
141
+ console.log("");
142
+ }
143
+ async function handleServerStartError(error) {
144
+ if (!isDatabaseLockError(error)) throw error;
145
+ const pidPath = resolve(process.cwd(), ".codex-relay/server.pid");
146
+ const logPath = resolve(process.cwd(), ".codex-relay/server.log");
147
+ const existingPid = await readRunningPid(pidPath);
148
+ const storedState = await readServerState();
149
+ const state = storedState?.pairingPayload ? storedState : await readServerLogState();
150
+ console.error("Codex Relay is already using its local pairing database.");
151
+ console.error("");
152
+ if (existingPid) {
153
+ console.error(`A background server appears to be running (pid ${existingPid}).`);
154
+ console.error("Use the existing server instead of starting a second one:");
155
+ console.error(" npx codex-relay qr");
156
+ console.error("");
157
+ console.error("To stop the background server:");
158
+ console.error(` kill -TERM ${existingPid}`);
159
+ console.error("");
160
+ console.error(`Logs: ${logPath}`);
161
+ } else {
162
+ console.error("Another Codex Relay process is already running or exited without cleanup.");
163
+ if (state?.pairingPayload) {
164
+ console.error("Use the existing server instead of starting a second one:");
165
+ console.error(" npx codex-relay qr");
166
+ console.error("");
167
+ }
168
+ console.error("Find it with:");
169
+ console.error(" lsof -nP apps/server/.codex-relay/auth.db apps/server/.codex-relay/auth.db-wal");
170
+ console.error(" lsof -nP -iTCP:8787 -sTCP:LISTEN");
171
+ console.error("");
172
+ console.error("Then stop that process with:");
173
+ console.error(" kill -TERM <pid>");
174
+ }
175
+ console.error("");
176
+ console.error("If you wanted a persistent server, start it once with:");
177
+ console.error(" npx codex-relay --bg");
178
+ process.exitCode = 1;
179
+ }
180
+ async function readApprovalSecret() {
181
+ if (process.env.CODEX_RELAY_APPROVAL_SECRET) return process.env.CODEX_RELAY_APPROVAL_SECRET;
182
+ return (await readFile(resolve(process.cwd(), ".codex-relay/approval-secret"), "utf8")).trim();
183
+ }
184
+ async function getApprovalEndpoint() {
185
+ const state = await readServerState();
186
+ const port = process.env.PORT ? Number(process.env.PORT) : state?.port ?? 8787;
187
+ const host = process.env.HOST ?? state?.host ?? "127.0.0.1";
188
+ return `http://${host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host}:${port}`;
189
+ }
190
+ async function readServerState() {
191
+ const state = await readFile(resolve(process.cwd(), ".codex-relay/server-state.json"), "utf8").then((value) => JSON.parse(value)).catch(() => void 0);
192
+ if (!state) return;
193
+ return {
194
+ connectUrl: typeof state.connectUrl === "string" ? state.connectUrl : void 0,
195
+ host: typeof state.host === "string" ? state.host : void 0,
196
+ listenUrl: typeof state.listenUrl === "string" ? state.listenUrl : void 0,
197
+ pairingPayload: typeof state.pairingPayload === "string" ? state.pairingPayload : void 0,
198
+ port: typeof state.port === "number" ? state.port : void 0
199
+ };
200
+ }
201
+ async function readServerLogState() {
202
+ const log = await readFile(resolve(process.cwd(), ".codex-relay/server.log"), "utf8").catch(() => void 0);
203
+ if (!log) return;
204
+ const connectUrl = lastLogValue(log, "Mobile");
205
+ const listenUrl = lastLogValue(log, "Server");
206
+ const pairingPayload = lastLogValue(log, "Pairing");
207
+ return pairingPayload ? {
208
+ connectUrl,
209
+ listenUrl,
210
+ pairingPayload
211
+ } : void 0;
212
+ }
213
+ function lastLogValue(log, label) {
214
+ const pattern = new RegExp(`${label}:\\s*(\\S+)`, "g");
215
+ let value;
216
+ for (const match of log.matchAll(pattern)) value = match[1];
217
+ return value;
218
+ }
219
+ function isDatabaseLockError(error) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ return message.includes("failed to open database") && message.includes("Locking error");
222
+ }
223
+ function normalizeApprovalCode(value) {
224
+ const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "").replaceAll("O", "0").replaceAll("I", "1");
225
+ return normalized.length === 8 ? `${normalized.slice(0, 4)}-${normalized.slice(4)}` : normalized;
226
+ }
227
+ //#endregion
228
+ export {};