SaylorTwift HF Staff commited on
Commit
89a2873
·
verified ·
1 Parent(s): 4bbfe8b

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. packages/cli/bin/lildax.cjs +130 -0
  2. packages/cli/script/build.ts +116 -0
  3. packages/cli/script/generate.ts +7 -0
  4. packages/cli/script/publish.ts +53 -0
  5. packages/cli/src/commands/commands.ts +52 -0
  6. packages/cli/src/commands/handlers/api.test.ts +35 -0
  7. packages/cli/src/commands/handlers/api.ts +85 -0
  8. packages/cli/src/commands/handlers/debug/agents.ts +21 -0
  9. packages/cli/src/commands/handlers/default.ts +13 -0
  10. packages/cli/src/commands/handlers/migrate.ts +5 -0
  11. packages/cli/src/commands/handlers/serve.ts +46 -0
  12. packages/cli/src/commands/handlers/service/password.ts +16 -0
  13. packages/cli/src/commands/handlers/service/restart.ts +14 -0
  14. packages/cli/src/commands/handlers/service/start.ts +12 -0
  15. packages/cli/src/commands/handlers/service/status.ts +13 -0
  16. packages/cli/src/commands/handlers/service/stop.ts +11 -0
  17. packages/cli/src/framework/runtime.ts +79 -0
  18. packages/cli/src/framework/spec.ts +42 -0
  19. packages/cli/src/index.ts +32 -0
  20. packages/cli/src/services/daemon.ts +192 -0
  21. packages/cli/src/tui.ts +37 -0
  22. packages/client/script/build.ts +30 -0
  23. packages/client/src/contract.ts +53 -0
  24. packages/client/src/effect.ts +25 -0
  25. packages/client/src/generated-effect/.httpapi-codegen.json +5 -0
  26. packages/client/src/generated-effect/client-error.ts +5 -0
  27. packages/client/src/generated-effect/client.ts +706 -0
  28. packages/client/src/generated-effect/index.ts +2 -0
  29. packages/client/src/generated/.httpapi-codegen.json +6 -0
  30. packages/client/src/generated/client-error.ts +11 -0
  31. packages/client/src/generated/client.ts +1029 -0
  32. packages/client/src/generated/index.ts +3 -0
  33. packages/client/src/generated/types.ts +0 -0
  34. packages/client/src/index.ts +2 -0
  35. packages/client/test/contract-identity.test.ts +58 -0
  36. packages/client/test/effect.test.ts +246 -0
  37. packages/client/test/import-boundaries.test.ts +68 -0
  38. packages/client/test/promise.test.ts +255 -0
  39. packages/codemode/src/codemode.ts +159 -0
  40. packages/codemode/src/index.ts +4 -0
  41. packages/codemode/src/interpreter/model.ts +201 -0
  42. packages/codemode/src/interpreter/runtime.ts +0 -0
  43. packages/codemode/src/openapi/TODO.md +19 -0
  44. packages/codemode/src/openapi/index.ts +130 -0
  45. packages/codemode/src/openapi/runtime.ts +326 -0
  46. packages/codemode/src/openapi/spec.ts +511 -0
  47. packages/codemode/src/openapi/types.ts +112 -0
  48. packages/codemode/src/stdlib/collections.ts +51 -0
  49. packages/codemode/src/stdlib/console.ts +4 -0
  50. packages/codemode/src/stdlib/date.ts +94 -0
packages/cli/bin/lildax.cjs ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require("child_process")
4
+ const fs = require("fs")
5
+ const path = require("path")
6
+ const os = require("os")
7
+
8
+ const forwardedSignals = ["SIGINT", "SIGTERM", "SIGHUP"]
9
+
10
+ function run(target) {
11
+ const child = childProcess.spawn(target, process.argv.slice(2), { stdio: "inherit" })
12
+ child.on("error", (error) => {
13
+ console.error(error.message)
14
+ process.exit(1)
15
+ })
16
+ const forwarders = {}
17
+ for (const signal of forwardedSignals) {
18
+ forwarders[signal] = () => {
19
+ try {
20
+ child.kill(signal)
21
+ } catch {}
22
+ }
23
+ process.on(signal, forwarders[signal])
24
+ }
25
+ child.on("exit", (code, signal) => {
26
+ for (const forwardedSignal of forwardedSignals) process.removeListener(forwardedSignal, forwarders[forwardedSignal])
27
+ if (signal) return process.kill(process.pid, signal)
28
+ process.exit(typeof code === "number" ? code : 0)
29
+ })
30
+ }
31
+
32
+ const envPath = process.env.OPENCODE_BIN_PATH
33
+ const scriptDir = path.dirname(fs.realpathSync(__filename))
34
+ const cached = path.join(scriptDir, ".lildax")
35
+ const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[os.platform()] || os.platform()
36
+ const arch = { x64: "x64", arm64: "arm64", arm: "arm" }[os.arch()] || os.arch()
37
+ const base = "@opencode-ai/cli-" + platform + "-" + arch
38
+ const binary = platform === "windows" ? "lildax.exe" : "lildax"
39
+
40
+ function supportsAvx2() {
41
+ if (arch !== "x64") return false
42
+ if (platform === "linux") {
43
+ try {
44
+ return /(^|\s)avx2(\s|$)/i.test(fs.readFileSync("/proc/cpuinfo", "utf8"))
45
+ } catch {
46
+ return false
47
+ }
48
+ }
49
+ if (platform === "darwin") {
50
+ try {
51
+ const result = childProcess.spawnSync("sysctl", ["-n", "hw.optional.avx2_0"], { encoding: "utf8", timeout: 1500 })
52
+ return result.status === 0 && (result.stdout || "").trim() === "1"
53
+ } catch {
54
+ return false
55
+ }
56
+ }
57
+ if (platform === "windows") {
58
+ const command =
59
+ '(Add-Type -MemberDefinition "[DllImport(""kernel32.dll"")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)'
60
+ for (const executable of ["powershell.exe", "pwsh.exe", "pwsh", "powershell"]) {
61
+ try {
62
+ const result = childProcess.spawnSync(executable, ["-NoProfile", "-NonInteractive", "-Command", command], {
63
+ encoding: "utf8",
64
+ timeout: 3000,
65
+ windowsHide: true,
66
+ })
67
+ if (result.status !== 0) continue
68
+ const output = (result.stdout || "").trim().toLowerCase()
69
+ if (output === "true" || output === "1") return true
70
+ if (output === "false" || output === "0") return false
71
+ } catch {
72
+ continue
73
+ }
74
+ }
75
+ }
76
+ return false
77
+ }
78
+
79
+ const names = (() => {
80
+ const baseline = arch === "x64" && !supportsAvx2()
81
+ if (platform === "linux") {
82
+ const musl = (() => {
83
+ try {
84
+ if (fs.existsSync("/etc/alpine-release")) return true
85
+ const result = childProcess.spawnSync("ldd", ["--version"], { encoding: "utf8" })
86
+ return ((result.stdout || "") + (result.stderr || "")).toLowerCase().includes("musl")
87
+ } catch {
88
+ return false
89
+ }
90
+ })()
91
+ if (musl)
92
+ return arch === "x64"
93
+ ? baseline
94
+ ? [`${base}-baseline-musl`, `${base}-musl`, `${base}-baseline`, base]
95
+ : [`${base}-musl`, `${base}-baseline-musl`, base, `${base}-baseline`]
96
+ : [`${base}-musl`, base]
97
+ return arch === "x64"
98
+ ? baseline
99
+ ? [`${base}-baseline`, base, `${base}-baseline-musl`, `${base}-musl`]
100
+ : [base, `${base}-baseline`, `${base}-musl`, `${base}-baseline-musl`]
101
+ : [base, `${base}-musl`]
102
+ }
103
+ return arch === "x64" ? (baseline ? [`${base}-baseline`, base] : [base, `${base}-baseline`]) : [base]
104
+ })()
105
+
106
+ function findBinary(startDir) {
107
+ let current = startDir
108
+ for (;;) {
109
+ const modules = path.join(current, "node_modules")
110
+ if (fs.existsSync(modules))
111
+ for (const name of names) {
112
+ const candidate = path.join(modules, name, "bin", binary)
113
+ if (fs.existsSync(candidate)) return candidate
114
+ }
115
+ const parent = path.dirname(current)
116
+ if (parent === current) return
117
+ current = parent
118
+ }
119
+ }
120
+
121
+ const resolved = envPath || (fs.existsSync(cached) ? cached : findBinary(scriptDir))
122
+ if (!resolved) {
123
+ console.error(
124
+ "It seems that your package manager failed to install the right lildax CLI package. Try manually installing " +
125
+ names.map((name) => `"${name}"`).join(" or ") +
126
+ " package",
127
+ )
128
+ process.exit(1)
129
+ }
130
+ run(resolved)
packages/cli/script/build.ts ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bun
2
+
3
+ import { $ } from "bun"
4
+ import { rm } from "fs/promises"
5
+ import path from "path"
6
+ import { Script } from "@opencode-ai/script"
7
+ import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
8
+ import pkg from "../package.json"
9
+ import { modelsData } from "./generate"
10
+
11
+ const dir = path.resolve(import.meta.dirname, "..")
12
+ const binary = "lildax"
13
+ process.chdir(dir)
14
+
15
+ await rm("dist", { recursive: true, force: true })
16
+
17
+ const singleFlag = process.argv.includes("--single")
18
+ const baselineFlag = process.argv.includes("--baseline")
19
+ const skipInstall = process.argv.includes("--skip-install")
20
+ const sourcemapsFlag = process.argv.includes("--sourcemaps")
21
+ const plugin = createSolidTransformPlugin()
22
+
23
+ const allTargets: {
24
+ os: string
25
+ arch: "arm64" | "x64"
26
+ abi?: "musl"
27
+ avx2?: false
28
+ }[] = [
29
+ { os: "linux", arch: "arm64" },
30
+ { os: "linux", arch: "x64" },
31
+ { os: "linux", arch: "x64", avx2: false },
32
+ { os: "linux", arch: "arm64", abi: "musl" },
33
+ { os: "linux", arch: "x64", abi: "musl" },
34
+ { os: "linux", arch: "x64", abi: "musl", avx2: false },
35
+ { os: "darwin", arch: "arm64" },
36
+ { os: "darwin", arch: "x64" },
37
+ { os: "darwin", arch: "x64", avx2: false },
38
+ { os: "win32", arch: "arm64" },
39
+ { os: "win32", arch: "x64" },
40
+ { os: "win32", arch: "x64", avx2: false },
41
+ ]
42
+
43
+ const targets = singleFlag
44
+ ? allTargets.filter((item) => {
45
+ if (item.os !== process.platform || item.arch !== process.arch) return false
46
+ if (item.avx2 === false) return baselineFlag
47
+ return item.abi === undefined
48
+ })
49
+ : allTargets
50
+
51
+ if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
52
+
53
+ for (const item of targets) {
54
+ const target = [
55
+ binary,
56
+ item.os === "win32" ? "windows" : item.os,
57
+ item.arch,
58
+ item.avx2 === false ? "baseline" : undefined,
59
+ item.abi,
60
+ ]
61
+ .filter(Boolean)
62
+ .join("-")
63
+ const name = target.replace(binary, "cli")
64
+ console.log(`building ${name}`)
65
+ const result = await Bun.build({
66
+ entrypoints: ["./src/index.ts"],
67
+ tsconfig: "./tsconfig.json",
68
+ plugins: [plugin],
69
+ external: ["node-gyp"],
70
+ format: "esm",
71
+ minify: true,
72
+ sourcemap: sourcemapsFlag ? "linked" : "none",
73
+ splitting: true,
74
+ compile: {
75
+ autoloadBunfig: false,
76
+ autoloadDotenv: false,
77
+ autoloadTsconfig: true,
78
+ autoloadPackageJson: true,
79
+ target: target.replace(binary, "bun") as Bun.Build.CompileTarget,
80
+ outfile: `./dist/${name}/bin/${binary}`,
81
+ execArgv: [`--user-agent=${binary}/${Script.version}`, "--use-system-ca", "--"],
82
+ windows: {},
83
+ },
84
+ define: {
85
+ OPENCODE_VERSION: `'${Script.version}'`,
86
+ OPENCODE_CLI_NAME: `'${binary}'`,
87
+ OPENCODE_MODELS_DEV: modelsData,
88
+ OPENCODE_CHANNEL: `'${Script.channel}'`,
89
+ OPENCODE_LIBC: item.os === "linux" ? `'${item.abi ?? "glibc"}'` : "undefined",
90
+ // FFF_LIBC selects the fff native lib variant: "musl" or "gnu".
91
+ FFF_LIBC: item.os === "linux" ? `'${item.abi ?? "gnu"}'` : "undefined",
92
+ ...(item.os === "linux" ? { "process.env.OPENTUI_LIBC": JSON.stringify(item.abi ?? "glibc") } : {}),
93
+ },
94
+ })
95
+
96
+ if (!result.success) {
97
+ for (const log of result.logs) console.error(log)
98
+ process.exit(1)
99
+ }
100
+
101
+ await Bun.write(
102
+ `./dist/${name}/package.json`,
103
+ JSON.stringify(
104
+ {
105
+ name: `@opencode-ai/${name}`,
106
+ version: Script.version,
107
+ license: "MIT",
108
+ repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
109
+ os: [item.os],
110
+ cpu: [item.arch],
111
+ },
112
+ null,
113
+ 2,
114
+ ),
115
+ )
116
+ }
packages/cli/script/generate.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const modelsUrl = process.env.OPENCODE_MODELS_URL || "https://models.opencode.ai"
2
+
3
+ export const modelsData = process.env.MODELS_DEV_API_JSON
4
+ ? await Bun.file(process.env.MODELS_DEV_API_JSON).text()
5
+ : await fetch(`${modelsUrl}/api.json`).then((response) => response.text())
6
+
7
+ console.log("Loaded models.dev snapshot")
packages/cli/script/publish.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bun
2
+ import { $ } from "bun"
3
+ import pkg from "../package.json"
4
+ import { Script } from "@opencode-ai/script"
5
+ import { fileURLToPath } from "url"
6
+
7
+ const dir = fileURLToPath(new URL("..", import.meta.url))
8
+ process.chdir(dir)
9
+
10
+ async function published(name: string, version: string) {
11
+ return (await $`npm view ${name}@${version} version`.nothrow()).exitCode === 0
12
+ }
13
+
14
+ async function publish(dir: string, name: string, version: string) {
15
+ if (process.platform !== "win32") await $`chmod -R 755 .`.cwd(dir)
16
+ if (await published(name, version)) return console.log(`already published ${name}@${version}`)
17
+ await $`bun pm pack`.cwd(dir)
18
+ await $`npm publish *.tgz --access public --tag ${Script.channel}`.cwd(dir)
19
+ }
20
+
21
+ const binaries: Record<string, string> = {}
22
+ for (const filepath of new Bun.Glob("*/package.json").scanSync({ cwd: "./dist" })) {
23
+ const item = await Bun.file(`./dist/${filepath}`).json()
24
+ binaries[item.name] = item.version
25
+ }
26
+ console.log("binaries", binaries)
27
+ const version = Object.values(binaries)[0]
28
+
29
+ await $`mkdir -p ./dist/${pkg.name}/bin`
30
+ await $`cp ./bin/lildax.cjs ./dist/${pkg.name}/bin/lildax`
31
+ await Bun.file(`./dist/${pkg.name}/package.json`).write(
32
+ JSON.stringify(
33
+ {
34
+ name: pkg.name,
35
+ bin: { lildax: "./bin/lildax" },
36
+ version,
37
+ license: pkg.license,
38
+ repository: { type: "git", url: "git+https://github.com/anomalyco/opencode.git" },
39
+ os: ["darwin", "linux", "win32"],
40
+ cpu: ["arm64", "x64"],
41
+ optionalDependencies: binaries,
42
+ },
43
+ null,
44
+ 2,
45
+ ),
46
+ )
47
+
48
+ await Promise.all(
49
+ Object.entries(binaries).map(([name, version]) =>
50
+ publish(`./dist/${name.replace("@opencode-ai/", "")}`, name, version),
51
+ ),
52
+ )
53
+ await publish(`./dist/${pkg.name}`, pkg.name, version)
packages/cli/src/commands/commands.ts ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Argument, Flag } from "effect/unstable/cli"
2
+ import { Spec } from "../framework/spec"
3
+
4
+ declare const OPENCODE_CLI_NAME: string | undefined
5
+
6
+ export const Commands = Spec.make(typeof OPENCODE_CLI_NAME === "string" ? OPENCODE_CLI_NAME : "opencode", {
7
+ description: "OpenCode 2.0 preview command line interface",
8
+ commands: [
9
+ Spec.make("api", {
10
+ description: "Make a request to the running server",
11
+ params: {
12
+ request: Argument.string("operation | method path").pipe(
13
+ Argument.withDescription("OpenAPI operation ID, or an HTTP method followed by a path"),
14
+ Argument.variadic({ min: 1, max: 2 }),
15
+ ),
16
+ data: Flag.string("data").pipe(Flag.withAlias("d"), Flag.withDescription("Request body"), Flag.optional),
17
+ header: Flag.string("header").pipe(
18
+ Flag.withAlias("H"),
19
+ Flag.withDescription("Request header in name:value form"),
20
+ Flag.atMost(100),
21
+ ),
22
+ param: Flag.keyValuePair("param").pipe(Flag.withDescription("OpenAPI path or query parameter"), Flag.optional),
23
+ },
24
+ }),
25
+ Spec.make("debug", {
26
+ description: "Debugging and troubleshooting tools",
27
+ commands: [Spec.make("agents", { description: "List all agents" })],
28
+ }),
29
+ Spec.make("migrate", { description: "Migrate v1 data to v2" }),
30
+ Spec.make("service", {
31
+ description: "Manage the background server",
32
+ commands: [
33
+ Spec.make("start", { description: "Start the background server" }),
34
+ Spec.make("restart", { description: "Restart the background server" }),
35
+ Spec.make("status", { description: "Show background server status" }),
36
+ Spec.make("stop", { description: "Stop the background server" }),
37
+ Spec.make("password", {
38
+ description: "Get or set the server password",
39
+ params: { value: Argument.string("value").pipe(Argument.optional) },
40
+ }),
41
+ ],
42
+ }),
43
+ Spec.make("serve", {
44
+ description: "Start the v2 API server",
45
+ params: {
46
+ hostname: Flag.string("hostname").pipe(Flag.withDefault("127.0.0.1")),
47
+ port: Flag.integer("port").pipe(Flag.optional),
48
+ register: Flag.boolean("register").pipe(Flag.withDefault(false)),
49
+ },
50
+ }),
51
+ ],
52
+ })
packages/cli/src/commands/handlers/api.test.ts ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, test } from "bun:test"
2
+ import { rawRequest, resolveOperation } from "./api"
3
+
4
+ describe("api request resolution", () => {
5
+ test("resolves an operation ID with path and query parameters", () => {
6
+ expect(
7
+ resolveOperation(
8
+ {
9
+ paths: {
10
+ "/api/session/{sessionID}": {
11
+ get: { operationId: "v2.session.get" },
12
+ },
13
+ },
14
+ },
15
+ "v2.session.get",
16
+ { sessionID: "ses/a", workspace: "work" },
17
+ ),
18
+ ).toEqual({ method: "GET", path: "/api/session/ses%2Fa?workspace=work" })
19
+ })
20
+
21
+ test("rejects a missing path parameter", () => {
22
+ expect(() =>
23
+ resolveOperation(
24
+ { paths: { "/api/session/{sessionID}": { get: { operationId: "v2.session.get" } } } },
25
+ "v2.session.get",
26
+ {},
27
+ ),
28
+ ).toThrow("Missing path parameter: sessionID")
29
+ })
30
+
31
+ test("resolves curl-like method and path input", () => {
32
+ expect(rawRequest(["post", "/api/foo"])).toEqual({ method: "POST", path: "/api/foo" })
33
+ expect(rawRequest(["v2.session.list"])).toBeUndefined()
34
+ })
35
+ })
packages/cli/src/commands/handlers/api.ts ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { EOL } from "node:os"
2
+ import { Effect, Option } from "effect"
3
+ import { Commands } from "../commands"
4
+ import { Runtime } from "../../framework/runtime"
5
+ import { Daemon } from "../../services/daemon"
6
+
7
+ const methods = new Set(["delete", "get", "head", "options", "patch", "post", "put"])
8
+
9
+ type Operation = {
10
+ operationId?: string
11
+ }
12
+
13
+ type OpenApi = {
14
+ paths?: Record<string, Record<string, Operation>>
15
+ }
16
+
17
+ export default Runtime.handler(
18
+ Commands.commands.api,
19
+ Effect.fn("cli.api")(function* (input) {
20
+ const daemon = yield* Daemon.Service
21
+ const transport = yield* daemon.transport()
22
+ const params = Option.getOrElse(input.param, () => ({}))
23
+ const request = yield* resolveRequest(transport, input.request, params)
24
+ const headers = new Headers(transport.headers)
25
+ for (const header of input.header) {
26
+ const index = header.indexOf(":")
27
+ if (index < 1) return yield* Effect.fail(new Error(`Invalid header, expected name:value: ${header}`))
28
+ headers.set(header.slice(0, index).trim(), header.slice(index + 1).trim())
29
+ }
30
+ const body = Option.getOrUndefined(input.data)
31
+ if (body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")
32
+
33
+ const response = yield* Effect.tryPromise(() =>
34
+ fetch(new URL(request.path, transport.url), {
35
+ method: request.method,
36
+ headers,
37
+ body,
38
+ }),
39
+ )
40
+ const output = yield* Effect.promise(() => response.text())
41
+ if (output) process.stdout.write(output + (output.endsWith(EOL) ? "" : EOL))
42
+ }),
43
+ )
44
+
45
+ export function resolveOperation(spec: OpenApi, operationID: string, params: Record<string, string>) {
46
+ for (const [path, operations] of Object.entries(spec.paths ?? {})) {
47
+ for (const [method, operation] of Object.entries(operations)) {
48
+ if (!methods.has(method) || operation.operationId !== operationID) continue
49
+ return { method: method.toUpperCase(), path: interpolate(path, params) }
50
+ }
51
+ }
52
+ throw new Error(`Operation not found: ${operationID}`)
53
+ }
54
+
55
+ export function rawRequest(input: readonly string[]) {
56
+ if (input.length !== 2 || !methods.has(input[0].toLowerCase()) || !input[1].startsWith("/")) return
57
+ return { method: input[0].toUpperCase(), path: input[1] }
58
+ }
59
+
60
+ function resolveRequest(
61
+ transport: { url: string; headers: RequestInit["headers"] },
62
+ input: readonly string[],
63
+ params: Record<string, string>,
64
+ ) {
65
+ const raw = rawRequest(input)
66
+ if (raw) return Effect.succeed(raw)
67
+ if (input.length !== 1) return Effect.fail(new Error("Expected an operation name or an HTTP method and path"))
68
+ return Effect.tryPromise(async () => {
69
+ const response = await fetch(new URL("/openapi.json", transport.url), { headers: transport.headers })
70
+ if (!response.ok) throw new Error(`Failed to load OpenAPI document: HTTP ${response.status}`)
71
+ return resolveOperation((await response.json()) as OpenApi, input[0], params)
72
+ })
73
+ }
74
+
75
+ function interpolate(path: string, params: Record<string, string>) {
76
+ const used = new Set<string>()
77
+ const pathname = path.replaceAll(/\{([^}]+)\}/g, (_, name: string) => {
78
+ const value = params[name]
79
+ if (value === undefined) throw new Error(`Missing path parameter: ${name}`)
80
+ used.add(name)
81
+ return encodeURIComponent(value)
82
+ })
83
+ const query = new URLSearchParams(Object.entries(params).filter(([name]) => !used.has(name))).toString()
84
+ return query ? `${pathname}?${query}` : pathname
85
+ }
packages/cli/src/commands/handlers/debug/agents.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { EOL } from "os"
2
+ import * as Effect from "effect/Effect"
3
+ import { Commands } from "../../commands"
4
+ import { Runtime } from "../../../framework/runtime"
5
+ import { Daemon } from "../../../services/daemon"
6
+
7
+ export default Runtime.handler(
8
+ Commands.commands.debug.commands.agents,
9
+ Effect.fn("cli.debug.agents")(function* () {
10
+ const daemon = yield* Daemon.Service
11
+ const client = yield* daemon.client()
12
+ const response = yield* Effect.promise(() => client.v2.agent.list({ location: { directory: process.cwd() } }))
13
+ process.stdout.write(
14
+ JSON.stringify(
15
+ response.data?.data.toSorted((a, b) => a.id.localeCompare(b.id)),
16
+ null,
17
+ 2,
18
+ ) + EOL,
19
+ )
20
+ }),
21
+ )
packages/cli/src/commands/handlers/default.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Commands } from "../commands"
2
+ import { Runtime } from "../../framework/runtime"
3
+ import { Effect } from "effect"
4
+ import { Daemon } from "../../services/daemon"
5
+
6
+ export default Runtime.handler(Commands, () =>
7
+ Effect.gen(function* () {
8
+ const daemon = yield* Daemon.Service
9
+ const transport = yield* daemon.transport()
10
+ const { runTui } = yield* Effect.promise(() => import("../../tui"))
11
+ yield* runTui(transport)
12
+ }),
13
+ )
packages/cli/src/commands/handlers/migrate.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import * as Effect from "effect/Effect"
2
+ import { Commands } from "../commands"
3
+ import { Runtime } from "../../framework/runtime"
4
+
5
+ export default Runtime.handler(Commands.commands.migrate, (_input) => Effect.log("No migrations to run."))
packages/cli/src/commands/handlers/serve.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NodeHttpServer } from "@effect/platform-node"
2
+ import { Credential } from "@opencode-ai/core/credential"
3
+ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
4
+ import { LayerNode } from "@opencode-ai/core/effect/layer-node"
5
+ import { PermissionSaved } from "@opencode-ai/core/permission/saved"
6
+ import { Context, Layer, Option } from "effect"
7
+ import * as Effect from "effect/Effect"
8
+ import { HttpRouter, HttpServer } from "effect/unstable/http"
9
+ import { createServer } from "node:http"
10
+ import { createRoutes } from "@opencode-ai/server/routes"
11
+ import { Commands } from "../commands"
12
+ import { Runtime } from "../../framework/runtime"
13
+ import { Daemon } from "../../services/daemon"
14
+
15
+ export default Runtime.handler(
16
+ Commands.commands.serve,
17
+ Effect.fn("cli.serve")(function* (input) {
18
+ return yield* Effect.scoped(
19
+ Effect.gen(function* () {
20
+ const daemon = yield* Daemon.Service
21
+ const address = yield* listen(input.hostname, input.port, yield* daemon.password())
22
+ if (input.register) yield* daemon.register(address)
23
+ console.log(`server listening on ${HttpServer.formatAddress(address)}`)
24
+ return yield* Effect.never
25
+ }),
26
+ )
27
+ }),
28
+ )
29
+
30
+ function listen(hostname: string, port: Option.Option<number>, password: string) {
31
+ if (Option.isSome(port)) return bind(hostname, port.value, password)
32
+ const next = (port: number): ReturnType<typeof bind> =>
33
+ bind(hostname, port, password).pipe(
34
+ Effect.catch((error) => (port === 65_535 ? Effect.fail(error) : next(port + 1))),
35
+ )
36
+ return next(4096)
37
+ }
38
+
39
+ function bind(hostname: string, port: number, password: string) {
40
+ return Layer.build(
41
+ HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
42
+ Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
43
+ Layer.provide(AppNodeBuilder.build(LayerNode.group([Credential.node, PermissionSaved.node]))),
44
+ ),
45
+ ).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
46
+ }
packages/cli/src/commands/handlers/service/password.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { EOL } from "os"
2
+ import { Option } from "effect"
3
+ import * as Effect from "effect/Effect"
4
+ import { Commands } from "../../commands"
5
+ import { Runtime } from "../../../framework/runtime"
6
+ import { Daemon } from "../../../services/daemon"
7
+
8
+ export default Runtime.handler(
9
+ Commands.commands.service.commands.password,
10
+ Effect.fn("cli.service.password")(function* (input) {
11
+ const daemon = yield* Daemon.Service
12
+ const value = Option.getOrUndefined(input.value)
13
+ if (value !== undefined) yield* daemon.stop()
14
+ process.stdout.write((yield* daemon.password(value)) + EOL)
15
+ }),
16
+ )
packages/cli/src/commands/handlers/service/restart.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { EOL } from "os"
2
+ import * as Effect from "effect/Effect"
3
+ import { Commands } from "../../commands"
4
+ import { Runtime } from "../../../framework/runtime"
5
+ import { Daemon } from "../../../services/daemon"
6
+
7
+ export default Runtime.handler(
8
+ Commands.commands.service.commands.restart,
9
+ Effect.fn("cli.service.restart")(function* () {
10
+ const daemon = yield* Daemon.Service
11
+ yield* daemon.stop()
12
+ process.stdout.write((yield* daemon.start()) + EOL)
13
+ }),
14
+ )
packages/cli/src/commands/handlers/service/start.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { EOL } from "os"
2
+ import * as Effect from "effect/Effect"
3
+ import { Commands } from "../../commands"
4
+ import { Runtime } from "../../../framework/runtime"
5
+ import { Daemon } from "../../../services/daemon"
6
+
7
+ export default Runtime.handler(
8
+ Commands.commands.service.commands.start,
9
+ Effect.fn("cli.service.start")(function* () {
10
+ process.stdout.write((yield* (yield* Daemon.Service).start()) + EOL)
11
+ }),
12
+ )
packages/cli/src/commands/handlers/service/status.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { EOL } from "os"
2
+ import * as Effect from "effect/Effect"
3
+ import { Commands } from "../../commands"
4
+ import { Runtime } from "../../../framework/runtime"
5
+ import { Daemon } from "../../../services/daemon"
6
+
7
+ export default Runtime.handler(
8
+ Commands.commands.service.commands.status,
9
+ Effect.fn("cli.service.status")(function* () {
10
+ const url = yield* (yield* Daemon.Service).status()
11
+ process.stdout.write((url ? `running ${url}` : "stopped") + EOL)
12
+ }),
13
+ )
packages/cli/src/commands/handlers/service/stop.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Effect from "effect/Effect"
2
+ import { Commands } from "../../commands"
3
+ import { Runtime } from "../../../framework/runtime"
4
+ import { Daemon } from "../../../services/daemon"
5
+
6
+ export default Runtime.handler(
7
+ Commands.commands.service.commands.stop,
8
+ Effect.fn("cli.service.stop")(function* () {
9
+ yield* (yield* Daemon.Service).stop()
10
+ }),
11
+ )
packages/cli/src/framework/runtime.ts ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Effect from "effect/Effect"
2
+ import * as Command from "effect/unstable/cli/Command"
3
+ import { Spec } from "./spec"
4
+ import { Daemon } from "../services/daemon"
5
+
6
+ export type Input<Value> =
7
+ Value extends Spec.Node<infer _Name, infer Command, infer _Commands>
8
+ ? Input<Command>
9
+ : Value extends Command.Command<infer _Name, infer Input, infer _Context, infer _Error, infer _Requirements>
10
+ ? Input
11
+ : never
12
+
13
+ type RuntimeHandler = (input: unknown) => Effect.Effect<void, unknown, Daemon.Service>
14
+ type Loader<Node extends Spec.Any> = () => Promise<{
15
+ default: (input: Input<Node>) => Effect.Effect<void, any, Daemon.Service>
16
+ }>
17
+ type ProvidedCommand = Command.Command<string, unknown, unknown, unknown, Daemon.Service>
18
+
19
+ export type Handlers<Node extends Spec.Any> = keyof Node["commands"] extends never
20
+ ? Loader<Node>
21
+ : { readonly $?: Loader<Node> } & { readonly [Key in keyof Node["commands"]]: Handlers<Node["commands"][Key]> }
22
+
23
+ interface LazyHandler {
24
+ readonly spec: Command.Command.Any
25
+ readonly load: () => Promise<{ default: RuntimeHandler }>
26
+ }
27
+
28
+ type RuntimeHandlers =
29
+ | (() => Promise<{ default: RuntimeHandler }>)
30
+ | {
31
+ readonly $?: () => Promise<{ default: RuntimeHandler }>
32
+ readonly [key: string]: RuntimeHandlers | (() => Promise<{ default: RuntimeHandler }>) | undefined
33
+ }
34
+
35
+ export function handler<const Node extends Spec.Any, Error, Requirements>(
36
+ _node: Node,
37
+ run: (input: Input<Node>) => Effect.Effect<void, Error, Requirements>,
38
+ ) {
39
+ return run
40
+ }
41
+
42
+ export function handlers<const Root extends Spec.Any>(root: Root, handlers: Handlers<Root>) {
43
+ const result: LazyHandler[] = []
44
+
45
+ function add(node: Spec.Any, value: RuntimeHandlers) {
46
+ if (typeof value === "function") {
47
+ result.push({ spec: node.spec, load: value as () => Promise<{ default: RuntimeHandler }> })
48
+ return
49
+ }
50
+ if (value.$) result.push({ spec: node.spec, load: value.$ as () => Promise<{ default: RuntimeHandler }> })
51
+ for (const [name, child] of Object.entries(node.commands)) add(child, value[name] as RuntimeHandlers)
52
+ }
53
+
54
+ add(root, handlers as RuntimeHandlers)
55
+ return result
56
+ }
57
+
58
+ export function run(commands: Spec.Any, handlers: ReadonlyArray<LazyHandler>, options: { readonly version: string }) {
59
+ return Command.run(provide(commands, handlers), options) as Effect.Effect<void, unknown, Command.Environment>
60
+ }
61
+
62
+ function provide(node: Spec.Any, handlers: ReadonlyArray<LazyHandler>): ProvidedCommand {
63
+ const handler = handlers.find((handler) => handler.spec === node.spec)
64
+ const spec = handler
65
+ ? node.spec.pipe(
66
+ Command.withHandler((input) =>
67
+ Effect.gen(function* () {
68
+ yield* Effect.flatMap(Effect.promise(handler.load), (module) => module.default(input))
69
+ }),
70
+ ),
71
+ )
72
+ : node.spec
73
+ if (!Object.keys(node.commands).length) return spec as ProvidedCommand
74
+ return spec.pipe(
75
+ Command.withSubcommands(Object.values(node.commands).map((child) => provide(child, handlers))),
76
+ ) as ProvidedCommand
77
+ }
78
+
79
+ export * as Runtime from "./runtime"
packages/cli/src/framework/spec.ts ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as Command from "effect/unstable/cli/Command"
2
+
3
+ type Options<Config extends Command.Command.Config, Commands extends ReadonlyArray<Any>> = {
4
+ readonly description?: string
5
+ readonly params?: Config
6
+ readonly commands?: Commands
7
+ }
8
+
9
+ export interface Node<
10
+ Name extends string,
11
+ Spec extends Command.Command<Name, any, any, any, any>,
12
+ Commands extends Children,
13
+ > {
14
+ readonly name: Name
15
+ readonly spec: Spec
16
+ readonly commands: Commands
17
+ }
18
+
19
+ export type Any = Node<string, Command.Command<any, any, any, any, any>, Children>
20
+ export type Children = Readonly<Record<string, Any>>
21
+
22
+ export function make<
23
+ const Name extends string,
24
+ const Config extends Command.Command.Config = {},
25
+ const Commands extends ReadonlyArray<Any> = [],
26
+ >(name: Name, options: Options<Config, Commands> = {}) {
27
+ const command = Command.make(name, options.params ?? ({} as Config))
28
+ const spec = options.description ? command.pipe(Command.withDescription(options.description)) : command
29
+ return {
30
+ name,
31
+ spec,
32
+ commands: Object.fromEntries(
33
+ (options.commands ?? []).map((command) => [command.name, command]),
34
+ ) as ChildrenOf<Commands>,
35
+ }
36
+ }
37
+
38
+ type ChildrenOf<Commands extends ReadonlyArray<Any>> = {
39
+ readonly [Node in Commands[number] as Node["name"]]: Node
40
+ }
41
+
42
+ export * as Spec from "./spec"
packages/cli/src/index.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bun
2
+
3
+ import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
4
+ import * as NodeServices from "@effect/platform-node/NodeServices"
5
+ import * as Effect from "effect/Effect"
6
+ import { Commands } from "./commands/commands"
7
+ import { Runtime } from "./framework/runtime"
8
+ import { Daemon } from "./services/daemon"
9
+
10
+ const Handlers = Runtime.handlers(Commands, {
11
+ $: () => import("./commands/handlers/default"),
12
+ api: () => import("./commands/handlers/api"),
13
+ debug: {
14
+ agents: () => import("./commands/handlers/debug/agents"),
15
+ },
16
+ migrate: () => import("./commands/handlers/migrate"),
17
+ service: {
18
+ start: () => import("./commands/handlers/service/start"),
19
+ restart: () => import("./commands/handlers/service/restart"),
20
+ status: () => import("./commands/handlers/service/status"),
21
+ stop: () => import("./commands/handlers/service/stop"),
22
+ password: () => import("./commands/handlers/service/password"),
23
+ },
24
+ serve: () => import("./commands/handlers/serve"),
25
+ })
26
+
27
+ Runtime.run(Commands, Handlers, { version: "local" }).pipe(
28
+ Effect.provide(Daemon.layer),
29
+ Effect.provide(NodeServices.layer),
30
+ Effect.scoped,
31
+ NodeRuntime.runMain,
32
+ )
packages/cli/src/services/daemon.ts ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Global } from "@opencode-ai/core/global"
2
+ import { InstallationVersion } from "@opencode-ai/core/installation/version"
3
+ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
4
+ import { ServerAuth } from "@opencode-ai/server/auth"
5
+ import { Context, Effect, FileSystem, Layer, Option, Schedule, Schema, Scope } from "effect"
6
+ import { HttpServer } from "effect/unstable/http"
7
+ import { randomBytes, randomUUID } from "crypto"
8
+ import { spawn } from "node:child_process"
9
+ import path from "path"
10
+
11
+ export interface Interface {
12
+ readonly client: () => Effect.Effect<ReturnType<typeof createOpencodeClient>, unknown>
13
+ readonly transport: () => Effect.Effect<{ url: string; headers: RequestInit["headers"] }, unknown>
14
+ readonly start: () => Effect.Effect<string, Error>
15
+ readonly status: () => Effect.Effect<string | undefined>
16
+ readonly stop: () => Effect.Effect<void, unknown>
17
+ readonly password: (value?: string) => Effect.Effect<string, unknown>
18
+ readonly register: (address: HttpServer.Address) => Effect.Effect<void, unknown, Scope.Scope>
19
+ }
20
+
21
+ export class Service extends Context.Service<Service, Interface>()("@opencode/cli/Daemon") {}
22
+
23
+ const Registration = Schema.Struct({
24
+ id: Schema.optional(Schema.String),
25
+ version: Schema.optional(Schema.String),
26
+ url: Schema.String,
27
+ pid: Schema.Int.check(Schema.isGreaterThan(0)),
28
+ })
29
+ type Registration = typeof Registration.Type
30
+
31
+ function sameRegistration(left: Registration, right: Registration) {
32
+ return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid
33
+ }
34
+
35
+ export const layer = Layer.effect(
36
+ Service,
37
+ Effect.gen(function* () {
38
+ const fs = yield* FileSystem.FileSystem
39
+ const directory = Global.Path.state
40
+ const file = path.join(directory, "server.json")
41
+ const passwordFile = path.join(directory, "password")
42
+ const decodeRegistration = Schema.decodeUnknownEffect(Schema.fromJsonString(Registration))
43
+
44
+ const password = Effect.fn("cli.daemon.password")(function* (value?: string) {
45
+ const existing = yield* fs.readFileString(passwordFile).pipe(Effect.catch(() => Effect.succeed(undefined)))
46
+ if (value === undefined && existing) return existing
47
+
48
+ // Keep one private credential across server restarts so discovered clients
49
+ // can reconnect without exposing a password flag or environment variable.
50
+ const generated = value ?? randomBytes(32).toString("base64url")
51
+ const temp = passwordFile + ".tmp"
52
+ yield* fs.makeDirectory(directory, { recursive: true })
53
+ yield* fs.writeFileString(temp, generated, { mode: 0o600 })
54
+ yield* fs.rename(temp, passwordFile)
55
+ return generated
56
+ })
57
+
58
+ const registration = Effect.fnUntraced(function* () {
59
+ return yield* fs.readFileString(file).pipe(Effect.flatMap(decodeRegistration))
60
+ })
61
+
62
+ const createClient = Effect.fnUntraced(function* (url: string) {
63
+ return createOpencodeClient({ baseUrl: url, headers: ServerAuth.headers({ password: yield* password() }) })
64
+ })
65
+
66
+ const healthy = Effect.fnUntraced(function* () {
67
+ const info = yield* registration()
68
+ const client = yield* createClient(info.url)
69
+ const response = yield* Effect.tryPromise(() => client.v2.health.get({ signal: AbortSignal.timeout(2_000) }))
70
+ if (response.data?.healthy === true) return info
71
+ return yield* Effect.fail(new Error("Registered server is not healthy"))
72
+ })
73
+
74
+ const compatible = Effect.fnUntraced(function* () {
75
+ const info = yield* healthy()
76
+ if (info.version === InstallationVersion) return info
77
+ return yield* Effect.fail(new Error("Registered server version does not match the client"))
78
+ })
79
+
80
+ const signal = (pid: number, signal: NodeJS.Signals) =>
81
+ Effect.try({ try: () => process.kill(pid, signal), catch: (cause) => cause }).pipe(Effect.ignore)
82
+
83
+ const awaitStopped = Effect.fnUntraced(function* (pid: number) {
84
+ const running = yield* Effect.try({ try: () => process.kill(pid, 0), catch: () => false }).pipe(
85
+ Effect.orElseSucceed(() => false),
86
+ )
87
+ if (!running) return true
88
+ return yield* Effect.fail(new Error(`Server process ${pid} is still running`))
89
+ })
90
+
91
+ const stopProcess = Effect.fnUntraced(function* (info: Registration) {
92
+ const current = yield* healthy().pipe(Effect.option)
93
+ if (Option.isNone(current) || !sameRegistration(current.value, info)) return
94
+
95
+ yield* signal(info.pid, "SIGTERM")
96
+ const stopped = yield* awaitStopped(info.pid).pipe(
97
+ Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
98
+ Effect.option,
99
+ )
100
+ if (Option.isSome(stopped)) return
101
+
102
+ const latest = yield* healthy().pipe(Effect.option)
103
+ if (Option.isNone(latest) || !sameRegistration(latest.value, info)) return
104
+ yield* signal(info.pid, "SIGKILL")
105
+ yield* awaitStopped(info.pid).pipe(
106
+ Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
107
+ )
108
+ })
109
+
110
+ const start = Effect.fn("cli.daemon.start")(function* () {
111
+ const existing = yield* healthy().pipe(Effect.option)
112
+ const found = Option.getOrUndefined(existing)
113
+ const compiled = path.basename(process.execPath).replace(/\.exe$/, "") !== "bun"
114
+ if (found?.version === InstallationVersion && compiled) return found.url
115
+ if (found) yield* stopProcess(found).pipe(Effect.ignore)
116
+
117
+ const entrypoint = compiled ? undefined : process.argv[1]
118
+ if (!compiled && entrypoint === undefined)
119
+ return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint"))
120
+ yield* Effect.try({
121
+ try: () => {
122
+ spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], {
123
+ detached: true,
124
+ stdio: "ignore",
125
+ }).unref()
126
+ },
127
+ catch: (cause) => new Error("Failed to start server", { cause }),
128
+ })
129
+
130
+ return yield* compatible().pipe(
131
+ Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))),
132
+ Effect.map((info) => info.url),
133
+ Effect.mapError(() => new Error("Failed to start server")),
134
+ )
135
+ })
136
+
137
+ const transport = Effect.fn("cli.daemon.transport")(function* () {
138
+ return { url: yield* start(), headers: ServerAuth.headers({ password: yield* password() }) }
139
+ })
140
+
141
+ const client = Effect.fn("cli.daemon.client")(function* () {
142
+ const connection = yield* transport()
143
+ return createOpencodeClient({ baseUrl: connection.url, headers: connection.headers })
144
+ })
145
+
146
+ const status = Effect.fn("cli.daemon.status")(function* () {
147
+ const existing = yield* healthy().pipe(Effect.option)
148
+ const found = Option.getOrUndefined(existing)
149
+ if (found?.version === InstallationVersion) return found.url
150
+ if (found) return undefined
151
+ yield* fs.remove(file).pipe(Effect.ignore)
152
+ return undefined
153
+ })
154
+
155
+ const stop = Effect.fn("cli.daemon.stop")(function* () {
156
+ const existing = yield* healthy().pipe(Effect.option)
157
+ // A stale registration may point at a PID that has since been reused by
158
+ // another process. Only signal the PID after authenticating the server.
159
+ if (Option.isNone(existing)) return yield* fs.remove(file).pipe(Effect.ignore)
160
+ yield* stopProcess(existing.value)
161
+ yield* fs.remove(file).pipe(Effect.ignore)
162
+ })
163
+
164
+ const register = Effect.fn("cli.daemon.register")(function* (address: HttpServer.Address) {
165
+ const id = randomUUID()
166
+ const temp = file + "." + id + ".tmp"
167
+ yield* fs.makeDirectory(directory, { recursive: true })
168
+ yield* fs.writeFileString(
169
+ temp,
170
+ JSON.stringify({ id, version: InstallationVersion, url: HttpServer.formatAddress(address), pid: process.pid }),
171
+ { mode: 0o600 },
172
+ )
173
+ yield* fs.rename(temp, file)
174
+ yield* registration().pipe(
175
+ Effect.flatMap((info) => (info.id === id ? Effect.void : signal(process.pid, "SIGTERM"))),
176
+ Effect.catch(() => signal(process.pid, "SIGTERM")),
177
+ Effect.repeat(Schedule.spaced("10 seconds")),
178
+ Effect.forkScoped,
179
+ )
180
+ yield* Effect.addFinalizer(() =>
181
+ registration().pipe(
182
+ Effect.flatMap((info) => (info.id === id ? fs.remove(file) : Effect.void)),
183
+ Effect.ignore,
184
+ ),
185
+ )
186
+ })
187
+
188
+ return Service.of({ client, transport, start, status, stop, password, register })
189
+ }),
190
+ )
191
+
192
+ export * as Daemon from "./daemon"
packages/cli/src/tui.ts ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { run } from "@opencode-ai/tui"
2
+ import { TuiConfig } from "@opencode-ai/tui/config"
3
+ import { Effect } from "effect"
4
+ import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
5
+ import { Global } from "@opencode-ai/core/global"
6
+
7
+ export function runTui(transport: { url: string; headers: RequestInit["headers"] }) {
8
+ const config = TuiConfig.resolve({}, { terminalSuspend: false })
9
+ return run({
10
+ ...transport,
11
+ args: {},
12
+ config,
13
+ fetch: gracefulFetch,
14
+ pluginHost: {
15
+ async start() {},
16
+ async dispose() {},
17
+ },
18
+ }).pipe(Effect.provide(AppNodeBuilder.build(Global.node)))
19
+ }
20
+
21
+ const legacyDefaults: Record<string, unknown> = {
22
+ "/config/providers": { providers: [], default: {} },
23
+ "/provider": { all: [], default: {}, connected: [] },
24
+ "/agent": [],
25
+ "/config": {},
26
+ }
27
+
28
+ const gracefulFetch = Object.assign(
29
+ async (input: RequestInfo | URL, init?: RequestInit) => {
30
+ const response = await fetch(input, init)
31
+ if (response.status !== 404) return response
32
+ const fallback = legacyDefaults[new URL(input instanceof Request ? input.url : input).pathname]
33
+ if (fallback === undefined) return response
34
+ return Response.json(fallback)
35
+ },
36
+ { preconnect: fetch.preconnect },
37
+ )
packages/client/script/build.ts ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NodeFileSystem } from "@effect/platform-node"
2
+ import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
3
+ import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract"
4
+ import { Effect } from "effect"
5
+ import { fileURLToPath } from "url"
6
+
7
+ const contract = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
8
+
9
+ await Effect.runPromise(
10
+ Effect.all(
11
+ [
12
+ write(
13
+ emitPromise(contract, {
14
+ outputTypes: {
15
+ "events.subscribe": {
16
+ name: "OpenCodeEventEncoded",
17
+ import: 'import type { OpenCodeEventEncoded } from "@opencode-ai/protocol/groups/event"',
18
+ },
19
+ },
20
+ }),
21
+ fileURLToPath(new URL("../src/generated", import.meta.url)),
22
+ ),
23
+ write(
24
+ emitEffectImported(contract, { module: "../contract", api: "ClientApi" }),
25
+ fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
26
+ ),
27
+ ],
28
+ { concurrency: 2, discard: true },
29
+ ).pipe(Effect.provide(NodeFileSystem.layer)),
30
+ )
packages/client/src/contract.ts ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { makeDefaultApi } from "@opencode-ai/protocol/api"
2
+ import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
3
+ import { HttpApiMiddleware } from "effect/unstable/httpapi"
4
+
5
+ class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
6
+ "@opencode-ai/client/LocationMiddleware",
7
+ ) {}
8
+
9
+ class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocationMiddleware>()(
10
+ "@opencode-ai/client/SessionLocationMiddleware",
11
+ { error: [InvalidRequestError, SessionNotFoundError] },
12
+ ) {}
13
+
14
+ export const ClientApi = makeDefaultApi({
15
+ locationMiddleware: LocationMiddleware,
16
+ sessionLocationMiddleware: SessionLocationMiddleware,
17
+ })
18
+
19
+ export const groupNames = {
20
+ "server.health": "health",
21
+ "server.location": "location",
22
+ "server.agent": "agents",
23
+ "server.session": "sessions",
24
+ "server.message": "messages",
25
+ "server.model": "models",
26
+ "server.provider": "providers",
27
+ "server.integration": "integrations",
28
+ "server.credential": "credentials",
29
+ "server.permission": "permissions",
30
+ "server.fs": "files",
31
+ "server.command": "commands",
32
+ "server.skill": "skills",
33
+ "server.event": "events",
34
+ "server.pty": "ptys",
35
+ "server.question": "questions",
36
+ "server.reference": "references",
37
+ "server.projectCopy": "projectCopies",
38
+ } as const
39
+
40
+ export const endpointNames = {
41
+ "session.messages": "list",
42
+ "integration.connect.key": "connectKey",
43
+ "integration.connect.oauth": "connectOauth",
44
+ "integration.attempt.status": "attemptStatus",
45
+ "integration.attempt.complete": "attemptComplete",
46
+ "integration.attempt.cancel": "attemptCancel",
47
+ "permission.request.list": "listRequests",
48
+ "permission.saved.list": "listSaved",
49
+ "permission.saved.remove": "removeSaved",
50
+ "question.request.list": "listRequests",
51
+ } as const
52
+
53
+ export const omitEndpoints = new Set(["fs.read", "pty.connect", "pty.connectToken"])
packages/client/src/effect.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import
2
+ // Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
3
+ export * from "./generated-effect/index"
4
+ export { Agent } from "@opencode-ai/schema/agent"
5
+ export { Command } from "@opencode-ai/schema/command"
6
+ export { Credential } from "@opencode-ai/schema/credential"
7
+ export { FileSystem } from "@opencode-ai/schema/filesystem"
8
+ export { Integration } from "@opencode-ai/schema/integration"
9
+ export { Location } from "@opencode-ai/schema/location"
10
+ export { Model } from "@opencode-ai/schema/model"
11
+ export { Permission } from "@opencode-ai/schema/permission"
12
+ export { PermissionSaved } from "@opencode-ai/schema/permission-saved"
13
+ export { Project } from "@opencode-ai/schema/project"
14
+ export { ProjectCopy } from "@opencode-ai/schema/project-copy"
15
+ export { Provider } from "@opencode-ai/schema/provider"
16
+ export { Pty } from "@opencode-ai/schema/pty"
17
+ export { Question } from "@opencode-ai/schema/question"
18
+ export { Reference } from "@opencode-ai/schema/reference"
19
+ export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
20
+ export { Session } from "@opencode-ai/schema/session"
21
+ export { SessionInput } from "@opencode-ai/schema/session-input"
22
+ export { SessionMessage } from "@opencode-ai/schema/session-message"
23
+ export { Skill } from "@opencode-ai/schema/skill"
24
+ export { Prompt } from "@opencode-ai/schema/prompt"
25
+ export type { OpenCodeEvent } from "@opencode-ai/protocol/groups/event"
packages/client/src/generated-effect/.httpapi-codegen.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ [
2
+ "client-error.ts",
3
+ "client.ts",
4
+ "index.ts"
5
+ ]
packages/client/src/generated-effect/client-error.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import { Schema } from "effect"
2
+
3
+ export class ClientError extends Schema.TaggedErrorClass<ClientError>()("ClientError", {
4
+ cause: Schema.Defect(),
5
+ }) {}
packages/client/src/generated-effect/client.ts ADDED
@@ -0,0 +1,706 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Generated by @opencode-ai/httpapi-codegen. Do not edit.
2
+ import { Effect, Stream, Schema } from "effect"
3
+ import { Sse } from "effect/unstable/encoding"
4
+ import { HttpClientError } from "effect/unstable/http"
5
+ import { HttpApiClient } from "effect/unstable/httpapi"
6
+ import { ClientApi } from "../contract"
7
+ import { ClientError } from "./client-error"
8
+
9
+ type RawClient = HttpApiClient.ForApi<typeof ClientApi>
10
+
11
+ const mapClientError = <E>(error: E) =>
12
+ HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error)
13
+ ? new ClientError({ cause: error })
14
+ : error
15
+
16
+ const Endpoint0_0 = (raw: RawClient["server.health"]) => () =>
17
+ raw["health.get"]({}).pipe(Effect.mapError(mapClientError))
18
+
19
+ const adaptGroup0 = (raw: RawClient["server.health"]) => ({ get: Endpoint0_0(raw) })
20
+
21
+ type Endpoint1_0Request = Parameters<RawClient["server.location"]["location.get"]>[0]
22
+ type Endpoint1_0Input = { readonly location?: Endpoint1_0Request["query"]["location"] }
23
+ const Endpoint1_0 = (raw: RawClient["server.location"]) => (input?: Endpoint1_0Input) =>
24
+ raw["location.get"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
25
+
26
+ const adaptGroup1 = (raw: RawClient["server.location"]) => ({ get: Endpoint1_0(raw) })
27
+
28
+ type Endpoint2_0Request = Parameters<RawClient["server.agent"]["agent.list"]>[0]
29
+ type Endpoint2_0Input = { readonly location?: Endpoint2_0Request["query"]["location"] }
30
+ const Endpoint2_0 = (raw: RawClient["server.agent"]) => (input?: Endpoint2_0Input) =>
31
+ raw["agent.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
32
+
33
+ const adaptGroup2 = (raw: RawClient["server.agent"]) => ({ list: Endpoint2_0(raw) })
34
+
35
+ type Endpoint3_0Request = Parameters<RawClient["server.session"]["session.list"]>[0]
36
+ type Endpoint3_0Input = {
37
+ readonly workspace?: Endpoint3_0Request["query"]["workspace"]
38
+ readonly limit?: Endpoint3_0Request["query"]["limit"]
39
+ readonly order?: Endpoint3_0Request["query"]["order"]
40
+ readonly search?: Endpoint3_0Request["query"]["search"]
41
+ readonly directory?: Endpoint3_0Request["query"]["directory"]
42
+ readonly project?: Endpoint3_0Request["query"]["project"]
43
+ readonly subpath?: Endpoint3_0Request["query"]["subpath"]
44
+ readonly cursor?: Endpoint3_0Request["query"]["cursor"]
45
+ }
46
+ const Endpoint3_0 = (raw: RawClient["server.session"]) => (input?: Endpoint3_0Input) =>
47
+ raw["session.list"]({
48
+ query: {
49
+ workspace: input?.["workspace"],
50
+ limit: input?.["limit"],
51
+ order: input?.["order"],
52
+ search: input?.["search"],
53
+ directory: input?.["directory"],
54
+ project: input?.["project"],
55
+ subpath: input?.["subpath"],
56
+ cursor: input?.["cursor"],
57
+ },
58
+ }).pipe(Effect.mapError(mapClientError))
59
+
60
+ type Endpoint3_1Request = Parameters<RawClient["server.session"]["session.create"]>[0]
61
+ type Endpoint3_1Input = {
62
+ readonly id?: Endpoint3_1Request["payload"]["id"]
63
+ readonly agent?: Endpoint3_1Request["payload"]["agent"]
64
+ readonly model?: Endpoint3_1Request["payload"]["model"]
65
+ readonly location?: Endpoint3_1Request["payload"]["location"]
66
+ }
67
+ const Endpoint3_1 = (raw: RawClient["server.session"]) => (input?: Endpoint3_1Input) =>
68
+ raw["session.create"]({
69
+ payload: { id: input?.["id"], agent: input?.["agent"], model: input?.["model"], location: input?.["location"] },
70
+ }).pipe(
71
+ Effect.mapError(mapClientError),
72
+ Effect.map((value) => value.data),
73
+ )
74
+
75
+ const Endpoint3_2 = (raw: RawClient["server.session"]) => () =>
76
+ raw["session.active"]({}).pipe(
77
+ Effect.mapError(mapClientError),
78
+ Effect.map((value) => value.data),
79
+ )
80
+
81
+ type Endpoint3_3Request = Parameters<RawClient["server.session"]["session.get"]>[0]
82
+ type Endpoint3_3Input = { readonly sessionID: Endpoint3_3Request["params"]["sessionID"] }
83
+ const Endpoint3_3 = (raw: RawClient["server.session"]) => (input: Endpoint3_3Input) =>
84
+ raw["session.get"]({ params: { sessionID: input["sessionID"] } }).pipe(
85
+ Effect.mapError(mapClientError),
86
+ Effect.map((value) => value.data),
87
+ )
88
+
89
+ type Endpoint3_4Request = Parameters<RawClient["server.session"]["session.switchAgent"]>[0]
90
+ type Endpoint3_4Input = {
91
+ readonly sessionID: Endpoint3_4Request["params"]["sessionID"]
92
+ readonly agent: Endpoint3_4Request["payload"]["agent"]
93
+ }
94
+ const Endpoint3_4 = (raw: RawClient["server.session"]) => (input: Endpoint3_4Input) =>
95
+ raw["session.switchAgent"]({ params: { sessionID: input["sessionID"] }, payload: { agent: input["agent"] } }).pipe(
96
+ Effect.mapError(mapClientError),
97
+ )
98
+
99
+ type Endpoint3_5Request = Parameters<RawClient["server.session"]["session.switchModel"]>[0]
100
+ type Endpoint3_5Input = {
101
+ readonly sessionID: Endpoint3_5Request["params"]["sessionID"]
102
+ readonly model: Endpoint3_5Request["payload"]["model"]
103
+ }
104
+ const Endpoint3_5 = (raw: RawClient["server.session"]) => (input: Endpoint3_5Input) =>
105
+ raw["session.switchModel"]({ params: { sessionID: input["sessionID"] }, payload: { model: input["model"] } }).pipe(
106
+ Effect.mapError(mapClientError),
107
+ )
108
+
109
+ type Endpoint3_6Request = Parameters<RawClient["server.session"]["session.prompt"]>[0]
110
+ type Endpoint3_6Input = {
111
+ readonly sessionID: Endpoint3_6Request["params"]["sessionID"]
112
+ readonly id?: Endpoint3_6Request["payload"]["id"]
113
+ readonly prompt: Endpoint3_6Request["payload"]["prompt"]
114
+ readonly delivery?: Endpoint3_6Request["payload"]["delivery"]
115
+ readonly resume?: Endpoint3_6Request["payload"]["resume"]
116
+ }
117
+ const Endpoint3_6 = (raw: RawClient["server.session"]) => (input: Endpoint3_6Input) =>
118
+ raw["session.prompt"]({
119
+ params: { sessionID: input["sessionID"] },
120
+ payload: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
121
+ }).pipe(
122
+ Effect.mapError(mapClientError),
123
+ Effect.map((value) => value.data),
124
+ )
125
+
126
+ type Endpoint3_7Request = Parameters<RawClient["server.session"]["session.compact"]>[0]
127
+ type Endpoint3_7Input = { readonly sessionID: Endpoint3_7Request["params"]["sessionID"] }
128
+ const Endpoint3_7 = (raw: RawClient["server.session"]) => (input: Endpoint3_7Input) =>
129
+ raw["session.compact"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
130
+
131
+ type Endpoint3_8Request = Parameters<RawClient["server.session"]["session.wait"]>[0]
132
+ type Endpoint3_8Input = { readonly sessionID: Endpoint3_8Request["params"]["sessionID"] }
133
+ const Endpoint3_8 = (raw: RawClient["server.session"]) => (input: Endpoint3_8Input) =>
134
+ raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
135
+
136
+ type Endpoint3_9Request = Parameters<RawClient["server.session"]["session.revert.stage"]>[0]
137
+ type Endpoint3_9Input = {
138
+ readonly sessionID: Endpoint3_9Request["params"]["sessionID"]
139
+ readonly messageID: Endpoint3_9Request["payload"]["messageID"]
140
+ readonly files?: Endpoint3_9Request["payload"]["files"]
141
+ }
142
+ const Endpoint3_9 = (raw: RawClient["server.session"]) => (input: Endpoint3_9Input) =>
143
+ raw["session.revert.stage"]({
144
+ params: { sessionID: input["sessionID"] },
145
+ payload: { messageID: input["messageID"], files: input["files"] },
146
+ }).pipe(
147
+ Effect.mapError(mapClientError),
148
+ Effect.map((value) => value.data),
149
+ )
150
+
151
+ type Endpoint3_10Request = Parameters<RawClient["server.session"]["session.revert.clear"]>[0]
152
+ type Endpoint3_10Input = { readonly sessionID: Endpoint3_10Request["params"]["sessionID"] }
153
+ const Endpoint3_10 = (raw: RawClient["server.session"]) => (input: Endpoint3_10Input) =>
154
+ raw["session.revert.clear"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
155
+
156
+ type Endpoint3_11Request = Parameters<RawClient["server.session"]["session.revert.commit"]>[0]
157
+ type Endpoint3_11Input = { readonly sessionID: Endpoint3_11Request["params"]["sessionID"] }
158
+ const Endpoint3_11 = (raw: RawClient["server.session"]) => (input: Endpoint3_11Input) =>
159
+ raw["session.revert.commit"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
160
+
161
+ type Endpoint3_12Request = Parameters<RawClient["server.session"]["session.context"]>[0]
162
+ type Endpoint3_12Input = { readonly sessionID: Endpoint3_12Request["params"]["sessionID"] }
163
+ const Endpoint3_12 = (raw: RawClient["server.session"]) => (input: Endpoint3_12Input) =>
164
+ raw["session.context"]({ params: { sessionID: input["sessionID"] } }).pipe(
165
+ Effect.mapError(mapClientError),
166
+ Effect.map((value) => value.data),
167
+ )
168
+
169
+ type Endpoint3_13Request = Parameters<RawClient["server.session"]["session.history"]>[0]
170
+ type Endpoint3_13Input = {
171
+ readonly sessionID: Endpoint3_13Request["params"]["sessionID"]
172
+ readonly limit?: Endpoint3_13Request["query"]["limit"]
173
+ readonly after?: Endpoint3_13Request["query"]["after"]
174
+ }
175
+ const Endpoint3_13 = (raw: RawClient["server.session"]) => (input: Endpoint3_13Input) =>
176
+ raw["session.history"]({
177
+ params: { sessionID: input["sessionID"] },
178
+ query: { limit: input["limit"], after: input["after"] },
179
+ }).pipe(Effect.mapError(mapClientError))
180
+
181
+ type Endpoint3_14Request = Parameters<RawClient["server.session"]["session.events"]>[0]
182
+ type Endpoint3_14Input = {
183
+ readonly sessionID: Endpoint3_14Request["params"]["sessionID"]
184
+ readonly after?: Endpoint3_14Request["query"]["after"]
185
+ }
186
+ const Endpoint3_14 = (raw: RawClient["server.session"]) => (input: Endpoint3_14Input) =>
187
+ Stream.unwrap(
188
+ raw["session.events"]({ params: { sessionID: input["sessionID"] }, query: { after: input["after"] } }).pipe(
189
+ Effect.mapError(mapClientError),
190
+ Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
191
+ ),
192
+ )
193
+
194
+ type Endpoint3_15Request = Parameters<RawClient["server.session"]["session.interrupt"]>[0]
195
+ type Endpoint3_15Input = { readonly sessionID: Endpoint3_15Request["params"]["sessionID"] }
196
+ const Endpoint3_15 = (raw: RawClient["server.session"]) => (input: Endpoint3_15Input) =>
197
+ raw["session.interrupt"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError))
198
+
199
+ type Endpoint3_16Request = Parameters<RawClient["server.session"]["session.message"]>[0]
200
+ type Endpoint3_16Input = {
201
+ readonly sessionID: Endpoint3_16Request["params"]["sessionID"]
202
+ readonly messageID: Endpoint3_16Request["params"]["messageID"]
203
+ }
204
+ const Endpoint3_16 = (raw: RawClient["server.session"]) => (input: Endpoint3_16Input) =>
205
+ raw["session.message"]({ params: { sessionID: input["sessionID"], messageID: input["messageID"] } }).pipe(
206
+ Effect.mapError(mapClientError),
207
+ Effect.map((value) => value.data),
208
+ )
209
+
210
+ const adaptGroup3 = (raw: RawClient["server.session"]) => ({
211
+ list: Endpoint3_0(raw),
212
+ create: Endpoint3_1(raw),
213
+ active: Endpoint3_2(raw),
214
+ get: Endpoint3_3(raw),
215
+ switchAgent: Endpoint3_4(raw),
216
+ switchModel: Endpoint3_5(raw),
217
+ prompt: Endpoint3_6(raw),
218
+ compact: Endpoint3_7(raw),
219
+ wait: Endpoint3_8(raw),
220
+ stage: Endpoint3_9(raw),
221
+ clear: Endpoint3_10(raw),
222
+ commit: Endpoint3_11(raw),
223
+ context: Endpoint3_12(raw),
224
+ history: Endpoint3_13(raw),
225
+ events: Endpoint3_14(raw),
226
+ interrupt: Endpoint3_15(raw),
227
+ message: Endpoint3_16(raw),
228
+ })
229
+
230
+ type Endpoint4_0Request = Parameters<RawClient["server.message"]["session.messages"]>[0]
231
+ type Endpoint4_0Input = {
232
+ readonly sessionID: Endpoint4_0Request["params"]["sessionID"]
233
+ readonly limit?: Endpoint4_0Request["query"]["limit"]
234
+ readonly order?: Endpoint4_0Request["query"]["order"]
235
+ readonly cursor?: Endpoint4_0Request["query"]["cursor"]
236
+ }
237
+ const Endpoint4_0 = (raw: RawClient["server.message"]) => (input: Endpoint4_0Input) =>
238
+ raw["session.messages"]({
239
+ params: { sessionID: input["sessionID"] },
240
+ query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] },
241
+ }).pipe(Effect.mapError(mapClientError))
242
+
243
+ const adaptGroup4 = (raw: RawClient["server.message"]) => ({ list: Endpoint4_0(raw) })
244
+
245
+ type Endpoint5_0Request = Parameters<RawClient["server.model"]["model.list"]>[0]
246
+ type Endpoint5_0Input = { readonly location?: Endpoint5_0Request["query"]["location"] }
247
+ const Endpoint5_0 = (raw: RawClient["server.model"]) => (input?: Endpoint5_0Input) =>
248
+ raw["model.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
249
+
250
+ const adaptGroup5 = (raw: RawClient["server.model"]) => ({ list: Endpoint5_0(raw) })
251
+
252
+ type Endpoint6_0Request = Parameters<RawClient["server.provider"]["provider.list"]>[0]
253
+ type Endpoint6_0Input = { readonly location?: Endpoint6_0Request["query"]["location"] }
254
+ const Endpoint6_0 = (raw: RawClient["server.provider"]) => (input?: Endpoint6_0Input) =>
255
+ raw["provider.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
256
+
257
+ type Endpoint6_1Request = Parameters<RawClient["server.provider"]["provider.get"]>[0]
258
+ type Endpoint6_1Input = {
259
+ readonly providerID: Endpoint6_1Request["params"]["providerID"]
260
+ readonly location?: Endpoint6_1Request["query"]["location"]
261
+ }
262
+ const Endpoint6_1 = (raw: RawClient["server.provider"]) => (input: Endpoint6_1Input) =>
263
+ raw["provider.get"]({ params: { providerID: input["providerID"] }, query: { location: input["location"] } }).pipe(
264
+ Effect.mapError(mapClientError),
265
+ )
266
+
267
+ const adaptGroup6 = (raw: RawClient["server.provider"]) => ({ list: Endpoint6_0(raw), get: Endpoint6_1(raw) })
268
+
269
+ type Endpoint7_0Request = Parameters<RawClient["server.integration"]["integration.list"]>[0]
270
+ type Endpoint7_0Input = { readonly location?: Endpoint7_0Request["query"]["location"] }
271
+ const Endpoint7_0 = (raw: RawClient["server.integration"]) => (input?: Endpoint7_0Input) =>
272
+ raw["integration.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
273
+
274
+ type Endpoint7_1Request = Parameters<RawClient["server.integration"]["integration.get"]>[0]
275
+ type Endpoint7_1Input = {
276
+ readonly integrationID: Endpoint7_1Request["params"]["integrationID"]
277
+ readonly location?: Endpoint7_1Request["query"]["location"]
278
+ }
279
+ const Endpoint7_1 = (raw: RawClient["server.integration"]) => (input: Endpoint7_1Input) =>
280
+ raw["integration.get"]({
281
+ params: { integrationID: input["integrationID"] },
282
+ query: { location: input["location"] },
283
+ }).pipe(Effect.mapError(mapClientError))
284
+
285
+ type Endpoint7_2Request = Parameters<RawClient["server.integration"]["integration.connect.key"]>[0]
286
+ type Endpoint7_2Input = {
287
+ readonly integrationID: Endpoint7_2Request["params"]["integrationID"]
288
+ readonly location?: Endpoint7_2Request["query"]["location"]
289
+ readonly key: Endpoint7_2Request["payload"]["key"]
290
+ readonly label?: Endpoint7_2Request["payload"]["label"]
291
+ }
292
+ const Endpoint7_2 = (raw: RawClient["server.integration"]) => (input: Endpoint7_2Input) =>
293
+ raw["integration.connect.key"]({
294
+ params: { integrationID: input["integrationID"] },
295
+ query: { location: input["location"] },
296
+ payload: { key: input["key"], label: input["label"] },
297
+ }).pipe(Effect.mapError(mapClientError))
298
+
299
+ type Endpoint7_3Request = Parameters<RawClient["server.integration"]["integration.connect.oauth"]>[0]
300
+ type Endpoint7_3Input = {
301
+ readonly integrationID: Endpoint7_3Request["params"]["integrationID"]
302
+ readonly location?: Endpoint7_3Request["query"]["location"]
303
+ readonly methodID: Endpoint7_3Request["payload"]["methodID"]
304
+ readonly inputs: Endpoint7_3Request["payload"]["inputs"]
305
+ readonly label?: Endpoint7_3Request["payload"]["label"]
306
+ }
307
+ const Endpoint7_3 = (raw: RawClient["server.integration"]) => (input: Endpoint7_3Input) =>
308
+ raw["integration.connect.oauth"]({
309
+ params: { integrationID: input["integrationID"] },
310
+ query: { location: input["location"] },
311
+ payload: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
312
+ }).pipe(Effect.mapError(mapClientError))
313
+
314
+ type Endpoint7_4Request = Parameters<RawClient["server.integration"]["integration.attempt.status"]>[0]
315
+ type Endpoint7_4Input = {
316
+ readonly attemptID: Endpoint7_4Request["params"]["attemptID"]
317
+ readonly location?: Endpoint7_4Request["query"]["location"]
318
+ }
319
+ const Endpoint7_4 = (raw: RawClient["server.integration"]) => (input: Endpoint7_4Input) =>
320
+ raw["integration.attempt.status"]({
321
+ params: { attemptID: input["attemptID"] },
322
+ query: { location: input["location"] },
323
+ }).pipe(Effect.mapError(mapClientError))
324
+
325
+ type Endpoint7_5Request = Parameters<RawClient["server.integration"]["integration.attempt.complete"]>[0]
326
+ type Endpoint7_5Input = {
327
+ readonly attemptID: Endpoint7_5Request["params"]["attemptID"]
328
+ readonly location?: Endpoint7_5Request["query"]["location"]
329
+ readonly code?: Endpoint7_5Request["payload"]["code"]
330
+ }
331
+ const Endpoint7_5 = (raw: RawClient["server.integration"]) => (input: Endpoint7_5Input) =>
332
+ raw["integration.attempt.complete"]({
333
+ params: { attemptID: input["attemptID"] },
334
+ query: { location: input["location"] },
335
+ payload: { code: input["code"] },
336
+ }).pipe(Effect.mapError(mapClientError))
337
+
338
+ type Endpoint7_6Request = Parameters<RawClient["server.integration"]["integration.attempt.cancel"]>[0]
339
+ type Endpoint7_6Input = {
340
+ readonly attemptID: Endpoint7_6Request["params"]["attemptID"]
341
+ readonly location?: Endpoint7_6Request["query"]["location"]
342
+ }
343
+ const Endpoint7_6 = (raw: RawClient["server.integration"]) => (input: Endpoint7_6Input) =>
344
+ raw["integration.attempt.cancel"]({
345
+ params: { attemptID: input["attemptID"] },
346
+ query: { location: input["location"] },
347
+ }).pipe(Effect.mapError(mapClientError))
348
+
349
+ const adaptGroup7 = (raw: RawClient["server.integration"]) => ({
350
+ list: Endpoint7_0(raw),
351
+ get: Endpoint7_1(raw),
352
+ connectKey: Endpoint7_2(raw),
353
+ connectOauth: Endpoint7_3(raw),
354
+ attemptStatus: Endpoint7_4(raw),
355
+ attemptComplete: Endpoint7_5(raw),
356
+ attemptCancel: Endpoint7_6(raw),
357
+ })
358
+
359
+ type Endpoint8_0Request = Parameters<RawClient["server.credential"]["credential.update"]>[0]
360
+ type Endpoint8_0Input = {
361
+ readonly credentialID: Endpoint8_0Request["params"]["credentialID"]
362
+ readonly location?: Endpoint8_0Request["query"]["location"]
363
+ readonly label: Endpoint8_0Request["payload"]["label"]
364
+ }
365
+ const Endpoint8_0 = (raw: RawClient["server.credential"]) => (input: Endpoint8_0Input) =>
366
+ raw["credential.update"]({
367
+ params: { credentialID: input["credentialID"] },
368
+ query: { location: input["location"] },
369
+ payload: { label: input["label"] },
370
+ }).pipe(Effect.mapError(mapClientError))
371
+
372
+ type Endpoint8_1Request = Parameters<RawClient["server.credential"]["credential.remove"]>[0]
373
+ type Endpoint8_1Input = {
374
+ readonly credentialID: Endpoint8_1Request["params"]["credentialID"]
375
+ readonly location?: Endpoint8_1Request["query"]["location"]
376
+ }
377
+ const Endpoint8_1 = (raw: RawClient["server.credential"]) => (input: Endpoint8_1Input) =>
378
+ raw["credential.remove"]({
379
+ params: { credentialID: input["credentialID"] },
380
+ query: { location: input["location"] },
381
+ }).pipe(Effect.mapError(mapClientError))
382
+
383
+ const adaptGroup8 = (raw: RawClient["server.credential"]) => ({ update: Endpoint8_0(raw), remove: Endpoint8_1(raw) })
384
+
385
+ type Endpoint9_0Request = Parameters<RawClient["server.permission"]["permission.request.list"]>[0]
386
+ type Endpoint9_0Input = { readonly location?: Endpoint9_0Request["query"]["location"] }
387
+ const Endpoint9_0 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_0Input) =>
388
+ raw["permission.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
389
+
390
+ type Endpoint9_1Request = Parameters<RawClient["server.permission"]["permission.saved.list"]>[0]
391
+ type Endpoint9_1Input = { readonly projectID?: Endpoint9_1Request["query"]["projectID"] }
392
+ const Endpoint9_1 = (raw: RawClient["server.permission"]) => (input?: Endpoint9_1Input) =>
393
+ raw["permission.saved.list"]({ query: { projectID: input?.["projectID"] } }).pipe(
394
+ Effect.mapError(mapClientError),
395
+ Effect.map((value) => value.data),
396
+ )
397
+
398
+ type Endpoint9_2Request = Parameters<RawClient["server.permission"]["permission.saved.remove"]>[0]
399
+ type Endpoint9_2Input = { readonly id: Endpoint9_2Request["params"]["id"] }
400
+ const Endpoint9_2 = (raw: RawClient["server.permission"]) => (input: Endpoint9_2Input) =>
401
+ raw["permission.saved.remove"]({ params: { id: input["id"] } }).pipe(Effect.mapError(mapClientError))
402
+
403
+ type Endpoint9_3Request = Parameters<RawClient["server.permission"]["session.permission.create"]>[0]
404
+ type Endpoint9_3Input = {
405
+ readonly sessionID: Endpoint9_3Request["params"]["sessionID"]
406
+ readonly id?: Endpoint9_3Request["payload"]["id"]
407
+ readonly action: Endpoint9_3Request["payload"]["action"]
408
+ readonly resources: Endpoint9_3Request["payload"]["resources"]
409
+ readonly save?: Endpoint9_3Request["payload"]["save"]
410
+ readonly metadata?: Endpoint9_3Request["payload"]["metadata"]
411
+ readonly source?: Endpoint9_3Request["payload"]["source"]
412
+ readonly agent?: Endpoint9_3Request["payload"]["agent"]
413
+ }
414
+ const Endpoint9_3 = (raw: RawClient["server.permission"]) => (input: Endpoint9_3Input) =>
415
+ raw["session.permission.create"]({
416
+ params: { sessionID: input["sessionID"] },
417
+ payload: {
418
+ id: input["id"],
419
+ action: input["action"],
420
+ resources: input["resources"],
421
+ save: input["save"],
422
+ metadata: input["metadata"],
423
+ source: input["source"],
424
+ agent: input["agent"],
425
+ },
426
+ }).pipe(
427
+ Effect.mapError(mapClientError),
428
+ Effect.map((value) => value.data),
429
+ )
430
+
431
+ type Endpoint9_4Request = Parameters<RawClient["server.permission"]["session.permission.list"]>[0]
432
+ type Endpoint9_4Input = { readonly sessionID: Endpoint9_4Request["params"]["sessionID"] }
433
+ const Endpoint9_4 = (raw: RawClient["server.permission"]) => (input: Endpoint9_4Input) =>
434
+ raw["session.permission.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
435
+ Effect.mapError(mapClientError),
436
+ Effect.map((value) => value.data),
437
+ )
438
+
439
+ type Endpoint9_5Request = Parameters<RawClient["server.permission"]["session.permission.get"]>[0]
440
+ type Endpoint9_5Input = {
441
+ readonly sessionID: Endpoint9_5Request["params"]["sessionID"]
442
+ readonly requestID: Endpoint9_5Request["params"]["requestID"]
443
+ }
444
+ const Endpoint9_5 = (raw: RawClient["server.permission"]) => (input: Endpoint9_5Input) =>
445
+ raw["session.permission.get"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
446
+ Effect.mapError(mapClientError),
447
+ Effect.map((value) => value.data),
448
+ )
449
+
450
+ type Endpoint9_6Request = Parameters<RawClient["server.permission"]["session.permission.reply"]>[0]
451
+ type Endpoint9_6Input = {
452
+ readonly sessionID: Endpoint9_6Request["params"]["sessionID"]
453
+ readonly requestID: Endpoint9_6Request["params"]["requestID"]
454
+ readonly reply: Endpoint9_6Request["payload"]["reply"]
455
+ readonly message?: Endpoint9_6Request["payload"]["message"]
456
+ }
457
+ const Endpoint9_6 = (raw: RawClient["server.permission"]) => (input: Endpoint9_6Input) =>
458
+ raw["session.permission.reply"]({
459
+ params: { sessionID: input["sessionID"], requestID: input["requestID"] },
460
+ payload: { reply: input["reply"], message: input["message"] },
461
+ }).pipe(Effect.mapError(mapClientError))
462
+
463
+ const adaptGroup9 = (raw: RawClient["server.permission"]) => ({
464
+ listRequests: Endpoint9_0(raw),
465
+ listSaved: Endpoint9_1(raw),
466
+ removeSaved: Endpoint9_2(raw),
467
+ create: Endpoint9_3(raw),
468
+ list: Endpoint9_4(raw),
469
+ get: Endpoint9_5(raw),
470
+ reply: Endpoint9_6(raw),
471
+ })
472
+
473
+ type Endpoint10_0Request = Parameters<RawClient["server.fs"]["fs.list"]>[0]
474
+ type Endpoint10_0Input = {
475
+ readonly location?: Endpoint10_0Request["query"]["location"]
476
+ readonly path?: Endpoint10_0Request["query"]["path"]
477
+ }
478
+ const Endpoint10_0 = (raw: RawClient["server.fs"]) => (input?: Endpoint10_0Input) =>
479
+ raw["fs.list"]({ query: { location: input?.["location"], path: input?.["path"] } }).pipe(
480
+ Effect.mapError(mapClientError),
481
+ )
482
+
483
+ type Endpoint10_1Request = Parameters<RawClient["server.fs"]["fs.find"]>[0]
484
+ type Endpoint10_1Input = {
485
+ readonly location?: Endpoint10_1Request["query"]["location"]
486
+ readonly query: Endpoint10_1Request["query"]["query"]
487
+ readonly type?: Endpoint10_1Request["query"]["type"]
488
+ readonly limit?: Endpoint10_1Request["query"]["limit"]
489
+ }
490
+ const Endpoint10_1 = (raw: RawClient["server.fs"]) => (input: Endpoint10_1Input) =>
491
+ raw["fs.find"]({
492
+ query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
493
+ }).pipe(Effect.mapError(mapClientError))
494
+
495
+ const adaptGroup10 = (raw: RawClient["server.fs"]) => ({ list: Endpoint10_0(raw), find: Endpoint10_1(raw) })
496
+
497
+ type Endpoint11_0Request = Parameters<RawClient["server.command"]["command.list"]>[0]
498
+ type Endpoint11_0Input = { readonly location?: Endpoint11_0Request["query"]["location"] }
499
+ const Endpoint11_0 = (raw: RawClient["server.command"]) => (input?: Endpoint11_0Input) =>
500
+ raw["command.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
501
+
502
+ const adaptGroup11 = (raw: RawClient["server.command"]) => ({ list: Endpoint11_0(raw) })
503
+
504
+ type Endpoint12_0Request = Parameters<RawClient["server.skill"]["skill.list"]>[0]
505
+ type Endpoint12_0Input = { readonly location?: Endpoint12_0Request["query"]["location"] }
506
+ const Endpoint12_0 = (raw: RawClient["server.skill"]) => (input?: Endpoint12_0Input) =>
507
+ raw["skill.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
508
+
509
+ const adaptGroup12 = (raw: RawClient["server.skill"]) => ({ list: Endpoint12_0(raw) })
510
+
511
+ const Endpoint13_0 = (raw: RawClient["server.event"]) => () =>
512
+ Stream.unwrap(
513
+ raw["event.subscribe"]({}).pipe(
514
+ Effect.mapError(mapClientError),
515
+ Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))),
516
+ ),
517
+ )
518
+
519
+ const adaptGroup13 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint13_0(raw) })
520
+
521
+ type Endpoint14_0Request = Parameters<RawClient["server.pty"]["pty.list"]>[0]
522
+ type Endpoint14_0Input = { readonly location?: Endpoint14_0Request["query"]["location"] }
523
+ const Endpoint14_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_0Input) =>
524
+ raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
525
+
526
+ type Endpoint14_1Request = Parameters<RawClient["server.pty"]["pty.create"]>[0]
527
+ type Endpoint14_1Input = {
528
+ readonly location?: Endpoint14_1Request["query"]["location"]
529
+ readonly command?: Endpoint14_1Request["payload"]["command"]
530
+ readonly args?: Endpoint14_1Request["payload"]["args"]
531
+ readonly cwd?: Endpoint14_1Request["payload"]["cwd"]
532
+ readonly title?: Endpoint14_1Request["payload"]["title"]
533
+ readonly env?: Endpoint14_1Request["payload"]["env"]
534
+ }
535
+ const Endpoint14_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint14_1Input) =>
536
+ raw["pty.create"]({
537
+ query: { location: input?.["location"] },
538
+ payload: {
539
+ command: input?.["command"],
540
+ args: input?.["args"],
541
+ cwd: input?.["cwd"],
542
+ title: input?.["title"],
543
+ env: input?.["env"],
544
+ },
545
+ }).pipe(Effect.mapError(mapClientError))
546
+
547
+ type Endpoint14_2Request = Parameters<RawClient["server.pty"]["pty.get"]>[0]
548
+ type Endpoint14_2Input = {
549
+ readonly ptyID: Endpoint14_2Request["params"]["ptyID"]
550
+ readonly location?: Endpoint14_2Request["query"]["location"]
551
+ }
552
+ const Endpoint14_2 = (raw: RawClient["server.pty"]) => (input: Endpoint14_2Input) =>
553
+ raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
554
+ Effect.mapError(mapClientError),
555
+ )
556
+
557
+ type Endpoint14_3Request = Parameters<RawClient["server.pty"]["pty.update"]>[0]
558
+ type Endpoint14_3Input = {
559
+ readonly ptyID: Endpoint14_3Request["params"]["ptyID"]
560
+ readonly location?: Endpoint14_3Request["query"]["location"]
561
+ readonly title?: Endpoint14_3Request["payload"]["title"]
562
+ readonly size?: Endpoint14_3Request["payload"]["size"]
563
+ }
564
+ const Endpoint14_3 = (raw: RawClient["server.pty"]) => (input: Endpoint14_3Input) =>
565
+ raw["pty.update"]({
566
+ params: { ptyID: input["ptyID"] },
567
+ query: { location: input["location"] },
568
+ payload: { title: input["title"], size: input["size"] },
569
+ }).pipe(Effect.mapError(mapClientError))
570
+
571
+ type Endpoint14_4Request = Parameters<RawClient["server.pty"]["pty.remove"]>[0]
572
+ type Endpoint14_4Input = {
573
+ readonly ptyID: Endpoint14_4Request["params"]["ptyID"]
574
+ readonly location?: Endpoint14_4Request["query"]["location"]
575
+ }
576
+ const Endpoint14_4 = (raw: RawClient["server.pty"]) => (input: Endpoint14_4Input) =>
577
+ raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe(
578
+ Effect.mapError(mapClientError),
579
+ )
580
+
581
+ const adaptGroup14 = (raw: RawClient["server.pty"]) => ({
582
+ list: Endpoint14_0(raw),
583
+ create: Endpoint14_1(raw),
584
+ get: Endpoint14_2(raw),
585
+ update: Endpoint14_3(raw),
586
+ remove: Endpoint14_4(raw),
587
+ })
588
+
589
+ type Endpoint15_0Request = Parameters<RawClient["server.question"]["question.request.list"]>[0]
590
+ type Endpoint15_0Input = { readonly location?: Endpoint15_0Request["query"]["location"] }
591
+ const Endpoint15_0 = (raw: RawClient["server.question"]) => (input?: Endpoint15_0Input) =>
592
+ raw["question.request.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
593
+
594
+ type Endpoint15_1Request = Parameters<RawClient["server.question"]["session.question.list"]>[0]
595
+ type Endpoint15_1Input = { readonly sessionID: Endpoint15_1Request["params"]["sessionID"] }
596
+ const Endpoint15_1 = (raw: RawClient["server.question"]) => (input: Endpoint15_1Input) =>
597
+ raw["session.question.list"]({ params: { sessionID: input["sessionID"] } }).pipe(
598
+ Effect.mapError(mapClientError),
599
+ Effect.map((value) => value.data),
600
+ )
601
+
602
+ type Endpoint15_2Request = Parameters<RawClient["server.question"]["session.question.reply"]>[0]
603
+ type Endpoint15_2Input = {
604
+ readonly sessionID: Endpoint15_2Request["params"]["sessionID"]
605
+ readonly requestID: Endpoint15_2Request["params"]["requestID"]
606
+ readonly answers: Endpoint15_2Request["payload"]["answers"]
607
+ }
608
+ const Endpoint15_2 = (raw: RawClient["server.question"]) => (input: Endpoint15_2Input) =>
609
+ raw["session.question.reply"]({
610
+ params: { sessionID: input["sessionID"], requestID: input["requestID"] },
611
+ payload: { answers: input["answers"] },
612
+ }).pipe(Effect.mapError(mapClientError))
613
+
614
+ type Endpoint15_3Request = Parameters<RawClient["server.question"]["session.question.reject"]>[0]
615
+ type Endpoint15_3Input = {
616
+ readonly sessionID: Endpoint15_3Request["params"]["sessionID"]
617
+ readonly requestID: Endpoint15_3Request["params"]["requestID"]
618
+ }
619
+ const Endpoint15_3 = (raw: RawClient["server.question"]) => (input: Endpoint15_3Input) =>
620
+ raw["session.question.reject"]({ params: { sessionID: input["sessionID"], requestID: input["requestID"] } }).pipe(
621
+ Effect.mapError(mapClientError),
622
+ )
623
+
624
+ const adaptGroup15 = (raw: RawClient["server.question"]) => ({
625
+ listRequests: Endpoint15_0(raw),
626
+ list: Endpoint15_1(raw),
627
+ reply: Endpoint15_2(raw),
628
+ reject: Endpoint15_3(raw),
629
+ })
630
+
631
+ type Endpoint16_0Request = Parameters<RawClient["server.reference"]["reference.list"]>[0]
632
+ type Endpoint16_0Input = { readonly location?: Endpoint16_0Request["query"]["location"] }
633
+ const Endpoint16_0 = (raw: RawClient["server.reference"]) => (input?: Endpoint16_0Input) =>
634
+ raw["reference.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError))
635
+
636
+ const adaptGroup16 = (raw: RawClient["server.reference"]) => ({ list: Endpoint16_0(raw) })
637
+
638
+ type Endpoint17_0Request = Parameters<RawClient["server.projectCopy"]["projectCopy.create"]>[0]
639
+ type Endpoint17_0Input = {
640
+ readonly projectID: Endpoint17_0Request["params"]["projectID"]
641
+ readonly location?: Endpoint17_0Request["query"]["location"]
642
+ readonly strategy: Endpoint17_0Request["payload"]["strategy"]
643
+ readonly directory: Endpoint17_0Request["payload"]["directory"]
644
+ readonly name?: Endpoint17_0Request["payload"]["name"]
645
+ }
646
+ const Endpoint17_0 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_0Input) =>
647
+ raw["projectCopy.create"]({
648
+ params: { projectID: input["projectID"] },
649
+ query: { location: input["location"] },
650
+ payload: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
651
+ }).pipe(Effect.mapError(mapClientError))
652
+
653
+ type Endpoint17_1Request = Parameters<RawClient["server.projectCopy"]["projectCopy.remove"]>[0]
654
+ type Endpoint17_1Input = {
655
+ readonly projectID: Endpoint17_1Request["params"]["projectID"]
656
+ readonly location?: Endpoint17_1Request["query"]["location"]
657
+ readonly directory: Endpoint17_1Request["payload"]["directory"]
658
+ readonly force: Endpoint17_1Request["payload"]["force"]
659
+ }
660
+ const Endpoint17_1 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_1Input) =>
661
+ raw["projectCopy.remove"]({
662
+ params: { projectID: input["projectID"] },
663
+ query: { location: input["location"] },
664
+ payload: { directory: input["directory"], force: input["force"] },
665
+ }).pipe(Effect.mapError(mapClientError))
666
+
667
+ type Endpoint17_2Request = Parameters<RawClient["server.projectCopy"]["projectCopy.refresh"]>[0]
668
+ type Endpoint17_2Input = {
669
+ readonly projectID: Endpoint17_2Request["params"]["projectID"]
670
+ readonly location?: Endpoint17_2Request["query"]["location"]
671
+ }
672
+ const Endpoint17_2 = (raw: RawClient["server.projectCopy"]) => (input: Endpoint17_2Input) =>
673
+ raw["projectCopy.refresh"]({
674
+ params: { projectID: input["projectID"] },
675
+ query: { location: input["location"] },
676
+ }).pipe(Effect.mapError(mapClientError))
677
+
678
+ const adaptGroup17 = (raw: RawClient["server.projectCopy"]) => ({
679
+ create: Endpoint17_0(raw),
680
+ remove: Endpoint17_1(raw),
681
+ refresh: Endpoint17_2(raw),
682
+ })
683
+
684
+ const adaptClient = (raw: RawClient) => ({
685
+ health: adaptGroup0(raw["server.health"]),
686
+ location: adaptGroup1(raw["server.location"]),
687
+ agents: adaptGroup2(raw["server.agent"]),
688
+ sessions: adaptGroup3(raw["server.session"]),
689
+ messages: adaptGroup4(raw["server.message"]),
690
+ models: adaptGroup5(raw["server.model"]),
691
+ providers: adaptGroup6(raw["server.provider"]),
692
+ integrations: adaptGroup7(raw["server.integration"]),
693
+ credentials: adaptGroup8(raw["server.credential"]),
694
+ permissions: adaptGroup9(raw["server.permission"]),
695
+ files: adaptGroup10(raw["server.fs"]),
696
+ commands: adaptGroup11(raw["server.command"]),
697
+ skills: adaptGroup12(raw["server.skill"]),
698
+ events: adaptGroup13(raw["server.event"]),
699
+ ptys: adaptGroup14(raw["server.pty"]),
700
+ questions: adaptGroup15(raw["server.question"]),
701
+ references: adaptGroup16(raw["server.reference"]),
702
+ projectCopies: adaptGroup17(raw["server.projectCopy"]),
703
+ })
704
+
705
+ export const make = (options?: { readonly baseUrl?: URL | string }) =>
706
+ HttpApiClient.make(ClientApi, options).pipe(Effect.map(adaptClient))
packages/client/src/generated-effect/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { ClientError } from "./client-error"
2
+ export * as OpenCode from "./client"
packages/client/src/generated/.httpapi-codegen.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ [
2
+ "client-error.ts",
3
+ "client.ts",
4
+ "index.ts",
5
+ "types.ts"
6
+ ]
packages/client/src/generated/client-error.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type ClientErrorReason = "Transport" | "UnexpectedStatus" | "UnsupportedContentType" | "MalformedResponse"
2
+
3
+ export class ClientError extends Error {
4
+ override readonly name = "ClientError"
5
+ constructor(
6
+ readonly reason: ClientErrorReason,
7
+ options?: ErrorOptions,
8
+ ) {
9
+ super(reason, options)
10
+ }
11
+ }
packages/client/src/generated/client.ts ADDED
@@ -0,0 +1,1029 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {
2
+ HealthGetOutput,
3
+ LocationGetInput,
4
+ LocationGetOutput,
5
+ AgentsListInput,
6
+ AgentsListOutput,
7
+ SessionsListInput,
8
+ SessionsListOutput,
9
+ SessionsCreateInput,
10
+ SessionsCreateOutput,
11
+ SessionsActiveOutput,
12
+ SessionsGetInput,
13
+ SessionsGetOutput,
14
+ SessionsSwitchAgentInput,
15
+ SessionsSwitchAgentOutput,
16
+ SessionsSwitchModelInput,
17
+ SessionsSwitchModelOutput,
18
+ SessionsPromptInput,
19
+ SessionsPromptOutput,
20
+ SessionsCompactInput,
21
+ SessionsCompactOutput,
22
+ SessionsWaitInput,
23
+ SessionsWaitOutput,
24
+ SessionsStageInput,
25
+ SessionsStageOutput,
26
+ SessionsClearInput,
27
+ SessionsClearOutput,
28
+ SessionsCommitInput,
29
+ SessionsCommitOutput,
30
+ SessionsContextInput,
31
+ SessionsContextOutput,
32
+ SessionsHistoryInput,
33
+ SessionsHistoryOutput,
34
+ SessionsEventsInput,
35
+ SessionsEventsOutput,
36
+ SessionsInterruptInput,
37
+ SessionsInterruptOutput,
38
+ SessionsMessageInput,
39
+ SessionsMessageOutput,
40
+ MessagesListInput,
41
+ MessagesListOutput,
42
+ ModelsListInput,
43
+ ModelsListOutput,
44
+ ProvidersListInput,
45
+ ProvidersListOutput,
46
+ ProvidersGetInput,
47
+ ProvidersGetOutput,
48
+ IntegrationsListInput,
49
+ IntegrationsListOutput,
50
+ IntegrationsGetInput,
51
+ IntegrationsGetOutput,
52
+ IntegrationsConnectKeyInput,
53
+ IntegrationsConnectKeyOutput,
54
+ IntegrationsConnectOauthInput,
55
+ IntegrationsConnectOauthOutput,
56
+ IntegrationsAttemptStatusInput,
57
+ IntegrationsAttemptStatusOutput,
58
+ IntegrationsAttemptCompleteInput,
59
+ IntegrationsAttemptCompleteOutput,
60
+ IntegrationsAttemptCancelInput,
61
+ IntegrationsAttemptCancelOutput,
62
+ CredentialsUpdateInput,
63
+ CredentialsUpdateOutput,
64
+ CredentialsRemoveInput,
65
+ CredentialsRemoveOutput,
66
+ PermissionsListRequestsInput,
67
+ PermissionsListRequestsOutput,
68
+ PermissionsListSavedInput,
69
+ PermissionsListSavedOutput,
70
+ PermissionsRemoveSavedInput,
71
+ PermissionsRemoveSavedOutput,
72
+ PermissionsCreateInput,
73
+ PermissionsCreateOutput,
74
+ PermissionsListInput,
75
+ PermissionsListOutput,
76
+ PermissionsGetInput,
77
+ PermissionsGetOutput,
78
+ PermissionsReplyInput,
79
+ PermissionsReplyOutput,
80
+ FilesListInput,
81
+ FilesListOutput,
82
+ FilesFindInput,
83
+ FilesFindOutput,
84
+ CommandsListInput,
85
+ CommandsListOutput,
86
+ SkillsListInput,
87
+ SkillsListOutput,
88
+ EventsSubscribeOutput,
89
+ PtysListInput,
90
+ PtysListOutput,
91
+ PtysCreateInput,
92
+ PtysCreateOutput,
93
+ PtysGetInput,
94
+ PtysGetOutput,
95
+ PtysUpdateInput,
96
+ PtysUpdateOutput,
97
+ PtysRemoveInput,
98
+ PtysRemoveOutput,
99
+ QuestionsListRequestsInput,
100
+ QuestionsListRequestsOutput,
101
+ QuestionsListInput,
102
+ QuestionsListOutput,
103
+ QuestionsReplyInput,
104
+ QuestionsReplyOutput,
105
+ QuestionsRejectInput,
106
+ QuestionsRejectOutput,
107
+ ReferencesListInput,
108
+ ReferencesListOutput,
109
+ ProjectCopiesCreateInput,
110
+ ProjectCopiesCreateOutput,
111
+ ProjectCopiesRemoveInput,
112
+ ProjectCopiesRemoveOutput,
113
+ ProjectCopiesRefreshInput,
114
+ ProjectCopiesRefreshOutput,
115
+ } from "./types"
116
+ import { ClientError } from "./client-error"
117
+
118
+ export interface ClientOptions {
119
+ readonly baseUrl: string
120
+ readonly fetch?: typeof globalThis.fetch
121
+ readonly headers?: HeadersInit
122
+ }
123
+
124
+ export interface RequestOptions {
125
+ readonly signal?: AbortSignal
126
+ readonly headers?: HeadersInit
127
+ }
128
+
129
+ interface RequestDescriptor {
130
+ readonly method: string
131
+ readonly path: string
132
+ readonly query?: Record<string, unknown>
133
+ readonly headers?: Record<string, unknown>
134
+ readonly body?: unknown
135
+ readonly successStatus: number
136
+ readonly declaredStatuses: ReadonlyArray<number>
137
+ readonly empty: boolean
138
+ }
139
+
140
+ export function make(options: ClientOptions) {
141
+ const fetch = options.fetch ?? globalThis.fetch
142
+
143
+ const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {
144
+ const url = new URL(descriptor.path, options.baseUrl)
145
+ for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value)
146
+ const headers = new Headers(options.headers)
147
+ for (const [key, value] of Object.entries(descriptor.headers ?? {})) {
148
+ if (value !== undefined && value !== null) headers.set(key, String(value))
149
+ }
150
+ for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)
151
+ if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")
152
+ return {
153
+ url,
154
+ init: {
155
+ method: descriptor.method,
156
+ signal: requestOptions?.signal,
157
+ headers,
158
+ body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body),
159
+ } satisfies RequestInit,
160
+ }
161
+ }
162
+
163
+ const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {
164
+ try {
165
+ const prepared = prepare(descriptor, requestOptions)
166
+ return await fetch(prepared.url, prepared.init)
167
+ } catch (cause) {
168
+ throw new ClientError("Transport", { cause })
169
+ }
170
+ }
171
+
172
+ const responseError = async (response: Response, descriptor: RequestDescriptor): Promise<never> => {
173
+ if (descriptor.declaredStatuses.includes(response.status)) throw await json(response)
174
+ try {
175
+ await response.body?.cancel()
176
+ } catch {}
177
+ throw new ClientError("UnexpectedStatus", { cause: { status: response.status } })
178
+ }
179
+
180
+ const request = async <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise<A> => {
181
+ const response = await execute(descriptor, requestOptions)
182
+ if (response.status !== descriptor.successStatus) return responseError(response, descriptor)
183
+ if (descriptor.empty) {
184
+ try {
185
+ await response.body?.cancel()
186
+ } catch {}
187
+ return undefined as A
188
+ }
189
+ return (await json(response)) as A
190
+ }
191
+
192
+ const sse = <A>(descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable<A> => ({
193
+ async *[Symbol.asyncIterator]() {
194
+ const response = await execute(descriptor, requestOptions)
195
+ if (response.status !== descriptor.successStatus) await responseError(response, descriptor)
196
+ if (!isContentType(response, "text/event-stream")) {
197
+ try {
198
+ await response.body?.cancel()
199
+ } catch {}
200
+ throw new ClientError("UnsupportedContentType")
201
+ }
202
+ if (response.body === null) throw new ClientError("MalformedResponse")
203
+ const reader = response.body.getReader()
204
+ const decoder = new TextDecoder()
205
+ let buffer = ""
206
+ try {
207
+ while (true) {
208
+ let next
209
+ try {
210
+ next = await reader.read()
211
+ } catch (cause) {
212
+ throw new ClientError("Transport", { cause })
213
+ }
214
+ buffer += decoder.decode(next.value, { stream: !next.done })
215
+ if (buffer.length > 1_048_576) throw new ClientError("MalformedResponse")
216
+ const trailingCarriageReturn = !next.done && buffer.endsWith("\r")
217
+ if (trailingCarriageReturn) buffer = buffer.slice(0, -1)
218
+ buffer = buffer.replaceAll("\r\n", "\n").replaceAll("\r", "\n")
219
+ if (trailingCarriageReturn) buffer += "\r"
220
+ if (next.done && buffer !== "") buffer += "\n\n"
221
+ let boundary = buffer.indexOf("\n\n")
222
+ while (boundary >= 0) {
223
+ const block = buffer.slice(0, boundary)
224
+ buffer = buffer.slice(boundary + 2)
225
+ const data = block
226
+ .split("\n")
227
+ .flatMap((line) => (line.startsWith("data:") ? [line.slice(5).trimStart()] : []))
228
+ .join("\n")
229
+ if (data !== "") {
230
+ try {
231
+ yield JSON.parse(data) as A
232
+ } catch (cause) {
233
+ throw new ClientError("MalformedResponse", { cause })
234
+ }
235
+ }
236
+ boundary = buffer.indexOf("\n\n")
237
+ }
238
+ if (next.done) return
239
+ }
240
+ } finally {
241
+ try {
242
+ await reader.cancel()
243
+ } catch {}
244
+ reader.releaseLock()
245
+ }
246
+ },
247
+ })
248
+
249
+ return {
250
+ health: {
251
+ get: (requestOptions?: RequestOptions) =>
252
+ request<HealthGetOutput>(
253
+ { method: "GET", path: `/api/health`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
254
+ requestOptions,
255
+ ),
256
+ },
257
+ location: {
258
+ get: (input?: LocationGetInput, requestOptions?: RequestOptions) =>
259
+ request<LocationGetOutput>(
260
+ {
261
+ method: "GET",
262
+ path: `/api/location`,
263
+ query: { location: input?.["location"] },
264
+ successStatus: 200,
265
+ declaredStatuses: [401, 400],
266
+ empty: false,
267
+ },
268
+ requestOptions,
269
+ ),
270
+ },
271
+ agents: {
272
+ list: (input?: AgentsListInput, requestOptions?: RequestOptions) =>
273
+ request<AgentsListOutput>(
274
+ {
275
+ method: "GET",
276
+ path: `/api/agent`,
277
+ query: { location: input?.["location"] },
278
+ successStatus: 200,
279
+ declaredStatuses: [401, 400],
280
+ empty: false,
281
+ },
282
+ requestOptions,
283
+ ),
284
+ },
285
+ sessions: {
286
+ list: (input?: SessionsListInput, requestOptions?: RequestOptions) =>
287
+ request<SessionsListOutput>(
288
+ {
289
+ method: "GET",
290
+ path: `/api/session`,
291
+ query: {
292
+ workspace: input?.["workspace"],
293
+ limit: input?.["limit"],
294
+ order: input?.["order"],
295
+ search: input?.["search"],
296
+ directory: input?.["directory"],
297
+ project: input?.["project"],
298
+ subpath: input?.["subpath"],
299
+ cursor: input?.["cursor"],
300
+ },
301
+ successStatus: 200,
302
+ declaredStatuses: [400, 401],
303
+ empty: false,
304
+ },
305
+ requestOptions,
306
+ ),
307
+ create: (input?: SessionsCreateInput, requestOptions?: RequestOptions) =>
308
+ request<{ readonly data: SessionsCreateOutput }>(
309
+ {
310
+ method: "POST",
311
+ path: `/api/session`,
312
+ body: {
313
+ id: input?.["id"],
314
+ agent: input?.["agent"],
315
+ model: input?.["model"],
316
+ location: input?.["location"],
317
+ },
318
+ successStatus: 200,
319
+ declaredStatuses: [401, 400],
320
+ empty: false,
321
+ },
322
+ requestOptions,
323
+ ).then((value) => value.data),
324
+ active: (requestOptions?: RequestOptions) =>
325
+ request<{ readonly data: SessionsActiveOutput }>(
326
+ {
327
+ method: "GET",
328
+ path: `/api/session/active`,
329
+ successStatus: 200,
330
+ declaredStatuses: [401, 400],
331
+ empty: false,
332
+ },
333
+ requestOptions,
334
+ ).then((value) => value.data),
335
+ get: (input: SessionsGetInput, requestOptions?: RequestOptions) =>
336
+ request<{ readonly data: SessionsGetOutput }>(
337
+ {
338
+ method: "GET",
339
+ path: `/api/session/${encodeURIComponent(input.sessionID)}`,
340
+ successStatus: 200,
341
+ declaredStatuses: [404, 400, 401],
342
+ empty: false,
343
+ },
344
+ requestOptions,
345
+ ).then((value) => value.data),
346
+ switchAgent: (input: SessionsSwitchAgentInput, requestOptions?: RequestOptions) =>
347
+ request<SessionsSwitchAgentOutput>(
348
+ {
349
+ method: "POST",
350
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/agent`,
351
+ body: { agent: input["agent"] },
352
+ successStatus: 204,
353
+ declaredStatuses: [404, 400, 401],
354
+ empty: true,
355
+ },
356
+ requestOptions,
357
+ ),
358
+ switchModel: (input: SessionsSwitchModelInput, requestOptions?: RequestOptions) =>
359
+ request<SessionsSwitchModelOutput>(
360
+ {
361
+ method: "POST",
362
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/model`,
363
+ body: { model: input["model"] },
364
+ successStatus: 204,
365
+ declaredStatuses: [404, 400, 401],
366
+ empty: true,
367
+ },
368
+ requestOptions,
369
+ ),
370
+ prompt: (input: SessionsPromptInput, requestOptions?: RequestOptions) =>
371
+ request<{ readonly data: SessionsPromptOutput }>(
372
+ {
373
+ method: "POST",
374
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/prompt`,
375
+ body: { id: input["id"], prompt: input["prompt"], delivery: input["delivery"], resume: input["resume"] },
376
+ successStatus: 200,
377
+ declaredStatuses: [409, 404, 400, 401],
378
+ empty: false,
379
+ },
380
+ requestOptions,
381
+ ).then((value) => value.data),
382
+ compact: (input: SessionsCompactInput, requestOptions?: RequestOptions) =>
383
+ request<SessionsCompactOutput>(
384
+ {
385
+ method: "POST",
386
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/compact`,
387
+ successStatus: 204,
388
+ declaredStatuses: [404, 503, 400, 401],
389
+ empty: true,
390
+ },
391
+ requestOptions,
392
+ ),
393
+ wait: (input: SessionsWaitInput, requestOptions?: RequestOptions) =>
394
+ request<SessionsWaitOutput>(
395
+ {
396
+ method: "POST",
397
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/wait`,
398
+ successStatus: 204,
399
+ declaredStatuses: [404, 503, 400, 401],
400
+ empty: true,
401
+ },
402
+ requestOptions,
403
+ ),
404
+ stage: (input: SessionsStageInput, requestOptions?: RequestOptions) =>
405
+ request<{ readonly data: SessionsStageOutput }>(
406
+ {
407
+ method: "POST",
408
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/stage`,
409
+ body: { messageID: input["messageID"], files: input["files"] },
410
+ successStatus: 200,
411
+ declaredStatuses: [404, 500, 400, 401],
412
+ empty: false,
413
+ },
414
+ requestOptions,
415
+ ).then((value) => value.data),
416
+ clear: (input: SessionsClearInput, requestOptions?: RequestOptions) =>
417
+ request<SessionsClearOutput>(
418
+ {
419
+ method: "POST",
420
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/clear`,
421
+ successStatus: 204,
422
+ declaredStatuses: [404, 500, 400, 401],
423
+ empty: true,
424
+ },
425
+ requestOptions,
426
+ ),
427
+ commit: (input: SessionsCommitInput, requestOptions?: RequestOptions) =>
428
+ request<SessionsCommitOutput>(
429
+ {
430
+ method: "POST",
431
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/revert/commit`,
432
+ successStatus: 204,
433
+ declaredStatuses: [404, 400, 401],
434
+ empty: true,
435
+ },
436
+ requestOptions,
437
+ ),
438
+ context: (input: SessionsContextInput, requestOptions?: RequestOptions) =>
439
+ request<{ readonly data: SessionsContextOutput }>(
440
+ {
441
+ method: "GET",
442
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/context`,
443
+ successStatus: 200,
444
+ declaredStatuses: [404, 500, 400, 401],
445
+ empty: false,
446
+ },
447
+ requestOptions,
448
+ ).then((value) => value.data),
449
+ history: (input: SessionsHistoryInput, requestOptions?: RequestOptions) =>
450
+ request<SessionsHistoryOutput>(
451
+ {
452
+ method: "GET",
453
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/history`,
454
+ query: { limit: input["limit"], after: input["after"] },
455
+ successStatus: 200,
456
+ declaredStatuses: [404, 400, 401],
457
+ empty: false,
458
+ },
459
+ requestOptions,
460
+ ),
461
+ events: (input: SessionsEventsInput, requestOptions?: RequestOptions): AsyncIterable<SessionsEventsOutput> =>
462
+ sse<SessionsEventsOutput>(
463
+ {
464
+ method: "GET",
465
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/event`,
466
+ query: { after: input["after"] },
467
+ successStatus: 200,
468
+ declaredStatuses: [404, 400, 401],
469
+ empty: false,
470
+ },
471
+ requestOptions,
472
+ ),
473
+ interrupt: (input: SessionsInterruptInput, requestOptions?: RequestOptions) =>
474
+ request<SessionsInterruptOutput>(
475
+ {
476
+ method: "POST",
477
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/interrupt`,
478
+ successStatus: 204,
479
+ declaredStatuses: [404, 400, 401],
480
+ empty: true,
481
+ },
482
+ requestOptions,
483
+ ),
484
+ message: (input: SessionsMessageInput, requestOptions?: RequestOptions) =>
485
+ request<{ readonly data: SessionsMessageOutput }>(
486
+ {
487
+ method: "GET",
488
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/message/${encodeURIComponent(input.messageID)}`,
489
+ successStatus: 200,
490
+ declaredStatuses: [404, 400, 401],
491
+ empty: false,
492
+ },
493
+ requestOptions,
494
+ ).then((value) => value.data),
495
+ },
496
+ messages: {
497
+ list: (input: MessagesListInput, requestOptions?: RequestOptions) =>
498
+ request<MessagesListOutput>(
499
+ {
500
+ method: "GET",
501
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/message`,
502
+ query: { limit: input["limit"], order: input["order"], cursor: input["cursor"] },
503
+ successStatus: 200,
504
+ declaredStatuses: [400, 404, 500, 401],
505
+ empty: false,
506
+ },
507
+ requestOptions,
508
+ ),
509
+ },
510
+ models: {
511
+ list: (input?: ModelsListInput, requestOptions?: RequestOptions) =>
512
+ request<ModelsListOutput>(
513
+ {
514
+ method: "GET",
515
+ path: `/api/model`,
516
+ query: { location: input?.["location"] },
517
+ successStatus: 200,
518
+ declaredStatuses: [503, 401, 400],
519
+ empty: false,
520
+ },
521
+ requestOptions,
522
+ ),
523
+ },
524
+ providers: {
525
+ list: (input?: ProvidersListInput, requestOptions?: RequestOptions) =>
526
+ request<ProvidersListOutput>(
527
+ {
528
+ method: "GET",
529
+ path: `/api/provider`,
530
+ query: { location: input?.["location"] },
531
+ successStatus: 200,
532
+ declaredStatuses: [503, 401, 400],
533
+ empty: false,
534
+ },
535
+ requestOptions,
536
+ ),
537
+ get: (input: ProvidersGetInput, requestOptions?: RequestOptions) =>
538
+ request<ProvidersGetOutput>(
539
+ {
540
+ method: "GET",
541
+ path: `/api/provider/${encodeURIComponent(input.providerID)}`,
542
+ query: { location: input["location"] },
543
+ successStatus: 200,
544
+ declaredStatuses: [404, 503, 401, 400],
545
+ empty: false,
546
+ },
547
+ requestOptions,
548
+ ),
549
+ },
550
+ integrations: {
551
+ list: (input?: IntegrationsListInput, requestOptions?: RequestOptions) =>
552
+ request<IntegrationsListOutput>(
553
+ {
554
+ method: "GET",
555
+ path: `/api/integration`,
556
+ query: { location: input?.["location"] },
557
+ successStatus: 200,
558
+ declaredStatuses: [401, 400],
559
+ empty: false,
560
+ },
561
+ requestOptions,
562
+ ),
563
+ get: (input: IntegrationsGetInput, requestOptions?: RequestOptions) =>
564
+ request<IntegrationsGetOutput>(
565
+ {
566
+ method: "GET",
567
+ path: `/api/integration/${encodeURIComponent(input.integrationID)}`,
568
+ query: { location: input["location"] },
569
+ successStatus: 200,
570
+ declaredStatuses: [401, 400],
571
+ empty: false,
572
+ },
573
+ requestOptions,
574
+ ),
575
+ connectKey: (input: IntegrationsConnectKeyInput, requestOptions?: RequestOptions) =>
576
+ request<IntegrationsConnectKeyOutput>(
577
+ {
578
+ method: "POST",
579
+ path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/key`,
580
+ query: { location: input["location"] },
581
+ body: { key: input["key"], label: input["label"] },
582
+ successStatus: 204,
583
+ declaredStatuses: [400, 401],
584
+ empty: true,
585
+ },
586
+ requestOptions,
587
+ ),
588
+ connectOauth: (input: IntegrationsConnectOauthInput, requestOptions?: RequestOptions) =>
589
+ request<IntegrationsConnectOauthOutput>(
590
+ {
591
+ method: "POST",
592
+ path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth`,
593
+ query: { location: input["location"] },
594
+ body: { methodID: input["methodID"], inputs: input["inputs"], label: input["label"] },
595
+ successStatus: 200,
596
+ declaredStatuses: [400, 401],
597
+ empty: false,
598
+ },
599
+ requestOptions,
600
+ ),
601
+ attemptStatus: (input: IntegrationsAttemptStatusInput, requestOptions?: RequestOptions) =>
602
+ request<IntegrationsAttemptStatusOutput>(
603
+ {
604
+ method: "GET",
605
+ path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
606
+ query: { location: input["location"] },
607
+ successStatus: 200,
608
+ declaredStatuses: [401, 400],
609
+ empty: false,
610
+ },
611
+ requestOptions,
612
+ ),
613
+ attemptComplete: (input: IntegrationsAttemptCompleteInput, requestOptions?: RequestOptions) =>
614
+ request<IntegrationsAttemptCompleteOutput>(
615
+ {
616
+ method: "POST",
617
+ path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}/complete`,
618
+ query: { location: input["location"] },
619
+ body: { code: input["code"] },
620
+ successStatus: 204,
621
+ declaredStatuses: [400, 401],
622
+ empty: true,
623
+ },
624
+ requestOptions,
625
+ ),
626
+ attemptCancel: (input: IntegrationsAttemptCancelInput, requestOptions?: RequestOptions) =>
627
+ request<IntegrationsAttemptCancelOutput>(
628
+ {
629
+ method: "DELETE",
630
+ path: `/api/integration/attempt/${encodeURIComponent(input.attemptID)}`,
631
+ query: { location: input["location"] },
632
+ successStatus: 204,
633
+ declaredStatuses: [401, 400],
634
+ empty: true,
635
+ },
636
+ requestOptions,
637
+ ),
638
+ },
639
+ credentials: {
640
+ update: (input: CredentialsUpdateInput, requestOptions?: RequestOptions) =>
641
+ request<CredentialsUpdateOutput>(
642
+ {
643
+ method: "PATCH",
644
+ path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
645
+ query: { location: input["location"] },
646
+ body: { label: input["label"] },
647
+ successStatus: 204,
648
+ declaredStatuses: [401, 400],
649
+ empty: true,
650
+ },
651
+ requestOptions,
652
+ ),
653
+ remove: (input: CredentialsRemoveInput, requestOptions?: RequestOptions) =>
654
+ request<CredentialsRemoveOutput>(
655
+ {
656
+ method: "DELETE",
657
+ path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
658
+ query: { location: input["location"] },
659
+ successStatus: 204,
660
+ declaredStatuses: [401, 400],
661
+ empty: true,
662
+ },
663
+ requestOptions,
664
+ ),
665
+ },
666
+ permissions: {
667
+ listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) =>
668
+ request<PermissionsListRequestsOutput>(
669
+ {
670
+ method: "GET",
671
+ path: `/api/permission/request`,
672
+ query: { location: input?.["location"] },
673
+ successStatus: 200,
674
+ declaredStatuses: [401, 400],
675
+ empty: false,
676
+ },
677
+ requestOptions,
678
+ ),
679
+ listSaved: (input?: PermissionsListSavedInput, requestOptions?: RequestOptions) =>
680
+ request<{ readonly data: PermissionsListSavedOutput }>(
681
+ {
682
+ method: "GET",
683
+ path: `/api/permission/saved`,
684
+ query: { projectID: input?.["projectID"] },
685
+ successStatus: 200,
686
+ declaredStatuses: [401, 400],
687
+ empty: false,
688
+ },
689
+ requestOptions,
690
+ ).then((value) => value.data),
691
+ removeSaved: (input: PermissionsRemoveSavedInput, requestOptions?: RequestOptions) =>
692
+ request<PermissionsRemoveSavedOutput>(
693
+ {
694
+ method: "DELETE",
695
+ path: `/api/permission/saved/${encodeURIComponent(input.id)}`,
696
+ successStatus: 204,
697
+ declaredStatuses: [401, 400],
698
+ empty: true,
699
+ },
700
+ requestOptions,
701
+ ),
702
+ create: (input: PermissionsCreateInput, requestOptions?: RequestOptions) =>
703
+ request<{ readonly data: PermissionsCreateOutput }>(
704
+ {
705
+ method: "POST",
706
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
707
+ body: {
708
+ id: input["id"],
709
+ action: input["action"],
710
+ resources: input["resources"],
711
+ save: input["save"],
712
+ metadata: input["metadata"],
713
+ source: input["source"],
714
+ agent: input["agent"],
715
+ },
716
+ successStatus: 200,
717
+ declaredStatuses: [404, 400, 401],
718
+ empty: false,
719
+ },
720
+ requestOptions,
721
+ ).then((value) => value.data),
722
+ list: (input: PermissionsListInput, requestOptions?: RequestOptions) =>
723
+ request<{ readonly data: PermissionsListOutput }>(
724
+ {
725
+ method: "GET",
726
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/permission`,
727
+ successStatus: 200,
728
+ declaredStatuses: [404, 400, 401],
729
+ empty: false,
730
+ },
731
+ requestOptions,
732
+ ).then((value) => value.data),
733
+ get: (input: PermissionsGetInput, requestOptions?: RequestOptions) =>
734
+ request<{ readonly data: PermissionsGetOutput }>(
735
+ {
736
+ method: "GET",
737
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}`,
738
+ successStatus: 200,
739
+ declaredStatuses: [404, 400, 401],
740
+ empty: false,
741
+ },
742
+ requestOptions,
743
+ ).then((value) => value.data),
744
+ reply: (input: PermissionsReplyInput, requestOptions?: RequestOptions) =>
745
+ request<PermissionsReplyOutput>(
746
+ {
747
+ method: "POST",
748
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/permission/${encodeURIComponent(input.requestID)}/reply`,
749
+ body: { reply: input["reply"], message: input["message"] },
750
+ successStatus: 204,
751
+ declaredStatuses: [404, 400, 401],
752
+ empty: true,
753
+ },
754
+ requestOptions,
755
+ ),
756
+ },
757
+ files: {
758
+ list: (input?: FilesListInput, requestOptions?: RequestOptions) =>
759
+ request<FilesListOutput>(
760
+ {
761
+ method: "GET",
762
+ path: `/api/fs/list`,
763
+ query: { location: input?.["location"], path: input?.["path"] },
764
+ successStatus: 200,
765
+ declaredStatuses: [401, 400],
766
+ empty: false,
767
+ },
768
+ requestOptions,
769
+ ),
770
+ find: (input: FilesFindInput, requestOptions?: RequestOptions) =>
771
+ request<FilesFindOutput>(
772
+ {
773
+ method: "GET",
774
+ path: `/api/fs/find`,
775
+ query: { location: input["location"], query: input["query"], type: input["type"], limit: input["limit"] },
776
+ successStatus: 200,
777
+ declaredStatuses: [401, 400],
778
+ empty: false,
779
+ },
780
+ requestOptions,
781
+ ),
782
+ },
783
+ commands: {
784
+ list: (input?: CommandsListInput, requestOptions?: RequestOptions) =>
785
+ request<CommandsListOutput>(
786
+ {
787
+ method: "GET",
788
+ path: `/api/command`,
789
+ query: { location: input?.["location"] },
790
+ successStatus: 200,
791
+ declaredStatuses: [401, 400],
792
+ empty: false,
793
+ },
794
+ requestOptions,
795
+ ),
796
+ },
797
+ skills: {
798
+ list: (input?: SkillsListInput, requestOptions?: RequestOptions) =>
799
+ request<SkillsListOutput>(
800
+ {
801
+ method: "GET",
802
+ path: `/api/skill`,
803
+ query: { location: input?.["location"] },
804
+ successStatus: 200,
805
+ declaredStatuses: [401, 400],
806
+ empty: false,
807
+ },
808
+ requestOptions,
809
+ ),
810
+ },
811
+ events: {
812
+ subscribe: (requestOptions?: RequestOptions): AsyncIterable<EventsSubscribeOutput> =>
813
+ sse<EventsSubscribeOutput>(
814
+ { method: "GET", path: `/api/event`, successStatus: 200, declaredStatuses: [401, 400], empty: false },
815
+ requestOptions,
816
+ ),
817
+ },
818
+ ptys: {
819
+ list: (input?: PtysListInput, requestOptions?: RequestOptions) =>
820
+ request<PtysListOutput>(
821
+ {
822
+ method: "GET",
823
+ path: `/api/pty`,
824
+ query: { location: input?.["location"] },
825
+ successStatus: 200,
826
+ declaredStatuses: [401, 400],
827
+ empty: false,
828
+ },
829
+ requestOptions,
830
+ ),
831
+ create: (input?: PtysCreateInput, requestOptions?: RequestOptions) =>
832
+ request<PtysCreateOutput>(
833
+ {
834
+ method: "POST",
835
+ path: `/api/pty`,
836
+ query: { location: input?.["location"] },
837
+ body: {
838
+ command: input?.["command"],
839
+ args: input?.["args"],
840
+ cwd: input?.["cwd"],
841
+ title: input?.["title"],
842
+ env: input?.["env"],
843
+ },
844
+ successStatus: 200,
845
+ declaredStatuses: [401, 400],
846
+ empty: false,
847
+ },
848
+ requestOptions,
849
+ ),
850
+ get: (input: PtysGetInput, requestOptions?: RequestOptions) =>
851
+ request<PtysGetOutput>(
852
+ {
853
+ method: "GET",
854
+ path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
855
+ query: { location: input["location"] },
856
+ successStatus: 200,
857
+ declaredStatuses: [404, 401, 400],
858
+ empty: false,
859
+ },
860
+ requestOptions,
861
+ ),
862
+ update: (input: PtysUpdateInput, requestOptions?: RequestOptions) =>
863
+ request<PtysUpdateOutput>(
864
+ {
865
+ method: "PUT",
866
+ path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
867
+ query: { location: input["location"] },
868
+ body: { title: input["title"], size: input["size"] },
869
+ successStatus: 200,
870
+ declaredStatuses: [404, 401, 400],
871
+ empty: false,
872
+ },
873
+ requestOptions,
874
+ ),
875
+ remove: (input: PtysRemoveInput, requestOptions?: RequestOptions) =>
876
+ request<PtysRemoveOutput>(
877
+ {
878
+ method: "DELETE",
879
+ path: `/api/pty/${encodeURIComponent(input.ptyID)}`,
880
+ query: { location: input["location"] },
881
+ successStatus: 204,
882
+ declaredStatuses: [404, 401, 400],
883
+ empty: true,
884
+ },
885
+ requestOptions,
886
+ ),
887
+ },
888
+ questions: {
889
+ listRequests: (input?: QuestionsListRequestsInput, requestOptions?: RequestOptions) =>
890
+ request<QuestionsListRequestsOutput>(
891
+ {
892
+ method: "GET",
893
+ path: `/api/question/request`,
894
+ query: { location: input?.["location"] },
895
+ successStatus: 200,
896
+ declaredStatuses: [401, 400],
897
+ empty: false,
898
+ },
899
+ requestOptions,
900
+ ),
901
+ list: (input: QuestionsListInput, requestOptions?: RequestOptions) =>
902
+ request<{ readonly data: QuestionsListOutput }>(
903
+ {
904
+ method: "GET",
905
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/question`,
906
+ successStatus: 200,
907
+ declaredStatuses: [404, 400, 401],
908
+ empty: false,
909
+ },
910
+ requestOptions,
911
+ ).then((value) => value.data),
912
+ reply: (input: QuestionsReplyInput, requestOptions?: RequestOptions) =>
913
+ request<QuestionsReplyOutput>(
914
+ {
915
+ method: "POST",
916
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reply`,
917
+ body: { answers: input["answers"] },
918
+ successStatus: 204,
919
+ declaredStatuses: [404, 400, 401],
920
+ empty: true,
921
+ },
922
+ requestOptions,
923
+ ),
924
+ reject: (input: QuestionsRejectInput, requestOptions?: RequestOptions) =>
925
+ request<QuestionsRejectOutput>(
926
+ {
927
+ method: "POST",
928
+ path: `/api/session/${encodeURIComponent(input.sessionID)}/question/${encodeURIComponent(input.requestID)}/reject`,
929
+ successStatus: 204,
930
+ declaredStatuses: [404, 400, 401],
931
+ empty: true,
932
+ },
933
+ requestOptions,
934
+ ),
935
+ },
936
+ references: {
937
+ list: (input?: ReferencesListInput, requestOptions?: RequestOptions) =>
938
+ request<ReferencesListOutput>(
939
+ {
940
+ method: "GET",
941
+ path: `/api/reference`,
942
+ query: { location: input?.["location"] },
943
+ successStatus: 200,
944
+ declaredStatuses: [401, 400],
945
+ empty: false,
946
+ },
947
+ requestOptions,
948
+ ),
949
+ },
950
+ projectCopies: {
951
+ create: (input: ProjectCopiesCreateInput, requestOptions?: RequestOptions) =>
952
+ request<ProjectCopiesCreateOutput>(
953
+ {
954
+ method: "POST",
955
+ path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`,
956
+ query: { location: input["location"] },
957
+ body: { strategy: input["strategy"], directory: input["directory"], name: input["name"] },
958
+ successStatus: 200,
959
+ declaredStatuses: [400, 401],
960
+ empty: false,
961
+ },
962
+ requestOptions,
963
+ ),
964
+ remove: (input: ProjectCopiesRemoveInput, requestOptions?: RequestOptions) =>
965
+ request<ProjectCopiesRemoveOutput>(
966
+ {
967
+ method: "DELETE",
968
+ path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy`,
969
+ query: { location: input["location"] },
970
+ body: { directory: input["directory"], force: input["force"] },
971
+ successStatus: 204,
972
+ declaredStatuses: [400, 401],
973
+ empty: true,
974
+ },
975
+ requestOptions,
976
+ ),
977
+ refresh: (input: ProjectCopiesRefreshInput, requestOptions?: RequestOptions) =>
978
+ request<ProjectCopiesRefreshOutput>(
979
+ {
980
+ method: "POST",
981
+ path: `/experimental/project/${encodeURIComponent(input.projectID)}/copy/refresh`,
982
+ query: { location: input["location"] },
983
+ successStatus: 204,
984
+ declaredStatuses: [400, 401],
985
+ empty: true,
986
+ },
987
+ requestOptions,
988
+ ),
989
+ },
990
+ }
991
+ }
992
+
993
+ function appendQuery(params: URLSearchParams, key: string, value: unknown): void {
994
+ if (value === undefined || value === null) return
995
+ if (Array.isArray(value)) {
996
+ for (const item of value) appendQuery(params, key, item)
997
+ return
998
+ }
999
+ if (typeof value === "object") {
1000
+ for (const [child, item] of Object.entries(value)) appendQuery(params, `${key}[${child}]`, item)
1001
+ return
1002
+ }
1003
+ params.append(key, String(value))
1004
+ }
1005
+
1006
+ async function json(response: Response): Promise<unknown> {
1007
+ if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {
1008
+ try {
1009
+ await response.body?.cancel()
1010
+ } catch {}
1011
+ throw new ClientError("UnsupportedContentType")
1012
+ }
1013
+ let text: string
1014
+ try {
1015
+ text = await response.text()
1016
+ } catch (cause) {
1017
+ throw new ClientError("Transport", { cause })
1018
+ }
1019
+ if (text === "") throw new ClientError("MalformedResponse")
1020
+ try {
1021
+ return JSON.parse(text)
1022
+ } catch (cause) {
1023
+ throw new ClientError("MalformedResponse", { cause })
1024
+ }
1025
+ }
1026
+
1027
+ function isContentType(response: Response, expected: string) {
1028
+ return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected
1029
+ }
packages/client/src/generated/index.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export { ClientError, type ClientErrorReason } from "./client-error"
2
+ export * as OpenCode from "./client"
3
+ export * from "./types"
packages/client/src/generated/types.ts ADDED
The diff for this file is too large to render. See raw diff
 
packages/client/src/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export * from "./generated/index"
2
+ export type { EventsSubscribeOutput as OpenCodeEvent } from "./generated/types"
packages/client/test/contract-identity.test.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect, test } from "bun:test"
2
+ import { Schema } from "effect"
3
+ import { AgentV2 } from "@opencode-ai/core/agent"
4
+ import { Location as CoreLocation } from "@opencode-ai/core/location"
5
+ import { ModelV2 } from "@opencode-ai/core/model"
6
+ import { SessionV2 } from "@opencode-ai/core/session"
7
+ import { SessionInput as CoreSessionInput } from "@opencode-ai/core/session/input"
8
+ import { SessionMessage as CoreSessionMessage } from "@opencode-ai/core/session/message"
9
+ import { Prompt as CorePrompt } from "@opencode-ai/core/session/prompt"
10
+ import { Agent } from "@opencode-ai/schema/agent"
11
+ import { Location } from "@opencode-ai/schema/location"
12
+ import { Model } from "@opencode-ai/schema/model"
13
+ import { Project } from "@opencode-ai/schema/project"
14
+ import { Provider } from "@opencode-ai/schema/provider"
15
+ import { Prompt } from "@opencode-ai/schema/prompt"
16
+ import { Session } from "@opencode-ai/schema/session"
17
+ import { SessionInput } from "@opencode-ai/schema/session-input"
18
+ import { SessionMessage } from "@opencode-ai/schema/session-message"
19
+ import { Workspace } from "@opencode-ai/schema/workspace"
20
+ import { Api } from "@opencode-ai/server/api"
21
+ import { compile, emitPromise } from "@opencode-ai/httpapi-codegen"
22
+ import { ClientApi, endpointNames, groupNames, omitEndpoints } from "../src/contract"
23
+
24
+ test("Core and Server reuse the authoritative Schema and Protocol values", () => {
25
+ expect(AgentV2.ID).toBe(Agent.ID)
26
+ expect(CoreLocation.Ref).toBe(Location.Ref)
27
+ expect(ModelV2.Ref).toBe(Model.Ref)
28
+ expect(SessionV2.Info).toBe(Session.Info)
29
+ expect(CoreSessionInput.Admitted).toBe(SessionInput.Admitted)
30
+ expect(CoreSessionMessage.Message).toBe(SessionMessage.Message)
31
+ expect(CorePrompt).toBe(Prompt)
32
+ expect(Api.groups["server.session"].identifier).toBe("server.session")
33
+ expect(Object.keys(ClientApi.groups)).toEqual(Object.keys(Api.groups))
34
+ expect(Session.ID.create()).toStartWith("ses_")
35
+ expect(Project.ID.global).toBe("global")
36
+ expect(Provider.ID.anthropic).toBe("anthropic")
37
+ expect(Workspace.ID.create()).toStartWith("wrk_")
38
+ })
39
+
40
+ test("client and Server contracts generate identically", () => {
41
+ const server = compile(Api, { groupNames, endpointNames, omitEndpoints })
42
+ const client = compile(ClientApi, { groupNames, endpointNames, omitEndpoints })
43
+
44
+ expect(emitPromise(client)).toEqual(emitPromise(server))
45
+ })
46
+
47
+ test("shared DTO schemas construct and decode plain objects", () => {
48
+ const made = Prompt.make({ text: "hello" })
49
+ const decoded = Schema.decodeUnknownSync(Prompt)({ text: "hello" })
50
+ const content = Schema.decodeUnknownSync(SessionMessage.AssistantText)({ type: "text", id: "part_1", text: "hi" })
51
+
52
+ expect(Object.getPrototypeOf(made)).toBe(Object.prototype)
53
+ expect(Object.getPrototypeOf(decoded)).toBe(Object.prototype)
54
+ expect(Object.getPrototypeOf(content)).toBe(Object.prototype)
55
+ expect(Prompt.ast.annotations?.identifier).toBe("Prompt")
56
+ expect(SessionMessage.AssistantText.ast.annotations?.identifier).toBe("Session.Message.Assistant.Text")
57
+ expect(CoreSessionMessage.AssistantText).toBe(SessionMessage.AssistantText)
58
+ })
packages/client/test/effect.test.ts ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect, test } from "bun:test"
2
+ import { DateTime, Effect, Stream } from "effect"
3
+ import { HttpClient, HttpClientResponse } from "effect/unstable/http"
4
+ import { AbsolutePath, Agent, Location, Model, OpenCode, Prompt, Session, SessionMessage } from "../src/effect"
5
+
6
+ test("sessions.get returns the decoded Effect projection", async () => {
7
+ const httpClient = HttpClient.make((request) =>
8
+ Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session))),
9
+ )
10
+ const result = await Effect.gen(function* () {
11
+ const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
12
+ return yield* client.sessions.get({ sessionID: Session.ID.make("ses_test") })
13
+ }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
14
+
15
+ expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000)
16
+ })
17
+
18
+ test("events.subscribe exposes and decodes the native Effect event stream", async () => {
19
+ const httpClient = HttpClient.make((request) =>
20
+ Effect.succeed(
21
+ HttpClientResponse.fromWeb(
22
+ request,
23
+ new Response(
24
+ `data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
25
+ `data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
26
+ { headers: { "content-type": "text/event-stream" } },
27
+ ),
28
+ ),
29
+ ),
30
+ )
31
+ const events = await Effect.gen(function* () {
32
+ const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
33
+ return yield* client.events.subscribe().pipe(Stream.runCollect)
34
+ }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
35
+
36
+ expect(Array.from(events).map((event) => event.type)).toEqual(["server.connected", "session.next.model.switched"])
37
+ const durable = events[1]
38
+ if (durable?.type !== "session.next.model.switched") throw new Error("Expected model event")
39
+ expect(DateTime.toEpochMillis(durable.data.timestamp)).toBe(1_717_171_717_000)
40
+ expect(durable.durable).toEqual({ aggregateID: "ses_test", seq: 1, version: 1 })
41
+ })
42
+
43
+ test("events.subscribe terminates on Effect protocol decode failures", async () => {
44
+ const httpClient = HttpClient.make((request) =>
45
+ Effect.succeed(
46
+ HttpClientResponse.fromWeb(
47
+ request,
48
+ new Response(`data: {"type":"server.connected"}\n\n`, {
49
+ headers: { "content-type": "text/event-stream" },
50
+ }),
51
+ ),
52
+ ),
53
+ )
54
+ const error = await Effect.gen(function* () {
55
+ const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
56
+ return yield* client.events.subscribe().pipe(Stream.runCollect, Effect.flip)
57
+ }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
58
+
59
+ expect(error._tag).toBe("ClientError")
60
+ })
61
+
62
+ test("session methods retain decoded Effect inputs and outputs", async () => {
63
+ const historyQueries: Array<Record<string, string>> = []
64
+ let historyPage = 0
65
+ const httpClient = HttpClient.make((request) => {
66
+ const url = request.url
67
+ if (url.includes("/event")) {
68
+ return Effect.succeed(
69
+ HttpClientResponse.fromWeb(
70
+ request,
71
+ new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
72
+ headers: { "content-type": "text/event-stream" },
73
+ }),
74
+ ),
75
+ )
76
+ }
77
+ if (url.includes("/history")) {
78
+ historyPage++
79
+ historyQueries.push(Object.fromEntries(request.urlParams.params))
80
+ return Effect.succeed(
81
+ HttpClientResponse.fromWeb(
82
+ request,
83
+ Response.json(
84
+ historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
85
+ ),
86
+ ),
87
+ )
88
+ }
89
+ if (url.includes("/prompt")) {
90
+ return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(admission)))
91
+ }
92
+ if (url.includes("/context")) {
93
+ return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: [] })))
94
+ }
95
+ if (url.includes("/message/")) {
96
+ return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json({ data: modelSwitchedMessage })))
97
+ }
98
+ if (url.endsWith("/api/session/active")) {
99
+ return Effect.succeed(
100
+ HttpClientResponse.fromWeb(request, Response.json({ data: { ses_test: { type: "running" } } })),
101
+ )
102
+ }
103
+ if (request.method === "POST" && url.endsWith("/api/session")) {
104
+ return Effect.succeed(HttpClientResponse.fromWeb(request, Response.json(session)))
105
+ }
106
+ if (request.method === "POST") {
107
+ return Effect.succeed(HttpClientResponse.fromWeb(request, new Response(null, { status: 204 })))
108
+ }
109
+ return Effect.succeed(
110
+ HttpClientResponse.fromWeb(request, Response.json({ data: [session.data], cursor: { next: "next" } })),
111
+ )
112
+ })
113
+ const result = await Effect.gen(function* () {
114
+ const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
115
+ const page = yield* client.sessions.list({ limit: 10 })
116
+ const active = yield* client.sessions.active()
117
+ const created = yield* client.sessions.create({
118
+ location: Location.Ref.make({ directory: AbsolutePath.make("/tmp/project") }),
119
+ })
120
+ yield* client.sessions.switchAgent({ sessionID: Session.ID.make("ses_test"), agent: Agent.ID.make("build") })
121
+ yield* client.sessions.switchModel({
122
+ sessionID: Session.ID.make("ses_test"),
123
+ model: Model.Ref.make({ id: "claude", providerID: "anthropic" }),
124
+ })
125
+ const admitted = yield* client.sessions.prompt({
126
+ sessionID: Session.ID.make("ses_test"),
127
+ prompt: Prompt.make({ text: "Hello" }),
128
+ resume: false,
129
+ })
130
+ yield* client.sessions.compact({ sessionID: Session.ID.make("ses_test") })
131
+ yield* client.sessions.wait({ sessionID: Session.ID.make("ses_test") })
132
+ const context = yield* client.sessions.context({ sessionID: Session.ID.make("ses_test") })
133
+ const history = yield* client.sessions.history({
134
+ sessionID: Session.ID.make("ses_test"),
135
+ after: 0,
136
+ limit: 1,
137
+ })
138
+ const historyNext = history.hasMore
139
+ ? yield* client.sessions.history({
140
+ sessionID: Session.ID.make("ses_test"),
141
+ after: history.data.at(-1)?.durable?.seq,
142
+ limit: 2,
143
+ })
144
+ : undefined
145
+ const events = yield* client.sessions
146
+ .events({ sessionID: Session.ID.make("ses_test"), after: 0 })
147
+ .pipe(Stream.runCollect)
148
+ yield* client.sessions.interrupt({ sessionID: Session.ID.make("ses_test") })
149
+ const message = yield* client.sessions.message({
150
+ sessionID: Session.ID.make("ses_test"),
151
+ messageID: SessionMessage.ID.make("msg_model"),
152
+ })
153
+ return { page, active, created, admitted, context, history, historyNext, events, message }
154
+ }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
155
+
156
+ expect(DateTime.toEpochMillis(result.page.data[0].time.created)).toBe(1_717_171_717_000)
157
+ expect(result.active).toEqual({ ses_test: { type: "running" } })
158
+ expect(Object.getPrototypeOf(result.page.data[0])).toBe(Object.prototype)
159
+ expect(Object.getPrototypeOf(result.created)).toBe(Object.prototype)
160
+ expect(result.created.id).toBe("ses_test")
161
+ expect(Object.getPrototypeOf(result.admitted)).toBe(Object.prototype)
162
+ expect(Object.getPrototypeOf(result.admitted.prompt)).toBe(Object.prototype)
163
+ expect(DateTime.toEpochMillis(result.admitted.timeCreated)).toBe(1_717_171_717_000)
164
+ expect(result.context).toEqual([])
165
+ expect(DateTime.toEpochMillis(result.history.data[0].data.timestamp)).toBe(1_717_171_717_000)
166
+ expect(result.history).toEqual(expect.objectContaining({ hasMore: true }))
167
+ expect(result.historyNext).toEqual({ data: [], hasMore: false })
168
+ expect(historyQueries[0]).toEqual({ limit: "1", after: "0" })
169
+ expect(historyQueries[1]).toEqual({ limit: "2", after: "1" })
170
+ expect(DateTime.toEpochMillis(result.events[0].data.timestamp)).toBe(1_717_171_717_000)
171
+ expect(result.message).toEqual(expect.objectContaining({ id: "msg_model", type: "model-switched" }))
172
+ })
173
+
174
+ test("sessions.history retains the typed SessionNotFoundError", async () => {
175
+ const httpClient = HttpClient.make((request) =>
176
+ Effect.succeed(
177
+ HttpClientResponse.fromWeb(
178
+ request,
179
+ Response.json(
180
+ { _tag: "SessionNotFoundError", sessionID: "ses_missing", message: "Session not found" },
181
+ { status: 404 },
182
+ ),
183
+ ),
184
+ ),
185
+ )
186
+ const error = await Effect.gen(function* () {
187
+ const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" })
188
+ return yield* client.sessions
189
+ .history({
190
+ sessionID: Session.ID.make("ses_missing"),
191
+ })
192
+ .pipe(Effect.flip)
193
+ }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise)
194
+
195
+ expect(error._tag).toBe("SessionNotFoundError")
196
+ })
197
+
198
+ const session = {
199
+ data: {
200
+ id: "ses_test",
201
+ projectID: "project",
202
+ cost: 0,
203
+ tokens: {
204
+ input: 1,
205
+ output: 2,
206
+ reasoning: 3,
207
+ cache: { read: 4, write: 5 },
208
+ },
209
+ time: {
210
+ created: 1_717_171_717_000,
211
+ updated: 1_717_171_717_000,
212
+ },
213
+ title: "Test",
214
+ location: { directory: "/tmp/project" },
215
+ },
216
+ }
217
+
218
+ const admission = {
219
+ data: {
220
+ admittedSeq: 0,
221
+ id: "msg_test",
222
+ sessionID: "ses_test",
223
+ prompt: { text: "Hello" },
224
+ delivery: "steer",
225
+ timeCreated: 1_717_171_717_000,
226
+ },
227
+ }
228
+
229
+ const modelSwitchedMessage = {
230
+ id: "msg_model",
231
+ type: "model-switched",
232
+ time: { created: 1_717_171_717_000 },
233
+ model: { id: "claude", providerID: "anthropic" },
234
+ }
235
+
236
+ const modelSwitchedEvent = {
237
+ id: "evt_model",
238
+ type: "session.next.model.switched",
239
+ durable: { aggregateID: "ses_test", seq: 1, version: 1 },
240
+ data: {
241
+ timestamp: 1_717_171_717_000,
242
+ sessionID: "ses_test",
243
+ messageID: "msg_model",
244
+ model: { id: "claude", providerID: "anthropic" },
245
+ },
246
+ }
packages/client/test/import-boundaries.test.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, expect, test } from "bun:test"
2
+ import { realpathSync } from "node:fs"
3
+ import { mkdtemp, rm } from "node:fs/promises"
4
+ import { join, resolve, sep } from "node:path"
5
+
6
+ const directory = resolve(import.meta.dir, "..")
7
+ const effect = realpathSync(resolve(import.meta.dir, "../node_modules/effect"))
8
+ const schema = resolve(import.meta.dir, "../../schema")
9
+ const protocol = resolve(import.meta.dir, "../../protocol")
10
+ const core = resolve(import.meta.dir, "../../core")
11
+ const server = resolve(import.meta.dir, "../../server")
12
+
13
+ describe("public import boundaries", () => {
14
+ test("isolates each public entrypoint", async () => {
15
+ const root = await bundleInputs("@opencode-ai/client", "browser")
16
+
17
+ expect(within(root, effect)).toEqual([])
18
+ expect(within(root, schema)).toEqual([])
19
+ expect(within(root, protocol)).toEqual([])
20
+ expect(within(root, core)).toEqual([])
21
+ expect(within(root, server)).toEqual([])
22
+
23
+ const network = await bundleInputs("@opencode-ai/client/effect", "browser")
24
+
25
+ expect(within(network, effect).length).toBeGreaterThan(0)
26
+ expect(within(network, schema).length).toBeGreaterThan(0)
27
+ expect(within(network, protocol).length).toBeGreaterThan(0)
28
+ expect(within(network, core)).toEqual([])
29
+ expect(within(network, server)).toEqual([])
30
+ })
31
+ })
32
+
33
+ async function bundleInputs(specifier: string, target: "browser" | "bun") {
34
+ const temporary = await mkdtemp(join(import.meta.dir, ".import-boundary-"))
35
+ const entrypoint = join(temporary, "index.ts")
36
+ const metafile = join(temporary, "meta.json")
37
+ try {
38
+ await Bun.write(entrypoint, `export * from ${JSON.stringify(specifier)}`)
39
+ const child = Bun.spawn(
40
+ [
41
+ process.execPath,
42
+ "build",
43
+ entrypoint,
44
+ `--target=${target}`,
45
+ "--format=esm",
46
+ "--packages=bundle",
47
+ `--metafile=${metafile}`,
48
+ `--outdir=${join(temporary, "out")}`,
49
+ ],
50
+ { cwd: directory, stdout: "pipe", stderr: "pipe" },
51
+ )
52
+ const [exitCode, stdout, stderr] = await Promise.all([
53
+ child.exited,
54
+ new Response(child.stdout).text(),
55
+ new Response(child.stderr).text(),
56
+ ])
57
+ if (exitCode !== 0) throw new Error(stdout + stderr)
58
+ const metadata = await Bun.file(metafile).json()
59
+ return Object.keys(metadata.inputs).map((input) => resolve(directory, input))
60
+ } finally {
61
+ await rm(temporary, { recursive: true, force: true })
62
+ }
63
+ }
64
+
65
+ function within(inputs: ReadonlyArray<string>, directory: string) {
66
+ const prefix = directory.endsWith(sep) ? directory : directory + sep
67
+ return inputs.filter((input) => input === directory || input.startsWith(prefix))
68
+ }
packages/client/test/promise.test.ts ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { expect, test } from "bun:test"
2
+ import { isSessionNotFoundError, isUnauthorizedError, OpenCode } from "../src"
3
+
4
+ test("exposes every standard HTTP API group", () => {
5
+ const client = OpenCode.make({ baseUrl: "http://localhost:3000" })
6
+
7
+ expect(Object.keys(client)).toEqual([
8
+ "health",
9
+ "location",
10
+ "agents",
11
+ "sessions",
12
+ "messages",
13
+ "models",
14
+ "providers",
15
+ "integrations",
16
+ "credentials",
17
+ "permissions",
18
+ "files",
19
+ "commands",
20
+ "skills",
21
+ "events",
22
+ "ptys",
23
+ "questions",
24
+ "references",
25
+ "projectCopies",
26
+ ])
27
+ expect(Object.keys(client.messages)).toEqual(["list"])
28
+ expect(Object.keys(client.integrations)).toEqual([
29
+ "list",
30
+ "get",
31
+ "connectKey",
32
+ "connectOauth",
33
+ "attemptStatus",
34
+ "attemptComplete",
35
+ "attemptCancel",
36
+ ])
37
+ expect(Object.keys(client.files)).toEqual(["list", "find"])
38
+ expect(Object.keys(client.ptys)).toEqual(["list", "create", "get", "update", "remove"])
39
+ })
40
+
41
+ test("sessions.get returns the wire projection", async () => {
42
+ const client = OpenCode.make({
43
+ baseUrl: "http://localhost:3000",
44
+ fetch: async (input) => {
45
+ expect(typeof input === "string" ? input : input instanceof URL ? input.href : input.url).toBe(
46
+ "http://localhost:3000/api/session/ses_test",
47
+ )
48
+ return Response.json(session)
49
+ },
50
+ })
51
+
52
+ const result = await client.sessions.get({ sessionID: "ses_test" })
53
+
54
+ expect(result.time.created).toBe(1_717_171_717_000)
55
+ })
56
+
57
+ test("events.subscribe exposes the Promise event stream wire projection", async () => {
58
+ const client = OpenCode.make({
59
+ baseUrl: "http://localhost:3000",
60
+ fetch: async () =>
61
+ new Response(
62
+ `: heartbeat\n\ndata: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n` +
63
+ `data: ${JSON.stringify(modelSwitchedEvent)}\n\n`,
64
+ { headers: { "content-type": "text/event-stream" } },
65
+ ),
66
+ })
67
+ const events = []
68
+ for await (const event of client.events.subscribe()) events.push(event)
69
+
70
+ expect(events).toEqual([{ id: "evt_connected", type: "server.connected", data: {} }, modelSwitchedEvent])
71
+ expect(events[1]?.type === "session.next.model.switched" && events[1].data.timestamp).toBe(1_717_171_717_000)
72
+ })
73
+
74
+ test("events.subscribe terminates on malformed Promise SSE data", async () => {
75
+ const client = OpenCode.make({
76
+ baseUrl: "http://localhost:3000",
77
+ fetch: async () => new Response("data: {not-json}\n\n", { headers: { "content-type": "text/event-stream" } }),
78
+ })
79
+
80
+ await expect(client.events.subscribe()[Symbol.asyncIterator]().next()).rejects.toMatchObject({
81
+ name: "ClientError",
82
+ reason: "MalformedResponse",
83
+ })
84
+ })
85
+
86
+ test("session methods use the public HTTP contract", async () => {
87
+ const requests: Array<{ url: string; init?: RequestInit }> = []
88
+ let historyPage = 0
89
+ const client = OpenCode.make({
90
+ baseUrl: "http://localhost:3000",
91
+ fetch: async (input, init) => {
92
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
93
+ requests.push({ url, init })
94
+ if (url.includes("/event")) {
95
+ return new Response(`data: ${JSON.stringify(modelSwitchedEvent)}\n\n`, {
96
+ headers: { "content-type": "text/event-stream" },
97
+ })
98
+ }
99
+ if (url.includes("/history")) {
100
+ historyPage++
101
+ return Response.json(
102
+ historyPage === 1 ? { data: [modelSwitchedEvent], hasMore: true } : { data: [], hasMore: false },
103
+ )
104
+ }
105
+ if (url.includes("/prompt")) return Response.json(admission)
106
+ if (url.includes("/context")) return Response.json({ data: [] })
107
+ if (url.includes("/message/")) return Response.json({ data: modelSwitchedMessage })
108
+ if (url.endsWith("/api/session/active")) return Response.json({ data: { ses_test: { type: "running" } } })
109
+ if (init?.method === "POST" && url.endsWith("/api/session")) return Response.json(session)
110
+ if (init?.method === "POST") return new Response(null, { status: 204 })
111
+ return Response.json({ data: [session.data], cursor: { next: "next" } })
112
+ },
113
+ })
114
+
115
+ const page = await client.sessions.list({ limit: 10, order: "desc" })
116
+ const active = await client.sessions.active()
117
+ const created = await client.sessions.create({ location: { directory: "/tmp/project" } })
118
+ await client.sessions.switchAgent({ sessionID: "ses_test", agent: "build" })
119
+ await client.sessions.switchModel({
120
+ sessionID: "ses_test",
121
+ model: { id: "claude", providerID: "anthropic" },
122
+ })
123
+ const admitted = await client.sessions.prompt({
124
+ sessionID: "ses_test",
125
+ prompt: { text: "Hello" },
126
+ resume: false,
127
+ })
128
+ await client.sessions.compact({ sessionID: "ses_test" })
129
+ await client.sessions.wait({ sessionID: "ses_test" })
130
+ const context = await client.sessions.context({ sessionID: "ses_test" })
131
+ const history = await client.sessions.history({ sessionID: "ses_test", after: 0, limit: 1 })
132
+ const historyAfter = history.data.at(-1)?.durable?.seq
133
+ const historyNext = history.hasMore
134
+ ? await client.sessions.history({ sessionID: "ses_test", after: historyAfter, limit: 2 })
135
+ : undefined
136
+ const events = []
137
+ for await (const event of client.sessions.events({ sessionID: "ses_test", after: 0 })) events.push(event)
138
+ await client.sessions.interrupt({ sessionID: "ses_test" })
139
+ const message = await client.sessions.message({ sessionID: "ses_test", messageID: "msg_model" })
140
+
141
+ expect(page.cursor.next).toBe("next")
142
+ expect(active).toEqual({ ses_test: { type: "running" } })
143
+ expect(created.id).toBe("ses_test")
144
+ expect(admitted.id).toBe("msg_test")
145
+ expect(context).toEqual([])
146
+ expect(history).toEqual({ data: [modelSwitchedEvent], hasMore: true })
147
+ expect(historyNext).toEqual({ data: [], hasMore: false })
148
+ expect(events).toEqual([modelSwitchedEvent])
149
+ expect(message).toEqual(modelSwitchedMessage)
150
+ expect(requests.map((request) => [request.init?.method, request.url])).toEqual([
151
+ ["GET", "http://localhost:3000/api/session?limit=10&order=desc"],
152
+ ["GET", "http://localhost:3000/api/session/active"],
153
+ ["POST", "http://localhost:3000/api/session"],
154
+ ["POST", "http://localhost:3000/api/session/ses_test/agent"],
155
+ ["POST", "http://localhost:3000/api/session/ses_test/model"],
156
+ ["POST", "http://localhost:3000/api/session/ses_test/prompt"],
157
+ ["POST", "http://localhost:3000/api/session/ses_test/compact"],
158
+ ["POST", "http://localhost:3000/api/session/ses_test/wait"],
159
+ ["GET", "http://localhost:3000/api/session/ses_test/context"],
160
+ ["GET", "http://localhost:3000/api/session/ses_test/history?limit=1&after=0"],
161
+ ["GET", "http://localhost:3000/api/session/ses_test/history?limit=2&after=1"],
162
+ ["GET", "http://localhost:3000/api/session/ses_test/event?after=0"],
163
+ ["POST", "http://localhost:3000/api/session/ses_test/interrupt"],
164
+ ["GET", "http://localhost:3000/api/session/ses_test/message/msg_model"],
165
+ ])
166
+ const body = requests.find((request) => request.url.endsWith("/api/session/ses_test/prompt"))?.init?.body
167
+ if (typeof body !== "string") throw new Error("Expected JSON request body")
168
+ expect(JSON.parse(body)).toEqual({
169
+ prompt: { text: "Hello" },
170
+ resume: false,
171
+ })
172
+ })
173
+
174
+ test("middleware errors remain declared client errors", async () => {
175
+ const client = OpenCode.make({
176
+ baseUrl: "http://localhost:3000",
177
+ fetch: async () =>
178
+ Response.json({ _tag: "UnauthorizedError", message: "Authentication required" }, { status: 401 }),
179
+ })
180
+
181
+ try {
182
+ await client.sessions.create({})
183
+ throw new Error("Expected request to fail")
184
+ } catch (error) {
185
+ expect(isUnauthorizedError(error)).toBe(true)
186
+ }
187
+ })
188
+
189
+ test("sessions.history decodes SessionNotFoundError", async () => {
190
+ const client = OpenCode.make({
191
+ baseUrl: "http://localhost:3000",
192
+ fetch: async () =>
193
+ Response.json(
194
+ { _tag: "SessionNotFoundError", sessionID: "ses_missing", message: "Session not found" },
195
+ { status: 404 },
196
+ ),
197
+ })
198
+
199
+ try {
200
+ await client.sessions.history({ sessionID: "ses_missing" })
201
+ throw new Error("Expected request to fail")
202
+ } catch (error) {
203
+ expect(isSessionNotFoundError(error)).toBe(true)
204
+ }
205
+ })
206
+
207
+ const session = {
208
+ data: {
209
+ id: "ses_test",
210
+ projectID: "project",
211
+ cost: 0,
212
+ tokens: {
213
+ input: 1,
214
+ output: 2,
215
+ reasoning: 3,
216
+ cache: { read: 4, write: 5 },
217
+ },
218
+ time: {
219
+ created: 1_717_171_717_000,
220
+ updated: 1_717_171_717_000,
221
+ },
222
+ title: "Test",
223
+ location: { directory: "/tmp/project" },
224
+ },
225
+ }
226
+
227
+ const admission = {
228
+ data: {
229
+ admittedSeq: 0,
230
+ id: "msg_test",
231
+ sessionID: "ses_test",
232
+ prompt: { text: "Hello" },
233
+ delivery: "steer",
234
+ timeCreated: 1_717_171_717_000,
235
+ },
236
+ }
237
+
238
+ const modelSwitchedMessage = {
239
+ id: "msg_model",
240
+ type: "model-switched",
241
+ time: { created: 1_717_171_717_000 },
242
+ model: { id: "claude", providerID: "anthropic" },
243
+ }
244
+
245
+ const modelSwitchedEvent = {
246
+ id: "evt_model",
247
+ type: "session.next.model.switched",
248
+ durable: { aggregateID: "ses_test", seq: 1, version: 1 },
249
+ data: {
250
+ timestamp: 1_717_171_717_000,
251
+ sessionID: "ses_test",
252
+ messageID: "msg_model",
253
+ model: { id: "claude", providerID: "anthropic" },
254
+ },
255
+ }
packages/codemode/src/codemode.ts ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Effect, Schema } from "effect"
2
+ import { executeWithLimits } from "./interpreter/runtime.js"
3
+ import { type HostTools, type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js"
4
+ import type { Definition } from "./tool.js"
5
+
6
+ /** A tool call admitted during an execution. */
7
+ export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js"
8
+
9
+ /** Resource budgets enforced independently during each CodeMode program execution. */
10
+ export type ExecutionLimits = {
11
+ /** Maximum wall-clock execution time in milliseconds. No default: absent means no timeout. */
12
+ readonly timeoutMs?: number
13
+ /** Maximum number of tool calls admitted by the runtime. No default: absent means unlimited. */
14
+ readonly maxToolCalls?: number
15
+ /** Maximum UTF-8 bytes of model-facing output. No default: absent means no truncation. */
16
+ readonly maxOutputBytes?: number
17
+ }
18
+
19
+ /** Controls how much of the tool catalog is inlined in agent instructions. */
20
+ export type DiscoveryOptions = {
21
+ /** Approximate token budget (chars/4, default 2000) for full catalog entries. */
22
+ readonly catalogBudget?: number
23
+ }
24
+
25
+ type ToolTree<R = never> = {
26
+ readonly [name: string]: Definition<R> | ToolTree<R>
27
+ }
28
+
29
+ export type ResolvedExecutionLimits = {
30
+ readonly timeoutMs: number | undefined
31
+ readonly maxToolCalls: number | undefined
32
+ readonly maxOutputBytes: number | undefined
33
+ }
34
+
35
+ /** Options for one CodeMode execution. */
36
+ export type ExecuteOptions<Tools extends Record<string, unknown> = {}> = {
37
+ /** Source for one program in the supported JavaScript subset. */
38
+ code: string
39
+ /** Explicit tool tree exposed to the program as `tools`. */
40
+ tools?: Tools & ToolTree<Services<Tools>>
41
+ /** Per-execution overrides for the default resource limits. */
42
+ limits?: ExecutionLimits
43
+ /** Observes decoded tool input immediately before tool execution. */
44
+ onToolCallStart?: (call: ToolRuntime.ToolCallStarted) => Effect.Effect<void, never, Services<Tools>>
45
+ /** Observes each admitted tool call as it settles, with outcome and duration. */
46
+ onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect<void, never, Services<Tools>>
47
+ }
48
+
49
+ /** A JSON value that can cross the confined interpreter boundary. */
50
+ export type DataValue = Schema.Json
51
+
52
+ /** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */
53
+ export type Options<Tools extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Tools>, "code"> & {
54
+ /** Progressive-disclosure configuration for the agent-facing tool catalog. */
55
+ readonly discovery?: DiscoveryOptions
56
+ }
57
+
58
+ /** Schema for a host tool input containing CodeMode source. */
59
+ export const Input = Schema.Struct({ code: Schema.String })
60
+ export type Input = typeof Input.Type
61
+
62
+ export const DiagnosticKind = Schema.Literals([
63
+ "ParseError",
64
+ "UnsupportedSyntax",
65
+ "UnknownTool",
66
+ "InvalidToolInput",
67
+ "InvalidToolOutput",
68
+ "InvalidDataValue",
69
+ "ToolCallLimitExceeded",
70
+ "TimeoutExceeded",
71
+ "ToolFailure",
72
+ "ExecutionFailure",
73
+ ])
74
+ /** Stable categories produced by program, schema, tool, and limit failures. */
75
+ export type DiagnosticKind = typeof DiagnosticKind.Type
76
+
77
+ export const Diagnostic = Schema.Struct({
78
+ kind: DiagnosticKind,
79
+ message: Schema.String,
80
+ location: Schema.optionalKey(Schema.Struct({ line: Schema.Number, column: Schema.Number })),
81
+ suggestions: Schema.optionalKey(Schema.Array(Schema.String)),
82
+ })
83
+ /** A normalized program diagnostic safe to return across an agent tool boundary. */
84
+ export type Diagnostic = typeof Diagnostic.Type
85
+
86
+ const ToolCallSchema = Schema.Struct({ name: Schema.String })
87
+ export const Success = Schema.Struct({
88
+ ok: Schema.Literal(true),
89
+ value: Schema.Json,
90
+ logs: Schema.optionalKey(Schema.Array(Schema.String)),
91
+ truncated: Schema.optionalKey(Schema.Boolean),
92
+ toolCalls: Schema.Array(ToolCallSchema),
93
+ })
94
+ /** Successful execution after the result has crossed the plain-data boundary. */
95
+ export type Success = typeof Success.Type
96
+
97
+ export const Failure = Schema.Struct({
98
+ ok: Schema.Literal(false),
99
+ error: Diagnostic,
100
+ logs: Schema.optionalKey(Schema.Array(Schema.String)),
101
+ truncated: Schema.optionalKey(Schema.Boolean),
102
+ toolCalls: Schema.Array(ToolCallSchema),
103
+ })
104
+ /** Failed execution with calls admitted before the diagnostic was produced. */
105
+ export type Failure = typeof Failure.Type
106
+
107
+ /** Schema for the structured success or diagnostic returned by CodeMode execution. */
108
+ export const Result = Schema.Union([Success, Failure])
109
+ /** Result of executing a CodeMode program. Program failures are data, not Effect failures. */
110
+ export type Result = typeof Result.Type
111
+
112
+ /** Reusable confined runtime over one explicit tool tree. */
113
+ export type Runtime<R = never> = {
114
+ readonly catalog: () => ReadonlyArray<ToolDescription>
115
+ readonly instructions: () => string
116
+ readonly execute: (code: string) => Effect.Effect<Result, never, R>
117
+ }
118
+
119
+ const validateLimit = <Value extends number | undefined>(
120
+ name: keyof ExecutionLimits,
121
+ value: Value,
122
+ minimum: number,
123
+ ): Value => {
124
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < minimum)) {
125
+ throw new RangeError(`${name} must be a safe integer greater than or equal to ${minimum}.`)
126
+ }
127
+ return value
128
+ }
129
+
130
+ const resolveExecutionLimits = (limits?: ExecutionLimits): ResolvedExecutionLimits => ({
131
+ timeoutMs: validateLimit("timeoutMs", limits?.timeoutMs, 1),
132
+ maxToolCalls: validateLimit("maxToolCalls", limits?.maxToolCalls, 0),
133
+ maxOutputBytes: validateLimit("maxOutputBytes", limits?.maxOutputBytes, 0),
134
+ })
135
+
136
+ /** Executes one Effect-native CodeMode program without constructing a reusable runtime. */
137
+ export const execute = <const Tools extends Record<string, unknown>>(
138
+ options: ExecuteOptions<Tools>,
139
+ ): Effect.Effect<Result, never, Services<Tools>> => {
140
+ const tools = (options.tools ?? {}) as HostTools<Services<Tools>>
141
+ ToolRuntime.assertValidTools(tools)
142
+ return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools))
143
+ }
144
+
145
+ /** Creates an Effect-native runtime over explicit, schema-described tools. */
146
+ export const make = <const Tools extends Record<string, unknown> = {}>(
147
+ options: Options<Tools> = {} as Options<Tools>,
148
+ ): Runtime<Services<Tools>> => {
149
+ const tools = (options.tools ?? {}) as HostTools<Services<Tools>>
150
+ ToolRuntime.assertValidTools(tools)
151
+ const limits = resolveExecutionLimits(options.limits)
152
+ const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget)
153
+
154
+ return {
155
+ catalog: () => prepared.catalog,
156
+ instructions: () => prepared.instructions,
157
+ execute: (code) => executeWithLimits<Tools>({ ...options, code }, limits, prepared.searchIndex),
158
+ }
159
+ }
packages/codemode/src/index.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export * as CodeMode from "./codemode.js"
2
+ export * as Tool from "./tool.js"
3
+ export * as OpenAPI from "./openapi/index.js"
4
+ export { ToolError, toolError } from "./tool-error.js"
packages/codemode/src/interpreter/model.ts ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { SafeObject } from "../tool-runtime.js"
2
+ import type { SandboxURL } from "../values.js"
3
+
4
+ export type SourcePosition = {
5
+ line: number
6
+ column: number
7
+ }
8
+
9
+ export type SourceLocation = {
10
+ start: SourcePosition
11
+ end: SourcePosition
12
+ }
13
+
14
+ export type AstNode = {
15
+ type: string
16
+ loc?: SourceLocation
17
+ [key: string]: unknown
18
+ }
19
+
20
+ export type ProgramNode = AstNode & {
21
+ type: "Program"
22
+ body: Array<AstNode>
23
+ }
24
+
25
+ export type Binding = {
26
+ mutable: boolean
27
+ value: unknown
28
+ initialized?: boolean
29
+ }
30
+
31
+ export type StatementResult =
32
+ | { kind: "none" }
33
+ | { kind: "value"; value: unknown }
34
+ | { kind: "return"; value: unknown }
35
+ | { kind: "break" }
36
+ | { kind: "continue" }
37
+
38
+ export type MemberReference = {
39
+ target: SafeObject | Array<unknown> | SandboxURL
40
+ key: string | number
41
+ }
42
+
43
+ export class CodeModeFunction {
44
+ constructor(
45
+ readonly parameters: ReadonlyArray<AstNode>,
46
+ readonly body: AstNode,
47
+ readonly capturedScopes: ReadonlyArray<Map<string, Binding>>,
48
+ ) {}
49
+ }
50
+
51
+ export class IntrinsicReference {
52
+ constructor(
53
+ readonly receiver: unknown,
54
+ readonly name: string,
55
+ ) {}
56
+ }
57
+
58
+ export class ComputedValue {
59
+ constructor(readonly value: unknown) {}
60
+ }
61
+
62
+ export class PromiseNamespace {}
63
+
64
+ export type PromiseMethodName = "all" | "allSettled" | "race" | "resolve" | "reject"
65
+
66
+ export class PromiseMethodReference {
67
+ constructor(readonly name: PromiseMethodName) {}
68
+ }
69
+
70
+ export type GlobalNamespaceName =
71
+ | "Object"
72
+ | "Math"
73
+ | "JSON"
74
+ | "Array"
75
+ | "console"
76
+ | "Date"
77
+ | "RegExp"
78
+ | "Map"
79
+ | "Set"
80
+ | "URL"
81
+ | "URLSearchParams"
82
+
83
+ export class GlobalNamespace {
84
+ constructor(readonly name: GlobalNamespaceName) {}
85
+ }
86
+
87
+ export class GlobalMethodReference {
88
+ constructor(
89
+ readonly namespace: GlobalNamespaceName | "Number" | "String",
90
+ readonly name: string,
91
+ ) {}
92
+ }
93
+
94
+ export class CoercionFunction {
95
+ constructor(readonly name: "Number" | "String" | "Boolean" | "parseInt" | "parseFloat") {}
96
+ }
97
+
98
+ export class UriFunction {
99
+ constructor(readonly name: "encodeURI" | "encodeURIComponent" | "decodeURI" | "decodeURIComponent") {}
100
+ }
101
+
102
+ export class ProgramThrow {
103
+ constructor(readonly value: unknown) {}
104
+ }
105
+
106
+ export class ErrorConstructorReference {
107
+ constructor(readonly name: string) {}
108
+ }
109
+
110
+ export type DiagnosticKind =
111
+ | "ParseError"
112
+ | "UnsupportedSyntax"
113
+ | "UnknownTool"
114
+ | "InvalidToolInput"
115
+ | "InvalidToolOutput"
116
+ | "InvalidDataValue"
117
+ | "ToolCallLimitExceeded"
118
+ | "TimeoutExceeded"
119
+ | "ToolFailure"
120
+ | "ExecutionFailure"
121
+
122
+ export const OptionalShortCircuit: unique symbol = Symbol("codemode.optional-short-circuit")
123
+
124
+ export const supportedSyntaxMessage =
125
+ "Supported orchestration syntax: tools.* calls (they return promises - resolve them with await), data literals, destructuring, optional chaining, template literals, conditionals, switch, loops (incl. for...of and for...in over object/array/tools keys), arrow functions, spread, try/catch, array methods (map/filter/find/findIndex/some/every/reduce/flatMap/forEach/sort/slice/concat/indexOf/lastIndexOf/at/flat/reverse/includes/join), string methods (incl. match/matchAll/replace/split with regular expressions), Date/RegExp/Map/Set/URL/URLSearchParams, URI encoding helpers, Object/Math/JSON helpers, captured console.log/warn/error/dir/table, and Promise.all/allSettled/race/resolve/reject over arrays mixing promises and plain values for parallel tool calls (promise chaining with .then/.catch is not supported - use await with try/catch)."
126
+
127
+ export class InterpreterRuntimeError extends Error {
128
+ readonly node?: AstNode
129
+ errorName: string = "Error"
130
+
131
+ constructor(
132
+ message: string,
133
+ node?: AstNode,
134
+ readonly kind: DiagnosticKind = "ExecutionFailure",
135
+ readonly suggestions?: ReadonlyArray<string>,
136
+ ) {
137
+ super(message)
138
+ this.name = "InterpreterRuntimeError"
139
+ if (node) this.node = node
140
+ }
141
+
142
+ as(errorName: string): this {
143
+ this.errorName = errorName
144
+ return this
145
+ }
146
+ }
147
+
148
+ export const unsupportedSyntax = (kind: string, node: AstNode): InterpreterRuntimeError =>
149
+ new InterpreterRuntimeError(
150
+ `Syntax '${kind}' is not supported in CodeMode. ${supportedSyntaxMessage}`,
151
+ node,
152
+ "UnsupportedSyntax",
153
+ [supportedSyntaxMessage],
154
+ )
155
+
156
+ export const isRecord = (value: unknown): value is Record<string, unknown> =>
157
+ typeof value === "object" && value !== null
158
+
159
+ export const asNode = (value: unknown, context: string): AstNode => {
160
+ if (!isRecord(value) || typeof value.type !== "string") {
161
+ throw new InterpreterRuntimeError(`Invalid AST node while reading ${context}.`)
162
+ }
163
+ return value as AstNode
164
+ }
165
+
166
+ export const getArray = (node: AstNode, key: string): Array<unknown> => {
167
+ const value = node[key]
168
+ if (!Array.isArray(value)) throw new InterpreterRuntimeError(`Expected '${key}' to be an array.`, node)
169
+ return value
170
+ }
171
+
172
+ export const getString = (node: AstNode, key: string): string => {
173
+ const value = node[key]
174
+ if (typeof value !== "string") throw new InterpreterRuntimeError(`Expected '${key}' to be a string.`, node)
175
+ return value
176
+ }
177
+
178
+ export const getBoolean = (node: AstNode, key: string): boolean => {
179
+ const value = node[key]
180
+ if (typeof value !== "boolean") throw new InterpreterRuntimeError(`Expected '${key}' to be a boolean.`, node)
181
+ return value
182
+ }
183
+
184
+ export const getOptionalNode = (node: AstNode, key: string): AstNode | undefined => {
185
+ const value = node[key]
186
+ if (value === undefined || value === null) return undefined
187
+ return asNode(value, key)
188
+ }
189
+
190
+ export const getNode = (node: AstNode, key: string): AstNode => asNode(node[key], key)
191
+
192
+ export const sourceLocation = (node: AstNode): { readonly line: number; readonly column: number } => ({
193
+ line: Math.max(1, (node.loc?.start.line ?? 2) - 1),
194
+ column: Math.max(1, (node.loc?.start.column ?? 4) - 3),
195
+ })
196
+
197
+ export const formatLocation = (node?: AstNode): string => {
198
+ if (!node?.loc) return ""
199
+ const location = sourceLocation(node)
200
+ return ` (line ${location.line}, col ${location.column})`
201
+ }
packages/codemode/src/interpreter/runtime.ts ADDED
The diff for this file is too large to render. See raw diff
 
packages/codemode/src/openapi/TODO.md ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenAPI Follow-ups
2
+
3
+ The initial adapter intentionally skips operations it cannot execute correctly. Future work may add:
4
+
5
+ - Cookie parameters, authentication, and cookie-header merging.
6
+ - Matrix, label, space-delimited, pipe-delimited, `allowReserved`, and parameter `content` serialization.
7
+ - External references and complete nested `$defs` support.
8
+ - Relative or templated server URLs and server variables.
9
+ - Base URLs containing query strings or fragments.
10
+ - Runtime response-schema validation and full content negotiation.
11
+ - Binary response values and explicit byte-oriented return types.
12
+ - Request/response projection for `readOnly` and `writeOnly` properties.
13
+ - SSE, WebSocket, and other streaming transports.
14
+ - Recovery of responses rejected by a status-filtering `HttpClient`.
15
+ - Configurable request and response size limits.
16
+ - Adapter-enforced redirect policy independent of the supplied `HttpClient`.
17
+ - Strict UTF-8 and empty-body validation for JSON responses.
18
+ - Compile-time rejection of parameter schemas with nested values unsupported by their serialization style; runtime rejects them before auth resolution.
19
+ - Complete malformed-security-scheme validation and broader auth-combination coverage.
packages/codemode/src/openapi/index.ts ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HttpClient } from "effect/unstable/http"
2
+ import { make, type Definition } from "../tool.js"
3
+ import { invoke } from "./runtime.js"
4
+ import {
5
+ componentDefinitions,
6
+ inputSchema,
7
+ isRecord,
8
+ methods,
9
+ nonEmptyString,
10
+ operationInput,
11
+ operationOutput,
12
+ operationPath,
13
+ operationSecurityRequirements,
14
+ securityRequirements,
15
+ securitySchemes,
16
+ specServerUrl,
17
+ validateBaseUrl,
18
+ } from "./spec.js"
19
+ import type { Operation, Options, Result, Skipped, Tools } from "./types.js"
20
+
21
+ export type {
22
+ AuthResolver,
23
+ Credential,
24
+ Document,
25
+ Operation,
26
+ Options,
27
+ Result,
28
+ SecurityScheme,
29
+ Skipped,
30
+ Tools,
31
+ } from "./types.js"
32
+
33
+ /**
34
+ * Builds a CodeMode tool subtree from an OpenAPI 3.x document, one tool per
35
+ * operation. Auth is resolved host-side via `auth.resolve` and never
36
+ * model-visible. Tools require `HttpClient.HttpClient`; unrepresentable
37
+ * operations land in `skipped`.
38
+ */
39
+ export const fromSpec = (options: Options): Result => {
40
+ const document = options.spec
41
+ const schemes = securitySchemes(document)
42
+ const defaultSecurity = securityRequirements(document.security)
43
+ const definitions = componentDefinitions(document)
44
+ const paths = isRecord(document.paths) ? document.paths : {}
45
+ const used = new Set<string>()
46
+ const namespaces = new Set<string>()
47
+ const skipped: Array<Skipped> = []
48
+ const tools = Object.create(null) as Tools
49
+
50
+ for (const [path, pathValue] of Object.entries(paths)) {
51
+ if (!isRecord(pathValue)) continue
52
+ for (const [method, operationValue] of Object.entries(pathValue)) {
53
+ if (!methods.has(method) || !isRecord(operationValue)) continue
54
+ const segments = operationPath(method, path, operationValue, used, namespaces)
55
+ const operation: Operation = {
56
+ operationId: nonEmptyString(operationValue.operationId),
57
+ method: method.toUpperCase(),
58
+ path,
59
+ summary: nonEmptyString(operationValue.summary),
60
+ description: nonEmptyString(operationValue.description),
61
+ }
62
+ const output = operationOutput(document, operationValue, definitions)
63
+ if (!output.ok) {
64
+ skipped.push({ method: operation.method, path, reason: output.reason })
65
+ continue
66
+ }
67
+
68
+ const resolvedBaseUrl = (() => {
69
+ if (options.baseUrl !== undefined) return validateBaseUrl(options.baseUrl)
70
+ if (operationValue.servers !== undefined) return specServerUrl(operationValue)
71
+ if (pathValue.servers !== undefined) return specServerUrl(pathValue)
72
+ return specServerUrl(document)
73
+ })()
74
+ if (!resolvedBaseUrl.ok) {
75
+ skipped.push({ method: operation.method, path, reason: resolvedBaseUrl.reason })
76
+ continue
77
+ }
78
+ const parsedInput = operationInput(document, pathValue, operationValue)
79
+ if (!parsedInput.ok) {
80
+ skipped.push({ method: operation.method, path, reason: parsedInput.reason })
81
+ continue
82
+ }
83
+ const input = parsedInput.value
84
+
85
+ const security = operationSecurityRequirements(operationValue.security, defaultSecurity, schemes)
86
+ if (!security.ok) {
87
+ skipped.push({ method: operation.method, path, reason: security.reason })
88
+ continue
89
+ }
90
+ const plan = {
91
+ operation,
92
+ url: `${resolvedBaseUrl.value.replace(/\/+$/, "")}${path}`,
93
+ fields: input.fields,
94
+ body: input.body,
95
+ security: security.value,
96
+ schemes,
97
+ auth: options.auth,
98
+ headers: options.headers ?? {},
99
+ }
100
+ used.add(segments.join("."))
101
+ for (const index of segments.slice(0, -1).keys()) namespaces.add(segments.slice(0, index + 1).join("."))
102
+ setTool(
103
+ tools,
104
+ segments,
105
+ make({
106
+ description: operation.description ?? operation.summary ?? `${operation.method} ${path}`,
107
+ input: inputSchema(input.fields, definitions),
108
+ output: output.value,
109
+ run: (input) => invoke(plan, input),
110
+ }),
111
+ )
112
+ }
113
+ }
114
+
115
+ return { tools, skipped }
116
+ }
117
+
118
+ const setTool = (tools: Tools, path: ReadonlyArray<string>, definition: Definition<HttpClient.HttpClient>): void => {
119
+ const [head, ...rest] = path
120
+ if (head === undefined) return
121
+ if (rest.length === 0) {
122
+ tools[head] = definition
123
+ return
124
+ }
125
+ const child = tools[head]
126
+ if (child === undefined || !isRecord(child) || child._tag === "CodeModeTool") {
127
+ tools[head] = Object.create(null) as Tools
128
+ }
129
+ setTool(tools[head] as Tools, rest, definition)
130
+ }
packages/codemode/src/openapi/runtime.ts ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Effect, Option, Schema, Stream } from "effect"
2
+ import { HttpClient, HttpClientRequest, HttpClientResponse, type HttpMethod } from "effect/unstable/http"
3
+ import { ToolError, toolError } from "../tool-error.js"
4
+ import { isRecord, own } from "./spec.js"
5
+ import type { AppliedAuth, Credential, Plan, SecurityScheme } from "./types.js"
6
+
7
+ const decodeJson = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)
8
+ const maxErrorBodyChars = 1_024
9
+ const maxResponseBodyBytes = 50 * 1024 * 1024
10
+
11
+ export const invoke = (plan: Plan, input: unknown): Effect.Effect<unknown, unknown, HttpClient.HttpClient> =>
12
+ Effect.gen(function* () {
13
+ const value = isRecord(input) ? input : {}
14
+
15
+ let request = yield* buildRequest(plan, value)
16
+
17
+ const auth = yield* resolveAuth(plan)
18
+ for (const [name, item] of Object.entries(auth.query)) {
19
+ request = HttpClientRequest.setUrlParam(request, name, item)
20
+ }
21
+ request = HttpClientRequest.setHeaders(request, auth.headers)
22
+
23
+ const client = yield* HttpClient.HttpClient
24
+ const response = yield* client
25
+ .execute(request)
26
+ .pipe(
27
+ Effect.catch((cause) =>
28
+ Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} failed: transport error`, cause)),
29
+ ),
30
+ )
31
+ const text = yield* readResponseBody(response, plan)
32
+ const mediaType = response.headers["content-type"]?.split(";")[0]?.trim().toLowerCase()
33
+ const json = mediaType === "application/json" || mediaType?.endsWith("+json") === true
34
+ const decoded = text === "" ? Option.some(null) : json ? decodeJson(text) : Option.none()
35
+ const parsed = json ? Option.getOrElse(decoded, () => text) : text === "" ? null : text
36
+ if (response.status < 200 || response.status >= 300) {
37
+ const rendered = typeof parsed === "string" ? parsed : (JSON.stringify(parsed) ?? "")
38
+ const summary =
39
+ rendered === "" || rendered === "null"
40
+ ? "no response body"
41
+ : rendered.length > maxErrorBodyChars
42
+ ? `${rendered.slice(0, maxErrorBodyChars)}...`
43
+ : rendered
44
+ return yield* Effect.fail(
45
+ toolError(`${plan.operation.method} ${plan.operation.path} failed with HTTP ${response.status}: ${summary}`),
46
+ )
47
+ }
48
+ if (json && Option.isNone(decoded)) {
49
+ return yield* Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} returned malformed JSON.`))
50
+ }
51
+ return parsed
52
+ })
53
+
54
+ const buildRequest = (
55
+ plan: Plan,
56
+ input: Readonly<Record<string, unknown>>,
57
+ ): Effect.Effect<HttpClientRequest.HttpClientRequest, ToolError> =>
58
+ Effect.gen(function* () {
59
+ // Validate every model-controlled value before auth resolution, which may refresh tokens.
60
+ const url = buildUrl(plan, input)
61
+ if (url instanceof ToolError) return yield* Effect.fail(url)
62
+ const missing = plan.fields.find(
63
+ (field) => field.required && field.location !== "path" && own(input, field.inputName) === undefined,
64
+ )
65
+ if (missing !== undefined) {
66
+ const label = missing.location === "body" ? "body field" : `${missing.location} parameter`
67
+ return yield* Effect.fail(toolError(`Missing required ${label} '${missing.inputName}'.`))
68
+ }
69
+
70
+ let request = HttpClientRequest.make(plan.operation.method as HttpMethod.HttpMethod)(url)
71
+ for (const field of plan.fields) {
72
+ if (field.location !== "query") continue
73
+ const item = own(input, field.inputName)
74
+ if (item === undefined) continue
75
+ const serialized = serializeQuery(request, field, item)
76
+ if (serialized instanceof ToolError) return yield* Effect.fail(serialized)
77
+ request = serialized
78
+ }
79
+
80
+ // Host headers first, then declared header parameters.
81
+ request = HttpClientRequest.setHeaders(request, plan.headers)
82
+ for (const field of plan.fields) {
83
+ if (field.location !== "header") continue
84
+ const item = own(input, field.inputName)
85
+ if (item === undefined) continue
86
+ const serialized = serializeSimple(field, item, String)
87
+ if (serialized instanceof ToolError) return yield* Effect.fail(serialized)
88
+ request = HttpClientRequest.setHeader(request, field.name, serialized)
89
+ }
90
+
91
+ const setBody = (value: unknown, mediaType: string) =>
92
+ HttpClientRequest.bodyJson(request, value).pipe(
93
+ Effect.map((next) => HttpClientRequest.setHeader(next, "content-type", mediaType)),
94
+ Effect.mapError((cause) =>
95
+ toolError(`Invalid JSON body for ${plan.operation.method} ${plan.operation.path}.`, cause),
96
+ ),
97
+ )
98
+ if (plan.body?.mode === "value") {
99
+ const field = plan.fields.find((field) => field.location === "body")
100
+ const body = field === undefined ? undefined : own(input, field.inputName)
101
+ if (body !== undefined) request = yield* setBody(body, plan.body.mediaType)
102
+ }
103
+ if (plan.body?.mode === "object") {
104
+ const entries = plan.fields.flatMap((field) => {
105
+ if (field.location !== "body") return []
106
+ const item = own(input, field.inputName)
107
+ return item === undefined ? [] : [[field.name, item] as const]
108
+ })
109
+ if (plan.body.required || entries.length > 0) {
110
+ request = yield* setBody(Object.fromEntries(entries), plan.body.mediaType)
111
+ }
112
+ }
113
+ return request
114
+ })
115
+
116
+ const resolveAuth = (plan: Plan): Effect.Effect<AppliedAuth, unknown> =>
117
+ Effect.gen(function* () {
118
+ const none: AppliedAuth = { headers: {}, query: {} }
119
+ if (plan.security.length === 0) return none
120
+
121
+ const unavailable: Array<string> = []
122
+ alternatives: for (const requirement of plan.security) {
123
+ const names = Object.keys(requirement)
124
+ if (names.length === 0) return none
125
+ const credentials: Array<readonly [string, SecurityScheme, Credential]> = []
126
+ for (const name of names) {
127
+ const scheme = own(plan.schemes, name)
128
+ if (scheme === undefined || plan.auth === undefined) {
129
+ unavailable.push(name)
130
+ continue alternatives
131
+ }
132
+ const credential = yield* plan.auth.resolve({
133
+ name,
134
+ definition: scheme,
135
+ scopes: requirement[name] ?? [],
136
+ operation: plan.operation,
137
+ })
138
+ if (credential === undefined) {
139
+ unavailable.push(name)
140
+ continue alternatives
141
+ }
142
+ credentials.push([name, scheme, credential])
143
+ }
144
+ const applied = applyCredentials(credentials)
145
+ return applied instanceof ToolError ? yield* Effect.fail(applied) : applied
146
+ }
147
+
148
+ return yield* Effect.fail(
149
+ toolError(
150
+ `${plan.operation.method} ${plan.operation.path} requires authentication; no credential available for: ${[...new Set(unavailable)].join(", ")}.`,
151
+ ),
152
+ )
153
+ })
154
+
155
+ const applyCredentials = (
156
+ credentials: ReadonlyArray<readonly [string, SecurityScheme, Credential]>,
157
+ ): AppliedAuth | ToolError => {
158
+ const headers = new Map<string, string>()
159
+ const query = new Map<string, string>()
160
+ const add = (carrier: "header" | "query", name: string, value: string): ToolError | undefined => {
161
+ const target = carrier === "header" ? headers : query
162
+ if (target.has(name)) return toolError(`Authentication resolves multiple credentials for ${carrier} '${name}'.`)
163
+ target.set(name, value)
164
+ }
165
+ for (const [name, definition, credential] of credentials) {
166
+ if (credential.type === "bearer") {
167
+ const duplicate = add("header", "authorization", `Bearer ${credential.token}`)
168
+ if (duplicate !== undefined) return duplicate
169
+ continue
170
+ }
171
+ if (credential.type === "basic") {
172
+ // Buffer instead of btoa: btoa throws on non-Latin-1 credentials.
173
+ const duplicate = add(
174
+ "header",
175
+ "authorization",
176
+ `Basic ${Buffer.from(`${credential.username}:${credential.password}`, "utf8").toString("base64")}`,
177
+ )
178
+ if (duplicate !== undefined) return duplicate
179
+ continue
180
+ }
181
+ if (credential.type === "header") {
182
+ const duplicate = add("header", credential.name.toLowerCase(), credential.value)
183
+ if (duplicate !== undefined) return duplicate
184
+ continue
185
+ }
186
+ // apiKey: the carrier comes from the scheme declaration.
187
+ if (definition.type !== "apiKey") {
188
+ return toolError(
189
+ `Security scheme '${name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`,
190
+ )
191
+ }
192
+ if (definition.in === "cookie") return toolError(`Cookie authentication '${name}' is not supported.`)
193
+ const parameter = definition.in === "header" ? definition.name.toLowerCase() : definition.name
194
+ const duplicate = add(definition.in, parameter, credential.value)
195
+ if (duplicate !== undefined) return duplicate
196
+ }
197
+ return { headers: Object.fromEntries(headers), query: Object.fromEntries(query) }
198
+ }
199
+
200
+ const buildUrl = (plan: Plan, input: Readonly<Record<string, unknown>>): string | ToolError => {
201
+ let url = plan.url
202
+ for (const field of plan.fields) {
203
+ if (field.location !== "path") continue
204
+ const item = own(input, field.inputName)
205
+ if (item === undefined) {
206
+ return toolError(`Missing required path parameter '${field.inputName}'.`)
207
+ }
208
+ const fieldValue = serializeSimple(field, item, (value) =>
209
+ encodeURIComponent(value).replace(
210
+ /[!'()*]/g,
211
+ (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
212
+ ),
213
+ )
214
+ if (fieldValue instanceof ToolError) return fieldValue
215
+ // '.'/'..' survive encoding and URL normalization collapses them, letting a
216
+ // model-supplied value retarget the request to a different endpoint.
217
+ if (fieldValue === "" || fieldValue === "." || fieldValue === "..") {
218
+ return toolError(`Invalid path parameter '${field.inputName}'.`)
219
+ }
220
+ url = url.replaceAll(`{${field.name}}`, fieldValue)
221
+ }
222
+ const unresolved = url.match(/\{[^{}]+\}/)
223
+ if (unresolved !== null) return toolError(`Unresolved path parameter ${unresolved[0]}.`)
224
+ return url
225
+ }
226
+
227
+ const serializeSimple = (
228
+ field: Plan["fields"][number],
229
+ value: unknown,
230
+ encode: (value: string) => string,
231
+ ): string | ToolError => {
232
+ const scalar = (item: unknown): string | ToolError =>
233
+ item !== null && typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean"
234
+ ? toolError(`Parameter '${field.inputName}' contains an unsupported nested value.`)
235
+ : encode(String(item))
236
+ if (Array.isArray(value)) {
237
+ const items = value.map(scalar)
238
+ const invalid = items.find((item): item is ToolError => item instanceof ToolError)
239
+ return invalid ?? items.join(",")
240
+ }
241
+ if (!isRecord(value)) return scalar(value)
242
+ const entries = Object.entries(value).flatMap<string | ToolError>(([name, item]) => {
243
+ const rendered = scalar(item)
244
+ if (rendered instanceof ToolError) return [rendered]
245
+ return field.explode ? [`${encode(name)}=${rendered}`] : [encode(name), rendered]
246
+ })
247
+ const invalid = entries.find((item): item is ToolError => item instanceof ToolError)
248
+ return invalid ?? entries.join(",")
249
+ }
250
+
251
+ const serializeQuery = (
252
+ request: HttpClientRequest.HttpClientRequest,
253
+ field: Plan["fields"][number],
254
+ value: unknown,
255
+ ): HttpClientRequest.HttpClientRequest | ToolError => {
256
+ if (field.style === "deepObject") {
257
+ if (!isRecord(value)) return toolError(`Deep-object parameter '${field.inputName}' must be an object.`)
258
+ return Object.entries(value).reduce<HttpClientRequest.HttpClientRequest | ToolError>((current, [name, item]) => {
259
+ if (current instanceof ToolError) return current
260
+ if (item === undefined || (item !== null && typeof item === "object")) {
261
+ return toolError(`Deep-object parameter '${field.inputName}' contains an unsupported nested value.`)
262
+ }
263
+ return HttpClientRequest.appendUrlParam(current, `${field.name}[${name}]`, String(item))
264
+ }, request)
265
+ }
266
+ if (Array.isArray(value)) {
267
+ const rendered = serializeSimple(field, value, String)
268
+ if (rendered instanceof ToolError) return rendered
269
+ if (!field.explode) return HttpClientRequest.appendUrlParam(request, field.name, rendered)
270
+ if (value.some((item) => item === undefined || (item !== null && typeof item === "object"))) {
271
+ return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`)
272
+ }
273
+ return value.reduce((current, item) => HttpClientRequest.appendUrlParam(current, field.name, String(item)), request)
274
+ }
275
+ if (isRecord(value) && field.explode) {
276
+ return Object.entries(value).reduce<HttpClientRequest.HttpClientRequest | ToolError>((current, [name, item]) => {
277
+ if (current instanceof ToolError) return current
278
+ if (item === undefined || (item !== null && typeof item === "object")) {
279
+ return toolError(`Query parameter '${field.inputName}' contains an unsupported nested value.`)
280
+ }
281
+ return HttpClientRequest.appendUrlParam(current, name, String(item))
282
+ }, request)
283
+ }
284
+ const rendered = serializeSimple(field, value, String)
285
+ return rendered instanceof ToolError ? rendered : HttpClientRequest.appendUrlParam(request, field.name, rendered)
286
+ }
287
+
288
+ const readResponseBody = (
289
+ response: HttpClientResponse.HttpClientResponse,
290
+ plan: Plan,
291
+ ): Effect.Effect<string, ToolError> =>
292
+ Effect.gen(function* () {
293
+ const contentLength = response.headers["content-length"]
294
+ const parsedSize = contentLength === undefined ? undefined : Number.parseInt(contentLength, 10)
295
+ const declaredSize =
296
+ parsedSize !== undefined && Number.isSafeInteger(parsedSize) && parsedSize >= 0 ? parsedSize : undefined
297
+ if (declaredSize !== undefined && declaredSize > maxResponseBodyBytes) {
298
+ return yield* Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`))
299
+ }
300
+ let body = Buffer.allocUnsafe(Math.min(maxResponseBodyBytes, declaredSize ?? 64 * 1024))
301
+ let size = 0
302
+ yield* Stream.runForEach(response.stream, (chunk) => {
303
+ if (size + chunk.byteLength > maxResponseBodyBytes) {
304
+ return Effect.fail(toolError(`${plan.operation.method} ${plan.operation.path} response exceeds 50 MiB.`))
305
+ }
306
+ if (size + chunk.byteLength > body.byteLength) {
307
+ const grown = Buffer.allocUnsafe(
308
+ Math.min(maxResponseBodyBytes, Math.max(size + chunk.byteLength, body.byteLength * 2)),
309
+ )
310
+ body.copy(grown, 0, 0, size)
311
+ body = grown
312
+ }
313
+ body.set(chunk, size)
314
+ size += chunk.byteLength
315
+ return Effect.void
316
+ }).pipe(
317
+ Effect.catch((cause) => {
318
+ if (cause instanceof ToolError) return Effect.fail(cause)
319
+ if (cause.reason._tag === "EmptyBodyError") return Effect.void
320
+ return Effect.fail(
321
+ toolError(`${plan.operation.method} ${plan.operation.path} failed while reading the response body.`, cause),
322
+ )
323
+ }),
324
+ )
325
+ return new TextDecoder().decode(body.subarray(0, size))
326
+ })
packages/codemode/src/openapi/spec.ts ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { fromSchemaOpenApi3_0, fromSchemaOpenApi3_1 } from "effect/JsonSchema"
2
+ import type { JsonSchema } from "../tool.js"
3
+ import { isBlockedMember } from "../tool-runtime.js"
4
+ import type {
5
+ Body,
6
+ Document,
7
+ InputField,
8
+ OperationInput,
9
+ Parsed,
10
+ SecurityRequirement,
11
+ SecurityScheme,
12
+ } from "./types.js"
13
+
14
+ export const methods = new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"])
15
+ const parameterLocations = ["path", "query", "header"] as const
16
+ const ignoredHeaderParameters = new Set(["accept", "content-type", "authorization"])
17
+
18
+ export const isRecord = (value: unknown): value is Record<string, unknown> =>
19
+ typeof value === "object" && value !== null && !Array.isArray(value)
20
+
21
+ const asArray = (value: unknown): ReadonlyArray<unknown> => (Array.isArray(value) ? value : [])
22
+
23
+ export const nonEmptyString = (value: unknown): string | undefined =>
24
+ typeof value === "string" && value !== "" ? value : undefined
25
+
26
+ // Guards record lookups keyed by spec- or model-controlled names against
27
+ // prototype-inherited values (e.g. a parameter named `toString`).
28
+ export const own = <T>(record: Readonly<Record<string, T>>, key: string): T | undefined =>
29
+ Object.hasOwn(record, key) ? record[key] : undefined
30
+
31
+ export const resolve = (document: Document, value: unknown): unknown => {
32
+ const next = (current: unknown, seen: ReadonlySet<string>): unknown => {
33
+ if (!isRecord(current)) return current
34
+ const ref = nonEmptyString(current.$ref)
35
+ if (ref === undefined || !ref.startsWith("#/") || seen.has(ref)) return current
36
+ const target = ref
37
+ .slice(2)
38
+ .split("/")
39
+ .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
40
+ .reduce<unknown>((item, segment) => (isRecord(item) ? own(item, segment) : undefined), document)
41
+ return target === undefined ? current : next(target, new Set([...seen, ref]))
42
+ }
43
+ return next(value, new Set())
44
+ }
45
+
46
+ const projectSchema = (document: Document, value: unknown): JsonSchema => {
47
+ if (!isRecord(value)) return {}
48
+ const normalized = nonEmptyString(document.openapi)?.startsWith("3.0")
49
+ ? fromSchemaOpenApi3_0(value)
50
+ : fromSchemaOpenApi3_1(value)
51
+ return Object.keys(normalized.definitions).length === 0
52
+ ? normalized.schema
53
+ : { ...normalized.schema, $defs: normalized.definitions }
54
+ }
55
+
56
+ export const componentDefinitions = (document: Document): Readonly<Record<string, JsonSchema>> => {
57
+ const components = isRecord(document.components) ? document.components : {}
58
+ const schemas = isRecord(components.schemas) ? components.schemas : {}
59
+ return Object.fromEntries(Object.entries(schemas).map(([name, value]) => [name, projectSchema(document, value)]))
60
+ }
61
+
62
+ const withDefinitions = (schema: JsonSchema, definitions: Readonly<Record<string, JsonSchema>>): JsonSchema => {
63
+ if (Object.keys(definitions).length === 0) return schema
64
+ const local = isRecord(schema.$defs) ? schema.$defs : {}
65
+ return { ...schema, $defs: { ...definitions, ...local } }
66
+ }
67
+
68
+ const isJsonMediaType = (mediaType: string): boolean => {
69
+ const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? ""
70
+ return normalized === "application/json" || normalized.endsWith("+json")
71
+ }
72
+
73
+ const isBinaryMediaType = (document: Document, mediaType: string, value: unknown): boolean => {
74
+ const normalized = mediaType.split(";")[0]?.trim().toLowerCase() ?? ""
75
+ if (!isJsonMediaType(normalized) && !normalized.startsWith("text/")) return true
76
+ if (!isRecord(value)) return false
77
+ const schema = resolve(document, value.schema)
78
+ return isRecord(schema) && schema.format === "binary"
79
+ }
80
+
81
+ const jsonContent = (
82
+ content: Record<string, unknown>,
83
+ ): { readonly mediaType: string; readonly schema: unknown } | undefined => {
84
+ const entry = Object.entries(content).find(([mediaType]) => isJsonMediaType(mediaType))
85
+ return entry !== undefined && isRecord(entry[1]) ? { mediaType: entry[0], schema: entry[1].schema } : undefined
86
+ }
87
+
88
+ const isFlattenableObjectBody = (
89
+ schema: unknown,
90
+ requestRequired: boolean,
91
+ ): schema is Record<string, unknown> & { readonly properties: Record<string, unknown> } =>
92
+ isRecord(schema) &&
93
+ requestRequired &&
94
+ schema.type === "object" &&
95
+ isRecord(schema.properties) &&
96
+ schema.additionalProperties === false &&
97
+ schema.nullable !== true &&
98
+ schema.allOf === undefined &&
99
+ schema.anyOf === undefined &&
100
+ schema.oneOf === undefined
101
+
102
+ type PlannedField = Omit<InputField, "inputName">
103
+
104
+ const operationParameters = (
105
+ document: Document,
106
+ pathItem: Record<string, unknown>,
107
+ operation: Record<string, unknown>,
108
+ ): Parsed<ReadonlyArray<PlannedField>> => {
109
+ // Operation-level parameters override path-level ones sharing (location, name).
110
+ const declared = new Map<
111
+ string,
112
+ { readonly name: string; readonly location: string; readonly parameter: Record<string, unknown> }
113
+ >()
114
+ for (const raw of [...asArray(pathItem.parameters), ...asArray(operation.parameters)]) {
115
+ const resolved = resolve(document, raw)
116
+ if (!isRecord(resolved)) return { ok: false, reason: "parameter declaration is invalid or unresolved" }
117
+ const name = nonEmptyString(resolved.name)
118
+ const location = nonEmptyString(resolved.in)
119
+ if (name === undefined || location === undefined)
120
+ return { ok: false, reason: "parameter declaration is missing name or location" }
121
+ declared.set(`${location}:${name}`, { name, location, parameter: resolved })
122
+ }
123
+ const unordered: Array<PlannedField> = []
124
+ for (const item of declared.values()) {
125
+ const name = item.name
126
+ const location = item.location
127
+ const resolved = item.parameter
128
+ if (location === "cookie") return { ok: false, reason: `cookie parameter '${name}' is not supported` }
129
+ if (location !== "path" && location !== "query" && location !== "header") {
130
+ return { ok: false, reason: `parameter '${name}' uses unsupported location '${location}'` }
131
+ }
132
+ if (location === "header" && ignoredHeaderParameters.has(name.toLowerCase())) continue
133
+ if (resolved.schema === undefined && resolved.content === undefined) {
134
+ return { ok: false, reason: `parameter '${name}' declares neither schema nor content` }
135
+ }
136
+ if (resolved.content !== undefined)
137
+ return { ok: false, reason: `parameter '${name}' uses unsupported content encoding` }
138
+ if (resolved.style !== undefined && nonEmptyString(resolved.style) === undefined) {
139
+ return { ok: false, reason: `parameter '${name}' has an invalid style` }
140
+ }
141
+ if (resolved.explode !== undefined && typeof resolved.explode !== "boolean") {
142
+ return { ok: false, reason: `parameter '${name}' has an invalid explode value` }
143
+ }
144
+ if (resolved.allowReserved !== undefined && typeof resolved.allowReserved !== "boolean") {
145
+ return { ok: false, reason: `parameter '${name}' has an invalid allowReserved value` }
146
+ }
147
+ if (resolved.allowReserved === true)
148
+ return { ok: false, reason: `parameter '${name}' uses unsupported allowReserved encoding` }
149
+ const declaredStyle = nonEmptyString(resolved.style) ?? (location === "query" ? "form" : "simple")
150
+ if (location === "query" && declaredStyle !== "form" && declaredStyle !== "deepObject") {
151
+ return { ok: false, reason: `query parameter '${name}' uses unsupported style '${declaredStyle}'` }
152
+ }
153
+ if (location !== "query" && declaredStyle !== "simple") {
154
+ return { ok: false, reason: `${location} parameter '${name}' uses unsupported style '${declaredStyle}'` }
155
+ }
156
+ const style = declaredStyle === "deepObject" ? "deepObject" : declaredStyle === "form" ? "form" : "simple"
157
+ const explode = typeof resolved.explode === "boolean" ? resolved.explode : style === "form"
158
+ if (style === "deepObject" && !explode) {
159
+ return { ok: false, reason: `query parameter '${name}' uses deepObject with explode=false` }
160
+ }
161
+ const base = projectSchema(document, resolved.schema)
162
+ const description = nonEmptyString(resolved.description)
163
+ unordered.push({
164
+ name,
165
+ location,
166
+ required: resolved.required === true || location === "path",
167
+ style,
168
+ explode,
169
+ schema: {
170
+ ...base,
171
+ ...(base.description === undefined && description !== undefined ? { description } : {}),
172
+ },
173
+ })
174
+ }
175
+ return {
176
+ ok: true,
177
+ value: parameterLocations.flatMap((location) => unordered.filter((field) => field.location === location)),
178
+ }
179
+ }
180
+
181
+ const operationBody = (
182
+ document: Document,
183
+ operation: Record<string, unknown>,
184
+ ): Parsed<{ readonly fields: ReadonlyArray<PlannedField>; readonly body: Body | undefined }> => {
185
+ const resolved = resolve(document, operation.requestBody)
186
+ if (!isRecord(resolved)) return { ok: true, value: { fields: [], body: undefined } }
187
+ const content = isRecord(resolved.content) ? resolved.content : {}
188
+ const selected = jsonContent(content)
189
+ if (selected === undefined) {
190
+ return {
191
+ ok: false,
192
+ reason: `request body has no JSON content (declared: ${Object.keys(content).join(", ") || "none"})`,
193
+ }
194
+ }
195
+ const schema = resolve(document, selected.schema)
196
+ const required = resolved.required === true
197
+ if (!isFlattenableObjectBody(schema, required)) {
198
+ return {
199
+ ok: true,
200
+ value: {
201
+ fields: [
202
+ {
203
+ name: "body",
204
+ location: "body",
205
+ required,
206
+ schema: projectSchema(document, selected.schema),
207
+ style: undefined,
208
+ explode: undefined,
209
+ },
210
+ ],
211
+ body: { required, mode: "value", mediaType: selected.mediaType },
212
+ },
213
+ }
214
+ }
215
+ const requiredProperties = new Set(
216
+ Array.isArray(schema.required) ? schema.required.filter((item): item is string => typeof item === "string") : [],
217
+ )
218
+ return {
219
+ ok: true,
220
+ value: {
221
+ fields: Object.entries(schema.properties).map(([name, value]) => ({
222
+ name,
223
+ location: "body" as const,
224
+ required: required && requiredProperties.has(name),
225
+ schema: projectSchema(document, value),
226
+ style: undefined,
227
+ explode: undefined,
228
+ })),
229
+ body: { required, mode: "object", mediaType: selected.mediaType },
230
+ },
231
+ }
232
+ }
233
+
234
+ export const operationInput = (
235
+ document: Document,
236
+ pathItem: Record<string, unknown>,
237
+ operation: Record<string, unknown>,
238
+ ): Parsed<OperationInput> => {
239
+ const parameters = operationParameters(document, pathItem, operation)
240
+ if (!parameters.ok) return parameters
241
+ const requestBody = operationBody(document, operation)
242
+ if (!requestBody.ok) return requestBody
243
+ const fields = [...parameters.value, ...requestBody.value.fields]
244
+
245
+ const conflicts = new Set(
246
+ [...Map.groupBy(fields, (field) => field.name)]
247
+ .filter(([, matches]) => new Set(matches.map((field) => field.location)).size > 1)
248
+ .map(([name]) => name),
249
+ )
250
+ const used = new Set<string>()
251
+ return {
252
+ ok: true,
253
+ value: {
254
+ fields: fields.map((field) => {
255
+ const visibleName = isBlockedMember(field.name) ? `${field.name}_2` : field.name
256
+ const base = conflicts.has(field.name) ? `${field.location}_${visibleName}` : visibleName
257
+ const next = (index: number): string => {
258
+ const candidate = index === 1 ? base : `${base}_${index}`
259
+ return used.has(candidate) ? next(index + 1) : candidate
260
+ }
261
+ const inputName = next(1)
262
+ used.add(inputName)
263
+ return { ...field, inputName }
264
+ }),
265
+ body: requestBody.value.body,
266
+ },
267
+ }
268
+ }
269
+
270
+ export const inputSchema = (
271
+ fields: ReadonlyArray<InputField>,
272
+ definitions: Readonly<Record<string, JsonSchema>>,
273
+ ): JsonSchema => {
274
+ const required = fields.filter((field) => field.required).map((field) => field.inputName)
275
+ return withDefinitions(
276
+ {
277
+ type: "object",
278
+ properties: Object.fromEntries(fields.map((field) => [field.inputName, field.schema])),
279
+ ...(required.length === 0 ? {} : { required }),
280
+ },
281
+ definitions,
282
+ )
283
+ }
284
+
285
+ const successfulResponses = (
286
+ document: Document,
287
+ operation: Record<string, unknown>,
288
+ ): Parsed<ReadonlyArray<Record<string, unknown>>> => {
289
+ if (!isRecord(operation.responses)) return { ok: true, value: [] }
290
+ const entries = Object.entries(operation.responses)
291
+ const selected = [
292
+ ...entries.filter(([status]) => /^2\d\d$/.test(status)).sort(([a], [b]) => a.localeCompare(b)),
293
+ ...entries.filter(([status]) => status.toUpperCase() === "2XX"),
294
+ ]
295
+ const responses: Array<Record<string, unknown>> = []
296
+ for (const [, value] of selected) {
297
+ const resolved = resolve(document, value)
298
+ if (!isRecord(resolved) || nonEmptyString(resolved.$ref) !== undefined) {
299
+ return { ok: false, reason: "successful response declaration is invalid or unresolved" }
300
+ }
301
+ responses.push(resolved)
302
+ }
303
+ return { ok: true, value: responses }
304
+ }
305
+
306
+ export const operationOutput = (
307
+ document: Document,
308
+ operation: Record<string, unknown>,
309
+ definitions: Readonly<Record<string, JsonSchema>>,
310
+ ): Parsed<JsonSchema | undefined> => {
311
+ if (operation["x-websocket"] === true) return { ok: false, reason: "WebSocket operations are not supported" }
312
+ const responses = successfulResponses(document, operation)
313
+ if (!responses.ok) return responses
314
+ const streams = responses.value.some(
315
+ (response) =>
316
+ isRecord(response.content) &&
317
+ Object.keys(response.content).some(
318
+ (mediaType) => mediaType.split(";")[0]?.trim().toLowerCase() === "text/event-stream",
319
+ ),
320
+ )
321
+ if (streams) return { ok: false, reason: "SSE operations are not supported" }
322
+ const binary = responses.value.some(
323
+ (response) =>
324
+ isRecord(response.content) &&
325
+ Object.entries(response.content).some(([mediaType, value]) => isBinaryMediaType(document, mediaType, value)),
326
+ )
327
+ if (binary) return { ok: false, reason: "binary responses are not supported" }
328
+
329
+ const outcomes: Array<JsonSchema> = []
330
+ for (const response of responses.value) {
331
+ if (response.content !== undefined && !isRecord(response.content)) return { ok: true, value: undefined }
332
+ const content = isRecord(response.content) ? response.content : {}
333
+ if (Object.keys(content).length === 0) {
334
+ outcomes.push({ type: "null" })
335
+ continue
336
+ }
337
+ for (const [mediaType, value] of Object.entries(content)) {
338
+ if (!isJsonMediaType(mediaType)) {
339
+ outcomes.push({ type: "string" })
340
+ continue
341
+ }
342
+ if (!isRecord(value) || value.schema === undefined) return { ok: true, value: undefined }
343
+ outcomes.push(projectSchema(document, value.schema))
344
+ }
345
+ }
346
+ if (outcomes.length === 0) return { ok: true, value: undefined }
347
+ return {
348
+ ok: true,
349
+ value: withDefinitions(outcomes.length === 1 ? (outcomes[0] ?? {}) : { anyOf: outcomes }, definitions),
350
+ }
351
+ }
352
+
353
+ const sanitizeOperationSegment = (raw: string): string => {
354
+ const base =
355
+ raw
356
+ .replaceAll(/[^A-Za-z0-9_$]+/g, "_")
357
+ .replace(/^_+|_+$/g, "")
358
+ .replace(/^([0-9])/, "_$1") || "operation"
359
+ return isBlockedMember(base) ? `${base}_2` : base
360
+ }
361
+
362
+ const fallbackOperationId = (method: string, path: string): string =>
363
+ [
364
+ method,
365
+ ...path
366
+ .split("/")
367
+ .filter((part) => part !== "")
368
+ .flatMap((part) => (part.startsWith("{") && part.endsWith("}") ? ["by", part.slice(1, -1)] : [part]))
369
+ .flatMap((part) => part.split(/[^A-Za-z0-9]+/).filter((word) => word !== "")),
370
+ ]
371
+ .map((word, index) => {
372
+ const lower = word.toLowerCase()
373
+ return index === 0 ? lower : `${lower.charAt(0).toUpperCase()}${lower.slice(1)}`
374
+ })
375
+ .join("")
376
+
377
+ export const operationPath = (
378
+ method: string,
379
+ path: string,
380
+ operation: Record<string, unknown>,
381
+ used: ReadonlySet<string>,
382
+ namespaces: ReadonlySet<string>,
383
+ ): ReadonlyArray<string> => {
384
+ const raw = nonEmptyString(operation.operationId)
385
+ const segments = (raw === undefined ? [fallbackOperationId(method, path)] : raw.split(".")).map(
386
+ sanitizeOperationSegment,
387
+ )
388
+ if (isOperationPathAvailable(segments, used, namespaces)) return segments
389
+ const conflict = segments.slice(0, -1).findIndex((_, index) => used.has(segments.slice(0, index + 1).join(".")))
390
+ if (conflict >= 0 && conflict + 1 < segments.length) {
391
+ const collapsed = segments.flatMap((segment, index) => {
392
+ if (index === conflict) {
393
+ const next = segments[index + 1] ?? ""
394
+ return [`${segment}${next.charAt(0).toUpperCase()}${next.slice(1)}`]
395
+ }
396
+ return index === conflict + 1 ? [] : [segment]
397
+ })
398
+ if (isOperationPathAvailable(collapsed, used, namespaces)) return collapsed
399
+ }
400
+ const fallback = segments.join("_")
401
+ const next = (index: number): string => {
402
+ const candidate = `${fallback}_${index}`
403
+ return isOperationPathAvailable([candidate], used, namespaces) ? candidate : next(index + 1)
404
+ }
405
+ return [next(2)]
406
+ }
407
+
408
+ const isOperationPathAvailable = (
409
+ segments: ReadonlyArray<string>,
410
+ used: ReadonlySet<string>,
411
+ namespaces: ReadonlySet<string>,
412
+ ): boolean => {
413
+ const key = segments.join(".")
414
+ if (used.has(key) || namespaces.has(key)) return false
415
+ return segments.slice(0, -1).every((_, index) => !used.has(segments.slice(0, index + 1).join(".")))
416
+ }
417
+
418
+ export const specServerUrl = (source: Record<string, unknown>): Parsed<string> => {
419
+ const server = asArray(source.servers).find(isRecord)
420
+ const url = server === undefined ? undefined : nonEmptyString(server.url)
421
+ if (url === undefined) return { ok: false, reason: "spec declares no servers; pass baseUrl" }
422
+ if (/\{[^{}]+\}/.test(url)) {
423
+ return { ok: false, reason: `server URL '${url}' is not an absolute URL; pass baseUrl` }
424
+ }
425
+ return validateBaseUrl(url)
426
+ }
427
+
428
+ export const validateBaseUrl = (value: string): Parsed<string> => {
429
+ if (!/^https?:\/\//i.test(value)) return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` }
430
+ const url = URL.parse(value)
431
+ if (url === null || (url.protocol !== "http:" && url.protocol !== "https:")) {
432
+ return { ok: false, reason: `server URL '${value}' is not an absolute HTTP(S) URL` }
433
+ }
434
+ if (url.search !== "" || url.hash !== "") {
435
+ return { ok: false, reason: `server URL '${value}' contains an unsupported query string or fragment` }
436
+ }
437
+ return { ok: true, value }
438
+ }
439
+
440
+ export const securityRequirements = (value: unknown): Parsed<ReadonlyArray<SecurityRequirement>> => {
441
+ if (value === undefined) return { ok: true, value: [] }
442
+ if (!Array.isArray(value)) return { ok: false, reason: "security declaration is not an array" }
443
+ const requirements: Array<SecurityRequirement> = []
444
+ for (const item of value) {
445
+ if (!isRecord(item)) return { ok: false, reason: "security requirement is not an object" }
446
+ const requirement = Object.create(null) as Record<string, ReadonlyArray<string>>
447
+ for (const [name, scopes] of Object.entries(item)) {
448
+ if (!Array.isArray(scopes)) return { ok: false, reason: "security requirement scopes are not string arrays" }
449
+ const parsed = scopes.filter((scope): scope is string => typeof scope === "string")
450
+ if (parsed.length !== scopes.length) {
451
+ return { ok: false, reason: "security requirement scopes are not string arrays" }
452
+ }
453
+ requirement[name] = parsed
454
+ }
455
+ requirements.push(requirement)
456
+ }
457
+ return { ok: true, value: requirements }
458
+ }
459
+
460
+ export const operationSecurityRequirements = (
461
+ value: unknown,
462
+ defaults: Parsed<ReadonlyArray<SecurityRequirement>>,
463
+ schemes: Readonly<Record<string, SecurityScheme>>,
464
+ ): Parsed<ReadonlyArray<SecurityRequirement>> => {
465
+ const parsed = value === undefined ? defaults : securityRequirements(value)
466
+ if (!parsed.ok) return parsed
467
+ const supported = parsed.value.filter((requirement) =>
468
+ Object.keys(requirement).every((name) => {
469
+ const scheme = own(schemes, name)
470
+ return scheme !== undefined && !(scheme.type === "apiKey" && scheme.in === "cookie")
471
+ }),
472
+ )
473
+ if (parsed.value.length === 0 || supported.length > 0) return { ok: true, value: supported }
474
+
475
+ const names = [...new Set(parsed.value.flatMap((requirement) => Object.keys(requirement)))]
476
+ const cookieScheme = names.find((name) => {
477
+ const definition = own(schemes, name)
478
+ return definition?.type === "apiKey" && definition.in === "cookie"
479
+ })
480
+ return {
481
+ ok: false,
482
+ reason:
483
+ cookieScheme === undefined
484
+ ? `security requirement references missing or malformed scheme: ${names.join(", ")}`
485
+ : `cookie authentication '${cookieScheme}' is not supported`,
486
+ }
487
+ }
488
+
489
+ export const securitySchemes = (document: Document): Readonly<Record<string, SecurityScheme>> => {
490
+ const components = isRecord(document.components) ? document.components : {}
491
+ const declared = isRecord(components.securitySchemes) ? components.securitySchemes : {}
492
+ return Object.fromEntries(
493
+ Object.entries(declared).flatMap<readonly [string, SecurityScheme]>(([name, value]) => {
494
+ const resolved = resolve(document, value)
495
+ if (!isRecord(resolved)) return []
496
+ const type = nonEmptyString(resolved.type)
497
+ if (type === "apiKey") {
498
+ const carrier = nonEmptyString(resolved.in)
499
+ const parameter = nonEmptyString(resolved.name)
500
+ if (parameter === undefined || (carrier !== "header" && carrier !== "query" && carrier !== "cookie")) return []
501
+ return [[name, { type, name: parameter, in: carrier }] as const]
502
+ }
503
+ if (type === "http") {
504
+ const scheme = nonEmptyString(resolved.scheme)?.toLowerCase()
505
+ return scheme === undefined ? [] : [[name, { type, scheme }] as const]
506
+ }
507
+ if (type === "oauth2" || type === "openIdConnect") return [[name, { type }] as const]
508
+ return []
509
+ }),
510
+ )
511
+ }
packages/codemode/src/openapi/types.ts ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Effect } from "effect"
2
+ import { HttpClient } from "effect/unstable/http"
3
+ import type { Definition, JsonSchema } from "../tool.js"
4
+
5
+ /** A parsed OpenAPI 3.x document. YAML must be parsed by the host. */
6
+ export type Document = Record<string, unknown>
7
+
8
+ /** The operation identity handed to auth resolution and errors. */
9
+ export type Operation = {
10
+ readonly operationId: string | undefined
11
+ readonly method: string
12
+ readonly path: string
13
+ readonly summary: string | undefined
14
+ readonly description: string | undefined
15
+ }
16
+
17
+ /** A resolved OpenAPI security scheme from `components.securitySchemes`. */
18
+ export type SecurityScheme =
19
+ | { readonly type: "apiKey"; readonly name: string; readonly in: "header" | "query" | "cookie" }
20
+ | { readonly type: "http"; readonly scheme: string }
21
+ | { readonly type: "oauth2" }
22
+ | { readonly type: "openIdConnect" }
23
+
24
+ /**
25
+ * Credential material returned by a host auth resolver. The carrier for `apiKey`
26
+ * comes from the scheme definition, not the credential. `header` is the escape
27
+ * hatch for nonstandard schemes.
28
+ */
29
+ export type Credential =
30
+ | { readonly type: "bearer"; readonly token: string }
31
+ | { readonly type: "basic"; readonly username: string; readonly password: string }
32
+ | { readonly type: "apiKey"; readonly value: string }
33
+ | { readonly type: "header"; readonly name: string; readonly value: string }
34
+
35
+ /**
36
+ * Resolves credential material for one named security scheme at call time.
37
+ * `undefined` means unavailable, try the next OR alternative; a failure aborts
38
+ * the call rather than falling through.
39
+ */
40
+ export type AuthResolver = (context: {
41
+ readonly name: string
42
+ readonly definition: SecurityScheme
43
+ readonly scopes: ReadonlyArray<string>
44
+ readonly operation: Operation
45
+ }) => Effect.Effect<Credential | undefined, unknown>
46
+
47
+ export type Options = {
48
+ readonly spec: Document
49
+ /** Overrides all document, path, and operation `servers`. Required when no applicable absolute server URL exists. */
50
+ readonly baseUrl?: string | undefined
51
+ /** Host credential resolution, keyed by security scheme name. */
52
+ readonly auth?: { readonly resolve: AuthResolver } | undefined
53
+ /** Static headers on every request. Not model-visible; declared header params may override them, auth always wins. */
54
+ readonly headers?: Readonly<Record<string, string>> | undefined
55
+ }
56
+
57
+ /** An operation that could not be represented as a tool, and why. */
58
+ export type Skipped = {
59
+ readonly method: string
60
+ readonly path: string
61
+ readonly reason: string
62
+ }
63
+
64
+ export type Tools = { [name: string]: Definition<HttpClient.HttpClient> | Tools }
65
+
66
+ export type Result = {
67
+ /** Tool subtree; the host places it under a key in its `tools` tree. */
68
+ readonly tools: Tools
69
+ readonly skipped: ReadonlyArray<Skipped>
70
+ }
71
+
72
+ export type Parsed<T> = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly reason: string }
73
+
74
+ export type InputLocation = "path" | "query" | "header" | "body"
75
+
76
+ export type InputField = {
77
+ /** Model-visible field name after cross-location collision handling. */
78
+ readonly inputName: string
79
+ /** Original parameter or body-property name used on the wire. */
80
+ readonly name: string
81
+ readonly location: InputLocation
82
+ readonly required: boolean
83
+ readonly schema: JsonSchema
84
+ readonly style: "simple" | "form" | "deepObject" | undefined
85
+ readonly explode: boolean | undefined
86
+ }
87
+
88
+ export type Body = { readonly required: boolean; readonly mode: "object" | "value"; readonly mediaType: string }
89
+
90
+ export type OperationInput = {
91
+ readonly fields: ReadonlyArray<InputField>
92
+ readonly body: Body | undefined
93
+ }
94
+
95
+ /** One OR alternative: scheme name -> required scopes. Empty object = unauthenticated is acceptable. */
96
+ export type SecurityRequirement = Readonly<Record<string, ReadonlyArray<string>>>
97
+
98
+ export type Plan = {
99
+ readonly operation: Operation
100
+ readonly url: string
101
+ readonly fields: ReadonlyArray<InputField>
102
+ readonly body: Body | undefined
103
+ readonly security: ReadonlyArray<SecurityRequirement>
104
+ readonly schemes: Readonly<Record<string, SecurityScheme>>
105
+ readonly auth: { readonly resolve: AuthResolver } | undefined
106
+ readonly headers: Readonly<Record<string, string>>
107
+ }
108
+
109
+ export type AppliedAuth = {
110
+ readonly headers: Readonly<Record<string, string>>
111
+ readonly query: Readonly<Record<string, string>>
112
+ }
packages/codemode/src/stdlib/collections.ts ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const arrayMethods = new Set([
2
+ "map",
3
+ "filter",
4
+ "find",
5
+ "findIndex",
6
+ "findLast",
7
+ "findLastIndex",
8
+ "some",
9
+ "every",
10
+ "includes",
11
+ "join",
12
+ "reduce",
13
+ "reduceRight",
14
+ "flatMap",
15
+ "forEach",
16
+ "sort",
17
+ "toSorted",
18
+ "slice",
19
+ "concat",
20
+ "indexOf",
21
+ "lastIndexOf",
22
+ "at",
23
+ "flat",
24
+ "reverse",
25
+ "toReversed",
26
+ "with",
27
+ "push",
28
+ "pop",
29
+ "shift",
30
+ "unshift",
31
+ "splice",
32
+ "fill",
33
+ "copyWithin",
34
+ "keys",
35
+ "values",
36
+ "entries",
37
+ ])
38
+
39
+ export const mapMethods = new Set(["get", "set", "has", "delete", "clear", "forEach", "keys", "values", "entries"])
40
+
41
+ export const setMethods = new Set(["add", "has", "delete", "clear", "forEach", "keys", "values", "entries"])
42
+
43
+ export const spreadItems = (value: unknown): Array<unknown> | undefined => {
44
+ if (Array.isArray(value)) return value
45
+ if (typeof value === "string") return Array.from(value)
46
+ if (value instanceof SandboxMap) return Array.from(value.map.entries(), ([key, item]) => [key, item])
47
+ if (value instanceof SandboxSet) return Array.from(value.set.values())
48
+ if (value instanceof SandboxURLSearchParams) return Array.from(value.params.entries(), ([key, item]) => [key, item])
49
+ return undefined
50
+ }
51
+ import { SandboxMap, SandboxSet, SandboxURLSearchParams } from "../values.js"
packages/codemode/src/stdlib/console.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ export const consoleMethods = new Set(["log", "info", "debug", "warn", "error", "dir", "table"])
2
+
3
+ /** Console formatting recursion ceiling; deeper values render as "...". */
4
+ export const MAX_CONSOLE_DEPTH = 32
packages/codemode/src/stdlib/date.ts ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const dateMethods = new Set([
2
+ "getTime",
3
+ "valueOf",
4
+ "toISOString",
5
+ "toJSON",
6
+ "toString",
7
+ "getFullYear",
8
+ "getMonth",
9
+ "getDate",
10
+ "getDay",
11
+ "getHours",
12
+ "getMinutes",
13
+ "getSeconds",
14
+ "getMilliseconds",
15
+ "getUTCFullYear",
16
+ "getUTCMonth",
17
+ "getUTCDate",
18
+ "getUTCDay",
19
+ "getUTCHours",
20
+ "getUTCMinutes",
21
+ "getUTCSeconds",
22
+ "getUTCMilliseconds",
23
+ "getTimezoneOffset",
24
+ ])
25
+
26
+ export const dateStatics = new Set(["now", "parse", "UTC"])
27
+
28
+ export const invokeDateStatic = (name: string, args: Array<unknown>, node: AstNode): number => {
29
+ switch (name) {
30
+ case "now":
31
+ return Date.now()
32
+ case "parse":
33
+ return Date.parse(coerceToString(args[0]))
34
+ case "UTC":
35
+ return Date.UTC(...(args.map((arg) => coerceToNumber(arg)) as Parameters<typeof Date.UTC>))
36
+ default:
37
+ throw new InterpreterRuntimeError(`Date.${name} is not available in CodeMode.`, node)
38
+ }
39
+ }
40
+
41
+ export const invokeDateMethod = (value: SandboxDate, name: string, node: AstNode): unknown => {
42
+ const hosted = new Date(value.time)
43
+ switch (name) {
44
+ case "getTime":
45
+ case "valueOf":
46
+ return value.time
47
+ case "toISOString":
48
+ if (!Number.isFinite(value.time)) throw new InterpreterRuntimeError("Invalid time value.", node)
49
+ return hosted.toISOString()
50
+ case "toJSON":
51
+ return Number.isFinite(value.time) ? hosted.toISOString() : null
52
+ case "toString":
53
+ return coerceToString(value)
54
+ case "getFullYear":
55
+ return hosted.getFullYear()
56
+ case "getMonth":
57
+ return hosted.getMonth()
58
+ case "getDate":
59
+ return hosted.getDate()
60
+ case "getDay":
61
+ return hosted.getDay()
62
+ case "getHours":
63
+ return hosted.getHours()
64
+ case "getMinutes":
65
+ return hosted.getMinutes()
66
+ case "getSeconds":
67
+ return hosted.getSeconds()
68
+ case "getMilliseconds":
69
+ return hosted.getMilliseconds()
70
+ case "getUTCFullYear":
71
+ return hosted.getUTCFullYear()
72
+ case "getUTCMonth":
73
+ return hosted.getUTCMonth()
74
+ case "getUTCDate":
75
+ return hosted.getUTCDate()
76
+ case "getUTCDay":
77
+ return hosted.getUTCDay()
78
+ case "getUTCHours":
79
+ return hosted.getUTCHours()
80
+ case "getUTCMinutes":
81
+ return hosted.getUTCMinutes()
82
+ case "getUTCSeconds":
83
+ return hosted.getUTCSeconds()
84
+ case "getUTCMilliseconds":
85
+ return hosted.getUTCMilliseconds()
86
+ case "getTimezoneOffset":
87
+ return hosted.getTimezoneOffset()
88
+ default:
89
+ throw new InterpreterRuntimeError(`Date method '${name}' is not available in CodeMode.`, node)
90
+ }
91
+ }
92
+ import { type AstNode, InterpreterRuntimeError } from "../interpreter/model.js"
93
+ import { SandboxDate } from "../values.js"
94
+ import { coerceToNumber, coerceToString } from "./value.js"