opencode-discord-notify 0.5.0 → 0.6.0

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/README-JP.md CHANGED
@@ -65,7 +65,8 @@ OpenCode を再起動してください。
65
65
  - **指定可能キー**: `sessionID`, `permissionID`, `type`, `pattern`, `messageID`, `callID`, `partID`, `role`, `directory`, `projectID`
66
66
  - **デフォルト動作**(未設定・空文字): 全て無効化(何も送信しない)
67
67
  - **全て送信したい場合**: 全キーを列挙してください
68
- - **注意**: `session.created` は `DISCORD_SEND_PARAMS` に関わらず `sessionID`, `projectID`, `directory` を必ず含みます
68
+ - **注意**: `session.created` は `DISCORD_SEND_PARAMS` に関わらず `sessionID` を必ず含みます
69
+ - `DISCORD_WEBHOOK_FALLBACK_URL`: フォールバック先のテキストチャネル webhook URL(任意。設定すると、`@everyone` または `@here` を含むメッセージが自動的にこの webhook にも送信されます。Forum webhook ではメンションが ping しない Discord の仕様上、有用)
69
70
 
70
71
  ## 仕様メモ
71
72
 
@@ -91,6 +92,11 @@ OpenCode を再起動してください。
91
92
  - `tool`: 通知しない
92
93
  - `reasoning`: 通知しない(内部思考が含まれる可能性があるため)
93
94
  - `DISCORD_SEND_PARAMS` の制御対象は embed の fields のみです(title/description/content/timestamp などは対象外)。また `share` は fields としては送りません(Session started の embed URL には `shareUrl` を使います)。
95
+ - `DISCORD_WEBHOOK_FALLBACK_URL` が設定されている場合:
96
+ - `@everyone` または `@here` を含むメッセージ(`DISCORD_WEBHOOK_COMPLETE_MENTION` または `DISCORD_WEBHOOK_PERMISSION_MENTION` 経由)は、Forum webhook(スレッド投稿)とフォールバック先のテキストチャネル webhook の両方に自動的に送信されます。
97
+ - これにより、Forum webhook ではメンションが ping しない Discord の仕様上、確実に通知を届けることができます。
98
+ - フォールバックメッセージには、`DISCORD_SEND_PARAMS` の設定に関わらず、常に `sessionID` と `thread title` フィールドが含まれます(テキストチャネルでのコンテキスト提供のため)。`thread title` は Forum のスレッド名と同じ値(最初のユーザーテキスト、または存在しない場合はセッションタイトル)です。
99
+ - フォールバック送信は Forum スレッドキューとは独立しており、即座に実行されます。
94
100
 
95
101
  ## 動作確認(手動)
96
102
 
package/README.md CHANGED
@@ -67,7 +67,8 @@ Optional:
67
67
  - **Allowed keys**: `sessionID`, `permissionID`, `type`, `pattern`, `messageID`, `callID`, `partID`, `role`, `directory`, `projectID`
68
68
  - **Default behavior** (unset/empty): all fields are disabled (nothing sent)
69
69
  - **To send all fields**: list all keys explicitly
70
- - **Note**: `session.created` always includes `sessionID`, `projectID`, `directory` regardless
70
+ - **Note**: `session.created` always includes `sessionID` regardless
71
+ - `DISCORD_WEBHOOK_FALLBACK_URL`: fallback webhook URL for text channel (optional; when set, messages containing `@everyone` or `@here` are automatically sent to this webhook as well; useful because Forum webhooks may not ping mentions due to Discord behavior)
71
72
 
72
73
  ## Notes / behavior
73
74
 
@@ -93,6 +94,11 @@ Optional:
93
94
  - `tool`: not posted
94
95
  - `reasoning`: not posted (to avoid exposing internal thoughts)
95
96
  - `DISCORD_SEND_PARAMS` controls embed fields only (it does not affect title/description/content/timestamp). `share` is not an embed field (but Session started uses `shareUrl` as the embed URL).
97
+ - When `DISCORD_WEBHOOK_FALLBACK_URL` is set:
98
+ - Messages containing `@everyone` or `@here` (via `DISCORD_WEBHOOK_COMPLETE_MENTION` or `DISCORD_WEBHOOK_PERMISSION_MENTION`) are automatically sent to both the Forum webhook (as a thread post) and the fallback text channel webhook.
99
+ - This ensures reliable notifications while maintaining thread structure in Forums, since Forum webhooks may not ping mentions due to Discord behavior.
100
+ - Fallback messages always include `sessionID` and `thread title` fields, regardless of `DISCORD_SEND_PARAMS` settings, to provide context in the text channel. The `thread title` is the same as the Forum thread name (first user text, or session title if unavailable).
101
+ - Fallback sending is independent of the Forum thread queue and happens immediately.
96
102
 
97
103
  ## Manual test
98
104
 
package/dist/index.js CHANGED
@@ -260,6 +260,49 @@ async function postDiscordWebhook(input, deps) {
260
260
  `Discord webhook failed: ${response.status} ${response.statusText} ${text}`
261
261
  );
262
262
  }
263
+ async function postFallbackIfNeeded(input, deps) {
264
+ const {
265
+ body,
266
+ mention,
267
+ sessionID,
268
+ fallbackUrl,
269
+ firstUserTextBySession,
270
+ lastSessionInfo
271
+ } = input;
272
+ if (!fallbackUrl || !mention) return;
273
+ const fallbackBody = {
274
+ ...body,
275
+ // thread_nameは削除(テキストチャネルでは不要)
276
+ thread_name: void 0
277
+ };
278
+ if (fallbackBody.embeds && fallbackBody.embeds.length > 0) {
279
+ const originalEmbed = fallbackBody.embeds[0];
280
+ const threadTitle = firstUserTextBySession.get(sessionID) || lastSessionInfo.get(sessionID)?.title;
281
+ const additionalFields = buildFields([
282
+ ["sessionID", sessionID],
283
+ ["thread title", threadTitle]
284
+ ]);
285
+ fallbackBody.embeds = [
286
+ {
287
+ ...originalEmbed,
288
+ fields: [
289
+ ...originalEmbed.fields ?? [],
290
+ ...additionalFields ?? []
291
+ ]
292
+ }
293
+ ];
294
+ }
295
+ try {
296
+ await postDiscordWebhook(
297
+ {
298
+ webhookUrl: fallbackUrl,
299
+ body: fallbackBody
300
+ },
301
+ deps
302
+ );
303
+ } catch (e) {
304
+ }
305
+ }
263
306
  var GLOBAL_GUARD_KEY = "__opencode_discord_notify_registered__";
264
307
  var plugin = async ({ client }) => {
265
308
  const globalWithGuard = globalThis;
@@ -281,6 +324,7 @@ var plugin = async ({ client }) => {
281
324
  const showErrorAlert = showErrorAlertRaw !== "0";
282
325
  const waitOnRateLimitMs = DEFAULT_RATE_LIMIT_WAIT_MS;
283
326
  const sendParams = parseSendParams(getEnv("DISCORD_SEND_PARAMS"));
327
+ const fallbackWebhookUrl = (getEnv("DISCORD_WEBHOOK_FALLBACK_URL") ?? "").trim() || void 0;
284
328
  const lastAlertAtByKey = /* @__PURE__ */ new Map();
285
329
  const sentTextPartIds = /* @__PURE__ */ new Set();
286
330
  const showToast = async ({ title, message, variant }) => {
@@ -517,11 +561,7 @@ var plugin = async ({ client }) => {
517
561
  ["directory", info?.directory],
518
562
  ["share", shareUrl]
519
563
  ],
520
- withForcedSendParams(sendParams, [
521
- "sessionID",
522
- "projectID",
523
- "directory"
524
- ])
564
+ withForcedSendParams(sendParams, ["sessionID"])
525
565
  )
526
566
  )
527
567
  };
@@ -533,6 +573,7 @@ var plugin = async ({ client }) => {
533
573
  const p = event.properties;
534
574
  const sessionID = p?.sessionID;
535
575
  if (!sessionID) return;
576
+ const mention = buildPermissionMention();
536
577
  const embed = {
537
578
  title: "Permission required",
538
579
  description: p?.title,
@@ -552,33 +593,56 @@ var plugin = async ({ client }) => {
552
593
  )
553
594
  )
554
595
  };
555
- const mention = buildPermissionMention();
556
- enqueueToThread(sessionID, {
596
+ const body = {
557
597
  content: mention ? `${mention.content}` : void 0,
558
598
  allowed_mentions: mention?.allowed_mentions,
559
599
  embeds: [embed]
560
- });
600
+ };
601
+ enqueueToThread(sessionID, body);
602
+ await postFallbackIfNeeded(
603
+ {
604
+ body,
605
+ mention,
606
+ sessionID,
607
+ fallbackUrl: fallbackWebhookUrl,
608
+ firstUserTextBySession,
609
+ lastSessionInfo
610
+ },
611
+ postDeps
612
+ );
561
613
  return;
562
614
  }
563
615
  case "session.idle": {
564
616
  const sessionID = event.properties?.sessionID;
565
617
  if (!sessionID) return;
618
+ const mention = buildCompleteMention();
566
619
  const embed = {
567
620
  title: "Session completed",
568
621
  color: COLORS.success,
569
622
  fields: buildFields(
570
623
  filterSendFields(
571
624
  [["sessionID", sessionID]],
572
- withForcedSendParams(sendParams, ["sessionID"])
625
+ sendParams
573
626
  )
574
627
  )
575
628
  };
576
- const mention = buildCompleteMention();
577
- enqueueToThread(sessionID, {
629
+ const body = {
578
630
  content: mention ? `${mention.content}` : void 0,
579
631
  allowed_mentions: mention?.allowed_mentions,
580
632
  embeds: [embed]
581
- });
633
+ };
634
+ enqueueToThread(sessionID, body);
635
+ await postFallbackIfNeeded(
636
+ {
637
+ body,
638
+ mention,
639
+ sessionID,
640
+ fallbackUrl: fallbackWebhookUrl,
641
+ firstUserTextBySession,
642
+ lastSessionInfo
643
+ },
644
+ postDeps
645
+ );
582
646
  return;
583
647
  }
584
648
  case "session.error": {
@@ -605,11 +669,24 @@ var plugin = async ({ client }) => {
605
669
  };
606
670
  if (!sessionID) return;
607
671
  const mention = buildCompleteMention();
608
- enqueueToThread(sessionID, {
609
- content: mention ? `$Session error` : void 0,
672
+ const body = {
673
+ // 🐛 既存バグ修正: `$Session error` `${mention.content}`
674
+ content: mention ? `${mention.content}` : void 0,
610
675
  allowed_mentions: mention?.allowed_mentions,
611
676
  embeds: [embed]
612
- });
677
+ };
678
+ enqueueToThread(sessionID, body);
679
+ await postFallbackIfNeeded(
680
+ {
681
+ body,
682
+ mention,
683
+ sessionID,
684
+ fallbackUrl: fallbackWebhookUrl,
685
+ firstUserTextBySession,
686
+ lastSessionInfo
687
+ },
688
+ postDeps
689
+ );
613
690
  await flushPending(sessionID);
614
691
  return;
615
692
  }
@@ -691,7 +768,8 @@ plugin.__test__ = {
691
768
  toIsoTimestamp,
692
769
  postDiscordWebhook,
693
770
  parseSendParams,
694
- getTodoStatusMarker
771
+ getTodoStatusMarker,
772
+ postFallbackIfNeeded
695
773
  };
696
774
  var index_default = plugin;
697
775
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-discord-notify",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A plugin that posts OpenCode events to a Discord webhook.",
5
5
  "license": "MIT",
6
6
  "type": "module",