export * as CommandPlugin from "./command.js" import { define } from "@opencode-ai/plugin/effect/plugin" import { McpEvent } from "@opencode-ai/schema/mcp-event" import { Effect, Stream } from "effect" import { Bus } from "../bus.js" import { Location } from "../location.js" import { MCP } from "../mcp/index.js" import PROMPT_INITIALIZE from "./command/initialize.txt" import PROMPT_REVIEW from "./command/review.txt" export const Plugin = define({ id: "opencode.command", effect: Effect.fn(function* (ctx) { const location = yield* Location.Service const mcp = yield* MCP.Service const bus = yield* Bus.Service const loaded = { prompts: [] as MCP.Prompt[] } yield* bus .subscribe(McpEvent.PromptsChanged) .pipe( Stream.runForEach(() => mcp.prompts().pipe( Effect.tap((prompts) => Effect.sync(() => (loaded.prompts = prompts))), Effect.andThen(ctx.command.reload()), ), ), Effect.forkScoped({ startImmediately: true }), ) loaded.prompts = yield* mcp.prompts() yield* ctx.command.transform((draft) => { draft.add({ name: "init", description: "guided AGENTS.md setup", execute: (input) => ctx.session .prompt({ ...input.prompt, sessionID: input.sessionID, text: append(PROMPT_INITIALIZE.replace("${path}", location.project.directory), input.prompt.text), delivery: input.delivery, }) .pipe(Effect.asVoid), }) draft.add({ name: "review", description: "review changes [commit|branch|pr], defaults to uncommitted", execute: (input) => ctx.session .prompt({ ...input.prompt, sessionID: input.sessionID, text: append(PROMPT_REVIEW.replace("${path}", location.project.directory), input.prompt.text), delivery: input.delivery, }) .pipe(Effect.asVoid), }) for (const prompt of loaded.prompts) { draft.add({ name: mcpCommandName(prompt.server, prompt.name), description: prompt.description, execute: (input) => Effect.gen(function* () { const result = yield* mcp.prompt({ server: prompt.server, name: prompt.name, args: Object.fromEntries( (prompt.arguments ?? []).map((argument, index) => [ argument.name, parseArguments(input.prompt.text)[index] ?? "", ]), ), }) if (!result) return yield* Effect.fail(new Error(`MCP prompt not found: ${prompt.server}:${prompt.name}`)) yield* ctx.session.prompt({ ...input.prompt, sessionID: input.sessionID, text: result.messages .map((message) => promptMessageText(message.content)) .join("\n") .trim(), delivery: input.delivery, }) }).pipe(Effect.asVoid), }) } }) }), }) function append(template: string, input: string) { return [template, input.trim()].filter(Boolean).join("\n\n") } function parseArguments(input: string) { return (input.match(argsRegex) ?? []).map((argument) => argument.replace(quoteTrimRegex, "")) } function promptMessageText(content: unknown) { if (typeof content === "string") return content if (!content || typeof content !== "object") return "" if (!("type" in content) || content.type !== "text") return "" if (!("text" in content) || typeof content.text !== "string") return "" return content.text } function mcpCommandName(server: string, prompt: string) { return `${sanitize(server)}:${sanitize(prompt)}` } function sanitize(value: string) { return value.replace(/[^a-zA-Z0-9_-]/g, "_") } const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi const quoteTrimRegex = /^["']|["']$/g