summaryrefslogtreecommitdiffhomepage
path: root/packages/sdk/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/sdk/src')
-rw-r--r--packages/sdk/src/api-promise.ts2
-rw-r--r--packages/sdk/src/client.ts862
-rw-r--r--packages/sdk/src/core/README.md3
-rw-r--r--packages/sdk/src/core/api-promise.ts92
-rw-r--r--packages/sdk/src/core/error.ts130
-rw-r--r--packages/sdk/src/core/resource.ts11
-rw-r--r--packages/sdk/src/core/streaming.ts315
-rw-r--r--packages/sdk/src/core/uploads.ts2
-rw-r--r--packages/sdk/src/error.ts2
-rw-r--r--packages/sdk/src/index.ts22
-rw-r--r--packages/sdk/src/internal/README.md3
-rw-r--r--packages/sdk/src/internal/builtin-types.ts93
-rw-r--r--packages/sdk/src/internal/decoders/line.ts135
-rw-r--r--packages/sdk/src/internal/detect-platform.ts196
-rw-r--r--packages/sdk/src/internal/errors.ts33
-rw-r--r--packages/sdk/src/internal/headers.ts97
-rw-r--r--packages/sdk/src/internal/parse.ts64
-rw-r--r--packages/sdk/src/internal/request-options.ts93
-rw-r--r--packages/sdk/src/internal/shim-types.ts26
-rw-r--r--packages/sdk/src/internal/shims.ts107
-rw-r--r--packages/sdk/src/internal/to-file.ts154
-rw-r--r--packages/sdk/src/internal/types.ts95
-rw-r--r--packages/sdk/src/internal/uploads.ts187
-rw-r--r--packages/sdk/src/internal/utils.ts8
-rw-r--r--packages/sdk/src/internal/utils/base64.ts40
-rw-r--r--packages/sdk/src/internal/utils/bytes.ts32
-rw-r--r--packages/sdk/src/internal/utils/env.ts18
-rw-r--r--packages/sdk/src/internal/utils/log.ts126
-rw-r--r--packages/sdk/src/internal/utils/path.ts88
-rw-r--r--packages/sdk/src/internal/utils/sleep.ts3
-rw-r--r--packages/sdk/src/internal/utils/uuid.ts17
-rw-r--r--packages/sdk/src/internal/utils/values.ts105
-rw-r--r--packages/sdk/src/lib/.keep4
-rw-r--r--packages/sdk/src/resource.ts2
-rw-r--r--packages/sdk/src/resources.ts1
-rw-r--r--packages/sdk/src/resources/app.ts190
-rw-r--r--packages/sdk/src/resources/config.ts438
-rw-r--r--packages/sdk/src/resources/event.ts243
-rw-r--r--packages/sdk/src/resources/file.ts52
-rw-r--r--packages/sdk/src/resources/find.ts133
-rw-r--r--packages/sdk/src/resources/index.ts81
-rw-r--r--packages/sdk/src/resources/session.ts555
-rw-r--r--packages/sdk/src/resources/shared.ts33
-rw-r--r--packages/sdk/src/resources/tui.ts37
-rw-r--r--packages/sdk/src/streaming.ts2
-rw-r--r--packages/sdk/src/uploads.ts2
-rw-r--r--packages/sdk/src/version.ts1
47 files changed, 4935 insertions, 0 deletions
diff --git a/packages/sdk/src/api-promise.ts b/packages/sdk/src/api-promise.ts
new file mode 100644
index 000000000..8c775ee69
--- /dev/null
+++ b/packages/sdk/src/api-promise.ts
@@ -0,0 +1,2 @@
+/** @deprecated Import from ./core/api-promise instead */
+export * from './core/api-promise';
diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts
new file mode 100644
index 000000000..c514ca877
--- /dev/null
+++ b/packages/sdk/src/client.ts
@@ -0,0 +1,862 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import type { RequestInit, RequestInfo, BodyInit } from './internal/builtin-types';
+import type { HTTPMethod, PromiseOrValue, MergedRequestInit, FinalizedRequestInit } from './internal/types';
+import { uuid4 } from './internal/utils/uuid';
+import { validatePositiveInteger, isAbsoluteURL, safeJSON } from './internal/utils/values';
+import { sleep } from './internal/utils/sleep';
+export type { Logger, LogLevel } from './internal/utils/log';
+import { castToError, isAbortError } from './internal/errors';
+import type { APIResponseProps } from './internal/parse';
+import { getPlatformHeaders } from './internal/detect-platform';
+import * as Shims from './internal/shims';
+import * as Opts from './internal/request-options';
+import { VERSION } from './version';
+import * as Errors from './core/error';
+import * as Uploads from './core/uploads';
+import * as API from './resources/index';
+import { APIPromise } from './core/api-promise';
+import {
+ App,
+ AppInitResponse,
+ AppLogParams,
+ AppLogResponse,
+ AppModesResponse,
+ AppProvidersResponse,
+ AppResource,
+ Mode,
+ Model,
+ Provider,
+} from './resources/app';
+import {
+ Config,
+ ConfigResource,
+ KeybindsConfig,
+ McpLocalConfig,
+ McpRemoteConfig,
+ ModeConfig,
+} from './resources/config';
+import { Event, EventListResponse } from './resources/event';
+import { File, FileReadParams, FileReadResponse, FileResource, FileStatusResponse } from './resources/file';
+import {
+ Find,
+ FindFilesParams,
+ FindFilesResponse,
+ FindSymbolsParams,
+ FindSymbolsResponse,
+ FindTextParams,
+ FindTextResponse,
+ Match,
+ Symbol,
+} from './resources/find';
+import {
+ AssistantMessage,
+ FilePart,
+ FilePartInput,
+ FilePartSource,
+ FilePartSourceText,
+ FileSource,
+ Message,
+ Part,
+ Session,
+ SessionAbortResponse,
+ SessionChatParams,
+ SessionDeleteResponse,
+ SessionInitParams,
+ SessionInitResponse,
+ SessionListResponse,
+ SessionMessagesResponse,
+ SessionResource,
+ SessionSummarizeParams,
+ SessionSummarizeResponse,
+ SnapshotPart,
+ StepFinishPart,
+ StepStartPart,
+ SymbolSource,
+ TextPart,
+ TextPartInput,
+ ToolPart,
+ ToolStateCompleted,
+ ToolStateError,
+ ToolStatePending,
+ ToolStateRunning,
+ UserMessage,
+} from './resources/session';
+import { Tui, TuiAppendPromptParams, TuiAppendPromptResponse, TuiOpenHelpResponse } from './resources/tui';
+import { type Fetch } from './internal/builtin-types';
+import { HeadersLike, NullableHeaders, buildHeaders } from './internal/headers';
+import { FinalRequestOptions, RequestOptions } from './internal/request-options';
+import { readEnv } from './internal/utils/env';
+import {
+ type LogLevel,
+ type Logger,
+ formatRequestDetails,
+ loggerFor,
+ parseLogLevel,
+} from './internal/utils/log';
+import { isEmptyObj } from './internal/utils/values';
+
+export interface ClientOptions {
+ /**
+ * Override the default base URL for the API, e.g., "https://api.example.com/v2/"
+ *
+ * Defaults to process.env['OPENCODE_BASE_URL'].
+ */
+ baseURL?: string | null | undefined;
+
+ /**
+ * The maximum amount of time (in milliseconds) that the client should wait for a response
+ * from the server before timing out a single request.
+ *
+ * Note that request timeouts are retried by default, so in a worst-case scenario you may wait
+ * much longer than this timeout before the promise succeeds or fails.
+ *
+ * @unit milliseconds
+ */
+ timeout?: number | undefined;
+ /**
+ * Additional `RequestInit` options to be passed to `fetch` calls.
+ * Properties will be overridden by per-request `fetchOptions`.
+ */
+ fetchOptions?: MergedRequestInit | undefined;
+
+ /**
+ * Specify a custom `fetch` function implementation.
+ *
+ * If not provided, we expect that `fetch` is defined globally.
+ */
+ fetch?: Fetch | undefined;
+
+ /**
+ * The maximum number of times that the client will retry a request in case of a
+ * temporary failure, like a network error or a 5XX error from the server.
+ *
+ * @default 2
+ */
+ maxRetries?: number | undefined;
+
+ /**
+ * Default headers to include with every request to the API.
+ *
+ * These can be removed in individual requests by explicitly setting the
+ * header to `null` in request options.
+ */
+ defaultHeaders?: HeadersLike | undefined;
+
+ /**
+ * Default query parameters to include with every request to the API.
+ *
+ * These can be removed in individual requests by explicitly setting the
+ * param to `undefined` in request options.
+ */
+ defaultQuery?: Record<string, string | undefined> | undefined;
+
+ /**
+ * Set the log level.
+ *
+ * Defaults to process.env['OPENCODE_LOG'] or 'warn' if it isn't set.
+ */
+ logLevel?: LogLevel | undefined;
+
+ /**
+ * Set the logger.
+ *
+ * Defaults to globalThis.console.
+ */
+ logger?: Logger | undefined;
+}
+
+/**
+ * API Client for interfacing with the Opencode API.
+ */
+export class Opencode {
+ baseURL: string;
+ maxRetries: number;
+ timeout: number;
+ logger: Logger | undefined;
+ logLevel: LogLevel | undefined;
+ fetchOptions: MergedRequestInit | undefined;
+
+ private fetch: Fetch;
+ #encoder: Opts.RequestEncoder;
+ protected idempotencyHeader?: string;
+ private _options: ClientOptions;
+
+ /**
+ * API Client for interfacing with the Opencode API.
+ *
+ * @param {string} [opts.baseURL=process.env['OPENCODE_BASE_URL'] ?? http://localhost:54321] - Override the default base URL for the API.
+ * @param {number} [opts.timeout=1 minute] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
+ * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
+ * @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation.
+ * @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request.
+ * @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API.
+ * @param {Record<string, string | undefined>} opts.defaultQuery - Default query parameters to include with every request to the API.
+ */
+ constructor({ baseURL = readEnv('OPENCODE_BASE_URL'), ...opts }: ClientOptions = {}) {
+ const options: ClientOptions = {
+ ...opts,
+ baseURL: baseURL || `http://localhost:54321`,
+ };
+
+ this.baseURL = options.baseURL!;
+ this.timeout = options.timeout ?? Opencode.DEFAULT_TIMEOUT /* 1 minute */;
+ this.logger = options.logger ?? console;
+ const defaultLogLevel = 'warn';
+ // Set default logLevel early so that we can log a warning in parseLogLevel.
+ this.logLevel = defaultLogLevel;
+ this.logLevel =
+ parseLogLevel(options.logLevel, 'ClientOptions.logLevel', this) ??
+ parseLogLevel(readEnv('OPENCODE_LOG'), "process.env['OPENCODE_LOG']", this) ??
+ defaultLogLevel;
+ this.fetchOptions = options.fetchOptions;
+ this.maxRetries = options.maxRetries ?? 2;
+ this.fetch = options.fetch ?? Shims.getDefaultFetch();
+ this.#encoder = Opts.FallbackEncoder;
+
+ this._options = options;
+ }
+
+ /**
+ * Create a new client instance re-using the same options given to the current client with optional overriding.
+ */
+ withOptions(options: Partial<ClientOptions>): this {
+ const client = new (this.constructor as any as new (props: ClientOptions) => typeof this)({
+ ...this._options,
+ baseURL: this.baseURL,
+ maxRetries: this.maxRetries,
+ timeout: this.timeout,
+ logger: this.logger,
+ logLevel: this.logLevel,
+ fetch: this.fetch,
+ fetchOptions: this.fetchOptions,
+ ...options,
+ });
+ return client;
+ }
+
+ /**
+ * Check whether the base URL is set to its default.
+ */
+ #baseURLOverridden(): boolean {
+ return this.baseURL !== 'http://localhost:54321';
+ }
+
+ protected defaultQuery(): Record<string, string | undefined> | undefined {
+ return this._options.defaultQuery;
+ }
+
+ protected validateHeaders({ values, nulls }: NullableHeaders) {
+ return;
+ }
+
+ /**
+ * Basic re-implementation of `qs.stringify` for primitive types.
+ */
+ protected stringifyQuery(query: Record<string, unknown>): string {
+ return Object.entries(query)
+ .filter(([_, value]) => typeof value !== 'undefined')
+ .map(([key, value]) => {
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
+ }
+ if (value === null) {
+ return `${encodeURIComponent(key)}=`;
+ }
+ throw new Errors.OpencodeError(
+ `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`,
+ );
+ })
+ .join('&');
+ }
+
+ private getUserAgent(): string {
+ return `${this.constructor.name}/JS ${VERSION}`;
+ }
+
+ protected defaultIdempotencyKey(): string {
+ return `stainless-node-retry-${uuid4()}`;
+ }
+
+ protected makeStatusError(
+ status: number,
+ error: Object,
+ message: string | undefined,
+ headers: Headers,
+ ): Errors.APIError {
+ return Errors.APIError.generate(status, error, message, headers);
+ }
+
+ buildURL(
+ path: string,
+ query: Record<string, unknown> | null | undefined,
+ defaultBaseURL?: string | undefined,
+ ): string {
+ const baseURL = (!this.#baseURLOverridden() && defaultBaseURL) || this.baseURL;
+ const url =
+ isAbsoluteURL(path) ?
+ new URL(path)
+ : new URL(baseURL + (baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path));
+
+ const defaultQuery = this.defaultQuery();
+ if (!isEmptyObj(defaultQuery)) {
+ query = { ...defaultQuery, ...query };
+ }
+
+ if (typeof query === 'object' && query && !Array.isArray(query)) {
+ url.search = this.stringifyQuery(query as Record<string, unknown>);
+ }
+
+ return url.toString();
+ }
+
+ /**
+ * Used as a callback for mutating the given `FinalRequestOptions` object.
+ */
+ protected async prepareOptions(options: FinalRequestOptions): Promise<void> {}
+
+ /**
+ * Used as a callback for mutating the given `RequestInit` object.
+ *
+ * This is useful for cases where you want to add certain headers based off of
+ * the request properties, e.g. `method` or `url`.
+ */
+ protected async prepareRequest(
+ request: RequestInit,
+ { url, options }: { url: string; options: FinalRequestOptions },
+ ): Promise<void> {}
+
+ get<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('get', path, opts);
+ }
+
+ post<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('post', path, opts);
+ }
+
+ patch<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('patch', path, opts);
+ }
+
+ put<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('put', path, opts);
+ }
+
+ delete<Rsp>(path: string, opts?: PromiseOrValue<RequestOptions>): APIPromise<Rsp> {
+ return this.methodRequest('delete', path, opts);
+ }
+
+ private methodRequest<Rsp>(
+ method: HTTPMethod,
+ path: string,
+ opts?: PromiseOrValue<RequestOptions>,
+ ): APIPromise<Rsp> {
+ return this.request(
+ Promise.resolve(opts).then((opts) => {
+ return { method, path, ...opts };
+ }),
+ );
+ }
+
+ request<Rsp>(
+ options: PromiseOrValue<FinalRequestOptions>,
+ remainingRetries: number | null = null,
+ ): APIPromise<Rsp> {
+ return new APIPromise(this, this.makeRequest(options, remainingRetries, undefined));
+ }
+
+ private async makeRequest(
+ optionsInput: PromiseOrValue<FinalRequestOptions>,
+ retriesRemaining: number | null,
+ retryOfRequestLogID: string | undefined,
+ ): Promise<APIResponseProps> {
+ const options = await optionsInput;
+ const maxRetries = options.maxRetries ?? this.maxRetries;
+ if (retriesRemaining == null) {
+ retriesRemaining = maxRetries;
+ }
+
+ await this.prepareOptions(options);
+
+ const { req, url, timeout } = await this.buildRequest(options, {
+ retryCount: maxRetries - retriesRemaining,
+ });
+
+ await this.prepareRequest(req, { url, options });
+
+ /** Not an API request ID, just for correlating local log entries. */
+ const requestLogID = 'log_' + ((Math.random() * (1 << 24)) | 0).toString(16).padStart(6, '0');
+ const retryLogStr = retryOfRequestLogID === undefined ? '' : `, retryOf: ${retryOfRequestLogID}`;
+ const startTime = Date.now();
+
+ loggerFor(this).debug(
+ `[${requestLogID}] sending request`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ method: options.method,
+ url,
+ options,
+ headers: req.headers,
+ }),
+ );
+
+ if (options.signal?.aborted) {
+ throw new Errors.APIUserAbortError();
+ }
+
+ const controller = new AbortController();
+ const response = await this.fetchWithTimeout(url, req, timeout, controller).catch(castToError);
+ const headersTime = Date.now();
+
+ if (response instanceof Error) {
+ const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
+ if (options.signal?.aborted) {
+ throw new Errors.APIUserAbortError();
+ }
+ // detect native connection timeout errors
+ // deno throws "TypeError: error sending request for url (https://example/): client error (Connect): tcp connect error: Operation timed out (os error 60): Operation timed out (os error 60)"
+ // undici throws "TypeError: fetch failed" with cause "ConnectTimeoutError: Connect Timeout Error (attempted address: example:443, timeout: 1ms)"
+ // others do not provide enough information to distinguish timeouts from other connection errors
+ const isTimeout =
+ isAbortError(response) ||
+ /timed? ?out/i.test(String(response) + ('cause' in response ? String(response.cause) : ''));
+ if (retriesRemaining) {
+ loggerFor(this).info(
+ `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - ${retryMessage}`,
+ );
+ loggerFor(this).debug(
+ `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (${retryMessage})`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url,
+ durationMs: headersTime - startTime,
+ message: response.message,
+ }),
+ );
+ return this.retryRequest(options, retriesRemaining, retryOfRequestLogID ?? requestLogID);
+ }
+ loggerFor(this).info(
+ `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} - error; no more retries left`,
+ );
+ loggerFor(this).debug(
+ `[${requestLogID}] connection ${isTimeout ? 'timed out' : 'failed'} (error; no more retries left)`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url,
+ durationMs: headersTime - startTime,
+ message: response.message,
+ }),
+ );
+ if (isTimeout) {
+ throw new Errors.APIConnectionTimeoutError();
+ }
+ throw new Errors.APIConnectionError({ cause: response });
+ }
+
+ const responseInfo = `[${requestLogID}${retryLogStr}] ${req.method} ${url} ${
+ response.ok ? 'succeeded' : 'failed'
+ } with status ${response.status} in ${headersTime - startTime}ms`;
+
+ if (!response.ok) {
+ const shouldRetry = await this.shouldRetry(response);
+ if (retriesRemaining && shouldRetry) {
+ const retryMessage = `retrying, ${retriesRemaining} attempts remaining`;
+
+ // We don't need the body of this response.
+ await Shims.CancelReadableStream(response.body);
+ loggerFor(this).info(`${responseInfo} - ${retryMessage}`);
+ loggerFor(this).debug(
+ `[${requestLogID}] response error (${retryMessage})`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url: response.url,
+ status: response.status,
+ headers: response.headers,
+ durationMs: headersTime - startTime,
+ }),
+ );
+ return this.retryRequest(
+ options,
+ retriesRemaining,
+ retryOfRequestLogID ?? requestLogID,
+ response.headers,
+ );
+ }
+
+ const retryMessage = shouldRetry ? `error; no more retries left` : `error; not retryable`;
+
+ loggerFor(this).info(`${responseInfo} - ${retryMessage}`);
+
+ const errText = await response.text().catch((err: any) => castToError(err).message);
+ const errJSON = safeJSON(errText);
+ const errMessage = errJSON ? undefined : errText;
+
+ loggerFor(this).debug(
+ `[${requestLogID}] response error (${retryMessage})`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url: response.url,
+ status: response.status,
+ headers: response.headers,
+ message: errMessage,
+ durationMs: Date.now() - startTime,
+ }),
+ );
+
+ const err = this.makeStatusError(response.status, errJSON, errMessage, response.headers);
+ throw err;
+ }
+
+ loggerFor(this).info(responseInfo);
+ loggerFor(this).debug(
+ `[${requestLogID}] response start`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url: response.url,
+ status: response.status,
+ headers: response.headers,
+ durationMs: headersTime - startTime,
+ }),
+ );
+
+ return { response, options, controller, requestLogID, retryOfRequestLogID, startTime };
+ }
+
+ async fetchWithTimeout(
+ url: RequestInfo,
+ init: RequestInit | undefined,
+ ms: number,
+ controller: AbortController,
+ ): Promise<Response> {
+ const { signal, method, ...options } = init || {};
+ if (signal) signal.addEventListener('abort', () => controller.abort());
+
+ const timeout = setTimeout(() => controller.abort(), ms);
+
+ const isReadableBody =
+ ((globalThis as any).ReadableStream && options.body instanceof (globalThis as any).ReadableStream) ||
+ (typeof options.body === 'object' && options.body !== null && Symbol.asyncIterator in options.body);
+
+ const fetchOptions: RequestInit = {
+ signal: controller.signal as any,
+ ...(isReadableBody ? { duplex: 'half' } : {}),
+ method: 'GET',
+ ...options,
+ };
+ if (method) {
+ // Custom methods like 'patch' need to be uppercased
+ // See https://github.com/nodejs/undici/issues/2294
+ fetchOptions.method = method.toUpperCase();
+ }
+
+ try {
+ // use undefined this binding; fetch errors if bound to something else in browser/cloudflare
+ return await this.fetch.call(undefined, url, fetchOptions);
+ } finally {
+ clearTimeout(timeout);
+ }
+ }
+
+ private async shouldRetry(response: Response): Promise<boolean> {
+ // Note this is not a standard header.
+ const shouldRetryHeader = response.headers.get('x-should-retry');
+
+ // If the server explicitly says whether or not to retry, obey.
+ if (shouldRetryHeader === 'true') return true;
+ if (shouldRetryHeader === 'false') return false;
+
+ // Retry on request timeouts.
+ if (response.status === 408) return true;
+
+ // Retry on lock timeouts.
+ if (response.status === 409) return true;
+
+ // Retry on rate limits.
+ if (response.status === 429) return true;
+
+ // Retry internal errors.
+ if (response.status >= 500) return true;
+
+ return false;
+ }
+
+ private async retryRequest(
+ options: FinalRequestOptions,
+ retriesRemaining: number,
+ requestLogID: string,
+ responseHeaders?: Headers | undefined,
+ ): Promise<APIResponseProps> {
+ let timeoutMillis: number | undefined;
+
+ // Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it.
+ const retryAfterMillisHeader = responseHeaders?.get('retry-after-ms');
+ if (retryAfterMillisHeader) {
+ const timeoutMs = parseFloat(retryAfterMillisHeader);
+ if (!Number.isNaN(timeoutMs)) {
+ timeoutMillis = timeoutMs;
+ }
+ }
+
+ // About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
+ const retryAfterHeader = responseHeaders?.get('retry-after');
+ if (retryAfterHeader && !timeoutMillis) {
+ const timeoutSeconds = parseFloat(retryAfterHeader);
+ if (!Number.isNaN(timeoutSeconds)) {
+ timeoutMillis = timeoutSeconds * 1000;
+ } else {
+ timeoutMillis = Date.parse(retryAfterHeader) - Date.now();
+ }
+ }
+
+ // If the API asks us to wait a certain amount of time (and it's a reasonable amount),
+ // just do what it says, but otherwise calculate a default
+ if (!(timeoutMillis && 0 <= timeoutMillis && timeoutMillis < 60 * 1000)) {
+ const maxRetries = options.maxRetries ?? this.maxRetries;
+ timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries);
+ }
+ await sleep(timeoutMillis);
+
+ return this.makeRequest(options, retriesRemaining - 1, requestLogID);
+ }
+
+ private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number {
+ const initialRetryDelay = 0.5;
+ const maxRetryDelay = 8.0;
+
+ const numRetries = maxRetries - retriesRemaining;
+
+ // Apply exponential backoff, but not more than the max.
+ const sleepSeconds = Math.min(initialRetryDelay * Math.pow(2, numRetries), maxRetryDelay);
+
+ // Apply some jitter, take up to at most 25 percent of the retry time.
+ const jitter = 1 - Math.random() * 0.25;
+
+ return sleepSeconds * jitter * 1000;
+ }
+
+ async buildRequest(
+ inputOptions: FinalRequestOptions,
+ { retryCount = 0 }: { retryCount?: number } = {},
+ ): Promise<{ req: FinalizedRequestInit; url: string; timeout: number }> {
+ const options = { ...inputOptions };
+ const { method, path, query, defaultBaseURL } = options;
+
+ const url = this.buildURL(path!, query as Record<string, unknown>, defaultBaseURL);
+ if ('timeout' in options) validatePositiveInteger('timeout', options.timeout);
+ options.timeout = options.timeout ?? this.timeout;
+ const { bodyHeaders, body } = this.buildBody({ options });
+ const reqHeaders = await this.buildHeaders({ options: inputOptions, method, bodyHeaders, retryCount });
+
+ const req: FinalizedRequestInit = {
+ method,
+ headers: reqHeaders,
+ ...(options.signal && { signal: options.signal }),
+ ...((globalThis as any).ReadableStream &&
+ body instanceof (globalThis as any).ReadableStream && { duplex: 'half' }),
+ ...(body && { body }),
+ ...((this.fetchOptions as any) ?? {}),
+ ...((options.fetchOptions as any) ?? {}),
+ };
+
+ return { req, url, timeout: options.timeout };
+ }
+
+ private async buildHeaders({
+ options,
+ method,
+ bodyHeaders,
+ retryCount,
+ }: {
+ options: FinalRequestOptions;
+ method: HTTPMethod;
+ bodyHeaders: HeadersLike;
+ retryCount: number;
+ }): Promise<Headers> {
+ let idempotencyHeaders: HeadersLike = {};
+ if (this.idempotencyHeader && method !== 'get') {
+ if (!options.idempotencyKey) options.idempotencyKey = this.defaultIdempotencyKey();
+ idempotencyHeaders[this.idempotencyHeader] = options.idempotencyKey;
+ }
+
+ const headers = buildHeaders([
+ idempotencyHeaders,
+ {
+ Accept: 'application/json',
+ 'User-Agent': this.getUserAgent(),
+ 'X-Stainless-Retry-Count': String(retryCount),
+ ...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
+ ...getPlatformHeaders(),
+ },
+ this._options.defaultHeaders,
+ bodyHeaders,
+ options.headers,
+ ]);
+
+ this.validateHeaders(headers);
+
+ return headers.values;
+ }
+
+ private buildBody({ options: { body, headers: rawHeaders } }: { options: FinalRequestOptions }): {
+ bodyHeaders: HeadersLike;
+ body: BodyInit | undefined;
+ } {
+ if (!body) {
+ return { bodyHeaders: undefined, body: undefined };
+ }
+ const headers = buildHeaders([rawHeaders]);
+ if (
+ // Pass raw type verbatim
+ ArrayBuffer.isView(body) ||
+ body instanceof ArrayBuffer ||
+ body instanceof DataView ||
+ (typeof body === 'string' &&
+ // Preserve legacy string encoding behavior for now
+ headers.values.has('content-type')) ||
+ // `Blob` is superset of `File`
+ body instanceof Blob ||
+ // `FormData` -> `multipart/form-data`
+ body instanceof FormData ||
+ // `URLSearchParams` -> `application/x-www-form-urlencoded`
+ body instanceof URLSearchParams ||
+ // Send chunked stream (each chunk has own `length`)
+ ((globalThis as any).ReadableStream && body instanceof (globalThis as any).ReadableStream)
+ ) {
+ return { bodyHeaders: undefined, body: body as BodyInit };
+ } else if (
+ typeof body === 'object' &&
+ (Symbol.asyncIterator in body ||
+ (Symbol.iterator in body && 'next' in body && typeof body.next === 'function'))
+ ) {
+ return { bodyHeaders: undefined, body: Shims.ReadableStreamFrom(body as AsyncIterable<Uint8Array>) };
+ } else {
+ return this.#encoder({ body, headers });
+ }
+ }
+
+ static Opencode = this;
+ static DEFAULT_TIMEOUT = 60000; // 1 minute
+
+ static OpencodeError = Errors.OpencodeError;
+ static APIError = Errors.APIError;
+ static APIConnectionError = Errors.APIConnectionError;
+ static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError;
+ static APIUserAbortError = Errors.APIUserAbortError;
+ static NotFoundError = Errors.NotFoundError;
+ static ConflictError = Errors.ConflictError;
+ static RateLimitError = Errors.RateLimitError;
+ static BadRequestError = Errors.BadRequestError;
+ static AuthenticationError = Errors.AuthenticationError;
+ static InternalServerError = Errors.InternalServerError;
+ static PermissionDeniedError = Errors.PermissionDeniedError;
+ static UnprocessableEntityError = Errors.UnprocessableEntityError;
+
+ static toFile = Uploads.toFile;
+
+ event: API.Event = new API.Event(this);
+ app: API.AppResource = new API.AppResource(this);
+ find: API.Find = new API.Find(this);
+ file: API.FileResource = new API.FileResource(this);
+ config: API.ConfigResource = new API.ConfigResource(this);
+ session: API.SessionResource = new API.SessionResource(this);
+ tui: API.Tui = new API.Tui(this);
+}
+Opencode.Event = Event;
+Opencode.AppResource = AppResource;
+Opencode.Find = Find;
+Opencode.FileResource = FileResource;
+Opencode.ConfigResource = ConfigResource;
+Opencode.SessionResource = SessionResource;
+Opencode.Tui = Tui;
+export declare namespace Opencode {
+ export type RequestOptions = Opts.RequestOptions;
+
+ export { Event as Event, type EventListResponse as EventListResponse };
+
+ export {
+ AppResource as AppResource,
+ type App as App,
+ type Mode as Mode,
+ type Model as Model,
+ type Provider as Provider,
+ type AppInitResponse as AppInitResponse,
+ type AppLogResponse as AppLogResponse,
+ type AppModesResponse as AppModesResponse,
+ type AppProvidersResponse as AppProvidersResponse,
+ type AppLogParams as AppLogParams,
+ };
+
+ export {
+ Find as Find,
+ type Match as Match,
+ type Symbol as Symbol,
+ type FindFilesResponse as FindFilesResponse,
+ type FindSymbolsResponse as FindSymbolsResponse,
+ type FindTextResponse as FindTextResponse,
+ type FindFilesParams as FindFilesParams,
+ type FindSymbolsParams as FindSymbolsParams,
+ type FindTextParams as FindTextParams,
+ };
+
+ export {
+ FileResource as FileResource,
+ type File as File,
+ type FileReadResponse as FileReadResponse,
+ type FileStatusResponse as FileStatusResponse,
+ type FileReadParams as FileReadParams,
+ };
+
+ export {
+ ConfigResource as ConfigResource,
+ type Config as Config,
+ type KeybindsConfig as KeybindsConfig,
+ type McpLocalConfig as McpLocalConfig,
+ type McpRemoteConfig as McpRemoteConfig,
+ type ModeConfig as ModeConfig,
+ };
+
+ export {
+ SessionResource as SessionResource,
+ type AssistantMessage as AssistantMessage,
+ type FilePart as FilePart,
+ type FilePartInput as FilePartInput,
+ type FilePartSource as FilePartSource,
+ type FilePartSourceText as FilePartSourceText,
+ type FileSource as FileSource,
+ type Message as Message,
+ type Part as Part,
+ type Session as Session,
+ type SnapshotPart as SnapshotPart,
+ type StepFinishPart as StepFinishPart,
+ type StepStartPart as StepStartPart,
+ type SymbolSource as SymbolSource,
+ type TextPart as TextPart,
+ type TextPartInput as TextPartInput,
+ type ToolPart as ToolPart,
+ type ToolStateCompleted as ToolStateCompleted,
+ type ToolStateError as ToolStateError,
+ type ToolStatePending as ToolStatePending,
+ type ToolStateRunning as ToolStateRunning,
+ type UserMessage as UserMessage,
+ type SessionListResponse as SessionListResponse,
+ type SessionDeleteResponse as SessionDeleteResponse,
+ type SessionAbortResponse as SessionAbortResponse,
+ type SessionInitResponse as SessionInitResponse,
+ type SessionMessagesResponse as SessionMessagesResponse,
+ type SessionSummarizeResponse as SessionSummarizeResponse,
+ type SessionChatParams as SessionChatParams,
+ type SessionInitParams as SessionInitParams,
+ type SessionSummarizeParams as SessionSummarizeParams,
+ };
+
+ export {
+ Tui as Tui,
+ type TuiAppendPromptResponse as TuiAppendPromptResponse,
+ type TuiOpenHelpResponse as TuiOpenHelpResponse,
+ type TuiAppendPromptParams as TuiAppendPromptParams,
+ };
+
+ export type MessageAbortedError = API.MessageAbortedError;
+ export type ProviderAuthError = API.ProviderAuthError;
+ export type UnknownError = API.UnknownError;
+}
diff --git a/packages/sdk/src/core/README.md b/packages/sdk/src/core/README.md
new file mode 100644
index 000000000..485fce861
--- /dev/null
+++ b/packages/sdk/src/core/README.md
@@ -0,0 +1,3 @@
+# `core`
+
+This directory holds public modules implementing non-resource-specific SDK functionality.
diff --git a/packages/sdk/src/core/api-promise.ts b/packages/sdk/src/core/api-promise.ts
new file mode 100644
index 000000000..fc1c6dd76
--- /dev/null
+++ b/packages/sdk/src/core/api-promise.ts
@@ -0,0 +1,92 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { type Opencode } from '../client';
+
+import { type PromiseOrValue } from '../internal/types';
+import { APIResponseProps, defaultParseResponse } from '../internal/parse';
+
+/**
+ * A subclass of `Promise` providing additional helper methods
+ * for interacting with the SDK.
+ */
+export class APIPromise<T> extends Promise<T> {
+ private parsedPromise: Promise<T> | undefined;
+ #client: Opencode;
+
+ constructor(
+ client: Opencode,
+ private responsePromise: Promise<APIResponseProps>,
+ private parseResponse: (
+ client: Opencode,
+ props: APIResponseProps,
+ ) => PromiseOrValue<T> = defaultParseResponse,
+ ) {
+ super((resolve) => {
+ // this is maybe a bit weird but this has to be a no-op to not implicitly
+ // parse the response body; instead .then, .catch, .finally are overridden
+ // to parse the response
+ resolve(null as any);
+ });
+ this.#client = client;
+ }
+
+ _thenUnwrap<U>(transform: (data: T, props: APIResponseProps) => U): APIPromise<U> {
+ return new APIPromise(this.#client, this.responsePromise, async (client, props) =>
+ transform(await this.parseResponse(client, props), props),
+ );
+ }
+
+ /**
+ * Gets the raw `Response` instance instead of parsing the response
+ * data.
+ *
+ * If you want to parse the response body but still get the `Response`
+ * instance, you can use {@link withResponse()}.
+ *
+ * 👋 Getting the wrong TypeScript type for `Response`?
+ * Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
+ * to your `tsconfig.json`.
+ */
+ asResponse(): Promise<Response> {
+ return this.responsePromise.then((p) => p.response);
+ }
+
+ /**
+ * Gets the parsed response data and the raw `Response` instance.
+ *
+ * If you just want to get the raw `Response` instance without parsing it,
+ * you can use {@link asResponse()}.
+ *
+ * 👋 Getting the wrong TypeScript type for `Response`?
+ * Try setting `"moduleResolution": "NodeNext"` or add `"lib": ["DOM"]`
+ * to your `tsconfig.json`.
+ */
+ async withResponse(): Promise<{ data: T; response: Response }> {
+ const [data, response] = await Promise.all([this.parse(), this.asResponse()]);
+ return { data, response };
+ }
+
+ private parse(): Promise<T> {
+ if (!this.parsedPromise) {
+ this.parsedPromise = this.responsePromise.then((data) => this.parseResponse(this.#client, data));
+ }
+ return this.parsedPromise;
+ }
+
+ override then<TResult1 = T, TResult2 = never>(
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
+ ): Promise<TResult1 | TResult2> {
+ return this.parse().then(onfulfilled, onrejected);
+ }
+
+ override catch<TResult = never>(
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null,
+ ): Promise<T | TResult> {
+ return this.parse().catch(onrejected);
+ }
+
+ override finally(onfinally?: (() => void) | undefined | null): Promise<T> {
+ return this.parse().finally(onfinally);
+ }
+}
diff --git a/packages/sdk/src/core/error.ts b/packages/sdk/src/core/error.ts
new file mode 100644
index 000000000..44698c739
--- /dev/null
+++ b/packages/sdk/src/core/error.ts
@@ -0,0 +1,130 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { castToError } from '../internal/errors';
+
+export class OpencodeError extends Error {}
+
+export class APIError<
+ TStatus extends number | undefined = number | undefined,
+ THeaders extends Headers | undefined = Headers | undefined,
+ TError extends Object | undefined = Object | undefined,
+> extends OpencodeError {
+ /** HTTP status for the response that caused the error */
+ readonly status: TStatus;
+ /** HTTP headers for the response that caused the error */
+ readonly headers: THeaders;
+ /** JSON body of the response that caused the error */
+ readonly error: TError;
+
+ constructor(status: TStatus, error: TError, message: string | undefined, headers: THeaders) {
+ super(`${APIError.makeMessage(status, error, message)}`);
+ this.status = status;
+ this.headers = headers;
+ this.error = error;
+ }
+
+ private static makeMessage(status: number | undefined, error: any, message: string | undefined) {
+ const msg =
+ error?.message ?
+ typeof error.message === 'string' ?
+ error.message
+ : JSON.stringify(error.message)
+ : error ? JSON.stringify(error)
+ : message;
+
+ if (status && msg) {
+ return `${status} ${msg}`;
+ }
+ if (status) {
+ return `${status} status code (no body)`;
+ }
+ if (msg) {
+ return msg;
+ }
+ return '(no status code or body)';
+ }
+
+ static generate(
+ status: number | undefined,
+ errorResponse: Object | undefined,
+ message: string | undefined,
+ headers: Headers | undefined,
+ ): APIError {
+ if (!status || !headers) {
+ return new APIConnectionError({ message, cause: castToError(errorResponse) });
+ }
+
+ const error = errorResponse as Record<string, any>;
+
+ if (status === 400) {
+ return new BadRequestError(status, error, message, headers);
+ }
+
+ if (status === 401) {
+ return new AuthenticationError(status, error, message, headers);
+ }
+
+ if (status === 403) {
+ return new PermissionDeniedError(status, error, message, headers);
+ }
+
+ if (status === 404) {
+ return new NotFoundError(status, error, message, headers);
+ }
+
+ if (status === 409) {
+ return new ConflictError(status, error, message, headers);
+ }
+
+ if (status === 422) {
+ return new UnprocessableEntityError(status, error, message, headers);
+ }
+
+ if (status === 429) {
+ return new RateLimitError(status, error, message, headers);
+ }
+
+ if (status >= 500) {
+ return new InternalServerError(status, error, message, headers);
+ }
+
+ return new APIError(status, error, message, headers);
+ }
+}
+
+export class APIUserAbortError extends APIError<undefined, undefined, undefined> {
+ constructor({ message }: { message?: string } = {}) {
+ super(undefined, undefined, message || 'Request was aborted.', undefined);
+ }
+}
+
+export class APIConnectionError extends APIError<undefined, undefined, undefined> {
+ constructor({ message, cause }: { message?: string | undefined; cause?: Error | undefined }) {
+ super(undefined, undefined, message || 'Connection error.', undefined);
+ // in some environments the 'cause' property is already declared
+ // @ts-ignore
+ if (cause) this.cause = cause;
+ }
+}
+
+export class APIConnectionTimeoutError extends APIConnectionError {
+ constructor({ message }: { message?: string } = {}) {
+ super({ message: message ?? 'Request timed out.' });
+ }
+}
+
+export class BadRequestError extends APIError<400, Headers> {}
+
+export class AuthenticationError extends APIError<401, Headers> {}
+
+export class PermissionDeniedError extends APIError<403, Headers> {}
+
+export class NotFoundError extends APIError<404, Headers> {}
+
+export class ConflictError extends APIError<409, Headers> {}
+
+export class UnprocessableEntityError extends APIError<422, Headers> {}
+
+export class RateLimitError extends APIError<429, Headers> {}
+
+export class InternalServerError extends APIError<number, Headers> {}
diff --git a/packages/sdk/src/core/resource.ts b/packages/sdk/src/core/resource.ts
new file mode 100644
index 000000000..ccf9032e4
--- /dev/null
+++ b/packages/sdk/src/core/resource.ts
@@ -0,0 +1,11 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import type { Opencode } from '../client';
+
+export abstract class APIResource {
+ protected _client: Opencode;
+
+ constructor(client: Opencode) {
+ this._client = client;
+ }
+}
diff --git a/packages/sdk/src/core/streaming.ts b/packages/sdk/src/core/streaming.ts
new file mode 100644
index 000000000..33dcfd126
--- /dev/null
+++ b/packages/sdk/src/core/streaming.ts
@@ -0,0 +1,315 @@
+import { OpencodeError } from './error';
+import { type ReadableStream } from '../internal/shim-types';
+import { makeReadableStream } from '../internal/shims';
+import { findDoubleNewlineIndex, LineDecoder } from '../internal/decoders/line';
+import { ReadableStreamToAsyncIterable } from '../internal/shims';
+import { isAbortError } from '../internal/errors';
+import { encodeUTF8 } from '../internal/utils/bytes';
+import { loggerFor } from '../internal/utils/log';
+import type { Opencode } from '../client';
+
+type Bytes = string | ArrayBuffer | Uint8Array | null | undefined;
+
+export type ServerSentEvent = {
+ event: string | null;
+ data: string;
+ raw: string[];
+};
+
+export class Stream<Item> implements AsyncIterable<Item> {
+ controller: AbortController;
+ #client: Opencode | undefined;
+
+ constructor(
+ private iterator: () => AsyncIterator<Item>,
+ controller: AbortController,
+ client?: Opencode,
+ ) {
+ this.controller = controller;
+ this.#client = client;
+ }
+
+ static fromSSEResponse<Item>(
+ response: Response,
+ controller: AbortController,
+ client?: Opencode,
+ ): Stream<Item> {
+ let consumed = false;
+ const logger = client ? loggerFor(client) : console;
+
+ async function* iterator(): AsyncIterator<Item, any, undefined> {
+ if (consumed) {
+ throw new OpencodeError('Cannot iterate over a consumed stream, use `.tee()` to split the stream.');
+ }
+ consumed = true;
+ let done = false;
+ try {
+ for await (const sse of _iterSSEMessages(response, controller)) {
+ try {
+ yield JSON.parse(sse.data);
+ } catch (e) {
+ logger.error(`Could not parse message into JSON:`, sse.data);
+ logger.error(`From chunk:`, sse.raw);
+ throw e;
+ }
+ }
+ done = true;
+ } catch (e) {
+ // If the user calls `stream.controller.abort()`, we should exit without throwing.
+ if (isAbortError(e)) return;
+ throw e;
+ } finally {
+ // If the user `break`s, abort the ongoing request.
+ if (!done) controller.abort();
+ }
+ }
+
+ return new Stream(iterator, controller, client);
+ }
+
+ /**
+ * Generates a Stream from a newline-separated ReadableStream
+ * where each item is a JSON value.
+ */
+ static fromReadableStream<Item>(
+ readableStream: ReadableStream,
+ controller: AbortController,
+ client?: Opencode,
+ ): Stream<Item> {
+ let consumed = false;
+
+ async function* iterLines(): AsyncGenerator<string, void, unknown> {
+ const lineDecoder = new LineDecoder();
+
+ const iter = ReadableStreamToAsyncIterable<Bytes>(readableStream);
+ for await (const chunk of iter) {
+ for (const line of lineDecoder.decode(chunk)) {
+ yield line;
+ }
+ }
+
+ for (const line of lineDecoder.flush()) {
+ yield line;
+ }
+ }
+
+ async function* iterator(): AsyncIterator<Item, any, undefined> {
+ if (consumed) {
+ throw new OpencodeError('Cannot iterate over a consumed stream, use `.tee()` to split the stream.');
+ }
+ consumed = true;
+ let done = false;
+ try {
+ for await (const line of iterLines()) {
+ if (done) continue;
+ if (line) yield JSON.parse(line);
+ }
+ done = true;
+ } catch (e) {
+ // If the user calls `stream.controller.abort()`, we should exit without throwing.
+ if (isAbortError(e)) return;
+ throw e;
+ } finally {
+ // If the user `break`s, abort the ongoing request.
+ if (!done) controller.abort();
+ }
+ }
+
+ return new Stream(iterator, controller, client);
+ }
+
+ [Symbol.asyncIterator](): AsyncIterator<Item> {
+ return this.iterator();
+ }
+
+ /**
+ * Splits the stream into two streams which can be
+ * independently read from at different speeds.
+ */
+ tee(): [Stream<Item>, Stream<Item>] {
+ const left: Array<Promise<IteratorResult<Item>>> = [];
+ const right: Array<Promise<IteratorResult<Item>>> = [];
+ const iterator = this.iterator();
+
+ const teeIterator = (queue: Array<Promise<IteratorResult<Item>>>): AsyncIterator<Item> => {
+ return {
+ next: () => {
+ if (queue.length === 0) {
+ const result = iterator.next();
+ left.push(result);
+ right.push(result);
+ }
+ return queue.shift()!;
+ },
+ };
+ };
+
+ return [
+ new Stream(() => teeIterator(left), this.controller, this.#client),
+ new Stream(() => teeIterator(right), this.controller, this.#client),
+ ];
+ }
+
+ /**
+ * Converts this stream to a newline-separated ReadableStream of
+ * JSON stringified values in the stream
+ * which can be turned back into a Stream with `Stream.fromReadableStream()`.
+ */
+ toReadableStream(): ReadableStream {
+ const self = this;
+ let iter: AsyncIterator<Item>;
+
+ return makeReadableStream({
+ async start() {
+ iter = self[Symbol.asyncIterator]();
+ },
+ async pull(ctrl: any) {
+ try {
+ const { value, done } = await iter.next();
+ if (done) return ctrl.close();
+
+ const bytes = encodeUTF8(JSON.stringify(value) + '\n');
+
+ ctrl.enqueue(bytes);
+ } catch (err) {
+ ctrl.error(err);
+ }
+ },
+ async cancel() {
+ await iter.return?.();
+ },
+ });
+ }
+}
+
+export async function* _iterSSEMessages(
+ response: Response,
+ controller: AbortController,
+): AsyncGenerator<ServerSentEvent, void, unknown> {
+ if (!response.body) {
+ controller.abort();
+ if (
+ typeof (globalThis as any).navigator !== 'undefined' &&
+ (globalThis as any).navigator.product === 'ReactNative'
+ ) {
+ throw new OpencodeError(
+ `The default react-native fetch implementation does not support streaming. Please use expo/fetch: https://docs.expo.dev/versions/latest/sdk/expo/#expofetch-api`,
+ );
+ }
+ throw new OpencodeError(`Attempted to iterate over a response with no body`);
+ }
+
+ const sseDecoder = new SSEDecoder();
+ const lineDecoder = new LineDecoder();
+
+ const iter = ReadableStreamToAsyncIterable<Bytes>(response.body);
+ for await (const sseChunk of iterSSEChunks(iter)) {
+ for (const line of lineDecoder.decode(sseChunk)) {
+ const sse = sseDecoder.decode(line);
+ if (sse) yield sse;
+ }
+ }
+
+ for (const line of lineDecoder.flush()) {
+ const sse = sseDecoder.decode(line);
+ if (sse) yield sse;
+ }
+}
+
+/**
+ * Given an async iterable iterator, iterates over it and yields full
+ * SSE chunks, i.e. yields when a double new-line is encountered.
+ */
+async function* iterSSEChunks(iterator: AsyncIterableIterator<Bytes>): AsyncGenerator<Uint8Array> {
+ let data = new Uint8Array();
+
+ for await (const chunk of iterator) {
+ if (chunk == null) {
+ continue;
+ }
+
+ const binaryChunk =
+ chunk instanceof ArrayBuffer ? new Uint8Array(chunk)
+ : typeof chunk === 'string' ? encodeUTF8(chunk)
+ : chunk;
+
+ let newData = new Uint8Array(data.length + binaryChunk.length);
+ newData.set(data);
+ newData.set(binaryChunk, data.length);
+ data = newData;
+
+ let patternIndex;
+ while ((patternIndex = findDoubleNewlineIndex(data)) !== -1) {
+ yield data.slice(0, patternIndex);
+ data = data.slice(patternIndex);
+ }
+ }
+
+ if (data.length > 0) {
+ yield data;
+ }
+}
+
+class SSEDecoder {
+ private data: string[];
+ private event: string | null;
+ private chunks: string[];
+
+ constructor() {
+ this.event = null;
+ this.data = [];
+ this.chunks = [];
+ }
+
+ decode(line: string) {
+ if (line.endsWith('\r')) {
+ line = line.substring(0, line.length - 1);
+ }
+
+ if (!line) {
+ // empty line and we didn't previously encounter any messages
+ if (!this.event && !this.data.length) return null;
+
+ const sse: ServerSentEvent = {
+ event: this.event,
+ data: this.data.join('\n'),
+ raw: this.chunks,
+ };
+
+ this.event = null;
+ this.data = [];
+ this.chunks = [];
+
+ return sse;
+ }
+
+ this.chunks.push(line);
+
+ if (line.startsWith(':')) {
+ return null;
+ }
+
+ let [fieldname, _, value] = partition(line, ':');
+
+ if (value.startsWith(' ')) {
+ value = value.substring(1);
+ }
+
+ if (fieldname === 'event') {
+ this.event = value;
+ } else if (fieldname === 'data') {
+ this.data.push(value);
+ }
+
+ return null;
+ }
+}
+
+function partition(str: string, delimiter: string): [string, string, string] {
+ const index = str.indexOf(delimiter);
+ if (index !== -1) {
+ return [str.substring(0, index), delimiter, str.substring(index + delimiter.length)];
+ }
+
+ return [str, '', ''];
+}
diff --git a/packages/sdk/src/core/uploads.ts b/packages/sdk/src/core/uploads.ts
new file mode 100644
index 000000000..2882ca6d1
--- /dev/null
+++ b/packages/sdk/src/core/uploads.ts
@@ -0,0 +1,2 @@
+export { type Uploadable } from '../internal/uploads';
+export { toFile, type ToFileInput } from '../internal/to-file';
diff --git a/packages/sdk/src/error.ts b/packages/sdk/src/error.ts
new file mode 100644
index 000000000..fc55f46c0
--- /dev/null
+++ b/packages/sdk/src/error.ts
@@ -0,0 +1,2 @@
+/** @deprecated Import from ./core/error instead */
+export * from './core/error';
diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts
new file mode 100644
index 000000000..43d2f6363
--- /dev/null
+++ b/packages/sdk/src/index.ts
@@ -0,0 +1,22 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export { Opencode as default } from './client';
+
+export { type Uploadable, toFile } from './core/uploads';
+export { APIPromise } from './core/api-promise';
+export { Opencode, type ClientOptions } from './client';
+export {
+ OpencodeError,
+ APIError,
+ APIConnectionError,
+ APIConnectionTimeoutError,
+ APIUserAbortError,
+ NotFoundError,
+ ConflictError,
+ RateLimitError,
+ BadRequestError,
+ AuthenticationError,
+ InternalServerError,
+ PermissionDeniedError,
+ UnprocessableEntityError,
+} from './core/error';
diff --git a/packages/sdk/src/internal/README.md b/packages/sdk/src/internal/README.md
new file mode 100644
index 000000000..3ef5a25ba
--- /dev/null
+++ b/packages/sdk/src/internal/README.md
@@ -0,0 +1,3 @@
+# `internal`
+
+The modules in this directory are not importable outside this package and will change between releases.
diff --git a/packages/sdk/src/internal/builtin-types.ts b/packages/sdk/src/internal/builtin-types.ts
new file mode 100644
index 000000000..c23d3bded
--- /dev/null
+++ b/packages/sdk/src/internal/builtin-types.ts
@@ -0,0 +1,93 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export type Fetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
+
+/**
+ * An alias to the builtin `RequestInit` type so we can
+ * easily alias it in import statements if there are name clashes.
+ *
+ * https://developer.mozilla.org/docs/Web/API/RequestInit
+ */
+type _RequestInit = RequestInit;
+
+/**
+ * An alias to the builtin `Response` type so we can
+ * easily alias it in import statements if there are name clashes.
+ *
+ * https://developer.mozilla.org/docs/Web/API/Response
+ */
+type _Response = Response;
+
+/**
+ * The type for the first argument to `fetch`.
+ *
+ * https://developer.mozilla.org/docs/Web/API/Window/fetch#resource
+ */
+type _RequestInfo = Request | URL | string;
+
+/**
+ * The type for constructing `RequestInit` Headers.
+ *
+ * https://developer.mozilla.org/docs/Web/API/RequestInit#setting_headers
+ */
+type _HeadersInit = RequestInit['headers'];
+
+/**
+ * The type for constructing `RequestInit` body.
+ *
+ * https://developer.mozilla.org/docs/Web/API/RequestInit#body
+ */
+type _BodyInit = RequestInit['body'];
+
+/**
+ * An alias to the builtin `Array<T>` type so we can
+ * easily alias it in import statements if there are name clashes.
+ */
+type _Array<T> = Array<T>;
+
+/**
+ * An alias to the builtin `Record<K, T>` type so we can
+ * easily alias it in import statements if there are name clashes.
+ */
+type _Record<K extends keyof any, T> = Record<K, T>;
+
+export type {
+ _Array as Array,
+ _BodyInit as BodyInit,
+ _HeadersInit as HeadersInit,
+ _Record as Record,
+ _RequestInfo as RequestInfo,
+ _RequestInit as RequestInit,
+ _Response as Response,
+};
+
+/**
+ * A copy of the builtin `EndingType` type as it isn't fully supported in certain
+ * environments and attempting to reference the global version will error.
+ *
+ * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L27941
+ */
+type EndingType = 'native' | 'transparent';
+
+/**
+ * A copy of the builtin `BlobPropertyBag` type as it isn't fully supported in certain
+ * environments and attempting to reference the global version will error.
+ *
+ * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L154
+ * https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#options
+ */
+export interface BlobPropertyBag {
+ endings?: EndingType;
+ type?: string;
+}
+
+/**
+ * A copy of the builtin `FilePropertyBag` type as it isn't fully supported in certain
+ * environments and attempting to reference the global version will error.
+ *
+ * https://github.com/microsoft/TypeScript/blob/49ad1a3917a0ea57f5ff248159256e12bb1cb705/src/lib/dom.generated.d.ts#L503
+ * https://developer.mozilla.org/en-US/docs/Web/API/File/File#options
+ */
+export interface FilePropertyBag extends BlobPropertyBag {
+ lastModified?: number;
+}
diff --git a/packages/sdk/src/internal/decoders/line.ts b/packages/sdk/src/internal/decoders/line.ts
new file mode 100644
index 000000000..b3bfa97cd
--- /dev/null
+++ b/packages/sdk/src/internal/decoders/line.ts
@@ -0,0 +1,135 @@
+import { concatBytes, decodeUTF8, encodeUTF8 } from '../utils/bytes';
+
+export type Bytes = string | ArrayBuffer | Uint8Array | null | undefined;
+
+/**
+ * A re-implementation of httpx's `LineDecoder` in Python that handles incrementally
+ * reading lines from text.
+ *
+ * https://github.com/encode/httpx/blob/920333ea98118e9cf617f246905d7b202510941c/httpx/_decoders.py#L258
+ */
+export class LineDecoder {
+ // prettier-ignore
+ static NEWLINE_CHARS = new Set(['\n', '\r']);
+ static NEWLINE_REGEXP = /\r\n|[\n\r]/g;
+
+ #buffer: Uint8Array;
+ #carriageReturnIndex: number | null;
+
+ constructor() {
+ this.#buffer = new Uint8Array();
+ this.#carriageReturnIndex = null;
+ }
+
+ decode(chunk: Bytes): string[] {
+ if (chunk == null) {
+ return [];
+ }
+
+ const binaryChunk =
+ chunk instanceof ArrayBuffer ? new Uint8Array(chunk)
+ : typeof chunk === 'string' ? encodeUTF8(chunk)
+ : chunk;
+
+ this.#buffer = concatBytes([this.#buffer, binaryChunk]);
+
+ const lines: string[] = [];
+ let patternIndex;
+ while ((patternIndex = findNewlineIndex(this.#buffer, this.#carriageReturnIndex)) != null) {
+ if (patternIndex.carriage && this.#carriageReturnIndex == null) {
+ // skip until we either get a corresponding `\n`, a new `\r` or nothing
+ this.#carriageReturnIndex = patternIndex.index;
+ continue;
+ }
+
+ // we got double \r or \rtext\n
+ if (
+ this.#carriageReturnIndex != null &&
+ (patternIndex.index !== this.#carriageReturnIndex + 1 || patternIndex.carriage)
+ ) {
+ lines.push(decodeUTF8(this.#buffer.subarray(0, this.#carriageReturnIndex - 1)));
+ this.#buffer = this.#buffer.subarray(this.#carriageReturnIndex);
+ this.#carriageReturnIndex = null;
+ continue;
+ }
+
+ const endIndex =
+ this.#carriageReturnIndex !== null ? patternIndex.preceding - 1 : patternIndex.preceding;
+
+ const line = decodeUTF8(this.#buffer.subarray(0, endIndex));
+ lines.push(line);
+
+ this.#buffer = this.#buffer.subarray(patternIndex.index);
+ this.#carriageReturnIndex = null;
+ }
+
+ return lines;
+ }
+
+ flush(): string[] {
+ if (!this.#buffer.length) {
+ return [];
+ }
+ return this.decode('\n');
+ }
+}
+
+/**
+ * This function searches the buffer for the end patterns, (\r or \n)
+ * and returns an object with the index preceding the matched newline and the
+ * index after the newline char. `null` is returned if no new line is found.
+ *
+ * ```ts
+ * findNewLineIndex('abc\ndef') -> { preceding: 2, index: 3 }
+ * ```
+ */
+function findNewlineIndex(
+ buffer: Uint8Array,
+ startIndex: number | null,
+): { preceding: number; index: number; carriage: boolean } | null {
+ const newline = 0x0a; // \n
+ const carriage = 0x0d; // \r
+
+ for (let i = startIndex ?? 0; i < buffer.length; i++) {
+ if (buffer[i] === newline) {
+ return { preceding: i, index: i + 1, carriage: false };
+ }
+
+ if (buffer[i] === carriage) {
+ return { preceding: i, index: i + 1, carriage: true };
+ }
+ }
+
+ return null;
+}
+
+export function findDoubleNewlineIndex(buffer: Uint8Array): number {
+ // This function searches the buffer for the end patterns (\r\r, \n\n, \r\n\r\n)
+ // and returns the index right after the first occurrence of any pattern,
+ // or -1 if none of the patterns are found.
+ const newline = 0x0a; // \n
+ const carriage = 0x0d; // \r
+
+ for (let i = 0; i < buffer.length - 1; i++) {
+ if (buffer[i] === newline && buffer[i + 1] === newline) {
+ // \n\n
+ return i + 2;
+ }
+ if (buffer[i] === carriage && buffer[i + 1] === carriage) {
+ // \r\r
+ return i + 2;
+ }
+ if (
+ buffer[i] === carriage &&
+ buffer[i + 1] === newline &&
+ i + 3 < buffer.length &&
+ buffer[i + 2] === carriage &&
+ buffer[i + 3] === newline
+ ) {
+ // \r\n\r\n
+ return i + 4;
+ }
+ }
+
+ return -1;
+}
diff --git a/packages/sdk/src/internal/detect-platform.ts b/packages/sdk/src/internal/detect-platform.ts
new file mode 100644
index 000000000..e82d95c92
--- /dev/null
+++ b/packages/sdk/src/internal/detect-platform.ts
@@ -0,0 +1,196 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { VERSION } from '../version';
+
+export const isRunningInBrowser = () => {
+ return (
+ // @ts-ignore
+ typeof window !== 'undefined' &&
+ // @ts-ignore
+ typeof window.document !== 'undefined' &&
+ // @ts-ignore
+ typeof navigator !== 'undefined'
+ );
+};
+
+type DetectedPlatform = 'deno' | 'node' | 'edge' | 'unknown';
+
+/**
+ * Note this does not detect 'browser'; for that, use getBrowserInfo().
+ */
+function getDetectedPlatform(): DetectedPlatform {
+ if (typeof Deno !== 'undefined' && Deno.build != null) {
+ return 'deno';
+ }
+ if (typeof EdgeRuntime !== 'undefined') {
+ return 'edge';
+ }
+ if (
+ Object.prototype.toString.call(
+ typeof (globalThis as any).process !== 'undefined' ? (globalThis as any).process : 0,
+ ) === '[object process]'
+ ) {
+ return 'node';
+ }
+ return 'unknown';
+}
+
+declare const Deno: any;
+declare const EdgeRuntime: any;
+type Arch = 'x32' | 'x64' | 'arm' | 'arm64' | `other:${string}` | 'unknown';
+type PlatformName =
+ | 'MacOS'
+ | 'Linux'
+ | 'Windows'
+ | 'FreeBSD'
+ | 'OpenBSD'
+ | 'iOS'
+ | 'Android'
+ | `Other:${string}`
+ | 'Unknown';
+type Browser = 'ie' | 'edge' | 'chrome' | 'firefox' | 'safari';
+type PlatformProperties = {
+ 'X-Stainless-Lang': 'js';
+ 'X-Stainless-Package-Version': string;
+ 'X-Stainless-OS': PlatformName;
+ 'X-Stainless-Arch': Arch;
+ 'X-Stainless-Runtime': 'node' | 'deno' | 'edge' | `browser:${Browser}` | 'unknown';
+ 'X-Stainless-Runtime-Version': string;
+};
+const getPlatformProperties = (): PlatformProperties => {
+ const detectedPlatform = getDetectedPlatform();
+ if (detectedPlatform === 'deno') {
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': normalizePlatform(Deno.build.os),
+ 'X-Stainless-Arch': normalizeArch(Deno.build.arch),
+ 'X-Stainless-Runtime': 'deno',
+ 'X-Stainless-Runtime-Version':
+ typeof Deno.version === 'string' ? Deno.version : Deno.version?.deno ?? 'unknown',
+ };
+ }
+ if (typeof EdgeRuntime !== 'undefined') {
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': 'Unknown',
+ 'X-Stainless-Arch': `other:${EdgeRuntime}`,
+ 'X-Stainless-Runtime': 'edge',
+ 'X-Stainless-Runtime-Version': (globalThis as any).process.version,
+ };
+ }
+ // Check if Node.js
+ if (detectedPlatform === 'node') {
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': normalizePlatform((globalThis as any).process.platform ?? 'unknown'),
+ 'X-Stainless-Arch': normalizeArch((globalThis as any).process.arch ?? 'unknown'),
+ 'X-Stainless-Runtime': 'node',
+ 'X-Stainless-Runtime-Version': (globalThis as any).process.version ?? 'unknown',
+ };
+ }
+
+ const browserInfo = getBrowserInfo();
+ if (browserInfo) {
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': 'Unknown',
+ 'X-Stainless-Arch': 'unknown',
+ 'X-Stainless-Runtime': `browser:${browserInfo.browser}`,
+ 'X-Stainless-Runtime-Version': browserInfo.version,
+ };
+ }
+
+ // TODO add support for Cloudflare workers, etc.
+ return {
+ 'X-Stainless-Lang': 'js',
+ 'X-Stainless-Package-Version': VERSION,
+ 'X-Stainless-OS': 'Unknown',
+ 'X-Stainless-Arch': 'unknown',
+ 'X-Stainless-Runtime': 'unknown',
+ 'X-Stainless-Runtime-Version': 'unknown',
+ };
+};
+
+type BrowserInfo = {
+ browser: Browser;
+ version: string;
+};
+
+declare const navigator: { userAgent: string } | undefined;
+
+// Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts
+function getBrowserInfo(): BrowserInfo | null {
+ if (typeof navigator === 'undefined' || !navigator) {
+ return null;
+ }
+
+ // NOTE: The order matters here!
+ const browserPatterns = [
+ { key: 'edge' as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'ie' as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'ie' as const, pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'chrome' as const, pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'firefox' as const, pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
+ { key: 'safari' as const, pattern: /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/ },
+ ];
+
+ // Find the FIRST matching browser
+ for (const { key, pattern } of browserPatterns) {
+ const match = pattern.exec(navigator.userAgent);
+ if (match) {
+ const major = match[1] || 0;
+ const minor = match[2] || 0;
+ const patch = match[3] || 0;
+
+ return { browser: key, version: `${major}.${minor}.${patch}` };
+ }
+ }
+
+ return null;
+}
+
+const normalizeArch = (arch: string): Arch => {
+ // Node docs:
+ // - https://nodejs.org/api/process.html#processarch
+ // Deno docs:
+ // - https://doc.deno.land/deno/stable/~/Deno.build
+ if (arch === 'x32') return 'x32';
+ if (arch === 'x86_64' || arch === 'x64') return 'x64';
+ if (arch === 'arm') return 'arm';
+ if (arch === 'aarch64' || arch === 'arm64') return 'arm64';
+ if (arch) return `other:${arch}`;
+ return 'unknown';
+};
+
+const normalizePlatform = (platform: string): PlatformName => {
+ // Node platforms:
+ // - https://nodejs.org/api/process.html#processplatform
+ // Deno platforms:
+ // - https://doc.deno.land/deno/stable/~/Deno.build
+ // - https://github.com/denoland/deno/issues/14799
+
+ platform = platform.toLowerCase();
+
+ // NOTE: this iOS check is untested and may not work
+ // Node does not work natively on IOS, there is a fork at
+ // https://github.com/nodejs-mobile/nodejs-mobile
+ // however it is unknown at the time of writing how to detect if it is running
+ if (platform.includes('ios')) return 'iOS';
+ if (platform === 'android') return 'Android';
+ if (platform === 'darwin') return 'MacOS';
+ if (platform === 'win32') return 'Windows';
+ if (platform === 'freebsd') return 'FreeBSD';
+ if (platform === 'openbsd') return 'OpenBSD';
+ if (platform === 'linux') return 'Linux';
+ if (platform) return `Other:${platform}`;
+ return 'Unknown';
+};
+
+let _platformHeaders: PlatformProperties;
+export const getPlatformHeaders = () => {
+ return (_platformHeaders ??= getPlatformProperties());
+};
diff --git a/packages/sdk/src/internal/errors.ts b/packages/sdk/src/internal/errors.ts
new file mode 100644
index 000000000..82c7b14d5
--- /dev/null
+++ b/packages/sdk/src/internal/errors.ts
@@ -0,0 +1,33 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export function isAbortError(err: unknown) {
+ return (
+ typeof err === 'object' &&
+ err !== null &&
+ // Spec-compliant fetch implementations
+ (('name' in err && (err as any).name === 'AbortError') ||
+ // Expo fetch
+ ('message' in err && String((err as any).message).includes('FetchRequestCanceledException')))
+ );
+}
+
+export const castToError = (err: any): Error => {
+ if (err instanceof Error) return err;
+ if (typeof err === 'object' && err !== null) {
+ try {
+ if (Object.prototype.toString.call(err) === '[object Error]') {
+ // @ts-ignore - not all envs have native support for cause yet
+ const error = new Error(err.message, err.cause ? { cause: err.cause } : {});
+ if (err.stack) error.stack = err.stack;
+ // @ts-ignore - not all envs have native support for cause yet
+ if (err.cause && !error.cause) error.cause = err.cause;
+ if (err.name) error.name = err.name;
+ return error;
+ }
+ } catch {}
+ try {
+ return new Error(JSON.stringify(err));
+ } catch {}
+ }
+ return new Error(err);
+};
diff --git a/packages/sdk/src/internal/headers.ts b/packages/sdk/src/internal/headers.ts
new file mode 100644
index 000000000..c724a9d22
--- /dev/null
+++ b/packages/sdk/src/internal/headers.ts
@@ -0,0 +1,97 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { isReadonlyArray } from './utils/values';
+
+type HeaderValue = string | undefined | null;
+export type HeadersLike =
+ | Headers
+ | readonly HeaderValue[][]
+ | Record<string, HeaderValue | readonly HeaderValue[]>
+ | undefined
+ | null
+ | NullableHeaders;
+
+const brand_privateNullableHeaders = /* @__PURE__ */ Symbol('brand.privateNullableHeaders');
+
+/**
+ * @internal
+ * Users can pass explicit nulls to unset default headers. When we parse them
+ * into a standard headers type we need to preserve that information.
+ */
+export type NullableHeaders = {
+ /** Brand check, prevent users from creating a NullableHeaders. */
+ [brand_privateNullableHeaders]: true;
+ /** Parsed headers. */
+ values: Headers;
+ /** Set of lowercase header names explicitly set to null. */
+ nulls: Set<string>;
+};
+
+function* iterateHeaders(headers: HeadersLike): IterableIterator<readonly [string, string | null]> {
+ if (!headers) return;
+
+ if (brand_privateNullableHeaders in headers) {
+ const { values, nulls } = headers;
+ yield* values.entries();
+ for (const name of nulls) {
+ yield [name, null];
+ }
+ return;
+ }
+
+ let shouldClear = false;
+ let iter: Iterable<readonly (HeaderValue | readonly HeaderValue[])[]>;
+ if (headers instanceof Headers) {
+ iter = headers.entries();
+ } else if (isReadonlyArray(headers)) {
+ iter = headers;
+ } else {
+ shouldClear = true;
+ iter = Object.entries(headers ?? {});
+ }
+ for (let row of iter) {
+ const name = row[0];
+ if (typeof name !== 'string') throw new TypeError('expected header name to be a string');
+ const values = isReadonlyArray(row[1]) ? row[1] : [row[1]];
+ let didClear = false;
+ for (const value of values) {
+ if (value === undefined) continue;
+
+ // Objects keys always overwrite older headers, they never append.
+ // Yield a null to clear the header before adding the new values.
+ if (shouldClear && !didClear) {
+ didClear = true;
+ yield [name, null];
+ }
+ yield [name, value];
+ }
+ }
+}
+
+export const buildHeaders = (newHeaders: HeadersLike[]): NullableHeaders => {
+ const targetHeaders = new Headers();
+ const nullHeaders = new Set<string>();
+ for (const headers of newHeaders) {
+ const seenHeaders = new Set<string>();
+ for (const [name, value] of iterateHeaders(headers)) {
+ const lowerName = name.toLowerCase();
+ if (!seenHeaders.has(lowerName)) {
+ targetHeaders.delete(name);
+ seenHeaders.add(lowerName);
+ }
+ if (value === null) {
+ targetHeaders.delete(name);
+ nullHeaders.add(lowerName);
+ } else {
+ targetHeaders.append(name, value);
+ nullHeaders.delete(lowerName);
+ }
+ }
+ }
+ return { [brand_privateNullableHeaders]: true, values: targetHeaders, nulls: nullHeaders };
+};
+
+export const isEmptyHeaders = (headers: HeadersLike) => {
+ for (const _ of iterateHeaders(headers)) return false;
+ return true;
+};
diff --git a/packages/sdk/src/internal/parse.ts b/packages/sdk/src/internal/parse.ts
new file mode 100644
index 000000000..dcb4026e3
--- /dev/null
+++ b/packages/sdk/src/internal/parse.ts
@@ -0,0 +1,64 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import type { FinalRequestOptions } from './request-options';
+import { Stream } from '../core/streaming';
+import { type Opencode } from '../client';
+import { formatRequestDetails, loggerFor } from './utils/log';
+
+export type APIResponseProps = {
+ response: Response;
+ options: FinalRequestOptions;
+ controller: AbortController;
+ requestLogID: string;
+ retryOfRequestLogID: string | undefined;
+ startTime: number;
+};
+
+export async function defaultParseResponse<T>(client: Opencode, props: APIResponseProps): Promise<T> {
+ const { response, requestLogID, retryOfRequestLogID, startTime } = props;
+ const body = await (async () => {
+ if (props.options.stream) {
+ loggerFor(client).debug('response', response.status, response.url, response.headers, response.body);
+
+ // Note: there is an invariant here that isn't represented in the type system
+ // that if you set `stream: true` the response type must also be `Stream<T>`
+
+ if (props.options.__streamClass) {
+ return props.options.__streamClass.fromSSEResponse(response, props.controller, client) as any;
+ }
+
+ return Stream.fromSSEResponse(response, props.controller, client) as any;
+ }
+
+ // fetch refuses to read the body when the status code is 204.
+ if (response.status === 204) {
+ return null as T;
+ }
+
+ if (props.options.__binaryResponse) {
+ return response as unknown as T;
+ }
+
+ const contentType = response.headers.get('content-type');
+ const mediaType = contentType?.split(';')[0]?.trim();
+ const isJSON = mediaType?.includes('application/json') || mediaType?.endsWith('+json');
+ if (isJSON) {
+ const json = await response.json();
+ return json as T;
+ }
+
+ const text = await response.text();
+ return text as unknown as T;
+ })();
+ loggerFor(client).debug(
+ `[${requestLogID}] response parsed`,
+ formatRequestDetails({
+ retryOfRequestLogID,
+ url: response.url,
+ status: response.status,
+ body,
+ durationMs: Date.now() - startTime,
+ }),
+ );
+ return body;
+}
diff --git a/packages/sdk/src/internal/request-options.ts b/packages/sdk/src/internal/request-options.ts
new file mode 100644
index 000000000..56765e5aa
--- /dev/null
+++ b/packages/sdk/src/internal/request-options.ts
@@ -0,0 +1,93 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { NullableHeaders } from './headers';
+
+import type { BodyInit } from './builtin-types';
+import { Stream } from '../core/streaming';
+import type { HTTPMethod, MergedRequestInit } from './types';
+import { type HeadersLike } from './headers';
+
+export type FinalRequestOptions = RequestOptions & { method: HTTPMethod; path: string };
+
+export type RequestOptions = {
+ /**
+ * The HTTP method for the request (e.g., 'get', 'post', 'put', 'delete').
+ */
+ method?: HTTPMethod;
+
+ /**
+ * The URL path for the request.
+ *
+ * @example "/v1/foo"
+ */
+ path?: string;
+
+ /**
+ * Query parameters to include in the request URL.
+ */
+ query?: object | undefined | null;
+
+ /**
+ * The request body. Can be a string, JSON object, FormData, or other supported types.
+ */
+ body?: unknown;
+
+ /**
+ * HTTP headers to include with the request. Can be a Headers object, plain object, or array of tuples.
+ */
+ headers?: HeadersLike;
+
+ /**
+ * The maximum number of times that the client will retry a request in case of a
+ * temporary failure, like a network error or a 5XX error from the server.
+ *
+ * @default 2
+ */
+ maxRetries?: number;
+
+ stream?: boolean | undefined;
+
+ /**
+ * The maximum amount of time (in milliseconds) that the client should wait for a response
+ * from the server before timing out a single request.
+ *
+ * @unit milliseconds
+ */
+ timeout?: number;
+
+ /**
+ * Additional `RequestInit` options to be passed to the underlying `fetch` call.
+ * These options will be merged with the client's default fetch options.
+ */
+ fetchOptions?: MergedRequestInit;
+
+ /**
+ * An AbortSignal that can be used to cancel the request.
+ */
+ signal?: AbortSignal | undefined | null;
+
+ /**
+ * A unique key for this request to enable idempotency.
+ */
+ idempotencyKey?: string;
+
+ /**
+ * Override the default base URL for this specific request.
+ */
+ defaultBaseURL?: string | undefined;
+
+ __binaryResponse?: boolean | undefined;
+ __streamClass?: typeof Stream;
+};
+
+export type EncodedContent = { bodyHeaders: HeadersLike; body: BodyInit };
+export type RequestEncoder = (request: { headers: NullableHeaders; body: unknown }) => EncodedContent;
+
+export const FallbackEncoder: RequestEncoder = ({ headers, body }) => {
+ return {
+ bodyHeaders: {
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify(body),
+ };
+};
diff --git a/packages/sdk/src/internal/shim-types.ts b/packages/sdk/src/internal/shim-types.ts
new file mode 100644
index 000000000..8ddf7b0ad
--- /dev/null
+++ b/packages/sdk/src/internal/shim-types.ts
@@ -0,0 +1,26 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+/**
+ * Shims for types that we can't always rely on being available globally.
+ *
+ * Note: these only exist at the type-level, there is no corresponding runtime
+ * version for any of these symbols.
+ */
+
+type NeverToAny<T> = T extends never ? any : T;
+
+/** @ts-ignore */
+type _DOMReadableStream<R = any> = globalThis.ReadableStream<R>;
+
+/** @ts-ignore */
+type _NodeReadableStream<R = any> = import('stream/web').ReadableStream<R>;
+
+type _ConditionalNodeReadableStream<R = any> =
+ typeof globalThis extends { ReadableStream: any } ? never : _NodeReadableStream<R>;
+
+type _ReadableStream<R = any> = NeverToAny<
+ | ([0] extends [1 & _DOMReadableStream<R>] ? never : _DOMReadableStream<R>)
+ | ([0] extends [1 & _ConditionalNodeReadableStream<R>] ? never : _ConditionalNodeReadableStream<R>)
+>;
+
+export type { _ReadableStream as ReadableStream };
diff --git a/packages/sdk/src/internal/shims.ts b/packages/sdk/src/internal/shims.ts
new file mode 100644
index 000000000..17a7967aa
--- /dev/null
+++ b/packages/sdk/src/internal/shims.ts
@@ -0,0 +1,107 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+/**
+ * This module provides internal shims and utility functions for environments where certain Node.js or global types may not be available.
+ *
+ * These are used to ensure we can provide a consistent behaviour between different JavaScript environments and good error
+ * messages in cases where an environment isn't fully supported.
+ */
+
+import type { Fetch } from './builtin-types';
+import type { ReadableStream } from './shim-types';
+
+export function getDefaultFetch(): Fetch {
+ if (typeof fetch !== 'undefined') {
+ return fetch as any;
+ }
+
+ throw new Error(
+ '`fetch` is not defined as a global; Either pass `fetch` to the client, `new Opencode({ fetch })` or polyfill the global, `globalThis.fetch = fetch`',
+ );
+}
+
+type ReadableStreamArgs = ConstructorParameters<typeof ReadableStream>;
+
+export function makeReadableStream(...args: ReadableStreamArgs): ReadableStream {
+ const ReadableStream = (globalThis as any).ReadableStream;
+ if (typeof ReadableStream === 'undefined') {
+ // Note: All of the platforms / runtimes we officially support already define
+ // `ReadableStream` as a global, so this should only ever be hit on unsupported runtimes.
+ throw new Error(
+ '`ReadableStream` is not defined as a global; You will need to polyfill it, `globalThis.ReadableStream = ReadableStream`',
+ );
+ }
+
+ return new ReadableStream(...args);
+}
+
+export function ReadableStreamFrom<T>(iterable: Iterable<T> | AsyncIterable<T>): ReadableStream<T> {
+ let iter: AsyncIterator<T> | Iterator<T> =
+ Symbol.asyncIterator in iterable ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator]();
+
+ return makeReadableStream({
+ start() {},
+ async pull(controller: any) {
+ const { done, value } = await iter.next();
+ if (done) {
+ controller.close();
+ } else {
+ controller.enqueue(value);
+ }
+ },
+ async cancel() {
+ await iter.return?.();
+ },
+ });
+}
+
+/**
+ * Most browsers don't yet have async iterable support for ReadableStream,
+ * and Node has a very different way of reading bytes from its "ReadableStream".
+ *
+ * This polyfill was pulled from https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
+ */
+export function ReadableStreamToAsyncIterable<T>(stream: any): AsyncIterableIterator<T> {
+ if (stream[Symbol.asyncIterator]) return stream;
+
+ const reader = stream.getReader();
+ return {
+ async next() {
+ try {
+ const result = await reader.read();
+ if (result?.done) reader.releaseLock(); // release lock when stream becomes closed
+ return result;
+ } catch (e) {
+ reader.releaseLock(); // release lock when stream becomes errored
+ throw e;
+ }
+ },
+ async return() {
+ const cancelPromise = reader.cancel();
+ reader.releaseLock();
+ await cancelPromise;
+ return { done: true, value: undefined };
+ },
+ [Symbol.asyncIterator]() {
+ return this;
+ },
+ };
+}
+
+/**
+ * Cancels a ReadableStream we don't need to consume.
+ * See https://undici.nodejs.org/#/?id=garbage-collection
+ */
+export async function CancelReadableStream(stream: any): Promise<void> {
+ if (stream === null || typeof stream !== 'object') return;
+
+ if (stream[Symbol.asyncIterator]) {
+ await stream[Symbol.asyncIterator]().return?.();
+ return;
+ }
+
+ const reader = stream.getReader();
+ const cancelPromise = reader.cancel();
+ reader.releaseLock();
+ await cancelPromise;
+}
diff --git a/packages/sdk/src/internal/to-file.ts b/packages/sdk/src/internal/to-file.ts
new file mode 100644
index 000000000..245e84933
--- /dev/null
+++ b/packages/sdk/src/internal/to-file.ts
@@ -0,0 +1,154 @@
+import { BlobPart, getName, makeFile, isAsyncIterable } from './uploads';
+import type { FilePropertyBag } from './builtin-types';
+import { checkFileSupport } from './uploads';
+
+type BlobLikePart = string | ArrayBuffer | ArrayBufferView | BlobLike | DataView;
+
+/**
+ * Intended to match DOM Blob, node-fetch Blob, node:buffer Blob, etc.
+ * Don't add arrayBuffer here, node-fetch doesn't have it
+ */
+interface BlobLike {
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/size) */
+ readonly size: number;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/type) */
+ readonly type: string;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/text) */
+ text(): Promise<string>;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Blob/slice) */
+ slice(start?: number, end?: number): BlobLike;
+}
+
+/**
+ * This check adds the arrayBuffer() method type because it is available and used at runtime
+ */
+const isBlobLike = (value: any): value is BlobLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.size === 'number' &&
+ typeof value.type === 'string' &&
+ typeof value.text === 'function' &&
+ typeof value.slice === 'function' &&
+ typeof value.arrayBuffer === 'function';
+
+/**
+ * Intended to match DOM File, node:buffer File, undici File, etc.
+ */
+interface FileLike extends BlobLike {
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/lastModified) */
+ readonly lastModified: number;
+ /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/File/name) */
+ readonly name?: string | undefined;
+}
+
+/**
+ * This check adds the arrayBuffer() method type because it is available and used at runtime
+ */
+const isFileLike = (value: any): value is FileLike & { arrayBuffer(): Promise<ArrayBuffer> } =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.name === 'string' &&
+ typeof value.lastModified === 'number' &&
+ isBlobLike(value);
+
+/**
+ * Intended to match DOM Response, node-fetch Response, undici Response, etc.
+ */
+export interface ResponseLike {
+ url: string;
+ blob(): Promise<BlobLike>;
+}
+
+const isResponseLike = (value: any): value is ResponseLike =>
+ value != null &&
+ typeof value === 'object' &&
+ typeof value.url === 'string' &&
+ typeof value.blob === 'function';
+
+export type ToFileInput =
+ | FileLike
+ | ResponseLike
+ | Exclude<BlobLikePart, string>
+ | AsyncIterable<BlobLikePart>;
+
+/**
+ * Helper for creating a {@link File} to pass to an SDK upload method from a variety of different data formats
+ * @param value the raw content of the file. Can be an {@link Uploadable}, {@link BlobLikePart}, or {@link AsyncIterable} of {@link BlobLikePart}s
+ * @param {string=} name the name of the file. If omitted, toFile will try to determine a file name from bits if possible
+ * @param {Object=} options additional properties
+ * @param {string=} options.type the MIME type of the content
+ * @param {number=} options.lastModified the last modified timestamp
+ * @returns a {@link File} with the given properties
+ */
+export async function toFile(
+ value: ToFileInput | PromiseLike<ToFileInput>,
+ name?: string | null | undefined,
+ options?: FilePropertyBag | undefined,
+): Promise<File> {
+ checkFileSupport();
+
+ // If it's a promise, resolve it.
+ value = await value;
+
+ // If we've been given a `File` we don't need to do anything
+ if (isFileLike(value)) {
+ if (value instanceof File) {
+ return value;
+ }
+ return makeFile([await value.arrayBuffer()], value.name);
+ }
+
+ if (isResponseLike(value)) {
+ const blob = await value.blob();
+ name ||= new URL(value.url).pathname.split(/[\\/]/).pop();
+
+ return makeFile(await getBytes(blob), name, options);
+ }
+
+ const parts = await getBytes(value);
+
+ name ||= getName(value);
+
+ if (!options?.type) {
+ const type = parts.find((part) => typeof part === 'object' && 'type' in part && part.type);
+ if (typeof type === 'string') {
+ options = { ...options, type };
+ }
+ }
+
+ return makeFile(parts, name, options);
+}
+
+async function getBytes(value: BlobLikePart | AsyncIterable<BlobLikePart>): Promise<Array<BlobPart>> {
+ let parts: Array<BlobPart> = [];
+ if (
+ typeof value === 'string' ||
+ ArrayBuffer.isView(value) || // includes Uint8Array, Buffer, etc.
+ value instanceof ArrayBuffer
+ ) {
+ parts.push(value);
+ } else if (isBlobLike(value)) {
+ parts.push(value instanceof Blob ? value : await value.arrayBuffer());
+ } else if (
+ isAsyncIterable(value) // includes Readable, ReadableStream, etc.
+ ) {
+ for await (const chunk of value) {
+ parts.push(...(await getBytes(chunk as BlobLikePart))); // TODO, consider validating?
+ }
+ } else {
+ const constructor = value?.constructor?.name;
+ throw new Error(
+ `Unexpected data type: ${typeof value}${
+ constructor ? `; constructor: ${constructor}` : ''
+ }${propsForError(value)}`,
+ );
+ }
+
+ return parts;
+}
+
+function propsForError(value: unknown): string {
+ if (typeof value !== 'object' || value === null) return '';
+ const props = Object.getOwnPropertyNames(value);
+ return `; props: [${props.map((p) => `"${p}"`).join(', ')}]`;
+}
diff --git a/packages/sdk/src/internal/types.ts b/packages/sdk/src/internal/types.ts
new file mode 100644
index 000000000..b668dfc0f
--- /dev/null
+++ b/packages/sdk/src/internal/types.ts
@@ -0,0 +1,95 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export type PromiseOrValue<T> = T | Promise<T>;
+export type HTTPMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
+
+export type KeysEnum<T> = { [P in keyof Required<T>]: true };
+
+export type FinalizedRequestInit = RequestInit & { headers: Headers };
+
+type NotAny<T> = [0] extends [1 & T] ? never : T;
+
+/**
+ * Some environments overload the global fetch function, and Parameters<T> only gets the last signature.
+ */
+type OverloadedParameters<T> =
+ T extends (
+ {
+ (...args: infer A): unknown;
+ (...args: infer B): unknown;
+ (...args: infer C): unknown;
+ (...args: infer D): unknown;
+ }
+ ) ?
+ A | B | C | D
+ : T extends (
+ {
+ (...args: infer A): unknown;
+ (...args: infer B): unknown;
+ (...args: infer C): unknown;
+ }
+ ) ?
+ A | B | C
+ : T extends (
+ {
+ (...args: infer A): unknown;
+ (...args: infer B): unknown;
+ }
+ ) ?
+ A | B
+ : T extends (...args: infer A) => unknown ? A
+ : never;
+
+/* eslint-disable */
+/**
+ * These imports attempt to get types from a parent package's dependencies.
+ * Unresolved bare specifiers can trigger [automatic type acquisition][1] in some projects, which
+ * would cause typescript to show types not present at runtime. To avoid this, we import
+ * directly from parent node_modules folders.
+ *
+ * We need to check multiple levels because we don't know what directory structure we'll be in.
+ * For example, pnpm generates directories like this:
+ * ```
+ * node_modules
+ * ├── .pnpm
+ * │ └── [email protected]
+ * │ └── node_modules
+ * │ └── pkg
+ * │ └── internal
+ * │ └── types.d.ts
+ * ├── pkg -> .pnpm/[email protected]/node_modules/pkg
+ * └── undici
+ * ```
+ *
+ * [1]: https://www.typescriptlang.org/tsconfig/#typeAcquisition
+ */
+/** @ts-ignore For users with \@types/node */
+type UndiciTypesRequestInit = NotAny<import('../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/undici-types/index.d.ts').RequestInit>;
+/** @ts-ignore For users with undici */
+type UndiciRequestInit = NotAny<import('../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/undici/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/undici/index.d.ts').RequestInit>;
+/** @ts-ignore For users with \@types/bun */
+type BunRequestInit = globalThis.FetchRequestInit;
+/** @ts-ignore For users with node-fetch@2 */
+type NodeFetch2RequestInit = NotAny<import('../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/@types/node-fetch/index.d.ts').RequestInit>;
+/** @ts-ignore For users with node-fetch@3, doesn't need file extension because types are at ./@types/index.d.ts */
+type NodeFetch3RequestInit = NotAny<import('../node_modules/node-fetch').RequestInit> | NotAny<import('../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../../node_modules/node-fetch').RequestInit> | NotAny<import('../../../../../../../../../../node_modules/node-fetch').RequestInit>;
+/** @ts-ignore For users who use Deno */
+type FetchRequestInit = NonNullable<OverloadedParameters<typeof fetch>[1]>;
+/* eslint-enable */
+
+type RequestInits =
+ | NotAny<UndiciTypesRequestInit>
+ | NotAny<UndiciRequestInit>
+ | NotAny<BunRequestInit>
+ | NotAny<NodeFetch2RequestInit>
+ | NotAny<NodeFetch3RequestInit>
+ | NotAny<RequestInit>
+ | NotAny<FetchRequestInit>;
+
+/**
+ * This type contains `RequestInit` options that may be available on the current runtime,
+ * including per-platform extensions like `dispatcher`, `agent`, `client`, etc.
+ */
+export type MergedRequestInit = RequestInits &
+ /** We don't include these in the types as they'll be overridden for every request. */
+ Partial<Record<'body' | 'headers' | 'method' | 'signal', never>>;
diff --git a/packages/sdk/src/internal/uploads.ts b/packages/sdk/src/internal/uploads.ts
new file mode 100644
index 000000000..eb55f834e
--- /dev/null
+++ b/packages/sdk/src/internal/uploads.ts
@@ -0,0 +1,187 @@
+import { type RequestOptions } from './request-options';
+import type { FilePropertyBag, Fetch } from './builtin-types';
+import type { Opencode } from '../client';
+import { ReadableStreamFrom } from './shims';
+
+export type BlobPart = string | ArrayBuffer | ArrayBufferView | Blob | DataView;
+type FsReadStream = AsyncIterable<Uint8Array> & { path: string | { toString(): string } };
+
+// https://github.com/oven-sh/bun/issues/5980
+interface BunFile extends Blob {
+ readonly name?: string | undefined;
+}
+
+export const checkFileSupport = () => {
+ if (typeof File === 'undefined') {
+ const { process } = globalThis as any;
+ const isOldNode =
+ typeof process?.versions?.node === 'string' && parseInt(process.versions.node.split('.')) < 20;
+ throw new Error(
+ '`File` is not defined as a global, which is required for file uploads.' +
+ (isOldNode ?
+ " Update to Node 20 LTS or newer, or set `globalThis.File` to `import('node:buffer').File`."
+ : ''),
+ );
+ }
+};
+
+/**
+ * Typically, this is a native "File" class.
+ *
+ * We provide the {@link toFile} utility to convert a variety of objects
+ * into the File class.
+ *
+ * For convenience, you can also pass a fetch Response, or in Node,
+ * the result of fs.createReadStream().
+ */
+export type Uploadable = File | Response | FsReadStream | BunFile;
+
+/**
+ * Construct a `File` instance. This is used to ensure a helpful error is thrown
+ * for environments that don't define a global `File` yet.
+ */
+export function makeFile(
+ fileBits: BlobPart[],
+ fileName: string | undefined,
+ options?: FilePropertyBag,
+): File {
+ checkFileSupport();
+ return new File(fileBits as any, fileName ?? 'unknown_file', options);
+}
+
+export function getName(value: any): string | undefined {
+ return (
+ (
+ (typeof value === 'object' &&
+ value !== null &&
+ (('name' in value && value.name && String(value.name)) ||
+ ('url' in value && value.url && String(value.url)) ||
+ ('filename' in value && value.filename && String(value.filename)) ||
+ ('path' in value && value.path && String(value.path)))) ||
+ ''
+ )
+ .split(/[\\/]/)
+ .pop() || undefined
+ );
+}
+
+export const isAsyncIterable = (value: any): value is AsyncIterable<any> =>
+ value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function';
+
+/**
+ * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value.
+ * Otherwise returns the request as is.
+ */
+export const maybeMultipartFormRequestOptions = async (
+ opts: RequestOptions,
+ fetch: Opencode | Fetch,
+): Promise<RequestOptions> => {
+ if (!hasUploadableValue(opts.body)) return opts;
+
+ return { ...opts, body: await createForm(opts.body, fetch) };
+};
+
+type MultipartFormRequestOptions = Omit<RequestOptions, 'body'> & { body: unknown };
+
+export const multipartFormRequestOptions = async (
+ opts: MultipartFormRequestOptions,
+ fetch: Opencode | Fetch,
+): Promise<RequestOptions> => {
+ return { ...opts, body: await createForm(opts.body, fetch) };
+};
+
+const supportsFormDataMap = /* @__PURE__ */ new WeakMap<Fetch, Promise<boolean>>();
+
+/**
+ * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending
+ * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]".
+ * This function detects if the fetch function provided supports the global FormData object to avoid
+ * confusing error messages later on.
+ */
+function supportsFormData(fetchObject: Opencode | Fetch): Promise<boolean> {
+ const fetch: Fetch = typeof fetchObject === 'function' ? fetchObject : (fetchObject as any).fetch;
+ const cached = supportsFormDataMap.get(fetch);
+ if (cached) return cached;
+ const promise = (async () => {
+ try {
+ const FetchResponse = (
+ 'Response' in fetch ?
+ fetch.Response
+ : (await fetch('data:,')).constructor) as typeof Response;
+ const data = new FormData();
+ if (data.toString() === (await new FetchResponse(data).text())) {
+ return false;
+ }
+ return true;
+ } catch {
+ // avoid false negatives
+ return true;
+ }
+ })();
+ supportsFormDataMap.set(fetch, promise);
+ return promise;
+}
+
+export const createForm = async <T = Record<string, unknown>>(
+ body: T | undefined,
+ fetch: Opencode | Fetch,
+): Promise<FormData> => {
+ if (!(await supportsFormData(fetch))) {
+ throw new TypeError(
+ 'The provided fetch function does not support file uploads with the current global FormData class.',
+ );
+ }
+ const form = new FormData();
+ await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value)));
+ return form;
+};
+
+// We check for Blob not File because Bun.File doesn't inherit from File,
+// but they both inherit from Blob and have a `name` property at runtime.
+const isNamedBlob = (value: unknown) => value instanceof Blob && 'name' in value;
+
+const isUploadable = (value: unknown) =>
+ typeof value === 'object' &&
+ value !== null &&
+ (value instanceof Response || isAsyncIterable(value) || isNamedBlob(value));
+
+const hasUploadableValue = (value: unknown): boolean => {
+ if (isUploadable(value)) return true;
+ if (Array.isArray(value)) return value.some(hasUploadableValue);
+ if (value && typeof value === 'object') {
+ for (const k in value) {
+ if (hasUploadableValue((value as any)[k])) return true;
+ }
+ }
+ return false;
+};
+
+const addFormValue = async (form: FormData, key: string, value: unknown): Promise<void> => {
+ if (value === undefined) return;
+ if (value == null) {
+ throw new TypeError(
+ `Received null for "${key}"; to pass null in FormData, you must use the string 'null'`,
+ );
+ }
+
+ // TODO: make nested formats configurable
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ form.append(key, String(value));
+ } else if (value instanceof Response) {
+ form.append(key, makeFile([await value.blob()], getName(value)));
+ } else if (isAsyncIterable(value)) {
+ form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value)));
+ } else if (isNamedBlob(value)) {
+ form.append(key, value, getName(value));
+ } else if (Array.isArray(value)) {
+ await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry)));
+ } else if (typeof value === 'object') {
+ await Promise.all(
+ Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)),
+ );
+ } else {
+ throw new TypeError(
+ `Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`,
+ );
+ }
+};
diff --git a/packages/sdk/src/internal/utils.ts b/packages/sdk/src/internal/utils.ts
new file mode 100644
index 000000000..3cbfacce2
--- /dev/null
+++ b/packages/sdk/src/internal/utils.ts
@@ -0,0 +1,8 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export * from './utils/values';
+export * from './utils/base64';
+export * from './utils/env';
+export * from './utils/log';
+export * from './utils/uuid';
+export * from './utils/sleep';
diff --git a/packages/sdk/src/internal/utils/base64.ts b/packages/sdk/src/internal/utils/base64.ts
new file mode 100644
index 000000000..05d7bd9b4
--- /dev/null
+++ b/packages/sdk/src/internal/utils/base64.ts
@@ -0,0 +1,40 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { OpencodeError } from '../../core/error';
+import { encodeUTF8 } from './bytes';
+
+export const toBase64 = (data: string | Uint8Array | null | undefined): string => {
+ if (!data) return '';
+
+ if (typeof (globalThis as any).Buffer !== 'undefined') {
+ return (globalThis as any).Buffer.from(data).toString('base64');
+ }
+
+ if (typeof data === 'string') {
+ data = encodeUTF8(data);
+ }
+
+ if (typeof btoa !== 'undefined') {
+ return btoa(String.fromCharCode.apply(null, data as any));
+ }
+
+ throw new OpencodeError('Cannot generate base64 string; Expected `Buffer` or `btoa` to be defined');
+};
+
+export const fromBase64 = (str: string): Uint8Array => {
+ if (typeof (globalThis as any).Buffer !== 'undefined') {
+ const buf = (globalThis as any).Buffer.from(str, 'base64');
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
+ }
+
+ if (typeof atob !== 'undefined') {
+ const bstr = atob(str);
+ const buf = new Uint8Array(bstr.length);
+ for (let i = 0; i < bstr.length; i++) {
+ buf[i] = bstr.charCodeAt(i);
+ }
+ return buf;
+ }
+
+ throw new OpencodeError('Cannot decode base64 string; Expected `Buffer` or `atob` to be defined');
+};
diff --git a/packages/sdk/src/internal/utils/bytes.ts b/packages/sdk/src/internal/utils/bytes.ts
new file mode 100644
index 000000000..8da627abe
--- /dev/null
+++ b/packages/sdk/src/internal/utils/bytes.ts
@@ -0,0 +1,32 @@
+export function concatBytes(buffers: Uint8Array[]): Uint8Array {
+ let length = 0;
+ for (const buffer of buffers) {
+ length += buffer.length;
+ }
+ const output = new Uint8Array(length);
+ let index = 0;
+ for (const buffer of buffers) {
+ output.set(buffer, index);
+ index += buffer.length;
+ }
+
+ return output;
+}
+
+let encodeUTF8_: (str: string) => Uint8Array;
+export function encodeUTF8(str: string) {
+ let encoder;
+ return (
+ encodeUTF8_ ??
+ ((encoder = new (globalThis as any).TextEncoder()), (encodeUTF8_ = encoder.encode.bind(encoder)))
+ )(str);
+}
+
+let decodeUTF8_: (bytes: Uint8Array) => string;
+export function decodeUTF8(bytes: Uint8Array) {
+ let decoder;
+ return (
+ decodeUTF8_ ??
+ ((decoder = new (globalThis as any).TextDecoder()), (decodeUTF8_ = decoder.decode.bind(decoder)))
+ )(bytes);
+}
diff --git a/packages/sdk/src/internal/utils/env.ts b/packages/sdk/src/internal/utils/env.ts
new file mode 100644
index 000000000..2d8480077
--- /dev/null
+++ b/packages/sdk/src/internal/utils/env.ts
@@ -0,0 +1,18 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+/**
+ * Read an environment variable.
+ *
+ * Trims beginning and trailing whitespace.
+ *
+ * Will return undefined if the environment variable doesn't exist or cannot be accessed.
+ */
+export const readEnv = (env: string): string | undefined => {
+ if (typeof (globalThis as any).process !== 'undefined') {
+ return (globalThis as any).process.env?.[env]?.trim() ?? undefined;
+ }
+ if (typeof (globalThis as any).Deno !== 'undefined') {
+ return (globalThis as any).Deno.env?.get?.(env)?.trim();
+ }
+ return undefined;
+};
diff --git a/packages/sdk/src/internal/utils/log.ts b/packages/sdk/src/internal/utils/log.ts
new file mode 100644
index 000000000..44ac16a02
--- /dev/null
+++ b/packages/sdk/src/internal/utils/log.ts
@@ -0,0 +1,126 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { hasOwn } from './values';
+import { type Opencode } from '../../client';
+import { RequestOptions } from '../request-options';
+
+type LogFn = (message: string, ...rest: unknown[]) => void;
+export type Logger = {
+ error: LogFn;
+ warn: LogFn;
+ info: LogFn;
+ debug: LogFn;
+};
+export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug';
+
+const levelNumbers = {
+ off: 0,
+ error: 200,
+ warn: 300,
+ info: 400,
+ debug: 500,
+};
+
+export const parseLogLevel = (
+ maybeLevel: string | undefined,
+ sourceName: string,
+ client: Opencode,
+): LogLevel | undefined => {
+ if (!maybeLevel) {
+ return undefined;
+ }
+ if (hasOwn(levelNumbers, maybeLevel)) {
+ return maybeLevel;
+ }
+ loggerFor(client).warn(
+ `${sourceName} was set to ${JSON.stringify(maybeLevel)}, expected one of ${JSON.stringify(
+ Object.keys(levelNumbers),
+ )}`,
+ );
+ return undefined;
+};
+
+function noop() {}
+
+function makeLogFn(fnLevel: keyof Logger, logger: Logger | undefined, logLevel: LogLevel) {
+ if (!logger || levelNumbers[fnLevel] > levelNumbers[logLevel]) {
+ return noop;
+ } else {
+ // Don't wrap logger functions, we want the stacktrace intact!
+ return logger[fnLevel].bind(logger);
+ }
+}
+
+const noopLogger = {
+ error: noop,
+ warn: noop,
+ info: noop,
+ debug: noop,
+};
+
+let cachedLoggers = /* @__PURE__ */ new WeakMap<Logger, [LogLevel, Logger]>();
+
+export function loggerFor(client: Opencode): Logger {
+ const logger = client.logger;
+ const logLevel = client.logLevel ?? 'off';
+ if (!logger) {
+ return noopLogger;
+ }
+
+ const cachedLogger = cachedLoggers.get(logger);
+ if (cachedLogger && cachedLogger[0] === logLevel) {
+ return cachedLogger[1];
+ }
+
+ const levelLogger = {
+ error: makeLogFn('error', logger, logLevel),
+ warn: makeLogFn('warn', logger, logLevel),
+ info: makeLogFn('info', logger, logLevel),
+ debug: makeLogFn('debug', logger, logLevel),
+ };
+
+ cachedLoggers.set(logger, [logLevel, levelLogger]);
+
+ return levelLogger;
+}
+
+export const formatRequestDetails = (details: {
+ options?: RequestOptions | undefined;
+ headers?: Headers | Record<string, string> | undefined;
+ retryOfRequestLogID?: string | undefined;
+ retryOf?: string | undefined;
+ url?: string | undefined;
+ status?: number | undefined;
+ method?: string | undefined;
+ durationMs?: number | undefined;
+ message?: unknown;
+ body?: unknown;
+}) => {
+ if (details.options) {
+ details.options = { ...details.options };
+ delete details.options['headers']; // redundant + leaks internals
+ }
+ if (details.headers) {
+ details.headers = Object.fromEntries(
+ (details.headers instanceof Headers ? [...details.headers] : Object.entries(details.headers)).map(
+ ([name, value]) => [
+ name,
+ (
+ name.toLowerCase() === 'authorization' ||
+ name.toLowerCase() === 'cookie' ||
+ name.toLowerCase() === 'set-cookie'
+ ) ?
+ '***'
+ : value,
+ ],
+ ),
+ );
+ }
+ if ('retryOfRequestLogID' in details) {
+ if (details.retryOfRequestLogID) {
+ details.retryOf = details.retryOfRequestLogID;
+ }
+ delete details.retryOfRequestLogID;
+ }
+ return details;
+};
diff --git a/packages/sdk/src/internal/utils/path.ts b/packages/sdk/src/internal/utils/path.ts
new file mode 100644
index 000000000..6ca96e0dd
--- /dev/null
+++ b/packages/sdk/src/internal/utils/path.ts
@@ -0,0 +1,88 @@
+import { OpencodeError } from '../../core/error';
+
+/**
+ * Percent-encode everything that isn't safe to have in a path without encoding safe chars.
+ *
+ * Taken from https://datatracker.ietf.org/doc/html/rfc3986#section-3.3:
+ * > unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ * > sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+ * > pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+ */
+export function encodeURIPath(str: string) {
+ return str.replace(/[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/g, encodeURIComponent);
+}
+
+const EMPTY = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.create(null));
+
+export const createPathTagFunction = (pathEncoder = encodeURIPath) =>
+ function path(statics: readonly string[], ...params: readonly unknown[]): string {
+ // If there are no params, no processing is needed.
+ if (statics.length === 1) return statics[0]!;
+
+ let postPath = false;
+ const invalidSegments = [];
+ const path = statics.reduce((previousValue, currentValue, index) => {
+ if (/[?#]/.test(currentValue)) {
+ postPath = true;
+ }
+ const value = params[index];
+ let encoded = (postPath ? encodeURIComponent : pathEncoder)('' + value);
+ if (
+ index !== params.length &&
+ (value == null ||
+ (typeof value === 'object' &&
+ // handle values from other realms
+ value.toString ===
+ Object.getPrototypeOf(Object.getPrototypeOf((value as any).hasOwnProperty ?? EMPTY) ?? EMPTY)
+ ?.toString))
+ ) {
+ encoded = value + '';
+ invalidSegments.push({
+ start: previousValue.length + currentValue.length,
+ length: encoded.length,
+ error: `Value of type ${Object.prototype.toString
+ .call(value)
+ .slice(8, -1)} is not a valid path parameter`,
+ });
+ }
+ return previousValue + currentValue + (index === params.length ? '' : encoded);
+ }, '');
+
+ const pathOnly = path.split(/[?#]/, 1)[0]!;
+ const invalidSegmentPattern = /(?<=^|\/)(?:\.|%2e){1,2}(?=\/|$)/gi;
+ let match;
+
+ // Find all invalid segments
+ while ((match = invalidSegmentPattern.exec(pathOnly)) !== null) {
+ invalidSegments.push({
+ start: match.index,
+ length: match[0].length,
+ error: `Value "${match[0]}" can\'t be safely passed as a path parameter`,
+ });
+ }
+
+ invalidSegments.sort((a, b) => a.start - b.start);
+
+ if (invalidSegments.length > 0) {
+ let lastEnd = 0;
+ const underline = invalidSegments.reduce((acc, segment) => {
+ const spaces = ' '.repeat(segment.start - lastEnd);
+ const arrows = '^'.repeat(segment.length);
+ lastEnd = segment.start + segment.length;
+ return acc + spaces + arrows;
+ }, '');
+
+ throw new OpencodeError(
+ `Path parameters result in path with invalid segments:\n${invalidSegments
+ .map((e) => e.error)
+ .join('\n')}\n${path}\n${underline}`,
+ );
+ }
+
+ return path;
+ };
+
+/**
+ * URI-encodes path params and ensures no unsafe /./ or /../ path segments are introduced.
+ */
+export const path = /* @__PURE__ */ createPathTagFunction(encodeURIPath);
diff --git a/packages/sdk/src/internal/utils/sleep.ts b/packages/sdk/src/internal/utils/sleep.ts
new file mode 100644
index 000000000..65e52962b
--- /dev/null
+++ b/packages/sdk/src/internal/utils/sleep.ts
@@ -0,0 +1,3 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
diff --git a/packages/sdk/src/internal/utils/uuid.ts b/packages/sdk/src/internal/utils/uuid.ts
new file mode 100644
index 000000000..b0e53aaf7
--- /dev/null
+++ b/packages/sdk/src/internal/utils/uuid.ts
@@ -0,0 +1,17 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+/**
+ * https://stackoverflow.com/a/2117523
+ */
+export let uuid4 = function () {
+ const { crypto } = globalThis as any;
+ if (crypto?.randomUUID) {
+ uuid4 = crypto.randomUUID.bind(crypto);
+ return crypto.randomUUID();
+ }
+ const u8 = new Uint8Array(1);
+ const randomByte = crypto ? () => crypto.getRandomValues(u8)[0]! : () => (Math.random() * 0xff) & 0xff;
+ return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>
+ (+c ^ (randomByte() & (15 >> (+c / 4)))).toString(16),
+ );
+};
diff --git a/packages/sdk/src/internal/utils/values.ts b/packages/sdk/src/internal/utils/values.ts
new file mode 100644
index 000000000..b2421fd88
--- /dev/null
+++ b/packages/sdk/src/internal/utils/values.ts
@@ -0,0 +1,105 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { OpencodeError } from '../../core/error';
+
+// https://url.spec.whatwg.org/#url-scheme-string
+const startsWithSchemeRegexp = /^[a-z][a-z0-9+.-]*:/i;
+
+export const isAbsoluteURL = (url: string): boolean => {
+ return startsWithSchemeRegexp.test(url);
+};
+
+export let isArray = (val: unknown): val is unknown[] => ((isArray = Array.isArray), isArray(val));
+export let isReadonlyArray = isArray as (val: unknown) => val is readonly unknown[];
+
+/** Returns an object if the given value isn't an object, otherwise returns as-is */
+export function maybeObj(x: unknown): object {
+ if (typeof x !== 'object') {
+ return {};
+ }
+
+ return x ?? {};
+}
+
+// https://stackoverflow.com/a/34491287
+export function isEmptyObj(obj: Object | null | undefined): boolean {
+ if (!obj) return true;
+ for (const _k in obj) return false;
+ return true;
+}
+
+// https://eslint.org/docs/latest/rules/no-prototype-builtins
+export function hasOwn<T extends object = object>(obj: T, key: PropertyKey): key is keyof T {
+ return Object.prototype.hasOwnProperty.call(obj, key);
+}
+
+export function isObj(obj: unknown): obj is Record<string, unknown> {
+ return obj != null && typeof obj === 'object' && !Array.isArray(obj);
+}
+
+export const ensurePresent = <T>(value: T | null | undefined): T => {
+ if (value == null) {
+ throw new OpencodeError(`Expected a value to be given but received ${value} instead.`);
+ }
+
+ return value;
+};
+
+export const validatePositiveInteger = (name: string, n: unknown): number => {
+ if (typeof n !== 'number' || !Number.isInteger(n)) {
+ throw new OpencodeError(`${name} must be an integer`);
+ }
+ if (n < 0) {
+ throw new OpencodeError(`${name} must be a positive integer`);
+ }
+ return n;
+};
+
+export const coerceInteger = (value: unknown): number => {
+ if (typeof value === 'number') return Math.round(value);
+ if (typeof value === 'string') return parseInt(value, 10);
+
+ throw new OpencodeError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
+};
+
+export const coerceFloat = (value: unknown): number => {
+ if (typeof value === 'number') return value;
+ if (typeof value === 'string') return parseFloat(value);
+
+ throw new OpencodeError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
+};
+
+export const coerceBoolean = (value: unknown): boolean => {
+ if (typeof value === 'boolean') return value;
+ if (typeof value === 'string') return value === 'true';
+ return Boolean(value);
+};
+
+export const maybeCoerceInteger = (value: unknown): number | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ return coerceInteger(value);
+};
+
+export const maybeCoerceFloat = (value: unknown): number | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ return coerceFloat(value);
+};
+
+export const maybeCoerceBoolean = (value: unknown): boolean | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+ return coerceBoolean(value);
+};
+
+export const safeJSON = (text: string) => {
+ try {
+ return JSON.parse(text);
+ } catch (err) {
+ return undefined;
+ }
+};
diff --git a/packages/sdk/src/lib/.keep b/packages/sdk/src/lib/.keep
new file mode 100644
index 000000000..7554f8b20
--- /dev/null
+++ b/packages/sdk/src/lib/.keep
@@ -0,0 +1,4 @@
+File generated from our OpenAPI spec by Stainless.
+
+This directory can be used to store custom files to expand the SDK.
+It is ignored by Stainless code generation and its content (other than this keep file) won't be touched.
diff --git a/packages/sdk/src/resource.ts b/packages/sdk/src/resource.ts
new file mode 100644
index 000000000..363e3516b
--- /dev/null
+++ b/packages/sdk/src/resource.ts
@@ -0,0 +1,2 @@
+/** @deprecated Import from ./core/resource instead */
+export * from './core/resource';
diff --git a/packages/sdk/src/resources.ts b/packages/sdk/src/resources.ts
new file mode 100644
index 000000000..b283d5781
--- /dev/null
+++ b/packages/sdk/src/resources.ts
@@ -0,0 +1 @@
+export * from './resources/index';
diff --git a/packages/sdk/src/resources/app.ts b/packages/sdk/src/resources/app.ts
new file mode 100644
index 000000000..69991a3fe
--- /dev/null
+++ b/packages/sdk/src/resources/app.ts
@@ -0,0 +1,190 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../core/resource';
+import { APIPromise } from '../core/api-promise';
+import { RequestOptions } from '../internal/request-options';
+
+export class AppResource extends APIResource {
+ /**
+ * Get app info
+ */
+ get(options?: RequestOptions): APIPromise<App> {
+ return this._client.get('/app', options);
+ }
+
+ /**
+ * Initialize the app
+ */
+ init(options?: RequestOptions): APIPromise<AppInitResponse> {
+ return this._client.post('/app/init', options);
+ }
+
+ /**
+ * Write a log entry to the server logs
+ */
+ log(body: AppLogParams, options?: RequestOptions): APIPromise<AppLogResponse> {
+ return this._client.post('/log', { body, ...options });
+ }
+
+ /**
+ * List all modes
+ */
+ modes(options?: RequestOptions): APIPromise<AppModesResponse> {
+ return this._client.get('/mode', options);
+ }
+
+ /**
+ * List all providers
+ */
+ providers(options?: RequestOptions): APIPromise<AppProvidersResponse> {
+ return this._client.get('/config/providers', options);
+ }
+}
+
+export interface App {
+ git: boolean;
+
+ hostname: string;
+
+ path: App.Path;
+
+ time: App.Time;
+}
+
+export namespace App {
+ export interface Path {
+ config: string;
+
+ cwd: string;
+
+ data: string;
+
+ root: string;
+
+ state: string;
+ }
+
+ export interface Time {
+ initialized?: number;
+ }
+}
+
+export interface Mode {
+ name: string;
+
+ tools: { [key: string]: boolean };
+
+ model?: Mode.Model;
+
+ prompt?: string;
+}
+
+export namespace Mode {
+ export interface Model {
+ modelID: string;
+
+ providerID: string;
+ }
+}
+
+export interface Model {
+ id: string;
+
+ attachment: boolean;
+
+ cost: Model.Cost;
+
+ limit: Model.Limit;
+
+ name: string;
+
+ options: { [key: string]: unknown };
+
+ reasoning: boolean;
+
+ release_date: string;
+
+ temperature: boolean;
+
+ tool_call: boolean;
+}
+
+export namespace Model {
+ export interface Cost {
+ input: number;
+
+ output: number;
+
+ cache_read?: number;
+
+ cache_write?: number;
+ }
+
+ export interface Limit {
+ context: number;
+
+ output: number;
+ }
+}
+
+export interface Provider {
+ id: string;
+
+ env: Array<string>;
+
+ models: { [key: string]: Model };
+
+ name: string;
+
+ api?: string;
+
+ npm?: string;
+}
+
+export type AppInitResponse = boolean;
+
+export type AppLogResponse = boolean;
+
+export type AppModesResponse = Array<Mode>;
+
+export interface AppProvidersResponse {
+ default: { [key: string]: string };
+
+ providers: Array<Provider>;
+}
+
+export interface AppLogParams {
+ /**
+ * Log level
+ */
+ level: 'debug' | 'info' | 'error' | 'warn';
+
+ /**
+ * Log message
+ */
+ message: string;
+
+ /**
+ * Service name for the log entry
+ */
+ service: string;
+
+ /**
+ * Additional metadata for the log entry
+ */
+ extra?: { [key: string]: unknown };
+}
+
+export declare namespace AppResource {
+ export {
+ type App as App,
+ type Mode as Mode,
+ type Model as Model,
+ type Provider as Provider,
+ type AppInitResponse as AppInitResponse,
+ type AppLogResponse as AppLogResponse,
+ type AppModesResponse as AppModesResponse,
+ type AppProvidersResponse as AppProvidersResponse,
+ type AppLogParams as AppLogParams,
+ };
+}
diff --git a/packages/sdk/src/resources/config.ts b/packages/sdk/src/resources/config.ts
new file mode 100644
index 000000000..e1fc09de5
--- /dev/null
+++ b/packages/sdk/src/resources/config.ts
@@ -0,0 +1,438 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../core/resource';
+import * as ConfigAPI from './config';
+import { APIPromise } from '../core/api-promise';
+import { RequestOptions } from '../internal/request-options';
+
+export class ConfigResource extends APIResource {
+ /**
+ * Get config info
+ */
+ get(options?: RequestOptions): APIPromise<Config> {
+ return this._client.get('/config', options);
+ }
+}
+
+export interface Config {
+ /**
+ * JSON schema reference for configuration validation
+ */
+ $schema?: string;
+
+ /**
+ * @deprecated Use 'share' field instead. Share newly created sessions
+ * automatically
+ */
+ autoshare?: boolean;
+
+ /**
+ * Automatically update to the latest version
+ */
+ autoupdate?: boolean;
+
+ /**
+ * Disable providers that are loaded automatically
+ */
+ disabled_providers?: Array<string>;
+
+ experimental?: Config.Experimental;
+
+ /**
+ * Additional instruction files or patterns to include
+ */
+ instructions?: Array<string>;
+
+ /**
+ * Custom keybind configurations
+ */
+ keybinds?: KeybindsConfig;
+
+ /**
+ * @deprecated Always uses stretch layout.
+ */
+ layout?: 'auto' | 'stretch';
+
+ /**
+ * MCP (Model Context Protocol) server configurations
+ */
+ mcp?: { [key: string]: McpLocalConfig | McpRemoteConfig };
+
+ /**
+ * Modes configuration, see https://opencode.ai/docs/modes
+ */
+ mode?: Config.Mode;
+
+ /**
+ * Model to use in the format of provider/model, eg anthropic/claude-2
+ */
+ model?: string;
+
+ /**
+ * Custom provider configurations and model overrides
+ */
+ provider?: { [key: string]: Config.Provider };
+
+ /**
+ * Control sharing behavior:'manual' allows manual sharing via commands, 'auto'
+ * enables automatic sharing, 'disabled' disables all sharing
+ */
+ share?: 'manual' | 'auto' | 'disabled';
+
+ /**
+ * Small model to use for tasks like summarization and title generation in the
+ * format of provider/model
+ */
+ small_model?: string;
+
+ /**
+ * Theme name to use for the interface
+ */
+ theme?: string;
+
+ /**
+ * Custom username to display in conversations instead of system username
+ */
+ username?: string;
+}
+
+export namespace Config {
+ export interface Experimental {
+ hook?: Experimental.Hook;
+ }
+
+ export namespace Experimental {
+ export interface Hook {
+ file_edited?: { [key: string]: Array<Hook.FileEdited> };
+
+ session_completed?: Array<Hook.SessionCompleted>;
+ }
+
+ export namespace Hook {
+ export interface FileEdited {
+ command: Array<string>;
+
+ environment?: { [key: string]: string };
+ }
+
+ export interface SessionCompleted {
+ command: Array<string>;
+
+ environment?: { [key: string]: string };
+ }
+ }
+ }
+
+ /**
+ * Modes configuration, see https://opencode.ai/docs/modes
+ */
+ export interface Mode {
+ build?: ConfigAPI.ModeConfig;
+
+ plan?: ConfigAPI.ModeConfig;
+
+ [k: string]: ConfigAPI.ModeConfig | undefined;
+ }
+
+ export interface Provider {
+ models: { [key: string]: Provider.Models };
+
+ id?: string;
+
+ api?: string;
+
+ env?: Array<string>;
+
+ name?: string;
+
+ npm?: string;
+
+ options?: { [key: string]: unknown };
+ }
+
+ export namespace Provider {
+ export interface Models {
+ id?: string;
+
+ attachment?: boolean;
+
+ cost?: Models.Cost;
+
+ limit?: Models.Limit;
+
+ name?: string;
+
+ options?: { [key: string]: unknown };
+
+ reasoning?: boolean;
+
+ release_date?: string;
+
+ temperature?: boolean;
+
+ tool_call?: boolean;
+ }
+
+ export namespace Models {
+ export interface Cost {
+ input: number;
+
+ output: number;
+
+ cache_read?: number;
+
+ cache_write?: number;
+ }
+
+ export interface Limit {
+ context: number;
+
+ output: number;
+ }
+ }
+ }
+}
+
+export interface KeybindsConfig {
+ /**
+ * Exit the application
+ */
+ app_exit: string;
+
+ /**
+ * Show help dialog
+ */
+ app_help: string;
+
+ /**
+ * Open external editor
+ */
+ editor_open: string;
+
+ /**
+ * Close file
+ */
+ file_close: string;
+
+ /**
+ * Split/unified diff
+ */
+ file_diff_toggle: string;
+
+ /**
+ * List files
+ */
+ file_list: string;
+
+ /**
+ * Search file
+ */
+ file_search: string;
+
+ /**
+ * Clear input field
+ */
+ input_clear: string;
+
+ /**
+ * Insert newline in input
+ */
+ input_newline: string;
+
+ /**
+ * Paste from clipboard
+ */
+ input_paste: string;
+
+ /**
+ * Submit input
+ */
+ input_submit: string;
+
+ /**
+ * Leader key for keybind combinations
+ */
+ leader: string;
+
+ /**
+ * Copy message
+ */
+ messages_copy: string;
+
+ /**
+ * Navigate to first message
+ */
+ messages_first: string;
+
+ /**
+ * Scroll messages down by half page
+ */
+ messages_half_page_down: string;
+
+ /**
+ * Scroll messages up by half page
+ */
+ messages_half_page_up: string;
+
+ /**
+ * Navigate to last message
+ */
+ messages_last: string;
+
+ /**
+ * Toggle layout
+ */
+ messages_layout_toggle: string;
+
+ /**
+ * Navigate to next message
+ */
+ messages_next: string;
+
+ /**
+ * Scroll messages down by one page
+ */
+ messages_page_down: string;
+
+ /**
+ * Scroll messages up by one page
+ */
+ messages_page_up: string;
+
+ /**
+ * Navigate to previous message
+ */
+ messages_previous: string;
+
+ /**
+ * Revert message
+ */
+ messages_revert: string;
+
+ /**
+ * List available models
+ */
+ model_list: string;
+
+ /**
+ * Create/update AGENTS.md
+ */
+ project_init: string;
+
+ /**
+ * Compact the session
+ */
+ session_compact: string;
+
+ /**
+ * Export session to editor
+ */
+ session_export: string;
+
+ /**
+ * Interrupt current session
+ */
+ session_interrupt: string;
+
+ /**
+ * List all sessions
+ */
+ session_list: string;
+
+ /**
+ * Create a new session
+ */
+ session_new: string;
+
+ /**
+ * Share current session
+ */
+ session_share: string;
+
+ /**
+ * Unshare current session
+ */
+ session_unshare: string;
+
+ /**
+ * Next mode
+ */
+ switch_mode: string;
+
+ /**
+ * Previous Mode
+ */
+ switch_mode_reverse: string;
+
+ /**
+ * List available themes
+ */
+ theme_list: string;
+
+ /**
+ * Toggle tool details
+ */
+ tool_details: string;
+}
+
+export interface McpLocalConfig {
+ /**
+ * Command and arguments to run the MCP server
+ */
+ command: Array<string>;
+
+ /**
+ * Type of MCP server connection
+ */
+ type: 'local';
+
+ /**
+ * Enable or disable the MCP server on startup
+ */
+ enabled?: boolean;
+
+ /**
+ * Environment variables to set when running the MCP server
+ */
+ environment?: { [key: string]: string };
+}
+
+export interface McpRemoteConfig {
+ /**
+ * Type of MCP server connection
+ */
+ type: 'remote';
+
+ /**
+ * URL of the remote MCP server
+ */
+ url: string;
+
+ /**
+ * Enable or disable the MCP server on startup
+ */
+ enabled?: boolean;
+
+ /**
+ * Headers to send with the request
+ */
+ headers?: { [key: string]: string };
+}
+
+export interface ModeConfig {
+ model?: string;
+
+ prompt?: string;
+
+ tools?: { [key: string]: boolean };
+}
+
+export declare namespace ConfigResource {
+ export {
+ type Config as Config,
+ type KeybindsConfig as KeybindsConfig,
+ type McpLocalConfig as McpLocalConfig,
+ type McpRemoteConfig as McpRemoteConfig,
+ type ModeConfig as ModeConfig,
+ };
+}
diff --git a/packages/sdk/src/resources/event.ts b/packages/sdk/src/resources/event.ts
new file mode 100644
index 000000000..a3dd9288c
--- /dev/null
+++ b/packages/sdk/src/resources/event.ts
@@ -0,0 +1,243 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../core/resource';
+import * as SessionAPI from './session';
+import * as Shared from './shared';
+import { APIPromise } from '../core/api-promise';
+import { Stream } from '../core/streaming';
+import { RequestOptions } from '../internal/request-options';
+
+export class Event extends APIResource {
+ /**
+ * Get events
+ */
+ list(options?: RequestOptions): APIPromise<Stream<EventListResponse>> {
+ return this._client.get('/event', { ...options, stream: true }) as APIPromise<Stream<EventListResponse>>;
+ }
+}
+
+export type EventListResponse =
+ | EventListResponse.EventLspClientDiagnostics
+ | EventListResponse.EventPermissionUpdated
+ | EventListResponse.EventFileEdited
+ | EventListResponse.EventInstallationUpdated
+ | EventListResponse.EventMessageUpdated
+ | EventListResponse.EventMessageRemoved
+ | EventListResponse.EventMessagePartUpdated
+ | EventListResponse.EventStorageWrite
+ | EventListResponse.EventSessionUpdated
+ | EventListResponse.EventSessionDeleted
+ | EventListResponse.EventSessionIdle
+ | EventListResponse.EventSessionError
+ | EventListResponse.EventFileWatcherUpdated
+ | EventListResponse.EventIdeInstalled;
+
+export namespace EventListResponse {
+ export interface EventLspClientDiagnostics {
+ properties: EventLspClientDiagnostics.Properties;
+
+ type: 'lsp.client.diagnostics';
+ }
+
+ export namespace EventLspClientDiagnostics {
+ export interface Properties {
+ path: string;
+
+ serverID: string;
+ }
+ }
+
+ export interface EventPermissionUpdated {
+ properties: EventPermissionUpdated.Properties;
+
+ type: 'permission.updated';
+ }
+
+ export namespace EventPermissionUpdated {
+ export interface Properties {
+ id: string;
+
+ metadata: { [key: string]: unknown };
+
+ sessionID: string;
+
+ time: Properties.Time;
+
+ title: string;
+ }
+
+ export namespace Properties {
+ export interface Time {
+ created: number;
+ }
+ }
+ }
+
+ export interface EventFileEdited {
+ properties: EventFileEdited.Properties;
+
+ type: 'file.edited';
+ }
+
+ export namespace EventFileEdited {
+ export interface Properties {
+ file: string;
+ }
+ }
+
+ export interface EventInstallationUpdated {
+ properties: EventInstallationUpdated.Properties;
+
+ type: 'installation.updated';
+ }
+
+ export namespace EventInstallationUpdated {
+ export interface Properties {
+ version: string;
+ }
+ }
+
+ export interface EventMessageUpdated {
+ properties: EventMessageUpdated.Properties;
+
+ type: 'message.updated';
+ }
+
+ export namespace EventMessageUpdated {
+ export interface Properties {
+ info: SessionAPI.Message;
+ }
+ }
+
+ export interface EventMessageRemoved {
+ properties: EventMessageRemoved.Properties;
+
+ type: 'message.removed';
+ }
+
+ export namespace EventMessageRemoved {
+ export interface Properties {
+ messageID: string;
+
+ sessionID: string;
+ }
+ }
+
+ export interface EventMessagePartUpdated {
+ properties: EventMessagePartUpdated.Properties;
+
+ type: 'message.part.updated';
+ }
+
+ export namespace EventMessagePartUpdated {
+ export interface Properties {
+ part: SessionAPI.Part;
+ }
+ }
+
+ export interface EventStorageWrite {
+ properties: EventStorageWrite.Properties;
+
+ type: 'storage.write';
+ }
+
+ export namespace EventStorageWrite {
+ export interface Properties {
+ key: string;
+
+ content?: unknown;
+ }
+ }
+
+ export interface EventSessionUpdated {
+ properties: EventSessionUpdated.Properties;
+
+ type: 'session.updated';
+ }
+
+ export namespace EventSessionUpdated {
+ export interface Properties {
+ info: SessionAPI.Session;
+ }
+ }
+
+ export interface EventSessionDeleted {
+ properties: EventSessionDeleted.Properties;
+
+ type: 'session.deleted';
+ }
+
+ export namespace EventSessionDeleted {
+ export interface Properties {
+ info: SessionAPI.Session;
+ }
+ }
+
+ export interface EventSessionIdle {
+ properties: EventSessionIdle.Properties;
+
+ type: 'session.idle';
+ }
+
+ export namespace EventSessionIdle {
+ export interface Properties {
+ sessionID: string;
+ }
+ }
+
+ export interface EventSessionError {
+ properties: EventSessionError.Properties;
+
+ type: 'session.error';
+ }
+
+ export namespace EventSessionError {
+ export interface Properties {
+ error?:
+ | Shared.ProviderAuthError
+ | Shared.UnknownError
+ | Properties.MessageOutputLengthError
+ | Shared.MessageAbortedError;
+
+ sessionID?: string;
+ }
+
+ export namespace Properties {
+ export interface MessageOutputLengthError {
+ data: unknown;
+
+ name: 'MessageOutputLengthError';
+ }
+ }
+ }
+
+ export interface EventFileWatcherUpdated {
+ properties: EventFileWatcherUpdated.Properties;
+
+ type: 'file.watcher.updated';
+ }
+
+ export namespace EventFileWatcherUpdated {
+ export interface Properties {
+ event: 'rename' | 'change';
+
+ file: string;
+ }
+ }
+
+ export interface EventIdeInstalled {
+ properties: EventIdeInstalled.Properties;
+
+ type: 'ide.installed';
+ }
+
+ export namespace EventIdeInstalled {
+ export interface Properties {
+ ide: string;
+ }
+ }
+}
+
+export declare namespace Event {
+ export { type EventListResponse as EventListResponse };
+}
diff --git a/packages/sdk/src/resources/file.ts b/packages/sdk/src/resources/file.ts
new file mode 100644
index 000000000..fd7bdac69
--- /dev/null
+++ b/packages/sdk/src/resources/file.ts
@@ -0,0 +1,52 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../core/resource';
+import { APIPromise } from '../core/api-promise';
+import { RequestOptions } from '../internal/request-options';
+
+export class FileResource extends APIResource {
+ /**
+ * Read a file
+ */
+ read(query: FileReadParams, options?: RequestOptions): APIPromise<FileReadResponse> {
+ return this._client.get('/file', { query, ...options });
+ }
+
+ /**
+ * Get file status
+ */
+ status(options?: RequestOptions): APIPromise<FileStatusResponse> {
+ return this._client.get('/file/status', options);
+ }
+}
+
+export interface File {
+ added: number;
+
+ path: string;
+
+ removed: number;
+
+ status: 'added' | 'deleted' | 'modified';
+}
+
+export interface FileReadResponse {
+ content: string;
+
+ type: 'raw' | 'patch';
+}
+
+export type FileStatusResponse = Array<File>;
+
+export interface FileReadParams {
+ path: string;
+}
+
+export declare namespace FileResource {
+ export {
+ type File as File,
+ type FileReadResponse as FileReadResponse,
+ type FileStatusResponse as FileStatusResponse,
+ type FileReadParams as FileReadParams,
+ };
+}
diff --git a/packages/sdk/src/resources/find.ts b/packages/sdk/src/resources/find.ts
new file mode 100644
index 000000000..13062c426
--- /dev/null
+++ b/packages/sdk/src/resources/find.ts
@@ -0,0 +1,133 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../core/resource';
+import { APIPromise } from '../core/api-promise';
+import { RequestOptions } from '../internal/request-options';
+
+export class Find extends APIResource {
+ /**
+ * Find files
+ */
+ files(query: FindFilesParams, options?: RequestOptions): APIPromise<FindFilesResponse> {
+ return this._client.get('/find/file', { query, ...options });
+ }
+
+ /**
+ * Find workspace symbols
+ */
+ symbols(query: FindSymbolsParams, options?: RequestOptions): APIPromise<FindSymbolsResponse> {
+ return this._client.get('/find/symbol', { query, ...options });
+ }
+
+ /**
+ * Find text in files
+ */
+ text(query: FindTextParams, options?: RequestOptions): APIPromise<FindTextResponse> {
+ return this._client.get('/find', { query, ...options });
+ }
+}
+
+export interface Match {
+ absolute_offset: number;
+
+ line_number: number;
+
+ lines: Match.Lines;
+
+ path: Match.Path;
+
+ submatches: Array<Match.Submatch>;
+}
+
+export namespace Match {
+ export interface Lines {
+ text: string;
+ }
+
+ export interface Path {
+ text: string;
+ }
+
+ export interface Submatch {
+ end: number;
+
+ match: Submatch.Match;
+
+ start: number;
+ }
+
+ export namespace Submatch {
+ export interface Match {
+ text: string;
+ }
+ }
+}
+
+export interface Symbol {
+ kind: number;
+
+ location: Symbol.Location;
+
+ name: string;
+}
+
+export namespace Symbol {
+ export interface Location {
+ range: Location.Range;
+
+ uri: string;
+ }
+
+ export namespace Location {
+ export interface Range {
+ end: Range.End;
+
+ start: Range.Start;
+ }
+
+ export namespace Range {
+ export interface End {
+ character: number;
+
+ line: number;
+ }
+
+ export interface Start {
+ character: number;
+
+ line: number;
+ }
+ }
+ }
+}
+
+export type FindFilesResponse = Array<string>;
+
+export type FindSymbolsResponse = Array<Symbol>;
+
+export type FindTextResponse = Array<Match>;
+
+export interface FindFilesParams {
+ query: string;
+}
+
+export interface FindSymbolsParams {
+ query: string;
+}
+
+export interface FindTextParams {
+ pattern: string;
+}
+
+export declare namespace Find {
+ export {
+ type Match as Match,
+ type Symbol as Symbol,
+ type FindFilesResponse as FindFilesResponse,
+ type FindSymbolsResponse as FindSymbolsResponse,
+ type FindTextResponse as FindTextResponse,
+ type FindFilesParams as FindFilesParams,
+ type FindSymbolsParams as FindSymbolsParams,
+ type FindTextParams as FindTextParams,
+ };
+}
diff --git a/packages/sdk/src/resources/index.ts b/packages/sdk/src/resources/index.ts
new file mode 100644
index 000000000..50df8ff46
--- /dev/null
+++ b/packages/sdk/src/resources/index.ts
@@ -0,0 +1,81 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export * from './shared';
+export {
+ AppResource,
+ type App,
+ type Mode,
+ type Model,
+ type Provider,
+ type AppInitResponse,
+ type AppLogResponse,
+ type AppModesResponse,
+ type AppProvidersResponse,
+ type AppLogParams,
+} from './app';
+export {
+ ConfigResource,
+ type Config,
+ type KeybindsConfig,
+ type McpLocalConfig,
+ type McpRemoteConfig,
+ type ModeConfig,
+} from './config';
+export { Event, type EventListResponse } from './event';
+export {
+ FileResource,
+ type File,
+ type FileReadResponse,
+ type FileStatusResponse,
+ type FileReadParams,
+} from './file';
+export {
+ Find,
+ type Match,
+ type Symbol,
+ type FindFilesResponse,
+ type FindSymbolsResponse,
+ type FindTextResponse,
+ type FindFilesParams,
+ type FindSymbolsParams,
+ type FindTextParams,
+} from './find';
+export {
+ SessionResource,
+ type AssistantMessage,
+ type FilePart,
+ type FilePartInput,
+ type FilePartSource,
+ type FilePartSourceText,
+ type FileSource,
+ type Message,
+ type Part,
+ type Session,
+ type SnapshotPart,
+ type StepFinishPart,
+ type StepStartPart,
+ type SymbolSource,
+ type TextPart,
+ type TextPartInput,
+ type ToolPart,
+ type ToolStateCompleted,
+ type ToolStateError,
+ type ToolStatePending,
+ type ToolStateRunning,
+ type UserMessage,
+ type SessionListResponse,
+ type SessionDeleteResponse,
+ type SessionAbortResponse,
+ type SessionInitResponse,
+ type SessionMessagesResponse,
+ type SessionSummarizeResponse,
+ type SessionChatParams,
+ type SessionInitParams,
+ type SessionSummarizeParams,
+} from './session';
+export {
+ Tui,
+ type TuiAppendPromptResponse,
+ type TuiOpenHelpResponse,
+ type TuiAppendPromptParams,
+} from './tui';
diff --git a/packages/sdk/src/resources/session.ts b/packages/sdk/src/resources/session.ts
new file mode 100644
index 000000000..6e160321d
--- /dev/null
+++ b/packages/sdk/src/resources/session.ts
@@ -0,0 +1,555 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../core/resource';
+import * as SessionAPI from './session';
+import * as Shared from './shared';
+import { APIPromise } from '../core/api-promise';
+import { RequestOptions } from '../internal/request-options';
+import { path } from '../internal/utils/path';
+
+export class SessionResource extends APIResource {
+ /**
+ * Create a new session
+ */
+ create(options?: RequestOptions): APIPromise<Session> {
+ return this._client.post('/session', options);
+ }
+
+ /**
+ * List all sessions
+ */
+ list(options?: RequestOptions): APIPromise<SessionListResponse> {
+ return this._client.get('/session', options);
+ }
+
+ /**
+ * Delete a session and all its data
+ */
+ delete(id: string, options?: RequestOptions): APIPromise<SessionDeleteResponse> {
+ return this._client.delete(path`/session/${id}`, options);
+ }
+
+ /**
+ * Abort a session
+ */
+ abort(id: string, options?: RequestOptions): APIPromise<SessionAbortResponse> {
+ return this._client.post(path`/session/${id}/abort`, options);
+ }
+
+ /**
+ * Create and send a new message to a session
+ */
+ chat(id: string, body: SessionChatParams, options?: RequestOptions): APIPromise<AssistantMessage> {
+ return this._client.post(path`/session/${id}/message`, { body, ...options });
+ }
+
+ /**
+ * Analyze the app and create an AGENTS.md file
+ */
+ init(id: string, body: SessionInitParams, options?: RequestOptions): APIPromise<SessionInitResponse> {
+ return this._client.post(path`/session/${id}/init`, { body, ...options });
+ }
+
+ /**
+ * List messages for a session
+ */
+ messages(id: string, options?: RequestOptions): APIPromise<SessionMessagesResponse> {
+ return this._client.get(path`/session/${id}/message`, options);
+ }
+
+ /**
+ * Share a session
+ */
+ share(id: string, options?: RequestOptions): APIPromise<Session> {
+ return this._client.post(path`/session/${id}/share`, options);
+ }
+
+ /**
+ * Summarize the session
+ */
+ summarize(
+ id: string,
+ body: SessionSummarizeParams,
+ options?: RequestOptions,
+ ): APIPromise<SessionSummarizeResponse> {
+ return this._client.post(path`/session/${id}/summarize`, { body, ...options });
+ }
+
+ /**
+ * Unshare the session
+ */
+ unshare(id: string, options?: RequestOptions): APIPromise<Session> {
+ return this._client.delete(path`/session/${id}/share`, options);
+ }
+}
+
+export interface AssistantMessage {
+ id: string;
+
+ cost: number;
+
+ modelID: string;
+
+ path: AssistantMessage.Path;
+
+ providerID: string;
+
+ role: 'assistant';
+
+ sessionID: string;
+
+ system: Array<string>;
+
+ time: AssistantMessage.Time;
+
+ tokens: AssistantMessage.Tokens;
+
+ error?:
+ | Shared.ProviderAuthError
+ | Shared.UnknownError
+ | AssistantMessage.MessageOutputLengthError
+ | Shared.MessageAbortedError;
+
+ summary?: boolean;
+}
+
+export namespace AssistantMessage {
+ export interface Path {
+ cwd: string;
+
+ root: string;
+ }
+
+ export interface Time {
+ created: number;
+
+ completed?: number;
+ }
+
+ export interface Tokens {
+ cache: Tokens.Cache;
+
+ input: number;
+
+ output: number;
+
+ reasoning: number;
+ }
+
+ export namespace Tokens {
+ export interface Cache {
+ read: number;
+
+ write: number;
+ }
+ }
+
+ export interface MessageOutputLengthError {
+ data: unknown;
+
+ name: 'MessageOutputLengthError';
+ }
+}
+
+export interface FilePart {
+ id: string;
+
+ messageID: string;
+
+ mime: string;
+
+ sessionID: string;
+
+ type: 'file';
+
+ url: string;
+
+ filename?: string;
+
+ source?: FilePartSource;
+}
+
+export interface FilePartInput {
+ mime: string;
+
+ type: 'file';
+
+ url: string;
+
+ id?: string;
+
+ filename?: string;
+
+ source?: FilePartSource;
+}
+
+export type FilePartSource = FileSource | SymbolSource;
+
+export interface FilePartSourceText {
+ end: number;
+
+ start: number;
+
+ value: string;
+}
+
+export interface FileSource {
+ path: string;
+
+ text: FilePartSourceText;
+
+ type: 'file';
+}
+
+export type Message = UserMessage | AssistantMessage;
+
+export type Part = TextPart | FilePart | ToolPart | StepStartPart | StepFinishPart | SnapshotPart;
+
+export interface Session {
+ id: string;
+
+ time: Session.Time;
+
+ title: string;
+
+ version: string;
+
+ parentID?: string;
+
+ revert?: Session.Revert;
+
+ share?: Session.Share;
+}
+
+export namespace Session {
+ export interface Time {
+ created: number;
+
+ updated: number;
+ }
+
+ export interface Revert {
+ messageID: string;
+
+ part: number;
+
+ snapshot?: string;
+ }
+
+ export interface Share {
+ url: string;
+ }
+}
+
+export interface SnapshotPart {
+ id: string;
+
+ messageID: string;
+
+ sessionID: string;
+
+ snapshot: string;
+
+ type: 'snapshot';
+}
+
+export interface StepFinishPart {
+ id: string;
+
+ cost: number;
+
+ messageID: string;
+
+ sessionID: string;
+
+ tokens: StepFinishPart.Tokens;
+
+ type: 'step-finish';
+}
+
+export namespace StepFinishPart {
+ export interface Tokens {
+ cache: Tokens.Cache;
+
+ input: number;
+
+ output: number;
+
+ reasoning: number;
+ }
+
+ export namespace Tokens {
+ export interface Cache {
+ read: number;
+
+ write: number;
+ }
+ }
+}
+
+export interface StepStartPart {
+ id: string;
+
+ messageID: string;
+
+ sessionID: string;
+
+ type: 'step-start';
+}
+
+export interface SymbolSource {
+ kind: number;
+
+ name: string;
+
+ path: string;
+
+ range: SymbolSource.Range;
+
+ text: FilePartSourceText;
+
+ type: 'symbol';
+}
+
+export namespace SymbolSource {
+ export interface Range {
+ end: Range.End;
+
+ start: Range.Start;
+ }
+
+ export namespace Range {
+ export interface End {
+ character: number;
+
+ line: number;
+ }
+
+ export interface Start {
+ character: number;
+
+ line: number;
+ }
+ }
+}
+
+export interface TextPart {
+ id: string;
+
+ messageID: string;
+
+ sessionID: string;
+
+ text: string;
+
+ type: 'text';
+
+ synthetic?: boolean;
+
+ time?: TextPart.Time;
+}
+
+export namespace TextPart {
+ export interface Time {
+ start: number;
+
+ end?: number;
+ }
+}
+
+export interface TextPartInput {
+ text: string;
+
+ type: 'text';
+
+ id?: string;
+
+ synthetic?: boolean;
+
+ time?: TextPartInput.Time;
+}
+
+export namespace TextPartInput {
+ export interface Time {
+ start: number;
+
+ end?: number;
+ }
+}
+
+export interface ToolPart {
+ id: string;
+
+ callID: string;
+
+ messageID: string;
+
+ sessionID: string;
+
+ state: ToolStatePending | ToolStateRunning | ToolStateCompleted | ToolStateError;
+
+ tool: string;
+
+ type: 'tool';
+}
+
+export interface ToolStateCompleted {
+ input: { [key: string]: unknown };
+
+ metadata: { [key: string]: unknown };
+
+ output: string;
+
+ status: 'completed';
+
+ time: ToolStateCompleted.Time;
+
+ title: string;
+}
+
+export namespace ToolStateCompleted {
+ export interface Time {
+ end: number;
+
+ start: number;
+ }
+}
+
+export interface ToolStateError {
+ error: string;
+
+ input: { [key: string]: unknown };
+
+ status: 'error';
+
+ time: ToolStateError.Time;
+}
+
+export namespace ToolStateError {
+ export interface Time {
+ end: number;
+
+ start: number;
+ }
+}
+
+export interface ToolStatePending {
+ status: 'pending';
+}
+
+export interface ToolStateRunning {
+ status: 'running';
+
+ time: ToolStateRunning.Time;
+
+ input?: unknown;
+
+ metadata?: { [key: string]: unknown };
+
+ title?: string;
+}
+
+export namespace ToolStateRunning {
+ export interface Time {
+ start: number;
+ }
+}
+
+export interface UserMessage {
+ id: string;
+
+ role: 'user';
+
+ sessionID: string;
+
+ time: UserMessage.Time;
+}
+
+export namespace UserMessage {
+ export interface Time {
+ created: number;
+ }
+}
+
+export type SessionListResponse = Array<Session>;
+
+export type SessionDeleteResponse = boolean;
+
+export type SessionAbortResponse = boolean;
+
+export type SessionInitResponse = boolean;
+
+export type SessionMessagesResponse = Array<SessionMessagesResponse.SessionMessagesResponseItem>;
+
+export namespace SessionMessagesResponse {
+ export interface SessionMessagesResponseItem {
+ info: SessionAPI.Message;
+
+ parts: Array<SessionAPI.Part>;
+ }
+}
+
+export type SessionSummarizeResponse = boolean;
+
+export interface SessionChatParams {
+ modelID: string;
+
+ parts: Array<TextPartInput | FilePartInput>;
+
+ providerID: string;
+
+ messageID?: string;
+
+ mode?: string;
+
+ tools?: { [key: string]: boolean };
+}
+
+export interface SessionInitParams {
+ messageID: string;
+
+ modelID: string;
+
+ providerID: string;
+}
+
+export interface SessionSummarizeParams {
+ modelID: string;
+
+ providerID: string;
+}
+
+export declare namespace SessionResource {
+ export {
+ type AssistantMessage as AssistantMessage,
+ type FilePart as FilePart,
+ type FilePartInput as FilePartInput,
+ type FilePartSource as FilePartSource,
+ type FilePartSourceText as FilePartSourceText,
+ type FileSource as FileSource,
+ type Message as Message,
+ type Part as Part,
+ type Session as Session,
+ type SnapshotPart as SnapshotPart,
+ type StepFinishPart as StepFinishPart,
+ type StepStartPart as StepStartPart,
+ type SymbolSource as SymbolSource,
+ type TextPart as TextPart,
+ type TextPartInput as TextPartInput,
+ type ToolPart as ToolPart,
+ type ToolStateCompleted as ToolStateCompleted,
+ type ToolStateError as ToolStateError,
+ type ToolStatePending as ToolStatePending,
+ type ToolStateRunning as ToolStateRunning,
+ type UserMessage as UserMessage,
+ type SessionListResponse as SessionListResponse,
+ type SessionDeleteResponse as SessionDeleteResponse,
+ type SessionAbortResponse as SessionAbortResponse,
+ type SessionInitResponse as SessionInitResponse,
+ type SessionMessagesResponse as SessionMessagesResponse,
+ type SessionSummarizeResponse as SessionSummarizeResponse,
+ type SessionChatParams as SessionChatParams,
+ type SessionInitParams as SessionInitParams,
+ type SessionSummarizeParams as SessionSummarizeParams,
+ };
+}
diff --git a/packages/sdk/src/resources/shared.ts b/packages/sdk/src/resources/shared.ts
new file mode 100644
index 000000000..31378f6fe
--- /dev/null
+++ b/packages/sdk/src/resources/shared.ts
@@ -0,0 +1,33 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+export interface MessageAbortedError {
+ data: unknown;
+
+ name: 'MessageAbortedError';
+}
+
+export interface ProviderAuthError {
+ data: ProviderAuthError.Data;
+
+ name: 'ProviderAuthError';
+}
+
+export namespace ProviderAuthError {
+ export interface Data {
+ message: string;
+
+ providerID: string;
+ }
+}
+
+export interface UnknownError {
+ data: UnknownError.Data;
+
+ name: 'UnknownError';
+}
+
+export namespace UnknownError {
+ export interface Data {
+ message: string;
+ }
+}
diff --git a/packages/sdk/src/resources/tui.ts b/packages/sdk/src/resources/tui.ts
new file mode 100644
index 000000000..c7efbdd6c
--- /dev/null
+++ b/packages/sdk/src/resources/tui.ts
@@ -0,0 +1,37 @@
+// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+import { APIResource } from '../core/resource';
+import { APIPromise } from '../core/api-promise';
+import { RequestOptions } from '../internal/request-options';
+
+export class Tui extends APIResource {
+ /**
+ * Append prompt to the TUI
+ */
+ appendPrompt(body: TuiAppendPromptParams, options?: RequestOptions): APIPromise<TuiAppendPromptResponse> {
+ return this._client.post('/tui/append-prompt', { body, ...options });
+ }
+
+ /**
+ * Open the help dialog
+ */
+ openHelp(options?: RequestOptions): APIPromise<TuiOpenHelpResponse> {
+ return this._client.post('/tui/open-help', options);
+ }
+}
+
+export type TuiAppendPromptResponse = boolean;
+
+export type TuiOpenHelpResponse = boolean;
+
+export interface TuiAppendPromptParams {
+ text: string;
+}
+
+export declare namespace Tui {
+ export {
+ type TuiAppendPromptResponse as TuiAppendPromptResponse,
+ type TuiOpenHelpResponse as TuiOpenHelpResponse,
+ type TuiAppendPromptParams as TuiAppendPromptParams,
+ };
+}
diff --git a/packages/sdk/src/streaming.ts b/packages/sdk/src/streaming.ts
new file mode 100644
index 000000000..9e6da1063
--- /dev/null
+++ b/packages/sdk/src/streaming.ts
@@ -0,0 +1,2 @@
+/** @deprecated Import from ./core/streaming instead */
+export * from './core/streaming';
diff --git a/packages/sdk/src/uploads.ts b/packages/sdk/src/uploads.ts
new file mode 100644
index 000000000..b2ef64710
--- /dev/null
+++ b/packages/sdk/src/uploads.ts
@@ -0,0 +1,2 @@
+/** @deprecated Import from ./core/uploads instead */
+export * from './core/uploads';
diff --git a/packages/sdk/src/version.ts b/packages/sdk/src/version.ts
new file mode 100644
index 000000000..48ca0ae07
--- /dev/null
+++ b/packages/sdk/src/version.ts
@@ -0,0 +1 @@
+export const VERSION = '0.1.0-alpha.20'; // x-release-please-version