summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorJames Long <[email protected]>2026-03-27 11:51:21 -0400
committerGitHub <[email protected]>2026-03-27 11:51:21 -0400
commita76be695c7d2e60683fe79c8a6dc2c402ab13349 (patch)
treed4ed22031b8ce90dee54f189626380ccc7f25eed /packages
parente528ed5d86dc386044552c9306af0e35baea1b95 (diff)
downloadopencode-a76be695c7d2e60683fe79c8a6dc2c402ab13349.tar.gz
opencode-a76be695c7d2e60683fe79c8a6dc2c402ab13349.zip
refactor(core): split out instance and route through workspaces (#19335)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/control-plane/workspace-router-middleware.ts45
-rw-r--r--packages/opencode/src/server/instance.ts307
-rw-r--r--packages/opencode/src/server/middleware.ts29
-rw-r--r--packages/opencode/src/server/routes/event.ts6
-rw-r--r--packages/opencode/src/server/server.ts347
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts224
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts94
-rw-r--r--packages/sdk/openapi.json168
8 files changed, 622 insertions, 598 deletions
diff --git a/packages/opencode/src/control-plane/workspace-router-middleware.ts b/packages/opencode/src/control-plane/workspace-router-middleware.ts
index 283350532..1fc19a22b 100644
--- a/packages/opencode/src/control-plane/workspace-router-middleware.ts
+++ b/packages/opencode/src/control-plane/workspace-router-middleware.ts
@@ -3,6 +3,8 @@ import { Flag } from "../flag/flag"
import { getAdaptor } from "./adaptors"
import { WorkspaceID } from "./schema"
import { Workspace } from "./workspace"
+import { InstanceRoutes } from "../server/instance"
+import { lazy } from "../util/lazy"
type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
@@ -20,16 +22,25 @@ function local(method: string, path: string) {
return false
}
-async function routeRequest(req: Request) {
- const url = new URL(req.url)
- const raw = url.searchParams.get("workspace") || req.headers.get("x-opencode-workspace")
+const routes = lazy(() => InstanceRoutes())
- if (!raw) return
+export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => {
+ if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
+ return routes().fetch(c.req.raw, c.env)
+ }
- if (local(req.method, url.pathname)) return
+ const url = new URL(c.req.url)
+ const raw = url.searchParams.get("workspace")
- const workspaceID = WorkspaceID.make(raw)
+ if (!raw) {
+ return routes().fetch(c.req.raw, c.env)
+ }
+ if (local(c.req.method, url.pathname)) {
+ return routes().fetch(c.req.raw, c.env)
+ }
+
+ const workspaceID = WorkspaceID.make(raw)
const workspace = await Workspace.get(workspaceID)
if (!workspace) {
return new Response(`Workspace not found: ${workspaceID}`, {
@@ -41,27 +52,13 @@ async function routeRequest(req: Request) {
}
const adaptor = await getAdaptor(workspace.type)
-
- const headers = new Headers(req.headers)
+ const headers = new Headers(c.req.raw.headers)
headers.delete("x-opencode-workspace")
return adaptor.fetch(workspace, `${url.pathname}${url.search}`, {
- method: req.method,
- body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(),
- signal: req.signal,
+ method: c.req.method,
+ body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(),
+ signal: c.req.raw.signal,
headers,
})
}
-
-export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c, next) => {
- // Only available in development for now
- if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
- return next()
- }
-
- const response = await routeRequest(c.req.raw)
- if (response) {
- return response
- }
- return next()
-}
diff --git a/packages/opencode/src/server/instance.ts b/packages/opencode/src/server/instance.ts
new file mode 100644
index 000000000..b99cf3d99
--- /dev/null
+++ b/packages/opencode/src/server/instance.ts
@@ -0,0 +1,307 @@
+import { describeRoute, resolver } from "hono-openapi"
+import { Hono } from "hono"
+import { proxy } from "hono/proxy"
+import z from "zod"
+import { createHash } from "node:crypto"
+import { Log } from "../util/log"
+import { Format } from "../format"
+import { TuiRoutes } from "./routes/tui"
+import { Instance } from "../project/instance"
+import { Vcs } from "../project/vcs"
+import { Agent } from "../agent/agent"
+import { Skill } from "../skill"
+import { Global } from "../global"
+import { LSP } from "../lsp"
+import { Command } from "../command"
+import { Flag } from "../flag/flag"
+import { Filesystem } from "@/util/filesystem"
+import { QuestionRoutes } from "./routes/question"
+import { PermissionRoutes } from "./routes/permission"
+import { ProjectRoutes } from "./routes/project"
+import { SessionRoutes } from "./routes/session"
+import { PtyRoutes } from "./routes/pty"
+import { McpRoutes } from "./routes/mcp"
+import { FileRoutes } from "./routes/file"
+import { ConfigRoutes } from "./routes/config"
+import { ExperimentalRoutes } from "./routes/experimental"
+import { ProviderRoutes } from "./routes/provider"
+import { EventRoutes } from "./routes/event"
+import { InstanceBootstrap } from "../project/bootstrap"
+import { errorHandler } from "./middleware"
+
+const log = Log.create({ service: "server" })
+
+const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
+ ? Promise.resolve(null)
+ : // @ts-expect-error - generated file at build time
+ import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
+
+const DEFAULT_CSP =
+ "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
+
+const csp = (hash = "") =>
+ `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
+
+export const InstanceRoutes = (app?: Hono) =>
+ (app ?? new Hono())
+ .onError(errorHandler(log))
+ .use(async (c, next) => {
+ const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
+ const directory = Filesystem.resolve(
+ (() => {
+ try {
+ return decodeURIComponent(raw)
+ } catch {
+ return raw
+ }
+ })(),
+ )
+
+ return Instance.provide({
+ directory,
+ init: InstanceBootstrap,
+ async fn() {
+ return next()
+ },
+ })
+ })
+ .route("/project", ProjectRoutes())
+ .route("/pty", PtyRoutes())
+ .route("/config", ConfigRoutes())
+ .route("/experimental", ExperimentalRoutes())
+ .route("/session", SessionRoutes())
+ .route("/permission", PermissionRoutes())
+ .route("/question", QuestionRoutes())
+ .route("/provider", ProviderRoutes())
+ .route("/", FileRoutes())
+ .route("/", EventRoutes())
+ .route("/mcp", McpRoutes())
+ .route("/tui", TuiRoutes())
+ .post(
+ "/instance/dispose",
+ describeRoute({
+ summary: "Dispose instance",
+ description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
+ operationId: "instance.dispose",
+ responses: {
+ 200: {
+ description: "Instance disposed",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ await Instance.dispose()
+ return c.json(true)
+ },
+ )
+ .get(
+ "/path",
+ describeRoute({
+ summary: "Get paths",
+ description: "Retrieve the current working directory and related path information for the OpenCode instance.",
+ operationId: "path.get",
+ responses: {
+ 200: {
+ description: "Path",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z
+ .object({
+ home: z.string(),
+ state: z.string(),
+ config: z.string(),
+ worktree: z.string(),
+ directory: z.string(),
+ })
+ .meta({
+ ref: "Path",
+ }),
+ ),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json({
+ home: Global.Path.home,
+ state: Global.Path.state,
+ config: Global.Path.config,
+ worktree: Instance.worktree,
+ directory: Instance.directory,
+ })
+ },
+ )
+ .get(
+ "/vcs",
+ describeRoute({
+ summary: "Get VCS info",
+ description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
+ operationId: "vcs.get",
+ responses: {
+ 200: {
+ description: "VCS info",
+ content: {
+ "application/json": {
+ schema: resolver(Vcs.Info),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const branch = await Vcs.branch()
+ return c.json({
+ branch,
+ })
+ },
+ )
+ .get(
+ "/command",
+ describeRoute({
+ summary: "List commands",
+ description: "Get a list of all available commands in the OpenCode system.",
+ operationId: "command.list",
+ responses: {
+ 200: {
+ description: "List of commands",
+ content: {
+ "application/json": {
+ schema: resolver(Command.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const commands = await Command.list()
+ return c.json(commands)
+ },
+ )
+ .get(
+ "/agent",
+ describeRoute({
+ summary: "List agents",
+ description: "Get a list of all available AI agents in the OpenCode system.",
+ operationId: "app.agents",
+ responses: {
+ 200: {
+ description: "List of agents",
+ content: {
+ "application/json": {
+ schema: resolver(Agent.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const modes = await Agent.list()
+ return c.json(modes)
+ },
+ )
+ .get(
+ "/skill",
+ describeRoute({
+ summary: "List skills",
+ description: "Get a list of all available skills in the OpenCode system.",
+ operationId: "app.skills",
+ responses: {
+ 200: {
+ description: "List of skills",
+ content: {
+ "application/json": {
+ schema: resolver(Skill.Info.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ const skills = await Skill.all()
+ return c.json(skills)
+ },
+ )
+ .get(
+ "/lsp",
+ describeRoute({
+ summary: "Get LSP status",
+ description: "Get LSP server status",
+ operationId: "lsp.status",
+ responses: {
+ 200: {
+ description: "LSP server status",
+ content: {
+ "application/json": {
+ schema: resolver(LSP.Status.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(await LSP.status())
+ },
+ )
+ .get(
+ "/formatter",
+ describeRoute({
+ summary: "Get formatter status",
+ description: "Get formatter status",
+ operationId: "formatter.status",
+ responses: {
+ 200: {
+ description: "Formatter status",
+ content: {
+ "application/json": {
+ schema: resolver(Format.Status.array()),
+ },
+ },
+ },
+ },
+ }),
+ async (c) => {
+ return c.json(await Format.status())
+ },
+ )
+ .all("/*", async (c) => {
+ const embeddedWebUI = await embeddedUIPromise
+ const path = c.req.path
+
+ if (embeddedWebUI) {
+ const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
+ if (!match) return c.json({ error: "Not Found" }, 404)
+ const file = Bun.file(match)
+ if (await file.exists()) {
+ c.header("Content-Type", file.type)
+ if (file.type.startsWith("text/html")) {
+ c.header("Content-Security-Policy", DEFAULT_CSP)
+ }
+ return c.body(await file.arrayBuffer())
+ } else {
+ return c.json({ error: "Not Found" }, 404)
+ }
+ } else {
+ const response = await proxy(`https://app.opencode.ai${path}`, {
+ ...c.req,
+ headers: {
+ ...c.req.raw.headers,
+ host: "app.opencode.ai",
+ },
+ })
+ const match = response.headers.get("content-type")?.includes("text/html")
+ ? (await response.clone().text()).match(
+ /<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
+ )
+ : undefined
+ const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
+ response.headers.set("Content-Security-Policy", csp(hash))
+ return response
+ }
+ })
diff --git a/packages/opencode/src/server/middleware.ts b/packages/opencode/src/server/middleware.ts
new file mode 100644
index 000000000..ebf0163cd
--- /dev/null
+++ b/packages/opencode/src/server/middleware.ts
@@ -0,0 +1,29 @@
+import { Provider } from "../provider/provider"
+import { NamedError } from "@opencode-ai/util/error"
+import { NotFoundError } from "../storage/db"
+import type { ContentfulStatusCode } from "hono/utils/http-status"
+import type { ErrorHandler } from "hono"
+import { HTTPException } from "hono/http-exception"
+import type { Log } from "../util/log"
+
+export function errorHandler(log: Log.Logger): ErrorHandler {
+ return (err, c) => {
+ log.error("failed", {
+ error: err,
+ })
+ if (err instanceof NamedError) {
+ let status: ContentfulStatusCode
+ if (err instanceof NotFoundError) status = 404
+ else if (err instanceof Provider.ModelNotFoundError) status = 400
+ else if (err.name === "ProviderAuthValidationFailed") status = 400
+ else if (err.name.startsWith("Worktree")) status = 400
+ else status = 500
+ return c.json(err.toObject(), { status })
+ }
+ if (err instanceof HTTPException) return err.getResponse()
+ const message = err instanceof Error && err.stack ? err.stack : err.toString()
+ return c.json(new NamedError.Unknown({ message }).toObject(), {
+ status: 500,
+ })
+ }
+}
diff --git a/packages/opencode/src/server/routes/event.ts b/packages/opencode/src/server/routes/event.ts
index 96284242f..989b85771 100644
--- a/packages/opencode/src/server/routes/event.ts
+++ b/packages/opencode/src/server/routes/event.ts
@@ -4,12 +4,11 @@ import { streamSSE } from "hono/streaming"
import { Log } from "@/util/log"
import { BusEvent } from "@/bus/bus-event"
import { Bus } from "@/bus"
-import { lazy } from "../../util/lazy"
import { AsyncQueue } from "../../util/queue"
const log = Log.create({ service: "server" })
-export const EventRoutes = lazy(() =>
+export const EventRoutes = () =>
new Hono().get(
"/event",
describeRoute({
@@ -81,5 +80,4 @@ export const EventRoutes = lazy(() =>
}
})
},
- ),
-)
+ )
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 7dc6ec1bd..cfb22929b 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -1,67 +1,30 @@
-import { createHash } from "node:crypto"
import { Log } from "../util/log"
import { describeRoute, generateSpecs, validator, resolver, openAPIRouteHandler } from "hono-openapi"
import { Hono } from "hono"
import { compress } from "hono/compress"
import { cors } from "hono/cors"
-import { proxy } from "hono/proxy"
import { basicAuth } from "hono/basic-auth"
import z from "zod"
-import { Provider } from "../provider/provider"
-import { NamedError } from "@opencode-ai/util/error"
-import { LSP } from "../lsp"
-import { Format } from "../format"
-import { TuiRoutes } from "./routes/tui"
-import { Instance } from "../project/instance"
-import { Vcs } from "../project/vcs"
-import { Agent } from "../agent/agent"
-import { Skill } from "../skill"
import { Auth } from "../auth"
import { Flag } from "../flag/flag"
-import { Command } from "../command"
-import { Global } from "../global"
-import { WorkspaceID } from "../control-plane/schema"
import { ProviderID } from "../provider/schema"
import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware"
-import { ProjectRoutes } from "./routes/project"
-import { SessionRoutes } from "./routes/session"
-import { PtyRoutes } from "./routes/pty"
-import { McpRoutes } from "./routes/mcp"
-import { FileRoutes } from "./routes/file"
-import { ConfigRoutes } from "./routes/config"
-import { ExperimentalRoutes } from "./routes/experimental"
-import { ProviderRoutes } from "./routes/provider"
-import { EventRoutes } from "./routes/event"
-import { InstanceBootstrap } from "../project/bootstrap"
-import { NotFoundError } from "../storage/db"
-import type { ContentfulStatusCode } from "hono/utils/http-status"
import { websocket } from "hono/bun"
-import { HTTPException } from "hono/http-exception"
import { errors } from "./error"
-import { Filesystem } from "@/util/filesystem"
-import { QuestionRoutes } from "./routes/question"
-import { PermissionRoutes } from "./routes/permission"
import { GlobalRoutes } from "./routes/global"
import { MDNS } from "./mdns"
import { lazy } from "@/util/lazy"
+import { errorHandler } from "./middleware"
+import { InstanceRoutes } from "./instance"
import { initProjectors } from "./projectors"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false
-const csp = (hash = "") =>
- `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'${hash ? ` 'sha256-${hash}'` : ""}; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:`
-
initProjectors()
export namespace Server {
const log = Log.create({ service: "server" })
- const DEFAULT_CSP =
- "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:"
- const embeddedUIPromise = Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI
- ? Promise.resolve(null)
- : // @ts-expect-error - generated file at build time
- import("opencode-web-ui.gen.ts").then((module) => module.default as Record<string, string>).catch(() => null)
const zipped = compress()
@@ -71,30 +34,12 @@ export namespace Server {
return false
}
- export const Default = lazy(() => createApp({}))
+ export const Default = lazy(() => ControlPlaneRoutes())
- export const createApp = (opts: { cors?: string[] }): Hono => {
+ export const ControlPlaneRoutes = (opts?: { cors?: string[] }): Hono => {
const app = new Hono()
return app
- .onError((err, c) => {
- log.error("failed", {
- error: err,
- })
- if (err instanceof NamedError) {
- let status: ContentfulStatusCode
- if (err instanceof NotFoundError) status = 404
- else if (err instanceof Provider.ModelNotFoundError) status = 400
- else if (err.name === "ProviderAuthValidationFailed") status = 400
- else if (err.name.startsWith("Worktree")) status = 400
- else status = 500
- return c.json(err.toObject(), { status })
- }
- if (err instanceof HTTPException) return err.getResponse()
- const message = err instanceof Error && err.stack ? err.stack : err.toString()
- return c.json(new NamedError.Unknown({ message }).toObject(), {
- status: 500,
- })
- })
+ .onError(errorHandler(log))
.use((c, next) => {
// Allow CORS preflight requests to succeed without auth.
// Browser clients sending Authorization headers will preflight with OPTIONS.
@@ -105,8 +50,8 @@ export namespace Server {
return basicAuth({ username, password })(c, next)
})
.use(async (c, next) => {
- const skipLogging = c.req.path === "/log"
- if (!skipLogging) {
+ const skip = c.req.path === "/log"
+ if (!skip) {
log.info("request", {
method: c.req.method,
path: c.req.path,
@@ -117,7 +62,7 @@ export namespace Server {
path: c.req.path,
})
await next()
- if (!skipLogging) {
+ if (!skip) {
timer.stop()
}
})
@@ -215,27 +160,6 @@ export namespace Server {
return c.json(true)
},
)
- .use(async (c, next) => {
- if (c.req.path === "/log") return next()
- const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
- const directory = Filesystem.resolve(
- (() => {
- try {
- return decodeURIComponent(raw)
- } catch {
- return raw
- }
- })(),
- )
-
- return Instance.provide({
- directory,
- init: InstanceBootstrap,
- async fn() {
- return next()
- },
- })
- })
.get(
"/doc",
openAPIRouteHandler(app, {
@@ -258,126 +182,6 @@ export namespace Server {
}),
),
)
- .use(WorkspaceRouterMiddleware)
- .route("/project", ProjectRoutes())
- .route("/pty", PtyRoutes())
- .route("/config", ConfigRoutes())
- .route("/experimental", ExperimentalRoutes())
- .route("/session", SessionRoutes())
- .route("/permission", PermissionRoutes())
- .route("/question", QuestionRoutes())
- .route("/provider", ProviderRoutes())
- .route("/", FileRoutes())
- .route("/", EventRoutes())
- .route("/mcp", McpRoutes())
- .route("/tui", TuiRoutes())
- .post(
- "/instance/dispose",
- describeRoute({
- summary: "Dispose instance",
- description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
- operationId: "instance.dispose",
- responses: {
- 200: {
- description: "Instance disposed",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
- },
- }),
- async (c) => {
- await Instance.dispose()
- return c.json(true)
- },
- )
- .get(
- "/path",
- describeRoute({
- summary: "Get paths",
- description: "Retrieve the current working directory and related path information for the OpenCode instance.",
- operationId: "path.get",
- responses: {
- 200: {
- description: "Path",
- content: {
- "application/json": {
- schema: resolver(
- z
- .object({
- home: z.string(),
- state: z.string(),
- config: z.string(),
- worktree: z.string(),
- directory: z.string(),
- })
- .meta({
- ref: "Path",
- }),
- ),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json({
- home: Global.Path.home,
- state: Global.Path.state,
- config: Global.Path.config,
- worktree: Instance.worktree,
- directory: Instance.directory,
- })
- },
- )
- .get(
- "/vcs",
- describeRoute({
- summary: "Get VCS info",
- description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
- operationId: "vcs.get",
- responses: {
- 200: {
- description: "VCS info",
- content: {
- "application/json": {
- schema: resolver(Vcs.Info),
- },
- },
- },
- },
- }),
- async (c) => {
- const branch = await Vcs.branch()
- return c.json({
- branch,
- })
- },
- )
- .get(
- "/command",
- describeRoute({
- summary: "List commands",
- description: "Get a list of all available commands in the OpenCode system.",
- operationId: "command.list",
- responses: {
- 200: {
- description: "List of commands",
- content: {
- "application/json": {
- schema: resolver(Command.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const commands = await Command.list()
- return c.json(commands)
- },
- )
.post(
"/log",
describeRoute({
@@ -430,132 +234,21 @@ export namespace Server {
return c.json(true)
},
)
- .get(
- "/agent",
- describeRoute({
- summary: "List agents",
- description: "Get a list of all available AI agents in the OpenCode system.",
- operationId: "app.agents",
- responses: {
- 200: {
- description: "List of agents",
- content: {
- "application/json": {
- schema: resolver(Agent.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const modes = await Agent.list()
- return c.json(modes)
- },
- )
- .get(
- "/skill",
- describeRoute({
- summary: "List skills",
- description: "Get a list of all available skills in the OpenCode system.",
- operationId: "app.skills",
- responses: {
- 200: {
- description: "List of skills",
- content: {
- "application/json": {
- schema: resolver(Skill.Info.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- const skills = await Skill.all()
- return c.json(skills)
- },
- )
- .get(
- "/lsp",
- describeRoute({
- summary: "Get LSP status",
- description: "Get LSP server status",
- operationId: "lsp.status",
- responses: {
- 200: {
- description: "LSP server status",
- content: {
- "application/json": {
- schema: resolver(LSP.Status.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(await LSP.status())
- },
- )
- .get(
- "/formatter",
- describeRoute({
- summary: "Get formatter status",
- description: "Get formatter status",
- operationId: "formatter.status",
- responses: {
- 200: {
- description: "Formatter status",
- content: {
- "application/json": {
- schema: resolver(Format.Status.array()),
- },
- },
- },
- },
- }),
- async (c) => {
- return c.json(await Format.status())
- },
- )
- .all("/*", async (c) => {
- const embeddedWebUI = await embeddedUIPromise
- const path = c.req.path
+ .use(WorkspaceRouterMiddleware)
+ }
- if (embeddedWebUI) {
- const match = embeddedWebUI[path.replace(/^\//, "")] ?? embeddedWebUI["index.html"] ?? null
- if (!match) return c.json({ error: "Not Found" }, 404)
- const file = Bun.file(match)
- if (await file.exists()) {
- c.header("Content-Type", file.type)
- if (file.type.startsWith("text/html")) {
- c.header("Content-Security-Policy", DEFAULT_CSP)
- }
- return c.body(await file.arrayBuffer())
- } else {
- return c.json({ error: "Not Found" }, 404)
- }
- } else {
- const response = await proxy(`https://app.opencode.ai${path}`, {
- ...c.req,
- headers: {
- ...c.req.raw.headers,
- host: "app.opencode.ai",
- },
- })
- const match = response.headers.get("content-type")?.includes("text/html")
- ? (await response.clone().text()).match(
- /<script\b(?![^>]*\bsrc\s*=)[^>]*\bid=(['"])oc-theme-preload-script\1[^>]*>([\s\S]*?)<\/script>/i,
- )
- : undefined
- const hash = match ? createHash("sha256").update(match[2]).digest("base64") : ""
- response.headers.set("Content-Security-Policy", csp(hash))
- return response
- }
- }) as unknown as Hono
+ export function createApp(opts: { cors?: string[] }) {
+ return ControlPlaneRoutes(opts)
}
export async function openapi() {
- // Cast to break excessive type recursion from long route chains
- const result = await generateSpecs(Default(), {
+ // Build a fresh app with all routes registered directly so
+ // hono-openapi can see describeRoute metadata (`.route()` wraps
+ // handlers when the sub-app has a custom errorHandler, which
+ // strips the metadata symbol).
+ const app = ControlPlaneRoutes()
+ InstanceRoutes(app)
+ const result = await generateSpecs(app, {
documentation: {
info: {
title: "opencode",
@@ -579,7 +272,7 @@ export namespace Server {
cors?: string[]
}) {
url = new URL(`http://${opts.hostname}:${opts.port}`)
- const app = createApp(opts)
+ const app = ControlPlaneRoutes({ cors: opts.cors })
const args = {
hostname: opts.hostname,
idleTimeout: 0,
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index 410906844..527584e7e 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -411,6 +411,113 @@ export class Auth extends HeyApiClient {
}
}
+export class App extends HeyApiClient {
+ /**
+ * Write log
+ *
+ * Write a log entry to the server logs with specified level and metadata.
+ */
+ public log<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ service?: string
+ level?: "debug" | "info" | "error" | "warn"
+ message?: string
+ extra?: {
+ [key: string]: unknown
+ }
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ { in: "body", key: "service" },
+ { in: "body", key: "level" },
+ { in: "body", key: "message" },
+ { in: "body", key: "extra" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post<AppLogResponses, AppLogErrors, ThrowOnError>({
+ url: "/log",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+
+ /**
+ * List agents
+ *
+ * Get a list of all available AI agents in the OpenCode system.
+ */
+ public agents<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<AppAgentsResponses, unknown, ThrowOnError>({
+ url: "/agent",
+ ...options,
+ ...params,
+ })
+ }
+
+ /**
+ * List skills
+ *
+ * Get a list of all available skills in the OpenCode system.
+ */
+ public skills<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ workspace?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "query", key: "workspace" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).get<AppSkillsResponses, unknown, ThrowOnError>({
+ url: "/skill",
+ ...options,
+ ...params,
+ })
+ }
+}
+
export class Project extends HeyApiClient {
/**
* List all projects
@@ -3773,113 +3880,6 @@ export class Command extends HeyApiClient {
}
}
-export class App extends HeyApiClient {
- /**
- * Write log
- *
- * Write a log entry to the server logs with specified level and metadata.
- */
- public log<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- service?: string
- level?: "debug" | "info" | "error" | "warn"
- message?: string
- extra?: {
- [key: string]: unknown
- }
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- { in: "body", key: "service" },
- { in: "body", key: "level" },
- { in: "body", key: "message" },
- { in: "body", key: "extra" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).post<AppLogResponses, AppLogErrors, ThrowOnError>({
- url: "/log",
- ...options,
- ...params,
- headers: {
- "Content-Type": "application/json",
- ...options?.headers,
- ...params.headers,
- },
- })
- }
-
- /**
- * List agents
- *
- * Get a list of all available AI agents in the OpenCode system.
- */
- public agents<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).get<AppAgentsResponses, unknown, ThrowOnError>({
- url: "/agent",
- ...options,
- ...params,
- })
- }
-
- /**
- * List skills
- *
- * Get a list of all available skills in the OpenCode system.
- */
- public skills<ThrowOnError extends boolean = false>(
- parameters?: {
- directory?: string
- workspace?: string
- },
- options?: Options<never, ThrowOnError>,
- ) {
- const params = buildClientParams(
- [parameters],
- [
- {
- args: [
- { in: "query", key: "directory" },
- { in: "query", key: "workspace" },
- ],
- },
- ],
- )
- return (options?.client ?? this.client).get<AppSkillsResponses, unknown, ThrowOnError>({
- url: "/skill",
- ...options,
- ...params,
- })
- }
-}
-
export class Lsp extends HeyApiClient {
/**
* Get LSP status
@@ -3962,6 +3962,11 @@ export class OpencodeClient extends HeyApiClient {
return (this._auth ??= new Auth({ client: this.client }))
}
+ private _app?: App
+ get app(): App {
+ return (this._app ??= new App({ client: this.client }))
+ }
+
private _project?: Project
get project(): Project {
return (this._project ??= new Project({ client: this.client }))
@@ -4062,11 +4067,6 @@ export class OpencodeClient extends HeyApiClient {
return (this._command ??= new Command({ client: this.client }))
}
- private _app?: App
- get app(): App {
- return (this._app ??= new App({ client: this.client }))
- }
-
private _lsp?: Lsp
get lsp(): Lsp {
return (this._lsp ??= new Lsp({ client: this.client }))
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 4d0b13539..318b8907a 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -2249,6 +2249,53 @@ export type AuthSetResponses = {
export type AuthSetResponse = AuthSetResponses[keyof AuthSetResponses]
+export type AppLogData = {
+ body?: {
+ /**
+ * Service name for the log entry
+ */
+ service: string
+ /**
+ * Log level
+ */
+ level: "debug" | "info" | "error" | "warn"
+ /**
+ * Log message
+ */
+ message: string
+ /**
+ * Additional metadata for the log entry
+ */
+ extra?: {
+ [key: string]: unknown
+ }
+ }
+ path?: never
+ query?: {
+ directory?: string
+ workspace?: string
+ }
+ url: "/log"
+}
+
+export type AppLogErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+}
+
+export type AppLogError = AppLogErrors[keyof AppLogErrors]
+
+export type AppLogResponses = {
+ /**
+ * Log entry written successfully
+ */
+ 200: boolean
+}
+
+export type AppLogResponse = AppLogResponses[keyof AppLogResponses]
+
export type ProjectListData = {
body?: never
path?: never
@@ -5036,53 +5083,6 @@ export type CommandListResponses = {
export type CommandListResponse = CommandListResponses[keyof CommandListResponses]
-export type AppLogData = {
- body?: {
- /**
- * Service name for the log entry
- */
- service: string
- /**
- * Log level
- */
- level: "debug" | "info" | "error" | "warn"
- /**
- * Log message
- */
- message: string
- /**
- * Additional metadata for the log entry
- */
- extra?: {
- [key: string]: unknown
- }
- }
- path?: never
- query?: {
- directory?: string
- workspace?: string
- }
- url: "/log"
-}
-
-export type AppLogErrors = {
- /**
- * Bad request
- */
- 400: BadRequestError
-}
-
-export type AppLogError = AppLogErrors[keyof AppLogErrors]
-
-export type AppLogResponses = {
- /**
- * Log entry written successfully
- */
- 200: boolean
-}
-
-export type AppLogResponse = AppLogResponses[keyof AppLogResponses]
-
export type AppAgentsData = {
body?: never
path?: never
diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json
index 007391177..5362e1daa 100644
--- a/packages/sdk/openapi.json
+++ b/packages/sdk/openapi.json
@@ -356,6 +356,90 @@
]
}
},
+ "/log": {
+ "post": {
+ "operationId": "app.log",
+ "parameters": [
+ {
+ "in": "query",
+ "name": "directory",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "in": "query",
+ "name": "workspace",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "summary": "Write log",
+ "description": "Write a log entry to the server logs with specified level and metadata.",
+ "responses": {
+ "200": {
+ "description": "Log entry written successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad request",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BadRequestError"
+ }
+ }
+ }
+ }
+ },
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "service": {
+ "description": "Service name for the log entry",
+ "type": "string"
+ },
+ "level": {
+ "description": "Log level",
+ "type": "string",
+ "enum": ["debug", "info", "error", "warn"]
+ },
+ "message": {
+ "description": "Log message",
+ "type": "string"
+ },
+ "extra": {
+ "description": "Additional metadata for the log entry",
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "required": ["service", "level", "message"]
+ }
+ }
+ }
+ },
+ "x-codeSamples": [
+ {
+ "lang": "js",
+ "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.log({\n ...\n})"
+ }
+ ]
+ }
+ },
"/project": {
"get": {
"operationId": "project.list",
@@ -6762,90 +6846,6 @@
]
}
},
- "/log": {
- "post": {
- "operationId": "app.log",
- "parameters": [
- {
- "in": "query",
- "name": "directory",
- "schema": {
- "type": "string"
- }
- },
- {
- "in": "query",
- "name": "workspace",
- "schema": {
- "type": "string"
- }
- }
- ],
- "summary": "Write log",
- "description": "Write a log entry to the server logs with specified level and metadata.",
- "responses": {
- "200": {
- "description": "Log entry written successfully",
- "content": {
- "application/json": {
- "schema": {
- "type": "boolean"
- }
- }
- }
- },
- "400": {
- "description": "Bad request",
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/BadRequestError"
- }
- }
- }
- }
- },
- "requestBody": {
- "content": {
- "application/json": {
- "schema": {
- "type": "object",
- "properties": {
- "service": {
- "description": "Service name for the log entry",
- "type": "string"
- },
- "level": {
- "description": "Log level",
- "type": "string",
- "enum": ["debug", "info", "error", "warn"]
- },
- "message": {
- "description": "Log message",
- "type": "string"
- },
- "extra": {
- "description": "Additional metadata for the log entry",
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {}
- }
- },
- "required": ["service", "level", "message"]
- }
- }
- }
- },
- "x-codeSamples": [
- {
- "lang": "js",
- "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.app.log({\n ...\n})"
- }
- ]
- }
- },
"/agent": {
"get": {
"operationId": "app.agents",