Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- packages/cli/bin/lildax.cjs +130 -0
- packages/cli/script/build.ts +116 -0
- packages/cli/script/generate.ts +7 -0
- packages/cli/script/publish.ts +53 -0
- packages/cli/src/commands/commands.ts +52 -0
- packages/cli/src/commands/handlers/api.test.ts +35 -0
- packages/cli/src/commands/handlers/api.ts +85 -0
- packages/cli/src/commands/handlers/debug/agents.ts +21 -0
- packages/cli/src/commands/handlers/default.ts +13 -0
- packages/cli/src/commands/handlers/migrate.ts +5 -0
- packages/cli/src/commands/handlers/serve.ts +46 -0
- packages/cli/src/commands/handlers/service/password.ts +16 -0
- packages/cli/src/commands/handlers/service/restart.ts +14 -0
- packages/cli/src/commands/handlers/service/start.ts +12 -0
- packages/cli/src/commands/handlers/service/status.ts +13 -0
- packages/cli/src/commands/handlers/service/stop.ts +11 -0
- packages/cli/src/framework/runtime.ts +79 -0
- packages/cli/src/framework/spec.ts +42 -0
- packages/cli/src/index.ts +32 -0
- packages/cli/src/services/daemon.ts +192 -0
- packages/cli/src/tui.ts +37 -0
- packages/client/script/build.ts +30 -0
- packages/client/src/contract.ts +53 -0
- packages/client/src/effect.ts +25 -0
- packages/client/src/generated-effect/.httpapi-codegen.json +5 -0
- packages/client/src/generated-effect/client-error.ts +5 -0
- packages/client/src/generated-effect/client.ts +706 -0
- packages/client/src/generated-effect/index.ts +2 -0
- packages/client/src/generated/.httpapi-codegen.json +6 -0
- packages/client/src/generated/client-error.ts +11 -0
- packages/client/src/generated/client.ts +1029 -0
- packages/client/src/generated/index.ts +3 -0
- packages/client/src/generated/types.ts +0 -0
- packages/client/src/index.ts +2 -0
- packages/client/test/contract-identity.test.ts +58 -0
- packages/client/test/effect.test.ts +246 -0
- packages/client/test/import-boundaries.test.ts +68 -0
- packages/client/test/promise.test.ts +255 -0
- packages/codemode/src/codemode.ts +159 -0
- packages/codemode/src/index.ts +4 -0
- packages/codemode/src/interpreter/model.ts +201 -0
- packages/codemode/src/interpreter/runtime.ts +0 -0
- packages/codemode/src/openapi/TODO.md +19 -0
- packages/codemode/src/openapi/index.ts +130 -0
- packages/codemode/src/openapi/runtime.ts +326 -0
- packages/codemode/src/openapi/spec.ts +511 -0
- packages/codemode/src/openapi/types.ts +112 -0
- packages/codemode/src/stdlib/collections.ts +51 -0
- packages/codemode/src/stdlib/console.ts +4 -0
- 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"
|