opencode-smart-voice-notify 1.0.6 → 1.0.8

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 CHANGED
@@ -1,16 +1,22 @@
1
- # OpenCode Smart Voice Notify
2
-
3
- > **Disclaimer**: This project is not built by the OpenCode team and is not affiliated with [OpenCode](https://opencode.ai) in any way. It is an independent community plugin.
4
-
5
- A smart voice notification plugin for [OpenCode](https://opencode.ai) with **multiple TTS engines** and an intelligent reminder system.
6
-
7
- ## Features
8
-
9
- ### Smart TTS Engine Selection
10
- The plugin automatically tries multiple TTS engines in order, falling back if one fails:
11
-
1
+ <!-- Dynamic Header -->
2
+ <img width="100%" src="https://capsule-render.vercel.app/api?type=waving&color=0:667eea,100:764ba2&height=120&section=header"/>
3
+
4
+ # OpenCode Smart Voice Notify
5
+
6
+ > **Disclaimer**: This project is not built by the OpenCode team and is not affiliated with [OpenCode](https://opencode.ai) in any way. It is an independent community plugin.
7
+
8
+ A smart voice notification plugin for [OpenCode](https://opencode.ai) with **multiple TTS engines** and an intelligent reminder system.
9
+
10
+ <img width="1456" height="720" alt="image" src="https://github.com/user-attachments/assets/52ccf357-2548-400b-a346-6362f2fc3180" />
11
+
12
+
13
+ ## Features
14
+
15
+ ### Smart TTS Engine Selection
16
+ The plugin automatically tries multiple TTS engines in order, falling back if one fails:
17
+
12
18
  1. **ElevenLabs** (Online) - High-quality, anime-like voices with natural expression
13
- 2. **Edge TTS** (Free) - Microsoft's neural voices, no API key required
19
+ 2. **Edge TTS** (Free) - Microsoft's neural voices, native Node.js implementation (no Python required)
14
20
  3. **Windows SAPI** (Offline) - Built-in Windows speech synthesis
15
21
  4. **Local Sound Files** (Fallback) - Plays bundled MP3 files if all TTS fails
16
22
 
@@ -25,185 +31,187 @@ The plugin automatically tries multiple TTS engines in order, falling back if on
25
31
  - Follow-up reminders with exponential backoff
26
32
  - Automatic cancellation when user responds
27
33
  - Per-notification type delays (permission requests are more urgent)
34
+ - **Smart Quota Handling**: Automatically falls back to free Edge TTS if ElevenLabs quota is exceeded
28
35
 
29
36
  ### System Integration
37
+ - **Native Edge TTS**: No external dependencies (Python/pip) required
30
38
  - Wake monitor from sleep before notifying
31
39
  - Auto-boost volume if too low
32
40
  - TUI toast notifications
33
41
  - Cross-platform support (Windows, macOS, Linux)
34
-
35
- ## Installation
36
-
37
- ### Option 1: From npm (Recommended)
38
-
39
- Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
40
-
41
- ```json
42
- {
43
- "$schema": "https://opencode.ai/config.json",
44
- "plugin": ["opencode-smart-voice-notify@latest"]
45
- }
46
- ```
47
-
48
- ### Option 2: From GitHub
49
-
50
- ```json
51
- {
52
- "$schema": "https://opencode.ai/config.json",
53
- "plugin": ["github:MasuRii/opencode-smart-voice-notify"]
54
- }
55
- ```
56
-
57
- ### Option 3: Local Development
58
-
59
- 1. Clone the repository:
60
- ```bash
61
- git clone https://github.com/MasuRii/opencode-smart-voice-notify.git
62
- ```
63
-
64
- 2. Reference the local path in your config:
65
- ```json
66
- {
67
- "plugin": ["file:///path/to/opencode-smart-voice-notify"]
68
- }
69
- ```
70
-
71
- ## Configuration
72
-
73
- ### Automatic Setup
74
-
75
- When you first run OpenCode with this plugin installed, it will **automatically create**:
76
-
77
- 1. **`~/.config/opencode/smart-voice-notify.jsonc`** - A comprehensive configuration file with all available options fully documented.
78
- 2. **`~/.config/opencode/assets/*.mp3`** - Bundled notification sound files.
79
-
80
- The auto-generated configuration includes all advanced settings, message arrays, and engine options, so you don't have to refer back to the documentation for available settings.
81
-
82
- ### Manual Configuration
83
-
84
- If you prefer to create the config manually, add a `smart-voice-notify.jsonc` file in your OpenCode config directory (`~/.config/opencode/`):
85
-
86
- ```jsonc
87
- {
88
- // ============================================================
89
- // NOTIFICATION MODE SETTINGS (Smart Notification System)
90
- // ============================================================
91
- // Controls how notifications are delivered:
92
- // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
93
- // 'tts-first' - Speak TTS immediately, no sound
94
- // 'both' - Play sound AND speak TTS immediately
95
- // 'sound-only' - Only play sound, no TTS at all
96
- "notificationMode": "sound-first",
97
-
98
- // ============================================================
99
- // TTS REMINDER SETTINGS (When user doesn't respond to sound)
100
- // ============================================================
101
-
102
- // Enable TTS reminder if user doesn't respond after sound notification
103
- "enableTTSReminder": true,
104
-
105
- // Delay (in seconds) before TTS reminder fires
106
- "ttsReminderDelaySeconds": 30, // Global default
107
- "idleReminderDelaySeconds": 30, // For task completion notifications
108
- "permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
109
-
110
- // Follow-up reminders if user STILL doesn't respond after first TTS
111
- "enableFollowUpReminders": true,
112
- "maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
113
- "reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
114
-
42
+
43
+ ## Installation
44
+
45
+ ### Option 1: From npm (Recommended)
46
+
47
+ Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
48
+
49
+ ```json
50
+ {
51
+ "$schema": "https://opencode.ai/config.json",
52
+ "plugin": ["opencode-smart-voice-notify@latest"]
53
+ }
54
+ ```
55
+
56
+ ### Option 2: From GitHub
57
+
58
+ ```json
59
+ {
60
+ "$schema": "https://opencode.ai/config.json",
61
+ "plugin": ["github:MasuRii/opencode-smart-voice-notify"]
62
+ }
63
+ ```
64
+
65
+ ### Option 3: Local Development
66
+
67
+ 1. Clone the repository:
68
+ ```bash
69
+ git clone https://github.com/MasuRii/opencode-smart-voice-notify.git
70
+ ```
71
+
72
+ 2. Reference the local path in your config:
73
+ ```json
74
+ {
75
+ "plugin": ["file:///path/to/opencode-smart-voice-notify"]
76
+ }
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ ### Automatic Setup
82
+
83
+ When you first run OpenCode with this plugin installed, it will **automatically create**:
84
+
85
+ 1. **`~/.config/opencode/smart-voice-notify.jsonc`** - A comprehensive configuration file with all available options fully documented.
86
+ 2. **`~/.config/opencode/assets/*.mp3`** - Bundled notification sound files.
87
+
88
+ The auto-generated configuration includes all advanced settings, message arrays, and engine options, so you don't have to refer back to the documentation for available settings.
89
+
90
+ ### Manual Configuration
91
+
92
+ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` file in your OpenCode config directory (`~/.config/opencode/`):
93
+
94
+ ```jsonc
95
+ {
96
+ // ============================================================
97
+ // NOTIFICATION MODE SETTINGS (Smart Notification System)
98
+ // ============================================================
99
+ // Controls how notifications are delivered:
100
+ // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
101
+ // 'tts-first' - Speak TTS immediately, no sound
102
+ // 'both' - Play sound AND speak TTS immediately
103
+ // 'sound-only' - Only play sound, no TTS at all
104
+ "notificationMode": "sound-first",
105
+
106
+ // ============================================================
107
+ // TTS REMINDER SETTINGS (When user doesn't respond to sound)
108
+ // ============================================================
109
+
110
+ // Enable TTS reminder if user doesn't respond after sound notification
111
+ "enableTTSReminder": true,
112
+
113
+ // Delay (in seconds) before TTS reminder fires
114
+ "ttsReminderDelaySeconds": 30, // Global default
115
+ "idleReminderDelaySeconds": 30, // For task completion notifications
116
+ "permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
117
+
118
+ // Follow-up reminders if user STILL doesn't respond after first TTS
119
+ "enableFollowUpReminders": true,
120
+ "maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
121
+ "reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
122
+
115
123
  // ============================================================
116
124
  // TTS ENGINE SELECTION
117
125
  // ============================================================
118
126
  // 'elevenlabs' - Best quality, anime-like voices (requires API key)
119
- // 'edge' - Good quality neural voices (free, requires: pip install edge-tts)
127
+ // 'edge' - Good quality neural voices (Free, Native Node.js implementation)
120
128
  // 'sapi' - Windows built-in voices (free, offline)
121
129
  "ttsEngine": "edge",
122
130
  "enableTTS": true,
123
-
124
- // ============================================================
125
- // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
126
- // ============================================================
127
- // Get your API key from: https://elevenlabs.io/app/settings/api-keys
128
- // "elevenLabsApiKey": "YOUR_API_KEY_HERE",
129
- "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
130
- "elevenLabsModel": "eleven_turbo_v2_5",
131
- "elevenLabsStability": 0.5,
132
- "elevenLabsSimilarity": 0.75,
133
- "elevenLabsStyle": 0.5,
134
-
135
- // ============================================================
136
- // EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
137
- // ============================================================
138
- "edgeVoice": "en-US-AnaNeural",
139
- "edgePitch": "+50Hz",
140
- "edgeRate": "+10%",
141
-
142
- // ============================================================
143
- // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
144
- // ============================================================
145
- "sapiVoice": "Microsoft Zira Desktop",
146
- "sapiRate": -1,
147
- "sapiPitch": "medium",
148
- "sapiVolume": "loud",
149
-
150
- // ============================================================
151
- // INITIAL TTS MESSAGES (Used immediately or after sound)
152
- // ============================================================
153
- "idleTTSMessages": [
154
- "All done! Your task has been completed successfully.",
155
- "Hey there! I finished working on your request.",
156
- "Task complete! Ready for your review whenever you are.",
157
- "Good news! Everything is done and ready for you.",
158
- "Finished! Let me know if you need anything else."
159
- ],
160
- "permissionTTSMessages": [
161
- "Attention please! I need your permission to continue.",
162
- "Hey! Quick approval needed to proceed with the task.",
163
- "Heads up! There is a permission request waiting for you.",
164
- "Excuse me! I need your authorization before I can continue.",
165
- "Permission required! Please review and approve when ready."
166
- ],
167
-
168
- // ============================================================
169
- // TTS REMINDER MESSAGES (Used after delay if no response)
170
- // ============================================================
171
- "idleReminderTTSMessages": [
172
- "Hey, are you still there? Your task has been waiting for review.",
173
- "Just a gentle reminder - I finished your request a while ago!",
174
- "Hello? I completed your task. Please take a look when you can.",
175
- "Still waiting for you! The work is done and ready for review.",
176
- "Knock knock! Your completed task is patiently waiting for you."
177
- ],
178
- "permissionReminderTTSMessages": [
179
- "Hey! I still need your permission to continue. Please respond!",
180
- "Reminder: There is a pending permission request. I cannot proceed without you.",
181
- "Hello? I am waiting for your approval. This is getting urgent!",
182
- "Please check your screen! I really need your permission to move forward.",
183
- "Still waiting for authorization! The task is on hold until you respond."
184
- ],
185
-
186
- // ============================================================
187
- // SOUND FILES (relative to OpenCode config directory)
188
- // ============================================================
189
- "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
190
- "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
191
-
192
- // ============================================================
193
- // GENERAL SETTINGS
194
- // ============================================================
195
- "wakeMonitor": true,
196
- "forceVolume": true,
197
- "volumeThreshold": 50,
198
- "enableToast": true,
199
- "enableSound": true,
200
- "idleThresholdSeconds": 60,
201
- "debugLog": false
202
- }
203
- ```
204
-
205
- See `example.config.jsonc` for more details.
206
-
131
+
132
+ // ============================================================
133
+ // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
134
+ // ============================================================
135
+ // Get your API key from: https://elevenlabs.io/app/settings/api-keys
136
+ // "elevenLabsApiKey": "YOUR_API_KEY_HERE",
137
+ "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
138
+ "elevenLabsModel": "eleven_turbo_v2_5",
139
+ "elevenLabsStability": 0.5,
140
+ "elevenLabsSimilarity": 0.75,
141
+ "elevenLabsStyle": 0.5,
142
+
143
+ // ============================================================
144
+ // EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
145
+ // ============================================================
146
+ "edgeVoice": "en-US-AnaNeural",
147
+ "edgePitch": "+50Hz",
148
+ "edgeRate": "+10%",
149
+
150
+ // ============================================================
151
+ // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
152
+ // ============================================================
153
+ "sapiVoice": "Microsoft Zira Desktop",
154
+ "sapiRate": -1,
155
+ "sapiPitch": "medium",
156
+ "sapiVolume": "loud",
157
+
158
+ // ============================================================
159
+ // INITIAL TTS MESSAGES (Used immediately or after sound)
160
+ // ============================================================
161
+ "idleTTSMessages": [
162
+ "All done! Your task has been completed successfully.",
163
+ "Hey there! I finished working on your request.",
164
+ "Task complete! Ready for your review whenever you are.",
165
+ "Good news! Everything is done and ready for you.",
166
+ "Finished! Let me know if you need anything else."
167
+ ],
168
+ "permissionTTSMessages": [
169
+ "Attention please! I need your permission to continue.",
170
+ "Hey! Quick approval needed to proceed with the task.",
171
+ "Heads up! There is a permission request waiting for you.",
172
+ "Excuse me! I need your authorization before I can continue.",
173
+ "Permission required! Please review and approve when ready."
174
+ ],
175
+
176
+ // ============================================================
177
+ // TTS REMINDER MESSAGES (Used after delay if no response)
178
+ // ============================================================
179
+ "idleReminderTTSMessages": [
180
+ "Hey, are you still there? Your task has been waiting for review.",
181
+ "Just a gentle reminder - I finished your request a while ago!",
182
+ "Hello? I completed your task. Please take a look when you can.",
183
+ "Still waiting for you! The work is done and ready for review.",
184
+ "Knock knock! Your completed task is patiently waiting for you."
185
+ ],
186
+ "permissionReminderTTSMessages": [
187
+ "Hey! I still need your permission to continue. Please respond!",
188
+ "Reminder: There is a pending permission request. I cannot proceed without you.",
189
+ "Hello? I am waiting for your approval. This is getting urgent!",
190
+ "Please check your screen! I really need your permission to move forward.",
191
+ "Still waiting for authorization! The task is on hold until you respond."
192
+ ],
193
+
194
+ // ============================================================
195
+ // SOUND FILES (relative to OpenCode config directory)
196
+ // ============================================================
197
+ "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
198
+ "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
199
+
200
+ // ============================================================
201
+ // GENERAL SETTINGS
202
+ // ============================================================
203
+ "wakeMonitor": true,
204
+ "forceVolume": true,
205
+ "volumeThreshold": 50,
206
+ "enableToast": true,
207
+ "enableSound": true,
208
+ "idleThresholdSeconds": 60,
209
+ "debugLog": false
210
+ }
211
+ ```
212
+
213
+ See `example.config.jsonc` for more details.
214
+
207
215
  ## Requirements
208
216
 
209
217
  ### For ElevenLabs TTS
@@ -211,64 +219,64 @@ See `example.config.jsonc` for more details.
211
219
  - Internet connection
212
220
 
213
221
  ### For Edge TTS
214
- - Python with `edge-tts` package:
215
- ```bash
216
- pip install edge-tts
217
- ```
222
+ - Internet connection (No external dependencies required)
218
223
 
219
224
  ### For Windows SAPI
220
225
  - Windows OS (uses built-in System.Speech)
221
226
 
222
227
  ### For Sound Playback
223
- - **Windows**: Built-in (uses Windows Media Player)
224
- - **macOS**: Built-in (`afplay`)
225
- - **Linux**: `paplay` or `aplay`
226
-
227
- ## Events Handled
228
-
229
- | Event | Action |
230
- |-------|--------|
231
- | `session.idle` | Agent finished working - notify user |
232
- | `permission.updated` | Permission request - alert user |
233
- | `permission.replied` | User responded - cancel pending reminders |
234
- | `message.updated` | New user message - cancel pending reminders |
235
- | `session.created` | New session - reset state |
236
-
237
- ## Development
238
-
239
- To develop on this plugin locally:
240
-
241
- 1. Clone the repository:
242
- ```bash
243
- git clone https://github.com/MasuRii/opencode-smart-voice-notify.git
244
- cd opencode-smart-voice-notify
245
- bun install # or npm install
246
- ```
247
-
248
- 2. Link to your OpenCode config:
249
- ```json
250
- {
251
- "plugin": ["file:///absolute/path/to/opencode-smart-voice-notify"]
252
- }
253
- ```
254
-
255
- ## Updating
256
-
257
- OpenCode does not automatically update plugins. To update to the latest version:
258
-
259
- ```bash
260
- # Clear the cached plugin
261
- rm -rf ~/.cache/opencode/node_modules/opencode-smart-voice-notify
262
-
263
- # Run OpenCode to trigger a fresh install
264
- opencode
265
- ```
266
-
267
- ## License
268
-
269
- MIT
270
-
271
- ## Support
272
-
273
- - Open an issue on [GitHub](https://github.com/MasuRii/opencode-smart-voice-notify/issues)
274
- - Check the [OpenCode docs](https://opencode.ai/docs/plugins)
228
+ - **Windows**: Built-in (uses Windows Media Player)
229
+ - **macOS**: Built-in (`afplay`)
230
+ - **Linux**: `paplay` or `aplay`
231
+
232
+ ## Events Handled
233
+
234
+ | Event | Action |
235
+ |-------|--------|
236
+ | `session.idle` | Agent finished working - notify user |
237
+ | `permission.updated` | Permission request - alert user |
238
+ | `permission.replied` | User responded - cancel pending reminders |
239
+ | `message.updated` | New user message - cancel pending reminders |
240
+ | `session.created` | New session - reset state |
241
+
242
+ ## Development
243
+
244
+ To develop on this plugin locally:
245
+
246
+ 1. Clone the repository:
247
+ ```bash
248
+ git clone https://github.com/MasuRii/opencode-smart-voice-notify.git
249
+ cd opencode-smart-voice-notify
250
+ bun install # or npm install
251
+ ```
252
+
253
+ 2. Link to your OpenCode config:
254
+ ```json
255
+ {
256
+ "plugin": ["file:///absolute/path/to/opencode-smart-voice-notify"]
257
+ }
258
+ ```
259
+
260
+ ## Updating
261
+
262
+ OpenCode does not automatically update plugins. To update to the latest version:
263
+
264
+ ```bash
265
+ # Clear the cached plugin
266
+ rm -rf ~/.cache/opencode/node_modules/opencode-smart-voice-notify
267
+
268
+ # Run OpenCode to trigger a fresh install
269
+ opencode
270
+ ```
271
+
272
+ ## License
273
+
274
+ MIT
275
+
276
+ ## Support
277
+
278
+ - Open an issue on [GitHub](https://github.com/MasuRii/opencode-smart-voice-notify/issues)
279
+ - Check the [OpenCode docs](https://opencode.ai/docs/plugins)
280
+
281
+ <!-- Dynamic Header -->
282
+ <img width="100%" src="https://capsule-render.vercel.app/api?type=waving&color=0:667eea,100:764ba2&height=120&section=header"/>
@@ -48,7 +48,7 @@
48
48
  // TTS ENGINE SELECTION
49
49
  // ============================================================
50
50
  // 'elevenlabs' - Best quality, anime-like voices (requires API key, free tier: 10k chars/month)
51
- // 'edge' - Good quality neural voices (free, requires: pip install edge-tts)
51
+ // 'edge' - Good quality neural voices (Free, Native Node.js implementation)
52
52
  // 'sapi' - Windows built-in voices (free, offline, robotic)
53
53
  "ttsEngine": "elevenlabs",
54
54
 
@@ -81,7 +81,7 @@
81
81
  // ============================================================
82
82
  // EDGE TTS SETTINGS (Free Neural Voices - Fallback)
83
83
  // ============================================================
84
- // Requires: pip install edge-tts
84
+ // Native Node.js implementation (No external dependencies)
85
85
 
86
86
  // Voice options (run 'edge-tts --list-voices' to see all):
87
87
  // 'en-US-AnaNeural' - Young, cute, cartoon-like (RECOMMENDED)
package/index.js CHANGED
@@ -198,6 +198,12 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
198
198
  fallbackSound: options.fallbackSound
199
199
  });
200
200
 
201
+ // CRITICAL FIX: Check if cancelled during playback (user responded while TTS was speaking)
202
+ if (!pendingReminders.has(type)) {
203
+ debugLog(`scheduleTTSReminder: ${type} cancelled during playback - aborting follow-up`);
204
+ return;
205
+ }
206
+
201
207
  // Clean up
202
208
  pendingReminders.delete(type);
203
209
 
@@ -270,6 +276,18 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
270
276
  await playSound(soundFile, soundLoops);
271
277
  }
272
278
 
279
+ // CRITICAL FIX: Check if user responded during sound playback
280
+ // For idle notifications: check if there was new activity after the idle start
281
+ if (type === 'idle' && lastUserActivityTime > lastSessionIdleTime) {
282
+ debugLog(`smartNotify: user active during sound - aborting idle reminder`);
283
+ return;
284
+ }
285
+ // For permission notifications: check if the permission was already handled
286
+ if (type === 'permission' && !activePermissionId) {
287
+ debugLog(`smartNotify: permission handled during sound - aborting reminder`);
288
+ return;
289
+ }
290
+
273
291
  // Step 2: Schedule TTS reminder if user doesn't respond
274
292
  if (config.enableTTSReminder && ttsMessage) {
275
293
  scheduleTTSReminder(type, ttsMessage, { fallbackSound });
@@ -347,9 +365,12 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
347
365
  // CRITICAL: Clear activePermissionId FIRST to prevent race condition
348
366
  // where permission.updated handler is still running async operations
349
367
  const repliedPermissionId = event.properties?.permissionID;
350
- if (activePermissionId === repliedPermissionId) {
368
+
369
+ // Match if IDs are equal, or if we have an active permission with unknown ID (undefined)
370
+ // (This happens if permission.updated received an event without permissionID)
371
+ if (activePermissionId === repliedPermissionId || activePermissionId === undefined) {
351
372
  activePermissionId = null;
352
- debugLog(`Permission replied: cleared activePermissionId ${repliedPermissionId}`);
373
+ debugLog(`Permission replied: cleared activePermissionId ${repliedPermissionId || '(unknown)'}`);
353
374
  }
354
375
  lastUserActivityTime = Date.now();
355
376
  cancelPendingReminder('permission'); // Cancel permission-specific reminder
@@ -402,7 +423,13 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
402
423
  if (event.type === "permission.updated") {
403
424
  // CRITICAL: Capture permissionID IMMEDIATELY (before any async work)
404
425
  // This prevents race condition where user responds before we finish notifying
405
- const permissionId = event.properties?.permissionID;
426
+ // NOTE: In permission.updated, the property is 'id', but in permission.replied it is 'permissionID'
427
+ const permissionId = event.properties?.id;
428
+
429
+ if (!permissionId) {
430
+ debugLog('permission.updated: permission ID missing. properties keys: ' + Object.keys(event.properties || {}).join(', '));
431
+ }
432
+
406
433
  activePermissionId = permissionId;
407
434
 
408
435
  debugLog(`permission.updated: notifying (permissionId=${permissionId})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-smart-voice-notify",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Smart voice notification plugin for OpenCode with multiple TTS engines (ElevenLabs, Edge TTS, Windows SAPI) and intelligent reminder system",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -38,7 +38,8 @@
38
38
  "node": ">=18.0.0"
39
39
  },
40
40
  "dependencies": {
41
- "@elevenlabs/elevenlabs-js": "^2.28.0"
41
+ "@elevenlabs/elevenlabs-js": "^2.28.0",
42
+ "msedge-tts": "^2.0.3"
42
43
  },
43
44
  "peerDependencies": {
44
45
  "@opencode-ai/plugin": "^1.0.0"
package/util/config.js CHANGED
@@ -3,239 +3,254 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import { fileURLToPath } from 'url';
5
5
 
6
- /**
7
- * Basic JSONC parser that strips single-line and multi-line comments.
8
- * @param {string} jsonc
9
- * @returns {any}
10
- */
11
- const parseJSONC = (jsonc) => {
12
- const stripped = jsonc.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m);
13
- return JSON.parse(stripped);
14
- };
15
-
16
- /**
17
- * Get the directory where this plugin is installed.
18
- * Used to find bundled assets like example.config.jsonc
19
- */
20
- const getPluginDir = () => {
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = path.dirname(__filename);
23
- return path.dirname(__dirname); // Go up from util/ to plugin root
24
- };
25
-
26
- /**
27
- * Generate a comprehensive default configuration file content.
28
- * This provides users with ALL available options fully documented.
29
- */
30
- const generateDefaultConfig = () => {
31
- return `{
32
- // ============================================================
33
- // OpenCode Smart Voice Notify - Configuration
34
- // ============================================================
35
- //
36
- // This file was auto-generated with all available options.
37
- // Customize the settings below to your preference.
38
- //
39
- // Sound files have been automatically copied to:
40
- // ~/.config/opencode/assets/
41
- //
42
- // Documentation: https://github.com/MasuRii/opencode-smart-voice-notify
43
- //
44
- // ============================================================
45
-
46
- // ============================================================
47
- // NOTIFICATION MODE SETTINGS (Smart Notification System)
48
- // ============================================================
49
- // Controls how notifications are delivered:
50
- // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
51
- // 'tts-first' - Speak TTS immediately, no sound
52
- // 'both' - Play sound AND speak TTS immediately
53
- // 'sound-only' - Only play sound, no TTS at all
54
- "notificationMode": "sound-first",
55
-
56
- // ============================================================
57
- // TTS REMINDER SETTINGS (When user doesn't respond to sound)
58
- // ============================================================
59
-
60
- // Enable TTS reminder if user doesn't respond after sound notification
61
- "enableTTSReminder": true,
62
-
63
- // Delay (in seconds) before TTS reminder fires
64
- // Set globally or per-notification type
65
- "ttsReminderDelaySeconds": 30, // Global default
66
- "idleReminderDelaySeconds": 30, // For task completion notifications
67
- "permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
68
-
69
- // Follow-up reminders if user STILL doesn't respond after first TTS
70
- "enableFollowUpReminders": true,
71
- "maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
72
- "reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
73
-
74
- // ============================================================
75
- // TTS ENGINE SELECTION
76
- // ============================================================
6
+ /**
7
+ * Basic JSONC parser that strips single-line and multi-line comments.
8
+ * @param {string} jsonc
9
+ * @returns {any}
10
+ */
11
+ const parseJSONC = (jsonc) => {
12
+ const stripped = jsonc.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m);
13
+ return JSON.parse(stripped);
14
+ };
15
+
16
+ /**
17
+ * Helper to format JSON values for the template.
18
+ * @param {any} val
19
+ * @param {number} indent
20
+ * @returns {string}
21
+ */
22
+ const formatJSON = (val, indent = 0) => {
23
+ const json = JSON.stringify(val, null, 4);
24
+ return indent > 0 ? json.replace(/\n/g, '\n' + ' '.repeat(indent)) : json;
25
+ };
26
+
27
+ /**
28
+ * Get the directory where this plugin is installed.
29
+ * Used to find bundled assets like example.config.jsonc
30
+ */
31
+ const getPluginDir = () => {
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = path.dirname(__filename);
34
+ return path.dirname(__dirname); // Go up from util/ to plugin root
35
+ };
36
+
37
+ /**
38
+ * Generate a comprehensive default configuration file content.
39
+ * This provides users with ALL available options fully documented.
40
+ * @param {object} overrides - Existing configuration to preserve
41
+ * @param {string} version - Current version to set in config
42
+ */
43
+ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
44
+ return `{
45
+ // ============================================================
46
+ // OpenCode Smart Voice Notify - Configuration
47
+ // ============================================================
48
+ //
49
+ // This file was auto-generated with all available options.
50
+ // Customize the settings below to your preference.
51
+ //
52
+ // Sound files have been automatically copied to:
53
+ // ~/.config/opencode/assets/
54
+ //
55
+ // Documentation: https://github.com/MasuRii/opencode-smart-voice-notify
56
+ //
57
+ // ============================================================
58
+
59
+ // Internal version tracking - DO NOT REMOVE
60
+ "_configVersion": "${version}",
61
+
62
+ // ============================================================
63
+ // NOTIFICATION MODE SETTINGS (Smart Notification System)
64
+ // ============================================================
65
+ // Controls how notifications are delivered:
66
+ // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
67
+ // 'tts-first' - Speak TTS immediately, no sound
68
+ // 'both' - Play sound AND speak TTS immediately
69
+ // 'sound-only' - Only play sound, no TTS at all
70
+ "notificationMode": "${overrides.notificationMode || 'sound-first'}",
71
+
72
+ // ============================================================
73
+ // TTS REMINDER SETTINGS (When user doesn't respond to sound)
74
+ // ============================================================
75
+
76
+ // Enable TTS reminder if user doesn't respond after sound notification
77
+ "enableTTSReminder": ${overrides.enableTTSReminder !== undefined ? overrides.enableTTSReminder : true},
78
+
79
+ // Delay (in seconds) before TTS reminder fires
80
+ // Set globally or per-notification type
81
+ "ttsReminderDelaySeconds": ${overrides.ttsReminderDelaySeconds !== undefined ? overrides.ttsReminderDelaySeconds : 30}, // Global default
82
+ "idleReminderDelaySeconds": ${overrides.idleReminderDelaySeconds !== undefined ? overrides.idleReminderDelaySeconds : 30}, // For task completion notifications
83
+ "permissionReminderDelaySeconds": ${overrides.permissionReminderDelaySeconds !== undefined ? overrides.permissionReminderDelaySeconds : 20}, // For permission requests (more urgent)
84
+
85
+ // Follow-up reminders if user STILL doesn't respond after first TTS
86
+ "enableFollowUpReminders": ${overrides.enableFollowUpReminders !== undefined ? overrides.enableFollowUpReminders : true},
87
+ "maxFollowUpReminders": ${overrides.maxFollowUpReminders !== undefined ? overrides.maxFollowUpReminders : 3}, // Max number of follow-up TTS reminders
88
+ "reminderBackoffMultiplier": ${overrides.reminderBackoffMultiplier !== undefined ? overrides.reminderBackoffMultiplier : 1.5}, // Each follow-up waits longer (30s, 45s, 67s...)
89
+
90
+ // ============================================================
91
+ // TTS ENGINE SELECTION
92
+ // ============================================================
77
93
  // 'elevenlabs' - Best quality, anime-like voices (requires API key, free tier: 10k chars/month)
78
94
  // 'edge' - Good quality neural voices (free, requires: pip install edge-tts)
79
95
  // 'sapi' - Windows built-in voices (free, offline, robotic)
80
- "ttsEngine": "elevenlabs",
96
+ "ttsEngine": "${overrides.ttsEngine || 'elevenlabs'}",
81
97
 
82
98
  // Enable TTS for notifications (falls back to sound files if TTS fails)
83
- "enableTTS": true,
84
-
85
- // ============================================================
86
- // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
87
- // ============================================================
88
- // Get your API key from: https://elevenlabs.io/app/settings/api-keys
89
- // Free tier: 10,000 characters/month
90
- //
91
- // To use ElevenLabs:
92
- // 1. Uncomment elevenLabsApiKey and add your key
93
- // 2. Change ttsEngine above to "elevenlabs"
94
- //
95
- // "elevenLabsApiKey": "YOUR_API_KEY_HERE",
96
-
97
- // Voice ID - Recommended cute/anime-like voices:
98
- // 'cgSgspJ2msm6clMCkdW9' - Jessica (Playful, Bright, Warm) - RECOMMENDED
99
- // 'FGY2WhTYpPnrIDTdsKH5' - Laura (Enthusiast, Quirky)
100
- // 'jsCqWAovK2LkecY7zXl4' - Freya (Expressive, Confident)
101
- // 'EXAVITQu4vr4xnSDxMaL' - Sarah (Soft, Warm)
102
- // Browse more at: https://elevenlabs.io/voice-library
103
- "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
104
-
105
- // Model: 'eleven_turbo_v2_5' (fast, good), 'eleven_multilingual_v2' (highest quality)
106
- "elevenLabsModel": "eleven_turbo_v2_5",
107
-
108
- // Voice tuning (0.0 to 1.0)
109
- "elevenLabsStability": 0.5, // Lower = more expressive, Higher = more consistent
110
- "elevenLabsSimilarity": 0.75, // How closely to match the original voice
111
- "elevenLabsStyle": 0.5, // Style exaggeration (higher = more expressive)
112
-
113
- // ============================================================
114
- // EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
115
- // ============================================================
116
- // Requires: pip install edge-tts
117
-
99
+ "enableTTS": ${overrides.enableTTS !== undefined ? overrides.enableTTS : true},
100
+
101
+ // ============================================================
102
+ // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
103
+ // ============================================================
104
+ // Get your API key from: https://elevenlabs.io/app/settings/api-keys
105
+ // Free tier: 10,000 characters/month
106
+ //
107
+ // To use ElevenLabs:
108
+ // 1. Uncomment elevenLabsApiKey and add your key
109
+ // 2. Change ttsEngine above to "elevenlabs"
110
+ //
111
+ ${overrides.elevenLabsApiKey ? `"elevenLabsApiKey": "${overrides.elevenLabsApiKey}",` : `// "elevenLabsApiKey": "YOUR_API_KEY_HERE",`}
112
+
113
+ // Voice ID - Recommended cute/anime-like voices:
114
+ // 'cgSgspJ2msm6clMCkdW9' - Jessica (Playful, Bright, Warm) - RECOMMENDED
115
+ // 'FGY2WhTYpPnrIDTdsKH5' - Laura (Enthusiast, Quirky)
116
+ // 'jsCqWAovK2LkecY7zXl4' - Freya (Expressive, Confident)
117
+ // 'EXAVITQu4vr4xnSDxMaL' - Sarah (Soft, Warm)
118
+ // Browse more at: https://elevenlabs.io/voice-library
119
+ "elevenLabsVoiceId": "${overrides.elevenLabsVoiceId || 'cgSgspJ2msm6clMCkdW9'}",
120
+
121
+ // Model: 'eleven_turbo_v2_5' (fast, good), 'eleven_multilingual_v2' (highest quality)
122
+ "elevenLabsModel": "${overrides.elevenLabsModel || 'eleven_turbo_v2_5'}",
123
+
124
+ // Voice tuning (0.0 to 1.0)
125
+ "elevenLabsStability": ${overrides.elevenLabsStability !== undefined ? overrides.elevenLabsStability : 0.5}, // Lower = more expressive, Higher = more consistent
126
+ "elevenLabsSimilarity": ${overrides.elevenLabsSimilarity !== undefined ? overrides.elevenLabsSimilarity : 0.75}, // How closely to match the original voice
127
+ "elevenLabsStyle": ${overrides.elevenLabsStyle !== undefined ? overrides.elevenLabsStyle : 0.5}, // Style exaggeration (higher = more expressive)
128
+
129
+ // ============================================================
130
+ // EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
131
+ // ============================================================
132
+ // Requires: pip install edge-tts
133
+
118
134
  // Voice options (run 'edge-tts --list-voices' to see all):
119
135
  // 'en-US-AnaNeural' - Young, cute, cartoon-like (RECOMMENDED)
120
136
  // 'en-US-JennyNeural' - Friendly, warm
121
137
  // 'en-US-AriaNeural' - Confident, clear
122
138
  // 'en-GB-SoniaNeural' - British, friendly
123
139
  // 'en-AU-NatashaNeural' - Australian, warm
124
- "edgeVoice": "en-US-JennyNeural",
140
+ "edgeVoice": "${overrides.edgeVoice || 'en-US-JennyNeural'}",
125
141
 
126
142
  // Pitch adjustment: +0Hz to +100Hz (higher = more anime-like)
127
- "edgePitch": "+0Hz",
128
-
129
- // Speech rate: -50% to +100%
130
- "edgeRate": "+10%",
131
-
132
- // ============================================================
133
- // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
134
- // ============================================================
135
-
136
- // Voice (run PowerShell to list all installed voices):
137
- // Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | % { $_.VoiceInfo.Name }
138
- //
139
- // Common Windows voices:
140
- // 'Microsoft Zira Desktop' - Female, US English
141
- // 'Microsoft David Desktop' - Male, US English
142
- // 'Microsoft Hazel Desktop' - Female, UK English
143
- "sapiVoice": "Microsoft Zira Desktop",
144
-
145
- // Speech rate: -10 (slowest) to +10 (fastest), 0 is normal
146
- "sapiRate": -1,
147
-
148
- // Pitch: 'x-low', 'low', 'medium', 'high', 'x-high'
149
- "sapiPitch": "medium",
150
-
151
- // Volume: 'silent', 'x-soft', 'soft', 'medium', 'loud', 'x-loud'
152
- "sapiVolume": "loud",
153
-
154
- // ============================================================
155
- // INITIAL TTS MESSAGES (Used immediately or after sound)
156
- // These are randomly selected each time for variety
157
- // ============================================================
158
-
159
- // Messages when agent finishes work (task completion)
160
- "idleTTSMessages": [
161
- "All done! Your task has been completed successfully.",
162
- "Hey there! I finished working on your request.",
163
- "Task complete! Ready for your review whenever you are.",
164
- "Good news! Everything is done and ready for you.",
165
- "Finished! Let me know if you need anything else."
166
- ],
167
-
168
- // Messages for permission requests
169
- "permissionTTSMessages": [
170
- "Attention please! I need your permission to continue.",
171
- "Hey! Quick approval needed to proceed with the task.",
172
- "Heads up! There is a permission request waiting for you.",
173
- "Excuse me! I need your authorization before I can continue.",
174
- "Permission required! Please review and approve when ready."
175
- ],
176
-
177
- // ============================================================
178
- // TTS REMINDER MESSAGES (More urgent - used after delay if no response)
179
- // These are more personalized and urgent to get user attention
180
- // ============================================================
181
-
182
- // Reminder messages when agent finished but user hasn't responded
183
- "idleReminderTTSMessages": [
184
- "Hey, are you still there? Your task has been waiting for review.",
185
- "Just a gentle reminder - I finished your request a while ago!",
186
- "Hello? I completed your task. Please take a look when you can.",
187
- "Still waiting for you! The work is done and ready for review.",
188
- "Knock knock! Your completed task is patiently waiting for you."
189
- ],
190
-
191
- // Reminder messages when permission still needed
192
- "permissionReminderTTSMessages": [
193
- "Hey! I still need your permission to continue. Please respond!",
194
- "Reminder: There is a pending permission request. I cannot proceed without you.",
195
- "Hello? I am waiting for your approval. This is getting urgent!",
196
- "Please check your screen! I really need your permission to move forward.",
197
- "Still waiting for authorization! The task is on hold until you respond."
198
- ],
199
-
200
- // ============================================================
201
- // SOUND FILES (For immediate notifications)
202
- // These are played first before TTS reminder kicks in
203
- // ============================================================
204
- // Paths are relative to ~/.config/opencode/ directory
205
- // Sound files are automatically copied here on first run
206
- // You can replace with your own custom MP3/WAV files
207
-
208
- "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
209
- "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
210
-
211
- // ============================================================
212
- // GENERAL SETTINGS
213
- // ============================================================
214
-
215
- // Wake monitor from sleep when notifying (Windows/macOS)
216
- "wakeMonitor": true,
217
-
218
- // Force system volume up if below threshold
219
- "forceVolume": true,
220
-
221
- // Volume threshold (0-100): force volume if current level is below this
222
- "volumeThreshold": 50,
223
-
224
- // Show TUI toast notifications in OpenCode terminal
225
- "enableToast": true,
226
-
227
- // Enable audio notifications (sound files and TTS)
228
- "enableSound": true,
229
-
230
- // Consider monitor asleep after this many seconds of inactivity (Windows only)
231
- "idleThresholdSeconds": 60,
232
-
233
- // Enable debug logging to ~/.config/opencode/smart-voice-notify-debug.log
234
- // Useful for troubleshooting notification issues
235
- "debugLog": false
236
- }
237
- `;
238
- };
143
+ "edgePitch": "${overrides.edgePitch || '+0Hz'}",
144
+
145
+ // Speech rate: -50% to +100%
146
+ "edgeRate": "${overrides.edgeRate || '+10%'}",
147
+
148
+ // ============================================================
149
+ // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
150
+ // ============================================================
151
+
152
+ // Voice (run PowerShell to list all installed voices):
153
+ // Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | % { $_.VoiceInfo.Name }
154
+ //
155
+ // Common Windows voices:
156
+ // 'Microsoft Zira Desktop' - Female, US English
157
+ // 'Microsoft David Desktop' - Male, US English
158
+ // 'Microsoft Hazel Desktop' - Female, UK English
159
+ "sapiVoice": "${overrides.sapiVoice || 'Microsoft Zira Desktop'}",
160
+
161
+ // Speech rate: -10 (slowest) to +10 (fastest), 0 is normal
162
+ "sapiRate": ${overrides.sapiRate !== undefined ? overrides.sapiRate : -1},
163
+
164
+ // Pitch: 'x-low', 'low', 'medium', 'high', 'x-high'
165
+ "sapiPitch": "${overrides.sapiPitch || 'medium'}",
166
+
167
+ // Volume: 'silent', 'x-soft', 'soft', 'medium', 'loud', 'x-loud'
168
+ "sapiVolume": "${overrides.sapiVolume || 'loud'}",
169
+
170
+ // ============================================================
171
+ // INITIAL TTS MESSAGES (Used immediately or after sound)
172
+ // These are randomly selected each time for variety
173
+ // ============================================================
174
+
175
+ // Messages when agent finishes work (task completion)
176
+ "idleTTSMessages": ${formatJSON(overrides.idleTTSMessages || [
177
+ "All done! Your task has been completed successfully.",
178
+ "Hey there! I finished working on your request.",
179
+ "Task complete! Ready for your review whenever you are.",
180
+ "Good news! Everything is done and ready for you.",
181
+ "Finished! Let me know if you need anything else."
182
+ ], 4)},
183
+
184
+ // Messages for permission requests
185
+ "permissionTTSMessages": ${formatJSON(overrides.permissionTTSMessages || [
186
+ "Attention please! I need your permission to continue.",
187
+ "Hey! Quick approval needed to proceed with the task.",
188
+ "Heads up! There is a permission request waiting for you.",
189
+ "Excuse me! I need your authorization before I can continue.",
190
+ "Permission required! Please review and approve when ready."
191
+ ], 4)},
192
+
193
+ // ============================================================
194
+ // TTS REMINDER MESSAGES (More urgent - used after delay if no response)
195
+ // These are more personalized and urgent to get user attention
196
+ // ============================================================
197
+
198
+ // Reminder messages when agent finished but user hasn't responded
199
+ "idleReminderTTSMessages": ${formatJSON(overrides.idleReminderTTSMessages || [
200
+ "Hey, are you still there? Your task has been waiting for review.",
201
+ "Just a gentle reminder - I finished your request a while ago!",
202
+ "Hello? I completed your task. Please take a look when you can.",
203
+ "Still waiting for you! The work is done and ready for review.",
204
+ "Knock knock! Your completed task is patiently waiting for you."
205
+ ], 4)},
206
+
207
+ // Reminder messages when permission still needed
208
+ "permissionReminderTTSMessages": ${formatJSON(overrides.permissionReminderTTSMessages || [
209
+ "Hey! I still need your permission to continue. Please respond!",
210
+ "Reminder: There is a pending permission request. I cannot proceed without you.",
211
+ "Hello? I am waiting for your approval. This is getting urgent!",
212
+ "Please check your screen! I really need your permission to move forward.",
213
+ "Still waiting for authorization! The task is on hold until you respond."
214
+ ], 4)},
215
+
216
+ // ============================================================
217
+ // SOUND FILES (For immediate notifications)
218
+ // These are played first before TTS reminder kicks in
219
+ // ============================================================
220
+ // Paths are relative to ~/.config/opencode/ directory
221
+ // Sound files are automatically copied here on first run
222
+ // You can replace with your own custom MP3/WAV files
223
+
224
+ "idleSound": "${overrides.idleSound || 'assets/Soft-high-tech-notification-sound-effect.mp3'}",
225
+ "permissionSound": "${overrides.permissionSound || 'assets/Machine-alert-beep-sound-effect.mp3'}",
226
+
227
+ // ============================================================
228
+ // GENERAL SETTINGS
229
+ // ============================================================
230
+
231
+ // Wake monitor from sleep when notifying (Windows/macOS)
232
+ "wakeMonitor": ${overrides.wakeMonitor !== undefined ? overrides.wakeMonitor : true},
233
+
234
+ // Force system volume up if below threshold
235
+ "forceVolume": ${overrides.forceVolume !== undefined ? overrides.forceVolume : true},
236
+
237
+ // Volume threshold (0-100): force volume if current level is below this
238
+ "volumeThreshold": ${overrides.volumeThreshold !== undefined ? overrides.volumeThreshold : 50},
239
+
240
+ // Show TUI toast notifications in OpenCode terminal
241
+ "enableToast": ${overrides.enableToast !== undefined ? overrides.enableToast : true},
242
+
243
+ // Enable audio notifications (sound files and TTS)
244
+ "enableSound": ${overrides.enableSound !== undefined ? overrides.enableSound : true},
245
+
246
+ // Consider monitor asleep after this many seconds of inactivity (Windows only)
247
+ "idleThresholdSeconds": ${overrides.idleThresholdSeconds !== undefined ? overrides.idleThresholdSeconds : 60},
248
+
249
+ // Enable debug logging to ~/.config/opencode/smart-voice-notify-debug.log
250
+ // Useful for troubleshooting notification issues
251
+ "debugLog": ${overrides.debugLog !== undefined ? overrides.debugLog : false}
252
+ }`;
253
+ };
239
254
 
240
255
  /**
241
256
  * Copy bundled assets (sound files) to the OpenCode config directory.
@@ -273,47 +288,62 @@ const copyBundledAssets = (configDir) => {
273
288
  }
274
289
  };
275
290
 
276
- /**
277
- * Loads a configuration file from the OpenCode config directory.
278
- * If the file doesn't exist, creates a default config file.
279
- * @param {string} name - Name of the config file (without .jsonc extension)
280
- * @param {object} defaults - Default values if file doesn't exist or is invalid
281
- * @returns {object}
282
- */
283
- export const loadConfig = (name, defaults = {}) => {
284
- const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
285
- const filePath = path.join(configDir, `${name}.jsonc`);
286
-
287
- if (!fs.existsSync(filePath)) {
288
- // Auto-create the default config file
289
- try {
290
- // Ensure config directory exists
291
- if (!fs.existsSync(configDir)) {
292
- fs.mkdirSync(configDir, { recursive: true });
293
- }
294
-
295
- // Write the default config
296
- const defaultConfig = generateDefaultConfig();
297
- fs.writeFileSync(filePath, defaultConfig, 'utf-8');
298
-
299
- // Also copy bundled assets (sound files) to the config directory
300
- copyBundledAssets(configDir);
301
-
302
- // Parse and return the newly created config merged with defaults
303
- const config = parseJSONC(defaultConfig);
304
- return { ...defaults, ...config };
305
- } catch (error) {
306
- // If we can't create the file, just return defaults
307
- return defaults;
308
- }
309
- }
310
-
311
- try {
312
- const content = fs.readFileSync(filePath, 'utf-8');
313
- const config = parseJSONC(content);
314
- return { ...defaults, ...config };
315
- } catch (error) {
316
- // Silently return defaults - don't use console.error as it breaks TUI
317
- return defaults;
318
- }
319
- };
291
+ /**
292
+ * Loads a configuration file from the OpenCode config directory.
293
+ * If the file doesn't exist, creates a default config file.
294
+ * Performs version checks and migrates config if necessary.
295
+ * @param {string} name - Name of the config file (without .jsonc extension)
296
+ * @param {object} defaults - Default values if file doesn't exist or is invalid
297
+ * @returns {object}
298
+ */
299
+ export const loadConfig = (name, defaults = {}) => {
300
+ const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
301
+ const filePath = path.join(configDir, `${name}.jsonc`);
302
+
303
+ // Get current version from package.json
304
+ const pluginDir = getPluginDir();
305
+ const pkg = JSON.parse(fs.readFileSync(path.join(pluginDir, 'package.json'), 'utf-8'));
306
+ const currentVersion = pkg.version;
307
+
308
+ let existingConfig = null;
309
+ if (fs.existsSync(filePath)) {
310
+ try {
311
+ const content = fs.readFileSync(filePath, 'utf-8');
312
+ existingConfig = parseJSONC(content);
313
+ } catch (error) {
314
+ // If file is invalid JSONC, we'll treat it as missing and overwrite
315
+ }
316
+ }
317
+
318
+ // Version check and migration logic
319
+ if (!existingConfig || existingConfig._configVersion !== currentVersion) {
320
+ try {
321
+ // Ensure config directory exists
322
+ if (!fs.existsSync(configDir)) {
323
+ fs.mkdirSync(configDir, { recursive: true });
324
+ }
325
+
326
+ // Generate new config content using existing values as overrides
327
+ // This preserves user settings while updating comments and adding new fields
328
+ const newConfigContent = generateDefaultConfig(existingConfig || {}, currentVersion);
329
+ fs.writeFileSync(filePath, newConfigContent, 'utf-8');
330
+
331
+ // Also ensure all bundled assets (sound files) are present in the config directory
332
+ copyBundledAssets(configDir);
333
+
334
+ if (existingConfig) {
335
+ console.log(`[Smart Voice Notify] Config migrated to version ${currentVersion}`);
336
+ } else {
337
+ console.log(`[Smart Voice Notify] Initialized default config at ${filePath}`);
338
+ }
339
+
340
+ // Re-parse the newly written config
341
+ existingConfig = parseJSONC(newConfigContent);
342
+ } catch (error) {
343
+ // If migration fails, try to return whatever we have or defaults
344
+ return existingConfig || defaults;
345
+ }
346
+ }
347
+
348
+ return { ...defaults, ...existingConfig };
349
+ };
package/util/tts.js CHANGED
@@ -110,6 +110,8 @@ export const getTTSConfig = () => {
110
110
  });
111
111
  };
112
112
 
113
+ let elevenLabsQuotaExceeded = false;
114
+
113
115
  /**
114
116
  * Creates a TTS utility instance
115
117
  * @param {object} params - { $, client }
@@ -119,6 +121,21 @@ export const createTTS = ({ $, client }) => {
119
121
  const config = getTTSConfig();
120
122
  const logFile = path.join(configDir, 'smart-voice-notify-debug.log');
121
123
 
124
+ const showToast = async (message, variant = 'info') => {
125
+ if (!config.enableToast) return;
126
+ try {
127
+ if (typeof client?.tui?.showToast === 'function') {
128
+ await client.tui.showToast({
129
+ body: {
130
+ message: message,
131
+ variant: variant,
132
+ duration: 6000
133
+ }
134
+ });
135
+ }
136
+ } catch (e) {}
137
+ };
138
+
122
139
  const debugLog = (message) => {
123
140
  if (!config.debugLog) return;
124
141
  try {
@@ -174,6 +191,8 @@ export const createTTS = ({ $, client }) => {
174
191
  * ElevenLabs Engine (Online, High Quality, Anime-like voices)
175
192
  */
176
193
  const speakWithElevenLabs = async (text) => {
194
+ if (elevenLabsQuotaExceeded) return false;
195
+
177
196
  if (!config.elevenLabsApiKey) {
178
197
  debugLog('speakWithElevenLabs: No API key configured');
179
198
  return false;
@@ -204,6 +223,19 @@ export const createTTS = ({ $, client }) => {
204
223
  return true;
205
224
  } catch (e) {
206
225
  debugLog(`speakWithElevenLabs error: ${e.message}`);
226
+
227
+ // Handle quota exceeded (401 specifically, or specific error message)
228
+ const isQuotaError =
229
+ e.statusCode === 401 ||
230
+ e.message?.includes('401') ||
231
+ e.message?.toLowerCase().includes('quota_exceeded') ||
232
+ e.message?.toLowerCase().includes('quota exceeded');
233
+
234
+ if (isQuotaError) {
235
+ elevenLabsQuotaExceeded = true;
236
+ await showToast("⚠️ ElevenLabs quota exceeded! Switching to Edge TTS for this session.", "error");
237
+ }
238
+
207
239
  return false;
208
240
  }
209
241
  };
@@ -212,16 +244,20 @@ export const createTTS = ({ $, client }) => {
212
244
  * Edge TTS Engine (Free, Neural voices)
213
245
  */
214
246
  const speakWithEdgeTTS = async (text) => {
215
- if (!$) return false;
216
247
  try {
217
- const voice = config.edgeVoice || 'en-US-AnaNeural';
248
+ const { MsEdgeTTS, OUTPUT_FORMAT } = await import('msedge-tts');
249
+ const tts = new MsEdgeTTS();
250
+ const voice = config.edgeVoice || 'en-US-JennyNeural';
218
251
  const pitch = config.edgePitch || '+0Hz';
219
- const rate = config.edgeRate || '+0%';
220
- const tempFile = path.join(os.tmpdir(), `opencode-edge-${Date.now()}.mp3`);
252
+ const rate = config.edgeRate || '+10%';
253
+ const volume = config.edgeVolume || '+0%';
221
254
 
222
- await $`edge-tts --voice ${voice} --pitch ${pitch} --rate ${rate} --text ${text} --write-media ${tempFile}`.quiet();
223
- await playAudioFile(tempFile);
224
- try { fs.unlinkSync(tempFile); } catch (e) {}
255
+ await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
256
+
257
+ const { audioFilePath } = await tts.toFile(os.tmpdir(), text, { pitch, rate, volume });
258
+
259
+ await playAudioFile(audioFilePath);
260
+ try { fs.unlinkSync(audioFilePath); } catch (e) {}
225
261
  return true;
226
262
  } catch (e) {
227
263
  debugLog(`speakWithEdgeTTS error: ${e.message}`);