opencode-smart-voice-notify 1.1.1 → 1.2.0

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
@@ -35,6 +35,13 @@ The plugin automatically tries multiple TTS engines in order, falling back if on
35
35
  - **Permission Batching**: Multiple simultaneous permission requests are batched into a single notification (e.g., "5 permission requests require your attention")
36
36
  - **Question Tool Support** (SDK v1.1.7+): Notifies when the agent asks questions and needs user input
37
37
 
38
+ ### AI-Generated Messages (Experimental)
39
+ - **Dynamic notifications**: Use a local AI to generate unique, contextual messages instead of preset static ones
40
+ - **OpenAI-compatible**: Works with Ollama, LM Studio, LocalAI, vLLM, llama.cpp, Jan.ai, or any OpenAI-compatible endpoint
41
+ - **User-hosted**: You provide your own AI endpoint - no cloud API keys required
42
+ - **Custom prompts**: Configure prompts per notification type for full control over AI personality
43
+ - **Smart fallback**: Automatically falls back to static messages if AI is unavailable
44
+
38
45
  ### System Integration
39
46
  - **Native Edge TTS**: No external dependencies (Python/pip) required
40
47
  - Wake monitor from sleep before notifying
@@ -96,86 +103,153 @@ The auto-generated configuration includes all advanced settings, message arrays,
96
103
 
97
104
  If you prefer to create the config manually, add a `smart-voice-notify.jsonc` file in your OpenCode config directory (`~/.config/opencode/`):
98
105
 
99
- ```jsonc
100
- {
101
- // ============================================================
102
- // NOTIFICATION MODE SETTINGS (Smart Notification System)
103
- // ============================================================
104
- // Controls how notifications are delivered:
105
- // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
106
- // 'tts-first' - Speak TTS immediately, no sound
107
- // 'both' - Play sound AND speak TTS immediately
108
- // 'sound-only' - Only play sound, no TTS at all
109
- "notificationMode": "sound-first",
110
-
111
- // ============================================================
112
- // TTS REMINDER SETTINGS (When user doesn't respond to sound)
113
- // ============================================================
114
-
115
- // Enable TTS reminder if user doesn't respond after sound notification
116
- "enableTTSReminder": true,
117
-
118
- // Delay (in seconds) before TTS reminder fires
119
- "ttsReminderDelaySeconds": 30, // Global default
120
- "idleReminderDelaySeconds": 30, // For task completion notifications
121
- "permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
122
-
106
+ ```jsonc
107
+ {
108
+ // ============================================================
109
+ // OpenCode Smart Voice Notify - Full Configuration Reference
110
+ // ============================================================
111
+ //
112
+ // IMPORTANT: This is a REFERENCE file showing ALL available options.
113
+ //
114
+ // To use this plugin:
115
+ // 1. Copy this file to: ~/.config/opencode/smart-voice-notify.jsonc
116
+ // (On Windows: C:\Users\<YourUser>\.config\opencode\smart-voice-notify.jsonc)
117
+ // 2. Customize the settings below to your preference
118
+ // 3. The plugin auto-creates a minimal config if none exists
119
+ //
120
+ // Sound files are automatically copied to ~/.config/opencode/assets/
121
+ // on first run. You can also use your own custom sound files.
122
+ //
123
+ // ============================================================
124
+
125
+ // ============================================================
126
+ // NOTIFICATION MODE SETTINGS (Smart Notification System)
127
+ // ============================================================
128
+ // Controls how notifications are delivered:
129
+ // 'sound-first' - Play sound immediately, TTS reminder after delay (RECOMMENDED)
130
+ // 'tts-first' - Speak TTS immediately, no sound
131
+ // 'both' - Play sound AND speak TTS immediately
132
+ // 'sound-only' - Only play sound, no TTS at all
133
+ "notificationMode": "sound-first",
134
+
135
+ // ============================================================
136
+ // TTS REMINDER SETTINGS (When user doesn't respond to sound)
137
+ // ============================================================
138
+
139
+ // Enable TTS reminder if user doesn't respond after sound notification
140
+ "enableTTSReminder": true,
141
+
142
+ // Delay (in seconds) before TTS reminder fires
143
+ // Set globally or per-notification type
144
+ "ttsReminderDelaySeconds": 30, // Global default
145
+ "idleReminderDelaySeconds": 30, // For task completion notifications
146
+ "permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
147
+
123
148
  // Follow-up reminders if user STILL doesn't respond after first TTS
124
149
  "enableFollowUpReminders": true,
125
150
  "maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
126
151
  "reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
127
-
152
+
128
153
  // ============================================================
129
154
  // PERMISSION BATCHING (Multiple permissions at once)
130
155
  // ============================================================
131
- // When multiple permissions arrive simultaneously, batch them into one notification
132
- "permissionBatchWindowMs": 800, // Batch window in milliseconds
133
-
156
+ // When multiple permissions arrive simultaneously (e.g., 5 at once),
157
+ // batch them into a single notification instead of playing 5 overlapping sounds.
158
+ // The notification will say "X permission requests require your attention".
159
+
160
+ // Batch window (ms) - how long to wait for more permissions before notifying
161
+ "permissionBatchWindowMs": 800,
162
+
134
163
  // ============================================================
135
164
  // TTS ENGINE SELECTION
136
165
  // ============================================================
137
- // 'elevenlabs' - Best quality, anime-like voices (requires API key)
166
+ // 'elevenlabs' - Best quality, anime-like voices (requires API key, free tier: 10k chars/month)
138
167
  // 'edge' - Good quality neural voices (Free, Native Node.js implementation)
139
- // 'sapi' - Windows built-in voices (free, offline)
140
- "ttsEngine": "edge",
168
+ // 'sapi' - Windows built-in voices (free, offline, robotic)
169
+ "ttsEngine": "elevenlabs",
170
+
171
+ // Enable TTS for notifications (falls back to sound files if TTS fails)
141
172
  "enableTTS": true,
142
-
143
- // ============================================================
144
- // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
145
- // ============================================================
146
- // Get your API key from: https://elevenlabs.io/app/settings/api-keys
147
- // "elevenLabsApiKey": "YOUR_API_KEY_HERE",
148
- "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
149
- "elevenLabsModel": "eleven_turbo_v2_5",
150
- "elevenLabsStability": 0.5,
151
- "elevenLabsSimilarity": 0.75,
152
- "elevenLabsStyle": 0.5,
153
-
154
- // ============================================================
155
- // EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
156
- // ============================================================
157
- "edgeVoice": "en-US-AnaNeural",
158
- "edgePitch": "+50Hz",
159
- "edgeRate": "+10%",
160
-
161
- // ============================================================
162
- // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
163
- // ============================================================
164
- "sapiVoice": "Microsoft Zira Desktop",
165
- "sapiRate": -1,
166
- "sapiPitch": "medium",
167
- "sapiVolume": "loud",
168
-
169
- // ============================================================
170
- // INITIAL TTS MESSAGES (Used immediately or after sound)
171
- // ============================================================
172
- "idleTTSMessages": [
173
- "All done! Your task has been completed successfully.",
174
- "Hey there! I finished working on your request.",
175
- "Task complete! Ready for your review whenever you are.",
176
- "Good news! Everything is done and ready for you.",
177
- "Finished! Let me know if you need anything else."
178
- ],
173
+
174
+ // ============================================================
175
+ // ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
176
+ // ============================================================
177
+ // Get your API key from: https://elevenlabs.io/app/settings/api-keys
178
+ // Free tier: 10,000 characters/month
179
+ "elevenLabsApiKey": "YOUR_API_KEY_HERE",
180
+
181
+ // Voice ID - Recommended cute/anime-like voices:
182
+ // 'cgSgspJ2msm6clMCkdW9' - Jessica (Playful, Bright, Warm) - RECOMMENDED
183
+ // 'FGY2WhTYpPnrIDTdsKH5' - Laura (Enthusiast, Quirky)
184
+ // 'jsCqWAovK2LkecY7zXl4' - Freya (Expressive, Confident)
185
+ // 'EXAVITQu4vr4xnSDxMaL' - Sarah (Soft, Warm)
186
+ // Browse more at: https://elevenlabs.io/voice-library
187
+ "elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
188
+
189
+ // Model: 'eleven_turbo_v2_5' (fast, good), 'eleven_multilingual_v2' (highest quality)
190
+ "elevenLabsModel": "eleven_turbo_v2_5",
191
+
192
+ // Voice tuning (0.0 to 1.0)
193
+ "elevenLabsStability": 0.5, // Lower = more expressive, Higher = more consistent
194
+ "elevenLabsSimilarity": 0.75, // How closely to match the original voice
195
+ "elevenLabsStyle": 0.5, // Style exaggeration (higher = more expressive)
196
+
197
+ // ============================================================
198
+ // EDGE TTS SETTINGS (Free Neural Voices - Fallback)
199
+ // ============================================================
200
+ // Native Node.js implementation (No external dependencies)
201
+
202
+ // Voice options (run 'edge-tts --list-voices' to see all):
203
+ // 'en-US-AnaNeural' - Young, cute, cartoon-like (RECOMMENDED)
204
+ // 'en-US-JennyNeural' - Friendly, warm
205
+ // 'en-US-AriaNeural' - Confident, clear
206
+ // 'en-GB-SoniaNeural' - British, friendly
207
+ // 'en-AU-NatashaNeural' - Australian, warm
208
+ "edgeVoice": "en-US-AnaNeural",
209
+
210
+ // Pitch adjustment: +0Hz to +100Hz (higher = more anime-like)
211
+ "edgePitch": "+50Hz",
212
+
213
+ // Speech rate: -50% to +100%
214
+ "edgeRate": "+10%",
215
+
216
+ // ============================================================
217
+ // SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
218
+ // ============================================================
219
+
220
+ // Voice (run PowerShell to list all installed voices):
221
+ // Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).GetInstalledVoices() | % { $_.VoiceInfo.Name }
222
+ //
223
+ // Common Windows voices:
224
+ // 'Microsoft Zira Desktop' - Female, US English
225
+ // 'Microsoft David Desktop' - Male, US English
226
+ // 'Microsoft Hazel Desktop' - Female, UK English
227
+ "sapiVoice": "Microsoft Zira Desktop",
228
+
229
+ // Speech rate: -10 (slowest) to +10 (fastest), 0 is normal
230
+ "sapiRate": -1,
231
+
232
+ // Pitch: 'x-low', 'low', 'medium', 'high', 'x-high'
233
+ "sapiPitch": "medium",
234
+
235
+ // Volume: 'silent', 'x-soft', 'soft', 'medium', 'loud', 'x-loud'
236
+ "sapiVolume": "loud",
237
+
238
+ // ============================================================
239
+ // INITIAL TTS MESSAGES (Used immediately or after sound)
240
+ // These are randomly selected each time for variety
241
+ // ============================================================
242
+
243
+ // Messages when agent finishes work (task completion)
244
+ "idleTTSMessages": [
245
+ "All done! Your task has been completed successfully.",
246
+ "Hey there! I finished working on your request.",
247
+ "Task complete! Ready for your review whenever you are.",
248
+ "Good news! Everything is done and ready for you.",
249
+ "Finished! Let me know if you need anything else."
250
+ ],
251
+
252
+ // Messages for permission requests
179
253
  "permissionTTSMessages": [
180
254
  "Attention please! I need your permission to continue.",
181
255
  "Hey! Quick approval needed to proceed with the task.",
@@ -183,22 +257,32 @@ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` fi
183
257
  "Excuse me! I need your authorization before I can continue.",
184
258
  "Permission required! Please review and approve when ready."
185
259
  ],
260
+
186
261
  // Messages for MULTIPLE permission requests (use {count} placeholder)
262
+ // Used when several permissions arrive simultaneously
187
263
  "permissionTTSMessagesMultiple": [
188
264
  "Attention please! There are {count} permission requests waiting for your approval.",
189
- "Hey! {count} permissions need your approval to continue."
265
+ "Hey! {count} permissions need your approval to continue.",
266
+ "Heads up! You have {count} pending permission requests.",
267
+ "Excuse me! I need your authorization for {count} different actions.",
268
+ "{count} permissions required! Please review and approve when ready."
190
269
  ],
191
-
192
- // ============================================================
193
- // TTS REMINDER MESSAGES (Used after delay if no response)
194
- // ============================================================
195
- "idleReminderTTSMessages": [
196
- "Hey, are you still there? Your task has been waiting for review.",
197
- "Just a gentle reminder - I finished your request a while ago!",
198
- "Hello? I completed your task. Please take a look when you can.",
199
- "Still waiting for you! The work is done and ready for review.",
200
- "Knock knock! Your completed task is patiently waiting for you."
201
- ],
270
+
271
+ // ============================================================
272
+ // TTS REMINDER MESSAGES (More urgent - used after delay if no response)
273
+ // These are more personalized and urgent to get user attention
274
+ // ============================================================
275
+
276
+ // Reminder messages when agent finished but user hasn't responded
277
+ "idleReminderTTSMessages": [
278
+ "Hey, are you still there? Your task has been waiting for review.",
279
+ "Just a gentle reminder - I finished your request a while ago!",
280
+ "Hello? I completed your task. Please take a look when you can.",
281
+ "Still waiting for you! The work is done and ready for review.",
282
+ "Knock knock! Your completed task is patiently waiting for you."
283
+ ],
284
+
285
+ // Reminder messages when permission still needed
202
286
  "permissionReminderTTSMessages": [
203
287
  "Hey! I still need your permission to continue. Please respond!",
204
288
  "Reminder: There is a pending permission request. I cannot proceed without you.",
@@ -206,33 +290,140 @@ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` fi
206
290
  "Please check your screen! I really need your permission to move forward.",
207
291
  "Still waiting for authorization! The task is on hold until you respond."
208
292
  ],
293
+
209
294
  // Reminder messages for MULTIPLE permissions (use {count} placeholder)
210
295
  "permissionReminderTTSMessagesMultiple": [
211
296
  "Hey! I still need your approval for {count} permissions. Please respond!",
212
- "Reminder: There are {count} pending permission requests."
297
+ "Reminder: There are {count} pending permission requests. I cannot proceed without you.",
298
+ "Hello? I am waiting for your approval on {count} items. This is getting urgent!",
299
+ "Please check your screen! {count} permissions are waiting for your response.",
300
+ "Still waiting for authorization on {count} requests! The task is on hold."
213
301
  ],
214
-
215
- // ============================================================
216
- // SOUND FILES (relative to OpenCode config directory)
217
- // ============================================================
218
- "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
219
- "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
220
-
221
- // ============================================================
222
- // GENERAL SETTINGS
223
- // ============================================================
224
- "wakeMonitor": true,
225
- "forceVolume": true,
226
- "volumeThreshold": 50,
227
- "enableToast": true,
228
- "enableSound": true,
229
- "idleThresholdSeconds": 60,
230
- "debugLog": false
231
- }
232
- ```
233
-
234
- See `example.config.jsonc` for more details.
302
+
303
+ // ============================================================
304
+ // QUESTION TOOL MESSAGES (SDK v1.1.7+ - Agent asking user questions)
305
+ // ============================================================
306
+ // The "question" tool allows the LLM to ask users questions during execution.
307
+ // This is useful for gathering preferences, clarifying instructions, or getting
308
+ // decisions on implementation choices.
309
+
310
+ // Messages when agent asks user a question
311
+ "questionTTSMessages": [
312
+ "Hey! I have a question for you. Please check your screen.",
313
+ "Attention! I need your input to continue.",
314
+ "Quick question! Please take a look when you have a moment.",
315
+ "I need some clarification. Could you please respond?",
316
+ "Question time! Your input is needed to proceed."
317
+ ],
318
+
319
+ // Messages for MULTIPLE questions (use {count} placeholder)
320
+ "questionTTSMessagesMultiple": [
321
+ "Hey! I have {count} questions for you. Please check your screen.",
322
+ "Attention! I need your input on {count} items to continue.",
323
+ "{count} questions need your attention. Please take a look!",
324
+ "I need some clarifications. There are {count} questions waiting for you.",
325
+ "Question time! {count} questions need your response to proceed."
326
+ ],
327
+
328
+ // Reminder messages for questions (more urgent - used after delay)
329
+ "questionReminderTTSMessages": [
330
+ "Hey! I am still waiting for your answer. Please check the questions!",
331
+ "Reminder: There is a question waiting for your response.",
332
+ "Hello? I need your input to continue. Please respond when you can.",
333
+ "Still waiting for your answer! The task is on hold.",
334
+ "Your input is needed! Please check the pending question."
335
+ ],
336
+
337
+ // Reminder messages for MULTIPLE questions (use {count} placeholder)
338
+ "questionReminderTTSMessagesMultiple": [
339
+ "Hey! I am still waiting for answers to {count} questions. Please respond!",
340
+ "Reminder: There are {count} questions waiting for your response.",
341
+ "Hello? I need your input on {count} items. Please respond when you can.",
342
+ "Still waiting for your answers on {count} questions! The task is on hold.",
343
+ "Your input is needed! {count} questions are pending your response."
344
+ ],
345
+
346
+ // Delay (in seconds) before question reminder fires
347
+ "questionReminderDelaySeconds": 25,
348
+
349
+ // Question batch window (ms) - how long to wait for more questions before notifying
350
+ "questionBatchWindowMs": 800,
351
+
352
+ // ============================================================
353
+ // SOUND FILES (For immediate notifications)
354
+ // These are played first before TTS reminder kicks in
355
+ // ============================================================
356
+ // Paths are relative to ~/.config/opencode/ directory
357
+ // The plugin automatically copies bundled sounds to assets/ on first run
358
+ // You can replace with your own custom MP3/WAV files
359
+
360
+ "idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
361
+ "permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
362
+ "questionSound": "assets/Machine-alert-beep-sound-effect.mp3",
363
+
364
+ // ============================================================
365
+ // GENERAL SETTINGS
366
+ // ============================================================
367
+
368
+ // Wake monitor from sleep when notifying (Windows/macOS)
369
+ "wakeMonitor": true,
370
+
371
+ // Force system volume up if below threshold
372
+ "forceVolume": true,
373
+
374
+ // Volume threshold (0-100): force volume if current level is below this
375
+ "volumeThreshold": 50,
376
+
377
+ // Show TUI toast notifications in OpenCode terminal
378
+ "enableToast": true,
379
+
380
+ // Enable audio notifications (sound files and TTS)
381
+ "enableSound": true,
382
+
383
+ // Consider monitor asleep after this many seconds of inactivity (Windows only)
384
+ "idleThresholdSeconds": 60,
385
+
386
+ // Enable debug logging to ~/.config/opencode/logs/smart-voice-notify-debug.log
387
+ // The logs folder is created automatically when debug logging is enabled
388
+ // Useful for troubleshooting notification issues
389
+ "debugLog": false
390
+ }
391
+ ```
235
392
 
393
+ See `example.config.jsonc` for more details.
394
+
395
+ ### AI Message Generation (Optional)
396
+
397
+ If you want dynamic, AI-generated notification messages instead of preset ones, you can connect to a local AI server:
398
+
399
+ 1. **Install a local AI server** (e.g., [Ollama](https://ollama.ai)):
400
+ ```bash
401
+ # Install Ollama and pull a model
402
+ ollama pull llama3
403
+ ```
404
+
405
+ 2. **Enable AI messages in your config**:
406
+ ```jsonc
407
+ {
408
+ "enableAIMessages": true,
409
+ "aiEndpoint": "http://localhost:11434/v1",
410
+ "aiModel": "llama3",
411
+ "aiApiKey": "",
412
+ "aiFallbackToStatic": true
413
+ }
414
+ ```
415
+
416
+ 3. **The AI will generate unique messages** for each notification, which are then spoken by your TTS engine.
417
+
418
+ **Supported AI Servers:**
419
+ | Server | Default Endpoint | API Key |
420
+ |--------|-----------------|---------|
421
+ | Ollama | `http://localhost:11434/v1` | Not needed |
422
+ | LM Studio | `http://localhost:1234/v1` | Not needed |
423
+ | LocalAI | `http://localhost:8080/v1` | Not needed |
424
+ | vLLM | `http://localhost:8000/v1` | Use "EMPTY" |
425
+ | Jan.ai | `http://localhost:1337/v1` | Required |
426
+
236
427
  ## Requirements
237
428
 
238
429
  ### For ElevenLabs TTS
@@ -16,6 +16,13 @@
16
16
  //
17
17
  // ============================================================
18
18
 
19
+ // ============================================================
20
+ // PLUGIN ENABLE/DISABLE
21
+ // ============================================================
22
+ // Master switch to enable or disable the entire plugin.
23
+ // Set to false to disable all notifications without uninstalling.
24
+ "enabled": true,
25
+
19
26
  // ============================================================
20
27
  // NOTIFICATION MODE SETTINGS (Smart Notification System)
21
28
  // ============================================================
@@ -243,6 +250,71 @@
243
250
  // Question batch window (ms) - how long to wait for more questions before notifying
244
251
  "questionBatchWindowMs": 800,
245
252
 
253
+ // ============================================================
254
+ // AI MESSAGE GENERATION (OpenAI-Compatible Endpoints)
255
+ // ============================================================
256
+ // Use a local/self-hosted AI to generate dynamic notification messages
257
+ // instead of using preset static messages. The AI generates the text,
258
+ // which is then spoken by your configured TTS engine (ElevenLabs, Edge, etc.)
259
+ //
260
+ // Supports: Ollama, LM Studio, LocalAI, vLLM, llama.cpp, Jan.ai, and any
261
+ // OpenAI-compatible endpoint. You provide your own endpoint URL and API key.
262
+ //
263
+ // HOW IT WORKS:
264
+ // 1. When a notification is triggered (task complete, permission needed, etc.)
265
+ // 2. If AI is enabled, the plugin sends a prompt to your AI server
266
+ // 3. The AI generates a unique, contextual notification message
267
+ // 4. That message is spoken by your TTS engine (ElevenLabs, Edge, SAPI)
268
+ // 5. If AI fails, it falls back to the static messages defined above
269
+
270
+ // Enable AI-generated messages (experimental feature)
271
+ // Default: false (uses static messages defined above)
272
+ "enableAIMessages": false,
273
+
274
+ // Your AI server endpoint URL
275
+ // Common local AI servers and their default endpoints:
276
+ // Ollama: http://localhost:11434/v1
277
+ // LM Studio: http://localhost:1234/v1
278
+ // LocalAI: http://localhost:8080/v1
279
+ // vLLM: http://localhost:8000/v1
280
+ // llama.cpp: http://localhost:8080/v1
281
+ // Jan.ai: http://localhost:1337/v1
282
+ // text-gen-webui: http://localhost:5000/v1
283
+ "aiEndpoint": "http://localhost:11434/v1",
284
+
285
+ // Model name to use (must match a model loaded in your AI server)
286
+ // Examples for Ollama: "llama3", "llama3.2", "mistral", "phi3", "gemma2", "qwen2"
287
+ // For LM Studio: Use the model name shown in the UI
288
+ "aiModel": "llama3",
289
+
290
+ // API key for your AI server
291
+ // Most local servers (Ollama, LM Studio, LocalAI) don't require a key - leave empty
292
+ // Only set this if your server requires authentication
293
+ // For vLLM with auth disabled, use "EMPTY"
294
+ "aiApiKey": "",
295
+
296
+ // Request timeout in milliseconds
297
+ // Local AI can be slow on first request (model loading), so 15 seconds is recommended
298
+ // Increase if you have a slower machine or larger models
299
+ "aiTimeout": 15000,
300
+
301
+ // Fall back to static messages (defined above) if AI generation fails
302
+ // Recommended: true - ensures notifications always work even if AI is down
303
+ "aiFallbackToStatic": true,
304
+
305
+ // Custom prompts for each notification type
306
+ // You can customize these to change the AI's personality/style
307
+ // The AI will generate a short message based on these prompts
308
+ // TIP: Keep prompts concise - they're sent with each notification
309
+ "aiPrompts": {
310
+ "idle": "Generate a single brief, friendly notification sentence (max 15 words) saying a coding task is complete. Be encouraging and warm. Output only the message, no quotes.",
311
+ "permission": "Generate a single brief, urgent but friendly notification sentence (max 15 words) asking the user to approve a permission request. Output only the message, no quotes.",
312
+ "question": "Generate a single brief, polite notification sentence (max 15 words) saying the assistant has a question and needs user input. Output only the message, no quotes.",
313
+ "idleReminder": "Generate a single brief, gentle reminder sentence (max 15 words) that a completed task is waiting for review. Be slightly more insistent. Output only the message, no quotes.",
314
+ "permissionReminder": "Generate a single brief, urgent reminder sentence (max 15 words) that permission approval is still needed. Convey importance. Output only the message, no quotes.",
315
+ "questionReminder": "Generate a single brief, polite but persistent reminder sentence (max 15 words) that a question is still waiting for an answer. Output only the message, no quotes."
316
+ },
317
+
246
318
  // ============================================================
247
319
  // SOUND FILES (For immediate notifications)
248
320
  // These are played first before TTS reminder kicks in
package/index.js CHANGED
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
4
  import { createTTS, getTTSConfig } from './util/tts.js';
5
+ import { getSmartMessage } from './util/ai-messages.js';
5
6
 
6
7
  /**
7
8
  * OpenCode Smart Voice Notify Plugin
@@ -23,9 +24,28 @@ import { createTTS, getTTSConfig } from './util/tts.js';
23
24
  */
24
25
  export default async function SmartVoiceNotifyPlugin({ project, client, $, directory, worktree }) {
25
26
  const config = getTTSConfig();
27
+
28
+ // Master switch: if plugin is disabled, return empty handlers immediately
29
+ if (config.enabled === false) {
30
+ const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
31
+ const logsDir = path.join(configDir, 'logs');
32
+ const logFile = path.join(logsDir, 'smart-voice-notify-debug.log');
33
+ if (config.debugLog) {
34
+ try {
35
+ if (!fs.existsSync(logsDir)) {
36
+ fs.mkdirSync(logsDir, { recursive: true });
37
+ }
38
+ const timestamp = new Date().toISOString();
39
+ fs.appendFileSync(logFile, `[${timestamp}] Plugin disabled via config (enabled: false) - no event handlers registered\n`);
40
+ } catch (e) {}
41
+ }
42
+ return {};
43
+ }
44
+
26
45
  const tts = createTTS({ $, client });
27
46
 
28
47
  const platform = os.platform();
48
+
29
49
  const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
30
50
  const logsDir = path.join(configDir, 'logs');
31
51
  const logFile = path.join(logsDir, 'smart-voice-notify-debug.log');
@@ -232,11 +252,11 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
232
252
  const storedCount = reminder?.itemCount || 1;
233
253
  let reminderMessage;
234
254
  if (type === 'permission') {
235
- reminderMessage = getPermissionMessage(storedCount, true);
255
+ reminderMessage = await getPermissionMessage(storedCount, true);
236
256
  } else if (type === 'question') {
237
- reminderMessage = getQuestionMessage(storedCount, true);
257
+ reminderMessage = await getQuestionMessage(storedCount, true);
238
258
  } else {
239
- reminderMessage = getRandomMessage(config.idleReminderTTSMessages);
259
+ reminderMessage = await getSmartMessage('idle', true, config.idleReminderTTSMessages);
240
260
  }
241
261
 
242
262
  // Check for ElevenLabs API key configuration issues
@@ -286,11 +306,11 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
286
306
  const followUpStoredCount = followUpReminder?.itemCount || 1;
287
307
  let followUpMessage;
288
308
  if (type === 'permission') {
289
- followUpMessage = getPermissionMessage(followUpStoredCount, true);
309
+ followUpMessage = await getPermissionMessage(followUpStoredCount, true);
290
310
  } else if (type === 'question') {
291
- followUpMessage = getQuestionMessage(followUpStoredCount, true);
311
+ followUpMessage = await getQuestionMessage(followUpStoredCount, true);
292
312
  } else {
293
- followUpMessage = getRandomMessage(config.idleReminderTTSMessages);
313
+ followUpMessage = await getSmartMessage('idle', true, config.idleReminderTTSMessages);
294
314
  }
295
315
 
296
316
  await tts.wakeMonitor();
@@ -372,11 +392,11 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
372
392
  if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
373
393
  let immediateMessage;
374
394
  if (type === 'permission') {
375
- immediateMessage = getRandomMessage(config.permissionTTSMessages);
395
+ immediateMessage = await getSmartMessage('permission', false, config.permissionTTSMessages);
376
396
  } else if (type === 'question') {
377
- immediateMessage = getRandomMessage(config.questionTTSMessages);
397
+ immediateMessage = await getSmartMessage('question', false, config.questionTTSMessages);
378
398
  } else {
379
- immediateMessage = getRandomMessage(config.idleTTSMessages);
399
+ immediateMessage = await getSmartMessage('idle', false, config.idleTTSMessages);
380
400
  }
381
401
 
382
402
  await tts.speak(immediateMessage, {
@@ -388,18 +408,19 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
388
408
 
389
409
  /**
390
410
  * Get a count-aware TTS message for permission requests
411
+ * Uses AI generation when enabled, falls back to static messages
391
412
  * @param {number} count - Number of permission requests
392
413
  * @param {boolean} isReminder - Whether this is a reminder message
393
- * @returns {string} The formatted message
414
+ * @returns {Promise<string>} The formatted message
394
415
  */
395
- const getPermissionMessage = (count, isReminder = false) => {
416
+ const getPermissionMessage = async (count, isReminder = false) => {
396
417
  const messages = isReminder
397
418
  ? config.permissionReminderTTSMessages
398
419
  : config.permissionTTSMessages;
399
420
 
400
421
  if (count === 1) {
401
- // Single permission - use regular message
402
- return getRandomMessage(messages);
422
+ // Single permission - use smart message (AI or static fallback)
423
+ return await getSmartMessage('permission', isReminder, messages, { count });
403
424
  } else {
404
425
  // Multiple permissions - use count-aware messages if available, or format dynamically
405
426
  const countMessages = isReminder
@@ -411,7 +432,11 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
411
432
  const template = getRandomMessage(countMessages);
412
433
  return template.replace('{count}', count.toString());
413
434
  } else {
414
- // Fallback: generate a dynamic message
435
+ // Try AI message with count context, fallback to dynamic message
436
+ const aiMessage = await getSmartMessage('permission', isReminder, [], { count });
437
+ if (aiMessage !== 'Notification') {
438
+ return aiMessage;
439
+ }
415
440
  return `Attention! There are ${count} permission requests waiting for your approval.`;
416
441
  }
417
442
  }
@@ -419,18 +444,19 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
419
444
 
420
445
  /**
421
446
  * Get a count-aware TTS message for question requests (SDK v1.1.7+)
447
+ * Uses AI generation when enabled, falls back to static messages
422
448
  * @param {number} count - Number of question requests
423
449
  * @param {boolean} isReminder - Whether this is a reminder message
424
- * @returns {string} The formatted message
450
+ * @returns {Promise<string>} The formatted message
425
451
  */
426
- const getQuestionMessage = (count, isReminder = false) => {
452
+ const getQuestionMessage = async (count, isReminder = false) => {
427
453
  const messages = isReminder
428
454
  ? config.questionReminderTTSMessages
429
455
  : config.questionTTSMessages;
430
456
 
431
457
  if (count === 1) {
432
- // Single question - use regular message
433
- return getRandomMessage(messages);
458
+ // Single question - use smart message (AI or static fallback)
459
+ return await getSmartMessage('question', isReminder, messages, { count });
434
460
  } else {
435
461
  // Multiple questions - use count-aware messages if available, or format dynamically
436
462
  const countMessages = isReminder
@@ -442,7 +468,11 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
442
468
  const template = getRandomMessage(countMessages);
443
469
  return template.replace('{count}', count.toString());
444
470
  } else {
445
- // Fallback: generate a dynamic message
471
+ // Try AI message with count context, fallback to dynamic message
472
+ const aiMessage = await getSmartMessage('question', isReminder, [], { count });
473
+ if (aiMessage !== 'Notification') {
474
+ return aiMessage;
475
+ }
446
476
  return `Hey! I have ${count} questions for you. Please check your screen.`;
447
477
  }
448
478
  }
@@ -489,8 +519,8 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
489
519
  }
490
520
 
491
521
  // Get count-aware TTS message
492
- const ttsMessage = getPermissionMessage(batchCount, false);
493
- const reminderMessage = getPermissionMessage(batchCount, true);
522
+ const ttsMessage = await getPermissionMessage(batchCount, false);
523
+ const reminderMessage = await getPermissionMessage(batchCount, true);
494
524
 
495
525
  // Smart notification: sound first, TTS reminder later
496
526
  await smartNotify('permission', {
@@ -563,8 +593,8 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
563
593
  }
564
594
 
565
595
  // Get count-aware TTS message (uses total question count, not request count)
566
- const ttsMessage = getQuestionMessage(totalQuestionCount, false);
567
- const reminderMessage = getQuestionMessage(totalQuestionCount, true);
596
+ const ttsMessage = await getQuestionMessage(totalQuestionCount, false);
597
+ const reminderMessage = await getQuestionMessage(totalQuestionCount, true);
568
598
 
569
599
  // Smart notification: sound first, TTS reminder later
570
600
  // Sound plays 2 times by default (matching permission behavior)
@@ -736,11 +766,14 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
736
766
  debugLog(`session.idle: notifying for session ${sessionID} (idleTime=${lastSessionIdleTime})`);
737
767
  await showToast("✅ Agent has finished working", "success", 5000);
738
768
 
769
+ // Get smart message for idle notification (AI or static fallback)
770
+ const idleTtsMessage = await getSmartMessage('idle', false, config.idleTTSMessages);
771
+
739
772
  // Smart notification: sound first, TTS reminder later
740
773
  await smartNotify('idle', {
741
774
  soundFile: config.idleSound,
742
775
  soundLoops: 1,
743
- ttsMessage: getRandomMessage(config.idleTTSMessages),
776
+ ttsMessage: idleTtsMessage,
744
777
  fallbackSound: config.idleSound
745
778
  });
746
779
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-smart-voice-notify",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
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",
@@ -0,0 +1,207 @@
1
+ /**
2
+ * AI Message Generation Module
3
+ *
4
+ * Generates dynamic notification messages using OpenAI-compatible AI endpoints.
5
+ * Supports: Ollama, LM Studio, LocalAI, vLLM, llama.cpp, Jan.ai, etc.
6
+ *
7
+ * Uses native fetch() - no external dependencies required.
8
+ */
9
+
10
+ import { getTTSConfig } from './tts.js';
11
+
12
+ /**
13
+ * Generate a message using an OpenAI-compatible AI endpoint
14
+ * @param {string} promptType - The type of prompt ('idle', 'permission', 'question', 'idleReminder', 'permissionReminder', 'questionReminder')
15
+ * @param {object} context - Optional context about the notification (for future use)
16
+ * @returns {Promise<string|null>} Generated message or null if failed
17
+ */
18
+ export async function generateAIMessage(promptType, context = {}) {
19
+ const config = getTTSConfig();
20
+
21
+ // Check if AI messages are enabled
22
+ if (!config.enableAIMessages) {
23
+ return null;
24
+ }
25
+
26
+ // Get the prompt for this type
27
+ const prompt = config.aiPrompts?.[promptType];
28
+ if (!prompt) {
29
+ console.error(`[AI Messages] No prompt configured for type: ${promptType}`);
30
+ return null;
31
+ }
32
+
33
+ try {
34
+ // Build headers
35
+ const headers = { 'Content-Type': 'application/json' };
36
+ if (config.aiApiKey) {
37
+ headers['Authorization'] = `Bearer ${config.aiApiKey}`;
38
+ }
39
+
40
+ // Build endpoint URL (ensure it ends with /chat/completions)
41
+ let endpoint = config.aiEndpoint || 'http://localhost:11434/v1';
42
+ if (!endpoint.endsWith('/chat/completions')) {
43
+ endpoint = endpoint.replace(/\/$/, '') + '/chat/completions';
44
+ }
45
+
46
+ // Create abort controller for timeout
47
+ const controller = new AbortController();
48
+ const timeout = setTimeout(() => controller.abort(), config.aiTimeout || 15000);
49
+
50
+ // Make the request
51
+ const response = await fetch(endpoint, {
52
+ method: 'POST',
53
+ headers,
54
+ signal: controller.signal,
55
+ body: JSON.stringify({
56
+ model: config.aiModel || 'llama3',
57
+ messages: [
58
+ {
59
+ role: 'system',
60
+ content: 'You are a helpful assistant that generates short notification messages. Output only the message text, nothing else. No quotes, no explanations.'
61
+ },
62
+ {
63
+ role: 'user',
64
+ content: prompt
65
+ }
66
+ ],
67
+ max_tokens: 1000, // High value to accommodate thinking models (e.g., Gemini 2.5) that use internal reasoning tokens
68
+ temperature: 0.7
69
+ })
70
+ });
71
+
72
+ clearTimeout(timeout);
73
+
74
+ if (!response.ok) {
75
+ const errorText = await response.text().catch(() => 'Unknown error');
76
+ console.error(`[AI Messages] API error ${response.status}: ${errorText}`);
77
+ return null;
78
+ }
79
+
80
+ const data = await response.json();
81
+
82
+ // Extract the message content
83
+ const message = data.choices?.[0]?.message?.content?.trim();
84
+
85
+ if (!message) {
86
+ console.error('[AI Messages] Empty response from AI');
87
+ return null;
88
+ }
89
+
90
+ // Clean up the message (remove quotes if AI added them)
91
+ let cleanMessage = message.replace(/^["']|["']$/g, '').trim();
92
+
93
+ // Validate message length (sanity check)
94
+ if (cleanMessage.length < 5 || cleanMessage.length > 200) {
95
+ console.error(`[AI Messages] Message length invalid: ${cleanMessage.length} chars`);
96
+ return null;
97
+ }
98
+
99
+ return cleanMessage;
100
+
101
+ } catch (error) {
102
+ if (error.name === 'AbortError') {
103
+ console.error(`[AI Messages] Request timed out after ${config.aiTimeout || 15000}ms`);
104
+ } else {
105
+ console.error(`[AI Messages] Error: ${error.message}`);
106
+ }
107
+ return null;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Get a smart message - tries AI first, falls back to static messages
113
+ * @param {string} eventType - 'idle', 'permission', 'question'
114
+ * @param {boolean} isReminder - Whether this is a reminder message
115
+ * @param {string[]} staticMessages - Array of static fallback messages
116
+ * @param {object} context - Optional context (e.g., { count: 3 } for batched notifications)
117
+ * @returns {Promise<string>} The message to speak
118
+ */
119
+ export async function getSmartMessage(eventType, isReminder, staticMessages, context = {}) {
120
+ const config = getTTSConfig();
121
+
122
+ // Determine the prompt type
123
+ const promptType = isReminder ? `${eventType}Reminder` : eventType;
124
+
125
+ // Try AI generation if enabled
126
+ if (config.enableAIMessages) {
127
+ try {
128
+ const aiMessage = await generateAIMessage(promptType, context);
129
+ if (aiMessage) {
130
+ // Log success for debugging
131
+ if (config.debugLog) {
132
+ console.log(`[AI Messages] Generated: ${aiMessage}`);
133
+ }
134
+ return aiMessage;
135
+ }
136
+ } catch (error) {
137
+ console.error(`[AI Messages] Generation failed: ${error.message}`);
138
+ }
139
+
140
+ // Check if fallback is disabled
141
+ if (!config.aiFallbackToStatic) {
142
+ // Return a generic message if fallback disabled and AI failed
143
+ return 'Notification: Please check your screen.';
144
+ }
145
+ }
146
+
147
+ // Fallback to static messages
148
+ if (!Array.isArray(staticMessages) || staticMessages.length === 0) {
149
+ return 'Notification';
150
+ }
151
+
152
+ return staticMessages[Math.floor(Math.random() * staticMessages.length)];
153
+ }
154
+
155
+ /**
156
+ * Test connectivity to the AI endpoint
157
+ * @returns {Promise<{success: boolean, message: string, model?: string}>}
158
+ */
159
+ export async function testAIConnection() {
160
+ const config = getTTSConfig();
161
+
162
+ if (!config.enableAIMessages) {
163
+ return { success: false, message: 'AI messages not enabled' };
164
+ }
165
+
166
+ try {
167
+ const headers = { 'Content-Type': 'application/json' };
168
+ if (config.aiApiKey) {
169
+ headers['Authorization'] = `Bearer ${config.aiApiKey}`;
170
+ }
171
+
172
+ // Try to list models (simpler endpoint to test connectivity)
173
+ let endpoint = config.aiEndpoint || 'http://localhost:11434/v1';
174
+ endpoint = endpoint.replace(/\/$/, '') + '/models';
175
+
176
+ const controller = new AbortController();
177
+ const timeout = setTimeout(() => controller.abort(), 5000);
178
+
179
+ const response = await fetch(endpoint, {
180
+ method: 'GET',
181
+ headers,
182
+ signal: controller.signal
183
+ });
184
+
185
+ clearTimeout(timeout);
186
+
187
+ if (response.ok) {
188
+ const data = await response.json();
189
+ const models = data.data?.map(m => m.id) || [];
190
+ return {
191
+ success: true,
192
+ message: `Connected! Available models: ${models.slice(0, 3).join(', ')}${models.length > 3 ? '...' : ''}`,
193
+ models
194
+ };
195
+ } else {
196
+ return { success: false, message: `HTTP ${response.status}: ${response.statusText}` };
197
+ }
198
+
199
+ } catch (error) {
200
+ if (error.name === 'AbortError') {
201
+ return { success: false, message: 'Connection timed out' };
202
+ }
203
+ return { success: false, message: error.message };
204
+ }
205
+ }
206
+
207
+ export default { generateAIMessage, getSmartMessage, testAIConnection };
package/util/config.js CHANGED
@@ -59,6 +59,13 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
59
59
  // Internal version tracking - DO NOT REMOVE
60
60
  "_configVersion": "${version}",
61
61
 
62
+ // ============================================================
63
+ // PLUGIN ENABLE/DISABLE
64
+ // ============================================================
65
+ // Master switch to enable or disable the entire plugin.
66
+ // Set to false to disable all notifications without uninstalling.
67
+ "enabled": ${overrides.enabled !== undefined ? overrides.enabled : true},
68
+
62
69
  // ============================================================
63
70
  // NOTIFICATION MODE SETTINGS (Smart Notification System)
64
71
  // ============================================================
@@ -290,6 +297,54 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
290
297
  // Question batch window (ms) - how long to wait for more questions before notifying
291
298
  "questionBatchWindowMs": ${overrides.questionBatchWindowMs !== undefined ? overrides.questionBatchWindowMs : 800},
292
299
 
300
+ // ============================================================
301
+ // AI MESSAGE GENERATION (OpenAI-Compatible Endpoints)
302
+ // ============================================================
303
+ // Use a local/self-hosted AI to generate dynamic notification messages
304
+ // instead of using preset static messages. The AI generates the text,
305
+ // which is then spoken by your configured TTS engine (ElevenLabs, Edge, etc.)
306
+ //
307
+ // Supports: Ollama, LM Studio, LocalAI, vLLM, llama.cpp, Jan.ai, and any
308
+ // OpenAI-compatible endpoint. You provide your own endpoint URL and API key.
309
+
310
+ // Enable AI-generated messages (experimental feature)
311
+ "enableAIMessages": ${overrides.enableAIMessages !== undefined ? overrides.enableAIMessages : false},
312
+
313
+ // Your AI server endpoint URL (e.g., Ollama: http://localhost:11434/v1)
314
+ // Common endpoints:
315
+ // Ollama: http://localhost:11434/v1
316
+ // LM Studio: http://localhost:1234/v1
317
+ // LocalAI: http://localhost:8080/v1
318
+ // vLLM: http://localhost:8000/v1
319
+ // Jan.ai: http://localhost:1337/v1
320
+ "aiEndpoint": "${overrides.aiEndpoint || 'http://localhost:11434/v1'}",
321
+
322
+ // Model name to use (depends on what's loaded in your AI server)
323
+ // Examples: "llama3", "mistral", "phi3", "gemma2", "qwen2"
324
+ "aiModel": "${overrides.aiModel || 'llama3'}",
325
+
326
+ // API key for your AI server (leave empty for Ollama/LM Studio/LocalAI)
327
+ // Only needed if your server requires authentication
328
+ "aiApiKey": "${overrides.aiApiKey || ''}",
329
+
330
+ // Request timeout in milliseconds (local AI can be slow on first request)
331
+ "aiTimeout": ${overrides.aiTimeout !== undefined ? overrides.aiTimeout : 15000},
332
+
333
+ // Fallback to static preset messages if AI generation fails
334
+ "aiFallbackToStatic": ${overrides.aiFallbackToStatic !== undefined ? overrides.aiFallbackToStatic : true},
335
+
336
+ // Custom prompts for each notification type
337
+ // The AI will generate a short message based on these prompts
338
+ // Keep prompts concise - they're sent with each notification
339
+ "aiPrompts": ${formatJSON(overrides.aiPrompts || {
340
+ "idle": "Generate a single brief, friendly notification sentence (max 15 words) saying a coding task is complete. Be encouraging and warm. Output only the message, no quotes.",
341
+ "permission": "Generate a single brief, urgent but friendly notification sentence (max 15 words) asking the user to approve a permission request. Output only the message, no quotes.",
342
+ "question": "Generate a single brief, polite notification sentence (max 15 words) saying the assistant has a question and needs user input. Output only the message, no quotes.",
343
+ "idleReminder": "Generate a single brief, gentle reminder sentence (max 15 words) that a completed task is waiting for review. Be slightly more insistent. Output only the message, no quotes.",
344
+ "permissionReminder": "Generate a single brief, urgent reminder sentence (max 15 words) that permission approval is still needed. Convey importance. Output only the message, no quotes.",
345
+ "questionReminder": "Generate a single brief, polite but persistent reminder sentence (max 15 words) that a question is still waiting for an answer. Output only the message, no quotes."
346
+ }, 4)},
347
+
293
348
  // ============================================================
294
349
  // SOUND FILES (For immediate notifications)
295
350
  // These are played first before TTS reminder kicks in