fix(httpapi): preserve SDK schema parity

This commit is contained in:
Kit Langton
2026-04-28 22:56:18 -04:00
parent ca2f54a68d
commit 5b1feb4c54
18 changed files with 445 additions and 411 deletions
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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<UnsupportedOAuthError>("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",
@@ -0,0 +1,18 @@
import { Schema } from "effect"
import { OpenApi } from "effect/unstable/httpapi"
export function described<S extends Schema.Top>(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
},
})
}
@@ -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",
@@ -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",
@@ -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({
@@ -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",
@@ -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",
@@ -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",
@@ -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.",
}),
)
@@ -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.",
}),
)
@@ -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",
@@ -33,6 +33,7 @@ type OpenApiSchema = {
additionalProperties?: OpenApiSchema | boolean
allOf?: OpenApiSchema[]
anyOf?: OpenApiSchema[]
description?: string
enum?: Array<string | boolean>
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<string, OpenApiSchema>
// 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<string, string>
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<string, ReadonlyArray<400 | 404>>
const LegacyComponentDescriptions = {
LogLevel: "Log level",
ServerConfig: "Server configuration for opencode serve and web commands",
LayoutConfig: "@deprecated Always uses stretch layout.",
} satisfies Record<string, string>
function matchLegacyOpenApi(input: Record<string, unknown>) {
const spec = input as OpenApiSpec
@@ -272,10 +99,16 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
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<string, unknown>) {
// 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<string, unknown>) {
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<string, unknown>) {
},
}
}
// 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<string, OpenApiSchema>) {
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, OpenApiSchema>): string {
return JSON.stringify(canonicalizeSchema(input, schemas))
}
function canonicalizeSchema(input: unknown, schemas: Record<string, OpenApiSchema>): 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<string, OpenApiSchema>) {
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])
}