UNPKG

@phren/cli

Version:

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

73 lines (72 loc) • 4.74 kB
// What this computer can launch: each harness's install state and, for Claude and Codex, its accounts. // `phren` is phren's own agent, run as `phren agent`; it needs no account here. // Served as GET /v1/harnesses and as `harnesses` on /v1/dispatch/capacity. See docs/accounts.md. import { existsSync } from "node:fs"; import path from "node:path"; import { codexHome } from "../home-paths.js"; import { claudeAccountRef, claudeAuthStatus, claudeHomes, CODEX_ACCOUNT } from "./claude-accounts.js"; import { toolVersion } from "./health.js"; const SOURCES = ["claude", "codex", "opencode", "copilot", "phren"]; /** The binary is `phren`; the agent answers `phren agent --version` from the separate @phren/agent package. */ const probeSource = (source) => source === "phren" ? toolVersion("phren agent", "phren", ["agent"]) : toolVersion(source); export async function harnessInventory(deps = {}) { const env = deps.env ?? process.env; const probe = deps.toolVersion ?? probeSource; const harnesses = await Promise.all(SOURCES.map(async (source) => { const found = await probe(source); // Only a missing binary is definite; a slow or failing --version (a loaded computer) stays usable. if (found.status === "missing") return { source, installed: false, usable: false, reason: "Not installed" }; // `phren agent` without @phren/agent prints an install hint and no version. if (source === "phren" && found.status === "error" && found.detail?.startsWith("--version exited")) { return { source, installed: false, usable: false, reason: "Not installed: npm install -g @phren/agent" }; } const base = { source, installed: true, ...(found.version ? { version: found.version } : {}) }; if (source === "claude") { const accounts = await Promise.all(claudeHomes(env).map(async (home) => { const auth = await claudeAuthStatus(home, deps.authRunner); // An unanswered sign-in check stays usable: only Claude saying "logged out" hides an account. const usable = auth.signedIn || auth.unknown === true; return { ...claudeAccountRef(home), signedIn: auth.signedIn, usable, ...(auth.plan ? { plan: auth.plan } : {}), ...(auth.signedIn ? {} : { reason: auth.reason ?? "Not signed in" }) }; })); const usable = accounts.some(account => account.usable); return { ...base, usable, ...(usable ? {} : { reason: "No signed-in Claude account" }), accounts }; } if (source === "codex") { const signedIn = existsSync(path.join(codexHome(env), "auth.json")); return { ...base, usable: signedIn, ...(signedIn ? {} : { reason: "Not signed in" }), accounts: [{ ...CODEX_ACCOUNT, signedIn, usable: signedIn, ...(signedIn ? {} : { reason: "Not signed in" }) }] }; } return { ...base, usable: true }; })); return { harnesses }; } /** The inventory, or undefined when it is not ready within `ms` (a cold `claude auth status` can take seconds). */ export function harnessInventoryWithin(ms, deps = {}) { let timer; const late = new Promise(resolve => { timer = setTimeout(() => resolve(undefined), ms); }); return Promise.race([harnessInventory(deps).catch(() => undefined), late]).finally(() => clearTimeout(timer)); } /** `PHREN_LAUNCH_CHECK=off`: no early launch refusal and no `harnesses` advertised to dispatch (tests, or a misreporting computer). */ export function launchCheckOff(env = process.env) { return /^(off|0|false|no)$/i.test(env.PHREN_LAUNCH_CHECK?.trim() ?? ""); } /** Whether a launch of `source` (and `accountId`) should work on a computer with this inventory. */ export function hasUsable(inventory, source, accountId) { const entry = inventory.harnesses.find(item => item.source === source); if (!entry || !entry.installed) return { ok: false, code: "harness_unavailable", reason: entry?.reason ?? "Not installed" }; if (!entry.accounts) { return accountId && accountId !== "default" ? { ok: false, code: "account_unavailable", reason: `${source} has no accounts` } : entry.usable ? { ok: true } : { ok: false, code: "harness_unavailable", reason: entry.reason ?? "Unavailable" }; } const id = accountId?.trim() || "default"; const account = entry.accounts.find(item => item.id === id); if (!account) return { ok: false, code: "account_unavailable", reason: `No ${source} account "${id}"` }; if (!account.usable) return { ok: false, code: "account_unavailable", reason: account.reason ?? "Not signed in" }; return { ok: true }; }