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 +293 -102
- package/example.config.jsonc +72 -0
- package/index.js +57 -24
- package/package.json +1 -1
- package/util/ai-messages.js +207 -0
- package/util/config.js +55 -0
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
|
-
//
|
|
103
|
-
// ============================================================
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
132
|
-
|
|
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": "
|
|
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
|
-
//
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
"
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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 (
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"
|
|
200
|
-
"
|
|
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
|
-
//
|
|
217
|
-
// ============================================================
|
|
218
|
-
"
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
package/example.config.jsonc
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
395
|
+
immediateMessage = await getSmartMessage('permission', false, config.permissionTTSMessages);
|
|
376
396
|
} else if (type === 'question') {
|
|
377
|
-
immediateMessage =
|
|
397
|
+
immediateMessage = await getSmartMessage('question', false, config.questionTTSMessages);
|
|
378
398
|
} else {
|
|
379
|
-
immediateMessage =
|
|
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
|
|
402
|
-
return
|
|
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
|
-
//
|
|
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
|
|
433
|
-
return
|
|
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
|
-
//
|
|
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:
|
|
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.
|
|
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
|