From 32ae4566c680408bb619051c1ce4df8cc66afb9c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 18 Jan 2026 16:22:50 +0000 Subject: [PATCH] feat(config): auto-enable configured plugins --- src/config/io.ts | 61 ++++- src/config/plugin-auto-enable.test.ts | 58 +++++ src/config/plugin-auto-enable.ts | 328 ++++++++++++++++++++++++++ src/config/schema.ts | 4 + src/config/types.clawdbot.ts | 6 + src/config/version.ts | 32 +++ src/config/zod-schema.ts | 6 + src/plugins/enable.ts | 46 ++++ src/plugins/loader.ts | 2 +- 9 files changed, 534 insertions(+), 9 deletions(-) create mode 100644 src/config/plugin-auto-enable.test.ts create mode 100644 src/config/plugin-auto-enable.ts create mode 100644 src/config/version.ts create mode 100644 src/plugins/enable.ts diff --git a/src/config/io.ts b/src/config/io.ts index 1e6e303cf..2153d9cd3 100644 --- a/src/config/io.ts +++ b/src/config/io.ts @@ -19,15 +19,18 @@ import { applySessionDefaults, applyTalkApiKey, } from "./defaults.js"; +import { VERSION } from "../version.js"; import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js"; import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js"; import { applyLegacyMigrations, findLegacyConfigIssues } from "./legacy.js"; import { normalizeConfigPaths } from "./normalize-paths.js"; import { resolveConfigPath, resolveStateDir } from "./paths.js"; +import { applyPluginAutoEnable } from "./plugin-auto-enable.js"; import { applyConfigOverrides } from "./runtime-overrides.js"; import type { ClawdbotConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js"; import { validateConfigObject } from "./validation.js"; import { ClawdbotSchema } from "./zod-schema.js"; +import { compareClawdbotVersions } from "./version.js"; // Re-export for backwards compatibility export { CircularIncludeError, ConfigIncludeError } from "./includes.js"; @@ -146,6 +149,30 @@ function formatLegacyMigrationLog(changes: string[]): string { return `Auto-migrated config:\n${changes.map((entry) => `- ${entry}`).join("\n")}`; } +function stampConfigVersion(cfg: ClawdbotConfig): ClawdbotConfig { + const now = new Date().toISOString(); + return { + ...cfg, + meta: { + ...cfg.meta, + lastTouchedVersion: VERSION, + lastTouchedAt: now, + }, + }; +} + +function warnIfConfigFromFuture(cfg: ClawdbotConfig, logger: Pick): void { + const touched = cfg.meta?.lastTouchedVersion; + if (!touched) return; + const cmp = compareClawdbotVersions(VERSION, touched); + if (cmp === null) return; + if (cmp < 0) { + logger.warn( + `Config was last written by a newer Clawdbot (${touched}); current version is ${VERSION}.`, + ); + } +} + function applyConfigEnv(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): void { const envConfig = cfg.env; if (!envConfig) return; @@ -205,7 +232,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { const writeConfigFileSync = (cfg: ClawdbotConfig) => { const dir = path.dirname(configPath); deps.fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); - const json = JSON.stringify(applyModelDefaults(cfg), null, 2).trimEnd().concat("\n"); + const json = JSON.stringify(applyModelDefaults(stampConfigVersion(cfg)), null, 2) + .trimEnd() + .concat("\n"); const tmp = path.join( dir, @@ -277,7 +306,13 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { const substituted = resolveConfigEnvVars(resolved, deps.env); const migrated = applyLegacyMigrations(substituted); - const resolvedConfig = migrated.next ?? substituted; + let resolvedConfig = migrated.next ?? substituted; + const autoEnable = applyPluginAutoEnable({ + config: coerceConfig(resolvedConfig), + env: deps.env, + }); + resolvedConfig = autoEnable.config; + const migrationChanges = [...migrated.changes, ...autoEnable.changes]; warnOnConfigMiskeys(resolvedConfig, deps.logger); if (typeof resolvedConfig !== "object" || resolvedConfig === null) return {}; const validated = ClawdbotSchema.safeParse(resolvedConfig); @@ -288,8 +323,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { } return {}; } - if (migrated.next && migrated.changes.length > 0) { - deps.logger.warn(formatLegacyMigrationLog(migrated.changes)); + warnIfConfigFromFuture(validated.data as ClawdbotConfig, deps.logger); + if (migrationChanges.length > 0) { + deps.logger.warn(formatLegacyMigrationLog(migrationChanges)); try { writeConfigFileSync(resolvedConfig as ClawdbotConfig); } catch (err) { @@ -426,7 +462,13 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { } const migrated = applyLegacyMigrations(substituted); - const resolvedConfigRaw = migrated.next ?? substituted; + let resolvedConfigRaw = migrated.next ?? substituted; + const autoEnable = applyPluginAutoEnable({ + config: coerceConfig(resolvedConfigRaw), + env: deps.env, + }); + resolvedConfigRaw = autoEnable.config; + const migrationChanges = [...migrated.changes, ...autoEnable.changes]; const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw); const validated = validateConfigObject(resolvedConfigRaw); @@ -444,8 +486,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { }; } - if (migrated.next && migrated.changes.length > 0) { - deps.logger.warn(formatLegacyMigrationLog(migrated.changes)); + warnIfConfigFromFuture(validated.config, deps.logger); + if (migrationChanges.length > 0) { + deps.logger.warn(formatLegacyMigrationLog(migrationChanges)); await writeConfigFile(validated.config).catch((err) => { deps.logger.warn(`Failed to write migrated config at ${configPath}: ${String(err)}`); }); @@ -486,7 +529,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) { async function writeConfigFile(cfg: ClawdbotConfig) { const dir = path.dirname(configPath); await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 }); - const json = JSON.stringify(applyModelDefaults(cfg), null, 2).trimEnd().concat("\n"); + const json = JSON.stringify(applyModelDefaults(stampConfigVersion(cfg)), null, 2) + .trimEnd() + .concat("\n"); const tmp = path.join( dir, diff --git a/src/config/plugin-auto-enable.test.ts b/src/config/plugin-auto-enable.test.ts new file mode 100644 index 000000000..4b13f8bad --- /dev/null +++ b/src/config/plugin-auto-enable.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { applyPluginAutoEnable } from "./plugin-auto-enable.js"; + +describe("applyPluginAutoEnable", () => { + it("enables configured channel plugins and updates allowlist", () => { + const result = applyPluginAutoEnable({ + config: { + channels: { slack: { botToken: "x" } }, + plugins: { allow: ["telegram"] }, + }, + }); + + expect(result.config.plugins?.entries?.slack?.enabled).toBe(true); + expect(result.config.plugins?.allow).toEqual(["telegram", "slack"]); + expect(result.changes.join("\n")).toContain('Enabled plugin "slack"'); + }); + + it("respects explicit disable", () => { + const result = applyPluginAutoEnable({ + config: { + channels: { slack: { botToken: "x" } }, + plugins: { entries: { slack: { enabled: false } } }, + }, + }); + + expect(result.config.plugins?.entries?.slack?.enabled).toBe(false); + expect(result.changes).toEqual([]); + }); + + it("enables provider auth plugins when profiles exist", () => { + const result = applyPluginAutoEnable({ + config: { + auth: { + profiles: { + "google-antigravity:default": { + provider: "google-antigravity", + mode: "oauth", + }, + }, + }, + }, + }); + + expect(result.config.plugins?.entries?.["google-antigravity-auth"]?.enabled).toBe(true); + }); + + it("skips when plugins are globally disabled", () => { + const result = applyPluginAutoEnable({ + config: { + channels: { slack: { botToken: "x" } }, + plugins: { enabled: false }, + }, + }); + + expect(result.config.plugins?.entries?.slack?.enabled).toBeUndefined(); + expect(result.changes).toEqual([]); + }); +}); diff --git a/src/config/plugin-auto-enable.ts b/src/config/plugin-auto-enable.ts new file mode 100644 index 000000000..eadebf0fd --- /dev/null +++ b/src/config/plugin-auto-enable.ts @@ -0,0 +1,328 @@ +import type { ClawdbotConfig } from "./config.js"; +import { hasAnyWhatsAppAuth } from "../web/accounts.js"; +import { normalizeProviderId } from "../agents/model-selection.js"; + +type PluginEnableChange = { + pluginId: string; + reason: string; +}; + +export type PluginAutoEnableResult = { + config: ClawdbotConfig; + changes: string[]; +}; + +const CHANNEL_PLUGIN_IDS = [ + "whatsapp", + "telegram", + "discord", + "slack", + "signal", + "imessage", + "msteams", + "matrix", + "zalo", + "zalouser", + "bluebubbles", +] as const; + +const PROVIDER_PLUGIN_IDS: Array<{ pluginId: string; providerId: string }> = [ + { pluginId: "google-antigravity-auth", providerId: "google-antigravity" }, + { pluginId: "google-gemini-cli-auth", providerId: "google-gemini-cli" }, + { pluginId: "qwen-portal-auth", providerId: "qwen-portal" }, + { pluginId: "copilot-proxy", providerId: "copilot-proxy" }, +]; + +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + +function hasNonEmptyString(value: unknown): boolean { + return typeof value === "string" && value.trim().length > 0; +} + +function recordHasKeys(value: unknown): boolean { + return isRecord(value) && Object.keys(value).length > 0; +} + +function accountsHaveKeys( + value: unknown, + keys: string[], +): boolean { + if (!isRecord(value)) return false; + for (const account of Object.values(value)) { + if (!isRecord(account)) continue; + for (const key of keys) { + if (hasNonEmptyString(account[key])) return true; + } + } + return false; +} + +function resolveChannelConfig(cfg: ClawdbotConfig, channelId: string): Record | null { + const channels = cfg.channels as Record | undefined; + const entry = channels?.[channelId]; + return isRecord(entry) ? entry : null; +} + +function isTelegramConfigured(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): boolean { + if (hasNonEmptyString(env.TELEGRAM_BOT_TOKEN)) return true; + const entry = resolveChannelConfig(cfg, "telegram"); + if (!entry) return false; + if (hasNonEmptyString(entry.botToken) || hasNonEmptyString(entry.tokenFile)) return true; + if (accountsHaveKeys(entry.accounts, ["botToken", "tokenFile"])) return true; + return recordHasKeys(entry); +} + +function isDiscordConfigured(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): boolean { + if (hasNonEmptyString(env.DISCORD_BOT_TOKEN)) return true; + const entry = resolveChannelConfig(cfg, "discord"); + if (!entry) return false; + if (hasNonEmptyString(entry.token)) return true; + if (accountsHaveKeys(entry.accounts, ["token"])) return true; + return recordHasKeys(entry); +} + +function isSlackConfigured(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): boolean { + if ( + hasNonEmptyString(env.SLACK_BOT_TOKEN) || + hasNonEmptyString(env.SLACK_APP_TOKEN) || + hasNonEmptyString(env.SLACK_USER_TOKEN) + ) { + return true; + } + const entry = resolveChannelConfig(cfg, "slack"); + if (!entry) return false; + if ( + hasNonEmptyString(entry.botToken) || + hasNonEmptyString(entry.appToken) || + hasNonEmptyString(entry.userToken) + ) { + return true; + } + if (accountsHaveKeys(entry.accounts, ["botToken", "appToken", "userToken"])) return true; + return recordHasKeys(entry); +} + +function isSignalConfigured(cfg: ClawdbotConfig): boolean { + const entry = resolveChannelConfig(cfg, "signal"); + if (!entry) return false; + if ( + hasNonEmptyString(entry.account) || + hasNonEmptyString(entry.httpUrl) || + hasNonEmptyString(entry.httpHost) || + typeof entry.httpPort === "number" || + hasNonEmptyString(entry.cliPath) + ) { + return true; + } + if (accountsHaveKeys(entry.accounts, ["account", "httpUrl", "httpHost", "cliPath"])) return true; + return recordHasKeys(entry); +} + +function isIMessageConfigured(cfg: ClawdbotConfig): boolean { + const entry = resolveChannelConfig(cfg, "imessage"); + if (!entry) return false; + if (hasNonEmptyString(entry.cliPath)) return true; + return recordHasKeys(entry); +} + +function isWhatsAppConfigured(cfg: ClawdbotConfig): boolean { + if (hasAnyWhatsAppAuth(cfg)) return true; + const entry = resolveChannelConfig(cfg, "whatsapp"); + if (!entry) return false; + return recordHasKeys(entry); +} + +function isGenericChannelConfigured(cfg: ClawdbotConfig, channelId: string): boolean { + const entry = resolveChannelConfig(cfg, channelId); + return recordHasKeys(entry); +} + +export function isChannelConfigured( + cfg: ClawdbotConfig, + channelId: string, + env: NodeJS.ProcessEnv = process.env, +): boolean { + switch (channelId) { + case "whatsapp": + return isWhatsAppConfigured(cfg); + case "telegram": + return isTelegramConfigured(cfg, env); + case "discord": + return isDiscordConfigured(cfg, env); + case "slack": + return isSlackConfigured(cfg, env); + case "signal": + return isSignalConfigured(cfg); + case "imessage": + return isIMessageConfigured(cfg); + default: + return isGenericChannelConfigured(cfg, channelId); + } +} + +function collectModelRefs(cfg: ClawdbotConfig): string[] { + const refs: string[] = []; + const pushModelRef = (value: unknown) => { + if (typeof value === "string" && value.trim()) refs.push(value.trim()); + }; + const collectFromAgent = (agent: Record | null | undefined) => { + if (!agent) return; + const model = agent.model; + if (typeof model === "string") { + pushModelRef(model); + } else if (isRecord(model)) { + pushModelRef(model.primary); + const fallbacks = model.fallbacks; + if (Array.isArray(fallbacks)) { + for (const entry of fallbacks) pushModelRef(entry); + } + } + const models = agent.models; + if (isRecord(models)) { + for (const key of Object.keys(models)) { + pushModelRef(key); + } + } + }; + + const defaults = cfg.agents?.defaults as Record | undefined; + collectFromAgent(defaults); + + const list = cfg.agents?.list; + if (Array.isArray(list)) { + for (const entry of list) { + if (isRecord(entry)) collectFromAgent(entry); + } + } + return refs; +} + +function extractProviderFromModelRef(value: string): string | null { + const trimmed = value.trim(); + const slash = trimmed.indexOf("/"); + if (slash <= 0) return null; + return normalizeProviderId(trimmed.slice(0, slash)); +} + +function isProviderConfigured(cfg: ClawdbotConfig, providerId: string): boolean { + const normalized = normalizeProviderId(providerId); + + const profiles = cfg.auth?.profiles; + if (profiles && typeof profiles === "object") { + for (const profile of Object.values(profiles)) { + if (!isRecord(profile)) continue; + const provider = normalizeProviderId(String(profile.provider ?? "")); + if (provider === normalized) return true; + } + } + + const providerConfig = cfg.models?.providers; + if (providerConfig && typeof providerConfig === "object") { + for (const key of Object.keys(providerConfig)) { + if (normalizeProviderId(key) === normalized) return true; + } + } + + const modelRefs = collectModelRefs(cfg); + for (const ref of modelRefs) { + const provider = extractProviderFromModelRef(ref); + if (provider && provider === normalized) return true; + } + + return false; +} + +function resolveConfiguredPlugins(cfg: ClawdbotConfig, env: NodeJS.ProcessEnv): PluginEnableChange[] { + const changes: PluginEnableChange[] = []; + for (const channelId of CHANNEL_PLUGIN_IDS) { + if (isChannelConfigured(cfg, channelId, env)) { + changes.push({ + pluginId: channelId, + reason: `${channelId} configured`, + }); + } + } + for (const mapping of PROVIDER_PLUGIN_IDS) { + if (isProviderConfigured(cfg, mapping.providerId)) { + changes.push({ + pluginId: mapping.pluginId, + reason: `${mapping.providerId} auth configured`, + }); + } + } + return changes; +} + +function isPluginExplicitlyDisabled(cfg: ClawdbotConfig, pluginId: string): boolean { + const entry = cfg.plugins?.entries?.[pluginId]; + return entry?.enabled === false; +} + +function isPluginDenied(cfg: ClawdbotConfig, pluginId: string): boolean { + const deny = cfg.plugins?.deny; + return Array.isArray(deny) && deny.includes(pluginId); +} + +function ensureAllowlisted(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig { + const allow = cfg.plugins?.allow; + if (!Array.isArray(allow) || allow.includes(pluginId)) return cfg; + return { + ...cfg, + plugins: { + ...cfg.plugins, + allow: [...allow, pluginId], + }, + }; +} + +function enablePluginEntry(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig { + const entries = { + ...cfg.plugins?.entries, + [pluginId]: { + ...(cfg.plugins?.entries?.[pluginId] as Record | undefined), + enabled: true, + }, + }; + return { + ...cfg, + plugins: { + ...cfg.plugins, + entries, + ...(cfg.plugins?.enabled === false ? { enabled: true } : {}), + }, + }; +} + +export function applyPluginAutoEnable(params: { + config: ClawdbotConfig; + env?: NodeJS.ProcessEnv; +}): PluginAutoEnableResult { + const env = params.env ?? process.env; + const configured = resolveConfiguredPlugins(params.config, env); + if (configured.length === 0) { + return { config: params.config, changes: [] }; + } + + let next = params.config; + const changes: string[] = []; + + if (next.plugins?.enabled === false) { + return { config: next, changes }; + } + + for (const entry of configured) { + if (isPluginDenied(next, entry.pluginId)) continue; + if (isPluginExplicitlyDisabled(next, entry.pluginId)) continue; + const allow = next.plugins?.allow; + const allowMissing = Array.isArray(allow) && !allow.includes(entry.pluginId); + const alreadyEnabled = next.plugins?.entries?.[entry.pluginId]?.enabled === true; + if (alreadyEnabled && !allowMissing) continue; + next = enablePluginEntry(next, entry.pluginId); + next = ensureAllowlisted(next, entry.pluginId); + changes.push(`Enabled plugin "${entry.pluginId}" (${entry.reason}).`); + } + + return { config: next, changes }; +} diff --git a/src/config/schema.ts b/src/config/schema.ts index 1e5f45aaf..30e643c2f 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -97,6 +97,8 @@ const GROUP_ORDER: Record = { }; const FIELD_LABELS: Record = { + "meta.lastTouchedVersion": "Config Last Touched Version", + "meta.lastTouchedAt": "Config Last Touched At", "update.channel": "Update Channel", "update.checkOnStart": "Update Check on Start", "gateway.remote.url": "Remote Gateway URL", @@ -296,6 +298,8 @@ const FIELD_LABELS: Record = { }; const FIELD_HELP: Record = { + "meta.lastTouchedVersion": "Auto-set when Clawdbot writes the config.", + "meta.lastTouchedAt": "ISO timestamp of the last config write (auto-set).", "update.channel": 'Update channel for npm installs ("stable" or "beta").', "update.checkOnStart": "Check for npm updates when the gateway starts (default: true).", "gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).", diff --git a/src/config/types.clawdbot.ts b/src/config/types.clawdbot.ts index 8bd959e6d..03339436b 100644 --- a/src/config/types.clawdbot.ts +++ b/src/config/types.clawdbot.ts @@ -24,6 +24,12 @@ import type { SkillsConfig } from "./types.skills.js"; import type { ToolsConfig } from "./types.tools.js"; export type ClawdbotConfig = { + meta?: { + /** Last clawdbot version that wrote this config. */ + lastTouchedVersion?: string; + /** ISO timestamp when this config was last written. */ + lastTouchedAt?: string; + }; auth?: AuthConfig; env?: { /** Opt-in: import missing secrets from a login shell environment (exec `$SHELL -l -c 'env -0'`). */ diff --git a/src/config/version.ts b/src/config/version.ts new file mode 100644 index 000000000..c17a2bfa7 --- /dev/null +++ b/src/config/version.ts @@ -0,0 +1,32 @@ +export type ClawdbotVersion = { + major: number; + minor: number; + patch: number; + revision: number; +}; + +const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-(\d+))?/; + +export function parseClawdbotVersion(raw: string | null | undefined): ClawdbotVersion | null { + if (!raw) return null; + const match = raw.trim().match(VERSION_RE); + if (!match) return null; + const [, major, minor, patch, revision] = match; + return { + major: Number.parseInt(major, 10), + minor: Number.parseInt(minor, 10), + patch: Number.parseInt(patch, 10), + revision: revision ? Number.parseInt(revision, 10) : 0, + }; +} + +export function compareClawdbotVersions(a: string | null | undefined, b: string | null | undefined): number | null { + const parsedA = parseClawdbotVersion(a); + const parsedB = parseClawdbotVersion(b); + if (!parsedA || !parsedB) return null; + if (parsedA.major !== parsedB.major) return parsedA.major < parsedB.major ? -1 : 1; + if (parsedA.minor !== parsedB.minor) return parsedA.minor < parsedB.minor ? -1 : 1; + if (parsedA.patch !== parsedB.patch) return parsedA.patch < parsedB.patch ? -1 : 1; + if (parsedA.revision !== parsedB.revision) return parsedA.revision < parsedB.revision ? -1 : 1; + return 0; +} diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 3533221b9..d870a4d74 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -8,6 +8,12 @@ import { CommandsSchema, MessagesSchema, SessionSchema } from "./zod-schema.sess export const ClawdbotSchema = z .object({ + meta: z + .object({ + lastTouchedVersion: z.string().optional(), + lastTouchedAt: z.string().optional(), + }) + .optional(), env: z .object({ shellEnv: z diff --git a/src/plugins/enable.ts b/src/plugins/enable.ts new file mode 100644 index 000000000..fe01e0523 --- /dev/null +++ b/src/plugins/enable.ts @@ -0,0 +1,46 @@ +import type { ClawdbotConfig } from "../config/config.js"; + +export type PluginEnableResult = { + config: ClawdbotConfig; + enabled: boolean; + reason?: string; +}; + +function ensureAllowlisted(cfg: ClawdbotConfig, pluginId: string): ClawdbotConfig { + const allow = cfg.plugins?.allow; + if (!Array.isArray(allow) || allow.includes(pluginId)) return cfg; + return { + ...cfg, + plugins: { + ...cfg.plugins, + allow: [...allow, pluginId], + }, + }; +} + +export function enablePluginInConfig(cfg: ClawdbotConfig, pluginId: string): PluginEnableResult { + if (cfg.plugins?.enabled === false) { + return { config: cfg, enabled: false, reason: "plugins disabled" }; + } + if (cfg.plugins?.deny?.includes(pluginId)) { + return { config: cfg, enabled: false, reason: "blocked by denylist" }; + } + + const entries = { + ...cfg.plugins?.entries, + [pluginId]: { + ...(cfg.plugins?.entries?.[pluginId] as Record | undefined), + enabled: true, + }, + }; + let next: ClawdbotConfig = { + ...cfg, + plugins: { + ...cfg.plugins, + ...(cfg.plugins?.enabled === false ? { enabled: true } : {}), + entries, + }, + }; + next = ensureAllowlisted(next, pluginId); + return { config: next, enabled: true }; +} diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 5d03bd50a..3e6b0f3b6 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -46,7 +46,7 @@ const registryCache = new Map(); const defaultLogger = () => createSubsystemLogger("plugins"); -const BUNDLED_ENABLED_BY_DEFAULT = new Set(["telegram", "whatsapp", "discord", "slack", "signal"]); +const BUNDLED_ENABLED_BY_DEFAULT = new Set(); const normalizeList = (value: unknown): string[] => { if (!Array.isArray(value)) return [];