From 5b1feb4c54d6729b3a209adec68fbe24364ffb71 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 28 Apr 2026 22:56:18 -0400 Subject: [PATCH] fix(httpapi): preserve SDK schema parity --- .../routes/instance/httpapi/groups/config.ts | 10 +- .../routes/instance/httpapi/groups/control.ts | 14 +- .../instance/httpapi/groups/experimental.ts | 38 +- .../routes/instance/httpapi/groups/file.ts | 13 +- .../routes/instance/httpapi/groups/global.ts | 25 +- .../instance/httpapi/groups/instance.ts | 17 +- .../routes/instance/httpapi/groups/mcp.ts | 32 +- .../instance/httpapi/groups/metadata.ts | 18 + .../instance/httpapi/groups/permission.ts | 14 +- .../routes/instance/httpapi/groups/project.ts | 19 +- .../instance/httpapi/groups/provider.ts | 9 +- .../routes/instance/httpapi/groups/pty.ts | 26 +- .../instance/httpapi/groups/question.ts | 18 +- .../routes/instance/httpapi/groups/session.ts | 103 +++-- .../routes/instance/httpapi/groups/sync.ts | 16 +- .../routes/instance/httpapi/groups/tui.ts | 34 +- .../instance/httpapi/groups/workspace.ts | 30 +- .../server/routes/instance/httpapi/public.ts | 420 ++++++++---------- 18 files changed, 445 insertions(+), 411 deletions(-) create mode 100644 packages/opencode/src/server/routes/instance/httpapi/groups/metadata.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts index ec88567c57..4ff406e2a4 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts @@ -1,8 +1,9 @@ import { Config } from "@/config/config" import { Provider } from "@/provider/provider" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/config" @@ -11,7 +12,7 @@ export const ConfigApi = HttpApi.make("config") HttpApiGroup.make("config") .add( HttpApiEndpoint.get("get", root, { - success: Config.Info, + success: described(Config.Info, "Get config info"), }).annotateMerge( OpenApi.annotations({ identifier: "config.get", @@ -21,7 +22,8 @@ export const ConfigApi = HttpApi.make("config") ), HttpApiEndpoint.patch("update", root, { payload: Config.Info, - success: Config.Info, + success: described(Config.Info, "Successfully updated config"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "config.update", @@ -30,7 +32,7 @@ export const ConfigApi = HttpApi.make("config") }), ), HttpApiEndpoint.get("providers", `${root}/providers`, { - success: Provider.ConfigProvidersResult, + success: described(Provider.ConfigProvidersResult, "List of providers"), }).annotateMerge( OpenApi.annotations({ identifier: "config.providers", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/control.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/control.ts index 34edd59341..33e6a8e4a0 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/control.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/control.ts @@ -1,7 +1,8 @@ import { Auth } from "@/auth" import { ProviderID } from "@/provider/schema" import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { described } from "./metadata" const AuthParams = Schema.Struct({ providerID: ProviderID, @@ -24,7 +25,7 @@ export const LogInput = Schema.Struct({ extra: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)).annotate({ description: "Additional metadata for the log entry", }), -}).annotate({ identifier: "AppLogInput" }) +}) export const ControlPaths = { auth: "/auth/:providerID", @@ -37,7 +38,8 @@ export const ControlApi = HttpApi.make("control").add( HttpApiEndpoint.put("authSet", ControlPaths.auth, { params: AuthParams, payload: Auth.Info, - success: Schema.Boolean, + success: described(Schema.Boolean, "Successfully set authentication credentials"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "auth.set", @@ -47,7 +49,8 @@ export const ControlApi = HttpApi.make("control").add( ), HttpApiEndpoint.delete("authRemove", ControlPaths.auth, { params: AuthParams, - success: Schema.Boolean, + success: described(Schema.Boolean, "Successfully removed authentication credentials"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "auth.remove", @@ -58,7 +61,8 @@ export const ControlApi = HttpApi.make("control").add( HttpApiEndpoint.post("log", ControlPaths.log, { query: LogQuery, payload: LogInput, - success: Schema.Boolean, + success: described(Schema.Boolean, "Log entry written successfully"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "app.log", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts index 2b1e5f9b1a..2a562b46b3 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/experimental.ts @@ -8,6 +8,7 @@ import { Schema, SchemaGetter } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const ConsoleStateResponse = Schema.Struct({ consoleManagedProviders: Schema.mutable(Schema.Array(Schema.String)), @@ -22,22 +23,22 @@ const ConsoleOrgOption = Schema.Struct({ orgID: Schema.String, orgName: Schema.String, active: Schema.Boolean, -}).annotate({ identifier: "ConsoleOrgOption" }) +}) const ConsoleOrgList = Schema.Struct({ orgs: Schema.Array(ConsoleOrgOption), -}).annotate({ identifier: "ConsoleOrgList" }) +}) export 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), + parameters: Schema.Unknown, }).annotate({ identifier: "ToolListItem" }) const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" }) export const ToolListQuery = Schema.Struct({ @@ -51,7 +52,7 @@ const QueryBoolean = Schema.Literals(["true", "false"]).pipe( encode: SchemaGetter.transform((value) => (value ? "true" : "false")), }), ) -const WorktreeList = Schema.Array(Schema.String).annotate({ identifier: "WorktreeList" }) +const WorktreeList = Schema.Array(Schema.String) export const SessionListQuery = Schema.Struct({ directory: Schema.optional(Schema.String), roots: Schema.optional(QueryBoolean), @@ -79,7 +80,7 @@ export const ExperimentalApi = HttpApi.make("experimental") HttpApiGroup.make("experimental") .add( HttpApiEndpoint.get("console", ExperimentalPaths.console, { - success: ConsoleStateResponse, + success: described(ConsoleStateResponse, "Active Console provider metadata"), }).annotateMerge( OpenApi.annotations({ identifier: "experimental.console.get", @@ -88,7 +89,7 @@ export const ExperimentalApi = HttpApi.make("experimental") }), ), HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, { - success: ConsoleOrgList, + success: described(ConsoleOrgList, "Switchable Console orgs"), }).annotateMerge( OpenApi.annotations({ identifier: "experimental.console.listOrgs", @@ -98,7 +99,7 @@ export const ExperimentalApi = HttpApi.make("experimental") ), HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, { payload: ConsoleSwitchPayload, - success: Schema.Boolean, + success: described(Schema.Boolean, "Switch success"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -109,7 +110,8 @@ export const ExperimentalApi = HttpApi.make("experimental") ), HttpApiEndpoint.get("tool", ExperimentalPaths.tool, { query: ToolListQuery, - success: ToolList, + success: described(ToolList, "Tools"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "tool.list", @@ -119,7 +121,8 @@ export const ExperimentalApi = HttpApi.make("experimental") }), ), HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, { - success: ToolIDs, + success: described(ToolIDs, "Tool IDs"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "tool.ids", @@ -129,7 +132,7 @@ export const ExperimentalApi = HttpApi.make("experimental") }), ), HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, { - success: WorktreeList, + success: described(WorktreeList, "List of worktree directories"), }).annotateMerge( OpenApi.annotations({ identifier: "worktree.list", @@ -139,7 +142,8 @@ export const ExperimentalApi = HttpApi.make("experimental") ), HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, { payload: Schema.optional(Worktree.CreateInput), - success: Worktree.Info, + success: described(Worktree.Info, "Worktree created"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "worktree.create", @@ -149,7 +153,8 @@ export const ExperimentalApi = HttpApi.make("experimental") ), HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, { payload: Worktree.RemoveInput, - success: Schema.Boolean, + success: described(Schema.Boolean, "Worktree removed"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "worktree.remove", @@ -159,7 +164,8 @@ export const ExperimentalApi = HttpApi.make("experimental") ), HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, { payload: Worktree.ResetInput, - success: Schema.Boolean, + success: described(Schema.Boolean, "Worktree reset"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "worktree.reset", @@ -169,7 +175,7 @@ export const ExperimentalApi = HttpApi.make("experimental") ), HttpApiEndpoint.get("session", ExperimentalPaths.session, { query: SessionListQuery, - success: Schema.Array(Session.GlobalInfo), + success: described(Schema.Array(Session.GlobalInfo), "List of sessions"), }).annotateMerge( OpenApi.annotations({ identifier: "experimental.session.list", @@ -179,7 +185,7 @@ export const ExperimentalApi = HttpApi.make("experimental") }), ), HttpApiEndpoint.get("resource", ExperimentalPaths.resource, { - success: Schema.Record(Schema.String, MCP.Resource), + success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"), }).annotateMerge( OpenApi.annotations({ identifier: "experimental.resource.list", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/file.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/file.ts index e80281e50f..3a4f3df7f9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/file.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/file.ts @@ -5,6 +5,7 @@ import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" export const FileQuery = Schema.Struct({ path: Schema.String, @@ -42,7 +43,7 @@ export const FileApi = HttpApi.make("file") .add( HttpApiEndpoint.get("findText", FilePaths.findText, { query: FindTextQuery, - success: Schema.Array(Ripgrep.SearchMatch), + success: described(Schema.Array(Ripgrep.SearchMatch), "Matches"), }).annotateMerge( OpenApi.annotations({ identifier: "find.text", @@ -52,7 +53,7 @@ export const FileApi = HttpApi.make("file") ), HttpApiEndpoint.get("findFile", FilePaths.findFile, { query: FindFileQuery, - success: Schema.Array(Schema.String), + success: described(Schema.Array(Schema.String), "File paths"), }).annotateMerge( OpenApi.annotations({ identifier: "find.files", @@ -62,7 +63,7 @@ export const FileApi = HttpApi.make("file") ), HttpApiEndpoint.get("findSymbol", FilePaths.findSymbol, { query: FindSymbolQuery, - success: Schema.Array(LSP.Symbol), + success: described(Schema.Array(LSP.Symbol), "Symbols"), }).annotateMerge( OpenApi.annotations({ identifier: "find.symbols", @@ -72,7 +73,7 @@ export const FileApi = HttpApi.make("file") ), HttpApiEndpoint.get("list", FilePaths.list, { query: FileQuery, - success: Schema.Array(File.Node), + success: described(Schema.Array(File.Node), "Files and directories"), }).annotateMerge( OpenApi.annotations({ identifier: "file.list", @@ -82,7 +83,7 @@ export const FileApi = HttpApi.make("file") ), HttpApiEndpoint.get("content", FilePaths.content, { query: FileQuery, - success: File.Content, + success: described(File.Content, "File content"), }).annotateMerge( OpenApi.annotations({ identifier: "file.read", @@ -91,7 +92,7 @@ export const FileApi = HttpApi.make("file") }), ), HttpApiEndpoint.get("status", FilePaths.status, { - success: Schema.Array(File.Info), + success: described(Schema.Array(File.Info), "File status"), }).annotateMerge( OpenApi.annotations({ identifier: "file.status", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts index f1fe23cf37..272b086065 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts @@ -1,22 +1,25 @@ import { Config } from "@/config/config" +import { BusEvent } from "@/bus/bus-event" +import { SyncEvent } from "@/sync" import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { described } from "./metadata" const GlobalHealth = Schema.Struct({ healthy: Schema.Literal(true), version: Schema.String, -}).annotate({ identifier: "GlobalHealth" }) +}) const GlobalEventSchema = Schema.Struct({ directory: Schema.String, project: Schema.optional(Schema.String), workspace: Schema.optional(Schema.String), - payload: Schema.Unknown, + payload: Schema.Union([...BusEvent.effectPayloads(), ...SyncEvent.effectPayloads()]), }).annotate({ identifier: "GlobalEvent" }) export const GlobalUpgradeInput = Schema.Struct({ target: Schema.optional(Schema.String), -}).annotate({ identifier: "GlobalUpgradeInput" }) +}) const GlobalUpgradeResult = Schema.Union([ Schema.Struct({ @@ -27,7 +30,7 @@ const GlobalUpgradeResult = Schema.Union([ success: Schema.Literal(false), error: Schema.String, }), -]).annotate({ identifier: "GlobalUpgradeResult" }) +]) export const GlobalPaths = { health: "/global/health", @@ -41,7 +44,7 @@ export const GlobalApi = HttpApi.make("global").add( HttpApiGroup.make("global") .add( HttpApiEndpoint.get("health", GlobalPaths.health, { - success: GlobalHealth, + success: described(GlobalHealth, "Health information"), }).annotateMerge( OpenApi.annotations({ identifier: "global.health", @@ -59,7 +62,7 @@ export const GlobalApi = HttpApi.make("global").add( }), ), HttpApiEndpoint.get("configGet", GlobalPaths.config, { - success: Config.Info, + success: described(Config.Info, "Get global config info"), }).annotateMerge( OpenApi.annotations({ identifier: "global.config.get", @@ -69,7 +72,8 @@ export const GlobalApi = HttpApi.make("global").add( ), HttpApiEndpoint.patch("configUpdate", GlobalPaths.config, { payload: Config.Info, - success: Config.Info, + success: described(Config.Info, "Successfully updated global config"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "global.config.update", @@ -78,7 +82,7 @@ export const GlobalApi = HttpApi.make("global").add( }), ), HttpApiEndpoint.post("dispose", GlobalPaths.dispose, { - success: Schema.Boolean, + success: described(Schema.Boolean, "Global disposed"), }).annotateMerge( OpenApi.annotations({ identifier: "global.dispose", @@ -88,7 +92,8 @@ export const GlobalApi = HttpApi.make("global").add( ), HttpApiEndpoint.post("upgrade", GlobalPaths.upgrade, { payload: GlobalUpgradeInput, - success: GlobalUpgradeResult, + success: described(GlobalUpgradeResult, "Upgrade result"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "global.upgrade", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts index 595d6376c2..cc450f448c 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/instance.ts @@ -8,6 +8,7 @@ import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const PathInfo = Schema.Struct({ home: Schema.String, @@ -38,7 +39,7 @@ export const InstanceApi = HttpApi.make("instance") HttpApiGroup.make("instance") .add( HttpApiEndpoint.post("dispose", InstancePaths.dispose, { - success: Schema.Boolean, + success: described(Schema.Boolean, "Instance disposed"), }).annotateMerge( OpenApi.annotations({ identifier: "instance.dispose", @@ -57,7 +58,7 @@ export const InstanceApi = HttpApi.make("instance") }), ), HttpApiEndpoint.get("vcs", InstancePaths.vcs, { - success: Vcs.Info, + success: described(Vcs.Info, "VCS info"), }).annotateMerge( OpenApi.annotations({ identifier: "vcs.get", @@ -68,7 +69,7 @@ export const InstanceApi = HttpApi.make("instance") ), HttpApiEndpoint.get("vcsDiff", InstancePaths.vcsDiff, { query: VcsDiffQuery, - success: Schema.Array(Vcs.FileDiff), + success: described(Schema.Array(Vcs.FileDiff), "VCS diff"), }).annotateMerge( OpenApi.annotations({ identifier: "vcs.diff", @@ -77,7 +78,7 @@ export const InstanceApi = HttpApi.make("instance") }), ), HttpApiEndpoint.get("command", InstancePaths.command, { - success: Schema.Array(Command.Info), + success: described(Schema.Array(Command.Info), "List of commands"), }).annotateMerge( OpenApi.annotations({ identifier: "command.list", @@ -86,7 +87,7 @@ export const InstanceApi = HttpApi.make("instance") }), ), HttpApiEndpoint.get("agent", InstancePaths.agent, { - success: Schema.Array(Agent.Info), + success: described(Schema.Array(Agent.Info), "List of agents"), }).annotateMerge( OpenApi.annotations({ identifier: "app.agents", @@ -95,7 +96,7 @@ export const InstanceApi = HttpApi.make("instance") }), ), HttpApiEndpoint.get("skill", InstancePaths.skill, { - success: Schema.Array(Skill.Info), + success: described(Schema.Array(Skill.Info), "List of skills"), }).annotateMerge( OpenApi.annotations({ identifier: "app.skills", @@ -104,7 +105,7 @@ export const InstanceApi = HttpApi.make("instance") }), ), HttpApiEndpoint.get("lsp", InstancePaths.lsp, { - success: Schema.Array(LSP.Status), + success: described(Schema.Array(LSP.Status), "LSP server status"), }).annotateMerge( OpenApi.annotations({ identifier: "lsp.status", @@ -113,7 +114,7 @@ export const InstanceApi = HttpApi.make("instance") }), ), HttpApiEndpoint.get("formatter", InstancePaths.formatter, { - success: Schema.Array(Format.Status), + success: described(Schema.Array(Format.Status), "Formatter status"), }).annotateMerge( OpenApi.annotations({ identifier: "formatter.status", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts index c327b3bd0a..149f8814a9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/mcp.ts @@ -4,23 +4,23 @@ import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" export const AddPayload = Schema.Struct({ name: Schema.String, config: ConfigMCP.Info, -}).annotate({ identifier: "McpAddInput" }) +}) export const StatusMap = Schema.Record(Schema.String, MCP.Status) export const AuthStartResponse = Schema.Struct({ authorizationUrl: Schema.String, - oauthState: Schema.String, -}).annotate({ identifier: "McpAuthStartResponse" }) +}) export const AuthCallbackPayload = Schema.Struct({ code: Schema.String, -}).annotate({ identifier: "McpAuthCallbackInput" }) +}) export const AuthRemoveResponse = Schema.Struct({ success: Schema.Literal(true), -}).annotate({ identifier: "McpAuthRemoveResponse" }) +}) export class UnsupportedOAuthError extends Schema.ErrorClass("McpUnsupportedOAuthError")( { error: Schema.String }, { httpApiStatus: 400 }, @@ -40,7 +40,7 @@ export const McpApi = HttpApi.make("mcp") HttpApiGroup.make("mcp") .add( HttpApiEndpoint.get("status", McpPaths.status, { - success: Schema.Record(Schema.String, MCP.Status), + success: described(Schema.Record(Schema.String, MCP.Status), "MCP server status"), }).annotateMerge( OpenApi.annotations({ identifier: "mcp.status", @@ -50,7 +50,7 @@ export const McpApi = HttpApi.make("mcp") ), HttpApiEndpoint.post("add", McpPaths.status, { payload: AddPayload, - success: StatusMap, + success: described(StatusMap, "MCP server added successfully"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -61,8 +61,8 @@ export const McpApi = HttpApi.make("mcp") ), HttpApiEndpoint.post("authStart", McpPaths.auth, { params: { name: Schema.String }, - success: AuthStartResponse, - error: UnsupportedOAuthError, + success: described(AuthStartResponse, "OAuth flow started"), + error: [UnsupportedOAuthError, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "mcp.auth.start", @@ -73,7 +73,8 @@ export const McpApi = HttpApi.make("mcp") HttpApiEndpoint.post("authCallback", McpPaths.authCallback, { params: { name: Schema.String }, payload: AuthCallbackPayload, - success: MCP.Status, + success: described(MCP.Status, "OAuth authentication completed"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "mcp.auth.callback", @@ -84,8 +85,8 @@ export const McpApi = HttpApi.make("mcp") ), HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, { params: { name: Schema.String }, - success: MCP.Status, - error: UnsupportedOAuthError, + success: described(MCP.Status, "OAuth authentication completed"), + error: [UnsupportedOAuthError, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "mcp.auth.authenticate", @@ -95,7 +96,8 @@ export const McpApi = HttpApi.make("mcp") ), HttpApiEndpoint.delete("authRemove", McpPaths.auth, { params: { name: Schema.String }, - success: AuthRemoveResponse, + success: described(AuthRemoveResponse, "OAuth credentials removed"), + error: HttpApiError.NotFound, }).annotateMerge( OpenApi.annotations({ identifier: "mcp.auth.remove", @@ -105,7 +107,7 @@ export const McpApi = HttpApi.make("mcp") ), HttpApiEndpoint.post("connect", McpPaths.connect, { params: { name: Schema.String }, - success: Schema.Boolean, + success: described(Schema.Boolean, "MCP server connected successfully"), }).annotateMerge( OpenApi.annotations({ identifier: "mcp.connect", @@ -114,7 +116,7 @@ export const McpApi = HttpApi.make("mcp") ), HttpApiEndpoint.post("disconnect", McpPaths.disconnect, { params: { name: Schema.String }, - success: Schema.Boolean, + success: described(Schema.Boolean, "MCP server disconnected successfully"), }).annotateMerge( OpenApi.annotations({ identifier: "mcp.disconnect", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/metadata.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/metadata.ts new file mode 100644 index 0000000000..f4841c538d --- /dev/null +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/metadata.ts @@ -0,0 +1,18 @@ +import { Schema } from "effect" +import { OpenApi } from "effect/unstable/httpapi" + +export function described(schema: S, description: string): S { + return schema.annotate({ description }) as S +} + +export function responseDescription(description: string) { + return OpenApi.annotations({ + transform: (operation) => { + const response = operation.responses?.["200"] + if (response && typeof response === "object" && "description" in response) { + response.description = description + } + return operation + }, + }) +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts index b93d3f3773..e06c98d9ef 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/permission.ts @@ -1,18 +1,23 @@ import { Permission } from "@/permission" import { PermissionID } from "@/permission/schema" import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/permission" +const ReplyPayload = Schema.Struct({ + reply: Permission.Reply, + message: Schema.optional(Schema.String), +}) export const PermissionApi = HttpApi.make("permission") .add( HttpApiGroup.make("permission") .add( HttpApiEndpoint.get("list", root, { - success: Schema.Array(Permission.Request), + success: described(Schema.Array(Permission.Request), "List of pending permissions"), }).annotateMerge( OpenApi.annotations({ identifier: "permission.list", @@ -22,8 +27,9 @@ export const PermissionApi = HttpApi.make("permission") ), HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { params: { requestID: PermissionID }, - payload: Permission.ReplyBody, - success: Schema.Boolean, + payload: ReplyPayload, + success: described(Schema.Boolean, "Permission processed successfully"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "permission.reply", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts index d2e7af6d2a..92019866e9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/project.ts @@ -1,18 +1,24 @@ import { Project } from "@/project/project" import { ProjectID } from "@/project/schema" import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/project" +const UpdatePayload = Schema.Struct({ + name: Schema.optional(Schema.String), + icon: Schema.optional(Project.Info.fields.icon), + commands: Schema.optional(Project.Info.fields.commands), +}) export const ProjectApi = HttpApi.make("project") .add( HttpApiGroup.make("project") .add( HttpApiEndpoint.get("list", root, { - success: Schema.Array(Project.Info), + success: described(Schema.Array(Project.Info), "List of projects"), }).annotateMerge( OpenApi.annotations({ identifier: "project.list", @@ -21,7 +27,7 @@ export const ProjectApi = HttpApi.make("project") }), ), HttpApiEndpoint.get("current", `${root}/current`, { - success: Project.Info, + success: described(Project.Info, "Current project information"), }).annotateMerge( OpenApi.annotations({ identifier: "project.current", @@ -30,7 +36,7 @@ export const ProjectApi = HttpApi.make("project") }), ), HttpApiEndpoint.post("initGit", `${root}/git/init`, { - success: Project.Info, + success: described(Project.Info, "Project information after git initialization"), }).annotateMerge( OpenApi.annotations({ identifier: "project.initGit", @@ -40,8 +46,9 @@ export const ProjectApi = HttpApi.make("project") ), HttpApiEndpoint.patch("update", `${root}/:projectID`, { params: { projectID: ProjectID }, - payload: Project.UpdatePayload, - success: Project.Info, + payload: UpdatePayload, + success: described(Project.Info, "Updated project information"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "project.update", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts index be9617e867..56dace0e5e 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/provider.ts @@ -5,6 +5,7 @@ import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/provider" @@ -13,7 +14,7 @@ export const ProviderApi = HttpApi.make("provider") HttpApiGroup.make("provider") .add( HttpApiEndpoint.get("list", root, { - success: Provider.ListResult, + success: described(Provider.ListResult, "List of providers"), }).annotateMerge( OpenApi.annotations({ identifier: "provider.list", @@ -22,7 +23,7 @@ export const ProviderApi = HttpApi.make("provider") }), ), HttpApiEndpoint.get("auth", `${root}/auth`, { - success: ProviderAuth.Methods, + success: described(ProviderAuth.Methods, "Provider auth methods"), }).annotateMerge( OpenApi.annotations({ identifier: "provider.auth", @@ -33,7 +34,7 @@ export const ProviderApi = HttpApi.make("provider") HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, { params: { providerID: ProviderID }, payload: ProviderAuth.AuthorizeInput, - success: Schema.UndefinedOr(ProviderAuth.Authorization), + success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -45,7 +46,7 @@ export const ProviderApi = HttpApi.make("provider") HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, { params: { providerID: ProviderID }, payload: ProviderAuth.CallbackInput, - success: Schema.Boolean, + success: described(Schema.Boolean, "OAuth callback processed successfully"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts index eb470b24b2..e3914579c1 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts @@ -4,6 +4,7 @@ import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/pty" export const Params = Schema.Struct({ ptyID: PtyID }) @@ -28,21 +29,25 @@ export const PtyApi = HttpApi.make("pty") .add( HttpApiGroup.make("pty") .add( - HttpApiEndpoint.get("shells", PtyPaths.shells, { success: Schema.Array(ShellItem) }).annotateMerge( + HttpApiEndpoint.get("shells", PtyPaths.shells, { success: described(Schema.Array(ShellItem), "List of shells") }).annotateMerge( OpenApi.annotations({ identifier: "pty.shells", summary: "List available shells", description: "Get a list of available shells on the system.", }), ), - HttpApiEndpoint.get("list", PtyPaths.list, { success: Schema.Array(Pty.Info) }).annotateMerge( + HttpApiEndpoint.get("list", PtyPaths.list, { success: described(Schema.Array(Pty.Info), "List of sessions") }).annotateMerge( OpenApi.annotations({ identifier: "pty.list", summary: "List PTY sessions", description: "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", }), ), - HttpApiEndpoint.post("create", PtyPaths.create, { payload: Pty.CreateInput, success: Pty.Info }).annotateMerge( + HttpApiEndpoint.post("create", PtyPaths.create, { + payload: Pty.CreateInput, + success: described(Pty.Info, "Created session"), + error: HttpApiError.BadRequest, + }).annotateMerge( OpenApi.annotations({ identifier: "pty.create", summary: "Create PTY session", @@ -51,7 +56,7 @@ export const PtyApi = HttpApi.make("pty") ), HttpApiEndpoint.get("get", PtyPaths.get, { params: { ptyID: PtyID }, - success: Pty.Info, + success: described(Pty.Info, "Session info"), error: HttpApiError.NotFound, }).annotateMerge( OpenApi.annotations({ @@ -63,8 +68,8 @@ export const PtyApi = HttpApi.make("pty") HttpApiEndpoint.put("update", PtyPaths.update, { params: { ptyID: PtyID }, payload: Pty.UpdateInput, - success: Pty.Info, - error: HttpApiError.NotFound, + success: described(Pty.Info, "Updated session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "pty.update", @@ -74,7 +79,8 @@ export const PtyApi = HttpApi.make("pty") ), HttpApiEndpoint.delete("remove", PtyPaths.remove, { params: { ptyID: PtyID }, - success: Schema.Boolean, + success: described(Schema.Boolean, "Session removed"), + error: HttpApiError.NotFound, }).annotateMerge( OpenApi.annotations({ identifier: "pty.remove", @@ -98,7 +104,11 @@ export const PtyApi = HttpApi.make("pty") export const PtyConnectApi = HttpApi.make("pty-connect").add( HttpApiGroup.make("pty-connect") .add( - HttpApiEndpoint.get("connect", PtyPaths.connect, { params: Params, success: Schema.Boolean }).annotateMerge( + HttpApiEndpoint.get("connect", PtyPaths.connect, { + params: Params, + success: described(Schema.Boolean, "Connected session"), + error: HttpApiError.NotFound, + }).annotateMerge( OpenApi.annotations({ identifier: "pty.connect", summary: "Connect to PTY session", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/question.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/question.ts index fa9a838108..de249823b7 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/question.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/question.ts @@ -1,18 +1,24 @@ import { Question } from "@/question" import { QuestionID } from "@/question/schema" import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/question" +const ReplyPayload = Schema.Struct({ + answers: Schema.Array(Question.Answer).annotate({ + description: "User answers in order of questions (each answer is an array of selected labels)", + }), +}) export const QuestionApi = HttpApi.make("question") .add( HttpApiGroup.make("question") .add( HttpApiEndpoint.get("list", root, { - success: Schema.Array(Question.Request), + success: described(Schema.Array(Question.Request), "List of pending questions"), }).annotateMerge( OpenApi.annotations({ identifier: "question.list", @@ -22,8 +28,9 @@ export const QuestionApi = HttpApi.make("question") ), HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, { params: { requestID: QuestionID }, - payload: Question.Reply, - success: Schema.Boolean, + payload: ReplyPayload, + success: described(Schema.Boolean, "Question answered successfully"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "question.reply", @@ -33,7 +40,8 @@ export const QuestionApi = HttpApi.make("question") ), HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, { params: { requestID: QuestionID }, - success: Schema.Boolean, + success: described(Schema.Boolean, "Question rejected successfully"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "question.reject", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts index 2463ef8b6e..5a388f1876 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/session.ts @@ -15,6 +15,7 @@ import { Schema, SchemaGetter, Struct } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/session" const QueryBoolean = Schema.Literals(["true", "false"]).pipe( @@ -46,35 +47,25 @@ export const UpdatePayload = Schema.Struct({ archived: Schema.optional(NonNegativeInt), }), ), -}).annotate({ identifier: "SessionUpdateInput" }) -export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"])).annotate({ - identifier: "SessionForkInput", }) +export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"])) export const InitPayload = Schema.Struct({ modelID: ModelID, providerID: ProviderID, messageID: MessageID, -}).annotate({ identifier: "SessionInitInput" }) +}) export const SummarizePayload = Schema.Struct({ providerID: ProviderID, modelID: ModelID, auto: Schema.optional(Schema.Boolean), -}).annotate({ identifier: "SessionSummarizeInput" }) -export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"])).annotate({ - identifier: "SessionPromptInput", -}) -export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInput.fields, ["sessionID"])).annotate({ - identifier: "SessionCommandInput", -}) -export const ShellPayload = Schema.Struct(Struct.omit(SessionPrompt.ShellInput.fields, ["sessionID"])).annotate({ - identifier: "SessionShellInput", -}) -export const RevertPayload = Schema.Struct(Struct.omit(SessionRevert.RevertInput.fields, ["sessionID"])).annotate({ - identifier: "SessionRevertInput", }) +export const PromptPayload = Schema.Struct(Struct.omit(SessionPrompt.PromptInput.fields, ["sessionID"])) +export const CommandPayload = Schema.Struct(Struct.omit(SessionPrompt.CommandInput.fields, ["sessionID"])) +export const ShellPayload = Schema.Struct(Struct.omit(SessionPrompt.ShellInput.fields, ["sessionID"])) +export const RevertPayload = Schema.Struct(Struct.omit(SessionRevert.RevertInput.fields, ["sessionID"])) export const PermissionResponsePayload = Schema.Struct({ response: Permission.Reply, -}).annotate({ identifier: "SessionPermissionResponseInput" }) +}) export const SessionPaths = { list: root, @@ -111,7 +102,7 @@ export const SessionApi = HttpApi.make("session") .add( HttpApiEndpoint.get("list", SessionPaths.list, { query: ListQuery, - success: Schema.Array(Session.Info), + success: described(Schema.Array(Session.Info), "List of sessions"), }).annotateMerge( OpenApi.annotations({ identifier: "session.list", @@ -120,7 +111,8 @@ export const SessionApi = HttpApi.make("session") }), ), HttpApiEndpoint.get("status", SessionPaths.status, { - success: StatusMap, + success: described(StatusMap, "Get session status"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "session.status", @@ -130,8 +122,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.get("get", SessionPaths.get, { params: { sessionID: SessionID }, - success: Session.Info, - error: HttpApiError.NotFound, + success: described(Session.Info, "Get session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.get", @@ -141,7 +133,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.get("children", SessionPaths.children, { params: { sessionID: SessionID }, - success: Schema.Array(Session.Info), + success: described(Schema.Array(Session.Info), "List of children"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.children", @@ -151,7 +144,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.get("todo", SessionPaths.todo, { params: { sessionID: SessionID }, - success: Schema.Array(Todo.Info), + success: described(Schema.Array(Todo.Info), "Todo list"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.todo", @@ -162,7 +156,7 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.get("diff", SessionPaths.diff, { params: { sessionID: SessionID }, query: DiffQuery, - success: Schema.Array(Snapshot.FileDiff), + success: described(Schema.Array(Snapshot.FileDiff), "Successfully retrieved diff"), }).annotateMerge( OpenApi.annotations({ identifier: "session.diff", @@ -173,7 +167,7 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.get("messages", SessionPaths.messages, { params: { sessionID: SessionID }, query: MessagesQuery, - success: Schema.Array(MessageV2.WithParts), + success: described(Schema.Array(MessageV2.WithParts), "List of messages"), error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ @@ -184,8 +178,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.get("message", SessionPaths.message, { params: { sessionID: SessionID, messageID: MessageID }, - success: MessageV2.WithParts, - error: HttpApiError.NotFound, + success: described(MessageV2.WithParts, "Message"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.message", @@ -195,7 +189,7 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.post("create", SessionPaths.create, { payload: [HttpApiSchema.NoContent, Session.CreateInput], - success: Session.Info, + success: described(Session.Info, "Successfully created session"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -206,7 +200,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.delete("remove", SessionPaths.remove, { params: { sessionID: SessionID }, - success: Schema.Boolean, + success: described(Schema.Boolean, "Successfully deleted session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.delete", @@ -217,7 +212,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.patch("update", SessionPaths.update, { params: { sessionID: SessionID }, payload: UpdatePayload, - success: Session.Info, + success: described(Session.Info, "Successfully updated session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.update", @@ -228,7 +224,7 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("fork", SessionPaths.fork, { params: { sessionID: SessionID }, payload: ForkPayload, - success: Session.Info, + success: described(Session.Info, "200"), }).annotateMerge( OpenApi.annotations({ identifier: "session.fork", @@ -238,7 +234,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.post("abort", SessionPaths.abort, { params: { sessionID: SessionID }, - success: Schema.Boolean, + success: described(Schema.Boolean, "Aborted session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.abort", @@ -249,7 +246,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("init", SessionPaths.init, { params: { sessionID: SessionID }, payload: InitPayload, - success: Schema.Boolean, + success: described(Schema.Boolean, "200"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.init", @@ -260,7 +258,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.post("share", SessionPaths.share, { params: { sessionID: SessionID }, - success: Session.Info, + success: described(Session.Info, "Successfully shared session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.share", @@ -270,7 +269,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.delete("unshare", SessionPaths.share, { params: { sessionID: SessionID }, - success: Session.Info, + success: described(Session.Info, "Successfully unshared session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.unshare", @@ -281,7 +281,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("summarize", SessionPaths.summarize, { params: { sessionID: SessionID }, payload: SummarizePayload, - success: Schema.Boolean, + success: described(Schema.Boolean, "Summarized session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.summarize", @@ -292,7 +293,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("prompt", SessionPaths.prompt, { params: { sessionID: SessionID }, payload: PromptPayload, - success: MessageV2.WithParts, + success: described(MessageV2.WithParts, "Created message"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.prompt", @@ -303,7 +305,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, { params: { sessionID: SessionID }, payload: PromptPayload, - success: HttpApiSchema.NoContent, + success: described(HttpApiSchema.NoContent, "Prompt accepted"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.prompt_async", @@ -315,7 +318,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("command", SessionPaths.command, { params: { sessionID: SessionID }, payload: CommandPayload, - success: MessageV2.WithParts, + success: described(MessageV2.WithParts, "Created message"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.command", @@ -326,7 +330,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("shell", SessionPaths.shell, { params: { sessionID: SessionID }, payload: ShellPayload, - success: MessageV2.WithParts, + success: described(MessageV2.WithParts, "Created message"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.shell", @@ -337,7 +342,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("revert", SessionPaths.revert, { params: { sessionID: SessionID }, payload: RevertPayload, - success: Session.Info, + success: described(Session.Info, "Updated session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.revert", @@ -348,7 +354,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, { params: { sessionID: SessionID }, - success: Session.Info, + success: described(Session.Info, "Updated session"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.unrevert", @@ -359,7 +366,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, { params: { sessionID: SessionID, permissionID: PermissionID }, payload: PermissionResponsePayload, - success: Schema.Boolean, + success: described(Schema.Boolean, "Permission processed successfully"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "permission.respond", @@ -370,7 +378,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, { params: { sessionID: SessionID, messageID: MessageID }, - success: Schema.Boolean, + success: described(Schema.Boolean, "Successfully deleted message"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "session.deleteMessage", @@ -381,7 +390,8 @@ export const SessionApi = HttpApi.make("session") ), HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, { params: { sessionID: SessionID, messageID: MessageID, partID: PartID }, - success: Schema.Boolean, + success: described(Schema.Boolean, "Successfully deleted part"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "part.delete", @@ -391,7 +401,8 @@ export const SessionApi = HttpApi.make("session") HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, { params: { sessionID: SessionID, messageID: MessageID, partID: PartID }, payload: MessageV2.Part, - success: MessageV2.Part, + success: described(MessageV2.Part, "Successfully updated part"), + error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ identifier: "part.update", diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts index f5aca158dd..1d9b08d9cb 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/sync.ts @@ -3,6 +3,7 @@ import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/sync" export const ReplayEvent = Schema.Struct({ @@ -11,14 +12,14 @@ export const ReplayEvent = Schema.Struct({ seq: NonNegativeInt, type: Schema.String, data: Schema.Record(Schema.String, Schema.Unknown), -}).annotate({ identifier: "SyncReplayEvent" }) +}) export const ReplayPayload = Schema.Struct({ directory: Schema.String, events: Schema.NonEmptyArray(ReplayEvent), -}).annotate({ identifier: "SyncReplayInput" }) +}) export const ReplayResponse = Schema.Struct({ sessionID: Schema.String, -}).annotate({ identifier: "SyncReplayResponse" }) +}) export const HistoryPayload = Schema.Record(Schema.String, NonNegativeInt) export const HistoryEvent = Schema.Struct({ id: Schema.String, @@ -26,7 +27,7 @@ export const HistoryEvent = Schema.Struct({ seq: NonNegativeInt, type: Schema.String, data: Schema.Record(Schema.String, Schema.Unknown), -}).annotate({ identifier: "SyncHistoryEvent" }) +}) export const SyncPaths = { start: `${root}/start`, @@ -39,7 +40,7 @@ export const SyncApi = HttpApi.make("sync") HttpApiGroup.make("sync") .add( HttpApiEndpoint.post("start", SyncPaths.start, { - success: Schema.Boolean, + success: described(Schema.Boolean, "Workspace sync started"), }).annotateMerge( OpenApi.annotations({ identifier: "sync.start", @@ -49,7 +50,7 @@ export const SyncApi = HttpApi.make("sync") ), HttpApiEndpoint.post("replay", SyncPaths.replay, { payload: ReplayPayload, - success: ReplayResponse, + success: described(ReplayResponse, "Replayed sync events"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -60,7 +61,7 @@ export const SyncApi = HttpApi.make("sync") ), HttpApiEndpoint.post("history", SyncPaths.history, { payload: HistoryPayload, - success: Schema.Array(HistoryEvent), + success: described(Schema.Array(HistoryEvent), "Sync events"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -87,4 +88,3 @@ export const SyncApi = HttpApi.make("sync") description: "Experimental HttpApi surface for selected instance routes.", }), ) - diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts index 7805fd47b2..a5d31bfa62 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/tui.ts @@ -3,18 +3,19 @@ import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/tui" -export const CommandPayload = Schema.Struct({ command: Schema.String }).annotate({ identifier: "TuiCommandInput" }) +export const CommandPayload = Schema.Struct({ command: Schema.String }) export const TuiRequestPayload = Schema.Struct({ path: Schema.String, body: Schema.Unknown, -}).annotate({ identifier: "TuiRequest" }) +}) const EventTuiPromptAppend = Schema.Struct({ type: Schema.Literal(TuiEvent.PromptAppend.type), properties: TuiEvent.PromptAppend.properties }).annotate({ identifier: "EventTuiPromptAppend" }) const EventTuiCommandExecute = Schema.Struct({ type: Schema.Literal(TuiEvent.CommandExecute.type), properties: TuiEvent.CommandExecute.properties }).annotate({ identifier: "EventTuiCommandExecute" }) const EventTuiToastShow = Schema.Struct({ type: Schema.Literal(TuiEvent.ToastShow.type), properties: TuiEvent.ToastShow.properties }).annotate({ identifier: "EventTuiToastShow" }) const EventTuiSessionSelect = Schema.Struct({ type: Schema.Literal(TuiEvent.SessionSelect.type), properties: TuiEvent.SessionSelect.properties }).annotate({ identifier: "EventTuiSessionSelect" }) -export const TuiPublishPayload = Schema.Union([EventTuiPromptAppend, EventTuiCommandExecute, EventTuiToastShow, EventTuiSessionSelect]).annotate({ identifier: "TuiEventInput" }) +export const TuiPublishPayload = Schema.Union([EventTuiPromptAppend, EventTuiCommandExecute, EventTuiToastShow, EventTuiSessionSelect]) export const TuiPaths = { appendPrompt: `${root}/append-prompt`, @@ -38,7 +39,7 @@ export const TuiApi = HttpApi.make("tui") .add( HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, { payload: TuiEvent.PromptAppend.properties, - success: Schema.Boolean, + success: described(Schema.Boolean, "Prompt processed successfully"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -47,42 +48,42 @@ export const TuiApi = HttpApi.make("tui") description: "Append prompt to the TUI.", }), ), - HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, { success: Schema.Boolean }).annotateMerge( + HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, { success: described(Schema.Boolean, "Help dialog opened successfully") }).annotateMerge( OpenApi.annotations({ identifier: "tui.openHelp", summary: "Open help dialog", description: "Open the help dialog in the TUI to display user assistance information.", }), ), - HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, { success: Schema.Boolean }).annotateMerge( + HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, { success: described(Schema.Boolean, "Session dialog opened successfully") }).annotateMerge( OpenApi.annotations({ identifier: "tui.openSessions", summary: "Open sessions dialog", description: "Open the session dialog.", }), ), - HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, { success: Schema.Boolean }).annotateMerge( + HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, { success: described(Schema.Boolean, "Theme dialog opened successfully") }).annotateMerge( OpenApi.annotations({ identifier: "tui.openThemes", summary: "Open themes dialog", description: "Open the theme dialog.", }), ), - HttpApiEndpoint.post("openModels", TuiPaths.openModels, { success: Schema.Boolean }).annotateMerge( + HttpApiEndpoint.post("openModels", TuiPaths.openModels, { success: described(Schema.Boolean, "Model dialog opened successfully") }).annotateMerge( OpenApi.annotations({ identifier: "tui.openModels", summary: "Open models dialog", description: "Open the model dialog.", }), ), - HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, { success: Schema.Boolean }).annotateMerge( + HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, { success: described(Schema.Boolean, "Prompt submitted successfully") }).annotateMerge( OpenApi.annotations({ identifier: "tui.submitPrompt", summary: "Submit TUI prompt", description: "Submit the prompt.", }), ), - HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, { success: Schema.Boolean }).annotateMerge( + HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, { success: described(Schema.Boolean, "Prompt cleared successfully") }).annotateMerge( OpenApi.annotations({ identifier: "tui.clearPrompt", summary: "Clear TUI prompt", @@ -91,7 +92,7 @@ export const TuiApi = HttpApi.make("tui") ), HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, { payload: CommandPayload, - success: Schema.Boolean, + success: described(Schema.Boolean, "Command executed successfully"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -102,7 +103,7 @@ export const TuiApi = HttpApi.make("tui") ), HttpApiEndpoint.post("showToast", TuiPaths.showToast, { payload: TuiEvent.ToastShow.properties, - success: Schema.Boolean, + success: described(Schema.Boolean, "Toast notification shown successfully"), }).annotateMerge( OpenApi.annotations({ identifier: "tui.showToast", @@ -112,7 +113,7 @@ export const TuiApi = HttpApi.make("tui") ), HttpApiEndpoint.post("publish", TuiPaths.publish, { payload: TuiPublishPayload, - success: Schema.Boolean, + success: described(Schema.Boolean, "Event published successfully"), error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ @@ -123,7 +124,7 @@ export const TuiApi = HttpApi.make("tui") ), HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, { payload: TuiEvent.SessionSelect.properties, - success: Schema.Boolean, + success: described(Schema.Boolean, "Session selected successfully"), error: [HttpApiError.BadRequest, HttpApiError.NotFound], }).annotateMerge( OpenApi.annotations({ @@ -132,7 +133,7 @@ export const TuiApi = HttpApi.make("tui") description: "Navigate the TUI to display the specified session.", }), ), - HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, { success: TuiRequestPayload }).annotateMerge( + HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, { success: described(TuiRequestPayload, "Next TUI request") }).annotateMerge( OpenApi.annotations({ identifier: "tui.control.next", summary: "Get next TUI request", @@ -141,7 +142,7 @@ export const TuiApi = HttpApi.make("tui") ), HttpApiEndpoint.post("controlResponse", TuiPaths.controlResponse, { payload: Schema.Unknown, - success: Schema.Boolean, + success: described(Schema.Boolean, "Response submitted successfully"), }).annotateMerge( OpenApi.annotations({ identifier: "tui.control.response", @@ -161,4 +162,3 @@ export const TuiApi = HttpApi.make("tui") description: "Experimental HttpApi surface for selected instance routes.", }), ) - diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts index 298e455984..0305c65365 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/workspace.ts @@ -2,22 +2,19 @@ import { Workspace } from "@/control-plane/workspace" import { WorkspaceAdaptorEntry } from "@/control-plane/types" import { NonNegativeInt } from "@/util/schema" import { Schema, Struct } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../auth" import { InstanceContextMiddleware } from "../instance-context" +import { described } from "./metadata" const root = "/experimental/workspace" -export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"])).annotate({ - identifier: "WorkspaceCreateInput", -}) +export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"])) export const SessionRestorePayload = Schema.Struct( Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]), -).annotate({ - identifier: "WorkspaceSessionRestoreInput", -}) +) export const SessionRestoreResponse = Schema.Struct({ total: NonNegativeInt, -}).annotate({ identifier: "WorkspaceSessionRestoreResponse" }) +}) export const WorkspacePaths = { adaptors: `${root}/adaptor`, @@ -32,7 +29,7 @@ export const WorkspaceApi = HttpApi.make("workspace") HttpApiGroup.make("workspace") .add( HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, { - success: Schema.Array(WorkspaceAdaptorEntry), + success: described(Schema.Array(WorkspaceAdaptorEntry), "Workspace adaptors"), }).annotateMerge( OpenApi.annotations({ identifier: "experimental.workspace.adaptor.list", @@ -40,7 +37,9 @@ export const WorkspaceApi = HttpApi.make("workspace") description: "List all available workspace adaptors for the current project.", }), ), - HttpApiEndpoint.get("list", WorkspacePaths.list, { success: Schema.Array(Workspace.Info) }).annotateMerge( + HttpApiEndpoint.get("list", WorkspacePaths.list, { + success: described(Schema.Array(Workspace.Info), "Workspaces"), + }).annotateMerge( OpenApi.annotations({ identifier: "experimental.workspace.list", summary: "List workspaces", @@ -49,7 +48,8 @@ export const WorkspaceApi = HttpApi.make("workspace") ), HttpApiEndpoint.post("create", WorkspacePaths.list, { payload: CreatePayload, - success: Workspace.Info, + success: described(Workspace.Info, "Workspace created"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "experimental.workspace.create", @@ -58,7 +58,7 @@ export const WorkspaceApi = HttpApi.make("workspace") }), ), HttpApiEndpoint.get("status", WorkspacePaths.status, { - success: Schema.Array(Workspace.ConnectionStatus), + success: described(Schema.Array(Workspace.ConnectionStatus), "Workspace status"), }).annotateMerge( OpenApi.annotations({ identifier: "experimental.workspace.status", @@ -68,7 +68,8 @@ export const WorkspaceApi = HttpApi.make("workspace") ), HttpApiEndpoint.delete("remove", WorkspacePaths.remove, { params: { id: Workspace.Info.fields.id }, - success: Schema.UndefinedOr(Workspace.Info), + success: described(Schema.UndefinedOr(Workspace.Info), "Workspace removed"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "experimental.workspace.remove", @@ -79,7 +80,8 @@ export const WorkspaceApi = HttpApi.make("workspace") HttpApiEndpoint.post("sessionRestore", WorkspacePaths.sessionRestore, { params: { id: Workspace.Info.fields.id }, payload: SessionRestorePayload, - success: SessionRestoreResponse, + success: described(SessionRestoreResponse, "Session replay started"), + error: HttpApiError.BadRequest, }).annotateMerge( OpenApi.annotations({ identifier: "experimental.workspace.sessionRestore", diff --git a/packages/opencode/src/server/routes/instance/httpapi/public.ts b/packages/opencode/src/server/routes/instance/httpapi/public.ts index 64d5f16a62..d9871c69be 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/public.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/public.ts @@ -33,6 +33,7 @@ type OpenApiSchema = { additionalProperties?: OpenApiSchema | boolean allOf?: OpenApiSchema[] anyOf?: OpenApiSchema[] + description?: string enum?: Array items?: OpenApiSchema maximum?: number @@ -76,185 +77,11 @@ const QueryParameterSchemas = { "GET /session/{sessionID}/message limit": { type: "integer", minimum: 0, maximum: Number.MAX_SAFE_INTEGER }, } satisfies Record -// Mapping of "METHOD /path" to the correct 200 response description from Hono spec -const ResponseDescriptions = { - "GET /global/health": "Health information", - "GET /global/config": "Get global config info", - "PATCH /global/config": "Successfully updated global config", - "POST /global/dispose": "Global disposed", - "POST /global/upgrade": "Upgrade result", - "PUT /auth/{providerID}": "Successfully set authentication credentials", - "DELETE /auth/{providerID}": "Successfully removed authentication credentials", - "POST /log": "Log entry written successfully", - "GET /experimental/workspace/adaptor": "Workspace adaptors", - "POST /experimental/workspace": "Workspace created", - "GET /experimental/workspace": "Workspaces", - "GET /experimental/workspace/status": "Workspace status", - "DELETE /experimental/workspace/{id}": "Workspace removed", - "POST /experimental/workspace/{id}/session-restore": "Session replay started", - "GET /project": "List of projects", - "GET /project/current": "Current project information", - "POST /project/git/init": "Project information after git initialization", - "PATCH /project/{projectID}": "Updated project information", - "GET /pty/shells": "List of shells", - "GET /pty": "List of sessions", - "POST /pty": "Created session", - "GET /pty/{ptyID}": "Session info", - "PUT /pty/{ptyID}": "Updated session", - "DELETE /pty/{ptyID}": "Session removed", - "GET /pty/{ptyID}/connect": "Connected session", - "GET /config": "Get config info", - "PATCH /config": "Successfully updated config", - "GET /config/providers": "List of providers", - "GET /experimental/console": "Active Console provider metadata", - "GET /experimental/console/orgs": "Switchable Console orgs", - "POST /experimental/console/switch": "Switch success", - "GET /experimental/tool/ids": "Tool IDs", - "GET /experimental/tool": "Tools", - "POST /experimental/worktree": "Worktree created", - "GET /experimental/worktree": "List of worktree directories", - "DELETE /experimental/worktree": "Worktree removed", - "POST /experimental/worktree/reset": "Worktree reset", - "GET /experimental/session": "List of sessions", - "GET /experimental/resource": "MCP resources", - "GET /session": "List of sessions", - "POST /session": "Successfully created session", - "GET /session/status": "Get session status", - "GET /session/{sessionID}": "Get session", - "DELETE /session/{sessionID}": "Successfully deleted session", - "PATCH /session/{sessionID}": "Successfully updated session", - "GET /session/{sessionID}/children": "List of children", - "GET /session/{sessionID}/todo": "Todo list", - "POST /session/{sessionID}/init": "200", - "POST /session/{sessionID}/fork": "200", - "POST /session/{sessionID}/abort": "Aborted session", - "POST /session/{sessionID}/share": "Successfully shared session", - "DELETE /session/{sessionID}/share": "Successfully unshared session", - "GET /session/{sessionID}/diff": "Successfully retrieved diff", - "POST /session/{sessionID}/summarize": "Summarized session", - "GET /session/{sessionID}/message": "List of messages", - "POST /session/{sessionID}/message": "Created message", - "GET /session/{sessionID}/message/{messageID}": "Message", - "DELETE /session/{sessionID}/message/{messageID}": "Successfully deleted message", - "DELETE /session/{sessionID}/message/{messageID}/part/{partID}": "Successfully deleted part", - "PATCH /session/{sessionID}/message/{messageID}/part/{partID}": "Successfully updated part", - "POST /session/{sessionID}/command": "Created message", - "POST /session/{sessionID}/shell": "Created message", - "POST /session/{sessionID}/revert": "Updated session", - "POST /session/{sessionID}/unrevert": "Updated session", - "POST /session/{sessionID}/permissions/{permissionID}": "Permission processed successfully", - "POST /permission/{requestID}/reply": "Permission processed successfully", - "GET /permission": "List of pending permissions", - "GET /question": "List of pending questions", - "POST /question/{requestID}/reply": "Question answered successfully", - "POST /question/{requestID}/reject": "Question rejected successfully", - "GET /provider": "List of providers", - "GET /provider/auth": "Provider auth methods", - "POST /provider/{providerID}/oauth/authorize": "Authorization URL and method", - "POST /provider/{providerID}/oauth/callback": "OAuth callback processed successfully", - "POST /sync/start": "Workspace sync started", - "POST /sync/replay": "Replayed sync events", - "POST /sync/history": "Sync events", - "GET /find": "Matches", - "GET /find/file": "File paths", - "GET /find/symbol": "Symbols", - "GET /file": "Files and directories", - "GET /file/content": "File content", - "GET /file/status": "File status", - "GET /mcp": "MCP server status", - "POST /mcp": "MCP server added successfully", - "POST /mcp/{name}/auth": "OAuth flow started", - "DELETE /mcp/{name}/auth": "OAuth credentials removed", - "POST /mcp/{name}/auth/callback": "OAuth authentication completed", - "POST /mcp/{name}/auth/authenticate": "OAuth authentication completed", - "POST /mcp/{name}/connect": "MCP server connected successfully", - "POST /mcp/{name}/disconnect": "MCP server disconnected successfully", - "POST /tui/append-prompt": "Prompt processed successfully", - "POST /tui/open-help": "Help dialog opened successfully", - "POST /tui/open-sessions": "Session dialog opened successfully", - "POST /tui/open-themes": "Theme dialog opened successfully", - "POST /tui/open-models": "Model dialog opened successfully", - "POST /tui/submit-prompt": "Prompt submitted successfully", - "POST /tui/clear-prompt": "Prompt cleared successfully", - "POST /tui/execute-command": "Command executed successfully", - "POST /tui/show-toast": "Toast notification shown successfully", - "POST /tui/publish": "Event published successfully", - "POST /tui/select-session": "Session selected successfully", - "GET /tui/control/next": "Next TUI request", - "POST /tui/control/response": "Response submitted successfully", - "POST /instance/dispose": "Instance disposed", - "GET /vcs": "VCS info", - "GET /vcs/diff": "VCS diff", - "GET /command": "List of commands", - "GET /agent": "List of agents", - "GET /skill": "List of skills", - "GET /lsp": "LSP server status", - "GET /formatter": "Formatter status", -} as const satisfies Record - -const LegacyErrorResponses = { - "PUT /auth/{providerID}": [400], - "DELETE /auth/{providerID}": [400], - "POST /log": [400], - "PATCH /global/config": [400], - "POST /global/upgrade": [400], - "POST /experimental/workspace": [400], - "POST /experimental/console/switch": [400], - "DELETE /experimental/workspace/{id}": [400], - "POST /experimental/workspace/{id}/session-restore": [400], - "GET /experimental/tool/ids": [400], - "GET /experimental/tool": [400], - "POST /experimental/worktree": [400], - "DELETE /experimental/worktree": [400], - "POST /experimental/worktree/reset": [400], - "PATCH /config": [400], - "PATCH /project/{projectID}": [400, 404], - "POST /pty": [400], - "GET /pty/{ptyID}": [404], - "PUT /pty/{ptyID}": [400], - "DELETE /pty/{ptyID}": [404], - "GET /pty/{ptyID}/connect": [404], - "POST /session": [400], - "GET /session/status": [400], - "GET /session/{sessionID}": [400, 404], - "DELETE /session/{sessionID}": [400, 404], - "PATCH /session/{sessionID}": [400, 404], - "GET /session/{sessionID}/children": [400, 404], - "GET /session/{sessionID}/todo": [400, 404], - "POST /session/{sessionID}/init": [400, 404], - "POST /session/{sessionID}/abort": [400, 404], - "POST /session/{sessionID}/share": [400, 404], - "DELETE /session/{sessionID}/share": [400, 404], - "POST /session/{sessionID}/summarize": [400, 404], - "GET /session/{sessionID}/message": [400, 404], - "POST /session/{sessionID}/message": [400, 404], - "GET /session/{sessionID}/message/{messageID}": [400, 404], - "DELETE /session/{sessionID}/message/{messageID}": [400, 404], - "DELETE /session/{sessionID}/message/{messageID}/part/{partID}": [400, 404], - "PATCH /session/{sessionID}/message/{messageID}/part/{partID}": [400, 404], - "POST /session/{sessionID}/prompt_async": [400, 404], - "POST /session/{sessionID}/command": [400, 404], - "POST /session/{sessionID}/shell": [400, 404], - "POST /session/{sessionID}/revert": [400, 404], - "POST /session/{sessionID}/unrevert": [400, 404], - "POST /session/{sessionID}/permissions/{permissionID}": [400, 404], - "POST /permission/{requestID}/reply": [400, 404], - "POST /question/{requestID}/reply": [400, 404], - "POST /question/{requestID}/reject": [400, 404], - "POST /provider/{providerID}/oauth/authorize": [400], - "POST /provider/{providerID}/oauth/callback": [400], - "POST /sync/replay": [400], - "POST /sync/history": [400], - "POST /mcp": [400], - "POST /mcp/{name}/auth": [404], - "POST /mcp/{name}/auth/callback": [400, 404], - "POST /mcp/{name}/auth/authenticate": [404], - "DELETE /mcp/{name}/auth": [404], - "POST /tui/append-prompt": [400], - "POST /tui/execute-command": [400], - "POST /tui/publish": [400], - "POST /tui/select-session": [400, 404], -} as const satisfies Record> +const LegacyComponentDescriptions = { + LogLevel: "Log level", + ServerConfig: "Server configuration for opencode serve and web commands", + LayoutConfig: "@deprecated Always uses stretch layout.", +} satisfies Record function matchLegacyOpenApi(input: Record) { const spec = input as OpenApiSpec @@ -272,10 +99,16 @@ function matchLegacyOpenApi(input: Record) { for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) { spec.components!.schemas![name] = stripOptionalNull(structuredClone(schema)) } + normalizeComponentNames(spec) + collapseDuplicateComponents(spec) + applyLegacySchemaOverrides(spec) + normalizeComponentDescriptions(spec) addLegacyErrorSchemas(spec) delete spec.components?.schemas?.Unauthorized delete spec.components?.schemas?.EffectHttpApiErrorBadRequest delete spec.components?.schemas?.EffectHttpApiErrorNotFound + delete spec.components?.schemas?.effect_HttpApiError_BadRequest + delete spec.components?.schemas?.effect_HttpApiError_NotFound delete spec.components?.securitySchemes for (const [path, item] of Object.entries(spec.paths ?? {})) { @@ -287,6 +120,8 @@ function matchLegacyOpenApi(input: Record) { // Hono's generated OpenAPI never marked request bodies as required. Keep // that SDK surface stable during the HttpApi migration. delete operation.requestBody.required + const body = operation.requestBody.content?.["application/json"] + if (body?.schema) body.schema = stripOptionalNull(structuredClone(body.schema)) if (path === "/experimental/workspace" && method === "post") { // Workspace creation fields `branch` and `extra` are Schema.NullOr — // genuinely nullable, not just optional. Re-add the null that the @@ -297,11 +132,17 @@ function matchLegacyOpenApi(input: Record) { if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] } } } + for (const response of Object.values(operation.responses ?? {})) { + for (const content of Object.values(response.content ?? {})) { + if (content.schema) content.schema = stripOptionalNull(structuredClone(content.schema)) + } + } // Hono applied auth as runtime middleware outside OpenAPI metadata, so the // legacy SDK did not expose auth schemes or generated 401 error unions. delete operation.security delete operation.responses?.["401"] normalizeLegacyErrorResponses(operation) + normalizeLegacyOperation(operation, path, method) if ((path === "/event" || path === "/global/event") && method === "get") { // HttpApi has no first-class SSE response schema, and these handlers are // raw/streaming routes. Document the actual wire protocol explicitly. @@ -314,15 +155,6 @@ function matchLegacyOpenApi(input: Record) { }, } } - // Apply response descriptions from the Hono spec to match legacy behavior - const routeKey = `${method.toUpperCase()} ${path}` as keyof typeof ResponseDescriptions - const responseDescription = ResponseDescriptions[routeKey] - applyLegacyErrorResponses(operation, routeKey) - inlineLegacyResponseSchemas(operation, routeKey) - if (responseDescription && operation.responses?.["200"]) { - const response200 = operation.responses["200"] as { description?: string } - response200.description = responseDescription - } if (!isInstanceRoute) continue operation.parameters = [ ...InstanceQueryParameters, @@ -369,57 +201,156 @@ function addLegacyErrorSchemas(spec: OpenApiSpec) { } } +function collapseDuplicateComponents(spec: OpenApiSpec) { + const schemas = spec.components?.schemas + if (!schemas) return + for (const name of Object.keys(schemas)) { + const base = name.replace(/\d+$/, "") + if (base === name || !schemas[base]) continue + if (stableSchema(schemas[name], schemas) !== stableSchema(schemas[base], schemas)) continue + rewriteRefs(spec, name, base) + delete schemas[name] + } +} + +function normalizeComponentNames(spec: OpenApiSpec) { + const schemas = spec.components?.schemas + if (!schemas) return + for (const name of Object.keys(schemas)) { + const next = componentTypeName(name) + if (next === name) continue + if (schemas[next]) { + if (stableSchema(schemas[name], schemas) === stableSchema(schemas[next], schemas)) { + rewriteRefs(spec, name, next) + delete schemas[name] + } + continue + } + schemas[next] = schemas[name] + rewriteRefs(spec, name, next) + delete schemas[name] + } +} + +function componentTypeName(name: string) { + if (!name.includes(".")) return name + return name + .split(".") + .filter((part) => !/^\d+$/.test(part)) + .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)) + .join("") +} + +function applyLegacySchemaOverrides(spec: OpenApiSpec) { + const schemas = spec.components?.schemas + if (!schemas) return + if (schemas.AgentConfig) schemas.AgentConfig.additionalProperties = {} + if (schemas.Command?.properties?.template) schemas.Command.properties.template = { type: "string" } + if (schemas.Workspace?.properties) { + schemas.Workspace.properties.branch = nullable(schemas.Workspace.properties.branch) + schemas.Workspace.properties.directory = nullable(schemas.Workspace.properties.directory) + schemas.Workspace.properties.extra = nullable(schemas.Workspace.properties.extra) + } + if (schemas.GlobalSession?.properties?.project) schemas.GlobalSession.properties.project = nullable(schemas.GlobalSession.properties.project) + const providerOptions = schemas.ProviderConfig?.properties?.options + if (providerOptions) providerOptions.additionalProperties = {} + const model = schemas.ProviderConfig?.properties?.models?.additionalProperties + const variants = typeof model === "object" ? model.properties?.variants?.additionalProperties : undefined + if (variants && typeof variants === "object") variants.additionalProperties = {} + const syncInfo = schemas.SyncEventSessionUpdated?.properties?.data?.properties?.info + if (syncInfo?.properties) makePropertiesNullable(syncInfo.properties) +} + +function normalizeComponentDescriptions(spec: OpenApiSpec) { + for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) { + const description = LegacyComponentDescriptions[name as keyof typeof LegacyComponentDescriptions] + if (description) { + schema.description = description + continue + } + delete schema.description + } +} + +function makePropertiesNullable(properties: Record) { + for (const [key, value] of Object.entries(properties)) { + if (key === "share" && value.properties?.url) { + value.properties.url = nullable(value.properties.url) + continue + } + if (key === "time" && value.properties) { + makePropertiesNullable(value.properties) + continue + } + properties[key] = nullable(value) + } +} + +function nullable(schema: OpenApiSchema): OpenApiSchema { + if (flattenOptions(schema.anyOf ?? schema.oneOf)?.some((item) => item.type === "null")) return schema + return { anyOf: [schema, { type: "null" }] } +} + +function stableSchema(input: unknown, schemas: Record): string { + return JSON.stringify(canonicalizeSchema(input, schemas)) +} + +function canonicalizeSchema(input: unknown, schemas: Record): unknown { + if (Array.isArray(input)) return input.map((item) => canonicalizeSchema(item, schemas)) + if (!input || typeof input !== "object") return input + const schema = input as OpenApiSchema + if (schema.$ref) return { $ref: canonicalRef(schema.$ref, schemas) } + return Object.fromEntries( + Object.entries(input) + .filter(([key]) => key !== "description") + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => [key, canonicalizeSchema(value, schemas)]), + ) +} + +function canonicalRef(ref: string, schemas: Record) { + const name = ref.replace("#/components/schemas/", "") + const base = name.replace(/\d+$/, "") + if (base !== name && schemas[base]) return `#/components/schemas/${base}` + return ref +} + +function rewriteRefs(input: unknown, from: string, to: string): void { + if (Array.isArray(input)) { + for (const item of input) rewriteRefs(item, from, to) + return + } + if (!input || typeof input !== "object") return + const schema = input as OpenApiSchema + if (schema.$ref === `#/components/schemas/${from}`) schema.$ref = `#/components/schemas/${to}` + for (const value of Object.values(input)) rewriteRefs(value, from, to) +} + function normalizeLegacyErrorResponses(operation: OpenApiOperation) { - if (operation.responses?.["400"] && isRefResponse(operation.responses["400"], "EffectHttpApiErrorBadRequest")) { + if (operation.responses?.["400"] && isBuiltInErrorResponse(operation.responses["400"], "BadRequest")) { operation.responses["400"] = legacyErrorResponse("Bad request", "BadRequestError") } - if (operation.responses?.["404"] && isRefResponse(operation.responses["404"], "EffectHttpApiErrorNotFound")) { + if (operation.responses?.["404"] && isBuiltInErrorResponse(operation.responses["404"], "NotFound")) { operation.responses["404"] = legacyErrorResponse("Not found", "NotFoundError") } } -function applyLegacyErrorResponses(operation: OpenApiOperation, route: string) { - const responses: ReadonlyArray<400 | 404> | undefined = LegacyErrorResponses[route as keyof typeof LegacyErrorResponses] - if (!responses) return - operation.responses ??= {} - if (responses.includes(400)) operation.responses["400"] = legacyErrorResponse("Bad request", "BadRequestError") - else if (operation.responses["400"] && isErrorResponse(operation.responses["400"], "BadRequest")) delete operation.responses["400"] - if (responses.includes(404)) operation.responses["404"] = legacyErrorResponse("Not found", "NotFoundError") - else if (operation.responses["404"] && isErrorResponse(operation.responses["404"], "NotFound")) delete operation.responses["404"] -} - -function inlineLegacyResponseSchemas(operation: OpenApiOperation, route: string) { - const response = operation.responses?.["200"] - const schema = response?.content?.["application/json"]?.schema - if (!schema) return - if (route === "POST /mcp/{name}/auth") { - response.content!["application/json"]!.schema = { - type: "object", - required: ["authorizationUrl"], - properties: { - authorizationUrl: { type: "string" }, +function normalizeLegacyOperation(operation: OpenApiOperation, path: string, method: string) { + if (path === "/experimental/console/switch" && method === "post") delete operation.responses?.["400"] + if (path === "/pty/{ptyID}" && method === "put") delete operation.responses?.["404"] + if ((path !== "/session/{sessionID}/message" && path !== "/session/{sessionID}/command") || method !== "post") return + const response = operation.responses?.["200"]?.content?.["application/json"] + if (!response) return + response.schema = { + type: "object", + required: ["info", "parts"], + properties: { + info: { $ref: "#/components/schemas/AssistantMessage" }, + parts: { + type: "array", + items: { $ref: "#/components/schemas/Part" }, }, - } - return - } - if (route === "DELETE /mcp/{name}/auth") { - response.content!["application/json"]!.schema = { - type: "object", - required: ["success"], - properties: { - success: { type: "boolean", enum: [true] }, - }, - } - return - } - if (route === "POST /sync/replay") { - response.content!["application/json"]!.schema = { - type: "object", - required: ["sessionID"], - properties: { - sessionID: { type: "string" }, - }, - } + }, } } @@ -427,8 +358,8 @@ function isRefResponse(response: OpenApiResponse, name: string) { return response.content?.["application/json"]?.schema?.$ref === `#/components/schemas/${name}` } -function isErrorResponse(response: OpenApiResponse, name: "BadRequest" | "NotFound") { - return isRefResponse(response, `EffectHttpApiError${name}`) || isRefResponse(response, `${name}Error`) +function isBuiltInErrorResponse(response: OpenApiResponse, name: "BadRequest" | "NotFound") { + return response.description === name || isRefResponse(response, `EffectHttpApiError${name}`) } function legacyErrorResponse(description: string, name: "BadRequestError" | "NotFoundError"): OpenApiResponse { @@ -488,6 +419,7 @@ function fixSelfReferencingComponents(spec: OpenApiSpec) { /** Strip `{type:"null"}` arms that Effect's `Schema.optional` adds to OpenAPI unions. */ function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema { + if (isEmptyObjectUnion(schema)) return { type: "object", properties: {} } const options = flattenOptions(schema.anyOf ?? schema.oneOf) if (options) { const withoutNull = options.filter((item) => item.type !== "null") @@ -496,8 +428,13 @@ function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema { if (schema.oneOf) schema.oneOf = withoutNull.map(stripOptionalNull) } if (schema.allOf) { - if (schema.type) delete schema.allOf - else schema.allOf = schema.allOf.map(stripOptionalNull) + const allOf = schema.allOf.map(stripOptionalNull) + if (schema.type) { + delete schema.allOf + for (const item of allOf) Object.assign(schema, item) + } else { + schema.allOf = allOf + } } if (schema.prefixItems && schema.items) delete schema.prefixItems if (schema.items) schema.items = stripOptionalNull(schema.items) @@ -512,6 +449,19 @@ function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema { return schema } +function isEmptyObjectUnion(schema: OpenApiSchema) { + const options = schema.anyOf ?? schema.oneOf + return options?.length === 2 && options.some(isBareObjectSchema) && options.some(isBareArraySchema) +} + +function isBareObjectSchema(schema: OpenApiSchema) { + return schema.type === "object" && !schema.properties && !schema.additionalProperties +} + +function isBareArraySchema(schema: OpenApiSchema) { + return schema.type === "array" && !schema.items && !schema.prefixItems +} + function flattenOptions(options: OpenApiSchema[] | undefined): OpenApiSchema[] | undefined { return options?.flatMap((item) => flattenOptions(item.anyOf ?? item.oneOf) ?? [item]) }