opencode-smart-voice-notify 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +585 -460
- package/example.config.jsonc +369 -369
- package/index.js +1511 -1468
- package/package.json +2 -2
- package/util/ai-messages.js +278 -278
- package/util/config.js +1058 -1058
- package/util/desktop-notify.js +319 -319
- package/util/focus-detect.js +372 -372
- package/util/per-project-sound.js +90 -90
- package/util/sound-theme.js +120 -120
- package/util/tts.js +720 -684
- package/util/webhook.js +743 -743
package/util/desktop-notify.js
CHANGED
|
@@ -1,319 +1,319 @@
|
|
|
1
|
-
import notifier from 'node-notifier';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Desktop Notification Module for OpenCode Smart Voice Notify
|
|
8
|
-
*
|
|
9
|
-
* Provides cross-platform native desktop notifications using node-notifier.
|
|
10
|
-
* Supports Windows Toast, macOS Notification Center, and Linux notify-send.
|
|
11
|
-
*
|
|
12
|
-
* Platform-specific behaviors:
|
|
13
|
-
* - Windows: Uses SnoreToast for Windows 8+ toast notifications
|
|
14
|
-
* - macOS: Uses terminal-notifier for Notification Center
|
|
15
|
-
* - Linux: Uses notify-send (requires libnotify-bin package)
|
|
16
|
-
*
|
|
17
|
-
* @module util/desktop-notify
|
|
18
|
-
* @see docs/ARCHITECT_PLAN.md - Phase 1, Task 1.2
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Debug logging to file.
|
|
23
|
-
* Only logs when config.debugLog is enabled.
|
|
24
|
-
* Writes to ~/.config/opencode/logs/smart-voice-notify-debug.log
|
|
25
|
-
*
|
|
26
|
-
* @param {string} message - Message to log
|
|
27
|
-
* @param {boolean} enabled - Whether debug logging is enabled
|
|
28
|
-
*/
|
|
29
|
-
const debugLog = (message, enabled = false) => {
|
|
30
|
-
if (!enabled) return;
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
|
|
34
|
-
const logsDir = path.join(configDir, 'logs');
|
|
35
|
-
|
|
36
|
-
if (!fs.existsSync(logsDir)) {
|
|
37
|
-
fs.mkdirSync(logsDir, { recursive: true });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const logFile = path.join(logsDir, 'smart-voice-notify-debug.log');
|
|
41
|
-
const timestamp = new Date().toISOString();
|
|
42
|
-
fs.appendFileSync(logFile, `[${timestamp}] [desktop-notify] ${message}\n`);
|
|
43
|
-
} catch (e) {
|
|
44
|
-
// Silently fail - logging should never break the plugin
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Get the current platform identifier.
|
|
50
|
-
* @returns {'darwin' | 'win32' | 'linux'} Platform string
|
|
51
|
-
*/
|
|
52
|
-
export const getPlatform = () => os.platform();
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Check if desktop notifications are likely to work on this platform.
|
|
56
|
-
*
|
|
57
|
-
* @returns {{ supported: boolean, reason?: string }} Support status and reason if not supported
|
|
58
|
-
*/
|
|
59
|
-
export const checkNotificationSupport = () => {
|
|
60
|
-
const platform = getPlatform();
|
|
61
|
-
|
|
62
|
-
switch (platform) {
|
|
63
|
-
case 'darwin':
|
|
64
|
-
// macOS always supports notifications via terminal-notifier (bundled)
|
|
65
|
-
return { supported: true };
|
|
66
|
-
|
|
67
|
-
case 'win32':
|
|
68
|
-
// Windows 8+ supports toast notifications via SnoreToast (bundled)
|
|
69
|
-
return { supported: true };
|
|
70
|
-
|
|
71
|
-
case 'linux':
|
|
72
|
-
// Linux requires notify-send from libnotify-bin package
|
|
73
|
-
// We don't check for its existence here - node-notifier handles the fallback
|
|
74
|
-
return { supported: true };
|
|
75
|
-
|
|
76
|
-
default:
|
|
77
|
-
return { supported: false, reason: `Unsupported platform: ${platform}` };
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Build platform-specific notification options.
|
|
83
|
-
* Normalizes options across different platforms while respecting their unique capabilities.
|
|
84
|
-
*
|
|
85
|
-
* @param {string} title - Notification title
|
|
86
|
-
* @param {string} message - Notification body/message
|
|
87
|
-
* @param {object} options - Additional options
|
|
88
|
-
* @param {number} [options.timeout=5] - Notification timeout in seconds
|
|
89
|
-
* @param {boolean} [options.sound=false] - Whether to play a sound (platform-specific)
|
|
90
|
-
* @param {string} [options.icon] - Absolute path to notification icon
|
|
91
|
-
* @param {string} [options.subtitle] - Subtitle (macOS only)
|
|
92
|
-
* @param {string} [options.urgency] - Urgency level: 'low', 'normal', 'critical' (Linux only)
|
|
93
|
-
* @returns {object} Platform-normalized notification options
|
|
94
|
-
*/
|
|
95
|
-
const buildPlatformOptions = (title, message, options = {}) => {
|
|
96
|
-
const platform = getPlatform();
|
|
97
|
-
const { timeout = 5, sound = false, icon, subtitle, urgency } = options;
|
|
98
|
-
|
|
99
|
-
// Base options common to all platforms
|
|
100
|
-
const baseOptions = {
|
|
101
|
-
title: title || 'OpenCode',
|
|
102
|
-
message: message || '',
|
|
103
|
-
sound: sound,
|
|
104
|
-
wait: false // Don't block - fire and forget
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
// Add icon if provided and exists
|
|
108
|
-
if (icon && fs.existsSync(icon)) {
|
|
109
|
-
baseOptions.icon = icon;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Platform-specific options
|
|
113
|
-
switch (platform) {
|
|
114
|
-
case 'darwin':
|
|
115
|
-
// macOS Notification Center options
|
|
116
|
-
return {
|
|
117
|
-
...baseOptions,
|
|
118
|
-
timeout: timeout,
|
|
119
|
-
subtitle: subtitle || undefined
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
case 'win32':
|
|
123
|
-
// Windows Toast options
|
|
124
|
-
return {
|
|
125
|
-
...baseOptions,
|
|
126
|
-
// Windows doesn't use timeout the same way - notifications persist until dismissed
|
|
127
|
-
// sound can be true/false or a system sound name
|
|
128
|
-
sound: sound
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
case 'linux':
|
|
132
|
-
// Linux notify-send options
|
|
133
|
-
return {
|
|
134
|
-
...baseOptions,
|
|
135
|
-
timeout: timeout, // Timeout in seconds
|
|
136
|
-
urgency: urgency || 'normal', // low, normal, critical
|
|
137
|
-
'app-name': 'OpenCode Smart Notify'
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
default:
|
|
141
|
-
return baseOptions;
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Send a native desktop notification.
|
|
147
|
-
*
|
|
148
|
-
* This is the main function for sending cross-platform desktop notifications.
|
|
149
|
-
* It handles platform-specific options and gracefully fails if notifications
|
|
150
|
-
* are not supported or the notifier encounters an error.
|
|
151
|
-
*
|
|
152
|
-
* @param {string} title - Notification title
|
|
153
|
-
* @param {string} message - Notification body/message
|
|
154
|
-
* @param {object} [options={}] - Notification options
|
|
155
|
-
* @param {number} [options.timeout=5] - Notification timeout in seconds
|
|
156
|
-
* @param {boolean} [options.sound=false] - Whether to play a sound
|
|
157
|
-
* @param {string} [options.icon] - Absolute path to notification icon
|
|
158
|
-
* @param {string} [options.subtitle] - Subtitle (macOS only)
|
|
159
|
-
* @param {string} [options.urgency='normal'] - Urgency level (Linux only)
|
|
160
|
-
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
161
|
-
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* // Simple notification
|
|
165
|
-
* await sendDesktopNotification('Task Complete', 'Your code is ready for review');
|
|
166
|
-
*
|
|
167
|
-
* @example
|
|
168
|
-
* // With options
|
|
169
|
-
* await sendDesktopNotification('Permission Required', 'Agent needs approval', {
|
|
170
|
-
* timeout: 10,
|
|
171
|
-
* urgency: 'critical',
|
|
172
|
-
* sound: true
|
|
173
|
-
* });
|
|
174
|
-
*/
|
|
175
|
-
export const sendDesktopNotification = async (title, message, options = {}) => {
|
|
176
|
-
// Handle null/undefined options gracefully
|
|
177
|
-
const opts = options || {};
|
|
178
|
-
const debug = opts.debugLog || false;
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
// Check platform support
|
|
182
|
-
const support = checkNotificationSupport();
|
|
183
|
-
if (!support.supported) {
|
|
184
|
-
debugLog(`Notification not supported: ${support.reason}`, debug);
|
|
185
|
-
return { success: false, error: support.reason };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Build platform-specific options
|
|
189
|
-
const notifyOptions = buildPlatformOptions(title, message, opts);
|
|
190
|
-
|
|
191
|
-
debugLog(`Sending notification: "${title}" - "${message}" (platform: ${getPlatform()})`, debug);
|
|
192
|
-
|
|
193
|
-
// Send notification using promise wrapper
|
|
194
|
-
return new Promise((resolve) => {
|
|
195
|
-
notifier.notify(notifyOptions, (error, response) => {
|
|
196
|
-
if (error) {
|
|
197
|
-
debugLog(`Notification error: ${error.message}`, debug);
|
|
198
|
-
resolve({ success: false, error: error.message });
|
|
199
|
-
} else {
|
|
200
|
-
debugLog(`Notification sent successfully (response: ${response})`, debug);
|
|
201
|
-
resolve({ success: true });
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
} catch (error) {
|
|
206
|
-
debugLog(`Notification exception: ${error.message}`, debug);
|
|
207
|
-
return { success: false, error: error.message };
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Send a notification for session idle (task completion).
|
|
213
|
-
* Pre-configured for task completion notifications.
|
|
214
|
-
*
|
|
215
|
-
* @param {string} message - Notification message
|
|
216
|
-
* @param {object} [options={}] - Additional options
|
|
217
|
-
* @param {string} [options.projectName] - Project name to include in title
|
|
218
|
-
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
219
|
-
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
220
|
-
*/
|
|
221
|
-
export const notifyTaskComplete = async (message, options = {}) => {
|
|
222
|
-
const title = options.projectName
|
|
223
|
-
? `✅ ${options.projectName} - Task Complete`
|
|
224
|
-
: '✅ OpenCode - Task Complete';
|
|
225
|
-
|
|
226
|
-
return sendDesktopNotification(title, message, {
|
|
227
|
-
timeout: 5,
|
|
228
|
-
sound: false, // We handle sound separately in the main plugin
|
|
229
|
-
...options
|
|
230
|
-
});
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Send a notification for permission requests.
|
|
235
|
-
* Pre-configured for permission request notifications (more urgent).
|
|
236
|
-
*
|
|
237
|
-
* @param {string} message - Notification message
|
|
238
|
-
* @param {object} [options={}] - Additional options
|
|
239
|
-
* @param {string} [options.projectName] - Project name to include in title
|
|
240
|
-
* @param {number} [options.count=1] - Number of permission requests
|
|
241
|
-
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
242
|
-
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
243
|
-
*/
|
|
244
|
-
export const notifyPermissionRequest = async (message, options = {}) => {
|
|
245
|
-
const count = options.count || 1;
|
|
246
|
-
const title = options.projectName
|
|
247
|
-
? `⚠️ ${options.projectName} - Permission Required`
|
|
248
|
-
: count > 1
|
|
249
|
-
? `⚠️ ${count} Permissions Required`
|
|
250
|
-
: '⚠️ OpenCode - Permission Required';
|
|
251
|
-
|
|
252
|
-
return sendDesktopNotification(title, message, {
|
|
253
|
-
timeout: 10, // Longer timeout for permissions
|
|
254
|
-
urgency: 'critical', // Higher urgency on Linux
|
|
255
|
-
sound: false, // We handle sound separately
|
|
256
|
-
...options
|
|
257
|
-
});
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Send a notification for question requests (SDK v1.1.7+).
|
|
262
|
-
* Pre-configured for question notifications.
|
|
263
|
-
*
|
|
264
|
-
* @param {string} message - Notification message
|
|
265
|
-
* @param {object} [options={}] - Additional options
|
|
266
|
-
* @param {string} [options.projectName] - Project name to include in title
|
|
267
|
-
* @param {number} [options.count=1] - Number of questions
|
|
268
|
-
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
269
|
-
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
270
|
-
*/
|
|
271
|
-
export const notifyQuestion = async (message, options = {}) => {
|
|
272
|
-
const count = options.count || 1;
|
|
273
|
-
const title = options.projectName
|
|
274
|
-
? `❓ ${options.projectName} - Question`
|
|
275
|
-
: count > 1
|
|
276
|
-
? `❓ ${count} Questions Need Your Input`
|
|
277
|
-
: '❓ OpenCode - Question';
|
|
278
|
-
|
|
279
|
-
return sendDesktopNotification(title, message, {
|
|
280
|
-
timeout: 8,
|
|
281
|
-
urgency: 'normal',
|
|
282
|
-
sound: false, // We handle sound separately
|
|
283
|
-
...options
|
|
284
|
-
});
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Send a notification for error events.
|
|
289
|
-
* Pre-configured for error notifications (most urgent).
|
|
290
|
-
*
|
|
291
|
-
* @param {string} message - Notification message
|
|
292
|
-
* @param {object} [options={}] - Additional options
|
|
293
|
-
* @param {string} [options.projectName] - Project name to include in title
|
|
294
|
-
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
295
|
-
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
296
|
-
*/
|
|
297
|
-
export const notifyError = async (message, options = {}) => {
|
|
298
|
-
const title = options.projectName
|
|
299
|
-
? `❌ ${options.projectName} - Error`
|
|
300
|
-
: '❌ OpenCode - Error';
|
|
301
|
-
|
|
302
|
-
return sendDesktopNotification(title, message, {
|
|
303
|
-
timeout: 15, // Longer timeout for errors
|
|
304
|
-
urgency: 'critical',
|
|
305
|
-
sound: false, // We handle sound separately
|
|
306
|
-
...options
|
|
307
|
-
});
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// Default export for convenience
|
|
311
|
-
export default {
|
|
312
|
-
sendDesktopNotification,
|
|
313
|
-
notifyTaskComplete,
|
|
314
|
-
notifyPermissionRequest,
|
|
315
|
-
notifyQuestion,
|
|
316
|
-
notifyError,
|
|
317
|
-
checkNotificationSupport,
|
|
318
|
-
getPlatform
|
|
319
|
-
};
|
|
1
|
+
import notifier from 'node-notifier';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Desktop Notification Module for OpenCode Smart Voice Notify
|
|
8
|
+
*
|
|
9
|
+
* Provides cross-platform native desktop notifications using node-notifier.
|
|
10
|
+
* Supports Windows Toast, macOS Notification Center, and Linux notify-send.
|
|
11
|
+
*
|
|
12
|
+
* Platform-specific behaviors:
|
|
13
|
+
* - Windows: Uses SnoreToast for Windows 8+ toast notifications
|
|
14
|
+
* - macOS: Uses terminal-notifier for Notification Center
|
|
15
|
+
* - Linux: Uses notify-send (requires libnotify-bin package)
|
|
16
|
+
*
|
|
17
|
+
* @module util/desktop-notify
|
|
18
|
+
* @see docs/ARCHITECT_PLAN.md - Phase 1, Task 1.2
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Debug logging to file.
|
|
23
|
+
* Only logs when config.debugLog is enabled.
|
|
24
|
+
* Writes to ~/.config/opencode/logs/smart-voice-notify-debug.log
|
|
25
|
+
*
|
|
26
|
+
* @param {string} message - Message to log
|
|
27
|
+
* @param {boolean} enabled - Whether debug logging is enabled
|
|
28
|
+
*/
|
|
29
|
+
const debugLog = (message, enabled = false) => {
|
|
30
|
+
if (!enabled) return;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
|
|
34
|
+
const logsDir = path.join(configDir, 'logs');
|
|
35
|
+
|
|
36
|
+
if (!fs.existsSync(logsDir)) {
|
|
37
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const logFile = path.join(logsDir, 'smart-voice-notify-debug.log');
|
|
41
|
+
const timestamp = new Date().toISOString();
|
|
42
|
+
fs.appendFileSync(logFile, `[${timestamp}] [desktop-notify] ${message}\n`);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// Silently fail - logging should never break the plugin
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the current platform identifier.
|
|
50
|
+
* @returns {'darwin' | 'win32' | 'linux'} Platform string
|
|
51
|
+
*/
|
|
52
|
+
export const getPlatform = () => os.platform();
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if desktop notifications are likely to work on this platform.
|
|
56
|
+
*
|
|
57
|
+
* @returns {{ supported: boolean, reason?: string }} Support status and reason if not supported
|
|
58
|
+
*/
|
|
59
|
+
export const checkNotificationSupport = () => {
|
|
60
|
+
const platform = getPlatform();
|
|
61
|
+
|
|
62
|
+
switch (platform) {
|
|
63
|
+
case 'darwin':
|
|
64
|
+
// macOS always supports notifications via terminal-notifier (bundled)
|
|
65
|
+
return { supported: true };
|
|
66
|
+
|
|
67
|
+
case 'win32':
|
|
68
|
+
// Windows 8+ supports toast notifications via SnoreToast (bundled)
|
|
69
|
+
return { supported: true };
|
|
70
|
+
|
|
71
|
+
case 'linux':
|
|
72
|
+
// Linux requires notify-send from libnotify-bin package
|
|
73
|
+
// We don't check for its existence here - node-notifier handles the fallback
|
|
74
|
+
return { supported: true };
|
|
75
|
+
|
|
76
|
+
default:
|
|
77
|
+
return { supported: false, reason: `Unsupported platform: ${platform}` };
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build platform-specific notification options.
|
|
83
|
+
* Normalizes options across different platforms while respecting their unique capabilities.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} title - Notification title
|
|
86
|
+
* @param {string} message - Notification body/message
|
|
87
|
+
* @param {object} options - Additional options
|
|
88
|
+
* @param {number} [options.timeout=5] - Notification timeout in seconds
|
|
89
|
+
* @param {boolean} [options.sound=false] - Whether to play a sound (platform-specific)
|
|
90
|
+
* @param {string} [options.icon] - Absolute path to notification icon
|
|
91
|
+
* @param {string} [options.subtitle] - Subtitle (macOS only)
|
|
92
|
+
* @param {string} [options.urgency] - Urgency level: 'low', 'normal', 'critical' (Linux only)
|
|
93
|
+
* @returns {object} Platform-normalized notification options
|
|
94
|
+
*/
|
|
95
|
+
const buildPlatformOptions = (title, message, options = {}) => {
|
|
96
|
+
const platform = getPlatform();
|
|
97
|
+
const { timeout = 5, sound = false, icon, subtitle, urgency } = options;
|
|
98
|
+
|
|
99
|
+
// Base options common to all platforms
|
|
100
|
+
const baseOptions = {
|
|
101
|
+
title: title || 'OpenCode',
|
|
102
|
+
message: message || '',
|
|
103
|
+
sound: sound,
|
|
104
|
+
wait: false // Don't block - fire and forget
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Add icon if provided and exists
|
|
108
|
+
if (icon && fs.existsSync(icon)) {
|
|
109
|
+
baseOptions.icon = icon;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Platform-specific options
|
|
113
|
+
switch (platform) {
|
|
114
|
+
case 'darwin':
|
|
115
|
+
// macOS Notification Center options
|
|
116
|
+
return {
|
|
117
|
+
...baseOptions,
|
|
118
|
+
timeout: timeout,
|
|
119
|
+
subtitle: subtitle || undefined
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
case 'win32':
|
|
123
|
+
// Windows Toast options
|
|
124
|
+
return {
|
|
125
|
+
...baseOptions,
|
|
126
|
+
// Windows doesn't use timeout the same way - notifications persist until dismissed
|
|
127
|
+
// sound can be true/false or a system sound name
|
|
128
|
+
sound: sound
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
case 'linux':
|
|
132
|
+
// Linux notify-send options
|
|
133
|
+
return {
|
|
134
|
+
...baseOptions,
|
|
135
|
+
timeout: timeout, // Timeout in seconds
|
|
136
|
+
urgency: urgency || 'normal', // low, normal, critical
|
|
137
|
+
'app-name': 'OpenCode Smart Notify'
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
return baseOptions;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Send a native desktop notification.
|
|
147
|
+
*
|
|
148
|
+
* This is the main function for sending cross-platform desktop notifications.
|
|
149
|
+
* It handles platform-specific options and gracefully fails if notifications
|
|
150
|
+
* are not supported or the notifier encounters an error.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} title - Notification title
|
|
153
|
+
* @param {string} message - Notification body/message
|
|
154
|
+
* @param {object} [options={}] - Notification options
|
|
155
|
+
* @param {number} [options.timeout=5] - Notification timeout in seconds
|
|
156
|
+
* @param {boolean} [options.sound=false] - Whether to play a sound
|
|
157
|
+
* @param {string} [options.icon] - Absolute path to notification icon
|
|
158
|
+
* @param {string} [options.subtitle] - Subtitle (macOS only)
|
|
159
|
+
* @param {string} [options.urgency='normal'] - Urgency level (Linux only)
|
|
160
|
+
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
161
|
+
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* // Simple notification
|
|
165
|
+
* await sendDesktopNotification('Task Complete', 'Your code is ready for review');
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* // With options
|
|
169
|
+
* await sendDesktopNotification('Permission Required', 'Agent needs approval', {
|
|
170
|
+
* timeout: 10,
|
|
171
|
+
* urgency: 'critical',
|
|
172
|
+
* sound: true
|
|
173
|
+
* });
|
|
174
|
+
*/
|
|
175
|
+
export const sendDesktopNotification = async (title, message, options = {}) => {
|
|
176
|
+
// Handle null/undefined options gracefully
|
|
177
|
+
const opts = options || {};
|
|
178
|
+
const debug = opts.debugLog || false;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Check platform support
|
|
182
|
+
const support = checkNotificationSupport();
|
|
183
|
+
if (!support.supported) {
|
|
184
|
+
debugLog(`Notification not supported: ${support.reason}`, debug);
|
|
185
|
+
return { success: false, error: support.reason };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Build platform-specific options
|
|
189
|
+
const notifyOptions = buildPlatformOptions(title, message, opts);
|
|
190
|
+
|
|
191
|
+
debugLog(`Sending notification: "${title}" - "${message}" (platform: ${getPlatform()})`, debug);
|
|
192
|
+
|
|
193
|
+
// Send notification using promise wrapper
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
notifier.notify(notifyOptions, (error, response) => {
|
|
196
|
+
if (error) {
|
|
197
|
+
debugLog(`Notification error: ${error.message}`, debug);
|
|
198
|
+
resolve({ success: false, error: error.message });
|
|
199
|
+
} else {
|
|
200
|
+
debugLog(`Notification sent successfully (response: ${response})`, debug);
|
|
201
|
+
resolve({ success: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
} catch (error) {
|
|
206
|
+
debugLog(`Notification exception: ${error.message}`, debug);
|
|
207
|
+
return { success: false, error: error.message };
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Send a notification for session idle (task completion).
|
|
213
|
+
* Pre-configured for task completion notifications.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} message - Notification message
|
|
216
|
+
* @param {object} [options={}] - Additional options
|
|
217
|
+
* @param {string} [options.projectName] - Project name to include in title
|
|
218
|
+
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
219
|
+
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
220
|
+
*/
|
|
221
|
+
export const notifyTaskComplete = async (message, options = {}) => {
|
|
222
|
+
const title = options.projectName
|
|
223
|
+
? `✅ ${options.projectName} - Task Complete`
|
|
224
|
+
: '✅ OpenCode - Task Complete';
|
|
225
|
+
|
|
226
|
+
return sendDesktopNotification(title, message, {
|
|
227
|
+
timeout: 5,
|
|
228
|
+
sound: false, // We handle sound separately in the main plugin
|
|
229
|
+
...options
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Send a notification for permission requests.
|
|
235
|
+
* Pre-configured for permission request notifications (more urgent).
|
|
236
|
+
*
|
|
237
|
+
* @param {string} message - Notification message
|
|
238
|
+
* @param {object} [options={}] - Additional options
|
|
239
|
+
* @param {string} [options.projectName] - Project name to include in title
|
|
240
|
+
* @param {number} [options.count=1] - Number of permission requests
|
|
241
|
+
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
242
|
+
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
243
|
+
*/
|
|
244
|
+
export const notifyPermissionRequest = async (message, options = {}) => {
|
|
245
|
+
const count = options.count || 1;
|
|
246
|
+
const title = options.projectName
|
|
247
|
+
? `⚠️ ${options.projectName} - Permission Required`
|
|
248
|
+
: count > 1
|
|
249
|
+
? `⚠️ ${count} Permissions Required`
|
|
250
|
+
: '⚠️ OpenCode - Permission Required';
|
|
251
|
+
|
|
252
|
+
return sendDesktopNotification(title, message, {
|
|
253
|
+
timeout: 10, // Longer timeout for permissions
|
|
254
|
+
urgency: 'critical', // Higher urgency on Linux
|
|
255
|
+
sound: false, // We handle sound separately
|
|
256
|
+
...options
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Send a notification for question requests (SDK v1.1.7+).
|
|
262
|
+
* Pre-configured for question notifications.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} message - Notification message
|
|
265
|
+
* @param {object} [options={}] - Additional options
|
|
266
|
+
* @param {string} [options.projectName] - Project name to include in title
|
|
267
|
+
* @param {number} [options.count=1] - Number of questions
|
|
268
|
+
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
269
|
+
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
270
|
+
*/
|
|
271
|
+
export const notifyQuestion = async (message, options = {}) => {
|
|
272
|
+
const count = options.count || 1;
|
|
273
|
+
const title = options.projectName
|
|
274
|
+
? `❓ ${options.projectName} - Question`
|
|
275
|
+
: count > 1
|
|
276
|
+
? `❓ ${count} Questions Need Your Input`
|
|
277
|
+
: '❓ OpenCode - Question';
|
|
278
|
+
|
|
279
|
+
return sendDesktopNotification(title, message, {
|
|
280
|
+
timeout: 8,
|
|
281
|
+
urgency: 'normal',
|
|
282
|
+
sound: false, // We handle sound separately
|
|
283
|
+
...options
|
|
284
|
+
});
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Send a notification for error events.
|
|
289
|
+
* Pre-configured for error notifications (most urgent).
|
|
290
|
+
*
|
|
291
|
+
* @param {string} message - Notification message
|
|
292
|
+
* @param {object} [options={}] - Additional options
|
|
293
|
+
* @param {string} [options.projectName] - Project name to include in title
|
|
294
|
+
* @param {boolean} [options.debugLog=false] - Enable debug logging
|
|
295
|
+
* @returns {Promise<{ success: boolean, error?: string }>} Result object
|
|
296
|
+
*/
|
|
297
|
+
export const notifyError = async (message, options = {}) => {
|
|
298
|
+
const title = options.projectName
|
|
299
|
+
? `❌ ${options.projectName} - Error`
|
|
300
|
+
: '❌ OpenCode - Error';
|
|
301
|
+
|
|
302
|
+
return sendDesktopNotification(title, message, {
|
|
303
|
+
timeout: 15, // Longer timeout for errors
|
|
304
|
+
urgency: 'critical',
|
|
305
|
+
sound: false, // We handle sound separately
|
|
306
|
+
...options
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Default export for convenience
|
|
311
|
+
export default {
|
|
312
|
+
sendDesktopNotification,
|
|
313
|
+
notifyTaskComplete,
|
|
314
|
+
notifyPermissionRequest,
|
|
315
|
+
notifyQuestion,
|
|
316
|
+
notifyError,
|
|
317
|
+
checkNotificationSupport,
|
|
318
|
+
getPlatform
|
|
319
|
+
};
|