Dax Raad 6 месяцев назад
Родитель
Сommit
0f1697b2ab

+ 19 - 8
bun.lock

@@ -26,7 +26,7 @@
     },
     "cloud/core": {
       "name": "@opencode/cloud-core",
-      "version": "0.5.13",
+      "version": "0.5.15",
       "dependencies": {
         "@aws-sdk/client-sts": "3.782.0",
         "drizzle-orm": "0.41.0",
@@ -40,7 +40,7 @@
     },
     "cloud/function": {
       "name": "@opencode/cloud-function",
-      "version": "0.5.13",
+      "version": "0.5.15",
       "dependencies": {
         "@ai-sdk/anthropic": "2.0.0",
         "@ai-sdk/openai": "2.0.2",
@@ -60,7 +60,7 @@
     },
     "cloud/web": {
       "name": "@opencode/cloud-web",
-      "version": "0.5.13",
+      "version": "0.5.15",
       "dependencies": {
         "@kobalte/core": "0.13.9",
         "@openauthjs/solid": "0.0.0-20250322224806",
@@ -79,7 +79,7 @@
     },
     "packages/function": {
       "name": "@opencode/function",
-      "version": "0.5.13",
+      "version": "0.5.15",
       "dependencies": {
         "@octokit/auth-app": "8.0.1",
         "@octokit/rest": "22.0.0",
@@ -94,7 +94,7 @@
     },
     "packages/opencode": {
       "name": "opencode",
-      "version": "0.5.13",
+      "version": "0.5.15",
       "bin": {
         "opencode": "./bin/opencode",
       },
@@ -144,7 +144,7 @@
     },
     "packages/plugin": {
       "name": "@opencode-ai/plugin",
-      "version": "0.5.13",
+      "version": "0.5.15",
       "dependencies": {
         "@opencode-ai/sdk": "workspace:*",
       },
@@ -156,16 +156,25 @@
     },
     "packages/sdk/js": {
       "name": "@opencode-ai/sdk",
-      "version": "0.5.13",
+      "version": "0.5.15",
+      "dependencies": {
+        "@hey-api/openapi-ts": "0.80.1",
+      },
       "devDependencies": {
         "@hey-api/openapi-ts": "0.80.1",
         "@tsconfig/node22": "catalog:",
         "typescript": "catalog:",
       },
     },
+    "packages/tmp": {
+      "name": "@opencode-ai/tmp",
+      "dependencies": {
+        "@opencode-ai/sdk": "workspace:",
+      },
+    },
     "packages/web": {
       "name": "@opencode/web",
-      "version": "0.5.13",
+      "version": "0.5.15",
       "dependencies": {
         "@astrojs/cloudflare": "12.6.3",
         "@astrojs/markdown-remark": "6.3.1",
@@ -678,6 +687,8 @@
 
     "@opencode-ai/sdk": ["@opencode-ai/sdk@workspace:packages/sdk/js"],
 
+    "@opencode-ai/tmp": ["@opencode-ai/tmp@workspace:packages/tmp"],
+
     "@opencode/cloud-app": ["@opencode/cloud-app@workspace:cloud/app"],
 
     "@opencode/cloud-core": ["@opencode/cloud-core@workspace:cloud/core"],

+ 2 - 2
packages/sdk/go/.stats.yml

@@ -1,4 +1,4 @@
 configured_endpoints: 41
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-d5200eaa145f567a58daa78941ab1141dd63f5f0cfe1596d5c9ecf12d34fea35.yml
-openapi_spec_hash: abeb66291dc158f2cdc90bf9945e283e
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/opencode%2Fopencode-6d8e9dfd438cac63fc7d689ea29adfff81ff8880c2d8e1e10fc36f375a721594.yml
+openapi_spec_hash: 7ac6028dd5957c67a98d91e790863c80
 config_hash: fb625e876313a9f8f31532348fa91f59

+ 1 - 0
packages/sdk/go/event.go

@@ -41,6 +41,7 @@ func (r *EventService) ListStreaming(ctx context.Context, opts ...option.Request
 		err error
 	)
 	opts = append(r.Options[:], opts...)
+	opts = append([]option.RequestOption{option.WithHeader("Accept", "text/event-stream")}, opts...)
 	path := "event"
 	err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...)
 	return ssestream.NewStream[EventListResponse](ssestream.NewDecoder(raw), err)

+ 3 - 0
packages/sdk/js/package.json

@@ -27,5 +27,8 @@
     "typescript": "catalog:",
     "@hey-api/openapi-ts": "0.80.1",
     "@tsconfig/node22": "catalog:"
+  },
+  "dependencies": {
+    "@hey-api/openapi-ts": "0.80.1"
   }
 }

+ 2 - 2
packages/sdk/js/src/client.ts

@@ -1,8 +1,8 @@
 export * from "./gen/types.gen.js"
 export { type Config as OpencodeClientConfig, OpencodeClient }
 
-import { createClient } from "./gen/client/client.js"
-import { type Config } from "./gen/client/types.js"
+import { createClient } from "./gen/client/client.gen.js"
+import { type Config } from "./gen/client/types.gen.js"
 import { OpencodeClient } from "./gen/sdk.gen.js"
 
 export function createOpencodeClient(config?: Config) {

+ 43 - 16
packages/sdk/js/src/gen/client/client.ts → packages/sdk/js/src/gen/client/client.gen.ts

@@ -1,4 +1,7 @@
-import type { Client, Config, RequestOptions } from "./types.js"
+// This file is auto-generated by @hey-api/openapi-ts
+
+import { createSseClient } from "../core/serverSentEvents.gen.js"
+import type { Client, Config, RequestOptions, ResolvedRequestOptions } from "./types.gen.js"
 import {
   buildUrl,
   createConfig,
@@ -7,7 +10,7 @@ import {
   mergeConfigs,
   mergeHeaders,
   setAuthParams,
-} from "./utils.js"
+} from "./utils.gen.js"
 
 type ReqInit = Omit<RequestInit, "body" | "headers"> & {
   body?: any
@@ -24,14 +27,15 @@ export const createClient = (config: Config = {}): Client => {
     return getConfig()
   }
 
-  const interceptors = createInterceptors<Request, Response, unknown, RequestOptions>()
+  const interceptors = createInterceptors<Request, Response, unknown, ResolvedRequestOptions>()
 
-  const request: Client["request"] = async (options) => {
+  const beforeRequest = async (options: RequestOptions) => {
     const opts = {
       ..._config,
       ...options,
       fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
       headers: mergeHeaders(_config.headers, options.headers),
+      serializedBody: undefined,
     }
 
     if (opts.security) {
@@ -46,18 +50,26 @@ export const createClient = (config: Config = {}): Client => {
     }
 
     if (opts.body && opts.bodySerializer) {
-      opts.body = opts.bodySerializer(opts.body)
+      opts.serializedBody = opts.bodySerializer(opts.body)
     }
 
     // remove Content-Type header if body is empty to avoid sending invalid requests
-    if (opts.body === undefined || opts.body === "") {
+    if (opts.serializedBody === undefined || opts.serializedBody === "") {
       opts.headers.delete("Content-Type")
     }
 
     const url = buildUrl(opts)
+
+    return { opts, url }
+  }
+
+  const request: Client["request"] = async (options) => {
+    // @ts-expect-error
+    const { opts, url } = await beforeRequest(options)
     const requestInit: ReqInit = {
       redirect: "follow",
       ...opts,
+      body: opts.serializedBody,
     }
 
     let request = new Request(url, requestInit)
@@ -166,20 +178,35 @@ export const createClient = (config: Config = {}): Client => {
         }
   }
 
+  const makeMethod = (method: Required<Config>["method"]) => {
+    const fn = (options: RequestOptions) => request({ ...options, method })
+    fn.sse = async (options: RequestOptions) => {
+      const { opts, url } = await beforeRequest(options)
+      return createSseClient({
+        ...opts,
+        body: opts.body as BodyInit | null | undefined,
+        headers: opts.headers as unknown as Record<string, string>,
+        method,
+        url,
+      })
+    }
+    return fn
+  }
+
   return {
     buildUrl,
-    connect: (options) => request({ ...options, method: "CONNECT" }),
-    delete: (options) => request({ ...options, method: "DELETE" }),
-    get: (options) => request({ ...options, method: "GET" }),
+    connect: makeMethod("CONNECT"),
+    delete: makeMethod("DELETE"),
+    get: makeMethod("GET"),
     getConfig,
-    head: (options) => request({ ...options, method: "HEAD" }),
+    head: makeMethod("HEAD"),
     interceptors,
-    options: (options) => request({ ...options, method: "OPTIONS" }),
-    patch: (options) => request({ ...options, method: "PATCH" }),
-    post: (options) => request({ ...options, method: "POST" }),
-    put: (options) => request({ ...options, method: "PUT" }),
+    options: makeMethod("OPTIONS"),
+    patch: makeMethod("PATCH"),
+    post: makeMethod("POST"),
+    put: makeMethod("PUT"),
     request,
     setConfig,
-    trace: (options) => request({ ...options, method: "TRACE" }),
-  }
+    trace: makeMethod("TRACE"),
+  } as Client
 }

+ 14 - 7
packages/sdk/js/src/gen/client/index.ts

@@ -1,8 +1,14 @@
-export type { Auth } from "../core/auth.js"
-export type { QuerySerializerOptions } from "../core/bodySerializer.js"
-export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer } from "../core/bodySerializer.js"
-export { buildClientParams } from "../core/params.js"
-export { createClient } from "./client.js"
+// This file is auto-generated by @hey-api/openapi-ts
+
+export type { Auth } from "../core/auth.gen.js"
+export type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
+export {
+  formDataBodySerializer,
+  jsonBodySerializer,
+  urlSearchParamsBodySerializer,
+} from "../core/bodySerializer.gen.js"
+export { buildClientParams } from "../core/params.gen.js"
+export { createClient } from "./client.gen.js"
 export type {
   Client,
   ClientOptions,
@@ -12,7 +18,8 @@ export type {
   OptionsLegacyParser,
   RequestOptions,
   RequestResult,
+  ResolvedRequestOptions,
   ResponseStyle,
   TDataShape,
-} from "./types.js"
-export { createConfig, mergeHeaders } from "./utils.js"
+} from "./types.gen.js"
+export { createConfig, mergeHeaders } from "./utils.gen.js"

+ 49 - 18
packages/sdk/js/src/gen/client/types.ts → packages/sdk/js/src/gen/client/types.gen.ts

@@ -1,6 +1,9 @@
-import type { Auth } from "../core/auth.js"
-import type { Client as CoreClient, Config as CoreConfig } from "../core/types.js"
-import type { Middleware } from "./utils.js"
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Auth } from "../core/auth.gen.js"
+import type { ServerSentEventsOptions, ServerSentEventsResult } from "../core/serverSentEvents.gen.js"
+import type { Client as CoreClient, Config as CoreConfig } from "../core/types.gen.js"
+import type { Middleware } from "./utils.gen.js"
 
 export type ResponseStyle = "data" | "fields"
 
@@ -49,13 +52,18 @@ export interface Config<T extends ClientOptions = ClientOptions>
 }
 
 export interface RequestOptions<
+  TData = unknown,
   TResponseStyle extends ResponseStyle = "fields",
   ThrowOnError extends boolean = boolean,
   Url extends string = string,
 > extends Config<{
-    responseStyle: TResponseStyle
-    throwOnError: ThrowOnError
-  }> {
+      responseStyle: TResponseStyle
+      throwOnError: ThrowOnError
+    }>,
+    Pick<
+      ServerSentEventsOptions<TData>,
+      "onSseError" | "onSseEvent" | "sseDefaultRetryDelay" | "sseMaxRetryAttempts" | "sseMaxRetryDelay"
+    > {
   /**
    * Any body that you want to add to your request.
    *
@@ -71,6 +79,14 @@ export interface RequestOptions<
   url: Url
 }
 
+export interface ResolvedRequestOptions<
+  TResponseStyle extends ResponseStyle = "fields",
+  ThrowOnError extends boolean = boolean,
+  Url extends string = string,
+> extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
+  serializedBody?: string
+}
+
 export type RequestResult<
   TData = unknown,
   TError = unknown,
@@ -112,23 +128,36 @@ export interface ClientOptions {
   throwOnError?: boolean
 }
 
-type MethodFn = <
+type MethodFnBase = <
   TData = unknown,
   TError = unknown,
   ThrowOnError extends boolean = false,
   TResponseStyle extends ResponseStyle = "fields",
 >(
-  options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method">,
+  options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
 ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
 
+type MethodFnServerSentEvents = <
+  TData = unknown,
+  TError = unknown,
+  ThrowOnError extends boolean = false,
+  TResponseStyle extends ResponseStyle = "fields",
+>(
+  options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method">,
+) => Promise<ServerSentEventsResult<TData, TError>>
+
+type MethodFn = MethodFnBase & {
+  sse: MethodFnServerSentEvents
+}
+
 type RequestFn = <
   TData = unknown,
   TError = unknown,
   ThrowOnError extends boolean = false,
   TResponseStyle extends ResponseStyle = "fields",
 >(
-  options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, "method"> &
-    Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, "method">,
+  options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, "method"> &
+    Pick<Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>, "method">,
 ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>
 
 type BuildUrlFn = <
@@ -143,7 +172,7 @@ type BuildUrlFn = <
 ) => string
 
 export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
-  interceptors: Middleware<Request, Response, unknown, RequestOptions>
+  interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>
 }
 
 /**
@@ -171,8 +200,10 @@ type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>
 export type Options<
   TData extends TDataShape = TDataShape,
   ThrowOnError extends boolean = boolean,
+  TResponse = unknown,
   TResponseStyle extends ResponseStyle = "fields",
-> = OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> & Omit<TData, "url">
+> = OmitKeys<RequestOptions<TResponse, TResponseStyle, ThrowOnError>, "body" | "path" | "query" | "url"> &
+  Omit<TData, "url">
 
 export type OptionsLegacyParser<
   TData = unknown,
@@ -180,12 +211,12 @@ export type OptionsLegacyParser<
   TResponseStyle extends ResponseStyle = "fields",
 > = TData extends { body?: any }
   ? TData extends { headers?: any }
-    ? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
-    : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "body" | "url"> &
+    ? OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "headers" | "url"> & TData
+    : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body" | "url"> &
         TData &
-        Pick<RequestOptions<TResponseStyle, ThrowOnError>, "headers">
+        Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "headers">
   : TData extends { headers?: any }
-    ? OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "headers" | "url"> &
+    ? OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "headers" | "url"> &
         TData &
-        Pick<RequestOptions<TResponseStyle, ThrowOnError>, "body">
-    : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, "url"> & TData
+        Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "body">
+    : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, "url"> & TData

+ 28 - 114
packages/sdk/js/src/gen/client/utils.ts → packages/sdk/js/src/gen/client/utils.gen.ts

@@ -1,84 +1,11 @@
-import { getAuthToken } from "../core/auth.js"
-import type { QuerySerializer, QuerySerializerOptions } from "../core/bodySerializer.js"
-import { jsonBodySerializer } from "../core/bodySerializer.js"
-import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.js"
-import type { Client, ClientOptions, Config, RequestOptions } from "./types.js"
-
-interface PathSerializer {
-  path: Record<string, unknown>
-  url: string
-}
-
-const PATH_PARAM_RE = /\{[^{}]+\}/g
-
-type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"
-type MatrixStyle = "label" | "matrix" | "simple"
-type ArraySeparatorStyle = ArrayStyle | MatrixStyle
+// This file is auto-generated by @hey-api/openapi-ts
 
-const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
-  let url = _url
-  const matches = _url.match(PATH_PARAM_RE)
-  if (matches) {
-    for (const match of matches) {
-      let explode = false
-      let name = match.substring(1, match.length - 1)
-      let style: ArraySeparatorStyle = "simple"
-
-      if (name.endsWith("*")) {
-        explode = true
-        name = name.substring(0, name.length - 1)
-      }
-
-      if (name.startsWith(".")) {
-        name = name.substring(1)
-        style = "label"
-      } else if (name.startsWith(";")) {
-        name = name.substring(1)
-        style = "matrix"
-      }
-
-      const value = path[name]
-
-      if (value === undefined || value === null) {
-        continue
-      }
-
-      if (Array.isArray(value)) {
-        url = url.replace(match, serializeArrayParam({ explode, name, style, value }))
-        continue
-      }
-
-      if (typeof value === "object") {
-        url = url.replace(
-          match,
-          serializeObjectParam({
-            explode,
-            name,
-            style,
-            value: value as Record<string, unknown>,
-            valueOnly: true,
-          }),
-        )
-        continue
-      }
-
-      if (style === "matrix") {
-        url = url.replace(
-          match,
-          `;${serializePrimitiveParam({
-            name,
-            value: value as string,
-          })}`,
-        )
-        continue
-      }
-
-      const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
-      url = url.replace(match, replaceValue)
-    }
-  }
-  return url
-}
+import { getAuthToken } from "../core/auth.gen.js"
+import type { QuerySerializerOptions } from "../core/bodySerializer.gen.js"
+import { jsonBodySerializer } from "../core/bodySerializer.gen.js"
+import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam } from "../core/pathSerializer.gen.js"
+import { getUrl } from "../core/utils.gen.js"
+import type { Client, ClientOptions, Config, RequestOptions } from "./types.gen.js"
 
 export const createQuerySerializer = <T = unknown>({ allowReserved, array, object }: QuerySerializerOptions = {}) => {
   const querySerializer = (queryParams: T) => {
@@ -161,6 +88,21 @@ export const getParseAs = (contentType: string | null): Exclude<Config["parseAs"
   return
 }
 
+const checkForExistence = (
+  options: Pick<RequestOptions, "auth" | "query"> & {
+    headers: Headers
+  },
+  name?: string,
+): boolean => {
+  if (!name) {
+    return false
+  }
+  if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) {
+    return true
+  }
+  return false
+}
+
 export const setAuthParams = async ({
   security,
   ...options
@@ -169,6 +111,10 @@ export const setAuthParams = async ({
     headers: Headers
   }) => {
   for (const auth of security) {
+    if (checkForExistence(options, auth.name)) {
+      continue
+    }
+
     const token = await getAuthToken(auth, options.auth)
 
     if (!token) {
@@ -192,13 +138,11 @@ export const setAuthParams = async ({
         options.headers.set(name, token)
         break
     }
-
-    return
   }
 }
 
-export const buildUrl: Client["buildUrl"] = (options) => {
-  const url = getUrl({
+export const buildUrl: Client["buildUrl"] = (options) =>
+  getUrl({
     baseUrl: options.baseUrl as string,
     path: options.path,
     query: options.query,
@@ -208,36 +152,6 @@ export const buildUrl: Client["buildUrl"] = (options) => {
         : createQuerySerializer(options.querySerializer),
     url: options.url,
   })
-  return url
-}
-
-export const getUrl = ({
-  baseUrl,
-  path,
-  query,
-  querySerializer,
-  url: _url,
-}: {
-  baseUrl?: string
-  path?: Record<string, unknown>
-  query?: Record<string, unknown>
-  querySerializer: QuerySerializer
-  url: string
-}) => {
-  const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
-  let url = (baseUrl ?? "") + pathUrl
-  if (path) {
-    url = defaultPathSerializer({ path, url })
-  }
-  let search = query ? querySerializer(query) : ""
-  if (search.startsWith("?")) {
-    search = search.substring(1)
-  }
-  if (search) {
-    url += `?${search}`
-  }
-  return url
-}
 
 export const mergeConfigs = (a: Config, b: Config): Config => {
   const config = { ...a, ...b }

+ 2 - 0
packages/sdk/js/src/gen/core/auth.ts → packages/sdk/js/src/gen/core/auth.gen.ts

@@ -1,3 +1,5 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
 export type AuthToken = string | undefined
 
 export interface Auth {

+ 5 - 1
packages/sdk/js/src/gen/core/bodySerializer.ts → packages/sdk/js/src/gen/core/bodySerializer.gen.ts

@@ -1,4 +1,6 @@
-import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.js"
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { ArrayStyle, ObjectStyle, SerializerOptions } from "./pathSerializer.gen.js"
 
 export type QuerySerializer = (query: Record<string, unknown>) => string
 
@@ -13,6 +15,8 @@ export interface QuerySerializerOptions {
 const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => {
   if (typeof value === "string" || value instanceof Blob) {
     data.append(key, value)
+  } else if (value instanceof Date) {
+    data.append(key, value.toISOString())
   } else {
     data.append(key, JSON.stringify(value))
   }

+ 2 - 0
packages/sdk/js/src/gen/core/params.ts → packages/sdk/js/src/gen/core/params.gen.ts

@@ -1,3 +1,5 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
 type Slot = "body" | "headers" | "path" | "query"
 
 export type Field =

+ 2 - 0
packages/sdk/js/src/gen/core/pathSerializer.ts → packages/sdk/js/src/gen/core/pathSerializer.gen.ts

@@ -1,3 +1,5 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
 interface SerializeOptions<T> extends SerializePrimitiveOptions, SerializerOptions<T> {}
 
 interface SerializePrimitiveOptions {

+ 210 - 0
packages/sdk/js/src/gen/core/serverSentEvents.gen.ts

@@ -0,0 +1,210 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Config } from "./types.gen.js"
+
+export type ServerSentEventsOptions<TData = unknown> = Omit<RequestInit, "method"> &
+  Pick<Config, "method" | "responseTransformer" | "responseValidator"> & {
+    /**
+     * Callback invoked when a network or parsing error occurs during streaming.
+     *
+     * This option applies only if the endpoint returns a stream of events.
+     *
+     * @param error The error that occurred.
+     */
+    onSseError?: (error: unknown) => void
+    /**
+     * Callback invoked when an event is streamed from the server.
+     *
+     * This option applies only if the endpoint returns a stream of events.
+     *
+     * @param event Event streamed from the server.
+     * @returns Nothing (void).
+     */
+    onSseEvent?: (event: StreamEvent<TData>) => void
+    /**
+     * Default retry delay in milliseconds.
+     *
+     * This option applies only if the endpoint returns a stream of events.
+     *
+     * @default 3000
+     */
+    sseDefaultRetryDelay?: number
+    /**
+     * Maximum number of retry attempts before giving up.
+     */
+    sseMaxRetryAttempts?: number
+    /**
+     * Maximum retry delay in milliseconds.
+     *
+     * Applies only when exponential backoff is used.
+     *
+     * This option applies only if the endpoint returns a stream of events.
+     *
+     * @default 30000
+     */
+    sseMaxRetryDelay?: number
+    /**
+     * Optional sleep function for retry backoff.
+     *
+     * Defaults to using `setTimeout`.
+     */
+    sseSleepFn?: (ms: number) => Promise<void>
+    url: string
+  }
+
+export interface StreamEvent<TData = unknown> {
+  data: TData
+  event?: string
+  id?: string
+  retry?: number
+}
+
+export type ServerSentEventsResult<TData = unknown, TReturn = void, TNext = unknown> = {
+  stream: AsyncGenerator<TData extends Record<string, unknown> ? TData[keyof TData] : TData, TReturn, TNext>
+}
+
+export const createSseClient = <TData = unknown>({
+  onSseError,
+  onSseEvent,
+  responseTransformer,
+  responseValidator,
+  sseDefaultRetryDelay,
+  sseMaxRetryAttempts,
+  sseMaxRetryDelay,
+  sseSleepFn,
+  url,
+  ...options
+}: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
+  let lastEventId: string | undefined
+
+  const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)))
+
+  const createStream = async function* () {
+    let retryDelay: number = sseDefaultRetryDelay ?? 3000
+    let attempt = 0
+    const signal = options.signal ?? new AbortController().signal
+
+    while (true) {
+      if (signal.aborted) break
+
+      attempt++
+
+      const headers =
+        options.headers instanceof Headers
+          ? options.headers
+          : new Headers(options.headers as Record<string, string> | undefined)
+
+      if (lastEventId !== undefined) {
+        headers.set("Last-Event-ID", lastEventId)
+      }
+
+      try {
+        const response = await fetch(url, { ...options, headers, signal })
+
+        if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`)
+
+        if (!response.body) throw new Error("No body in SSE response")
+
+        const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
+
+        let buffer = ""
+
+        const abortHandler = () => {
+          try {
+            reader.cancel()
+          } catch {
+            // noop
+          }
+        }
+
+        signal.addEventListener("abort", abortHandler)
+
+        try {
+          while (true) {
+            const { done, value } = await reader.read()
+            if (done) break
+            buffer += value
+
+            const chunks = buffer.split("\n\n")
+            buffer = chunks.pop() ?? ""
+
+            for (const chunk of chunks) {
+              const lines = chunk.split("\n")
+              const dataLines: Array<string> = []
+              let eventName: string | undefined
+
+              for (const line of lines) {
+                if (line.startsWith("data:")) {
+                  dataLines.push(line.replace(/^data:\s*/, ""))
+                } else if (line.startsWith("event:")) {
+                  eventName = line.replace(/^event:\s*/, "")
+                } else if (line.startsWith("id:")) {
+                  lastEventId = line.replace(/^id:\s*/, "")
+                } else if (line.startsWith("retry:")) {
+                  const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10)
+                  if (!Number.isNaN(parsed)) {
+                    retryDelay = parsed
+                  }
+                }
+              }
+
+              let data: unknown
+              let parsedJson = false
+
+              if (dataLines.length) {
+                const rawData = dataLines.join("\n")
+                try {
+                  data = JSON.parse(rawData)
+                  parsedJson = true
+                } catch {
+                  data = rawData
+                }
+              }
+
+              if (parsedJson) {
+                if (responseValidator) {
+                  await responseValidator(data)
+                }
+
+                if (responseTransformer) {
+                  data = await responseTransformer(data)
+                }
+              }
+
+              onSseEvent?.({
+                data,
+                event: eventName,
+                id: lastEventId,
+                retry: retryDelay,
+              })
+
+              if (dataLines.length) {
+                yield data as any
+              }
+            }
+          }
+        } finally {
+          signal.removeEventListener("abort", abortHandler)
+          reader.releaseLock()
+        }
+
+        break // exit loop on normal completion
+      } catch (error) {
+        // connection failed or aborted; retry after delay
+        onSseError?.(error)
+
+        if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) {
+          break // stop after firing error
+        }
+
+        // exponential backoff: double retry each attempt, cap at 30s
+        const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000)
+        await sleep(backoff)
+      }
+    }
+  }
+
+  const stream = createStream()
+
+  return { stream }
+}

+ 4 - 2
packages/sdk/js/src/gen/core/types.ts → packages/sdk/js/src/gen/core/types.gen.ts

@@ -1,5 +1,7 @@
-import type { Auth, AuthToken } from "./auth.js"
-import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.js"
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { Auth, AuthToken } from "./auth.gen.js"
+import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from "./bodySerializer.gen.js"
 
 export interface Client<RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never> {
   /**

+ 109 - 0
packages/sdk/js/src/gen/core/utils.gen.ts

@@ -0,0 +1,109 @@
+// This file is auto-generated by @hey-api/openapi-ts
+
+import type { QuerySerializer } from "./bodySerializer.gen.js"
+import {
+  type ArraySeparatorStyle,
+  serializeArrayParam,
+  serializeObjectParam,
+  serializePrimitiveParam,
+} from "./pathSerializer.gen.js"
+
+export interface PathSerializer {
+  path: Record<string, unknown>
+  url: string
+}
+
+export const PATH_PARAM_RE = /\{[^{}]+\}/g
+
+export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
+  let url = _url
+  const matches = _url.match(PATH_PARAM_RE)
+  if (matches) {
+    for (const match of matches) {
+      let explode = false
+      let name = match.substring(1, match.length - 1)
+      let style: ArraySeparatorStyle = "simple"
+
+      if (name.endsWith("*")) {
+        explode = true
+        name = name.substring(0, name.length - 1)
+      }
+
+      if (name.startsWith(".")) {
+        name = name.substring(1)
+        style = "label"
+      } else if (name.startsWith(";")) {
+        name = name.substring(1)
+        style = "matrix"
+      }
+
+      const value = path[name]
+
+      if (value === undefined || value === null) {
+        continue
+      }
+
+      if (Array.isArray(value)) {
+        url = url.replace(match, serializeArrayParam({ explode, name, style, value }))
+        continue
+      }
+
+      if (typeof value === "object") {
+        url = url.replace(
+          match,
+          serializeObjectParam({
+            explode,
+            name,
+            style,
+            value: value as Record<string, unknown>,
+            valueOnly: true,
+          }),
+        )
+        continue
+      }
+
+      if (style === "matrix") {
+        url = url.replace(
+          match,
+          `;${serializePrimitiveParam({
+            name,
+            value: value as string,
+          })}`,
+        )
+        continue
+      }
+
+      const replaceValue = encodeURIComponent(style === "label" ? `.${value as string}` : (value as string))
+      url = url.replace(match, replaceValue)
+    }
+  }
+  return url
+}
+
+export const getUrl = ({
+  baseUrl,
+  path,
+  query,
+  querySerializer,
+  url: _url,
+}: {
+  baseUrl?: string
+  path?: Record<string, unknown>
+  query?: Record<string, unknown>
+  querySerializer: QuerySerializer
+  url: string
+}) => {
+  const pathUrl = _url.startsWith("/") ? _url : `/${_url}`
+  let url = (baseUrl ?? "") + pathUrl
+  if (path) {
+    url = defaultPathSerializer({ path, url })
+  }
+  let search = query ? querySerializer(query) : ""
+  if (search.startsWith("?")) {
+    search = search.substring(1)
+  }
+  if (search) {
+    url += `?${search}`
+  }
+  return url
+}

+ 1 - 1
packages/sdk/js/src/gen/sdk.gen.ts

@@ -123,7 +123,7 @@ class Event extends _HeyApiClient {
    * Get events
    */
   public subscribe<ThrowOnError extends boolean = false>(options?: Options<EventSubscribeData, ThrowOnError>) {
-    return (options?.client ?? this._client).get<EventSubscribeResponses, unknown, ThrowOnError>({
+    return (options?.client ?? this._client).get.sse<EventSubscribeResponses, unknown, ThrowOnError>({
       url: "/event",
       ...options,
     })

+ 1 - 1
packages/sdk/js/src/server.ts

@@ -17,7 +17,7 @@ export async function createOpencodeServer(config?: ServerConfig) {
     config ?? {},
   )
 
-  const proc = spawn(`opencode`, [`servel`, `--hostname=${config.hostname}`, `--port=${config.port}`], {
+  const proc = spawn(`opencode`, [`serve`, `--hostname=${config.hostname}`, `--port=${config.port}`], {
     signal: config.signal,
   })