From 729ec769cfb4b1a8b5568b736c7c3457c5bb5d44 Mon Sep 17 00:00:00 2001 From: showxu <10173746+showxu@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:04:03 +0800 Subject: [PATCH] docs: present swift-codex with the shared README layout --- Documentation/Usage/Integration.md | 419 ++++++++++++++++++++++++++ Documentation/Usage/README.md | 2 + README.md | 465 +++-------------------------- 3 files changed, 462 insertions(+), 424 deletions(-) create mode 100644 Documentation/Usage/Integration.md diff --git a/Documentation/Usage/Integration.md b/Documentation/Usage/Integration.md new file mode 100644 index 0000000..f6aa7ea --- /dev/null +++ b/Documentation/Usage/Integration.md @@ -0,0 +1,419 @@ +# Usage guide + +## Overview + +swift-codex lets Swift apps and servers work with the OpenAI Codex CLI. It +launches the `codex` executable that you install and speaks each of its +machine interfaces: the JSONL event stream of `codex exec`, the JSON-RPC +protocol of `codex app-server`, and the Model Context Protocol tools of +`codex mcp-server`. Each interface is a separate product, so a target depends +only on the layer it uses. + +- `Codex` threads and turns: run a prompt and read the final response, stream + each event as it arrives, resume a thread, and request structured output + with a JSON Schema. +- Direct `codex exec` and `codex exec resume` processes with raw stdout lines, + typed JSONL events, and bounded output capture. +- A typed App Server client whose stable and experimental protocol models are + generated at build time from a schema pinned to OpenAI Codex CLI 0.154.0. +- App Server transports over stdio, Foundation `URLSession` WebSockets, and + SwiftNIO WebSockets, plus Vapor and Hummingbird adapters that accept + downstream WebSocket connections for a gateway. +- An MCP client for `codex mcp-server` with tool calls, server messages, + approval requests, and cancellation. + +## Quick start + +Install the OpenAI Codex CLI and sign in with it first. `Codex` finds `codex` +on `PATH` and runs each turn through `codex exec`: + +```swift +import Codex + +func summarizeRepository() async throws { + let codex = Codex() + let thread = codex.startThread() + let turn = try await thread.run(.text("Summarize this repository.")) + + print(turn.finalResponse) +} +``` + +`run(_:options:)` waits for the turn to finish and returns a `Turn` with the +final response, the completed items, and token usage. A new thread receives its +`id` from the first `thread.started` event, and later turns on the same +`CodexThread` continue that session. + +## Products + +| Product | Use it for | +| --- | --- | +| `Codex` | Threads and turns, with buffered and streamed runs | +| `CodexExec` | `codex exec` and `codex exec resume` processes and JSONL decoding | +| `CodexAppServerClient` | Typed App Server calls, notifications, server requests, and connection lifecycle | +| `CodexAppServerProtocol` | Generated stable and experimental App Server protocol models | +| `CodexAppServerRuntime` | The message transport protocol and raw JSON-RPC envelopes | +| `CodexAppServerStdio` | A local `codex app-server --listen stdio://` process transport | +| `CodexAppServerURLSession` | A Foundation `URLSession` WebSocket client transport | +| `CodexAppServerNIO` | A SwiftNIO WebSocket client transport | +| `CodexAppServerVapor` | A Vapor adapter that accepts downstream WebSocket connections | +| `CodexAppServerHummingbird` | A Hummingbird adapter that accepts downstream WebSocket connections | +| `CodexMCP` | `codex mcp-server` tool calls, server messages, approvals, and cancellation | + +There is no umbrella `CodexAppServer` product. A local client combines +`CodexAppServerClient` with one client transport. A gateway combines a Vapor +or Hummingbird adapter with an upstream transport through +`CodexAppServerRuntime`. + +## Usage + +## Stream events + +`runStreamed(_:options:)` returns the turn's events in order, without +buffering them: + +```swift +import Codex + +func streamTurn() async throws { + let thread = Codex().startThread() + let streamedTurn = try await thread.runStreamed(.text("Inspect the test suite.")) + + for try await event in streamedTurn { + switch event { + case .threadStarted(let id): + print("thread:", id) + case .itemCompleted(let item): + print("item:", item) + case .turnCompleted(let usage): + print("output tokens:", usage.outputTokens) + case .turnFailed(let error): + print("failed:", error.message) + default: + break + } + } +} +``` + +`ThreadEvent`, `ThreadItem`, `Usage`, and `ThreadError` are aliases for the +`CodexExec` event models, so both products share one protocol model. + +## Configure and resume threads + +`CodexOptions` configures the client, `ThreadOptions` configures every turn on +a thread, and `TurnOptions` configures a single turn. Values such as the +sandbox mode and approval policy use the CLI's own configuration strings: + +```swift +import Codex +import Foundation + +func reviewWorkspace() async throws { + let codex = Codex(options: .init( + codexPathOverride: URL(fileURLWithPath: "/opt/homebrew/bin/codex") + )) + let thread = codex.startThread(options: .init( + sandboxMode: "read-only", + workingDirectory: URL(fileURLWithPath: "/path/to/workspace"), + skipGitRepoCheck: true + )) + + let first = try await thread.run(.text("Review the current diff.")) + print(first.finalResponse) + + if let id = thread.id { + let resumed = codex.resumeThread(id) + let followUp = try await resumed.run(.text("List the riskiest change.")) + print(followUp.finalResponse) + } +} +``` + +`ThreadOptions` also sets the model, reasoning effort, network access, web +search mode, and additional writable directories. `CodexOptions` sets an API +key, a base URL, configuration overrides, and a replacement environment for the +launched process. `Input.items(_:)` combines text with local images. + +## Request structured output + +Pass a JSON Schema in `TurnOptions` to constrain the final response: + +```swift +import Codex + +func summarizeAsJSON() async throws { + let schema: CodexConfigObject = [ + "type": .string("object"), + "properties": .object([ + "summary": .object(["type": .string("string")]) + ]), + "required": .array([.string("summary")]), + "additionalProperties": .bool(false), + ] + + let thread = Codex().startThread() + let turn = try await thread.run( + .text("Summarize Package.swift."), + options: .init(outputSchema: schema) + ) + print(turn.finalResponse) +} +``` + +## Run `codex exec` directly + +`CodexExec` maps requests onto the `codex exec` command line and returns a +process handle. Read `stdoutLines` as plain text, or decode them with +`CodexExecJSONLDecoder` in JSONL mode. Always await `waitForTermination()`, +even after a stream error, so the process is cleaned up and its exit status and +stderr are available: + +```swift +import CodexExec + +func runExec() async throws { + let client = CodexExecClient() + let handle = try await client.run(.init( + promptInput: .text("Inspect Package.swift."), + outputMode: .jsonl, + options: .init(ignoreUserConfig: true, sandboxMode: "read-only") + )) + + do { + for try await event in CodexExecJSONLDecoder().decode(handle.stdoutLines) { + print(event) + } + } catch { + _ = try? await handle.waitForTermination() + throw error + } + + let termination = try await handle.waitForTermination() + print(termination.exitInterpretation) +} +``` + +`ignoreUserConfig` maps to `--ignore-user-config`. It keeps the user's +`config.toml` models, MCP servers, and hooks out of the request while still +using `CODEX_HOME` for authentication. Resume a session with +`client.resume(_:)` and a `.sessionID(_:)`, `.last`, or `.lastAll` selector. + +Capture is bounded. By default `CodexExecLaunchConfiguration.outputLimits` +retains 8 MiB and 65,536 complete lines of stdout and 1 MiB of stderr. Output +beyond a limit ends the stream with `CodexExecError.outputCaptureLimitExceeded`, +which carries the retained output and the dropped byte counts. + +## Connect to the App Server + +`CodexAppServerClient` performs the `initialize` handshake over a transport you +provide and returns a `CodexAppServerConnection` with typed methods: + +```swift +import CodexAppServerClient +import CodexAppServerProtocol +import CodexAppServerStdio + +func listModels() async throws { + let client = CodexAppServerClient( + sessionConfiguration: .init( + clientInfo: .init(name: "my_app", title: "My App", version: "1.0.0") + ), + transportFactory: { try CodexAppServerStdioTransport() } + ) + let connection = try await client.start() + + do { + let response = try await connection.modelList(.init()) + for model in response.data { + print(model.id) + } + } catch { + await connection.close() + throw error + } + await connection.close() +} +``` + +Typed methods cover threads and turns, reviews, command execution, the file +system, configuration, models, accounts, MCP servers, apps and skills, and +experimental realtime APIs. Parameter and result types come from +`CodexAppServerProtocol.Stable` and `CodexAppServerProtocol.Experimental`. Set +`experimentalApi: true` in the session configuration to use experimental +methods. Experimental models follow the pinned experimental schema and do not +carry the stability expectations of the stable namespace. + +Each release pins the vendored schema to an exact OpenAI Codex CLI release; the +[schema lock file](../../Vendor/CodexAppServerProtocolSchema/upstream.lock.json) +records it, and the [method adoption inventory](../../Documentation/Reference/CodexAppServerMethodAdoption.md) +lists the methods with typed bindings. + +## Handle notifications and server requests + +A connection streams notifications and requests that the server sends to the +client, such as approval prompts. Answer each request with +`resolveServerRequest(_:with:)` or `rejectServerRequest(_:code:message:data:)`: + +```swift +import CodexAppServerClient + +func observe(connection: CodexAppServerConnection) async throws { + for try await notification in connection.notifications { + print(notification) + } +} + +func handleServerRequests(connection: CodexAppServerConnection) async throws { + for try await request in connection.typedServerRequests { + if case .commandExecutionApproval(let handle) = request { + try await connection.rejectServerRequest( + handle, + code: -32603, + message: "Command approval is not supported by this client." + ) + } + } +} +``` + +To forward fields that the pinned stable model does not include, set +`inboundMessageMode: .raw` and use `rawNotifications`, `rawServerRequests`, and +`sendRawRequest(method:params:)`. Use `.rawOrdered` and `rawInboundMessages` +when your state depends on the combined order of notifications and server +requests. Typed streams finish immediately in raw modes, and raw streams finish +immediately in typed mode. + +## Choose an App Server transport + +`CodexAppServerStdioTransport` launches `codex app-server --listen stdio://` +and finds `codex` on `PATH` unless you set an executable. Use +`CodexAppServerURLSessionTransport` or `CodexAppServerNIOTransport.connect(url:)` +to reach an App Server WebSocket endpoint instead: + +```swift +import CodexAppServerClient +import CodexAppServerStdio +import CodexAppServerURLSession +import Foundation + +func connectToExplicitBinary() async throws { + let process = CodexAppServerStdioConfiguration( + executableURL: URL(fileURLWithPath: "/opt/homebrew/bin/codex"), + workingDirectoryURL: URL(fileURLWithPath: "/path/to/workspace") + ) + let client = CodexAppServerClient( + sessionConfiguration: .init( + clientInfo: .init(name: "my_app", version: "1.0.0"), + experimentalApi: true + ), + transportFactory: { try CodexAppServerStdioTransport(configuration: process) } + ) + let connection = try await client.start() + await connection.close() +} + +func connectToWebSocket(url: URL) async throws { + let client = CodexAppServerClient( + sessionConfiguration: .init(clientInfo: .init(name: "my_app", version: "1.0.0")), + transportFactory: { CodexAppServerURLSessionTransport(url: url) } + ) + let connection = try await client.start() + await connection.close() +} +``` + +## Bridge a WebSocket gateway + +`CodexAppServerVapor.webSocket(on:_:onConnect:)` and +`CodexAppServerHummingbird.webSocket(on:_:onConnect:)` register a WebSocket +route and hand each accepted connection to you as a +`CodexAppServerMessageTransport`. A transparent gateway pumps messages between +that downstream transport and an upstream one: + +```swift +import CodexAppServerRuntime + +func bridge( + downstream: any CodexAppServerMessageTransport, + upstream: any CodexAppServerMessageTransport +) async { + await withTaskGroup(of: Void.self) { group in + group.addTask { await pump(from: downstream, to: upstream) } + group.addTask { await pump(from: upstream, to: downstream) } + + _ = await group.next() + group.cancelAll() + await downstream.close() + await upstream.close() + } +} + +private func pump( + from source: any CodexAppServerMessageTransport, + to sink: any CodexAppServerMessageTransport +) async { + do { + for try await message in source.inboundMessages { + try await sink.sendMessage(message) + } + } catch { + return + } +} +``` + +The adapters only carry WebSocket messages. Authentication, rate limits, +auditing, message policy, and request routing belong to your gateway. Do not +call `CodexAppServerClient.start()` inside a transparent bridge: it performs a +second `initialize` handshake on behalf of the gateway. + +## Use the MCP server + +`CodexMCPClient` launches `codex mcp-server`, calls its `codex` tool, and +routes server messages and approval requests to each call handle: + +```swift +import CodexMCP + +func runThroughMCP() async throws { + let client = CodexMCPClient(clientInfo: .init( + name: "my_app", + version: "1.0.0", + requestedProtocolVersion: "2025-03-26" + )) + try await client.start() + + do { + let handle = try await client.runCodex(.init(prompt: "Inspect this workspace.")) + + let approvals = Task { + for await approval in handle.approvalRequests { + try? await handle.respond(to: approval.requestID, with: .deny) + } + } + defer { approvals.cancel() } + + let result = try await handle.value() + print(result.content) + + if let threadID = result.threadID { + let reply = try await client.reply(.init(threadID: threadID, prompt: "Continue.")) + print(try await reply.value().content) + } + try await client.stop() + } catch { + try? await client.stop() + throw error + } +} +``` + +`handle.cancel()` cancels an in-flight call, and the client also provides +`ping()` and `listTools()`. Set `CodexMCPLaunchOptions` to choose the +executable, working directory, and environment. + +`CodexMCP` needs a CLI that still provides `codex mcp-server`. It is validated +against OpenAI Codex CLI 0.139.0. Version 0.154.0 no longer provides that +command, so use `CodexAppServerClient` or `CodexExec` with current CLI +releases. diff --git a/Documentation/Usage/README.md b/Documentation/Usage/README.md index a75de21..ea9d6f2 100644 --- a/Documentation/Usage/README.md +++ b/Documentation/Usage/README.md @@ -1,5 +1,7 @@ # Usage +- [Integration guide](Integration.md): product selection and end-to-end examples. + This directory is the user-facing usage layer for `swift-codex`. Architecture docs define current truth. Usage docs show how to consume the diff --git a/README.md b/README.md index 6afe3ba..6fb0446 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,54 @@ +
+

- swift-codex logo + swift-codex logo

swift-codex

- Swift interfaces for the OpenAI Codex CLI through its exec, App Server, and MCP protocols. + Drive the Codex CLI from Swift through separate exec, App Server, and MCP interfaces.

CI - Swift 6.2+ - Platforms: macOS 14+ - License: Apache-2.0 WITH Swift-exception + Swift 6.2+ + Platforms: macOS 14+ + License: Apache-2.0 WITH Swift-exception

-[Overview](#overview) · [Install](#install) · [Quick start](#quick-start) · -[Products](#products) · [Usage](#usage) · [Requirements](#requirements) · -[Documentation](#documentation) · [Contributing](#contributing) · -[License](#license) +

+ Overview · + Quick look · + Install · + Usage · + Requirements · + Documentation · + Contributing · + License +

-> [!NOTE] -> swift-codex is pre-1.0. Minor releases may include source-breaking -> changes, so depend on it with `.upToNextMinor(from:)`. +
## Overview -swift-codex lets Swift apps and servers work with the OpenAI Codex CLI. It -launches the `codex` executable that you install and speaks each of its -machine interfaces: the JSONL event stream of `codex exec`, the JSON-RPC -protocol of `codex app-server`, and the Model Context Protocol tools of -`codex mcp-server`. Each interface is a separate product, so a target depends -only on the layer it uses. - -- `Codex` threads and turns: run a prompt and read the final response, stream - each event as it arrives, resume a thread, and request structured output - with a JSON Schema. -- Direct `codex exec` and `codex exec resume` processes with raw stdout lines, - typed JSONL events, and bounded output capture. -- A typed App Server client whose stable and experimental protocol models are - generated at build time from a schema pinned to OpenAI Codex CLI 0.154.0. -- App Server transports over stdio, Foundation `URLSession` WebSockets, and - SwiftNIO WebSockets, plus Vapor and Hummingbird adapters that accept - downstream WebSocket connections for a gateway. -- An MCP client for `codex mcp-server` with tool calls, server messages, - approval requests, and cancellation. +Keep agent sessions inside a Swift app or server: run a turn, stream events, and resume a thread. Import the product for the machine interface you use; the [integration guide](Documentation/Usage/Integration.md) covers product selection, transports, approvals, and execution limits. + +## Quick look + +Install the Codex CLI and sign in with it before running a turn. + +```swift +import Codex + +func summarizeRepository() async throws { + let codex = Codex() + let thread = codex.startThread() + let turn = try await thread.run(.text("Summarize this repository.")) + + print(turn.finalResponse) +} +``` ## Install @@ -70,407 +74,19 @@ targets: [ Depend on the narrowest product for your integration: `CodexExec` for direct process control, `CodexMCP` for the MCP server, or `CodexAppServerClient` with one transport, such as `CodexAppServerStdio`, for the App Server. The -[Products](#products) table lists all eleven. +[products guide](Documentation/Usage/Integration.md#products) table lists all eleven. `CodexAppServerProtocol` and `CodexAppServerClient` generate their Swift models from the vendored protocol schema with a SwiftPM build tool plugin during a normal build. The generated code is not checked into the repository. -## Quick start - -Install the OpenAI Codex CLI and sign in with it first. `Codex` finds `codex` -on `PATH` and runs each turn through `codex exec`: - -```swift -import Codex - -func summarizeRepository() async throws { - let codex = Codex() - let thread = codex.startThread() - let turn = try await thread.run(.text("Summarize this repository.")) - - print(turn.finalResponse) -} -``` - -`run(_:options:)` waits for the turn to finish and returns a `Turn` with the -final response, the completed items, and token usage. A new thread receives its -`id` from the first `thread.started` event, and later turns on the same -`CodexThread` continue that session. - -## Products - -| Product | Use it for | -| --- | --- | -| `Codex` | Threads and turns, with buffered and streamed runs | -| `CodexExec` | `codex exec` and `codex exec resume` processes and JSONL decoding | -| `CodexAppServerClient` | Typed App Server calls, notifications, server requests, and connection lifecycle | -| `CodexAppServerProtocol` | Generated stable and experimental App Server protocol models | -| `CodexAppServerRuntime` | The message transport protocol and raw JSON-RPC envelopes | -| `CodexAppServerStdio` | A local `codex app-server --listen stdio://` process transport | -| `CodexAppServerURLSession` | A Foundation `URLSession` WebSocket client transport | -| `CodexAppServerNIO` | A SwiftNIO WebSocket client transport | -| `CodexAppServerVapor` | A Vapor adapter that accepts downstream WebSocket connections | -| `CodexAppServerHummingbird` | A Hummingbird adapter that accepts downstream WebSocket connections | -| `CodexMCP` | `codex mcp-server` tool calls, server messages, approvals, and cancellation | - -There is no umbrella `CodexAppServer` product. A local client combines -`CodexAppServerClient` with one client transport. A gateway combines a Vapor -or Hummingbird adapter with an upstream transport through -`CodexAppServerRuntime`. +> [!NOTE] +> swift-codex is pre-1.0. Minor releases may include source-breaking +> changes, so depend on it with `.upToNextMinor(from:)`. ## Usage -### Stream events - -`runStreamed(_:options:)` returns the turn's events in order, without -buffering them: - -```swift -import Codex - -func streamTurn() async throws { - let thread = Codex().startThread() - let streamedTurn = try await thread.runStreamed(.text("Inspect the test suite.")) - - for try await event in streamedTurn { - switch event { - case .threadStarted(let id): - print("thread:", id) - case .itemCompleted(let item): - print("item:", item) - case .turnCompleted(let usage): - print("output tokens:", usage.outputTokens) - case .turnFailed(let error): - print("failed:", error.message) - default: - break - } - } -} -``` - -`ThreadEvent`, `ThreadItem`, `Usage`, and `ThreadError` are aliases for the -`CodexExec` event models, so both products share one protocol model. - -### Configure and resume threads - -`CodexOptions` configures the client, `ThreadOptions` configures every turn on -a thread, and `TurnOptions` configures a single turn. Values such as the -sandbox mode and approval policy use the CLI's own configuration strings: - -```swift -import Codex -import Foundation - -func reviewWorkspace() async throws { - let codex = Codex(options: .init( - codexPathOverride: URL(fileURLWithPath: "/opt/homebrew/bin/codex") - )) - let thread = codex.startThread(options: .init( - sandboxMode: "read-only", - workingDirectory: URL(fileURLWithPath: "/path/to/workspace"), - skipGitRepoCheck: true - )) - - let first = try await thread.run(.text("Review the current diff.")) - print(first.finalResponse) - - if let id = thread.id { - let resumed = codex.resumeThread(id) - let followUp = try await resumed.run(.text("List the riskiest change.")) - print(followUp.finalResponse) - } -} -``` - -`ThreadOptions` also sets the model, reasoning effort, network access, web -search mode, and additional writable directories. `CodexOptions` sets an API -key, a base URL, configuration overrides, and a replacement environment for the -launched process. `Input.items(_:)` combines text with local images. - -### Request structured output - -Pass a JSON Schema in `TurnOptions` to constrain the final response: - -```swift -import Codex - -func summarizeAsJSON() async throws { - let schema: CodexConfigObject = [ - "type": .string("object"), - "properties": .object([ - "summary": .object(["type": .string("string")]) - ]), - "required": .array([.string("summary")]), - "additionalProperties": .bool(false), - ] - - let thread = Codex().startThread() - let turn = try await thread.run( - .text("Summarize Package.swift."), - options: .init(outputSchema: schema) - ) - print(turn.finalResponse) -} -``` - -### Run `codex exec` directly - -`CodexExec` maps requests onto the `codex exec` command line and returns a -process handle. Read `stdoutLines` as plain text, or decode them with -`CodexExecJSONLDecoder` in JSONL mode. Always await `waitForTermination()`, -even after a stream error, so the process is cleaned up and its exit status and -stderr are available: - -```swift -import CodexExec - -func runExec() async throws { - let client = CodexExecClient() - let handle = try await client.run(.init( - promptInput: .text("Inspect Package.swift."), - outputMode: .jsonl, - options: .init(ignoreUserConfig: true, sandboxMode: "read-only") - )) - - do { - for try await event in CodexExecJSONLDecoder().decode(handle.stdoutLines) { - print(event) - } - } catch { - _ = try? await handle.waitForTermination() - throw error - } - - let termination = try await handle.waitForTermination() - print(termination.exitInterpretation) -} -``` - -`ignoreUserConfig` maps to `--ignore-user-config`. It keeps the user's -`config.toml` models, MCP servers, and hooks out of the request while still -using `CODEX_HOME` for authentication. Resume a session with -`client.resume(_:)` and a `.sessionID(_:)`, `.last`, or `.lastAll` selector. - -Capture is bounded. By default `CodexExecLaunchConfiguration.outputLimits` -retains 8 MiB and 65,536 complete lines of stdout and 1 MiB of stderr. Output -beyond a limit ends the stream with `CodexExecError.outputCaptureLimitExceeded`, -which carries the retained output and the dropped byte counts. - -### Connect to the App Server - -`CodexAppServerClient` performs the `initialize` handshake over a transport you -provide and returns a `CodexAppServerConnection` with typed methods: - -```swift -import CodexAppServerClient -import CodexAppServerProtocol -import CodexAppServerStdio - -func listModels() async throws { - let client = CodexAppServerClient( - sessionConfiguration: .init( - clientInfo: .init(name: "my_app", title: "My App", version: "1.0.0") - ), - transportFactory: { try CodexAppServerStdioTransport() } - ) - let connection = try await client.start() - - do { - let response = try await connection.modelList(.init()) - for model in response.data { - print(model.id) - } - } catch { - await connection.close() - throw error - } - await connection.close() -} -``` - -Typed methods cover threads and turns, reviews, command execution, the file -system, configuration, models, accounts, MCP servers, apps and skills, and -experimental realtime APIs. Parameter and result types come from -`CodexAppServerProtocol.Stable` and `CodexAppServerProtocol.Experimental`. Set -`experimentalApi: true` in the session configuration to use experimental -methods. Experimental models follow the pinned experimental schema and do not -carry the stability expectations of the stable namespace. - -Each release pins the vendored schema to an exact OpenAI Codex CLI release; the -[schema lock file](Vendor/CodexAppServerProtocolSchema/upstream.lock.json) -records it, and the [method adoption inventory](Documentation/Reference/CodexAppServerMethodAdoption.md) -lists the methods with typed bindings. - -### Handle notifications and server requests - -A connection streams notifications and requests that the server sends to the -client, such as approval prompts. Answer each request with -`resolveServerRequest(_:with:)` or `rejectServerRequest(_:code:message:data:)`: - -```swift -import CodexAppServerClient - -func observe(connection: CodexAppServerConnection) async throws { - for try await notification in connection.notifications { - print(notification) - } -} - -func handleServerRequests(connection: CodexAppServerConnection) async throws { - for try await request in connection.typedServerRequests { - if case .commandExecutionApproval(let handle) = request { - try await connection.rejectServerRequest( - handle, - code: -32603, - message: "Command approval is not supported by this client." - ) - } - } -} -``` - -To forward fields that the pinned stable model does not include, set -`inboundMessageMode: .raw` and use `rawNotifications`, `rawServerRequests`, and -`sendRawRequest(method:params:)`. Use `.rawOrdered` and `rawInboundMessages` -when your state depends on the combined order of notifications and server -requests. Typed streams finish immediately in raw modes, and raw streams finish -immediately in typed mode. - -### Choose an App Server transport - -`CodexAppServerStdioTransport` launches `codex app-server --listen stdio://` -and finds `codex` on `PATH` unless you set an executable. Use -`CodexAppServerURLSessionTransport` or `CodexAppServerNIOTransport.connect(url:)` -to reach an App Server WebSocket endpoint instead: - -```swift -import CodexAppServerClient -import CodexAppServerStdio -import CodexAppServerURLSession -import Foundation - -func connectToExplicitBinary() async throws { - let process = CodexAppServerStdioConfiguration( - executableURL: URL(fileURLWithPath: "/opt/homebrew/bin/codex"), - workingDirectoryURL: URL(fileURLWithPath: "/path/to/workspace") - ) - let client = CodexAppServerClient( - sessionConfiguration: .init( - clientInfo: .init(name: "my_app", version: "1.0.0"), - experimentalApi: true - ), - transportFactory: { try CodexAppServerStdioTransport(configuration: process) } - ) - let connection = try await client.start() - await connection.close() -} - -func connectToWebSocket(url: URL) async throws { - let client = CodexAppServerClient( - sessionConfiguration: .init(clientInfo: .init(name: "my_app", version: "1.0.0")), - transportFactory: { CodexAppServerURLSessionTransport(url: url) } - ) - let connection = try await client.start() - await connection.close() -} -``` - -### Bridge a WebSocket gateway - -`CodexAppServerVapor.webSocket(on:_:onConnect:)` and -`CodexAppServerHummingbird.webSocket(on:_:onConnect:)` register a WebSocket -route and hand each accepted connection to you as a -`CodexAppServerMessageTransport`. A transparent gateway pumps messages between -that downstream transport and an upstream one: - -```swift -import CodexAppServerRuntime - -func bridge( - downstream: any CodexAppServerMessageTransport, - upstream: any CodexAppServerMessageTransport -) async { - await withTaskGroup(of: Void.self) { group in - group.addTask { await pump(from: downstream, to: upstream) } - group.addTask { await pump(from: upstream, to: downstream) } - - _ = await group.next() - group.cancelAll() - await downstream.close() - await upstream.close() - } -} - -private func pump( - from source: any CodexAppServerMessageTransport, - to sink: any CodexAppServerMessageTransport -) async { - do { - for try await message in source.inboundMessages { - try await sink.sendMessage(message) - } - } catch { - return - } -} -``` - -The adapters only carry WebSocket messages. Authentication, rate limits, -auditing, message policy, and request routing belong to your gateway. Do not -call `CodexAppServerClient.start()` inside a transparent bridge: it performs a -second `initialize` handshake on behalf of the gateway. - -### Use the MCP server - -`CodexMCPClient` launches `codex mcp-server`, calls its `codex` tool, and -routes server messages and approval requests to each call handle: - -```swift -import CodexMCP - -func runThroughMCP() async throws { - let client = CodexMCPClient(clientInfo: .init( - name: "my_app", - version: "1.0.0", - requestedProtocolVersion: "2025-03-26" - )) - try await client.start() - - do { - let handle = try await client.runCodex(.init(prompt: "Inspect this workspace.")) - - let approvals = Task { - for await approval in handle.approvalRequests { - try? await handle.respond(to: approval.requestID, with: .deny) - } - } - defer { approvals.cancel() } - - let result = try await handle.value() - print(result.content) - - if let threadID = result.threadID { - let reply = try await client.reply(.init(threadID: threadID, prompt: "Continue.")) - print(try await reply.value().content) - } - try await client.stop() - } catch { - try? await client.stop() - throw error - } -} -``` - -`handle.cancel()` cancels an in-flight call, and the client also provides -`ping()` and `listTools()`. Set `CodexMCPLaunchOptions` to choose the -executable, working directory, and environment. - -`CodexMCP` needs a CLI that still provides `codex mcp-server`. It is validated -against OpenAI Codex CLI 0.139.0. Version 0.154.0 no longer provides that -command, so use `CodexAppServerClient` or `CodexExec` with current CLI -releases. +Use `Codex` for thread and turn conveniences or select a lower-level product for process, JSON-RPC, or MCP control. The [integration guide](Documentation/Usage/Integration.md) covers streaming, thread resume, structured output, App Server transports, and MCP requests. ## Requirements @@ -493,6 +109,7 @@ the executable, authentication, and launch configuration. ## Documentation +- [Usage guide](Documentation/Usage/Integration.md): examples, behavior, and limits. - [Documentation index](Documentation/README.md): how the guides, architecture notes, and reference material are organized. - Usage guides for [Codex](Documentation/Usage/Codex/README.md),