openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
817 lines (816 loc) • 35.1 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, l as normalizeOptionalStringifiedId } from "./string-coerce-mnp54Vah.js";
import { i as formatErrorMessage } from "./errors-BXgSefBE.js";
import { n as normalizeAtHashSlug } from "./string-normalization-WNUDCpXX.js";
import { c as isRecord } from "./utils-CCC-BEJH.js";
import { t as DEFAULT_ACCOUNT_ID } from "./account-id-Df9e41E6.js";
import { n as sleepWithAbort } from "./backoff-S1yazmy1.js";
import { i as resolveToolsBySender } from "./group-policy-CShy7eWv.js";
import { r as createLazyRuntimeModule } from "./lazy-runtime-D-7_JraP.js";
import { n as resolveTargetsWithOptionalToken } from "./target-resolvers-C4bHUKEV.js";
import "./error-runtime-C8vbtAJt.js";
import "./runtime-env-D_2q8-VK.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { i as createChatChannelPlugin } from "./core-DSxVv-v1.js";
import "./channel-core-IukkNCd-.js";
import "./channel-policy-CIW58SA5.js";
import { t as createChannelDirectoryAdapter } from "./directory-runtime-om8jJAZt.js";
import { t as createRuntimeDirectoryLiveAdapter } from "./runtime-forwarders-Dxil5z45.js";
import { a as resolveConfiguredFromCredentialStatuses, r as projectCredentialSnapshotFields } from "./account-snapshot-fields-VbzFdu8Y.js";
import { w as createChannelMessageAdapterFromOutbound } from "./channel-outbound-B3_Zy-kG.js";
import { c as createNestedAllowlistOverrideResolver, o as createAccountScopedAllowlistNameResolver, r as buildLegacyDmAccountAllowlistAdapter } from "./allowlist-config-edit-dX8vJeki.js";
import { t as PAIRING_APPROVED_MESSAGE } from "./pairing-message-DNhqI-OE.js";
import { _ as resolveEnabledConfiguredAccountId, d as createDefaultChannelRuntimeState, m as asString, o as buildTokenChannelStatusSummary, p as appendMatchMetadata, u as createComputedAccountStatusAdapter } from "./status-helpers-BevxX6Pu.js";
import { i as createPairingPrefixStripper } from "./channel-pairing-Bk6_JhTm.js";
import { c as resolveDiscordAccountAllowFrom, r as listDiscordAccountIds, s as resolveDiscordAccount } from "./accounts-BI_6uJlY.js";
import "./channel-api-_pvHxxap.js";
import { t as parseDiscordTarget } from "./target-parsing-CxulEx_f.js";
import { t as resolveDiscordOutboundSessionRoute } from "./outbound-session-route-BzeFQp5N.js";
import { n as looksLikeDiscordTargetId, r as normalizeDiscordMessagingTarget } from "./normalize-SUovoNvy.js";
import { t as getDiscordRuntime } from "./runtime-n11XewvP.js";
import { a as shouldSuppressLocalDiscordExecApprovalPrompt } from "./approval-shared-CvDEtoxb.js";
import { t as getDiscordApprovalCapability } from "./approval-native-OgnqttC5.js";
import { r as resolveRequiredDiscordChannelPermissions } from "./audit-core-BK6RTSya.js";
import { t as discordMessageActions$1 } from "./channel-actions-DPUWlPaE.js";
import { n as resolveDiscordCurrentConversationIdentity } from "./conversation-identity-LwFFXgXY.js";
import { n as setThreadBindingMaxAgeBySessionKey, t as setThreadBindingIdleTimeoutBySessionKey } from "./thread-bindings.session-updates-n5lDXku4.js";
import { n as discordOutbound } from "./outbound-adapter-AcNudkf5.js";
import { i as discordSecurityAdapter, n as discordConfigAdapter, r as discordSetupAdapter, t as createDiscordPluginBase } from "./shared-ChMXo4Cv.js";
import { t as normalizeExplicitDiscordSessionKey } from "./session-key-normalization-0WA7az_3.js";
//#region extensions/discord/src/channel.conversation.ts
function resolveDiscordAttachedOutboundTarget(params) {
if (params.threadId == null) return params.to;
const threadId = normalizeOptionalStringifiedId(params.threadId) ?? "";
return threadId ? `channel:${threadId}` : params.to;
}
function buildDiscordCrossContextPresentation(params) {
return {
tone: "neutral",
blocks: [...params.message.trim() ? [{
type: "text",
text: params.message
}, { type: "divider" }] : [], {
type: "context",
text: `From ${params.originLabel}`
}]
};
}
function normalizeDiscordAcpConversationId(conversationId) {
const normalized = conversationId.trim();
return normalized ? { conversationId: normalized } : null;
}
function matchDiscordAcpConversation(params) {
if (params.bindingConversationId === params.conversationId) return {
conversationId: params.conversationId,
matchPriority: 2
};
if (params.parentConversationId && params.parentConversationId !== params.conversationId && params.bindingConversationId === params.parentConversationId) return {
conversationId: params.parentConversationId,
matchPriority: 1
};
return null;
}
function resolveDiscordConversationIdFromTargets(targets) {
for (const raw of targets) {
const trimmed = raw?.trim();
if (!trimmed) continue;
try {
const target = parseDiscordTarget(trimmed, { defaultKind: "channel" });
if (target?.normalized) return target.normalized;
} catch {
const mentionMatch = trimmed.match(/^<#(\d+)>$/);
if (mentionMatch?.[1]) return `channel:${mentionMatch[1]}`;
if (/^\d{6,}$/.test(trimmed)) return normalizeDiscordMessagingTarget(trimmed);
}
}
}
function parseDiscordParentChannelFromSessionKey(raw) {
const sessionKey = normalizeLowercaseStringOrEmpty(raw);
if (!sessionKey) return;
const match = sessionKey.match(/(?:^|:)channel:([^:]+)$/);
return match?.[1] ? `channel:${match[1]}` : void 0;
}
function resolveDiscordCommandConversation(params) {
const threadConversation = resolveDiscordThreadConversationRef(params);
if (threadConversation) return threadConversation;
const conversationId = resolveDiscordCurrentConversationIdentity({
from: params.from,
chatType: params.chatType,
originatingTo: params.originatingTo,
commandTo: params.commandTo,
fallbackTo: params.fallbackTo
});
return conversationId ? { conversationId } : null;
}
function resolveDiscordThreadConversationRef(params) {
const threadId = normalizeOptionalStringifiedId(params.threadId);
if (!threadId) return null;
const targets = [
params.originatingTo ?? params.to,
params.commandTo,
params.fallbackTo ?? params.conversationId
];
const parentConversationId = normalizeDiscordMessagingTarget(normalizeOptionalStringifiedId(params.threadParentId) ?? "") || parseDiscordParentChannelFromSessionKey(params.parentSessionKey) || resolveDiscordConversationIdFromTargets(targets);
return {
conversationId: threadId,
...parentConversationId && parentConversationId !== threadId ? { parentConversationId } : {}
};
}
function resolveDiscordInboundConversation(params) {
const threadConversation = resolveDiscordThreadConversationRef({
to: params.to,
conversationId: params.conversationId,
threadId: params.threadId,
threadParentId: params.threadParentId
});
if (threadConversation) return threadConversation;
const conversationId = resolveDiscordCurrentConversationIdentity({
from: params.from,
chatType: params.isGroup ? "group" : "direct",
originatingTo: params.to,
fallbackTo: params.conversationId
});
return conversationId ? { conversationId } : null;
}
//#endregion
//#region extensions/discord/src/channel.loaders.ts
let discordProviderRuntimePromise;
let discordProbeRuntimePromise;
let discordAuditModulePromise;
let discordSendModulePromise;
let discordDirectoryLiveModulePromise;
const loadDiscordDirectoryConfigModule = createLazyRuntimeModule(() => import("./directory-config-CcndzdfR.js"));
const loadDiscordResolveChannelsModule = createLazyRuntimeModule(() => import("./resolve-channels-BhGmEeFE.js"));
const loadDiscordResolveUsersModule = createLazyRuntimeModule(() => import("./resolve-users-BaJ9MWtp.js"));
const loadDiscordThreadBindingsManagerModule = createLazyRuntimeModule(() => import("./thread-bindings.manager-BmnPM0iX.js"));
const loadDiscordTargetResolverModule = createLazyRuntimeModule(() => import("./target-resolver-D3qKjG84.js"));
async function loadDiscordProviderRuntime() {
discordProviderRuntimePromise ??= import("./provider.runtime.js");
return await discordProviderRuntimePromise;
}
async function loadDiscordProbeRuntime() {
discordProbeRuntimePromise ??= import("./probe.runtime-C5NO5NbR.js");
return await discordProbeRuntimePromise;
}
async function loadDiscordAuditModule() {
discordAuditModulePromise ??= import("./audit-PHH_IBx4.js");
return await discordAuditModulePromise;
}
async function loadDiscordSendModule() {
discordSendModulePromise ??= import("./send-B-l8DwLG.js");
return await discordSendModulePromise;
}
async function loadDiscordDirectoryLiveModule() {
discordDirectoryLiveModulePromise ??= import("./directory-live-Cz96H4KV.js");
return await discordDirectoryLiveModulePromise;
}
//#endregion
//#region extensions/discord/src/group-policy.ts
function normalizeDiscordSlug(value) {
return normalizeAtHashSlug(value);
}
function resolveDiscordGuildEntry(guilds, groupSpace) {
if (!guilds || Object.keys(guilds).length === 0) return null;
const space = normalizeOptionalString(groupSpace) ?? "";
if (space && guilds[space]) return guilds[space];
const normalized = normalizeDiscordSlug(space);
if (normalized && guilds[normalized]) return guilds[normalized];
if (normalized) {
const match = Object.values(guilds).find((entry) => normalizeDiscordSlug(entry?.slug ?? void 0) === normalized);
if (match) return match;
}
return guilds["*"] ?? null;
}
function resolveDiscordChannelEntry(channelEntries, params) {
if (!channelEntries || Object.keys(channelEntries).length === 0) return;
const groupChannel = params.groupChannel;
const channelSlug = normalizeDiscordSlug(groupChannel);
return (params.groupId ? channelEntries[params.groupId] : void 0) ?? (channelSlug ? channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`] : void 0) ?? (groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : void 0);
}
function resolveSenderToolsEntry(entry, params) {
if (!entry) return;
return resolveToolsBySender({
toolsBySender: entry.toolsBySender,
senderId: params.senderId,
senderName: params.senderName,
senderUsername: params.senderUsername,
senderE164: params.senderE164
}) ?? entry.tools;
}
function resolveDiscordPolicyContext(params) {
const guildEntry = resolveDiscordGuildEntry((params.accountId ? params.cfg.channels?.discord?.accounts?.[params.accountId]?.guilds : void 0) ?? params.cfg.channels?.discord?.guilds, params.groupSpace);
const channelEntries = guildEntry?.channels;
return {
guildEntry,
channelEntry: channelEntries && Object.keys(channelEntries).length > 0 ? resolveDiscordChannelEntry(channelEntries, params) : void 0
};
}
function resolveDiscordGroupRequireMention(params) {
const context = resolveDiscordPolicyContext(params);
if (typeof context.channelEntry?.requireMention === "boolean") return context.channelEntry.requireMention;
if (typeof context.guildEntry?.requireMention === "boolean") return context.guildEntry.requireMention;
return true;
}
function resolveDiscordGroupToolPolicy(params) {
const context = resolveDiscordPolicyContext(params);
const channelPolicy = resolveSenderToolsEntry(context.channelEntry, params);
if (channelPolicy) return channelPolicy;
return resolveSenderToolsEntry(context.guildEntry, params);
}
//#endregion
//#region extensions/discord/src/status-issues.ts
function readDiscordAccountStatus(value) {
if (!isRecord(value)) return null;
return {
accountId: value.accountId,
enabled: value.enabled,
configured: value.configured,
running: value.running,
connected: value.connected,
healthState: value.healthState,
application: value.application,
audit: value.audit
};
}
function readDiscordApplicationSummary(value) {
if (!isRecord(value)) return {};
const intentsRaw = value.intents;
if (!isRecord(intentsRaw)) return {};
return { intents: { messageContent: intentsRaw.messageContent === "enabled" || intentsRaw.messageContent === "limited" || intentsRaw.messageContent === "disabled" ? intentsRaw.messageContent : void 0 } };
}
function readDiscordPermissionsAuditSummary(value) {
if (!isRecord(value)) return {};
const unresolvedChannels = typeof value.unresolvedChannels === "number" && Number.isFinite(value.unresolvedChannels) ? value.unresolvedChannels : void 0;
const channelsRaw = value.channels;
return {
unresolvedChannels,
channels: Array.isArray(channelsRaw) ? channelsRaw.map((entry) => {
if (!isRecord(entry)) return null;
const channelId = asString(entry.channelId);
if (!channelId) return null;
const ok = typeof entry.ok === "boolean" ? entry.ok : void 0;
const missing = Array.isArray(entry.missing) ? entry.missing.map((v) => asString(v)).filter(Boolean) : void 0;
const error = asString(entry.error) ?? null;
const matchKey = asString(entry.matchKey) ?? void 0;
const matchSource = asString(entry.matchSource) ?? void 0;
return {
channelId,
ok,
missing: missing?.length ? missing : void 0,
error,
matchKey,
matchSource
};
}).filter(Boolean) : void 0
};
}
function collectDiscordStatusIssues(accounts) {
const issues = [];
for (const entry of accounts) {
const account = readDiscordAccountStatus(entry);
if (!account) continue;
const accountId = resolveEnabledConfiguredAccountId(account);
if (!accountId) continue;
const running = account.running === true;
const healthState = asString(account.healthState);
if (healthState === "stale-socket" || healthState === "stuck" || healthState === "disconnected" || healthState === "not-running") {
const runningLabel = running ? "running" : "not running";
issues.push({
channel: "discord",
accountId,
kind: "runtime",
message: `Discord gateway transport is degraded (${healthState}; account is ${runningLabel}).`,
fix: "Check gateway event-loop health and Discord connectivity, then restart the Discord channel or gateway if the transport does not recover."
});
} else if (running && account.connected === false) issues.push({
channel: "discord",
accountId,
kind: "runtime",
message: "Discord gateway transport is running but disconnected.",
fix: "Check gateway logs for Discord websocket errors and wait for reconnect; restart the Discord channel or gateway if it does not recover."
});
if (readDiscordApplicationSummary(account.application).intents?.messageContent === "disabled") issues.push({
channel: "discord",
accountId,
kind: "intent",
message: "Message Content Intent is disabled. Bot may not see normal channel messages.",
fix: "Enable Message Content Intent in Discord Dev Portal → Bot → Privileged Gateway Intents, or require mention-only operation."
});
const audit = readDiscordPermissionsAuditSummary(account.audit);
if (audit.unresolvedChannels && audit.unresolvedChannels > 0) issues.push({
channel: "discord",
accountId,
kind: "config",
message: `Some configured guild channels are not numeric IDs (unresolvedChannels=${audit.unresolvedChannels}). Permission audit can only check numeric channel IDs.`,
fix: "Use numeric channel IDs as keys in channels.discord.guilds.*.channels (then rerun channels status --probe)."
});
for (const channel of audit.channels ?? []) {
if (channel.ok === true) continue;
const missing = channel.missing?.length ? ` missing ${channel.missing.join(", ")}` : "";
const error = channel.error ? `: ${channel.error}` : "";
const baseMessage = `Channel ${channel.channelId} permission check failed.${missing}${error}`;
issues.push({
channel: "discord",
accountId,
kind: "permissions",
message: appendMatchMetadata(baseMessage, {
matchKey: channel.matchKey,
matchSource: channel.matchSource
}),
fix: "Ensure the bot role can view + send in this channel (and that channel overrides don't deny it)."
});
}
}
return issues;
}
//#endregion
//#region extensions/discord/src/channel.ts
const DISCORD_ACCOUNT_STARTUP_STAGGER_MS = 1e4;
const discordMessageAdapter = createChannelMessageAdapterFromOutbound({
id: "discord",
outbound: discordOutbound,
live: {
capabilities: {
draftPreview: true,
previewFinalization: true,
progressUpdates: true
},
finalizer: { capabilities: {
finalEdit: true,
normalFallback: true,
discardPending: true
} }
}
});
function startDiscordStartupProbe(params) {
(async () => {
try {
const probe = await (await loadDiscordProbeRuntime()).probeDiscord(params.token, 2500, { includeApplication: true });
if (params.abortSignal.aborted) return;
params.setStatus({
accountId: params.accountId,
bot: probe.bot,
application: probe.application
});
if (probe.ok) {
const username = probe.bot?.username?.trim();
if (username) params.log?.info?.(`[${params.accountId}] Discord bot probe resolved @${username}`);
} else if (getDiscordRuntime().logging.shouldLogVerbose()) params.log?.debug?.(`[${params.accountId}] bot probe degraded: ${probe.error ?? `status ${probe.status ?? "unknown"}`}`);
const messageContent = probe.application?.intents?.messageContent;
if (messageContent === "disabled") params.log?.warn?.(`[${params.accountId}] Discord Message Content Intent is disabled; bot may not respond to channel messages. Enable it in Discord Dev Portal (Bot → Privileged Gateway Intents) or require mentions.`);
else if (messageContent === "limited") params.log?.info?.(`[${params.accountId}] Discord Message Content Intent is limited; bots under 100 servers can use it without verification.`);
} catch (err) {
if (!params.abortSignal.aborted) params.setStatus({
accountId: params.accountId,
bot: void 0,
application: void 0
});
if (getDiscordRuntime().logging.shouldLogVerbose()) params.log?.debug?.(`[${params.accountId}] bot probe failed: ${String(err)}`);
}
})();
}
function shouldTreatDiscordDeliveredTextAsVisible(params) {
return params.kind === "block" && typeof params.text === "string" && params.text.trim().length > 0;
}
function resolveRuntimeDiscordMessageActions() {
try {
return getDiscordRuntime().channel?.discord?.messageActions ?? null;
} catch {
return null;
}
}
const discordMessageActions = {
resolveExecutionMode: (ctx) => resolveRuntimeDiscordMessageActions()?.resolveExecutionMode?.(ctx) ?? discordMessageActions$1.resolveExecutionMode?.(ctx) ?? "local",
describeMessageTool: (ctx) => resolveRuntimeDiscordMessageActions()?.describeMessageTool?.(ctx) ?? discordMessageActions$1.describeMessageTool?.(ctx) ?? null,
extractToolSend: (ctx) => resolveRuntimeDiscordMessageActions()?.extractToolSend?.(ctx) ?? discordMessageActions$1.extractToolSend?.(ctx) ?? null,
prepareSendPayload: (ctx) => resolveRuntimeDiscordMessageActions()?.prepareSendPayload?.(ctx) ?? discordMessageActions$1.prepareSendPayload?.(ctx) ?? null,
handleAction: async (ctx) => {
const runtimeHandleAction = resolveRuntimeDiscordMessageActions()?.handleAction;
if (runtimeHandleAction) return await runtimeHandleAction(ctx);
if (!discordMessageActions$1.handleAction) throw new Error("Discord message actions not available");
return await discordMessageActions$1.handleAction(ctx);
}
};
function resolveDiscordStartupDelayMs(cfg, accountId) {
const startupIndex = listDiscordAccountIds(cfg).filter((candidateId) => {
const candidate = resolveDiscordAccount({
cfg,
accountId: candidateId
});
return candidate.enabled && (resolveConfiguredFromCredentialStatuses(candidate) ?? Boolean(normalizeOptionalString(candidate.token)));
}).findIndex((candidateId) => candidateId === accountId);
return startupIndex <= 0 ? 0 : startupIndex * DISCORD_ACCOUNT_STARTUP_STAGGER_MS;
}
function formatDiscordIntents(intents) {
if (!intents) return "unknown";
return [
`messageContent=${intents.messageContent ?? "unknown"}`,
`guildMembers=${intents.guildMembers ?? "unknown"}`,
`presence=${intents.presence ?? "unknown"}`
].join(" ");
}
const resolveDiscordAllowlistGroupOverrides = createNestedAllowlistOverrideResolver({
resolveRecord: (account) => account.config.guilds,
outerLabel: (guildKey) => `guild ${guildKey}`,
resolveOuterEntries: (guildCfg) => guildCfg?.users,
resolveChildren: (guildCfg) => guildCfg?.channels,
innerLabel: (guildKey, channelKey) => `guild ${guildKey} / channel ${channelKey}`,
resolveInnerEntries: (channelCfg) => channelCfg?.users
});
const resolveDiscordAllowlistNames = createAccountScopedAllowlistNameResolver({
resolveAccount: resolveDiscordAccount,
resolveToken: (account) => account.token,
resolveNames: async ({ token, entries }) => (await loadDiscordResolveUsersModule()).resolveDiscordUserAllowlist({
token,
entries
})
});
function toConversationLifecycleBinding(binding) {
return {
boundAt: binding.boundAt,
lastActivityAt: typeof binding.lastActivityAt === "number" ? binding.lastActivityAt : binding.boundAt,
idleTimeoutMs: typeof binding.idleTimeoutMs === "number" ? binding.idleTimeoutMs : void 0,
maxAgeMs: typeof binding.maxAgeMs === "number" ? binding.maxAgeMs : void 0
};
}
const discordPlugin = createChatChannelPlugin({
base: {
...createDiscordPluginBase({ setup: discordSetupAdapter }),
allowlist: {
...buildLegacyDmAccountAllowlistAdapter({
channelId: "discord",
resolveAccount: resolveDiscordAccount,
normalize: ({ cfg, accountId, values }) => discordConfigAdapter.formatAllowFrom({
cfg,
accountId,
allowFrom: values
}),
resolveDmAllowFrom: (account, { cfg }) => resolveDiscordAccountAllowFrom({
cfg,
accountId: account.accountId
}),
resolveGroupPolicy: (account) => account.config.groupPolicy,
resolveGroupOverrides: resolveDiscordAllowlistGroupOverrides
}),
resolveNames: resolveDiscordAllowlistNames
},
groups: {
resolveRequireMention: resolveDiscordGroupRequireMention,
resolveToolPolicy: resolveDiscordGroupToolPolicy
},
mentions: { stripPatterns: () => ["<@!?\\d+>"] },
agentPrompt: { messageToolHints: () => [
"- Discord mentions: use canonical outbound syntax: users `<@USER_ID>`, channels `<#CHANNEL_ID>`, and roles `<@&ROLE_ID>`. Plain `@name` text only pings when a configured `mentionAliases` entry rewrites it; do not use the legacy `<@!USER_ID>` nickname form.",
"- Discord components: set `components` when sending messages to include buttons, selects, or v2 containers.",
"- Forms: add `components.modal` (title, fields). OpenClaw adds a trigger button and routes submissions as new messages."
] },
messaging: {
targetPrefixes: ["discord"],
normalizeTarget: normalizeDiscordMessagingTarget,
resolveInboundConversation: ({ from, to, conversationId, threadId, threadParentId, isGroup }) => resolveDiscordInboundConversation({
from,
to,
conversationId,
threadId,
threadParentId,
isGroup
}),
normalizeExplicitSessionKey: ({ sessionKey, ctx }) => normalizeExplicitDiscordSessionKey(sessionKey, ctx),
resolveSessionTarget: ({ id }) => normalizeDiscordMessagingTarget(`channel:${id}`),
inferTargetChatType: ({ to }) => {
try {
const parsed = parseDiscordTarget(to, { defaultKind: "channel" });
if (!parsed) return;
return parsed?.kind === "user" ? "direct" : "channel";
} catch {
return;
}
},
buildCrossContextPresentation: buildDiscordCrossContextPresentation,
resolveOutboundSessionRoute: (params) => resolveDiscordOutboundSessionRoute(params),
targetResolver: {
looksLikeId: looksLikeDiscordTargetId,
hint: "<channelId|user:ID|channel:ID>",
resolveTarget: async ({ cfg, accountId, input, normalized, preferredKind }) => {
const resolved = await (await loadDiscordTargetResolverModule()).resolveDiscordTarget(input, {
cfg,
accountId
}, preferredKind === "user" ? { defaultKind: "user" } : preferredKind === "channel" || preferredKind === "group" ? { defaultKind: "channel" } : {});
if (!resolved) return null;
return {
to: resolved.normalized,
kind: resolved.kind === "user" ? "user" : "channel",
display: resolved.raw,
source: resolved.normalized === normalized ? "normalized" : "directory"
};
}
}
},
approvalCapability: getDiscordApprovalCapability(),
directory: createChannelDirectoryAdapter({
listPeers: async (params) => (await loadDiscordDirectoryConfigModule()).listDiscordDirectoryPeersFromConfig(params),
listGroups: async (params) => (await loadDiscordDirectoryConfigModule()).listDiscordDirectoryGroupsFromConfig(params),
...createRuntimeDirectoryLiveAdapter({
getRuntime: loadDiscordDirectoryLiveModule,
listPeersLive: (runtime) => runtime.listDiscordDirectoryPeersLive,
listGroupsLive: (runtime) => runtime.listDiscordDirectoryGroupsLive
})
}),
message: discordMessageAdapter,
resolver: { resolveTargets: async ({ cfg, accountId, inputs, kind }) => {
const account = resolveDiscordAccount({
cfg,
accountId
});
if (kind === "group") return resolveTargetsWithOptionalToken({
token: account.token,
inputs,
missingTokenNote: "missing Discord token",
resolveWithToken: async ({ token, inputs: inputsValue }) => (await loadDiscordResolveChannelsModule()).resolveDiscordChannelAllowlist({
token,
entries: inputsValue
}),
mapResolved: (entry) => ({
input: entry.input,
resolved: entry.resolved,
id: entry.channelId ?? entry.guildId,
name: entry.channelName ?? entry.guildName ?? (entry.guildId && !entry.channelId ? entry.guildId : void 0),
note: entry.note
})
});
return resolveTargetsWithOptionalToken({
token: account.token,
inputs,
missingTokenNote: "missing Discord token",
resolveWithToken: async ({ token, inputs: inputsLocal }) => (await loadDiscordResolveUsersModule()).resolveDiscordUserAllowlist({
token,
entries: inputsLocal
}),
mapResolved: (entry) => ({
input: entry.input,
resolved: entry.resolved,
id: entry.id,
name: entry.name,
note: entry.note
})
});
} },
actions: discordMessageActions,
bindings: {
compileConfiguredBinding: ({ conversationId }) => normalizeDiscordAcpConversationId(conversationId),
matchInboundConversation: ({ compiledBinding, conversationId, parentConversationId }) => matchDiscordAcpConversation({
bindingConversationId: compiledBinding.conversationId,
conversationId,
parentConversationId
}),
resolveCommandConversation: ({ threadId, threadParentId, parentSessionKey, from, chatType, originatingTo, commandTo, fallbackTo }) => resolveDiscordCommandConversation({
threadId,
threadParentId,
parentSessionKey,
from,
chatType,
originatingTo,
commandTo,
fallbackTo
})
},
conversationBindings: {
supportsCurrentConversationBinding: true,
defaultTopLevelPlacement: "child",
createManager: async ({ cfg, accountId }) => (await loadDiscordThreadBindingsManagerModule()).createThreadBindingManager({
cfg,
accountId: accountId ?? void 0,
persist: false,
enableSweeper: false
}),
setIdleTimeoutBySessionKey: ({ targetSessionKey, accountId, idleTimeoutMs }) => setThreadBindingIdleTimeoutBySessionKey({
targetSessionKey,
accountId: accountId ?? void 0,
idleTimeoutMs
}).map(toConversationLifecycleBinding),
setMaxAgeBySessionKey: ({ targetSessionKey, accountId, maxAgeMs }) => setThreadBindingMaxAgeBySessionKey({
targetSessionKey,
accountId: accountId ?? void 0,
maxAgeMs
}).map(toConversationLifecycleBinding)
},
heartbeat: { sendTyping: async ({ cfg, to, accountId, threadId }) => {
const target = parseDiscordTarget(resolveDiscordAttachedOutboundTarget({
to,
threadId
}), { defaultKind: "channel" });
if (!target || target.kind !== "channel") return;
await (await loadDiscordSendModule()).sendTypingDiscord(target.id, {
cfg,
accountId: accountId ?? void 0
});
} },
status: createComputedAccountStatusAdapter({
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, {
connected: false,
reconnectAttempts: 0,
lastConnectedAt: null,
lastDisconnect: null,
lastEventAt: null
}),
collectStatusIssues: collectDiscordStatusIssues,
buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot, { includeMode: false }),
probeAccount: async ({ account, timeoutMs }) => (await loadDiscordProbeRuntime()).probeDiscord(account.token, timeoutMs, { includeApplication: true }),
formatCapabilitiesProbe: ({ probe }) => {
const discordProbe = probe;
const lines = [];
if (discordProbe?.bot?.username) {
const botId = discordProbe.bot.id ? ` (${discordProbe.bot.id})` : "";
lines.push({ text: `Bot: @${discordProbe.bot.username}${botId}` });
}
if (discordProbe?.application?.intents) lines.push({ text: `Intents: ${formatDiscordIntents(discordProbe.application.intents)}` });
return lines;
},
buildCapabilitiesDiagnostics: async ({ account, target }) => {
if (!target?.trim()) return;
const parsedTarget = parseDiscordTarget(target.trim(), { defaultKind: "channel" });
const details = { target: {
raw: target,
normalized: parsedTarget?.normalized,
kind: parsedTarget?.kind,
channelId: parsedTarget?.kind === "channel" ? parsedTarget.id : void 0
} };
if (!parsedTarget || parsedTarget.kind !== "channel") return {
details,
lines: [{
text: "Permissions: Target looks like a DM user; pass channel:<id> to audit channel permissions.",
tone: "error"
}]
};
const token = account.token?.trim();
if (!token) return {
details,
lines: [{
text: "Permissions: Discord bot token missing for permission audit.",
tone: "error"
}]
};
const statusCfg = { channels: { discord: { accounts: { [account.accountId]: {
...account.config,
token
} } } } };
try {
const perms = await (await loadDiscordSendModule()).fetchChannelPermissionsDiscord(parsedTarget.id, {
cfg: statusCfg,
token,
accountId: account.accountId ?? void 0
});
const missingRequired = resolveRequiredDiscordChannelPermissions(perms.channelType).filter((permission) => !perms.permissions.includes(permission));
details.permissions = {
channelId: perms.channelId,
guildId: perms.guildId,
isDm: perms.isDm,
channelType: perms.channelType,
permissions: perms.permissions,
missingRequired,
raw: perms.raw
};
return {
details,
lines: [{ text: `Permissions (${perms.channelId}): ${perms.permissions.length ? perms.permissions.join(", ") : "none"}` }, missingRequired.length > 0 ? {
text: `Missing required: ${missingRequired.join(", ")}`,
tone: "warn"
} : {
text: "Missing required: none",
tone: "success"
}]
};
} catch (err) {
const message = formatErrorMessage(err);
details.permissions = {
channelId: parsedTarget.id,
error: message
};
return {
details,
lines: [{
text: `Permissions: ${message}`,
tone: "error"
}]
};
}
},
auditAccount: async ({ account, timeoutMs, cfg }) => {
const { auditDiscordChannelPermissions, collectDiscordAuditChannelIds } = await loadDiscordAuditModule();
const { channelIds, unresolvedChannels } = collectDiscordAuditChannelIds({
cfg,
accountId: account.accountId
});
if (!channelIds.length && unresolvedChannels === 0) return;
const botToken = account.token?.trim();
if (!botToken) return {
ok: unresolvedChannels === 0,
checkedChannels: 0,
unresolvedChannels,
channels: [],
elapsedMs: 0
};
return {
...await auditDiscordChannelPermissions({
cfg,
token: botToken,
accountId: account.accountId,
channelIds,
timeoutMs
}),
unresolvedChannels
};
},
resolveAccountSnapshot: ({ account, runtime, probe, audit }) => {
const configured = resolveConfiguredFromCredentialStatuses(account) ?? Boolean(account.token?.trim());
const app = runtime?.application ?? probe?.application;
const bot = runtime?.bot ?? probe?.bot;
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
extra: {
...projectCredentialSnapshotFields(account),
connected: runtime?.connected ?? false,
reconnectAttempts: runtime?.reconnectAttempts,
lastConnectedAt: runtime?.lastConnectedAt ?? null,
lastDisconnect: runtime?.lastDisconnect ?? null,
lastEventAt: runtime?.lastEventAt ?? null,
application: app ?? void 0,
bot: bot ?? void 0,
audit
}
};
}
}),
gateway: { startAccount: async (ctx) => {
const account = ctx.account;
if (account.tokenStatus === "configured_unavailable") throw new Error(`Discord bot token configured for account "${account.accountId}" is unavailable; resolve SecretRefs against the active runtime snapshot before using this account.`);
const startupDelayMs = resolveDiscordStartupDelayMs(ctx.cfg, account.accountId);
if (startupDelayMs > 0) {
ctx.log?.info(`[${account.accountId}] delaying provider startup ${Math.round(startupDelayMs / 1e3)}s to reduce Discord startup rate limits`);
try {
await sleepWithAbort(startupDelayMs, ctx.abortSignal);
} catch {
return;
}
}
const token = account.token.trim();
startDiscordStartupProbe({
accountId: account.accountId,
token,
abortSignal: ctx.abortSignal,
setStatus: ctx.setStatus,
log: ctx.log
});
ctx.log?.info(`[${account.accountId}] starting provider`);
return (await loadDiscordProviderRuntime()).monitorDiscordProvider({
token,
accountId: account.accountId,
config: ctx.cfg,
runtime: ctx.runtime,
channelRuntime: ctx.channelRuntime,
abortSignal: ctx.abortSignal,
mediaMaxMb: account.config.mediaMaxMb,
historyLimit: account.config.historyLimit,
setStatus: (patch) => ctx.setStatus({
accountId: account.accountId,
...patch
})
});
} }
},
pairing: { text: {
idLabel: "discordUserId",
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: createPairingPrefixStripper(/^(discord|user):/i),
notify: async ({ cfg, id, message, accountId }) => {
await (await loadDiscordSendModule()).sendMessageDiscord(`user:${id}`, message, {
cfg,
...accountId ? { accountId } : {}
});
}
} },
security: discordSecurityAdapter,
threading: { scopedAccountReplyToMode: {
resolveAccount: (cfg, accountId) => resolveDiscordAccount({
cfg,
accountId
}),
resolveReplyToMode: (account) => account.config.replyToMode,
fallback: "off"
} },
outbound: {
...discordOutbound,
preferFinalAssistantVisibleText: true,
shouldTreatDeliveredTextAsVisible: shouldTreatDiscordDeliveredTextAsVisible,
shouldSuppressLocalPayloadPrompt: ({ cfg, accountId, payload, hint }) => shouldSuppressLocalDiscordExecApprovalPrompt({
cfg,
accountId,
payload,
hint
})
}
});
//#endregion
export { resolveDiscordGroupToolPolicy as i, collectDiscordStatusIssues as n, resolveDiscordGroupRequireMention as r, discordPlugin as t };