import { Account } from "@/account/account" import { AccountID, OrgID } from "@/account/schema" import { Agent } from "@/agent/agent" import { Config } from "@/config" import { InstanceState } from "@/effect" import { MCP } from "@/mcp" import { Project } from "@/project" import { ProviderID, ModelID } from "@/provider/schema" import { ToolRegistry } from "@/tool" import * as EffectZod from "@/util/effect-zod" import { Worktree } from "@/worktree" import { Effect, Layer, Option, Schema } from "effect" import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "./auth" const ConsoleStateResponse = Schema.Struct({ consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)), activeOrgName: Schema.optionalKey(Schema.String), switchableOrgCount: Schema.Number, }).annotate({ identifier: "ConsoleState" }) const ConsoleOrgOption = Schema.Struct({ accountID: Schema.String, accountEmail: Schema.String, accountUrl: Schema.String, orgID: Schema.String, orgName: Schema.String, active: Schema.Boolean, }).annotate({ identifier: "ConsoleOrgOption" }) const ConsoleOrgList = Schema.Struct({ orgs: Schema.Array(ConsoleOrgOption), }).annotate({ identifier: "ConsoleOrgList" }) const ConsoleSwitchPayload = Schema.Struct({ accountID: AccountID, orgID: OrgID, }).annotate({ identifier: "ConsoleSwitchInput" }) const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" }) const ToolListItem = Schema.Struct({ id: Schema.String, description: Schema.String, parameters: Schema.Record(Schema.String, Schema.Any), }).annotate({ identifier: "ToolListItem" }) const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" }) const ToolListQuery = Schema.Struct({ provider: ProviderID, model: ModelID, }) const WorktreeList = Schema.Array(Schema.String).annotate({ identifier: "WorktreeList" }) export const ExperimentalPaths = { console: "/experimental/console", consoleOrgs: "/experimental/console/orgs", consoleSwitch: "/experimental/console/switch", tool: "/experimental/tool", toolIDs: "/experimental/tool/ids", worktree: "/experimental/worktree", worktreeReset: "/experimental/worktree/reset", resource: "/experimental/resource", } as const export const ExperimentalApi = HttpApi.make("experimental") .add( HttpApiGroup.make("experimental") .add( HttpApiEndpoint.get("console", ExperimentalPaths.console, { success: ConsoleStateResponse, }).annotateMerge( OpenApi.annotations({ identifier: "experimental.console.get", summary: "Get active Console provider metadata", description: "Get the active Console org name and the set of provider IDs managed by that Console org.", }), ), HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, { success: ConsoleOrgList, }).annotateMerge( OpenApi.annotations({ identifier: "experimental.console.listOrgs", summary: "List switchable Console orgs", description: "Get the available Console orgs across logged-in accounts, including the current active org.", }), ), HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, { payload: ConsoleSwitchPayload, success: Schema.Boolean, }).annotateMerge( OpenApi.annotations({ identifier: "experimental.console.switchOrg", summary: "Switch active Console org", description: "Persist a new active Console account/org selection for the current local OpenCode state.", }), ), HttpApiEndpoint.get("tool", ExperimentalPaths.tool, { query: ToolListQuery, success: ToolList, }).annotateMerge( OpenApi.annotations({ identifier: "tool.list", summary: "List tools", description: "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.", }), ), HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, { success: ToolIDs, }).annotateMerge( OpenApi.annotations({ identifier: "tool.ids", summary: "List tool IDs", description: "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", }), ), HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, { success: WorktreeList, }).annotateMerge( OpenApi.annotations({ identifier: "worktree.list", summary: "List worktrees", description: "List all sandbox worktrees for the current project.", }), ), HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, { payload: Schema.optional(Worktree.CreateInput), success: Worktree.Info, }).annotateMerge( OpenApi.annotations({ identifier: "worktree.create", summary: "Create worktree", description: "Create a new git worktree for the current project and run any configured startup scripts.", }), ), HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, { payload: Worktree.RemoveInput, success: Schema.Boolean, }).annotateMerge( OpenApi.annotations({ identifier: "worktree.remove", summary: "Remove worktree", description: "Remove a git worktree and delete its branch.", }), ), HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, { payload: Worktree.ResetInput, success: Schema.Boolean, }).annotateMerge( OpenApi.annotations({ identifier: "worktree.reset", summary: "Reset worktree", description: "Reset a worktree branch to the primary default branch.", }), ), HttpApiEndpoint.get("resource", ExperimentalPaths.resource, { success: Schema.Record(Schema.String, MCP.Resource), }).annotateMerge( OpenApi.annotations({ identifier: "experimental.resource.list", summary: "Get MCP resources", description: "Get all available MCP resources from connected servers. Optionally filter by name.", }), ), ) .annotateMerge( OpenApi.annotations({ title: "experimental", description: "Experimental HttpApi read-only routes.", }), ) .middleware(Authorization), ) .annotateMerge( OpenApi.annotations({ title: "opencode experimental HttpApi", version: "0.0.1", description: "Experimental HttpApi surface for selected instance routes.", }), ) export const experimentalHandlers = Layer.unwrap( Effect.gen(function* () { const account = yield* Account.Service const agents = yield* Agent.Service const config = yield* Config.Service const mcp = yield* MCP.Service const project = yield* Project.Service const registry = yield* ToolRegistry.Service const worktreeSvc = yield* Worktree.Service const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { const [state, groups] = yield* Effect.all( [config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)], { concurrency: "unbounded", }, ) return { consoleManagedProviders: state.consoleManagedProviders, ...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}), switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0), } }) const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () { const [groups, active] = yield* Effect.all( [account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)], { concurrency: "unbounded", }, ) const info = Option.getOrUndefined(active) return { orgs: groups.flatMap((group) => group.orgs.map((org) => ({ accountID: group.account.id, accountEmail: group.account.email, accountUrl: group.account.url, orgID: org.id, orgName: org.name, active: !!info && info.id === group.account.id && info.active_org_id === org.id, })), ), } }) const switchConsole = Effect.fn("ExperimentalHttpApi.consoleSwitch")(function* (ctx: { payload: typeof ConsoleSwitchPayload.Type }) { yield* account .use(ctx.payload.accountID, Option.some(ctx.payload.orgID)) .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({})))) return true }) const tool = Effect.fn("ExperimentalHttpApi.tool")(function* (ctx: { query: typeof ToolListQuery.Type }) { const list = yield* registry.tools({ providerID: ctx.query.provider, modelID: ctx.query.model, agent: yield* agents.get(yield* agents.defaultAgent()), }) return list.map((item) => ({ id: item.id, description: item.description, parameters: EffectZod.toJsonSchema(item.parameters), })) }) const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () { return yield* registry.ids() }) const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () { const ctx = yield* InstanceState.context return yield* project.sandboxes(ctx.project.id) }) const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: { payload: Worktree.CreateInput | undefined }) { return yield* worktreeSvc.create(ctx.payload) }) const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: { payload: Worktree.RemoveInput }) { const ctx = yield* InstanceState.context yield* worktreeSvc.remove(input.payload) yield* project.removeSandbox(ctx.project.id, input.payload.directory) return true }) const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: { payload: Worktree.ResetInput }) { yield* worktreeSvc.reset(ctx.payload) return true }) const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () { return yield* mcp.resources() }) return HttpApiBuilder.group(ExperimentalApi, "experimental", (handlers) => handlers .handle("console", getConsole) .handle("consoleOrgs", listConsoleOrgs) .handle("consoleSwitch", switchConsole) .handle("tool", tool) .handle("toolIDs", toolIDs) .handle("worktree", worktree) .handle("worktreeCreate", worktreeCreate) .handle("worktreeRemove", worktreeRemove) .handle("worktreeReset", worktreeReset) .handle("resource", resource), ) }), ).pipe( Layer.provide(Account.defaultLayer), Layer.provide(Agent.defaultLayer), Layer.provide(Config.defaultLayer), Layer.provide(MCP.defaultLayer), Layer.provide(Project.defaultLayer), Layer.provide(ToolRegistry.defaultLayer), Layer.provide(Worktree.defaultLayer), )