@btatum5/codex-bridge 0.1.0 → 1.3.3

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/src/index.js ADDED
@@ -0,0 +1,35 @@
1
+ // FILE: index.js
2
+ // Purpose: Small entrypoint wrapper for bridge lifecycle commands.
3
+ // Layer: CLI entry
4
+ // Exports: bridge lifecycle, pairing reset, thread resume/watch, and macOS service helpers.
5
+ // Depends on: ./bridge, ./secure-device-state, ./session-state, ./rollout-watch, ./macos-launch-agent
6
+
7
+ const { startBridge } = require("./bridge");
8
+ const { resetBridgeDeviceState } = require("./secure-device-state");
9
+ const { openLastActiveThread } = require("./session-state");
10
+ const { watchThreadRollout } = require("./rollout-watch");
11
+ const { readBridgeConfig } = require("./codex-desktop-refresher");
12
+ const {
13
+ getMacOSBridgeServiceStatus,
14
+ printMacOSBridgePairingQr,
15
+ printMacOSBridgeServiceStatus,
16
+ resetMacOSBridgePairing,
17
+ runMacOSBridgeService,
18
+ startMacOSBridgeService,
19
+ stopMacOSBridgeService,
20
+ } = require("./macos-launch-agent");
21
+
22
+ module.exports = {
23
+ getMacOSBridgeServiceStatus,
24
+ printMacOSBridgePairingQr,
25
+ printMacOSBridgeServiceStatus,
26
+ readBridgeConfig,
27
+ resetMacOSBridgePairing,
28
+ startBridge,
29
+ runMacOSBridgeService,
30
+ startMacOSBridgeService,
31
+ stopMacOSBridgeService,
32
+ resetBridgePairing: resetBridgeDeviceState,
33
+ openLastActiveThread,
34
+ watchThreadRollout,
35
+ };
@@ -0,0 +1,457 @@
1
+ // FILE: macos-launch-agent.js
2
+ // Purpose: Owns macOS-only launchd install/start/stop/status helpers for the background Codex bridge.
3
+ // Layer: CLI helper
4
+ // Exports: start/stop/status helpers plus the launchd service runner used by `codex-bridge up`.
5
+ // Depends on: child_process, fs, os, path, ./bridge, ./daemon-state, ./codex-desktop-refresher, ./qr, ./secure-device-state
6
+
7
+ const { execFileSync } = require("child_process");
8
+ const fs = require("fs");
9
+ const os = require("os");
10
+ const path = require("path");
11
+ const { startBridge } = require("./bridge");
12
+ const { readBridgeConfig } = require("./codex-desktop-refresher");
13
+ const { printQR } = require("./qr");
14
+ const { resetBridgeDeviceState } = require("./secure-device-state");
15
+ const {
16
+ clearBridgeStatus,
17
+ clearPairingSession,
18
+ ensureCodexLogsDir,
19
+ ensureCodexStateDir,
20
+ readBridgeStatus,
21
+ readDaemonConfig,
22
+ readPairingSession,
23
+ resolveBridgeStderrLogPath,
24
+ resolveBridgeStdoutLogPath,
25
+ resolveCodexStateDir,
26
+ writeBridgeStatus,
27
+ writeDaemonConfig,
28
+ writePairingSession,
29
+ } = require("./daemon-state");
30
+
31
+ const SERVICE_LABEL = "com.codex.bridge";
32
+ const DEFAULT_PAIRING_WAIT_TIMEOUT_MS = 10_000;
33
+ const DEFAULT_PAIRING_WAIT_INTERVAL_MS = 200;
34
+
35
+ // Runs the bridge inside launchd while keeping QR rendering in the foreground CLI command.
36
+ function runMacOSBridgeService({
37
+ env = process.env,
38
+ platform = process.platform,
39
+ } = {}) {
40
+ assertDarwinPlatform(platform);
41
+ const config = readDaemonConfig({ env });
42
+ if (!config?.relayUrl) {
43
+ const message = "No relay URL configured for the macOS bridge service.";
44
+ // Clear any stale QR so the CLI does not keep showing a pairing payload for a dead service.
45
+ clearPairingSession({ env });
46
+ writeBridgeStatus({
47
+ state: "error",
48
+ connectionStatus: "error",
49
+ pid: process.pid,
50
+ lastError: message,
51
+ }, { env });
52
+ console.error(`[codex-bridge] ${message}`);
53
+ return;
54
+ }
55
+
56
+ startBridge({
57
+ config,
58
+ printPairingQr: false,
59
+ onPairingPayload(pairingPayload) {
60
+ writePairingSession(pairingPayload, { env });
61
+ },
62
+ onBridgeStatus(status) {
63
+ writeBridgeStatus(status, { env });
64
+ },
65
+ });
66
+ }
67
+
68
+ // Prepares config + launchd state and optionally waits for the fresh pairing payload written by the service.
69
+ async function startMacOSBridgeService({
70
+ env = process.env,
71
+ platform = process.platform,
72
+ fsImpl = fs,
73
+ execFileSyncImpl = execFileSync,
74
+ osImpl = os,
75
+ nodePath = process.execPath,
76
+ cliPath = path.resolve(__dirname, "..", "bin", "codex-bridge.js"),
77
+ waitForPairing = false,
78
+ pairingTimeoutMs = DEFAULT_PAIRING_WAIT_TIMEOUT_MS,
79
+ pairingPollIntervalMs = DEFAULT_PAIRING_WAIT_INTERVAL_MS,
80
+ } = {}) {
81
+ assertDarwinPlatform(platform);
82
+ const config = readBridgeConfig({ env });
83
+ assertRelayConfigured(config);
84
+ const startedAt = Date.now();
85
+
86
+ writeDaemonConfig(config, { env, fsImpl });
87
+ clearPairingSession({ env, fsImpl });
88
+ clearBridgeStatus({ env, fsImpl });
89
+ ensureCodexStateDir({ env, fsImpl, osImpl });
90
+ ensureCodexLogsDir({ env, fsImpl, osImpl });
91
+
92
+ const plistPath = writeLaunchAgentPlist({
93
+ env,
94
+ fsImpl,
95
+ osImpl,
96
+ nodePath,
97
+ cliPath,
98
+ });
99
+ restartLaunchAgent({
100
+ env,
101
+ execFileSyncImpl,
102
+ plistPath,
103
+ });
104
+
105
+ if (!waitForPairing) {
106
+ return {
107
+ plistPath,
108
+ pairingSession: null,
109
+ };
110
+ }
111
+
112
+ const pairingSession = await waitForFreshPairingSession({
113
+ env,
114
+ fsImpl,
115
+ startedAt,
116
+ timeoutMs: pairingTimeoutMs,
117
+ intervalMs: pairingPollIntervalMs,
118
+ });
119
+ return {
120
+ plistPath,
121
+ pairingSession,
122
+ };
123
+ }
124
+
125
+ function stopMacOSBridgeService({
126
+ env = process.env,
127
+ platform = process.platform,
128
+ execFileSyncImpl = execFileSync,
129
+ fsImpl = fs,
130
+ } = {}) {
131
+ assertDarwinPlatform(platform);
132
+ bootoutLaunchAgent({
133
+ env,
134
+ execFileSyncImpl,
135
+ ignoreMissing: true,
136
+ });
137
+ clearPairingSession({ env, fsImpl });
138
+ clearBridgeStatus({ env, fsImpl });
139
+ }
140
+
141
+ // Revokes pairing immediately on macOS by stopping the daemon before rotating identity/trust state.
142
+ function resetMacOSBridgePairing({
143
+ env = process.env,
144
+ platform = process.platform,
145
+ execFileSyncImpl = execFileSync,
146
+ fsImpl = fs,
147
+ resetBridgePairingImpl = resetBridgeDeviceState,
148
+ } = {}) {
149
+ assertDarwinPlatform(platform);
150
+ stopMacOSBridgeService({
151
+ env,
152
+ platform,
153
+ execFileSyncImpl,
154
+ fsImpl,
155
+ });
156
+ return resetBridgePairingImpl();
157
+ }
158
+
159
+ function getMacOSBridgeServiceStatus({
160
+ env = process.env,
161
+ platform = process.platform,
162
+ execFileSyncImpl = execFileSync,
163
+ fsImpl = fs,
164
+ } = {}) {
165
+ assertDarwinPlatform(platform);
166
+ const launchd = readLaunchAgentState({ env, execFileSyncImpl });
167
+ return {
168
+ label: SERVICE_LABEL,
169
+ platform: "darwin",
170
+ installed: fsImpl.existsSync(resolveLaunchAgentPlistPath({ env })),
171
+ launchdLoaded: launchd.loaded,
172
+ launchdPid: launchd.pid,
173
+ bridgeStatus: readBridgeStatus({ env, fsImpl }),
174
+ pairingSession: readPairingSession({ env, fsImpl }),
175
+ stdoutLogPath: resolveBridgeStdoutLogPath({ env }),
176
+ stderrLogPath: resolveBridgeStderrLogPath({ env }),
177
+ };
178
+ }
179
+
180
+ function printMacOSBridgeServiceStatus(options = {}) {
181
+ const status = getMacOSBridgeServiceStatus(options);
182
+ const bridgeState = status.bridgeStatus?.state || "unknown";
183
+ const connectionStatus = status.bridgeStatus?.connectionStatus || "unknown";
184
+ const pairingCreatedAt = status.pairingSession?.createdAt || "none";
185
+ console.log(`[codex-bridge] Service label: ${status.label}`);
186
+ console.log(`[codex-bridge] Installed: ${status.installed ? "yes" : "no"}`);
187
+ console.log(`[codex-bridge] Launchd loaded: ${status.launchdLoaded ? "yes" : "no"}`);
188
+ console.log(`[codex-bridge] PID: ${status.launchdPid || status.bridgeStatus?.pid || "unknown"}`);
189
+ console.log(`[codex-bridge] Bridge state: ${bridgeState}`);
190
+ console.log(`[codex-bridge] Connection: ${connectionStatus}`);
191
+ console.log(`[codex-bridge] Pairing payload: ${pairingCreatedAt}`);
192
+ console.log(`[codex-bridge] Stdout log: ${status.stdoutLogPath}`);
193
+ console.log(`[codex-bridge] Stderr log: ${status.stderrLogPath}`);
194
+ }
195
+
196
+ function printMacOSBridgePairingQr({ pairingSession = null, env = process.env, fsImpl = fs } = {}) {
197
+ const nextPairingSession = pairingSession || readPairingSession({ env, fsImpl });
198
+ const pairingPayload = nextPairingSession?.pairingPayload;
199
+ if (!pairingPayload) {
200
+ throw new Error("The macOS bridge service did not publish a pairing payload yet.");
201
+ }
202
+
203
+ printQR(pairingPayload);
204
+ }
205
+
206
+ // Persists a launch agent that always runs the Node CLI entrypoint in service mode.
207
+ function writeLaunchAgentPlist({
208
+ env = process.env,
209
+ fsImpl = fs,
210
+ osImpl = os,
211
+ nodePath = process.execPath,
212
+ cliPath = path.resolve(__dirname, "..", "bin", "codex-bridge.js"),
213
+ } = {}) {
214
+ const plistPath = resolveLaunchAgentPlistPath({ env, osImpl });
215
+ const stateDir = resolveCodexStateDir({ env, osImpl });
216
+ const stdoutLogPath = resolveBridgeStdoutLogPath({ env, osImpl });
217
+ const stderrLogPath = resolveBridgeStderrLogPath({ env, osImpl });
218
+ const homeDir = env.HOME || osImpl.homedir();
219
+ const serialized = buildLaunchAgentPlist({
220
+ homeDir,
221
+ pathEnv: env.PATH || "",
222
+ stateDir,
223
+ stdoutLogPath,
224
+ stderrLogPath,
225
+ nodePath,
226
+ cliPath,
227
+ });
228
+
229
+ fsImpl.mkdirSync(path.dirname(plistPath), { recursive: true });
230
+ fsImpl.writeFileSync(plistPath, serialized, "utf8");
231
+ return plistPath;
232
+ }
233
+
234
+ function buildLaunchAgentPlist({
235
+ homeDir,
236
+ pathEnv,
237
+ stateDir,
238
+ stdoutLogPath,
239
+ stderrLogPath,
240
+ nodePath,
241
+ cliPath,
242
+ }) {
243
+ return `<?xml version="1.0" encoding="UTF-8"?>
244
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
245
+ <plist version="1.0">
246
+ <dict>
247
+ <key>Label</key>
248
+ <string>${escapeXml(SERVICE_LABEL)}</string>
249
+ <key>ProgramArguments</key>
250
+ <array>
251
+ <string>${escapeXml(nodePath)}</string>
252
+ <string>${escapeXml(cliPath)}</string>
253
+ <string>run-service</string>
254
+ </array>
255
+ <key>RunAtLoad</key>
256
+ <true/>
257
+ <key>KeepAlive</key>
258
+ <dict>
259
+ <key>SuccessfulExit</key>
260
+ <false/>
261
+ </dict>
262
+ <key>WorkingDirectory</key>
263
+ <string>${escapeXml(homeDir)}</string>
264
+ <key>EnvironmentVariables</key>
265
+ <dict>
266
+ <key>HOME</key>
267
+ <string>${escapeXml(homeDir)}</string>
268
+ <key>PATH</key>
269
+ <string>${escapeXml(pathEnv)}</string>
270
+ <key>CODEX_BRIDGE_DEVICE_STATE_DIR</key>
271
+ <string>${escapeXml(stateDir)}</string>
272
+ </dict>
273
+ <key>StandardOutPath</key>
274
+ <string>${escapeXml(stdoutLogPath)}</string>
275
+ <key>StandardErrorPath</key>
276
+ <string>${escapeXml(stderrLogPath)}</string>
277
+ </dict>
278
+ </plist>
279
+ `;
280
+ }
281
+
282
+ async function waitForFreshPairingSession({
283
+ env = process.env,
284
+ fsImpl = fs,
285
+ startedAt = Date.now(),
286
+ timeoutMs = DEFAULT_PAIRING_WAIT_TIMEOUT_MS,
287
+ intervalMs = DEFAULT_PAIRING_WAIT_INTERVAL_MS,
288
+ } = {}) {
289
+ const deadline = Date.now() + timeoutMs;
290
+
291
+ while (Date.now() <= deadline) {
292
+ const pairingSession = readPairingSession({ env, fsImpl });
293
+ const createdAt = Date.parse(pairingSession?.createdAt || "");
294
+ if (pairingSession?.pairingPayload && Number.isFinite(createdAt) && createdAt >= startedAt) {
295
+ return pairingSession;
296
+ }
297
+ await sleep(intervalMs);
298
+ }
299
+
300
+ throw new Error(
301
+ `Timed out waiting for the macOS bridge service to publish a pairing QR. `
302
+ + `Check ${resolveBridgeStderrLogPath({ env })}.`
303
+ );
304
+ }
305
+
306
+ function restartLaunchAgent({
307
+ env = process.env,
308
+ execFileSyncImpl = execFileSync,
309
+ plistPath,
310
+ } = {}) {
311
+ bootoutLaunchAgent({
312
+ env,
313
+ execFileSyncImpl,
314
+ ignoreMissing: true,
315
+ });
316
+ execFileSyncImpl("launchctl", [
317
+ "bootstrap",
318
+ launchAgentDomain(env),
319
+ plistPath,
320
+ ], { stdio: ["ignore", "ignore", "pipe"] });
321
+ }
322
+
323
+ function bootoutLaunchAgent({
324
+ env = process.env,
325
+ execFileSyncImpl = execFileSync,
326
+ ignoreMissing = false,
327
+ } = {}) {
328
+ const bootoutTargets = [
329
+ // Some macOS setups only fully unload the agent when bootout targets the plist path.
330
+ [launchAgentDomain(env), resolveLaunchAgentPlistPath({ env })],
331
+ [launchAgentLabelDomain(env)],
332
+ ];
333
+ let lastError = null;
334
+
335
+ for (const targetArgs of bootoutTargets) {
336
+ try {
337
+ execFileSyncImpl("launchctl", [
338
+ "bootout",
339
+ ...targetArgs,
340
+ ], { stdio: ["ignore", "ignore", "pipe"] });
341
+ return;
342
+ } catch (error) {
343
+ lastError = error;
344
+ }
345
+ }
346
+
347
+ if (ignoreMissing && isMissingLaunchAgentError(lastError)) {
348
+ return;
349
+ }
350
+ throw lastError;
351
+ }
352
+
353
+ function readLaunchAgentState({
354
+ env = process.env,
355
+ execFileSyncImpl = execFileSync,
356
+ } = {}) {
357
+ try {
358
+ const output = execFileSyncImpl("launchctl", [
359
+ "print",
360
+ launchAgentLabelDomain(env),
361
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
362
+ return {
363
+ loaded: true,
364
+ pid: parseLaunchdPid(output),
365
+ raw: output,
366
+ };
367
+ } catch (error) {
368
+ if (isMissingLaunchAgentError(error)) {
369
+ return {
370
+ loaded: false,
371
+ pid: null,
372
+ raw: "",
373
+ };
374
+ }
375
+ throw error;
376
+ }
377
+ }
378
+
379
+ function resolveLaunchAgentPlistPath({ env = process.env, osImpl = os } = {}) {
380
+ const homeDir = env.HOME || osImpl.homedir();
381
+ return path.join(homeDir, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
382
+ }
383
+
384
+ function assertDarwinPlatform(platform = process.platform) {
385
+ if (platform !== "darwin") {
386
+ throw new Error("macOS bridge service management is only available on macOS.");
387
+ }
388
+ }
389
+
390
+ function assertRelayConfigured(config) {
391
+ if (typeof config?.relayUrl === "string" && config.relayUrl.trim()) {
392
+ return;
393
+ }
394
+ throw new Error("No relay URL configured. Run ./run-local-codex.sh or set CODEX_BRIDGE_RELAY before enabling the macOS bridge service.");
395
+ }
396
+
397
+ function launchAgentDomain(env) {
398
+ return `gui/${resolveUid(env)}`;
399
+ }
400
+
401
+ function launchAgentLabelDomain(env) {
402
+ return `${launchAgentDomain(env)}/${SERVICE_LABEL}`;
403
+ }
404
+
405
+ function resolveUid(env) {
406
+ if (typeof process.getuid === "function") {
407
+ return process.getuid();
408
+ }
409
+
410
+ const uid = Number.parseInt(env?.UID || process.env.UID || "", 10);
411
+ if (Number.isFinite(uid)) {
412
+ return uid;
413
+ }
414
+
415
+ throw new Error("Could not determine the current macOS user id for launchctl.");
416
+ }
417
+
418
+ function parseLaunchdPid(output) {
419
+ const match = typeof output === "string" ? output.match(/\bpid = (\d+)/) : null;
420
+ return match ? Number.parseInt(match[1], 10) : null;
421
+ }
422
+
423
+ function isMissingLaunchAgentError(error) {
424
+ const combined = [
425
+ error?.message,
426
+ error?.stderr?.toString?.("utf8"),
427
+ error?.stdout?.toString?.("utf8"),
428
+ ].filter(Boolean).join("\n").toLowerCase();
429
+ return combined.includes("could not find service")
430
+ || combined.includes("service could not be found")
431
+ || combined.includes("no such process");
432
+ }
433
+
434
+ function escapeXml(value) {
435
+ return String(value)
436
+ .replaceAll("&", "&amp;")
437
+ .replaceAll("<", "&lt;")
438
+ .replaceAll(">", "&gt;")
439
+ .replaceAll('"', "&quot;")
440
+ .replaceAll("'", "&apos;");
441
+ }
442
+
443
+ function sleep(ms) {
444
+ return new Promise((resolve) => setTimeout(resolve, ms));
445
+ }
446
+
447
+ module.exports = {
448
+ buildLaunchAgentPlist,
449
+ getMacOSBridgeServiceStatus,
450
+ printMacOSBridgePairingQr,
451
+ printMacOSBridgeServiceStatus,
452
+ resetMacOSBridgePairing,
453
+ resolveLaunchAgentPlistPath,
454
+ runMacOSBridgeService,
455
+ startMacOSBridgeService,
456
+ stopMacOSBridgeService,
457
+ };
@@ -0,0 +1,95 @@
1
+ // FILE: notifications-handler.js
2
+ // Purpose: Intercepts notifications/push/* bridge RPCs and forwards device registration to the configured push service.
3
+ // Layer: Bridge handler
4
+ // Exports: createNotificationsHandler
5
+ // Depends on: none
6
+
7
+ function createNotificationsHandler({ pushServiceClient, logPrefix = "[codex-bridge]" } = {}) {
8
+ function handleNotificationsRequest(rawMessage, sendResponse) {
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(rawMessage);
12
+ } catch {
13
+ return false;
14
+ }
15
+
16
+ const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
17
+ if (method !== "notifications/push/register") {
18
+ return false;
19
+ }
20
+
21
+ const id = parsed.id;
22
+ const params = parsed.params || {};
23
+
24
+ handleNotificationsMethod(method, params)
25
+ .then((result) => {
26
+ sendResponse(JSON.stringify({ id, result }));
27
+ })
28
+ .catch((error) => {
29
+ console.error(`${logPrefix} push registration failed: ${error.message}`);
30
+ sendResponse(JSON.stringify({
31
+ id,
32
+ error: {
33
+ code: -32000,
34
+ message: error.userMessage || error.message || "Push registration failed.",
35
+ data: {
36
+ errorCode: error.errorCode || "push_registration_failed",
37
+ },
38
+ },
39
+ }));
40
+ });
41
+
42
+ return true;
43
+ }
44
+
45
+ async function handleNotificationsMethod(method, params) {
46
+ if (!pushServiceClient?.hasConfiguredBaseUrl) {
47
+ return { ok: false, skipped: true };
48
+ }
49
+
50
+ const deviceToken = readString(params.deviceToken);
51
+ const alertsEnabled = Boolean(params.alertsEnabled);
52
+ const apnsEnvironment = readAPNsEnvironment(params.appEnvironment);
53
+ if (!deviceToken) {
54
+ throw notificationsError(
55
+ "missing_device_token",
56
+ "notifications/push/register requires a deviceToken."
57
+ );
58
+ }
59
+
60
+ await pushServiceClient.registerDevice({
61
+ deviceToken,
62
+ alertsEnabled,
63
+ apnsEnvironment,
64
+ });
65
+
66
+ return {
67
+ ok: true,
68
+ alertsEnabled,
69
+ apnsEnvironment,
70
+ };
71
+ }
72
+
73
+ return {
74
+ handleNotificationsRequest,
75
+ };
76
+ }
77
+
78
+ function readString(value) {
79
+ return typeof value === "string" && value.trim() ? value.trim() : null;
80
+ }
81
+
82
+ function readAPNsEnvironment(value) {
83
+ return value === "development" ? "development" : "production";
84
+ }
85
+
86
+ function notificationsError(errorCode, userMessage) {
87
+ const error = new Error(userMessage);
88
+ error.errorCode = errorCode;
89
+ error.userMessage = userMessage;
90
+ return error;
91
+ }
92
+
93
+ module.exports = {
94
+ createNotificationsHandler,
95
+ };