refactor(core): split out instance and route through workspaces (#19335)
parent
e528ed5d86
commit
a76be695c7
|
|
@ -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")
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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(() =>
|
|||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue