184 lines
5.0 KiB
JavaScript
184 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { createServer } from "node:http";
|
|
import { executeCodexDesktopRefreshHint } from "./codex-desktop-refresh-hint.mjs";
|
|
import { detectCodexDesktopIntegration } from "./codex-desktop-integration-probe.mjs";
|
|
|
|
const subscribers = new Set();
|
|
const recentEvents = [];
|
|
let nextEventId = 1;
|
|
|
|
function parsePort(value) {
|
|
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 4318;
|
|
}
|
|
|
|
function readRequestBody(request) {
|
|
return new Promise((resolve, reject) => {
|
|
let raw = "";
|
|
request.setEncoding("utf8");
|
|
request.on("data", (chunk) => {
|
|
raw += chunk;
|
|
if (raw.length > 64 * 1024) {
|
|
reject(new Error("CODEX_DESKTOP_REFRESH_HINT_TOO_LARGE"));
|
|
request.destroy();
|
|
}
|
|
});
|
|
request.on("end", () => {
|
|
resolve(raw);
|
|
});
|
|
request.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function writeJson(response, statusCode, payload) {
|
|
response.writeHead(statusCode, {
|
|
"Content-Type": "application/json",
|
|
"Cache-Control": "no-store",
|
|
});
|
|
response.end(`${JSON.stringify(payload)}\n`);
|
|
}
|
|
|
|
function writeSse(response, event) {
|
|
if (event.id !== undefined) {
|
|
response.write(`id: ${event.id}\n`);
|
|
}
|
|
response.write(`event: ${event.event}\n`);
|
|
response.write(`data: ${JSON.stringify(event.data)}\n\n`);
|
|
}
|
|
|
|
function buildSafeRefreshEvent(payload, result) {
|
|
return {
|
|
eventId: nextEventId++,
|
|
eventType: "codex_desktop_refresh",
|
|
receivedAt: new Date().toISOString(),
|
|
targetThreadRef: String(payload?.targetThreadRef || "").trim() || undefined,
|
|
sourceMessageId: String(payload?.sourceMessageId || "").trim() || undefined,
|
|
appName: String(result?.appName || payload?.appName || "Codex").trim() || "Codex",
|
|
refreshMode: String(payload?.refreshMode || "deeplink-reload").trim() || "deeplink-reload",
|
|
status: result?.status,
|
|
deepLink: result?.deepLink,
|
|
detail: result?.detail || result?.error,
|
|
};
|
|
}
|
|
|
|
function publishRefreshEvent(eventData) {
|
|
recentEvents.push(eventData);
|
|
while (recentEvents.length > 50) {
|
|
recentEvents.shift();
|
|
}
|
|
for (const subscriber of subscribers) {
|
|
writeSse(subscriber, {
|
|
id: eventData.eventId,
|
|
event: "codex_desktop_refresh",
|
|
data: eventData,
|
|
});
|
|
}
|
|
}
|
|
|
|
function handleEvents(request, response) {
|
|
if (request.method !== "GET") {
|
|
writeJson(response, 405, { status: "failed", error: "METHOD_NOT_ALLOWED" });
|
|
return;
|
|
}
|
|
response.writeHead(200, {
|
|
"Content-Type": "text/event-stream",
|
|
"Cache-Control": "no-store",
|
|
Connection: "keep-alive",
|
|
"X-Accel-Buffering": "no",
|
|
});
|
|
subscribers.add(response);
|
|
writeSse(response, {
|
|
event: "ready",
|
|
data: {
|
|
ok: true,
|
|
service: "boss-codex-desktop-refresh-bridge",
|
|
},
|
|
});
|
|
const keepAlive = setInterval(() => {
|
|
response.write(": keepalive\n\n");
|
|
}, 15000);
|
|
request.on("close", () => {
|
|
clearInterval(keepAlive);
|
|
subscribers.delete(response);
|
|
});
|
|
}
|
|
|
|
async function handleRefresh(request, response) {
|
|
if (request.method !== "POST") {
|
|
writeJson(response, 405, { status: "failed", error: "METHOD_NOT_ALLOWED" });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const raw = await readRequestBody(request);
|
|
const payload = JSON.parse(raw || "{}");
|
|
const result = await executeCodexDesktopRefreshHint(payload);
|
|
publishRefreshEvent(buildSafeRefreshEvent(payload, result));
|
|
writeJson(response, result.status === "failed" ? 422 : 200, result);
|
|
} catch (error) {
|
|
writeJson(response, 400, {
|
|
status: "failed",
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
}
|
|
|
|
const host = process.env.BOSS_CODEX_DESKTOP_BRIDGE_HOST || "127.0.0.1";
|
|
const port = parsePort(process.env.BOSS_CODEX_DESKTOP_BRIDGE_PORT);
|
|
|
|
const server = createServer(async (request, response) => {
|
|
if (request.url === "/health") {
|
|
writeJson(response, 200, {
|
|
ok: true,
|
|
service: "boss-codex-desktop-refresh-bridge",
|
|
});
|
|
return;
|
|
}
|
|
if (request.url === "/api/v1/codex-desktop/events") {
|
|
handleEvents(request, response);
|
|
return;
|
|
}
|
|
if (request.url === "/api/v1/codex-desktop/events/recent") {
|
|
writeJson(response, 200, {
|
|
ok: true,
|
|
events: recentEvents,
|
|
});
|
|
return;
|
|
}
|
|
if (request.url === "/api/v1/codex-desktop/capabilities") {
|
|
try {
|
|
writeJson(response, 200, await detectCodexDesktopIntegration());
|
|
} catch (error) {
|
|
writeJson(response, 500, {
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (request.url === "/api/v1/codex-desktop/refresh") {
|
|
await handleRefresh(request, response);
|
|
return;
|
|
}
|
|
writeJson(response, 404, { status: "failed", error: "NOT_FOUND" });
|
|
});
|
|
|
|
server.listen(port, host, () => {
|
|
const address = server.address();
|
|
process.stdout.write(
|
|
`${JSON.stringify({
|
|
status: "ready",
|
|
service: "boss-codex-desktop-refresh-bridge",
|
|
host: address.address,
|
|
port: address.port,
|
|
})}\n`,
|
|
);
|
|
});
|
|
|
|
process.on("SIGTERM", () => {
|
|
server.close(() => {
|
|
process.exit(0);
|
|
});
|
|
});
|