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
|
|
@ -13,7 +13,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@effect/platform-node": "catalog:",
|
||||
"@opencode-ai/codemode": "workspace:*",
|
||||
"@opencode-ai/core": "workspace:*",
|
||||
"@opencode-ai/protocol": "workspace:*",
|
||||
"@opencode-ai/simulation": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
export * as ServerCodeMode from "./code-mode"
|
||||
|
||||
import { NodeHttpClient } from "@effect/platform-node"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { OpenAPI, Tool } from "@opencode-ai/codemode"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClient, HttpClientRequest } from "effect/unstable/http"
|
||||
import { OpenApi } from "effect/unstable/httpapi"
|
||||
import type { Server } from "node:http"
|
||||
import { Api } from "./api"
|
||||
import { ServerAuth } from "./auth"
|
||||
|
||||
export function replacement(server: Server, password: string): LayerNode.Replacement {
|
||||
return ToolRegistry.codeModeReplacement(makeTools(client(server), password))
|
||||
}
|
||||
|
||||
export function makeTools(client: Layer.Layer<HttpClient.HttpClient>, password: string): ToolRegistry.CodeModeTools {
|
||||
return {
|
||||
opencode: bindTools(
|
||||
OpenAPI.fromSpec({
|
||||
spec: { ...OpenApi.fromApi(Api) },
|
||||
baseUrl: "http://opencode.local",
|
||||
headers: ServerAuth.headers({ username: "opencode", password }),
|
||||
}).tools,
|
||||
client,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function client(server: Server) {
|
||||
return Layer.effect(
|
||||
HttpClient.HttpClient,
|
||||
Effect.gen(function* () {
|
||||
const client = yield* HttpClient.HttpClient
|
||||
return HttpClient.mapRequest(client, (request) => {
|
||||
const address = server.address()
|
||||
if (!address || typeof address === "string") throw new Error("OpenCode server is not listening")
|
||||
const local =
|
||||
address.address === "0.0.0.0" ? "127.0.0.1" : address.address === "::" ? "::1" : address.address
|
||||
const host = local.includes(":") && !local.startsWith("[") ? `[${local}]` : local
|
||||
const url = new URL(request.url)
|
||||
return HttpClientRequest.setUrl(
|
||||
request,
|
||||
new URL(`${url.pathname}${url.search}${url.hash}`, `http://${host}:${address.port}`),
|
||||
)
|
||||
})
|
||||
}),
|
||||
).pipe(Layer.provide(NodeHttpClient.layerNodeHttp))
|
||||
}
|
||||
|
||||
function bindTools(tools: OpenAPI.Tools, client: Layer.Layer<HttpClient.HttpClient>): ToolRegistry.CodeModeTools {
|
||||
return Object.fromEntries(
|
||||
Object.entries(tools).map(([name, value]) => [
|
||||
name,
|
||||
Tool.isDefinition<HttpClient.HttpClient>(value)
|
||||
? Tool.make({
|
||||
description: value.description,
|
||||
input: value.input,
|
||||
output: value.output,
|
||||
run: (input) => value.run(input).pipe(Effect.provide(client)),
|
||||
})
|
||||
: bindTools(value, client),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
|
@ -10,15 +10,15 @@ import { HealthGroup } from "@opencode-ai/protocol/groups/health"
|
|||
import { Context, Effect, Layer, Option } from "effect"
|
||||
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
|
||||
import { HttpApi, HttpApiClient } from "effect/unstable/httpapi"
|
||||
import { createServer } from "node:http"
|
||||
import { createServer, type Server } from "node:http"
|
||||
import { ServerAuth } from "./auth"
|
||||
import { ServerCodeMode } from "./code-mode"
|
||||
import { createRoutes } from "./routes"
|
||||
|
||||
export type Options = {
|
||||
readonly hostname: string
|
||||
readonly port: Option.Option<number>
|
||||
readonly password: string
|
||||
readonly replacements?: (server: Server) => LayerNode.Replacements
|
||||
}
|
||||
|
||||
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) {
|
||||
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> =>
|
||||
bind(options.hostname, port, options.password).pipe(
|
||||
bind(options, port).pipe(
|
||||
Effect.catch((error) => (port === 65_535 ? Effect.fail(error) : next(port + 1))),
|
||||
)
|
||||
return next(4096)
|
||||
}
|
||||
|
||||
function bind(hostname: string, port: number, password: string) {
|
||||
function bind(options: Options, port: number) {
|
||||
const server = createServer()
|
||||
return Layer.build(
|
||||
HttpRouter.serve(createRoutes(password, [ServerCodeMode.replacement(server, password)]), {
|
||||
HttpRouter.serve(createRoutes(options.password, options.replacements?.(server)), {
|
||||
disableListenLog: true,
|
||||
}).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]))),
|
||||
),
|
||||
).pipe(
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { CodeMode } from "@opencode-ai/codemode"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpClient, HttpClientResponse } from "effect/unstable/http"
|
||||
import { ServerCodeMode } from "../src/code-mode"
|
||||
|
||||
test("exposes the authenticated server API through CodeMode", async () => {
|
||||
const requests: Array<{ readonly url: string; readonly authorization?: string }> = []
|
||||
const client = Layer.succeed(
|
||||
HttpClient.HttpClient,
|
||||
HttpClient.make((request) => {
|
||||
requests.push({ url: request.url, authorization: request.headers.authorization })
|
||||
return Effect.succeed(
|
||||
HttpClientResponse.fromWeb(
|
||||
request,
|
||||
Response.json(
|
||||
{ healthy: true, version: "test", pid: 1 },
|
||||
{ headers: { "content-type": "application/json" } },
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
const result = await CodeMode.make({ tools: ServerCodeMode.makeTools(client, "secret") })
|
||||
.execute("return await tools.opencode.v2.health.get({})")
|
||||
.pipe(Effect.runPromise)
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
value: { healthy: true, version: "test", pid: 1 },
|
||||
toolCalls: [{ name: "opencode.v2.health.get" }],
|
||||
})
|
||||
expect(requests).toEqual([
|
||||
{
|
||||
url: "http://opencode.local/api/health",
|
||||
authorization: `Basic ${Buffer.from("opencode:secret").toString("base64")}`,
|
||||
},
|
||||
])
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue