mcp-voice-hooks 1.0.6 → 1.0.12
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/.claude/hooks/pre-speak-hook.sh +3 -0
- package/.claude/hooks/pre-tool-hook.sh +1 -31
- package/.claude/hooks/pre-wait-hook.sh +3 -0
- package/.claude/hooks/stop-hook.sh +1 -30
- package/CLAUDE.local.md +3 -10
- package/README.md +48 -71
- package/bin/cli.js +20 -43
- package/dist/unified-server.js +310 -53
- package/dist/unified-server.js.map +1 -1
- package/package.json +5 -3
- package/public/app.js +451 -45
- package/public/index.html +255 -61
- package/.claude/hooks/post-tool-voice-hook.sh +0 -10
@@ -1,33 +1,3 @@
|
|
1
1
|
#!/bin/bash
|
2
|
-
|
3
|
-
# Pre-Tool Hook - Checks for pending utterances before allowing tool execution
|
4
|
-
# Forces Claude to use dequeue_utterances tool if there are pending utterances
|
5
|
-
|
6
|
-
# Get port from environment variable or use default
|
7
2
|
PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
|
8
|
-
|
9
|
-
# Check has-pending-utterances endpoint
|
10
|
-
response=$(curl -s http://localhost:${PORT}/api/has-pending-utterances 2>/dev/null)
|
11
|
-
|
12
|
-
if [ $? -ne 0 ]; then
|
13
|
-
# Server not available, allow tool execution
|
14
|
-
echo '{"decision": "approve"}'
|
15
|
-
exit 0
|
16
|
-
fi
|
17
|
-
|
18
|
-
# Extract pending status
|
19
|
-
hasPending=$(echo "$response" | jq -r '.hasPending')
|
20
|
-
pendingCount=$(echo "$response" | jq -r '.pendingCount')
|
21
|
-
|
22
|
-
if [ "$hasPending" = "true" ]; then
|
23
|
-
# There are pending utterances, block tool execution
|
24
|
-
cat <<EOF
|
25
|
-
{
|
26
|
-
"decision": "block",
|
27
|
-
"reason": "There are $pendingCount pending voice utterances. Please use dequeue_utterances to process them first."
|
28
|
-
}
|
29
|
-
EOF
|
30
|
-
else
|
31
|
-
# No pending utterances, allow tool execution
|
32
|
-
echo '{"decision": "approve"}'
|
33
|
-
fi
|
3
|
+
curl -s -X POST http://localhost:${PORT}/api/hooks/pre-tool || echo '{"decision": "approve", "reason": "voice-hooks unavailable"}'
|
@@ -1,32 +1,3 @@
|
|
1
1
|
#!/bin/bash
|
2
|
-
|
3
|
-
# Stop Hook - Intelligently decides whether to wait for voice input
|
4
|
-
# Checks if there have been any utterances since the last timeout
|
5
|
-
|
6
|
-
# Get port from environment variable or use default
|
7
2
|
PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
|
8
|
-
|
9
|
-
# Check should-wait endpoint
|
10
|
-
response=$(curl -s http://localhost:${PORT}/api/should-wait 2>/dev/null)
|
11
|
-
|
12
|
-
if [ $? -ne 0 ]; then
|
13
|
-
# Server not available, allow stop
|
14
|
-
echo '{"decision": "approve"}'
|
15
|
-
exit 0
|
16
|
-
fi
|
17
|
-
|
18
|
-
# Extract shouldWait boolean
|
19
|
-
shouldWait=$(echo "$response" | jq -r '.shouldWait')
|
20
|
-
|
21
|
-
if [ "$shouldWait" = "true" ]; then
|
22
|
-
# There have been utterances since last timeout, block and ask to wait
|
23
|
-
cat <<EOF
|
24
|
-
{
|
25
|
-
"decision": "block",
|
26
|
-
"reason": "Assistant tried to end its response. Stopping is not allowed without first checking for voice input. Assistant should now use wait_for_utterance to check for voice input"
|
27
|
-
}
|
28
|
-
EOF
|
29
|
-
else
|
30
|
-
# No utterances since last timeout, allow stop
|
31
|
-
echo '{"decision": "approve", "reason": "No utterances since last timeout"}'
|
32
|
-
fi
|
3
|
+
curl -s -X POST http://localhost:${PORT}/api/hooks/stop || echo '{"decision": "approve", "reason": "voice-hooks unavailable"}'
|
package/CLAUDE.local.md
CHANGED
@@ -4,17 +4,10 @@
|
|
4
4
|
# 1. Build the project first
|
5
5
|
npm run build
|
6
6
|
|
7
|
-
# 2.
|
8
|
-
|
7
|
+
# 2. Bump version (patch, minor, or major) - creates a commit and tag
|
8
|
+
HUSKY=0 npm version patch --registry https://registry.npmjs.org/
|
9
9
|
|
10
|
-
# 3.
|
11
|
-
npm version patch --registry https://registry.npmjs.org/
|
12
|
-
|
13
|
-
# Alternative: Bump version without creating a commit/tag
|
14
|
-
# npm version patch --no-git-tag-version
|
15
|
-
# Then manually commit the changes
|
16
|
-
|
17
|
-
# 4. Publish to npm (this creates the .tgz file automatically)
|
10
|
+
# 3. Publish to npm (this creates the .tgz file automatically)
|
18
11
|
npm publish --registry https://registry.npmjs.org/
|
19
12
|
|
20
13
|
# Note: It can take 1-5 minutes for the package to be available globally
|
package/README.md
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
Real-time voice interaction for Claude Code. Speak naturally while Claude works - interrupt, redirect, or provide continuous feedback without stopping.
|
4
4
|
|
5
|
+
Optionally enable text-to-speech to have Claude speak back to you.
|
6
|
+
|
7
|
+
Mac only for now.
|
8
|
+
|
5
9
|
## Demo
|
6
10
|
|
7
11
|
[](https://youtu.be/KpkxvJ65gbM)
|
@@ -15,11 +19,11 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
15
19
|
- Using hooks to ensure Claude checks for voice input before tool use and before stopping
|
16
20
|
- Allowing natural interruptions like "No, stop that" or "Wait, try something else"
|
17
21
|
|
18
|
-
##
|
22
|
+
## Browser Compatibility
|
19
23
|
|
20
|
-
-
|
21
|
-
-
|
22
|
-
-
|
24
|
+
- ✅ **Chrome**: Full support for speech recognition and text-to-speech
|
25
|
+
- ✅ **Safari**: Full support for speech recognition and text-to-speech
|
26
|
+
- ❌ **Edge**: Speech recognition not working on Apple Silicon (language-not-supported error)
|
23
27
|
|
24
28
|
## Installation in Your Own Project
|
25
29
|
|
@@ -52,21 +56,41 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
52
56
|
claude
|
53
57
|
```
|
54
58
|
|
55
|
-
|
59
|
+
**Important**: After the first-time installation, you will need to restart Claude for the hooks to take effect. This is because the hooks are automatically installed when the MCP server starts for the first time.
|
56
60
|
|
57
|
-
|
61
|
+
3. **Open the voice interface** at <http://localhost:5111> and start speaking!
|
58
62
|
|
59
|
-
|
63
|
+
You need to send one text message to Claude to trigger the voice hooks.
|
60
64
|
|
61
|
-
|
65
|
+
## Voice responses
|
62
66
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
67
|
+
There are two options for voice responses:
|
68
|
+
|
69
|
+
1. Browser Text-to-Speech (Cloud)
|
70
|
+
2. Browser Text-to-Speech (Local)
|
71
|
+
3. Mac System Voice
|
72
|
+
|
73
|
+
### Selecting and downloading high quality System Voices (Mac only)
|
74
|
+
|
75
|
+
When "Mac System Voice" is selected, the system uses macOS's built-in `say` command.
|
76
|
+
|
77
|
+
Configure the system voice in `System Settings > Accessibility > Spoken Content > System Voice`
|
78
|
+
|
79
|
+
I recommend using a Siri voice, as they are much higher quality.
|
80
|
+
|
81
|
+
Click the info icon next to the system voice dropdown. Search for "Siri" to find the highest quality voices. You'll have to trigger a download of the voice.
|
82
|
+
|
83
|
+
It may take a while to download.
|
84
|
+
|
85
|
+
Once it's downloaded, you can select it in the system voice dropdown.
|
86
|
+
|
87
|
+
Test it with the bash command:
|
88
|
+
|
89
|
+
```bash
|
90
|
+
say "Hi, this is your mac system voice"
|
91
|
+
```
|
92
|
+
|
93
|
+
You can also download other high quality voices in the same way. Other voices will show up in the browser voice dropdown, but for Siri voices you need to set the system voice and select Mac System Voice in the browser voice dropdown.
|
70
94
|
|
71
95
|
## Manual Hook Installation
|
72
96
|
|
@@ -100,6 +124,10 @@ This will:
|
|
100
124
|
- Clean up voice hooks from your project's `.claude/settings.json`
|
101
125
|
- Preserve any custom hooks you've added
|
102
126
|
|
127
|
+
## Known Limitations
|
128
|
+
|
129
|
+
- **Intermittent Stop Hook Execution**: Claude Code's Stop hooks are not triggered consistently. Sometimes the assistant can end responses without the Stop hook being executed. I believe this is an issue with Claude Code's hook system, not with mcp-voice-hooks. When working correctly, the Stop hook should prevent the assistant from stopping without first checking for voice input.
|
130
|
+
|
103
131
|
## Development Mode
|
104
132
|
|
105
133
|
If you're developing mcp-voice-hooks itself:
|
@@ -152,65 +180,14 @@ and then configure claude to use the mcp proxy like so:
|
|
152
180
|
}
|
153
181
|
```
|
154
182
|
|
155
|
-
|
183
|
+
### Port Configuration
|
156
184
|
|
157
|
-
|
185
|
+
The default port is 5111. To use a different port, add to your project's `.claude/settings.json`:
|
158
186
|
|
159
|
-
```json
|
160
|
-
{
|
187
|
+
```json
|
161
188
|
{
|
162
|
-
"hooks": {
|
163
|
-
"PostToolUse": [
|
164
|
-
{
|
165
|
-
"matcher": "^mcp__voice-hooks__",
|
166
|
-
"hooks": [
|
167
|
-
{
|
168
|
-
"type": "command",
|
169
|
-
"command": "./.claude/hooks/post-tool-voice-hook.sh"
|
170
|
-
}
|
171
|
-
]
|
172
|
-
}
|
173
|
-
]
|
174
|
-
},
|
175
189
|
"env": {
|
176
|
-
"
|
190
|
+
"MCP_VOICE_HOOKS_PORT": "8080"
|
177
191
|
}
|
178
192
|
}
|
179
|
-
|
180
|
-
```
|
181
|
-
|
182
|
-
### Configuration
|
183
|
-
|
184
|
-
Voice responses are disabled by default. To enable them:
|
185
|
-
|
186
|
-
Add to your Claude Code settings JSON:
|
187
|
-
|
188
|
-
```json
|
189
|
-
{
|
190
|
-
"env": {
|
191
|
-
"VOICE_RESPONSES_ENABLED": "true"
|
192
|
-
}
|
193
|
-
}
|
194
|
-
```
|
195
|
-
|
196
|
-
To disable voice responses, set the value to `false` or remove the setting entirely.
|
197
|
-
|
198
|
-
### High quality voice responses
|
199
|
-
|
200
|
-
These voice responses are spoken by your Mac's system voice.
|
201
|
-
|
202
|
-
Configure in `System Settings > Accessibility > Spoken Content > System Voice`
|
203
|
-
|
204
|
-
I recommend using a Siri voice, as they are much higher quality.
|
205
|
-
|
206
|
-
Click the info icon next to the system voice dropdown. Search for "Siri" to find the highest quality voices. You'll have to trigger a download of the voice.
|
207
|
-
|
208
|
-
It may take a while to download.
|
209
|
-
|
210
|
-
Once it's downloaded, you can select it in the system voice dropdown.
|
211
|
-
|
212
|
-
Test it with the bash command:
|
213
|
-
|
214
|
-
```bash
|
215
|
-
say "Hi, this is your mac system voice"
|
216
|
-
```
|
193
|
+
```
|
package/bin/cli.js
CHANGED
@@ -163,15 +163,22 @@ async function configureClaudeCodeSettings() {
|
|
163
163
|
"command": "sh ~/.mcp-voice-hooks/hooks/pre-tool-hook.sh"
|
164
164
|
}
|
165
165
|
]
|
166
|
-
}
|
167
|
-
|
168
|
-
|
166
|
+
},
|
167
|
+
{
|
168
|
+
"matcher": "^mcp__voice-hooks__speak$",
|
169
|
+
"hooks": [
|
170
|
+
{
|
171
|
+
"type": "command",
|
172
|
+
"command": "sh ~/.mcp-voice-hooks/hooks/pre-speak-hook.sh"
|
173
|
+
}
|
174
|
+
]
|
175
|
+
},
|
169
176
|
{
|
170
|
-
"matcher": "^mcp__voice-
|
177
|
+
"matcher": "^mcp__voice-hooks__wait_for_utterance$",
|
171
178
|
"hooks": [
|
172
179
|
{
|
173
180
|
"type": "command",
|
174
|
-
"command": "sh ~/.mcp-voice-hooks/hooks/
|
181
|
+
"command": "sh ~/.mcp-voice-hooks/hooks/pre-wait-hook.sh"
|
175
182
|
}
|
176
183
|
]
|
177
184
|
}
|
@@ -199,49 +206,19 @@ async function configureClaudeCodeSettings() {
|
|
199
206
|
async function ensureHooksInstalled() {
|
200
207
|
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
201
208
|
const hooksDir = path.join(userDir, 'hooks');
|
202
|
-
const packageHooksDir = path.join(__dirname, '..', '.claude', 'hooks');
|
203
209
|
|
204
210
|
try {
|
205
|
-
|
206
|
-
if (!fs.existsSync(hooksDir)) {
|
207
|
-
fs.mkdirSync(hooksDir, { recursive: true });
|
208
|
-
console.log('🔧 Installing hooks for first time...');
|
209
|
-
}
|
211
|
+
console.log('🔄 Updating hooks to latest version...');
|
210
212
|
|
211
|
-
//
|
212
|
-
|
213
|
-
|
214
|
-
const hookFiles = fs.readdirSync(packageHooksDir).filter(file => file.endsWith('.sh'));
|
215
|
-
|
216
|
-
for (const hookFile of hookFiles) {
|
217
|
-
const sourcePath = path.join(packageHooksDir, hookFile);
|
218
|
-
const destPath = path.join(hooksDir, hookFile);
|
219
|
-
|
220
|
-
// Check if hook doesn't exist or is different
|
221
|
-
if (!fs.existsSync(destPath)) {
|
222
|
-
needsUpdate = true;
|
223
|
-
break;
|
224
|
-
}
|
225
|
-
|
226
|
-
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
227
|
-
const destContent = fs.readFileSync(destPath, 'utf8');
|
228
|
-
|
229
|
-
if (sourceContent !== destContent) {
|
230
|
-
needsUpdate = true;
|
231
|
-
break;
|
232
|
-
}
|
233
|
-
}
|
213
|
+
// Always remove and recreate the hooks directory to ensure clean state
|
214
|
+
if (fs.existsSync(hooksDir)) {
|
215
|
+
fs.rmSync(hooksDir, { recursive: true, force: true });
|
234
216
|
}
|
235
217
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
console.log('✅ Hooks and settings updated');
|
241
|
-
} else {
|
242
|
-
// Even if hooks are up to date, ensure settings are correct
|
243
|
-
await configureClaudeCodeSettings();
|
244
|
-
}
|
218
|
+
// Recreate with latest hooks
|
219
|
+
await ensureUserDirectorySetup();
|
220
|
+
await configureClaudeCodeSettings();
|
221
|
+
console.log('✅ Hooks and settings updated');
|
245
222
|
} catch (error) {
|
246
223
|
// Silently continue if hooks can't be updated
|
247
224
|
console.warn('⚠️ Could not auto-update hooks:', error.message);
|