@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
83 lines (82 loc) • 2.88 kB
JavaScript
/**
* Collect the agents running on this machine and join them onto the graph.
*
* This module stays free of heavy imports on purpose: the graph view calls it
* on a timer, and `cli-registry.ts` treats cold start as a constraint. The
* project lookup is injected rather than imported, which keeps the FTS index
* out of this path and makes the join testable without a store.
*/
import { isFeatureEnabled } from "../utils-helpers.js";
import { debugLog } from "../shared.js";
import { errorMessage } from "../utils.js";
/** Off by default, like every other optional feature here. */
export function agentsEnabled() {
return isFeatureEnabled("PHREN_FEATURE_AGENTS", false);
}
/**
* Ask every available provider, in order. A provider that throws or hangs is
* skipped rather than allowed to break a repaint; duplicates by id keep the
* first answer, so an earlier provider wins.
*/
export function collectAgents(providers) {
const seen = new Set();
const out = [];
for (const provider of providers) {
let records = [];
try {
if (!provider.available())
continue;
records = provider.list();
}
catch (err) {
debugLog(`agent provider ${provider.name}: ${errorMessage(err)}`);
continue;
}
for (const record of records) {
const key = `${record.provider ?? provider.name}:${record.id}`;
if (seen.has(key))
continue;
seen.add(key);
out.push({ provider: provider.name, ...record });
}
}
return out;
}
/**
* Attach each agent to the phren project its directory belongs to. The
* resolver is the same one the hooks use, so a git worktree lands on the
* repository it came from. Agents outside any project keep a null project and
* still appear in the list.
*/
export function joinAgents(records, resolve) {
const cache = new Map();
return records.map((record) => {
let project = cache.get(record.cwd);
if (project === undefined) {
try {
project = resolve(record.cwd);
}
catch (err) {
debugLog(`agent join ${record.cwd}: ${errorMessage(err)}`);
project = null;
}
cache.set(record.cwd, project);
}
return { ...record, project };
});
}
/** Sort for display: what you are looking at first, then busy, then the rest. */
export function sortAgents(agents) {
const rank = (a) => {
if (a.focused)
return 0;
if (a.status === "working")
return 1;
if (a.status === "error")
return 2;
if (a.status === "idle")
return 3;
return 4;
};
return agents.slice().sort((a, b) => rank(a) - rank(b) || a.label.localeCompare(b.label));
}