@openclaw/irc 2026.7.1-beta.2 → 2026.7.1-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-BTyt-Cxx.js";
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-C-P2GolT.js";
2
2
  import { n as setIrcRuntime } from "./runtime-DZfYm3vY.js";
3
3
  export { ircPlugin, ircSetupAdapter, ircSetupWizard, listEnabledIrcAccounts, listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount, setIrcRuntime };
@@ -4,7 +4,7 @@ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries
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 { createChatChannelPlugin, parseOptionalDelimitedEntries, tryReadSecretFileSync } from "openclaw/plugin-sdk/channel-core";
7
+ import { buildChannelOutboundSessionRoute, createChatChannelPlugin, parseOptionalDelimitedEntries, tryReadSecretFileSync } from "openclaw/plugin-sdk/channel-core";
8
8
  import { composeAccountWarningCollectors, createAllowlistProviderOpenWarningCollector, createDangerousNameMatchingMutableAllowlistWarningCollector } 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";
@@ -27,7 +27,7 @@ import { randomUUID } from "node:crypto";
27
27
  import { DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/routing";
28
28
  import { applyAccountNameToChannelSection, createAllowFromSection, createPromptParsedAllowFromForAccount, createSetupInputPresenceValidator, createSetupTranslator, createStandardChannelSetupStatus, createTopLevelChannelAllowFromSetter, createTopLevelChannelDmPolicySetter, formatDocsLink, patchScopedAccountConfig, setSetupChannelEnabled } from "openclaw/plugin-sdk/setup";
29
29
  //#region extensions/irc/src/accounts.ts
30
- const TRUTHY_ENV = new Set([
30
+ const TRUTHY_ENV = /* @__PURE__ */ new Set([
31
31
  "true",
32
32
  "1",
33
33
  "yes",
@@ -197,7 +197,7 @@ const collectIrcMutableAllowlistWarnings = createDangerousNameMatchingMutableAll
197
197
  });
198
198
  //#endregion
199
199
  //#region extensions/irc/src/gateway.ts
200
- const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-CdlR5F_m.js"));
200
+ const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-CLTE89ol.js"));
201
201
  async function startIrcGatewayAccount(ctx) {
202
202
  const account = ctx.account;
203
203
  const statusSink = createAccountStatusSink({
@@ -321,12 +321,30 @@ function makeIrcMessageId() {
321
321
  }
322
322
  //#endregion
323
323
  //#region extensions/irc/src/client.ts
324
- const IRC_ERROR_CODES = new Set([
324
+ const IRC_ERROR_CODES = /* @__PURE__ */ new Set([
325
325
  "432",
326
326
  "464",
327
327
  "465"
328
328
  ]);
329
- const IRC_NICK_COLLISION_CODES = new Set(["433", "436"]);
329
+ const IRC_NICK_COLLISION_CODES = /* @__PURE__ */ new Set(["433", "436"]);
330
+ const IRC_MAX_LINE_BYTES = 512;
331
+ function takeIrcPrivmsgChunk(text, maxChars, maxBytes) {
332
+ let end = 0;
333
+ let bytes = 0;
334
+ for (const codePoint of text) {
335
+ const codePointBytes = Buffer.byteLength(codePoint, "utf8");
336
+ if (end > 0 && end + codePoint.length > maxChars || bytes + codePointBytes > maxBytes) break;
337
+ end += codePoint.length;
338
+ bytes += codePointBytes;
339
+ }
340
+ if (end === 0) throw new Error("IRC target leaves no room for message text within the 512-byte line limit");
341
+ if (end === text.length) return text;
342
+ const fitted = text.slice(0, end);
343
+ if (text[end] === " ") return fitted;
344
+ const splitAt = fitted.lastIndexOf(" ");
345
+ if (splitAt >= Math.floor(fitted.length / 2)) return fitted.slice(0, splitAt);
346
+ return fitted;
347
+ }
330
348
  function toError(err) {
331
349
  if (err instanceof Error) return err;
332
350
  return new Error(typeof err === "string" ? err : JSON.stringify(err));
@@ -358,7 +376,7 @@ function buildIrcNickServCommands(options) {
358
376
  }
359
377
  async function connectIrcClient(options) {
360
378
  const timeoutMs = options.connectTimeoutMs != null ? options.connectTimeoutMs : 15e3;
361
- const messageChunkMaxChars = options.messageChunkMaxChars != null ? options.messageChunkMaxChars : 350;
379
+ const messageChunkMaxChars = Math.max(1, Math.floor(options.messageChunkMaxChars ?? 350));
362
380
  if (!options.host.trim()) throw new Error("IRC host is required");
363
381
  if (!options.nick.trim()) throw new Error("IRC nick is required");
364
382
  const desiredNick = options.nick.trim();
@@ -407,7 +425,8 @@ async function connectIrcClient(options) {
407
425
  if (nickServEnabled && !nickServRecoverAttempted && nickservPassword) {
408
426
  nickServRecoverAttempted = true;
409
427
  try {
410
- sendRaw(`PRIVMSG ${sanitizeIrcTarget(options.nickserv?.service?.trim() || "NickServ")} :GHOST ${desiredNick} ${nickservPassword}`);
428
+ const service = sanitizeIrcTarget(options.nickserv?.service?.trim() || "NickServ");
429
+ sendRaw(`PRIVMSG ${service} :GHOST ${desiredNick} ${nickservPassword}`);
411
430
  sendRaw(`NICK ${desiredNick}`);
412
431
  return true;
413
432
  } catch (err) {
@@ -436,15 +455,11 @@ async function connectIrcClient(options) {
436
455
  const normalizedTarget = sanitizeIrcTarget(target);
437
456
  const cleaned = sanitizeIrcOutboundText(text);
438
457
  if (!cleaned) return;
458
+ const lineOverheadBytes = Buffer.byteLength(`PRIVMSG ${normalizedTarget} :\r\n`, "utf8");
459
+ const maxChunkBytes = IRC_MAX_LINE_BYTES - lineOverheadBytes;
439
460
  let remaining = cleaned;
440
461
  while (remaining.length > 0) {
441
- let chunk = remaining;
442
- if (chunk.length > messageChunkMaxChars) {
443
- let splitAt = chunk.lastIndexOf(" ", messageChunkMaxChars);
444
- if (splitAt < Math.floor(messageChunkMaxChars / 2)) splitAt = messageChunkMaxChars;
445
- chunk = chunk.slice(0, splitAt).trim();
446
- }
447
- if (!chunk) break;
462
+ const chunk = takeIrcPrivmsgChunk(remaining, messageChunkMaxChars, maxChunkBytes).trim();
448
463
  sendRaw(`PRIVMSG ${normalizedTarget} :${chunk}`);
449
464
  remaining = remaining.slice(chunk.length).trimStart();
450
465
  }
@@ -481,7 +496,8 @@ async function connectIrcClient(options) {
481
496
  const line = parseIrcLine(rawLine);
482
497
  if (!line) continue;
483
498
  if (line.command === "PING") {
484
- sendRaw(`PONG :${line.trailing != null ? line.trailing : line.params[0] != null ? line.params[0] : ""}`);
499
+ const payload = line.trailing != null ? line.trailing : line.params[0] != null ? line.params[0] : "";
500
+ sendRaw(`PONG :${payload}`);
485
501
  continue;
486
502
  }
487
503
  if (line.command === "NICK") {
@@ -566,7 +582,10 @@ async function connectIrcClient(options) {
566
582
  socket.once("close", () => {
567
583
  if (!closed) {
568
584
  closed = true;
585
+ removeAbortListener?.();
586
+ removeAbortListener = null;
569
587
  if (!ready) fail(/* @__PURE__ */ new Error("IRC connection closed before ready"));
588
+ else options.onDisconnect?.();
570
589
  }
571
590
  });
572
591
  if (options.abortSignal) {
@@ -583,7 +602,12 @@ async function connectIrcClient(options) {
583
602
  removeAbortListener = () => options.abortSignal?.removeEventListener("abort", abort);
584
603
  }
585
604
  }
586
- await withTimeout(readyPromise, timeoutMs, "IRC connect");
605
+ try {
606
+ await withTimeout(readyPromise, timeoutMs, "IRC connect");
607
+ } catch (error) {
608
+ close();
609
+ throw error;
610
+ }
587
611
  return {
588
612
  get nick() {
589
613
  return currentNick;
@@ -636,6 +660,25 @@ function normalizeIrcMessagingTarget(raw) {
636
660
  if (!target || !looksLikeIrcTargetId(target)) return;
637
661
  return target;
638
662
  }
663
+ function resolveIrcOutboundSessionRoute(params) {
664
+ const target = normalizeIrcMessagingTarget(params.target);
665
+ if (!target) return null;
666
+ const chatType = isChannelTarget(target) ? "group" : "direct";
667
+ return buildChannelOutboundSessionRoute({
668
+ cfg: params.cfg,
669
+ agentId: params.agentId,
670
+ channel: "irc",
671
+ accountId: params.accountId,
672
+ recipientSessionExact: chatType === "direct" ? "direct-alias" : false,
673
+ peer: {
674
+ kind: chatType,
675
+ id: target
676
+ },
677
+ chatType,
678
+ from: `irc:${target}`,
679
+ to: target
680
+ });
681
+ }
639
682
  function looksLikeIrcTargetId(raw) {
640
683
  const trimmed = raw.trim();
641
684
  if (!trimmed) return false;
@@ -903,21 +946,22 @@ const ircSetupAdapter = {
903
946
  name: setupInput.name
904
947
  });
905
948
  const portInput = typeof setupInput.port === "number" ? String(setupInput.port) : setupInput.port ?? "";
949
+ const patch = {
950
+ enabled: true,
951
+ host: setupInput.host?.trim(),
952
+ port: portInput ? parsePort(portInput, setupInput.tls === false ? 6667 : 6697) : void 0,
953
+ tls: setupInput.tls,
954
+ nick: setupInput.nick?.trim(),
955
+ username: setupInput.username?.trim(),
956
+ realname: setupInput.realname?.trim(),
957
+ password: setupInput.password?.trim(),
958
+ channels: setupInput.channels
959
+ };
906
960
  return patchScopedAccountConfig({
907
961
  cfg: namedConfig,
908
962
  channelKey: channel$1,
909
963
  accountId,
910
- patch: {
911
- enabled: true,
912
- host: setupInput.host?.trim(),
913
- port: portInput ? parsePort(portInput, setupInput.tls === false ? 6667 : 6697) : void 0,
914
- tls: setupInput.tls,
915
- nick: setupInput.nick?.trim(),
916
- username: setupInput.username?.trim(),
917
- realname: setupInput.realname?.trim(),
918
- password: setupInput.password?.trim(),
919
- channels: setupInput.channels
920
- }
964
+ patch
921
965
  });
922
966
  }
923
967
  };
@@ -1286,7 +1330,7 @@ const meta = {
1286
1330
  systemImage: "number",
1287
1331
  markdownCapable: true
1288
1332
  };
1289
- const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-CdlR5F_m.js"));
1333
+ const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-CLTE89ol.js"));
1290
1334
  function normalizePairingTarget(raw) {
1291
1335
  const normalized = normalizeIrcAllowEntry(raw);
1292
1336
  if (!normalized) return "";
@@ -1423,6 +1467,7 @@ const ircPlugin = createChatChannelPlugin({
1423
1467
  messaging: {
1424
1468
  targetPrefixes: ["irc"],
1425
1469
  normalizeTarget: normalizeIrcMessagingTarget,
1470
+ resolveOutboundSessionRoute: (params) => resolveIrcOutboundSessionRoute(params),
1426
1471
  targetResolver: {
1427
1472
  looksLikeId: looksLikeIrcTargetId,
1428
1473
  hint: "<#channel|nick>"
@@ -1,2 +1,2 @@
1
- import { t as ircPlugin } from "./channel-BTyt-Cxx.js";
1
+ import { t as ircPlugin } from "./channel-C-P2GolT.js";
2
2
  export { ircPlugin };
@@ -1,4 +1,4 @@
1
- import { a as resolveIrcRequireMention, 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-BTyt-Cxx.js";
1
+ import { a as resolveIrcRequireMention, 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-C-P2GolT.js";
2
2
  import { t as getIrcRuntime } from "./runtime-DZfYm3vY.js";
3
3
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
4
4
  import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
@@ -331,6 +331,7 @@ async function handleIrcInbound(params) {
331
331
  }
332
332
  //#endregion
333
333
  //#region extensions/irc/src/monitor.ts
334
+ const IRC_MONITOR_RECONNECT_DELAY_MS = 1e3;
334
335
  function resolveIrcInboundTarget(params) {
335
336
  const rawTarget = params.target;
336
337
  if (isChannelTarget(rawTarget)) return {
@@ -358,67 +359,114 @@ async function monitorIrcProvider(opts) {
358
359
  accountId: account.accountId
359
360
  });
360
361
  let client = null;
361
- client = await connectIrcClient(buildIrcConnectOptions(account, {
362
- channels: account.config.channels,
363
- abortSignal: opts.abortSignal,
364
- onLine: (line) => {
365
- if (core.logging.shouldLogVerbose()) logger.debug?.(`[${account.accountId}] << ${line}`);
366
- },
367
- onNotice: (text, target) => {
368
- if (core.logging.shouldLogVerbose()) logger.debug?.(`[${account.accountId}] notice ${target ?? ""}: ${text}`);
369
- },
370
- onError: (error) => {
371
- logger.error(`[${account.accountId}] IRC error: ${error.message}`);
372
- },
373
- onPrivmsg: async (event) => {
374
- if (!client) return;
375
- if (normalizeLowercaseStringOrEmpty(event.senderNick) === normalizeLowercaseStringOrEmpty(client.nick)) return;
376
- const inboundTarget = resolveIrcInboundTarget({
377
- target: event.target,
378
- senderNick: event.senderNick
379
- });
380
- const message = {
381
- messageId: makeIrcMessageId(),
382
- target: inboundTarget.target,
383
- rawTarget: inboundTarget.rawTarget,
384
- senderNick: event.senderNick,
385
- senderUser: event.senderUser,
386
- senderHost: event.senderHost,
387
- text: event.text,
388
- timestamp: Date.now(),
389
- isGroup: inboundTarget.isGroup
390
- };
391
- core.channel.activity.record({
392
- channel: "irc",
393
- accountId: account.accountId,
394
- direction: "inbound",
395
- at: message.timestamp
362
+ let reconnectTimer = null;
363
+ let stopped = false;
364
+ const monitorAbort = new AbortController();
365
+ let removeAbortListener = null;
366
+ if (opts.abortSignal) {
367
+ const forwardAbort = () => monitorAbort.abort();
368
+ if (opts.abortSignal.aborted) forwardAbort();
369
+ else {
370
+ opts.abortSignal.addEventListener("abort", forwardAbort, { once: true });
371
+ removeAbortListener = () => opts.abortSignal?.removeEventListener("abort", forwardAbort);
372
+ }
373
+ }
374
+ function scheduleReconnect() {
375
+ if (stopped || monitorAbort.signal.aborted || reconnectTimer) return;
376
+ reconnectTimer = setTimeout(() => {
377
+ reconnectTimer = null;
378
+ connect().catch((error) => {
379
+ if (stopped || monitorAbort.signal.aborted) return;
380
+ const message = error instanceof Error ? error.message : String(error);
381
+ logger.error(`[${account.accountId}] IRC reconnect failed: ${message}`);
382
+ scheduleReconnect();
396
383
  });
397
- if (opts.onMessage) {
398
- await opts.onMessage(message, client);
399
- return;
384
+ }, IRC_MONITOR_RECONNECT_DELAY_MS);
385
+ }
386
+ async function connect() {
387
+ if (stopped || monitorAbort.signal.aborted) return;
388
+ const nextClient = await connectIrcClient(buildIrcConnectOptions(account, {
389
+ channels: account.config.channels,
390
+ abortSignal: monitorAbort.signal,
391
+ onLine: (line) => {
392
+ if (core.logging.shouldLogVerbose()) logger.debug?.(`[${account.accountId}] << ${line}`);
393
+ },
394
+ onNotice: (text, target) => {
395
+ if (core.logging.shouldLogVerbose()) logger.debug?.(`[${account.accountId}] notice ${target ?? ""}: ${text}`);
396
+ },
397
+ onError: (error) => {
398
+ logger.error(`[${account.accountId}] IRC error: ${error.message}`);
399
+ },
400
+ onDisconnect: () => {
401
+ if (stopped || monitorAbort.signal.aborted) return;
402
+ client = null;
403
+ logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${IRC_MONITOR_RECONNECT_DELAY_MS}ms`);
404
+ scheduleReconnect();
405
+ },
406
+ onPrivmsg: async (event) => {
407
+ if (!client) return;
408
+ if (normalizeLowercaseStringOrEmpty(event.senderNick) === normalizeLowercaseStringOrEmpty(client.nick)) return;
409
+ const inboundTarget = resolveIrcInboundTarget({
410
+ target: event.target,
411
+ senderNick: event.senderNick
412
+ });
413
+ const message = {
414
+ messageId: makeIrcMessageId(),
415
+ target: inboundTarget.target,
416
+ rawTarget: inboundTarget.rawTarget,
417
+ senderNick: event.senderNick,
418
+ senderUser: event.senderUser,
419
+ senderHost: event.senderHost,
420
+ text: event.text,
421
+ timestamp: Date.now(),
422
+ isGroup: inboundTarget.isGroup
423
+ };
424
+ core.channel.activity.record({
425
+ channel: "irc",
426
+ accountId: account.accountId,
427
+ direction: "inbound",
428
+ at: message.timestamp
429
+ });
430
+ if (opts.onMessage) {
431
+ await opts.onMessage(message, client);
432
+ return;
433
+ }
434
+ await handleIrcInbound({
435
+ message,
436
+ account,
437
+ config: cfg,
438
+ runtime,
439
+ connectedNick: client.nick,
440
+ sendReply: async (target, text) => {
441
+ client?.sendPrivmsg(target, text);
442
+ opts.statusSink?.({ lastOutboundAt: Date.now() });
443
+ core.channel.activity.record({
444
+ channel: "irc",
445
+ accountId: account.accountId,
446
+ direction: "outbound"
447
+ });
448
+ },
449
+ statusSink: opts.statusSink
450
+ });
400
451
  }
401
- await handleIrcInbound({
402
- message,
403
- account,
404
- config: cfg,
405
- runtime,
406
- connectedNick: client.nick,
407
- sendReply: async (target, text) => {
408
- client?.sendPrivmsg(target, text);
409
- opts.statusSink?.({ lastOutboundAt: Date.now() });
410
- core.channel.activity.record({
411
- channel: "irc",
412
- accountId: account.accountId,
413
- direction: "outbound"
414
- });
415
- },
416
- statusSink: opts.statusSink
417
- });
452
+ }));
453
+ if (stopped || monitorAbort.signal.aborted) {
454
+ nextClient.quit("shutdown");
455
+ return;
418
456
  }
419
- }));
420
- logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${client.nick}`);
457
+ client = nextClient;
458
+ logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
459
+ }
460
+ await connect();
421
461
  return { stop: () => {
462
+ stopped = true;
463
+ removeAbortListener?.();
464
+ removeAbortListener = null;
465
+ if (!monitorAbort.signal.aborted) monitorAbort.abort();
466
+ if (reconnectTimer) {
467
+ clearTimeout(reconnectTimer);
468
+ reconnectTimer = null;
469
+ }
422
470
  client?.quit("shutdown");
423
471
  client = null;
424
472
  } };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@openclaw/irc",
3
- "version": "2026.7.1-beta.2",
3
+ "version": "2026.7.1-beta.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@openclaw/irc",
9
- "version": "2026.7.1-beta.2",
9
+ "version": "2026.7.1-beta.4",
10
10
  "dependencies": {
11
11
  "zod": "4.4.3"
12
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/irc",
3
- "version": "2026.7.1-beta.2",
3
+ "version": "2026.7.1-beta.4",
4
4
  "description": "OpenClaw IRC channel plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -39,10 +39,10 @@
39
39
  }
40
40
  },
41
41
  "compat": {
42
- "pluginApi": ">=2026.7.1-beta.2"
42
+ "pluginApi": ">=2026.7.1-beta.4"
43
43
  },
44
44
  "build": {
45
- "openclawVersion": "2026.7.1-beta.2",
45
+ "openclawVersion": "2026.7.1-beta.4",
46
46
  "bundledDist": false
47
47
  },
48
48
  "release": {
@@ -68,7 +68,7 @@
68
68
  "README.md"
69
69
  ],
70
70
  "peerDependencies": {
71
- "openclaw": ">=2026.7.1-beta.2"
71
+ "openclaw": ">=2026.7.1-beta.4"
72
72
  },
73
73
  "peerDependenciesMeta": {
74
74
  "openclaw": {