codex-relay 1.5.0 → 1.5.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.
Files changed (3) hide show
  1. package/README.md +14 -1
  2. package/dist/src.js +132 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,7 +20,12 @@ npx codex-relay@latest
20
20
 
21
21
  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 the relay detects multiple possible network addresses, the QR includes them and the app automatically uses the first address it can reach. If scanning is not available, paste the full pairing payload into the app.
22
22
 
23
- When the app shows an approval code, approve it on the computer:
23
+ When the app shows an approval code, the interactive relay terminal shows the
24
+ same code and asks `Approve? [y/N]`. Check that the codes match, then type `y`
25
+ and press Enter in that terminal. Other input leaves the request unapproved.
26
+ No second terminal is needed.
27
+
28
+ For background relays or non-interactive output, use the approval command:
24
29
 
25
30
  ```sh
26
31
  npx codex-relay@latest approve XXXX-XXXX
@@ -28,6 +33,14 @@ npx codex-relay@latest approve XXXX-XXXX
28
33
 
29
34
  After approval, the phone can list Codex threads, start new work, stream messages, and handle approval prompts from the local Codex runtime.
30
35
 
36
+ The thread list shows the 20 most recently active, non-archived root sessions across
37
+ workspaces. This does not delete or archive older sessions. Set
38
+ `CODEX_RELAY_THREAD_LIST_LIMIT=100` before starting the relay to show more history.
39
+ The relay reads Codex's session index so refreshing the list does not repeatedly
40
+ parse large legacy rollout files. An empty index triggers a bounded history lookup;
41
+ legacy files copied into a partially populated index may need to be discovered by
42
+ Codex before they appear in the relay.
43
+
31
44
  ## Shared Terminal and Mobile Sessions
32
45
 
33
46
  On macOS, Codex Relay prefers Codex's shared Unix socket so terminal and mobile clients can follow the same live sessions. If the shared app-server cannot start or initialize, the relay prints a warning and continues with a private app-server.
package/dist/src.js CHANGED
@@ -188,34 +188,32 @@ var CodexAppServerClient = class {
188
188
  initialize() {
189
189
  return this.ensureInitialized();
190
190
  }
191
- async listThreads(limit = 80) {
192
- const threads = [];
193
- const seenCursors = /* @__PURE__ */ new Set();
194
- let cursor;
195
- do {
196
- const response = await this.request("thread/list", {
197
- archived: false,
198
- limit,
199
- sortDirection: "desc",
200
- sortKey: "recency_at",
201
- sourceKinds: [
202
- "cli",
203
- "vscode",
204
- "exec",
205
- "appServer"
206
- ],
207
- ...cursor ? {
208
- cursor,
209
- useStateDbOnly: true
210
- } : {}
211
- });
212
- threads.push(...response.data);
213
- const nextCursor = response.nextCursor ?? void 0;
214
- if (!nextCursor || seenCursors.has(nextCursor)) break;
215
- seenCursors.add(nextCursor);
216
- cursor = nextCursor;
217
- } while (cursor);
218
- return threads;
191
+ async listThreads(limit = Number(process.env.CODEX_RELAY_THREAD_LIST_LIMIT ?? 20)) {
192
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new Error("CODEX_RELAY_THREAD_LIST_LIMIT must be a positive integer.");
193
+ const startedAt = Date.now();
194
+ const params = {
195
+ archived: false,
196
+ limit,
197
+ sortDirection: "desc",
198
+ sortKey: "recency_at",
199
+ sourceKinds: [
200
+ "cli",
201
+ "vscode",
202
+ "exec",
203
+ "appServer"
204
+ ]
205
+ };
206
+ let response = await this.request("thread/list", {
207
+ ...params,
208
+ useStateDbOnly: true
209
+ });
210
+ if (response.data.length === 0) response = await this.request("thread/list", params);
211
+ relayDebugLog("thread.list.completed", {
212
+ durationMs: Date.now() - startedAt,
213
+ limit,
214
+ count: response.data.length
215
+ });
216
+ return response.data.slice(0, limit);
219
217
  }
220
218
  async readThread(threadId, options = {}) {
221
219
  return (await this.request("thread/read", {
@@ -7431,8 +7429,91 @@ function errorMessage(error) {
7431
7429
  return error instanceof Error ? error.message : "Codex run failed.";
7432
7430
  }
7433
7431
  //#endregion
7432
+ //#region src/terminal-pairing.ts
7433
+ function createTerminalPairingApprover(options) {
7434
+ const queue = [];
7435
+ let active;
7436
+ let reader;
7437
+ let expiry;
7438
+ let answering = false;
7439
+ let closed = false;
7440
+ function print(message) {
7441
+ options.output.write(`${message}\n`);
7442
+ }
7443
+ function close() {
7444
+ closed = true;
7445
+ queue.length = 0;
7446
+ active = void 0;
7447
+ clearTimeout(expiry);
7448
+ reader?.close();
7449
+ }
7450
+ function next() {
7451
+ if (closed || active || answering) return;
7452
+ clearTimeout(expiry);
7453
+ active = queue.shift();
7454
+ while (active && active.expiresAt <= Date.now()) active = queue.shift();
7455
+ if (!active) return;
7456
+ print(`\nPairing request from ${(active.clientName ?? "mobile device").replace(/[\p{Cc}\p{Cf}]/gu, "").slice(0, 80)}. Code: ${active.approvalCode}`);
7457
+ print("Check that this code matches your phone. Approve? [y/N] (then Enter)");
7458
+ expiry = setTimeout(() => {
7459
+ print("Pairing request expired. Open the pairing link again to retry.");
7460
+ active = void 0;
7461
+ next();
7462
+ }, Math.max(1, active.expiresAt - Date.now()));
7463
+ }
7464
+ async function answer(line) {
7465
+ if (!active || answering || closed) return;
7466
+ const pairing = active;
7467
+ active = void 0;
7468
+ answering = true;
7469
+ clearTimeout(expiry);
7470
+ try {
7471
+ if (!/^(y|yes)$/i.test(line.trim())) {
7472
+ print("Not approved. You can still use the approve command for this request.");
7473
+ return;
7474
+ }
7475
+ const approved = await options.sessions.approvePendingPairing(pairing.approvalCode, Date.now());
7476
+ if (approved) options.onApproved(approved);
7477
+ else print("This pairing request expired or was already completed. Open the pairing link again.");
7478
+ } catch {
7479
+ print("Could not approve pairing. Try the approve command or open the pairing link again.");
7480
+ } finally {
7481
+ answering = false;
7482
+ next();
7483
+ }
7484
+ }
7485
+ return {
7486
+ close,
7487
+ async request(approvalCode) {
7488
+ if (closed || !options.input.isTTY || !options.output.isTTY) return;
7489
+ try {
7490
+ const pairing = await options.sessions.getPendingPairing(approvalCode, Date.now());
7491
+ if (!pairing || pairing.approved || closed) return;
7492
+ if (active?.approvalCode === approvalCode || queue.some((item) => item.approvalCode === approvalCode)) return;
7493
+ if (queue.length >= 10) {
7494
+ print("More pairing requests are waiting. Use the approve command for this request.");
7495
+ return;
7496
+ }
7497
+ if (!reader) {
7498
+ reader = createInterface({
7499
+ input: options.input,
7500
+ terminal: false
7501
+ });
7502
+ reader.on("line", (line) => void answer(line));
7503
+ reader.on("close", close);
7504
+ }
7505
+ queue.push(pairing);
7506
+ next();
7507
+ } catch {
7508
+ print("Could not show pairing approval. Use the approve command instead.");
7509
+ }
7510
+ }
7511
+ };
7512
+ }
7513
+ //#endregion
7434
7514
  //#region src/index.ts
7435
7515
  const port = Number(process.env.PORT ?? 8787);
7516
+ let activePort = port;
7436
7517
  const hostname$1 = process.env.HOST ?? "0.0.0.0";
7437
7518
  const dangerouslyAutoApprove = process.env.CODEX_RELAY_DANGEROUSLY_AUTO_APPROVE === "1";
7438
7519
  const serverIdentity = await getServerIdentity();
@@ -7457,6 +7538,12 @@ const color = {
7457
7538
  };
7458
7539
  const npxCommand = "npx codex-relay@latest";
7459
7540
  const sessionStore = await createTursoPairingSessionStore(process.env.CODEX_RELAY_AUTH_DB_PATH ?? await prepareCodexRelayDataPath("auth.db", ["auth.db-shm", "auth.db-wal"]));
7541
+ const terminalPairing = createTerminalPairingApprover({
7542
+ input: process.stdin,
7543
+ output: process.stdout,
7544
+ sessions: sessionStore,
7545
+ onApproved: () => logRuntimeEvent("Approved", "Pairing approved. Waiting for your phone to finish connecting.")
7546
+ });
7460
7547
  const preferencesStore = createFileRuntimePreferencesStore(process.env.CODEX_RELAY_PREFERENCES_PATH ?? await prepareCodexRelayDataPath("preferences.json"));
7461
7548
  const appServerMode = resolveCodexAppServerMode();
7462
7549
  const relayAppServer = appServerMode.mode === "socket" ? new CodexAppServerClient({
@@ -7487,8 +7574,9 @@ serve({
7487
7574
  onPairAttempt: ({ remoteAddress }) => {
7488
7575
  logRuntimeEvent("Pairing", `Handshake received${remoteAddress ? ` from ${remoteAddress}` : ""}.`);
7489
7576
  },
7490
- onPairApprovalRequested: ({ clientName }) => {
7491
- logRuntimeEvent("Approval", `Pairing approval requested${clientName ? ` from ${clientName}` : ""}. Use the code shown in the mobile app to approve locally.`);
7577
+ onPairApprovalRequested: ({ approvalCode, clientName }) => {
7578
+ logRuntimeEvent("Approval", `Pairing approval requested${clientName ? ` from ${clientName}` : ""}. Command: ${formatApprovalCommand(approvalCode, activePort)}`);
7579
+ terminalPairing.request(approvalCode);
7492
7580
  },
7493
7581
  onPairApproved: ({ clientName }) => {
7494
7582
  logRuntimeEvent("Approved", `Pairing request approved${clientName ? ` for ${clientName}` : ""}. Waiting for secure session pickup.`);
@@ -7505,6 +7593,7 @@ serve({
7505
7593
  hostname: hostname$1,
7506
7594
  port
7507
7595
  }, (info) => {
7596
+ activePort = info.port;
7508
7597
  const listenUrl = `http://${info.address}:${info.port}`;
7509
7598
  const connectUrlCandidates = getConnectUrlCandidates({
7510
7599
  listenUrl,
@@ -7546,6 +7635,18 @@ serve({
7546
7635
  port: info.port,
7547
7636
  sharedAppServerRemoteAddress: relayAppServer?.appServerMode === "socket" ? resolveCodexSharedAppServerRemoteAddress() : void 0
7548
7637
  }));
7638
+ }).on("error", (error) => {
7639
+ if (error.code !== "EADDRINUSE") throw error;
7640
+ console.error(`Codex Relay could not start: ${hostname$1}:${port} is already in use.`);
7641
+ console.error("Another Codex Relay instance or another application may be listening there.");
7642
+ console.error("If it is your existing relay, print its pairing QR with:");
7643
+ console.error(` ${npxCommand} qr`);
7644
+ console.error("To stop a background relay using this data directory:");
7645
+ console.error(` ${npxCommand} stop`);
7646
+ console.error("Otherwise, identify the listener before stopping it:");
7647
+ console.error(` lsof -nP -iTCP:${port} -sTCP:LISTEN`);
7648
+ console.error("To run a separate relay, use a free PORT and a separate CODEX_RELAY_HOME.");
7649
+ stopRelayAppServer(1);
7549
7650
  });
7550
7651
  function stopRelayAppServer(exitCode) {
7551
7652
  relayAppServer?.close();
@@ -7577,7 +7678,7 @@ function formatStartupInstructions(details) {
7577
7678
  ` ${color.command(`${npxCommand} approve <code>`)} Approve a device`,
7578
7679
  "",
7579
7680
  details.dangerouslyAutoApprove ? `${color.prompt("›")} Pairing requests will be auto-approved.` : `${color.prompt("›")} Waiting for pairing requests`,
7580
- details.dangerouslyAutoApprove ? `${color.prompt("›")} Disable this for normal use.` : `${color.prompt("›")} Approve a device with ${color.command(formatApprovalCommand("<code>", details.port))}`
7681
+ details.dangerouslyAutoApprove ? `${color.prompt("›")} Disable this for normal use.` : process.stdin.isTTY && process.stdout.isTTY ? `${color.prompt("›")} Approve a device here when prompted: type y and press Enter.` : `${color.prompt("›")} Approve a device with ${color.command(formatApprovalCommand("<code>", details.port))}`
7581
7682
  ],
7582
7683
  ""
7583
7684
  ].join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Local Codex Relay CLI bridge for the Codex Relay mobile app.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {