| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- import { LayerNode } from "@kirincode-ai/core/effect/layer-node"
- import path from "path"
- import { InstanceState } from "@/effect/instance-state"
- import { EffectBridge } from "@/effect/bridge"
- import type { InstanceContext } from "@/project/instance-context"
- import { Effect, Layer, Context, Schema } from "effect"
- import { Config } from "@/config/config"
- import { MCP } from "../mcp"
- import { Skill } from "../skill"
- import PROMPT_INITIALIZE from "./template/initialize.txt"
- import PROMPT_REVIEW from "./template/review.txt"
- import { LegacyEvent } from "@kirincode-ai/schema/legacy-event"
- type State = {
- commands: Record<string, Info>
- }
- export const Event = {
- Executed: LegacyEvent.CommandExecuted,
- }
- export const Info = Schema.Struct({
- name: Schema.String,
- description: Schema.optional(Schema.String),
- agent: Schema.optional(Schema.String),
- model: Schema.optional(Schema.String),
- source: Schema.optional(Schema.Literals(["command", "mcp", "skill"])),
- // Some command templates are lazy promises from MCP prompt resolution.
- template: Schema.Unknown,
- subtask: Schema.optional(Schema.Boolean),
- hints: Schema.Array(Schema.String),
- }).annotate({ identifier: "Command" })
- export type Info = Omit<Schema.Schema.Type<typeof Info>, "template"> & { template: Promise<string> | string }
- export function hints(template: 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<Info | undefined>
- readonly list: () => Effect.Effect<Info[]>
- }
- export class Service extends Context.Service<Service, Interface>()("@kirincode/Command") {}
- const layer = Layer.effect(
- Service,
- Effect.gen(function* () {
- const config = yield* Config.Service
- const mcp = yield* MCP.Service
- const skill = yield* Skill.Service
- const init = Effect.fn("Command.state")(function* (ctx: InstanceContext) {
- const cfg = yield* config.get()
- const bridge = yield* EffectBridge.make()
- const commands: Record<string, Info> = {}
- commands[Default.INIT] = {
- name: Default.INIT,
- description: "guided AGENTS.md setup",
- 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(yield* mcp.prompts())) {
- commands[name] = {
- name,
- source: "mcp",
- description: prompt.description,
- get template() {
- return bridge.promise(
- mcp
- .getPrompt(
- prompt.client,
- prompt.name,
- prompt.arguments
- ? Object.fromEntries(prompt.arguments.map((argument, i) => [argument.name, `$${i + 1}`]))
- : {},
- )
- .pipe(
- Effect.map(
- (template) =>
- template?.messages
- .map((message) => (message.content.type === "text" ? message.content.text : ""))
- .join("\n") || "",
- ),
- ),
- )
- },
- hints: prompt.arguments?.map((_, i) => `$${i + 1}`) ?? [],
- }
- }
- for (const item of yield* skill.all()) {
- if (commands[item.name]) continue
- const dir = item.location === "<built-in>" ? undefined : path.dirname(item.location)
- commands[item.name] = {
- name: item.name,
- description: item.description,
- source: "skill",
- get template() {
- if (!dir) return item.content
- return [
- item.content,
- "",
- `Base directory for this skill: ${dir}`,
- "Relative paths in this skill (e.g., scripts/, references/) are relative to this base directory.",
- ].join("\n")
- },
- hints: [],
- }
- }
- return {
- commands,
- }
- })
- const state = yield* InstanceState.make<State>((ctx) => init(ctx))
- const get = Effect.fn("Command.get")(function* (name: string) {
- const s = yield* InstanceState.get(state)
- return s.commands[name]
- })
- const list = Effect.fn("Command.list")(function* () {
- const s = yield* InstanceState.get(state)
- return Object.values(s.commands)
- })
- return Service.of({ get, list })
- }),
- )
- export const node = LayerNode.make({ service: Service, layer: layer, deps: [Config.node, MCP.node, Skill.node] })
- export * as Command from "."
|