@sillybit/opencode-plugin-everynotify 0.1.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sillybit
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.md ADDED
@@ -0,0 +1,147 @@
1
+ # EveryNotify — Multi-Service Notifications for opencode
2
+
3
+ [![npm version](https://img.shields.io/npm/v/opencode-plugin-everynotify.svg)](https://www.npmjs.com/package/opencode-plugin-everynotify)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ EveryNotify is a lightweight, zero-dependency notification plugin for [opencode](https://github.com/opencode-ai/plugin) designed to keep you informed about your AI-driven development sessions. It dispatches notifications to multiple services simultaneously, ensuring you stay updated on task progress, errors, and interaction requests, even when you're not actively monitoring your terminal.
7
+
8
+ ## Overview
9
+
10
+ In long-running development tasks or deep research sessions, it's common to switch focus while opencode works in the background. EveryNotify bridges the gap between your terminal and your preferred notification device, allowing you to react quickly when opencode requires input or has finished its work.
11
+
12
+ ### Why EveryNotify?
13
+
14
+ - **Multitasking Efficiency**: Walk away from your desk while opencode processes complex requests.
15
+ - **Immediate Awareness**: Get notified the instant an error occurs or a permission is requested.
16
+ - **Centralized Logs**: Use Slack, Discord, or Telegram as a secondary history of your opencode sessions.
17
+ - **Universal Reach**: Works across desktop and mobile through native service apps.
18
+
19
+ ## Features
20
+
21
+ - ✅ **4 Notification Services**: Native support for Pushover, Telegram, Slack, and Discord.
22
+ - ✅ **Automatic Event Detection**: Notifies on session completion, idle states, errors, and questions.
23
+ - ✅ **Rich Session Meta**: Notifications include the project name, session ID, and total elapsed time.
24
+ - ✅ **Intelligent Debouncing**: Prevents notification storms by aggregating repeated events within a 1-second window.
25
+ - ✅ **Fault Tolerance**: Isolated service calls ensure that a failure in one provider doesn't block others.
26
+ - ✅ **Zero Runtime Dependencies**: Built entirely on standard Node.js APIs and native `fetch()`.
27
+ - ✅ **Privacy & Control**: Completely opt-in; no notifications are sent until you enable and configure a service.
28
+
29
+ ## Installation
30
+
31
+ Install EveryNotify into your opencode environment using npm:
32
+
33
+ ```bash
34
+ npm install opencode-plugin-everynotify
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ EveryNotify utilizes a simple JSON configuration file named `.everynotify.json`. The plugin aggregates configuration from two potential scopes:
40
+
41
+ 1. **Global Configuration**: `~/.config/opencode/.everynotify.json`
42
+ _Use this for your default tokens and webhook URLs across all projects._
43
+ 2. **Project Configuration**: `.opencode/.everynotify.json` (inside your project directory)
44
+ _Use this to override settings or redirect notifications for a specific repository._
45
+
46
+ ### Example Configuration
47
+
48
+ Create your `.everynotify.json` with the tokens for the services you want to use. You can enable multiple services at once.
49
+
50
+ ```json
51
+ {
52
+ "pushover": {
53
+ "enabled": true,
54
+ "token": "KzG789...your_app_token",
55
+ "userKey": "uQi678...your_user_key",
56
+ "priority": 0
57
+ },
58
+ "telegram": {
59
+ "enabled": true,
60
+ "botToken": "123456:ABC-DEF...your_bot_token",
61
+ "chatId": "987654321"
62
+ },
63
+ "slack": {
64
+ "enabled": false,
65
+ "webhookUrl": "https://hooks.slack.com/services/T000/B000/XXXX"
66
+ },
67
+ "discord": {
68
+ "enabled": false,
69
+ "webhookUrl": "https://discord.com/api/webhooks/0000/XXXX"
70
+ }
71
+ }
72
+ ```
73
+
74
+ ## Usage
75
+
76
+ EveryNotify is a "fire-and-forget" plugin. Once installed and configured, it requires no manual intervention. opencode will automatically detect and load the plugin, which then runs silently in the background.
77
+
78
+ When an event is triggered, EveryNotify builds a descriptive message (e.g., `[complete] my-project (elapsed: 12m 30s)`) and dispatches it to all services marked as `"enabled": true`.
79
+
80
+ ## Supported Services
81
+
82
+ | Service | Requirements | Recommended For | Setup Link |
83
+ | ------------ | ------------------- | ------------------------------- | ------------------------------------------------------------------------- |
84
+ | **Pushover** | App Token, User Key | High-priority mobile alerts | [Pushover API](https://pushover.net/api) |
85
+ | **Telegram** | Bot Token, Chat ID | Instant messaging & groups | [Telegram Bots](https://core.telegram.org/bots) |
86
+ | **Slack** | Webhook URL | Team collaboration & logging | [Slack Webhooks](https://api.slack.com/messaging/webhooks) |
87
+ | **Discord** | Webhook URL | Community & server-based alerts | [Discord Webhooks](https://discord.com/developers/docs/resources/webhook) |
88
+
89
+ ## Event Types
90
+
91
+ The plugin monitors several key lifecycle hooks within opencode:
92
+
93
+ - **`complete`**: Dispatched when a task is finished and the session becomes idle. Also triggers when a subagent completes its assigned task.
94
+ - **`error`**: Dispatched if the session crashes or encounters a fatal execution error. Includes the error message in the notification.
95
+ - **`permission`**: Dispatched when opencode pauses to ask for tool execution permissions or file access.
96
+ - **`question`**: Dispatched when opencode uses the `question` tool to seek clarification from the user.
97
+
98
+ ## Advanced Features
99
+
100
+ ### Priority Management (Pushover)
101
+
102
+ For Pushover users, you can customize the `priority` level:
103
+
104
+ - `-2`: Lowest priority (no notification)
105
+ - `-1`: Quiet notification
106
+ - `0`: Normal priority (default)
107
+ - `1`: High priority (bypasses quiet hours)
108
+ - `2`: Emergency priority (requires acknowledgment)
109
+
110
+ ### Session Enrichment
111
+
112
+ EveryNotify automatically calculates the time elapsed since the first user message in a session. This duration is included in the notification text to give you context on how long the task took to complete.
113
+
114
+ ### Project Overrides
115
+
116
+ If you are working on a sensitive or client-specific project, you can place a `.everynotify.json` file in the project's `.opencode/` directory to send notifications to a specific Slack channel or Telegram group, bypassing your global configuration.
117
+
118
+ ## Development
119
+
120
+ Developers looking to extend EveryNotify or add new service providers should consult [AGENTS.md](./AGENTS.md).
121
+
122
+ ### Build & Test Workflow
123
+
124
+ The project uses [Bun](https://bun.sh) for lightning-fast development:
125
+
126
+ - **Install Dependencies**: `bun install`
127
+ - **Build Plugin**: `bun run build`
128
+ - **Run Tests**: `bun test`
129
+ - **Type Check**: `bun run typecheck`
130
+
131
+ ## Contributing
132
+
133
+ We welcome contributions of all kinds!
134
+
135
+ 1. **Bug Reports**: Open an issue describing the bug and your environment.
136
+ 2. **Feature Requests**: Propose new services or event hooks via issues.
137
+ 3. **Pull Requests**: Follow the existing code style (2-space indent, strict TypeScript) and ensure all tests pass.
138
+
139
+ ## License
140
+
141
+ Distributed under the **MIT License**. See `LICENSE` for more information.
142
+
143
+ ## Credits
144
+
145
+ Created by **[Sillybit](https://sillybit.io)** — Pixel Perfect Innovation.
146
+
147
+ Built with ❤️ for the opencode community.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * EveryNotify Plugin — Configuration Loader
3
+ *
4
+ * Loads configuration from:
5
+ * 1. Global: ~/.config/opencode/.everynotify.json
6
+ * 2. Project: .opencode/.everynotify.json (in project directory)
7
+ *
8
+ * Merge order: defaults ← global ← project (project wins)
9
+ * All services disabled by default.
10
+ */
11
+ import type { EverynotifyConfig } from "./types";
12
+ /**
13
+ * Default configuration — all services disabled, empty credentials
14
+ */
15
+ export declare const DEFAULT_CONFIG: EverynotifyConfig;
16
+ /**
17
+ * Get config file path for a given scope
18
+ * @param scope "global" or "project"
19
+ * @param directory project directory (used for project scope)
20
+ * @returns full path to config file
21
+ */
22
+ export declare function getConfigPath(scope: "global" | "project", directory: string): string;
23
+ /**
24
+ * Load configuration from global and project scopes
25
+ * Merge order: defaults ← global ← project (project wins)
26
+ * @param directory project directory
27
+ * @returns merged EverynotifyConfig
28
+ */
29
+ export declare function loadConfig(directory: string): EverynotifyConfig;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * EveryNotify Plugin — Dispatcher
3
+ *
4
+ * Dispatches notifications to all enabled services in parallel with:
5
+ * - Debouncing: 1000ms per event type (prevents duplicate notifications)
6
+ * - Timeout: 5s per service call (via AbortController)
7
+ * - Error isolation: One service failing doesn't block others (Promise.allSettled)
8
+ */
9
+ import type { EverynotifyConfig, NotificationPayload } from "./types";
10
+ /**
11
+ * Truncate text to max length, appending "… [truncated]" if over limit
12
+ * Shared utility function used by all services
13
+ */
14
+ export declare function truncate(text: string, maxLength: number): string;
15
+ /**
16
+ * Dispatcher interface returned by createDispatcher
17
+ */
18
+ interface Dispatcher {
19
+ dispatch: (payload: NotificationPayload) => Promise<void>;
20
+ }
21
+ /**
22
+ * Create a dispatcher that sends notifications to all enabled services
23
+ *
24
+ * @param config - EverynotifyConfig with enabled services
25
+ * @returns Dispatcher with dispatch() function
26
+ */
27
+ export declare function createDispatcher(config: EverynotifyConfig): Dispatcher;
28
+ export {};
@@ -0,0 +1,23 @@
1
+ /**
2
+ * EveryNotify Plugin — Entry Point
3
+ *
4
+ * Implements the opencode plugin interface with three hooks:
5
+ * - event: Handles session.idle, session.error, permission.updated events
6
+ * - permission.ask: Handles permission requests (debounce handles overlap with event hook)
7
+ * - tool.execute.before: Detects question tool usage
8
+ *
9
+ * Session enrichment:
10
+ * - Calls client.session.get() to check parentID (subagent detection)
11
+ * - Calls client.session.messages() to calculate elapsed time
12
+ * - All SDK calls wrapped in try/catch with fallback to null
13
+ */
14
+ import type { Plugin } from "@opencode-ai/plugin";
15
+ /**
16
+ * EveryNotify Plugin
17
+ *
18
+ * @param input - PluginInput from opencode SDK
19
+ * @returns Hooks object with event, permission.ask, and tool.execute.before hooks
20
+ */
21
+ declare const EverynotifyPlugin: Plugin;
22
+ export default EverynotifyPlugin;
23
+ export { EverynotifyPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,397 @@
1
+ // src/index.ts
2
+ import * as path2 from "path";
3
+
4
+ // src/config.ts
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import * as os from "os";
8
+ var DEFAULT_CONFIG = {
9
+ pushover: {
10
+ enabled: false,
11
+ token: "",
12
+ userKey: "",
13
+ priority: 0
14
+ },
15
+ telegram: {
16
+ enabled: false,
17
+ botToken: "",
18
+ chatId: ""
19
+ },
20
+ slack: {
21
+ enabled: false,
22
+ webhookUrl: ""
23
+ },
24
+ discord: {
25
+ enabled: false,
26
+ webhookUrl: ""
27
+ }
28
+ };
29
+ function getConfigPath(scope, directory) {
30
+ if (scope === "global") {
31
+ return path.join(os.homedir(), ".config", "opencode", ".everynotify.json");
32
+ }
33
+ return path.join(directory, ".opencode", ".everynotify.json");
34
+ }
35
+ function deepMerge(target, source) {
36
+ const result = { ...target };
37
+ if (source.pushover) {
38
+ result.pushover = { ...result.pushover, ...source.pushover };
39
+ }
40
+ if (source.telegram) {
41
+ result.telegram = { ...result.telegram, ...source.telegram };
42
+ }
43
+ if (source.slack) {
44
+ result.slack = { ...result.slack, ...source.slack };
45
+ }
46
+ if (source.discord) {
47
+ result.discord = { ...result.discord, ...source.discord };
48
+ }
49
+ return result;
50
+ }
51
+ function loadConfigFile(filePath) {
52
+ try {
53
+ if (!fs.existsSync(filePath)) {
54
+ return null;
55
+ }
56
+ const content = fs.readFileSync(filePath, "utf-8");
57
+ const parsed = JSON.parse(content);
58
+ return parsed;
59
+ } catch (error) {
60
+ const errorMsg = error instanceof Error ? error.message : String(error);
61
+ console.error(`[EveryNotify] Failed to load config from ${filePath}: ${errorMsg}`);
62
+ return null;
63
+ }
64
+ }
65
+ function loadConfig(directory) {
66
+ let config = { ...DEFAULT_CONFIG };
67
+ const globalPath = getConfigPath("global", directory);
68
+ const globalConfig = loadConfigFile(globalPath);
69
+ if (globalConfig) {
70
+ config = deepMerge(config, globalConfig);
71
+ }
72
+ const projectPath = getConfigPath("project", directory);
73
+ const projectConfig = loadConfigFile(projectPath);
74
+ if (projectConfig) {
75
+ config = deepMerge(config, projectConfig);
76
+ }
77
+ const allDisabled = !config.pushover.enabled && !config.telegram.enabled && !config.slack.enabled && !config.discord.enabled;
78
+ if (allDisabled) {
79
+ console.error("[EveryNotify] No services configured. Enable services in .everynotify.json");
80
+ }
81
+ return config;
82
+ }
83
+
84
+ // src/services/pushover.ts
85
+ function truncate(text, maxLength) {
86
+ if (text.length <= maxLength) {
87
+ return text;
88
+ }
89
+ const suffix = "… [truncated]";
90
+ return text.slice(0, maxLength - suffix.length) + suffix;
91
+ }
92
+ async function send(config, payload, signal) {
93
+ try {
94
+ const body = new URLSearchParams({
95
+ token: config.token,
96
+ user: config.userKey,
97
+ message: truncate(payload.message, 1024),
98
+ title: truncate(payload.title, 250),
99
+ priority: String(config.priority ?? 0)
100
+ });
101
+ const response = await fetch("https://api.pushover.net/1/messages.json", {
102
+ method: "POST",
103
+ headers: {
104
+ "Content-Type": "application/x-www-form-urlencoded"
105
+ },
106
+ body: body.toString(),
107
+ signal
108
+ });
109
+ if (!response.ok) {
110
+ const errorText = await response.text();
111
+ console.error(`[EveryNotify] Pushover error: ${response.status} ${errorText}`);
112
+ return;
113
+ }
114
+ } catch (error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ console.error(`[EveryNotify] Pushover failed: ${message}`);
117
+ }
118
+ }
119
+
120
+ // src/services/telegram.ts
121
+ function truncate2(text, maxLength) {
122
+ if (text.length <= maxLength) {
123
+ return text;
124
+ }
125
+ const suffix = "… [truncated]";
126
+ return text.slice(0, maxLength - suffix.length) + suffix;
127
+ }
128
+ function formatMessage(payload) {
129
+ const truncatedTitle = truncate2(payload.title, 250);
130
+ const truncatedMessage = truncate2(payload.message, 3840);
131
+ return `<b>${truncatedTitle}</b>
132
+ ${truncatedMessage}`;
133
+ }
134
+ async function send2(config, payload, signal) {
135
+ try {
136
+ const text = formatMessage(payload);
137
+ const body = JSON.stringify({
138
+ chat_id: config.chatId,
139
+ text,
140
+ parse_mode: "HTML"
141
+ });
142
+ const response = await fetch(`https://api.telegram.org/bot${config.botToken}/sendMessage`, {
143
+ method: "POST",
144
+ headers: {
145
+ "Content-Type": "application/json"
146
+ },
147
+ body,
148
+ signal
149
+ });
150
+ if (!response.ok) {
151
+ const errorText = await response.text();
152
+ console.error(`[EveryNotify] Telegram error: ${response.status} ${errorText}`);
153
+ return;
154
+ }
155
+ } catch (error) {
156
+ const message = error instanceof Error ? error.message : String(error);
157
+ console.error(`[EveryNotify] Telegram failed: ${message}`);
158
+ }
159
+ }
160
+
161
+ // src/services/slack.ts
162
+ function truncate3(text, maxLength) {
163
+ if (text.length <= maxLength) {
164
+ return text;
165
+ }
166
+ const suffix = "… [truncated]";
167
+ return text.slice(0, maxLength - suffix.length) + suffix;
168
+ }
169
+ function formatSlackMessage(payload) {
170
+ return `*${payload.title}*
171
+ ${payload.message}`;
172
+ }
173
+ async function send3(config, payload, signal) {
174
+ try {
175
+ const text = truncate3(formatSlackMessage(payload), 40000);
176
+ const response = await fetch(config.webhookUrl, {
177
+ method: "POST",
178
+ headers: {
179
+ "Content-Type": "application/json"
180
+ },
181
+ body: JSON.stringify({ text }),
182
+ signal
183
+ });
184
+ if (!response.ok) {
185
+ const errorText = await response.text();
186
+ console.error(`[EveryNotify] Slack error: ${response.status} ${errorText}`);
187
+ return;
188
+ }
189
+ } catch (error) {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ console.error(`[EveryNotify] Slack failed: ${message}`);
192
+ }
193
+ }
194
+
195
+ // src/services/discord.ts
196
+ function truncate4(text, maxLength) {
197
+ if (text.length <= maxLength) {
198
+ return text;
199
+ }
200
+ const suffix = "… [truncated]";
201
+ return text.slice(0, maxLength - suffix.length) + suffix;
202
+ }
203
+ function formatDiscordMessage(payload) {
204
+ return `**${payload.title}**
205
+ ${payload.message}`;
206
+ }
207
+ async function send4(config, payload, signal) {
208
+ try {
209
+ const content = truncate4(formatDiscordMessage(payload), 2000);
210
+ const response = await fetch(config.webhookUrl, {
211
+ method: "POST",
212
+ headers: {
213
+ "Content-Type": "application/json"
214
+ },
215
+ body: JSON.stringify({ content }),
216
+ signal
217
+ });
218
+ if (response.status === 429) {
219
+ const retryAfter = response.headers.get("Retry-After");
220
+ console.error(`[EveryNotify] Discord rate limited. Retry-After: ${retryAfter}s`);
221
+ return;
222
+ }
223
+ if (!response.ok) {
224
+ const errorText = await response.text();
225
+ console.error(`[EveryNotify] Discord error: ${response.status} ${errorText}`);
226
+ return;
227
+ }
228
+ } catch (error) {
229
+ const message = error instanceof Error ? error.message : String(error);
230
+ console.error(`[EveryNotify] Discord failed: ${message}`);
231
+ }
232
+ }
233
+
234
+ // src/dispatcher.ts
235
+ function createDispatcher(config) {
236
+ const services = [];
237
+ if (config.pushover.enabled) {
238
+ services.push({
239
+ name: "Pushover",
240
+ send,
241
+ config: config.pushover
242
+ });
243
+ }
244
+ if (config.telegram.enabled) {
245
+ services.push({
246
+ name: "Telegram",
247
+ send: send2,
248
+ config: config.telegram
249
+ });
250
+ }
251
+ if (config.slack.enabled) {
252
+ services.push({
253
+ name: "Slack",
254
+ send: send3,
255
+ config: config.slack
256
+ });
257
+ }
258
+ if (config.discord.enabled) {
259
+ services.push({
260
+ name: "Discord",
261
+ send: send4,
262
+ config: config.discord
263
+ });
264
+ }
265
+ const lastDispatchTime = new Map;
266
+ async function dispatch(payload) {
267
+ const now = Date.now();
268
+ const lastTime = lastDispatchTime.get(payload.eventType) ?? 0;
269
+ if (now - lastTime < 1000) {
270
+ return;
271
+ }
272
+ lastDispatchTime.set(payload.eventType, now);
273
+ if (services.length === 0) {
274
+ return;
275
+ }
276
+ const promises = services.map(async (service) => {
277
+ const controller = new AbortController;
278
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
279
+ try {
280
+ await service.send(service.config, payload, controller.signal);
281
+ } finally {
282
+ clearTimeout(timeoutId);
283
+ }
284
+ });
285
+ const results = await Promise.allSettled(promises);
286
+ results.forEach((result, index) => {
287
+ if (result.status === "rejected") {
288
+ console.error(`[EveryNotify] ${services[index].name} failed:`, result.reason);
289
+ }
290
+ });
291
+ }
292
+ return { dispatch };
293
+ }
294
+
295
+ // src/index.ts
296
+ var EverynotifyPlugin = async (input) => {
297
+ const { client, directory } = input;
298
+ const config = loadConfig(directory);
299
+ const { dispatch } = createDispatcher(config);
300
+ async function buildPayload(eventType, sessionID, extraMessage) {
301
+ const projectName = directory ? path2.basename(directory) : null;
302
+ let elapsedSeconds = null;
303
+ let isSubagent = false;
304
+ try {
305
+ if (sessionID) {
306
+ const sessionResult = await client.session.get({
307
+ path: { id: sessionID }
308
+ });
309
+ if (sessionResult.data?.parentID) {
310
+ isSubagent = true;
311
+ }
312
+ const messagesResult = await client.session.messages({
313
+ path: { id: sessionID }
314
+ });
315
+ const messages = messagesResult.data;
316
+ if (messages && messages.length > 0) {
317
+ const firstUserMessage = messages.find((msg) => msg.info?.role === "user");
318
+ if (firstUserMessage?.info?.time?.created) {
319
+ const startTime = new Date(firstUserMessage.info.time.created).getTime();
320
+ const now = Date.now();
321
+ elapsedSeconds = Math.floor((now - startTime) / 1000);
322
+ }
323
+ }
324
+ }
325
+ } catch (error) {}
326
+ let finalEventType = eventType;
327
+ if (eventType === "complete" && isSubagent) {
328
+ finalEventType = "subagent_complete";
329
+ }
330
+ const title = `[${finalEventType}] ${projectName || "opencode"}`;
331
+ let message = extraMessage || "Event occurred";
332
+ if (elapsedSeconds !== null) {
333
+ const minutes = Math.floor(elapsedSeconds / 60);
334
+ const seconds = elapsedSeconds % 60;
335
+ message += ` (elapsed: ${minutes}m ${seconds}s)`;
336
+ }
337
+ return {
338
+ eventType: finalEventType,
339
+ title,
340
+ message,
341
+ projectName,
342
+ timestamp: Date.now(),
343
+ sessionID,
344
+ elapsedSeconds
345
+ };
346
+ }
347
+ async function eventHook({ event }) {
348
+ try {
349
+ const sessionID = event.properties?.sessionID || null;
350
+ if (event.type === "session.idle") {
351
+ const payload = await buildPayload("complete", sessionID);
352
+ await dispatch(payload);
353
+ } else if (event.type === "session.error") {
354
+ const rawError = event.properties?.error;
355
+ const errorMessage = rawError?.data?.message ?? rawError?.name ?? "Unknown error";
356
+ const payload = await buildPayload("error", sessionID, errorMessage);
357
+ await dispatch(payload);
358
+ } else if (event.type === "permission.updated") {
359
+ const payload = await buildPayload("permission", sessionID);
360
+ await dispatch(payload);
361
+ }
362
+ } catch (error) {
363
+ const errorMsg = error instanceof Error ? error.message : String(error);
364
+ console.error(`[EveryNotify] Event hook error: ${errorMsg}`);
365
+ }
366
+ }
367
+ async function permissionAskHook(_input, _output) {
368
+ try {
369
+ const payload = await buildPayload("permission", null);
370
+ await dispatch(payload);
371
+ } catch (error) {
372
+ const errorMsg = error instanceof Error ? error.message : String(error);
373
+ console.error(`[EveryNotify] Permission.ask hook error: ${errorMsg}`);
374
+ }
375
+ }
376
+ async function toolExecuteBeforeHook(input2, _output) {
377
+ try {
378
+ if (input2.tool === "question") {
379
+ const payload = await buildPayload("question", null);
380
+ await dispatch(payload);
381
+ }
382
+ } catch (error) {
383
+ const errorMsg = error instanceof Error ? error.message : String(error);
384
+ console.error(`[EveryNotify] Tool.execute.before hook error: ${errorMsg}`);
385
+ }
386
+ }
387
+ return {
388
+ event: eventHook,
389
+ "permission.ask": permissionAskHook,
390
+ "tool.execute.before": toolExecuteBeforeHook
391
+ };
392
+ };
393
+ var src_default = EverynotifyPlugin;
394
+ export {
395
+ src_default as default,
396
+ EverynotifyPlugin
397
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Discord notification service
3
+ * API: https://discord.com/developers/docs/resources/webhook#execute-webhook
4
+ *
5
+ * CRITICAL: Uses application/json
6
+ * Message limit: 2000 chars (Discord webhook content field limit)
7
+ * Rate limit: 10 requests per 10 seconds per webhook
8
+ */
9
+ import type { DiscordConfig, NotificationPayload } from "../types";
10
+ /**
11
+ * Send notification via Discord webhook API
12
+ *
13
+ * @param config - Discord configuration (webhookUrl)
14
+ * @param payload - Notification payload (title, message, etc.)
15
+ * @param signal - AbortSignal for timeout control
16
+ */
17
+ export declare function send(config: DiscordConfig, payload: NotificationPayload, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Pushover notification service
3
+ * API: https://pushover.net/api
4
+ *
5
+ * CRITICAL: Uses application/x-www-form-urlencoded (NOT JSON)
6
+ * Message limit: 1024 chars
7
+ * Title limit: 250 chars
8
+ */
9
+ import type { PushoverConfig, NotificationPayload } from "../types";
10
+ /**
11
+ * Send notification via Pushover API
12
+ *
13
+ * @param config - Pushover configuration (token, userKey, priority)
14
+ * @param payload - Notification payload (title, message, etc.)
15
+ * @param signal - AbortSignal for timeout control
16
+ */
17
+ export declare function send(config: PushoverConfig, payload: NotificationPayload, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Slack notification service
3
+ * API: https://api.slack.com/messaging/webhooks
4
+ *
5
+ * Uses Incoming Webhooks with JSON POST
6
+ * Message limit: 40000 chars (Slack text field limit)
7
+ * Formatting: mrkdwn (Slack markdown)
8
+ */
9
+ import type { SlackConfig, NotificationPayload } from "../types";
10
+ /**
11
+ * Send notification via Slack Incoming Webhook
12
+ *
13
+ * @param config - Slack configuration (webhookUrl)
14
+ * @param payload - Notification payload (title, message, etc.)
15
+ * @param signal - AbortSignal for timeout control
16
+ */
17
+ export declare function send(config: SlackConfig, payload: NotificationPayload, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Telegram notification service
3
+ * API: https://core.telegram.org/bots/api#sendmessage
4
+ *
5
+ * CRITICAL: Uses application/json (JSON body)
6
+ * Message limit: 4096 chars
7
+ * Supports HTML formatting via parse_mode: "HTML"
8
+ */
9
+ import type { TelegramConfig, NotificationPayload } from "../types";
10
+ /**
11
+ * Send notification via Telegram Bot API
12
+ *
13
+ * @param config - Telegram configuration (botToken, chatId)
14
+ * @param payload - Notification payload (title, message, etc.)
15
+ * @param signal - AbortSignal for timeout control
16
+ */
17
+ export declare function send(config: TelegramConfig, payload: NotificationPayload, signal: AbortSignal): Promise<void>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * EveryNotify Plugin — Shared Type Definitions
3
+ *
4
+ * All types for notification payloads, service configurations, and dispatch functions.
5
+ * No validation logic here — types only.
6
+ */
7
+ /**
8
+ * Event types that trigger notifications
9
+ */
10
+ export type EventType = "complete" | "subagent_complete" | "error" | "permission" | "question";
11
+ /**
12
+ * Notification payload sent to all enabled services
13
+ */
14
+ export interface NotificationPayload {
15
+ eventType: EventType;
16
+ title: string;
17
+ message: string;
18
+ projectName: string | null;
19
+ timestamp: number;
20
+ sessionID: string | null;
21
+ elapsedSeconds: number | null;
22
+ }
23
+ /**
24
+ * Pushover service configuration
25
+ * API: https://pushover.net/api
26
+ * - token: 30-character app token
27
+ * - userKey: 30-character user key
28
+ * - priority: optional priority level (-2 to 2)
29
+ */
30
+ export interface PushoverConfig {
31
+ enabled: boolean;
32
+ token: string;
33
+ userKey: string;
34
+ priority?: number;
35
+ }
36
+ /**
37
+ * Telegram service configuration
38
+ * API: https://core.telegram.org/bots/api#sendmessage
39
+ * - botToken: bot token from BotFather
40
+ * - chatId: target chat ID
41
+ */
42
+ export interface TelegramConfig {
43
+ enabled: boolean;
44
+ botToken: string;
45
+ chatId: string;
46
+ }
47
+ /**
48
+ * Slack service configuration
49
+ * API: https://api.slack.com/messaging/webhooks
50
+ * - webhookUrl: incoming webhook URL
51
+ */
52
+ export interface SlackConfig {
53
+ enabled: boolean;
54
+ webhookUrl: string;
55
+ }
56
+ /**
57
+ * Discord service configuration
58
+ * API: https://discord.com/developers/docs/resources/webhook#execute-webhook
59
+ * - webhookUrl: webhook URL
60
+ */
61
+ export interface DiscordConfig {
62
+ enabled: boolean;
63
+ webhookUrl: string;
64
+ }
65
+ /**
66
+ * Top-level configuration object containing all service configs
67
+ */
68
+ export interface EverynotifyConfig {
69
+ pushover: PushoverConfig;
70
+ telegram: TelegramConfig;
71
+ slack: SlackConfig;
72
+ discord: DiscordConfig;
73
+ }
74
+ /**
75
+ * Function signature for service send functions
76
+ * Each service implements this interface
77
+ */
78
+ export type ServiceSendFunction = (config: any, payload: NotificationPayload, signal: AbortSignal) => Promise<void>;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@sillybit/opencode-plugin-everynotify",
3
+ "version": "0.1.1",
4
+ "description": "Multi-service notification plugin for opencode — Pushover, Telegram, Slack, Discord",
5
+ "author": "Sillybit <https://sillybit.io>",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/sillybit-io/opencode-plugin-everynotify.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/sillybit-io/opencode-plugin-everynotify/issues"
12
+ },
13
+ "homepage": "https://github.com/sillybit-io/opencode-plugin-everynotify#readme",
14
+ "type": "module",
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "bun build src/index.ts --outdir dist --target node && tsc --emitDeclarationOnly",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "bun test",
24
+ "prepublishOnly": "bun run build"
25
+ },
26
+ "devDependencies": {
27
+ "@opencode-ai/plugin": "^1.1.53",
28
+ "@types/node": "^22.0.0",
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "peerDependencies": {
32
+ "@opencode-ai/plugin": ">=1.1.53"
33
+ },
34
+ "keywords": [
35
+ "opencode",
36
+ "plugin",
37
+ "notification",
38
+ "pushover",
39
+ "telegram",
40
+ "slack",
41
+ "discord"
42
+ ],
43
+ "license": "MIT",
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "provenance": true
47
+ }
48
+ }