relay-companion
Version:
Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.
149 lines (140 loc) • 5.14 kB
JavaScript
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { notifyCodexDesktopThreads } from "./codex-desktop.js";
import { codexHome, storeDir } from "./host-paths.js";
const SESSION_META_READ_LIMIT = 256 * 1024;
const RELAY_CODEX_ORIGINATOR = "granular_relay_companion";
export function relayCodexProjectThreadIds(state, { relayRoot = path.join(os.homedir(), "Relay") } = {}) {
const wanted = path.resolve(relayRoot);
const ids = [];
for (const row of Object.values(state?.packets || {})) {
const threadId = String(row?.codexThreadId || "").trim();
const cwd = String(row?.openCwd || "").trim();
if (!threadId || !cwd) continue;
if (path.resolve(cwd) !== wanted) continue;
ids.push(threadId);
}
return Array.from(new Set(ids));
}
async function firstJsonLine(filePath) {
let handle;
try {
handle = await fs.promises.open(filePath, "r");
const buffer = Buffer.allocUnsafe(SESSION_META_READ_LIMIT);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
const newline = buffer.subarray(0, bytesRead).indexOf(0x0a);
if (newline < 0 && bytesRead === buffer.length) return null;
const line = buffer.subarray(0, newline < 0 ? bytesRead : newline).toString("utf8").trim();
if (!line) return null;
return JSON.parse(line);
} catch {
return null;
} finally {
await handle?.close().catch(() => {});
}
}
async function codexSessionFiles(root) {
const files = [];
const pending = [root];
while (pending.length) {
const dir = pending.pop();
let entries;
try {
entries = await fs.promises.readdir(dir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.isSymbolicLink()) continue;
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) pending.push(entryPath);
else if (entry.isFile() && entry.name.endsWith(".jsonl")) files.push(entryPath);
}
}
return files;
}
export async function relayCodexSessionThreadIds({
sessionsRoot = path.join(codexHome(), "sessions"),
relayRoot = path.join(os.homedir(), "Relay"),
} = {}) {
const wanted = path.resolve(relayRoot);
const files = await codexSessionFiles(sessionsRoot);
const ids = [];
const concurrency = Math.min(16, Math.max(1, files.length));
let next = 0;
await Promise.all(
Array.from({ length: concurrency }, async () => {
while (next < files.length) {
const filePath = files[next++];
const row = await firstJsonLine(filePath);
if (row?.type !== "session_meta") continue;
const meta = row.payload || {};
if (String(meta.originator || "") !== RELAY_CODEX_ORIGINATOR) continue;
const cwd = String(meta.cwd || "").trim();
if (!cwd || path.resolve(cwd) !== wanted) continue;
const threadId = String(meta.id || meta.session_id || "").trim();
if (threadId) ids.push(threadId);
}
}),
);
return Array.from(new Set(ids));
}
export async function repairRelayCodexProjectAssignments({
statePath = path.join(storeDir(), "state.json"),
sessionsRoot = path.join(codexHome(), "sessions"),
relayRoot = path.join(os.homedir(), "Relay"),
notify = notifyCodexDesktopThreads,
log = () => {},
} = {}) {
let state = null;
let stateUnavailable = false;
try {
state = JSON.parse(fs.readFileSync(statePath, "utf8"));
} catch (error) {
stateUnavailable = true;
if (error?.code !== "ENOENT") log(`Codex Relay project repair skipped: ${error?.message || error}`);
}
const packetIds = relayCodexProjectThreadIds(state, { relayRoot });
const sessionIds = await relayCodexSessionThreadIds({ sessionsRoot, relayRoot });
const threadIds = Array.from(new Set([...packetIds, ...sessionIds]));
if (!threadIds.length) {
return { attempted: false, reason: stateUnavailable ? "state-unavailable" : "no-relay-threads", threadIds: [] };
}
const workspaceRootsByThreadId = Object.fromEntries(threadIds.map((threadId) => [threadId, [relayRoot]]));
const result = await notify({
threadIds,
workspaceRootsByThreadId,
ensureWorkspaceRoot: relayRoot,
openThreadId: null,
assignmentOnly: true,
});
if (result?.ok) log(`Codex Relay project repaired for ${threadIds.length} existing task${threadIds.length === 1 ? "" : "s"}`);
return { ...result, threadIds };
}
export function startRelayCodexProjectRepairLoop({
repair = repairRelayCodexProjectAssignments,
retryMs = 60_000,
setTimeoutImpl = setTimeout,
log = () => {},
} = {}) {
let stopped = false;
let timer = null;
const run = async () => {
if (stopped) return;
try {
const result = await repair({ log });
if (result?.ok || result?.reason === "no-relay-threads" || result?.reason === "not-darwin") return;
} catch (error) {
log(`Codex Relay project repair failed: ${error?.message || error}`);
}
if (stopped) return;
timer = setTimeoutImpl(() => void run(), retryMs);
timer?.unref?.();
};
void run();
return () => {
stopped = true;
if (timer) clearTimeout(timer);
};
}