@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
439 lines (438 loc) • 22.4 kB
JavaScript
import { moduleEnabled } from "./modules/runtime.js";
import * as fs from "fs";
import * as path from "path";
import { fileURLToPath } from "url";
import { findPhrenPath, getProjectDirs, EXEC_TIMEOUT_QUICK_MS, debugLog, isRecord, hookConfigPath, homeDir, readRootManifest, } from "./shared.js";
import { buildIndex, detectProject, findFtsCacheForPath, listIndexedDocumentPaths, queryRows } from "./shared/index.js";
import { mergeConfig, getWorkflowPolicy } from "./shared/governance.js";
import { getMcpEnabledPreference, getHooksEnabledPreference } from "./init/init.js";
import { getManagementPreset, resolveManagementCapabilities } from "./init/management-preset.js";
import { getTelemetrySummary } from "./telemetry.js";
import { runGit, errorMessage } from "./utils.js";
import { logger } from "./logger.js";
import { readRuntimeHealth, resolveTaskFilePath, FINDINGS_FILENAME } from "./data/access.js";
import { assessSyncOutage } from "./shared/governance.js";
import { resolveRuntimeProfile } from "./runtime-profile.js";
import { describeProfileMapping } from "./profile-store.js";
import { renderPhrenArt } from "./phren-art.js";
import { RESET, BOLD, DIM, GREEN, YELLOW, RED, CYAN } from "./shell/render.js";
import { storeWeight } from "./store-weight.js";
import { activeStoreAuthFailure, storeAuthDetail } from "./sync/auth.js";
import { describeAutoSave } from "./sync/outcome.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function readPackageVersion() {
try {
const pkgPath = path.resolve(__dirname, "..", "package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
return typeof pkg.version === "string" ? pkg.version : "unknown";
}
catch (err) {
if ((process.env.PHREN_DEBUG))
logger.debug("status", `readPackageVersion: ${errorMessage(err)}`);
return "unknown";
}
}
function check(ok) {
return ok ? `${GREEN}ok${RESET}` : `${RED}missing${RESET}`;
}
function countBullets(filePath) {
if (!fs.existsSync(filePath))
return 0;
const content = fs.readFileSync(filePath, "utf8");
return content.split("\n").filter((l) => l.startsWith("- ")).length;
}
function countQueueItems(phrenPath, project) {
const queueFile = path.join(phrenPath, project, "review.md");
return countBullets(queueFile);
}
function hasCommandHook(value) {
if (!Array.isArray(value))
return false;
return value.some((entry) => {
if (!isRecord(entry) || !Array.isArray(entry.hooks))
return false;
return entry.hooks.some((hook) => isRecord(hook) && typeof hook.command === "string" && (hook.command.includes("phren") || hook.command.includes("phren")));
});
}
export async function runStatus() {
const phrenPath = findPhrenPath();
if (!phrenPath) {
console.log(`${RED}phren not found${RESET}. Run ${CYAN}npx @phren/cli init${RESET} to set up.`);
process.exit(1);
}
const cwd = process.cwd();
const manifest = readRootManifest(phrenPath);
const profile = resolveRuntimeProfile(phrenPath);
const activeProject = detectProject(phrenPath, cwd, profile);
const version = readPackageVersion();
console.log("");
console.log(renderPhrenArt(" "));
console.log(`\n${BOLD}phren${RESET} ${DIM}v${version}${RESET}\n`);
// Active project
if (activeProject) {
console.log(` ${DIM}project${RESET} ${activeProject}`);
// Effective config for this project
try {
const resolved = mergeConfig(phrenPath, activeProject);
const globalWorkflow = getWorkflowPolicy(phrenPath);
const projectSensitivity = resolved.findingSensitivity !== globalWorkflow.findingSensitivity
? `${resolved.findingSensitivity} ${DIM}(project override)${RESET}`
: resolved.findingSensitivity;
const projectTaskMode = resolved.taskMode !== globalWorkflow.taskMode
? `${resolved.taskMode} ${DIM}(project override)${RESET}`
: resolved.taskMode;
console.log(` ${DIM}sensitivity${RESET} ${projectSensitivity}`);
if (moduleEnabled(phrenPath, "tasks", profile))
console.log(` ${DIM}task mode${RESET} ${projectTaskMode}`);
if (resolved.proactivity.base || resolved.proactivity.findings || resolved.proactivity.tasks) {
const parts = [];
if (resolved.proactivity.base)
parts.push(`base:${resolved.proactivity.base}`);
if (resolved.proactivity.findings)
parts.push(`findings:${resolved.proactivity.findings}`);
if (moduleEnabled(phrenPath, "tasks", profile) && resolved.proactivity.tasks)
parts.push(`tasks:${resolved.proactivity.tasks}`);
console.log(` ${DIM}proactivity${RESET} ${parts.join(" ")} ${DIM}(project override)${RESET}`);
}
}
catch (err) {
if ((process.env.PHREN_DEBUG))
logger.debug("status", `statusConfig: ${errorMessage(err)}`);
}
}
// Phren path and config
console.log(` ${DIM}path${RESET} ${phrenPath}`);
console.log(` ${DIM}mode${RESET} ${manifest?.installMode || "unknown"}`);
try {
const w = storeWeight(phrenPath, profile);
const k = (n) => (n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : String(n));
console.log(` ${DIM}weight${RESET} ${k(w.findings)} words of findings · ${k(w.reference)} archived${moduleEnabled(phrenPath, "tasks", profile) ? ` · ${k(w.tasks)} in tasks` : ""} · ${k(w.skills)} in skills · AGENTS.md ${w.globalClaude} words`);
}
catch (err) {
logger.debug("status", `weight: ${errorMessage(err)}`);
}
if (manifest?.workspaceRoot) {
console.log(` ${DIM}workspace${RESET} ${manifest.workspaceRoot}`);
}
if (manifest?.syncMode) {
console.log(` ${DIM}sync${RESET} ${manifest.syncMode}`);
}
if (profile) {
const mapping = describeProfileMapping(phrenPath);
const assumed = !process.env.PHREN_PROFILE?.trim() && !mapping.mapped && mapping.assumed === profile;
const suffix = assumed
? ` ${YELLOW}(assumed; machine "${mapping.machine}" is not mapped in machines.yaml)${RESET}`
: "";
console.log(` ${DIM}profile${RESET} ${profile}${suffix}`);
}
// Management preset + MCP + hooks status
const preset = getManagementPreset(phrenPath);
const caps = resolveManagementCapabilities(phrenPath);
const mcpEnabled = getMcpEnabledPreference(phrenPath);
const hooksEnabled = getHooksEnabledPreference(phrenPath);
console.log(` ${DIM}preset${RESET} ${preset}`);
console.log(` ${DIM}mcp${RESET} ${mcpEnabled ? `${GREEN}on${RESET}` : `${YELLOW}off${RESET}`}`);
console.log(` ${DIM}hooks${RESET} ${hooksEnabled ? `${GREEN}on${RESET}` : `${YELLOW}off${RESET}`}`);
// What phren touches on this machine under the current preset.
const touches = [
caps.linkGlobalClaudeMd ? "~/.claude/CLAUDE.md" : null,
caps.installSkillLinks ? "~/.claude/skills" : null,
caps.installWrappers ? "~/.local/bin wrappers" : null,
caps.repoMirroring ? "repo mirrors" : null,
].filter(Boolean);
console.log(` ${DIM}touches${RESET} ${touches.length ? touches.join(", ") : `${DIM}store only${RESET}`}`);
// Hook health: check ~/.claude/settings.json
let hooksInstalled = false;
let mcpConfigured = false;
if (manifest?.installMode === "project-local" && manifest.workspaceRoot) {
const workspaceMcp = path.join(manifest.workspaceRoot, ".vscode", "mcp.json");
if (fs.existsSync(workspaceMcp)) {
try {
const parsed = JSON.parse(fs.readFileSync(workspaceMcp, "utf8"));
const settings = isRecord(parsed) ? parsed : {};
const servers = isRecord(settings.servers) ? settings.servers : undefined;
mcpConfigured = Boolean(servers?.phren || servers?.phren);
}
catch (err) {
if ((process.env.PHREN_DEBUG))
logger.debug("status", `statusWorkspaceMcp parse: ${errorMessage(err)}`);
}
}
}
else {
const settingsPath = hookConfigPath("claude");
if (fs.existsSync(settingsPath)) {
try {
const parsed = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
const settings = isRecord(parsed) ? parsed : {};
const mcpServers = isRecord(settings.mcpServers) ? settings.mcpServers : undefined;
const hooks = isRecord(settings.hooks) ? settings.hooks : undefined;
mcpConfigured = Boolean(mcpServers?.phren || mcpServers?.phren);
const hookEvents = ["UserPromptSubmit", "Stop", "SessionStart"];
hooksInstalled = hookEvents.every((event) => hasCommandHook(hooks?.[event]));
}
catch (err) {
if ((process.env.PHREN_DEBUG))
logger.debug("status", `statusHooks settingsParse: ${errorMessage(err)}`);
}
}
}
console.log(` ${DIM}mcp cfg${RESET} ${check(mcpConfigured)} ${DIM}(${manifest?.installMode === "project-local" ? ".vscode/mcp.json" : "settings.json"})${RESET}`);
if (manifest?.installMode === "project-local") {
console.log(` ${DIM}hooks cfg${RESET} ${DIM}n/a in project-local mode${RESET}`);
}
else {
console.log(` ${DIM}hooks cfg${RESET} ${check(hooksInstalled)} ${DIM}(settings.json)${RESET}`);
}
// FTS index health
let ftsIndexOk = false;
let ftsIndexSize = 0;
let ftsDocCount = null;
try {
const cache = findFtsCacheForPath(phrenPath, profile);
ftsIndexOk = cache.exists;
ftsIndexSize = cache.sizeBytes ?? 0;
if (!ftsIndexOk) {
const db = await buildIndex(phrenPath, profile || undefined);
const healthRow = queryRows(db, "SELECT count(*) FROM docs", []);
const count = Number(healthRow?.[0]?.[0] ?? 0);
if (Number.isFinite(count) && count >= 0) {
ftsIndexOk = true;
ftsDocCount = count;
}
}
}
catch (err) {
if ((process.env.PHREN_DEBUG))
logger.debug("status", `statusFtsIndex: ${errorMessage(err)}`);
}
const ftsLabel = ftsIndexOk
? `${GREEN}ok${RESET} ${DIM}(${ftsIndexSize > 0 ? `${(ftsIndexSize / 1024).toFixed(0)} KB` : `${ftsDocCount ?? 0} docs`})${RESET}`
: `${YELLOW}not built${RESET} ${DIM}(run a search to build)${RESET}`;
console.log(` ${DIM}fts${RESET} ${ftsLabel}`);
try {
const { getOllamaUrl, checkOllamaAvailable, checkModelAvailable, getEmbeddingModel } = await import("./shared/ollama.js");
const { getEmbeddingCache, formatEmbeddingCoverage } = await import("./shared/embedding-cache.js");
const ollamaUrl = getOllamaUrl();
if (!ollamaUrl) {
console.log(` ${DIM}semantic${RESET} ${DIM}disabled (optional)${RESET}`);
}
else {
const available = await checkOllamaAvailable();
if (!available) {
console.log(` ${DIM}semantic${RESET} ${YELLOW}offline${RESET} ${DIM}(${ollamaUrl})${RESET}`);
}
else {
const modelReady = await checkModelAvailable();
const model = getEmbeddingModel();
if (!modelReady) {
console.log(` ${DIM}semantic${RESET} ${YELLOW}model missing${RESET} ${DIM}(${model})${RESET}`);
}
else {
const cache = getEmbeddingCache(phrenPath);
await cache.load().catch(() => { });
const coverage = cache.coverage(listIndexedDocumentPaths(phrenPath, profile || undefined));
console.log(` ${DIM}semantic${RESET} ${GREEN}ready${RESET} ${DIM}(${model}; ${formatEmbeddingCoverage(coverage)})${RESET}`);
}
}
}
}
catch (err) {
if ((process.env.PHREN_DEBUG))
logger.debug("status", `statusSemantic: ${errorMessage(err)}`);
}
// Agent integration status
function hasPhrenEntry(filePath) {
if (!fs.existsSync(filePath))
return false;
try {
const raw = fs.readFileSync(filePath, "utf8");
return raw.includes('"phren"') || raw.includes("'phren'") || raw.includes('"phren"') || raw.includes("'phren'");
}
catch (err) {
if ((process.env.PHREN_DEBUG))
logger.debug("status", `hasPhrenEntry: ${errorMessage(err)}`);
return false;
}
}
function agentConfigured(candidates) {
return candidates.some(hasPhrenEntry);
}
const home = homeDir();
const agentChecks = manifest?.installMode === "project-local" && manifest.workspaceRoot ? [
{
name: "VS Code (workspace)",
configured: fs.existsSync(path.join(manifest.workspaceRoot, ".vscode", "mcp.json")),
},
] : [
{
name: "Claude Code",
configured: agentConfigured([
path.join(home, ".claude.json"),
path.join(home, ".claude", "settings.json"),
]),
},
{
name: "Cursor",
configured: agentConfigured([
path.join(home, ".cursor", "mcp.json"),
path.join(home, ".config", "Cursor", "User", "mcp.json"),
path.join(home, "Library", "Application Support", "Cursor", "User", "mcp.json"),
]),
},
{
name: "Copilot CLI",
configured: agentConfigured([
path.join(home, ".copilot", "mcp-config.json"),
path.join(home, ".config", "github-copilot", "mcp.json"),
path.join(home, "Library", "Application Support", "github-copilot", "mcp.json"),
]),
},
{
name: "Codex",
configured: agentConfigured([
path.join(home, ".codex", "config.json"),
path.join(home, ".codex", "mcp.json"),
path.join(home, ".codex", "config.toml"),
]),
},
{
name: "Windsurf",
configured: agentConfigured([
path.join(home, ".windsurf", "mcp.json"),
path.join(home, ".config", "Windsurf", "User", "mcp.json"),
path.join(home, "Library", "Application Support", "Windsurf", "User", "mcp.json"),
]),
},
];
const configuredAgents = agentChecks.filter((a) => a.configured).map((a) => a.name);
const missingAgents = agentChecks.filter((a) => !a.configured).map((a) => a.name);
if (configuredAgents.length > 0) {
console.log(` ${DIM}agents${RESET} ${GREEN}${configuredAgents.join(", ")}${RESET}`);
}
if (missingAgents.length > 0) {
console.log(` ${DIM} Not configured: ${missingAgents.join(", ")} — run phren init to add${RESET}`);
}
// Stores
try {
const { resolveAllStores } = await import("./store-registry.js");
const stores = resolveAllStores(phrenPath);
if (stores.length > 0) {
const primaryCount = stores.filter((s) => s.role === "primary").length;
const teamCount = stores.filter((s) => s.role === "team").length;
const readonlyCount = stores.filter((s) => s.role === "readonly").length;
const roleParts = [];
if (primaryCount > 0)
roleParts.push(`${primaryCount} primary`);
if (teamCount > 0)
roleParts.push(`${teamCount} team`);
if (readonlyCount > 0)
roleParts.push(`${readonlyCount} readonly`);
console.log(`\n ${BOLD}Stores${RESET} ${DIM}(${stores.length} stores: ${roleParts.join(", ")})${RESET}`);
for (const store of stores) {
const exists = store.available !== false;
const existsLabel = exists ? `${GREEN}yes${RESET}` : `${RED}no${RESET}`;
const auth = exists ? activeStoreAuthFailure(store.path) : undefined;
const syncDetail = auth ? ` ${YELLOW}${storeAuthDetail(auth)}${RESET}` : store.remote ? ` remote=${DIM}${store.remote}${RESET}` : "";
console.log(` ${store.name} ${DIM}(${store.role}, ${store.sync})${RESET} path=${existsLabel}${syncDetail}`);
}
// A store attached here whose folder is gone is not cosmetic: any
// project it claims cannot be written until the folder is back.
const { describeUnavailableStore } = await import("./store-registry.js");
for (const store of stores.filter((s) => s.available === false)) {
console.log(` ${YELLOW}! ${describeUnavailableStore(store)}${RESET}`);
if (store.projects?.length) {
console.log(` ${YELLOW}writes to ${store.projects.join(", ")} will fail until it is attached${RESET}`);
}
}
}
}
catch (err) {
if ((process.env.PHREN_DEBUG))
logger.debug("status", `statusStores: ${errorMessage(err)}`);
}
// Stats
const projectDirs = getProjectDirs(phrenPath, profile);
let totalFindings = 0;
let totalTask = 0;
let totalQueue = 0;
for (const dir of projectDirs) {
const projName = path.basename(dir);
totalFindings += countBullets(path.join(phrenPath, projName, FINDINGS_FILENAME));
const taskPath = moduleEnabled(phrenPath, "tasks", profile) ? resolveTaskFilePath(phrenPath, projName) : null;
if (taskPath)
totalTask += countBullets(taskPath);
totalQueue += countQueueItems(phrenPath, projName);
}
console.log(`\n ${DIM}phren holds${RESET} ${projectDirs.length} projects, ${totalFindings} findings${moduleEnabled(phrenPath, "tasks", profile) ? `, ${totalTask} tasks` : ""}, ${totalQueue} queued`);
const gitTarget = manifest?.installMode === "project-local" && manifest.workspaceRoot ? manifest.workspaceRoot : phrenPath;
const isGitRepo = runGit(gitTarget, ["rev-parse", "--is-inside-work-tree"], EXEC_TIMEOUT_QUICK_MS, debugLog) === "true";
const hasOriginRemote = isGitRepo && Boolean(runGit(gitTarget, ["remote", "get-url", "origin"], EXEC_TIMEOUT_QUICK_MS, debugLog));
const runtime = readRuntimeHealth(phrenPath);
if (manifest?.installMode === "project-local") {
console.log(`\n ${DIM}sync${RESET} workspace-managed`);
console.log(` auto-save ${runtime.lastAutoSave?.status || "n/a"}`);
}
else if (isGitRepo && !hasOriginRemote) {
console.log(`\n ${DIM}sync${RESET} local-only ${DIM}(no git remote)${RESET}`);
console.log(` auto-save ${runtime.lastAutoSave?.status || "n/a"}`);
console.log(` local commits ${runtime.lastSync?.unsyncedCommits ?? 0}`);
}
else {
console.log(`\n ${DIM}sync${RESET} auto-save ${runtime.lastAutoSave?.status || "n/a"}`);
if (runtime.lastAutoSave?.status === "sync-failed" || runtime.lastAutoSave?.status === "error") {
console.log(` ${RED}${describeAutoSave(runtime.lastAutoSave, runtime.lastSync)}${RESET}`);
}
console.log(` last pull ${runtime.lastSync?.lastPullStatus || "n/a"}${runtime.lastSync?.lastPullAt ? ` @ ${runtime.lastSync.lastPullAt}` : ""}`);
console.log(` last push ${runtime.lastSync?.lastPushStatus || "n/a"}${runtime.lastSync?.lastPushAt ? ` @ ${runtime.lastSync.lastPushAt}` : ""}`);
console.log(` unsynced commits ${runtime.lastSync?.unsyncedCommits ?? 0}`);
if (runtime.lastSync?.lastSuccessfulPushAt) {
console.log(` last successful push ${runtime.lastSync.lastSuccessfulPushAt}`);
}
if (runtime.lastSync?.lastPushDetail && !activeStoreAuthFailure(phrenPath)) {
console.log(` push detail ${runtime.lastSync.lastPushDetail}`);
}
const outage = assessSyncOutage(runtime.lastSync);
if (outage.degraded) {
console.log(` ${RED}! ${outage.summary}${RESET}`);
}
}
// Recent changes (git log)
if (isGitRepo) {
const log = runGit(gitTarget, ["log", "--oneline", "-5", "--no-decorate"], EXEC_TIMEOUT_QUICK_MS, debugLog);
if (log) {
console.log(`\n ${DIM}recent${RESET}`);
for (const line of log.split("\n")) {
console.log(` ${DIM}${line}${RESET}`);
}
}
const dirty = runGit(gitTarget, ["status", "--porcelain"], EXEC_TIMEOUT_QUICK_MS, debugLog);
if (dirty) {
const count = dirty.split("\n").filter(Boolean).length;
console.log(` ${YELLOW}${count} uncommitted change(s)${RESET}`);
}
}
else {
console.log(`\n ${DIM}${gitTarget} is not a git repo${RESET}`);
}
// Health: the same data Phren Hook serves at /v1/health/details.
if (moduleEnabled(phrenPath, "hook", profile)) {
try {
const { hookRequest } = await import("./bridge/client.js");
const { healthDetails, formatHealth } = await import("./bridge/health.js");
const hook = await hookRequest("/v1/health", undefined, undefined, 2_000).catch(() => undefined);
const health = await healthDetails({ store: phrenPath, hookVersion: typeof hook?.version === "string" ? hook.version : undefined });
console.log(`\n ${BOLD}Health${RESET} ${DIM}(${health.computer.name})${RESET}`);
for (const line of formatHealth(health, { dim: DIM, reset: RESET, red: RED, yellow: YELLOW, green: GREEN }))
console.log(line);
}
catch (err) {
logger.debug("status", `health: ${errorMessage(err)}`);
}
}
// Telemetry
const telemetry = getTelemetrySummary(phrenPath);
const firstLine = telemetry.split("\n")[0];
console.log(`\n ${BOLD}${firstLine}${RESET}`);
console.log("");
}