@openclaw/irc 2026.8.1-beta.2 → 2026.8.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 listIrcAccountIds, g as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveDefaultIrcAccountId, y as resolveIrcAccount } from "./channel-BxbQEBnS.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,12 @@
1
- import { t as IrcChannelConfigSchema } from "./config-schema-DdB_h5mL.js";
1
+ import { t as IrcChannelConfigSchema } from "./config-schema-CpB6AaUh.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 { defineStableChannelIngressIdentity, identityEntryAuthenticationClassifier } from "openclaw/plugin-sdk/channel-ingress-runtime";
9
+ import { buildMutableAllowEntryDetector, collectStandardAllowlistLists, createAllowlistProviderOpenWarningCollector, createConditionalWarningCollector, createDangerousNameMatchingMutableAllowlistWarningCollector, resolveScopeKeyCaseInsensitive, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
9
10
  import { createChannelDirectoryAdapter, createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime";
10
11
  import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
11
12
  import { buildBaseChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
@@ -14,17 +15,19 @@ import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2, normali
14
15
  import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
15
16
  import { isTruthyEnvValue } from "openclaw/plugin-sdk/runtime-env";
16
17
  import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
17
- import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
18
+ import { resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
18
19
  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";
20
+ import { createAccountStatusSink, createMessageReceiptFromOutboundResults, createReplyToFanout, defineChannelMessageAdapter, runPassiveAccountLifecycle, sanitizeForPlainText } from "openclaw/plugin-sdk/channel-outbound";
20
21
  import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";
21
22
  import { chunkTextForOutbound, convertMarkdownTables, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
23
+ import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
22
24
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
23
25
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
24
26
  import net from "node:net";
25
27
  import tls from "node:tls";
26
28
  import { withTimeout } from "openclaw/plugin-sdk/security-runtime";
27
29
  import { randomUUID } from "node:crypto";
30
+ import { assertSecretOwnerAvailable } from "openclaw/plugin-sdk/channel-secret-owner-runtime";
28
31
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
29
32
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
30
33
  import { applyAccountNameToChannelSection, createAllowFromSection, createPromptParsedAllowFromForAccount, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, createTopLevelChannelAllowFromSetter, createTopLevelChannelDmPolicySetter, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
@@ -42,6 +45,16 @@ const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefau
42
45
  hasImplicitDefaultAccount: (cfg) => Boolean((cfg.channels?.irc?.host?.trim() || process.env.IRC_HOST?.trim()) && (cfg.channels?.irc?.nick?.trim() || process.env.IRC_NICK?.trim()))
43
46
  });
44
47
  function resolvePassword(accountId, merged) {
48
+ const configPassword = resolveSecretInputString({
49
+ value: merged.password,
50
+ path: `channels.irc.accounts.${accountId}.password`,
51
+ mode: "inspect"
52
+ });
53
+ if (configPassword.status === "configured_unavailable") return {
54
+ password: "",
55
+ source: "config",
56
+ unavailable: true
57
+ };
45
58
  if (accountId === DEFAULT_ACCOUNT_ID) {
46
59
  const envPassword = process.env.IRC_PASSWORD?.trim();
47
60
  if (envPassword) return {
@@ -51,7 +64,7 @@ function resolvePassword(accountId, merged) {
51
64
  }
52
65
  if (merged.passwordFile?.trim()) {
53
66
  let diagnostic;
54
- const filePassword = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
67
+ const password = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
55
68
  rejectSymlink: true,
56
69
  credentialDiagnostic: {
57
70
  configPath: `channels.irc.accounts.${accountId}.passwordFile`,
@@ -60,8 +73,8 @@ function resolvePassword(accountId, merged) {
60
73
  }
61
74
  }
62
75
  });
63
- if (filePassword) return {
64
- password: filePassword,
76
+ if (password) return {
77
+ password,
65
78
  source: "passwordFile"
66
79
  };
67
80
  return {
@@ -70,12 +83,8 @@ function resolvePassword(accountId, merged) {
70
83
  diagnostic
71
84
  };
72
85
  }
73
- const configPassword = normalizeResolvedSecretInputString({
74
- value: merged.password,
75
- path: `channels.irc.accounts.${accountId}.password`
76
- });
77
- if (configPassword) return {
78
- password: configPassword,
86
+ if (configPassword.status === "available") return {
87
+ password: configPassword.value,
79
88
  source: "config"
80
89
  };
81
90
  return {
@@ -85,23 +94,29 @@ function resolvePassword(accountId, merged) {
85
94
  }
86
95
  function resolveNickServConfig(accountId, nickserv) {
87
96
  const base = nickserv ?? {};
97
+ const configPassword = resolveSecretInputString({
98
+ value: base.password,
99
+ path: `channels.irc.accounts.${accountId}.nickserv.password`,
100
+ mode: "inspect"
101
+ });
102
+ const unavailable = Boolean(configPassword.ref);
88
103
  const envPassword = accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_PASSWORD?.trim() : void 0;
89
104
  const envRegisterEmail = accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_REGISTER_EMAIL?.trim() : void 0;
90
105
  const passwordFile = base.passwordFile?.trim();
91
- let resolvedPassword = normalizeResolvedSecretInputString({
92
- value: base.password,
93
- path: `channels.irc.accounts.${accountId}.nickserv.password`
94
- }) || envPassword || "";
106
+ let resolvedPassword;
95
107
  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;
108
+ if (!unavailable) {
109
+ resolvedPassword = configPassword.value || envPassword;
110
+ if (!resolvedPassword && passwordFile) resolvedPassword = tryReadSecretFileSync(passwordFile, "IRC NickServ password file", {
111
+ rejectSymlink: true,
112
+ credentialDiagnostic: {
113
+ configPath: `channels.irc.accounts.${accountId}.nickserv.passwordFile`,
114
+ report: (value) => {
115
+ diagnostic = value;
116
+ }
102
117
  }
103
- }
104
- }) ?? "";
118
+ });
119
+ }
105
120
  return {
106
121
  config: {
107
122
  ...base,
@@ -110,7 +125,8 @@ function resolveNickServConfig(accountId, nickserv) {
110
125
  password: resolvedPassword || void 0,
111
126
  registerEmail: base.registerEmail?.trim() || envRegisterEmail || void 0
112
127
  },
113
- diagnostic
128
+ diagnostic,
129
+ unavailable: base.enabled !== false && unavailable
114
130
  };
115
131
  }
116
132
  function resolveIrcAccount(params) {
@@ -154,7 +170,7 @@ function resolveIrcAccount(params) {
154
170
  realname,
155
171
  password: passwordResolution.password,
156
172
  passwordSource: passwordResolution.source,
157
- tokenStatus: diagnostics.length > 0 ? "configured_unavailable" : passwordResolution.password || nickservResolution.config.password ? "available" : "missing",
173
+ tokenStatus: diagnostics.length > 0 || passwordResolution.unavailable || nickservResolution.unavailable ? "configured_unavailable" : passwordResolution.password || nickservResolution.config.password ? "available" : "missing",
158
174
  ...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
159
175
  config
160
176
  };
@@ -183,7 +199,7 @@ const collectIrcMutableAllowlistWarnings = createDangerousNameMatchingMutableAll
183
199
  });
184
200
  //#endregion
185
201
  //#region extensions/irc/src/gateway.ts
186
- const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-kWMtRD4g.js"));
202
+ const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-DZcQfOqp.js"));
187
203
  async function startIrcGatewayAccount(ctx) {
188
204
  const account = ctx.account;
189
205
  const statusSink = createAccountStatusSink({
@@ -222,8 +238,158 @@ function stripIrcControlChars(value) {
222
238
  return out;
223
239
  }
224
240
  //#endregion
225
- //#region extensions/irc/src/protocol.ts
241
+ //#region extensions/irc/src/normalize.ts
226
242
  const IRC_TARGET_PATTERN$1 = /^[^\s:]+$/u;
243
+ function isChannelTarget(target) {
244
+ return target.startsWith("#") || target.startsWith("&");
245
+ }
246
+ function normalizeIrcMessagingTarget(raw) {
247
+ const trimmed = raw.trim();
248
+ if (!trimmed) return;
249
+ let target = trimmed;
250
+ if (normalizeLowercaseStringOrEmpty(target).startsWith("irc:")) target = target.slice(4).trim();
251
+ if (normalizeLowercaseStringOrEmpty(target).startsWith("channel:")) {
252
+ target = target.slice(8).trim();
253
+ if (!target.startsWith("#") && !target.startsWith("&")) target = `#${target}`;
254
+ }
255
+ if (normalizeLowercaseStringOrEmpty(target).startsWith("user:")) target = target.slice(5).trim();
256
+ if (!target || !looksLikeIrcTargetId(target)) return;
257
+ return target;
258
+ }
259
+ function resolveIrcOutboundSessionRoute(params) {
260
+ const target = normalizeIrcMessagingTarget(params.target);
261
+ if (!target) return null;
262
+ const chatType = isChannelTarget(target) ? "group" : "direct";
263
+ return buildChannelOutboundSessionRoute({
264
+ cfg: params.cfg,
265
+ agentId: params.agentId,
266
+ channel: "irc",
267
+ accountId: params.accountId,
268
+ recipientSessionExact: chatType === "direct" ? "direct-alias" : false,
269
+ peer: {
270
+ kind: chatType,
271
+ id: target
272
+ },
273
+ chatType,
274
+ from: `irc:${target}`,
275
+ to: target
276
+ });
277
+ }
278
+ function looksLikeIrcTargetId(raw) {
279
+ const trimmed = raw.trim();
280
+ if (!trimmed) return false;
281
+ if (hasIrcControlChars(trimmed)) return false;
282
+ return IRC_TARGET_PATTERN$1.test(trimmed);
283
+ }
284
+ function normalizeIrcAllowEntry(raw) {
285
+ let value = normalizeLowercaseStringOrEmpty(raw);
286
+ if (!value) return "";
287
+ if (value.startsWith("irc:")) value = value.slice(4);
288
+ if (value.startsWith("user:")) value = value.slice(5);
289
+ return value.trim();
290
+ }
291
+ function buildIrcAllowlistCandidates(message, params) {
292
+ const nick = normalizeLowercaseStringOrEmpty(message.senderNick);
293
+ const user = normalizeOptionalLowercaseString(message.senderUser);
294
+ const host = normalizeOptionalLowercaseString(message.senderHost);
295
+ const candidates = /* @__PURE__ */ new Set();
296
+ if (nick && params?.allowNameMatching === true) candidates.add(nick);
297
+ if (nick && user) candidates.add(`${nick}!${user}`);
298
+ if (nick && host) candidates.add(`${nick}@${host}`);
299
+ if (nick && user && host) candidates.add(`${nick}!${user}@${host}`);
300
+ return [...candidates];
301
+ }
302
+ const ircIngressIdentity = defineStableChannelIngressIdentity({
303
+ key: "irc-id",
304
+ authentication: "asserted",
305
+ normalizeEntry: normalizeIrcStableEntry,
306
+ normalizeSubject: normalizeLowercaseStringOrEmpty,
307
+ sensitivity: "pii",
308
+ aliases: [
309
+ {
310
+ key: "irc-id-nick-user",
311
+ kind: "stable-id",
312
+ normalizeEntry: normalizeIrcNickUserEntry,
313
+ normalizeSubject: normalizeLowercaseStringOrEmpty,
314
+ authentication: "mutable",
315
+ sensitivity: "pii"
316
+ },
317
+ {
318
+ key: "irc-id-nick-host",
319
+ kind: "stable-id",
320
+ authentication: "asserted",
321
+ normalizeEntry: normalizeIrcNickHostEntry,
322
+ normalizeSubject: normalizeLowercaseStringOrEmpty,
323
+ sensitivity: "pii"
324
+ },
325
+ {
326
+ key: "irc-nick",
327
+ kind: "plugin:irc-nick",
328
+ normalizeEntry: normalizeIrcNickEntry,
329
+ normalizeSubject: normalizeLowercaseStringOrEmpty,
330
+ authentication: "mutable",
331
+ sensitivity: "pii"
332
+ }
333
+ ],
334
+ isWildcardEntry: (entry) => normalizeIrcAllowEntry(entry) === "*",
335
+ resolveEntryId: ({ entryIndex, fieldKey }) => `irc-entry-${entryIndex + 1}:${fieldKey === "irc-nick" ? "nick" : "id"}`
336
+ });
337
+ function isBareNick(value) {
338
+ return !value.includes("!") && !value.includes("@");
339
+ }
340
+ function hasVerifiedHost(value) {
341
+ return value.includes("@");
342
+ }
343
+ function isHostlessNickUser(value) {
344
+ return value.includes("!") && !value.includes("@");
345
+ }
346
+ function normalizeIrcStableEntry(value) {
347
+ const normalized = normalizeIrcAllowEntry(value);
348
+ if (!normalized.includes("!") || !hasVerifiedHost(normalized)) return null;
349
+ return normalized;
350
+ }
351
+ function normalizeIrcNickHostEntry(value) {
352
+ const normalized = normalizeIrcAllowEntry(value);
353
+ return !normalized.includes("!") && hasVerifiedHost(normalized) ? normalized : null;
354
+ }
355
+ function normalizeIrcNickUserEntry(value) {
356
+ const normalized = normalizeIrcAllowEntry(value);
357
+ if (!normalized || normalized === "*" || !isHostlessNickUser(normalized)) return null;
358
+ return normalized;
359
+ }
360
+ function normalizeIrcNickEntry(value) {
361
+ const normalized = normalizeIrcAllowEntry(value);
362
+ if (!normalized || normalized === "*" || !isBareNick(normalized)) return null;
363
+ return normalized;
364
+ }
365
+ function createIrcIngressSubject(message) {
366
+ const candidates = buildIrcAllowlistCandidates(message, { allowNameMatching: true });
367
+ const stableCandidates = candidates.filter((candidate) => hasVerifiedHost(candidate));
368
+ const nick = normalizeLowercaseStringOrEmpty(message.senderNick);
369
+ return {
370
+ stableId: stableCandidates[stableCandidates.length - 1] ?? nick,
371
+ aliases: {
372
+ "irc-id-nick-user": candidates.find((candidate) => isHostlessNickUser(candidate)),
373
+ "irc-id-nick-host": stableCandidates.find((candidate) => !candidate.includes("!") && candidate.includes("@")),
374
+ "irc-nick": nick
375
+ }
376
+ };
377
+ }
378
+ //#endregion
379
+ //#region extensions/irc/src/outbound-base.ts
380
+ function sanitizeIrcAssistantText(text) {
381
+ return sanitizeForPlainText(sanitizeAssistantVisibleText(text));
382
+ }
383
+ const ircOutboundBaseAdapter = {
384
+ deliveryMode: "direct",
385
+ chunker: chunkTextForOutbound,
386
+ chunkerMode: "markdown",
387
+ textChunkLimit: 350,
388
+ sanitizeText: ({ text }) => sanitizeIrcAssistantText(text)
389
+ };
390
+ //#endregion
391
+ //#region extensions/irc/src/protocol.ts
392
+ const IRC_TARGET_PATTERN = /^[^\s:]+$/u;
227
393
  function parseIrcLine(line) {
228
394
  const raw = line.replace(/[\r\n]+/g, "").trim();
229
395
  if (!raw) return null;
@@ -302,7 +468,7 @@ function sanitizeIrcTarget(raw) {
302
468
  if (!decoded) throw new Error("IRC target is required");
303
469
  if (decoded !== decoded.trim()) throw new Error(`Invalid IRC target: ${raw}`);
304
470
  if (hasIrcControlChars(decoded)) throw new Error(`Invalid IRC target: ${raw}`);
305
- if (!IRC_TARGET_PATTERN$1.test(decoded)) throw new Error(`Invalid IRC target: ${raw}`);
471
+ if (!IRC_TARGET_PATTERN.test(decoded)) throw new Error(`Invalid IRC target: ${raw}`);
306
472
  return decoded;
307
473
  }
308
474
  function makeIrcMessageId() {
@@ -613,6 +779,8 @@ async function connectIrcClient(options) {
613
779
  //#endregion
614
780
  //#region extensions/irc/src/connect-options.ts
615
781
  function buildIrcConnectOptions(account, overrides = {}) {
782
+ assertSecretOwnerAvailable("account", `irc:${account.accountId}`);
783
+ if (account.tokenStatus === "configured_unavailable") throw new Error(`IRC credentials for account "${account.accountId}" are configured but unavailable.`);
616
784
  return {
617
785
  host: account.host,
618
786
  port: account.port,
@@ -632,68 +800,6 @@ function buildIrcConnectOptions(account, overrides = {}) {
632
800
  };
633
801
  }
634
802
  //#endregion
635
- //#region extensions/irc/src/normalize.ts
636
- const IRC_TARGET_PATTERN = /^[^\s:]+$/u;
637
- function isChannelTarget(target) {
638
- return target.startsWith("#") || target.startsWith("&");
639
- }
640
- function normalizeIrcMessagingTarget(raw) {
641
- const trimmed = raw.trim();
642
- if (!trimmed) return;
643
- let target = trimmed;
644
- if (normalizeLowercaseStringOrEmpty(target).startsWith("irc:")) target = target.slice(4).trim();
645
- if (normalizeLowercaseStringOrEmpty(target).startsWith("channel:")) {
646
- target = target.slice(8).trim();
647
- if (!target.startsWith("#") && !target.startsWith("&")) target = `#${target}`;
648
- }
649
- if (normalizeLowercaseStringOrEmpty(target).startsWith("user:")) target = target.slice(5).trim();
650
- if (!target || !looksLikeIrcTargetId(target)) return;
651
- return target;
652
- }
653
- function resolveIrcOutboundSessionRoute(params) {
654
- const target = normalizeIrcMessagingTarget(params.target);
655
- if (!target) return null;
656
- const chatType = isChannelTarget(target) ? "group" : "direct";
657
- return buildChannelOutboundSessionRoute({
658
- cfg: params.cfg,
659
- agentId: params.agentId,
660
- channel: "irc",
661
- accountId: params.accountId,
662
- recipientSessionExact: chatType === "direct" ? "direct-alias" : false,
663
- peer: {
664
- kind: chatType,
665
- id: target
666
- },
667
- chatType,
668
- from: `irc:${target}`,
669
- to: target
670
- });
671
- }
672
- function looksLikeIrcTargetId(raw) {
673
- const trimmed = raw.trim();
674
- if (!trimmed) return false;
675
- if (hasIrcControlChars(trimmed)) return false;
676
- return IRC_TARGET_PATTERN.test(trimmed);
677
- }
678
- function normalizeIrcAllowEntry(raw) {
679
- let value = normalizeLowercaseStringOrEmpty(raw);
680
- if (!value) return "";
681
- if (value.startsWith("irc:")) value = value.slice(4);
682
- if (value.startsWith("user:")) value = value.slice(5);
683
- return value.trim();
684
- }
685
- function buildIrcAllowlistCandidates(message, params) {
686
- const nick = normalizeLowercaseStringOrEmpty(message.senderNick);
687
- const user = normalizeOptionalLowercaseString(message.senderUser);
688
- const host = normalizeOptionalLowercaseString(message.senderHost);
689
- const candidates = /* @__PURE__ */ new Set();
690
- if (nick && params?.allowNameMatching === true) candidates.add(nick);
691
- if (nick && user) candidates.add(`${nick}!${user}`);
692
- if (nick && host) candidates.add(`${nick}@${host}`);
693
- if (nick && user && host) candidates.add(`${nick}!${user}@${host}`);
694
- return [...candidates];
695
- }
696
- //#endregion
697
803
  //#region extensions/irc/src/send.ts
698
804
  function recordIrcOutboundActivity(accountId) {
699
805
  try {
@@ -713,7 +819,7 @@ function resolveTarget(to, opts) {
713
819
  if (fromOpt) return fromOpt;
714
820
  throw new Error(`Invalid IRC target: ${to}`);
715
821
  }
716
- async function sendMessageIrc(to, text, opts) {
822
+ async function sendIrcMessages(to, messages, opts, onDeliveryResult) {
717
823
  const cfg = requireRuntimeConfig(opts.cfg, "IRC send");
718
824
  const account = resolveIrcAccount({
719
825
  cfg,
@@ -726,35 +832,108 @@ async function sendMessageIrc(to, text, opts) {
726
832
  channel: "irc",
727
833
  accountId: account.accountId
728
834
  });
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",
835
+ const preparedMessages = messages.map((message) => {
836
+ const prepared = stripMarkdown(convertMarkdownTables(message.text.trim(), tableMode));
837
+ if (!prepared.trim()) throw new Error("Message must be non-empty for IRC sends");
838
+ return {
839
+ payload: message.replyTo ? `${prepared}\n\n[reply:${message.replyTo}]` : prepared,
840
+ replyTo: message.replyTo
841
+ };
842
+ });
843
+ if (preparedMessages.length === 0) return [];
844
+ opts.abortSignal?.throwIfAborted();
845
+ let transient;
846
+ const client = opts.client?.isReady() ? opts.client : transient = await connectIrcClient(buildIrcConnectOptions(account, {
847
+ connectTimeoutMs: 12e3,
848
+ abortSignal: opts.abortSignal
849
+ }));
850
+ const results = [];
851
+ try {
852
+ opts.abortSignal?.throwIfAborted();
853
+ if (transient && (target.startsWith("#") || target.startsWith("&"))) client.join(target);
854
+ for (const message of preparedMessages) {
855
+ opts.abortSignal?.throwIfAborted();
856
+ if (!client.isReady()) throw new Error("IRC connection closed before send");
857
+ await opts.onPlatformSendDispatch?.();
858
+ opts.abortSignal?.throwIfAborted();
859
+ if (!client.isReady()) throw new Error("IRC connection closed before send");
860
+ client.sendPrivmsg(target, message.payload);
861
+ recordIrcOutboundActivity(account.accountId);
862
+ const messageId = makeIrcMessageId();
863
+ const result = {
748
864
  messageId,
749
- conversationId: target
750
- }],
751
- kind: "text",
752
- ...opts.replyTo ? { replyToId: opts.replyTo } : {}
753
- })
754
- };
865
+ target,
866
+ receipt: createMessageReceiptFromOutboundResults({
867
+ results: [{
868
+ channel: "irc",
869
+ messageId,
870
+ conversationId: target
871
+ }],
872
+ kind: "text",
873
+ ...message.replyTo ? { replyToId: message.replyTo } : {}
874
+ })
875
+ };
876
+ results.push(result);
877
+ await onDeliveryResult?.(result);
878
+ }
879
+ return results;
880
+ } finally {
881
+ transient?.quit("sent");
882
+ }
883
+ }
884
+ async function sendMessageIrc(to, text, opts) {
885
+ const result = (await sendIrcMessages(to, [{
886
+ text,
887
+ ...opts.replyTo ? { replyTo: opts.replyTo } : {}
888
+ }], opts))[0];
889
+ if (!result) throw new Error("Message must be non-empty for IRC sends");
890
+ return result;
755
891
  }
756
892
  //#endregion
757
893
  //#region extensions/irc/src/message-adapter.ts
894
+ function toIrcMessageResult({ target, ...result }) {
895
+ return {
896
+ ...result,
897
+ target: {
898
+ kind: "conversation",
899
+ id: target
900
+ }
901
+ };
902
+ }
903
+ async function sendIrcMessage(...args) {
904
+ return toIrcMessageResult(await sendMessageIrc(...args));
905
+ }
906
+ const sendFormattedIrcText = async (ctx) => {
907
+ const { chunkMarkdownTextWithMode, resolveChunkMode, resolveTextChunkLimit } = await import("openclaw/plugin-sdk/reply-chunking");
908
+ const accountId = ctx.accountId ?? void 0;
909
+ const textLimit = ctx.formatting?.textLimit ?? resolveTextChunkLimit(ctx.cfg, "irc", accountId, { fallbackLimit: ircOutboundBaseAdapter.textChunkLimit });
910
+ const chunkMode = ctx.formatting?.chunkMode ?? resolveChunkMode(ctx.cfg, "irc", accountId);
911
+ const chunkText = (text) => ctx.formatting ? ircOutboundBaseAdapter.chunker(text, textLimit, { formatting: ctx.formatting }) : ircOutboundBaseAdapter.chunker(text, textLimit);
912
+ let chunks;
913
+ if (chunkMode === "newline") {
914
+ const blocks = chunkMarkdownTextWithMode(ctx.text, textLimit, chunkMode);
915
+ if (blocks.length === 0 && ctx.text) blocks.push(ctx.text);
916
+ chunks = blocks.flatMap((block) => {
917
+ const blockChunks = chunkText(block);
918
+ return blockChunks.length === 0 && block ? [block] : blockChunks;
919
+ });
920
+ } else chunks = chunkText(ctx.text);
921
+ const nextReplyToId = createReplyToFanout(ctx);
922
+ return (await sendIrcMessages(ctx.to, chunks.map((text) => {
923
+ const replyTo = nextReplyToId();
924
+ return replyTo ? {
925
+ text,
926
+ replyTo
927
+ } : { text };
928
+ }), {
929
+ cfg: ctx.cfg,
930
+ accountId,
931
+ abortSignal: ctx.abortSignal,
932
+ onPlatformSendDispatch: ctx.onPlatformSendDispatch
933
+ }, async (result) => {
934
+ await ctx.onDeliveryResult?.(attachChannelToResult("irc", toIrcMessageResult(result)));
935
+ })).map((result) => attachChannelToResult("irc", toIrcMessageResult(result)));
936
+ };
758
937
  const ircMessageAdapter = defineChannelMessageAdapter({
759
938
  id: "irc",
760
939
  durableFinal: { capabilities: {
@@ -763,12 +942,12 @@ const ircMessageAdapter = defineChannelMessageAdapter({
763
942
  replyTo: true
764
943
  } },
765
944
  send: {
766
- text: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageIrc(to, text, {
945
+ text: async ({ cfg, to, text, accountId, replyToId }) => await sendIrcMessage(to, text, {
767
946
  cfg,
768
947
  accountId: accountId ?? void 0,
769
948
  replyTo: replyToId ?? void 0
770
949
  }),
771
- media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageIrc(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
950
+ media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendIrcMessage(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
772
951
  cfg,
773
952
  accountId: accountId ?? void 0,
774
953
  replyTo: replyToId ?? void 0
@@ -776,18 +955,6 @@ const ircMessageAdapter = defineChannelMessageAdapter({
776
955
  }
777
956
  });
778
957
  //#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
958
  //#region extensions/irc/src/policy.ts
792
959
  function resolveIrcGroupScope(params) {
793
960
  const { "*": wildcard, ...groups } = params.groups ?? {};
@@ -1399,7 +1566,7 @@ const meta = {
1399
1566
  systemImage: "number",
1400
1567
  markdownCapable: true
1401
1568
  };
1402
- const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-kWMtRD4g.js"));
1569
+ const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-DZcQfOqp.js"));
1403
1570
  function normalizePairingTarget(raw) {
1404
1571
  const normalized = normalizeIrcAllowEntry(raw);
1405
1572
  if (!normalized) return "";
@@ -1453,9 +1620,10 @@ const resolveIrcDmPolicy = createScopedDmSecurityResolver({
1453
1620
  resolvePolicy: (account) => account.config.dmPolicy,
1454
1621
  resolveAllowFrom: (account) => account.config.allowFrom,
1455
1622
  policyPathSuffix: "dmPolicy",
1623
+ classifyEntryAuthentication: identityEntryAuthenticationClassifier(ircIngressIdentity),
1456
1624
  normalizeEntry: (raw) => normalizeIrcAllowEntry(raw)
1457
1625
  });
1458
- const collectIrcSecurityWarnings = composeAccountWarningCollectors(createAllowlistProviderOpenWarningCollector({
1626
+ const collectIrcGroupPolicyWarnings = createAllowlistProviderOpenWarningCollector({
1459
1627
  providerConfigPresent: (cfg) => cfg.channels?.irc !== void 0,
1460
1628
  resolveGroupPolicy: (account) => account.config.groupPolicy,
1461
1629
  buildOpenWarning: {
@@ -1463,7 +1631,19 @@ const collectIrcSecurityWarnings = composeAccountWarningCollectors(createAllowli
1463
1631
  openBehavior: "allows all channels and senders (mention-gated)",
1464
1632
  remediation: "Prefer channels.irc.groupPolicy=\"allowlist\" with channels.irc.groups"
1465
1633
  }
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.");
1634
+ });
1635
+ const collectIrcOpenGroupFindings = createConditionalWarningCollector.findings({
1636
+ collectWarnings: collectIrcGroupPolicyWarnings,
1637
+ checkId: "channels.irc.groups.open",
1638
+ severity: "critical",
1639
+ title: "IRC security warning"
1640
+ });
1641
+ const collectIrcSecurityWarnings = (params) => [
1642
+ ...collectIrcOpenGroupFindings(params),
1643
+ ...!params.account.config.tls ? ["- IRC TLS is disabled (channels.irc.tls=false); traffic and credentials are plaintext."] : [],
1644
+ ...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."] : [],
1645
+ ...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."] : []
1646
+ ];
1467
1647
  const ircPlugin = createChatChannelPlugin({
1468
1648
  base: {
1469
1649
  id: "irc",
@@ -1630,7 +1810,10 @@ const ircPlugin = createChatChannelPlugin({
1630
1810
  collectWarnings: collectIrcSecurityWarnings
1631
1811
  },
1632
1812
  outbound: {
1633
- base: ircOutboundBaseAdapter,
1813
+ base: {
1814
+ ...ircOutboundBaseAdapter,
1815
+ sendFormattedText: sendFormattedIrcText
1816
+ },
1634
1817
  attachedResults: {
1635
1818
  channel: "irc",
1636
1819
  sendText: ({ onDeliveryResult: _onDeliveryResult, ...ctx }) => ircMessageAdapter.send.text(ctx),
@@ -1642,4 +1825,4 @@ const ircPlugin = createChatChannelPlugin({
1642
1825
  }
1643
1826
  });
1644
1827
  //#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 };
1828
+ export { listIrcAccountIds as _, resolveIrcGroupRequireMention as a, connectIrcClient as c, sanitizeIrcAssistantText as d, createIrcIngressSubject as f, listEnabledIrcAccounts as g, normalizeIrcAllowEntry as h, resolveIrcGroupMatch as i, parseIrcLine as l, isChannelTarget as m, ircSetupWizard as n, sendMessageIrc as o, ircIngressIdentity as p, ircSetupAdapter as r, buildIrcConnectOptions as s, ircPlugin as t, parseIrcPrefix as u, resolveDefaultIrcAccountId as v, resolveIrcAccount as y };
@@ -1,2 +1,2 @@
1
- import { t as IrcChannelConfigSchema } from "./config-schema-DdB_h5mL.js";
1
+ import { t as IrcChannelConfigSchema } from "./config-schema-CpB6AaUh.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-BxbQEBnS.js";
2
2
  export { ircPlugin };
@@ -1,51 +1,18 @@
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 connectIrcClient, d as sanitizeIrcAssistantText, f as createIrcIngressSubject, h as normalizeIrcAllowEntry, i as resolveIrcGroupMatch, l as parseIrcLine, m as isChannelTarget, o as sendMessageIrc, p as ircIngressIdentity, s as buildIrcConnectOptions, u as parseIrcPrefix, y as resolveIrcAccount } from "./channel-BxbQEBnS.js";
2
2
  import { t as getIrcRuntime } from "./runtime-xy-FcjJC.js";
3
+ import { channelIngressRoutes, createChannelIngressResolver } from "openclaw/plugin-sdk/channel-ingress-runtime";
3
4
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
4
5
  import { bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
5
6
  import { randomUUID } from "node:crypto";
6
7
  import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
7
8
  import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
8
9
  import { logInboundDrop, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
9
- import { channelIngressRoutes, createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
10
10
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
11
11
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
12
12
  import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
13
13
  import { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
14
14
  //#region extensions/irc/src/inbound.ts
15
15
  const CHANNEL_ID = "irc";
16
- const ircIngressIdentity = defineStableChannelIngressIdentity({
17
- key: "irc-id",
18
- normalizeEntry: normalizeIrcStableEntry,
19
- normalizeSubject: normalizeLowercaseStringOrEmpty,
20
- sensitivity: "pii",
21
- aliases: [
22
- {
23
- key: "irc-id-nick-user",
24
- kind: "stable-id",
25
- normalizeEntry: normalizeIrcNickUserEntry,
26
- normalizeSubject: normalizeLowercaseStringOrEmpty,
27
- dangerous: true,
28
- sensitivity: "pii"
29
- },
30
- {
31
- key: "irc-id-nick-host",
32
- kind: "stable-id",
33
- normalizeEntry: () => null,
34
- normalizeSubject: normalizeLowercaseStringOrEmpty,
35
- sensitivity: "pii"
36
- },
37
- {
38
- key: "irc-nick",
39
- kind: "plugin:irc-nick",
40
- normalizeEntry: normalizeIrcNickEntry,
41
- normalizeSubject: normalizeLowercaseStringOrEmpty,
42
- dangerous: true,
43
- sensitivity: "pii"
44
- }
45
- ],
46
- isWildcardEntry: (entry) => normalizeIrcAllowEntry(entry) === "*",
47
- resolveEntryId: ({ entryIndex, fieldKey }) => `irc-entry-${entryIndex + 1}:${fieldKey === "irc-nick" ? "nick" : "id"}`
48
- });
49
16
  const escapeIrcRegexLiteral = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50
17
  const IRC_NICK_CHARACTER = String.raw`[A-Za-z0-9_\-\[\]\\\x60^{}|~]`;
51
18
  const IRC_RFC1459_CASE_EQUIVALENTS = /* @__PURE__ */ new Map([
@@ -64,46 +31,9 @@ function buildIrcNickMentionPattern(value) {
64
31
  return equivalent ? `[${escapeIrcRegexLiteral(character)}${escapeIrcRegexLiteral(equivalent)}]` : escapeIrcRegexLiteral(character);
65
32
  }).join("");
66
33
  }
67
- function isBareNick(value) {
68
- return !value.includes("!") && !value.includes("@");
69
- }
70
- function hasVerifiedHost(value) {
71
- return value.includes("@");
72
- }
73
- function isHostlessNickUser(value) {
74
- return value.includes("!") && !value.includes("@");
75
- }
76
- function normalizeIrcStableEntry(value) {
77
- const normalized = normalizeIrcAllowEntry(value);
78
- if (!normalized || normalized === "*" || !hasVerifiedHost(normalized)) return null;
79
- return normalized;
80
- }
81
- function normalizeIrcNickUserEntry(value) {
82
- const normalized = normalizeIrcAllowEntry(value);
83
- if (!normalized || normalized === "*" || !isHostlessNickUser(normalized)) return null;
84
- return normalized;
85
- }
86
- function normalizeIrcNickEntry(value) {
87
- const normalized = normalizeIrcAllowEntry(value);
88
- if (!normalized || normalized === "*" || !isBareNick(normalized)) return null;
89
- return normalized;
90
- }
91
34
  function hasEntries(entries) {
92
35
  return normalizeStringEntries(entries).some((entry) => normalizeIrcAllowEntry(entry));
93
36
  }
94
- function createIrcIngressSubject(message) {
95
- const candidates = buildIrcAllowlistCandidates(message, { allowNameMatching: true });
96
- const stableCandidates = candidates.filter((candidate) => hasVerifiedHost(candidate));
97
- const nick = normalizeLowercaseStringOrEmpty(message.senderNick);
98
- return {
99
- stableId: stableCandidates[stableCandidates.length - 1] ?? nick,
100
- aliases: {
101
- "irc-id-nick-user": candidates.find((candidate) => isHostlessNickUser(candidate)),
102
- "irc-id-nick-host": stableCandidates.find((candidate) => !candidate.includes("!") && candidate.includes("@")),
103
- "irc-nick": nick
104
- }
105
- };
106
- }
107
37
  function routeDescriptorsForIrcGroup(params) {
108
38
  if (!params.isGroup) return [];
109
39
  return channelIngressRoutes(params.groupPolicy === "allowlist" && {
@@ -455,14 +385,16 @@ function decodeIrcIngressPayload(payload, claimedId) {
455
385
  }
456
386
  function inspectIrcIngress(raw, phase) {
457
387
  try {
388
+ const projection = inspectRawPrivmsg(raw.body.rawLine);
389
+ if (phase === "claim") raw.claimedProjection = projection;
458
390
  return {
459
- eventId: raw.eventId,
460
- laneKey: inspectRawPrivmsg(raw.rawLine).laneKey
391
+ eventId: raw.body.eventId,
392
+ laneKey: projection.laneKey
461
393
  };
462
394
  } catch (error) {
463
395
  if (phase === "admission" && error instanceof IrcIngressPayloadError) return {
464
- eventId: raw.eventId,
465
- laneKey: `invalid:${raw.eventId}`
396
+ eventId: raw.body.eventId,
397
+ laneKey: `invalid:${raw.body.eventId}`
466
398
  };
467
399
  throw error;
468
400
  }
@@ -479,8 +411,8 @@ function createIrcIngressMonitor(options) {
479
411
  inspect: (raw, context) => inspectIrcIngress(raw, context.phase),
480
412
  payload: {
481
413
  version: IRC_INGRESS_PAYLOAD_VERSION,
482
- serialize: (raw) => raw,
483
- deserialize: (body) => body,
414
+ serialize: (raw) => raw.body,
415
+ deserialize: (body) => ({ body }),
484
416
  encode: ({ body }) => ({
485
417
  version: IRC_INGRESS_PAYLOAD_VERSION,
486
418
  ...body
@@ -490,13 +422,13 @@ function createIrcIngressMonitor(options) {
490
422
  },
491
423
  deliver: (raw, lifecycle, claim) => {
492
424
  const message = {
493
- ...inspectRawPrivmsg(raw.rawLine).message,
425
+ ...raw.claimedProjection.message,
494
426
  messageId: claim.id,
495
- timestamp: raw.receivedAt
427
+ timestamp: raw.body.receivedAt
496
428
  };
497
429
  return options.dispatch(message, lifecycle, {
498
- connectedNick: raw.connectedNick.trim(),
499
- connectionEpoch: raw.connectionEpoch.trim()
430
+ connectedNick: raw.body.connectedNick.trim(),
431
+ connectionEpoch: raw.body.connectionEpoch.trim()
500
432
  });
501
433
  },
502
434
  pollIntervalMs: options.pollIntervalMs ?? IRC_INGRESS_POLL_INTERVAL_MS,
@@ -526,13 +458,13 @@ function createIrcIngressMonitor(options) {
526
458
  const receivedAt = Date.now();
527
459
  const normalizedNick = connectedNick.trim();
528
460
  if (!normalizedNick) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress connected nickname is required."));
529
- return monitor.admit({
461
+ return monitor.admit({ body: {
530
462
  eventId,
531
463
  rawLine,
532
464
  receivedAt,
533
465
  connectionEpoch: epoch,
534
466
  connectedNick: normalizedNick
535
- }, { receivedAt }).then(() => void 0);
467
+ } }, { receivedAt }).then(() => void 0);
536
468
  }
537
469
  };
538
470
  },
@@ -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(),
@@ -76,7 +77,6 @@ const IrcChannelConfigSchema = buildChannelConfigSchema(buildMultiAccountChannel
76
77
  groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
77
78
  groups: z.record(z.string(), ChannelGroupEntrySchema.optional()).optional(),
78
79
  channels: z.array(z.string()).optional(),
79
- mentionPatterns: z.array(z.string()).optional(),
80
80
  markdown: MarkdownConfigSchema,
81
81
  ...ReplyRuntimeConfigSchemaShape
82
82
  }).strict(), {
@@ -1,5 +1,5 @@
1
- import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
2
- //#region extensions/irc/doctor-contract-api.ts
1
+ import { asObjectRecord, defineChannelAliasMigration, stripRetiredChannelKeys } from "openclaw/plugin-sdk/runtime-doctor-migrations";
2
+ //#region extensions/irc/config-doctor-api.ts
3
3
  const streamingAliasMigration = defineChannelAliasMigration({
4
4
  channelId: "irc",
5
5
  streaming: {
@@ -8,9 +8,39 @@ const streamingAliasMigration = defineChannelAliasMigration({
8
8
  },
9
9
  accountStreamingReplacesRoot: true
10
10
  });
11
- const legacyConfigRules = streamingAliasMigration.legacyConfigRules;
11
+ const RETIRED_IRC_KEYS = /* @__PURE__ */ new Set(["mentionPatterns"]);
12
+ const legacyConfigRules = [
13
+ ...streamingAliasMigration.legacyConfigRules,
14
+ {
15
+ path: ["channels", "irc"],
16
+ match: (value) => Object.hasOwn(asObjectRecord(value) ?? {}, "mentionPatterns"),
17
+ message: "channels.irc.mentionPatterns was accepted but never read; configure mention patterns with messages.groupChat.mentionPatterns. Run \"openclaw doctor --fix\"."
18
+ },
19
+ {
20
+ path: [
21
+ "channels",
22
+ "irc",
23
+ "accounts"
24
+ ],
25
+ match: (value) => Object.values(asObjectRecord(value) ?? {}).some((account) => Object.hasOwn(asObjectRecord(account) ?? {}, "mentionPatterns")),
26
+ message: "channels.irc.accounts.<id>.mentionPatterns was accepted but never read; configure mention patterns with messages.groupChat.mentionPatterns. Run \"openclaw doctor --fix\"."
27
+ }
28
+ ];
12
29
  function normalizeCompatibilityConfig({ cfg }) {
13
- return streamingAliasMigration.normalizeChannelConfig({ cfg });
30
+ const aliases = streamingAliasMigration.normalizeChannelConfig({ cfg });
31
+ const changes = [...aliases.changes];
32
+ return {
33
+ config: stripRetiredChannelKeys({
34
+ cfg: aliases.config,
35
+ channelId: "irc",
36
+ keys: RETIRED_IRC_KEYS,
37
+ scope: "root-and-accounts",
38
+ onRemove: (removed) => {
39
+ changes.push(`Removed ${removed.pathPrefix}.${removed.key}; it was never read.`);
40
+ }
41
+ }).config,
42
+ changes
43
+ };
14
44
  }
15
45
  //#endregion
16
46
  export { legacyConfigRules, normalizeCompatibilityConfig };
@@ -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
  },
@@ -227,12 +247,6 @@
227
247
  "type": "string"
228
248
  }
229
249
  },
230
- "mentionPatterns": {
231
- "type": "array",
232
- "items": {
233
- "type": "string"
234
- }
235
- },
236
250
  "markdown": {
237
251
  "type": "object",
238
252
  "properties": {
@@ -355,6 +369,26 @@
355
369
  "configWrites": {
356
370
  "type": "boolean"
357
371
  },
372
+ "replyToMode": {
373
+ "anyOf": [
374
+ {
375
+ "type": "string",
376
+ "const": "off"
377
+ },
378
+ {
379
+ "type": "string",
380
+ "const": "first"
381
+ },
382
+ {
383
+ "type": "string",
384
+ "const": "all"
385
+ },
386
+ {
387
+ "type": "string",
388
+ "const": "batched"
389
+ }
390
+ ]
391
+ },
358
392
  "dangerouslyAllowNameMatching": {
359
393
  "type": "boolean"
360
394
  },
@@ -553,12 +587,6 @@
553
587
  "type": "string"
554
588
  }
555
589
  },
556
- "mentionPatterns": {
557
- "type": "array",
558
- "items": {
559
- "type": "string"
560
- }
561
- },
562
590
  "markdown": {
563
591
  "type": "object",
564
592
  "properties": {
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.8.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.8.1"
125
125
  },
126
126
  "build": {
127
- "openclawVersion": "2026.8.1-beta.2",
127
+ "openclawVersion": "2026.8.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.8.1"
153
153
  },
154
154
  "peerDependenciesMeta": {
155
155
  "openclaw": {