From 17189d1001cd3a9289d8066015ef37c6626e4f2a Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 25 Oct 2026 15:16:42 -0700 Subject: [PATCH 01/11] refactor(client): nest experimental endpoints under client.experimental --- packages/app/src/home/projects/controller.tsx | 2 +- packages/app/src/providers/connect/mcp.ts | 4 +- packages/app/src/runtime/server/sync.tsx | 6 +- packages/app/src/session/revert.ts | 2 +- .../app/src/settings/general/controllers.ts | 2 +- packages/cli/src/acp/sessions.ts | 2 +- .../cli/src/commands/handlers/auth/login.ts | 2 +- .../src/commands/handlers/session/export.ts | 2 +- packages/cli/src/commands/handlers/stats.ts | 2 +- packages/cli/src/run/noninteractive.ts | 2 +- packages/cli/test/fixture/mini-wait.ts | 8 +- packages/cli/test/run/noninteractive.test.ts | 4 +- packages/client/script/build.ts | 6 +- packages/client/src/contract.ts | 2 +- packages/client/src/effect/api/api.ts | 688 +++++++------ .../client/src/effect/generated/client.ts | 436 +++++---- packages/client/src/promise/api.ts | 3 +- .../client/src/promise/generated/client.ts | 923 +++++++++--------- .../client/src/promise/generated/types.ts | 920 ++++++++--------- packages/client/src/solid/data.ts | 2 +- packages/client/test/api.types.ts | 14 +- packages/client/test/effect.test.ts | 12 +- packages/client/test/promise.test.ts | 102 +- .../desktop/src/renderer/migration-status.tsx | 6 +- .../src/details/server-panel.tsx | 4 +- packages/httpapi-codegen/src/index.ts | 213 +++- .../httpapi-codegen/test/generate.test.ts | 73 ++ packages/plugin/src/effect/plugin.ts | 4 +- packages/plugin/src/effect/session.ts | 6 +- packages/plugin/src/promise/plugin.ts | 4 +- packages/plugin/src/promise/session.ts | 6 +- packages/protocol/src/client.ts | 6 + packages/sdk/test/contract-identity.test.ts | 6 +- packages/sdk/test/embedded.test.ts | 24 +- packages/sdk/test/instances-effect.test.ts | 6 +- packages/sdk/test/promise.test.ts | 2 +- packages/sdk/test/transport.test.ts | 4 +- packages/server/test/message.test.ts | 2 +- packages/server/test/sse-failure.test.ts | 2 +- packages/tui/src/component/devtools-bar.tsx | 2 +- packages/tui/src/component/dialog-mcp.tsx | 2 +- .../tui/src/component/migration-overlay.tsx | 2 +- .../tui/src/feature-plugins/system/stats.tsx | 2 +- packages/tui/src/mini/stream-v2.transport.ts | 2 +- packages/tui/src/routes/session/index.tsx | 6 +- .../tui/test/mini/stream-v2.transport.test.ts | 2 +- 46 files changed, 1947 insertions(+), 1585 deletions(-) diff --git a/packages/app/src/home/projects/controller.tsx b/packages/app/src/home/projects/controller.tsx index f531d264ca23..4368096bc4d5 100644 --- a/packages/app/src/home/projects/controller.tsx +++ b/packages/app/src/home/projects/controller.tsx @@ -124,7 +124,7 @@ export function createHomeProjectsController(home: HomeController) { Schema.fromJsonString(Schema.Record(Schema.String, Schema.Json)), )(await file.text()) - const api = home.server.context(conn).sdk.api.session + const api = home.server.context(conn).sdk.api.experimental.session // SAFETY: the import endpoint validates the transcript, including exports written by earlier releases. const imported = await api.import({ diff --git a/packages/app/src/providers/connect/mcp.ts b/packages/app/src/providers/connect/mcp.ts index c578895ed306..3adc26ce2572 100644 --- a/packages/app/src/providers/connect/mcp.ts +++ b/packages/app/src/providers/connect/mcp.ts @@ -35,11 +35,11 @@ export function useMcpToggle(directory?: Accessor, onSuccess const enabled = typeof input === "string" ? server.status.status !== "connected" : input.enabled if (!enabled) { - await serverSDK.api.mcp.disconnect({ server: name, location: ref }) + await serverSDK.api.experimental.mcp.disconnect({ server: name, location: ref }) } if (enabled && server.status.status !== "needs_auth") { - await serverSDK.api.mcp.connect({ server: name, location: ref }) + await serverSDK.api.experimental.mcp.connect({ server: name, location: ref }) } data.location.mcp.server.invalidate(ref) diff --git a/packages/app/src/runtime/server/sync.tsx b/packages/app/src/runtime/server/sync.tsx index 1c739c257d7b..e745ede74647 100644 --- a/packages/app/src/runtime/server/sync.tsx +++ b/packages/app/src/runtime/server/sync.tsx @@ -289,7 +289,7 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) { const updateConfigMutation = useMutation(() => ({ mutationFn: async (config: Config) => { // TODO: Restore config updates when the V2 client exposes a config API. - // await serverSDK.api.config.update({ config }) + // await serverSDK.api.experimental.config.update({ config }) throw new Error(`Config updates are unavailable: ${Object.keys(config).length} fields were not saved`) }, onSuccess: () => { @@ -318,10 +318,10 @@ export function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) { await toggleMcp({ status, connect: async () => { - await serverSDK.api.mcp.connect({ server: name, location: { directory: key } }) + await serverSDK.api.experimental.mcp.connect({ server: name, location: { directory: key } }) }, disconnect: async () => { - await serverSDK.api.mcp.disconnect({ server: name, location: { directory: key } }) + await serverSDK.api.experimental.mcp.disconnect({ server: name, location: { directory: key } }) }, authenticate: async () => { const server = (await serverSDK.api.mcp.list({ location: { directory: key } })).data.find( diff --git a/packages/app/src/session/revert.ts b/packages/app/src/session/revert.ts index ca79e163561a..88ad51875e77 100644 --- a/packages/app/src/session/revert.ts +++ b/packages/app/src/session/revert.ts @@ -67,7 +67,7 @@ export function createSessionRevert(input: { // Like the TUI, stop at the first failure instead of waiting on work that was never interrupted. if (!(await request(() => server.api.session.interrupt({ sessionID })))) return - if (!(await request(() => server.api.session.wait({ sessionID })))) return + if (!(await request(() => server.api.experimental.session.wait({ sessionID })))) return if (!(await request(() => server.api.session.revert.stage({ sessionID, messageID: message.id })))) return // Like the TUI, pending inputs are left alone: the revert hides them, committing it drops them, diff --git a/packages/app/src/settings/general/controllers.ts b/packages/app/src/settings/general/controllers.ts index e6e7c3df0cef..a6f6bcbb4de9 100644 --- a/packages/app/src/settings/general/controllers.ts +++ b/packages/app/src/settings/general/controllers.ts @@ -60,7 +60,7 @@ export function createServerShellController(server: Accessor { actions.mutate(previous) showToast({ diff --git a/packages/cli/src/acp/sessions.ts b/packages/cli/src/acp/sessions.ts index 1384893e597b..5a39eac2aaaf 100644 --- a/packages/cli/src/acp/sessions.ts +++ b/packages/cli/src/acp/sessions.ts @@ -101,7 +101,7 @@ export const make = Effect.fnUntraced(function* (input: { const config = mcpConfig(server) if (isDeepStrictEqual(registered.get(server.name), config)) return Effect.void registered.set(server.name, config) - return input.client.mcp.add({ server: server.name, location: { directory: attached.cwd }, config }).pipe( + return input.client.experimental.mcp.add({ server: server.name, location: { directory: attached.cwd }, config }).pipe( Effect.catch(ACPClient.classify), Effect.onError(() => Effect.sync(() => registered.delete(server.name))), Effect.uninterruptible, diff --git a/packages/cli/src/commands/handlers/auth/login.ts b/packages/cli/src/commands/handlers/auth/login.ts index 7bd17698e9a3..82b31b581685 100644 --- a/packages/cli/src/commands/handlers/auth/login.ts +++ b/packages/cli/src/commands/handlers/auth/login.ts @@ -61,7 +61,7 @@ const findIntegration = Effect.fn("cli.auth.login.integration")(function* (clien if (protocol === "http:" || protocol === "https:") { const progress = spinner() progress.start("Discovering authentication provider...") - yield* request((signal) => client.integration.wellknown.add({ url: target, location }, { signal })).pipe( + yield* request((signal) => client.experimental.integration.wellknown.add({ url: target, location }, { signal })).pipe( Effect.tap(() => Effect.sync(() => progress.stop("Authentication provider discovered"))), Effect.tapCause(() => Effect.sync(() => progress.stop("Discovery failed", 1))), ) diff --git a/packages/cli/src/commands/handlers/session/export.ts b/packages/cli/src/commands/handlers/session/export.ts index 52a155b7bf64..f52ef725b928 100644 --- a/packages/cli/src/commands/handlers/session/export.ts +++ b/packages/cli/src/commands/handlers/session/export.ts @@ -68,7 +68,7 @@ export default Runtime.handler( }) if (!sessionID) return const data = yield* Effect.tryPromise({ - try: () => client.session.export({ sessionID, sanitize: input.sanitize }), + try: () => client.experimental.session.export({ sessionID, sanitize: input.sanitize }), catch: (cause) => cause, }) process.stdout.write(JSON.stringify(data, null, 2) + EOL) diff --git a/packages/cli/src/commands/handlers/stats.ts b/packages/cli/src/commands/handlers/stats.ts index 1fd251f11663..b6507ab49468 100644 --- a/packages/cli/src/commands/handlers/stats.ts +++ b/packages/cli/src/commands/handlers/stats.ts @@ -29,7 +29,7 @@ const handler = Effect.fn("cli.stats")(function* (input: Runtime.Input - client.session.stats( + client.experimental.session.stats( { from: range.from, to: range.to, diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index af42a1ac0782..22900f3e1165 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -758,7 +758,7 @@ export async function runNonInteractivePrompt(input: Input) { return } - const waiting = input.client.session.wait({ sessionID: input.sessionID }) + const waiting = input.client.experimental.session.wait({ sessionID: input.sessionID }) await Promise.race([waiting, completed.then(() => waiting)]) finalizing = true const projected = await reconcile() diff --git a/packages/cli/test/fixture/mini-wait.ts b/packages/cli/test/fixture/mini-wait.ts index 5787110bfbe3..acd941119720 100644 --- a/packages/cli/test/fixture/mini-wait.ts +++ b/packages/cli/test/fixture/mini-wait.ts @@ -14,11 +14,11 @@ const timeout = (error: unknown) => error.cause.name === "TimeoutError" await Promise.all([ - connection.sdk.session.wait({ sessionID: "ses_initial" }), - replacement.session.wait({ sessionID: "ses_reconnected" }), - assert.rejects(OpenCode.make({ baseUrl: endpoint.url }).session.wait({ sessionID: "ses_control" }), timeout), + connection.sdk.experimental.session.wait({ sessionID: "ses_initial" }), + replacement.experimental.session.wait({ sessionID: "ses_reconnected" }), + assert.rejects(OpenCode.make({ baseUrl: endpoint.url }).experimental.session.wait({ sessionID: "ses_control" }), timeout), assert.rejects( - connection.sdk.session.wait({ sessionID: "ses_cancelled" }, { signal: AbortSignal.timeout(100) }), + connection.sdk.experimental.session.wait({ sessionID: "ses_cancelled" }, { signal: AbortSignal.timeout(100) }), timeout, ), ]) diff --git a/packages/cli/test/run/noninteractive.test.ts b/packages/cli/test/run/noninteractive.test.ts index 24d56c4d27ad..0edf2b7ebbf4 100644 --- a/packages/cli/test/run/noninteractive.test.ts +++ b/packages/cli/test/run/noninteractive.test.ts @@ -253,7 +253,7 @@ async function run(input: { ) spyOn(sdk.session.form, "cancel").mockImplementation((request) => (input.cancel?.(request) ?? ok(undefined)) as never) let promptID = "msg_prompt" - spyOn(sdk.session, "wait").mockImplementation(() => input.wait?.() ?? wait.promise) + spyOn(sdk.experimental.session, "wait").mockImplementation(() => input.wait?.() ?? wait.promise) spyOn(sdk.message, "list").mockImplementation(() => ok({ data: input.messages?.(promptID) ?? [{ id: promptID, type: "user", text: "hello", time: { created: 1 } }], @@ -479,7 +479,7 @@ describe("runNonInteractivePrompt", () => { ]) expect(output.stderr).toBe("") const sdk = await run({ compatibility: "v1", turn: (messageID) => [prompted(messageID), settled()] }) - expect(sdk.session.wait).not.toHaveBeenCalled() + expect(sdk.experimental.session.wait).not.toHaveBeenCalled() expect(sdk.message.list).not.toHaveBeenCalled() }) diff --git a/packages/client/script/build.ts b/packages/client/script/build.ts index 5068e32f1fa8..ac25359525e3 100644 --- a/packages/client/script/build.ts +++ b/packages/client/script/build.ts @@ -1,6 +1,6 @@ import { NodeFileSystem } from "@effect/platform-node" import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode/httpapi-codegen" -import { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode/protocol/client" +import { ClientApi, clientNamespace, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode/protocol/client" import { Agent } from "@opencode/schema/agent" import { Command } from "@opencode/schema/command" import { Config } from "@opencode/schema/config" @@ -43,8 +43,8 @@ import { Workspace } from "@opencode/schema/workspace" import { Effect, Schema } from "effect" import { fileURLToPath } from "url" -const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints }) -const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints }) +const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints, namespace: clientNamespace }) +const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints, namespace: clientNamespace }) const effectTypeReferences = [ ...namespaceTypes("Agent", "@opencode/schema/agent", Agent), ...namespaceTypes("Command", "@opencode/schema/command", Command), diff --git a/packages/client/src/contract.ts b/packages/client/src/contract.ts index aa033158041c..b5326bfa00d3 100644 --- a/packages/client/src/contract.ts +++ b/packages/client/src/contract.ts @@ -1 +1 @@ -export { ClientApi, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode/protocol/client" +export { ClientApi, clientNamespace, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode/protocol/client" diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index ebf112861692..a37159676a71 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -7,16 +7,17 @@ import type { Session } from "@opencode/schema/session" import type { AbsolutePath } from "@opencode/schema/schema" import type { Project } from "@opencode/schema/project" import type { RelativePath } from "@opencode/schema/schema" -import type { Money } from "@opencode/schema/money" import type { Model } from "@opencode/schema/model" -import type { DateTime } from "effect" import type { Permission } from "@opencode/schema/permission" import type { SessionMessage } from "@opencode/schema/session-message" import type { SessionInbox } from "@opencode/schema/session-inbox" import type { PromptInput } from "@opencode/schema/prompt-input" import type { AgentAttachment } from "@opencode/schema/prompt" -import type { Skill } from "@opencode/schema/skill" import type { FileDiff } from "@opencode/schema/file-diff" +import type { Form } from "@opencode/schema/form" +import type { DateTime } from "effect" +import type { Money } from "@opencode/schema/money" +import type { Skill } from "@opencode/schema/skill" import type { InstructionEntry } from "@opencode/schema/instruction-entry" import type { Schema } from "effect" import type { Event } from "@opencode/schema/event" @@ -25,7 +26,6 @@ import type { Workspace } from "@opencode/schema/workspace" import type { Instruction } from "@opencode/schema/instruction" import type { Snapshot } from "@opencode/schema/snapshot" import type { Shell } from "@opencode/schema/shell" -import type { Form } from "@opencode/schema/form" import type { Provider } from "@opencode/schema/provider" import type { Integration } from "@opencode/schema/integration" import type { Mcp } from "@opencode/schema/mcp" @@ -137,71 +137,6 @@ export type SessionListOutput = { } export type SessionListOperation = (input?: SessionListInput) => Effect.Effect -export type SessionStatsInput = { - readonly from?: number | undefined - readonly to?: number | undefined - readonly project?: Project.ID | undefined - readonly timezone?: string | undefined - readonly tools?: "none" | "summary" | "detail" | undefined -} -export type SessionStatsOutput = { - readonly range: { readonly from: DateTime.Utc; readonly to: DateTime.Utc } - readonly sessions: number - readonly subagents: number - readonly prompts: number - readonly steps: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly cost: Money.USD - readonly tools: - | { readonly mode: "none" } - | { - readonly mode: "summary" - readonly totals: { - readonly calls: number - readonly succeeded: number - readonly failed: number - readonly unfinished: number - } - } - | { - readonly mode: "detail" - readonly totals: { - readonly calls: number - readonly succeeded: number - readonly failed: number - readonly unfinished: number - } - readonly usage: ReadonlyArray<{ - readonly name: string - readonly calls: number - readonly succeeded: number - readonly failed: number - readonly unfinished: number - readonly durationP50?: number | undefined - }> - } - readonly activeDays: number - readonly streak: number - readonly activity: ReadonlyArray<{ readonly date: string; readonly steps: number }> - readonly models: ReadonlyArray<{ - readonly model: Model.Ref - readonly steps: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly cost: Money.USD - }> -} -export type SessionStatsOperation = (input?: SessionStatsInput) => Effect.Effect - export type SessionCreateInput = { readonly id?: Session.ID | undefined readonly parentID?: Session.ID | undefined @@ -215,18 +150,6 @@ export type SessionCreateInput = { export type SessionCreateOutput = Session.Info export type SessionCreateOperation = (input?: SessionCreateInput) => Effect.Effect -export type SessionImportInput = { - readonly info: Session.Info - readonly messages: ReadonlyArray - readonly location?: Location.PublicRef | undefined -} -export type SessionImportOutput = Session.Info -export type SessionImportOperation = (input: SessionImportInput) => Effect.Effect - -export type SessionExportInput = { readonly sessionID: Session.ID; readonly sanitize?: boolean | undefined } -export type SessionExportOutput = { readonly info: Session.Info; readonly messages: ReadonlyArray } -export type SessionExportOperation = (input: SessionExportInput) => Effect.Effect - export type SessionActiveOutput = { readonly [x: Session.ID]: { readonly type: "running" } } export type SessionActiveOperation = () => Effect.Effect @@ -298,14 +221,6 @@ export type SessionCommandInput = { export type SessionCommandOutput = void export type SessionCommandOperation = (input: SessionCommandInput) => Effect.Effect -export type SessionSkillInput = { - readonly sessionID: Session.ID - readonly id: Skill.ID - readonly resume?: boolean | undefined -} -export type SessionSkillOutput = void -export type SessionSkillOperation = (input: SessionSkillInput) => Effect.Effect - export type SessionSyntheticInput = { readonly sessionID: Session.ID readonly id?: SessionMessage.ID | undefined @@ -336,10 +251,6 @@ export type SessionCompactInput = { export type SessionCompactOutput = SessionInbox.Compaction export type SessionCompactOperation = (input: SessionCompactInput) => Effect.Effect -export type SessionWaitInput = { readonly sessionID: Session.ID } -export type SessionWaitOutput = void -export type SessionWaitOperation = (input: SessionWaitInput) => Effect.Effect - export type SessionRevertStageInput = { readonly sessionID: Session.ID readonly messageID: SessionMessage.ID @@ -397,40 +308,261 @@ export type SessionInboxUpdateOperation = ( input: SessionInboxUpdateInput, ) => Effect.Effect -export type SessionInstructionsEntryListInput = { readonly sessionID: Session.ID } -export type SessionInstructionsEntryListOutput = ReadonlyArray -export type SessionInstructionsEntryListOperation = ( - input: SessionInstructionsEntryListInput, -) => Effect.Effect +export type SessionGenerateInput = { readonly sessionID: Session.ID; readonly prompt: string } +export type SessionGenerateOutput = { readonly text: string } +export type SessionGenerateOperation = ( + input: SessionGenerateInput, +) => Effect.Effect + +export type SessionInterruptInput = { readonly sessionID: Session.ID; readonly resume?: boolean | undefined } +export type SessionInterruptOutput = { readonly interrupted: boolean } +export type SessionInterruptOperation = ( + input: SessionInterruptInput, +) => Effect.Effect + +export type SessionBackgroundInput = { readonly sessionID: Session.ID } +export type SessionBackgroundOutput = void +export type SessionBackgroundOperation = ( + input: SessionBackgroundInput, +) => Effect.Effect + +export type SessionMessageGetInput = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } +export type SessionMessageGetOutput = SessionMessage.Info +export type SessionMessageGetOperation = ( + input: SessionMessageGetInput, +) => Effect.Effect + +export type SessionFormListInput = { readonly sessionID: string } +export type SessionFormListOutput = ReadonlyArray +export type SessionFormListOperation = ( + input: SessionFormListInput, +) => Effect.Effect + +export type SessionFormCreateInput = { + readonly sessionID: string + readonly id?: Form.ID | undefined + readonly title: string + readonly metadata?: Form.Metadata | undefined + readonly fields: Form.Fields +} +export type SessionFormCreateOutput = Form.Info +export type SessionFormCreateOperation = ( + input: SessionFormCreateInput, +) => Effect.Effect + +export type SessionFormGetInput = { readonly sessionID: string; readonly formID: Form.ID } +export type SessionFormGetOutput = Form.Detail +export type SessionFormGetOperation = (input: SessionFormGetInput) => Effect.Effect + +export type SessionFormReplyInput = { + readonly sessionID: string + readonly formID: Form.ID + readonly answer: Form.Answer +} +export type SessionFormReplyOutput = void +export type SessionFormReplyOperation = ( + input: SessionFormReplyInput, +) => Effect.Effect + +export type SessionFormCancelInput = { + readonly sessionID: string + readonly formID: Form.ID + readonly message?: string | undefined +} +export type SessionFormCancelOutput = void +export type SessionFormCancelOperation = ( + input: SessionFormCancelInput, +) => Effect.Effect + +export type SessionEnvironmentInput = { + readonly sessionID: Session.ID + readonly variables: { readonly [x: string]: string } +} +export type SessionEnvironmentOutput = void +export type SessionEnvironmentOperation = ( + input: SessionEnvironmentInput, +) => Effect.Effect + +export type SessionViewInput = { readonly sessionID: Session.ID; readonly idle: DateTime.Utc } +export type SessionViewOutput = void +export type SessionViewOperation = (input: SessionViewInput) => Effect.Effect + +export interface SessionApi { + readonly list: SessionListOperation + readonly create: SessionCreateOperation + readonly active: SessionActiveOperation + readonly get: SessionGetOperation + readonly remove: SessionRemoveOperation + readonly fork: SessionForkOperation + readonly switchAgent: SessionSwitchAgentOperation + readonly switchModel: SessionSwitchModelOperation + readonly update: SessionUpdateOperation + readonly move: SessionMoveOperation + readonly prompt: SessionPromptOperation + readonly command: SessionCommandOperation + readonly synthetic: SessionSyntheticOperation + readonly shell: SessionShellOperation + readonly compact: SessionCompactOperation + readonly revert: { + readonly stage: SessionRevertStageOperation + readonly clear: SessionRevertClearOperation + readonly commit: SessionRevertCommitOperation + } + readonly context: SessionContextOperation + readonly diff: SessionDiffOperation + readonly inbox: { + readonly list: SessionInboxListOperation + readonly cancel: SessionInboxCancelOperation + readonly update: SessionInboxUpdateOperation + } + readonly generate: SessionGenerateOperation + readonly interrupt: SessionInterruptOperation + readonly background: SessionBackgroundOperation + readonly message: { readonly get: SessionMessageGetOperation } + readonly form: { + readonly list: SessionFormListOperation + readonly create: SessionFormCreateOperation + readonly get: SessionFormGetOperation + readonly reply: SessionFormReplyOperation + readonly cancel: SessionFormCancelOperation + } + readonly environment: SessionEnvironmentOperation + readonly view: SessionViewOperation +} + +export type ExperimentalSessionStatsInput = { + readonly from?: number | undefined + readonly to?: number | undefined + readonly project?: Project.ID | undefined + readonly timezone?: string | undefined + readonly tools?: "none" | "summary" | "detail" | undefined +} +export type ExperimentalSessionStatsOutput = { + readonly range: { readonly from: DateTime.Utc; readonly to: DateTime.Utc } + readonly sessions: number + readonly subagents: number + readonly prompts: number + readonly steps: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly cost: Money.USD + readonly tools: + | { readonly mode: "none" } + | { + readonly mode: "summary" + readonly totals: { + readonly calls: number + readonly succeeded: number + readonly failed: number + readonly unfinished: number + } + } + | { + readonly mode: "detail" + readonly totals: { + readonly calls: number + readonly succeeded: number + readonly failed: number + readonly unfinished: number + } + readonly usage: ReadonlyArray<{ + readonly name: string + readonly calls: number + readonly succeeded: number + readonly failed: number + readonly unfinished: number + readonly durationP50?: number | undefined + }> + } + readonly activeDays: number + readonly streak: number + readonly activity: ReadonlyArray<{ readonly date: string; readonly steps: number }> + readonly models: ReadonlyArray<{ + readonly model: Model.Ref + readonly steps: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly cost: Money.USD + }> +} +export type ExperimentalSessionStatsOperation = ( + input?: ExperimentalSessionStatsInput, +) => Effect.Effect + +export type ExperimentalSessionImportInput = { + readonly info: Session.Info + readonly messages: ReadonlyArray + readonly location?: Location.PublicRef | undefined +} +export type ExperimentalSessionImportOutput = Session.Info +export type ExperimentalSessionImportOperation = ( + input: ExperimentalSessionImportInput, +) => Effect.Effect + +export type ExperimentalSessionExportInput = { readonly sessionID: Session.ID; readonly sanitize?: boolean | undefined } +export type ExperimentalSessionExportOutput = { + readonly info: Session.Info + readonly messages: ReadonlyArray +} +export type ExperimentalSessionExportOperation = ( + input: ExperimentalSessionExportInput, +) => Effect.Effect + +export type ExperimentalSessionSkillInput = { + readonly sessionID: Session.ID + readonly id: Skill.ID + readonly resume?: boolean | undefined +} +export type ExperimentalSessionSkillOutput = void +export type ExperimentalSessionSkillOperation = ( + input: ExperimentalSessionSkillInput, +) => Effect.Effect + +export type ExperimentalSessionWaitInput = { readonly sessionID: Session.ID } +export type ExperimentalSessionWaitOutput = void +export type ExperimentalSessionWaitOperation = ( + input: ExperimentalSessionWaitInput, +) => Effect.Effect -export type SessionInstructionsEntryPutInput = { +export type ExperimentalSessionInstructionsEntryListInput = { readonly sessionID: Session.ID } +export type ExperimentalSessionInstructionsEntryListOutput = ReadonlyArray +export type ExperimentalSessionInstructionsEntryListOperation = ( + input: ExperimentalSessionInstructionsEntryListInput, +) => Effect.Effect + +export type ExperimentalSessionInstructionsEntryPutInput = { readonly sessionID: Session.ID readonly key: InstructionEntry.Key readonly value: Schema.Json } -export type SessionInstructionsEntryPutOutput = void -export type SessionInstructionsEntryPutOperation = ( - input: SessionInstructionsEntryPutInput, -) => Effect.Effect - -export type SessionInstructionsEntryRemoveInput = { readonly sessionID: Session.ID; readonly key: InstructionEntry.Key } -export type SessionInstructionsEntryRemoveOutput = void -export type SessionInstructionsEntryRemoveOperation = ( - input: SessionInstructionsEntryRemoveInput, -) => Effect.Effect +export type ExperimentalSessionInstructionsEntryPutOutput = void +export type ExperimentalSessionInstructionsEntryPutOperation = ( + input: ExperimentalSessionInstructionsEntryPutInput, +) => Effect.Effect -export type SessionGenerateInput = { readonly sessionID: Session.ID; readonly prompt: string } -export type SessionGenerateOutput = { readonly text: string } -export type SessionGenerateOperation = ( - input: SessionGenerateInput, -) => Effect.Effect +export type ExperimentalSessionInstructionsEntryRemoveInput = { + readonly sessionID: Session.ID + readonly key: InstructionEntry.Key +} +export type ExperimentalSessionInstructionsEntryRemoveOutput = void +export type ExperimentalSessionInstructionsEntryRemoveOperation = ( + input: ExperimentalSessionInstructionsEntryRemoveInput, +) => Effect.Effect -export type SessionLogInput = { +export type ExperimentalSessionLogInput = { readonly sessionID: Session.ID readonly after?: Event.Seq | undefined readonly follow?: boolean | undefined } -export type SessionLogOutput = +export type ExperimentalSessionLogOutput = | ( | { readonly id: Event.ID @@ -1238,135 +1370,24 @@ export type SessionLogOutput = } ) | EventLog.Synced -export type SessionLogOperation = (input: SessionLogInput) => Stream.Stream - -export type SessionInterruptInput = { readonly sessionID: Session.ID; readonly resume?: boolean | undefined } -export type SessionInterruptOutput = { readonly interrupted: boolean } -export type SessionInterruptOperation = ( - input: SessionInterruptInput, -) => Effect.Effect - -export type SessionBackgroundInput = { readonly sessionID: Session.ID } -export type SessionBackgroundOutput = void -export type SessionBackgroundOperation = ( - input: SessionBackgroundInput, -) => Effect.Effect - -export type SessionMessageGetInput = { readonly sessionID: Session.ID; readonly messageID: SessionMessage.ID } -export type SessionMessageGetOutput = SessionMessage.Info -export type SessionMessageGetOperation = ( - input: SessionMessageGetInput, -) => Effect.Effect - -export type SessionFormListInput = { readonly sessionID: string } -export type SessionFormListOutput = ReadonlyArray -export type SessionFormListOperation = ( - input: SessionFormListInput, -) => Effect.Effect - -export type SessionFormCreateInput = { - readonly sessionID: string - readonly id?: Form.ID | undefined - readonly title: string - readonly metadata?: Form.Metadata | undefined - readonly fields: Form.Fields -} -export type SessionFormCreateOutput = Form.Info -export type SessionFormCreateOperation = ( - input: SessionFormCreateInput, -) => Effect.Effect - -export type SessionFormGetInput = { readonly sessionID: string; readonly formID: Form.ID } -export type SessionFormGetOutput = Form.Detail -export type SessionFormGetOperation = (input: SessionFormGetInput) => Effect.Effect - -export type SessionFormReplyInput = { - readonly sessionID: string - readonly formID: Form.ID - readonly answer: Form.Answer -} -export type SessionFormReplyOutput = void -export type SessionFormReplyOperation = ( - input: SessionFormReplyInput, -) => Effect.Effect - -export type SessionFormCancelInput = { - readonly sessionID: string - readonly formID: Form.ID - readonly message?: string | undefined -} -export type SessionFormCancelOutput = void -export type SessionFormCancelOperation = ( - input: SessionFormCancelInput, -) => Effect.Effect - -export type SessionEnvironmentInput = { - readonly sessionID: Session.ID - readonly variables: { readonly [x: string]: string } -} -export type SessionEnvironmentOutput = void -export type SessionEnvironmentOperation = ( - input: SessionEnvironmentInput, -) => Effect.Effect - -export type SessionViewInput = { readonly sessionID: Session.ID; readonly idle: DateTime.Utc } -export type SessionViewOutput = void -export type SessionViewOperation = (input: SessionViewInput) => Effect.Effect - -export interface SessionApi { - readonly list: SessionListOperation - readonly stats: SessionStatsOperation - readonly create: SessionCreateOperation - readonly import: SessionImportOperation - readonly export: SessionExportOperation - readonly active: SessionActiveOperation - readonly get: SessionGetOperation - readonly remove: SessionRemoveOperation - readonly fork: SessionForkOperation - readonly switchAgent: SessionSwitchAgentOperation - readonly switchModel: SessionSwitchModelOperation - readonly update: SessionUpdateOperation - readonly move: SessionMoveOperation - readonly prompt: SessionPromptOperation - readonly command: SessionCommandOperation - readonly skill: SessionSkillOperation - readonly synthetic: SessionSyntheticOperation - readonly shell: SessionShellOperation - readonly compact: SessionCompactOperation - readonly wait: SessionWaitOperation - readonly revert: { - readonly stage: SessionRevertStageOperation - readonly clear: SessionRevertClearOperation - readonly commit: SessionRevertCommitOperation - } - readonly context: SessionContextOperation - readonly diff: SessionDiffOperation - readonly inbox: { - readonly list: SessionInboxListOperation - readonly cancel: SessionInboxCancelOperation - readonly update: SessionInboxUpdateOperation - } +export type ExperimentalSessionLogOperation = ( + input: ExperimentalSessionLogInput, +) => Stream.Stream + +export interface ExperimentalSessionApi { + readonly stats: ExperimentalSessionStatsOperation + readonly import: ExperimentalSessionImportOperation + readonly export: ExperimentalSessionExportOperation + readonly skill: ExperimentalSessionSkillOperation + readonly wait: ExperimentalSessionWaitOperation readonly instructions: { readonly entry: { - readonly list: SessionInstructionsEntryListOperation - readonly put: SessionInstructionsEntryPutOperation - readonly remove: SessionInstructionsEntryRemoveOperation + readonly list: ExperimentalSessionInstructionsEntryListOperation + readonly put: ExperimentalSessionInstructionsEntryPutOperation + readonly remove: ExperimentalSessionInstructionsEntryRemoveOperation } } - readonly generate: SessionGenerateOperation - readonly log: SessionLogOperation - readonly interrupt: SessionInterruptOperation - readonly background: SessionBackgroundOperation - readonly message: { readonly get: SessionMessageGetOperation } - readonly form: { - readonly list: SessionFormListOperation - readonly create: SessionFormCreateOperation - readonly get: SessionFormGetOperation - readonly reply: SessionFormReplyOperation - readonly cancel: SessionFormCancelOperation - } - readonly environment: SessionEnvironmentOperation - readonly view: SessionViewOperation + readonly log: ExperimentalSessionLogOperation } export type MessageListInput = { @@ -1411,15 +1432,17 @@ export interface ModelApi { readonly default: ModelDefaultOperation } -export type GenerateTextInput = { readonly prompt: string; readonly model?: Model.Ref | undefined } -export type GenerateTextOutput = { readonly text: string } -export type GenerateTextOperation = (input: GenerateTextInput) => Effect.Effect +export type ExperimentalGenerateTextInput = { readonly prompt: string; readonly model?: Model.Ref | undefined } +export type ExperimentalGenerateTextOutput = { readonly text: string } +export type ExperimentalGenerateTextOperation = ( + input: ExperimentalGenerateTextInput, +) => Effect.Effect -export interface GenerateApi { - readonly text: GenerateTextOperation +export interface ExperimentalGenerateApi { + readonly text: ExperimentalGenerateTextOperation } -export type VoiceModelsOutput = { +export type ExperimentalVoiceModelsOutput = { readonly models: ReadonlyArray<{ readonly model: string readonly provider: string @@ -1428,25 +1451,27 @@ export type VoiceModelsOutput = { }> readonly default?: string | undefined } -export type VoiceModelsOperation = () => Effect.Effect +export type ExperimentalVoiceModelsOperation = () => Effect.Effect -export type VoiceSessionInput = { +export type ExperimentalVoiceSessionInput = { readonly model: string readonly session?: { readonly [x: string]: unknown } | undefined readonly transport: | { readonly type: "webrtc"; readonly sdp: string; readonly dcid?: number | undefined } | { readonly type: "websocket" } } -export type VoiceSessionOutput = { +export type ExperimentalVoiceSessionOutput = { readonly transport: | { readonly type: "webrtc"; readonly sdp: string } | { readonly type: "websocket"; readonly url: string; readonly token: string; readonly expiresAt: number } } -export type VoiceSessionOperation = (input: VoiceSessionInput) => Effect.Effect +export type ExperimentalVoiceSessionOperation = ( + input: ExperimentalVoiceSessionInput, +) => Effect.Effect -export interface VoiceApi { - readonly models: VoiceModelsOperation - readonly session: VoiceSessionOperation +export interface ExperimentalVoiceApi { + readonly models: ExperimentalVoiceModelsOperation + readonly session: ExperimentalVoiceSessionOperation } export type ProviderListInput = { readonly location?: { readonly directory?: string | undefined } | undefined } @@ -1481,15 +1506,6 @@ export type IntegrationGetInput = { export type IntegrationGetOutput = { readonly location: Location.PublicRef; readonly data: Integration.Info } export type IntegrationGetOperation = (input: IntegrationGetInput) => Effect.Effect -export type IntegrationWellknownAddInput = { - readonly location?: { readonly directory?: string | undefined } | undefined - readonly url: string -} -export type IntegrationWellknownAddOutput = void -export type IntegrationWellknownAddOperation = ( - input: IntegrationWellknownAddInput, -) => Effect.Effect - export type IntegrationConnectKeyInput = { readonly integrationID: Integration.ID readonly location?: { readonly directory?: string | undefined } | undefined @@ -1603,7 +1619,6 @@ export type IntegrationCommandCancelOperation = ( export interface IntegrationApi { readonly list: IntegrationListOperation readonly get: IntegrationGetOperation - readonly wellknown: { readonly add: IntegrationWellknownAddOperation } readonly connect: { readonly key: IntegrationConnectKeyOperation readonly external: IntegrationConnectExternalOperation @@ -1621,61 +1636,90 @@ export interface IntegrationApi { } } +export type ExperimentalIntegrationWellknownAddInput = { + readonly location?: { readonly directory?: string | undefined } | undefined + readonly url: string +} +export type ExperimentalIntegrationWellknownAddOutput = void +export type ExperimentalIntegrationWellknownAddOperation = ( + input: ExperimentalIntegrationWellknownAddInput, +) => Effect.Effect + +export interface ExperimentalIntegrationApi { + readonly wellknown: { readonly add: ExperimentalIntegrationWellknownAddOperation } +} + export type McpListInput = { readonly location?: { readonly directory?: string | undefined } | undefined } export type McpListOutput = { readonly location: Location.PublicRef; readonly data: ReadonlyArray } export type McpListOperation = (input?: McpListInput) => Effect.Effect -export type McpAddInput = { +export type McpResourceCatalogInput = { readonly location?: { readonly directory?: string | undefined } | undefined } +export type McpResourceCatalogOutput = { readonly location: Location.PublicRef; readonly data: Mcp.ResourceCatalog } +export type McpResourceCatalogOperation = ( + input?: McpResourceCatalogInput, +) => Effect.Effect + +export interface McpApi { + readonly list: McpListOperation + readonly resource: { readonly catalog: McpResourceCatalogOperation } +} + +export type ExperimentalMcpAddInput = { readonly server: string readonly location?: { readonly directory?: string | undefined } | undefined readonly config: Mcp.LocalConfig | Mcp.RemoteConfig } -export type McpAddOutput = void -export type McpAddOperation = (input: McpAddInput) => Effect.Effect +export type ExperimentalMcpAddOutput = void +export type ExperimentalMcpAddOperation = ( + input: ExperimentalMcpAddInput, +) => Effect.Effect -export type McpRemoveInput = { +export type ExperimentalMcpRemoveInput = { readonly server: string readonly location?: { readonly directory?: string | undefined } | undefined } -export type McpRemoveOutput = void -export type McpRemoveOperation = (input: McpRemoveInput) => Effect.Effect +export type ExperimentalMcpRemoveOutput = void +export type ExperimentalMcpRemoveOperation = ( + input: ExperimentalMcpRemoveInput, +) => Effect.Effect -export type McpConnectInput = { +export type ExperimentalMcpConnectInput = { readonly server: string readonly location?: { readonly directory?: string | undefined } | undefined } -export type McpConnectOutput = void -export type McpConnectOperation = (input: McpConnectInput) => Effect.Effect +export type ExperimentalMcpConnectOutput = void +export type ExperimentalMcpConnectOperation = ( + input: ExperimentalMcpConnectInput, +) => Effect.Effect -export type McpDisconnectInput = { +export type ExperimentalMcpDisconnectInput = { readonly server: string readonly location?: { readonly directory?: string | undefined } | undefined } -export type McpDisconnectOutput = void -export type McpDisconnectOperation = (input: McpDisconnectInput) => Effect.Effect - -export type McpResourceCatalogInput = { readonly location?: { readonly directory?: string | undefined } | undefined } -export type McpResourceCatalogOutput = { readonly location: Location.PublicRef; readonly data: Mcp.ResourceCatalog } -export type McpResourceCatalogOperation = ( - input?: McpResourceCatalogInput, -) => Effect.Effect +export type ExperimentalMcpDisconnectOutput = void +export type ExperimentalMcpDisconnectOperation = ( + input: ExperimentalMcpDisconnectInput, +) => Effect.Effect -export type McpClientTokenInput = { +export type ExperimentalMcpClientTokenInput = { readonly location?: { readonly directory?: string | undefined } | undefined readonly name: string readonly "x-opencode-ticket"?: string | undefined } -export type McpClientTokenOutput = { readonly location: Location.PublicRef; readonly data: PtyTicket.ConnectToken } -export type McpClientTokenOperation = (input: McpClientTokenInput) => Effect.Effect +export type ExperimentalMcpClientTokenOutput = { + readonly location: Location.PublicRef + readonly data: PtyTicket.ConnectToken +} +export type ExperimentalMcpClientTokenOperation = ( + input: ExperimentalMcpClientTokenInput, +) => Effect.Effect -export interface McpApi { - readonly list: McpListOperation - readonly add: McpAddOperation - readonly remove: McpRemoveOperation - readonly connect: McpConnectOperation - readonly disconnect: McpDisconnectOperation - readonly resource: { readonly catalog: McpResourceCatalogOperation } - readonly client: { readonly token: McpClientTokenOperation } +export interface ExperimentalMcpApi { + readonly add: ExperimentalMcpAddOperation + readonly remove: ExperimentalMcpRemoveOperation + readonly connect: ExperimentalMcpConnectOperation + readonly disconnect: ExperimentalMcpDisconnectOperation + readonly client: { readonly token: ExperimentalMcpClientTokenOperation } } export type CredentialListOutput = ReadonlyArray @@ -2288,7 +2332,7 @@ export interface DebugApi { readonly location: { readonly list: DebugLocationListOperation; readonly evict: DebugLocationEvictOperation } } -export type MigrationV1StatusOutput = +export type ExperimentalMigrationV1StatusOutput = | { readonly status: "required" | "completed" } | { readonly status: "running" @@ -2299,10 +2343,13 @@ export type MigrationV1StatusOutput = } } | { readonly status: "error"; readonly error: string } -export type MigrationV1StatusOperation = () => Effect.Effect +export type ExperimentalMigrationV1StatusOperation = () => Effect.Effect< + ExperimentalMigrationV1StatusOutput, + E +> -export interface MigrationApi { - readonly v1: { readonly status: MigrationV1StatusOperation } +export interface ExperimentalMigrationApi { + readonly v1: { readonly status: ExperimentalMigrationV1StatusOperation } } export type WebsearchProvidersInput = { readonly location?: { readonly directory?: string | undefined } | undefined } @@ -2327,12 +2374,19 @@ export interface WebsearchApi { readonly query: WebsearchQueryOperation } -export type UsageLimitsInput = { readonly location?: { readonly directory?: string | undefined } | undefined } -export type UsageLimitsOutput = { readonly location: Location.PublicRef; readonly data: UsageLimits.Info | null } -export type UsageLimitsOperation = (input?: UsageLimitsInput) => Effect.Effect +export type ExperimentalUsageLimitsInput = { + readonly location?: { readonly directory?: string | undefined } | undefined +} +export type ExperimentalUsageLimitsOutput = { + readonly location: Location.PublicRef + readonly data: UsageLimits.Info | null +} +export type ExperimentalUsageLimitsOperation = ( + input?: ExperimentalUsageLimitsInput, +) => Effect.Effect -export interface UsageApi { - readonly limits: UsageLimitsOperation +export interface ExperimentalUsageApi { + readonly limits: ExperimentalUsageLimitsOperation } export type ConfigGetInput = { readonly location?: { readonly directory?: string | undefined } | undefined } @@ -2346,14 +2400,19 @@ export type ConfigShellsOutput = ReadonlyArray<{ }> export type ConfigShellsOperation = () => Effect.Effect -export type ConfigUpdateInput = { readonly shell: string | null } -export type ConfigUpdateOutput = void -export type ConfigUpdateOperation = (input: ConfigUpdateInput) => Effect.Effect - export interface ConfigApi { readonly get: ConfigGetOperation readonly shells: ConfigShellsOperation - readonly update: ConfigUpdateOperation +} + +export type ExperimentalConfigUpdateInput = { readonly shell: string | null } +export type ExperimentalConfigUpdateOutput = void +export type ExperimentalConfigUpdateOperation = ( + input: ExperimentalConfigUpdateInput, +) => Effect.Effect + +export interface ExperimentalConfigApi { + readonly update: ExperimentalConfigUpdateOperation } export interface AppApi { @@ -2362,10 +2421,18 @@ export interface AppApi { readonly agent: AgentApi readonly plugin: PluginApi readonly session: SessionApi + readonly experimental: ExperimentalApi & { + readonly session: ExperimentalSessionApi + readonly generate: ExperimentalGenerateApi + readonly voice: ExperimentalVoiceApi + readonly integration: ExperimentalIntegrationApi + readonly mcp: ExperimentalMcpApi + readonly migration: ExperimentalMigrationApi + readonly usage: ExperimentalUsageApi + readonly config: ExperimentalConfigApi + } readonly message: MessageApi readonly model: ModelApi - readonly generate: GenerateApi - readonly voice: VoiceApi readonly provider: ProviderApi readonly integration: IntegrationApi readonly mcp: McpApi @@ -2379,14 +2446,11 @@ export interface AppApi { readonly rpc: RpcApi readonly event: EventApi readonly pty: PtyApi - readonly experimental: ExperimentalApi readonly shell: ShellApi readonly reference: ReferenceApi readonly worktree: WorktreeApi readonly vcs: VcsApi readonly debug: DebugApi - readonly migration: MigrationApi readonly websearch: WebsearchApi - readonly usage: UsageApi readonly config: ConfigApi } diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 69c1136c0dac..7f529eb2ba13 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -24,14 +24,8 @@ import type { PluginUpdateOutput, SessionListInput, SessionListOutput, - SessionStatsInput, - SessionStatsOutput, SessionCreateInput, SessionCreateOutput, - SessionImportInput, - SessionImportOutput, - SessionExportInput, - SessionExportOutput, SessionActiveOutput, SessionGetInput, SessionGetOutput, @@ -51,16 +45,12 @@ import type { SessionPromptOutput, SessionCommandInput, SessionCommandOutput, - SessionSkillInput, - SessionSkillOutput, SessionSyntheticInput, SessionSyntheticOutput, SessionShellInput, SessionShellOutput, SessionCompactInput, SessionCompactOutput, - SessionWaitInput, - SessionWaitOutput, SessionRevertStageInput, SessionRevertStageOutput, SessionRevertClearInput, @@ -77,16 +67,8 @@ import type { SessionInboxCancelOutput, SessionInboxUpdateInput, SessionInboxUpdateOutput, - SessionInstructionsEntryListInput, - SessionInstructionsEntryListOutput, - SessionInstructionsEntryPutInput, - SessionInstructionsEntryPutOutput, - SessionInstructionsEntryRemoveInput, - SessionInstructionsEntryRemoveOutput, SessionGenerateInput, SessionGenerateOutput, - SessionLogInput, - SessionLogOutput, SessionInterruptInput, SessionInterruptOutput, SessionBackgroundInput, @@ -107,17 +89,35 @@ import type { SessionEnvironmentOutput, SessionViewInput, SessionViewOutput, + ExperimentalSessionStatsInput, + ExperimentalSessionStatsOutput, + ExperimentalSessionImportInput, + ExperimentalSessionImportOutput, + ExperimentalSessionExportInput, + ExperimentalSessionExportOutput, + ExperimentalSessionSkillInput, + ExperimentalSessionSkillOutput, + ExperimentalSessionWaitInput, + ExperimentalSessionWaitOutput, + ExperimentalSessionInstructionsEntryListInput, + ExperimentalSessionInstructionsEntryListOutput, + ExperimentalSessionInstructionsEntryPutInput, + ExperimentalSessionInstructionsEntryPutOutput, + ExperimentalSessionInstructionsEntryRemoveInput, + ExperimentalSessionInstructionsEntryRemoveOutput, + ExperimentalSessionLogInput, + ExperimentalSessionLogOutput, MessageListInput, MessageListOutput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, - GenerateTextInput, - GenerateTextOutput, - VoiceModelsOutput, - VoiceSessionInput, - VoiceSessionOutput, + ExperimentalGenerateTextInput, + ExperimentalGenerateTextOutput, + ExperimentalVoiceModelsOutput, + ExperimentalVoiceSessionInput, + ExperimentalVoiceSessionOutput, ProviderListInput, ProviderListOutput, ProviderGetInput, @@ -126,8 +126,6 @@ import type { IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, - IntegrationWellknownAddInput, - IntegrationWellknownAddOutput, IntegrationConnectKeyInput, IntegrationConnectKeyOutput, IntegrationConnectExternalInput, @@ -146,20 +144,22 @@ import type { IntegrationCommandStatusOutput, IntegrationCommandCancelInput, IntegrationCommandCancelOutput, + ExperimentalIntegrationWellknownAddInput, + ExperimentalIntegrationWellknownAddOutput, McpListInput, McpListOutput, - McpAddInput, - McpAddOutput, - McpRemoveInput, - McpRemoveOutput, - McpConnectInput, - McpConnectOutput, - McpDisconnectInput, - McpDisconnectOutput, McpResourceCatalogInput, McpResourceCatalogOutput, - McpClientTokenInput, - McpClientTokenOutput, + ExperimentalMcpAddInput, + ExperimentalMcpAddOutput, + ExperimentalMcpRemoveInput, + ExperimentalMcpRemoveOutput, + ExperimentalMcpConnectInput, + ExperimentalMcpConnectOutput, + ExperimentalMcpDisconnectInput, + ExperimentalMcpDisconnectOutput, + ExperimentalMcpClientTokenInput, + ExperimentalMcpClientTokenOutput, CredentialListOutput, CredentialCreateInput, CredentialCreateOutput, @@ -269,18 +269,18 @@ import type { DebugLocationListOutput, DebugLocationEvictInput, DebugLocationEvictOutput, - MigrationV1StatusOutput, + ExperimentalMigrationV1StatusOutput, WebsearchProvidersInput, WebsearchProvidersOutput, WebsearchQueryInput, WebsearchQueryOutput, - UsageLimitsInput, - UsageLimitsOutput, + ExperimentalUsageLimitsInput, + ExperimentalUsageLimitsOutput, ConfigGetInput, ConfigGetOutput, ConfigShellsOutput, - ConfigUpdateInput, - ConfigUpdateOutput, + ExperimentalConfigUpdateInput, + ExperimentalConfigUpdateOutput, } from "../api/api.js" import { ClientError } from "./client-error.js" @@ -399,22 +399,6 @@ const EndpointSessionList = (raw: RawClient["server.session"]) => (input?: Sessi }).pipe(Effect.mapError(mapClientError)), ) -const EndpointSessionStats = (raw: RawClient["server.session"]) => (input?: SessionStatsInput) => - preserveEffect()( - raw["session.stats"]({ - query: { - from: input?.["from"], - to: input?.["to"], - project: input?.["project"], - timezone: input?.["timezone"], - tools: input?.["tools"], - }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) - const EndpointSessionCreate = (raw: RawClient["server.session"]) => (input?: SessionCreateInput) => preserveEffect()( raw["session.create"]({ @@ -434,24 +418,6 @@ const EndpointSessionCreate = (raw: RawClient["server.session"]) => (input?: Ses ), ) -const EndpointSessionImport = (raw: RawClient["server.session"]) => (input: SessionImportInput) => - preserveEffect()( - raw["session.import"]({ - payload: { info: input["info"], messages: input["messages"], location: input["location"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) - -const EndpointSessionExport = (raw: RawClient["server.session"]) => (input: SessionExportInput) => - preserveEffect()( - raw["session.export"]({ params: { sessionID: input["sessionID"] }, query: { sanitize: input["sanitize"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) - const EndpointSessionActive = (raw: RawClient["server.session"]) => () => preserveEffect()( raw["session.active"]({}).pipe( @@ -551,14 +517,6 @@ const EndpointSessionCommand = (raw: RawClient["server.session"]) => (input: Ses }).pipe(Effect.mapError(mapClientError)), ) -const EndpointSessionSkill = (raw: RawClient["server.session"]) => (input: SessionSkillInput) => - preserveEffect()( - raw["session.skill"]({ - params: { sessionID: input["sessionID"] }, - payload: { id: input["id"], resume: input["resume"] }, - }).pipe(Effect.mapError(mapClientError)), - ) - const EndpointSessionSynthetic = (raw: RawClient["server.session"]) => (input: SessionSyntheticInput) => preserveEffect()( raw["session.synthetic"]({ @@ -596,11 +554,6 @@ const EndpointSessionCompact = (raw: RawClient["server.session"]) => (input: Ses ), ) -const EndpointSessionWait = (raw: RawClient["server.session"]) => (input: SessionWaitInput) => - preserveEffect()( - raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), - ) - const EndpointSessionRevertStage = (raw: RawClient["server.session"]) => (input: SessionRevertStageInput) => preserveEffect()( raw["session.revert.stage"]({ @@ -664,32 +617,6 @@ const EndpointSessionInboxUpdate = (raw: RawClient["server.session"]) => (input: }).pipe(Effect.mapError(mapClientError)), ) -const EndpointSessionInstructionsEntryList = - (raw: RawClient["server.session"]) => (input: SessionInstructionsEntryListInput) => - preserveEffect()( - raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) - -const EndpointSessionInstructionsEntryPut = - (raw: RawClient["server.session"]) => (input: SessionInstructionsEntryPutInput) => - preserveEffect()( - raw["session.instructions.entry.put"]({ - params: { sessionID: input["sessionID"], key: input["key"] }, - payload: { value: input["value"] }, - }).pipe(Effect.mapError(mapClientError)), - ) - -const EndpointSessionInstructionsEntryRemove = - (raw: RawClient["server.session"]) => (input: SessionInstructionsEntryRemoveInput) => - preserveEffect()( - raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( - Effect.mapError(mapClientError), - ), - ) - const EndpointSessionGenerate = (raw: RawClient["server.session"]) => (input: SessionGenerateInput) => preserveEffect()( raw["session.generate"]({ params: { sessionID: input["sessionID"] }, payload: { prompt: input["prompt"] } }).pipe( @@ -698,19 +625,6 @@ const EndpointSessionGenerate = (raw: RawClient["server.session"]) => (input: Se ), ) -const EndpointSessionLog = (raw: RawClient["server.session"]) => (input: SessionLogInput) => - preserveStream()( - Stream.unwrap( - raw["session.log"]({ - params: { sessionID: input["sessionID"] }, - query: { after: input["after"], follow: input["follow"] }, - }).pipe( - Effect.mapError(mapClientError), - Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), - ), - ), - ) - const EndpointSessionInterrupt = (raw: RawClient["server.session"]) => (input: SessionInterruptInput) => preserveEffect()( raw["session.interrupt"]({ params: { sessionID: input["sessionID"] }, query: { resume: input["resume"] } }).pipe( @@ -791,10 +705,7 @@ const EndpointSessionView = (raw: RawClient["server.session"]) => (input: Sessio const adaptGroupSession = (raw: RawClient["server.session"]) => ({ list: EndpointSessionList(raw), - stats: EndpointSessionStats(raw), create: EndpointSessionCreate(raw), - import: EndpointSessionImport(raw), - export: EndpointSessionExport(raw), active: EndpointSessionActive(raw), get: EndpointSessionGet(raw), remove: EndpointSessionRemove(raw), @@ -805,11 +716,9 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({ move: EndpointSessionMove(raw), prompt: EndpointSessionPrompt(raw), command: EndpointSessionCommand(raw), - skill: EndpointSessionSkill(raw), synthetic: EndpointSessionSynthetic(raw), shell: EndpointSessionShell(raw), compact: EndpointSessionCompact(raw), - wait: EndpointSessionWait(raw), revert: { stage: EndpointSessionRevertStage(raw), clear: EndpointSessionRevertClear(raw), @@ -822,15 +731,7 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({ cancel: EndpointSessionInboxCancel(raw), update: EndpointSessionInboxUpdate(raw), }, - instructions: { - entry: { - list: EndpointSessionInstructionsEntryList(raw), - put: EndpointSessionInstructionsEntryPut(raw), - remove: EndpointSessionInstructionsEntryRemove(raw), - }, - }, generate: EndpointSessionGenerate(raw), - log: EndpointSessionLog(raw), interrupt: EndpointSessionInterrupt(raw), background: EndpointSessionBackground(raw), message: { get: EndpointSessionMessageGet(raw) }, @@ -845,6 +746,111 @@ const adaptGroupSession = (raw: RawClient["server.session"]) => ({ view: EndpointSessionView(raw), }) +const EndpointExperimentalSessionStats = + (raw: RawClient["server.session"]) => (input?: ExperimentalSessionStatsInput) => + preserveEffect()( + raw["session.stats"]({ + query: { + from: input?.["from"], + to: input?.["to"], + project: input?.["project"], + timezone: input?.["timezone"], + tools: input?.["tools"], + }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const EndpointExperimentalSessionImport = + (raw: RawClient["server.session"]) => (input: ExperimentalSessionImportInput) => + preserveEffect()( + raw["session.import"]({ + payload: { info: input["info"], messages: input["messages"], location: input["location"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const EndpointExperimentalSessionExport = + (raw: RawClient["server.session"]) => (input: ExperimentalSessionExportInput) => + preserveEffect()( + raw["session.export"]({ params: { sessionID: input["sessionID"] }, query: { sanitize: input["sanitize"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const EndpointExperimentalSessionSkill = (raw: RawClient["server.session"]) => (input: ExperimentalSessionSkillInput) => + preserveEffect()( + raw["session.skill"]({ + params: { sessionID: input["sessionID"] }, + payload: { id: input["id"], resume: input["resume"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const EndpointExperimentalSessionWait = (raw: RawClient["server.session"]) => (input: ExperimentalSessionWaitInput) => + preserveEffect()( + raw["session.wait"]({ params: { sessionID: input["sessionID"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const EndpointExperimentalSessionInstructionsEntryList = + (raw: RawClient["server.session"]) => (input: ExperimentalSessionInstructionsEntryListInput) => + preserveEffect()( + raw["session.instructions.entry.list"]({ params: { sessionID: input["sessionID"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) + +const EndpointExperimentalSessionInstructionsEntryPut = + (raw: RawClient["server.session"]) => (input: ExperimentalSessionInstructionsEntryPutInput) => + preserveEffect()( + raw["session.instructions.entry.put"]({ + params: { sessionID: input["sessionID"], key: input["key"] }, + payload: { value: input["value"] }, + }).pipe(Effect.mapError(mapClientError)), + ) + +const EndpointExperimentalSessionInstructionsEntryRemove = + (raw: RawClient["server.session"]) => (input: ExperimentalSessionInstructionsEntryRemoveInput) => + preserveEffect()( + raw["session.instructions.entry.remove"]({ params: { sessionID: input["sessionID"], key: input["key"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) + +const EndpointExperimentalSessionLog = (raw: RawClient["server.session"]) => (input: ExperimentalSessionLogInput) => + preserveStream()( + Stream.unwrap( + raw["session.log"]({ + params: { sessionID: input["sessionID"] }, + query: { after: input["after"], follow: input["follow"] }, + }).pipe( + Effect.mapError(mapClientError), + Effect.map((stream) => stream.pipe(Stream.mapError(mapClientError))), + ), + ), + ) + +const adaptGroupExperimentalSession = (raw: RawClient["server.session"]) => ({ + stats: EndpointExperimentalSessionStats(raw), + import: EndpointExperimentalSessionImport(raw), + export: EndpointExperimentalSessionExport(raw), + skill: EndpointExperimentalSessionSkill(raw), + wait: EndpointExperimentalSessionWait(raw), + instructions: { + entry: { + list: EndpointExperimentalSessionInstructionsEntryList(raw), + put: EndpointExperimentalSessionInstructionsEntryPut(raw), + remove: EndpointExperimentalSessionInstructionsEntryRemove(raw), + }, + }, + log: EndpointExperimentalSessionLog(raw), +}) + const EndpointMessageList = (raw: RawClient["server.message"]) => (input: MessageListInput) => preserveEffect()( raw["session.messages"]({ @@ -870,26 +876,29 @@ const adaptGroupModel = (raw: RawClient["server.model"]) => ({ default: EndpointModelDefault(raw), }) -const EndpointGenerateText = (raw: RawClient["server.generate"]) => (input: GenerateTextInput) => - preserveEffect()( - raw["generate.text"]({ payload: { prompt: input["prompt"], model: input["model"] } }).pipe( - Effect.mapError(mapClientError), - Effect.map((value) => value.data), - ), - ) +const EndpointExperimentalGenerateText = + (raw: RawClient["server.generate"]) => (input: ExperimentalGenerateTextInput) => + preserveEffect()( + raw["generate.text"]({ payload: { prompt: input["prompt"], model: input["model"] } }).pipe( + Effect.mapError(mapClientError), + Effect.map((value) => value.data), + ), + ) -const adaptGroupGenerate = (raw: RawClient["server.generate"]) => ({ text: EndpointGenerateText(raw) }) +const adaptGroupExperimentalGenerate = (raw: RawClient["server.generate"]) => ({ + text: EndpointExperimentalGenerateText(raw), +}) -const EndpointVoiceModels = (raw: RawClient["server.voice"]) => () => - preserveEffect()( +const EndpointExperimentalVoiceModels = (raw: RawClient["server.voice"]) => () => + preserveEffect()( raw["voice.models"]({}).pipe( Effect.mapError(mapClientError), Effect.map((value) => value.data), ), ) -const EndpointVoiceSession = (raw: RawClient["server.voice"]) => (input: VoiceSessionInput) => - preserveEffect()( +const EndpointExperimentalVoiceSession = (raw: RawClient["server.voice"]) => (input: ExperimentalVoiceSessionInput) => + preserveEffect()( raw["voice.session"]({ payload: { model: input["model"], session: input["session"], transport: input["transport"] }, }).pipe( @@ -898,9 +907,9 @@ const EndpointVoiceSession = (raw: RawClient["server.voice"]) => (input: VoiceSe ), ) -const adaptGroupVoice = (raw: RawClient["server.voice"]) => ({ - models: EndpointVoiceModels(raw), - session: EndpointVoiceSession(raw), +const adaptGroupExperimentalVoice = (raw: RawClient["server.voice"]) => ({ + models: EndpointExperimentalVoiceModels(raw), + session: EndpointExperimentalVoiceSession(raw), }) const EndpointProviderList = (raw: RawClient["server.provider"]) => (input?: ProviderListInput) => @@ -933,14 +942,6 @@ const EndpointIntegrationGet = (raw: RawClient["server.integration"]) => (input: }).pipe(Effect.mapError(mapClientError)), ) -const EndpointIntegrationWellknownAdd = - (raw: RawClient["server.integration"]) => (input: IntegrationWellknownAddInput) => - preserveEffect()( - raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe( - Effect.mapError(mapClientError), - ), - ) - const EndpointIntegrationConnectKey = (raw: RawClient["server.integration"]) => (input: IntegrationConnectKeyInput) => preserveEffect()( raw["integration.connect.key"]({ @@ -1027,7 +1028,6 @@ const EndpointIntegrationCommandCancel = const adaptGroupIntegration = (raw: RawClient["server.integration"]) => ({ list: EndpointIntegrationList(raw), get: EndpointIntegrationGet(raw), - wellknown: { add: EndpointIntegrationWellknownAdd(raw) }, connect: { key: EndpointIntegrationConnectKey(raw), external: EndpointIntegrationConnectExternal(raw) }, oauth: { connect: EndpointIntegrationOauthConnect(raw), @@ -1042,13 +1042,35 @@ const adaptGroupIntegration = (raw: RawClient["server.integration"]) => ({ }, }) +const EndpointExperimentalIntegrationWellknownAdd = + (raw: RawClient["server.integration"]) => (input: ExperimentalIntegrationWellknownAddInput) => + preserveEffect()( + raw["integration.wellknown.add"]({ query: { location: input["location"] }, payload: { url: input["url"] } }).pipe( + Effect.mapError(mapClientError), + ), + ) + +const adaptGroupExperimentalIntegration = (raw: RawClient["server.integration"]) => ({ + wellknown: { add: EndpointExperimentalIntegrationWellknownAdd(raw) }, +}) + const EndpointMcpList = (raw: RawClient["server.mcp"]) => (input?: McpListInput) => preserveEffect()( raw["mcp.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -const EndpointMcpAdd = (raw: RawClient["server.mcp"]) => (input: McpAddInput) => - preserveEffect()( +const EndpointMcpResourceCatalog = (raw: RawClient["server.mcp"]) => (input?: McpResourceCatalogInput) => + preserveEffect()( + raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const adaptGroupMcp = (raw: RawClient["server.mcp"]) => ({ + list: EndpointMcpList(raw), + resource: { catalog: EndpointMcpResourceCatalog(raw) }, +}) + +const EndpointExperimentalMcpAdd = (raw: RawClient["server.mcp"]) => (input: ExperimentalMcpAddInput) => + preserveEffect()( raw["mcp.add"]({ params: { server: input["server"] }, query: { location: input["location"] }, @@ -1056,48 +1078,41 @@ const EndpointMcpAdd = (raw: RawClient["server.mcp"]) => (input: McpAddInput) => }).pipe(Effect.mapError(mapClientError)), ) -const EndpointMcpRemove = (raw: RawClient["server.mcp"]) => (input: McpRemoveInput) => - preserveEffect()( +const EndpointExperimentalMcpRemove = (raw: RawClient["server.mcp"]) => (input: ExperimentalMcpRemoveInput) => + preserveEffect()( raw["mcp.remove"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ), ) -const EndpointMcpConnect = (raw: RawClient["server.mcp"]) => (input: McpConnectInput) => - preserveEffect()( +const EndpointExperimentalMcpConnect = (raw: RawClient["server.mcp"]) => (input: ExperimentalMcpConnectInput) => + preserveEffect()( raw["mcp.connect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ), ) -const EndpointMcpDisconnect = (raw: RawClient["server.mcp"]) => (input: McpDisconnectInput) => - preserveEffect()( +const EndpointExperimentalMcpDisconnect = (raw: RawClient["server.mcp"]) => (input: ExperimentalMcpDisconnectInput) => + preserveEffect()( raw["mcp.disconnect"]({ params: { server: input["server"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ), ) -const EndpointMcpResourceCatalog = (raw: RawClient["server.mcp"]) => (input?: McpResourceCatalogInput) => - preserveEffect()( - raw["mcp.resource.catalog"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), - ) - -const EndpointMcpClientToken = (raw: RawClient["server.mcp"]) => (input: McpClientTokenInput) => - preserveEffect()( +const EndpointExperimentalMcpClientToken = (raw: RawClient["server.mcp"]) => (input: ExperimentalMcpClientTokenInput) => + preserveEffect()( raw["mcp.clientToken"]({ query: { location: input["location"], name: input["name"] }, headers: { "x-opencode-ticket": input["x-opencode-ticket"] }, }).pipe(Effect.mapError(mapClientError)), ) -const adaptGroupMcp = (raw: RawClient["server.mcp"]) => ({ - list: EndpointMcpList(raw), - add: EndpointMcpAdd(raw), - remove: EndpointMcpRemove(raw), - connect: EndpointMcpConnect(raw), - disconnect: EndpointMcpDisconnect(raw), - resource: { catalog: EndpointMcpResourceCatalog(raw) }, - client: { token: EndpointMcpClientToken(raw) }, +const adaptGroupExperimentalMcp = (raw: RawClient["server.mcp"]) => ({ + add: EndpointExperimentalMcpAdd(raw), + remove: EndpointExperimentalMcpRemove(raw), + connect: EndpointExperimentalMcpConnect(raw), + disconnect: EndpointExperimentalMcpDisconnect(raw), + client: { token: EndpointExperimentalMcpClientToken(raw) }, }) const EndpointCredentialList = (raw: RawClient["server.credential"]) => () => @@ -1635,10 +1650,14 @@ const adaptGroupDebug = (raw: RawClient["server.debug"]) => ({ location: { list: EndpointDebugLocationList(raw), evict: EndpointDebugLocationEvict(raw) }, }) -const EndpointMigrationV1Status = (raw: RawClient["server.migration"]) => () => - preserveEffect()(raw["migration.v1.status"]({}).pipe(Effect.mapError(mapClientError))) +const EndpointExperimentalMigrationV1Status = (raw: RawClient["server.migration"]) => () => + preserveEffect()( + raw["migration.v1.status"]({}).pipe(Effect.mapError(mapClientError)), + ) -const adaptGroupMigration = (raw: RawClient["server.migration"]) => ({ v1: { status: EndpointMigrationV1Status(raw) } }) +const adaptGroupExperimentalMigration = (raw: RawClient["server.migration"]) => ({ + v1: { status: EndpointExperimentalMigrationV1Status(raw) }, +}) const EndpointWebsearchProviders = (raw: RawClient["server.websearch"]) => (input?: WebsearchProvidersInput) => preserveEffect()( @@ -1658,12 +1677,14 @@ const adaptGroupWebsearch = (raw: RawClient["server.websearch"]) => ({ query: EndpointWebsearchQuery(raw), }) -const EndpointUsageLimits = (raw: RawClient["server.usage"]) => (input?: UsageLimitsInput) => - preserveEffect()( +const EndpointExperimentalUsageLimits = (raw: RawClient["server.usage"]) => (input?: ExperimentalUsageLimitsInput) => + preserveEffect()( raw["usage.limits"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)), ) -const adaptGroupUsage = (raw: RawClient["server.usage"]) => ({ limits: EndpointUsageLimits(raw) }) +const adaptGroupExperimentalUsage = (raw: RawClient["server.usage"]) => ({ + limits: EndpointExperimentalUsageLimits(raw), +}) const EndpointConfigGet = (raw: RawClient["server.config"]) => (input?: ConfigGetInput) => preserveEffect()( @@ -1673,15 +1694,18 @@ const EndpointConfigGet = (raw: RawClient["server.config"]) => (input?: ConfigGe const EndpointConfigShells = (raw: RawClient["server.config"]) => () => preserveEffect()(raw["config.shells"]({}).pipe(Effect.mapError(mapClientError))) -const EndpointConfigUpdate = (raw: RawClient["server.config"]) => (input: ConfigUpdateInput) => - preserveEffect()( - raw["config.update"]({ payload: { shell: input["shell"] } }).pipe(Effect.mapError(mapClientError)), - ) - const adaptGroupConfig = (raw: RawClient["server.config"]) => ({ get: EndpointConfigGet(raw), shells: EndpointConfigShells(raw), - update: EndpointConfigUpdate(raw), +}) + +const EndpointExperimentalConfigUpdate = (raw: RawClient["server.config"]) => (input: ExperimentalConfigUpdateInput) => + preserveEffect()( + raw["config.update"]({ payload: { shell: input["shell"] } }).pipe(Effect.mapError(mapClientError)), + ) + +const adaptGroupExperimentalConfig = (raw: RawClient["server.config"]) => ({ + update: EndpointExperimentalConfigUpdate(raw), }) const adaptClient = (raw: RawClient) => ({ @@ -1690,10 +1714,19 @@ const adaptClient = (raw: RawClient) => ({ agent: adaptGroupAgent(raw["server.agent"]), plugin: adaptGroupPlugin(raw["server.plugin"]), session: adaptGroupSession(raw["server.session"]), + experimental: { + ...adaptGroupExperimental(raw["server.experimental"]), + session: adaptGroupExperimentalSession(raw["server.session"]), + generate: adaptGroupExperimentalGenerate(raw["server.generate"]), + voice: adaptGroupExperimentalVoice(raw["server.voice"]), + integration: adaptGroupExperimentalIntegration(raw["server.integration"]), + mcp: adaptGroupExperimentalMcp(raw["server.mcp"]), + migration: adaptGroupExperimentalMigration(raw["server.migration"]), + usage: adaptGroupExperimentalUsage(raw["server.usage"]), + config: adaptGroupExperimentalConfig(raw["server.config"]), + }, message: adaptGroupMessage(raw["server.message"]), model: adaptGroupModel(raw["server.model"]), - generate: adaptGroupGenerate(raw["server.generate"]), - voice: adaptGroupVoice(raw["server.voice"]), provider: adaptGroupProvider(raw["server.provider"]), integration: adaptGroupIntegration(raw["server.integration"]), mcp: adaptGroupMcp(raw["server.mcp"]), @@ -1707,15 +1740,12 @@ const adaptClient = (raw: RawClient) => ({ rpc: adaptGroupRpc(raw["server.rpc"]), event: adaptGroupEvent(raw["server.event"]), pty: adaptGroupPty(raw["server.pty"]), - experimental: adaptGroupExperimental(raw["server.experimental"]), shell: adaptGroupShell(raw["server.shell"]), reference: adaptGroupReference(raw["server.reference"]), worktree: adaptGroupWorktree(raw["server.worktree"]), vcs: adaptGroupVcs(raw["server.vcs"]), debug: adaptGroupDebug(raw["server.debug"]), - migration: adaptGroupMigration(raw["server.migration"]), websearch: adaptGroupWebsearch(raw["server.websearch"]), - usage: adaptGroupUsage(raw["server.usage"]), config: adaptGroupConfig(raw["server.config"]), }) diff --git a/packages/client/src/promise/api.ts b/packages/client/src/promise/api.ts index ed9b4bd9570a..d9d12fce18c2 100644 --- a/packages/client/src/promise/api.ts +++ b/packages/client/src/promise/api.ts @@ -9,7 +9,8 @@ export type CommandApi = Client["command"] export type ConfigApi = Client["config"] export type CredentialApi = Client["credential"] export type EventApi = Client["event"] -export type GenerateApi = Client["generate"] +export type ExperimentalGenerateApi = Client["experimental"]["generate"] +export type ExperimentalSessionApi = Client["experimental"]["session"] export type IntegrationApi = Client["integration"] export type McpApi = Client["mcp"] export type ModelApi = Client["model"] diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 8c19478c024b..3a523cdb1384 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -18,14 +18,8 @@ import type { PluginUpdateOutput, SessionListInput, SessionListOutput, - SessionStatsInput, - SessionStatsOutput, SessionCreateInput, SessionCreateOutput, - SessionImportInput, - SessionImportOutput, - SessionExportInput, - SessionExportOutput, SessionActiveOutput, SessionGetInput, SessionGetOutput, @@ -45,16 +39,12 @@ import type { SessionPromptOutput, SessionCommandInput, SessionCommandOutput, - SessionSkillInput, - SessionSkillOutput, SessionSyntheticInput, SessionSyntheticOutput, SessionShellInput, SessionShellOutput, SessionCompactInput, SessionCompactOutput, - SessionWaitInput, - SessionWaitOutput, SessionRevertStageInput, SessionRevertStageOutput, SessionRevertClearInput, @@ -71,16 +61,8 @@ import type { SessionInboxCancelOutput, SessionInboxUpdateInput, SessionInboxUpdateOutput, - SessionInstructionsEntryListInput, - SessionInstructionsEntryListOutput, - SessionInstructionsEntryPutInput, - SessionInstructionsEntryPutOutput, - SessionInstructionsEntryRemoveInput, - SessionInstructionsEntryRemoveOutput, SessionGenerateInput, SessionGenerateOutput, - SessionLogInput, - SessionLogOutput, SessionInterruptInput, SessionInterruptOutput, SessionBackgroundInput, @@ -101,17 +83,35 @@ import type { SessionEnvironmentOutput, SessionViewInput, SessionViewOutput, + ExperimentalSessionStatsInput, + ExperimentalSessionStatsOutput, + ExperimentalSessionImportInput, + ExperimentalSessionImportOutput, + ExperimentalSessionExportInput, + ExperimentalSessionExportOutput, + ExperimentalSessionSkillInput, + ExperimentalSessionSkillOutput, + ExperimentalSessionWaitInput, + ExperimentalSessionWaitOutput, + ExperimentalSessionInstructionsEntryListInput, + ExperimentalSessionInstructionsEntryListOutput, + ExperimentalSessionInstructionsEntryPutInput, + ExperimentalSessionInstructionsEntryPutOutput, + ExperimentalSessionInstructionsEntryRemoveInput, + ExperimentalSessionInstructionsEntryRemoveOutput, + ExperimentalSessionLogInput, + ExperimentalSessionLogOutput, MessageListInput, MessageListOutput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, - GenerateTextInput, - GenerateTextOutput, - VoiceModelsOutput, - VoiceSessionInput, - VoiceSessionOutput, + ExperimentalGenerateTextInput, + ExperimentalGenerateTextOutput, + ExperimentalVoiceModelsOutput, + ExperimentalVoiceSessionInput, + ExperimentalVoiceSessionOutput, ProviderListInput, ProviderListOutput, ProviderGetInput, @@ -120,8 +120,6 @@ import type { IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, - IntegrationWellknownAddInput, - IntegrationWellknownAddOutput, IntegrationConnectKeyInput, IntegrationConnectKeyOutput, IntegrationConnectExternalInput, @@ -140,20 +138,22 @@ import type { IntegrationCommandStatusOutput, IntegrationCommandCancelInput, IntegrationCommandCancelOutput, + ExperimentalIntegrationWellknownAddInput, + ExperimentalIntegrationWellknownAddOutput, McpListInput, McpListOutput, - McpAddInput, - McpAddOutput, - McpRemoveInput, - McpRemoveOutput, - McpConnectInput, - McpConnectOutput, - McpDisconnectInput, - McpDisconnectOutput, McpResourceCatalogInput, McpResourceCatalogOutput, - McpClientTokenInput, - McpClientTokenOutput, + ExperimentalMcpAddInput, + ExperimentalMcpAddOutput, + ExperimentalMcpRemoveInput, + ExperimentalMcpRemoveOutput, + ExperimentalMcpConnectInput, + ExperimentalMcpConnectOutput, + ExperimentalMcpDisconnectInput, + ExperimentalMcpDisconnectOutput, + ExperimentalMcpClientTokenInput, + ExperimentalMcpClientTokenOutput, CredentialListOutput, CredentialCreateInput, CredentialCreateOutput, @@ -191,8 +191,8 @@ import type { FileListOutput, FileFindInput, FileFindOutput, - FileWriteInput, - FileWriteOutput, + ExperimentalFileWriteInput, + ExperimentalFileWriteOutput, CommandListInput, CommandListOutput, SkillListInput, @@ -267,18 +267,18 @@ import type { DebugLocationListOutput, DebugLocationEvictInput, DebugLocationEvictOutput, - MigrationV1StatusOutput, + ExperimentalMigrationV1StatusOutput, WebsearchProvidersInput, WebsearchProvidersOutput, WebsearchQueryInput, WebsearchQueryOutput, - UsageLimitsInput, - UsageLimitsOutput, + ExperimentalUsageLimitsInput, + ExperimentalUsageLimitsOutput, ConfigGetInput, ConfigGetOutput, ConfigShellsOutput, - ConfigUpdateInput, - ConfigUpdateOutput, + ExperimentalConfigUpdateInput, + ExperimentalConfigUpdateOutput, } from "./types.js" import { ClientError } from "./client-error.js" @@ -569,24 +569,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - stats: (input?: SessionStatsInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionStatsOutput }>( - { - method: "GET", - path: `/api/experimental/session/stats`, - query: { - from: input?.["from"], - to: input?.["to"], - project: input?.["project"], - timezone: input?.["timezone"], - tools: input?.["tools"], - }, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ).then((value) => value.data), create: (input?: SessionCreateInput, requestOptions?: RequestOptions) => request<{ readonly data: SessionCreateOutput }>( { @@ -608,30 +590,6 @@ export function make(options: ClientOptions) { }, requestOptions, ).then((value) => value.data), - import: (input: SessionImportInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionImportOutput }>( - { - method: "POST", - path: `/api/experimental/session/import`, - body: { info: input["info"], messages: input["messages"], location: input["location"] }, - successStatus: 200, - declaredStatuses: [400, 401, 404, 409], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - export: (input: SessionExportInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionExportOutput }>( - { - method: "GET", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/export`, - query: { sanitize: input["sanitize"] }, - successStatus: 200, - declaredStatuses: [400, 401, 404, 500], - empty: false, - }, - requestOptions, - ).then((value) => value.data), active: (requestOptions?: RequestOptions) => request<{ readonly data: SessionActiveOutput }>( { @@ -770,18 +728,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - skill: (input: SessionSkillInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/skill`, - body: { id: input["id"], resume: input["resume"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), synthetic: (input: SessionSyntheticInput, requestOptions?: RequestOptions) => request<{ readonly data: SessionSyntheticOutput }>( { @@ -825,17 +771,6 @@ export function make(options: ClientOptions) { }, requestOptions, ).then((value) => value.data), - wait: (input: SessionWaitInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/wait`, - successStatus: 204, - declaredStatuses: [400, 401, 404, 503], - empty: true, - }, - requestOptions, - ), revert: { stage: (input: SessionRevertStageInput, requestOptions?: RequestOptions) => request<{ readonly data: SessionRevertStageOutput }>( @@ -931,44 +866,6 @@ export function make(options: ClientOptions) { requestOptions, ), }, - instructions: { - entry: { - list: (input: SessionInstructionsEntryListInput, requestOptions?: RequestOptions) => - request<{ readonly data: SessionInstructionsEntryListOutput }>( - { - method: "GET", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries`, - successStatus: 200, - declaredStatuses: [400, 401, 404], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - put: (input: SessionInstructionsEntryPutInput, requestOptions?: RequestOptions) => - request( - { - method: "PUT", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`, - body: { value: input["value"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404, 413], - empty: true, - }, - requestOptions, - ), - remove: (input: SessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), - }, - }, generate: (input: SessionGenerateInput, requestOptions?: RequestOptions) => request<{ readonly data: SessionGenerateOutput }>( { @@ -981,18 +878,6 @@ export function make(options: ClientOptions) { }, requestOptions, ).then((value) => value.data), - log: (input: SessionLogInput, requestOptions?: RequestOptions): AsyncIterable => - sse( - { - method: "GET", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/log`, - query: { after: input["after"], follow: input["follow"] }, - successStatus: 200, - declaredStatuses: [400, 401, 404], - empty: false, - }, - requestOptions, - ), interrupt: (input: SessionInterruptInput, requestOptions?: RequestOptions) => request( { @@ -1114,6 +999,432 @@ export function make(options: ClientOptions) { requestOptions, ), }, + experimental: { + session: { + stats: (input?: ExperimentalSessionStatsInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalSessionStatsOutput }>( + { + method: "GET", + path: `/api/experimental/session/stats`, + query: { + from: input?.["from"], + to: input?.["to"], + project: input?.["project"], + timezone: input?.["timezone"], + tools: input?.["tools"], + }, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + import: (input: ExperimentalSessionImportInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalSessionImportOutput }>( + { + method: "POST", + path: `/api/experimental/session/import`, + body: { info: input["info"], messages: input["messages"], location: input["location"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404, 409], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + export: (input: ExperimentalSessionExportInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalSessionExportOutput }>( + { + method: "GET", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/export`, + query: { sanitize: input["sanitize"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404, 500], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + skill: (input: ExperimentalSessionSkillInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/skill`, + body: { id: input["id"], resume: input["resume"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + wait: (input: ExperimentalSessionWaitInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/wait`, + successStatus: 204, + declaredStatuses: [400, 401, 404, 503], + empty: true, + }, + requestOptions, + ), + instructions: { + entry: { + list: (input: ExperimentalSessionInstructionsEntryListInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalSessionInstructionsEntryListOutput }>( + { + method: "GET", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries`, + successStatus: 200, + declaredStatuses: [400, 401, 404], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + put: (input: ExperimentalSessionInstructionsEntryPutInput, requestOptions?: RequestOptions) => + request( + { + method: "PUT", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`, + body: { value: input["value"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404, 413], + empty: true, + }, + requestOptions, + ), + remove: (input: ExperimentalSessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/instructions/entries/${encodeURIComponent(input.key)}`, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + }, + }, + log: ( + input: ExperimentalSessionLogInput, + requestOptions?: RequestOptions, + ): AsyncIterable => + sse( + { + method: "GET", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/log`, + query: { after: input["after"], follow: input["follow"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404], + empty: false, + }, + requestOptions, + ), + }, + generate: { + text: (input: ExperimentalGenerateTextInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalGenerateTextOutput }>( + { + method: "POST", + path: `/api/experimental/generate`, + body: { prompt: input["prompt"], model: input["model"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + }, + voice: { + models: (requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalVoiceModelsOutput }>( + { + method: "GET", + path: `/api/experimental/voice/models`, + successStatus: 200, + declaredStatuses: [400, 401, 404, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + session: (input: ExperimentalVoiceSessionInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalVoiceSessionOutput }>( + { + method: "POST", + path: `/api/experimental/voice/session`, + body: { model: input["model"], session: input["session"], transport: input["transport"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + }, + integration: { + wellknown: { + add: (input: ExperimentalIntegrationWellknownAddInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/integration/wellknown`, + query: { location: input["location"] }, + body: { url: input["url"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + }, + }, + mcp: { + add: (input: ExperimentalMcpAddInput, requestOptions?: RequestOptions) => + request( + { + method: "PUT", + path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`, + query: { location: input["location"] }, + body: { config: input["config"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + remove: (input: ExperimentalMcpRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + connect: (input: ExperimentalMcpConnectInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/connect`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + disconnect: (input: ExperimentalMcpDisconnectInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/disconnect`, + query: { location: input["location"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + client: { + token: (input: ExperimentalMcpClientTokenInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/client-mcp/token`, + query: { location: input["location"], name: input["name"] }, + headers: { "x-opencode-ticket": input["x-opencode-ticket"] }, + successStatus: 200, + declaredStatuses: [400, 401, 403, 404], + empty: false, + }, + requestOptions, + ), + }, + }, + file: { + write: (input: ExperimentalFileWriteInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/fs/write`, + query: { location: input["location"], path: input["path"] }, + body: input["payload"], + successStatus: 200, + declaredStatuses: [400, 401, 404], + empty: false, + binaryBody: true, + }, + requestOptions, + ), + }, + persistentPty: { + read: (input: ExperimentalPersistentPtyReadInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalPersistentPtyReadOutput }>( + { + method: "GET", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/terminal/read`, + query: { lines: input["lines"] }, + successStatus: 200, + declaredStatuses: [400, 401, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + list: (input: ExperimentalPersistentPtyListInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalPersistentPtyListOutput }>( + { + method: "GET", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/terminal`, + successStatus: 200, + declaredStatuses: [400, 401, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + create: (input: ExperimentalPersistentPtyCreateInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalPersistentPtyCreateOutput }>( + { + method: "POST", + path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/terminal`, + body: { + command: input["command"], + args: input["args"], + cwd: input["cwd"], + title: input["title"], + env: input["env"], + size: input["size"], + }, + successStatus: 200, + declaredStatuses: [400, 401, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + shutdown: (requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/persistent-pty/shutdown`, + successStatus: 204, + declaredStatuses: [400, 401, 503], + empty: true, + }, + requestOptions, + ), + handoff: (requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/experimental/persistent-pty/handoff`, + successStatus: 200, + declaredStatuses: [400, 401, 503], + empty: false, + }, + requestOptions, + ), + get: (input: ExperimentalPersistentPtyGetInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalPersistentPtyGetOutput }>( + { + method: "GET", + path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}`, + successStatus: 200, + declaredStatuses: [400, 401, 404, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + update: (input: ExperimentalPersistentPtyUpdateInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalPersistentPtyUpdateOutput }>( + { + method: "PUT", + path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}`, + body: { attachmentID: input["attachmentID"], size: input["size"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + snapshot: (input: ExperimentalPersistentPtySnapshotInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalPersistentPtySnapshotOutput }>( + { + method: "GET", + path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}/snapshot`, + successStatus: 200, + declaredStatuses: [400, 401, 404, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + remove: (input: ExperimentalPersistentPtyRemoveInput, requestOptions?: RequestOptions) => + request( + { + method: "DELETE", + path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}`, + successStatus: 204, + declaredStatuses: [400, 401, 404, 503], + empty: true, + }, + requestOptions, + ), + connectToken: (input: ExperimentalPersistentPtyConnectTokenInput, requestOptions?: RequestOptions) => + request<{ readonly data: ExperimentalPersistentPtyConnectTokenOutput }>( + { + method: "POST", + path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}/connect-token`, + headers: { "x-opencode-ticket": input["x-opencode-ticket"] }, + successStatus: 200, + declaredStatuses: [400, 401, 403, 404, 503], + empty: false, + }, + requestOptions, + ).then((value) => value.data), + }, + migration: { + v1: { + status: (requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/experimental/migration/v1`, + successStatus: 200, + declaredStatuses: [400, 401], + empty: false, + }, + requestOptions, + ), + }, + }, + usage: { + limits: (input?: ExperimentalUsageLimitsInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/experimental/usage/limits`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [400, 401, 404, 503], + empty: false, + }, + requestOptions, + ), + }, + config: { + update: (input: ExperimentalConfigUpdateInput, requestOptions?: RequestOptions) => + request( + { + method: "PATCH", + path: `/api/experimental/config`, + body: { shell: input["shell"] }, + successStatus: 204, + declaredStatuses: [400, 401, 404], + empty: true, + }, + requestOptions, + ), + }, + }, message: { list: (input: MessageListInput, requestOptions?: RequestOptions) => request( @@ -1154,45 +1465,6 @@ export function make(options: ClientOptions) { requestOptions, ), }, - generate: { - text: (input: GenerateTextInput, requestOptions?: RequestOptions) => - request<{ readonly data: GenerateTextOutput }>( - { - method: "POST", - path: `/api/experimental/generate`, - body: { prompt: input["prompt"], model: input["model"] }, - successStatus: 200, - declaredStatuses: [400, 401, 404, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - }, - voice: { - models: (requestOptions?: RequestOptions) => - request<{ readonly data: VoiceModelsOutput }>( - { - method: "GET", - path: `/api/experimental/voice/models`, - successStatus: 200, - declaredStatuses: [400, 401, 404, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - session: (input: VoiceSessionInput, requestOptions?: RequestOptions) => - request<{ readonly data: VoiceSessionOutput }>( - { - method: "POST", - path: `/api/experimental/voice/session`, - body: { model: input["model"], session: input["session"], transport: input["transport"] }, - successStatus: 200, - declaredStatuses: [400, 401, 404, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - }, provider: { list: (input?: ProviderListInput, requestOptions?: RequestOptions) => request( @@ -1244,21 +1516,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - wellknown: { - add: (input: IntegrationWellknownAddInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/integration/wellknown`, - query: { location: input["location"] }, - body: { url: input["url"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), - }, connect: { key: (input: IntegrationConnectKeyInput, requestOptions?: RequestOptions) => request( @@ -1392,55 +1649,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - add: (input: McpAddInput, requestOptions?: RequestOptions) => - request( - { - method: "PUT", - path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`, - query: { location: input["location"] }, - body: { config: input["config"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), - remove: (input: McpRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), - connect: (input: McpConnectInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/connect`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), - disconnect: (input: McpDisconnectInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/disconnect`, - query: { location: input["location"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), resource: { catalog: (input?: McpResourceCatalogInput, requestOptions?: RequestOptions) => request( @@ -1455,21 +1663,6 @@ export function make(options: ClientOptions) { requestOptions, ), }, - client: { - token: (input: McpClientTokenInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/client-mcp/token`, - query: { location: input["location"], name: input["name"] }, - headers: { "x-opencode-ticket": input["x-opencode-ticket"] }, - successStatus: 200, - declaredStatuses: [400, 401, 403, 404], - empty: false, - }, - requestOptions, - ), - }, }, credential: { list: (requestOptions?: RequestOptions) => @@ -1720,20 +1913,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - write: (input: FileWriteInput, requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/fs/write`, - query: { location: input["location"], path: input["path"] }, - body: input["payload"], - successStatus: 200, - declaredStatuses: [400, 401, 404], - empty: false, - binaryBody: true, - }, - requestOptions, - ), }, command: { list: (input?: CommandListInput, requestOptions?: RequestOptions) => @@ -1870,131 +2049,6 @@ export function make(options: ClientOptions) { ), }, }, - experimental: { - persistentPty: { - read: (input: ExperimentalPersistentPtyReadInput, requestOptions?: RequestOptions) => - request<{ readonly data: ExperimentalPersistentPtyReadOutput }>( - { - method: "GET", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/terminal/read`, - query: { lines: input["lines"] }, - successStatus: 200, - declaredStatuses: [400, 401, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - list: (input: ExperimentalPersistentPtyListInput, requestOptions?: RequestOptions) => - request<{ readonly data: ExperimentalPersistentPtyListOutput }>( - { - method: "GET", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/terminal`, - successStatus: 200, - declaredStatuses: [400, 401, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - create: (input: ExperimentalPersistentPtyCreateInput, requestOptions?: RequestOptions) => - request<{ readonly data: ExperimentalPersistentPtyCreateOutput }>( - { - method: "POST", - path: `/api/experimental/session/${encodeURIComponent(input.sessionID)}/terminal`, - body: { - command: input["command"], - args: input["args"], - cwd: input["cwd"], - title: input["title"], - env: input["env"], - size: input["size"], - }, - successStatus: 200, - declaredStatuses: [400, 401, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - shutdown: (requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/persistent-pty/shutdown`, - successStatus: 204, - declaredStatuses: [400, 401, 503], - empty: true, - }, - requestOptions, - ), - handoff: (requestOptions?: RequestOptions) => - request( - { - method: "POST", - path: `/api/experimental/persistent-pty/handoff`, - successStatus: 200, - declaredStatuses: [400, 401, 503], - empty: false, - }, - requestOptions, - ), - get: (input: ExperimentalPersistentPtyGetInput, requestOptions?: RequestOptions) => - request<{ readonly data: ExperimentalPersistentPtyGetOutput }>( - { - method: "GET", - path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}`, - successStatus: 200, - declaredStatuses: [400, 401, 404, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - update: (input: ExperimentalPersistentPtyUpdateInput, requestOptions?: RequestOptions) => - request<{ readonly data: ExperimentalPersistentPtyUpdateOutput }>( - { - method: "PUT", - path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}`, - body: { attachmentID: input["attachmentID"], size: input["size"] }, - successStatus: 200, - declaredStatuses: [400, 401, 404, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - snapshot: (input: ExperimentalPersistentPtySnapshotInput, requestOptions?: RequestOptions) => - request<{ readonly data: ExperimentalPersistentPtySnapshotOutput }>( - { - method: "GET", - path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}/snapshot`, - successStatus: 200, - declaredStatuses: [400, 401, 404, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - remove: (input: ExperimentalPersistentPtyRemoveInput, requestOptions?: RequestOptions) => - request( - { - method: "DELETE", - path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}`, - successStatus: 204, - declaredStatuses: [400, 401, 404, 503], - empty: true, - }, - requestOptions, - ), - connectToken: (input: ExperimentalPersistentPtyConnectTokenInput, requestOptions?: RequestOptions) => - request<{ readonly data: ExperimentalPersistentPtyConnectTokenOutput }>( - { - method: "POST", - path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}/connect-token`, - headers: { "x-opencode-ticket": input["x-opencode-ticket"] }, - successStatus: 200, - declaredStatuses: [400, 401, 403, 404, 503], - empty: false, - }, - requestOptions, - ).then((value) => value.data), - }, - }, shell: { list: (input?: ShellListInput, requestOptions?: RequestOptions) => request( @@ -2248,21 +2302,6 @@ export function make(options: ClientOptions) { ), }, }, - migration: { - v1: { - status: (requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/experimental/migration/v1`, - successStatus: 200, - declaredStatuses: [400, 401], - empty: false, - }, - requestOptions, - ), - }, - }, websearch: { providers: (input?: WebsearchProvidersInput, requestOptions?: RequestOptions) => request( @@ -2290,20 +2329,6 @@ export function make(options: ClientOptions) { requestOptions, ), }, - usage: { - limits: (input?: UsageLimitsInput, requestOptions?: RequestOptions) => - request( - { - method: "GET", - path: `/api/experimental/usage/limits`, - query: { location: input?.["location"] }, - successStatus: 200, - declaredStatuses: [400, 401, 404, 503], - empty: false, - }, - requestOptions, - ), - }, config: { get: (input?: ConfigGetInput, requestOptions?: RequestOptions) => request( @@ -2328,18 +2353,6 @@ export function make(options: ClientOptions) { }, requestOptions, ), - update: (input: ConfigUpdateInput, requestOptions?: RequestOptions) => - request( - { - method: "PATCH", - path: `/api/experimental/config`, - body: { shell: input["shell"] }, - successStatus: 204, - declaredStatuses: [400, 401, 404], - empty: true, - }, - requestOptions, - ), }, } } diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 8aba08b66962..2ac2dec2d0d1 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -69,18 +69,7 @@ export type FileDiffInfo = { export type SessionsCursor = string -export type SessionStatsToolTotals = { calls: number; succeeded: number; failed: number; unfinished: number } - -export type SessionStatsToolUsage = { - name: string - calls: number - succeeded: number - failed: number - unfinished: number - durationP50?: number -} - -export type SessionStatsActivity = { date: string; steps: number } +export type SessionActive = { type: "running" } export type PromptBase64 = string @@ -92,6 +81,12 @@ export type SkillID = string export type SkillName = string +export type SessionInboxDelivery = "steer" | "queue" + +export type SessionInboxSyntheticPayload = { text: string; description?: string; metadata?: { [x: string]: JsonValue } } + +export type SessionInboxCompactionPayload = {} + export type ShellID = string export type SessionMessageNative = { [x: string]: JsonValue } @@ -116,17 +111,40 @@ export type SessionStructuredError = { retryAfterMs?: number } -export type SessionActive = { type: "running" } +export type SessionGenerateResponse = { data: { text: string } } -export type SessionInboxDelivery = "steer" | "queue" +export type SessionInterruptResponse = { interrupted: boolean } -export type SessionInboxSyntheticPayload = { text: string; description?: string; metadata?: { [x: string]: JsonValue } } +export type FormID = string -export type SessionInboxCompactionPayload = {} +export type FormMetadata = { [x: string]: JsonValue } -export type InstructionEntryKey = string +export type FormWhen = { + key: string + op: "eq" | "neq" + value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean +} -export type SessionGenerateResponse = { data: { text: string } } +export type FormOption = { value: string; label: string; description?: string } + +export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string } + +export type FormValue = string | number | "Infinity" | "-Infinity" | "NaN" | boolean | Array + +export type SessionStatsToolTotals = { calls: number; succeeded: number; failed: number; unfinished: number } + +export type SessionStatsToolUsage = { + name: string + calls: number + succeeded: number + failed: number + unfinished: number + durationP50?: number +} + +export type SessionStatsActivity = { date: string; steps: number } + +export type InstructionEntryKey = string export type EventID = string @@ -150,24 +168,6 @@ export type SessionMessageToolStateRunning1 = { metadata: { [x: string]: JsonValue } } -export type SessionInterruptResponse = { interrupted: boolean } - -export type FormID = string - -export type FormMetadata = { [x: string]: JsonValue } - -export type FormWhen = { - key: string - op: "eq" | "neq" - value: string | number | "Infinity" | "-Infinity" | "NaN" | boolean -} - -export type FormOption = { value: string; label: string; description?: string } - -export type FormExternalField = { key: string; type: "external"; url: string; title?: string; description?: string } - -export type FormValue = string | number | "Infinity" | "-Infinity" | "NaN" | boolean | Array - export type ModelReasoningField = "reasoning" | "reasoning_content" | "reasoning_text" | (string & {}) export type ModelMaxTokensField = "max_completion_tokens" | "max_tokens" @@ -417,11 +417,6 @@ export type SessionRevert = { files?: Array } -export type SessionStatsTools = - | { mode: "none" } - | { mode: "summary"; totals: SessionStatsToolTotals } - | { mode: "detail"; totals: SessionStatsToolTotals; usage: Array } - export type PromptFileAttachment = { data: PromptBase64 mime: string @@ -454,6 +449,24 @@ export type SkillInfo = { content: string } +export type SessionInboxSynthetic = { + id: SessionMessageID + sessionID: SessionID + time: { created: number } + type: "synthetic" + payload: SessionInboxSyntheticPayload + delivery: SessionInboxDelivery +} + +export type SessionInboxCompaction = { + id: SessionMessageID + sessionID: SessionID + time: { created: number } + type: "compaction" + payload: SessionInboxCompactionPayload + delivery: SessionInboxDelivery +} + export type SessionMessageShell = { id: SessionMessageID metadata?: { [x: string]: JsonValue } @@ -528,43 +541,6 @@ export type SessionMessageIdle = { error?: SessionStructuredError } -export type SessionInboxSynthetic = { - id: SessionMessageID - sessionID: SessionID - time: { created: number } - type: "synthetic" - payload: SessionInboxSyntheticPayload - delivery: SessionInboxDelivery -} - -export type SessionInboxCompaction = { - id: SessionMessageID - sessionID: SessionID - time: { created: number } - type: "compaction" - payload: SessionInboxCompactionPayload - delivery: SessionInboxDelivery -} - -export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue } - -export type InstructionEntrySnapshot = Array<{ key: InstructionEntryKey; value: JsonValue; removed: boolean }> - -export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: EventSeq } - -export type LocationRef = { directory: AbsolutePath; workspaceID?: WorkspaceID } - -export type SessionMessageAssistantText1 = { type: "text"; text: string; native?: SessionMessageNative1 } - -export type SessionMessageAssistantReasoning1 = { - type: "reasoning" - text: string - native?: SessionMessageNative1 - time?: { created: number; completed?: number } -} - -export type ToolContent1 = ToolTextContent | ToolFileContent1 - export type FormNumberField = { key: string title?: string @@ -637,6 +613,30 @@ export type FormMultiselectField = { export type FormAnswer = { [x: string]: FormValue } +export type SessionStatsTools = + | { mode: "none" } + | { mode: "summary"; totals: SessionStatsToolTotals } + | { mode: "detail"; totals: SessionStatsToolTotals; usage: Array } + +export type InstructionEntryInfo = { key: InstructionEntryKey; value: JsonValue } + +export type InstructionEntrySnapshot = Array<{ key: InstructionEntryKey; value: JsonValue; removed: boolean }> + +export type EventLogSynced = { type: "log.synced"; aggregateID: string; seq?: EventSeq } + +export type LocationRef = { directory: AbsolutePath; workspaceID?: WorkspaceID } + +export type SessionMessageAssistantText1 = { type: "text"; text: string; native?: SessionMessageNative1 } + +export type SessionMessageAssistantReasoning1 = { + type: "reasoning" + text: string + native?: SessionMessageNative1 + time?: { created: number; completed?: number } +} + +export type ToolContent1 = ToolTextContent | ToolFileContent1 + export type ModelCompatibility = { reasoningField?: ModelReasoningField requireReasoning?: boolean @@ -907,8 +907,6 @@ export type ReferenceSource = ReferenceLocalSource | ReferenceGitSource export type WorktreeList = Array -export type SessionStatsModelUsage = { model: ModelRef; steps: number; tokens: TokenUsageInfo; cost: MoneyUSD } - export type SessionMessageModelSelected = { id: SessionMessageID metadata?: { [x: string]: JsonValue } @@ -932,6 +930,8 @@ export type SessionMessageCompactionCompleted = { tokens?: TokenUsageInfo } +export type SessionStatsModelUsage = { model: ModelRef; steps: number; tokens: TokenUsageInfo; cost: MoneyUSD } + export type ProviderRequest = { settings: ProviderSettings headers: { [x: string]: string } @@ -962,23 +962,23 @@ export type UsageLimitsBudget = { resetsAt: number } -export type SessionMessageUser = { - id: SessionMessageID - metadata?: { [x: string]: JsonValue } - time: { created: number } +export type SessionInboxUserPayload = { text: string files?: Array agents?: Array skills?: Array - type: "user" + metadata?: { [x: string]: JsonValue } } -export type SessionInboxUserPayload = { +export type SessionMessageUser = { + id: SessionMessageID + metadata?: { [x: string]: JsonValue } + time: { created: number } text: string files?: Array agents?: Array skills?: Array - metadata?: { [x: string]: JsonValue } + type: "user" } export type SessionInboxUserPayload1 = { @@ -1004,6 +1004,19 @@ export type SessionMessageToolStateError = { metadata?: { [x: string]: JsonValue } } +export type FormField = + | FormStringField + | FormNumberField + | FormIntegerField + | FormBooleanField + | FormMultiselectField + | FormExternalField + +export type FormState = + | { status: "pending" } + | { status: "answered"; answer: FormAnswer } + | { status: "cancelled"; message?: string } + export type SessionAgentSelected = { id: EventID created: number @@ -1985,19 +1998,6 @@ export type SessionMessageToolStateError1 = { metadata?: { [x: string]: JsonValue } } -export type FormField = - | FormStringField - | FormNumberField - | FormIntegerField - | FormBooleanField - | FormMultiselectField - | FormExternalField - -export type FormState = - | { status: "pending" } - | { status: "answered"; answer: FormAnswer } - | { status: "cancelled"; message?: string } - export type ProviderInfo = { id: ProviderID canonical?: ProviderID @@ -2087,6 +2087,11 @@ export type ReferenceInfo = { source: ReferenceSource } +export type SessionMessageCompaction = + | SessionMessageCompactionRunning + | SessionMessageCompactionCompleted + | SessionMessageCompactionFailed + export type SessionStatsInfo = { range: { from: number; to: number } sessions: number @@ -2102,11 +2107,6 @@ export type SessionStatsInfo = { models: Array } -export type SessionMessageCompaction = - | SessionMessageCompactionRunning - | SessionMessageCompactionCompleted - | SessionMessageCompactionFailed - export type ModelInfo = { id: string modelID: string @@ -2406,6 +2406,8 @@ export type SessionMessageAssistantTool = { time: { created: number; ran?: number; completed?: number } } +export type FormFields = [FormField, ...Array] + export type SessionInboxItem = | { type: "user"; payload: SessionInboxUserPayload1; delivery: SessionInboxDelivery } | { type: "synthetic"; payload: SessionInboxSyntheticPayload1; delivery: SessionInboxDelivery } @@ -2426,8 +2428,6 @@ export type SessionMessageAssistantTool1 = { time: { created: number; ran?: number; completed?: number } } -export type FormFields = [FormField, ...Array] - export type CredentialEntry = { id: CredentialID integrationID: IntegrationID @@ -2463,21 +2463,6 @@ export type SessionMessageAssistant = { retry?: SessionMessageAssistantRetry } -export type SessionInboxEnqueued = { - id: EventID - created: number - metadata?: { [x: string]: any } - type: "session.inbox.enqueued" - durable: { aggregateID: string; seq: EventSeq; version: 1 } - location?: LocationRef - data: { sessionID: SessionID; inboxID: SessionMessageID; item: SessionInboxItem } -} - -export type SessionMessageAssistantContentEncoded = - | SessionMessageAssistantText1 - | SessionMessageAssistantReasoning1 - | SessionMessageAssistantTool1 - export type FormInfo = { id: FormID; sessionID: string; title: string; metadata?: FormMetadata; fields: FormFields } export type FormDetail = { @@ -2495,6 +2480,21 @@ export type IntegrationKeyMethod = { type: "key"; label?: string; form?: FormFie export type IntegrationExternalMethod = { id: IntegrationMethodID; type: "external"; label: string; form?: FormFields } +export type SessionInboxEnqueued = { + id: EventID + created: number + metadata?: { [x: string]: any } + type: "session.inbox.enqueued" + durable: { aggregateID: string; seq: EventSeq; version: 1 } + location?: LocationRef + data: { sessionID: SessionID; inboxID: SessionMessageID; item: SessionInboxItem } +} + +export type SessionMessageAssistantContentEncoded = + | SessionMessageAssistantText1 + | SessionMessageAssistantReasoning1 + | SessionMessageAssistantTool1 + export type FormInfo1 = { id: FormID; sessionID: string; title: string; metadata?: FormMetadata1; fields: FormFields2 } export type SessionMessageInfo = @@ -2510,16 +2510,6 @@ export type SessionMessageInfo = | SessionMessageCompaction | SessionMessageIdle -export type SessionMessageContentUpdated = { - id: EventID - created: number - metadata?: { [x: string]: any } - type: "session.message.content.updated" - durable: { aggregateID: string; seq: EventSeq; version: 1 } - location?: LocationRef - data: { sessionID: SessionID; messageID: SessionMessageID; content: Array } -} - export type IntegrationMethod = | IntegrationOAuthMethod | IntegrationCommandMethod @@ -2527,7 +2517,17 @@ export type IntegrationMethod = | IntegrationExternalMethod | IntegrationEnvMethod -export type FormCreated = { +export type SessionMessageContentUpdated = { + id: EventID + created: number + metadata?: { [x: string]: any } + type: "session.message.content.updated" + durable: { aggregateID: string; seq: EventSeq; version: 1 } + location?: LocationRef + data: { sessionID: SessionID; messageID: SessionMessageID; content: Array } +} + +export type FormCreated = { id: EventID created: number metadata?: { [x: string]: any } @@ -2543,6 +2543,14 @@ export type SessionMessagesResponse = { cursor: { previous?: string | null; next?: string | null } } +export type IntegrationInfo = { + id: IntegrationID + name: string + metadata?: { [x: string]: any } + methods: Array + connections: Array +} + export type SessionEventDurable = | SessionCreated | SessionAgentSelected @@ -2592,14 +2600,6 @@ export type SessionEventDurable = | SessionUsageRecorded | SessionMessageContentUpdated -export type IntegrationInfo = { - id: IntegrationID - name: string - metadata?: { [x: string]: any } - methods: Array - connections: Array -} - export type V2Event = | LocationShutdown | ModelsDevRefreshed @@ -2749,22 +2749,6 @@ export type SessionNotFoundError = { export const isSessionNotFoundError = (value: unknown): value is SessionNotFoundError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionNotFoundError" -export type ConflictError = { - readonly _tag: "ConflictError" - readonly message: string - readonly resource?: string | undefined -} -export const isConflictError = (value: unknown): value is ConflictError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError" - -export type UnknownError = { - readonly _tag: "UnknownError" - readonly message: string - readonly ref?: string | undefined -} -export const isUnknownError = (value: unknown): value is UnknownError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnknownError" - export type MessageNotFoundError = { readonly _tag: "MessageNotFoundError" readonly sessionID: string @@ -2774,6 +2758,14 @@ export type MessageNotFoundError = { export const isMessageNotFoundError = (value: unknown): value is MessageNotFoundError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "MessageNotFoundError" +export type ConflictError = { + readonly _tag: "ConflictError" + readonly message: string + readonly resource?: string | undefined +} +export const isConflictError = (value: unknown): value is ConflictError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ConflictError" + export type CommandNotFoundError = { readonly _tag: "CommandNotFoundError" readonly command: string @@ -2790,14 +2782,6 @@ export type CommandExecutionError = { export const isCommandExecutionError = (value: unknown): value is CommandExecutionError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "CommandExecutionError" -export type SkillNotFoundError = { - readonly _tag: "SkillNotFoundError" - readonly skill: string - readonly message: string -} -export const isSkillNotFoundError = (value: unknown): value is SkillNotFoundError => - typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SkillNotFoundError" - export type SessionBusyError = { readonly _tag: "SessionBusyError" readonly sessionID: string @@ -2806,17 +2790,13 @@ export type SessionBusyError = { export const isSessionBusyError = (value: unknown): value is SessionBusyError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SessionBusyError" -export type InstructionEntryValueTooLargeError = { - readonly _tag: "InstructionEntryValueTooLargeError" - readonly actualBytes: number - readonly maxBytes: number +export type UnknownError = { + readonly _tag: "UnknownError" readonly message: string + readonly ref?: string | undefined } -export const isInstructionEntryValueTooLargeError = (value: unknown): value is InstructionEntryValueTooLargeError => - typeof value === "object" && - value !== null && - "_tag" in value && - value["_tag"] === "InstructionEntryValueTooLargeError" +export const isUnknownError = (value: unknown): value is UnknownError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "UnknownError" export type FormNotFoundError = { readonly _tag: "FormNotFoundError"; readonly id: string; readonly message: string } export const isFormNotFoundError = (value: unknown): value is FormNotFoundError => @@ -2838,6 +2818,26 @@ export type FormAlreadySettledError = { export const isFormAlreadySettledError = (value: unknown): value is FormAlreadySettledError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "FormAlreadySettledError" +export type SkillNotFoundError = { + readonly _tag: "SkillNotFoundError" + readonly skill: string + readonly message: string +} +export const isSkillNotFoundError = (value: unknown): value is SkillNotFoundError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "SkillNotFoundError" + +export type InstructionEntryValueTooLargeError = { + readonly _tag: "InstructionEntryValueTooLargeError" + readonly actualBytes: number + readonly maxBytes: number + readonly message: string +} +export const isInstructionEntryValueTooLargeError = (value: unknown): value is InstructionEntryValueTooLargeError => + typeof value === "object" && + value !== null && + "_tag" in value && + value["_tag"] === "InstructionEntryValueTooLargeError" + export type ProviderNotFoundError = { readonly _tag: "ProviderNotFoundError" readonly providerID: string @@ -3107,46 +3107,6 @@ export type SessionListInput = { export type SessionListOutput = SessionsResponse -export type SessionStatsInput = { - readonly from?: { - readonly from?: number | undefined - readonly to?: number | undefined - readonly project?: string | undefined - readonly timezone?: string | undefined - readonly tools?: "none" | "summary" | "detail" | undefined - }["from"] - readonly to?: { - readonly from?: number | undefined - readonly to?: number | undefined - readonly project?: string | undefined - readonly timezone?: string | undefined - readonly tools?: "none" | "summary" | "detail" | undefined - }["to"] - readonly project?: { - readonly from?: number | undefined - readonly to?: number | undefined - readonly project?: string | undefined - readonly timezone?: string | undefined - readonly tools?: "none" | "summary" | "detail" | undefined - }["project"] - readonly timezone?: { - readonly from?: number | undefined - readonly to?: number | undefined - readonly project?: string | undefined - readonly timezone?: string | undefined - readonly tools?: "none" | "summary" | "detail" | undefined - }["timezone"] - readonly tools?: { - readonly from?: number | undefined - readonly to?: number | undefined - readonly project?: string | undefined - readonly timezone?: string | undefined - readonly tools?: "none" | "summary" | "detail" | undefined - }["tools"] -} - -export type SessionStatsOutput = { data: SessionStatsInfo }["data"] - export type SessionCreateInput = { readonly id?: { readonly id?: string | null @@ -3264,177 +3224,6 @@ export type SessionCreateInput = { export type SessionCreateOutput = { data: SessionInfo }["data"] -export type SessionImportInput = { - readonly info: { - readonly info: { - readonly id: string - readonly parentID?: string - readonly fork?: { - readonly sessionID: string - readonly boundary: - | { readonly type: "before"; readonly messageID: string } - | { readonly type: "through"; readonly messageID: string } - } - readonly projectID: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly cost: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly outcome?: "succeeded" | "failed" | "interrupted" - readonly time: { - readonly created: number - readonly updated: number - readonly idle?: number - readonly viewed?: number - readonly archived?: number - } - readonly title?: string - readonly subpath?: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly permissions?: ReadonlyArray<{ - readonly action: string - readonly resource: string - readonly effect: "allow" | "deny" | "ask" - }> - readonly revert?: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly files?: ReadonlyArray<{ - readonly file: string - readonly patch: string - readonly additions: number - readonly deletions: number - readonly status: "added" | "deleted" | "modified" - }> - } - readonly location: { readonly directory: string } - } - readonly messages: ReadonlyArray - readonly location?: { readonly directory: string } | null - }["info"] - readonly messages: { - readonly info: { - readonly id: string - readonly parentID?: string - readonly fork?: { - readonly sessionID: string - readonly boundary: - | { readonly type: "before"; readonly messageID: string } - | { readonly type: "through"; readonly messageID: string } - } - readonly projectID: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly cost: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly outcome?: "succeeded" | "failed" | "interrupted" - readonly time: { - readonly created: number - readonly updated: number - readonly idle?: number - readonly viewed?: number - readonly archived?: number - } - readonly title?: string - readonly subpath?: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly permissions?: ReadonlyArray<{ - readonly action: string - readonly resource: string - readonly effect: "allow" | "deny" | "ask" - }> - readonly revert?: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly files?: ReadonlyArray<{ - readonly file: string - readonly patch: string - readonly additions: number - readonly deletions: number - readonly status: "added" | "deleted" | "modified" - }> - } - readonly location: { readonly directory: string } - } - readonly messages: ReadonlyArray - readonly location?: { readonly directory: string } | null - }["messages"] - readonly location?: { - readonly info: { - readonly id: string - readonly parentID?: string - readonly fork?: { - readonly sessionID: string - readonly boundary: - | { readonly type: "before"; readonly messageID: string } - | { readonly type: "through"; readonly messageID: string } - } - readonly projectID: string - readonly agent?: string - readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } - readonly cost: number - readonly tokens: { - readonly input: number - readonly output: number - readonly reasoning: number - readonly cache: { readonly read: number; readonly write: number } - } - readonly outcome?: "succeeded" | "failed" | "interrupted" - readonly time: { - readonly created: number - readonly updated: number - readonly idle?: number - readonly viewed?: number - readonly archived?: number - } - readonly title?: string - readonly subpath?: string - readonly metadata?: { readonly [x: string]: JsonValue } - readonly permissions?: ReadonlyArray<{ - readonly action: string - readonly resource: string - readonly effect: "allow" | "deny" | "ask" - }> - readonly revert?: { - readonly messageID: string - readonly partID?: string - readonly snapshot?: string - readonly files?: ReadonlyArray<{ - readonly file: string - readonly patch: string - readonly additions: number - readonly deletions: number - readonly status: "added" | "deleted" | "modified" - }> - } - readonly location: { readonly directory: string } - } - readonly messages: ReadonlyArray - readonly location?: { readonly directory: string } | null - }["location"] -} - -export type SessionImportOutput = { data: SessionInfo }["data"] - -export type SessionExportInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly sanitize?: { readonly sanitize?: boolean | undefined }["sanitize"] -} - -export type SessionExportOutput = { data: SessionTransferData }["data"] - export type SessionActiveOutput = { data: { [x: SessionID]: SessionActive } }["data"] export type SessionGetInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } @@ -3808,14 +3597,6 @@ export type SessionCommandInput = { export type SessionCommandOutput = void -export type SessionSkillInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly id: { readonly id: string; readonly resume?: boolean | undefined }["id"] - readonly resume?: { readonly id: string; readonly resume?: boolean | undefined }["resume"] -} - -export type SessionSkillOutput = void - export type SessionSyntheticInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] readonly id?: { @@ -3889,10 +3670,6 @@ export type SessionCompactInput = { export type SessionCompactOutput = { data: SessionInboxCompaction }["data"] -export type SessionWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionWaitOutput = void - export type SessionRevertStageInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] readonly messageID: { readonly messageID: string; readonly files?: boolean | undefined }["messageID"] @@ -3953,25 +3730,6 @@ export type SessionInboxUpdateInput = { export type SessionInboxUpdateOutput = void -export type SessionInstructionsEntryListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } - -export type SessionInstructionsEntryListOutput = { data: Array }["data"] - -export type SessionInstructionsEntryPutInput = { - readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"] - readonly key: { readonly sessionID: string; readonly key: string }["key"] - readonly value: { readonly value: JsonValue }["value"] -} - -export type SessionInstructionsEntryPutOutput = void - -export type SessionInstructionsEntryRemoveInput = { - readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"] - readonly key: { readonly sessionID: string; readonly key: string }["key"] -} - -export type SessionInstructionsEntryRemoveOutput = void - export type SessionGenerateInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] readonly prompt: { readonly prompt: string }["prompt"] @@ -3979,14 +3737,6 @@ export type SessionGenerateInput = { export type SessionGenerateOutput = SessionGenerateResponse["data"] -export type SessionLogInput = { - readonly sessionID: { readonly sessionID: string }["sessionID"] - readonly after?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["after"] - readonly follow?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["follow"] -} - -export type SessionLogOutput = SessionLogItem - export type SessionInterruptInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] readonly resume?: { readonly resume?: boolean | undefined }["resume"] @@ -4898,6 +4648,258 @@ export type SessionViewInput = { export type SessionViewOutput = void +export type ExperimentalSessionStatsInput = { + readonly from?: { + readonly from?: number | undefined + readonly to?: number | undefined + readonly project?: string | undefined + readonly timezone?: string | undefined + readonly tools?: "none" | "summary" | "detail" | undefined + }["from"] + readonly to?: { + readonly from?: number | undefined + readonly to?: number | undefined + readonly project?: string | undefined + readonly timezone?: string | undefined + readonly tools?: "none" | "summary" | "detail" | undefined + }["to"] + readonly project?: { + readonly from?: number | undefined + readonly to?: number | undefined + readonly project?: string | undefined + readonly timezone?: string | undefined + readonly tools?: "none" | "summary" | "detail" | undefined + }["project"] + readonly timezone?: { + readonly from?: number | undefined + readonly to?: number | undefined + readonly project?: string | undefined + readonly timezone?: string | undefined + readonly tools?: "none" | "summary" | "detail" | undefined + }["timezone"] + readonly tools?: { + readonly from?: number | undefined + readonly to?: number | undefined + readonly project?: string | undefined + readonly timezone?: string | undefined + readonly tools?: "none" | "summary" | "detail" | undefined + }["tools"] +} + +export type ExperimentalSessionStatsOutput = { data: SessionStatsInfo }["data"] + +export type ExperimentalSessionImportInput = { + readonly info: { + readonly info: { + readonly id: string + readonly parentID?: string + readonly fork?: { + readonly sessionID: string + readonly boundary: + | { readonly type: "before"; readonly messageID: string } + | { readonly type: "through"; readonly messageID: string } + } + readonly projectID: string + readonly agent?: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly outcome?: "succeeded" | "failed" | "interrupted" + readonly time: { + readonly created: number + readonly updated: number + readonly idle?: number + readonly viewed?: number + readonly archived?: number + } + readonly title?: string + readonly subpath?: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly permissions?: ReadonlyArray<{ + readonly action: string + readonly resource: string + readonly effect: "allow" | "deny" | "ask" + }> + readonly revert?: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly files?: ReadonlyArray<{ + readonly file: string + readonly patch: string + readonly additions: number + readonly deletions: number + readonly status: "added" | "deleted" | "modified" + }> + } + readonly location: { readonly directory: string } + } + readonly messages: ReadonlyArray + readonly location?: { readonly directory: string } | null + }["info"] + readonly messages: { + readonly info: { + readonly id: string + readonly parentID?: string + readonly fork?: { + readonly sessionID: string + readonly boundary: + | { readonly type: "before"; readonly messageID: string } + | { readonly type: "through"; readonly messageID: string } + } + readonly projectID: string + readonly agent?: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly outcome?: "succeeded" | "failed" | "interrupted" + readonly time: { + readonly created: number + readonly updated: number + readonly idle?: number + readonly viewed?: number + readonly archived?: number + } + readonly title?: string + readonly subpath?: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly permissions?: ReadonlyArray<{ + readonly action: string + readonly resource: string + readonly effect: "allow" | "deny" | "ask" + }> + readonly revert?: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly files?: ReadonlyArray<{ + readonly file: string + readonly patch: string + readonly additions: number + readonly deletions: number + readonly status: "added" | "deleted" | "modified" + }> + } + readonly location: { readonly directory: string } + } + readonly messages: ReadonlyArray + readonly location?: { readonly directory: string } | null + }["messages"] + readonly location?: { + readonly info: { + readonly id: string + readonly parentID?: string + readonly fork?: { + readonly sessionID: string + readonly boundary: + | { readonly type: "before"; readonly messageID: string } + | { readonly type: "through"; readonly messageID: string } + } + readonly projectID: string + readonly agent?: string + readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { readonly read: number; readonly write: number } + } + readonly outcome?: "succeeded" | "failed" | "interrupted" + readonly time: { + readonly created: number + readonly updated: number + readonly idle?: number + readonly viewed?: number + readonly archived?: number + } + readonly title?: string + readonly subpath?: string + readonly metadata?: { readonly [x: string]: JsonValue } + readonly permissions?: ReadonlyArray<{ + readonly action: string + readonly resource: string + readonly effect: "allow" | "deny" | "ask" + }> + readonly revert?: { + readonly messageID: string + readonly partID?: string + readonly snapshot?: string + readonly files?: ReadonlyArray<{ + readonly file: string + readonly patch: string + readonly additions: number + readonly deletions: number + readonly status: "added" | "deleted" | "modified" + }> + } + readonly location: { readonly directory: string } + } + readonly messages: ReadonlyArray + readonly location?: { readonly directory: string } | null + }["location"] +} + +export type ExperimentalSessionImportOutput = { data: SessionInfo }["data"] + +export type ExperimentalSessionExportInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly sanitize?: { readonly sanitize?: boolean | undefined }["sanitize"] +} + +export type ExperimentalSessionExportOutput = { data: SessionTransferData }["data"] + +export type ExperimentalSessionSkillInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly id: { readonly id: string; readonly resume?: boolean | undefined }["id"] + readonly resume?: { readonly id: string; readonly resume?: boolean | undefined }["resume"] +} + +export type ExperimentalSessionSkillOutput = void + +export type ExperimentalSessionWaitInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] } + +export type ExperimentalSessionWaitOutput = void + +export type ExperimentalSessionInstructionsEntryListInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] +} + +export type ExperimentalSessionInstructionsEntryListOutput = { data: Array }["data"] + +export type ExperimentalSessionInstructionsEntryPutInput = { + readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"] + readonly key: { readonly sessionID: string; readonly key: string }["key"] + readonly value: { readonly value: JsonValue }["value"] +} + +export type ExperimentalSessionInstructionsEntryPutOutput = void + +export type ExperimentalSessionInstructionsEntryRemoveInput = { + readonly sessionID: { readonly sessionID: string; readonly key: string }["sessionID"] + readonly key: { readonly sessionID: string; readonly key: string }["key"] +} + +export type ExperimentalSessionInstructionsEntryRemoveOutput = void + +export type ExperimentalSessionLogInput = { + readonly sessionID: { readonly sessionID: string }["sessionID"] + readonly after?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["after"] + readonly follow?: { readonly after?: number | undefined; readonly follow?: boolean | undefined }["follow"] +} + +export type ExperimentalSessionLogOutput = SessionLogItem + export type MessageListInput = { readonly sessionID: { readonly sessionID: string }["sessionID"] readonly limit?: { @@ -4988,7 +4990,7 @@ export type ModelDefaultInput = { export type ModelDefaultOutput = { location: LocationPublicRef; data: ModelInfo | null } -export type GenerateTextInput = { +export type ExperimentalGenerateTextInput = { readonly prompt: { readonly prompt: string readonly model?: { readonly id: string; readonly providerID: string; readonly variant?: string } | null @@ -4999,11 +5001,11 @@ export type GenerateTextInput = { }["model"] } -export type GenerateTextOutput = GenerateTextResponse["data"] +export type ExperimentalGenerateTextOutput = GenerateTextResponse["data"] -export type VoiceModelsOutput = VoiceModelsResponse["data"] +export type ExperimentalVoiceModelsOutput = VoiceModelsResponse["data"] -export type VoiceSessionInput = { +export type ExperimentalVoiceSessionInput = { readonly model: { readonly model: string readonly session?: { readonly [x: string]: unknown } | undefined @@ -5027,7 +5029,7 @@ export type VoiceSessionInput = { }["transport"] } -export type VoiceSessionOutput = VoiceSessionResponse["data"] +export type ExperimentalVoiceSessionOutput = VoiceSessionResponse["data"] export type ProviderListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] @@ -5055,13 +5057,6 @@ export type IntegrationGetInput = { export type IntegrationGetOutput = { location: LocationPublicRef; data: IntegrationInfo } -export type IntegrationWellknownAddInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] - readonly url: { readonly url: string }["url"] -} - -export type IntegrationWellknownAddOutput = void - export type IntegrationConnectKeyInput = { readonly integrationID: { readonly integrationID: string }["integrationID"] readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] @@ -5178,13 +5173,26 @@ export type IntegrationCommandCancelInput = { export type IntegrationCommandCancelOutput = void +export type ExperimentalIntegrationWellknownAddInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] + readonly url: { readonly url: string }["url"] +} + +export type ExperimentalIntegrationWellknownAddOutput = void + export type McpListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] } export type McpListOutput = { location: LocationPublicRef; data: Array } -export type McpAddInput = { +export type McpResourceCatalogInput = { + readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] +} + +export type McpResourceCatalogOutput = { location: LocationPublicRef; data: McpResourceCatalog } + +export type ExperimentalMcpAddInput = { readonly server: { readonly server: string }["server"] readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] readonly config: { @@ -5221,36 +5229,30 @@ export type McpAddInput = { }["config"] } -export type McpAddOutput = void +export type ExperimentalMcpAddOutput = void -export type McpRemoveInput = { +export type ExperimentalMcpRemoveInput = { readonly server: { readonly server: string }["server"] readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] } -export type McpRemoveOutput = void +export type ExperimentalMcpRemoveOutput = void -export type McpConnectInput = { +export type ExperimentalMcpConnectInput = { readonly server: { readonly server: string }["server"] readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] } -export type McpConnectOutput = void +export type ExperimentalMcpConnectOutput = void -export type McpDisconnectInput = { +export type ExperimentalMcpDisconnectInput = { readonly server: { readonly server: string }["server"] readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] } -export type McpDisconnectOutput = void - -export type McpResourceCatalogInput = { - readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] -} - -export type McpResourceCatalogOutput = { location: LocationPublicRef; data: McpResourceCatalog } +export type ExperimentalMcpDisconnectOutput = void -export type McpClientTokenInput = { +export type ExperimentalMcpClientTokenInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined readonly name: string @@ -5262,7 +5264,7 @@ export type McpClientTokenInput = { readonly "x-opencode-ticket"?: { readonly "x-opencode-ticket"?: string | undefined }["x-opencode-ticket"] } -export type McpClientTokenOutput = { location: LocationPublicRef; data: PtyTicketConnectToken } +export type ExperimentalMcpClientTokenOutput = { location: LocationPublicRef; data: PtyTicketConnectToken } export type CredentialListOutput = { data: Array }["data"] @@ -5597,7 +5599,7 @@ export type FileFindInput = { export type FileFindOutput = { location: LocationPublicRef; data: Array } -export type FileWriteInput = { +export type ExperimentalFileWriteInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined readonly path: string @@ -5609,7 +5611,7 @@ export type FileWriteInput = { readonly payload: globalThis.Uint8Array } -export type FileWriteOutput = { location: LocationPublicRef; data: FileSystemWrite } +export type ExperimentalFileWriteOutput = { location: LocationPublicRef; data: FileSystemWrite } export type CommandListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] @@ -6049,7 +6051,7 @@ export type DebugLocationEvictInput = { export type DebugLocationEvictOutput = void -export type MigrationV1StatusOutput = +export type ExperimentalMigrationV1StatusOutput = | { status: "required" | "completed" } | { status: "running"; progress: { label: string; numerator?: number | undefined; denominator?: number | undefined } } | { status: "error"; error: string } @@ -6071,11 +6073,11 @@ export type WebsearchQueryOutput = { data: { providerID: WebSearchID; results: Array } } -export type UsageLimitsInput = { +export type ExperimentalUsageLimitsInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] } -export type UsageLimitsOutput = { location: LocationPublicRef; data: UsageLimitsInfo | null } +export type ExperimentalUsageLimitsOutput = { location: LocationPublicRef; data: UsageLimitsInfo | null } export type ConfigGetInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"] @@ -6085,6 +6087,6 @@ export type ConfigGetOutput = Array export type ConfigShellsOutput = Array -export type ConfigUpdateInput = { readonly shell: { readonly shell: string | null }["shell"] } +export type ExperimentalConfigUpdateInput = { readonly shell: { readonly shell: string | null }["shell"] } -export type ConfigUpdateOutput = void +export type ExperimentalConfigUpdateOutput = void diff --git a/packages/client/src/solid/data.ts b/packages/client/src/solid/data.ts index 5115f667da07..678b5cb50183 100644 --- a/packages/client/src/solid/data.ts +++ b/packages/client/src/solid/data.ts @@ -1385,7 +1385,7 @@ export function createData(config: CreateDataInput) { } const vcs = locationResource("vcs", (location) => api().vcs.get({ location })) - const usageLimits = locationResource("usageLimits", (location) => api().usage.limits({ location })) + const usageLimits = locationResource("usageLimits", (location) => api().experimental.usage.limits({ location })) const shells = locationResource("shell", async (location) => { const response = await api().shell.list({ location }) const ref = { directory: response.location.directory } diff --git a/packages/client/test/api.types.ts b/packages/client/test/api.types.ts index 02533630fed6..93aad966fe02 100644 --- a/packages/client/test/api.types.ts +++ b/packages/client/test/api.types.ts @@ -18,30 +18,30 @@ const effectSession: Effect.Effect = effectClient.session sessionID: "ses_test" as Session.ID, }) -declare const sessionID: Parameters[0]["sessionID"] +declare const sessionID: Parameters[0]["sessionID"] const effectList: Effect.Effect< ReadonlyArray<{ readonly key: string; readonly value: unknown }>, unknown -> = effectApi.session.instructions.entry.list({ sessionID }) -const effectPut: Effect.Effect = effectApi.session.instructions.entry.put({ +> = effectApi.experimental.session.instructions.entry.list({ sessionID }) +const effectPut: Effect.Effect = effectApi.experimental.session.instructions.entry.put({ sessionID, key: "review-notes", value: { text: "Check the diff" }, }) -const effectRemove: Effect.Effect = effectApi.session.instructions.entry.remove({ +const effectRemove: Effect.Effect = effectApi.experimental.session.instructions.entry.remove({ sessionID, key: "review-notes", }) const promiseList: Promise> = - promiseClient.session.instructions.entry.list({ sessionID: "ses_test" }) -const promisePut: Promise = promiseClient.session.instructions.entry.put({ + promiseClient.experimental.session.instructions.entry.list({ sessionID: "ses_test" }) +const promisePut: Promise = promiseClient.experimental.session.instructions.entry.put({ sessionID: "ses_test", key: "review-notes", value: { text: "Check the diff" }, }) -const promiseRemove: Promise = promiseClient.session.instructions.entry.remove({ +const promiseRemove: Promise = promiseClient.experimental.session.instructions.entry.remove({ sessionID: "ses_test", key: "review-notes", }) diff --git a/packages/client/test/effect.test.ts b/packages/client/test/effect.test.ts index b1c1d156847f..211192538954 100644 --- a/packages/client/test/effect.test.ts +++ b/packages/client/test/effect.test.ts @@ -92,13 +92,13 @@ test("session instructions methods use the public HTTP contract", async () => { }) const result = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - const listed = yield* client.session.instructions.entry.list({ sessionID: Session.ID.make("ses_test") }) - yield* client.session.instructions.entry.put({ + const listed = yield* client.experimental.session.instructions.entry.list({ sessionID: Session.ID.make("ses_test") }) + yield* client.experimental.session.instructions.entry.put({ sessionID: Session.ID.make("ses_test"), key: "review-notes", value: instructions[0].value, }) - yield* client.session.instructions.entry.remove({ + yield* client.experimental.session.instructions.entry.remove({ sessionID: Session.ID.make("ses_test"), key: "review-notes", }) @@ -264,9 +264,9 @@ test("session methods retain decoded Effect inputs and outputs", async () => { resume: false, }) yield* client.session.compact({ sessionID: Session.ID.make("ses_test") }) - yield* client.session.wait({ sessionID: Session.ID.make("ses_test") }) + yield* client.experimental.session.wait({ sessionID: Session.ID.make("ses_test") }) const context = yield* client.session.context({ sessionID: Session.ID.make("ses_test") }) - const log = yield* client.session + const log = yield* client.experimental.session .log({ sessionID: Session.ID.make("ses_test"), after: Event.Seq.make(0) }) .pipe(Stream.runCollect) const interrupted = yield* client.session.interrupt({ sessionID: Session.ID.make("ses_test") }) @@ -314,7 +314,7 @@ test("session.log retains the typed SessionNotFoundError", async () => { ) const error = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.session.log({ sessionID: Session.ID.make("ses_missing") }).pipe(Stream.runCollect, Effect.flip) + return yield* client.experimental.session.log({ sessionID: Session.ID.make("ses_missing") }).pipe(Stream.runCollect, Effect.flip) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(error._tag).toBe("SessionNotFoundError") diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 3283ddaaaa13..08bd9e64c16e 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -10,10 +10,9 @@ test("exposes every standard HTTP API group", () => { "agent", "plugin", "session", + "experimental", "message", "model", - "generate", - "voice", "provider", "integration", "mcp", @@ -27,34 +26,99 @@ test("exposes every standard HTTP API group", () => { "rpc", "event", "pty", - "experimental", "shell", "reference", "worktree", "vcs", "debug", - "migration", "websearch", - "usage", "config", ]) expect(Object.keys(client.debug)).toEqual(["heapDump", "location"]) expect(Object.keys(client.debug.location)).toEqual(["list", "evict"]) expect(Object.keys(client.message)).toEqual(["list"]) - expect(Object.keys(client.integration)).toEqual(["list", "get", "wellknown", "connect", "oauth", "command"]) - expect(Object.keys(client.integration.wellknown)).toEqual(["add"]) + expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "oauth", "command"]) expect(Object.keys(client.integration.connect)).toEqual(["key", "external"]) expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"]) expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"]) expect(Object.keys(client.websearch)).toEqual(["providers", "query"]) - expect(Object.keys(client.voice)).toEqual(["models", "session"]) - expect(Object.keys(client.usage)).toEqual(["limits"]) - expect(Object.keys(client.file)).toEqual(["read", "list", "find", "write"]) + expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) + expect(Object.keys(client.mcp)).toEqual(["list", "resource"]) + expect(Object.keys(client.config)).toEqual(["get", "shells"]) expect(Object.keys(client.vcs)).toEqual(["init", "get", "base", "status", "branch", "diff"]) expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove", "connect"]) expect(Object.keys(client.pty.connect)).toEqual(["token"]) - expect(Object.keys(client.experimental)).toEqual(["persistentPty"]) + expect(Object.keys(client.experimental)).toEqual([ + "session", + "generate", + "voice", + "integration", + "mcp", + "file", + "persistentPty", + "migration", + "usage", + "config", + ]) + expect(Object.keys(client.experimental.session)).toEqual([ + "stats", + "import", + "export", + "skill", + "wait", + "instructions", + "log", + ]) + expect(Object.keys(client.experimental.voice)).toEqual(["models", "session"]) + expect(Object.keys(client.experimental.usage)).toEqual(["limits"]) + expect(Object.keys(client.experimental.mcp)).toEqual(["add", "remove", "connect", "disconnect", "client"]) + expect(Object.keys(client.experimental.integration.wellknown)).toEqual(["add"]) + expect(Object.keys(client.experimental.file)).toEqual(["write"]) expect(client.experimental.persistentPty.read).toBeFunction() +}) + +test("nests a method under client.experimental exactly when its route is experimental", async () => { + let url: URL | undefined + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input) => { + url = new URL(input instanceof Request ? input.url : String(input)) + throw new Error("captured") + }, + }) + // Every field reads as "x" so path params resolve; request options stay unset. + const input = new Proxy( + {}, + { get: (_target, key) => (key === "signal" || key === "headers" || key === "onActivity" ? undefined : "x") }, + ) + const routes: Array<{ readonly method: string; readonly experimentalRoute: boolean }> = [] + const visit = async (node: object, path: ReadonlyArray): Promise => { + for (const [name, value] of Object.entries(node)) { + // `rpc` is a callable that also carries its generated routes. + if (typeof value !== "function" || Object.keys(value).length > 0) { + await visit(value, [...path, name]) + continue + } + url = undefined + const result: unknown = value(input) + if (typeof result === "object" && result !== null && Symbol.asyncIterator in result) { + await (result as AsyncIterable)[Symbol.asyncIterator]() + .next() + .catch(() => undefined) + } else await Promise.resolve(result).catch(() => undefined) + const method = [...path, name].join(".") + if (url === undefined) throw new Error(`${method} did not send a request`) + routes.push({ method, experimentalRoute: url.pathname.startsWith("/api/experimental/") }) + } + } + await visit(client, []) + + expect(routes.filter((route) => route.experimentalRoute).length).toBeGreaterThan(20) + expect( + routes + .filter((route) => route.experimentalRoute !== route.method.startsWith("experimental.")) + .map((route) => route.method), + ).toEqual([]) expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "output", "remove"]) expect(Object.keys(client.project)).toEqual(["list", "update"]) expect(Object.keys(client.worktree)).toEqual(["list", "create", "remove", "refresh"]) @@ -253,7 +317,7 @@ test("generate.text uses the locationless public contract", async () => { }, }) - expect(await client.generate.text({ prompt: "ping" })).toEqual({ text: "pong" }) + expect(await client.experimental.generate.text({ prompt: "ping" })).toEqual({ text: "pong" }) expect(request?.url).toBe("http://localhost:3000/api/experimental/generate") expect(await request?.json()).toEqual({ prompt: "ping" }) }) @@ -324,7 +388,7 @@ test("experimental wellknown integration add uses the public HTTP contract", asy }, }) - await client.integration.wellknown.add({ + await client.experimental.integration.wellknown.add({ url: "https://example.com", location: { directory: "/tmp/project" }, }) @@ -586,13 +650,13 @@ test("session instructions methods use the public HTTP contract", async () => { }, }) - const result = await client.session.instructions.entry.list({ sessionID: "ses_test" }) - await client.session.instructions.entry.put({ + const result = await client.experimental.session.instructions.entry.list({ sessionID: "ses_test" }) + await client.experimental.session.instructions.entry.put({ sessionID: "ses_test", key: "review-notes", value: instructions[0].value, }) - await client.session.instructions.entry.remove({ sessionID: "ses_test", key: "review-notes" }) + await client.experimental.session.instructions.entry.remove({ sessionID: "ses_test", key: "review-notes" }) expect(result).toEqual(instructions) expect(requests).toEqual([ @@ -971,10 +1035,10 @@ test("session methods use the public HTTP contract", async () => { resume: false, }) await client.session.compact({ sessionID: "ses_test" }) - await client.session.wait({ sessionID: "ses_test" }) + await client.experimental.session.wait({ sessionID: "ses_test" }) const context = await client.session.context({ sessionID: "ses_test" }) const log = [] - for await (const item of client.session.log({ sessionID: "ses_test", after: 0 })) log.push(item) + for await (const item of client.experimental.session.log({ sessionID: "ses_test", after: 0 })) log.push(item) const interrupted = await client.session.interrupt({ sessionID: "ses_test", resume: true }) const message = await client.session.message.get({ sessionID: "ses_test", messageID: "msg_model" }) @@ -1050,7 +1114,7 @@ test("session.log decodes SessionNotFoundError", async () => { }) try { - await client.session.log({ sessionID: "ses_missing" })[Symbol.asyncIterator]().next() + await client.experimental.session.log({ sessionID: "ses_missing" })[Symbol.asyncIterator]().next() throw new Error("Expected request to fail") } catch (error) { expect(isSessionNotFoundError(error)).toBe(true) diff --git a/packages/desktop/src/renderer/migration-status.tsx b/packages/desktop/src/renderer/migration-status.tsx index c14124a7035c..35fa12539d0c 100644 --- a/packages/desktop/src/renderer/migration-status.tsx +++ b/packages/desktop/src/renderer/migration-status.tsx @@ -1,11 +1,11 @@ -import { OpenCode, type MigrationV1StatusOutput } from "@opencode/client/promise" +import { OpenCode, type ExperimentalMigrationV1StatusOutput } from "@opencode/client/promise" import { useLanguage } from "@opencode/app/desktop" import { Loader } from "@opencode/ui/loader" import { showToast, toaster, Toast } from "@opencode/ui/toast" import { createRoot, createSignal, onCleanup, onMount } from "solid-js" import type { ServerReadyData } from "../shared/ipc-contract" -type Progress = Extract["progress"] +type Progress = Extract["progress"] export function MigrationStatus(props: { server: ServerReadyData }) { const language = useLanguage() @@ -87,7 +87,7 @@ export function MigrationStatus(props: { server: ServerReadyData }) { void (async () => { while (true) { - const status = await client.migration.v1.status({ signal: abort.signal }) + const status = await client.experimental.migration.v1.status({ signal: abort.signal }) setProgress(status.status === "running" ? status.progress : undefined) if (status.status === "running") show() diff --git a/packages/gui-extensions/src/details/server-panel.tsx b/packages/gui-extensions/src/details/server-panel.tsx index 23ef51b7e199..4592c1d8becc 100644 --- a/packages/gui-extensions/src/details/server-panel.tsx +++ b/packages/gui-extensions/src/details/server-panel.tsx @@ -185,11 +185,11 @@ function McpMenu(props: ServiceMenuProps) { if (!server) return if (!input.enabled) { - await client.mcp.disconnect({ server: input.name, location: ref }) + await client.experimental.mcp.disconnect({ server: input.name, location: ref }) } if (input.enabled && server.status.status !== "needs_auth") { - await client.mcp.connect({ server: input.name, location: ref }) + await client.experimental.mcp.connect({ server: input.name, location: ref }) } source.location.mcp.server.invalidate(ref) diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 98c84e52efd8..313b40990677 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -61,6 +61,7 @@ export class GenerationError extends Schema.TaggedError()("Gene export type Endpoint = { readonly group: string + readonly groupPath: ReadonlyArray readonly sourceGroup: string readonly topLevel: boolean readonly endpoint: HttpApiEndpoint.Top @@ -79,6 +80,8 @@ export type Endpoint = { export type Group = { readonly identifier: string + /** Where the group's methods live on the client; empty for top-level groups outside a namespace. */ + readonly path: ReadonlyArray readonly sourceIdentifier: string readonly module: string readonly endpoints: ReadonlyArray @@ -103,6 +106,11 @@ export function compile> readonly omitEndpoints?: ReadonlySet + /** + * Nests an endpoint's group under a client namespace, so `client..` becomes + * `client...`. A group whose name equals the namespace is not repeated. + */ + readonly namespace?: (endpoint: HttpApiEndpoint.Top) => string | undefined }, ): Contract { const endpoints: Array = [] @@ -112,7 +120,21 @@ export function compile item.requiredForClient) if (required !== undefined) { @@ -182,6 +204,7 @@ export function compile endpoint.sourceGroup)).size > 1) { throw new GenerationError({ reason: `Client group name collision: ${identifier}` }) } + const path = endpoints[0].groupPath + if (endpoints.some((endpoint) => endpoint.groupPath.join("\0") !== path.join("\0"))) { + throw new GenerationError({ reason: `Client group path conflict: ${identifier}` }) + } // Module names derive from the group identifier so unrelated groups never rename. const sanitized = identifier.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") const reserved = /^(aux|client|client-error|con|index|nul|prn|com[1-9]|lpt[1-9])$/i.test(sanitized) @@ -234,10 +261,9 @@ export function compile() const groupTypeNames = new Set() const endpointTypeNames = new Set() const operationKeys = new Set() @@ -272,19 +298,19 @@ export function compile item.clientPath[0]))].map((name) => [name]) - : [[group.identifier]] - for (const path of paths) { - const key = path.join("\0") - for (const existing of publicNames) { - if (existing === key || existing.startsWith(`${key}\0`) || key.startsWith(`${existing}\0`)) { - throw new GenerationError({ reason: `Client name collision: ${path.join(".")}` }) - } + } + const visit = (node: GroupNode, path: ReadonlyArray) => { + const keys = new Set() + for (const entry of node.entries) { + const names = typeof entry === "string" ? [entry] : [...new Set(entry.endpoints.map((item) => item.clientPath[0]))] + for (const name of names) { + if (keys.has(name)) throw new GenerationError({ reason: `Client name collision: ${[...path, name].join(".")}` }) + keys.add(name) } - publicNames.add(key) } + for (const [name, child] of node.children) visit(child, [...path, name]) } + visit(groupTree(groups), []) return { groups, } @@ -428,17 +454,22 @@ function renderEffectShape( ) return `${endpoints.join("\n\n")}\n\nexport interface ${groupShapeName(group)} {\n${methods}\n}` }) - const clientFields = groups.flatMap((group) => - group.endpoints[0]?.topLevel - ? [ - renderClientTree( - group.endpoints, - (endpoint) => `${groupShapeTypeName(group, endpoint)}`, - (name, value) => `readonly ${JSON.stringify(name)}: ${value}`, - "\n", - ), - ] - : [`readonly ${JSON.stringify(group.identifier)}: ${groupShapeName(group)}`], + const clientFields = renderGroupTree( + groups, + (group) => + group.endpoints[0]?.topLevel + ? { + fields: renderClientTree( + group.endpoints, + (endpoint) => `${groupShapeTypeName(group, endpoint)}`, + (name, value) => `readonly ${JSON.stringify(name)}: ${value}`, + "\n", + ), + } + : { whole: `${groupShapeName(group)}` }, + (name, value) => `readonly ${JSON.stringify(name)}: ${value}`, + (wholes, fields) => [...wholes, ...(fields.length === 0 ? [] : [`{\n${fields.join("\n")}\n}`])].join(" & "), + "\n", ) return `// Generated by @opencode/httpapi-codegen. Do not edit. import type { Effect, Stream } from "effect" @@ -447,7 +478,7 @@ ${[...imports].join("\n")} ${endpointTypes.join("\n\n")} export interface AppApi { -${clientFields.join("\n")} +${clientFields} } ` } @@ -710,12 +741,15 @@ function renderImportedEffectFiles( ) return `${methods.join("\n\n")}\n\nconst adaptGroup${groupTypeName(group)} = (raw: ${rawGroup}) => ({ ${fields} })` }) - const fields = groups.flatMap((group) => - group.endpoints[0]?.topLevel - ? [`...adaptGroup${groupTypeName(group)}(raw)`] - : [ - `${JSON.stringify(group.identifier)}: adaptGroup${groupTypeName(group)}(raw[${JSON.stringify(group.sourceIdentifier)}])`, - ], + const fields = renderGroupTree( + groups, + (group) => + group.endpoints[0]?.topLevel + ? { fields: `...adaptGroup${groupTypeName(group)}(raw)` } + : { whole: `adaptGroup${groupTypeName(group)}(raw[${JSON.stringify(group.sourceIdentifier)}])` }, + (name, value) => `${JSON.stringify(name)}: ${value}`, + spreadObject, + ", ", ) const usesStream = groups.some((group) => group.endpoints.some((item) => item.operation.success === "stream")) const imported = "api" in options @@ -753,7 +787,7 @@ function renderImportedEffectFiles( options.shapeModule === undefined ? "" : `const preserveEffect = () => (effect: Effect.Effect & ([A] extends [Actual] ? unknown : { readonly __generatedOutputWiderThanContract: [expected: Actual, generated: A] })): Effect.Effect => effect\n${usesStream ? "const preserveStream = () => (stream: Stream.Stream & ([A] extends [Actual] ? unknown : { readonly __generatedOutputWiderThanContract: [expected: Actual, generated: A] })): Stream.Stream => stream\n" : ""}\n` - const client = `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/encoding"\nimport { HttpClientError } from "effect/http"\n${httpApiImport}${imports}\n${shapeImport}import { ClientError } from "./client-error.js"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${preserve}${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` + const client = `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/encoding"\nimport { HttpClientError } from "effect/http"\n${httpApiImport}${imports}\n${shapeImport}import { ClientError } from "./client-error.js"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${preserve}${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` return [ { path: "client-error.ts", @@ -910,7 +944,8 @@ function renderPromiseClient(groups: ReadonlyArray) { return [...(promiseInputMode(endpoint) === "none" ? [] : [`${prefix}Input`]), `${prefix}Output`] }), ) - const fields = groups.map((group) => { + const groupFields = new Map() + for (const group of groups) { const methods = group.endpoints.map((endpoint) => { const prefix = promiseTypePrefix(group.identifier, endpoint.clientPath) const inputMode = promiseInputMode(endpoint) @@ -949,16 +984,24 @@ function renderPromiseClient(groups: ReadonlyArray) { const unwrap = endpoint.unwrapData ? ".then((value) => value.data)" : "" return `(${argument}) => request<${endpoint.unwrapData ? `{ readonly data: ${prefix}Output }` : `${prefix}Output`}>(${descriptor}, requestOptions)${unwrap}` }) - const fields = renderClientTree( - group.endpoints, - (_endpoint, endpointIndex) => methods[endpointIndex], - (name, value) => `${JSON.stringify(name)}: ${value}`, - ", ", + groupFields.set( + group, + renderClientTree( + group.endpoints, + (_endpoint, endpointIndex) => methods[endpointIndex], + (name, value) => `${JSON.stringify(name)}: ${value}`, + ", ", + ), ) - if (group.endpoints[0]?.topLevel) return fields - return `${JSON.stringify(group.identifier)}: { ${fields} }` - }) - return `import type { ${imports.join(", ")} } from "./types.js"\nimport { ClientError } from "./client-error.js"\n\nexport interface ClientOptions {\n readonly baseUrl: string\n readonly fetch?: typeof globalThis.fetch\n readonly headers?: RequestInit["headers"]\n}\n\nexport interface RequestOptions {\n readonly signal?: AbortSignal\n readonly headers?: RequestInit["headers"]\n /** Reports every chunk a streaming response receives, including keepalive comments that yield no event. */\n readonly onActivity?: () => void\n}\n\ninterface RequestDescriptor {\n readonly method: string\n readonly path: string\n readonly query?: Record\n readonly headers?: Record\n readonly body?: unknown\n readonly successStatus: number\n readonly declaredStatuses: ReadonlyArray\n readonly empty: boolean\n}\n\nconst maxSseEventBytes = 16 * 1024 * 1024\n\nexport function make(options: ClientOptions) {\n const fetch = options.fetch ?? globalThis.fetch\n\n const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n // A leading slash would replace any path prefix on baseUrl, so join relative to it.\n const baseUrl = new URL(options.baseUrl)\n if (!baseUrl.pathname.endsWith("/")) baseUrl.pathname += "/"\n const url = new URL(descriptor.path.slice(1), baseUrl)\n for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value)\n const headers = new Headers(options.headers)\n for (const [key, value] of Object.entries(descriptor.headers ?? {})) {\n if (value !== undefined && value !== null) headers.set(key, String(value))\n }\n for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)\n if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")\n return {\n url,\n init: {\n method: descriptor.method,\n signal: requestOptions?.signal,\n headers,\n body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body),\n } satisfies RequestInit,\n }\n }\n\n const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n try {\n const prepared = prepare(descriptor, requestOptions)\n return await fetch(prepared.url, prepared.init)\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n }\n\n const responseError = async (response: Response, descriptor: RequestDescriptor): Promise => {\n if (descriptor.declaredStatuses.includes(response.status)) throw declared(await json(response) as DeclaredErrorBody)\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnexpectedStatus", { cause: { status: response.status }, detail: String(response.status) })\n }\n\n const request = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise => {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) return responseError(response, descriptor)\n if (descriptor.empty) {\n try {\n await response.body?.cancel()\n } catch {}\n return undefined as A\n }\n return await json(response) as A\n }\n\n const sse = (descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable => ({\n async *[Symbol.asyncIterator]() {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) await responseError(response, descriptor)\n if (!isContentType(response, "text/event-stream")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType", { detail: response.headers.get("content-type") })\n }\n if (response.body === null) throw new ClientError("MalformedResponse")\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ""\n try {\n while (true) {\n let next: ReadableStreamReadResult\n try {\n next = await reader.read()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (!next.done) requestOptions?.onActivity?.()\n buffer += decoder.decode(next.value, { stream: !next.done })\n if (buffer.length > maxSseEventBytes) throw new ClientError("SseEventTooLarge")\n const trailingCarriageReturn = !next.done && buffer.endsWith("\\r")\n if (trailingCarriageReturn) buffer = buffer.slice(0, -1)\n buffer = buffer.replaceAll("\\r\\n", "\\n").replaceAll("\\r", "\\n")\n if (trailingCarriageReturn) buffer += "\\r"\n if (next.done && buffer !== "") buffer += "\\n\\n"\n let boundary = buffer.indexOf("\\n\\n")\n while (boundary >= 0) {\n const block = buffer.slice(0, boundary)\n buffer = buffer.slice(boundary + 2)\n const data = block.split("\\n").flatMap((line) => line.startsWith("data:") ? [line.slice(5).trimStart()] : []).join("\\n")\n if (data !== "") {\n try {\n yield JSON.parse(data) as A\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n }\n boundary = buffer.indexOf("\\n\\n")\n }\n if (next.done) return\n }\n } finally {\n try {\n await reader.cancel()\n } catch {}\n reader.releaseLock()\n }\n },\n })\n\n return { ${fields.join(", ")} }\n}\n\nfunction appendQuery(params: URLSearchParams, key: string, value: unknown): void {\n if (value === undefined) return\n if (value === null) {\n params.append(key, "null")\n return\n }\n if (Array.isArray(value)) {\n for (const item of value) appendQuery(params, key, item)\n return\n }\n if (typeof value === "object") {\n for (const [child, item] of Object.entries(value)) appendQuery(params, \`\${key}[\${child}]\`, item)\n return\n }\n params.append(key, String(value))\n}\n\nasync function json(response: Response): Promise {\n if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType", { detail: response.headers.get("content-type") })\n }\n let text: string\n try {\n text = await response.text()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (text === "") throw new ClientError("MalformedResponse")\n try {\n return JSON.parse(text)\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n}\n\ntype DeclaredErrorBody = { readonly _tag?: string; readonly message?: string; readonly data?: { readonly message?: string } }\n\n/** Throw declared error bodies as Errors. The body's fields stay on the error, so narrowing on \`_tag\` or \`name\` still works. */\nfunction declared(body: DeclaredErrorBody) {\n const error = Object.assign(new Error(body.message ?? body.data?.message), body)\n if (body._tag) error.name = body._tag\n return error\n}\n\nfunction isContentType(response: Response, expected: string) {\n return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected\n}\n` + } + const fields = renderGroupTree( + groups, + (group) => ({ fields: groupFields.get(group)! }), + (name, value) => `${JSON.stringify(name)}: ${value}`, + spreadObject, + ", ", + ) + return `import type { ${imports.join(", ")} } from "./types.js"\nimport { ClientError } from "./client-error.js"\n\nexport interface ClientOptions {\n readonly baseUrl: string\n readonly fetch?: typeof globalThis.fetch\n readonly headers?: RequestInit["headers"]\n}\n\nexport interface RequestOptions {\n readonly signal?: AbortSignal\n readonly headers?: RequestInit["headers"]\n /** Reports every chunk a streaming response receives, including keepalive comments that yield no event. */\n readonly onActivity?: () => void\n}\n\ninterface RequestDescriptor {\n readonly method: string\n readonly path: string\n readonly query?: Record\n readonly headers?: Record\n readonly body?: unknown\n readonly successStatus: number\n readonly declaredStatuses: ReadonlyArray\n readonly empty: boolean\n}\n\nconst maxSseEventBytes = 16 * 1024 * 1024\n\nexport function make(options: ClientOptions) {\n const fetch = options.fetch ?? globalThis.fetch\n\n const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n // A leading slash would replace any path prefix on baseUrl, so join relative to it.\n const baseUrl = new URL(options.baseUrl)\n if (!baseUrl.pathname.endsWith("/")) baseUrl.pathname += "/"\n const url = new URL(descriptor.path.slice(1), baseUrl)\n for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value)\n const headers = new Headers(options.headers)\n for (const [key, value] of Object.entries(descriptor.headers ?? {})) {\n if (value !== undefined && value !== null) headers.set(key, String(value))\n }\n for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)\n if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")\n return {\n url,\n init: {\n method: descriptor.method,\n signal: requestOptions?.signal,\n headers,\n body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body),\n } satisfies RequestInit,\n }\n }\n\n const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n try {\n const prepared = prepare(descriptor, requestOptions)\n return await fetch(prepared.url, prepared.init)\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n }\n\n const responseError = async (response: Response, descriptor: RequestDescriptor): Promise => {\n if (descriptor.declaredStatuses.includes(response.status)) throw declared(await json(response) as DeclaredErrorBody)\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnexpectedStatus", { cause: { status: response.status }, detail: String(response.status) })\n }\n\n const request = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise => {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) return responseError(response, descriptor)\n if (descriptor.empty) {\n try {\n await response.body?.cancel()\n } catch {}\n return undefined as A\n }\n return await json(response) as A\n }\n\n const sse = (descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable => ({\n async *[Symbol.asyncIterator]() {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) await responseError(response, descriptor)\n if (!isContentType(response, "text/event-stream")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType", { detail: response.headers.get("content-type") })\n }\n if (response.body === null) throw new ClientError("MalformedResponse")\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ""\n try {\n while (true) {\n let next: ReadableStreamReadResult\n try {\n next = await reader.read()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (!next.done) requestOptions?.onActivity?.()\n buffer += decoder.decode(next.value, { stream: !next.done })\n if (buffer.length > maxSseEventBytes) throw new ClientError("SseEventTooLarge")\n const trailingCarriageReturn = !next.done && buffer.endsWith("\\r")\n if (trailingCarriageReturn) buffer = buffer.slice(0, -1)\n buffer = buffer.replaceAll("\\r\\n", "\\n").replaceAll("\\r", "\\n")\n if (trailingCarriageReturn) buffer += "\\r"\n if (next.done && buffer !== "") buffer += "\\n\\n"\n let boundary = buffer.indexOf("\\n\\n")\n while (boundary >= 0) {\n const block = buffer.slice(0, boundary)\n buffer = buffer.slice(boundary + 2)\n const data = block.split("\\n").flatMap((line) => line.startsWith("data:") ? [line.slice(5).trimStart()] : []).join("\\n")\n if (data !== "") {\n try {\n yield JSON.parse(data) as A\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n }\n boundary = buffer.indexOf("\\n\\n")\n }\n if (next.done) return\n }\n } finally {\n try {\n await reader.cancel()\n } catch {}\n reader.releaseLock()\n }\n },\n })\n\n return { ${fields} }\n}\n\nfunction appendQuery(params: URLSearchParams, key: string, value: unknown): void {\n if (value === undefined) return\n if (value === null) {\n params.append(key, "null")\n return\n }\n if (Array.isArray(value)) {\n for (const item of value) appendQuery(params, key, item)\n return\n }\n if (typeof value === "object") {\n for (const [child, item] of Object.entries(value)) appendQuery(params, \`\${key}[\${child}]\`, item)\n return\n }\n params.append(key, String(value))\n}\n\nasync function json(response: Response): Promise {\n if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType", { detail: response.headers.get("content-type") })\n }\n let text: string\n try {\n text = await response.text()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (text === "") throw new ClientError("MalformedResponse")\n try {\n return JSON.parse(text)\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n}\n\ntype DeclaredErrorBody = { readonly _tag?: string; readonly message?: string; readonly data?: { readonly message?: string } }\n\n/** Throw declared error bodies as Errors. The body's fields stay on the error, so narrowing on \`_tag\` or \`name\` still works. */\nfunction declared(body: DeclaredErrorBody) {\n const error = Object.assign(new Error(body.message ?? body.data?.message), body)\n if (body._tag) error.name = body._tag\n return error\n}\n\nfunction isContentType(response: Response, expected: string) {\n return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected\n}\n` } function promiseTypePrefix(group: string, path: ReadonlyArray) { @@ -1025,6 +1068,68 @@ function renderClientTree( return render(root) } +// A node of the public client object: groups whose methods sit at this path, and nested +// namespaces, kept in declaration order so adding a group never reorders unrelated output. +type GroupNode = { + readonly entries: Array + readonly children: Map +} + +function groupTree(groups: ReadonlyArray) { + const root: GroupNode = { entries: [], children: new Map() } + for (const group of groups) { + const node = group.path.reduce((parent, name) => { + const existing = parent.children.get(name) + if (existing !== undefined) return existing + const child: GroupNode = { entries: [], children: new Map() } + parent.children.set(name, child) + parent.entries.push(name) + return child + }, root) + node.entries.push(group) + } + return root +} + +/** + * Renders the client object from its groups. A group either renders as one whole value (a + * reference or call) or as fields; a node holding exactly one whole and nothing else collapses + * to that whole, anything else goes through `combine`. + */ +function renderGroupTree( + groups: ReadonlyArray, + member: (group: Group) => { readonly whole: string } | { readonly fields: string }, + field: (name: string, value: string) => string, + combine: (wholes: ReadonlyArray, fields: ReadonlyArray) => string, + separator: string, +) { + const parts = (node: GroupNode) => { + const wholes: Array = [] + const fields: Array = [] + for (const entry of node.entries) { + if (typeof entry === "string") { + fields.push(field(entry, value(node.children.get(entry)!))) + continue + } + const rendered = member(entry) + if ("whole" in rendered) wholes.push(rendered.whole) + else if (rendered.fields !== "") fields.push(rendered.fields) + } + return { wholes, fields } + } + const value = (node: GroupNode): string => { + const { wholes, fields } = parts(node) + return wholes.length === 1 && fields.length === 0 ? wholes[0] : combine(wholes, fields) + } + const root = parts(groupTree(groups)) + if (root.wholes.length > 0) throw new GenerationError({ reason: "Client root cannot hold a nested group" }) + return root.fields.join(separator) +} + +function spreadObject(wholes: ReadonlyArray, fields: ReadonlyArray) { + return `{ ${[...wholes.map((whole) => `...${whole}`), ...fields].join(", ")} }` +} + function identifierPart(value: string) { const identifier = value .split(/[^A-Za-z0-9]+/) @@ -1948,14 +2053,18 @@ function renderClient(groups: ReadonlyArray) { ) .join("\n") const api = `HttpApi.make("generated")${groups.map((group) => `.add(Group${groupTypeName(group)})`).join("")}` - const fields = groups.flatMap((group) => { - if (!group.endpoints[0]?.topLevel) { - return [ - `${JSON.stringify(group.identifier)}: adaptGroup${groupTypeName(group)}(raw[${JSON.stringify(group.identifier)}])`, - ] - } - const raw = `{ ${group.endpoints.map((item) => `${JSON.stringify(item.endpoint.identifier)}: raw[${JSON.stringify(item.endpoint.identifier)}]`).join(", ")} }` - return [`...adaptGroup${groupTypeName(group)}(${raw})`] - }) - return `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect } from "effect"\nimport { HttpApi, HttpApiClient } from "effect/http-api"\n${imports}\n\nconst Api = ${api}\nconst adaptClient = (raw: HttpApiClient.ForApi) => ({ ${fields.join(", ")} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) =>\n HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))\n` + const fields = renderGroupTree( + groups, + (group) => { + if (!group.endpoints[0]?.topLevel) { + return { whole: `adaptGroup${groupTypeName(group)}(raw[${JSON.stringify(group.identifier)}])` } + } + const raw = `{ ${group.endpoints.map((item) => `${JSON.stringify(item.endpoint.identifier)}: raw[${JSON.stringify(item.endpoint.identifier)}]`).join(", ")} }` + return { fields: `...adaptGroup${groupTypeName(group)}(${raw})` } + }, + (name, value) => `${JSON.stringify(name)}: ${value}`, + spreadObject, + ", ", + ) + return `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect } from "effect"\nimport { HttpApi, HttpApiClient } from "effect/http-api"\n${imports}\n\nconst Api = ${api}\nconst adaptClient = (raw: HttpApiClient.ForApi) => ({ ${fields} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) =>\n HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))\n` } diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index f766e7b6fe96..d2570a8044d0 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -393,6 +393,79 @@ describe("HttpApiCodegen.generate", () => { expect(contract.groups[0]?.endpoints[0]?.operation).toMatchObject({ group: "sessions", name: "get" }) }) + test("nests namespaced endpoints and merges them with a group of the same name", async () => { + const source = HttpApi.make("test") + .add( + HttpApiGroup.make("server.session") + .add(HttpApiEndpoint.get("get", "/session", { success: Schema.String })) + .add(HttpApiEndpoint.get("stats", "/experimental/session/stats", { success: Schema.String })), + ) + .add( + HttpApiGroup.make("server.experimental").add( + HttpApiEndpoint.get("read", "/experimental/terminal", { success: Schema.String }).annotateMerge( + OpenApi.annotations({ identifier: "server.experimental.terminal.read" }), + ), + ), + ) + const contract = compileContract(source, { + groupNames: { "server.session": "session", "server.experimental": "experimental" }, + namespace: (endpoint) => (endpoint.path.startsWith("/experimental/") ? "experimental" : undefined), + }) + + expect(contract.groups.map((group) => [group.identifier, group.path])).toEqual([ + ["session", ["session"]], + ["experimental.session", ["experimental", "session"]], + ["experimental", ["experimental"]], + ]) + + const paths: Array = [] + await using emitted = await emittedModule(emitPromise(contract)) + const client = emitted.module.OpenCode.make({ + baseUrl: "https://example.com", + fetch: async (input: RequestInfo | URL) => { + paths.push(new URL(input instanceof Request ? input.url : input).pathname) + return Response.json("ok") + }, + }) + expect(Object.keys(client)).toEqual(["session", "experimental"]) + expect(Object.keys(client.session)).toEqual(["get"]) + expect(Object.keys(client.experimental)).toEqual(["session", "terminal"]) + expect(await client.session.get()).toBe("ok") + expect(await client.experimental.session.stats()).toBe("ok") + expect(await client.experimental.terminal.read()).toBe("ok") + expect(paths).toEqual(["/session", "/experimental/session/stats", "/experimental/terminal"]) + + const imported = emitEffectImported(contract, { module: "@example/api", api: "Api" }) + expect(imported.files.find((file) => file.path === "client.ts")?.content).toContain( + '"experimental": { ...adaptGroupExperimental(raw["server.experimental"]), "session": adaptGroupExperimentalSession(raw["server.session"]) }', + ) + const shape = emitEffectShape(contract).files.find((file) => file.path === "api.ts")?.content + expect(shape).toContain('readonly "session": SessionApi') + expect(shape).toContain( + 'readonly "experimental": ExperimentalApi & {\nreadonly "session": ExperimentalSessionApi\n}', + ) + }) + + test("rejects a namespaced group that collides with a method in the namespace", () => { + const source = HttpApi.make("test") + .add( + HttpApiGroup.make("experimental").add( + HttpApiEndpoint.get("read", "/experimental/session", { success: Schema.String }).annotateMerge( + OpenApi.annotations({ identifier: "experimental.session.read" }), + ), + ), + ) + .add( + HttpApiGroup.make("session").add( + HttpApiEndpoint.get("stats", "/experimental/stats", { success: Schema.String }), + ), + ) + + expect(() => compileContract(source, { namespace: () => "experimental" })).toThrow( + "Client name collision: experimental.session", + ) + }) + test("derives nested paths from OpenAPI operation IDs", () => { const source = HttpApi.make("test").add( HttpApiGroup.make("server.session").add( diff --git a/packages/plugin/src/effect/plugin.ts b/packages/plugin/src/effect/plugin.ts index 3ed3b699911d..b84c2e924209 100644 --- a/packages/plugin/src/effect/plugin.ts +++ b/packages/plugin/src/effect/plugin.ts @@ -1,4 +1,4 @@ -import type { CredentialApi, ExperimentalApi, GenerateApi, PluginApi } from "@opencode/client/effect/api" +import type { CredentialApi, ExperimentalApi, ExperimentalGenerateApi, PluginApi } from "@opencode/client/effect/api" import type { Location } from "@opencode/schema/location" import type { Effect, Scope } from "effect" import type { PluginOptions } from "../options.js" @@ -38,7 +38,7 @@ export interface Context { readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain - readonly generate: GenerateApi + readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick, "list"> readonly provider: ProviderDomain diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index 3f5708229f94..576f6b29ce0d 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -1,4 +1,4 @@ -import type { SessionApi } from "@opencode/client/effect/api" +import type { ExperimentalSessionApi, SessionApi } from "@opencode/client/effect/api" import type { GenerationOptionsFields, Message, SystemPart } from "@opencode/ai/effect" import type { Agent } from "@opencode/schema/agent" import type { Model } from "@opencode/schema/model" @@ -167,8 +167,8 @@ export type SessionDomain = Pick< | "interrupt" | "update" | "move" - | "wait" | "context" -> & { +> & + Pick, "wait"> & { readonly hook: ModelHooks } diff --git a/packages/plugin/src/promise/plugin.ts b/packages/plugin/src/promise/plugin.ts index 3bb8ddf4effa..e059da892550 100644 --- a/packages/plugin/src/promise/plugin.ts +++ b/packages/plugin/src/promise/plugin.ts @@ -1,5 +1,5 @@ import type { OpenCodeClient } from "@opencode/client" -import type { CredentialApi, GenerateApi, PluginApi } from "@opencode/client/promise/api" +import type { CredentialApi, ExperimentalGenerateApi, PluginApi } from "@opencode/client/promise/api" import type { Location } from "@opencode/schema/location" import type { PluginOptions } from "../options.js" import type { App } from "../app.js" @@ -38,7 +38,7 @@ export interface Context { readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain - readonly generate: GenerateApi + readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick readonly provider: ProviderDomain diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index 64684d665269..c5c4272cb5f4 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -1,4 +1,4 @@ -import type { SessionApi } from "@opencode/client/promise/api" +import type { ExperimentalSessionApi, SessionApi } from "@opencode/client/promise/api" import type { GenerationOptionsFields, Message, SystemPart } from "@opencode/ai/effect" import type { Agent } from "@opencode/schema/agent" import type { Model } from "@opencode/schema/model" @@ -167,8 +167,8 @@ export type SessionDomain = Pick< | "interrupt" | "update" | "move" - | "wait" | "context" -> & { +> & + Pick & { readonly hook: ModelHooks } diff --git a/packages/protocol/src/client.ts b/packages/protocol/src/client.ts index 57f1cb6c7c26..3a25f6188bc4 100644 --- a/packages/protocol/src/client.ts +++ b/packages/protocol/src/client.ts @@ -68,6 +68,12 @@ export const groupNames = { "server.config": "config", } as const +// Experimental routes carry no compatibility promise, so the generated clients nest them under +// `client.experimental` instead of beside stable methods. The URL decides rather than the OpenAPI +// id, because the URL is what callers depend on and an id can miss its `experimental.` prefix. +export const clientNamespace = (endpoint: { readonly path: string }) => + endpoint.path === "/api/experimental" || endpoint.path.startsWith("/api/experimental/") ? "experimental" : undefined + export const promiseOmitEndpoints = new Set(["pty.connect", "persistentPty.connect", "mcp.client"]) export const effectOmitEndpoints = new Set([ "fs.read", diff --git a/packages/sdk/test/contract-identity.test.ts b/packages/sdk/test/contract-identity.test.ts index 6149bb05fa8e..fec447b281f3 100644 --- a/packages/sdk/test/contract-identity.test.ts +++ b/packages/sdk/test/contract-identity.test.ts @@ -19,7 +19,7 @@ import { SessionMessage } from "@opencode/schema/session-message" import { Workspace } from "@opencode/schema/workspace" import { Worktree } from "@opencode/schema/worktree" import { Api } from "@opencode/server/api" -import { ClientApi, groupNames, promiseOmitEndpoints } from "@opencode/protocol/client" +import { ClientApi, clientNamespace, groupNames, promiseOmitEndpoints } from "@opencode/protocol/client" import { compile, emitPromise } from "@opencode/httpapi-codegen" const SDK = await import("../src/index") @@ -97,8 +97,8 @@ test("Core and Server reuse the authoritative Schema and Protocol values", () => }) test("client and Server contracts generate identically", () => { - const server = compile(Api, { groupNames, omitEndpoints: promiseOmitEndpoints }) - const client = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints }) + const server = compile(Api, { groupNames, omitEndpoints: promiseOmitEndpoints, namespace: clientNamespace }) + const client = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints, namespace: clientNamespace }) expect(emitPromise(client)).toEqual(emitPromise(server)) }) diff --git a/packages/sdk/test/embedded.test.ts b/packages/sdk/test/embedded.test.ts index 3c2e44740616..2f53f805b2d2 100644 --- a/packages/sdk/test/embedded.test.ts +++ b/packages/sdk/test/embedded.test.ts @@ -68,7 +68,7 @@ for (const selection of ["explicit", "default"] as const) { }, ) - const result = yield* opencode.generate.text({ + const result = yield* opencode.experimental.generate.text({ prompt: "Say ready", ...(selection === "explicit" ? { @@ -137,7 +137,7 @@ it.live("seeds host plugins from CreateOptions and waits for delayed model plugi ) const [defaultModel, generated] = yield* Effect.all( - [opencode.model.default({ location: location(fixture) }), opencode.generate.text({ prompt: "Say ready" })], + [opencode.model.default({ location: location(fixture) }), opencode.experimental.generate.text({ prompt: "Say ready" })], { concurrency: "unbounded" }, ) @@ -223,16 +223,16 @@ it.live( }) const context = yield* opencode.sessions.context({ sessionID: id }) const pendingAfterAdmit = yield* opencode.sessions.inbox.list({ sessionID: id }) - yield* opencode.sessions.instructions.entry.put({ sessionID: id, key: "deploy-target", value: "production" }) - yield* opencode.sessions.instructions.entry.put({ sessionID: id, key: "flags", value: { beta: true } }) - const contextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id }) - yield* opencode.sessions.instructions.entry.remove({ sessionID: id, key: "flags" }) - const remainingContextEntries = yield* opencode.sessions.instructions.entry.list({ sessionID: id }) + yield* opencode.experimental.session.instructions.entry.put({ sessionID: id, key: "deploy-target", value: "production" }) + yield* opencode.experimental.session.instructions.entry.put({ sessionID: id, key: "flags", value: { beta: true } }) + const contextEntries = yield* opencode.experimental.session.instructions.entry.list({ sessionID: id }) + yield* opencode.experimental.session.instructions.entry.remove({ sessionID: id, key: "flags" }) + const remainingContextEntries = yield* opencode.experimental.session.instructions.entry.list({ sessionID: id }) const wake = yield* opencode.sessions.prompt({ sessionID: id, text: "Promote this input", }) - const prompted = yield* opencode.sessions.log({ sessionID: id, follow: true }).pipe( + const prompted = yield* opencode.experimental.session.log({ sessionID: id, follow: true }).pipe( Stream.filter((event) => event.type === "session.inbox.delivered" && event.data.inboxID === wake.id), Stream.runHead, Effect.timeout("10 seconds"), @@ -240,7 +240,7 @@ it.live( ) const wakeContext = yield* opencode.sessions.context({ sessionID: id }) const pendingAfterPromote = yield* opencode.sessions.inbox.list({ sessionID: id }) - const event = yield* opencode.sessions.log({ sessionID: id }).pipe( + const event = yield* opencode.experimental.session.log({ sessionID: id }).pipe( Stream.filter((item) => item.type === "session.model.selected"), Stream.take(1), Stream.runHead, @@ -255,10 +255,10 @@ it.live( const missingSessionID = fixture.sdk.Session.ID.create() const missing = yield* Effect.all( [ - opencode.sessions.log({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip), + opencode.experimental.session.log({ sessionID: missingSessionID }).pipe(Stream.runHead, Effect.flip), opencode.sessions.interrupt({ sessionID: missingSessionID }).pipe(Effect.flip), opencode.sessions.message.get({ sessionID: missingSessionID, messageID: modelMessage.id }).pipe(Effect.flip), - opencode.sessions.instructions.entry.list({ sessionID: missingSessionID }).pipe(Effect.flip), + opencode.experimental.session.instructions.entry.list({ sessionID: missingSessionID }).pipe(Effect.flip), opencode.sessions.inbox.list({ sessionID: missingSessionID }).pipe(Effect.flip), ], { concurrency: "unbounded" }, @@ -635,7 +635,7 @@ it.live( expect(calls).toEqual(["create"]) yield* Deferred.succeed(createRelease, undefined) - yield* opencode.sessions.wait({ sessionID: session.id }) + yield* opencode.experimental.session.wait({ sessionID: session.id }) // Provisioning settled, the workspace connected, and the turn completed. The // memory driver rejects the actual spawn, which surfaces to the model as an // ordinary tool error before the final text response. diff --git a/packages/sdk/test/instances-effect.test.ts b/packages/sdk/test/instances-effect.test.ts index 15515d510b4c..c47d9a1d3254 100644 --- a/packages/sdk/test/instances-effect.test.ts +++ b/packages/sdk/test/instances-effect.test.ts @@ -126,7 +126,7 @@ it.live( expect((yield* llm.requests()).length).toBe(1) yield* Deferred.succeed(release, undefined) yield* llm.wait(3).pipe(Effect.timeout("5 seconds")) - yield* second.sessions.wait({ sessionID: session.id }) + yield* second.experimental.session.wait({ sessionID: session.id }) expect(configured).toEqual(["thread-recovery", "thread-recovery"]) expect(executed).toEqual([2]) @@ -215,7 +215,7 @@ it.live( for (const session of [first, second]) { const admitted = yield* opencode.sessions.prompt({ sessionID: session.id, text: "Hello" }) expect(admitted.payload.text).toBe(`Hello:${session.location.workspaceID}`) - yield* opencode.sessions.wait({ sessionID: session.id }) + yield* opencode.experimental.session.wait({ sessionID: session.id }) } expect(configured).toEqual(["same-thread", "same-thread"]) expect(placements.map((location) => location.workspaceID)).toEqual([firstWorkspace, secondWorkspace]) @@ -225,7 +225,7 @@ it.live( directory: AbsolutePath.make(directory.path), workspaceID: secondWorkspace, }) - yield* opencode.sessions.wait({ sessionID: first.id }) + yield* opencode.experimental.session.wait({ sessionID: first.id }) const moved = yield* opencode.sessions.get({ sessionID: first.id }) expect(moved.location.workspaceID).toBe(secondWorkspace) const admitted = yield* opencode.sessions.prompt({ sessionID: first.id, text: "Moved", resume: false }) diff --git a/packages/sdk/test/promise.test.ts b/packages/sdk/test/promise.test.ts index 1671906a24be..a389ac25372c 100644 --- a/packages/sdk/test/promise.test.ts +++ b/packages/sdk/test/promise.test.ts @@ -18,7 +18,7 @@ test("Promise host uses the embedded router", async () => { const session = await opencode.sessions.create({ location }) const selected = await opencode.sessions.get({ sessionID: session.id }) const page = await opencode.sessions.list({ directory: directory.path }) - const events = Array.fromAsync(opencode.sessions.log({ sessionID: session.id })) + const events = Array.fromAsync(opencode.experimental.session.log({ sessionID: session.id })) expect(selected.id).toBe(session.id) expect(page.data.some((item) => item.id === session.id)).toBe(true) diff --git a/packages/sdk/test/transport.test.ts b/packages/sdk/test/transport.test.ts index 6955fcdb552c..bdf4daa94b57 100644 --- a/packages/sdk/test/transport.test.ts +++ b/packages/sdk/test/transport.test.ts @@ -40,14 +40,14 @@ for (const entrypoint of ["create", "layer"] as const) { location: Location.Ref.make({ directory: AbsolutePath.make(directory.path) }), }) expect((yield* client.sessions.get({ sessionID: session.id })).id).toBe(session.id) - const events = yield* client.sessions.log({ sessionID: session.id }).pipe(Stream.runCollect) + const events = yield* client.experimental.session.log({ sessionID: session.id }).pipe(Stream.runCollect) expect(events.some((event) => event.type === "session.created")).toBe(true) expect(yield* client.events.subscribe().pipe(Stream.take(1), Stream.runCollect)).toMatchObject([ { type: "server.connected" }, ]) const missing = Session.ID.create() // Error responses must decode over the SDK transport too; sessions.get is served by core without HTTP. - expect(yield* client.sessions.export({ sessionID: missing }).pipe(Effect.flip)).toMatchObject({ + expect(yield* client.experimental.session.export({ sessionID: missing }).pipe(Effect.flip)).toMatchObject({ _tag: "SessionNotFoundError", }) expect(yield* client.sessions.get({ sessionID: missing }).pipe(Effect.flip)).toMatchObject({ diff --git a/packages/server/test/message.test.ts b/packages/server/test/message.test.ts index 2899f87d03f2..84957e4f2934 100644 --- a/packages/server/test/message.test.ts +++ b/packages/server/test/message.test.ts @@ -48,7 +48,7 @@ const setup = Effect.gen(function* () { }) const session = yield* Effect.promise(async () => { const template = await api.session.create({ title: "Message filtering" }) - return api.session.import({ info: { ...template, id: Session.ID.create() }, messages }) + return api.experimental.session.import({ info: { ...template, id: Session.ID.create() }, messages }) }) return { api, handler, sessionID: session.id } }) diff --git a/packages/server/test/sse-failure.test.ts b/packages/server/test/sse-failure.test.ts index df5c19ef5e88..adf5622f5da4 100644 --- a/packages/server/test/sse-failure.test.ts +++ b/packages/server/test/sse-failure.test.ts @@ -58,7 +58,7 @@ it.live("the generated Effect client fails the session log stream with the serve const received: unknown[] = [] const exit = yield* Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://opencode.local" }) - return yield* client.session + return yield* client.experimental.session .log({ sessionID: Session.ID.make("ses_test") }) .pipe(Stream.runForEach((item) => Effect.sync(() => received.push(item)))) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.exit) diff --git a/packages/tui/src/component/devtools-bar.tsx b/packages/tui/src/component/devtools-bar.tsx index 10af2019d600..a80163edea66 100644 --- a/packages/tui/src/component/devtools-bar.tsx +++ b/packages/tui/src/component/devtools-bar.tsx @@ -162,7 +162,7 @@ export function DevToolsBar() { const events = await (sessionID ? (async () => { const events: { readonly created: number }[] = [] - for await (const event of client.api.session.log({ sessionID, follow: false })) { + for await (const event of client.api.experimental.session.log({ sessionID, follow: false })) { if (event.type !== "log.synced") events.push(event) } // Durable events stay in aggregate order even when their wall-clock timestamps differ. diff --git a/packages/tui/src/component/dialog-mcp.tsx b/packages/tui/src/component/dialog-mcp.tsx index 68e3778b42ff..a8241623fd13 100644 --- a/packages/tui/src/component/dialog-mcp.tsx +++ b/packages/tui/src/component/dialog-mcp.tsx @@ -121,7 +121,7 @@ export function DialogMcp(props: { initialServer?: string; details?: boolean } = setLoading((prev) => new Set(prev).add(name)) const target = current() const input = { server: name, location: { directory: target.directory } } - const call = server.status.status === "connected" ? client.api.mcp.disconnect(input) : client.api.mcp.connect(input) + const call = server.status.status === "connected" ? client.api.experimental.mcp.disconnect(input) : client.api.experimental.mcp.connect(input) void call.catch(toast.error).finally(() => setLoading((prev) => { const next = new Set(prev) diff --git a/packages/tui/src/component/migration-overlay.tsx b/packages/tui/src/component/migration-overlay.tsx index fc73e36b853a..3f6676fd1bad 100644 --- a/packages/tui/src/component/migration-overlay.tsx +++ b/packages/tui/src/component/migration-overlay.tsx @@ -19,7 +19,7 @@ export function MigrationOverlay() { await Bun.sleep(1_000) void (async () => { while (true) { - const status = await client.api.migration.v1.status({ signal: abort.signal }) + const status = await client.api.experimental.migration.v1.status({ signal: abort.signal }) setProgress(status.status === "running" ? status.progress : undefined) if (status.status === "completed") return if (status.status === "error") throw new Error(status.error) diff --git a/packages/tui/src/feature-plugins/system/stats.tsx b/packages/tui/src/feature-plugins/system/stats.tsx index 950d322422dc..09d79e426bb5 100644 --- a/packages/tui/src/feature-plugins/system/stats.tsx +++ b/packages/tui/src/feature-plugins/system/stats.tsx @@ -140,7 +140,7 @@ export function StatsPoster(props: { stats: SessionStatsInfo }) { function StatsPage(props: { context: Plugin.Context; onClose: () => void }) { const [result] = createResource(() => { const now = new Date() - return props.context.client.session.stats({ + return props.context.client.experimental.session.stats({ from: new Date(now.getFullYear(), 0, 1).getTime(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", tools: "none", diff --git a/packages/tui/src/mini/stream-v2.transport.ts b/packages/tui/src/mini/stream-v2.transport.ts index 52117b21d55c..4d2f84e41d68 100644 --- a/packages/tui/src/mini/stream-v2.transport.ts +++ b/packages/tui/src/mini/stream-v2.transport.ts @@ -911,7 +911,7 @@ export async function createSessionTransport(input: StreamInput): Promise ok(input.active?.() ?? {})) spyOn(client.session.inbox, "list").mockImplementation((request) => ok(input.pending?.[request.sessionID] ?? [])) - spyOn(client.session, "wait").mockImplementation(() => input.wait?.() ?? ok(undefined)) + spyOn(client.experimental.session, "wait").mockImplementation(() => input.wait?.() ?? ok(undefined)) spyOn(client.session.message, "get").mockImplementation((request) => { const message = input.messages?.[request.sessionID]?.find((item) => item.id === request.messageID) return message ? (ok(message) as never) : Promise.reject(new Error(`message not found: ${request.messageID}`)) From e4fdecafe6369788b25744a30364fc12b338bdfa Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 25 Oct 2026 15:17:54 -0700 Subject: [PATCH 02/11] test(httpapi-codegen): cover namespaced groups in the effect client --- packages/httpapi-codegen/test/generate.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index d2570a8044d0..96340a3b6209 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -435,6 +435,9 @@ describe("HttpApiCodegen.generate", () => { expect(await client.experimental.terminal.read()).toBe("ok") expect(paths).toEqual(["/session", "/experimental/session/stats", "/experimental/terminal"]) + expect(emitEffect(contract).files.find((file) => file.path === "client.ts")?.content).toContain( + '"experimental": { ...adaptGroupExperimental(raw["experimental"]), "session": adaptGroupExperimentalSession(raw["experimental.session"]) }', + ) const imported = emitEffectImported(contract, { module: "@example/api", api: "Api" }) expect(imported.files.find((file) => file.path === "client.ts")?.content).toContain( '"experimental": { ...adaptGroupExperimental(raw["server.experimental"]), "session": adaptGroupExperimentalSession(raw["server.session"]) }', From 3ff1e46a31326c473fa8adaa15ab40d16f8d7606 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 25 Oct 2026 15:46:25 -0700 Subject: [PATCH 03/11] fix(httpapi-codegen): key projected raw clients by client group name; update docs for experimental namespace --- packages/httpapi-codegen/src/index.ts | 6 ++++-- packages/httpapi-codegen/test/generate.test.ts | 11 +++++++++++ packages/plugin/src/effect/integration.ts | 2 +- packages/plugin/src/promise/integration.ts | 2 +- services/www/src/docs/content/build/client/effect.mdx | 2 +- .../www/src/docs/content/build/plugins/effect.mdx | 8 ++++---- services/www/src/docs/content/build/plugins/index.mdx | 2 +- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 313b40990677..7ad9e750bc4d 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -680,8 +680,10 @@ function renderImportedEffectFiles( readonly shapeModule?: string }, ): Output["files"] { + // An imported Api or group keeps its source group names; a projection rebuilds groups under their client names. + const rawKey = (group: Group) => JSON.stringify("endpoints" in options ? group.identifier : group.sourceIdentifier) const adapters = groups.map((group) => { - const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${JSON.stringify(group.sourceIdentifier)}]` + const rawGroup = group.endpoints[0]?.topLevel ? "RawClient" : `RawClient[${rawKey(group)}]` const methods = group.endpoints.map((item) => { const prefix = endpointTypeName(group, item) const adapter = endpointAdapterName(group, item) @@ -746,7 +748,7 @@ function renderImportedEffectFiles( (group) => group.endpoints[0]?.topLevel ? { fields: `...adaptGroup${groupTypeName(group)}(raw)` } - : { whole: `adaptGroup${groupTypeName(group)}(raw[${JSON.stringify(group.sourceIdentifier)}])` }, + : { whole: `adaptGroup${groupTypeName(group)}(raw[${rawKey(group)}])` }, (name, value) => `${JSON.stringify(name)}: ${value}`, spreadObject, ", ", diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 96340a3b6209..64fd453e1083 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -442,6 +442,17 @@ describe("HttpApiCodegen.generate", () => { expect(imported.files.find((file) => file.path === "client.ts")?.content).toContain( '"experimental": { ...adaptGroupExperimental(raw["server.experimental"]), "session": adaptGroupExperimentalSession(raw["server.session"]) }', ) + // A projection rebuilds the split group under its client name, so the raw client is keyed by that name. + const projected = emitEffectImported(contract, { + module: "@example/api", + endpoints: { "session.get": "Get", "experimental.session.stats": "Stats", "experimental.read": "Read" }, + }).files.find((file) => file.path === "client.ts")?.content + expect(projected).toContain('HttpApiGroup.make("experimental.session").add(Stats)') + expect(projected).toContain('const adaptGroupExperimentalSession = (raw: RawClient["experimental.session"])') + expect(projected).toContain( + '"experimental": { ...adaptGroupExperimental(raw["experimental"]), "session": adaptGroupExperimentalSession(raw["experimental.session"]) }', + ) + expect(projected).not.toContain('raw["server.') const shape = emitEffectShape(contract).files.find((file) => file.path === "api.ts")?.content expect(shape).toContain('readonly "session": SessionApi') expect(shape).toContain( diff --git a/packages/plugin/src/effect/integration.ts b/packages/plugin/src/effect/integration.ts index dea0f616781b..c6a0b5eeadec 100644 --- a/packages/plugin/src/effect/integration.ts +++ b/packages/plugin/src/effect/integration.ts @@ -99,7 +99,7 @@ export interface IntegrationEditor { } } -export interface IntegrationDomain extends Omit, "wellknown"> { +export interface IntegrationDomain extends IntegrationApi { readonly transform: Transform readonly reload: () => Effect.Effect readonly connection: { diff --git a/packages/plugin/src/promise/integration.ts b/packages/plugin/src/promise/integration.ts index 630b391826a6..22d6b66abf1b 100644 --- a/packages/plugin/src/promise/integration.ts +++ b/packages/plugin/src/promise/integration.ts @@ -91,7 +91,7 @@ export interface IntegrationEditor { } } -export interface IntegrationDomain extends Omit { +export interface IntegrationDomain extends IntegrationApi { readonly transform: Transform readonly reload: () => Promise readonly connection: { diff --git a/services/www/src/docs/content/build/client/effect.mdx b/services/www/src/docs/content/build/client/effect.mdx index 9358c63e72a0..938f2941d4d9 100644 --- a/services/www/src/docs/content/build/client/effect.mdx +++ b/services/www/src/docs/content/build/client/effect.mdx @@ -58,7 +58,7 @@ const sessions = yield* client.session.list() ## Stream events -Streaming operations such as `event.subscribe()` and `session.log()` return Effect Streams. +Streaming operations such as `event.subscribe()` and `experimental.session.log()` return Effect Streams. {/* prettier-ignore */} ```ts diff --git a/services/www/src/docs/content/build/plugins/effect.mdx b/services/www/src/docs/content/build/plugins/effect.mdx index 329f732a2166..b5cabcadad89 100644 --- a/services/www/src/docs/content/build/plugins/effect.mdx +++ b/services/www/src/docs/content/build/plugins/effect.mdx @@ -126,7 +126,7 @@ interface Context { readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain - readonly generate: GenerateApi + readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick, "list"> readonly provider: ProviderDomain @@ -629,7 +629,7 @@ interface IntegrationEditor { } } -interface IntegrationDomain extends Omit, "wellknown"> { +interface IntegrationDomain extends IntegrationApi { readonly transform: Transform readonly reload: () => Effect.Effect readonly connection: { @@ -879,9 +879,9 @@ type SessionDomain = Pick< | "interrupt" | "update" | "move" - | "wait" | "context" -> +> & + Pick, "wait"> ``` ### Skills diff --git a/services/www/src/docs/content/build/plugins/index.mdx b/services/www/src/docs/content/build/plugins/index.mdx index 102b4a97448c..f9d1fc52110f 100644 --- a/services/www/src/docs/content/build/plugins/index.mdx +++ b/services/www/src/docs/content/build/plugins/index.mdx @@ -858,7 +858,7 @@ interface SessionContext { compact(input: SessionCompactInput, requestOptions?: RequestOptions): Promise synthetic(input: SessionSyntheticInput, requestOptions?: RequestOptions): Promise interrupt(input: SessionInterruptInput, requestOptions?: RequestOptions): Promise<{ interrupted: boolean }> - wait(input: SessionWaitInput, requestOptions?: RequestOptions): Promise + wait(input: ExperimentalSessionWaitInput, requestOptions?: RequestOptions): Promise } ``` From ee633628158ebfd051767faccf0b088d8eb9e45c Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sun, 25 Oct 2026 17:09:17 -0700 Subject: [PATCH 04/11] chore(app): clear lint:changed findings in files touched by the experimental rename --- packages/app/src/providers/connect/mcp.ts | 13 ++++++++----- packages/app/src/settings/general/controllers.ts | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/app/src/providers/connect/mcp.ts b/packages/app/src/providers/connect/mcp.ts index 3adc26ce2572..686a07c2e882 100644 --- a/packages/app/src/providers/connect/mcp.ts +++ b/packages/app/src/providers/connect/mcp.ts @@ -13,7 +13,7 @@ export type McpControls = { change: (name: string, enabled: boolean) => void } -export function useMcpToggle(directory?: Accessor, onSuccess?: () => unknown) { +export function useMcpToggle(directory?: Accessor, onSuccess?: () => void) { const data = useData() const serverSDK = useServerSDK() const platform = usePlatform() @@ -27,12 +27,15 @@ export function useMcpToggle(directory?: Accessor, onSuccess return useMutation(() => ({ mutationFn: async (input: string | { name: string; enabled: boolean; directory?: string }) => { - const name = typeof input === "string" ? input : input.name - const ref = typeof input !== "string" && input.directory ? { directory: input.directory } : location() + // A bare name toggles the server; an object sets an explicit state. + // oxlint-disable-next-line anti-slop/no-runtime-typeof -- discriminates this function's own input union + const request: { name: string; enabled?: boolean; directory?: string } = typeof input === "string" ? { name: input } : input + const name = request.name + const ref = request.directory ? { directory: request.directory } : location() const server = (await serverSDK.api.mcp.list({ location: ref })).data.find((item) => item.name === name) - if (!server || (server.status.status === "pending" && typeof input === "string")) return - const enabled = typeof input === "string" ? server.status.status !== "connected" : input.enabled + if (!server || (server.status.status === "pending" && request.enabled === undefined)) return + const enabled = request.enabled ?? server.status.status !== "connected" if (!enabled) { await serverSDK.api.experimental.mcp.disconnect({ server: name, location: ref }) diff --git a/packages/app/src/settings/general/controllers.ts b/packages/app/src/settings/general/controllers.ts index a6f6bcbb4de9..86bd9051a703 100644 --- a/packages/app/src/settings/general/controllers.ts +++ b/packages/app/src/settings/general/controllers.ts @@ -61,7 +61,7 @@ export function createServerShellController(server: Accessor { + .catch((error) => { actions.mutate(previous) showToast({ variant: "error", From b6449b34662667fb6fe8257c2a887002b2292411 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 26 Oct 2026 11:17:27 -0700 Subject: [PATCH 05/11] chore(app): clear lint:changed findings in renamed experimental callers --- packages/app/src/runtime/server/sync.tsx | 1 + packages/app/src/voice/tools.ts | 1 + packages/app/src/voice/transport.ts | 9 ++++++++- packages/app/src/workspaces/files/model.tsx | 2 ++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/app/src/runtime/server/sync.tsx b/packages/app/src/runtime/server/sync.tsx index bbea34ac139e..0bd6b3299686 100644 --- a/packages/app/src/runtime/server/sync.tsx +++ b/packages/app/src/runtime/server/sync.tsx @@ -244,6 +244,7 @@ function createServerSyncContextInner(serverSDK: ServerSDK, data: Data) { if (update.time.archived) return rest const previous = projects.find((project) => project.id === update.id) + // The wire payload carries no worktrees; keep the inventory this project already loaded. const next = withWorktreeInventory( previous ? updateProjectInfo(previous, update) : normalizeProjectInfo(update), diff --git a/packages/app/src/voice/tools.ts b/packages/app/src/voice/tools.ts index 2b0967f09667..1b9e2de2afd0 100644 --- a/packages/app/src/voice/tools.ts +++ b/packages/app/src/voice/tools.ts @@ -120,6 +120,7 @@ export class VoiceTools { const seen = new Set() const tabs = new Set(this.screen.tabs()) + // Best match first; among equal matches, top-level sessions with an open tab, then the most recent. const rank = (session: SessionInfo) => (search.score(session) ?? 0) * 4 + (session.parentID ? 0 : 2) + (tabs.has(session.id) ? 1 : 0) diff --git a/packages/app/src/voice/transport.ts b/packages/app/src/voice/transport.ts index d7edcddc42b7..673966e1c4a8 100644 --- a/packages/app/src/voice/transport.ts +++ b/packages/app/src/voice/transport.ts @@ -8,6 +8,13 @@ export class VoiceFailure extends Error { } } +/** A client event sent over the Live data channel; the transport adds `event_id`. */ +export type VoiceClientEvent = + | { type: "session.input_audio.mute" | "session.input_audio.unmute" | "response.create" } + | { type: "session.update"; session: { delegation: { type: "responses"; responses: { instructions: string } } } } + | { type: "response.item.create"; item: { type: "function_call_output"; call_id: string; output: string } } + | { type: "session.commentary.append"; delegation_id: null; content: string } + export class VoiceTransport { readonly abort = new AbortController() private peer = new RTCPeerConnection({ iceServers: [], bundlePolicy: "max-bundle", rtcpMuxPolicy: "require" }) @@ -153,7 +160,7 @@ export class VoiceTransport { this.timer = setInterval(() => this.inputOptions.levels(this.level(this.input), this.level(this.output)), 100) } - send(event: object) { + send(event: VoiceClientEvent) { if (this.ended || this.channel.readyState !== "open") throw new VoiceFailure("dropped") this.channel.send(JSON.stringify({ event_id: crypto.randomUUID(), ...event })) } diff --git a/packages/app/src/workspaces/files/model.tsx b/packages/app/src/workspaces/files/model.tsx index 08e47775721a..1ce3bb1d833d 100644 --- a/packages/app/src/workspaces/files/model.tsx +++ b/packages/app/src/workspaces/files/model.tsx @@ -296,12 +296,14 @@ export const { use: useFile, provider: FileProvider } = createSimpleContext({ // and the open tabs ask again on reconnect (a restarted server forgets its watches). Older servers lack the route. const watch = (input: string) => { const file = path.normalize(input) + if (!file || path.absolute(file)) return void serverSDK.api.experimental.file.watch({ path: file, location: { directory: scope() } }).catch(() => {}) } const unwatch = (input: string) => { const file = path.normalize(input) + if (!file || path.absolute(file)) return void serverSDK.api.experimental.file.unwatch({ path: file, location: { directory: scope() } }).catch(() => {}) } From bb5669f316bf6b9f37ce86d035aafed3340487ed Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 26 Oct 2026 11:40:35 -0700 Subject: [PATCH 06/11] docs: add experimental API rules to AGENTS.md and enforce experimental.* identifiers --- AGENTS.md | 9 +++++++ packages/protocol/openapi.json | 24 +++++++++---------- .../protocol/src/groups/persistent-pty.ts | 21 ++++++++-------- packages/protocol/src/groups/session.ts | 2 +- .../test/experimental-identifiers.test.ts | 24 +++++++++++++++++++ 5 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 packages/protocol/test/experimental-identifiers.test.ts diff --git a/AGENTS.md b/AGENTS.md index 0971dbb1c03f..6dda62f75a97 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -163,6 +163,15 @@ const table = sqliteTable("session", { }) ``` +### Experimental API + +- An experimental endpoint lives at `/api/experimental//...`, its OpenAPI identifier is `experimental..`, and it lands in the SDK as `client.experimental..`. Its description says "Experimental: may change without compatibility guarantees." +- `packages/protocol/test/experimental-identifiers.test.ts` enforces the path and identifier match in both directions; the SDK namespace follows from the path. +- An experimental field or parameter on a stable endpoint goes under an `experimental` key (`experimental.`), the same way config does (`config.experimental.*`). +- New endpoints and new default behaviors start experimental. +- To graduate one, move the route out of `/api/experimental`, drop the `experimental.` prefix, and note it in `CHANGELOG.md`. +- Experimental changes ship without deprecated aliases: rename or remove, then update the callers. + ## Testing - Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option. diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index b4cd7f1b3a85..e3e7356b30a4 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -3885,7 +3885,7 @@ "/api/experimental/session/{sessionID}/log": { "get": { "tags": ["session"], - "operationId": "session.log", + "operationId": "experimental.session.log", "parameters": [ { "name": "sessionID", @@ -11260,7 +11260,7 @@ "/api/experimental/session/{sessionID}/terminal/read": { "get": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.read", + "operationId": "experimental.persistentPty.read", "parameters": [ { "name": "sessionID", @@ -11350,7 +11350,7 @@ "/api/experimental/session/{sessionID}/terminal": { "get": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.list", + "operationId": "experimental.persistentPty.list", "parameters": [ { "name": "sessionID", @@ -11417,7 +11417,7 @@ }, "post": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.create", + "operationId": "experimental.persistentPty.create", "parameters": [ { "name": "sessionID", @@ -11493,7 +11493,7 @@ "/api/experimental/persistent-pty/shutdown": { "post": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.shutdown", + "operationId": "experimental.persistentPty.shutdown", "parameters": [], "security": [], "responses": { @@ -11536,7 +11536,7 @@ "/api/experimental/persistent-pty/handoff": { "post": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.handoff", + "operationId": "experimental.persistentPty.handoff", "parameters": [], "security": [], "responses": { @@ -11600,7 +11600,7 @@ "/api/experimental/persistent-pty/{ptyID}": { "get": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.get", + "operationId": "experimental.persistentPty.get", "parameters": [ { "name": "ptyID", @@ -11674,7 +11674,7 @@ }, "put": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.update", + "operationId": "experimental.persistentPty.update", "parameters": [ { "name": "ptyID", @@ -11758,7 +11758,7 @@ }, "delete": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.remove", + "operationId": "experimental.persistentPty.remove", "parameters": [ { "name": "ptyID", @@ -11820,7 +11820,7 @@ "/api/experimental/persistent-pty/{ptyID}/snapshot": { "get": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.snapshot", + "operationId": "experimental.persistentPty.snapshot", "parameters": [ { "name": "ptyID", @@ -11896,7 +11896,7 @@ "/api/experimental/persistent-pty/{ptyID}/connect-token": { "post": { "tags": ["persistentPty"], - "operationId": "server.experimental.persistentPty.connectToken", + "operationId": "experimental.persistentPty.connectToken", "parameters": [ { "name": "ptyID", @@ -11997,7 +11997,7 @@ "/api/experimental/persistent-pty/{ptyID}/connect": { "get": { "tags": ["persistentPty"], - "operationId": "persistentPty.connect", + "operationId": "experimental.persistentPty.connect", "parameters": [ { "name": "ptyID", diff --git a/packages/protocol/src/groups/persistent-pty.ts b/packages/protocol/src/groups/persistent-pty.ts index d77ef6c87895..720ea75bd868 100644 --- a/packages/protocol/src/groups/persistent-pty.ts +++ b/packages/protocol/src/groups/persistent-pty.ts @@ -31,6 +31,7 @@ export const PersistentPtyGroup = HttpApiGroup.make("server.experimental") error: [ServiceUnavailableError], }).annotateMerge( OpenApi.annotations({ + identifier: "experimental.persistentPty.read", summary: "Read the session's most recently controlled terminal", description: "Read the last physical rows without changing selection or taking control. Omitted lines uses the live terminal height; larger counts include retained history. Blank rows are preserved. Screen dimensions and cursor remain relative to the live screen. Returns null when no current terminal exists. Selection is server-local and resets on restart. Experimental: may change without compatibility guarantees.", @@ -42,7 +43,7 @@ export const PersistentPtyGroup = HttpApiGroup.make("server.experimental") params: { sessionID: Session.ID }, success: Schema.Struct({ data: Schema.Array(PersistentPty.Info) }), error: errors, - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.list" })), ) .add( HttpApiEndpoint.post("persistentPty.create", "/api/experimental/session/:sessionID/terminal", { @@ -50,26 +51,26 @@ export const PersistentPtyGroup = HttpApiGroup.make("server.experimental") payload: PersistentPty.CreateInput, success: Schema.Struct({ data: PersistentPty.Info }), error: errors, - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.create" })), ) .add( HttpApiEndpoint.post("persistentPty.shutdown", "/api/experimental/persistent-pty/shutdown", { success: HttpApiSchema.NoContent, error: [ServiceUnavailableError], - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.shutdown" })), ) .add( HttpApiEndpoint.post("persistentPty.handoff", "/api/experimental/persistent-pty/handoff", { success: Schema.Struct({ handoff: Schema.NullOr(PersistentPty.Handoff) }), error: [ServiceUnavailableError], - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.handoff" })), ) .add( HttpApiEndpoint.get("persistentPty.get", "/api/experimental/persistent-pty/:ptyID", { params: { ptyID: Pty.ID }, success: Schema.Struct({ data: PersistentPty.Info }), error: terminalErrors, - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.get" })), ) .add( HttpApiEndpoint.put("persistentPty.update", "/api/experimental/persistent-pty/:ptyID", { @@ -77,21 +78,21 @@ export const PersistentPtyGroup = HttpApiGroup.make("server.experimental") payload: PersistentPty.UpdateInput, success: Schema.Struct({ data: PersistentPty.Info }), error: terminalErrors, - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.update" })), ) .add( HttpApiEndpoint.get("persistentPty.snapshot", "/api/experimental/persistent-pty/:ptyID/snapshot", { params: { ptyID: Pty.ID }, success: Schema.Struct({ data: PersistentPty.Snapshot }), error: terminalErrors, - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.snapshot" })), ) .add( HttpApiEndpoint.delete("persistentPty.remove", "/api/experimental/persistent-pty/:ptyID", { params: { ptyID: Pty.ID }, success: HttpApiSchema.NoContent, error: terminalErrors, - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.remove" })), ) .add( HttpApiEndpoint.post("persistentPty.connectToken", "/api/experimental/persistent-pty/:ptyID/connect-token", { @@ -99,7 +100,7 @@ export const PersistentPtyGroup = HttpApiGroup.make("server.experimental") headers: Schema.Struct({ [PTY_CONNECT_TOKEN_HEADER]: Schema.optional(Schema.String) }), success: Schema.Struct({ data: PtyTicket.ConnectToken }), error: [ForbiddenError, PtyNotFoundError, ServiceUnavailableError], - }), + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.persistentPty.connectToken" })), ) .add( HttpApiEndpoint.get("persistentPty.connect", PERSISTENT_PTY_CONNECT_PATH, { @@ -108,7 +109,7 @@ export const PersistentPtyGroup = HttpApiGroup.make("server.experimental") error: [ForbiddenError, PtyNotFoundError, ServiceUnavailableError], }).annotateMerge( OpenApi.annotations({ - identifier: "persistentPty.connect", + identifier: "experimental.persistentPty.connect", summary: "Connect to a persistent PTY", description: "Stream persistent PTY output through the OpenCode server.", transform: (operation) => ({ diff --git a/packages/protocol/src/groups/session.ts b/packages/protocol/src/groups/session.ts index 47ac7cd47248..a1249ce90546 100644 --- a/packages/protocol/src/groups/session.ts +++ b/packages/protocol/src/groups/session.ts @@ -665,7 +665,7 @@ export const makeSessionGroup = { + const operations = Object.entries(OpenApi.fromApi(ClientApi).paths).flatMap(([path, item]) => + methods.flatMap((method) => { + const operation = item[method] + return operation ? [{ path, identifier: operation.operationId }] : [] + }), + ) + const offenders = operations + .filter( + ({ path, identifier }) => + (path === "/api/experimental" || path.startsWith("/api/experimental/")) !== + identifier.startsWith("experimental."), + ) + .map(({ path, identifier }) => `${identifier} ${path}`) + + expect(operations.length).toBeGreaterThan(100) + expect(offenders).toEqual([]) +}) From 8820bdd51658cdbd4035a7f7a8b82e0571c9d330 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 26 Oct 2026 15:13:41 -0700 Subject: [PATCH 07/11] feat(plugin): move experimental-backed plugin methods under ctx.experimental ctx.generate, ctx.session.wait and ctx.session.instructions move to ctx.experimental.{generate,session.wait,session.instructions} in both the Effect and Promise plugin Context, mirroring client.experimental. No aliases. A core test derives the rule from the Promise adapter's endpoint mapping. --- AGENTS.md | 1 + packages/core/src/plugin/host.ts | 46 ++++++++------- .../plugin/experimental-namespace.test.ts | 58 +++++++++++++++++++ packages/core/test/plugin/host.ts | 24 ++++---- .../test/plugin/session-instructions.test.ts | 52 ++++++++++------- packages/plugin/src/effect/plugin.ts | 11 +++- packages/plugin/src/effect/session.ts | 5 +- packages/plugin/src/promise/adapter.ts | 54 ++++++++++------- packages/plugin/src/promise/plugin.ts | 10 +++- packages/plugin/src/promise/session.ts | 5 +- .../src/docs/content/build/plugins/effect.mdx | 19 +++--- .../src/docs/content/build/plugins/index.mdx | 44 ++++++++------ 12 files changed, 217 insertions(+), 112 deletions(-) create mode 100644 packages/core/test/plugin/experimental-namespace.test.ts diff --git a/AGENTS.md b/AGENTS.md index 6dda62f75a97..3de1a5f04663 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,6 +171,7 @@ const table = sqliteTable("session", { - New endpoints and new default behaviors start experimental. - To graduate one, move the route out of `/api/experimental`, drop the `experimental.` prefix, and note it in `CHANGELOG.md`. - Experimental changes ship without deprecated aliases: rename or remove, then update the callers. +- Plugins reach experimental endpoints only through `ctx.experimental` (`ctx.experimental.session.wait`, `ctx.experimental.generate`), in both the Effect and Promise `Context`, with no alias elsewhere on `ctx`. Experimental hooks are named `experimental.*` (`experimental.ws.handshake`). `packages/core/test/plugin/experimental-namespace.test.ts` checks this against the Promise adapter's endpoint mapping. ## Testing diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 6bcf93f2d6cb..9a003b6d24dc 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -279,13 +279,34 @@ export const make = Effect.fn("PluginHost.make")(function* ( }) as Plugin.Context["event"]["subscribe"], }, experimental: { + generate: { + text: (input) => generate.text(input).pipe(Effect.map((text) => ({ text }))), + }, + session: { + wait: (input) => sessions.wait(input.sessionID), + instructions: { + entry: { + list: Effect.fn(function* (input) { + yield* sessions.get(input.sessionID) + return yield* instructionEntries.list(input.sessionID) + }), + put: Effect.fn(function* (input) { + const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) + yield* sessions.get(input.sessionID) + yield* instructionEntries.put({ ...input, key }) + }), + remove: Effect.fn(function* (input) { + const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) + yield* sessions.get(input.sessionID) + yield* instructionEntries.remove({ ...input, key }) + }), + }, + }, + }, terminal: { read: (input) => persistentPty.read(input.sessionID, input.lines), }, }, - generate: { - text: (input) => generate.text(input).pipe(Effect.map((text) => ({ text }))), - }, integration: { list: () => response(integration.list()), get: Effect.fn(function* (input) { @@ -597,26 +618,7 @@ export const make = Effect.fn("PluginHost.make")(function* ( sessions .interrupt(input.sessionID, { resume: input.resume }) .pipe(Effect.map((interrupted) => ({ interrupted }))), - wait: (input) => sessions.wait(input.sessionID), context: (input) => sessions.context(input.sessionID), - instructions: { - entry: { - list: Effect.fn(function* (input) { - yield* sessions.get(input.sessionID) - return yield* instructionEntries.list(input.sessionID) - }), - put: Effect.fn(function* (input) { - const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) - yield* sessions.get(input.sessionID) - yield* instructionEntries.put({ ...input, key }) - }), - remove: Effect.fn(function* (input) { - const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) - yield* sessions.get(input.sessionID) - yield* instructionEntries.remove({ ...input, key }) - }), - }, - }, }, } return context diff --git a/packages/core/test/plugin/experimental-namespace.test.ts b/packages/core/test/plugin/experimental-namespace.test.ts new file mode 100644 index 000000000000..3683c692d94c --- /dev/null +++ b/packages/core/test/plugin/experimental-namespace.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from "bun:test" +import { PluginPromise } from "@opencode/core/plugin/promise" +import { adaptedEndpoints } from "@opencode/plugin/promise/adapter" +import type { Context } from "@opencode/plugin/promise/plugin" +import { Effect } from "effect" +import { host } from "./host" + +// The Promise adapter records the endpoint and Effect host method behind every API method it builds, +// so this follows the real mapping instead of a hand-kept list of experimental methods. +test("ctx.experimental holds exactly the plugin methods backed by /api/experimental endpoints", async () => { + const effectContext = host() + let promiseContext: Context | undefined + await Effect.runPromise( + Effect.scoped( + PluginPromise.fromPromise({ + id: "test.experimental-namespace", + setup: (context) => { + promiseContext = context + }, + }).effect(effectContext), + ), + ) + + const hostPaths = new Map() + walk(effectContext, (path, value) => hostPaths.set(value, path)) + const methods: Array<{ readonly promise: string; readonly host: string | undefined; readonly endpoint: string }> = [] + walk(promiseContext, (path, value) => { + const adapted = adaptedEndpoints.get(value) + if (adapted) methods.push({ promise: path, host: hostPaths.get(adapted.method), endpoint: adapted.endpoint.path }) + }) + const isExperimental = (endpoint: string) => + endpoint === "/api/experimental" || endpoint.startsWith("/api/experimental/") + + expect(methods.length).toBeGreaterThan(40) + expect(methods.filter(({ endpoint }) => isExperimental(endpoint)).map(({ promise }) => promise)).toContain( + "experimental.session.wait", + ) + expect( + methods + .filter( + ({ promise, host, endpoint }) => + isExperimental(endpoint) !== promise.startsWith("experimental.") || + isExperimental(endpoint) !== Boolean(host?.startsWith("experimental.")), + ) + .map(({ promise, host, endpoint }) => `ctx.${promise} (host ${host ?? "unknown"}) -> ${endpoint}`), + ).toEqual([]) +}) + +function walk(root: unknown, visit: (path: string, value: object) => void) { + const seen = new Set() + const step = (value: unknown, path: ReadonlyArray) => { + if ((typeof value !== "object" && typeof value !== "function") || value === null || seen.has(value)) return + seen.add(value) + if (typeof value === "function") visit(path.join("."), value) + Object.entries(value).forEach(([key, child]) => step(child, [...path, key])) + } + step(root, []) +} diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 8fdb96d3bafe..05fd46dad8e7 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -69,13 +69,23 @@ export function host(overrides: Overrides = {}): Plugin.Context { }, event: { subscribe: events(overrides.event?.subscribe ?? (() => Stream.empty)) }, experimental: overrides.experimental ?? { + generate: { + text: () => Effect.die("unused experimental.generate.text"), + }, + session: { + wait: () => Effect.die("unused experimental.session.wait"), + instructions: { + entry: { + list: () => Effect.die("unused experimental.session.instructions.entry.list"), + put: () => Effect.die("unused experimental.session.instructions.entry.put"), + remove: () => Effect.die("unused experimental.session.instructions.entry.remove"), + }, + }, + }, terminal: { read: () => Effect.die("unused experimental.terminal.read"), }, }, - generate: overrides.generate ?? { - text: () => Effect.die("unused generate.text"), - }, integration: overrides.integration ?? { list: () => Effect.die("unused integration.list"), get: () => Effect.die("unused integration.get"), @@ -183,15 +193,7 @@ export function host(overrides: Overrides = {}): Plugin.Context { move: overrides.session?.move ?? (() => Effect.die("unused session.move")), synthetic: overrides.session?.synthetic ?? (() => Effect.die("unused session.synthetic")), interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")), - wait: overrides.session?.wait ?? (() => Effect.die("unused session.wait")), context: overrides.session?.context ?? (() => Effect.die("unused session.context")), - instructions: overrides.session?.instructions ?? { - entry: { - list: () => Effect.die("unused session.instructions.entry.list"), - put: () => Effect.die("unused session.instructions.entry.put"), - remove: () => Effect.die("unused session.instructions.entry.remove"), - }, - }, }, } } diff --git a/packages/core/test/plugin/session-instructions.test.ts b/packages/core/test/plugin/session-instructions.test.ts index 123a6fe6b42b..42866b2441f0 100644 --- a/packages/core/test/plugin/session-instructions.test.ts +++ b/packages/core/test/plugin/session-instructions.test.ts @@ -30,12 +30,12 @@ it.live("Effect plugins can put, list, and remove session instruction entries se const context = yield* PluginHost.make(plugins) const created = yield* context.session.create({ title: "Plugin instructions" }) - yield* context.session.instructions.entry.put({ + yield* context.experimental.session.instructions.entry.put({ sessionID: created.id, key: "deploy-target", value: "production", }) - expect(yield* context.session.instructions.entry.list({ sessionID: created.id })).toEqual([ + expect(yield* context.experimental.session.instructions.entry.list({ sessionID: created.id })).toEqual([ { key: "deploy-target", value: "production" }, ]) expect(yield* sessions.inbox(created.id)).toEqual([]) @@ -44,7 +44,7 @@ it.live("Effect plugins can put, list, and remove session instruction entries se expect(first.initial).toBe(['', "production", ""].join("\n")) expect(first.entries).toEqual([]) - yield* context.session.instructions.entry.put({ + yield* context.experimental.session.instructions.entry.put({ sessionID: created.id, key: "deploy-target", value: { region: "us-east-1" }, @@ -64,8 +64,8 @@ it.live("Effect plugins can put, list, and remove session instruction entries se }), ]) - yield* context.session.instructions.entry.remove({ sessionID: created.id, key: "deploy-target" }) - expect(yield* context.session.instructions.entry.list({ sessionID: created.id })).toEqual([]) + yield* context.experimental.session.instructions.entry.remove({ sessionID: created.id, key: "deploy-target" }) + expect(yield* context.experimental.session.instructions.entry.list({ sessionID: created.id })).toEqual([]) const third = yield* prepareRequest(created.id) expect(third.entries.at(-1)?.message).toMatchObject({ @@ -76,9 +76,13 @@ it.live("Effect plugins can put, list, and remove session instruction entries se const missing = Session.ID.create() for (const operation of [ - context.session.instructions.entry.list({ sessionID: missing }), - context.session.instructions.entry.put({ sessionID: missing, key: "deploy-target", value: "production" }), - context.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }), + context.experimental.session.instructions.entry.list({ sessionID: missing }), + context.experimental.session.instructions.entry.put({ + sessionID: missing, + key: "deploy-target", + value: "production", + }), + context.experimental.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }), ]) { expect(yield* operation.pipe(Effect.flip)).toMatchObject({ _tag: "Session.NotFoundError", @@ -86,7 +90,7 @@ it.live("Effect plugins can put, list, and remove session instruction entries se }) } expect( - yield* context.session.instructions.entry + yield* context.experimental.session.instructions.entry .put({ sessionID: created.id, key: "oversized", @@ -97,13 +101,13 @@ it.live("Effect plugins can put, list, and remove session instruction entries se for (const key of ["Bad Key", "invalid/key"]) { for (const operation of [ - context.session.instructions.entry.put({ sessionID: created.id, key, value: "rejected" }), - context.session.instructions.entry.remove({ sessionID: created.id, key }), + context.experimental.session.instructions.entry.put({ sessionID: created.id, key, value: "rejected" }), + context.experimental.session.instructions.entry.remove({ sessionID: created.id, key }), ]) { expect(yield* operation.pipe(Effect.flip)).toMatchObject({ _tag: "SchemaError" }) } } - yield* context.session.instructions.entry.put({ + yield* context.experimental.session.instructions.entry.put({ sessionID: created.id, key: "recovered", value: "still works", @@ -127,38 +131,42 @@ it.live("Promise plugins can put, list, and remove session instruction entries s id: "test.session-instructions.put", async setup(ctx) { expect( - await ctx.session.instructions.entry.put({ + await ctx.experimental.session.instructions.entry.put({ sessionID: created.id, key: "team.guidelines", value: "Prefer small, focused changes.", }), ).toBeUndefined() - expect(await ctx.session.instructions.entry.list({ sessionID: created.id })).toEqual([ + expect(await ctx.experimental.session.instructions.entry.list({ sessionID: created.id })).toEqual([ { key: "team.guidelines", value: "Prefer small, focused changes." }, ]) const missing = Session.ID.create() - await expect(ctx.session.instructions.entry.list({ sessionID: missing })).rejects.toMatchObject({ + await expect(ctx.experimental.session.instructions.entry.list({ sessionID: missing })).rejects.toMatchObject({ _tag: "Session.NotFoundError", sessionID: missing, }) await expect( - ctx.session.instructions.entry.put({ sessionID: missing, key: "deploy-target", value: "production" }), + ctx.experimental.session.instructions.entry.put({ + sessionID: missing, + key: "deploy-target", + value: "production", + }), ).rejects.toMatchObject({ _tag: "Session.NotFoundError", sessionID: missing }) await expect( - ctx.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }), + ctx.experimental.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }), ).rejects.toMatchObject({ _tag: "Session.NotFoundError", sessionID: missing }) await expect( - ctx.session.instructions.entry.put({ + ctx.experimental.session.instructions.entry.put({ sessionID: created.id, key: "invalid/key", value: "rejected", }), ).rejects.toMatchObject({ _tag: "SchemaError" }) await expect( - ctx.session.instructions.entry.remove({ sessionID: created.id, key: "invalid/key" }), + ctx.experimental.session.instructions.entry.remove({ sessionID: created.id, key: "invalid/key" }), ).rejects.toMatchObject({ _tag: "SchemaError" }) await expect( - ctx.session.instructions.entry.put({ + ctx.experimental.session.instructions.entry.put({ sessionID: created.id, key: "oversized", value: "x".repeat(InstructionEntry.MaxValueBytes), @@ -177,12 +185,12 @@ it.live("Promise plugins can put, list, and remove session instruction entries s id: "test.session-instructions.remove", async setup(ctx) { expect( - await ctx.session.instructions.entry.remove({ + await ctx.experimental.session.instructions.entry.remove({ sessionID: created.id, key: "team.guidelines", }), ).toBeUndefined() - expect(await ctx.session.instructions.entry.list({ sessionID: created.id })).toEqual([]) + expect(await ctx.experimental.session.instructions.entry.list({ sessionID: created.id })).toEqual([]) }, }).effect(context) diff --git a/packages/plugin/src/effect/plugin.ts b/packages/plugin/src/effect/plugin.ts index b84c2e924209..b4f7a532e6d6 100644 --- a/packages/plugin/src/effect/plugin.ts +++ b/packages/plugin/src/effect/plugin.ts @@ -1,4 +1,10 @@ -import type { CredentialApi, ExperimentalApi, ExperimentalGenerateApi, PluginApi } from "@opencode/client/effect/api" +import type { + CredentialApi, + ExperimentalApi, + ExperimentalGenerateApi, + ExperimentalSessionApi, + PluginApi, +} from "@opencode/client/effect/api" import type { Location } from "@opencode/schema/location" import type { Effect, Scope } from "effect" import type { PluginOptions } from "../options.js" @@ -33,12 +39,13 @@ export interface Context { readonly credential: Pick, "activate"> readonly event: EventDomain readonly experimental: { + readonly generate: ExperimentalGenerateApi + readonly session: Pick, "wait" | "instructions"> readonly terminal: Pick["persistentPty"], "read"> } readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain - readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick, "list"> readonly provider: ProviderDomain diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index 32004c44bea9..010bc67d550d 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -1,4 +1,4 @@ -import type { ExperimentalSessionApi, SessionApi } from "@opencode/client/effect/api" +import type { SessionApi } from "@opencode/client/effect/api" import type { GenerationOptionsFields, Message, SystemPart } from "@opencode/ai/effect" import type { Agent } from "@opencode/schema/agent" import type { Model } from "@opencode/schema/model" @@ -168,7 +168,6 @@ export type SessionDomain = Pick< | "update" | "move" | "context" -> & - Pick, "wait" | "instructions"> & { +> & { readonly hook: ModelHooks } diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index a9dc64640cf1..5da0732cff4f 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -28,6 +28,12 @@ interface CompiledEndpoint { } const compiledEndpoints = new WeakMap() + +/** The endpoint and Effect host method behind each adapted Promise method, so tests can check where endpoints surface. */ +export const adaptedEndpoints = new WeakMap< + object, + { readonly endpoint: HttpApiEndpoint.Top; readonly method: object } +>() const JsonInput = Schema.fromJsonString(Schema.Unknown) interface HostRpcCallContext { @@ -268,7 +274,7 @@ export function fromPromise(plugin: Plugin) { method: (input: never) => Effect.Effect, ) => { const compiled = compileEndpoint(endpoint) - return ((input?: unknown) => + const adapted = (input?: unknown) => Effect.gen(function* () { // Match the generated Promise client, whose request body crosses JSON before endpoint decoding. const normalized = yield* Schema.encodeUnknownEffect(JsonInput)(input ?? {}).pipe( @@ -278,7 +284,9 @@ export function fromPromise(plugin: Plugin) { const result = yield* method(Object.assign({}, ...decoded) as never) if (compiled.noContent) return undefined return yield* compiled.encode(result) - }).pipe(Effect.runPromiseWith(runtime))) as PromiseMethod + }).pipe(Effect.runPromiseWith(runtime)) + adaptedEndpoints.set(adapted, { endpoint, method }) + return adapted as PromiseMethod } const transform = @@ -341,13 +349,32 @@ export function fromPromise(plugin: Plugin) { ), }, experimental: { + generate: { + text: adaptApiMethod(GenerateEndpoints["generate.text"], host.experimental.generate.text), + }, + session: { + wait: adaptApiMethod(SessionEndpoints["session.wait"], host.experimental.session.wait), + instructions: { + entry: { + list: adaptApiMethod( + SessionEndpoints["session.instructions.entry.list"], + host.experimental.session.instructions.entry.list, + ), + put: adaptApiMethod( + SessionEndpoints["session.instructions.entry.put"], + host.experimental.session.instructions.entry.put, + ), + remove: adaptApiMethod( + SessionEndpoints["session.instructions.entry.remove"], + host.experimental.session.instructions.entry.remove, + ), + }, + }, + }, terminal: { read: adaptApiMethod(ExperimentalEndpoints["persistentPty.read"], host.experimental.terminal.read), }, }, - generate: { - text: adaptApiMethod(GenerateEndpoints["generate.text"], host.generate.text), - }, model: { list: adaptApiMethod(ModelEndpoints["model.list"], host.model.list), default: adaptApiMethod(ModelEndpoints["model.default"], host.model.default), @@ -610,24 +637,7 @@ export function fromPromise(plugin: Plugin) { interrupt: adaptApiMethod(SessionEndpoints["session.interrupt"], host.session.interrupt), update: adaptApiMethod(SessionEndpoints["session.update"], host.session.update), move: adaptApiMethod(SessionEndpoints["session.move"], host.session.move), - wait: adaptApiMethod(SessionEndpoints["session.wait"], host.session.wait), context: adaptApiMethod(SessionEndpoints["session.context"], host.session.context), - instructions: { - entry: { - list: adaptApiMethod( - SessionEndpoints["session.instructions.entry.list"], - host.session.instructions.entry.list, - ), - put: adaptApiMethod( - SessionEndpoints["session.instructions.entry.put"], - host.session.instructions.entry.put, - ), - remove: adaptApiMethod( - SessionEndpoints["session.instructions.entry.remove"], - host.session.instructions.entry.remove, - ), - }, - }, }, shell: { hook: (name, callback) => diff --git a/packages/plugin/src/promise/plugin.ts b/packages/plugin/src/promise/plugin.ts index e059da892550..80a3c8a6ffdf 100644 --- a/packages/plugin/src/promise/plugin.ts +++ b/packages/plugin/src/promise/plugin.ts @@ -1,5 +1,10 @@ import type { OpenCodeClient } from "@opencode/client" -import type { CredentialApi, ExperimentalGenerateApi, PluginApi } from "@opencode/client/promise/api" +import type { + CredentialApi, + ExperimentalGenerateApi, + ExperimentalSessionApi, + PluginApi, +} from "@opencode/client/promise/api" import type { Location } from "@opencode/schema/location" import type { PluginOptions } from "../options.js" import type { App } from "../app.js" @@ -33,12 +38,13 @@ export interface Context { readonly credential: Pick readonly event: EventDomain readonly experimental: { + readonly generate: ExperimentalGenerateApi + readonly session: Pick readonly terminal: Pick } readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain - readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick readonly provider: ProviderDomain diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index 1f1e06efbd97..77b84ec67c7a 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -1,4 +1,4 @@ -import type { ExperimentalSessionApi, SessionApi } from "@opencode/client/promise/api" +import type { SessionApi } from "@opencode/client/promise/api" import type { GenerationOptionsFields, Message, SystemPart } from "@opencode/ai/effect" import type { Agent } from "@opencode/schema/agent" import type { Model } from "@opencode/schema/model" @@ -168,7 +168,6 @@ export type SessionDomain = Pick< | "update" | "move" | "context" -> & - Pick & { +> & { readonly hook: ModelHooks } diff --git a/services/www/src/docs/content/build/plugins/effect.mdx b/services/www/src/docs/content/build/plugins/effect.mdx index 9cd85b369564..ccda2801b543 100644 --- a/services/www/src/docs/content/build/plugins/effect.mdx +++ b/services/www/src/docs/content/build/plugins/effect.mdx @@ -121,12 +121,13 @@ interface Context { readonly command: CommandDomain readonly event: EventDomain readonly experimental: { + readonly generate: ExperimentalGenerateApi + readonly session: Pick, "wait" | "instructions"> readonly terminal: Pick["persistentPty"], "read"> } readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain - readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick, "list"> readonly provider: ProviderDomain @@ -858,17 +859,18 @@ effect: (ctx) => }), ``` -Attach, list, or remove keyed session instruction entries with `instructions.entry`. Entries render as `` blocks in the session's instructions, and updates or removals are announced at the next step boundary without queuing a message turn. +Attach, list, or remove keyed session instruction entries with the experimental `ctx.experimental.session.instructions.entry`. Entries render as `` blocks in the session's instructions, and updates or removals are announced at the next step boundary without queuing a message turn. ```ts effect: (ctx) => Effect.gen(function* () { const created = yield* ctx.session.create({ title: "Review" }).pipe(Effect.orDie) - yield* ctx.session.instructions.entry + const instructions = ctx.experimental.session.instructions + yield* instructions.entry .put({ sessionID: created.id, key: "deploy-target", value: "production" }) .pipe(Effect.orDie) - const entries = yield* ctx.session.instructions.entry.list({ sessionID: created.id }).pipe(Effect.orDie) - yield* ctx.session.instructions.entry.remove({ sessionID: created.id, key: "deploy-target" }).pipe(Effect.orDie) + const entries = yield* instructions.entry.list({ sessionID: created.id }).pipe(Effect.orDie) + yield* instructions.entry.remove({ sessionID: created.id, key: "deploy-target" }).pipe(Effect.orDie) }), ``` @@ -895,10 +897,13 @@ type SessionDomain = Pick< | "update" | "move" | "context" -> & - Pick, "wait" | "instructions"> +> ``` +Endpoints under `/api/experimental` reach plugins only through `ctx.experimental`: `ctx.experimental.generate`, +`ctx.experimental.session.wait`, `ctx.experimental.session.instructions` and `ctx.experimental.terminal.read`. They +may change between releases. + ### Skills Read skills at the current location. diff --git a/services/www/src/docs/content/build/plugins/index.mdx b/services/www/src/docs/content/build/plugins/index.mdx index e00192c68bf9..9f786fd92851 100644 --- a/services/www/src/docs/content/build/plugins/index.mdx +++ b/services/www/src/docs/content/build/plugins/index.mdx @@ -725,12 +725,12 @@ interface ReferenceEditor { } ``` -### Generate +### Generate (experimental) Generate text with a selected model without creating a session, invoking tools, or adding to session history. ```ts -const review = await ctx.generate.text({ +const review = await ctx.experimental.generate.text({ model: { providerID: "anthropic", id: "claude-sonnet-5-5" }, prompt: "Review this proposed action.", }) @@ -821,7 +821,7 @@ Rename, interrupt, or wait for a session. ```ts await ctx.session.update({ sessionID, title: "Review" }) await ctx.session.interrupt({ sessionID, resume: false }) -await ctx.session.wait({ sessionID }) +await ctx.experimental.session.wait({ sessionID }) ``` Remove a temporary session with `remove`. Removal stops active execution and recursively deletes child sessions. @@ -837,14 +837,18 @@ the next step boundary by default. Pass `delivery: "queue"` to preserve queue or const compaction = await ctx.session.compact({ sessionID }) ``` -Attach, list, or remove keyed session instruction entries with `instructions.entry`. Entries render as `` blocks in the session's instructions, and updates or removals are announced at the next step boundary without queuing a message turn. +Attach, list, or remove keyed session instruction entries with the experimental `ctx.experimental.session.instructions.entry`. Entries render as `` blocks in the session's instructions, and updates or removals are announced at the next step boundary without queuing a message turn. ```ts -await ctx.session.instructions.entry.put({ sessionID, key: "deploy-target", value: "production" }) -const entries = await ctx.session.instructions.entry.list({ sessionID }) -await ctx.session.instructions.entry.remove({ sessionID, key: "deploy-target" }) +await ctx.experimental.session.instructions.entry.put({ sessionID, key: "deploy-target", value: "production" }) +const entries = await ctx.experimental.session.instructions.entry.list({ sessionID }) +await ctx.experimental.session.instructions.entry.remove({ sessionID, key: "deploy-target" }) ``` +Endpoints under `/api/experimental` reach plugins only through `ctx.experimental`: `ctx.experimental.generate`, +`ctx.experimental.session.wait`, `ctx.experimental.session.instructions` and `ctx.experimental.terminal.read`. They +may change between releases. + #### Reference Schemas: [`Session.Info`](/api#schema-Session.Info), [`Model.Ref`](/api#schema-Model.Ref), @@ -857,16 +861,6 @@ interface SessionContext { get(input: SessionGetInput, requestOptions?: RequestOptions): Promise remove(input: SessionRemoveInput, requestOptions?: RequestOptions): Promise context(input: SessionContextInput, requestOptions?: RequestOptions): Promise - instructions: { - entry: { - list( - input: SessionInstructionsEntryListInput, - requestOptions?: RequestOptions, - ): Promise - put(input: SessionInstructionsEntryPutInput, requestOptions?: RequestOptions): Promise - remove(input: SessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions): Promise - } - } switchAgent(input: SessionSwitchAgentInput, requestOptions?: RequestOptions): Promise switchModel(input: SessionSwitchModelInput, requestOptions?: RequestOptions): Promise update(input: SessionUpdateInput, requestOptions?: RequestOptions): Promise @@ -877,7 +871,21 @@ interface SessionContext { compact(input: SessionCompactInput, requestOptions?: RequestOptions): Promise synthetic(input: SessionSyntheticInput, requestOptions?: RequestOptions): Promise interrupt(input: SessionInterruptInput, requestOptions?: RequestOptions): Promise<{ interrupted: boolean }> +} + +// ctx.experimental.session +interface ExperimentalSessionContext { wait(input: ExperimentalSessionWaitInput, requestOptions?: RequestOptions): Promise + instructions: { + entry: { + list( + input: SessionInstructionsEntryListInput, + requestOptions?: RequestOptions, + ): Promise + put(input: SessionInstructionsEntryPutInput, requestOptions?: RequestOptions): Promise + remove(input: SessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions): Promise + } + } } ``` @@ -1694,7 +1702,7 @@ await ctx.permission.hook("evaluate", async (event) => { if (event.action === "read") return const messages = await ctx.session.context({ sessionID: event.sessionID }) - const review = await ctx.generate.text({ + const review = await ctx.experimental.generate.text({ model: { providerID: "anthropic", id: "claude-sonnet-5-5" }, prompt: buildSafetyPrompt({ messages, action: event.action, resources: event.resources }), }) From 363eda294da65eaed3af1a25f43d0993693f4a0e Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 26 Oct 2026 16:14:01 -0700 Subject: [PATCH 08/11] feat(sdk): keep pre-experimental SDK and plugin paths as deprecated aliases Old client and plugin paths forward to client.experimental / ctx.experimental until the next minor. Adds .github/RELEASE_REMINDERS.md and the AGENTS.md rule for deprecating shipped SDK and plugin API. --- .github/RELEASE_REMINDERS.md | 28 +++ AGENTS.md | 9 +- packages/client/src/deprecated-aliases.ts | 100 ++++++++ packages/client/src/effect/api.ts | 1 + packages/client/src/effect/client.ts | 2 + packages/client/src/effect/deprecated.ts | 219 ++++++++++++++++++ packages/client/src/promise/api.ts | 2 + packages/client/src/promise/client.ts | 8 + packages/client/src/promise/deprecated.ts | 141 +++++++++++ packages/client/src/promise/index.ts | 1 + packages/client/test/effect.test.ts | 25 +- packages/client/test/promise.test.ts | 51 +++- packages/core/src/plugin/host.ts | 65 +++--- .../plugin/experimental-namespace.test.ts | 54 +++-- packages/core/test/plugin/host.ts | 42 ++-- packages/plugin/src/effect/integration.ts | 2 +- packages/plugin/src/effect/plugin.ts | 2 + packages/plugin/src/effect/session.ts | 6 +- packages/plugin/src/promise/adapter.ts | 61 ++--- packages/plugin/src/promise/integration.ts | 2 +- packages/plugin/src/promise/plugin.ts | 2 + packages/plugin/src/promise/session.ts | 6 +- .../src/docs/content/build/plugins/effect.mdx | 7 +- .../src/docs/content/build/plugins/index.mdx | 11 +- 24 files changed, 734 insertions(+), 113 deletions(-) create mode 100644 .github/RELEASE_REMINDERS.md create mode 100644 packages/client/src/deprecated-aliases.ts create mode 100644 packages/client/src/effect/deprecated.ts create mode 100644 packages/client/src/promise/deprecated.ts diff --git a/.github/RELEASE_REMINDERS.md b/.github/RELEASE_REMINDERS.md new file mode 100644 index 000000000000..a000a3b462fa --- /dev/null +++ b/.github/RELEASE_REMINDERS.md @@ -0,0 +1,28 @@ +# Release reminders + +Read this before cutting a minor or major release, act on every item in its section, and delete each item once it ships. Add an item whenever a change leaves something to finish in a later release, such as a deprecated alias to remove. + +## Next minor + +- [ ] Remove the deprecated pre-experimental SDK and plugin aliases from [#59072](https://github.com/anomalyco/opencode/pull/59072): + - SDK methods (`packages/client/src/deprecated-aliases.ts`, plus `file.write` in `packages/client/src/promise/client.ts`), each now at `client.experimental.`: + - `client.session.{stats,import,export,skill,wait,log}` + - `client.session.instructions.entry.{list,put,remove}` + - `client.generate.text` + - `client.voice.{models,session}` + - `client.migration.v1.status` + - `client.usage.limits` + - `client.integration.wellknown.add` + - `client.mcp.{add,remove,connect,disconnect}` and `client.mcp.client.token` + - `client.file.{watch,unwatch}` and, in the Promise client only, `client.file.write` + - `client.config.update` + - SDK types renamed with an `Experimental` prefix (`packages/client/src/promise/deprecated.ts`, `packages/client/src/effect/deprecated.ts`, and `GenerateApi` in `packages/client/src/promise/api.ts`), for example `SessionWaitInput` → `ExperimentalSessionWaitInput`. + - Plugin `Context` paths (`packages/plugin/src/{effect,promise}/{plugin,session}.ts`, wired in `packages/core/src/plugin/host.ts`, `packages/plugin/src/promise/adapter.ts` and `packages/core/test/plugin/host.ts`), each now under `ctx.experimental`: + - `ctx.generate` + - `ctx.session.wait` + - `ctx.session.instructions` + - Then drop the `Omit` in `packages/plugin/src/{effect,promise}/integration.ts` and the alias expectations in `packages/client/test/{promise,effect}.test.ts` and `packages/core/test/plugin/experimental-namespace.test.ts`. + +## Next major + +No items yet. diff --git a/AGENTS.md b/AGENTS.md index 3de1a5f04663..fa1eda87647f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -170,8 +170,13 @@ const table = sqliteTable("session", { - An experimental field or parameter on a stable endpoint goes under an `experimental` key (`experimental.`), the same way config does (`config.experimental.*`). - New endpoints and new default behaviors start experimental. - To graduate one, move the route out of `/api/experimental`, drop the `experimental.` prefix, and note it in `CHANGELOG.md`. -- Experimental changes ship without deprecated aliases: rename or remove, then update the callers. -- Plugins reach experimental endpoints only through `ctx.experimental` (`ctx.experimental.session.wait`, `ctx.experimental.generate`), in both the Effect and Promise `Context`, with no alias elsewhere on `ctx`. Experimental hooks are named `experimental.*` (`experimental.ws.handshake`). `packages/core/test/plugin/experimental-namespace.test.ts` checks this against the Promise adapter's endpoint mapping. +- Experimental HTTP routes change without aliases: rename or remove the route, then update the callers. +- A breaking move of shipped SDK or plugin API (including moving it under `experimental`) keeps the old path as a deprecated alias of the new one until the next minor. Mark the alias `@deprecated`, naming the replacement and "Removed in the next minor", and add its removal to `.github/RELEASE_REMINDERS.md`. +- Plugins reach experimental endpoints through `ctx.experimental` (`ctx.experimental.session.wait`, `ctx.experimental.generate`), in both the Effect and Promise `Context`; any other path to them on `ctx` is a deprecated alias. Experimental hooks are named `experimental.*` (`experimental.ws.handshake`). `packages/core/test/plugin/experimental-namespace.test.ts` checks this against the Promise adapter's endpoint mapping. + +## Releases + +- Before cutting a minor or major release, read `.github/RELEASE_REMINDERS.md` and act on its section for that release. ## Testing diff --git a/packages/client/src/deprecated-aliases.ts b/packages/client/src/deprecated-aliases.ts new file mode 100644 index 000000000000..15328d32e726 --- /dev/null +++ b/packages/client/src/deprecated-aliases.ts @@ -0,0 +1,100 @@ +export * as DeprecatedAliases from "./deprecated-aliases.js" + +// Pre-experimental paths of the methods #59072 moved under `client.experimental`. Removed in the next minor; see +// .github/RELEASE_REMINDERS.md. + +type Shape = { + readonly session: object + readonly file: object + readonly config: object + readonly mcp: object + readonly integration: object + readonly experimental: { + readonly session: { + readonly stats: unknown + readonly import: unknown + readonly export: unknown + readonly skill: unknown + readonly wait: unknown + readonly log: unknown + readonly instructions: { + readonly entry: { readonly list: unknown; readonly put: unknown; readonly remove: unknown } + } + } + readonly generate: { readonly text: unknown } + readonly voice: { readonly models: unknown; readonly session: unknown } + readonly integration: { readonly wellknown: { readonly add: unknown } } + readonly mcp: { + readonly add: unknown + readonly remove: unknown + readonly connect: unknown + readonly disconnect: unknown + readonly client: { readonly token: unknown } + } + readonly file: { readonly watch: unknown; readonly unwatch: unknown } + readonly migration: { readonly v1: { readonly status: unknown } } + readonly usage: { readonly limits: unknown } + readonly config: { readonly update: unknown } + } +} + +export function make(raw: Client) { + const experimental = raw.experimental + return { + session: { + ...(raw.session as Client["session"]), + /** @deprecated Use `client.experimental.session.stats`. Removed in the next minor. */ + stats: experimental.session.stats as Client["experimental"]["session"]["stats"], + /** @deprecated Use `client.experimental.session.import`. Removed in the next minor. */ + import: experimental.session.import as Client["experimental"]["session"]["import"], + /** @deprecated Use `client.experimental.session.export`. Removed in the next minor. */ + export: experimental.session.export as Client["experimental"]["session"]["export"], + /** @deprecated Use `client.experimental.session.skill`. Removed in the next minor. */ + skill: experimental.session.skill as Client["experimental"]["session"]["skill"], + /** @deprecated Use `client.experimental.session.wait`. Removed in the next minor. */ + wait: experimental.session.wait as Client["experimental"]["session"]["wait"], + /** @deprecated Use `client.experimental.session.log`. Removed in the next minor. */ + log: experimental.session.log as Client["experimental"]["session"]["log"], + /** @deprecated Use `client.experimental.session.instructions`. Removed in the next minor. */ + instructions: experimental.session.instructions as Client["experimental"]["session"]["instructions"], + }, + /** @deprecated Use `client.experimental.generate`. Removed in the next minor. */ + generate: experimental.generate as Client["experimental"]["generate"], + /** @deprecated Use `client.experimental.voice`. Removed in the next minor. */ + voice: experimental.voice as Client["experimental"]["voice"], + /** @deprecated Use `client.experimental.migration`. Removed in the next minor. */ + migration: experimental.migration as Client["experimental"]["migration"], + /** @deprecated Use `client.experimental.usage`. Removed in the next minor. */ + usage: experimental.usage as Client["experimental"]["usage"], + integration: { + ...(raw.integration as Client["integration"]), + /** @deprecated Use `client.experimental.integration.wellknown`. Removed in the next minor. */ + wellknown: experimental.integration.wellknown as Client["experimental"]["integration"]["wellknown"], + }, + mcp: { + ...(raw.mcp as Client["mcp"]), + /** @deprecated Use `client.experimental.mcp.add`. Removed in the next minor. */ + add: experimental.mcp.add as Client["experimental"]["mcp"]["add"], + /** @deprecated Use `client.experimental.mcp.remove`. Removed in the next minor. */ + remove: experimental.mcp.remove as Client["experimental"]["mcp"]["remove"], + /** @deprecated Use `client.experimental.mcp.connect`. Removed in the next minor. */ + connect: experimental.mcp.connect as Client["experimental"]["mcp"]["connect"], + /** @deprecated Use `client.experimental.mcp.disconnect`. Removed in the next minor. */ + disconnect: experimental.mcp.disconnect as Client["experimental"]["mcp"]["disconnect"], + /** @deprecated Use `client.experimental.mcp.client`. Removed in the next minor. */ + client: experimental.mcp.client as Client["experimental"]["mcp"]["client"], + }, + file: { + ...(raw.file as Client["file"]), + /** @deprecated Use `client.experimental.file.watch`. Removed in the next minor. */ + watch: experimental.file.watch as Client["experimental"]["file"]["watch"], + /** @deprecated Use `client.experimental.file.unwatch`. Removed in the next minor. */ + unwatch: experimental.file.unwatch as Client["experimental"]["file"]["unwatch"], + }, + config: { + ...(raw.config as Client["config"]), + /** @deprecated Use `client.experimental.config.update`. Removed in the next minor. */ + update: experimental.config.update as Client["experimental"]["config"]["update"], + }, + } +} diff --git a/packages/client/src/effect/api.ts b/packages/client/src/effect/api.ts index e6f0e9990022..277ebbe6c5e0 100644 --- a/packages/client/src/effect/api.ts +++ b/packages/client/src/effect/api.ts @@ -4,5 +4,6 @@ export type { RpcApi, RpcClient } from "./rpc.js" export type { SelectedEvent } from "../shared-events.js" export type * from "./api/api.js" +export type * from "./deprecated.js" export type WebSearchApi = WebsearchApi diff --git a/packages/client/src/effect/client.ts b/packages/client/src/effect/client.ts index 4aa01eacb9a6..8f0f0d86b7d8 100644 --- a/packages/client/src/effect/client.ts +++ b/packages/client/src/effect/client.ts @@ -2,6 +2,7 @@ export * as OpenCode from "./client.js" import { Cause, Context, Effect, Stream } from "effect" import { HttpClient, HttpClientRequest } from "effect/http" +import { DeprecatedAliases } from "../deprecated-aliases.js" import { SharedEvents } from "../shared-events.js" import { ClientError, OpenCode } from "./generated/index.js" import type { EventSubscribeInput } from "./api.js" @@ -49,6 +50,7 @@ export const make = Effect.fn("OpenCode.make")(function* (options?: { readonly b ) return { ...raw, + ...DeprecatedAliases.make(raw), event: { ...raw.event, subscribe }, rpc: Object.assign( RpcClientRuntime.make( diff --git a/packages/client/src/effect/deprecated.ts b/packages/client/src/effect/deprecated.ts new file mode 100644 index 000000000000..d7c97a681e60 --- /dev/null +++ b/packages/client/src/effect/deprecated.ts @@ -0,0 +1,219 @@ +// Pre-experimental names of the types #59072 renamed with an `Experimental` prefix. Removed in the next minor; see +// .github/RELEASE_REMINDERS.md. +import type * as Experimental from "./api/api.js" + +/** @deprecated Use `ExperimentalConfigUpdateInput`. Removed in the next minor. */ +export type ConfigUpdateInput = Experimental.ExperimentalConfigUpdateInput + +/** @deprecated Use `ExperimentalConfigUpdateOperation`. Removed in the next minor. */ +export type ConfigUpdateOperation = Experimental.ExperimentalConfigUpdateOperation + +/** @deprecated Use `ExperimentalConfigUpdateOutput`. Removed in the next minor. */ +export type ConfigUpdateOutput = Experimental.ExperimentalConfigUpdateOutput + +/** @deprecated Use `ExperimentalFileUnwatchInput`. Removed in the next minor. */ +export type FileUnwatchInput = Experimental.ExperimentalFileUnwatchInput + +/** @deprecated Use `ExperimentalFileUnwatchOperation`. Removed in the next minor. */ +export type FileUnwatchOperation = Experimental.ExperimentalFileUnwatchOperation + +/** @deprecated Use `ExperimentalFileUnwatchOutput`. Removed in the next minor. */ +export type FileUnwatchOutput = Experimental.ExperimentalFileUnwatchOutput + +/** @deprecated Use `ExperimentalFileWatchInput`. Removed in the next minor. */ +export type FileWatchInput = Experimental.ExperimentalFileWatchInput + +/** @deprecated Use `ExperimentalFileWatchOperation`. Removed in the next minor. */ +export type FileWatchOperation = Experimental.ExperimentalFileWatchOperation + +/** @deprecated Use `ExperimentalFileWatchOutput`. Removed in the next minor. */ +export type FileWatchOutput = Experimental.ExperimentalFileWatchOutput + +/** @deprecated Use `ExperimentalGenerateApi`. Removed in the next minor. */ +export type GenerateApi = Experimental.ExperimentalGenerateApi + +/** @deprecated Use `ExperimentalGenerateTextInput`. Removed in the next minor. */ +export type GenerateTextInput = Experimental.ExperimentalGenerateTextInput + +/** @deprecated Use `ExperimentalGenerateTextOperation`. Removed in the next minor. */ +export type GenerateTextOperation = Experimental.ExperimentalGenerateTextOperation + +/** @deprecated Use `ExperimentalGenerateTextOutput`. Removed in the next minor. */ +export type GenerateTextOutput = Experimental.ExperimentalGenerateTextOutput + +/** @deprecated Use `ExperimentalIntegrationWellknownAddInput`. Removed in the next minor. */ +export type IntegrationWellknownAddInput = Experimental.ExperimentalIntegrationWellknownAddInput + +/** @deprecated Use `ExperimentalIntegrationWellknownAddOperation`. Removed in the next minor. */ +export type IntegrationWellknownAddOperation = Experimental.ExperimentalIntegrationWellknownAddOperation + +/** @deprecated Use `ExperimentalIntegrationWellknownAddOutput`. Removed in the next minor. */ +export type IntegrationWellknownAddOutput = Experimental.ExperimentalIntegrationWellknownAddOutput + +/** @deprecated Use `ExperimentalMcpAddInput`. Removed in the next minor. */ +export type McpAddInput = Experimental.ExperimentalMcpAddInput + +/** @deprecated Use `ExperimentalMcpAddOperation`. Removed in the next minor. */ +export type McpAddOperation = Experimental.ExperimentalMcpAddOperation + +/** @deprecated Use `ExperimentalMcpAddOutput`. Removed in the next minor. */ +export type McpAddOutput = Experimental.ExperimentalMcpAddOutput + +/** @deprecated Use `ExperimentalMcpClientTokenInput`. Removed in the next minor. */ +export type McpClientTokenInput = Experimental.ExperimentalMcpClientTokenInput + +/** @deprecated Use `ExperimentalMcpClientTokenOperation`. Removed in the next minor. */ +export type McpClientTokenOperation = Experimental.ExperimentalMcpClientTokenOperation + +/** @deprecated Use `ExperimentalMcpClientTokenOutput`. Removed in the next minor. */ +export type McpClientTokenOutput = Experimental.ExperimentalMcpClientTokenOutput + +/** @deprecated Use `ExperimentalMcpConnectInput`. Removed in the next minor. */ +export type McpConnectInput = Experimental.ExperimentalMcpConnectInput + +/** @deprecated Use `ExperimentalMcpConnectOperation`. Removed in the next minor. */ +export type McpConnectOperation = Experimental.ExperimentalMcpConnectOperation + +/** @deprecated Use `ExperimentalMcpConnectOutput`. Removed in the next minor. */ +export type McpConnectOutput = Experimental.ExperimentalMcpConnectOutput + +/** @deprecated Use `ExperimentalMcpDisconnectInput`. Removed in the next minor. */ +export type McpDisconnectInput = Experimental.ExperimentalMcpDisconnectInput + +/** @deprecated Use `ExperimentalMcpDisconnectOperation`. Removed in the next minor. */ +export type McpDisconnectOperation = Experimental.ExperimentalMcpDisconnectOperation + +/** @deprecated Use `ExperimentalMcpDisconnectOutput`. Removed in the next minor. */ +export type McpDisconnectOutput = Experimental.ExperimentalMcpDisconnectOutput + +/** @deprecated Use `ExperimentalMcpRemoveInput`. Removed in the next minor. */ +export type McpRemoveInput = Experimental.ExperimentalMcpRemoveInput + +/** @deprecated Use `ExperimentalMcpRemoveOperation`. Removed in the next minor. */ +export type McpRemoveOperation = Experimental.ExperimentalMcpRemoveOperation + +/** @deprecated Use `ExperimentalMcpRemoveOutput`. Removed in the next minor. */ +export type McpRemoveOutput = Experimental.ExperimentalMcpRemoveOutput + +/** @deprecated Use `ExperimentalMigrationApi`. Removed in the next minor. */ +export type MigrationApi = Experimental.ExperimentalMigrationApi + +/** @deprecated Use `ExperimentalMigrationV1StatusOperation`. Removed in the next minor. */ +export type MigrationV1StatusOperation = Experimental.ExperimentalMigrationV1StatusOperation + +/** @deprecated Use `ExperimentalMigrationV1StatusOutput`. Removed in the next minor. */ +export type MigrationV1StatusOutput = Experimental.ExperimentalMigrationV1StatusOutput + +/** @deprecated Use `ExperimentalSessionExportInput`. Removed in the next minor. */ +export type SessionExportInput = Experimental.ExperimentalSessionExportInput + +/** @deprecated Use `ExperimentalSessionExportOperation`. Removed in the next minor. */ +export type SessionExportOperation = Experimental.ExperimentalSessionExportOperation + +/** @deprecated Use `ExperimentalSessionExportOutput`. Removed in the next minor. */ +export type SessionExportOutput = Experimental.ExperimentalSessionExportOutput + +/** @deprecated Use `ExperimentalSessionImportInput`. Removed in the next minor. */ +export type SessionImportInput = Experimental.ExperimentalSessionImportInput + +/** @deprecated Use `ExperimentalSessionImportOperation`. Removed in the next minor. */ +export type SessionImportOperation = Experimental.ExperimentalSessionImportOperation + +/** @deprecated Use `ExperimentalSessionImportOutput`. Removed in the next minor. */ +export type SessionImportOutput = Experimental.ExperimentalSessionImportOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListInput`. Removed in the next minor. */ +export type SessionInstructionsEntryListInput = Experimental.ExperimentalSessionInstructionsEntryListInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListOperation`. Removed in the next minor. */ +export type SessionInstructionsEntryListOperation = + Experimental.ExperimentalSessionInstructionsEntryListOperation + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryListOutput = Experimental.ExperimentalSessionInstructionsEntryListOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutInput`. Removed in the next minor. */ +export type SessionInstructionsEntryPutInput = Experimental.ExperimentalSessionInstructionsEntryPutInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOperation`. Removed in the next minor. */ +export type SessionInstructionsEntryPutOperation = + Experimental.ExperimentalSessionInstructionsEntryPutOperation + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryPutOutput = Experimental.ExperimentalSessionInstructionsEntryPutOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveInput`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveInput = Experimental.ExperimentalSessionInstructionsEntryRemoveInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOperation`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveOperation = + Experimental.ExperimentalSessionInstructionsEntryRemoveOperation + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveOutput = Experimental.ExperimentalSessionInstructionsEntryRemoveOutput + +/** @deprecated Use `ExperimentalSessionLogInput`. Removed in the next minor. */ +export type SessionLogInput = Experimental.ExperimentalSessionLogInput + +/** @deprecated Use `ExperimentalSessionLogOperation`. Removed in the next minor. */ +export type SessionLogOperation = Experimental.ExperimentalSessionLogOperation + +/** @deprecated Use `ExperimentalSessionLogOutput`. Removed in the next minor. */ +export type SessionLogOutput = Experimental.ExperimentalSessionLogOutput + +/** @deprecated Use `ExperimentalSessionSkillInput`. Removed in the next minor. */ +export type SessionSkillInput = Experimental.ExperimentalSessionSkillInput + +/** @deprecated Use `ExperimentalSessionSkillOperation`. Removed in the next minor. */ +export type SessionSkillOperation = Experimental.ExperimentalSessionSkillOperation + +/** @deprecated Use `ExperimentalSessionSkillOutput`. Removed in the next minor. */ +export type SessionSkillOutput = Experimental.ExperimentalSessionSkillOutput + +/** @deprecated Use `ExperimentalSessionStatsInput`. Removed in the next minor. */ +export type SessionStatsInput = Experimental.ExperimentalSessionStatsInput + +/** @deprecated Use `ExperimentalSessionStatsOperation`. Removed in the next minor. */ +export type SessionStatsOperation = Experimental.ExperimentalSessionStatsOperation + +/** @deprecated Use `ExperimentalSessionStatsOutput`. Removed in the next minor. */ +export type SessionStatsOutput = Experimental.ExperimentalSessionStatsOutput + +/** @deprecated Use `ExperimentalSessionWaitInput`. Removed in the next minor. */ +export type SessionWaitInput = Experimental.ExperimentalSessionWaitInput + +/** @deprecated Use `ExperimentalSessionWaitOperation`. Removed in the next minor. */ +export type SessionWaitOperation = Experimental.ExperimentalSessionWaitOperation + +/** @deprecated Use `ExperimentalSessionWaitOutput`. Removed in the next minor. */ +export type SessionWaitOutput = Experimental.ExperimentalSessionWaitOutput + +/** @deprecated Use `ExperimentalUsageApi`. Removed in the next minor. */ +export type UsageApi = Experimental.ExperimentalUsageApi + +/** @deprecated Use `ExperimentalUsageLimitsInput`. Removed in the next minor. */ +export type UsageLimitsInput = Experimental.ExperimentalUsageLimitsInput + +/** @deprecated Use `ExperimentalUsageLimitsOperation`. Removed in the next minor. */ +export type UsageLimitsOperation = Experimental.ExperimentalUsageLimitsOperation + +/** @deprecated Use `ExperimentalUsageLimitsOutput`. Removed in the next minor. */ +export type UsageLimitsOutput = Experimental.ExperimentalUsageLimitsOutput + +/** @deprecated Use `ExperimentalVoiceApi`. Removed in the next minor. */ +export type VoiceApi = Experimental.ExperimentalVoiceApi + +/** @deprecated Use `ExperimentalVoiceModelsOperation`. Removed in the next minor. */ +export type VoiceModelsOperation = Experimental.ExperimentalVoiceModelsOperation + +/** @deprecated Use `ExperimentalVoiceModelsOutput`. Removed in the next minor. */ +export type VoiceModelsOutput = Experimental.ExperimentalVoiceModelsOutput + +/** @deprecated Use `ExperimentalVoiceSessionInput`. Removed in the next minor. */ +export type VoiceSessionInput = Experimental.ExperimentalVoiceSessionInput + +/** @deprecated Use `ExperimentalVoiceSessionOperation`. Removed in the next minor. */ +export type VoiceSessionOperation = Experimental.ExperimentalVoiceSessionOperation + +/** @deprecated Use `ExperimentalVoiceSessionOutput`. Removed in the next minor. */ +export type VoiceSessionOutput = Experimental.ExperimentalVoiceSessionOutput diff --git a/packages/client/src/promise/api.ts b/packages/client/src/promise/api.ts index 12bec6b7971b..2c3d7d67c114 100644 --- a/packages/client/src/promise/api.ts +++ b/packages/client/src/promise/api.ts @@ -10,6 +10,8 @@ export type CommandApi = Client["command"] export type ConfigApi = Client["config"] export type CredentialApi = Client["credential"] export type EventApi = Client["event"] +/** @deprecated Use `ExperimentalGenerateApi`. Removed in the next minor. */ +export type GenerateApi = Client["generate"] export type ExperimentalGenerateApi = Client["experimental"]["generate"] export type ExperimentalSessionApi = Client["experimental"]["session"] export type IntegrationApi = Client["integration"] diff --git a/packages/client/src/promise/client.ts b/packages/client/src/promise/client.ts index cbcab3dcdf5c..84ded7fc33f2 100644 --- a/packages/client/src/promise/client.ts +++ b/packages/client/src/promise/client.ts @@ -1,5 +1,6 @@ export * as OpenCode from "./client.js" +import { DeprecatedAliases } from "../deprecated-aliases.js" import { SharedEvents } from "../shared-events.js" import { OpenCode } from "./generated/index.js" import type { ClientOptions } from "./generated/client.js" @@ -10,8 +11,15 @@ export type { ClientOptions, RequestOptions } from "./generated/client.js" export function make(options: ClientOptions) { const raw = OpenCode.make(options) const events = SharedEvents.make((signal, onActivity) => raw.event.subscribe(undefined, { signal, onActivity })) + const deprecated = DeprecatedAliases.make(raw) return { ...raw, + ...deprecated, + file: { + ...deprecated.file, + /** @deprecated Use `client.experimental.file.write`. Removed in the next minor. */ + write: raw.experimental.file.write, + }, rpc: Object.assign(makeRpc(raw, events), raw.rpc), event: events, } diff --git a/packages/client/src/promise/deprecated.ts b/packages/client/src/promise/deprecated.ts new file mode 100644 index 000000000000..163f86b89aed --- /dev/null +++ b/packages/client/src/promise/deprecated.ts @@ -0,0 +1,141 @@ +// Pre-experimental names of the types #59072 renamed with an `Experimental` prefix. Removed in the next minor; see +// .github/RELEASE_REMINDERS.md. +import type * as Experimental from "./generated/types.js" + +/** @deprecated Use `ExperimentalConfigUpdateInput`. Removed in the next minor. */ +export type ConfigUpdateInput = Experimental.ExperimentalConfigUpdateInput + +/** @deprecated Use `ExperimentalConfigUpdateOutput`. Removed in the next minor. */ +export type ConfigUpdateOutput = Experimental.ExperimentalConfigUpdateOutput + +/** @deprecated Use `ExperimentalFileUnwatchInput`. Removed in the next minor. */ +export type FileUnwatchInput = Experimental.ExperimentalFileUnwatchInput + +/** @deprecated Use `ExperimentalFileUnwatchOutput`. Removed in the next minor. */ +export type FileUnwatchOutput = Experimental.ExperimentalFileUnwatchOutput + +/** @deprecated Use `ExperimentalFileWatchInput`. Removed in the next minor. */ +export type FileWatchInput = Experimental.ExperimentalFileWatchInput + +/** @deprecated Use `ExperimentalFileWatchOutput`. Removed in the next minor. */ +export type FileWatchOutput = Experimental.ExperimentalFileWatchOutput + +/** @deprecated Use `ExperimentalFileWriteInput`. Removed in the next minor. */ +export type FileWriteInput = Experimental.ExperimentalFileWriteInput + +/** @deprecated Use `ExperimentalFileWriteOutput`. Removed in the next minor. */ +export type FileWriteOutput = Experimental.ExperimentalFileWriteOutput + +/** @deprecated Use `ExperimentalGenerateTextInput`. Removed in the next minor. */ +export type GenerateTextInput = Experimental.ExperimentalGenerateTextInput + +/** @deprecated Use `ExperimentalGenerateTextOutput`. Removed in the next minor. */ +export type GenerateTextOutput = Experimental.ExperimentalGenerateTextOutput + +/** @deprecated Use `ExperimentalIntegrationWellknownAddInput`. Removed in the next minor. */ +export type IntegrationWellknownAddInput = Experimental.ExperimentalIntegrationWellknownAddInput + +/** @deprecated Use `ExperimentalIntegrationWellknownAddOutput`. Removed in the next minor. */ +export type IntegrationWellknownAddOutput = Experimental.ExperimentalIntegrationWellknownAddOutput + +/** @deprecated Use `ExperimentalMcpAddInput`. Removed in the next minor. */ +export type McpAddInput = Experimental.ExperimentalMcpAddInput + +/** @deprecated Use `ExperimentalMcpAddOutput`. Removed in the next minor. */ +export type McpAddOutput = Experimental.ExperimentalMcpAddOutput + +/** @deprecated Use `ExperimentalMcpClientTokenInput`. Removed in the next minor. */ +export type McpClientTokenInput = Experimental.ExperimentalMcpClientTokenInput + +/** @deprecated Use `ExperimentalMcpClientTokenOutput`. Removed in the next minor. */ +export type McpClientTokenOutput = Experimental.ExperimentalMcpClientTokenOutput + +/** @deprecated Use `ExperimentalMcpConnectInput`. Removed in the next minor. */ +export type McpConnectInput = Experimental.ExperimentalMcpConnectInput + +/** @deprecated Use `ExperimentalMcpConnectOutput`. Removed in the next minor. */ +export type McpConnectOutput = Experimental.ExperimentalMcpConnectOutput + +/** @deprecated Use `ExperimentalMcpDisconnectInput`. Removed in the next minor. */ +export type McpDisconnectInput = Experimental.ExperimentalMcpDisconnectInput + +/** @deprecated Use `ExperimentalMcpDisconnectOutput`. Removed in the next minor. */ +export type McpDisconnectOutput = Experimental.ExperimentalMcpDisconnectOutput + +/** @deprecated Use `ExperimentalMcpRemoveInput`. Removed in the next minor. */ +export type McpRemoveInput = Experimental.ExperimentalMcpRemoveInput + +/** @deprecated Use `ExperimentalMcpRemoveOutput`. Removed in the next minor. */ +export type McpRemoveOutput = Experimental.ExperimentalMcpRemoveOutput + +/** @deprecated Use `ExperimentalMigrationV1StatusOutput`. Removed in the next minor. */ +export type MigrationV1StatusOutput = Experimental.ExperimentalMigrationV1StatusOutput + +/** @deprecated Use `ExperimentalSessionExportInput`. Removed in the next minor. */ +export type SessionExportInput = Experimental.ExperimentalSessionExportInput + +/** @deprecated Use `ExperimentalSessionExportOutput`. Removed in the next minor. */ +export type SessionExportOutput = Experimental.ExperimentalSessionExportOutput + +/** @deprecated Use `ExperimentalSessionImportInput`. Removed in the next minor. */ +export type SessionImportInput = Experimental.ExperimentalSessionImportInput + +/** @deprecated Use `ExperimentalSessionImportOutput`. Removed in the next minor. */ +export type SessionImportOutput = Experimental.ExperimentalSessionImportOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListInput`. Removed in the next minor. */ +export type SessionInstructionsEntryListInput = Experimental.ExperimentalSessionInstructionsEntryListInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryListOutput = Experimental.ExperimentalSessionInstructionsEntryListOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutInput`. Removed in the next minor. */ +export type SessionInstructionsEntryPutInput = Experimental.ExperimentalSessionInstructionsEntryPutInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryPutOutput = Experimental.ExperimentalSessionInstructionsEntryPutOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveInput`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveInput = Experimental.ExperimentalSessionInstructionsEntryRemoveInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveOutput = Experimental.ExperimentalSessionInstructionsEntryRemoveOutput + +/** @deprecated Use `ExperimentalSessionLogInput`. Removed in the next minor. */ +export type SessionLogInput = Experimental.ExperimentalSessionLogInput + +/** @deprecated Use `ExperimentalSessionLogOutput`. Removed in the next minor. */ +export type SessionLogOutput = Experimental.ExperimentalSessionLogOutput + +/** @deprecated Use `ExperimentalSessionSkillInput`. Removed in the next minor. */ +export type SessionSkillInput = Experimental.ExperimentalSessionSkillInput + +/** @deprecated Use `ExperimentalSessionSkillOutput`. Removed in the next minor. */ +export type SessionSkillOutput = Experimental.ExperimentalSessionSkillOutput + +/** @deprecated Use `ExperimentalSessionStatsInput`. Removed in the next minor. */ +export type SessionStatsInput = Experimental.ExperimentalSessionStatsInput + +/** @deprecated Use `ExperimentalSessionStatsOutput`. Removed in the next minor. */ +export type SessionStatsOutput = Experimental.ExperimentalSessionStatsOutput + +/** @deprecated Use `ExperimentalSessionWaitInput`. Removed in the next minor. */ +export type SessionWaitInput = Experimental.ExperimentalSessionWaitInput + +/** @deprecated Use `ExperimentalSessionWaitOutput`. Removed in the next minor. */ +export type SessionWaitOutput = Experimental.ExperimentalSessionWaitOutput + +/** @deprecated Use `ExperimentalUsageLimitsInput`. Removed in the next minor. */ +export type UsageLimitsInput = Experimental.ExperimentalUsageLimitsInput + +/** @deprecated Use `ExperimentalUsageLimitsOutput`. Removed in the next minor. */ +export type UsageLimitsOutput = Experimental.ExperimentalUsageLimitsOutput + +/** @deprecated Use `ExperimentalVoiceModelsOutput`. Removed in the next minor. */ +export type VoiceModelsOutput = Experimental.ExperimentalVoiceModelsOutput + +/** @deprecated Use `ExperimentalVoiceSessionInput`. Removed in the next minor. */ +export type VoiceSessionInput = Experimental.ExperimentalVoiceSessionInput + +/** @deprecated Use `ExperimentalVoiceSessionOutput`. Removed in the next minor. */ +export type VoiceSessionOutput = Experimental.ExperimentalVoiceSessionOutput diff --git a/packages/client/src/promise/index.ts b/packages/client/src/promise/index.ts index 04d10d1eef5b..a75e6d58639c 100644 --- a/packages/client/src/promise/index.ts +++ b/packages/client/src/promise/index.ts @@ -1,6 +1,7 @@ import type { OpenCode } from "./client.js" export * from "./generated/index.js" +export type * from "./deprecated.js" export { OpenCode } from "./client.js" export type { AgentApi, diff --git a/packages/client/test/effect.test.ts b/packages/client/test/effect.test.ts index 211192538954..3bbc028ac118 100644 --- a/packages/client/test/effect.test.ts +++ b/packages/client/test/effect.test.ts @@ -74,6 +74,23 @@ test("session.get returns the decoded Effect projection", async () => { expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000) }) +test("pre-experimental paths stay as deprecated aliases of client.experimental", async () => { + const client = await OpenCode.make({ baseUrl: "http://localhost:3000" }).pipe( + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make(() => Effect.die("unused")), + ), + Effect.runPromise, + ) + expect(client.session.wait).toBe(client.experimental.session.wait) + expect(client.session.instructions).toBe(client.experimental.session.instructions) + expect(client.generate).toBe(client.experimental.generate) + expect(client.mcp.client).toBe(client.experimental.mcp.client) + expect(client.config.update).toBe(client.experimental.config.update) + expect(client.config.get).toBeFunction() + expect("write" in client.file).toBe(false) +}) + test("session instructions methods use the public HTTP contract", async () => { const requests: Array<{ method: string; url: string; body?: unknown }> = [] const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }] @@ -92,7 +109,9 @@ test("session instructions methods use the public HTTP contract", async () => { }) const result = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - const listed = yield* client.experimental.session.instructions.entry.list({ sessionID: Session.ID.make("ses_test") }) + const listed = yield* client.experimental.session.instructions.entry.list({ + sessionID: Session.ID.make("ses_test"), + }) yield* client.experimental.session.instructions.entry.put({ sessionID: Session.ID.make("ses_test"), key: "review-notes", @@ -314,7 +333,9 @@ test("session.log retains the typed SessionNotFoundError", async () => { ) const error = await Effect.gen(function* () { const client = yield* OpenCode.make({ baseUrl: "http://localhost:3000" }) - return yield* client.experimental.session.log({ sessionID: Session.ID.make("ses_missing") }).pipe(Stream.runCollect, Effect.flip) + return yield* client.experimental.session + .log({ sessionID: Session.ID.make("ses_missing") }) + .pipe(Stream.runCollect, Effect.flip) }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient), Effect.runPromise) expect(error._tag).toBe("SessionNotFoundError") diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 14b1738c4c9d..495c03bf6547 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -33,18 +33,22 @@ test("exposes every standard HTTP API group", () => { "debug", "websearch", "config", + "generate", + "voice", + "migration", + "usage", ]) expect(Object.keys(client.debug)).toEqual(["heapDump", "location"]) expect(Object.keys(client.debug.location)).toEqual(["list", "evict"]) expect(Object.keys(client.message)).toEqual(["list"]) - expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "oauth", "command"]) + expect(Object.keys(client.integration)).toEqual(["list", "get", "connect", "oauth", "command", "wellknown"]) expect(Object.keys(client.integration.connect)).toEqual(["key", "external"]) expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"]) expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"]) expect(Object.keys(client.websearch)).toEqual(["providers", "query"]) - expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) - expect(Object.keys(client.mcp)).toEqual(["list", "resource"]) - expect(Object.keys(client.config)).toEqual(["get", "shells"]) + expect(Object.keys(client.file)).toEqual(["read", "list", "find", "watch", "unwatch", "write"]) + expect(Object.keys(client.mcp)).toEqual(["list", "resource", "add", "remove", "connect", "disconnect", "client"]) + expect(Object.keys(client.config)).toEqual(["get", "shells", "update"]) expect(Object.keys(client.vcs)).toEqual(["init", "get", "base", "status", "branch", "diff"]) expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove", "connect"]) expect(Object.keys(client.pty.connect)).toEqual(["token"]) @@ -94,6 +98,7 @@ test("nests a method under client.experimental exactly when its route is experim { get: (_target, key) => (key === "signal" || key === "headers" || key === "onActivity" ? undefined : "x") }, ) const routes: Array<{ readonly method: string; readonly experimentalRoute: boolean }> = [] + const deprecated: Array = [] const visit = async (node: object, path: ReadonlyArray): Promise => { for (const [name, value] of Object.entries(node)) { // `rpc` is a callable that also carries its generated routes. @@ -101,10 +106,19 @@ test("nests a method under client.experimental exactly when its route is experim await visit(value, [...path, name]) continue } + const target = [...path, name].reduce( + (parent, key) => (parent as Record | undefined)?.[key], + client.experimental, + ) + if (path[0] !== "experimental" && target === value) { + deprecated.push([...path, name].join(".")) + continue + } url = undefined const result: unknown = value(input) if (typeof result === "object" && result !== null && Symbol.asyncIterator in result) { - await (result as AsyncIterable)[Symbol.asyncIterator]() + await (result as AsyncIterable) + [Symbol.asyncIterator]() .next() .catch(() => undefined) } else await Promise.resolve(result).catch(() => undefined) @@ -121,6 +135,33 @@ test("nests a method under client.experimental exactly when its route is experim .filter((route) => route.experimentalRoute !== route.method.startsWith("experimental.")) .map((route) => route.method), ).toEqual([]) + // Pre-experimental paths kept as deprecated aliases until the next minor (.github/RELEASE_REMINDERS.md). + expect(deprecated.toSorted()).toEqual([ + "config.update", + "file.unwatch", + "file.watch", + "file.write", + "generate.text", + "integration.wellknown.add", + "mcp.add", + "mcp.client.token", + "mcp.connect", + "mcp.disconnect", + "mcp.remove", + "migration.v1.status", + "session.export", + "session.import", + "session.instructions.entry.list", + "session.instructions.entry.put", + "session.instructions.entry.remove", + "session.log", + "session.skill", + "session.stats", + "session.wait", + "usage.limits", + "voice.models", + "voice.session", + ]) expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "output", "remove"]) expect(Object.keys(client.project)).toEqual(["list", "update"]) expect(Object.keys(client.worktree)).toEqual(["list", "create", "remove", "refresh"]) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 9a003b6d24dc..193c0d94a6a9 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -116,6 +116,36 @@ export const make = Effect.fn("PluginHost.make")(function* ( }).pipe(Effect.provide(locations.get(ref)), Effect.orDie), ) + const experimental: Plugin.Context["experimental"] = { + generate: { + text: (input) => generate.text(input).pipe(Effect.map((text) => ({ text }))), + }, + session: { + wait: (input) => sessions.wait(input.sessionID), + instructions: { + entry: { + list: Effect.fn(function* (input) { + yield* sessions.get(input.sessionID) + return yield* instructionEntries.list(input.sessionID) + }), + put: Effect.fn(function* (input) { + const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) + yield* sessions.get(input.sessionID) + yield* instructionEntries.put({ ...input, key }) + }), + remove: Effect.fn(function* (input) { + const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) + yield* sessions.get(input.sessionID) + yield* instructionEntries.remove({ ...input, key }) + }), + }, + }, + }, + terminal: { + read: (input) => persistentPty.read(input.sessionID, input.lines), + }, + } + // Keep the instance graph's inferred types independent of Session handles. const context: Plugin.Context = { app, @@ -278,35 +308,9 @@ export const make = Effect.fn("PluginHost.make")(function* ( ) }) as Plugin.Context["event"]["subscribe"], }, - experimental: { - generate: { - text: (input) => generate.text(input).pipe(Effect.map((text) => ({ text }))), - }, - session: { - wait: (input) => sessions.wait(input.sessionID), - instructions: { - entry: { - list: Effect.fn(function* (input) { - yield* sessions.get(input.sessionID) - return yield* instructionEntries.list(input.sessionID) - }), - put: Effect.fn(function* (input) { - const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) - yield* sessions.get(input.sessionID) - yield* instructionEntries.put({ ...input, key }) - }), - remove: Effect.fn(function* (input) { - const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) - yield* sessions.get(input.sessionID) - yield* instructionEntries.remove({ ...input, key }) - }), - }, - }, - }, - terminal: { - read: (input) => persistentPty.read(input.sessionID, input.lines), - }, - }, + experimental, + // Deprecated pre-experimental path; removed in the next minor (.github/RELEASE_REMINDERS.md). + generate: experimental.generate, integration: { list: () => response(integration.list()), get: Effect.fn(function* (input) { @@ -619,6 +623,9 @@ export const make = Effect.fn("PluginHost.make")(function* ( .interrupt(input.sessionID, { resume: input.resume }) .pipe(Effect.map((interrupted) => ({ interrupted }))), context: (input) => sessions.context(input.sessionID), + // Deprecated pre-experimental paths; removed in the next minor (.github/RELEASE_REMINDERS.md). + wait: experimental.session.wait, + instructions: experimental.session.instructions, }, } return context diff --git a/packages/core/test/plugin/experimental-namespace.test.ts b/packages/core/test/plugin/experimental-namespace.test.ts index 3683c692d94c..f0eead7ca377 100644 --- a/packages/core/test/plugin/experimental-namespace.test.ts +++ b/packages/core/test/plugin/experimental-namespace.test.ts @@ -21,38 +21,58 @@ test("ctx.experimental holds exactly the plugin methods backed by /api/experimen ), ) - const hostPaths = new Map() - walk(effectContext, (path, value) => hostPaths.set(value, path)) - const methods: Array<{ readonly promise: string; readonly host: string | undefined; readonly endpoint: string }> = [] - walk(promiseContext, (path, value) => { + const hostPaths = paths(effectContext) + const methods = Array.from(paths(promiseContext)).flatMap(([value, promise]) => { const adapted = adaptedEndpoints.get(value) - if (adapted) methods.push({ promise: path, host: hostPaths.get(adapted.method), endpoint: adapted.endpoint.path }) + return adapted ? [{ promise, host: hostPaths.get(adapted.method) ?? [], endpoint: adapted.endpoint.path }] : [] }) const isExperimental = (endpoint: string) => endpoint === "/api/experimental" || endpoint.startsWith("/api/experimental/") + const underExperimental = (list: ReadonlyArray) => list.some((path) => path.startsWith("experimental.")) expect(methods.length).toBeGreaterThan(40) - expect(methods.filter(({ endpoint }) => isExperimental(endpoint)).map(({ promise }) => promise)).toContain( + expect(methods.filter(({ endpoint }) => isExperimental(endpoint)).flatMap(({ promise }) => promise)).toContain( "experimental.session.wait", ) expect( methods .filter( ({ promise, host, endpoint }) => - isExperimental(endpoint) !== promise.startsWith("experimental.") || - isExperimental(endpoint) !== Boolean(host?.startsWith("experimental.")), + isExperimental(endpoint) !== underExperimental(promise) || + isExperimental(endpoint) !== underExperimental(host), ) - .map(({ promise, host, endpoint }) => `ctx.${promise} (host ${host ?? "unknown"}) -> ${endpoint}`), + .map( + ({ promise, host, endpoint }) => + `ctx.${promise.join("|")} (host ${host.join("|") || "unknown"}) -> ${endpoint}`, + ), ).toEqual([]) + + // Pre-experimental paths kept as deprecated aliases until the next minor (.github/RELEASE_REMINDERS.md). + const deprecated = [ + "generate.text", + "session.instructions.entry.list", + "session.instructions.entry.put", + "session.instructions.entry.remove", + "session.wait", + ] + const outside = (pick: (method: (typeof methods)[number]) => ReadonlyArray) => + methods + .filter(({ endpoint }) => isExperimental(endpoint)) + .flatMap((method) => pick(method).filter((path) => !path.startsWith("experimental."))) + .toSorted() + expect(outside((method) => method.promise)).toEqual(deprecated) + expect(outside((method) => method.host)).toEqual(deprecated) }) -function walk(root: unknown, visit: (path: string, value: object) => void) { - const seen = new Set() - const step = (value: unknown, path: ReadonlyArray) => { - if ((typeof value !== "object" && typeof value !== "function") || value === null || seen.has(value)) return - seen.add(value) - if (typeof value === "function") visit(path.join("."), value) - Object.entries(value).forEach(([key, child]) => step(child, [...path, key])) +// Every path to every function, so a method reachable from both its home and a deprecated alias lists both. +function paths(root: unknown) { + const result = new Map>() + const step = (value: unknown, path: ReadonlyArray, ancestors: ReadonlySet) => { + if ((typeof value !== "object" && typeof value !== "function") || value === null || ancestors.has(value)) return + if (typeof value === "function") result.set(value, [...(result.get(value) ?? []), path.join(".")]) + const next = new Set(ancestors).add(value) + Object.entries(value).forEach(([key, child]) => step(child, [...path, key], next)) } - step(root, []) + step(root, [], new Set()) + return result } diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index 05fd46dad8e7..c551f026a076 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -12,11 +12,29 @@ import { WebSearch } from "@opencode/core/websearch" import { Effect, Stream } from "effect" type EventSource = ReturnType -type Overrides = Partial> & { +type Overrides = Partial> & { readonly session?: Partial readonly event?: { readonly subscribe: () => EventSource } } export function host(overrides: Overrides = {}): Plugin.Context { + const experimental: Plugin.Context["experimental"] = overrides.experimental ?? { + generate: { + text: () => Effect.die("unused experimental.generate.text"), + }, + session: { + wait: () => Effect.die("unused experimental.session.wait"), + instructions: { + entry: { + list: () => Effect.die("unused experimental.session.instructions.entry.list"), + put: () => Effect.die("unused experimental.session.instructions.entry.put"), + remove: () => Effect.die("unused experimental.session.instructions.entry.remove"), + }, + }, + }, + terminal: { + read: () => Effect.die("unused experimental.terminal.read"), + }, + } return { app: overrides.app ?? { name: "test", version: "test", channel: "test" }, location: @@ -68,24 +86,8 @@ export function host(overrides: Overrides = {}): Plugin.Context { activate: () => Effect.die("unused credential.activate"), }, event: { subscribe: events(overrides.event?.subscribe ?? (() => Stream.empty)) }, - experimental: overrides.experimental ?? { - generate: { - text: () => Effect.die("unused experimental.generate.text"), - }, - session: { - wait: () => Effect.die("unused experimental.session.wait"), - instructions: { - entry: { - list: () => Effect.die("unused experimental.session.instructions.entry.list"), - put: () => Effect.die("unused experimental.session.instructions.entry.put"), - remove: () => Effect.die("unused experimental.session.instructions.entry.remove"), - }, - }, - }, - terminal: { - read: () => Effect.die("unused experimental.terminal.read"), - }, - }, + experimental, + generate: experimental.generate, integration: overrides.integration ?? { list: () => Effect.die("unused integration.list"), get: () => Effect.die("unused integration.get"), @@ -194,6 +196,8 @@ export function host(overrides: Overrides = {}): Plugin.Context { synthetic: overrides.session?.synthetic ?? (() => Effect.die("unused session.synthetic")), interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")), context: overrides.session?.context ?? (() => Effect.die("unused session.context")), + wait: experimental.session.wait, + instructions: experimental.session.instructions, }, } } diff --git a/packages/plugin/src/effect/integration.ts b/packages/plugin/src/effect/integration.ts index 970ce3503cb5..7f9d2cf46b25 100644 --- a/packages/plugin/src/effect/integration.ts +++ b/packages/plugin/src/effect/integration.ts @@ -99,7 +99,7 @@ export interface IntegrationEditor { } } -export interface IntegrationDomain extends IntegrationApi { +export interface IntegrationDomain extends Omit, "wellknown"> { readonly transform: Transform readonly reload: () => Effect.Effect readonly connection: { diff --git a/packages/plugin/src/effect/plugin.ts b/packages/plugin/src/effect/plugin.ts index b4f7a532e6d6..2e376bbf6412 100644 --- a/packages/plugin/src/effect/plugin.ts +++ b/packages/plugin/src/effect/plugin.ts @@ -46,6 +46,8 @@ export interface Context { readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain + /** @deprecated Use `ctx.experimental.generate`. Removed in the next minor. */ + readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick, "list"> readonly provider: ProviderDomain diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index 010bc67d550d..de6f4d0ae05c 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -1,4 +1,4 @@ -import type { SessionApi } from "@opencode/client/effect/api" +import type { ExperimentalSessionApi, SessionApi } from "@opencode/client/effect/api" import type { GenerationOptionsFields, Message, SystemPart } from "@opencode/ai/effect" import type { Agent } from "@opencode/schema/agent" import type { Model } from "@opencode/schema/model" @@ -170,4 +170,8 @@ export type SessionDomain = Pick< | "context" > & { readonly hook: ModelHooks + /** @deprecated Use `ctx.experimental.session.wait`. Removed in the next minor. */ + readonly wait: ExperimentalSessionApi["wait"] + /** @deprecated Use `ctx.experimental.session.instructions`. Removed in the next minor. */ + readonly instructions: ExperimentalSessionApi["instructions"] } diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index 5da0732cff4f..4d00b44e2990 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -300,6 +300,34 @@ export function fromPromise(plugin: Plugin) { }), ) + const experimental: PromiseContext["experimental"] = { + generate: { + text: adaptApiMethod(GenerateEndpoints["generate.text"], host.experimental.generate.text), + }, + session: { + wait: adaptApiMethod(SessionEndpoints["session.wait"], host.experimental.session.wait), + instructions: { + entry: { + list: adaptApiMethod( + SessionEndpoints["session.instructions.entry.list"], + host.experimental.session.instructions.entry.list, + ), + put: adaptApiMethod( + SessionEndpoints["session.instructions.entry.put"], + host.experimental.session.instructions.entry.put, + ), + remove: adaptApiMethod( + SessionEndpoints["session.instructions.entry.remove"], + host.experimental.session.instructions.entry.remove, + ), + }, + }, + }, + terminal: { + read: adaptApiMethod(ExperimentalEndpoints["persistentPty.read"], host.experimental.terminal.read), + }, + } + const context2: PromiseContext = { app: host.app, location: host.location, @@ -348,33 +376,9 @@ export function fromPromise(plugin: Plugin) { options, ), }, - experimental: { - generate: { - text: adaptApiMethod(GenerateEndpoints["generate.text"], host.experimental.generate.text), - }, - session: { - wait: adaptApiMethod(SessionEndpoints["session.wait"], host.experimental.session.wait), - instructions: { - entry: { - list: adaptApiMethod( - SessionEndpoints["session.instructions.entry.list"], - host.experimental.session.instructions.entry.list, - ), - put: adaptApiMethod( - SessionEndpoints["session.instructions.entry.put"], - host.experimental.session.instructions.entry.put, - ), - remove: adaptApiMethod( - SessionEndpoints["session.instructions.entry.remove"], - host.experimental.session.instructions.entry.remove, - ), - }, - }, - }, - terminal: { - read: adaptApiMethod(ExperimentalEndpoints["persistentPty.read"], host.experimental.terminal.read), - }, - }, + experimental, + // Deprecated pre-experimental path; removed in the next minor (.github/RELEASE_REMINDERS.md). + generate: experimental.generate, model: { list: adaptApiMethod(ModelEndpoints["model.list"], host.model.list), default: adaptApiMethod(ModelEndpoints["model.default"], host.model.default), @@ -638,6 +642,9 @@ export function fromPromise(plugin: Plugin) { update: adaptApiMethod(SessionEndpoints["session.update"], host.session.update), move: adaptApiMethod(SessionEndpoints["session.move"], host.session.move), context: adaptApiMethod(SessionEndpoints["session.context"], host.session.context), + // Deprecated pre-experimental paths; removed in the next minor (.github/RELEASE_REMINDERS.md). + wait: experimental.session.wait, + instructions: experimental.session.instructions, }, shell: { hook: (name, callback) => diff --git a/packages/plugin/src/promise/integration.ts b/packages/plugin/src/promise/integration.ts index 437dfb107a5d..358eeaeb65e2 100644 --- a/packages/plugin/src/promise/integration.ts +++ b/packages/plugin/src/promise/integration.ts @@ -91,7 +91,7 @@ export interface IntegrationEditor { } } -export interface IntegrationDomain extends IntegrationApi { +export interface IntegrationDomain extends Omit { readonly transform: Transform readonly reload: () => Promise readonly connection: { diff --git a/packages/plugin/src/promise/plugin.ts b/packages/plugin/src/promise/plugin.ts index 80a3c8a6ffdf..08b6fd3c8c71 100644 --- a/packages/plugin/src/promise/plugin.ts +++ b/packages/plugin/src/promise/plugin.ts @@ -45,6 +45,8 @@ export interface Context { readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain + /** @deprecated Use `ctx.experimental.generate`. Removed in the next minor. */ + readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick readonly provider: ProviderDomain diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index 77b84ec67c7a..45326b134649 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -1,4 +1,4 @@ -import type { SessionApi } from "@opencode/client/promise/api" +import type { ExperimentalSessionApi, SessionApi } from "@opencode/client/promise/api" import type { GenerationOptionsFields, Message, SystemPart } from "@opencode/ai/effect" import type { Agent } from "@opencode/schema/agent" import type { Model } from "@opencode/schema/model" @@ -170,4 +170,8 @@ export type SessionDomain = Pick< | "context" > & { readonly hook: ModelHooks + /** @deprecated Use `ctx.experimental.session.wait`. Removed in the next minor. */ + readonly wait: ExperimentalSessionApi["wait"] + /** @deprecated Use `ctx.experimental.session.instructions`. Removed in the next minor. */ + readonly instructions: ExperimentalSessionApi["instructions"] } diff --git a/services/www/src/docs/content/build/plugins/effect.mdx b/services/www/src/docs/content/build/plugins/effect.mdx index ccda2801b543..76291e31df85 100644 --- a/services/www/src/docs/content/build/plugins/effect.mdx +++ b/services/www/src/docs/content/build/plugins/effect.mdx @@ -630,7 +630,7 @@ interface IntegrationEditor { } } -interface IntegrationDomain extends IntegrationApi { +interface IntegrationDomain extends Omit, "wellknown"> { readonly transform: Transform readonly reload: () => Effect.Effect readonly connection: { @@ -900,9 +900,10 @@ type SessionDomain = Pick< > ``` -Endpoints under `/api/experimental` reach plugins only through `ctx.experimental`: `ctx.experimental.generate`, +Endpoints under `/api/experimental` reach plugins through `ctx.experimental`: `ctx.experimental.generate`, `ctx.experimental.session.wait`, `ctx.experimental.session.instructions` and `ctx.experimental.terminal.read`. They -may change between releases. +may change between releases. The older `ctx.generate`, `ctx.session.wait` and `ctx.session.instructions` still work +but are deprecated and will be removed in the next minor release. ### Skills diff --git a/services/www/src/docs/content/build/plugins/index.mdx b/services/www/src/docs/content/build/plugins/index.mdx index 9f786fd92851..bf6b5edfa2ad 100644 --- a/services/www/src/docs/content/build/plugins/index.mdx +++ b/services/www/src/docs/content/build/plugins/index.mdx @@ -845,9 +845,10 @@ const entries = await ctx.experimental.session.instructions.entry.list({ session await ctx.experimental.session.instructions.entry.remove({ sessionID, key: "deploy-target" }) ``` -Endpoints under `/api/experimental` reach plugins only through `ctx.experimental`: `ctx.experimental.generate`, +Endpoints under `/api/experimental` reach plugins through `ctx.experimental`: `ctx.experimental.generate`, `ctx.experimental.session.wait`, `ctx.experimental.session.instructions` and `ctx.experimental.terminal.read`. They -may change between releases. +may change between releases. The older `ctx.generate`, `ctx.session.wait` and `ctx.session.instructions` still work +but are deprecated and will be removed in the next minor release. #### Reference @@ -879,11 +880,11 @@ interface ExperimentalSessionContext { instructions: { entry: { list( - input: SessionInstructionsEntryListInput, + input: ExperimentalSessionInstructionsEntryListInput, requestOptions?: RequestOptions, ): Promise - put(input: SessionInstructionsEntryPutInput, requestOptions?: RequestOptions): Promise - remove(input: SessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions): Promise + put(input: ExperimentalSessionInstructionsEntryPutInput, requestOptions?: RequestOptions): Promise + remove(input: ExperimentalSessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions): Promise } } } From da96c1cc5b9b738a9dc8b7285e3301cf50129ec3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 26 Oct 2026 16:25:17 -0700 Subject: [PATCH 09/11] fix(sdk): keep pre-experimental members on the Effect group interfaces --- .github/RELEASE_REMINDERS.md | 2 +- packages/client/src/effect/deprecated.ts | 61 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/.github/RELEASE_REMINDERS.md b/.github/RELEASE_REMINDERS.md index a000a3b462fa..12ea617ebc72 100644 --- a/.github/RELEASE_REMINDERS.md +++ b/.github/RELEASE_REMINDERS.md @@ -16,7 +16,7 @@ Read this before cutting a minor or major release, act on every item in its sect - `client.mcp.{add,remove,connect,disconnect}` and `client.mcp.client.token` - `client.file.{watch,unwatch}` and, in the Promise client only, `client.file.write` - `client.config.update` - - SDK types renamed with an `Experimental` prefix (`packages/client/src/promise/deprecated.ts`, `packages/client/src/effect/deprecated.ts`, and `GenerateApi` in `packages/client/src/promise/api.ts`), for example `SessionWaitInput` → `ExperimentalSessionWaitInput`. + - SDK types renamed with an `Experimental` prefix (`packages/client/src/promise/deprecated.ts`, `packages/client/src/effect/deprecated.ts`, and `GenerateApi` in `packages/client/src/promise/api.ts`), for example `SessionWaitInput` → `ExperimentalSessionWaitInput`. `packages/client/src/effect/deprecated.ts` also augments the Effect group interfaces (`AppApi`, `SessionApi`, `IntegrationApi`, `McpApi`, `FileApi`, `ConfigApi`) with the old members. - Plugin `Context` paths (`packages/plugin/src/{effect,promise}/{plugin,session}.ts`, wired in `packages/core/src/plugin/host.ts`, `packages/plugin/src/promise/adapter.ts` and `packages/core/test/plugin/host.ts`), each now under `ctx.experimental`: - `ctx.generate` - `ctx.session.wait` diff --git a/packages/client/src/effect/deprecated.ts b/packages/client/src/effect/deprecated.ts index d7c97a681e60..517407c6bb98 100644 --- a/packages/client/src/effect/deprecated.ts +++ b/packages/client/src/effect/deprecated.ts @@ -217,3 +217,64 @@ export type VoiceSessionOperation = Experimental.ExperimentalVoiceSes /** @deprecated Use `ExperimentalVoiceSessionOutput`. Removed in the next minor. */ export type VoiceSessionOutput = Experimental.ExperimentalVoiceSessionOutput + +// The group interfaces keep their pre-experimental members too, so code typed against them still compiles. +declare module "./api/api.js" { + interface AppApi { + /** @deprecated Use `client.experimental.generate`. Removed in the next minor. */ + readonly generate: ExperimentalGenerateApi + /** @deprecated Use `client.experimental.voice`. Removed in the next minor. */ + readonly voice: ExperimentalVoiceApi + /** @deprecated Use `client.experimental.migration`. Removed in the next minor. */ + readonly migration: ExperimentalMigrationApi + /** @deprecated Use `client.experimental.usage`. Removed in the next minor. */ + readonly usage: ExperimentalUsageApi + } + + interface SessionApi { + /** @deprecated Use `client.experimental.session.stats`. Removed in the next minor. */ + readonly stats: ExperimentalSessionApi["stats"] + /** @deprecated Use `client.experimental.session.import`. Removed in the next minor. */ + readonly import: ExperimentalSessionApi["import"] + /** @deprecated Use `client.experimental.session.export`. Removed in the next minor. */ + readonly export: ExperimentalSessionApi["export"] + /** @deprecated Use `client.experimental.session.skill`. Removed in the next minor. */ + readonly skill: ExperimentalSessionApi["skill"] + /** @deprecated Use `client.experimental.session.wait`. Removed in the next minor. */ + readonly wait: ExperimentalSessionApi["wait"] + /** @deprecated Use `client.experimental.session.instructions`. Removed in the next minor. */ + readonly instructions: ExperimentalSessionApi["instructions"] + /** @deprecated Use `client.experimental.session.log`. Removed in the next minor. */ + readonly log: ExperimentalSessionApi["log"] + } + + interface IntegrationApi { + /** @deprecated Use `client.experimental.integration.wellknown`. Removed in the next minor. */ + readonly wellknown: ExperimentalIntegrationApi["wellknown"] + } + + interface McpApi { + /** @deprecated Use `client.experimental.mcp.add`. Removed in the next minor. */ + readonly add: ExperimentalMcpApi["add"] + /** @deprecated Use `client.experimental.mcp.remove`. Removed in the next minor. */ + readonly remove: ExperimentalMcpApi["remove"] + /** @deprecated Use `client.experimental.mcp.connect`. Removed in the next minor. */ + readonly connect: ExperimentalMcpApi["connect"] + /** @deprecated Use `client.experimental.mcp.disconnect`. Removed in the next minor. */ + readonly disconnect: ExperimentalMcpApi["disconnect"] + /** @deprecated Use `client.experimental.mcp.client`. Removed in the next minor. */ + readonly client: ExperimentalMcpApi["client"] + } + + interface FileApi { + /** @deprecated Use `client.experimental.file.watch`. Removed in the next minor. */ + readonly watch: ExperimentalFileApi["watch"] + /** @deprecated Use `client.experimental.file.unwatch`. Removed in the next minor. */ + readonly unwatch: ExperimentalFileApi["unwatch"] + } + + interface ConfigApi { + /** @deprecated Use `client.experimental.config.update`. Removed in the next minor. */ + readonly update: ExperimentalConfigApi["update"] + } +} From 5bee8ee9b258ba31de56acf6524dd0d13a932b30 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 26 Oct 2026 16:53:26 -0700 Subject: [PATCH 10/11] feat(client): generate deprecated SDK aliases from the codegen compile(..., { aliases: true }) keeps each pre-experimental client path and type name as a @deprecated alias of its client.experimental replacement, removed in the next minor. Replaces the hand-written alias files and reverts the plugin context to its dev names. --- .github/RELEASE_REMINDERS.md | 26 +- AGENTS.md | 4 +- packages/client/script/generate.ts | 22 +- packages/client/src/deprecated-aliases.ts | 100 ------ packages/client/src/effect/api.ts | 1 - packages/client/src/effect/api/api.ts | 309 ++++++++++++++++++ packages/client/src/effect/client.ts | 2 - packages/client/src/effect/deprecated.ts | 280 ---------------- .../client/src/effect/generated/client.ts | 175 +++++++--- packages/client/src/promise/client.ts | 8 - packages/client/src/promise/deprecated.ts | 141 -------- .../client/src/promise/generated/client.ts | 95 +++++- .../client/src/promise/generated/types.ts | 138 ++++++++ packages/client/src/promise/index.ts | 1 - .../client/test/deprecated-aliases.test.ts | 45 +++ packages/client/test/effect.test.ts | 17 - packages/client/test/promise.test.ts | 2 +- packages/core/src/plugin/host.ts | 63 ++-- .../plugin/experimental-namespace.test.ts | 78 ----- packages/core/test/plugin/host.ts | 40 +-- .../test/plugin/session-instructions.test.ts | 52 ++- packages/httpapi-codegen/src/index.ts | 309 +++++++++++++++--- .../httpapi-codegen/test/generate.test.ts | 94 ++++++ packages/plugin/src/effect/plugin.ts | 11 +- packages/plugin/src/effect/session.ts | 11 +- packages/plugin/src/promise/adapter.ts | 71 ++-- packages/plugin/src/promise/plugin.ts | 10 +- packages/plugin/src/promise/session.ts | 11 +- .../src/docs/content/build/plugins/effect.mdx | 19 +- .../src/docs/content/build/plugins/index.mdx | 45 +-- 30 files changed, 1236 insertions(+), 944 deletions(-) delete mode 100644 packages/client/src/deprecated-aliases.ts delete mode 100644 packages/client/src/effect/deprecated.ts delete mode 100644 packages/client/src/promise/deprecated.ts create mode 100644 packages/client/test/deprecated-aliases.test.ts delete mode 100644 packages/core/test/plugin/experimental-namespace.test.ts diff --git a/.github/RELEASE_REMINDERS.md b/.github/RELEASE_REMINDERS.md index 12ea617ebc72..bcb3c7c9ef2c 100644 --- a/.github/RELEASE_REMINDERS.md +++ b/.github/RELEASE_REMINDERS.md @@ -2,27 +2,13 @@ Read this before cutting a minor or major release, act on every item in its section, and delete each item once it ships. Add an item whenever a change leaves something to finish in a later release, such as a deprecated alias to remove. -## Next minor +## Remove at next minor -- [ ] Remove the deprecated pre-experimental SDK and plugin aliases from [#59072](https://github.com/anomalyco/opencode/pull/59072): - - SDK methods (`packages/client/src/deprecated-aliases.ts`, plus `file.write` in `packages/client/src/promise/client.ts`), each now at `client.experimental.`: - - `client.session.{stats,import,export,skill,wait,log}` - - `client.session.instructions.entry.{list,put,remove}` - - `client.generate.text` - - `client.voice.{models,session}` - - `client.migration.v1.status` - - `client.usage.limits` - - `client.integration.wellknown.add` - - `client.mcp.{add,remove,connect,disconnect}` and `client.mcp.client.token` - - `client.file.{watch,unwatch}` and, in the Promise client only, `client.file.write` - - `client.config.update` - - SDK types renamed with an `Experimental` prefix (`packages/client/src/promise/deprecated.ts`, `packages/client/src/effect/deprecated.ts`, and `GenerateApi` in `packages/client/src/promise/api.ts`), for example `SessionWaitInput` → `ExperimentalSessionWaitInput`. `packages/client/src/effect/deprecated.ts` also augments the Effect group interfaces (`AppApi`, `SessionApi`, `IntegrationApi`, `McpApi`, `FileApi`, `ConfigApi`) with the old members. - - Plugin `Context` paths (`packages/plugin/src/{effect,promise}/{plugin,session}.ts`, wired in `packages/core/src/plugin/host.ts`, `packages/plugin/src/promise/adapter.ts` and `packages/core/test/plugin/host.ts`), each now under `ctx.experimental`: - - `ctx.generate` - - `ctx.session.wait` - - `ctx.session.instructions` - - Then drop the `Omit` in `packages/plugin/src/{effect,promise}/integration.ts` and the alias expectations in `packages/client/test/{promise,effect}.test.ts` and `packages/core/test/plugin/experimental-namespace.test.ts`. +- [ ] The deprecated pre-experimental SDK aliases from [#59072](https://github.com/anomalyco/opencode/pull/59072). Each old path is now `client.experimental.`, and each old type name is now `Experimental`: + - Methods: `client.session.{stats,import,export,skill,wait,log}`, `client.session.instructions.entry.{list,put,remove}`, `client.generate.text`, `client.voice.{models,session}`, `client.migration.v1.status`, `client.usage.limits`, `client.integration.wellknown.add`, `client.mcp.{add,remove,connect,disconnect}`, `client.mcp.client.token`, `client.file.{watch,unwatch}`, `client.config.update`, and `client.file.write` (Promise client only). + - Types: the Input, Output and (Effect) Operation types of those methods, such as `SessionWaitInput`, plus the Effect `GenerateApi`, `VoiceApi`, `MigrationApi` and `UsageApi`, and the Promise `GenerateApi` in `packages/client/src/promise/api.ts`. + - To remove: drop `aliases: true` from both `compile` calls in `packages/client/script/generate.ts` and run `bun run generate` in `packages/client`; delete `GenerateApi` from `packages/client/src/promise/api.ts`; delete `packages/client/test/deprecated-aliases.test.ts` and the alias entries in `packages/client/test/promise.test.ts`; drop the `Omit` in `packages/plugin/src/{effect,promise}/integration.ts`. -## Next major +## Remove at next major No items yet. diff --git a/AGENTS.md b/AGENTS.md index fa1eda87647f..608eba7873ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,8 +171,8 @@ const table = sqliteTable("session", { - New endpoints and new default behaviors start experimental. - To graduate one, move the route out of `/api/experimental`, drop the `experimental.` prefix, and note it in `CHANGELOG.md`. - Experimental HTTP routes change without aliases: rename or remove the route, then update the callers. -- A breaking move of shipped SDK or plugin API (including moving it under `experimental`) keeps the old path as a deprecated alias of the new one until the next minor. Mark the alias `@deprecated`, naming the replacement and "Removed in the next minor", and add its removal to `.github/RELEASE_REMINDERS.md`. -- Plugins reach experimental endpoints through `ctx.experimental` (`ctx.experimental.session.wait`, `ctx.experimental.generate`), in both the Effect and Promise `Context`; any other path to them on `ctx` is a deprecated alias. Experimental hooks are named `experimental.*` (`experimental.ws.handshake`). `packages/core/test/plugin/experimental-namespace.test.ts` checks this against the Promise adapter's endpoint mapping. +- A breaking move of shipped SDK or plugin API (including moving it under `experimental`) keeps the old path and type names as deprecated aliases of the new ones until the next minor, each marked `@deprecated` with the replacement and "Removed in the next minor". Generated SDK moves get them from the codegen (`compile(..., { aliases: true })` in `packages/client/script/generate.ts`), not by hand. Add the removal to `.github/RELEASE_REMINDERS.md`. +- Experimental plugin hooks are named `experimental.*` (`experimental.ws.handshake`). ## Releases diff --git a/packages/client/script/generate.ts b/packages/client/script/generate.ts index ac25359525e3..78893dd9c3e6 100644 --- a/packages/client/script/generate.ts +++ b/packages/client/script/generate.ts @@ -1,6 +1,12 @@ import { NodeFileSystem } from "@effect/platform-node" import { compile, emitEffectImported, emitEffectShape, emitPromise, write } from "@opencode/httpapi-codegen" -import { ClientApi, clientNamespace, effectOmitEndpoints, groupNames, promiseOmitEndpoints } from "@opencode/protocol/client" +import { + ClientApi, + clientNamespace, + effectOmitEndpoints, + groupNames, + promiseOmitEndpoints, +} from "@opencode/protocol/client" import { Agent } from "@opencode/schema/agent" import { Command } from "@opencode/schema/command" import { Config } from "@opencode/schema/config" @@ -43,8 +49,18 @@ import { Workspace } from "@opencode/schema/workspace" import { Effect, Schema } from "effect" import { fileURLToPath } from "url" -const promiseContract = compile(ClientApi, { groupNames, omitEndpoints: promiseOmitEndpoints, namespace: clientNamespace }) -const effectContract = compile(ClientApi, { groupNames, omitEndpoints: effectOmitEndpoints, namespace: clientNamespace }) +const promiseContract = compile(ClientApi, { + groupNames, + omitEndpoints: promiseOmitEndpoints, + namespace: clientNamespace, + aliases: true, +}) +const effectContract = compile(ClientApi, { + groupNames, + omitEndpoints: effectOmitEndpoints, + namespace: clientNamespace, + aliases: true, +}) const effectTypeReferences = [ ...namespaceTypes("Agent", "@opencode/schema/agent", Agent), ...namespaceTypes("Command", "@opencode/schema/command", Command), diff --git a/packages/client/src/deprecated-aliases.ts b/packages/client/src/deprecated-aliases.ts deleted file mode 100644 index 15328d32e726..000000000000 --- a/packages/client/src/deprecated-aliases.ts +++ /dev/null @@ -1,100 +0,0 @@ -export * as DeprecatedAliases from "./deprecated-aliases.js" - -// Pre-experimental paths of the methods #59072 moved under `client.experimental`. Removed in the next minor; see -// .github/RELEASE_REMINDERS.md. - -type Shape = { - readonly session: object - readonly file: object - readonly config: object - readonly mcp: object - readonly integration: object - readonly experimental: { - readonly session: { - readonly stats: unknown - readonly import: unknown - readonly export: unknown - readonly skill: unknown - readonly wait: unknown - readonly log: unknown - readonly instructions: { - readonly entry: { readonly list: unknown; readonly put: unknown; readonly remove: unknown } - } - } - readonly generate: { readonly text: unknown } - readonly voice: { readonly models: unknown; readonly session: unknown } - readonly integration: { readonly wellknown: { readonly add: unknown } } - readonly mcp: { - readonly add: unknown - readonly remove: unknown - readonly connect: unknown - readonly disconnect: unknown - readonly client: { readonly token: unknown } - } - readonly file: { readonly watch: unknown; readonly unwatch: unknown } - readonly migration: { readonly v1: { readonly status: unknown } } - readonly usage: { readonly limits: unknown } - readonly config: { readonly update: unknown } - } -} - -export function make(raw: Client) { - const experimental = raw.experimental - return { - session: { - ...(raw.session as Client["session"]), - /** @deprecated Use `client.experimental.session.stats`. Removed in the next minor. */ - stats: experimental.session.stats as Client["experimental"]["session"]["stats"], - /** @deprecated Use `client.experimental.session.import`. Removed in the next minor. */ - import: experimental.session.import as Client["experimental"]["session"]["import"], - /** @deprecated Use `client.experimental.session.export`. Removed in the next minor. */ - export: experimental.session.export as Client["experimental"]["session"]["export"], - /** @deprecated Use `client.experimental.session.skill`. Removed in the next minor. */ - skill: experimental.session.skill as Client["experimental"]["session"]["skill"], - /** @deprecated Use `client.experimental.session.wait`. Removed in the next minor. */ - wait: experimental.session.wait as Client["experimental"]["session"]["wait"], - /** @deprecated Use `client.experimental.session.log`. Removed in the next minor. */ - log: experimental.session.log as Client["experimental"]["session"]["log"], - /** @deprecated Use `client.experimental.session.instructions`. Removed in the next minor. */ - instructions: experimental.session.instructions as Client["experimental"]["session"]["instructions"], - }, - /** @deprecated Use `client.experimental.generate`. Removed in the next minor. */ - generate: experimental.generate as Client["experimental"]["generate"], - /** @deprecated Use `client.experimental.voice`. Removed in the next minor. */ - voice: experimental.voice as Client["experimental"]["voice"], - /** @deprecated Use `client.experimental.migration`. Removed in the next minor. */ - migration: experimental.migration as Client["experimental"]["migration"], - /** @deprecated Use `client.experimental.usage`. Removed in the next minor. */ - usage: experimental.usage as Client["experimental"]["usage"], - integration: { - ...(raw.integration as Client["integration"]), - /** @deprecated Use `client.experimental.integration.wellknown`. Removed in the next minor. */ - wellknown: experimental.integration.wellknown as Client["experimental"]["integration"]["wellknown"], - }, - mcp: { - ...(raw.mcp as Client["mcp"]), - /** @deprecated Use `client.experimental.mcp.add`. Removed in the next minor. */ - add: experimental.mcp.add as Client["experimental"]["mcp"]["add"], - /** @deprecated Use `client.experimental.mcp.remove`. Removed in the next minor. */ - remove: experimental.mcp.remove as Client["experimental"]["mcp"]["remove"], - /** @deprecated Use `client.experimental.mcp.connect`. Removed in the next minor. */ - connect: experimental.mcp.connect as Client["experimental"]["mcp"]["connect"], - /** @deprecated Use `client.experimental.mcp.disconnect`. Removed in the next minor. */ - disconnect: experimental.mcp.disconnect as Client["experimental"]["mcp"]["disconnect"], - /** @deprecated Use `client.experimental.mcp.client`. Removed in the next minor. */ - client: experimental.mcp.client as Client["experimental"]["mcp"]["client"], - }, - file: { - ...(raw.file as Client["file"]), - /** @deprecated Use `client.experimental.file.watch`. Removed in the next minor. */ - watch: experimental.file.watch as Client["experimental"]["file"]["watch"], - /** @deprecated Use `client.experimental.file.unwatch`. Removed in the next minor. */ - unwatch: experimental.file.unwatch as Client["experimental"]["file"]["unwatch"], - }, - config: { - ...(raw.config as Client["config"]), - /** @deprecated Use `client.experimental.config.update`. Removed in the next minor. */ - update: experimental.config.update as Client["experimental"]["config"]["update"], - }, - } -} diff --git a/packages/client/src/effect/api.ts b/packages/client/src/effect/api.ts index 277ebbe6c5e0..e6f0e9990022 100644 --- a/packages/client/src/effect/api.ts +++ b/packages/client/src/effect/api.ts @@ -4,6 +4,5 @@ export type { RpcApi, RpcClient } from "./rpc.js" export type { SelectedEvent } from "../shared-events.js" export type * from "./api/api.js" -export type * from "./deprecated.js" export type WebSearchApi = WebsearchApi diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index cc7978683cf4..bffdda2278df 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -429,6 +429,39 @@ export interface SessionApi { } readonly environment: SessionEnvironmentOperation readonly view: SessionViewOperation + + /** @deprecated Use `client.experimental.session.stats`. Removed in the next minor. */ + readonly stats: ExperimentalSessionStatsOperation + + /** @deprecated Use `client.experimental.session.import`. Removed in the next minor. */ + readonly import: ExperimentalSessionImportOperation + + /** @deprecated Use `client.experimental.session.export`. Removed in the next minor. */ + readonly export: ExperimentalSessionExportOperation + + /** @deprecated Use `client.experimental.session.skill`. Removed in the next minor. */ + readonly skill: ExperimentalSessionSkillOperation + + /** @deprecated Use `client.experimental.session.wait`. Removed in the next minor. */ + readonly wait: ExperimentalSessionWaitOperation + + /** @deprecated Use `client.experimental.session.instructions`. Removed in the next minor. */ + readonly instructions: { + /** @deprecated Use `client.experimental.session.instructions.entry`. Removed in the next minor. */ + readonly entry: { + /** @deprecated Use `client.experimental.session.instructions.entry.list`. Removed in the next minor. */ + readonly list: ExperimentalSessionInstructionsEntryListOperation + + /** @deprecated Use `client.experimental.session.instructions.entry.put`. Removed in the next minor. */ + readonly put: ExperimentalSessionInstructionsEntryPutOperation + + /** @deprecated Use `client.experimental.session.instructions.entry.remove`. Removed in the next minor. */ + readonly remove: ExperimentalSessionInstructionsEntryRemoveOperation + } + } + + /** @deprecated Use `client.experimental.session.log`. Removed in the next minor. */ + readonly log: ExperimentalSessionLogOperation } export type ExperimentalSessionStatsInput = { @@ -1638,6 +1671,12 @@ export interface IntegrationApi { readonly status: IntegrationCommandStatusOperation readonly cancel: IntegrationCommandCancelOperation } + + /** @deprecated Use `client.experimental.integration.wellknown`. Removed in the next minor. */ + readonly wellknown: { + /** @deprecated Use `client.experimental.integration.wellknown.add`. Removed in the next minor. */ + readonly add: ExperimentalIntegrationWellknownAddOperation + } } export type ExperimentalIntegrationWellknownAddInput = { @@ -1666,6 +1705,24 @@ export type McpResourceCatalogOperation = ( export interface McpApi { readonly list: McpListOperation readonly resource: { readonly catalog: McpResourceCatalogOperation } + + /** @deprecated Use `client.experimental.mcp.add`. Removed in the next minor. */ + readonly add: ExperimentalMcpAddOperation + + /** @deprecated Use `client.experimental.mcp.remove`. Removed in the next minor. */ + readonly remove: ExperimentalMcpRemoveOperation + + /** @deprecated Use `client.experimental.mcp.connect`. Removed in the next minor. */ + readonly connect: ExperimentalMcpConnectOperation + + /** @deprecated Use `client.experimental.mcp.disconnect`. Removed in the next minor. */ + readonly disconnect: ExperimentalMcpDisconnectOperation + + /** @deprecated Use `client.experimental.mcp.client`. Removed in the next minor. */ + readonly client: { + /** @deprecated Use `client.experimental.mcp.client.token`. Removed in the next minor. */ + readonly token: ExperimentalMcpClientTokenOperation + } } export type ExperimentalMcpAddInput = { @@ -1891,6 +1948,12 @@ export type FileFindOperation = (input: FileFindInput) => Effect.Effe export interface FileApi { readonly list: FileListOperation readonly find: FileFindOperation + + /** @deprecated Use `client.experimental.file.watch`. Removed in the next minor. */ + readonly watch: ExperimentalFileWatchOperation + + /** @deprecated Use `client.experimental.file.unwatch`. Removed in the next minor. */ + readonly unwatch: ExperimentalFileUnwatchOperation } export type ExperimentalFileWatchInput = { @@ -2532,6 +2595,9 @@ export type ConfigShellsOperation = () => Effect.Effect { readonly get: ConfigGetOperation readonly shells: ConfigShellsOperation + + /** @deprecated Use `client.experimental.config.update`. Removed in the next minor. */ + readonly update: ExperimentalConfigUpdateOperation } export type ExperimentalConfigUpdateInput = { readonly shell: string | null } @@ -2544,6 +2610,219 @@ export interface ExperimentalConfigApi { readonly update: ExperimentalConfigUpdateOperation } +/** @deprecated Use `ExperimentalSessionStatsInput`. Removed in the next minor. */ +export type SessionStatsInput = ExperimentalSessionStatsInput + +/** @deprecated Use `ExperimentalSessionStatsOutput`. Removed in the next minor. */ +export type SessionStatsOutput = ExperimentalSessionStatsOutput + +/** @deprecated Use `ExperimentalSessionStatsOperation`. Removed in the next minor. */ +export type SessionStatsOperation = ExperimentalSessionStatsOperation + +/** @deprecated Use `ExperimentalSessionImportInput`. Removed in the next minor. */ +export type SessionImportInput = ExperimentalSessionImportInput + +/** @deprecated Use `ExperimentalSessionImportOutput`. Removed in the next minor. */ +export type SessionImportOutput = ExperimentalSessionImportOutput + +/** @deprecated Use `ExperimentalSessionImportOperation`. Removed in the next minor. */ +export type SessionImportOperation = ExperimentalSessionImportOperation + +/** @deprecated Use `ExperimentalSessionExportInput`. Removed in the next minor. */ +export type SessionExportInput = ExperimentalSessionExportInput + +/** @deprecated Use `ExperimentalSessionExportOutput`. Removed in the next minor. */ +export type SessionExportOutput = ExperimentalSessionExportOutput + +/** @deprecated Use `ExperimentalSessionExportOperation`. Removed in the next minor. */ +export type SessionExportOperation = ExperimentalSessionExportOperation + +/** @deprecated Use `ExperimentalSessionSkillInput`. Removed in the next minor. */ +export type SessionSkillInput = ExperimentalSessionSkillInput + +/** @deprecated Use `ExperimentalSessionSkillOutput`. Removed in the next minor. */ +export type SessionSkillOutput = ExperimentalSessionSkillOutput + +/** @deprecated Use `ExperimentalSessionSkillOperation`. Removed in the next minor. */ +export type SessionSkillOperation = ExperimentalSessionSkillOperation + +/** @deprecated Use `ExperimentalSessionWaitInput`. Removed in the next minor. */ +export type SessionWaitInput = ExperimentalSessionWaitInput + +/** @deprecated Use `ExperimentalSessionWaitOutput`. Removed in the next minor. */ +export type SessionWaitOutput = ExperimentalSessionWaitOutput + +/** @deprecated Use `ExperimentalSessionWaitOperation`. Removed in the next minor. */ +export type SessionWaitOperation = ExperimentalSessionWaitOperation + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListInput`. Removed in the next minor. */ +export type SessionInstructionsEntryListInput = ExperimentalSessionInstructionsEntryListInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryListOutput = ExperimentalSessionInstructionsEntryListOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListOperation`. Removed in the next minor. */ +export type SessionInstructionsEntryListOperation = ExperimentalSessionInstructionsEntryListOperation + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutInput`. Removed in the next minor. */ +export type SessionInstructionsEntryPutInput = ExperimentalSessionInstructionsEntryPutInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryPutOutput = ExperimentalSessionInstructionsEntryPutOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOperation`. Removed in the next minor. */ +export type SessionInstructionsEntryPutOperation = ExperimentalSessionInstructionsEntryPutOperation + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveInput`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveInput = ExperimentalSessionInstructionsEntryRemoveInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveOutput = ExperimentalSessionInstructionsEntryRemoveOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOperation`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveOperation = ExperimentalSessionInstructionsEntryRemoveOperation + +/** @deprecated Use `ExperimentalSessionLogInput`. Removed in the next minor. */ +export type SessionLogInput = ExperimentalSessionLogInput + +/** @deprecated Use `ExperimentalSessionLogOutput`. Removed in the next minor. */ +export type SessionLogOutput = ExperimentalSessionLogOutput + +/** @deprecated Use `ExperimentalSessionLogOperation`. Removed in the next minor. */ +export type SessionLogOperation = ExperimentalSessionLogOperation + +/** @deprecated Use `ExperimentalGenerateTextInput`. Removed in the next minor. */ +export type GenerateTextInput = ExperimentalGenerateTextInput + +/** @deprecated Use `ExperimentalGenerateTextOutput`. Removed in the next minor. */ +export type GenerateTextOutput = ExperimentalGenerateTextOutput + +/** @deprecated Use `ExperimentalGenerateTextOperation`. Removed in the next minor. */ +export type GenerateTextOperation = ExperimentalGenerateTextOperation + +/** @deprecated Use `ExperimentalVoiceModelsOutput`. Removed in the next minor. */ +export type VoiceModelsOutput = ExperimentalVoiceModelsOutput + +/** @deprecated Use `ExperimentalVoiceModelsOperation`. Removed in the next minor. */ +export type VoiceModelsOperation = ExperimentalVoiceModelsOperation + +/** @deprecated Use `ExperimentalVoiceSessionInput`. Removed in the next minor. */ +export type VoiceSessionInput = ExperimentalVoiceSessionInput + +/** @deprecated Use `ExperimentalVoiceSessionOutput`. Removed in the next minor. */ +export type VoiceSessionOutput = ExperimentalVoiceSessionOutput + +/** @deprecated Use `ExperimentalVoiceSessionOperation`. Removed in the next minor. */ +export type VoiceSessionOperation = ExperimentalVoiceSessionOperation + +/** @deprecated Use `ExperimentalIntegrationWellknownAddInput`. Removed in the next minor. */ +export type IntegrationWellknownAddInput = ExperimentalIntegrationWellknownAddInput + +/** @deprecated Use `ExperimentalIntegrationWellknownAddOutput`. Removed in the next minor. */ +export type IntegrationWellknownAddOutput = ExperimentalIntegrationWellknownAddOutput + +/** @deprecated Use `ExperimentalIntegrationWellknownAddOperation`. Removed in the next minor. */ +export type IntegrationWellknownAddOperation = ExperimentalIntegrationWellknownAddOperation + +/** @deprecated Use `ExperimentalMcpAddInput`. Removed in the next minor. */ +export type McpAddInput = ExperimentalMcpAddInput + +/** @deprecated Use `ExperimentalMcpAddOutput`. Removed in the next minor. */ +export type McpAddOutput = ExperimentalMcpAddOutput + +/** @deprecated Use `ExperimentalMcpAddOperation`. Removed in the next minor. */ +export type McpAddOperation = ExperimentalMcpAddOperation + +/** @deprecated Use `ExperimentalMcpRemoveInput`. Removed in the next minor. */ +export type McpRemoveInput = ExperimentalMcpRemoveInput + +/** @deprecated Use `ExperimentalMcpRemoveOutput`. Removed in the next minor. */ +export type McpRemoveOutput = ExperimentalMcpRemoveOutput + +/** @deprecated Use `ExperimentalMcpRemoveOperation`. Removed in the next minor. */ +export type McpRemoveOperation = ExperimentalMcpRemoveOperation + +/** @deprecated Use `ExperimentalMcpConnectInput`. Removed in the next minor. */ +export type McpConnectInput = ExperimentalMcpConnectInput + +/** @deprecated Use `ExperimentalMcpConnectOutput`. Removed in the next minor. */ +export type McpConnectOutput = ExperimentalMcpConnectOutput + +/** @deprecated Use `ExperimentalMcpConnectOperation`. Removed in the next minor. */ +export type McpConnectOperation = ExperimentalMcpConnectOperation + +/** @deprecated Use `ExperimentalMcpDisconnectInput`. Removed in the next minor. */ +export type McpDisconnectInput = ExperimentalMcpDisconnectInput + +/** @deprecated Use `ExperimentalMcpDisconnectOutput`. Removed in the next minor. */ +export type McpDisconnectOutput = ExperimentalMcpDisconnectOutput + +/** @deprecated Use `ExperimentalMcpDisconnectOperation`. Removed in the next minor. */ +export type McpDisconnectOperation = ExperimentalMcpDisconnectOperation + +/** @deprecated Use `ExperimentalMcpClientTokenInput`. Removed in the next minor. */ +export type McpClientTokenInput = ExperimentalMcpClientTokenInput + +/** @deprecated Use `ExperimentalMcpClientTokenOutput`. Removed in the next minor. */ +export type McpClientTokenOutput = ExperimentalMcpClientTokenOutput + +/** @deprecated Use `ExperimentalMcpClientTokenOperation`. Removed in the next minor. */ +export type McpClientTokenOperation = ExperimentalMcpClientTokenOperation + +/** @deprecated Use `ExperimentalFileWatchInput`. Removed in the next minor. */ +export type FileWatchInput = ExperimentalFileWatchInput + +/** @deprecated Use `ExperimentalFileWatchOutput`. Removed in the next minor. */ +export type FileWatchOutput = ExperimentalFileWatchOutput + +/** @deprecated Use `ExperimentalFileWatchOperation`. Removed in the next minor. */ +export type FileWatchOperation = ExperimentalFileWatchOperation + +/** @deprecated Use `ExperimentalFileUnwatchInput`. Removed in the next minor. */ +export type FileUnwatchInput = ExperimentalFileUnwatchInput + +/** @deprecated Use `ExperimentalFileUnwatchOutput`. Removed in the next minor. */ +export type FileUnwatchOutput = ExperimentalFileUnwatchOutput + +/** @deprecated Use `ExperimentalFileUnwatchOperation`. Removed in the next minor. */ +export type FileUnwatchOperation = ExperimentalFileUnwatchOperation + +/** @deprecated Use `ExperimentalMigrationV1StatusOutput`. Removed in the next minor. */ +export type MigrationV1StatusOutput = ExperimentalMigrationV1StatusOutput + +/** @deprecated Use `ExperimentalMigrationV1StatusOperation`. Removed in the next minor. */ +export type MigrationV1StatusOperation = ExperimentalMigrationV1StatusOperation + +/** @deprecated Use `ExperimentalUsageLimitsInput`. Removed in the next minor. */ +export type UsageLimitsInput = ExperimentalUsageLimitsInput + +/** @deprecated Use `ExperimentalUsageLimitsOutput`. Removed in the next minor. */ +export type UsageLimitsOutput = ExperimentalUsageLimitsOutput + +/** @deprecated Use `ExperimentalUsageLimitsOperation`. Removed in the next minor. */ +export type UsageLimitsOperation = ExperimentalUsageLimitsOperation + +/** @deprecated Use `ExperimentalConfigUpdateInput`. Removed in the next minor. */ +export type ConfigUpdateInput = ExperimentalConfigUpdateInput + +/** @deprecated Use `ExperimentalConfigUpdateOutput`. Removed in the next minor. */ +export type ConfigUpdateOutput = ExperimentalConfigUpdateOutput + +/** @deprecated Use `ExperimentalConfigUpdateOperation`. Removed in the next minor. */ +export type ConfigUpdateOperation = ExperimentalConfigUpdateOperation + +/** @deprecated Use `ExperimentalGenerateApi`. Removed in the next minor. */ +export type GenerateApi = ExperimentalGenerateApi + +/** @deprecated Use `ExperimentalVoiceApi`. Removed in the next minor. */ +export type VoiceApi = ExperimentalVoiceApi + +/** @deprecated Use `ExperimentalMigrationApi`. Removed in the next minor. */ +export type MigrationApi = ExperimentalMigrationApi + +/** @deprecated Use `ExperimentalUsageApi`. Removed in the next minor. */ +export type UsageApi = ExperimentalUsageApi + export interface AppApi { readonly server: ServerApi readonly location: LocationApi @@ -2583,4 +2862,34 @@ export interface AppApi { readonly debug: DebugApi readonly websearch: WebsearchApi readonly config: ConfigApi + + /** @deprecated Use `client.experimental.generate`. Removed in the next minor. */ + readonly generate: { + /** @deprecated Use `client.experimental.generate.text`. Removed in the next minor. */ + readonly text: ExperimentalGenerateTextOperation + } + + /** @deprecated Use `client.experimental.voice`. Removed in the next minor. */ + readonly voice: { + /** @deprecated Use `client.experimental.voice.models`. Removed in the next minor. */ + readonly models: ExperimentalVoiceModelsOperation + + /** @deprecated Use `client.experimental.voice.session`. Removed in the next minor. */ + readonly session: ExperimentalVoiceSessionOperation + } + + /** @deprecated Use `client.experimental.migration`. Removed in the next minor. */ + readonly migration: { + /** @deprecated Use `client.experimental.migration.v1`. Removed in the next minor. */ + readonly v1: { + /** @deprecated Use `client.experimental.migration.v1.status`. Removed in the next minor. */ + readonly status: ExperimentalMigrationV1StatusOperation + } + } + + /** @deprecated Use `client.experimental.usage`. Removed in the next minor. */ + readonly usage: { + /** @deprecated Use `client.experimental.usage.limits`. Removed in the next minor. */ + readonly limits: ExperimentalUsageLimitsOperation + } } diff --git a/packages/client/src/effect/client.ts b/packages/client/src/effect/client.ts index 8f0f0d86b7d8..4aa01eacb9a6 100644 --- a/packages/client/src/effect/client.ts +++ b/packages/client/src/effect/client.ts @@ -2,7 +2,6 @@ export * as OpenCode from "./client.js" import { Cause, Context, Effect, Stream } from "effect" import { HttpClient, HttpClientRequest } from "effect/http" -import { DeprecatedAliases } from "../deprecated-aliases.js" import { SharedEvents } from "../shared-events.js" import { ClientError, OpenCode } from "./generated/index.js" import type { EventSubscribeInput } from "./api.js" @@ -50,7 +49,6 @@ export const make = Effect.fn("OpenCode.make")(function* (options?: { readonly b ) return { ...raw, - ...DeprecatedAliases.make(raw), event: { ...raw.event, subscribe }, rpc: Object.assign( RpcClientRuntime.make( diff --git a/packages/client/src/effect/deprecated.ts b/packages/client/src/effect/deprecated.ts deleted file mode 100644 index 517407c6bb98..000000000000 --- a/packages/client/src/effect/deprecated.ts +++ /dev/null @@ -1,280 +0,0 @@ -// Pre-experimental names of the types #59072 renamed with an `Experimental` prefix. Removed in the next minor; see -// .github/RELEASE_REMINDERS.md. -import type * as Experimental from "./api/api.js" - -/** @deprecated Use `ExperimentalConfigUpdateInput`. Removed in the next minor. */ -export type ConfigUpdateInput = Experimental.ExperimentalConfigUpdateInput - -/** @deprecated Use `ExperimentalConfigUpdateOperation`. Removed in the next minor. */ -export type ConfigUpdateOperation = Experimental.ExperimentalConfigUpdateOperation - -/** @deprecated Use `ExperimentalConfigUpdateOutput`. Removed in the next minor. */ -export type ConfigUpdateOutput = Experimental.ExperimentalConfigUpdateOutput - -/** @deprecated Use `ExperimentalFileUnwatchInput`. Removed in the next minor. */ -export type FileUnwatchInput = Experimental.ExperimentalFileUnwatchInput - -/** @deprecated Use `ExperimentalFileUnwatchOperation`. Removed in the next minor. */ -export type FileUnwatchOperation = Experimental.ExperimentalFileUnwatchOperation - -/** @deprecated Use `ExperimentalFileUnwatchOutput`. Removed in the next minor. */ -export type FileUnwatchOutput = Experimental.ExperimentalFileUnwatchOutput - -/** @deprecated Use `ExperimentalFileWatchInput`. Removed in the next minor. */ -export type FileWatchInput = Experimental.ExperimentalFileWatchInput - -/** @deprecated Use `ExperimentalFileWatchOperation`. Removed in the next minor. */ -export type FileWatchOperation = Experimental.ExperimentalFileWatchOperation - -/** @deprecated Use `ExperimentalFileWatchOutput`. Removed in the next minor. */ -export type FileWatchOutput = Experimental.ExperimentalFileWatchOutput - -/** @deprecated Use `ExperimentalGenerateApi`. Removed in the next minor. */ -export type GenerateApi = Experimental.ExperimentalGenerateApi - -/** @deprecated Use `ExperimentalGenerateTextInput`. Removed in the next minor. */ -export type GenerateTextInput = Experimental.ExperimentalGenerateTextInput - -/** @deprecated Use `ExperimentalGenerateTextOperation`. Removed in the next minor. */ -export type GenerateTextOperation = Experimental.ExperimentalGenerateTextOperation - -/** @deprecated Use `ExperimentalGenerateTextOutput`. Removed in the next minor. */ -export type GenerateTextOutput = Experimental.ExperimentalGenerateTextOutput - -/** @deprecated Use `ExperimentalIntegrationWellknownAddInput`. Removed in the next minor. */ -export type IntegrationWellknownAddInput = Experimental.ExperimentalIntegrationWellknownAddInput - -/** @deprecated Use `ExperimentalIntegrationWellknownAddOperation`. Removed in the next minor. */ -export type IntegrationWellknownAddOperation = Experimental.ExperimentalIntegrationWellknownAddOperation - -/** @deprecated Use `ExperimentalIntegrationWellknownAddOutput`. Removed in the next minor. */ -export type IntegrationWellknownAddOutput = Experimental.ExperimentalIntegrationWellknownAddOutput - -/** @deprecated Use `ExperimentalMcpAddInput`. Removed in the next minor. */ -export type McpAddInput = Experimental.ExperimentalMcpAddInput - -/** @deprecated Use `ExperimentalMcpAddOperation`. Removed in the next minor. */ -export type McpAddOperation = Experimental.ExperimentalMcpAddOperation - -/** @deprecated Use `ExperimentalMcpAddOutput`. Removed in the next minor. */ -export type McpAddOutput = Experimental.ExperimentalMcpAddOutput - -/** @deprecated Use `ExperimentalMcpClientTokenInput`. Removed in the next minor. */ -export type McpClientTokenInput = Experimental.ExperimentalMcpClientTokenInput - -/** @deprecated Use `ExperimentalMcpClientTokenOperation`. Removed in the next minor. */ -export type McpClientTokenOperation = Experimental.ExperimentalMcpClientTokenOperation - -/** @deprecated Use `ExperimentalMcpClientTokenOutput`. Removed in the next minor. */ -export type McpClientTokenOutput = Experimental.ExperimentalMcpClientTokenOutput - -/** @deprecated Use `ExperimentalMcpConnectInput`. Removed in the next minor. */ -export type McpConnectInput = Experimental.ExperimentalMcpConnectInput - -/** @deprecated Use `ExperimentalMcpConnectOperation`. Removed in the next minor. */ -export type McpConnectOperation = Experimental.ExperimentalMcpConnectOperation - -/** @deprecated Use `ExperimentalMcpConnectOutput`. Removed in the next minor. */ -export type McpConnectOutput = Experimental.ExperimentalMcpConnectOutput - -/** @deprecated Use `ExperimentalMcpDisconnectInput`. Removed in the next minor. */ -export type McpDisconnectInput = Experimental.ExperimentalMcpDisconnectInput - -/** @deprecated Use `ExperimentalMcpDisconnectOperation`. Removed in the next minor. */ -export type McpDisconnectOperation = Experimental.ExperimentalMcpDisconnectOperation - -/** @deprecated Use `ExperimentalMcpDisconnectOutput`. Removed in the next minor. */ -export type McpDisconnectOutput = Experimental.ExperimentalMcpDisconnectOutput - -/** @deprecated Use `ExperimentalMcpRemoveInput`. Removed in the next minor. */ -export type McpRemoveInput = Experimental.ExperimentalMcpRemoveInput - -/** @deprecated Use `ExperimentalMcpRemoveOperation`. Removed in the next minor. */ -export type McpRemoveOperation = Experimental.ExperimentalMcpRemoveOperation - -/** @deprecated Use `ExperimentalMcpRemoveOutput`. Removed in the next minor. */ -export type McpRemoveOutput = Experimental.ExperimentalMcpRemoveOutput - -/** @deprecated Use `ExperimentalMigrationApi`. Removed in the next minor. */ -export type MigrationApi = Experimental.ExperimentalMigrationApi - -/** @deprecated Use `ExperimentalMigrationV1StatusOperation`. Removed in the next minor. */ -export type MigrationV1StatusOperation = Experimental.ExperimentalMigrationV1StatusOperation - -/** @deprecated Use `ExperimentalMigrationV1StatusOutput`. Removed in the next minor. */ -export type MigrationV1StatusOutput = Experimental.ExperimentalMigrationV1StatusOutput - -/** @deprecated Use `ExperimentalSessionExportInput`. Removed in the next minor. */ -export type SessionExportInput = Experimental.ExperimentalSessionExportInput - -/** @deprecated Use `ExperimentalSessionExportOperation`. Removed in the next minor. */ -export type SessionExportOperation = Experimental.ExperimentalSessionExportOperation - -/** @deprecated Use `ExperimentalSessionExportOutput`. Removed in the next minor. */ -export type SessionExportOutput = Experimental.ExperimentalSessionExportOutput - -/** @deprecated Use `ExperimentalSessionImportInput`. Removed in the next minor. */ -export type SessionImportInput = Experimental.ExperimentalSessionImportInput - -/** @deprecated Use `ExperimentalSessionImportOperation`. Removed in the next minor. */ -export type SessionImportOperation = Experimental.ExperimentalSessionImportOperation - -/** @deprecated Use `ExperimentalSessionImportOutput`. Removed in the next minor. */ -export type SessionImportOutput = Experimental.ExperimentalSessionImportOutput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryListInput`. Removed in the next minor. */ -export type SessionInstructionsEntryListInput = Experimental.ExperimentalSessionInstructionsEntryListInput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryListOperation`. Removed in the next minor. */ -export type SessionInstructionsEntryListOperation = - Experimental.ExperimentalSessionInstructionsEntryListOperation - -/** @deprecated Use `ExperimentalSessionInstructionsEntryListOutput`. Removed in the next minor. */ -export type SessionInstructionsEntryListOutput = Experimental.ExperimentalSessionInstructionsEntryListOutput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryPutInput`. Removed in the next minor. */ -export type SessionInstructionsEntryPutInput = Experimental.ExperimentalSessionInstructionsEntryPutInput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOperation`. Removed in the next minor. */ -export type SessionInstructionsEntryPutOperation = - Experimental.ExperimentalSessionInstructionsEntryPutOperation - -/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOutput`. Removed in the next minor. */ -export type SessionInstructionsEntryPutOutput = Experimental.ExperimentalSessionInstructionsEntryPutOutput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveInput`. Removed in the next minor. */ -export type SessionInstructionsEntryRemoveInput = Experimental.ExperimentalSessionInstructionsEntryRemoveInput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOperation`. Removed in the next minor. */ -export type SessionInstructionsEntryRemoveOperation = - Experimental.ExperimentalSessionInstructionsEntryRemoveOperation - -/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOutput`. Removed in the next minor. */ -export type SessionInstructionsEntryRemoveOutput = Experimental.ExperimentalSessionInstructionsEntryRemoveOutput - -/** @deprecated Use `ExperimentalSessionLogInput`. Removed in the next minor. */ -export type SessionLogInput = Experimental.ExperimentalSessionLogInput - -/** @deprecated Use `ExperimentalSessionLogOperation`. Removed in the next minor. */ -export type SessionLogOperation = Experimental.ExperimentalSessionLogOperation - -/** @deprecated Use `ExperimentalSessionLogOutput`. Removed in the next minor. */ -export type SessionLogOutput = Experimental.ExperimentalSessionLogOutput - -/** @deprecated Use `ExperimentalSessionSkillInput`. Removed in the next minor. */ -export type SessionSkillInput = Experimental.ExperimentalSessionSkillInput - -/** @deprecated Use `ExperimentalSessionSkillOperation`. Removed in the next minor. */ -export type SessionSkillOperation = Experimental.ExperimentalSessionSkillOperation - -/** @deprecated Use `ExperimentalSessionSkillOutput`. Removed in the next minor. */ -export type SessionSkillOutput = Experimental.ExperimentalSessionSkillOutput - -/** @deprecated Use `ExperimentalSessionStatsInput`. Removed in the next minor. */ -export type SessionStatsInput = Experimental.ExperimentalSessionStatsInput - -/** @deprecated Use `ExperimentalSessionStatsOperation`. Removed in the next minor. */ -export type SessionStatsOperation = Experimental.ExperimentalSessionStatsOperation - -/** @deprecated Use `ExperimentalSessionStatsOutput`. Removed in the next minor. */ -export type SessionStatsOutput = Experimental.ExperimentalSessionStatsOutput - -/** @deprecated Use `ExperimentalSessionWaitInput`. Removed in the next minor. */ -export type SessionWaitInput = Experimental.ExperimentalSessionWaitInput - -/** @deprecated Use `ExperimentalSessionWaitOperation`. Removed in the next minor. */ -export type SessionWaitOperation = Experimental.ExperimentalSessionWaitOperation - -/** @deprecated Use `ExperimentalSessionWaitOutput`. Removed in the next minor. */ -export type SessionWaitOutput = Experimental.ExperimentalSessionWaitOutput - -/** @deprecated Use `ExperimentalUsageApi`. Removed in the next minor. */ -export type UsageApi = Experimental.ExperimentalUsageApi - -/** @deprecated Use `ExperimentalUsageLimitsInput`. Removed in the next minor. */ -export type UsageLimitsInput = Experimental.ExperimentalUsageLimitsInput - -/** @deprecated Use `ExperimentalUsageLimitsOperation`. Removed in the next minor. */ -export type UsageLimitsOperation = Experimental.ExperimentalUsageLimitsOperation - -/** @deprecated Use `ExperimentalUsageLimitsOutput`. Removed in the next minor. */ -export type UsageLimitsOutput = Experimental.ExperimentalUsageLimitsOutput - -/** @deprecated Use `ExperimentalVoiceApi`. Removed in the next minor. */ -export type VoiceApi = Experimental.ExperimentalVoiceApi - -/** @deprecated Use `ExperimentalVoiceModelsOperation`. Removed in the next minor. */ -export type VoiceModelsOperation = Experimental.ExperimentalVoiceModelsOperation - -/** @deprecated Use `ExperimentalVoiceModelsOutput`. Removed in the next minor. */ -export type VoiceModelsOutput = Experimental.ExperimentalVoiceModelsOutput - -/** @deprecated Use `ExperimentalVoiceSessionInput`. Removed in the next minor. */ -export type VoiceSessionInput = Experimental.ExperimentalVoiceSessionInput - -/** @deprecated Use `ExperimentalVoiceSessionOperation`. Removed in the next minor. */ -export type VoiceSessionOperation = Experimental.ExperimentalVoiceSessionOperation - -/** @deprecated Use `ExperimentalVoiceSessionOutput`. Removed in the next minor. */ -export type VoiceSessionOutput = Experimental.ExperimentalVoiceSessionOutput - -// The group interfaces keep their pre-experimental members too, so code typed against them still compiles. -declare module "./api/api.js" { - interface AppApi { - /** @deprecated Use `client.experimental.generate`. Removed in the next minor. */ - readonly generate: ExperimentalGenerateApi - /** @deprecated Use `client.experimental.voice`. Removed in the next minor. */ - readonly voice: ExperimentalVoiceApi - /** @deprecated Use `client.experimental.migration`. Removed in the next minor. */ - readonly migration: ExperimentalMigrationApi - /** @deprecated Use `client.experimental.usage`. Removed in the next minor. */ - readonly usage: ExperimentalUsageApi - } - - interface SessionApi { - /** @deprecated Use `client.experimental.session.stats`. Removed in the next minor. */ - readonly stats: ExperimentalSessionApi["stats"] - /** @deprecated Use `client.experimental.session.import`. Removed in the next minor. */ - readonly import: ExperimentalSessionApi["import"] - /** @deprecated Use `client.experimental.session.export`. Removed in the next minor. */ - readonly export: ExperimentalSessionApi["export"] - /** @deprecated Use `client.experimental.session.skill`. Removed in the next minor. */ - readonly skill: ExperimentalSessionApi["skill"] - /** @deprecated Use `client.experimental.session.wait`. Removed in the next minor. */ - readonly wait: ExperimentalSessionApi["wait"] - /** @deprecated Use `client.experimental.session.instructions`. Removed in the next minor. */ - readonly instructions: ExperimentalSessionApi["instructions"] - /** @deprecated Use `client.experimental.session.log`. Removed in the next minor. */ - readonly log: ExperimentalSessionApi["log"] - } - - interface IntegrationApi { - /** @deprecated Use `client.experimental.integration.wellknown`. Removed in the next minor. */ - readonly wellknown: ExperimentalIntegrationApi["wellknown"] - } - - interface McpApi { - /** @deprecated Use `client.experimental.mcp.add`. Removed in the next minor. */ - readonly add: ExperimentalMcpApi["add"] - /** @deprecated Use `client.experimental.mcp.remove`. Removed in the next minor. */ - readonly remove: ExperimentalMcpApi["remove"] - /** @deprecated Use `client.experimental.mcp.connect`. Removed in the next minor. */ - readonly connect: ExperimentalMcpApi["connect"] - /** @deprecated Use `client.experimental.mcp.disconnect`. Removed in the next minor. */ - readonly disconnect: ExperimentalMcpApi["disconnect"] - /** @deprecated Use `client.experimental.mcp.client`. Removed in the next minor. */ - readonly client: ExperimentalMcpApi["client"] - } - - interface FileApi { - /** @deprecated Use `client.experimental.file.watch`. Removed in the next minor. */ - readonly watch: ExperimentalFileApi["watch"] - /** @deprecated Use `client.experimental.file.unwatch`. Removed in the next minor. */ - readonly unwatch: ExperimentalFileApi["unwatch"] - } - - interface ConfigApi { - /** @deprecated Use `client.experimental.config.update`. Removed in the next minor. */ - readonly update: ExperimentalConfigApi["update"] - } -} diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index 5133523e639b..44765b7542df 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -1740,47 +1740,140 @@ const adaptGroupExperimentalConfig = (raw: RawClient["server.config"]) => ({ update: EndpointExperimentalConfigUpdate(raw), }) -const adaptClient = (raw: RawClient) => ({ - server: adaptGroupServer(raw["server.server"]), - location: adaptGroupLocation(raw["server.location"]), - agent: adaptGroupAgent(raw["server.agent"]), - plugin: adaptGroupPlugin(raw["server.plugin"]), - session: adaptGroupSession(raw["server.session"]), - experimental: { - ...adaptGroupExperimental(raw["server.experimental"]), - session: adaptGroupExperimentalSession(raw["server.session"]), - generate: adaptGroupExperimentalGenerate(raw["server.generate"]), - voice: adaptGroupExperimentalVoice(raw["server.voice"]), - integration: adaptGroupExperimentalIntegration(raw["server.integration"]), - mcp: adaptGroupExperimentalMcp(raw["server.mcp"]), - file: adaptGroupExperimentalFile(raw["server.fs"]), - migration: adaptGroupExperimentalMigration(raw["server.migration"]), - usage: adaptGroupExperimentalUsage(raw["server.usage"]), - config: adaptGroupExperimentalConfig(raw["server.config"]), - }, - message: adaptGroupMessage(raw["server.message"]), - model: adaptGroupModel(raw["server.model"]), - provider: adaptGroupProvider(raw["server.provider"]), - integration: adaptGroupIntegration(raw["server.integration"]), - mcp: adaptGroupMcp(raw["server.mcp"]), - credential: adaptGroupCredential(raw["server.credential"]), - project: adaptGroupProject(raw["server.project"]), - form: adaptGroupForm(raw["server.form"]), - permission: adaptGroupPermission(raw["server.permission"]), - file: adaptGroupFile(raw["server.fs"]), - command: adaptGroupCommand(raw["server.command"]), - skill: adaptGroupSkill(raw["server.skill"]), - rpc: adaptGroupRpc(raw["server.rpc"]), - event: adaptGroupEvent(raw["server.event"]), - pty: adaptGroupPty(raw["server.pty"]), - shell: adaptGroupShell(raw["server.shell"]), - reference: adaptGroupReference(raw["server.reference"]), - worktree: adaptGroupWorktree(raw["server.worktree"]), - vcs: adaptGroupVcs(raw["server.vcs"]), - debug: adaptGroupDebug(raw["server.debug"]), - websearch: adaptGroupWebsearch(raw["server.websearch"]), - config: adaptGroupConfig(raw["server.config"]), -}) +const adaptClient = (raw: RawClient) => { + const client = { + server: adaptGroupServer(raw["server.server"]), + location: adaptGroupLocation(raw["server.location"]), + agent: adaptGroupAgent(raw["server.agent"]), + plugin: adaptGroupPlugin(raw["server.plugin"]), + session: adaptGroupSession(raw["server.session"]), + experimental: { + ...adaptGroupExperimental(raw["server.experimental"]), + session: adaptGroupExperimentalSession(raw["server.session"]), + generate: adaptGroupExperimentalGenerate(raw["server.generate"]), + voice: adaptGroupExperimentalVoice(raw["server.voice"]), + integration: adaptGroupExperimentalIntegration(raw["server.integration"]), + mcp: adaptGroupExperimentalMcp(raw["server.mcp"]), + file: adaptGroupExperimentalFile(raw["server.fs"]), + migration: adaptGroupExperimentalMigration(raw["server.migration"]), + usage: adaptGroupExperimentalUsage(raw["server.usage"]), + config: adaptGroupExperimentalConfig(raw["server.config"]), + }, + message: adaptGroupMessage(raw["server.message"]), + model: adaptGroupModel(raw["server.model"]), + provider: adaptGroupProvider(raw["server.provider"]), + integration: adaptGroupIntegration(raw["server.integration"]), + mcp: adaptGroupMcp(raw["server.mcp"]), + credential: adaptGroupCredential(raw["server.credential"]), + project: adaptGroupProject(raw["server.project"]), + form: adaptGroupForm(raw["server.form"]), + permission: adaptGroupPermission(raw["server.permission"]), + file: adaptGroupFile(raw["server.fs"]), + command: adaptGroupCommand(raw["server.command"]), + skill: adaptGroupSkill(raw["server.skill"]), + rpc: adaptGroupRpc(raw["server.rpc"]), + event: adaptGroupEvent(raw["server.event"]), + pty: adaptGroupPty(raw["server.pty"]), + shell: adaptGroupShell(raw["server.shell"]), + reference: adaptGroupReference(raw["server.reference"]), + worktree: adaptGroupWorktree(raw["server.worktree"]), + vcs: adaptGroupVcs(raw["server.vcs"]), + debug: adaptGroupDebug(raw["server.debug"]), + websearch: adaptGroupWebsearch(raw["server.websearch"]), + config: adaptGroupConfig(raw["server.config"]), + } + return { + ...client, + session: { + ...client.session, + /** @deprecated Use `client.experimental.session.stats`. Removed in the next minor. */ + stats: client.experimental.session.stats, + /** @deprecated Use `client.experimental.session.import`. Removed in the next minor. */ + import: client.experimental.session.import, + /** @deprecated Use `client.experimental.session.export`. Removed in the next minor. */ + export: client.experimental.session.export, + /** @deprecated Use `client.experimental.session.skill`. Removed in the next minor. */ + skill: client.experimental.session.skill, + /** @deprecated Use `client.experimental.session.wait`. Removed in the next minor. */ + wait: client.experimental.session.wait, + /** @deprecated Use `client.experimental.session.instructions`. Removed in the next minor. */ + instructions: { + /** @deprecated Use `client.experimental.session.instructions.entry`. Removed in the next minor. */ + entry: { + /** @deprecated Use `client.experimental.session.instructions.entry.list`. Removed in the next minor. */ + list: client.experimental.session.instructions.entry.list, + /** @deprecated Use `client.experimental.session.instructions.entry.put`. Removed in the next minor. */ + put: client.experimental.session.instructions.entry.put, + /** @deprecated Use `client.experimental.session.instructions.entry.remove`. Removed in the next minor. */ + remove: client.experimental.session.instructions.entry.remove, + }, + }, + /** @deprecated Use `client.experimental.session.log`. Removed in the next minor. */ + log: client.experimental.session.log, + }, + /** @deprecated Use `client.experimental.generate`. Removed in the next minor. */ + generate: { + /** @deprecated Use `client.experimental.generate.text`. Removed in the next minor. */ + text: client.experimental.generate.text, + }, + /** @deprecated Use `client.experimental.voice`. Removed in the next minor. */ + voice: { + /** @deprecated Use `client.experimental.voice.models`. Removed in the next minor. */ + models: client.experimental.voice.models, + /** @deprecated Use `client.experimental.voice.session`. Removed in the next minor. */ + session: client.experimental.voice.session, + }, + integration: { + ...client.integration, + /** @deprecated Use `client.experimental.integration.wellknown`. Removed in the next minor. */ + wellknown: { + /** @deprecated Use `client.experimental.integration.wellknown.add`. Removed in the next minor. */ + add: client.experimental.integration.wellknown.add, + }, + }, + mcp: { + ...client.mcp, + /** @deprecated Use `client.experimental.mcp.add`. Removed in the next minor. */ + add: client.experimental.mcp.add, + /** @deprecated Use `client.experimental.mcp.remove`. Removed in the next minor. */ + remove: client.experimental.mcp.remove, + /** @deprecated Use `client.experimental.mcp.connect`. Removed in the next minor. */ + connect: client.experimental.mcp.connect, + /** @deprecated Use `client.experimental.mcp.disconnect`. Removed in the next minor. */ + disconnect: client.experimental.mcp.disconnect, + /** @deprecated Use `client.experimental.mcp.client`. Removed in the next minor. */ + client: { + /** @deprecated Use `client.experimental.mcp.client.token`. Removed in the next minor. */ + token: client.experimental.mcp.client.token, + }, + }, + file: { + ...client.file, + /** @deprecated Use `client.experimental.file.watch`. Removed in the next minor. */ + watch: client.experimental.file.watch, + /** @deprecated Use `client.experimental.file.unwatch`. Removed in the next minor. */ + unwatch: client.experimental.file.unwatch, + }, + /** @deprecated Use `client.experimental.migration`. Removed in the next minor. */ + migration: { + /** @deprecated Use `client.experimental.migration.v1`. Removed in the next minor. */ + v1: { + /** @deprecated Use `client.experimental.migration.v1.status`. Removed in the next minor. */ + status: client.experimental.migration.v1.status, + }, + }, + /** @deprecated Use `client.experimental.usage`. Removed in the next minor. */ + usage: { + /** @deprecated Use `client.experimental.usage.limits`. Removed in the next minor. */ + limits: client.experimental.usage.limits, + }, + config: { + ...client.config, + /** @deprecated Use `client.experimental.config.update`. Removed in the next minor. */ + update: client.experimental.config.update, + }, + } +} export const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(ClientApi, options).pipe(Effect.map(adaptClient)) diff --git a/packages/client/src/promise/client.ts b/packages/client/src/promise/client.ts index 84ded7fc33f2..cbcab3dcdf5c 100644 --- a/packages/client/src/promise/client.ts +++ b/packages/client/src/promise/client.ts @@ -1,6 +1,5 @@ export * as OpenCode from "./client.js" -import { DeprecatedAliases } from "../deprecated-aliases.js" import { SharedEvents } from "../shared-events.js" import { OpenCode } from "./generated/index.js" import type { ClientOptions } from "./generated/client.js" @@ -11,15 +10,8 @@ export type { ClientOptions, RequestOptions } from "./generated/client.js" export function make(options: ClientOptions) { const raw = OpenCode.make(options) const events = SharedEvents.make((signal, onActivity) => raw.event.subscribe(undefined, { signal, onActivity })) - const deprecated = DeprecatedAliases.make(raw) return { ...raw, - ...deprecated, - file: { - ...deprecated.file, - /** @deprecated Use `client.experimental.file.write`. Removed in the next minor. */ - write: raw.experimental.file.write, - }, rpc: Object.assign(makeRpc(raw, events), raw.rpc), event: events, } diff --git a/packages/client/src/promise/deprecated.ts b/packages/client/src/promise/deprecated.ts deleted file mode 100644 index 163f86b89aed..000000000000 --- a/packages/client/src/promise/deprecated.ts +++ /dev/null @@ -1,141 +0,0 @@ -// Pre-experimental names of the types #59072 renamed with an `Experimental` prefix. Removed in the next minor; see -// .github/RELEASE_REMINDERS.md. -import type * as Experimental from "./generated/types.js" - -/** @deprecated Use `ExperimentalConfigUpdateInput`. Removed in the next minor. */ -export type ConfigUpdateInput = Experimental.ExperimentalConfigUpdateInput - -/** @deprecated Use `ExperimentalConfigUpdateOutput`. Removed in the next minor. */ -export type ConfigUpdateOutput = Experimental.ExperimentalConfigUpdateOutput - -/** @deprecated Use `ExperimentalFileUnwatchInput`. Removed in the next minor. */ -export type FileUnwatchInput = Experimental.ExperimentalFileUnwatchInput - -/** @deprecated Use `ExperimentalFileUnwatchOutput`. Removed in the next minor. */ -export type FileUnwatchOutput = Experimental.ExperimentalFileUnwatchOutput - -/** @deprecated Use `ExperimentalFileWatchInput`. Removed in the next minor. */ -export type FileWatchInput = Experimental.ExperimentalFileWatchInput - -/** @deprecated Use `ExperimentalFileWatchOutput`. Removed in the next minor. */ -export type FileWatchOutput = Experimental.ExperimentalFileWatchOutput - -/** @deprecated Use `ExperimentalFileWriteInput`. Removed in the next minor. */ -export type FileWriteInput = Experimental.ExperimentalFileWriteInput - -/** @deprecated Use `ExperimentalFileWriteOutput`. Removed in the next minor. */ -export type FileWriteOutput = Experimental.ExperimentalFileWriteOutput - -/** @deprecated Use `ExperimentalGenerateTextInput`. Removed in the next minor. */ -export type GenerateTextInput = Experimental.ExperimentalGenerateTextInput - -/** @deprecated Use `ExperimentalGenerateTextOutput`. Removed in the next minor. */ -export type GenerateTextOutput = Experimental.ExperimentalGenerateTextOutput - -/** @deprecated Use `ExperimentalIntegrationWellknownAddInput`. Removed in the next minor. */ -export type IntegrationWellknownAddInput = Experimental.ExperimentalIntegrationWellknownAddInput - -/** @deprecated Use `ExperimentalIntegrationWellknownAddOutput`. Removed in the next minor. */ -export type IntegrationWellknownAddOutput = Experimental.ExperimentalIntegrationWellknownAddOutput - -/** @deprecated Use `ExperimentalMcpAddInput`. Removed in the next minor. */ -export type McpAddInput = Experimental.ExperimentalMcpAddInput - -/** @deprecated Use `ExperimentalMcpAddOutput`. Removed in the next minor. */ -export type McpAddOutput = Experimental.ExperimentalMcpAddOutput - -/** @deprecated Use `ExperimentalMcpClientTokenInput`. Removed in the next minor. */ -export type McpClientTokenInput = Experimental.ExperimentalMcpClientTokenInput - -/** @deprecated Use `ExperimentalMcpClientTokenOutput`. Removed in the next minor. */ -export type McpClientTokenOutput = Experimental.ExperimentalMcpClientTokenOutput - -/** @deprecated Use `ExperimentalMcpConnectInput`. Removed in the next minor. */ -export type McpConnectInput = Experimental.ExperimentalMcpConnectInput - -/** @deprecated Use `ExperimentalMcpConnectOutput`. Removed in the next minor. */ -export type McpConnectOutput = Experimental.ExperimentalMcpConnectOutput - -/** @deprecated Use `ExperimentalMcpDisconnectInput`. Removed in the next minor. */ -export type McpDisconnectInput = Experimental.ExperimentalMcpDisconnectInput - -/** @deprecated Use `ExperimentalMcpDisconnectOutput`. Removed in the next minor. */ -export type McpDisconnectOutput = Experimental.ExperimentalMcpDisconnectOutput - -/** @deprecated Use `ExperimentalMcpRemoveInput`. Removed in the next minor. */ -export type McpRemoveInput = Experimental.ExperimentalMcpRemoveInput - -/** @deprecated Use `ExperimentalMcpRemoveOutput`. Removed in the next minor. */ -export type McpRemoveOutput = Experimental.ExperimentalMcpRemoveOutput - -/** @deprecated Use `ExperimentalMigrationV1StatusOutput`. Removed in the next minor. */ -export type MigrationV1StatusOutput = Experimental.ExperimentalMigrationV1StatusOutput - -/** @deprecated Use `ExperimentalSessionExportInput`. Removed in the next minor. */ -export type SessionExportInput = Experimental.ExperimentalSessionExportInput - -/** @deprecated Use `ExperimentalSessionExportOutput`. Removed in the next minor. */ -export type SessionExportOutput = Experimental.ExperimentalSessionExportOutput - -/** @deprecated Use `ExperimentalSessionImportInput`. Removed in the next minor. */ -export type SessionImportInput = Experimental.ExperimentalSessionImportInput - -/** @deprecated Use `ExperimentalSessionImportOutput`. Removed in the next minor. */ -export type SessionImportOutput = Experimental.ExperimentalSessionImportOutput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryListInput`. Removed in the next minor. */ -export type SessionInstructionsEntryListInput = Experimental.ExperimentalSessionInstructionsEntryListInput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryListOutput`. Removed in the next minor. */ -export type SessionInstructionsEntryListOutput = Experimental.ExperimentalSessionInstructionsEntryListOutput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryPutInput`. Removed in the next minor. */ -export type SessionInstructionsEntryPutInput = Experimental.ExperimentalSessionInstructionsEntryPutInput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOutput`. Removed in the next minor. */ -export type SessionInstructionsEntryPutOutput = Experimental.ExperimentalSessionInstructionsEntryPutOutput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveInput`. Removed in the next minor. */ -export type SessionInstructionsEntryRemoveInput = Experimental.ExperimentalSessionInstructionsEntryRemoveInput - -/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOutput`. Removed in the next minor. */ -export type SessionInstructionsEntryRemoveOutput = Experimental.ExperimentalSessionInstructionsEntryRemoveOutput - -/** @deprecated Use `ExperimentalSessionLogInput`. Removed in the next minor. */ -export type SessionLogInput = Experimental.ExperimentalSessionLogInput - -/** @deprecated Use `ExperimentalSessionLogOutput`. Removed in the next minor. */ -export type SessionLogOutput = Experimental.ExperimentalSessionLogOutput - -/** @deprecated Use `ExperimentalSessionSkillInput`. Removed in the next minor. */ -export type SessionSkillInput = Experimental.ExperimentalSessionSkillInput - -/** @deprecated Use `ExperimentalSessionSkillOutput`. Removed in the next minor. */ -export type SessionSkillOutput = Experimental.ExperimentalSessionSkillOutput - -/** @deprecated Use `ExperimentalSessionStatsInput`. Removed in the next minor. */ -export type SessionStatsInput = Experimental.ExperimentalSessionStatsInput - -/** @deprecated Use `ExperimentalSessionStatsOutput`. Removed in the next minor. */ -export type SessionStatsOutput = Experimental.ExperimentalSessionStatsOutput - -/** @deprecated Use `ExperimentalSessionWaitInput`. Removed in the next minor. */ -export type SessionWaitInput = Experimental.ExperimentalSessionWaitInput - -/** @deprecated Use `ExperimentalSessionWaitOutput`. Removed in the next minor. */ -export type SessionWaitOutput = Experimental.ExperimentalSessionWaitOutput - -/** @deprecated Use `ExperimentalUsageLimitsInput`. Removed in the next minor. */ -export type UsageLimitsInput = Experimental.ExperimentalUsageLimitsInput - -/** @deprecated Use `ExperimentalUsageLimitsOutput`. Removed in the next minor. */ -export type UsageLimitsOutput = Experimental.ExperimentalUsageLimitsOutput - -/** @deprecated Use `ExperimentalVoiceModelsOutput`. Removed in the next minor. */ -export type VoiceModelsOutput = Experimental.ExperimentalVoiceModelsOutput - -/** @deprecated Use `ExperimentalVoiceSessionInput`. Removed in the next minor. */ -export type VoiceSessionInput = Experimental.ExperimentalVoiceSessionInput - -/** @deprecated Use `ExperimentalVoiceSessionOutput`. Removed in the next minor. */ -export type VoiceSessionOutput = Experimental.ExperimentalVoiceSessionOutput diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 800d7cbd7a43..95444f425ea2 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -438,7 +438,7 @@ export function make(options: ClientOptions) { }, }) - return { + const client = { server: { info: (requestOptions?: RequestOptions) => request( @@ -2415,6 +2415,99 @@ export function make(options: ClientOptions) { ), }, } + return { + ...client, + session: { + ...client.session, + /** @deprecated Use `client.experimental.session.stats`. Removed in the next minor. */ + stats: client.experimental.session.stats, + /** @deprecated Use `client.experimental.session.import`. Removed in the next minor. */ + import: client.experimental.session.import, + /** @deprecated Use `client.experimental.session.export`. Removed in the next minor. */ + export: client.experimental.session.export, + /** @deprecated Use `client.experimental.session.skill`. Removed in the next minor. */ + skill: client.experimental.session.skill, + /** @deprecated Use `client.experimental.session.wait`. Removed in the next minor. */ + wait: client.experimental.session.wait, + /** @deprecated Use `client.experimental.session.instructions`. Removed in the next minor. */ + instructions: { + /** @deprecated Use `client.experimental.session.instructions.entry`. Removed in the next minor. */ + entry: { + /** @deprecated Use `client.experimental.session.instructions.entry.list`. Removed in the next minor. */ + list: client.experimental.session.instructions.entry.list, + /** @deprecated Use `client.experimental.session.instructions.entry.put`. Removed in the next minor. */ + put: client.experimental.session.instructions.entry.put, + /** @deprecated Use `client.experimental.session.instructions.entry.remove`. Removed in the next minor. */ + remove: client.experimental.session.instructions.entry.remove, + }, + }, + /** @deprecated Use `client.experimental.session.log`. Removed in the next minor. */ + log: client.experimental.session.log, + }, + /** @deprecated Use `client.experimental.generate`. Removed in the next minor. */ + generate: { + /** @deprecated Use `client.experimental.generate.text`. Removed in the next minor. */ + text: client.experimental.generate.text, + }, + /** @deprecated Use `client.experimental.voice`. Removed in the next minor. */ + voice: { + /** @deprecated Use `client.experimental.voice.models`. Removed in the next minor. */ + models: client.experimental.voice.models, + /** @deprecated Use `client.experimental.voice.session`. Removed in the next minor. */ + session: client.experimental.voice.session, + }, + integration: { + ...client.integration, + /** @deprecated Use `client.experimental.integration.wellknown`. Removed in the next minor. */ + wellknown: { + /** @deprecated Use `client.experimental.integration.wellknown.add`. Removed in the next minor. */ + add: client.experimental.integration.wellknown.add, + }, + }, + mcp: { + ...client.mcp, + /** @deprecated Use `client.experimental.mcp.add`. Removed in the next minor. */ + add: client.experimental.mcp.add, + /** @deprecated Use `client.experimental.mcp.remove`. Removed in the next minor. */ + remove: client.experimental.mcp.remove, + /** @deprecated Use `client.experimental.mcp.connect`. Removed in the next minor. */ + connect: client.experimental.mcp.connect, + /** @deprecated Use `client.experimental.mcp.disconnect`. Removed in the next minor. */ + disconnect: client.experimental.mcp.disconnect, + /** @deprecated Use `client.experimental.mcp.client`. Removed in the next minor. */ + client: { + /** @deprecated Use `client.experimental.mcp.client.token`. Removed in the next minor. */ + token: client.experimental.mcp.client.token, + }, + }, + file: { + ...client.file, + /** @deprecated Use `client.experimental.file.write`. Removed in the next minor. */ + write: client.experimental.file.write, + /** @deprecated Use `client.experimental.file.watch`. Removed in the next minor. */ + watch: client.experimental.file.watch, + /** @deprecated Use `client.experimental.file.unwatch`. Removed in the next minor. */ + unwatch: client.experimental.file.unwatch, + }, + /** @deprecated Use `client.experimental.migration`. Removed in the next minor. */ + migration: { + /** @deprecated Use `client.experimental.migration.v1`. Removed in the next minor. */ + v1: { + /** @deprecated Use `client.experimental.migration.v1.status`. Removed in the next minor. */ + status: client.experimental.migration.v1.status, + }, + }, + /** @deprecated Use `client.experimental.usage`. Removed in the next minor. */ + usage: { + /** @deprecated Use `client.experimental.usage.limits`. Removed in the next minor. */ + limits: client.experimental.usage.limits, + }, + config: { + ...client.config, + /** @deprecated Use `client.experimental.config.update`. Removed in the next minor. */ + update: client.experimental.config.update, + }, + } } function encodePath(value: string): string { diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index e44966f35f68..c5cd524b74e4 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -4352,3 +4352,141 @@ export type ConfigShellsOutput = Array export type ExperimentalConfigUpdateInput = { readonly shell: string | null } export type ExperimentalConfigUpdateOutput = void + +/** @deprecated Use `ExperimentalSessionStatsInput`. Removed in the next minor. */ +export type SessionStatsInput = ExperimentalSessionStatsInput + +/** @deprecated Use `ExperimentalSessionStatsOutput`. Removed in the next minor. */ +export type SessionStatsOutput = ExperimentalSessionStatsOutput + +/** @deprecated Use `ExperimentalSessionImportInput`. Removed in the next minor. */ +export type SessionImportInput = ExperimentalSessionImportInput + +/** @deprecated Use `ExperimentalSessionImportOutput`. Removed in the next minor. */ +export type SessionImportOutput = ExperimentalSessionImportOutput + +/** @deprecated Use `ExperimentalSessionExportInput`. Removed in the next minor. */ +export type SessionExportInput = ExperimentalSessionExportInput + +/** @deprecated Use `ExperimentalSessionExportOutput`. Removed in the next minor. */ +export type SessionExportOutput = ExperimentalSessionExportOutput + +/** @deprecated Use `ExperimentalSessionSkillInput`. Removed in the next minor. */ +export type SessionSkillInput = ExperimentalSessionSkillInput + +/** @deprecated Use `ExperimentalSessionSkillOutput`. Removed in the next minor. */ +export type SessionSkillOutput = ExperimentalSessionSkillOutput + +/** @deprecated Use `ExperimentalSessionWaitInput`. Removed in the next minor. */ +export type SessionWaitInput = ExperimentalSessionWaitInput + +/** @deprecated Use `ExperimentalSessionWaitOutput`. Removed in the next minor. */ +export type SessionWaitOutput = ExperimentalSessionWaitOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListInput`. Removed in the next minor. */ +export type SessionInstructionsEntryListInput = ExperimentalSessionInstructionsEntryListInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryListOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryListOutput = ExperimentalSessionInstructionsEntryListOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutInput`. Removed in the next minor. */ +export type SessionInstructionsEntryPutInput = ExperimentalSessionInstructionsEntryPutInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryPutOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryPutOutput = ExperimentalSessionInstructionsEntryPutOutput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveInput`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveInput = ExperimentalSessionInstructionsEntryRemoveInput + +/** @deprecated Use `ExperimentalSessionInstructionsEntryRemoveOutput`. Removed in the next minor. */ +export type SessionInstructionsEntryRemoveOutput = ExperimentalSessionInstructionsEntryRemoveOutput + +/** @deprecated Use `ExperimentalSessionLogInput`. Removed in the next minor. */ +export type SessionLogInput = ExperimentalSessionLogInput + +/** @deprecated Use `ExperimentalSessionLogOutput`. Removed in the next minor. */ +export type SessionLogOutput = ExperimentalSessionLogOutput + +/** @deprecated Use `ExperimentalGenerateTextInput`. Removed in the next minor. */ +export type GenerateTextInput = ExperimentalGenerateTextInput + +/** @deprecated Use `ExperimentalGenerateTextOutput`. Removed in the next minor. */ +export type GenerateTextOutput = ExperimentalGenerateTextOutput + +/** @deprecated Use `ExperimentalVoiceModelsOutput`. Removed in the next minor. */ +export type VoiceModelsOutput = ExperimentalVoiceModelsOutput + +/** @deprecated Use `ExperimentalVoiceSessionInput`. Removed in the next minor. */ +export type VoiceSessionInput = ExperimentalVoiceSessionInput + +/** @deprecated Use `ExperimentalVoiceSessionOutput`. Removed in the next minor. */ +export type VoiceSessionOutput = ExperimentalVoiceSessionOutput + +/** @deprecated Use `ExperimentalIntegrationWellknownAddInput`. Removed in the next minor. */ +export type IntegrationWellknownAddInput = ExperimentalIntegrationWellknownAddInput + +/** @deprecated Use `ExperimentalIntegrationWellknownAddOutput`. Removed in the next minor. */ +export type IntegrationWellknownAddOutput = ExperimentalIntegrationWellknownAddOutput + +/** @deprecated Use `ExperimentalMcpAddInput`. Removed in the next minor. */ +export type McpAddInput = ExperimentalMcpAddInput + +/** @deprecated Use `ExperimentalMcpAddOutput`. Removed in the next minor. */ +export type McpAddOutput = ExperimentalMcpAddOutput + +/** @deprecated Use `ExperimentalMcpRemoveInput`. Removed in the next minor. */ +export type McpRemoveInput = ExperimentalMcpRemoveInput + +/** @deprecated Use `ExperimentalMcpRemoveOutput`. Removed in the next minor. */ +export type McpRemoveOutput = ExperimentalMcpRemoveOutput + +/** @deprecated Use `ExperimentalMcpConnectInput`. Removed in the next minor. */ +export type McpConnectInput = ExperimentalMcpConnectInput + +/** @deprecated Use `ExperimentalMcpConnectOutput`. Removed in the next minor. */ +export type McpConnectOutput = ExperimentalMcpConnectOutput + +/** @deprecated Use `ExperimentalMcpDisconnectInput`. Removed in the next minor. */ +export type McpDisconnectInput = ExperimentalMcpDisconnectInput + +/** @deprecated Use `ExperimentalMcpDisconnectOutput`. Removed in the next minor. */ +export type McpDisconnectOutput = ExperimentalMcpDisconnectOutput + +/** @deprecated Use `ExperimentalMcpClientTokenInput`. Removed in the next minor. */ +export type McpClientTokenInput = ExperimentalMcpClientTokenInput + +/** @deprecated Use `ExperimentalMcpClientTokenOutput`. Removed in the next minor. */ +export type McpClientTokenOutput = ExperimentalMcpClientTokenOutput + +/** @deprecated Use `ExperimentalFileWriteInput`. Removed in the next minor. */ +export type FileWriteInput = ExperimentalFileWriteInput + +/** @deprecated Use `ExperimentalFileWriteOutput`. Removed in the next minor. */ +export type FileWriteOutput = ExperimentalFileWriteOutput + +/** @deprecated Use `ExperimentalFileWatchInput`. Removed in the next minor. */ +export type FileWatchInput = ExperimentalFileWatchInput + +/** @deprecated Use `ExperimentalFileWatchOutput`. Removed in the next minor. */ +export type FileWatchOutput = ExperimentalFileWatchOutput + +/** @deprecated Use `ExperimentalFileUnwatchInput`. Removed in the next minor. */ +export type FileUnwatchInput = ExperimentalFileUnwatchInput + +/** @deprecated Use `ExperimentalFileUnwatchOutput`. Removed in the next minor. */ +export type FileUnwatchOutput = ExperimentalFileUnwatchOutput + +/** @deprecated Use `ExperimentalMigrationV1StatusOutput`. Removed in the next minor. */ +export type MigrationV1StatusOutput = ExperimentalMigrationV1StatusOutput + +/** @deprecated Use `ExperimentalUsageLimitsInput`. Removed in the next minor. */ +export type UsageLimitsInput = ExperimentalUsageLimitsInput + +/** @deprecated Use `ExperimentalUsageLimitsOutput`. Removed in the next minor. */ +export type UsageLimitsOutput = ExperimentalUsageLimitsOutput + +/** @deprecated Use `ExperimentalConfigUpdateInput`. Removed in the next minor. */ +export type ConfigUpdateInput = ExperimentalConfigUpdateInput + +/** @deprecated Use `ExperimentalConfigUpdateOutput`. Removed in the next minor. */ +export type ConfigUpdateOutput = ExperimentalConfigUpdateOutput diff --git a/packages/client/src/promise/index.ts b/packages/client/src/promise/index.ts index a75e6d58639c..04d10d1eef5b 100644 --- a/packages/client/src/promise/index.ts +++ b/packages/client/src/promise/index.ts @@ -1,7 +1,6 @@ import type { OpenCode } from "./client.js" export * from "./generated/index.js" -export type * from "./deprecated.js" export { OpenCode } from "./client.js" export type { AgentApi, diff --git a/packages/client/test/deprecated-aliases.test.ts b/packages/client/test/deprecated-aliases.test.ts new file mode 100644 index 000000000000..438eea0c26da --- /dev/null +++ b/packages/client/test/deprecated-aliases.test.ts @@ -0,0 +1,45 @@ +import { expect, test } from "bun:test" +import { compile } from "@opencode/httpapi-codegen" +import { + ClientApi, + clientNamespace, + effectOmitEndpoints, + groupNames, + promiseOmitEndpoints, +} from "@opencode/protocol/client" +import { Effect } from "effect" +import { HttpClient } from "effect/http" +import { OpenCode as EffectOpenCode } from "../src/effect/index" +import { OpenCode as PromiseOpenCode } from "../src/promise/index" + +// The pre-experimental paths stay until the next minor (.github/RELEASE_REMINDERS.md). +const aliases = (omitEndpoints: ReadonlySet) => + compile(ClientApi, { groupNames, omitEndpoints, namespace: clientNamespace, aliases: true }).aliases ?? [] + +const at = (root: unknown, path: ReadonlyArray) => + path.reduce((parent, key) => (parent as Record | undefined)?.[key], root) + +const expectSameFunctions = (client: unknown, list: ReturnType) => { + expect(list.length).toBeGreaterThan(20) + for (const alias of list) { + expect(alias.to[0]).toBe("experimental") + const target = at(client, alias.to) + expect(target).toBeFunction() + expect(at(client, alias.from)).toBe(target) + } +} + +test("every Promise client alias is the same function as its client.experimental path", () => { + expectSameFunctions(PromiseOpenCode.make({ baseUrl: "http://localhost:3000" }), aliases(promiseOmitEndpoints)) +}) + +test("every Effect client alias is the same function as its client.experimental path", async () => { + const client = await EffectOpenCode.make({ baseUrl: "http://localhost:3000" }).pipe( + Effect.provideService( + HttpClient.HttpClient, + HttpClient.make(() => Effect.die("unused")), + ), + Effect.runPromise, + ) + expectSameFunctions(client, aliases(effectOmitEndpoints)) +}) diff --git a/packages/client/test/effect.test.ts b/packages/client/test/effect.test.ts index 3bbc028ac118..cc132f4e7701 100644 --- a/packages/client/test/effect.test.ts +++ b/packages/client/test/effect.test.ts @@ -74,23 +74,6 @@ test("session.get returns the decoded Effect projection", async () => { expect(DateTime.toEpochMillis(result.time.created)).toBe(1_717_171_717_000) }) -test("pre-experimental paths stay as deprecated aliases of client.experimental", async () => { - const client = await OpenCode.make({ baseUrl: "http://localhost:3000" }).pipe( - Effect.provideService( - HttpClient.HttpClient, - HttpClient.make(() => Effect.die("unused")), - ), - Effect.runPromise, - ) - expect(client.session.wait).toBe(client.experimental.session.wait) - expect(client.session.instructions).toBe(client.experimental.session.instructions) - expect(client.generate).toBe(client.experimental.generate) - expect(client.mcp.client).toBe(client.experimental.mcp.client) - expect(client.config.update).toBe(client.experimental.config.update) - expect(client.config.get).toBeFunction() - expect("write" in client.file).toBe(false) -}) - test("session instructions methods use the public HTTP contract", async () => { const requests: Array<{ method: string; url: string; body?: unknown }> = [] const instructions = [{ key: "review-notes", value: { text: "Check the diff", priority: 1 } }] diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 495c03bf6547..7d115d453a66 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -46,7 +46,7 @@ test("exposes every standard HTTP API group", () => { expect(Object.keys(client.integration.oauth)).toEqual(["connect", "status", "complete", "cancel"]) expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"]) expect(Object.keys(client.websearch)).toEqual(["providers", "query"]) - expect(Object.keys(client.file)).toEqual(["read", "list", "find", "watch", "unwatch", "write"]) + expect(Object.keys(client.file)).toEqual(["read", "list", "find", "write", "watch", "unwatch"]) expect(Object.keys(client.mcp)).toEqual(["list", "resource", "add", "remove", "connect", "disconnect", "client"]) expect(Object.keys(client.config)).toEqual(["get", "shells", "update"]) expect(Object.keys(client.vcs)).toEqual(["init", "get", "base", "status", "branch", "diff"]) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 193c0d94a6a9..6bcf93f2d6cb 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -116,36 +116,6 @@ export const make = Effect.fn("PluginHost.make")(function* ( }).pipe(Effect.provide(locations.get(ref)), Effect.orDie), ) - const experimental: Plugin.Context["experimental"] = { - generate: { - text: (input) => generate.text(input).pipe(Effect.map((text) => ({ text }))), - }, - session: { - wait: (input) => sessions.wait(input.sessionID), - instructions: { - entry: { - list: Effect.fn(function* (input) { - yield* sessions.get(input.sessionID) - return yield* instructionEntries.list(input.sessionID) - }), - put: Effect.fn(function* (input) { - const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) - yield* sessions.get(input.sessionID) - yield* instructionEntries.put({ ...input, key }) - }), - remove: Effect.fn(function* (input) { - const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) - yield* sessions.get(input.sessionID) - yield* instructionEntries.remove({ ...input, key }) - }), - }, - }, - }, - terminal: { - read: (input) => persistentPty.read(input.sessionID, input.lines), - }, - } - // Keep the instance graph's inferred types independent of Session handles. const context: Plugin.Context = { app, @@ -308,9 +278,14 @@ export const make = Effect.fn("PluginHost.make")(function* ( ) }) as Plugin.Context["event"]["subscribe"], }, - experimental, - // Deprecated pre-experimental path; removed in the next minor (.github/RELEASE_REMINDERS.md). - generate: experimental.generate, + experimental: { + terminal: { + read: (input) => persistentPty.read(input.sessionID, input.lines), + }, + }, + generate: { + text: (input) => generate.text(input).pipe(Effect.map((text) => ({ text }))), + }, integration: { list: () => response(integration.list()), get: Effect.fn(function* (input) { @@ -622,10 +597,26 @@ export const make = Effect.fn("PluginHost.make")(function* ( sessions .interrupt(input.sessionID, { resume: input.resume }) .pipe(Effect.map((interrupted) => ({ interrupted }))), + wait: (input) => sessions.wait(input.sessionID), context: (input) => sessions.context(input.sessionID), - // Deprecated pre-experimental paths; removed in the next minor (.github/RELEASE_REMINDERS.md). - wait: experimental.session.wait, - instructions: experimental.session.instructions, + instructions: { + entry: { + list: Effect.fn(function* (input) { + yield* sessions.get(input.sessionID) + return yield* instructionEntries.list(input.sessionID) + }), + put: Effect.fn(function* (input) { + const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) + yield* sessions.get(input.sessionID) + yield* instructionEntries.put({ ...input, key }) + }), + remove: Effect.fn(function* (input) { + const key = yield* Schema.decodeUnknownEffect(InstructionEntry.Key)(input.key) + yield* sessions.get(input.sessionID) + yield* instructionEntries.remove({ ...input, key }) + }), + }, + }, }, } return context diff --git a/packages/core/test/plugin/experimental-namespace.test.ts b/packages/core/test/plugin/experimental-namespace.test.ts deleted file mode 100644 index f0eead7ca377..000000000000 --- a/packages/core/test/plugin/experimental-namespace.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { expect, test } from "bun:test" -import { PluginPromise } from "@opencode/core/plugin/promise" -import { adaptedEndpoints } from "@opencode/plugin/promise/adapter" -import type { Context } from "@opencode/plugin/promise/plugin" -import { Effect } from "effect" -import { host } from "./host" - -// The Promise adapter records the endpoint and Effect host method behind every API method it builds, -// so this follows the real mapping instead of a hand-kept list of experimental methods. -test("ctx.experimental holds exactly the plugin methods backed by /api/experimental endpoints", async () => { - const effectContext = host() - let promiseContext: Context | undefined - await Effect.runPromise( - Effect.scoped( - PluginPromise.fromPromise({ - id: "test.experimental-namespace", - setup: (context) => { - promiseContext = context - }, - }).effect(effectContext), - ), - ) - - const hostPaths = paths(effectContext) - const methods = Array.from(paths(promiseContext)).flatMap(([value, promise]) => { - const adapted = adaptedEndpoints.get(value) - return adapted ? [{ promise, host: hostPaths.get(adapted.method) ?? [], endpoint: adapted.endpoint.path }] : [] - }) - const isExperimental = (endpoint: string) => - endpoint === "/api/experimental" || endpoint.startsWith("/api/experimental/") - const underExperimental = (list: ReadonlyArray) => list.some((path) => path.startsWith("experimental.")) - - expect(methods.length).toBeGreaterThan(40) - expect(methods.filter(({ endpoint }) => isExperimental(endpoint)).flatMap(({ promise }) => promise)).toContain( - "experimental.session.wait", - ) - expect( - methods - .filter( - ({ promise, host, endpoint }) => - isExperimental(endpoint) !== underExperimental(promise) || - isExperimental(endpoint) !== underExperimental(host), - ) - .map( - ({ promise, host, endpoint }) => - `ctx.${promise.join("|")} (host ${host.join("|") || "unknown"}) -> ${endpoint}`, - ), - ).toEqual([]) - - // Pre-experimental paths kept as deprecated aliases until the next minor (.github/RELEASE_REMINDERS.md). - const deprecated = [ - "generate.text", - "session.instructions.entry.list", - "session.instructions.entry.put", - "session.instructions.entry.remove", - "session.wait", - ] - const outside = (pick: (method: (typeof methods)[number]) => ReadonlyArray) => - methods - .filter(({ endpoint }) => isExperimental(endpoint)) - .flatMap((method) => pick(method).filter((path) => !path.startsWith("experimental."))) - .toSorted() - expect(outside((method) => method.promise)).toEqual(deprecated) - expect(outside((method) => method.host)).toEqual(deprecated) -}) - -// Every path to every function, so a method reachable from both its home and a deprecated alias lists both. -function paths(root: unknown) { - const result = new Map>() - const step = (value: unknown, path: ReadonlyArray, ancestors: ReadonlySet) => { - if ((typeof value !== "object" && typeof value !== "function") || value === null || ancestors.has(value)) return - if (typeof value === "function") result.set(value, [...(result.get(value) ?? []), path.join(".")]) - const next = new Set(ancestors).add(value) - Object.entries(value).forEach(([key, child]) => step(child, [...path, key], next)) - } - step(root, [], new Set()) - return result -} diff --git a/packages/core/test/plugin/host.ts b/packages/core/test/plugin/host.ts index c551f026a076..8fdb96d3bafe 100644 --- a/packages/core/test/plugin/host.ts +++ b/packages/core/test/plugin/host.ts @@ -12,29 +12,11 @@ import { WebSearch } from "@opencode/core/websearch" import { Effect, Stream } from "effect" type EventSource = ReturnType -type Overrides = Partial> & { +type Overrides = Partial> & { readonly session?: Partial readonly event?: { readonly subscribe: () => EventSource } } export function host(overrides: Overrides = {}): Plugin.Context { - const experimental: Plugin.Context["experimental"] = overrides.experimental ?? { - generate: { - text: () => Effect.die("unused experimental.generate.text"), - }, - session: { - wait: () => Effect.die("unused experimental.session.wait"), - instructions: { - entry: { - list: () => Effect.die("unused experimental.session.instructions.entry.list"), - put: () => Effect.die("unused experimental.session.instructions.entry.put"), - remove: () => Effect.die("unused experimental.session.instructions.entry.remove"), - }, - }, - }, - terminal: { - read: () => Effect.die("unused experimental.terminal.read"), - }, - } return { app: overrides.app ?? { name: "test", version: "test", channel: "test" }, location: @@ -86,8 +68,14 @@ export function host(overrides: Overrides = {}): Plugin.Context { activate: () => Effect.die("unused credential.activate"), }, event: { subscribe: events(overrides.event?.subscribe ?? (() => Stream.empty)) }, - experimental, - generate: experimental.generate, + experimental: overrides.experimental ?? { + terminal: { + read: () => Effect.die("unused experimental.terminal.read"), + }, + }, + generate: overrides.generate ?? { + text: () => Effect.die("unused generate.text"), + }, integration: overrides.integration ?? { list: () => Effect.die("unused integration.list"), get: () => Effect.die("unused integration.get"), @@ -195,9 +183,15 @@ export function host(overrides: Overrides = {}): Plugin.Context { move: overrides.session?.move ?? (() => Effect.die("unused session.move")), synthetic: overrides.session?.synthetic ?? (() => Effect.die("unused session.synthetic")), interrupt: overrides.session?.interrupt ?? (() => Effect.die("unused session.interrupt")), + wait: overrides.session?.wait ?? (() => Effect.die("unused session.wait")), context: overrides.session?.context ?? (() => Effect.die("unused session.context")), - wait: experimental.session.wait, - instructions: experimental.session.instructions, + instructions: overrides.session?.instructions ?? { + entry: { + list: () => Effect.die("unused session.instructions.entry.list"), + put: () => Effect.die("unused session.instructions.entry.put"), + remove: () => Effect.die("unused session.instructions.entry.remove"), + }, + }, }, } } diff --git a/packages/core/test/plugin/session-instructions.test.ts b/packages/core/test/plugin/session-instructions.test.ts index 42866b2441f0..123a6fe6b42b 100644 --- a/packages/core/test/plugin/session-instructions.test.ts +++ b/packages/core/test/plugin/session-instructions.test.ts @@ -30,12 +30,12 @@ it.live("Effect plugins can put, list, and remove session instruction entries se const context = yield* PluginHost.make(plugins) const created = yield* context.session.create({ title: "Plugin instructions" }) - yield* context.experimental.session.instructions.entry.put({ + yield* context.session.instructions.entry.put({ sessionID: created.id, key: "deploy-target", value: "production", }) - expect(yield* context.experimental.session.instructions.entry.list({ sessionID: created.id })).toEqual([ + expect(yield* context.session.instructions.entry.list({ sessionID: created.id })).toEqual([ { key: "deploy-target", value: "production" }, ]) expect(yield* sessions.inbox(created.id)).toEqual([]) @@ -44,7 +44,7 @@ it.live("Effect plugins can put, list, and remove session instruction entries se expect(first.initial).toBe(['', "production", ""].join("\n")) expect(first.entries).toEqual([]) - yield* context.experimental.session.instructions.entry.put({ + yield* context.session.instructions.entry.put({ sessionID: created.id, key: "deploy-target", value: { region: "us-east-1" }, @@ -64,8 +64,8 @@ it.live("Effect plugins can put, list, and remove session instruction entries se }), ]) - yield* context.experimental.session.instructions.entry.remove({ sessionID: created.id, key: "deploy-target" }) - expect(yield* context.experimental.session.instructions.entry.list({ sessionID: created.id })).toEqual([]) + yield* context.session.instructions.entry.remove({ sessionID: created.id, key: "deploy-target" }) + expect(yield* context.session.instructions.entry.list({ sessionID: created.id })).toEqual([]) const third = yield* prepareRequest(created.id) expect(third.entries.at(-1)?.message).toMatchObject({ @@ -76,13 +76,9 @@ it.live("Effect plugins can put, list, and remove session instruction entries se const missing = Session.ID.create() for (const operation of [ - context.experimental.session.instructions.entry.list({ sessionID: missing }), - context.experimental.session.instructions.entry.put({ - sessionID: missing, - key: "deploy-target", - value: "production", - }), - context.experimental.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }), + context.session.instructions.entry.list({ sessionID: missing }), + context.session.instructions.entry.put({ sessionID: missing, key: "deploy-target", value: "production" }), + context.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }), ]) { expect(yield* operation.pipe(Effect.flip)).toMatchObject({ _tag: "Session.NotFoundError", @@ -90,7 +86,7 @@ it.live("Effect plugins can put, list, and remove session instruction entries se }) } expect( - yield* context.experimental.session.instructions.entry + yield* context.session.instructions.entry .put({ sessionID: created.id, key: "oversized", @@ -101,13 +97,13 @@ it.live("Effect plugins can put, list, and remove session instruction entries se for (const key of ["Bad Key", "invalid/key"]) { for (const operation of [ - context.experimental.session.instructions.entry.put({ sessionID: created.id, key, value: "rejected" }), - context.experimental.session.instructions.entry.remove({ sessionID: created.id, key }), + context.session.instructions.entry.put({ sessionID: created.id, key, value: "rejected" }), + context.session.instructions.entry.remove({ sessionID: created.id, key }), ]) { expect(yield* operation.pipe(Effect.flip)).toMatchObject({ _tag: "SchemaError" }) } } - yield* context.experimental.session.instructions.entry.put({ + yield* context.session.instructions.entry.put({ sessionID: created.id, key: "recovered", value: "still works", @@ -131,42 +127,38 @@ it.live("Promise plugins can put, list, and remove session instruction entries s id: "test.session-instructions.put", async setup(ctx) { expect( - await ctx.experimental.session.instructions.entry.put({ + await ctx.session.instructions.entry.put({ sessionID: created.id, key: "team.guidelines", value: "Prefer small, focused changes.", }), ).toBeUndefined() - expect(await ctx.experimental.session.instructions.entry.list({ sessionID: created.id })).toEqual([ + expect(await ctx.session.instructions.entry.list({ sessionID: created.id })).toEqual([ { key: "team.guidelines", value: "Prefer small, focused changes." }, ]) const missing = Session.ID.create() - await expect(ctx.experimental.session.instructions.entry.list({ sessionID: missing })).rejects.toMatchObject({ + await expect(ctx.session.instructions.entry.list({ sessionID: missing })).rejects.toMatchObject({ _tag: "Session.NotFoundError", sessionID: missing, }) await expect( - ctx.experimental.session.instructions.entry.put({ - sessionID: missing, - key: "deploy-target", - value: "production", - }), + ctx.session.instructions.entry.put({ sessionID: missing, key: "deploy-target", value: "production" }), ).rejects.toMatchObject({ _tag: "Session.NotFoundError", sessionID: missing }) await expect( - ctx.experimental.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }), + ctx.session.instructions.entry.remove({ sessionID: missing, key: "deploy-target" }), ).rejects.toMatchObject({ _tag: "Session.NotFoundError", sessionID: missing }) await expect( - ctx.experimental.session.instructions.entry.put({ + ctx.session.instructions.entry.put({ sessionID: created.id, key: "invalid/key", value: "rejected", }), ).rejects.toMatchObject({ _tag: "SchemaError" }) await expect( - ctx.experimental.session.instructions.entry.remove({ sessionID: created.id, key: "invalid/key" }), + ctx.session.instructions.entry.remove({ sessionID: created.id, key: "invalid/key" }), ).rejects.toMatchObject({ _tag: "SchemaError" }) await expect( - ctx.experimental.session.instructions.entry.put({ + ctx.session.instructions.entry.put({ sessionID: created.id, key: "oversized", value: "x".repeat(InstructionEntry.MaxValueBytes), @@ -185,12 +177,12 @@ it.live("Promise plugins can put, list, and remove session instruction entries s id: "test.session-instructions.remove", async setup(ctx) { expect( - await ctx.experimental.session.instructions.entry.remove({ + await ctx.session.instructions.entry.remove({ sessionID: created.id, key: "team.guidelines", }), ).toBeUndefined() - expect(await ctx.experimental.session.instructions.entry.list({ sessionID: created.id })).toEqual([]) + expect(await ctx.session.instructions.entry.list({ sessionID: created.id })).toEqual([]) }, }).effect(context) diff --git a/packages/httpapi-codegen/src/index.ts b/packages/httpapi-codegen/src/index.ts index 60b27fb72215..44401be47617 100644 --- a/packages/httpapi-codegen/src/index.ts +++ b/packages/httpapi-codegen/src/index.ts @@ -33,6 +33,16 @@ export type Output = { export type Contract = { readonly groups: ReadonlyArray + readonly aliases?: ReadonlyArray +} + +/** A namespaced endpoint's pre-namespace client path and group, kept as a deprecated alias of its new location. */ +export type Alias = { + readonly from: ReadonlyArray + readonly to: ReadonlyArray + readonly fromGroup: string + readonly group: Group + readonly endpoint: Endpoint } export type EffectTypeReference = { @@ -111,10 +121,17 @@ export function compile..`. A group whose name equals the namespace is not repeated. */ readonly namespace?: (endpoint: HttpApiEndpoint.Top) => string | undefined + /** + * Keeps each namespaced endpoint's pre-namespace client path and generated type names as deprecated aliases of + * the namespaced ones, so moving an endpoint into a namespace does not break callers. + */ + readonly aliases?: boolean }, ): Contract { const endpoints: Array = [] const portable = new Map() + // Where each namespaced endpoint would sit without its namespace. + const unnamespaced = new Map }>() HttpApi.reflect(api, { onGroup() {}, @@ -125,8 +142,7 @@ export function compile) => { const keys = new Set() for (const entry of node.entries) { - const names = typeof entry === "string" ? [entry] : [...new Set(entry.endpoints.map((item) => item.clientPath[0]))] + const names = + typeof entry === "string" ? [entry] : [...new Set(entry.endpoints.map((item) => item.clientPath[0]))] for (const name of names) { if (keys.has(name)) throw new GenerationError({ reason: `Client name collision: ${[...path, name].join(".")}` }) keys.add(name) @@ -311,9 +332,36 @@ export function compile + group.endpoints.flatMap((endpoint): Array => { + const previous = unnamespaced.get(endpoint) + if (previous === undefined) return [] + return [ + { + from: [...previous.path, ...endpoint.clientPath], + to: [...group.path, ...endpoint.clientPath], + fromGroup: previous.group, + group, + endpoint, + }, + ] + }), + ) + const paths = groups.flatMap((group) => group.endpoints.map((endpoint) => [...group.path, ...endpoint.clientPath])) + for (const alias of aliases) { + const key = alias.from.join("\0") + const conflict = paths.some((path) => { + const other = path.join("\0") + return other === key || other.startsWith(`${key}\0`) || key.startsWith(`${other}\0`) + }) + if (conflict) throw new GenerationError({ reason: `Client alias collision: ${alias.from.join(".")}` }) } + assertUniqueClientPaths( + aliases.map((alias) => alias.from), + (path) => `Client alias collision: ${path.join(".")}`, + ) + return { groups, aliases } } export function emitEffect(contract: Contract): Output { @@ -323,7 +371,7 @@ export function emitEffect(contract: Contract): Output { reason: `Effect schema requires authoritative import: ${endpoint.group}.${endpoint.endpoint.identifier}`, }) } - return { operations: operations(contract.groups), files: renderEffectFiles(contract.groups) } + return { operations: operations(contract.groups), files: renderEffectFiles(contract.groups, contract.aliases ?? []) } } export function emitEffectImported( @@ -339,7 +387,7 @@ export function emitEffectImported( ): Output { return { operations: operations(contract.groups), - files: renderImportedEffectFiles(contract.groups, options), + files: renderImportedEffectFiles(contract.groups, contract.aliases ?? [], options), } } @@ -355,7 +403,12 @@ export function emitEffectShape( files: [ { path: "api.ts", - content: renderEffectShape(contract.groups, options?.typeReferences ?? [], options?.outputTypes), + content: renderEffectShape( + contract.groups, + options?.typeReferences ?? [], + options?.outputTypes, + contract.aliases ?? [], + ), }, ], } @@ -375,14 +428,17 @@ export function emitPromise( return { operations: promiseOperations(groups), files: [ - { path: "types.ts", content: renderPromiseTypes(groups, options?.outputTypes, options?.mutableOutputs ?? false) }, + { + path: "types.ts", + content: renderPromiseTypes(groups, options?.outputTypes, options?.mutableOutputs ?? false, contract.aliases), + }, { path: "client-error.ts", content: `export type ClientErrorReason = "Transport" | "UnexpectedStatus" | "UnsupportedContentType" | "MalformedResponse" | "SseEventTooLarge"\n\nexport class ClientError extends Error {\n override readonly name = "ClientError"\n constructor(readonly reason: ClientErrorReason, options?: ErrorOptions & { readonly detail?: string | null }) {\n const detail = options?.detail ?? (options?.cause instanceof Error ? options.cause.message : undefined)\n super(detail ? \`\${reason}: \${detail}\` : reason, options)\n }\n}\n`, }, { path: "client.ts", - content: normalizePromiseClientContent(renderPromiseClient(groups), groups), + content: normalizePromiseClientContent(renderPromiseClient(groups, contract.aliases), groups), }, { path: "index.ts", @@ -396,7 +452,8 @@ export function emitPromise( function renderEffectShape( groups: ReadonlyArray, typeReferences: ReadonlyArray, - outputTypes?: Readonly>, + outputTypes: Readonly> | undefined, + aliases: ReadonlyArray, ) { const references = effectTypeReferences(typeReferences) const imports = new Set() @@ -419,6 +476,64 @@ function renderEffectShape( if (collision !== undefined) { throw new GenerationError({ reason: `Generated Effect type collides with imported type: ${collision}` }) } + // An alias joins the interface of the deepest group above its old path, so code typed against that group keeps + // compiling; an alias with no such group becomes a field of AppApi. + type AliasField = { readonly path: ReadonlyArray; readonly value: string; readonly alias: Alias } + const groupAliases = new Map>() + const rootAliases: Array = [] + for (const alias of aliases) { + const value = `${groupShapeTypeName(alias.group, alias.endpoint)}` + const owner = groups + .filter( + (group) => + group.path.length > 0 && + group.path.length < alias.from.length && + group.path.every((name, index) => alias.from[index] === name), + ) + .toSorted((a, b) => b.path.length - a.path.length)[0] + if (owner === undefined) { + rootAliases.push({ path: alias.from, value, alias }) + continue + } + groupAliases.set(owner, [ + ...(groupAliases.get(owner) ?? []), + { path: alias.from.slice(owner.path.length), value, alias }, + ]) + } + const rootNames = new Set( + groups.flatMap((group) => group.endpoints.map((endpoint) => [...group.path, ...endpoint.clientPath][0])), + ) + const unowned = rootAliases.find((item) => rootNames.has(item.path[0])) + if (unowned !== undefined) { + throw new GenerationError({ reason: `Client alias has no group to join: ${unowned.alias.from.join(".")}` }) + } + const aliasTypes: Array<{ readonly name: string; readonly target: string; readonly generic?: true }> = + aliases.flatMap((alias) => { + const from = `${identifierPart(alias.fromGroup)}${alias.endpoint.clientPath.map(identifierPart).join("")}` + const to = endpointTypeName(alias.group, alias.endpoint) + return [ + ...(alias.endpoint.operation.inputMode === "none" ? [] : [{ name: `${from}Input`, target: `${to}Input` }]), + { name: `${from}Output`, target: `${to}Output` }, + { name: `${from}Operation`, target: `${to}Operation`, generic: true as const }, + ] + }) + // A group whose endpoints all moved into one group keeps its interface name as an alias of that group's. + for (const fromGroup of new Set(aliases.map((alias) => alias.fromGroup))) { + if (groups.some((group) => group.identifier === fromGroup)) continue + const moved = aliases.filter((alias) => alias.fromGroup === fromGroup) + const target = moved[0].group + if (moved.some((alias) => alias.group !== target) || moved.length !== target.endpoints.length) continue + aliasTypes.push({ name: `${identifierPart(fromGroup)}Api`, target: groupShapeName(target), generic: true }) + } + const takenNames = new Set([...externalNames, ...generatedNames]) + for (const { name } of aliasTypes) { + if (takenNames.has(name)) throw new GenerationError({ reason: `Client type alias collision: ${name}` }) + takenNames.add(name) + } + const renderedAliasTypes = aliasTypes.map( + (item) => + `${deprecatedTypeAliasDoc(item.target)}\nexport type ${item.name}${item.generic ? "" : ""} = ${item.target}${item.generic ? "" : ""}`, + ) const endpointTypes = groups.map((group) => { const endpoints = group.endpoints.map((endpoint) => { const prefix = endpointTypeName(group, endpoint) @@ -451,6 +566,7 @@ function renderEffectShape( (endpoint) => `${groupShapeTypeName(group, endpoint)}`, (name, value) => `readonly ${JSON.stringify(name)}: ${value}`, "\n", + groupAliases.get(group), ) return `${endpoints.join("\n\n")}\n\nexport interface ${groupShapeName(group)} {\n${methods}\n}` }) @@ -471,14 +587,21 @@ function renderEffectShape( (wholes, fields) => [...wholes, ...(fields.length === 0 ? [] : [`{\n${fields.join("\n")}\n}`])].join(" & "), "\n", ) + const rootAliasFields = renderClientTree( + [], + () => "", + (name, value) => `readonly ${JSON.stringify(name)}: ${value}`, + "\n", + rootAliases, + ) return `// Generated by @opencode/httpapi-codegen. Do not edit. import type { Effect, Stream } from "effect" ${[...imports].join("\n")} -${endpointTypes.join("\n\n")} +${[...endpointTypes, ...renderedAliasTypes].join("\n\n")} export interface AppApi { -${clientFields} +${[clientFields, rootAliasFields].filter(Boolean).join("\n")} } ` } @@ -653,7 +776,7 @@ function promiseOperations(groups: ReadonlyArray) { ) } -function renderEffectFiles(groups: ReadonlyArray): Output["files"] { +function renderEffectFiles(groups: ReadonlyArray, aliases: ReadonlyArray): Output["files"] { return [ ...groups.map((group) => ({ path: `${group.module}.ts`, content: renderGroup(group) })), { @@ -661,7 +784,7 @@ function renderEffectFiles(groups: ReadonlyArray): Output["files"] { content: 'import { Schema } from "effect"\n\nexport class ClientError extends Schema.TaggedError()("ClientError", {\n cause: Schema.Defect(),\n}) {}\n', }, - { path: "client.ts", content: renderClient(groups) }, + { path: "client.ts", content: renderClient(groups, aliases) }, { path: "index.ts", content: 'export { ClientError } from "./client-error.js"\nexport * as OpenCode from "./client.js"\n', @@ -671,6 +794,7 @@ function renderEffectFiles(groups: ReadonlyArray): Output["files"] { function renderImportedEffectFiles( groups: ReadonlyArray, + aliases: ReadonlyArray, options: | { readonly module: string; readonly api: string; readonly shapeModule?: string } | { readonly module: string; readonly group: string; readonly shapeModule?: string } @@ -789,7 +913,7 @@ function renderImportedEffectFiles( options.shapeModule === undefined ? "" : `const preserveEffect = () => (effect: Effect.Effect & ([A] extends [Actual] ? unknown : { readonly __generatedOutputWiderThanContract: [expected: Actual, generated: A] })): Effect.Effect => effect\n${usesStream ? "const preserveStream = () => (stream: Stream.Stream & ([A] extends [Actual] ? unknown : { readonly __generatedOutputWiderThanContract: [expected: Actual, generated: A] })): Stream.Stream => stream\n" : ""}\n` - const client = `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/encoding"\nimport { HttpClientError } from "effect/http"\n${httpApiImport}${imports}\n${shapeImport}import { ClientError } from "./client-error.js"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${preserve}${adapters.join("\n\n")}\n\nconst adaptClient = (raw: RawClient) => ({ ${fields} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` + const client = `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect${usesStream ? ", Stream" : ""}, Schema } from "effect"\nimport { Sse } from "effect/encoding"\nimport { HttpClientError } from "effect/http"\n${httpApiImport}${imports}\n${shapeImport}import { ClientError } from "./client-error.js"\n\n${projection?.source ?? ""}type RawClient = HttpApiClient.ForApi\n\nconst mapClientError = (error: E) => HttpClientError.isHttpClientError(error) || Schema.isSchemaError(error) || Sse.Retry.is(error) ? new ClientError({ cause: error }) : error\n\n${preserve}${adapters.join("\n\n")}\n\nconst adaptClient = ${aliases.length === 0 ? `(raw: RawClient) => ({ ${fields} })` : `(raw: RawClient) => {\n const client = { ${fields} }\n return ${renderAliasedClient(groups, aliases, "client")}\n}`}\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) => HttpApiClient.make(${api}, options).pipe(Effect.map(adaptClient))\n` return [ { path: "client-error.ts", @@ -836,6 +960,7 @@ function renderPromiseTypes( groups: ReadonlyArray, outputTypes?: Readonly>, mutableOutputs = false, + aliases: ReadonlyArray = [], ) { const types = new Map() const typeOf = (schema: Schema.Top, decoded = false) => { @@ -932,6 +1057,17 @@ function renderPromiseTypes( ), ...Object.values(outputTypes ?? {}).map((output) => output.name), ]) + const aliasTypes = aliases.flatMap((alias) => { + const from = promiseTypePrefix(alias.fromGroup, alias.endpoint.clientPath) + const to = promiseTypePrefix(alias.group.identifier, alias.endpoint.clientPath) + return [...(promiseInputMode(alias.endpoint) === "none" ? [] : ["Input"]), "Output"].map( + (suffix) => [`${from}${suffix}`, `${to}${suffix}`] as const, + ) + }) + for (const [name] of aliasTypes) { + if (reservedNames.has(name)) throw new GenerationError({ reason: `Client type alias collision: ${name}` }) + reservedNames.add(name) + } const rendered = structuralTypes(outputSchemas, mutableOutputs, reservedNames) const resolve = (source: string) => rendered.types.reduce((result, type, index) => result.replaceAll(`__PROMISE_TYPE_${index}__`, type), source) @@ -943,14 +1079,19 @@ function renderPromiseTypes( ? `export type JsonValue = null | boolean | number | string | ${mutableOutputs ? "Array | { [key: string]: JsonValue }" : "ReadonlyArray | { readonly [key: string]: JsonValue }"}` : "" const imports = [...new Set(Object.values(outputTypes ?? {}).map((override) => override.import))] - return [...imports, json, ...rendered.definitions, ...resolvedErrors, resolvedOperations].filter(Boolean).join("\n\n") + const resolvedAliases = aliasTypes + .map(([from, to]) => `${deprecatedTypeAliasDoc(to)}\nexport type ${from} = ${to}`) + .join("\n\n") + return [...imports, json, ...rendered.definitions, ...resolvedErrors, resolvedOperations, resolvedAliases] + .filter(Boolean) + .join("\n\n") } function mutableType(type: string) { return type.replaceAll("ReadonlyArray<", "Array<").replaceAll(/\breadonly\s+/g, "") } -function renderPromiseClient(groups: ReadonlyArray) { +function renderPromiseClient(groups: ReadonlyArray, aliases: ReadonlyArray = []) { const imports = groups.flatMap((group) => group.endpoints.flatMap((endpoint) => { const prefix = promiseTypePrefix(group.identifier, endpoint.clientPath) @@ -1014,7 +1155,11 @@ function renderPromiseClient(groups: ReadonlyArray) { spreadObject, ", ", ) - return `import type { ${imports.join(", ")} } from "./types.js"\nimport { ClientError } from "./client-error.js"\n\nexport interface ClientOptions {\n readonly baseUrl: string\n readonly fetch?: typeof globalThis.fetch\n readonly headers?: RequestInit["headers"]\n}\n\nexport interface RequestOptions {\n readonly signal?: AbortSignal\n readonly headers?: RequestInit["headers"]\n /** Reports every chunk a streaming response receives, including keepalive comments that yield no event. */\n readonly onActivity?: () => void\n}\n\ninterface RequestDescriptor {\n readonly method: string\n readonly path: string\n readonly query?: Record\n readonly headers?: Record\n readonly body?: unknown\n readonly successStatus: number\n readonly declaredStatuses: ReadonlyArray\n readonly empty: boolean\n}\n\nconst maxSseEventBytes = 16 * 1024 * 1024\n\nexport function make(options: ClientOptions) {\n const fetch = options.fetch ?? globalThis.fetch\n\n const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n // A leading slash would replace any path prefix on baseUrl, so join relative to it.\n const baseUrl = new URL(options.baseUrl)\n if (!baseUrl.pathname.endsWith("/")) baseUrl.pathname += "/"\n const url = new URL(descriptor.path.slice(1), baseUrl)\n for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value)\n const headers = new Headers(options.headers)\n for (const [key, value] of Object.entries(descriptor.headers ?? {})) {\n if (value !== undefined && value !== null) headers.set(key, String(value))\n }\n for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)\n if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")\n return {\n url,\n init: {\n method: descriptor.method,\n signal: requestOptions?.signal,\n headers,\n body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body),\n } satisfies RequestInit,\n }\n }\n\n const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n try {\n const prepared = prepare(descriptor, requestOptions)\n return await fetch(prepared.url, prepared.init)\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n }\n\n const responseError = async (response: Response, descriptor: RequestDescriptor): Promise => {\n if (descriptor.declaredStatuses.includes(response.status)) throw declared(await json(response) as DeclaredErrorBody)\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnexpectedStatus", { cause: { status: response.status }, detail: String(response.status) })\n }\n\n const request = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise => {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) return responseError(response, descriptor)\n if (descriptor.empty) {\n try {\n await response.body?.cancel()\n } catch {}\n return undefined as A\n }\n return await json(response) as A\n }\n\n const sse = (descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable => ({\n async *[Symbol.asyncIterator]() {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) await responseError(response, descriptor)\n if (!isContentType(response, "text/event-stream")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType", { detail: response.headers.get("content-type") })\n }\n if (response.body === null) throw new ClientError("MalformedResponse")\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ""\n try {\n while (true) {\n let next: ReadableStreamReadResult\n try {\n next = await reader.read()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (!next.done) requestOptions?.onActivity?.()\n buffer += decoder.decode(next.value, { stream: !next.done })\n if (buffer.length > maxSseEventBytes) throw new ClientError("SseEventTooLarge")\n const trailingCarriageReturn = !next.done && buffer.endsWith("\\r")\n if (trailingCarriageReturn) buffer = buffer.slice(0, -1)\n buffer = buffer.replaceAll("\\r\\n", "\\n").replaceAll("\\r", "\\n")\n if (trailingCarriageReturn) buffer += "\\r"\n if (next.done && buffer !== "") buffer += "\\n\\n"\n let boundary = buffer.indexOf("\\n\\n")\n while (boundary >= 0) {\n const block = buffer.slice(0, boundary)\n buffer = buffer.slice(boundary + 2)\n const data = block.split("\\n").flatMap((line) => line.startsWith("data:") ? [line.slice(5).trimStart()] : []).join("\\n")\n if (data !== "") {\n try {\n yield JSON.parse(data) as A\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n }\n boundary = buffer.indexOf("\\n\\n")\n }\n if (next.done) return\n }\n } finally {\n try {\n await reader.cancel()\n } catch {}\n reader.releaseLock()\n }\n },\n })\n\n return { ${fields} }\n}\n\nfunction appendQuery(params: URLSearchParams, key: string, value: unknown): void {\n if (value === undefined) return\n if (value === null) {\n params.append(key, "null")\n return\n }\n if (Array.isArray(value)) {\n for (const item of value) appendQuery(params, key, item)\n return\n }\n if (typeof value === "object") {\n for (const [child, item] of Object.entries(value)) appendQuery(params, \`\${key}[\${child}]\`, item)\n return\n }\n params.append(key, String(value))\n}\n\nasync function json(response: Response): Promise {\n if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType", { detail: response.headers.get("content-type") })\n }\n let text: string\n try {\n text = await response.text()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (text === "") throw new ClientError("MalformedResponse")\n try {\n return JSON.parse(text)\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n}\n\ntype DeclaredErrorBody = { readonly _tag?: string; readonly message?: string; readonly data?: { readonly message?: string } }\n\n/** Throw declared error bodies as Errors. The body's fields stay on the error, so narrowing on \`_tag\` or \`name\` still works. */\nfunction declared(body: DeclaredErrorBody) {\n const error = Object.assign(new Error(body.message ?? body.data?.message), body)\n if (body._tag) error.name = body._tag\n return error\n}\n\nfunction isContentType(response: Response, expected: string) {\n return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected\n}\n` + const returned = + aliases.length === 0 + ? `return { ${fields} }` + : `const client = { ${fields} }\n return ${renderAliasedClient(groups, aliases, "client")}` + return `import type { ${imports.join(", ")} } from "./types.js"\nimport { ClientError } from "./client-error.js"\n\nexport interface ClientOptions {\n readonly baseUrl: string\n readonly fetch?: typeof globalThis.fetch\n readonly headers?: RequestInit["headers"]\n}\n\nexport interface RequestOptions {\n readonly signal?: AbortSignal\n readonly headers?: RequestInit["headers"]\n /** Reports every chunk a streaming response receives, including keepalive comments that yield no event. */\n readonly onActivity?: () => void\n}\n\ninterface RequestDescriptor {\n readonly method: string\n readonly path: string\n readonly query?: Record\n readonly headers?: Record\n readonly body?: unknown\n readonly successStatus: number\n readonly declaredStatuses: ReadonlyArray\n readonly empty: boolean\n}\n\nconst maxSseEventBytes = 16 * 1024 * 1024\n\nexport function make(options: ClientOptions) {\n const fetch = options.fetch ?? globalThis.fetch\n\n const prepare = (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n // A leading slash would replace any path prefix on baseUrl, so join relative to it.\n const baseUrl = new URL(options.baseUrl)\n if (!baseUrl.pathname.endsWith("/")) baseUrl.pathname += "/"\n const url = new URL(descriptor.path.slice(1), baseUrl)\n for (const [key, value] of Object.entries(descriptor.query ?? {})) appendQuery(url.searchParams, key, value)\n const headers = new Headers(options.headers)\n for (const [key, value] of Object.entries(descriptor.headers ?? {})) {\n if (value !== undefined && value !== null) headers.set(key, String(value))\n }\n for (const [key, value] of new Headers(requestOptions?.headers)) headers.set(key, value)\n if (descriptor.body !== undefined && !headers.has("content-type")) headers.set("content-type", "application/json")\n return {\n url,\n init: {\n method: descriptor.method,\n signal: requestOptions?.signal,\n headers,\n body: descriptor.body === undefined ? undefined : JSON.stringify(descriptor.body),\n } satisfies RequestInit,\n }\n }\n\n const execute = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions) => {\n try {\n const prepared = prepare(descriptor, requestOptions)\n return await fetch(prepared.url, prepared.init)\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n }\n\n const responseError = async (response: Response, descriptor: RequestDescriptor): Promise => {\n if (descriptor.declaredStatuses.includes(response.status)) throw declared(await json(response) as DeclaredErrorBody)\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnexpectedStatus", { cause: { status: response.status }, detail: String(response.status) })\n }\n\n const request = async (descriptor: RequestDescriptor, requestOptions?: RequestOptions): Promise => {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) return responseError(response, descriptor)\n if (descriptor.empty) {\n try {\n await response.body?.cancel()\n } catch {}\n return undefined as A\n }\n return await json(response) as A\n }\n\n const sse = (descriptor: RequestDescriptor, requestOptions?: RequestOptions): AsyncIterable => ({\n async *[Symbol.asyncIterator]() {\n const response = await execute(descriptor, requestOptions)\n if (response.status !== descriptor.successStatus) await responseError(response, descriptor)\n if (!isContentType(response, "text/event-stream")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType", { detail: response.headers.get("content-type") })\n }\n if (response.body === null) throw new ClientError("MalformedResponse")\n const reader = response.body.getReader()\n const decoder = new TextDecoder()\n let buffer = ""\n try {\n while (true) {\n let next: ReadableStreamReadResult\n try {\n next = await reader.read()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (!next.done) requestOptions?.onActivity?.()\n buffer += decoder.decode(next.value, { stream: !next.done })\n if (buffer.length > maxSseEventBytes) throw new ClientError("SseEventTooLarge")\n const trailingCarriageReturn = !next.done && buffer.endsWith("\\r")\n if (trailingCarriageReturn) buffer = buffer.slice(0, -1)\n buffer = buffer.replaceAll("\\r\\n", "\\n").replaceAll("\\r", "\\n")\n if (trailingCarriageReturn) buffer += "\\r"\n if (next.done && buffer !== "") buffer += "\\n\\n"\n let boundary = buffer.indexOf("\\n\\n")\n while (boundary >= 0) {\n const block = buffer.slice(0, boundary)\n buffer = buffer.slice(boundary + 2)\n const data = block.split("\\n").flatMap((line) => line.startsWith("data:") ? [line.slice(5).trimStart()] : []).join("\\n")\n if (data !== "") {\n try {\n yield JSON.parse(data) as A\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n }\n boundary = buffer.indexOf("\\n\\n")\n }\n if (next.done) return\n }\n } finally {\n try {\n await reader.cancel()\n } catch {}\n reader.releaseLock()\n }\n },\n })\n\n ${returned}\n}\n\nfunction appendQuery(params: URLSearchParams, key: string, value: unknown): void {\n if (value === undefined) return\n if (value === null) {\n params.append(key, "null")\n return\n }\n if (Array.isArray(value)) {\n for (const item of value) appendQuery(params, key, item)\n return\n }\n if (typeof value === "object") {\n for (const [child, item] of Object.entries(value)) appendQuery(params, \`\${key}[\${child}]\`, item)\n return\n }\n params.append(key, String(value))\n}\n\nasync function json(response: Response): Promise {\n if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {\n try {\n await response.body?.cancel()\n } catch {}\n throw new ClientError("UnsupportedContentType", { detail: response.headers.get("content-type") })\n }\n let text: string\n try {\n text = await response.text()\n } catch (cause) {\n throw new ClientError("Transport", { cause })\n }\n if (text === "") throw new ClientError("MalformedResponse")\n try {\n return JSON.parse(text)\n } catch (cause) {\n throw new ClientError("MalformedResponse", { cause })\n }\n}\n\ntype DeclaredErrorBody = { readonly _tag?: string; readonly message?: string; readonly data?: { readonly message?: string } }\n\n/** Throw declared error bodies as Errors. The body's fields stay on the error, so narrowing on \`_tag\` or \`name\` still works. */\nfunction declared(body: DeclaredErrorBody) {\n const error = Object.assign(new Error(body.message ?? body.data?.message), body)\n if (body._tag) error.name = body._tag\n return error\n}\n\nfunction isContentType(response: Response, expected: string) {\n return response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === expected\n}\n` } function promiseTypePrefix(group: string, path: ReadonlyArray) { @@ -1055,30 +1200,113 @@ function assertUniqueClientPaths( } } +/** + * Renders a group's methods as nested fields. `aliases` adds deprecated fields at their path relative to the tree; + * a field that holds only aliases carries the deprecation note. + */ function renderClientTree( endpoints: ReadonlyArray, leaf: (endpoint: Endpoint, index: number) => string, field: (name: string, value: string) => string, separator: string, + aliases: ReadonlyArray<{ readonly path: ReadonlyArray; readonly value: string; readonly alias: Alias }> = [], ) { - type Node = { endpoint?: { readonly value: Endpoint; readonly index: number }; readonly children: Map } - const root: Node = { children: new Map() } + type Node = { + endpoint?: { readonly value: Endpoint; readonly index: number } + alias?: { readonly value: string; readonly alias: Alias; readonly depth: number } + real: boolean + readonly children: Map + } + const root: Node = { real: false, children: new Map() } + const insert = (path: ReadonlyArray, real: boolean) => + path.reduce((parent, name) => { + const child: Node = parent.children.get(name) ?? { real: false, children: new Map() } + child.real ||= real + parent.children.set(name, child) + return child + }, root) endpoints.forEach((endpoint, index) => { - const node = endpoint.clientPath.reduce((parent, name) => { + insert(endpoint.clientPath, true).endpoint = { value: endpoint, index } + }) + for (const item of aliases) { + const node = insert(item.path, false) + if (node.endpoint !== undefined || node.alias !== undefined || node.children.size > 0) { + throw new GenerationError({ reason: `Client alias collision: ${item.alias.from.join(".")}` }) + } + node.alias = { value: item.value, alias: item.alias, depth: item.path.length } + } + const firstAlias = (node: Node): { readonly alias: Alias; readonly depth: number } => + node.alias ?? firstAlias(node.children.values().next().value!) + const render = (node: Node, depth: number): string => + Array.from(node.children, ([name, child]) => { + const value = + child.endpoint !== undefined + ? leaf(child.endpoint.value, child.endpoint.index) + : child.alias !== undefined + ? child.alias.value + : `{ ${render(child, depth + 1)} }` + if (child.real) return field(name, value) + const { alias, depth: aliasDepth } = firstAlias(child) + return `\n${deprecatedAliasDoc(alias.to.slice(0, alias.to.length - (aliasDepth - depth - 1)))}\n${field(name, value)}` + }).join(separator) + return render(root, 0) +} + +function deprecatedAliasDoc(replacement: ReadonlyArray) { + return `/** @deprecated Use \`client.${replacement.join(".")}\`. Removed in the next minor. */` +} + +function deprecatedTypeAliasDoc(replacement: string) { + return `/** @deprecated Use \`${replacement}\`. Removed in the next minor. */` +} + +function propertyAccess(root: string, path: ReadonlyArray) { + return path.reduce( + (result, name) => + /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? `${result}.${name}` : `${result}[${JSON.stringify(name)}]`, + root, + ) +} + +/** + * Renders the runtime client with its aliases: `{ ...client, }`, where each alias is the namespaced + * function itself and an existing object on an alias path is spread rather than replaced. + */ +function renderAliasedClient(groups: ReadonlyArray, aliases: ReadonlyArray, client: string) { + const existing = new Set( + groups.flatMap((group) => + group.endpoints.flatMap((endpoint) => { + const path = [...group.path, ...endpoint.clientPath] + return path.slice(0, -1).map((_, index) => path.slice(0, index + 1).join("\0")) + }), + ), + ) + type Node = { alias?: Alias; readonly children: Map } + const root: Node = { children: new Map() } + for (const alias of aliases) { + const node = alias.from.reduce((parent, name) => { const child: Node = parent.children.get(name) ?? { children: new Map() } parent.children.set(name, child) return child }, root) - node.endpoint = { value: endpoint, index } - }) - const render = (node: Node): string => - Array.from(node.children, ([name, child]) => - field( - name, - child.endpoint === undefined ? `{ ${render(child)} }` : leaf(child.endpoint.value, child.endpoint.index), - ), - ).join(separator) - return render(root) + node.alias = alias + } + const firstAlias = (node: Node): Alias => node.alias ?? firstAlias(node.children.values().next().value!) + const render = (node: Node, path: ReadonlyArray): string => + Array.from(node.children, ([name, child]) => { + const childPath = [...path, name] + const key = JSON.stringify(name) + if (child.alias !== undefined) { + return `\n${deprecatedAliasDoc(child.alias.to)}\n${key}: ${propertyAccess(client, child.alias.to)}` + } + if (existing.has(childPath.join("\0"))) { + return `${key}: { ...${propertyAccess(client, childPath)}, ${render(child, childPath)} }` + } + const alias = firstAlias(child) + const replacement = alias.to.slice(0, alias.to.length - (alias.from.length - childPath.length)) + return `\n${deprecatedAliasDoc(replacement)}\n${key}: { ${render(child, childPath)} }` + }).join(", ") + return `{ ...${client}, ${render(root, [])} }` } // A node of the public client object: groups whose methods sit at this path, and nested @@ -1200,9 +1428,7 @@ function structuralTypes(schemas: ReadonlyArray, mutable: boolean, r const pattern = `(?() @@ -1256,10 +1482,7 @@ function structuralType(schema: Schema.Top) { } return type } - return preserveStringSuggestions( - expand(document.codes[0].Type) - .replaceAll("Schema.Json", "JsonValue"), - ) + return preserveStringSuggestions(expand(document.codes[0].Type).replaceAll("Schema.Json", "JsonValue")) } function includesUndefined(ast: SchemaAST.AST): boolean { @@ -2063,7 +2286,7 @@ function renderSchemas(slots: ReadonlyArray) { return [...artifacts, ...references, ...declarations].join("\n\n") } -function renderClient(groups: ReadonlyArray) { +function renderClient(groups: ReadonlyArray, aliases: ReadonlyArray) { const imports = groups .map( (group) => @@ -2084,5 +2307,5 @@ function renderClient(groups: ReadonlyArray) { spreadObject, ", ", ) - return `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect } from "effect"\nimport { HttpApi, HttpApiClient } from "effect/http-api"\n${imports}\n\nconst Api = ${api}\nconst adaptClient = (raw: HttpApiClient.ForApi) => ({ ${fields} })\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) =>\n HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))\n` + return `// Generated by @opencode/httpapi-codegen. Do not edit.\nimport { Effect } from "effect"\nimport { HttpApi, HttpApiClient } from "effect/http-api"\n${imports}\n\nconst Api = ${api}\nconst adaptClient = ${aliases.length === 0 ? `(raw: HttpApiClient.ForApi) => ({ ${fields} })` : `(raw: HttpApiClient.ForApi) => {\n const client = { ${fields} }\n return ${renderAliasedClient(groups, aliases, "client")}\n}`}\n\nexport const make = (options?: { readonly baseUrl?: URL | string }) =>\n HttpApiClient.make(Api, options).pipe(Effect.map(adaptClient))\n` } diff --git a/packages/httpapi-codegen/test/generate.test.ts b/packages/httpapi-codegen/test/generate.test.ts index 9866272e6355..8e4b68d9c0ac 100644 --- a/packages/httpapi-codegen/test/generate.test.ts +++ b/packages/httpapi-codegen/test/generate.test.ts @@ -480,6 +480,100 @@ describe("HttpApiCodegen.generate", () => { ) }) + test("keeps pre-namespace paths and type names as deprecated aliases", async () => { + const source = HttpApi.make("test") + .add( + HttpApiGroup.make("server.session") + .add(HttpApiEndpoint.get("get", "/session", { success: Schema.String })) + .add( + HttpApiEndpoint.get("stats", "/experimental/session/:sessionID/stats", { + params: { sessionID: Schema.String }, + success: Schema.String, + }), + ), + ) + .add( + HttpApiGroup.make("server.generate").add( + HttpApiEndpoint.post("text", "/experimental/generate/text", { + payload: Schema.Struct({ prompt: Schema.String }), + success: Schema.String, + }), + ), + ) + const options = { + groupNames: { "server.session": "session", "server.generate": "generate" }, + namespace: (endpoint: HttpApiEndpoint.Top) => + endpoint.path.startsWith("/experimental/") ? "experimental" : undefined, + } + expect(compileContract(source, options).aliases).toBeUndefined() + expect( + emitPromise(compileContract(source, options)) + .files.map((file) => file.content) + .join(""), + ).not.toContain("@deprecated") + + const contract = compileContract(source, { ...options, aliases: true }) + expect(contract.aliases?.map((alias) => [alias.from.join("."), alias.to.join(".")])).toEqual([ + ["session.stats", "experimental.session.stats"], + ["generate.text", "experimental.generate.text"], + ]) + + const paths: Array = [] + const promise = emitPromise(contract) + await using emitted = await emittedModule(promise) + const client = emitted.module.OpenCode.make({ + baseUrl: "https://example.com", + fetch: async (input: RequestInfo | URL) => { + paths.push(new URL(input instanceof Request ? input.url : input).pathname) + return Response.json("ok") + }, + }) + expect(Object.keys(client)).toEqual(["session", "experimental", "generate"]) + expect(Object.keys(client.session)).toEqual(["get", "stats"]) + expect(client.session.stats).toBe(client.experimental.session.stats) + expect(client.generate.text).toBe(client.experimental.generate.text) + expect(await client.session.stats({ sessionID: "a" })).toBe("ok") + expect(paths).toEqual(["/experimental/session/a/stats"]) + const promiseFile = (path: string) => promise.files.find((file) => file.path === path)?.content + expect(promiseFile("client.ts")).toContain( + "/** @deprecated Use `client.experimental.generate.text`. Removed in the next minor. */", + ) + expect(promiseFile("types.ts")).toContain( + "/** @deprecated Use `ExperimentalSessionStatsInput`. Removed in the next minor. */\nexport type SessionStatsInput = ExperimentalSessionStatsInput", + ) + + const shape = emitEffectShape(contract).files.find((file) => file.path === "api.ts")?.content + // The alias joins the interface of the group still at its old path, so code typed against SessionApi compiles. + expect(shape).toContain( + 'export interface SessionApi {\nreadonly "get": SessionGetOperation\n\n/** @deprecated Use `client.experimental.session.stats`. Removed in the next minor. */\nreadonly "stats": ExperimentalSessionStatsOperation\n}', + ) + expect(shape).toContain("export type SessionStatsOperation = ExperimentalSessionStatsOperation") + expect(shape).toContain("export type GenerateApi = ExperimentalGenerateApi") + expect(shape).toContain( + '/** @deprecated Use `client.experimental.generate`. Removed in the next minor. */\nreadonly "generate": {', + ) + expect(emitEffect(contract).files.find((file) => file.path === "client.ts")?.content).toContain( + 'return { ...client, "session": { ...client.session, ', + ) + }) + + test("rejects an alias that collides with a client path", () => { + const source = HttpApi.make("test").add( + HttpApiGroup.make("session") + .add(HttpApiEndpoint.get("stats", "/session/stats", { success: Schema.String })) + .add( + HttpApiEndpoint.get("experimentalStats", "/experimental/session/stats", { + success: Schema.String, + }).annotateMerge(OpenApi.annotations({ identifier: "experimental.session.stats" })), + ), + ) + const namespace = (endpoint: HttpApiEndpoint.Top) => + endpoint.path.startsWith("/experimental/") ? "experimental" : undefined + + expect(() => compileContract(source, { namespace })).not.toThrow() + expect(() => compileContract(source, { namespace, aliases: true })).toThrow("Client alias collision: session.stats") + }) + test("derives nested paths from OpenAPI operation IDs", () => { const source = HttpApi.make("test").add( HttpApiGroup.make("server.session").add( diff --git a/packages/plugin/src/effect/plugin.ts b/packages/plugin/src/effect/plugin.ts index 2e376bbf6412..b84c2e924209 100644 --- a/packages/plugin/src/effect/plugin.ts +++ b/packages/plugin/src/effect/plugin.ts @@ -1,10 +1,4 @@ -import type { - CredentialApi, - ExperimentalApi, - ExperimentalGenerateApi, - ExperimentalSessionApi, - PluginApi, -} from "@opencode/client/effect/api" +import type { CredentialApi, ExperimentalApi, ExperimentalGenerateApi, PluginApi } from "@opencode/client/effect/api" import type { Location } from "@opencode/schema/location" import type { Effect, Scope } from "effect" import type { PluginOptions } from "../options.js" @@ -39,14 +33,11 @@ export interface Context { readonly credential: Pick, "activate"> readonly event: EventDomain readonly experimental: { - readonly generate: ExperimentalGenerateApi - readonly session: Pick, "wait" | "instructions"> readonly terminal: Pick["persistentPty"], "read"> } readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain - /** @deprecated Use `ctx.experimental.generate`. Removed in the next minor. */ readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick, "list"> diff --git a/packages/plugin/src/effect/session.ts b/packages/plugin/src/effect/session.ts index de6f4d0ae05c..90446cf156fa 100644 --- a/packages/plugin/src/effect/session.ts +++ b/packages/plugin/src/effect/session.ts @@ -168,10 +168,7 @@ export type SessionDomain = Pick< | "update" | "move" | "context" -> & { - readonly hook: ModelHooks - /** @deprecated Use `ctx.experimental.session.wait`. Removed in the next minor. */ - readonly wait: ExperimentalSessionApi["wait"] - /** @deprecated Use `ctx.experimental.session.instructions`. Removed in the next minor. */ - readonly instructions: ExperimentalSessionApi["instructions"] -} +> & + Pick, "wait" | "instructions"> & { + readonly hook: ModelHooks + } diff --git a/packages/plugin/src/promise/adapter.ts b/packages/plugin/src/promise/adapter.ts index 4d00b44e2990..a9dc64640cf1 100644 --- a/packages/plugin/src/promise/adapter.ts +++ b/packages/plugin/src/promise/adapter.ts @@ -28,12 +28,6 @@ interface CompiledEndpoint { } const compiledEndpoints = new WeakMap() - -/** The endpoint and Effect host method behind each adapted Promise method, so tests can check where endpoints surface. */ -export const adaptedEndpoints = new WeakMap< - object, - { readonly endpoint: HttpApiEndpoint.Top; readonly method: object } ->() const JsonInput = Schema.fromJsonString(Schema.Unknown) interface HostRpcCallContext { @@ -274,7 +268,7 @@ export function fromPromise(plugin: Plugin) { method: (input: never) => Effect.Effect, ) => { const compiled = compileEndpoint(endpoint) - const adapted = (input?: unknown) => + return ((input?: unknown) => Effect.gen(function* () { // Match the generated Promise client, whose request body crosses JSON before endpoint decoding. const normalized = yield* Schema.encodeUnknownEffect(JsonInput)(input ?? {}).pipe( @@ -284,9 +278,7 @@ export function fromPromise(plugin: Plugin) { const result = yield* method(Object.assign({}, ...decoded) as never) if (compiled.noContent) return undefined return yield* compiled.encode(result) - }).pipe(Effect.runPromiseWith(runtime)) - adaptedEndpoints.set(adapted, { endpoint, method }) - return adapted as PromiseMethod + }).pipe(Effect.runPromiseWith(runtime))) as PromiseMethod } const transform = @@ -300,34 +292,6 @@ export function fromPromise(plugin: Plugin) { }), ) - const experimental: PromiseContext["experimental"] = { - generate: { - text: adaptApiMethod(GenerateEndpoints["generate.text"], host.experimental.generate.text), - }, - session: { - wait: adaptApiMethod(SessionEndpoints["session.wait"], host.experimental.session.wait), - instructions: { - entry: { - list: adaptApiMethod( - SessionEndpoints["session.instructions.entry.list"], - host.experimental.session.instructions.entry.list, - ), - put: adaptApiMethod( - SessionEndpoints["session.instructions.entry.put"], - host.experimental.session.instructions.entry.put, - ), - remove: adaptApiMethod( - SessionEndpoints["session.instructions.entry.remove"], - host.experimental.session.instructions.entry.remove, - ), - }, - }, - }, - terminal: { - read: adaptApiMethod(ExperimentalEndpoints["persistentPty.read"], host.experimental.terminal.read), - }, - } - const context2: PromiseContext = { app: host.app, location: host.location, @@ -376,9 +340,14 @@ export function fromPromise(plugin: Plugin) { options, ), }, - experimental, - // Deprecated pre-experimental path; removed in the next minor (.github/RELEASE_REMINDERS.md). - generate: experimental.generate, + experimental: { + terminal: { + read: adaptApiMethod(ExperimentalEndpoints["persistentPty.read"], host.experimental.terminal.read), + }, + }, + generate: { + text: adaptApiMethod(GenerateEndpoints["generate.text"], host.generate.text), + }, model: { list: adaptApiMethod(ModelEndpoints["model.list"], host.model.list), default: adaptApiMethod(ModelEndpoints["model.default"], host.model.default), @@ -641,10 +610,24 @@ export function fromPromise(plugin: Plugin) { interrupt: adaptApiMethod(SessionEndpoints["session.interrupt"], host.session.interrupt), update: adaptApiMethod(SessionEndpoints["session.update"], host.session.update), move: adaptApiMethod(SessionEndpoints["session.move"], host.session.move), + wait: adaptApiMethod(SessionEndpoints["session.wait"], host.session.wait), context: adaptApiMethod(SessionEndpoints["session.context"], host.session.context), - // Deprecated pre-experimental paths; removed in the next minor (.github/RELEASE_REMINDERS.md). - wait: experimental.session.wait, - instructions: experimental.session.instructions, + instructions: { + entry: { + list: adaptApiMethod( + SessionEndpoints["session.instructions.entry.list"], + host.session.instructions.entry.list, + ), + put: adaptApiMethod( + SessionEndpoints["session.instructions.entry.put"], + host.session.instructions.entry.put, + ), + remove: adaptApiMethod( + SessionEndpoints["session.instructions.entry.remove"], + host.session.instructions.entry.remove, + ), + }, + }, }, shell: { hook: (name, callback) => diff --git a/packages/plugin/src/promise/plugin.ts b/packages/plugin/src/promise/plugin.ts index 08b6fd3c8c71..e059da892550 100644 --- a/packages/plugin/src/promise/plugin.ts +++ b/packages/plugin/src/promise/plugin.ts @@ -1,10 +1,5 @@ import type { OpenCodeClient } from "@opencode/client" -import type { - CredentialApi, - ExperimentalGenerateApi, - ExperimentalSessionApi, - PluginApi, -} from "@opencode/client/promise/api" +import type { CredentialApi, ExperimentalGenerateApi, PluginApi } from "@opencode/client/promise/api" import type { Location } from "@opencode/schema/location" import type { PluginOptions } from "../options.js" import type { App } from "../app.js" @@ -38,14 +33,11 @@ export interface Context { readonly credential: Pick readonly event: EventDomain readonly experimental: { - readonly generate: ExperimentalGenerateApi - readonly session: Pick readonly terminal: Pick } readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain - /** @deprecated Use `ctx.experimental.generate`. Removed in the next minor. */ readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick diff --git a/packages/plugin/src/promise/session.ts b/packages/plugin/src/promise/session.ts index 45326b134649..48422b78fbcc 100644 --- a/packages/plugin/src/promise/session.ts +++ b/packages/plugin/src/promise/session.ts @@ -168,10 +168,7 @@ export type SessionDomain = Pick< | "update" | "move" | "context" -> & { - readonly hook: ModelHooks - /** @deprecated Use `ctx.experimental.session.wait`. Removed in the next minor. */ - readonly wait: ExperimentalSessionApi["wait"] - /** @deprecated Use `ctx.experimental.session.instructions`. Removed in the next minor. */ - readonly instructions: ExperimentalSessionApi["instructions"] -} +> & + Pick & { + readonly hook: ModelHooks + } diff --git a/services/www/src/docs/content/build/plugins/effect.mdx b/services/www/src/docs/content/build/plugins/effect.mdx index 76291e31df85..d4235180087a 100644 --- a/services/www/src/docs/content/build/plugins/effect.mdx +++ b/services/www/src/docs/content/build/plugins/effect.mdx @@ -121,13 +121,12 @@ interface Context { readonly command: CommandDomain readonly event: EventDomain readonly experimental: { - readonly generate: ExperimentalGenerateApi - readonly session: Pick, "wait" | "instructions"> readonly terminal: Pick["persistentPty"], "read"> } readonly integration: IntegrationDomain readonly mcp: MCPDomain readonly model: ModelDomain + readonly generate: ExperimentalGenerateApi readonly permission: PermissionDomain readonly plugin: Pick, "list"> readonly provider: ProviderDomain @@ -859,18 +858,17 @@ effect: (ctx) => }), ``` -Attach, list, or remove keyed session instruction entries with the experimental `ctx.experimental.session.instructions.entry`. Entries render as `` blocks in the session's instructions, and updates or removals are announced at the next step boundary without queuing a message turn. +Attach, list, or remove keyed session instruction entries with `instructions.entry`. Entries render as `` blocks in the session's instructions, and updates or removals are announced at the next step boundary without queuing a message turn. ```ts effect: (ctx) => Effect.gen(function* () { const created = yield* ctx.session.create({ title: "Review" }).pipe(Effect.orDie) - const instructions = ctx.experimental.session.instructions - yield* instructions.entry + yield* ctx.session.instructions.entry .put({ sessionID: created.id, key: "deploy-target", value: "production" }) .pipe(Effect.orDie) - const entries = yield* instructions.entry.list({ sessionID: created.id }).pipe(Effect.orDie) - yield* instructions.entry.remove({ sessionID: created.id, key: "deploy-target" }).pipe(Effect.orDie) + const entries = yield* ctx.session.instructions.entry.list({ sessionID: created.id }).pipe(Effect.orDie) + yield* ctx.session.instructions.entry.remove({ sessionID: created.id, key: "deploy-target" }).pipe(Effect.orDie) }), ``` @@ -896,15 +894,12 @@ type SessionDomain = Pick< | "interrupt" | "update" | "move" + | "wait" | "context" + | "instructions" > ``` -Endpoints under `/api/experimental` reach plugins through `ctx.experimental`: `ctx.experimental.generate`, -`ctx.experimental.session.wait`, `ctx.experimental.session.instructions` and `ctx.experimental.terminal.read`. They -may change between releases. The older `ctx.generate`, `ctx.session.wait` and `ctx.session.instructions` still work -but are deprecated and will be removed in the next minor release. - ### Skills Read skills at the current location. diff --git a/services/www/src/docs/content/build/plugins/index.mdx b/services/www/src/docs/content/build/plugins/index.mdx index bf6b5edfa2ad..87da9b2f1c2b 100644 --- a/services/www/src/docs/content/build/plugins/index.mdx +++ b/services/www/src/docs/content/build/plugins/index.mdx @@ -725,12 +725,12 @@ interface ReferenceEditor { } ``` -### Generate (experimental) +### Generate Generate text with a selected model without creating a session, invoking tools, or adding to session history. ```ts -const review = await ctx.experimental.generate.text({ +const review = await ctx.generate.text({ model: { providerID: "anthropic", id: "claude-sonnet-5-5" }, prompt: "Review this proposed action.", }) @@ -821,7 +821,7 @@ Rename, interrupt, or wait for a session. ```ts await ctx.session.update({ sessionID, title: "Review" }) await ctx.session.interrupt({ sessionID, resume: false }) -await ctx.experimental.session.wait({ sessionID }) +await ctx.session.wait({ sessionID }) ``` Remove a temporary session with `remove`. Removal stops active execution and recursively deletes child sessions. @@ -837,19 +837,14 @@ the next step boundary by default. Pass `delivery: "queue"` to preserve queue or const compaction = await ctx.session.compact({ sessionID }) ``` -Attach, list, or remove keyed session instruction entries with the experimental `ctx.experimental.session.instructions.entry`. Entries render as `` blocks in the session's instructions, and updates or removals are announced at the next step boundary without queuing a message turn. +Attach, list, or remove keyed session instruction entries with `instructions.entry`. Entries render as `` blocks in the session's instructions, and updates or removals are announced at the next step boundary without queuing a message turn. ```ts -await ctx.experimental.session.instructions.entry.put({ sessionID, key: "deploy-target", value: "production" }) -const entries = await ctx.experimental.session.instructions.entry.list({ sessionID }) -await ctx.experimental.session.instructions.entry.remove({ sessionID, key: "deploy-target" }) +await ctx.session.instructions.entry.put({ sessionID, key: "deploy-target", value: "production" }) +const entries = await ctx.session.instructions.entry.list({ sessionID }) +await ctx.session.instructions.entry.remove({ sessionID, key: "deploy-target" }) ``` -Endpoints under `/api/experimental` reach plugins through `ctx.experimental`: `ctx.experimental.generate`, -`ctx.experimental.session.wait`, `ctx.experimental.session.instructions` and `ctx.experimental.terminal.read`. They -may change between releases. The older `ctx.generate`, `ctx.session.wait` and `ctx.session.instructions` still work -but are deprecated and will be removed in the next minor release. - #### Reference Schemas: [`Session.Info`](/api#schema-Session.Info), [`Model.Ref`](/api#schema-Model.Ref), @@ -862,6 +857,16 @@ interface SessionContext { get(input: SessionGetInput, requestOptions?: RequestOptions): Promise remove(input: SessionRemoveInput, requestOptions?: RequestOptions): Promise context(input: SessionContextInput, requestOptions?: RequestOptions): Promise + instructions: { + entry: { + list( + input: ExperimentalSessionInstructionsEntryListInput, + requestOptions?: RequestOptions, + ): Promise + put(input: ExperimentalSessionInstructionsEntryPutInput, requestOptions?: RequestOptions): Promise + remove(input: ExperimentalSessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions): Promise + } + } switchAgent(input: SessionSwitchAgentInput, requestOptions?: RequestOptions): Promise switchModel(input: SessionSwitchModelInput, requestOptions?: RequestOptions): Promise update(input: SessionUpdateInput, requestOptions?: RequestOptions): Promise @@ -872,21 +877,7 @@ interface SessionContext { compact(input: SessionCompactInput, requestOptions?: RequestOptions): Promise synthetic(input: SessionSyntheticInput, requestOptions?: RequestOptions): Promise interrupt(input: SessionInterruptInput, requestOptions?: RequestOptions): Promise<{ interrupted: boolean }> -} - -// ctx.experimental.session -interface ExperimentalSessionContext { wait(input: ExperimentalSessionWaitInput, requestOptions?: RequestOptions): Promise - instructions: { - entry: { - list( - input: ExperimentalSessionInstructionsEntryListInput, - requestOptions?: RequestOptions, - ): Promise - put(input: ExperimentalSessionInstructionsEntryPutInput, requestOptions?: RequestOptions): Promise - remove(input: ExperimentalSessionInstructionsEntryRemoveInput, requestOptions?: RequestOptions): Promise - } - } } ``` @@ -1703,7 +1694,7 @@ await ctx.permission.hook("evaluate", async (event) => { if (event.action === "read") return const messages = await ctx.session.context({ sessionID: event.sessionID }) - const review = await ctx.experimental.generate.text({ + const review = await ctx.generate.text({ model: { providerID: "anthropic", id: "claude-sonnet-5-5" }, prompt: buildSafetyPrompt({ messages, action: event.action, resources: event.resources }), }) From 10d53e32d151fc17b4153ba883430039f5df4067 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 28 Oct 2026 12:09:37 -0700 Subject: [PATCH 11/11] chore: drop release reminders doc --- .github/RELEASE_REMINDERS.md | 14 -------------- AGENTS.md | 6 +----- 2 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 .github/RELEASE_REMINDERS.md diff --git a/.github/RELEASE_REMINDERS.md b/.github/RELEASE_REMINDERS.md deleted file mode 100644 index bcb3c7c9ef2c..000000000000 --- a/.github/RELEASE_REMINDERS.md +++ /dev/null @@ -1,14 +0,0 @@ -# Release reminders - -Read this before cutting a minor or major release, act on every item in its section, and delete each item once it ships. Add an item whenever a change leaves something to finish in a later release, such as a deprecated alias to remove. - -## Remove at next minor - -- [ ] The deprecated pre-experimental SDK aliases from [#59072](https://github.com/anomalyco/opencode/pull/59072). Each old path is now `client.experimental.`, and each old type name is now `Experimental`: - - Methods: `client.session.{stats,import,export,skill,wait,log}`, `client.session.instructions.entry.{list,put,remove}`, `client.generate.text`, `client.voice.{models,session}`, `client.migration.v1.status`, `client.usage.limits`, `client.integration.wellknown.add`, `client.mcp.{add,remove,connect,disconnect}`, `client.mcp.client.token`, `client.file.{watch,unwatch}`, `client.config.update`, and `client.file.write` (Promise client only). - - Types: the Input, Output and (Effect) Operation types of those methods, such as `SessionWaitInput`, plus the Effect `GenerateApi`, `VoiceApi`, `MigrationApi` and `UsageApi`, and the Promise `GenerateApi` in `packages/client/src/promise/api.ts`. - - To remove: drop `aliases: true` from both `compile` calls in `packages/client/script/generate.ts` and run `bun run generate` in `packages/client`; delete `GenerateApi` from `packages/client/src/promise/api.ts`; delete `packages/client/test/deprecated-aliases.test.ts` and the alias entries in `packages/client/test/promise.test.ts`; drop the `Omit` in `packages/plugin/src/{effect,promise}/integration.ts`. - -## Remove at next major - -No items yet. diff --git a/AGENTS.md b/AGENTS.md index 608eba7873ca..ab024b3fa9b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,13 +171,9 @@ const table = sqliteTable("session", { - New endpoints and new default behaviors start experimental. - To graduate one, move the route out of `/api/experimental`, drop the `experimental.` prefix, and note it in `CHANGELOG.md`. - Experimental HTTP routes change without aliases: rename or remove the route, then update the callers. -- A breaking move of shipped SDK or plugin API (including moving it under `experimental`) keeps the old path and type names as deprecated aliases of the new ones until the next minor, each marked `@deprecated` with the replacement and "Removed in the next minor". Generated SDK moves get them from the codegen (`compile(..., { aliases: true })` in `packages/client/script/generate.ts`), not by hand. Add the removal to `.github/RELEASE_REMINDERS.md`. +- A breaking move of shipped SDK or plugin API (including moving it under `experimental`) keeps the old path and type names as deprecated aliases of the new ones until the next minor, each marked `@deprecated` with the replacement and "Removed in the next minor". Generated SDK moves get them from the codegen (`compile(..., { aliases: true })` in `packages/client/script/generate.ts`), not by hand. - Experimental plugin hooks are named `experimental.*` (`experimental.ws.handshake`). -## Releases - -- Before cutting a minor or major release, read `.github/RELEASE_REMINDERS.md` and act on its section for that release. - ## Testing - Avoid mocks as much as possible, you shouldn't be using globalThis.\* at all unless it's the only option.