opencode-discord-notify 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +163 -87
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -264,6 +264,7 @@ var plugin = async ({ client }) => {
264
264
  const toastCooldownMs = 3e4;
265
265
  const sendParams = parseSendParams(getEnv("DISCORD_SEND_PARAMS"));
266
266
  const lastAlertAtByKey = /* @__PURE__ */ new Map();
267
+ const sentTextPartIds = /* @__PURE__ */ new Set();
267
268
  const showToast = async ({ title, message, variant }) => {
268
269
  try {
269
270
  await client.tui.showToast({
@@ -424,6 +425,50 @@ var plugin = async ({ client }) => {
424
425
  function buildPermissionMention() {
425
426
  return buildMention(permissionMention, "DISCORD_WEBHOOK_PERMISSION_MENTION");
426
427
  }
428
+ function escapeDiscordMention(text) {
429
+ return text.replace(/^@/g, "@\u200B");
430
+ }
431
+ async function handleTextPart(input) {
432
+ const { part, role, sessionID, messageID, replay } = input;
433
+ const partID = part?.id;
434
+ if (!partID) return;
435
+ if (!replay && role === "assistant" && !part?.time?.end) return;
436
+ if (sentTextPartIds.has(partID)) return;
437
+ sentTextPartIds.add(partID);
438
+ const text = escapeDiscordMention(safeString(part?.text));
439
+ if (role === "user" && excludeInputContext && isInputContextText(text)) {
440
+ return;
441
+ }
442
+ if (role === "user" && text.trim() === "" || text.trim() === "(empty)") {
443
+ return;
444
+ }
445
+ if (role === "user" && !firstUserTextBySession.has(sessionID)) {
446
+ const normalized = normalizeThreadTitle(text);
447
+ if (normalized) firstUserTextBySession.set(sessionID, normalized);
448
+ }
449
+ const embed = {
450
+ title: getTextPartEmbedTitle(role),
451
+ color: COLORS.info,
452
+ fields: buildFields(
453
+ filterSendFields(
454
+ [
455
+ ["sessionID", sessionID],
456
+ ["messageID", messageID],
457
+ ["partID", partID],
458
+ ["role", role]
459
+ ],
460
+ sendParams
461
+ )
462
+ ),
463
+ description: truncateText(text || "(empty)", 4096)
464
+ };
465
+ enqueueToThread(sessionID, { embeds: [embed] });
466
+ if (role === "user") {
467
+ await flushPending(sessionID);
468
+ } else if (shouldFlush(sessionID)) {
469
+ await flushPending(sessionID);
470
+ }
471
+ }
427
472
  function setIfChanged(map, key, next) {
428
473
  const prev = map.get(key);
429
474
  if (prev === next) return false;
@@ -465,7 +510,102 @@ var plugin = async ({ client }) => {
465
510
  };
466
511
  lastSessionInfo.set(sessionID, { title, shareUrl });
467
512
  enqueueToThread(sessionID, { embeds: [embed] });
468
- if (shouldFlush(sessionID)) await flushPending(sessionID);
513
+ return;
514
+ }
515
+ case "permission.updated": {
516
+ const p = event.properties;
517
+ const sessionID = p?.sessionID;
518
+ if (!sessionID) return;
519
+ const embed = {
520
+ title: "Permission required",
521
+ description: p?.title,
522
+ color: COLORS.warning,
523
+ timestamp: toIsoTimestamp(p?.time?.created),
524
+ fields: buildFields(
525
+ filterSendFields(
526
+ [
527
+ ["sessionID", sessionID],
528
+ ["permissionID", p?.id],
529
+ ["type", p?.type],
530
+ ["pattern", p?.pattern],
531
+ ["messageID", p?.messageID],
532
+ ["callID", p?.callID]
533
+ ],
534
+ sendParams
535
+ )
536
+ )
537
+ };
538
+ const mention = buildPermissionMention();
539
+ enqueueToThread(sessionID, {
540
+ content: mention ? `${mention.content}` : void 0,
541
+ allowed_mentions: mention?.allowed_mentions,
542
+ embeds: [embed]
543
+ });
544
+ return;
545
+ }
546
+ case "session.idle": {
547
+ const sessionID = event.properties?.sessionID;
548
+ if (!sessionID) return;
549
+ const embed = {
550
+ title: "Session completed",
551
+ color: COLORS.success,
552
+ fields: buildFields(
553
+ filterSendFields(
554
+ [["sessionID", sessionID]],
555
+ withForcedSendParams(sendParams, ["sessionID"])
556
+ )
557
+ )
558
+ };
559
+ const mention = buildCompleteMention();
560
+ enqueueToThread(sessionID, {
561
+ content: mention ? `${mention.content}` : void 0,
562
+ allowed_mentions: mention?.allowed_mentions,
563
+ embeds: [embed]
564
+ });
565
+ return;
566
+ }
567
+ case "session.error": {
568
+ const p = event.properties;
569
+ const sessionID = p?.sessionID;
570
+ const errorStr = safeString(p?.error);
571
+ const embed = {
572
+ title: "Session error",
573
+ color: COLORS.error,
574
+ description: errorStr ? errorStr.length > 4096 ? errorStr.slice(0, 4093) + "..." : errorStr : void 0,
575
+ fields: buildFields(
576
+ filterSendFields(
577
+ [["sessionID", sessionID]],
578
+ withForcedSendParams(sendParams, [
579
+ "sessionID",
580
+ "projectID",
581
+ "directory"
582
+ ])
583
+ )
584
+ )
585
+ };
586
+ if (!sessionID) return;
587
+ const mention = buildCompleteMention();
588
+ enqueueToThread(sessionID, {
589
+ content: mention ? `$Session error` : void 0,
590
+ allowed_mentions: mention?.allowed_mentions,
591
+ embeds: [embed]
592
+ });
593
+ await flushPending(sessionID);
594
+ return;
595
+ }
596
+ case "todo.updated": {
597
+ const p = event.properties;
598
+ const sessionID = p?.sessionID;
599
+ if (!sessionID) return;
600
+ const embed = {
601
+ title: "Todo updated",
602
+ color: COLORS.info,
603
+ fields: buildFields(
604
+ filterSendFields([["sessionID", sessionID]], sendParams)
605
+ ),
606
+ description: buildTodoChecklist(p?.todos)
607
+ };
608
+ enqueueToThread(sessionID, { embeds: [embed] });
469
609
  return;
470
610
  }
471
611
  case "message.updated": {
@@ -473,52 +613,22 @@ var plugin = async ({ client }) => {
473
613
  const messageID = info?.id;
474
614
  const role = info?.role;
475
615
  if (!messageID) return;
476
- if (role === "user" || role === "assistant") {
477
- messageRoleById.set(messageID, role);
478
- const pendingParts = pendingTextPartsByMessageId.get(messageID);
479
- if (pendingParts?.length) {
480
- pendingTextPartsByMessageId.delete(messageID);
481
- for (const pendingPart of pendingParts) {
482
- const sessionID = pendingPart?.sessionID;
483
- const partID = pendingPart?.id;
484
- const type = pendingPart?.type;
485
- if (!sessionID || !partID || type !== "text") continue;
486
- const text = safeString(pendingPart?.text);
487
- if (role === "user" && excludeInputContext && isInputContextText(text)) {
488
- const snapshot = JSON.stringify({
489
- type,
490
- role,
491
- skipped: "input_context"
492
- });
493
- setIfChanged(/* @__PURE__ */ new Map(), partID, snapshot);
494
- continue;
495
- }
496
- if (role === "user" && !firstUserTextBySession.has(sessionID)) {
497
- const normalized = normalizeThreadTitle(text);
498
- if (normalized)
499
- firstUserTextBySession.set(sessionID, normalized);
500
- }
501
- const embed = {
502
- title: getTextPartEmbedTitle(role),
503
- color: COLORS.info,
504
- fields: buildFields(
505
- filterSendFields(
506
- [
507
- ["sessionID", sessionID],
508
- ["messageID", messageID],
509
- ["partID", partID],
510
- ["role", role]
511
- ],
512
- sendParams
513
- )
514
- ),
515
- description: truncateText(text || "(empty)", 4096)
516
- };
517
- enqueueToThread(sessionID, { embeds: [embed] });
518
- if (role === "user") await flushPending(sessionID);
519
- else if (shouldFlush(sessionID)) await flushPending(sessionID);
520
- }
521
- }
616
+ if (role !== "user" && role !== "assistant") return;
617
+ messageRoleById.set(messageID, role);
618
+ const pendingParts = pendingTextPartsByMessageId.get(messageID);
619
+ if (!pendingParts?.length) return;
620
+ pendingTextPartsByMessageId.delete(messageID);
621
+ for (const part of pendingParts) {
622
+ const sessionID = part?.sessionID;
623
+ const partID = part?.id;
624
+ if (!sessionID || !partID || part?.type !== "text") continue;
625
+ await handleTextPart({
626
+ part,
627
+ role,
628
+ sessionID,
629
+ messageID,
630
+ replay: true
631
+ });
522
632
  }
523
633
  return;
524
634
  }
@@ -538,46 +648,12 @@ var plugin = async ({ client }) => {
538
648
  pendingTextPartsByMessageId.set(messageID, list);
539
649
  return;
540
650
  }
541
- if (type === "text") {
542
- if (role === "assistant" && !part?.time?.end) return;
543
- const text = safeString(part?.text);
544
- if (role === "user" && excludeInputContext && isInputContextText(text)) {
545
- const snapshot = JSON.stringify({
546
- type,
547
- role,
548
- skipped: "input_context"
549
- });
550
- setIfChanged(/* @__PURE__ */ new Map(), partID, snapshot);
551
- return;
552
- }
553
- if (role === "user" && !firstUserTextBySession.has(sessionID)) {
554
- const normalized = normalizeThreadTitle(text);
555
- if (normalized)
556
- firstUserTextBySession.set(sessionID, normalized);
557
- }
558
- const embed = {
559
- title: getTextPartEmbedTitle(role),
560
- color: COLORS.info,
561
- fields: buildFields(
562
- filterSendFields(
563
- [
564
- ["sessionID", sessionID],
565
- ["messageID", messageID],
566
- ["partID", partID],
567
- ["role", role]
568
- ],
569
- sendParams
570
- )
571
- ),
572
- description: truncateText(text || "(empty)", 4096)
573
- };
574
- enqueueToThread(sessionID, { embeds: [embed] });
575
- if (role === "user") {
576
- await flushPending(sessionID);
577
- } else if (shouldFlush(sessionID)) {
578
- await flushPending(sessionID);
579
- }
580
- }
651
+ await handleTextPart({
652
+ part,
653
+ role,
654
+ sessionID,
655
+ messageID
656
+ });
581
657
  return;
582
658
  }
583
659
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-discord-notify",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "A plugin that posts OpenCode events to a Discord webhook.",
5
5
  "license": "MIT",
6
6
  "type": "module",