import { Agent } from "@/agent/agent" import { Command } from "@/command" import { Format } from "@/format" import { Global } from "@opencode-ai/core/global" import { LSP } from "@/lsp" import { Vcs } from "@/project" import { Skill } from "@/skill" import * as InstanceState from "@/effect/instance-state" import { Effect, Layer, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" import { markInstanceForDisposal } from "./lifecycle" const PathInfo = Schema.Struct({ home: Schema.String, state: Schema.String, config: Schema.String, worktree: Schema.String, directory: Schema.String, }).annotate({ identifier: "Path" }) const VcsDiffQuery = Schema.Struct({ mode: Vcs.Mode, }) export const InstancePaths = { dispose: "/instance/dispose", path: "/path", vcs: "/vcs", vcsDiff: "/vcs/diff", command: "/command", agent: "/agent", skill: "/skill", lsp: "/lsp", formatter: "/formatter", } as const export const InstanceApi = HttpApi.make("instance") .add( HttpApiGroup.make("instance") .add( HttpApiEndpoint.post("dispose", InstancePaths.dispose, { success: Schema.Boolean, }).annotateMerge( OpenApi.annotations({ identifier: "instance.dispose", summary: "Dispose instance", description: "Clean up and dispose the current OpenCode instance, releasing all resources.", }), ), HttpApiEndpoint.get("path", InstancePaths.path, { success: PathInfo, }).annotateMerge( OpenApi.annotations({ identifier: "path.get", summary: "Get paths", description: "Retrieve the current working directory and related path information for the OpenCode instance.", }), ), HttpApiEndpoint.get("vcs", InstancePaths.vcs, { success: Vcs.Info, }).annotateMerge( OpenApi.annotations({ identifier: "vcs.get", summary: "Get VCS info", description: "Retrieve version control system (VCS) information for the current project, such as git branch.", }), ), HttpApiEndpoint.get("vcsDiff", InstancePaths.vcsDiff, { query: VcsDiffQuery, success: Schema.Array(Vcs.FileDiff), }).annotateMerge( OpenApi.annotations({ identifier: "vcs.diff", summary: "Get VCS diff", description: "Retrieve the current git diff for the working tree or against the default branch.", }), ), HttpApiEndpoint.get("command", InstancePaths.command, { success: Schema.Array(Command.Info), }).annotateMerge( OpenApi.annotations({ identifier: "command.list", summary: "List commands", description: "Get a list of all available commands in the OpenCode system.", }), ), HttpApiEndpoint.get("agent", InstancePaths.agent, { success: Schema.Array(Agent.Info), }).annotateMerge( OpenApi.annotations({ identifier: "app.agents", summary: "List agents", description: "Get a list of all available AI agents in the OpenCode system.", }), ), HttpApiEndpoint.get("skill", InstancePaths.skill, { success: Schema.Array(Skill.Info), }).annotateMerge( OpenApi.annotations({ identifier: "app.skills", summary: "List skills", description: "Get a list of all available skills in the OpenCode system.", }), ), HttpApiEndpoint.get("lsp", InstancePaths.lsp, { success: Schema.Array(LSP.Status), }).annotateMerge( OpenApi.annotations({ identifier: "lsp.status", summary: "Get LSP status", description: "Get LSP server status", }), ), HttpApiEndpoint.get("formatter", InstancePaths.formatter, { success: Schema.Array(Format.Status), }).annotateMerge( OpenApi.annotations({ identifier: "formatter.status", summary: "Get formatter status", description: "Get formatter status", }), ), ) .annotateMerge( OpenApi.annotations({ title: "instance", description: "Experimental HttpApi instance read routes.", }), ) .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ title: "opencode experimental HttpApi", version: "0.0.1", description: "Experimental HttpApi surface for selected instance routes.", }), ) export const instanceHandlers = Layer.unwrap( Effect.gen(function* () { const agent = yield* Agent.Service const command = yield* Command.Service const format = yield* Format.Service const lsp = yield* LSP.Service const skill = yield* Skill.Service const vcs = yield* Vcs.Service const dispose = Effect.fn("InstanceHttpApi.dispose")(function* () { yield* markInstanceForDisposal(yield* InstanceState.context) return true }) const getPath = Effect.fn("InstanceHttpApi.path")(function* () { const ctx = yield* InstanceState.context return { home: Global.Path.home, state: Global.Path.state, config: Global.Path.config, worktree: ctx.worktree, directory: ctx.directory, } }) const getVcs = Effect.fn("InstanceHttpApi.vcs")(function* () { const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 }) return { branch, default_branch } }) const getVcsDiff = Effect.fn("InstanceHttpApi.vcsDiff")(function* (ctx: { query: { mode: Vcs.Mode } }) { return yield* vcs.diff(ctx.query.mode) }) const getCommand = Effect.fn("InstanceHttpApi.command")(function* () { return yield* command.list() }) const getAgent = Effect.fn("InstanceHttpApi.agent")(function* () { return yield* agent.list() }) const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () { return yield* skill.all() }) const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () { return yield* lsp.status() }) const getFormatter = Effect.fn("InstanceHttpApi.formatter")(function* () { return yield* format.status() }) return HttpApiBuilder.group(InstanceApi, "instance", (handlers) => handlers .handle("dispose", dispose) .handle("path", getPath) .handle("vcs", getVcs) .handle("vcsDiff", getVcsDiff) .handle("command", getCommand) .handle("agent", getAgent) .handle("skill", getSkill) .handle("lsp", getLsp) .handle("formatter", getFormatter), ) }), ).pipe( Layer.provide(Agent.defaultLayer), Layer.provide(Command.defaultLayer), Layer.provide(Format.defaultLayer), Layer.provide(LSP.defaultLayer), Layer.provide(Skill.defaultLayer), Layer.provide(Vcs.defaultLayer), )