opencode-smart-voice-notify 1.0.5 → 1.0.7
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/index.js +9 -3
- package/package.json +1 -1
- package/util/config.js +134 -104
- package/util/tts.js +22 -9
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import os from 'os';
|
|
3
1
|
import fs from 'fs';
|
|
4
|
-
import
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
5
4
|
import { createTTS, getTTSConfig } from './util/tts.js';
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -183,6 +182,13 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
183
182
|
: config.idleReminderTTSMessages;
|
|
184
183
|
|
|
185
184
|
const reminderMessage = getRandomMessage(reminderMessages);
|
|
185
|
+
|
|
186
|
+
// Check for ElevenLabs API key configuration issues
|
|
187
|
+
// If user hasn't responded (reminder firing) and config is missing, warn about fallback
|
|
188
|
+
if (config.ttsEngine === 'elevenlabs' && (!config.elevenLabsApiKey || config.elevenLabsApiKey.trim() === '')) {
|
|
189
|
+
debugLog('ElevenLabs API key missing during reminder - showing fallback toast');
|
|
190
|
+
await showToast("⚠️ ElevenLabs API Key missing! Falling back to Edge TTS.", "warning", 6000);
|
|
191
|
+
}
|
|
186
192
|
|
|
187
193
|
// Speak the reminder using TTS
|
|
188
194
|
await tts.wakeMonitor();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-smart-voice-notify",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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",
|
package/util/config.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
6
|
/**
|
|
7
7
|
* Basic JSONC parser that strips single-line and multi-line comments.
|
|
8
8
|
* @param {string} jsonc
|
|
@@ -13,6 +13,17 @@ const parseJSONC = (jsonc) => {
|
|
|
13
13
|
return JSON.parse(stripped);
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Helper to format JSON values for the template.
|
|
18
|
+
* @param {any} val
|
|
19
|
+
* @param {number} indent
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
const formatJSON = (val, indent = 0) => {
|
|
23
|
+
const json = JSON.stringify(val, null, 4);
|
|
24
|
+
return indent > 0 ? json.replace(/\n/g, '\n' + ' '.repeat(indent)) : json;
|
|
25
|
+
};
|
|
26
|
+
|
|
16
27
|
/**
|
|
17
28
|
* Get the directory where this plugin is installed.
|
|
18
29
|
* Used to find bundled assets like example.config.jsonc
|
|
@@ -26,8 +37,10 @@ const getPluginDir = () => {
|
|
|
26
37
|
/**
|
|
27
38
|
* Generate a comprehensive default configuration file content.
|
|
28
39
|
* This provides users with ALL available options fully documented.
|
|
40
|
+
* @param {object} overrides - Existing configuration to preserve
|
|
41
|
+
* @param {string} version - Current version to set in config
|
|
29
42
|
*/
|
|
30
|
-
const generateDefaultConfig = () => {
|
|
43
|
+
const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
|
|
31
44
|
return `{
|
|
32
45
|
// ============================================================
|
|
33
46
|
// OpenCode Smart Voice Notify - Configuration
|
|
@@ -43,6 +56,9 @@ const generateDefaultConfig = () => {
|
|
|
43
56
|
//
|
|
44
57
|
// ============================================================
|
|
45
58
|
|
|
59
|
+
// Internal version tracking - DO NOT REMOVE
|
|
60
|
+
"_configVersion": "${version}",
|
|
61
|
+
|
|
46
62
|
// ============================================================
|
|
47
63
|
// NOTIFICATION MODE SETTINGS (Smart Notification System)
|
|
48
64
|
// ============================================================
|
|
@@ -51,25 +67,25 @@ const generateDefaultConfig = () => {
|
|
|
51
67
|
// 'tts-first' - Speak TTS immediately, no sound
|
|
52
68
|
// 'both' - Play sound AND speak TTS immediately
|
|
53
69
|
// 'sound-only' - Only play sound, no TTS at all
|
|
54
|
-
"notificationMode": "sound-first",
|
|
70
|
+
"notificationMode": "${overrides.notificationMode || 'sound-first'}",
|
|
55
71
|
|
|
56
72
|
// ============================================================
|
|
57
73
|
// TTS REMINDER SETTINGS (When user doesn't respond to sound)
|
|
58
74
|
// ============================================================
|
|
59
75
|
|
|
60
76
|
// Enable TTS reminder if user doesn't respond after sound notification
|
|
61
|
-
"enableTTSReminder": true,
|
|
77
|
+
"enableTTSReminder": ${overrides.enableTTSReminder !== undefined ? overrides.enableTTSReminder : true},
|
|
62
78
|
|
|
63
79
|
// Delay (in seconds) before TTS reminder fires
|
|
64
80
|
// Set globally or per-notification type
|
|
65
|
-
"ttsReminderDelaySeconds": 30, // Global default
|
|
66
|
-
"idleReminderDelaySeconds": 30, // For task completion notifications
|
|
67
|
-
"permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
|
|
81
|
+
"ttsReminderDelaySeconds": ${overrides.ttsReminderDelaySeconds !== undefined ? overrides.ttsReminderDelaySeconds : 30}, // Global default
|
|
82
|
+
"idleReminderDelaySeconds": ${overrides.idleReminderDelaySeconds !== undefined ? overrides.idleReminderDelaySeconds : 30}, // For task completion notifications
|
|
83
|
+
"permissionReminderDelaySeconds": ${overrides.permissionReminderDelaySeconds !== undefined ? overrides.permissionReminderDelaySeconds : 20}, // For permission requests (more urgent)
|
|
68
84
|
|
|
69
85
|
// Follow-up reminders if user STILL doesn't respond after first TTS
|
|
70
|
-
"enableFollowUpReminders": true,
|
|
71
|
-
"maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
|
|
72
|
-
"reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
|
|
86
|
+
"enableFollowUpReminders": ${overrides.enableFollowUpReminders !== undefined ? overrides.enableFollowUpReminders : true},
|
|
87
|
+
"maxFollowUpReminders": ${overrides.maxFollowUpReminders !== undefined ? overrides.maxFollowUpReminders : 3}, // Max number of follow-up TTS reminders
|
|
88
|
+
"reminderBackoffMultiplier": ${overrides.reminderBackoffMultiplier !== undefined ? overrides.reminderBackoffMultiplier : 1.5}, // Each follow-up waits longer (30s, 45s, 67s...)
|
|
73
89
|
|
|
74
90
|
// ============================================================
|
|
75
91
|
// TTS ENGINE SELECTION
|
|
@@ -77,10 +93,10 @@ const generateDefaultConfig = () => {
|
|
|
77
93
|
// 'elevenlabs' - Best quality, anime-like voices (requires API key, free tier: 10k chars/month)
|
|
78
94
|
// 'edge' - Good quality neural voices (free, requires: pip install edge-tts)
|
|
79
95
|
// 'sapi' - Windows built-in voices (free, offline, robotic)
|
|
80
|
-
"ttsEngine": "
|
|
96
|
+
"ttsEngine": "${overrides.ttsEngine || 'elevenlabs'}",
|
|
81
97
|
|
|
82
98
|
// Enable TTS for notifications (falls back to sound files if TTS fails)
|
|
83
|
-
"enableTTS": true,
|
|
99
|
+
"enableTTS": ${overrides.enableTTS !== undefined ? overrides.enableTTS : true},
|
|
84
100
|
|
|
85
101
|
// ============================================================
|
|
86
102
|
// ELEVENLABS SETTINGS (Best Quality - Anime-like Voices)
|
|
@@ -92,7 +108,7 @@ const generateDefaultConfig = () => {
|
|
|
92
108
|
// 1. Uncomment elevenLabsApiKey and add your key
|
|
93
109
|
// 2. Change ttsEngine above to "elevenlabs"
|
|
94
110
|
//
|
|
95
|
-
|
|
111
|
+
${overrides.elevenLabsApiKey ? `"elevenLabsApiKey": "${overrides.elevenLabsApiKey}",` : `// "elevenLabsApiKey": "YOUR_API_KEY_HERE",`}
|
|
96
112
|
|
|
97
113
|
// Voice ID - Recommended cute/anime-like voices:
|
|
98
114
|
// 'cgSgspJ2msm6clMCkdW9' - Jessica (Playful, Bright, Warm) - RECOMMENDED
|
|
@@ -100,15 +116,15 @@ const generateDefaultConfig = () => {
|
|
|
100
116
|
// 'jsCqWAovK2LkecY7zXl4' - Freya (Expressive, Confident)
|
|
101
117
|
// 'EXAVITQu4vr4xnSDxMaL' - Sarah (Soft, Warm)
|
|
102
118
|
// Browse more at: https://elevenlabs.io/voice-library
|
|
103
|
-
"elevenLabsVoiceId": "cgSgspJ2msm6clMCkdW9",
|
|
119
|
+
"elevenLabsVoiceId": "${overrides.elevenLabsVoiceId || 'cgSgspJ2msm6clMCkdW9'}",
|
|
104
120
|
|
|
105
121
|
// Model: 'eleven_turbo_v2_5' (fast, good), 'eleven_multilingual_v2' (highest quality)
|
|
106
|
-
"elevenLabsModel": "eleven_turbo_v2_5",
|
|
122
|
+
"elevenLabsModel": "${overrides.elevenLabsModel || 'eleven_turbo_v2_5'}",
|
|
107
123
|
|
|
108
124
|
// Voice tuning (0.0 to 1.0)
|
|
109
|
-
"elevenLabsStability": 0.5, // Lower = more expressive, Higher = more consistent
|
|
110
|
-
"elevenLabsSimilarity": 0.75, // How closely to match the original voice
|
|
111
|
-
"elevenLabsStyle": 0.5, // Style exaggeration (higher = more expressive)
|
|
125
|
+
"elevenLabsStability": ${overrides.elevenLabsStability !== undefined ? overrides.elevenLabsStability : 0.5}, // Lower = more expressive, Higher = more consistent
|
|
126
|
+
"elevenLabsSimilarity": ${overrides.elevenLabsSimilarity !== undefined ? overrides.elevenLabsSimilarity : 0.75}, // How closely to match the original voice
|
|
127
|
+
"elevenLabsStyle": ${overrides.elevenLabsStyle !== undefined ? overrides.elevenLabsStyle : 0.5}, // Style exaggeration (higher = more expressive)
|
|
112
128
|
|
|
113
129
|
// ============================================================
|
|
114
130
|
// EDGE TTS SETTINGS (Free Neural Voices - Default Engine)
|
|
@@ -121,13 +137,13 @@ const generateDefaultConfig = () => {
|
|
|
121
137
|
// 'en-US-AriaNeural' - Confident, clear
|
|
122
138
|
// 'en-GB-SoniaNeural' - British, friendly
|
|
123
139
|
// 'en-AU-NatashaNeural' - Australian, warm
|
|
124
|
-
"edgeVoice": "en-US-
|
|
140
|
+
"edgeVoice": "${overrides.edgeVoice || 'en-US-JennyNeural'}",
|
|
125
141
|
|
|
126
142
|
// Pitch adjustment: +0Hz to +100Hz (higher = more anime-like)
|
|
127
|
-
"edgePitch": "+
|
|
143
|
+
"edgePitch": "${overrides.edgePitch || '+0Hz'}",
|
|
128
144
|
|
|
129
145
|
// Speech rate: -50% to +100%
|
|
130
|
-
"edgeRate": "+10%",
|
|
146
|
+
"edgeRate": "${overrides.edgeRate || '+10%'}",
|
|
131
147
|
|
|
132
148
|
// ============================================================
|
|
133
149
|
// SAPI SETTINGS (Windows Built-in - Last Resort Fallback)
|
|
@@ -140,16 +156,16 @@ const generateDefaultConfig = () => {
|
|
|
140
156
|
// 'Microsoft Zira Desktop' - Female, US English
|
|
141
157
|
// 'Microsoft David Desktop' - Male, US English
|
|
142
158
|
// 'Microsoft Hazel Desktop' - Female, UK English
|
|
143
|
-
"sapiVoice": "Microsoft Zira Desktop",
|
|
159
|
+
"sapiVoice": "${overrides.sapiVoice || 'Microsoft Zira Desktop'}",
|
|
144
160
|
|
|
145
161
|
// Speech rate: -10 (slowest) to +10 (fastest), 0 is normal
|
|
146
|
-
"sapiRate": -1,
|
|
162
|
+
"sapiRate": ${overrides.sapiRate !== undefined ? overrides.sapiRate : -1},
|
|
147
163
|
|
|
148
164
|
// Pitch: 'x-low', 'low', 'medium', 'high', 'x-high'
|
|
149
|
-
"sapiPitch": "medium",
|
|
165
|
+
"sapiPitch": "${overrides.sapiPitch || 'medium'}",
|
|
150
166
|
|
|
151
167
|
// Volume: 'silent', 'x-soft', 'soft', 'medium', 'loud', 'x-loud'
|
|
152
|
-
"sapiVolume": "loud",
|
|
168
|
+
"sapiVolume": "${overrides.sapiVolume || 'loud'}",
|
|
153
169
|
|
|
154
170
|
// ============================================================
|
|
155
171
|
// INITIAL TTS MESSAGES (Used immediately or after sound)
|
|
@@ -157,22 +173,22 @@ const generateDefaultConfig = () => {
|
|
|
157
173
|
// ============================================================
|
|
158
174
|
|
|
159
175
|
// Messages when agent finishes work (task completion)
|
|
160
|
-
"idleTTSMessages": [
|
|
176
|
+
"idleTTSMessages": ${formatJSON(overrides.idleTTSMessages || [
|
|
161
177
|
"All done! Your task has been completed successfully.",
|
|
162
178
|
"Hey there! I finished working on your request.",
|
|
163
179
|
"Task complete! Ready for your review whenever you are.",
|
|
164
180
|
"Good news! Everything is done and ready for you.",
|
|
165
181
|
"Finished! Let me know if you need anything else."
|
|
166
|
-
],
|
|
182
|
+
], 4)},
|
|
167
183
|
|
|
168
184
|
// Messages for permission requests
|
|
169
|
-
"permissionTTSMessages": [
|
|
185
|
+
"permissionTTSMessages": ${formatJSON(overrides.permissionTTSMessages || [
|
|
170
186
|
"Attention please! I need your permission to continue.",
|
|
171
187
|
"Hey! Quick approval needed to proceed with the task.",
|
|
172
188
|
"Heads up! There is a permission request waiting for you.",
|
|
173
189
|
"Excuse me! I need your authorization before I can continue.",
|
|
174
190
|
"Permission required! Please review and approve when ready."
|
|
175
|
-
],
|
|
191
|
+
], 4)},
|
|
176
192
|
|
|
177
193
|
// ============================================================
|
|
178
194
|
// TTS REMINDER MESSAGES (More urgent - used after delay if no response)
|
|
@@ -180,22 +196,22 @@ const generateDefaultConfig = () => {
|
|
|
180
196
|
// ============================================================
|
|
181
197
|
|
|
182
198
|
// Reminder messages when agent finished but user hasn't responded
|
|
183
|
-
"idleReminderTTSMessages": [
|
|
199
|
+
"idleReminderTTSMessages": ${formatJSON(overrides.idleReminderTTSMessages || [
|
|
184
200
|
"Hey, are you still there? Your task has been waiting for review.",
|
|
185
201
|
"Just a gentle reminder - I finished your request a while ago!",
|
|
186
202
|
"Hello? I completed your task. Please take a look when you can.",
|
|
187
203
|
"Still waiting for you! The work is done and ready for review.",
|
|
188
204
|
"Knock knock! Your completed task is patiently waiting for you."
|
|
189
|
-
],
|
|
205
|
+
], 4)},
|
|
190
206
|
|
|
191
207
|
// Reminder messages when permission still needed
|
|
192
|
-
"permissionReminderTTSMessages": [
|
|
208
|
+
"permissionReminderTTSMessages": ${formatJSON(overrides.permissionReminderTTSMessages || [
|
|
193
209
|
"Hey! I still need your permission to continue. Please respond!",
|
|
194
210
|
"Reminder: There is a pending permission request. I cannot proceed without you.",
|
|
195
211
|
"Hello? I am waiting for your approval. This is getting urgent!",
|
|
196
212
|
"Please check your screen! I really need your permission to move forward.",
|
|
197
213
|
"Still waiting for authorization! The task is on hold until you respond."
|
|
198
|
-
],
|
|
214
|
+
], 4)},
|
|
199
215
|
|
|
200
216
|
// ============================================================
|
|
201
217
|
// SOUND FILES (For immediate notifications)
|
|
@@ -205,77 +221,77 @@ const generateDefaultConfig = () => {
|
|
|
205
221
|
// Sound files are automatically copied here on first run
|
|
206
222
|
// You can replace with your own custom MP3/WAV files
|
|
207
223
|
|
|
208
|
-
"idleSound": "assets/Soft-high-tech-notification-sound-effect.mp3",
|
|
209
|
-
"permissionSound": "assets/Machine-alert-beep-sound-effect.mp3",
|
|
224
|
+
"idleSound": "${overrides.idleSound || 'assets/Soft-high-tech-notification-sound-effect.mp3'}",
|
|
225
|
+
"permissionSound": "${overrides.permissionSound || 'assets/Machine-alert-beep-sound-effect.mp3'}",
|
|
210
226
|
|
|
211
227
|
// ============================================================
|
|
212
228
|
// GENERAL SETTINGS
|
|
213
229
|
// ============================================================
|
|
214
230
|
|
|
215
231
|
// Wake monitor from sleep when notifying (Windows/macOS)
|
|
216
|
-
"wakeMonitor": true,
|
|
232
|
+
"wakeMonitor": ${overrides.wakeMonitor !== undefined ? overrides.wakeMonitor : true},
|
|
217
233
|
|
|
218
234
|
// Force system volume up if below threshold
|
|
219
|
-
"forceVolume": true,
|
|
235
|
+
"forceVolume": ${overrides.forceVolume !== undefined ? overrides.forceVolume : true},
|
|
220
236
|
|
|
221
237
|
// Volume threshold (0-100): force volume if current level is below this
|
|
222
|
-
"volumeThreshold": 50,
|
|
238
|
+
"volumeThreshold": ${overrides.volumeThreshold !== undefined ? overrides.volumeThreshold : 50},
|
|
223
239
|
|
|
224
240
|
// Show TUI toast notifications in OpenCode terminal
|
|
225
|
-
"enableToast": true,
|
|
241
|
+
"enableToast": ${overrides.enableToast !== undefined ? overrides.enableToast : true},
|
|
226
242
|
|
|
227
243
|
// Enable audio notifications (sound files and TTS)
|
|
228
|
-
"enableSound": true,
|
|
244
|
+
"enableSound": ${overrides.enableSound !== undefined ? overrides.enableSound : true},
|
|
229
245
|
|
|
230
246
|
// Consider monitor asleep after this many seconds of inactivity (Windows only)
|
|
231
|
-
"idleThresholdSeconds": 60,
|
|
247
|
+
"idleThresholdSeconds": ${overrides.idleThresholdSeconds !== undefined ? overrides.idleThresholdSeconds : 60},
|
|
232
248
|
|
|
233
249
|
// Enable debug logging to ~/.config/opencode/smart-voice-notify-debug.log
|
|
234
250
|
// Useful for troubleshooting notification issues
|
|
235
|
-
"debugLog": false
|
|
236
|
-
}
|
|
237
|
-
`;
|
|
251
|
+
"debugLog": ${overrides.debugLog !== undefined ? overrides.debugLog : false}
|
|
252
|
+
}`;
|
|
238
253
|
};
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Copy bundled assets (sound files) to the OpenCode config directory.
|
|
242
|
-
* @param {string} configDir - The OpenCode config directory path
|
|
243
|
-
*/
|
|
244
|
-
const copyBundledAssets = (configDir) => {
|
|
245
|
-
try {
|
|
246
|
-
const pluginDir = getPluginDir();
|
|
247
|
-
const sourceAssetsDir = path.join(pluginDir, 'assets');
|
|
248
|
-
const targetAssetsDir = path.join(configDir, 'assets');
|
|
249
|
-
|
|
250
|
-
// Check if source assets exist (they should be bundled with the plugin)
|
|
251
|
-
if (!fs.existsSync(sourceAssetsDir)) {
|
|
252
|
-
return; // No bundled assets to copy
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Create target assets directory if it doesn't exist
|
|
256
|
-
if (!fs.existsSync(targetAssetsDir)) {
|
|
257
|
-
fs.mkdirSync(targetAssetsDir, { recursive: true });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Copy each asset file if it doesn't already exist in target
|
|
261
|
-
const assetFiles = fs.readdirSync(sourceAssetsDir);
|
|
262
|
-
for (const file of assetFiles) {
|
|
263
|
-
const sourcePath = path.join(sourceAssetsDir, file);
|
|
264
|
-
const targetPath = path.join(targetAssetsDir, file);
|
|
265
|
-
|
|
266
|
-
// Only copy if target doesn't exist (don't overwrite user customizations)
|
|
267
|
-
if (!fs.existsSync(targetPath) && fs.statSync(sourcePath).isFile()) {
|
|
268
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
} catch (error) {
|
|
272
|
-
// Silently fail - assets are optional
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Copy bundled assets (sound files) to the OpenCode config directory.
|
|
257
|
+
* @param {string} configDir - The OpenCode config directory path
|
|
258
|
+
*/
|
|
259
|
+
const copyBundledAssets = (configDir) => {
|
|
260
|
+
try {
|
|
261
|
+
const pluginDir = getPluginDir();
|
|
262
|
+
const sourceAssetsDir = path.join(pluginDir, 'assets');
|
|
263
|
+
const targetAssetsDir = path.join(configDir, 'assets');
|
|
264
|
+
|
|
265
|
+
// Check if source assets exist (they should be bundled with the plugin)
|
|
266
|
+
if (!fs.existsSync(sourceAssetsDir)) {
|
|
267
|
+
return; // No bundled assets to copy
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Create target assets directory if it doesn't exist
|
|
271
|
+
if (!fs.existsSync(targetAssetsDir)) {
|
|
272
|
+
fs.mkdirSync(targetAssetsDir, { recursive: true });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Copy each asset file if it doesn't already exist in target
|
|
276
|
+
const assetFiles = fs.readdirSync(sourceAssetsDir);
|
|
277
|
+
for (const file of assetFiles) {
|
|
278
|
+
const sourcePath = path.join(sourceAssetsDir, file);
|
|
279
|
+
const targetPath = path.join(targetAssetsDir, file);
|
|
280
|
+
|
|
281
|
+
// Only copy if target doesn't exist (don't overwrite user customizations)
|
|
282
|
+
if (!fs.existsSync(targetPath) && fs.statSync(sourcePath).isFile()) {
|
|
283
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
// Silently fail - assets are optional
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
276
291
|
/**
|
|
277
292
|
* Loads a configuration file from the OpenCode config directory.
|
|
278
293
|
* If the file doesn't exist, creates a default config file.
|
|
294
|
+
* Performs version checks and migrates config if necessary.
|
|
279
295
|
* @param {string} name - Name of the config file (without .jsonc extension)
|
|
280
296
|
* @param {object} defaults - Default values if file doesn't exist or is invalid
|
|
281
297
|
* @returns {object}
|
|
@@ -284,36 +300,50 @@ export const loadConfig = (name, defaults = {}) => {
|
|
|
284
300
|
const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
|
|
285
301
|
const filePath = path.join(configDir, `${name}.jsonc`);
|
|
286
302
|
|
|
287
|
-
|
|
288
|
-
|
|
303
|
+
// Get current version from package.json
|
|
304
|
+
const pluginDir = getPluginDir();
|
|
305
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pluginDir, 'package.json'), 'utf-8'));
|
|
306
|
+
const currentVersion = pkg.version;
|
|
307
|
+
|
|
308
|
+
let existingConfig = null;
|
|
309
|
+
if (fs.existsSync(filePath)) {
|
|
310
|
+
try {
|
|
311
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
312
|
+
existingConfig = parseJSONC(content);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
// If file is invalid JSONC, we'll treat it as missing and overwrite
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Version check and migration logic
|
|
319
|
+
if (!existingConfig || existingConfig._configVersion !== currentVersion) {
|
|
289
320
|
try {
|
|
290
321
|
// Ensure config directory exists
|
|
291
322
|
if (!fs.existsSync(configDir)) {
|
|
292
323
|
fs.mkdirSync(configDir, { recursive: true });
|
|
293
324
|
}
|
|
294
325
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
326
|
+
// Generate new config content using existing values as overrides
|
|
327
|
+
// This preserves user settings while updating comments and adding new fields
|
|
328
|
+
const newConfigContent = generateDefaultConfig(existingConfig || {}, currentVersion);
|
|
329
|
+
fs.writeFileSync(filePath, newConfigContent, 'utf-8');
|
|
298
330
|
|
|
299
|
-
// Also
|
|
331
|
+
// Also ensure all bundled assets (sound files) are present in the config directory
|
|
300
332
|
copyBundledAssets(configDir);
|
|
301
333
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
334
|
+
if (existingConfig) {
|
|
335
|
+
console.log(`[Smart Voice Notify] Config migrated to version ${currentVersion}`);
|
|
336
|
+
} else {
|
|
337
|
+
console.log(`[Smart Voice Notify] Initialized default config at ${filePath}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Re-parse the newly written config
|
|
341
|
+
existingConfig = parseJSONC(newConfigContent);
|
|
305
342
|
} catch (error) {
|
|
306
|
-
// If
|
|
307
|
-
return defaults;
|
|
343
|
+
// If migration fails, try to return whatever we have or defaults
|
|
344
|
+
return existingConfig || defaults;
|
|
308
345
|
}
|
|
309
346
|
}
|
|
310
347
|
|
|
311
|
-
|
|
312
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
313
|
-
const config = parseJSONC(content);
|
|
314
|
-
return { ...defaults, ...config };
|
|
315
|
-
} catch (error) {
|
|
316
|
-
// Silently return defaults - don't use console.error as it breaks TUI
|
|
317
|
-
return defaults;
|
|
318
|
-
}
|
|
348
|
+
return { ...defaults, ...existingConfig };
|
|
319
349
|
};
|
package/util/tts.js
CHANGED
|
@@ -20,8 +20,8 @@ export const getTTSConfig = () => {
|
|
|
20
20
|
elevenLabsStability: 0.5,
|
|
21
21
|
elevenLabsSimilarity: 0.75,
|
|
22
22
|
elevenLabsStyle: 0.5,
|
|
23
|
-
edgeVoice: 'en-US-
|
|
24
|
-
edgePitch: '+
|
|
23
|
+
edgeVoice: 'en-US-JennyNeural',
|
|
24
|
+
edgePitch: '+0Hz',
|
|
25
25
|
edgeRate: '+10%',
|
|
26
26
|
sapiVoice: 'Microsoft Zira Desktop',
|
|
27
27
|
sapiRate: -1,
|
|
@@ -245,7 +245,7 @@ export const createTTS = ({ $, client }) => {
|
|
|
245
245
|
|
|
246
246
|
const ssml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
247
247
|
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
|
|
248
|
-
<voice name="${voice.replace(/
|
|
248
|
+
<voice name="${voice.replace(/"/g, '"')}">
|
|
249
249
|
<prosody rate="${ratePercent}" pitch="${pitch}" volume="${volume}">
|
|
250
250
|
${escapedText}
|
|
251
251
|
</prosody>
|
|
@@ -255,14 +255,27 @@ export const createTTS = ({ $, client }) => {
|
|
|
255
255
|
const scriptContent = `
|
|
256
256
|
Add-Type -AssemblyName System.Speech
|
|
257
257
|
$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
$
|
|
258
|
+
try {
|
|
259
|
+
$synth.Rate = ${rate}
|
|
260
|
+
try { $synth.SelectVoice("${voice.replace(/"/g, '""')}") } catch { }
|
|
261
|
+
$ssml = @"
|
|
262
|
+
${ssml}
|
|
263
|
+
"@
|
|
264
|
+
$synth.SpeakSsml($ssml)
|
|
265
|
+
} catch {
|
|
266
|
+
[Console]::Error.WriteLine($_.Exception.Message)
|
|
267
|
+
exit 1
|
|
268
|
+
} finally {
|
|
269
|
+
if ($synth) { $synth.Dispose() }
|
|
270
|
+
}
|
|
263
271
|
`;
|
|
264
272
|
fs.writeFileSync(scriptPath, scriptContent, 'utf-8');
|
|
265
|
-
await $`powershell.exe -NoProfile -ExecutionPolicy Bypass -File ${scriptPath}`.quiet();
|
|
273
|
+
const result = await $`powershell.exe -NoProfile -ExecutionPolicy Bypass -File ${scriptPath}`.nothrow().quiet();
|
|
274
|
+
|
|
275
|
+
if (result.exitCode !== 0) {
|
|
276
|
+
debugLog(`speakWithSAPI failed with code ${result.exitCode}: ${result.stderr}`);
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
266
279
|
return true;
|
|
267
280
|
} catch (e) {
|
|
268
281
|
debugLog(`speakWithSAPI error: ${e.message}`);
|