Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ jobs:
env:
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: ${{ runner.os == 'Windows' && 'true' || 'false' }}

- name: Check generated client
if: runner.os == 'Linux'
working-directory: packages/client
run: bun run check:generated

- name: Run HttpApi exerciser gates
if: runner.os == 'Linux'
working-directory: packages/opencode
Expand Down
77 changes: 77 additions & 0 deletions CONTEXT.md

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# @opencode-ai/client

Private generation target for clients derived directly from OpenCode's authoritative Effect `HttpApi`.

## Entrypoints

- `@opencode-ai/client`: zero-Effect Promise client using `fetch`.
- `@opencode-ai/client/effect`: rich Effect network client using an environment-provided `HttpClient`.

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.

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.

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.

Effect consumers construct canonical decoded inputs:

```ts
import { AbsolutePath, Location, OpenCode, Prompt } from "@opencode-ai/client/effect"

const client = yield * OpenCode.make({ baseUrl: "https://opencode.example" })
yield *
client.sessions.create({
location: Location.Ref.make({ directory: AbsolutePath.make("/workspace") }),
})
yield * client.sessions.prompt({ sessionID, prompt: Prompt.make({ text: "Hello" }) })
```
39 changes: 39 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/client",
"private": true,
"type": "module",
"license": "MIT",
"exports": {
".": "./src/index.ts",
"./effect": "./src/effect.ts"
},
"scripts": {
"generate": "bun run script/build.ts",
"check:generated": "bun run generate && git diff --exit-code -- src/generated src/generated-effect",
"test": "bun test --timeout 5000",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
"@opencode-ai/schema": "workspace:*",
"@opencode-ai/protocol": "workspace:*"
},
"peerDependencies": {
"effect": "4.0.0-beta.83"
},
"peerDependenciesMeta": {
"effect": {
"optional": true
}
},
"devDependencies": {
"@effect/platform-node": "catalog:",
"@opencode-ai/core": "workspace:*",
"@opencode-ai/httpapi-codegen": "workspace:*",
"@opencode-ai/server": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@typescript/native-preview": "catalog:",
"effect": "catalog:"
}
}
23 changes: 23 additions & 0 deletions packages/client/script/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NodeFileSystem } from "@effect/platform-node"
import { compile, emitEffectImported, emitPromise, write } from "@opencode-ai/httpapi-codegen"
import { Api } from "@opencode-ai/server/api"
import { Effect } from "effect"
import { HttpApi } from "effect/unstable/httpapi"
import { fileURLToPath } from "url"

const contract = compile(HttpApi.make("opencode-client").add(Api.groups["server.session"]), {
groupNames: { "server.session": "sessions" },
})

await Effect.runPromise(
Effect.all(
[
write(emitPromise(contract), fileURLToPath(new URL("../src/generated", import.meta.url))),
write(
emitEffectImported(contract, { module: "../contract", group: "SessionGroup" }),
fileURLToPath(new URL("../src/generated-effect", import.meta.url)),
),
],
{ concurrency: 2, discard: true },
).pipe(Effect.provide(NodeFileSystem.layer)),
)
19 changes: 19 additions & 0 deletions packages/client/src/contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { makeDefaultApi } from "@opencode-ai/protocol/api"
import { InvalidRequestError, SessionNotFoundError } from "@opencode-ai/protocol/errors"
import { HttpApiMiddleware } from "effect/unstable/httpapi"

class LocationMiddleware extends HttpApiMiddleware.Service<LocationMiddleware>()(
"@opencode-ai/client/LocationMiddleware",
) {}

class SessionLocationMiddleware extends HttpApiMiddleware.Service<SessionLocationMiddleware>()(
"@opencode-ai/client/SessionLocationMiddleware",
{ error: [InvalidRequestError, SessionNotFoundError] },
) {}

const Api = makeDefaultApi({
locationMiddleware: LocationMiddleware,
sessionLocationMiddleware: SessionLocationMiddleware,
})

export const SessionGroup = Api.groups["server.session"]
12 changes: 12 additions & 0 deletions packages/client/src/effect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// TODO: Keep additional network capabilities inside Schema and Protocol as the client grows; /effect must never import
// Core or Server. Preserve these datatype exports so internal model reorganizations do not require caller migrations.
export * from "./generated-effect/index"
export { Agent } from "@opencode-ai/schema/agent"
export { Location } from "@opencode-ai/schema/location"
export { Model } from "@opencode-ai/schema/model"
export { Provider } from "@opencode-ai/schema/provider"
export { AbsolutePath, RelativePath } from "@opencode-ai/schema/schema"
export { Session } from "@opencode-ai/schema/session"
export { SessionInput } from "@opencode-ai/schema/session-input"
export { SessionMessage } from "@opencode-ai/schema/session-message"
export { Prompt } from "@opencode-ai/schema/prompt"
5 changes: 5 additions & 0 deletions packages/client/src/generated-effect/.httpapi-codegen.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"client-error.ts",
"client.ts",
"index.ts"
]
5 changes: 5 additions & 0 deletions packages/client/src/generated-effect/client-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Schema } from "effect"

export class ClientError extends Schema.TaggedErrorClass<ClientError>()("ClientError", {
cause: Schema.Defect(),
}) {}
Loading
Loading