refactor(protocol): extract server contracts (#33708)
This commit is contained in:
parent
f9dac262ff
commit
56a37c3640
77 changed files with 1070 additions and 878 deletions
|
|
@ -1,56 +1,8 @@
|
|||
import { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
||||
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, makeEventGroup } from "./groups/event"
|
||||
import type { Definition } from "@opencode-ai/core/event"
|
||||
import { AgentGroup } from "./groups/agent"
|
||||
import { HealthGroup } from "./groups/health"
|
||||
import { PtyGroup } from "./groups/pty"
|
||||
import { QuestionGroup } from "./groups/question"
|
||||
import { ReferenceGroup } from "./groups/reference"
|
||||
import { Authorization } from "./middleware/authorization"
|
||||
import { LocationGroup } from "./groups/location"
|
||||
import { IntegrationGroup } from "./groups/integration"
|
||||
import { CredentialGroup } from "./groups/credential"
|
||||
import { ProjectCopyGroup } from "./groups/project-copy"
|
||||
import { makeDefaultApi } from "@opencode-ai/protocol/api"
|
||||
import { LocationMiddleware } from "./location"
|
||||
import { SessionLocationMiddleware } from "./middleware/session-location"
|
||||
|
||||
const makeApiFromGroup = <const Group extends HttpApiGroup.Any>(eventGroup: Group) =>
|
||||
HttpApi.make("server")
|
||||
.add(HealthGroup)
|
||||
.add(LocationGroup)
|
||||
.add(AgentGroup)
|
||||
.add(SessionGroup)
|
||||
.add(MessageGroup)
|
||||
.add(ModelGroup)
|
||||
.add(ProviderGroup)
|
||||
.add(IntegrationGroup)
|
||||
.add(CredentialGroup)
|
||||
.add(PermissionGroup)
|
||||
.add(FileSystemGroup)
|
||||
.add(CommandGroup)
|
||||
.add(SkillGroup)
|
||||
.add(eventGroup)
|
||||
.add(PtyGroup)
|
||||
.add(QuestionGroup)
|
||||
.add(ReferenceGroup)
|
||||
.add(ProjectCopyGroup)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "opencode HttpApi",
|
||||
version: "0.0.1",
|
||||
description: "Experimental HttpApi surface for selected instance routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(Authorization)
|
||||
.middleware(SchemaErrorMiddleware)
|
||||
|
||||
export const makeApi = (definitions: ReadonlyArray<Definition>) => makeApiFromGroup(makeEventGroup(definitions))
|
||||
|
||||
export const Api = makeApiFromGroup(EventGroup)
|
||||
export const Api = makeDefaultApi({
|
||||
locationMiddleware: LocationMiddleware,
|
||||
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,111 +0,0 @@
|
|||
import { Schema } from "effect"
|
||||
|
||||
export class InvalidRequestError extends Schema.TaggedErrorClass<InvalidRequestError>()(
|
||||
"InvalidRequestError",
|
||||
{
|
||||
message: Schema.String,
|
||||
kind: Schema.optional(Schema.String),
|
||||
field: Schema.optional(Schema.String),
|
||||
},
|
||||
{ httpApiStatus: 400 },
|
||||
) {}
|
||||
|
||||
export class UnauthorizedError extends Schema.TaggedErrorClass<UnauthorizedError>()(
|
||||
"UnauthorizedError",
|
||||
{ message: Schema.String },
|
||||
{ httpApiStatus: 401 },
|
||||
) {}
|
||||
|
||||
export class ConflictError extends Schema.TaggedErrorClass<ConflictError>()(
|
||||
"ConflictError",
|
||||
{
|
||||
message: Schema.String,
|
||||
resource: Schema.optional(Schema.String),
|
||||
},
|
||||
{ httpApiStatus: 409 },
|
||||
) {}
|
||||
|
||||
export class ServiceUnavailableError extends Schema.TaggedErrorClass<ServiceUnavailableError>()(
|
||||
"ServiceUnavailableError",
|
||||
{
|
||||
message: Schema.String,
|
||||
service: Schema.optional(Schema.String),
|
||||
},
|
||||
{ httpApiStatus: 503 },
|
||||
) {}
|
||||
|
||||
export class UnknownError extends Schema.TaggedErrorClass<UnknownError>()(
|
||||
"UnknownError",
|
||||
{
|
||||
message: Schema.String,
|
||||
ref: Schema.optional(Schema.String),
|
||||
},
|
||||
{ httpApiStatus: 500 },
|
||||
) {}
|
||||
|
||||
export class ProviderNotFoundError extends Schema.TaggedErrorClass<ProviderNotFoundError>()(
|
||||
"ProviderNotFoundError",
|
||||
{
|
||||
providerID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class SessionNotFoundError extends Schema.TaggedErrorClass<SessionNotFoundError>()(
|
||||
"SessionNotFoundError",
|
||||
{
|
||||
sessionID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class MessageNotFoundError extends Schema.TaggedErrorClass<MessageNotFoundError>()(
|
||||
"MessageNotFoundError",
|
||||
{
|
||||
sessionID: Schema.String,
|
||||
messageID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class InvalidCursorError extends Schema.TaggedErrorClass<InvalidCursorError>()(
|
||||
"InvalidCursorError",
|
||||
{ message: Schema.String },
|
||||
{ httpApiStatus: 400 },
|
||||
) {}
|
||||
|
||||
export class PermissionNotFoundError extends Schema.TaggedErrorClass<PermissionNotFoundError>()(
|
||||
"PermissionNotFoundError",
|
||||
{
|
||||
requestID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class QuestionNotFoundError extends Schema.TaggedErrorClass<QuestionNotFoundError>()(
|
||||
"QuestionNotFoundError",
|
||||
{
|
||||
requestID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class ForbiddenError extends Schema.TaggedErrorClass<ForbiddenError>()(
|
||||
"ForbiddenError",
|
||||
{ message: Schema.String },
|
||||
{ httpApiStatus: 403 },
|
||||
) {}
|
||||
|
||||
export class PtyNotFoundError extends Schema.TaggedErrorClass<PtyNotFoundError>()(
|
||||
"PtyNotFoundError",
|
||||
{
|
||||
ptyID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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 { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const AgentGroup = HttpApiGroup.make("server.agent")
|
||||
.add(
|
||||
HttpApiEndpoint.get("agent.list", "/api/agent", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(AgentV2.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.agent.list",
|
||||
summary: "List agents",
|
||||
description: "Retrieve currently registered agents.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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 { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const CommandGroup = HttpApiGroup.make("server.command")
|
||||
.add(
|
||||
HttpApiEndpoint.get("command.list", "/api/command", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(CommandV2.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.command.list",
|
||||
summary: "List commands",
|
||||
description: "Retrieve currently registered commands.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "commands",
|
||||
description: "Experimental command routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { Credential } from "@opencode-ai/core/credential"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { LocationMiddleware, LocationQuery, locationQueryOpenApi } from "./location"
|
||||
|
||||
export const CredentialGroup = HttpApiGroup.make("server.credential")
|
||||
.add(
|
||||
HttpApiEndpoint.patch("credential.update", "/api/credential/:credentialID", {
|
||||
params: { credentialID: Credential.ID },
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({ label: Schema.String }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.credential.update",
|
||||
summary: "Update credential",
|
||||
description: "Update a stored credential label.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("credential.remove", "/api/credential/:credentialID", {
|
||||
params: { credentialID: Credential.ID },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.credential.remove",
|
||||
summary: "Remove credential",
|
||||
description: "Remove a stored integration credential.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { PublicEventManifest } from "@opencode-ai/core/public-event-manifest"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import type { Definition } from "@opencode-ai/core/event"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
|
||||
const fields = {
|
||||
id: EventV2.ID,
|
||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
||||
durable: Schema.optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Int, version: Schema.Int })),
|
||||
location: Schema.optional(Location.Ref),
|
||||
}
|
||||
|
||||
const schema = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) =>
|
||||
Schema.Union([
|
||||
...definitions.map((definition) =>
|
||||
Schema.Struct({
|
||||
...fields,
|
||||
type: Schema.Literal(definition.type),
|
||||
data: definition.data,
|
||||
}).annotate({ identifier: `V2Event.${definition.type}` }),
|
||||
),
|
||||
...(definitions.some((definition) => definition.type === "server.connected")
|
||||
? []
|
||||
: [
|
||||
Schema.Struct({
|
||||
...fields,
|
||||
type: Schema.Literal("server.connected"),
|
||||
data: Schema.Struct({}),
|
||||
}).annotate({ identifier: "V2Event.server.connected" }),
|
||||
]),
|
||||
]).annotate({ identifier: "V2Event" })
|
||||
|
||||
export const makeEventGroup = <const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) =>
|
||||
HttpApiGroup.make("server.event")
|
||||
.add(
|
||||
HttpApiEndpoint.get("event.subscribe", "/api/event", {
|
||||
success: schema(definitions),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.event.subscribe",
|
||||
summary: "Subscribe to events",
|
||||
description: "Subscribe to native event payloads for the server.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." }))
|
||||
|
||||
const EventSchema = schema(PublicEventManifest.Definitions)
|
||||
|
||||
export const EventGroup = HttpApiGroup.make("server.event")
|
||||
.add(
|
||||
HttpApiEndpoint.get("event.subscribe", "/api/event", {
|
||||
success: EventSchema,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.event.subscribe",
|
||||
summary: "Subscribe to events",
|
||||
description: "Subscribe to native event payloads for the server.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "events", description: "Experimental event stream route." }))
|
||||
export type Event = typeof EventSchema.Type
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { PositiveInt, RelativePath } from "@opencode-ai/core/schema"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
const ListQuery = Schema.Struct({
|
||||
...LocationQuery.fields,
|
||||
path: RelativePath.pipe(Schema.optional),
|
||||
})
|
||||
|
||||
const FindQuery = Schema.Struct({
|
||||
...LocationQuery.fields,
|
||||
query: FileSystem.FindInput.fields.query,
|
||||
type: FileSystem.FindInput.fields.type,
|
||||
limit: Schema.NumberFromString.pipe(Schema.decodeTo(PositiveInt), Schema.optional),
|
||||
})
|
||||
|
||||
export const FileSystemGroup = HttpApiGroup.make("server.fs")
|
||||
.add(
|
||||
HttpApiEndpoint.get("fs.read", "/api/fs/read/*", {
|
||||
query: LocationQuery,
|
||||
success: Schema.Uint8Array.pipe(HttpApiSchema.asUint8Array()),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.fs.read",
|
||||
summary: "Read file",
|
||||
description: "Serve one file relative to the requested location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("fs.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.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("fs.find", "/api/fs/find", {
|
||||
query: FindQuery,
|
||||
success: Location.response(Schema.Array(FileSystem.Entry)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.fs.find",
|
||||
summary: "Find files",
|
||||
description: "Find recursively ranked filesystem entries relative to the requested location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "filesystem",
|
||||
description: "Experimental location-scoped filesystem routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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,131 +0,0 @@
|
|||
import { Integration } from "@opencode-ai/core/integration"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { InvalidRequestError } from "../errors"
|
||||
import { LocationMiddleware, LocationQuery, locationQueryOpenApi } from "./location"
|
||||
|
||||
const Inputs = Schema.Record(Schema.String, Schema.String)
|
||||
|
||||
export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
.add(
|
||||
HttpApiEndpoint.get("integration.list", "/api/integration", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(Integration.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.integration.list",
|
||||
summary: "List integrations",
|
||||
description: "Retrieve available integrations and their authentication methods.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("integration.get", "/api/integration/:integrationID", {
|
||||
params: { integrationID: Integration.ID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.UndefinedOr(Integration.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.integration.get",
|
||||
summary: "Get integration",
|
||||
description: "Retrieve one integration and its authentication methods.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("integration.connect.key", "/api/integration/:integrationID/connect/key", {
|
||||
params: { integrationID: Integration.ID },
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({
|
||||
key: Schema.String,
|
||||
label: Schema.optional(Schema.String),
|
||||
}),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: InvalidRequestError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.integration.connect.key",
|
||||
summary: "Connect with key",
|
||||
description: "Run a key authentication method and store the resulting credential.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("integration.connect.oauth", "/api/integration/:integrationID/connect/oauth", {
|
||||
params: { integrationID: Integration.ID },
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({
|
||||
methodID: Integration.MethodID,
|
||||
inputs: Inputs,
|
||||
label: Schema.optional(Schema.String),
|
||||
}),
|
||||
success: Location.response(Integration.Attempt),
|
||||
error: InvalidRequestError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.integration.connect.oauth",
|
||||
summary: "Begin OAuth connection",
|
||||
description: "Start an OAuth attempt and return the authorization details.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("integration.attempt.status", "/api/integration/attempt/:attemptID", {
|
||||
params: { attemptID: Integration.AttemptID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(Integration.AttemptStatus),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.integration.attempt.status",
|
||||
summary: "Get OAuth attempt status",
|
||||
description: "Poll the current status of an OAuth attempt.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("integration.attempt.complete", "/api/integration/attempt/:attemptID/complete", {
|
||||
params: { attemptID: Integration.AttemptID },
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({ code: Schema.optional(Schema.String) }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: InvalidRequestError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.integration.attempt.complete",
|
||||
summary: "Complete OAuth connection",
|
||||
description: "Complete a code-based OAuth attempt and store the resulting credential.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("integration.attempt.cancel", "/api/integration/attempt/:attemptID", {
|
||||
params: { attemptID: Integration.AttemptID },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.integration.attempt.cancel",
|
||||
summary: "Cancel OAuth connection",
|
||||
description: "Cancel an OAuth attempt and release its resources.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({ title: "integrations", description: "Integration discovery and authentication routes." }),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
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 { SessionLocationMiddleware } from "../middleware/session-location"
|
||||
|
||||
export const SessionMessagesQuery = 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: "SessionMessagesQuery" })
|
||||
|
||||
export const MessageGroup = HttpApiGroup.make("server.message")
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.messages", "/api/session/:sessionID/message", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
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: "SessionMessagesResponse" }),
|
||||
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
|
||||
})
|
||||
.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: "messages",
|
||||
description: "Experimental message routes.",
|
||||
}),
|
||||
)
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
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 { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const ModelGroup = HttpApiGroup.make("server.model")
|
||||
.add(
|
||||
HttpApiEndpoint.get("model.list", "/api/model", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(ModelV2.Info)),
|
||||
error: ServiceUnavailableError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.model.list",
|
||||
summary: "List models",
|
||||
description: "Retrieve available models ordered by release date.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "models",
|
||||
description: "Experimental model routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
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 { SessionLocationMiddleware } from "../middleware/session-location"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const PermissionGroup = HttpApiGroup.make("server.permission")
|
||||
.add(
|
||||
HttpApiEndpoint.get("permission.request.list", "/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.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
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(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.permission.saved.list",
|
||||
summary: "List saved permissions",
|
||||
description: "Retrieve saved permissions, optionally filtered by project.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("permission.saved.remove", "/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.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.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." }))
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { ProjectCopy } from "@opencode-ai/core/project/copy"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Schema, Struct } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { LocationMiddleware, LocationQuery, locationQueryOpenApi } from "./location"
|
||||
|
||||
const root = "/experimental/project/:projectID/copy"
|
||||
|
||||
export class ProjectCopyError extends Schema.ErrorClass<ProjectCopyError>("ProjectCopyError")(
|
||||
{
|
||||
name: Schema.Literal("ProjectCopyError"),
|
||||
data: Schema.Struct({
|
||||
message: Schema.String,
|
||||
forceRequired: Schema.optional(Schema.Boolean),
|
||||
}),
|
||||
},
|
||||
{ httpApiStatus: 400 },
|
||||
) {}
|
||||
|
||||
const CreatePayload = Schema.Struct(Struct.omit(ProjectCopy.CreateInput.fields, ["projectID", "sourceDirectory"]))
|
||||
const RemovePayload = Schema.Struct(Struct.omit(ProjectCopy.RemoveInput.fields, ["projectID"]))
|
||||
|
||||
export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
||||
.add(
|
||||
HttpApiEndpoint.post("projectCopy.create", root, {
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: LocationQuery,
|
||||
payload: CreatePayload,
|
||||
success: ProjectCopy.Copy,
|
||||
error: ProjectCopyError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.projectCopy.create" })),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("projectCopy.remove", root, {
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: LocationQuery,
|
||||
payload: RemovePayload,
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: ProjectCopyError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.projectCopy.remove" })),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("projectCopy.refresh", `${root}/refresh`, {
|
||||
params: { projectID: ProjectV2.ID },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: ProjectCopyError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.projectCopy.refresh" })),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "projectCopy", description: "Project copy management routes." }))
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
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 { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const ProviderGroup = HttpApiGroup.make("server.provider")
|
||||
.add(
|
||||
HttpApiEndpoint.get("provider.list", "/api/provider", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(ProviderV2.Info)),
|
||||
error: ServiceUnavailableError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.provider.list",
|
||||
summary: "List providers",
|
||||
description: "Retrieve active AI providers so clients can show provider availability and configuration.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("provider.get", "/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 provider",
|
||||
description: "Retrieve a single AI provider so clients can inspect its availability and endpoint settings.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "providers",
|
||||
description: "Experimental provider routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import { Pty } from "@opencode-ai/core/pty"
|
||||
import { PtyID } from "@opencode-ai/core/pty/schema"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { ForbiddenError, PtyNotFoundError } from "../errors"
|
||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const PTY_CONNECT_TICKET_QUERY = "ticket"
|
||||
export const PTY_CONNECT_TOKEN_HEADER = "x-opencode-ticket"
|
||||
export const PTY_CONNECT_TOKEN_HEADER_VALUE = "1"
|
||||
|
||||
const PTY_CONNECT_PATH = /^\/api\/pty\/[^/]+\/connect$/
|
||||
|
||||
// Authorization middleware skips credential checks when this matches; the PTY connect handler
|
||||
// is then responsible for consuming and validating the ticket.
|
||||
export function hasPtyConnectTicketURL(url: URL) {
|
||||
return PTY_CONNECT_PATH.test(url.pathname) && !!url.searchParams.get(PTY_CONNECT_TICKET_QUERY)
|
||||
}
|
||||
|
||||
export const PtyGroup = HttpApiGroup.make("server.pty")
|
||||
.add(
|
||||
HttpApiEndpoint.get("pty.list", "/api/pty", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(Pty.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.list",
|
||||
summary: "List PTY sessions",
|
||||
description: "List PTY sessions for a location, including exited sessions retained until removal.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("pty.create", "/api/pty", {
|
||||
query: LocationQuery,
|
||||
payload: Pty.CreateInput,
|
||||
success: Location.response(Pty.Info),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.create",
|
||||
summary: "Create PTY session",
|
||||
description: "Create a pseudo-terminal session for a location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("pty.get", "/api/pty/:ptyID", {
|
||||
params: { ptyID: PtyID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(Pty.Info),
|
||||
error: PtyNotFoundError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.get",
|
||||
summary: "Get PTY session",
|
||||
description: "Get one PTY session, including its exit code once exited.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("pty.update", "/api/pty/:ptyID", {
|
||||
params: { ptyID: PtyID },
|
||||
query: LocationQuery,
|
||||
payload: Pty.UpdateInput,
|
||||
success: Location.response(Pty.Info),
|
||||
error: PtyNotFoundError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.update",
|
||||
summary: "Update PTY session",
|
||||
description: "Update the title or viewport size of one PTY session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("pty.remove", "/api/pty/:ptyID", {
|
||||
params: { ptyID: PtyID },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: PtyNotFoundError,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.remove",
|
||||
summary: "Remove PTY session",
|
||||
description: "Terminate and remove one PTY session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("pty.connectToken", "/api/pty/:ptyID/connect-token", {
|
||||
params: { ptyID: PtyID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(PtyTicket.ConnectToken),
|
||||
error: [ForbiddenError, PtyNotFoundError],
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.connectToken",
|
||||
summary: "Create PTY WebSocket token",
|
||||
description: "Create a short-lived single-use ticket for opening a PTY WebSocket connection.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
// Query fields are decoded in the raw handler after the existence check so a missing
|
||||
// session responds with an empty 404 before any upgrade work.
|
||||
HttpApiEndpoint.get("pty.connect", "/api/pty/:ptyID/connect", {
|
||||
params: { ptyID: PtyID },
|
||||
success: Schema.Boolean,
|
||||
error: [ForbiddenError, PtyNotFoundError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.pty.connect",
|
||||
summary: "Connect to PTY session",
|
||||
description: "Establish a WebSocket connection streaming PTY output and accepting terminal input.",
|
||||
transform: (operation) => ({
|
||||
...operation,
|
||||
parameters: [
|
||||
...(operation.parameters ?? []),
|
||||
...["location[directory]", "location[workspace]", "cursor", PTY_CONNECT_TICKET_QUERY].map((name) => ({
|
||||
in: "query",
|
||||
name,
|
||||
schema: { type: "string" },
|
||||
})),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(OpenApi.annotations({ title: "pty", description: "Experimental location-scoped PTY routes." }))
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,75 +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 { 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.get("session.question.list", "/api/session/:sessionID/question", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(QuestionV2.Request) }),
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.question.list",
|
||||
summary: "List session question requests",
|
||||
description: "Retrieve pending question requests owned by a session.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.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." }),
|
||||
)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||
import { LocationMiddleware, LocationQuery, locationQueryOpenApi } from "./location"
|
||||
|
||||
export const ReferenceGroup = HttpApiGroup.make("server.reference")
|
||||
.add(
|
||||
HttpApiEndpoint.get("reference.list", "/api/reference", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(Reference.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.reference.list",
|
||||
summary: "List references",
|
||||
description: "List references available in the requested location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "reference",
|
||||
description: "Location-scoped project references.",
|
||||
}),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
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,
|
||||
MessageNotFoundError,
|
||||
ServiceUnavailableError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
} from "../errors"
|
||||
import { SessionLocationMiddleware } from "../middleware/session-location"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
|
||||
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("SessionsCursor"),
|
||||
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: "SessionsQuery" })
|
||||
|
||||
export const SessionGroup = HttpApiGroup.make("server.session")
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.list", "/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: "SessionsResponse" }),
|
||||
error: [InvalidCursorError, InvalidRequestError],
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.list",
|
||||
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("session.create", "/api/session", {
|
||||
payload: Schema.Struct({
|
||||
id: SessionV2.ID.pipe(Schema.optional),
|
||||
agent: AgentV2.ID.pipe(Schema.optional),
|
||||
model: ModelV2.Ref.pipe(Schema.optional),
|
||||
location: Location.Ref.pipe(Schema.optional),
|
||||
}),
|
||||
success: Schema.Struct({ data: SessionV2.Info }),
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.create",
|
||||
summary: "Create session",
|
||||
description: "Create a session at the requested location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.get", "/api/session/:sessionID", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Struct({ data: SessionV2.Info }),
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.get",
|
||||
summary: "Get session",
|
||||
description: "Retrieve a session by ID.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.switchAgent", "/api/session/:sessionID/agent", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
payload: Schema.Struct({ agent: AgentV2.ID }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.switchAgent",
|
||||
summary: "Switch session agent",
|
||||
description: "Switch the agent used by subsequent provider turns.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.switchModel", "/api/session/:sessionID/model", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
payload: Schema.Struct({ model: ModelV2.Ref }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.switchModel",
|
||||
summary: "Switch session model",
|
||||
description: "Switch the model used by subsequent provider turns.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.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: SessionInput.Admitted }),
|
||||
error: [ConflictError, SessionNotFoundError],
|
||||
})
|
||||
.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("session.compact", "/api/session/:sessionID/compact", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.compact",
|
||||
summary: "Compact session",
|
||||
description: "Compact a session conversation.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.wait", "/api/session/:sessionID/wait", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, ServiceUnavailableError],
|
||||
})
|
||||
.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.post("session.revert.stage", "/api/session/:sessionID/revert/stage", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
payload: Schema.Struct({ messageID: SessionMessage.ID, files: Schema.Boolean.pipe(Schema.optional) }),
|
||||
success: Schema.Struct({ data: SessionV2.RevertState }),
|
||||
error: [MessageNotFoundError, SessionNotFoundError, UnknownError],
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.session.revert.stage",
|
||||
summary: "Stage session revert",
|
||||
description: "Stage or move a reversible session boundary and optionally apply its file changes.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.revert.clear", "/api/session/:sessionID/revert/clear", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: [SessionNotFoundError, UnknownError],
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.session.revert.clear", summary: "Clear staged revert" })),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.revert.commit", "/api/session/:sessionID/revert/commit", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: SessionNotFoundError,
|
||||
})
|
||||
.middleware(SessionLocationMiddleware)
|
||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.session.revert.commit", summary: "Commit staged revert" })),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.context", "/api/session/:sessionID/context", {
|
||||
params: { sessionID: SessionV2.ID },
|
||||
success: Schema.Struct({ data: Schema.Array(SessionMessage.Message) }),
|
||||
error: [SessionNotFoundError, UnknownError],
|
||||
})
|
||||
.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: "sessions",
|
||||
description: "Experimental session routes.",
|
||||
}),
|
||||
)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
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 { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
||||
|
||||
export const SkillGroup = HttpApiGroup.make("server.skill")
|
||||
.add(
|
||||
HttpApiEndpoint.get("skill.list", "/api/skill", {
|
||||
query: LocationQuery,
|
||||
success: Location.response(Schema.Array(SkillV2.Info)),
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.skill.list",
|
||||
summary: "List skills",
|
||||
description: "Retrieve currently registered skills.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
title: "skills",
|
||||
description: "Experimental skill routes.",
|
||||
}),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
|
@ -3,7 +3,7 @@ import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
|||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||
import { Layer } from "effect"
|
||||
import { layer as locationLayer } from "./groups/location"
|
||||
import { layer as locationLayer } from "./location"
|
||||
import { sessionLocationLayer } from "./middleware/session-location"
|
||||
import { MessageHandler } from "./handlers/message"
|
||||
import { ModelHandler } from "./handlers/model"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { AgentV2 } from "@opencode-ai/core/agent"
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
import { response } from "../location"
|
||||
|
||||
export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
|
||||
handlers.handle("agent.list", () =>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
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"
|
||||
import { response } from "../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 } from "effect"
|
|||
import { HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
import { response } from "../location"
|
||||
|
||||
export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { Integration } from "@opencode-ai/core/integration"
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { InvalidRequestError } from "../errors"
|
||||
import { response } from "../groups/location"
|
||||
import { InvalidRequestError } from "@opencode-ai/protocol/errors"
|
||||
import { response } from "../location"
|
||||
|
||||
const authorize = <A, R>(effect: Effect.Effect<A, Integration.AuthorizationError, R>) =>
|
||||
effect.pipe(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||
import { Effect, Schema } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors"
|
||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "@opencode-ai/protocol/errors"
|
||||
|
||||
const DefaultMessagesLimit = 50
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
import { response } from "../location"
|
||||
|
||||
export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ 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"
|
||||
import { PermissionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { response } from "../location"
|
||||
|
||||
function missingRequest(id: PermissionV2.ID) {
|
||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` })
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { Git } from "@opencode-ai/core/git"
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { ProjectCopyError } from "../groups/project-copy"
|
||||
import { ProjectCopyError } from "@opencode-ai/protocol/groups/project-copy"
|
||||
|
||||
export const ProjectCopyHandler = HttpApiBuilder.group(Api, "server.projectCopy", (handlers) =>
|
||||
Effect.succeed(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { Catalog } from "@opencode-ai/core/catalog"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { ProviderNotFoundError } from "../errors"
|
||||
import { response } from "../groups/location"
|
||||
import { ProviderNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { response } from "../location"
|
||||
|
||||
export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -8,9 +8,13 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|||
import * as Socket from "effect/unstable/socket/Socket"
|
||||
import { Api } from "../api"
|
||||
import { CorsConfig, isAllowedRequestOrigin } from "../cors"
|
||||
import { ForbiddenError, PtyNotFoundError } from "../errors"
|
||||
import { PTY_CONNECT_TICKET_QUERY, PTY_CONNECT_TOKEN_HEADER, PTY_CONNECT_TOKEN_HEADER_VALUE } from "../groups/pty"
|
||||
import { response } from "../groups/location"
|
||||
import { ForbiddenError, PtyNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import {
|
||||
PTY_CONNECT_TICKET_QUERY,
|
||||
PTY_CONNECT_TOKEN_HEADER,
|
||||
PTY_CONNECT_TOKEN_HEADER_VALUE,
|
||||
} from "@opencode-ai/protocol/groups/pty"
|
||||
import { response } from "../location"
|
||||
import { PtyEnvironment } from "../pty-environment"
|
||||
|
||||
const ticketScope = Effect.gen(function* () {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ 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"
|
||||
import { QuestionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import { response } from "../location"
|
||||
|
||||
function missingRequest(id: QuestionV2.ID) {
|
||||
return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` })
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Reference } from "@opencode-ai/core/reference"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
import { response } from "../location"
|
||||
|
||||
export const ReferenceHandler = HttpApiBuilder.group(Api, "server.reference", (handlers) =>
|
||||
handlers.handle("reference.list", () => response(Reference.Service.use((reference) => reference.list()))),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||
import { DateTime, Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { SessionsCursor } from "../groups/session"
|
||||
import { SessionsCursor } from "@opencode-ai/protocol/groups/session"
|
||||
import {
|
||||
ConflictError,
|
||||
InvalidCursorError,
|
||||
|
|
@ -10,7 +10,7 @@ import {
|
|||
ServiceUnavailableError,
|
||||
SessionNotFoundError,
|
||||
UnknownError,
|
||||
} from "../errors"
|
||||
} from "@opencode-ai/protocol/errors"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
|
||||
const DefaultSessionsLimit = 50
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { response } from "../groups/location"
|
||||
import { response } from "../location"
|
||||
|
||||
export const SkillHandler = HttpApiBuilder.group(Api, "server.skill", (handlers) =>
|
||||
handlers.handle("skill.list", () => response(SkillV2.Service.use((skill) => skill.list()))),
|
||||
|
|
|
|||
|
|
@ -1,35 +1,16 @@
|
|||
import { Location } from "@opencode-ai/core/location"
|
||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { Effect, Layer, Schema } from "effect"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpServerRequest } from "effect/unstable/http"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, OpenApi } from "effect/unstable/httpapi"
|
||||
import { HttpApiMiddleware } 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: "LocationQuery" })
|
||||
export type LocationServices = Layer.Success<ReturnType<(typeof LocationServiceMap)["get"]>>
|
||||
|
||||
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 class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware, { provides: LocationServices }>()(
|
||||
"@opencode/HttpApiLocation",
|
||||
) {}
|
||||
|
||||
export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
||||
return Effect.gen(function* () {
|
||||
|
|
@ -45,32 +26,6 @@ export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
|||
})
|
||||
}
|
||||
|
||||
export type LocationServices = Layer.Success<ReturnType<typeof LocationServiceMap.get>>
|
||||
|
||||
export class LocationMiddleware extends HttpApiMiddleware.Service<
|
||||
LocationMiddleware,
|
||||
{
|
||||
provides: LocationServices
|
||||
}
|
||||
>()("@opencode/HttpApiLocation") {}
|
||||
|
||||
export const LocationGroup = HttpApiGroup.make("server.location")
|
||||
.add(
|
||||
HttpApiEndpoint.get("location.get", "/api/location", {
|
||||
query: LocationQuery,
|
||||
success: Location.Info,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "v2.location.get",
|
||||
summary: "Get location",
|
||||
description: "Resolve the requested location or the server default location.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.middleware(LocationMiddleware)
|
||||
|
||||
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"]
|
||||
|
|
@ -1,17 +1,14 @@
|
|||
import { ServerAuth } from "../auth"
|
||||
import { UnauthorizedError } from "../errors"
|
||||
import { hasPtyConnectTicketURL } from "../groups/pty"
|
||||
import { UnauthorizedError } from "@opencode-ai/protocol/errors"
|
||||
import { Authorization } from "@opencode-ai/protocol/middleware/authorization"
|
||||
export { Authorization } from "@opencode-ai/protocol/middleware/authorization"
|
||||
import { hasPtyConnectTicketURL } from "@opencode-ai/protocol/groups/pty"
|
||||
import { Effect, Encoding, Layer, Redacted } from "effect"
|
||||
import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
|
||||
const AUTH_TOKEN_QUERY = "auth_token"
|
||||
const WWW_AUTHENTICATE = 'Basic realm="Secure Area"'
|
||||
|
||||
export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/HttpApiAuthorization", {
|
||||
error: UnauthorizedError,
|
||||
}) {}
|
||||
|
||||
function emptyCredential() {
|
||||
return { username: "", password: Redacted.make("") }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Effect } from "effect"
|
||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||
import { InvalidRequestError } from "../errors"
|
||||
import { InvalidRequestError } from "@opencode-ai/protocol/errors"
|
||||
import { SchemaErrorMiddleware } from "@opencode-ai/protocol/middleware/schema-error"
|
||||
export { SchemaErrorMiddleware } from "@opencode-ai/protocol/middleware/schema-error"
|
||||
|
||||
const REASON_LIMIT = 1024
|
||||
|
||||
|
|
@ -9,11 +11,6 @@ function truncateReason(reason: string) {
|
|||
return reason.slice(0, REASON_LIMIT) + `... (${reason.length - REASON_LIMIT} more chars)`
|
||||
}
|
||||
|
||||
export class SchemaErrorMiddleware extends HttpApiMiddleware.Service<SchemaErrorMiddleware>()(
|
||||
"@opencode/HttpApiSchemaError",
|
||||
{ error: InvalidRequestError },
|
||||
) {}
|
||||
|
||||
export const schemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrorMiddleware, (error) => {
|
||||
const reason = truncateReason(error.cause.message)
|
||||
return Effect.logWarning("schema rejection").pipe(
|
||||
|
|
|
|||
|
|
@ -9,14 +9,12 @@ 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"
|
||||
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||
import type { LocationServices } from "../location"
|
||||
|
||||
export class SessionLocationMiddleware extends HttpApiMiddleware.Service<
|
||||
SessionLocationMiddleware,
|
||||
{
|
||||
provides: LocationServices
|
||||
}
|
||||
{ provides: LocationServices }
|
||||
>()("@opencode/HttpApiSessionLocation", {
|
||||
error: [InvalidRequestError, SessionNotFoundError],
|
||||
}) {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue