@sillybit/opencode-plugin-everynotify 0.1.1 → 0.3.4
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 +165 -4
- package/dist/config.d.ts +6 -2
- package/dist/dispatcher.d.ts +5 -7
- package/dist/index.js +266 -143
- package/dist/logger.d.ts +35 -0
- package/dist/types.d.ts +39 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# EveryNotify — Multi-Service Notifications for opencode
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/opencode-plugin-everynotify)
|
|
3
|
+
[](https://www.npmjs.com/package/@sillybit/opencode-plugin-everynotify)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
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.
|
|
@@ -25,15 +25,55 @@ In long-running development tasks or deep research sessions, it's common to swit
|
|
|
25
25
|
- ✅ **Fault Tolerance**: Isolated service calls ensure that a failure in one provider doesn't block others.
|
|
26
26
|
- ✅ **Zero Runtime Dependencies**: Built entirely on standard Node.js APIs and native `fetch()`.
|
|
27
27
|
- ✅ **Privacy & Control**: Completely opt-in; no notifications are sent until you enable and configure a service.
|
|
28
|
+
- ✅ **Event Filtering**: Selectively enable or disable notifications for specific event types.
|
|
29
|
+
- ✅ **Rich Content**: Notifications include the actual assistant response text for better context.
|
|
30
|
+
- ✅ **Truncation Direction**: Choose whether to keep the beginning or end of long messages when they exceed service limits.
|
|
28
31
|
|
|
29
32
|
## Installation
|
|
30
33
|
|
|
31
34
|
Install EveryNotify into your opencode environment using npm:
|
|
32
35
|
|
|
33
36
|
```bash
|
|
34
|
-
npm install opencode-plugin-everynotify
|
|
37
|
+
npm install @sillybit/opencode-plugin-everynotify
|
|
35
38
|
```
|
|
36
39
|
|
|
40
|
+
## Using with opencode
|
|
41
|
+
|
|
42
|
+
opencode does not auto-load npm packages; you must **register the plugin** in your opencode config so it is loaded at startup.
|
|
43
|
+
|
|
44
|
+
### Option 1: Register via config (recommended)
|
|
45
|
+
|
|
46
|
+
Add EveryNotify to the `plugin` array in your opencode config. opencode will install and load it automatically (using Bun) when you run opencode.
|
|
47
|
+
|
|
48
|
+
**Global config** (all projects): edit `~/.config/opencode/opencode.json`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"plugin": ["@sillybit/opencode-plugin-everynotify"]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Project config** (single project): add or edit `opencode.json` in your project root:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"plugin": ["@sillybit/opencode-plugin-everynotify"]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
You can list multiple plugins in the same array, e.g. `["@sillybit/opencode-plugin-everynotify", "opencode-wakatime"]`.
|
|
65
|
+
|
|
66
|
+
### Option 2: Local plugin directory
|
|
67
|
+
|
|
68
|
+
Alternatively, place the plugin in opencode’s plugin directory so it is loaded as a local plugin:
|
|
69
|
+
|
|
70
|
+
- **Project-only**: `.opencode/plugins/` (e.g. symlink or copy from `node_modules/@sillybit/opencode-plugin-everynotify`)
|
|
71
|
+
- **All projects**: `~/.config/opencode/plugins/`
|
|
72
|
+
|
|
73
|
+
Files in these directories are loaded automatically; no `plugin` entry in opencode config is required for them.
|
|
74
|
+
|
|
75
|
+
After the plugin is loaded, configure your notification services (see [Configuration](#configuration)) and optionally add a `.everynotify.json` for per-project overrides.
|
|
76
|
+
|
|
37
77
|
## Configuration
|
|
38
78
|
|
|
39
79
|
EveryNotify utilizes a simple JSON configuration file named `.everynotify.json`. The plugin aggregates configuration from two potential scopes:
|
|
@@ -67,15 +107,132 @@ Create your `.everynotify.json` with the tokens for the services you want to use
|
|
|
67
107
|
"discord": {
|
|
68
108
|
"enabled": false,
|
|
69
109
|
"webhookUrl": "https://discord.com/api/webhooks/0000/XXXX"
|
|
110
|
+
},
|
|
111
|
+
"log": {
|
|
112
|
+
"enabled": true,
|
|
113
|
+
"level": "warn"
|
|
114
|
+
},
|
|
115
|
+
"events": {
|
|
116
|
+
"complete": true,
|
|
117
|
+
"subagent_complete": true,
|
|
118
|
+
"error": true,
|
|
119
|
+
"permission": false,
|
|
120
|
+
"question": true
|
|
121
|
+
},
|
|
122
|
+
"truncateFrom": "end"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuration Options
|
|
127
|
+
|
|
128
|
+
### Event Filtering
|
|
129
|
+
|
|
130
|
+
You can control which events trigger notifications by adding an `events` block to your `.everynotify.json`. All events are enabled (`true`) by default to ensure backward compatibility.
|
|
131
|
+
|
|
132
|
+
| Event | Description |
|
|
133
|
+
| ------------------- | -------------------------------------------------------------------------- |
|
|
134
|
+
| `complete` | The main opencode session has finished its task and is now idle. |
|
|
135
|
+
| `subagent_complete` | A subagent has completed its specific assigned task. |
|
|
136
|
+
| `error` | A fatal error or crash occurred during the session. |
|
|
137
|
+
| `permission` | opencode is waiting for you to grant permission for a tool or file access. |
|
|
138
|
+
| `question` | The `question` tool was used to ask you for clarification. |
|
|
139
|
+
|
|
140
|
+
### Message Truncation
|
|
141
|
+
|
|
142
|
+
Each notification service has a maximum message length (e.g., Pushover: 1024 chars, Discord: 2000 chars). When a message exceeds the limit, EveryNotify truncates it and adds `...` as an indicator.
|
|
143
|
+
|
|
144
|
+
The `truncateFrom` option controls which part of the message is kept:
|
|
145
|
+
|
|
146
|
+
| Value | Behavior | Best For |
|
|
147
|
+
| --------- | ---------------------------------- | --------------------------------------------- |
|
|
148
|
+
| `"end"` | Keep beginning, trim end (default) | When the start of the message has the context |
|
|
149
|
+
| `"start"` | Keep end, trim beginning | When the conclusion/result at the end matters |
|
|
150
|
+
|
|
151
|
+
**Global setting** applies to all services:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"truncateFrom": "start"
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Per-service override** — for example, keep message endings on Pushover but beginnings on Slack:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{
|
|
163
|
+
"truncateFrom": "end",
|
|
164
|
+
"pushover": {
|
|
165
|
+
"enabled": true,
|
|
166
|
+
"token": "...",
|
|
167
|
+
"userKey": "...",
|
|
168
|
+
"truncateFrom": "start"
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Service-level `truncateFrom` takes priority over the global setting.
|
|
174
|
+
|
|
175
|
+
### Rich Message Content
|
|
176
|
+
|
|
177
|
+
EveryNotify provides rich context in its notifications by extracting the assistant's last response. Instead of generic "Task completed" messages, you receive the actual summary or answer provided by opencode.
|
|
178
|
+
|
|
179
|
+
**Notification Priority:**
|
|
180
|
+
|
|
181
|
+
1. **Errors**: The specific error message always takes top priority.
|
|
182
|
+
2. **Assistant Text**: The actual text from the assistant's final response.
|
|
183
|
+
3. **Fallback**: If no text is found, it defaults to a generic "Task completed" message.
|
|
184
|
+
|
|
185
|
+
**Message Format Example:**
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
[complete] my-project
|
|
189
|
+
I've successfully implemented the authentication system with JWT tokens and refresh token rotation. The API endpoints are secured and tests are passing. (elapsed: 2m 18s)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### File-Based Logging
|
|
193
|
+
|
|
194
|
+
EveryNotify includes an optional file-based logging system to help you troubleshoot service delivery or configuration issues without cluttering your terminal.
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"log": {
|
|
199
|
+
"enabled": true,
|
|
200
|
+
"level": "warn"
|
|
70
201
|
}
|
|
71
202
|
}
|
|
72
203
|
```
|
|
73
204
|
|
|
205
|
+
**Options:**
|
|
206
|
+
|
|
207
|
+
- `enabled`: Set to `true` to activate file-based logging (default: `false`).
|
|
208
|
+
- `level`: The minimum severity to log.
|
|
209
|
+
- `"error"`: Only records failed service dispatches and critical system errors.
|
|
210
|
+
- `"warn"`: Records errors plus non-critical warnings like missing configuration files (default).
|
|
211
|
+
|
|
212
|
+
**Log File Details:**
|
|
213
|
+
|
|
214
|
+
- **Location**: `~/.config/opencode/.everynotify.log`
|
|
215
|
+
- **Format**: `[ISO-8601] [LEVEL] [EveryNotify] Message`
|
|
216
|
+
- **Rotation**: Logs are automatically rotated every 7 days (based on file modification time).
|
|
217
|
+
- **Cleanup**: EveryNotify maintains a maximum of 4 rotated files (e.g., `.everynotify.log.2026-02-01`), automatically deleting the oldest.
|
|
218
|
+
|
|
219
|
+
**Example Entry:**
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
[2026-02-08T14:30:45.123Z] [ERROR] [EveryNotify] Service dispatch failed: Pushover: Network timeout
|
|
223
|
+
[2026-02-08T14:30:45.456Z] [WARN] [EveryNotify] Config file not found at ~/.config/opencode/.everynotify.json
|
|
224
|
+
```
|
|
225
|
+
|
|
74
226
|
## Usage
|
|
75
227
|
|
|
76
|
-
EveryNotify is a "fire-and-forget" plugin. Once
|
|
228
|
+
EveryNotify is a "fire-and-forget" plugin. Once you have [registered it with opencode](#using-with-opencode) and [configured](#configuration) at least one service with `"enabled": true`, it runs automatically. opencode loads the plugin at startup; EveryNotify then listens for events and sends notifications in the background with no further action needed.
|
|
77
229
|
|
|
78
|
-
When an event is triggered, EveryNotify builds a
|
|
230
|
+
When an event is triggered, EveryNotify builds a rich message containing the event type, project name, the actual assistant response, and the total elapsed time.
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
`[complete] my-project: I've finished the refactor. (elapsed: 12m 30s)`
|
|
234
|
+
|
|
235
|
+
Notifications are dispatched to all services marked as `"enabled": true`.
|
|
79
236
|
|
|
80
237
|
## Supported Services
|
|
81
238
|
|
|
@@ -107,6 +264,10 @@ For Pushover users, you can customize the `priority` level:
|
|
|
107
264
|
- `1`: High priority (bypasses quiet hours)
|
|
108
265
|
- `2`: Emergency priority (requires acknowledgment)
|
|
109
266
|
|
|
267
|
+
### Message Truncation Direction
|
|
268
|
+
|
|
269
|
+
See [Message Truncation](#message-truncation) under Configuration Options for details on the `truncateFrom` setting. This is especially useful for services with tight message limits like Pushover (1024 chars), where the end of a message often contains the most important information (the result or conclusion).
|
|
270
|
+
|
|
110
271
|
### Session Enrichment
|
|
111
272
|
|
|
112
273
|
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.
|
package/dist/config.d.ts
CHANGED
|
@@ -20,10 +20,14 @@ export declare const DEFAULT_CONFIG: EverynotifyConfig;
|
|
|
20
20
|
* @returns full path to config file
|
|
21
21
|
*/
|
|
22
22
|
export declare function getConfigPath(scope: "global" | "project", directory: string): string;
|
|
23
|
+
export interface LoadConfigResult {
|
|
24
|
+
config: EverynotifyConfig;
|
|
25
|
+
warnings: string[];
|
|
26
|
+
}
|
|
23
27
|
/**
|
|
24
28
|
* Load configuration from global and project scopes
|
|
25
29
|
* Merge order: defaults ← global ← project (project wins)
|
|
26
30
|
* @param directory project directory
|
|
27
|
-
* @returns
|
|
31
|
+
* @returns config and any warnings encountered during loading
|
|
28
32
|
*/
|
|
29
|
-
export declare function loadConfig(directory: string):
|
|
33
|
+
export declare function loadConfig(directory: string): LoadConfigResult;
|
package/dist/dispatcher.d.ts
CHANGED
|
@@ -6,12 +6,9 @@
|
|
|
6
6
|
* - Timeout: 5s per service call (via AbortController)
|
|
7
7
|
* - Error isolation: One service failing doesn't block others (Promise.allSettled)
|
|
8
8
|
*/
|
|
9
|
-
import type { EverynotifyConfig, NotificationPayload } from "./types";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* Shared utility function used by all services
|
|
13
|
-
*/
|
|
14
|
-
export declare function truncate(text: string, maxLength: number): string;
|
|
9
|
+
import type { EverynotifyConfig, NotificationPayload, TruncationMode } from "./types";
|
|
10
|
+
import type { Logger } from "./logger";
|
|
11
|
+
export declare function truncate(text: string, maxLength: number, from?: TruncationMode): string;
|
|
15
12
|
/**
|
|
16
13
|
* Dispatcher interface returned by createDispatcher
|
|
17
14
|
*/
|
|
@@ -22,7 +19,8 @@ interface Dispatcher {
|
|
|
22
19
|
* Create a dispatcher that sends notifications to all enabled services
|
|
23
20
|
*
|
|
24
21
|
* @param config - EverynotifyConfig with enabled services
|
|
22
|
+
* @param logger - Logger instance for error logging
|
|
25
23
|
* @returns Dispatcher with dispatch() function
|
|
26
24
|
*/
|
|
27
|
-
export declare function createDispatcher(config: EverynotifyConfig): Dispatcher;
|
|
25
|
+
export declare function createDispatcher(config: EverynotifyConfig, logger: Logger): Dispatcher;
|
|
28
26
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import * as
|
|
2
|
+
import * as path3 from "path";
|
|
3
3
|
|
|
4
4
|
// src/config.ts
|
|
5
5
|
import * as fs from "fs";
|
|
@@ -24,7 +24,19 @@ var DEFAULT_CONFIG = {
|
|
|
24
24
|
discord: {
|
|
25
25
|
enabled: false,
|
|
26
26
|
webhookUrl: ""
|
|
27
|
-
}
|
|
27
|
+
},
|
|
28
|
+
log: {
|
|
29
|
+
enabled: false,
|
|
30
|
+
level: "warn"
|
|
31
|
+
},
|
|
32
|
+
events: {
|
|
33
|
+
complete: true,
|
|
34
|
+
subagent_complete: true,
|
|
35
|
+
error: true,
|
|
36
|
+
permission: true,
|
|
37
|
+
question: true
|
|
38
|
+
},
|
|
39
|
+
truncateFrom: "end"
|
|
28
40
|
};
|
|
29
41
|
function getConfigPath(scope, directory) {
|
|
30
42
|
if (scope === "global") {
|
|
@@ -46,9 +58,18 @@ function deepMerge(target, source) {
|
|
|
46
58
|
if (source.discord) {
|
|
47
59
|
result.discord = { ...result.discord, ...source.discord };
|
|
48
60
|
}
|
|
61
|
+
if (source.log) {
|
|
62
|
+
result.log = { ...result.log, ...source.log };
|
|
63
|
+
}
|
|
64
|
+
if (source.events) {
|
|
65
|
+
result.events = { ...result.events, ...source.events };
|
|
66
|
+
}
|
|
67
|
+
if (source.truncateFrom !== undefined) {
|
|
68
|
+
result.truncateFrom = source.truncateFrom;
|
|
69
|
+
}
|
|
49
70
|
return result;
|
|
50
71
|
}
|
|
51
|
-
function loadConfigFile(filePath) {
|
|
72
|
+
function loadConfigFile(filePath, warnings) {
|
|
52
73
|
try {
|
|
53
74
|
if (!fs.existsSync(filePath)) {
|
|
54
75
|
return null;
|
|
@@ -58,208 +79,182 @@ function loadConfigFile(filePath) {
|
|
|
58
79
|
return parsed;
|
|
59
80
|
} catch (error) {
|
|
60
81
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
61
|
-
|
|
82
|
+
warnings.push(`Failed to load config from ${filePath}: ${errorMsg}`);
|
|
62
83
|
return null;
|
|
63
84
|
}
|
|
64
85
|
}
|
|
65
86
|
function loadConfig(directory) {
|
|
87
|
+
const warnings = [];
|
|
66
88
|
let config = { ...DEFAULT_CONFIG };
|
|
67
89
|
const globalPath = getConfigPath("global", directory);
|
|
68
|
-
const globalConfig = loadConfigFile(globalPath);
|
|
90
|
+
const globalConfig = loadConfigFile(globalPath, warnings);
|
|
69
91
|
if (globalConfig) {
|
|
70
92
|
config = deepMerge(config, globalConfig);
|
|
71
93
|
}
|
|
72
94
|
const projectPath = getConfigPath("project", directory);
|
|
73
|
-
const projectConfig = loadConfigFile(projectPath);
|
|
95
|
+
const projectConfig = loadConfigFile(projectPath, warnings);
|
|
74
96
|
if (projectConfig) {
|
|
75
97
|
config = deepMerge(config, projectConfig);
|
|
76
98
|
}
|
|
77
99
|
const allDisabled = !config.pushover.enabled && !config.telegram.enabled && !config.slack.enabled && !config.discord.enabled;
|
|
78
100
|
if (allDisabled) {
|
|
79
|
-
|
|
101
|
+
warnings.push("No services configured. Enable services in .everynotify.json");
|
|
80
102
|
}
|
|
81
|
-
return config;
|
|
103
|
+
return { config, warnings };
|
|
82
104
|
}
|
|
83
105
|
|
|
84
106
|
// 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
107
|
async function send(config, payload, signal) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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}`);
|
|
108
|
+
const body = new URLSearchParams({
|
|
109
|
+
token: config.token,
|
|
110
|
+
user: config.userKey,
|
|
111
|
+
message: truncate(payload.message, 1024, config.truncateFrom),
|
|
112
|
+
title: truncate(payload.title, 250, config.truncateFrom),
|
|
113
|
+
priority: String(config.priority ?? 0)
|
|
114
|
+
});
|
|
115
|
+
const response = await fetch("https://api.pushover.net/1/messages.json", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
119
|
+
},
|
|
120
|
+
body: body.toString(),
|
|
121
|
+
signal
|
|
122
|
+
});
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errorText = await response.text();
|
|
125
|
+
throw new Error(`Pushover API error: ${response.status} ${errorText}`);
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
// src/services/telegram.ts
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
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);
|
|
130
|
+
function formatMessage(payload, truncateFrom) {
|
|
131
|
+
const truncatedTitle = truncate(payload.title, 250, truncateFrom);
|
|
132
|
+
const truncatedMessage = truncate(payload.message, 3840, truncateFrom);
|
|
131
133
|
return `<b>${truncatedTitle}</b>
|
|
132
134
|
${truncatedMessage}`;
|
|
133
135
|
}
|
|
134
136
|
async function send2(config, payload, signal) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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}`);
|
|
137
|
+
const text = formatMessage(payload, config.truncateFrom);
|
|
138
|
+
const body = JSON.stringify({
|
|
139
|
+
chat_id: config.chatId,
|
|
140
|
+
text,
|
|
141
|
+
parse_mode: "HTML"
|
|
142
|
+
});
|
|
143
|
+
const response = await fetch(`https://api.telegram.org/bot${config.botToken}/sendMessage`, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "application/json"
|
|
147
|
+
},
|
|
148
|
+
body,
|
|
149
|
+
signal
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
const errorText = await response.text();
|
|
153
|
+
throw new Error(`Telegram API error: ${response.status} ${errorText}`);
|
|
158
154
|
}
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
// 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
158
|
function formatSlackMessage(payload) {
|
|
170
159
|
return `*${payload.title}*
|
|
171
160
|
${payload.message}`;
|
|
172
161
|
}
|
|
173
162
|
async function send3(config, payload, signal) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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}`);
|
|
163
|
+
const text = truncate(formatSlackMessage(payload), 40000, config.truncateFrom);
|
|
164
|
+
const response = await fetch(config.webhookUrl, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "application/json"
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({ text }),
|
|
170
|
+
signal
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const errorText = await response.text();
|
|
174
|
+
throw new Error(`Slack API error: ${response.status} ${errorText}`);
|
|
192
175
|
}
|
|
193
176
|
}
|
|
194
177
|
|
|
195
178
|
// 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
179
|
function formatDiscordMessage(payload) {
|
|
204
180
|
return `**${payload.title}**
|
|
205
181
|
${payload.message}`;
|
|
206
182
|
}
|
|
207
183
|
async function send4(config, payload, signal) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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}`);
|
|
184
|
+
const content = truncate(formatDiscordMessage(payload), 2000, config.truncateFrom);
|
|
185
|
+
const response = await fetch(config.webhookUrl, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
"Content-Type": "application/json"
|
|
189
|
+
},
|
|
190
|
+
body: JSON.stringify({ content }),
|
|
191
|
+
signal
|
|
192
|
+
});
|
|
193
|
+
if (response.status === 429) {
|
|
194
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
195
|
+
throw new Error(`Discord rate limited. Retry-After: ${retryAfter}s`);
|
|
196
|
+
}
|
|
197
|
+
if (!response.ok) {
|
|
198
|
+
const errorText = await response.text();
|
|
199
|
+
throw new Error(`Discord API error: ${response.status} ${errorText}`);
|
|
231
200
|
}
|
|
232
201
|
}
|
|
233
202
|
|
|
234
203
|
// src/dispatcher.ts
|
|
235
|
-
function
|
|
204
|
+
function truncate(text, maxLength, from = "end") {
|
|
205
|
+
if (text.length <= maxLength) {
|
|
206
|
+
return text;
|
|
207
|
+
}
|
|
208
|
+
const indicator = "...";
|
|
209
|
+
if (maxLength <= indicator.length) {
|
|
210
|
+
return indicator;
|
|
211
|
+
}
|
|
212
|
+
if (from === "start") {
|
|
213
|
+
return indicator + text.slice(text.length - (maxLength - indicator.length));
|
|
214
|
+
}
|
|
215
|
+
return text.slice(0, maxLength - indicator.length) + indicator;
|
|
216
|
+
}
|
|
217
|
+
function createDispatcher(config, logger) {
|
|
236
218
|
const services = [];
|
|
219
|
+
const globalTruncateFrom = config.truncateFrom ?? "end";
|
|
237
220
|
if (config.pushover.enabled) {
|
|
238
221
|
services.push({
|
|
239
222
|
name: "Pushover",
|
|
240
223
|
send,
|
|
241
|
-
config:
|
|
224
|
+
config: {
|
|
225
|
+
...config.pushover,
|
|
226
|
+
truncateFrom: config.pushover.truncateFrom ?? globalTruncateFrom
|
|
227
|
+
}
|
|
242
228
|
});
|
|
243
229
|
}
|
|
244
230
|
if (config.telegram.enabled) {
|
|
245
231
|
services.push({
|
|
246
232
|
name: "Telegram",
|
|
247
233
|
send: send2,
|
|
248
|
-
config:
|
|
234
|
+
config: {
|
|
235
|
+
...config.telegram,
|
|
236
|
+
truncateFrom: config.telegram.truncateFrom ?? globalTruncateFrom
|
|
237
|
+
}
|
|
249
238
|
});
|
|
250
239
|
}
|
|
251
240
|
if (config.slack.enabled) {
|
|
252
241
|
services.push({
|
|
253
242
|
name: "Slack",
|
|
254
243
|
send: send3,
|
|
255
|
-
config:
|
|
244
|
+
config: {
|
|
245
|
+
...config.slack,
|
|
246
|
+
truncateFrom: config.slack.truncateFrom ?? globalTruncateFrom
|
|
247
|
+
}
|
|
256
248
|
});
|
|
257
249
|
}
|
|
258
250
|
if (config.discord.enabled) {
|
|
259
251
|
services.push({
|
|
260
252
|
name: "Discord",
|
|
261
253
|
send: send4,
|
|
262
|
-
config:
|
|
254
|
+
config: {
|
|
255
|
+
...config.discord,
|
|
256
|
+
truncateFrom: config.discord.truncateFrom ?? globalTruncateFrom
|
|
257
|
+
}
|
|
263
258
|
});
|
|
264
259
|
}
|
|
265
260
|
const lastDispatchTime = new Map;
|
|
@@ -285,22 +280,119 @@ function createDispatcher(config) {
|
|
|
285
280
|
const results = await Promise.allSettled(promises);
|
|
286
281
|
results.forEach((result, index) => {
|
|
287
282
|
if (result.status === "rejected") {
|
|
288
|
-
|
|
283
|
+
const errorMsg = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
284
|
+
logger.error(`${services[index].name} failed: ${errorMsg}`);
|
|
289
285
|
}
|
|
290
286
|
});
|
|
291
287
|
}
|
|
292
288
|
return { dispatch };
|
|
293
289
|
}
|
|
294
290
|
|
|
291
|
+
// src/logger.ts
|
|
292
|
+
import * as fs2 from "node:fs";
|
|
293
|
+
import * as path2 from "node:path";
|
|
294
|
+
import * as os2 from "node:os";
|
|
295
|
+
function getLogFilePath() {
|
|
296
|
+
return path2.join(os2.homedir(), ".config", "opencode", ".everynotify.log");
|
|
297
|
+
}
|
|
298
|
+
function createLogger(config, fsDeps) {
|
|
299
|
+
if (!config.log.enabled) {
|
|
300
|
+
return { error: () => {}, warn: () => {} };
|
|
301
|
+
}
|
|
302
|
+
const _fs = {
|
|
303
|
+
mkdirSync: fsDeps?.mkdirSync ?? fs2.mkdirSync,
|
|
304
|
+
appendFileSync: fsDeps?.appendFileSync ?? fs2.appendFileSync,
|
|
305
|
+
statSync: fsDeps?.statSync ?? fs2.statSync,
|
|
306
|
+
renameSync: fsDeps?.renameSync ?? fs2.renameSync,
|
|
307
|
+
readdirSync: fsDeps?.readdirSync ?? fs2.readdirSync,
|
|
308
|
+
unlinkSync: fsDeps?.unlinkSync ?? fs2.unlinkSync
|
|
309
|
+
};
|
|
310
|
+
const logFilePath = getLogFilePath();
|
|
311
|
+
const logDir = path2.dirname(logFilePath);
|
|
312
|
+
const level = config.log.level || "warn";
|
|
313
|
+
let disabled = false;
|
|
314
|
+
try {
|
|
315
|
+
_fs.mkdirSync(logDir, { recursive: true });
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
+
console.error(`[EveryNotify] Failed to create log directory: ${message}`);
|
|
319
|
+
disabled = true;
|
|
320
|
+
}
|
|
321
|
+
function rotateIfNeeded() {
|
|
322
|
+
try {
|
|
323
|
+
const stat = _fs.statSync(logFilePath);
|
|
324
|
+
const ageMs = Date.now() - stat.mtime.getTime();
|
|
325
|
+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
326
|
+
if (ageMs > sevenDaysMs) {
|
|
327
|
+
const mtimeDate = stat.mtime.toISOString().split("T")[0];
|
|
328
|
+
const rotatedPath = `${logFilePath}.${mtimeDate}`;
|
|
329
|
+
_fs.renameSync(logFilePath, rotatedPath);
|
|
330
|
+
cleanupRotatedFiles();
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error.code !== "ENOENT") {
|
|
334
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
335
|
+
console.error(`[EveryNotify] Rotation check failed: ${message}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function cleanupRotatedFiles() {
|
|
340
|
+
try {
|
|
341
|
+
const dir = path2.dirname(logFilePath);
|
|
342
|
+
const baseName = path2.basename(logFilePath);
|
|
343
|
+
const files = _fs.readdirSync(dir).filter((f) => f.startsWith(`${baseName}.`) && /\d{4}-\d{2}-\d{2}$/.test(f)).sort();
|
|
344
|
+
while (files.length > 4) {
|
|
345
|
+
const oldest = files.shift();
|
|
346
|
+
_fs.unlinkSync(path2.join(dir, oldest));
|
|
347
|
+
}
|
|
348
|
+
} catch (error) {
|
|
349
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
350
|
+
console.error(`[EveryNotify] Cleanup failed: ${message}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function writeLog(levelStr, msg) {
|
|
354
|
+
if (disabled)
|
|
355
|
+
return;
|
|
356
|
+
try {
|
|
357
|
+
rotateIfNeeded();
|
|
358
|
+
const timestamp = new Date().toISOString();
|
|
359
|
+
const line = `[${timestamp}] [${levelStr}] [EveryNotify] ${msg}
|
|
360
|
+
`;
|
|
361
|
+
_fs.appendFileSync(logFilePath, line, "utf-8");
|
|
362
|
+
} catch (error) {
|
|
363
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
364
|
+
console.error(`[EveryNotify] Log write failed: ${message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
error(msg) {
|
|
369
|
+
writeLog("ERROR", msg);
|
|
370
|
+
},
|
|
371
|
+
warn(msg) {
|
|
372
|
+
if (level === "error")
|
|
373
|
+
return;
|
|
374
|
+
writeLog("WARN", msg);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
295
379
|
// src/index.ts
|
|
296
380
|
var EverynotifyPlugin = async (input) => {
|
|
297
381
|
const { client, directory } = input;
|
|
298
|
-
const config = loadConfig(directory);
|
|
299
|
-
const
|
|
382
|
+
const { config, warnings } = loadConfig(directory);
|
|
383
|
+
const logger = createLogger(config);
|
|
384
|
+
for (const warning of warnings) {
|
|
385
|
+
logger.warn(warning);
|
|
386
|
+
}
|
|
387
|
+
const { dispatch } = createDispatcher(config, logger);
|
|
388
|
+
function isEventEnabled(eventType) {
|
|
389
|
+
return config.events[eventType] !== false;
|
|
390
|
+
}
|
|
300
391
|
async function buildPayload(eventType, sessionID, extraMessage) {
|
|
301
|
-
const projectName = directory ?
|
|
392
|
+
const projectName = directory ? path3.basename(directory) : null;
|
|
302
393
|
let elapsedSeconds = null;
|
|
303
394
|
let isSubagent = false;
|
|
395
|
+
let assistantText = null;
|
|
304
396
|
try {
|
|
305
397
|
if (sessionID) {
|
|
306
398
|
const sessionResult = await client.session.get({
|
|
@@ -320,6 +412,17 @@ var EverynotifyPlugin = async (input) => {
|
|
|
320
412
|
const now = Date.now();
|
|
321
413
|
elapsedSeconds = Math.floor((now - startTime) / 1000);
|
|
322
414
|
}
|
|
415
|
+
const lastAssistantMessage = [...messages].reverse().find((msg) => msg.info?.role === "assistant");
|
|
416
|
+
if (lastAssistantMessage?.parts) {
|
|
417
|
+
const textParts = lastAssistantMessage.parts.filter((part) => part.type === "text");
|
|
418
|
+
if (textParts.length > 0) {
|
|
419
|
+
const lastTextPart = textParts[textParts.length - 1];
|
|
420
|
+
const text = lastTextPart.text?.trim();
|
|
421
|
+
if (text) {
|
|
422
|
+
assistantText = text;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
323
426
|
}
|
|
324
427
|
}
|
|
325
428
|
} catch (error) {}
|
|
@@ -328,7 +431,7 @@ var EverynotifyPlugin = async (input) => {
|
|
|
328
431
|
finalEventType = "subagent_complete";
|
|
329
432
|
}
|
|
330
433
|
const title = `[${finalEventType}] ${projectName || "opencode"}`;
|
|
331
|
-
let message = extraMessage
|
|
434
|
+
let message = extraMessage ?? assistantText ?? "Task completed";
|
|
332
435
|
if (elapsedSeconds !== null) {
|
|
333
436
|
const minutes = Math.floor(elapsedSeconds / 60);
|
|
334
437
|
const seconds = elapsedSeconds % 60;
|
|
@@ -348,40 +451,60 @@ var EverynotifyPlugin = async (input) => {
|
|
|
348
451
|
try {
|
|
349
452
|
const sessionID = event.properties?.sessionID || null;
|
|
350
453
|
if (event.type === "session.idle") {
|
|
454
|
+
if (!isEventEnabled("complete")) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
351
457
|
const payload = await buildPayload("complete", sessionID);
|
|
458
|
+
if (payload.eventType === "subagent_complete" && !isEventEnabled("subagent_complete")) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
352
461
|
await dispatch(payload);
|
|
353
462
|
} else if (event.type === "session.error") {
|
|
463
|
+
if (!isEventEnabled("error")) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
354
466
|
const rawError = event.properties?.error;
|
|
355
467
|
const errorMessage = rawError?.data?.message ?? rawError?.name ?? "Unknown error";
|
|
356
468
|
const payload = await buildPayload("error", sessionID, errorMessage);
|
|
357
469
|
await dispatch(payload);
|
|
358
470
|
} else if (event.type === "permission.updated") {
|
|
471
|
+
if (!isEventEnabled("permission")) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
359
474
|
const payload = await buildPayload("permission", sessionID);
|
|
360
475
|
await dispatch(payload);
|
|
361
476
|
}
|
|
362
477
|
} catch (error) {
|
|
363
478
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
364
|
-
|
|
479
|
+
logger.error(`Event hook error: ${errorMsg}`);
|
|
365
480
|
}
|
|
366
481
|
}
|
|
367
|
-
async function permissionAskHook(
|
|
482
|
+
async function permissionAskHook(input2, _output) {
|
|
368
483
|
try {
|
|
369
|
-
|
|
484
|
+
if (!isEventEnabled("permission")) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const sessionID = input2?.sessionID ?? null;
|
|
488
|
+
const payload = await buildPayload("permission", sessionID);
|
|
370
489
|
await dispatch(payload);
|
|
371
490
|
} catch (error) {
|
|
372
491
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
373
|
-
|
|
492
|
+
logger.error(`Permission.ask hook error: ${errorMsg}`);
|
|
374
493
|
}
|
|
375
494
|
}
|
|
376
495
|
async function toolExecuteBeforeHook(input2, _output) {
|
|
377
496
|
try {
|
|
497
|
+
if (!isEventEnabled("question")) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
378
500
|
if (input2.tool === "question") {
|
|
379
|
-
const
|
|
501
|
+
const sessionID = input2?.sessionID ?? null;
|
|
502
|
+
const payload = await buildPayload("question", sessionID);
|
|
380
503
|
await dispatch(payload);
|
|
381
504
|
}
|
|
382
505
|
} catch (error) {
|
|
383
506
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
384
|
-
|
|
507
|
+
logger.error(`Tool.execute.before hook error: ${errorMsg}`);
|
|
385
508
|
}
|
|
386
509
|
}
|
|
387
510
|
return {
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EveryNotify Plugin — File-Based Logger
|
|
3
|
+
*
|
|
4
|
+
* Provides a simple file-based logger with 7-day rotation.
|
|
5
|
+
* Writes to ~/.config/opencode/.everynotify.log with automatic cleanup.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import type { EverynotifyConfig } from "./types";
|
|
9
|
+
export interface FsDeps {
|
|
10
|
+
mkdirSync: typeof fs.mkdirSync;
|
|
11
|
+
appendFileSync: typeof fs.appendFileSync;
|
|
12
|
+
statSync: typeof fs.statSync;
|
|
13
|
+
renameSync: typeof fs.renameSync;
|
|
14
|
+
readdirSync: typeof fs.readdirSync;
|
|
15
|
+
unlinkSync: typeof fs.unlinkSync;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Logger interface with error and warn methods
|
|
19
|
+
*/
|
|
20
|
+
export interface Logger {
|
|
21
|
+
error(msg: string): void;
|
|
22
|
+
warn(msg: string): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Returns the absolute path to the log file
|
|
26
|
+
* @returns Log file path: ~/.config/opencode/.everynotify.log
|
|
27
|
+
*/
|
|
28
|
+
export declare function getLogFilePath(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Creates a logger instance based on configuration
|
|
31
|
+
* @param config - EveryNotify configuration object
|
|
32
|
+
* @param fsDeps - Optional fs operations override for testing
|
|
33
|
+
* @returns Logger instance (no-op if logging disabled)
|
|
34
|
+
*/
|
|
35
|
+
export declare function createLogger(config: EverynotifyConfig, fsDeps?: Partial<FsDeps>): Logger;
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
* All types for notification payloads, service configurations, and dispatch functions.
|
|
5
5
|
* No validation logic here — types only.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Truncation direction for messages exceeding service limits
|
|
9
|
+
* - "end": Keep beginning, truncate end (default)
|
|
10
|
+
* - "start": Keep end, truncate beginning
|
|
11
|
+
*/
|
|
12
|
+
export type TruncationMode = "start" | "end";
|
|
7
13
|
/**
|
|
8
14
|
* Event types that trigger notifications
|
|
9
15
|
*/
|
|
@@ -32,6 +38,7 @@ export interface PushoverConfig {
|
|
|
32
38
|
token: string;
|
|
33
39
|
userKey: string;
|
|
34
40
|
priority?: number;
|
|
41
|
+
truncateFrom?: TruncationMode;
|
|
35
42
|
}
|
|
36
43
|
/**
|
|
37
44
|
* Telegram service configuration
|
|
@@ -43,6 +50,7 @@ export interface TelegramConfig {
|
|
|
43
50
|
enabled: boolean;
|
|
44
51
|
botToken: string;
|
|
45
52
|
chatId: string;
|
|
53
|
+
truncateFrom?: TruncationMode;
|
|
46
54
|
}
|
|
47
55
|
/**
|
|
48
56
|
* Slack service configuration
|
|
@@ -52,6 +60,7 @@ export interface TelegramConfig {
|
|
|
52
60
|
export interface SlackConfig {
|
|
53
61
|
enabled: boolean;
|
|
54
62
|
webhookUrl: string;
|
|
63
|
+
truncateFrom?: TruncationMode;
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* Discord service configuration
|
|
@@ -61,6 +70,33 @@ export interface SlackConfig {
|
|
|
61
70
|
export interface DiscordConfig {
|
|
62
71
|
enabled: boolean;
|
|
63
72
|
webhookUrl: string;
|
|
73
|
+
truncateFrom?: TruncationMode;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Logging configuration
|
|
77
|
+
* Controls debug output and error logging behavior
|
|
78
|
+
* - enabled: whether logging is active
|
|
79
|
+
* - level: minimum log level to output ("error" or "warn")
|
|
80
|
+
*/
|
|
81
|
+
export interface LogConfig {
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
level?: "error" | "warn";
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Events configuration
|
|
87
|
+
* Controls which event types trigger notifications
|
|
88
|
+
* - complete: main session completion events
|
|
89
|
+
* - subagent_complete: subagent task completion events
|
|
90
|
+
* - error: session error events
|
|
91
|
+
* - permission: permission request events
|
|
92
|
+
* - question: question tool usage events
|
|
93
|
+
*/
|
|
94
|
+
export interface EventsConfig {
|
|
95
|
+
complete: boolean;
|
|
96
|
+
subagent_complete: boolean;
|
|
97
|
+
error: boolean;
|
|
98
|
+
permission: boolean;
|
|
99
|
+
question: boolean;
|
|
64
100
|
}
|
|
65
101
|
/**
|
|
66
102
|
* Top-level configuration object containing all service configs
|
|
@@ -70,6 +106,9 @@ export interface EverynotifyConfig {
|
|
|
70
106
|
telegram: TelegramConfig;
|
|
71
107
|
slack: SlackConfig;
|
|
72
108
|
discord: DiscordConfig;
|
|
109
|
+
log: LogConfig;
|
|
110
|
+
events: EventsConfig;
|
|
111
|
+
truncateFrom?: TruncationMode;
|
|
73
112
|
}
|
|
74
113
|
/**
|
|
75
114
|
* Function signature for service send functions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sillybit/opencode-plugin-everynotify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Multi-service notification plugin for opencode — Pushover, Telegram, Slack, Discord",
|
|
5
5
|
"author": "Sillybit <https://sillybit.io>",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@opencode-ai/plugin": "^1.1.53",
|
|
28
|
+
"@types/bun": "^1.3.8",
|
|
28
29
|
"@types/node": "^22.0.0",
|
|
29
30
|
"typescript": "^5.9.3"
|
|
30
31
|
},
|