diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index 8ffdf93ba10..1b57d9fc5e4 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -1,8 +1,9 @@ export * as PluginV2 from "./plugin" +import type { Plugin } from "@opencode-ai/plugin/v2/effect" +import { Event, ID, type Info } from "@opencode-ai/schema/plugin" import { makeLocationNode } from "./effect/app-node" import { Context, Effect, Exit, Layer, Scope, Semaphore } from "effect" -import { Plugin } from "@opencode-ai/schema/plugin" import { AgentV2 } from "./agent" import { AISDK } from "./aisdk" import { Catalog } from "./catalog" @@ -18,14 +19,8 @@ import { State } from "./state" import { ToolRegistry } from "./tool/registry" import { ToolHooks } from "./tool/hooks" -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 activate: (plugins: readonly import("@opencode-ai/plugin/v2/effect").Plugin[]) => Effect.Effect + readonly activate: (plugins: readonly { readonly plugin: Plugin; readonly version?: string }[]) => Effect.Effect readonly list: () => Effect.Effect } @@ -36,16 +31,20 @@ const layer = Layer.effect( Effect.gen(function* () { const events = yield* EventV2.Service const scope = yield* Scope.make() - const active = new Map() + const active = new Map() const lock = Semaphore.makeUnsafe(1) - let generation: readonly ID[] | undefined = [] - let host: Parameters[0] + let generation: readonly { readonly id: typeof ID.Type; readonly version?: string }[] | undefined = [] + let host: Parameters[0] const activate = Effect.fn("Plugin.activate")(function* ( - plugins: readonly import("@opencode-ai/plugin/v2/effect").Plugin[], + plugins: readonly { readonly plugin: Plugin; readonly version?: string }[], ) { - const definitions = plugins.map((plugin) => ({ ...plugin, id: ID.make(plugin.id) })) - const ids = new Set() + const definitions = plugins.map((entry) => ({ + ...entry.plugin, + id: ID.make(entry.plugin.id), + ...(entry.version === undefined ? {} : { version: entry.version }), + })) + const ids = new Set() for (const definition of definitions) { if (ids.has(definition.id)) return yield* Effect.die(new Error(`Duplicate plugin ID: ${definition.id}`)) ids.add(definition.id) @@ -56,7 +55,9 @@ const layer = Layer.effect( if ( generation !== undefined && generation.length === definitions.length && - generation.every((id, index) => id === definitions[index]?.id) + generation.every( + (plugin, index) => plugin.id === definitions[index]?.id && plugin.version === definitions[index]?.version, + ) ) { return } @@ -87,7 +88,10 @@ const layer = Layer.effect( }), ) if (Exit.isFailure(exit)) return yield* exit - generation = definitions.map((definition) => definition.id) + generation = definitions.map((definition) => ({ + id: definition.id, + ...(definition.version === undefined ? {} : { version: definition.version }), + })) }), ) }) diff --git a/packages/core/src/plugin/supervisor.ts b/packages/core/src/plugin/supervisor.ts index 19410e5001f..4d648a7d6f3 100644 --- a/packages/core/src/plugin/supervisor.ts +++ b/packages/core/src/plugin/supervisor.ts @@ -2,7 +2,7 @@ export * as PluginSupervisor from "./supervisor" import type { Plugin } from "@opencode-ai/plugin/v2/effect" import { Event } from "@opencode-ai/schema/config" -import { Context, Effect, Fiber, Layer, Schema, Semaphore, Stream } from "effect" +import { Context, Effect, Fiber, Layer, Option, Schema, Semaphore, Stream } from "effect" import path from "path" import { fileURLToPath, pathToFileURL } from "url" import { Config } from "../config" @@ -42,6 +42,7 @@ type Operation = readonly type: "add" readonly target: string readonly options: Record + readonly mtime?: number } | { readonly type: "remove" @@ -57,6 +58,7 @@ type Candidate = readonly type: "package" readonly specifier: string readonly options: Record + readonly mtime?: number } type ConfiguredPackage = { @@ -94,7 +96,16 @@ const scan = Effect.fn("PluginSupervisor.scan")(function* (entries: readonly Con }), ) // Explicit config is applied last so it can remove auto-discovered packages. - return [...discovered, ...configured] + return yield* Effect.forEach([...discovered, ...configured], (operation) => { + if (operation.type === "remove" || !path.isAbsolute(operation.target)) return Effect.succeed(operation) + return fs.stat(operation.target).pipe( + Effect.map((info) => ({ + ...operation, + mtime: Option.getOrElse(info.mtime, () => new Date(0)).getTime(), + })), + Effect.catch(() => Effect.succeed(operation)), + ) + }) }) const resolve = Effect.fn("PluginSupervisor.resolve")(function* ( @@ -143,7 +154,16 @@ function apply(pre: readonly Plugin[], post: readonly Plugin[], operations: read enabled.has(definition.id) ? [{ type: "definition", definition }] : [], ) const configured: Candidate[] = Array.from(packages.values()).flatMap((item) => - item.enabled ? [{ type: "package", specifier: item.operation.target, options: item.operation.options }] : [], + item.enabled + ? [ + { + type: "package", + specifier: item.operation.target, + options: item.operation.options, + ...(item.operation.mtime === undefined ? {} : { mtime: item.operation.mtime }), + }, + ] + : [], ) const posts: Candidate[] = post.flatMap((definition) => enabled.has(definition.id) ? [{ type: "definition", definition }] : [], @@ -153,21 +173,29 @@ function apply(pre: readonly Plugin[], post: readonly Plugin[], operations: read const load = Effect.fn("PluginSupervisor.load")(function* (plan: readonly Candidate[]) { return yield* Effect.forEach(plan, (candidate) => { - if (candidate.type === "definition") return Effect.succeed(candidate.definition) + if (candidate.type === "definition") return Effect.succeed({ plugin: candidate.definition }) return Effect.gen(function* () { const npm = yield* Npm.Service const entrypoint = path.isAbsolute(candidate.specifier) ? pathToFileURL(candidate.specifier).href : (yield* npm.add(candidate.specifier)).entrypoint if (!entrypoint) return - yield* Effect.log({ msg: "loading plugin", id: candidate.specifier, entrypoint }) - const mod = yield* Effect.promise(() => import(entrypoint)) + // Bun currently ignores query parameters when caching file:// imports. + const source = + candidate.mtime === undefined + ? entrypoint + : `${candidate.specifier.replaceAll("\\", "/")}?mtime=${candidate.mtime}` + yield* Effect.log({ msg: "loading plugin", id: candidate.specifier, entrypoint: source }) + const mod = yield* Effect.promise(() => import(source)) const value = (yield* Schema.decodeUnknownEffect(PluginModule)(mod)).default const plugin = "effect" in value ? value : PluginPromise.fromPromise(value) return { - id: plugin.id, - effect: (host) => plugin.effect({ ...host, options: candidate.options }), - } satisfies Plugin + plugin: { + id: plugin.id, + effect: (host) => plugin.effect({ ...host, options: candidate.options }), + } satisfies Plugin, + ...(candidate.mtime === undefined ? {} : { version: String(candidate.mtime) }), + } }).pipe(Effect.catchCause(() => Effect.succeed(undefined))) }).pipe(Effect.map((plugins) => plugins.filter((plugin) => plugin !== undefined))) }) @@ -231,7 +259,6 @@ const layer = Layer.effect( const config = yield* Config.Service const events = yield* EventV2.Service const lock = Semaphore.makeUnsafe(1) - let applied: string | undefined const reload = Effect.fn("PluginSupervisor.reload")(() => lock.withPermit( Effect.gen(function* () { @@ -241,15 +268,11 @@ const layer = Layer.effect( const pre = [...internal.pre, ...sdk.all()] // Read the current layered config before resolving plugin directives and packages. const entries = yield* config.entries() - // Skip duplicate watcher notifications and config edits unrelated to plugins. const operations = yield* scan(entries) - const version = JSON.stringify(operations) - if (version === applied) return // Apply config operations and load enabled package plugins into one ordered generation. const plugins = yield* resolve(pre, internal.post, operations) // Replace the active generation in one scoped, batched activation. yield* registry.activate(plugins) - applied = version }), ), ) diff --git a/packages/core/test/config/plugin.test.ts b/packages/core/test/config/plugin.test.ts index c3c84d81016..34a69ffbc50 100644 --- a/packages/core/test/config/plugin.test.ts +++ b/packages/core/test/config/plugin.test.ts @@ -1,7 +1,10 @@ import fs from "fs/promises" import path from "path" +import { pathToFileURL } from "url" import { describe, expect } from "bun:test" import { define } from "@opencode-ai/plugin/v2/effect" +import { Config as ConfigSchema } from "@opencode-ai/schema/config" +import { Plugin } from "@opencode-ai/schema/plugin" import { AgentV2 } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" @@ -33,7 +36,7 @@ describe("PluginSupervisor config", () => { yield* ready() expect( (yield* plugins.list()).map((plugin) => plugin.id).filter((id) => id.startsWith("opencode.provider.")), - ).toEqual([PluginV2.ID.make("opencode.provider.openai")]) + ).toEqual([Plugin.ID.make("opencode.provider.openai")]) }), ), ) @@ -122,6 +125,43 @@ describe("PluginSupervisor config", () => { ), ) + it.live("reloads an auto-discovered plugin when its file changes", () => + withLocation( + undefined, + Effect.gen(function* () { + yield* ready() + const agents = yield* AgentV2.Service + const events = yield* EventV2.Service + const location = yield* Location.Service + const plugins = yield* PluginV2.Service + const file = path.join(location.directory, ".opencode", "plugin", "mutable.ts") + const first = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id + + expect(first).toBeDefined() + expect((yield* agents.get(AgentV2.ID.make("mutable")))?.description).toBe("first") + + yield* Effect.promise(async () => { + await fs.writeFile(file, mutablePlugin("second")) + const modified = new Date(Date.now() + 5_000) + await fs.utimes(file, modified, modified) + }) + yield* events.publish(ConfigSchema.Event.Updated, {}) + yield* waitUntil( + Effect.gen(function* () { + const current = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id + return current === first && (yield* agents.get(AgentV2.ID.make("mutable")))?.description === "second" + }), + ) + }), + false, + async (directory) => { + const plugin = path.join(directory, ".opencode", "plugin") + await fs.mkdir(plugin, { recursive: true }) + await fs.writeFile(path.join(plugin, "mutable.ts"), mutablePlugin("first")) + }, + ), + ) + it.live("applies explicit removals after auto-discovery", () => withLocation( { plugins: ["-*"] }, @@ -192,13 +232,19 @@ const ready = Effect.fnUntraced(function* () { yield* supervisor.ready }) -function withLocation(config: unknown, effect: Effect.Effect, fixtures = false) { +function withLocation( + config: unknown, + effect: Effect.Effect, + fixtures = false, + prepare?: (directory: string) => Promise, +) { return Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ).pipe( Effect.tap((tmp) => Effect.promise(async () => { + await prepare?.(tmp.path) if (fixtures) { const directory = path.join(tmp.path, ".opencode") await fs.mkdir(directory, { recursive: true }) @@ -223,3 +269,30 @@ function withLocation(config: unknown, effect: Effect.Effect, ), ) } + +function mutablePlugin(description: string) { + const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/v2/promise/index.ts")).href + return ` +import { define } from ${JSON.stringify(plugin)} + +export default define({ + id: "mutable-plugin", + setup: async (ctx) => { + await ctx.agent.transform((agents) => { + agents.update("mutable", (agent) => { + agent.description = ${JSON.stringify(description)} + agent.mode = "subagent" + }) + }) + }, +}) +` +} + +const waitUntil = Effect.fnUntraced(function* (condition: Effect.Effect) { + for (let attempt = 0; attempt < 200; attempt++) { + if (yield* condition) return + yield* Effect.sleep("10 millis") + } + return yield* Effect.die("Timed out waiting for plugin reload") +}) diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index be6e150680b..f0bef9fd325 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -2,6 +2,7 @@ import fs from "fs/promises" import path from "path" import { describe, expect } from "bun:test" import { Config } from "@opencode-ai/schema/config" +import { Plugin } from "@opencode-ai/schema/plugin" import { Context, DateTime, Effect, Equal, Hash, Schema, Stream } from "effect" import { define } from "@opencode-ai/plugin/v2/effect" import { AgentV2 } from "@opencode-ai/core/agent" @@ -50,7 +51,7 @@ describe("LocationServiceMap", () => { ), ) - expect(plugins.map((plugin) => plugin.id)).toEqual([PluginV2.ID.make("opencode.agent")]) + expect(plugins.map((plugin) => plugin.id)).toEqual([Plugin.ID.make("opencode.agent")]) }), ), ), @@ -377,7 +378,7 @@ describe("LocationServiceMap", () => { }) .pipe(Effect.asVoid), }) - yield* plugins.activate([{ id: PluginV2.ID.make(reviewer.id), effect: reviewer.effect }]) + yield* plugins.activate([{ plugin: reviewer }]) expect(yield* (yield* AgentV2.Service).get(AgentV2.ID.make("reviewer"))).toMatchObject({ description: "Reviews code", diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index 8917171929f..6037f4538b1 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -2,6 +2,7 @@ import { describe, expect } from "bun:test" import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect" import { define } from "@opencode-ai/plugin/v2/effect" import { Config as ConfigSchema } from "@opencode-ai/schema/config" +import { Plugin } from "@opencode-ai/schema/plugin" import { AgentV2 } from "@opencode-ai/core/agent" import { EventV2 } from "@opencode-ai/core/event" import { PluginV2 } from "@opencode-ai/core/plugin" @@ -37,15 +38,15 @@ describe("PluginV2", () => { }), ) - it.effect("skips identical generations and replaces changed plugin IDs", () => + it.effect("skips identical generations and replaces changed plugin versions", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service const agents = yield* AgentV2.Service let description = "first" - const managed = (id: string) => + const managed = () => define({ - id, + id: "managed", effect: (ctx) => ctx.agent .transform((agents) => @@ -56,15 +57,15 @@ describe("PluginV2", () => { .pipe(Effect.asVoid), }) - yield* plugins.activate([managed("managed")]) + yield* plugins.activate([{ plugin: managed() }]) expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first") description = "second" - yield* plugins.activate([managed("managed")]) + yield* plugins.activate([{ plugin: managed() }]) expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first") - yield* plugins.activate([managed("managed-next")]) + yield* plugins.activate([{ plugin: managed(), version: "next" }]) expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second") yield* plugins.activate([]) @@ -75,14 +76,14 @@ describe("PluginV2", () => { it.effect("rejects duplicate IDs before replacing the active generation", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service - const active = PluginV2.ID.make("active") - const duplicate = PluginV2.ID.make("duplicate") - yield* plugins.activate([{ id: active, effect: () => Effect.void }]) + const active = Plugin.ID.make("active") + const duplicate = "duplicate" + yield* plugins.activate([{ plugin: { id: active, effect: () => Effect.void } }]) const result = yield* plugins .activate([ - { id: duplicate, effect: () => Effect.void }, - { id: duplicate, effect: () => Effect.void }, + { plugin: { id: duplicate, effect: () => Effect.void } }, + { plugin: { id: duplicate, effect: () => Effect.void } }, ]) .pipe(Effect.exit) @@ -105,11 +106,11 @@ describe("PluginV2", () => { .pipe(Effect.asVoid), }) - expect(Exit.isFailure(yield* plugins.activate([plugin]).pipe(Effect.exit))).toBe(true) + expect(Exit.isFailure(yield* plugins.activate([{ plugin }]).pipe(Effect.exit))).toBe(true) fail = false - yield* plugins.activate([plugin]) + yield* plugins.activate([{ plugin }]) - expect(yield* plugins.list()).toEqual([{ id: PluginV2.ID.make("retry") }]) + expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("retry") }]) }), ) @@ -119,8 +120,10 @@ describe("PluginV2", () => { const closed: string[] = [] yield* plugins.activate( ["first", "second"].map((id) => ({ - id: PluginV2.ID.make(id), - effect: () => Effect.addFinalizer(() => Effect.sync(() => closed.push(id))), + plugin: { + id, + effect: () => Effect.addFinalizer(() => Effect.sync(() => closed.push(id))), + }, })), ) @@ -143,9 +146,7 @@ describe("PluginV2", () => { ), }) - yield* plugins - .activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }]) - .pipe(Effect.provideService(Secret, "secret")) + yield* plugins.activate([{ plugin }]).pipe(Effect.provideService(Secret, "secret")) expect(visible).toBe(false) }), @@ -170,7 +171,7 @@ describe("PluginV2", () => { .pipe(Effect.orDie), }) - yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }]) + yield* plugins.activate([{ plugin }]) expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toContain( "plugin_tool", ) @@ -205,7 +206,7 @@ describe("PluginV2", () => { }), }) - yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }]) + yield* plugins.activate([{ plugin }]) expect((yield* registry.materialize({ model: testModel })).definitions.map((tool) => tool.name)).toEqual([ "plain", @@ -257,7 +258,7 @@ describe("PluginV2", () => { }), }) - yield* plugins.activate([{ id: PluginV2.ID.make(plugin.id), effect: plugin.effect }]) + yield* plugins.activate([{ plugin }]) const materialized = yield* registry.materialize({ model: testModel }) const settlement = yield* materialized.settle({ diff --git a/packages/core/test/plugin/provider-kilo.test.ts b/packages/core/test/plugin/provider-kilo.test.ts index bec50272eff..1ff356523ca 100644 --- a/packages/core/test/plugin/provider-kilo.test.ts +++ b/packages/core/test/plugin/provider-kilo.test.ts @@ -19,9 +19,7 @@ const addPlugin = Effect.fn(function* () { describe("KiloPlugin", () => { it.effect("is registered so legacy referer headers can be applied", () => - Effect.sync(() => - expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.kilo")), - ), + Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.kilo")), ) it.effect("applies legacy referer headers only to kilo", () => diff --git a/packages/core/test/plugin/provider-llmgateway.test.ts b/packages/core/test/plugin/provider-llmgateway.test.ts index 8cc45c6027b..8f4af578898 100644 --- a/packages/core/test/plugin/provider-llmgateway.test.ts +++ b/packages/core/test/plugin/provider-llmgateway.test.ts @@ -21,9 +21,7 @@ const addPlugin = Effect.fn(function* () { describe("LLMGatewayPlugin", () => { it.effect("is registered so legacy referer headers can be applied", () => - Effect.sync(() => - expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.llmgateway")), - ), + Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.llmgateway")), ) it.effect("applies legacy referer headers only to enabled llmgateway", () => diff --git a/packages/core/test/plugin/provider-nvidia.test.ts b/packages/core/test/plugin/provider-nvidia.test.ts index baf763c8d14..c176cef4ac6 100644 --- a/packages/core/test/plugin/provider-nvidia.test.ts +++ b/packages/core/test/plugin/provider-nvidia.test.ts @@ -19,9 +19,7 @@ const addPlugin = Effect.fn(function* () { describe("NvidiaPlugin", () => { it.effect("is registered so legacy referer headers can be applied", () => - Effect.sync(() => - expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.nvidia")), - ), + Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.nvidia")), ) it.effect("applies NVIDIA tracking headers only to nvidia", () => diff --git a/packages/core/test/plugin/provider-openrouter.test.ts b/packages/core/test/plugin/provider-openrouter.test.ts index e0be8b5fa74..60dfc67b51e 100644 --- a/packages/core/test/plugin/provider-openrouter.test.ts +++ b/packages/core/test/plugin/provider-openrouter.test.ts @@ -22,9 +22,7 @@ const addPlugin = Effect.fn(function* () { describe("OpenRouterPlugin", () => { it.effect("is registered so legacy OpenRouter behavior can be applied", () => - Effect.sync(() => - expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.openrouter")), - ), + Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.openrouter")), ) it.effect("applies legacy referer headers only to openrouter", () => diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index a1265b6a8f3..4de00105b75 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -43,7 +43,7 @@ function withEnv(vars: Record, effect: () = describe("SnowflakeCortexPlugin", () => { it.effect("is registered in ProviderPlugins before OpenAICompatiblePlugin", () => Effect.sync(() => { - expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.snowflake-cortex")) + expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.snowflake-cortex") const ids = ProviderPlugins.map((p) => p.id) expect(ids.indexOf("opencode.provider.snowflake-cortex")).toBeLessThan( ids.indexOf("opencode.provider.openai-compatible"), diff --git a/packages/core/test/plugin/provider-zenmux.test.ts b/packages/core/test/plugin/provider-zenmux.test.ts index 0ce286d6124..9c44800ebc4 100644 --- a/packages/core/test/plugin/provider-zenmux.test.ts +++ b/packages/core/test/plugin/provider-zenmux.test.ts @@ -24,9 +24,7 @@ function required(value: T | undefined): T { describe("ZenmuxPlugin", () => { it.effect("is registered so legacy referer headers can be applied", () => - Effect.sync(() => - expect(ProviderPlugins.map((item) => item.id)).toContain(PluginV2.ID.make("opencode.provider.zenmux")), - ), + Effect.sync(() => expect(ProviderPlugins.map((item) => item.id)).toContain("opencode.provider.zenmux")), ) it.effect("applies the exact legacy Zenmux headers", () => diff --git a/packages/core/test/shared-schema.test.ts b/packages/core/test/shared-schema.test.ts index ecdd555fa8a..7d227aac19b 100644 --- a/packages/core/test/shared-schema.test.ts +++ b/packages/core/test/shared-schema.test.ts @@ -22,14 +22,12 @@ import { FileSystem } from "@opencode-ai/schema/filesystem" import { Integration } from "@opencode-ai/schema/integration" import { LLM } from "@opencode-ai/schema/llm" import { Permission } from "@opencode-ai/schema/permission" -import { Plugin } from "@opencode-ai/schema/plugin" import { Pty } from "@opencode-ai/schema/pty" import { Reference } from "@opencode-ai/schema/reference" import { SessionTodo } from "@opencode-ai/schema/session-todo" import { Skill } from "@opencode-ai/schema/skill" import { AbsolutePath, DateTimeUtcFromMillis, optional, statics } from "@opencode-ai/schema/schema" import { ProviderV2 } from "@opencode-ai/core/provider" -import { PluginV2 } from "@opencode-ai/core/plugin" test("Core reuses the canonical shared schemas", async () => { const [ @@ -129,8 +127,6 @@ test("Core reuses the canonical shared schemas", async () => { [corePermission.Ruleset, Permission.Ruleset], [corePermissionV1.Event, PermissionV1.Event], [coreProjectCopy.Event, ProjectDirectories.Event], - [PluginV2.ID, Plugin.ID], - [PluginV2.Event, Plugin.Event], [corePty.Info, Pty.Info], [corePty.Event, Pty.Event], [coreProject.ID, Project.ID],