diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index d542e5bdb..e67d449bb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -38,11 +38,11 @@ using var client = new OpenCodeClient(new OpenCodeClientOptions ## πŸ“‹ Environment Information -**OpenCode.Sdk Version:** +**OpenCodeAI.Sdk Version:** -- Version: (e.g., `0.1.0-nightly.20260831.abc1234`) +- Version: (e.g., `0.8.0-nightly.20260831.abc1234`) - Package source: (NuGet.org / GitHub Packages nightly / built from source at commit …) -- Also using `OpenCode.Sdk.Extensions`? (yes / no) +- Also using `OpenCodeAI.Sdk.Extensions`? (yes / no) **opencode Server:** diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 0556ec71a..44a439600 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -2,7 +2,7 @@ ## Supported Versions -This project is pre-1.0 and has not published a stable release yet. Until `0.1.0` ships, the +This project is pre-1.0 and has not published a stable release yet. Until the first release ships, the supported version is whatever is current on `master` (and the nightly packages built from it). Security patches are prioritized by CVSS v3.0 rating: @@ -41,8 +41,8 @@ suspected vulnerability as a public issue. ## Scope -This policy covers the SDK packages in this repository β€” `OpenCode.Sdk` and -`OpenCode.Sdk.Extensions` β€” and the repository's own tooling and workflows. +This policy covers the SDK packages in this repository β€” `OpenCodeAI.Sdk` and +`OpenCodeAI.Sdk.Extensions` β€” and the repository's own tooling and workflows. Two things are explicitly **out of scope**: diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 1df99eba9..81396d8f6 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -11,7 +11,7 @@ # target: nuget publishes to NuGet.org via Trusted Publishing (OIDC through NuGet/login@v1, no # API key β€” mirrors localstack-dotnet-client's publish-nuget.yml, PR #54). This requires a Trusted # Publishing policy configured on the NuGet.org account that will own this package (package IDs -# OpenCode.Sdk and OpenCode.Sdk.Extensions, repository Blind-Striker/opencode-sdk-dotnet, workflow +# OpenCodeAI.Sdk and OpenCodeAI.Sdk.Extensions, repository Blind-Striker/opencode-sdk-dotnet, workflow # file publish-nuget.yml, no environment) β€” the maintainer must create that policy on nuget.org # BEFORE the first nuget run; until then only target: github succeeds. The NuGet.org profile (or # organization) name the policy is created under must also be set as the repository variable @@ -24,7 +24,7 @@ on: workflow_dispatch: inputs: version: - description: "Exact version to stamp on the packages (e.g. 0.1.0-preview.1 or 0.1.0)" + description: "Exact version to stamp on the packages (e.g. 0.8.0-preview.1 or 0.8.0)" required: true target: type: choice diff --git a/CHANGELOG.md b/CHANGELOG.md index 358d70237..75d45e056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,28 +5,36 @@ Each released version links straight to its GitHub Release tag. ## [Unreleased] -> **The first release.** `0.1.0` has not shipped to NuGet.org yet β€” nightly builds of everything -> below are on [GitHub Packages](README.md#nightly-builds-github-packages) today, versioned -> `0.1.0-nightly.{yyyyMMdd}.{shortSha}` from every code push to `master`. +> **The first release.** Everything below is the surface of the first published package, +> `0.8.0-preview.1`. There is no migration to perform: no earlier version was ever published, and +> the pin moved several times on the way here. Nightly builds of `master` are on +> [GitHub Packages](README.md#nightly-builds-github-packages) as +> `0.8.0-nightly.{yyyyMMdd}.{shortSha}`. ### ✨ New features - **Refreshed to upstream `89f1943`.** Worktree operations now resolve their project through an optional location query. Call create, list, remove, and refresh directly on `WorktreesClient`; - the project-bound `ProjectWorktreesClient` and its factory are gone, which nightly consumers will - meet as a compile break. Create's strategy and destination directory are both optional now, as + the project-bound `ProjectWorktreesClient` and its factory are gone. Create's strategy and + destination directory are both optional now, as upstream declares them; remove retains its required JSON body. The list operation also exposes its declared `WorktreeError` failure arm. Compaction completion payloads now expose the optional model and provider state supplied by the upstream schema. Project updates accept an optional canonical directory; execution interruption events include the upstream inactivity reason. -- **`OpenCode.Sdk` β€” the typed client.** **135 of the 140 operations** in the pinned OpenAPI +- **`OpenCodeAI.Sdk` β€” the typed client.** **135 of the 140 operations** in the pinned OpenAPI snapshot are callable across **28 client families**: sessions, PTYs, persistent PTYs, shells, events, MCP servers, integrations, projects, worktrees, workspaces, providers, language models, agents, skills, commands, forms, permissions, credentials, plugins, RPC, references, VCS, websearch, file system, generation, server, debug, and experimental. Every operation carries a generated request type and a generated response envelope; bound handles (`SessionClient`, `PtyClient`, `PersistentPtyClient`) partially apply a resource id over the shared pipeline. +- **Refreshed to upstream `f9bc223`.** The compaction surface grew: `IProviderCompaction` is a + `mode`-tagged union (`ProviderCompactionLocal`, `ProviderCompactionProvider`, and the usual + `UnknownProviderCompaction` carrier) and hangs off `ModelInfo.Compaction` and + `ProviderInfo.Compaction`. A completed compaction and the `session.compaction.ended` event both + carry an optional `SessionProviderContext`, with its `SessionProviderContextProvenance` record. + Every one of those properties is optional, so nothing that compiled before stops compiling. - **Refreshed to upstream `48f2466`.** The pinned snapshot moved, and the surface moved with it. The live event stream gained its first prefix-tagged arm: every `rpc.*` event dispatches to `EventRpc`, tried after the declared literal tags and before the unknown carrier, and @@ -34,7 +42,7 @@ Each released version links straight to its GitHub Release tag. `v2.plugin.awaitActivation`, `v2.plugin.check`, and `v2.plugin.update` as `PluginsClient.AwaitPluginActivationAsync`, `CheckPluginUpdatesAsync`, and `UpdatePluginsAsync`, and `v2.rpc.call` as `CallAsync` on the new `RpcClient` family. Upstream also removed and - reshaped the plugin types, which nightly consumers will meet as compile breaks: the + reshaped the plugin types: the `plugin.added` event arm is gone, and `PluginAdded` and `PluginAddedData` with it; `IPluginInfo` and its `PluginInfoActive`, `PluginInfoFailed`, and `UnknownPluginInfo` variants are replaced by the `PluginInfo` record, whose `State` is the new `IPluginState` union (`PluginStateActive`, @@ -42,7 +50,7 @@ Each released version links straight to its GitHub Release tag. variant; `PluginSourcePackage.Package` is now `Target`, joined by `Version`, `Outdated`, and `Updating`; and `PluginListResponse.Plugins` is an `IReadOnlyList`. - **A standalone server launcher.** `OpenCodeServer.StartAsync()` starts, monitors, and stops a - private `opencode serve` child β€” generated lease credential, stdin-EOF ownership, bounded tree + private `opencode2 serve` child β€” generated lease credential, stdin-EOF ownership, bounded tree termination β€” and `CreateClient()` hands back a client already bound to it. An optional `OpenCodeServerOutput` collector, supplied through `OpenCodeServerOptions.Output`, retains a bounded tail of the child's stdout and stderr for pull snapshots that report truncation, and it @@ -57,7 +65,16 @@ Each released version links straight to its GitHub Release tag. hand-written WebSocket doors over a shared, family-neutral socket core: read frames, write input, dispose to close. The persistent family adds attach/handoff/snapshot semantics, byte-typed output and checkpoints, and the framed input protocol with viewport tracking. -- **Dependency injection through `OpenCode.Sdk.Extensions`.** `AddOpenCode(Action<…>)` or +- **Terminal connection lifetime a caller can drive.** A connection-owned receiver assembles + frames into an internal queue independently of the caller's enumeration, so canceling a read ends + only that wait: the same healthy connection can be read again, or written to, afterwards. Both + connect options gained an init-only `SendTimeout` (default 30 seconds) bounding each input or + resize send end to end, including the wait for the send gate. Connection termination delivers the + frames it already holds before reporting its outcome, while disposal abandons unread frames and + joins the connection's own work. A persistent PTY keeps its locally requested viewport coherent + with the input it labels, rather than letting an inbound resize report overwrite it. See + [the terminals guide](docs/guide/terminals.md#-cancellation-deadlines-and-disposal). +- **Dependency injection through `OpenCodeAI.Sdk.Extensions`.** `AddOpenCode(Action<…>)` or `AddOpenCode(IConfiguration)` registers one singleton client owning its transport for the container's lifetime, plus every sub-client resolved from that same instance β€” inject `SessionsClient` or `EventsClient` directly. @@ -75,11 +92,6 @@ Each released version links straight to its GitHub Release tag. ### πŸ› οΈ General -- **Breaking (nightly API):** `RpcClient.PostCallAsync` is now `CallAsync`; - `PluginsClient.PostAwaitActivationAsync`, `PostCheckAsync`, and `PostUpdateAsync` are now - `AwaitPluginActivationAsync`, `CheckPluginUpdatesAsync`, and `UpdatePluginsAsync`. Their - `OpenCodeRoutes` members use the corresponding names without `Async`; request/response types and - wire behavior are unchanged. - **Target frameworks:** `netstandard2.0`, `net472`, `net8.0`, `net9.0`, `net10.0` β€” for both packages. The suite runs on `net472` on Windows, real-process launcher tests included, and on `net8.0`/`net9.0`/`net10.0` on all three OSes. `netstandard2.0` is a consumption target rather @@ -90,7 +102,7 @@ Each released version links straight to its GitHub Release tag. [`spec/SNAPSHOT.md`](spec/SNAPSHOT.md). - **Generated output is committed and reviewed as source**, locked by a public-API baseline and verified by regeneration, so a protocol refresh arrives as a readable diff. -- **Test suite:** 5,010 tests green on Windows β€” the fullest leg, and the only one that adds the +- **Test suite:** 5,138 tests green on Windows β€” the fullest leg, and the only one that adds the `net472` assemblies. Linux and macOS run the same suite on the three modern targets. ### πŸ“‹ Important Notes diff --git a/Directory.Build.props b/Directory.Build.props index e0e1d491e..cd1428f0e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,7 +10,7 @@ enable - 0.1.0 + 0.8.0 Deniz Δ°rgin Deniz Δ°rgin Deniz Δ°rgin @@ -51,6 +51,7 @@ + diff --git a/README.md b/README.md index f45bbffff..621f972e9 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # opencode SDK for .NET -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![NuGet](https://img.shields.io/badge/NuGet-coming%20soon-lightgrey)](https://www.nuget.org/packages/OpenCode.Sdk) [![CI](https://github.com/Blind-Striker/opencode-sdk-dotnet/actions/workflows/ci.yml/badge.svg)](https://github.com/Blind-Striker/opencode-sdk-dotnet/actions/workflows/ci.yml) [![Linux Tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.localstackfor.net%2Fbadges%2Ftests%2Flinux%2Fblind-striker%2Fopencode-sdk-dotnet%2Fmaster)](https://api.localstackfor.net/redirect/test-results/linux/blind-striker/opencode-sdk-dotnet/master) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![NuGet](https://img.shields.io/nuget/vpre/OpenCodeAI.Sdk)](https://www.nuget.org/packages/OpenCodeAI.Sdk) [![CI](https://github.com/Blind-Striker/opencode-sdk-dotnet/actions/workflows/ci.yml/badge.svg)](https://github.com/Blind-Striker/opencode-sdk-dotnet/actions/workflows/ci.yml) [![Linux Tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.localstackfor.net%2Fbadges%2Ftests%2Flinux%2Fblind-striker%2Fopencode-sdk-dotnet%2Fmaster)](https://api.localstackfor.net/redirect/test-results/linux/blind-striker/opencode-sdk-dotnet/master) -> **πŸš€ Quick Start**: the first release is upcoming β€” [nightly builds](#-installation) are on GitHub Packages today | [Quick start](#-quick-start) | [Guide](docs/guide/getting-started.md) +> **πŸš€ Quick Start**: [Install](#-installation) | [Quick start](#-quick-start) | [Guide](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/getting-started.md) > **Unofficial.** This project is not affiliated with or endorsed by the > [opencode](https://opencode.ai) team. @@ -16,27 +16,29 @@ so what you call is exactly what the server declares. ## πŸŽ‰ Project Status -**Pre-release, and the surface is complete.** Everything below is landed and covered; the only -thing missing is the first published package. +**Pre-release, and the protocol surface is complete.** Everything below is landed and covered. +What is still outstanding is the first published package and opencode's third connection mode β€” +attaching to a registered background service. - βœ… **135 of 140 operations** callable, across 28 client families β€” sessions, PTYs, persistent PTYs, shells, events, MCP servers, integrations, providers, permissions, credentials, VCS, worktrees, websearch, RPC, and more -- βœ… **5,010 tests** green on Windows β€” the fullest leg, the only one that adds the `net472` +- βœ… **5,138 tests** green on Windows β€” the fullest leg, the only one that adds the `net472` assemblies. Linux and macOS run the same suite on `net8.0`, `net9.0`, and `net10.0` - βœ… **Server-sent event streams**, global and per-session, over the same transport as one-shot calls - βœ… **PTY and persistent-PTY terminal sessions** through hand-written WebSocket doors - βœ… **A launcher** β€” `OpenCodeServer.StartAsync()` starts, monitors, and stops a private - `opencode serve` child for you + `opencode2 serve` child for you - βœ… **Source-generated `System.Text.Json`** with no reflection fallback; both packages declare `IsAotCompatible` on `net10.0` -- πŸ”œ **First release (0.1.0)** β€” packaging is the current work; see [CHANGELOG.md](CHANGELOG.md) +- 🚧 **Pre-1.0 and iterating** β€” released as `0.8.0-preview.N`; the public surface is + locked by a reviewed baseline but may still move before `1.0.0`. See [CHANGELOG.md](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/CHANGELOG.md) - πŸ”œ **Background-service attachment** and an **MCP server** over this SDK β€” both planned, neither started -**Versioning**: the SDK builds against an accepted OpenAPI snapshot, never a live branch. The exact -upstream commit and the refresh procedure live in [`spec/SNAPSHOT.md`](spec/SNAPSHOT.md); today's -pin is `89f1943d` on upstream's `v2` branch. +**Versioning**: the SDK builds against an accepted OpenAPI snapshot of upstream's `v2` branch, +never a live branch. The exact commit and the refresh procedure live in +[`spec/SNAPSHOT.md`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/spec/SNAPSHOT.md), which owns that pin. ## πŸš€ Platform Compatibility & Quality Status @@ -65,8 +67,8 @@ on, and the `net472` leg is what exercises its compile surface. | Package | NuGet.org | GitHub Packages | |---------|-----------|-----------------| -| **OpenCode.Sdk** | [![NuGet](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.localstackfor.net%2Fbadges%2Fpackages%2Fnuget%2FOpenCode.Sdk%3Fprerelease%3Dtrue)](https://www.nuget.org/packages/OpenCode.Sdk) | [![GitHub Packages](https://img.shields.io/badge/GitHub%20Packages-nightly-blue)](https://github.com/Blind-Striker/opencode-sdk-dotnet/pkgs/nuget/OpenCode.Sdk) | -| **OpenCode.Sdk.Extensions** | [![NuGet](https://img.shields.io/endpoint?url=https%3A%2F%2Fapi.localstackfor.net%2Fbadges%2Fpackages%2Fnuget%2FOpenCode.Sdk.Extensions%3Fprerelease%3Dtrue)](https://www.nuget.org/packages/OpenCode.Sdk.Extensions) | [![GitHub Packages](https://img.shields.io/badge/GitHub%20Packages-nightly-blue)](https://github.com/Blind-Striker/opencode-sdk-dotnet/pkgs/nuget/OpenCode.Sdk.Extensions) | +| **OpenCodeAI.Sdk** | [![NuGet](https://img.shields.io/nuget/vpre/OpenCodeAI.Sdk)](https://www.nuget.org/packages/OpenCodeAI.Sdk) | [![GitHub Packages](https://img.shields.io/badge/GitHub%20Packages-nightly-blue)](https://github.com/Blind-Striker/opencode-sdk-dotnet/pkgs/nuget/OpenCodeAI.Sdk) | +| **OpenCodeAI.Sdk.Extensions** | [![NuGet](https://img.shields.io/nuget/vpre/OpenCodeAI.Sdk.Extensions)](https://www.nuget.org/packages/OpenCodeAI.Sdk.Extensions) | [![GitHub Packages](https://img.shields.io/badge/GitHub%20Packages-nightly-blue)](https://github.com/Blind-Striker/opencode-sdk-dotnet/pkgs/nuget/OpenCodeAI.Sdk.Extensions) | ## Table of Contents @@ -103,12 +105,17 @@ on, and the `net472` leg is what exercises its compile surface. ## Prerequisites -You need an `opencode` server. Install the CLI -([opencode.ai](https://opencode.ai) β€” the v2 line ships as `@opencode-ai/cli@next` and installs the -`opencode2` command), then either run it yourself: +You need an `opencode` server. The v2 line ships as `@opencode-ai/cli@next` and installs a single +command, `opencode2` ([opencode.ai](https://opencode.ai)): ```sh -OPENCODE_SERVER_PASSWORD=your-password opencode2 serve --hostname 127.0.0.1 --port 4096 +npm install -g @opencode-ai/cli@next +``` + +Then either run it yourself: + +```sh +OPENCODE_PASSWORD=your-password opencode2 serve --hostname 127.0.0.1 --port 4096 ``` …or let the SDK start one for you β€” that is what `OpenCodeServer.StartAsync()` in the @@ -118,19 +125,22 @@ OPENCODE_SERVER_PASSWORD=your-password opencode2 serve --hostname 127.0.0.1 --po ### Stable (NuGet.org) -**The first release is upcoming.** Nothing is on NuGet.org yet; when `0.1.0` ships, installation -will be the usual: - ```bash -dotnet add package OpenCode.Sdk -dotnet add package OpenCode.Sdk.Extensions # dependency injection, optional +dotnet add package OpenCodeAI.Sdk --prerelease +dotnet add package OpenCodeAI.Sdk.Extensions --prerelease # dependency injection, optional ``` -Until then, use the nightly feed below. +> **The package id is not the namespace.** You install `OpenCodeAI.Sdk`, and you write +> `using OpenCode.Sdk;` β€” the assemblies and every public namespace are `OpenCode.Sdk`. +> nuget.org reserves the `OpenCode.` id prefix for an unrelated owner, so the artifact carries a +> different name than the code inside it. + +The version badge above shows the latest published prerelease. Nightly builds of `master` are on +the GitHub Packages feed below. ### Nightly builds (GitHub Packages) -Every code push to `master` publishes `0.1.0-nightly.{yyyyMMdd}.{shortSha}` to GitHub Packages: +Every code push to `master` publishes `0.8.0-nightly.{yyyyMMdd}.{shortSha}` to GitHub Packages: ```bash # Add the GitHub Packages source (PAT: classic token with the read:packages scope) @@ -141,8 +151,8 @@ dotnet nuget add source https://nuget.pkg.github.com/Blind-Striker/index.json \ --store-password-in-clear-text # Install the nightly packages -dotnet add package OpenCode.Sdk --prerelease --source github-opencode-sdk -dotnet add package OpenCode.Sdk.Extensions --prerelease --source github-opencode-sdk +dotnet add package OpenCodeAI.Sdk --prerelease --source github-opencode-sdk +dotnet add package OpenCodeAI.Sdk.Extensions --prerelease --source github-opencode-sdk ``` Prefer keeping the token out of shell history? Commit a `nuget.config` next to your solution and @@ -176,7 +186,7 @@ keep the credentials in environment variables: ### The SDK starts the server -No ambient process, no endpoint to configure β€” the launcher starts a private `opencode serve` +No ambient process, no endpoint to configure β€” the launcher starts a private `opencode2 serve` child, mints its credential, and hands you a client bound to it. Disposing the server stops the child. To keep the child's output for diagnostics, pass an `OpenCodeServerOutput` collector in `OpenCodeServerOptions.Output` and read its snapshot whenever you like β€” bounded, and still @@ -208,7 +218,7 @@ await foreach (var @event in client.Events.SubscribeAsync(window.Token)) using var client = new OpenCodeClient(new OpenCodeClientOptions { Endpoint = new Uri("http://127.0.0.1:4096"), - Password = Environment.GetEnvironmentVariable("OPENCODE_SERVER_PASSWORD"), + Password = Environment.GetEnvironmentVariable("OPENCODE_PASSWORD"), }); var health = await client.GetHealthAsync(); @@ -235,7 +245,7 @@ var builder = Host.CreateApplicationBuilder(args); builder.Services.AddOpenCode(options => { options.Endpoint = new Uri("http://127.0.0.1:4096"); - options.Password = Environment.GetEnvironmentVariable("OPENCODE_SERVER_PASSWORD"); + options.Password = Environment.GetEnvironmentVariable("OPENCODE_PASSWORD"); }); builder.Services.AddHostedService(); @@ -264,7 +274,7 @@ overload shown above. **135 of the 140 operations** in the pinned snapshot are callable. The remaining five are not omissions β€” each one is a recorded decision with a named cause, and -[`src/OpenCode.Sdk/.generation-incomplete`](src/OpenCode.Sdk/.generation-incomplete) is the +[`src/OpenCode.Sdk/.generation-incomplete`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/src/OpenCode.Sdk/.generation-incomplete) is the machine-readable map that the build itself reads. **Three operations are declined**, because admitting them would mean inventing a contract upstream @@ -282,31 +292,29 @@ does not declare: **Two operations are transport-owned**: `v2.pty.connect` and `v2.persistentPty.connect` are WebSocket upgrades that the HTTP pipeline cannot carry. They are fully usable β€” through the hand-written `PtySession` and `PersistentPtySession` doors described in -[the terminals guide](docs/guide/terminals.md) β€” they simply are not generated. +[the terminals guide](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/terminals.md) β€” they simply are not generated. ## πŸ“š Documentation | Guide | What it covers | |---|---| -| [**The guide**](docs/guide/README.md) | Index of every page below, in reading order | -| [Getting started](docs/guide/getting-started.md) | Install, first call, and the shape of the client family | -| [Connection modes](docs/guide/connection-modes.md) | The standalone launcher, an external server, and DI registration | -| [Streaming](docs/guide/streaming.md) | The global event bus and per-session server-sent event streams | -| [Terminals](docs/guide/terminals.md) | PTY and persistent-PTY sessions over the WebSocket doors | -| [Errors and responses](docs/guide/errors-and-responses.md) | Throwing versus `NoThrow`, and the typed error model | -| [Pagination](docs/guide/pagination.md) | Cursor-carrying list envelopes and `EnumerateMessagesAsync` | +| [**The guide**](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/README.md) | Index of every page below, in reading order | +| [Getting started](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/getting-started.md) | Install, first call, and the shape of the client family | +| [Connection modes](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/connection-modes.md) | The standalone launcher, an external server, and DI registration | +| [Streaming](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/streaming.md) | The global event bus and per-session server-sent event streams | +| [Terminals](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/terminals.md) | PTY and persistent-PTY sessions over the WebSocket doors | +| [Errors and responses](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/errors-and-responses.md) | Throwing versus `NoThrow`, and the typed error model | +| [Pagination](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/pagination.md) | Cursor-carrying list envelopes and `EnumerateMessagesAsync` | -Architecture, decision records, and engineering policy live under [`docs/`](docs) β€” start at -[`AGENTS.md`](AGENTS.md) if you want the internals rather than the API. +Architecture, decision records, and engineering policy live under [`docs/`](https://github.com/Blind-Striker/opencode-sdk-dotnet/tree/master/docs) β€” start at +[`AGENTS.md`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/AGENTS.md) if you want the internals rather than the API. ## Known Issues -- **Nothing is on NuGet.org yet.** The `0.1.0` package is prepared but unpublished β€” - publication is currently blocked by an upstream package-ID prefix reservation dispute β€” so - `dotnet add package OpenCode.Sdk` will not resolve. Use the - [GitHub Packages nightly feed](#nightly-builds-github-packages) until the first stable release; - the nightly and stable packages are built from the same sources by two workflows that share the - same verify-and-pack steps. +- **The package id differs from the assembly name.** nuget.org reserves the `OpenCode.` id prefix + for an unrelated owner, so the packages publish as `OpenCodeAI.Sdk` and + `OpenCodeAI.Sdk.Extensions` while the assemblies and every public namespace stay `OpenCode.Sdk`. + Nothing about your code changes: only the `PackageReference` carries the `OpenCodeAI` name. - **Response bodies larger than 1 MB allocate an extra copy on `net472` and `netstandard2.0`.** The downlevel array pool caps its buckets at 1 MB, so a rent above that cap falls through to a @@ -319,7 +327,7 @@ Architecture, decision records, and engineering policy live under [`docs/`](docs reuses it across messages and consumer read enumerations. Pooling remains a benchmark-gated follow-up; repeated reads on the same connection do not allocate another receive buffer. The receiver also queues undelivered frames, so slow or absent consumers can grow memory; see the - [terminal lifetime contract](docs/architecture/client-runtime.md). + [terminal lifetime contract](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/architecture/client-runtime.md). - **Attaching to an existing background service is not implemented.** opencode's third connection mode β€” discovering a registered daemon through its registration file (`Service.discover` / @@ -331,19 +339,19 @@ Architecture, decision records, and engineering policy live under [`docs/`](docs stream: events published while you are disconnected are gone, and a consumer slower than the producer can overflow and fail the stream. This is the server's contract, not an SDK limitation β€” if you need durable history for one session, use the per-session log stream instead - ([streaming guide](docs/guide/streaming.md)). + ([streaming guide](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/guide/streaming.md)). - **Running the in-repo sandbox against a different server needs `--no-launch-profile`.** The checked-in `launchSettings.json` prefills `OPENCODE_SANDBOX_ENDPOINT` at port 4096, and `dotnet run` applies the default profile unless told otherwise β€” so without the flag the sandbox silently addresses 4096 whatever your environment says. The prefill stays deliberately: it is what makes zero-argument F5 work against a local server. See - [`tests/OpenCode.Sdk.Sandbox/README.md`](tests/OpenCode.Sdk.Sandbox/README.md). + [`tests/OpenCode.Sdk.Sandbox/README.md`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/tests/OpenCode.Sdk.Sandbox/README.md). ## Developing We appreciate contributions in the form of feedback, bug reports, and pull requests. Read -[CONTRIBUTING.md](.github/CONTRIBUTING.md) first β€” it carries the full gate and the commit +[CONTRIBUTING.md](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/.github/CONTRIBUTING.md) first β€” it carries the full gate and the commit convention. ### Building the Project @@ -365,10 +373,10 @@ provisions the matching official ripgrep archive and verifies its SHA-256 digest ### Sandbox Application -[`tests/OpenCode.Sdk.Sandbox`](tests/OpenCode.Sdk.Sandbox) is a committed playground that drives +[`tests/OpenCode.Sdk.Sandbox`](https://github.com/Blind-Striker/opencode-sdk-dotnet/tree/master/tests/OpenCode.Sdk.Sandbox) is a committed playground that drives the SDK against a real `opencode2 serve` under a debugger β€” the standing breadth walkthrough, the SSE stream modes, the PTY legs, and the standalone-launcher demo. Its -[README](tests/OpenCode.Sdk.Sandbox/README.md) documents every mode. +[README](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/tests/OpenCode.Sdk.Sandbox/README.md) documents every mode. ```bash dotnet run --project tests/OpenCode.Sdk.Sandbox -- --standalone @@ -381,7 +389,7 @@ dotnet test --configuration Release --no-build ``` The full completion gate β€” analyzers, formatting, and the suite β€” is -[`docs/engineering/quality-gates.md`](docs/engineering/quality-gates.md). +[`docs/engineering/quality-gates.md`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/docs/engineering/quality-gates.md). ## Community @@ -392,8 +400,10 @@ questions, and proposals all land there for now. ## Changelog -Please refer to [`CHANGELOG.md`](CHANGELOG.md) to see the complete list of changes for each release. +Please refer to [`CHANGELOG.md`](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/CHANGELOG.md) to see the complete list of changes for each release. ## License -Licensed under MIT, see [LICENSE](LICENSE) for the full text. +Licensed under MIT, see [LICENSE](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/LICENSE) for the full text. Content derived from +upstream opencode carries its own notice in +[THIRD-PARTY-NOTICES.md](https://github.com/Blind-Striker/opencode-sdk-dotnet/blob/master/THIRD-PARTY-NOTICES.md). diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md new file mode 100644 index 000000000..97ac065a8 --- /dev/null +++ b/THIRD-PARTY-NOTICES.md @@ -0,0 +1,46 @@ +# Third-Party Notices + +This repository's own code is licensed under the MIT License in [`LICENSE`](LICENSE). It also +carries content derived from a third-party project, reproduced here with that project's notice as +its license requires. + +## opencode + +Upstream project: [anomalyco/opencode](https://github.com/anomalyco/opencode) + +Derived content in this repository: + +- `spec/openapi.json` β€” the accepted snapshot of upstream's `packages/protocol/openapi.json`, at + the exact commit [`spec/SNAPSHOT.md`](spec/SNAPSHOT.md) pins. It is produced from upstream's own + generator, so it is a derived copy rather than an independent description. +- `spec/patches/` β€” snapshot-production patches whose context lines quote upstream TypeScript + source. The patches themselves are this repository's work; the quoted context is upstream's. +- `external/opencode` β€” a submodule reference. Nothing of it is redistributed by this repository; + a clone fetches it from upstream directly. + +Generated SDK source under `src/OpenCode.Sdk/` is emitted by this repository's own generator from +the accepted snapshot. Upstream's notice follows. + +``` +MIT License + +Copyright (c) 2025 opencode + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 4184e097e..58430bfc7 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -28,8 +28,9 @@ accepted OpenAPI snapshot and rides one hand-written transport runtime. targets. Linux and macOS live verification also passed on net8/net9/net10, including the persistent daemon round trip and normal PTY reuse after read cancellation. `architecture/client-runtime.md` and ADR-0023 own the contract. -- **Packages** β€” `OpenCode.Sdk` and `OpenCode.Sdk.Extensions` pack at the single-sourced - `VersionPrefix 0.1.0`, and every `master` push publishes a `0.1.0-nightly.*` build to GitHub +- **Packages** β€” the two packages publish as `OpenCodeAI.Sdk` and `OpenCodeAI.Sdk.Extensions` + (the assemblies stay `OpenCode.Sdk`) and pack at the single-sourced + `VersionPrefix 0.8.0`, and every `master` push publishes a `0.8.0-nightly.*` build to GitHub Packages. NuGet.org publication is currently blocked by an upstream prefix reservation dispute; the manual publish lane is wired and waits on it. @@ -89,8 +90,9 @@ is revisited at each boundary. value, a numeric range and a file path both invisible behind bare strings, a WebSocket close code overloaded across two causes, and two declared arms the handler cannot produce. Findings stay diagnostic and never feed generation or curation (ADR-0013). -- **Release mechanics** β€” ADR-0006's shape is wired. Open: the release-notes flow and the - prerelease-versus-stable wording for the first tagged release. +- **Release mechanics** β€” ADR-0006's shape is wired, and the first tagged release ships as a + `0.8.0-preview.N` prerelease, iterating the preview and patch numbers toward `1.0.0`. Open: + the release-notes flow. - **Deferred design questions, each parked behind a named trigger** β€” splitting validated client configuration from the transport factory (reopens when M6 attaches telemetry or hooks, or when Extensions gains a concrete named-client need); a parent-mediated handle door for flat diff --git a/docs/architecture/client-runtime.md b/docs/architecture/client-runtime.md index 2aa3a4ec8..0824de1d9 100644 --- a/docs/architecture/client-runtime.md +++ b/docs/architecture/client-runtime.md @@ -340,7 +340,8 @@ local server launcher. Protocol and generated-model rules live in `OpenCodeServer.StartAsync(OpenCodeServerOptions?, CancellationToken)` is the standalone door (upstream `Standalone.start` parity), hand-written over `System.Diagnostics.Process` with no process-management dependency (ADR-0001). Every call is always a fresh private server on port -zero: the caller's `Command` plus `--stdio --port 0` is the argv, and a freshly generated lease +zero: the caller's `Command` β€” `opencode2 serve` by default, the single executable the line +this SDK speaks to installs β€” plus `--stdio --port 0` is the argv, and a freshly generated lease credential is injected into the child environment as `OPENCODE_PASSWORD`, after any caller-supplied `Environment` entries so it can never be shadowed. Readiness is the single JSON stdout line the child prints once fully booted; stdin stays open as the ownership lease for as long as the server diff --git a/docs/architecture/platform-and-packaging.md b/docs/architecture/platform-and-packaging.md index 5fb896189..810b0678e 100644 --- a/docs/architecture/platform-and-packaging.md +++ b/docs/architecture/platform-and-packaging.md @@ -43,10 +43,15 @@ Two separate API vintages shape the downlevel legs, and conflating them would mi ## Packages -- `OpenCode.Sdk` is the core typed client. The local server launcher belongs in this package when - its milestone lands (ADR-0001); `docs/ROADMAP.md` owns delivery status. +- `OpenCode.Sdk` is the core typed client, and the local server launcher ships inside it + (ADR-0001). - `OpenCode.Sdk.Extensions` owns dependency-injection registration. DI dependencies do not enter the core package. +- The two names above are assembly and namespace names. The published package ids are + `OpenCodeAI.Sdk` and `OpenCodeAI.Sdk.Extensions`, because nuget.org reserves the `OpenCode.` + prefix for an unrelated owner. Each packable project sets its own `PackageId`; the assemblies, + the public namespaces, and therefore consumer source are unaffected, and a project reference + packs as a dependency on the corresponding package id. - Exact package references and dependency versions are read from project files and `Directory.Packages.props`. Documentation records policy, not a second version inventory. - A future package is added only for a real distribution boundary; repository layout alone does diff --git a/docs/engineering/workflow.md b/docs/engineering/workflow.md index cf284dd14..3722e3a1c 100644 --- a/docs/engineering/workflow.md +++ b/docs/engineering/workflow.md @@ -14,7 +14,7 @@ history. - Align on structure and direction before writing. Prefer the smallest correct change over a broad refactor. - When implementation contradicts current canon, stop the affected work and follow - `../agents/deviation-protocol.md`; never silently code around the contradiction. + `deviation-protocol.md`; never silently code around the contradiction. ## Repository artifacts and temporary work diff --git a/docs/guide/README.md b/docs/guide/README.md index 69f573808..529adebff 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -1,5 +1,7 @@ # πŸ“š The opencode SDK guide +Date: 2026-08-31 + Six pages for people **calling** the SDK. Read them in order the first time: install the package and make a call, decide how you reach a server, then the three subsystems that behave differently from ordinary request/response calls β€” event streams, terminals, and cursor lists β€” plus the error diff --git a/docs/guide/connection-modes.md b/docs/guide/connection-modes.md index 1f07f8689..4af29f9c2 100644 --- a/docs/guide/connection-modes.md +++ b/docs/guide/connection-modes.md @@ -1,5 +1,7 @@ # πŸ”Œ Connection modes +Date: 2026-09-08 + There are two ways to get a client bound to a running opencode server: **let the SDK start one**, or **point it at one you already have**. Dependency injection is not a third way in β€” it is how you register either of them with a container. @@ -11,7 +13,7 @@ register either of them with a container. ## πŸš€ The SDK starts the server -`OpenCodeServer.StartAsync()` launches a private `opencode serve` child, waits for it to report +`OpenCodeServer.StartAsync()` launches a private `opencode2 serve` child, waits for it to report readiness, mints its credential, and hands you an owner object. No ambient process, no endpoint to configure, no port to pick. @@ -45,7 +47,7 @@ it started: ```csharp await using var server = await OpenCodeServer.StartAsync(new OpenCodeServerOptions { - Command = ["opencode2", "serve"], + Command = ["/opt/opencode/bin/opencode2", "serve"], WorkingDirectory = "/srv/my-project", Environment = new Dictionary(StringComparer.Ordinal) { ["OPENCODE_LOG_LEVEL"] = "debug" }, ReadinessTimeout = TimeSpan.FromSeconds(90), @@ -55,7 +57,7 @@ await using var server = await OpenCodeServer.StartAsync(new OpenCodeServerOptio | Option | Default | What it does | |---|---|---| -| `Command` | `["opencode", "serve"]` | The executable plus its leading arguments. The launcher appends `--stdio --port 0` itself. | +| `Command` | `["opencode2", "serve"]` | The executable plus its leading arguments. The launcher appends `--stdio --port 0` itself. | | `WorkingDirectory` | `null` | The child's working directory; `null` inherits yours. | | `Environment` | `null` | Extra environment entries for the child. | | `ReadinessTimeout` | 60 s | How long to wait for the readiness line before failing and ending the child. | @@ -99,7 +101,7 @@ var endpoint = new Uri("http://127.0.0.1:4096"); using var client = new OpenCodeClient(new OpenCodeClientOptions { Endpoint = endpoint, - Password = Environment.GetEnvironmentVariable("OPENCODE_SERVER_PASSWORD"), + Password = Environment.GetEnvironmentVariable("OPENCODE_PASSWORD"), }); using var probe = new CancellationTokenSource(TimeSpan.FromSeconds(5)); @@ -116,23 +118,25 @@ the SDK carries no version comparand of its own and no network-timeout knob yet, `CancellationTokenSource` is the honest timeout and your own expectation is the honest version check. -**About `OPENCODE_SERVER_PASSWORD`**: that is the variable *the opencode CLI* reads when you start +**About `OPENCODE_PASSWORD`**: that is the variable *the opencode CLI* reads when you start a server with authentication β€” ```sh -OPENCODE_SERVER_PASSWORD=your-password opencode2 serve --hostname 127.0.0.1 --port 4096 +OPENCODE_PASSWORD=your-password opencode2 serve --hostname 127.0.0.1 --port 4096 ``` -β€” and the client must present the same value as its Basic password. The SDK never reads it, or any -other environment variable, for you. Reading it in the snippet above is your application's choice; -a configuration section or a secret store works exactly as well. A server started **without** a -password expects anonymous requests, so leave `Password` as `null` for one. +β€” and the client must present the same value as its Basic password. `OPENCODE_SERVER_PASSWORD` is +the CLI's legacy name for the same value, still honored as a fallback, so an older setup keeps +working. The SDK never reads either one, or any other environment variable, for you. Reading it in +the snippet above is your application's choice; a configuration section or a secret store works +exactly as well. A server started **without** a password expects anonymous requests, so leave +`Password` as `null` for one. ## 🧩 Registering with dependency injection `OpenCode.Sdk.Extensions` adds `AddOpenCode` to `IServiceCollection`. What lands in the container is deliberately small: **one `OpenCodeClient` singleton** holding the transport open for the -container's lifetime, and **each of the 27 families registered as its own singleton resolved from +container's lifetime, and **each of the 28 families registered as its own singleton resolved from that one client**. A service therefore asks for the family it actually uses β€” `EventsClient`, `PtysClient`, `WorktreesClient` β€” and all of them share a single pipeline and a single disposal at shutdown. diff --git a/docs/guide/errors-and-responses.md b/docs/guide/errors-and-responses.md index ae4ee8f79..b75fcd369 100644 --- a/docs/guide/errors-and-responses.md +++ b/docs/guide/errors-and-responses.md @@ -1,5 +1,7 @@ # 🧱 Errors and responses +Date: 2026-08-31 + Every one-shot call answers with a **response envelope** that carries the same four members no matter which operation produced it, and every failure is either a typed API error you can branch on or a transport failure you cannot. Nothing is stringly typed, and nothing is swallowed. diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index bbd2e4b96..01d3cc5b0 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -1,6 +1,6 @@ # πŸš€ Getting started -Date: 2026-09-06 +Date: 2026-09-08 Install the package, point a client at a server, and make three calls. Ten minutes, and the last one talks to a model. @@ -13,9 +13,18 @@ one talks to a model. ## πŸ“¦ Install -**Nothing is on NuGet.org yet** β€” `0.1.0` is prepared but unpublished. Where the packages can be -had in the meantime, the exact `dotnet nuget add source` command, the two package names, and the -`read:packages` token requirement all live in one place so they never drift: +```bash +dotnet add package OpenCodeAI.Sdk --prerelease +dotnet add package OpenCodeAI.Sdk.Extensions --prerelease # dependency injection, optional +``` + +> **The package id is not the namespace.** You install `OpenCodeAI.Sdk` and you write +> `using OpenCode.Sdk;` β€” nuget.org reserves the `OpenCode.` id prefix for an unrelated owner, so +> the artifact carries a different name than the code inside it. + +The packages target `netstandard2.0`, `net472`, `net8.0`, `net9.0`, and `net10.0`, so any project +on one of those works. Nightly builds of `master` live on a GitHub Packages feed; the source +command and its `read:packages` token requirement live in one place so they never drift: [**Installation** in the root README](../../README.md#-installation). You also need an `opencode` server. Either install the CLI and run one yourself, or let the SDK @@ -36,7 +45,7 @@ using OpenCode.Sdk.Models; using var client = new OpenCodeClient(new OpenCodeClientOptions { Endpoint = new Uri("http://127.0.0.1:4096"), - Password = Environment.GetEnvironmentVariable("OPENCODE_SERVER_PASSWORD"), + Password = Environment.GetEnvironmentVariable("OPENCODE_PASSWORD"), }); ``` @@ -54,7 +63,7 @@ the server's working directory. Session listing filters with `SessionListRequest `Project`, independently of these headers. See the [location contract](../architecture/client-runtime.md#location) for ambient and per-call header behavior. -> **πŸ”‘ The SDK reads no environment variables of its own.** `OPENCODE_SERVER_PASSWORD` above is +> **πŸ”‘ The SDK reads no environment variables of its own.** `OPENCODE_PASSWORD` above is > *your* code reading *your* environment β€” exactly how opencode's own CLI layers it. Options are > snapshotted at construction, so changing the environment later never reaches a live client. @@ -99,11 +108,11 @@ Console.WriteLine(generated.Generate.Text); ## 🧭 How the client is organised -The root client exposes **27 families** as properties β€” `Sessions`, `Events`, `Ptys`, +The root client exposes **28 families** as properties β€” `Sessions`, `Events`, `Ptys`, `PersistentPtys`, `Shells`, `Providers`, `LanguageModels`, `Agents`, `Skills`, `Commands`, `Permissions`, `Credentials`, `Projects`, `Workspaces`, `Worktrees`, `Vcs`, `FileSystem`, `Forms`, -`Generation`, `Integrations`, `McpServers`, `Plugins`, `References`, `Server`, `Websearch`, -`Debug`, and `Experimental`: +`Generation`, `Integrations`, `McpServers`, `Plugins`, `References`, `Rpc`, `Server`, +`Websearch`, `Debug`, and `Experimental`: ```csharp var providers = await client.Providers.ListProvidersAsync(); diff --git a/docs/guide/pagination.md b/docs/guide/pagination.md index 0d1918ba6..ad5ff4350 100644 --- a/docs/guide/pagination.md +++ b/docs/guide/pagination.md @@ -1,5 +1,7 @@ # πŸ“‘ Pagination +Date: 2026-08-31 + Two operations in the API return more than fits in one response, and both use the same opaque cursor envelope: **listing a session's messages** and **listing sessions**. Everything else returns its whole answer at once. diff --git a/docs/guide/terminals.md b/docs/guide/terminals.md index 43e401ad1..9b975e381 100644 --- a/docs/guide/terminals.md +++ b/docs/guide/terminals.md @@ -1,5 +1,7 @@ # πŸ–₯️ Terminals +Date: 2026-09-08 + opencode has two terminal families, and they are genuinely different animals: | | **PTY** | **Persistent PTY** | @@ -18,6 +20,7 @@ generated from an OpenAPI document. - [🧡 A normal PTY, end to end](#-a-normal-pty-end-to-end) - [♻️ Resuming with a cursor](#️-resuming-with-a-cursor) - [🧷 Persistent PTYs](#-persistent-ptys) +- [πŸ›‘ Cancellation, deadlines, and disposal](#-cancellation-deadlines-and-disposal) - [πŸͺŸ The Windows platform note](#-the-windows-platform-note) - [🎫 Connect tokens](#-connect-tokens) @@ -64,7 +67,9 @@ Three rules worth knowing before your first surprise: 1. **Enter is `\r`.** `WriteAsync` sends exactly the bytes you give it. A command ending in `\n` renders on the terminal and then sits there forever, unsubmitted. 2. **One read at a time.** A session carries one active read enumeration; starting a second - concurrently throws `InvalidOperationException`. Writes are serialized for you. + concurrently throws `InvalidOperationException`. Writes are serialized for you. Canceling a read + does not end the connection β€” see + [cancellation, deadlines, and disposal](#-cancellation-deadlines-and-disposal). 3. **There is no end-of-command marker on this wire.** A terminal just goes quiet. If you need to know a command finished, either wait for the stream to settle or ask `GetPtyAsync` for the PTY's status and exit code β€” the exit code is *not* on the socket. @@ -184,6 +189,80 @@ The collection client also carries the daemon's lifecycle doors: `HandoffAsync` to outlive this server until a replacement claims it, and `ShutdownAsync` stops the daemon **and every terminal it owns** β€” not just one. +## πŸ›‘ Cancellation, deadlines, and disposal + +Both terminal families share one lifetime model. This is what it means for a caller; the exact +contract is owned by [the client runtime architecture](../architecture/client-runtime.md). + +**Canceling a read ends the read, not the connection.** A connection-owned receiver assembles +frames independently of your enumeration, so the token you hand `ReadAsync` bounds *your* wait and +nothing else. Stop reading, send input, start a new enumeration on the same session β€” it still +works: + +```csharp +using (var window = new CancellationTokenSource(TimeSpan.FromSeconds(5))) +{ + try + { + await foreach (var frame in terminal.ReadAsync(window.Token)) + { + // …stop once you have seen enough + } + } + catch (OperationCanceledException) + { + // the wait ended; the connection did not + } +} + +await terminal.WriteAsync("echo again\r"); + +await foreach (var frame in terminal.ReadAsync(cancellationToken)) +{ + // same connection, still healthy +} +``` + +This is an SDK contract, not something `IAsyncEnumerable` gives you for free, and not an upstream +cancel/resume API. "One read at a time" still holds β€” it is a *concurrent* second enumeration that +throws, not a later one. + +**Frames you have not read are queued, and the queue is not history.** The receiver holds only what +it has not handed you yet, so a connection nobody reads grows in memory; the queue is deliberately +unbounded until a real workload argues for a limit. It is not a replay buffer either β€” to go back, +reconnect with a [cursor](#️-resuming-with-a-cursor). + +**When the connection ends, you still get what it already had.** A remote close or a transport +failure hands you the queued frames in order first, then completes normally or throws. That ending +is stable: a later read reports the same outcome rather than a fresh error. + +**Disposal is the other path, and it does not wait for you.** `DisposeAsync` closes the socket, +abandons unread frames, and joins the connection's own work. Reads after it are empty, writes throw +`ObjectDisposedException`, and two callers disposing at once await the same cleanup. + +**Sends carry their own deadline.** `PtyConnectOptions.SendTimeout` and +`PersistentPtyConnectOptions.SendTimeout` default to 30 seconds and bound each input or resize send +end to end, the wait for the send gate included: + +```csharp +await using var bounded = await pty.ConnectAsync(new PtyConnectOptions +{ + SendTimeout = TimeSpan.FromSeconds(5), +}); +``` + +It is a fixed total per call β€” progress does not restart it β€” and it is not a connect timeout, an +idle timeout, or a command-execution timeout. Expiry while the call is still queued fails that call +alone; expiry once the bytes are going out ends the attachment, because whether the server saw them +is no longer knowable. Nothing is retried. Your own cancellation stays +`OperationCanceledException`; the SDK's deadline is an `OpenCodeTransportException` naming a +timeout. + +**A persistent PTY keeps your viewport intent.** The outbound size starts at the attachment's size +and moves when a `ResizeAsync` send succeeds. A `PersistentPtyResizedFrame` tells you how the +server is rendering β€” it does not overwrite what you asked for, and every input you send carries a +size coherent with its place in the send order. + ## πŸͺŸ The Windows platform note **Persistent PTYs do not work on Windows hosts.** At the pinned snapshot, upstream's `opencode-pty` diff --git a/spec/SNAPSHOT.md b/spec/SNAPSHOT.md index 1f52d760f..e2fe0fd86 100644 --- a/spec/SNAPSHOT.md +++ b/spec/SNAPSHOT.md @@ -22,6 +22,9 @@ scope from `@opencode-ai`. The published channel has not moved with it: npm's `@ carries a reserved placeholder on `latest`, so `@opencode-ai/cli@next` stays the installable pre-release and is what the row above names. +`openapi.json` is derived from upstream's own generator output, so upstream's MIT notice +travels with it in [`THIRD-PARTY-NOTICES.md`](../THIRD-PARTY-NOTICES.md). + Platform evidence for the v2 line: internal research, 2026-08-16, "The opencode v2 platform: branch, surface, architecture, distribution". diff --git a/src/OpenCode.Sdk.Extensions/OpenCode.Sdk.Extensions.csproj b/src/OpenCode.Sdk.Extensions/OpenCode.Sdk.Extensions.csproj index 048339c86..77e5fbc4e 100644 --- a/src/OpenCode.Sdk.Extensions/OpenCode.Sdk.Extensions.csproj +++ b/src/OpenCode.Sdk.Extensions/OpenCode.Sdk.Extensions.csproj @@ -2,8 +2,10 @@ $(AllTargetFrameworks) - Dependency injection integration for the unofficial opencode .NET SDK. - opencode;sdk;dependency-injection;options + + OpenCodeAI.Sdk.Extensions + Dependency injection integration for the unofficial opencode .NET SDK: registers one singleton client and every sub-client from it. The package id is OpenCodeAI.Sdk.Extensions; the assembly and every namespace are OpenCode.Sdk. + opencode;opencode2;sdk;dependency-injection;di;options;hosting;dotnet;csharp true diff --git a/src/OpenCode.Sdk/OpenCode.Sdk.csproj b/src/OpenCode.Sdk/OpenCode.Sdk.csproj index 83b32a04b..be3af7a4a 100644 --- a/src/OpenCode.Sdk/OpenCode.Sdk.csproj +++ b/src/OpenCode.Sdk/OpenCode.Sdk.csproj @@ -2,8 +2,12 @@ $(AllTargetFrameworks) - Unofficial .NET SDK for opencode: a typed client for the opencode HTTP API. - opencode;sdk;client;ai;agent + + OpenCodeAI.Sdk + Unofficial .NET SDK for opencode: a typed client for the opencode HTTP API, with server-sent event streams, PTY and persistent-PTY terminals over WebSockets, cursor pagination, typed errors, and a local server launcher. The package id is OpenCodeAI.Sdk; the assembly and every namespace are OpenCode.Sdk. + opencode;opencode2;sdk;client;http-client;openapi;ai;agent;llm;sse;streaming;pty;terminal;dotnet;csharp true true diff --git a/src/OpenCode.Sdk/OpenCodeServerOptions.cs b/src/OpenCode.Sdk/OpenCodeServerOptions.cs index 92d63a192..bfe712f59 100644 --- a/src/OpenCode.Sdk/OpenCodeServerOptions.cs +++ b/src/OpenCode.Sdk/OpenCodeServerOptions.cs @@ -10,10 +10,12 @@ public sealed class OpenCodeServerOptions /// /// Gets or sets the server command: the executable followed by its leading arguments. The /// launcher appends --stdio --port 0 β€” the reference client's exact standalone argv. - /// The default runs opencode serve from the PATH, upstream's own default command - /// shape; tests and tools point this at a source-run instead. + /// The default runs opencode2 serve from the PATH: the CLI package of the line this + /// SDK speaks to declares exactly one executable and names it opencode2. Upstream's + /// own standalone mode re-invokes the executable it is already running rather than naming + /// one, which an SDK cannot do. Tests and tools point this at a source run instead. /// - public IReadOnlyList Command { get; set; } = ["opencode", "serve"]; + public IReadOnlyList Command { get; set; } = ["opencode2", "serve"]; /// Gets or sets the child's working directory; null inherits the caller's. public string? WorkingDirectory { get; set; } diff --git a/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json b/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json index c43446ffd..b80759020 100644 --- a/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json +++ b/tests/OpenCode.Sdk.Sandbox/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandLineArgs": "--stream", "environmentVariables": { "OPENCODE_SANDBOX_ENDPOINT": "http://127.0.0.1:4096", - "OPENCODE_SERVER_PASSWORD": "123456" + "OPENCODE_PASSWORD": "123456" } }, "sandbox-events": { @@ -13,7 +13,7 @@ "commandLineArgs": "--events", "environmentVariables": { "OPENCODE_SANDBOX_ENDPOINT": "http://127.0.0.1:4096", - "OPENCODE_SERVER_PASSWORD": "123456" + "OPENCODE_PASSWORD": "123456" } } } diff --git a/tests/OpenCode.Sdk.Sandbox/README.md b/tests/OpenCode.Sdk.Sandbox/README.md index 5f7ca3674..5d8eae803 100644 --- a/tests/OpenCode.Sdk.Sandbox/README.md +++ b/tests/OpenCode.Sdk.Sandbox/README.md @@ -16,11 +16,11 @@ Configuration comes from environment variables, prefilled for the IDE by ## Running Start a server with a fixed password so the checked-in profile matches (`serve` adopts the -same `OPENCODE_SERVER_PASSWORD` variable; without it the server generates and prints a +same `OPENCODE_PASSWORD` variable; without it the server generates and prints a random one): ```sh -OPENCODE_SERVER_PASSWORD=123456 opencode2 serve --hostname 127.0.0.1 --port 4096 +OPENCODE_PASSWORD=123456 opencode2 serve --hostname 127.0.0.1 --port 4096 ``` Then F5 with one of the sandbox profiles, or run either stream mode directly: