@openclaw/discord 2026.3.2 → 2026.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/discord";
3
3
  import { discordPlugin } from "./src/channel.js";
4
4
  import { setDiscordRuntime } from "./src/runtime.js";
5
5
  import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/discord",
3
- "version": "2026.3.2",
3
+ "version": "2026.3.7",
4
4
  "description": "OpenClaw Discord channel plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -1,4 +1,4 @@
1
- import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk/discord";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import { discordPlugin } from "./channel.js";
4
4
  import { setDiscordRuntime } from "./runtime.js";
package/src/channel.ts CHANGED
@@ -1,5 +1,13 @@
1
+ import {
2
+ buildAccountScopedDmSecurityPolicy,
3
+ collectOpenProviderGroupPolicyWarnings,
4
+ collectOpenGroupPolicyConfiguredRouteWarnings,
5
+ createScopedAccountConfigAccessors,
6
+ formatAllowFromLowercase,
7
+ } from "openclaw/plugin-sdk/compat";
1
8
  import {
2
9
  applyAccountNameToChannelSection,
10
+ buildComputedAccountStatusSnapshot,
3
11
  buildChannelConfigSchema,
4
12
  buildTokenChannelStatusSummary,
5
13
  collectDiscordAuditChannelIds,
@@ -8,8 +16,8 @@ import {
8
16
  deleteAccountFromConfigSection,
9
17
  discordOnboardingAdapter,
10
18
  DiscordConfigSchema,
11
- formatPairingApproveHint,
12
19
  getChatChannelMeta,
20
+ inspectDiscordAccount,
13
21
  listDiscordAccountIds,
14
22
  listDiscordDirectoryGroupsFromConfig,
15
23
  listDiscordDirectoryPeersFromConfig,
@@ -19,17 +27,17 @@ import {
19
27
  normalizeDiscordMessagingTarget,
20
28
  normalizeDiscordOutboundTarget,
21
29
  PAIRING_APPROVED_MESSAGE,
30
+ projectCredentialSnapshotFields,
31
+ resolveConfiguredFromCredentialStatuses,
22
32
  resolveDiscordAccount,
23
33
  resolveDefaultDiscordAccountId,
24
34
  resolveDiscordGroupRequireMention,
25
35
  resolveDiscordGroupToolPolicy,
26
- resolveOpenProviderRuntimeGroupPolicy,
27
- resolveDefaultGroupPolicy,
28
36
  setAccountEnabledInConfigSection,
29
37
  type ChannelMessageActionAdapter,
30
38
  type ChannelPlugin,
31
39
  type ResolvedDiscordAccount,
32
- } from "openclaw/plugin-sdk";
40
+ } from "openclaw/plugin-sdk/discord";
33
41
  import { getDiscordRuntime } from "./runtime.js";
34
42
 
35
43
  const meta = getChatChannelMeta("discord");
@@ -48,6 +56,13 @@ const discordMessageActions: ChannelMessageActionAdapter = {
48
56
  },
49
57
  };
50
58
 
59
+ const discordConfigAccessors = createScopedAccountConfigAccessors({
60
+ resolveAccount: ({ cfg, accountId }) => resolveDiscordAccount({ cfg, accountId }),
61
+ resolveAllowFrom: (account: ResolvedDiscordAccount) => account.config.dm?.allowFrom,
62
+ formatAllowFrom: (allowFrom) => formatAllowFromLowercase({ allowFrom }),
63
+ resolveDefaultTo: (account: ResolvedDiscordAccount) => account.config.defaultTo,
64
+ });
65
+
51
66
  export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
52
67
  id: "discord",
53
68
  meta: {
@@ -80,6 +95,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
80
95
  config: {
81
96
  listAccountIds: (cfg) => listDiscordAccountIds(cfg),
82
97
  resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
98
+ inspectAccount: (cfg, accountId) => inspectDiscordAccount({ cfg, accountId }),
83
99
  defaultAccountId: (cfg) => resolveDefaultDiscordAccountId(cfg),
84
100
  setAccountEnabled: ({ cfg, accountId, enabled }) =>
85
101
  setAccountEnabledInConfigSection({
@@ -104,58 +120,49 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
104
120
  configured: Boolean(account.token?.trim()),
105
121
  tokenSource: account.tokenSource,
106
122
  }),
107
- resolveAllowFrom: ({ cfg, accountId }) =>
108
- (resolveDiscordAccount({ cfg, accountId }).config.dm?.allowFrom ?? []).map((entry) =>
109
- String(entry),
110
- ),
111
- formatAllowFrom: ({ allowFrom }) =>
112
- allowFrom
113
- .map((entry) => String(entry).trim())
114
- .filter(Boolean)
115
- .map((entry) => entry.toLowerCase()),
116
- resolveDefaultTo: ({ cfg, accountId }) =>
117
- resolveDiscordAccount({ cfg, accountId }).config.defaultTo?.trim() || undefined,
123
+ ...discordConfigAccessors,
118
124
  },
119
125
  security: {
120
126
  resolveDmPolicy: ({ cfg, accountId, account }) => {
121
- const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
122
- const useAccountPath = Boolean(cfg.channels?.discord?.accounts?.[resolvedAccountId]);
123
- const allowFromPath = useAccountPath
124
- ? `channels.discord.accounts.${resolvedAccountId}.dm.`
125
- : "channels.discord.dm.";
126
- return {
127
- policy: account.config.dm?.policy ?? "pairing",
127
+ return buildAccountScopedDmSecurityPolicy({
128
+ cfg,
129
+ channelKey: "discord",
130
+ accountId,
131
+ fallbackAccountId: account.accountId ?? DEFAULT_ACCOUNT_ID,
132
+ policy: account.config.dm?.policy,
128
133
  allowFrom: account.config.dm?.allowFrom ?? [],
129
- allowFromPath,
130
- approveHint: formatPairingApproveHint("discord"),
134
+ allowFromPathSuffix: "dm.",
131
135
  normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
132
- };
136
+ });
133
137
  },
134
138
  collectWarnings: ({ account, cfg }) => {
135
- const warnings: string[] = [];
136
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
137
- const { groupPolicy } = resolveOpenProviderRuntimeGroupPolicy({
138
- providerConfigPresent: cfg.channels?.discord !== undefined,
139
- groupPolicy: account.config.groupPolicy,
140
- defaultGroupPolicy,
141
- });
142
139
  const guildEntries = account.config.guilds ?? {};
143
140
  const guildsConfigured = Object.keys(guildEntries).length > 0;
144
141
  const channelAllowlistConfigured = guildsConfigured;
145
142
 
146
- if (groupPolicy === "open") {
147
- if (channelAllowlistConfigured) {
148
- warnings.push(
149
- `- Discord guilds: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels.`,
150
- );
151
- } else {
152
- warnings.push(
153
- `- Discord guilds: groupPolicy="open" with no guild/channel allowlist; any channel can trigger (mention-gated). Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels.`,
154
- );
155
- }
156
- }
157
-
158
- return warnings;
143
+ return collectOpenProviderGroupPolicyWarnings({
144
+ cfg,
145
+ providerConfigPresent: cfg.channels?.discord !== undefined,
146
+ configuredGroupPolicy: account.config.groupPolicy,
147
+ collect: (groupPolicy) =>
148
+ collectOpenGroupPolicyConfiguredRouteWarnings({
149
+ groupPolicy,
150
+ routeAllowlistConfigured: channelAllowlistConfigured,
151
+ configureRouteAllowlist: {
152
+ surface: "Discord guilds",
153
+ openScope: "any channel not explicitly denied",
154
+ groupPolicyPath: "channels.discord.groupPolicy",
155
+ routeAllowlistPath: "channels.discord.guilds.<id>.channels",
156
+ },
157
+ missingRouteAllowlist: {
158
+ surface: "Discord guilds",
159
+ openBehavior:
160
+ "with no guild/channel allowlist; any channel can trigger (mention-gated)",
161
+ remediation:
162
+ 'Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels',
163
+ },
164
+ }),
165
+ });
159
166
  },
160
167
  },
161
168
  groups: {
@@ -302,10 +309,11 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
302
309
  textChunkLimit: 2000,
303
310
  pollMaxOptions: 10,
304
311
  resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
305
- sendText: async ({ to, text, accountId, deps, replyToId, silent }) => {
312
+ sendText: async ({ cfg, to, text, accountId, deps, replyToId, silent }) => {
306
313
  const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
307
314
  const result = await send(to, text, {
308
315
  verbose: false,
316
+ cfg,
309
317
  replyTo: replyToId ?? undefined,
310
318
  accountId: accountId ?? undefined,
311
319
  silent: silent ?? undefined,
@@ -313,6 +321,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
313
321
  return { channel: "discord", ...result };
314
322
  },
315
323
  sendMedia: async ({
324
+ cfg,
316
325
  to,
317
326
  text,
318
327
  mediaUrl,
@@ -325,6 +334,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
325
334
  const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
326
335
  const result = await send(to, text, {
327
336
  verbose: false,
337
+ cfg,
328
338
  mediaUrl,
329
339
  mediaLocalRoots,
330
340
  replyTo: replyToId ?? undefined,
@@ -333,8 +343,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
333
343
  });
334
344
  return { channel: "discord", ...result };
335
345
  },
336
- sendPoll: async ({ to, poll, accountId, silent }) =>
346
+ sendPoll: async ({ cfg, to, poll, accountId, silent }) =>
337
347
  await getDiscordRuntime().channel.discord.sendPollDiscord(to, poll, {
348
+ cfg,
338
349
  accountId: accountId ?? undefined,
339
350
  silent: silent ?? undefined,
340
351
  }),
@@ -386,19 +397,21 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
386
397
  return { ...audit, unresolvedChannels };
387
398
  },
388
399
  buildAccountSnapshot: ({ account, runtime, probe, audit }) => {
389
- const configured = Boolean(account.token?.trim());
400
+ const configured =
401
+ resolveConfiguredFromCredentialStatuses(account) ?? Boolean(account.token?.trim());
390
402
  const app = runtime?.application ?? (probe as { application?: unknown })?.application;
391
403
  const bot = runtime?.bot ?? (probe as { bot?: unknown })?.bot;
392
- return {
404
+ const base = buildComputedAccountStatusSnapshot({
393
405
  accountId: account.accountId,
394
406
  name: account.name,
395
407
  enabled: account.enabled,
396
408
  configured,
397
- tokenSource: account.tokenSource,
398
- running: runtime?.running ?? false,
399
- lastStartAt: runtime?.lastStartAt ?? null,
400
- lastStopAt: runtime?.lastStopAt ?? null,
401
- lastError: runtime?.lastError ?? null,
409
+ runtime,
410
+ probe,
411
+ });
412
+ return {
413
+ ...base,
414
+ ...projectCredentialSnapshotFields(account),
402
415
  connected: runtime?.connected ?? false,
403
416
  reconnectAttempts: runtime?.reconnectAttempts,
404
417
  lastConnectedAt: runtime?.lastConnectedAt ?? null,
@@ -406,10 +419,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
406
419
  lastEventAt: runtime?.lastEventAt ?? null,
407
420
  application: app ?? undefined,
408
421
  bot: bot ?? undefined,
409
- probe,
410
422
  audit,
411
- lastInboundAt: runtime?.lastInboundAt ?? null,
412
- lastOutboundAt: runtime?.lastOutboundAt ?? null,
413
423
  };
414
424
  },
415
425
  },
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "openclaw/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/discord";
2
2
 
3
3
  let runtime: PluginRuntime | null = null;
4
4
 
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
3
  import { registerDiscordSubagentHooks } from "./subagent-hooks.js";
4
4
 
@@ -35,7 +35,7 @@ const hookMocks = vi.hoisted(() => ({
35
35
  unbindThreadBindingsBySessionKey: vi.fn(() => []),
36
36
  }));
37
37
 
38
- vi.mock("openclaw/plugin-sdk", () => ({
38
+ vi.mock("openclaw/plugin-sdk/discord", () => ({
39
39
  resolveDiscordAccount: hookMocks.resolveDiscordAccount,
40
40
  autoBindSpawnedDiscordSubagent: hookMocks.autoBindSpawnedDiscordSubagent,
41
41
  listThreadBindingsBySessionKey: hookMocks.listThreadBindingsBySessionKey,
@@ -1,10 +1,10 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
2
2
  import {
3
3
  autoBindSpawnedDiscordSubagent,
4
4
  listThreadBindingsBySessionKey,
5
5
  resolveDiscordAccount,
6
6
  unbindThreadBindingsBySessionKey,
7
- } from "openclaw/plugin-sdk";
7
+ } from "openclaw/plugin-sdk/discord";
8
8
 
9
9
  function summarizeError(err: unknown): string {
10
10
  if (err instanceof Error) {