mcp-voice-hooks 1.0.1 → 1.0.6
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-tool-hook.sh +4 -1
- package/.claude/hooks/stop-hook.sh +4 -1
- package/CLAUDE.local.md +25 -0
- package/README.md +82 -13
- package/bin/cli.js +156 -5
- package/dist/hook-merger.d.ts +33 -0
- package/dist/hook-merger.js +54 -0
- package/dist/hook-merger.js.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/unified-server.js +3 -3
- package/dist/unified-server.js.map +1 -1
- package/package.json +1 -1
- package/public/app.js +1 -4
- package/test-npx-clean/mcp-voice-hooks-1.0.1.tgz +0 -0
- package/test-npx-clean/package/dist/chunk-IYGM5COW.js +12 -0
- package/test-npx-clean/package/dist/chunk-IYGM5COW.js.map +1 -0
- package/test-npx-clean/package/dist/index.d.ts +2 -0
- package/test-npx-clean/package/dist/index.js +125 -0
- package/test-npx-clean/package/dist/index.js.map +1 -0
- package/test-npx-clean/package/dist/unified-server.d.ts +1 -0
- package/test-npx-clean/package/dist/unified-server.js +352 -0
- package/test-npx-clean/package/dist/unified-server.js.map +1 -0
- package/.claude/hooks/hook-check.sh +0 -15
- /package/{mcp-voice-hooks-1.0.0.tgz → test-npx-clean/package/mcp-voice-hooks-1.0.0.tgz} +0 -0
- /package/{mcp-voice-hooks-1.0.1.tgz → test-npx-clean/package/mcp-voice-hooks-1.0.1.tgz} +0 -0
@@ -3,8 +3,11 @@
|
|
3
3
|
# Pre-Tool Hook - Checks for pending utterances before allowing tool execution
|
4
4
|
# Forces Claude to use dequeue_utterances tool if there are pending utterances
|
5
5
|
|
6
|
+
# Get port from environment variable or use default
|
7
|
+
PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
|
8
|
+
|
6
9
|
# Check has-pending-utterances endpoint
|
7
|
-
response=$(curl -s http://localhost
|
10
|
+
response=$(curl -s http://localhost:${PORT}/api/has-pending-utterances 2>/dev/null)
|
8
11
|
|
9
12
|
if [ $? -ne 0 ]; then
|
10
13
|
# Server not available, allow tool execution
|
@@ -3,8 +3,11 @@
|
|
3
3
|
# Stop Hook - Intelligently decides whether to wait for voice input
|
4
4
|
# Checks if there have been any utterances since the last timeout
|
5
5
|
|
6
|
+
# Get port from environment variable or use default
|
7
|
+
PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
|
8
|
+
|
6
9
|
# Check should-wait endpoint
|
7
|
-
response=$(curl -s http://localhost
|
10
|
+
response=$(curl -s http://localhost:${PORT}/api/should-wait 2>/dev/null)
|
8
11
|
|
9
12
|
if [ $? -ne 0 ]; then
|
10
13
|
# Server not available, allow stop
|
package/CLAUDE.local.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# To publish to npm
|
2
|
+
|
3
|
+
```bash
|
4
|
+
# 1. Build the project first
|
5
|
+
npm run build
|
6
|
+
|
7
|
+
# 2. Update roadmap.md with version info and stage it
|
8
|
+
git add roadmap.md
|
9
|
+
|
10
|
+
# 3. Bump version (patch, minor, or major) - creates a commit and tag
|
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)
|
18
|
+
npm publish --registry https://registry.npmjs.org/
|
19
|
+
|
20
|
+
# Note: It can take 1-5 minutes for the package to be available globally
|
21
|
+
# Check availability at: https://www.npmjs.com/package/mcp-voice-hooks
|
22
|
+
|
23
|
+
# Optional: Create .tgz file without publishing
|
24
|
+
npm pack
|
25
|
+
```
|
package/README.md
CHANGED
@@ -23,17 +23,15 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
23
23
|
|
24
24
|
## Installation in Your Own Project
|
25
25
|
|
26
|
-
1. **
|
26
|
+
1. **Add the MCP server**:
|
27
27
|
|
28
|
-
|
29
|
-
npx mcp-voice-hooks install-hooks
|
30
|
-
```
|
28
|
+
Run the following command to automatically add the MCP server to your current project in `~/.claude.json`:
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
```bash
|
31
|
+
claude mcp add voice-hooks npx mcp-voice-hooks
|
32
|
+
```
|
35
33
|
|
36
|
-
|
34
|
+
or manually add the following to your project's `.mcp.json`:
|
37
35
|
|
38
36
|
```json
|
39
37
|
{
|
@@ -48,13 +46,59 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
48
46
|
}
|
49
47
|
```
|
50
48
|
|
51
|
-
|
49
|
+
2. **Start Claude Code**:
|
52
50
|
|
53
51
|
```bash
|
54
52
|
claude
|
55
53
|
```
|
56
54
|
|
57
|
-
|
55
|
+
3. **Open the voice interface** at <http://localhost:5111> and start speaking!
|
56
|
+
|
57
|
+
The hooks are automatically installed when the MCP server starts. You need to send one text message to Claude to trigger the voice hooks.
|
58
|
+
|
59
|
+
**Note**: After the first-time installation, you may need to restart Claude for the hooks to take effect.
|
60
|
+
|
61
|
+
The default port is 5111. To use a different port, add to your project's `.claude/settings.json`:
|
62
|
+
|
63
|
+
```json
|
64
|
+
{
|
65
|
+
"env": {
|
66
|
+
"MCP_VOICE_HOOKS_PORT": "8080"
|
67
|
+
}
|
68
|
+
}
|
69
|
+
```
|
70
|
+
|
71
|
+
## Manual Hook Installation
|
72
|
+
|
73
|
+
The hooks are automatically installed/updated when the MCP server starts. However, if you need to manually install or reconfigure the hooks:
|
74
|
+
|
75
|
+
```bash
|
76
|
+
npx mcp-voice-hooks install-hooks
|
77
|
+
```
|
78
|
+
|
79
|
+
This will:
|
80
|
+
|
81
|
+
- Clean up any existing `~/.mcp-voice-hooks` directory contents
|
82
|
+
- Install/update hook scripts to `~/.mcp-voice-hooks/hooks/`
|
83
|
+
- Configure your project's `.claude/settings.json`
|
84
|
+
|
85
|
+
## Uninstallation
|
86
|
+
|
87
|
+
To completely remove MCP Voice Hooks:
|
88
|
+
|
89
|
+
```bash
|
90
|
+
# Remove hooks and settings
|
91
|
+
npx mcp-voice-hooks uninstall
|
92
|
+
|
93
|
+
# Also remove from Claude MCP servers
|
94
|
+
claude mcp remove voice-hooks
|
95
|
+
```
|
96
|
+
|
97
|
+
This will:
|
98
|
+
|
99
|
+
- Remove the `~/.mcp-voice-hooks` directory
|
100
|
+
- Clean up voice hooks from your project's `.claude/settings.json`
|
101
|
+
- Preserve any custom hooks you've added
|
58
102
|
|
59
103
|
## Development Mode
|
60
104
|
|
@@ -62,7 +106,7 @@ If you're developing mcp-voice-hooks itself:
|
|
62
106
|
|
63
107
|
```bash
|
64
108
|
# 1. Clone the repository
|
65
|
-
git clone https://github.com/
|
109
|
+
git clone https://github.com/johnmatthewtennant/mcp-voice-hooks.git
|
66
110
|
cd mcp-voice-hooks
|
67
111
|
|
68
112
|
# 2. Install dependencies
|
@@ -78,7 +122,12 @@ npx mcp-voice-hooks install-hooks
|
|
78
122
|
claude
|
79
123
|
```
|
80
124
|
|
81
|
-
|
125
|
+
**Important**: When developing with `npm link`:
|
126
|
+
|
127
|
+
- Claude runs the compiled JavaScript from the `dist` folder, not your TypeScript source
|
128
|
+
- After making changes to **TypeScript files** (`src/*.ts`), you must run `npm run build`
|
129
|
+
- For changes to **browser files** (`public/*`), just restart Claude Code
|
130
|
+
- Then restart Claude Code to use the updated code
|
82
131
|
|
83
132
|
### Hot Reload
|
84
133
|
|
@@ -103,7 +152,7 @@ and then configure claude to use the mcp proxy like so:
|
|
103
152
|
}
|
104
153
|
```
|
105
154
|
|
106
|
-
##
|
155
|
+
## Voice responses (Mac only)
|
107
156
|
|
108
157
|
Add the post tool hook to your claude settings:
|
109
158
|
|
@@ -145,3 +194,23 @@ Add to your Claude Code settings JSON:
|
|
145
194
|
```
|
146
195
|
|
147
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
|
+
```
|
package/bin/cli.js
CHANGED
@@ -5,6 +5,7 @@ import path from 'path';
|
|
5
5
|
import os from 'os';
|
6
6
|
import { spawn } from 'child_process';
|
7
7
|
import { fileURLToPath } from 'url';
|
8
|
+
import { replaceVoiceHooks, areHooksEqual } from '../dist/hook-merger.js';
|
8
9
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url);
|
10
11
|
const __dirname = path.dirname(__filename);
|
@@ -26,10 +27,16 @@ async function main() {
|
|
26
27
|
|
27
28
|
console.log('\n✅ Installation complete!');
|
28
29
|
console.log('📝 To start the server, run: npx mcp-voice-hooks');
|
30
|
+
} else if (command === 'uninstall') {
|
31
|
+
console.log('🗑️ Uninstalling MCP Voice Hooks...');
|
32
|
+
await uninstall();
|
29
33
|
} else {
|
30
|
-
// Default behavior:
|
34
|
+
// Default behavior: ensure hooks are installed/updated, then run the MCP server
|
31
35
|
console.log('🎤 MCP Voice Hooks - Starting server...');
|
32
|
-
|
36
|
+
|
37
|
+
// Auto-install/update hooks on every startup
|
38
|
+
await ensureHooksInstalled();
|
39
|
+
|
33
40
|
console.log('');
|
34
41
|
await runMCPServer();
|
35
42
|
}
|
@@ -46,7 +53,30 @@ async function ensureUserDirectorySetup() {
|
|
46
53
|
|
47
54
|
console.log('📁 Setting up user directory:', userDir);
|
48
55
|
|
49
|
-
//
|
56
|
+
// Clean up existing directory contents (except README.md if it exists)
|
57
|
+
if (fs.existsSync(userDir)) {
|
58
|
+
console.log('🧹 Cleaning up existing directory...');
|
59
|
+
|
60
|
+
// Remove hooks directory if it exists
|
61
|
+
if (fs.existsSync(hooksDir)) {
|
62
|
+
fs.rmSync(hooksDir, { recursive: true, force: true });
|
63
|
+
console.log('✅ Cleaned up old hooks');
|
64
|
+
}
|
65
|
+
|
66
|
+
// Remove any other files
|
67
|
+
const files = fs.readdirSync(userDir);
|
68
|
+
for (const file of files) {
|
69
|
+
const filePath = path.join(userDir, file);
|
70
|
+
const stat = fs.statSync(filePath);
|
71
|
+
if (stat.isDirectory()) {
|
72
|
+
fs.rmSync(filePath, { recursive: true, force: true });
|
73
|
+
} else {
|
74
|
+
fs.unlinkSync(filePath);
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
// Create directories
|
50
80
|
if (!fs.existsSync(userDir)) {
|
51
81
|
fs.mkdirSync(userDir, { recursive: true });
|
52
82
|
console.log('✅ Created user directory');
|
@@ -74,6 +104,15 @@ async function ensureUserDirectorySetup() {
|
|
74
104
|
} else {
|
75
105
|
console.log('⚠️ Package hooks directory not found, skipping hook installation');
|
76
106
|
}
|
107
|
+
|
108
|
+
// Copy README.md from project root to user directory
|
109
|
+
const projectReadmePath = path.join(__dirname, '..', 'README.md');
|
110
|
+
const userReadmePath = path.join(userDir, 'README.md');
|
111
|
+
|
112
|
+
if (fs.existsSync(projectReadmePath)) {
|
113
|
+
fs.copyFileSync(projectReadmePath, userReadmePath);
|
114
|
+
console.log('✅ Copied README.md');
|
115
|
+
}
|
77
116
|
}
|
78
117
|
|
79
118
|
// Automatically configure Claude Code settings
|
@@ -139,14 +178,76 @@ async function configureClaudeCodeSettings() {
|
|
139
178
|
]
|
140
179
|
};
|
141
180
|
|
142
|
-
//
|
143
|
-
settings.hooks
|
181
|
+
// Replace voice hooks intelligently
|
182
|
+
const updatedHooks = replaceVoiceHooks(settings.hooks || {}, hookConfig);
|
183
|
+
|
184
|
+
// Check if hooks actually changed (ignoring order)
|
185
|
+
if (areHooksEqual(settings.hooks || {}, updatedHooks)) {
|
186
|
+
console.log('✅ Claude settings already up to date');
|
187
|
+
return;
|
188
|
+
}
|
189
|
+
|
190
|
+
// Update settings with new hooks
|
191
|
+
settings.hooks = updatedHooks;
|
144
192
|
|
145
193
|
// Write settings back
|
146
194
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
147
195
|
console.log('✅ Updated project Claude Code settings');
|
148
196
|
}
|
149
197
|
|
198
|
+
// Silent hook installation check - runs on every startup
|
199
|
+
async function ensureHooksInstalled() {
|
200
|
+
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
201
|
+
const hooksDir = path.join(userDir, 'hooks');
|
202
|
+
const packageHooksDir = path.join(__dirname, '..', '.claude', 'hooks');
|
203
|
+
|
204
|
+
try {
|
205
|
+
// Only create directories if they don't exist
|
206
|
+
if (!fs.existsSync(hooksDir)) {
|
207
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
208
|
+
console.log('🔧 Installing hooks for first time...');
|
209
|
+
}
|
210
|
+
|
211
|
+
// Check if hooks need updating
|
212
|
+
let needsUpdate = false;
|
213
|
+
if (fs.existsSync(packageHooksDir)) {
|
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
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
if (needsUpdate) {
|
237
|
+
console.log('🔄 Updating hooks to latest version...');
|
238
|
+
await ensureUserDirectorySetup();
|
239
|
+
await configureClaudeCodeSettings();
|
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
|
+
}
|
245
|
+
} catch (error) {
|
246
|
+
// Silently continue if hooks can't be updated
|
247
|
+
console.warn('⚠️ Could not auto-update hooks:', error.message);
|
248
|
+
}
|
249
|
+
}
|
250
|
+
|
150
251
|
// Run the MCP server
|
151
252
|
async function runMCPServer() {
|
152
253
|
const serverPath = path.join(__dirname, '..', 'dist', 'unified-server.js');
|
@@ -179,6 +280,56 @@ async function runMCPServer() {
|
|
179
280
|
});
|
180
281
|
}
|
181
282
|
|
283
|
+
// Uninstall MCP Voice Hooks
|
284
|
+
async function uninstall() {
|
285
|
+
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
286
|
+
const claudeSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
|
287
|
+
|
288
|
+
// Step 1: Remove ~/.mcp-voice-hooks directory
|
289
|
+
if (fs.existsSync(userDir)) {
|
290
|
+
console.log('📁 Removing user directory:', userDir);
|
291
|
+
fs.rmSync(userDir, { recursive: true, force: true });
|
292
|
+
console.log('✅ Removed ~/.mcp-voice-hooks');
|
293
|
+
} else {
|
294
|
+
console.log('ℹ️ ~/.mcp-voice-hooks directory not found');
|
295
|
+
}
|
296
|
+
|
297
|
+
// Step 2: Remove voice hooks from Claude settings
|
298
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
299
|
+
try {
|
300
|
+
console.log('⚙️ Removing voice hooks from Claude settings...');
|
301
|
+
|
302
|
+
const settingsContent = fs.readFileSync(claudeSettingsPath, 'utf8');
|
303
|
+
const settings = JSON.parse(settingsContent);
|
304
|
+
|
305
|
+
if (settings.hooks) {
|
306
|
+
// Remove voice hooks
|
307
|
+
const cleanedHooks = removeVoiceHooks(settings.hooks);
|
308
|
+
|
309
|
+
if (Object.keys(cleanedHooks).length === 0) {
|
310
|
+
// If no hooks remain, remove the hooks property entirely
|
311
|
+
delete settings.hooks;
|
312
|
+
} else {
|
313
|
+
settings.hooks = cleanedHooks;
|
314
|
+
}
|
315
|
+
|
316
|
+
// Write updated settings
|
317
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2));
|
318
|
+
console.log('✅ Removed voice hooks from Claude settings');
|
319
|
+
} else {
|
320
|
+
console.log('ℹ️ No hooks found in Claude settings');
|
321
|
+
}
|
322
|
+
} catch (error) {
|
323
|
+
console.log('⚠️ Could not update Claude settings:', error.message);
|
324
|
+
}
|
325
|
+
} else {
|
326
|
+
console.log('ℹ️ No Claude settings file found in current project');
|
327
|
+
}
|
328
|
+
|
329
|
+
console.log('\n✅ Uninstallation complete!');
|
330
|
+
console.log('👋 MCP Voice Hooks has been removed.');
|
331
|
+
}
|
332
|
+
|
182
333
|
// Run the main function
|
183
334
|
main().catch(error => {
|
184
335
|
console.error('❌ Unexpected error:', error);
|
@@ -0,0 +1,33 @@
|
|
1
|
+
interface Hook {
|
2
|
+
type: string;
|
3
|
+
command: string;
|
4
|
+
}
|
5
|
+
interface HookConfig {
|
6
|
+
matcher: string;
|
7
|
+
hooks: Hook[];
|
8
|
+
}
|
9
|
+
interface HookSettings {
|
10
|
+
[hookType: string]: HookConfig[];
|
11
|
+
}
|
12
|
+
/**
|
13
|
+
* Removes any existing voice hooks that reference our directory
|
14
|
+
* @param hooks - The current hooks configuration
|
15
|
+
* @returns The hooks with voice hooks removed
|
16
|
+
*/
|
17
|
+
declare function removeVoiceHooks(hooks?: HookSettings): HookSettings;
|
18
|
+
/**
|
19
|
+
* Replaces voice hooks - removes any existing ones and adds new ones
|
20
|
+
* @param existingHooks - The current hooks configuration
|
21
|
+
* @param voiceHooks - The voice hooks to add
|
22
|
+
* @returns The updated hooks configuration
|
23
|
+
*/
|
24
|
+
declare function replaceVoiceHooks(existingHooks: HookSettings | undefined, voiceHooks: HookSettings): HookSettings;
|
25
|
+
/**
|
26
|
+
* Checks if two hook settings are semantically equal (ignoring order)
|
27
|
+
* @param hooks1 - First hooks configuration
|
28
|
+
* @param hooks2 - Second hooks configuration
|
29
|
+
* @returns True if they contain the same hooks regardless of order
|
30
|
+
*/
|
31
|
+
declare function areHooksEqual(hooks1?: HookSettings, hooks2?: HookSettings): boolean;
|
32
|
+
|
33
|
+
export { type HookSettings, areHooksEqual, removeVoiceHooks, replaceVoiceHooks };
|
@@ -0,0 +1,54 @@
|
|
1
|
+
// src/hook-merger.ts
|
2
|
+
function removeVoiceHooks(hooks = {}) {
|
3
|
+
const cleaned = {};
|
4
|
+
const voiceHookPattern = /\.mcp-voice-hooks/;
|
5
|
+
for (const [hookType, hookArray] of Object.entries(hooks)) {
|
6
|
+
cleaned[hookType] = hookArray.filter((hookConfig) => {
|
7
|
+
return !hookConfig.hooks.some((hook) => voiceHookPattern.test(hook.command));
|
8
|
+
});
|
9
|
+
if (cleaned[hookType].length === 0) {
|
10
|
+
delete cleaned[hookType];
|
11
|
+
}
|
12
|
+
}
|
13
|
+
return cleaned;
|
14
|
+
}
|
15
|
+
function replaceVoiceHooks(existingHooks = {}, voiceHooks) {
|
16
|
+
const cleaned = removeVoiceHooks(existingHooks);
|
17
|
+
const result = JSON.parse(JSON.stringify(cleaned));
|
18
|
+
for (const [hookType, hookArray] of Object.entries(voiceHooks)) {
|
19
|
+
if (!result[hookType]) {
|
20
|
+
result[hookType] = hookArray;
|
21
|
+
} else {
|
22
|
+
result[hookType].push(...hookArray);
|
23
|
+
}
|
24
|
+
}
|
25
|
+
return result;
|
26
|
+
}
|
27
|
+
function areHooksEqual(hooks1 = {}, hooks2 = {}) {
|
28
|
+
const types1 = Object.keys(hooks1).sort();
|
29
|
+
const types2 = Object.keys(hooks2).sort();
|
30
|
+
if (types1.join(",") !== types2.join(",")) {
|
31
|
+
return false;
|
32
|
+
}
|
33
|
+
for (const hookType of types1) {
|
34
|
+
const configs1 = hooks1[hookType];
|
35
|
+
const configs2 = hooks2[hookType];
|
36
|
+
if (configs1.length !== configs2.length) {
|
37
|
+
return false;
|
38
|
+
}
|
39
|
+
const normalized1 = configs1.map((config) => JSON.stringify(config)).sort();
|
40
|
+
const normalized2 = configs2.map((config) => JSON.stringify(config)).sort();
|
41
|
+
for (let i = 0; i < normalized1.length; i++) {
|
42
|
+
if (normalized1[i] !== normalized2[i]) {
|
43
|
+
return false;
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
return true;
|
48
|
+
}
|
49
|
+
export {
|
50
|
+
areHooksEqual,
|
51
|
+
removeVoiceHooks,
|
52
|
+
replaceVoiceHooks
|
53
|
+
};
|
54
|
+
//# sourceMappingURL=hook-merger.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["../src/hook-merger.ts"],"sourcesContent":["interface Hook {\n type: string;\n command: string;\n}\n\ninterface HookConfig {\n matcher: string;\n hooks: Hook[];\n}\n\nexport interface HookSettings {\n [hookType: string]: HookConfig[];\n}\n\n/**\n * Removes any existing voice hooks that reference our directory\n * @param hooks - The current hooks configuration\n * @returns The hooks with voice hooks removed\n */\nexport function removeVoiceHooks(hooks: HookSettings = {}): HookSettings {\n const cleaned: HookSettings = {};\n const voiceHookPattern = /\\.mcp-voice-hooks/;\n \n for (const [hookType, hookArray] of Object.entries(hooks)) {\n cleaned[hookType] = hookArray.filter(hookConfig => {\n // Keep this hook config only if none of its commands reference our directory\n return !hookConfig.hooks.some(hook => voiceHookPattern.test(hook.command));\n });\n \n // Remove empty arrays\n if (cleaned[hookType].length === 0) {\n delete cleaned[hookType];\n }\n }\n \n return cleaned;\n}\n\n/**\n * Replaces voice hooks - removes any existing ones and adds new ones\n * @param existingHooks - The current hooks configuration\n * @param voiceHooks - The voice hooks to add\n * @returns The updated hooks configuration\n */\nexport function replaceVoiceHooks(existingHooks: HookSettings = {}, voiceHooks: HookSettings): HookSettings {\n // First, remove any existing voice hooks\n const cleaned = removeVoiceHooks(existingHooks);\n \n // Then merge in the new voice hooks\n const result: HookSettings = JSON.parse(JSON.stringify(cleaned)); // Deep clone\n \n for (const [hookType, hookArray] of Object.entries(voiceHooks)) {\n if (!result[hookType]) {\n result[hookType] = hookArray;\n } else {\n result[hookType].push(...hookArray);\n }\n }\n \n return result;\n}\n\n/**\n * Checks if two hook settings are semantically equal (ignoring order)\n * @param hooks1 - First hooks configuration\n * @param hooks2 - Second hooks configuration\n * @returns True if they contain the same hooks regardless of order\n */\nexport function areHooksEqual(hooks1: HookSettings = {}, hooks2: HookSettings = {}): boolean {\n const types1 = Object.keys(hooks1).sort();\n const types2 = Object.keys(hooks2).sort();\n \n // Different hook types\n if (types1.join(',') !== types2.join(',')) {\n return false;\n }\n \n // Check each hook type\n for (const hookType of types1) {\n const configs1 = hooks1[hookType];\n const configs2 = hooks2[hookType];\n \n if (configs1.length !== configs2.length) {\n return false;\n }\n \n // Create normalized strings for comparison\n const normalized1 = configs1\n .map(config => JSON.stringify(config))\n .sort();\n const normalized2 = configs2\n .map(config => JSON.stringify(config))\n .sort();\n \n // Compare sorted arrays\n for (let i = 0; i < normalized1.length; i++) {\n if (normalized1[i] !== normalized2[i]) {\n return false;\n }\n }\n }\n \n return true;\n}"],"mappings":";AAmBO,SAAS,iBAAiB,QAAsB,CAAC,GAAiB;AACvE,QAAM,UAAwB,CAAC;AAC/B,QAAM,mBAAmB;AAEzB,aAAW,CAAC,UAAU,SAAS,KAAK,OAAO,QAAQ,KAAK,GAAG;AACzD,YAAQ,QAAQ,IAAI,UAAU,OAAO,gBAAc;AAEjD,aAAO,CAAC,WAAW,MAAM,KAAK,UAAQ,iBAAiB,KAAK,KAAK,OAAO,CAAC;AAAA,IAC3E,CAAC;AAGD,QAAI,QAAQ,QAAQ,EAAE,WAAW,GAAG;AAClC,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,kBAAkB,gBAA8B,CAAC,GAAG,YAAwC;AAE1G,QAAM,UAAU,iBAAiB,aAAa;AAG9C,QAAM,SAAuB,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC;AAE/D,aAAW,CAAC,UAAU,SAAS,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC9D,QAAI,CAAC,OAAO,QAAQ,GAAG;AACrB,aAAO,QAAQ,IAAI;AAAA,IACrB,OAAO;AACL,aAAO,QAAQ,EAAE,KAAK,GAAG,SAAS;AAAA,IACpC;AAAA,EACF;AAEA,SAAO;AACT;AAQO,SAAS,cAAc,SAAuB,CAAC,GAAG,SAAuB,CAAC,GAAY;AAC3F,QAAM,SAAS,OAAO,KAAK,MAAM,EAAE,KAAK;AACxC,QAAM,SAAS,OAAO,KAAK,MAAM,EAAE,KAAK;AAGxC,MAAI,OAAO,KAAK,GAAG,MAAM,OAAO,KAAK,GAAG,GAAG;AACzC,WAAO;AAAA,EACT;AAGA,aAAW,YAAY,QAAQ;AAC7B,UAAM,WAAW,OAAO,QAAQ;AAChC,UAAM,WAAW,OAAO,QAAQ;AAEhC,QAAI,SAAS,WAAW,SAAS,QAAQ;AACvC,aAAO;AAAA,IACT;AAGA,UAAM,cAAc,SACjB,IAAI,YAAU,KAAK,UAAU,MAAM,CAAC,EACpC,KAAK;AACR,UAAM,cAAc,SACjB,IAAI,YAAU,KAAK,UAAU,MAAM,CAAC,EACpC,KAAK;AAGR,aAAS,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;AAC3C,UAAI,YAAY,CAAC,MAAM,YAAY,CAAC,GAAG;AACrC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
|
package/dist/index.js
CHANGED
@@ -45,7 +45,7 @@ var HttpServer = class {
|
|
45
45
|
app;
|
46
46
|
utteranceQueue;
|
47
47
|
port;
|
48
|
-
constructor(utteranceQueue, port =
|
48
|
+
constructor(utteranceQueue, port = 5111) {
|
49
49
|
this.utteranceQueue = utteranceQueue;
|
50
50
|
this.port = port;
|
51
51
|
this.app = express();
|
@@ -115,10 +115,11 @@ var HttpServer = class {
|
|
115
115
|
// src/index.ts
|
116
116
|
async function main() {
|
117
117
|
const utteranceQueue = new InMemoryUtteranceQueue();
|
118
|
-
const
|
118
|
+
const port = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;
|
119
|
+
const httpServer = new HttpServer(utteranceQueue, port);
|
119
120
|
await httpServer.start();
|
120
121
|
console.log("Voice Hooks servers ready!");
|
121
|
-
console.log(
|
122
|
+
console.log(`- HTTP server: http://localhost:${port}`);
|
122
123
|
console.log("- MCP server: Ready for stdio connection");
|
123
124
|
}
|
124
125
|
main().catch(console.error);
|
package/dist/index.js.map
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":3,"sources":["../src/utterance-queue.ts","../src/http-server.ts","../src/index.ts"],"sourcesContent":["import { Utterance, UtteranceQueue } from './types.js';\nimport { randomUUID } from 'crypto';\nimport { debugLog } from './debug.js';\n\nexport class InMemoryUtteranceQueue implements UtteranceQueue {\n public utterances: Utterance[] = [];\n\n add(text: string, timestamp?: Date): Utterance {\n const utterance: Utterance = {\n id: randomUUID(),\n text: text.trim(),\n timestamp: timestamp || new Date(),\n status: 'pending'\n };\n \n this.utterances.push(utterance);\n debugLog(`[Queue] queued:\t\"${utterance.text}\"\t[id: ${utterance.id}]`);\n return utterance;\n }\n\n getRecent(limit: number = 10): Utterance[] {\n return this.utterances\n .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n .slice(0, limit);\n }\n\n markDelivered(id: string): void {\n const utterance = this.utterances.find(u => u.id === id);\n if (utterance) {\n utterance.status = 'delivered';\n debugLog(`[Queue] delivered:\t\"${utterance.text}\"\t[id: ${id}]`);\n }\n }\n\n clear(): void {\n const count = this.utterances.length;\n this.utterances = [];\n debugLog(`[Queue] Cleared ${count} utterances`);\n }\n}","import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { InMemoryUtteranceQueue } from './utterance-queue.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nexport class HttpServer {\n private app: express.Application;\n private utteranceQueue: InMemoryUtteranceQueue;\n private port: number;\n\n constructor(utteranceQueue: InMemoryUtteranceQueue, port: number =
|
1
|
+
{"version":3,"sources":["../src/utterance-queue.ts","../src/http-server.ts","../src/index.ts"],"sourcesContent":["import { Utterance, UtteranceQueue } from './types.js';\nimport { randomUUID } from 'crypto';\nimport { debugLog } from './debug.js';\n\nexport class InMemoryUtteranceQueue implements UtteranceQueue {\n public utterances: Utterance[] = [];\n\n add(text: string, timestamp?: Date): Utterance {\n const utterance: Utterance = {\n id: randomUUID(),\n text: text.trim(),\n timestamp: timestamp || new Date(),\n status: 'pending'\n };\n \n this.utterances.push(utterance);\n debugLog(`[Queue] queued:\t\"${utterance.text}\"\t[id: ${utterance.id}]`);\n return utterance;\n }\n\n getRecent(limit: number = 10): Utterance[] {\n return this.utterances\n .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n .slice(0, limit);\n }\n\n markDelivered(id: string): void {\n const utterance = this.utterances.find(u => u.id === id);\n if (utterance) {\n utterance.status = 'delivered';\n debugLog(`[Queue] delivered:\t\"${utterance.text}\"\t[id: ${id}]`);\n }\n }\n\n clear(): void {\n const count = this.utterances.length;\n this.utterances = [];\n debugLog(`[Queue] Cleared ${count} utterances`);\n }\n}","import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { InMemoryUtteranceQueue } from './utterance-queue.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nexport class HttpServer {\n private app: express.Application;\n private utteranceQueue: InMemoryUtteranceQueue;\n private port: number;\n\n constructor(utteranceQueue: InMemoryUtteranceQueue, port: number = 5111) {\n this.utteranceQueue = utteranceQueue;\n this.port = port;\n this.app = express();\n this.setupMiddleware();\n this.setupRoutes();\n }\n\n private setupMiddleware() {\n this.app.use(cors());\n this.app.use(express.json());\n this.app.use(express.static(path.join(__dirname, '..', 'public')));\n }\n\n private setupRoutes() {\n // API Routes\n this.app.post('/api/potential-utterances', (req: express.Request, res: express.Response) => {\n const { text, timestamp } = req.body;\n \n if (!text || !text.trim()) {\n res.status(400).json({ error: 'Text is required' });\n return;\n }\n\n const parsedTimestamp = timestamp ? new Date(timestamp) : undefined;\n const utterance = this.utteranceQueue.add(text, parsedTimestamp);\n res.json({\n success: true,\n utterance: {\n id: utterance.id,\n text: utterance.text,\n timestamp: utterance.timestamp,\n status: utterance.status,\n },\n });\n });\n\n this.app.get('/api/utterances', (req: express.Request, res: express.Response) => {\n const limit = parseInt(req.query.limit as string) || 10;\n const utterances = this.utteranceQueue.getRecent(limit);\n \n res.json({\n utterances: utterances.map(u => ({\n id: u.id,\n text: u.text,\n timestamp: u.timestamp,\n status: u.status,\n })),\n });\n });\n\n this.app.get('/api/utterances/status', (req: express.Request, res: express.Response) => {\n const total = this.utteranceQueue.utterances.length;\n const pending = this.utteranceQueue.utterances.filter(u => u.status === 'pending').length;\n const delivered = this.utteranceQueue.utterances.filter(u => u.status === 'delivered').length;\n\n res.json({\n total,\n pending,\n delivered,\n });\n });\n\n // Serve the browser client\n this.app.get('/', (req: express.Request, res: express.Response) => {\n res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));\n });\n }\n\n start(): Promise<void> {\n return new Promise((resolve) => {\n this.app.listen(this.port, () => {\n console.log(`HTTP Server running on http://localhost:${this.port}`);\n resolve();\n });\n });\n }\n}","import { InMemoryUtteranceQueue } from './utterance-queue.js';\nimport { HttpServer } from './http-server.js';\n\nasync function main() {\n // Shared utterance queue between HTTP and MCP servers\n const utteranceQueue = new InMemoryUtteranceQueue();\n \n // Start HTTP server for browser client\n const port = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;\n const httpServer = new HttpServer(utteranceQueue, port);\n await httpServer.start();\n \n // Note: MCP server runs separately via `npm run mcp` command\n \n console.log('Voice Hooks servers ready!');\n console.log(`- HTTP server: http://localhost:${port}`);\n console.log('- MCP server: Ready for stdio connection');\n}\n\nmain().catch(console.error);"],"mappings":";;;;;AACA,SAAS,kBAAkB;AAGpB,IAAM,yBAAN,MAAuD;AAAA,EACrD,aAA0B,CAAC;AAAA,EAElC,IAAI,MAAc,WAA6B;AAC7C,UAAM,YAAuB;AAAA,MAC3B,IAAI,WAAW;AAAA,MACf,MAAM,KAAK,KAAK;AAAA,MAChB,WAAW,aAAa,oBAAI,KAAK;AAAA,MACjC,QAAQ;AAAA,IACV;AAEA,SAAK,WAAW,KAAK,SAAS;AAC9B,aAAS,oBAAoB,UAAU,IAAI,UAAU,UAAU,EAAE,GAAG;AACpE,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,QAAgB,IAAiB;AACzC,WAAO,KAAK,WACT,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ,CAAC,EAC5D,MAAM,GAAG,KAAK;AAAA,EACnB;AAAA,EAEA,cAAc,IAAkB;AAC9B,UAAM,YAAY,KAAK,WAAW,KAAK,OAAK,EAAE,OAAO,EAAE;AACvD,QAAI,WAAW;AACb,gBAAU,SAAS;AACnB,eAAS,uBAAuB,UAAU,IAAI,UAAU,EAAE,GAAG;AAAA,IAC/D;AAAA,EACF;AAAA,EAEA,QAAc;AACZ,UAAM,QAAQ,KAAK,WAAW;AAC9B,SAAK,aAAa,CAAC;AACnB,aAAS,mBAAmB,KAAK,aAAa;AAAA,EAChD;AACF;;;ACvCA,OAAO,aAAa;AACpB,OAAO,UAAU;AACjB,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAG9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,KAAK,QAAQ,UAAU;AAElC,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,gBAAwC,OAAe,MAAM;AACvE,SAAK,iBAAiB;AACtB,SAAK,OAAO;AACZ,SAAK,MAAM,QAAQ;AACnB,SAAK,gBAAgB;AACrB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,kBAAkB;AACxB,SAAK,IAAI,IAAI,KAAK,CAAC;AACnB,SAAK,IAAI,IAAI,QAAQ,KAAK,CAAC;AAC3B,SAAK,IAAI,IAAI,QAAQ,OAAO,KAAK,KAAK,WAAW,MAAM,QAAQ,CAAC,CAAC;AAAA,EACnE;AAAA,EAEQ,cAAc;AAEpB,SAAK,IAAI,KAAK,6BAA6B,CAAC,KAAsB,QAA0B;AAC1F,YAAM,EAAE,MAAM,UAAU,IAAI,IAAI;AAEhC,UAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,GAAG;AACzB,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,mBAAmB,CAAC;AAClD;AAAA,MACF;AAEA,YAAM,kBAAkB,YAAY,IAAI,KAAK,SAAS,IAAI;AAC1D,YAAM,YAAY,KAAK,eAAe,IAAI,MAAM,eAAe;AAC/D,UAAI,KAAK;AAAA,QACP,SAAS;AAAA,QACT,WAAW;AAAA,UACT,IAAI,UAAU;AAAA,UACd,MAAM,UAAU;AAAA,UAChB,WAAW,UAAU;AAAA,UACrB,QAAQ,UAAU;AAAA,QACpB;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAED,SAAK,IAAI,IAAI,mBAAmB,CAAC,KAAsB,QAA0B;AAC/E,YAAM,QAAQ,SAAS,IAAI,MAAM,KAAe,KAAK;AACrD,YAAM,aAAa,KAAK,eAAe,UAAU,KAAK;AAEtD,UAAI,KAAK;AAAA,QACP,YAAY,WAAW,IAAI,QAAM;AAAA,UAC/B,IAAI,EAAE;AAAA,UACN,MAAM,EAAE;AAAA,UACR,WAAW,EAAE;AAAA,UACb,QAAQ,EAAE;AAAA,QACZ,EAAE;AAAA,MACJ,CAAC;AAAA,IACH,CAAC;AAED,SAAK,IAAI,IAAI,0BAA0B,CAAC,KAAsB,QAA0B;AACtF,YAAM,QAAQ,KAAK,eAAe,WAAW;AAC7C,YAAM,UAAU,KAAK,eAAe,WAAW,OAAO,OAAK,EAAE,WAAW,SAAS,EAAE;AACnF,YAAM,YAAY,KAAK,eAAe,WAAW,OAAO,OAAK,EAAE,WAAW,WAAW,EAAE;AAEvF,UAAI,KAAK;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAGD,SAAK,IAAI,IAAI,KAAK,CAAC,KAAsB,QAA0B;AACjE,UAAI,SAAS,KAAK,KAAK,WAAW,MAAM,UAAU,YAAY,CAAC;AAAA,IACjE,CAAC;AAAA,EACH;AAAA,EAEA,QAAuB;AACrB,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,WAAK,IAAI,OAAO,KAAK,MAAM,MAAM;AAC/B,gBAAQ,IAAI,2CAA2C,KAAK,IAAI,EAAE;AAClE,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;;;ACxFA,eAAe,OAAO;AAEpB,QAAM,iBAAiB,IAAI,uBAAuB;AAGlD,QAAM,OAAO,QAAQ,IAAI,uBAAuB,SAAS,QAAQ,IAAI,oBAAoB,IAAI;AAC7F,QAAM,aAAa,IAAI,WAAW,gBAAgB,IAAI;AACtD,QAAM,WAAW,MAAM;AAIvB,UAAQ,IAAI,4BAA4B;AACxC,UAAQ,IAAI,mCAAmC,IAAI,EAAE;AACrD,UAAQ,IAAI,0CAA0C;AACxD;AAEA,KAAK,EAAE,MAAM,QAAQ,KAAK;","names":[]}
|
package/dist/unified-server.js
CHANGED
@@ -208,7 +208,7 @@ app.delete("/api/utterances", (req, res) => {
|
|
208
208
|
app.get("/", (req, res) => {
|
209
209
|
res.sendFile(path.join(__dirname, "..", "public", "index.html"));
|
210
210
|
});
|
211
|
-
var HTTP_PORT =
|
211
|
+
var HTTP_PORT = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;
|
212
212
|
app.listen(HTTP_PORT, () => {
|
213
213
|
console.log(`[HTTP] Server listening on http://localhost:${HTTP_PORT}`);
|
214
214
|
console.log(`[Mode] Running in ${IS_MCP_MANAGED ? "MCP-managed" : "standalone"} mode`);
|
@@ -267,7 +267,7 @@ if (IS_MCP_MANAGED) {
|
|
267
267
|
try {
|
268
268
|
if (name === "dequeue_utterances") {
|
269
269
|
const limit = args?.limit ?? 10;
|
270
|
-
const response = await fetch(
|
270
|
+
const response = await fetch(`http://localhost:${HTTP_PORT}/api/dequeue-utterances`, {
|
271
271
|
method: "POST",
|
272
272
|
headers: { "Content-Type": "application/json" },
|
273
273
|
body: JSON.stringify({ limit })
|
@@ -301,7 +301,7 @@ ${data.utterances.reverse().map((u) => `"${u.text}" [time: ${new Date(u.timestam
|
|
301
301
|
Math.min(MAX_WAIT_TIMEOUT_SECONDS, requestedSeconds)
|
302
302
|
);
|
303
303
|
debugLog(`[MCP] Calling wait_for_utterance with ${secondsToWait}s timeout`);
|
304
|
-
const response = await fetch(
|
304
|
+
const response = await fetch(`http://localhost:${HTTP_PORT}/api/wait-for-utterances`, {
|
305
305
|
method: "POST",
|
306
306
|
headers: { "Content-Type": "application/json" },
|
307
307
|
body: JSON.stringify({ seconds_to_wait: secondsToWait })
|