UNPKG

relay-companion

Version:

Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.

112 lines (102 loc) • 4.25 kB
// Which agent surfaces this machine can actually reach. // // The pill offers three destinations: Claude Code, Claude Cowork, and Codex. // Offering one the user does not have is a promise the verb cannot keep, so the // picker greys what is missing and says why. // // Detection is by RUNTIME, not by branding — each surface is reachable through a // different thing, and the thing that runs the work is what must exist: // // Claude Code — Claude Desktop's Code surface and its existing login. // Claude Cowork — Claude Desktop's Cowork surface and the same existing login. // Codex — the `codex` CLI (its app-server creates threads) or the // ChatGPT/Codex desktop app. // // NOTHING here needs an API key. Every surface rides the user's own installed, // already-logged-in tooling, so a run draws down THEIR subscription — which is // also why "installed" and "logged in" are different questions and both matter. import { execFileSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; function firstExisting(paths) { for (const p of paths) { try { if (p && fs.existsSync(p)) return p; } catch {} } return ""; } function onPath(binary) { try { const out = execFileSync("/usr/bin/which", [binary], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }); return String(out).trim(); } catch { return ""; } } export function claudeCliPath({ env = process.env, homedir = os.homedir() } = {}) { return ( String(env.RELAY_CLAUDE_CLI_PATH || "").trim() || firstExisting([ path.join(homedir, ".claude", "local", "claude"), "/opt/homebrew/bin/claude", "/usr/local/bin/claude", ]) || onPath("claude") ); } export function codexCliPath({ env = process.env } = {}) { return ( String(env.CODEX_CLI_PATH || "").trim() || firstExisting(["/opt/homebrew/bin/codex", "/usr/local/bin/codex"]) || onPath("codex") ); } export function appPath(appName, { platform = process.platform, homedir = os.homedir() } = {}) { if (platform !== "darwin") return ""; const names = appName === "Codex" ? ["ChatGPT", "Codex"] : [appName]; const roots = ["/Applications", path.join(homedir, "Applications")]; for (const name of names) { const hit = firstExisting(roots.map((root) => path.join(root, `${name}.app`))); if (hit) return hit; } return ""; } /** * @returns {{ [app: string]: { available: boolean, reason: string, via: string } }} * keyed by the exact names the pill's picker shows. */ export function detectAgentSurfaces(options = {}) { const codexCli = codexCliPath(options); const claudeCli = claudeCliPath(options); const chatgptApp = appPath("Codex", options); // ChatGPT.app (or Codex.app) const claudeApp = appPath("Claude", options); return { "Claude Code": claudeCli ? { available: true, reason: "", via: claudeCli } : { available: false, reason: "Claude Code isn’t installed on this Mac", via: "" }, "Claude Cowork": { available: false, reason: "Claude Cowork is temporarily unavailable in Relay", via: "", }, Codex: codexCli || chatgptApp ? { available: true, reason: "", via: codexCli || chatgptApp } : { available: false, reason: "Codex isn’t installed on this Mac", via: "" }, // THE APP, NOT THE CLI (Sven, 2026-08-17): "Open in Claude Code" opens the // Claude desktop app and "Open in Codex" the ChatGPT/Codex desktop app, so // for OPENING a relay the desktop app is what must exist — the CLI-keyed // entries above answer the other question, what can RUN a request. Off // macOS there is no desktop probe here, and an unknown app counts as // present: never hide a real app. _claudeDesktop: desktopSurface("Claude", claudeApp, options), _codexDesktop: desktopSurface("Codex", chatgptApp, options), }; } function desktopSurface(label, hit, { platform = process.platform } = {}) { if (platform !== "darwin") return { available: true, reason: "", via: "" }; return hit ? { available: true, reason: "", via: hit } : { available: false, reason: `${label} isn’t installed on this Mac`, via: "" }; }