UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

119 lines (118 loc) • 8.36 kB
// Continuation destinations expose login labels and quota, never auth material. import { z } from "zod"; import { dispatchProjectDirectory, PEER_COPILOT_REASON, projectName } from "./dispatch.js"; import { chooseAccount } from "./account-choice.js"; import { harnessInventoryWithin } from "./harnesses.js"; import { peerHost, isLocalComputer } from "./dispatch-hosts.js"; import { hookPeers, optionalHookPeers } from "./peers.js"; import { BridgeError } from "./protocol.js"; const account = z.object({ id: z.string().max(64), label: z.string().max(200), usable: z.boolean(), signedIn: z.boolean(), email: z.string().optional(), plan: z.string().optional(), current: z.boolean().default(false), fiveHourResetsAt: z.string().optional(), weekResetsAt: z.string().optional(), fiveHourLeft: z.number().min(0).max(100).optional(), weekLeft: z.number().min(0).max(100).optional(), resetsAt: z.string().optional() }); const availability = z.object({ computerId: z.string().uuid().optional(), hasCheckout: z.boolean(), harnesses: z.array(z.object({ source: z.enum(["claude", "codex", "opencode", "copilot"]), usable: z.boolean(), reason: z.string().optional(), accounts: z.array(account).optional(), })) }); export async function localMoveAvailability(project, usage, conductor = false) { const name = project === null || project === undefined || project === "" ? undefined : projectName.parse(project); const [checkout, found, limits] = await Promise.all([ conductor ? Promise.resolve(true) : name ? dispatchProjectDirectory(name).then(() => true, () => false) : Promise.resolve(false), harnessInventoryWithin(2_500), usage().catch(() => ({})), ]); const { claudeMoveAccounts } = await import("./hand-off.js"); const rooms = claudeMoveAccounts(limits); const { claudeHomes, claudeAccountRef, claudeAccountEmail, claudeAccountSubscription } = await import("./claude-accounts.js"); const discovered = found?.harnesses ?? []; const claude = discovered.find(row => row.source === "claude"); const accounts = [...claude?.accounts ?? []]; for (const home of claudeHomes()) if (!accounts.some(login => login.id === home.id)) { const ref = claudeAccountRef(home), email = claudeAccountEmail(home), subscription = claudeAccountSubscription(home); accounts.push({ ...ref, usable: false, signedIn: false, ...(email ? { email } : {}), ...(subscription?.plan ? { plan: subscription.plan } : {}) }); } const inventory = claude ? discovered.map(row => row.source === "claude" ? { ...row, accounts } : row) : [...discovered, { source: "claude", installed: false, usable: false, accounts }]; return availability.parse({ hasCheckout: checkout, harnesses: inventory.filter(row => row.source !== "phren").map(row => ({ source: row.source, usable: row.usable, ...(row.accounts ? { accounts: row.accounts.map(login => { const room = row.source === "claude" ? rooms.find(item => item.id === login.id) : undefined; return { ...room, id: login.id, label: login.label ?? login.id, usable: login.usable && room?.fiveHourLeft !== 0 && room?.weekLeft !== 0, signedIn: login.signedIn, ...(login.email ? { email: login.email } : {}), ...(login.plan ? { plan: login.plan } : {}) }; }) } : {}), })) }); } /** Exact verified names only; self also accepts the name health displays. */ export async function movePeer(computer, self) { if (computer === undefined || computer === self) return undefined; const peers = await hookPeers().catch(error => { if (error instanceof BridgeError && error.details?.hooksYaml === "missing") return []; throw error; }); const peer = peers.find(row => row.name === computer); if (peer) return peerHost(peer); if (isLocalComputer(computer)) return undefined; throw new BridgeError(404, "Unknown computer. Add its verified connection to hooks.yaml."); } export async function peerMoveAvailability(peer, project, conductor = false) { try { return availability.parse(await peer.request(`/v1/sessions/move/availability?project=${encodeURIComponent(project)}${conductor ? "&conductor=1" : ""}`, undefined, 10_000)); } catch { throw new BridgeError(409, `Computer ${peer.name} is offline or cannot report continuation availability.`); } } function chosenLogin(accounts) { return chooseAccount(accounts.map(login => ({ account: login.id, usable: login.usable, exhausted: login.fiveHourLeft === 0 || login.weekLeft === 0, fiveHour: { leftPercent: login.fiveHourLeft }, week: { leftPercent: login.weekLeft }, }))).choice; } export async function checkMovePeer(peer, project, to, conductor = false) { if (to.harness === "copilot") throw new BridgeError(409, PEER_COPILOT_REASON); const facts = await peerMoveAvailability(peer, project, conductor); if (!facts.hasCheckout) throw new BridgeError(409, `Computer ${peer.name} has no checkout of ${project}.`); const harness = facts.harnesses.find(row => row.source === to.harness); const choice = to.harness === "claude" && !to.account && harness?.accounts ? chosenLogin(harness.accounts) : undefined; const login = harness?.accounts?.find(row => row.id === (to.account ?? choice?.account ?? "default")); if (!harness?.usable || (harness.accounts && !login?.usable) || login?.fiveHourLeft === 0 || login?.weekLeft === 0) { throw new BridgeError(409, `The harness or account is not usable on ${peer.name}.`); } if (!facts.computerId) throw new BridgeError(409, `Update ${peer.name}'s Hook to verify its continuation identity.`); return { ...(facts.computerId ? { computerId: facts.computerId } : {}), ...(choice ? { account: choice.account, accountChoice: choice.reason } : {}) }; } export async function moveComputers(project, self, local, currentAccount, conductor = false) { const name = project === null || project === undefined || project === "" ? "" : projectName.parse(project); const { peers } = await optionalHookPeers(); const here = await local(); const publicFacts = (facts, remote = false) => ({ ...(facts.computerId ? { computerId: facts.computerId } : {}), hasCheckout: facts.hasCheckout, harnesses: facts.harnesses.map(row => ({ source: row.source, usable: remote && row.source === "copilot" ? false : row.usable, ...(remote && row.source === "copilot" ? { reason: PEER_COPILOT_REASON } : {}), ...(row.source === "claude" && row.accounts ? { selectedAccount: chosenLogin(row.accounts)?.account } : {}), ...(row.accounts ? { accounts: row.accounts.map(({ signedIn: _signedIn, ...login }) => ({ ...login, current: !remote && row.source === "claude" && login.id === currentAccount })) } : {}), })) }); const computers = await Promise.all(peers.map(async (peer) => { try { return { name: peer.name, self: false, online: true, ...publicFacts(await peerMoveAvailability(peerHost(peer), name, conductor), true) }; } catch { return { name: peer.name, self: false, online: false, hasCheckout: false, harnesses: [] }; } })); return { computers: [{ name: self, self: true, online: true, ...publicFacts(here) }, ...computers], localClaudeLogins: here.harnesses.find(row => row.source === "claude")?.accounts?.filter(login => login.signedIn).length ?? 0 }; } /** Re-read enrollment before every request that may transmit a brief. */ export async function currentMovePeer(captured) { const current = await movePeer(captured.name).catch(() => undefined); if (!current || !current.fingerprint || current.fingerprint !== captured.fingerprint) throw new BridgeError(409, "The destination enrollment was revoked or changed; no brief was sent."); return { ...current, request: async (route, data, timeout) => { const enrolled = await movePeer(captured.name).catch(() => undefined); if (!enrolled || enrolled.fingerprint !== captured.fingerprint) throw new BridgeError(409, "The destination enrollment was revoked or changed; no brief was sent."); return enrolled.request(route, data, timeout); } }; }