diff --git a/bun.lock b/bun.lock index 2bb58f67d7..5ead2ee029 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,5 @@ { - "lockfileVersion": 1, + "lockfileVersion": 2, "configVersion": 1, "workspaces": { "": { @@ -87,14 +87,18 @@ "name": "@opencode-ai/cli", "version": "1.15.13", "bin": { - "opencode": "./src/index.ts", + "lildax": "./src/index.ts", }, "dependencies": { "@effect/platform-node": "catalog:", "@opencode-ai/core": "workspace:*", + "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/server": "workspace:*", + "@parcel/watcher": "2.5.1", "effect": "catalog:", }, "devDependencies": { + "@opencode-ai/script": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:", @@ -512,6 +516,7 @@ "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/server": "workspace:*", "@opencode-ai/ui": "workspace:*", "@openrouter/ai-sdk-provider": "2.8.1", "@opentelemetry/api": "1.9.0", @@ -649,6 +654,20 @@ "typescript": "catalog:", }, }, + "packages/server": { + "name": "@opencode-ai/server", + "version": "1.15.13", + "dependencies": { + "@opencode-ai/core": "workspace:*", + "drizzle-orm": "catalog:", + "effect": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, "packages/slack": { "name": "@opencode-ai/slack", "version": "1.15.13", @@ -839,23 +858,23 @@ }, }, "trustedDependencies": [ - "esbuild", "tree-sitter-powershell", - "protobufjs", - "electron", "web-tree-sitter", "tree-sitter-bash", + "esbuild", + "electron", + "protobufjs", ], "patchedDependencies": { - "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "virtua@0.49.1": "patches/virtua@0.49.1.patch", - "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", - "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", + "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", + "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@ai-sdk/xai@3.0.82": "patches/@ai-sdk%2Fxai@3.0.82.patch", - "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", - "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", + "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "gcp-metadata@8.1.2": "patches/gcp-metadata@8.1.2.patch", + "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", }, "overrides": { "@opentui/core": "catalog:", @@ -1729,6 +1748,8 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"], + "@opencode-ai/server": ["@opencode-ai/server@workspace:packages/server"], + "@opencode-ai/slack": ["@opencode-ai/slack@workspace:packages/slack"], "@opencode-ai/stats-app": ["@opencode-ai/stats-app@workspace:packages/stats/app"], diff --git a/packages/cli/package.json b/packages/cli/package.json index 8221953562..bcf1418fb5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -6,7 +6,7 @@ "type": "module", "license": "MIT", "bin": { - "opencode": "./src/index.ts" + "lildax": "./src/index.ts" }, "scripts": { "build": "bun run script/build.ts", @@ -16,9 +16,13 @@ "dependencies": { "@effect/platform-node": "catalog:", "@opencode-ai/core": "workspace:*", + "@opencode-ai/sdk": "workspace:*", + "@opencode-ai/server": "workspace:*", + "@parcel/watcher": "2.5.1", "effect": "catalog:" }, "devDependencies": { + "@opencode-ai/script": "workspace:*", "@tsconfig/bun": "catalog:", "@types/bun": "catalog:", "@typescript/native-preview": "catalog:" diff --git a/packages/cli/script/build.ts b/packages/cli/script/build.ts new file mode 100644 index 0000000000..7b16318ef3 --- /dev/null +++ b/packages/cli/script/build.ts @@ -0,0 +1,92 @@ +#!/usr/bin/env bun + +import { $ } from "bun" +import { rm } from "fs/promises" +import path from "path" +import { Script } from "@opencode-ai/script" +import { modelsData } from "./generate" +import pkg from "../package.json" + +const dir = path.resolve(import.meta.dirname, "..") +const binary = "lildax" +process.chdir(dir) + +await rm("dist", { recursive: true, force: true }) + +const singleFlag = process.argv.includes("--single") +const baselineFlag = process.argv.includes("--baseline") +const skipInstall = process.argv.includes("--skip-install") +const sourcemapsFlag = process.argv.includes("--sourcemaps") + +const allTargets: { + os: string + arch: "arm64" | "x64" + abi?: "musl" + avx2?: false +}[] = [ + { os: "linux", arch: "arm64" }, + { os: "linux", arch: "x64" }, + { os: "linux", arch: "x64", avx2: false }, + { os: "linux", arch: "arm64", abi: "musl" }, + { os: "linux", arch: "x64", abi: "musl" }, + { os: "linux", arch: "x64", abi: "musl", avx2: false }, + { os: "darwin", arch: "arm64" }, + { os: "darwin", arch: "x64" }, + { os: "darwin", arch: "x64", avx2: false }, + { os: "win32", arch: "arm64" }, + { os: "win32", arch: "x64" }, + { os: "win32", arch: "x64", avx2: false }, +] + +const targets = singleFlag + ? allTargets.filter((item) => { + if (item.os !== process.platform || item.arch !== process.arch) return false + if (item.avx2 === false) return baselineFlag + return item.abi === undefined + }) + : allTargets + +if (!skipInstall) await $`bun install --os="*" --cpu="*" @parcel/watcher@${pkg.dependencies["@parcel/watcher"]}` + +for (const item of targets) { + const name = [ + binary, + item.os === "win32" ? "windows" : item.os, + item.arch, + item.avx2 === false ? "baseline" : undefined, + item.abi, + ] + .filter(Boolean) + .join("-") + console.log(`building ${name}`) + const result = await Bun.build({ + entrypoints: ["./src/index.ts"], + tsconfig: "./tsconfig.json", + external: ["node-gyp"], + format: "esm", + minify: true, + sourcemap: sourcemapsFlag ? "linked" : "none", + splitting: true, + compile: { + autoloadBunfig: false, + autoloadDotenv: false, + autoloadTsconfig: true, + autoloadPackageJson: true, + target: name.replace(binary, "bun") as Bun.Build.CompileTarget, + outfile: `./dist/${name}/bin/${binary}`, + execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"], + windows: {}, + }, + define: { + OPENCODE_VERSION: `'${Script.version}'`, + OPENCODE_CLI_NAME: `'${binary}'`, + OPENCODE_MODELS_DEV: modelsData, + OPENCODE_CHANNEL: `'${Script.channel}'`, + OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined", + }, + }) + + if (result.success) continue + for (const log of result.logs) console.error(log) + process.exit(1) +} diff --git a/packages/cli/script/generate.ts b/packages/cli/script/generate.ts new file mode 100644 index 0000000000..d98565e298 --- /dev/null +++ b/packages/cli/script/generate.ts @@ -0,0 +1,7 @@ +const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.dev" + +export const modelsData = process.env.MODELS_DEV_API_JSON + ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() + : await fetch(`${modelsUrl}/api.json`).then((response) => response.text()) + +console.log("Loaded models.dev snapshot") diff --git a/packages/cli/src/api.ts b/packages/cli/src/api.ts deleted file mode 100644 index d4a4a4fa77..0000000000 --- a/packages/cli/src/api.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CliApi } from "./cli-api" - -export const Api = CliApi.make("opencode", { - description: "OpenCode command line interface", - commands: [ - CliApi.make("debug", { - description: "Debugging and troubleshooting tools", - commands: [CliApi.make("agents", { description: "List all agents" })], - }), - CliApi.make("migrate", { description: "Migrate v1 data to v2" }), - ], -}) diff --git a/packages/cli/src/commands/commands.ts b/packages/cli/src/commands/commands.ts new file mode 100644 index 0000000000..39594e9951 --- /dev/null +++ b/packages/cli/src/commands/commands.ts @@ -0,0 +1,36 @@ +import { Argument, Flag } from "effect/unstable/cli" +import { Spec } from "../framework/spec" + +declare const OPENCODE_CLI_NAME: string | undefined + +export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", { + description: "OpenCode 2.0 preview command line interface", + commands: [ + Spec.make("debug", { + description: "Debugging and troubleshooting tools", + commands: [Spec.make("agents", { description: "List all agents" })], + }), + Spec.make("migrate", { description: "Migrate v1 data to v2" }), + Spec.make("service", { + description: "Manage the background server", + commands: [ + Spec.make("start", { description: "Start the background server" }), + Spec.make("restart", { description: "Restart the background server" }), + Spec.make("status", { description: "Show background server status" }), + Spec.make("stop", { description: "Stop the background server" }), + Spec.make("password", { + description: "Get or set the server password", + params: { value: Argument.string("value").pipe(Argument.optional) }, + }), + ], + }), + Spec.make("serve", { + description: "Start the v2 API server", + params: { + hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")), + port: Flag.integer("port").pipe(Flag.optional), + register: Flag.boolean("register").pipe(Flag.withDefault(false)), + }, + }), + ], +}) diff --git a/packages/cli/src/commands/handlers/debug/agents.ts b/packages/cli/src/commands/handlers/debug/agents.ts new file mode 100644 index 0000000000..3a0c20cb06 --- /dev/null +++ b/packages/cli/src/commands/handlers/debug/agents.ts @@ -0,0 +1,21 @@ +import { EOL } from "os" +import * as Effect from "effect/Effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Daemon } from "../../../services/daemon" + +export default Runtime.handler( + Commands.commands.debug.commands.agents, + Effect.fn("cli.debug.agents")(function* () { + const daemon = yield* Daemon.Service + const client = yield* daemon.client() + const response = yield* Effect.promise(() => client.v2.agent.list({ location: { directory: process.cwd() } })) + process.stdout.write( + JSON.stringify( + response.data?.data.toSorted((a, b) => a.id.localeCompare(b.id)), + null, + 2, + ) + EOL, + ) + }), +) diff --git a/packages/cli/src/commands/handlers/migrate.ts b/packages/cli/src/commands/handlers/migrate.ts new file mode 100644 index 0000000000..c73c7750df --- /dev/null +++ b/packages/cli/src/commands/handlers/migrate.ts @@ -0,0 +1,5 @@ +import * as Effect from "effect/Effect" +import { Commands } from "../commands" +import { Runtime } from "../../framework/runtime" + +export default Runtime.handler(Commands.commands.migrate, (_input) => Effect.log("No migrations to run.")) diff --git a/packages/cli/src/commands/handlers/serve.ts b/packages/cli/src/commands/handlers/serve.ts new file mode 100644 index 0000000000..62c64df433 --- /dev/null +++ b/packages/cli/src/commands/handlers/serve.ts @@ -0,0 +1,39 @@ +import { NodeHttpServer } from "@effect/platform-node" +import { Context, Layer, Option } from "effect" +import * as Effect from "effect/Effect" +import { HttpRouter, HttpServer } from "effect/unstable/http" +import { createServer } from "node:http" +import { createRoutes } from "@opencode-ai/server/routes" +import { Commands } from "../commands" +import { Runtime } from "../../framework/runtime" +import { Daemon } from "../../services/daemon" + +export default Runtime.handler( + Commands.commands.serve, + Effect.fn("cli.serve")(function* (input) { + return yield* Effect.scoped( + Effect.gen(function* () { + const daemon = yield* Daemon.Service + const address = yield* listen(input.hostname, input.port, yield* daemon.password()) + if (input.register) yield* daemon.register(address) + console.log(`server listening on ${HttpServer.formatAddress(address)}`) + return yield* Effect.never + }), + ) + }), +) + +function listen(hostname: string, port: Option.Option, password: string) { + if (Option.isSome(port)) return bind(hostname, port.value, password) + // Preserve the familiar default when available, but let the OS choose a free + // port when another local server already owns 4096. + return bind(hostname, 4096, password).pipe(Effect.catch(() => bind(hostname, 0, password))) +} + +function bind(hostname: string, port: number, password: string) { + return Layer.build( + HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe( + Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })), + ), + ).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address)) +} diff --git a/packages/cli/src/commands/handlers/service/password.ts b/packages/cli/src/commands/handlers/service/password.ts new file mode 100644 index 0000000000..6bf49d50d0 --- /dev/null +++ b/packages/cli/src/commands/handlers/service/password.ts @@ -0,0 +1,16 @@ +import { EOL } from "os" +import { Option } from "effect" +import * as Effect from "effect/Effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Daemon } from "../../../services/daemon" + +export default Runtime.handler( + Commands.commands.service.commands.password, + Effect.fn("cli.service.password")(function* (input) { + const daemon = yield* Daemon.Service + const value = Option.getOrUndefined(input.value) + if (value !== undefined) yield* daemon.stop() + process.stdout.write((yield* daemon.password(value)) + EOL) + }), +) diff --git a/packages/cli/src/commands/handlers/service/restart.ts b/packages/cli/src/commands/handlers/service/restart.ts new file mode 100644 index 0000000000..d348987d16 --- /dev/null +++ b/packages/cli/src/commands/handlers/service/restart.ts @@ -0,0 +1,14 @@ +import { EOL } from "os" +import * as Effect from "effect/Effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Daemon } from "../../../services/daemon" + +export default Runtime.handler( + Commands.commands.service.commands.restart, + Effect.fn("cli.service.restart")(function* () { + const daemon = yield* Daemon.Service + yield* daemon.stop() + process.stdout.write((yield* daemon.start()) + EOL) + }), +) diff --git a/packages/cli/src/commands/handlers/service/start.ts b/packages/cli/src/commands/handlers/service/start.ts new file mode 100644 index 0000000000..0d6fbaada9 --- /dev/null +++ b/packages/cli/src/commands/handlers/service/start.ts @@ -0,0 +1,12 @@ +import { EOL } from "os" +import * as Effect from "effect/Effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Daemon } from "../../../services/daemon" + +export default Runtime.handler( + Commands.commands.service.commands.start, + Effect.fn("cli.service.start")(function* () { + process.stdout.write((yield* (yield* Daemon.Service).start()) + EOL) + }), +) diff --git a/packages/cli/src/commands/handlers/service/status.ts b/packages/cli/src/commands/handlers/service/status.ts new file mode 100644 index 0000000000..d409970e8b --- /dev/null +++ b/packages/cli/src/commands/handlers/service/status.ts @@ -0,0 +1,13 @@ +import { EOL } from "os" +import * as Effect from "effect/Effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Daemon } from "../../../services/daemon" + +export default Runtime.handler( + Commands.commands.service.commands.status, + Effect.fn("cli.service.status")(function* () { + const url = yield* (yield* Daemon.Service).status() + process.stdout.write((url ? `running ${url}` : "stopped") + EOL) + }), +) diff --git a/packages/cli/src/commands/handlers/service/stop.ts b/packages/cli/src/commands/handlers/service/stop.ts new file mode 100644 index 0000000000..8da9b04cff --- /dev/null +++ b/packages/cli/src/commands/handlers/service/stop.ts @@ -0,0 +1,11 @@ +import * as Effect from "effect/Effect" +import { Commands } from "../../commands" +import { Runtime } from "../../../framework/runtime" +import { Daemon } from "../../../services/daemon" + +export default Runtime.handler( + Commands.commands.service.commands.stop, + Effect.fn("cli.service.stop")(function* () { + yield* (yield* Daemon.Service).stop() + }), +) diff --git a/packages/cli/src/cli-builder.ts b/packages/cli/src/framework/runtime.ts similarity index 59% rename from packages/cli/src/cli-builder.ts rename to packages/cli/src/framework/runtime.ts index e3dbaf8cb3..eee9ff795b 100644 --- a/packages/cli/src/cli-builder.ts +++ b/packages/cli/src/framework/runtime.ts @@ -1,19 +1,20 @@ import * as Effect from "effect/Effect" import * as Command from "effect/unstable/cli/Command" -import { CliApi } from "./cli-api" +import { Spec } from "./spec" +import { Daemon } from "../services/daemon" export type Input = - Value extends CliApi.Node - ? Input + Value extends Spec.Node + ? Input : Value extends Command.Command ? Input : never -type RuntimeHandler = (input: unknown) => Effect.Effect -type Loader = () => Promise<{ default: (input: Input) => Effect.Effect }> -type ProvidedCommand = Command.Command +type RuntimeHandler = (input: unknown) => Effect.Effect +type Loader = () => Promise<{ default: (input: Input) => Effect.Effect }> +type ProvidedCommand = Command.Command -export type Handlers = keyof Node["commands"] extends never +export type Handlers = keyof Node["commands"] extends never ? Loader : { readonly $?: Loader } & { readonly [Key in keyof Node["commands"]]: Handlers } @@ -29,17 +30,17 @@ type RuntimeHandlers = readonly [key: string]: RuntimeHandlers | (() => Promise<{ default: RuntimeHandler }>) | undefined } -export function handler( +export function handler( _node: Node, - run: (input: Input) => Effect.Effect, + run: (input: Input) => Effect.Effect, ) { return run } -export function handlers(root: Root, handlers: Handlers) { +export function handlers(root: Root, handlers: Handlers) { const result: LazyHandler[] = [] - function add(node: CliApi.Any, value: RuntimeHandlers) { + function add(node: Spec.Any, value: RuntimeHandlers) { if (typeof value === "function") { result.push({ spec: node.spec, load: value as () => Promise<{ default: RuntimeHandler }> }) return @@ -52,11 +53,11 @@ export function handlers(root: Root, handlers: Ha return result } -export function run(api: CliApi.Any, handlers: ReadonlyArray, options: { readonly version: string }) { - return Command.run(provide(api, handlers), options) as Effect.Effect +export function run(commands: Spec.Any, handlers: ReadonlyArray, options: { readonly version: string }) { + return Command.run(provide(commands, handlers), options) as Effect.Effect } -function provide(node: CliApi.Any, handlers: ReadonlyArray): ProvidedCommand { +function provide(node: Spec.Any, handlers: ReadonlyArray): ProvidedCommand { const spec: Command.Command.Any = Object.keys(node.commands).length ? (node.spec as Command.Command).pipe( Command.withSubcommands(Object.values(node.commands).map((child) => provide(child, handlers))), @@ -65,8 +66,12 @@ function provide(node: CliApi.Any, handlers: ReadonlyArray): Provid const handler = handlers.find((handler) => handler.spec === node.spec) if (!handler) return spec as ProvidedCommand return spec.pipe( - Command.withHandler((input) => Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))), + Command.withHandler((input) => + Effect.gen(function* () { + yield* Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input)) + }), + ), ) as ProvidedCommand } -export * as CliBuilder from "./cli-builder" +export * as Runtime from "./runtime" diff --git a/packages/cli/src/cli-api.ts b/packages/cli/src/framework/spec.ts similarity index 97% rename from packages/cli/src/cli-api.ts rename to packages/cli/src/framework/spec.ts index 038428d66b..3bb47e5e5e 100644 --- a/packages/cli/src/cli-api.ts +++ b/packages/cli/src/framework/spec.ts @@ -39,4 +39,4 @@ type ChildrenOf> = { readonly [Node in Commands[number] as Node["name"]]: Node } -export * as CliApi from "./cli-api" +export * as Spec from "./spec" diff --git a/packages/cli/src/handlers/debug/agents.ts b/packages/cli/src/handlers/debug/agents.ts deleted file mode 100644 index 85eec4555d..0000000000 --- a/packages/cli/src/handlers/debug/agents.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { EOL } from "os" -import { AgentV2 } from "@opencode-ai/core/agent" -import { LocationServiceMap } from "@opencode-ai/core/location-layer" -import { PluginBoot } from "@opencode-ai/core/plugin/boot" -import { AbsolutePath } from "@opencode-ai/core/schema" -import * as Effect from "effect/Effect" -import { Api } from "../../api" -import { CliBuilder } from "../../cli-builder" - -export default CliBuilder.handler( - Api.commands.debug.commands.agents, - Effect.fn("cli.debug.agents")( - function* () { - const svc = { - plugin: yield* PluginBoot.Service, - agent: yield* AgentV2.Service, - } - yield* svc.plugin.wait() - process.stdout.write( - JSON.stringify( - (yield* svc.agent.all()).sort((a, b) => a.id.localeCompare(b.id)), - null, - 2, - ) + EOL, - ) - }, - Effect.provide(LocationServiceMap.get({ directory: AbsolutePath.make(process.cwd()) })), - Effect.provide(LocationServiceMap.layer), - ), -) diff --git a/packages/cli/src/handlers/migrate.ts b/packages/cli/src/handlers/migrate.ts deleted file mode 100644 index 0d9c1e6aca..0000000000 --- a/packages/cli/src/handlers/migrate.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as Effect from "effect/Effect" -import { Api } from "../api" -import { CliBuilder } from "../cli-builder" - -export default CliBuilder.handler(Api.commands.migrate, (_input) => Effect.log("No migrations to run.")) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0a1f21a21f..75837af534 100755 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,17 +3,27 @@ import * as NodeRuntime from "@effect/platform-node/NodeRuntime" import * as NodeServices from "@effect/platform-node/NodeServices" import * as Effect from "effect/Effect" -import { Api } from "./api" -import { CliBuilder } from "./cli-builder" +import { Commands } from "./commands/commands" +import { Runtime } from "./framework/runtime" +import { Daemon } from "./services/daemon" -const Handlers = CliBuilder.handlers(Api, { +const Handlers = Runtime.handlers(Commands, { debug: { - agents: () => import("./handlers/debug/agents"), + agents: () => import("./commands/handlers/debug/agents"), }, - migrate: () => import("./handlers/migrate"), + migrate: () => import("./commands/handlers/migrate"), + service: { + start: () => import("./commands/handlers/service/start"), + restart: () => import("./commands/handlers/service/restart"), + status: () => import("./commands/handlers/service/status"), + stop: () => import("./commands/handlers/service/stop"), + password: () => import("./commands/handlers/service/password"), + }, + serve: () => import("./commands/handlers/serve"), }) -CliBuilder.run(Api, Handlers, { version: "local" }).pipe( +Runtime.run(Commands, Handlers, { version: "local" }).pipe( + Effect.provide(Daemon.defaultLayer), Effect.provide(NodeServices.layer), Effect.scoped, NodeRuntime.runMain, diff --git a/packages/cli/src/services/daemon.ts b/packages/cli/src/services/daemon.ts new file mode 100644 index 0000000000..8500add61a --- /dev/null +++ b/packages/cli/src/services/daemon.ts @@ -0,0 +1,145 @@ +import { Global } from "@opencode-ai/core/global" +import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" +import { ServerAuth } from "@opencode-ai/server/auth" +import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect" +import { HttpServer } from "effect/unstable/http" +import { randomBytes } from "crypto" +import path from "path" + +export interface Interface { + readonly client: () => Effect.Effect, unknown> + readonly start: () => Effect.Effect + readonly status: () => Effect.Effect + readonly stop: () => Effect.Effect + readonly password: (value?: string) => Effect.Effect + readonly register: (address: HttpServer.Address) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/cli/Daemon") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem + const directory = Global.Path.state + const file = path.join(directory, "server.json") + const passwordFile = path.join(directory, "password") + const decodeRegistration = Schema.decodeUnknownEffect( + Schema.fromJsonString(Schema.Struct({ url: Schema.String, pid: Schema.Number })), + ) + + const password = Effect.fn("cli.daemon.password")(function* (value?: string) { + const existing = yield* fs.readFileString(passwordFile).pipe(Effect.catch(() => Effect.succeed(undefined))) + if (value === undefined && existing) return existing + + // Keep one private credential across server restarts so discovered clients + // can reconnect without exposing a password flag or environment variable. + const generated = value ?? randomBytes(32).toString("base64url") + const temp = passwordFile + ".tmp" + yield* fs.makeDirectory(directory, { recursive: true }) + yield* fs.writeFileString(temp, generated, { mode: 0o600 }) + yield* fs.rename(temp, passwordFile) + return generated + }) + + const registration = Effect.fnUntraced(function* () { + return yield* fs.readFileString(file).pipe(Effect.flatMap(decodeRegistration)) + }) + + const createClient = Effect.fnUntraced(function* (url: string) { + return createOpencodeClient({ baseUrl: url, headers: ServerAuth.headers({ password: yield* password() }) }) + }) + + const healthy = Effect.fnUntraced(function* () { + const info = yield* registration() + const client = yield* createClient(info.url) + const response = yield* Effect.tryPromise(() => client.v2.health.get()) + if (response.data?.healthy === true) return info + return yield* Effect.fail(new Error("Registered server is not healthy")) + }) + + const start = Effect.fn("cli.daemon.start")(function* () { + const existing = yield* healthy().pipe(Effect.option) + const found = Option.getOrUndefined(existing) + if (found) return found.url + + yield* Effect.sync(() => { + const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun" + Bun.spawn([process.execPath, ...(compiled ? [] : [Bun.main]), "serve", "--register"], { + stdin: "ignore", + stdout: "ignore", + stderr: "ignore", + }).unref() + }) + + return yield* healthy().pipe( + Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), + Effect.map((info) => info.url), + Effect.mapError(() => new Error("Failed to start server")), + ) + }) + + const client = Effect.fn("cli.daemon.client")(function* () { + return yield* createClient(yield* start()) + }) + + const status = Effect.fn("cli.daemon.status")(function* () { + const existing = yield* healthy().pipe(Effect.option) + const found = Option.getOrUndefined(existing) + if (found) return found.url + yield* fs.remove(file).pipe(Effect.ignore) + return undefined + }) + + const signal = (pid: number, signal: NodeJS.Signals) => + Effect.try({ try: () => process.kill(pid, signal), catch: (cause) => cause }).pipe(Effect.ignore) + + const awaitStopped = Effect.fnUntraced(function* (pid: number) { + const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe( + Effect.orElseSucceed(() => false), + ) + if (!running) return true + return yield* Effect.fail(new Error(`Server process ${pid} is still running`)) + }) + + const stop = Effect.fn("cli.daemon.stop")(function* () { + const existing = yield* healthy().pipe(Effect.option) + // A stale registration may point at a PID that has since been reused by + // another process. Only signal the PID after authenticating the server. + if (Option.isNone(existing)) return yield* fs.remove(file).pipe(Effect.ignore) + const pid = existing.value.pid + yield* signal(pid, "SIGTERM") + const stopped = yield* awaitStopped(pid).pipe( + Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), + Effect.option, + ) + if (Option.isNone(stopped)) { + yield* signal(pid, "SIGKILL") + yield* awaitStopped(pid).pipe( + Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), + ) + } + yield* fs.remove(file).pipe(Effect.ignore) + }) + + const register = Effect.fn("cli.daemon.register")(function* (address: HttpServer.Address) { + const temp = file + ".tmp" + yield* fs.makeDirectory(directory, { recursive: true }) + yield* fs.writeFileString( + temp, + JSON.stringify({ url: HttpServer.formatAddress(address), pid: process.pid }), + { mode: 0o600 }, + ) + yield* fs.rename(temp, file) + // The metadata file represents this live listener, not persistent config. + // Scope shutdown removes it when the server exits normally. + yield* Effect.addFinalizer(() => fs.remove(file).pipe(Effect.ignore)) + }) + + return Service.of({ client, start, status, stop, password, register }) + }), +) + +export const defaultLayer = layer + +export * as Daemon from "./daemon" diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index fe5c4d217b..00ef125468 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -2,6 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "extends": "@tsconfig/bun/tsconfig.json", "compilerOptions": { + "lib": ["ESNext", "DOM", "DOM.Iterable"], "noUncheckedIndexedAccess": false } } diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 9538218a6b..ab0ca7e7d9 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -87,6 +87,7 @@ "@opencode-ai/llm": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/script": "workspace:*", + "@opencode-ai/server": "workspace:*", "@opencode-ai/sdk": "workspace:*", "@opencode-ai/ui": "workspace:*", "@openrouter/ai-sdk-provider": "2.8.1", diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index 57b8b37d99..b80e57222e 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -20,7 +20,7 @@ import { SessionApi } from "./groups/session" import { SyncApi } from "./groups/sync" import { TuiApi } from "./groups/tui" import { WorkspaceApi } from "./groups/workspace" -import { V2Api } from "./groups/v2" +import { V2Api } from "@opencode-ai/server/api" // GlobalEventSchema snapshots the registry after event-producing groups register their variants. import { GlobalApi } from "./groups/global" import { Authorization } from "./middleware/authorization" @@ -60,7 +60,6 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance") .addHttpApi(ProviderApi) .addHttpApi(SessionApi) .addHttpApi(SyncApi) - .addHttpApi(V2Api) .addHttpApi(TuiApi) .addHttpApi(WorkspaceApi) .middleware(SchemaErrorMiddleware) @@ -69,6 +68,7 @@ export const OpenCodeHttpApi = HttpApi.make("opencode") .addHttpApi(RootHttpApi) .addHttpApi(EventApi) .addHttpApi(InstanceHttpApi) + .addHttpApi(V2Api) .addHttpApi(PtyConnectApi) .annotate(HttpApi.AdditionalSchemas, [EventSchema, Question.Replied, Question.Rejected]) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts deleted file mode 100644 index 0cd768e0d9..0000000000 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { HttpApi, OpenApi } from "effect/unstable/httpapi" -import { MessageGroup } from "./v2/message" -import { ModelGroup } from "./v2/model" -import { ProviderGroup } from "./v2/provider" -import { SessionGroup } from "./v2/session" -import { PermissionGroup, PermissionSavedGroup, SessionPermissionGroup } from "./v2/permission" -import { FileSystemGroup } from "./v2/fs" -import { CommandGroup } from "./v2/command" -import { SkillGroup } from "./v2/skill" -import { EventGroup } from "./v2/event" - -export const V2Api = HttpApi.make("v2") - .add(SessionGroup) - .add(MessageGroup) - .add(ModelGroup) - .add(ProviderGroup) - .add(PermissionGroup) - .add(SessionPermissionGroup) - .add(PermissionSavedGroup) - .add(FileSystemGroup) - .add(CommandGroup) - .add(SkillGroup) - .add(EventGroup) - .annotateMerge( - OpenApi.annotations({ - title: "opencode experimental HttpApi", - version: "0.0.1", - description: "Experimental HttpApi surface for selected instance routes.", - }), - ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/event.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/event.ts deleted file mode 100644 index bde4bbb86d..0000000000 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/event.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { EventV2 } from "@opencode-ai/core/event" -import { Location } from "@opencode-ai/core/location" -import { EventV2Bridge } from "@/event-v2-bridge" -import { Effect, Stream } from "effect" -import { HttpServerResponse } from "effect/unstable/http" -import { HttpApiBuilder } from "effect/unstable/httpapi" -import * as Sse from "effect/unstable/encoding/Sse" -import { InstanceHttpApi } from "../../api" - -function eventData(data: unknown): Sse.Event { - return { - _tag: "Event", - event: "message", - id: undefined, - data: JSON.stringify(data), - } -} - -export const eventHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.event", (handlers) => - handlers.handleRaw("events", () => - Effect.gen(function* () { - const events = yield* EventV2Bridge.Service - const location = yield* Location.Service - const connected = { - id: EventV2.ID.create(), - type: "server.connected", - location: new Location.Info({ - directory: location.directory, - workspaceID: location.workspaceID, - project: location.project, - }), - data: {}, - } - return HttpServerResponse.stream( - Stream.make(connected).pipe( - Stream.concat( - events.all().pipe( - Stream.filter( - (event) => - event.location?.directory === location.directory && - event.location.workspaceID === location.workspaceID, - ), - ), - ), - Stream.map(eventData), - Stream.pipeThroughChannel(Sse.encode()), - Stream.encodeText, - ), - { - contentType: "text/event-stream", - headers: { - "Cache-Control": "no-cache, no-transform", - "X-Accel-Buffering": "no", - "X-Content-Type-Options": "nosniff", - }, - }, - ) - }), - ), -) diff --git a/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts b/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts index db6554590f..43ee1e174a 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/middleware/authorization.ts @@ -4,7 +4,7 @@ import { HttpEffect, HttpRouter, HttpServerRequest, HttpServerResponse } from "e import { HttpApiError, HttpApiMiddleware } from "effect/unstable/httpapi" import { hasPtyConnectTicketURL } from "@/server/shared/pty-ticket" import { isPublicUIPath } from "@/server/shared/public-ui" -import { UnauthorizedError } from "../errors" +export { V2Authorization, v2AuthorizationLayer } from "@opencode-ai/server/middleware/authorization" const AUTH_TOKEN_QUERY = "auth_token" const UNAUTHORIZED = 401 @@ -20,13 +20,6 @@ export class Authorization extends HttpApiMiddleware.Service()( }, ) {} -export class V2Authorization extends HttpApiMiddleware.Service()( - "@opencode/ExperimentalHttpApiV2Authorization", - { - error: UnauthorizedError, - }, -) {} - export class PtyConnectAuthorization extends HttpApiMiddleware.Service()( "@opencode/ExperimentalHttpApiPtyConnectAuthorization", { @@ -152,27 +145,3 @@ export const ptyConnectAuthorizationLayer = Layer.effect( ) }), ) - -export const v2AuthorizationLayer = Layer.effect( - V2Authorization, - Effect.gen(function* () { - const config = yield* ServerAuth.Config - if (!ServerAuth.required(config)) return V2Authorization.of((effect) => effect) - return V2Authorization.of((effect) => - Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest - return yield* credentialFromRequest(request).pipe( - Effect.flatMap((credential) => - Effect.gen(function* () { - if (ServerAuth.authorized(credential, config)) return yield* effect - yield* HttpEffect.appendPreResponseHandler((_request, response) => - Effect.succeed(HttpServerResponse.setHeader(response, "www-authenticate", WWW_AUTHENTICATE)), - ) - return yield* new UnauthorizedError({ message: "Authentication required" }) - }), - ), - ) - }), - ) - }), -) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index dfbb1a88b7..1ce65cb8f9 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -44,6 +44,7 @@ import { Todo } from "@/session/todo" import { SessionShare } from "@/share/session" import { ShareNext } from "@/share/share-next" import { EventV2Bridge } from "@/event-v2-bridge" +import { EventV2 } from "@opencode-ai/core/event" import { Database } from "@opencode-ai/core/database/database" import { Skill } from "@/skill" import { Snapshot } from "@/snapshot" @@ -56,6 +57,7 @@ import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@/server/cors import { serveUIEffect } from "@/server/shared/ui" import { ServerAuth } from "@/server/auth" import { InstanceHttpApi, RootHttpApi } from "./api" +import { V2Api } from "@opencode-ai/server/api" import { PublicApi } from "./public" import { authorizationLayer, @@ -82,7 +84,8 @@ import { questionHandlers } from "./handlers/question" import { sessionHandlers } from "./handlers/session" import { syncHandlers } from "./handlers/sync" import { tuiHandlers } from "./handlers/tui" -import { v2Handlers } from "./handlers/v2" +import { v2Handlers } from "@opencode-ai/server/handlers" +import { schemaErrorLayer as v2SchemaErrorLayer } from "@opencode-ai/server/middleware/schema-error" import { workspaceHandlers } from "./handlers/workspace" import { instanceContextLayer } from "./middleware/instance-context" import { workspaceRoutingLayer } from "./middleware/workspace-routing" @@ -144,14 +147,17 @@ const instanceApiRoutes = HttpApiBuilder.layer(InstanceHttpApi).pipe( providerHandlers, sessionHandlers, syncHandlers, - v2Handlers, tuiHandlers, workspaceHandlers, ]), ) const instanceRoutes = instanceApiRoutes.pipe( - Layer.provide([httpApiAuthLayer, v2HttpApiAuthLayer, workspaceRoutingLive, instanceContextLayer, schemaErrorLayer]), + Layer.provide([httpApiAuthLayer, workspaceRoutingLive, instanceContextLayer, schemaErrorLayer]), +) +const v2Routes = HttpApiBuilder.layer(V2Api).pipe( + Layer.provide(v2Handlers), + Layer.provide([v2HttpApiAuthLayer, v2SchemaErrorLayer]), ) // `OpenApi.fromApi` is non-trivial; defer until /doc is actually hit so @@ -186,7 +192,7 @@ type RouteRequirements = export function createRoutes( corsOptions?: CorsOptions, ): Layer.Layer { - return Layer.mergeAll(rootApiRoutes, eventApiRoutes, ptyConnectApiRoutes, instanceRoutes, docRoute, uiRoute).pipe( + return Layer.mergeAll(rootApiRoutes, eventApiRoutes, ptyConnectApiRoutes, instanceRoutes, v2Routes, docRoute, uiRoute).pipe( Layer.provide([ errorLayer, compressionLayer, @@ -226,6 +232,7 @@ export function createRoutes( ShareNext.defaultLayer, Snapshot.defaultLayer, EventV2Bridge.defaultLayer, + EventV2.defaultLayer, Skill.defaultLayer, Todo.defaultLayer, ToolRegistry.defaultLayer, diff --git a/packages/opencode/test/server/httpapi-exercise/index.ts b/packages/opencode/test/server/httpapi-exercise/index.ts index 13ed74accf..98cd1b1ba6 100644 --- a/packages/opencode/test/server/httpapi-exercise/index.ts +++ b/packages/opencode/test/server/httpapi-exercise/index.ts @@ -624,6 +624,11 @@ const scenarios: Scenario[] = [ check(auth.test === undefined, "auth remove should delete provider from isolated auth file") }), ), + http.protected.get("/api/health", "v2.health.get").json(200, (body) => { + object(body) + check(body.healthy === true, "v2 server should report healthy") + }), + http.protected.get("/api/agent", "v2.agent.list").json(200, locationData(array)), http.protected.get("/api/model", "v2.model.list").json(200, locationData(array)), http.protected.get("/api/provider", "v2.provider.list").json(200, locationData(array)), http.protected.get("/api/command", "v2.command.list").json(200, locationData(array)), diff --git a/packages/opencode/test/server/httpapi-query-schema-drift.test.ts b/packages/opencode/test/server/httpapi-query-schema-drift.test.ts index 064bcc97e2..cdaf554f09 100644 --- a/packages/opencode/test/server/httpapi-query-schema-drift.test.ts +++ b/packages/opencode/test/server/httpapi-query-schema-drift.test.ts @@ -24,7 +24,7 @@ import { SessionPaths, } from "../../src/server/routes/instance/httpapi/groups/session" import { PtyPaths } from "../../src/server/routes/instance/httpapi/groups/pty" -import { MessagesQuery as V2MessagesQuery } from "../../src/server/routes/instance/httpapi/groups/v2/message" +import { MessagesQuery as V2MessagesQuery } from "@opencode-ai/server/groups/v2/message" import { QueryBoolean, QueryBooleanOpenApi } from "../../src/server/routes/instance/httpapi/groups/query" import { resetDatabase } from "../fixture/db" import { disposeAllInstances, tmpdir } from "../fixture/fixture" diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 09fac6ecfb..bf749739d2 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -255,6 +255,8 @@ import type { TuiShowToastResponses, TuiSubmitPromptErrors, TuiSubmitPromptResponses, + V2AgentListErrors, + V2AgentListResponses, V2CommandListErrors, V2CommandListResponses, V2EventSubscribeErrors, @@ -263,6 +265,8 @@ import type { V2FsListResponses, V2FsReadErrors, V2FsReadResponses, + V2HealthGetErrors, + V2HealthGetResponses, V2ModelListErrors, V2ModelListResponses, V2PermissionRequestListErrors, @@ -4463,653 +4467,6 @@ export class Sync extends HeyApiClient { } } -export class Permission2 extends HeyApiClient { - /** - * List session permission requests - * - * Retrieve pending permission requests owned by a session. - */ - public list( - parameters: { - sessionID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) - return (options?.client ?? this.client).get< - V2SessionPermissionListResponses, - V2SessionPermissionListErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/permission/request", - ...options, - ...params, - }) - } - - /** - * Reply to pending permission request - * - * Respond to a pending permission request owned by a session. - */ - public reply( - parameters: { - sessionID: string - requestID: string - reply?: PermissionV2Reply - message?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "path", key: "requestID" }, - { in: "body", key: "reply" }, - { in: "body", key: "message" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post< - V2SessionPermissionReplyResponses, - V2SessionPermissionReplyErrors, - ThrowOnError - >({ - url: "/api/session/{sessionID}/permission/request/{requestID}/reply", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - -export class Session3 extends HeyApiClient { - /** - * List v2 sessions - * - * Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list. - */ - public list( - parameters?: { - workspace?: string - limit?: number - order?: "asc" | "desc" - search?: string - directory?: string - project?: string - subpath?: string - cursor?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "workspace" }, - { in: "query", key: "limit" }, - { in: "query", key: "order" }, - { in: "query", key: "search" }, - { in: "query", key: "directory" }, - { in: "query", key: "project" }, - { in: "query", key: "subpath" }, - { in: "query", key: "cursor" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session", - ...options, - ...params, - }) - } - - /** - * Send v2 message - * - * Create a v2 session message and queue it for the agent loop. - */ - public prompt( - parameters: { - sessionID: string - directory?: string - workspace?: string - prompt?: Prompt - delivery?: SessionDelivery - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "body", key: "prompt" }, - { in: "body", key: "delivery" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/prompt", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - - /** - * Compact v2 session - * - * Compact a v2 session conversation. - */ - public compact( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/compact", - ...options, - ...params, - }) - } - - /** - * Wait for v2 session - * - * Wait for a v2 session agent loop to become idle. - */ - public wait( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/api/session/{sessionID}/wait", - ...options, - ...params, - }) - } - - /** - * Get v2 session context - * - * Retrieve the active context messages for a v2 session (all messages after the last compaction). - */ - public context( - parameters: { - sessionID: string - directory?: string - workspace?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/context", - ...options, - ...params, - }) - } - - /** - * Get v2 session messages - * - * Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline. - */ - public messages( - parameters: { - sessionID: string - directory?: string - workspace?: string - limit?: number - order?: "asc" | "desc" - cursor?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "sessionID" }, - { in: "query", key: "directory" }, - { in: "query", key: "workspace" }, - { in: "query", key: "limit" }, - { in: "query", key: "order" }, - { in: "query", key: "cursor" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/session/{sessionID}/message", - ...options, - ...params, - }) - } - - private _permission?: Permission2 - get permission(): Permission2 { - return (this._permission ??= new Permission2({ client: this.client })) - } -} - -export class Model extends HeyApiClient { - /** - * List v2 models - * - * Retrieve available v2 models ordered by release date. - */ - public list( - parameters?: { - location?: { - directory?: string - workspace?: string - } - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/model", - ...options, - ...params, - }) - } -} - -export class Provider2 extends HeyApiClient { - /** - * List v2 providers - * - * Retrieve active v2 AI providers so clients can show provider availability and configuration. - */ - public list( - parameters?: { - location?: { - directory?: string - workspace?: string - } - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/provider", - ...options, - ...params, - }) - } - - /** - * Get v2 provider - * - * Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings. - */ - public get( - parameters: { - providerID: string - location?: { - directory?: string - workspace?: string - } - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { in: "query", key: "location" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/provider/{providerID}", - ...options, - ...params, - }) - } -} - -export class Request extends HeyApiClient { - /** - * List pending permission requests - * - * Retrieve pending permission requests for a location. - */ - public list( - parameters?: { - location?: { - directory?: string - workspace?: string - } - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get< - V2PermissionRequestListResponses, - V2PermissionRequestListErrors, - ThrowOnError - >({ - url: "/api/permission/request", - ...options, - ...params, - }) - } -} - -export class Saved extends HeyApiClient { - /** - * List saved permissions - * - * Retrieve saved permissions, optionally filtered by project. - */ - public list( - parameters?: { - projectID?: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "projectID" }] }]) - return (options?.client ?? this.client).get< - V2PermissionSavedListResponses, - V2PermissionSavedListErrors, - ThrowOnError - >({ - url: "/api/permission/saved", - ...options, - ...params, - }) - } - - /** - * Remove saved permission - * - * Remove a saved permission by ID. - */ - public remove( - parameters: { - id: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "id" }] }]) - return (options?.client ?? this.client).delete< - V2PermissionSavedRemoveResponses, - V2PermissionSavedRemoveErrors, - ThrowOnError - >({ - url: "/api/permission/saved/{id}", - ...options, - ...params, - }) - } -} - -export class Permission3 extends HeyApiClient { - private _request?: Request - get request(): Request { - return (this._request ??= new Request({ client: this.client })) - } - - private _saved?: Saved - get saved(): Saved { - return (this._saved ??= new Saved({ client: this.client })) - } -} - -export class Fs extends HeyApiClient { - /** - * Read file - * - * Read one file relative to the requested location. - */ - public read( - parameters: { - location?: { - directory?: string - workspace?: string - } - path: string - reference?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "query", key: "path" }, - { in: "query", key: "reference" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/fs/read", - ...options, - ...params, - }) - } - - /** - * List directory - * - * List direct children of one directory relative to the requested location. - */ - public list( - parameters?: { - location?: { - directory?: string - workspace?: string - } - path?: string - reference?: string - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "location" }, - { in: "query", key: "path" }, - { in: "query", key: "reference" }, - ], - }, - ], - ) - return (options?.client ?? this.client).get({ - url: "/api/fs/list", - ...options, - ...params, - }) - } -} - -export class Command2 extends HeyApiClient { - /** - * List v2 commands - * - * Retrieve currently registered v2 commands. - */ - public list( - parameters?: { - location?: { - directory?: string - workspace?: string - } - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/command", - ...options, - ...params, - }) - } -} - -export class Skill extends HeyApiClient { - /** - * List v2 skills - * - * Retrieve currently registered v2 skills. - */ - public list( - parameters?: { - location?: { - directory?: string - workspace?: string - } - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).get({ - url: "/api/skill", - ...options, - ...params, - }) - } -} - -export class Event2 extends HeyApiClient { - /** - * Subscribe to v2 events - * - * Subscribe to native EventV2 payloads for a location. - */ - public subscribe( - parameters?: { - location?: { - directory?: string - workspace?: string - } - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) - return (options?.client ?? this.client).sse.get({ - url: "/api/event", - ...options, - ...params, - }) - } -} - -export class V2 extends HeyApiClient { - private _session?: Session3 - get session(): Session3 { - return (this._session ??= new Session3({ client: this.client })) - } - - private _model?: Model - get model(): Model { - return (this._model ??= new Model({ client: this.client })) - } - - private _provider?: Provider2 - get provider(): Provider2 { - return (this._provider ??= new Provider2({ client: this.client })) - } - - private _permission?: Permission3 - get permission(): Permission3 { - return (this._permission ??= new Permission3({ client: this.client })) - } - - private _fs?: Fs - get fs(): Fs { - return (this._fs ??= new Fs({ client: this.client })) - } - - private _command?: Command2 - get command(): Command2 { - return (this._command ??= new Command2({ client: this.client })) - } - - private _skill?: Skill - get skill(): Skill { - return (this._skill ??= new Skill({ client: this.client })) - } - - private _event?: Event2 - get event(): Event2 { - return (this._event ??= new Event2({ client: this.client })) - } -} - export class Control extends HeyApiClient { /** * Get next TUI request @@ -5557,6 +4914,654 @@ export class Tui extends HeyApiClient { } } +export class Health extends HeyApiClient { + /** + * Check v2 server health + * + * Check whether the v2 API server is ready to accept requests. + */ + public get(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/api/health", + ...options, + }) + } +} + +export class Agent extends HeyApiClient { + /** + * List v2 agents + * + * Retrieve currently registered v2 agents. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/agent", + ...options, + ...params, + }) + } +} + +export class Permission2 extends HeyApiClient { + /** + * List session permission requests + * + * Retrieve pending permission requests owned by a session. + */ + public list( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).get< + V2SessionPermissionListResponses, + V2SessionPermissionListErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/permission/request", + ...options, + ...params, + }) + } + + /** + * Reply to pending permission request + * + * Respond to a pending permission request owned by a session. + */ + public reply( + parameters: { + sessionID: string + requestID: string + reply?: PermissionV2Reply + message?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "requestID" }, + { in: "body", key: "reply" }, + { in: "body", key: "message" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post< + V2SessionPermissionReplyResponses, + V2SessionPermissionReplyErrors, + ThrowOnError + >({ + url: "/api/session/{sessionID}/permission/request/{requestID}/reply", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + +export class Session3 extends HeyApiClient { + /** + * List v2 sessions + * + * Retrieve sessions in the requested order. Items keep that order across pages; use cursor.next or cursor.previous to move through the ordered list. + */ + public list( + parameters?: { + workspace?: string + limit?: number + order?: "asc" | "desc" + search?: string + directory?: string + project?: string + subpath?: string + cursor?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "workspace" }, + { in: "query", key: "limit" }, + { in: "query", key: "order" }, + { in: "query", key: "search" }, + { in: "query", key: "directory" }, + { in: "query", key: "project" }, + { in: "query", key: "subpath" }, + { in: "query", key: "cursor" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/session", + ...options, + ...params, + }) + } + + /** + * Send v2 message + * + * Create a v2 session message and queue it for the agent loop. + */ + public prompt( + parameters: { + sessionID: string + prompt?: Prompt + delivery?: SessionDelivery + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "body", key: "prompt" }, + { in: "body", key: "delivery" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/prompt", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + + /** + * Compact v2 session + * + * Compact a v2 session conversation. + */ + public compact( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/compact", + ...options, + ...params, + }) + } + + /** + * Wait for v2 session + * + * Wait for a v2 session agent loop to become idle. + */ + public wait( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).post({ + url: "/api/session/{sessionID}/wait", + ...options, + ...params, + }) + } + + /** + * Get v2 session context + * + * Retrieve the active context messages for a v2 session (all messages after the last compaction). + */ + public context( + parameters: { + sessionID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "sessionID" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/context", + ...options, + ...params, + }) + } + + /** + * Get v2 session messages + * + * Retrieve projected v2 messages for a session. Items keep the requested order across pages; use cursor.next or cursor.previous to move through the ordered timeline. + */ + public messages( + parameters: { + sessionID: string + limit?: number + order?: "asc" | "desc" + cursor?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "limit" }, + { in: "query", key: "order" }, + { in: "query", key: "cursor" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/session/{sessionID}/message", + ...options, + ...params, + }) + } + + private _permission?: Permission2 + get permission(): Permission2 { + return (this._permission ??= new Permission2({ client: this.client })) + } +} + +export class Model extends HeyApiClient { + /** + * List v2 models + * + * Retrieve available v2 models ordered by release date. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/model", + ...options, + ...params, + }) + } +} + +export class Provider2 extends HeyApiClient { + /** + * List v2 providers + * + * Retrieve active v2 AI providers so clients can show provider availability and configuration. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/provider", + ...options, + ...params, + }) + } + + /** + * Get v2 provider + * + * Retrieve a single v2 AI provider so clients can inspect its availability and endpoint settings. + */ + public get( + parameters: { + providerID: string + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "location" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/provider/{providerID}", + ...options, + ...params, + }) + } +} + +export class Request extends HeyApiClient { + /** + * List pending permission requests + * + * Retrieve pending permission requests for a location. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get< + V2PermissionRequestListResponses, + V2PermissionRequestListErrors, + ThrowOnError + >({ + url: "/api/permission/request", + ...options, + ...params, + }) + } +} + +export class Saved extends HeyApiClient { + /** + * List saved permissions + * + * Retrieve saved permissions, optionally filtered by project. + */ + public list( + parameters?: { + projectID?: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "projectID" }] }]) + return (options?.client ?? this.client).get< + V2PermissionSavedListResponses, + V2PermissionSavedListErrors, + ThrowOnError + >({ + url: "/api/permission/saved", + ...options, + ...params, + }) + } + + /** + * Remove saved permission + * + * Remove a saved permission by ID. + */ + public remove( + parameters: { + id: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "id" }] }]) + return (options?.client ?? this.client).delete< + V2PermissionSavedRemoveResponses, + V2PermissionSavedRemoveErrors, + ThrowOnError + >({ + url: "/api/permission/saved/{id}", + ...options, + ...params, + }) + } +} + +export class Permission3 extends HeyApiClient { + private _request?: Request + get request(): Request { + return (this._request ??= new Request({ client: this.client })) + } + + private _saved?: Saved + get saved(): Saved { + return (this._saved ??= new Saved({ client: this.client })) + } +} + +export class Fs extends HeyApiClient { + /** + * Read file + * + * Read one file relative to the requested location. + */ + public read( + parameters: { + location?: { + directory?: string + workspace?: string + } + path: string + reference?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "location" }, + { in: "query", key: "path" }, + { in: "query", key: "reference" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/fs/read", + ...options, + ...params, + }) + } + + /** + * List directory + * + * List direct children of one directory relative to the requested location. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + path?: string + reference?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "location" }, + { in: "query", key: "path" }, + { in: "query", key: "reference" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/api/fs/list", + ...options, + ...params, + }) + } +} + +export class Command2 extends HeyApiClient { + /** + * List v2 commands + * + * Retrieve currently registered v2 commands. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/command", + ...options, + ...params, + }) + } +} + +export class Skill extends HeyApiClient { + /** + * List v2 skills + * + * Retrieve currently registered v2 skills. + */ + public list( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).get({ + url: "/api/skill", + ...options, + ...params, + }) + } +} + +export class Event2 extends HeyApiClient { + /** + * Subscribe to v2 events + * + * Subscribe to native EventV2 payloads for a location. + */ + public subscribe( + parameters?: { + location?: { + directory?: string + workspace?: string + } + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "location" }] }]) + return (options?.client ?? this.client).sse.get({ + url: "/api/event", + ...options, + ...params, + }) + } +} + +export class V2 extends HeyApiClient { + private _health?: Health + get health(): Health { + return (this._health ??= new Health({ client: this.client })) + } + + private _agent?: Agent + get agent(): Agent { + return (this._agent ??= new Agent({ client: this.client })) + } + + private _session?: Session3 + get session(): Session3 { + return (this._session ??= new Session3({ client: this.client })) + } + + private _model?: Model + get model(): Model { + return (this._model ??= new Model({ client: this.client })) + } + + private _provider?: Provider2 + get provider(): Provider2 { + return (this._provider ??= new Provider2({ client: this.client })) + } + + private _permission?: Permission3 + get permission(): Permission3 { + return (this._permission ??= new Permission3({ client: this.client })) + } + + private _fs?: Fs + get fs(): Fs { + return (this._fs ??= new Fs({ client: this.client })) + } + + private _command?: Command2 + get command(): Command2 { + return (this._command ??= new Command2({ client: this.client })) + } + + private _skill?: Skill + get skill(): Skill { + return (this._skill ??= new Skill({ client: this.client })) + } + + private _event?: Event2 + get event(): Event2 { + return (this._event ??= new Event2({ client: this.client })) + } +} + export class OpencodeClient extends HeyApiClient { public static readonly __registry = new HeyApiRegistry() @@ -5690,13 +5695,13 @@ export class OpencodeClient extends HeyApiClient { return (this._sync ??= new Sync({ client: this.client })) } - private _v2?: V2 - get v2(): V2 { - return (this._v2 ??= new V2({ client: this.client })) - } - private _tui?: Tui get tui(): Tui { return (this._tui ??= new Tui({ client: this.client })) } + + private _v2?: V2 + get v2(): V2 { + return (this._v2 ??= new V2({ client: this.client })) + } } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9a809fdc43..dba1baeef5 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2499,56 +2499,6 @@ export type SessionBusyError = { message: string } -export type V2SessionsResponse = { - items: Array - cursor: { - previous?: string - next?: string - } -} - -export type InvalidCursorError = { - _tag: "InvalidCursorError" - message: string -} - -export type UnauthorizedError = { - _tag: "UnauthorizedError" - message: string -} - -export type SessionNotFoundError = { - _tag: "SessionNotFoundError" - sessionID: string - message: string -} - -export type ServiceUnavailableError = { - _tag: "ServiceUnavailableError" - message: string - service?: string -} - -export type UnknownError1 = { - _tag: "UnknownError" - message: string - ref?: string -} - -export type V2SessionMessagesResponse = { - items: Array - cursor: { - previous?: string - next?: string - } -} - -export type ProviderNotFoundError = { - _tag: "ProviderNotFoundError" - providerID: string - message: string -} - export type EventTuiPromptAppend = { type: "tui.prompt.append" properties: { @@ -2625,6 +2575,56 @@ export type WorkspaceWarpError = { } } +export type UnauthorizedError = { + _tag: "UnauthorizedError" + message: string +} + +export type V2SessionsResponse = { + items: Array + cursor: { + previous?: string + next?: string + } +} + +export type InvalidCursorError = { + _tag: "InvalidCursorError" + message: string +} + +export type SessionNotFoundError = { + _tag: "SessionNotFoundError" + sessionID: string + message: string +} + +export type ServiceUnavailableError = { + _tag: "ServiceUnavailableError" + message: string + service?: string +} + +export type UnknownError1 = { + _tag: "UnknownError" + message: string + ref?: string +} + +export type V2SessionMessagesResponse = { + items: Array + cursor: { + previous?: string + next?: string + } +} + +export type ProviderNotFoundError = { + _tag: "ProviderNotFoundError" + providerID: string + message: string +} + export type EffectHttpApiErrorForbidden = { _tag: "Forbidden" } @@ -3452,6 +3452,49 @@ export type ProjectCopyCopy = { directory: string } +export type LocationInfo = { + directory: string + workspaceID?: string + project: { + id: string + directory: string + } +} + +export type PermissionV2Effect = "allow" | "deny" | "ask" + +export type PermissionV2Rule = { + action: string + resource: string + effect: PermissionV2Effect +} + +export type PermissionV2Ruleset = Array + +export type AgentV2Info = { + id: string + model?: { + id: string + providerID: string + variant?: string + } + request: { + headers: { + [key: string]: string + } + body: { + [key: string]: unknown + } + } + system?: string + description?: string + mode: "subagent" | "primary" | "all" + hidden: boolean + color?: string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" + steps?: number + permissions: PermissionV2Ruleset +} + export type LocationRef = { directory: string workspaceID?: string @@ -3692,15 +3735,6 @@ export type SessionMessage = | SessionMessageAssistant | SessionMessageCompaction -export type LocationInfo = { - directory: string - workspaceID?: string - project: { - id: string - directory: string - } -} - export type ProviderV2Info = { id: string name: string @@ -8188,767 +8222,6 @@ export type SyncHistoryListResponses = { export type SyncHistoryListResponse = SyncHistoryListResponses[keyof SyncHistoryListResponses] -export type V2SessionListData = { - body?: never - path?: never - query?: { - workspace?: string - limit?: number - order?: "asc" | "desc" - search?: string - directory?: string - project?: string - subpath?: string - /** - * Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. - */ - cursor?: string - } - url: "/api/session" -} - -export type V2SessionListErrors = { - /** - * InvalidCursorError | InvalidRequestError - */ - 400: InvalidCursorError | InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors] - -export type V2SessionListResponses = { - /** - * Success - */ - 200: { - data: V2SessionsResponse - } -} - -export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] - -export type V2SessionPromptData = { - body?: { - prompt: Prompt - delivery?: SessionDelivery - } - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/api/session/{sessionID}/prompt" -} - -export type V2SessionPromptErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableError -} - -export type V2SessionPromptError = V2SessionPromptErrors[keyof V2SessionPromptErrors] - -export type V2SessionPromptResponses = { - /** - * Success - */ - 200: { - data: SessionMessage - } -} - -export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses] - -export type V2SessionCompactData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/api/session/{sessionID}/compact" -} - -export type V2SessionCompactErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableError -} - -export type V2SessionCompactError = V2SessionCompactErrors[keyof V2SessionCompactErrors] - -export type V2SessionCompactResponses = { - /** - * - */ - 204: void -} - -export type V2SessionCompactResponse = V2SessionCompactResponses[keyof V2SessionCompactResponses] - -export type V2SessionWaitData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/api/session/{sessionID}/wait" -} - -export type V2SessionWaitErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableError -} - -export type V2SessionWaitError = V2SessionWaitErrors[keyof V2SessionWaitErrors] - -export type V2SessionWaitResponses = { - /** - * - */ - 204: void -} - -export type V2SessionWaitResponse = V2SessionWaitResponses[keyof V2SessionWaitResponses] - -export type V2SessionContextData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - } - url: "/api/session/{sessionID}/context" -} - -export type V2SessionContextErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * UnknownError - */ - 500: UnknownError1 -} - -export type V2SessionContextError = V2SessionContextErrors[keyof V2SessionContextErrors] - -export type V2SessionContextResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses] - -export type V2SessionMessagesData = { - body?: never - path: { - sessionID: string - } - query?: { - directory?: string - workspace?: string - limit?: number - order?: "asc" | "desc" - /** - * Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order. - */ - cursor?: string - } - url: "/api/session/{sessionID}/message" -} - -export type V2SessionMessagesErrors = { - /** - * InvalidCursorError | InvalidRequestError - */ - 400: InvalidCursorError | InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError - /** - * UnknownError - */ - 500: UnknownError1 -} - -export type V2SessionMessagesError = V2SessionMessagesErrors[keyof V2SessionMessagesErrors] - -export type V2SessionMessagesResponses = { - /** - * Success - */ - 200: { - data: V2SessionMessagesResponse - } -} - -export type V2SessionMessagesResponse2 = V2SessionMessagesResponses[keyof V2SessionMessagesResponses] - -export type V2ModelListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string - workspace?: string - } - } - url: "/api/model" -} - -export type V2ModelListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableError -} - -export type V2ModelListError = V2ModelListErrors[keyof V2ModelListErrors] - -export type V2ModelListResponses = { - /** - * Success - */ - 200: { - location: LocationInfo - data: Array - } -} - -export type V2ModelListResponse = V2ModelListResponses[keyof V2ModelListResponses] - -export type V2ProviderListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string - workspace?: string - } - } - url: "/api/provider" -} - -export type V2ProviderListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableError -} - -export type V2ProviderListError = V2ProviderListErrors[keyof V2ProviderListErrors] - -export type V2ProviderListResponses = { - /** - * Success - */ - 200: { - location: LocationInfo - data: Array - } -} - -export type V2ProviderListResponse = V2ProviderListResponses[keyof V2ProviderListResponses] - -export type V2ProviderGetData = { - body?: never - path: { - providerID: string - } - query?: { - location?: { - directory?: string - workspace?: string - } - } - url: "/api/provider/{providerID}" -} - -export type V2ProviderGetErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * ProviderNotFoundError - */ - 404: ProviderNotFoundError - /** - * ServiceUnavailableError - */ - 503: ServiceUnavailableError -} - -export type V2ProviderGetError = V2ProviderGetErrors[keyof V2ProviderGetErrors] - -export type V2ProviderGetResponses = { - /** - * Success - */ - 200: { - location: LocationInfo - data: ProviderV2Info - } -} - -export type V2ProviderGetResponse = V2ProviderGetResponses[keyof V2ProviderGetResponses] - -export type V2PermissionRequestListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string - workspace?: string - } - } - url: "/api/permission/request" -} - -export type V2PermissionRequestListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PermissionRequestListError = V2PermissionRequestListErrors[keyof V2PermissionRequestListErrors] - -export type V2PermissionRequestListResponses = { - /** - * Success - */ - 200: { - location: LocationInfo - data: Array - } -} - -export type V2PermissionRequestListResponse = V2PermissionRequestListResponses[keyof V2PermissionRequestListResponses] - -export type V2SessionPermissionListData = { - body?: never - path: { - sessionID: string - } - query?: never - url: "/api/session/{sessionID}/permission/request" -} - -export type V2SessionPermissionListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError - */ - 404: SessionNotFoundError -} - -export type V2SessionPermissionListError = V2SessionPermissionListErrors[keyof V2SessionPermissionListErrors] - -export type V2SessionPermissionListResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2SessionPermissionListResponse = V2SessionPermissionListResponses[keyof V2SessionPermissionListResponses] - -export type V2SessionPermissionReplyData = { - body?: { - reply: PermissionV2Reply - message?: string - } - path: { - sessionID: string - requestID: string - } - query?: never - url: "/api/session/{sessionID}/permission/request/{requestID}/reply" -} - -export type V2SessionPermissionReplyErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError - /** - * SessionNotFoundError | PermissionNotFoundError - */ - 404: SessionNotFoundError | PermissionNotFoundError -} - -export type V2SessionPermissionReplyError = V2SessionPermissionReplyErrors[keyof V2SessionPermissionReplyErrors] - -export type V2SessionPermissionReplyResponses = { - /** - * - */ - 204: void -} - -export type V2SessionPermissionReplyResponse = - V2SessionPermissionReplyResponses[keyof V2SessionPermissionReplyResponses] - -export type V2PermissionSavedListData = { - body?: never - path?: never - query?: { - projectID?: string - } - url: "/api/permission/saved" -} - -export type V2PermissionSavedListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PermissionSavedListError = V2PermissionSavedListErrors[keyof V2PermissionSavedListErrors] - -export type V2PermissionSavedListResponses = { - /** - * Success - */ - 200: { - data: Array - } -} - -export type V2PermissionSavedListResponse = V2PermissionSavedListResponses[keyof V2PermissionSavedListResponses] - -export type V2PermissionSavedRemoveData = { - body?: never - path: { - id: string - } - query?: never - url: "/api/permission/saved/{id}" -} - -export type V2PermissionSavedRemoveErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2PermissionSavedRemoveError = V2PermissionSavedRemoveErrors[keyof V2PermissionSavedRemoveErrors] - -export type V2PermissionSavedRemoveResponses = { - /** - * - */ - 204: void -} - -export type V2PermissionSavedRemoveResponse = V2PermissionSavedRemoveResponses[keyof V2PermissionSavedRemoveResponses] - -export type V2FsReadData = { - body?: never - path?: never - query: { - location?: { - directory?: string - workspace?: string - } - path: string - reference?: string - } - url: "/api/fs/read" -} - -export type V2FsReadErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2FsReadError = V2FsReadErrors[keyof V2FsReadErrors] - -export type V2FsReadResponses = { - /** - * Success - */ - 200: { - location: LocationInfo - data: LocationFileSystemTextContent | LocationFileSystemBinaryContent - } -} - -export type V2FsReadResponse = V2FsReadResponses[keyof V2FsReadResponses] - -export type V2FsListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string - workspace?: string - } - path?: string - reference?: string - } - url: "/api/fs/list" -} - -export type V2FsListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2FsListError = V2FsListErrors[keyof V2FsListErrors] - -export type V2FsListResponses = { - /** - * Success - */ - 200: { - location: LocationInfo - data: Array - } -} - -export type V2FsListResponse = V2FsListResponses[keyof V2FsListResponses] - -export type V2CommandListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string - workspace?: string - } - } - url: "/api/command" -} - -export type V2CommandListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2CommandListError = V2CommandListErrors[keyof V2CommandListErrors] - -export type V2CommandListResponses = { - /** - * Success - */ - 200: { - location: LocationInfo - data: Array - } -} - -export type V2CommandListResponse = V2CommandListResponses[keyof V2CommandListResponses] - -export type V2SkillListData = { - body?: never - path?: never - query?: { - location?: { - directory?: string - workspace?: string - } - } - url: "/api/skill" -} - -export type V2SkillListErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2SkillListError = V2SkillListErrors[keyof V2SkillListErrors] - -export type V2SkillListResponses = { - /** - * Success - */ - 200: { - location: LocationInfo - data: Array - } -} - -export type V2SkillListResponse = V2SkillListResponses[keyof V2SkillListResponses] - -export type V2EventSubscribeData = { - body?: never - path?: never - query?: { - location?: { - directory?: string - workspace?: string - } - } - url: "/api/event" -} - -export type V2EventSubscribeErrors = { - /** - * InvalidRequestError - */ - 400: InvalidRequestError - /** - * UnauthorizedError - */ - 401: UnauthorizedError -} - -export type V2EventSubscribeError = V2EventSubscribeErrors[keyof V2EventSubscribeErrors] - -export type V2EventSubscribeResponses = { - /** - * Success - */ - 200: string -} - -export type V2EventSubscribeResponse = V2EventSubscribeResponses[keyof V2EventSubscribeResponses] - export type TuiAppendPromptData = { body?: { text: string @@ -9564,6 +8837,821 @@ export type ExperimentalWorkspaceWarpResponses = { export type ExperimentalWorkspaceWarpResponse = ExperimentalWorkspaceWarpResponses[keyof ExperimentalWorkspaceWarpResponses] +export type V2HealthGetData = { + body?: never + path?: never + query?: never + url: "/api/health" +} + +export type V2HealthGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2HealthGetError = V2HealthGetErrors[keyof V2HealthGetErrors] + +export type V2HealthGetResponses = { + /** + * Success + */ + 200: { + healthy: true + } +} + +export type V2HealthGetResponse = V2HealthGetResponses[keyof V2HealthGetResponses] + +export type V2AgentListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/agent" +} + +export type V2AgentListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2AgentListError = V2AgentListErrors[keyof V2AgentListErrors] + +export type V2AgentListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2AgentListResponse = V2AgentListResponses[keyof V2AgentListResponses] + +export type V2SessionListData = { + body?: never + path?: never + query?: { + workspace?: string + limit?: number + order?: "asc" | "desc" + search?: string + directory?: string + project?: string + subpath?: string + /** + * Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. + */ + cursor?: string + } + url: "/api/session" +} + +export type V2SessionListErrors = { + /** + * InvalidCursorError | InvalidRequestError + */ + 400: InvalidCursorError | InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2SessionListError = V2SessionListErrors[keyof V2SessionListErrors] + +export type V2SessionListResponses = { + /** + * Success + */ + 200: { + data: V2SessionsResponse + } +} + +export type V2SessionListResponse = V2SessionListResponses[keyof V2SessionListResponses] + +export type V2SessionPromptData = { + body?: { + prompt: Prompt + delivery?: SessionDelivery + } + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/prompt" +} + +export type V2SessionPromptErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2SessionPromptError = V2SessionPromptErrors[keyof V2SessionPromptErrors] + +export type V2SessionPromptResponses = { + /** + * Success + */ + 200: { + data: SessionMessage + } +} + +export type V2SessionPromptResponse = V2SessionPromptResponses[keyof V2SessionPromptResponses] + +export type V2SessionCompactData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/compact" +} + +export type V2SessionCompactErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2SessionCompactError = V2SessionCompactErrors[keyof V2SessionCompactErrors] + +export type V2SessionCompactResponses = { + /** + * + */ + 204: void +} + +export type V2SessionCompactResponse = V2SessionCompactResponses[keyof V2SessionCompactResponses] + +export type V2SessionWaitData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/wait" +} + +export type V2SessionWaitErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2SessionWaitError = V2SessionWaitErrors[keyof V2SessionWaitErrors] + +export type V2SessionWaitResponses = { + /** + * + */ + 204: void +} + +export type V2SessionWaitResponse = V2SessionWaitResponses[keyof V2SessionWaitResponses] + +export type V2SessionContextData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/context" +} + +export type V2SessionContextErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * UnknownError + */ + 500: UnknownError1 +} + +export type V2SessionContextError = V2SessionContextErrors[keyof V2SessionContextErrors] + +export type V2SessionContextResponses = { + /** + * Success + */ + 200: { + data: Array + } +} + +export type V2SessionContextResponse = V2SessionContextResponses[keyof V2SessionContextResponses] + +export type V2SessionMessagesData = { + body?: never + path: { + sessionID: string + } + query?: { + limit?: number + order?: "asc" | "desc" + /** + * Opaque pagination cursor returned as cursor.previous or cursor.next in the previous response. Do not combine with order. + */ + cursor?: string + } + url: "/api/session/{sessionID}/message" +} + +export type V2SessionMessagesErrors = { + /** + * InvalidCursorError | InvalidRequestError + */ + 400: InvalidCursorError | InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError + /** + * UnknownError + */ + 500: UnknownError1 +} + +export type V2SessionMessagesError = V2SessionMessagesErrors[keyof V2SessionMessagesErrors] + +export type V2SessionMessagesResponses = { + /** + * Success + */ + 200: { + data: V2SessionMessagesResponse + } +} + +export type V2SessionMessagesResponse2 = V2SessionMessagesResponses[keyof V2SessionMessagesResponses] + +export type V2ModelListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/model" +} + +export type V2ModelListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2ModelListError = V2ModelListErrors[keyof V2ModelListErrors] + +export type V2ModelListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2ModelListResponse = V2ModelListResponses[keyof V2ModelListResponses] + +export type V2ProviderListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/provider" +} + +export type V2ProviderListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2ProviderListError = V2ProviderListErrors[keyof V2ProviderListErrors] + +export type V2ProviderListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2ProviderListResponse = V2ProviderListResponses[keyof V2ProviderListResponses] + +export type V2ProviderGetData = { + body?: never + path: { + providerID: string + } + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/provider/{providerID}" +} + +export type V2ProviderGetErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * ProviderNotFoundError + */ + 404: ProviderNotFoundError + /** + * ServiceUnavailableError + */ + 503: ServiceUnavailableError +} + +export type V2ProviderGetError = V2ProviderGetErrors[keyof V2ProviderGetErrors] + +export type V2ProviderGetResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: ProviderV2Info + } +} + +export type V2ProviderGetResponse = V2ProviderGetResponses[keyof V2ProviderGetResponses] + +export type V2PermissionRequestListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/permission/request" +} + +export type V2PermissionRequestListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2PermissionRequestListError = V2PermissionRequestListErrors[keyof V2PermissionRequestListErrors] + +export type V2PermissionRequestListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2PermissionRequestListResponse = V2PermissionRequestListResponses[keyof V2PermissionRequestListResponses] + +export type V2SessionPermissionListData = { + body?: never + path: { + sessionID: string + } + query?: never + url: "/api/session/{sessionID}/permission/request" +} + +export type V2SessionPermissionListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError + */ + 404: SessionNotFoundError +} + +export type V2SessionPermissionListError = V2SessionPermissionListErrors[keyof V2SessionPermissionListErrors] + +export type V2SessionPermissionListResponses = { + /** + * Success + */ + 200: { + data: Array + } +} + +export type V2SessionPermissionListResponse = V2SessionPermissionListResponses[keyof V2SessionPermissionListResponses] + +export type V2SessionPermissionReplyData = { + body?: { + reply: PermissionV2Reply + message?: string + } + path: { + sessionID: string + requestID: string + } + query?: never + url: "/api/session/{sessionID}/permission/request/{requestID}/reply" +} + +export type V2SessionPermissionReplyErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError + /** + * SessionNotFoundError | PermissionNotFoundError + */ + 404: SessionNotFoundError | PermissionNotFoundError +} + +export type V2SessionPermissionReplyError = V2SessionPermissionReplyErrors[keyof V2SessionPermissionReplyErrors] + +export type V2SessionPermissionReplyResponses = { + /** + * + */ + 204: void +} + +export type V2SessionPermissionReplyResponse = + V2SessionPermissionReplyResponses[keyof V2SessionPermissionReplyResponses] + +export type V2PermissionSavedListData = { + body?: never + path?: never + query?: { + projectID?: string + } + url: "/api/permission/saved" +} + +export type V2PermissionSavedListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2PermissionSavedListError = V2PermissionSavedListErrors[keyof V2PermissionSavedListErrors] + +export type V2PermissionSavedListResponses = { + /** + * Success + */ + 200: { + data: Array + } +} + +export type V2PermissionSavedListResponse = V2PermissionSavedListResponses[keyof V2PermissionSavedListResponses] + +export type V2PermissionSavedRemoveData = { + body?: never + path: { + id: string + } + query?: never + url: "/api/permission/saved/{id}" +} + +export type V2PermissionSavedRemoveErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2PermissionSavedRemoveError = V2PermissionSavedRemoveErrors[keyof V2PermissionSavedRemoveErrors] + +export type V2PermissionSavedRemoveResponses = { + /** + * + */ + 204: void +} + +export type V2PermissionSavedRemoveResponse = V2PermissionSavedRemoveResponses[keyof V2PermissionSavedRemoveResponses] + +export type V2FsReadData = { + body?: never + path?: never + query: { + location?: { + directory?: string + workspace?: string + } + path: string + reference?: string + } + url: "/api/fs/read" +} + +export type V2FsReadErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2FsReadError = V2FsReadErrors[keyof V2FsReadErrors] + +export type V2FsReadResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: LocationFileSystemTextContent | LocationFileSystemBinaryContent + } +} + +export type V2FsReadResponse = V2FsReadResponses[keyof V2FsReadResponses] + +export type V2FsListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + path?: string + reference?: string + } + url: "/api/fs/list" +} + +export type V2FsListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2FsListError = V2FsListErrors[keyof V2FsListErrors] + +export type V2FsListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2FsListResponse = V2FsListResponses[keyof V2FsListResponses] + +export type V2CommandListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/command" +} + +export type V2CommandListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2CommandListError = V2CommandListErrors[keyof V2CommandListErrors] + +export type V2CommandListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2CommandListResponse = V2CommandListResponses[keyof V2CommandListResponses] + +export type V2SkillListData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/skill" +} + +export type V2SkillListErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2SkillListError = V2SkillListErrors[keyof V2SkillListErrors] + +export type V2SkillListResponses = { + /** + * Success + */ + 200: { + location: LocationInfo + data: Array + } +} + +export type V2SkillListResponse = V2SkillListResponses[keyof V2SkillListResponses] + +export type V2EventSubscribeData = { + body?: never + path?: never + query?: { + location?: { + directory?: string + workspace?: string + } + } + url: "/api/event" +} + +export type V2EventSubscribeErrors = { + /** + * InvalidRequestError + */ + 400: InvalidRequestError + /** + * UnauthorizedError + */ + 401: UnauthorizedError +} + +export type V2EventSubscribeError = V2EventSubscribeErrors[keyof V2EventSubscribeErrors] + +export type V2EventSubscribeResponses = { + /** + * Success + */ + 200: string +} + +export type V2EventSubscribeResponse = V2EventSubscribeResponses[keyof V2EventSubscribeResponses] + export type PtyConnectData = { body?: never path: { diff --git a/packages/server/package.json b/packages/server/package.json new file mode 100644 index 0000000000..0eb698857f --- /dev/null +++ b/packages/server/package.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode-ai/server", + "version": "1.15.13", + "private": true, + "type": "module", + "license": "MIT", + "exports": { + "./*": "./src/*.ts" + }, + "scripts": { + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@opencode-ai/core": "workspace:*", + "drizzle-orm": "catalog:", + "effect": "catalog:" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:" + } +} diff --git a/packages/server/src/api.ts b/packages/server/src/api.ts new file mode 100644 index 0000000000..f45ef9288f --- /dev/null +++ b/packages/server/src/api.ts @@ -0,0 +1,36 @@ +import { HttpApi, OpenApi } from "effect/unstable/httpapi" +import { SchemaErrorMiddleware } from "./middleware/schema-error" +import { MessageGroup } from "./groups/v2/message" +import { ModelGroup } from "./groups/v2/model" +import { ProviderGroup } from "./groups/v2/provider" +import { SessionGroup } from "./groups/v2/session" +import { PermissionGroup, PermissionSavedGroup, SessionPermissionGroup } from "./groups/v2/permission" +import { FileSystemGroup } from "./groups/v2/fs" +import { CommandGroup } from "./groups/v2/command" +import { SkillGroup } from "./groups/v2/skill" +import { EventGroup } from "./groups/v2/event" +import { AgentGroup } from "./groups/v2/agent" +import { HealthGroup } from "./groups/v2/health" + +export const V2Api = HttpApi.make("v2") + .add(HealthGroup) + .add(AgentGroup) + .add(SessionGroup) + .add(MessageGroup) + .add(ModelGroup) + .add(ProviderGroup) + .add(PermissionGroup) + .add(SessionPermissionGroup) + .add(PermissionSavedGroup) + .add(FileSystemGroup) + .add(CommandGroup) + .add(SkillGroup) + .add(EventGroup) + .annotateMerge( + OpenApi.annotations({ + title: "opencode experimental HttpApi", + version: "0.0.1", + description: "Experimental HttpApi surface for selected instance routes.", + }), + ) + .middleware(SchemaErrorMiddleware) diff --git a/packages/server/src/auth.ts b/packages/server/src/auth.ts new file mode 100644 index 0000000000..6758fda3d2 --- /dev/null +++ b/packages/server/src/auth.ts @@ -0,0 +1,63 @@ +export * as ServerAuth from "./auth" + +import { Config as EffectConfig, Context, Effect, Layer, Option, Redacted } from "effect" + +export type Credentials = { + password?: string + username?: string +} + +export type DecodedCredentials = { + readonly username: string + readonly password: Redacted.Redacted +} + +export type Info = { + readonly password: Option.Option + readonly username: string +} + +export class Config extends Context.Service()("@opencode/ServerAuthConfig") { + static layer(input: Info) { + return Layer.succeed(this, this.of(input)) + } + + static get defaultLayer() { + return Layer.effect( + this, + Effect.gen(function* () { + return Config.of( + yield* EffectConfig.all({ + password: EffectConfig.string("OPENCODE_SERVER_PASSWORD").pipe(EffectConfig.option), + username: EffectConfig.string("OPENCODE_SERVER_USERNAME").pipe(EffectConfig.withDefault("opencode")), + }), + ) + }), + ) + } +} + +export function required(config: Info) { + return Option.isSome(config.password) && config.password.value !== "" +} + +export function authorized(credentials: DecodedCredentials, config: Info) { + return ( + Option.isSome(config.password) && + credentials.username === config.username && + Redacted.value(credentials.password) === config.password.value + ) +} + +export function header(credentials?: Credentials) { + const password = credentials?.password ?? process.env.OPENCODE_SERVER_PASSWORD + if (!password) return undefined + + return `Basic ${Buffer.from(`${credentials?.username ?? process.env.OPENCODE_SERVER_USERNAME ?? "opencode"}:${password}`).toString("base64")}` +} + +export function headers(credentials?: Credentials) { + const authorization = header(credentials) + if (!authorization) return undefined + return { Authorization: authorization } +} diff --git a/packages/server/src/errors.ts b/packages/server/src/errors.ts new file mode 100644 index 0000000000..b9862d19f7 --- /dev/null +++ b/packages/server/src/errors.ts @@ -0,0 +1,68 @@ +import { Schema } from "effect" + +export class InvalidRequestError extends Schema.TaggedErrorClass()( + "InvalidRequestError", + { + message: Schema.String, + kind: Schema.optional(Schema.String), + field: Schema.optional(Schema.String), + }, + { httpApiStatus: 400 }, +) {} + +export class UnauthorizedError extends Schema.TaggedErrorClass()( + "UnauthorizedError", + { message: Schema.String }, + { httpApiStatus: 401 }, +) {} + +export class ServiceUnavailableError extends Schema.TaggedErrorClass()( + "ServiceUnavailableError", + { + message: Schema.String, + service: Schema.optional(Schema.String), + }, + { httpApiStatus: 503 }, +) {} + +export class UnknownError extends Schema.TaggedErrorClass()( + "UnknownError", + { + message: Schema.String, + ref: Schema.optional(Schema.String), + }, + { httpApiStatus: 500 }, +) {} + +export class ProviderNotFoundError extends Schema.TaggedErrorClass()( + "ProviderNotFoundError", + { + providerID: Schema.String, + message: Schema.String, + }, + { httpApiStatus: 404 }, +) {} + +export class SessionNotFoundError extends Schema.TaggedErrorClass()( + "SessionNotFoundError", + { + sessionID: Schema.String, + message: Schema.String, + }, + { httpApiStatus: 404 }, +) {} + +export class InvalidCursorError extends Schema.TaggedErrorClass()( + "InvalidCursorError", + { message: Schema.String }, + { httpApiStatus: 400 }, +) {} + +export class PermissionNotFoundError extends Schema.TaggedErrorClass()( + "PermissionNotFoundError", + { + requestID: Schema.String, + message: Schema.String, + }, + { httpApiStatus: 404 }, +) {} diff --git a/packages/server/src/groups/v2/agent.ts b/packages/server/src/groups/v2/agent.ts new file mode 100644 index 0000000000..1fdc33d377 --- /dev/null +++ b/packages/server/src/groups/v2/agent.ts @@ -0,0 +1,24 @@ +import { AgentV2 } from "@opencode-ai/core/agent" +import { Location } from "@opencode-ai/core/location" +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { V2Authorization } from "../../middleware/authorization" +import { LocationQuery, locationQueryOpenApi, V2LocationMiddleware } from "./location" + +export const AgentGroup = HttpApiGroup.make("v2.agent") + .add( + HttpApiEndpoint.get("agents", "/api/agent", { + query: LocationQuery, + success: Location.response(Schema.Array(AgentV2.Info)), + }) + .annotateMerge(locationQueryOpenApi) + .annotateMerge( + OpenApi.annotations({ + identifier: "v2.agent.list", + summary: "List v2 agents", + description: "Retrieve currently registered v2 agents.", + }), + ), + ) + .middleware(V2LocationMiddleware) + .middleware(V2Authorization) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/command.ts b/packages/server/src/groups/v2/command.ts similarity index 100% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/command.ts rename to packages/server/src/groups/v2/command.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/event.ts b/packages/server/src/groups/v2/event.ts similarity index 100% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/event.ts rename to packages/server/src/groups/v2/event.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/fs.ts b/packages/server/src/groups/v2/fs.ts similarity index 100% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/fs.ts rename to packages/server/src/groups/v2/fs.ts diff --git a/packages/server/src/groups/v2/health.ts b/packages/server/src/groups/v2/health.ts new file mode 100644 index 0000000000..9ad38210db --- /dev/null +++ b/packages/server/src/groups/v2/health.ts @@ -0,0 +1,17 @@ +import { Schema } from "effect" +import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { V2Authorization } from "../../middleware/authorization" + +export const HealthGroup = HttpApiGroup.make("v2.health") + .add( + HttpApiEndpoint.get("health", "/api/health", { + success: Schema.Struct({ healthy: Schema.Literal(true) }), + }).annotateMerge( + OpenApi.annotations({ + identifier: "v2.health.get", + summary: "Check v2 server health", + description: "Check whether the v2 API server is ready to accept requests.", + }), + ), + ) + .middleware(V2Authorization) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts b/packages/server/src/groups/v2/location.ts similarity index 96% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts rename to packages/server/src/groups/v2/location.ts index 34967d6087..23083899d8 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/location.ts +++ b/packages/server/src/groups/v2/location.ts @@ -1,4 +1,5 @@ import { Catalog } from "@opencode-ai/core/catalog" +import { AgentV2 } from "@opencode-ai/core/agent" import { CommandV2 } from "@opencode-ai/core/command" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-layer" @@ -55,7 +56,9 @@ export class V2LocationMiddleware extends HttpApiMiddleware.Service< { provides: | Catalog.Service + | AgentV2.Service | CommandV2.Service + | Location.Service | PluginBoot.Service | PermissionV2.Service | ProjectReference.Service diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts b/packages/server/src/groups/v2/message.ts similarity index 91% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts rename to packages/server/src/groups/v2/message.ts index 109b63f97d..75ae4aadd6 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/message.ts +++ b/packages/server/src/groups/v2/message.ts @@ -1,14 +1,12 @@ -import { SessionID } from "@/session/schema" +import { SessionV2 } from "@opencode-ai/core/session" import { SessionMessage } from "@opencode-ai/core/session/message" import { Schema } from "effect" import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors" import { V2Authorization } from "../../middleware/authorization" -import { WorkspaceRoutingQueryFields } from "../../middleware/workspace-routing" import { data } from "./response" export const MessagesQuery = Schema.Struct({ - ...WorkspaceRoutingQueryFields, limit: Schema.optional( Schema.NumberFromString.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(200)), ).annotate({ @@ -28,7 +26,7 @@ export const MessagesQuery = Schema.Struct({ export const MessageGroup = HttpApiGroup.make("v2.message") .add( HttpApiEndpoint.get("messages", "/api/session/:sessionID/message", { - params: { sessionID: SessionID }, + params: { sessionID: SessionV2.ID }, query: MessagesQuery, success: data(Schema.Struct({ items: Schema.Array(SessionMessage.Message), diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/model.ts b/packages/server/src/groups/v2/model.ts similarity index 100% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/model.ts rename to packages/server/src/groups/v2/model.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/permission.ts b/packages/server/src/groups/v2/permission.ts similarity index 100% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/permission.ts rename to packages/server/src/groups/v2/permission.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/provider.ts b/packages/server/src/groups/v2/provider.ts similarity index 100% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/provider.ts rename to packages/server/src/groups/v2/provider.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/response.ts b/packages/server/src/groups/v2/response.ts similarity index 100% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/response.ts rename to packages/server/src/groups/v2/response.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts b/packages/server/src/groups/v2/session.ts similarity index 93% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts rename to packages/server/src/groups/v2/session.ts index fdf6b26dd2..94fddb18b7 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/session.ts +++ b/packages/server/src/groups/v2/session.ts @@ -1,4 +1,3 @@ -import { SessionID } from "@/session/schema" import { SessionMessage } from "@opencode-ai/core/session/message" import { Prompt } from "@opencode-ai/core/session/prompt" import { SessionV2 } from "@opencode-ai/core/session" @@ -15,7 +14,6 @@ import { UnknownError, } from "../../errors" import { V2Authorization } from "../../middleware/authorization" -import { WorkspaceRoutingQuery } from "../../middleware/workspace-routing" import { data } from "./response" const SessionsQueryFields = { @@ -108,8 +106,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session") ) .add( HttpApiEndpoint.post("prompt", "/api/session/:sessionID/prompt", { - params: { sessionID: SessionID }, - query: WorkspaceRoutingQuery, + params: { sessionID: SessionV2.ID }, payload: Schema.Struct({ prompt: Prompt, delivery: SessionV2.Delivery.pipe(Schema.optional), @@ -126,8 +123,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session") ) .add( HttpApiEndpoint.post("compact", "/api/session/:sessionID/compact", { - params: { sessionID: SessionID }, - query: WorkspaceRoutingQuery, + params: { sessionID: SessionV2.ID }, success: HttpApiSchema.NoContent, error: [SessionNotFoundError, ServiceUnavailableError], }).annotateMerge( @@ -140,8 +136,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session") ) .add( HttpApiEndpoint.post("wait", "/api/session/:sessionID/wait", { - params: { sessionID: SessionID }, - query: WorkspaceRoutingQuery, + params: { sessionID: SessionV2.ID }, success: HttpApiSchema.NoContent, error: [SessionNotFoundError, ServiceUnavailableError], }).annotateMerge( @@ -154,8 +149,7 @@ export const SessionGroup = HttpApiGroup.make("v2.session") ) .add( HttpApiEndpoint.get("context", "/api/session/:sessionID/context", { - params: { sessionID: SessionID }, - query: WorkspaceRoutingQuery, + params: { sessionID: SessionV2.ID }, success: data(Schema.Array(SessionMessage.Message)), error: [SessionNotFoundError, UnknownError], }).annotateMerge( diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/v2/skill.ts b/packages/server/src/groups/v2/skill.ts similarity index 100% rename from packages/opencode/src/server/routes/instance/httpapi/groups/v2/skill.ts rename to packages/server/src/groups/v2/skill.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts b/packages/server/src/handlers.ts similarity index 51% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts rename to packages/server/src/handlers.ts index c6152f6ddd..8e54d84b18 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts +++ b/packages/server/src/handlers.ts @@ -2,18 +2,22 @@ import { SessionV2 } from "@opencode-ai/core/session" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { PermissionSaved } from "@opencode-ai/core/permission/saved" import { Layer } from "effect" -import { layer as v2LocationLayer } from "../groups/v2/location" -import { messageHandlers } from "./v2/message" -import { modelHandlers } from "./v2/model" -import { providerHandlers } from "./v2/provider" -import { sessionHandlers } from "./v2/session" -import { permissionHandlers, savedPermissionHandlers, sessionPermissionHandlers } from "./v2/permission" -import { fileSystemHandlers } from "./v2/fs" -import { commandHandlers } from "./v2/command" -import { skillHandlers } from "./v2/skill" -import { eventHandlers } from "./v2/event" +import { layer as v2LocationLayer } from "./groups/v2/location" +import { messageHandlers } from "./handlers/v2/message" +import { modelHandlers } from "./handlers/v2/model" +import { providerHandlers } from "./handlers/v2/provider" +import { sessionHandlers } from "./handlers/v2/session" +import { permissionHandlers, savedPermissionHandlers, sessionPermissionHandlers } from "./handlers/v2/permission" +import { fileSystemHandlers } from "./handlers/v2/fs" +import { commandHandlers } from "./handlers/v2/command" +import { skillHandlers } from "./handlers/v2/skill" +import { eventHandlers } from "./handlers/v2/event" +import { agentHandlers } from "./handlers/v2/agent" +import { healthHandlers } from "./handlers/v2/health" export const v2Handlers = Layer.mergeAll( + healthHandlers, + agentHandlers, sessionHandlers, messageHandlers, modelHandlers, diff --git a/packages/server/src/handlers/v2/agent.ts b/packages/server/src/handlers/v2/agent.ts new file mode 100644 index 0000000000..ae759e0a1b --- /dev/null +++ b/packages/server/src/handlers/v2/agent.ts @@ -0,0 +1,15 @@ +import { AgentV2 } from "@opencode-ai/core/agent" +import { PluginBoot } from "@opencode-ai/core/plugin/boot" +import { Effect } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { V2Api } from "../../api" +import { response } from "../../groups/v2/location" + +export const agentHandlers = HttpApiBuilder.group(V2Api, "v2.agent", (handlers) => + handlers.handle("agents", () => + Effect.gen(function* () { + yield* PluginBoot.Service.use((plugin) => plugin.wait()) + return yield* response(AgentV2.Service.use((agent) => agent.all())) + }), + ), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/command.ts b/packages/server/src/handlers/v2/command.ts similarity index 67% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2/command.ts rename to packages/server/src/handlers/v2/command.ts index d9448e0a05..551ad4bce2 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/command.ts +++ b/packages/server/src/handlers/v2/command.ts @@ -1,9 +1,9 @@ import { CommandV2 } from "@opencode-ai/core/command" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../../api" +import { V2Api } from "../../api" import { response } from "../../groups/v2/location" -export const commandHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.command", (handlers) => +export const commandHandlers = HttpApiBuilder.group(V2Api, "v2.command", (handlers) => handlers.handle("commands", () => response(CommandV2.Service.use((command) => command.list()))), ) diff --git a/packages/server/src/handlers/v2/event.ts b/packages/server/src/handlers/v2/event.ts new file mode 100644 index 0000000000..c13fbcbebf --- /dev/null +++ b/packages/server/src/handlers/v2/event.ts @@ -0,0 +1,61 @@ +import { EventV2 } from "@opencode-ai/core/event" +import { Location } from "@opencode-ai/core/location" +import { Effect, Stream } from "effect" +import { HttpServerResponse } from "effect/unstable/http" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import * as Sse from "effect/unstable/encoding/Sse" +import { V2Api } from "../../api" + +function eventData(data: unknown): Sse.Event { + return { + _tag: "Event", + event: "message", + id: undefined, + data: JSON.stringify(data), + } +} + +export const eventHandlers = HttpApiBuilder.group(V2Api, "v2.event", (handlers) => + Effect.gen(function* () { + const events = yield* EventV2.Service + return handlers.handleRaw("events", () => + Effect.gen(function* () { + const location = yield* Location.Service + const connected = { + id: EventV2.ID.create(), + type: "server.connected", + location: new Location.Info({ + directory: location.directory, + workspaceID: location.workspaceID, + project: location.project, + }), + data: {}, + } + return HttpServerResponse.stream( + Stream.make(connected).pipe( + Stream.concat( + events.all().pipe( + Stream.filter( + (event) => + event.location?.directory === location.directory && + event.location.workspaceID === location.workspaceID, + ), + ), + ), + Stream.map(eventData), + Stream.pipeThroughChannel(Sse.encode()), + Stream.encodeText, + ), + { + contentType: "text/event-stream", + headers: { + "Cache-Control": "no-cache, no-transform", + "X-Accel-Buffering": "no", + "X-Content-Type-Options": "nosniff", + }, + }, + ) + }), + ) + }), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/fs.ts b/packages/server/src/handlers/v2/fs.ts similarity index 76% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2/fs.ts rename to packages/server/src/handlers/v2/fs.ts index b407b21fd8..87c2dd8a18 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/fs.ts +++ b/packages/server/src/handlers/v2/fs.ts @@ -1,10 +1,10 @@ import { FileSystem } from "@opencode-ai/core/filesystem" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../../api" +import { V2Api } from "../../api" import { response } from "../../groups/v2/location" -export const fileSystemHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.fs", (handlers) => +export const fileSystemHandlers = HttpApiBuilder.group(V2Api, "v2.fs", (handlers) => Effect.gen(function* () { return handlers .handle("read", (ctx) => response(FileSystem.Service.use((fs) => fs.read(ctx.query)))) diff --git a/packages/server/src/handlers/v2/health.ts b/packages/server/src/handlers/v2/health.ts new file mode 100644 index 0000000000..5d66e5f250 --- /dev/null +++ b/packages/server/src/handlers/v2/health.ts @@ -0,0 +1,7 @@ +import { Effect } from "effect" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { V2Api } from "../../api" + +export const healthHandlers = HttpApiBuilder.group(V2Api, "v2.health", (handlers) => + handlers.handle("health", () => Effect.succeed({ healthy: true as const })), +) diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts b/packages/server/src/handlers/v2/message.ts similarity index 95% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts rename to packages/server/src/handlers/v2/message.ts index e0d9228170..3638bca116 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/message.ts +++ b/packages/server/src/handlers/v2/message.ts @@ -3,7 +3,7 @@ import { SessionV2 } from "@opencode-ai/core/session" import { Effect, Schema } from "effect" import * as DateTime from "effect/DateTime" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../../api" +import { V2Api } from "../../api" import { InvalidCursorError, SessionNotFoundError, UnknownError } from "../../errors" import { make } from "../../groups/v2/response" @@ -29,7 +29,7 @@ const cursor = { }, } -export const messageHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.message", (handlers) => +export const messageHandlers = HttpApiBuilder.group(V2Api, "v2.message", (handlers) => Effect.gen(function* () { const session = yield* SessionV2.Service diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/model.ts b/packages/server/src/handlers/v2/model.ts similarity index 85% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2/model.ts rename to packages/server/src/handlers/v2/model.ts index 7df713d331..8e78705524 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/model.ts +++ b/packages/server/src/handlers/v2/model.ts @@ -2,7 +2,7 @@ import { Catalog } from "@opencode-ai/core/catalog" import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../../api" +import { V2Api } from "../../api" import { ServiceUnavailableError } from "../../errors" import { response } from "../../groups/v2/location" @@ -11,7 +11,7 @@ const catalogUnavailable = new ServiceUnavailableError({ service: "catalog", }) -export const modelHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.model", (handlers) => +export const modelHandlers = HttpApiBuilder.group(V2Api, "v2.model", (handlers) => Effect.gen(function* () { return handlers.handle( "models", diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/permission.ts b/packages/server/src/handlers/v2/permission.ts similarity index 90% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2/permission.ts rename to packages/server/src/handlers/v2/permission.ts index d241bcea7f..9cd3df7dae 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/permission.ts +++ b/packages/server/src/handlers/v2/permission.ts @@ -7,7 +7,7 @@ import { SessionTable } from "@opencode-ai/core/session/sql" import { eq } from "drizzle-orm" import { Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../../api" +import { V2Api } from "../../api" import { PermissionNotFoundError, SessionNotFoundError } from "../../errors" import { response } from "../../groups/v2/location" import { make } from "../../groups/v2/response" @@ -16,7 +16,7 @@ function missingRequest(id: PermissionV2.ID) { return new PermissionNotFoundError({ requestID: id, message: `Permission request not found: ${id}` }) } -export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.permission", (handlers) => +export const permissionHandlers = HttpApiBuilder.group(V2Api, "v2.permission", (handlers) => Effect.gen(function* () { return handlers.handle( "permissionRequests", @@ -27,7 +27,7 @@ export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.perm }), ) -export const sessionPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session.permission", (handlers) => +export const sessionPermissionHandlers = HttpApiBuilder.group(V2Api, "v2.session.permission", (handlers) => Effect.gen(function* () { const { db } = yield* Database.Service const locations = yield* LocationServiceMap @@ -86,7 +86,7 @@ export const sessionPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, " }), ) -export const savedPermissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.permission.saved", (handlers) => +export const savedPermissionHandlers = HttpApiBuilder.group(V2Api, "v2.permission.saved", (handlers) => Effect.gen(function* () { const saved = yield* PermissionSaved.Service return handlers diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/provider.ts b/packages/server/src/handlers/v2/provider.ts similarity index 91% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2/provider.ts rename to packages/server/src/handlers/v2/provider.ts index 37c9429517..d0b71f3b91 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/provider.ts +++ b/packages/server/src/handlers/v2/provider.ts @@ -2,7 +2,7 @@ import { Catalog } from "@opencode-ai/core/catalog" import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { Effect } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../../api" +import { V2Api } from "../../api" import { ProviderNotFoundError, ServiceUnavailableError } from "../../errors" import { response } from "../../groups/v2/location" @@ -11,7 +11,7 @@ const catalogUnavailable = new ServiceUnavailableError({ service: "catalog", }) -export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.provider", (handlers) => +export const providerHandlers = HttpApiBuilder.group(V2Api, "v2.provider", (handlers) => Effect.gen(function* () { return handlers .handle( diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts b/packages/server/src/handlers/v2/session.ts similarity index 97% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts rename to packages/server/src/handlers/v2/session.ts index 77d7265131..cb64f798fa 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/session.ts +++ b/packages/server/src/handlers/v2/session.ts @@ -1,14 +1,14 @@ import { SessionV2 } from "@opencode-ai/core/session" import { DateTime, Effect } from "effect" import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../../api" +import { V2Api } from "../../api" import { SessionsCursor } from "../../groups/v2/session" import { InvalidCursorError, ServiceUnavailableError, SessionNotFoundError, UnknownError } from "../../errors" import { make } from "../../groups/v2/response" const DefaultSessionsLimit = 50 -export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.session", (handlers) => +export const sessionHandlers = HttpApiBuilder.group(V2Api, "v2.session", (handlers) => Effect.gen(function* () { const session = yield* SessionV2.Service diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/skill.ts b/packages/server/src/handlers/v2/skill.ts similarity index 64% rename from packages/opencode/src/server/routes/instance/httpapi/handlers/v2/skill.ts rename to packages/server/src/handlers/v2/skill.ts index e10ae66ab2..a4e98cfda1 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2/skill.ts +++ b/packages/server/src/handlers/v2/skill.ts @@ -1,8 +1,8 @@ import { SkillV2 } from "@opencode-ai/core/skill" import { HttpApiBuilder } from "effect/unstable/httpapi" -import { InstanceHttpApi } from "../../api" +import { V2Api } from "../../api" import { response } from "../../groups/v2/location" -export const skillHandlers = HttpApiBuilder.group(InstanceHttpApi, "v2.skill", (handlers) => +export const skillHandlers = HttpApiBuilder.group(V2Api, "v2.skill", (handlers) => handlers.handle("skills", () => response(SkillV2.Service.use((skill) => skill.list()))), ) diff --git a/packages/server/src/middleware/authorization.ts b/packages/server/src/middleware/authorization.ts new file mode 100644 index 0000000000..0411c60bfb --- /dev/null +++ b/packages/server/src/middleware/authorization.ts @@ -0,0 +1,60 @@ +import { ServerAuth } from "../auth" +import { UnauthorizedError } from "../errors" +import { Effect, Encoding, Layer, Redacted } from "effect" +import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http" +import { HttpApiMiddleware } from "effect/unstable/httpapi" + +const AUTH_TOKEN_QUERY = "auth_token" +const WWW_AUTHENTICATE = 'Basic realm="Secure Area"' + +export class V2Authorization extends HttpApiMiddleware.Service()( + "@opencode/ExperimentalHttpApiV2Authorization", + { + error: UnauthorizedError, + }, +) {} + +function emptyCredential() { + return { username: "", password: Redacted.make("") } +} + +function decodeCredential(input: string) { + return Effect.fromResult(Encoding.decodeBase64String(input)).pipe( + Effect.match({ + onFailure: emptyCredential, + onSuccess: (header) => { + const separator = header.indexOf(":") + if (separator === -1) return emptyCredential() + return { username: header.slice(0, separator), password: Redacted.make(header.slice(separator + 1)) } + }, + }), + ) +} + +function credentialFromRequest(request: HttpServerRequest.HttpServerRequest) { + const url = new URL(request.url, "http://localhost") + const token = url.searchParams.get(AUTH_TOKEN_QUERY) + if (token) return decodeCredential(token) + const match = /^Basic\s+(.+)$/i.exec(request.headers.authorization ?? "") + if (match) return decodeCredential(match[1]) + return Effect.succeed(emptyCredential()) +} + +export const v2AuthorizationLayer = Layer.effect( + V2Authorization, + Effect.gen(function* () { + const config = yield* ServerAuth.Config + if (!ServerAuth.required(config)) return V2Authorization.of((effect) => effect) + return V2Authorization.of((effect) => + Effect.gen(function* () { + const request = yield* HttpServerRequest.HttpServerRequest + const credential = yield* credentialFromRequest(request) + if (ServerAuth.authorized(credential, config)) return yield* effect + yield* HttpEffect.appendPreResponseHandler((_request, response) => + Effect.succeed(HttpServerResponse.setHeader(response, "www-authenticate", WWW_AUTHENTICATE)), + ) + return yield* new UnauthorizedError({ message: "Authentication required" }) + }), + ) + }), +) diff --git a/packages/server/src/middleware/schema-error.ts b/packages/server/src/middleware/schema-error.ts new file mode 100644 index 0000000000..e4b21dd3a4 --- /dev/null +++ b/packages/server/src/middleware/schema-error.ts @@ -0,0 +1,23 @@ +import * as Log from "@opencode-ai/core/util/log" +import { Effect } from "effect" +import { HttpApiMiddleware } from "effect/unstable/httpapi" +import { InvalidRequestError } from "../errors" + +const log = Log.create({ service: "server" }) +const REASON_LIMIT = 1024 + +function truncateReason(reason: string) { + if (reason.length <= REASON_LIMIT) return reason + return reason.slice(0, REASON_LIMIT) + `... (${reason.length - REASON_LIMIT} more chars)` +} + +export class SchemaErrorMiddleware extends HttpApiMiddleware.Service()( + "@opencode/HttpApiSchemaError", + { error: InvalidRequestError }, +) {} + +export const schemaErrorLayer = HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrorMiddleware, (error) => { + const reason = truncateReason(error.cause.message) + log.warn("schema rejection", { kind: error.kind, reason }) + return Effect.fail(new InvalidRequestError({ message: reason, kind: error.kind })) +}) diff --git a/packages/server/src/routes.ts b/packages/server/src/routes.ts new file mode 100644 index 0000000000..52fbe6ed4b --- /dev/null +++ b/packages/server/src/routes.ts @@ -0,0 +1,36 @@ +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { PermissionSaved } from "@opencode-ai/core/permission/saved" +import { SessionV2 } from "@opencode-ai/core/session" +import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { Layer, Option } from "effect" +import { V2Api } from "./api" +import { ServerAuth } from "./auth" +import { v2Handlers } from "./handlers" +import { v2AuthorizationLayer } from "./middleware/authorization" +import { schemaErrorLayer } from "./middleware/schema-error" + +export function createRoutes(password?: string) { + return HttpApiBuilder.layer(V2Api).pipe( + Layer.provide(v2Handlers), + Layer.provide(v2AuthorizationLayer), + Layer.provide(schemaErrorLayer), + Layer.provide( + password + ? ServerAuth.Config.layer({ username: "opencode", password: Option.some(password) }) + : ServerAuth.Config.defaultLayer, + ), + Layer.provide(LocationServiceMap.layer), + Layer.provide(PermissionSaved.layer), + Layer.provide(SessionV2.defaultLayer), + Layer.provide(Database.defaultLayer), + Layer.provide(EventV2.defaultLayer), + Layer.provide(FetchHttpClient.layer), + ) +} + +export const routes = createRoutes() + +export const webHandler = () => HttpRouter.toWebHandler(routes.pipe(Layer.provide(HttpServer.layerServices)), { disableLogger: true }) diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000000..fe5c4d217b --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@tsconfig/bun/tsconfig.json", + "compilerOptions": { + "noUncheckedIndexedAccess": false + } +}