From d1f3a12bf1615c7c4ab99e6b80839eb3eb22334f Mon Sep 17 00:00:00 2001 From: Heinrich-XIAO <74563446+Heinrich-XIAO@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:28:35 -0500 Subject: [PATCH 1/6] fix: read config from home and event type --- bun.lock | 10 +++++++++- src/index.ts | 24 ++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 975e445..678d9ed 100644 --- a/bun.lock +++ b/bun.lock @@ -5,14 +5,20 @@ "": { "name": "opencode-discord-notification", "devDependencies": { + "@opencode-ai/plugin": "latest", "@types/bun": "latest", + "typescript": "latest", }, "peerDependencies": { - "typescript": "^5", + "@opencode-ai/plugin": "*", }, }, }, "packages": { + "@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.10", "", { "dependencies": { "@opencode-ai/sdk": "1.2.10", "zod": "4.1.8" } }, "sha512-Z1BMqNHnD8AGAEb+kUz0b2SOuiODwdQLdCA4aVGTXqkGzhiD44OVxr85MeoJ5AMTnnea9SnJ3jp9GAQ5riXA5g=="], + + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.10", "", {}, "sha512-SyXcVqry2hitPVvQtvXOhqsWyFhSycG/+LTLYXrcq8AFmd9FR7dyBSDB3f5Ol6IPkYOegk8P2Eg2kKPNSNiKGw=="], + "@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="], "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], @@ -22,5 +28,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], } } diff --git a/src/index.ts b/src/index.ts index 337036b..40e11b9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,12 +10,13 @@ interface DiscordWebhookConfig { export const DiscordNotificationPlugin: Plugin = async ({ client, project }) => { return { event: async ({ event }) => { + const eventType = (event as any)?.type; // 1. Handle Session Completed (Green) - if (event.type === "session.idle") { + if (eventType === "session.idle") { await handleNotification(client, project, event, "idle"); } // 2. Handle Permission Request (Orange) - else if (event.type === "permission.asked") { + else if (eventType === "permission.asked") { await handleNotification(client, project, event, "permission"); } }, @@ -31,10 +32,21 @@ async function handleNotification(client: any, project: any, event: any, type: " if (!config.webhookUrl) { try { - const configPath = "/var/home/frieser/.config/opencode/discord-notification-config.json"; - const configFile = Bun.file(configPath); - if (await configFile.exists()) { - config = await configFile.json(); + const candidatePaths: string[] = []; + const processHome = (globalThis as any)?.process?.env?.HOME; + if (processHome) { + candidatePaths.push(`${processHome}/.config/opencode/discord-notification-config.json`); + } + const bunHome = (globalThis as any)?.Bun?.env?.HOME || (globalThis as any)?.Bun?.env?.home; + if (bunHome && bunHome !== processHome) { + candidatePaths.push(`${bunHome}/.config/opencode/discord-notification-config.json`); + } + for (const candidate of candidatePaths) { + const configFile = Bun.file(candidate); + if (await configFile.exists()) { + config = await configFile.json(); + break; + } } } catch (e) {} } From 64e1fb2a05488b87d4955a8f7cf7c384d587ff01 Mon Sep 17 00:00:00 2001 From: Heinrich-XIAO <74563446+Heinrich-XIAO@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:07:25 -0500 Subject: [PATCH 2/6] feat: add discord bot dm support --- README.md | 16 ++++++++++++ src/index.ts | 72 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2a33bf1..f1f6a20 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,22 @@ Create a configuration file at `~/.config/opencode/discord-notification-config.j } ``` +You can optionally include a `mention` entry (for example `"<@123456789012345678>"`) to ping a Discord user or role whenever a notification fires. To additionally send the notification as a DM, provide `botToken` (your Discord bot token with message permissions) and `dmUserId` (the recipient’s Discord user ID); the plugin will open or reuse a DM channel before posting the embed. + +```json +{ + "mention": "<@123456789012345678>", + "botToken": "bot-token-value", + "dmUserId": "123456789012345678" +} +``` + +Keep the bot token secret and rotate it if it is ever exposed. Leave `botToken` or `dmUserId` out if you do not want the DM path. + +### Config Reference for Kilo + +When running under Kilo (the Kilo CLI 1.0 from Kilo-Org/kilo), configuration is managed through `/connect` (provider setup), `kilo auth` (credential helpers), and the merged files in `~/.config/kilo/` (`config.json`, `opencode.json`, and `opencode.jsonc`). Kilo merges those files, so place provider, model, permission, and MCP settings into `opencode.json` or `opencode.jsonc` there and restart the CLI after editing. The snippet above can either live within that file or in `~/.config/opencode/discord-notification-config.json` if your schema does not allow custom keys. + ## Development 1. Clone the repo. diff --git a/src/index.ts b/src/index.ts index 40e11b9..c9b68fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,9 @@ interface DiscordWebhookConfig { enabled?: boolean; username?: string; avatarUrl?: string; + mention?: string; + botToken?: string; + dmUserId?: string; } export const DiscordNotificationPlugin: Plugin = async ({ client, project }) => { @@ -127,25 +130,70 @@ async function handleNotification(client: any, project: any, event: any, type: " description = "OpenCode has paused execution and is waiting for you to authorize the operation shown above."; } + const embed = { + title, + description: description.length > 1500 ? description.substring(0, 1497) + "..." : description, + color, + fields, + footer: { text: `Session ID: ${sessionId}` }, + timestamp: new Date().toISOString() + }; + + const webhookPayload: Record = { + username: config.username || "OpenCode Notifier", + avatar_url: config.avatarUrl, + embeds: [embed] + }; + + const mentionText = config.mention?.trim(); + if (mentionText) { + webhookPayload.content = mentionText; + } + await fetch(config.webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username: config.username || "OpenCode Notifier", - avatar_url: config.avatarUrl, - embeds: [{ - title, - description: description.length > 1500 ? description.substring(0, 1497) + "..." : description, - color, - fields, - footer: { text: `Session ID: ${sessionId}` }, - timestamp: new Date().toISOString() - }] - }), + body: JSON.stringify(webhookPayload), }); + + if (config.botToken && config.dmUserId) { + await sendDiscordDm(config.botToken, config.dmUserId, embed, mentionText); + } } catch (e) { console.error("Discord Plugin Error:", e); } } +async function sendDiscordDm(botToken: string, userId: string, embed: Record, mentionText?: string) { + try { + const channelRes = await fetch("https://discord.com/api/v10/users/@me/channels", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${botToken}` + }, + body: JSON.stringify({ recipient_id: userId }) + }); + if (!channelRes.ok) return; + const channel = (await channelRes.json()) as { id?: string }; + if (!channel?.id) return; + + const dmPayload: Record = { embeds: [embed] }; + if (mentionText) { + dmPayload.content = mentionText; + } + + await fetch(`https://discord.com/api/v10/channels/${channel.id}/messages`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${botToken}` + }, + body: JSON.stringify(dmPayload) + }); + } catch (error) { + console.error("Discord Plugin DM Error:", error); + } +} + export default DiscordNotificationPlugin; From 2aa6fb9f1c3c98bd7d25692e54b5c799e8582d11 Mon Sep 17 00:00:00 2001 From: Heinrich-XIAO <74563446+Heinrich-XIAO@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:14:06 -0500 Subject: [PATCH 3/6] refactor: unify userId for ping and dm --- README.md | 9 ++++----- src/index.ts | 17 +++++++---------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f1f6a20..3a936c8 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,16 @@ Create a configuration file at `~/.config/opencode/discord-notification-config.j } ``` -You can optionally include a `mention` entry (for example `"<@123456789012345678>"`) to ping a Discord user or role whenever a notification fires. To additionally send the notification as a DM, provide `botToken` (your Discord bot token with message permissions) and `dmUserId` (the recipient’s Discord user ID); the plugin will open or reuse a DM channel before posting the embed. +Provide `userId` to tell the plugin who to ping—the same ID is used for message mentions and, when paired with `botToken`, to send a DM. `botToken` should be a Discord bot token with the `messages.write` scope. Example: ```json { - "mention": "<@123456789012345678>", - "botToken": "bot-token-value", - "dmUserId": "123456789012345678" + "userId": "123456789012345678", + "botToken": "bot-token-value" } ``` -Keep the bot token secret and rotate it if it is ever exposed. Leave `botToken` or `dmUserId` out if you do not want the DM path. +The plugin automatically wraps `userId` in `<@...>` for the webhook content and DM payload. Keep the bot token secret and rotate it if it is ever exposed; omit `botToken` if you only need webhook notifications. ### Config Reference for Kilo diff --git a/src/index.ts b/src/index.ts index c9b68fc..29d96d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,9 +5,8 @@ interface DiscordWebhookConfig { enabled?: boolean; username?: string; avatarUrl?: string; - mention?: string; + userId?: string; botToken?: string; - dmUserId?: string; } export const DiscordNotificationPlugin: Plugin = async ({ client, project }) => { @@ -145,7 +144,7 @@ async function handleNotification(client: any, project: any, event: any, type: " embeds: [embed] }; - const mentionText = config.mention?.trim(); + const mentionText = config.userId ? `<@${config.userId.trim()}>` : undefined; if (mentionText) { webhookPayload.content = mentionText; } @@ -156,15 +155,15 @@ async function handleNotification(client: any, project: any, event: any, type: " body: JSON.stringify(webhookPayload), }); - if (config.botToken && config.dmUserId) { - await sendDiscordDm(config.botToken, config.dmUserId, embed, mentionText); + if (config.botToken && config.userId) { + await sendDiscordDm(config.botToken, config.userId, embed); } } catch (e) { console.error("Discord Plugin Error:", e); } } -async function sendDiscordDm(botToken: string, userId: string, embed: Record, mentionText?: string) { +async function sendDiscordDm(botToken: string, userId: string, embed: Record) { try { const channelRes = await fetch("https://discord.com/api/v10/users/@me/channels", { method: "POST", @@ -178,10 +177,8 @@ async function sendDiscordDm(botToken: string, userId: string, embed: Record = { embeds: [embed] }; - if (mentionText) { - dmPayload.content = mentionText; - } + const mentionText = `<@${userId}>`; + const dmPayload: Record = { embeds: [embed], content: mentionText }; await fetch(`https://discord.com/api/v10/channels/${channel.id}/messages`, { method: "POST", From 7bd3962c209c266e5b7490cd084039fbd8bc5fea Mon Sep 17 00:00:00 2001 From: Heinrich-XIAO <74563446+Heinrich-XIAO@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:20:07 -0500 Subject: [PATCH 4/6] docs: remove Kilo reference --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 3a936c8..7023836 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,6 @@ Provide `userId` to tell the plugin who to ping—the same ID is used for messag The plugin automatically wraps `userId` in `<@...>` for the webhook content and DM payload. Keep the bot token secret and rotate it if it is ever exposed; omit `botToken` if you only need webhook notifications. -### Config Reference for Kilo - -When running under Kilo (the Kilo CLI 1.0 from Kilo-Org/kilo), configuration is managed through `/connect` (provider setup), `kilo auth` (credential helpers), and the merged files in `~/.config/kilo/` (`config.json`, `opencode.json`, and `opencode.jsonc`). Kilo merges those files, so place provider, model, permission, and MCP settings into `opencode.json` or `opencode.jsonc` there and restart the CLI after editing. The snippet above can either live within that file or in `~/.config/opencode/discord-notification-config.json` if your schema does not allow custom keys. - ## Development 1. Clone the repo. From 4cf33220eddf0595c98d4d9cf03bf73519ed5aa5 Mon Sep 17 00:00:00 2001 From: Heinrich-XIAO <74563446+Heinrich-XIAO@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:22:00 -0500 Subject: [PATCH 5/6] revert: drop README updates --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 7023836..2a33bf1 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,6 @@ Create a configuration file at `~/.config/opencode/discord-notification-config.j } ``` -Provide `userId` to tell the plugin who to ping—the same ID is used for message mentions and, when paired with `botToken`, to send a DM. `botToken` should be a Discord bot token with the `messages.write` scope. Example: - -```json -{ - "userId": "123456789012345678", - "botToken": "bot-token-value" -} -``` - -The plugin automatically wraps `userId` in `<@...>` for the webhook content and DM payload. Keep the bot token secret and rotate it if it is ever exposed; omit `botToken` if you only need webhook notifications. - ## Development 1. Clone the repo. From 80d8f9a34b0c2a2aae47c1df1c97e7f9c812e15c Mon Sep 17 00:00:00 2001 From: Heinrich-XIAO <74563446+Heinrich-XIAO@users.noreply.github.com> Date: Sun, 22 Feb 2026 20:24:29 -0500 Subject: [PATCH 6/6] fix: ensure bot mentions and server posts --- README.md | 12 ++++++++++++ src/index.ts | 53 ++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2a33bf1..776c156 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,18 @@ Create a configuration file at `~/.config/opencode/discord-notification-config.j } ``` +Provide `userId` to tell the plugin who to ping—the same ID is used for message mentions, optional bot DMs, and optional bot posts to server channels. `botToken` should be a Discord bot token with the `messages.write` scope. Example: + +```json +{ + "userId": "123456789012345678", + "botToken": "bot-token-value", + "serverChannelId": "987654321098765432" +} +``` + +The plugin automatically wraps `userId` in `<@...>` for mentions and uses the same sanitized ID when calling the bot APIs. Include `serverChannelId` to have the bot post the embed into a specific guild channel (alongside the webhook and optional DM). Keep `botToken` secret and rotate it if it is ever exposed; omit `botToken` if you only want webhook notifications. + ## Development 1. Clone the repo. diff --git a/src/index.ts b/src/index.ts index 29d96d0..0e3dd85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ interface DiscordWebhookConfig { avatarUrl?: string; userId?: string; botToken?: string; + serverChannelId?: string; } export const DiscordNotificationPlugin: Plugin = async ({ client, project }) => { @@ -144,9 +145,11 @@ async function handleNotification(client: any, project: any, event: any, type: " embeds: [embed] }; - const mentionText = config.userId ? `<@${config.userId.trim()}>` : undefined; + const cleanUserId = config.userId ? sanitizeUserId(config.userId) : ""; + const mentionText = cleanUserId ? `<@${cleanUserId}>` : undefined; if (mentionText) { webhookPayload.content = mentionText; + webhookPayload.allowed_mentions = { users: [cleanUserId] }; } await fetch(config.webhookUrl, { @@ -155,15 +158,19 @@ async function handleNotification(client: any, project: any, event: any, type: " body: JSON.stringify(webhookPayload), }); - if (config.botToken && config.userId) { - await sendDiscordDm(config.botToken, config.userId, embed); + if (config.botToken && cleanUserId) { + await sendDiscordDm(config.botToken, cleanUserId, embed, mentionText); + } + + if (config.botToken && config.serverChannelId) { + await sendDiscordServerMessage(config.botToken, config.serverChannelId, embed, mentionText, cleanUserId); } } catch (e) { console.error("Discord Plugin Error:", e); } } -async function sendDiscordDm(botToken: string, userId: string, embed: Record) { +async function sendDiscordDm(botToken: string, userId: string, embed: Record, mentionOverride?: string) { try { const channelRes = await fetch("https://discord.com/api/v10/users/@me/channels", { method: "POST", @@ -177,8 +184,12 @@ async function sendDiscordDm(botToken: string, userId: string, embed: Record`; - const dmPayload: Record = { embeds: [embed], content: mentionText }; + const mentionText = mentionOverride ?? (userId ? `<@${sanitizeUserId(userId)}>` : ""); + const dmPayload: Record = { + embeds: [embed], + content: mentionText, + allowed_mentions: { users: [userId] } + }; await fetch(`https://discord.com/api/v10/channels/${channel.id}/messages`, { method: "POST", @@ -193,4 +204,34 @@ async function sendDiscordDm(botToken: string, userId: string, embed: Record, mentionText?: string, mentionUserId?: string) { + try { + const cleanChannel = channelId.trim(); + if (!cleanChannel) return; + + const payload: Record = { + embeds: [embed] + }; + if (mentionText && mentionUserId) { + payload.content = mentionText; + payload.allowed_mentions = { users: [mentionUserId] }; + } + + await fetch(`https://discord.com/api/v10/channels/${cleanChannel}/messages`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bot ${botToken}` + }, + body: JSON.stringify(payload) + }); + } catch (error) { + console.error("Discord Plugin Server DM Error:", error); + } +} + +function sanitizeUserId(value: string): string { + return (value || "").trim().replace(/[^0-9]/g, ""); +} + export default DiscordNotificationPlugin;