refactor(server): canonicalize service API (#31049)
This commit is contained in:
parent
53ff1b57c9
commit
fe0c4f8c74
388 changed files with 7075 additions and 4064 deletions
|
|
@ -1,19 +1,20 @@
|
|||
import { HttpApi, OpenApi } from "effect/unstable/httpapi"
|
||||
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
||||
import { MessageGroup } from "./groups/v2/message"
|
||||
import { ModelGroup } from "./groups/v2/model"
|
||||
import { ProviderGroup } from "./groups/v2/provider"
|
||||
import { SessionGroup } from "./groups/v2/session"
|
||||
import { PermissionGroup, PermissionSavedGroup, SessionPermissionGroup } from "./groups/v2/permission"
|
||||
import { FileSystemGroup } from "./groups/v2/fs"
|
||||
import { CommandGroup } from "./groups/v2/command"
|
||||
import { SkillGroup } from "./groups/v2/skill"
|
||||
import { EventGroup } from "./groups/v2/event"
|
||||
import { AgentGroup } from "./groups/v2/agent"
|
||||
import { HealthGroup } from "./groups/v2/health"
|
||||
import { QuestionGroup, SessionQuestionGroup } from "./groups/v2/question"
|
||||
import { MessageGroup } from "./groups/message"
|
||||
import { ModelGroup } from "./groups/model"
|
||||
import { ProviderGroup } from "./groups/provider"
|
||||
import { SessionGroup } from "./groups/session"
|
||||
import { PermissionGroup } from "./groups/permission"
|
||||
import { FileSystemGroup } from "./groups/fs"
|
||||
import { CommandGroup } from "./groups/command"
|
||||
import { SkillGroup } from "./groups/skill"
|
||||
import { EventGroup } from "./groups/event"
|
||||
import { AgentGroup } from "./groups/agent"
|
||||
import { HealthGroup } from "./groups/health"
|
||||
import { QuestionGroup } from "./groups/question"
|
||||
import { Authorization } from "./middleware/authorization"
|
||||
|
||||
export const V2Api = HttpApi.make("v2")
|
||||
export const Api = HttpApi.make("server")
|
||||
.add(HealthGroup)
|
||||
.add(AgentGroup)
|
||||
.add(SessionGroup)
|
||||
|
|
@ -21,19 +22,17 @@ export const V2Api = HttpApi.make("v2")
|
|||
.add(ModelGroup)
|
||||
.add(ProviderGroup)
|
||||
.add(PermissionGroup)
|
||||
.add(SessionPermissionGroup)
|
||||
.add(PermissionSavedGroup)
|
||||
.add(FileSystemGroup)
|
||||
.add(CommandGroup)
|
||||
.add(SkillGroup)
|
||||
.add(EventGroup)
|
||||
.add(QuestionGroup)
|
||||
.add(SessionQuestionGroup)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode experimental HttpApi",
|
||||
title: "opencode HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(Authorization)
|
||||
.middleware(SchemaErrorMiddleware)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@ 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"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const AgentGroup = HttpApiGroup.make("v2.agent")
|
||||
export const AgentGroup = HttpApiGroup.make("server.agent")
|
||||
.add(
|
||||
HttpApiEndpoint.get("agents", "/api/agent", {
|
||||
HttpApiEndpoint.get("agent.list", "/api/agent", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(AgentV2.Info)),
|
||||
})
|
||||
|
|
@ -15,10 +14,9 @@ export const AgentGroup = HttpApiGroup.make("v2.agent")
|
|||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.agent.list",
|
||||
summary: "List v2 agents",
|
||||
description: "Retrieve currently registered v2 agents.",
|
||||
summary: "List agents",
|
||||
description: "Retrieve currently registered agents.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -2,12 +2,11 @@ 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"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const CommandGroup = HttpApiGroup.make("v2.command")
|
||||
export const CommandGroup = HttpApiGroup.make("server.command")
|
||||
.add(
|
||||
HttpApiEndpoint.get("commands", "/api/command", {
|
||||
HttpApiEndpoint.get("command.list", "/api/command", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(CommandV2.Info)),
|
||||
})
|
||||
|
|
@ -15,16 +14,15 @@ export const CommandGroup = HttpApiGroup.make("v2.command")
|
|||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.command.list",
|
||||
summary: "List v2 commands",
|
||||
description: "Retrieve currently registered v2 commands.",
|
||||
summary: "List commands",
|
||||
description: "Retrieve currently registered commands.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 commands",
|
||||
description: "Experimental v2 command routes.",
|
||||
title: "commands",
|
||||
description: "Experimental command routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -2,8 +2,7 @@ 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"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
const Event = Schema.Struct({
|
||||
id: EventV2.ID,
|
||||
|
|
@ -14,9 +13,9 @@ const Event = Schema.Struct({
|
|||
data: Schema.Unknown,
|
||||
})
|
||||
|
||||
export const EventGroup = HttpApiGroup.make("v2.event")
|
||||
export const EventGroup = HttpApiGroup.make("server.event")
|
||||
.add(
|
||||
HttpApiEndpoint.get("events", "/api/event", {
|
||||
HttpApiEndpoint.get("event.subscribe", "/api/event", {
|
||||
query: LocationQuery,
|
||||
success: Schema.String.pipe(HttpApiSchema.asText({ contentType: "text/event-stream" })),
|
||||
})
|
||||
|
|
@ -24,13 +23,12 @@ export const EventGroup = HttpApiGroup.make("v2.event")
|
|||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.event.subscribe",
|
||||
summary: "Subscribe to v2 events",
|
||||
description: "Subscribe to native EventV2 payloads for a location.",
|
||||
summary: "Subscribe to events",
|
||||
description: "Subscribe to native event payloads for a location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "v2 events", description: "Experimental v2 event stream route." }))
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." }))
|
||||
.middleware(LocationMiddleware)
|
||||
|
||||
export type Event = typeof Event.Type
|
||||
|
|
@ -3,8 +3,7 @@ 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"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
const ReadQuery = Schema.Struct({
|
||||
...LocationQuery.fields,
|
||||
|
|
@ -18,9 +17,9 @@ const ListQuery = Schema.Struct({
|
|||
reference: Schema.String.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
export const FileSystemGroup = HttpApiGroup.make("v2.fs")
|
||||
export const FileSystemGroup = HttpApiGroup.make("server.fs")
|
||||
.add(
|
||||
HttpApiEndpoint.get("read", "/api/fs/read", {
|
||||
HttpApiEndpoint.get("fs.read", "/api/fs/read", {
|
||||
query: ReadQuery,
|
||||
success: Location.response(FileSystem.Content),
|
||||
})
|
||||
|
|
@ -34,7 +33,7 @@ export const FileSystemGroup = HttpApiGroup.make("v2.fs")
|
|||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("list", "/api/fs/list", {
|
||||
HttpApiEndpoint.get("fs.list", "/api/fs/list", {
|
||||
query: ListQuery,
|
||||
success: Location.response(Schema.Array(FileSystem.Entry)),
|
||||
})
|
||||
|
|
@ -49,9 +48,8 @@ export const FileSystemGroup = HttpApiGroup.make("v2.fs")
|
|||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 filesystem",
|
||||
description: "Experimental v2 location-scoped filesystem routes.",
|
||||
title: "filesystem",
|
||||
description: "Experimental location-scoped filesystem routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
.middleware(LocationMiddleware)
|
||||
15
packages/server/src/groups/health.ts
Normal file
15
packages/server/src/groups/health.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
export const HealthGroup = HttpApiGroup.make("server.health")
|
||||
.add(
|
||||
HttpApiEndpoint.get("health.get", "/api/health", {
|
||||
success: Schema.Struct({ healthy: Schema.Literal(true) }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.health.get",
|
||||
summary: "Check server health",
|
||||
description: "Check whether the API server is ready to accept requests.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,16 +1,8 @@
|
|||
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"
|
||||
|
|
@ -22,7 +14,7 @@ export const LocationQuery = Schema.Struct({
|
|||
workspace: Schema.optional(Schema.String),
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "V2LocationQuery" })
|
||||
}).annotate({ identifier: "LocationQuery" })
|
||||
|
||||
export const locationQueryOpenApi = OpenApi.annotations({
|
||||
transform: (operation) => {
|
||||
|
|
@ -53,22 +45,14 @@ export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
|||
})
|
||||
}
|
||||
|
||||
export class V2LocationMiddleware extends HttpApiMiddleware.Service<
|
||||
V2LocationMiddleware,
|
||||
export type LocationServices = Layer.Success<ReturnType<typeof LocationServiceMap.get>>
|
||||
|
||||
export class LocationMiddleware extends HttpApiMiddleware.Service<
|
||||
LocationMiddleware,
|
||||
{
|
||||
provides:
|
||||
| Catalog.Service
|
||||
| AgentV2.Service
|
||||
| CommandV2.Service
|
||||
| Location.Service
|
||||
| PluginBoot.Service
|
||||
| PermissionV2.Service
|
||||
| ProjectReference.Service
|
||||
| FileSystem.Service
|
||||
| SkillV2.Service
|
||||
| QuestionV2.Service
|
||||
provides: LocationServices
|
||||
}
|
||||
>()("@opencode/ExperimentalHttpApiV2Location") {}
|
||||
>()("@opencode/HttpApiLocation") {}
|
||||
|
||||
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
|
||||
const query = new URL(request.url, "http://localhost").searchParams
|
||||
|
|
@ -82,10 +66,10 @@ function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
|
|||
}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
V2LocationMiddleware,
|
||||
LocationMiddleware,
|
||||
Effect.gen(function* () {
|
||||
const locations = yield* LocationServiceMap
|
||||
return V2LocationMiddleware.of((effect) =>
|
||||
return LocationMiddleware.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
return yield* effect.pipe(Effect.provide(locations.get(ref(request))))
|
||||
|
|
@ -2,10 +2,10 @@ 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"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors"
|
||||
import { SessionLocationMiddleware } from "../middleware/session-location"
|
||||
|
||||
export const MessagesQuery = Schema.Struct({
|
||||
export const SessionMessagesQuery = Schema.Struct({
|
||||
limit: Schema.optional(
|
||||
Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)),
|
||||
).annotate({
|
||||
|
|
@ -20,34 +20,35 @@ export const MessagesQuery = Schema.Struct({
|
|||
"Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order.",
|
||||
}),
|
||||
),
|
||||
}).annotate({ identifier: "V2SessionMessagesQuery" })
|
||||
}).annotate({ identifier: "SessionMessagesQuery" })
|
||||
|
||||
export const MessageGroup = HttpApiGroup.make("v2.message")
|
||||
export const MessageGroup = HttpApiGroup.make("server.message")
|
||||
.add(
|
||||
HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", {
|
||||
HttpApiEndpoint.get("session.messages", "/api/session/:sessionID/message", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
query: MessagesQuery,
|
||||
query: SessionMessagesQuery,
|
||||
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" }),
|
||||
}).annotate({ identifier: "SessionMessagesResponse" }),
|
||||
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.",
|
||||
}),
|
||||
),
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.messages",
|
||||
summary: "Get session messages",
|
||||
description:
|
||||
"Retrieve projected 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.",
|
||||
title: "messages",
|
||||
description: "Experimental message routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
|
|
@ -2,13 +2,12 @@ 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"
|
||||
import { ServiceUnavailableError } from "../errors"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const ModelGroup = HttpApiGroup.make("v2.model")
|
||||
export const ModelGroup = HttpApiGroup.make("server.model")
|
||||
.add(
|
||||
HttpApiEndpoint.get("models", "/api/model", {
|
||||
HttpApiEndpoint.get("model.list", "/api/model", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(ModelV2.Info)),
|
||||
error: ServiceUnavailableError,
|
||||
|
|
@ -17,16 +16,15 @@ export const ModelGroup = HttpApiGroup.make("v2.model")
|
|||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.model.list",
|
||||
summary: "List v2 models",
|
||||
description: "Retrieve available v2 models ordered by release date.",
|
||||
summary: "List models",
|
||||
description: "Retrieve available models ordered by release date.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 models",
|
||||
description: "Experimental v2 model routes.",
|
||||
title: "models",
|
||||
description: "Experimental model routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -5,13 +5,13 @@ 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"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../errors"
|
||||
import { SessionLocationMiddleware } from "../middleware/session-location"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const PermissionGroup = HttpApiGroup.make("v2.permission")
|
||||
export const PermissionGroup = HttpApiGroup.make("server.permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("permissionRequests", "/api/permission/request", {
|
||||
HttpApiEndpoint.get("permission.request.list", "/api/permission/request", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(PermissionV2.Request)),
|
||||
})
|
||||
|
|
@ -24,49 +24,8 @@ export const PermissionGroup = HttpApiGroup.make("v2.permission")
|
|||
}),
|
||||
),
|
||||
)
|
||||
.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", {
|
||||
HttpApiEndpoint.get("permission.saved.list", "/api/permission/saved", {
|
||||
query: Schema.Struct({ projectID: ProjectV2.ID.pipe(Schema.optional) }),
|
||||
success: Schema.Struct({ data: Schema.Array(PermissionSaved.Info) }),
|
||||
}).annotateMerge(
|
||||
|
|
@ -78,7 +37,7 @@ export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
|
|||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("removeSavedPermission", "/api/permission/saved/:id", {
|
||||
HttpApiEndpoint.delete("permission.saved.remove", "/api/permission/saved/:id", {
|
||||
params: { id: PermissionSaved.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
}).annotateMerge(
|
||||
|
|
@ -89,7 +48,41 @@ export const PermissionSavedGroup = HttpApiGroup.make("v2.permission.saved")
|
|||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({ title: "v2 saved permissions", description: "Experimental v2 saved permission routes." }),
|
||||
.middleware(LocationMiddleware)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(PermissionV2.Request) }),
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.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("session.permission.reply", "/api/session/:sessionID/permission/: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],
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.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: "permissions", description: "Experimental permission routes." }),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
|
|
@ -2,13 +2,12 @@ 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"
|
||||
import { ProviderNotFoundError, ServiceUnavailableError } from "../errors"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const ProviderGroup = HttpApiGroup.make("v2.provider")
|
||||
export const ProviderGroup = HttpApiGroup.make("server.provider")
|
||||
.add(
|
||||
HttpApiEndpoint.get("providers", "/api/provider", {
|
||||
HttpApiEndpoint.get("provider.list", "/api/provider", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(ProviderV2.Info)),
|
||||
error: ServiceUnavailableError,
|
||||
|
|
@ -17,13 +16,13 @@ export const ProviderGroup = HttpApiGroup.make("v2.provider")
|
|||
.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.",
|
||||
summary: "List providers",
|
||||
description: "Retrieve active AI providers so clients can show provider availability and configuration.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("provider", "/api/provider/:providerID", {
|
||||
HttpApiEndpoint.get("provider.get", "/api/provider/:providerID", {
|
||||
params: { providerID: ProviderV2.ID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(ProviderV2.Info),
|
||||
|
|
@ -33,17 +32,16 @@ export const ProviderGroup = HttpApiGroup.make("v2.provider")
|
|||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.provider.get",
|
||||
summary: "Get v2 provider",
|
||||
summary: "Get provider",
|
||||
description:
|
||||
"Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings.",
|
||||
"Retrieve a single AI provider so clients can inspect its availability and endpoint settings.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 providers",
|
||||
description: "Experimental v2 provider routes.",
|
||||
title: "providers",
|
||||
description: "Experimental provider routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
.middleware(LocationMiddleware)
|
||||
60
packages/server/src/groups/question.ts
Normal file
60
packages/server/src/groups/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 { SessionLocationMiddleware } from "../middleware/session-location"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const QuestionGroup = HttpApiGroup.make("server.question")
|
||||
.add(
|
||||
HttpApiEndpoint.get("question.request.list", "/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: "questions", description: "Experimental question routes." }))
|
||||
.middleware(LocationMiddleware)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.question.reply", "/api/session/:sessionID/question/:requestID/reply", {
|
||||
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
|
||||
payload: QuestionV2.Reply,
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, QuestionNotFoundError],
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.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("session.question.reject", "/api/session/:sessionID/question/:requestID/reject", {
|
||||
params: { sessionID: SessionV2.ID, requestID: QuestionV2.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, QuestionNotFoundError],
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.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: "session questions", description: "Experimental session question routes." }),
|
||||
)
|
||||
|
|
@ -14,8 +14,8 @@ import {
|
|||
ServiceUnavailableError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
} from "../../errors"
|
||||
import { V2Authorization } from "../../middleware/authorization"
|
||||
} from "../errors"
|
||||
import { SessionLocationMiddleware } from "../middleware/session-location"
|
||||
|
||||
const SessionsQueryFields = {
|
||||
workspace: WorkspaceV2.ID.pipe(Schema.optional),
|
||||
|
|
@ -57,7 +57,7 @@ const encodeSessionsCursor = Schema.encodeSync(SessionsCursorJson)
|
|||
const decodeSessionsCursor = Schema.decodeUnknownEffect(SessionsCursorJson)
|
||||
|
||||
export const SessionsCursor = Schema.String.pipe(
|
||||
Schema.brand("V2SessionsCursor"),
|
||||
Schema.brand("SessionsCursor"),
|
||||
withStatics((schema) => {
|
||||
const make = schema.make
|
||||
return {
|
||||
|
|
@ -82,11 +82,11 @@ export const SessionsQuery = Schema.Struct({
|
|||
project: ProjectV2.ID.pipe(Schema.optional),
|
||||
subpath: RelativePath.pipe(Schema.optional),
|
||||
cursor: SessionsCursorQuery.fields.cursor.pipe(Schema.optional),
|
||||
}).annotate({ identifier: "V2SessionsQuery" })
|
||||
}).annotate({ identifier: "SessionsQuery" })
|
||||
|
||||
export const SessionGroup = HttpApiGroup.make("v2.session")
|
||||
export const SessionGroup = HttpApiGroup.make("server.session")
|
||||
.add(
|
||||
HttpApiEndpoint.get("sessions", "/api/session", {
|
||||
HttpApiEndpoint.get("session.list", "/api/session", {
|
||||
query: SessionsQuery,
|
||||
success: Schema.Struct({
|
||||
data: Schema.Array(SessionV2.Info),
|
||||
|
|
@ -94,19 +94,19 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||
previous: SessionsCursor.pipe(Schema.optional),
|
||||
next: SessionsCursor.pipe(Schema.optional),
|
||||
}),
|
||||
}).annotate({ identifier: "V2SessionsResponse" }),
|
||||
}).annotate({ identifier: "SessionsResponse" }),
|
||||
error: [InvalidCursorError, InvalidRequestError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.list",
|
||||
summary: "List v2 sessions",
|
||||
summary: "List 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", {
|
||||
HttpApiEndpoint.post("session.prompt", "/api/session/:sessionID/prompt", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
payload: Schema.Struct({
|
||||
id: SessionMessage.ID.pipe(Schema.optional),
|
||||
|
|
@ -116,57 +116,64 @@ export const SessionGroup = HttpApiGroup.make("v2.session")
|
|||
}),
|
||||
success: Schema.Struct({ data: SessionInput.Admitted }),
|
||||
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.",
|
||||
}),
|
||||
),
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.prompt",
|
||||
summary: "Send message",
|
||||
description: "Durably admit one session input and schedule agent-loop execution unless resume is false.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", {
|
||||
HttpApiEndpoint.post("session.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.",
|
||||
}),
|
||||
),
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.compact",
|
||||
summary: "Compact session",
|
||||
description: "Compact a session conversation.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", {
|
||||
HttpApiEndpoint.post("session.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.",
|
||||
}),
|
||||
),
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.wait",
|
||||
summary: "Wait for session",
|
||||
description: "Wait for a session agent loop to become idle.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("context", "/api/session/:sessionID/context", {
|
||||
HttpApiEndpoint.get("session.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).",
|
||||
}),
|
||||
),
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.context",
|
||||
summary: "Get session context",
|
||||
description: "Retrieve the active context messages for a session (all messages after the last compaction).",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2",
|
||||
description: "Experimental v2 routes.",
|
||||
title: "sessions",
|
||||
description: "Experimental session routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2Authorization)
|
||||
|
|
@ -2,12 +2,11 @@ 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"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const SkillGroup = HttpApiGroup.make("v2.skill")
|
||||
export const SkillGroup = HttpApiGroup.make("server.skill")
|
||||
.add(
|
||||
HttpApiEndpoint.get("skills", "/api/skill", {
|
||||
HttpApiEndpoint.get("skill.list", "/api/skill", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(SkillV2.Info)),
|
||||
})
|
||||
|
|
@ -15,16 +14,15 @@ export const SkillGroup = HttpApiGroup.make("v2.skill")
|
|||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.skill.list",
|
||||
summary: "List v2 skills",
|
||||
description: "Retrieve currently registered v2 skills.",
|
||||
summary: "List skills",
|
||||
description: "Retrieve currently registered skills.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "v2 skills",
|
||||
description: "Experimental v2 skill routes.",
|
||||
title: "skills",
|
||||
description: "Experimental skill routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(V2LocationMiddleware)
|
||||
.middleware(V2Authorization)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
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)
|
||||
|
|
@ -2,56 +2,40 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Layer } from "effect"
|
||||
import { layer as v2LocationLayer } from "./groups/v2/location"
|
||||
import { messageHandlers } from "./handlers/v2/message"
|
||||
import { modelHandlers } from "./handlers/v2/model"
|
||||
import { providerHandlers } from "./handlers/v2/provider"
|
||||
import { sessionHandlers } from "./handlers/v2/session"
|
||||
import { permissionHandlers, savedPermissionHandlers, sessionPermissionHandlers } from "./handlers/v2/permission"
|
||||
import { fileSystemHandlers } from "./handlers/v2/fs"
|
||||
import { commandHandlers } from "./handlers/v2/command"
|
||||
import { skillHandlers } from "./handlers/v2/skill"
|
||||
import { eventHandlers } from "./handlers/v2/event"
|
||||
import { agentHandlers } from "./handlers/v2/agent"
|
||||
import { healthHandlers } from "./handlers/v2/health"
|
||||
import { questionHandlers, sessionQuestionHandlers } from "./handlers/v2/question"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { layer as locationLayer } from "./groups/location"
|
||||
import { sessionLocationLayer } from "./middleware/session-location"
|
||||
import { MessageHandler } from "./handlers/message"
|
||||
import { ModelHandler } from "./handlers/model"
|
||||
import { ProviderHandler } from "./handlers/provider"
|
||||
import { SessionHandler } from "./handlers/session"
|
||||
import { PermissionHandler } from "./handlers/permission"
|
||||
import { FileSystemHandler } from "./handlers/fs"
|
||||
import { CommandHandler } from "./handlers/command"
|
||||
import { SkillHandler } from "./handlers/skill"
|
||||
import { EventHandler } from "./handlers/event"
|
||||
import { AgentHandler } from "./handlers/agent"
|
||||
import { HealthHandler } from "./handlers/health"
|
||||
import { QuestionHandler } from "./handlers/question"
|
||||
import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
|
||||
import { SessionProjector } from "@opencode-ai/core/session/projector"
|
||||
import { SessionStore } from "@opencode-ai/core/session/store"
|
||||
|
||||
const routedSessions = SessionV2.layer.pipe(
|
||||
Layer.provide(SessionProjector.layer),
|
||||
Layer.provide(SessionExecutionLocal.layer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(SessionStore.layer),
|
||||
Layer.provide(EventV2.layer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(ProjectV2.defaultLayer),
|
||||
Layer.orDie,
|
||||
)
|
||||
|
||||
export const v2Handlers = Layer.mergeAll(
|
||||
healthHandlers,
|
||||
agentHandlers,
|
||||
sessionHandlers,
|
||||
messageHandlers,
|
||||
modelHandlers,
|
||||
providerHandlers,
|
||||
permissionHandlers,
|
||||
sessionPermissionHandlers,
|
||||
savedPermissionHandlers,
|
||||
fileSystemHandlers,
|
||||
commandHandlers,
|
||||
skillHandlers,
|
||||
eventHandlers,
|
||||
questionHandlers,
|
||||
sessionQuestionHandlers,
|
||||
export const handlers = Layer.mergeAll(
|
||||
HealthHandler,
|
||||
AgentHandler,
|
||||
SessionHandler,
|
||||
MessageHandler,
|
||||
ModelHandler,
|
||||
ProviderHandler,
|
||||
PermissionHandler,
|
||||
FileSystemHandler,
|
||||
CommandHandler,
|
||||
SkillHandler,
|
||||
EventHandler,
|
||||
QuestionHandler,
|
||||
).pipe(
|
||||
Layer.provide(v2LocationLayer),
|
||||
Layer.provide(sessionLocationLayer),
|
||||
Layer.provide(locationLayer),
|
||||
Layer.provide(SessionV2.defaultLayer),
|
||||
Layer.provide(SessionExecutionLocal.defaultLayer),
|
||||
Layer.provide(PermissionSaved.defaultLayer),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(PermissionSaved.layer),
|
||||
Layer.provide(routedSessions),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import { AgentV2 } from "@opencode-ai/core/agent"
|
|||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { response } from "../../groups/v2/location"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
|
||||
export const agentHandlers = HttpApiBuilder.group(V2Api, "v2.agent", (handlers) =>
|
||||
handlers.handle("agents", () =>
|
||||
export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
|
||||
handlers.handle("agent.list", () =>
|
||||
Effect.gen(function* () {
|
||||
yield* PluginBoot.Service.use((plugin) => plugin.wait())
|
||||
return yield* response(AgentV2.Service.use((agent) => agent.all()))
|
||||
9
packages/server/src/handlers/command.ts
Normal file
9
packages/server/src/handlers/command.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { CommandV2 } from "@opencode-ai/core/command"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
|
||||
export const CommandHandler = HttpApiBuilder.group(Api, "server.command", (handlers) =>
|
||||
handlers.handle("command.list", () => response(CommandV2.Service.use((command) => command.list()))),
|
||||
)
|
||||
|
|
@ -4,7 +4,7 @@ import { Effect, Stream } from "effect"
|
|||
import { HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import * as Sse from "effect/unstable/encoding/Sse"
|
||||
import { V2Api } from "../../api"
|
||||
import { Api } from "../api"
|
||||
|
||||
function eventData(data: unknown): Sse.Event {
|
||||
return {
|
||||
|
|
@ -15,10 +15,10 @@ function eventData(data: unknown): Sse.Event {
|
|||
}
|
||||
}
|
||||
|
||||
export const eventHandlers = HttpApiBuilder.group(V2Api, "v2.event", (handlers) =>
|
||||
export const EventHandler = HttpApiBuilder.group(Api, "server.event", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const events = yield* EventV2.Service
|
||||
return handlers.handleRaw("events", () =>
|
||||
return handlers.handleRaw("event.subscribe", () =>
|
||||
Effect.gen(function* () {
|
||||
const location = yield* Location.Service
|
||||
const connected = {
|
||||
13
packages/server/src/handlers/fs.ts
Normal file
13
packages/server/src/handlers/fs.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
|
||||
export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers
|
||||
.handle("fs.read", (ctx) => response(FileSystem.Service.use((fs) => fs.read(ctx.query))))
|
||||
.handle("fs.list", (ctx) => response(FileSystem.Service.use((fs) => fs.list(ctx.query))))
|
||||
}),
|
||||
)
|
||||
7
packages/server/src/handlers/health.ts
Normal file
7
packages/server/src/handlers/health.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
|
||||
export const HealthHandler = HttpApiBuilder.group(Api, "server.health", (handlers) =>
|
||||
handlers.handle("health.get", () => Effect.succeed({ healthy: true as const })),
|
||||
)
|
||||
|
|
@ -2,8 +2,8 @@ import { SessionMessage } from "@opencode-ai/core/session/message"
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors"
|
||||
import { Api } from "../api"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors"
|
||||
|
||||
const DefaultMessagesLimit = 50
|
||||
|
||||
|
|
@ -24,12 +24,12 @@ const cursor = {
|
|||
},
|
||||
}
|
||||
|
||||
export const messageHandlers = HttpApiBuilder.group(V2Api, "v2.message", (handlers) =>
|
||||
export const MessageHandler = HttpApiBuilder.group(Api, "server.message", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
|
||||
return handlers.handle(
|
||||
"messages",
|
||||
"session.messages",
|
||||
Effect.fn(function* (ctx) {
|
||||
if (ctx.query.cursor && ctx.query.order !== undefined)
|
||||
return yield* new InvalidCursorError({ message: "Cursor cannot be combined with order" })
|
||||
|
|
@ -56,7 +56,7 @@ export const messageHandlers = HttpApiBuilder.group(V2Api, "v2.message", (handle
|
|||
),
|
||||
Effect.catchTag("Session.MessageDecodeError", (error) => {
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
return Effect.logError("failed to decode v2 session message").pipe(
|
||||
return Effect.logError("failed to decode session message").pipe(
|
||||
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
|
||||
Effect.andThen(
|
||||
Effect.fail(
|
||||
|
|
@ -2,19 +2,19 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
|||
import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { ServiceUnavailableError } from "../../errors"
|
||||
import { response } from "../../groups/v2/location"
|
||||
import { Api } from "../api"
|
||||
import { ServiceUnavailableError } from "../errors"
|
||||
import { response } from "../groups/location"
|
||||
|
||||
const catalogUnavailable = new ServiceUnavailableError({
|
||||
message: "Model catalog is unavailable",
|
||||
service: "catalog",
|
||||
})
|
||||
|
||||
export const modelHandlers = HttpApiBuilder.group(V2Api, "v2.model", (handlers) =>
|
||||
export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers.handle(
|
||||
"models",
|
||||
"model.list",
|
||||
Effect.fn(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const pluginBoot = yield* PluginBoot.Service
|
||||
57
packages/server/src/handlers/permission.ts
Normal file
57
packages/server/src/handlers/permission.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { PermissionNotFoundError } from "../errors"
|
||||
import { response } from "../groups/location"
|
||||
|
||||
function missingRequest(id: PermissionV2.ID) {
|
||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
||||
}
|
||||
|
||||
export const PermissionHandler = HttpApiBuilder.group(Api, "server.permission", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers
|
||||
.handle(
|
||||
"permission.request.list",
|
||||
Effect.fn(function* () {
|
||||
return yield* response((yield* PermissionV2.Service).list())
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.permission.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
const permission = yield* PermissionV2.Service
|
||||
return { data: yield* permission.forSession(ctx.params.sessionID) }
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.permission.reply",
|
||||
Effect.fn(function* (ctx) {
|
||||
const permission = yield* PermissionV2.Service
|
||||
const request = yield* permission.get(ctx.params.requestID)
|
||||
if (!request || request.sessionID !== ctx.params.sessionID) return yield* missingRequest(ctx.params.requestID)
|
||||
yield* permission
|
||||
.reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message })
|
||||
.pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID)))
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"permission.saved.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
const location = yield* Location.Service
|
||||
return { data: yield* (yield* PermissionSaved.Service).list({ projectID: ctx.query.projectID ?? location.project.id }) }
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"permission.saved.remove",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* (yield* PermissionSaved.Service).remove(ctx.params.id)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -3,20 +3,20 @@ import { PluginBoot } from "@opencode-ai/core/plugin/boot"
|
|||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { ProviderNotFoundError, ServiceUnavailableError } from "../../errors"
|
||||
import { response } from "../../groups/v2/location"
|
||||
import { Api } from "../api"
|
||||
import { ProviderNotFoundError, ServiceUnavailableError } from "../errors"
|
||||
import { response } from "../groups/location"
|
||||
|
||||
const catalogUnavailable = new ServiceUnavailableError({
|
||||
message: "Provider catalog is unavailable",
|
||||
service: "catalog",
|
||||
})
|
||||
|
||||
export const providerHandlers = HttpApiBuilder.group(V2Api, "v2.provider", (handlers) =>
|
||||
export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers
|
||||
.handle(
|
||||
"providers",
|
||||
"provider.list",
|
||||
Effect.fn(function* () {
|
||||
const catalog = yield* Catalog.Service
|
||||
const pluginBoot = yield* PluginBoot.Service
|
||||
|
|
@ -25,7 +25,7 @@ export const providerHandlers = HttpApiBuilder.group(V2Api, "v2.provider", (hand
|
|||
}),
|
||||
)
|
||||
.handle(
|
||||
"provider",
|
||||
"provider.get",
|
||||
Effect.fn(function* (ctx) {
|
||||
const catalog = yield* Catalog.Service
|
||||
const pluginBoot = yield* PluginBoot.Service
|
||||
55
packages/server/src/handlers/question.ts
Normal file
55
packages/server/src/handlers/question.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { QuestionNotFoundError } from "../errors"
|
||||
import { response } from "../groups/location"
|
||||
|
||||
function missingRequest(id: QuestionV2.ID) {
|
||||
return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` })
|
||||
}
|
||||
|
||||
export const QuestionHandler = HttpApiBuilder.group(Api, "server.question", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const withOwnedQuestion = Effect.fnUntraced(function* <A, E>(
|
||||
sessionID: QuestionV2.Request["sessionID"],
|
||||
requestID: QuestionV2.ID,
|
||||
use: (question: QuestionV2.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
const question = yield* QuestionV2.Service
|
||||
const request = (yield* question.list()).find((request) => request.id === requestID)
|
||||
if (!request || request.sessionID !== sessionID) return yield* missingRequest(requestID)
|
||||
return yield* use(question)
|
||||
})
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
"question.request.list",
|
||||
Effect.fn(function* () {
|
||||
return yield* response((yield* QuestionV2.Service).list())
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.question.reply",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
|
||||
question
|
||||
.reply({ requestID: ctx.params.requestID, answers: ctx.payload.answers })
|
||||
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"session.question.reject",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
|
||||
question
|
||||
.reject(ctx.params.requestID)
|
||||
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { DateTime, Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { SessionsCursor } from "../../groups/v2/session"
|
||||
import { Api } from "../api"
|
||||
import { SessionsCursor } from "../groups/session"
|
||||
import {
|
||||
ConflictError,
|
||||
InvalidCursorError,
|
||||
ServiceUnavailableError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
} from "../../errors"
|
||||
} from "../errors"
|
||||
|
||||
const DefaultSessionsLimit = 50
|
||||
|
||||
export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handlers) =>
|
||||
export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* SessionV2.Service
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
"sessions",
|
||||
"session.list",
|
||||
Effect.fn(function* (ctx) {
|
||||
const query =
|
||||
ctx.query.cursor !== undefined
|
||||
|
|
@ -62,7 +62,7 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
}),
|
||||
)
|
||||
.handle(
|
||||
"prompt",
|
||||
"session.prompt",
|
||||
Effect.fn(function* (ctx) {
|
||||
return {
|
||||
data: yield* session
|
||||
|
|
@ -95,7 +95,7 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
}),
|
||||
)
|
||||
.handle(
|
||||
"compact",
|
||||
"session.compact",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.compact({ sessionID: ctx.params.sessionID }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
|
|
@ -109,8 +109,8 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
Effect.catchTag("Session.OperationUnavailableError", (error) =>
|
||||
Effect.fail(
|
||||
new ServiceUnavailableError({
|
||||
message: `V2 session ${error.operation} is not available yet`,
|
||||
service: `v2.session.${error.operation}`,
|
||||
message: `Session ${error.operation} is not available yet`,
|
||||
service: `session.${error.operation}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
@ -119,7 +119,7 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
}),
|
||||
)
|
||||
.handle(
|
||||
"wait",
|
||||
"session.wait",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* session.wait(ctx.params.sessionID).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", (error) =>
|
||||
|
|
@ -133,8 +133,8 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
Effect.catchTag("Session.OperationUnavailableError", (error) =>
|
||||
Effect.fail(
|
||||
new ServiceUnavailableError({
|
||||
message: `V2 session ${error.operation} is not available yet`,
|
||||
service: `v2.session.${error.operation}`,
|
||||
message: `Session ${error.operation} is not available yet`,
|
||||
service: `session.${error.operation}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
|
@ -143,7 +143,7 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
}),
|
||||
)
|
||||
.handle(
|
||||
"context",
|
||||
"session.context",
|
||||
Effect.fn(function* (ctx) {
|
||||
return {
|
||||
data: yield* session.context(ctx.params.sessionID).pipe(
|
||||
|
|
@ -157,7 +157,7 @@ export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handle
|
|||
),
|
||||
Effect.catchTag("Session.MessageDecodeError", (error) => {
|
||||
const ref = `err_${crypto.randomUUID().slice(0, 8)}`
|
||||
return Effect.logError("failed to decode v2 session message").pipe(
|
||||
return Effect.logError("failed to decode session message").pipe(
|
||||
Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }),
|
||||
Effect.andThen(
|
||||
Effect.fail(
|
||||
8
packages/server/src/handlers/skill.ts
Normal file
8
packages/server/src/handlers/skill.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
|
||||
export const SkillHandler = HttpApiBuilder.group(Api, "server.skill", (handlers) =>
|
||||
handlers.handle("skill.list", () => response(SkillV2.Service.use((skill) => skill.list()))),
|
||||
)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { CommandV2 } from "@opencode-ai/core/command"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { response } from "../../groups/v2/location"
|
||||
|
||||
export const commandHandlers = HttpApiBuilder.group(V2Api, "v2.command", (handlers) =>
|
||||
handlers.handle("commands", () => response(CommandV2.Service.use((command) => command.list()))),
|
||||
)
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { response } from "../../groups/v2/location"
|
||||
|
||||
export const fileSystemHandlers = HttpApiBuilder.group(V2Api, "v2.fs", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers
|
||||
.handle("read", (ctx) => response(FileSystem.Service.use((fs) => fs.read(ctx.query))))
|
||||
.handle("list", (ctx) => response(FileSystem.Service.use((fs) => fs.list(ctx.query))))
|
||||
}),
|
||||
)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
|
||||
export const healthHandlers = HttpApiBuilder.group(V2Api, "v2.health", (handlers) =>
|
||||
handlers.handle("health", () => Effect.succeed({ healthy: true as const })),
|
||||
)
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionV2 } from "@opencode-ai/core/permission"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { PermissionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { response } from "../../groups/v2/location"
|
||||
|
||||
function missingRequest(id: PermissionV2.ID) {
|
||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
||||
}
|
||||
|
||||
export const permissionHandlers = HttpApiBuilder.group(V2Api, "v2.permission", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers.handle(
|
||||
"permissionRequests",
|
||||
Effect.fn(function* () {
|
||||
return yield* response((yield* PermissionV2.Service).list())
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const sessionPermissionHandlers = HttpApiBuilder.group(V2Api, "v2.session.permission", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
const withSessionPermission = Effect.fnUntraced(function* <A, E>(
|
||||
sessionID: Parameters<PermissionV2.Interface["forSession"]>[0],
|
||||
use: (permission: PermissionV2.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row)
|
||||
return yield* new SessionNotFoundError({
|
||||
sessionID,
|
||||
message: `Session not found: ${sessionID}`,
|
||||
})
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
return yield* use(yield* PermissionV2.Service)
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(
|
||||
locations.get({ directory: AbsolutePath.make(row.directory), workspaceID: row.workspaceID ?? undefined }),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
"sessionPermissionRequests",
|
||||
Effect.fn(function* (ctx) {
|
||||
return yield* withSessionPermission(ctx.params.sessionID, (permission) =>
|
||||
permission.forSession(ctx.params.sessionID).pipe(Effect.map((data) => ({ data }))),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"permissionRequestReply",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withSessionPermission(ctx.params.sessionID, (permission) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* permission.get(ctx.params.requestID)
|
||||
if (!request || request.sessionID !== ctx.params.sessionID)
|
||||
return yield* missingRequest(ctx.params.requestID)
|
||||
yield* permission
|
||||
.reply({ requestID: ctx.params.requestID, reply: ctx.payload.reply, message: ctx.payload.message })
|
||||
.pipe(Effect.catchTag("PermissionV2.NotFoundError", () => missingRequest(ctx.params.requestID)))
|
||||
}),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const savedPermissionHandlers = HttpApiBuilder.group(V2Api, "v2.permission.saved", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const saved = yield* PermissionSaved.Service
|
||||
return handlers
|
||||
.handle(
|
||||
"savedPermissions",
|
||||
Effect.fn(function* (ctx) {
|
||||
return { data: yield* saved.list({ projectID: ctx.query.projectID }) }
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"removeSavedPermission",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* saved.remove(ctx.params.id)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { QuestionV2 } from "@opencode-ai/core/question"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { QuestionNotFoundError, SessionNotFoundError } from "../../errors"
|
||||
import { response } from "../../groups/v2/location"
|
||||
|
||||
function missingRequest(id: QuestionV2.ID) {
|
||||
return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` })
|
||||
}
|
||||
|
||||
export const questionHandlers = HttpApiBuilder.group(V2Api, "v2.question", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
return handlers.handle(
|
||||
"questionRequests",
|
||||
Effect.fn(function* () {
|
||||
return yield* response((yield* QuestionV2.Service).list())
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
export const sessionQuestionHandlers = HttpApiBuilder.group(V2Api, "v2.session.question", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
const withSessionQuestion = Effect.fnUntraced(function* <A, E>(
|
||||
sessionID: QuestionV2.Request["sessionID"],
|
||||
use: (question: QuestionV2.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row)
|
||||
return yield* new SessionNotFoundError({
|
||||
sessionID,
|
||||
message: `Session not found: ${sessionID}`,
|
||||
})
|
||||
|
||||
return yield* Effect.gen(function* () {
|
||||
return yield* use(yield* QuestionV2.Service)
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
Effect.provide(
|
||||
locations.get({ directory: AbsolutePath.make(row.directory), workspaceID: row.workspaceID ?? undefined }),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
const withOwnedQuestion = Effect.fnUntraced(function* <A, E>(
|
||||
sessionID: QuestionV2.Request["sessionID"],
|
||||
requestID: QuestionV2.ID,
|
||||
use: (question: QuestionV2.Interface) => Effect.Effect<A, E>,
|
||||
) {
|
||||
return yield* withSessionQuestion(sessionID, (question) =>
|
||||
Effect.gen(function* () {
|
||||
const request = (yield* question.list()).find((request) => request.id === requestID)
|
||||
if (!request || request.sessionID !== sessionID) return yield* missingRequest(requestID)
|
||||
return yield* use(question)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return handlers
|
||||
.handle(
|
||||
"questionRequestReply",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
|
||||
question
|
||||
.reply({ requestID: ctx.params.requestID, answers: ctx.payload.answers })
|
||||
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"questionRequestReject",
|
||||
Effect.fn(function* (ctx) {
|
||||
yield* withOwnedQuestion(ctx.params.sessionID, ctx.params.requestID, (question) =>
|
||||
question
|
||||
.reject(ctx.params.requestID)
|
||||
.pipe(Effect.catchTag("QuestionV2.NotFoundError", () => missingRequest(ctx.params.requestID))),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { V2Api } from "../../api"
|
||||
import { response } from "../../groups/v2/location"
|
||||
|
||||
export const skillHandlers = HttpApiBuilder.group(V2Api, "v2.skill", (handlers) =>
|
||||
handlers.handle("skills", () => response(SkillV2.Service.use((skill) => skill.list()))),
|
||||
)
|
||||
|
|
@ -7,8 +7,8 @@ import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
|||
const AUTH_TOKEN_QUERY = "auth_token"
|
||||
const WWW_AUTHENTICATE = 'Basic realm="Secure Area"'
|
||||
|
||||
export class V2Authorization extends HttpApiMiddleware.Service<V2Authorization>()(
|
||||
"@opencode/ExperimentalHttpApiV2Authorization",
|
||||
export class Authorization extends HttpApiMiddleware.Service<Authorization>()(
|
||||
"@opencode/HttpApiAuthorization",
|
||||
{
|
||||
error: UnauthorizedError,
|
||||
},
|
||||
|
|
@ -40,12 +40,12 @@ function credentialFromRequest(request: HttpServerRequest.HttpServerRequest) {
|
|||
return Effect.succeed(emptyCredential())
|
||||
}
|
||||
|
||||
export const v2AuthorizationLayer = Layer.effect(
|
||||
V2Authorization,
|
||||
export const authorizationLayer = Layer.effect(
|
||||
Authorization,
|
||||
Effect.gen(function* () {
|
||||
const config = yield* ServerAuth.Config
|
||||
if (!ServerAuth.required(config)) return V2Authorization.of((effect) => effect)
|
||||
return V2Authorization.of((effect) =>
|
||||
if (!ServerAuth.required(config)) return Authorization.of((effect) => effect)
|
||||
return Authorization.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const request = yield* HttpServerRequest.HttpServerRequest
|
||||
const credential = yield* credentialFromRequest(request)
|
||||
|
|
|
|||
66
packages/server/src/middleware/session-location.ts
Normal file
66
packages/server/src/middleware/session-location.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionTable } from "@opencode-ai/core/session/sql"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { HttpRouter } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { InvalidRequestError, SessionNotFoundError } from "../errors"
|
||||
import type { LocationServices } from "../groups/location"
|
||||
|
||||
export class SessionLocationMiddleware extends HttpApiMiddleware.Service<
|
||||
SessionLocationMiddleware,
|
||||
{
|
||||
provides: LocationServices
|
||||
}
|
||||
>()("@opencode/HttpApiSessionLocation", {
|
||||
error: [InvalidRequestError, SessionNotFoundError],
|
||||
}) {}
|
||||
|
||||
const decodeSessionID = Schema.decodeUnknownEffect(SessionV2.ID)
|
||||
|
||||
export const sessionLocationLayer = Layer.effect(
|
||||
SessionLocationMiddleware,
|
||||
Effect.gen(function* () {
|
||||
const { db } = yield* Database.Service
|
||||
const locations = yield* LocationServiceMap
|
||||
|
||||
return SessionLocationMiddleware.of((effect) =>
|
||||
Effect.gen(function* () {
|
||||
const route = yield* HttpRouter.RouteContext
|
||||
const sessionID = yield* decodeSessionID(route.params.sessionID).pipe(
|
||||
Effect.mapError(
|
||||
() =>
|
||||
new InvalidRequestError({
|
||||
message: "Invalid session ID",
|
||||
field: "sessionID",
|
||||
}),
|
||||
),
|
||||
)
|
||||
const row = yield* db
|
||||
.select({ directory: SessionTable.directory, workspaceID: SessionTable.workspace_id })
|
||||
.from(SessionTable)
|
||||
.where(eq(SessionTable.id, sessionID))
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!row)
|
||||
return yield* new SessionNotFoundError({
|
||||
sessionID,
|
||||
message: `Session not found: ${sessionID}`,
|
||||
})
|
||||
|
||||
return yield* effect.pipe(
|
||||
Effect.provide(
|
||||
locations.get({
|
||||
directory: AbsolutePath.make(row.directory),
|
||||
workspaceID: row.workspaceID ? WorkspaceV2.ID.make(row.workspaceID) : undefined,
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
|
@ -1,21 +1,19 @@
|
|||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Layer, Option } from "effect"
|
||||
import { V2Api } from "./api"
|
||||
import { Api } from "./api"
|
||||
import { ServerAuth } from "./auth"
|
||||
import { v2Handlers } from "./handlers"
|
||||
import { v2AuthorizationLayer } from "./middleware/authorization"
|
||||
import { handlers } from "./handlers"
|
||||
import { authorizationLayer } from "./middleware/authorization"
|
||||
import { schemaErrorLayer } from "./middleware/schema-error"
|
||||
|
||||
export function createRoutes(password?: string) {
|
||||
return HttpApiBuilder.layer(V2Api).pipe(
|
||||
Layer.provide(v2Handlers),
|
||||
Layer.provide(v2AuthorizationLayer),
|
||||
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
|
||||
Layer.provide(handlers),
|
||||
Layer.provide(authorizationLayer),
|
||||
Layer.provide(schemaErrorLayer),
|
||||
Layer.provide(
|
||||
password
|
||||
|
|
@ -23,8 +21,6 @@ export function createRoutes(password?: string) {
|
|||
: ServerAuth.Config.defaultLayer,
|
||||
),
|
||||
Layer.provide(LocationServiceMap.layer),
|
||||
Layer.provide(PermissionSaved.layer),
|
||||
Layer.provide(SessionV2.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(EventV2.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue