bobs-opencode-discord-notifier 0.1.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.
- package/README.md +45 -0
- package/package.json +29 -0
- package/src/index.ts +141 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# opencode-discord-notification
|
|
2
|
+
|
|
3
|
+
OpenCode plugin that sends Discord notifications on session completion and permission requests.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- ✅ **Completion Notifications:** Get a Discord message when OpenCode finishes a long task.
|
|
10
|
+
- 📊 **Context Stats:** Includes context usage percentage and total tokens.
|
|
11
|
+
- 🤖 **Model Info:** Shows which model was used for the response.
|
|
12
|
+
- ⚠️ **Permission Alerts:** Real-time notifications when OpenCode is blocked waiting for terminal permissions, including the command it's trying to run.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Add it to your `opencode.json`:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"plugin": ["opencode-discord-notification@0.1.1"]
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
Create a configuration file at `~/.config/opencode/discord-notification-config.json`:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"webhookUrl": "https://discord.com/api/webhooks/...",
|
|
32
|
+
"username": "OpenCode Notifier",
|
|
33
|
+
"avatarUrl": "https://opencode.ai/logo.png"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
1. Clone the repo.
|
|
40
|
+
2. Install dependencies: `bun install`.
|
|
41
|
+
3. Type-check: `bun x tsc`.
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bobs-opencode-discord-notifier",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "OpenCode plugin that sends Discord notifications on session completion and permission requests.",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"module": "src/index.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"opencode",
|
|
14
|
+
"plugin",
|
|
15
|
+
"discord",
|
|
16
|
+
"notification",
|
|
17
|
+
"ai"
|
|
18
|
+
],
|
|
19
|
+
"author": "BobBurton9000",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@opencode-ai/plugin": "*"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@opencode-ai/plugin": "latest",
|
|
26
|
+
"@types/node": "latest",
|
|
27
|
+
"typescript": "latest"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
5
|
+
|
|
6
|
+
interface DiscordWebhookConfig {
|
|
7
|
+
webhookUrl?: string;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
username?: string;
|
|
10
|
+
avatarUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DiscordNotificationPlugin: Plugin = async ({ client, project }) => {
|
|
14
|
+
return {
|
|
15
|
+
event: async ({ event }) => {
|
|
16
|
+
// 1. Handle Session Completed (Green)
|
|
17
|
+
if (event.type === "session.idle") {
|
|
18
|
+
await handleNotification(client, project, event, "idle");
|
|
19
|
+
}
|
|
20
|
+
// 2. Handle Permission Request (Orange)
|
|
21
|
+
else if ((event.type as string) === "permission.asked") {
|
|
22
|
+
await handleNotification(client, project, event, "permission");
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
async function handleNotification(client: any, project: any, event: any, type: "idle" | "permission") {
|
|
29
|
+
try {
|
|
30
|
+
// 1. GET CONFIGURATION
|
|
31
|
+
// Priority 1: project.config (opencode.json)
|
|
32
|
+
// Priority 2: Local file (for development or if schema is strict)
|
|
33
|
+
let config: DiscordWebhookConfig = project.config?.discordNotifications || {};
|
|
34
|
+
|
|
35
|
+
if (!config.webhookUrl) {
|
|
36
|
+
try {
|
|
37
|
+
const configPath = join(homedir(), ".config", "opencode", "discord-notification-config.json");
|
|
38
|
+
if (existsSync(configPath)) {
|
|
39
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!config.enabled || !config.webhookUrl) return;
|
|
45
|
+
|
|
46
|
+
const sessionId = event.properties?.sessionID || event.properties?.id || event.properties?.sessionId;
|
|
47
|
+
if (!sessionId) return;
|
|
48
|
+
|
|
49
|
+
// Only wait on idle to give tokens time to settle
|
|
50
|
+
if (type === "idle") {
|
|
51
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const [sRes, mRes] = await Promise.all([
|
|
55
|
+
client.session.get({ path: { id: sessionId } }),
|
|
56
|
+
client.session.messages({ path: { id: sessionId } })
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
const session = (sRes as any).data || sRes;
|
|
60
|
+
const messages = (mRes as any).data || mRes || [];
|
|
61
|
+
|
|
62
|
+
let lastText = "Response completed.";
|
|
63
|
+
let contextUsage = "N/A";
|
|
64
|
+
let modelName = session?.model?.name || "Unknown";
|
|
65
|
+
let totalTokensAccumulated = 0;
|
|
66
|
+
let pendingCommand = "";
|
|
67
|
+
|
|
68
|
+
// Analyze messages
|
|
69
|
+
messages.forEach((m: any) => {
|
|
70
|
+
const isAssistant = (m.info?.role || m.role) === "assistant";
|
|
71
|
+
if (isAssistant) {
|
|
72
|
+
const t = m.info?.tokens || m.tokens;
|
|
73
|
+
if (t) {
|
|
74
|
+
const turnTotal = (t.input || 0) + (t.output || 0) + (t.cache?.read || 0);
|
|
75
|
+
totalTokensAccumulated = Math.max(totalTokensAccumulated, turnTotal);
|
|
76
|
+
}
|
|
77
|
+
if (type === "permission" && m.parts) {
|
|
78
|
+
const toolPart = m.parts.find((p: any) => p.type === "tool" && (p.state?.status === "pending" || p.state?.status === "running"));
|
|
79
|
+
if (toolPart) {
|
|
80
|
+
const input = toolPart.state?.input || {};
|
|
81
|
+
pendingCommand = input.command || input.filePath || JSON.stringify(input);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (totalTokensAccumulated > 0 && session?.model?.limit?.context) {
|
|
88
|
+
const percentage = ((totalTokensAccumulated / session.model.limit.context) * 100).toFixed(2);
|
|
89
|
+
contextUsage = `${percentage}%`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const assistantMessages = messages.filter((m: any) => (m.info?.role || m.role) === "assistant");
|
|
93
|
+
const lastAssistant = assistantMessages[assistantMessages.length - 1];
|
|
94
|
+
if (lastAssistant) {
|
|
95
|
+
const text = lastAssistant.parts?.filter((p: any) => p.type === "text").map((p: any) => p.text).join("\n");
|
|
96
|
+
if (text) lastText = text;
|
|
97
|
+
modelName = (lastAssistant.info?.modelID || lastAssistant.modelID) || modelName;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const isPermission = type === "permission";
|
|
101
|
+
const title = isPermission ? "⚠️ Permission Required" : "✅ Response Completed";
|
|
102
|
+
const color = isPermission ? 0xffa500 : 0x00ff00;
|
|
103
|
+
|
|
104
|
+
let description = lastText;
|
|
105
|
+
const fields = [
|
|
106
|
+
{ name: "📊 Context Usage", value: contextUsage, inline: true },
|
|
107
|
+
{ name: "🔢 Total Tokens", value: `${totalTokensAccumulated.toLocaleString()} tokens`, inline: true },
|
|
108
|
+
{ name: "🤖 Model", value: modelName, inline: true }
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
if (isPermission) {
|
|
112
|
+
fields.unshift({
|
|
113
|
+
name: "🔒 Blocked Command / Action",
|
|
114
|
+
value: `\`\`\`bash\n${pendingCommand || "Check terminal for details"}\n\`\`\``,
|
|
115
|
+
inline: false
|
|
116
|
+
});
|
|
117
|
+
description = "OpenCode has paused execution and is waiting for you to authorize the operation shown above.";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await fetch(config.webhookUrl, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
username: config.username || "OpenCode Notifier",
|
|
125
|
+
avatar_url: config.avatarUrl,
|
|
126
|
+
embeds: [{
|
|
127
|
+
title,
|
|
128
|
+
description: description.length > 1500 ? description.substring(0, 1497) + "..." : description,
|
|
129
|
+
color,
|
|
130
|
+
fields,
|
|
131
|
+
footer: { text: `Session ID: ${sessionId}` },
|
|
132
|
+
timestamp: new Date().toISOString()
|
|
133
|
+
}]
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.error("Discord Plugin Error:", e);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default DiscordNotificationPlugin;
|