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:53 -04:00
parent f8626865b9
commit 6a91a682e4
13 changed files with 954 additions and 839 deletions
+3 -3
View File
@@ -3,9 +3,9 @@ export default {
setup: async (ctx) => { setup: async (ctx) => {
await ctx.agent.transform((agents) => { await ctx.agent.transform((agents) => {
agents.update("sample-plugin-agent", (agent) => { agents.update("sample-plugin-agent", (agent) => {
agent.description = "Example subagent registered by .opencode/plugins/sample-agent.ts" agent.description = "Example primary agent registered by .opencode/plugins/sample-agent.ts"
agent.mode = "subagent" agent.mode = "primary"
agent.prompt = [ agent.system = [
"You are the sample plugin agent for this repository.", "You are the sample plugin agent for this repository.",
"Use this agent to verify that local plugin auto-discovery can add agents.", "Use this agent to verify that local plugin auto-discovery can add agents.",
"Keep responses concise and explain which plugin registered you when asked.", "Keep responses concise and explain which plugin registered you when asked.",
File diff suppressed because it is too large Load Diff
+16
View File
@@ -4,6 +4,8 @@ import type {
LocationGetOutput, LocationGetOutput,
AgentListInput, AgentListInput,
AgentListOutput, AgentListOutput,
PluginListInput,
PluginListOutput,
SessionListInput, SessionListInput,
SessionListOutput, SessionListOutput,
SessionCreateInput, SessionCreateInput,
@@ -310,6 +312,20 @@ export function make(options: ClientOptions) {
requestOptions, 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: { session: {
list: (input?: SessionListInput, requestOptions?: RequestOptions) => list: (input?: SessionListInput, requestOptions?: RequestOptions) =>
request<SessionListOutput>( request<SessionListOutput>(
+15
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 = { export type SessionListInput = {
readonly workspace?: { readonly workspace?: {
readonly workspace?: string | undefined readonly workspace?: string | undefined
+6
View File
@@ -21,12 +21,15 @@ import { ToolRegistry } from "./tool/registry"
export const ID = Plugin.ID export const ID = Plugin.ID
export type ID = typeof ID.Type export type ID = typeof ID.Type
export const Info = Plugin.Info
export type Info = Plugin.Info
export const Event = Plugin.Event export const Event = Plugin.Event
export interface Interface { export interface Interface {
readonly add: (id: ID, effect: PluginDefinition["effect"]) => Effect.Effect<void> readonly add: (id: ID, effect: PluginDefinition["effect"]) => Effect.Effect<void>
readonly remove: (id: ID) => Effect.Effect<void> readonly remove: (id: ID) => Effect.Effect<void>
readonly wait: (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") {} export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Plugin") {}
@@ -139,6 +142,9 @@ export const layer = Layer.effect(
add, add,
remove, remove,
wait, wait,
list: Effect.fn("Plugin.list")(function* () {
return Array.from(active.keys()).map((id) => ({ id }))
}),
}) })
host = yield* PluginHost.make(service) host = yield* PluginHost.make(service)
return service return service
File diff suppressed because it is too large Load Diff
+2
View File
@@ -13,6 +13,7 @@ import { SkillGroup } from "./groups/skill"
import { EventGroup, makeEventGroup } from "./groups/event" import { EventGroup, makeEventGroup } from "./groups/event"
import type { Definition } from "@opencode-ai/schema/event" import type { Definition } from "@opencode-ai/schema/event"
import { AgentGroup } from "./groups/agent" import { AgentGroup } from "./groups/agent"
import { PluginGroup } from "./groups/plugin"
import { HealthGroup } from "./groups/health" import { HealthGroup } from "./groups/health"
import { PtyGroup } from "./groups/pty" import { PtyGroup } from "./groups/pty"
import { ShellGroup } from "./groups/shell" import { ShellGroup } from "./groups/shell"
@@ -42,6 +43,7 @@ const makeApiFromGroup = <
.add(HealthGroup) .add(HealthGroup)
.add(LocationGroup.middleware(locationMiddleware)) .add(LocationGroup.middleware(locationMiddleware))
.add(AgentGroup.middleware(locationMiddleware)) .add(AgentGroup.middleware(locationMiddleware))
.add(PluginGroup.middleware(locationMiddleware))
.add(makeSessionGroup(sessionLocationMiddleware)) .add(makeSessionGroup(sessionLocationMiddleware))
.add(MessageGroup.middleware(sessionLocationMiddleware)) .add(MessageGroup.middleware(sessionLocationMiddleware))
.add(ModelGroup.middleware(locationMiddleware)) .add(ModelGroup.middleware(locationMiddleware))
+1
View File
@@ -20,6 +20,7 @@ export const groupNames = {
"server.health": "health", "server.health": "health",
"server.location": "location", "server.location": "location",
"server.agent": "agent", "server.agent": "agent",
"server.plugin": "plugin",
"server.session": "session", "server.session": "session",
"server.message": "message", "server.message": "message",
"server.model": "model", "server.model": "model",
+27
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.",
}),
)
+5
View File
@@ -6,6 +6,11 @@ import { define, inventory } from "./event"
export const ID = Schema.String.pipe(Schema.brand("Plugin.ID")) export const ID = Schema.String.pipe(Schema.brand("Plugin.ID"))
export type ID = typeof ID.Type 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({ const Added = define({
type: "plugin.added", type: "plugin.added",
schema: { id: ID }, schema: { id: ID },
+1 -1
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 // `PluginContext`, so `ctx.agent.transform` and every other hook behave exactly
// as they do for a config-discovered plugin. Define agent profiles here at // as they do for a config-discovered plugin. Define agent profiles here at
// startup, then select one per Session with `sessions.create({ agent })`. // startup, then select one per Session with `sessions.create({ agent })`.
plugin: plugins.register, plugin: Object.assign(plugins.register, client.plugin),
} }
}) })
+2
View File
@@ -10,6 +10,7 @@ import { CommandHandler } from "./handlers/command"
import { SkillHandler } from "./handlers/skill" import { SkillHandler } from "./handlers/skill"
import { EventHandler } from "./handlers/event" import { EventHandler } from "./handlers/event"
import { AgentHandler } from "./handlers/agent" import { AgentHandler } from "./handlers/agent"
import { PluginHandler } from "./handlers/plugin"
import { HealthHandler } from "./handlers/health" import { HealthHandler } from "./handlers/health"
import { PtyHandler } from "./handlers/pty" import { PtyHandler } from "./handlers/pty"
import { ShellHandler } from "./handlers/shell" import { ShellHandler } from "./handlers/shell"
@@ -26,6 +27,7 @@ export const handlers = Layer.mergeAll(
HealthHandler, HealthHandler,
LocationHandler, LocationHandler,
AgentHandler, AgentHandler,
PluginHandler,
SessionHandler, SessionHandler,
MessageHandler, MessageHandler,
ModelHandler, ModelHandler,
+13
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()))
}),
),
)