index.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import { Bus } from "@/bus"
  2. import { BusEvent } from "@/bus/bus-event"
  3. import z from "zod"
  4. import { Config } from "../config/config"
  5. import { Instance } from "../project/instance"
  6. import { Identifier } from "../id/id"
  7. import PROMPT_INITIALIZE from "./template/initialize.txt"
  8. import PROMPT_REVIEW from "./template/review.txt"
  9. import { MCP } from "../mcp"
  10. export namespace Command {
  11. export const Event = {
  12. Executed: BusEvent.define(
  13. "command.executed",
  14. z.object({
  15. name: z.string(),
  16. sessionID: Identifier.schema("session"),
  17. arguments: z.string(),
  18. messageID: Identifier.schema("message"),
  19. }),
  20. ),
  21. }
  22. export const Info = z
  23. .object({
  24. name: z.string(),
  25. description: z.string().optional(),
  26. agent: z.string().optional(),
  27. model: z.string().optional(),
  28. // workaround for zod not supporting async functions natively so we use getters
  29. // https://zod.dev/v4/changelog?id=zfunction
  30. template: z.promise(z.string()).or(z.string()),
  31. subtask: z.boolean().optional(),
  32. hints: z.array(z.string()),
  33. })
  34. .meta({
  35. ref: "Command",
  36. })
  37. // for some reason zod is inferring `string` for z.promise(z.string()).or(z.string()) so we have to manually override it
  38. export type Info = Omit<z.infer<typeof Info>, "template"> & { template: Promise<string> | string }
  39. export function hints(template: string): string[] {
  40. const result: string[] = []
  41. const numbered = template.match(/\$\d+/g)
  42. if (numbered) {
  43. for (const match of [...new Set(numbered)].sort()) result.push(match)
  44. }
  45. if (template.includes("$ARGUMENTS")) result.push("$ARGUMENTS")
  46. return result
  47. }
  48. export const Default = {
  49. INIT: "init",
  50. REVIEW: "review",
  51. } as const
  52. const state = Instance.state(async () => {
  53. const cfg = await Config.get()
  54. const result: Record<string, Info> = {
  55. [Default.INIT]: {
  56. name: Default.INIT,
  57. description: "create/update AGENTS.md",
  58. get template() {
  59. return PROMPT_INITIALIZE.replace("${path}", Instance.worktree)
  60. },
  61. hints: hints(PROMPT_INITIALIZE),
  62. },
  63. [Default.REVIEW]: {
  64. name: Default.REVIEW,
  65. description: "review changes [commit|branch|pr], defaults to uncommitted",
  66. get template() {
  67. return PROMPT_REVIEW.replace("${path}", Instance.worktree)
  68. },
  69. subtask: true,
  70. hints: hints(PROMPT_REVIEW),
  71. },
  72. }
  73. for (const [name, command] of Object.entries(cfg.command ?? {})) {
  74. result[name] = {
  75. name,
  76. agent: command.agent,
  77. model: command.model,
  78. description: command.description,
  79. get template() {
  80. return command.template
  81. },
  82. subtask: command.subtask,
  83. hints: hints(command.template),
  84. }
  85. }
  86. for (const [name, prompt] of Object.entries(await MCP.prompts())) {
  87. result[name] = {
  88. name,
  89. description: prompt.description,
  90. get template() {
  91. // since a getter can't be async we need to manually return a promise here
  92. return new Promise<string>(async (resolve, reject) => {
  93. const template = await MCP.getPrompt(
  94. prompt.client,
  95. prompt.name,
  96. prompt.arguments
  97. ? // substitute each argument with $1, $2, etc.
  98. Object.fromEntries(prompt.arguments?.map((argument, i) => [argument.name, `$${i + 1}`]))
  99. : {},
  100. ).catch(reject)
  101. resolve(
  102. template?.messages
  103. .map((message) => (message.content.type === "text" ? message.content.text : ""))
  104. .join("\n") || "",
  105. )
  106. })
  107. },
  108. hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [],
  109. }
  110. }
  111. return result
  112. })
  113. export async function get(name: string) {
  114. return state().then((x) => x[name])
  115. }
  116. export async function list() {
  117. return state().then((x) => Object.values(x))
  118. }
  119. }