@nextclaw/channel-runtime 0.1.4 → 0.1.5

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 (2) hide show
  1. package/dist/index.js +171 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -228,6 +228,9 @@ var DEFAULT_MEDIA_MAX_MB = 8;
228
228
  var MEDIA_FETCH_TIMEOUT_MS = 15e3;
229
229
  var TYPING_HEARTBEAT_MS = 8e3;
230
230
  var TYPING_AUTO_STOP_MS = 45e3;
231
+ var DISCORD_TEXT_LIMIT = 2e3;
232
+ var DISCORD_MAX_LINES_PER_MESSAGE = 17;
233
+ var FENCE_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
231
234
  var DiscordChannel = class extends BaseChannel {
232
235
  name = "discord";
233
236
  client = null;
@@ -285,16 +288,23 @@ var DiscordChannel = class extends BaseChannel {
285
288
  }
286
289
  this.stopTyping(msg.chatId);
287
290
  const textChannel = channel;
288
- const payload = {
289
- content: msg.content ?? ""
290
- };
291
- if (msg.replyTo) {
292
- payload.reply = { messageReference: msg.replyTo };
291
+ const chunks = chunkDiscordText(msg.content ?? "");
292
+ if (chunks.length === 0) {
293
+ return;
293
294
  }
294
- if (msg.metadata?.silent === true) {
295
- payload.flags = MessageFlags.SuppressNotifications;
295
+ const flags = msg.metadata?.silent === true ? MessageFlags.SuppressNotifications : void 0;
296
+ for (const chunk of chunks) {
297
+ const payload = {
298
+ content: chunk
299
+ };
300
+ if (msg.replyTo) {
301
+ payload.reply = { messageReference: msg.replyTo };
302
+ }
303
+ if (flags !== void 0) {
304
+ payload.flags = flags;
305
+ }
306
+ await textChannel.send(payload);
296
307
  }
297
- await textChannel.send(payload);
298
308
  }
299
309
  async handleIncoming(message) {
300
310
  const selfUserId = this.client?.user?.id;
@@ -533,6 +543,159 @@ function buildAttachmentSummary(attachments) {
533
543
  }
534
544
  return `<media:document> (${count} ${count === 1 ? "file" : "files"})`;
535
545
  }
546
+ function countLines(text) {
547
+ if (!text) {
548
+ return 0;
549
+ }
550
+ return text.split("\n").length;
551
+ }
552
+ function parseFenceLine(line) {
553
+ const match = line.match(FENCE_RE);
554
+ if (!match) {
555
+ return null;
556
+ }
557
+ const indent = match[1] ?? "";
558
+ const marker = match[2] ?? "";
559
+ return {
560
+ indent,
561
+ markerChar: marker[0] ?? "`",
562
+ markerLen: marker.length,
563
+ openLine: line
564
+ };
565
+ }
566
+ function closeFenceLine(openFence) {
567
+ return `${openFence.indent}${openFence.markerChar.repeat(openFence.markerLen)}`;
568
+ }
569
+ function closeFenceIfNeeded(text, openFence) {
570
+ if (!openFence) {
571
+ return text;
572
+ }
573
+ const closeLine = closeFenceLine(openFence);
574
+ if (!text) {
575
+ return closeLine;
576
+ }
577
+ if (!text.endsWith("\n")) {
578
+ return `${text}
579
+ ${closeLine}`;
580
+ }
581
+ return `${text}${closeLine}`;
582
+ }
583
+ function splitLongLine(line, maxChars, opts) {
584
+ const limit = Math.max(1, Math.floor(maxChars));
585
+ if (line.length <= limit) {
586
+ return [line];
587
+ }
588
+ const chunks = [];
589
+ let remaining = line;
590
+ while (remaining.length > limit) {
591
+ if (opts.preserveWhitespace) {
592
+ chunks.push(remaining.slice(0, limit));
593
+ remaining = remaining.slice(limit);
594
+ continue;
595
+ }
596
+ const window = remaining.slice(0, limit);
597
+ let breakIndex = -1;
598
+ for (let index = window.length - 1; index >= 0; index -= 1) {
599
+ if (/\s/.test(window[index])) {
600
+ breakIndex = index;
601
+ break;
602
+ }
603
+ }
604
+ if (breakIndex <= 0) {
605
+ breakIndex = limit;
606
+ }
607
+ chunks.push(remaining.slice(0, breakIndex));
608
+ remaining = remaining.slice(breakIndex);
609
+ }
610
+ if (remaining.length) {
611
+ chunks.push(remaining);
612
+ }
613
+ return chunks;
614
+ }
615
+ function chunkDiscordText(text, opts = {}) {
616
+ const maxChars = Math.max(1, Math.floor(opts.maxChars ?? DISCORD_TEXT_LIMIT));
617
+ const maxLines = Math.max(1, Math.floor(opts.maxLines ?? DISCORD_MAX_LINES_PER_MESSAGE));
618
+ const body = text ?? "";
619
+ if (!body) {
620
+ return [];
621
+ }
622
+ if (body.length <= maxChars && countLines(body) <= maxLines) {
623
+ return [body];
624
+ }
625
+ const lines = body.split("\n");
626
+ const chunks = [];
627
+ let current = "";
628
+ let currentLines = 0;
629
+ let openFence = null;
630
+ const flush = () => {
631
+ if (!current) {
632
+ return;
633
+ }
634
+ const payload = closeFenceIfNeeded(current, openFence);
635
+ if (payload.trim().length) {
636
+ chunks.push(payload);
637
+ }
638
+ current = "";
639
+ currentLines = 0;
640
+ if (openFence) {
641
+ current = openFence.openLine;
642
+ currentLines = 1;
643
+ }
644
+ };
645
+ for (const line of lines) {
646
+ const fenceInfo = parseFenceLine(line);
647
+ const wasInsideFence = openFence !== null;
648
+ let nextOpenFence = openFence;
649
+ if (fenceInfo) {
650
+ if (!openFence) {
651
+ nextOpenFence = fenceInfo;
652
+ } else if (openFence.markerChar === fenceInfo.markerChar && fenceInfo.markerLen >= openFence.markerLen) {
653
+ nextOpenFence = null;
654
+ }
655
+ }
656
+ const reserveChars = nextOpenFence ? closeFenceLine(nextOpenFence).length + 1 : 0;
657
+ const reserveLines = nextOpenFence ? 1 : 0;
658
+ const effectiveMaxChars = maxChars - reserveChars;
659
+ const effectiveMaxLines = maxLines - reserveLines;
660
+ const charLimit = effectiveMaxChars > 0 ? effectiveMaxChars : maxChars;
661
+ const lineLimit = effectiveMaxLines > 0 ? effectiveMaxLines : maxLines;
662
+ const prefixLength = current.length > 0 ? current.length + 1 : 0;
663
+ const segmentLimit = Math.max(1, charLimit - prefixLength);
664
+ const segments = splitLongLine(line, segmentLimit, {
665
+ preserveWhitespace: wasInsideFence
666
+ });
667
+ for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
668
+ const segment2 = segments[segmentIndex];
669
+ const isContinuation = segmentIndex > 0;
670
+ const delimiter = isContinuation ? "" : current.length > 0 ? "\n" : "";
671
+ const addition = `${delimiter}${segment2}`;
672
+ const nextLength = current.length + addition.length;
673
+ const nextLineCount = currentLines + (isContinuation ? 0 : 1);
674
+ const exceedsChars = nextLength > charLimit;
675
+ const exceedsLines = nextLineCount > lineLimit;
676
+ if ((exceedsChars || exceedsLines) && current.length > 0) {
677
+ flush();
678
+ }
679
+ if (current.length > 0) {
680
+ current += addition;
681
+ if (!isContinuation) {
682
+ currentLines += 1;
683
+ }
684
+ } else {
685
+ current = segment2;
686
+ currentLines = 1;
687
+ }
688
+ }
689
+ openFence = nextOpenFence;
690
+ }
691
+ if (current.length) {
692
+ const payload = closeFenceIfNeeded(current, openFence);
693
+ if (payload.trim().length) {
694
+ chunks.push(payload);
695
+ }
696
+ }
697
+ return chunks;
698
+ }
536
699
 
537
700
  // src/channels/email.ts
538
701
  import { ImapFlow } from "imapflow";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-runtime",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "description": "Runtime implementations for NextClaw builtin channel plugins.",
6
6
  "type": "module",