chore: generate

This commit is contained in:
opencode-agent[bot] 2026-04-29 13:36:05 +00:00
commit df147b65fd
25 changed files with 971 additions and 708 deletions

View file

@ -29,14 +29,18 @@ export const PtyApi = HttpApi.make("pty")
.add( .add(
HttpApiGroup.make("pty") HttpApiGroup.make("pty")
.add( .add(
HttpApiEndpoint.get("shells", PtyPaths.shells, { success: described(Schema.Array(ShellItem), "List of shells") }).annotateMerge( HttpApiEndpoint.get("shells", PtyPaths.shells, {
success: described(Schema.Array(ShellItem), "List of shells"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "pty.shells", identifier: "pty.shells",
summary: "List available shells", summary: "List available shells",
description: "Get a list of available shells on the system.", description: "Get a list of available shells on the system.",
}), }),
), ),
HttpApiEndpoint.get("list", PtyPaths.list, { success: described(Schema.Array(Pty.Info), "List of sessions") }).annotateMerge( HttpApiEndpoint.get("list", PtyPaths.list, {
success: described(Schema.Array(Pty.Info), "List of sessions"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "pty.list", identifier: "pty.list",
summary: "List PTY sessions", summary: "List PTY sessions",

View file

@ -11,11 +11,28 @@ export const TuiRequestPayload = Schema.Struct({
path: Schema.String, path: Schema.String,
body: Schema.Unknown, body: Schema.Unknown,
}) })
const EventTuiPromptAppend = Schema.Struct({ type: Schema.Literal(TuiEvent.PromptAppend.type), properties: TuiEvent.PromptAppend.properties }).annotate({ identifier: "EventTuiPromptAppend" }) const EventTuiPromptAppend = Schema.Struct({
const EventTuiCommandExecute = Schema.Struct({ type: Schema.Literal(TuiEvent.CommandExecute.type), properties: TuiEvent.CommandExecute.properties }).annotate({ identifier: "EventTuiCommandExecute" }) type: Schema.Literal(TuiEvent.PromptAppend.type),
const EventTuiToastShow = Schema.Struct({ type: Schema.Literal(TuiEvent.ToastShow.type), properties: TuiEvent.ToastShow.properties }).annotate({ identifier: "EventTuiToastShow" }) properties: TuiEvent.PromptAppend.properties,
const EventTuiSessionSelect = Schema.Struct({ type: Schema.Literal(TuiEvent.SessionSelect.type), properties: TuiEvent.SessionSelect.properties }).annotate({ identifier: "EventTuiSessionSelect" }) }).annotate({ identifier: "EventTuiPromptAppend" })
export const TuiPublishPayload = Schema.Union([EventTuiPromptAppend, EventTuiCommandExecute, EventTuiToastShow, EventTuiSessionSelect]) const EventTuiCommandExecute = Schema.Struct({
type: Schema.Literal(TuiEvent.CommandExecute.type),
properties: TuiEvent.CommandExecute.properties,
}).annotate({ identifier: "EventTuiCommandExecute" })
const EventTuiToastShow = Schema.Struct({
type: Schema.Literal(TuiEvent.ToastShow.type),
properties: TuiEvent.ToastShow.properties,
}).annotate({ identifier: "EventTuiToastShow" })
const EventTuiSessionSelect = Schema.Struct({
type: Schema.Literal(TuiEvent.SessionSelect.type),
properties: TuiEvent.SessionSelect.properties,
}).annotate({ identifier: "EventTuiSessionSelect" })
export const TuiPublishPayload = Schema.Union([
EventTuiPromptAppend,
EventTuiCommandExecute,
EventTuiToastShow,
EventTuiSessionSelect,
])
export const TuiPaths = { export const TuiPaths = {
appendPrompt: `${root}/append-prompt`, appendPrompt: `${root}/append-prompt`,
@ -48,42 +65,54 @@ export const TuiApi = HttpApi.make("tui")
description: "Append prompt to the TUI.", description: "Append prompt to the TUI.",
}), }),
), ),
HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, { success: described(Schema.Boolean, "Help dialog opened successfully") }).annotateMerge( HttpApiEndpoint.post("openHelp", TuiPaths.openHelp, {
success: described(Schema.Boolean, "Help dialog opened successfully"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "tui.openHelp", identifier: "tui.openHelp",
summary: "Open help dialog", summary: "Open help dialog",
description: "Open the help dialog in the TUI to display user assistance information.", description: "Open the help dialog in the TUI to display user assistance information.",
}), }),
), ),
HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, { success: described(Schema.Boolean, "Session dialog opened successfully") }).annotateMerge( HttpApiEndpoint.post("openSessions", TuiPaths.openSessions, {
success: described(Schema.Boolean, "Session dialog opened successfully"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "tui.openSessions", identifier: "tui.openSessions",
summary: "Open sessions dialog", summary: "Open sessions dialog",
description: "Open the session dialog.", description: "Open the session dialog.",
}), }),
), ),
HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, { success: described(Schema.Boolean, "Theme dialog opened successfully") }).annotateMerge( HttpApiEndpoint.post("openThemes", TuiPaths.openThemes, {
success: described(Schema.Boolean, "Theme dialog opened successfully"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "tui.openThemes", identifier: "tui.openThemes",
summary: "Open themes dialog", summary: "Open themes dialog",
description: "Open the theme dialog.", description: "Open the theme dialog.",
}), }),
), ),
HttpApiEndpoint.post("openModels", TuiPaths.openModels, { success: described(Schema.Boolean, "Model dialog opened successfully") }).annotateMerge( HttpApiEndpoint.post("openModels", TuiPaths.openModels, {
success: described(Schema.Boolean, "Model dialog opened successfully"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "tui.openModels", identifier: "tui.openModels",
summary: "Open models dialog", summary: "Open models dialog",
description: "Open the model dialog.", description: "Open the model dialog.",
}), }),
), ),
HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, { success: described(Schema.Boolean, "Prompt submitted successfully") }).annotateMerge( HttpApiEndpoint.post("submitPrompt", TuiPaths.submitPrompt, {
success: described(Schema.Boolean, "Prompt submitted successfully"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "tui.submitPrompt", identifier: "tui.submitPrompt",
summary: "Submit TUI prompt", summary: "Submit TUI prompt",
description: "Submit the prompt.", description: "Submit the prompt.",
}), }),
), ),
HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, { success: described(Schema.Boolean, "Prompt cleared successfully") }).annotateMerge( HttpApiEndpoint.post("clearPrompt", TuiPaths.clearPrompt, {
success: described(Schema.Boolean, "Prompt cleared successfully"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "tui.clearPrompt", identifier: "tui.clearPrompt",
summary: "Clear TUI prompt", summary: "Clear TUI prompt",
@ -133,7 +162,9 @@ export const TuiApi = HttpApi.make("tui")
description: "Navigate the TUI to display the specified session.", description: "Navigate the TUI to display the specified session.",
}), }),
), ),
HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, { success: described(TuiRequestPayload, "Next TUI request") }).annotateMerge( HttpApiEndpoint.get("controlNext", TuiPaths.controlNext, {
success: described(TuiRequestPayload, "Next TUI request"),
}).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "tui.control.next", identifier: "tui.control.next",
summary: "Get next TUI request", summary: "Get next TUI request",

View file

@ -9,9 +9,7 @@ import { described } from "./metadata"
const root = "/experimental/workspace" const root = "/experimental/workspace"
export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"])) export const CreatePayload = Schema.Struct(Struct.omit(Workspace.CreateInput.fields, ["projectID"]))
export const SessionRestorePayload = Schema.Struct( export const SessionRestorePayload = Schema.Struct(Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]))
Struct.omit(Workspace.SessionRestoreInput.fields, ["workspaceID"]),
)
export const SessionRestoreResponse = Schema.Struct({ export const SessionRestoreResponse = Schema.Struct({
total: NonNegativeInt, total: NonNegativeInt,
}) })

View file

@ -7,28 +7,28 @@ import { InstanceHttpApi } from "../api"
import { markInstanceForDisposal } from "../lifecycle" import { markInstanceForDisposal } from "../lifecycle"
export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (handlers) => export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const providerSvc = yield* Provider.Service const providerSvc = yield* Provider.Service
const configSvc = yield* Config.Service const configSvc = yield* Config.Service
const get = Effect.fn("ConfigHttpApi.get")(function* () { const get = Effect.fn("ConfigHttpApi.get")(function* () {
return yield* configSvc.get() return yield* configSvc.get()
}) })
const update = Effect.fn("ConfigHttpApi.update")(function* (ctx) { const update = Effect.fn("ConfigHttpApi.update")(function* (ctx) {
yield* configSvc.update(ctx.payload, { dispose: false }) yield* configSvc.update(ctx.payload, { dispose: false })
yield* markInstanceForDisposal(yield* InstanceState.context) yield* markInstanceForDisposal(yield* InstanceState.context)
return ctx.payload return ctx.payload
}) })
const providers = Effect.fn("ConfigHttpApi.providers")(function* () { const providers = Effect.fn("ConfigHttpApi.providers")(function* () {
const providers = yield* providerSvc.list() const providers = yield* providerSvc.list()
return { return {
providers: Object.values(providers), providers: Object.values(providers),
default: Provider.defaultModelIDs(providers), default: Provider.defaultModelIDs(providers),
} }
}) })
return handlers.handle("get", get).handle("update", update).handle("providers", providers) return handlers.handle("get", get).handle("update", update).handle("providers", providers)
}), }),
) )

View file

@ -7,28 +7,28 @@ import { RootHttpApi } from "../api"
import { LogInput } from "../groups/control" import { LogInput } from "../groups/control"
export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (handlers) => export const controlHandlers = HttpApiBuilder.group(RootHttpApi, "control", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const auth = yield* Auth.Service const auth = yield* Auth.Service
const authSet = Effect.fn("ControlHttpApi.authSet")(function* (ctx: { const authSet = Effect.fn("ControlHttpApi.authSet")(function* (ctx: {
params: { providerID: ProviderID } params: { providerID: ProviderID }
payload: Auth.Info payload: Auth.Info
}) { }) {
yield* auth.set(ctx.params.providerID, ctx.payload).pipe(Effect.orDie) yield* auth.set(ctx.params.providerID, ctx.payload).pipe(Effect.orDie)
return true return true
}) })
const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderID } }) { const authRemove = Effect.fn("ControlHttpApi.authRemove")(function* (ctx: { params: { providerID: ProviderID } }) {
yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie) yield* auth.remove(ctx.params.providerID).pipe(Effect.orDie)
return true return true
}) })
const log = Effect.fn("ControlHttpApi.log")(function* (ctx: { payload: typeof LogInput.Type }) { const log = Effect.fn("ControlHttpApi.log")(function* (ctx: { payload: typeof LogInput.Type }) {
const logger = Log.create({ service: ctx.payload.service }) const logger = Log.create({ service: ctx.payload.service })
logger[ctx.payload.level](ctx.payload.message, ctx.payload.extra) logger[ctx.payload.level](ctx.payload.message, ctx.payload.extra)
return true return true
}) })
return handlers.handle("authSet", authSet).handle("authRemove", authRemove).handle("log", log) return handlers.handle("authSet", authSet).handle("authRemove", authRemove).handle("log", log)
}), }),
) )

View file

@ -15,141 +15,141 @@ import { InstanceHttpApi } from "../api"
import { ConsoleSwitchPayload, SessionListQuery, ToolListQuery } from "../groups/experimental" import { ConsoleSwitchPayload, SessionListQuery, ToolListQuery } from "../groups/experimental"
export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "experimental", (handlers) => export const experimentalHandlers = HttpApiBuilder.group(InstanceHttpApi, "experimental", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const account = yield* Account.Service const account = yield* Account.Service
const agents = yield* Agent.Service const agents = yield* Agent.Service
const config = yield* Config.Service const config = yield* Config.Service
const mcp = yield* MCP.Service const mcp = yield* MCP.Service
const project = yield* Project.Service const project = yield* Project.Service
const registry = yield* ToolRegistry.Service const registry = yield* ToolRegistry.Service
const worktreeSvc = yield* Worktree.Service const worktreeSvc = yield* Worktree.Service
const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () { const getConsole = Effect.fn("ExperimentalHttpApi.console")(function* () {
const [state, groups] = yield* Effect.all( const [state, groups] = yield* Effect.all(
[config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)], [config.getConsoleState(), account.orgsByAccount().pipe(Effect.orDie)],
{ {
concurrency: "unbounded", concurrency: "unbounded",
}, },
) )
return { return {
consoleManagedProviders: state.consoleManagedProviders, consoleManagedProviders: state.consoleManagedProviders,
...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}), ...(state.activeOrgName ? { activeOrgName: state.activeOrgName } : {}),
switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0), switchableOrgCount: groups.reduce((count, group) => count + group.orgs.length, 0),
} }
})
const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () {
const [groups, active] = yield* Effect.all(
[account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)],
{
concurrency: "unbounded",
},
)
const info = Option.getOrUndefined(active)
return {
orgs: groups.flatMap((group) =>
group.orgs.map((org) => ({
accountID: group.account.id,
accountEmail: group.account.email,
accountUrl: group.account.url,
orgID: org.id,
orgName: org.name,
active: !!info && info.id === group.account.id && info.active_org_id === org.id,
})),
),
}
})
const switchConsole = Effect.fn("ExperimentalHttpApi.consoleSwitch")(function* (ctx: {
payload: typeof ConsoleSwitchPayload.Type
}) {
yield* account
.use(ctx.payload.accountID, Option.some(ctx.payload.orgID))
.pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
return true
})
const tool = Effect.fn("ExperimentalHttpApi.tool")(function* (ctx: { query: typeof ToolListQuery.Type }) {
const list = yield* registry.tools({
providerID: ctx.query.provider,
modelID: ctx.query.model,
agent: yield* agents.get(yield* agents.defaultAgent()),
}) })
return list.map((item) => ({
id: item.id,
description: item.description,
parameters: EffectZod.toJsonSchema(item.parameters),
}))
})
const listConsoleOrgs = Effect.fn("ExperimentalHttpApi.consoleOrgs")(function* () { const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () {
const [groups, active] = yield* Effect.all( return yield* registry.ids()
[account.orgsByAccount().pipe(Effect.orDie), account.active().pipe(Effect.orDie)], })
{
concurrency: "unbounded", const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () {
}, const ctx = yield* InstanceState.context
) return yield* project.sandboxes(ctx.project.id)
const info = Option.getOrUndefined(active) })
return {
orgs: groups.flatMap((group) => const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: {
group.orgs.map((org) => ({ payload: Worktree.CreateInput | undefined
accountID: group.account.id, }) {
accountEmail: group.account.email, return yield* worktreeSvc.create(ctx.payload)
accountUrl: group.account.url, })
orgID: org.id,
orgName: org.name, const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: {
active: !!info && info.id === group.account.id && info.active_org_id === org.id, payload: Worktree.RemoveInput
})), }) {
), const ctx = yield* InstanceState.context
} yield* worktreeSvc.remove(input.payload)
yield* project.removeSandbox(ctx.project.id, input.payload.directory)
return true
})
const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: {
payload: Worktree.ResetInput
}) {
yield* worktreeSvc.reset(ctx.payload)
return true
})
const session = Effect.fn("ExperimentalHttpApi.session")(function* (ctx: { query: typeof SessionListQuery.Type }) {
const limit = ctx.query.limit ?? 100
const sessions = Array.from(
Session.listGlobal({
directory: ctx.query.directory,
roots: ctx.query.roots,
start: ctx.query.start,
cursor: ctx.query.cursor,
search: ctx.query.search,
limit: limit + 1,
archived: ctx.query.archived,
}),
)
const list = sessions.length > limit ? sessions.slice(0, limit) : sessions
return HttpServerResponse.jsonUnsafe(list, {
headers:
sessions.length > limit && list.length > 0
? { "x-next-cursor": String(list[list.length - 1].time.updated) }
: undefined,
}) })
})
const switchConsole = Effect.fn("ExperimentalHttpApi.consoleSwitch")(function* (ctx: { const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () {
payload: typeof ConsoleSwitchPayload.Type return yield* mcp.resources()
}) { })
yield* account
.use(ctx.payload.accountID, Option.some(ctx.payload.orgID))
.pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
return true
})
const tool = Effect.fn("ExperimentalHttpApi.tool")(function* (ctx: { query: typeof ToolListQuery.Type }) { return handlers
const list = yield* registry.tools({ .handle("console", getConsole)
providerID: ctx.query.provider, .handle("consoleOrgs", listConsoleOrgs)
modelID: ctx.query.model, .handle("consoleSwitch", switchConsole)
agent: yield* agents.get(yield* agents.defaultAgent()), .handle("tool", tool)
}) .handle("toolIDs", toolIDs)
return list.map((item) => ({ .handle("worktree", worktree)
id: item.id, .handle("worktreeCreate", worktreeCreate)
description: item.description, .handle("worktreeRemove", worktreeRemove)
parameters: EffectZod.toJsonSchema(item.parameters), .handle("worktreeReset", worktreeReset)
})) .handle("session", session)
}) .handle("resource", resource)
}),
const toolIDs = Effect.fn("ExperimentalHttpApi.toolIDs")(function* () {
return yield* registry.ids()
})
const worktree = Effect.fn("ExperimentalHttpApi.worktree")(function* () {
const ctx = yield* InstanceState.context
return yield* project.sandboxes(ctx.project.id)
})
const worktreeCreate = Effect.fn("ExperimentalHttpApi.worktreeCreate")(function* (ctx: {
payload: Worktree.CreateInput | undefined
}) {
return yield* worktreeSvc.create(ctx.payload)
})
const worktreeRemove = Effect.fn("ExperimentalHttpApi.worktreeRemove")(function* (input: {
payload: Worktree.RemoveInput
}) {
const ctx = yield* InstanceState.context
yield* worktreeSvc.remove(input.payload)
yield* project.removeSandbox(ctx.project.id, input.payload.directory)
return true
})
const worktreeReset = Effect.fn("ExperimentalHttpApi.worktreeReset")(function* (ctx: {
payload: Worktree.ResetInput
}) {
yield* worktreeSvc.reset(ctx.payload)
return true
})
const session = Effect.fn("ExperimentalHttpApi.session")(function* (ctx: { query: typeof SessionListQuery.Type }) {
const limit = ctx.query.limit ?? 100
const sessions = Array.from(
Session.listGlobal({
directory: ctx.query.directory,
roots: ctx.query.roots,
start: ctx.query.start,
cursor: ctx.query.cursor,
search: ctx.query.search,
limit: limit + 1,
archived: ctx.query.archived,
}),
)
const list = sessions.length > limit ? sessions.slice(0, limit) : sessions
return HttpServerResponse.jsonUnsafe(list, {
headers:
sessions.length > limit && list.length > 0
? { "x-next-cursor": String(list[list.length - 1].time.updated) }
: undefined,
})
})
const resource = Effect.fn("ExperimentalHttpApi.resource")(function* () {
return yield* mcp.resources()
})
return handlers
.handle("console", getConsole)
.handle("consoleOrgs", listConsoleOrgs)
.handle("consoleSwitch", switchConsole)
.handle("tool", tool)
.handle("toolIDs", toolIDs)
.handle("worktree", worktree)
.handle("worktreeCreate", worktreeCreate)
.handle("worktreeRemove", worktreeRemove)
.handle("worktreeReset", worktreeReset)
.handle("session", session)
.handle("resource", resource)
}),
) )

View file

@ -6,49 +6,49 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api" import { InstanceHttpApi } from "../api"
export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) => export const fileHandlers = HttpApiBuilder.group(InstanceHttpApi, "file", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const svc = yield* File.Service const svc = yield* File.Service
const ripgrep = yield* Ripgrep.Service const ripgrep = yield* Ripgrep.Service
const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) { const findText = Effect.fn("FileHttpApi.findText")(function* (ctx: { query: { pattern: string } }) {
return (yield* ripgrep return (yield* ripgrep
.search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 }) .search({ cwd: (yield* InstanceState.context).directory, pattern: ctx.query.pattern, limit: 10 })
.pipe(Effect.orDie)).items .pipe(Effect.orDie)).items
})
const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: {
query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number }
}) {
return yield* svc.search({
query: ctx.query.query,
limit: ctx.query.limit ?? 10,
dirs: ctx.query.dirs !== "false",
type: ctx.query.type,
}) })
})
const findFile = Effect.fn("FileHttpApi.findFile")(function* (ctx: { const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () {
query: { query: string; dirs?: "true" | "false"; type?: "file" | "directory"; limit?: number } return []
}) { })
return yield* svc.search({
query: ctx.query.query,
limit: ctx.query.limit ?? 10,
dirs: ctx.query.dirs !== "false",
type: ctx.query.type,
})
})
const findSymbol = Effect.fn("FileHttpApi.findSymbol")(function* () { const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) {
return [] return yield* svc.list(ctx.query.path)
}) })
const list = Effect.fn("FileHttpApi.list")(function* (ctx: { query: { path: string } }) { const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) {
return yield* svc.list(ctx.query.path) return yield* svc.read(ctx.query.path)
}) })
const content = Effect.fn("FileHttpApi.content")(function* (ctx: { query: { path: string } }) { const status = Effect.fn("FileHttpApi.status")(function* () {
return yield* svc.read(ctx.query.path) return yield* svc.status()
}) })
const status = Effect.fn("FileHttpApi.status")(function* () { return handlers
return yield* svc.status() .handle("findText", findText)
}) .handle("findFile", findFile)
.handle("findSymbol", findSymbol)
return handlers .handle("list", list)
.handle("findText", findText) .handle("content", content)
.handle("findFile", findFile) .handle("status", status)
.handle("findSymbol", findSymbol) }),
.handle("list", list)
.handle("content", content)
.handle("status", status)
}),
) )

View file

@ -65,92 +65,92 @@ function eventResponse() {
} }
export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handlers) => export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const config = yield* Config.Service const config = yield* Config.Service
const installation = yield* Installation.Service const installation = yield* Installation.Service
const health = Effect.fn("GlobalHttpApi.health")(function* () { const health = Effect.fn("GlobalHttpApi.health")(function* () {
return { healthy: true as const, version: InstallationVersion } return { healthy: true as const, version: InstallationVersion }
})
const event = Effect.fn("GlobalHttpApi.event")(function* () {
return eventResponse()
})
const configGet = Effect.fn("GlobalHttpApi.configGet")(function* () {
return yield* config.getGlobal()
})
const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
return yield* config.updateGlobal(ctx.payload)
})
const dispose = Effect.fn("GlobalHttpApi.dispose")(function* () {
yield* Effect.promise(() => Instance.disposeAll())
GlobalBus.emit("event", {
directory: "global",
payload: { type: "global.disposed", properties: {} },
}) })
return true
})
const event = Effect.fn("GlobalHttpApi.event")(function* () { const upgrade = Effect.fn("GlobalHttpApi.upgrade")(function* (ctx: { payload: typeof GlobalUpgradeInput.Type }) {
return eventResponse() const method = yield* installation.method()
}) if (method === "unknown") {
return {
const configGet = Effect.fn("GlobalHttpApi.configGet")(function* () { status: 400,
return yield* config.getGlobal() body: { success: false as const, error: "Unknown installation method" },
})
const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
return yield* config.updateGlobal(ctx.payload)
})
const dispose = Effect.fn("GlobalHttpApi.dispose")(function* () {
yield* Effect.promise(() => Instance.disposeAll())
GlobalBus.emit("event", {
directory: "global",
payload: { type: "global.disposed", properties: {} },
})
return true
})
const upgrade = Effect.fn("GlobalHttpApi.upgrade")(function* (ctx: { payload: typeof GlobalUpgradeInput.Type }) {
const method = yield* installation.method()
if (method === "unknown") {
return {
status: 400,
body: { success: false as const, error: "Unknown installation method" },
}
} }
const target = ctx.payload.target || (yield* installation.latest(method)) }
const result = yield* installation.upgrade(method, target).pipe( const target = ctx.payload.target || (yield* installation.latest(method))
Effect.as({ status: 200, body: { success: true as const, version: target } }), const result = yield* installation.upgrade(method, target).pipe(
Effect.catch((err) => Effect.as({ status: 200, body: { success: true as const, version: target } }),
Effect.succeed({ Effect.catch((err) =>
status: 500, Effect.succeed({
body: { status: 500,
success: false as const, body: {
error: err instanceof Error ? err.message : String(err), success: false as const,
}, error: err instanceof Error ? err.message : String(err),
}), },
), }),
) ),
if (!result.body.success) return result )
GlobalBus.emit("event", { if (!result.body.success) return result
directory: "global", GlobalBus.emit("event", {
payload: { directory: "global",
type: Installation.Event.Updated.type, payload: {
properties: { version: target }, type: Installation.Event.Updated.type,
}, properties: { version: target },
}) },
return result
}) })
return result
})
const upgradeRaw = Effect.fn("GlobalHttpApi.upgradeRaw")(function* (ctx: { const upgradeRaw = Effect.fn("GlobalHttpApi.upgradeRaw")(function* (ctx: {
request: HttpServerRequest.HttpServerRequest request: HttpServerRequest.HttpServerRequest
}) { }) {
const body = yield* Effect.orDie(ctx.request.text) const body = yield* Effect.orDie(ctx.request.text)
const json = parseBody(body) const json = parseBody(body)
if (json === undefined) { if (json === undefined) {
return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 }) return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 })
} }
const payload = yield* Schema.decodeUnknownEffect(GlobalUpgradeInput)(json).pipe( const payload = yield* Schema.decodeUnknownEffect(GlobalUpgradeInput)(json).pipe(
Effect.map((payload) => ({ valid: true as const, payload })), Effect.map((payload) => ({ valid: true as const, payload })),
Effect.catch(() => Effect.succeed({ valid: false as const })), Effect.catch(() => Effect.succeed({ valid: false as const })),
) )
if (!payload.valid) { if (!payload.valid) {
return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 }) return HttpServerResponse.jsonUnsafe({ success: false, error: "Invalid request body" }, { status: 400 })
} }
const result = yield* upgrade({ payload: payload.payload }) const result = yield* upgrade({ payload: payload.payload })
return HttpServerResponse.jsonUnsafe(result.body, { status: result.status }) return HttpServerResponse.jsonUnsafe(result.body, { status: result.status })
}) })
return handlers return handlers
.handle("health", health) .handle("health", health)
.handleRaw("event", event) .handleRaw("event", event)
.handle("configGet", configGet) .handle("configGet", configGet)
.handle("configUpdate", configUpdate) .handle("configUpdate", configUpdate)
.handle("dispose", dispose) .handle("dispose", dispose)
.handleRaw("upgrade", upgradeRaw) .handleRaw("upgrade", upgradeRaw)
}), }),
) )

View file

@ -12,68 +12,68 @@ import { InstanceHttpApi } from "../api"
import { markInstanceForDisposal } from "../lifecycle" import { markInstanceForDisposal } from "../lifecycle"
export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance", (handlers) => export const instanceHandlers = HttpApiBuilder.group(InstanceHttpApi, "instance", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const agent = yield* Agent.Service const agent = yield* Agent.Service
const command = yield* Command.Service const command = yield* Command.Service
const format = yield* Format.Service const format = yield* Format.Service
const lsp = yield* LSP.Service const lsp = yield* LSP.Service
const skill = yield* Skill.Service const skill = yield* Skill.Service
const vcs = yield* Vcs.Service const vcs = yield* Vcs.Service
const dispose = Effect.fn("InstanceHttpApi.dispose")(function* () { const dispose = Effect.fn("InstanceHttpApi.dispose")(function* () {
yield* markInstanceForDisposal(yield* InstanceState.context) yield* markInstanceForDisposal(yield* InstanceState.context)
return true return true
}) })
const getPath = Effect.fn("InstanceHttpApi.path")(function* () { const getPath = Effect.fn("InstanceHttpApi.path")(function* () {
const ctx = yield* InstanceState.context const ctx = yield* InstanceState.context
return { return {
home: Global.Path.home, home: Global.Path.home,
state: Global.Path.state, state: Global.Path.state,
config: Global.Path.config, config: Global.Path.config,
worktree: ctx.worktree, worktree: ctx.worktree,
directory: ctx.directory, directory: ctx.directory,
} }
}) })
const getVcs = Effect.fn("InstanceHttpApi.vcs")(function* () { const getVcs = Effect.fn("InstanceHttpApi.vcs")(function* () {
const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 }) const [branch, default_branch] = yield* Effect.all([vcs.branch(), vcs.defaultBranch()], { concurrency: 2 })
return { branch, default_branch } return { branch, default_branch }
}) })
const getVcsDiff = Effect.fn("InstanceHttpApi.vcsDiff")(function* (ctx: { query: { mode: Vcs.Mode } }) { const getVcsDiff = Effect.fn("InstanceHttpApi.vcsDiff")(function* (ctx: { query: { mode: Vcs.Mode } }) {
return yield* vcs.diff(ctx.query.mode) return yield* vcs.diff(ctx.query.mode)
}) })
const getCommand = Effect.fn("InstanceHttpApi.command")(function* () { const getCommand = Effect.fn("InstanceHttpApi.command")(function* () {
return yield* command.list() return yield* command.list()
}) })
const getAgent = Effect.fn("InstanceHttpApi.agent")(function* () { const getAgent = Effect.fn("InstanceHttpApi.agent")(function* () {
return yield* agent.list() return yield* agent.list()
}) })
const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () { const getSkill = Effect.fn("InstanceHttpApi.skill")(function* () {
return yield* skill.all() return yield* skill.all()
}) })
const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () { const getLsp = Effect.fn("InstanceHttpApi.lsp")(function* () {
return yield* lsp.status() return yield* lsp.status()
}) })
const getFormatter = Effect.fn("InstanceHttpApi.formatter")(function* () { const getFormatter = Effect.fn("InstanceHttpApi.formatter")(function* () {
return yield* format.status() return yield* format.status()
}) })
return handlers return handlers
.handle("dispose", dispose) .handle("dispose", dispose)
.handle("path", getPath) .handle("path", getPath)
.handle("vcs", getVcs) .handle("vcs", getVcs)
.handle("vcsDiff", getVcsDiff) .handle("vcsDiff", getVcsDiff)
.handle("command", getCommand) .handle("command", getCommand)
.handle("agent", getAgent) .handle("agent", getAgent)
.handle("skill", getSkill) .handle("skill", getSkill)
.handle("lsp", getLsp) .handle("lsp", getLsp)
.handle("formatter", getFormatter) .handle("formatter", getFormatter)
}), }),
) )

View file

@ -5,64 +5,64 @@ import { InstanceHttpApi } from "../api"
import { AddPayload, AuthCallbackPayload, StatusMap, UnsupportedOAuthError } from "../groups/mcp" import { AddPayload, AuthCallbackPayload, StatusMap, UnsupportedOAuthError } from "../groups/mcp"
export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handlers) => export const mcpHandlers = HttpApiBuilder.group(InstanceHttpApi, "mcp", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const mcp = yield* MCP.Service const mcp = yield* MCP.Service
const status = Effect.fn("McpHttpApi.status")(function* () { const status = Effect.fn("McpHttpApi.status")(function* () {
return yield* mcp.status() return yield* mcp.status()
}) })
const add = Effect.fn("McpHttpApi.add")(function* (ctx: { payload: typeof AddPayload.Type }) { const add = Effect.fn("McpHttpApi.add")(function* (ctx: { payload: typeof AddPayload.Type }) {
const result = (yield* mcp.add(ctx.payload.name, ctx.payload.config)).status const result = (yield* mcp.add(ctx.payload.name, ctx.payload.config)).status
return yield* Schema.decodeUnknownEffect(StatusMap)( return yield* Schema.decodeUnknownEffect(StatusMap)(
"status" in result ? { [ctx.payload.name]: result } : result, "status" in result ? { [ctx.payload.name]: result } : result,
).pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) ).pipe(Effect.mapError(() => new HttpApiError.BadRequest({})))
}) })
const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) { const authStart = Effect.fn("McpHttpApi.authStart")(function* (ctx: { params: { name: string } }) {
if (!(yield* mcp.supportsOAuth(ctx.params.name))) { if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` }) return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
} }
return yield* mcp.startAuth(ctx.params.name) return yield* mcp.startAuth(ctx.params.name)
}) })
const authCallback = Effect.fn("McpHttpApi.authCallback")(function* (ctx: { const authCallback = Effect.fn("McpHttpApi.authCallback")(function* (ctx: {
params: { name: string } params: { name: string }
payload: typeof AuthCallbackPayload.Type payload: typeof AuthCallbackPayload.Type
}) { }) {
return yield* mcp.finishAuth(ctx.params.name, ctx.payload.code) return yield* mcp.finishAuth(ctx.params.name, ctx.payload.code)
}) })
const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) { const authAuthenticate = Effect.fn("McpHttpApi.authAuthenticate")(function* (ctx: { params: { name: string } }) {
if (!(yield* mcp.supportsOAuth(ctx.params.name))) { if (!(yield* mcp.supportsOAuth(ctx.params.name))) {
return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` }) return yield* new UnsupportedOAuthError({ error: `MCP server ${ctx.params.name} does not support OAuth` })
} }
return yield* mcp.authenticate(ctx.params.name) return yield* mcp.authenticate(ctx.params.name)
}) })
const authRemove = Effect.fn("McpHttpApi.authRemove")(function* (ctx: { params: { name: string } }) { const authRemove = Effect.fn("McpHttpApi.authRemove")(function* (ctx: { params: { name: string } }) {
yield* mcp.removeAuth(ctx.params.name) yield* mcp.removeAuth(ctx.params.name)
return { success: true as const } return { success: true as const }
}) })
const connect = Effect.fn("McpHttpApi.connect")(function* (ctx: { params: { name: string } }) { const connect = Effect.fn("McpHttpApi.connect")(function* (ctx: { params: { name: string } }) {
yield* mcp.connect(ctx.params.name) yield* mcp.connect(ctx.params.name)
return true return true
}) })
const disconnect = Effect.fn("McpHttpApi.disconnect")(function* (ctx: { params: { name: string } }) { const disconnect = Effect.fn("McpHttpApi.disconnect")(function* (ctx: { params: { name: string } }) {
yield* mcp.disconnect(ctx.params.name) yield* mcp.disconnect(ctx.params.name)
return true return true
}) })
return handlers return handlers
.handle("status", status) .handle("status", status)
.handle("add", add) .handle("add", add)
.handle("authStart", authStart) .handle("authStart", authStart)
.handle("authCallback", authCallback) .handle("authCallback", authCallback)
.handle("authAuthenticate", authAuthenticate) .handle("authAuthenticate", authAuthenticate)
.handle("authRemove", authRemove) .handle("authRemove", authRemove)
.handle("connect", connect) .handle("connect", connect)
.handle("disconnect", disconnect) .handle("disconnect", disconnect)
}), }),
) )

View file

@ -5,25 +5,25 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api" import { InstanceHttpApi } from "../api"
export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permission", (handlers) => export const permissionHandlers = HttpApiBuilder.group(InstanceHttpApi, "permission", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const svc = yield* Permission.Service const svc = yield* Permission.Service
const list = Effect.fn("PermissionHttpApi.list")(function* () { const list = Effect.fn("PermissionHttpApi.list")(function* () {
return yield* svc.list() return yield* svc.list()
})
const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: {
params: { requestID: PermissionID }
payload: Permission.ReplyBody
}) {
yield* svc.reply({
requestID: ctx.params.requestID,
reply: ctx.payload.reply,
message: ctx.payload.message,
}) })
return true
})
const reply = Effect.fn("PermissionHttpApi.reply")(function* (ctx: { return handlers.handle("list", list).handle("reply", reply)
params: { requestID: PermissionID } }),
payload: Permission.ReplyBody
}) {
yield* svc.reply({
requestID: ctx.params.requestID,
reply: ctx.payload.reply,
message: ctx.payload.message,
})
return true
})
return handlers.handle("list", list).handle("reply", reply)
}),
) )

View file

@ -9,38 +9,38 @@ import { InstanceHttpApi } from "../api"
import { markInstanceForReload } from "../lifecycle" import { markInstanceForReload } from "../lifecycle"
export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", (handlers) => export const projectHandlers = HttpApiBuilder.group(InstanceHttpApi, "project", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const svc = yield* Project.Service const svc = yield* Project.Service
const list = Effect.fn("ProjectHttpApi.list")(function* () { const list = Effect.fn("ProjectHttpApi.list")(function* () {
return yield* svc.list() return yield* svc.list()
}) })
const current = Effect.fn("ProjectHttpApi.current")(function* () { const current = Effect.fn("ProjectHttpApi.current")(function* () {
return (yield* InstanceState.context).project return (yield* InstanceState.context).project
}) })
const initGit = Effect.fn("ProjectHttpApi.initGit")(function* () { const initGit = Effect.fn("ProjectHttpApi.initGit")(function* () {
const ctx = yield* InstanceState.context const ctx = yield* InstanceState.context
const next = yield* svc.initGit({ directory: ctx.directory, project: ctx.project }) const next = yield* svc.initGit({ directory: ctx.directory, project: ctx.project })
if (next.id === ctx.project.id && next.vcs === ctx.project.vcs && next.worktree === ctx.project.worktree) if (next.id === ctx.project.id && next.vcs === ctx.project.vcs && next.worktree === ctx.project.worktree)
return next
yield* markInstanceForReload(ctx, {
directory: ctx.directory,
worktree: ctx.directory,
project: next,
init: () => AppRuntime.runPromise(InstanceBootstrap),
})
return next return next
yield* markInstanceForReload(ctx, {
directory: ctx.directory,
worktree: ctx.directory,
project: next,
init: () => AppRuntime.runPromise(InstanceBootstrap),
}) })
return next
})
const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: { const update = Effect.fn("ProjectHttpApi.update")(function* (ctx: {
params: { projectID: ProjectID } params: { projectID: ProjectID }
payload: Project.UpdatePayload payload: Project.UpdatePayload
}) { }) {
return yield* svc.update({ ...ctx.payload, projectID: ctx.params.projectID }) return yield* svc.update({ ...ctx.payload, projectID: ctx.params.projectID })
}) })
return handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update) return handlers.handle("list", list).handle("current", current).handle("initGit", initGit).handle("update", update)
}), }),
) )

View file

@ -10,80 +10,80 @@ import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api" import { InstanceHttpApi } from "../api"
export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider", (handlers) => export const providerHandlers = HttpApiBuilder.group(InstanceHttpApi, "provider", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const cfg = yield* Config.Service const cfg = yield* Config.Service
const provider = yield* Provider.Service const provider = yield* Provider.Service
const svc = yield* ProviderAuth.Service const svc = yield* ProviderAuth.Service
const list = Effect.fn("ProviderHttpApi.list")(function* () { const list = Effect.fn("ProviderHttpApi.list")(function* () {
const config = yield* cfg.get() const config = yield* cfg.get()
const all = yield* Effect.promise(() => ModelsDev.get()) const all = yield* Effect.promise(() => ModelsDev.get())
const disabled = new Set(config.disabled_providers ?? []) const disabled = new Set(config.disabled_providers ?? [])
const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined const enabled = config.enabled_providers ? new Set(config.enabled_providers) : undefined
const filtered: Record<string, (typeof all)[string]> = {} const filtered: Record<string, (typeof all)[string]> = {}
for (const [key, value] of Object.entries(all)) { for (const [key, value] of Object.entries(all)) {
if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) filtered[key] = value if ((enabled ? enabled.has(key) : true) && !disabled.has(key)) filtered[key] = value
} }
const connected = yield* provider.list() const connected = yield* provider.list()
const providers = Object.assign( const providers = Object.assign(
mapValues(filtered, (item) => Provider.fromModelsDevProvider(item)), mapValues(filtered, (item) => Provider.fromModelsDevProvider(item)),
connected, connected,
) )
return { return {
all: Object.values(providers), all: Object.values(providers),
default: Provider.defaultModelIDs(providers), default: Provider.defaultModelIDs(providers),
connected: Object.keys(connected), connected: Object.keys(connected),
} }
}) })
const auth = Effect.fn("ProviderHttpApi.auth")(function* () { const auth = Effect.fn("ProviderHttpApi.auth")(function* () {
return yield* svc.methods() return yield* svc.methods()
}) })
const authorize = Effect.fn("ProviderHttpApi.authorize")(function* (ctx: { const authorize = Effect.fn("ProviderHttpApi.authorize")(function* (ctx: {
params: { providerID: ProviderID } params: { providerID: ProviderID }
payload: ProviderAuth.AuthorizeInput payload: ProviderAuth.AuthorizeInput
}) { }) {
return yield* svc return yield* svc
.authorize({ .authorize({
providerID: ctx.params.providerID, providerID: ctx.params.providerID,
method: ctx.payload.method, method: ctx.payload.method,
inputs: ctx.payload.inputs, inputs: ctx.payload.inputs,
}) })
.pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({})))) .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
}) })
const authorizeRaw = Effect.fn("ProviderHttpApi.authorizeRaw")(function* (ctx: { const authorizeRaw = Effect.fn("ProviderHttpApi.authorizeRaw")(function* (ctx: {
params: { providerID: ProviderID } params: { providerID: ProviderID }
request: HttpServerRequest.HttpServerRequest request: HttpServerRequest.HttpServerRequest
}) { }) {
const body = yield* Effect.orDie(ctx.request.text) const body = yield* Effect.orDie(ctx.request.text)
const payload = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ProviderAuth.AuthorizeInput))(body).pipe( const payload = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(ProviderAuth.AuthorizeInput))(body).pipe(
Effect.mapError(() => new HttpApiError.BadRequest({})), Effect.mapError(() => new HttpApiError.BadRequest({})),
) )
const result = yield* authorize({ params: ctx.params, payload }) const result = yield* authorize({ params: ctx.params, payload })
if (result === undefined) return HttpServerResponse.empty({ status: 200 }) if (result === undefined) return HttpServerResponse.empty({ status: 200 })
return HttpServerResponse.jsonUnsafe(result) return HttpServerResponse.jsonUnsafe(result)
}) })
const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: { const callback = Effect.fn("ProviderHttpApi.callback")(function* (ctx: {
params: { providerID: ProviderID } params: { providerID: ProviderID }
payload: ProviderAuth.CallbackInput payload: ProviderAuth.CallbackInput
}) { }) {
yield* svc yield* svc
.callback({ .callback({
providerID: ctx.params.providerID, providerID: ctx.params.providerID,
method: ctx.payload.method, method: ctx.payload.method,
code: ctx.payload.code, code: ctx.payload.code,
}) })
.pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({})))) .pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))))
return true return true
}) })
return handlers return handlers
.handle("list", list) .handle("list", list)
.handle("auth", auth) .handle("auth", auth)
.handleRaw("authorize", authorizeRaw) .handleRaw("authorize", authorizeRaw)
.handle("callback", callback) .handle("callback", callback)
}), }),
) )

View file

@ -88,7 +88,9 @@ export const ptyConnectRoute = HttpRouter.add(
}, },
send: (data: string | Uint8Array | ArrayBuffer) => { send: (data: string | Uint8Array | ArrayBuffer) => {
if (closed) return if (closed) return
Effect.runFork(write(data instanceof ArrayBuffer ? new Uint8Array(data) : data).pipe(Effect.catch(() => Effect.void))) Effect.runFork(
write(data instanceof ArrayBuffer ? new Uint8Array(data) : data).pipe(Effect.catch(() => Effect.void)),
)
}, },
close: (code?: number, reason?: string) => { close: (code?: number, reason?: string) => {
if (closed) return if (closed) return

View file

@ -5,29 +5,29 @@ import { HttpApiBuilder } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api" import { InstanceHttpApi } from "../api"
export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) => export const questionHandlers = HttpApiBuilder.group(InstanceHttpApi, "question", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
const svc = yield* Question.Service const svc = yield* Question.Service
const list = Effect.fn("QuestionHttpApi.list")(function* () { const list = Effect.fn("QuestionHttpApi.list")(function* () {
return yield* svc.list() return yield* svc.list()
})
const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: {
params: { requestID: QuestionID }
payload: Question.Reply
}) {
yield* svc.reply({
requestID: ctx.params.requestID,
answers: ctx.payload.answers,
}) })
return true
})
const reply = Effect.fn("QuestionHttpApi.reply")(function* (ctx: { const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) {
params: { requestID: QuestionID } yield* svc.reject(ctx.params.requestID)
payload: Question.Reply return true
}) { })
yield* svc.reply({
requestID: ctx.params.requestID,
answers: ctx.payload.answers,
})
return true
})
const reject = Effect.fn("QuestionHttpApi.reject")(function* (ctx: { params: { requestID: QuestionID } }) { return handlers.handle("list", list).handle("reply", reply).handle("reject", reject)
yield* svc.reject(ctx.params.requestID) }),
return true
})
return handlers.handle("list", list).handle("reply", reply).handle("reject", reject)
}),
) )

View file

@ -25,7 +25,20 @@ import * as Stream from "effect/Stream"
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http" import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi" import { HttpApiBuilder, HttpApiError, HttpApiSchema } from "effect/unstable/httpapi"
import { InstanceHttpApi } from "../api" import { InstanceHttpApi } from "../api"
import { CommandPayload, DiffQuery, ForkPayload, InitPayload, ListQuery, MessagesQuery, PermissionResponsePayload, PromptPayload, RevertPayload, ShellPayload, SummarizePayload, UpdatePayload } from "../groups/session" import {
CommandPayload,
DiffQuery,
ForkPayload,
InitPayload,
ListQuery,
MessagesQuery,
PermissionResponsePayload,
PromptPayload,
RevertPayload,
ShellPayload,
SummarizePayload,
UpdatePayload,
} from "../groups/session"
const log = Log.create({ service: "server" }) const log = Log.create({ service: "server" })
@ -88,40 +101,42 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
params: { sessionID: SessionID } params: { sessionID: SessionID }
query: typeof MessagesQuery.Type query: typeof MessagesQuery.Type
}) { }) {
return yield* mapNotFound(Effect.gen(function* () { return yield* mapNotFound(
if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({}) Effect.gen(function* () {
if (ctx.query.before) { if (ctx.query.before && ctx.query.limit === undefined) return yield* new HttpApiError.BadRequest({})
const before = ctx.query.before if (ctx.query.before) {
yield* Effect.try({ const before = ctx.query.before
try: () => MessageV2.cursor.decode(before), yield* Effect.try({
catch: () => new HttpApiError.BadRequest({}), try: () => MessageV2.cursor.decode(before),
}) catch: () => new HttpApiError.BadRequest({}),
} })
if (ctx.query.limit === undefined || ctx.query.limit === 0) { }
if (ctx.query.limit === undefined || ctx.query.limit === 0) {
yield* session.get(ctx.params.sessionID)
return yield* session.messages({ sessionID: ctx.params.sessionID })
}
yield* session.get(ctx.params.sessionID) yield* session.get(ctx.params.sessionID)
return yield* session.messages({ sessionID: ctx.params.sessionID }) const page = MessageV2.page({
} sessionID: ctx.params.sessionID,
limit: ctx.query.limit,
before: ctx.query.before,
})
if (!page.cursor) return page.items
yield* session.get(ctx.params.sessionID) const request = yield* HttpServerRequest.HttpServerRequest
const page = MessageV2.page({ const url = new URL(request.url, "http://localhost")
sessionID: ctx.params.sessionID, url.searchParams.set("limit", ctx.query.limit.toString())
limit: ctx.query.limit, url.searchParams.set("before", page.cursor)
before: ctx.query.before, return HttpServerResponse.jsonUnsafe(page.items, {
}) headers: {
if (!page.cursor) return page.items "Access-Control-Expose-Headers": "Link, X-Next-Cursor",
Link: `<${url.toString()}>; rel="next"`,
const request = yield* HttpServerRequest.HttpServerRequest "X-Next-Cursor": page.cursor,
const url = new URL(request.url, "http://localhost") },
url.searchParams.set("limit", ctx.query.limit.toString()) })
url.searchParams.set("before", page.cursor) }),
return HttpServerResponse.jsonUnsafe(page.items, { )
headers: {
"Access-Control-Expose-Headers": "Link, X-Next-Cursor",
Link: `<${url.toString()}>; rel="next"`,
"X-Next-Cursor": page.cursor,
},
})
}))
}) })
const message = Effect.fn("SessionHttpApi.message")(function* (ctx: { const message = Effect.fn("SessionHttpApi.message")(function* (ctx: {

View file

@ -19,11 +19,12 @@ import * as Socket from "effect/unstable/socket/Socket"
type HandlerEffect = Effect.Effect<HttpServerResponse.HttpServerResponse, unhandled, never> type HandlerEffect = Effect.Effect<HttpServerResponse.HttpServerResponse, unhandled, never>
export class InstanceContextMiddleware extends HttpApiMiddleware.Service<InstanceContextMiddleware, { export class InstanceContextMiddleware extends HttpApiMiddleware.Service<
requires: Session.Service InstanceContextMiddleware,
}>()( {
"@opencode/ExperimentalHttpApiInstanceContext", requires: Session.Service
) {} }
>()("@opencode/ExperimentalHttpApiInstanceContext") {}
function decode(input: string) { function decode(input: string) {
try { try {
@ -53,9 +54,14 @@ function requestHeaders(request: HttpServerRequest.HttpServerRequest) {
return sourceRequest(request).headers return sourceRequest(request).headers
} }
function writeSocket(write: (data: string | Uint8Array | Socket.CloseEvent) => Effect.Effect<void, unknown>, data: unknown) { function writeSocket(
write: (data: string | Uint8Array | Socket.CloseEvent) => Effect.Effect<void, unknown>,
data: unknown,
) {
if (data instanceof Blob) { if (data instanceof Blob) {
void data.arrayBuffer().then((buffer) => Effect.runFork(write(new Uint8Array(buffer)).pipe(Effect.catch(() => Effect.void)))) void data
.arrayBuffer()
.then((buffer) => Effect.runFork(write(new Uint8Array(buffer)).pipe(Effect.catch(() => Effect.void))))
return return
} }
if (typeof data === "string" || data instanceof Uint8Array) { if (typeof data === "string" || data instanceof Uint8Array) {
@ -78,7 +84,8 @@ function proxyWebSocket(request: HttpServerRequest.HttpServerRequest, target: st
queue.length = 0 queue.length = 0
} }
remote.onmessage = (event) => writeSocket(write, event.data) remote.onmessage = (event) => writeSocket(write, event.data)
remote.onerror = () => Effect.runFork(write(new Socket.CloseEvent(1011, "proxy error")).pipe(Effect.catch(() => Effect.void))) remote.onerror = () =>
Effect.runFork(write(new Socket.CloseEvent(1011, "proxy error")).pipe(Effect.catch(() => Effect.void)))
remote.onclose = (event) => remote.onclose = (event) =>
Effect.runFork(write(new Socket.CloseEvent(event.code, event.reason)).pipe(Effect.catch(() => Effect.void))) Effect.runFork(write(new Socket.CloseEvent(event.code, event.reason)).pipe(Effect.catch(() => Effect.void)))
@ -109,7 +116,9 @@ function proxyRemote(
const url = workspaceProxyURL(target.url, requestURL) const url = workspaceProxyURL(target.url, requestURL)
const source = sourceRequest(request) const source = sourceRequest(request)
if (source.headers.get("upgrade")?.toLowerCase() === "websocket") return proxyWebSocket(request, url) if (source.headers.get("upgrade")?.toLowerCase() === "websocket") return proxyWebSocket(request, url)
return Effect.promise(() => ServerProxy.http(url, target.headers, source, workspace.id)).pipe(Effect.map(HttpServerResponse.raw)) return Effect.promise(() => ServerProxy.http(url, target.headers, source, workspace.id)).pipe(
Effect.map(HttpServerResponse.raw),
)
} }
function requestContext() { function requestContext() {
@ -118,14 +127,19 @@ function requestContext() {
) )
} }
function provideRequestContext(effect: HandlerEffect, request: HttpServerRequest.HttpServerRequest, sessionWorkspaceID?: WorkspaceID) { function provideRequestContext(
effect: HandlerEffect,
request: HttpServerRequest.HttpServerRequest,
sessionWorkspaceID?: WorkspaceID,
) {
return Effect.gen(function* () { return Effect.gen(function* () {
const url = new URL(request.url, "http://localhost") const url = new URL(request.url, "http://localhost")
const headers = requestHeaders(request) const headers = requestHeaders(request)
const envWorkspaceID = Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined const envWorkspaceID = Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined
const workspaceParam = url.searchParams.get("workspace") const workspaceParam = url.searchParams.get("workspace")
const workspaceID = sessionWorkspaceID ?? (workspaceParam ? WorkspaceID.make(workspaceParam) : undefined) const workspaceID = sessionWorkspaceID ?? (workspaceParam ? WorkspaceID.make(workspaceParam) : undefined)
const workspace = workspaceID && !envWorkspaceID ? yield* Effect.promise(() => Workspace.get(workspaceID)) : undefined const workspace =
workspaceID && !envWorkspaceID ? yield* Effect.promise(() => Workspace.get(workspaceID)) : undefined
if (workspaceID && !workspace && !envWorkspaceID) { if (workspaceID && !workspace && !envWorkspaceID) {
return HttpServerResponse.text(`Workspace not found: ${workspaceID}`, { return HttpServerResponse.text(`Workspace not found: ${workspaceID}`, {
@ -134,7 +148,12 @@ function provideRequestContext(effect: HandlerEffect, request: HttpServerRequest
}) })
} }
if (workspace && !isLocalWorkspaceRoute(request.method, url.pathname) && !url.pathname.startsWith("/console") && !envWorkspaceID) { if (
workspace &&
!isLocalWorkspaceRoute(request.method, url.pathname) &&
!url.pathname.startsWith("/console") &&
!envWorkspaceID
) {
const adaptor = yield* Effect.promise(() => getAdaptor(workspace.projectID, workspace.type)) const adaptor = yield* Effect.promise(() => getAdaptor(workspace.projectID, workspace.type))
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace))) const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace)))
if (target.type === "remote") return yield* proxyRemote(request, workspace, target, url) if (target.type === "remote") return yield* proxyRemote(request, workspace, target, url)
@ -186,6 +205,8 @@ export const instanceContextLayer = Layer.succeed(
InstanceContextMiddleware.of((effect) => provideInstanceContext(effect)), InstanceContextMiddleware.of((effect) => provideInstanceContext(effect)),
) )
export const instanceRouterLayer = HttpRouter.middleware()(Effect.succeed((effect) => export const instanceRouterLayer = HttpRouter.middleware()(
requestContext().pipe(Effect.flatMap((request) => provideRequestContext(effect, request))), Effect.succeed((effect) =>
)).layer requestContext().pipe(Effect.flatMap((request) => provideRequestContext(effect, request))),
),
).layer

View file

@ -14,7 +14,10 @@ export const markInstanceForDisposal = (ctx: InstanceContext) =>
export const markInstanceForReload = (ctx: InstanceContext, next: Parameters<typeof Instance.reload>[0]) => export const markInstanceForReload = (ctx: InstanceContext, next: Parameters<typeof Instance.reload>[0]) =>
HttpEffect.appendPreResponseHandler((_request, response) => HttpEffect.appendPreResponseHandler((_request, response) =>
Effect.as(Effect.uninterruptible(Effect.promise(() => Instance.restore(ctx, () => Instance.reload(next)))), response), Effect.as(
Effect.uninterruptible(Effect.promise(() => Instance.restore(ctx, () => Instance.reload(next)))),
response,
),
) )
export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) => export const disposeMiddleware: HttpMiddleware.HttpMiddleware = (effect) =>

View file

@ -126,8 +126,13 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
// Workspace creation fields `branch` and `extra` are Schema.NullOr — // Workspace creation fields `branch` and `extra` are Schema.NullOr —
// genuinely nullable, not just optional. Re-add the null that the // genuinely nullable, not just optional. Re-add the null that the
// component-level strip above removed. // component-level strip above removed.
const ref = operation.requestBody.content?.["application/json"]?.schema?.$ref?.replace("#/components/schemas/", "") const ref = operation.requestBody.content?.["application/json"]?.schema?.$ref?.replace(
const properties = ref ? spec.components?.schemas?.[ref]?.properties : operation.requestBody.content?.["application/json"]?.schema?.properties "#/components/schemas/",
"",
)
const properties = ref
? spec.components?.schemas?.[ref]?.properties
: operation.requestBody.content?.["application/json"]?.schema?.properties
if (properties?.branch) properties.branch = { anyOf: [properties.branch, { type: "null" }] } if (properties?.branch) properties.branch = { anyOf: [properties.branch, { type: "null" }] }
if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] } if (properties?.extra) properties.extra = { anyOf: [properties.extra, { type: "null" }] }
} }
@ -150,7 +155,10 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
description: "Event stream", description: "Event stream",
content: { content: {
"text/event-stream": { "text/event-stream": {
schema: path === "/event" ? { $ref: "#/components/schemas/Event" } : { $ref: "#/components/schemas/GlobalEvent" }, schema:
path === "/event"
? { $ref: "#/components/schemas/Event" }
: { $ref: "#/components/schemas/GlobalEvent" },
}, },
}, },
} }
@ -251,7 +259,8 @@ function applyLegacySchemaOverrides(spec: OpenApiSpec) {
schemas.Workspace.properties.directory = nullable(schemas.Workspace.properties.directory) schemas.Workspace.properties.directory = nullable(schemas.Workspace.properties.directory)
schemas.Workspace.properties.extra = nullable(schemas.Workspace.properties.extra) schemas.Workspace.properties.extra = nullable(schemas.Workspace.properties.extra)
} }
if (schemas.GlobalSession?.properties?.project) schemas.GlobalSession.properties.project = nullable(schemas.GlobalSession.properties.project) if (schemas.GlobalSession?.properties?.project)
schemas.GlobalSession.properties.project = nullable(schemas.GlobalSession.properties.project)
const providerOptions = schemas.ProviderConfig?.properties?.options const providerOptions = schemas.ProviderConfig?.properties?.options
if (providerOptions) providerOptions.additionalProperties = {} if (providerOptions) providerOptions.additionalProperties = {}
const model = schemas.ProviderConfig?.properties?.models?.additionalProperties const model = schemas.ProviderConfig?.properties?.models?.additionalProperties
@ -486,12 +495,11 @@ function normalizeParameter(param: OpenApiParameter, route: string) {
param.schema = stripOptionalNull(param.schema) param.schema = stripOptionalNull(param.schema)
} }
export const PublicApi = OpenCodeHttpApi export const PublicApi = OpenCodeHttpApi.annotateMerge(
.annotateMerge( OpenApi.annotations({
OpenApi.annotations({ title: "opencode",
title: "opencode", version: "1.0.0",
version: "1.0.0", description: "opencode api",
description: "opencode api", transform: matchLegacyOpenApi,
transform: matchLegacyOpenApi, }),
}), )
)

View file

@ -38,7 +38,9 @@ type ServerApp = {
request(input: string | URL | Request, init?: RequestInit): Response | Promise<Response> request(input: string | URL | Request, init?: RequestInit): Response | Promise<Response>
} }
const DefaultHono = lazy(() => withBackend({ backend: "hono", reason: "stable" }, createHono({}, { backend: "hono", reason: "stable" }))) const DefaultHono = lazy(() =>
withBackend({ backend: "hono", reason: "stable" }, createHono({}, { backend: "hono", reason: "stable" })),
)
const DefaultHttpApi = lazy(() => createDefaultHttpApi()) const DefaultHttpApi = lazy(() => createDefaultHttpApi())
function select() { function select() {
@ -86,7 +88,10 @@ function createHttpApi() {
} }
} }
function createHono(opts: { cors?: string[] }, selection: ServerBackend.Selection = ServerBackend.force(select(), "hono")) { function createHono(
opts: { cors?: string[] },
selection: ServerBackend.Selection = ServerBackend.force(select(), "hono"),
) {
const backendAttributes = ServerBackend.attributes(selection) const backendAttributes = ServerBackend.attributes(selection)
const app = new Hono() const app = new Hono()
.onError(ErrorMiddleware) .onError(ErrorMiddleware)

View file

@ -461,7 +461,9 @@ type AssistantError = z.infer<typeof AssistantErrorZod>
// Effect Schema for the same union — used by HttpApi OpenAPI generation. // Effect Schema for the same union — used by HttpApi OpenAPI generation.
const AssistantErrorSchema = Schema.Union([ const AssistantErrorSchema = Schema.Union([
AuthError.EffectSchema, AuthError.EffectSchema,
Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({ identifier: "UnknownError" }), Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({
identifier: "UnknownError",
}),
OutputLengthError.EffectSchema, OutputLengthError.EffectSchema,
AbortedError.EffectSchema, AbortedError.EffectSchema,
StructuredOutputError.EffectSchema, StructuredOutputError.EffectSchema,

View file

@ -11,8 +11,6 @@ export const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
*/ */
export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
/** /**
* Optional public JSON field that can hold explicit `undefined` on the type * Optional public JSON field that can hold explicit `undefined` on the type
* side but encodes it as an omitted key, matching legacy `JSON.stringify`. * side but encodes it as an omitted key, matching legacy `JSON.stringify`.

View file

@ -126,9 +126,9 @@ function requestBodyKey(spec: OpenApiSpec, body: unknown) {
function requestBodySchemaKind(spec: OpenApiSpec, schema: OpenApiSchema | undefined) { function requestBodySchemaKind(spec: OpenApiSpec, schema: OpenApiSchema | undefined) {
if (!schema) return "" if (!schema) return ""
const resolved = (schema.$ref ? spec.components?.schemas?.[schema.$ref.replace("#/components/schemas/", "")] : schema) as const resolved = (
| OpenApiSchema schema.$ref ? spec.components?.schemas?.[schema.$ref.replace("#/components/schemas/", "")] : schema
| undefined ) as OpenApiSchema | undefined
if (resolved?.properties) return "object" if (resolved?.properties) return "object"
if (resolved?.anyOf ?? resolved?.oneOf ?? resolved?.allOf) return "object" if (resolved?.anyOf ?? resolved?.oneOf ?? resolved?.allOf) return "object"
return resolved?.type ?? schema.type ?? "inline" return resolved?.type ?? schema.type ?? "inline"

View file

@ -214,7 +214,11 @@ describe("workspace HttpApi", () => {
const workspace = await Instance.provide({ const workspace = await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
registerAdaptor(Instance.project.id, "remote-target", remoteAdaptor(path.join(tmp.path, ".remote"), "https://remote.test/base")) registerAdaptor(
Instance.project.id,
"remote-target",
remoteAdaptor(path.join(tmp.path, ".remote"), "https://remote.test/base"),
)
return Workspace.create({ return Workspace.create({
type: "remote-target", type: "remote-target",
branch: null, branch: null,

View file

@ -1339,10 +1339,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"rows": { "rows": {
"type": "number" "type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
}, },
"cols": { "cols": {
"type": "number" "type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["rows", "cols"] "required": ["rows", "cols"]
@ -5595,10 +5599,14 @@
"required": ["text"] "required": ["text"]
}, },
"line_number": { "line_number": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"absolute_offset": { "absolute_offset": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"submatches": { "submatches": {
"type": "array", "type": "array",
@ -5615,10 +5623,14 @@
"required": ["text"] "required": ["text"]
}, },
"start": { "start": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"end": { "end": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["match", "start", "end"] "required": ["match", "start", "end"]
@ -6901,7 +6913,9 @@
}, },
"duration": { "duration": {
"description": "Duration in milliseconds", "description": "Duration in milliseconds",
"type": "number" "type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["message", "variant"] "required": ["message", "variant"]
@ -7621,13 +7635,19 @@
"type": "object", "type": "object",
"properties": { "properties": {
"created": { "created": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"updated": { "updated": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"initialized": { "initialized": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["created", "updated"] "required": ["created", "updated"]
@ -7913,10 +7933,14 @@
"type": "string" "type": "string"
}, },
"additions": { "additions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"deletions": { "deletions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"status": { "status": {
"type": "string", "type": "string",
@ -8039,7 +8063,9 @@
"type": "string" "type": "string"
}, },
"retries": { "retries": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["message", "retries"] "required": ["message", "retries"]
@ -8083,7 +8109,9 @@
"type": "string" "type": "string"
}, },
"statusCode": { "statusCode": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"isRetryable": { "isRetryable": {
"type": "boolean" "type": "boolean"
@ -8420,13 +8448,17 @@
"const": "retry" "const": "retry"
}, },
"attempt": { "attempt": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"message": { "message": {
"type": "string" "type": "string"
}, },
"next": { "next": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["type", "attempt", "message", "next"] "required": ["type", "attempt", "message", "next"]
@ -8591,7 +8623,9 @@
}, },
"duration": { "duration": {
"description": "Duration in milliseconds", "description": "Duration in milliseconds",
"type": "number" "type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["message", "variant"] "required": ["message", "variant"]
@ -8777,7 +8811,9 @@
"enum": ["running", "exited"] "enum": ["running", "exited"]
}, },
"pid": { "pid": {
"type": "number" "type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["id", "title", "command", "args", "cwd", "status", "pid"] "required": ["id", "title", "command", "args", "cwd", "status", "pid"]
@ -8835,7 +8871,9 @@
"pattern": "^pty.*" "pattern": "^pty.*"
}, },
"exitCode": { "exitCode": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["id", "exitCode"] "required": ["id", "exitCode"]
@ -9024,7 +9062,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"created": { "created": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["created"] "required": ["created"]
@ -9102,10 +9142,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"created": { "created": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"completed": { "completed": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["created"] "required": ["created"]
@ -9173,25 +9217,37 @@
"type": "object", "type": "object",
"properties": { "properties": {
"total": { "total": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"input": { "input": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"output": { "output": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"reasoning": { "reasoning": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"cache": { "cache": {
"type": "object", "type": "object",
"properties": { "properties": {
"read": { "read": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"write": { "write": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["read", "write"] "required": ["read", "write"]
@ -9311,10 +9367,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"start": { "start": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"end": { "end": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["start"] "required": ["start"]
@ -9408,10 +9468,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"start": { "start": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"end": { "end": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["start"] "required": ["start"]
@ -9427,12 +9491,12 @@
}, },
"start": { "start": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
}, },
"end": { "end": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
} }
}, },
@ -9461,10 +9525,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"line": { "line": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"character": { "character": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["line", "character"] "required": ["line", "character"]
@ -9473,10 +9541,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"line": { "line": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"character": { "character": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["line", "character"] "required": ["line", "character"]
@ -9505,7 +9577,7 @@
}, },
"kind": { "kind": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
} }
}, },
@ -9625,7 +9697,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"start": { "start": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["start"] "required": ["start"]
@ -9664,13 +9738,19 @@
"type": "object", "type": "object",
"properties": { "properties": {
"start": { "start": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"end": { "end": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"compacted": { "compacted": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["start", "end"] "required": ["start", "end"]
@ -9712,10 +9792,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"start": { "start": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"end": { "end": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["start", "end"] "required": ["start", "end"]
@ -9834,25 +9918,37 @@
"type": "object", "type": "object",
"properties": { "properties": {
"total": { "total": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"input": { "input": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"output": { "output": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"reasoning": { "reasoning": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"cache": { "cache": {
"type": "object", "type": "object",
"properties": { "properties": {
"read": { "read": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"write": { "write": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["read", "write"] "required": ["read", "write"]
@ -9949,12 +10045,12 @@
}, },
"start": { "start": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
}, },
"end": { "end": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
} }
}, },
@ -9983,7 +10079,9 @@
"const": "retry" "const": "retry"
}, },
"attempt": { "attempt": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"error": { "error": {
"$ref": "#/components/schemas/APIError" "$ref": "#/components/schemas/APIError"
@ -9992,7 +10090,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"created": { "created": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["created"] "required": ["created"]
@ -10090,7 +10190,9 @@
"$ref": "#/components/schemas/Part" "$ref": "#/components/schemas/Part"
}, },
"time": { "time": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["sessionID", "part", "time"] "required": ["sessionID", "part", "time"]
@ -10182,13 +10284,19 @@
"type": "object", "type": "object",
"properties": { "properties": {
"additions": { "additions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"deletions": { "deletions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"files": { "files": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"diffs": { "diffs": {
"type": "array", "type": "array",
@ -10218,16 +10326,24 @@
"type": "object", "type": "object",
"properties": { "properties": {
"created": { "created": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"updated": { "updated": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"compacting": { "compacting": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"archived": { "archived": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["created", "updated"] "required": ["created", "updated"]
@ -10434,7 +10550,9 @@
"$ref": "#/components/schemas/Part" "$ref": "#/components/schemas/Part"
}, },
"time": { "time": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["sessionID", "part", "time"] "required": ["sessionID", "part", "time"]
@ -10631,13 +10749,19 @@
"type": "object", "type": "object",
"properties": { "properties": {
"additions": { "additions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"deletions": { "deletions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"files": { "files": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"diffs": { "diffs": {
"type": "array", "type": "array",
@ -10694,7 +10818,9 @@
"created": { "created": {
"anyOf": [ "anyOf": [
{ {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
{ {
"type": "null" "type": "null"
@ -10704,7 +10830,9 @@
"updated": { "updated": {
"anyOf": [ "anyOf": [
{ {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
{ {
"type": "null" "type": "null"
@ -10714,7 +10842,9 @@
"compacting": { "compacting": {
"anyOf": [ "anyOf": [
{ {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
{ {
"type": "null" "type": "null"
@ -10724,7 +10854,9 @@
"archived": { "archived": {
"anyOf": [ "anyOf": [
{ {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
{ {
"type": "null" "type": "null"
@ -11481,7 +11613,9 @@
}, },
"timeout": { "timeout": {
"description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
"type": "number" "type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["type", "command"] "required": ["type", "command"]
@ -11547,7 +11681,9 @@
}, },
"timeout": { "timeout": {
"description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.",
"type": "number" "type": "integer",
"exclusiveMinimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["type", "url"] "required": ["type", "url"]
@ -12055,7 +12191,9 @@
"type": "string" "type": "string"
}, },
"expires": { "expires": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"accountId": { "accountId": {
"type": "string" "type": "string"
@ -12458,7 +12596,9 @@
"type": "string" "type": "string"
}, },
"switchableOrgCount": { "switchableOrgCount": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["consoleManagedProviders", "switchableOrgCount"] "required": ["consoleManagedProviders", "switchableOrgCount"]
@ -12579,13 +12719,19 @@
"type": "object", "type": "object",
"properties": { "properties": {
"additions": { "additions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"deletions": { "deletions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"files": { "files": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"diffs": { "diffs": {
"type": "array", "type": "array",
@ -12615,16 +12761,24 @@
"type": "object", "type": "object",
"properties": { "properties": {
"created": { "created": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"updated": { "updated": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"compacting": { "compacting": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"archived": { "archived": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["created", "updated"] "required": ["created", "updated"]
@ -12710,10 +12864,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"start": { "start": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"end": { "end": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
} }
}, },
"required": ["start"] "required": ["start"]
@ -12776,12 +12934,12 @@
}, },
"start": { "start": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
}, },
"end": { "end": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
} }
}, },
@ -12956,7 +13114,9 @@
"type": "string" "type": "string"
}, },
"kind": { "kind": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"location": { "location": {
"type": "object", "type": "object",
@ -13029,16 +13189,24 @@
"type": "object", "type": "object",
"properties": { "properties": {
"oldStart": { "oldStart": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"oldLines": { "oldLines": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"newStart": { "newStart": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"newLines": { "newLines": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"lines": { "lines": {
"type": "array", "type": "array",
@ -13074,12 +13242,12 @@
}, },
"added": { "added": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
}, },
"removed": { "removed": {
"type": "integer", "type": "integer",
"minimum": -9007199254740991, "minimum": 0,
"maximum": 9007199254740991 "maximum": 9007199254740991
}, },
"status": { "status": {
@ -13360,10 +13528,14 @@
"type": "string" "type": "string"
}, },
"additions": { "additions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"deletions": { "deletions": {
"type": "number" "type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}, },
"status": { "status": {
"type": "string", "type": "string",