feat(core): add command registry (#30624)
This commit is contained in:
parent
70bb710715
commit
1ff19103a2
150 changed files with 4642 additions and 2546 deletions
24
packages/server/src/groups/v2/agent.ts
Normal file
24
packages/server/src/groups/v2/agent.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const AgentGroup = HttpApiGroup.make("v2.agent")
|
||||
.add(
|
||||
HttpApiEndpoint.get("agents", "/api/agent", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(AgentV2.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.agent.list",
|
||||
summary: "List v2 agents",
|
||||
description: "Retrieve currently registered v2 agents.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
30
packages/server/src/groups/v2/command.ts
Normal file
30
packages/server/src/groups/v2/command.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { CommandV2 } from "@opencode-ai/core/command"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const CommandGroup = HttpApiGroup.make("v2.command")
|
||||
.add(
|
||||
HttpApiEndpoint.get("commands", "/api/command", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(CommandV2.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.command.list",
|
||||
summary: "List v2 commands",
|
||||
description: "Retrieve currently registered v2 commands.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 commands",
|
||||
description: "Experimental v2 command routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
36
packages/server/src/groups/v2/event.ts
Normal file
36
packages/server/src/groups/v2/event.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
const Event = Schema.Struct({
|
||||
id: EventV2.ID,
|
||||
type: Schema.String,
|
||||
location: Location.Info.pipe(Schema.optional),
|
||||
metadata: Schema.Record(Schema.String, Schema.Unknown).pipe(Schema.optional),
|
||||
version: Schema.Number.pipe(Schema.optional),
|
||||
data: Schema.Unknown,
|
||||
})
|
||||
|
||||
export const EventGroup = HttpApiGroup.make("v2.event")
|
||||
.add(
|
||||
HttpApiEndpoint.get("events", "/api/event", {
|
||||
query: LocationQuery,
|
||||
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.event.subscribe",
|
||||
summary: "Subscribe to v2 events",
|
||||
description: "Subscribe to native EventV2 payloads for a location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 events", description: "Experimental v2 event stream route." }))
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
|
||||
export type Event = typeof Event.Type
|
||||
57
packages/server/src/groups/v2/fs.ts
Normal file
57
packages/server/src/groups/v2/fs.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { RelativePath } from "@opencode-ai/core/schema"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
const ReadQuery = Schema.Struct({
|
||||
...LocationQuery.fields,
|
||||
path: RelativePath,
|
||||
reference: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const ListQuery = Schema.Struct({
|
||||
...LocationQuery.fields,
|
||||
path: RelativePath.pipe(Schema.optional),
|
||||
reference: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
export const FileSystemGroup = HttpApiGroup.make("v2.fs")
|
||||
.add(
|
||||
HttpApiEndpoint.get("read", "/api/fs/read", {
|
||||
query: ReadQuery,
|
||||
success: Location.response(FileSystem.Content),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.fs.read",
|
||||
summary: "Read file",
|
||||
description: "Read one file relative to the requested location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", "/api/fs/list", {
|
||||
query: ListQuery,
|
||||
success: Location.response(Schema.Array(FileSystem.Entry)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.fs.list",
|
||||
summary: "List directory",
|
||||
description: "List direct children of one directory relative to the requested location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 filesystem",
|
||||
description: "Experimental v2 location-scoped filesystem routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
17
packages/server/src/groups/v2/health.ts
Normal file
17
packages/server/src/groups/v2/health.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
|
||||
export const HealthGroup = HttpApiGroup.make("v2.health")
|
||||
.add(
|
||||
HttpApiEndpoint.get("health", "/api/health", {
|
||||
success: Schema.Struct({ healthy: Schema.Literal(true) }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.health.get",
|
||||
summary: "Check v2 server health",
|
||||
description: "Check whether the v2 API server is ready to accept requests.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
95
packages/server/src/groups/v2/location.ts
Normal file
95
packages/server/src/groups/v2/location.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { CommandV2 } from "@opencode-ai/core/command"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { ProjectReference } from "@opencode-ai/core/project-reference"
|
||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpServerRequest } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
export const LocationQuery = Schema.Struct({
|
||||
location: Schema.optional(
|
||||
Schema.Struct({
|
||||
directory: Schema.optional(Schema.String),
|
||||
workspace: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "V2LocationQuery" })
|
||||
|
||||
export const locationQueryOpenApi = OpenApi.annotations({
|
||||
transform: (operation) => {
|
||||
const parameters = operation.parameters
|
||||
if (!Array.isArray(parameters)) return operation
|
||||
return {
|
||||
...operation,
|
||||
parameters: parameters.map((parameter) =>
|
||||
parameter?.name === "location" && parameter?.in === "query"
|
||||
? { ...parameter, style: "deepObject", explode: true }
|
||||
: parameter,
|
||||
),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
return {
|
||||
location: new Location.Info({
|
||||
directory: location.directory,
|
||||
workspaceID: location.workspaceID,
|
||||
project: location.project,
|
||||
}),
|
||||
data: yield* data,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export class V2LocationMiddleware extends HttpApiMiddleware.Service<
|
||||
V2LocationMiddleware,
|
||||
{
|
||||
provides:
|
||||
| Catalog.Service
|
||||
| AgentV2.Service
|
||||
| CommandV2.Service
|
||||
| Location.Service
|
||||
| PluginBoot.Service
|
||||
| PermissionV2.Service
|
||||
| ProjectReference.Service
|
||||
| FileSystem.Service
|
||||
| SkillV2.Service
|
||||
| QuestionV2.Service
|
||||
}
|
||||
>()("@opencode/ExperimentalHttpApiV2Location") {}
|
||||
|
||||
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
|
||||
const query = new URL(request.url, "http://localhost").searchParams
|
||||
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
|
||||
return {
|
||||
directory: AbsolutePath.make(
|
||||
query.get("location[directory]") || request.headers["x-opencode-directory"] || process.cwd(),
|
||||
),
|
||||
workspaceID: workspaceID ? WorkspaceV2.ID.make(workspaceID) : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
V2LocationMiddleware,
|
||||
Effect.gen(function* () {
|
||||
const locations = yield* LocationServiceMap
|
||||
return V2LocationMiddleware.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
return yield* effect.pipe(Effect.provide(locations.get(ref(request))))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
53
packages/server/src/groups/v2/message.ts
Normal file
53
packages/server/src/groups/v2/message.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
|
||||
export const MessagesQuery = Schema.Struct({
|
||||
limit: Schema.optional(
|
||||
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
|
||||
).annotate({
|
||||
description: "Maximum number of messages to return. When omitted, the endpoint returns its default page size.",
|
||||
}),
|
||||
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
|
||||
description: "Message order for the first page. Use desc for newest first or asc for oldest first.",
|
||||
}),
|
||||
cursor: Schema.optional(
|
||||
Schema.String.annotate({
|
||||
description:
|
||||
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "V2SessionMessagesQuery" })
|
||||
|
||||
export const MessageGroup = HttpApiGroup.make("v2.message")
|
||||
.add(
|
||||
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
query: MessagesQuery,
|
||||
success: Schema.Struct({
|
||||
data: Schema.Array(SessionMessage.Message),
|
||||
cursor: Schema.Struct({
|
||||
previous: Schema.String.pipe(Schema.optional),
|
||||
next: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionMessagesResponse" }),
|
||||
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.messages",
|
||||
summary: "Get v2 session messages",
|
||||
description:
|
||||
"Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 messages",
|
||||
description: "Experimental v2 message routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
32
packages/server/src/groups/v2/model.ts
Normal file
32
packages/server/src/groups/v2/model.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ServiceUnavailableError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const ModelGroup = HttpApiGroup.make("v2.model")
|
||||
.add(
|
||||
HttpApiEndpoint.get("models", "/api/model", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(ModelV2.Info)),
|
||||
error: ServiceUnavailableError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.model.list",
|
||||
summary: "List v2 models",
|
||||
description: "Retrieve available v2 models ordered by release date.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 models",
|
||||
description: "Experimental v2 model routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
95
packages/server/src/groups/v2/permission.ts
Normal file
95
packages/server/src/groups/v2/permission.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const PermissionGroup = HttpApiGroup.make("v2.permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("permissionRequests", "/api/permission/request", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(PermissionV2.Request)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.request.list",
|
||||
summary: "List pending permission requests",
|
||||
description: "Retrieve pending permission requests for a location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 permissions", description: "Experimental v2 permission routes." }))
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
|
||||
export const SessionPermissionGroup = HttpApiGroup.make("v2.session.permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("sessionPermissionRequests", "/api/session/:sessionID/permission/request", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(PermissionV2.Request) }),
|
||||
error: SessionNotFoundError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.list",
|
||||
summary: "List session permission requests",
|
||||
description: "Retrieve pending permission requests owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("permissionRequestReply", "/api/session/:sessionID/permission/request/:requestID/reply", {
|
||||
params: { sessionID: SessionV2.ID, requestID: PermissionV2.ID },
|
||||
payload: Schema.Struct({
|
||||
reply: PermissionV2.Reply,
|
||||
message: Schema.String.pipe(Schema.optional),
|
||||
}),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, PermissionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.permission.reply",
|
||||
summary: "Reply to pending permission request",
|
||||
description: "Respond to a pending permission request owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({ title: "v2 session permissions", description: "Experimental v2 session permission routes." }),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
|
||||
export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
|
||||
.add(
|
||||
HttpApiEndpoint.get("savedPermissions", "/api/permission/saved", {
|
||||
query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }),
|
||||
success: Schema.Struct({ data: Schema.Array(PermissionSaved.Info) }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.list",
|
||||
summary: "List saved permissions",
|
||||
description: "Retrieve saved permissions, optionally filtered by project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("removeSavedPermission", "/api/permission/saved/:id", {
|
||||
params: { id: PermissionSaved.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.remove",
|
||||
summary: "Remove saved permission",
|
||||
description: "Remove a saved permission by ID.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({ title: "v2 saved permissions", description: "Experimental v2 saved permission routes." }),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
49
packages/server/src/groups/v2/provider.ts
Normal file
49
packages/server/src/groups/v2/provider.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ProviderNotFoundError, ServiceUnavailableError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const ProviderGroup = HttpApiGroup.make("v2.provider")
|
||||
.add(
|
||||
HttpApiEndpoint.get("providers", "/api/provider", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(ProviderV2.Info)),
|
||||
error: ServiceUnavailableError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.provider.list",
|
||||
summary: "List v2 providers",
|
||||
description: "Retrieve active v2 AI providers so clients can show provider availability and configuration.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("provider", "/api/provider/:providerID", {
|
||||
params: { providerID: ProviderV2.ID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(ProviderV2.Info),
|
||||
error: [ProviderNotFoundError, ServiceUnavailableError],
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.provider.get",
|
||||
summary: "Get v2 provider",
|
||||
description:
|
||||
"Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 providers",
|
||||
description: "Experimental v2 provider routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
60
packages/server/src/groups/v2/question.ts
Normal file
60
packages/server/src/groups/v2/question.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { QuestionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const QuestionGroup = HttpApiGroup.make("v2.question")
|
||||
.add(
|
||||
HttpApiEndpoint.get("questionRequests", "/api/question/request", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(QuestionV2.Request)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.question.request.list",
|
||||
summary: "List pending question requests",
|
||||
description: "Retrieve pending question requests for a location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 questions", description: "Experimental v2 question routes." }))
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
|
||||
export const SessionQuestionGroup = HttpApiGroup.make("v2.session.question")
|
||||
.add(
|
||||
HttpApiEndpoint.post("questionRequestReply", "/api/session/:sessionID/question/request/:requestID/reply", {
|
||||
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
|
||||
payload: QuestionV2.Reply,
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, QuestionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.question.reply",
|
||||
summary: "Reply to pending question request",
|
||||
description: "Answer a pending question request owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("questionRequestReject", "/api/session/:sessionID/question/request/:requestID/reject", {
|
||||
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, QuestionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.question.reject",
|
||||
summary: "Reject pending question request",
|
||||
description: "Reject a pending question request owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({ title: "v2 session questions", description: "Experimental v2 session question routes." }),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
172
packages/server/src/groups/v2/session.ts
Normal file
172
packages/server/src/groups/v2/session.ts
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { SessionInput } from "@opencode-ai/core/session/input"
|
||||
import { Prompt } from "@opencode-ai/core/session/prompt"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { AbsolutePath, PositiveInt, RelativePath, withStatics } from "@opencode-ai/core/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { Schema, Struct } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import {
|
||||
ConflictError,
|
||||
InvalidCursorError,
|
||||
InvalidRequestError,
|
||||
ServiceUnavailableError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
} from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
|
||||
const SessionsQueryFields = {
|
||||
workspace: WorkspaceV2.ID.pipe(Schema.optional),
|
||||
limit: Schema.NumberFromString.pipe(Schema.decodeTo(PositiveInt), Schema.optional).annotate({
|
||||
description: "Maximum number of sessions to return. Defaults to the newest 50 sessions.",
|
||||
}),
|
||||
order: Schema.optional(Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")])).annotate({
|
||||
description: "Session order for the first page. Use desc for newest first or asc for oldest first.",
|
||||
}),
|
||||
search: Schema.optional(Schema.String),
|
||||
}
|
||||
|
||||
const SessionsDirectoryQuery = Schema.Struct({
|
||||
...SessionsQueryFields,
|
||||
directory: AbsolutePath,
|
||||
})
|
||||
|
||||
const SessionsProjectQuery = Schema.Struct({
|
||||
...SessionsQueryFields,
|
||||
project: ProjectV2.ID,
|
||||
subpath: RelativePath.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const SessionsAllQuery = Schema.Struct(SessionsQueryFields)
|
||||
|
||||
const withCursor = <Fields extends Schema.Struct.Fields>(schema: Schema.Struct<Fields>) =>
|
||||
schema.mapFields((fields) => ({
|
||||
...Struct.omit(fields, ["limit"]),
|
||||
anchor: SessionV2.ListAnchor,
|
||||
}))
|
||||
|
||||
const SessionsCursorInput = Schema.Union([
|
||||
withCursor(SessionsDirectoryQuery),
|
||||
withCursor(SessionsProjectQuery),
|
||||
withCursor(SessionsAllQuery),
|
||||
])
|
||||
const SessionsCursorJson = Schema.fromJsonString(SessionsCursorInput)
|
||||
const encodeSessionsCursor = Schema.encodeSync(SessionsCursorJson)
|
||||
const decodeSessionsCursor = Schema.decodeUnknownEffect(SessionsCursorJson)
|
||||
|
||||
export const SessionsCursor = Schema.String.pipe(
|
||||
Schema.brand("V2SessionsCursor"),
|
||||
withStatics((schema) => {
|
||||
const make = schema.make
|
||||
return {
|
||||
make: (input: typeof SessionsCursorInput.Type) =>
|
||||
make(Buffer.from(encodeSessionsCursor(input)).toString("base64url")),
|
||||
parse: (input: string) => decodeSessionsCursor(Buffer.from(input, "base64url").toString("utf8")),
|
||||
}
|
||||
}),
|
||||
)
|
||||
export type SessionsCursor = typeof SessionsCursor.Type
|
||||
|
||||
const SessionsCursorQuery = Schema.Struct({
|
||||
cursor: SessionsCursor.annotate({
|
||||
description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response.",
|
||||
}),
|
||||
limit: SessionsQueryFields.limit,
|
||||
})
|
||||
|
||||
export const SessionsQuery = Schema.Struct({
|
||||
...SessionsQueryFields,
|
||||
directory: AbsolutePath.pipe(Schema.optional),
|
||||
project: ProjectV2.ID.pipe(Schema.optional),
|
||||
subpath: RelativePath.pipe(Schema.optional),
|
||||
cursor: SessionsCursorQuery.fields.cursor.pipe(Schema.optional),
|
||||
}).annotate({ identifier: "V2SessionsQuery" })
|
||||
|
||||
export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||
.add(
|
||||
HttpApiEndpoint.get("sessions", "/api/session", {
|
||||
query: SessionsQuery,
|
||||
success: Schema.Struct({
|
||||
data: Schema.Array(SessionV2.Info),
|
||||
cursor: Schema.Struct({
|
||||
previous: SessionsCursor.pipe(Schema.optional),
|
||||
next: SessionsCursor.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionsResponse" }),
|
||||
error: [InvalidCursorError, InvalidRequestError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.list",
|
||||
summary: "List v2 sessions",
|
||||
description:
|
||||
"Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
payload: Schema.Struct({
|
||||
id: SessionMessage.ID.pipe(Schema.optional),
|
||||
prompt: Prompt,
|
||||
delivery: SessionInput.Delivery.pipe(Schema.optional),
|
||||
resume: Schema.Boolean.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: SessionMessage.User }),
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.prompt",
|
||||
summary: "Send v2 message",
|
||||
description: "Durably admit one v2 session input and schedule agent-loop execution unless resume is false.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.compact",
|
||||
summary: "Compact v2 session",
|
||||
description: "Compact a v2 session conversation.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.wait",
|
||||
summary: "Wait for v2 session",
|
||||
description: "Wait for a v2 session agent loop to become idle.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(SessionMessage.Message) }),
|
||||
error: [SessionNotFoundError, UnknownError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.context",
|
||||
summary: "Get v2 session context",
|
||||
description: "Retrieve the active context messages for a v2 session (all messages after the last compaction).",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2",
|
||||
description: "Experimental v2 routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
30
packages/server/src/groups/v2/skill.ts
Normal file
30
packages/server/src/groups/v2/skill.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location"
|
||||
|
||||
export const SkillGroup = HttpApiGroup.make("v2.skill")
|
||||
.add(
|
||||
HttpApiEndpoint.get("skills", "/api/skill", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(SkillV2.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.skill.list",
|
||||
summary: "List v2 skills",
|
||||
description: "Retrieve currently registered v2 skills.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 skills",
|
||||
description: "Experimental v2 skill routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
Loading…
Add table
Add a link
Reference in a new issue