feat(server): add loaded locations debug endpoint

This commit is contained in:
Dax Raad 2026-07-05 15:58:43 -04:00
commit a75978815a
13 changed files with 118 additions and 1 deletions

View file

@ -862,6 +862,13 @@ export interface VcsApi<E = never> {
readonly diff: VcsDiffOperation<E>
}
export type Endpoint25_0Output = EffectValue<ReturnType<RawClient["server.debug"]["debug.location"]>>
export type DebugLocationOperation<E = never> = () => Effect.Effect<Endpoint25_0Output, E>
export interface DebugApi<E = never> {
readonly location: DebugLocationOperation<E>
}
export interface AppApi<E = never> {
readonly health: HealthApi<E>
readonly location: LocationApi<E>
@ -888,4 +895,5 @@ export interface AppApi<E = never> {
readonly reference: ReferenceApi<E>
readonly projectCopy: ProjectCopyApi<E>
readonly vcs: VcsApi<E>
readonly debug: DebugApi<E>
}

View file

@ -1043,6 +1043,11 @@ const Endpoint24_1 = (raw: RawClient["server.vcs"]) => (input: Endpoint24_1Input
const adaptGroup24 = (raw: RawClient["server.vcs"]) => ({ status: Endpoint24_0(raw), diff: Endpoint24_1(raw) })
const Endpoint25_0 = (raw: RawClient["server.debug"]) => () =>
raw["debug.location"]({}).pipe(Effect.mapError(mapClientError))
const adaptGroup25 = (raw: RawClient["server.debug"]) => ({ location: Endpoint25_0(raw) })
const adaptClient = (raw: RawClient) => ({
health: adaptGroup0(raw["server.health"]),
location: adaptGroup1(raw["server.location"]),
@ -1069,6 +1074,7 @@ const adaptClient = (raw: RawClient) => ({
reference: adaptGroup22(raw["server.reference"]),
projectCopy: adaptGroup23(raw["server.projectCopy"]),
vcs: adaptGroup24(raw["server.vcs"]),
debug: adaptGroup25(raw["server.debug"]),
})
export const make = (options?: { readonly baseUrl?: URL | string }) =>

View file

@ -173,6 +173,7 @@ import type {
VcsStatusOutput,
VcsDiffInput,
VcsDiffOutput,
DebugLocationOutput,
} from "./types"
import { ClientError } from "./client-error"
@ -1448,6 +1449,19 @@ export function make(options: ClientOptions) {
requestOptions,
),
},
debug: {
location: (requestOptions?: RequestOptions) =>
request<DebugLocationOutput>(
{
method: "GET",
path: `/api/debug/location`,
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
}
}

View file

@ -6003,3 +6003,5 @@ export type VcsDiffOutput = {
readonly status?: "added" | "deleted" | "modified"
}>
}
export type DebugLocationOutput = ReadonlyArray<{ readonly directory: string; readonly workspaceID?: string }>

View file

@ -30,7 +30,9 @@ test("exposes every standard HTTP API group", () => {
"reference",
"projectCopy",
"vcs",
"debug",
])
expect(Object.keys(client.debug)).toEqual(["location"])
expect(Object.keys(client.message)).toEqual(["list"])
expect(Object.keys(client.integration)).toEqual([
"list",

View file

@ -16,6 +16,7 @@ import type { Definition } from "@opencode-ai/schema/event"
import { AgentGroup } from "./groups/agent.js"
import { PluginGroup } from "./groups/plugin.js"
import { HealthGroup } from "./groups/health.js"
import { DebugGroup } from "./groups/debug.js"
import { PtyGroup } from "./groups/pty.js"
import { ShellGroup } from "./groups/shell.js"
import { makeQuestionGroup } from "./groups/question.js"
@ -81,6 +82,7 @@ type ApiGroups<
Event extends HttpApiGroup.Any,
> =
| typeof HealthGroup
| typeof DebugGroup
| LocationGroups<LocationId>
| FormGroups<LocationId, LocationService, FormLocationId, FormLocationService>
| SessionGroups<SessionLocationId, SessionLocationService>
@ -165,6 +167,7 @@ const makeApiFromGroup = <
.add(ReferenceGroup.middleware(locationMiddleware))
.add(ProjectCopyGroup.middleware(locationMiddleware))
.add(VcsGroup.middleware(locationMiddleware))
.add(DebugGroup)
.annotateMerge(
OpenApi.annotations({
title: "opencode HttpApi",

View file

@ -34,6 +34,7 @@ export const ClientApi: ClientApiShape = makeDefaultApi({
export const groupNames = {
"server.health": "health",
"server.debug": "debug",
"server.location": "location",
"server.agent": "agent",
"server.plugin": "plugin",

View file

@ -0,0 +1,15 @@
import { Location } from "@opencode-ai/schema/location"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
export const DebugGroup = HttpApiGroup.make("server.debug").add(
HttpApiEndpoint.get("debug.location", "/api/debug/location", {
success: Schema.Array(Location.Ref),
}).annotateMerge(
OpenApi.annotations({
identifier: "v2.debug.location",
summary: "List loaded locations",
description: "List locations currently loaded by the server.",
}),
),
)

View file

@ -276,6 +276,8 @@ import type {
V2CredentialRemoveResponses,
V2CredentialUpdateErrors,
V2CredentialUpdateResponses,
V2DebugLocationErrors,
V2DebugLocationResponses,
V2EventChangesErrors,
V2EventChangesResponses,
V2EventSubscribeErrors,
@ -8036,6 +8038,20 @@ export class Vcs2 extends HeyApiClient {
}
}
export class Debug extends HeyApiClient {
/**
* List loaded locations
*
* List locations currently loaded by the server.
*/
public location<ThrowOnError extends boolean = false>(options?: Options<never, ThrowOnError>) {
return (options?.client ?? this.client).get<V2DebugLocationResponses, V2DebugLocationErrors, ThrowOnError>({
url: "/api/debug/location",
...options,
})
}
}
export class V2 extends HeyApiClient {
private _health?: Health
get health(): Health {
@ -8156,6 +8172,11 @@ export class V2 extends HeyApiClient {
get vcs(): Vcs2 {
return (this._vcs ??= new Vcs2({ client: this.client }))
}
private _debug?: Debug
get debug(): Debug {
return (this._debug ??= new Debug({ client: this.client }))
}
}
export class OpencodeClient extends HeyApiClient {

View file

@ -19054,6 +19054,35 @@ export type V2VcsDiffResponses = {
export type V2VcsDiffResponse = V2VcsDiffResponses[keyof V2VcsDiffResponses]
export type V2DebugLocationData = {
body?: never
path?: never
query?: never
url: "/api/debug/location"
}
export type V2DebugLocationErrors = {
/**
* InvalidRequestError
*/
400: InvalidRequestErrorV2
/**
* UnauthorizedError
*/
401: UnauthorizedErrorV2
}
export type V2DebugLocationError = V2DebugLocationErrors[keyof V2DebugLocationErrors]
export type V2DebugLocationResponses = {
/**
* Success
*/
200: Array<LocationRef2>
}
export type V2DebugLocationResponse = V2DebugLocationResponses[keyof V2DebugLocationResponses]
export type PtyConnectData = {
body?: never
path: {

View file

@ -13,6 +13,7 @@ import { EventHandler } from "./handlers/event"
import { AgentHandler } from "./handlers/agent"
import { PluginHandler } from "./handlers/plugin"
import { HealthHandler } from "./handlers/health"
import { DebugHandler } from "./handlers/debug"
import { PtyHandler } from "./handlers/pty"
import { ShellHandler } from "./handlers/shell"
import { QuestionHandler } from "./handlers/question"
@ -27,6 +28,7 @@ import { VcsHandler } from "./handlers/vcs"
export const handlers = Layer.mergeAll(
HealthHandler,
DebugHandler,
LocationHandler,
AgentHandler,
PluginHandler,

View file

@ -0,0 +1,14 @@
import { LocationServiceMap } from "@opencode-ai/core/location-service-map"
import { Effect, Option, RcMap } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
export const DebugHandler = HttpApiBuilder.group(Api, "server.debug", (handlers) =>
handlers.handle(
"debug.location",
Effect.fn(function* () {
const locations = Option.getOrThrow(yield* Effect.serviceOption(LocationServiceMap.Service))
return Array.from(yield* RcMap.keys(locations.rcMap))
}),
),
)

View file

@ -83,7 +83,7 @@ function makeRoutes<AuthError, AuthServices>(
: AppNodeBuilder.build(applicationServices, replacements)
return HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers),
Layer.provide(handlers.pipe(Layer.provide(serviceLayer))),
Layer.provide(formLocationLayer),
Layer.provide(sessionLocationLayer),
Layer.provide(layer),