@openclaw/irc 2026.8.1-beta.2 → 2026.9.1-beta.1

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/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { _ as resolveDefaultIrcAccountId, g as listIrcAccountIds, h as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveIrcAccount } from "./channel-Bfg9kJRw.js";
1
+ import { _ as resolveDefaultIrcAccountId, g as listIrcAccountIds, h as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveIrcAccount } from "./channel-W4DLS5ip.js";
2
2
  import { n as setIrcRuntime } from "./runtime-xy-FcjJC.js";
3
3
  export { ircPlugin, ircSetupAdapter, ircSetupWizard, listEnabledIrcAccounts, listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount, setIrcRuntime };
@@ -1,11 +1,11 @@
1
- import { t as IrcChannelConfigSchema } from "./config-schema-DdB_h5mL.js";
1
+ import { t as IrcChannelConfigSchema } from "./config-schema-BDXGjxOr.js";
2
2
  import { t as getIrcRuntime } from "./runtime-xy-FcjJC.js";
3
3
  import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-D_DC3YWj.js";
4
4
  import { createAccountListHelpers, describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
5
5
  import { formatNormalizedAllowFromEntries } from "openclaw/plugin-sdk/allow-from";
6
6
  import { adaptScopedAccountAccessor, createScopedChannelConfigAdapter, createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers";
7
7
  import { buildChannelOutboundSessionRoute, createChatChannelPlugin, parseOptionalDelimitedEntries } from "openclaw/plugin-sdk/channel-core";
8
- import { buildMutableAllowEntryDetector, collectStandardAllowlistLists, composeAccountWarningCollectors, createAllowlistProviderOpenWarningCollector, createDangerousNameMatchingMutableAllowlistWarningCollector, resolveScopeKeyCaseInsensitive, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
8
+ import { buildMutableAllowEntryDetector, collectStandardAllowlistLists, createAllowlistProviderOpenWarningCollector, createConditionalWarningCollector, createDangerousNameMatchingMutableAllowlistWarningCollector, resolveScopeKeyCaseInsensitive, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
9
9
  import { createChannelDirectoryAdapter, createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime";
10
10
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
11
11
  import { buildBaseChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
@@ -14,17 +14,19 @@ import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2, normali
14
14
  import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
15
15
  import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
16
16
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
17
- import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
17
+ import { resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
18
18
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, normalizeStringifiedOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
19
- import { createAccountStatusSink, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, runPassiveAccountLifecycle, sanitizeForPlainText } from "openclaw/plugin-sdk/channel-outbound";
19
+ import { createAccountStatusSink, createMessageReceiptFromOutboundResults, createReplyToFanout, defineChannelMessageAdapter, runPassiveAccountLifecycle, sanitizeForPlainText } from "openclaw/plugin-sdk/channel-outbound";
20
20
  import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";
21
21
  import { chunkTextForOutbound, convertMarkdownTables, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
22
+ import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
22
23
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
23
24
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
24
25
  import net from "node:net";
25
26
  import tls from "node:tls";
26
27
  import { withTimeout } from "openclaw/plugin-sdk/security-runtime";
27
28
  import { randomUUID } from "node:crypto";
29
+ import { assertSecretOwnerAvailable } from "openclaw/plugin-sdk/channel-secret-owner-runtime";
28
30
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
29
31
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
30
32
  import { applyAccountNameToChannelSection, createAllowFromSection, createPromptParsedAllowFromForAccount, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, createTopLevelChannelAllowFromSetter, createTopLevelChannelDmPolicySetter, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
@@ -42,6 +44,16 @@ const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefau
42
44
  hasImplicitDefaultAccount: (cfg) => Boolean((cfg.channels?.irc?.host?.trim() || process.env.IRC_HOST?.trim()) && (cfg.channels?.irc?.nick?.trim() || process.env.IRC_NICK?.trim()))
43
45
  });
44
46
  function resolvePassword(accountId, merged) {
47
+ const configPassword = resolveSecretInputString({
48
+ value: merged.password,
49
+ path: `channels.irc.accounts.${accountId}.password`,
50
+ mode: "inspect"
51
+ });
52
+ if (configPassword.status === "configured_unavailable") return {
53
+ password: "",
54
+ source: "config",
55
+ unavailable: true
56
+ };
45
57
  if (accountId === DEFAULT_ACCOUNT_ID) {
46
58
  const envPassword = process.env.IRC_PASSWORD?.trim();
47
59
  if (envPassword) return {
@@ -51,7 +63,7 @@ function resolvePassword(accountId, merged) {
51
63
  }
52
64
  if (merged.passwordFile?.trim()) {
53
65
  let diagnostic;
54
- const filePassword = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
66
+ const password = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
55
67
  rejectSymlink: true,
56
68
  credentialDiagnostic: {
57
69
  configPath: `channels.irc.accounts.${accountId}.passwordFile`,
@@ -60,8 +72,8 @@ function resolvePassword(accountId, merged) {
60
72
  }
61
73
  }
62
74
  });
63
- if (filePassword) return {
64
- password: filePassword,
75
+ if (password) return {
76
+ password,
65
77
  source: "passwordFile"
66
78
  };
67
79
  return {
@@ -70,12 +82,8 @@ function resolvePassword(accountId, merged) {
70
82
  diagnostic
71
83
  };
72
84
  }
73
- const configPassword = normalizeResolvedSecretInputString({
74
- value: merged.password,
75
- path: `channels.irc.accounts.${accountId}.password`
76
- });
77
- if (configPassword) return {
78
- password: configPassword,
85
+ if (configPassword.status === "available") return {
86
+ password: configPassword.value,
79
87
  source: "config"
80
88
  };
81
89
  return {
@@ -85,23 +93,29 @@ function resolvePassword(accountId, merged) {
85
93
  }
86
94
  function resolveNickServConfig(accountId, nickserv) {
87
95
  const base = nickserv ?? {};
96
+ const configPassword = resolveSecretInputString({
97
+ value: base.password,
98
+ path: `channels.irc.accounts.${accountId}.nickserv.password`,
99
+ mode: "inspect"
100
+ });
101
+ const unavailable = Boolean(configPassword.ref);
88
102
  const envPassword = accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_PASSWORD?.trim() : void 0;
89
103
  const envRegisterEmail = accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_REGISTER_EMAIL?.trim() : void 0;
90
104
  const passwordFile = base.passwordFile?.trim();
91
- let resolvedPassword = normalizeResolvedSecretInputString({
92
- value: base.password,
93
- path: `channels.irc.accounts.${accountId}.nickserv.password`
94
- }) || envPassword || "";
105
+ let resolvedPassword;
95
106
  let diagnostic;
96
- if (!resolvedPassword && passwordFile) resolvedPassword = tryReadSecretFileSync(passwordFile, "IRC NickServ password file", {
97
- rejectSymlink: true,
98
- credentialDiagnostic: {
99
- configPath: `channels.irc.accounts.${accountId}.nickserv.passwordFile`,
100
- report: (value) => {
101
- diagnostic = value;
107
+ if (!unavailable) {
108
+ resolvedPassword = configPassword.value || envPassword;
109
+ if (!resolvedPassword && passwordFile) resolvedPassword = tryReadSecretFileSync(passwordFile, "IRC NickServ password file", {
110
+ rejectSymlink: true,
111
+ credentialDiagnostic: {
112
+ configPath: `channels.irc.accounts.${accountId}.nickserv.passwordFile`,
113
+ report: (value) => {
114
+ diagnostic = value;
115
+ }
102
116
  }
103
- }
104
- }) ?? "";
117
+ });
118
+ }
105
119
  return {
106
120
  config: {
107
121
  ...base,
@@ -110,7 +124,8 @@ function resolveNickServConfig(accountId, nickserv) {
110
124
  password: resolvedPassword || void 0,
111
125
  registerEmail: base.registerEmail?.trim() || envRegisterEmail || void 0
112
126
  },
113
- diagnostic
127
+ diagnostic,
128
+ unavailable: base.enabled !== false && unavailable
114
129
  };
115
130
  }
116
131
  function resolveIrcAccount(params) {
@@ -154,7 +169,7 @@ function resolveIrcAccount(params) {
154
169
  realname,
155
170
  password: passwordResolution.password,
156
171
  passwordSource: passwordResolution.source,
157
- tokenStatus: diagnostics.length > 0 ? "configured_unavailable" : passwordResolution.password || nickservResolution.config.password ? "available" : "missing",
172
+ tokenStatus: diagnostics.length > 0 || passwordResolution.unavailable || nickservResolution.unavailable ? "configured_unavailable" : passwordResolution.password || nickservResolution.config.password ? "available" : "missing",
158
173
  ...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
159
174
  config
160
175
  };
@@ -183,7 +198,7 @@ const collectIrcMutableAllowlistWarnings = createDangerousNameMatchingMutableAll
183
198
  });
184
199
  //#endregion
185
200
  //#region extensions/irc/src/gateway.ts
186
- const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-kWMtRD4g.js"));
201
+ const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-CA62Mbrh.js"));
187
202
  async function startIrcGatewayAccount(ctx) {
188
203
  const account = ctx.account;
189
204
  const statusSink = createAccountStatusSink({
@@ -208,6 +223,18 @@ async function startIrcGatewayAccount(ctx) {
208
223
  });
209
224
  }
210
225
  //#endregion
226
+ //#region extensions/irc/src/outbound-base.ts
227
+ function sanitizeIrcAssistantText(text) {
228
+ return sanitizeForPlainText(sanitizeAssistantVisibleText(text));
229
+ }
230
+ const ircOutboundBaseAdapter = {
231
+ deliveryMode: "direct",
232
+ chunker: chunkTextForOutbound,
233
+ chunkerMode: "markdown",
234
+ textChunkLimit: 350,
235
+ sanitizeText: ({ text }) => sanitizeIrcAssistantText(text)
236
+ };
237
+ //#endregion
211
238
  //#region extensions/irc/src/control-chars.ts
212
239
  function isIrcControlChar(charCode) {
213
240
  return charCode <= 31 || charCode === 127;
@@ -613,6 +640,8 @@ async function connectIrcClient(options) {
613
640
  //#endregion
614
641
  //#region extensions/irc/src/connect-options.ts
615
642
  function buildIrcConnectOptions(account, overrides = {}) {
643
+ assertSecretOwnerAvailable("account", `irc:${account.accountId}`);
644
+ if (account.tokenStatus === "configured_unavailable") throw new Error(`IRC credentials for account "${account.accountId}" are configured but unavailable.`);
616
645
  return {
617
646
  host: account.host,
618
647
  port: account.port,
@@ -713,7 +742,7 @@ function resolveTarget(to, opts) {
713
742
  if (fromOpt) return fromOpt;
714
743
  throw new Error(`Invalid IRC target: ${to}`);
715
744
  }
716
- async function sendMessageIrc(to, text, opts) {
745
+ async function sendIrcMessages(to, messages, opts, onDeliveryResult) {
717
746
  const cfg = requireRuntimeConfig(opts.cfg, "IRC send");
718
747
  const account = resolveIrcAccount({
719
748
  cfg,
@@ -726,35 +755,108 @@ async function sendMessageIrc(to, text, opts) {
726
755
  channel: "irc",
727
756
  accountId: account.accountId
728
757
  });
729
- const prepared = stripMarkdown(convertMarkdownTables(text.trim(), tableMode));
730
- if (!prepared.trim()) throw new Error("Message must be non-empty for IRC sends");
731
- const payload = opts.replyTo ? `${prepared}\n\n[reply:${opts.replyTo}]` : prepared;
732
- const client = opts.client;
733
- if (client?.isReady()) client.sendPrivmsg(target, payload);
734
- else {
735
- const transient = await connectIrcClient(buildIrcConnectOptions(account, { connectTimeoutMs: 12e3 }));
736
- if (target.startsWith("#") || target.startsWith("&")) transient.join(target);
737
- transient.sendPrivmsg(target, payload);
738
- transient.quit("sent");
739
- }
740
- recordIrcOutboundActivity(account.accountId);
741
- const messageId = makeIrcMessageId();
742
- return {
743
- messageId,
744
- target,
745
- receipt: createMessageReceiptFromOutboundResults({
746
- results: [{
747
- channel: "irc",
758
+ const preparedMessages = messages.map((message) => {
759
+ const prepared = stripMarkdown(convertMarkdownTables(message.text.trim(), tableMode));
760
+ if (!prepared.trim()) throw new Error("Message must be non-empty for IRC sends");
761
+ return {
762
+ payload: message.replyTo ? `${prepared}\n\n[reply:${message.replyTo}]` : prepared,
763
+ replyTo: message.replyTo
764
+ };
765
+ });
766
+ if (preparedMessages.length === 0) return [];
767
+ opts.abortSignal?.throwIfAborted();
768
+ let transient;
769
+ const client = opts.client?.isReady() ? opts.client : transient = await connectIrcClient(buildIrcConnectOptions(account, {
770
+ connectTimeoutMs: 12e3,
771
+ abortSignal: opts.abortSignal
772
+ }));
773
+ const results = [];
774
+ try {
775
+ opts.abortSignal?.throwIfAborted();
776
+ if (transient && (target.startsWith("#") || target.startsWith("&"))) client.join(target);
777
+ for (const message of preparedMessages) {
778
+ opts.abortSignal?.throwIfAborted();
779
+ if (!client.isReady()) throw new Error("IRC connection closed before send");
780
+ await opts.onPlatformSendDispatch?.();
781
+ opts.abortSignal?.throwIfAborted();
782
+ if (!client.isReady()) throw new Error("IRC connection closed before send");
783
+ client.sendPrivmsg(target, message.payload);
784
+ recordIrcOutboundActivity(account.accountId);
785
+ const messageId = makeIrcMessageId();
786
+ const result = {
748
787
  messageId,
749
- conversationId: target
750
- }],
751
- kind: "text",
752
- ...opts.replyTo ? { replyToId: opts.replyTo } : {}
753
- })
754
- };
788
+ target,
789
+ receipt: createMessageReceiptFromOutboundResults({
790
+ results: [{
791
+ channel: "irc",
792
+ messageId,
793
+ conversationId: target
794
+ }],
795
+ kind: "text",
796
+ ...message.replyTo ? { replyToId: message.replyTo } : {}
797
+ })
798
+ };
799
+ results.push(result);
800
+ await onDeliveryResult?.(result);
801
+ }
802
+ return results;
803
+ } finally {
804
+ transient?.quit("sent");
805
+ }
806
+ }
807
+ async function sendMessageIrc(to, text, opts) {
808
+ const result = (await sendIrcMessages(to, [{
809
+ text,
810
+ ...opts.replyTo ? { replyTo: opts.replyTo } : {}
811
+ }], opts))[0];
812
+ if (!result) throw new Error("Message must be non-empty for IRC sends");
813
+ return result;
755
814
  }
756
815
  //#endregion
757
816
  //#region extensions/irc/src/message-adapter.ts
817
+ function toIrcMessageResult({ target, ...result }) {
818
+ return {
819
+ ...result,
820
+ target: {
821
+ kind: "conversation",
822
+ id: target
823
+ }
824
+ };
825
+ }
826
+ async function sendIrcMessage(...args) {
827
+ return toIrcMessageResult(await sendMessageIrc(...args));
828
+ }
829
+ const sendFormattedIrcText = async (ctx) => {
830
+ const { chunkMarkdownTextWithMode, resolveChunkMode, resolveTextChunkLimit } = await import("openclaw/plugin-sdk/reply-chunking");
831
+ const accountId = ctx.accountId ?? void 0;
832
+ const textLimit = ctx.formatting?.textLimit ?? resolveTextChunkLimit(ctx.cfg, "irc", accountId, { fallbackLimit: ircOutboundBaseAdapter.textChunkLimit });
833
+ const chunkMode = ctx.formatting?.chunkMode ?? resolveChunkMode(ctx.cfg, "irc", accountId);
834
+ const chunkText = (text) => ctx.formatting ? ircOutboundBaseAdapter.chunker(text, textLimit, { formatting: ctx.formatting }) : ircOutboundBaseAdapter.chunker(text, textLimit);
835
+ let chunks;
836
+ if (chunkMode === "newline") {
837
+ const blocks = chunkMarkdownTextWithMode(ctx.text, textLimit, chunkMode);
838
+ if (blocks.length === 0 && ctx.text) blocks.push(ctx.text);
839
+ chunks = blocks.flatMap((block) => {
840
+ const blockChunks = chunkText(block);
841
+ return blockChunks.length === 0 && block ? [block] : blockChunks;
842
+ });
843
+ } else chunks = chunkText(ctx.text);
844
+ const nextReplyToId = createReplyToFanout(ctx);
845
+ return (await sendIrcMessages(ctx.to, chunks.map((text) => {
846
+ const replyTo = nextReplyToId();
847
+ return replyTo ? {
848
+ text,
849
+ replyTo
850
+ } : { text };
851
+ }), {
852
+ cfg: ctx.cfg,
853
+ accountId,
854
+ abortSignal: ctx.abortSignal,
855
+ onPlatformSendDispatch: ctx.onPlatformSendDispatch
856
+ }, async (result) => {
857
+ await ctx.onDeliveryResult?.(attachChannelToResult("irc", toIrcMessageResult(result)));
858
+ })).map((result) => attachChannelToResult("irc", toIrcMessageResult(result)));
859
+ };
758
860
  const ircMessageAdapter = defineChannelMessageAdapter({
759
861
  id: "irc",
760
862
  durableFinal: { capabilities: {
@@ -763,12 +865,12 @@ const ircMessageAdapter = defineChannelMessageAdapter({
763
865
  replyTo: true
764
866
  } },
765
867
  send: {
766
- text: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageIrc(to, text, {
868
+ text: async ({ cfg, to, text, accountId, replyToId }) => await sendIrcMessage(to, text, {
767
869
  cfg,
768
870
  accountId: accountId ?? void 0,
769
871
  replyTo: replyToId ?? void 0
770
872
  }),
771
- media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageIrc(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
873
+ media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendIrcMessage(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
772
874
  cfg,
773
875
  accountId: accountId ?? void 0,
774
876
  replyTo: replyToId ?? void 0
@@ -776,18 +878,6 @@ const ircMessageAdapter = defineChannelMessageAdapter({
776
878
  }
777
879
  });
778
880
  //#endregion
779
- //#region extensions/irc/src/outbound-base.ts
780
- function sanitizeIrcAssistantText(text) {
781
- return sanitizeForPlainText(sanitizeAssistantVisibleText(text));
782
- }
783
- const ircOutboundBaseAdapter = {
784
- deliveryMode: "direct",
785
- chunker: chunkTextForOutbound,
786
- chunkerMode: "markdown",
787
- textChunkLimit: 350,
788
- sanitizeText: ({ text }) => sanitizeIrcAssistantText(text)
789
- };
790
- //#endregion
791
881
  //#region extensions/irc/src/policy.ts
792
882
  function resolveIrcGroupScope(params) {
793
883
  const { "*": wildcard, ...groups } = params.groups ?? {};
@@ -1399,7 +1489,7 @@ const meta = {
1399
1489
  systemImage: "number",
1400
1490
  markdownCapable: true
1401
1491
  };
1402
- const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-kWMtRD4g.js"));
1492
+ const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-CA62Mbrh.js"));
1403
1493
  function normalizePairingTarget(raw) {
1404
1494
  const normalized = normalizeIrcAllowEntry(raw);
1405
1495
  if (!normalized) return "";
@@ -1455,7 +1545,7 @@ const resolveIrcDmPolicy = createScopedDmSecurityResolver({
1455
1545
  policyPathSuffix: "dmPolicy",
1456
1546
  normalizeEntry: (raw) => normalizeIrcAllowEntry(raw)
1457
1547
  });
1458
- const collectIrcSecurityWarnings = composeAccountWarningCollectors(createAllowlistProviderOpenWarningCollector({
1548
+ const collectIrcGroupPolicyWarnings = createAllowlistProviderOpenWarningCollector({
1459
1549
  providerConfigPresent: (cfg) => cfg.channels?.irc !== void 0,
1460
1550
  resolveGroupPolicy: (account) => account.config.groupPolicy,
1461
1551
  buildOpenWarning: {
@@ -1463,7 +1553,19 @@ const collectIrcSecurityWarnings = composeAccountWarningCollectors(createAllowli
1463
1553
  openBehavior: "allows all channels and senders (mention-gated)",
1464
1554
  remediation: "Prefer channels.irc.groupPolicy=\"allowlist\" with channels.irc.groups"
1465
1555
  }
1466
- }), (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.");
1556
+ });
1557
+ const collectIrcOpenGroupFindings = createConditionalWarningCollector.findings({
1558
+ collectWarnings: collectIrcGroupPolicyWarnings,
1559
+ checkId: "channels.irc.groups.open",
1560
+ severity: "critical",
1561
+ title: "IRC security warning"
1562
+ });
1563
+ const collectIrcSecurityWarnings = (params) => [
1564
+ ...collectIrcOpenGroupFindings(params),
1565
+ ...!params.account.config.tls ? ["- IRC TLS is disabled (channels.irc.tls=false); traffic and credentials are plaintext."] : [],
1566
+ ...params.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."] : [],
1567
+ ...params.account.config.nickserv?.register && !params.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."] : []
1568
+ ];
1467
1569
  const ircPlugin = createChatChannelPlugin({
1468
1570
  base: {
1469
1571
  id: "irc",
@@ -1630,7 +1732,10 @@ const ircPlugin = createChatChannelPlugin({
1630
1732
  collectWarnings: collectIrcSecurityWarnings
1631
1733
  },
1632
1734
  outbound: {
1633
- base: ircOutboundBaseAdapter,
1735
+ base: {
1736
+ ...ircOutboundBaseAdapter,
1737
+ sendFormattedText: sendFormattedIrcText
1738
+ },
1634
1739
  attachedResults: {
1635
1740
  channel: "irc",
1636
1741
  sendText: ({ onDeliveryResult: _onDeliveryResult, ...ctx }) => ircMessageAdapter.send.text(ctx),
@@ -1642,4 +1747,4 @@ const ircPlugin = createChatChannelPlugin({
1642
1747
  }
1643
1748
  });
1644
1749
  //#endregion
1645
- export { resolveDefaultIrcAccountId as _, resolveIrcGroupRequireMention as a, buildIrcAllowlistCandidates as c, buildIrcConnectOptions as d, connectIrcClient as f, listIrcAccountIds as g, listEnabledIrcAccounts as h, resolveIrcGroupMatch as i, isChannelTarget as l, parseIrcPrefix as m, ircSetupWizard as n, sanitizeIrcAssistantText as o, parseIrcLine as p, ircSetupAdapter as r, sendMessageIrc as s, ircPlugin as t, normalizeIrcAllowEntry as u, resolveIrcAccount as v };
1750
+ export { resolveDefaultIrcAccountId as _, resolveIrcGroupRequireMention as a, isChannelTarget as c, connectIrcClient as d, parseIrcLine as f, listIrcAccountIds as g, listEnabledIrcAccounts as h, resolveIrcGroupMatch as i, normalizeIrcAllowEntry as l, sanitizeIrcAssistantText as m, ircSetupWizard as n, sendMessageIrc as o, parseIrcPrefix as p, ircSetupAdapter as r, buildIrcAllowlistCandidates as s, ircPlugin as t, buildIrcConnectOptions as u, resolveIrcAccount as v };
@@ -1,2 +1,2 @@
1
- import { t as IrcChannelConfigSchema } from "./config-schema-DdB_h5mL.js";
1
+ import { t as IrcChannelConfigSchema } from "./config-schema-BDXGjxOr.js";
2
2
  export { IrcChannelConfigSchema };
@@ -1,2 +1,2 @@
1
- import { t as ircPlugin } from "./channel-Bfg9kJRw.js";
1
+ import { t as ircPlugin } from "./channel-W4DLS5ip.js";
2
2
  export { ircPlugin };
@@ -1,4 +1,4 @@
1
- import { a as resolveIrcGroupRequireMention, c as buildIrcAllowlistCandidates, d as buildIrcConnectOptions, f as connectIrcClient, i as resolveIrcGroupMatch, l as isChannelTarget, m as parseIrcPrefix, o as sanitizeIrcAssistantText, p as parseIrcLine, s as sendMessageIrc, u as normalizeIrcAllowEntry, v as resolveIrcAccount } from "./channel-Bfg9kJRw.js";
1
+ import { a as resolveIrcGroupRequireMention, c as isChannelTarget, d as connectIrcClient, f as parseIrcLine, i as resolveIrcGroupMatch, l as normalizeIrcAllowEntry, m as sanitizeIrcAssistantText, o as sendMessageIrc, p as parseIrcPrefix, s as buildIrcAllowlistCandidates, u as buildIrcConnectOptions, v as resolveIrcAccount } from "./channel-W4DLS5ip.js";
2
2
  import { t as getIrcRuntime } from "./runtime-xy-FcjJC.js";
3
3
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import { bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
@@ -455,14 +455,16 @@ function decodeIrcIngressPayload(payload, claimedId) {
455
455
  }
456
456
  function inspectIrcIngress(raw, phase) {
457
457
  try {
458
+ const projection = inspectRawPrivmsg(raw.body.rawLine);
459
+ if (phase === "claim") raw.claimedProjection = projection;
458
460
  return {
459
- eventId: raw.eventId,
460
- laneKey: inspectRawPrivmsg(raw.rawLine).laneKey
461
+ eventId: raw.body.eventId,
462
+ laneKey: projection.laneKey
461
463
  };
462
464
  } catch (error) {
463
465
  if (phase === "admission" && error instanceof IrcIngressPayloadError) return {
464
- eventId: raw.eventId,
465
- laneKey: `invalid:${raw.eventId}`
466
+ eventId: raw.body.eventId,
467
+ laneKey: `invalid:${raw.body.eventId}`
466
468
  };
467
469
  throw error;
468
470
  }
@@ -479,8 +481,8 @@ function createIrcIngressMonitor(options) {
479
481
  inspect: (raw, context) => inspectIrcIngress(raw, context.phase),
480
482
  payload: {
481
483
  version: IRC_INGRESS_PAYLOAD_VERSION,
482
- serialize: (raw) => raw,
483
- deserialize: (body) => body,
484
+ serialize: (raw) => raw.body,
485
+ deserialize: (body) => ({ body }),
484
486
  encode: ({ body }) => ({
485
487
  version: IRC_INGRESS_PAYLOAD_VERSION,
486
488
  ...body
@@ -490,13 +492,13 @@ function createIrcIngressMonitor(options) {
490
492
  },
491
493
  deliver: (raw, lifecycle, claim) => {
492
494
  const message = {
493
- ...inspectRawPrivmsg(raw.rawLine).message,
495
+ ...raw.claimedProjection.message,
494
496
  messageId: claim.id,
495
- timestamp: raw.receivedAt
497
+ timestamp: raw.body.receivedAt
496
498
  };
497
499
  return options.dispatch(message, lifecycle, {
498
- connectedNick: raw.connectedNick.trim(),
499
- connectionEpoch: raw.connectionEpoch.trim()
500
+ connectedNick: raw.body.connectedNick.trim(),
501
+ connectionEpoch: raw.body.connectionEpoch.trim()
500
502
  });
501
503
  },
502
504
  pollIntervalMs: options.pollIntervalMs ?? IRC_INGRESS_POLL_INTERVAL_MS,
@@ -526,13 +528,13 @@ function createIrcIngressMonitor(options) {
526
528
  const receivedAt = Date.now();
527
529
  const normalizedNick = connectedNick.trim();
528
530
  if (!normalizedNick) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress connected nickname is required."));
529
- return monitor.admit({
531
+ return monitor.admit({ body: {
530
532
  eventId,
531
533
  rawLine,
532
534
  receivedAt,
533
535
  connectionEpoch: epoch,
534
536
  connectedNick: normalizedNick
535
- }, { receivedAt }).then(() => void 0);
537
+ } }, { receivedAt }).then(() => void 0);
536
538
  }
537
539
  };
538
540
  },
@@ -1,5 +1,5 @@
1
1
  import { createChannelConfigUiHints } from "openclaw/plugin-sdk/channel-core";
2
- import { ChannelGroupEntrySchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, buildChannelConfigSchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
2
+ import { ChannelGroupEntrySchema, DmPolicySchema, GroupPolicySchema, MarkdownConfigSchema, ReplyRuntimeConfigSchemaShape, ReplyToModeSchema, buildChannelConfigSchema, buildMultiAccountChannelSchema, requireOpenAllowFrom } from "openclaw/plugin-sdk/channel-config-schema";
3
3
  import { z } from "zod";
4
4
  //#region extensions/irc/src/config-ui-hints.ts
5
5
  const ircChannelConfigUiHints = {
@@ -60,6 +60,7 @@ const IrcChannelConfigSchema = buildChannelConfigSchema(buildMultiAccountChannel
60
60
  name: z.string().optional(),
61
61
  enabled: z.boolean().optional(),
62
62
  configWrites: z.boolean().optional(),
63
+ replyToMode: ReplyToModeSchema.optional(),
63
64
  dangerouslyAllowNameMatching: z.boolean().optional(),
64
65
  host: z.string().optional(),
65
66
  port: z.number().int().min(1).max(65535).optional(),
@@ -29,6 +29,26 @@
29
29
  "configWrites": {
30
30
  "type": "boolean"
31
31
  },
32
+ "replyToMode": {
33
+ "anyOf": [
34
+ {
35
+ "type": "string",
36
+ "const": "off"
37
+ },
38
+ {
39
+ "type": "string",
40
+ "const": "first"
41
+ },
42
+ {
43
+ "type": "string",
44
+ "const": "all"
45
+ },
46
+ {
47
+ "type": "string",
48
+ "const": "batched"
49
+ }
50
+ ]
51
+ },
32
52
  "dangerouslyAllowNameMatching": {
33
53
  "type": "boolean"
34
54
  },
@@ -355,6 +375,26 @@
355
375
  "configWrites": {
356
376
  "type": "boolean"
357
377
  },
378
+ "replyToMode": {
379
+ "anyOf": [
380
+ {
381
+ "type": "string",
382
+ "const": "off"
383
+ },
384
+ {
385
+ "type": "string",
386
+ "const": "first"
387
+ },
388
+ {
389
+ "type": "string",
390
+ "const": "all"
391
+ },
392
+ {
393
+ "type": "string",
394
+ "const": "batched"
395
+ }
396
+ ]
397
+ },
358
398
  "dangerouslyAllowNameMatching": {
359
399
  "type": "boolean"
360
400
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/irc",
3
- "version": "2026.8.1-beta.2",
3
+ "version": "2026.9.1-beta.1",
4
4
  "description": "OpenClaw IRC channel plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -121,10 +121,10 @@
121
121
  }
122
122
  },
123
123
  "compat": {
124
- "pluginApi": ">=2026.8.1-beta.2"
124
+ "pluginApi": ">=2026.9.1-beta.1"
125
125
  },
126
126
  "build": {
127
- "openclawVersion": "2026.8.1-beta.2",
127
+ "openclawVersion": "2026.9.1-beta.1",
128
128
  "bundledDist": false
129
129
  },
130
130
  "release": {
@@ -149,7 +149,7 @@
149
149
  "README.md"
150
150
  ],
151
151
  "peerDependencies": {
152
- "openclaw": ">=2026.8.1-beta.2"
152
+ "openclaw": ">=2026.9.1-beta.1"
153
153
  },
154
154
  "peerDependenciesMeta": {
155
155
  "openclaw": {