UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

876 lines (875 loc) 31.8 kB
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, f as normalizeStringifiedOptionalString } from "./string-coerce-mnp54Vah.js"; import { b as parseStrictPositiveInteger } from "./number-coercion-CJQ8TR--.js"; import { t as formatDocsLink } from "./links-CsLBrRff.js"; import { _ as uniqueStrings, l as normalizeStringEntries } from "./string-normalization-WNUDCpXX.js"; import { n as normalizeAccountId, t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js"; import { t as sanitizeForPlainText } from "./sanitize-text-BOGhD8MR.js"; import { l as createScopedDmSecurityResolver, s as createScopedChannelConfigAdapter, t as adaptScopedAccountAccessor } from "./channel-config-helpers-lj5quus3.js"; import "./number-runtime-DBLVDypr.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import { t as chunkTextForOutbound } from "./text-chunking-D45TeeEs.js"; import { n as describeAccountSnapshot } from "./account-helpers-pcH3lGFY.js"; import { a as createSetupInputPresenceValidator, l as patchScopedAccountConfig, t as applyAccountNameToChannelSection } from "./setup-helpers-DO5nSeNy.js"; import { i as createChatChannelPlugin } from "./core-DSxVv-v1.js"; import "./channel-core-IukkNCd-.js"; import "./routing-CQ63ICPX.js"; import { t as createAccountStatusSink } from "./channel-lifecycle.core-Bfh0_sXw.js"; import { n as formatNormalizedAllowFromEntries } from "./allow-from-DCaV3hzI.js"; import { _ as createAllowlistProviderOpenWarningCollector, m as composeAccountWarningCollectors, n as createDangerousNameMatchingMutableAllowlistWarningCollector } from "./channel-policy-CIW58SA5.js"; import { t as createChannelDirectoryAdapter } from "./directory-runtime-om8jJAZt.js"; import { i as createResolvedDirectoryEntriesLister } from "./directory-config-helpers-DEDTHx2P.js"; import { t as createSetupTranslator } from "./i18n-7g_f4oaD.js"; import { J as setSetupChannelEnabled, a as createAllowFromSection, d as createPromptParsedAllowFromForAccount, f as createStandardChannelSetupStatus, h as createTopLevelChannelDmPolicySetter, p as createTopLevelChannelAllowFromSetter } from "./setup-wizard-helpers-Dv-t1nb8.js"; import "./setup-DHfdBmUU.js"; import { T as defineChannelMessageAdapter } from "./channel-outbound-B3_Zy-kG.js"; import { t as PAIRING_APPROVED_MESSAGE } from "./pairing-message-DNhqI-OE.js"; import { d as createDefaultChannelRuntimeState, n as buildBaseChannelStatusSummary, u as createComputedAccountStatusAdapter } from "./status-helpers-BevxX6Pu.js"; import "./channel-status-DxlXr0Xh.js"; import { h as runStoppablePassiveMonitor } from "./extension-shared-B8fkO3TV.js"; import { a as isChannelTarget, c as normalizeIrcMessagingTarget, h as resolveIrcAccount, l as buildIrcConnectOptions, m as resolveDefaultIrcAccountId, n as resolveIrcRequireMention, o as looksLikeIrcTargetId, p as listIrcAccountIds, r as sendMessageIrc, s as normalizeIrcAllowEntry, t as resolveIrcGroupMatch, u as connectIrcClient } from "./policy-XVDgZOja.js"; import { t as IrcChannelConfigSchema } from "./config-schema-Cc7LZDV1.js"; import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-CIWBy1xC.js"; //#region extensions/irc/src/doctor.ts function asObjectRecord(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : null; } function isIrcMutableAllowEntry(raw) { const text = normalizeLowercaseStringOrEmpty(raw); if (!text || text === "*") return false; const normalized = text.replace(/^irc:/, "").replace(/^user:/, "").trim(); return !normalized.includes("!") && !normalized.includes("@"); } const collectIrcMutableAllowlistWarnings = createDangerousNameMatchingMutableAllowlistWarningCollector({ channel: "irc", detector: isIrcMutableAllowEntry, collectLists: (scope) => { const lists = [{ pathLabel: `${scope.prefix}.allowFrom`, list: scope.account.allowFrom }, { pathLabel: `${scope.prefix}.groupAllowFrom`, list: scope.account.groupAllowFrom }]; const groups = asObjectRecord(scope.account.groups); if (groups) for (const [groupKey, groupRaw] of Object.entries(groups)) { const group = asObjectRecord(groupRaw); if (!group) continue; lists.push({ pathLabel: `${scope.prefix}.groups.${groupKey}.allowFrom`, list: group.allowFrom }); } return lists; } }); //#endregion //#region extensions/irc/src/gateway.ts let ircChannelRuntimePromise$1; async function loadIrcChannelRuntime$1() { ircChannelRuntimePromise$1 ??= import("./channel-runtime-iuNmlp4Z.js"); return await ircChannelRuntimePromise$1; } async function startIrcGatewayAccount(ctx) { const account = ctx.account; const statusSink = createAccountStatusSink({ accountId: ctx.accountId, setStatus: ctx.setStatus }); if (!account.configured) throw new Error(`IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`); ctx.log?.info?.(`[${account.accountId}] starting IRC provider (${account.host}:${account.port}${account.tls ? " tls" : ""})`); const { monitorIrcProvider } = await loadIrcChannelRuntime$1(); await runStoppablePassiveMonitor({ abortSignal: ctx.abortSignal, start: async () => await monitorIrcProvider({ accountId: account.accountId, config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, statusSink }) }); } //#endregion //#region extensions/irc/src/message-adapter.ts const ircMessageAdapter = defineChannelMessageAdapter({ id: "irc", durableFinal: { capabilities: { text: true, media: true, replyTo: true } }, send: { text: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageIrc(to, text, { cfg, accountId: accountId ?? void 0, replyTo: replyToId ?? void 0 }), media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageIrc(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, { cfg, accountId: accountId ?? void 0, replyTo: replyToId ?? void 0 }) } }); //#endregion //#region extensions/irc/src/outbound-base.ts const ircOutboundBaseAdapter = { deliveryMode: "direct", chunker: chunkTextForOutbound, chunkerMode: "markdown", textChunkLimit: 350, sanitizeText: ({ text }) => sanitizeForPlainText(text) }; //#endregion //#region extensions/irc/src/probe.ts function formatError(err) { if (err instanceof Error) return err.message; return typeof err === "string" ? err : JSON.stringify(err); } async function probeIrc(cfg, opts) { const account = resolveIrcAccount({ cfg, accountId: opts?.accountId }); const base = { ok: false, host: account.host, port: account.port, tls: account.tls, nick: account.nick }; if (!account.configured) return { ...base, error: "missing host or nick" }; const started = Date.now(); try { const client = await connectIrcClient(buildIrcConnectOptions(account, { connectTimeoutMs: opts?.timeoutMs ?? 8e3 })); const elapsed = Date.now() - started; client.quit("probe"); return { ...base, ok: true, latencyMs: elapsed }; } catch (err) { return { ...base, error: formatError(err) }; } } //#endregion //#region extensions/irc/src/setup-core.ts const channel$1 = "irc"; const setIrcTopLevelDmPolicy = createTopLevelChannelDmPolicySetter({ channel: channel$1 }); const setIrcTopLevelAllowFrom = createTopLevelChannelAllowFromSetter({ channel: channel$1 }); const validateIrcRequiredSetupInput = createSetupInputPresenceValidator({ whenNotUseEnv: [{ someOf: ["host"], message: "IRC requires host." }, { someOf: ["nick"], message: "IRC requires nick." }] }); function parsePort(raw, fallback) { const trimmed = raw.trim(); if (!trimmed) return fallback; const parsed = parseStrictPositiveInteger(trimmed); if (parsed === void 0 || parsed > 65535) return fallback; return parsed; } function validateIrcPortInput(input) { const raw = input.port; if (raw === void 0 || raw === null || raw === "") return null; const parsed = parseStrictPositiveInteger(String(raw)); return parsed !== void 0 && parsed <= 65535 ? null : "IRC port must be between 1 and 65535."; } function updateIrcAccountConfig(cfg, accountId, patch) { return patchScopedAccountConfig({ cfg, channelKey: channel$1, accountId, patch, ensureChannelEnabled: false, ensureAccountEnabled: false }); } function setIrcDmPolicy(cfg, dmPolicy) { return setIrcTopLevelDmPolicy(cfg, dmPolicy); } function setIrcAllowFrom(cfg, allowFrom) { return setIrcTopLevelAllowFrom(cfg, allowFrom); } function setIrcNickServ(cfg, accountId, nickserv) { return updateIrcAccountConfig(cfg, accountId, { nickserv }); } function setIrcGroupAccess(cfg, accountId, policy, entries, normalizeGroupEntry) { if (policy !== "allowlist") return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: policy }); const normalizedEntries = [...new Set(entries.flatMap((entry) => normalizeGroupEntry(entry) ?? []))]; return updateIrcAccountConfig(cfg, accountId, { enabled: true, groupPolicy: "allowlist", groups: Object.fromEntries(normalizedEntries.map((entry) => [entry, {}])) }); } const ircSetupAdapter = { resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ cfg, channelKey: channel$1, accountId, name }), validateInput: (params) => validateIrcRequiredSetupInput(params) ?? validateIrcPortInput(params.input), applyAccountConfig: ({ cfg, accountId, input }) => { const setupInput = input; const namedConfig = applyAccountNameToChannelSection({ cfg, channelKey: channel$1, accountId, name: setupInput.name }); const portInput = typeof setupInput.port === "number" ? String(setupInput.port) : setupInput.port ?? ""; return patchScopedAccountConfig({ cfg: namedConfig, channelKey: channel$1, accountId, patch: { enabled: true, host: setupInput.host?.trim(), port: portInput ? parsePort(portInput, setupInput.tls === false ? 6667 : 6697) : void 0, tls: setupInput.tls, nick: setupInput.nick?.trim(), username: setupInput.username?.trim(), realname: setupInput.realname?.trim(), password: setupInput.password?.trim(), channels: setupInput.channels } }); } }; //#endregion //#region extensions/irc/src/setup-surface.ts const t = createSetupTranslator(); const channel = "irc"; const USE_ENV_FLAG = "__ircUseEnv"; const TLS_FLAG = "__ircTls"; function parseListInput(raw) { return normalizeStringEntries(raw.split(/[\n,;]+/g)); } function normalizeGroupEntry(raw) { const trimmed = raw.trim(); if (!trimmed) return null; if (trimmed === "*") return "*"; const normalized = normalizeIrcMessagingTarget(trimmed) ?? trimmed; if (isChannelTarget(normalized)) return normalized; return `#${normalized.replace(/^#+/, "")}`; } const promptIrcAllowFrom = createPromptParsedAllowFromForAccount({ defaultAccountId: (cfg) => resolveDefaultIrcAccountId(cfg), noteTitle: t("wizard.irc.allowlistTitle"), noteLines: [ t("wizard.irc.allowlistIntro"), t("wizard.irc.examples"), "- alice", "- alice!ident@example.org", t("wizard.irc.multipleEntries") ], message: t("wizard.irc.allowFromPrompt"), placeholder: "alice, bob!ident@example.org", parseEntries: (raw) => ({ entries: normalizeStringEntries(parseListInput(raw).map((entry) => normalizeIrcAllowEntry(entry))) }), getExistingAllowFrom: ({ cfg }) => cfg.channels?.irc?.allowFrom ?? [], applyAllowFrom: ({ cfg, allowFrom }) => setIrcAllowFrom(cfg, allowFrom) }); async function promptIrcNickServConfig(params) { const existing = resolveIrcAccount({ cfg: params.cfg, accountId: params.accountId }).config.nickserv; const hasExisting = Boolean(existing?.password || existing?.passwordFile); if (!await params.prompter.confirm({ message: hasExisting ? t("wizard.irc.nickServUpdatePrompt") : t("wizard.irc.nickServConfigurePrompt"), initialValue: hasExisting })) return params.cfg; const service = (await params.prompter.text({ message: t("wizard.irc.nickServServicePrompt"), initialValue: existing?.service || "NickServ", validate: (value) => normalizeStringifiedOptionalString(value) ? void 0 : "Required" })).trim(); const useEnvPassword = params.accountId === "default" && Boolean(process.env.IRC_NICKSERV_PASSWORD?.trim()) && !(existing?.password || existing?.passwordFile) ? await params.prompter.confirm({ message: t("wizard.irc.nickServPasswordEnvPrompt"), initialValue: true }) : false; const password = useEnvPassword ? void 0 : (await params.prompter.text({ message: t("wizard.irc.nickServPasswordPrompt"), validate: () => void 0 })).trim(); if (!password && !useEnvPassword) return setIrcNickServ(params.cfg, params.accountId, { enabled: false, service }); const register = await params.prompter.confirm({ message: t("wizard.irc.nickServRegisterPrompt"), initialValue: existing?.register ?? false }); const registerEmail = register ? (await params.prompter.text({ message: t("wizard.irc.nickServRegisterEmailPrompt"), initialValue: existing?.registerEmail || (params.accountId === "default" ? process.env.IRC_NICKSERV_REGISTER_EMAIL : void 0), validate: (value) => normalizeStringifiedOptionalString(value) ? void 0 : "Required" })).trim() : void 0; return setIrcNickServ(params.cfg, params.accountId, { enabled: true, service, ...password ? { password } : {}, register, ...registerEmail ? { registerEmail } : {} }); } const ircDmPolicy = { label: "IRC", channel, policyKey: "channels.irc.dmPolicy", allowFromKey: "channels.irc.allowFrom", getCurrent: (cfg) => cfg.channels?.irc?.dmPolicy ?? "pairing", setPolicy: (cfg, policy) => setIrcDmPolicy(cfg, policy), promptAllowFrom: async ({ cfg, prompter, accountId }) => await promptIrcAllowFrom({ cfg, prompter, accountId }) }; const ircSetupWizard = { channel, status: createStandardChannelSetupStatus({ channelLabel: "IRC", configuredLabel: t("wizard.channels.statusConfigured"), unconfiguredLabel: t("wizard.channels.statusNeedsHostNick"), configuredHint: t("wizard.channels.statusConfigured"), unconfiguredHint: t("wizard.channels.statusNeedsHostNick"), configuredScore: 1, unconfiguredScore: 0, includeStatusLine: true, resolveConfigured: ({ cfg, accountId }) => resolveIrcAccount({ cfg, accountId }).configured }), introNote: { title: t("wizard.irc.setupTitle"), lines: [ t("wizard.irc.helpNeedsHostNick"), t("wizard.irc.helpRecommendedTls"), t("wizard.irc.helpNickServOptional"), t("wizard.irc.helpGroupControl"), t("wizard.irc.helpMentionGate"), t("wizard.irc.helpEnvVars"), `Docs: ${formatDocsLink("/channels/irc", "channels/irc")}` ], shouldShow: ({ cfg, accountId }) => !resolveIrcAccount({ cfg, accountId }).configured }, prepare: async ({ cfg, accountId, credentialValues, prompter }) => { const resolved = resolveIrcAccount({ cfg, accountId }); const isDefaultAccount = accountId === DEFAULT_ACCOUNT_ID; const envHost = isDefaultAccount ? normalizeOptionalString(process.env.IRC_HOST) ?? "" : ""; const envNick = isDefaultAccount ? normalizeOptionalString(process.env.IRC_NICK) ?? "" : ""; if (Boolean(envHost && envNick && !resolved.config.host && !resolved.config.nick)) { if (await prompter.confirm({ message: t("wizard.irc.envPrompt"), initialValue: true })) return { cfg: updateIrcAccountConfig(cfg, accountId, { enabled: true }), credentialValues: { ...credentialValues, [USE_ENV_FLAG]: "1" } }; } const tls = await prompter.confirm({ message: t("wizard.irc.tlsPrompt"), initialValue: resolved.config.tls ?? true }); return { cfg: updateIrcAccountConfig(cfg, accountId, { enabled: true, tls }), credentialValues: { ...credentialValues, [USE_ENV_FLAG]: "0", [TLS_FLAG]: tls ? "1" : "0" } }; }, credentials: [], textInputs: [ { inputKey: "httpHost", message: t("wizard.irc.serverHostPrompt"), currentValue: ({ cfg, accountId }) => resolveIrcAccount({ cfg, accountId }).config.host || void 0, shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1", validate: ({ value }) => normalizeStringifiedOptionalString(value) ? void 0 : "Required", normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", applySet: async ({ cfg, accountId, value }) => updateIrcAccountConfig(cfg, accountId, { enabled: true, host: value }) }, { inputKey: "httpPort", message: t("wizard.irc.serverPortPrompt"), currentValue: ({ cfg, accountId }) => String(resolveIrcAccount({ cfg, accountId }).config.port ?? ""), shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1", initialValue: ({ cfg, accountId, credentialValues }) => { const resolved = resolveIrcAccount({ cfg, accountId }); const tls = credentialValues[TLS_FLAG] !== "0"; const defaultPort = resolved.config.port ?? (tls ? 6697 : 6667); return String(defaultPort); }, validate: ({ value }) => { const parsed = parseStrictPositiveInteger(normalizeStringifiedOptionalString(value) ?? ""); return parsed !== void 0 && parsed <= 65535 ? void 0 : "Use a port between 1 and 65535"; }, normalizeValue: ({ value }) => String(parsePort(value, 6697)), applySet: async ({ cfg, accountId, value }) => updateIrcAccountConfig(cfg, accountId, { enabled: true, port: parsePort(value, 6697) }) }, { inputKey: "token", message: t("wizard.irc.nickPrompt"), currentValue: ({ cfg, accountId }) => resolveIrcAccount({ cfg, accountId }).config.nick || void 0, shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1", validate: ({ value }) => normalizeStringifiedOptionalString(value) ? void 0 : "Required", normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", applySet: async ({ cfg, accountId, value }) => updateIrcAccountConfig(cfg, accountId, { enabled: true, nick: value }) }, { inputKey: "userId", message: t("wizard.irc.usernamePrompt"), currentValue: ({ cfg, accountId }) => resolveIrcAccount({ cfg, accountId }).config.username || void 0, shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1", initialValue: ({ cfg, accountId, credentialValues }) => resolveIrcAccount({ cfg, accountId }).config.username || credentialValues.token || "openclaw", validate: ({ value }) => normalizeStringifiedOptionalString(value) ? void 0 : "Required", normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", applySet: async ({ cfg, accountId, value }) => updateIrcAccountConfig(cfg, accountId, { enabled: true, username: value }) }, { inputKey: "deviceName", message: t("wizard.irc.realNamePrompt"), currentValue: ({ cfg, accountId }) => resolveIrcAccount({ cfg, accountId }).config.realname || void 0, shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1", initialValue: ({ cfg, accountId }) => resolveIrcAccount({ cfg, accountId }).config.realname || "OpenClaw", validate: ({ value }) => normalizeStringifiedOptionalString(value) ? void 0 : "Required", normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", applySet: async ({ cfg, accountId, value }) => updateIrcAccountConfig(cfg, accountId, { enabled: true, realname: value }) }, { inputKey: "groupChannels", message: t("wizard.irc.autoJoinPrompt"), placeholder: "#openclaw, #ops", required: false, applyEmptyValue: true, currentValue: ({ cfg, accountId }) => resolveIrcAccount({ cfg, accountId }).config.channels?.join(", "), shouldPrompt: ({ credentialValues }) => credentialValues[USE_ENV_FLAG] !== "1", normalizeValue: ({ value }) => parseListInput(value).map((entry) => normalizeGroupEntry(entry)).filter((entry) => Boolean(entry && entry !== "*")).filter((entry) => isChannelTarget(entry)).join(", "), applySet: async ({ cfg, accountId, value }) => { const channels = parseListInput(value).map((entry) => normalizeGroupEntry(entry)).filter((entry) => Boolean(entry && entry !== "*")).filter((entry) => isChannelTarget(entry)); return updateIrcAccountConfig(cfg, accountId, { enabled: true, channels: channels.length > 0 ? channels : void 0 }); } } ], groupAccess: { label: "IRC channels", placeholder: "#openclaw, #ops, *", currentPolicy: ({ cfg, accountId }) => resolveIrcAccount({ cfg, accountId }).config.groupPolicy ?? "allowlist", currentEntries: ({ cfg, accountId }) => Object.keys(resolveIrcAccount({ cfg, accountId }).config.groups ?? {}), updatePrompt: ({ cfg, accountId }) => Boolean(resolveIrcAccount({ cfg, accountId }).config.groups), setPolicy: ({ cfg, accountId, policy }) => setIrcGroupAccess(cfg, accountId, policy, [], normalizeGroupEntry), resolveAllowlist: async ({ entries }) => uniqueStrings(entries.map((entry) => normalizeGroupEntry(entry)).filter((entry) => Boolean(entry))), applyAllowlist: ({ cfg, accountId, resolved }) => setIrcGroupAccess(cfg, accountId, "allowlist", resolved, normalizeGroupEntry) }, allowFrom: createAllowFromSection({ helpTitle: t("wizard.irc.allowlistTitle"), helpLines: [ t("wizard.irc.allowlistIntro"), t("wizard.irc.examples"), "- alice", "- alice!ident@example.org", t("wizard.irc.multipleEntries") ], message: t("wizard.irc.allowFromPrompt"), placeholder: "alice, bob!ident@example.org", invalidWithoutCredentialNote: t("wizard.irc.allowFromInvalid"), parseId: (raw) => { return normalizeIrcAllowEntry(raw) || null; }, apply: async ({ cfg, allowFrom }) => setIrcAllowFrom(cfg, allowFrom) }), finalize: async ({ cfg, accountId, prompter }) => { let next = cfg; const resolvedAfterGroups = resolveIrcAccount({ cfg: next, accountId }); if (resolvedAfterGroups.config.groupPolicy === "allowlist") { if (Object.keys(resolvedAfterGroups.config.groups ?? {}).length > 0) { if (!await prompter.confirm({ message: t("wizard.irc.requireMentionPrompt"), initialValue: true })) { const groups = resolvedAfterGroups.config.groups ?? {}; const patched = Object.fromEntries(Object.entries(groups).map(([key, value]) => [key, { ...value, requireMention: false }])); next = updateIrcAccountConfig(next, accountId, { groups: patched }); } } } next = await promptIrcNickServConfig({ cfg: next, prompter, accountId }); return { cfg: next }; }, completionNote: { title: t("wizard.irc.nextStepsTitle"), lines: [ t("wizard.irc.nextRestartGateway"), t("wizard.irc.nextStatusCommand"), `Docs: ${formatDocsLink("/channels/irc", "channels/irc")}` ] }, dmPolicy: ircDmPolicy, disable: (cfg) => setSetupChannelEnabled(cfg, channel, false) }; //#endregion //#region extensions/irc/src/channel.ts const meta = { id: "irc", label: "IRC", selectionLabel: "IRC (Server + Nick)", docsPath: "/channels/irc", docsLabel: "irc", blurb: "classic IRC networks; host, nick, channels.", order: 80, detailLabel: "IRC", systemImage: "number", markdownCapable: true }; let ircChannelRuntimePromise; async function loadIrcChannelRuntime() { ircChannelRuntimePromise ??= import("./channel-runtime-iuNmlp4Z.js"); return await ircChannelRuntimePromise; } function normalizePairingTarget(raw) { const normalized = normalizeIrcAllowEntry(raw); if (!normalized) return ""; return normalized.split(/[!@]/, 1)[0]?.trim() ?? ""; } const listIrcDirectoryPeersFromConfig = createResolvedDirectoryEntriesLister({ kind: "user", resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), resolveSources: (account) => [ account.config.allowFrom ?? [], account.config.groupAllowFrom ?? [], ...Object.values(account.config.groups ?? {}).map((group) => group.allowFrom ?? []) ], normalizeId: (entry) => normalizePairingTarget(entry) || null }); const listIrcDirectoryGroupsFromConfig = createResolvedDirectoryEntriesLister({ kind: "group", resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), resolveSources: (account) => [account.config.channels ?? [], Object.keys(account.config.groups ?? {})], normalizeId: (entry) => { const normalized = normalizeIrcMessagingTarget(entry); return normalized && isChannelTarget(normalized) ? normalized : null; } }); const ircConfigAdapter = createScopedChannelConfigAdapter({ sectionKey: "irc", listAccountIds: listIrcAccountIds, resolveAccount: adaptScopedAccountAccessor(resolveIrcAccount), defaultAccountId: resolveDefaultIrcAccountId, clearBaseFields: [ "name", "host", "port", "tls", "nick", "username", "realname", "password", "passwordFile", "channels" ], resolveAllowFrom: (account) => account.config.allowFrom, formatAllowFrom: (allowFrom) => formatNormalizedAllowFromEntries({ allowFrom, normalizeEntry: normalizeIrcAllowEntry }), resolveDefaultTo: (account) => account.config.defaultTo }); const resolveIrcDmPolicy = createScopedDmSecurityResolver({ channelKey: "irc", resolvePolicy: (account) => account.config.dmPolicy, resolveAllowFrom: (account) => account.config.allowFrom, policyPathSuffix: "dmPolicy", normalizeEntry: (raw) => normalizeIrcAllowEntry(raw) }); const collectIrcSecurityWarnings = composeAccountWarningCollectors(createAllowlistProviderOpenWarningCollector({ providerConfigPresent: (cfg) => cfg.channels?.irc !== void 0, resolveGroupPolicy: (account) => account.config.groupPolicy, buildOpenWarning: { surface: "IRC channels", openBehavior: "allows all channels and senders (mention-gated)", remediation: "Prefer channels.irc.groupPolicy=\"allowlist\" with channels.irc.groups" } }), (account) => !account.config.tls && "- IRC TLS is disabled (channels.irc.tls=false); traffic and credentials are plaintext.", (account) => account.config.nickserv?.register && "- IRC NickServ registration is enabled (channels.irc.nickserv.register=true); this sends \"REGISTER\" on every connect. Disable after first successful registration.", (account) => account.config.nickserv?.register && !account.config.nickserv.password?.trim() && "- IRC NickServ registration is enabled but no NickServ password is resolved; set channels.irc.nickserv.password, channels.irc.nickserv.passwordFile, or IRC_NICKSERV_PASSWORD."); const ircPlugin = createChatChannelPlugin({ base: { id: "irc", meta: { ...meta, quickstartAllowFrom: true }, setup: ircSetupAdapter, setupWizard: ircSetupWizard, capabilities: { chatTypes: ["direct", "group"], media: true, blockStreaming: true }, reload: { configPrefixes: ["channels.irc"] }, configSchema: IrcChannelConfigSchema, config: { ...ircConfigAdapter, hasConfiguredState: ({ env }) => typeof env?.IRC_HOST === "string" && env.IRC_HOST.trim().length > 0 && typeof env?.IRC_NICK === "string" && env.IRC_NICK.trim().length > 0, isConfigured: (account) => account.configured, describeAccount: (account) => describeAccountSnapshot({ account, configured: account.configured, extra: { host: account.host, port: account.port, tls: account.tls, nick: account.nick, passwordSource: account.passwordSource } }) }, secrets: { secretTargetRegistryEntries, collectRuntimeConfigAssignments }, doctor: { groupAllowFromFallbackToAllowFrom: false, collectMutableAllowlistWarnings: collectIrcMutableAllowlistWarnings }, groups: { resolveRequireMention: ({ cfg, accountId, groupId }) => { const account = resolveIrcAccount({ cfg, accountId }); if (!groupId) return true; const match = resolveIrcGroupMatch({ groups: account.config.groups, target: groupId }); return resolveIrcRequireMention({ groupConfig: match.groupConfig, wildcardConfig: match.wildcardConfig }); }, resolveToolPolicy: ({ cfg, accountId, groupId }) => { const account = resolveIrcAccount({ cfg, accountId }); if (!groupId) return; const match = resolveIrcGroupMatch({ groups: account.config.groups, target: groupId }); return match.groupConfig?.tools ?? match.wildcardConfig?.tools; } }, messaging: { targetPrefixes: ["irc"], normalizeTarget: normalizeIrcMessagingTarget, targetResolver: { looksLikeId: looksLikeIrcTargetId, hint: "<#channel|nick>" } }, message: ircMessageAdapter, resolver: { resolveTargets: async ({ inputs, kind }) => { return inputs.map((input) => { const normalized = normalizeIrcMessagingTarget(input); if (!normalized) return { input, resolved: false, note: "invalid IRC target" }; if (kind === "group") { const groupId = isChannelTarget(normalized) ? normalized : `#${normalized}`; return { input, resolved: true, id: groupId, name: groupId }; } if (isChannelTarget(normalized)) return { input, resolved: false, note: "expected user target" }; return { input, resolved: true, id: normalized, name: normalized }; }); } }, directory: createChannelDirectoryAdapter({ listPeers: async (params) => listIrcDirectoryPeersFromConfig(params), listGroups: async (params) => { return (await listIrcDirectoryGroupsFromConfig(params)).map((entry) => Object.assign({}, entry, { name: entry.id })); } }), status: createComputedAccountStatusAdapter({ defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID), buildChannelSummary: ({ account, snapshot }) => ({ ...buildBaseChannelStatusSummary(snapshot), host: account.host, port: snapshot.port, tls: account.tls, nick: account.nick, probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null }), probeAccount: async ({ cfg, account, timeoutMs }) => probeIrc(cfg, { accountId: account.accountId, timeoutMs }), resolveAccountSnapshot: ({ account }) => ({ accountId: account.accountId, name: account.name, enabled: account.enabled, configured: account.configured, extra: { host: account.host, port: account.port, tls: account.tls, nick: account.nick, passwordSource: account.passwordSource } }) }), gateway: { startAccount: async (ctx) => await startIrcGatewayAccount({ ...ctx, cfg: ctx.cfg }) } }, pairing: { text: { idLabel: "ircUser", message: PAIRING_APPROVED_MESSAGE, normalizeAllowEntry: (entry) => normalizeIrcAllowEntry(entry), notify: async ({ cfg, id, message }) => { const target = normalizePairingTarget(id); if (!target) throw new Error(`invalid IRC pairing id: ${id}`); const { sendMessageIrc } = await loadIrcChannelRuntime(); await sendMessageIrc(target, message, { cfg }); } } }, security: { resolveDmPolicy: resolveIrcDmPolicy, collectWarnings: collectIrcSecurityWarnings }, outbound: { base: ircOutboundBaseAdapter, attachedResults: { channel: "irc", sendText: async ({ cfg, to, text, accountId, replyToId }) => { const { sendMessageIrc } = await loadIrcChannelRuntime(); return await sendMessageIrc(to, text, { cfg, accountId: accountId ?? void 0, replyTo: replyToId ?? void 0 }); }, sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => { const { sendMessageIrc } = await loadIrcChannelRuntime(); return await sendMessageIrc(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, { cfg, accountId: accountId ?? void 0, replyTo: replyToId ?? void 0 }); } } } }); //#endregion export { ircSetupWizard as n, ircSetupAdapter as r, ircPlugin as t };