@phren/cli
Version:
Knowledge layer for AI agents - CLI, MCP server, and data layer
962 lines (957 loc) • 46 kB
JavaScript
import { commandOwner, disabledHint } from "./modules/registry.js";
/**
* Command registry - single source of truth for help generation and dispatch.
*
* Order in REGISTRY is load-bearing: it drives cheat-sheet ordering and
* within-topic ordering. Don't sort.
*/
// Native handlers load lazily via `native()` below - statically importing
// cli-handlers.js would pull init/init.js (and its transitive deps) into
// every phren invocation, defeating cold-start.
// ── Types ────────────────────────────────────────────────────────────────────
export const TOPIC_ORDER = [
"core",
"projects",
"skills",
"hooks",
"config",
"maintain",
"setup",
"stores",
"team",
];
function native(fn) {
return async (args) => {
const mod = await import("./cli-handlers.js");
return mod[fn](args);
};
}
// ── Doc topics (not commands) ────────────────────────────────────────────────
export const ENV_HELP = `Environment variables:
PHREN_PATH Override phren directory (default: ~/.phren)
PHREN_PROFILE Active profile name
PHREN_DEBUG Enable debug logging (set to 1)
PHREN_PULL_INTERVAL_SECONDS Remote check interval in seconds (default: off; 0 disables)
Embeddings:
PHREN_OLLAMA_URL Ollama base URL (default: http://localhost:11434, 'off' to disable)
PHREN_EMBEDDING_API_URL OpenAI-compatible /embeddings endpoint
PHREN_EMBEDDING_API_KEY API key for embedding endpoint
PHREN_EMBEDDING_MODEL Embedding model (default: nomic-embed-text)
Context injection:
PHREN_CONTEXT_TOKEN_BUDGET Max tokens injected per prompt (default: 550)
PHREN_MAX_INJECT_TOKENS Hard cap on total injected tokens (default: 2000)
PHREN_HOOK_TIMEOUT_MS Hook subprocess timeout in ms (default: 14000)
Feature flags:
PHREN_FEATURE_AGENTS=1 Show running coding agents on the graph
PHREN_FEATURE_TOOL_HOOK=0 Skip the PostToolUse subprocess (perf)
PHREN_FEATURE_AUTO_EXTRACT=0 Disable auto memory extraction
PHREN_FEATURE_AUTO_CAPTURE=1 Extract insights from conversations
PHREN_FEATURE_SEMANTIC_DEDUP=1 LLM-based dedup on add_finding
PHREN_FEATURE_HYBRID_SEARCH=0 Disable TF-IDF cosine fallback
Run 'phren help all' to see everything.
`;
export const DOC_TOPICS = {
env: ENV_HELP,
};
// ── Subcommand catalogues ────────────────────────────────────────────────────
// Lifted from the old HELP_TOPICS string blocks. Verbatim where preserved.
const PROJECTS_SUBCOMMANDS = [
{ name: "list", usage: "phren projects list", summary: "List all tracked projects" },
{ name: "configure", usage: "phren projects configure <name> [--ownership <mode>] [--hooks on|off]", summary: "Update per-project settings" },
{ name: "remove", usage: "phren projects remove <name>", summary: "Remove a project" },
];
const SCHEDULE_SUBCOMMANDS = [
{ name: "list", usage: "phren schedule list [project]", summary: "List scheduled prompts" },
{ name: "add", usage: "phren schedule add <project> --name <name> --harness <name> --computer <name> [--model <model>] [--account <id>] [--projects <a,b>] [--session new|reuse] --every <kind> [timing] (--prompt <text>|--prompt-file <path>)", summary: "Add a scheduled prompt" },
{ name: "remove", usage: "phren schedule remove <project> <id>", summary: "Remove a scheduled prompt" },
{ name: "enable", usage: "phren schedule enable <project> <id>", summary: "Resume a scheduled prompt" },
{ name: "disable", usage: "phren schedule disable <project> <id>", summary: "Pause a scheduled prompt" },
{ name: "run", usage: "phren schedule run <project> <id>", summary: "Run a schedule through the local Hook" },
{ name: "history", usage: "phren schedule history [project] [--id <id>] [--limit <n>]", summary: "Show local schedule runs" },
];
const SKILLS_SUBCOMMANDS = [
{ name: "list", usage: "phren skills list", summary: "List installed skills" },
{ name: "add", usage: "phren skills add <project> <path>", summary: "Link a skill into a project" },
{ name: "show", usage: "phren skills show <name>", summary: "Show skill content" },
{ name: "resolve", usage: "phren skills resolve <project|global>", summary: "Print resolved skill manifest" },
{ name: "doctor", usage: "phren skills doctor <project|global>", summary: "Diagnose skill visibility" },
{ name: "sync", usage: "phren skills sync <project|global>", summary: "Regenerate skill mirror" },
{ name: "enable", usage: "phren skills enable <project|global> <name>", summary: "Enable a disabled skill" },
{ name: "disable", usage: "phren skills disable <project|global> <name>", summary: "Disable a skill without deleting" },
{ name: "remove", usage: "phren skills remove <project> <name>", summary: "Remove a skill" },
];
const HOOKS_SUBCOMMANDS = [
{ name: "list", usage: "phren hooks list [--project <name>]", summary: "Show hook status per tool" },
{ name: "enable", usage: "phren hooks enable <tool>", summary: "Enable hooks for a tool" },
{ name: "disable", usage: "phren hooks disable <tool>", summary: "Disable hooks for a tool" },
{ name: "add-custom", usage: "phren hooks add-custom <event> <cmd>", summary: "Add a custom hook" },
{ name: "remove-custom", usage: "phren hooks remove-custom <event>", summary: "Remove custom hooks" },
{ name: "errors", usage: "phren hooks errors [--limit <n>]", summary: "Show recent hook errors" },
];
const CONFIG_SUBCOMMANDS = [
{ name: "show", usage: "phren config show [--project <name>]", summary: "Show current config" },
{ name: "policy", usage: "phren config policy [get|set ...]", summary: "Retention, TTL, confidence, decay" },
{ name: "workflow", usage: "phren config workflow [get|set ...]", summary: "Risky-memory thresholds" },
{ name: "proactivity", usage: "phren config proactivity [level] [--scope base|findings|tasks]", summary: "Auto-capture level; --scope limits it to findings or tasks" },
{ name: "proactivity.findings", usage: "phren config proactivity.findings [level]", summary: "Findings-only auto-capture level" },
{ name: "proactivity.tasks", usage: "phren config proactivity.tasks [level]", summary: "Task-only auto-capture level" },
{ name: "task-mode", usage: "phren config task-mode [mode]", summary: "Set task automation mode" },
{ name: "finding-sensitivity", usage: "phren config finding-sensitivity [lvl]", summary: "Set finding capture sensitivity" },
{ name: "index", usage: "phren config index [get|set ...]", summary: "Indexer include/exclude globs" },
{ name: "synonyms", usage: "phren config synonyms [list|add|remove]", summary: "Manage learned synonyms" },
{ name: "project-ownership", usage: "phren config project-ownership [mode]", summary: "Default ownership for new projects" },
{ name: "machines", usage: "phren config machines", summary: "Registered machines" },
{ name: "profiles", usage: "phren config profiles", summary: "Profiles and projects" },
{ name: "telemetry", usage: "phren config telemetry [on|off]", summary: "Opt-in usage telemetry" },
{ name: "mcp-profile", usage: "phren config mcp-profile [core|full]", summary: "MCP tool surface: 10 core tools or all of them" },
{ name: "clanker", usage: "phren config clanker [on|off]", summary: "Compact id/title/keyword rows from the hook and search/list tools; full text by id" },
{ name: "pull-interval", usage: "phren config pull-interval [seconds|off]", summary: "Periodic MCP remote checks (default: off; 0 disables)" },
];
const MAINTAIN_SUBCOMMANDS = [
{ name: "govern", usage: "phren maintain govern [project]", summary: "Queue stale memories for review" },
{ name: "prune", usage: "phren maintain prune [project]", summary: "Delete expired entries" },
{ name: "consolidate", usage: "phren maintain consolidate [project]", summary: "Deduplicate findings" },
{ name: "summarize", usage: "phren maintain summarize [project] [--llm] [--force]", summary: "Write what each topic archive amounts to, and what phren knows per project" },
{ name: "extract", usage: "phren maintain extract [project]", summary: "Mine git/GitHub signals" },
];
const STORE_SUBCOMMANDS = [
{ name: "list", usage: "phren store list", summary: "List registered stores" },
{ name: "identity", usage: "phren store identity [--create]", summary: "Read or explicitly register this store's portable identity" },
{ name: "add", usage: "phren store add <name> --remote <url>", summary: "Add a team store" },
{ name: "remove", usage: "phren store remove <name>", summary: "Remove a store (local only)" },
{ name: "sync", usage: "phren store sync", summary: "Pull and push all stores" },
];
const TEAM_SUBCOMMANDS = [
{ name: "init", usage: "phren team init <name> [--remote <url>]", summary: "Create a new team store" },
{ name: "join", usage: "phren team join <git-url> [--name <name>]", summary: "Join an existing team store" },
{ name: "add-project", usage: "phren team add-project <store> <project>", summary: "Add a project to a team store" },
{ name: "list", usage: "phren team list", summary: "List team stores" },
];
// ── Registry ─────────────────────────────────────────────────────────────────
export const REGISTRY = [
{
name: "pair", topic: "setup", usage: "phren pair [--minutes <1-30>] [--port <n>] [--no-install] | phren pair --scope gitboy-read|gitboy-write --key <public-key-file|->",
summary: "Connect your phone with a pairing QR code, or authorize a scoped key for gitboy (gitboy-read: project memory; gitboy-write: save findings)",
run: async (args) => {
const { runPair } = await import("./bridge/pair.js");
const { VERSION } = await import("./package-metadata.js");
return runPair(args, VERSION);
},
},
{
name: "bridge", topic: "setup", usage: "phren bridge <install|status|doctor|update|rollback|uninstall|enroll-computer|accounts|discover|link|fanouts archive|git-host|speech-key set|speech-voice|speech-model|speech-region>",
summary: "Install Phren Hook and enroll phone or computer connections",
subcommands: [
{ name: "enroll-computer", usage: "phren bridge enroll-computer <name> [--accept <public-key-file>]", summary: "Print or accept a restricted computer dispatch key" },
{ name: "accounts", usage: "phren bridge accounts [add <slug> [--label <name>] | label <id> <label> | credit <id> [--amount <usd> [--expires <date>] [--label <name>] | --remove <credit>]]", summary: "List this computer's harnesses and Claude accounts, add a Claude account home, label one, or enter an API credit for a Console login" },
{ name: "discover", usage: "phren bridge discover", summary: "List computers you already reach over ssh that run Phren Hook and are not linked" },
{ name: "link", usage: "phren bridge link <ssh-host> [--name <name>] [--as <name>] [--back-address <address>] [--yes]", summary: "Link this computer and an ssh host both ways in one step, after confirming" },
{ name: "fanouts archive", usage: "phren bridge fanouts archive [--dry-run] [--parent <session-id>] [--older-than <minutes>]", summary: "Archive finished fan-out jobs older than 24 hours, or one parent chat's, or older than N minutes" },
{ name: "git-host", usage: "phren bridge git-host [list | set <domain> gitlab|gitboy | remove <domain>]", summary: "Store, list or forget the GitLab or gitboy token the Hook uses for merge requests and pipelines (token from stdin, mode 600)" },
{ name: "speech-key set", usage: "phren bridge speech-key set", summary: "Store this computer's ElevenLabs key for spoken replies and dictation (read from stdin, mode 600)" },
{ name: "speech-voice", usage: "phren bridge speech-voice [show | set <voice-id> | clear]", summary: "Show or set the ElevenLabs voice talk mode speaks with; kept across install and update" },
{ name: "speech-model", usage: "phren bridge speech-model [show | set <model-id> | clear]", summary: "Show or set the ElevenLabs model talk mode speaks with (default eleven_v4_turbo); kept across install and update" },
{ name: "speech-region", usage: "phren bridge speech-region [show | us | global]", summary: "Show or set where talk mode reaches ElevenLabs: global (default) or the US-only endpoint; kept across install and update" },
],
run: async (args) => {
const { runBridge } = await import("./bridge/command.js");
const { VERSION } = await import("./package-metadata.js");
return runBridge(args, VERSION);
},
},
{
name: "dispatch", topic: "core", usage: "phren dispatch <computer|anywhere> <project> --label <label> --prompt <brief> [--harness codex|claude|opencode|copilot] [--model <model>] [--effort <effort>] [--account <id>] [--permission-mode <mode>] [--keep-open]",
summary: "Dispatch a worker brief through Phren Hook",
subcommands: [{ name: "status", usage: "phren dispatch status", summary: "List local dispatch receipts" },
{ name: "returns", usage: "phren dispatch returns", summary: "List unread worker returns (done, needs you, blocked, gone) and mark them read" },
{ name: "sessions", usage: "phren dispatch sessions", summary: "List live agent sessions on this and every enrolled computer" },
{ name: "usage", usage: "phren dispatch usage [--json]", summary: "Show agent usage on this and every enrolled computer, merged by account" },
{ name: "report", usage: "phren dispatch report --prs <JSON>", summary: "Record this worker turn's PR evidence for the integrator" },
{ name: "close-self", usage: "phren dispatch close-self", summary: "Close the pane this agent runs in once its work is committed and reported (the sanctioned form of herdr pane close)" }],
run: async (args) => (await import("./bridge/dispatch-command.js")).runDispatch(args),
},
{
name: "sudo", topic: "core", standalone: true, usage: "phren sudo <command...>",
summary: "Run sudo -A with Phren Hook's askpass: your phone shows the command and asks for the password, for `!` commands and agents with no terminal",
run: async (args) => (await import("./bridge/sudo.js")).runSudo(args),
},
{
name: "computers", topic: "core", standalone: true,
usage: "phren computers [<name>] [--resources] [--pick [mac|linux|any]] [--exclude a,b] [--local] [--json] | phren computers mcp",
summary: "Each computer's load, memory, disk, battery and heavy jobs from Phren Hook, without Phren memory; `mcp` serves them read-only to agents",
subcommands: [{ name: "mcp", usage: "phren computers mcp", summary: "Stdio MCP server: list_computers, get_resources, pick_computer, get_usage (read-only, no memory)" }],
run: async (args) => (await import("./computers/command.js")).runComputers(args),
},
{
name: "usage", topic: "core", standalone: true,
usage: "phren usage [--per-computer] [--computer <name>] [--local] [--json]",
summary: "Agent usage per harness (limits, resets, spend) across computers from Phren Hook, without Phren memory",
run: async (args) => (await import("./computers/command.js")).runUsage(args),
},
{
name: "owner-inbox", topic: "core", usage: "phren owner-inbox [list [--all] | add <title> [--project <slug>] [--id <uuid>] | resolve <id> [--computer <name>] [--resolution <text>]]",
summary: "List, add and resolve what waits on the owner",
run: async (args) => (await import("./bridge/dispatch-command.js")).runOwnerInbox(args),
},
{
name: "hand-off", topic: "core", usage: "phren hand-off <computer|local> --session <id> --text <prompt> [--account <id>]",
summary: "Send work to an existing agent session through Phren Hook",
run: async (args) => (await import("./bridge/dispatch-command.js")).runHandOff(args),
},
{
name: "move", topic: "core", usage: "phren move <session-or-pane> --to <claude|codex|opencode|copilot> [--model <model>] [--effort <effort>] [--account <id>] [--computer <name>] [--no-wait]",
summary: "Move a live agent session to another harness: it writes a hand-off and exits, and the new agent continues in its pane",
run: async (args) => (await import("./bridge/dispatch-command.js")).runMove(args),
},
{
name: "canary", topic: "core", usage: "phren canary [--daily on|off]",
summary: "Exercise launch, schedules, transcripts and peers through Phren Hook; saves canary.json",
run: async (args) => (await import("./bridge/canary.js")).runCanaryCommand(args),
},
{
name: "conductor", topic: "core", usage: "phren conductor <subcommand>",
summary: "Make or stop this computer's conductor, list linked computer sets, and manage standing grants",
subcommands: [
{ name: "integrator", usage: "phren conductor integrator [--session <id> [--computer <name>] | --clear]", summary: "Configure the session that receives structured PR-ready reports" },
{ name: "status", usage: "phren conductor status", summary: "This computer's conductor and the set name it holds" },
{ name: "make", usage: "phren conductor make [--pane <id>] [--mux herdr:<name>|tmux:<name>]", summary: "Make the agent in this pane (or --pane) this computer's conductor" },
{ name: "stop", usage: "phren conductor stop [--pane <id>]", summary: "End this computer's conductor role; the agent keeps running" },
{ name: "sets", usage: "phren conductor sets [--json] | phren conductor sets name <name>|--clear", summary: "List linked computer sets with reachability and conductors, or name this computer's set" },
{ name: "grants", usage: "phren conductor grants [list|add|remove]", summary: "List, add, or remove conductor grants" },
],
run: async (args) => (await import("./bridge/dispatch-command.js")).runConductor(args),
},
{
name: "authority", topic: "core", usage: "phren authority [list | show <project> | set <project> ... | clear <project> | confirm <project> <actions>]",
summary: "Read the owner's release authority policy; the owner changes it or confirms an ask-first release",
subcommands: [
{ name: "list", usage: "phren authority list", summary: "Every project the policy names, and confirmations waiting to be used" },
{ name: "show", usage: "phren authority show <project>", summary: "One project's go and ask-first actions, ceiling, and the line to quote in a brief" },
{ name: "set", usage: "phren authority set <project> [--default go|ask] [--go <actions>] [--ask <actions>] [--max-permission-mode <mode>|none] [--note <text>]", summary: "Change a project's entry (owner, interactive terminal)" },
{ name: "clear", usage: "phren authority clear <project>", summary: "Remove a project's entry (owner, interactive terminal)" },
{ name: "confirm", usage: "phren authority confirm <project> <actions> [--minutes <n>]", summary: "Let one agent dispatch do ask-first release actions (owner, interactive terminal)" },
],
run: async (args) => (await import("./bridge/authority-command.js")).runAuthority(args),
},
// Setup (featured: init, quickstart)
{
name: "init",
topic: "setup",
usage: "phren init [--advanced] [--preset managed|assisted|manual] [--mode shared|project-local] [--machine <n>] [--profile <n>] [--dry-run] [-y]",
cheatUsage: "phren init",
summary: "Set up phren",
featured: true,
run: native("runInitCommand"),
},
{
name: "quickstart",
topic: "setup",
usage: "phren quickstart",
summary: "Quick setup: init + project scaffold",
featured: true,
run: async () => {
const { handleQuickstart } = await import("./cli/ops.js");
await handleQuickstart();
},
},
// Projects (featured: add)
{
name: "add",
topic: "projects",
usage: "phren add [path] [--ownership <mode>]",
summary: "Register a project",
featured: true,
run: native("runAddCommand"),
},
{
name: "projects",
topic: "projects",
usage: "phren projects <subcommand>",
summary: "Manage tracked projects",
subcommands: PROJECTS_SUBCOMMANDS,
run: async (args, ctx) => {
const { handleProjectsNamespace } = await import("./cli/namespaces.js");
await handleProjectsNamespace(args, ctx.profile());
},
},
{
name: "schedule",
topic: "projects",
usage: "phren schedule <subcommand>",
summary: "Manage scheduled prompts",
subcommands: SCHEDULE_SUBCOMMANDS,
run: async (args, ctx) => {
const { handleScheduleCommand } = await import("./cli/schedules.js");
return handleScheduleCommand(args, ctx.phrenPath());
},
},
// Core (featured: search, status, doctor, web-ui, tasks, graph, shell)
{
name: "search",
topic: "core",
usage: "phren search <query>",
summary: "Search what phren knows",
featured: true,
run: async (args, ctx) => {
const { parseSearchArgs } = await import("./cli/search.js");
const { handleSearch } = await import("./cli/actions.js");
const opts = parseSearchArgs(ctx.phrenPath(), args);
if (!opts)
return;
await handleSearch(opts, ctx.profile());
},
},
{
name: "status",
topic: "core",
usage: "phren status",
summary: "Health check",
featured: true,
run: native("runStatusCommand"),
},
{
name: "doctor",
topic: "core",
usage: "phren doctor [--fix]",
summary: "Diagnose and repair",
featured: true,
run: async (args) => {
const { handleDoctor } = await import("./cli/actions.js");
await handleDoctor(args);
},
},
{
name: "web-ui",
topic: "core",
usage: "phren web-ui [--port <n>]",
summary: "Open the knowledge graph",
featured: true,
run: async (args) => {
const { handleMemoryUi } = await import("./cli/actions.js");
await handleMemoryUi(args);
},
},
{
name: "tasks",
topic: "core",
usage: "phren tasks [profile] [--responsibility=human|agent] [--readiness=ready|waiting-on-human|waiting-on-task]",
summary: "Cross-project task view",
featured: true,
run: async (args, ctx) => {
const { handleTaskListArgs } = await import("./cli/namespaces-tasks.js");
await handleTaskListArgs(args, ctx.profile());
},
},
{
name: "graph",
topic: "core",
usage: "phren graph",
summary: "Fragment knowledge graph",
featured: true,
run: async (args) => {
const { handleGraphNamespace } = await import("./cli/graph.js");
await handleGraphNamespace(args);
},
},
{
name: "code",
topic: "core",
usage: "phren code <index|status|search|outline|refs|def|usage> [project]",
summary: "Index a project's code and look up its functions, types and where they are used",
subcommands: [
{ name: "index", usage: "phren code index <project> [--full] [--repo <path>]", summary: "Build or refresh the code index" },
{ name: "status", usage: "phren code status <project> [--top <n>]", summary: "Show index counts, languages and the most used functions and types" },
{ name: "search", usage: "phren code search <project> <query> [--kind k] [--limit n]", summary: "Find functions, types and variables by name" },
{ name: "outline", usage: "phren code outline <project> <path>", summary: "A file's functions, types and variables in source order, nested" },
{ name: "refs", usage: "phren code refs <project> <name>", summary: "Where a function, type or variable is used, grouped by file" },
{ name: "def", usage: "phren code def <project> <name>", summary: "Where it is defined: doc, snippet and last change" },
{ name: "usage", usage: "phren code usage <project> [--top n]", summary: "Most and least used functions and types" },
],
run: async (args, ctx) => (await (await import("./modules/code-package.js")).requireCodePackage(ctx.phrenPath())).runCodeCommand(args, ctx),
},
{
name: "agent",
topic: "core",
usage: "phren agent [options] [task]",
summary: "Run phren's coding agent (needs @phren/agent); phren agent --help lists its options",
run: async (args) => (await import("./modules/agent-package.js")).runAgentCommand(args),
},
{
name: "shell",
topic: "core",
usage: "phren shell [--view <view>] [--project <name>] [--here]",
summary: "Interactive memory shell",
featured: true,
run: async (args, ctx) => {
const { handleShell } = await import("./cli/actions.js");
await handleShell(args, ctx.profile());
},
},
// Other core commands
{
name: "add-finding",
topic: "core",
usage: 'phren add-finding <project> "<insight>"',
summary: "Tell phren what you learned",
run: async (args) => {
const { handleAddFinding } = await import("./cli/actions.js");
await handleAddFinding(args[0], args.slice(1).join(" "));
},
},
{
name: "pin",
topic: "core",
usage: 'phren pin <project> "<truth>"',
summary: "Save a truth (always-inject, never decays)",
run: async (args) => {
const { handlePinCanonical } = await import("./cli/actions.js");
await handlePinCanonical(args[0], args.slice(1).join(" "));
},
},
{
name: "review",
topic: "core",
usage: "phren review [project]",
summary: "Show review queue",
run: async (args) => {
const { handleReviewNamespace } = await import("./cli/namespaces.js");
await handleReviewNamespace(args);
},
},
{
name: "session-context",
topic: "core",
usage: "phren session-context",
summary: "Current session state",
run: async () => {
const { handleSessionContext } = await import("./cli/actions.js");
await handleSessionContext();
},
},
{
name: "sessions",
topic: "core",
usage: "phren sessions",
summary: "List recent sessions",
run: async (args) => {
const { handleSessionsView } = await import("./cli/ops.js");
await handleSessionsView(args);
},
},
{
name: "task",
topic: "core",
usage: "phren task <subcommand>",
summary: "Manage tasks",
run: async (args) => {
const { handleTaskNamespace } = await import("./cli/namespaces.js");
await handleTaskNamespace(args);
},
},
{
name: "finding",
topic: "core",
usage: "phren finding <subcommand>",
summary: "Manage findings",
run: async (args) => {
const { handleFindingNamespace } = await import("./cli/namespaces.js");
await handleFindingNamespace(args);
},
},
{
name: "note",
aliases: ["notes"],
topic: "core",
usage: "phren note <add|list|edit|remove|promote>",
summary: "Manage lightweight daily notes",
subcommands: [
{ name: "add", usage: 'phren note add <project> "<text>" [--date YYYY-MM-DD]', summary: "Add a daily note" },
{ name: "list", usage: "phren note list <project> [--date YYYY-MM-DD]", summary: "List notes" },
{ name: "edit", usage: 'phren note edit <project> <nid> "<text>"', summary: "Edit a note" },
{ name: "remove", usage: "phren note remove <project> <nid>", summary: "Remove a note" },
{ name: "promote", usage: "phren note promote <project> <nid> [--type <type>]", summary: "Promote a note to a finding" },
],
featured: true,
run: async (args, ctx) => {
const { handleNoteNamespace } = await import("./cli/namespaces.js");
await handleNoteNamespace(args, ctx.profile());
},
},
{
name: "search-fragments",
topic: "core",
usage: "phren search-fragments <query>",
summary: "Search the named-fragment graph",
run: async (args, ctx) => {
const { handleFragmentSearch } = await import("./cli/actions.js");
await handleFragmentSearch(args, ctx.profile());
},
},
{
name: "related-docs",
topic: "core",
usage: "phren related-docs <entity>",
summary: "Find docs that share fragments with an entity",
run: async (args, ctx) => {
const { handleRelatedDocs } = await import("./cli/actions.js");
await handleRelatedDocs(args, ctx.profile());
},
},
{
name: "truths",
topic: "core",
usage: "phren truths [project]",
summary: "Show pinned truths for a project",
run: async (args) => {
const { handleTruths } = await import("./cli/actions.js");
await handleTruths(args[0]);
},
},
{
name: "promote",
topic: "core",
usage: "phren promote <finding> --to <store>",
summary: "Move a finding from personal to a team store",
run: async (args) => {
const { handlePromoteNamespace } = await import("./cli/namespaces.js");
await handlePromoteNamespace(args);
},
},
// Skills
{
name: "skills",
topic: "skills",
usage: "phren skills <subcommand>",
summary: "Manage skills",
subcommands: SKILLS_SUBCOMMANDS,
run: async (args, ctx) => {
const { handleSkillsNamespace } = await import("./cli/namespaces.js");
await handleSkillsNamespace(args, ctx.profile());
},
},
{
name: "detect-skills",
topic: "skills",
usage: "phren detect-skills [--import]",
summary: "Find untracked skills in ~/.claude/skills/",
run: async (args, ctx) => {
const { handleDetectSkills } = await import("./cli/namespaces.js");
await handleDetectSkills(args, ctx.profile());
},
},
// Hooks (namespace shares name with topic - distinct concept)
{
name: "hooks",
topic: "hooks",
usage: "phren hooks <subcommand>",
summary: "Manage lifecycle hooks",
subcommands: HOOKS_SUBCOMMANDS,
run: async (args) => {
const { handleHooksNamespace } = await import("./cli/namespaces.js");
await handleHooksNamespace(args);
},
},
// Config
{
name: "fanout",
topic: "config",
usage: "phren fanout run|usage|resume|review|list|archive",
summary: "Launch and manage usage-aware agent workers",
run: async (args, ctx) => (await import("./fanout/command.js")).runFanout(args, ctx),
},
{
name: "modules",
topic: "config",
usage: "phren modules <list|enable|disable> [name] [--profile <name>]",
summary: "List or configure enabled modules",
subcommands: [
{ name: "list", usage: "phren modules list [--profile <name>]", summary: "Show effective enablement and source" },
{ name: "enable", usage: "phren modules enable <name> [--profile <name>]", summary: "Enable a module" },
{ name: "disable", usage: "phren modules disable <name> [--profile <name>]", summary: "Disable a module, preserving its data" },
],
run: async (args, ctx) => (await import("./modules/command.js")).runModules(args, ctx),
},
{
name: "config",
topic: "config",
usage: "phren config <subcommand>",
summary: "View and update configuration",
subcommands: CONFIG_SUBCOMMANDS,
run: async (args) => {
const { handleConfig } = await import("./cli/config.js");
await handleConfig(args);
},
},
// Maintain
{
name: "maintain",
topic: "maintain",
usage: "phren maintain <subcommand>",
summary: "Memory maintenance operations",
subcommands: MAINTAIN_SUBCOMMANDS,
run: async (args) => {
const { handleMaintain } = await import("./cli/govern.js");
await handleMaintain(args);
},
},
{
name: "consolidation-status",
topic: "maintain",
usage: "phren consolidation-status",
summary: "Report findings consolidation health",
run: async (args) => {
const { handleConsolidationStatus } = await import("./cli/actions.js");
await handleConsolidationStatus(args);
},
},
{
name: "quality-feedback",
topic: "maintain",
usage: "phren quality-feedback",
summary: "Inspect injected-memory feedback",
run: async (args) => {
const { handleQualityFeedback } = await import("./cli/actions.js");
await handleQualityFeedback(args);
},
},
// Setup (the rest)
{
name: "mcp-mode",
topic: "setup",
usage: "phren mcp-mode [on|off|status]",
summary: "Toggle MCP integration",
run: native("runMcpModeCommand"),
},
{
name: "hooks-mode",
topic: "setup",
usage: "phren hooks-mode [on|off|status]",
summary: "Toggle hook execution",
run: native("runHooksModeCommand"),
},
{
name: "preset",
topic: "setup",
usage: "phren preset [status|managed|assisted|manual] [-y]",
summary: "Set the management preset (how much phren wires up)",
run: native("runPresetCommand"),
},
{
name: "snippet",
topic: "setup",
usage: "phren snippet",
summary: "Print the self-wiring snippet for assisted/manual presets",
run: native("runSnippetCommand"),
},
{
name: "verify",
topic: "setup",
usage: "phren verify",
summary: "Check init completed OK",
run: native("runVerifyCommand"),
},
{
name: "uninstall",
topic: "setup",
usage: "phren uninstall",
summary: "Remove phren config and hooks",
run: native("runUninstallCommand"),
},
{
name: "update",
topic: "setup",
usage: "phren update [--refresh-starter]",
summary: "Update to latest version",
run: async (args) => {
const { handleUpdate } = await import("./cli/actions.js");
await handleUpdate(args);
},
},
{
name: "profile",
topic: "setup",
usage: "phren profile <subcommand>",
summary: "Manage machine-to-profile mappings",
run: async (args) => {
const { handleProfileNamespace } = await import("./cli/namespaces.js");
await handleProfileNamespace(args);
},
},
// Stores
{
name: "store",
topic: "stores",
usage: "phren store <subcommand>",
summary: "Manage knowledge stores",
subcommands: STORE_SUBCOMMANDS,
run: async (args) => {
const { handleStoreNamespace } = await import("./cli/namespaces.js");
await handleStoreNamespace(args);
},
},
// Team
{
name: "team",
topic: "team",
usage: "phren team <subcommand>",
summary: "Manage team stores",
subcommands: TEAM_SUBCOMMANDS,
run: async (args) => {
const { handleTeamNamespace } = await import("./cli/team.js");
await handleTeamNamespace(args);
},
},
// Hidden - internal hooks (called by Claude/Cursor/etc., not by humans)
{
name: "hook-prompt",
topic: "core",
usage: "phren hook-prompt",
summary: "Internal: UserPromptSubmit hook",
hidden: true,
run: async () => {
const { handleHookPrompt } = await import("./cli/hooks.js");
await handleHookPrompt();
},
},
{
name: "hook-session-start",
topic: "core",
usage: "phren hook-session-start",
summary: "Internal: SessionStart hook",
hidden: true,
run: async () => {
const { handleHookSessionStart } = await import("./cli/hooks.js");
await handleHookSessionStart();
},
},
{
name: "hook-stop",
topic: "core",
usage: "phren hook-stop",
summary: "Internal: Stop hook",
hidden: true,
run: async () => {
const { handleHookStop } = await import("./cli/hooks.js");
await handleHookStop();
},
},
{
name: "hook-context",
topic: "core",
usage: "phren hook-context",
summary: "Internal: context-injection hook",
hidden: true,
run: async () => {
const { handleHookContext } = await import("./cli/hooks.js");
await handleHookContext();
},
},
{
name: "hook-tool",
topic: "core",
usage: "phren hook-tool",
summary: "Internal: PostToolUse hook",
hidden: true,
run: async () => {
const { handleHookTool } = await import("./cli/hooks.js");
await handleHookTool();
},
},
{
name: "background-sync",
topic: "core",
usage: "phren background-sync",
summary: "Internal: background git sync",
hidden: true,
run: async () => {
const { handleBackgroundSync } = await import("./cli/hooks.js");
await handleBackgroundSync();
},
},
{
name: "background-maintenance",
topic: "core",
usage: "phren background-maintenance",
summary: "Internal: scheduled maintenance",
hidden: true,
run: async (args) => {
const { handleBackgroundMaintenance } = await import("./cli/govern.js");
await handleBackgroundMaintenance(args[0]);
},
},
{
name: "background-reindex",
topic: "core",
usage: "phren background-reindex",
summary: "Internal: rebuild the FTS index off the hook path",
hidden: true,
run: async (_args, ctx) => {
const { buildIndex } = await import("./shared/index.js");
await buildIndex(ctx.phrenPath(), ctx.profile());
},
},
{
name: "debug-injection",
topic: "core",
usage: "phren debug-injection",
summary: "Internal: dump injection rationale",
hidden: true,
run: async (args, ctx) => {
const { handleDebugInjection } = await import("./cli/ops.js");
await handleDebugInjection(args, ctx.profile());
},
},
{
name: "inspect-index",
topic: "core",
usage: "phren inspect-index",
summary: "Internal: dump index contents",
hidden: true,
run: async (args, ctx) => {
const { handleInspectIndex } = await import("./cli/ops.js");
await handleInspectIndex(args, ctx.profile());
},
},
// Hidden - undocumented aliases of namespaced commands (kept for back-compat;
// user-facing form lives under config / maintain / skills).
{
name: "skill-list",
topic: "skills",
usage: "phren skill-list",
summary: "Alias of `phren skills list`",
hidden: true,
run: async (_args, ctx) => {
const { handleSkillList } = await import("./cli/namespaces.js");
await handleSkillList(ctx.profile());
},
},
{
name: "policy",
topic: "config",
usage: "phren policy [get|set ...]",
summary: "Alias of `phren config policy`",
hidden: true,
run: async (args) => {
const { handleRetentionPolicy } = await import("./cli/config.js");
await handleRetentionPolicy(args);
},
},
{
name: "workflow",
topic: "config",
usage: "phren workflow [get|set ...]",
summary: "Alias of `phren config workflow`",
hidden: true,
run: async (args) => {
const { handleWorkflowPolicy } = await import("./cli/config.js");
await handleWorkflowPolicy(args);
},
},
{
name: "index-policy",
topic: "config",
usage: "phren index-policy [...]",
summary: "Alias of `phren config index`",
hidden: true,
run: async (args) => {
const { handleIndexPolicy } = await import("./cli/config.js");
await handleIndexPolicy(args);
},
},
{
name: "extract-memories",
topic: "maintain",
usage: "phren extract-memories [project]",
summary: "Alias of `phren maintain extract`",
hidden: true,
run: async (args) => {
const { handleExtractMemories } = await import("./cli/extract.js");
await handleExtractMemories(args[0]);
},
},
{
name: "govern-memories",
topic: "maintain",
usage: "phren govern-memories [project]",
summary: "Alias of `phren maintain govern`",
hidden: true,
run: async (args) => {
const { handleGovernMemories } = await import("./cli/govern.js");
await handleGovernMemories(args[0]);
},
},
{
name: "prune-memories",
topic: "maintain",
usage: "phren prune-memories [project]",
summary: "Alias of `phren maintain prune`",
hidden: true,
run: async (args) => {
const { handlePruneMemories } = await import("./cli/govern.js");
await handlePruneMemories(args);
},
},
{
name: "consolidate-memories",
topic: "maintain",
usage: "phren consolidate-memories [project]",
summary: "Alias of `phren maintain consolidate`",
hidden: true,
run: async (args) => {
const { handleConsolidateMemories } = await import("./cli/govern.js");
await handleConsolidateMemories(args);
},
},
// Hidden - `phren link` was removed; this prints a removal notice.
{
name: "link",
topic: "setup",
usage: "phren link",
summary: "Reconcile the existing installation",
hidden: true,
run: native("runLinkRemovedNotice"),
},
];
// ── Lookup ───────────────────────────────────────────────────────────────────
export function lookupCommand(name, snapshot) {
for (const cmd of commandsForModules(snapshot)) {
if (cmd.name === name)
return cmd;
if (cmd.aliases?.includes(name))
return cmd;
}
return undefined;
}
/** Topic IDs available to `phren help <topic>`, including doc topics and `all`. */
export function helpTopicNames() {
return [...TOPIC_ORDER, ...Object.keys(DOC_TOPICS), "all"];
}
/** Topic IDs that are command groups (excludes doc topics and `all`). */
export function commandTopics() {
return TOPIC_ORDER;
}
export function disabledCommand(command, snapshot) {
if (!snapshot.has("tasks") && /^config proactivity .*--scope(?:=|\s+)(?:tasks|task)(?:\s|$)/.test(command))
return disabledHint("tasks");
const owner = commandOwner(command);
return owner && !snapshot.has(owner.name) ? disabledHint(owner.name) : undefined;
}
export function commandsForModules(snapshot) {
if (!snapshot)
return REGISTRY;
return REGISTRY.filter(command => !disabledCommand(command.name, snapshot)).map(command => ({
...command,
usage: command.usage.replace(/<([^<>]*\|[^<>]*)>/g, (_match, choices) => `<${choices.split("|").filter(choice => !disabledCommand(`${command.name} ${choice}`, snapshot)).join("|")}>`),
aliases: command.aliases?.filter(alias => !disabledCommand(alias, snapshot)),
subcommands: command.subcommands?.filter(sub => !disabledCommand(`${command.name} ${sub.name}`, snapshot))
.map(sub => snapshot.has("tasks") ? sub : { ...sub, usage: sub.usage.replace("base|findings|tasks", "base|findings"), summary: sub.summary?.replace("findings or tasks", "findings") }),
run: async (args, ctx) => {
const unavailable = disabledCommand([command.name, ...args].join(" "), snapshot);
if (unavailable) {
console.error(unavailable);
return 1;
}
return command.run(args, ctx);
},
}));
}