UNPKG

@phren/cli

Version:

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

135 lines (134 loc) • 9.29 kB
import { hostname } from "node:os"; import { disabledHint } from "../modules/registry.js"; import { ownHostKey } from "./health.js"; import { computerLabel } from "./computer-identity.js"; import { peerRequest } from "./peers.js"; import { BridgeError, errorCode, object } from "./protocol.js"; export async function localCaller() { const hostKey = await ownHostKey(); return { name: hostname(), ...(hostKey ? { hostKey } : {}) }; } export function callerQuery(caller) { return new URLSearchParams({ name: caller.name, ...(caller.hostKey ? { hostKey: caller.hostKey } : {}) }).toString(); } export async function askPeers(peers, caller, ask = peerRequest) { return Promise.all(peers.map(async (peer) => { try { const answer = await ask(peer, `/v1/conductor?${callerQuery(caller)}`, undefined, 8_000); const computer = object(answer.computer); const conductor = answer.conductor && typeof answer.conductor === "object" ? object(answer.conductor) : undefined; const set = object(answer.set); return { peer, ok: true, ...(typeof computer.id === "string" ? { id: computer.id } : {}), names: [computer.name, ...(Array.isArray(computer.aliases) ? computer.aliases : [])].filter((name) => typeof name === "string"), ...(conductor ? { conductor } : {}), ...(Array.isArray(answer.peers) ? { peers: answer.peers.filter((name) => typeof name === "string").slice(0, 32) } : {}), ...(typeof answer.knowsCaller === "boolean" ? { knowsCaller: answer.knowsCaller } : {}), ...(typeof set.namedAt === "string" && (typeof set.name === "string" || set.name === null) ? { set: { name: set.name, namedAt: set.namedAt } } : {}) }; } catch (error) { // With its conductor module off, a peer cannot run a conductor. An // older Hook has no /v1/conductor and cannot rule one out. if (error instanceof BridgeError && error.status === 404 && error.message === disabledHint("conductor")) return { peer, ok: false, error: "Its conductor module is off.", disabled: true }; const message = error instanceof BridgeError && error.status === 404 ? "Its Hook is too old to report a conductor." : error instanceof Error ? error.message : "Unreachable."; const code = errorCode(error); return { peer, ok: false, error: message, ...(code ? { code } : {}) }; } })); } /** A peer outside this computer's set: it answered and does not list this computer. */ const oneWay = (answer) => answer.ok && answer.knowsCaller === false; /** The names members report that are neither this computer nor one of its * peers: computers in the set that this Hook cannot ask directly. */ export function indirectMembers(answers, localNames) { const known = new Set([...localNames, ...answers.flatMap(answer => [answer.peer.name, answer.peer.address, ...(answer.ok ? answer.names : [])])].map(computerLabel)); const found = new Map(); for (const answer of answers) { if (!answer.ok || oneWay(answer)) continue; for (const name of answer.peers ?? []) if (!known.has(computerLabel(name)) && !found.has(computerLabel(name))) found.set(computerLabel(name), name); } return [...found.values()].sort((a, b) => a.localeCompare(b)); } export const linkHint = (name) => `Link it with phren bridge link ${name}.`; export const oneWayHint = (name) => `${name} does not link back. Run phren bridge link ${name}.`; /** One conductor per set: a live conductor on any member, and the members that could not say. */ export function setConductor(answers, localNames) { const members = answers.filter(answer => !oneWay(answer)); const hit = members.find(answer => answer.ok && answer.conductor); const target = hit?.ok ? object(hit.conductor).target : undefined; return { ...(hit ? { found: { computer: hit.peer.name, ...(target ? { target: object(target) } : {}) } } : {}), unchecked: [ ...members.flatMap(answer => !answer.ok && !answer.disabled ? [{ computer: answer.peer.name, error: answer.error, ...(answer.code ? { code: answer.code } : {}) }] : []), ...indirectMembers(answers, localNames).map(computer => ({ computer, error: `Not linked with this computer, so it could not be asked. ${linkHint(computer)}` })), ], }; } export async function groupConductor(peers, localNames, ask = peerRequest, caller) { return setConductor(await askPeers(peers, caller ?? await localCaller(), ask), localNames); } const newest = (names) => names.filter((name) => !!name) .sort((a, b) => Date.parse(b.namedAt) - Date.parse(a.namedAt))[0]; const setId = (ids, fallback) => { const known = ids.filter((value) => !!value).sort(); return `set:${known[0] ?? fallback.toLowerCase()}`; }; function conductorOf(entries) { const running = entries.filter(entry => entry.conductor); const first = running[0]; const target = first ? object(first.conductor).target : undefined; return { ...(first ? { conductor: { computer: first.computer, ...(target ? { target: object(target) } : {}) } } : {}), conductors: running.length }; } /** * Every set this Hook can see: its own (itself, its two-way and not yet known * peers, and computers they link that this Hook does not), then one set per * one-way peer. `unlinked` are computers the store knows with no link here. */ export function buildSets(input) { const { local } = input; // A peer's directory can name this computer differently from its hostname. // Only a matching id establishes that the peer's name belongs to self. const reports = input.reportedComputers ?? []; const selfReports = local.id ? reports.filter(row => row.id === local.id) : []; const selfAnswers = input.answers.filter(answer => answer.ok && local.id && answer.id === local.id); const selfNames = [...local.names, local.name, ...selfReports.flatMap(row => [row.name, ...row.aliases]), ...selfAnswers.flatMap(answer => [answer.peer.name, answer.peer.address, ...(answer.ok ? answer.names : [])])]; const reportedSelf = indirectMembers(input.answers, local.names).find(name => selfReports.some(row => [row.name, ...row.aliases].some(alias => computerLabel(alias) === computerLabel(name)))); const selfName = reportedSelf ?? selfAnswers.map(answer => answer.peer.name).sort()[0] ?? local.name; const answers = input.answers.filter(answer => !(answer.ok && local.id && answer.id === local.id)); const members = answers.filter(answer => !oneWay(answer)); const computers = [{ name: selfName, ...(local.id ? { id: local.id } : {}), local: true, reachable: true, link: "self", ...(local.conductor ? { conductor: local.conductor } : {}) }]; for (const answer of members) { if (answer.ok) computers.push({ name: answer.peer.name, ...(answer.id ? { id: answer.id } : {}), reachable: true, link: answer.knowsCaller === true ? "two-way" : "unknown", ...(answer.conductor ? { conductor: answer.conductor } : {}), ...(answer.knowsCaller === undefined ? { hint: "Its Hook cannot say whether it links back. Update it with phren bridge update." } : {}) }); else computers.push({ name: answer.peer.name, reachable: !!answer.disabled, link: "unknown", error: answer.error, ...(answer.code ? { code: answer.code } : {}) }); } const indirect = indirectMembers(answers, selfNames); for (const name of indirect) computers.push({ name, link: "indirect", hint: linkHint(name) }); const name = newest([local.set, ...members.map(answer => answer.ok ? answer.set : undefined)]); const own = { id: setId([local.id, ...members.map(answer => answer.ok ? answer.id : undefined)], local.name), ...(name?.name ? { name: name.name, namedAt: name.namedAt } : {}), local: true, computers, ...conductorOf([{ computer: selfName, conductor: local.conductor }, ...members.map(answer => ({ computer: answer.peer.name, conductor: answer.ok ? answer.conductor : undefined }))]) }; const others = answers.flatMap(answer => { if (!answer.ok || !oneWay(answer)) return []; return [{ id: setId([answer.id], answer.peer.name), ...(answer.set?.name ? { name: answer.set.name, namedAt: answer.set.namedAt } : {}), local: false, computers: [{ name: answer.peer.name, ...(answer.id ? { id: answer.id } : {}), reachable: true, link: "one-way", ...(answer.conductor ? { conductor: answer.conductor } : {}), hint: oneWayHint(answer.peer.name) }], ...conductorOf([{ computer: answer.peer.name, conductor: answer.conductor }]) }]; }); const indirectLabels = new Set(indirect.map(computerLabel)); const selfLabels = new Set(selfNames.map(computerLabel)); const unlinked = input.computers.filter(row => !row.local && !row.linked && !(local.id && row.id === local.id) && ![row.name, ...row.aliases].some(known => selfLabels.has(computerLabel(known)) || indirectLabels.has(computerLabel(known)))); return { sets: [own, ...others], unlinked }; }