import { Hono } from "hono" import { describeRoute, validator, resolver } from "hono-openapi" import z from "zod" import { ProviderID, ModelID } from "../../provider/schema" import { ToolRegistry } from "../../tool/registry" import { Worktree } from "../../worktree" import { Instance } from "../../project/instance" import { Project } from "../../project/project" import { MCP } from "../../mcp" import { Session } from "../../session" import { Config } from "../../config/config" import { ConsoleState } from "../../config/console-state" import { Account, AccountID, OrgID } from "../../account" import { zodToJsonSchema } from "zod-to-json-schema" import { errors } from "../error" import { lazy } from "../../util/lazy" import { WorkspaceRoutes } from "./workspace" import { Agent } from "@/agent/agent" const ConsoleOrgOption = z.object({ accountID: z.string(), accountEmail: z.string(), accountUrl: z.string(), orgID: z.string(), orgName: z.string(), active: z.boolean(), }) const ConsoleOrgList = z.object({ orgs: z.array(ConsoleOrgOption), }) const ConsoleSwitchBody = z.object({ accountID: z.string(), orgID: z.string(), }) export const ExperimentalRoutes = lazy(() => new Hono() .get( "/console", describeRoute({ summary: "Get active Console provider metadata", description: "Get the active Console org name and the set of provider IDs managed by that Console org.", operationId: "experimental.console.get", responses: { 200: { description: "Active Console provider metadata", content: { "application/json": { schema: resolver(ConsoleState), }, }, }, }, }), async (c) => { const [consoleState, groups] = await Promise.all([Config.getConsoleState(), Account.orgsByAccount()]) return c.json({ ...consoleState, switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0), }) }, ) .get( "/console/orgs", describeRoute({ summary: "List switchable Console orgs", description: "Get the available Console orgs across logged-in accounts, including the current active org.", operationId: "experimental.console.listOrgs", responses: { 200: { description: "Switchable Console orgs", content: { "application/json": { schema: resolver(ConsoleOrgList), }, }, }, }, }), async (c) => { const [groups, active] = await Promise.all([Account.orgsByAccount(), Account.active()]) const 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: !!active && active.id === group.account.id && active.active_org_id === org.id, })), ) return c.json({ orgs }) }, ) .post( "/console/switch", describeRoute({ summary: "Switch active Console org", description: "Persist a new active Console account/org selection for the current local OpenCode state.", operationId: "experimental.console.switchOrg", responses: { 200: { description: "Switch success", content: { "application/json": { schema: resolver(z.boolean()), }, }, }, }, }), validator("json", ConsoleSwitchBody), async (c) => { const body = c.req.valid("json") await Account.switchOrg(AccountID.make(body.accountID), OrgID.make(body.orgID)) return c.json(true) }, ) .get( "/tool/ids", describeRoute({ summary: "List tool IDs", description: "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", operationId: "tool.ids", responses: { 200: { description: "Tool IDs", content: { "application/json": { schema: resolver(z.array(z.string()).meta({ ref: "ToolIDs" })), }, }, }, ...errors(400), }, }), async (c) => { return c.json(await ToolRegistry.ids()) }, ) .get( "/tool", describeRoute({ summary: "List tools", description: "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.", operationId: "tool.list", responses: { 200: { description: "Tools", content: { "application/json": { schema: resolver( z .array( z .object({ id: z.string(), description: z.string(), parameters: z.any(), }) .meta({ ref: "ToolListItem" }), ) .meta({ ref: "ToolList" }), ), }, }, }, ...errors(400), }, }), validator( "query", z.object({ provider: z.string(), model: z.string(), }), ), async (c) => { const { provider, model } = c.req.valid("query") const tools = await ToolRegistry.tools({ providerID: ProviderID.make(provider), modelID: ModelID.make(model), agent: await Agent.get(await Agent.defaultAgent()), }) return c.json( tools.map((t) => ({ id: t.id, description: t.description, // Handle both Zod schemas and plain JSON schemas parameters: (t.parameters as any)?._def ? zodToJsonSchema(t.parameters as any) : t.parameters, })), ) }, ) .route("/workspace", WorkspaceRoutes()) .post( "/worktree", describeRoute({ summary: "Create worktree", description: "Create a new git worktree for the current project and run any configured startup scripts.", operationId: "worktree.create", responses: { 200: { description: "Worktree created", content: { "application/json": { schema: resolver(Worktree.Info), }, }, }, ...errors(400), }, }), validator("json", Worktree.CreateInput.optional()), async (c) => { const body = c.req.valid("json") const worktree = await Worktree.create(body) return c.json(worktree) }, ) .get( "/worktree", describeRoute({ summary: "List worktrees", description: "List all sandbox worktrees for the current project.", operationId: "worktree.list", responses: { 200: { description: "List of worktree directories", content: { "application/json": { schema: resolver(z.array(z.string())), }, }, }, }, }), async (c) => { const sandboxes = await Project.sandboxes(Instance.project.id) return c.json(sandboxes) }, ) .delete( "/worktree", describeRoute({ summary: "Remove worktree", description: "Remove a git worktree and delete its branch.", operationId: "worktree.remove", responses: { 200: { description: "Worktree removed", content: { "application/json": { schema: resolver(z.boolean()), }, }, }, ...errors(400), }, }), validator("json", Worktree.RemoveInput), async (c) => { const body = c.req.valid("json") await Worktree.remove(body) await Project.removeSandbox(Instance.project.id, body.directory) return c.json(true) }, ) .post( "/worktree/reset", describeRoute({ summary: "Reset worktree", description: "Reset a worktree branch to the primary default branch.", operationId: "worktree.reset", responses: { 200: { description: "Worktree reset", content: { "application/json": { schema: resolver(z.boolean()), }, }, }, ...errors(400), }, }), validator("json", Worktree.ResetInput), async (c) => { const body = c.req.valid("json") await Worktree.reset(body) return c.json(true) }, ) .get( "/session", describeRoute({ summary: "List sessions", description: "Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.", operationId: "experimental.session.list", responses: { 200: { description: "List of sessions", content: { "application/json": { schema: resolver(Session.GlobalInfo.array()), }, }, }, }, }), validator( "query", z.object({ directory: z.string().optional().meta({ description: "Filter sessions by project directory" }), roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }), start: z.coerce .number() .optional() .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }), cursor: z.coerce .number() .optional() .meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }), search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }), limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }), archived: z.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" }), }), ), async (c) => { const query = c.req.valid("query") const limit = query.limit ?? 100 const sessions: Session.GlobalInfo[] = [] for await (const session of Session.listGlobal({ directory: query.directory, roots: query.roots, start: query.start, cursor: query.cursor, search: query.search, limit: limit + 1, archived: query.archived, })) { sessions.push(session) } const hasMore = sessions.length > limit const list = hasMore ? sessions.slice(0, limit) : sessions if (hasMore && list.length > 0) { c.header("x-next-cursor", String(list[list.length - 1].time.updated)) } return c.json(list) }, ) .get( "/resource", describeRoute({ summary: "Get MCP resources", description: "Get all available MCP resources from connected servers. Optionally filter by name.", operationId: "experimental.resource.list", responses: { 200: { description: "MCP resources", content: { "application/json": { schema: resolver(z.record(z.string(), MCP.Resource)), }, }, }, }, }), async (c) => { return c.json(await MCP.resources()) }, ), )