@openclaw/irc 2026.7.2-beta.7 → 2026.8.1-beta.3

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-DoavT7vs.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-BO1GdA4V.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-BPoye3--.js";
1
+ import { t as IrcChannelConfigSchema } from "./config-schema-DdB_h5mL.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,18 @@ 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
19
  import { createAccountStatusSink, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, runPassiveAccountLifecycle, sanitizeForPlainText } from "openclaw/plugin-sdk/channel-outbound";
20
20
  import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";
21
- import { chunkTextForOutbound, convertMarkdownTables, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
21
+ import { chunkTextForOutbound, convertMarkdownTables, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
22
22
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
23
23
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
24
24
  import net from "node:net";
25
25
  import tls from "node:tls";
26
26
  import { withTimeout } from "openclaw/plugin-sdk/security-runtime";
27
27
  import { randomUUID } from "node:crypto";
28
+ import { assertSecretOwnerAvailable } from "openclaw/plugin-sdk/channel-secret-owner-runtime";
28
29
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
29
30
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
30
31
  import { applyAccountNameToChannelSection, createAllowFromSection, createPromptParsedAllowFromForAccount, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, createTopLevelChannelAllowFromSetter, createTopLevelChannelDmPolicySetter, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
@@ -42,6 +43,16 @@ const { listAccountIds: listIrcAccountIds, resolveDefaultAccountId: resolveDefau
42
43
  hasImplicitDefaultAccount: (cfg) => Boolean((cfg.channels?.irc?.host?.trim() || process.env.IRC_HOST?.trim()) && (cfg.channels?.irc?.nick?.trim() || process.env.IRC_NICK?.trim()))
43
44
  });
44
45
  function resolvePassword(accountId, merged) {
46
+ const configPassword = resolveSecretInputString({
47
+ value: merged.password,
48
+ path: `channels.irc.accounts.${accountId}.password`,
49
+ mode: "inspect"
50
+ });
51
+ if (configPassword.status === "configured_unavailable") return {
52
+ password: "",
53
+ source: "config",
54
+ unavailable: true
55
+ };
45
56
  if (accountId === DEFAULT_ACCOUNT_ID) {
46
57
  const envPassword = process.env.IRC_PASSWORD?.trim();
47
58
  if (envPassword) return {
@@ -51,7 +62,7 @@ function resolvePassword(accountId, merged) {
51
62
  }
52
63
  if (merged.passwordFile?.trim()) {
53
64
  let diagnostic;
54
- const filePassword = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
65
+ const password = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
55
66
  rejectSymlink: true,
56
67
  credentialDiagnostic: {
57
68
  configPath: `channels.irc.accounts.${accountId}.passwordFile`,
@@ -60,8 +71,8 @@ function resolvePassword(accountId, merged) {
60
71
  }
61
72
  }
62
73
  });
63
- if (filePassword) return {
64
- password: filePassword,
74
+ if (password) return {
75
+ password,
65
76
  source: "passwordFile"
66
77
  };
67
78
  return {
@@ -70,12 +81,8 @@ function resolvePassword(accountId, merged) {
70
81
  diagnostic
71
82
  };
72
83
  }
73
- const configPassword = normalizeResolvedSecretInputString({
74
- value: merged.password,
75
- path: `channels.irc.accounts.${accountId}.password`
76
- });
77
- if (configPassword) return {
78
- password: configPassword,
84
+ if (configPassword.status === "available") return {
85
+ password: configPassword.value,
79
86
  source: "config"
80
87
  };
81
88
  return {
@@ -85,23 +92,29 @@ function resolvePassword(accountId, merged) {
85
92
  }
86
93
  function resolveNickServConfig(accountId, nickserv) {
87
94
  const base = nickserv ?? {};
95
+ const configPassword = resolveSecretInputString({
96
+ value: base.password,
97
+ path: `channels.irc.accounts.${accountId}.nickserv.password`,
98
+ mode: "inspect"
99
+ });
100
+ const unavailable = Boolean(configPassword.ref);
88
101
  const envPassword = accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_PASSWORD?.trim() : void 0;
89
102
  const envRegisterEmail = accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_NICKSERV_REGISTER_EMAIL?.trim() : void 0;
90
103
  const passwordFile = base.passwordFile?.trim();
91
- let resolvedPassword = normalizeResolvedSecretInputString({
92
- value: base.password,
93
- path: `channels.irc.accounts.${accountId}.nickserv.password`
94
- }) || envPassword || "";
104
+ let resolvedPassword;
95
105
  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;
106
+ if (!unavailable) {
107
+ resolvedPassword = configPassword.value || envPassword;
108
+ if (!resolvedPassword && passwordFile) resolvedPassword = tryReadSecretFileSync(passwordFile, "IRC NickServ password file", {
109
+ rejectSymlink: true,
110
+ credentialDiagnostic: {
111
+ configPath: `channels.irc.accounts.${accountId}.nickserv.passwordFile`,
112
+ report: (value) => {
113
+ diagnostic = value;
114
+ }
102
115
  }
103
- }
104
- }) ?? "";
116
+ });
117
+ }
105
118
  return {
106
119
  config: {
107
120
  ...base,
@@ -110,7 +123,8 @@ function resolveNickServConfig(accountId, nickserv) {
110
123
  password: resolvedPassword || void 0,
111
124
  registerEmail: base.registerEmail?.trim() || envRegisterEmail || void 0
112
125
  },
113
- diagnostic
126
+ diagnostic,
127
+ unavailable: base.enabled !== false && unavailable
114
128
  };
115
129
  }
116
130
  function resolveIrcAccount(params) {
@@ -154,7 +168,7 @@ function resolveIrcAccount(params) {
154
168
  realname,
155
169
  password: passwordResolution.password,
156
170
  passwordSource: passwordResolution.source,
157
- tokenStatus: diagnostics.length > 0 ? "configured_unavailable" : passwordResolution.password || nickservResolution.config.password ? "available" : "missing",
171
+ tokenStatus: diagnostics.length > 0 || passwordResolution.unavailable || nickservResolution.unavailable ? "configured_unavailable" : passwordResolution.password || nickservResolution.config.password ? "available" : "missing",
158
172
  ...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
159
173
  config
160
174
  };
@@ -183,7 +197,7 @@ const collectIrcMutableAllowlistWarnings = createDangerousNameMatchingMutableAll
183
197
  });
184
198
  //#endregion
185
199
  //#region extensions/irc/src/gateway.ts
186
- const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-DsN_0hPp.js"));
200
+ const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-CSpBc1ef.js"));
187
201
  async function startIrcGatewayAccount(ctx) {
188
202
  const account = ctx.account;
189
203
  const statusSink = createAccountStatusSink({
@@ -334,7 +348,7 @@ function takeIrcPrivmsgChunk(text, maxChars, maxBytes) {
334
348
  if (splitAt >= Math.floor(fitted.length / 2)) return fitted.slice(0, splitAt);
335
349
  return fitted;
336
350
  }
337
- function toError(err) {
351
+ function toIrcError(err) {
338
352
  if (err instanceof Error) return err;
339
353
  return new Error(typeof err === "string" ? err : JSON.stringify(err));
340
354
  }
@@ -391,7 +405,7 @@ async function connectIrcClient(options) {
391
405
  rejectReady = reject;
392
406
  });
393
407
  const fail = (err) => {
394
- const error = toError(err);
408
+ const error = toIrcError(err);
395
409
  if (options.onError) options.onError(error);
396
410
  if (!ready && rejectReady) {
397
411
  rejectReady(error);
@@ -613,6 +627,8 @@ async function connectIrcClient(options) {
613
627
  //#endregion
614
628
  //#region extensions/irc/src/connect-options.ts
615
629
  function buildIrcConnectOptions(account, overrides = {}) {
630
+ assertSecretOwnerAvailable("account", `irc:${account.accountId}`);
631
+ if (account.tokenStatus === "configured_unavailable") throw new Error(`IRC credentials for account "${account.accountId}" are configured but unavailable.`);
616
632
  return {
617
633
  host: account.host,
618
634
  port: account.port,
@@ -726,9 +742,9 @@ async function sendMessageIrc(to, text, opts) {
726
742
  channel: "irc",
727
743
  accountId: account.accountId
728
744
  });
729
- const prepared = convertMarkdownTables(text.trim(), tableMode);
745
+ const prepared = stripMarkdown(convertMarkdownTables(text.trim(), tableMode));
746
+ if (!prepared.trim()) throw new Error("Message must be non-empty for IRC sends");
730
747
  const payload = opts.replyTo ? `${prepared}\n\n[reply:${opts.replyTo}]` : prepared;
731
- if (!payload.trim()) throw new Error("Message must be non-empty for IRC sends");
732
748
  const client = opts.client;
733
749
  if (client?.isReady()) client.sendPrivmsg(target, payload);
734
750
  else {
@@ -755,6 +771,16 @@ async function sendMessageIrc(to, text, opts) {
755
771
  }
756
772
  //#endregion
757
773
  //#region extensions/irc/src/message-adapter.ts
774
+ async function sendIrcMessage(...args) {
775
+ const { target, ...result } = await sendMessageIrc(...args);
776
+ return {
777
+ ...result,
778
+ target: {
779
+ kind: "conversation",
780
+ id: target
781
+ }
782
+ };
783
+ }
758
784
  const ircMessageAdapter = defineChannelMessageAdapter({
759
785
  id: "irc",
760
786
  durableFinal: { capabilities: {
@@ -763,12 +789,12 @@ const ircMessageAdapter = defineChannelMessageAdapter({
763
789
  replyTo: true
764
790
  } },
765
791
  send: {
766
- text: async ({ cfg, to, text, accountId, replyToId }) => await sendMessageIrc(to, text, {
792
+ text: async ({ cfg, to, text, accountId, replyToId }) => await sendIrcMessage(to, text, {
767
793
  cfg,
768
794
  accountId: accountId ?? void 0,
769
795
  replyTo: replyToId ?? void 0
770
796
  }),
771
- media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendMessageIrc(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
797
+ media: async ({ cfg, to, text, mediaUrl, accountId, replyToId }) => await sendIrcMessage(to, mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text, {
772
798
  cfg,
773
799
  accountId: accountId ?? void 0,
774
800
  replyTo: replyToId ?? void 0
@@ -1028,7 +1054,8 @@ const ircSetupContract = defineChannelSetupContract({
1028
1054
  cli: {
1029
1055
  flags: "--use-env",
1030
1056
  description: "Use IRC environment configuration"
1031
- }
1057
+ },
1058
+ envVars: ["IRC_HOST", "IRC_NICK"]
1032
1059
  }
1033
1060
  },
1034
1061
  legacyAdapter: ircSetupAdapter
@@ -1398,7 +1425,7 @@ const meta = {
1398
1425
  systemImage: "number",
1399
1426
  markdownCapable: true
1400
1427
  };
1401
- const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-DsN_0hPp.js"));
1428
+ const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-CSpBc1ef.js"));
1402
1429
  function normalizePairingTarget(raw) {
1403
1430
  const normalized = normalizeIrcAllowEntry(raw);
1404
1431
  if (!normalized) return "";
@@ -1454,7 +1481,7 @@ const resolveIrcDmPolicy = createScopedDmSecurityResolver({
1454
1481
  policyPathSuffix: "dmPolicy",
1455
1482
  normalizeEntry: (raw) => normalizeIrcAllowEntry(raw)
1456
1483
  });
1457
- const collectIrcSecurityWarnings = composeAccountWarningCollectors(createAllowlistProviderOpenWarningCollector({
1484
+ const collectIrcGroupPolicyWarnings = createAllowlistProviderOpenWarningCollector({
1458
1485
  providerConfigPresent: (cfg) => cfg.channels?.irc !== void 0,
1459
1486
  resolveGroupPolicy: (account) => account.config.groupPolicy,
1460
1487
  buildOpenWarning: {
@@ -1462,7 +1489,19 @@ const collectIrcSecurityWarnings = composeAccountWarningCollectors(createAllowli
1462
1489
  openBehavior: "allows all channels and senders (mention-gated)",
1463
1490
  remediation: "Prefer channels.irc.groupPolicy=\"allowlist\" with channels.irc.groups"
1464
1491
  }
1465
- }), (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.");
1492
+ });
1493
+ const collectIrcOpenGroupFindings = createConditionalWarningCollector.findings({
1494
+ collectWarnings: collectIrcGroupPolicyWarnings,
1495
+ checkId: "channels.irc.groups.open",
1496
+ severity: "critical",
1497
+ title: "IRC security warning"
1498
+ });
1499
+ const collectIrcSecurityWarnings = (params) => [
1500
+ ...collectIrcOpenGroupFindings(params),
1501
+ ...!params.account.config.tls ? ["- IRC TLS is disabled (channels.irc.tls=false); traffic and credentials are plaintext."] : [],
1502
+ ...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."] : [],
1503
+ ...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."] : []
1504
+ ];
1466
1505
  const ircPlugin = createChatChannelPlugin({
1467
1506
  base: {
1468
1507
  id: "irc",
@@ -1470,7 +1509,6 @@ const ircPlugin = createChatChannelPlugin({
1470
1509
  ...meta,
1471
1510
  quickstartAllowFrom: true
1472
1511
  },
1473
- setup: ircSetupAdapter,
1474
1512
  setupContract: ircSetupContract,
1475
1513
  setupWizard: ircSetupWizard,
1476
1514
  capabilities: {
@@ -1532,6 +1570,10 @@ const ircPlugin = createChatChannelPlugin({
1532
1570
  messaging: {
1533
1571
  targetPrefixes: ["irc"],
1534
1572
  normalizeTarget: normalizeIrcMessagingTarget,
1573
+ inferTargetChatType: ({ to }) => {
1574
+ const target = normalizeIrcMessagingTarget(to);
1575
+ return target ? isChannelTarget(target) ? "group" : "direct" : void 0;
1576
+ },
1535
1577
  resolveOutboundSessionRoute: (params) => resolveIrcOutboundSessionRoute(params),
1536
1578
  targetResolver: {
1537
1579
  looksLikeId: looksLikeIrcTargetId,
@@ -1,2 +1,2 @@
1
- import { t as IrcChannelConfigSchema } from "./config-schema-BPoye3--.js";
1
+ import { t as IrcChannelConfigSchema } from "./config-schema-DdB_h5mL.js";
2
2
  export { IrcChannelConfigSchema };
@@ -1,2 +1,2 @@
1
- import { t as ircPlugin } from "./channel-DoavT7vs.js";
1
+ import { t as ircPlugin } from "./channel-BO1GdA4V.js";
2
2
  export { ircPlugin };
@@ -1,10 +1,11 @@
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-DoavT7vs.js";
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-BO1GdA4V.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";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
7
- import { buildChannelInboundEventContext, logInboundDrop, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
7
+ import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
8
+ import { logInboundDrop, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
8
9
  import { channelIngressRoutes, createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
9
10
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
10
11
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
@@ -46,6 +47,23 @@ const ircIngressIdentity = defineStableChannelIngressIdentity({
46
47
  resolveEntryId: ({ entryIndex, fieldKey }) => `irc-entry-${entryIndex + 1}:${fieldKey === "irc-nick" ? "nick" : "id"}`
47
48
  });
48
49
  const escapeIrcRegexLiteral = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
50
+ const IRC_NICK_CHARACTER = String.raw`[A-Za-z0-9_\-\[\]\\\x60^{}|~]`;
51
+ const IRC_RFC1459_CASE_EQUIVALENTS = /* @__PURE__ */ new Map([
52
+ ["[", "{"],
53
+ ["{", "["],
54
+ ["]", "}"],
55
+ ["}", "]"],
56
+ ["\\", "|"],
57
+ ["|", "\\"],
58
+ ["^", "~"],
59
+ ["~", "^"]
60
+ ]);
61
+ function buildIrcNickMentionPattern(value) {
62
+ return Array.from(value, (character) => {
63
+ const equivalent = IRC_RFC1459_CASE_EQUIVALENTS.get(character);
64
+ return equivalent ? `[${escapeIrcRegexLiteral(character)}${escapeIrcRegexLiteral(equivalent)}]` : escapeIrcRegexLiteral(character);
65
+ }).join("");
66
+ }
49
67
  function isBareNick(value) {
50
68
  return !value.includes("!") && !value.includes("@");
51
69
  }
@@ -165,7 +183,7 @@ async function handleIrcInbound(params) {
165
183
  const hasControlCommand = core.channel.text.hasControlCommand(rawBody, config);
166
184
  const mentionRegexes = core.channel.mentions.buildMentionRegexes(config);
167
185
  const mentionNick = connectedNick?.trim() || account.nick;
168
- const explicitMentionRegex = mentionNick ? new RegExp(`\\b${escapeIrcRegexLiteral(mentionNick)}\\b[:,]?`, "i") : null;
186
+ const explicitMentionRegex = mentionNick ? new RegExp(`(?<!${IRC_NICK_CHARACTER})${buildIrcNickMentionPattern(mentionNick)}(?!${IRC_NICK_CHARACTER})[:,]?`, "i") : null;
169
187
  const wasMentioned = core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes) || (explicitMentionRegex ? explicitMentionRegex.test(rawBody) : false);
170
188
  const requireMention = message.isGroup ? resolveIrcGroupRequireMention({
171
189
  groups: account.config.groups,
@@ -173,6 +191,17 @@ async function handleIrcInbound(params) {
173
191
  }) : false;
174
192
  const routeGroupAllowFrom = normalizeStringEntries(groupMatch.groupConfig?.allowFrom?.length ? groupMatch.groupConfig.allowFrom : groupMatch.wildcardConfig?.allowFrom);
175
193
  const accessGroupPolicy = groupPolicy === "open" && (hasEntries(account.config.groupAllowFrom) || hasEntries(routeGroupAllowFrom)) ? "allowlist" : groupPolicy;
194
+ const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
195
+ const peerId = message.isGroup ? channelTarget : message.senderNick;
196
+ const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
197
+ cfg: config,
198
+ channel: CHANNEL_ID,
199
+ accountId: account.accountId,
200
+ peer: {
201
+ kind: message.isGroup ? "group" : "direct",
202
+ id: peerId
203
+ }
204
+ });
176
205
  const access = await createChannelIngressResolver({
177
206
  channelId: CHANNEL_ID,
178
207
  accountId: account.accountId,
@@ -185,6 +214,12 @@ async function handleIrcInbound(params) {
185
214
  kind: message.isGroup ? "group" : "direct",
186
215
  id: message.target
187
216
  },
217
+ contextBinding: {
218
+ agentId: route.agentId,
219
+ sessionKey: route.sessionKey,
220
+ ...message.messageId ? { messageId: message.messageId } : {},
221
+ inboundEventKind: "user_request"
222
+ },
188
223
  route: routeDescriptorsForIrcGroup({
189
224
  isGroup: message.isGroup,
190
225
  groupPolicy,
@@ -262,17 +297,6 @@ async function handleIrcInbound(params) {
262
297
  kind: "failed-retryable",
263
298
  error: turnAdoptionLifecycle.abortSignal.reason
264
299
  };
265
- const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
266
- const peerId = message.isGroup ? channelTarget : message.senderNick;
267
- const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
268
- cfg: config,
269
- channel: CHANNEL_ID,
270
- accountId: account.accountId,
271
- peer: {
272
- kind: message.isGroup ? "group" : "direct",
273
- id: peerId
274
- }
275
- });
276
300
  const fromLabel = message.isGroup ? message.target : senderDisplay;
277
301
  const body = buildEnvelope({
278
302
  channel: "IRC",
@@ -282,7 +306,8 @@ async function handleIrcInbound(params) {
282
306
  });
283
307
  const groupSystemPrompt = normalizeOptionalString(groupMatch.groupConfig?.systemPrompt);
284
308
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
285
- const ctxPayload = buildChannelInboundEventContext({
309
+ const ctxPayload = core.channel.inbound.buildContext({
310
+ channelIngress: access,
286
311
  channel: CHANNEL_ID,
287
312
  accountId: route.accountId,
288
313
  messageId: message.messageId,
@@ -430,14 +455,16 @@ function decodeIrcIngressPayload(payload, claimedId) {
430
455
  }
431
456
  function inspectIrcIngress(raw, phase) {
432
457
  try {
458
+ const projection = inspectRawPrivmsg(raw.body.rawLine);
459
+ if (phase === "claim") raw.claimedProjection = projection;
433
460
  return {
434
- eventId: raw.eventId,
435
- laneKey: inspectRawPrivmsg(raw.rawLine).laneKey
461
+ eventId: raw.body.eventId,
462
+ laneKey: projection.laneKey
436
463
  };
437
464
  } catch (error) {
438
465
  if (phase === "admission" && error instanceof IrcIngressPayloadError) return {
439
- eventId: raw.eventId,
440
- laneKey: `invalid:${raw.eventId}`
466
+ eventId: raw.body.eventId,
467
+ laneKey: `invalid:${raw.body.eventId}`
441
468
  };
442
469
  throw error;
443
470
  }
@@ -454,8 +481,8 @@ function createIrcIngressMonitor(options) {
454
481
  inspect: (raw, context) => inspectIrcIngress(raw, context.phase),
455
482
  payload: {
456
483
  version: IRC_INGRESS_PAYLOAD_VERSION,
457
- serialize: (raw) => raw,
458
- deserialize: (body) => body,
484
+ serialize: (raw) => raw.body,
485
+ deserialize: (body) => ({ body }),
459
486
  encode: ({ body }) => ({
460
487
  version: IRC_INGRESS_PAYLOAD_VERSION,
461
488
  ...body
@@ -465,13 +492,13 @@ function createIrcIngressMonitor(options) {
465
492
  },
466
493
  deliver: (raw, lifecycle, claim) => {
467
494
  const message = {
468
- ...inspectRawPrivmsg(raw.rawLine).message,
495
+ ...raw.claimedProjection.message,
469
496
  messageId: claim.id,
470
- timestamp: raw.receivedAt
497
+ timestamp: raw.body.receivedAt
471
498
  };
472
499
  return options.dispatch(message, lifecycle, {
473
- connectedNick: raw.connectedNick.trim(),
474
- connectionEpoch: raw.connectionEpoch.trim()
500
+ connectedNick: raw.body.connectedNick.trim(),
501
+ connectionEpoch: raw.body.connectionEpoch.trim()
475
502
  });
476
503
  },
477
504
  pollIntervalMs: options.pollIntervalMs ?? IRC_INGRESS_POLL_INTERVAL_MS,
@@ -501,13 +528,13 @@ function createIrcIngressMonitor(options) {
501
528
  const receivedAt = Date.now();
502
529
  const normalizedNick = connectedNick.trim();
503
530
  if (!normalizedNick) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress connected nickname is required."));
504
- return monitor.admit({
531
+ return monitor.admit({ body: {
505
532
  eventId,
506
533
  rawLine,
507
534
  receivedAt,
508
535
  connectionEpoch: epoch,
509
536
  connectedNick: normalizedNick
510
- }, { receivedAt }).then(() => void 0);
537
+ } }, { receivedAt }).then(() => void 0);
511
538
  }
512
539
  };
513
540
  },
@@ -592,6 +619,7 @@ async function monitorIrcProvider(opts) {
592
619
  });
593
620
  function scheduleReconnect() {
594
621
  if (stopped || monitorAbort.signal.aborted || reconnectTimer) return;
622
+ opts.statusSink?.({ lifecycle: "recovering" });
595
623
  reconnectTimer = setTimeout(() => {
596
624
  reconnectTimer = null;
597
625
  connect().catch((error) => {
@@ -648,6 +676,7 @@ async function monitorIrcProvider(opts) {
648
676
  return;
649
677
  }
650
678
  ingress.start();
679
+ opts.statusSink?.(channelReadyPatch());
651
680
  logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
652
681
  }
653
682
  try {
@@ -59,6 +59,7 @@ const IrcNickServSchema = z.object({
59
59
  const IrcChannelConfigSchema = buildChannelConfigSchema(buildMultiAccountChannelSchema(z.object({
60
60
  name: z.string().optional(),
61
61
  enabled: z.boolean().optional(),
62
+ configWrites: z.boolean().optional(),
62
63
  dangerouslyAllowNameMatching: z.boolean().optional(),
63
64
  host: z.string().optional(),
64
65
  port: z.number().int().min(1).max(65535).optional(),
@@ -1,4 +1,4 @@
1
- import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor";
1
+ import { defineChannelAliasMigration } from "openclaw/plugin-sdk/runtime-doctor-migrations";
2
2
  //#region extensions/irc/doctor-contract-api.ts
3
3
  const streamingAliasMigration = defineChannelAliasMigration({
4
4
  channelId: "irc",
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "id": "irc",
3
+ "doctorContract": {
4
+ "configRepair": true
5
+ },
3
6
  "activation": {
4
7
  "onStartup": false
5
8
  },
@@ -23,6 +26,9 @@
23
26
  "enabled": {
24
27
  "type": "boolean"
25
28
  },
29
+ "configWrites": {
30
+ "type": "boolean"
31
+ },
26
32
  "dangerouslyAllowNameMatching": {
27
33
  "type": "boolean"
28
34
  },
@@ -346,6 +352,9 @@
346
352
  "enabled": {
347
353
  "type": "boolean"
348
354
  },
355
+ "configWrites": {
356
+ "type": "boolean"
357
+ },
349
358
  "dangerouslyAllowNameMatching": {
350
359
  "type": "boolean"
351
360
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/irc",
3
- "version": "2026.7.2-beta.7",
3
+ "version": "2026.8.1-beta.3",
4
4
  "description": "OpenClaw IRC channel plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -111,16 +111,20 @@
111
111
  "cli": {
112
112
  "flags": "--use-env",
113
113
  "description": "Use IRC environment configuration"
114
- }
114
+ },
115
+ "envVars": [
116
+ "IRC_HOST",
117
+ "IRC_NICK"
118
+ ]
115
119
  }
116
120
  ]
117
121
  }
118
122
  },
119
123
  "compat": {
120
- "pluginApi": ">=2026.7.2-beta.7"
124
+ "pluginApi": ">=2026.8.1-beta.3"
121
125
  },
122
126
  "build": {
123
- "openclawVersion": "2026.7.2-beta.7",
127
+ "openclawVersion": "2026.8.1-beta.3",
124
128
  "bundledDist": false
125
129
  },
126
130
  "release": {
@@ -145,7 +149,7 @@
145
149
  "README.md"
146
150
  ],
147
151
  "peerDependencies": {
148
- "openclaw": ">=2026.7.2-beta.7"
152
+ "openclaw": ">=2026.8.1-beta.3"
149
153
  },
150
154
  "peerDependenciesMeta": {
151
155
  "openclaw": {