codex-relay 1.3.1 → 1.3.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 CHANGED
@@ -28,6 +28,26 @@ npx codex-relay@latest approve XXXX-XXXX
28
28
 
29
29
  After approval, the phone can list Codex threads, start new work, stream messages, and handle approval prompts from the local Codex runtime.
30
30
 
31
+ ## Shared Terminal and Mobile Sessions
32
+
33
+ By default, Codex Relay starts a private app-server process. A terminal TUI that was started separately can resume the same saved thread, but it does not receive the relay process's live events.
34
+
35
+ On macOS, Linux, or WSL, opt in to Codex's shared app-server socket:
36
+
37
+ ```sh
38
+ npx codex-relay@latest --shared-app-server
39
+ ```
40
+
41
+ Then connect a new terminal TUI to the shared app-server socket:
42
+
43
+ ```sh
44
+ codex resume --remote unix://
45
+ ```
46
+
47
+ Pass a thread ID after `unix://` to open a specific thread. The relay prints the attach command at startup. Mobile and the connected terminal can then observe the same live sessions through one socket-backed app-server. An already-running standalone TUI cannot be converted in place; exit it and reconnect with `--remote`.
48
+
49
+ Shared mode requires a recent Codex CLI with Unix-socket app-server and `resume --remote` support. If those features are unavailable, update Codex or omit `--shared-app-server` to keep the existing private mode. Native Windows is not currently supported; use WSL or private mode there.
50
+
31
51
  ## Background Mode
32
52
 
33
53
  To keep the relay running after the command returns:
@@ -69,6 +89,12 @@ npx codex-relay@latest --bg
69
89
 
70
90
  Start the relay in the background.
71
91
 
92
+ ```sh
93
+ npx codex-relay@latest --shared-app-server
94
+ ```
95
+
96
+ Start the relay through Codex's shared app-server socket.
97
+
72
98
  ```sh
73
99
  npx codex-relay@latest qr
74
100
  ```
@@ -99,6 +125,7 @@ The relay listens on `0.0.0.0:8787` by default. Configure it with environment va
99
125
  | `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`. |
100
126
  | `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically. |
101
127
  | `CODEX_RELAY_DANGEROUSLY_AUTO_APPROVE` | Set to `1` to auto-approve mobile pairing requests. Prefer the CLI flag for local use. |
128
+ | `CODEX_RELAY_APP_SERVER_MODE` | Set to `socket` for shared terminal/mobile sessions. Defaults to `stdio`. |
102
129
  | `CODEX_HOME` | Codex home directory, used when reading Codex session metadata. |
103
130
  | `CODEX_BIN` | Codex CLI executable path. |
104
131
 
package/dist/cli.js CHANGED
@@ -48,16 +48,18 @@ function isRelayProcessCommand(command) {
48
48
  //#endregion
49
49
  //#region src/cli.ts
50
50
  const npxCommand = "npx codex-relay@latest";
51
- 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").option("--debug", "write verbose relay diagnostics to debug.log").option("--dangerously-auto-approve", "automatically approve mobile pairing requests without a local approval command").addHelpText("after", `
51
+ 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").option("--debug", "write verbose relay diagnostics to debug.log").option("--shared-app-server", "use a shared Codex app-server socket so terminal and mobile can share live sessions").option("--dangerously-auto-approve", "automatically approve mobile pairing requests without a local approval command").addHelpText("after", `
52
52
 
53
53
  Examples:
54
54
  ${npxCommand} Start the relay and print a pairing QR
55
+ ${npxCommand} --shared-app-server Share live sessions with a connected terminal
55
56
  ${npxCommand} --bg Start the relay in the background
56
57
  ${npxCommand} qr Print the current pairing QR
57
58
  ${npxCommand} clear Sign out every paired mobile app
58
59
  ${npxCommand} approve CODE Approve a pending mobile pairing request`).action(async (options) => {
59
60
  if (options.debug) process.env.CODEX_RELAY_DEBUG = "1";
60
61
  if (options.dangerouslyAutoApprove) process.env.CODEX_RELAY_DANGEROUSLY_AUTO_APPROVE = "1";
62
+ if (options.sharedAppServer) process.env.CODEX_RELAY_APP_SERVER_MODE = "socket";
61
63
  if (options.bg) {
62
64
  await startBackgroundServer();
63
65
  return;
package/dist/src.js CHANGED
@@ -6,6 +6,7 @@ import { execFile, spawn } from "node:child_process";
6
6
  import fs, { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
7
7
  import { access, appendFile, copyFile, mkdir, open, readFile, readdir, realpath, stat, writeFile } from "node:fs/promises";
8
8
  import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
9
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import { z } from "zod";
11
12
  import { promisify } from "node:util";
@@ -19,6 +20,7 @@ import { Hono } from "hono";
19
20
  import { cors } from "hono/cors";
20
21
  import * as pty from "@lydell/node-pty";
21
22
  import { createInterface } from "node:readline";
23
+ import WebSocket from "ws";
22
24
  import { Codex } from "@openai/codex-sdk";
23
25
  import { gcm } from "@noble/ciphers/aes.js";
24
26
  import { randomBytes as randomBytes$1, utf8ToBytes } from "@noble/ciphers/utils.js";
@@ -26,23 +28,49 @@ import { ed25519, x25519 } from "@noble/curves/ed25519.js";
26
28
  import { hkdf } from "@noble/hashes/hkdf.js";
27
29
  import { sha256 } from "@noble/hashes/sha2.js";
28
30
  //#region src/codex-binary.ts
29
- const appServerArgs = [
31
+ const stdioAppServerArgs = [
30
32
  "app-server",
31
33
  "--listen",
32
34
  "stdio://"
33
35
  ];
36
+ const sharedServerArgs = [
37
+ "app-server",
38
+ "--listen",
39
+ "unix://"
40
+ ];
34
41
  const windowsShellExtensions = new Set([".bat", ".cmd"]);
42
+ function resolveCodexAppServerMode(env = process.env) {
43
+ const configuredMode = env.CODEX_RELAY_APP_SERVER_MODE?.trim().toLowerCase();
44
+ if (!configuredMode || configuredMode === "stdio") return "stdio";
45
+ if (configuredMode === "socket") return "socket";
46
+ throw new Error(`Unsupported CODEX_RELAY_APP_SERVER_MODE ${JSON.stringify(configuredMode)}. Expected "stdio" or "socket".`);
47
+ }
35
48
  function resolveCodexAppServerSpawn(input = {}) {
36
49
  const platform$1 = input.platform ?? platform();
37
50
  const command = resolveCodexBinary(input.env ?? process.env);
38
51
  const isWindows = platform$1 === "win32";
39
52
  return {
40
53
  command,
41
- args: [...appServerArgs],
54
+ args: [...stdioAppServerArgs],
42
55
  shell: isWindows && shouldUseWindowsShell(command),
43
56
  windowsHide: isWindows
44
57
  };
45
58
  }
59
+ function resolveCodexSharedAppServerSpawn(input = {}) {
60
+ const platform$2 = input.platform ?? platform();
61
+ assertAppServerModeSupported("socket", platform$2);
62
+ const command = resolveCodexBinary(input.env ?? process.env);
63
+ const isWindows = platform$2 === "win32";
64
+ return {
65
+ command,
66
+ args: [...sharedServerArgs],
67
+ shell: isWindows && shouldUseWindowsShell(command),
68
+ windowsHide: isWindows
69
+ };
70
+ }
71
+ function assertAppServerModeSupported(mode, platform) {
72
+ if (mode === "socket" && platform === "win32") throw new Error("Shared Codex app-server mode requires macOS, Linux, or WSL.");
73
+ }
46
74
  function resolveCodexBinary(env) {
47
75
  return env.CODEX_BIN?.trim() || "codex";
48
76
  }
@@ -60,6 +88,11 @@ var CodexAppServerClient = class {
60
88
  pending = /* @__PURE__ */ new Map();
61
89
  requestHandlers = /* @__PURE__ */ new Set();
62
90
  readline;
91
+ sharedServer;
92
+ socket;
93
+ initialize() {
94
+ return this.ensureInitialized();
95
+ }
63
96
  async listThreads(limit = 80) {
64
97
  return (await this.request("thread/list", {
65
98
  limit,
@@ -136,8 +169,12 @@ var CodexAppServerClient = class {
136
169
  this.pending.clear();
137
170
  this.readline?.close();
138
171
  this.child?.kill();
172
+ this.socket?.close();
173
+ this.sharedServer?.kill();
139
174
  this.readline = void 0;
140
175
  this.child = void 0;
176
+ this.socket = void 0;
177
+ this.sharedServer = void 0;
141
178
  this.initialized = void 0;
142
179
  }
143
180
  async request(method, params) {
@@ -150,15 +187,12 @@ var CodexAppServerClient = class {
150
187
  resolve: (value) => resolve(value),
151
188
  reject
152
189
  });
153
- this.child.stdin.write(`${JSON.stringify({
190
+ this.writeJson({
154
191
  id,
155
192
  method,
156
193
  params
157
- })}\n`, (error) => {
158
- if (error) {
159
- this.pending.delete(id);
160
- reject(error);
161
- }
194
+ }).catch((error) => {
195
+ if (this.pending.delete(id)) reject(error);
162
196
  });
163
197
  });
164
198
  }
@@ -166,7 +200,33 @@ var CodexAppServerClient = class {
166
200
  if (!this.initialized) this.initialized = this.start();
167
201
  return this.initialized;
168
202
  }
169
- start() {
203
+ async start() {
204
+ try {
205
+ if (resolveCodexAppServerMode() === "socket") {
206
+ this.sharedServer = await startSharedCodexAppServer();
207
+ await this.connectSharedCodexAppServer();
208
+ } else this.startStdioCodexAppServer();
209
+ await this.requestRaw("initialize", {
210
+ clientInfo: {
211
+ name: "codex-relay",
212
+ title: "Codex Relay Mobile Server",
213
+ version: "1.2.0"
214
+ },
215
+ capabilities: { experimentalApi: true }
216
+ });
217
+ } catch (error) {
218
+ this.readline?.close();
219
+ this.readline = void 0;
220
+ this.child?.kill();
221
+ this.child = void 0;
222
+ this.socket?.close();
223
+ this.socket = void 0;
224
+ this.sharedServer?.kill();
225
+ this.sharedServer = void 0;
226
+ throw error;
227
+ }
228
+ }
229
+ startStdioCodexAppServer() {
170
230
  const spawnConfig = resolveCodexAppServerSpawn();
171
231
  this.child = spawn(spawnConfig.command, spawnConfig.args, {
172
232
  env: process.env,
@@ -187,14 +247,31 @@ var CodexAppServerClient = class {
187
247
  this.child = void 0;
188
248
  this.initialized = void 0;
189
249
  });
190
- return this.requestRaw("initialize", {
191
- clientInfo: {
192
- name: "codex-relay",
193
- title: "Codex Relay Mobile Server",
194
- version: "1.2.0"
195
- },
196
- capabilities: { experimentalApi: true }
197
- }).then(() => void 0);
250
+ }
251
+ async connectSharedCodexAppServer() {
252
+ const socket = new WebSocket(`ws+unix://${sharedCodexAppServerSocketPath()}:/`, { perMessageDeflate: false });
253
+ this.socket = socket;
254
+ await new Promise((resolve, reject) => {
255
+ const handleOpen = () => {
256
+ socket.off("error", handleInitialError);
257
+ resolve();
258
+ };
259
+ const handleInitialError = (error) => {
260
+ socket.off("open", handleOpen);
261
+ reject(error);
262
+ };
263
+ socket.once("open", handleOpen);
264
+ socket.once("error", handleInitialError);
265
+ });
266
+ socket.on("message", (data) => this.handleLine(String(data)));
267
+ socket.on("error", (error) => this.rejectAll(error));
268
+ socket.once("close", (code, reason) => {
269
+ this.rejectAll(/* @__PURE__ */ new Error(`Codex app-server socket closed with ${code}${reason.length > 0 ? `: ${String(reason)}` : "."}`));
270
+ this.socket = void 0;
271
+ this.sharedServer?.kill();
272
+ this.sharedServer = void 0;
273
+ this.initialized = void 0;
274
+ });
198
275
  }
199
276
  requestRaw(method, params) {
200
277
  const id = this.nextId++;
@@ -210,11 +287,8 @@ var CodexAppServerClient = class {
210
287
  resolve: (value) => resolve(value),
211
288
  reject
212
289
  });
213
- this.child.stdin.write(`${request}\n`, (error) => {
214
- if (error) {
215
- this.pending.delete(id);
216
- reject(error);
217
- }
290
+ this.writeSerializedJson(request).catch((error) => {
291
+ if (this.pending.delete(id)) reject(error);
218
292
  });
219
293
  });
220
294
  }
@@ -256,12 +330,22 @@ var CodexAppServerClient = class {
256
330
  } else pending.resolve(message.result);
257
331
  }
258
332
  writeJson(payload) {
333
+ return this.writeSerializedJson(JSON.stringify(payload));
334
+ }
335
+ writeSerializedJson(payload) {
259
336
  return new Promise((resolve, reject) => {
337
+ if (this.socket) {
338
+ this.socket.send(payload, (error) => {
339
+ if (error) reject(error);
340
+ else resolve();
341
+ });
342
+ return;
343
+ }
260
344
  if (!this.child?.stdin) {
261
345
  reject(/* @__PURE__ */ new Error("Codex app-server is not running."));
262
346
  return;
263
347
  }
264
- this.child.stdin.write(`${JSON.stringify(payload)}\n`, (error) => {
348
+ this.child.stdin.write(`${payload}\n`, (error) => {
265
349
  if (error) reject(error);
266
350
  else resolve();
267
351
  });
@@ -272,6 +356,46 @@ var CodexAppServerClient = class {
272
356
  this.pending.clear();
273
357
  }
274
358
  };
359
+ async function startSharedCodexAppServer() {
360
+ const spawnConfig = resolveCodexSharedAppServerSpawn();
361
+ const child = spawn(spawnConfig.command, spawnConfig.args, {
362
+ env: process.env,
363
+ shell: spawnConfig.shell,
364
+ windowsHide: spawnConfig.windowsHide
365
+ });
366
+ let spawnError;
367
+ let exitReason;
368
+ let stderr = "";
369
+ child.stdout.resume();
370
+ child.stderr.on("data", (chunk) => {
371
+ if (stderr.length < 8192) stderr += String(chunk).slice(0, 8192 - stderr.length);
372
+ });
373
+ child.once("error", (error) => {
374
+ spawnError = error;
375
+ });
376
+ child.once("exit", (code, signal) => {
377
+ exitReason = signal ?? code ?? 1;
378
+ });
379
+ const socketPath = sharedCodexAppServerSocketPath();
380
+ for (let attempt = 0; attempt < 100; attempt += 1) {
381
+ if (spawnError) throw new Error(`Failed to start the shared Codex app-server: ${spawnError.message}`);
382
+ try {
383
+ await access(socketPath);
384
+ return child;
385
+ } catch {
386
+ if (exitReason !== void 0) {
387
+ const detail = stderr.trim();
388
+ throw new Error(`Failed to start the shared Codex app-server (exit ${exitReason})${detail ? `: ${detail}` : "."}`);
389
+ }
390
+ await setTimeout$1(25);
391
+ }
392
+ }
393
+ child.kill();
394
+ throw new Error(`Timed out waiting for the shared Codex app-server socket at ${socketPath}.`);
395
+ }
396
+ function sharedCodexAppServerSocketPath() {
397
+ return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "app-server-control", "app-server-control.sock");
398
+ }
275
399
  function debugAppServer(kind, method, id, detail) {
276
400
  if (process.env.CODEX_RELAY_DEBUG_APP_SERVER !== "1") return;
277
401
  console.log(`[app-server] ${kind}${id === void 0 ? "" : ` #${id}`}${method ? ` ${method}` : ""}${detail ? ` ${detail}` : ""}`);
@@ -6274,8 +6398,17 @@ const color = {
6274
6398
  const npxCommand = "npx codex-relay@latest";
6275
6399
  const sessionStore = await createTursoPairingSessionStore(process.env.CODEX_RELAY_AUTH_DB_PATH ?? await prepareCodexRelayDataPath("auth.db", ["auth.db-shm", "auth.db-wal"]));
6276
6400
  const preferencesStore = createFileRuntimePreferencesStore(process.env.CODEX_RELAY_PREFERENCES_PATH ?? await prepareCodexRelayDataPath("preferences.json"));
6401
+ const appServerMode = resolveCodexAppServerMode();
6402
+ const sharedAppServer = appServerMode === "socket" ? new CodexAppServerClient() : void 0;
6403
+ if (sharedAppServer) {
6404
+ await sharedAppServer.initialize();
6405
+ process.once("SIGINT", () => stopSharedAppServer(130));
6406
+ process.once("SIGTERM", () => stopSharedAppServer(143));
6407
+ process.once("exit", () => sharedAppServer.close());
6408
+ }
6277
6409
  serve({
6278
6410
  fetch: createApp({
6411
+ appServer: sharedAppServer,
6279
6412
  pairing: {
6280
6413
  approvalSecret,
6281
6414
  dangerouslyAutoApprove,
@@ -6346,9 +6479,14 @@ serve({
6346
6479
  dangerouslyAutoApprove,
6347
6480
  listenUrl,
6348
6481
  pairingPayload,
6349
- port: info.port
6482
+ port: info.port,
6483
+ sharedAppServer: appServerMode === "socket"
6350
6484
  }));
6351
6485
  });
6486
+ function stopSharedAppServer(exitCode) {
6487
+ sharedAppServer?.close();
6488
+ process.exit(exitCode);
6489
+ }
6352
6490
  function formatStartupInstructions(details) {
6353
6491
  return [
6354
6492
  "",
@@ -6361,6 +6499,11 @@ function formatStartupInstructions(details) {
6361
6499
  `${color.prompt("›")} Server: ${color.muted(details.listenUrl)}`,
6362
6500
  "",
6363
6501
  `${color.prompt("›")} Pairing: ${color.url(details.pairingPayload)}`,
6502
+ ...details.sharedAppServer ? [
6503
+ "",
6504
+ `${color.prompt("›")} Terminal: ${color.command("codex resume --remote unix://")}`,
6505
+ ` ${color.muted("Connect through the shared Codex app-server to follow and steer the same live sessions.")}`
6506
+ ] : [],
6364
6507
  "",
6365
6508
  `${color.prompt("›")} Commands`,
6366
6509
  ` ${color.command(npxCommand)} Start and print a pairing QR`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -41,7 +41,7 @@
41
41
  "@noble/ciphers": "2.2.0",
42
42
  "@noble/curves": "2.2.0",
43
43
  "@noble/hashes": "2.2.0",
44
- "@openai/codex-sdk": "0.144.1",
44
+ "@openai/codex-sdk": "0.144.5",
45
45
  "base64-js": "1.5.1",
46
46
  "commander": "^14.0.3",
47
47
  "es-git": "^0.6.0",