refactor: route channel runtime via plugin api

This commit is contained in:
Peter Steinberger
2026-01-18 11:00:19 +00:00
parent 676d41d415
commit ee6e534ccb
82 changed files with 1253 additions and 3167 deletions

View File

@@ -1,17 +1,55 @@
import type { PluginRegistry } from "./registry.js";
let activeRegistry: PluginRegistry | null = null;
let activeRegistryKey: string | null = null;
const createEmptyRegistry = (): PluginRegistry => ({
plugins: [],
tools: [],
hooks: [],
typedHooks: [],
channels: [],
providers: [],
gatewayHandlers: {},
httpHandlers: [],
cliRegistrars: [],
services: [],
diagnostics: [],
});
const REGISTRY_STATE = Symbol.for("clawdbot.pluginRegistryState");
type RegistryState = {
registry: PluginRegistry | null;
key: string | null;
};
const state: RegistryState = (() => {
const globalState = globalThis as typeof globalThis & {
[REGISTRY_STATE]?: RegistryState;
};
if (!globalState[REGISTRY_STATE]) {
globalState[REGISTRY_STATE] = {
registry: createEmptyRegistry(),
key: null,
};
}
return globalState[REGISTRY_STATE] as RegistryState;
})();
export function setActivePluginRegistry(registry: PluginRegistry, cacheKey?: string) {
activeRegistry = registry;
activeRegistryKey = cacheKey ?? null;
state.registry = registry;
state.key = cacheKey ?? null;
}
export function getActivePluginRegistry(): PluginRegistry | null {
return activeRegistry;
return state.registry;
}
export function requireActivePluginRegistry(): PluginRegistry {
if (!state.registry) {
state.registry = createEmptyRegistry();
}
return state.registry;
}
export function getActivePluginRegistryKey(): string | null {
return activeRegistryKey;
return state.key;
}

View File

@@ -1,32 +1,99 @@
import { createRequire } from "node:module";
import { chunkMarkdownText, resolveTextChunkLimit } from "../../auto-reply/chunk.js";
import { hasControlCommand } from "../../auto-reply/command-detection.js";
import { chunkMarkdownText, chunkText, resolveTextChunkLimit } from "../../auto-reply/chunk.js";
import {
hasControlCommand,
isControlCommandMessage,
shouldComputeCommandAuthorized,
} from "../../auto-reply/command-detection.js";
import { shouldHandleTextCommands } from "../../auto-reply/commands-registry.js";
import {
createInboundDebouncer,
resolveInboundDebounceMs,
} from "../../auto-reply/inbound-debounce.js";
import { formatAgentEnvelope } from "../../auto-reply/envelope.js";
import { dispatchReplyFromConfig } from "../../auto-reply/reply/dispatch-from-config.js";
import { buildMentionRegexes, matchesMentionPatterns } from "../../auto-reply/reply/mentions.js";
import { dispatchReplyWithBufferedBlockDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
import { createReplyDispatcherWithTyping } from "../../auto-reply/reply/reply-dispatcher.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { resolveEffectiveMessagesConfig, resolveHumanDelayConfig } from "../../agents/identity.js";
import { createMemoryGetTool, createMemorySearchTool } from "../../agents/tools/memory-tool.js";
import { handleSlackAction } from "../../agents/tools/slack-actions.js";
import { handleWhatsAppAction } from "../../agents/tools/whatsapp-actions.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { discordMessageActions } from "../../channels/plugins/actions/discord.js";
import { telegramMessageActions } from "../../channels/plugins/actions/telegram.js";
import { createWhatsAppLoginTool } from "../../channels/plugins/agent-tools/whatsapp-login.js";
import { monitorWebChannel } from "../../channels/web/index.js";
import {
resolveChannelGroupPolicy,
resolveChannelGroupRequireMention,
} from "../../config/group-policy.js";
import { resolveStateDir } from "../../config/paths.js";
import { loadConfig, writeConfigFile } from "../../config/config.js";
import {
recordSessionMetaFromInbound,
resolveStorePath,
updateLastRoute,
} from "../../config/sessions.js";
import { auditDiscordChannelPermissions } from "../../discord/audit.js";
import { listDiscordDirectoryGroupsLive, listDiscordDirectoryPeersLive } from "../../discord/directory-live.js";
import { monitorDiscordProvider } from "../../discord/monitor.js";
import { probeDiscord } from "../../discord/probe.js";
import { resolveDiscordChannelAllowlist } from "../../discord/resolve-channels.js";
import { resolveDiscordUserAllowlist } from "../../discord/resolve-users.js";
import { sendMessageDiscord, sendPollDiscord } from "../../discord/send.js";
import { enqueueSystemEvent } from "../../infra/system-events.js";
import { monitorIMessageProvider } from "../../imessage/monitor.js";
import { probeIMessage } from "../../imessage/probe.js";
import { sendMessageIMessage } from "../../imessage/send.js";
import { shouldLogVerbose } from "../../globals.js";
import { getChildLogger } from "../../logging.js";
import { normalizeLogLevel } from "../../logging/levels.js";
import { isVoiceCompatibleAudio } from "../../media/audio.js";
import { mediaKindFromMime } from "../../media/constants.js";
import { fetchRemoteMedia } from "../../media/fetch.js";
import { getImageMetadata, resizeToJpeg } from "../../media/image-ops.js";
import { detectMime } from "../../media/mime.js";
import { saveMediaBuffer } from "../../media/store.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js";
import {
readChannelAllowFromStore,
upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import { runCommandWithTimeout } from "../../process/exec.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { monitorSignalProvider } from "../../signal/index.js";
import { probeSignal } from "../../signal/probe.js";
import { sendMessageSignal } from "../../signal/send.js";
import { monitorSlackProvider } from "../../slack/index.js";
import { listSlackDirectoryGroupsLive, listSlackDirectoryPeersLive } from "../../slack/directory-live.js";
import { probeSlack } from "../../slack/probe.js";
import { resolveSlackChannelAllowlist } from "../../slack/resolve-channels.js";
import { resolveSlackUserAllowlist } from "../../slack/resolve-users.js";
import { sendMessageSlack } from "../../slack/send.js";
import {
auditTelegramGroupMembership,
collectTelegramUnmentionedGroupIds,
} from "../../telegram/audit.js";
import { monitorTelegramProvider } from "../../telegram/monitor.js";
import { probeTelegram } from "../../telegram/probe.js";
import { sendMessageTelegram } from "../../telegram/send.js";
import { resolveTelegramToken } from "../../telegram/token.js";
import { loadWebMedia } from "../../web/media.js";
import { getActiveWebListener } from "../../web/active-listener.js";
import {
getWebAuthAgeMs,
logoutWeb,
logWebSelfId,
readWebSelfId,
webAuthExists,
} from "../../web/auth-store.js";
import { loginWeb } from "../../web/login.js";
import { startWebLoginWithQr, waitForWebLogin } from "../../web/login-qr.js";
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";
import { registerMemoryCli } from "../../cli/memory-cli.js";
import type { PluginRuntime } from "./types.js";
@@ -48,17 +115,42 @@ function resolveVersion(): string {
export function createPluginRuntime(): PluginRuntime {
return {
version: resolveVersion(),
config: {
loadConfig,
writeConfigFile,
},
system: {
enqueueSystemEvent,
runCommandWithTimeout,
},
media: {
loadWebMedia,
detectMime,
mediaKindFromMime,
isVoiceCompatibleAudio,
getImageMetadata,
resizeToJpeg,
},
tools: {
createMemoryGetTool,
createMemorySearchTool,
registerMemoryCli,
},
channel: {
text: {
chunkMarkdownText,
resolveTextChunkLimit,
hasControlCommand,
},
text: {
chunkMarkdownText,
chunkText,
resolveTextChunkLimit,
hasControlCommand,
},
reply: {
dispatchReplyWithBufferedBlockDispatcher,
createReplyDispatcherWithTyping,
resolveEffectiveMessagesConfig,
resolveHumanDelayConfig,
dispatchReplyFromConfig,
finalizeInboundContext,
formatAgentEnvelope,
},
routing: {
resolveAgentRoute,
@@ -72,6 +164,11 @@ export function createPluginRuntime(): PluginRuntime {
fetchRemoteMedia,
saveMediaBuffer,
},
session: {
resolveStorePath,
recordSessionMetaFromInbound,
updateLastRoute,
},
mentions: {
buildMentionRegexes,
matchesMentionPatterns,
@@ -84,8 +181,68 @@ export function createPluginRuntime(): PluginRuntime {
createInboundDebouncer,
resolveInboundDebounceMs,
},
commands: {
resolveCommandAuthorizedFromAuthorizers,
commands: {
resolveCommandAuthorizedFromAuthorizers,
isControlCommandMessage,
shouldComputeCommandAuthorized,
shouldHandleTextCommands,
},
discord: {
messageActions: discordMessageActions,
auditChannelPermissions: auditDiscordChannelPermissions,
listDirectoryGroupsLive: listDiscordDirectoryGroupsLive,
listDirectoryPeersLive: listDiscordDirectoryPeersLive,
probeDiscord,
resolveChannelAllowlist: resolveDiscordChannelAllowlist,
resolveUserAllowlist: resolveDiscordUserAllowlist,
sendMessageDiscord,
sendPollDiscord,
monitorDiscordProvider,
},
slack: {
listDirectoryGroupsLive: listSlackDirectoryGroupsLive,
listDirectoryPeersLive: listSlackDirectoryPeersLive,
probeSlack,
resolveChannelAllowlist: resolveSlackChannelAllowlist,
resolveUserAllowlist: resolveSlackUserAllowlist,
sendMessageSlack,
monitorSlackProvider,
handleSlackAction,
},
telegram: {
auditGroupMembership: auditTelegramGroupMembership,
collectUnmentionedGroupIds: collectTelegramUnmentionedGroupIds,
probeTelegram,
resolveTelegramToken,
sendMessageTelegram,
monitorTelegramProvider,
messageActions: telegramMessageActions,
},
signal: {
probeSignal,
sendMessageSignal,
monitorSignalProvider,
},
imessage: {
monitorIMessageProvider,
probeIMessage,
sendMessageIMessage,
},
whatsapp: {
getActiveWebListener,
getWebAuthAgeMs,
logoutWeb,
logWebSelfId,
readWebSelfId,
webAuthExists,
sendMessageWhatsApp,
sendPollWhatsApp,
loginWeb,
startWebLoginWithQr,
waitForWebLogin,
monitorWebChannel,
handleWhatsAppAction,
createLoginTool: createWhatsAppLoginTool,
},
},
logging: {

View File

@@ -31,8 +31,99 @@ type ResolveCommandAuthorizedFromAuthorizers =
typeof import("../../channels/command-gating.js").resolveCommandAuthorizedFromAuthorizers;
type ResolveTextChunkLimit = typeof import("../../auto-reply/chunk.js").resolveTextChunkLimit;
type ChunkMarkdownText = typeof import("../../auto-reply/chunk.js").chunkMarkdownText;
type ChunkText = typeof import("../../auto-reply/chunk.js").chunkText;
type HasControlCommand = typeof import("../../auto-reply/command-detection.js").hasControlCommand;
type IsControlCommandMessage =
typeof import("../../auto-reply/command-detection.js").isControlCommandMessage;
type ShouldComputeCommandAuthorized =
typeof import("../../auto-reply/command-detection.js").shouldComputeCommandAuthorized;
type ShouldHandleTextCommands =
typeof import("../../auto-reply/commands-registry.js").shouldHandleTextCommands;
type DispatchReplyFromConfig =
typeof import("../../auto-reply/reply/dispatch-from-config.js").dispatchReplyFromConfig;
type FinalizeInboundContext =
typeof import("../../auto-reply/reply/inbound-context.js").finalizeInboundContext;
type FormatAgentEnvelope = typeof import("../../auto-reply/envelope.js").formatAgentEnvelope;
type ResolveStateDir = typeof import("../../config/paths.js").resolveStateDir;
type RecordSessionMetaFromInbound =
typeof import("../../config/sessions.js").recordSessionMetaFromInbound;
type ResolveStorePath = typeof import("../../config/sessions.js").resolveStorePath;
type UpdateLastRoute = typeof import("../../config/sessions.js").updateLastRoute;
type LoadConfig = typeof import("../../config/config.js").loadConfig;
type WriteConfigFile = typeof import("../../config/config.js").writeConfigFile;
type EnqueueSystemEvent = typeof import("../../infra/system-events.js").enqueueSystemEvent;
type RunCommandWithTimeout = typeof import("../../process/exec.js").runCommandWithTimeout;
type LoadWebMedia = typeof import("../../web/media.js").loadWebMedia;
type DetectMime = typeof import("../../media/mime.js").detectMime;
type MediaKindFromMime = typeof import("../../media/constants.js").mediaKindFromMime;
type IsVoiceCompatibleAudio = typeof import("../../media/audio.js").isVoiceCompatibleAudio;
type GetImageMetadata = typeof import("../../media/image-ops.js").getImageMetadata;
type ResizeToJpeg = typeof import("../../media/image-ops.js").resizeToJpeg;
type CreateMemoryGetTool =
typeof import("../../agents/tools/memory-tool.js").createMemoryGetTool;
type CreateMemorySearchTool =
typeof import("../../agents/tools/memory-tool.js").createMemorySearchTool;
type RegisterMemoryCli = typeof import("../../cli/memory-cli.js").registerMemoryCli;
type DiscordMessageActions =
typeof import("../../channels/plugins/actions/discord.js").discordMessageActions;
type AuditDiscordChannelPermissions =
typeof import("../../discord/audit.js").auditDiscordChannelPermissions;
type ListDiscordDirectoryGroupsLive =
typeof import("../../discord/directory-live.js").listDiscordDirectoryGroupsLive;
type ListDiscordDirectoryPeersLive =
typeof import("../../discord/directory-live.js").listDiscordDirectoryPeersLive;
type ProbeDiscord = typeof import("../../discord/probe.js").probeDiscord;
type ResolveDiscordChannelAllowlist =
typeof import("../../discord/resolve-channels.js").resolveDiscordChannelAllowlist;
type ResolveDiscordUserAllowlist =
typeof import("../../discord/resolve-users.js").resolveDiscordUserAllowlist;
type SendMessageDiscord = typeof import("../../discord/send.js").sendMessageDiscord;
type SendPollDiscord = typeof import("../../discord/send.js").sendPollDiscord;
type MonitorDiscordProvider = typeof import("../../discord/monitor.js").monitorDiscordProvider;
type ListSlackDirectoryGroupsLive =
typeof import("../../slack/directory-live.js").listSlackDirectoryGroupsLive;
type ListSlackDirectoryPeersLive =
typeof import("../../slack/directory-live.js").listSlackDirectoryPeersLive;
type ProbeSlack = typeof import("../../slack/probe.js").probeSlack;
type ResolveSlackChannelAllowlist =
typeof import("../../slack/resolve-channels.js").resolveSlackChannelAllowlist;
type ResolveSlackUserAllowlist =
typeof import("../../slack/resolve-users.js").resolveSlackUserAllowlist;
type SendMessageSlack = typeof import("../../slack/send.js").sendMessageSlack;
type MonitorSlackProvider = typeof import("../../slack/index.js").monitorSlackProvider;
type HandleSlackAction = typeof import("../../agents/tools/slack-actions.js").handleSlackAction;
type AuditTelegramGroupMembership =
typeof import("../../telegram/audit.js").auditTelegramGroupMembership;
type CollectTelegramUnmentionedGroupIds =
typeof import("../../telegram/audit.js").collectTelegramUnmentionedGroupIds;
type ProbeTelegram = typeof import("../../telegram/probe.js").probeTelegram;
type ResolveTelegramToken = typeof import("../../telegram/token.js").resolveTelegramToken;
type SendMessageTelegram = typeof import("../../telegram/send.js").sendMessageTelegram;
type MonitorTelegramProvider = typeof import("../../telegram/monitor.js").monitorTelegramProvider;
type TelegramMessageActions =
typeof import("../../channels/plugins/actions/telegram.js").telegramMessageActions;
type ProbeSignal = typeof import("../../signal/probe.js").probeSignal;
type SendMessageSignal = typeof import("../../signal/send.js").sendMessageSignal;
type MonitorSignalProvider = typeof import("../../signal/index.js").monitorSignalProvider;
type MonitorIMessageProvider = typeof import("../../imessage/monitor.js").monitorIMessageProvider;
type ProbeIMessage = typeof import("../../imessage/probe.js").probeIMessage;
type SendMessageIMessage = typeof import("../../imessage/send.js").sendMessageIMessage;
type GetActiveWebListener = typeof import("../../web/active-listener.js").getActiveWebListener;
type GetWebAuthAgeMs = typeof import("../../web/auth-store.js").getWebAuthAgeMs;
type LogoutWeb = typeof import("../../web/auth-store.js").logoutWeb;
type LogWebSelfId = typeof import("../../web/auth-store.js").logWebSelfId;
type ReadWebSelfId = typeof import("../../web/auth-store.js").readWebSelfId;
type WebAuthExists = typeof import("../../web/auth-store.js").webAuthExists;
type SendMessageWhatsApp = typeof import("../../web/outbound.js").sendMessageWhatsApp;
type SendPollWhatsApp = typeof import("../../web/outbound.js").sendPollWhatsApp;
type LoginWeb = typeof import("../../web/login.js").loginWeb;
type StartWebLoginWithQr = typeof import("../../web/login-qr.js").startWebLoginWithQr;
type WaitForWebLogin = typeof import("../../web/login-qr.js").waitForWebLogin;
type MonitorWebChannel = typeof import("../../channels/web/index.js").monitorWebChannel;
type HandleWhatsAppAction =
typeof import("../../agents/tools/whatsapp-actions.js").handleWhatsAppAction;
type CreateWhatsAppLoginTool =
typeof import("../../channels/plugins/agent-tools/whatsapp-login.js").createWhatsAppLoginTool;
export type RuntimeLogger = {
debug?: (message: string) => void;
@@ -43,9 +134,31 @@ export type RuntimeLogger = {
export type PluginRuntime = {
version: string;
config: {
loadConfig: LoadConfig;
writeConfigFile: WriteConfigFile;
};
system: {
enqueueSystemEvent: EnqueueSystemEvent;
runCommandWithTimeout: RunCommandWithTimeout;
};
media: {
loadWebMedia: LoadWebMedia;
detectMime: DetectMime;
mediaKindFromMime: MediaKindFromMime;
isVoiceCompatibleAudio: IsVoiceCompatibleAudio;
getImageMetadata: GetImageMetadata;
resizeToJpeg: ResizeToJpeg;
};
tools: {
createMemoryGetTool: CreateMemoryGetTool;
createMemorySearchTool: CreateMemorySearchTool;
registerMemoryCli: RegisterMemoryCli;
};
channel: {
text: {
chunkMarkdownText: ChunkMarkdownText;
chunkText: ChunkText;
resolveTextChunkLimit: ResolveTextChunkLimit;
hasControlCommand: HasControlCommand;
};
@@ -54,6 +167,9 @@ export type PluginRuntime = {
createReplyDispatcherWithTyping: CreateReplyDispatcherWithTyping;
resolveEffectiveMessagesConfig: ResolveEffectiveMessagesConfig;
resolveHumanDelayConfig: ResolveHumanDelayConfig;
dispatchReplyFromConfig: DispatchReplyFromConfig;
finalizeInboundContext: FinalizeInboundContext;
formatAgentEnvelope: FormatAgentEnvelope;
};
routing: {
resolveAgentRoute: ResolveAgentRoute;
@@ -67,6 +183,11 @@ export type PluginRuntime = {
fetchRemoteMedia: FetchRemoteMedia;
saveMediaBuffer: SaveMediaBuffer;
};
session: {
resolveStorePath: ResolveStorePath;
recordSessionMetaFromInbound: RecordSessionMetaFromInbound;
updateLastRoute: UpdateLastRoute;
};
mentions: {
buildMentionRegexes: BuildMentionRegexes;
matchesMentionPatterns: MatchesMentionPatterns;
@@ -81,6 +202,66 @@ export type PluginRuntime = {
};
commands: {
resolveCommandAuthorizedFromAuthorizers: ResolveCommandAuthorizedFromAuthorizers;
isControlCommandMessage: IsControlCommandMessage;
shouldComputeCommandAuthorized: ShouldComputeCommandAuthorized;
shouldHandleTextCommands: ShouldHandleTextCommands;
};
discord: {
messageActions: DiscordMessageActions;
auditChannelPermissions: AuditDiscordChannelPermissions;
listDirectoryGroupsLive: ListDiscordDirectoryGroupsLive;
listDirectoryPeersLive: ListDiscordDirectoryPeersLive;
probeDiscord: ProbeDiscord;
resolveChannelAllowlist: ResolveDiscordChannelAllowlist;
resolveUserAllowlist: ResolveDiscordUserAllowlist;
sendMessageDiscord: SendMessageDiscord;
sendPollDiscord: SendPollDiscord;
monitorDiscordProvider: MonitorDiscordProvider;
};
slack: {
listDirectoryGroupsLive: ListSlackDirectoryGroupsLive;
listDirectoryPeersLive: ListSlackDirectoryPeersLive;
probeSlack: ProbeSlack;
resolveChannelAllowlist: ResolveSlackChannelAllowlist;
resolveUserAllowlist: ResolveSlackUserAllowlist;
sendMessageSlack: SendMessageSlack;
monitorSlackProvider: MonitorSlackProvider;
handleSlackAction: HandleSlackAction;
};
telegram: {
auditGroupMembership: AuditTelegramGroupMembership;
collectUnmentionedGroupIds: CollectTelegramUnmentionedGroupIds;
probeTelegram: ProbeTelegram;
resolveTelegramToken: ResolveTelegramToken;
sendMessageTelegram: SendMessageTelegram;
monitorTelegramProvider: MonitorTelegramProvider;
messageActions: TelegramMessageActions;
};
signal: {
probeSignal: ProbeSignal;
sendMessageSignal: SendMessageSignal;
monitorSignalProvider: MonitorSignalProvider;
};
imessage: {
monitorIMessageProvider: MonitorIMessageProvider;
probeIMessage: ProbeIMessage;
sendMessageIMessage: SendMessageIMessage;
};
whatsapp: {
getActiveWebListener: GetActiveWebListener;
getWebAuthAgeMs: GetWebAuthAgeMs;
logoutWeb: LogoutWeb;
logWebSelfId: LogWebSelfId;
readWebSelfId: ReadWebSelfId;
webAuthExists: WebAuthExists;
sendMessageWhatsApp: SendMessageWhatsApp;
sendPollWhatsApp: SendPollWhatsApp;
loginWeb: LoginWeb;
startWebLoginWithQr: StartWebLoginWithQr;
waitForWebLogin: WaitForWebLogin;
monitorWebChannel: MonitorWebChannel;
handleWhatsAppAction: HandleWhatsAppAction;
createLoginTool: CreateWhatsAppLoginTool;
};
};
logging: {