/** * Pi-facing bootstrap and status commands for codebase-memory-mcp. * * This file is deliberately an adapter. The official CBM installer owns * binary lifecycle and this package's native bridge generation; the official CLI owns indexing * and project membership. No Pi mcp.json entry or local CBM tool registry is * written here. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { assessVersion, describeError, executeInstall, formatStatusReport, isUsableRelease, parseInstallArgs, parseIndexPhase, projectStatusFromResult, type InstallIO, type InstallRequest, type LifecycleRuntime, type ProjectStatus, } from "./cbm-lifecycle.ts"; import { createNativeRuntime } from "./cbm-native.ts"; const DISABLED = /^(1|true)$/i.test(process.env.CBM_HOOKS_DISABLE ?? ""); const DEBUG = /^(1|true)$/i.test(process.env.CBM_HOOKS_DEBUG ?? ""); function debug(message: string): void { if (DEBUG) process.stderr.write(`[cbm-bootstrap] ${message}\n`); } export function registerBootstrapCommands(pi: ExtensionAPI, runtime: LifecycleRuntime = createNativeRuntime()): void { pi.registerCommand("cbm-install", { description: "Install or update codebase-memory-mcp through the official installer and refresh its native Pi bridge. Indexing is opt-in with --index.", handler: async (args, ctx) => { const parsed = parseInstallArgs(args ?? ""); if (parsed.error) { ctx.ui.notify(`Usage: /cbm-install [--yes] [--index|--no-index] [--dry-run] [--force] [--dir ]\n${parsed.error}`, "error"); return; } const request: InstallRequest = { cwd: ctx.cwd, interactive: ctx.hasUI, yes: parsed.yes, noIndex: parsed.noIndex, force: parsed.force, index: parsed.index, dryRun: parsed.dryRun, directory: parsed.directory, signal: ctx.signal, }; const io: InstallIO = { confirm: (message) => ctx.ui.confirm("Install / update codebase-memory-mcp", message), progress: (message) => ctx.ui.setStatus?.("cbm", message), }; try { const outcome = await executeInstall(request, runtime, io); ctx.ui.setStatus?.("cbm", undefined); const level = outcome.status === "ready" || outcome.status === "dry-run" || outcome.status === "cancelled-by-user" ? "info" : "warning"; ctx.ui.notify(`codebase-memory-mcp: ${outcome.message}`, level); } catch (error) { ctx.ui.setStatus?.("cbm", undefined); debug(`install command failed: ${describeError(error)}`); ctx.ui.notify(`codebase-memory-mcp install failed: ${describeError(error)}`, "error"); } }, }); pi.registerCommand("cbm-index", { description: "Index the current project with the active official CBM binary without changing installer state.", handler: async (_args, ctx) => { try { const snapshot = await runtime.inspect(ctx.signal); if (!snapshot.active) { ctx.ui.notify("codebase-memory-mcp index unavailable: the active official binary is missing", "error"); return; } if (!isUsableRelease(snapshot.version)) { ctx.ui.notify(`codebase-memory-mcp index unavailable: release ${snapshot.version ?? "unknown"} is not supported`, "error"); return; } if (snapshot.bridge.state !== "ready") { ctx.ui.notify(`codebase-memory-mcp index unavailable: the native Pi bridge is ${snapshot.bridge.state}`, "error"); return; } ctx.ui.setStatus?.("cbm", `Indexing ${ctx.cwd}…`); const phase = parseIndexPhase(await runtime.index(snapshot.active.path, ctx.cwd, ctx.signal)); ctx.ui.setStatus?.("cbm", undefined); const reason = phase.reason ? `: ${phase.reason}` : ""; ctx.ui.notify(`codebase-memory-mcp index: ${phase.status}${reason}`, phase.status === "indexed" ? "info" : "warning"); } catch (error) { ctx.ui.setStatus?.("cbm", undefined); debug(`index command failed: ${describeError(error)}`); ctx.ui.notify(`codebase-memory-mcp index failed: ${describeError(error)}`, "error"); } }, }); pi.registerCommand("cbm-status", { description: "Show the active and managed CBM binaries, native bridge readiness, release compatibility, and project-index state.", handler: async (_args, ctx) => { try { const snapshot = await runtime.inspect(ctx.signal); let project: { status: ProjectStatus; reason?: string } = { status: "unknown", reason: "canonical project resolver was not queried" }; if (snapshot.active && isUsableRelease(snapshot.version)) { try { project = projectStatusFromResult(await runtime.projectStatus(snapshot.active.path, ctx.cwd, ctx.signal)); } catch (error) { project = { status: "error", reason: describeError(error) }; } } else if (!snapshot.active) { project = { status: "unknown", reason: "active binary is unavailable" }; } else { project = { status: "unknown", reason: `release ${snapshot.version ?? "unknown"} is not supported` }; } const report = formatStatusReport(snapshot, { cwd: ctx.cwd, projectStatus: project.status, reason: project.reason }); const releaseState = assessVersion(snapshot.version); const ready = snapshot.bridge.state === "ready" && project.status === "ready" && isUsableRelease(snapshot.version); const level = releaseState === "forward-compatible" ? "warning" : ready ? "info" : "warning"; ctx.ui.notify(`codebase-memory-mcp status:\n${report}`, level); } catch (error) { debug(`status command failed: ${describeError(error)}`); ctx.ui.notify(`codebase-memory-mcp status unavailable: ${describeError(error)}`, "error"); } }, }); } export default function (pi: ExtensionAPI): void { if (DISABLED) { debug("disabled by CBM_HOOKS_DISABLE"); return; } registerBootstrapCommands(pi); }