index.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import { BusEvent } from "@/bus/bus-event"
  2. import z from "zod"
  3. import { Config } from "../config/config"
  4. import { Instance } from "../project/instance"
  5. import { Identifier } from "../id/id"
  6. import PROMPT_INITIALIZE from "./template/initialize.txt"
  7. import PROMPT_REVIEW from "./template/review.txt"
  8. import { MCP } from "../mcp"
  9. export namespace Command {
  10. export const Event = {
  11. Executed: BusEvent.define(
  12. "command.executed",
  13. z.object({
  14. name: z.string(),
  15. sessionID: Identifier.schema("session"),
  16. arguments: z.string(),
  17. messageID: Identifier.schema("message"),
  18. }),
  19. ),
  20. }
  21. export const Info = z
  22. .object({
  23. name: z.string(),
  24. description: z.string().optional(),
  25. agent: z.string().optional(),
  26. model: z.string().optional(),
  27. mcp: z.boolean().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. mcp: true,
  90. description: prompt.description,
  91. get template() {
  92. // since a getter can't be async we need to manually return a promise here
  93. return new Promise<string>(async (resolve, reject) => {
  94. const template = await MCP.getPrompt(
  95. prompt.client,
  96. prompt.name,
  97. prompt.arguments
  98. ? // substitute each argument with $1, $2, etc.
  99. Object.fromEntries(prompt.arguments?.map((argument, i) => [argument.name, `$${i + 1}`]))
  100. : {},
  101. ).catch(reject)
  102. resolve(
  103. template?.messages
  104. .map((message) => (message.content.type === "text" ? message.content.text : ""))
  105. .join("\n") || "",
  106. )
  107. })
  108. },
  109. hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [],
  110. }
  111. }
  112. return result
  113. })
  114. export async function get(name: string) {
  115. return state().then((x) => x[name])
  116. }
  117. export async function list() {
  118. return state().then((x) => Object.values(x))
  119. }
  120. }