remodex-windows-fix 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +183 -0
- package/bin/phodex.js +8 -0
- package/bin/remodex-relay.js +19 -0
- package/bin/remodex.js +44 -0
- package/cloudflare/README.md +16 -0
- package/cloudflare/worker.mjs +221 -0
- package/package.json +48 -0
- package/relay/README.md +39 -0
- package/relay/relay.js +199 -0
- package/render.yaml +10 -0
- package/src/bridge.js +330 -0
- package/src/codex-desktop-refresher.js +338 -0
- package/src/codex-transport.js +277 -0
- package/src/git-handler.js +617 -0
- package/src/index.js +11 -0
- package/src/qr.js +21 -0
- package/src/relay-server.js +61 -0
- package/src/rollout-watch.js +176 -0
- package/src/scripts/codex-refresh.applescript +51 -0
- package/src/session-state.js +57 -0
- package/src/workspace-paths.js +181 -0
- package/wrangler.toml +13 -0
package/relay/relay.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// FILE: relay.js
|
|
2
|
+
// Purpose: Thin WebSocket relay used by the default hosted Remodex pairing flow.
|
|
3
|
+
// Layer: Standalone server module
|
|
4
|
+
// Exports: setupRelay, getRelayStats
|
|
5
|
+
|
|
6
|
+
const { WebSocket } = require("ws");
|
|
7
|
+
|
|
8
|
+
const MAX_HISTORY = 500;
|
|
9
|
+
const CLEANUP_DELAY_MS = 60_000;
|
|
10
|
+
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
11
|
+
const CLOSE_CODE_SESSION_UNAVAILABLE = 4002;
|
|
12
|
+
const CLOSE_CODE_IPHONE_REPLACED = 4003;
|
|
13
|
+
|
|
14
|
+
// In-memory session registry for one Mac host and one live iPhone client per session.
|
|
15
|
+
const sessions = new Map();
|
|
16
|
+
|
|
17
|
+
// Attaches relay behavior to a ws WebSocketServer instance.
|
|
18
|
+
function setupRelay(wss) {
|
|
19
|
+
const heartbeat = setInterval(() => {
|
|
20
|
+
for (const ws of wss.clients) {
|
|
21
|
+
if (ws._relayAlive === false) {
|
|
22
|
+
ws.terminate();
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
ws._relayAlive = false;
|
|
26
|
+
ws.ping();
|
|
27
|
+
}
|
|
28
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
29
|
+
|
|
30
|
+
wss.on("close", () => clearInterval(heartbeat));
|
|
31
|
+
|
|
32
|
+
wss.on("connection", (ws, req) => {
|
|
33
|
+
const urlPath = req.url || "";
|
|
34
|
+
const match = urlPath.match(/^\/relay\/([^/?]+)/);
|
|
35
|
+
const sessionId = match?.[1];
|
|
36
|
+
const role = req.headers["x-role"];
|
|
37
|
+
|
|
38
|
+
if (!sessionId || (role !== "mac" && role !== "iphone")) {
|
|
39
|
+
ws.close(4000, "Missing sessionId or invalid x-role header");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ws._relayAlive = true;
|
|
44
|
+
ws.on("pong", () => {
|
|
45
|
+
ws._relayAlive = true;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Only the Mac host is allowed to create a fresh session room.
|
|
49
|
+
if (role === "iphone" && !sessions.has(sessionId)) {
|
|
50
|
+
ws.close(CLOSE_CODE_SESSION_UNAVAILABLE, "Mac session not available");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!sessions.has(sessionId)) {
|
|
55
|
+
sessions.set(sessionId, {
|
|
56
|
+
mac: null,
|
|
57
|
+
clients: new Set(),
|
|
58
|
+
history: [],
|
|
59
|
+
cleanupTimer: null,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const session = sessions.get(sessionId);
|
|
64
|
+
|
|
65
|
+
if (role === "iphone" && session.mac?.readyState !== WebSocket.OPEN) {
|
|
66
|
+
ws.close(CLOSE_CODE_SESSION_UNAVAILABLE, "Mac session not available");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (session.cleanupTimer) {
|
|
71
|
+
clearTimeout(session.cleanupTimer);
|
|
72
|
+
session.cleanupTimer = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (role === "mac") {
|
|
76
|
+
if (session.mac && session.mac.readyState === WebSocket.OPEN) {
|
|
77
|
+
session.mac.close(4001, "Replaced by new Mac connection");
|
|
78
|
+
}
|
|
79
|
+
session.mac = ws;
|
|
80
|
+
console.log(`[relay] Mac connected -> session ${sessionId}`);
|
|
81
|
+
} else {
|
|
82
|
+
// Keep one live iPhone RPC client per session to avoid competing sockets.
|
|
83
|
+
for (const existingClient of session.clients) {
|
|
84
|
+
if (existingClient === ws) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
existingClient.readyState === WebSocket.OPEN
|
|
89
|
+
|| existingClient.readyState === WebSocket.CONNECTING
|
|
90
|
+
) {
|
|
91
|
+
existingClient.close(
|
|
92
|
+
CLOSE_CODE_IPHONE_REPLACED,
|
|
93
|
+
"Replaced by newer iPhone connection"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
session.clients.delete(existingClient);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
session.clients.add(ws);
|
|
100
|
+
console.log(
|
|
101
|
+
`[relay] iPhone connected -> session ${sessionId} (${session.clients.size} client(s))`
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// Replay recent host output so a reconnecting client can catch up.
|
|
105
|
+
for (const msg of session.history) {
|
|
106
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
107
|
+
ws.send(msg);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ws.on("message", (data) => {
|
|
113
|
+
const msg = typeof data === "string" ? data : data.toString("utf-8");
|
|
114
|
+
|
|
115
|
+
if (role === "mac") {
|
|
116
|
+
session.history.push(msg);
|
|
117
|
+
if (session.history.length > MAX_HISTORY) {
|
|
118
|
+
session.history.shift();
|
|
119
|
+
}
|
|
120
|
+
for (const client of session.clients) {
|
|
121
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
122
|
+
client.send(msg);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} else if (session.mac?.readyState === WebSocket.OPEN) {
|
|
126
|
+
session.mac.send(msg);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
ws.on("close", () => {
|
|
131
|
+
if (role === "mac") {
|
|
132
|
+
if (session.mac === ws) {
|
|
133
|
+
session.mac = null;
|
|
134
|
+
console.log(`[relay] Mac disconnected -> session ${sessionId}`);
|
|
135
|
+
for (const client of session.clients) {
|
|
136
|
+
if (client.readyState === WebSocket.OPEN || client.readyState === WebSocket.CONNECTING) {
|
|
137
|
+
client.close(CLOSE_CODE_SESSION_UNAVAILABLE, "Mac disconnected");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
session.clients.delete(ws);
|
|
143
|
+
console.log(
|
|
144
|
+
`[relay] iPhone disconnected -> session ${sessionId} (${session.clients.size} remaining)`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
scheduleCleanup(sessionId);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
ws.on("error", (err) => {
|
|
151
|
+
console.error(
|
|
152
|
+
`[relay] WebSocket error (${role}, session ${sessionId}):`,
|
|
153
|
+
err.message
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function scheduleCleanup(sessionId) {
|
|
160
|
+
const session = sessions.get(sessionId);
|
|
161
|
+
if (!session) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (session.mac || session.clients.size > 0 || session.cleanupTimer) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
session.cleanupTimer = setTimeout(() => {
|
|
169
|
+
const activeSession = sessions.get(sessionId);
|
|
170
|
+
if (activeSession && !activeSession.mac && activeSession.clients.size === 0) {
|
|
171
|
+
sessions.delete(sessionId);
|
|
172
|
+
console.log(`[relay] Session ${sessionId} cleaned up`);
|
|
173
|
+
}
|
|
174
|
+
}, CLEANUP_DELAY_MS);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Exposes lightweight runtime stats for health/status endpoints.
|
|
178
|
+
function getRelayStats() {
|
|
179
|
+
let totalClients = 0;
|
|
180
|
+
let sessionsWithMac = 0;
|
|
181
|
+
|
|
182
|
+
for (const session of sessions.values()) {
|
|
183
|
+
totalClients += session.clients.size;
|
|
184
|
+
if (session.mac) {
|
|
185
|
+
sessionsWithMac += 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
activeSessions: sessions.size,
|
|
191
|
+
sessionsWithMac,
|
|
192
|
+
totalClients,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
setupRelay,
|
|
198
|
+
getRelayStats,
|
|
199
|
+
};
|
package/render.yaml
ADDED
package/src/bridge.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// FILE: bridge.js
|
|
2
|
+
// Purpose: Runs Codex locally, bridges relay traffic, and coordinates desktop refreshes for Codex.app.
|
|
3
|
+
// Layer: CLI service
|
|
4
|
+
// Exports: startBridge
|
|
5
|
+
// Depends on: ws, uuid, ./qr, ./codex-desktop-refresher, ./codex-transport
|
|
6
|
+
|
|
7
|
+
const WebSocket = require("ws");
|
|
8
|
+
const { v4: uuidv4 } = require("uuid");
|
|
9
|
+
const {
|
|
10
|
+
CodexDesktopRefresher,
|
|
11
|
+
readBridgeConfig,
|
|
12
|
+
} = require("./codex-desktop-refresher");
|
|
13
|
+
const { createCodexTransport } = require("./codex-transport");
|
|
14
|
+
const {
|
|
15
|
+
registerWorkspacePath,
|
|
16
|
+
registerWorkspacePathsFromMessage,
|
|
17
|
+
restoreWorkspacePathsFromDisplay,
|
|
18
|
+
rewriteWorkspacePathsForDisplay,
|
|
19
|
+
} = require("./workspace-paths");
|
|
20
|
+
const { printQR } = require("./qr");
|
|
21
|
+
const { rememberActiveThread } = require("./session-state");
|
|
22
|
+
const { handleGitRequest } = require("./git-handler");
|
|
23
|
+
|
|
24
|
+
function startBridge() {
|
|
25
|
+
const config = readBridgeConfig();
|
|
26
|
+
registerWorkspacePath(process.cwd());
|
|
27
|
+
const sessionId = uuidv4();
|
|
28
|
+
const relayBaseUrl = config.relayUrl.replace(/\/+$/, "");
|
|
29
|
+
const relaySessionUrl = `${relayBaseUrl}/${sessionId}`;
|
|
30
|
+
const desktopRefresher = new CodexDesktopRefresher({
|
|
31
|
+
enabled: config.refreshEnabled,
|
|
32
|
+
debounceMs: config.refreshDebounceMs,
|
|
33
|
+
refreshCommand: config.refreshCommand,
|
|
34
|
+
bundleId: config.codexBundleId,
|
|
35
|
+
appPath: config.codexAppPath,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Keep the local Codex runtime alive across transient relay disconnects.
|
|
39
|
+
let socket = null;
|
|
40
|
+
let isShuttingDown = false;
|
|
41
|
+
let reconnectAttempt = 0;
|
|
42
|
+
let reconnectTimer = null;
|
|
43
|
+
let lastConnectionStatus = null;
|
|
44
|
+
let codexHandshakeState = config.codexEndpoint ? "warm" : "cold";
|
|
45
|
+
const forwardedInitializeRequestIds = new Set();
|
|
46
|
+
const pendingCodexMessages = [];
|
|
47
|
+
|
|
48
|
+
const codex = createCodexTransport({
|
|
49
|
+
endpoint: config.codexEndpoint,
|
|
50
|
+
env: process.env,
|
|
51
|
+
logPrefix: "[remodex]",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
codex.onError((error) => {
|
|
55
|
+
if (config.codexEndpoint) {
|
|
56
|
+
console.error(`[remodex] Failed to connect to Codex endpoint: ${config.codexEndpoint}`);
|
|
57
|
+
} else {
|
|
58
|
+
console.error("[remodex] Failed to start `codex app-server`.");
|
|
59
|
+
console.error("[remodex] Make sure the `codex` CLI is installed and available in PATH.");
|
|
60
|
+
}
|
|
61
|
+
console.error(error.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function clearReconnectTimer() {
|
|
66
|
+
if (!reconnectTimer) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
clearTimeout(reconnectTimer);
|
|
71
|
+
reconnectTimer = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Keeps npm start output compact by emitting only high-signal connection states.
|
|
75
|
+
function logConnectionStatus(status) {
|
|
76
|
+
if (lastConnectionStatus === status) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
lastConnectionStatus = status;
|
|
81
|
+
console.log(`[remodex] ${status}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Retries the relay socket while preserving the active Codex process and session id.
|
|
85
|
+
function scheduleRelayReconnect(closeCode) {
|
|
86
|
+
if (isShuttingDown) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (closeCode === 4000 || closeCode === 4001) {
|
|
91
|
+
logConnectionStatus("disconnected");
|
|
92
|
+
shutdown(codex, () => socket, () => {
|
|
93
|
+
isShuttingDown = true;
|
|
94
|
+
clearReconnectTimer();
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (reconnectTimer) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
reconnectAttempt += 1;
|
|
104
|
+
const delayMs = Math.min(1_000 * reconnectAttempt, 5_000);
|
|
105
|
+
logConnectionStatus("connecting");
|
|
106
|
+
reconnectTimer = setTimeout(() => {
|
|
107
|
+
reconnectTimer = null;
|
|
108
|
+
connectRelay();
|
|
109
|
+
}, delayMs);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function connectRelay() {
|
|
113
|
+
if (isShuttingDown) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logConnectionStatus("connecting");
|
|
118
|
+
const nextSocket = new WebSocket(relaySessionUrl, {
|
|
119
|
+
headers: { "x-role": "mac" },
|
|
120
|
+
});
|
|
121
|
+
socket = nextSocket;
|
|
122
|
+
|
|
123
|
+
nextSocket.on("open", () => {
|
|
124
|
+
clearReconnectTimer();
|
|
125
|
+
reconnectAttempt = 0;
|
|
126
|
+
logConnectionStatus("connected");
|
|
127
|
+
while (pendingCodexMessages.length > 0) {
|
|
128
|
+
const buffered = pendingCodexMessages.shift();
|
|
129
|
+
nextSocket.send(buffered);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
nextSocket.on("message", (data) => {
|
|
134
|
+
const message = typeof data === "string" ? data : data.toString("utf8");
|
|
135
|
+
const normalizedMessage = restoreWorkspacePathsFromDisplay(message);
|
|
136
|
+
if (handleBridgeManagedHandshakeMessage(normalizedMessage, nextSocket)) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (handleGitRequest(normalizedMessage, (response) => nextSocket.send(response))) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
desktopRefresher.handleInbound(normalizedMessage);
|
|
143
|
+
rememberThreadFromMessage("phone", normalizedMessage);
|
|
144
|
+
codex.send(normalizedMessage);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
nextSocket.on("close", (code) => {
|
|
148
|
+
logConnectionStatus("disconnected");
|
|
149
|
+
if (socket === nextSocket) {
|
|
150
|
+
socket = null;
|
|
151
|
+
}
|
|
152
|
+
scheduleRelayReconnect(code);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
nextSocket.on("error", () => {
|
|
156
|
+
logConnectionStatus("disconnected");
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
printQR(sessionId, relayBaseUrl);
|
|
161
|
+
connectRelay();
|
|
162
|
+
|
|
163
|
+
codex.onMessage((message) => {
|
|
164
|
+
trackCodexHandshakeState(message);
|
|
165
|
+
desktopRefresher.handleOutbound(message);
|
|
166
|
+
rememberThreadFromMessage("codex", message);
|
|
167
|
+
registerWorkspacePathsFromMessage(message);
|
|
168
|
+
const forwardedMessage = rewriteWorkspacePathsForDisplay(message);
|
|
169
|
+
if (socket?.readyState === WebSocket.OPEN) {
|
|
170
|
+
socket.send(forwardedMessage);
|
|
171
|
+
} else {
|
|
172
|
+
pendingCodexMessages.push(forwardedMessage);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
codex.onClose(() => {
|
|
177
|
+
logConnectionStatus("disconnected");
|
|
178
|
+
isShuttingDown = true;
|
|
179
|
+
clearReconnectTimer();
|
|
180
|
+
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
|
|
181
|
+
socket.close();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
process.on("SIGINT", () => shutdown(codex, () => socket, () => {
|
|
186
|
+
isShuttingDown = true;
|
|
187
|
+
clearReconnectTimer();
|
|
188
|
+
}));
|
|
189
|
+
process.on("SIGTERM", () => shutdown(codex, () => socket, () => {
|
|
190
|
+
isShuttingDown = true;
|
|
191
|
+
clearReconnectTimer();
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
function rememberThreadFromMessage(source, rawMessage) {
|
|
195
|
+
const threadId = extractThreadId(rawMessage);
|
|
196
|
+
if (!threadId) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
rememberActiveThread(threadId, source);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// The spawned/shared Codex app-server stays warm across phone reconnects.
|
|
204
|
+
// When iPhone reconnects it sends initialize again, but forwarding that to the
|
|
205
|
+
// already-initialized Codex transport only produces "Already initialized".
|
|
206
|
+
function handleBridgeManagedHandshakeMessage(rawMessage, relaySocket) {
|
|
207
|
+
let parsed = null;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(rawMessage);
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
|
|
215
|
+
if (!method) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (method === "initialize" && parsed.id != null) {
|
|
220
|
+
if (codexHandshakeState !== "warm") {
|
|
221
|
+
forwardedInitializeRequestIds.add(String(parsed.id));
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
relaySocket.send(JSON.stringify({
|
|
226
|
+
id: parsed.id,
|
|
227
|
+
result: {
|
|
228
|
+
bridgeManaged: true,
|
|
229
|
+
},
|
|
230
|
+
}));
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (method === "initialized") {
|
|
235
|
+
return codexHandshakeState === "warm";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Learns whether the underlying Codex transport has already completed its own MCP handshake.
|
|
242
|
+
function trackCodexHandshakeState(rawMessage) {
|
|
243
|
+
let parsed = null;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(rawMessage);
|
|
246
|
+
} catch {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const responseId = parsed?.id;
|
|
251
|
+
if (responseId == null) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const responseKey = String(responseId);
|
|
256
|
+
if (!forwardedInitializeRequestIds.has(responseKey)) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
forwardedInitializeRequestIds.delete(responseKey);
|
|
261
|
+
|
|
262
|
+
if (parsed?.result != null) {
|
|
263
|
+
codexHandshakeState = "warm";
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const errorMessage = typeof parsed?.error?.message === "string"
|
|
268
|
+
? parsed.error.message.toLowerCase()
|
|
269
|
+
: "";
|
|
270
|
+
if (errorMessage.includes("already initialized")) {
|
|
271
|
+
codexHandshakeState = "warm";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function shutdown(codex, getSocket, beforeExit = () => {}) {
|
|
277
|
+
beforeExit();
|
|
278
|
+
|
|
279
|
+
const socket = getSocket();
|
|
280
|
+
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
|
|
281
|
+
socket.close();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
codex.shutdown();
|
|
285
|
+
|
|
286
|
+
setTimeout(() => process.exit(0), 100);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function extractThreadId(rawMessage) {
|
|
290
|
+
let parsed = null;
|
|
291
|
+
try {
|
|
292
|
+
parsed = JSON.parse(rawMessage);
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const method = parsed?.method;
|
|
298
|
+
const params = parsed?.params;
|
|
299
|
+
|
|
300
|
+
if (method === "turn/start") {
|
|
301
|
+
return readString(params?.threadId) || readString(params?.thread_id);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (method === "thread/start" || method === "thread/started") {
|
|
305
|
+
return (
|
|
306
|
+
readString(params?.threadId)
|
|
307
|
+
|| readString(params?.thread_id)
|
|
308
|
+
|| readString(params?.thread?.id)
|
|
309
|
+
|| readString(params?.thread?.threadId)
|
|
310
|
+
|| readString(params?.thread?.thread_id)
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (method === "turn/completed") {
|
|
315
|
+
return (
|
|
316
|
+
readString(params?.threadId)
|
|
317
|
+
|| readString(params?.thread_id)
|
|
318
|
+
|| readString(params?.turn?.threadId)
|
|
319
|
+
|| readString(params?.turn?.thread_id)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function readString(value) {
|
|
327
|
+
return typeof value === "string" && value ? value : null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = { startBridge };
|