mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
refactor(protocol): refine audited endpoints
This commit is contained in:
+18
-18
@@ -120,17 +120,17 @@ Review endpoints in document order. For each endpoint, select one disposition an
|
||||
| [ ] 033 | `POST` | `/api/integration/{integrationID}/connect/command` | `integration.command.connect` | | |
|
||||
| [ ] 034 | `GET` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.status` | | |
|
||||
| [ ] 035 | `DELETE` | `/api/integration/{integrationID}/connect/command/{attemptID}` | `integration.command.cancel` | | |
|
||||
| [ ] 036 | `GET` | `/api/mcp` | `mcp.list` | | |
|
||||
| [ ] 037 | `PUT` | `/api/mcp/{server}` | `mcp.add` | | |
|
||||
| [ ] 038 | `DELETE` | `/api/mcp/{server}` | `mcp.remove` | | |
|
||||
| [ ] 039 | `POST` | `/api/mcp/{server}/connect` | `mcp.connect` | | |
|
||||
| [ ] 040 | `POST` | `/api/mcp/{server}/disconnect` | `mcp.disconnect` | | |
|
||||
| [ ] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | | |
|
||||
| [ ] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | | |
|
||||
| [ ] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | | |
|
||||
| [ ] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | | |
|
||||
| [ ] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | | |
|
||||
| [ ] 046 | `POST` | `/api/websearch` | `websearch.query` | | |
|
||||
| [x] 036 | `GET` | `/api/mcp` | `mcp.list` | Keep | MCP inventory and connection status retained. |
|
||||
| [x] 037 | `PUT` | `/api/experimental/mcp/{server}` | `experimental.mcp.add` | Experimental-only | Runtime-only MCP override; does not persist configuration. |
|
||||
| [x] 038 | `DELETE` | `/api/experimental/mcp/{server}` | `experimental.mcp.remove` | Experimental-only | Runtime removal override; missing server returns `404`. |
|
||||
| [x] 039 | `POST` | `/api/experimental/mcp/{server}/connect` | `experimental.mcp.connect` | Experimental-only | Runtime connection override retained outside the stable API. |
|
||||
| [x] 040 | `POST` | `/api/experimental/mcp/{server}/disconnect` | `experimental.mcp.disconnect` | Experimental-only | Runtime disconnection override retained outside the stable API. |
|
||||
| [ ] 041 | `GET` | `/api/mcp/resource` | `mcp.resource.catalog` | | Deferred for later review. |
|
||||
| [x] 042 | `PATCH` | `/api/credential/{credentialID}` | `credential.update` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 043 | `DELETE` | `/api/credential/{credentialID}` | `credential.remove` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 044 | `POST` | `/api/credential/{credentialID}/activate` | `credential.activate` | Change | Removed redundant location query; credentials and events are global. |
|
||||
| [x] 045 | `GET` | `/api/websearch/provider` | `websearch.providers` | Keep | Provider availability remains location-scoped; singular resource path retained. |
|
||||
| [x] 046 | `POST` | `/api/websearch` | `websearch.query` | Keep | Unknown provider remains an invalid request; published time documented as Unix epoch milliseconds. |
|
||||
|
||||
## Group 4: Session lifecycle
|
||||
|
||||
@@ -138,13 +138,13 @@ Review endpoints in document order. For each endpoint, select one disposition an
|
||||
|
||||
| Done | Method | Path | Operation ID | Decision | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| [ ] 047 | `GET` | `/api/session` | `session.list` | | |
|
||||
| [ ] 048 | `POST` | `/api/session` | `session.create` | | |
|
||||
| [ ] 049 | `GET` | `/api/session/stats` | `session.stats` | | |
|
||||
| [ ] 050 | `GET` | `/api/session/active` | `session.active` | | |
|
||||
| [ ] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | | |
|
||||
| [ ] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | | |
|
||||
| [ ] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | | |
|
||||
| [x] 047 | `GET` | `/api/session` | `session.list` | Keep | Existing filtering, ordering, and cursor contract retained for now. |
|
||||
| [x] 048 | `POST` | `/api/session` | `session.create` | Keep | Existing creation contract retained; model reference includes optional variant. |
|
||||
| [x] 049 | `GET` | `/api/experimental/session/stats` | `experimental.session.stats` | Experimental-only | Session analytics retained outside the stable API commitment. |
|
||||
| [x] 050 | `GET` | `/api/session/active` | `session.active` | Keep | Status record retained for future active-state expansion. |
|
||||
| [x] 051 | `GET` | `/api/session/{sessionID}` | `session.get` | Keep | Specific session read and typed `404` retained. |
|
||||
| [x] 052 | `DELETE` | `/api/session/{sessionID}` | `session.remove` | Keep | Session and child deletion with typed `404` retained. |
|
||||
| [x] 053 | `POST` | `/api/session/{sessionID}/fork` | `session.fork` | Change | Request now accepts optional branded `before` message ID; omission copies full history. |
|
||||
| [ ] 054 | `POST` | `/api/session/{sessionID}/agent` | `session.switchAgent` | | |
|
||||
| [ ] 055 | `POST` | `/api/session/{sessionID}/model` | `session.switchModel` | | |
|
||||
| [ ] 056 | `POST` | `/api/session/{sessionID}/rename` | `session.rename` | | |
|
||||
|
||||
@@ -35,11 +35,11 @@ for (const shared of [true, false]) {
|
||||
const url = new URL(route.request().url())
|
||||
const target = url.searchParams.get("location[directory]") ?? directory
|
||||
requests.push({ path: url.pathname, directory: target })
|
||||
if (url.pathname === "/api/mcp/figma-desktop/connect") {
|
||||
if (url.pathname === "/api/experimental/mcp/figma-desktop/connect") {
|
||||
connected.add(target)
|
||||
return route.fulfill({ status: 204 })
|
||||
}
|
||||
if (url.pathname === "/api/mcp/figma-desktop/disconnect") {
|
||||
if (url.pathname === "/api/experimental/mcp/figma-desktop/disconnect") {
|
||||
connected.delete(target)
|
||||
return route.fulfill({ status: 204 })
|
||||
}
|
||||
@@ -72,7 +72,7 @@ for (const shared of [true, false]) {
|
||||
await expect(toggle).toBeChecked()
|
||||
await expect(toggle).toBeEnabled()
|
||||
expect(connected).toEqual(new Set([workspace]))
|
||||
expect(requests).toContainEqual({ path: "/api/mcp/figma-desktop/connect", directory: workspace })
|
||||
expect(requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace })
|
||||
expect(requests).toContainEqual({ path: "/api/mcp/resource", directory: workspace })
|
||||
expect(requests.every((request) => request.directory === workspace)).toBe(true)
|
||||
await testInfo.attach("workspace-connected", { body: await page.screenshot(), contentType: "image/png" })
|
||||
@@ -82,7 +82,7 @@ for (const shared of [true, false]) {
|
||||
await expect(toggle).not.toBeChecked()
|
||||
await expect(toggle).toBeEnabled()
|
||||
expect(connected.size).toBe(0)
|
||||
expect(requests).toContainEqual({ path: "/api/mcp/figma-desktop/disconnect", directory: workspace })
|
||||
expect(requests).toContainEqual({ path: "/api/experimental/mcp/figma-desktop/disconnect", directory: workspace })
|
||||
expect(requests.every((request) => request.directory === workspace)).toBe(true)
|
||||
})
|
||||
}
|
||||
@@ -114,11 +114,11 @@ for (const surface of ["popover", "dialog"] as const) {
|
||||
const url = new URL(route.request().url())
|
||||
const target = url.searchParams.get("location[directory]") ?? directory
|
||||
requests.push({ path: url.pathname, directory: target })
|
||||
if (url.pathname === "/api/mcp/figma-desktop/disconnect") {
|
||||
if (url.pathname === "/api/experimental/mcp/figma-desktop/disconnect") {
|
||||
state.status = "disabled"
|
||||
return route.fulfill({ status: 204 })
|
||||
}
|
||||
if (url.pathname === "/api/mcp/figma-desktop/connect") {
|
||||
if (url.pathname === "/api/experimental/mcp/figma-desktop/connect") {
|
||||
state.status = state.fail ? "failed" : "connected"
|
||||
// Connection failures are reported by the refreshed status, not the HTTP response.
|
||||
return route.fulfill({ status: 204 })
|
||||
@@ -167,7 +167,7 @@ for (const surface of ["popover", "dialog"] as const) {
|
||||
await expect(toggle).toBeChecked({ checked: surface === "popover" })
|
||||
await expect(toggle).toBeEnabled()
|
||||
expect(requests.filter((request) => request.path.endsWith("/connect"))).toEqual([
|
||||
{ path: "/api/mcp/figma-desktop/connect", directory: workspace },
|
||||
{ path: "/api/experimental/mcp/figma-desktop/connect", directory: workspace },
|
||||
])
|
||||
expect(requests.every((request) => request.directory === workspace)).toBe(true)
|
||||
await expect(toast).toHaveCSS("opacity", "1")
|
||||
|
||||
@@ -62,7 +62,7 @@ test("every MCP row hit area toggles exactly once and keeps the submenu open", a
|
||||
await expect(submenu).toBeVisible()
|
||||
if (target === "keyboard") await expect(toggle).toBeFocused()
|
||||
expect(writes).toHaveLength(index + 1)
|
||||
expect(writes[index]).toBe(`/api/mcp/figma/${enabled ? "connect" : "disconnect"}`)
|
||||
expect(writes[index]).toBe(`/api/experimental/mcp/figma/${enabled ? "connect" : "disconnect"}`)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export const DialogFork: Component = () => {
|
||||
const dir = base64Encode(location().directory)
|
||||
|
||||
serverSDK.api.session
|
||||
.fork({ sessionID, boundary: { type: "before", messageID: item.id } })
|
||||
.fork({ sessionID, before: item.id })
|
||||
.then((forked) => {
|
||||
data.session.remember(forked)
|
||||
dialog.close()
|
||||
|
||||
@@ -105,7 +105,7 @@ export const SettingsProviders: Component<{
|
||||
const credentials = integration.data?.connections.filter((item) => item.type === "credential") ?? []
|
||||
if (credentials.length === 0) throw new Error(`No removable credentials found for ${name}`)
|
||||
await Promise.all(
|
||||
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id, location })),
|
||||
credentials.map((credential) => serverSdk.api.credential.remove({ credentialID: credential.id })),
|
||||
)
|
||||
showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -263,7 +263,6 @@ export function make(input: { readonly client: OpenCodeClient; readonly connecti
|
||||
forkSession: async (params) => {
|
||||
const forked = await input.client.session.fork({
|
||||
sessionID: params.sessionId,
|
||||
boundary: { type: "through" },
|
||||
})
|
||||
const state = await attach(forked, forked.location.directory, params.mcpServers ?? [])
|
||||
await replay(state)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { handlePromptErrors, requireInteractive } from "../../../ui/prompt"
|
||||
import { createClient, loadIntegrations, location, request } from "./shared"
|
||||
import { createClient, loadIntegrations, request } from "./shared"
|
||||
import { chooseCredential, chooseIntegration } from "./account"
|
||||
|
||||
export default Runtime.handler(
|
||||
@@ -35,7 +35,7 @@ const logout = Effect.fn("cli.auth.logout.run")(function* (input: {
|
||||
const credentialID = yield* chooseCredential(integration, "log out", input.credential)
|
||||
const progress = spinner()
|
||||
progress.start("Removing credential...")
|
||||
yield* request((signal) => client.credential.remove({ credentialID, location }, { signal })).pipe(
|
||||
yield* request((signal) => client.credential.remove({ credentialID }, { signal })).pipe(
|
||||
Effect.tap(() => Effect.sync(() => progress.stop(`Removed account from ${integration.name}`))),
|
||||
Effect.tapCause(() => Effect.sync(() => progress.stop("Failed to remove credential", 1))),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Effect, Option } from "effect"
|
||||
import { Commands } from "../../commands"
|
||||
import { Runtime } from "../../../framework/runtime"
|
||||
import { handlePromptErrors, requireInteractive } from "../../../ui/prompt"
|
||||
import { createClient, loadIntegrations, location, request } from "./shared"
|
||||
import { createClient, loadIntegrations, request } from "./shared"
|
||||
import { chooseCredential, chooseIntegration } from "./account"
|
||||
|
||||
export default Runtime.handler(
|
||||
@@ -35,7 +35,7 @@ const switchAccount = Effect.fn("cli.auth.switch.run")(function* (input: {
|
||||
const credentialID = yield* chooseCredential(integration, "switch to", input.credential)
|
||||
const progress = spinner()
|
||||
progress.start("Switching account...")
|
||||
yield* request((signal) => client.credential.activate({ credentialID, location }, { signal })).pipe(
|
||||
yield* request((signal) => client.credential.activate({ credentialID }, { signal })).pipe(
|
||||
Effect.tap(() => Effect.sync(() => progress.stop(`Switched account for ${integration.name}`))),
|
||||
Effect.tapCause(() => Effect.sync(() => progress.stop("Failed to switch account", 1))),
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ export default Runtime.handler(
|
||||
|
||||
yield* Effect.forEach(
|
||||
credentials,
|
||||
(connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id, location })),
|
||||
(connection) => Effect.promise(() => client.credential.remove({ credentialID: connection.id })),
|
||||
{ discard: true },
|
||||
)
|
||||
process.stdout.write(`Removed OAuth credentials for ${input.name}` + EOL)
|
||||
|
||||
@@ -112,7 +112,7 @@ async function selectSession(input: {
|
||||
return {
|
||||
session: input.fork
|
||||
? await input.client.session
|
||||
.fork({ sessionID: explicit.id, boundary: { type: "through" } }, ...requestOptions(input.signal))
|
||||
.fork({ sessionID: explicit.id }, ...requestOptions(input.signal))
|
||||
.catch((error) => {
|
||||
throw new SessionTargetMutationError(error)
|
||||
})
|
||||
@@ -126,7 +126,7 @@ async function selectSession(input: {
|
||||
return {
|
||||
session: input.fork
|
||||
? await input.client.session
|
||||
.fork({ sessionID: selected.id, boundary: { type: "through" } }, ...requestOptions(input.signal))
|
||||
.fork({ sessionID: selected.id }, ...requestOptions(input.signal))
|
||||
.catch((error) => {
|
||||
throw new SessionTargetMutationError(error)
|
||||
})
|
||||
|
||||
@@ -168,7 +168,7 @@ describe("acp service lifecycle", () => {
|
||||
method: "POST",
|
||||
path: "/api/session/ses_loaded/fork",
|
||||
query: {},
|
||||
body: { boundary: { type: "through" } },
|
||||
body: {},
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ export type SessionRemoveInput = { readonly sessionID: Session.ID }
|
||||
export type SessionRemoveOutput = void
|
||||
export type SessionRemoveOperation<E = never> = (input: SessionRemoveInput) => Effect.Effect<SessionRemoveOutput, E>
|
||||
|
||||
export type SessionForkInput = { readonly sessionID: Session.ID; readonly boundary: Session.ForkRequestBoundary }
|
||||
export type SessionForkInput = { readonly sessionID: Session.ID; readonly before?: SessionMessage.ID | undefined }
|
||||
export type SessionForkOutput = Session.Info
|
||||
export type SessionForkOperation<E = never> = (input: SessionForkInput) => Effect.Effect<SessionForkOutput, E>
|
||||
|
||||
@@ -1641,29 +1641,19 @@ export interface McpApi<E = never> {
|
||||
readonly resource: { readonly catalog: McpResourceCatalogOperation<E> }
|
||||
}
|
||||
|
||||
export type CredentialUpdateInput = {
|
||||
readonly credentialID: Credential.ID
|
||||
readonly location?: { readonly directory?: string | undefined } | undefined
|
||||
readonly label: string
|
||||
}
|
||||
export type CredentialUpdateInput = { readonly credentialID: Credential.ID; readonly label: string }
|
||||
export type CredentialUpdateOutput = void
|
||||
export type CredentialUpdateOperation<E = never> = (
|
||||
input: CredentialUpdateInput,
|
||||
) => Effect.Effect<CredentialUpdateOutput, E>
|
||||
|
||||
export type CredentialActivateInput = {
|
||||
readonly credentialID: Credential.ID
|
||||
readonly location?: { readonly directory?: string | undefined } | undefined
|
||||
}
|
||||
export type CredentialActivateInput = { readonly credentialID: Credential.ID }
|
||||
export type CredentialActivateOutput = void
|
||||
export type CredentialActivateOperation<E = never> = (
|
||||
input: CredentialActivateInput,
|
||||
) => Effect.Effect<CredentialActivateOutput, E>
|
||||
|
||||
export type CredentialRemoveInput = {
|
||||
readonly credentialID: Credential.ID
|
||||
readonly location?: { readonly directory?: string | undefined } | undefined
|
||||
}
|
||||
export type CredentialRemoveInput = { readonly credentialID: Credential.ID }
|
||||
export type CredentialRemoveOutput = void
|
||||
export type CredentialRemoveOperation<E = never> = (
|
||||
input: CredentialRemoveInput,
|
||||
|
||||
@@ -430,7 +430,7 @@ const EndpointSessionRemove = (raw: RawClient["server.session"]) => (input: Sess
|
||||
|
||||
const EndpointSessionFork = (raw: RawClient["server.session"]) => (input: SessionForkInput) =>
|
||||
preserveEffect<SessionForkOutput>()(
|
||||
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { boundary: input["boundary"] } }).pipe(
|
||||
raw["session.fork"]({ params: { sessionID: input["sessionID"] }, payload: { before: input["before"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
Effect.map((value) => value.data),
|
||||
),
|
||||
@@ -969,25 +969,20 @@ const EndpointCredentialUpdate = (raw: RawClient["server.credential"]) => (input
|
||||
preserveEffect<CredentialUpdateOutput>()(
|
||||
raw["credential.update"]({
|
||||
params: { credentialID: input["credentialID"] },
|
||||
query: { location: input["location"] },
|
||||
payload: { label: input["label"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const EndpointCredentialActivate = (raw: RawClient["server.credential"]) => (input: CredentialActivateInput) =>
|
||||
preserveEffect<CredentialActivateOutput>()(
|
||||
raw["credential.activate"]({
|
||||
params: { credentialID: input["credentialID"] },
|
||||
query: { location: input["location"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
raw["credential.activate"]({ params: { credentialID: input["credentialID"] } }).pipe(
|
||||
Effect.mapError(mapClientError),
|
||||
),
|
||||
)
|
||||
|
||||
const EndpointCredentialRemove = (raw: RawClient["server.credential"]) => (input: CredentialRemoveInput) =>
|
||||
preserveEffect<CredentialRemoveOutput>()(
|
||||
raw["credential.remove"]({
|
||||
params: { credentialID: input["credentialID"] },
|
||||
query: { location: input["location"] },
|
||||
}).pipe(Effect.mapError(mapClientError)),
|
||||
raw["credential.remove"]({ params: { credentialID: input["credentialID"] } }).pipe(Effect.mapError(mapClientError)),
|
||||
)
|
||||
|
||||
const adaptGroupCredential = (raw: RawClient["server.credential"]) => ({
|
||||
|
||||
@@ -517,7 +517,7 @@ export function make(options: ClientOptions) {
|
||||
request<{ readonly data: SessionStatsOutput }>(
|
||||
{
|
||||
method: "GET",
|
||||
path: `/api/session/stats`,
|
||||
path: `/api/experimental/session/stats`,
|
||||
query: {
|
||||
from: input?.["from"],
|
||||
to: input?.["to"],
|
||||
@@ -613,7 +613,7 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/session/${encodeURIComponent(input.sessionID)}/fork`,
|
||||
body: { boundary: input["boundary"] },
|
||||
body: { before: input["before"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
@@ -1129,7 +1129,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { key: input["key"], answer: input["answer"], label: input["label"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1156,7 +1156,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/oauth/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1169,7 +1169,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { code: input["code"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: true,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1196,7 +1196,7 @@ export function make(options: ClientOptions) {
|
||||
query: { location: input["location"] },
|
||||
body: { methodID: input["methodID"], label: input["label"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1208,7 +1208,7 @@ export function make(options: ClientOptions) {
|
||||
path: `/api/integration/${encodeURIComponent(input.integrationID)}/connect/command/${encodeURIComponent(input.attemptID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 200,
|
||||
declaredStatuses: [400, 401],
|
||||
declaredStatuses: [400, 401, 404],
|
||||
empty: false,
|
||||
},
|
||||
requestOptions,
|
||||
@@ -1244,7 +1244,7 @@ export function make(options: ClientOptions) {
|
||||
request<McpAddOutput>(
|
||||
{
|
||||
method: "PUT",
|
||||
path: `/api/mcp/${encodeURIComponent(input.server)}`,
|
||||
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`,
|
||||
query: { location: input["location"] },
|
||||
body: { config: input["config"] },
|
||||
successStatus: 204,
|
||||
@@ -1257,7 +1257,7 @@ export function make(options: ClientOptions) {
|
||||
request<McpRemoveOutput>(
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/mcp/${encodeURIComponent(input.server)}`,
|
||||
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
@@ -1269,7 +1269,7 @@ export function make(options: ClientOptions) {
|
||||
request<McpConnectOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/mcp/${encodeURIComponent(input.server)}/connect`,
|
||||
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/connect`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
@@ -1281,7 +1281,7 @@ export function make(options: ClientOptions) {
|
||||
request<McpDisconnectOutput>(
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/mcp/${encodeURIComponent(input.server)}/disconnect`,
|
||||
path: `/api/experimental/mcp/${encodeURIComponent(input.server)}/disconnect`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401, 404],
|
||||
@@ -1310,7 +1310,6 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "PATCH",
|
||||
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
|
||||
query: { location: input["location"] },
|
||||
body: { label: input["label"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
@@ -1323,7 +1322,6 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "POST",
|
||||
path: `/api/credential/${encodeURIComponent(input.credentialID)}/activate`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
@@ -1335,7 +1333,6 @@ export function make(options: ClientOptions) {
|
||||
{
|
||||
method: "DELETE",
|
||||
path: `/api/credential/${encodeURIComponent(input.credentialID)}`,
|
||||
query: { location: input["location"] },
|
||||
successStatus: 204,
|
||||
declaredStatuses: [400, 401],
|
||||
empty: true,
|
||||
|
||||
@@ -2510,6 +2510,24 @@ export type IntegrationNotFoundError = {
|
||||
export const isIntegrationNotFoundError = (value: unknown): value is IntegrationNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationNotFoundError"
|
||||
|
||||
export type IntegrationAttemptNotFoundError = {
|
||||
readonly _tag: "IntegrationAttemptNotFoundError"
|
||||
readonly integrationID: string
|
||||
readonly attemptID: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isIntegrationAttemptNotFoundError = (value: unknown): value is IntegrationAttemptNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationAttemptNotFoundError"
|
||||
|
||||
export type IntegrationMethodNotFoundError = {
|
||||
readonly _tag: "IntegrationMethodNotFoundError"
|
||||
readonly integrationID: string
|
||||
readonly methodID: string
|
||||
readonly message: string
|
||||
}
|
||||
export const isIntegrationMethodNotFoundError = (value: unknown): value is IntegrationMethodNotFoundError =>
|
||||
typeof value === "object" && value !== null && "_tag" in value && value["_tag"] === "IntegrationMethodNotFoundError"
|
||||
|
||||
export type McpServerNotFoundError = {
|
||||
readonly _tag: "McpServerNotFoundError"
|
||||
readonly server: string
|
||||
@@ -3832,9 +3850,7 @@ export type SessionRemoveOutput = void
|
||||
|
||||
export type SessionForkInput = {
|
||||
readonly sessionID: { readonly sessionID: string }["sessionID"]
|
||||
readonly boundary: {
|
||||
readonly boundary: { readonly type: "before"; readonly messageID: string } | { readonly type: "through" }
|
||||
}["boundary"]
|
||||
readonly before?: { readonly before?: string | undefined }["before"]
|
||||
}
|
||||
|
||||
export type SessionForkOutput = { data: SessionInfo }["data"]
|
||||
@@ -4691,23 +4707,16 @@ export type McpResourceCatalogOutput = { location: LocationPublicRef; data: McpR
|
||||
|
||||
export type CredentialUpdateInput = {
|
||||
readonly credentialID: { readonly credentialID: string }["credentialID"]
|
||||
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
|
||||
readonly label: { readonly label: string }["label"]
|
||||
}
|
||||
|
||||
export type CredentialUpdateOutput = void
|
||||
|
||||
export type CredentialActivateInput = {
|
||||
readonly credentialID: { readonly credentialID: string }["credentialID"]
|
||||
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
|
||||
}
|
||||
export type CredentialActivateInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] }
|
||||
|
||||
export type CredentialActivateOutput = void
|
||||
|
||||
export type CredentialRemoveInput = {
|
||||
readonly credentialID: { readonly credentialID: string }["credentialID"]
|
||||
readonly location?: { readonly location?: { readonly directory?: string | undefined } | undefined }["location"]
|
||||
}
|
||||
export type CredentialRemoveInput = { readonly credentialID: { readonly credentialID: string }["credentialID"] }
|
||||
|
||||
export type CredentialRemoveOutput = void
|
||||
|
||||
|
||||
@@ -239,12 +239,10 @@ test("credential.activate uses the public HTTP contract", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await client.credential.activate({ credentialID: "cred_work", location: { directory: "/tmp/project" } })
|
||||
await client.credential.activate({ credentialID: "cred_work" })
|
||||
|
||||
expect(request?.method).toBe("POST")
|
||||
expect(request?.url).toBe(
|
||||
"http://localhost:3000/api/credential/cred_work/activate?location%5Bdirectory%5D=%2Ftmp%2Fproject",
|
||||
)
|
||||
expect(request?.url).toBe("http://localhost:3000/api/credential/cred_work/activate")
|
||||
})
|
||||
|
||||
test("integration connections optionally submit a form answer", async () => {
|
||||
|
||||
@@ -113,7 +113,12 @@ export class AuthorizationError extends Schema.TaggedError<AuthorizationError>()
|
||||
cause: Schema.Defect(),
|
||||
}) {}
|
||||
|
||||
export type Error = CodeRequiredError | AuthorizationError
|
||||
export class AttemptNotFoundError extends Schema.TaggedError<AttemptNotFoundError>()("Integration.AttemptNotFound", {
|
||||
integrationID: ID,
|
||||
attemptID: AttemptID,
|
||||
}) {}
|
||||
|
||||
export type Error = CodeRequiredError | AuthorizationError | AttemptNotFoundError
|
||||
|
||||
export { Event } from "@opencode/schema/integration"
|
||||
|
||||
@@ -188,13 +193,13 @@ export interface Interface extends State.Transformable<Editor> {
|
||||
readonly status: (input: {
|
||||
readonly integrationID: ID
|
||||
readonly attemptID: AttemptID
|
||||
}) => Effect.Effect<AttemptStatus>
|
||||
}) => Effect.Effect<AttemptStatus, AttemptNotFoundError>
|
||||
/** Completes the attempt and stores its credential. */
|
||||
readonly complete: (input: {
|
||||
readonly integrationID: ID
|
||||
readonly attemptID: AttemptID
|
||||
readonly code?: string
|
||||
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError>
|
||||
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError | AttemptNotFoundError>
|
||||
/** Cancels an attempt and releases its resources. */
|
||||
readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
|
||||
}
|
||||
@@ -207,7 +212,7 @@ export interface Interface extends State.Transformable<Editor> {
|
||||
readonly status: (input: {
|
||||
readonly integrationID: ID
|
||||
readonly attemptID: AttemptID
|
||||
}) => Effect.Effect<CommandAttemptStatus>
|
||||
}) => Effect.Effect<CommandAttemptStatus, AttemptNotFoundError>
|
||||
readonly cancel: (input: { readonly integrationID: ID; readonly attemptID: AttemptID }) => Effect.Effect<void>
|
||||
}
|
||||
}
|
||||
@@ -718,7 +723,7 @@ const layer = Layer.effect(
|
||||
status: Effect.fn("Integration.oauth.status")(function* (input) {
|
||||
const attempt = (yield* SynchronizedRef.get(attempts)).get(input.attemptID)
|
||||
if (!attempt || attempt.integrationID !== input.integrationID)
|
||||
return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
|
||||
return yield* new AttemptNotFoundError(input)
|
||||
if (attempt.status === "failed") {
|
||||
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
|
||||
}
|
||||
@@ -732,7 +737,7 @@ const layer = Layer.effect(
|
||||
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
|
||||
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
|
||||
})
|
||||
if (!attempt) return yield* Effect.die(new Error(`OAuth attempt not found: ${input.attemptID}`))
|
||||
if (!attempt) return yield* new AttemptNotFoundError(input)
|
||||
if (attempt.status !== "pending") return
|
||||
if (attempt.authorization.mode === "code" && input.code === undefined) {
|
||||
return yield* new CodeRequiredError({ attemptID: input.attemptID })
|
||||
@@ -764,7 +769,7 @@ const layer = Layer.effect(
|
||||
status: Effect.fn("Integration.command.status")(function* (input) {
|
||||
const attempt = (yield* SynchronizedRef.get(commandAttempts)).get(input.attemptID)
|
||||
if (!attempt || attempt.integrationID !== input.integrationID)
|
||||
return yield* Effect.die(new Error(`Command attempt not found: ${input.attemptID}`))
|
||||
return yield* new AttemptNotFoundError(input)
|
||||
if (attempt.status === "pending") {
|
||||
return {
|
||||
status: attempt.status,
|
||||
|
||||
@@ -94,7 +94,7 @@ type CompactInput = Parameters<Session.Handle["compact"]>[0] & { sessionID: Sess
|
||||
|
||||
type ForkInput = {
|
||||
sessionID: SessionSchema.ID
|
||||
boundary: SessionSchema.ForkRequestBoundary
|
||||
before?: SessionMessage.ID
|
||||
}
|
||||
|
||||
export {
|
||||
@@ -308,17 +308,17 @@ const layer = Layer.effect(
|
||||
.where(
|
||||
and(
|
||||
eq(SessionMessageTable.session_id, input.sessionID),
|
||||
input.boundary.type === "before" ? eq(SessionMessageTable.id, input.boundary.messageID) : undefined,
|
||||
input.before ? eq(SessionMessageTable.id, input.before) : undefined,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(SessionMessageTable.seq))
|
||||
.limit(1)
|
||||
.get()
|
||||
.pipe(Effect.orDie)
|
||||
if (!boundary && input.boundary.type === "before")
|
||||
if (!boundary && input.before)
|
||||
return yield* new MessageNotFoundError({
|
||||
sessionID: input.sessionID,
|
||||
messageID: input.boundary.messageID,
|
||||
messageID: input.before,
|
||||
})
|
||||
if (!boundary) return yield* new ForkEmptyError({ sessionID: input.sessionID })
|
||||
const sessionID = SessionSchema.ID.create()
|
||||
@@ -336,7 +336,7 @@ const layer = Layer.effect(
|
||||
yield* bus.publish(SessionEvent.Forked, {
|
||||
sessionID,
|
||||
parentID: parent.id,
|
||||
boundary: { ...input.boundary, messageID: boundary.id },
|
||||
boundary: { type: input.before ? "before" : "through", messageID: boundary.id },
|
||||
...inherited,
|
||||
})
|
||||
return yield* result.get(sessionID).pipe(Effect.orDie)
|
||||
|
||||
@@ -75,11 +75,11 @@ function required<T>(value: T | undefined): T {
|
||||
return value
|
||||
}
|
||||
|
||||
function eventually<A>(
|
||||
effect: Effect.Effect<A>,
|
||||
function eventually<A, E>(
|
||||
effect: Effect.Effect<A, E>,
|
||||
predicate: (value: A) => boolean,
|
||||
remaining = 1000,
|
||||
): Effect.Effect<A, Error> {
|
||||
): Effect.Effect<A, E | Error> {
|
||||
return Effect.gen(function* () {
|
||||
const value = yield* effect
|
||||
if (predicate(value)) return value
|
||||
|
||||
@@ -380,7 +380,7 @@ describe("Session.create", () => {
|
||||
|
||||
yield* session.prompt({ sessionID: created.id, text: "Fork context", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, created.id, "steer")
|
||||
const forked = yield* session.fork({ sessionID: created.id, boundary: { type: "through" } })
|
||||
const forked = yield* session.fork({ sessionID: created.id })
|
||||
expect(forked.metadata).toEqual(metadata)
|
||||
|
||||
// Absent stays absent: no empty-object normalization.
|
||||
@@ -402,7 +402,7 @@ describe("Session.create", () => {
|
||||
|
||||
yield* session.prompt({ sessionID: created.id, text: "Fork context", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, created.id, "steer")
|
||||
const forked = yield* session.fork({ sessionID: created.id, boundary: { type: "through" } })
|
||||
const forked = yield* session.fork({ sessionID: created.id })
|
||||
expect(forked.permissions).toEqual(permissions)
|
||||
|
||||
const replaced = [{ action: "shell", resource: "*", effect: "ask" as const }]
|
||||
@@ -541,7 +541,7 @@ describe("Session.create", () => {
|
||||
yield* session.synthetic({ sessionID: parent.id, text: "parent note", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
|
||||
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const forked = yield* session.fork({ sessionID: parent.id })
|
||||
const parentContext = yield* session.context(parent.id)
|
||||
const forkContext = yield* session.context(forked.id)
|
||||
const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
|
||||
@@ -596,7 +596,7 @@ describe("Session.create", () => {
|
||||
yield* session.prompt({ sessionID: parent.id, text: "First", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
|
||||
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const forked = yield* session.fork({ sessionID: parent.id })
|
||||
const row = yield* db.select().from(SessionTable).where(eq(SessionTable.id, forked.id)).get().pipe(Effect.orDie)
|
||||
|
||||
expect(forked.title).toBeUndefined()
|
||||
@@ -614,7 +614,7 @@ describe("Session.create", () => {
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
yield* session.synthetic({ sessionID: parent.id, text: "Second", resume: false })
|
||||
yield* SessionInbox.promote(db, bus, parent.id, "steer")
|
||||
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const forked = yield* session.fork({ sessionID: parent.id })
|
||||
const original = (yield* session.context(forked.id)).map((message) => message.id)
|
||||
const recorded = yield* db
|
||||
.select()
|
||||
@@ -657,7 +657,7 @@ describe("Session.create", () => {
|
||||
{ discard: true },
|
||||
)
|
||||
|
||||
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const forked = yield* session.fork({ sessionID: parent.id })
|
||||
const inheritedList = yield* entries.list(forked.id)
|
||||
const inheritedValues = yield* entries.load(forked.id).pipe(Effect.flatMap(Instructions.read))
|
||||
|
||||
@@ -730,7 +730,7 @@ describe("Session.create", () => {
|
||||
executed: true,
|
||||
})
|
||||
|
||||
const forked = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const forked = yield* session.fork({ sessionID: parent.id })
|
||||
|
||||
expect(yield* session.context(parent.id)).toMatchObject([
|
||||
Expected.user("Run both tools"),
|
||||
@@ -760,7 +760,7 @@ describe("Session.create", () => {
|
||||
})
|
||||
yield* bus.publish(SessionEvent.Shell.Started, { sessionID: parent.id, shell })
|
||||
|
||||
const running = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const running = yield* session.fork({ sessionID: parent.id })
|
||||
|
||||
expect(yield* session.context(parent.id)).toMatchObject([
|
||||
Expected.user("Run a shell"),
|
||||
@@ -773,7 +773,7 @@ describe("Session.create", () => {
|
||||
shell: { ...shell, status: "exited", exit: 0, time: { started: 0, completed: 1 } },
|
||||
output: { output: "complete", cursor: 8, size: 8, truncated: false },
|
||||
})
|
||||
const completed = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const completed = yield* session.fork({ sessionID: parent.id })
|
||||
|
||||
expect(yield* session.context(running.id)).toMatchObject([Expected.user("Run a shell")])
|
||||
expect(yield* session.context(completed.id)).toMatchObject([
|
||||
@@ -789,7 +789,7 @@ describe("Session.create", () => {
|
||||
const parent = yield* session.create({ location })
|
||||
|
||||
expect(
|
||||
yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } }).pipe(Effect.flip),
|
||||
yield* session.fork({ sessionID: parent.id }).pipe(Effect.flip),
|
||||
).toMatchObject({ _tag: "Session.ForkEmptyError", sessionID: parent.id })
|
||||
}),
|
||||
)
|
||||
@@ -830,13 +830,13 @@ describe("Session.create", () => {
|
||||
|
||||
const forked = yield* session.fork({
|
||||
sessionID: parent.id,
|
||||
boundary: { type: "before", messageID: second.id },
|
||||
before: second.id,
|
||||
})
|
||||
const beforeFirst = yield* session.fork({
|
||||
sessionID: parent.id,
|
||||
boundary: { type: "before", messageID: first.id },
|
||||
before: first.id,
|
||||
})
|
||||
const complete = yield* session.fork({ sessionID: parent.id, boundary: { type: "through" } })
|
||||
const complete = yield* session.fork({ sessionID: parent.id })
|
||||
|
||||
const context = yield* session.context(forked.id)
|
||||
const history = Array.from(yield* Stream.runCollect(logEvents(session, forked.id)))
|
||||
|
||||
@@ -186,7 +186,7 @@ describe("Session.diff", () => {
|
||||
expect(yield* diff({ from: steer }).pipe(Effect.flip)).toMatchObject({
|
||||
_tag: "Session.MessageNotFoundError",
|
||||
})
|
||||
const forked = yield* sessions.fork({ sessionID: created.id, boundary: { type: "through" } })
|
||||
const forked = yield* sessions.fork({ sessionID: created.id })
|
||||
expect((yield* sessions.diff({ sessionID: forked.id, context: 0 })).map(summarize)).toEqual([
|
||||
["third.txt", "added", 1, 0],
|
||||
])
|
||||
|
||||
@@ -1497,7 +1497,7 @@ describe("SessionRunnerLLM", () => {
|
||||
expect(continued.system.map((part) => part.text)).toContain("Checkpoint instructions")
|
||||
expect(systemTexts(continued)).toEqual(["Newest instructions"])
|
||||
|
||||
const forked = yield* s.session.fork({ sessionID, boundary: { type: "before", messageID: after.id } })
|
||||
const forked = yield* s.session.fork({ sessionID, before: after.id })
|
||||
yield* s.session.prompt({ sessionID: forked.id, text: "Fork prompt", resume: false })
|
||||
yield* s.session.resume(forked.id)
|
||||
expect(s.requests.at(-1)?.messages[0]).toEqual(replacement[0])
|
||||
@@ -1552,7 +1552,7 @@ describe("SessionRunnerLLM", () => {
|
||||
s.systemBaseline = "Latest context"
|
||||
yield* s.runPrompt("Third")
|
||||
|
||||
const forked = yield* s.session.fork({ sessionID, boundary: { type: "before", messageID: second.id } })
|
||||
const forked = yield* s.session.fork({ sessionID, before: second.id })
|
||||
expect(
|
||||
yield* s.db.select().from(InstructionStateTable).where(eq(InstructionStateTable.session_id, forked.id)).get(),
|
||||
).toMatchObject({
|
||||
@@ -1597,14 +1597,14 @@ describe("SessionRunnerLLM", () => {
|
||||
s.systemBaseline = "Changed context"
|
||||
const second = yield* s.runPrompt("Second")
|
||||
|
||||
const child = yield* s.session.fork({ sessionID, boundary: { type: "before", messageID: second.id } })
|
||||
const child = yield* s.session.fork({ sessionID, before: second.id })
|
||||
const inheritedFirst = (yield* s.session.messages({ sessionID: child.id })).find(
|
||||
(message) => message.type === "user" && message.text === "First",
|
||||
)
|
||||
if (!inheritedFirst) return yield* Effect.die(new Error("Nested fork boundary message not found"))
|
||||
const grandchild = yield* s.session.fork({
|
||||
sessionID: child.id,
|
||||
boundary: { type: "before", messageID: inheritedFirst.id },
|
||||
before: inheritedFirst.id,
|
||||
})
|
||||
|
||||
expect(
|
||||
@@ -2036,7 +2036,7 @@ describe("SessionRunnerLLM", () => {
|
||||
yield* replaySessionProjection(sessionID)
|
||||
const latest = yield* s.runPrompt("Third")
|
||||
expect(systemTexts(s.requests[3])).toEqual(["Replacement context"])
|
||||
const fork = yield* s.session.fork({ sessionID, boundary: { type: "before", messageID: latest.id } })
|
||||
const fork = yield* s.session.fork({ sessionID, before: latest.id })
|
||||
expect(
|
||||
(yield* s.session.context(fork.id)).flatMap((message) => (message.type === "system" ? [message.text] : [])),
|
||||
).toEqual(["Replacement context"])
|
||||
|
||||
@@ -129,7 +129,7 @@ describe("Session.skill", () => {
|
||||
resume: false,
|
||||
})
|
||||
yield* SessionInbox.promote(database.db, bus, session.id, "steer")
|
||||
const forked = yield* sessions.fork({ sessionID: session.id, boundary: { type: "before", messageID: selected } })
|
||||
const forked = yield* sessions.fork({ sessionID: session.id, before: selected })
|
||||
|
||||
expect(yield* sessions.messages({ sessionID: forked.id })).toEqual([
|
||||
expect.objectContaining({ type: "user", text: "Before the skill" }),
|
||||
|
||||
+138
-133
@@ -931,10 +931,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/stats": {
|
||||
"/api/experimental/session/stats": {
|
||||
"get": {
|
||||
"tags": ["session"],
|
||||
"operationId": "session.stats",
|
||||
"operationId": "experimental.session.stats",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "from",
|
||||
@@ -1519,7 +1519,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Create a child session by copying projected history through or before a message boundary.",
|
||||
"description": "Create a child session by copying projected history before a message. Omit before to copy the full history.",
|
||||
"summary": "Fork session",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -1527,11 +1527,18 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"boundary": {
|
||||
"$ref": "#/components/schemas/Session.ForkRequestBoundary"
|
||||
"before": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["boundary"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
@@ -5373,6 +5380,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Run a key authentication method and store the resulting credential.",
|
||||
@@ -5644,6 +5661,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Poll the current status of an OAuth attempt.",
|
||||
@@ -5813,6 +5847,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Complete a code-based OAuth attempt and store the resulting credential.",
|
||||
@@ -5934,6 +5985,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationMethodNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Start a command authentication attempt.",
|
||||
@@ -6060,6 +6128,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Poll the current status and output of a command authentication attempt.",
|
||||
@@ -6232,10 +6317,10 @@
|
||||
"summary": "List MCP servers"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}": {
|
||||
"/api/experimental/mcp/{server}": {
|
||||
"put": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.add",
|
||||
"operationId": "experimental.mcp.add",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6331,7 +6416,7 @@
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.remove",
|
||||
"operationId": "experimental.mcp.remove",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6412,10 +6497,10 @@
|
||||
"summary": "Remove MCP server"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}/connect": {
|
||||
"/api/experimental/mcp/{server}/connect": {
|
||||
"post": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.connect",
|
||||
"operationId": "experimental.mcp.connect",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6496,10 +6581,10 @@
|
||||
"summary": "Connect MCP server"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}/disconnect": {
|
||||
"/api/experimental/mcp/{server}/disconnect": {
|
||||
"post": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.disconnect",
|
||||
"operationId": "experimental.mcp.disconnect",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6675,36 +6760,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -6764,36 +6819,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -6838,36 +6863,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -15121,6 +15116,46 @@
|
||||
"required": ["id", "type", "label"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationAttemptNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["IntegrationAttemptNotFoundError"]
|
||||
},
|
||||
"integrationID": {
|
||||
"type": "string"
|
||||
},
|
||||
"attemptID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "integrationID", "attemptID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationMethodNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["IntegrationMethodNotFoundError"]
|
||||
},
|
||||
"integrationID": {
|
||||
"type": "string"
|
||||
},
|
||||
"methodID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "integrationID", "methodID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16960,36 +16995,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Session.ForkRequestBoundary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["before"]
|
||||
},
|
||||
"messageID": {
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
}
|
||||
},
|
||||
"required": ["type", "messageID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["through"]
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"Session.Inbox.Compaction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -43,7 +43,6 @@ type LocationGroups<LocationId extends HttpApiMiddleware.AnyId> =
|
||||
| HttpApiGroup.AddMiddleware<typeof IntegrationGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof WebSearchGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof McpGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof CredentialGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof ProjectGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof FileSystemGroup, LocationId>
|
||||
| HttpApiGroup.AddMiddleware<typeof CommandGroup, LocationId>
|
||||
@@ -88,6 +87,7 @@ type ApiGroups<
|
||||
| typeof MigrationGroup
|
||||
| typeof GenerateGroup
|
||||
| typeof PersistentPtyGroup
|
||||
| typeof CredentialGroup
|
||||
| LocationGroups<LocationId>
|
||||
| FormGroups<LocationId, LocationService, FormLocationId, FormLocationService>
|
||||
| SessionGroups<SessionLocationId, SessionLocationService>
|
||||
@@ -158,7 +158,7 @@ const makeApiFromGroup = <
|
||||
.add(ProviderGroup.middleware(locationMiddleware))
|
||||
.add(IntegrationGroup.middleware(locationMiddleware))
|
||||
.add(McpGroup.middleware(locationMiddleware))
|
||||
.add(CredentialGroup.middleware(locationMiddleware))
|
||||
.add(CredentialGroup)
|
||||
.add(ProjectGroup.middleware(locationMiddleware))
|
||||
.add(makeFormGroup(locationMiddleware, formLocationMiddleware))
|
||||
.add(makePermissionGroup(locationMiddleware, sessionLocationMiddleware))
|
||||
|
||||
@@ -91,6 +91,26 @@ export class IntegrationNotFoundError extends Schema.TaggedError<IntegrationNotF
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class IntegrationAttemptNotFoundError extends Schema.TaggedError<IntegrationAttemptNotFoundError>()(
|
||||
"IntegrationAttemptNotFoundError",
|
||||
{
|
||||
integrationID: Schema.String,
|
||||
attemptID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class IntegrationMethodNotFoundError extends Schema.TaggedError<IntegrationMethodNotFoundError>()(
|
||||
"IntegrationMethodNotFoundError",
|
||||
{
|
||||
integrationID: Schema.String,
|
||||
methodID: Schema.String,
|
||||
message: Schema.String,
|
||||
},
|
||||
{ httpApiStatus: 404 },
|
||||
) {}
|
||||
|
||||
export class ProjectNotFoundError extends Schema.TaggedError<ProjectNotFoundError>()(
|
||||
"ProjectNotFoundError",
|
||||
{
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { Credential } from "@opencode/schema/credential"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||
|
||||
export const CredentialGroup = HttpApiGroup.make("server.credential")
|
||||
.add(
|
||||
HttpApiEndpoint.patch("credential.update", "/api/credential/:credentialID", {
|
||||
params: { credentialID: Credential.ID },
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({ label: Schema.String }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "credential.update",
|
||||
@@ -24,10 +21,8 @@ export const CredentialGroup = HttpApiGroup.make("server.credential")
|
||||
.add(
|
||||
HttpApiEndpoint.post("credential.activate", "/api/credential/:credentialID/activate", {
|
||||
params: { credentialID: Credential.ID },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "credential.activate",
|
||||
@@ -39,10 +34,8 @@ export const CredentialGroup = HttpApiGroup.make("server.credential")
|
||||
.add(
|
||||
HttpApiEndpoint.delete("credential.remove", "/api/credential/:credentialID", {
|
||||
params: { credentialID: Credential.ID },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "credential.remove",
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Location } from "@opencode/schema/location"
|
||||
import { Form } from "@opencode/schema/form"
|
||||
import { Schema } from "effect"
|
||||
import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi"
|
||||
import { IntegrationNotFoundError, InvalidRequestError } from "../errors.js"
|
||||
import {
|
||||
IntegrationAttemptNotFoundError,
|
||||
IntegrationMethodNotFoundError,
|
||||
IntegrationNotFoundError,
|
||||
InvalidRequestError,
|
||||
} from "../errors.js"
|
||||
import { LocationQuery, locationQueryOpenApi } from "./location.js"
|
||||
|
||||
export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
@@ -63,7 +68,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
label: Schema.optional(Schema.String),
|
||||
}),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: InvalidRequestError,
|
||||
error: [IntegrationNotFoundError, InvalidRequestError],
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
@@ -100,6 +105,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
params: { integrationID: Integration.ID, attemptID: Integration.AttemptID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(Integration.AttemptStatus),
|
||||
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError],
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
@@ -119,7 +125,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
query: LocationQuery,
|
||||
payload: Schema.Struct({ code: Schema.optional(Schema.String) }),
|
||||
success: HttpApiSchema.NoContent,
|
||||
error: InvalidRequestError,
|
||||
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError, InvalidRequestError],
|
||||
},
|
||||
)
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
@@ -155,7 +161,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
label: Schema.optional(Schema.String),
|
||||
}),
|
||||
success: Location.response(Integration.CommandAttempt),
|
||||
error: InvalidRequestError,
|
||||
error: [IntegrationNotFoundError, IntegrationMethodNotFoundError, InvalidRequestError],
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
@@ -171,6 +177,7 @@ export const IntegrationGroup = HttpApiGroup.make("server.integration")
|
||||
params: { integrationID: Integration.ID, attemptID: Integration.AttemptID },
|
||||
query: LocationQuery,
|
||||
success: Location.response(Integration.CommandAttemptStatus),
|
||||
error: [IntegrationNotFoundError, IntegrationAttemptNotFoundError],
|
||||
})
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
|
||||
@@ -21,7 +21,7 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.put("mcp.add", "/api/mcp/:server", {
|
||||
HttpApiEndpoint.put("mcp.add", "/api/experimental/mcp/:server", {
|
||||
params: { server: Schema.String },
|
||||
query: LocationQuery,
|
||||
// Wrapped in a struct because the client codegen flattens payload fields and cannot
|
||||
@@ -32,14 +32,14 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.add",
|
||||
identifier: "experimental.mcp.add",
|
||||
summary: "Add MCP server",
|
||||
description: "Add an MCP server at runtime or replace an existing one, connecting it immediately.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.delete("mcp.remove", "/api/mcp/:server", {
|
||||
HttpApiEndpoint.delete("mcp.remove", "/api/experimental/mcp/:server", {
|
||||
params: { server: Schema.String },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
@@ -48,14 +48,14 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.remove",
|
||||
identifier: "experimental.mcp.remove",
|
||||
summary: "Remove MCP server",
|
||||
description: "Stop an MCP server and remove it from the runtime set until restart.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("mcp.connect", "/api/mcp/:server/connect", {
|
||||
HttpApiEndpoint.post("mcp.connect", "/api/experimental/mcp/:server/connect", {
|
||||
params: { server: Schema.String },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
@@ -64,14 +64,14 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.connect",
|
||||
identifier: "experimental.mcp.connect",
|
||||
summary: "Connect MCP server",
|
||||
description: "Connect an MCP server at runtime, overriding a disabled configuration until restart.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.post("mcp.disconnect", "/api/mcp/:server/disconnect", {
|
||||
HttpApiEndpoint.post("mcp.disconnect", "/api/experimental/mcp/:server/disconnect", {
|
||||
params: { server: Schema.String },
|
||||
query: LocationQuery,
|
||||
success: HttpApiSchema.NoContent,
|
||||
@@ -80,7 +80,7 @@ export const McpGroup = HttpApiGroup.make("server.mcp")
|
||||
.annotateMerge(locationQueryOpenApi)
|
||||
.annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "mcp.disconnect",
|
||||
identifier: "experimental.mcp.disconnect",
|
||||
summary: "Disconnect MCP server",
|
||||
description: "Disconnect an MCP server at runtime, removing its tools until reconnected.",
|
||||
}),
|
||||
|
||||
@@ -175,7 +175,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
),
|
||||
)
|
||||
.add(
|
||||
HttpApiEndpoint.get("session.stats", "/api/session/stats", {
|
||||
HttpApiEndpoint.get("session.stats", "/api/experimental/session/stats", {
|
||||
query: Schema.Struct({
|
||||
from: Schema.NumberFromString.pipe(Schema.optional),
|
||||
to: Schema.NumberFromString.pipe(Schema.optional),
|
||||
@@ -187,7 +187,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
error: InvalidRequestError,
|
||||
}).annotateMerge(
|
||||
OpenApi.annotations({
|
||||
identifier: "session.stats",
|
||||
identifier: "experimental.session.stats",
|
||||
summary: "Get session statistics",
|
||||
description: "Aggregate local session activity, usage, and tool reliability for a time range.",
|
||||
}),
|
||||
@@ -285,7 +285,7 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
.add(
|
||||
HttpApiEndpoint.post("session.fork", "/api/session/:sessionID/fork", {
|
||||
params: { sessionID: Session.ID },
|
||||
payload: Schema.Struct({ boundary: Session.ForkRequestBoundary }),
|
||||
payload: Schema.Struct({ before: SessionMessage.ID.pipe(Schema.optional) }),
|
||||
success: Schema.Struct({ data: PublicSessionInfo }),
|
||||
error: [SessionNotFoundError, MessageNotFoundError, InvalidRequestError],
|
||||
})
|
||||
@@ -294,7 +294,8 @@ export const makeSessionGroup = <I extends HttpApiMiddleware.AnyId, S>(sessionLo
|
||||
OpenApi.annotations({
|
||||
identifier: "session.fork",
|
||||
summary: "Fork session",
|
||||
description: "Create a child session by copying projected history through or before a message boundary.",
|
||||
description:
|
||||
"Create a child session by copying projected history before a message. Omit before to copy the full history.",
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,9 +8,3 @@ export const Boundary = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("through"), messageID: SessionMessage.ID }),
|
||||
]).annotate({ identifier: "Session.ForkBoundary" })
|
||||
export type Boundary = typeof Boundary.Type
|
||||
|
||||
export const RequestBoundary = Schema.Union([
|
||||
Schema.Struct({ type: Schema.Literal("before"), messageID: SessionMessage.ID }),
|
||||
Schema.Struct({ type: Schema.Literal("through") }),
|
||||
]).annotate({ identifier: "Session.ForkRequestBoundary" })
|
||||
export type RequestBoundary = typeof RequestBoundary.Type
|
||||
|
||||
@@ -26,8 +26,6 @@ export const Event = SessionEvent
|
||||
export { Revert }
|
||||
export const ForkBoundary = SessionFork.Boundary
|
||||
export type ForkBoundary = SessionFork.Boundary
|
||||
export const ForkRequestBoundary = SessionFork.RequestBoundary
|
||||
export type ForkRequestBoundary = SessionFork.RequestBoundary
|
||||
|
||||
export interface Info extends Schema.Schema.Type<typeof Info> {}
|
||||
export const Info = Schema.Struct({
|
||||
|
||||
@@ -26,7 +26,9 @@ export const Result = Schema.Struct({
|
||||
title: Schema.String.pipe(optional),
|
||||
content: Schema.String.pipe(optional),
|
||||
time: Schema.Struct({
|
||||
published: Schema.Finite.pipe(optional),
|
||||
published: Schema.Finite.pipe(optional).annotate({
|
||||
description: "Publication time in milliseconds since the Unix epoch",
|
||||
}),
|
||||
}),
|
||||
}).annotate({ identifier: "WebSearch.Result" })
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ test("Promise instances are lazy, share by key and Location, and stay isolated b
|
||||
)
|
||||
|
||||
await first.sessions.switchAgent({ sessionID, agent: "plan" })
|
||||
const fork = await first.sessions.fork({ sessionID, boundary: { type: "through" } })
|
||||
const fork = await first.sessions.fork({ sessionID })
|
||||
expect(fork.metadata).toEqual(original.metadata)
|
||||
expect(fork.location).toEqual(original.location)
|
||||
expect(fork.fork?.sessionID).toBe(sessionID)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Integration } from "@opencode/core/integration"
|
||||
import { Credential } from "@opencode/core/credential"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
@@ -8,24 +8,24 @@ export const CredentialHandler = HttpApiBuilder.group(Api, "server.credential",
|
||||
.handle(
|
||||
"credential.update",
|
||||
Effect.fn(function* (ctx) {
|
||||
const integration = yield* Integration.Service
|
||||
yield* integration.connection.update(ctx.params.credentialID, { label: ctx.payload.label })
|
||||
const credential = yield* Credential.Service
|
||||
yield* credential.update(ctx.params.credentialID, { label: ctx.payload.label })
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"credential.activate",
|
||||
Effect.fn(function* (ctx) {
|
||||
const integration = yield* Integration.Service
|
||||
yield* integration.connection.activate(ctx.params.credentialID)
|
||||
const credential = yield* Credential.Service
|
||||
yield* credential.activate(ctx.params.credentialID)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
)
|
||||
.handle(
|
||||
"credential.remove",
|
||||
Effect.fn(function* (ctx) {
|
||||
const integration = yield* Integration.Service
|
||||
yield* integration.connection.remove(ctx.params.credentialID)
|
||||
const credential = yield* Credential.Service
|
||||
yield* credential.remove(ctx.params.credentialID)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Integration } from "@opencode/core/integration"
|
||||
import { Effect } from "effect"
|
||||
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi"
|
||||
import { Api } from "../api"
|
||||
import { IntegrationNotFoundError, InvalidRequestError } from "@opencode/protocol/errors"
|
||||
import {
|
||||
IntegrationAttemptNotFoundError,
|
||||
IntegrationMethodNotFoundError,
|
||||
IntegrationNotFoundError,
|
||||
InvalidRequestError,
|
||||
} from "@opencode/protocol/errors"
|
||||
import { response } from "../location"
|
||||
import { WellKnown } from "@opencode/core/wellknown"
|
||||
|
||||
@@ -61,6 +66,11 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
|
||||
"integration.connect.key",
|
||||
Effect.fn(function* (ctx) {
|
||||
const service = yield* Integration.Service
|
||||
if (!(yield* service.get(ctx.params.integrationID)))
|
||||
return yield* new IntegrationNotFoundError({
|
||||
integrationID: ctx.params.integrationID,
|
||||
message: `Integration not found: ${ctx.params.integrationID}`,
|
||||
})
|
||||
yield* authorize(
|
||||
service.connection.key({
|
||||
integrationID: ctx.params.integrationID,
|
||||
@@ -92,11 +102,27 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
|
||||
"integration.oauth.status",
|
||||
Effect.fn(function* (ctx) {
|
||||
const service = yield* Integration.Service
|
||||
return yield* response(
|
||||
service.oauth.status({
|
||||
if (!(yield* service.get(ctx.params.integrationID)))
|
||||
return yield* new IntegrationNotFoundError({
|
||||
integrationID: ctx.params.integrationID,
|
||||
attemptID: ctx.params.attemptID,
|
||||
}),
|
||||
message: `Integration not found: ${ctx.params.integrationID}`,
|
||||
})
|
||||
return yield* response(
|
||||
service.oauth
|
||||
.status({
|
||||
integrationID: ctx.params.integrationID,
|
||||
attemptID: ctx.params.attemptID,
|
||||
})
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new IntegrationAttemptNotFoundError({
|
||||
integrationID: error.integrationID,
|
||||
attemptID: error.attemptID,
|
||||
message: `OAuth attempt not found: ${error.attemptID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -104,6 +130,11 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
|
||||
"integration.oauth.complete",
|
||||
Effect.fn(function* (ctx) {
|
||||
const service = yield* Integration.Service
|
||||
if (!(yield* service.get(ctx.params.integrationID)))
|
||||
return yield* new IntegrationNotFoundError({
|
||||
integrationID: ctx.params.integrationID,
|
||||
message: `Integration not found: ${ctx.params.integrationID}`,
|
||||
})
|
||||
yield* service.oauth
|
||||
.complete({
|
||||
integrationID: ctx.params.integrationID,
|
||||
@@ -111,19 +142,24 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
|
||||
code: ctx.payload.code,
|
||||
})
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new InvalidRequestError({
|
||||
message:
|
||||
error._tag === "Integration.CodeRequired"
|
||||
? "Authorization code is required"
|
||||
: "Authentication failed",
|
||||
kind:
|
||||
error._tag === "Integration.CodeRequired"
|
||||
? "integration_code_required"
|
||||
: "integration_authorization",
|
||||
}),
|
||||
),
|
||||
Effect.mapError((error) => {
|
||||
if (error._tag === "Integration.AttemptNotFound")
|
||||
return new IntegrationAttemptNotFoundError({
|
||||
integrationID: error.integrationID,
|
||||
attemptID: error.attemptID,
|
||||
message: `OAuth attempt not found: ${error.attemptID}`,
|
||||
})
|
||||
return new InvalidRequestError({
|
||||
message:
|
||||
error._tag === "Integration.CodeRequired"
|
||||
? "Authorization code is required"
|
||||
: "Authentication failed",
|
||||
kind:
|
||||
error._tag === "Integration.CodeRequired"
|
||||
? "integration_code_required"
|
||||
: "integration_authorization",
|
||||
})
|
||||
}),
|
||||
)
|
||||
return HttpApiSchema.NoContent.make()
|
||||
}),
|
||||
@@ -143,6 +179,18 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
|
||||
"integration.command.connect",
|
||||
Effect.fn(function* (ctx) {
|
||||
const service = yield* Integration.Service
|
||||
const integration = yield* service.get(ctx.params.integrationID)
|
||||
if (!integration)
|
||||
return yield* new IntegrationNotFoundError({
|
||||
integrationID: ctx.params.integrationID,
|
||||
message: `Integration not found: ${ctx.params.integrationID}`,
|
||||
})
|
||||
if (!integration.methods.some((method) => method.type === "command" && method.id === ctx.payload.methodID))
|
||||
return yield* new IntegrationMethodNotFoundError({
|
||||
integrationID: ctx.params.integrationID,
|
||||
methodID: ctx.payload.methodID,
|
||||
message: `Integration method not found: ${ctx.payload.methodID}`,
|
||||
})
|
||||
return yield* response(
|
||||
authorize(
|
||||
service.command.connect({
|
||||
@@ -158,11 +206,27 @@ export const IntegrationHandler = HttpApiBuilder.group(Api, "server.integration"
|
||||
"integration.command.status",
|
||||
Effect.fn(function* (ctx) {
|
||||
const service = yield* Integration.Service
|
||||
return yield* response(
|
||||
service.command.status({
|
||||
if (!(yield* service.get(ctx.params.integrationID)))
|
||||
return yield* new IntegrationNotFoundError({
|
||||
integrationID: ctx.params.integrationID,
|
||||
attemptID: ctx.params.attemptID,
|
||||
}),
|
||||
message: `Integration not found: ${ctx.params.integrationID}`,
|
||||
})
|
||||
return yield* response(
|
||||
service.command
|
||||
.status({
|
||||
integrationID: ctx.params.integrationID,
|
||||
attemptID: ctx.params.attemptID,
|
||||
})
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(error) =>
|
||||
new IntegrationAttemptNotFoundError({
|
||||
integrationID: error.integrationID,
|
||||
attemptID: error.attemptID,
|
||||
message: `Command attempt not found: ${error.attemptID}`,
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -209,7 +209,7 @@ export const SessionHandler = HttpApiBuilder.group(Api, "server.session", (handl
|
||||
"session.fork",
|
||||
Effect.fn(function* (ctx) {
|
||||
return {
|
||||
data: yield* session.fork({ sessionID: ctx.params.sessionID, boundary: ctx.payload.boundary }).pipe(
|
||||
data: yield* session.fork({ sessionID: ctx.params.sessionID, before: ctx.payload.before }).pipe(
|
||||
Effect.catchTag("Session.NotFoundError", missingSession),
|
||||
Effect.catchTag("Session.MessageNotFoundError", missingMessage),
|
||||
Effect.catchTag(
|
||||
|
||||
@@ -160,6 +160,7 @@ function makeRoutes<AuthError, AuthServices>(
|
||||
Layer.succeedContext(
|
||||
Context.pick(
|
||||
Database.Service,
|
||||
Credential.Service,
|
||||
PermissionSaved.Service,
|
||||
PluginUpdate.Service,
|
||||
Project.Service,
|
||||
|
||||
@@ -680,7 +680,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
return
|
||||
}
|
||||
void client.api.session
|
||||
.fork({ sessionID: match, boundary: { type: "through" } })
|
||||
.fork({ sessionID: match })
|
||||
.then((result) => route.navigate({ type: "session", sessionID: result.id, prompt: startupPrompt }))
|
||||
.catch(toast.error)
|
||||
})
|
||||
@@ -693,7 +693,7 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||
if (forked || !args.sessionID || !args.fork) return
|
||||
forked = true
|
||||
void client.api.session
|
||||
.fork({ sessionID: args.sessionID, boundary: { type: "through" } })
|
||||
.fork({ sessionID: args.sessionID })
|
||||
.then((result) => route.navigate({ type: "session", sessionID: result.id, prompt: startupPrompt }))
|
||||
.catch(toast.error)
|
||||
})
|
||||
|
||||
@@ -200,7 +200,7 @@ function manageConnections(
|
||||
onSelect: () => {
|
||||
if (credentialConnections(current() ?? integration)[0]?.id === connection.id) return
|
||||
void client.api.credential
|
||||
.activate({ credentialID: connection.id, location: locationQuery(location) })
|
||||
.activate({ credentialID: connection.id })
|
||||
.catch(toast.error)
|
||||
},
|
||||
}
|
||||
@@ -224,7 +224,7 @@ function manageConnections(
|
||||
const label = value.trim()
|
||||
if (!label) return
|
||||
void client.api.credential
|
||||
.update({ credentialID: option.value, label, location: locationQuery(location) })
|
||||
.update({ credentialID: option.value, label })
|
||||
.then(() => manageConnections(integration, methods, location, dialog, onConnected))
|
||||
.catch(toast.error)
|
||||
}}
|
||||
@@ -241,7 +241,7 @@ function manageConnections(
|
||||
if (deleting() !== option.value) return setDeleting(option.value)
|
||||
const final = credentialConnections(current() ?? integration).length === 1
|
||||
void client.api.credential
|
||||
.remove({ credentialID: option.value, location: locationQuery(location) })
|
||||
.remove({ credentialID: option.value })
|
||||
.then(() => {
|
||||
setDeleting(undefined)
|
||||
if (!final) return
|
||||
|
||||
@@ -23,7 +23,7 @@ export function DialogFork(props: { sessionID: string; messageID?: string; onMov
|
||||
const result = await client.api.session
|
||||
.fork({
|
||||
sessionID: props.sessionID,
|
||||
boundary: messageID ? { type: "before", messageID } : { type: "through" },
|
||||
before: messageID,
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.show({ message: errorMessage(error), variant: "error", duration: 5000 })
|
||||
|
||||
@@ -112,7 +112,7 @@ async function renderMcp(options?: { failed?: boolean; location?: { directory: s
|
||||
if (request.method === "DELETE") return new Response(null, { status: 204 })
|
||||
return json({ location, data: { status: "pending" } })
|
||||
}
|
||||
if (url.pathname === "/api/mcp/linear/connect" && request.method === "POST") {
|
||||
if (url.pathname === "/api/experimental/mcp/linear/connect" && request.method === "POST") {
|
||||
connect++
|
||||
return new Response(null, { status: 204 })
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ test("stats shows only this year and returns after errors or success", async ()
|
||||
setup.renderer.start()
|
||||
const requests: URL[] = []
|
||||
const calls = createFetch((url) => {
|
||||
if (url.pathname !== "/api/session/stats") return undefined
|
||||
if (url.pathname !== "/api/experimental/session/stats") return undefined
|
||||
requests.push(url)
|
||||
if (requests.length === 1) return json({ message: "offline" }, { status: 503 })
|
||||
return json({
|
||||
|
||||
+138
-133
@@ -931,10 +931,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/stats": {
|
||||
"/api/experimental/session/stats": {
|
||||
"get": {
|
||||
"tags": ["session"],
|
||||
"operationId": "session.stats",
|
||||
"operationId": "experimental.session.stats",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "from",
|
||||
@@ -1519,7 +1519,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Create a child session by copying projected history through or before a message boundary.",
|
||||
"description": "Create a child session by copying projected history before a message. Omit before to copy the full history.",
|
||||
"summary": "Fork session",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -1527,11 +1527,18 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"boundary": {
|
||||
"$ref": "#/components/schemas/Session.ForkRequestBoundary"
|
||||
"before": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["boundary"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
@@ -5373,6 +5380,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Run a key authentication method and store the resulting credential.",
|
||||
@@ -5644,6 +5661,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Poll the current status of an OAuth attempt.",
|
||||
@@ -5813,6 +5847,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Complete a code-based OAuth attempt and store the resulting credential.",
|
||||
@@ -5934,6 +5985,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationMethodNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Start a command authentication attempt.",
|
||||
@@ -6060,6 +6128,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Poll the current status and output of a command authentication attempt.",
|
||||
@@ -6232,10 +6317,10 @@
|
||||
"summary": "List MCP servers"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}": {
|
||||
"/api/experimental/mcp/{server}": {
|
||||
"put": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.add",
|
||||
"operationId": "experimental.mcp.add",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6331,7 +6416,7 @@
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.remove",
|
||||
"operationId": "experimental.mcp.remove",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6412,10 +6497,10 @@
|
||||
"summary": "Remove MCP server"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}/connect": {
|
||||
"/api/experimental/mcp/{server}/connect": {
|
||||
"post": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.connect",
|
||||
"operationId": "experimental.mcp.connect",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6496,10 +6581,10 @@
|
||||
"summary": "Connect MCP server"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}/disconnect": {
|
||||
"/api/experimental/mcp/{server}/disconnect": {
|
||||
"post": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.disconnect",
|
||||
"operationId": "experimental.mcp.disconnect",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6675,36 +6760,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -6764,36 +6819,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -6838,36 +6863,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -15121,6 +15116,46 @@
|
||||
"required": ["id", "type", "label"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationAttemptNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["IntegrationAttemptNotFoundError"]
|
||||
},
|
||||
"integrationID": {
|
||||
"type": "string"
|
||||
},
|
||||
"attemptID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "integrationID", "attemptID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationMethodNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["IntegrationMethodNotFoundError"]
|
||||
},
|
||||
"integrationID": {
|
||||
"type": "string"
|
||||
},
|
||||
"methodID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "integrationID", "methodID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16960,36 +16995,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Session.ForkRequestBoundary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["before"]
|
||||
},
|
||||
"messageID": {
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
}
|
||||
},
|
||||
"required": ["type", "messageID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["through"]
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"Session.Inbox.Compaction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+138
-133
@@ -931,10 +931,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/session/stats": {
|
||||
"/api/experimental/session/stats": {
|
||||
"get": {
|
||||
"tags": ["session"],
|
||||
"operationId": "session.stats",
|
||||
"operationId": "experimental.session.stats",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "from",
|
||||
@@ -1519,7 +1519,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Create a child session by copying projected history through or before a message boundary.",
|
||||
"description": "Create a child session by copying projected history before a message. Omit before to copy the full history.",
|
||||
"summary": "Fork session",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -1527,11 +1527,18 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"boundary": {
|
||||
"$ref": "#/components/schemas/Session.ForkRequestBoundary"
|
||||
"before": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["boundary"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
}
|
||||
@@ -5373,6 +5380,16 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Run a key authentication method and store the resulting credential.",
|
||||
@@ -5644,6 +5661,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Poll the current status of an OAuth attempt.",
|
||||
@@ -5813,6 +5847,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Complete a code-based OAuth attempt and store the resulting credential.",
|
||||
@@ -5934,6 +5985,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationMethodNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationMethodNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Start a command authentication attempt.",
|
||||
@@ -6060,6 +6128,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "IntegrationNotFoundError | IntegrationAttemptNotFoundError",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationNotFoundErrorEncoded"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationAttemptNotFoundErrorEncoded"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Poll the current status and output of a command authentication attempt.",
|
||||
@@ -6232,10 +6317,10 @@
|
||||
"summary": "List MCP servers"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}": {
|
||||
"/api/experimental/mcp/{server}": {
|
||||
"put": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.add",
|
||||
"operationId": "experimental.mcp.add",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6331,7 +6416,7 @@
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.remove",
|
||||
"operationId": "experimental.mcp.remove",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6412,10 +6497,10 @@
|
||||
"summary": "Remove MCP server"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}/connect": {
|
||||
"/api/experimental/mcp/{server}/connect": {
|
||||
"post": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.connect",
|
||||
"operationId": "experimental.mcp.connect",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6496,10 +6581,10 @@
|
||||
"summary": "Connect MCP server"
|
||||
}
|
||||
},
|
||||
"/api/mcp/{server}/disconnect": {
|
||||
"/api/experimental/mcp/{server}/disconnect": {
|
||||
"post": {
|
||||
"tags": ["mcp"],
|
||||
"operationId": "mcp.disconnect",
|
||||
"operationId": "experimental.mcp.disconnect",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "server",
|
||||
@@ -6675,36 +6760,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -6764,36 +6819,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -6838,36 +6863,6 @@
|
||||
"type": "string"
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"required": false,
|
||||
"style": "deepObject",
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"security": [],
|
||||
@@ -15121,6 +15116,46 @@
|
||||
"required": ["id", "type", "label"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationAttemptNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["IntegrationAttemptNotFoundError"]
|
||||
},
|
||||
"integrationID": {
|
||||
"type": "string"
|
||||
},
|
||||
"attemptID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "integrationID", "attemptID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationMethodNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"_tag": {
|
||||
"type": "string",
|
||||
"enum": ["IntegrationMethodNotFoundError"]
|
||||
},
|
||||
"integrationID": {
|
||||
"type": "string"
|
||||
},
|
||||
"methodID": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["_tag", "integrationID", "methodID", "message"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IntegrationNotFoundErrorEncoded": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16960,36 +16995,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Session.ForkRequestBoundary": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["before"]
|
||||
},
|
||||
"messageID": {
|
||||
"type": "string",
|
||||
"pattern": "^msg_"
|
||||
}
|
||||
},
|
||||
"required": ["type", "messageID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["through"]
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"Session.Inbox.Compaction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user