141 lines · 4.3 KB · TypeScript
RawDownload
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 PROMPT_LEARN from "./template/learn.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(),      mcp: z.boolean().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<z.infer<typeof Info>, "template"> & { template: Promise<string> | 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",    LEARN: "learn",  } as const   const state = Instance.state(async () => {    const cfg = await Config.get()     const result: Record<string, Info> = {      [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),      },      [Default.LEARN]: {        name: Default.LEARN,        description: "distill conversation into a reusable learned skill",        get template() {          return PROMPT_LEARN        },        hints: hints(PROMPT_LEARN),      },    }     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,        mcp: true,        description: prompt.description,        get template() {          // since a getter can't be async we need to manually return a promise here          return new Promise<string>(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))  }}