codex-relay 1.3.1 → 1.4.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 +37 -0
- package/dist/api-schema.js +2 -2
- package/dist/api-schema2.js +76 -1
- package/dist/cli.js +4 -2
- package/dist/paths.js +78 -0
- package/dist/src.js +548 -58
- package/package.json +2 -2
- package/src/api-schema.ts +81 -0
package/README.md
CHANGED
|
@@ -28,6 +28,36 @@ 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
|
+
Opt in to Codex's shared app-server:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npx codex-relay@latest --shared-app-server
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
When a shared app-server is already running, the relay attaches to it instead of starting another one. If the relay's own socket connection resets, it reconnects without deliberately stopping the shared app-server.
|
|
42
|
+
|
|
43
|
+
Then connect a new terminal TUI to the shared app-server. On macOS, Linux, or WSL:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
codex resume --remote unix://
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
On native Windows, use the loopback WebSocket endpoint:
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
codex resume --remote ws://127.0.0.1:8788
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Pass a thread ID after the remote endpoint 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`.
|
|
56
|
+
|
|
57
|
+
Shared mode uses Codex's experimental app-server transport. A directly connected terminal TUI has its own WebSocket connection, which the relay cannot observe or reconnect. If that terminal reports a socket reset while the thread continues on mobile, reconnect it with the matching remote endpoint above and append the thread ID if needed.
|
|
58
|
+
|
|
59
|
+
Shared mode requires a recent Codex CLI with app-server and `resume --remote` support. It uses a Unix socket on macOS, Linux, and WSL, or a loopback-only WebSocket on Windows. If those features are unavailable, update Codex or omit `--shared-app-server` to keep the existing private mode.
|
|
60
|
+
|
|
31
61
|
## Background Mode
|
|
32
62
|
|
|
33
63
|
To keep the relay running after the command returns:
|
|
@@ -69,6 +99,12 @@ npx codex-relay@latest --bg
|
|
|
69
99
|
|
|
70
100
|
Start the relay in the background.
|
|
71
101
|
|
|
102
|
+
```sh
|
|
103
|
+
npx codex-relay@latest --shared-app-server
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Start the relay through Codex's shared app-server socket.
|
|
107
|
+
|
|
72
108
|
```sh
|
|
73
109
|
npx codex-relay@latest qr
|
|
74
110
|
```
|
|
@@ -99,6 +135,7 @@ The relay listens on `0.0.0.0:8787` by default. Configure it with environment va
|
|
|
99
135
|
| `CODEX_RELAY_AUTH_DB_PATH` | Pairing and session database path. Defaults to `.codex-relay/auth.db`. |
|
|
100
136
|
| `CODEX_RELAY_APPROVAL_SECRET` | Secret used by the local approve command. Usually generated automatically. |
|
|
101
137
|
| `CODEX_RELAY_DANGEROUSLY_AUTO_APPROVE` | Set to `1` to auto-approve mobile pairing requests. Prefer the CLI flag for local use. |
|
|
138
|
+
| `CODEX_RELAY_APP_SERVER_MODE` | Set to `socket` for shared terminal/mobile sessions. Defaults to `stdio`. |
|
|
102
139
|
| `CODEX_HOME` | Codex home directory, used when reading Codex session metadata. |
|
|
103
140
|
| `CODEX_BIN` | Codex CLI executable path. |
|
|
104
141
|
|
package/dist/api-schema.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
export { AgentSkillSchema, AgentSkillSourceSchema, ApprovalDecisionSchema, ApprovalModeSchema, ArchiveThreadResponseSchema, ChatMessageKindSchema, ChatMessagePromptDetailsSchema, ChatMessageRoleSchema, ChatMessageSchema, ChatMessageStateSchema, CheckoutWorkspaceBranchRequestSchema, CodexModelSchema, CommitPushWorkspaceRequestSchema, ContextWindowUsageSchema, CreateThreadRequestSchema, CreateThreadResponseSchema, EncryptedPayloadSchema, ErrorResponseSchema, ImageAttachmentUploadResponseSchema, InterruptThreadRunResponseSchema, IsoDateTimeSchema, KnownReasoningEffortSchema, ListModelsResponseSchema, ListQueuedThreadInputsResponseSchema, ListSkillsResponseSchema, ListThreadsResponseSchema, ListWorkspaceDirectoriesResponseSchema, ListWorkspaceFilesResponseSchema, PairEncryptedPayloadSchema, PairRequestSchema, PairResponseSchema, PendingInputRequestOptionSchema, PendingInputRequestQuestionSchema, PendingInputRequestSchema, PromptAttachmentSchema, PromptAttachmentSummarySchema, PromptContextInputSchema, PromptContextSchema, PromptSkillSchema, QueuedThreadInputActionResponseSchema, QueuedThreadInputSchema, RateLimitBucketSchema, RateLimitWindowSchema, RateLimitsResponseSchema, ReasoningEffortSchema, ResolveApprovalRequestSchema, ResolveApprovalResponseSchema, RunThreadRequestSchema, RunThreadResponseSchema, RuntimeModeSchema, RuntimePreferencesByWorkspacePathSchema, RuntimePreferencesResponseSchema, RuntimePreferencesSchema, SandboxModeSchema, StatusResponseSchema, StreamThreadRunEventSchema, StreamThreadRunRequestSchema, SubmitThreadInputResponseSchema, ThreadCollaborationModeSchema, ThreadContextWindowResponseSchema, ThreadDetailResponseSchema, ThreadGoalResponseSchema, ThreadGoalSchema, ThreadGoalStatusSchema, ThreadMessageDetailFieldSchema, ThreadMessageDetailResponseSchema, ThreadRunOptionsSchema, ThreadStateSchema, ThreadSummarySchema, UpdateRuntimePreferencesRequestSchema, UpdateThreadGoalRequestSchema, UpdateWorkspaceFileContentRequestSchema, VersionResponseSchema, WORKSPACE_PREVIEW_OPEN_PROTOCOL, WORKSPACE_PREVIEW_TAB_VALUES, WebPreviewTargetSchema, WorkspaceChangesResponseSchema, WorkspaceDirectoryEntrySchema, WorkspaceFileContentResponseSchema, WorkspaceFileMentionSchema, WorkspaceGitActionResponseSchema, WorkspaceMarkdownPreviewTargetSchema, WorkspacePreviewNavigationRequestSchema, WorkspacePreviewTabSchema, WorkspaceSelectionRequestSchema, WorkspaceTailscaleServeRequestSchema, WorkspaceTailscaleServeResponseSchema, WorkspaceTerminalInputRequestSchema, WorkspaceTerminalOutputChunkSchema, WorkspaceTerminalOutputResponseSchema, WorkspaceTerminalResizeRequestSchema, WorkspaceTerminalSessionResponseSchema, WorkspaceTerminalStartRequestSchema, apiPaths, chatMessageDetailsFromPromptContext, createOpenApiDocument, isPromptSkillMarkdownMention, normalizePromptContext, promptMarkdownWithSkills, promptSkillDisplayName, promptSkillMentionLabel, promptSkillMentionMarkdown, promptSkillMentionTextCandidates, stripPromptSkillMentions };
|
|
1
|
+
import { $ as RunThreadResponseSchema, $t as promptSkillMentionTextCandidates, A as PairRequestSchema, At as WorkspaceFileMentionSchema, B as PushNotificationIntentSchema, Bt as WorkspaceTerminalOutputResponseSchema, C as ListModelsResponseSchema, Ct as VersionResponseSchema, D as ListWorkspaceDirectoriesResponseSchema, Dt as WorkspaceChangesResponseSchema, E as ListThreadsResponseSchema, Et as WebPreviewTargetSchema, F as PromptAttachmentSchema, Ft as WorkspaceSelectionRequestSchema, G as RateLimitBucketSchema, Gt as chatMessageDetailsFromPromptContext, H as PushNotificationSettingsResponseSchema, Ht as WorkspaceTerminalSessionResponseSchema, I as PromptAttachmentSummarySchema, It as WorkspaceTailscaleServeRequestSchema, J as ReasoningEffortSchema, Jt as normalizePromptContext, K as RateLimitWindowSchema, Kt as createOpenApiDocument, L as PromptContextInputSchema, Lt as WorkspaceTailscaleServeResponseSchema, M as PendingInputRequestOptionSchema, Mt as WorkspaceMarkdownPreviewTargetSchema, N as PendingInputRequestQuestionSchema, Nt as WorkspacePreviewNavigationRequestSchema, O as ListWorkspaceFilesResponseSchema, Ot as WorkspaceDirectoryEntrySchema, P as PendingInputRequestSchema, Pt as WorkspacePreviewTabSchema, Q as RunThreadRequestSchema, Qt as promptSkillMentionMarkdown, R as PromptContextSchema, Rt as WorkspaceTerminalInputRequestSchema, S as KnownReasoningEffortSchema, St as UpdateWorkspaceFileContentRequestSchema, T as ListSkillsResponseSchema, Tt as WORKSPACE_PREVIEW_TAB_VALUES, U as QueuedThreadInputActionResponseSchema, Ut as WorkspaceTerminalStartRequestSchema, V as PushNotificationPreferencesSchema, Vt as WorkspaceTerminalResizeRequestSchema, W as QueuedThreadInputSchema, Wt as apiPaths, X as ResolveApprovalRequestSchema, Xt as promptSkillDisplayName, Y as RegisterPushNotificationRequestSchema, Yt as promptMarkdownWithSkills, Z as ResolveApprovalResponseSchema, Zt as promptSkillMentionLabel, _ as EncryptedPayloadSchema, _t as ThreadRunOptionsSchema, a as ArchiveThreadResponseSchema, at as StatusResponseSchema, b as InterruptThreadRunResponseSchema, bt as UpdateRuntimePreferencesRequestSchema, c as ChatMessageRoleSchema, ct as SubmitThreadInputResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadDetailResponseSchema, en as stripPromptSkillMentions, et as RuntimeModeSchema, f as CodexModelSchema, ft as ThreadGoalResponseSchema, g as CreateThreadResponseSchema, gt as ThreadMessageDetailResponseSchema, h as CreateThreadRequestSchema, ht as ThreadMessageDetailFieldSchema, i as ApprovalModeSchema, it as SandboxModeSchema, j as PairResponseSchema, jt as WorkspaceGitActionResponseSchema, k as PairEncryptedPayloadSchema, kt as WorkspaceFileContentResponseSchema, l as ChatMessageSchema, lt as ThreadCollaborationModeSchema, m as ContextWindowUsageSchema, mt as ThreadGoalStatusSchema, n as AgentSkillSourceSchema, nt as RuntimePreferencesResponseSchema, o as ChatMessageKindSchema, ot as StreamThreadRunEventSchema, p as CommitPushWorkspaceRequestSchema, pt as ThreadGoalSchema, q as RateLimitsResponseSchema, qt as isPromptSkillMarkdownMention, r as ApprovalDecisionSchema, rt as RuntimePreferencesSchema, s as ChatMessagePromptDetailsSchema, st as StreamThreadRunRequestSchema, t as AgentSkillSchema, tt as RuntimePreferencesByWorkspacePathSchema, u as ChatMessageStateSchema, ut as ThreadContextWindowResponseSchema, v as ErrorResponseSchema, vt as ThreadStateSchema, w as ListQueuedThreadInputsResponseSchema, wt as WORKSPACE_PREVIEW_OPEN_PROTOCOL, x as IsoDateTimeSchema, xt as UpdateThreadGoalRequestSchema, y as ImageAttachmentUploadResponseSchema, yt as ThreadSummarySchema, z as PromptSkillSchema, zt as WorkspaceTerminalOutputChunkSchema } from "./api-schema2.js";
|
|
2
|
+
export { AgentSkillSchema, AgentSkillSourceSchema, ApprovalDecisionSchema, ApprovalModeSchema, ArchiveThreadResponseSchema, ChatMessageKindSchema, ChatMessagePromptDetailsSchema, ChatMessageRoleSchema, ChatMessageSchema, ChatMessageStateSchema, CheckoutWorkspaceBranchRequestSchema, CodexModelSchema, CommitPushWorkspaceRequestSchema, ContextWindowUsageSchema, CreateThreadRequestSchema, CreateThreadResponseSchema, EncryptedPayloadSchema, ErrorResponseSchema, ImageAttachmentUploadResponseSchema, InterruptThreadRunResponseSchema, IsoDateTimeSchema, KnownReasoningEffortSchema, ListModelsResponseSchema, ListQueuedThreadInputsResponseSchema, ListSkillsResponseSchema, ListThreadsResponseSchema, ListWorkspaceDirectoriesResponseSchema, ListWorkspaceFilesResponseSchema, PairEncryptedPayloadSchema, PairRequestSchema, PairResponseSchema, PendingInputRequestOptionSchema, PendingInputRequestQuestionSchema, PendingInputRequestSchema, PromptAttachmentSchema, PromptAttachmentSummarySchema, PromptContextInputSchema, PromptContextSchema, PromptSkillSchema, PushNotificationIntentSchema, PushNotificationPreferencesSchema, PushNotificationSettingsResponseSchema, QueuedThreadInputActionResponseSchema, QueuedThreadInputSchema, RateLimitBucketSchema, RateLimitWindowSchema, RateLimitsResponseSchema, ReasoningEffortSchema, RegisterPushNotificationRequestSchema, ResolveApprovalRequestSchema, ResolveApprovalResponseSchema, RunThreadRequestSchema, RunThreadResponseSchema, RuntimeModeSchema, RuntimePreferencesByWorkspacePathSchema, RuntimePreferencesResponseSchema, RuntimePreferencesSchema, SandboxModeSchema, StatusResponseSchema, StreamThreadRunEventSchema, StreamThreadRunRequestSchema, SubmitThreadInputResponseSchema, ThreadCollaborationModeSchema, ThreadContextWindowResponseSchema, ThreadDetailResponseSchema, ThreadGoalResponseSchema, ThreadGoalSchema, ThreadGoalStatusSchema, ThreadMessageDetailFieldSchema, ThreadMessageDetailResponseSchema, ThreadRunOptionsSchema, ThreadStateSchema, ThreadSummarySchema, UpdateRuntimePreferencesRequestSchema, UpdateThreadGoalRequestSchema, UpdateWorkspaceFileContentRequestSchema, VersionResponseSchema, WORKSPACE_PREVIEW_OPEN_PROTOCOL, WORKSPACE_PREVIEW_TAB_VALUES, WebPreviewTargetSchema, WorkspaceChangesResponseSchema, WorkspaceDirectoryEntrySchema, WorkspaceFileContentResponseSchema, WorkspaceFileMentionSchema, WorkspaceGitActionResponseSchema, WorkspaceMarkdownPreviewTargetSchema, WorkspacePreviewNavigationRequestSchema, WorkspacePreviewTabSchema, WorkspaceSelectionRequestSchema, WorkspaceTailscaleServeRequestSchema, WorkspaceTailscaleServeResponseSchema, WorkspaceTerminalInputRequestSchema, WorkspaceTerminalOutputChunkSchema, WorkspaceTerminalOutputResponseSchema, WorkspaceTerminalResizeRequestSchema, WorkspaceTerminalSessionResponseSchema, WorkspaceTerminalStartRequestSchema, apiPaths, chatMessageDetailsFromPromptContext, createOpenApiDocument, isPromptSkillMarkdownMention, normalizePromptContext, promptMarkdownWithSkills, promptSkillDisplayName, promptSkillMentionLabel, promptSkillMentionMarkdown, promptSkillMentionTextCandidates, stripPromptSkillMentions };
|
package/dist/api-schema2.js
CHANGED
|
@@ -104,6 +104,20 @@ const RuntimePreferencesResponseSchema = z.object({
|
|
|
104
104
|
threadId: z.string().trim().min(1).optional(),
|
|
105
105
|
workspacePath: z.string().trim().min(1).optional()
|
|
106
106
|
});
|
|
107
|
+
const PushNotificationIntentSchema = z.enum(["turn_terminal", "action_required"]);
|
|
108
|
+
const PushNotificationPreferencesSchema = z.object({
|
|
109
|
+
actionRequired: z.boolean(),
|
|
110
|
+
turnTerminal: z.boolean()
|
|
111
|
+
});
|
|
112
|
+
const RegisterPushNotificationRequestSchema = z.object({
|
|
113
|
+
expoPushToken: z.string().trim().regex(/^(?:Expo|Exponent)PushToken\[[^\]\r\n]{1,512}\]$/),
|
|
114
|
+
platform: z.enum(["android", "ios"]),
|
|
115
|
+
preferences: PushNotificationPreferencesSchema
|
|
116
|
+
});
|
|
117
|
+
const PushNotificationSettingsResponseSchema = z.object({
|
|
118
|
+
preferences: PushNotificationPreferencesSchema,
|
|
119
|
+
registered: z.boolean()
|
|
120
|
+
});
|
|
107
121
|
const CodexModelSchema = z.object({
|
|
108
122
|
id: z.string().min(1),
|
|
109
123
|
model: z.string().min(1),
|
|
@@ -721,6 +735,7 @@ const apiPaths = {
|
|
|
721
735
|
sessionRefresh: "/v1/session/refresh",
|
|
722
736
|
status: "/v1/status",
|
|
723
737
|
preferences: "/v1/preferences",
|
|
738
|
+
pushNotifications: "/v1/notifications/push",
|
|
724
739
|
rateLimits: "/v1/rate-limits",
|
|
725
740
|
models: "/v1/models",
|
|
726
741
|
skills: "/v1/skills",
|
|
@@ -777,6 +792,31 @@ function createOpenApiDocument() {
|
|
|
777
792
|
"400": jsonResponse("ErrorResponse")
|
|
778
793
|
}
|
|
779
794
|
} },
|
|
795
|
+
"/v1/notifications/push": {
|
|
796
|
+
get: {
|
|
797
|
+
summary: "Read push notification settings for this paired device",
|
|
798
|
+
responses: {
|
|
799
|
+
"200": jsonResponse("PushNotificationSettingsResponse"),
|
|
800
|
+
"401": jsonResponse("ErrorResponse")
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
put: {
|
|
804
|
+
summary: "Register push notifications for this paired device",
|
|
805
|
+
requestBody: jsonRequest("RegisterPushNotificationRequest"),
|
|
806
|
+
responses: {
|
|
807
|
+
"200": jsonResponse("PushNotificationSettingsResponse"),
|
|
808
|
+
"400": jsonResponse("ErrorResponse"),
|
|
809
|
+
"401": jsonResponse("ErrorResponse")
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
delete: {
|
|
813
|
+
summary: "Remove push notifications for this paired device",
|
|
814
|
+
responses: {
|
|
815
|
+
"200": jsonResponse("PushNotificationSettingsResponse"),
|
|
816
|
+
"401": jsonResponse("ErrorResponse")
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
},
|
|
780
820
|
"/v1/threads": {
|
|
781
821
|
get: {
|
|
782
822
|
summary: "List locally known threads",
|
|
@@ -1243,6 +1283,41 @@ function createOpenApiDocument() {
|
|
|
1243
1283
|
workspacePath: { type: "string" }
|
|
1244
1284
|
}
|
|
1245
1285
|
},
|
|
1286
|
+
PushNotificationPreferences: {
|
|
1287
|
+
type: "object",
|
|
1288
|
+
required: ["actionRequired", "turnTerminal"],
|
|
1289
|
+
properties: {
|
|
1290
|
+
actionRequired: { type: "boolean" },
|
|
1291
|
+
turnTerminal: { type: "boolean" }
|
|
1292
|
+
}
|
|
1293
|
+
},
|
|
1294
|
+
RegisterPushNotificationRequest: {
|
|
1295
|
+
type: "object",
|
|
1296
|
+
required: [
|
|
1297
|
+
"expoPushToken",
|
|
1298
|
+
"platform",
|
|
1299
|
+
"preferences"
|
|
1300
|
+
],
|
|
1301
|
+
properties: {
|
|
1302
|
+
expoPushToken: {
|
|
1303
|
+
type: "string",
|
|
1304
|
+
pattern: "^(?:Expo|Exponent)PushToken\\[[^\\]\\r\\n]{1,512}\\]$"
|
|
1305
|
+
},
|
|
1306
|
+
platform: {
|
|
1307
|
+
type: "string",
|
|
1308
|
+
enum: ["android", "ios"]
|
|
1309
|
+
},
|
|
1310
|
+
preferences: { $ref: "#/components/schemas/PushNotificationPreferences" }
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
PushNotificationSettingsResponse: {
|
|
1314
|
+
type: "object",
|
|
1315
|
+
required: ["preferences", "registered"],
|
|
1316
|
+
properties: {
|
|
1317
|
+
preferences: { $ref: "#/components/schemas/PushNotificationPreferences" },
|
|
1318
|
+
registered: { type: "boolean" }
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1246
1321
|
WorkspaceDirectoryEntry: {
|
|
1247
1322
|
type: "object",
|
|
1248
1323
|
required: ["name", "path"],
|
|
@@ -1725,4 +1800,4 @@ function jsonResponse(schemaName) {
|
|
|
1725
1800
|
};
|
|
1726
1801
|
}
|
|
1727
1802
|
//#endregion
|
|
1728
|
-
export {
|
|
1803
|
+
export { RunThreadResponseSchema as $, promptSkillMentionTextCandidates as $t, PairRequestSchema as A, WorkspaceFileMentionSchema as At, PushNotificationIntentSchema as B, WorkspaceTerminalOutputResponseSchema as Bt, ListModelsResponseSchema as C, VersionResponseSchema as Ct, ListWorkspaceDirectoriesResponseSchema as D, WorkspaceChangesResponseSchema as Dt, ListThreadsResponseSchema as E, WebPreviewTargetSchema as Et, PromptAttachmentSchema as F, WorkspaceSelectionRequestSchema as Ft, RateLimitBucketSchema as G, chatMessageDetailsFromPromptContext as Gt, PushNotificationSettingsResponseSchema as H, WorkspaceTerminalSessionResponseSchema as Ht, PromptAttachmentSummarySchema as I, WorkspaceTailscaleServeRequestSchema as It, ReasoningEffortSchema as J, normalizePromptContext as Jt, RateLimitWindowSchema as K, createOpenApiDocument as Kt, PromptContextInputSchema as L, WorkspaceTailscaleServeResponseSchema as Lt, PendingInputRequestOptionSchema as M, WorkspaceMarkdownPreviewTargetSchema as Mt, PendingInputRequestQuestionSchema as N, WorkspacePreviewNavigationRequestSchema as Nt, ListWorkspaceFilesResponseSchema as O, WorkspaceDirectoryEntrySchema as Ot, PendingInputRequestSchema as P, WorkspacePreviewTabSchema as Pt, RunThreadRequestSchema as Q, promptSkillMentionMarkdown as Qt, PromptContextSchema as R, WorkspaceTerminalInputRequestSchema as Rt, KnownReasoningEffortSchema as S, UpdateWorkspaceFileContentRequestSchema as St, ListSkillsResponseSchema as T, WORKSPACE_PREVIEW_TAB_VALUES as Tt, QueuedThreadInputActionResponseSchema as U, WorkspaceTerminalStartRequestSchema as Ut, PushNotificationPreferencesSchema as V, WorkspaceTerminalResizeRequestSchema as Vt, QueuedThreadInputSchema as W, apiPaths as Wt, ResolveApprovalRequestSchema as X, promptSkillDisplayName as Xt, RegisterPushNotificationRequestSchema as Y, promptMarkdownWithSkills as Yt, ResolveApprovalResponseSchema as Z, promptSkillMentionLabel as Zt, EncryptedPayloadSchema as _, ThreadRunOptionsSchema as _t, ArchiveThreadResponseSchema as a, StatusResponseSchema as at, InterruptThreadRunResponseSchema as b, UpdateRuntimePreferencesRequestSchema as bt, ChatMessageRoleSchema as c, SubmitThreadInputResponseSchema as ct, CheckoutWorkspaceBranchRequestSchema as d, ThreadDetailResponseSchema as dt, stripPromptSkillMentions as en, RuntimeModeSchema as et, CodexModelSchema as f, ThreadGoalResponseSchema as ft, CreateThreadResponseSchema as g, ThreadMessageDetailResponseSchema as gt, CreateThreadRequestSchema as h, ThreadMessageDetailFieldSchema as ht, ApprovalModeSchema as i, SandboxModeSchema as it, PairResponseSchema as j, WorkspaceGitActionResponseSchema as jt, PairEncryptedPayloadSchema as k, WorkspaceFileContentResponseSchema as kt, ChatMessageSchema as l, ThreadCollaborationModeSchema as lt, ContextWindowUsageSchema as m, ThreadGoalStatusSchema as mt, AgentSkillSourceSchema as n, RuntimePreferencesResponseSchema as nt, ChatMessageKindSchema as o, StreamThreadRunEventSchema as ot, CommitPushWorkspaceRequestSchema as p, ThreadGoalSchema as pt, RateLimitsResponseSchema as q, isPromptSkillMarkdownMention as qt, ApprovalDecisionSchema as r, RuntimePreferencesSchema as rt, ChatMessagePromptDetailsSchema as s, StreamThreadRunRequestSchema as st, AgentSkillSchema as t, RuntimePreferencesByWorkspacePathSchema as tt, ChatMessageStateSchema as u, ThreadContextWindowResponseSchema as ut, ErrorResponseSchema as v, ThreadStateSchema as vt, ListQueuedThreadInputsResponseSchema as w, WORKSPACE_PREVIEW_OPEN_PROTOCOL as wt, IsoDateTimeSchema as x, UpdateThreadGoalRequestSchema as xt, ImageAttachmentUploadResponseSchema as y, ThreadSummarySchema as yt, PromptSkillSchema as z, WorkspaceTerminalOutputChunkSchema as zt };
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { Wt as apiPaths } from "./api-schema2.js";
|
|
3
3
|
import { n as codexRelayHome, o as getConnectUrlGuidance, r as legacyCodexRelayDataPath, s as createTursoPairingSessionStore, t as codexRelayDataPath } from "./paths.js";
|
|
4
4
|
import { Command } from "@commander-js/extra-typings";
|
|
5
5
|
import qrcode from "qrcode-terminal";
|
|
@@ -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/paths.js
CHANGED
|
@@ -122,6 +122,16 @@ async function createTursoPairingSessionStore(path) {
|
|
|
122
122
|
created_at INTEGER NOT NULL,
|
|
123
123
|
updated_at INTEGER NOT NULL
|
|
124
124
|
);
|
|
125
|
+
|
|
126
|
+
CREATE TABLE IF NOT EXISTS push_notification_subscriptions (
|
|
127
|
+
client_session_id TEXT PRIMARY KEY,
|
|
128
|
+
expo_push_token TEXT NOT NULL,
|
|
129
|
+
platform TEXT NOT NULL,
|
|
130
|
+
turn_terminal_enabled INTEGER NOT NULL,
|
|
131
|
+
action_required_enabled INTEGER NOT NULL,
|
|
132
|
+
created_at INTEGER NOT NULL,
|
|
133
|
+
updated_at INTEGER NOT NULL
|
|
134
|
+
);
|
|
125
135
|
`);
|
|
126
136
|
await ensurePairingSessionColumns();
|
|
127
137
|
async function countActive(now) {
|
|
@@ -178,6 +188,7 @@ async function createTursoPairingSessionStore(path) {
|
|
|
178
188
|
const pendingRow = await transaction.prepare("SELECT COUNT(*) AS count FROM pending_pairings").get();
|
|
179
189
|
await transaction.prepare("DELETE FROM pairing_sessions").run();
|
|
180
190
|
await transaction.prepare("DELETE FROM pending_pairings").run();
|
|
191
|
+
await transaction.prepare("DELETE FROM push_notification_subscriptions").run();
|
|
181
192
|
return {
|
|
182
193
|
pendingPairingsCleared: Number(pendingRow?.count ?? 0),
|
|
183
194
|
sessionsCleared: Number(sessionRow?.count ?? 0)
|
|
@@ -185,6 +196,12 @@ async function createTursoPairingSessionStore(path) {
|
|
|
185
196
|
})();
|
|
186
197
|
},
|
|
187
198
|
countActive,
|
|
199
|
+
async deletePushNotificationSubscription(clientSessionId) {
|
|
200
|
+
await db.prepare("DELETE FROM push_notification_subscriptions WHERE client_session_id = ?").run(clientSessionId);
|
|
201
|
+
},
|
|
202
|
+
async deletePushNotificationSubscriptionsByExpoPushToken(expoPushToken) {
|
|
203
|
+
await db.prepare("DELETE FROM push_notification_subscriptions WHERE expo_push_token = ?").run(expoPushToken);
|
|
204
|
+
},
|
|
188
205
|
async createPendingPairing(pairing) {
|
|
189
206
|
const now = Date.now();
|
|
190
207
|
await db.prepare(`INSERT INTO pending_pairings (
|
|
@@ -227,6 +244,16 @@ async function createTursoPairingSessionStore(path) {
|
|
|
227
244
|
deleteSession,
|
|
228
245
|
deletePendingPairing,
|
|
229
246
|
getPendingPairing,
|
|
247
|
+
async getPushNotificationSubscription(clientSessionId) {
|
|
248
|
+
const row = await db.prepare(`SELECT client_session_id AS clientSessionId,
|
|
249
|
+
expo_push_token AS expoPushToken,
|
|
250
|
+
platform,
|
|
251
|
+
turn_terminal_enabled AS turnTerminal,
|
|
252
|
+
action_required_enabled AS actionRequired
|
|
253
|
+
FROM push_notification_subscriptions
|
|
254
|
+
WHERE client_session_id = ?`).get(clientSessionId);
|
|
255
|
+
return row ? pushNotificationSubscriptionFromRow(row) : void 0;
|
|
256
|
+
},
|
|
230
257
|
async getValidSession(tokenHash, now) {
|
|
231
258
|
const row = await db.prepare(`SELECT client_name AS clientName,
|
|
232
259
|
client_session_id AS clientSessionId,
|
|
@@ -251,9 +278,30 @@ async function createTursoPairingSessionStore(path) {
|
|
|
251
278
|
secureSession: decodeSecureSession(row)
|
|
252
279
|
};
|
|
253
280
|
},
|
|
281
|
+
async listActivePushNotificationSubscriptions(now) {
|
|
282
|
+
return resultRows(await db.prepare(`SELECT subscriptions.client_session_id AS clientSessionId,
|
|
283
|
+
subscriptions.expo_push_token AS expoPushToken,
|
|
284
|
+
subscriptions.platform,
|
|
285
|
+
subscriptions.turn_terminal_enabled AS turnTerminal,
|
|
286
|
+
subscriptions.action_required_enabled AS actionRequired
|
|
287
|
+
FROM push_notification_subscriptions AS subscriptions
|
|
288
|
+
INNER JOIN pairing_sessions AS sessions
|
|
289
|
+
ON sessions.client_session_id = subscriptions.client_session_id
|
|
290
|
+
WHERE sessions.expires_at > ?
|
|
291
|
+
GROUP BY subscriptions.client_session_id`).all(now)).flatMap((row) => {
|
|
292
|
+
const subscription = pushNotificationSubscriptionFromRow(row);
|
|
293
|
+
return subscription ? [subscription] : [];
|
|
294
|
+
});
|
|
295
|
+
},
|
|
254
296
|
async pruneExpired(now) {
|
|
255
297
|
await db.prepare("DELETE FROM pairing_sessions WHERE expires_at <= ?").run(now);
|
|
256
298
|
await db.prepare("DELETE FROM pending_pairings WHERE expires_at <= ?").run(now);
|
|
299
|
+
await db.prepare(`DELETE FROM push_notification_subscriptions
|
|
300
|
+
WHERE NOT EXISTS (
|
|
301
|
+
SELECT 1
|
|
302
|
+
FROM pairing_sessions
|
|
303
|
+
WHERE pairing_sessions.client_session_id = push_notification_subscriptions.client_session_id
|
|
304
|
+
)`).run();
|
|
257
305
|
},
|
|
258
306
|
async rotateSession(oldTokenHash, newTokenHash, session) {
|
|
259
307
|
const now = Date.now();
|
|
@@ -292,6 +340,25 @@ async function createTursoPairingSessionStore(path) {
|
|
|
292
340
|
next_server_counter = ?,
|
|
293
341
|
updated_at = ?
|
|
294
342
|
WHERE token_hash = ?`).run(secure.keyEpoch, secure.mobileToServerKey, secure.serverToMobileKey, secure.lastMobileCounter, secure.nextServerCounter, now, tokenHash);
|
|
343
|
+
},
|
|
344
|
+
async upsertPushNotificationSubscription(subscription) {
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
await db.prepare(`INSERT INTO push_notification_subscriptions (
|
|
347
|
+
client_session_id,
|
|
348
|
+
expo_push_token,
|
|
349
|
+
platform,
|
|
350
|
+
turn_terminal_enabled,
|
|
351
|
+
action_required_enabled,
|
|
352
|
+
created_at,
|
|
353
|
+
updated_at
|
|
354
|
+
)
|
|
355
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
356
|
+
ON CONFLICT(client_session_id) DO UPDATE SET
|
|
357
|
+
expo_push_token = excluded.expo_push_token,
|
|
358
|
+
platform = excluded.platform,
|
|
359
|
+
turn_terminal_enabled = excluded.turn_terminal_enabled,
|
|
360
|
+
action_required_enabled = excluded.action_required_enabled,
|
|
361
|
+
updated_at = excluded.updated_at`).run(subscription.clientSessionId, subscription.expoPushToken, subscription.platform, subscription.turnTerminal ? 1 : 0, subscription.actionRequired ? 1 : 0, now, now);
|
|
295
362
|
}
|
|
296
363
|
};
|
|
297
364
|
async function ensurePairingSessionColumns() {
|
|
@@ -329,6 +396,17 @@ function decodeSecureSession(row) {
|
|
|
329
396
|
serverToMobileKey: toByteArray(row.serverToMobileKey)
|
|
330
397
|
};
|
|
331
398
|
}
|
|
399
|
+
function pushNotificationSubscriptionFromRow(row) {
|
|
400
|
+
const platform = row.platform;
|
|
401
|
+
if (typeof row.clientSessionId !== "string" || typeof row.expoPushToken !== "string" || platform !== "android" && platform !== "ios") return;
|
|
402
|
+
return {
|
|
403
|
+
actionRequired: Number(row.actionRequired) === 1,
|
|
404
|
+
clientSessionId: row.clientSessionId,
|
|
405
|
+
expoPushToken: row.expoPushToken,
|
|
406
|
+
platform,
|
|
407
|
+
turnTerminal: Number(row.turnTerminal) === 1
|
|
408
|
+
};
|
|
409
|
+
}
|
|
332
410
|
function resultRows(result) {
|
|
333
411
|
if (Array.isArray(result)) return result;
|
|
334
412
|
if (result && typeof result === "object" && Array.isArray(result.rows)) return result.rows;
|
package/dist/src.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as PairRequestSchema, Bt as WorkspaceTerminalOutputResponseSchema, C as ListModelsResponseSchema, Ct as VersionResponseSchema, D as ListWorkspaceDirectoriesResponseSchema, Dt as WorkspaceChangesResponseSchema, E as ListThreadsResponseSchema, Gt as chatMessageDetailsFromPromptContext, H as PushNotificationSettingsResponseSchema, Ht as WorkspaceTerminalSessionResponseSchema, It as WorkspaceTailscaleServeRequestSchema, J as ReasoningEffortSchema, Jt as normalizePromptContext, Kt as createOpenApiDocument, Lt as WorkspaceTailscaleServeResponseSchema, O as ListWorkspaceFilesResponseSchema, Q as RunThreadRequestSchema, Rt as WorkspaceTerminalInputRequestSchema, S as KnownReasoningEffortSchema, St as UpdateWorkspaceFileContentRequestSchema, T as ListSkillsResponseSchema, U as QueuedThreadInputActionResponseSchema, Ut as WorkspaceTerminalStartRequestSchema, Vt as WorkspaceTerminalResizeRequestSchema, Wt as apiPaths, X as ResolveApprovalRequestSchema, Y as RegisterPushNotificationRequestSchema, Yt as promptMarkdownWithSkills, Z as ResolveApprovalResponseSchema, _ as EncryptedPayloadSchema, a as ArchiveThreadResponseSchema, at as StatusResponseSchema, b as InterruptThreadRunResponseSchema, bt as UpdateRuntimePreferencesRequestSchema, ct as SubmitThreadInputResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadDetailResponseSchema, en as stripPromptSkillMentions, ft as ThreadGoalResponseSchema, gt as ThreadMessageDetailResponseSchema, h as CreateThreadRequestSchema, ht as ThreadMessageDetailFieldSchema, j as PairResponseSchema, jt as WorkspaceGitActionResponseSchema, kt as WorkspaceFileContentResponseSchema, l as ChatMessageSchema, m as ContextWindowUsageSchema, mt as ThreadGoalStatusSchema, nt as RuntimePreferencesResponseSchema, ot as StreamThreadRunEventSchema, p as CommitPushWorkspaceRequestSchema, pt as ThreadGoalSchema, q as RateLimitsResponseSchema, rt as RuntimePreferencesSchema, st as StreamThreadRunRequestSchema, ut as ThreadContextWindowResponseSchema, w as ListQueuedThreadInputsResponseSchema, xt as UpdateThreadGoalRequestSchema, y as ImageAttachmentUploadResponseSchema, yt as ThreadSummarySchema } from "./api-schema2.js";
|
|
2
2
|
import { a as getConnectUrlCandidates, i as createPairingQrPayload, o as getConnectUrlGuidance, r as legacyCodexRelayDataPath, s as createTursoPairingSessionStore, t as codexRelayDataPath } from "./paths.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import qrcode from "qrcode-terminal";
|
|
@@ -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";
|
|
@@ -18,7 +19,9 @@ import { openRepository } from "es-git";
|
|
|
18
19
|
import { Hono } from "hono";
|
|
19
20
|
import { cors } from "hono/cors";
|
|
20
21
|
import * as pty from "@lydell/node-pty";
|
|
22
|
+
import { connect } from "node:net";
|
|
21
23
|
import { createInterface } from "node:readline";
|
|
24
|
+
import WebSocket from "ws";
|
|
22
25
|
import { Codex } from "@openai/codex-sdk";
|
|
23
26
|
import { gcm } from "@noble/ciphers/aes.js";
|
|
24
27
|
import { randomBytes as randomBytes$1, utf8ToBytes } from "@noble/ciphers/utils.js";
|
|
@@ -26,23 +29,48 @@ import { ed25519, x25519 } from "@noble/curves/ed25519.js";
|
|
|
26
29
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
27
30
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
28
31
|
//#region src/codex-binary.ts
|
|
29
|
-
const
|
|
32
|
+
const stdioAppServerArgs = [
|
|
30
33
|
"app-server",
|
|
31
34
|
"--listen",
|
|
32
35
|
"stdio://"
|
|
33
36
|
];
|
|
37
|
+
const sharedWindowsServerUrl = "ws://127.0.0.1:8788";
|
|
34
38
|
const windowsShellExtensions = new Set([".bat", ".cmd"]);
|
|
39
|
+
function resolveCodexAppServerMode(env = process.env) {
|
|
40
|
+
const configuredMode = env.CODEX_RELAY_APP_SERVER_MODE?.trim().toLowerCase();
|
|
41
|
+
if (!configuredMode || configuredMode === "stdio") return "stdio";
|
|
42
|
+
if (configuredMode === "socket") return "socket";
|
|
43
|
+
throw new Error(`Unsupported CODEX_RELAY_APP_SERVER_MODE ${JSON.stringify(configuredMode)}. Expected "stdio" or "socket".`);
|
|
44
|
+
}
|
|
35
45
|
function resolveCodexAppServerSpawn(input = {}) {
|
|
36
|
-
const platform$
|
|
46
|
+
const platform$2 = input.platform ?? platform();
|
|
47
|
+
const command = resolveCodexBinary(input.env ?? process.env);
|
|
48
|
+
const isWindows = platform$2 === "win32";
|
|
49
|
+
return {
|
|
50
|
+
command,
|
|
51
|
+
args: [...stdioAppServerArgs],
|
|
52
|
+
shell: isWindows && shouldUseWindowsShell(command),
|
|
53
|
+
windowsHide: isWindows
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function resolveCodexSharedAppServerSpawn(input = {}) {
|
|
57
|
+
const platform$3 = input.platform ?? platform();
|
|
37
58
|
const command = resolveCodexBinary(input.env ?? process.env);
|
|
38
|
-
const isWindows = platform$
|
|
59
|
+
const isWindows = platform$3 === "win32";
|
|
39
60
|
return {
|
|
40
61
|
command,
|
|
41
|
-
args: [
|
|
62
|
+
args: [
|
|
63
|
+
"app-server",
|
|
64
|
+
"--listen",
|
|
65
|
+
resolveCodexSharedAppServerRemoteAddress(platform$3)
|
|
66
|
+
],
|
|
42
67
|
shell: isWindows && shouldUseWindowsShell(command),
|
|
43
68
|
windowsHide: isWindows
|
|
44
69
|
};
|
|
45
70
|
}
|
|
71
|
+
function resolveCodexSharedAppServerRemoteAddress(platform$1 = platform()) {
|
|
72
|
+
return platform$1 === "win32" ? sharedWindowsServerUrl : "unix://";
|
|
73
|
+
}
|
|
46
74
|
function resolveCodexBinary(env) {
|
|
47
75
|
return env.CODEX_BIN?.trim() || "codex";
|
|
48
76
|
}
|
|
@@ -51,15 +79,71 @@ function shouldUseWindowsShell(command) {
|
|
|
51
79
|
return extension === "" || windowsShellExtensions.has(extension);
|
|
52
80
|
}
|
|
53
81
|
//#endregion
|
|
82
|
+
//#region src/debug-log.ts
|
|
83
|
+
function isRelayDebugEnabled() {
|
|
84
|
+
return process.env.CODEX_RELAY_DEBUG === "1" || process.env.CODEX_RELAY_DEBUG_STREAM === "1";
|
|
85
|
+
}
|
|
86
|
+
function relayDebugLog(event, fields = {}) {
|
|
87
|
+
if (!isRelayDebugEnabled()) return;
|
|
88
|
+
const entry = sanitizeDebugValue({
|
|
89
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
90
|
+
event,
|
|
91
|
+
...fields
|
|
92
|
+
});
|
|
93
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
94
|
+
const path = process.env.CODEX_RELAY_DEBUG_LOG_PATH?.trim();
|
|
95
|
+
if (path) mkdir(dirname(path), { recursive: true }).then(() => appendFile(path, line, { mode: 384 })).catch(() => void 0);
|
|
96
|
+
console.log(`[debug] ${event} ${formatDebugFields(fields)}`.trimEnd());
|
|
97
|
+
}
|
|
98
|
+
function sanitizeDebugValue(value) {
|
|
99
|
+
if (value instanceof Error) return {
|
|
100
|
+
message: value.message,
|
|
101
|
+
name: value.name,
|
|
102
|
+
stack: value.stack
|
|
103
|
+
};
|
|
104
|
+
if (Array.isArray(value)) return value.map(sanitizeDebugValue);
|
|
105
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeDebugValue(item)]));
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
function formatDebugFields(fields) {
|
|
109
|
+
return Object.entries(fields).map(([key, value]) => `${key}=${formatDebugValue(value)}`).join(" ");
|
|
110
|
+
}
|
|
111
|
+
function formatDebugValue(value) {
|
|
112
|
+
if (value === void 0) return "undefined";
|
|
113
|
+
if (value === null) return "null";
|
|
114
|
+
if (typeof value === "string") return value.includes(" ") ? JSON.stringify(value) : value;
|
|
115
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
116
|
+
return JSON.stringify(sanitizeDebugValue(value));
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
54
119
|
//#region src/app-server.ts
|
|
120
|
+
const sharedSocketReconnectDelaysMs = [
|
|
121
|
+
50,
|
|
122
|
+
100,
|
|
123
|
+
250,
|
|
124
|
+
500,
|
|
125
|
+
1e3,
|
|
126
|
+
2e3
|
|
127
|
+
];
|
|
55
128
|
var CodexAppServerClient = class {
|
|
56
129
|
child;
|
|
130
|
+
closed = false;
|
|
57
131
|
initialized;
|
|
58
132
|
notificationHandlers = /* @__PURE__ */ new Set();
|
|
59
133
|
nextId = 1;
|
|
60
134
|
pending = /* @__PURE__ */ new Map();
|
|
135
|
+
reconnecting;
|
|
61
136
|
requestHandlers = /* @__PURE__ */ new Set();
|
|
62
137
|
readline;
|
|
138
|
+
sharedServer;
|
|
139
|
+
socket;
|
|
140
|
+
startSharedServer;
|
|
141
|
+
constructor(options = {}) {
|
|
142
|
+
this.startSharedServer = options.startSharedServer ?? startSharedCodexAppServer;
|
|
143
|
+
}
|
|
144
|
+
initialize() {
|
|
145
|
+
return this.ensureInitialized();
|
|
146
|
+
}
|
|
63
147
|
async listThreads(limit = 80) {
|
|
64
148
|
return (await this.request("thread/list", {
|
|
65
149
|
limit,
|
|
@@ -132,12 +216,19 @@ var CodexAppServerClient = class {
|
|
|
132
216
|
});
|
|
133
217
|
}
|
|
134
218
|
close() {
|
|
219
|
+
if (this.closed) return;
|
|
220
|
+
this.closed = true;
|
|
135
221
|
for (const pending of this.pending.values()) pending.reject(/* @__PURE__ */ new Error("Codex app-server was closed."));
|
|
136
222
|
this.pending.clear();
|
|
137
223
|
this.readline?.close();
|
|
138
224
|
this.child?.kill();
|
|
225
|
+
const socket = this.socket;
|
|
226
|
+
this.socket = void 0;
|
|
227
|
+
socket?.close();
|
|
228
|
+
this.sharedServer?.kill();
|
|
139
229
|
this.readline = void 0;
|
|
140
230
|
this.child = void 0;
|
|
231
|
+
this.sharedServer = void 0;
|
|
141
232
|
this.initialized = void 0;
|
|
142
233
|
}
|
|
143
234
|
async request(method, params) {
|
|
@@ -150,23 +241,48 @@ var CodexAppServerClient = class {
|
|
|
150
241
|
resolve: (value) => resolve(value),
|
|
151
242
|
reject
|
|
152
243
|
});
|
|
153
|
-
this.
|
|
244
|
+
this.writeJson({
|
|
154
245
|
id,
|
|
155
246
|
method,
|
|
156
247
|
params
|
|
157
|
-
})
|
|
158
|
-
if (error)
|
|
159
|
-
this.pending.delete(id);
|
|
160
|
-
reject(error);
|
|
161
|
-
}
|
|
248
|
+
}).catch((error) => {
|
|
249
|
+
if (this.pending.delete(id)) reject(error);
|
|
162
250
|
});
|
|
163
251
|
});
|
|
164
252
|
}
|
|
165
253
|
ensureInitialized() {
|
|
166
|
-
if (
|
|
254
|
+
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Codex app-server was closed."));
|
|
255
|
+
if (!this.initialized) {
|
|
256
|
+
const initialized = this.start();
|
|
257
|
+
this.initialized = initialized;
|
|
258
|
+
initialized.catch(() => {
|
|
259
|
+
if (this.initialized === initialized) this.initialized = void 0;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
167
262
|
return this.initialized;
|
|
168
263
|
}
|
|
169
|
-
start() {
|
|
264
|
+
async start() {
|
|
265
|
+
const mode = resolveCodexAppServerMode();
|
|
266
|
+
try {
|
|
267
|
+
if (mode === "socket") await this.startOrAttachSharedCodexAppServer();
|
|
268
|
+
else this.startStdioCodexAppServer();
|
|
269
|
+
await this.initializeAppServer();
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (mode === "stdio") this.stopStdioCodexAppServer();
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async initializeAppServer() {
|
|
276
|
+
await this.requestRaw("initialize", {
|
|
277
|
+
clientInfo: {
|
|
278
|
+
name: "codex-relay",
|
|
279
|
+
title: "Codex Relay Mobile Server",
|
|
280
|
+
version: "1.2.0"
|
|
281
|
+
},
|
|
282
|
+
capabilities: { experimentalApi: true }
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
startStdioCodexAppServer() {
|
|
170
286
|
const spawnConfig = resolveCodexAppServerSpawn();
|
|
171
287
|
this.child = spawn(spawnConfig.command, spawnConfig.args, {
|
|
172
288
|
env: process.env,
|
|
@@ -187,14 +303,153 @@ var CodexAppServerClient = class {
|
|
|
187
303
|
this.child = void 0;
|
|
188
304
|
this.initialized = void 0;
|
|
189
305
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
306
|
+
}
|
|
307
|
+
stopStdioCodexAppServer() {
|
|
308
|
+
this.readline?.close();
|
|
309
|
+
this.readline = void 0;
|
|
310
|
+
this.child?.kill();
|
|
311
|
+
this.child = void 0;
|
|
312
|
+
}
|
|
313
|
+
async startOrAttachSharedCodexAppServer() {
|
|
314
|
+
try {
|
|
315
|
+
await this.connectSharedCodexAppServer();
|
|
316
|
+
relayDebugLog("app_server.shared_socket.attached", {
|
|
317
|
+
ownership: "attached",
|
|
318
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
} catch (error) {
|
|
322
|
+
const attachError = asError(error);
|
|
323
|
+
relayDebugLog("app_server.shared_socket.attach_failed", {
|
|
324
|
+
message: attachError.message,
|
|
325
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
326
|
+
});
|
|
327
|
+
if (this.sharedServer) throw attachError;
|
|
328
|
+
}
|
|
329
|
+
const sharedServer = await this.startSharedServer();
|
|
330
|
+
this.sharedServer = sharedServer;
|
|
331
|
+
this.observeSharedCodexAppServer(sharedServer);
|
|
332
|
+
relayDebugLog("app_server.shared_process.started", {
|
|
333
|
+
ownership: "relay-owned",
|
|
334
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
await this.connectSharedCodexAppServer();
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (this.sharedServer === sharedServer) {
|
|
340
|
+
sharedServer.kill();
|
|
341
|
+
this.sharedServer = void 0;
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
observeSharedCodexAppServer(sharedServer) {
|
|
347
|
+
sharedServer.on("error", (error) => {
|
|
348
|
+
relayDebugLog("app_server.shared_process.error", {
|
|
349
|
+
message: error.message,
|
|
350
|
+
ownership: "relay-owned"
|
|
351
|
+
});
|
|
352
|
+
if (this.sharedServer === sharedServer) this.sharedServer = void 0;
|
|
353
|
+
});
|
|
354
|
+
sharedServer.once("exit", (code, signal) => {
|
|
355
|
+
if (this.sharedServer !== sharedServer) return;
|
|
356
|
+
this.sharedServer = void 0;
|
|
357
|
+
const error = /* @__PURE__ */ new Error(`Codex shared app-server exited with ${signal ?? code ?? 1}.`);
|
|
358
|
+
relayDebugLog("app_server.shared_process.exited", {
|
|
359
|
+
message: error.message,
|
|
360
|
+
ownership: "relay-owned"
|
|
361
|
+
});
|
|
362
|
+
if (this.socket) this.handleSharedSocketFailure(this.socket, error);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
async connectSharedCodexAppServer() {
|
|
366
|
+
const remoteAddress = resolveCodexSharedAppServerRemoteAddress();
|
|
367
|
+
const socket = new WebSocket(remoteAddress === "unix://" ? `ws+unix://${sharedCodexAppServerSocketPath()}:/` : remoteAddress, { perMessageDeflate: false });
|
|
368
|
+
this.socket = socket;
|
|
369
|
+
try {
|
|
370
|
+
await new Promise((resolve, reject) => {
|
|
371
|
+
const handleOpen = () => {
|
|
372
|
+
socket.off("error", handleInitialError);
|
|
373
|
+
resolve();
|
|
374
|
+
};
|
|
375
|
+
const handleInitialError = (error) => {
|
|
376
|
+
socket.off("open", handleOpen);
|
|
377
|
+
reject(error);
|
|
378
|
+
};
|
|
379
|
+
socket.once("open", handleOpen);
|
|
380
|
+
socket.once("error", handleInitialError);
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (this.socket === socket) this.socket = void 0;
|
|
384
|
+
socket.close();
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
socket.on("message", (data) => this.handleLine(String(data)));
|
|
388
|
+
socket.on("error", (error) => this.handleSharedSocketFailure(socket, error));
|
|
389
|
+
socket.once("close", (code, reason) => {
|
|
390
|
+
this.handleSharedSocketFailure(socket, /* @__PURE__ */ new Error(`Codex app-server socket closed with ${code}${reason.length > 0 ? `: ${String(reason)}` : "."}`));
|
|
391
|
+
});
|
|
392
|
+
relayDebugLog("app_server.shared_socket.connected", {
|
|
393
|
+
ownership: this.sharedAppServerOwnership(),
|
|
394
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
handleSharedSocketFailure(socket, error) {
|
|
398
|
+
if (this.socket !== socket) return;
|
|
399
|
+
this.socket = void 0;
|
|
400
|
+
this.rejectAll(error);
|
|
401
|
+
this.initialized = void 0;
|
|
402
|
+
relayDebugLog("app_server.shared_socket.disconnected", {
|
|
403
|
+
message: error.message,
|
|
404
|
+
ownership: this.sharedAppServerOwnership()
|
|
405
|
+
});
|
|
406
|
+
this.scheduleSharedSocketReconnect();
|
|
407
|
+
}
|
|
408
|
+
sharedAppServerOwnership() {
|
|
409
|
+
return this.sharedServer ? "relay-owned" : "attached";
|
|
410
|
+
}
|
|
411
|
+
scheduleSharedSocketReconnect() {
|
|
412
|
+
if (this.closed || this.reconnecting) return;
|
|
413
|
+
const reconnecting = this.reconnectSharedCodexAppServer();
|
|
414
|
+
this.reconnecting = reconnecting;
|
|
415
|
+
this.initialized = reconnecting;
|
|
416
|
+
reconnecting.then(() => {
|
|
417
|
+
if (this.reconnecting === reconnecting) this.reconnecting = void 0;
|
|
418
|
+
}, (error) => {
|
|
419
|
+
relayDebugLog("app_server.shared_socket.reconnect_failed", {
|
|
420
|
+
message: asError(error).message,
|
|
421
|
+
ownership: this.sharedAppServerOwnership()
|
|
422
|
+
});
|
|
423
|
+
if (this.initialized === reconnecting) this.initialized = void 0;
|
|
424
|
+
if (this.reconnecting === reconnecting) this.reconnecting = void 0;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
async reconnectSharedCodexAppServer() {
|
|
428
|
+
let lastError;
|
|
429
|
+
for (const delayMs of sharedSocketReconnectDelaysMs) {
|
|
430
|
+
await setTimeout$1(delayMs);
|
|
431
|
+
if (this.closed) return;
|
|
432
|
+
try {
|
|
433
|
+
await this.connectSharedCodexAppServer();
|
|
434
|
+
await this.initializeAppServer();
|
|
435
|
+
relayDebugLog("app_server.shared_socket.reconnected", {
|
|
436
|
+
ownership: this.sharedAppServerOwnership(),
|
|
437
|
+
socketPath: sharedCodexAppServerSocketPath()
|
|
438
|
+
});
|
|
439
|
+
return;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
lastError = asError(error);
|
|
442
|
+
const socket = this.socket;
|
|
443
|
+
this.socket = void 0;
|
|
444
|
+
socket?.close();
|
|
445
|
+
relayDebugLog("app_server.shared_socket.reconnect_retry", {
|
|
446
|
+
delayMs,
|
|
447
|
+
message: lastError.message,
|
|
448
|
+
ownership: this.sharedAppServerOwnership()
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
throw lastError ?? /* @__PURE__ */ new Error("Unable to reconnect to the shared Codex app-server.");
|
|
198
453
|
}
|
|
199
454
|
requestRaw(method, params) {
|
|
200
455
|
const id = this.nextId++;
|
|
@@ -210,11 +465,8 @@ var CodexAppServerClient = class {
|
|
|
210
465
|
resolve: (value) => resolve(value),
|
|
211
466
|
reject
|
|
212
467
|
});
|
|
213
|
-
this.
|
|
214
|
-
if (error)
|
|
215
|
-
this.pending.delete(id);
|
|
216
|
-
reject(error);
|
|
217
|
-
}
|
|
468
|
+
this.writeSerializedJson(request).catch((error) => {
|
|
469
|
+
if (this.pending.delete(id)) reject(error);
|
|
218
470
|
});
|
|
219
471
|
});
|
|
220
472
|
}
|
|
@@ -256,12 +508,22 @@ var CodexAppServerClient = class {
|
|
|
256
508
|
} else pending.resolve(message.result);
|
|
257
509
|
}
|
|
258
510
|
writeJson(payload) {
|
|
511
|
+
return this.writeSerializedJson(JSON.stringify(payload));
|
|
512
|
+
}
|
|
513
|
+
writeSerializedJson(payload) {
|
|
259
514
|
return new Promise((resolve, reject) => {
|
|
515
|
+
if (this.socket) {
|
|
516
|
+
this.socket.send(payload, (error) => {
|
|
517
|
+
if (error) reject(error);
|
|
518
|
+
else resolve();
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
260
522
|
if (!this.child?.stdin) {
|
|
261
523
|
reject(/* @__PURE__ */ new Error("Codex app-server is not running."));
|
|
262
524
|
return;
|
|
263
525
|
}
|
|
264
|
-
this.child.stdin.write(`${
|
|
526
|
+
this.child.stdin.write(`${payload}\n`, (error) => {
|
|
265
527
|
if (error) reject(error);
|
|
266
528
|
else resolve();
|
|
267
529
|
});
|
|
@@ -272,6 +534,69 @@ var CodexAppServerClient = class {
|
|
|
272
534
|
this.pending.clear();
|
|
273
535
|
}
|
|
274
536
|
};
|
|
537
|
+
async function startSharedCodexAppServer() {
|
|
538
|
+
const spawnConfig = resolveCodexSharedAppServerSpawn();
|
|
539
|
+
const child = spawn(spawnConfig.command, spawnConfig.args, {
|
|
540
|
+
env: process.env,
|
|
541
|
+
shell: spawnConfig.shell,
|
|
542
|
+
windowsHide: spawnConfig.windowsHide
|
|
543
|
+
});
|
|
544
|
+
let spawnError;
|
|
545
|
+
let exitReason;
|
|
546
|
+
let stderr = "";
|
|
547
|
+
child.stdout.resume();
|
|
548
|
+
child.stderr.on("data", (chunk) => {
|
|
549
|
+
if (stderr.length < 8192) stderr += String(chunk).slice(0, 8192 - stderr.length);
|
|
550
|
+
});
|
|
551
|
+
child.once("error", (error) => {
|
|
552
|
+
spawnError = error;
|
|
553
|
+
});
|
|
554
|
+
child.once("exit", (code, signal) => {
|
|
555
|
+
exitReason = signal ?? code ?? 1;
|
|
556
|
+
});
|
|
557
|
+
const remoteAddress = resolveCodexSharedAppServerRemoteAddress();
|
|
558
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
559
|
+
if (spawnError) throw new Error(`Failed to start the shared Codex app-server: ${spawnError.message}`);
|
|
560
|
+
try {
|
|
561
|
+
await waitForSharedCodexAppServer(remoteAddress);
|
|
562
|
+
return child;
|
|
563
|
+
} catch {
|
|
564
|
+
if (exitReason !== void 0) {
|
|
565
|
+
const detail = stderr.trim();
|
|
566
|
+
throw new Error(`Failed to start the shared Codex app-server (exit ${exitReason})${detail ? `: ${detail}` : "."}`);
|
|
567
|
+
}
|
|
568
|
+
await setTimeout$1(25);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
child.kill();
|
|
572
|
+
throw new Error(`Timed out waiting for the shared Codex app-server at ${remoteAddress}.`);
|
|
573
|
+
}
|
|
574
|
+
async function waitForSharedCodexAppServer(remoteAddress) {
|
|
575
|
+
if (remoteAddress === "unix://") {
|
|
576
|
+
await access(sharedCodexAppServerSocketPath());
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const url = new URL(remoteAddress);
|
|
580
|
+
await new Promise((resolve, reject) => {
|
|
581
|
+
const socket = connect({
|
|
582
|
+
host: url.hostname,
|
|
583
|
+
port: Number(url.port)
|
|
584
|
+
});
|
|
585
|
+
const onError = (error) => reject(error);
|
|
586
|
+
socket.once("error", onError);
|
|
587
|
+
socket.once("connect", () => {
|
|
588
|
+
socket.off("error", onError);
|
|
589
|
+
socket.destroy();
|
|
590
|
+
resolve();
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function sharedCodexAppServerSocketPath() {
|
|
595
|
+
return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "app-server-control", "app-server-control.sock");
|
|
596
|
+
}
|
|
597
|
+
function asError(error) {
|
|
598
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
599
|
+
}
|
|
275
600
|
function debugAppServer(kind, method, id, detail) {
|
|
276
601
|
if (process.env.CODEX_RELAY_DEBUG_APP_SERVER !== "1") return;
|
|
277
602
|
console.log(`[app-server] ${kind}${id === void 0 ? "" : ` #${id}`}${method ? ` ${method}` : ""}${detail ? ` ${detail}` : ""}`);
|
|
@@ -461,41 +786,79 @@ function sumPositiveIntegers(values) {
|
|
|
461
786
|
return found ? sum : null;
|
|
462
787
|
}
|
|
463
788
|
//#endregion
|
|
464
|
-
//#region src/
|
|
465
|
-
|
|
466
|
-
|
|
789
|
+
//#region src/push-notifications.ts
|
|
790
|
+
const expoPushEndpoint = "https://exp.host/--/api/v2/push/send";
|
|
791
|
+
const expoPushChunkSize = 100;
|
|
792
|
+
function createExpoPushNotificationSender(fetchImpl = fetch) {
|
|
793
|
+
return { async send(notifications) {
|
|
794
|
+
const invalidExpoPushTokens = /* @__PURE__ */ new Set();
|
|
795
|
+
for (const chunk of chunks(notifications, expoPushChunkSize)) {
|
|
796
|
+
const response = await fetchImpl(expoPushEndpoint, {
|
|
797
|
+
body: JSON.stringify(chunk.map((notification) => ({
|
|
798
|
+
body: notification.body,
|
|
799
|
+
channelId: "default",
|
|
800
|
+
data: notification.data,
|
|
801
|
+
priority: "high",
|
|
802
|
+
sound: "default",
|
|
803
|
+
title: notification.title,
|
|
804
|
+
to: notification.to
|
|
805
|
+
}))),
|
|
806
|
+
headers: {
|
|
807
|
+
accept: "application/json",
|
|
808
|
+
"content-type": "application/json"
|
|
809
|
+
},
|
|
810
|
+
method: "POST"
|
|
811
|
+
});
|
|
812
|
+
const body = await response.json().catch(() => void 0);
|
|
813
|
+
if (!response.ok) throw new Error(`Expo push service returned ${response.status}.`);
|
|
814
|
+
const tickets = expoPushTickets(body);
|
|
815
|
+
for (const [index, ticket] of tickets.entries()) {
|
|
816
|
+
const notification = chunk[index];
|
|
817
|
+
if (notification && isDeviceNotRegistered(ticket)) invalidExpoPushTokens.add(notification.to);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return { invalidExpoPushTokens: [...invalidExpoPushTokens] };
|
|
821
|
+
} };
|
|
467
822
|
}
|
|
468
|
-
function
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
...
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const path = process.env.CODEX_RELAY_DEBUG_LOG_PATH?.trim();
|
|
477
|
-
if (path) mkdir(dirname(path), { recursive: true }).then(() => appendFile(path, line, { mode: 384 })).catch(() => void 0);
|
|
478
|
-
console.log(`[debug] ${event} ${formatDebugFields(fields)}`.trimEnd());
|
|
823
|
+
function createPushNotificationDispatcher(input) {
|
|
824
|
+
return { async dispatch(event) {
|
|
825
|
+
const subscriptions = await input.sessions.listActivePushNotificationSubscriptions(Date.now());
|
|
826
|
+
const selectedTokens = new Set(subscriptions.filter((subscription) => notificationEnabled(subscription, event.intent)).map((subscription) => subscription.expoPushToken));
|
|
827
|
+
if (selectedTokens.size === 0) return;
|
|
828
|
+
const delivery = await input.sender.send([...selectedTokens].map((expoPushToken) => notificationForEvent(expoPushToken, event)));
|
|
829
|
+
await Promise.all(delivery.invalidExpoPushTokens.map((expoPushToken) => input.sessions.deletePushNotificationSubscriptionsByExpoPushToken(expoPushToken)));
|
|
830
|
+
} };
|
|
479
831
|
}
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
832
|
+
function notificationEnabled(subscription, intent) {
|
|
833
|
+
return intent === "action_required" ? subscription.actionRequired : subscription.turnTerminal;
|
|
834
|
+
}
|
|
835
|
+
function notificationForEvent(expoPushToken, event) {
|
|
836
|
+
return {
|
|
837
|
+
body: event.intent === "action_required" ? "Codex needs your attention." : "A Codex turn has finished.",
|
|
838
|
+
data: {
|
|
839
|
+
intent: event.intent,
|
|
840
|
+
threadId: event.threadId,
|
|
841
|
+
...event.turnId ? { turnId: event.turnId } : {}
|
|
842
|
+
},
|
|
843
|
+
title: "Codex Relay",
|
|
844
|
+
to: expoPushToken
|
|
485
845
|
};
|
|
486
|
-
if (Array.isArray(value)) return value.map(sanitizeDebugValue);
|
|
487
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeDebugValue(item)]));
|
|
488
|
-
return value;
|
|
489
846
|
}
|
|
490
|
-
function
|
|
491
|
-
|
|
847
|
+
function expoPushTickets(value) {
|
|
848
|
+
if (!value || typeof value !== "object") return [];
|
|
849
|
+
const data = value.data;
|
|
850
|
+
return Array.isArray(data) ? data : [];
|
|
492
851
|
}
|
|
493
|
-
function
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
if (
|
|
497
|
-
|
|
498
|
-
|
|
852
|
+
function isDeviceNotRegistered(ticket) {
|
|
853
|
+
if (!ticket || typeof ticket !== "object") return false;
|
|
854
|
+
const record = ticket;
|
|
855
|
+
if (record.status !== "error" || !record.details || typeof record.details !== "object") return false;
|
|
856
|
+
return record.details.error === "DeviceNotRegistered";
|
|
857
|
+
}
|
|
858
|
+
function chunks(items, size) {
|
|
859
|
+
const result = [];
|
|
860
|
+
for (let index = 0; index < items.length; index += size) result.push(items.slice(index, index + size));
|
|
861
|
+
return result;
|
|
499
862
|
}
|
|
500
863
|
//#endregion
|
|
501
864
|
//#region src/preferences-store.ts
|
|
@@ -1062,6 +1425,11 @@ function createApp(options = {}) {
|
|
|
1062
1425
|
const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
|
|
1063
1426
|
const activeStreamControllers = /* @__PURE__ */ new Set();
|
|
1064
1427
|
const threadOptions = { workingDirectory: workspacePath };
|
|
1428
|
+
const pushNotificationDispatcher = options.pairing ? createPushNotificationDispatcher({
|
|
1429
|
+
sender: options.pushNotificationSender ?? createExpoPushNotificationSender(),
|
|
1430
|
+
sessions: options.pairing.sessions
|
|
1431
|
+
}) : void 0;
|
|
1432
|
+
if (appServer && pushNotificationDispatcher) observeAppServerPushNotifications(appServer, pushNotificationDispatcher);
|
|
1065
1433
|
const scheduleAppServerHistoryLoad = (threadId, cachedMessages) => {
|
|
1066
1434
|
if (!appServer || appServerHistoryLoadsByThreadId.has(threadId)) return;
|
|
1067
1435
|
const startedAt = Date.now();
|
|
@@ -1286,6 +1654,48 @@ function createApp(options = {}) {
|
|
|
1286
1654
|
});
|
|
1287
1655
|
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1288
1656
|
});
|
|
1657
|
+
app.get(apiPaths.pushNotifications, async (c) => {
|
|
1658
|
+
const clientSessionId = await pairedClientSessionIdForAuthorization(c.req.header("authorization"), options.pairing);
|
|
1659
|
+
if (!clientSessionId) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("push_notifications_unavailable", "Pair this device again to manage notifications."), 401);
|
|
1660
|
+
const subscription = await options.pairing.sessions.getPushNotificationSubscription(clientSessionId);
|
|
1661
|
+
const response = PushNotificationSettingsResponseSchema.parse({
|
|
1662
|
+
preferences: subscription ? {
|
|
1663
|
+
actionRequired: subscription.actionRequired,
|
|
1664
|
+
turnTerminal: subscription.turnTerminal
|
|
1665
|
+
} : defaultPushNotificationPreferences(),
|
|
1666
|
+
registered: Boolean(subscription)
|
|
1667
|
+
});
|
|
1668
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1669
|
+
});
|
|
1670
|
+
app.put(apiPaths.pushNotifications, async (c) => {
|
|
1671
|
+
if (!options.pairing) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("pairing_required", "Pair this device before registering notifications."), 404);
|
|
1672
|
+
const parsed = await parseRequestJson(c, options.pairing, secureSessionsByTokenHash, RegisterPushNotificationRequestSchema);
|
|
1673
|
+
if (!parsed.success) return secureJson(c, options.pairing, secureSessionsByTokenHash, validationError(parsed.error), 400);
|
|
1674
|
+
const clientSessionId = await pairedClientSessionIdForAuthorization(c.req.header("authorization"), options.pairing);
|
|
1675
|
+
if (!clientSessionId) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("push_notifications_unavailable", "Pair this device again to manage notifications."), 401);
|
|
1676
|
+
await options.pairing.sessions.upsertPushNotificationSubscription({
|
|
1677
|
+
actionRequired: parsed.data.preferences.actionRequired,
|
|
1678
|
+
clientSessionId,
|
|
1679
|
+
expoPushToken: parsed.data.expoPushToken,
|
|
1680
|
+
platform: parsed.data.platform,
|
|
1681
|
+
turnTerminal: parsed.data.preferences.turnTerminal
|
|
1682
|
+
});
|
|
1683
|
+
const response = PushNotificationSettingsResponseSchema.parse({
|
|
1684
|
+
preferences: parsed.data.preferences,
|
|
1685
|
+
registered: true
|
|
1686
|
+
});
|
|
1687
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1688
|
+
});
|
|
1689
|
+
app.delete(apiPaths.pushNotifications, async (c) => {
|
|
1690
|
+
const clientSessionId = await pairedClientSessionIdForAuthorization(c.req.header("authorization"), options.pairing);
|
|
1691
|
+
if (!clientSessionId) return secureJson(c, options.pairing, secureSessionsByTokenHash, apiError("push_notifications_unavailable", "Pair this device again to manage notifications."), 401);
|
|
1692
|
+
await options.pairing.sessions.deletePushNotificationSubscription(clientSessionId);
|
|
1693
|
+
const response = PushNotificationSettingsResponseSchema.parse({
|
|
1694
|
+
preferences: defaultPushNotificationPreferences(),
|
|
1695
|
+
registered: false
|
|
1696
|
+
});
|
|
1697
|
+
return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
|
|
1698
|
+
});
|
|
1289
1699
|
app.get(apiPaths.workspaceDirectories, async (c) => {
|
|
1290
1700
|
const targetPath = resolve(c.req.query("path") ?? workspacePath);
|
|
1291
1701
|
try {
|
|
@@ -2282,6 +2692,18 @@ function createApp(options = {}) {
|
|
|
2282
2692
|
function parseBearerToken(value) {
|
|
2283
2693
|
return (value?.match(/^Bearer\s+(.+)$/i))?.[1];
|
|
2284
2694
|
}
|
|
2695
|
+
async function pairedClientSessionIdForAuthorization(authorization, pairing) {
|
|
2696
|
+
if (!pairing) return;
|
|
2697
|
+
const token = parseBearerToken(authorization);
|
|
2698
|
+
if (!token) return;
|
|
2699
|
+
return (await pairing.sessions.getValidSession(pairing.hashClientToken(token), Date.now()))?.clientSessionId;
|
|
2700
|
+
}
|
|
2701
|
+
function defaultPushNotificationPreferences() {
|
|
2702
|
+
return {
|
|
2703
|
+
actionRequired: false,
|
|
2704
|
+
turnTerminal: false
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2285
2707
|
function normalizeApprovalCode(value) {
|
|
2286
2708
|
const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "").replaceAll("O", "0").replaceAll("I", "1");
|
|
2287
2709
|
return normalized.length === 8 ? `${normalized.slice(0, 4)}-${normalized.slice(4)}` : normalized;
|
|
@@ -5297,6 +5719,55 @@ function structuredStreamMessage(role, event, fallbackContent) {
|
|
|
5297
5719
|
function isApprovalServerRequest(method) {
|
|
5298
5720
|
return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval" || method === "item/permissions/requestApproval" || method === "item/tool/requestUserInput" || method === "mcpServer/elicitation/request" || method === "execCommandApproval" || method === "applyPatchApproval";
|
|
5299
5721
|
}
|
|
5722
|
+
function observeAppServerPushNotifications(appServer, dispatcher) {
|
|
5723
|
+
const dispatchedEventIds = /* @__PURE__ */ new Set();
|
|
5724
|
+
const dispatch = (event, eventId) => {
|
|
5725
|
+
if (dispatchedEventIds.has(eventId)) return;
|
|
5726
|
+
if (dispatchedEventIds.size >= 1e3) dispatchedEventIds.clear();
|
|
5727
|
+
dispatchedEventIds.add(eventId);
|
|
5728
|
+
dispatcher.dispatch(event).catch((error) => {
|
|
5729
|
+
relayDebugLog("push_notification.dispatch_failed", {
|
|
5730
|
+
error: errorMessage(error),
|
|
5731
|
+
intent: event.intent,
|
|
5732
|
+
threadId: event.threadId,
|
|
5733
|
+
turnId: event.turnId
|
|
5734
|
+
});
|
|
5735
|
+
});
|
|
5736
|
+
};
|
|
5737
|
+
appServer.onNotification((notification) => {
|
|
5738
|
+
const event = pushNotificationEventFromTerminalNotification(notification);
|
|
5739
|
+
if (!event) return;
|
|
5740
|
+
dispatch(event, `${event.intent}:${event.threadId}:${event.turnId ?? ""}`);
|
|
5741
|
+
});
|
|
5742
|
+
appServer.onRequest((request) => {
|
|
5743
|
+
const event = pushNotificationEventFromApprovalRequest(request);
|
|
5744
|
+
if (!event) return;
|
|
5745
|
+
dispatch(event, `${event.intent}:${event.threadId}:${event.turnId ?? ""}:${request.id}`);
|
|
5746
|
+
});
|
|
5747
|
+
}
|
|
5748
|
+
function pushNotificationEventFromTerminalNotification(notification) {
|
|
5749
|
+
if (notification.method !== "turn/completed" && notification.method !== "turn/failed") return;
|
|
5750
|
+
const params = recordParams(notification);
|
|
5751
|
+
const status = turnStatus(params);
|
|
5752
|
+
if (status === "aborted" || status === "cancelled" || status === "canceled" || status === "interrupted") return;
|
|
5753
|
+
const threadId = firstString(params, ["threadId", "conversationId"]);
|
|
5754
|
+
if (!threadId) return;
|
|
5755
|
+
return {
|
|
5756
|
+
intent: "turn_terminal",
|
|
5757
|
+
threadId,
|
|
5758
|
+
turnId: firstString(params, ["turnId"]) ?? turnIdFromParams(params)
|
|
5759
|
+
};
|
|
5760
|
+
}
|
|
5761
|
+
function pushNotificationEventFromApprovalRequest(request) {
|
|
5762
|
+
if (!isApprovalServerRequest(request.method)) return;
|
|
5763
|
+
const approval = approvalMessageFromRequest(request);
|
|
5764
|
+
if (!approval) return;
|
|
5765
|
+
return {
|
|
5766
|
+
intent: "action_required",
|
|
5767
|
+
threadId: approval.threadId,
|
|
5768
|
+
turnId: approval.turnId
|
|
5769
|
+
};
|
|
5770
|
+
}
|
|
5300
5771
|
function approvalMessageFromRequest(request) {
|
|
5301
5772
|
const params = recordParams(request);
|
|
5302
5773
|
const threadId = firstString(params, ["threadId", "conversationId"]);
|
|
@@ -6274,8 +6745,17 @@ const color = {
|
|
|
6274
6745
|
const npxCommand = "npx codex-relay@latest";
|
|
6275
6746
|
const sessionStore = await createTursoPairingSessionStore(process.env.CODEX_RELAY_AUTH_DB_PATH ?? await prepareCodexRelayDataPath("auth.db", ["auth.db-shm", "auth.db-wal"]));
|
|
6276
6747
|
const preferencesStore = createFileRuntimePreferencesStore(process.env.CODEX_RELAY_PREFERENCES_PATH ?? await prepareCodexRelayDataPath("preferences.json"));
|
|
6748
|
+
const appServerMode = resolveCodexAppServerMode();
|
|
6749
|
+
const sharedAppServer = appServerMode === "socket" ? new CodexAppServerClient() : void 0;
|
|
6750
|
+
if (sharedAppServer) {
|
|
6751
|
+
await sharedAppServer.initialize();
|
|
6752
|
+
process.once("SIGINT", () => stopSharedAppServer(130));
|
|
6753
|
+
process.once("SIGTERM", () => stopSharedAppServer(143));
|
|
6754
|
+
process.once("exit", () => sharedAppServer.close());
|
|
6755
|
+
}
|
|
6277
6756
|
serve({
|
|
6278
6757
|
fetch: createApp({
|
|
6758
|
+
appServer: sharedAppServer,
|
|
6279
6759
|
pairing: {
|
|
6280
6760
|
approvalSecret,
|
|
6281
6761
|
dangerouslyAutoApprove,
|
|
@@ -6346,9 +6826,14 @@ serve({
|
|
|
6346
6826
|
dangerouslyAutoApprove,
|
|
6347
6827
|
listenUrl,
|
|
6348
6828
|
pairingPayload,
|
|
6349
|
-
port: info.port
|
|
6829
|
+
port: info.port,
|
|
6830
|
+
sharedAppServerRemoteAddress: appServerMode === "socket" ? resolveCodexSharedAppServerRemoteAddress() : void 0
|
|
6350
6831
|
}));
|
|
6351
6832
|
});
|
|
6833
|
+
function stopSharedAppServer(exitCode) {
|
|
6834
|
+
sharedAppServer?.close();
|
|
6835
|
+
process.exit(exitCode);
|
|
6836
|
+
}
|
|
6352
6837
|
function formatStartupInstructions(details) {
|
|
6353
6838
|
return [
|
|
6354
6839
|
"",
|
|
@@ -6361,6 +6846,11 @@ function formatStartupInstructions(details) {
|
|
|
6361
6846
|
`${color.prompt("›")} Server: ${color.muted(details.listenUrl)}`,
|
|
6362
6847
|
"",
|
|
6363
6848
|
`${color.prompt("›")} Pairing: ${color.url(details.pairingPayload)}`,
|
|
6849
|
+
...details.sharedAppServerRemoteAddress ? [
|
|
6850
|
+
"",
|
|
6851
|
+
`${color.prompt("›")} Terminal: ${color.command(`codex resume --remote ${details.sharedAppServerRemoteAddress}`)}`,
|
|
6852
|
+
` ${color.muted("Connect through the shared Codex app-server to follow and steer the same live sessions.")}`
|
|
6853
|
+
] : [],
|
|
6364
6854
|
"",
|
|
6365
6855
|
`${color.prompt("›")} Commands`,
|
|
6366
6856
|
` ${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
|
+
"version": "1.4.0",
|
|
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.
|
|
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",
|
package/src/api-schema.ts
CHANGED
|
@@ -95,6 +95,27 @@ export const RuntimePreferencesResponseSchema = z.object({
|
|
|
95
95
|
workspacePath: z.string().trim().min(1).optional(),
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
export const PushNotificationIntentSchema = z.enum(["turn_terminal", "action_required"]);
|
|
99
|
+
|
|
100
|
+
export const PushNotificationPreferencesSchema = z.object({
|
|
101
|
+
actionRequired: z.boolean(),
|
|
102
|
+
turnTerminal: z.boolean(),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
export const RegisterPushNotificationRequestSchema = z.object({
|
|
106
|
+
expoPushToken: z
|
|
107
|
+
.string()
|
|
108
|
+
.trim()
|
|
109
|
+
.regex(/^(?:Expo|Exponent)PushToken\[[^\]\r\n]{1,512}\]$/),
|
|
110
|
+
platform: z.enum(["android", "ios"]),
|
|
111
|
+
preferences: PushNotificationPreferencesSchema,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const PushNotificationSettingsResponseSchema = z.object({
|
|
115
|
+
preferences: PushNotificationPreferencesSchema,
|
|
116
|
+
registered: z.boolean(),
|
|
117
|
+
});
|
|
118
|
+
|
|
98
119
|
export const CodexModelSchema = z.object({
|
|
99
120
|
id: z.string().min(1),
|
|
100
121
|
model: z.string().min(1),
|
|
@@ -753,6 +774,12 @@ export type RuntimePreferencesByWorkspacePath = z.infer<
|
|
|
753
774
|
typeof RuntimePreferencesByWorkspacePathSchema
|
|
754
775
|
>;
|
|
755
776
|
export type RuntimePreferencesResponse = z.infer<typeof RuntimePreferencesResponseSchema>;
|
|
777
|
+
export type PushNotificationIntent = z.infer<typeof PushNotificationIntentSchema>;
|
|
778
|
+
export type PushNotificationPreferences = z.infer<typeof PushNotificationPreferencesSchema>;
|
|
779
|
+
export type RegisterPushNotificationRequest = z.infer<typeof RegisterPushNotificationRequestSchema>;
|
|
780
|
+
export type PushNotificationSettingsResponse = z.infer<
|
|
781
|
+
typeof PushNotificationSettingsResponseSchema
|
|
782
|
+
>;
|
|
756
783
|
export type ContextWindowUsage = z.infer<typeof ContextWindowUsageSchema>;
|
|
757
784
|
export type RateLimitBucket = z.infer<typeof RateLimitBucketSchema>;
|
|
758
785
|
export type RateLimitWindow = z.infer<typeof RateLimitWindowSchema>;
|
|
@@ -1033,6 +1060,7 @@ export const apiPaths = {
|
|
|
1033
1060
|
sessionRefresh: "/v1/session/refresh",
|
|
1034
1061
|
status: "/v1/status",
|
|
1035
1062
|
preferences: "/v1/preferences",
|
|
1063
|
+
pushNotifications: "/v1/notifications/push",
|
|
1036
1064
|
rateLimits: "/v1/rate-limits",
|
|
1037
1065
|
models: "/v1/models",
|
|
1038
1066
|
skills: "/v1/skills",
|
|
@@ -1111,6 +1139,31 @@ export function createOpenApiDocument() {
|
|
|
1111
1139
|
},
|
|
1112
1140
|
},
|
|
1113
1141
|
},
|
|
1142
|
+
"/v1/notifications/push": {
|
|
1143
|
+
get: {
|
|
1144
|
+
summary: "Read push notification settings for this paired device",
|
|
1145
|
+
responses: {
|
|
1146
|
+
"200": jsonResponse("PushNotificationSettingsResponse"),
|
|
1147
|
+
"401": jsonResponse("ErrorResponse"),
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
put: {
|
|
1151
|
+
summary: "Register push notifications for this paired device",
|
|
1152
|
+
requestBody: jsonRequest("RegisterPushNotificationRequest"),
|
|
1153
|
+
responses: {
|
|
1154
|
+
"200": jsonResponse("PushNotificationSettingsResponse"),
|
|
1155
|
+
"400": jsonResponse("ErrorResponse"),
|
|
1156
|
+
"401": jsonResponse("ErrorResponse"),
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
delete: {
|
|
1160
|
+
summary: "Remove push notifications for this paired device",
|
|
1161
|
+
responses: {
|
|
1162
|
+
"200": jsonResponse("PushNotificationSettingsResponse"),
|
|
1163
|
+
"401": jsonResponse("ErrorResponse"),
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1114
1167
|
"/v1/threads": {
|
|
1115
1168
|
get: {
|
|
1116
1169
|
summary: "List locally known threads",
|
|
@@ -1491,6 +1544,34 @@ export function createOpenApiDocument() {
|
|
|
1491
1544
|
workspacePath: { type: "string" },
|
|
1492
1545
|
},
|
|
1493
1546
|
},
|
|
1547
|
+
PushNotificationPreferences: {
|
|
1548
|
+
type: "object",
|
|
1549
|
+
required: ["actionRequired", "turnTerminal"],
|
|
1550
|
+
properties: {
|
|
1551
|
+
actionRequired: { type: "boolean" },
|
|
1552
|
+
turnTerminal: { type: "boolean" },
|
|
1553
|
+
},
|
|
1554
|
+
},
|
|
1555
|
+
RegisterPushNotificationRequest: {
|
|
1556
|
+
type: "object",
|
|
1557
|
+
required: ["expoPushToken", "platform", "preferences"],
|
|
1558
|
+
properties: {
|
|
1559
|
+
expoPushToken: {
|
|
1560
|
+
type: "string",
|
|
1561
|
+
pattern: "^(?:Expo|Exponent)PushToken\\[[^\\]\\r\\n]{1,512}\\]$",
|
|
1562
|
+
},
|
|
1563
|
+
platform: { type: "string", enum: ["android", "ios"] },
|
|
1564
|
+
preferences: { $ref: "#/components/schemas/PushNotificationPreferences" },
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1567
|
+
PushNotificationSettingsResponse: {
|
|
1568
|
+
type: "object",
|
|
1569
|
+
required: ["preferences", "registered"],
|
|
1570
|
+
properties: {
|
|
1571
|
+
preferences: { $ref: "#/components/schemas/PushNotificationPreferences" },
|
|
1572
|
+
registered: { type: "boolean" },
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1494
1575
|
WorkspaceDirectoryEntry: {
|
|
1495
1576
|
type: "object",
|
|
1496
1577
|
required: ["name", "path"],
|