@openclaw/irc 2026.10.10-beta.5 → 2026.11.5-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/.setup/{channel-Bi4lj8N2.mjs → channel-DRdPa321.mjs} +266 -326
  2. package/dist/.setup/channel-plugin-api-DFjxQCOi.mjs +2 -0
  3. package/dist/.setup/channel-runtime-D2sqTm8e.mjs +579 -0
  4. package/dist/.setup/{config-schema-Cy9tItsu.mjs → config-schema-CQuAQW7O.mjs} +1 -4
  5. package/dist/.setup/{runtime-C_V9tmpk.mjs → runtime-CsdAoZex.mjs} +1 -2
  6. package/dist/.setup/{secret-contract-C5i-29Hz.mjs → secret-contract-0Ccz8X9F.mjs} +1 -2
  7. package/dist/.setup/{secret-contract-api-4IT9_EK6.mjs → secret-contract-api-BabNUhhB.mjs} +1 -1
  8. package/dist/.source-module-urls/4751df78f545ae971ceb22b6e4bff6086f63313240899a1d9884b970424a305e.json +1 -1
  9. package/dist/api.js +2 -2
  10. package/dist/channel-config-api.js +1 -1
  11. package/dist/channel-plugin-api.js +1 -1
  12. package/dist/doctor-contract-api.js +6 -12
  13. package/dist/index.js +1 -2
  14. package/dist/runtime-api.js +1 -1
  15. package/dist/secret-contract-api.js +1 -1
  16. package/dist/setup-entry.js +3 -4
  17. package/node_modules/zod/package.json +1 -1
  18. package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
  19. package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
  20. package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
  21. package/node_modules/zod/src/v4/core/compile.ts +9 -0
  22. package/node_modules/zod/src/v4/core/schemas.ts +35 -1
  23. package/node_modules/zod/src/v4/core/util.ts +13 -0
  24. package/node_modules/zod/src/v4/core/versions.ts +1 -1
  25. package/node_modules/zod/v4/core/compile.cjs +8 -0
  26. package/node_modules/zod/v4/core/compile.js +8 -0
  27. package/node_modules/zod/v4/core/schemas.cjs +30 -0
  28. package/node_modules/zod/v4/core/schemas.d.cts +3 -1
  29. package/node_modules/zod/v4/core/schemas.d.ts +3 -1
  30. package/node_modules/zod/v4/core/schemas.js +30 -0
  31. package/node_modules/zod/v4/core/util.cjs +15 -0
  32. package/node_modules/zod/v4/core/util.d.cts +2 -0
  33. package/node_modules/zod/v4/core/util.d.ts +2 -0
  34. package/node_modules/zod/v4/core/util.js +14 -0
  35. package/node_modules/zod/v4/core/versions.cjs +1 -1
  36. package/node_modules/zod/v4/core/versions.js +1 -1
  37. package/package.json +5 -5
  38. package/dist/.setup/channel-plugin-api-1I8kWfv8.mjs +0 -2
  39. package/dist/.setup/channel-runtime-B4OMtNnX.mjs +0 -652
@@ -0,0 +1,2 @@
1
+ export { t as ircPlugin } from "./channel-DRdPa321.mjs";
2
+ import "../channel-plugin-api.js";
@@ -0,0 +1,579 @@
1
+ import { a as buildIrcConnectOptions, c as parseIrcPrefix, d as sanitizeIrcAssistantText, f as createIrcIngressSubject, h as normalizeIrcAllowEntry, i as sendMessageIrc, l as resolveIrcGroupMatch, m as isChannelTarget, o as connectIrcClient, p as ircIngressIdentity, s as parseIrcLine, u as resolveIrcGroupRequireMention, y as resolveIrcAccount } from "./channel-DRdPa321.mjs";
2
+ import { t as getIrcRuntime } from "./runtime-CsdAoZex.mjs";
3
+ import { channelIngressRoutes } from "openclaw/plugin-sdk/channel-ingress-runtime";
4
+ import { bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
5
+ import { computeBackoff } from "openclaw/plugin-sdk/runtime-env";
6
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
7
+ import { createChannelInboundEnvelopeBuilderAsync, logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
8
+ import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
9
+ import { randomUUID } from "node:crypto";
10
+ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
11
+ import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
12
+ import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
13
+ import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
14
+ import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
15
+ import { GROUP_POLICY_BLOCKED_LABEL, resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
16
+ import { escapeRegExp } from "openclaw/plugin-sdk/text-utility-runtime";
17
+ // extensions/irc/src/irc-ingress.ts
18
+ const IRC_INGRESS_PAYLOAD_VERSION = 1;
19
+ const IRC_INGRESS_POLL_INTERVAL_MS = 1e3;
20
+ const IrcIngressPayloadError = createChannelIngressError("IrcIngressPayloadError");
21
+ function inspectRawPrivmsg(rawLine) {
22
+ const line = parseIrcLine(rawLine);
23
+ if (!line || line.command !== "PRIVMSG") throw new IrcIngressPayloadError("IRC ingress row is not a PRIVMSG line.");
24
+ const rawTarget = line.params[0]?.trim() ?? "";
25
+ const text = line.trailing ?? line.params[1] ?? "";
26
+ const prefix = parseIrcPrefix(line.prefix);
27
+ const senderNick = prefix.nick?.trim() ?? "";
28
+ if (!rawTarget || !senderNick || !text.trim()) throw new IrcIngressPayloadError("IRC PRIVMSG line is missing target, sender, or text.");
29
+ const isGroup = isChannelTarget(rawTarget);
30
+ const target = isGroup ? rawTarget : senderNick;
31
+ const lanePeer = normalizeLowercaseStringOrEmpty(target);
32
+ return {
33
+ laneKey: `${isGroup ? "channel" : "direct"}:${lanePeer}`,
34
+ message: {
35
+ target,
36
+ rawTarget,
37
+ senderNick,
38
+ senderUser: prefix.user?.trim() || void 0,
39
+ senderHost: prefix.host?.trim() || void 0,
40
+ text,
41
+ isGroup
42
+ }
43
+ };
44
+ }
45
+ function decodeIrcIngressPayload(payload, claimedId) {
46
+ 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.`);
47
+ const validPayload = payload;
48
+ return {
49
+ version: validPayload.version,
50
+ body: {
51
+ eventId: validPayload.eventId,
52
+ receivedAt: validPayload.receivedAt,
53
+ connectionEpoch: validPayload.connectionEpoch,
54
+ connectedNick: validPayload.connectedNick,
55
+ rawLine: validPayload.rawLine
56
+ }
57
+ };
58
+ }
59
+ function inspectIrcIngress(raw, phase) {
60
+ try {
61
+ const projection = inspectRawPrivmsg(raw.body.rawLine);
62
+ if (phase === "claim") raw.claimedProjection = projection;
63
+ return {
64
+ eventId: raw.body.eventId,
65
+ laneKey: projection.laneKey
66
+ };
67
+ } catch (error) {
68
+ if (phase === "admission" && error instanceof IrcIngressPayloadError) return {
69
+ eventId: raw.body.eventId,
70
+ laneKey: `invalid:${raw.body.eventId}`
71
+ };
72
+ throw error;
73
+ }
74
+ }
75
+ function resolveIrcIngressNonRetryableFailure(error) {
76
+ return error instanceof IrcIngressPayloadError ? {
77
+ reason: "invalid-event",
78
+ message: error.message
79
+ } : null;
80
+ }
81
+ function createIrcIngressMonitor(options) {
82
+ const monitor = createChannelIngressMonitor({
83
+ queue: () => getIrcRuntime().state.openChannelIngressQueue({ accountId: options.accountId }),
84
+ inspect: (raw, context) => inspectIrcIngress(raw, context.phase),
85
+ payload: {
86
+ version: IRC_INGRESS_PAYLOAD_VERSION,
87
+ serialize: (raw) => raw.body,
88
+ deserialize: (body) => ({ body }),
89
+ encode: ({ body }) => ({
90
+ version: IRC_INGRESS_PAYLOAD_VERSION,
91
+ ...body
92
+ }),
93
+ decode: (payload, { claim }) => decodeIrcIngressPayload(payload, claim.id),
94
+ createClaimError: (_kind, claim) => new IrcIngressPayloadError(`IRC ingress row ${claim.id} has invalid metadata.`)
95
+ },
96
+ deliver: (raw, lifecycle, claim) => {
97
+ const message = {
98
+ ...raw.claimedProjection.message,
99
+ messageId: claim.id,
100
+ timestamp: raw.body.receivedAt
101
+ };
102
+ return options.dispatch(message, lifecycle, {
103
+ connectedNick: raw.body.connectedNick.trim(),
104
+ connectionEpoch: raw.body.connectionEpoch.trim()
105
+ });
106
+ },
107
+ pollIntervalMs: IRC_INGRESS_POLL_INTERVAL_MS,
108
+ retention: {
109
+ completedMaxEntries: 1e3,
110
+ failedMaxEntries: 1e3
111
+ },
112
+ drain: {
113
+ resolveNonRetryableFailure: resolveIrcIngressNonRetryableFailure,
114
+ onLog: (message) => options.runtime.log?.(`irc ${message}`)
115
+ },
116
+ createStoppedError: () => /* @__PURE__ */ new Error("IRC ingress is stopped."),
117
+ onError: (error) => options.runtime.error?.(`irc ingress drain failed: ${String(error)}`)
118
+ });
119
+ return {
120
+ openConnection: (connectionEpoch = randomUUID()) => {
121
+ const epoch = connectionEpoch.trim();
122
+ if (!epoch) throw new Error("IRC ingress connection epoch is required.");
123
+ let sequence = 0;
124
+ return {
125
+ connectionEpoch: epoch,
126
+ accept: (rawLine, connectedNick) => {
127
+ if (monitor.isStopped()) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress is stopped."));
128
+ sequence += 1;
129
+ const eventId = `local:${epoch}:${String(sequence).padStart(12, "0")}`;
130
+ const receivedAt = Date.now();
131
+ const normalizedNick = connectedNick.trim();
132
+ if (!normalizedNick) return Promise.reject(/* @__PURE__ */ new Error("IRC ingress connected nickname is required."));
133
+ return monitor.admit({ body: {
134
+ eventId,
135
+ rawLine,
136
+ receivedAt,
137
+ connectionEpoch: epoch,
138
+ connectedNick: normalizedNick
139
+ } }, { receivedAt }).then(() => void 0);
140
+ }
141
+ };
142
+ },
143
+ start: monitor.start,
144
+ pause: monitor.pause,
145
+ stop: monitor.stop,
146
+ waitForIdle: monitor.waitForIdle
147
+ };
148
+ }
149
+ // extensions/irc/src/reconnect-backoff.ts
150
+ const IRC_RECONNECT_BACKOFF = {
151
+ initialMs: 1e3,
152
+ maxMs: 3e4,
153
+ factor: 2,
154
+ jitter: .2
155
+ };
156
+ const IRC_RECONNECT_STABLE_MS = 6e4;
157
+ function createIrcReconnectBackoff() {
158
+ let attempt = 0;
159
+ let connectedAt;
160
+ return {
161
+ markConnected: () => {
162
+ connectedAt = Date.now();
163
+ },
164
+ nextDelayMs: () => {
165
+ if (connectedAt !== void 0 && Date.now() - connectedAt >= IRC_RECONNECT_STABLE_MS) attempt = 0;
166
+ connectedAt = void 0;
167
+ attempt += 1;
168
+ return computeBackoff(IRC_RECONNECT_BACKOFF, attempt);
169
+ }
170
+ };
171
+ }
172
+ // extensions/irc/src/monitor.ts
173
+ const CHANNEL_ID = "irc";
174
+ const IRC_NICK_CHARACTER = String.raw`[A-Za-z0-9_\-\[\]\\\x60^{}|~]`;
175
+ const IRC_RFC1459_CASE_EQUIVALENTS = /* @__PURE__ */ new Map([
176
+ ["[", "{"],
177
+ ["{", "["],
178
+ ["]", "}"],
179
+ ["}", "]"],
180
+ ["\\", "|"],
181
+ ["|", "\\"],
182
+ ["^", "~"],
183
+ ["~", "^"]
184
+ ]);
185
+ function buildIrcNickMentionPattern(value) {
186
+ return Array.from(value, (character) => {
187
+ const equivalent = IRC_RFC1459_CASE_EQUIVALENTS.get(character);
188
+ return equivalent ? `[${escapeRegExp(character)}${escapeRegExp(equivalent)}]` : escapeRegExp(character);
189
+ }).join("");
190
+ }
191
+ function hasEntries(entries) {
192
+ return normalizeStringEntries(entries).some((entry) => normalizeIrcAllowEntry(entry));
193
+ }
194
+ async function monitorIrcProvider(opts) {
195
+ const core = getIrcRuntime();
196
+ const cfg = opts.config ?? core.config.current();
197
+ const account = resolveIrcAccount({
198
+ cfg,
199
+ accountId: opts.accountId
200
+ });
201
+ const runtime = resolveLoggerBackedRuntime(opts.runtime, core.logging.getChildLogger());
202
+ if (!account.configured) throw new Error(`IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`);
203
+ const logger = core.logging.getChildLogger({
204
+ channel: "irc",
205
+ accountId: account.accountId
206
+ });
207
+ let client = null;
208
+ let activeConnectionEpoch = null;
209
+ let ingressPause = Promise.resolve();
210
+ let reconnectTimer = null;
211
+ const reconnectBackoff = createIrcReconnectBackoff();
212
+ let stopped = false;
213
+ const monitorAbort = new AbortController();
214
+ const abortSignal = opts.abortSignal ? AbortSignal.any([opts.abortSignal, monitorAbort.signal]) : monitorAbort.signal;
215
+ const ingress = createIrcIngressMonitor({
216
+ accountId: account.accountId,
217
+ runtime,
218
+ dispatch: async (message, turnAdoptionLifecycle, context) => {
219
+ if (!client || stopped || abortSignal.aborted) return {
220
+ kind: "failed-retryable",
221
+ error: /* @__PURE__ */ new Error("IRC transport disconnected before ingress dispatch.")
222
+ };
223
+ if (normalizeLowercaseStringOrEmpty(message.senderNick) === normalizeLowercaseStringOrEmpty(context.connectedNick)) return { kind: "completed" };
224
+ if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) {
225
+ logger.warn?.(`[${account.accountId}] dropping replayed IRC DM after the connection changed`);
226
+ return { kind: "completed" };
227
+ }
228
+ const deliverReply = async (target, payload) => {
229
+ await deliverFormattedTextWithAttachments({
230
+ payload: {
231
+ ...payload,
232
+ text: sanitizeIrcAssistantText(payload.text ?? "")
233
+ },
234
+ send: async ({ text }) => {
235
+ const replyClient = client;
236
+ if (!replyClient || !replyClient.isReady() || stopped || abortSignal.aborted) throw new Error("IRC transport disconnected before reply send.");
237
+ if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) throw new Error("IRC connection changed before private reply send.");
238
+ await replyClient.sendPrivmsg(target, text);
239
+ opts.statusSink?.({ lastOutboundAt: Date.now() });
240
+ core.channel.activity.record({
241
+ channel: "irc",
242
+ accountId: account.accountId,
243
+ direction: "outbound"
244
+ });
245
+ }
246
+ });
247
+ };
248
+ const pairing = createChannelPairingController({
249
+ core,
250
+ channel: CHANNEL_ID,
251
+ accountId: account.accountId
252
+ });
253
+ const rawBody = message.text?.trim() ?? "";
254
+ if (!rawBody) return { kind: "completed" };
255
+ if (turnAdoptionLifecycle?.abortSignal.aborted) return {
256
+ kind: "failed-retryable",
257
+ error: turnAdoptionLifecycle.abortSignal.reason
258
+ };
259
+ opts.statusSink?.({ lastInboundAt: message.timestamp });
260
+ const senderDisplay = message.senderHost ? `${message.senderNick}!${message.senderUser ?? "?"}@${message.senderHost}` : message.senderNick;
261
+ const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
262
+ const dmPolicy = account.config.dmPolicy ?? "pairing";
263
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
264
+ const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
265
+ providerConfigPresent: cfg.channels?.irc !== void 0,
266
+ groupPolicy: account.config.groupPolicy,
267
+ defaultGroupPolicy
268
+ });
269
+ warnMissingProviderGroupPolicyFallbackOnce({
270
+ providerMissingFallbackApplied,
271
+ providerKey: "irc",
272
+ accountId: account.accountId,
273
+ blockedLabel: GROUP_POLICY_BLOCKED_LABEL.channel,
274
+ log: (messageLocal) => runtime.log?.(messageLocal)
275
+ });
276
+ const groupMatch = resolveIrcGroupMatch({
277
+ groups: account.config.groups,
278
+ target: message.target
279
+ });
280
+ const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
281
+ cfg,
282
+ surface: CHANNEL_ID
283
+ });
284
+ const hasControlCommand = core.channel.text.hasControlCommand(rawBody, cfg);
285
+ const mentionRegexes = core.channel.mentions.buildMentionRegexes(cfg);
286
+ const mentionNick = context.connectedNick.trim() || account.nick;
287
+ const explicitMentionRegex = mentionNick ? new RegExp(`(?<!${IRC_NICK_CHARACTER})${buildIrcNickMentionPattern(mentionNick)}(?!${IRC_NICK_CHARACTER})[:,]?`, "i") : null;
288
+ const wasMentioned = core.channel.mentions.matchesMentionPatterns(rawBody, mentionRegexes) || (explicitMentionRegex ? explicitMentionRegex.test(rawBody) : false);
289
+ const requireMention = message.isGroup ? resolveIrcGroupRequireMention({
290
+ groups: account.config.groups,
291
+ target: message.target
292
+ }) : false;
293
+ const routeGroupAllowFrom = normalizeStringEntries(groupMatch.groupConfig?.allowFrom?.length ? groupMatch.groupConfig.allowFrom : groupMatch.wildcardConfig?.allowFrom);
294
+ const accessGroupPolicy = groupPolicy === "open" && (hasEntries(account.config.groupAllowFrom) || hasEntries(routeGroupAllowFrom)) ? "allowlist" : groupPolicy;
295
+ const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
296
+ const peerId = message.isGroup ? channelTarget : message.senderNick;
297
+ const route = resolveAgentRoute({
298
+ cfg,
299
+ channel: CHANNEL_ID,
300
+ accountId: account.accountId,
301
+ peer: {
302
+ kind: message.isGroup ? "group" : "direct",
303
+ id: peerId
304
+ }
305
+ });
306
+ const access = await core.channel.inbound.ingress.createResolver({
307
+ channelId: CHANNEL_ID,
308
+ accountId: account.accountId,
309
+ identity: ircIngressIdentity,
310
+ cfg,
311
+ readStoreAllowFrom: async () => await pairing.readAllowFromStore()
312
+ }).message({
313
+ subject: createIrcIngressSubject(message),
314
+ conversation: {
315
+ kind: message.isGroup ? "group" : "direct",
316
+ id: message.target
317
+ },
318
+ contextBinding: {
319
+ agentId: route.agentId,
320
+ sessionKey: route.sessionKey,
321
+ ...message.messageId ? { messageId: message.messageId } : {},
322
+ inboundEventKind: "user_request"
323
+ },
324
+ route: message.isGroup ? channelIngressRoutes(groupPolicy === "allowlist" && {
325
+ id: "irc:channel",
326
+ allowed: groupMatch.hasConfiguredGroups && groupMatch.allowed,
327
+ precedence: 0,
328
+ matchId: "irc-channel",
329
+ blockReason: "channel_not_allowlisted"
330
+ }, (groupMatch.groupConfig?.enabled === false || groupMatch.wildcardConfig?.enabled === false) && {
331
+ id: "irc:channel-enabled",
332
+ enabled: false,
333
+ precedence: 10,
334
+ blockReason: "channel_disabled"
335
+ }, hasEntries(routeGroupAllowFrom) && {
336
+ id: "irc:channel-sender",
337
+ precedence: 20,
338
+ senderPolicy: "replace",
339
+ senderAllowFrom: routeGroupAllowFrom
340
+ }) : [],
341
+ mentionFacts: message.isGroup ? {
342
+ canDetectMention: true,
343
+ wasMentioned,
344
+ hasAnyMention: wasMentioned
345
+ } : void 0,
346
+ dmPolicy,
347
+ groupPolicy: accessGroupPolicy,
348
+ policy: {
349
+ groupAllowFromFallbackToAllowFrom: false,
350
+ mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled",
351
+ activation: {
352
+ requireMention: message.isGroup && requireMention,
353
+ allowTextCommands
354
+ }
355
+ },
356
+ allowFrom: account.config.allowFrom,
357
+ groupAllowFrom: account.config.groupAllowFrom,
358
+ command: {
359
+ allowTextCommands,
360
+ hasControlCommand
361
+ }
362
+ });
363
+ const commandAuthorized = access.commandAccess.authorized;
364
+ if (access.ingress.admission === "pairing-required") {
365
+ await pairing.issueChallenge({
366
+ senderId: normalizeLowercaseStringOrEmpty(senderDisplay),
367
+ senderIdLine: `Your IRC id: ${senderDisplay}`,
368
+ meta: { name: message.senderNick || void 0 },
369
+ sendPairingReply: async (text) => {
370
+ await deliverReply(message.senderNick, { text });
371
+ },
372
+ onReplyError: (err) => {
373
+ runtime.error?.(`irc: pairing reply failed for ${senderDisplay}: ${String(err)}`);
374
+ }
375
+ });
376
+ runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
377
+ return { kind: "completed" };
378
+ }
379
+ if (access.ingress.admission === "skip") {
380
+ runtime.log?.(`irc: drop channel ${message.target} (missing-mention)`);
381
+ return { kind: "completed" };
382
+ }
383
+ if (access.ingress.admission !== "dispatch") {
384
+ if (message.isGroup && access.ingress.decisiveGateId === "command" && access.commandAccess.shouldBlockControlCommand) {
385
+ logInboundDrop({
386
+ log: (line) => runtime.log?.(line),
387
+ channel: CHANNEL_ID,
388
+ reason: "control command (unauthorized)",
389
+ target: senderDisplay
390
+ });
391
+ return { kind: "completed" };
392
+ }
393
+ if (message.isGroup) {
394
+ if (access.routeAccess.reason === "channel_not_allowlisted") runtime.log?.(`irc: drop channel ${message.target} (not allowlisted)`);
395
+ else if (access.routeAccess.reason === "channel_disabled") runtime.log?.(`irc: drop channel ${message.target} (disabled)`);
396
+ else runtime.log?.(`irc: drop group sender ${senderDisplay} (policy=${groupPolicy})`);
397
+ } else runtime.log?.(`irc: drop DM sender ${senderDisplay} (dmPolicy=${dmPolicy})`);
398
+ return { kind: "completed" };
399
+ }
400
+ if (turnAdoptionLifecycle?.abortSignal.aborted) return {
401
+ kind: "failed-retryable",
402
+ error: turnAdoptionLifecycle.abortSignal.reason
403
+ };
404
+ const fromLabel = message.isGroup ? message.target : senderDisplay;
405
+ const body = (await createChannelInboundEnvelopeBuilderAsync({
406
+ cfg,
407
+ route
408
+ }))({
409
+ channel: "IRC",
410
+ from: fromLabel,
411
+ timestamp: message.timestamp,
412
+ body: rawBody
413
+ });
414
+ const groupSystemPrompt = normalizeOptionalString(groupMatch.groupConfig?.systemPrompt);
415
+ const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(account.config);
416
+ const ctxPayload = core.channel.inbound.buildContext({
417
+ channelIngress: access,
418
+ channel: CHANNEL_ID,
419
+ accountId: route.accountId,
420
+ messageId: message.messageId,
421
+ timestamp: message.timestamp,
422
+ from: message.isGroup ? `channel:${channelTarget}` : `irc:${senderDisplay}`,
423
+ sender: {
424
+ id: senderDisplay,
425
+ name: message.senderNick || void 0
426
+ },
427
+ conversation: {
428
+ kind: message.isGroup ? "group" : "direct",
429
+ id: peerId,
430
+ label: fromLabel
431
+ },
432
+ route: {
433
+ agentId: route.agentId,
434
+ dmScope: route.dmScope,
435
+ accountId: route.accountId,
436
+ routeSessionKey: route.sessionKey
437
+ },
438
+ reply: {
439
+ to: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`,
440
+ originatingTo: message.isGroup ? `channel:${channelTarget}` : `irc:${peerId}`
441
+ },
442
+ message: {
443
+ body,
444
+ bodyForAgent: rawBody,
445
+ rawBody,
446
+ commandBody: rawBody
447
+ },
448
+ access: {
449
+ commands: { authorized: commandAuthorized },
450
+ mentions: {
451
+ canDetectMention: message.isGroup,
452
+ wasMentioned
453
+ }
454
+ },
455
+ extra: {
456
+ GroupSubject: message.isGroup ? message.target : void 0,
457
+ GroupSystemPrompt: message.isGroup ? groupSystemPrompt : void 0
458
+ }
459
+ });
460
+ await core.channel.inbound.dispatch({
461
+ cfg,
462
+ channel: CHANNEL_ID,
463
+ accountId: account.accountId,
464
+ route: {
465
+ agentId: route.agentId,
466
+ sessionKey: route.sessionKey
467
+ },
468
+ ctxPayload,
469
+ delivery: {
470
+ deliver: async (payload) => {
471
+ await deliverReply(peerId, payload);
472
+ },
473
+ onError: (err, info) => {
474
+ runtime.error?.(`irc ${info.kind} reply failed: ${String(err)}`);
475
+ }
476
+ },
477
+ replyPipeline: {},
478
+ replyOptions: {
479
+ ...bindIngressLifecycleToReplyOptions(turnAdoptionLifecycle),
480
+ skillFilter: groupMatch.groupConfig?.skills,
481
+ disableBlockStreaming: typeof blockStreamingEnabled === "boolean" ? !blockStreamingEnabled : void 0
482
+ },
483
+ record: { onRecordError: (err) => {
484
+ runtime.error?.(`irc: failed updating session meta: ${String(err)}`);
485
+ } }
486
+ });
487
+ }
488
+ });
489
+ function scheduleReconnect() {
490
+ if (stopped || abortSignal.aborted || reconnectTimer) return;
491
+ opts.statusSink?.({
492
+ connected: false,
493
+ lifecycle: "recovering"
494
+ });
495
+ const delayMs = reconnectBackoff.nextDelayMs();
496
+ reconnectTimer = setTimeout(() => {
497
+ reconnectTimer = null;
498
+ connect().catch((error) => {
499
+ if (stopped || abortSignal.aborted) return;
500
+ const message = error instanceof Error ? error.message : String(error);
501
+ logger.error(`[${account.accountId}] IRC reconnect failed: ${message}`);
502
+ opts.statusSink?.({ lastError: message });
503
+ scheduleReconnect();
504
+ });
505
+ }, delayMs);
506
+ return delayMs;
507
+ }
508
+ async function connect() {
509
+ if (stopped || abortSignal.aborted) return;
510
+ const ingressConnection = ingress.openConnection();
511
+ const nextClient = await connectIrcClient(buildIrcConnectOptions(account, {
512
+ channels: account.config.channels,
513
+ abortSignal,
514
+ onNotice: (text, target) => {
515
+ if (core.logging.shouldLogVerbose()) logger.debug?.(`[${account.accountId}] notice ${target ?? ""} len=${text.length}`);
516
+ },
517
+ onError: (error) => {
518
+ logger.error(`[${account.accountId}] IRC error: ${error.message}`);
519
+ },
520
+ onDisconnect: () => {
521
+ if (stopped || abortSignal.aborted) return;
522
+ ingressPause = ingress.pause();
523
+ if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
524
+ client = null;
525
+ opts.statusSink?.({ lastDisconnect: { at: Date.now() } });
526
+ const delayMs = scheduleReconnect();
527
+ if (delayMs !== void 0) logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${delayMs}ms`);
528
+ },
529
+ onPrivmsg: async (event) => {
530
+ await ingressConnection.accept(event.rawLine, event.connectedNick);
531
+ if (normalizeLowercaseStringOrEmpty(event.senderNick) === normalizeLowercaseStringOrEmpty(event.connectedNick)) return;
532
+ core.channel.activity.record({
533
+ channel: "irc",
534
+ accountId: account.accountId,
535
+ direction: "inbound",
536
+ at: Date.now()
537
+ });
538
+ }
539
+ }));
540
+ if (stopped || abortSignal.aborted) {
541
+ nextClient.quit("shutdown");
542
+ return;
543
+ }
544
+ client = nextClient;
545
+ activeConnectionEpoch = ingressConnection.connectionEpoch;
546
+ await ingressPause;
547
+ if (client !== nextClient || !nextClient.isReady()) {
548
+ if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
549
+ return;
550
+ }
551
+ ingress.start();
552
+ reconnectBackoff.markConnected();
553
+ opts.statusSink?.(channelReadyPatch());
554
+ logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
555
+ }
556
+ try {
557
+ await connect();
558
+ } catch (error) {
559
+ await ingress.stop();
560
+ throw error;
561
+ }
562
+ let stopTask;
563
+ return { stop: () => {
564
+ stopTask ??= (async () => {
565
+ stopped = true;
566
+ if (!abortSignal.aborted) monitorAbort.abort();
567
+ if (reconnectTimer) {
568
+ clearTimeout(reconnectTimer);
569
+ reconnectTimer = null;
570
+ }
571
+ client?.quit("shutdown");
572
+ client = null;
573
+ activeConnectionEpoch = null;
574
+ await ingress.stop();
575
+ })();
576
+ return stopTask;
577
+ } };
578
+ }
579
+ export { monitorIrcProvider, sendMessageIrc };
@@ -1,7 +1,7 @@
1
1
  import { createChannelConfigUiHints } from "openclaw/plugin-sdk/channel-core";
2
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
- //#region extensions/irc/src/config-ui-hints.ts
4
+ // extensions/irc/src/config-schema.ts
5
5
  const ircChannelConfigUiHints = {
6
6
  "": {
7
7
  label: "IRC",
@@ -40,8 +40,6 @@ const ircChannelConfigUiHints = {
40
40
  configWrites: true
41
41
  })
42
42
  };
43
- //#endregion
44
- //#region extensions/irc/src/config-schema.ts
45
43
  const IrcNickServSchema = z.object({
46
44
  enabled: z.boolean().optional(),
47
45
  service: z.string().optional(),
@@ -95,5 +93,4 @@ const IrcConfigSchema = buildMultiAccountChannelSchema(IrcAccountSchemaBase, {
95
93
  }
96
94
  });
97
95
  const IrcChannelConfigSchema = buildChannelConfigSchema(IrcConfigSchema, { uiHints: ircChannelConfigUiHints });
98
- //#endregion
99
96
  export { IrcChannelConfigSchema as t };
@@ -1,8 +1,7 @@
1
1
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
2
- //#region extensions/irc/src/runtime.ts
2
+ // extensions/irc/src/runtime.ts
3
3
  const { setRuntime: setIrcRuntime, getRuntime: getIrcRuntime, tryGetRuntime: getOptionalIrcRuntime } = createPluginRuntimeStore({
4
4
  pluginId: "irc",
5
5
  errorMessage: "IRC runtime not initialized"
6
6
  });
7
- //#endregion
8
7
  export { getOptionalIrcRuntime as n, setIrcRuntime as r, getIrcRuntime as t };
@@ -1,5 +1,5 @@
1
1
  import { collectNestedChannelFieldAssignments, collectSimpleChannelFieldAssignments, createChannelSecretContract, isBaseFieldActiveForChannelSurface, isEnabledFlag, isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
2
- //#region extensions/irc/src/secret-contract.ts
2
+ // extensions/irc/src/secret-contract.ts
3
3
  const channelSecrets = createChannelSecretContract({
4
4
  channelKey: "irc",
5
5
  account: ["nickserv.password", "password"],
@@ -25,5 +25,4 @@ const channelSecrets = createChannelSecretContract({
25
25
  }
26
26
  });
27
27
  const { secretTargetRegistryEntries, collectRuntimeConfigAssignments } = channelSecrets;
28
- //#endregion
29
28
  export { collectRuntimeConfigAssignments as n, secretTargetRegistryEntries as r, channelSecrets as t };
@@ -1,2 +1,2 @@
1
- export { t as channelSecrets, n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-C5i-29Hz.mjs";
1
+ export { t as channelSecrets, n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-0Ccz8X9F.mjs";
2
2
  import "../secret-contract-api.js";
@@ -1 +1 @@
1
- {"version":3,"entries":[{"output":"api.js","source":"extensions/irc/api.ts"},{"output":"channel-config-api.js","source":"extensions/irc/channel-config-api.ts"},{"output":"channel-plugin-api.js","source":"extensions/irc/channel-plugin-api.ts"},{"output":"doctor-contract-api.js","source":"extensions/irc/doctor-contract-api.ts"},{"output":"index.js","source":"extensions/irc/index.ts"},{"output":"runtime-api.js","source":"extensions/irc/runtime-api.ts"},{"output":"secret-contract-api.js","source":"extensions/irc/secret-contract-api.ts"},{"output":"setup-entry.js","source":"extensions/irc/setup-entry.ts"}],"chunks":[{"output":".setup/channel-Bi4lj8N2.mjs","sha256":"d903c2aedb1d0f0d9431401fe002fa3b1ff025ed566eac44bebce48dfb082ac1","facadeSource":null,"sources":["extensions/irc/src/accounts.ts","extensions/irc/src/channel.ts","extensions/irc/src/client.ts","extensions/irc/src/connect-options.ts","extensions/irc/src/control-chars.ts","extensions/irc/src/doctor.ts","extensions/irc/src/gateway.ts","extensions/irc/src/ingress-identity.ts","extensions/irc/src/message-adapter.ts","extensions/irc/src/normalize.ts","extensions/irc/src/outbound-base.ts","extensions/irc/src/policy.ts","extensions/irc/src/probe.ts","extensions/irc/src/protocol.ts","extensions/irc/src/send.ts","extensions/irc/src/setup-core.ts","extensions/irc/src/setup-surface.ts","packages/normalization-core/src/grapheme.ts","packages/normalization-core/src/utf16-slice.ts"]},{"output":".setup/channel-plugin-api-1I8kWfv8.mjs","sha256":"1bcc3a6fc15d466011c3a9b76038b1bfe424ecbd24cb9560d9879a4812d1d8b8","facadeSource":"extensions/irc/channel-plugin-api.ts","sources":[]},{"output":".setup/channel-runtime-B4OMtNnX.mjs","sha256":"7b59600c948833eef4005e4e45fdddf50a094700be3ac4076c73c22c3f5e6e11","facadeSource":"extensions/irc/src/channel-runtime.ts","sources":["extensions/irc/src/inbound.ts","extensions/irc/src/irc-ingress.ts","extensions/irc/src/monitor.ts","extensions/irc/src/reconnect-backoff.ts"]},{"output":".setup/config-schema-Cy9tItsu.mjs","sha256":"add8978c6443fac3c442d7d66449010bad9142b38ad2d4f127e048a27379424d","facadeSource":null,"sources":["extensions/irc/src/config-schema.ts","extensions/irc/src/config-ui-hints.ts"]},{"output":".setup/runtime-C_V9tmpk.mjs","sha256":"3f1f1b4d7ee63152a339b6a4614a28f57972bf655c73e0e7083c837370bc094c","facadeSource":null,"sources":["extensions/irc/src/runtime.ts"]},{"output":".setup/secret-contract-api-4IT9_EK6.mjs","sha256":"8d9275ff824f6c76b94e80dc043be8a12df9c8b3dc7203faa58bd2fc1bf3c244","facadeSource":"extensions/irc/secret-contract-api.ts","sources":[]},{"output":".setup/secret-contract-C5i-29Hz.mjs","sha256":"2eb99571f8fac572bc6d2a7cafe7bf7f2d864101256eb7b5aa6876fd031ee914","facadeSource":null,"sources":["extensions/irc/src/secret-contract.ts"]},{"output":"api.js","sha256":"e39b0d10e678b0912ca1196b3b4df0993ba195f73e5ac5bd9e2569c06b91dcc8","facadeSource":"extensions/irc/api.ts","sources":[]},{"output":"channel-config-api.js","sha256":"0992ef0872e2bb3dd21649d22af2bb491279f6f01d80466db2d38fed0c69f534","facadeSource":"extensions/irc/channel-config-api.ts","sources":[]},{"output":"channel-plugin-api.js","sha256":"ebeefd0b6ec09b436ad8cca5df7aa3e194e436356d7c77c39529f86e866b650f","facadeSource":"extensions/irc/channel-plugin-api.ts","sources":[]},{"output":"doctor-contract-api.js","sha256":"de1a30eb327e6de2799b8b84a8b4bd0f607d90051a2ab67f6a08ed3d29a54505","facadeSource":"extensions/irc/doctor-contract-api.ts","sources":["extensions/irc/config-doctor-api.ts","packages/normalization-core/src/record-coerce.ts","src/config/channel-config-normalization.ts","src/plugin-sdk/legacy-webhook-listener-migration.ts"]},{"output":"index.js","sha256":"26465ab4a608138b3cec93e4593f91f076be42b0fea9edad110953d71ec2e78c","facadeSource":"extensions/irc/index.ts","sources":["extensions/irc/index.ts"]},{"output":"runtime-api.js","sha256":"52b1074caae3be2b4f35e6ab34f26576c94d3a970587212b24eb8d9ac501720c","facadeSource":"extensions/irc/runtime-api.ts","sources":[]},{"output":"secret-contract-api.js","sha256":"ba5440395ed290e6115b9fa94429ffd9302dad0a9077f89307928fdc106fa4f0","facadeSource":"extensions/irc/secret-contract-api.ts","sources":[]},{"output":"setup-entry.js","sha256":"bcd384958367c5d4a1805150fedb16ce1ffa687c29756609f199e35670d6d7d4","facadeSource":"extensions/irc/setup-entry.ts","sources":["extensions/irc/setup-entry.ts"]}],"unrenderedSources":["extensions/irc/api.ts","extensions/irc/channel-config-api.ts","extensions/irc/channel-plugin-api.ts","extensions/irc/doctor-contract-api.ts","extensions/irc/runtime-api.ts","extensions/irc/secret-contract-api.ts","extensions/irc/src/channel-runtime.ts","src/plugin-sdk/text-grapheme.ts"]}
1
+ {"version":3,"entries":[{"output":"api.js","source":"extensions/irc/api.ts"},{"output":"channel-config-api.js","source":"extensions/irc/channel-config-api.ts"},{"output":"channel-plugin-api.js","source":"extensions/irc/channel-plugin-api.ts"},{"output":"doctor-contract-api.js","source":"extensions/irc/doctor-contract-api.ts"},{"output":"index.js","source":"extensions/irc/index.ts"},{"output":"runtime-api.js","source":"extensions/irc/runtime-api.ts"},{"output":"secret-contract-api.js","source":"extensions/irc/secret-contract-api.ts"},{"output":"setup-entry.js","source":"extensions/irc/setup-entry.ts"}],"chunks":[{"output":".setup/channel-DRdPa321.mjs","sha256":"38bc0a4bed895c3d8887640d7ccb0d3fbc4f77050e17f47e6badd61a88d8b188","facadeSource":null,"sources":["extensions/irc/src/accounts.ts","extensions/irc/src/channel.ts","extensions/irc/src/client.ts","extensions/irc/src/connect-options.ts","extensions/irc/src/control-chars.ts","extensions/irc/src/ingress-identity.ts","extensions/irc/src/normalize.ts","extensions/irc/src/outbound-base.ts","extensions/irc/src/policy.ts","extensions/irc/src/probe.ts","extensions/irc/src/protocol.ts","extensions/irc/src/send.ts","extensions/irc/src/setup-core.ts","extensions/irc/src/setup-surface.ts","packages/normalization-core/src/grapheme.ts","packages/normalization-core/src/utf16-slice.ts"]},{"output":".setup/channel-plugin-api-DFjxQCOi.mjs","sha256":"f377ed5103dacadfbf9e7c419ee4db1dbf1c1ccc28992f585519f3de19901482","facadeSource":"extensions/irc/channel-plugin-api.ts","sources":[]},{"output":".setup/channel-runtime-D2sqTm8e.mjs","sha256":"00b338b21502782ae1ff34d2921388eaa0dc0a8016f6afc555cfc73830c2fb73","facadeSource":"extensions/irc/src/channel-runtime.ts","sources":["extensions/irc/src/irc-ingress.ts","extensions/irc/src/monitor.ts","extensions/irc/src/reconnect-backoff.ts"]},{"output":".setup/config-schema-CQuAQW7O.mjs","sha256":"34c888b0637a2718940b5bfb88a79e2db75a2b9dd1eb4acb6d7dd7761d086833","facadeSource":null,"sources":["extensions/irc/src/config-schema.ts"]},{"output":".setup/runtime-CsdAoZex.mjs","sha256":"b482d973b5d4bf23cc5ec66bdc228ee7151794a36eec497e57ad9e9e27aa48b0","facadeSource":null,"sources":["extensions/irc/src/runtime.ts"]},{"output":".setup/secret-contract-0Ccz8X9F.mjs","sha256":"a2107dd2d2a2f07298fa31d9228fba57db3af18a0cf3f2e8039db0f6cbe9de15","facadeSource":null,"sources":["extensions/irc/src/secret-contract.ts"]},{"output":".setup/secret-contract-api-BabNUhhB.mjs","sha256":"f6810408d2a21938d4e2b0e823f443859ca71d84723d877f9497b6a7221c5c0d","facadeSource":"extensions/irc/secret-contract-api.ts","sources":[]},{"output":"api.js","sha256":"e602c3ac3be5f7dcfce8eed4a98e153a4fe6046f9d01df171c8573bd1390d1cb","facadeSource":"extensions/irc/api.ts","sources":[]},{"output":"channel-config-api.js","sha256":"1061f098b8a9134951ec7da5160fcf080c11b461550739c197b93d6ec6063122","facadeSource":"extensions/irc/channel-config-api.ts","sources":[]},{"output":"channel-plugin-api.js","sha256":"61b8334b8b68eec51976781b8ac6a6cce6361ad60fc6cda87dc242de9ad24830","facadeSource":"extensions/irc/channel-plugin-api.ts","sources":[]},{"output":"doctor-contract-api.js","sha256":"8b0484eda902108365f4548d42566303c6dcbb3ccfc89ba0e06a8633a6864819","facadeSource":"extensions/irc/doctor-contract-api.ts","sources":["extensions/irc/config-doctor-api.ts","packages/normalization-core/src/record-coerce.ts","src/config/channel-config-normalization.ts","src/plugin-sdk/legacy-webhook-listener-migration.ts"]},{"output":"index.js","sha256":"c399885749c923bf11c27c34eb67cc4f07def79ee3610b899d92db1a75e944f6","facadeSource":"extensions/irc/index.ts","sources":["extensions/irc/index.ts"]},{"output":"runtime-api.js","sha256":"71a18976cee49bc28f82b6e930e758f6efe6ea5527dff725f7cac5a823600190","facadeSource":"extensions/irc/runtime-api.ts","sources":[]},{"output":"secret-contract-api.js","sha256":"01ed5e83bd4b44f756a0591ab306fb1f569fe95d78d19671a9c41e48a0a4857d","facadeSource":"extensions/irc/secret-contract-api.ts","sources":[]},{"output":"setup-entry.js","sha256":"53c37b74b627efa52fb5397bcdb4a4510af8a084cd738704afc62c8825254f35","facadeSource":"extensions/irc/setup-entry.ts","sources":["extensions/irc/setup-entry.ts"]}],"unrenderedSources":["extensions/irc/api.ts","extensions/irc/channel-config-api.ts","extensions/irc/channel-plugin-api.ts","extensions/irc/doctor-contract-api.ts","extensions/irc/runtime-api.ts","extensions/irc/secret-contract-api.ts","extensions/irc/src/channel-runtime.ts","src/plugin-sdk/text-grapheme.ts"]}
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { _ as listIrcAccountIds, g as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveDefaultIrcAccountId, y as resolveIrcAccount } from "./.setup/channel-Bi4lj8N2.mjs";
2
- import { r as setIrcRuntime } from "./.setup/runtime-C_V9tmpk.mjs";
1
+ import { _ as listIrcAccountIds, g as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveDefaultIrcAccountId, y as resolveIrcAccount } from "./.setup/channel-DRdPa321.mjs";
2
+ import { r as setIrcRuntime } from "./.setup/runtime-CsdAoZex.mjs";
3
3
  export { ircPlugin, ircSetupAdapter, ircSetupWizard, listEnabledIrcAccounts, listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount, setIrcRuntime };