@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
119 lines (118 loc) • 8.36 kB
JavaScript
// 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);
} };
}