@sillybit/opencode-plugin-everynotify 0.2.0 → 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 +42 -1
- package/dist/config.d.ts +6 -2
- package/dist/dispatcher.d.ts +2 -6
- package/dist/index.js +156 -171
- package/dist/logger.d.ts +11 -1
- package/dist/types.d.ts +11 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ In long-running development tasks or deep research sessions, it's common to swit
|
|
|
27
27
|
- ✅ **Privacy & Control**: Completely opt-in; no notifications are sent until you enable and configure a service.
|
|
28
28
|
- ✅ **Event Filtering**: Selectively enable or disable notifications for specific event types.
|
|
29
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.
|
|
30
31
|
|
|
31
32
|
## Installation
|
|
32
33
|
|
|
@@ -117,7 +118,8 @@ Create your `.everynotify.json` with the tokens for the services you want to use
|
|
|
117
118
|
"error": true,
|
|
118
119
|
"permission": false,
|
|
119
120
|
"question": true
|
|
120
|
-
}
|
|
121
|
+
},
|
|
122
|
+
"truncateFrom": "end"
|
|
121
123
|
}
|
|
122
124
|
```
|
|
123
125
|
|
|
@@ -135,6 +137,41 @@ You can control which events trigger notifications by adding an `events` block t
|
|
|
135
137
|
| `permission` | opencode is waiting for you to grant permission for a tool or file access. |
|
|
136
138
|
| `question` | The `question` tool was used to ask you for clarification. |
|
|
137
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
|
+
|
|
138
175
|
### Rich Message Content
|
|
139
176
|
|
|
140
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.
|
|
@@ -227,6 +264,10 @@ For Pushover users, you can customize the `priority` level:
|
|
|
227
264
|
- `1`: High priority (bypasses quiet hours)
|
|
228
265
|
- `2`: Emergency priority (requires acknowledgment)
|
|
229
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
|
+
|
|
230
271
|
### Session Enrichment
|
|
231
272
|
|
|
232
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,13 +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";
|
|
9
|
+
import type { EverynotifyConfig, NotificationPayload, TruncationMode } from "./types";
|
|
10
10
|
import type { Logger } from "./logger";
|
|
11
|
-
|
|
12
|
-
* Truncate text to max length, appending "… [truncated]" if over limit
|
|
13
|
-
* Shared utility function used by all services
|
|
14
|
-
*/
|
|
15
|
-
export declare function truncate(text: string, maxLength: number): string;
|
|
11
|
+
export declare function truncate(text: string, maxLength: number, from?: TruncationMode): string;
|
|
16
12
|
/**
|
|
17
13
|
* Dispatcher interface returned by createDispatcher
|
|
18
14
|
*/
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,8 @@ var DEFAULT_CONFIG = {
|
|
|
35
35
|
error: true,
|
|
36
36
|
permission: true,
|
|
37
37
|
question: true
|
|
38
|
-
}
|
|
38
|
+
},
|
|
39
|
+
truncateFrom: "end"
|
|
39
40
|
};
|
|
40
41
|
function getConfigPath(scope, directory) {
|
|
41
42
|
if (scope === "global") {
|
|
@@ -63,9 +64,12 @@ function deepMerge(target, source) {
|
|
|
63
64
|
if (source.events) {
|
|
64
65
|
result.events = { ...result.events, ...source.events };
|
|
65
66
|
}
|
|
67
|
+
if (source.truncateFrom !== undefined) {
|
|
68
|
+
result.truncateFrom = source.truncateFrom;
|
|
69
|
+
}
|
|
66
70
|
return result;
|
|
67
71
|
}
|
|
68
|
-
function loadConfigFile(filePath) {
|
|
72
|
+
function loadConfigFile(filePath, warnings) {
|
|
69
73
|
try {
|
|
70
74
|
if (!fs.existsSync(filePath)) {
|
|
71
75
|
return null;
|
|
@@ -75,208 +79,182 @@ function loadConfigFile(filePath) {
|
|
|
75
79
|
return parsed;
|
|
76
80
|
} catch (error) {
|
|
77
81
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
78
|
-
|
|
82
|
+
warnings.push(`Failed to load config from ${filePath}: ${errorMsg}`);
|
|
79
83
|
return null;
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
function loadConfig(directory) {
|
|
87
|
+
const warnings = [];
|
|
83
88
|
let config = { ...DEFAULT_CONFIG };
|
|
84
89
|
const globalPath = getConfigPath("global", directory);
|
|
85
|
-
const globalConfig = loadConfigFile(globalPath);
|
|
90
|
+
const globalConfig = loadConfigFile(globalPath, warnings);
|
|
86
91
|
if (globalConfig) {
|
|
87
92
|
config = deepMerge(config, globalConfig);
|
|
88
93
|
}
|
|
89
94
|
const projectPath = getConfigPath("project", directory);
|
|
90
|
-
const projectConfig = loadConfigFile(projectPath);
|
|
95
|
+
const projectConfig = loadConfigFile(projectPath, warnings);
|
|
91
96
|
if (projectConfig) {
|
|
92
97
|
config = deepMerge(config, projectConfig);
|
|
93
98
|
}
|
|
94
99
|
const allDisabled = !config.pushover.enabled && !config.telegram.enabled && !config.slack.enabled && !config.discord.enabled;
|
|
95
100
|
if (allDisabled) {
|
|
96
|
-
|
|
101
|
+
warnings.push("No services configured. Enable services in .everynotify.json");
|
|
97
102
|
}
|
|
98
|
-
return config;
|
|
103
|
+
return { config, warnings };
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
// src/services/pushover.ts
|
|
102
|
-
function truncate(text, maxLength) {
|
|
103
|
-
if (text.length <= maxLength) {
|
|
104
|
-
return text;
|
|
105
|
-
}
|
|
106
|
-
const suffix = "… [truncated]";
|
|
107
|
-
return text.slice(0, maxLength - suffix.length) + suffix;
|
|
108
|
-
}
|
|
109
107
|
async function send(config, payload, signal) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
console.error(`[EveryNotify] Pushover error: ${response.status} ${errorText}`);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
-
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}`);
|
|
134
126
|
}
|
|
135
127
|
}
|
|
136
128
|
|
|
137
129
|
// src/services/telegram.ts
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
const suffix = "… [truncated]";
|
|
143
|
-
return text.slice(0, maxLength - suffix.length) + suffix;
|
|
144
|
-
}
|
|
145
|
-
function formatMessage(payload) {
|
|
146
|
-
const truncatedTitle = truncate2(payload.title, 250);
|
|
147
|
-
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);
|
|
148
133
|
return `<b>${truncatedTitle}</b>
|
|
149
134
|
${truncatedMessage}`;
|
|
150
135
|
}
|
|
151
136
|
async function send2(config, payload, signal) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
console.error(`[EveryNotify] Telegram error: ${response.status} ${errorText}`);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
} catch (error) {
|
|
173
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
-
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}`);
|
|
175
154
|
}
|
|
176
155
|
}
|
|
177
156
|
|
|
178
157
|
// src/services/slack.ts
|
|
179
|
-
function truncate3(text, maxLength) {
|
|
180
|
-
if (text.length <= maxLength) {
|
|
181
|
-
return text;
|
|
182
|
-
}
|
|
183
|
-
const suffix = "… [truncated]";
|
|
184
|
-
return text.slice(0, maxLength - suffix.length) + suffix;
|
|
185
|
-
}
|
|
186
158
|
function formatSlackMessage(payload) {
|
|
187
159
|
return `*${payload.title}*
|
|
188
160
|
${payload.message}`;
|
|
189
161
|
}
|
|
190
162
|
async function send3(config, payload, signal) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
console.error(`[EveryNotify] Slack error: ${response.status} ${errorText}`);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
} catch (error) {
|
|
207
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
-
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}`);
|
|
209
175
|
}
|
|
210
176
|
}
|
|
211
177
|
|
|
212
178
|
// src/services/discord.ts
|
|
213
|
-
function truncate4(text, maxLength) {
|
|
214
|
-
if (text.length <= maxLength) {
|
|
215
|
-
return text;
|
|
216
|
-
}
|
|
217
|
-
const suffix = "… [truncated]";
|
|
218
|
-
return text.slice(0, maxLength - suffix.length) + suffix;
|
|
219
|
-
}
|
|
220
179
|
function formatDiscordMessage(payload) {
|
|
221
180
|
return `**${payload.title}**
|
|
222
181
|
${payload.message}`;
|
|
223
182
|
}
|
|
224
183
|
async function send4(config, payload, signal) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const errorText = await response.text();
|
|
242
|
-
console.error(`[EveryNotify] Discord error: ${response.status} ${errorText}`);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
} catch (error) {
|
|
246
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
247
|
-
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}`);
|
|
248
200
|
}
|
|
249
201
|
}
|
|
250
202
|
|
|
251
203
|
// src/dispatcher.ts
|
|
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
|
+
}
|
|
252
217
|
function createDispatcher(config, logger) {
|
|
253
218
|
const services = [];
|
|
219
|
+
const globalTruncateFrom = config.truncateFrom ?? "end";
|
|
254
220
|
if (config.pushover.enabled) {
|
|
255
221
|
services.push({
|
|
256
222
|
name: "Pushover",
|
|
257
223
|
send,
|
|
258
|
-
config:
|
|
224
|
+
config: {
|
|
225
|
+
...config.pushover,
|
|
226
|
+
truncateFrom: config.pushover.truncateFrom ?? globalTruncateFrom
|
|
227
|
+
}
|
|
259
228
|
});
|
|
260
229
|
}
|
|
261
230
|
if (config.telegram.enabled) {
|
|
262
231
|
services.push({
|
|
263
232
|
name: "Telegram",
|
|
264
233
|
send: send2,
|
|
265
|
-
config:
|
|
234
|
+
config: {
|
|
235
|
+
...config.telegram,
|
|
236
|
+
truncateFrom: config.telegram.truncateFrom ?? globalTruncateFrom
|
|
237
|
+
}
|
|
266
238
|
});
|
|
267
239
|
}
|
|
268
240
|
if (config.slack.enabled) {
|
|
269
241
|
services.push({
|
|
270
242
|
name: "Slack",
|
|
271
243
|
send: send3,
|
|
272
|
-
config:
|
|
244
|
+
config: {
|
|
245
|
+
...config.slack,
|
|
246
|
+
truncateFrom: config.slack.truncateFrom ?? globalTruncateFrom
|
|
247
|
+
}
|
|
273
248
|
});
|
|
274
249
|
}
|
|
275
250
|
if (config.discord.enabled) {
|
|
276
251
|
services.push({
|
|
277
252
|
name: "Discord",
|
|
278
253
|
send: send4,
|
|
279
|
-
config:
|
|
254
|
+
config: {
|
|
255
|
+
...config.discord,
|
|
256
|
+
truncateFrom: config.discord.truncateFrom ?? globalTruncateFrom
|
|
257
|
+
}
|
|
280
258
|
});
|
|
281
259
|
}
|
|
282
260
|
const lastDispatchTime = new Map;
|
|
@@ -303,7 +281,6 @@ function createDispatcher(config, logger) {
|
|
|
303
281
|
results.forEach((result, index) => {
|
|
304
282
|
if (result.status === "rejected") {
|
|
305
283
|
const errorMsg = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
306
|
-
console.error(`[EveryNotify] ${services[index].name} failed:`, result.reason);
|
|
307
284
|
logger.error(`${services[index].name} failed: ${errorMsg}`);
|
|
308
285
|
}
|
|
309
286
|
});
|
|
@@ -318,30 +295,70 @@ import * as os2 from "node:os";
|
|
|
318
295
|
function getLogFilePath() {
|
|
319
296
|
return path2.join(os2.homedir(), ".config", "opencode", ".everynotify.log");
|
|
320
297
|
}
|
|
321
|
-
function createLogger(config) {
|
|
298
|
+
function createLogger(config, fsDeps) {
|
|
322
299
|
if (!config.log.enabled) {
|
|
323
300
|
return { error: () => {}, warn: () => {} };
|
|
324
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
|
+
};
|
|
325
310
|
const logFilePath = getLogFilePath();
|
|
326
311
|
const logDir = path2.dirname(logFilePath);
|
|
327
312
|
const level = config.log.level || "warn";
|
|
328
313
|
let disabled = false;
|
|
329
314
|
try {
|
|
330
|
-
|
|
315
|
+
_fs.mkdirSync(logDir, { recursive: true });
|
|
331
316
|
} catch (error) {
|
|
332
317
|
const message = error instanceof Error ? error.message : String(error);
|
|
333
318
|
console.error(`[EveryNotify] Failed to create log directory: ${message}`);
|
|
334
319
|
disabled = true;
|
|
335
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
|
+
}
|
|
336
353
|
function writeLog(levelStr, msg) {
|
|
337
354
|
if (disabled)
|
|
338
355
|
return;
|
|
339
356
|
try {
|
|
340
|
-
rotateIfNeeded(
|
|
357
|
+
rotateIfNeeded();
|
|
341
358
|
const timestamp = new Date().toISOString();
|
|
342
359
|
const line = `[${timestamp}] [${levelStr}] [EveryNotify] ${msg}
|
|
343
360
|
`;
|
|
344
|
-
|
|
361
|
+
_fs.appendFileSync(logFilePath, line, "utf-8");
|
|
345
362
|
} catch (error) {
|
|
346
363
|
const message = error instanceof Error ? error.message : String(error);
|
|
347
364
|
console.error(`[EveryNotify] Log write failed: ${message}`);
|
|
@@ -358,44 +375,15 @@ function createLogger(config) {
|
|
|
358
375
|
}
|
|
359
376
|
};
|
|
360
377
|
}
|
|
361
|
-
function rotateIfNeeded(logFilePath) {
|
|
362
|
-
try {
|
|
363
|
-
const stat = fs2.statSync(logFilePath);
|
|
364
|
-
const ageMs = Date.now() - stat.mtime.getTime();
|
|
365
|
-
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
366
|
-
if (ageMs > sevenDaysMs) {
|
|
367
|
-
const mtimeDate = stat.mtime.toISOString().split("T")[0];
|
|
368
|
-
const rotatedPath = `${logFilePath}.${mtimeDate}`;
|
|
369
|
-
fs2.renameSync(logFilePath, rotatedPath);
|
|
370
|
-
cleanupRotatedFiles(logFilePath);
|
|
371
|
-
}
|
|
372
|
-
} catch (error) {
|
|
373
|
-
if (error.code !== "ENOENT") {
|
|
374
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
375
|
-
console.error(`[EveryNotify] Rotation check failed: ${message}`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
function cleanupRotatedFiles(logFilePath) {
|
|
380
|
-
try {
|
|
381
|
-
const dir = path2.dirname(logFilePath);
|
|
382
|
-
const baseName = path2.basename(logFilePath);
|
|
383
|
-
const files = fs2.readdirSync(dir).filter((f) => f.startsWith(`${baseName}.`) && /\d{4}-\d{2}-\d{2}$/.test(f)).sort();
|
|
384
|
-
while (files.length > 4) {
|
|
385
|
-
const oldest = files.shift();
|
|
386
|
-
fs2.unlinkSync(path2.join(dir, oldest));
|
|
387
|
-
}
|
|
388
|
-
} catch (error) {
|
|
389
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
390
|
-
console.error(`[EveryNotify] Cleanup failed: ${message}`);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
378
|
|
|
394
379
|
// src/index.ts
|
|
395
380
|
var EverynotifyPlugin = async (input) => {
|
|
396
381
|
const { client, directory } = input;
|
|
397
|
-
const config = loadConfig(directory);
|
|
382
|
+
const { config, warnings } = loadConfig(directory);
|
|
398
383
|
const logger = createLogger(config);
|
|
384
|
+
for (const warning of warnings) {
|
|
385
|
+
logger.warn(warning);
|
|
386
|
+
}
|
|
399
387
|
const { dispatch } = createDispatcher(config, logger);
|
|
400
388
|
function isEventEnabled(eventType) {
|
|
401
389
|
return config.events[eventType] !== false;
|
|
@@ -488,7 +476,6 @@ var EverynotifyPlugin = async (input) => {
|
|
|
488
476
|
}
|
|
489
477
|
} catch (error) {
|
|
490
478
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
491
|
-
console.error(`[EveryNotify] Event hook error: ${errorMsg}`);
|
|
492
479
|
logger.error(`Event hook error: ${errorMsg}`);
|
|
493
480
|
}
|
|
494
481
|
}
|
|
@@ -502,7 +489,6 @@ var EverynotifyPlugin = async (input) => {
|
|
|
502
489
|
await dispatch(payload);
|
|
503
490
|
} catch (error) {
|
|
504
491
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
505
|
-
console.error(`[EveryNotify] Permission.ask hook error: ${errorMsg}`);
|
|
506
492
|
logger.error(`Permission.ask hook error: ${errorMsg}`);
|
|
507
493
|
}
|
|
508
494
|
}
|
|
@@ -518,7 +504,6 @@ var EverynotifyPlugin = async (input) => {
|
|
|
518
504
|
}
|
|
519
505
|
} catch (error) {
|
|
520
506
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
521
|
-
console.error(`[EveryNotify] Tool.execute.before hook error: ${errorMsg}`);
|
|
522
507
|
logger.error(`Tool.execute.before hook error: ${errorMsg}`);
|
|
523
508
|
}
|
|
524
509
|
}
|
package/dist/logger.d.ts
CHANGED
|
@@ -4,7 +4,16 @@
|
|
|
4
4
|
* Provides a simple file-based logger with 7-day rotation.
|
|
5
5
|
* Writes to ~/.config/opencode/.everynotify.log with automatic cleanup.
|
|
6
6
|
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
7
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
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Logger interface with error and warn methods
|
|
10
19
|
*/
|
|
@@ -20,6 +29,7 @@ export declare function getLogFilePath(): string;
|
|
|
20
29
|
/**
|
|
21
30
|
* Creates a logger instance based on configuration
|
|
22
31
|
* @param config - EveryNotify configuration object
|
|
32
|
+
* @param fsDeps - Optional fs operations override for testing
|
|
23
33
|
* @returns Logger instance (no-op if logging disabled)
|
|
24
34
|
*/
|
|
25
|
-
export declare function createLogger(config: EverynotifyConfig): Logger;
|
|
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,7 @@ export interface SlackConfig {
|
|
|
61
70
|
export interface DiscordConfig {
|
|
62
71
|
enabled: boolean;
|
|
63
72
|
webhookUrl: string;
|
|
73
|
+
truncateFrom?: TruncationMode;
|
|
64
74
|
}
|
|
65
75
|
/**
|
|
66
76
|
* Logging configuration
|
|
@@ -98,6 +108,7 @@ export interface EverynotifyConfig {
|
|
|
98
108
|
discord: DiscordConfig;
|
|
99
109
|
log: LogConfig;
|
|
100
110
|
events: EventsConfig;
|
|
111
|
+
truncateFrom?: TruncationMode;
|
|
101
112
|
}
|
|
102
113
|
/**
|
|
103
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
|
},
|