opencode-smart-voice-notify 1.0.10 → 1.0.13
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 +91 -60
- package/example.config.jsonc +31 -1
- package/index.js +228 -50
- package/package.json +5 -4
- package/util/config.js +30 -1
- package/util/tts.js +30 -1
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ The plugin automatically tries multiple TTS engines in order, falling back if on
|
|
|
32
32
|
- Automatic cancellation when user responds
|
|
33
33
|
- Per-notification type delays (permission requests are more urgent)
|
|
34
34
|
- **Smart Quota Handling**: Automatically falls back to free Edge TTS if ElevenLabs quota is exceeded
|
|
35
|
+
- **Permission Batching**: Multiple simultaneous permission requests are batched into a single notification (e.g., "5 permission requests require your attention")
|
|
35
36
|
|
|
36
37
|
### System Integration
|
|
37
38
|
- **Native Edge TTS**: No external dependencies (Python/pip) required
|
|
@@ -40,18 +41,20 @@ The plugin automatically tries multiple TTS engines in order, falling back if on
|
|
|
40
41
|
- TUI toast notifications
|
|
41
42
|
- Cross-platform support (Windows, macOS, Linux)
|
|
42
43
|
|
|
43
|
-
## Installation
|
|
44
|
-
|
|
45
|
-
### Option 1: From npm (Recommended)
|
|
46
|
-
|
|
47
|
-
Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
|
|
48
|
-
|
|
49
|
-
```json
|
|
50
|
-
{
|
|
51
|
-
"$schema": "https://opencode.ai/config.json",
|
|
52
|
-
"plugin": ["opencode-smart-voice-notify@latest"]
|
|
53
|
-
}
|
|
54
|
-
```
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
### Option 1: From npm/Bun (Recommended)
|
|
47
|
+
|
|
48
|
+
Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"$schema": "https://opencode.ai/config.json",
|
|
53
|
+
"plugin": ["opencode-smart-voice-notify@latest"]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **Note**: OpenCode will automatically install the plugin using your system's package manager (npm or bun).
|
|
55
58
|
|
|
56
59
|
### Option 2: From GitHub
|
|
57
60
|
|
|
@@ -80,10 +83,11 @@ Add to your OpenCode config file (`~/.config/opencode/opencode.json`):
|
|
|
80
83
|
|
|
81
84
|
### Automatic Setup
|
|
82
85
|
|
|
83
|
-
When you first run OpenCode with this plugin installed, it will **automatically create**:
|
|
84
|
-
|
|
85
|
-
1. **`~/.config/opencode/smart-voice-notify.jsonc`** - A comprehensive configuration file with all available options fully documented.
|
|
86
|
-
2. **`~/.config/opencode/assets/*.mp3`** - Bundled notification sound files.
|
|
86
|
+
When you first run OpenCode with this plugin installed, it will **automatically create**:
|
|
87
|
+
|
|
88
|
+
1. **`~/.config/opencode/smart-voice-notify.jsonc`** - A comprehensive configuration file with all available options fully documented.
|
|
89
|
+
2. **`~/.config/opencode/assets/*.mp3`** - Bundled notification sound files.
|
|
90
|
+
3. **`~/.config/opencode/logs/`** - Debug log folder (created when debug logging is enabled).
|
|
87
91
|
|
|
88
92
|
The auto-generated configuration includes all advanced settings, message arrays, and engine options, so you don't have to refer back to the documentation for available settings.
|
|
89
93
|
|
|
@@ -115,10 +119,16 @@ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` fi
|
|
|
115
119
|
"idleReminderDelaySeconds": 30, // For task completion notifications
|
|
116
120
|
"permissionReminderDelaySeconds": 20, // For permission requests (more urgent)
|
|
117
121
|
|
|
118
|
-
// Follow-up reminders if user STILL doesn't respond after first TTS
|
|
119
|
-
"enableFollowUpReminders": true,
|
|
120
|
-
"maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
|
|
121
|
-
"reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
|
|
122
|
+
// Follow-up reminders if user STILL doesn't respond after first TTS
|
|
123
|
+
"enableFollowUpReminders": true,
|
|
124
|
+
"maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
|
|
125
|
+
"reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
|
|
126
|
+
|
|
127
|
+
// ============================================================
|
|
128
|
+
// PERMISSION BATCHING (Multiple permissions at once)
|
|
129
|
+
// ============================================================
|
|
130
|
+
// When multiple permissions arrive simultaneously, batch them into one notification
|
|
131
|
+
"permissionBatchWindowMs": 800, // Batch window in milliseconds
|
|
122
132
|
|
|
123
133
|
// ============================================================
|
|
124
134
|
// TTS ENGINE SELECTION
|
|
@@ -165,13 +175,18 @@ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` fi
|
|
|
165
175
|
"Good news! Everything is done and ready for you.",
|
|
166
176
|
"Finished! Let me know if you need anything else."
|
|
167
177
|
],
|
|
168
|
-
"permissionTTSMessages": [
|
|
169
|
-
"Attention please! I need your permission to continue.",
|
|
170
|
-
"Hey! Quick approval needed to proceed with the task.",
|
|
171
|
-
"Heads up! There is a permission request waiting for you.",
|
|
172
|
-
"Excuse me! I need your authorization before I can continue.",
|
|
173
|
-
"Permission required! Please review and approve when ready."
|
|
174
|
-
],
|
|
178
|
+
"permissionTTSMessages": [
|
|
179
|
+
"Attention please! I need your permission to continue.",
|
|
180
|
+
"Hey! Quick approval needed to proceed with the task.",
|
|
181
|
+
"Heads up! There is a permission request waiting for you.",
|
|
182
|
+
"Excuse me! I need your authorization before I can continue.",
|
|
183
|
+
"Permission required! Please review and approve when ready."
|
|
184
|
+
],
|
|
185
|
+
// Messages for MULTIPLE permission requests (use {count} placeholder)
|
|
186
|
+
"permissionTTSMessagesMultiple": [
|
|
187
|
+
"Attention please! There are {count} permission requests waiting for your approval.",
|
|
188
|
+
"Hey! {count} permissions need your approval to continue."
|
|
189
|
+
],
|
|
175
190
|
|
|
176
191
|
// ============================================================
|
|
177
192
|
// TTS REMINDER MESSAGES (Used after delay if no response)
|
|
@@ -183,13 +198,18 @@ If you prefer to create the config manually, add a `smart-voice-notify.jsonc` fi
|
|
|
183
198
|
"Still waiting for you! The work is done and ready for review.",
|
|
184
199
|
"Knock knock! Your completed task is patiently waiting for you."
|
|
185
200
|
],
|
|
186
|
-
"permissionReminderTTSMessages": [
|
|
187
|
-
"Hey! I still need your permission to continue. Please respond!",
|
|
188
|
-
"Reminder: There is a pending permission request. I cannot proceed without you.",
|
|
189
|
-
"Hello? I am waiting for your approval. This is getting urgent!",
|
|
190
|
-
"Please check your screen! I really need your permission to move forward.",
|
|
191
|
-
"Still waiting for authorization! The task is on hold until you respond."
|
|
192
|
-
],
|
|
201
|
+
"permissionReminderTTSMessages": [
|
|
202
|
+
"Hey! I still need your permission to continue. Please respond!",
|
|
203
|
+
"Reminder: There is a pending permission request. I cannot proceed without you.",
|
|
204
|
+
"Hello? I am waiting for your approval. This is getting urgent!",
|
|
205
|
+
"Please check your screen! I really need your permission to move forward.",
|
|
206
|
+
"Still waiting for authorization! The task is on hold until you respond."
|
|
207
|
+
],
|
|
208
|
+
// Reminder messages for MULTIPLE permissions (use {count} placeholder)
|
|
209
|
+
"permissionReminderTTSMessagesMultiple": [
|
|
210
|
+
"Hey! I still need your approval for {count} permissions. Please respond!",
|
|
211
|
+
"Reminder: There are {count} pending permission requests."
|
|
212
|
+
],
|
|
193
213
|
|
|
194
214
|
// ============================================================
|
|
195
215
|
// SOUND FILES (relative to OpenCode config directory)
|
|
@@ -229,33 +249,44 @@ See `example.config.jsonc` for more details.
|
|
|
229
249
|
- **macOS**: Built-in (`afplay`)
|
|
230
250
|
- **Linux**: `paplay` or `aplay`
|
|
231
251
|
|
|
232
|
-
## Events Handled
|
|
233
|
-
|
|
234
|
-
| Event | Action |
|
|
235
|
-
|-------|--------|
|
|
236
|
-
| `session.idle` | Agent finished working - notify user |
|
|
237
|
-
| `permission.
|
|
238
|
-
| `permission.
|
|
239
|
-
| `
|
|
240
|
-
| `
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
To develop on this plugin locally:
|
|
245
|
-
|
|
246
|
-
1. Clone the repository:
|
|
247
|
-
```bash
|
|
248
|
-
git clone https://github.com/MasuRii/opencode-smart-voice-notify.git
|
|
249
|
-
cd opencode-smart-voice-notify
|
|
250
|
-
bun install # or npm install
|
|
251
|
-
```
|
|
252
|
+
## Events Handled
|
|
253
|
+
|
|
254
|
+
| Event | Action |
|
|
255
|
+
|-------|--------|
|
|
256
|
+
| `session.idle` | Agent finished working - notify user |
|
|
257
|
+
| `permission.asked` | Permission request (SDK v1.1.1+) - alert user |
|
|
258
|
+
| `permission.updated` | Permission request (SDK v1.0.x) - alert user |
|
|
259
|
+
| `permission.replied` | User responded - cancel pending reminders |
|
|
260
|
+
| `message.updated` | New user message - cancel pending reminders |
|
|
261
|
+
| `session.created` | New session - reset state |
|
|
262
|
+
|
|
263
|
+
> **Note**: The plugin supports both OpenCode SDK v1.0.x and v1.1.x for backward compatibility.
|
|
252
264
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
```
|
|
265
|
+
## Development
|
|
266
|
+
|
|
267
|
+
To develop on this plugin locally:
|
|
268
|
+
|
|
269
|
+
1. Clone the repository:
|
|
270
|
+
```bash
|
|
271
|
+
git clone https://github.com/MasuRii/opencode-smart-voice-notify.git
|
|
272
|
+
cd opencode-smart-voice-notify
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
2. Install dependencies:
|
|
276
|
+
```bash
|
|
277
|
+
# Using Bun (recommended)
|
|
278
|
+
bun install
|
|
279
|
+
|
|
280
|
+
# Or using npm
|
|
281
|
+
npm install
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
3. Link to your OpenCode config:
|
|
285
|
+
```json
|
|
286
|
+
{
|
|
287
|
+
"plugin": ["file:///absolute/path/to/opencode-smart-voice-notify"]
|
|
288
|
+
}
|
|
289
|
+
```
|
|
259
290
|
|
|
260
291
|
## Updating
|
|
261
292
|
|
package/example.config.jsonc
CHANGED
|
@@ -44,6 +44,16 @@
|
|
|
44
44
|
"maxFollowUpReminders": 3, // Max number of follow-up TTS reminders
|
|
45
45
|
"reminderBackoffMultiplier": 1.5, // Each follow-up waits longer (30s, 45s, 67s...)
|
|
46
46
|
|
|
47
|
+
// ============================================================
|
|
48
|
+
// PERMISSION BATCHING (Multiple permissions at once)
|
|
49
|
+
// ============================================================
|
|
50
|
+
// When multiple permissions arrive simultaneously (e.g., 5 at once),
|
|
51
|
+
// batch them into a single notification instead of playing 5 overlapping sounds.
|
|
52
|
+
// The notification will say "X permission requests require your attention".
|
|
53
|
+
|
|
54
|
+
// Batch window (ms) - how long to wait for more permissions before notifying
|
|
55
|
+
"permissionBatchWindowMs": 800,
|
|
56
|
+
|
|
47
57
|
// ============================================================
|
|
48
58
|
// TTS ENGINE SELECTION
|
|
49
59
|
// ============================================================
|
|
@@ -141,6 +151,16 @@
|
|
|
141
151
|
"Excuse me! I need your authorization before I can continue.",
|
|
142
152
|
"Permission required! Please review and approve when ready."
|
|
143
153
|
],
|
|
154
|
+
|
|
155
|
+
// Messages for MULTIPLE permission requests (use {count} placeholder)
|
|
156
|
+
// Used when several permissions arrive simultaneously
|
|
157
|
+
"permissionTTSMessagesMultiple": [
|
|
158
|
+
"Attention please! There are {count} permission requests waiting for your approval.",
|
|
159
|
+
"Hey! {count} permissions need your approval to continue.",
|
|
160
|
+
"Heads up! You have {count} pending permission requests.",
|
|
161
|
+
"Excuse me! I need your authorization for {count} different actions.",
|
|
162
|
+
"{count} permissions required! Please review and approve when ready."
|
|
163
|
+
],
|
|
144
164
|
|
|
145
165
|
// ============================================================
|
|
146
166
|
// TTS REMINDER MESSAGES (More urgent - used after delay if no response)
|
|
@@ -165,6 +185,15 @@
|
|
|
165
185
|
"Still waiting for authorization! The task is on hold until you respond."
|
|
166
186
|
],
|
|
167
187
|
|
|
188
|
+
// Reminder messages for MULTIPLE permissions (use {count} placeholder)
|
|
189
|
+
"permissionReminderTTSMessagesMultiple": [
|
|
190
|
+
"Hey! I still need your approval for {count} permissions. Please respond!",
|
|
191
|
+
"Reminder: There are {count} pending permission requests. I cannot proceed without you.",
|
|
192
|
+
"Hello? I am waiting for your approval on {count} items. This is getting urgent!",
|
|
193
|
+
"Please check your screen! {count} permissions are waiting for your response.",
|
|
194
|
+
"Still waiting for authorization on {count} requests! The task is on hold."
|
|
195
|
+
],
|
|
196
|
+
|
|
168
197
|
// ============================================================
|
|
169
198
|
// SOUND FILES (For immediate notifications)
|
|
170
199
|
// These are played first before TTS reminder kicks in
|
|
@@ -198,7 +227,8 @@
|
|
|
198
227
|
// Consider monitor asleep after this many seconds of inactivity (Windows only)
|
|
199
228
|
"idleThresholdSeconds": 60,
|
|
200
229
|
|
|
201
|
-
// Enable debug logging to ~/.config/opencode/smart-voice-notify-debug.log
|
|
230
|
+
// Enable debug logging to ~/.config/opencode/logs/smart-voice-notify-debug.log
|
|
231
|
+
// The logs folder is created automatically when debug logging is enabled
|
|
202
232
|
// Useful for troubleshooting notification issues
|
|
203
233
|
"debugLog": false
|
|
204
234
|
}
|
package/index.js
CHANGED
|
@@ -27,7 +27,17 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
27
27
|
|
|
28
28
|
const platform = os.platform();
|
|
29
29
|
const configDir = process.env.OPENCODE_CONFIG_DIR || path.join(os.homedir(), '.config', 'opencode');
|
|
30
|
-
const
|
|
30
|
+
const logsDir = path.join(configDir, 'logs');
|
|
31
|
+
const logFile = path.join(logsDir, 'smart-voice-notify-debug.log');
|
|
32
|
+
|
|
33
|
+
// Ensure logs directory exists if debug logging is enabled
|
|
34
|
+
if (config.debugLog && !fs.existsSync(logsDir)) {
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Silently fail - logging is optional
|
|
39
|
+
}
|
|
40
|
+
}
|
|
31
41
|
|
|
32
42
|
// Track pending TTS reminders (can be cancelled if user responds)
|
|
33
43
|
const pendingReminders = new Map();
|
|
@@ -47,6 +57,20 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
47
57
|
// before async notification code runs. Set on permission.updated, cleared on permission.replied.
|
|
48
58
|
let activePermissionId = null;
|
|
49
59
|
|
|
60
|
+
// ========================================
|
|
61
|
+
// PERMISSION BATCHING STATE
|
|
62
|
+
// Batches multiple simultaneous permission requests into a single notification
|
|
63
|
+
// ========================================
|
|
64
|
+
|
|
65
|
+
// Array of permission IDs waiting to be notified (collected during batch window)
|
|
66
|
+
let pendingPermissionBatch = [];
|
|
67
|
+
|
|
68
|
+
// Timeout ID for the batch window (debounce timer)
|
|
69
|
+
let permissionBatchTimeout = null;
|
|
70
|
+
|
|
71
|
+
// Batch window duration in milliseconds (how long to wait for more permissions)
|
|
72
|
+
const PERMISSION_BATCH_WINDOW_MS = config.permissionBatchWindowMs || 800;
|
|
73
|
+
|
|
50
74
|
/**
|
|
51
75
|
* Write debug message to log file
|
|
52
76
|
*/
|
|
@@ -137,8 +161,8 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
137
161
|
* Schedule a TTS reminder if user doesn't respond within configured delay.
|
|
138
162
|
* The reminder uses a personalized TTS message.
|
|
139
163
|
* @param {string} type - 'idle' or 'permission'
|
|
140
|
-
* @param {string} message - The TTS message to speak
|
|
141
|
-
* @param {object} options - Additional options
|
|
164
|
+
* @param {string} message - The TTS message to speak (used directly, supports count-aware messages)
|
|
165
|
+
* @param {object} options - Additional options (fallbackSound, permissionCount)
|
|
142
166
|
*/
|
|
143
167
|
const scheduleTTSReminder = (type, message, options = {}) => {
|
|
144
168
|
// Check if TTS reminders are enabled
|
|
@@ -156,7 +180,10 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
156
180
|
// Cancel any existing reminder of this type
|
|
157
181
|
cancelPendingReminder(type);
|
|
158
182
|
|
|
159
|
-
|
|
183
|
+
// Store permission count for generating count-aware messages in reminders
|
|
184
|
+
const permissionCount = options.permissionCount || 1;
|
|
185
|
+
|
|
186
|
+
debugLog(`scheduleTTSReminder: scheduling ${type} TTS in ${delaySeconds}s (count=${permissionCount})`);
|
|
160
187
|
|
|
161
188
|
const timeoutId = setTimeout(async () => {
|
|
162
189
|
try {
|
|
@@ -174,14 +201,17 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
174
201
|
return;
|
|
175
202
|
}
|
|
176
203
|
|
|
177
|
-
debugLog(`scheduleTTSReminder: firing ${type} TTS reminder`);
|
|
178
|
-
|
|
179
|
-
// Get the appropriate reminder messages (more personalized/urgent)
|
|
180
|
-
const reminderMessages = type === 'permission'
|
|
181
|
-
? config.permissionReminderTTSMessages
|
|
182
|
-
: config.idleReminderTTSMessages;
|
|
204
|
+
debugLog(`scheduleTTSReminder: firing ${type} TTS reminder (count=${reminder?.permissionCount || 1})`);
|
|
183
205
|
|
|
184
|
-
|
|
206
|
+
// Get the appropriate reminder message
|
|
207
|
+
// For permissions with count > 1, use the count-aware message generator
|
|
208
|
+
const storedCount = reminder?.permissionCount || 1;
|
|
209
|
+
let reminderMessage;
|
|
210
|
+
if (type === 'permission') {
|
|
211
|
+
reminderMessage = getPermissionMessage(storedCount, true);
|
|
212
|
+
} else {
|
|
213
|
+
reminderMessage = getRandomMessage(config.idleReminderTTSMessages);
|
|
214
|
+
}
|
|
185
215
|
|
|
186
216
|
// Check for ElevenLabs API key configuration issues
|
|
187
217
|
// If user hasn't responded (reminder firing) and config is missing, warn about fallback
|
|
@@ -226,7 +256,15 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
226
256
|
return;
|
|
227
257
|
}
|
|
228
258
|
|
|
229
|
-
|
|
259
|
+
// Use count-aware message for follow-ups too
|
|
260
|
+
const followUpStoredCount = followUpReminder?.permissionCount || 1;
|
|
261
|
+
let followUpMessage;
|
|
262
|
+
if (type === 'permission') {
|
|
263
|
+
followUpMessage = getPermissionMessage(followUpStoredCount, true);
|
|
264
|
+
} else {
|
|
265
|
+
followUpMessage = getRandomMessage(config.idleReminderTTSMessages);
|
|
266
|
+
}
|
|
267
|
+
|
|
230
268
|
await tts.wakeMonitor();
|
|
231
269
|
await tts.forceVolume();
|
|
232
270
|
await tts.speak(followUpMessage, {
|
|
@@ -240,7 +278,8 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
240
278
|
pendingReminders.set(type, {
|
|
241
279
|
timeoutId: followUpTimeoutId,
|
|
242
280
|
scheduledAt: Date.now(),
|
|
243
|
-
followUpCount
|
|
281
|
+
followUpCount,
|
|
282
|
+
permissionCount: storedCount // Preserve the count for follow-ups
|
|
244
283
|
});
|
|
245
284
|
}
|
|
246
285
|
}
|
|
@@ -250,11 +289,12 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
250
289
|
}
|
|
251
290
|
}, delayMs);
|
|
252
291
|
|
|
253
|
-
// Store the pending reminder
|
|
292
|
+
// Store the pending reminder with permission count
|
|
254
293
|
pendingReminders.set(type, {
|
|
255
294
|
timeoutId,
|
|
256
295
|
scheduledAt: Date.now(),
|
|
257
|
-
followUpCount: 0
|
|
296
|
+
followUpCount: 0,
|
|
297
|
+
permissionCount // Store count for later use
|
|
258
298
|
});
|
|
259
299
|
};
|
|
260
300
|
|
|
@@ -268,7 +308,8 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
268
308
|
soundFile,
|
|
269
309
|
soundLoops = 1,
|
|
270
310
|
ttsMessage,
|
|
271
|
-
fallbackSound
|
|
311
|
+
fallbackSound,
|
|
312
|
+
permissionCount = 1 // Support permission count for batched notifications
|
|
272
313
|
} = options;
|
|
273
314
|
|
|
274
315
|
// Step 1: Play the immediate sound notification
|
|
@@ -290,7 +331,7 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
290
331
|
|
|
291
332
|
// Step 2: Schedule TTS reminder if user doesn't respond
|
|
292
333
|
if (config.enableTTSReminder && ttsMessage) {
|
|
293
|
-
scheduleTTSReminder(type, ttsMessage, { fallbackSound });
|
|
334
|
+
scheduleTTSReminder(type, ttsMessage, { fallbackSound, permissionCount });
|
|
294
335
|
}
|
|
295
336
|
|
|
296
337
|
// Step 3: If TTS-first mode is enabled, also speak immediately
|
|
@@ -306,6 +347,108 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
306
347
|
}
|
|
307
348
|
};
|
|
308
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Get a count-aware TTS message for permission requests
|
|
352
|
+
* @param {number} count - Number of permission requests
|
|
353
|
+
* @param {boolean} isReminder - Whether this is a reminder message
|
|
354
|
+
* @returns {string} The formatted message
|
|
355
|
+
*/
|
|
356
|
+
const getPermissionMessage = (count, isReminder = false) => {
|
|
357
|
+
const messages = isReminder
|
|
358
|
+
? config.permissionReminderTTSMessages
|
|
359
|
+
: config.permissionTTSMessages;
|
|
360
|
+
|
|
361
|
+
if (count === 1) {
|
|
362
|
+
// Single permission - use regular message
|
|
363
|
+
return getRandomMessage(messages);
|
|
364
|
+
} else {
|
|
365
|
+
// Multiple permissions - use count-aware messages if available, or format dynamically
|
|
366
|
+
const countMessages = isReminder
|
|
367
|
+
? config.permissionReminderTTSMessagesMultiple
|
|
368
|
+
: config.permissionTTSMessagesMultiple;
|
|
369
|
+
|
|
370
|
+
if (countMessages && countMessages.length > 0) {
|
|
371
|
+
// Use configured multi-permission messages (replace {count} placeholder)
|
|
372
|
+
const template = getRandomMessage(countMessages);
|
|
373
|
+
return template.replace('{count}', count.toString());
|
|
374
|
+
} else {
|
|
375
|
+
// Fallback: generate a dynamic message
|
|
376
|
+
return `Attention! There are ${count} permission requests waiting for your approval.`;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Process the batched permission requests as a single notification
|
|
383
|
+
* Called after the batch window expires
|
|
384
|
+
*/
|
|
385
|
+
const processPermissionBatch = async () => {
|
|
386
|
+
// Capture and clear the batch
|
|
387
|
+
const batch = [...pendingPermissionBatch];
|
|
388
|
+
const batchCount = batch.length;
|
|
389
|
+
pendingPermissionBatch = [];
|
|
390
|
+
permissionBatchTimeout = null;
|
|
391
|
+
|
|
392
|
+
if (batchCount === 0) {
|
|
393
|
+
debugLog('processPermissionBatch: empty batch, skipping');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
debugLog(`processPermissionBatch: processing ${batchCount} permission(s)`);
|
|
398
|
+
|
|
399
|
+
// Set activePermissionId to the first one (for race condition checks)
|
|
400
|
+
// We track all IDs in the batch for proper cleanup
|
|
401
|
+
activePermissionId = batch[0];
|
|
402
|
+
|
|
403
|
+
// Show toast with count
|
|
404
|
+
const toastMessage = batchCount === 1
|
|
405
|
+
? "⚠️ Permission request requires your attention"
|
|
406
|
+
: `⚠️ ${batchCount} permission requests require your attention`;
|
|
407
|
+
await showToast(toastMessage, "warning", 8000);
|
|
408
|
+
|
|
409
|
+
// CHECK: Did user already respond while we were showing toast?
|
|
410
|
+
if (pendingPermissionBatch.length > 0) {
|
|
411
|
+
// New permissions arrived during toast - they'll be handled in next batch
|
|
412
|
+
debugLog('processPermissionBatch: new permissions arrived during toast');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check if any permission was already replied to
|
|
416
|
+
if (activePermissionId === null) {
|
|
417
|
+
debugLog('processPermissionBatch: aborted - user already responded');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Get count-aware TTS message
|
|
422
|
+
const ttsMessage = getPermissionMessage(batchCount, false);
|
|
423
|
+
const reminderMessage = getPermissionMessage(batchCount, true);
|
|
424
|
+
|
|
425
|
+
// Smart notification: sound first, TTS reminder later
|
|
426
|
+
await smartNotify('permission', {
|
|
427
|
+
soundFile: config.permissionSound,
|
|
428
|
+
soundLoops: batchCount === 1 ? 2 : Math.min(3, batchCount), // More loops for more permissions
|
|
429
|
+
ttsMessage: reminderMessage,
|
|
430
|
+
fallbackSound: config.permissionSound,
|
|
431
|
+
// Pass count for potential use in notification
|
|
432
|
+
permissionCount: batchCount
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Speak immediately if in TTS-first or both mode (with count-aware message)
|
|
436
|
+
if (config.notificationMode === 'tts-first' || config.notificationMode === 'both') {
|
|
437
|
+
await tts.wakeMonitor();
|
|
438
|
+
await tts.forceVolume();
|
|
439
|
+
await tts.speak(ttsMessage, {
|
|
440
|
+
enableTTS: true,
|
|
441
|
+
fallbackSound: config.permissionSound
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Final check: if user responded during notification, cancel scheduled reminder
|
|
446
|
+
if (activePermissionId === null) {
|
|
447
|
+
debugLog('processPermissionBatch: user responded during notification - cancelling reminder');
|
|
448
|
+
cancelPendingReminder('permission');
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
|
|
309
452
|
return {
|
|
310
453
|
event: async ({ event }) => {
|
|
311
454
|
try {
|
|
@@ -313,9 +456,13 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
313
456
|
// USER ACTIVITY DETECTION
|
|
314
457
|
// Cancels pending TTS reminders when user responds
|
|
315
458
|
// ========================================
|
|
316
|
-
// NOTE: OpenCode event types (
|
|
459
|
+
// NOTE: OpenCode event types (supporting SDK v1.0.x and v1.1.x):
|
|
317
460
|
// - message.updated: fires when a message is added/updated (use properties.info.role to check user vs assistant)
|
|
461
|
+
// - permission.updated (SDK v1.0.x): fires when a permission request is created
|
|
462
|
+
// - permission.asked (SDK v1.1.1+): fires when a permission request is created (replaces permission.updated)
|
|
318
463
|
// - permission.replied: fires when user responds to a permission request
|
|
464
|
+
// - SDK v1.0.x: uses permissionID, response
|
|
465
|
+
// - SDK v1.1.1+: uses requestID, reply
|
|
319
466
|
// - session.created: fires when a new session starts
|
|
320
467
|
//
|
|
321
468
|
// CRITICAL: message.updated fires for EVERY modification to a message (not just creation).
|
|
@@ -361,20 +508,37 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
361
508
|
|
|
362
509
|
if (event.type === "permission.replied") {
|
|
363
510
|
// User responded to a permission request (granted or denied)
|
|
364
|
-
// Structure
|
|
511
|
+
// Structure varies by SDK version:
|
|
512
|
+
// - Old SDK: event.properties.{ sessionID, permissionID, response }
|
|
513
|
+
// - New SDK (v1.1.1+): event.properties.{ sessionID, requestID, reply }
|
|
365
514
|
// CRITICAL: Clear activePermissionId FIRST to prevent race condition
|
|
366
|
-
// where permission.updated handler is still running async operations
|
|
367
|
-
const repliedPermissionId = event.properties?.permissionID;
|
|
515
|
+
// where permission.updated/asked handler is still running async operations
|
|
516
|
+
const repliedPermissionId = event.properties?.permissionID || event.properties?.requestID;
|
|
517
|
+
const response = event.properties?.response || event.properties?.reply;
|
|
518
|
+
|
|
519
|
+
// Remove this permission from the pending batch (if still waiting)
|
|
520
|
+
if (repliedPermissionId && pendingPermissionBatch.includes(repliedPermissionId)) {
|
|
521
|
+
pendingPermissionBatch = pendingPermissionBatch.filter(id => id !== repliedPermissionId);
|
|
522
|
+
debugLog(`Permission replied: removed ${repliedPermissionId} from pending batch (${pendingPermissionBatch.length} remaining)`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// If batch is now empty and we have a pending batch timeout, we can cancel it
|
|
526
|
+
// (user responded to all permissions before batch window expired)
|
|
527
|
+
if (pendingPermissionBatch.length === 0 && permissionBatchTimeout) {
|
|
528
|
+
clearTimeout(permissionBatchTimeout);
|
|
529
|
+
permissionBatchTimeout = null;
|
|
530
|
+
debugLog('Permission replied: cancelled batch timeout (all permissions handled)');
|
|
531
|
+
}
|
|
368
532
|
|
|
369
533
|
// Match if IDs are equal, or if we have an active permission with unknown ID (undefined)
|
|
370
|
-
// (This happens if permission.updated received an event without permissionID)
|
|
534
|
+
// (This happens if permission.updated/asked received an event without permissionID)
|
|
371
535
|
if (activePermissionId === repliedPermissionId || activePermissionId === undefined) {
|
|
372
536
|
activePermissionId = null;
|
|
373
537
|
debugLog(`Permission replied: cleared activePermissionId ${repliedPermissionId || '(unknown)'}`);
|
|
374
538
|
}
|
|
375
539
|
lastUserActivityTime = Date.now();
|
|
376
540
|
cancelPendingReminder('permission'); // Cancel permission-specific reminder
|
|
377
|
-
debugLog(`Permission replied: ${event.type} (response=${
|
|
541
|
+
debugLog(`Permission replied: ${event.type} (response=${response}) - cancelled permission reminder`);
|
|
378
542
|
}
|
|
379
543
|
|
|
380
544
|
if (event.type === "session.created") {
|
|
@@ -384,6 +548,14 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
384
548
|
activePermissionId = null;
|
|
385
549
|
seenUserMessageIds.clear();
|
|
386
550
|
cancelAllPendingReminders();
|
|
551
|
+
|
|
552
|
+
// Reset permission batch state
|
|
553
|
+
pendingPermissionBatch = [];
|
|
554
|
+
if (permissionBatchTimeout) {
|
|
555
|
+
clearTimeout(permissionBatchTimeout);
|
|
556
|
+
permissionBatchTimeout = null;
|
|
557
|
+
}
|
|
558
|
+
|
|
387
559
|
debugLog(`Session created: ${event.type} - reset all tracking state`);
|
|
388
560
|
}
|
|
389
561
|
|
|
@@ -418,42 +590,48 @@ export default async function SmartVoiceNotifyPlugin({ project, client, $, direc
|
|
|
418
590
|
}
|
|
419
591
|
|
|
420
592
|
// ========================================
|
|
421
|
-
// NOTIFICATION 2: Permission Request
|
|
593
|
+
// NOTIFICATION 2: Permission Request (BATCHED)
|
|
422
594
|
// ========================================
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
595
|
+
// NOTE: OpenCode SDK v1.1.1+ changed permission events:
|
|
596
|
+
// - Old: "permission.updated" with properties.id
|
|
597
|
+
// - New: "permission.asked" with properties.id
|
|
598
|
+
// We support both for backward compatibility.
|
|
599
|
+
//
|
|
600
|
+
// BATCHING: When multiple permissions arrive simultaneously (e.g., 5 at once),
|
|
601
|
+
// we batch them into a single notification instead of playing 5 overlapping sounds.
|
|
602
|
+
if (event.type === "permission.updated" || event.type === "permission.asked") {
|
|
603
|
+
// Capture permissionID
|
|
427
604
|
const permissionId = event.properties?.id;
|
|
428
605
|
|
|
429
606
|
if (!permissionId) {
|
|
430
|
-
debugLog(
|
|
607
|
+
debugLog(`${event.type}: permission ID missing. properties keys: ` + Object.keys(event.properties || {}).join(', '));
|
|
431
608
|
}
|
|
432
609
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
debugLog(
|
|
441
|
-
return;
|
|
610
|
+
// Add to the pending batch (avoid duplicates)
|
|
611
|
+
if (permissionId && !pendingPermissionBatch.includes(permissionId)) {
|
|
612
|
+
pendingPermissionBatch.push(permissionId);
|
|
613
|
+
debugLog(`${event.type}: added ${permissionId} to batch (now ${pendingPermissionBatch.length} pending)`);
|
|
614
|
+
} else if (!permissionId) {
|
|
615
|
+
// If no ID, still count it (use a placeholder)
|
|
616
|
+
pendingPermissionBatch.push(`unknown-${Date.now()}`);
|
|
617
|
+
debugLog(`${event.type}: added unknown permission to batch (now ${pendingPermissionBatch.length} pending)`);
|
|
442
618
|
}
|
|
443
|
-
|
|
444
|
-
// Smart notification: sound first, TTS reminder later
|
|
445
|
-
await smartNotify('permission', {
|
|
446
|
-
soundFile: config.permissionSound,
|
|
447
|
-
soundLoops: 2,
|
|
448
|
-
ttsMessage: getRandomMessage(config.permissionTTSMessages),
|
|
449
|
-
fallbackSound: config.permissionSound
|
|
450
|
-
});
|
|
451
619
|
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
620
|
+
// Reset the batch window timer (debounce)
|
|
621
|
+
// This gives more permissions a chance to arrive before we notify
|
|
622
|
+
if (permissionBatchTimeout) {
|
|
623
|
+
clearTimeout(permissionBatchTimeout);
|
|
456
624
|
}
|
|
625
|
+
|
|
626
|
+
permissionBatchTimeout = setTimeout(async () => {
|
|
627
|
+
try {
|
|
628
|
+
await processPermissionBatch();
|
|
629
|
+
} catch (e) {
|
|
630
|
+
debugLog(`processPermissionBatch error: ${e.message}`);
|
|
631
|
+
}
|
|
632
|
+
}, PERMISSION_BATCH_WINDOW_MS);
|
|
633
|
+
|
|
634
|
+
debugLog(`${event.type}: batch window reset (will process in ${PERMISSION_BATCH_WINDOW_MS}ms if no more arrive)`);
|
|
457
635
|
}
|
|
458
636
|
} catch (e) {
|
|
459
637
|
debugLog(`event handler error: ${e.message}`);
|
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.13",
|
|
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",
|
|
@@ -35,13 +35,14 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/MasuRii/opencode-smart-voice-notify#readme",
|
|
37
37
|
"engines": {
|
|
38
|
-
"node": ">=18.0.0"
|
|
38
|
+
"node": ">=18.0.0",
|
|
39
|
+
"bun": ">=1.0.0"
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
41
|
-
"@elevenlabs/elevenlabs-js": "^2.
|
|
42
|
+
"@elevenlabs/elevenlabs-js": "^2.29.0",
|
|
42
43
|
"msedge-tts": "^2.0.3"
|
|
43
44
|
},
|
|
44
45
|
"peerDependencies": {
|
|
45
|
-
"@opencode-ai/plugin": "^1.
|
|
46
|
+
"@opencode-ai/plugin": "^1.1.4"
|
|
46
47
|
}
|
|
47
48
|
}
|
package/util/config.js
CHANGED
|
@@ -189,6 +189,16 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
|
|
|
189
189
|
"Excuse me! I need your authorization before I can continue.",
|
|
190
190
|
"Permission required! Please review and approve when ready."
|
|
191
191
|
], 4)},
|
|
192
|
+
|
|
193
|
+
// Messages for MULTIPLE permission requests (use {count} placeholder)
|
|
194
|
+
// Used when several permissions arrive simultaneously
|
|
195
|
+
"permissionTTSMessagesMultiple": ${formatJSON(overrides.permissionTTSMessagesMultiple || [
|
|
196
|
+
"Attention please! There are {count} permission requests waiting for your approval.",
|
|
197
|
+
"Hey! {count} permissions need your approval to continue.",
|
|
198
|
+
"Heads up! You have {count} pending permission requests.",
|
|
199
|
+
"Excuse me! I need your authorization for {count} different actions.",
|
|
200
|
+
"{count} permissions required! Please review and approve when ready."
|
|
201
|
+
], 4)},
|
|
192
202
|
|
|
193
203
|
// ============================================================
|
|
194
204
|
// TTS REMINDER MESSAGES (More urgent - used after delay if no response)
|
|
@@ -213,6 +223,24 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
|
|
|
213
223
|
"Still waiting for authorization! The task is on hold until you respond."
|
|
214
224
|
], 4)},
|
|
215
225
|
|
|
226
|
+
// Reminder messages for MULTIPLE permissions (use {count} placeholder)
|
|
227
|
+
"permissionReminderTTSMessagesMultiple": ${formatJSON(overrides.permissionReminderTTSMessagesMultiple || [
|
|
228
|
+
"Hey! I still need your approval for {count} permissions. Please respond!",
|
|
229
|
+
"Reminder: There are {count} pending permission requests. I cannot proceed without you.",
|
|
230
|
+
"Hello? I am waiting for your approval on {count} items. This is getting urgent!",
|
|
231
|
+
"Please check your screen! {count} permissions are waiting for your response.",
|
|
232
|
+
"Still waiting for authorization on {count} requests! The task is on hold."
|
|
233
|
+
], 4)},
|
|
234
|
+
|
|
235
|
+
// ============================================================
|
|
236
|
+
// PERMISSION BATCHING (Multiple permissions at once)
|
|
237
|
+
// ============================================================
|
|
238
|
+
// When multiple permissions arrive simultaneously, batch them into one notification
|
|
239
|
+
// This prevents overlapping sounds when 5+ permissions come at once
|
|
240
|
+
|
|
241
|
+
// Batch window (ms) - how long to wait for more permissions before notifying
|
|
242
|
+
"permissionBatchWindowMs": ${overrides.permissionBatchWindowMs !== undefined ? overrides.permissionBatchWindowMs : 800},
|
|
243
|
+
|
|
216
244
|
// ============================================================
|
|
217
245
|
// SOUND FILES (For immediate notifications)
|
|
218
246
|
// These are played first before TTS reminder kicks in
|
|
@@ -246,7 +274,8 @@ const generateDefaultConfig = (overrides = {}, version = '1.0.0') => {
|
|
|
246
274
|
// Consider monitor asleep after this many seconds of inactivity (Windows only)
|
|
247
275
|
"idleThresholdSeconds": ${overrides.idleThresholdSeconds !== undefined ? overrides.idleThresholdSeconds : 60},
|
|
248
276
|
|
|
249
|
-
// Enable debug logging to ~/.config/opencode/smart-voice-notify-debug.log
|
|
277
|
+
// Enable debug logging to ~/.config/opencode/logs/smart-voice-notify-debug.log
|
|
278
|
+
// The logs folder is created automatically when debug logging is enabled
|
|
250
279
|
// Useful for troubleshooting notification issues
|
|
251
280
|
"debugLog": ${overrides.debugLog !== undefined ? overrides.debugLog : false}
|
|
252
281
|
}`;
|
package/util/tts.js
CHANGED
|
@@ -71,6 +71,14 @@ export const getTTSConfig = () => {
|
|
|
71
71
|
'Excuse me! I need your authorization before I can continue.',
|
|
72
72
|
'Permission required! Please review and approve when ready.'
|
|
73
73
|
],
|
|
74
|
+
// Messages for MULTIPLE permission requests (use {count} placeholder)
|
|
75
|
+
permissionTTSMessagesMultiple: [
|
|
76
|
+
'Attention please! There are {count} permission requests waiting for your approval.',
|
|
77
|
+
'Hey! {count} permissions need your approval to continue.',
|
|
78
|
+
'Heads up! You have {count} pending permission requests.',
|
|
79
|
+
'Excuse me! I need your authorization for {count} different actions.',
|
|
80
|
+
'{count} permissions required! Please review and approve when ready.'
|
|
81
|
+
],
|
|
74
82
|
|
|
75
83
|
// ============================================================
|
|
76
84
|
// TTS REMINDER MESSAGES (More urgent/personalized - used after delay)
|
|
@@ -91,6 +99,17 @@ export const getTTSConfig = () => {
|
|
|
91
99
|
'Please check your screen! I really need your permission to move forward.',
|
|
92
100
|
'Still waiting for authorization! The task is on hold until you respond.'
|
|
93
101
|
],
|
|
102
|
+
// Reminder messages for MULTIPLE permissions (use {count} placeholder)
|
|
103
|
+
permissionReminderTTSMessagesMultiple: [
|
|
104
|
+
'Hey! I still need your approval for {count} permissions. Please respond!',
|
|
105
|
+
'Reminder: There are {count} pending permission requests. I cannot proceed without you.',
|
|
106
|
+
'Hello? I am waiting for your approval on {count} items. This is getting urgent!',
|
|
107
|
+
'Please check your screen! {count} permissions are waiting for your response.',
|
|
108
|
+
'Still waiting for authorization on {count} requests! The task is on hold.'
|
|
109
|
+
],
|
|
110
|
+
|
|
111
|
+
// Permission batch window (ms) - how long to wait for more permissions before notifying
|
|
112
|
+
permissionBatchWindowMs: 800,
|
|
94
113
|
|
|
95
114
|
// ============================================================
|
|
96
115
|
// SOUND FILES (Used for immediate notifications)
|
|
@@ -120,7 +139,17 @@ let elevenLabsQuotaExceeded = false;
|
|
|
120
139
|
*/
|
|
121
140
|
export const createTTS = ({ $, client }) => {
|
|
122
141
|
const config = getTTSConfig();
|
|
123
|
-
const
|
|
142
|
+
const logsDir = path.join(configDir, 'logs');
|
|
143
|
+
const logFile = path.join(logsDir, 'smart-voice-notify-debug.log');
|
|
144
|
+
|
|
145
|
+
// Ensure logs directory exists if debug logging is enabled
|
|
146
|
+
if (config.debugLog && !fs.existsSync(logsDir)) {
|
|
147
|
+
try {
|
|
148
|
+
fs.mkdirSync(logsDir, { recursive: true });
|
|
149
|
+
} catch (e) {
|
|
150
|
+
// Silently fail - logging is optional
|
|
151
|
+
}
|
|
152
|
+
}
|
|
124
153
|
|
|
125
154
|
// Debug logging function (defined early so it can be passed to Linux platform)
|
|
126
155
|
const debugLog = (message) => {
|