opencode-discord-notify 0.1.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/LICENSE +21 -0
- package/README-JP.md +132 -0
- package/README.md +127 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +539 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 j4rviscmd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README-JP.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# opencode-discord-notify
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-discord-notify)
|
|
5
|
+
[](https://www.npmjs.com/package/opencode-discord-notify)
|
|
6
|
+
[](https://www.npmjs.com/package/opencode-discord-notify)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
[English](README.md) | 日本語
|
|
12
|
+
|
|
13
|
+
<!-- markdownlint-disable -->
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="assets/image/sample-forum-ch.png" width="700" alt="Discord Forum channel example" />
|
|
16
|
+
</p>
|
|
17
|
+
<!-- markdownlint-enable -->
|
|
18
|
+
|
|
19
|
+
OpenCode のイベントを Discord Webhook に通知するプラグインです。
|
|
20
|
+
Discord の Forum チャンネル webhook を前提に、セッション開始時(または最初の通知タイミング)にスレッド(投稿)を作成して、その後の更新を同スレッドに流します。
|
|
21
|
+
通常のテキストチャンネル webhook でも利用できます(その場合はスレッドが作れないため、チャンネルへ直投稿します)。
|
|
22
|
+
|
|
23
|
+
## 使い方
|
|
24
|
+
|
|
25
|
+
`opencode.json` / `opencode.jsonc` にプラグインを追加します。
|
|
26
|
+
|
|
27
|
+
```jsonc
|
|
28
|
+
{
|
|
29
|
+
"plugin": ["opencode-discord-notify@latest"],
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
バージョン固定したい場合:
|
|
34
|
+
|
|
35
|
+
```jsonc
|
|
36
|
+
{
|
|
37
|
+
"plugin": ["opencode-discord-notify@0.1.0"],
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## できること
|
|
42
|
+
|
|
43
|
+
- `session.created`: セッション開始 → 開始通知をキュー(スレッド作成/送信は後続イベントで条件が揃ったタイミングで実行されることがある)
|
|
44
|
+
- `permission.updated`: 権限要求 → 通知
|
|
45
|
+
- `session.idle`: セッション完了 → 通知
|
|
46
|
+
- `session.error`: エラー → 通知(`sessionID` が無いケースは通知しない)
|
|
47
|
+
- `todo.updated`: Todo 更新 → チェックリスト形式で通知(順序は受信順 / `cancelled` は除外)
|
|
48
|
+
- `message.updated`: メッセージ情報更新 → 通知しない(role 判定用に追跡。role 未確定で保留した `text` part を後から通知することがある)
|
|
49
|
+
- `message.part.updated`: メッセージ本文/ツール結果更新 → `text` は user は即時通知、assistant は確定時(`time.end`)のみ通知。`tool` は通知しない(`reasoning` は通知しない / 重複イベントは抑制)
|
|
50
|
+
|
|
51
|
+
## セットアップ
|
|
52
|
+
|
|
53
|
+
### 1) 依存のインストール
|
|
54
|
+
|
|
55
|
+
グローバルにインストールします。
|
|
56
|
+
|
|
57
|
+
- `npm i -g @opencode-ai/plugin`
|
|
58
|
+
|
|
59
|
+
### 2) プラグイン配置
|
|
60
|
+
|
|
61
|
+
プロジェクト直下に以下のファイルを置きます。
|
|
62
|
+
|
|
63
|
+
- `.opencode/plugin/discord-notification.ts`
|
|
64
|
+
|
|
65
|
+
(グローバルに使いたい場合は `~/.config/opencode/plugin/` 配下でもOKです)
|
|
66
|
+
|
|
67
|
+
> [!WARNING]
|
|
68
|
+
> グローバル(`~/.config/opencode/plugin/`)とプロジェクト(`.opencode/plugin/`)の両方に配置すると、プラグインが二重に読み込まれて通知が重複します。どちらか一方にしてください。
|
|
69
|
+
|
|
70
|
+
### 3) Discord 側の準備
|
|
71
|
+
|
|
72
|
+
- Discord の Forum チャンネルで Webhook を作成してください。
|
|
73
|
+
- テキストチャンネル webhook でも動きますが、スレッド作成(`thread_name`)は Forum 向けの挙動が前提です。
|
|
74
|
+
|
|
75
|
+
### 4) 環境変数
|
|
76
|
+
|
|
77
|
+
必須:
|
|
78
|
+
|
|
79
|
+
- `DISCORD_WEBHOOK_URL`: Discord webhook URL(未設定の場合は no-op)
|
|
80
|
+
|
|
81
|
+
任意:
|
|
82
|
+
|
|
83
|
+
- `DISCORD_WEBHOOK_USERNAME`: 投稿者名
|
|
84
|
+
- `DISCORD_WEBHOOK_AVATAR_URL`: アイコン URL
|
|
85
|
+
- `DISCORD_WEBHOOK_COMPLETE_MENTION`: `session.idle` / `session.error` の通知本文に付けるメンション(`@everyone` または `@here` のみ許容。Forum webhook の仕様上、ping は常に発生しない)
|
|
86
|
+
- `DISCORD_WEBHOOK_PERMISSION_MENTION`: `permission.updated` の通知本文に付けるメンション(`DISCORD_WEBHOOK_COMPLETE_MENTION` へのフォールバックなし。`@everyone` または `@here` のみ許容。Forum webhook の仕様上、ping は常に発生しない)
|
|
87
|
+
- `DISCORD_WEBHOOK_EXCLUDE_INPUT_CONTEXT`: `1` のとき input context(`<file>` から始まる user `text` part)を通知しない(デフォルト: `1` / `0` で無効化)
|
|
88
|
+
|
|
89
|
+
## 仕様メモ
|
|
90
|
+
|
|
91
|
+
- `DISCORD_WEBHOOK_URL` 未設定の場合は no-op(ログに警告のみ)です。
|
|
92
|
+
- Forum スレッド作成時は `?wait=true` を付け、レスポンスの `channel_id` を thread ID として利用します。
|
|
93
|
+
- スレッド名(`thread_name`)は以下の優先度です(最大100文字)。
|
|
94
|
+
1. 最初の user `text`
|
|
95
|
+
2. session title
|
|
96
|
+
3. `session <sessionID>`
|
|
97
|
+
4. `untitled`
|
|
98
|
+
- Forum スレッド作成に失敗した場合は、取りこぼし防止のためチャンネル直投稿にフォールバックします(テキストチャンネル webhook など)。
|
|
99
|
+
- `permission.updated` / `session.idle` は thread がまだ作られていない場合、いったん通知をキューし、スレッド作成に必要な情報(スレッド名など)が揃ったタイミングで送信されることがあります(取りこぼし防止)。
|
|
100
|
+
- `session.error` は upstream の payload で `sessionID` が optional のため、`sessionID` が無い場合は通知しません。
|
|
101
|
+
- `DISCORD_WEBHOOK_COMPLETE_MENTION=@everyone`(または `@here`)を設定すると、通知本文にその文字列を含めて投稿します。ただし Forum webhook の仕様上、ping は常に発生しません(文字列として表示されるだけ)。
|
|
102
|
+
- `DISCORD_WEBHOOK_PERMISSION_MENTION=@everyone`(または `@here`)を設定すると、`permission.updated` の通知本文にその文字列を含めて投稿します。ただし Forum webhook の仕様上、ping は常に発生しません(文字列として表示されるだけ)。
|
|
103
|
+
- `todo.updated` は、`todos` を受信した順のままチェックリスト形式で通知します(`in_progress` は `[▶]`、`completed` は `[✓]`、`cancelled` は除外)。長い/大量の todo は Discord embed の制約に合わせて省略されることがあります(空の場合は `(no todos)` / 省略時は `...and more` を付与)。
|
|
104
|
+
- `message.updated` は通知しません(role 判定用に追跡。role 未確定で保留した `text` part を後から通知することがあります)。
|
|
105
|
+
- `message.part.updated` は以下の方針です。
|
|
106
|
+
- `text`: user は即時通知。assistant は `part.time.end` がある確定時のみ通知(ストリーミング途中更新は通知しない)
|
|
107
|
+
- `tool`: 通知しない
|
|
108
|
+
- `reasoning`: 通知しない(内部思考が含まれる可能性があるため)
|
|
109
|
+
|
|
110
|
+
## 動作確認(手動)
|
|
111
|
+
|
|
112
|
+
1. OpenCode を起動してセッション開始 → 最初の通知タイミングで Forum にスレッドが増える
|
|
113
|
+
2. 権限要求が出るケースを作る → 同スレッドに通知(未作成なら後続イベントの通知タイミングでスレッド作成されることがある)
|
|
114
|
+
3. セッション完了 → `session.idle` が通知される(`DISCORD_WEBHOOK_COMPLETE_MENTION` 設定時は本文に `@everyone` / `@here` を含めて投稿するが Forum webhook では ping は発生しない)
|
|
115
|
+
4. エラー発生 → `session.error` が通知される(`sessionID` 無しは通知されない / `DISCORD_WEBHOOK_COMPLETE_MENTION` 設定時は本文に `@everyone` / `@here` を含めて投稿するが Forum webhook では ping は発生しない)
|
|
116
|
+
|
|
117
|
+
## 開発
|
|
118
|
+
|
|
119
|
+
- 依存のインストール: `npm i`
|
|
120
|
+
- フォーマット: `npx prettier . --write`
|
|
121
|
+
- プラグイン本体: `.opencode/plugin/discord-notification.ts`
|
|
122
|
+
|
|
123
|
+
## 今後の展望(予定)
|
|
124
|
+
|
|
125
|
+
- npm パッケージとして公開(インストール/更新を簡単にする)
|
|
126
|
+
- 複数 webhook / 複数チャンネル対応(用途別に振り分け)
|
|
127
|
+
- 通知内容のカスタマイズ(通知するイベント、本文テンプレ、メンション付与ポリシーなど)
|
|
128
|
+
- 設定ファイル(例: `opencode-discord-notify.config.json`)を読み取り、必要に応じて環境変数から値を解決する方式も検討
|
|
129
|
+
- Discord の制限対策を強化(レート制限時のリトライ、分割投稿、長文省略ルールの改善)
|
|
130
|
+
- CI 整備(lint/format の自動化、簡単なテスト追加)
|
|
131
|
+
|
|
132
|
+
**PR / Issue 大歓迎です。**
|
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# opencode-discord-notify
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-discord-notify)
|
|
5
|
+
[](https://www.npmjs.com/package/opencode-discord-notify)
|
|
6
|
+
[](https://www.npmjs.com/package/opencode-discord-notify)
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
English | [日本語](README-JP.md)
|
|
12
|
+
|
|
13
|
+
<!-- markdownlint-disable -->
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="assets/image/sample-forum-ch.png" width="700" alt="Discord Forum channel example" />
|
|
16
|
+
</p>
|
|
17
|
+
<!-- markdownlint-enable -->
|
|
18
|
+
|
|
19
|
+
A plugin that posts OpenCode events to a Discord webhook.
|
|
20
|
+
|
|
21
|
+
It is optimized for Discord Forum channel webhooks: it creates one thread per session (via `thread_name`) and posts subsequent updates to the same thread.
|
|
22
|
+
It also works with regular text channel webhooks (in that case, it falls back to posting directly to the channel because threads cannot be created).
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Add this plugin to your `opencode.json` / `opencode.jsonc`:
|
|
27
|
+
|
|
28
|
+
```jsonc
|
|
29
|
+
{
|
|
30
|
+
"plugin": ["opencode-discord-notify@latest"],
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If you want to pin a version:
|
|
35
|
+
|
|
36
|
+
```jsonc
|
|
37
|
+
{
|
|
38
|
+
"plugin": ["opencode-discord-notify@0.1.0"],
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## What it does
|
|
43
|
+
|
|
44
|
+
- `session.created`: session started → queues a start notification (thread creation / sending may happen later when required info is available)
|
|
45
|
+
- `permission.updated`: permission request → posts a notification
|
|
46
|
+
- `session.idle`: session finished → posts a notification
|
|
47
|
+
- `session.error`: error → posts a notification (skips if `sessionID` is not present)
|
|
48
|
+
- `todo.updated`: todo updates → posts a checklist (keeps received order; excludes `cancelled`)
|
|
49
|
+
- `message.updated`: does not notify (tracked for role inference; may emit previously-held text later)
|
|
50
|
+
- `message.part.updated`:
|
|
51
|
+
- `text`: user text is posted immediately; assistant text is posted only when finalized (`time.end`)
|
|
52
|
+
- `tool`: not posted
|
|
53
|
+
- `reasoning`: not posted
|
|
54
|
+
|
|
55
|
+
## Setup
|
|
56
|
+
|
|
57
|
+
### 1) Install dependencies
|
|
58
|
+
|
|
59
|
+
Install the OpenCode plugin runner globally.
|
|
60
|
+
|
|
61
|
+
- `npm i -g @opencode-ai/plugin`
|
|
62
|
+
|
|
63
|
+
### 2) Place the plugin file
|
|
64
|
+
|
|
65
|
+
Put the plugin file in your project:
|
|
66
|
+
|
|
67
|
+
- `.opencode/plugin/discord-notification.ts`
|
|
68
|
+
|
|
69
|
+
(If you want to use it globally, place it under `~/.config/opencode/plugin/` instead.)
|
|
70
|
+
|
|
71
|
+
> [!WARNING]
|
|
72
|
+
> If you place the plugin in both the global directory (`~/.config/opencode/plugin/`) and the project directory (`.opencode/plugin/`), it may be loaded twice and send duplicate notifications. Choose either global or project placement, not both.
|
|
73
|
+
|
|
74
|
+
### 3) Create a Discord webhook
|
|
75
|
+
|
|
76
|
+
- Recommended: create a webhook in a Discord Forum channel.
|
|
77
|
+
- A webhook in a regular text channel also works, but thread creation using `thread_name` is a Forum-oriented behavior.
|
|
78
|
+
|
|
79
|
+
### 4) Environment variables
|
|
80
|
+
|
|
81
|
+
Required:
|
|
82
|
+
|
|
83
|
+
- `DISCORD_WEBHOOK_URL`: Discord webhook URL (if not set, the plugin does nothing)
|
|
84
|
+
|
|
85
|
+
Optional:
|
|
86
|
+
|
|
87
|
+
- `DISCORD_WEBHOOK_USERNAME`: username for webhook posts
|
|
88
|
+
- `DISCORD_WEBHOOK_AVATAR_URL`: avatar URL for webhook posts
|
|
89
|
+
- `DISCORD_WEBHOOK_COMPLETE_MENTION`: mention to put in `session.idle` / `session.error` messages (only `@everyone` or `@here` supported; Forum webhooks may not actually ping due to Discord behavior)
|
|
90
|
+
- `DISCORD_WEBHOOK_PERMISSION_MENTION`: mention to put in `permission.updated` messages (no fallback to `DISCORD_WEBHOOK_COMPLETE_MENTION`; only `@everyone` or `@here` supported; Forum webhooks may not actually ping due to Discord behavior)
|
|
91
|
+
- `DISCORD_WEBHOOK_EXCLUDE_INPUT_CONTEXT`: when set to `1`, exclude "input context" (user text parts that start with `<file>`) from notifications (default: `1`; set to `0` to disable)
|
|
92
|
+
|
|
93
|
+
## Notes / behavior
|
|
94
|
+
|
|
95
|
+
- If `DISCORD_WEBHOOK_URL` is not set, it becomes a no-op (logs a warning only).
|
|
96
|
+
- For Forum thread creation, it appends `?wait=true` and uses `channel_id` in the response as the thread ID.
|
|
97
|
+
- `thread_name` priority order (max 100 chars):
|
|
98
|
+
1. first user `text`
|
|
99
|
+
2. session title
|
|
100
|
+
3. `session <sessionID>`
|
|
101
|
+
4. `untitled`
|
|
102
|
+
- If thread creation fails (e.g. on non-Forum webhooks), it falls back to posting directly to the channel.
|
|
103
|
+
- `permission.updated` / `session.idle` may be queued until the thread name becomes available.
|
|
104
|
+
- `session.error` is skipped when `sessionID` is missing in the upstream payload.
|
|
105
|
+
- `DISCORD_WEBHOOK_COMPLETE_MENTION=@everyone` (or `@here`) is included as message content, but Forum webhooks may not actually ping.
|
|
106
|
+
- `DISCORD_WEBHOOK_PERMISSION_MENTION=@everyone` (or `@here`) is included as message content for `permission.updated`, but Forum webhooks may not actually ping.
|
|
107
|
+
- `todo.updated` posts a checklist in the order received (`in_progress` = `[▶]`, `completed` = `[✓]`, `cancelled` excluded). Long lists may be truncated to fit embed constraints.
|
|
108
|
+
- `message.updated` is not posted (tracked for role inference; may post a previously-held text part later).
|
|
109
|
+
- `message.part.updated` policy:
|
|
110
|
+
- `text`: user is posted immediately; assistant is posted only when finalized (when `part.time.end` exists)
|
|
111
|
+
- `tool`: not posted
|
|
112
|
+
- `reasoning`: not posted (to avoid exposing internal thoughts)
|
|
113
|
+
|
|
114
|
+
## Manual test
|
|
115
|
+
|
|
116
|
+
1. Start OpenCode → a new thread appears in the Forum channel on the first notification timing
|
|
117
|
+
2. Trigger a permission request → a notification is posted to the same thread (if the thread isn't created yet, it may be created later)
|
|
118
|
+
3. Finish the session → `session.idle` is posted
|
|
119
|
+
4. Trigger an error → `session.error` is posted (skipped if no `sessionID`)
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
- Install deps: `npm i`
|
|
124
|
+
- Format: `npx prettier . --write`
|
|
125
|
+
- Plugin source: `.opencode/plugin/discord-notification.ts`
|
|
126
|
+
|
|
127
|
+
PRs and issues are welcome.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
var COLORS = {
|
|
3
|
+
info: 5793266,
|
|
4
|
+
success: 5763719,
|
|
5
|
+
warning: 16705372,
|
|
6
|
+
error: 15548997
|
|
7
|
+
};
|
|
8
|
+
function safeString(value) {
|
|
9
|
+
if (value === null || value === void 0) return "";
|
|
10
|
+
if (typeof value === "string") return value;
|
|
11
|
+
try {
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
} catch {
|
|
14
|
+
return String(value);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function toIsoTimestamp(ms) {
|
|
18
|
+
if (typeof ms !== "number") return void 0;
|
|
19
|
+
if (!Number.isFinite(ms)) return void 0;
|
|
20
|
+
return new Date(ms).toISOString();
|
|
21
|
+
}
|
|
22
|
+
function buildFields(fields, inline = false) {
|
|
23
|
+
const result = [];
|
|
24
|
+
for (const [name, rawValue] of fields) {
|
|
25
|
+
const value = safeString(rawValue);
|
|
26
|
+
if (!value) continue;
|
|
27
|
+
result.push({
|
|
28
|
+
name,
|
|
29
|
+
value: value.length > 1024 ? value.slice(0, 1021) + "..." : value,
|
|
30
|
+
inline
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return result.length ? result : void 0;
|
|
34
|
+
}
|
|
35
|
+
function getEnv(name) {
|
|
36
|
+
try {
|
|
37
|
+
return process.env[name];
|
|
38
|
+
} catch {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function withQuery(url, params) {
|
|
43
|
+
const u = new URL(url);
|
|
44
|
+
for (const [k, v] of Object.entries(params)) {
|
|
45
|
+
if (!v) continue;
|
|
46
|
+
u.searchParams.set(k, v);
|
|
47
|
+
}
|
|
48
|
+
return u.toString();
|
|
49
|
+
}
|
|
50
|
+
async function postDiscordWebhook(input) {
|
|
51
|
+
const { webhookUrl, threadId, wait, body } = input;
|
|
52
|
+
const url = withQuery(webhookUrl, {
|
|
53
|
+
thread_id: threadId,
|
|
54
|
+
wait: wait ? "true" : void 0
|
|
55
|
+
});
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: {
|
|
59
|
+
"content-type": "application/json"
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body)
|
|
62
|
+
});
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
const text = await response.text().catch(() => "");
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Discord webhook failed: ${response.status} ${response.statusText} ${text}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (!wait) return void 0;
|
|
70
|
+
const json = await response.json().catch(() => void 0);
|
|
71
|
+
if (!json || typeof json !== "object") return void 0;
|
|
72
|
+
const channelId = json.channel_id;
|
|
73
|
+
const messageId = json.id;
|
|
74
|
+
if (typeof channelId !== "string" || typeof messageId !== "string")
|
|
75
|
+
return void 0;
|
|
76
|
+
return {
|
|
77
|
+
id: messageId,
|
|
78
|
+
channel_id: channelId
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
var plugin = async () => {
|
|
82
|
+
const webhookUrl = getEnv("DISCORD_WEBHOOK_URL");
|
|
83
|
+
const username = getEnv("DISCORD_WEBHOOK_USERNAME");
|
|
84
|
+
const avatarUrl = getEnv("DISCORD_WEBHOOK_AVATAR_URL");
|
|
85
|
+
const completeMentionRaw = (getEnv("DISCORD_WEBHOOK_COMPLETE_MENTION") ?? "").trim();
|
|
86
|
+
const completeMention = completeMentionRaw || void 0;
|
|
87
|
+
const permissionMentionRaw = (getEnv("DISCORD_WEBHOOK_PERMISSION_MENTION") ?? "").trim();
|
|
88
|
+
const permissionMention = permissionMentionRaw || void 0;
|
|
89
|
+
const excludeInputContextRaw = (getEnv("DISCORD_WEBHOOK_EXCLUDE_INPUT_CONTEXT") ?? "1").trim();
|
|
90
|
+
const excludeInputContext = excludeInputContextRaw !== "0";
|
|
91
|
+
const sessionToThread = /* @__PURE__ */ new Map();
|
|
92
|
+
const threadCreateInFlight = /* @__PURE__ */ new Map();
|
|
93
|
+
const pendingPostsBySession = /* @__PURE__ */ new Map();
|
|
94
|
+
const firstUserTextBySession = /* @__PURE__ */ new Map();
|
|
95
|
+
const pendingTextPartsByMessageId = /* @__PURE__ */ new Map();
|
|
96
|
+
const sessionSerial = /* @__PURE__ */ new Map();
|
|
97
|
+
const lastSessionInfo = /* @__PURE__ */ new Map();
|
|
98
|
+
const lastPartSnapshotById = /* @__PURE__ */ new Map();
|
|
99
|
+
const messageRoleById = /* @__PURE__ */ new Map();
|
|
100
|
+
function normalizeThreadTitle(value) {
|
|
101
|
+
return safeString(value).replace(/\s+/g, " ").trim();
|
|
102
|
+
}
|
|
103
|
+
function isInputContextText(text) {
|
|
104
|
+
return text.trimStart().startsWith("<file>");
|
|
105
|
+
}
|
|
106
|
+
function buildThreadName(sessionID) {
|
|
107
|
+
const fromUser = normalizeThreadTitle(firstUserTextBySession.get(sessionID));
|
|
108
|
+
if (fromUser) return fromUser.slice(0, 100);
|
|
109
|
+
const fromSessionTitle = normalizeThreadTitle(
|
|
110
|
+
lastSessionInfo.get(sessionID)?.title
|
|
111
|
+
);
|
|
112
|
+
if (fromSessionTitle) return fromSessionTitle.slice(0, 100);
|
|
113
|
+
const fromSessionId = normalizeThreadTitle(
|
|
114
|
+
sessionID ? `session ${sessionID}` : ""
|
|
115
|
+
);
|
|
116
|
+
if (fromSessionId) return fromSessionId.slice(0, 100);
|
|
117
|
+
return "untitled";
|
|
118
|
+
}
|
|
119
|
+
async function sendToChannel(body) {
|
|
120
|
+
if (!webhookUrl) return;
|
|
121
|
+
await postDiscordWebhook({
|
|
122
|
+
webhookUrl,
|
|
123
|
+
body: {
|
|
124
|
+
...body,
|
|
125
|
+
username,
|
|
126
|
+
avatar_url: avatarUrl
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function enqueueToThread(sessionID, body) {
|
|
131
|
+
const queue = pendingPostsBySession.get(sessionID) ?? [];
|
|
132
|
+
queue.push(body);
|
|
133
|
+
pendingPostsBySession.set(sessionID, queue);
|
|
134
|
+
}
|
|
135
|
+
function enqueueSerial(sessionID, task) {
|
|
136
|
+
const prev = sessionSerial.get(sessionID) ?? Promise.resolve();
|
|
137
|
+
const next = prev.then(task, task);
|
|
138
|
+
sessionSerial.set(sessionID, next);
|
|
139
|
+
next.finally(() => {
|
|
140
|
+
if (sessionSerial.get(sessionID) === next) sessionSerial.delete(sessionID);
|
|
141
|
+
});
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
async function ensureThread(sessionID) {
|
|
145
|
+
if (!webhookUrl) return void 0;
|
|
146
|
+
const existingThreadId = sessionToThread.get(sessionID);
|
|
147
|
+
if (existingThreadId) return existingThreadId;
|
|
148
|
+
const inflight = threadCreateInFlight.get(sessionID);
|
|
149
|
+
if (inflight) return await inflight;
|
|
150
|
+
const create = (async () => {
|
|
151
|
+
const queue = pendingPostsBySession.get(sessionID) ?? [];
|
|
152
|
+
const first = queue.shift();
|
|
153
|
+
if (queue.length) pendingPostsBySession.set(sessionID, queue);
|
|
154
|
+
else pendingPostsBySession.delete(sessionID);
|
|
155
|
+
if (!first) return void 0;
|
|
156
|
+
const threadName = buildThreadName(sessionID);
|
|
157
|
+
try {
|
|
158
|
+
const res = await postDiscordWebhook({
|
|
159
|
+
webhookUrl,
|
|
160
|
+
wait: true,
|
|
161
|
+
body: {
|
|
162
|
+
...first,
|
|
163
|
+
thread_name: threadName,
|
|
164
|
+
username,
|
|
165
|
+
avatar_url: avatarUrl
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
if (res?.channel_id) {
|
|
169
|
+
sessionToThread.set(sessionID, res.channel_id);
|
|
170
|
+
return res.channel_id;
|
|
171
|
+
}
|
|
172
|
+
warn(`failed to capture thread_id for session ${sessionID}`);
|
|
173
|
+
return void 0;
|
|
174
|
+
} catch (e) {
|
|
175
|
+
await sendToChannel(first);
|
|
176
|
+
return void 0;
|
|
177
|
+
} finally {
|
|
178
|
+
threadCreateInFlight.delete(sessionID);
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
threadCreateInFlight.set(sessionID, create);
|
|
182
|
+
return await create;
|
|
183
|
+
}
|
|
184
|
+
async function flushPending(sessionID) {
|
|
185
|
+
return enqueueSerial(sessionID, async () => {
|
|
186
|
+
if (!webhookUrl) return;
|
|
187
|
+
const threadId = sessionToThread.get(sessionID) ?? await ensureThread(sessionID);
|
|
188
|
+
const queue = pendingPostsBySession.get(sessionID);
|
|
189
|
+
if (!queue?.length) return;
|
|
190
|
+
try {
|
|
191
|
+
if (threadId) {
|
|
192
|
+
for (const body of queue) {
|
|
193
|
+
await postDiscordWebhook({
|
|
194
|
+
webhookUrl,
|
|
195
|
+
threadId,
|
|
196
|
+
body: {
|
|
197
|
+
...body,
|
|
198
|
+
username,
|
|
199
|
+
avatar_url: avatarUrl
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
for (const body of queue) {
|
|
205
|
+
await sendToChannel(body);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
pendingPostsBySession.delete(sessionID);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function shouldFlush(sessionID) {
|
|
214
|
+
return sessionToThread.has(sessionID) || firstUserTextBySession.has(sessionID);
|
|
215
|
+
}
|
|
216
|
+
function warn(message, error) {
|
|
217
|
+
if (error) console.warn(`[opencode-discord-notify] ${message}`, error);
|
|
218
|
+
else console.warn(`[opencode-discord-notify] ${message}`);
|
|
219
|
+
}
|
|
220
|
+
function buildMention(mention, nameForLog) {
|
|
221
|
+
if (!mention) return void 0;
|
|
222
|
+
if (mention === "@everyone" || mention === "@here") {
|
|
223
|
+
return {
|
|
224
|
+
content: mention,
|
|
225
|
+
allowed_mentions: {
|
|
226
|
+
parse: ["everyone"]
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
warn(
|
|
231
|
+
`${nameForLog} is set but unsupported: ${mention}. Only @everyone/@here are supported.`
|
|
232
|
+
);
|
|
233
|
+
return {
|
|
234
|
+
content: mention,
|
|
235
|
+
allowed_mentions: {
|
|
236
|
+
parse: []
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function buildCompleteMention() {
|
|
241
|
+
return buildMention(completeMention, "DISCORD_WEBHOOK_COMPLETE_MENTION");
|
|
242
|
+
}
|
|
243
|
+
function buildPermissionMention() {
|
|
244
|
+
return buildMention(permissionMention, "DISCORD_WEBHOOK_PERMISSION_MENTION");
|
|
245
|
+
}
|
|
246
|
+
function normalizeTodoContent(value) {
|
|
247
|
+
return safeString(value).replace(/\s+/g, " ").trim();
|
|
248
|
+
}
|
|
249
|
+
function truncateText(value, maxLength) {
|
|
250
|
+
if (value.length <= maxLength) return value;
|
|
251
|
+
if (maxLength <= 3) return value.slice(0, maxLength);
|
|
252
|
+
return value.slice(0, maxLength - 3) + "...";
|
|
253
|
+
}
|
|
254
|
+
function setIfChanged(map, key, next) {
|
|
255
|
+
const prev = map.get(key);
|
|
256
|
+
if (prev === next) return false;
|
|
257
|
+
map.set(key, next);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
function buildTodoChecklist(todos) {
|
|
261
|
+
const maxDescription = 4096;
|
|
262
|
+
const items = Array.isArray(todos) ? todos : [];
|
|
263
|
+
let matchCount = 0;
|
|
264
|
+
let description = "";
|
|
265
|
+
let truncated = false;
|
|
266
|
+
for (const item of items) {
|
|
267
|
+
const status = item?.status;
|
|
268
|
+
if (status === "cancelled") continue;
|
|
269
|
+
const content = normalizeTodoContent(item?.content);
|
|
270
|
+
if (!content) continue;
|
|
271
|
+
const marker = status === "completed" ? "[\u2713]" : status === "in_progress" ? "[\u25B6]" : "[ ]";
|
|
272
|
+
const line = `> ${marker} ${truncateText(content, 200)}`;
|
|
273
|
+
const nextChunk = (description ? "\n" : "") + line;
|
|
274
|
+
if (description.length + nextChunk.length > maxDescription) {
|
|
275
|
+
truncated = true;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
description += nextChunk;
|
|
279
|
+
matchCount += 1;
|
|
280
|
+
}
|
|
281
|
+
if (!description) {
|
|
282
|
+
return "> (no todos)";
|
|
283
|
+
}
|
|
284
|
+
if (truncated || matchCount < items.length) {
|
|
285
|
+
const moreLine = `${description ? "\n" : ""}> ...and more`;
|
|
286
|
+
if (description.length + moreLine.length <= maxDescription) {
|
|
287
|
+
description += moreLine;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return description;
|
|
291
|
+
}
|
|
292
|
+
if (!webhookUrl) {
|
|
293
|
+
warn("DISCORD_WEBHOOK_URL is not set; plugin will be a no-op");
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
event: async ({ event }) => {
|
|
297
|
+
try {
|
|
298
|
+
switch (event.type) {
|
|
299
|
+
case "session.created": {
|
|
300
|
+
const info = event.properties?.info;
|
|
301
|
+
const sessionID = info?.id;
|
|
302
|
+
if (!sessionID) return;
|
|
303
|
+
const title = info?.title ?? "(untitled)";
|
|
304
|
+
const directory = info?.directory;
|
|
305
|
+
const projectID = info?.projectID;
|
|
306
|
+
const shareUrl = info?.share?.url;
|
|
307
|
+
const createdAt = toIsoTimestamp(info?.time?.created);
|
|
308
|
+
const embed = {
|
|
309
|
+
title: "Session started",
|
|
310
|
+
description: title,
|
|
311
|
+
url: shareUrl,
|
|
312
|
+
color: COLORS.info,
|
|
313
|
+
timestamp: createdAt,
|
|
314
|
+
fields: buildFields(
|
|
315
|
+
[
|
|
316
|
+
["sessionID", sessionID],
|
|
317
|
+
["projectID", projectID],
|
|
318
|
+
["directory", directory],
|
|
319
|
+
["share", shareUrl]
|
|
320
|
+
],
|
|
321
|
+
false
|
|
322
|
+
)
|
|
323
|
+
};
|
|
324
|
+
lastSessionInfo.set(sessionID, { title, shareUrl });
|
|
325
|
+
enqueueToThread(sessionID, { embeds: [embed] });
|
|
326
|
+
if (shouldFlush(sessionID)) await flushPending(sessionID);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
case "permission.updated": {
|
|
330
|
+
const p = event.properties;
|
|
331
|
+
const sessionID = p?.sessionID;
|
|
332
|
+
if (!sessionID) return;
|
|
333
|
+
const embed = {
|
|
334
|
+
title: "Permission required",
|
|
335
|
+
description: p?.title,
|
|
336
|
+
color: COLORS.warning,
|
|
337
|
+
timestamp: toIsoTimestamp(p?.time?.created),
|
|
338
|
+
fields: buildFields(
|
|
339
|
+
[
|
|
340
|
+
["sessionID", sessionID],
|
|
341
|
+
["permissionID", p?.id],
|
|
342
|
+
["type", p?.type],
|
|
343
|
+
["pattern", p?.pattern],
|
|
344
|
+
["messageID", p?.messageID],
|
|
345
|
+
["callID", p?.callID]
|
|
346
|
+
],
|
|
347
|
+
false
|
|
348
|
+
)
|
|
349
|
+
};
|
|
350
|
+
const mention = buildPermissionMention();
|
|
351
|
+
enqueueToThread(sessionID, {
|
|
352
|
+
content: mention ? `${mention.content} Permission required` : void 0,
|
|
353
|
+
allowed_mentions: mention?.allowed_mentions,
|
|
354
|
+
embeds: [embed]
|
|
355
|
+
});
|
|
356
|
+
if (shouldFlush(sessionID)) await flushPending(sessionID);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
case "session.idle": {
|
|
360
|
+
const sessionID = event.properties?.sessionID;
|
|
361
|
+
if (!sessionID) return;
|
|
362
|
+
const embed = {
|
|
363
|
+
title: "Session completed",
|
|
364
|
+
color: COLORS.success,
|
|
365
|
+
fields: buildFields([["sessionID", sessionID]], false)
|
|
366
|
+
};
|
|
367
|
+
const mention = buildCompleteMention();
|
|
368
|
+
enqueueToThread(sessionID, {
|
|
369
|
+
content: mention ? `${mention.content} Session completed` : void 0,
|
|
370
|
+
allowed_mentions: mention?.allowed_mentions,
|
|
371
|
+
embeds: [embed]
|
|
372
|
+
});
|
|
373
|
+
await flushPending(sessionID);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
case "session.error": {
|
|
377
|
+
const p = event.properties;
|
|
378
|
+
const sessionID = p?.sessionID;
|
|
379
|
+
const errorStr = safeString(p?.error);
|
|
380
|
+
const embed = {
|
|
381
|
+
title: "Session error",
|
|
382
|
+
color: COLORS.error,
|
|
383
|
+
description: errorStr ? errorStr.length > 4096 ? errorStr.slice(0, 4093) + "..." : errorStr : void 0,
|
|
384
|
+
fields: buildFields([["sessionID", sessionID]], false)
|
|
385
|
+
};
|
|
386
|
+
if (!sessionID) return;
|
|
387
|
+
const mention = buildCompleteMention();
|
|
388
|
+
enqueueToThread(sessionID, {
|
|
389
|
+
content: mention ? `${mention.content} Session error` : void 0,
|
|
390
|
+
allowed_mentions: mention?.allowed_mentions,
|
|
391
|
+
embeds: [embed]
|
|
392
|
+
});
|
|
393
|
+
await flushPending(sessionID);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
case "todo.updated": {
|
|
397
|
+
const p = event.properties;
|
|
398
|
+
const sessionID = p?.sessionID;
|
|
399
|
+
if (!sessionID) return;
|
|
400
|
+
const embed = {
|
|
401
|
+
title: "Todo updated",
|
|
402
|
+
color: COLORS.info,
|
|
403
|
+
fields: buildFields([["sessionID", sessionID]], false),
|
|
404
|
+
description: buildTodoChecklist(p?.todos)
|
|
405
|
+
};
|
|
406
|
+
enqueueToThread(sessionID, { embeds: [embed] });
|
|
407
|
+
if (shouldFlush(sessionID)) await flushPending(sessionID);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
case "message.updated": {
|
|
411
|
+
const info = event.properties?.info;
|
|
412
|
+
const messageID = info?.id;
|
|
413
|
+
const role = info?.role;
|
|
414
|
+
if (!messageID) return;
|
|
415
|
+
if (role === "user" || role === "assistant") {
|
|
416
|
+
messageRoleById.set(messageID, role);
|
|
417
|
+
const pendingParts = pendingTextPartsByMessageId.get(messageID);
|
|
418
|
+
if (pendingParts?.length) {
|
|
419
|
+
pendingTextPartsByMessageId.delete(messageID);
|
|
420
|
+
for (const pendingPart of pendingParts) {
|
|
421
|
+
const sessionID = pendingPart?.sessionID;
|
|
422
|
+
const partID = pendingPart?.id;
|
|
423
|
+
const type = pendingPart?.type;
|
|
424
|
+
if (!sessionID || !partID || type !== "text") continue;
|
|
425
|
+
const text = safeString(pendingPart?.text);
|
|
426
|
+
if (role === "user" && excludeInputContext && isInputContextText(text)) {
|
|
427
|
+
const snapshot2 = JSON.stringify({
|
|
428
|
+
type,
|
|
429
|
+
role,
|
|
430
|
+
skipped: "input_context"
|
|
431
|
+
});
|
|
432
|
+
setIfChanged(lastPartSnapshotById, partID, snapshot2);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const snapshot = JSON.stringify({ type, role, text });
|
|
436
|
+
if (!setIfChanged(lastPartSnapshotById, partID, snapshot))
|
|
437
|
+
continue;
|
|
438
|
+
if (role === "user" && !firstUserTextBySession.has(sessionID)) {
|
|
439
|
+
const normalized = normalizeThreadTitle(text);
|
|
440
|
+
if (normalized)
|
|
441
|
+
firstUserTextBySession.set(sessionID, normalized);
|
|
442
|
+
}
|
|
443
|
+
const embed = {
|
|
444
|
+
title: role === "user" ? "Message part updated: text (user)" : "Message part updated: text (assistant)",
|
|
445
|
+
color: COLORS.info,
|
|
446
|
+
fields: buildFields(
|
|
447
|
+
[
|
|
448
|
+
["sessionID", sessionID],
|
|
449
|
+
["messageID", messageID],
|
|
450
|
+
["partID", partID],
|
|
451
|
+
["role", role]
|
|
452
|
+
],
|
|
453
|
+
false
|
|
454
|
+
),
|
|
455
|
+
description: truncateText(text || "(empty)", 4096)
|
|
456
|
+
};
|
|
457
|
+
enqueueToThread(sessionID, { embeds: [embed] });
|
|
458
|
+
if (role === "user") await flushPending(sessionID);
|
|
459
|
+
else if (shouldFlush(sessionID)) await flushPending(sessionID);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
case "message.part.updated": {
|
|
466
|
+
const p = event.properties;
|
|
467
|
+
const part = p?.part;
|
|
468
|
+
const sessionID = part?.sessionID;
|
|
469
|
+
const messageID = part?.messageID;
|
|
470
|
+
const partID = part?.id;
|
|
471
|
+
const type = part?.type;
|
|
472
|
+
if (!sessionID || !messageID || !partID || !type) return;
|
|
473
|
+
if (type === "reasoning") return;
|
|
474
|
+
if (type === "text") {
|
|
475
|
+
const role = messageRoleById.get(messageID);
|
|
476
|
+
if (role !== "assistant" && role !== "user") {
|
|
477
|
+
const list = pendingTextPartsByMessageId.get(messageID) ?? [];
|
|
478
|
+
list.push(part);
|
|
479
|
+
pendingTextPartsByMessageId.set(messageID, list);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (role === "assistant" && !part?.time?.end) return;
|
|
483
|
+
const text = safeString(part?.text);
|
|
484
|
+
if (role === "user" && excludeInputContext && isInputContextText(text)) {
|
|
485
|
+
const snapshot2 = JSON.stringify({
|
|
486
|
+
type,
|
|
487
|
+
role,
|
|
488
|
+
skipped: "input_context"
|
|
489
|
+
});
|
|
490
|
+
setIfChanged(lastPartSnapshotById, partID, snapshot2);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const snapshot = JSON.stringify({ type, role, text });
|
|
494
|
+
if (!setIfChanged(lastPartSnapshotById, partID, snapshot)) return;
|
|
495
|
+
if (role === "user" && !firstUserTextBySession.has(sessionID)) {
|
|
496
|
+
const normalized = normalizeThreadTitle(text);
|
|
497
|
+
if (normalized)
|
|
498
|
+
firstUserTextBySession.set(sessionID, normalized);
|
|
499
|
+
}
|
|
500
|
+
const embed = {
|
|
501
|
+
title: role === "user" ? "Message part updated: text (user)" : "Message part updated: text (assistant)",
|
|
502
|
+
color: COLORS.info,
|
|
503
|
+
fields: buildFields(
|
|
504
|
+
[
|
|
505
|
+
["sessionID", sessionID],
|
|
506
|
+
["messageID", messageID],
|
|
507
|
+
["partID", partID],
|
|
508
|
+
["role", role]
|
|
509
|
+
],
|
|
510
|
+
false
|
|
511
|
+
),
|
|
512
|
+
description: truncateText(text || "(empty)", 4096)
|
|
513
|
+
};
|
|
514
|
+
enqueueToThread(sessionID, { embeds: [embed] });
|
|
515
|
+
if (role === "user") {
|
|
516
|
+
await flushPending(sessionID);
|
|
517
|
+
} else if (shouldFlush(sessionID)) {
|
|
518
|
+
await flushPending(sessionID);
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (type === "tool") return;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
default:
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
} catch (e) {
|
|
529
|
+
warn(`failed handling event ${event.type}`, e);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
var index_default = plugin;
|
|
535
|
+
var DiscordNotificationPlugin = plugin;
|
|
536
|
+
export {
|
|
537
|
+
DiscordNotificationPlugin,
|
|
538
|
+
index_default as default
|
|
539
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-discord-notify",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A plugin that posts OpenCode events to a Discord webhook.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"README-JP.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup src/index.ts --format esm --dts --out-dir dist --clean",
|
|
29
|
+
"format": "prettier . --write",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@opencode-ai/plugin": ">=1.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@opencode-ai/plugin": "1.0.194",
|
|
37
|
+
"@types/node": "^20.19.27",
|
|
38
|
+
"prettier": "^3.7.4",
|
|
39
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
40
|
+
"tsup": "^8.5.0",
|
|
41
|
+
"typescript": "^5.8.3"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"opencode",
|
|
45
|
+
"discord",
|
|
46
|
+
"webhook",
|
|
47
|
+
"plugin"
|
|
48
|
+
],
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "https://github.com/j4rviscmd/opencode-discord-notify"
|
|
52
|
+
}
|
|
53
|
+
}
|