refactor(cli): own Code Mode host wiring
This commit is contained in:
parent
42b63d6660
commit
b09ebbea3c
7 changed files with 19 additions and 17 deletions
2
bun.lock
2
bun.lock
|
|
@ -101,6 +101,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
|
"@opencode-ai/codemode": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
|
|
@ -813,7 +814,6 @@
|
||||||
"version": "1.17.14",
|
"version": "1.17.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/codemode": "workspace:*",
|
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/simulation": "workspace:*",
|
"@opencode-ai/simulation": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/client": "workspace:*",
|
"@opencode-ai/client": "workspace:*",
|
||||||
|
"@opencode-ai/codemode": "workspace:*",
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/plugin": "workspace:*",
|
"@opencode-ai/plugin": "workspace:*",
|
||||||
"@opencode-ai/schema": "workspace:*",
|
"@opencode-ai/schema": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
export * as ServerCodeMode from "./code-mode"
|
export * as CodeModeHost from "./code-mode"
|
||||||
|
|
||||||
import { NodeHttpClient } from "@effect/platform-node"
|
import { NodeHttpClient } from "@effect/platform-node"
|
||||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||||
import { OpenAPI, Tool } from "@opencode-ai/codemode"
|
import { OpenAPI, Tool } from "@opencode-ai/codemode"
|
||||||
|
import { Api } from "@opencode-ai/server/api"
|
||||||
|
import { ServerAuth } from "@opencode-ai/server/auth"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||||
import { OpenApi } from "effect/unstable/httpapi"
|
import { OpenApi } from "effect/unstable/httpapi"
|
||||||
import type { Server } from "node:http"
|
import type { Server } from "node:http"
|
||||||
import { Api } from "./api"
|
|
||||||
import { ServerAuth } from "./auth"
|
|
||||||
|
|
||||||
export function replacement(server: Server, password: string): LayerNode.Replacement {
|
export function replacements(server: Server, password: string): LayerNode.Replacements {
|
||||||
return ToolRegistry.codeModeReplacement(makeTools(client(server), password))
|
return [ToolRegistry.codeModeReplacement(makeTools(client(server), password))]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeTools(client: Layer.Layer<HttpClient.HttpClient>, password: string): ToolRegistry.CodeModeTools {
|
export function makeTools(client: Layer.Layer<HttpClient.HttpClient>, password: string): ToolRegistry.CodeModeTools {
|
||||||
|
|
@ -13,6 +13,7 @@ import { randomBytes, randomUUID } from "node:crypto"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
import { Effect, FileSystem, Logger, Option, Redacted, Schedule, Schema } from "effect"
|
||||||
import { HttpServer } from "effect/unstable/http"
|
import { HttpServer } from "effect/unstable/http"
|
||||||
|
import { CodeModeHost } from "./code-mode"
|
||||||
import { Env } from "./env"
|
import { Env } from "./env"
|
||||||
import { ServiceConfig } from "./services/service-config"
|
import { ServiceConfig } from "./services/service-config"
|
||||||
import { Updater } from "./services/updater"
|
import { Updater } from "./services/updater"
|
||||||
|
|
@ -63,6 +64,7 @@ const processEffect = Effect.fnUntraced(function* (options: Options) {
|
||||||
hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
|
hostname: options.hostname ?? config.hostname ?? "127.0.0.1",
|
||||||
port: Option.fromNullishOr(options.port ?? config.port),
|
port: Option.fromNullishOr(options.port ?? config.port),
|
||||||
password,
|
password,
|
||||||
|
replacements: (server) => CodeModeHost.replacements(server, password),
|
||||||
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
}).pipe(Effect.provide(Logger.layer([], { mergeWithExisting: false })))
|
||||||
if (options.mode === "service") yield* register(address, password)
|
if (options.mode === "service") yield* register(address, password)
|
||||||
const url = HttpServer.formatAddress(address)
|
const url = HttpServer.formatAddress(address)
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import { expect, test } from "bun:test"
|
||||||
import { CodeMode } from "@opencode-ai/codemode"
|
import { CodeMode } from "@opencode-ai/codemode"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||||
import { ServerCodeMode } from "../src/code-mode"
|
import { CodeModeHost } from "../src/code-mode"
|
||||||
|
|
||||||
test("exposes the authenticated server API through CodeMode", async () => {
|
test("exposes the authenticated OpenCode API through CodeMode", async () => {
|
||||||
const requests: Array<{ readonly url: string; readonly authorization?: string }> = []
|
const requests: Array<{ readonly url: string; readonly authorization?: string }> = []
|
||||||
const client = Layer.succeed(
|
const client = Layer.succeed(
|
||||||
HttpClient.HttpClient,
|
HttpClient.HttpClient,
|
||||||
|
|
@ -21,7 +21,7 @@ test("exposes the authenticated server API through CodeMode", async () => {
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const result = await CodeMode.make({ tools: ServerCodeMode.makeTools(client, "secret") })
|
const result = await CodeMode.make({ tools: CodeModeHost.makeTools(client, "secret") })
|
||||||
.execute("return await tools.opencode.v2.health.get({})")
|
.execute("return await tools.opencode.v2.health.get({})")
|
||||||
.pipe(Effect.runPromise)
|
.pipe(Effect.runPromise)
|
||||||
|
|
||||||
|
|
@ -13,7 +13,6 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@effect/platform-node": "catalog:",
|
"@effect/platform-node": "catalog:",
|
||||||
"@opencode-ai/codemode": "workspace:*",
|
|
||||||
"@opencode-ai/core": "workspace:*",
|
"@opencode-ai/core": "workspace:*",
|
||||||
"@opencode-ai/protocol": "workspace:*",
|
"@opencode-ai/protocol": "workspace:*",
|
||||||
"@opencode-ai/simulation": "workspace:*",
|
"@opencode-ai/simulation": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,15 @@ import { HealthGroup } from "@opencode-ai/protocol/groups/health"
|
||||||
import { Context, Effect, Layer, Option } from "effect"
|
import { Context, Effect, Layer, Option } from "effect"
|
||||||
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||||
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
|
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
|
||||||
import { createServer } from "node:http"
|
import { createServer, type Server } from "node:http"
|
||||||
import { ServerAuth } from "./auth"
|
import { ServerAuth } from "./auth"
|
||||||
import { ServerCodeMode } from "./code-mode"
|
|
||||||
import { createRoutes } from "./routes"
|
import { createRoutes } from "./routes"
|
||||||
|
|
||||||
export type Options = {
|
export type Options = {
|
||||||
readonly hostname: string
|
readonly hostname: string
|
||||||
readonly port: Option.Option<number>
|
readonly port: Option.Option<number>
|
||||||
readonly password: string
|
readonly password: string
|
||||||
|
readonly replacements?: (server: Server) => LayerNode.Replacements
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReadinessApi = HttpApi.make("readiness").add(HealthGroup)
|
const ReadinessApi = HttpApi.make("readiness").add(HealthGroup)
|
||||||
|
|
@ -43,21 +43,21 @@ export const start = Effect.fn("ServerProcess.start")(function* (options: Option
|
||||||
})
|
})
|
||||||
|
|
||||||
function listen(options: Options) {
|
function listen(options: Options) {
|
||||||
if (Option.isSome(options.port)) return bind(options.hostname, options.port.value, options.password)
|
if (Option.isSome(options.port)) return bind(options, options.port.value)
|
||||||
const next = (port: number): ReturnType<typeof bind> =>
|
const next = (port: number): ReturnType<typeof bind> =>
|
||||||
bind(options.hostname, port, options.password).pipe(
|
bind(options, port).pipe(
|
||||||
Effect.catch((error) => (port === 65_535 ? Effect.fail(error) : next(port + 1))),
|
Effect.catch((error) => (port === 65_535 ? Effect.fail(error) : next(port + 1))),
|
||||||
)
|
)
|
||||||
return next(4096)
|
return next(4096)
|
||||||
}
|
}
|
||||||
|
|
||||||
function bind(hostname: string, port: number, password: string) {
|
function bind(options: Options, port: number) {
|
||||||
const server = createServer()
|
const server = createServer()
|
||||||
return Layer.build(
|
return Layer.build(
|
||||||
HttpRouter.serve(createRoutes(password, [ServerCodeMode.replacement(server, password)]), {
|
HttpRouter.serve(createRoutes(options.password, options.replacements?.(server)), {
|
||||||
disableListenLog: true,
|
disableListenLog: true,
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: hostname })),
|
Layer.provideMerge(NodeHttpServer.layer(() => server, { port, host: options.hostname })),
|
||||||
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
|
Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node, Project.node]))),
|
||||||
),
|
),
|
||||||
).pipe(
|
).pipe(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue