mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 06:33:01 -04:00
116 lines
3.9 KiB
TypeScript
116 lines
3.9 KiB
TypeScript
export * as CommandPlugin from "./command.js"
|
|
|
|
import { define } from "@opencode-ai/plugin/effect/plugin"
|
|
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(MCP.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
|