refactor(protocol): extract server contracts (#33708)
This commit is contained in:
@@ -539,6 +539,7 @@
|
|||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
@@ -656,6 +657,18 @@
|
|||||||
"@opentui/solid",
|
"@opentui/solid",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"packages/protocol": {
|
||||||
|
"name": "@opencode-ai/protocol",
|
||||||
|
"dependencies": {
|
||||||
|
"@opencode-ai/schema": "workspace:*",
|
||||||
|
"effect": "catalog:",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/bun": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/schema": {
|
"packages/schema": {
|
||||||
"name": "@opencode-ai/schema",
|
"name": "@opencode-ai/schema",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -697,6 +710,7 @@
|
|||||||
"version": "1.17.10",
|
"version": "1.17.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:",
|
"effect": "catalog:",
|
||||||
},
|
},
|
||||||
@@ -1863,6 +1877,8 @@
|
|||||||
|
|
||||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
"@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"],
|
||||||
|
|
||||||
|
"@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"],
|
||||||
|
|
||||||
"@opencode-ai/schema": ["@opencode-ai/schema@workspace:packages/schema"],
|
"@opencode-ai/schema": ["@opencode-ai/schema@workspace:packages/schema"],
|
||||||
|
|
||||||
"@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"],
|
"@opencode-ai/script": ["@opencode-ai/script@workspace:packages/script"],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FSUtil } from "./fs-util"
|
|||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { PositiveInt, RelativePath } from "./schema"
|
import { PositiveInt, RelativePath } from "./schema"
|
||||||
import { FileSystemSearch } from "./filesystem/search"
|
import { FileSystemSearch } from "./filesystem/search"
|
||||||
import { Entry, FileSystem, Match } from "@opencode-ai/schema/filesystem"
|
import { Entry, FileSystem, FindInput, Match } from "@opencode-ai/schema/filesystem"
|
||||||
export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
|
export { Entry, Match, Submatch } from "@opencode-ai/schema/filesystem"
|
||||||
|
|
||||||
export const ReadInput = Schema.Struct({
|
export const ReadInput = Schema.Struct({
|
||||||
@@ -28,11 +28,7 @@ export const ListInput = Schema.Struct({
|
|||||||
})
|
})
|
||||||
export type ListInput = typeof ListInput.Type
|
export type ListInput = typeof ListInput.Type
|
||||||
|
|
||||||
export class FindInput extends Schema.Class<FindInput>("FileSystem.FindInput")({
|
export { FindInput }
|
||||||
query: Schema.String,
|
|
||||||
type: Schema.Literals(["file", "directory"]).pipe(Schema.optional),
|
|
||||||
limit: PositiveInt.pipe(Schema.optional),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class GlobInput extends Schema.Class<GlobInput>("FileSystem.GlobInput")({
|
export class GlobInput extends Schema.Class<GlobInput>("FileSystem.GlobInput")({
|
||||||
pattern: Schema.String,
|
pattern: Schema.String,
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ import {
|
|||||||
} from "effect"
|
} from "effect"
|
||||||
import { Integration } from "@opencode-ai/schema/integration"
|
import { Integration } from "@opencode-ai/schema/integration"
|
||||||
import { Credential } from "./credential"
|
import { Credential } from "./credential"
|
||||||
import { withStatics } from "./schema"
|
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
import { Identifier } from "./util/identifier"
|
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { IntegrationConnection } from "./integration/connection"
|
import { IntegrationConnection } from "./integration/connection"
|
||||||
|
|
||||||
@@ -28,10 +26,7 @@ export type ID = Integration.ID
|
|||||||
export const MethodID = Integration.MethodID
|
export const MethodID = Integration.MethodID
|
||||||
export type MethodID = Integration.MethodID
|
export type MethodID = Integration.MethodID
|
||||||
|
|
||||||
export const AttemptID = Schema.String.pipe(
|
export const AttemptID = Integration.AttemptID
|
||||||
Schema.brand("Integration.AttemptID"),
|
|
||||||
withStatics((schema) => ({ create: () => schema.make("con_" + Identifier.ascending()) })),
|
|
||||||
)
|
|
||||||
export type AttemptID = typeof AttemptID.Type
|
export type AttemptID = typeof AttemptID.Type
|
||||||
|
|
||||||
export const When = Integration.When
|
export const When = Integration.When
|
||||||
@@ -58,12 +53,8 @@ export type EnvMethod = Integration.EnvMethod
|
|||||||
export const Method = Integration.Method
|
export const Method = Integration.Method
|
||||||
export type Method = Integration.Method
|
export type Method = Integration.Method
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("Integration.Info")({
|
export const Info = Integration.Info
|
||||||
id: ID,
|
export type Info = Integration.Info
|
||||||
name: Schema.String,
|
|
||||||
methods: Schema.mutable(Schema.Array(Method)),
|
|
||||||
connections: Schema.mutable(Schema.Array(IntegrationConnection.Info)),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export const Inputs = Integration.Inputs
|
export const Inputs = Integration.Inputs
|
||||||
export type Inputs = Integration.Inputs
|
export type Inputs = Integration.Inputs
|
||||||
@@ -102,28 +93,10 @@ export interface EnvImplementation {
|
|||||||
|
|
||||||
export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation
|
export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation
|
||||||
|
|
||||||
export class Attempt extends Schema.Class<Attempt>("Integration.Attempt")({
|
export const Attempt = Integration.Attempt
|
||||||
attemptID: AttemptID,
|
export type Attempt = Integration.Attempt
|
||||||
url: Schema.String,
|
|
||||||
instructions: Schema.String,
|
|
||||||
mode: Schema.Literals(["auto", "code"]),
|
|
||||||
time: Schema.Struct({
|
|
||||||
created: Schema.Number,
|
|
||||||
expires: Schema.Number,
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
const Time = Schema.Struct({
|
export const AttemptStatus = Integration.AttemptStatus
|
||||||
created: Schema.Number,
|
|
||||||
expires: Schema.Number,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const AttemptStatus = Schema.Union([
|
|
||||||
Schema.Struct({ status: Schema.Literal("pending"), time: Time }),
|
|
||||||
Schema.Struct({ status: Schema.Literal("complete"), time: Time }),
|
|
||||||
Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: Time }),
|
|
||||||
Schema.Struct({ status: Schema.Literal("expired"), time: Time }),
|
|
||||||
]).pipe(Schema.toTaggedUnion("status"))
|
|
||||||
export type AttemptStatus = typeof AttemptStatus.Type
|
export type AttemptStatus = typeof AttemptStatus.Type
|
||||||
|
|
||||||
export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Integration.CodeRequired", {
|
export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Integration.CodeRequired", {
|
||||||
|
|||||||
@@ -1,30 +1,15 @@
|
|||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer } from "effect"
|
||||||
import { Ref } from "@opencode-ai/schema/location"
|
import { Info, Ref, response } from "@opencode-ai/schema/location"
|
||||||
import { Project } from "./project"
|
import { Project } from "./project"
|
||||||
import { AbsolutePath, optionalOmitUndefined } from "./schema"
|
|
||||||
import { WorkspaceV2 } from "./workspace"
|
|
||||||
|
|
||||||
export * as Location from "./location"
|
export * as Location from "./location"
|
||||||
|
|
||||||
export { Ref }
|
export { Info, Ref, response }
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("Location.Info")({
|
|
||||||
directory: AbsolutePath,
|
|
||||||
workspaceID: optionalOmitUndefined(WorkspaceV2.ID),
|
|
||||||
project: Schema.Struct({
|
|
||||||
id: Project.ID,
|
|
||||||
directory: AbsolutePath,
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export interface Interface extends Info {
|
export interface Interface extends Info {
|
||||||
readonly vcs?: Project.Vcs
|
readonly vcs?: Project.Vcs
|
||||||
}
|
}
|
||||||
|
|
||||||
export function response<S extends Schema.Top>(data: S) {
|
|
||||||
return Schema.Struct({ location: Info, data })
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Location") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/Location") {}
|
||||||
|
|
||||||
export const layer = (ref: Ref) =>
|
export const layer = (ref: Ref) =>
|
||||||
|
|||||||
@@ -4,22 +4,13 @@ import { eq } from "drizzle-orm"
|
|||||||
import { Context, Effect, Layer, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
import { ProjectV2 } from "../project"
|
import { ProjectV2 } from "../project"
|
||||||
import { withStatics } from "../schema"
|
|
||||||
import { Identifier } from "../util/identifier"
|
|
||||||
import { PermissionTable } from "./sql"
|
import { PermissionTable } from "./sql"
|
||||||
|
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(
|
export const ID = PermissionSaved.ID
|
||||||
Schema.brand("PermissionSaved.ID"),
|
|
||||||
withStatics((schema) => ({ create: () => schema.make("psv_" + Identifier.ascending()) })),
|
|
||||||
)
|
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
export const Info = Schema.Struct({
|
export const Info = PermissionSaved.Info
|
||||||
id: ID,
|
|
||||||
projectID: ProjectV2.ID,
|
|
||||||
action: Schema.String,
|
|
||||||
resource: Schema.String,
|
|
||||||
}).annotate({ identifier: "PermissionSaved.Info" })
|
|
||||||
export type Info = typeof Info.Type
|
export type Info = typeof Info.Type
|
||||||
|
|
||||||
export const ListInput = Schema.Struct({
|
export const ListInput = Schema.Struct({
|
||||||
|
|||||||
@@ -14,24 +14,15 @@ import { EventV2 } from "../event"
|
|||||||
import { Database } from "../database/database"
|
import { Database } from "../database/database"
|
||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { ProjectDirectoriesEvent } from "@opencode-ai/schema/project-directories"
|
import { ProjectDirectoriesEvent } from "@opencode-ai/schema/project-directories"
|
||||||
|
import { ProjectCopy } from "@opencode-ai/schema/project-copy"
|
||||||
|
|
||||||
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
|
export const StrategyID = ProjectCopy.StrategyID
|
||||||
export type StrategyID = typeof StrategyID.Type
|
export type StrategyID = typeof StrategyID.Type
|
||||||
|
|
||||||
export const CreateInput = Schema.Struct({
|
export const CreateInput = ProjectCopy.CreateInput
|
||||||
projectID: Project.ID,
|
|
||||||
strategy: StrategyID,
|
|
||||||
sourceDirectory: AbsolutePath,
|
|
||||||
directory: AbsolutePath,
|
|
||||||
name: Schema.optional(Schema.String),
|
|
||||||
}).annotate({ identifier: "ProjectCopy.CreateInput" })
|
|
||||||
export type CreateInput = typeof CreateInput.Type
|
export type CreateInput = typeof CreateInput.Type
|
||||||
|
|
||||||
export const RemoveInput = Schema.Struct({
|
export const RemoveInput = ProjectCopy.RemoveInput
|
||||||
projectID: Project.ID,
|
|
||||||
directory: AbsolutePath,
|
|
||||||
force: Schema.Boolean,
|
|
||||||
}).annotate({ identifier: "ProjectCopy.RemoveInput" })
|
|
||||||
export type RemoveInput = typeof RemoveInput.Type
|
export type RemoveInput = typeof RemoveInput.Type
|
||||||
|
|
||||||
export const RefreshInput = Schema.Struct({
|
export const RefreshInput = Schema.Struct({
|
||||||
@@ -45,9 +36,7 @@ export const RefreshResult = Schema.Struct({
|
|||||||
}).annotate({ identifier: "ProjectCopy.RefreshResult" })
|
}).annotate({ identifier: "ProjectCopy.RefreshResult" })
|
||||||
export type RefreshResult = typeof RefreshResult.Type
|
export type RefreshResult = typeof RefreshResult.Type
|
||||||
|
|
||||||
export const Copy = Schema.Struct({
|
export const Copy = ProjectCopy.Copy
|
||||||
directory: AbsolutePath,
|
|
||||||
}).annotate({ identifier: "ProjectCopy.Copy" })
|
|
||||||
export type Copy = typeof Copy.Type
|
export type Copy = typeof Copy.Type
|
||||||
|
|
||||||
export const ListEntry = Schema.Struct({
|
export const ListEntry = Schema.Struct({
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ export * as Pty from "./pty"
|
|||||||
|
|
||||||
import type { Disp, Proc } from "#pty"
|
import type { Disp, Proc } from "#pty"
|
||||||
import { Context, Effect, Layer, Schema, Types } from "effect"
|
import { Context, Effect, Layer, Schema, Types } from "effect"
|
||||||
import { PtyEvent, PtyInfo } from "@opencode-ai/schema/pty"
|
import { PtyEvent, PtyInfo, Pty } from "@opencode-ai/schema/pty"
|
||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
import { Location } from "./location"
|
import { Location } from "./location"
|
||||||
import { PositiveInt } from "./schema"
|
|
||||||
import { PtyID } from "./pty/schema"
|
import { PtyID } from "./pty/schema"
|
||||||
import { Shell } from "./shell"
|
import { Shell } from "./shell"
|
||||||
import { lazy } from "./util/lazy"
|
import { lazy } from "./util/lazy"
|
||||||
@@ -40,25 +39,11 @@ export const Info = PtyInfo
|
|||||||
|
|
||||||
export type Info = Types.DeepMutable<typeof Info.Type>
|
export type Info = Types.DeepMutable<typeof Info.Type>
|
||||||
|
|
||||||
export const CreateInput = Schema.Struct({
|
export const CreateInput = Pty.CreateInput
|
||||||
command: Schema.optional(Schema.String),
|
|
||||||
args: Schema.optional(Schema.Array(Schema.String)),
|
|
||||||
cwd: Schema.optional(Schema.String),
|
|
||||||
title: Schema.optional(Schema.String),
|
|
||||||
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type CreateInput = Types.DeepMutable<typeof CreateInput.Type>
|
export type CreateInput = Types.DeepMutable<typeof CreateInput.Type>
|
||||||
|
|
||||||
export const UpdateInput = Schema.Struct({
|
export const UpdateInput = Pty.UpdateInput
|
||||||
title: Schema.optional(Schema.String),
|
|
||||||
size: Schema.optional(
|
|
||||||
Schema.Struct({
|
|
||||||
rows: PositiveInt,
|
|
||||||
cols: PositiveInt,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type UpdateInput = Types.DeepMutable<typeof UpdateInput.Type>
|
export type UpdateInput = Types.DeepMutable<typeof UpdateInput.Type>
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
export * as PtyTicket from "./ticket"
|
export * as PtyTicket from "./ticket"
|
||||||
|
|
||||||
import { WorkspaceV2 } from "../workspace"
|
import { WorkspaceV2 } from "../workspace"
|
||||||
import { PositiveInt } from "../schema"
|
import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
|
||||||
import { PtyID } from "./schema"
|
import { PtyID } from "./schema"
|
||||||
import { Cache, Context, Duration, Effect, Layer, Schema } from "effect"
|
import { Cache, Context, Duration, Effect, Layer } from "effect"
|
||||||
import { LayerNode } from "../effect/layer-node"
|
import { LayerNode } from "../effect/layer-node"
|
||||||
|
|
||||||
const DEFAULT_TTL = Duration.seconds(60)
|
const DEFAULT_TTL = Duration.seconds(60)
|
||||||
const CAPACITY = 10_000
|
const CAPACITY = 10_000
|
||||||
|
|
||||||
export const ConnectToken = Schema.Struct({
|
export const ConnectToken = PtyTicket.ConnectToken
|
||||||
ticket: Schema.String,
|
|
||||||
expires_in: PositiveInt,
|
|
||||||
})
|
|
||||||
|
|
||||||
export type Scope = {
|
export type Scope = {
|
||||||
readonly ptyID: PtyID
|
readonly ptyID: PtyID
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * as Reference from "./reference"
|
export * as Reference from "./reference"
|
||||||
|
|
||||||
import { Context, Effect, Layer, Schema, Scope, Types } from "effect"
|
import { Context, Effect, Layer, Scope, Types } from "effect"
|
||||||
import { Reference } from "@opencode-ai/schema/reference"
|
import { Reference } from "@opencode-ai/schema/reference"
|
||||||
import { Global } from "./global"
|
import { Global } from "./global"
|
||||||
import { EventV2 } from "./event"
|
import { EventV2 } from "./event"
|
||||||
@@ -20,13 +20,8 @@ export type Source = Reference.Source
|
|||||||
|
|
||||||
export const Event = Reference.Event
|
export const Event = Reference.Event
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("Reference.Info")({
|
export const Info = Reference.Info
|
||||||
name: Schema.String,
|
export type Info = Reference.Info
|
||||||
path: AbsolutePath,
|
|
||||||
description: Schema.String.pipe(Schema.optional),
|
|
||||||
hidden: Schema.Boolean.pipe(Schema.optional),
|
|
||||||
source: Source,
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
type Data = {
|
type Data = {
|
||||||
sources: Map<string, Types.DeepMutable<Source>>
|
sources: Map<string, Types.DeepMutable<Source>>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@
|
|||||||
"@openauthjs/openauth": "catalog:",
|
"@openauthjs/openauth": "catalog:",
|
||||||
"@opencode-ai/llm": "workspace:*",
|
"@opencode-ai/llm": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
"@opencode-ai/script": "workspace:*",
|
"@opencode-ai/script": "workspace:*",
|
||||||
"@opencode-ai/sdk": "workspace:*",
|
"@opencode-ai/sdk": "workspace:*",
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import { SessionApi } from "./groups/session"
|
|||||||
import { SyncApi } from "./groups/sync"
|
import { SyncApi } from "./groups/sync"
|
||||||
import { TuiApi } from "./groups/tui"
|
import { TuiApi } from "./groups/tui"
|
||||||
import { WorkspaceApi } from "./groups/workspace"
|
import { WorkspaceApi } from "./groups/workspace"
|
||||||
import { makeApi } from "@opencode-ai/server/api"
|
import { makeApi } from "@opencode-ai/protocol/api"
|
||||||
|
import { LocationMiddleware } from "@opencode-ai/server/location"
|
||||||
|
import { SessionLocationMiddleware } from "@opencode-ai/server/middleware/session-location"
|
||||||
import { GlobalApi } from "./groups/global"
|
import { GlobalApi } from "./groups/global"
|
||||||
import { Authorization } from "./middleware/authorization"
|
import { Authorization } from "./middleware/authorization"
|
||||||
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
||||||
@@ -43,7 +45,11 @@ const EventSchema = Schema.Union([
|
|||||||
InstanceDisposed,
|
InstanceDisposed,
|
||||||
]).annotate({ identifier: "Event" })
|
]).annotate({ identifier: "Event" })
|
||||||
|
|
||||||
export const ServerApi = makeApi(EventManifest.Latest.values().toArray())
|
export const ServerApi = makeApi({
|
||||||
|
definitions: EventManifest.Latest.values().toArray(),
|
||||||
|
locationMiddleware: LocationMiddleware,
|
||||||
|
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||||
|
})
|
||||||
|
|
||||||
export const RootHttpApi = HttpApi.make("opencode-root")
|
export const RootHttpApi = HttpApi.make("opencode-root")
|
||||||
.addHttpApi(ControlApi)
|
.addHttpApi(ControlApi)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
SessionPaths,
|
SessionPaths,
|
||||||
} from "../../src/server/routes/instance/httpapi/groups/session"
|
} from "../../src/server/routes/instance/httpapi/groups/session"
|
||||||
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty"
|
||||||
import { SessionMessagesQuery } from "@opencode-ai/server/groups/message"
|
import { SessionMessagesQuery } from "@opencode-ai/protocol/groups/message"
|
||||||
import { QueryBoolean, QueryBooleanOpenApi } from "../../src/server/routes/instance/httpapi/groups/query"
|
import { QueryBoolean, QueryBooleanOpenApi } from "../../src/server/routes/instance/httpapi/groups/query"
|
||||||
import { resetDatabase } from "../fixture/db"
|
import { resetDatabase } from "../fixture/db"
|
||||||
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
import { disposeAllInstances, tmpdir } from "../fixture/fixture"
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"name": "@opencode-ai/protocol",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"exports": {
|
||||||
|
"./*": "./src/*.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsgo --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@opencode-ai/schema": "workspace:*",
|
||||||
|
"effect": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/bun": "catalog:",
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"@typescript/native-preview": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Context } from "effect"
|
||||||
|
import { HttpApi, HttpApiGroup, HttpApiMiddleware, 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 { makeSessionGroup } from "./groups/session"
|
||||||
|
import { makePermissionGroup } 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/schema/event"
|
||||||
|
import { AgentGroup } from "./groups/agent"
|
||||||
|
import { HealthGroup } from "./groups/health"
|
||||||
|
import { PtyGroup } from "./groups/pty"
|
||||||
|
import { makeQuestionGroup } 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"
|
||||||
|
|
||||||
|
// Protocol owns middleware placement, while Server injects concrete keys so Core service identities stay downstream.
|
||||||
|
const makeApiFromGroup = <
|
||||||
|
const Group extends HttpApiGroup.Any,
|
||||||
|
LocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
LocationService,
|
||||||
|
SessionLocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
SessionLocationService,
|
||||||
|
>(
|
||||||
|
eventGroup: Group,
|
||||||
|
locationMiddleware: Context.Key<LocationId, LocationService>,
|
||||||
|
sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>,
|
||||||
|
) =>
|
||||||
|
HttpApi.make("server")
|
||||||
|
.add(HealthGroup)
|
||||||
|
.add(LocationGroup.middleware(locationMiddleware))
|
||||||
|
.add(AgentGroup.middleware(locationMiddleware))
|
||||||
|
.add(makeSessionGroup(sessionLocationMiddleware))
|
||||||
|
.add(MessageGroup.middleware(sessionLocationMiddleware))
|
||||||
|
.add(ModelGroup.middleware(locationMiddleware))
|
||||||
|
.add(ProviderGroup.middleware(locationMiddleware))
|
||||||
|
.add(IntegrationGroup.middleware(locationMiddleware))
|
||||||
|
.add(CredentialGroup.middleware(locationMiddleware))
|
||||||
|
.add(makePermissionGroup(locationMiddleware, sessionLocationMiddleware))
|
||||||
|
.add(FileSystemGroup.middleware(locationMiddleware))
|
||||||
|
.add(CommandGroup.middleware(locationMiddleware))
|
||||||
|
.add(SkillGroup.middleware(locationMiddleware))
|
||||||
|
.add(eventGroup)
|
||||||
|
.add(PtyGroup.middleware(locationMiddleware))
|
||||||
|
.add(makeQuestionGroup(locationMiddleware, sessionLocationMiddleware))
|
||||||
|
.add(ReferenceGroup.middleware(locationMiddleware))
|
||||||
|
.add(ProjectCopyGroup.middleware(locationMiddleware))
|
||||||
|
.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 = <
|
||||||
|
LocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
LocationService,
|
||||||
|
SessionLocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
SessionLocationService,
|
||||||
|
>(options: {
|
||||||
|
readonly definitions: ReadonlyArray<Definition>
|
||||||
|
readonly locationMiddleware: Context.Key<LocationId, LocationService>
|
||||||
|
readonly sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>
|
||||||
|
}) =>
|
||||||
|
makeApiFromGroup(makeEventGroup(options.definitions), options.locationMiddleware, options.sessionLocationMiddleware)
|
||||||
|
|
||||||
|
export const makeDefaultApi = <
|
||||||
|
LocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
LocationService,
|
||||||
|
SessionLocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
SessionLocationService,
|
||||||
|
>(options: {
|
||||||
|
readonly locationMiddleware: Context.Key<LocationId, LocationService>
|
||||||
|
readonly sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>
|
||||||
|
}) => makeApiFromGroup(EventGroup, options.locationMiddleware, options.sessionLocationMiddleware)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Agent } from "@opencode-ai/schema/agent"
|
||||||
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
|
export const AgentGroup = HttpApiGroup.make("server.agent").add(
|
||||||
|
HttpApiEndpoint.get("agent.list", "/api/agent", {
|
||||||
|
query: LocationQuery,
|
||||||
|
success: Location.response(Schema.Array(Agent.Info)),
|
||||||
|
})
|
||||||
|
.annotateMerge(locationQueryOpenApi)
|
||||||
|
.annotateMerge(
|
||||||
|
OpenApi.annotations({
|
||||||
|
identifier: "v2.agent.list",
|
||||||
|
summary: "List agents",
|
||||||
|
description: "Retrieve currently registered agents.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { CommandV2 } from "@opencode-ai/core/command"
|
import { Command } from "@opencode-ai/schema/command"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
export const CommandGroup = HttpApiGroup.make("server.command")
|
export const CommandGroup = HttpApiGroup.make("server.command")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("command.list", "/api/command", {
|
HttpApiEndpoint.get("command.list", "/api/command", {
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: Location.response(Schema.Array(CommandV2.Info)),
|
success: Location.response(Schema.Array(Command.Info)),
|
||||||
})
|
})
|
||||||
.annotateMerge(locationQueryOpenApi)
|
.annotateMerge(locationQueryOpenApi)
|
||||||
.annotateMerge(
|
.annotateMerge(
|
||||||
@@ -25,4 +25,3 @@ export const CommandGroup = HttpApiGroup.make("server.command")
|
|||||||
description: "Experimental command routes.",
|
description: "Experimental command routes.",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
+2
-3
@@ -1,7 +1,7 @@
|
|||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/schema/credential"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { LocationMiddleware, LocationQuery, locationQueryOpenApi } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
export const CredentialGroup = HttpApiGroup.make("server.credential")
|
export const CredentialGroup = HttpApiGroup.make("server.credential")
|
||||||
.add(
|
.add(
|
||||||
@@ -35,4 +35,3 @@ export const CredentialGroup = HttpApiGroup.make("server.credential")
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Event } from "@opencode-ai/schema/event"
|
||||||
|
import { EventManifest } from "@opencode-ai/schema/event-manifest"
|
||||||
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
|
import type { Definition } from "@opencode-ai/schema/event"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
id: Event.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 = (definitions: ReadonlyArray<Definition>) =>
|
||||||
|
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" })
|
||||||
|
|
||||||
|
const make = (definitions: ReadonlyArray<Definition>) => {
|
||||||
|
const EventSchema = schema(definitions)
|
||||||
|
return {
|
||||||
|
schema: EventSchema,
|
||||||
|
group: 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 const makeEventGroup = (definitions: ReadonlyArray<Definition>) => make(definitions).group
|
||||||
|
|
||||||
|
const event = make(EventManifest.ServerDefinitions)
|
||||||
|
export const EventGroup = event.group
|
||||||
|
export type Event = typeof event.schema.Type
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
import { FileSystem } from "@opencode-ai/schema/filesystem"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { PositiveInt, RelativePath } from "@opencode-ai/core/schema"
|
import { PositiveInt, RelativePath } from "@opencode-ai/schema/schema"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
const ListQuery = Schema.Struct({
|
const ListQuery = Schema.Struct({
|
||||||
...LocationQuery.fields,
|
...LocationQuery.fields,
|
||||||
@@ -66,4 +66,3 @@ export const FileSystemGroup = HttpApiGroup.make("server.fs")
|
|||||||
description: "Experimental location-scoped filesystem routes.",
|
description: "Experimental location-scoped filesystem routes.",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
+3
-4
@@ -1,9 +1,9 @@
|
|||||||
import { Integration } from "@opencode-ai/core/integration"
|
import { Integration } from "@opencode-ai/schema/integration"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { InvalidRequestError } from "../errors"
|
import { InvalidRequestError } from "../errors"
|
||||||
import { LocationMiddleware, LocationQuery, locationQueryOpenApi } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
const Inputs = Schema.Record(Schema.String, Schema.String)
|
const Inputs = Schema.Record(Schema.String, Schema.String)
|
||||||
|
|
||||||
@@ -128,4 +128,3 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
|||||||
.annotateMerge(
|
.annotateMerge(
|
||||||
OpenApi.annotations({ title: "integrations", description: "Integration discovery and authentication routes." }),
|
OpenApi.annotations({ title: "integrations", description: "Integration discovery and authentication routes." }),
|
||||||
)
|
)
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
|
||||||
|
export const LocationQuery = Schema.Struct({
|
||||||
|
location: Schema.optional(
|
||||||
|
Schema.Struct({
|
||||||
|
directory: Schema.optional(Schema.String),
|
||||||
|
workspace: Schema.optional(Schema.String),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}).annotate({ identifier: "LocationQuery" })
|
||||||
|
|
||||||
|
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 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.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors"
|
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors"
|
||||||
import { SessionLocationMiddleware } from "../middleware/session-location"
|
|
||||||
|
|
||||||
export const SessionMessagesQuery = Schema.Struct({
|
export const SessionMessagesQuery = Schema.Struct({
|
||||||
limit: Schema.optional(
|
limit: Schema.optional(
|
||||||
@@ -25,7 +24,7 @@ export const SessionMessagesQuery = Schema.Struct({
|
|||||||
export const MessageGroup = HttpApiGroup.make("server.message")
|
export const MessageGroup = HttpApiGroup.make("server.message")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("session.messages", "/api/session/:sessionID/message", {
|
HttpApiEndpoint.get("session.messages", "/api/session/:sessionID/message", {
|
||||||
params: { sessionID: SessionV2.ID },
|
params: { sessionID: Session.ID },
|
||||||
query: SessionMessagesQuery,
|
query: SessionMessagesQuery,
|
||||||
success: Schema.Struct({
|
success: Schema.Struct({
|
||||||
data: Schema.Array(SessionMessage.Message),
|
data: Schema.Array(SessionMessage.Message),
|
||||||
@@ -35,16 +34,14 @@ export const MessageGroup = HttpApiGroup.make("server.message")
|
|||||||
}),
|
}),
|
||||||
}).annotate({ identifier: "SessionMessagesResponse" }),
|
}).annotate({ identifier: "SessionMessagesResponse" }),
|
||||||
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
|
error: [InvalidCursorError, SessionNotFoundError, UnknownError],
|
||||||
})
|
}).annotateMerge(
|
||||||
.middleware(SessionLocationMiddleware)
|
OpenApi.annotations({
|
||||||
.annotateMerge(
|
identifier: "v2.session.messages",
|
||||||
OpenApi.annotations({
|
summary: "Get session messages",
|
||||||
identifier: "v2.session.messages",
|
description:
|
||||||
summary: "Get session messages",
|
"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.",
|
||||||
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(
|
.annotateMerge(
|
||||||
OpenApi.annotations({
|
OpenApi.annotations({
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { Model } from "@opencode-ai/schema/model"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { ServiceUnavailableError } from "../errors"
|
import { ServiceUnavailableError } from "../errors"
|
||||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
export const ModelGroup = HttpApiGroup.make("server.model")
|
export const ModelGroup = HttpApiGroup.make("server.model")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("model.list", "/api/model", {
|
HttpApiEndpoint.get("model.list", "/api/model", {
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: Location.response(Schema.Array(ModelV2.Info)),
|
success: Location.response(Schema.Array(Model.Info)),
|
||||||
error: ServiceUnavailableError,
|
error: ServiceUnavailableError,
|
||||||
})
|
})
|
||||||
.annotateMerge(locationQueryOpenApi)
|
.annotateMerge(locationQueryOpenApi)
|
||||||
@@ -27,4 +27,3 @@ export const ModelGroup = HttpApiGroup.make("server.model")
|
|||||||
description: "Experimental model routes.",
|
description: "Experimental model routes.",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Permission } from "@opencode-ai/schema/permission"
|
||||||
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
|
import { PermissionSaved } from "@opencode-ai/schema/permission-saved"
|
||||||
|
import { Project } from "@opencode-ai/schema/project"
|
||||||
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
|
import { Context, Schema } from "effect"
|
||||||
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { PermissionNotFoundError, SessionNotFoundError } from "../errors"
|
||||||
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
|
export const makePermissionGroup = <
|
||||||
|
LocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
LocationService,
|
||||||
|
SessionLocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
SessionLocationService,
|
||||||
|
>(
|
||||||
|
locationMiddleware: Context.Key<LocationId, LocationService>,
|
||||||
|
sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>,
|
||||||
|
) =>
|
||||||
|
HttpApiGroup.make("server.permission")
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("permission.request.list", "/api/permission/request", {
|
||||||
|
query: LocationQuery,
|
||||||
|
success: Location.response(Schema.Array(Permission.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: Project.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.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Effect applies group middleware only to endpoints already added; session endpoints use session placement below.
|
||||||
|
.middleware(locationMiddleware)
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("session.permission.list", "/api/session/:sessionID/permission", {
|
||||||
|
params: { sessionID: Session.ID },
|
||||||
|
success: Schema.Struct({ data: Schema.Array(Permission.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: Session.ID, requestID: Permission.ID },
|
||||||
|
payload: Schema.Struct({
|
||||||
|
reply: Permission.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." }))
|
||||||
+6
-7
@@ -1,8 +1,8 @@
|
|||||||
import { ProjectCopy } from "@opencode-ai/core/project/copy"
|
import { ProjectCopy } from "@opencode-ai/schema/project-copy"
|
||||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/schema/project"
|
||||||
import { Schema, Struct } from "effect"
|
import { Schema, Struct } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { LocationMiddleware, LocationQuery, locationQueryOpenApi } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
const root = "/experimental/project/:projectID/copy"
|
const root = "/experimental/project/:projectID/copy"
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ const RemovePayload = Schema.Struct(Struct.omit(ProjectCopy.RemoveInput.fields,
|
|||||||
export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("projectCopy.create", root, {
|
HttpApiEndpoint.post("projectCopy.create", root, {
|
||||||
params: { projectID: ProjectV2.ID },
|
params: { projectID: Project.ID },
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
payload: CreatePayload,
|
payload: CreatePayload,
|
||||||
success: ProjectCopy.Copy,
|
success: ProjectCopy.Copy,
|
||||||
@@ -34,7 +34,7 @@ export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
|||||||
)
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.delete("projectCopy.remove", root, {
|
HttpApiEndpoint.delete("projectCopy.remove", root, {
|
||||||
params: { projectID: ProjectV2.ID },
|
params: { projectID: Project.ID },
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
payload: RemovePayload,
|
payload: RemovePayload,
|
||||||
success: HttpApiSchema.NoContent,
|
success: HttpApiSchema.NoContent,
|
||||||
@@ -45,7 +45,7 @@ export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
|||||||
)
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("projectCopy.refresh", `${root}/refresh`, {
|
HttpApiEndpoint.post("projectCopy.refresh", `${root}/refresh`, {
|
||||||
params: { projectID: ProjectV2.ID },
|
params: { projectID: Project.ID },
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: HttpApiSchema.NoContent,
|
success: HttpApiSchema.NoContent,
|
||||||
error: ProjectCopyError,
|
error: ProjectCopyError,
|
||||||
@@ -54,4 +54,3 @@ export const ProjectCopyGroup = HttpApiGroup.make("server.projectCopy")
|
|||||||
.annotateMerge(OpenApi.annotations({ identifier: "v2.projectCopy.refresh" })),
|
.annotateMerge(OpenApi.annotations({ identifier: "v2.projectCopy.refresh" })),
|
||||||
)
|
)
|
||||||
.annotateMerge(OpenApi.annotations({ title: "projectCopy", description: "Project copy management routes." }))
|
.annotateMerge(OpenApi.annotations({ title: "projectCopy", description: "Project copy management routes." }))
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { Provider } from "@opencode-ai/schema/provider"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { ProviderNotFoundError, ServiceUnavailableError } from "../errors"
|
import { ProviderNotFoundError, ServiceUnavailableError } from "../errors"
|
||||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
export const ProviderGroup = HttpApiGroup.make("server.provider")
|
export const ProviderGroup = HttpApiGroup.make("server.provider")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("provider.list", "/api/provider", {
|
HttpApiEndpoint.get("provider.list", "/api/provider", {
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: Location.response(Schema.Array(ProviderV2.Info)),
|
success: Location.response(Schema.Array(Provider.Info)),
|
||||||
error: ServiceUnavailableError,
|
error: ServiceUnavailableError,
|
||||||
})
|
})
|
||||||
.annotateMerge(locationQueryOpenApi)
|
.annotateMerge(locationQueryOpenApi)
|
||||||
@@ -23,9 +23,9 @@ export const ProviderGroup = HttpApiGroup.make("server.provider")
|
|||||||
)
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("provider.get", "/api/provider/:providerID", {
|
HttpApiEndpoint.get("provider.get", "/api/provider/:providerID", {
|
||||||
params: { providerID: ProviderV2.ID },
|
params: { providerID: Provider.ID },
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: Location.response(ProviderV2.Info),
|
success: Location.response(Provider.Info),
|
||||||
error: [ProviderNotFoundError, ServiceUnavailableError],
|
error: [ProviderNotFoundError, ServiceUnavailableError],
|
||||||
})
|
})
|
||||||
.annotateMerge(locationQueryOpenApi)
|
.annotateMerge(locationQueryOpenApi)
|
||||||
@@ -43,4 +43,3 @@ export const ProviderGroup = HttpApiGroup.make("server.provider")
|
|||||||
description: "Experimental provider routes.",
|
description: "Experimental provider routes.",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Pty } from "@opencode-ai/core/pty"
|
import { Pty } from "@opencode-ai/schema/pty"
|
||||||
import { PtyID } from "@opencode-ai/core/pty/schema"
|
import { PtyTicket } from "@opencode-ai/schema/pty-ticket"
|
||||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { ForbiddenError, PtyNotFoundError } from "../errors"
|
import { ForbiddenError, PtyNotFoundError } from "../errors"
|
||||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
export const PTY_CONNECT_TICKET_QUERY = "ticket"
|
export const PTY_CONNECT_TICKET_QUERY = "ticket"
|
||||||
export const PTY_CONNECT_TOKEN_HEADER = "x-opencode-ticket"
|
export const PTY_CONNECT_TOKEN_HEADER = "x-opencode-ticket"
|
||||||
@@ -51,7 +50,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
|||||||
)
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("pty.get", "/api/pty/:ptyID", {
|
HttpApiEndpoint.get("pty.get", "/api/pty/:ptyID", {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: Pty.ID },
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: Location.response(Pty.Info),
|
success: Location.response(Pty.Info),
|
||||||
error: PtyNotFoundError,
|
error: PtyNotFoundError,
|
||||||
@@ -67,7 +66,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
|||||||
)
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.put("pty.update", "/api/pty/:ptyID", {
|
HttpApiEndpoint.put("pty.update", "/api/pty/:ptyID", {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: Pty.ID },
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
payload: Pty.UpdateInput,
|
payload: Pty.UpdateInput,
|
||||||
success: Location.response(Pty.Info),
|
success: Location.response(Pty.Info),
|
||||||
@@ -84,7 +83,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
|||||||
)
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.delete("pty.remove", "/api/pty/:ptyID", {
|
HttpApiEndpoint.delete("pty.remove", "/api/pty/:ptyID", {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: Pty.ID },
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: HttpApiSchema.NoContent,
|
success: HttpApiSchema.NoContent,
|
||||||
error: PtyNotFoundError,
|
error: PtyNotFoundError,
|
||||||
@@ -100,7 +99,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
|||||||
)
|
)
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.post("pty.connectToken", "/api/pty/:ptyID/connect-token", {
|
HttpApiEndpoint.post("pty.connectToken", "/api/pty/:ptyID/connect-token", {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: Pty.ID },
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: Location.response(PtyTicket.ConnectToken),
|
success: Location.response(PtyTicket.ConnectToken),
|
||||||
error: [ForbiddenError, PtyNotFoundError],
|
error: [ForbiddenError, PtyNotFoundError],
|
||||||
@@ -118,7 +117,7 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
|||||||
// Query fields are decoded in the raw handler after the existence check so a missing
|
// 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.
|
// session responds with an empty 404 before any upgrade work.
|
||||||
HttpApiEndpoint.get("pty.connect", "/api/pty/:ptyID/connect", {
|
HttpApiEndpoint.get("pty.connect", "/api/pty/:ptyID/connect", {
|
||||||
params: { ptyID: PtyID },
|
params: { ptyID: Pty.ID },
|
||||||
success: Schema.Boolean,
|
success: Schema.Boolean,
|
||||||
error: [ForbiddenError, PtyNotFoundError],
|
error: [ForbiddenError, PtyNotFoundError],
|
||||||
}).annotateMerge(
|
}).annotateMerge(
|
||||||
@@ -141,4 +140,3 @@ export const PtyGroup = HttpApiGroup.make("server.pty")
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.annotateMerge(OpenApi.annotations({ title: "pty", description: "Experimental location-scoped PTY routes." }))
|
.annotateMerge(OpenApi.annotations({ title: "pty", description: "Experimental location-scoped PTY routes." }))
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Question } from "@opencode-ai/schema/question"
|
||||||
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
|
import { Context, Schema } from "effect"
|
||||||
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import { QuestionNotFoundError, SessionNotFoundError } from "../errors"
|
||||||
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
|
export const makeQuestionGroup = <
|
||||||
|
LocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
LocationService,
|
||||||
|
SessionLocationId extends HttpApiMiddleware.AnyId,
|
||||||
|
SessionLocationService,
|
||||||
|
>(
|
||||||
|
locationMiddleware: Context.Key<LocationId, LocationService>,
|
||||||
|
sessionLocationMiddleware: Context.Key<SessionLocationId, SessionLocationService>,
|
||||||
|
) =>
|
||||||
|
HttpApiGroup.make("server.question")
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("question.request.list", "/api/question/request", {
|
||||||
|
query: LocationQuery,
|
||||||
|
success: Location.response(Schema.Array(Question.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." }))
|
||||||
|
// Effect applies group middleware only to endpoints already added; session endpoints use session placement below.
|
||||||
|
.middleware(locationMiddleware)
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("session.question.list", "/api/session/:sessionID/question", {
|
||||||
|
params: { sessionID: Session.ID },
|
||||||
|
success: Schema.Struct({ data: Schema.Array(Question.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: Session.ID, requestID: Question.ID },
|
||||||
|
payload: Question.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: Session.ID, requestID: Question.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,8 +1,8 @@
|
|||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Reference } from "@opencode-ai/core/reference"
|
import { Reference } from "@opencode-ai/schema/reference"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { LocationMiddleware, LocationQuery, locationQueryOpenApi } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
export const ReferenceGroup = HttpApiGroup.make("server.reference")
|
export const ReferenceGroup = HttpApiGroup.make("server.reference")
|
||||||
.add(
|
.add(
|
||||||
@@ -25,4 +25,3 @@ export const ReferenceGroup = HttpApiGroup.make("server.reference")
|
|||||||
description: "Location-scoped project references.",
|
description: "Location-scoped project references.",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
|
import { SessionInput } from "@opencode-ai/schema/session-input"
|
||||||
|
import { Prompt } from "@opencode-ai/schema/prompt"
|
||||||
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
|
import { Project } from "@opencode-ai/schema/project"
|
||||||
|
import { AbsolutePath, PositiveInt, RelativePath, withStatics } from "@opencode-ai/schema/schema"
|
||||||
|
import { Workspace } from "@opencode-ai/schema/workspace"
|
||||||
|
import { Context, Encoding, Result, Schema, Struct } from "effect"
|
||||||
|
import { HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||||
|
import {
|
||||||
|
ConflictError,
|
||||||
|
InvalidCursorError,
|
||||||
|
InvalidRequestError,
|
||||||
|
MessageNotFoundError,
|
||||||
|
ServiceUnavailableError,
|
||||||
|
SessionNotFoundError,
|
||||||
|
UnknownError,
|
||||||
|
} from "../errors"
|
||||||
|
import { Agent } from "@opencode-ai/schema/agent"
|
||||||
|
import { Model } from "@opencode-ai/schema/model"
|
||||||
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
|
import { Revert } from "@opencode-ai/schema/revert"
|
||||||
|
|
||||||
|
const SessionsQueryFields = {
|
||||||
|
workspace: Workspace.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: Project.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: Session.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.bind(schema)
|
||||||
|
return {
|
||||||
|
make: (input: typeof SessionsCursorInput.Type) => make(Encoding.encodeBase64Url(encodeSessionsCursor(input))),
|
||||||
|
parse: (input: string) => decodeSessionsCursor(Result.getOrThrow(Encoding.decodeBase64UrlString(input))),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
export type SessionsCursor = typeof SessionsCursor.Type
|
||||||
|
|
||||||
|
const SessionsQueryCursor = SessionsCursor.annotate({
|
||||||
|
description: "Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response.",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const SessionsQuery = Schema.Struct({
|
||||||
|
...SessionsQueryFields,
|
||||||
|
directory: AbsolutePath.pipe(Schema.optional),
|
||||||
|
project: Project.ID.pipe(Schema.optional),
|
||||||
|
subpath: RelativePath.pipe(Schema.optional),
|
||||||
|
cursor: SessionsQueryCursor.pipe(Schema.optional),
|
||||||
|
}).annotate({ identifier: "SessionsQuery" })
|
||||||
|
|
||||||
|
export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLocationMiddleware: Context.Key<I, S>) =>
|
||||||
|
HttpApiGroup.make("server.session")
|
||||||
|
.add(
|
||||||
|
HttpApiEndpoint.get("session.list", "/api/session", {
|
||||||
|
query: SessionsQuery,
|
||||||
|
success: Schema.Struct({
|
||||||
|
data: Schema.Array(Session.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: Session.ID.pipe(Schema.optional),
|
||||||
|
agent: Agent.ID.pipe(Schema.optional),
|
||||||
|
model: Model.Ref.pipe(Schema.optional),
|
||||||
|
location: Location.Ref.pipe(Schema.optional),
|
||||||
|
}),
|
||||||
|
success: Schema.Struct({ data: Session.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: Session.ID },
|
||||||
|
success: Schema.Struct({ data: Session.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: Session.ID },
|
||||||
|
payload: Schema.Struct({ agent: Agent.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: Session.ID },
|
||||||
|
payload: Schema.Struct({ model: Model.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: Session.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: Session.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: Session.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: Session.ID },
|
||||||
|
payload: Schema.Struct({ messageID: SessionMessage.ID, files: Schema.Boolean.pipe(Schema.optional) }),
|
||||||
|
success: Schema.Struct({ data: Revert.State }),
|
||||||
|
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: Session.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: Session.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: Session.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,14 +1,14 @@
|
|||||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
import { Skill } from "@opencode-ai/schema/skill"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/schema/location"
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
||||||
import { LocationQuery, locationQueryOpenApi, LocationMiddleware } from "./location"
|
import { LocationQuery, locationQueryOpenApi } from "./location"
|
||||||
|
|
||||||
export const SkillGroup = HttpApiGroup.make("server.skill")
|
export const SkillGroup = HttpApiGroup.make("server.skill")
|
||||||
.add(
|
.add(
|
||||||
HttpApiEndpoint.get("skill.list", "/api/skill", {
|
HttpApiEndpoint.get("skill.list", "/api/skill", {
|
||||||
query: LocationQuery,
|
query: LocationQuery,
|
||||||
success: Location.response(Schema.Array(SkillV2.Info)),
|
success: Location.response(Schema.Array(Skill.Info)),
|
||||||
})
|
})
|
||||||
.annotateMerge(locationQueryOpenApi)
|
.annotateMerge(locationQueryOpenApi)
|
||||||
.annotateMerge(
|
.annotateMerge(
|
||||||
@@ -25,4 +25,3 @@ export const SkillGroup = HttpApiGroup.make("server.skill")
|
|||||||
description: "Experimental skill routes.",
|
description: "Experimental skill routes.",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.middleware(LocationMiddleware)
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||||
|
import { UnauthorizedError } from "../errors"
|
||||||
|
|
||||||
|
export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/HttpApiAuthorization", {
|
||||||
|
error: UnauthorizedError,
|
||||||
|
}) {}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||||
|
import { InvalidRequestError } from "../errors"
|
||||||
|
|
||||||
|
export class SchemaErrorMiddleware extends HttpApiMiddleware.Service<SchemaErrorMiddleware>()(
|
||||||
|
"@opencode/HttpApiSchemaError",
|
||||||
|
{ error: InvalidRequestError },
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { SessionsCursor } from "../src/groups/session"
|
||||||
|
import { Session } from "@opencode-ai/schema/session"
|
||||||
|
|
||||||
|
describe("SessionsCursor", () => {
|
||||||
|
test("round trips without Node globals", async () => {
|
||||||
|
const input = {
|
||||||
|
workspace: undefined,
|
||||||
|
search: "protocol",
|
||||||
|
order: "desc" as const,
|
||||||
|
anchor: { id: Session.ID.make("ses_test"), time: 1, direction: "next" as const },
|
||||||
|
}
|
||||||
|
const cursor = SessionsCursor.make(input)
|
||||||
|
|
||||||
|
expect(await Effect.runPromise(SessionsCursor.parse(cursor))).toEqual(input)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/tsconfig",
|
||||||
|
"extends": "@tsconfig/bun/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"noUncheckedIndexedAccess": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export * as Credential from "./credential"
|
export * as Credential from "./credential"
|
||||||
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { Integration } from "./integration"
|
import { IntegrationMethodID } from "./integration-id"
|
||||||
import { ascending } from "./identifier"
|
import { ascending } from "./identifier"
|
||||||
import { NonNegativeInt, withStatics } from "./schema"
|
import { NonNegativeInt, withStatics } from "./schema"
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export type ID = typeof ID.Type
|
|||||||
export interface OAuth extends Schema.Schema.Type<typeof OAuth> {}
|
export interface OAuth extends Schema.Schema.Type<typeof OAuth> {}
|
||||||
export const OAuth = Schema.Struct({
|
export const OAuth = Schema.Struct({
|
||||||
type: Schema.Literal("oauth"),
|
type: Schema.Literal("oauth"),
|
||||||
methodID: Integration.MethodID,
|
methodID: IntegrationMethodID,
|
||||||
refresh: Schema.String,
|
refresh: Schema.String,
|
||||||
access: Schema.String,
|
access: Schema.String,
|
||||||
expires: NonNegativeInt,
|
expires: NonNegativeInt,
|
||||||
|
|||||||
@@ -31,3 +31,9 @@ export const Match = Schema.Struct({
|
|||||||
text: Schema.String,
|
text: Schema.String,
|
||||||
submatches: Schema.Array(Submatch),
|
submatches: Schema.Array(Submatch),
|
||||||
}).annotate({ identifier: "FileSystem.Match" })
|
}).annotate({ identifier: "FileSystem.Match" })
|
||||||
|
|
||||||
|
export class FindInput extends Schema.Class<FindInput>("FileSystem.FindInput")({
|
||||||
|
query: Schema.String,
|
||||||
|
type: Schema.Literals(["file", "directory"]).pipe(Schema.optional),
|
||||||
|
limit: PositiveInt.pipe(Schema.optional),
|
||||||
|
}) {}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ export { LLM } from "./llm"
|
|||||||
export { Location } from "./location"
|
export { Location } from "./location"
|
||||||
export { Model } from "./model"
|
export { Model } from "./model"
|
||||||
export { Permission } from "./permission"
|
export { Permission } from "./permission"
|
||||||
|
export { PermissionSaved } from "./permission-saved"
|
||||||
export { Project } from "./project"
|
export { Project } from "./project"
|
||||||
|
export { ProjectCopy } from "./project-copy"
|
||||||
export { Provider } from "./provider"
|
export { Provider } from "./provider"
|
||||||
export { Reference } from "./reference"
|
export { Reference } from "./reference"
|
||||||
export { Revert } from "./revert"
|
export { Revert } from "./revert"
|
||||||
@@ -17,6 +19,9 @@ export { Session } from "./session"
|
|||||||
export { SessionInput } from "./session-input"
|
export { SessionInput } from "./session-input"
|
||||||
export { SessionMessage } from "./session-message"
|
export { SessionMessage } from "./session-message"
|
||||||
export { Skill } from "./skill"
|
export { Skill } from "./skill"
|
||||||
|
export { Pty } from "./pty"
|
||||||
|
export { PtyTicket } from "./pty-ticket"
|
||||||
|
export { Question } from "./question"
|
||||||
export { Workspace } from "./workspace"
|
export { Workspace } from "./workspace"
|
||||||
export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt"
|
export { Prompt, Source, FileAttachment, AgentAttachment } from "./prompt"
|
||||||
export * from "./schema"
|
export * from "./schema"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
|
export const IntegrationID = Schema.String.pipe(Schema.brand("Integration.ID"))
|
||||||
|
export type IntegrationID = typeof IntegrationID.Type
|
||||||
|
|
||||||
|
export const IntegrationMethodID = Schema.String.pipe(Schema.brand("Integration.MethodID"))
|
||||||
|
export type IntegrationMethodID = typeof IntegrationMethodID.Type
|
||||||
@@ -2,11 +2,15 @@ export * as Integration from "./integration"
|
|||||||
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { define, inventory } from "./event"
|
import { define, inventory } from "./event"
|
||||||
|
import { Connection } from "./connection"
|
||||||
|
import { ascending } from "./identifier"
|
||||||
|
import { withStatics } from "./schema"
|
||||||
|
import { IntegrationID, IntegrationMethodID } from "./integration-id"
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(Schema.brand("Integration.ID"))
|
export const ID = IntegrationID
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
export const MethodID = Schema.String.pipe(Schema.brand("Integration.MethodID"))
|
export const MethodID = IntegrationMethodID
|
||||||
export type MethodID = typeof MethodID.Type
|
export type MethodID = typeof MethodID.Type
|
||||||
|
|
||||||
export interface When extends Schema.Schema.Type<typeof When> {}
|
export interface When extends Schema.Schema.Type<typeof When> {}
|
||||||
@@ -88,3 +92,37 @@ export const Ref = Schema.Struct({
|
|||||||
id: ID,
|
id: ID,
|
||||||
name: Schema.String,
|
name: Schema.String,
|
||||||
}).annotate({ identifier: "Integration.Ref" })
|
}).annotate({ identifier: "Integration.Ref" })
|
||||||
|
|
||||||
|
export class Info extends Schema.Class<Info>("Integration.Info")({
|
||||||
|
id: ID,
|
||||||
|
name: Schema.String,
|
||||||
|
methods: Schema.mutable(Schema.Array(Method)),
|
||||||
|
connections: Schema.mutable(Schema.Array(Connection.Info)),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export const AttemptID = Schema.String.pipe(
|
||||||
|
Schema.brand("Integration.AttemptID"),
|
||||||
|
withStatics((schema) => ({ create: () => schema.make("con_" + ascending()) })),
|
||||||
|
)
|
||||||
|
export type AttemptID = typeof AttemptID.Type
|
||||||
|
|
||||||
|
const AttemptTime = Schema.Struct({
|
||||||
|
created: Schema.Number,
|
||||||
|
expires: Schema.Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
export class Attempt extends Schema.Class<Attempt>("Integration.Attempt")({
|
||||||
|
attemptID: AttemptID,
|
||||||
|
url: Schema.String,
|
||||||
|
instructions: Schema.String,
|
||||||
|
mode: Schema.Literals(["auto", "code"]),
|
||||||
|
time: AttemptTime,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export const AttemptStatus = Schema.Union([
|
||||||
|
Schema.Struct({ status: Schema.Literal("pending"), time: AttemptTime }),
|
||||||
|
Schema.Struct({ status: Schema.Literal("complete"), time: AttemptTime }),
|
||||||
|
Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: AttemptTime }),
|
||||||
|
Schema.Struct({ status: Schema.Literal("expired"), time: AttemptTime }),
|
||||||
|
]).pipe(Schema.toTaggedUnion("status"))
|
||||||
|
export type AttemptStatus = typeof AttemptStatus.Type
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export * as Location from "./location"
|
export * as Location from "./location"
|
||||||
|
|
||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { AbsolutePath } from "./schema"
|
import { AbsolutePath, optionalOmitUndefined } from "./schema"
|
||||||
|
import { ProjectID } from "./project-id"
|
||||||
import { WorkspaceID } from "./workspace-id"
|
import { WorkspaceID } from "./workspace-id"
|
||||||
|
|
||||||
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
|
export interface Ref extends Schema.Schema.Type<typeof Ref> {}
|
||||||
@@ -12,3 +13,16 @@ export const Ref = Schema.Struct({
|
|||||||
Schema.withConstructorDefault(Effect.succeed(undefined)),
|
Schema.withConstructorDefault(Effect.succeed(undefined)),
|
||||||
),
|
),
|
||||||
}).annotate({ identifier: "Location.Ref" })
|
}).annotate({ identifier: "Location.Ref" })
|
||||||
|
|
||||||
|
export class Info extends Schema.Class<Info>("Location.Info")({
|
||||||
|
directory: AbsolutePath,
|
||||||
|
workspaceID: optionalOmitUndefined(WorkspaceID),
|
||||||
|
project: Schema.Struct({
|
||||||
|
id: ProjectID,
|
||||||
|
directory: AbsolutePath,
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export function response<S extends Schema.Top>(data: S) {
|
||||||
|
return Schema.Struct({ location: Info, data })
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export * as PermissionSaved from "./permission-saved"
|
||||||
|
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { ascending } from "./identifier"
|
||||||
|
import { ProjectID } from "./project-id"
|
||||||
|
import { withStatics } from "./schema"
|
||||||
|
|
||||||
|
export const ID = Schema.String.pipe(
|
||||||
|
Schema.brand("PermissionSaved.ID"),
|
||||||
|
withStatics((schema) => ({ create: () => schema.make("psv_" + ascending()) })),
|
||||||
|
)
|
||||||
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
|
export const Info = Schema.Struct({
|
||||||
|
id: ID,
|
||||||
|
projectID: ProjectID,
|
||||||
|
action: Schema.String,
|
||||||
|
resource: Schema.String,
|
||||||
|
}).annotate({ identifier: "PermissionSaved.Info" })
|
||||||
|
export type Info = typeof Info.Type
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export * as ProjectCopy from "./project-copy"
|
||||||
|
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { ProjectID } from "./project-id"
|
||||||
|
import { AbsolutePath } from "./schema"
|
||||||
|
|
||||||
|
export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID"))
|
||||||
|
export type StrategyID = typeof StrategyID.Type
|
||||||
|
|
||||||
|
export const CreateInput = Schema.Struct({
|
||||||
|
projectID: ProjectID,
|
||||||
|
strategy: StrategyID,
|
||||||
|
sourceDirectory: AbsolutePath,
|
||||||
|
directory: AbsolutePath,
|
||||||
|
name: Schema.optional(Schema.String),
|
||||||
|
}).annotate({ identifier: "ProjectCopy.CreateInput" })
|
||||||
|
export type CreateInput = typeof CreateInput.Type
|
||||||
|
|
||||||
|
export const RemoveInput = Schema.Struct({
|
||||||
|
projectID: ProjectID,
|
||||||
|
directory: AbsolutePath,
|
||||||
|
force: Schema.Boolean,
|
||||||
|
}).annotate({ identifier: "ProjectCopy.RemoveInput" })
|
||||||
|
export type RemoveInput = typeof RemoveInput.Type
|
||||||
|
|
||||||
|
export const Copy = Schema.Struct({
|
||||||
|
directory: AbsolutePath,
|
||||||
|
}).annotate({ identifier: "ProjectCopy.Copy" })
|
||||||
|
export type Copy = typeof Copy.Type
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Schema } from "effect"
|
||||||
|
import { withStatics } from "./schema"
|
||||||
|
|
||||||
|
export const ProjectID = Schema.String.pipe(
|
||||||
|
Schema.brand("Project.ID"),
|
||||||
|
withStatics((schema) => ({ global: schema.make("global") })),
|
||||||
|
)
|
||||||
|
export type ProjectID = typeof ProjectID.Type
|
||||||
@@ -2,12 +2,10 @@ export * as Project from "./project"
|
|||||||
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { define, inventory } from "./event"
|
import { define, inventory } from "./event"
|
||||||
import { NonNegativeInt, optionalOmitUndefined, withStatics } from "./schema"
|
import { NonNegativeInt, optionalOmitUndefined } from "./schema"
|
||||||
|
import { ProjectID } from "./project-id"
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(
|
export const ID = ProjectID
|
||||||
Schema.brand("Project.ID"),
|
|
||||||
withStatics((schema) => ({ global: schema.make("global") })),
|
|
||||||
)
|
|
||||||
export type ID = typeof ID.Type
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
export const Vcs = Schema.Literal("git")
|
export const Vcs = Schema.Literal("git")
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export * as PtyTicket from "./pty-ticket"
|
||||||
|
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { PositiveInt } from "./schema"
|
||||||
|
|
||||||
|
export const ConnectToken = Schema.Struct({
|
||||||
|
ticket: Schema.String,
|
||||||
|
expires_in: PositiveInt,
|
||||||
|
})
|
||||||
@@ -3,7 +3,7 @@ export * as Pty from "./pty"
|
|||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { define, inventory } from "./event"
|
import { define, inventory } from "./event"
|
||||||
import { ascending } from "./identifier"
|
import { ascending } from "./identifier"
|
||||||
import { NonNegativeInt } from "./schema"
|
import { NonNegativeInt, PositiveInt } from "./schema"
|
||||||
import { withStatics } from "./schema"
|
import { withStatics } from "./schema"
|
||||||
|
|
||||||
const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
|
const IDSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID"))
|
||||||
@@ -33,3 +33,23 @@ const Exited = define({ type: "pty.exited", schema: { id: ID, exitCode: NonNegat
|
|||||||
const Deleted = define({ type: "pty.deleted", schema: { id: ID } })
|
const Deleted = define({ type: "pty.deleted", schema: { id: ID } })
|
||||||
export const Event = { Created, Updated, Exited, Deleted, Definitions: inventory(Created, Updated, Exited, Deleted) }
|
export const Event = { Created, Updated, Exited, Deleted, Definitions: inventory(Created, Updated, Exited, Deleted) }
|
||||||
export const PtyEvent = Event
|
export const PtyEvent = Event
|
||||||
|
|
||||||
|
export const CreateInput = Schema.Struct({
|
||||||
|
command: Schema.optional(Schema.String),
|
||||||
|
args: Schema.optional(Schema.Array(Schema.String)),
|
||||||
|
cwd: Schema.optional(Schema.String),
|
||||||
|
title: Schema.optional(Schema.String),
|
||||||
|
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||||
|
})
|
||||||
|
export type CreateInput = typeof CreateInput.Type
|
||||||
|
|
||||||
|
export const UpdateInput = Schema.Struct({
|
||||||
|
title: Schema.optional(Schema.String),
|
||||||
|
size: Schema.optional(
|
||||||
|
Schema.Struct({
|
||||||
|
rows: PositiveInt,
|
||||||
|
cols: PositiveInt,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
export type UpdateInput = typeof UpdateInput.Type
|
||||||
|
|||||||
@@ -26,3 +26,11 @@ export const GitSource = Schema.Struct({
|
|||||||
|
|
||||||
export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type"))
|
export const Source = Schema.Union([LocalSource, GitSource]).pipe(Schema.toTaggedUnion("type"))
|
||||||
export type Source = typeof Source.Type
|
export type Source = typeof Source.Type
|
||||||
|
|
||||||
|
export class Info extends Schema.Class<Info>("Reference.Info")({
|
||||||
|
name: Schema.String,
|
||||||
|
path: AbsolutePath,
|
||||||
|
description: Schema.String.pipe(Schema.optional),
|
||||||
|
hidden: Schema.Boolean.pipe(Schema.optional),
|
||||||
|
source: Source,
|
||||||
|
}) {}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { FileSystem } from "../src/filesystem"
|
||||||
|
|
||||||
|
describe("schema compatibility", () => {
|
||||||
|
test("moved class schemas remain constructible", () => {
|
||||||
|
const input = new FileSystem.FindInput({ query: "src" })
|
||||||
|
expect(input).toBeInstanceOf(FileSystem.FindInput)
|
||||||
|
expect(input.query).toBe("src")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"drizzle-orm": "catalog:",
|
"drizzle-orm": "catalog:",
|
||||||
"effect": "catalog:"
|
"effect": "catalog:"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,56 +1,8 @@
|
|||||||
import { HttpApi, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
import { makeDefaultApi } from "@opencode-ai/protocol/api"
|
||||||
import { SchemaErrorMiddleware } from "./middleware/schema-error"
|
import { LocationMiddleware } from "./location"
|
||||||
import { MessageGroup } from "./groups/message"
|
import { SessionLocationMiddleware } from "./middleware/session-location"
|
||||||
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"
|
|
||||||
|
|
||||||
const makeApiFromGroup = <const Group extends HttpApiGroup.Any>(eventGroup: Group) =>
|
export const Api = makeDefaultApi({
|
||||||
HttpApi.make("server")
|
locationMiddleware: LocationMiddleware,
|
||||||
.add(HealthGroup)
|
sessionLocationMiddleware: SessionLocationMiddleware,
|
||||||
.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)
|
|
||||||
|
|||||||
@@ -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,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,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,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,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.",
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -3,7 +3,7 @@ import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
|||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
import { PtyTicket } from "@opencode-ai/core/pty/ticket"
|
||||||
import { Layer } from "effect"
|
import { Layer } from "effect"
|
||||||
import { layer as locationLayer } from "./groups/location"
|
import { layer as locationLayer } from "./location"
|
||||||
import { sessionLocationLayer } from "./middleware/session-location"
|
import { sessionLocationLayer } from "./middleware/session-location"
|
||||||
import { MessageHandler } from "./handlers/message"
|
import { MessageHandler } from "./handlers/message"
|
||||||
import { ModelHandler } from "./handlers/model"
|
import { ModelHandler } from "./handlers/model"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { AgentV2 } from "@opencode-ai/core/agent"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
|
export const AgentHandler = HttpApiBuilder.group(Api, "server.agent", (handlers) =>
|
||||||
handlers.handle("agent.list", () =>
|
handlers.handle("agent.list", () =>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { CommandV2 } from "@opencode-ai/core/command"
|
import { CommandV2 } from "@opencode-ai/core/command"
|
||||||
import { Effect } from "effect"
|
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
export const CommandHandler = HttpApiBuilder.group(Api, "server.command", (handlers) =>
|
export const CommandHandler = HttpApiBuilder.group(Api, "server.command", (handlers) =>
|
||||||
handlers.handle("command.list", () => response(CommandV2.Service.use((command) => command.list()))),
|
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 { HttpServerResponse } from "effect/unstable/http"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handlers) =>
|
export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Integration } from "@opencode-ai/core/integration"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { InvalidRequestError } from "../errors"
|
import { InvalidRequestError } from "@opencode-ai/protocol/errors"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
const authorize = <A, R>(effect: Effect.Effect<A, Integration.AuthorizationError, R>) =>
|
const authorize = <A, R>(effect: Effect.Effect<A, Integration.AuthorizationError, R>) =>
|
||||||
effect.pipe(
|
effect.pipe(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { SessionV2 } from "@opencode-ai/core/session"
|
|||||||
import { Effect, Schema } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../errors"
|
import { InvalidCursorError, SessionNotFoundError, UnknownError } from "@opencode-ai/protocol/errors"
|
||||||
|
|
||||||
const DefaultMessagesLimit = 50
|
const DefaultMessagesLimit = 50
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Catalog } from "@opencode-ai/core/catalog"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
|
export const ModelHandler = HttpApiBuilder.group(Api, "server.model", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { PermissionNotFoundError } from "../errors"
|
import { PermissionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
function missingRequest(id: PermissionV2.ID) {
|
function missingRequest(id: PermissionV2.ID) {
|
||||||
return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${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 { Effect } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
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) =>
|
export const ProjectCopyHandler = HttpApiBuilder.group(Api, "server.projectCopy", (handlers) =>
|
||||||
Effect.succeed(
|
Effect.succeed(
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { ProviderNotFoundError } from "../errors"
|
import { ProviderNotFoundError } from "@opencode-ai/protocol/errors"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) =>
|
export const ProviderHandler = HttpApiBuilder.group(Api, "server.provider", (handlers) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
|||||||
import * as Socket from "effect/unstable/socket/Socket"
|
import * as Socket from "effect/unstable/socket/Socket"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { CorsConfig, isAllowedRequestOrigin } from "../cors"
|
import { CorsConfig, isAllowedRequestOrigin } from "../cors"
|
||||||
import { ForbiddenError, PtyNotFoundError } from "../errors"
|
import { ForbiddenError, PtyNotFoundError } from "@opencode-ai/protocol/errors"
|
||||||
import { PTY_CONNECT_TICKET_QUERY, PTY_CONNECT_TOKEN_HEADER, PTY_CONNECT_TOKEN_HEADER_VALUE } from "../groups/pty"
|
import {
|
||||||
import { response } from "../groups/location"
|
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"
|
import { PtyEnvironment } from "../pty-environment"
|
||||||
|
|
||||||
const ticketScope = Effect.gen(function* () {
|
const ticketScope = Effect.gen(function* () {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { QuestionV2 } from "@opencode-ai/core/question"
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { QuestionNotFoundError } from "../errors"
|
import { QuestionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
function missingRequest(id: QuestionV2.ID) {
|
function missingRequest(id: QuestionV2.ID) {
|
||||||
return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` })
|
return new QuestionNotFoundError({ requestID: id, message: `Question request not found: ${id}` })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Reference } from "@opencode-ai/core/reference"
|
import { Reference } from "@opencode-ai/core/reference"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
export const ReferenceHandler = HttpApiBuilder.group(Api, "server.reference", (handlers) =>
|
export const ReferenceHandler = HttpApiBuilder.group(Api, "server.reference", (handlers) =>
|
||||||
handlers.handle("reference.list", () => response(Reference.Service.use((reference) => reference.list()))),
|
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 { DateTime, Effect } from "effect"
|
||||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { SessionsCursor } from "../groups/session"
|
import { SessionsCursor } from "@opencode-ai/protocol/groups/session"
|
||||||
import {
|
import {
|
||||||
ConflictError,
|
ConflictError,
|
||||||
InvalidCursorError,
|
InvalidCursorError,
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
ServiceUnavailableError,
|
ServiceUnavailableError,
|
||||||
SessionNotFoundError,
|
SessionNotFoundError,
|
||||||
UnknownError,
|
UnknownError,
|
||||||
} from "../errors"
|
} from "@opencode-ai/protocol/errors"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
|
|
||||||
const DefaultSessionsLimit = 50
|
const DefaultSessionsLimit = 50
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SkillV2 } from "@opencode-ai/core/skill"
|
import { SkillV2 } from "@opencode-ai/core/skill"
|
||||||
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
import { HttpApiBuilder } from "effect/unstable/httpapi"
|
||||||
import { Api } from "../api"
|
import { Api } from "../api"
|
||||||
import { response } from "../groups/location"
|
import { response } from "../location"
|
||||||
|
|
||||||
export const SkillHandler = HttpApiBuilder.group(Api, "server.skill", (handlers) =>
|
export const SkillHandler = HttpApiBuilder.group(Api, "server.skill", (handlers) =>
|
||||||
handlers.handle("skill.list", () => response(SkillV2.Service.use((skill) => skill.list()))),
|
handlers.handle("skill.list", () => response(SkillV2.Service.use((skill) => skill.list()))),
|
||||||
|
|||||||
@@ -1,35 +1,16 @@
|
|||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
import { LocationServiceMap } from "@opencode-ai/core/location-layer"
|
||||||
import { FileSystem } from "@opencode-ai/core/filesystem"
|
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
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 { 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({
|
export type LocationServices = Layer.Success<ReturnType<(typeof LocationServiceMap)["get"]>>
|
||||||
location: Schema.optional(
|
|
||||||
Schema.Struct({
|
|
||||||
directory: Schema.optional(Schema.String),
|
|
||||||
workspace: Schema.optional(Schema.String),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}).annotate({ identifier: "LocationQuery" })
|
|
||||||
|
|
||||||
export const locationQueryOpenApi = OpenApi.annotations({
|
export class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware, { provides: LocationServices }>()(
|
||||||
transform: (operation) => {
|
"@opencode/HttpApiLocation",
|
||||||
const parameters = operation.parameters
|
) {}
|
||||||
if (!Array.isArray(parameters)) return operation
|
|
||||||
return {
|
|
||||||
...operation,
|
|
||||||
parameters: parameters.map((parameter) =>
|
|
||||||
parameter?.name === "location" && parameter?.in === "query"
|
|
||||||
? { ...parameter, style: "deepObject", explode: true }
|
|
||||||
: parameter,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
export function response<A, E, R>(data: Effect.Effect<A, E, R>) {
|
||||||
return Effect.gen(function* () {
|
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 {
|
function ref(request: HttpServerRequest.HttpServerRequest): Location.Ref {
|
||||||
const query = new URL(request.url, "http://localhost").searchParams
|
const query = new URL(request.url, "http://localhost").searchParams
|
||||||
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
|
const workspaceID = query.get("location[workspace]") || request.headers["x-opencode-workspace"]
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
import { ServerAuth } from "../auth"
|
import { ServerAuth } from "../auth"
|
||||||
import { UnauthorizedError } from "../errors"
|
import { UnauthorizedError } from "@opencode-ai/protocol/errors"
|
||||||
import { hasPtyConnectTicketURL } from "../groups/pty"
|
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 { Effect, Encoding, Layer, Redacted } from "effect"
|
||||||
import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
||||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
|
||||||
|
|
||||||
const AUTH_TOKEN_QUERY = "auth_token"
|
const AUTH_TOKEN_QUERY = "auth_token"
|
||||||
const WWW_AUTHENTICATE = 'Basic realm="Secure Area"'
|
const WWW_AUTHENTICATE = 'Basic realm="Secure Area"'
|
||||||
|
|
||||||
export class Authorization extends HttpApiMiddleware.Service<Authorization>()("@opencode/HttpApiAuthorization", {
|
|
||||||
error: UnauthorizedError,
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
function emptyCredential() {
|
function emptyCredential() {
|
||||||
return { username: "", password: Redacted.make("") }
|
return { username: "", password: Redacted.make("") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
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
|
const REASON_LIMIT = 1024
|
||||||
|
|
||||||
@@ -9,11 +11,6 @@ function truncateReason(reason: string) {
|
|||||||
return reason.slice(0, REASON_LIMIT) + `... (${reason.length - REASON_LIMIT} more chars)`
|
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) => {
|
export const schemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrorMiddleware, (error) => {
|
||||||
const reason = truncateReason(error.cause.message)
|
const reason = truncateReason(error.cause.message)
|
||||||
return Effect.logWarning("schema rejection").pipe(
|
return Effect.logWarning("schema rejection").pipe(
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import { eq } from "drizzle-orm"
|
|||||||
import { Effect, Layer, Schema } from "effect"
|
import { Effect, Layer, Schema } from "effect"
|
||||||
import { HttpRouter } from "effect/unstable/http"
|
import { HttpRouter } from "effect/unstable/http"
|
||||||
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
||||||
import { InvalidRequestError, SessionNotFoundError } from "../errors"
|
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
|
||||||
import type { LocationServices } from "../groups/location"
|
import type { LocationServices } from "../location"
|
||||||
|
|
||||||
export class SessionLocationMiddleware extends HttpApiMiddleware.Service<
|
export class SessionLocationMiddleware extends HttpApiMiddleware.Service<
|
||||||
SessionLocationMiddleware,
|
SessionLocationMiddleware,
|
||||||
{
|
{ provides: LocationServices }
|
||||||
provides: LocationServices
|
|
||||||
}
|
|
||||||
>()("@opencode/HttpApiSessionLocation", {
|
>()("@opencode/HttpApiSessionLocation", {
|
||||||
error: [InvalidRequestError, SessionNotFoundError],
|
error: [InvalidRequestError, SessionNotFoundError],
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
Reference in New Issue
Block a user