codex-relay 1.3.2 → 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 CHANGED
@@ -32,21 +32,31 @@ After approval, the phone can list Codex threads, start new work, stream message
32
32
 
33
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
34
 
35
- On macOS, Linux, or WSL, opt in to Codex's shared app-server socket:
35
+ Opt in to Codex's shared app-server:
36
36
 
37
37
  ```sh
38
38
  npx codex-relay@latest --shared-app-server
39
39
  ```
40
40
 
41
- Then connect a new terminal TUI to the shared app-server socket:
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:
42
44
 
43
45
  ```sh
44
46
  codex resume --remote unix://
45
47
  ```
46
48
 
47
- Pass a thread ID after `unix://` to open a specific thread. The relay prints the attach command at startup. Mobile and the connected terminal can then observe the same live sessions through one socket-backed app-server. An already-running standalone TUI cannot be converted in place; exit it and reconnect with `--remote`.
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.
48
58
 
49
- Shared mode requires a recent Codex CLI with Unix-socket app-server and `resume --remote` support. If those features are unavailable, update Codex or omit `--shared-app-server` to keep the existing private mode. Native Windows is not currently supported; use WSL or private mode there.
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.
50
60
 
51
61
  ## Background Mode
52
62
 
@@ -1,2 +1,2 @@
1
- import { $ as RuntimePreferencesSchema, A as PairRequestSchema, At as WorkspacePreviewTabSchema, B as QueuedThreadInputActionResponseSchema, Bt as apiPaths, C as ListModelsResponseSchema, Ct as WorkspaceChangesResponseSchema, D as ListWorkspaceDirectoriesResponseSchema, Dt as WorkspaceGitActionResponseSchema, E as ListThreadsResponseSchema, Et as WorkspaceFileMentionSchema, F as PromptAttachmentSchema, Ft as WorkspaceTerminalOutputChunkSchema, G as ReasoningEffortSchema, Gt as promptMarkdownWithSkills, H as RateLimitBucketSchema, Ht as createOpenApiDocument, I as PromptAttachmentSummarySchema, It as WorkspaceTerminalOutputResponseSchema, J as RunThreadRequestSchema, Jt as promptSkillMentionMarkdown, K as ResolveApprovalRequestSchema, Kt as promptSkillDisplayName, L as PromptContextInputSchema, Lt as WorkspaceTerminalResizeRequestSchema, M as PendingInputRequestOptionSchema, Mt as WorkspaceTailscaleServeRequestSchema, N as PendingInputRequestQuestionSchema, Nt as WorkspaceTailscaleServeResponseSchema, O as ListWorkspaceFilesResponseSchema, Ot as WorkspaceMarkdownPreviewTargetSchema, P as PendingInputRequestSchema, Pt as WorkspaceTerminalInputRequestSchema, Q as RuntimePreferencesResponseSchema, R as PromptContextSchema, Rt as WorkspaceTerminalSessionResponseSchema, S as KnownReasoningEffortSchema, St as WebPreviewTargetSchema, T as ListSkillsResponseSchema, Tt as WorkspaceFileContentResponseSchema, U as RateLimitWindowSchema, Ut as isPromptSkillMarkdownMention, V as QueuedThreadInputSchema, Vt as chatMessageDetailsFromPromptContext, W as RateLimitsResponseSchema, Wt as normalizePromptContext, X as RuntimeModeSchema, Xt as stripPromptSkillMentions, Y as RunThreadResponseSchema, Yt as promptSkillMentionTextCandidates, Z as RuntimePreferencesByWorkspacePathSchema, _ as EncryptedPayloadSchema, _t as UpdateThreadGoalRequestSchema, a as ArchiveThreadResponseSchema, at as ThreadCollaborationModeSchema, b as InterruptThreadRunResponseSchema, bt as WORKSPACE_PREVIEW_OPEN_PROTOCOL, c as ChatMessageRoleSchema, ct as ThreadGoalResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadMessageDetailFieldSchema, et as SandboxModeSchema, f as CodexModelSchema, ft as ThreadMessageDetailResponseSchema, g as CreateThreadResponseSchema, gt as UpdateRuntimePreferencesRequestSchema, h as CreateThreadRequestSchema, ht as ThreadSummarySchema, i as ApprovalModeSchema, it as SubmitThreadInputResponseSchema, j as PairResponseSchema, jt as WorkspaceSelectionRequestSchema, k as PairEncryptedPayloadSchema, kt as WorkspacePreviewNavigationRequestSchema, l as ChatMessageSchema, lt as ThreadGoalSchema, m as ContextWindowUsageSchema, mt as ThreadStateSchema, n as AgentSkillSourceSchema, nt as StreamThreadRunEventSchema, o as ChatMessageKindSchema, ot as ThreadContextWindowResponseSchema, p as CommitPushWorkspaceRequestSchema, pt as ThreadRunOptionsSchema, q as ResolveApprovalResponseSchema, qt as promptSkillMentionLabel, r as ApprovalDecisionSchema, rt as StreamThreadRunRequestSchema, s as ChatMessagePromptDetailsSchema, st as ThreadDetailResponseSchema, t as AgentSkillSchema, tt as StatusResponseSchema, u as ChatMessageStateSchema, ut as ThreadGoalStatusSchema, v as ErrorResponseSchema, vt as UpdateWorkspaceFileContentRequestSchema, w as ListQueuedThreadInputsResponseSchema, wt as WorkspaceDirectoryEntrySchema, x as IsoDateTimeSchema, xt as WORKSPACE_PREVIEW_TAB_VALUES, y as ImageAttachmentUploadResponseSchema, yt as VersionResponseSchema, z as PromptSkillSchema, zt as WorkspaceTerminalStartRequestSchema } 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, 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 };
@@ -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 { RuntimePreferencesSchema as $, PairRequestSchema as A, WorkspacePreviewTabSchema as At, QueuedThreadInputActionResponseSchema as B, apiPaths as Bt, ListModelsResponseSchema as C, WorkspaceChangesResponseSchema as Ct, ListWorkspaceDirectoriesResponseSchema as D, WorkspaceGitActionResponseSchema as Dt, ListThreadsResponseSchema as E, WorkspaceFileMentionSchema as Et, PromptAttachmentSchema as F, WorkspaceTerminalOutputChunkSchema as Ft, ReasoningEffortSchema as G, promptMarkdownWithSkills as Gt, RateLimitBucketSchema as H, createOpenApiDocument as Ht, PromptAttachmentSummarySchema as I, WorkspaceTerminalOutputResponseSchema as It, RunThreadRequestSchema as J, promptSkillMentionMarkdown as Jt, ResolveApprovalRequestSchema as K, promptSkillDisplayName as Kt, PromptContextInputSchema as L, WorkspaceTerminalResizeRequestSchema as Lt, PendingInputRequestOptionSchema as M, WorkspaceTailscaleServeRequestSchema as Mt, PendingInputRequestQuestionSchema as N, WorkspaceTailscaleServeResponseSchema as Nt, ListWorkspaceFilesResponseSchema as O, WorkspaceMarkdownPreviewTargetSchema as Ot, PendingInputRequestSchema as P, WorkspaceTerminalInputRequestSchema as Pt, RuntimePreferencesResponseSchema as Q, PromptContextSchema as R, WorkspaceTerminalSessionResponseSchema as Rt, KnownReasoningEffortSchema as S, WebPreviewTargetSchema as St, ListSkillsResponseSchema as T, WorkspaceFileContentResponseSchema as Tt, RateLimitWindowSchema as U, isPromptSkillMarkdownMention as Ut, QueuedThreadInputSchema as V, chatMessageDetailsFromPromptContext as Vt, RateLimitsResponseSchema as W, normalizePromptContext as Wt, RuntimeModeSchema as X, stripPromptSkillMentions as Xt, RunThreadResponseSchema as Y, promptSkillMentionTextCandidates as Yt, RuntimePreferencesByWorkspacePathSchema as Z, EncryptedPayloadSchema as _, UpdateThreadGoalRequestSchema as _t, ArchiveThreadResponseSchema as a, ThreadCollaborationModeSchema as at, InterruptThreadRunResponseSchema as b, WORKSPACE_PREVIEW_OPEN_PROTOCOL as bt, ChatMessageRoleSchema as c, ThreadGoalResponseSchema as ct, CheckoutWorkspaceBranchRequestSchema as d, ThreadMessageDetailFieldSchema as dt, SandboxModeSchema as et, CodexModelSchema as f, ThreadMessageDetailResponseSchema as ft, CreateThreadResponseSchema as g, UpdateRuntimePreferencesRequestSchema as gt, CreateThreadRequestSchema as h, ThreadSummarySchema as ht, ApprovalModeSchema as i, SubmitThreadInputResponseSchema as it, PairResponseSchema as j, WorkspaceSelectionRequestSchema as jt, PairEncryptedPayloadSchema as k, WorkspacePreviewNavigationRequestSchema as kt, ChatMessageSchema as l, ThreadGoalSchema as lt, ContextWindowUsageSchema as m, ThreadStateSchema as mt, AgentSkillSourceSchema as n, StreamThreadRunEventSchema as nt, ChatMessageKindSchema as o, ThreadContextWindowResponseSchema as ot, CommitPushWorkspaceRequestSchema as p, ThreadRunOptionsSchema as pt, ResolveApprovalResponseSchema as q, promptSkillMentionLabel as qt, ApprovalDecisionSchema as r, StreamThreadRunRequestSchema as rt, ChatMessagePromptDetailsSchema as s, ThreadDetailResponseSchema as st, AgentSkillSchema as t, StatusResponseSchema as tt, ChatMessageStateSchema as u, ThreadGoalStatusSchema as ut, ErrorResponseSchema as v, UpdateWorkspaceFileContentRequestSchema as vt, ListQueuedThreadInputsResponseSchema as w, WorkspaceDirectoryEntrySchema as wt, IsoDateTimeSchema as x, WORKSPACE_PREVIEW_TAB_VALUES as xt, ImageAttachmentUploadResponseSchema as y, VersionResponseSchema as yt, PromptSkillSchema as z, WorkspaceTerminalStartRequestSchema as zt };
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 { Bt as apiPaths } from "./api-schema2.js";
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";
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 { $ as RuntimePreferencesSchema, A as PairRequestSchema, B as QueuedThreadInputActionResponseSchema, Bt as apiPaths, C as ListModelsResponseSchema, Ct as WorkspaceChangesResponseSchema, D as ListWorkspaceDirectoriesResponseSchema, Dt as WorkspaceGitActionResponseSchema, E as ListThreadsResponseSchema, G as ReasoningEffortSchema, Gt as promptMarkdownWithSkills, Ht as createOpenApiDocument, It as WorkspaceTerminalOutputResponseSchema, J as RunThreadRequestSchema, K as ResolveApprovalRequestSchema, Lt as WorkspaceTerminalResizeRequestSchema, Mt as WorkspaceTailscaleServeRequestSchema, Nt as WorkspaceTailscaleServeResponseSchema, O as ListWorkspaceFilesResponseSchema, Pt as WorkspaceTerminalInputRequestSchema, Q as RuntimePreferencesResponseSchema, Rt as WorkspaceTerminalSessionResponseSchema, S as KnownReasoningEffortSchema, T as ListSkillsResponseSchema, Tt as WorkspaceFileContentResponseSchema, Vt as chatMessageDetailsFromPromptContext, W as RateLimitsResponseSchema, Wt as normalizePromptContext, Xt as stripPromptSkillMentions, _ as EncryptedPayloadSchema, _t as UpdateThreadGoalRequestSchema, a as ArchiveThreadResponseSchema, b as InterruptThreadRunResponseSchema, ct as ThreadGoalResponseSchema, d as CheckoutWorkspaceBranchRequestSchema, dt as ThreadMessageDetailFieldSchema, ft as ThreadMessageDetailResponseSchema, gt as UpdateRuntimePreferencesRequestSchema, h as CreateThreadRequestSchema, ht as ThreadSummarySchema, it as SubmitThreadInputResponseSchema, j as PairResponseSchema, l as ChatMessageSchema, lt as ThreadGoalSchema, m as ContextWindowUsageSchema, nt as StreamThreadRunEventSchema, ot as ThreadContextWindowResponseSchema, p as CommitPushWorkspaceRequestSchema, q as ResolveApprovalResponseSchema, rt as StreamThreadRunRequestSchema, st as ThreadDetailResponseSchema, tt as StatusResponseSchema, ut as ThreadGoalStatusSchema, vt as UpdateWorkspaceFileContentRequestSchema, w as ListQueuedThreadInputsResponseSchema, y as ImageAttachmentUploadResponseSchema, yt as VersionResponseSchema, zt as WorkspaceTerminalStartRequestSchema } from "./api-schema2.js";
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";
@@ -19,6 +19,7 @@ import { openRepository } from "es-git";
19
19
  import { Hono } from "hono";
20
20
  import { cors } from "hono/cors";
21
21
  import * as pty from "@lydell/node-pty";
22
+ import { connect } from "node:net";
22
23
  import { createInterface } from "node:readline";
23
24
  import WebSocket from "ws";
24
25
  import { Codex } from "@openai/codex-sdk";
@@ -33,11 +34,7 @@ const stdioAppServerArgs = [
33
34
  "--listen",
34
35
  "stdio://"
35
36
  ];
36
- const sharedServerArgs = [
37
- "app-server",
38
- "--listen",
39
- "unix://"
40
- ];
37
+ const sharedWindowsServerUrl = "ws://127.0.0.1:8788";
41
38
  const windowsShellExtensions = new Set([".bat", ".cmd"]);
42
39
  function resolveCodexAppServerMode(env = process.env) {
43
40
  const configuredMode = env.CODEX_RELAY_APP_SERVER_MODE?.trim().toLowerCase();
@@ -46,9 +43,9 @@ function resolveCodexAppServerMode(env = process.env) {
46
43
  throw new Error(`Unsupported CODEX_RELAY_APP_SERVER_MODE ${JSON.stringify(configuredMode)}. Expected "stdio" or "socket".`);
47
44
  }
48
45
  function resolveCodexAppServerSpawn(input = {}) {
49
- const platform$1 = input.platform ?? platform();
46
+ const platform$2 = input.platform ?? platform();
50
47
  const command = resolveCodexBinary(input.env ?? process.env);
51
- const isWindows = platform$1 === "win32";
48
+ const isWindows = platform$2 === "win32";
52
49
  return {
53
50
  command,
54
51
  args: [...stdioAppServerArgs],
@@ -57,19 +54,22 @@ function resolveCodexAppServerSpawn(input = {}) {
57
54
  };
58
55
  }
59
56
  function resolveCodexSharedAppServerSpawn(input = {}) {
60
- const platform$2 = input.platform ?? platform();
61
- assertAppServerModeSupported("socket", platform$2);
57
+ const platform$3 = input.platform ?? platform();
62
58
  const command = resolveCodexBinary(input.env ?? process.env);
63
- const isWindows = platform$2 === "win32";
59
+ const isWindows = platform$3 === "win32";
64
60
  return {
65
61
  command,
66
- args: [...sharedServerArgs],
62
+ args: [
63
+ "app-server",
64
+ "--listen",
65
+ resolveCodexSharedAppServerRemoteAddress(platform$3)
66
+ ],
67
67
  shell: isWindows && shouldUseWindowsShell(command),
68
68
  windowsHide: isWindows
69
69
  };
70
70
  }
71
- function assertAppServerModeSupported(mode, platform) {
72
- if (mode === "socket" && platform === "win32") throw new Error("Shared Codex app-server mode requires macOS, Linux, or WSL.");
71
+ function resolveCodexSharedAppServerRemoteAddress(platform$1 = platform()) {
72
+ return platform$1 === "win32" ? sharedWindowsServerUrl : "unix://";
73
73
  }
74
74
  function resolveCodexBinary(env) {
75
75
  return env.CODEX_BIN?.trim() || "codex";
@@ -79,17 +79,68 @@ function shouldUseWindowsShell(command) {
79
79
  return extension === "" || windowsShellExtensions.has(extension);
80
80
  }
81
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
82
119
  //#region src/app-server.ts
120
+ const sharedSocketReconnectDelaysMs = [
121
+ 50,
122
+ 100,
123
+ 250,
124
+ 500,
125
+ 1e3,
126
+ 2e3
127
+ ];
83
128
  var CodexAppServerClient = class {
84
129
  child;
130
+ closed = false;
85
131
  initialized;
86
132
  notificationHandlers = /* @__PURE__ */ new Set();
87
133
  nextId = 1;
88
134
  pending = /* @__PURE__ */ new Map();
135
+ reconnecting;
89
136
  requestHandlers = /* @__PURE__ */ new Set();
90
137
  readline;
91
138
  sharedServer;
92
139
  socket;
140
+ startSharedServer;
141
+ constructor(options = {}) {
142
+ this.startSharedServer = options.startSharedServer ?? startSharedCodexAppServer;
143
+ }
93
144
  initialize() {
94
145
  return this.ensureInitialized();
95
146
  }
@@ -165,15 +216,18 @@ var CodexAppServerClient = class {
165
216
  });
166
217
  }
167
218
  close() {
219
+ if (this.closed) return;
220
+ this.closed = true;
168
221
  for (const pending of this.pending.values()) pending.reject(/* @__PURE__ */ new Error("Codex app-server was closed."));
169
222
  this.pending.clear();
170
223
  this.readline?.close();
171
224
  this.child?.kill();
172
- this.socket?.close();
225
+ const socket = this.socket;
226
+ this.socket = void 0;
227
+ socket?.close();
173
228
  this.sharedServer?.kill();
174
229
  this.readline = void 0;
175
230
  this.child = void 0;
176
- this.socket = void 0;
177
231
  this.sharedServer = void 0;
178
232
  this.initialized = void 0;
179
233
  }
@@ -197,35 +251,37 @@ var CodexAppServerClient = class {
197
251
  });
198
252
  }
199
253
  ensureInitialized() {
200
- if (!this.initialized) this.initialized = this.start();
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
+ }
201
262
  return this.initialized;
202
263
  }
203
264
  async start() {
265
+ const mode = resolveCodexAppServerMode();
204
266
  try {
205
- if (resolveCodexAppServerMode() === "socket") {
206
- this.sharedServer = await startSharedCodexAppServer();
207
- await this.connectSharedCodexAppServer();
208
- } else this.startStdioCodexAppServer();
209
- await this.requestRaw("initialize", {
210
- clientInfo: {
211
- name: "codex-relay",
212
- title: "Codex Relay Mobile Server",
213
- version: "1.2.0"
214
- },
215
- capabilities: { experimentalApi: true }
216
- });
267
+ if (mode === "socket") await this.startOrAttachSharedCodexAppServer();
268
+ else this.startStdioCodexAppServer();
269
+ await this.initializeAppServer();
217
270
  } catch (error) {
218
- this.readline?.close();
219
- this.readline = void 0;
220
- this.child?.kill();
221
- this.child = void 0;
222
- this.socket?.close();
223
- this.socket = void 0;
224
- this.sharedServer?.kill();
225
- this.sharedServer = void 0;
271
+ if (mode === "stdio") this.stopStdioCodexAppServer();
226
272
  throw error;
227
273
  }
228
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
+ }
229
285
  startStdioCodexAppServer() {
230
286
  const spawnConfig = resolveCodexAppServerSpawn();
231
287
  this.child = spawn(spawnConfig.command, spawnConfig.args, {
@@ -248,31 +304,153 @@ var CodexAppServerClient = class {
248
304
  this.initialized = void 0;
249
305
  });
250
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
+ }
251
365
  async connectSharedCodexAppServer() {
252
- const socket = new WebSocket(`ws+unix://${sharedCodexAppServerSocketPath()}:/`, { perMessageDeflate: false });
366
+ const remoteAddress = resolveCodexSharedAppServerRemoteAddress();
367
+ const socket = new WebSocket(remoteAddress === "unix://" ? `ws+unix://${sharedCodexAppServerSocketPath()}:/` : remoteAddress, { perMessageDeflate: false });
253
368
  this.socket = socket;
254
- await new Promise((resolve, reject) => {
255
- const handleOpen = () => {
256
- socket.off("error", handleInitialError);
257
- resolve();
258
- };
259
- const handleInitialError = (error) => {
260
- socket.off("open", handleOpen);
261
- reject(error);
262
- };
263
- socket.once("open", handleOpen);
264
- socket.once("error", handleInitialError);
265
- });
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
+ }
266
387
  socket.on("message", (data) => this.handleLine(String(data)));
267
- socket.on("error", (error) => this.rejectAll(error));
388
+ socket.on("error", (error) => this.handleSharedSocketFailure(socket, error));
268
389
  socket.once("close", (code, reason) => {
269
- this.rejectAll(/* @__PURE__ */ new Error(`Codex app-server socket closed with ${code}${reason.length > 0 ? `: ${String(reason)}` : "."}`));
270
- this.socket = void 0;
271
- this.sharedServer?.kill();
272
- this.sharedServer = void 0;
273
- this.initialized = void 0;
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()
274
395
  });
275
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.");
453
+ }
276
454
  requestRaw(method, params) {
277
455
  const id = this.nextId++;
278
456
  const request = JSON.stringify({
@@ -376,11 +554,11 @@ async function startSharedCodexAppServer() {
376
554
  child.once("exit", (code, signal) => {
377
555
  exitReason = signal ?? code ?? 1;
378
556
  });
379
- const socketPath = sharedCodexAppServerSocketPath();
557
+ const remoteAddress = resolveCodexSharedAppServerRemoteAddress();
380
558
  for (let attempt = 0; attempt < 100; attempt += 1) {
381
559
  if (spawnError) throw new Error(`Failed to start the shared Codex app-server: ${spawnError.message}`);
382
560
  try {
383
- await access(socketPath);
561
+ await waitForSharedCodexAppServer(remoteAddress);
384
562
  return child;
385
563
  } catch {
386
564
  if (exitReason !== void 0) {
@@ -391,11 +569,34 @@ async function startSharedCodexAppServer() {
391
569
  }
392
570
  }
393
571
  child.kill();
394
- throw new Error(`Timed out waiting for the shared Codex app-server socket at ${socketPath}.`);
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
+ });
395
593
  }
396
594
  function sharedCodexAppServerSocketPath() {
397
595
  return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "app-server-control", "app-server-control.sock");
398
596
  }
597
+ function asError(error) {
598
+ return error instanceof Error ? error : new Error(String(error));
599
+ }
399
600
  function debugAppServer(kind, method, id, detail) {
400
601
  if (process.env.CODEX_RELAY_DEBUG_APP_SERVER !== "1") return;
401
602
  console.log(`[app-server] ${kind}${id === void 0 ? "" : ` #${id}`}${method ? ` ${method}` : ""}${detail ? ` ${detail}` : ""}`);
@@ -585,41 +786,79 @@ function sumPositiveIntegers(values) {
585
786
  return found ? sum : null;
586
787
  }
587
788
  //#endregion
588
- //#region src/debug-log.ts
589
- function isRelayDebugEnabled() {
590
- return process.env.CODEX_RELAY_DEBUG === "1" || process.env.CODEX_RELAY_DEBUG_STREAM === "1";
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
+ } };
591
822
  }
592
- function relayDebugLog(event, fields = {}) {
593
- if (!isRelayDebugEnabled()) return;
594
- const entry = sanitizeDebugValue({
595
- ts: (/* @__PURE__ */ new Date()).toISOString(),
596
- event,
597
- ...fields
598
- });
599
- const line = `${JSON.stringify(entry)}\n`;
600
- const path = process.env.CODEX_RELAY_DEBUG_LOG_PATH?.trim();
601
- if (path) mkdir(dirname(path), { recursive: true }).then(() => appendFile(path, line, { mode: 384 })).catch(() => void 0);
602
- 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
+ } };
603
831
  }
604
- function sanitizeDebugValue(value) {
605
- if (value instanceof Error) return {
606
- message: value.message,
607
- name: value.name,
608
- stack: value.stack
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
609
845
  };
610
- if (Array.isArray(value)) return value.map(sanitizeDebugValue);
611
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitizeDebugValue(item)]));
612
- return value;
613
846
  }
614
- function formatDebugFields(fields) {
615
- return Object.entries(fields).map(([key, value]) => `${key}=${formatDebugValue(value)}`).join(" ");
847
+ function expoPushTickets(value) {
848
+ if (!value || typeof value !== "object") return [];
849
+ const data = value.data;
850
+ return Array.isArray(data) ? data : [];
616
851
  }
617
- function formatDebugValue(value) {
618
- if (value === void 0) return "undefined";
619
- if (value === null) return "null";
620
- if (typeof value === "string") return value.includes(" ") ? JSON.stringify(value) : value;
621
- if (typeof value === "number" || typeof value === "boolean") return String(value);
622
- return JSON.stringify(sanitizeDebugValue(value));
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;
623
862
  }
624
863
  //#endregion
625
864
  //#region src/preferences-store.ts
@@ -1186,6 +1425,11 @@ function createApp(options = {}) {
1186
1425
  const secureSessionsByTokenHash = /* @__PURE__ */ new Map();
1187
1426
  const activeStreamControllers = /* @__PURE__ */ new Set();
1188
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);
1189
1433
  const scheduleAppServerHistoryLoad = (threadId, cachedMessages) => {
1190
1434
  if (!appServer || appServerHistoryLoadsByThreadId.has(threadId)) return;
1191
1435
  const startedAt = Date.now();
@@ -1410,6 +1654,48 @@ function createApp(options = {}) {
1410
1654
  });
1411
1655
  return secureJson(c, options.pairing, secureSessionsByTokenHash, response);
1412
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
+ });
1413
1699
  app.get(apiPaths.workspaceDirectories, async (c) => {
1414
1700
  const targetPath = resolve(c.req.query("path") ?? workspacePath);
1415
1701
  try {
@@ -2406,6 +2692,18 @@ function createApp(options = {}) {
2406
2692
  function parseBearerToken(value) {
2407
2693
  return (value?.match(/^Bearer\s+(.+)$/i))?.[1];
2408
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
+ }
2409
2707
  function normalizeApprovalCode(value) {
2410
2708
  const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "").replaceAll("O", "0").replaceAll("I", "1");
2411
2709
  return normalized.length === 8 ? `${normalized.slice(0, 4)}-${normalized.slice(4)}` : normalized;
@@ -5421,6 +5719,55 @@ function structuredStreamMessage(role, event, fallbackContent) {
5421
5719
  function isApprovalServerRequest(method) {
5422
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";
5423
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
+ }
5424
5771
  function approvalMessageFromRequest(request) {
5425
5772
  const params = recordParams(request);
5426
5773
  const threadId = firstString(params, ["threadId", "conversationId"]);
@@ -6480,7 +6827,7 @@ serve({
6480
6827
  listenUrl,
6481
6828
  pairingPayload,
6482
6829
  port: info.port,
6483
- sharedAppServer: appServerMode === "socket"
6830
+ sharedAppServerRemoteAddress: appServerMode === "socket" ? resolveCodexSharedAppServerRemoteAddress() : void 0
6484
6831
  }));
6485
6832
  });
6486
6833
  function stopSharedAppServer(exitCode) {
@@ -6499,9 +6846,9 @@ function formatStartupInstructions(details) {
6499
6846
  `${color.prompt("›")} Server: ${color.muted(details.listenUrl)}`,
6500
6847
  "",
6501
6848
  `${color.prompt("›")} Pairing: ${color.url(details.pairingPayload)}`,
6502
- ...details.sharedAppServer ? [
6849
+ ...details.sharedAppServerRemoteAddress ? [
6503
6850
  "",
6504
- `${color.prompt("›")} Terminal: ${color.command("codex resume --remote unix://")}`,
6851
+ `${color.prompt("›")} Terminal: ${color.command(`codex resume --remote ${details.sharedAppServerRemoteAddress}`)}`,
6505
6852
  ` ${color.muted("Connect through the shared Codex app-server to follow and steer the same live sessions.")}`
6506
6853
  ] : [],
6507
6854
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-relay",
3
- "version": "1.3.2",
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": {
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"],