diff --git a/packages/client/src/effect/api/api.ts b/packages/client/src/effect/api/api.ts index c77bf4e831..ca78d62ead 100644 --- a/packages/client/src/effect/api/api.ts +++ b/packages/client/src/effect/api/api.ts @@ -820,55 +820,71 @@ export interface EventApi { readonly subscribe: EventSubscribeOperation } -type Endpoint20_0Request = Parameters[0] +type Endpoint20_0Request = Parameters[0] export type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } -export type Endpoint20_0Output = EffectValue> -export type PtyListOperation = (input?: Endpoint20_0Input) => Effect.Effect +export type Endpoint20_0Output = EffectValue> +export type PtyShellsOperation = (input?: Endpoint20_0Input) => Effect.Effect -type Endpoint20_1Request = Parameters[0] -export type Endpoint20_1Input = { - readonly location?: Endpoint20_1Request["query"]["location"] - readonly command?: Endpoint20_1Request["payload"]["command"] - readonly args?: Endpoint20_1Request["payload"]["args"] - readonly cwd?: Endpoint20_1Request["payload"]["cwd"] - readonly title?: Endpoint20_1Request["payload"]["title"] - readonly env?: Endpoint20_1Request["payload"]["env"] -} -export type Endpoint20_1Output = EffectValue> -export type PtyCreateOperation = (input?: Endpoint20_1Input) => Effect.Effect +type Endpoint20_1Request = Parameters[0] +export type Endpoint20_1Input = { readonly location?: Endpoint20_1Request["query"]["location"] } +export type Endpoint20_1Output = EffectValue> +export type PtyListOperation = (input?: Endpoint20_1Input) => Effect.Effect -type Endpoint20_2Request = Parameters[0] +type Endpoint20_2Request = Parameters[0] export type Endpoint20_2Input = { - readonly ptyID: Endpoint20_2Request["params"]["ptyID"] readonly location?: Endpoint20_2Request["query"]["location"] + readonly command?: Endpoint20_2Request["payload"]["command"] + readonly args?: Endpoint20_2Request["payload"]["args"] + readonly cwd?: Endpoint20_2Request["payload"]["cwd"] + readonly title?: Endpoint20_2Request["payload"]["title"] + readonly env?: Endpoint20_2Request["payload"]["env"] } -export type Endpoint20_2Output = EffectValue> -export type PtyGetOperation = (input: Endpoint20_2Input) => Effect.Effect +export type Endpoint20_2Output = EffectValue> +export type PtyCreateOperation = (input?: Endpoint20_2Input) => Effect.Effect -type Endpoint20_3Request = Parameters[0] +type Endpoint20_3Request = Parameters[0] export type Endpoint20_3Input = { readonly ptyID: Endpoint20_3Request["params"]["ptyID"] readonly location?: Endpoint20_3Request["query"]["location"] - readonly title?: Endpoint20_3Request["payload"]["title"] - readonly size?: Endpoint20_3Request["payload"]["size"] } -export type Endpoint20_3Output = EffectValue> -export type PtyUpdateOperation = (input: Endpoint20_3Input) => Effect.Effect +export type Endpoint20_3Output = EffectValue> +export type PtyGetOperation = (input: Endpoint20_3Input) => Effect.Effect -type Endpoint20_4Request = Parameters[0] +type Endpoint20_4Request = Parameters[0] export type Endpoint20_4Input = { readonly ptyID: Endpoint20_4Request["params"]["ptyID"] readonly location?: Endpoint20_4Request["query"]["location"] + readonly title?: Endpoint20_4Request["payload"]["title"] + readonly size?: Endpoint20_4Request["payload"]["size"] } -export type Endpoint20_4Output = EffectValue> -export type PtyRemoveOperation = (input: Endpoint20_4Input) => Effect.Effect +export type Endpoint20_4Output = EffectValue> +export type PtyUpdateOperation = (input: Endpoint20_4Input) => Effect.Effect + +type Endpoint20_5Request = Parameters[0] +export type Endpoint20_5Input = { + readonly ptyID: Endpoint20_5Request["params"]["ptyID"] + readonly location?: Endpoint20_5Request["query"]["location"] +} +export type Endpoint20_5Output = EffectValue> +export type PtyRemoveOperation = (input: Endpoint20_5Input) => Effect.Effect + +type Endpoint20_6Request = Parameters[0] +export type Endpoint20_6Input = { + readonly ptyID: Endpoint20_6Request["params"]["ptyID"] + readonly location?: Endpoint20_6Request["query"]["location"] + readonly "x-opencode-ticket"?: Endpoint20_6Request["headers"]["x-opencode-ticket"] +} +export type Endpoint20_6Output = EffectValue> +export type PtyConnectTokenOperation = (input: Endpoint20_6Input) => Effect.Effect export interface PtyApi { + readonly shells: PtyShellsOperation readonly list: PtyListOperation readonly create: PtyCreateOperation readonly get: PtyGetOperation readonly update: PtyUpdateOperation readonly remove: PtyRemoveOperation + readonly connectToken: PtyConnectTokenOperation } type Endpoint21_0Request = Parameters[0] diff --git a/packages/client/src/effect/generated/client.ts b/packages/client/src/effect/generated/client.ts index b2f983733d..01e2791840 100644 --- a/packages/client/src/effect/generated/client.ts +++ b/packages/client/src/effect/generated/client.ts @@ -977,21 +977,26 @@ const Endpoint19_0 = (raw: RawClient["server.event"]) => () => const adaptGroup19 = (raw: RawClient["server.event"]) => ({ subscribe: Endpoint19_0(raw) }) -type Endpoint20_0Request = Parameters[0] +type Endpoint20_0Request = Parameters[0] type Endpoint20_0Input = { readonly location?: Endpoint20_0Request["query"]["location"] } const Endpoint20_0 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_0Input) => + raw["pty.shells"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) + +type Endpoint20_1Request = Parameters[0] +type Endpoint20_1Input = { readonly location?: Endpoint20_1Request["query"]["location"] } +const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) => raw["pty.list"]({ query: { location: input?.["location"] } }).pipe(Effect.mapError(mapClientError)) -type Endpoint20_1Request = Parameters[0] -type Endpoint20_1Input = { - readonly location?: Endpoint20_1Request["query"]["location"] - readonly command?: Endpoint20_1Request["payload"]["command"] - readonly args?: Endpoint20_1Request["payload"]["args"] - readonly cwd?: Endpoint20_1Request["payload"]["cwd"] - readonly title?: Endpoint20_1Request["payload"]["title"] - readonly env?: Endpoint20_1Request["payload"]["env"] +type Endpoint20_2Request = Parameters[0] +type Endpoint20_2Input = { + readonly location?: Endpoint20_2Request["query"]["location"] + readonly command?: Endpoint20_2Request["payload"]["command"] + readonly args?: Endpoint20_2Request["payload"]["args"] + readonly cwd?: Endpoint20_2Request["payload"]["cwd"] + readonly title?: Endpoint20_2Request["payload"]["title"] + readonly env?: Endpoint20_2Request["payload"]["env"] } -const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Input) => +const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_2Input) => raw["pty.create"]({ query: { location: input?.["location"] }, payload: { @@ -1003,46 +1008,61 @@ const Endpoint20_1 = (raw: RawClient["server.pty"]) => (input?: Endpoint20_1Inpu }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint20_2Request = Parameters[0] -type Endpoint20_2Input = { - readonly ptyID: Endpoint20_2Request["params"]["ptyID"] - readonly location?: Endpoint20_2Request["query"]["location"] +type Endpoint20_3Request = Parameters[0] +type Endpoint20_3Input = { + readonly ptyID: Endpoint20_3Request["params"]["ptyID"] + readonly location?: Endpoint20_3Request["query"]["location"] } -const Endpoint20_2 = (raw: RawClient["server.pty"]) => (input: Endpoint20_2Input) => +const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) => raw["pty.get"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) -type Endpoint20_3Request = Parameters[0] -type Endpoint20_3Input = { - readonly ptyID: Endpoint20_3Request["params"]["ptyID"] - readonly location?: Endpoint20_3Request["query"]["location"] - readonly title?: Endpoint20_3Request["payload"]["title"] - readonly size?: Endpoint20_3Request["payload"]["size"] +type Endpoint20_4Request = Parameters[0] +type Endpoint20_4Input = { + readonly ptyID: Endpoint20_4Request["params"]["ptyID"] + readonly location?: Endpoint20_4Request["query"]["location"] + readonly title?: Endpoint20_4Request["payload"]["title"] + readonly size?: Endpoint20_4Request["payload"]["size"] } -const Endpoint20_3 = (raw: RawClient["server.pty"]) => (input: Endpoint20_3Input) => +const Endpoint20_4 = (raw: RawClient["server.pty"]) => (input: Endpoint20_4Input) => raw["pty.update"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] }, payload: { title: input["title"], size: input["size"] }, }).pipe(Effect.mapError(mapClientError)) -type Endpoint20_4Request = Parameters[0] -type Endpoint20_4Input = { - readonly ptyID: Endpoint20_4Request["params"]["ptyID"] - readonly location?: Endpoint20_4Request["query"]["location"] +type Endpoint20_5Request = Parameters[0] +type Endpoint20_5Input = { + readonly ptyID: Endpoint20_5Request["params"]["ptyID"] + readonly location?: Endpoint20_5Request["query"]["location"] } -const Endpoint20_4 = (raw: RawClient["server.pty"]) => (input: Endpoint20_4Input) => +const Endpoint20_5 = (raw: RawClient["server.pty"]) => (input: Endpoint20_5Input) => raw["pty.remove"]({ params: { ptyID: input["ptyID"] }, query: { location: input["location"] } }).pipe( Effect.mapError(mapClientError), ) +type Endpoint20_6Request = Parameters[0] +type Endpoint20_6Input = { + readonly ptyID: Endpoint20_6Request["params"]["ptyID"] + readonly location?: Endpoint20_6Request["query"]["location"] + readonly "x-opencode-ticket"?: Endpoint20_6Request["headers"]["x-opencode-ticket"] +} +const Endpoint20_6 = (raw: RawClient["server.pty"]) => (input: Endpoint20_6Input) => + raw["pty.connectToken"]({ + params: { ptyID: input["ptyID"] }, + query: { location: input["location"] }, + headers: { "x-opencode-ticket": input["x-opencode-ticket"] }, + }).pipe(Effect.mapError(mapClientError)) + const adaptGroup20 = (raw: RawClient["server.pty"]) => ({ - list: Endpoint20_0(raw), - create: Endpoint20_1(raw), - get: Endpoint20_2(raw), - update: Endpoint20_3(raw), - remove: Endpoint20_4(raw), + shells: Endpoint20_0(raw), + list: Endpoint20_1(raw), + create: Endpoint20_2(raw), + get: Endpoint20_3(raw), + update: Endpoint20_4(raw), + remove: Endpoint20_5(raw), + connectToken: Endpoint20_6(raw), }) type Endpoint21_0Request = Parameters[0] diff --git a/packages/client/src/promise/generated/client.ts b/packages/client/src/promise/generated/client.ts index 25f4008336..36915d0186 100644 --- a/packages/client/src/promise/generated/client.ts +++ b/packages/client/src/promise/generated/client.ts @@ -162,6 +162,8 @@ import type { SkillListInput, SkillListOutput, EventSubscribeOutput, + PtyShellsInput, + PtyShellsOutput, PtyListInput, PtyListOutput, PtyCreateInput, @@ -172,6 +174,8 @@ import type { PtyUpdateOutput, PtyRemoveInput, PtyRemoveOutput, + PtyConnectTokenInput, + PtyConnectTokenOutput, ShellListInput, ShellListOutput, ShellCreateInput, @@ -1430,6 +1434,18 @@ export function make(options: ClientOptions) { ), }, pty: { + shells: (input?: PtyShellsInput, requestOptions?: RequestOptions) => + request( + { + method: "GET", + path: `/api/pty/shells`, + query: { location: input?.["location"] }, + successStatus: 200, + declaredStatuses: [401, 400], + empty: false, + }, + requestOptions, + ), list: (input?: PtyListInput, requestOptions?: RequestOptions) => request( { @@ -1498,6 +1514,19 @@ export function make(options: ClientOptions) { }, requestOptions, ), + connectToken: (input: PtyConnectTokenInput, requestOptions?: RequestOptions) => + request( + { + method: "POST", + path: `/api/pty/${encodeURIComponent(input.ptyID)}/connect-token`, + query: { location: input["location"] }, + headers: { "x-opencode-ticket": input["x-opencode-ticket"] }, + successStatus: 200, + declaredStatuses: [403, 404, 401, 400], + empty: false, + }, + requestOptions, + ), }, shell: { list: (input?: ShellListInput, requestOptions?: RequestOptions) => diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 631efb57f2..b55c5e9a08 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -501,6 +501,10 @@ export type QuestionTool = { messageID: string; callID: string } export type QuestionAnswer = Array +export type PtyShell = { path: string; name: string; acceptable: boolean } + +export type PtyTicketConnectToken = { ticket: string; expires_in: number } + export type ShellInfo1 = { id: string status: "running" | "exited" | "timeout" | "killed" @@ -2530,6 +2534,10 @@ export type PtyNotFoundError = { readonly _tag: "PtyNotFoundError"; readonly pty export const isPtyNotFoundError = (value: unknown): value is PtyNotFoundError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "PtyNotFoundError" +export type ForbiddenError = { readonly _tag: "ForbiddenError"; readonly message: string } +export const isForbiddenError = (value: unknown): value is ForbiddenError => + typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ForbiddenError" + export type ShellNotFoundError = { readonly _tag: "ShellNotFoundError"; readonly id: string; readonly message: string } export const isShellNotFoundError = (value: unknown): value is ShellNotFoundError => typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "ShellNotFoundError" @@ -4649,6 +4657,17 @@ export type SkillListOutput = { export type EventSubscribeOutput = V2Event +export type PtyShellsInput = { + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] +} + +export type PtyShellsOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: Array +} + export type PtyListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined @@ -4744,6 +4763,19 @@ export type PtyRemoveInput = { export type PtyRemoveOutput = void +export type PtyConnectTokenInput = { + readonly ptyID: { readonly ptyID: string }["ptyID"] + readonly location?: { + readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined + }["location"] + readonly "x-opencode-ticket"?: { readonly "x-opencode-ticket"?: string | undefined }["x-opencode-ticket"] +} + +export type PtyConnectTokenOutput = { + location: { directory: string; workspaceID?: string; project: { id: string; directory: string } } + data: PtyTicketConnectToken +} + export type ShellListInput = { readonly location?: { readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined diff --git a/packages/client/test/promise.test.ts b/packages/client/test/promise.test.ts index 6716829119..5845465636 100644 --- a/packages/client/test/promise.test.ts +++ b/packages/client/test/promise.test.ts @@ -44,11 +44,38 @@ test("exposes every standard HTTP API group", () => { expect(Object.keys(client.integration.command)).toEqual(["connect", "status", "cancel"]) expect(Object.keys(client.file)).toEqual(["read", "list", "find"]) expect(Object.keys(client.vcs)).toEqual(["status", "diff"]) - expect(Object.keys(client.pty)).toEqual(["list", "create", "get", "update", "remove"]) + expect(Object.keys(client.pty)).toEqual(["shells", "list", "create", "get", "update", "remove", "connectToken"]) expect(Object.keys(client.shell)).toEqual(["list", "create", "get", "timeout", "output", "remove"]) expect(Object.keys(client.project)).toEqual(["list", "current", "directories"]) }) +test("PTY connect token sends the required browser header", async () => { + let request: Request | undefined + const client = OpenCode.make({ + baseUrl: "http://localhost:3000", + fetch: async (input, init) => { + request = input instanceof Request ? input : new Request(input, init) + return Response.json({ + location: { directory: "/tmp/project", project: { id: "proj_test", directory: "/tmp/project" } }, + data: { ticket: "ticket", expires_in: 60 }, + }) + }, + }) + + const result = await client.pty.connectToken({ + ptyID: "pty_test", + location: { directory: "/tmp/project" }, + "x-opencode-ticket": "1", + }) + + expect(result.data.ticket).toBe("ticket") + expect(request?.method).toBe("POST") + expect(request?.headers.get("x-opencode-ticket")).toBe("1") + expect(request?.url).toBe( + "http://localhost:3000/api/pty/pty_test/connect-token?location%5Bdirectory%5D=%2Ftmp%2Fproject", + ) +}) + test("server.get uses the public HTTP contract", async () => { let request: Request | undefined const client = OpenCode.make({