diff --git a/src/auto-reply/reply/commands-acp.test.ts b/src/auto-reply/reply/commands-acp.test.ts index e41fbd80ec2..5d732e4b4e6 100644 --- a/src/auto-reply/reply/commands-acp.test.ts +++ b/src/auto-reply/reply/commands-acp.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { AcpRuntimeError } from "../../acp/runtime/errors.js"; +import { setDefaultChannelPluginRegistryForTests } from "../../commands/channel-test-helpers.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionBindingRecord } from "../../infra/outbound/session-binding-service.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js"; @@ -395,6 +396,7 @@ async function runInternalAcpCommand(params: { describe("/acp command", () => { beforeEach(() => { + setDefaultChannelPluginRegistryForTests(); acpManagerTesting.resetAcpSessionManagerForTests(); hoisted.listAcpSessionEntriesMock.mockReset().mockResolvedValue([]); hoisted.callGatewayMock.mockReset().mockResolvedValue({ ok: true }); diff --git a/src/auto-reply/reply/commands-acp/context.ts b/src/auto-reply/reply/commands-acp/context.ts index fd5eb50ee09..59db08384af 100644 --- a/src/auto-reply/reply/commands-acp/context.ts +++ b/src/auto-reply/reply/commands-acp/context.ts @@ -1,4 +1,3 @@ -import { buildFeishuConversationId } from "../../../../extensions/feishu/src/conversation-id.js"; import { buildTelegramTopicConversationId, normalizeConversationText, @@ -7,6 +6,7 @@ import { import { DISCORD_THREAD_BINDING_CHANNEL } from "../../../channels/thread-bindings-policy.js"; import { resolveConversationIdFromTargets } from "../../../infra/outbound/conversation-id.js"; import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js"; +import { buildFeishuConversationId } from "../../../plugin-sdk/feishu.js"; import { parseAgentSessionKey } from "../../../routing/session-key.js"; import type { HandleCommandsParams } from "../commands-types.js"; import { parseDiscordParentChannelFromSessionKey } from "../discord-parent-channel.js"; diff --git a/src/auto-reply/templating.ts b/src/auto-reply/templating.ts index c97584aeae3..a32fdc3ba87 100644 --- a/src/auto-reply/templating.ts +++ b/src/auto-reply/templating.ts @@ -1,9 +1,9 @@ -import type { StickerMetadata } from "../../extensions/telegram/src/bot/types.js"; import type { ChannelId } from "../channels/plugins/types.js"; import type { MediaUnderstandingDecision, MediaUnderstandingOutput, } from "../media-understanding/types.js"; +import type { StickerMetadata } from "../plugin-sdk/telegram.js"; import type { InputProvenance } from "../sessions/input-provenance.js"; import type { InternalMessageChannel } from "../utils/message-channel.js"; import type { CommandArgs } from "./commands-registry.types.js"; diff --git a/src/channels/plugins/actions/discord.ts b/src/channels/plugins/actions/discord.ts index 6b8689effb3..ccf1b489035 100644 --- a/src/channels/plugins/actions/discord.ts +++ b/src/channels/plugins/actions/discord.ts @@ -1,2 +1,2 @@ -// Shim: re-exports from extension +// Public entrypoint for the Discord channel action adapter. export * from "../../../../extensions/discord/src/channel-actions.js"; diff --git a/src/channels/plugins/actions/telegram.ts b/src/channels/plugins/actions/telegram.ts index 57a690d2208..7961baf334f 100644 --- a/src/channels/plugins/actions/telegram.ts +++ b/src/channels/plugins/actions/telegram.ts @@ -1 +1,2 @@ +// Public entrypoint for the Telegram channel action adapter. export * from "../../../../extensions/telegram/src/channel-actions.js"; diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts new file mode 100644 index 00000000000..c7cae53de20 --- /dev/null +++ b/src/channels/plugins/bundled.ts @@ -0,0 +1,88 @@ +import { bluebubblesPlugin } from "../../../extensions/bluebubbles/src/channel.js"; +import { discordPlugin } from "../../../extensions/discord/src/channel.js"; +import { discordSetupPlugin } from "../../../extensions/discord/src/channel.setup.js"; +import { setDiscordRuntime } from "../../../extensions/discord/src/runtime.js"; +import { feishuPlugin } from "../../../extensions/feishu/src/channel.js"; +import { googlechatPlugin } from "../../../extensions/googlechat/src/channel.js"; +import { imessagePlugin } from "../../../extensions/imessage/src/channel.js"; +import { imessageSetupPlugin } from "../../../extensions/imessage/src/channel.setup.js"; +import { ircPlugin } from "../../../extensions/irc/src/channel.js"; +import { linePlugin } from "../../../extensions/line/src/channel.js"; +import { lineSetupPlugin } from "../../../extensions/line/src/channel.setup.js"; +import { setLineRuntime } from "../../../extensions/line/src/runtime.js"; +import { matrixPlugin } from "../../../extensions/matrix/src/channel.js"; +import { mattermostPlugin } from "../../../extensions/mattermost/src/channel.js"; +import { msteamsPlugin } from "../../../extensions/msteams/src/channel.js"; +import { nextcloudTalkPlugin } from "../../../extensions/nextcloud-talk/src/channel.js"; +import { nostrPlugin } from "../../../extensions/nostr/src/channel.js"; +import { signalPlugin } from "../../../extensions/signal/src/channel.js"; +import { signalSetupPlugin } from "../../../extensions/signal/src/channel.setup.js"; +import { slackPlugin } from "../../../extensions/slack/src/channel.js"; +import { slackSetupPlugin } from "../../../extensions/slack/src/channel.setup.js"; +import { synologyChatPlugin } from "../../../extensions/synology-chat/src/channel.js"; +import { telegramPlugin } from "../../../extensions/telegram/src/channel.js"; +import { telegramSetupPlugin } from "../../../extensions/telegram/src/channel.setup.js"; +import { setTelegramRuntime } from "../../../extensions/telegram/src/runtime.js"; +import { tlonPlugin } from "../../../extensions/tlon/src/channel.js"; +import { whatsappPlugin } from "../../../extensions/whatsapp/src/channel.js"; +import { whatsappSetupPlugin } from "../../../extensions/whatsapp/src/channel.setup.js"; +import { zaloPlugin } from "../../../extensions/zalo/src/channel.js"; +import { zalouserPlugin } from "../../../extensions/zalouser/src/channel.js"; +import type { ChannelId, ChannelPlugin } from "./types.js"; + +export const bundledChannelPlugins = [ + bluebubblesPlugin, + discordPlugin, + feishuPlugin, + googlechatPlugin, + imessagePlugin, + ircPlugin, + linePlugin, + matrixPlugin, + mattermostPlugin, + msteamsPlugin, + nextcloudTalkPlugin, + nostrPlugin, + signalPlugin, + slackPlugin, + synologyChatPlugin, + telegramPlugin, + tlonPlugin, + whatsappPlugin, + zaloPlugin, + zalouserPlugin, +] as ChannelPlugin[]; + +export const bundledChannelSetupPlugins = [ + telegramSetupPlugin, + whatsappSetupPlugin, + discordSetupPlugin, + ircPlugin, + googlechatPlugin, + slackSetupPlugin, + signalSetupPlugin, + imessageSetupPlugin, + lineSetupPlugin, +] as ChannelPlugin[]; + +const bundledChannelPluginsById = new Map( + bundledChannelPlugins.map((plugin) => [plugin.id, plugin] as const), +); + +export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefined { + return bundledChannelPluginsById.get(id); +} + +export function requireBundledChannelPlugin(id: ChannelId): ChannelPlugin { + const plugin = getBundledChannelPlugin(id); + if (!plugin) { + throw new Error(`missing bundled channel plugin: ${id}`); + } + return plugin; +} + +export const bundledChannelRuntimeSetters = { + setDiscordRuntime, + setLineRuntime, + setTelegramRuntime, +}; diff --git a/src/channels/plugins/contracts/registry.ts b/src/channels/plugins/contracts/registry.ts index f5bb1d845e2..567181cef46 100644 --- a/src/channels/plugins/contracts/registry.ts +++ b/src/channels/plugins/contracts/registry.ts @@ -1,33 +1,11 @@ import { expect, vi } from "vitest"; -import { bluebubblesPlugin } from "../../../../extensions/bluebubbles/src/channel.js"; -import { discordPlugin } from "../../../../extensions/discord/src/channel.js"; -import { setDiscordRuntime } from "../../../../extensions/discord/src/runtime.js"; -import { feishuPlugin } from "../../../../extensions/feishu/src/channel.js"; -import { googlechatPlugin } from "../../../../extensions/googlechat/src/channel.js"; -import { imessagePlugin } from "../../../../extensions/imessage/src/channel.js"; -import { ircPlugin } from "../../../../extensions/irc/src/channel.js"; -import { linePlugin } from "../../../../extensions/line/src/channel.js"; -import { setLineRuntime } from "../../../../extensions/line/src/runtime.js"; -import { matrixPlugin } from "../../../../extensions/matrix/src/channel.js"; -import { mattermostPlugin } from "../../../../extensions/mattermost/src/channel.js"; -import { msteamsPlugin } from "../../../../extensions/msteams/src/channel.js"; -import { nextcloudTalkPlugin } from "../../../../extensions/nextcloud-talk/src/channel.js"; -import { nostrPlugin } from "../../../../extensions/nostr/src/channel.js"; -import { signalPlugin } from "../../../../extensions/signal/src/channel.js"; -import { slackPlugin } from "../../../../extensions/slack/src/channel.js"; -import { synologyChatPlugin } from "../../../../extensions/synology-chat/src/channel.js"; -import { telegramPlugin } from "../../../../extensions/telegram/src/channel.js"; -import { setTelegramRuntime } from "../../../../extensions/telegram/src/runtime.js"; -import { tlonPlugin } from "../../../../extensions/tlon/src/channel.js"; -import { whatsappPlugin } from "../../../../extensions/whatsapp/src/channel.js"; -import { zaloPlugin } from "../../../../extensions/zalo/src/channel.js"; -import { zalouserPlugin } from "../../../../extensions/zalouser/src/channel.js"; import type { OpenClawConfig } from "../../../config/config.js"; import { resolveDefaultLineAccountId, resolveLineAccount, listLineAccountIds, } from "../../../line/accounts.js"; +import { bundledChannelRuntimeSetters, requireBundledChannelPlugin } from "../bundled.js"; import type { ChannelPlugin } from "../types.js"; type PluginContractEntry = { @@ -84,7 +62,7 @@ const telegramGetCapabilitiesMock = vi.fn(); const discordListActionsMock = vi.fn(); const discordGetCapabilitiesMock = vi.fn(); -setTelegramRuntime({ +bundledChannelRuntimeSetters.setTelegramRuntime({ channel: { telegram: { messageActions: { @@ -95,7 +73,7 @@ setTelegramRuntime({ }, } as never); -setDiscordRuntime({ +bundledChannelRuntimeSetters.setDiscordRuntime({ channel: { discord: { messageActions: { @@ -106,7 +84,7 @@ setDiscordRuntime({ }, } as never); -setLineRuntime({ +bundledChannelRuntimeSetters.setLineRuntime({ channel: { line: { listLineAccountIds, @@ -118,32 +96,32 @@ setLineRuntime({ } as never); export const pluginContractRegistry: PluginContractEntry[] = [ - { id: "bluebubbles", plugin: bluebubblesPlugin }, - { id: "discord", plugin: discordPlugin }, - { id: "feishu", plugin: feishuPlugin }, - { id: "googlechat", plugin: googlechatPlugin }, - { id: "imessage", plugin: imessagePlugin }, - { id: "irc", plugin: ircPlugin }, - { id: "line", plugin: linePlugin }, - { id: "matrix", plugin: matrixPlugin }, - { id: "mattermost", plugin: mattermostPlugin }, - { id: "msteams", plugin: msteamsPlugin }, - { id: "nextcloud-talk", plugin: nextcloudTalkPlugin }, - { id: "nostr", plugin: nostrPlugin }, - { id: "signal", plugin: signalPlugin }, - { id: "slack", plugin: slackPlugin }, - { id: "synology-chat", plugin: synologyChatPlugin }, - { id: "telegram", plugin: telegramPlugin }, - { id: "tlon", plugin: tlonPlugin }, - { id: "whatsapp", plugin: whatsappPlugin }, - { id: "zalo", plugin: zaloPlugin }, - { id: "zalouser", plugin: zalouserPlugin }, + { id: "bluebubbles", plugin: requireBundledChannelPlugin("bluebubbles") }, + { id: "discord", plugin: requireBundledChannelPlugin("discord") }, + { id: "feishu", plugin: requireBundledChannelPlugin("feishu") }, + { id: "googlechat", plugin: requireBundledChannelPlugin("googlechat") }, + { id: "imessage", plugin: requireBundledChannelPlugin("imessage") }, + { id: "irc", plugin: requireBundledChannelPlugin("irc") }, + { id: "line", plugin: requireBundledChannelPlugin("line") }, + { id: "matrix", plugin: requireBundledChannelPlugin("matrix") }, + { id: "mattermost", plugin: requireBundledChannelPlugin("mattermost") }, + { id: "msteams", plugin: requireBundledChannelPlugin("msteams") }, + { id: "nextcloud-talk", plugin: requireBundledChannelPlugin("nextcloud-talk") }, + { id: "nostr", plugin: requireBundledChannelPlugin("nostr") }, + { id: "signal", plugin: requireBundledChannelPlugin("signal") }, + { id: "slack", plugin: requireBundledChannelPlugin("slack") }, + { id: "synology-chat", plugin: requireBundledChannelPlugin("synology-chat") }, + { id: "telegram", plugin: requireBundledChannelPlugin("telegram") }, + { id: "tlon", plugin: requireBundledChannelPlugin("tlon") }, + { id: "whatsapp", plugin: requireBundledChannelPlugin("whatsapp") }, + { id: "zalo", plugin: requireBundledChannelPlugin("zalo") }, + { id: "zalouser", plugin: requireBundledChannelPlugin("zalouser") }, ]; export const actionContractRegistry: ActionsContractEntry[] = [ { id: "slack", - plugin: slackPlugin, + plugin: requireBundledChannelPlugin("slack"), unsupportedAction: "poll", cases: [ { @@ -217,7 +195,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [ }, { id: "mattermost", - plugin: mattermostPlugin, + plugin: requireBundledChannelPlugin("mattermost"), unsupportedAction: "poll", cases: [ { @@ -265,7 +243,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [ }, { id: "telegram", - plugin: telegramPlugin, + plugin: requireBundledChannelPlugin("telegram"), cases: [ { name: "forwards runtime-backed Telegram actions and capabilities", @@ -283,7 +261,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [ }, { id: "discord", - plugin: discordPlugin, + plugin: requireBundledChannelPlugin("discord"), cases: [ { name: "forwards runtime-backed Discord actions and capabilities", @@ -304,7 +282,7 @@ export const actionContractRegistry: ActionsContractEntry[] = [ export const setupContractRegistry: SetupContractEntry[] = [ { id: "slack", - plugin: slackPlugin, + plugin: requireBundledChannelPlugin("slack"), cases: [ { name: "default account stores tokens and enables the channel", @@ -334,7 +312,7 @@ export const setupContractRegistry: SetupContractEntry[] = [ }, { id: "mattermost", - plugin: mattermostPlugin, + plugin: requireBundledChannelPlugin("mattermost"), cases: [ { name: "default account stores token and normalized base URL", @@ -363,7 +341,7 @@ export const setupContractRegistry: SetupContractEntry[] = [ }, { id: "line", - plugin: linePlugin, + plugin: requireBundledChannelPlugin("line"), cases: [ { name: "default account stores token and secret", @@ -396,7 +374,7 @@ export const setupContractRegistry: SetupContractEntry[] = [ export const statusContractRegistry: StatusContractEntry[] = [ { id: "slack", - plugin: slackPlugin, + plugin: requireBundledChannelPlugin("slack"), cases: [ { name: "configured account produces a configured status snapshot", @@ -424,7 +402,7 @@ export const statusContractRegistry: StatusContractEntry[] = [ }, { id: "mattermost", - plugin: mattermostPlugin, + plugin: requireBundledChannelPlugin("mattermost"), cases: [ { name: "configured account preserves connectivity details in the snapshot", @@ -455,7 +433,7 @@ export const statusContractRegistry: StatusContractEntry[] = [ }, { id: "line", - plugin: linePlugin, + plugin: requireBundledChannelPlugin("line"), cases: [ { name: "configured account produces a webhook status snapshot", diff --git a/src/channels/plugins/normalize/discord.ts b/src/channels/plugins/normalize/discord.ts deleted file mode 100644 index e4fcc4e9c00..00000000000 --- a/src/channels/plugins/normalize/discord.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Shim: re-exports from extension -export * from "../../../../extensions/discord/src/normalize.js"; diff --git a/src/channels/plugins/normalize/telegram.ts b/src/channels/plugins/normalize/telegram.ts deleted file mode 100644 index ab3971ff32b..00000000000 --- a/src/channels/plugins/normalize/telegram.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../../extensions/telegram/src/normalize.js"; diff --git a/src/channels/plugins/normalize/whatsapp.ts b/src/channels/plugins/normalize/whatsapp.ts index 1e464489818..edff8bfe5e1 100644 --- a/src/channels/plugins/normalize/whatsapp.ts +++ b/src/channels/plugins/normalize/whatsapp.ts @@ -1,2 +1,25 @@ -// Shim: re-exports from extensions/whatsapp/src/normalize.ts -export * from "../../../../extensions/whatsapp/src/normalize.js"; +import { normalizeWhatsAppTarget } from "../../../whatsapp/normalize.js"; +import { looksLikeHandleOrPhoneTarget, trimMessagingTarget } from "./shared.js"; + +export function normalizeWhatsAppMessagingTarget(raw: string): string | undefined { + const trimmed = trimMessagingTarget(raw); + if (!trimmed) { + return undefined; + } + return normalizeWhatsAppTarget(trimmed) ?? undefined; +} + +export function normalizeWhatsAppAllowFromEntries(allowFrom: Array): string[] { + return allowFrom + .map((entry) => String(entry).trim()) + .filter((entry): entry is string => Boolean(entry)) + .map((entry) => (entry === "*" ? entry : normalizeWhatsAppTarget(entry))) + .filter((entry): entry is string => Boolean(entry)); +} + +export function looksLikeWhatsAppTargetId(raw: string): boolean { + return looksLikeHandleOrPhoneTarget({ + raw, + prefixPattern: /^whatsapp:/i, + }); +} diff --git a/src/channels/plugins/setup-registry.ts b/src/channels/plugins/setup-registry.ts index a8c7212ca1f..bbb82022da9 100644 --- a/src/channels/plugins/setup-registry.ts +++ b/src/channels/plugins/setup-registry.ts @@ -1,17 +1,9 @@ -import { discordSetupPlugin } from "../../../extensions/discord/src/channel.setup.js"; -import { googlechatPlugin } from "../../../extensions/googlechat/src/channel.js"; -import { imessageSetupPlugin } from "../../../extensions/imessage/src/channel.setup.js"; -import { ircPlugin } from "../../../extensions/irc/src/channel.js"; -import { lineSetupPlugin } from "../../../extensions/line/src/channel.setup.js"; -import { signalSetupPlugin } from "../../../extensions/signal/src/channel.setup.js"; -import { slackSetupPlugin } from "../../../extensions/slack/src/channel.setup.js"; -import { telegramSetupPlugin } from "../../../extensions/telegram/src/channel.setup.js"; -import { whatsappSetupPlugin } from "../../../extensions/whatsapp/src/channel.setup.js"; import { getActivePluginRegistryVersion, requireActivePluginRegistry, } from "../../plugins/runtime.js"; import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js"; +import { bundledChannelSetupPlugins } from "./bundled.js"; import type { ChannelId, ChannelPlugin } from "./types.js"; type CachedChannelSetupPlugins = { @@ -28,18 +20,6 @@ const EMPTY_CHANNEL_SETUP_CACHE: CachedChannelSetupPlugins = { let cachedChannelSetupPlugins = EMPTY_CHANNEL_SETUP_CACHE; -const BUNDLED_CHANNEL_SETUP_PLUGINS = [ - telegramSetupPlugin, - whatsappSetupPlugin, - discordSetupPlugin, - ircPlugin, - googlechatPlugin, - slackSetupPlugin, - signalSetupPlugin, - imessageSetupPlugin, - lineSetupPlugin, -] as ChannelPlugin[]; - function dedupeSetupPlugins(plugins: ChannelPlugin[]): ChannelPlugin[] { const seen = new Set(); const resolved: ChannelPlugin[] = []; @@ -77,7 +57,7 @@ function resolveCachedChannelSetupPlugins(): CachedChannelSetupPlugins { const registryPlugins = (registry.channelSetups ?? []).map((entry) => entry.plugin); const sorted = sortChannelSetupPlugins( - registryPlugins.length > 0 ? registryPlugins : BUNDLED_CHANNEL_SETUP_PLUGINS, + registryPlugins.length > 0 ? registryPlugins : bundledChannelSetupPlugins, ); const byId = new Map(); for (const plugin of sorted) { diff --git a/src/channels/plugins/status-issues/discord.ts b/src/channels/plugins/status-issues/discord.ts deleted file mode 100644 index f42578df1e9..00000000000 --- a/src/channels/plugins/status-issues/discord.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Shim: re-exports from extension -export * from "../../../../extensions/discord/src/status-issues.js"; diff --git a/src/channels/plugins/status-issues/telegram.ts b/src/channels/plugins/status-issues/telegram.ts deleted file mode 100644 index 26425a07ae4..00000000000 --- a/src/channels/plugins/status-issues/telegram.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "../../../../extensions/telegram/src/status-issues.js"; diff --git a/src/channels/plugins/status-issues/whatsapp.ts b/src/channels/plugins/status-issues/whatsapp.ts deleted file mode 100644 index 45be4231ed2..00000000000 --- a/src/channels/plugins/status-issues/whatsapp.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Shim: re-exports from extensions/whatsapp/src/status-issues.ts -export * from "../../../../extensions/whatsapp/src/status-issues.js"; diff --git a/src/commands/channel-test-helpers.ts b/src/commands/channel-test-helpers.ts index a76f747b326..38f3621146f 100644 --- a/src/commands/channel-test-helpers.ts +++ b/src/commands/channel-test-helpers.ts @@ -1,10 +1,4 @@ -import { discordPlugin } from "../../extensions/discord/src/channel.js"; -import { feishuPlugin } from "../../extensions/feishu/src/channel.js"; -import { imessagePlugin } from "../../extensions/imessage/src/channel.js"; -import { signalPlugin } from "../../extensions/signal/src/channel.js"; -import { slackPlugin } from "../../extensions/slack/src/channel.js"; -import { telegramPlugin } from "../../extensions/telegram/src/channel.js"; -import { whatsappPlugin } from "../../extensions/whatsapp/src/channel.js"; +import { requireBundledChannelPlugin } from "../channels/plugins/bundled.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createTestRegistry } from "../test-utils/channel-plugins.js"; import { getChannelSetupWizardAdapter } from "./channel-setup/registry.js"; @@ -27,13 +21,13 @@ type PatchedSetupAdapterFields = { export function setDefaultChannelPluginRegistryForTests(): void { const channels = [ - { pluginId: "discord", plugin: discordPlugin, source: "test" }, - { pluginId: "feishu", plugin: feishuPlugin, source: "test" }, - { pluginId: "slack", plugin: slackPlugin, source: "test" }, - { pluginId: "telegram", plugin: telegramPlugin, source: "test" }, - { pluginId: "whatsapp", plugin: whatsappPlugin, source: "test" }, - { pluginId: "signal", plugin: signalPlugin, source: "test" }, - { pluginId: "imessage", plugin: imessagePlugin, source: "test" }, + { pluginId: "discord", plugin: requireBundledChannelPlugin("discord"), source: "test" }, + { pluginId: "feishu", plugin: requireBundledChannelPlugin("feishu"), source: "test" }, + { pluginId: "slack", plugin: requireBundledChannelPlugin("slack"), source: "test" }, + { pluginId: "telegram", plugin: requireBundledChannelPlugin("telegram"), source: "test" }, + { pluginId: "whatsapp", plugin: requireBundledChannelPlugin("whatsapp"), source: "test" }, + { pluginId: "signal", plugin: requireBundledChannelPlugin("signal"), source: "test" }, + { pluginId: "imessage", plugin: requireBundledChannelPlugin("imessage"), source: "test" }, ] as unknown as Parameters[0]; setActivePluginRegistry(createTestRegistry(channels)); } diff --git a/src/config/plugin-auto-enable.ts b/src/config/plugin-auto-enable.ts index 2a7524b2558..1c93c61c800 100644 --- a/src/config/plugin-auto-enable.ts +++ b/src/config/plugin-auto-enable.ts @@ -1,4 +1,3 @@ -import { hasAnyWhatsAppAuth } from "../../extensions/whatsapp/src/accounts.js"; import { normalizeProviderId } from "../agents/model-selection.js"; import { getChannelPluginCatalogEntry, @@ -9,6 +8,7 @@ import { listChatChannels, normalizeChatChannelId, } from "../channels/registry.js"; +import { hasAnyWhatsAppAuth } from "../plugin-sdk/whatsapp.js"; import { loadPluginManifestRegistry, type PluginManifestRegistry, diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 46cce98d193..3d93ee58465 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -1,7 +1,7 @@ import { DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS, DISCORD_DEFAULT_LISTENER_TIMEOUT_MS, -} from "../../extensions/discord/src/monitor/timeouts.js"; +} from "../plugin-sdk/discord.js"; import { MEDIA_AUDIO_FIELD_HELP } from "./media-audio-field-metadata.js"; import { IRC_FIELD_HELP } from "./schema.irc.js"; import { describeTalkSilenceTimeoutDefaults } from "./talk-defaults.js"; diff --git a/src/config/sessions/explicit-session-key-normalization.ts b/src/config/sessions/explicit-session-key-normalization.ts index 984b70487a3..08543e5a6d0 100644 --- a/src/config/sessions/explicit-session-key-normalization.ts +++ b/src/config/sessions/explicit-session-key-normalization.ts @@ -1,5 +1,5 @@ -import { normalizeExplicitDiscordSessionKey } from "../../../extensions/discord/src/session-key-normalization.js"; import type { MsgContext } from "../../auto-reply/templating.js"; +import { normalizeExplicitDiscordSessionKey } from "../../plugin-sdk/discord.js"; type ExplicitSessionKeyNormalizer = (sessionKey: string, ctx: MsgContext) => string; type ExplicitSessionKeyNormalizerEntry = { diff --git a/src/config/types.discord.ts b/src/config/types.discord.ts index a27fd3f8b45..c9269c6b8fd 100644 --- a/src/config/types.discord.ts +++ b/src/config/types.discord.ts @@ -1,4 +1,4 @@ -import type { DiscordPluralKitConfig } from "../../extensions/discord/src/pluralkit.js"; +import type { DiscordPluralKitConfig } from "../plugin-sdk/discord.js"; import type { BlockStreamingChunkConfig, BlockStreamingCoalesceConfig, diff --git a/src/cron/isolated-agent/delivery-target.ts b/src/cron/isolated-agent/delivery-target.ts index 4a70352e233..e903cd15cab 100644 --- a/src/cron/isolated-agent/delivery-target.ts +++ b/src/cron/isolated-agent/delivery-target.ts @@ -1,4 +1,3 @@ -import { resolveWhatsAppAccount } from "../../../extensions/whatsapp/src/accounts.js"; import type { ChannelId } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import { @@ -14,6 +13,7 @@ import { resolveSessionDeliveryTarget, } from "../../infra/outbound/targets.js"; import { readChannelAllowFromStoreSync } from "../../pairing/pairing-store.js"; +import { resolveWhatsAppAccount } from "../../plugin-sdk/whatsapp.js"; import { buildChannelAccountBindings } from "../../routing/bindings.js"; import { normalizeAccountId, normalizeAgentId } from "../../routing/session-key.js"; import { normalizeWhatsAppTarget } from "../../whatsapp/normalize.js"; diff --git a/src/plugin-sdk/account-resolution.ts b/src/plugin-sdk/account-resolution.ts index e25c2cc74cb..4aceec2c945 100644 --- a/src/plugin-sdk/account-resolution.ts +++ b/src/plugin-sdk/account-resolution.ts @@ -1,3 +1,4 @@ +/** Resolve an account by id, then fall back to the default account when the primary lacks credentials. */ export function resolveAccountWithDefaultFallback(params: { accountId?: string | null; normalizeAccountId: (accountId?: string | null) => string; @@ -23,6 +24,7 @@ export function resolveAccountWithDefaultFallback(params: { return fallback; } +/** List normalized configured account ids from a raw channel account record map. */ export function listConfiguredAccountIds(params: { accounts: Record | undefined; normalizeAccountId: (accountId: string) => string; diff --git a/src/plugin-sdk/agent-media-payload.ts b/src/plugin-sdk/agent-media-payload.ts index 98d12a8420b..5fa1fb767f5 100644 --- a/src/plugin-sdk/agent-media-payload.ts +++ b/src/plugin-sdk/agent-media-payload.ts @@ -7,6 +7,7 @@ export type AgentMediaPayload = { MediaTypes?: string[]; }; +/** Convert outbound media descriptors into the legacy agent payload field layout. */ export function buildAgentMediaPayload( mediaList: Array<{ path: string; contentType?: string | null }>, ): AgentMediaPayload { diff --git a/src/plugin-sdk/allow-from.ts b/src/plugin-sdk/allow-from.ts index 9b43a8ced6d..f03f2427558 100644 --- a/src/plugin-sdk/allow-from.ts +++ b/src/plugin-sdk/allow-from.ts @@ -1,3 +1,4 @@ +/** Lowercase and optionally strip prefixes from allowlist entries before sender comparisons. */ export function formatAllowFromLowercase(params: { allowFrom: Array; stripPrefixRe?: RegExp; @@ -9,6 +10,7 @@ export function formatAllowFromLowercase(params: { .map((entry) => entry.toLowerCase()); } +/** Normalize allowlist entries through a channel-provided parser or canonicalizer. */ export function formatNormalizedAllowFromEntries(params: { allowFrom: Array; normalizeEntry: (entry: string) => string | undefined | null; @@ -20,6 +22,7 @@ export function formatNormalizedAllowFromEntries(params: { .filter((entry): entry is string => Boolean(entry)); } +/** Check whether a sender id matches a simple normalized allowlist with wildcard support. */ export function isNormalizedSenderAllowed(params: { senderId: string | number; allowFrom: Array; @@ -45,6 +48,7 @@ type ParsedChatAllowTarget = | { kind: "chat_identifier"; chatIdentifier: string } | { kind: "handle"; handle: string }; +/** Match chat-aware allowlist entries against sender, chat id, guid, or identifier fields. */ export function isAllowedParsedChatSender(params: { allowFrom: Array; sender: string; diff --git a/src/plugin-sdk/allowlist-config-edit.ts b/src/plugin-sdk/allowlist-config-edit.ts index 4c9f10ec278..c9f2a92e3be 100644 --- a/src/plugin-sdk/allowlist-config-edit.ts +++ b/src/plugin-sdk/allowlist-config-edit.ts @@ -183,6 +183,7 @@ function applyAccountScopedAllowlistConfigEdit(params: { }; } +/** Build the default account-scoped allowlist editor used by channel plugins with config-backed lists. */ export function buildAccountScopedAllowlistConfigEditor(params: { channelId: ChannelId; normalize: (params: { diff --git a/src/plugin-sdk/allowlist-resolution.ts b/src/plugin-sdk/allowlist-resolution.ts index 8e955e422b3..1acf87f4d1c 100644 --- a/src/plugin-sdk/allowlist-resolution.ts +++ b/src/plugin-sdk/allowlist-resolution.ts @@ -6,6 +6,7 @@ export type BasicAllowlistResolutionEntry = { note?: string; }; +/** Clone allowlist resolution entries into a plain serializable shape for UI and docs output. */ export function mapBasicAllowlistResolutionEntries( entries: BasicAllowlistResolutionEntry[], ): BasicAllowlistResolutionEntry[] { @@ -18,6 +19,7 @@ export function mapBasicAllowlistResolutionEntries( })); } +/** Map allowlist inputs sequentially so resolver side effects stay ordered and predictable. */ export async function mapAllowlistResolutionInputs(params: { inputs: string[]; mapInput: (input: string) => Promise | T; diff --git a/src/plugin-sdk/boolean-param.ts b/src/plugin-sdk/boolean-param.ts index 4616eaec3b8..9e583027052 100644 --- a/src/plugin-sdk/boolean-param.ts +++ b/src/plugin-sdk/boolean-param.ts @@ -1,3 +1,4 @@ +/** Read loose boolean params from tool input that may arrive as booleans or "true"/"false" strings. */ export function readBooleanParam( params: Record, key: string, diff --git a/src/plugin-sdk/channel-config-helpers.ts b/src/plugin-sdk/channel-config-helpers.ts index aa405c4b9b7..564bc86bc68 100644 --- a/src/plugin-sdk/channel-config-helpers.ts +++ b/src/plugin-sdk/channel-config-helpers.ts @@ -10,16 +10,19 @@ import type { OpenClawConfig } from "../config/config.js"; import { normalizeAccountId } from "../routing/session-key.js"; import { normalizeStringEntries } from "../shared/string-normalization.js"; +/** Coerce mixed allowlist config values into plain strings without trimming or deduping. */ export function mapAllowFromEntries( allowFrom: Array | null | undefined, ): string[] { return (allowFrom ?? []).map((entry) => String(entry)); } +/** Normalize user-facing allowlist entries the same way config and doctor flows expect. */ export function formatTrimmedAllowFromEntries(allowFrom: Array): string[] { return normalizeStringEntries(allowFrom); } +/** Collapse nullable config scalars into a trimmed optional string. */ export function resolveOptionalConfigString( value: string | number | null | undefined, ): string | undefined { @@ -30,6 +33,7 @@ export function resolveOptionalConfigString( return normalized || undefined; } +/** Build the shared allowlist/default target adapter surface for account-scoped channel configs. */ export function createScopedAccountConfigAccessors(params: { resolveAccount: (params: { cfg: OpenClawConfig; accountId?: string | null }) => ResolvedAccount; resolveAllowFrom: (account: ResolvedAccount) => Array | null | undefined; @@ -59,6 +63,7 @@ export function createScopedAccountConfigAccessors(params: { }; } +/** Build the common CRUD/config helpers for channels that store multiple named accounts. */ export function createScopedChannelConfigBase< ResolvedAccount, Config extends OpenClawConfig = OpenClawConfig, @@ -104,6 +109,7 @@ export function createScopedChannelConfigBase< }; } +/** Convert account-specific DM security fields into the shared runtime policy resolver shape. */ export function createScopedDmSecurityResolver< ResolvedAccount extends { accountId?: string | null }, >(params: { @@ -143,6 +149,7 @@ export function createScopedDmSecurityResolver< }); } +/** Read the effective WhatsApp allowlist through the active plugin contract. */ export function resolveWhatsAppConfigAllowFrom(params: { cfg: OpenClawConfig; accountId?: string | null; @@ -153,10 +160,12 @@ export function resolveWhatsAppConfigAllowFrom(params: { : []; } +/** Format WhatsApp allowlist entries with the same normalization used by the channel plugin. */ export function formatWhatsAppConfigAllowFromEntries(allowFrom: Array): string[] { return normalizeWhatsAppAllowFromEntries(allowFrom); } +/** Resolve the effective WhatsApp default recipient after account and root config fallback. */ export function resolveWhatsAppConfigDefaultTo(params: { cfg: OpenClawConfig; accountId?: string | null; @@ -167,6 +176,7 @@ export function resolveWhatsAppConfigDefaultTo(params: { return (account?.defaultTo ?? root?.defaultTo)?.trim() || undefined; } +/** Read iMessage allowlist entries from the active plugin's resolved account view. */ export function resolveIMessageConfigAllowFrom(params: { cfg: OpenClawConfig; accountId?: string | null; @@ -178,6 +188,7 @@ export function resolveIMessageConfigAllowFrom(params: { return mapAllowFromEntries(account.config.allowFrom); } +/** Resolve the effective iMessage default recipient from the plugin-resolved account config. */ export function resolveIMessageConfigDefaultTo(params: { cfg: OpenClawConfig; accountId?: string | null; diff --git a/src/plugin-sdk/channel-lifecycle.ts b/src/plugin-sdk/channel-lifecycle.ts index 7d4fea578d5..28045aeb058 100644 --- a/src/plugin-sdk/channel-lifecycle.ts +++ b/src/plugin-sdk/channel-lifecycle.ts @@ -11,6 +11,7 @@ type PassiveAccountLifecycleParams = { onStop?: () => void | Promise; }; +/** Bind a fixed account id into a status writer so lifecycle code can emit partial snapshots. */ export function createAccountStatusSink(params: { accountId: string; setStatus: (next: ChannelAccountSnapshot) => void; diff --git a/src/plugin-sdk/channel-send-result.ts b/src/plugin-sdk/channel-send-result.ts index e64ff290fea..b73df6f0448 100644 --- a/src/plugin-sdk/channel-send-result.ts +++ b/src/plugin-sdk/channel-send-result.ts @@ -4,6 +4,7 @@ export type ChannelSendRawResult = { error?: string | null; }; +/** Normalize raw channel send results into the shape shared outbound callers expect. */ export function buildChannelSendResult(channel: string, result: ChannelSendRawResult) { return { channel, diff --git a/src/plugin-sdk/command-auth.ts b/src/plugin-sdk/command-auth.ts index 2e95974cf1f..0a09e0c1dcd 100644 --- a/src/plugin-sdk/command-auth.ts +++ b/src/plugin-sdk/command-auth.ts @@ -33,6 +33,7 @@ export type ResolveSenderCommandAuthorizationWithRuntimeParams = Omit< runtime: CommandAuthorizationRuntime; }; +/** Fast-path DM command authorization when only policy and sender allowlist state matter. */ export function resolveDirectDmAuthorizationOutcome(params: { isGroup: boolean; dmPolicy: string; @@ -50,6 +51,7 @@ export function resolveDirectDmAuthorizationOutcome(params: { return "allowed"; } +/** Runtime-backed wrapper around sender command authorization for grouped helper surfaces. */ export async function resolveSenderCommandAuthorizationWithRuntime( params: ResolveSenderCommandAuthorizationWithRuntimeParams, ): ReturnType { @@ -60,6 +62,7 @@ export async function resolveSenderCommandAuthorizationWithRuntime( }); } +/** Compute effective allowlists and command authorization for one inbound sender. */ export async function resolveSenderCommandAuthorization( params: ResolveSenderCommandAuthorizationParams, ): Promise<{ diff --git a/src/plugin-sdk/config-paths.ts b/src/plugin-sdk/config-paths.ts index 06940f1842a..00c67a3036b 100644 --- a/src/plugin-sdk/config-paths.ts +++ b/src/plugin-sdk/config-paths.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "../config/config.js"; +/** Resolve the config path prefix for a channel account, falling back to the root channel section. */ export function resolveChannelAccountConfigBasePath(params: { cfg: OpenClawConfig; channelKey: string; diff --git a/src/plugin-sdk/discord-send.ts b/src/plugin-sdk/discord-send.ts index d3cdaf38a22..6cca5f9f803 100644 --- a/src/plugin-sdk/discord-send.ts +++ b/src/plugin-sdk/discord-send.ts @@ -11,6 +11,7 @@ type DiscordSendMediaOptionInput = DiscordSendOptionInput & { mediaLocalRoots?: readonly string[]; }; +/** Build the common Discord send options from SDK-level reply payload fields. */ export function buildDiscordSendOptions(input: DiscordSendOptionInput) { return { verbose: false, @@ -20,6 +21,7 @@ export function buildDiscordSendOptions(input: DiscordSendOptionInput) { }; } +/** Extend the base Discord send options with media-specific fields. */ export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput) { return { ...buildDiscordSendOptions(input), @@ -28,6 +30,7 @@ export function buildDiscordSendMediaOptions(input: DiscordSendMediaOptionInput) }; } +/** Stamp raw Discord send results with the channel id expected by shared outbound flows. */ export function tagDiscordChannelResult(result: DiscordSendResult) { return { channel: "discord" as const, ...result }; } diff --git a/src/plugin-sdk/discord.ts b/src/plugin-sdk/discord.ts index c2060fa4b3e..d9e40370d23 100644 --- a/src/plugin-sdk/discord.ts +++ b/src/plugin-sdk/discord.ts @@ -23,9 +23,15 @@ export { looksLikeDiscordTargetId, normalizeDiscordMessagingTarget, normalizeDiscordOutboundTarget, -} from "../channels/plugins/normalize/discord.js"; +} from "../../extensions/discord/src/normalize.js"; export { collectDiscordAuditChannelIds } from "../../extensions/discord/src/audit.js"; -export { collectDiscordStatusIssues } from "../channels/plugins/status-issues/discord.js"; +export { collectDiscordStatusIssues } from "../../extensions/discord/src/status-issues.js"; +export { + DISCORD_DEFAULT_INBOUND_WORKER_TIMEOUT_MS, + DISCORD_DEFAULT_LISTENER_TIMEOUT_MS, +} from "../../extensions/discord/src/monitor/timeouts.js"; +export { normalizeExplicitDiscordSessionKey } from "../../extensions/discord/src/session-key-normalization.js"; +export type { DiscordPluralKitConfig } from "../../extensions/discord/src/pluralkit.js"; export { resolveDefaultGroupPolicy, diff --git a/src/plugin-sdk/entrypoints.ts b/src/plugin-sdk/entrypoints.ts index 04b7902de9e..cc337c16fcf 100644 --- a/src/plugin-sdk/entrypoints.ts +++ b/src/plugin-sdk/entrypoints.ts @@ -4,18 +4,21 @@ export const pluginSdkEntrypoints = [...pluginSdkEntryList]; export const pluginSdkSubpaths = pluginSdkEntrypoints.filter((entry) => entry !== "index"); +/** Map every SDK entrypoint name to its source file path inside the repo. */ export function buildPluginSdkEntrySources() { return Object.fromEntries( pluginSdkEntrypoints.map((entry) => [entry, `src/plugin-sdk/${entry}.ts`]), ); } +/** List the public package specifiers that should resolve to plugin SDK entrypoints. */ export function buildPluginSdkSpecifiers() { return pluginSdkEntrypoints.map((entry) => entry === "index" ? "openclaw/plugin-sdk" : `openclaw/plugin-sdk/${entry}`, ); } +/** Build the package.json exports map for all plugin SDK subpaths. */ export function buildPluginSdkPackageExports() { return Object.fromEntries( pluginSdkEntrypoints.map((entry) => [ @@ -28,6 +31,7 @@ export function buildPluginSdkPackageExports() { ); } +/** List the dist artifacts expected for every generated plugin SDK entrypoint. */ export function listPluginSdkDistArtifacts() { return pluginSdkEntrypoints.flatMap((entry) => [ `dist/plugin-sdk/${entry}.js`, diff --git a/src/plugin-sdk/feishu.ts b/src/plugin-sdk/feishu.ts index 4a6d8dc6251..ee15823738b 100644 --- a/src/plugin-sdk/feishu.ts +++ b/src/plugin-sdk/feishu.ts @@ -76,6 +76,10 @@ export { createDefaultChannelRuntimeState, } from "./status-helpers.js"; export { withTempDownloadPath } from "./temp-path.js"; +export { + buildFeishuConversationId, + parseFeishuConversationId, +} from "../../extensions/feishu/src/conversation-id.js"; export { createFixedWindowRateLimiter, createWebhookAnomalyTracker, diff --git a/src/plugin-sdk/fetch-auth.ts b/src/plugin-sdk/fetch-auth.ts index fc04e4aa910..10945bb2a44 100644 --- a/src/plugin-sdk/fetch-auth.ts +++ b/src/plugin-sdk/fetch-auth.ts @@ -6,6 +6,7 @@ function isAuthFailureStatus(status: number): boolean { return status === 401 || status === 403; } +/** Retry a fetch with bearer tokens from the provided scopes when the unauthenticated attempt fails. */ export async function fetchWithBearerAuthScopeFallback(params: { url: string; scopes: readonly string[]; diff --git a/src/plugin-sdk/file-lock.ts b/src/plugin-sdk/file-lock.ts index 98277381868..3870c38fc35 100644 --- a/src/plugin-sdk/file-lock.ts +++ b/src/plugin-sdk/file-lock.ts @@ -100,6 +100,7 @@ async function releaseHeldLock(normalizedFile: string): Promise { await fs.rm(current.lockPath, { force: true }).catch(() => undefined); } +/** Acquire a re-entrant process-local file lock backed by a `.lock` sidecar file. */ export async function acquireFileLock( filePath: string, options: FileLockOptions, @@ -147,6 +148,7 @@ export async function acquireFileLock( throw new Error(`file lock timeout for ${normalizedFile}`); } +/** Run an async callback while holding a file lock, always releasing the lock afterward. */ export async function withFileLock( filePath: string, options: FileLockOptions, diff --git a/src/plugin-sdk/group-access.ts b/src/plugin-sdk/group-access.ts index 5a58242338b..bec84b4ba7c 100644 --- a/src/plugin-sdk/group-access.ts +++ b/src/plugin-sdk/group-access.ts @@ -40,6 +40,7 @@ export type MatchedGroupAccessDecision = { reason: MatchedGroupAccessReason; }; +/** Downgrade sender-scoped group policy to open mode when no allowlist is configured. */ export function resolveSenderScopedGroupPolicy(params: { groupPolicy: GroupPolicy; groupAllowFrom: string[]; @@ -50,6 +51,7 @@ export function resolveSenderScopedGroupPolicy(params: { return params.groupAllowFrom.length > 0 ? "allowlist" : "open"; } +/** Evaluate route-level group access after policy, route match, and enablement checks. */ export function evaluateGroupRouteAccessForPolicy(params: { groupPolicy: GroupPolicy; routeAllowlistConfigured: boolean; @@ -96,6 +98,7 @@ export function evaluateGroupRouteAccessForPolicy(params: { }; } +/** Evaluate generic allowlist match state for channels that compare derived group identifiers. */ export function evaluateMatchedGroupAccessForPolicy(params: { groupPolicy: GroupPolicy; allowlistConfigured: boolean; @@ -142,6 +145,7 @@ export function evaluateMatchedGroupAccessForPolicy(params: { }; } +/** Evaluate sender access for an already-resolved group policy and allowlist. */ export function evaluateSenderGroupAccessForPolicy(params: { groupPolicy: GroupPolicy; providerMissingFallbackApplied?: boolean; @@ -184,6 +188,7 @@ export function evaluateSenderGroupAccessForPolicy(params: { }; } +/** Resolve provider fallback policy first, then evaluate sender access against that result. */ export function evaluateSenderGroupAccess(params: { providerConfigPresent: boolean; configuredGroupPolicy?: GroupPolicy; diff --git a/src/plugin-sdk/inbound-envelope.ts b/src/plugin-sdk/inbound-envelope.ts index 2a4ff0aaa06..f3662b725c3 100644 --- a/src/plugin-sdk/inbound-envelope.ts +++ b/src/plugin-sdk/inbound-envelope.ts @@ -24,6 +24,7 @@ type InboundRouteResolveParams = { peer: TPeer; }; +/** Create an envelope formatter bound to one resolved route and session store. */ export function createInboundEnvelopeBuilder(params: { cfg: TConfig; route: RouteLike; @@ -54,6 +55,7 @@ export function createInboundEnvelopeBuilder(params: { }; } +/** Resolve a route first, then return both the route and a formatter for future inbound messages. */ export function resolveInboundRouteEnvelopeBuilder< TConfig, TEnvelope, @@ -111,6 +113,7 @@ type InboundRouteEnvelopeRuntime< }; }; +/** Runtime-driven variant of inbound envelope resolution for plugins that already expose grouped helpers. */ export function resolveInboundRouteEnvelopeBuilderWithRuntime< TConfig, TEnvelope, diff --git a/src/plugin-sdk/inbound-reply-dispatch.ts b/src/plugin-sdk/inbound-reply-dispatch.ts index cf11b3ee451..b2ba466a21c 100644 --- a/src/plugin-sdk/inbound-reply-dispatch.ts +++ b/src/plugin-sdk/inbound-reply-dispatch.ts @@ -20,6 +20,7 @@ type DispatchReplyWithBufferedBlockDispatcherFn = type ReplyDispatchFromConfigOptions = Omit; +/** Run `dispatchReplyFromConfig` with a dispatcher that always gets its settled callback. */ export async function dispatchReplyFromConfigWithSettledDispatcher(params: { cfg: OpenClawConfig; ctxPayload: FinalizedMsgContext; @@ -40,6 +41,7 @@ export async function dispatchReplyFromConfigWithSettledDispatcher(params: { }); } +/** Assemble the common inbound reply dispatch dependencies for a resolved route. */ export function buildInboundReplyDispatchBase(params: { cfg: OpenClawConfig; channel: string; @@ -80,6 +82,7 @@ type RecordInboundSessionAndDispatchReplyParams = Parameters< typeof recordInboundSessionAndDispatchReply >[0]; +/** Resolve the shared dispatch base and immediately record + dispatch one inbound reply turn. */ export async function dispatchInboundReplyWithBase( params: BuildInboundReplyDispatchBaseParams & Pick< @@ -97,6 +100,7 @@ export async function dispatchInboundReplyWithBase( }); } +/** Record the inbound session first, then dispatch the reply using normalized outbound delivery. */ export async function recordInboundSessionAndDispatchReply(params: { cfg: OpenClawConfig; channel: string; diff --git a/src/plugin-sdk/json-store.ts b/src/plugin-sdk/json-store.ts index 5c08be6c561..faff8f64e59 100644 --- a/src/plugin-sdk/json-store.ts +++ b/src/plugin-sdk/json-store.ts @@ -2,6 +2,7 @@ import fs from "node:fs"; import { writeJsonAtomic } from "../infra/json-files.js"; import { safeParseJson } from "../utils.js"; +/** Read JSON from disk and fall back cleanly when the file is missing or invalid. */ export async function readJsonFileWithFallback( filePath: string, fallback: T, @@ -22,6 +23,7 @@ export async function readJsonFileWithFallback( } } +/** Write JSON with secure file permissions and atomic replacement semantics. */ export async function writeJsonFileAtomically(filePath: string, value: unknown): Promise { await writeJsonAtomic(filePath, value, { mode: 0o600, diff --git a/src/plugin-sdk/keyed-async-queue.ts b/src/plugin-sdk/keyed-async-queue.ts index 6e79cf35d59..0f07f3c8462 100644 --- a/src/plugin-sdk/keyed-async-queue.ts +++ b/src/plugin-sdk/keyed-async-queue.ts @@ -3,6 +3,7 @@ export type KeyedAsyncQueueHooks = { onSettle?: () => void; }; +/** Serialize async work per key while allowing unrelated keys to run concurrently. */ export function enqueueKeyedTask(params: { tails: Map>; key: string; diff --git a/src/plugin-sdk/oauth-utils.ts b/src/plugin-sdk/oauth-utils.ts index a6465d4d40e..e96a1856946 100644 --- a/src/plugin-sdk/oauth-utils.ts +++ b/src/plugin-sdk/oauth-utils.ts @@ -1,11 +1,13 @@ import { createHash, randomBytes } from "node:crypto"; +/** Encode a flat object as application/x-www-form-urlencoded form data. */ export function toFormUrlEncoded(data: Record): string { return Object.entries(data) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join("&"); } +/** Generate a PKCE verifier/challenge pair suitable for OAuth authorization flows. */ export function generatePkceVerifierChallenge(): { verifier: string; challenge: string } { const verifier = randomBytes(32).toString("base64url"); const challenge = createHash("sha256").update(verifier).digest("base64url"); diff --git a/src/plugin-sdk/outbound-media.ts b/src/plugin-sdk/outbound-media.ts index 731ca9d140e..979f8ac77a3 100644 --- a/src/plugin-sdk/outbound-media.ts +++ b/src/plugin-sdk/outbound-media.ts @@ -5,6 +5,7 @@ export type OutboundMediaLoadOptions = { mediaLocalRoots?: readonly string[]; }; +/** Load outbound media from a remote URL or approved local path using the shared web-media policy. */ export async function loadOutboundMediaFromUrl( mediaUrl: string, options: OutboundMediaLoadOptions = {}, diff --git a/src/plugin-sdk/pairing-access.ts b/src/plugin-sdk/pairing-access.ts index 31f0cd4d3a7..260f24c89d1 100644 --- a/src/plugin-sdk/pairing-access.ts +++ b/src/plugin-sdk/pairing-access.ts @@ -8,6 +8,7 @@ type ScopedUpsertInput = Omit< "channel" | "accountId" >; +/** Scope pairing store operations to one channel/account pair for plugin-facing helpers. */ export function createScopedPairingAccess(params: { core: PluginRuntime; channel: ChannelId; diff --git a/src/plugin-sdk/persistent-dedupe.ts b/src/plugin-sdk/persistent-dedupe.ts index 0b33824c795..7a6ea159841 100644 --- a/src/plugin-sdk/persistent-dedupe.ts +++ b/src/plugin-sdk/persistent-dedupe.ts @@ -91,6 +91,7 @@ function pruneData( }); } +/** Create a dedupe helper that combines in-memory fast checks with a lock-protected disk store. */ export function createPersistentDedupe(options: PersistentDedupeOptions): PersistentDedupe { const ttlMs = Math.max(0, Math.floor(options.ttlMs)); const memoryMaxSize = Math.max(0, Math.floor(options.memoryMaxSize)); diff --git a/src/plugin-sdk/provider-auth-result.ts b/src/plugin-sdk/provider-auth-result.ts index c16c23cc15e..ece6cebb0df 100644 --- a/src/plugin-sdk/provider-auth-result.ts +++ b/src/plugin-sdk/provider-auth-result.ts @@ -2,6 +2,7 @@ import type { AuthProfileCredential } from "../agents/auth-profiles/types.js"; import type { OpenClawConfig } from "../config/config.js"; import type { ProviderAuthResult } from "../plugins/types.js"; +/** Build the standard auth result payload for OAuth-style provider login flows. */ export function buildOauthProviderAuthResult(params: { providerId: string; defaultModel: string; diff --git a/src/plugin-sdk/reply-payload.ts b/src/plugin-sdk/reply-payload.ts index e141da2a940..a35380f5250 100644 --- a/src/plugin-sdk/reply-payload.ts +++ b/src/plugin-sdk/reply-payload.ts @@ -5,6 +5,7 @@ export type OutboundReplyPayload = { replyToId?: string; }; +/** Extract the supported outbound reply fields from loose tool or agent payload objects. */ export function normalizeOutboundReplyPayload( payload: Record, ): OutboundReplyPayload { @@ -24,6 +25,7 @@ export function normalizeOutboundReplyPayload( }; } +/** Wrap a deliverer so callers can hand it arbitrary payloads while channels receive normalized data. */ export function createNormalizedOutboundDeliverer( handler: (payload: OutboundReplyPayload) => Promise, ): (payload: unknown) => Promise { @@ -36,6 +38,7 @@ export function createNormalizedOutboundDeliverer( }; } +/** Prefer multi-attachment payloads, then fall back to the legacy single-media field. */ export function resolveOutboundMediaUrls(payload: { mediaUrls?: string[]; mediaUrl?: string; @@ -49,6 +52,7 @@ export function resolveOutboundMediaUrls(payload: { return []; } +/** Send media-first payloads intact, or chunk text-only payloads through the caller's transport hooks. */ export async function sendPayloadWithChunkedTextAndMedia< TContext extends { payload: object }, TResult, @@ -90,6 +94,7 @@ export async function sendPayloadWithChunkedTextAndMedia< return lastResult!; } +/** Detect numeric-looking target ids for channels that distinguish ids from handles. */ export function isNumericTargetId(raw: string): boolean { const trimmed = raw.trim(); if (!trimmed) { @@ -98,6 +103,7 @@ export function isNumericTargetId(raw: string): boolean { return /^\d{3,}$/.test(trimmed); } +/** Append attachment links to plain text when the channel cannot send media inline. */ export function formatTextWithAttachmentLinks( text: string | undefined, mediaUrls: string[], @@ -118,6 +124,7 @@ export function formatTextWithAttachmentLinks( return `${trimmedText}\n\n${mediaBlock}`; } +/** Send a caption with only the first media item, mirroring caption-limited channel transports. */ export async function sendMediaWithLeadingCaption(params: { mediaUrls: string[]; caption: string; diff --git a/src/plugin-sdk/request-url.ts b/src/plugin-sdk/request-url.ts index 2ba7354cc28..a1ce2216276 100644 --- a/src/plugin-sdk/request-url.ts +++ b/src/plugin-sdk/request-url.ts @@ -1,3 +1,4 @@ +/** Extract a string URL from the common request-like inputs accepted by fetch helpers. */ export function resolveRequestUrl(input: RequestInfo | URL): string { if (typeof input === "string") { return input; diff --git a/src/plugin-sdk/resolution-notes.ts b/src/plugin-sdk/resolution-notes.ts index 9baf64c21d4..49b2f9c67e9 100644 --- a/src/plugin-sdk/resolution-notes.ts +++ b/src/plugin-sdk/resolution-notes.ts @@ -1,3 +1,4 @@ +/** Format a short note that separates successfully resolved targets from unresolved passthrough values. */ export function formatResolvedUnresolvedNote(params: { resolved: string[]; unresolved: string[]; diff --git a/src/plugin-sdk/run-command.ts b/src/plugin-sdk/run-command.ts index 03f0846a57e..7cf626ce8ef 100644 --- a/src/plugin-sdk/run-command.ts +++ b/src/plugin-sdk/run-command.ts @@ -13,6 +13,7 @@ export type PluginCommandRunOptions = { env?: NodeJS.ProcessEnv; }; +/** Run a plugin-managed command with timeout handling and normalized stdout/stderr results. */ export async function runPluginCommandWithTimeout( options: PluginCommandRunOptions, ): Promise { diff --git a/src/plugin-sdk/runtime-store.ts b/src/plugin-sdk/runtime-store.ts index de0d84131e1..67e8bb3644c 100644 --- a/src/plugin-sdk/runtime-store.ts +++ b/src/plugin-sdk/runtime-store.ts @@ -1,3 +1,4 @@ +/** Create a tiny mutable runtime slot with strict access when the runtime has not been initialized. */ export function createPluginRuntimeStore(errorMessage: string): { setRuntime: (next: T) => void; clearRuntime: () => void; diff --git a/src/plugin-sdk/runtime.ts b/src/plugin-sdk/runtime.ts index c438a4e9788..75b6f955dc7 100644 --- a/src/plugin-sdk/runtime.ts +++ b/src/plugin-sdk/runtime.ts @@ -6,6 +6,7 @@ type LoggerLike = { error: (message: string) => void; }; +/** Adapt a simple logger into the RuntimeEnv contract used by shared plugin SDK helpers. */ export function createLoggerBackedRuntime(params: { logger: LoggerLike; exitError?: (code: number) => Error; @@ -23,6 +24,7 @@ export function createLoggerBackedRuntime(params: { }; } +/** Reuse an existing runtime when present, otherwise synthesize one from the provided logger. */ export function resolveRuntimeEnv(params: { runtime?: RuntimeEnv; logger: LoggerLike; @@ -31,6 +33,7 @@ export function resolveRuntimeEnv(params: { return params.runtime ?? createLoggerBackedRuntime(params); } +/** Resolve a runtime that treats exit requests as unsupported errors instead of process termination. */ export function resolveRuntimeEnvWithUnavailableExit(params: { runtime?: RuntimeEnv; logger: LoggerLike; diff --git a/src/plugin-sdk/secret-input-schema.ts b/src/plugin-sdk/secret-input-schema.ts index 579d80df441..96d5247c767 100644 --- a/src/plugin-sdk/secret-input-schema.ts +++ b/src/plugin-sdk/secret-input-schema.ts @@ -7,6 +7,7 @@ import { SECRET_PROVIDER_ALIAS_PATTERN, } from "../secrets/ref-contract.js"; +/** Build the shared zod schema for secret inputs accepted by plugin auth/config surfaces. */ export function buildSecretInputSchema() { const providerSchema = z .string() diff --git a/src/plugin-sdk/slack-message-actions.ts b/src/plugin-sdk/slack-message-actions.ts index 1dbf597d0bf..ef7a5f12876 100644 --- a/src/plugin-sdk/slack-message-actions.ts +++ b/src/plugin-sdk/slack-message-actions.ts @@ -15,6 +15,7 @@ function readSlackBlocksParam(actionParams: Record) { return parseSlackBlocksInput(actionParams.blocks) as Record[] | undefined; } +/** Translate generic channel action requests into Slack-specific tool invocations and payload shapes. */ export async function handleSlackMessageAction(params: { providerId: string; ctx: ChannelMessageActionContext; diff --git a/src/plugin-sdk/ssrf-policy.ts b/src/plugin-sdk/ssrf-policy.ts index 351938d0456..420f7dfc6b7 100644 --- a/src/plugin-sdk/ssrf-policy.ts +++ b/src/plugin-sdk/ssrf-policy.ts @@ -24,6 +24,7 @@ function isHostnameAllowedBySuffixAllowlist( return allowlist.some((entry) => normalized === entry || normalized.endsWith(`.${entry}`)); } +/** Normalize suffix-style host allowlists into lowercase canonical entries with wildcard collapse. */ export function normalizeHostnameSuffixAllowlist( input?: readonly string[], defaults?: readonly string[], @@ -39,6 +40,7 @@ export function normalizeHostnameSuffixAllowlist( return Array.from(new Set(normalized)); } +/** Check whether a URL is HTTPS and its hostname matches the normalized suffix allowlist. */ export function isHttpsUrlAllowedByHostnameSuffixAllowlist( url: string, allowlist: readonly string[], diff --git a/src/plugin-sdk/status-helpers.ts b/src/plugin-sdk/status-helpers.ts index 42aad35a702..231c438b8ef 100644 --- a/src/plugin-sdk/status-helpers.ts +++ b/src/plugin-sdk/status-helpers.ts @@ -9,6 +9,7 @@ type RuntimeLifecycleSnapshot = { lastOutboundAt?: number | null; }; +/** Create the baseline runtime snapshot shape used by channel/account status stores. */ export function createDefaultChannelRuntimeState>( accountId: string, extra?: T, @@ -29,6 +30,7 @@ export function createDefaultChannelRuntimeState>( snapshot: { configured?: boolean | null; @@ -65,6 +68,7 @@ export function buildProbeChannelStatusSummary, diff --git a/src/plugin-sdk/telegram.ts b/src/plugin-sdk/telegram.ts index e8e823a6628..2db97fb74b5 100644 --- a/src/plugin-sdk/telegram.ts +++ b/src/plugin-sdk/telegram.ts @@ -48,7 +48,7 @@ export { export { looksLikeTelegramTargetId, normalizeTelegramMessagingTarget, -} from "../channels/plugins/normalize/telegram.js"; +} from "../../extensions/telegram/src/normalize.js"; export { parseTelegramReplyToMessageId, parseTelegramThreadId, @@ -58,7 +58,7 @@ export { normalizeTelegramAllowFromEntry, } from "../../extensions/telegram/src/allow-from.js"; export { fetchTelegramChatId } from "../../extensions/telegram/src/api-fetch.js"; -export { collectTelegramStatusIssues } from "../channels/plugins/status-issues/telegram.js"; +export { collectTelegramStatusIssues } from "../../extensions/telegram/src/status-issues.js"; export { sendTelegramPayloadMessages } from "../../extensions/telegram/src/outbound-adapter.js"; export { buildBrowseProvidersButton, @@ -72,6 +72,7 @@ export { isTelegramExecApprovalApprover, isTelegramExecApprovalClientEnabled, } from "../../extensions/telegram/src/exec-approvals.js"; +export type { StickerMetadata } from "../../extensions/telegram/src/bot/types.js"; export { resolveAllowlistProviderRuntimeGroupPolicy, diff --git a/src/plugin-sdk/temp-path.ts b/src/plugin-sdk/temp-path.ts index c418fe9f664..436377fe5e1 100644 --- a/src/plugin-sdk/temp-path.ts +++ b/src/plugin-sdk/temp-path.ts @@ -40,6 +40,7 @@ function isNodeErrorWithCode(err: unknown, code: string): boolean { ); } +/** Build a unique temp file path with sanitized prefix/extension parts. */ export function buildRandomTempFilePath(params: { prefix: string; extension?: string; @@ -58,6 +59,7 @@ export function buildRandomTempFilePath(params: { return path.join(resolveTempRoot(params.tmpDir), `${prefix}-${now}-${uuid}${extension}`); } +/** Create a temporary download directory, run the callback, then clean it up best-effort. */ export async function withTempDownloadPath( params: { prefix: string; diff --git a/src/plugin-sdk/text-chunking.ts b/src/plugin-sdk/text-chunking.ts index 47c98c10851..724c651168a 100644 --- a/src/plugin-sdk/text-chunking.ts +++ b/src/plugin-sdk/text-chunking.ts @@ -1,5 +1,6 @@ import { chunkTextByBreakResolver } from "../shared/text-chunking.js"; +/** Chunk outbound text while preferring newline boundaries over spaces. */ export function chunkTextForOutbound(text: string, limit: number): string[] { return chunkTextByBreakResolver(text, limit, (window) => { const lastNewline = window.lastIndexOf("\n"); diff --git a/src/plugin-sdk/tool-send.ts b/src/plugin-sdk/tool-send.ts index 835cd688d8a..61ee56fa9ac 100644 --- a/src/plugin-sdk/tool-send.ts +++ b/src/plugin-sdk/tool-send.ts @@ -1,3 +1,4 @@ +/** Extract the canonical send target fields from tool arguments when the action matches. */ export function extractToolSend( args: Record, expectedAction = "sendMessage", diff --git a/src/plugin-sdk/webhook-memory-guards.ts b/src/plugin-sdk/webhook-memory-guards.ts index 50a43c0b3ab..6b4061cac3b 100644 --- a/src/plugin-sdk/webhook-memory-guards.ts +++ b/src/plugin-sdk/webhook-memory-guards.ts @@ -48,6 +48,7 @@ export type WebhookAnomalyTracker = { clear: () => void; }; +/** Create a simple fixed-window rate limiter for in-memory webhook protection. */ export function createFixedWindowRateLimiter(options: { windowMs: number; maxRequests: number; @@ -104,6 +105,7 @@ export function createFixedWindowRateLimiter(options: { }; } +/** Count keyed events in memory with optional TTL pruning and bounded cardinality. */ export function createBoundedCounter(options: { maxTrackedKeys: number; ttlMs?: number; @@ -161,6 +163,7 @@ export function createBoundedCounter(options: { }; } +/** Track repeated webhook failures and emit sampled logs for suspicious request patterns. */ export function createWebhookAnomalyTracker(options?: { maxTrackedKeys?: number; ttlMs?: number; diff --git a/src/plugin-sdk/webhook-path.ts b/src/plugin-sdk/webhook-path.ts index 41e4bd0ba98..fa68c9e20ee 100644 --- a/src/plugin-sdk/webhook-path.ts +++ b/src/plugin-sdk/webhook-path.ts @@ -1,3 +1,4 @@ +/** Normalize webhook paths into the canonical registry form used by route lookup. */ export function normalizeWebhookPath(raw: string): string { const trimmed = raw.trim(); if (!trimmed) { @@ -10,6 +11,7 @@ export function normalizeWebhookPath(raw: string): string { return withSlash; } +/** Resolve the effective webhook path from explicit path, URL, or default fallback. */ export function resolveWebhookPath(params: { webhookPath?: string; webhookUrl?: string; diff --git a/src/plugin-sdk/webhook-request-guards.ts b/src/plugin-sdk/webhook-request-guards.ts index a45df7c06dd..f181859bc84 100644 --- a/src/plugin-sdk/webhook-request-guards.ts +++ b/src/plugin-sdk/webhook-request-guards.ts @@ -81,6 +81,7 @@ function respondWebhookBodyReadError(params: { return { ok: false }; } +/** Create an in-memory limiter that caps concurrent webhook handlers per key. */ export function createWebhookInFlightLimiter(options?: { maxInFlightPerKey?: number; maxTrackedKeys?: number; @@ -127,6 +128,7 @@ export function createWebhookInFlightLimiter(options?: { }; } +/** Detect JSON content types, including structured syntax suffixes like `application/ld+json`. */ export function isJsonContentType(value: string | string[] | undefined): boolean { const first = Array.isArray(value) ? value[0] : value; if (!first) { @@ -136,6 +138,7 @@ export function isJsonContentType(value: string | string[] | undefined): boolean return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json")); } +/** Apply method, rate-limit, and content-type guards before a webhook handler reads the body. */ export function applyBasicWebhookRequestGuards(params: { req: IncomingMessage; res: ServerResponse; @@ -176,6 +179,7 @@ export function applyBasicWebhookRequestGuards(params: { return true; } +/** Start the shared webhook request lifecycle and return a release hook for in-flight tracking. */ export function beginWebhookRequestPipelineOrReject(params: { req: IncomingMessage; res: ServerResponse; @@ -226,6 +230,7 @@ export function beginWebhookRequestPipelineOrReject(params: { }; } +/** Read a webhook request body with bounded size/time limits and translate failures into responses. */ export async function readWebhookBodyOrReject(params: { req: IncomingMessage; res: ServerResponse; @@ -260,6 +265,7 @@ export async function readWebhookBodyOrReject(params: { } } +/** Read and parse a JSON webhook body, rejecting malformed or oversized payloads consistently. */ export async function readJsonWebhookBodyOrReject(params: { req: IncomingMessage; res: ServerResponse; diff --git a/src/plugin-sdk/webhook-targets.ts b/src/plugin-sdk/webhook-targets.ts index 791f4591101..e3dd9eda01d 100644 --- a/src/plugin-sdk/webhook-targets.ts +++ b/src/plugin-sdk/webhook-targets.ts @@ -24,6 +24,7 @@ export type RegisterWebhookPluginRouteOptions = Omit< "path" | "fallbackPath" >; +/** Register a webhook target and lazily install the matching plugin HTTP route on first use. */ export function registerWebhookTargetWithPluginRoute(params: { targetsByPath: Map; target: T; @@ -54,6 +55,7 @@ function getPathTeardownMap(targetsByPath: Map): Map( targetsByPath: Map, target: T, @@ -99,6 +101,7 @@ export function registerWebhookTarget( return { target: normalizedTarget, unregister }; } +/** Resolve all registered webhook targets for the incoming request path. */ export function resolveWebhookTargets( req: IncomingMessage, targetsByPath: Map, @@ -112,6 +115,7 @@ export function resolveWebhookTargets( return { path, targets }; } +/** Run common webhook guards, then dispatch only when the request path resolves to live targets. */ export async function withResolvedWebhookRequestPipeline(params: { req: IncomingMessage; res: ServerResponse; @@ -183,6 +187,7 @@ function finalizeMatchedWebhookTarget(matched: T | undefined): WebhookTargetM return { kind: "single", target: matched }; } +/** Match exactly one synchronous target or report whether resolution was empty or ambiguous. */ export function resolveSingleWebhookTarget( targets: readonly T[], isMatch: (target: T) => boolean, @@ -201,6 +206,7 @@ export function resolveSingleWebhookTarget( return finalizeMatchedWebhookTarget(matched); } +/** Async variant of single-target resolution for auth checks that need I/O. */ export async function resolveSingleWebhookTargetAsync( targets: readonly T[], isMatch: (target: T) => Promise, @@ -219,6 +225,7 @@ export async function resolveSingleWebhookTargetAsync( return finalizeMatchedWebhookTarget(matched); } +/** Resolve an authorized target and send the standard unauthorized or ambiguous response on failure. */ export async function resolveWebhookTargetWithAuthOrReject(params: { targets: readonly T[]; res: ServerResponse; @@ -234,6 +241,7 @@ export async function resolveWebhookTargetWithAuthOrReject(params: { return resolveWebhookTargetMatchOrReject(params, match); } +/** Synchronous variant of webhook auth resolution for cheap in-memory match checks. */ export function resolveWebhookTargetWithAuthOrRejectSync(params: { targets: readonly T[]; res: ServerResponse; @@ -270,6 +278,7 @@ function resolveWebhookTargetMatchOrReject( return null; } +/** Reject non-POST webhook requests with the conventional Allow header. */ export function rejectNonPostWebhookRequest(req: IncomingMessage, res: ServerResponse): boolean { if (req.method === "POST") { return false; diff --git a/src/plugin-sdk/whatsapp.ts b/src/plugin-sdk/whatsapp.ts index e84a60e785c..759a56b080e 100644 --- a/src/plugin-sdk/whatsapp.ts +++ b/src/plugin-sdk/whatsapp.ts @@ -26,6 +26,12 @@ export { listWhatsAppDirectoryGroupsFromConfig, listWhatsAppDirectoryPeersFromConfig, } from "../channels/plugins/directory-config.js"; +export { + hasAnyWhatsAppAuth, + listEnabledWhatsAppAccounts, + resolveWhatsAppAccount, +} from "../../extensions/whatsapp/src/accounts.js"; +export { normalizeWhatsAppAllowFromEntries } from "../channels/plugins/normalize/whatsapp.js"; export { collectAllowlistProviderGroupPolicyWarnings, collectOpenGroupPolicyRouteAllowlistWarnings, diff --git a/src/plugin-sdk/windows-spawn.ts b/src/plugin-sdk/windows-spawn.ts index 16d24de629a..9a296508165 100644 --- a/src/plugin-sdk/windows-spawn.ts +++ b/src/plugin-sdk/windows-spawn.ts @@ -52,6 +52,7 @@ function isFilePath(candidate: string): boolean { } } +/** Resolve a Windows command name through PATH and PATHEXT so wrapper inspection sees the real file. */ export function resolveWindowsExecutablePath(command: string, env: NodeJS.ProcessEnv): string { if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) { return command; @@ -188,6 +189,7 @@ function resolveEntrypointFromPackageJson( return null; } +/** Resolve the safest direct spawn candidate for Windows wrappers, scripts, and binaries. */ export function resolveWindowsSpawnProgramCandidate( params: ResolveWindowsSpawnProgramCandidateParams, ): WindowsSpawnProgramCandidate { @@ -250,6 +252,7 @@ export function resolveWindowsSpawnProgramCandidate( }; } +/** Apply shell-fallback policy when Windows wrapper resolution could not find a direct entrypoint. */ export function applyWindowsSpawnProgramPolicy(params: { candidate: WindowsSpawnProgramCandidate; allowShellFallback?: boolean; @@ -275,6 +278,7 @@ export function applyWindowsSpawnProgramPolicy(params: { ); } +/** Resolve the final Windows spawn program after candidate discovery and fallback policy. */ export function resolveWindowsSpawnProgram( params: ResolveWindowsSpawnProgramParams, ): WindowsSpawnProgram { @@ -285,6 +289,7 @@ export function resolveWindowsSpawnProgram( }); } +/** Combine a resolved Windows spawn program with call-site argv for actual process launch. */ export function materializeWindowsSpawnProgram( program: WindowsSpawnProgram, argv: string[], diff --git a/src/plugins/contracts/auth.contract.test.ts b/src/plugins/contracts/auth.contract.test.ts index 386c7acb6e7..cca85917c59 100644 --- a/src/plugins/contracts/auth.contract.test.ts +++ b/src/plugins/contracts/auth.contract.test.ts @@ -51,7 +51,13 @@ function buildPrompter(): WizardPrompter { intro: async () => {}, outro: async () => {}, note: async () => {}, - select: async (params: WizardSelectParams) => params.options[0].value, + select: async (params: WizardSelectParams) => { + const option = params.options[0]; + if (!option) { + throw new Error("missing select option"); + } + return option.value; + }, multiselect: async (params: WizardMultiSelectParams) => params.initialValues ?? [], text: async () => "", confirm: async () => false, diff --git a/src/plugins/contracts/discovery.contract.test.ts b/src/plugins/contracts/discovery.contract.test.ts index 3fff691b6af..9ca5f1184e6 100644 --- a/src/plugins/contracts/discovery.contract.test.ts +++ b/src/plugins/contracts/discovery.contract.test.ts @@ -59,10 +59,10 @@ function requireProvider(providers: ProviderPlugin[], providerId: string) { return provider; } -function buildTestModel(params: { id: string; name: string }): ModelDefinitionConfig { +function createModelConfig(id: string, name = id): ModelDefinitionConfig { return { - id: params.id, - name: params.name, + id, + name, reasoning: false, input: ["text"], cost: { @@ -75,7 +75,6 @@ function buildTestModel(params: { id: string; name: string }): ModelDefinitionCo maxTokens: 8_192, }; } - describe("provider discovery contract", () => { afterEach(() => { resolveCopilotApiTokenMock.mockReset(); @@ -244,7 +243,7 @@ describe("provider discovery contract", () => { providers: { ollama: { baseUrl: "http://ollama-host:11434/v1/", - models: [buildTestModel({ id: "llama3.2", name: "llama3.2" })], + models: [createModelConfig("llama3.2")], }, }, }, @@ -257,7 +256,7 @@ describe("provider discovery contract", () => { baseUrl: "http://ollama-host:11434", api: "ollama", apiKey: "ollama-local", - models: [expect.objectContaining({ id: "llama3.2", name: "llama3.2" })], + models: [createModelConfig("llama3.2")], }, }); expect(buildOllamaProviderMock).not.toHaveBeenCalled(); diff --git a/src/plugins/runtime/runtime-discord.ts b/src/plugins/runtime/runtime-discord.ts index 1ca7b32bf9b..6aadba32a9a 100644 --- a/src/plugins/runtime/runtime-discord.ts +++ b/src/plugins/runtime/runtime-discord.ts @@ -1,4 +1,5 @@ import { auditDiscordChannelPermissions } from "../../../extensions/discord/src/audit.js"; +import { discordMessageActions } from "../../../extensions/discord/src/channel-actions.js"; import { listDiscordDirectoryGroupsLive, listDiscordDirectoryPeersLive, @@ -29,7 +30,6 @@ import { sendTypingDiscord, unpinMessageDiscord, } from "../../../extensions/discord/src/send.js"; -import { discordMessageActions } from "../../channels/plugins/actions/discord.js"; import { createDiscordTypingLease } from "./runtime-discord-typing.js"; import type { PluginRuntimeChannel } from "./types-channel.js"; diff --git a/src/plugins/runtime/runtime-telegram.ts b/src/plugins/runtime/runtime-telegram.ts index 1e6993ef489..9481c718565 100644 --- a/src/plugins/runtime/runtime-telegram.ts +++ b/src/plugins/runtime/runtime-telegram.ts @@ -2,6 +2,7 @@ import { auditTelegramGroupMembership, collectTelegramUnmentionedGroupIds, } from "../../../extensions/telegram/src/audit.js"; +import { telegramMessageActions } from "../../../extensions/telegram/src/channel-actions.js"; import { monitorTelegramProvider } from "../../../extensions/telegram/src/monitor.js"; import { probeTelegram } from "../../../extensions/telegram/src/probe.js"; import { @@ -20,7 +21,6 @@ import { setTelegramThreadBindingMaxAgeBySessionKey, } from "../../../extensions/telegram/src/thread-bindings.js"; import { resolveTelegramToken } from "../../../extensions/telegram/src/token.js"; -import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js"; import { createTelegramTypingLease } from "./runtime-telegram-typing.js"; import type { PluginRuntimeChannel } from "./types-channel.js"; diff --git a/src/plugins/runtime/types-channel.ts b/src/plugins/runtime/types-channel.ts index b0b5941f24d..f8e6e095ef5 100644 --- a/src/plugins/runtime/types-channel.ts +++ b/src/plugins/runtime/types-channel.ts @@ -87,7 +87,7 @@ export type PluginRuntimeChannel = { shouldHandleTextCommands: typeof import("../../auto-reply/commands-registry.js").shouldHandleTextCommands; }; discord: { - messageActions: typeof import("../../channels/plugins/actions/discord.js").discordMessageActions; + messageActions: typeof import("../../../extensions/discord/src/channel-actions.js").discordMessageActions; auditChannelPermissions: typeof import("../../../extensions/discord/src/audit.js").auditDiscordChannelPermissions; listDirectoryGroupsLive: typeof import("../../../extensions/discord/src/directory-live.js").listDiscordDirectoryGroupsLive; listDirectoryPeersLive: typeof import("../../../extensions/discord/src/directory-live.js").listDiscordDirectoryPeersLive; @@ -147,7 +147,7 @@ export type PluginRuntimeChannel = { sendMessageTelegram: typeof import("../../../extensions/telegram/src/send.js").sendMessageTelegram; sendPollTelegram: typeof import("../../../extensions/telegram/src/send.js").sendPollTelegram; monitorTelegramProvider: typeof import("../../../extensions/telegram/src/monitor.js").monitorTelegramProvider; - messageActions: typeof import("../../channels/plugins/actions/telegram.js").telegramMessageActions; + messageActions: typeof import("../../../extensions/telegram/src/channel-actions.js").telegramMessageActions; threadBindings: { setIdleTimeoutBySessionKey: typeof import("../../../extensions/telegram/src/thread-bindings.js").setTelegramThreadBindingIdleTimeoutBySessionKey; setMaxAgeBySessionKey: typeof import("../../../extensions/telegram/src/thread-bindings.js").setTelegramThreadBindingMaxAgeBySessionKey;