From 1b10ab36d2c5de743510574dec673b72647f9071 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 18 Apr 2026 01:37:46 +0900 Subject: [PATCH] refactor(plugin): migrate to V1 PluginModule format Convert the default export from the legacy callable Plugin to the V1 PluginModule shape (`{ id, server }`) documented by opencode's plugin SDK. This aligns oh-my-openagent with the canonical plugin entry format and removes plugin-format legacy debt. Drop the module-level `let activePluginDispose` cleanup guard: opencode instantiates plugins in a scope-bound Layer per server and dynamic-imports fresh modules on reload, so module-level state is not preserved across reloads. Individual managers already register their own SIGINT/SIGTERM cleanup (skill-mcp-manager, background-agent process-cleanup), so the orphaned createPluginDispose call provided no runtime value. Remove the non-standard `name` return field that opencode's Hooks interface does not include. The PluginModule's `id` now carries the plugin identity instead. Drop the unused `lspManager` import that only fed the orphaned createPluginDispose. Update src/index.test.ts and src/index.telemetry.test.ts to call `plugin.server(ctx)` instead of `plugin(ctx)`, and assert the V1 shape. --- src/index.telemetry.test.ts | 7 ++--- src/index.test.ts | 21 ++++++++++----- src/index.ts | 52 +++++++++++++++---------------------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/index.telemetry.test.ts b/src/index.telemetry.test.ts index f3026a1cb..1f552f7eb 100644 --- a/src/index.telemetry.test.ts +++ b/src/index.telemetry.test.ts @@ -125,12 +125,13 @@ describe("OhMyOpenCodePlugin telemetry isolation", () => { const { default: plugin } = await import(`./index?telemetry=${Date.now()}-${Math.random()}`) // when - const result = await plugin({ + const result = await plugin.server({ directory: "/tmp/project", client: {}, - } as Parameters[0]) + } as Parameters[0]) // then - expect(result).toMatchObject({ name: "oh-my-openagent" }) + expect(typeof result).toBe("object") + expect(result).not.toBeNull() }) }) diff --git a/src/index.test.ts b/src/index.test.ts index 7101b9f09..00af70bb9 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -257,7 +257,7 @@ const mockCreatePluginInterface = mock(() => ({})) const mockInitializeOpenClaw = mock(async () => {}) const mockStartTmuxCheck = mock(() => {}) -let OhMyOpenCodePlugin: (typeof import("./index"))["default"] +let pluginModule: (typeof import("./index"))["default"] function installIndexModuleMocks(): void { mock.module("./cli/config-manager/config-context", () => ({ @@ -337,7 +337,7 @@ describe("OhMyOpenCodePlugin", () => { beforeEach(async () => { mock.restore() installIndexModuleMocks() - ;({ default: OhMyOpenCodePlugin } = await importFreshIndexModule()) + ;({ default: pluginModule } = await importFreshIndexModule()) mockInitConfigContext.mockClear() mockDetectExternalSkillPlugin.mockClear() mockGetSkillPluginConflictWarning.mockClear() @@ -375,10 +375,10 @@ describe("OhMyOpenCodePlugin", () => { }) // when - await OhMyOpenCodePlugin({ + await pluginModule.server({ directory: "/tmp/project", client: {}, - } as Parameters[0]) + } as Parameters[0]) // then expect(mockInitializeOpenClaw).toHaveBeenCalledTimes(1) @@ -390,12 +390,21 @@ describe("OhMyOpenCodePlugin", () => { mockLoadPluginConfig.mockReturnValue({}) // when - await OhMyOpenCodePlugin({ + await pluginModule.server({ directory: "/tmp/project", client: {}, - } as Parameters[0]) + } as Parameters[0]) // then expect(mockInitializeOpenClaw).not.toHaveBeenCalled() }) + + it("exports a V1 PluginModule shape with id and server", () => { + // given the plugin module is loaded + // when inspecting the default export + // then it has the expected V1 shape + expect(typeof pluginModule).toBe("object") + expect(pluginModule.id).toBe("oh-my-openagent") + expect(typeof pluginModule.server).toBe("function") + }) }) diff --git a/src/index.ts b/src/index.ts index b36e6c4b9..b41f611e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { initConfigContext } from "./cli/config-manager/config-context" -import type { Plugin } from "@opencode-ai/plugin" +import type { Hooks, Plugin, PluginModule } from "@opencode-ai/plugin" import type { HookName } from "./config" @@ -9,7 +9,6 @@ import { createRuntimeTmuxConfig, isTmuxIntegrationEnabled } from "./create-runt import { createTools } from "./create-tools" import { initializeOpenClaw } from "./openclaw" import { createPluginInterface } from "./plugin-interface" -import { createPluginDispose, type PluginDispose } from "./plugin-dispose" import { loadPluginConfig } from "./plugin-config" import { createModelCacheState } from "./plugin-state" @@ -17,27 +16,23 @@ import { createFirstMessageVariantGate } from "./shared/first-message-variant" import { injectServerAuthIntoClient, log, logLegacyPluginStartupWarning } from "./shared" import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector" import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash" -import { lspManager } from "./tools/lsp/client" import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog" -let activePluginDispose: PluginDispose | null = null - -const OhMyOpenCodePlugin: Plugin = async (ctx) => { +const serverPlugin: Plugin = async (input, _options): Promise => { initConfigContext("opencode", null) log("[OhMyOpenCodePlugin] ENTRY - plugin loading", { - directory: ctx.directory, + directory: input.directory, }) logLegacyPluginStartupWarning() - const skillPluginCheck = detectExternalSkillPlugin(ctx.directory) + const skillPluginCheck = detectExternalSkillPlugin(input.directory) if (skillPluginCheck.detected && skillPluginCheck.pluginName) { console.warn(getSkillPluginConflictWarning(skillPluginCheck.pluginName)) } - injectServerAuthIntoClient(ctx.client) - await activePluginDispose?.() + injectServerAuthIntoClient(input.client) - const pluginConfig = loadPluginConfig(ctx.directory, ctx) + const pluginConfig = loadPluginConfig(input.directory, input) const posthog = createPluginPostHog() const distinctId = getPostHogDistinctId() @@ -78,7 +73,7 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { const modelCacheState = createModelCacheState() const managers = createManagers({ - ctx, + ctx: input, pluginConfig, tmuxConfig, modelCacheState, @@ -86,13 +81,13 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { }) const toolsResult = await createTools({ - ctx, + ctx: input, pluginConfig, managers, }) const hooks = createHooks({ - ctx, + ctx: input, pluginConfig, modelCacheState, backgroundManager: managers.backgroundManager, @@ -102,15 +97,8 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { availableSkills: toolsResult.availableSkills, }) - const dispose = createPluginDispose({ - backgroundManager: managers.backgroundManager, - skillMcpManager: managers.skillMcpManager, - lspManager, - disposeHooks: hooks.disposeHooks, - }) - const pluginInterface = createPluginInterface({ - ctx, + ctx: input, pluginConfig, firstMessageVariantGate, managers, @@ -118,30 +106,32 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => { tools: toolsResult.filteredTools, }) - activePluginDispose = dispose - return { - name: "oh-my-openagent", ...pluginInterface, "experimental.session.compacting": async ( - _input: { sessionID: string }, + compactingInput: { sessionID: string }, output: { context: string[] }, ): Promise => { - await hooks.compactionContextInjector?.capture(_input.sessionID) - await hooks.compactionTodoPreserver?.capture(_input.sessionID) + await hooks.compactionContextInjector?.capture(compactingInput.sessionID) + await hooks.compactionTodoPreserver?.capture(compactingInput.sessionID) await hooks.claudeCodeHooks?.["experimental.session.compacting"]?.( - _input, + compactingInput, output, ) if (hooks.compactionContextInjector) { - output.context.push(hooks.compactionContextInjector.inject(_input.sessionID)) + output.context.push(hooks.compactionContextInjector.inject(compactingInput.sessionID)) } }, } } -export default OhMyOpenCodePlugin +const pluginModule: PluginModule = { + id: "oh-my-openagent", + server: serverPlugin, +} + +export default pluginModule export type { OhMyOpenCodeConfig,