Skip to content

Commit cdd67cf

Browse files
authored
feat(sdk): add HttpApi clients and embedded host (anomalyco#33445)
1 parent c45d1db commit cdd67cf

59 files changed

Lines changed: 4629 additions & 316 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ jobs:
6969
env:
7070
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}
7171

72+
- name: Check generated client
73+
if: runner.os == 'Linux'
74+
working-directory: packages/client
75+
run: bun run check:generated
76+
7277
- name: Run HttpApi exerciser gates
7378
if: runner.os == 'Linux'
7479
working-directory: packages/opencode

CONTEXT.md

Lines changed: 77 additions & 0 deletions
Large diffs are not rendered by default.

bun.lock

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/client/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# @opencode-ai/client
2+
3+
Private generation target for clients derived directly from OpenCode's authoritative Effect `HttpApi`.
4+
5+
## Entrypoints
6+
7+
- `@opencode-ai/client`: zero-Effect Promise client using `fetch`.
8+
- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`.
9+
10+
The generated surface starts with the Session group from Server's concrete API. The build compiler reads `@opencode-ai/server/api`; the generated Effect runtime imports a client-local projection built from Protocol, with a generation-equivalence test preventing transport drift. Run `bun run generate` after changing the contract and `bun run check:generated` to detect committed-output drift.
11+
12+
The Effect entrypoint uses canonical decoded values such as `Session.ID`, `Location.Ref`, and `Prompt`. These datatypes come from the lightweight `@opencode-ai/schema` package and are re-exported so callers depend only on the client surface. Protocol owns endpoint construction and middleware placement; Server supplies the concrete middleware keys used by the build-time API.
13+
14+
The Promise root remains structural and has no Core or Effect runtime dependency. `/effect` depends only on Effect, Schema, and Protocol and is browser-bundle safe. Bundle-boundary tests enforce both import graphs.
15+
16+
Effect consumers construct canonical decoded inputs:
17+
18+
```ts
19+
import { AbsolutePath, Location, OpenCode, Prompt } from "@opencode-ai/client/effect"
20+
21+
const client = yield * OpenCode.make({ baseUrl: "https://opencode.example" })
22+
yield *
23+
client.sessions.create({
24+
location: Location.Ref.make({ directory: AbsolutePath.make("/workspace") }),
25+
})
26+
yield * client.sessions.prompt({ sessionID, prompt: Prompt.make({ text: "Hello" }) })
27+
```

packages/client/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"$schema": "https://json.schemastore.org/package.json",
3+
"name": "@opencode-ai/client",
4+
"private": true,
5+
"type": "module",
6+
"license": "MIT",
7+
"exports": {
8+
".": "./src/index.ts",
9+
"./effect": "./src/effect.ts"
10+
},
11+
"scripts": {
12+
"generate": "bun run script/build.ts",
13+
"check:generated": "bun run generate && git diff --exit-code -- src/generated src/generated-effect",
14+
"test": "bun test --timeout 5000",
15+
"typecheck": "tsgo --noEmit"
16+
},
17+
"dependencies": {
18+
"@opencode-ai/schema": "workspace:*",
19+
"@opencode-ai/protocol": "workspace:*"
20+
},
21+
"peerDependencies": {
22+
"effect": "4.0.0-beta.83"
23+
},
24+
"peerDependenciesMeta": {
25+
"effect": {
26+
"optional": true
27+
}
28+
},
29+
"devDependencies": {
30+
"@effect/platform-node": "catalog:",
31+
"@opencode-ai/core": "workspace:*",
32+
"@opencode-ai/httpapi-codegen": "workspace:*",
33+
"@opencode-ai/server": "workspace:*",
34+
"@tsconfig/bun": "catalog:",
35+
"@types/bun": "catalog:",
36+
"@typescript/native-preview": "catalog:",
37+
"effect": "catalog:"
38+
}
39+
}

packages/client/script/build.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { NodeFileSystem } from "@effect/platform-node"
2+
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
3+
import { Api } from "@opencode-ai/server/api"
4+
import { Effect } from "effect"
5+
import { HttpApi } from "effect/unstable/httpapi"
6+
import { fileURLToPath } from "url"
7+
8+
const contract = compile(HttpApi.make("opencode-client").add(Api.groups["server.session"]), {
9+
groupNames: { "server.session": "sessions" },
10+
})
11+
12+
await Effect.runPromise(
13+
Effect.all(
14+
[
15+
write(emitPromise(contract), fileURLToPath(new URL("../src/generated", import.meta.url))),
16+
write(
17+
emitEffectImported(contract, { module: "../contract", group: "SessionGroup" }),
18+
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
19+
),
20+
],
21+
{ concurrency: 2, discard: true },
22+
).pipe(Effect.provide(NodeFileSystem.layer)),
23+
)

packages/client/src/contract.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
const Api = makeDefaultApi({
15+
locationMiddleware: LocationMiddleware,
16+
sessionLocationMiddleware: SessionLocationMiddleware,
17+
})
18+
19+
export const SessionGroup = Api.groups["server.session"]

packages/client/src/effect.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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 { Location } from "@opencode-ai/schema/location"
6+
export { Model } from "@opencode-ai/schema/model"
7+
export { Provider } from "@opencode-ai/schema/provider"
8+
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
9+
export { Session } from "@opencode-ai/schema/session"
10+
export { SessionInput } from "@opencode-ai/schema/session-input"
11+
export { SessionMessage } from "@opencode-ai/schema/session-message"
12+
export { Prompt } from "@opencode-ai/schema/prompt"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[
2+
"client-error.ts",
3+
"client.ts",
4+
"index.ts"
5+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Schema } from "effect"
2+
3+
export class ClientError extends Schema.TaggedErrorClass<ClientError>()("ClientError", {
4+
cause: Schema.Defect(),
5+
}) {}

0 commit comments

Comments
 (0)