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 +14 -4
- package/dist/api-schema.js +2 -2
- package/dist/api-schema2.js +76 -1
- package/dist/cli.js +1 -1
- package/dist/paths.js +78 -0
- package/dist/src.js +439 -92
- package/package.json +1 -1
- package/src/api-schema.ts +81 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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";
|
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";
|
|
@@ -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
|
|
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$
|
|
46
|
+
const platform$2 = input.platform ?? platform();
|
|
50
47
|
const command = resolveCodexBinary(input.env ?? process.env);
|
|
51
|
-
const isWindows = platform$
|
|
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$
|
|
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$
|
|
59
|
+
const isWindows = platform$3 === "win32";
|
|
64
60
|
return {
|
|
65
61
|
command,
|
|
66
|
-
args: [
|
|
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
|
|
72
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
206
|
-
|
|
207
|
-
|
|
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.
|
|
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
|
|
366
|
+
const remoteAddress = resolveCodexSharedAppServerRemoteAddress();
|
|
367
|
+
const socket = new WebSocket(remoteAddress === "unix://" ? `ws+unix://${sharedCodexAppServerSocketPath()}:/` : remoteAddress, { perMessageDeflate: false });
|
|
253
368
|
this.socket = socket;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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.
|
|
388
|
+
socket.on("error", (error) => this.handleSharedSocketFailure(socket, error));
|
|
268
389
|
socket.once("close", (code, reason) => {
|
|
269
|
-
this.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.
|
|
273
|
-
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
...
|
|
598
|
-
|
|
599
|
-
|
|
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
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
|
615
|
-
|
|
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
|
|
618
|
-
if (
|
|
619
|
-
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
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.
|
|
6849
|
+
...details.sharedAppServerRemoteAddress ? [
|
|
6503
6850
|
"",
|
|
6504
|
-
`${color.prompt("›")} Terminal: ${color.command(
|
|
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
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"],
|