UNPKG

@phren/cli

Version:

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

954 lines (949 loc) • 44.7 kB
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>] --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: "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]", summary: "Connect your phone: show a pairing QR code and authorize the phone that scans it", 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|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>]", summary: "List this computer's harnesses and Claude accounts, add a Claude account home, or label one" }, { 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: "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" }], 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: "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); }, })); }