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.
@@ -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
+ };