relay-companion
Version:
Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.
199 lines (181 loc) • 8.16 kB
JavaScript
// Real idle-wake for Claude DESKTOP sessions.
//
// Why this exists. A staged hook injection is only consumed when the session
// takes a turn, so a relay clicked into an idle chat sits there invisibly. The
// platform's supported way to make an idle session take a turn is a channel
// notification — and the capability ships in the CLI Claude Desktop already
// runs. The one missing piece is the flag: Desktop never passes --channels, and
// there is no file, env var or setting that adds it (its --mcp-config and
// --settings are handed over as inline JSON, and the CLI's stdin is an
// anonymous socketpair owned by the app, so nothing external can reach it).
//
// So we add the flag at the only remaining seam: the executable path Desktop
// spawns. `claude` is moved aside to `claude.relay-real` and replaced by a tiny
// shim that execs it with --channels appended. Verified on 2.1.221:
// - a shell script CAN exec the hardened-runtime signed binary (it keeps its
// own embedded signature; the bundle's resource seal is not checked here),
// - Desktop performs NO signature/hash check at spawn time,
// - `relay mcp` is ALREADY an MCP server in every Desktop session, so
// `--channels server:relay` names a server that is really there.
//
// Safety rules, because this modifies software we do not own:
// - never shim twice, never shim a shim (the real binary is identified by
// being a Mach-O, and recorded in a manifest);
// - verify the shim runs (`--version`) BEFORE leaving it in place, and roll
// back automatically if it does not;
// - `uninstallDesktopWake` restores the original byte-for-byte;
// - Desktop auto-updates the CLI into a NEW version directory, so this is
// re-applied per version by the daemon rather than assumed permanent.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFileSync } from "node:child_process";
const REAL_SUFFIX = ".relay-real";
const MANIFEST = "relay-desktop-wake.json";
const MACHO_MAGICS = new Set([0xfeedfacf, 0xcffaedfe, 0xfeedface, 0xcefaedfe, 0xcafebabe, 0xbebafeca]);
export function claudeCodeRoot({ home = os.homedir() } = {}) {
return path.join(home, "Library", "Application Support", "Claude", "claude-code");
}
/** Every installed CLI version directory, newest-looking last. */
export function installedCliVersions({ home = os.homedir() } = {}) {
let entries = [];
try {
entries = fs.readdirSync(claudeCodeRoot({ home }), { withFileTypes: true })
.filter((e) => e.isDirectory() && /^\d+\.\d+\.\d+/.test(e.name))
.map((e) => e.name);
} catch {
return [];
}
return entries.sort(compareVersions);
}
function compareVersions(a, b) {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < 3; i += 1) {
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
}
return a.localeCompare(b);
}
export function cliBinaryPath(version, { home = os.homedir() } = {}) {
return path.join(claudeCodeRoot({ home }), version, "claude.app", "Contents", "MacOS", "claude");
}
/** A real Mach-O executable (not our shell shim). */
export function isMachO(file) {
try {
const fd = fs.openSync(file, "r");
try {
const buf = Buffer.alloc(4);
fs.readSync(fd, buf, 0, 4, 0);
return MACHO_MAGICS.has(buf.readUInt32BE(0)) || MACHO_MAGICS.has(buf.readUInt32LE(0));
} finally {
fs.closeSync(fd);
}
} catch {
return false;
}
}
export function shimSource(realPath, channels) {
// `exec` so Desktop's process tree, signals and exit codes are unchanged —
// the shim must be invisible to everything except the argument list.
return `#!/bin/sh
# Installed by Relay so Claude Desktop sessions can be woken by an incoming
# relay (adds --channels; see src/desktop-wake.js). Remove with: relay wake-off
exec ${JSON.stringify(realPath)} --dangerously-load-development-channels ${JSON.stringify(channels)} "$@"
`;
}
function manifestPath({ home = os.homedir() } = {}) {
return path.join(home, ".relay", MANIFEST);
}
function readManifest(opts) {
try {
return JSON.parse(fs.readFileSync(manifestPath(opts), "utf8"));
} catch {
return { versions: {} };
}
}
function writeManifest(data, opts = {}) {
const file = manifestPath(opts);
try {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
} catch {}
}
export function wakeStatus({ home = os.homedir() } = {}) {
return installedCliVersions({ home }).map((version) => {
const bin = cliBinaryPath(version, { home });
const real = `${bin}${REAL_SUFFIX}`;
return {
version,
installed: fs.existsSync(real) && fs.existsSync(bin) && !isMachO(bin),
present: fs.existsSync(bin),
};
});
}
/**
* Shim one version. Returns { ok, version, reason }. Never throws: a failure
* must leave Claude Desktop exactly as it was.
*/
export function installDesktopWake(version, { home = os.homedir(), channels = "server:relay", log = () => {} } = {}) {
const bin = cliBinaryPath(version, { home });
const real = `${bin}${REAL_SUFFIX}`;
if (!fs.existsSync(bin)) return { ok: false, version, reason: "cli_missing" };
if (fs.existsSync(real) && !isMachO(bin)) return { ok: true, version, reason: "already_installed" };
// Refuse anything that is not the untouched Mach-O: never shim a shim.
if (!isMachO(bin)) return { ok: false, version, reason: "not_original_binary" };
try {
fs.renameSync(bin, real);
} catch (err) {
return { ok: false, version, reason: `rename_failed:${err.message}` };
}
try {
fs.writeFileSync(bin, shimSource(real, channels), { mode: 0o755 });
// Prove it before trusting it: the shim must launch the real CLI.
const out = execFileSync(bin, ["--version"], { encoding: "utf8", timeout: 30_000 });
if (!/\d+\.\d+\.\d+/.test(out)) throw new Error(`unexpected --version output: ${out.trim().slice(0, 80)}`);
} catch (err) {
// Roll back to the original, byte for byte.
try {
fs.rmSync(bin, { force: true });
fs.renameSync(real, bin);
} catch {}
log(`desktop wake install failed for ${version}: ${err.message}`);
return { ok: false, version, reason: `verify_failed:${err.message}` };
}
const manifest = readManifest({ home });
manifest.versions[version] = { installedAt: new Date().toISOString(), channels, real };
writeManifest(manifest, { home });
log(`desktop wake installed for CLI ${version}`);
return { ok: true, version, reason: "installed" };
}
/** Restore the original binary for one version. */
export function uninstallDesktopWake(version, { home = os.homedir(), log = () => {} } = {}) {
const bin = cliBinaryPath(version, { home });
const real = `${bin}${REAL_SUFFIX}`;
if (!fs.existsSync(real)) return { ok: true, version, reason: "not_installed" };
try {
if (fs.existsSync(bin) && !isMachO(bin)) fs.rmSync(bin, { force: true });
if (!fs.existsSync(bin)) fs.renameSync(real, bin);
else fs.rmSync(real, { force: true }); // original already back in place
} catch (err) {
return { ok: false, version, reason: `restore_failed:${err.message}` };
}
const manifest = readManifest({ home });
delete manifest.versions[version];
writeManifest(manifest, { home });
log(`desktop wake removed for CLI ${version}`);
return { ok: true, version, reason: "removed" };
}
/**
* Keep every installed CLI version shimmed. Desktop auto-updates into a new
* version directory, which silently drops the shim — so the daemon calls this
* periodically rather than treating installation as permanent.
*/
export function syncDesktopWake({ home = os.homedir(), channels = "server:relay", log = () => {} } = {}) {
if (process.platform !== "darwin") return [];
return installedCliVersions({ home }).map((version) =>
installDesktopWake(version, { home, channels, log }),
);
}
export function uninstallAllDesktopWake({ home = os.homedir(), log = () => {} } = {}) {
return installedCliVersions({ home }).map((version) => uninstallDesktopWake(version, { home, log }));
}