feat(core): add plugin list endpoint

Adds a plugin.list HTTP endpoint that reports currently loaded plugins, wires it through protocol, server, and generated clients, and merges it into sdk-next's plugin API. Also updates the sample plugin to register a primary agent instead of a subagent for local testing.
This commit is contained in:
Dax Raad 2026-07-01 00:53:22 -04:00
commit 6a91a682e4
13 changed files with 998 additions and 883 deletions

View file

@ -3,9 +3,9 @@ export default {
setup: async (ctx) => {
await ctx.agent.transform((agents) => {
agents.update("sample-plugin-agent", (agent) => {
agent.description = "Example subagent registered by .opencode/plugins/sample-agent.ts"
agent.mode = "subagent"
agent.prompt = [
agent.description = "Example primary agent registered by .opencode/plugins/sample-agent.ts"
agent.mode = "primary"
agent.system = [
"You are the sample plugin agent for this repository.",
"Use this agent to verify that local plugin auto-discovery can add agents.",
"Keep responses concise and explain which plugin registered you when asked.",

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,8 @@ import type {
LocationGetOutput,
AgentListInput,
AgentListOutput,
PluginListInput,
PluginListOutput,
SessionListInput,
SessionListOutput,
SessionCreateInput,
@ -310,6 +312,20 @@ export function make(options: ClientOptions) {
requestOptions,
),
},
plugin: {
list: (input?: PluginListInput, requestOptions?: RequestOptions) =>
request<PluginListOutput>(
{
method: "GET",
path: `/api/plugin`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
session: {
list: (input?: SessionListInput, requestOptions?: RequestOptions) =>
request<SessionListOutput>(

View file

@ -168,6 +168,21 @@ export type AgentListOutput = {
}>
}
export type PluginListInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}
export type PluginListOutput = {
readonly location: {
readonly directory: string
readonly workspaceID?: string
readonly project: { readonly id: string; readonly directory: string }
}
readonly data: ReadonlyArray<{ readonly id: string }>
}
export type SessionListInput = {
readonly workspace?: {
readonly workspace?: string | undefined

View file

@ -21,12 +21,15 @@ import { ToolRegistry } from "./tool/registry"
export const ID = Plugin.ID
export type ID = typeof ID.Type
export const Info = Plugin.Info
export type Info = Plugin.Info
export const Event = Plugin.Event
export interface Interface {
readonly add: (id: ID, effect: PluginDefinition["effect"]) => Effect.Effect<void>
readonly remove: (id: ID) => Effect.Effect<void>
readonly wait: (id: ID) => Effect.Effect<void>
readonly list: () => Effect.Effect<Info[]>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
@ -139,6 +142,9 @@ export const layer = Layer.effect(
add,
remove,
wait,
list: Effect.fn("Plugin.list")(function* () {
return Array.from(active.keys()).map((id) => ({ id }))
}),
})
host = yield* PluginHost.make(service)
return service

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,7 @@ import { SkillGroup } from "./groups/skill"
import { EventGroup, makeEventGroup } from "./groups/event"
import type { Definition } from "@opencode-ai/schema/event"
import { AgentGroup } from "./groups/agent"
import { PluginGroup } from "./groups/plugin"
import { HealthGroup } from "./groups/health"
import { PtyGroup } from "./groups/pty"
import { ShellGroup } from "./groups/shell"
@ -42,6 +43,7 @@ const makeApiFromGroup = <
.add(HealthGroup)
.add(LocationGroup.middleware(locationMiddleware))
.add(AgentGroup.middleware(locationMiddleware))
.add(PluginGroup.middleware(locationMiddleware))
.add(makeSessionGroup(sessionLocationMiddleware))
.add(MessageGroup.middleware(sessionLocationMiddleware))
.add(ModelGroup.middleware(locationMiddleware))

View file

@ -20,6 +20,7 @@ export const groupNames = {
"server.health": "health",
"server.location": "location",
"server.agent": "agent",
"server.plugin": "plugin",
"server.session": "session",
"server.message": "message",
"server.model": "model",

View file

@ -0,0 +1,27 @@
import { Location } from "@opencode-ai/schema/location"
import { Plugin } from "@opencode-ai/schema/plugin"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
import { LocationQuery, locationQueryOpenApi } from "./location"
export const PluginGroup = HttpApiGroup.make("server.plugin")
.add(
HttpApiEndpoint.get("plugin.list", "/api/plugin", {
query: LocationQuery,
success: Location.response(Schema.Array(Plugin.Info)),
})
.annotateMerge(locationQueryOpenApi)
.annotateMerge(
OpenApi.annotations({
identifier: "v2.plugin.list",
summary: "List plugins",
description: "Retrieve currently loaded plugins.",
}),
),
)
.annotateMerge(
OpenApi.annotations({
title: "plugins",
description: "Experimental plugin routes.",
}),
)

View file

@ -6,6 +6,11 @@ import { define, inventory } from "./event"
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
export type ID = typeof ID.Type
export interface Info extends Schema.Schema.Type<typeof Info> {}
export const Info = Schema.Struct({
id: ID,
}).annotate({ identifier: "Plugin.Info" })
const Added = define({
type: "plugin.added",
schema: { id: ID },

View file

@ -44,7 +44,7 @@ export const create = Effect.fn("OpenCode.create")(function* () {
// `PluginContext`, so `ctx.agent.transform` and every other hook behave exactly
// as they do for a config-discovered plugin. Define agent profiles here at
// startup, then select one per Session with `sessions.create({ agent })`.
plugin: plugins.register,
plugin: Object.assign(plugins.register, client.plugin),
}
})

View file

@ -10,6 +10,7 @@ import { CommandHandler } from "./handlers/command"
import { SkillHandler } from "./handlers/skill"
import { EventHandler } from "./handlers/event"
import { AgentHandler } from "./handlers/agent"
import { PluginHandler } from "./handlers/plugin"
import { HealthHandler } from "./handlers/health"
import { PtyHandler } from "./handlers/pty"
import { ShellHandler } from "./handlers/shell"
@ -26,6 +27,7 @@ export const handlers = Layer.mergeAll(
HealthHandler,
LocationHandler,
AgentHandler,
PluginHandler,
SessionHandler,
MessageHandler,
ModelHandler,

View file

@ -0,0 +1,13 @@
import { PluginV2 } from "@opencode-ai/core/plugin"
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
import { response } from "../location"
export const PluginHandler = HttpApiBuilder.group(Api, "server.plugin", (handlers) =>
handlers.handle("plugin.list", () =>
Effect.gen(function* () {
return yield* response(PluginV2.Service.use((plugin) => plugin.list()))
}),
),
)