@openclaw/irc 2026.7.2-beta.2 → 2026.7.2-beta.4

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 { g as resolveIrcAccount, h as resolveDefaultIrcAccountId, m as listIrcAccountIds, n as ircSetupWizard, p as listEnabledIrcAccounts, r as ircSetupAdapter, t as ircPlugin } from "./channel-DivPJINh.js";
1
+ import { _ as resolveIrcAccount, g as resolveDefaultIrcAccountId, h as listIrcAccountIds, m as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin } from "./channel-BuxKUEja.js";
2
2
  import { n as setIrcRuntime } from "./runtime-xy-FcjJC.js";
3
3
  export { ircPlugin, ircSetupAdapter, ircSetupWizard, listEnabledIrcAccounts, listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount, setIrcRuntime };
@@ -1,10 +1,10 @@
1
1
  import { t as IrcChannelConfigSchema } from "./config-schema-BPoye3--.js";
2
2
  import { t as getIrcRuntime } from "./runtime-xy-FcjJC.js";
3
- import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-PJHaoTSe.js";
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
- import { buildChannelOutboundSessionRoute, createChatChannelPlugin, parseOptionalDelimitedEntries, tryReadSecretFileSync } from "openclaw/plugin-sdk/channel-core";
7
+ import { buildChannelOutboundSessionRoute, createChatChannelPlugin, parseOptionalDelimitedEntries } from "openclaw/plugin-sdk/channel-core";
8
8
  import { buildMutableAllowEntryDetector, collectStandardAllowlistLists, composeAccountWarningCollectors, createAllowlistProviderOpenWarningCollector, 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";
@@ -12,18 +12,19 @@ import { buildBaseChannelStatusSummary, createComputedAccountStatusAdapter, crea
12
12
  import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$2, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
13
13
  import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
14
14
  import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
15
+ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
15
16
  import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
16
17
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, normalizeStringifiedOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
17
- import { createAccountStatusSink, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, sanitizeForPlainText } from "openclaw/plugin-sdk/channel-outbound";
18
+ import { createAccountStatusSink, createMessageReceiptFromOutboundResults, defineChannelMessageAdapter, runPassiveAccountLifecycle, sanitizeForPlainText } from "openclaw/plugin-sdk/channel-outbound";
18
19
  import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";
19
20
  import { chunkTextForOutbound, convertMarkdownTables, sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
20
- import { runStoppablePassiveMonitor } from "openclaw/plugin-sdk/extension-shared";
21
21
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
22
22
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
23
23
  import net from "node:net";
24
24
  import tls from "node:tls";
25
25
  import { withTimeout } from "openclaw/plugin-sdk/security-runtime";
26
26
  import { randomUUID } from "node:crypto";
27
+ import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
27
28
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
28
29
  import { applyAccountNameToChannelSection, createAllowFromSection, createPromptParsedAllowFromForAccount, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, createTopLevelChannelAllowFromSetter, createTopLevelChannelDmPolicySetter, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
29
30
  //#region extensions/irc/src/accounts.ts
@@ -66,11 +67,25 @@ function resolvePassword(accountId, merged) {
66
67
  };
67
68
  }
68
69
  if (merged.passwordFile?.trim()) {
69
- const filePassword = tryReadSecretFileSync(merged.passwordFile, "IRC password file", { rejectSymlink: true });
70
+ let diagnostic;
71
+ const filePassword = tryReadSecretFileSync(merged.passwordFile, "IRC password file", {
72
+ rejectSymlink: true,
73
+ credentialDiagnostic: {
74
+ configPath: `channels.irc.accounts.${accountId}.passwordFile`,
75
+ report: (value) => {
76
+ diagnostic = value;
77
+ }
78
+ }
79
+ });
70
80
  if (filePassword) return {
71
81
  password: filePassword,
72
82
  source: "passwordFile"
73
83
  };
84
+ return {
85
+ password: "",
86
+ source: "passwordFile",
87
+ diagnostic
88
+ };
74
89
  }
75
90
  const configPassword = normalizeResolvedSecretInputString({
76
91
  value: merged.password,
@@ -94,13 +109,25 @@ function resolveNickServConfig(accountId, nickserv) {
94
109
  value: base.password,
95
110
  path: `channels.irc.accounts.${accountId}.nickserv.password`
96
111
  }) || envPassword || "";
97
- if (!resolvedPassword && passwordFile) resolvedPassword = tryReadSecretFileSync(passwordFile, "IRC NickServ password file", { rejectSymlink: true }) ?? "";
112
+ let diagnostic;
113
+ if (!resolvedPassword && passwordFile) resolvedPassword = tryReadSecretFileSync(passwordFile, "IRC NickServ password file", {
114
+ rejectSymlink: true,
115
+ credentialDiagnostic: {
116
+ configPath: `channels.irc.accounts.${accountId}.nickserv.passwordFile`,
117
+ report: (value) => {
118
+ diagnostic = value;
119
+ }
120
+ }
121
+ }) ?? "";
98
122
  return {
99
- ...base,
100
- service: normalizeOptionalString(base.service),
101
- passwordFile: passwordFile || void 0,
102
- password: resolvedPassword || void 0,
103
- registerEmail: base.registerEmail?.trim() || envRegisterEmail || void 0
123
+ config: {
124
+ ...base,
125
+ service: normalizeOptionalString(base.service),
126
+ passwordFile: passwordFile || void 0,
127
+ password: resolvedPassword || void 0,
128
+ registerEmail: base.registerEmail?.trim() || envRegisterEmail || void 0
129
+ },
130
+ diagnostic
104
131
  };
105
132
  }
106
133
  function resolveIrcAccount(params) {
@@ -119,7 +146,8 @@ function resolveIrcAccount(params) {
119
146
  const username = (merged.username?.trim() || (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_USERNAME?.trim() : "") || nick || "openclaw").trim();
120
147
  const realname = (merged.realname?.trim() || (accountId === DEFAULT_ACCOUNT_ID ? process.env.IRC_REALNAME?.trim() : "") || "OpenClaw").trim();
121
148
  const passwordResolution = resolvePassword(accountId, merged);
122
- const nickserv = resolveNickServConfig(accountId, merged.nickserv);
149
+ const nickservResolution = resolveNickServConfig(accountId, merged.nickserv);
150
+ const diagnostics = [passwordResolution.diagnostic, nickservResolution.diagnostic].filter((diagnostic) => Boolean(diagnostic));
123
151
  const config = {
124
152
  ...merged,
125
153
  channels: merged.channels ?? envChannels,
@@ -129,7 +157,7 @@ function resolveIrcAccount(params) {
129
157
  nick,
130
158
  username,
131
159
  realname,
132
- nickserv
160
+ nickserv: nickservResolution.config
133
161
  };
134
162
  return {
135
163
  accountId,
@@ -144,6 +172,8 @@ function resolveIrcAccount(params) {
144
172
  realname,
145
173
  password: passwordResolution.password,
146
174
  passwordSource: passwordResolution.source,
175
+ tokenStatus: diagnostics.length > 0 ? "configured_unavailable" : passwordResolution.password || nickservResolution.config.password ? "available" : "missing",
176
+ ...diagnostics.length > 0 ? { credentialDiagnostics: diagnostics } : {},
147
177
  config
148
178
  };
149
179
  };
@@ -172,7 +202,7 @@ const collectIrcMutableAllowlistWarnings = createDangerousNameMatchingMutableAll
172
202
  });
173
203
  //#endregion
174
204
  //#region extensions/irc/src/gateway.ts
175
- const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-Dg0LFlsh.js"));
205
+ const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-BrNJqIB_.js"));
176
206
  async function startIrcGatewayAccount(ctx) {
177
207
  const account = ctx.account;
178
208
  const statusSink = createAccountStatusSink({
@@ -182,7 +212,7 @@ async function startIrcGatewayAccount(ctx) {
182
212
  if (!account.configured) throw new Error(`IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`);
183
213
  ctx.log?.info?.(`[${account.accountId}] starting IRC provider (${account.host}:${account.port}${account.tls ? " tls" : ""})`);
184
214
  const { monitorIrcProvider } = await loadIrcChannelRuntime$1();
185
- await runStoppablePassiveMonitor({
215
+ await runPassiveAccountLifecycle({
186
216
  abortSignal: ctx.abortSignal,
187
217
  start: async () => await monitorIrcProvider({
188
218
  accountId: account.accountId,
@@ -190,7 +220,10 @@ async function startIrcGatewayAccount(ctx) {
190
220
  runtime: ctx.runtime,
191
221
  abortSignal: ctx.abortSignal,
192
222
  statusSink
193
- })
223
+ }),
224
+ stop: async (monitor) => {
225
+ await monitor.stop();
226
+ }
194
227
  });
195
228
  }
196
229
  //#endregion
@@ -524,7 +557,7 @@ async function connectIrcClient(options) {
524
557
  if (line.command === "PRIVMSG") {
525
558
  const targetParam = line.params[0];
526
559
  const target = targetParam ? targetParam.trim() : "";
527
- const text = line.trailing != null ? line.trailing : "";
560
+ const text = line.trailing ?? line.params[1] ?? "";
528
561
  const prefix = parseIrcPrefix(line.prefix);
529
562
  const senderNick = prefix.nick ? prefix.nick.trim() : "";
530
563
  if (!target || !senderNick || !text.trim()) continue;
@@ -532,6 +565,7 @@ async function connectIrcClient(options) {
532
565
  senderNick,
533
566
  senderUser: prefix.user ? prefix.user.trim() : void 0,
534
567
  senderHost: prefix.host ? prefix.host.trim() : void 0,
568
+ connectedNick: currentNick,
535
569
  target,
536
570
  text,
537
571
  rawLine
@@ -909,6 +943,7 @@ function setIrcGroupAccess(cfg, accountId, policy, entries, normalizeGroupEntry)
909
943
  });
910
944
  }
911
945
  const ircSetupAdapter = {
946
+ singleAccountKeysToMove: ["password"],
912
947
  resolveAccountId: ({ accountId }) => normalizeAccountId$1(accountId),
913
948
  applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
914
949
  cfg,
@@ -945,6 +980,75 @@ const ircSetupAdapter = {
945
980
  });
946
981
  }
947
982
  };
983
+ const ircSetupContract = defineChannelSetupContract({
984
+ fields: {
985
+ host: {
986
+ kind: "string",
987
+ cli: {
988
+ flags: "--host <host>",
989
+ description: "IRC server host"
990
+ }
991
+ },
992
+ port: {
993
+ kind: "string",
994
+ cli: {
995
+ flags: "--port <port>",
996
+ description: "IRC server port"
997
+ }
998
+ },
999
+ tls: {
1000
+ kind: "boolean",
1001
+ cli: {
1002
+ flags: "--tls",
1003
+ description: "Use TLS for IRC"
1004
+ }
1005
+ },
1006
+ nick: {
1007
+ kind: "string",
1008
+ cli: {
1009
+ flags: "--nick <nick>",
1010
+ description: "IRC nickname"
1011
+ }
1012
+ },
1013
+ username: {
1014
+ kind: "string",
1015
+ cli: {
1016
+ flags: "--username <name>",
1017
+ description: "IRC username"
1018
+ }
1019
+ },
1020
+ realname: {
1021
+ kind: "string",
1022
+ cli: {
1023
+ flags: "--realname <name>",
1024
+ description: "IRC real name"
1025
+ }
1026
+ },
1027
+ channels: {
1028
+ kind: "string-list",
1029
+ cli: {
1030
+ flags: "--channels <names>",
1031
+ description: "IRC channels"
1032
+ }
1033
+ },
1034
+ password: {
1035
+ kind: "string",
1036
+ sensitive: true,
1037
+ cli: {
1038
+ flags: "--password <password>",
1039
+ description: "IRC server password"
1040
+ }
1041
+ },
1042
+ useEnv: {
1043
+ kind: "boolean",
1044
+ cli: {
1045
+ flags: "--use-env",
1046
+ description: "Use IRC environment configuration"
1047
+ }
1048
+ }
1049
+ },
1050
+ legacyAdapter: ircSetupAdapter
1051
+ });
948
1052
  //#endregion
949
1053
  //#region extensions/irc/src/setup-surface.ts
950
1054
  const t = createSetupTranslator();
@@ -1310,7 +1414,7 @@ const meta = {
1310
1414
  systemImage: "number",
1311
1415
  markdownCapable: true
1312
1416
  };
1313
- const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-Dg0LFlsh.js"));
1417
+ const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-BrNJqIB_.js"));
1314
1418
  function normalizePairingTarget(raw) {
1315
1419
  const normalized = normalizeIrcAllowEntry(raw);
1316
1420
  if (!normalized) return "";
@@ -1383,6 +1487,7 @@ const ircPlugin = createChatChannelPlugin({
1383
1487
  quickstartAllowFrom: true
1384
1488
  },
1385
1489
  setup: ircSetupAdapter,
1490
+ setupContract: ircSetupContract,
1386
1491
  setupWizard: ircSetupWizard,
1387
1492
  capabilities: {
1388
1493
  chatTypes: ["direct", "group"],
@@ -1403,7 +1508,8 @@ const ircPlugin = createChatChannelPlugin({
1403
1508
  port: account.port,
1404
1509
  tls: account.tls,
1405
1510
  nick: account.nick,
1406
- passwordSource: account.passwordSource
1511
+ passwordSource: account.passwordSource,
1512
+ tokenStatus: account.tokenStatus
1407
1513
  }
1408
1514
  })
1409
1515
  },
@@ -1510,7 +1616,8 @@ const ircPlugin = createChatChannelPlugin({
1510
1616
  port: account.port,
1511
1617
  tls: account.tls,
1512
1618
  nick: account.nick,
1513
- passwordSource: account.passwordSource
1619
+ passwordSource: account.passwordSource,
1620
+ tokenStatus: account.tokenStatus
1514
1621
  }
1515
1622
  })
1516
1623
  }),
@@ -1558,4 +1665,4 @@ const ircPlugin = createChatChannelPlugin({
1558
1665
  }
1559
1666
  });
1560
1667
  //#endregion
1561
- export { resolveIrcGroupRequireMention as a, isChannelTarget as c, connectIrcClient as d, makeIrcMessageId as f, resolveIrcAccount as g, resolveDefaultIrcAccountId as h, resolveIrcGroupMatch as i, normalizeIrcAllowEntry as l, listIrcAccountIds as m, ircSetupWizard as n, sendMessageIrc as o, listEnabledIrcAccounts as p, ircSetupAdapter as r, buildIrcAllowlistCandidates as s, ircPlugin as t, buildIrcConnectOptions as u };
1668
+ export { resolveIrcAccount as _, resolveIrcGroupRequireMention as a, isChannelTarget as c, connectIrcClient as d, parseIrcLine as f, resolveDefaultIrcAccountId as g, listIrcAccountIds as h, resolveIrcGroupMatch as i, normalizeIrcAllowEntry as l, listEnabledIrcAccounts as m, ircSetupWizard as n, sendMessageIrc as o, parseIrcPrefix as p, ircSetupAdapter as r, buildIrcAllowlistCandidates as s, ircPlugin as t, buildIrcConnectOptions as u };
@@ -1,2 +1,2 @@
1
- import { t as ircPlugin } from "./channel-DivPJINh.js";
1
+ import { t as ircPlugin } from "./channel-BuxKUEja.js";
2
2
  export { ircPlugin };
@@ -1,13 +1,13 @@
1
- import { a as resolveIrcGroupRequireMention, c as isChannelTarget, d as connectIrcClient, f as makeIrcMessageId, g as resolveIrcAccount, i as resolveIrcGroupMatch, l as normalizeIrcAllowEntry, o as sendMessageIrc, s as buildIrcAllowlistCandidates, u as buildIrcConnectOptions } from "./channel-DivPJINh.js";
1
+ import { _ as resolveIrcAccount, a as resolveIrcGroupRequireMention, c as isChannelTarget, d as connectIrcClient, f as parseIrcLine, i as resolveIrcGroupMatch, l as normalizeIrcAllowEntry, o as sendMessageIrc, p as parseIrcPrefix, s as buildIrcAllowlistCandidates, u as buildIrcConnectOptions } from "./channel-BuxKUEja.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
- import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
4
+ import { bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
5
+ import { randomUUID } from "node:crypto";
5
6
  import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
6
- import { logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
7
+ import { buildChannelInboundEventContext, logInboundDrop, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
7
8
  import { channelIngressRoutes, createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
8
9
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
9
10
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
10
- import { resolveInboundRouteEnvelopeBuilderWithRuntime } from "openclaw/plugin-sdk/inbound-envelope";
11
11
  import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
12
12
  import { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
13
13
  //#region extensions/irc/src/inbound.ts
@@ -121,7 +121,7 @@ async function deliverIrcReply(params) {
121
121
  });
122
122
  }
123
123
  async function handleIrcInbound(params) {
124
- const { message, account, config, runtime, connectedNick, statusSink } = params;
124
+ const { message, account, config, runtime, connectedNick, statusSink, turnAdoptionLifecycle } = params;
125
125
  const core = getIrcRuntime();
126
126
  const pairing = createChannelPairingController({
127
127
  core,
@@ -129,7 +129,11 @@ async function handleIrcInbound(params) {
129
129
  accountId: account.accountId
130
130
  });
131
131
  const rawBody = message.text?.trim() ?? "";
132
- if (!rawBody) return;
132
+ if (!rawBody) return { kind: "completed" };
133
+ if (turnAdoptionLifecycle?.abortSignal.aborted) return {
134
+ kind: "failed-retryable",
135
+ error: turnAdoptionLifecycle.abortSignal.reason
136
+ };
133
137
  statusSink?.({ lastInboundAt: message.timestamp });
134
138
  const senderDisplay = message.senderHost ? `${message.senderNick}!${message.senderUser ?? "?"}@${message.senderHost}` : message.senderNick;
135
139
  const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
@@ -229,11 +233,11 @@ async function handleIrcInbound(params) {
229
233
  }
230
234
  });
231
235
  runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
232
- return;
236
+ return { kind: "completed" };
233
237
  }
234
238
  if (access.ingress.admission === "skip") {
235
239
  runtime.log?.(`irc: drop channel ${message.target} (missing-mention)`);
236
- return;
240
+ return { kind: "completed" };
237
241
  }
238
242
  if (access.ingress.admission !== "dispatch") {
239
243
  if (message.isGroup && access.ingress.decisiveGateId === "command" && access.commandAccess.shouldBlockControlCommand) {
@@ -243,29 +247,31 @@ async function handleIrcInbound(params) {
243
247
  reason: "control command (unauthorized)",
244
248
  target: senderDisplay
245
249
  });
246
- return;
250
+ return { kind: "completed" };
247
251
  }
248
252
  if (message.isGroup) if (access.routeAccess.reason === "channel_not_allowlisted") runtime.log?.(`irc: drop channel ${message.target} (not allowlisted)`);
249
253
  else if (access.routeAccess.reason === "channel_disabled") runtime.log?.(`irc: drop channel ${message.target} (disabled)`);
250
254
  else runtime.log?.(`irc: drop group sender ${senderDisplay} (policy=${groupPolicy})`);
251
255
  else runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
252
- return;
256
+ return { kind: "completed" };
253
257
  }
258
+ if (turnAdoptionLifecycle?.abortSignal.aborted) return {
259
+ kind: "failed-retryable",
260
+ error: turnAdoptionLifecycle.abortSignal.reason
261
+ };
254
262
  const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
255
263
  const peerId = message.isGroup ? channelTarget : message.senderNick;
256
- const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
264
+ const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
257
265
  cfg: config,
258
266
  channel: CHANNEL_ID,
259
267
  accountId: account.accountId,
260
268
  peer: {
261
269
  kind: message.isGroup ? "group" : "direct",
262
270
  id: peerId
263
- },
264
- runtime: core.channel,
265
- sessionStore: config.session?.store
271
+ }
266
272
  });
267
273
  const fromLabel = message.isGroup ? message.target : senderDisplay;
268
- const { storePath, body } = buildEnvelope({
274
+ const body = buildEnvelope({
269
275
  channel: "IRC",
270
276
  from: fromLabel,
271
277
  timestamp: message.timestamp,
@@ -273,39 +279,74 @@ async function handleIrcInbound(params) {
273
279
  });
274
280
  const groupSystemPrompt = normalizeOptionalString(groupMatch.groupConfig?.systemPrompt);
275
281
  const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
276
- const ctxPayload = core.channel.reply.finalizeInboundContext({
277
- Body: body,
278
- RawBody: rawBody,
279
- CommandBody: rawBody,
280
- From: message.isGroup ? `channel:${channelTarget}` : `irc:${senderDisplay}`,
281
- To: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
282
- SessionKey: route.sessionKey,
283
- AccountId: route.accountId,
284
- ChatType: message.isGroup ? "group" : "direct",
285
- ConversationLabel: fromLabel,
286
- SenderName: message.senderNick || void 0,
287
- SenderId: senderDisplay,
288
- GroupSubject: message.isGroup ? message.target : void 0,
289
- GroupSystemPrompt: message.isGroup ? groupSystemPrompt : void 0,
290
- Provider: CHANNEL_ID,
291
- Surface: CHANNEL_ID,
292
- WasMentioned: message.isGroup ? wasMentioned : void 0,
293
- MessageSid: message.messageId,
294
- Timestamp: message.timestamp,
295
- OriginatingChannel: CHANNEL_ID,
296
- OriginatingTo: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
297
- CommandAuthorized: commandAuthorized
282
+ const ctxPayload = buildChannelInboundEventContext({
283
+ channel: CHANNEL_ID,
284
+ accountId: route.accountId,
285
+ messageId: message.messageId,
286
+ timestamp: message.timestamp,
287
+ from: message.isGroup ? `channel:${channelTarget}` : `irc:${senderDisplay}`,
288
+ sender: {
289
+ id: senderDisplay,
290
+ name: message.senderNick || void 0
291
+ },
292
+ conversation: {
293
+ kind: message.isGroup ? "group" : "direct",
294
+ id: peerId,
295
+ label: fromLabel
296
+ },
297
+ route: {
298
+ agentId: route.agentId,
299
+ dmScope: route.dmScope,
300
+ accountId: route.accountId,
301
+ routeSessionKey: route.sessionKey
302
+ },
303
+ reply: {
304
+ to: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
305
+ originatingTo: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`
306
+ },
307
+ message: {
308
+ body,
309
+ bodyForAgent: rawBody,
310
+ rawBody,
311
+ commandBody: rawBody
312
+ },
313
+ access: {
314
+ commands: { authorized: commandAuthorized },
315
+ mentions: {
316
+ canDetectMention: message.isGroup,
317
+ wasMentioned
318
+ }
319
+ },
320
+ extra: {
321
+ GroupSubject: message.isGroup ? message.target : void 0,
322
+ GroupSystemPrompt: message.isGroup ? groupSystemPrompt : void 0
323
+ }
298
324
  });
299
- await core.channel.inbound.dispatchReply({
325
+ const ingressState = { handoff: "none" };
326
+ const trackedIngressLifecycle = turnAdoptionLifecycle ? {
327
+ ...turnAdoptionLifecycle,
328
+ onAdopted: async () => {
329
+ ingressState.handoff = "adopted";
330
+ await turnAdoptionLifecycle.onAdopted();
331
+ },
332
+ onDeferred: () => {
333
+ ingressState.handoff = "deferred";
334
+ turnAdoptionLifecycle.onDeferred();
335
+ },
336
+ onAbandoned: async () => {
337
+ ingressState.handoff = "abandoned";
338
+ await turnAdoptionLifecycle.onAbandoned();
339
+ }
340
+ } : void 0;
341
+ await core.channel.inbound.dispatch({
300
342
  cfg: config,
301
343
  channel: CHANNEL_ID,
302
344
  accountId: account.accountId,
303
- agentId: route.agentId,
304
- routeSessionKey: route.sessionKey,
305
- storePath,
345
+ route: {
346
+ agentId: route.agentId,
347
+ sessionKey: route.sessionKey
348
+ },
306
349
  ctxPayload,
307
- recordInboundSession: core.channel.session.recordInboundSession,
308
- dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
309
350
  delivery: {
310
351
  deliver: async (payload) => {
311
352
  await deliverIrcReply({
@@ -323,6 +364,7 @@ async function handleIrcInbound(params) {
323
364
  },
324
365
  replyPipeline: {},
325
366
  replyOptions: {
367
+ ...trackedIngressLifecycle ? bindIngressLifecycleToReplyOptions(trackedIngressLifecycle) : {},
326
368
  skillFilter: groupMatch.groupConfig?.skills,
327
369
  disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
328
370
  },
@@ -330,23 +372,162 @@ async function handleIrcInbound(params) {
330
372
  runtime.error?.(`irc: failed updating session meta: ${String(err)}`);
331
373
  } }
332
374
  });
375
+ if (turnAdoptionLifecycle?.abortSignal.aborted && ingressState.handoff === "none") return {
376
+ kind: "failed-retryable",
377
+ error: turnAdoptionLifecycle.abortSignal.reason
378
+ };
379
+ if (turnAdoptionLifecycle && ingressState.handoff === "none") {
380
+ await turnAdoptionLifecycle.onAdopted();
381
+ ingressState.handoff = "adopted";
382
+ }
383
+ return ingressState.handoff === "deferred" || ingressState.handoff === "abandoned" ? { kind: "deferred" } : { kind: "completed" };
333
384
  }
334
385
  //#endregion
335
- //#region extensions/irc/src/monitor.ts
336
- const IRC_MONITOR_RECONNECT_DELAY_MS = 1e3;
337
- function resolveIrcInboundTarget(params) {
338
- const rawTarget = params.target;
339
- if (isChannelTarget(rawTarget)) return {
340
- isGroup: true,
341
- target: rawTarget,
342
- rawTarget
386
+ //#region extensions/irc/src/irc-ingress.ts
387
+ const IRC_INGRESS_PAYLOAD_VERSION = 1;
388
+ const IRC_INGRESS_POLL_INTERVAL_MS = 1e3;
389
+ const IRC_INGRESS_PRUNE_INTERVAL_MS = 3600 * 1e3;
390
+ const IRC_INGRESS_TOMBSTONE_TTL_MS = 720 * 60 * 60 * 1e3;
391
+ const IRC_INGRESS_TOMBSTONE_MAX_ENTRIES = 1e3;
392
+ var IrcIngressPayloadError = class extends Error {
393
+ constructor(message) {
394
+ super(message);
395
+ this.name = "IrcIngressPayloadError";
396
+ }
397
+ };
398
+ function inspectRawPrivmsg(rawLine) {
399
+ const line = parseIrcLine(rawLine);
400
+ if (!line || line.command !== "PRIVMSG") throw new IrcIngressPayloadError("IRC ingress row is not a PRIVMSG line.");
401
+ const rawTarget = line.params[0]?.trim() ?? "";
402
+ const text = line.trailing ?? line.params[1] ?? "";
403
+ const prefix = parseIrcPrefix(line.prefix);
404
+ const senderNick = prefix.nick?.trim() ?? "";
405
+ if (!rawTarget || !senderNick || !text.trim()) throw new IrcIngressPayloadError("IRC PRIVMSG line is missing target, sender, or text.");
406
+ const isGroup = isChannelTarget(rawTarget);
407
+ const target = isGroup ? rawTarget : senderNick;
408
+ const lanePeer = normalizeLowercaseStringOrEmpty(target);
409
+ return {
410
+ laneKey: `${isGroup ? "channel" : "direct"}:${lanePeer}`,
411
+ message: {
412
+ target,
413
+ rawTarget,
414
+ senderNick,
415
+ senderUser: prefix.user?.trim() || void 0,
416
+ senderHost: prefix.host?.trim() || void 0,
417
+ text,
418
+ isGroup
419
+ }
420
+ };
421
+ }
422
+ function decodeIrcIngressPayload(payload, claimedId) {
423
+ if (!payload || typeof payload !== "object" || Array.isArray(payload) || typeof payload.eventId !== "string" || !Number.isSafeInteger(payload.receivedAt) || (payload.receivedAt ?? 0) <= 0 || typeof payload.connectionEpoch !== "string" || !payload.connectionEpoch?.trim() || typeof payload.connectedNick !== "string" || !payload.connectedNick?.trim() || typeof payload.rawLine !== "string") throw new IrcIngressPayloadError(`IRC ingress row ${claimedId} has invalid metadata.`);
424
+ const validPayload = payload;
425
+ return {
426
+ version: validPayload.version,
427
+ body: {
428
+ eventId: validPayload.eventId,
429
+ receivedAt: validPayload.receivedAt,
430
+ connectionEpoch: validPayload.connectionEpoch,
431
+ connectedNick: validPayload.connectedNick,
432
+ rawLine: validPayload.rawLine
433
+ }
343
434
  };
435
+ }
436
+ function inspectIrcIngress(raw, phase) {
437
+ try {
438
+ return {
439
+ eventId: raw.eventId,
440
+ laneKey: inspectRawPrivmsg(raw.rawLine).laneKey
441
+ };
442
+ } catch (error) {
443
+ if (phase === "admission" && error instanceof IrcIngressPayloadError) return {
444
+ eventId: raw.eventId,
445
+ laneKey: `invalid:${raw.eventId}`
446
+ };
447
+ throw error;
448
+ }
449
+ }
450
+ function resolveIrcIngressNonRetryableFailure(error) {
451
+ return error instanceof IrcIngressPayloadError ? {
452
+ reason: "invalid-event",
453
+ message: error.message
454
+ } : null;
455
+ }
456
+ function createIrcIngressMonitor(options) {
457
+ const monitor = createChannelIngressMonitor({
458
+ queue: options.queue ?? (() => getIrcRuntime().state.openChannelIngressQueue({ accountId: options.accountId })),
459
+ inspect: (raw, context) => inspectIrcIngress(raw, context.phase),
460
+ payload: {
461
+ version: IRC_INGRESS_PAYLOAD_VERSION,
462
+ serialize: (raw) => raw,
463
+ deserialize: (body) => body,
464
+ encode: ({ body }) => ({
465
+ version: IRC_INGRESS_PAYLOAD_VERSION,
466
+ ...body
467
+ }),
468
+ decode: (payload, { claim }) => decodeIrcIngressPayload(payload, claim.id),
469
+ createClaimError: (_kind, claim) => new IrcIngressPayloadError(`IRC ingress row ${claim.id} has invalid metadata.`)
470
+ },
471
+ deliver: (raw, lifecycle, claim) => {
472
+ const message = {
473
+ ...inspectRawPrivmsg(raw.rawLine).message,
474
+ messageId: claim.id,
475
+ timestamp: raw.receivedAt
476
+ };
477
+ return options.dispatch(message, lifecycle, {
478
+ connectedNick: raw.connectedNick.trim(),
479
+ connectionEpoch: raw.connectionEpoch.trim()
480
+ });
481
+ },
482
+ pollIntervalMs: options.pollIntervalMs ?? IRC_INGRESS_POLL_INTERVAL_MS,
483
+ retention: {
484
+ pruneIntervalMs: IRC_INGRESS_PRUNE_INTERVAL_MS,
485
+ completedTtlMs: IRC_INGRESS_TOMBSTONE_TTL_MS,
486
+ completedMaxEntries: IRC_INGRESS_TOMBSTONE_MAX_ENTRIES,
487
+ failedTtlMs: IRC_INGRESS_TOMBSTONE_TTL_MS,
488
+ failedMaxEntries: IRC_INGRESS_TOMBSTONE_MAX_ENTRIES
489
+ },
490
+ drain: {
491
+ resolveNonRetryableFailure: resolveIrcIngressNonRetryableFailure,
492
+ ...options.adoptionStallTimeoutMs === void 0 ? {} : { adoptionStallTimeoutMs: options.adoptionStallTimeoutMs },
493
+ onLog: (message) => options.runtime.log?.(`irc ${message}`)
494
+ },
495
+ createStoppedError: () => /* @__PURE__ */ new Error("IRC ingress is stopped."),
496
+ onError: (error) => options.runtime.error?.(`irc ingress drain failed: ${String(error)}`)
497
+ });
344
498
  return {
345
- isGroup: false,
346
- target: params.senderNick.trim() || rawTarget,
347
- rawTarget
499
+ openConnection: (connectionEpoch = randomUUID()) => {
500
+ const epoch = connectionEpoch.trim();
501
+ if (!epoch) throw new Error("IRC ingress connection epoch is required.");
502
+ let sequence = 0;
503
+ return {
504
+ connectionEpoch: epoch,
505
+ accept: (rawLine, connectedNick) => {
506
+ if (monitor.isStopped()) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress is stopped."));
507
+ sequence += 1;
508
+ const eventId = `local:${epoch}:${String(sequence).padStart(12, "0")}`;
509
+ const receivedAt = Date.now();
510
+ const normalizedNick = connectedNick.trim();
511
+ if (!normalizedNick) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress connected nickname is required."));
512
+ return monitor.admit({
513
+ eventId,
514
+ rawLine,
515
+ receivedAt,
516
+ connectionEpoch: epoch,
517
+ connectedNick: normalizedNick
518
+ }, { receivedAt }).then(() => void 0);
519
+ }
520
+ };
521
+ },
522
+ start: monitor.start,
523
+ pause: monitor.pause,
524
+ stop: monitor.stop,
525
+ waitForIdle: monitor.waitForIdle
348
526
  };
349
527
  }
528
+ //#endregion
529
+ //#region extensions/irc/src/monitor.ts
530
+ const IRC_MONITOR_RECONNECT_DELAY_MS = 1e3;
350
531
  async function monitorIrcProvider(opts) {
351
532
  const core = getIrcRuntime();
352
533
  const cfg = opts.config ?? core.config.current();
@@ -361,6 +542,8 @@ async function monitorIrcProvider(opts) {
361
542
  accountId: account.accountId
362
543
  });
363
544
  let client = null;
545
+ let activeConnectionEpoch = null;
546
+ let ingressPause = Promise.resolve();
364
547
  let reconnectTimer = null;
365
548
  let stopped = false;
366
549
  const monitorAbort = new AbortController();
@@ -373,6 +556,48 @@ async function monitorIrcProvider(opts) {
373
556
  removeAbortListener = () => opts.abortSignal?.removeEventListener("abort", forwardAbort);
374
557
  }
375
558
  }
559
+ const ingress = createIrcIngressMonitor({
560
+ accountId: account.accountId,
561
+ runtime,
562
+ ...opts.ingressQueue ? { queue: opts.ingressQueue } : {},
563
+ dispatch: async (message, turnAdoptionLifecycle, context) => {
564
+ const activeClient = client;
565
+ if (!activeClient || stopped || monitorAbort.signal.aborted) return {
566
+ kind: "failed-retryable",
567
+ error: /* @__PURE__ */ new Error("IRC transport disconnected before ingress dispatch.")
568
+ };
569
+ if (normalizeLowercaseStringOrEmpty(message.senderNick) === normalizeLowercaseStringOrEmpty(context.connectedNick)) return { kind: "completed" };
570
+ if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) {
571
+ logger.warn?.(`[${account.accountId}] dropping replayed IRC DM after the connection changed`);
572
+ return { kind: "completed" };
573
+ }
574
+ if (opts.onMessage) {
575
+ await opts.onMessage(message, activeClient);
576
+ return { kind: "completed" };
577
+ }
578
+ return await handleIrcInbound({
579
+ message,
580
+ account,
581
+ config: cfg,
582
+ runtime,
583
+ connectedNick: context.connectedNick,
584
+ turnAdoptionLifecycle,
585
+ sendReply: async (target, text) => {
586
+ const replyClient = client;
587
+ if (!replyClient || !replyClient.isReady() || stopped || monitorAbort.signal.aborted) throw new Error("IRC transport disconnected before reply send.");
588
+ if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) throw new Error("IRC connection changed before private reply send.");
589
+ replyClient.sendPrivmsg(target, text);
590
+ opts.statusSink?.({ lastOutboundAt: Date.now() });
591
+ core.channel.activity.record({
592
+ channel: "irc",
593
+ accountId: account.accountId,
594
+ direction: "outbound"
595
+ });
596
+ },
597
+ statusSink: opts.statusSink
598
+ });
599
+ }
600
+ });
376
601
  function scheduleReconnect() {
377
602
  if (stopped || monitorAbort.signal.aborted || reconnectTimer) return;
378
603
  reconnectTimer = setTimeout(() => {
@@ -387,6 +612,7 @@ async function monitorIrcProvider(opts) {
387
612
  }
388
613
  async function connect() {
389
614
  if (stopped || monitorAbort.signal.aborted) return;
615
+ const ingressConnection = ingress.openConnection();
390
616
  const nextClient = await connectIrcClient(buildIrcConnectOptions(account, {
391
617
  channels: account.config.channels,
392
618
  abortSignal: monitorAbort.signal,
@@ -401,54 +627,20 @@ async function monitorIrcProvider(opts) {
401
627
  },
402
628
  onDisconnect: () => {
403
629
  if (stopped || monitorAbort.signal.aborted) return;
630
+ ingressPause = ingress.pause();
631
+ if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
404
632
  client = null;
405
633
  logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${IRC_MONITOR_RECONNECT_DELAY_MS}ms`);
406
634
  scheduleReconnect();
407
635
  },
408
636
  onPrivmsg: async (event) => {
409
- if (!client) return;
410
- if (normalizeLowercaseStringOrEmpty(event.senderNick) === normalizeLowercaseStringOrEmpty(client.nick)) return;
411
- const inboundTarget = resolveIrcInboundTarget({
412
- target: event.target,
413
- senderNick: event.senderNick
414
- });
415
- const message = {
416
- messageId: makeIrcMessageId(),
417
- target: inboundTarget.target,
418
- rawTarget: inboundTarget.rawTarget,
419
- senderNick: event.senderNick,
420
- senderUser: event.senderUser,
421
- senderHost: event.senderHost,
422
- text: event.text,
423
- timestamp: Date.now(),
424
- isGroup: inboundTarget.isGroup
425
- };
637
+ await ingressConnection.accept(event.rawLine, event.connectedNick);
638
+ if (normalizeLowercaseStringOrEmpty(event.senderNick) === normalizeLowercaseStringOrEmpty(event.connectedNick)) return;
426
639
  core.channel.activity.record({
427
640
  channel: "irc",
428
641
  accountId: account.accountId,
429
642
  direction: "inbound",
430
- at: message.timestamp
431
- });
432
- if (opts.onMessage) {
433
- await opts.onMessage(message, client);
434
- return;
435
- }
436
- await handleIrcInbound({
437
- message,
438
- account,
439
- config: cfg,
440
- runtime,
441
- connectedNick: client.nick,
442
- sendReply: async (target, text) => {
443
- client?.sendPrivmsg(target, text);
444
- opts.statusSink?.({ lastOutboundAt: Date.now() });
445
- core.channel.activity.record({
446
- channel: "irc",
447
- accountId: account.accountId,
448
- direction: "outbound"
449
- });
450
- },
451
- statusSink: opts.statusSink
643
+ at: Date.now()
452
644
  });
453
645
  }
454
646
  }));
@@ -457,20 +649,40 @@ async function monitorIrcProvider(opts) {
457
649
  return;
458
650
  }
459
651
  client = nextClient;
652
+ activeConnectionEpoch = ingressConnection.connectionEpoch;
653
+ await ingressPause;
654
+ if (client !== nextClient || !nextClient.isReady()) {
655
+ if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
656
+ return;
657
+ }
658
+ ingress.start();
460
659
  logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
461
660
  }
462
- await connect();
463
- return { stop: () => {
464
- stopped = true;
661
+ try {
662
+ await connect();
663
+ } catch (error) {
465
664
  removeAbortListener?.();
466
665
  removeAbortListener = null;
467
- if (!monitorAbort.signal.aborted) monitorAbort.abort();
468
- if (reconnectTimer) {
469
- clearTimeout(reconnectTimer);
470
- reconnectTimer = null;
471
- }
472
- client?.quit("shutdown");
473
- client = null;
666
+ await ingress.stop();
667
+ throw error;
668
+ }
669
+ let stopTask;
670
+ return { stop: () => {
671
+ stopTask ??= (async () => {
672
+ stopped = true;
673
+ removeAbortListener?.();
674
+ removeAbortListener = null;
675
+ if (!monitorAbort.signal.aborted) monitorAbort.abort();
676
+ if (reconnectTimer) {
677
+ clearTimeout(reconnectTimer);
678
+ reconnectTimer = null;
679
+ }
680
+ client?.quit("shutdown");
681
+ client = null;
682
+ activeConnectionEpoch = null;
683
+ await ingress.stop();
684
+ })();
685
+ return stopTask;
474
686
  } };
475
687
  }
476
688
  //#endregion
@@ -28,6 +28,7 @@ function collectRuntimeConfigAssignments(params) {
28
28
  defaults: params.defaults,
29
29
  context: params.context,
30
30
  topLevelActive: isBaseFieldActiveForChannelSurface(surface, "nickserv") && isRecord(irc.nickserv) && isEnabledFlag(irc.nickserv),
31
+ topLevelInheritedAccountActive: ({ account, enabled }) => enabled && !Object.hasOwn(account, "nickserv") && isEnabledFlag(irc.nickserv),
31
32
  topInactiveReason: "no enabled account inherits this top-level IRC nickserv config or NickServ is disabled.",
32
33
  accountActive: ({ account, enabled }) => enabled && isRecord(account.nickserv) && isEnabledFlag(account.nickserv),
33
34
  accountInactiveReason: "IRC account is disabled or NickServ is disabled for this account."
@@ -1,2 +1,2 @@
1
- import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-PJHaoTSe.js";
1
+ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-D_DC3YWj.js";
2
2
  export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/irc",
3
- "version": "2026.7.2-beta.2",
3
+ "version": "2026.7.2-beta.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/irc",
9
- "version": "2026.7.2-beta.2",
9
+ "version": "2026.7.2-beta.4",
10
10
  "dependencies": {
11
11
  "zod": "4.4.3"
12
12
  }
@@ -6,20 +6,6 @@
6
6
  "channels": [
7
7
  "irc"
8
8
  ],
9
- "channelEnvVars": {
10
- "irc": [
11
- "IRC_HOST",
12
- "IRC_PORT",
13
- "IRC_TLS",
14
- "IRC_NICK",
15
- "IRC_USERNAME",
16
- "IRC_REALNAME",
17
- "IRC_PASSWORD",
18
- "IRC_CHANNELS",
19
- "IRC_NICKSERV_PASSWORD",
20
- "IRC_NICKSERV_REGISTER_EMAIL"
21
- ]
22
- },
23
9
  "configSchema": {
24
10
  "type": "object",
25
11
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/irc",
3
- "version": "2026.7.2-beta.2",
3
+ "version": "2026.7.2-beta.4",
4
4
  "description": "OpenClaw IRC channel plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -15,8 +15,19 @@
15
15
  "allowInvalidConfigRecovery": true
16
16
  },
17
17
  "setupEntry": "./dist/setup-entry.js",
18
+ "setupFeatures": {
19
+ "configPromotion": true
20
+ },
18
21
  "channel": {
19
22
  "id": "irc",
23
+ "configuredState": {
24
+ "env": {
25
+ "allOf": [
26
+ "IRC_HOST",
27
+ "IRC_NICK"
28
+ ]
29
+ }
30
+ },
20
31
  "label": "IRC",
21
32
  "selectionLabel": "IRC (Server + Nick)",
22
33
  "detailLabel": "IRC",
@@ -27,20 +38,89 @@
27
38
  "internet-relay-chat"
28
39
  ],
29
40
  "systemImage": "network",
30
- "configuredState": {
31
- "env": {
32
- "allOf": [
33
- "IRC_HOST",
34
- "IRC_NICK"
35
- ]
36
- }
41
+ "setup": {
42
+ "fields": [
43
+ {
44
+ "key": "host",
45
+ "kind": "string",
46
+ "cli": {
47
+ "flags": "--host <host>",
48
+ "description": "IRC server host"
49
+ }
50
+ },
51
+ {
52
+ "key": "port",
53
+ "kind": "string",
54
+ "cli": {
55
+ "flags": "--port <port>",
56
+ "description": "IRC server port"
57
+ }
58
+ },
59
+ {
60
+ "key": "tls",
61
+ "kind": "boolean",
62
+ "cli": {
63
+ "flags": "--tls",
64
+ "description": "Use TLS for IRC"
65
+ }
66
+ },
67
+ {
68
+ "key": "nick",
69
+ "kind": "string",
70
+ "cli": {
71
+ "flags": "--nick <nick>",
72
+ "description": "IRC nickname"
73
+ }
74
+ },
75
+ {
76
+ "key": "username",
77
+ "kind": "string",
78
+ "cli": {
79
+ "flags": "--username <name>",
80
+ "description": "IRC username"
81
+ }
82
+ },
83
+ {
84
+ "key": "realname",
85
+ "kind": "string",
86
+ "cli": {
87
+ "flags": "--realname <name>",
88
+ "description": "IRC real name"
89
+ }
90
+ },
91
+ {
92
+ "key": "channels",
93
+ "kind": "string-list",
94
+ "cli": {
95
+ "flags": "--channels <names>",
96
+ "description": "IRC channels"
97
+ }
98
+ },
99
+ {
100
+ "key": "password",
101
+ "kind": "string",
102
+ "sensitive": true,
103
+ "cli": {
104
+ "flags": "--password <password>",
105
+ "description": "IRC server password"
106
+ }
107
+ },
108
+ {
109
+ "key": "useEnv",
110
+ "kind": "boolean",
111
+ "cli": {
112
+ "flags": "--use-env",
113
+ "description": "Use IRC environment configuration"
114
+ }
115
+ }
116
+ ]
37
117
  }
38
118
  },
39
119
  "compat": {
40
- "pluginApi": ">=2026.7.2-beta.2"
120
+ "pluginApi": ">=2026.7.2-beta.4"
41
121
  },
42
122
  "build": {
43
- "openclawVersion": "2026.7.2-beta.2",
123
+ "openclawVersion": "2026.7.2-beta.4",
44
124
  "bundledDist": false
45
125
  },
46
126
  "release": {
@@ -66,7 +146,7 @@
66
146
  "README.md"
67
147
  ],
68
148
  "peerDependencies": {
69
- "openclaw": ">=2026.7.2-beta.2"
149
+ "openclaw": ">=2026.7.2-beta.4"
70
150
  },
71
151
  "peerDependenciesMeta": {
72
152
  "openclaw": {