Prepare Effect HttpApi backend parity (#24853)
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { Provider } from "@/provider/provider"
|
||||
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"
|
||||
|
||||
export const ConfigApi = HttpApi.make("config")
|
||||
.add(
|
||||
HttpApiGroup.make("config")
|
||||
.add(
|
||||
HttpApiEndpoint.get("get", root, {
|
||||
success: described(Config.Info, "Get config info"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "config.get",
|
||||
summary: "Get configuration",
|
||||
description: "Retrieve the current OpenCode configuration settings and preferences.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("update", root, {
|
||||
payload: Config.Info,
|
||||
success: described(Config.Info, "Successfully updated config"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "config.update",
|
||||
summary: "Update configuration",
|
||||
description: "Update OpenCode configuration settings and preferences.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("providers", `${root}/providers`, {
|
||||
success: described(Provider.ConfigProvidersResult, "List of providers"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "config.providers",
|
||||
summary: "List config providers",
|
||||
description: "Get a list of all configured AI providers and their default models.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "config",
|
||||
description: "Experimental HttpApi config routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Auth } from "@/auth"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { described } from "./metadata"
|
||||
|
||||
const AuthParams = Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
})
|
||||
|
||||
const LogQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
workspace: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
export const LogInput = Schema.Struct({
|
||||
service: Schema.String.annotate({ description: "Service name for the log entry" }),
|
||||
level: Schema.Union([
|
||||
Schema.Literal("debug"),
|
||||
Schema.Literal("info"),
|
||||
Schema.Literal("error"),
|
||||
Schema.Literal("warn"),
|
||||
]).annotate({ description: "Log level" }),
|
||||
message: Schema.String.annotate({ description: "Log message" }),
|
||||
extra: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)).annotate({
|
||||
description: "Additional metadata for the log entry",
|
||||
}),
|
||||
})
|
||||
|
||||
export const ControlPaths = {
|
||||
auth: "/auth/:providerID",
|
||||
log: "/log",
|
||||
} as const
|
||||
|
||||
export const ControlApi = HttpApi.make("control").add(
|
||||
HttpApiGroup.make("control")
|
||||
.add(
|
||||
HttpApiEndpoint.put("authSet", ControlPaths.auth, {
|
||||
params: AuthParams,
|
||||
payload: Auth.Info,
|
||||
success: described(Schema.Boolean, "Successfully set authentication credentials"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "auth.set",
|
||||
summary: "Set auth credentials",
|
||||
description: "Set authentication credentials",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("authRemove", ControlPaths.auth, {
|
||||
params: AuthParams,
|
||||
success: described(Schema.Boolean, "Successfully removed authentication credentials"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "auth.remove",
|
||||
summary: "Remove auth credentials",
|
||||
description: "Remove authentication credentials",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("log", ControlPaths.log, {
|
||||
query: LogQuery,
|
||||
payload: LogInput,
|
||||
success: described(Schema.Boolean, "Log entry written successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "app.log",
|
||||
summary: "Write log",
|
||||
description: "Write a log entry to the server logs with specified level and metadata.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "control", description: "Control plane routes." })),
|
||||
)
|
||||
@@ -0,0 +1,212 @@
|
||||
import { AccountID, OrgID } from "@/account/schema"
|
||||
import { MCP } from "@/mcp"
|
||||
import { ProviderID, ModelID } from "@/provider/schema"
|
||||
import { Session } from "@/session/session"
|
||||
import { Worktree } from "@/worktree"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
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)),
|
||||
activeOrgName: Schema.optionalKey(Schema.String),
|
||||
switchableOrgCount: NonNegativeInt,
|
||||
}).annotate({ identifier: "ConsoleState" })
|
||||
|
||||
const ConsoleOrgOption = Schema.Struct({
|
||||
accountID: Schema.String,
|
||||
accountEmail: Schema.String,
|
||||
accountUrl: Schema.String,
|
||||
orgID: Schema.String,
|
||||
orgName: Schema.String,
|
||||
active: Schema.Boolean,
|
||||
})
|
||||
|
||||
const ConsoleOrgList = Schema.Struct({
|
||||
orgs: Schema.Array(ConsoleOrgOption),
|
||||
})
|
||||
|
||||
export const ConsoleSwitchPayload = Schema.Struct({
|
||||
accountID: AccountID,
|
||||
orgID: OrgID,
|
||||
})
|
||||
|
||||
const ToolIDs = Schema.Array(Schema.String).annotate({ identifier: "ToolIDs" })
|
||||
const ToolListItem = Schema.Struct({
|
||||
id: Schema.String,
|
||||
description: Schema.String,
|
||||
parameters: Schema.Unknown,
|
||||
}).annotate({ identifier: "ToolListItem" })
|
||||
const ToolList = Schema.Array(ToolListItem).annotate({ identifier: "ToolList" })
|
||||
export const ToolListQuery = Schema.Struct({
|
||||
provider: ProviderID,
|
||||
model: ModelID,
|
||||
})
|
||||
|
||||
const QueryBoolean = Schema.Literals(["true", "false"]).pipe(
|
||||
Schema.decodeTo(Schema.Boolean, {
|
||||
decode: SchemaGetter.transform((value) => value === "true"),
|
||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
||||
}),
|
||||
)
|
||||
const WorktreeList = Schema.Array(Schema.String)
|
||||
export const SessionListQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
roots: Schema.optional(QueryBoolean),
|
||||
start: Schema.optional(Schema.NumberFromString),
|
||||
cursor: Schema.optional(Schema.NumberFromString),
|
||||
search: Schema.optional(Schema.String),
|
||||
limit: Schema.optional(Schema.NumberFromString),
|
||||
archived: Schema.optional(QueryBoolean),
|
||||
})
|
||||
|
||||
export const ExperimentalPaths = {
|
||||
console: "/experimental/console",
|
||||
consoleOrgs: "/experimental/console/orgs",
|
||||
consoleSwitch: "/experimental/console/switch",
|
||||
tool: "/experimental/tool",
|
||||
toolIDs: "/experimental/tool/ids",
|
||||
worktree: "/experimental/worktree",
|
||||
worktreeReset: "/experimental/worktree/reset",
|
||||
session: "/experimental/session",
|
||||
resource: "/experimental/resource",
|
||||
} as const
|
||||
|
||||
export const ExperimentalApi = HttpApi.make("experimental")
|
||||
.add(
|
||||
HttpApiGroup.make("experimental")
|
||||
.add(
|
||||
HttpApiEndpoint.get("console", ExperimentalPaths.console, {
|
||||
success: described(ConsoleStateResponse, "Active Console provider metadata"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.console.get",
|
||||
summary: "Get active Console provider metadata",
|
||||
description: "Get the active Console org name and the set of provider IDs managed by that Console org.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("consoleOrgs", ExperimentalPaths.consoleOrgs, {
|
||||
success: described(ConsoleOrgList, "Switchable Console orgs"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.console.listOrgs",
|
||||
summary: "List switchable Console orgs",
|
||||
description: "Get the available Console orgs across logged-in accounts, including the current active org.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("consoleSwitch", ExperimentalPaths.consoleSwitch, {
|
||||
payload: ConsoleSwitchPayload,
|
||||
success: described(Schema.Boolean, "Switch success"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.console.switchOrg",
|
||||
summary: "Switch active Console org",
|
||||
description: "Persist a new active Console account/org selection for the current local OpenCode state.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("tool", ExperimentalPaths.tool, {
|
||||
query: ToolListQuery,
|
||||
success: described(ToolList, "Tools"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tool.list",
|
||||
summary: "List tools",
|
||||
description:
|
||||
"Get a list of available tools with their JSON schema parameters for a specific provider and model combination.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("toolIDs", ExperimentalPaths.toolIDs, {
|
||||
success: described(ToolIDs, "Tool IDs"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tool.ids",
|
||||
summary: "List tool IDs",
|
||||
description:
|
||||
"Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("worktree", ExperimentalPaths.worktree, {
|
||||
success: described(WorktreeList, "List of worktree directories"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "worktree.list",
|
||||
summary: "List worktrees",
|
||||
description: "List all sandbox worktrees for the current project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("worktreeCreate", ExperimentalPaths.worktree, {
|
||||
payload: Schema.optional(Worktree.CreateInput),
|
||||
success: described(Worktree.Info, "Worktree created"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "worktree.create",
|
||||
summary: "Create worktree",
|
||||
description: "Create a new git worktree for the current project and run any configured startup scripts.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("worktreeRemove", ExperimentalPaths.worktree, {
|
||||
payload: Worktree.RemoveInput,
|
||||
success: described(Schema.Boolean, "Worktree removed"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "worktree.remove",
|
||||
summary: "Remove worktree",
|
||||
description: "Remove a git worktree and delete its branch.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("worktreeReset", ExperimentalPaths.worktreeReset, {
|
||||
payload: Worktree.ResetInput,
|
||||
success: described(Schema.Boolean, "Worktree reset"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "worktree.reset",
|
||||
summary: "Reset worktree",
|
||||
description: "Reset a worktree branch to the primary default branch.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("session", ExperimentalPaths.session, {
|
||||
query: SessionListQuery,
|
||||
success: described(Schema.Array(Session.GlobalInfo), "List of sessions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.session.list",
|
||||
summary: "List sessions",
|
||||
description:
|
||||
"Get a list of all OpenCode sessions across projects, sorted by most recently updated. Archived sessions are excluded by default.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("resource", ExperimentalPaths.resource, {
|
||||
success: described(Schema.Record(Schema.String, MCP.Resource), "MCP resources"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.resource.list",
|
||||
summary: "Get MCP resources",
|
||||
description: "Get all available MCP resources from connected servers. Optionally filter by name.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "experimental",
|
||||
description: "Experimental HttpApi read-only routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,119 @@
|
||||
import { File } from "@/file"
|
||||
import { Ripgrep } from "@/file/ripgrep"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
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,
|
||||
})
|
||||
|
||||
export const FindTextQuery = Schema.Struct({
|
||||
pattern: Schema.String,
|
||||
})
|
||||
|
||||
export const FindFileQuery = Schema.Struct({
|
||||
query: Schema.String,
|
||||
dirs: Schema.optional(Schema.Literals(["true", "false"])),
|
||||
type: Schema.optional(Schema.Literals(["file", "directory"])),
|
||||
limit: Schema.optional(
|
||||
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
|
||||
),
|
||||
})
|
||||
|
||||
export const FindSymbolQuery = Schema.Struct({
|
||||
query: Schema.String,
|
||||
})
|
||||
|
||||
export const FilePaths = {
|
||||
findText: "/find",
|
||||
findFile: "/find/file",
|
||||
findSymbol: "/find/symbol",
|
||||
list: "/file",
|
||||
content: "/file/content",
|
||||
status: "/file/status",
|
||||
} as const
|
||||
|
||||
export const FileApi = HttpApi.make("file")
|
||||
.add(
|
||||
HttpApiGroup.make("file")
|
||||
.add(
|
||||
HttpApiEndpoint.get("findText", FilePaths.findText, {
|
||||
query: FindTextQuery,
|
||||
success: described(Schema.Array(Ripgrep.SearchMatch), "Matches"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "find.text",
|
||||
summary: "Find text",
|
||||
description: "Search for text patterns across files in the project using ripgrep.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("findFile", FilePaths.findFile, {
|
||||
query: FindFileQuery,
|
||||
success: described(Schema.Array(Schema.String), "File paths"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "find.files",
|
||||
summary: "Find files",
|
||||
description: "Search for files or directories by name or pattern in the project directory.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("findSymbol", FilePaths.findSymbol, {
|
||||
query: FindSymbolQuery,
|
||||
success: described(Schema.Array(LSP.Symbol), "Symbols"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "find.symbols",
|
||||
summary: "Find symbols",
|
||||
description: "Search for workspace symbols like functions, classes, and variables using LSP.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("list", FilePaths.list, {
|
||||
query: FileQuery,
|
||||
success: described(Schema.Array(File.Node), "Files and directories"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "file.list",
|
||||
summary: "List files",
|
||||
description: "List files and directories in a specified path.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("content", FilePaths.content, {
|
||||
query: FileQuery,
|
||||
success: described(File.Content, "File content"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "file.read",
|
||||
summary: "Read file",
|
||||
description: "Read the content of a specified file.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("status", FilePaths.status, {
|
||||
success: described(Schema.Array(File.Info), "File status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "file.status",
|
||||
summary: "Get file status",
|
||||
description: "Get the git status of all files in the project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "file",
|
||||
description: "Experimental HttpApi file routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Config } from "@/config/config"
|
||||
import { BusEvent } from "@/bus/bus-event"
|
||||
import { SyncEvent } from "@/sync"
|
||||
import { Schema } from "effect"
|
||||
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,
|
||||
})
|
||||
|
||||
const GlobalEventSchema = Schema.Struct({
|
||||
directory: Schema.String,
|
||||
project: Schema.optional(Schema.String),
|
||||
workspace: Schema.optional(Schema.String),
|
||||
payload: Schema.Union([...BusEvent.effectPayloads(), ...SyncEvent.effectPayloads()]),
|
||||
}).annotate({ identifier: "GlobalEvent" })
|
||||
|
||||
export const GlobalUpgradeInput = Schema.Struct({
|
||||
target: Schema.optional(Schema.String),
|
||||
})
|
||||
|
||||
const GlobalUpgradeResult = Schema.Union([
|
||||
Schema.Struct({
|
||||
success: Schema.Literal(true),
|
||||
version: Schema.String,
|
||||
}),
|
||||
Schema.Struct({
|
||||
success: Schema.Literal(false),
|
||||
error: Schema.String,
|
||||
}),
|
||||
])
|
||||
|
||||
export const GlobalPaths = {
|
||||
health: "/global/health",
|
||||
event: "/global/event",
|
||||
config: "/global/config",
|
||||
dispose: "/global/dispose",
|
||||
upgrade: "/global/upgrade",
|
||||
} as const
|
||||
|
||||
export const GlobalApi = HttpApi.make("global").add(
|
||||
HttpApiGroup.make("global")
|
||||
.add(
|
||||
HttpApiEndpoint.get("health", GlobalPaths.health, {
|
||||
success: described(GlobalHealth, "Health information"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.health",
|
||||
summary: "Get health",
|
||||
description: "Get health information about the OpenCode server.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("event", GlobalPaths.event, {
|
||||
success: GlobalEventSchema,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.event",
|
||||
summary: "Get global events",
|
||||
description: "Subscribe to global events from the OpenCode system using server-sent events.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("configGet", GlobalPaths.config, {
|
||||
success: described(Config.Info, "Get global config info"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.config.get",
|
||||
summary: "Get global configuration",
|
||||
description: "Retrieve the current global OpenCode configuration settings and preferences.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("configUpdate", GlobalPaths.config, {
|
||||
payload: Config.Info,
|
||||
success: described(Config.Info, "Successfully updated global config"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.config.update",
|
||||
summary: "Update global configuration",
|
||||
description: "Update global OpenCode configuration settings and preferences.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("dispose", GlobalPaths.dispose, {
|
||||
success: described(Schema.Boolean, "Global disposed"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.dispose",
|
||||
summary: "Dispose instance",
|
||||
description: "Clean up and dispose all OpenCode instances, releasing all resources.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("upgrade", GlobalPaths.upgrade, {
|
||||
payload: GlobalUpgradeInput,
|
||||
success: described(GlobalUpgradeResult, "Upgrade result"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "global.upgrade",
|
||||
summary: "Upgrade opencode",
|
||||
description: "Upgrade opencode to the specified version or latest if not specified.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "global", description: "Global server routes." })),
|
||||
)
|
||||
@@ -0,0 +1,141 @@
|
||||
import { Agent } from "@/agent/agent"
|
||||
import { Command } from "@/command"
|
||||
import { Format } from "@/format"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import { Vcs } from "@/project/vcs"
|
||||
import { Skill } from "@/skill"
|
||||
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,
|
||||
state: Schema.String,
|
||||
config: Schema.String,
|
||||
worktree: Schema.String,
|
||||
directory: Schema.String,
|
||||
}).annotate({ identifier: "Path" })
|
||||
|
||||
export const VcsDiffQuery = Schema.Struct({
|
||||
mode: Vcs.Mode,
|
||||
})
|
||||
|
||||
export const InstancePaths = {
|
||||
dispose: "/instance/dispose",
|
||||
path: "/path",
|
||||
vcs: "/vcs",
|
||||
vcsDiff: "/vcs/diff",
|
||||
command: "/command",
|
||||
agent: "/agent",
|
||||
skill: "/skill",
|
||||
lsp: "/lsp",
|
||||
formatter: "/formatter",
|
||||
} as const
|
||||
|
||||
export const InstanceApi = HttpApi.make("instance")
|
||||
.add(
|
||||
HttpApiGroup.make("instance")
|
||||
.add(
|
||||
HttpApiEndpoint.post("dispose", InstancePaths.dispose, {
|
||||
success: described(Schema.Boolean, "Instance disposed"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "instance.dispose",
|
||||
summary: "Dispose instance",
|
||||
description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("path", InstancePaths.path, {
|
||||
success: PathInfo,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "path.get",
|
||||
summary: "Get paths",
|
||||
description:
|
||||
"Retrieve the current working directory and related path information for the OpenCode instance.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("vcs", InstancePaths.vcs, {
|
||||
success: described(Vcs.Info, "VCS info"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "vcs.get",
|
||||
summary: "Get VCS info",
|
||||
description:
|
||||
"Retrieve version control system (VCS) information for the current project, such as git branch.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("vcsDiff", InstancePaths.vcsDiff, {
|
||||
query: VcsDiffQuery,
|
||||
success: described(Schema.Array(Vcs.FileDiff), "VCS diff"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "vcs.diff",
|
||||
summary: "Get VCS diff",
|
||||
description: "Retrieve the current git diff for the working tree or against the default branch.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("command", InstancePaths.command, {
|
||||
success: described(Schema.Array(Command.Info), "List of commands"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "command.list",
|
||||
summary: "List commands",
|
||||
description: "Get a list of all available commands in the OpenCode system.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("agent", InstancePaths.agent, {
|
||||
success: described(Schema.Array(Agent.Info), "List of agents"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "app.agents",
|
||||
summary: "List agents",
|
||||
description: "Get a list of all available AI agents in the OpenCode system.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("skill", InstancePaths.skill, {
|
||||
success: described(Schema.Array(Skill.Info), "List of skills"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "app.skills",
|
||||
summary: "List skills",
|
||||
description: "Get a list of all available skills in the OpenCode system.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("lsp", InstancePaths.lsp, {
|
||||
success: described(Schema.Array(LSP.Status), "LSP server status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "lsp.status",
|
||||
summary: "Get LSP status",
|
||||
description: "Get LSP server status",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("formatter", InstancePaths.formatter, {
|
||||
success: described(Schema.Array(Format.Status), "Formatter status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "formatter.status",
|
||||
summary: "Get formatter status",
|
||||
description: "Get formatter status",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "instance",
|
||||
description: "Experimental HttpApi instance read routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,142 @@
|
||||
import { MCP } from "@/mcp"
|
||||
import { ConfigMCP } from "@/config/mcp"
|
||||
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,
|
||||
})
|
||||
|
||||
export const StatusMap = Schema.Record(Schema.String, MCP.Status)
|
||||
export const AuthStartResponse = Schema.Struct({
|
||||
authorizationUrl: Schema.String,
|
||||
})
|
||||
export const AuthCallbackPayload = Schema.Struct({
|
||||
code: Schema.String,
|
||||
})
|
||||
export const AuthRemoveResponse = Schema.Struct({
|
||||
success: Schema.Literal(true),
|
||||
})
|
||||
export class UnsupportedOAuthError extends Schema.ErrorClass<UnsupportedOAuthError>("McpUnsupportedOAuthError")(
|
||||
{ error: Schema.String },
|
||||
{ httpApiStatus: 400 },
|
||||
) {}
|
||||
|
||||
export const McpPaths = {
|
||||
status: "/mcp",
|
||||
auth: "/mcp/:name/auth",
|
||||
authCallback: "/mcp/:name/auth/callback",
|
||||
authAuthenticate: "/mcp/:name/auth/authenticate",
|
||||
connect: "/mcp/:name/connect",
|
||||
disconnect: "/mcp/:name/disconnect",
|
||||
} as const
|
||||
|
||||
export const McpApi = HttpApi.make("mcp")
|
||||
.add(
|
||||
HttpApiGroup.make("mcp")
|
||||
.add(
|
||||
HttpApiEndpoint.get("status", McpPaths.status, {
|
||||
success: described(Schema.Record(Schema.String, MCP.Status), "MCP server status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.status",
|
||||
summary: "Get MCP status",
|
||||
description: "Get the status of all Model Context Protocol (MCP) servers.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("add", McpPaths.status, {
|
||||
payload: AddPayload,
|
||||
success: described(StatusMap, "MCP server added successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.add",
|
||||
summary: "Add MCP server",
|
||||
description: "Dynamically add a new Model Context Protocol (MCP) server to the system.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("authStart", McpPaths.auth, {
|
||||
params: { name: Schema.String },
|
||||
success: described(AuthStartResponse, "OAuth flow started"),
|
||||
error: [UnsupportedOAuthError, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.auth.start",
|
||||
summary: "Start MCP OAuth",
|
||||
description: "Start OAuth authentication flow for a Model Context Protocol (MCP) server.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("authCallback", McpPaths.authCallback, {
|
||||
params: { name: Schema.String },
|
||||
payload: AuthCallbackPayload,
|
||||
success: described(MCP.Status, "OAuth authentication completed"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.auth.callback",
|
||||
summary: "Complete MCP OAuth",
|
||||
description:
|
||||
"Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("authAuthenticate", McpPaths.authAuthenticate, {
|
||||
params: { name: Schema.String },
|
||||
success: described(MCP.Status, "OAuth authentication completed"),
|
||||
error: [UnsupportedOAuthError, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.auth.authenticate",
|
||||
summary: "Authenticate MCP OAuth",
|
||||
description: "Start OAuth flow and wait for callback (opens browser).",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("authRemove", McpPaths.auth, {
|
||||
params: { name: Schema.String },
|
||||
success: described(AuthRemoveResponse, "OAuth credentials removed"),
|
||||
error: HttpApiError.NotFound,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.auth.remove",
|
||||
summary: "Remove MCP OAuth",
|
||||
description: "Remove OAuth credentials for an MCP server.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("connect", McpPaths.connect, {
|
||||
params: { name: Schema.String },
|
||||
success: described(Schema.Boolean, "MCP server connected successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.connect",
|
||||
description: "Connect an MCP server.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("disconnect", McpPaths.disconnect, {
|
||||
params: { name: Schema.String },
|
||||
success: described(Schema.Boolean, "MCP server disconnected successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.disconnect",
|
||||
description: "Disconnect an MCP server.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "mcp",
|
||||
description: "Experimental HttpApi MCP routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
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 = "/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: described(Schema.Array(Permission.Request), "List of pending permissions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.list",
|
||||
summary: "List pending permissions",
|
||||
description: "Get all pending permission requests across all sessions.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||
params: { requestID: PermissionID },
|
||||
payload: ReplyPayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.reply",
|
||||
summary: "Respond to permission request",
|
||||
description: "Approve or deny a permission request from the AI assistant.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "permission",
|
||||
description: "Experimental HttpApi permission routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
import { Project } from "@/project/project"
|
||||
import { ProjectID } from "@/project/schema"
|
||||
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 = "/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: described(Schema.Array(Project.Info), "List of projects"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "project.list",
|
||||
summary: "List all projects",
|
||||
description: "Get a list of projects that have been opened with OpenCode.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("current", `${root}/current`, {
|
||||
success: described(Project.Info, "Current project information"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "project.current",
|
||||
summary: "Get current project",
|
||||
description: "Retrieve the currently active project that OpenCode is working with.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("initGit", `${root}/git/init`, {
|
||||
success: described(Project.Info, "Project information after git initialization"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "project.initGit",
|
||||
summary: "Initialize git repository",
|
||||
description: "Create a git repository for the current project and return the refreshed project info.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("update", `${root}/:projectID`, {
|
||||
params: { projectID: ProjectID },
|
||||
payload: UpdatePayload,
|
||||
success: described(Project.Info, "Updated project information"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "project.update",
|
||||
summary: "Update project",
|
||||
description: "Update project properties such as name, icon, and commands.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "project",
|
||||
description: "Experimental HttpApi project routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,74 @@
|
||||
import { ProviderAuth } from "@/provider/auth"
|
||||
import { Provider } from "@/provider/provider"
|
||||
import { ProviderID } from "@/provider/schema"
|
||||
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"
|
||||
|
||||
export const ProviderApi = HttpApi.make("provider")
|
||||
.add(
|
||||
HttpApiGroup.make("provider")
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", root, {
|
||||
success: described(Provider.ListResult, "List of providers"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "provider.list",
|
||||
summary: "List providers",
|
||||
description: "Get a list of all available AI providers, including both available and connected ones.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("auth", `${root}/auth`, {
|
||||
success: described(ProviderAuth.Methods, "Provider auth methods"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "provider.auth",
|
||||
summary: "Get provider auth methods",
|
||||
description: "Retrieve available authentication methods for all AI providers.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("authorize", `${root}/:providerID/oauth/authorize`, {
|
||||
params: { providerID: ProviderID },
|
||||
payload: ProviderAuth.AuthorizeInput,
|
||||
success: described(Schema.UndefinedOr(ProviderAuth.Authorization), "Authorization URL and method"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "provider.oauth.authorize",
|
||||
summary: "Start OAuth authorization",
|
||||
description: "Start the OAuth authorization flow for a provider.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("callback", `${root}/:providerID/oauth/callback`, {
|
||||
params: { providerID: ProviderID },
|
||||
payload: ProviderAuth.CallbackInput,
|
||||
success: described(Schema.Boolean, "OAuth callback processed successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "provider.oauth.callback",
|
||||
summary: "Handle OAuth callback",
|
||||
description: "Handle the OAuth callback from a provider after user authorization.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "provider",
|
||||
description: "Experimental HttpApi provider routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
import { Pty } from "@/pty"
|
||||
import { PtyID } from "@/pty/schema"
|
||||
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 })
|
||||
export const CursorQuery = Schema.Struct({ cursor: Schema.optional(Schema.String) })
|
||||
export const ShellItem = Schema.Struct({
|
||||
path: Schema.String,
|
||||
name: Schema.String,
|
||||
acceptable: Schema.Boolean,
|
||||
})
|
||||
|
||||
export const PtyPaths = {
|
||||
shells: `${root}/shells`,
|
||||
list: root,
|
||||
create: root,
|
||||
get: `${root}/:ptyID`,
|
||||
update: `${root}/:ptyID`,
|
||||
remove: `${root}/:ptyID`,
|
||||
connect: `${root}/:ptyID/connect`,
|
||||
} as const
|
||||
|
||||
export const PtyApi = HttpApi.make("pty")
|
||||
.add(
|
||||
HttpApiGroup.make("pty")
|
||||
.add(
|
||||
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: 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: described(Pty.Info, "Created session"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.create",
|
||||
summary: "Create PTY session",
|
||||
description: "Create a new pseudo-terminal (PTY) session for running shell commands and processes.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("get", PtyPaths.get, {
|
||||
params: { ptyID: PtyID },
|
||||
success: described(Pty.Info, "Session info"),
|
||||
error: HttpApiError.NotFound,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.get",
|
||||
summary: "Get PTY session",
|
||||
description: "Retrieve detailed information about a specific pseudo-terminal (PTY) session.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.put("update", PtyPaths.update, {
|
||||
params: { ptyID: PtyID },
|
||||
payload: Pty.UpdateInput,
|
||||
success: described(Pty.Info, "Updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.update",
|
||||
summary: "Update PTY session",
|
||||
description: "Update properties of an existing pseudo-terminal (PTY) session.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("remove", PtyPaths.remove, {
|
||||
params: { ptyID: PtyID },
|
||||
success: described(Schema.Boolean, "Session removed"),
|
||||
error: HttpApiError.NotFound,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "pty.remove",
|
||||
summary: "Remove PTY session",
|
||||
description: "Remove and terminate a specific pseudo-terminal (PTY) session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "pty", description: "Experimental HttpApi PTY routes." }))
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
|
||||
export const PtyConnectApi = HttpApi.make("pty-connect").add(
|
||||
HttpApiGroup.make("pty-connect")
|
||||
.add(
|
||||
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",
|
||||
description:
|
||||
"Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "pty", description: "PTY websocket route." })),
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Question } from "@/question"
|
||||
import { QuestionID } from "@/question/schema"
|
||||
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 = "/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: described(Schema.Array(Question.Request), "List of pending questions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "question.list",
|
||||
summary: "List pending questions",
|
||||
description: "Get all pending question requests across all sessions.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("reply", `${root}/:requestID/reply`, {
|
||||
params: { requestID: QuestionID },
|
||||
payload: ReplyPayload,
|
||||
success: described(Schema.Boolean, "Question answered successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "question.reply",
|
||||
summary: "Reply to question request",
|
||||
description: "Provide answers to a question request from the AI assistant.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("reject", `${root}/:requestID/reject`, {
|
||||
params: { requestID: QuestionID },
|
||||
success: described(Schema.Boolean, "Question rejected successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "question.reject",
|
||||
summary: "Reject question request",
|
||||
description: "Reject a question request from the AI assistant.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "question",
|
||||
description: "Question routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Effect HttpApi surface for instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,428 @@
|
||||
import { Permission } from "@/permission"
|
||||
import { PermissionID } from "@/permission/schema"
|
||||
import { ModelID, ProviderID } from "@/provider/schema"
|
||||
import { Session } from "@/session/session"
|
||||
import { MessageV2 } from "@/session/message-v2"
|
||||
import { SessionPrompt } from "@/session/prompt"
|
||||
import { SessionRevert } from "@/session/revert"
|
||||
import { SessionStatus } from "@/session/status"
|
||||
import { SessionSummary } from "@/session/summary"
|
||||
import { Todo } from "@/session/todo"
|
||||
import { MessageID, PartID, SessionID } from "@/session/schema"
|
||||
import { Snapshot } from "@/snapshot"
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
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(
|
||||
Schema.decodeTo(Schema.Boolean, {
|
||||
decode: SchemaGetter.transform((value) => value === "true"),
|
||||
encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
|
||||
}),
|
||||
)
|
||||
export const ListQuery = Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
scope: Schema.optional(Schema.Literals(["project"])),
|
||||
path: Schema.optional(Schema.String),
|
||||
roots: Schema.optional(QueryBoolean),
|
||||
start: Schema.optional(Schema.NumberFromString),
|
||||
search: Schema.optional(Schema.String),
|
||||
limit: Schema.optional(Schema.NumberFromString),
|
||||
})
|
||||
export const DiffQuery = Schema.Struct(Struct.omit(SessionSummary.DiffInput.fields, ["sessionID"]))
|
||||
export const MessagesQuery = Schema.Struct({
|
||||
limit: Schema.optional(Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0))),
|
||||
before: Schema.optional(Schema.String),
|
||||
})
|
||||
export const StatusMap = Schema.Record(Schema.String, SessionStatus.Info)
|
||||
export const UpdatePayload = Schema.Struct({
|
||||
title: Schema.optional(Schema.String),
|
||||
permission: Schema.optional(Permission.Ruleset),
|
||||
time: Schema.optional(
|
||||
Schema.Struct({
|
||||
archived: Schema.optional(NonNegativeInt),
|
||||
}),
|
||||
),
|
||||
})
|
||||
export const ForkPayload = Schema.Struct(Struct.omit(Session.ForkInput.fields, ["sessionID"]))
|
||||
export const InitPayload = Schema.Struct({
|
||||
modelID: ModelID,
|
||||
providerID: ProviderID,
|
||||
messageID: MessageID,
|
||||
})
|
||||
export const SummarizePayload = Schema.Struct({
|
||||
providerID: ProviderID,
|
||||
modelID: ModelID,
|
||||
auto: Schema.optional(Schema.Boolean),
|
||||
})
|
||||
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,
|
||||
})
|
||||
|
||||
export const SessionPaths = {
|
||||
list: root,
|
||||
status: `${root}/status`,
|
||||
get: `${root}/:sessionID`,
|
||||
children: `${root}/:sessionID/children`,
|
||||
todo: `${root}/:sessionID/todo`,
|
||||
diff: `${root}/:sessionID/diff`,
|
||||
messages: `${root}/:sessionID/message`,
|
||||
message: `${root}/:sessionID/message/:messageID`,
|
||||
create: root,
|
||||
remove: `${root}/:sessionID`,
|
||||
update: `${root}/:sessionID`,
|
||||
fork: `${root}/:sessionID/fork`,
|
||||
abort: `${root}/:sessionID/abort`,
|
||||
share: `${root}/:sessionID/share`,
|
||||
init: `${root}/:sessionID/init`,
|
||||
summarize: `${root}/:sessionID/summarize`,
|
||||
prompt: `${root}/:sessionID/message`,
|
||||
promptAsync: `${root}/:sessionID/prompt_async`,
|
||||
command: `${root}/:sessionID/command`,
|
||||
shell: `${root}/:sessionID/shell`,
|
||||
revert: `${root}/:sessionID/revert`,
|
||||
unrevert: `${root}/:sessionID/unrevert`,
|
||||
permissions: `${root}/:sessionID/permissions/:permissionID`,
|
||||
deleteMessage: `${root}/:sessionID/message/:messageID`,
|
||||
deletePart: `${root}/:sessionID/message/:messageID/part/:partID`,
|
||||
updatePart: `${root}/:sessionID/message/:messageID/part/:partID`,
|
||||
} as const
|
||||
|
||||
export const SessionApi = HttpApi.make("session")
|
||||
.add(
|
||||
HttpApiGroup.make("session")
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", SessionPaths.list, {
|
||||
query: ListQuery,
|
||||
success: described(Schema.Array(Session.Info), "List of sessions"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.list",
|
||||
summary: "List sessions",
|
||||
description: "Get a list of all OpenCode sessions, sorted by most recently updated.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("status", SessionPaths.status, {
|
||||
success: described(StatusMap, "Get session status"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.status",
|
||||
summary: "Get session status",
|
||||
description: "Retrieve the current status of all sessions, including active, idle, and completed states.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("get", SessionPaths.get, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Session.Info, "Get session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.get",
|
||||
summary: "Get session",
|
||||
description: "Retrieve detailed information about a specific OpenCode session.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("children", SessionPaths.children, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Schema.Array(Session.Info), "List of children"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.children",
|
||||
summary: "Get session children",
|
||||
description: "Retrieve all child sessions that were forked from the specified parent session.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("todo", SessionPaths.todo, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Schema.Array(Todo.Info), "Todo list"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.todo",
|
||||
summary: "Get session todos",
|
||||
description: "Retrieve the todo list associated with a specific session, showing tasks and action items.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("diff", SessionPaths.diff, {
|
||||
params: { sessionID: SessionID },
|
||||
query: DiffQuery,
|
||||
success: described(Schema.Array(Snapshot.FileDiff), "Successfully retrieved diff"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.diff",
|
||||
summary: "Get message diff",
|
||||
description: "Get the file changes (diff) that resulted from a specific user message in the session.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("messages", SessionPaths.messages, {
|
||||
params: { sessionID: SessionID },
|
||||
query: MessagesQuery,
|
||||
success: described(Schema.Array(MessageV2.WithParts), "List of messages"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.messages",
|
||||
summary: "Get session messages",
|
||||
description: "Retrieve all messages in a session, including user prompts and AI responses.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("message", SessionPaths.message, {
|
||||
params: { sessionID: SessionID, messageID: MessageID },
|
||||
success: described(MessageV2.WithParts, "Message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.message",
|
||||
summary: "Get message",
|
||||
description: "Retrieve a specific message from a session by its message ID.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("create", SessionPaths.create, {
|
||||
payload: [HttpApiSchema.NoContent, Session.CreateInput],
|
||||
success: described(Session.Info, "Successfully created session"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.create",
|
||||
summary: "Create session",
|
||||
description: "Create a new OpenCode session for interacting with AI assistants and managing conversations.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("remove", SessionPaths.remove, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Schema.Boolean, "Successfully deleted session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.delete",
|
||||
summary: "Delete session",
|
||||
description: "Delete a session and permanently remove all associated data, including messages and history.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("update", SessionPaths.update, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: UpdatePayload,
|
||||
success: described(Session.Info, "Successfully updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.update",
|
||||
summary: "Update session",
|
||||
description: "Update properties of an existing session, such as title or other metadata.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("fork", SessionPaths.fork, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: ForkPayload,
|
||||
success: described(Session.Info, "200"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.fork",
|
||||
summary: "Fork session",
|
||||
description: "Create a new session by forking an existing session at a specific message point.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("abort", SessionPaths.abort, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Schema.Boolean, "Aborted session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.abort",
|
||||
summary: "Abort session",
|
||||
description: "Abort an active session and stop any ongoing AI processing or command execution.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("init", SessionPaths.init, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: InitPayload,
|
||||
success: described(Schema.Boolean, "200"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.init",
|
||||
summary: "Initialize session",
|
||||
description:
|
||||
"Analyze the current application and create an AGENTS.md file with project-specific agent configurations.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("share", SessionPaths.share, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Session.Info, "Successfully shared session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.share",
|
||||
summary: "Share session",
|
||||
description: "Create a shareable link for a session, allowing others to view the conversation.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("unshare", SessionPaths.share, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Session.Info, "Successfully unshared session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.unshare",
|
||||
summary: "Unshare session",
|
||||
description: "Remove the shareable link for a session, making it private again.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("summarize", SessionPaths.summarize, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: SummarizePayload,
|
||||
success: described(Schema.Boolean, "Summarized session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.summarize",
|
||||
summary: "Summarize session",
|
||||
description: "Generate a concise summary of the session using AI compaction to preserve key information.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("prompt", SessionPaths.prompt, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: PromptPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.prompt",
|
||||
summary: "Send message",
|
||||
description: "Create and send a new message to a session, streaming the AI response.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("promptAsync", SessionPaths.promptAsync, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: PromptPayload,
|
||||
success: described(HttpApiSchema.NoContent, "Prompt accepted"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.prompt_async",
|
||||
summary: "Send async message",
|
||||
description:
|
||||
"Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("command", SessionPaths.command, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: CommandPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.command",
|
||||
summary: "Send command",
|
||||
description: "Send a new command to a session for execution by the AI assistant.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("shell", SessionPaths.shell, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: ShellPayload,
|
||||
success: described(MessageV2.WithParts, "Created message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.shell",
|
||||
summary: "Run shell command",
|
||||
description: "Execute a shell command within the session context and return the AI's response.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("revert", SessionPaths.revert, {
|
||||
params: { sessionID: SessionID },
|
||||
payload: RevertPayload,
|
||||
success: described(Session.Info, "Updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.revert",
|
||||
summary: "Revert message",
|
||||
description:
|
||||
"Revert a specific message in a session, undoing its effects and restoring the previous state.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("unrevert", SessionPaths.unrevert, {
|
||||
params: { sessionID: SessionID },
|
||||
success: described(Session.Info, "Updated session"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.unrevert",
|
||||
summary: "Restore reverted messages",
|
||||
description: "Restore all previously reverted messages in a session.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("permissionRespond", SessionPaths.permissions, {
|
||||
params: { sessionID: SessionID, permissionID: PermissionID },
|
||||
payload: PermissionResponsePayload,
|
||||
success: described(Schema.Boolean, "Permission processed successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "permission.respond",
|
||||
summary: "Respond to permission",
|
||||
description: "Approve or deny a permission request from the AI assistant.",
|
||||
deprecated: true,
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("deleteMessage", SessionPaths.deleteMessage, {
|
||||
params: { sessionID: SessionID, messageID: MessageID },
|
||||
success: described(Schema.Boolean, "Successfully deleted message"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.deleteMessage",
|
||||
summary: "Delete message",
|
||||
description:
|
||||
"Permanently delete a specific message and all of its parts from a session without reverting file changes.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("deletePart", SessionPaths.deletePart, {
|
||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||
success: described(Schema.Boolean, "Successfully deleted part"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "part.delete",
|
||||
description: "Delete a part from a message.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.patch("updatePart", SessionPaths.updatePart, {
|
||||
params: { sessionID: SessionID, messageID: MessageID, partID: PartID },
|
||||
payload: MessageV2.Part,
|
||||
success: described(MessageV2.Part, "Successfully updated part"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "part.update",
|
||||
description: "Update a part in a message.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "session",
|
||||
description: "Experimental HttpApi session routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
import { NonNegativeInt } from "@/util/schema"
|
||||
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({
|
||||
id: Schema.String,
|
||||
aggregateID: Schema.String,
|
||||
seq: NonNegativeInt,
|
||||
type: Schema.String,
|
||||
data: Schema.Record(Schema.String, Schema.Unknown),
|
||||
})
|
||||
export const ReplayPayload = Schema.Struct({
|
||||
directory: Schema.String,
|
||||
events: Schema.NonEmptyArray(ReplayEvent),
|
||||
})
|
||||
export const ReplayResponse = Schema.Struct({
|
||||
sessionID: Schema.String,
|
||||
})
|
||||
export const HistoryPayload = Schema.Record(Schema.String, NonNegativeInt)
|
||||
export const HistoryEvent = Schema.Struct({
|
||||
id: Schema.String,
|
||||
aggregate_id: Schema.String,
|
||||
seq: NonNegativeInt,
|
||||
type: Schema.String,
|
||||
data: Schema.Record(Schema.String, Schema.Unknown),
|
||||
})
|
||||
|
||||
export const SyncPaths = {
|
||||
start: `${root}/start`,
|
||||
replay: `${root}/replay`,
|
||||
history: `${root}/history`,
|
||||
} as const
|
||||
|
||||
export const SyncApi = HttpApi.make("sync")
|
||||
.add(
|
||||
HttpApiGroup.make("sync")
|
||||
.add(
|
||||
HttpApiEndpoint.post("start", SyncPaths.start, {
|
||||
success: described(Schema.Boolean, "Workspace sync started"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "sync.start",
|
||||
summary: "Start workspace sync",
|
||||
description: "Start sync loops for workspaces in the current project that have active sessions.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("replay", SyncPaths.replay, {
|
||||
payload: ReplayPayload,
|
||||
success: described(ReplayResponse, "Replayed sync events"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "sync.replay",
|
||||
summary: "Replay sync events",
|
||||
description: "Validate and replay a complete sync event history.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("history", SyncPaths.history, {
|
||||
payload: HistoryPayload,
|
||||
success: described(Schema.Array(HistoryEvent), "Sync events"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "sync.history.list",
|
||||
summary: "List sync events",
|
||||
description:
|
||||
"List sync events for all aggregates. Keys are aggregate IDs the client already knows about, values are the last known sequence ID. Events with seq > value are returned for those aggregates. Aggregates not listed in the input get their full history.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "sync",
|
||||
description: "Experimental HttpApi sync routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
import { TuiEvent } from "@/cli/cmd/tui/event"
|
||||
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 })
|
||||
export const TuiRequestPayload = Schema.Struct({
|
||||
path: Schema.String,
|
||||
body: Schema.Unknown,
|
||||
})
|
||||
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])
|
||||
|
||||
export const TuiPaths = {
|
||||
appendPrompt: `${root}/append-prompt`,
|
||||
openHelp: `${root}/open-help`,
|
||||
openSessions: `${root}/open-sessions`,
|
||||
openThemes: `${root}/open-themes`,
|
||||
openModels: `${root}/open-models`,
|
||||
submitPrompt: `${root}/submit-prompt`,
|
||||
clearPrompt: `${root}/clear-prompt`,
|
||||
executeCommand: `${root}/execute-command`,
|
||||
showToast: `${root}/show-toast`,
|
||||
publish: `${root}/publish`,
|
||||
selectSession: `${root}/select-session`,
|
||||
controlNext: `${root}/control/next`,
|
||||
controlResponse: `${root}/control/response`,
|
||||
} as const
|
||||
|
||||
export const TuiApi = HttpApi.make("tui")
|
||||
.add(
|
||||
HttpApiGroup.make("tui")
|
||||
.add(
|
||||
HttpApiEndpoint.post("appendPrompt", TuiPaths.appendPrompt, {
|
||||
payload: TuiEvent.PromptAppend.properties,
|
||||
success: described(Schema.Boolean, "Prompt processed successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.appendPrompt",
|
||||
summary: "Append TUI prompt",
|
||||
description: "Append prompt to the TUI.",
|
||||
}),
|
||||
),
|
||||
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: 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: 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: 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: 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: described(Schema.Boolean, "Prompt cleared successfully") }).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.clearPrompt",
|
||||
summary: "Clear TUI prompt",
|
||||
description: "Clear the prompt.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("executeCommand", TuiPaths.executeCommand, {
|
||||
payload: CommandPayload,
|
||||
success: described(Schema.Boolean, "Command executed successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.executeCommand",
|
||||
summary: "Execute TUI command",
|
||||
description: "Execute a TUI command.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("showToast", TuiPaths.showToast, {
|
||||
payload: TuiEvent.ToastShow.properties,
|
||||
success: described(Schema.Boolean, "Toast notification shown successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.showToast",
|
||||
summary: "Show TUI toast",
|
||||
description: "Show a toast notification in the TUI.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("publish", TuiPaths.publish, {
|
||||
payload: TuiPublishPayload,
|
||||
success: described(Schema.Boolean, "Event published successfully"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.publish",
|
||||
summary: "Publish TUI event",
|
||||
description: "Publish a TUI event.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("selectSession", TuiPaths.selectSession, {
|
||||
payload: TuiEvent.SessionSelect.properties,
|
||||
success: described(Schema.Boolean, "Session selected successfully"),
|
||||
error: [HttpApiError.BadRequest, HttpApiError.NotFound],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.selectSession",
|
||||
summary: "Select session",
|
||||
description: "Navigate the TUI to display the specified session.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, { success: described(TuiRequestPayload, "Next TUI request") }).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.control.next",
|
||||
summary: "Get next TUI request",
|
||||
description: "Retrieve the next TUI request from the queue for processing.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("controlResponse", TuiPaths.controlResponse, {
|
||||
payload: Schema.Unknown,
|
||||
success: described(Schema.Boolean, "Response submitted successfully"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "tui.control.response",
|
||||
summary: "Submit TUI response",
|
||||
description: "Submit a response to the TUI request queue to complete a pending request.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "tui", description: "Experimental HttpApi TUI routes." }))
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
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, 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"]))
|
||||
export const SessionRestorePayload = Schema.Struct(
|
||||
Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]),
|
||||
)
|
||||
export const SessionRestoreResponse = Schema.Struct({
|
||||
total: NonNegativeInt,
|
||||
})
|
||||
|
||||
export const WorkspacePaths = {
|
||||
adaptors: `${root}/adaptor`,
|
||||
list: root,
|
||||
status: `${root}/status`,
|
||||
remove: `${root}/:id`,
|
||||
sessionRestore: `${root}/:id/session-restore`,
|
||||
} as const
|
||||
|
||||
export const WorkspaceApi = HttpApi.make("workspace")
|
||||
.add(
|
||||
HttpApiGroup.make("workspace")
|
||||
.add(
|
||||
HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, {
|
||||
success: described(Schema.Array(WorkspaceAdaptorEntry), "Workspace adaptors"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.adaptor.list",
|
||||
summary: "List workspace adaptors",
|
||||
description: "List all available workspace adaptors for the current project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("list", WorkspacePaths.list, {
|
||||
success: described(Schema.Array(Workspace.Info), "Workspaces"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.list",
|
||||
summary: "List workspaces",
|
||||
description: "List all workspaces.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("create", WorkspacePaths.list, {
|
||||
payload: CreatePayload,
|
||||
success: described(Workspace.Info, "Workspace created"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.create",
|
||||
summary: "Create workspace",
|
||||
description: "Create a workspace for the current project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.get("status", WorkspacePaths.status, {
|
||||
success: described(Schema.Array(Workspace.ConnectionStatus), "Workspace status"),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.status",
|
||||
summary: "Workspace status",
|
||||
description: "Get connection status for workspaces in the current project.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.delete("remove", WorkspacePaths.remove, {
|
||||
params: { id: Workspace.Info.fields.id },
|
||||
success: described(Schema.UndefinedOr(Workspace.Info), "Workspace removed"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.remove",
|
||||
summary: "Remove workspace",
|
||||
description: "Remove an existing workspace.",
|
||||
}),
|
||||
),
|
||||
HttpApiEndpoint.post("sessionRestore", WorkspacePaths.sessionRestore, {
|
||||
params: { id: Workspace.Info.fields.id },
|
||||
payload: SessionRestorePayload,
|
||||
success: described(SessionRestoreResponse, "Session replay started"),
|
||||
error: HttpApiError.BadRequest,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "experimental.workspace.sessionRestore",
|
||||
summary: "Restore session into workspace",
|
||||
description: "Replay a session's sync events into the target workspace in batches.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "workspace", description: "Experimental HttpApi workspace routes." }))
|
||||
.middleware(InstanceContextMiddleware)
|
||||
.middleware(Authorization),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
Reference in New Issue
Block a user