import { Bus } from "@/bus" import { BusEvent } from "@/bus/bus-event" import z from "zod" import { Config } from "../config/config" import { Instance } from "../project/instance" import { Identifier } from "../id/id" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" import { MCP } from "../mcp" export namespace Command { export const Event = { Executed: BusEvent.define( "command.executed", z.object({ name: z.string(), sessionID: Identifier.schema("session"), arguments: z.string(), messageID: Identifier.schema("message"), }), ), } export const Info = z .object({ name: z.string(), description: z.string().optional(), agent: z.string().optional(), model: z.string().optional(), // workaround for zod not supporting async functions natively so we use getters // https://zod.dev/v4/changelog?id=zfunction template: z.promise(z.string()).or(z.string()), subtask: z.boolean().optional(), hints: z.array(z.string()), }) .meta({ ref: "Command", }) // for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it export type Info = Omit, "template"> & { template: Promise | string } export function hints(template: string): string[] { const result: string[] = [] const numbered = template.match(/\$\d+/g) if (numbered) { for (const match of [...new Set(numbered)].sort()) result.push(match) } if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS") return result } export const Default = { INIT: "init", REVIEW: "review", } as const const state = Instance.state(async () => { const cfg = await Config.get() const result: Record = { [Default.INIT]: { name: Default.INIT, description: "create/update AGENTS.md", get template() { return PROMPT_INITIALIZE.replace("${path}", Instance.worktree) }, hints: hints(PROMPT_INITIALIZE), }, [Default.REVIEW]: { name: Default.REVIEW, description: "review changes [commit|branch|pr], defaults to uncommitted", get template() { return PROMPT_REVIEW.replace("${path}", Instance.worktree) }, subtask: true, hints: hints(PROMPT_REVIEW), }, } for (const [name, command] of Object.entries(cfg.command ?? {})) { result[name] = { name, agent: command.agent, model: command.model, description: command.description, get template() { return command.template }, subtask: command.subtask, hints: hints(command.template), } } for (const [name, prompt] of Object.entries(await MCP.prompts())) { result[name] = { name, description: prompt.description, get template() { // since a getter can't be async we need to manually return a promise here return new Promise(async (resolve, reject) => { const template = await MCP.getPrompt( prompt.client, prompt.name, prompt.arguments ? // substitute each argument with $1, $2, etc. Object.fromEntries(prompt.arguments?.map((argument, i) => [argument.name, `$${i + 1}`])) : {}, ).catch(reject) resolve( template?.messages .map((message) => (message.content.type === "text" ? message.content.text : "")) .join("\n") || "", ) }) }, hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [], } } return result }) export async function get(name: string) { return state().then((x) => x[name]) } export async function list() { return state().then((x) => Object.values(x)) } }