@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/README.md +473 -23
- package/bin/codex-bridge.js +136 -100
- package/bin/phodex.js +8 -0
- package/bin/remodex.js +8 -0
- package/package.json +38 -24
- package/src/bridge.js +622 -0
- package/src/codex-desktop-refresher.js +776 -0
- package/src/codex-transport.js +238 -0
- package/src/daemon-state.js +170 -0
- package/src/desktop-handler.js +407 -0
- package/src/git-handler.js +1267 -0
- package/src/index.js +35 -0
- package/src/macos-launch-agent.js +457 -0
- package/src/notifications-handler.js +95 -0
- package/src/push-notification-completion-dedupe.js +147 -0
- package/src/push-notification-service-client.js +151 -0
- package/src/push-notification-tracker.js +688 -0
- package/src/qr.js +19 -0
- package/src/rollout-live-mirror.js +730 -0
- package/src/rollout-watch.js +853 -0
- package/src/scripts/codex-handoff.applescript +100 -0
- package/src/scripts/codex-refresh.applescript +51 -0
- package/src/secure-device-state.js +430 -0
- package/src/secure-transport.js +738 -0
- package/src/session-state.js +62 -0
- package/src/thread-context-handler.js +80 -0
- package/src/workspace-handler.js +464 -0
- package/server.mjs +0 -290
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// FILE: codex-transport.js
|
|
2
|
+
// Purpose: Abstracts the Codex-side transport so the bridge can talk to either a spawned app-server or an existing WebSocket endpoint.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: createCodexTransport
|
|
5
|
+
// Depends on: child_process, ws
|
|
6
|
+
|
|
7
|
+
const { spawn } = require("child_process");
|
|
8
|
+
const WebSocket = require("ws");
|
|
9
|
+
|
|
10
|
+
function createCodexTransport({
|
|
11
|
+
endpoint = "",
|
|
12
|
+
env = process.env,
|
|
13
|
+
WebSocketImpl = WebSocket,
|
|
14
|
+
} = {}) {
|
|
15
|
+
if (endpoint) {
|
|
16
|
+
return createWebSocketTransport({ endpoint, WebSocketImpl });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return createSpawnTransport({ env });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createSpawnTransport({ env }) {
|
|
23
|
+
const launch = createCodexLaunchPlan({ env });
|
|
24
|
+
const codex = spawn(launch.command, launch.args, launch.options);
|
|
25
|
+
|
|
26
|
+
let stdoutBuffer = "";
|
|
27
|
+
let stderrBuffer = "";
|
|
28
|
+
let didRequestShutdown = false;
|
|
29
|
+
let didReportError = false;
|
|
30
|
+
const listeners = createListenerBag();
|
|
31
|
+
|
|
32
|
+
codex.on("error", (error) => {
|
|
33
|
+
didReportError = true;
|
|
34
|
+
listeners.emitError(error);
|
|
35
|
+
});
|
|
36
|
+
codex.on("close", (code, signal) => {
|
|
37
|
+
if (!didRequestShutdown && !didReportError && code !== 0) {
|
|
38
|
+
didReportError = true;
|
|
39
|
+
listeners.emitError(createCodexCloseError({
|
|
40
|
+
code,
|
|
41
|
+
signal,
|
|
42
|
+
stderrBuffer,
|
|
43
|
+
launchDescription: launch.description,
|
|
44
|
+
}));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
listeners.emitClose(code, signal);
|
|
49
|
+
});
|
|
50
|
+
// Ignore broken-pipe shutdown noise once the child is already going away.
|
|
51
|
+
codex.stdin.on("error", (error) => {
|
|
52
|
+
if (didRequestShutdown && isIgnorableStdinShutdownError(error)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (isIgnorableStdinShutdownError(error)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
didReportError = true;
|
|
61
|
+
listeners.emitError(error);
|
|
62
|
+
});
|
|
63
|
+
// Keep stderr muted during normal operation, but preserve enough output to
|
|
64
|
+
// explain launch failures when the child exits before the bridge can use it.
|
|
65
|
+
codex.stderr.on("data", (chunk) => {
|
|
66
|
+
stderrBuffer = appendOutputBuffer(stderrBuffer, chunk.toString("utf8"));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
codex.stdout.on("data", (chunk) => {
|
|
70
|
+
stdoutBuffer += chunk.toString("utf8");
|
|
71
|
+
const lines = stdoutBuffer.split("\n");
|
|
72
|
+
stdoutBuffer = lines.pop() || "";
|
|
73
|
+
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
const trimmedLine = line.trim();
|
|
76
|
+
if (trimmedLine) {
|
|
77
|
+
listeners.emitMessage(trimmedLine);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
mode: "spawn",
|
|
84
|
+
describe() {
|
|
85
|
+
return launch.description;
|
|
86
|
+
},
|
|
87
|
+
send(message) {
|
|
88
|
+
if (!codex.stdin.writable || codex.stdin.destroyed || codex.stdin.writableEnded) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
codex.stdin.write(message.endsWith("\n") ? message : `${message}\n`);
|
|
93
|
+
},
|
|
94
|
+
onMessage(handler) {
|
|
95
|
+
listeners.onMessage = handler;
|
|
96
|
+
},
|
|
97
|
+
onClose(handler) {
|
|
98
|
+
listeners.onClose = handler;
|
|
99
|
+
},
|
|
100
|
+
onError(handler) {
|
|
101
|
+
listeners.onError = handler;
|
|
102
|
+
},
|
|
103
|
+
shutdown() {
|
|
104
|
+
didRequestShutdown = true;
|
|
105
|
+
shutdownCodexProcess(codex);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Builds a single, platform-aware launch path so the bridge never "guesses"
|
|
111
|
+
// between multiple commands and accidentally starts duplicate runtimes.
|
|
112
|
+
function createCodexLaunchPlan({ env }) {
|
|
113
|
+
const sharedOptions = {
|
|
114
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
115
|
+
env: { ...env },
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (process.platform === "win32") {
|
|
119
|
+
return {
|
|
120
|
+
command: env.ComSpec || "cmd.exe",
|
|
121
|
+
args: ["/d", "/c", "codex app-server"],
|
|
122
|
+
options: {
|
|
123
|
+
...sharedOptions,
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
},
|
|
126
|
+
description: "`cmd.exe /d /c codex app-server`",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
command: "codex",
|
|
132
|
+
args: ["app-server"],
|
|
133
|
+
options: sharedOptions,
|
|
134
|
+
description: "`codex app-server`",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Stops the exact process tree we launched on Windows so the shell wrapper
|
|
139
|
+
// does not leave a child Codex process running in the background.
|
|
140
|
+
function shutdownCodexProcess(codex) {
|
|
141
|
+
if (codex.killed || codex.exitCode !== null) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (process.platform === "win32" && codex.pid) {
|
|
146
|
+
const killer = spawn("taskkill", ["/pid", String(codex.pid), "/t", "/f"], {
|
|
147
|
+
stdio: "ignore",
|
|
148
|
+
windowsHide: true,
|
|
149
|
+
});
|
|
150
|
+
killer.on("error", () => {
|
|
151
|
+
codex.kill();
|
|
152
|
+
});
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
codex.kill("SIGTERM");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createCodexCloseError({ code, signal, stderrBuffer, launchDescription }) {
|
|
160
|
+
const details = stderrBuffer.trim();
|
|
161
|
+
const reason = details || `Process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}.`;
|
|
162
|
+
return new Error(`Codex launcher ${launchDescription} failed: ${reason}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function appendOutputBuffer(buffer, chunk) {
|
|
166
|
+
const next = `${buffer}${chunk}`;
|
|
167
|
+
return next.slice(-4_096);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isIgnorableStdinShutdownError(error) {
|
|
171
|
+
return error?.code === "EPIPE" || error?.code === "ERR_STREAM_DESTROYED";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createWebSocketTransport({ endpoint, WebSocketImpl = WebSocket }) {
|
|
175
|
+
const socket = new WebSocketImpl(endpoint);
|
|
176
|
+
const listeners = createListenerBag();
|
|
177
|
+
const openState = WebSocketImpl.OPEN ?? WebSocket.OPEN ?? 1;
|
|
178
|
+
const connectingState = WebSocketImpl.CONNECTING ?? WebSocket.CONNECTING ?? 0;
|
|
179
|
+
|
|
180
|
+
socket.on("message", (chunk) => {
|
|
181
|
+
const message = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
182
|
+
if (message.trim()) {
|
|
183
|
+
listeners.emitMessage(message);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
socket.on("close", (code, reason) => {
|
|
188
|
+
const safeReason = reason ? reason.toString("utf8") : "no reason";
|
|
189
|
+
listeners.emitClose(code, safeReason);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
socket.on("error", (error) => listeners.emitError(error));
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
mode: "websocket",
|
|
196
|
+
describe() {
|
|
197
|
+
return endpoint;
|
|
198
|
+
},
|
|
199
|
+
send(message) {
|
|
200
|
+
if (socket.readyState === openState) {
|
|
201
|
+
socket.send(message);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
onMessage(handler) {
|
|
205
|
+
listeners.onMessage = handler;
|
|
206
|
+
},
|
|
207
|
+
onClose(handler) {
|
|
208
|
+
listeners.onClose = handler;
|
|
209
|
+
},
|
|
210
|
+
onError(handler) {
|
|
211
|
+
listeners.onError = handler;
|
|
212
|
+
},
|
|
213
|
+
shutdown() {
|
|
214
|
+
if (socket.readyState === openState || socket.readyState === connectingState) {
|
|
215
|
+
socket.close();
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function createListenerBag() {
|
|
222
|
+
return {
|
|
223
|
+
onMessage: null,
|
|
224
|
+
onClose: null,
|
|
225
|
+
onError: null,
|
|
226
|
+
emitMessage(message) {
|
|
227
|
+
this.onMessage?.(message);
|
|
228
|
+
},
|
|
229
|
+
emitClose(...args) {
|
|
230
|
+
this.onClose?.(...args);
|
|
231
|
+
},
|
|
232
|
+
emitError(error) {
|
|
233
|
+
this.onError?.(error);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { createCodexTransport };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// FILE: daemon-state.js
|
|
2
|
+
// Purpose: Persists macOS service config/runtime state outside the repo for the launchd bridge flow.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: path resolvers plus read/write helpers for daemon config, pairing payloads, and service status.
|
|
5
|
+
// Depends on: fs, os, path
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
|
+
|
|
11
|
+
const DEFAULT_STATE_DIR = path.join(os.homedir(), ".codex", "bridge");
|
|
12
|
+
const DAEMON_CONFIG_FILE = "daemon-config.json";
|
|
13
|
+
const PAIRING_SESSION_FILE = "pairing-session.json";
|
|
14
|
+
const BRIDGE_STATUS_FILE = "bridge-status.json";
|
|
15
|
+
const LOGS_DIR = "logs";
|
|
16
|
+
const BRIDGE_STDOUT_LOG_FILE = "bridge.stdout.log";
|
|
17
|
+
const BRIDGE_STDERR_LOG_FILE = "bridge.stderr.log";
|
|
18
|
+
|
|
19
|
+
// Reuses the existing Codex state root so daemon mode keeps the same local-first storage model.
|
|
20
|
+
function resolveCodexStateDir({ env = process.env, osImpl = os } = {}) {
|
|
21
|
+
return firstNonEmptyString([
|
|
22
|
+
env.CODEX_BRIDGE_DEVICE_STATE_DIR,
|
|
23
|
+
env.REMODEX_DEVICE_STATE_DIR,
|
|
24
|
+
])
|
|
25
|
+
|| resolveLegacyAwareDefaultStateDir(osImpl);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveDaemonConfigPath(options = {}) {
|
|
29
|
+
return path.join(resolveCodexStateDir(options), DAEMON_CONFIG_FILE);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolvePairingSessionPath(options = {}) {
|
|
33
|
+
return path.join(resolveCodexStateDir(options), PAIRING_SESSION_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveBridgeStatusPath(options = {}) {
|
|
37
|
+
return path.join(resolveCodexStateDir(options), BRIDGE_STATUS_FILE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveBridgeLogsDir(options = {}) {
|
|
41
|
+
return path.join(resolveCodexStateDir(options), LOGS_DIR);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveBridgeStdoutLogPath(options = {}) {
|
|
45
|
+
return path.join(resolveBridgeLogsDir(options), BRIDGE_STDOUT_LOG_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveBridgeStderrLogPath(options = {}) {
|
|
49
|
+
return path.join(resolveBridgeLogsDir(options), BRIDGE_STDERR_LOG_FILE);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeDaemonConfig(config, options = {}) {
|
|
53
|
+
writeJsonFile(resolveDaemonConfigPath(options), config, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readDaemonConfig(options = {}) {
|
|
57
|
+
return readJsonFile(resolveDaemonConfigPath(options), options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Persists the pairing payload so foreground CLI commands can render the QR locally.
|
|
61
|
+
function writePairingSession(pairingPayload, { now = () => Date.now(), ...options } = {}) {
|
|
62
|
+
writeJsonFile(resolvePairingSessionPath(options), {
|
|
63
|
+
createdAt: new Date(now()).toISOString(),
|
|
64
|
+
pairingPayload,
|
|
65
|
+
}, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readPairingSession(options = {}) {
|
|
69
|
+
return readJsonFile(resolvePairingSessionPath(options), options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function clearPairingSession({ fsImpl = fs, ...options } = {}) {
|
|
73
|
+
removeFile(resolvePairingSessionPath(options), fsImpl);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Captures the last known service heartbeat so `codex-bridge status` does not depend on launchctl output alone.
|
|
77
|
+
function writeBridgeStatus(status, { now = () => Date.now(), ...options } = {}) {
|
|
78
|
+
writeJsonFile(resolveBridgeStatusPath(options), {
|
|
79
|
+
...status,
|
|
80
|
+
updatedAt: new Date(now()).toISOString(),
|
|
81
|
+
}, options);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readBridgeStatus(options = {}) {
|
|
85
|
+
return readJsonFile(resolveBridgeStatusPath(options), options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function clearBridgeStatus({ fsImpl = fs, ...options } = {}) {
|
|
89
|
+
removeFile(resolveBridgeStatusPath(options), fsImpl);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ensureCodexStateDir({ fsImpl = fs, ...options } = {}) {
|
|
93
|
+
fsImpl.mkdirSync(resolveCodexStateDir(options), { recursive: true });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureCodexLogsDir({ fsImpl = fs, ...options } = {}) {
|
|
97
|
+
fsImpl.mkdirSync(resolveBridgeLogsDir(options), { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeJsonFile(targetPath, value, { fsImpl = fs } = {}) {
|
|
101
|
+
fsImpl.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
102
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
103
|
+
fsImpl.writeFileSync(targetPath, serialized, { mode: 0o600 });
|
|
104
|
+
try {
|
|
105
|
+
fsImpl.chmodSync(targetPath, 0o600);
|
|
106
|
+
} catch {
|
|
107
|
+
// Best-effort only on filesystems without POSIX mode support.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function readJsonFile(targetPath, { fsImpl = fs } = {}) {
|
|
112
|
+
if (!fsImpl.existsSync(targetPath)) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(fsImpl.readFileSync(targetPath, "utf8"));
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function removeFile(targetPath, fsImpl) {
|
|
124
|
+
try {
|
|
125
|
+
fsImpl.rmSync(targetPath, { force: true });
|
|
126
|
+
} catch {
|
|
127
|
+
// Missing runtime files should not block control-plane commands.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeNonEmptyString(value) {
|
|
132
|
+
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function firstNonEmptyString(values) {
|
|
136
|
+
for (const value of values) {
|
|
137
|
+
const normalized = normalizeNonEmptyString(value);
|
|
138
|
+
if (normalized) {
|
|
139
|
+
return normalized;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return "";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolveLegacyAwareDefaultStateDir(osImpl) {
|
|
146
|
+
const legacyStateDir = path.join(osImpl.homedir(), ".remodex");
|
|
147
|
+
return fs.existsSync(legacyStateDir)
|
|
148
|
+
? legacyStateDir
|
|
149
|
+
: DEFAULT_STATE_DIR;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
clearBridgeStatus,
|
|
154
|
+
clearPairingSession,
|
|
155
|
+
ensureCodexLogsDir,
|
|
156
|
+
ensureCodexStateDir,
|
|
157
|
+
readBridgeStatus,
|
|
158
|
+
readDaemonConfig,
|
|
159
|
+
readPairingSession,
|
|
160
|
+
resolveBridgeLogsDir,
|
|
161
|
+
resolveBridgeStderrLogPath,
|
|
162
|
+
resolveBridgeStatusPath,
|
|
163
|
+
resolveBridgeStdoutLogPath,
|
|
164
|
+
resolveDaemonConfigPath,
|
|
165
|
+
resolvePairingSessionPath,
|
|
166
|
+
resolveCodexStateDir,
|
|
167
|
+
writeBridgeStatus,
|
|
168
|
+
writeDaemonConfig,
|
|
169
|
+
writePairingSession,
|
|
170
|
+
};
|