codex-relay 1.5.1 → 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.
- package/README.md +14 -1
- package/dist/src.js +120 -31
- 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,
|
|
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 =
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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}` : ""}.
|
|
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,
|
|
@@ -7589,7 +7678,7 @@ function formatStartupInstructions(details) {
|
|
|
7589
7678
|
` ${color.command(`${npxCommand} approve <code>`)} Approve a device`,
|
|
7590
7679
|
"",
|
|
7591
7680
|
details.dangerouslyAutoApprove ? `${color.prompt("›")} Pairing requests will be auto-approved.` : `${color.prompt("›")} Waiting for pairing requests`,
|
|
7592
|
-
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))}`
|
|
7593
7682
|
],
|
|
7594
7683
|
""
|
|
7595
7684
|
].join("\n");
|