import { BusEvent } from "@/bus/bus-event" import { InstanceState } from "@/effect/instance-state" import { makeRunPromise } from "@/effect/run-service" import { SessionID, MessageID } from "@/session/schema" import { Effect, Layer, ServiceMap } from "effect" import z from "zod" import { Config } from "../config/config" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" import { MCP } from "../mcp" import { Skill } from "../skill" import { Log } from "../util/log" export namespace Command { const log = Log.create({ service: "command" }) type State = { commands: Record ensure: () => Promise } export const Event = { Executed: BusEvent.define( "command.executed", z.object({ name: z.string(), sessionID: SessionID.zod, arguments: z.string(), messageID: MessageID.zod, }), ), } export const Info = z .object({ name: z.string(), description: z.string().optional(), agent: z.string().optional(), model: z.string().optional(), source: z.enum(["command", "mcp", "skill"]).optional(), template: z.promise(z.string()).or(z.string()), subtask: z.boolean().optional(), hints: z.array(z.string()), }) .meta({ ref: "Command", }) 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 export interface Interface { readonly get: (name: string) => Effect.Effect readonly list: () => Effect.Effect } export class Service extends ServiceMap.Service()("@opencode/Command") {} export const layer = Layer.effect( Service, Effect.gen(function* () { const cache = yield* InstanceState.make( Effect.fn("Command.state")(function* (ctx) { const commands: Record = {} let task: Promise | undefined async function load() { const cfg = await Config.get() commands[Default.INIT] = { name: Default.INIT, description: "create/update AGENTS.md", source: "command", get template() { return PROMPT_INITIALIZE.replace("${path}", ctx.worktree) }, hints: hints(PROMPT_INITIALIZE), } commands[Default.REVIEW] = { name: Default.REVIEW, description: "review changes [commit|branch|pr], defaults to uncommitted", source: "command", get template() { return PROMPT_REVIEW.replace("${path}", ctx.worktree) }, subtask: true, hints: hints(PROMPT_REVIEW), } for (const [name, command] of Object.entries(cfg.command ?? {})) { commands[name] = { name, agent: command.agent, model: command.model, description: command.description, source: "command", get template() { return command.template }, subtask: command.subtask, hints: hints(command.template), } } for (const [name, prompt] of Object.entries(await MCP.prompts())) { commands[name] = { name, source: "mcp", description: prompt.description, get template() { return new Promise(async (resolve, reject) => { const template = await MCP.getPrompt( prompt.client, prompt.name, prompt.arguments ? 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}`) ?? [], } } for (const skill of await Skill.all()) { if (commands[skill.name]) continue commands[skill.name] = { name: skill.name, description: skill.description, source: "skill", get template() { return skill.content }, hints: [], } } } return { commands, ensure: () => { task ??= Effect.runPromise( Effect.tryPromise({ try: load, catch: (cause) => cause, }).pipe(Effect.catchCause((cause) => Effect.sync(() => log.error("init failed", { cause })))), ) return task }, } }), ) const get = Effect.fn("Command.get")(function* (name: string) { const state = yield* InstanceState.get(cache) yield* Effect.promise(() => state.ensure()) return state.commands[name] }) const list = Effect.fn("Command.list")(function* () { const state = yield* InstanceState.get(cache) yield* Effect.promise(() => state.ensure()) return Object.values(state.commands) }) return Service.of({ get, list }) }), ) const runPromise = makeRunPromise(Service, layer) export async function get(name: string) { return runPromise((svc) => svc.get(name)) } export async function list() { return runPromise((svc) => svc.list()) } }