diff --git a/extensions/irc/channel-plugin-api.ts b/extensions/irc/channel-plugin-api.ts new file mode 100644 index 00000000000..89ce5ce14fb --- /dev/null +++ b/extensions/irc/channel-plugin-api.ts @@ -0,0 +1,3 @@ +// Keep bundled channel entry imports narrow so bootstrap/discovery paths do +// not drag IRC runtime/send/monitor surfaces into lightweight plugin loads. +export { ircPlugin } from "./src/channel.js"; diff --git a/extensions/irc/index.test.ts b/extensions/irc/index.test.ts new file mode 100644 index 00000000000..a097752a8cf --- /dev/null +++ b/extensions/irc/index.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import entry from "./index.js"; +import setupEntry from "./setup-entry.js"; + +describe("irc bundled entries", () => { + it("loads the channel plugin without importing the broad api barrel", () => { + const plugin = entry.loadChannelPlugin(); + expect(plugin.id).toBe("irc"); + }); + + it("loads the setup plugin without importing the broad api barrel", () => { + const plugin = setupEntry.loadSetupPlugin(); + expect(plugin.id).toBe("irc"); + }); +}); diff --git a/extensions/irc/index.ts b/extensions/irc/index.ts index 2fe1f5e3c3b..0df2821a8d8 100644 --- a/extensions/irc/index.ts +++ b/extensions/irc/index.ts @@ -6,11 +6,11 @@ export default defineBundledChannelEntry({ description: "IRC channel plugin", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "ircPlugin", }, runtime: { - specifier: "./api.js", + specifier: "./runtime-api.js", exportName: "setIrcRuntime", }, }); diff --git a/extensions/irc/runtime-api.ts b/extensions/irc/runtime-api.ts new file mode 100644 index 00000000000..00cbd87af5e --- /dev/null +++ b/extensions/irc/runtime-api.ts @@ -0,0 +1,3 @@ +// Keep the bundled runtime entry narrow so generic runtime activation does not +// import the broad IRC API barrel just to install runtime state. +export { setIrcRuntime } from "./src/runtime.js"; diff --git a/extensions/irc/setup-entry.ts b/extensions/irc/setup-entry.ts index 2ea75121957..16e7732960a 100644 --- a/extensions/irc/setup-entry.ts +++ b/extensions/irc/setup-entry.ts @@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr export default defineBundledChannelSetupEntry({ importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "ircPlugin", }, }); diff --git a/extensions/irc/src/channel-runtime.ts b/extensions/irc/src/channel-runtime.ts new file mode 100644 index 00000000000..26984c543ae --- /dev/null +++ b/extensions/irc/src/channel-runtime.ts @@ -0,0 +1,4 @@ +// Runtime-only IRC helpers for lazy chat plugin hooks. +// Keeping this boundary separate keeps bundled entry loads off monitor/send. +export { monitorIrcProvider } from "./monitor.js"; +export { sendMessageIrc } from "./send.js"; diff --git a/extensions/irc/src/channel.ts b/extensions/irc/src/channel.ts index 376ec08cab2..683a5654dce 100644 --- a/extensions/irc/src/channel.ts +++ b/extensions/irc/src/channel.ts @@ -37,7 +37,6 @@ import { } from "./channel-api.js"; import { IrcChannelConfigSchema } from "./config-schema.js"; import { collectIrcMutableAllowlistWarnings } from "./doctor.js"; -import { monitorIrcProvider } from "./monitor.js"; import { normalizeIrcMessagingTarget, looksLikeIrcTargetId, @@ -48,7 +47,6 @@ import { resolveIrcGroupMatch, resolveIrcRequireMention } from "./policy.js"; import { probeIrc } from "./probe.js"; import { getIrcRuntime } from "./runtime.js"; import { collectRuntimeConfigAssignments, secretTargetRegistryEntries } from "./secret-contract.js"; -import { sendMessageIrc } from "./send.js"; import { ircSetupAdapter } from "./setup-core.js"; import { ircSetupWizard } from "./setup-surface.js"; import type { CoreConfig, IrcProbe } from "./types.js"; @@ -66,6 +64,15 @@ const meta = { markdownCapable: true, }; +type IrcChannelRuntimeModule = typeof import("./channel-runtime.js"); + +let ircChannelRuntimePromise: Promise | undefined; + +async function loadIrcChannelRuntime(): Promise { + ircChannelRuntimePromise ??= import("./channel-runtime.js"); + return await ircChannelRuntimePromise; +} + function normalizePairingTarget(raw: string): string { const normalized = normalizeIrcAllowEntry(raw); if (!normalized) { @@ -325,6 +332,7 @@ export const ircPlugin: ChannelPlugin = createChat ctx.log?.info( `[${account.accountId}] starting IRC provider (${account.host}:${account.port}${account.tls ? " tls" : ""})`, ); + const { monitorIrcProvider } = await loadIrcChannelRuntime(); await runStoppablePassiveMonitor({ abortSignal: ctx.abortSignal, start: async () => @@ -349,6 +357,7 @@ export const ircPlugin: ChannelPlugin = createChat if (!target) { throw new Error(`invalid IRC pairing id: ${id}`); } + const { sendMessageIrc } = await loadIrcChannelRuntime(); await sendMessageIrc(target, message); }, }, @@ -367,18 +376,22 @@ export const ircPlugin: ChannelPlugin = createChat }, attachedResults: { channel: "irc", - sendText: async ({ cfg, to, text, accountId, replyToId }) => - await sendMessageIrc(to, text, { + sendText: async ({ cfg, to, text, accountId, replyToId }) => { + const { sendMessageIrc } = await loadIrcChannelRuntime(); + return await sendMessageIrc(to, text, { cfg: cfg as CoreConfig, accountId: accountId ?? undefined, replyTo: replyToId ?? undefined, - }), - sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => - await sendMessageIrc(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, { + }); + }, + sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => { + const { sendMessageIrc } = await loadIrcChannelRuntime(); + return await sendMessageIrc(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, { cfg: cfg as CoreConfig, accountId: accountId ?? undefined, replyTo: replyToId ?? undefined, - }), + }); + }, }, }, }); diff --git a/extensions/irc/src/setup.test.ts b/extensions/irc/src/setup.test.ts index 5bf534ba1bf..598c8b7faeb 100644 --- a/extensions/irc/src/setup.test.ts +++ b/extensions/irc/src/setup.test.ts @@ -28,13 +28,13 @@ import type { CoreConfig } from "./types.js"; const hoisted = vi.hoisted(() => ({ monitorIrcProvider: vi.fn(), + sendMessageIrc: vi.fn(), })); -vi.mock("./monitor.js", async () => { - const actual = await vi.importActual("./monitor.js"); +vi.mock("./channel-runtime.js", () => { return { - ...actual, monitorIrcProvider: hoisted.monitorIrcProvider, + sendMessageIrc: hoisted.sendMessageIrc, }; }); diff --git a/extensions/lobster/src/lobster-taskflow.test.ts b/extensions/lobster/src/lobster-taskflow.test.ts index 3be9c46d7ea..73d771dbcf2 100644 --- a/extensions/lobster/src/lobster-taskflow.test.ts +++ b/extensions/lobster/src/lobster-taskflow.test.ts @@ -7,16 +7,6 @@ type BoundTaskFlow = ReturnType< NonNullable["taskFlow"]["bindSession"] >; -function expectManagedFlowFailure( - result: Awaited>, -) { - expect(result.ok).toBe(false); - if (result.ok) { - throw new Error("Expected managed Lobster flow to fail"); - } - return result; -} - function createFakeTaskFlow(overrides?: Partial) { const baseFlow = { flowId: "flow-1", @@ -170,8 +160,11 @@ describe("runManagedLobsterFlow", () => { goal: "Run Lobster workflow", }); - const failure = expectManagedFlowFailure(result); - expect(failure.error.message).toBe("boom"); + expect(result.ok).toBe(false); + if (result.ok) { + throw new Error("expected managed Lobster flow to fail"); + } + expect(result.error.message).toBe("boom"); expect(taskFlow.fail).toHaveBeenCalledWith({ flowId: "flow-1", expectedRevision: 1, @@ -198,8 +191,11 @@ describe("runManagedLobsterFlow", () => { goal: "Run Lobster workflow", }); - const failure = expectManagedFlowFailure(result); - expect(failure.error.message).toBe("crashed"); + expect(result.ok).toBe(false); + if (result.ok) { + throw new Error("expected managed Lobster flow to fail"); + } + expect(result.error.message).toBe("crashed"); expect(taskFlow.fail).toHaveBeenCalledWith({ flowId: "flow-1", expectedRevision: 1, @@ -274,8 +270,11 @@ describe("resumeManagedLobsterFlow", () => { }, }); - const failure = expectManagedFlowFailure(result); - expect(failure.error.message).toMatch(/revision_conflict/); + expect(result.ok).toBe(false); + if (result.ok) { + throw new Error("expected resumed Lobster flow to fail"); + } + expect(result.error.message).toMatch(/revision_conflict/); expect(runner.run).not.toHaveBeenCalled(); }); diff --git a/extensions/lobster/src/lobster-taskflow.ts b/extensions/lobster/src/lobster-taskflow.ts index 04c7e62a854..1dfd2f8e488 100644 --- a/extensions/lobster/src/lobster-taskflow.ts +++ b/extensions/lobster/src/lobster-taskflow.ts @@ -101,8 +101,6 @@ function toJsonLike(value: unknown, seen = new WeakSet()): JsonLike { seen.delete(value); return jsonObject; } - default: - return null; } return null; } diff --git a/extensions/matrix/channel-plugin-api.ts b/extensions/matrix/channel-plugin-api.ts index 20c485c90b8..4129dc75cc5 100644 --- a/extensions/matrix/channel-plugin-api.ts +++ b/extensions/matrix/channel-plugin-api.ts @@ -1,3 +1,3 @@ // Keep bundled channel entry imports narrow so bootstrap/discovery paths do -// not drag Matrix setup and onboarding helpers into lightweight plugin loads. +// not pull the broad Matrix API barrel into lightweight plugin loads. export { matrixPlugin } from "./src/channel.js"; diff --git a/extensions/matrix/cli-metadata.ts b/extensions/matrix/cli-metadata.ts index aad8ffba7ef..a3f03a9ceaf 100644 --- a/extensions/matrix/cli-metadata.ts +++ b/extensions/matrix/cli-metadata.ts @@ -1,6 +1,8 @@ import { definePluginEntry } from "openclaw/plugin-sdk/core"; import { registerMatrixCliMetadata } from "./src/cli-metadata.js"; +export { registerMatrixCliMetadata } from "./src/cli-metadata.js"; + export default definePluginEntry({ id: "matrix", name: "Matrix", diff --git a/extensions/matrix/index.ts b/extensions/matrix/index.ts index 86a4e52f1b4..1395ae4084c 100644 --- a/extensions/matrix/index.ts +++ b/extensions/matrix/index.ts @@ -1,6 +1,6 @@ import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/channel-entry-contract"; -import { registerMatrixCliMetadata } from "./src/cli-metadata.js"; +import { registerMatrixCliMetadata } from "./cli-metadata.js"; export default defineBundledChannelEntry({ id: "matrix", diff --git a/scripts/lib/bundled-runtime-sidecar-paths.json b/scripts/lib/bundled-runtime-sidecar-paths.json index 15d7bfe17a8..bcf07ed612f 100644 --- a/scripts/lib/bundled-runtime-sidecar-paths.json +++ b/scripts/lib/bundled-runtime-sidecar-paths.json @@ -9,6 +9,7 @@ "dist/extensions/google/runtime-api.js", "dist/extensions/googlechat/runtime-api.js", "dist/extensions/imessage/runtime-api.js", + "dist/extensions/irc/runtime-api.js", "dist/extensions/line/runtime-api.js", "dist/extensions/lobster/runtime-api.js", "dist/extensions/matrix/helper-api.js", diff --git a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts index eeceb833dc7..fbe4bc96e73 100644 --- a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts @@ -89,6 +89,9 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { 'export { createWebhookInFlightLimiter, readJsonWebhookBodyOrReject, type WebhookInFlightLimiter } from "openclaw/plugin-sdk/webhook-request-guards";', 'export { setGoogleChatRuntime } from "./src/runtime.js";', ], + [bundledPluginFile("irc", "runtime-api.ts")]: [ + 'export { setIrcRuntime } from "./src/runtime.js";', + ], [bundledPluginFile("matrix", "runtime-api.ts")]: [ 'export * from "./src/auth-precedence.js";', 'export { requiresExplicitMatrixDefaultAccount, resolveMatrixDefaultOrOnlyAccountId } from "./src/account-selection.js";',