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.
@@ -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:3000/api/has-pending-utterances 2>/dev/null)
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:3000/api/should-wait 2>/dev/null)
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
@@ -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. **Install the hooks** (first time only):
26
+ 1. **Add the MCP server**:
27
27
 
28
- ```bash
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
- This will:
33
- - Install hook scripts to `~/.mcp-voice-hooks/hooks/`
34
- - Configure your project's `.claude/settings.json`
30
+ ```bash
31
+ claude mcp add voice-hooks npx mcp-voice-hooks
32
+ ```
35
33
 
36
- 2. **Add the MCP server** to your project's `.mcp.json`:
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
- 3. **Start Claude Code**:
49
+ 2. **Start Claude Code**:
52
50
 
53
51
  ```bash
54
52
  claude
55
53
  ```
56
54
 
57
- 4. **Open the voice interface** at <http://localhost:3000> and start speaking! Note: you need to send one text message to Claude to trigger the voice hooks.
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/yourusername/mcp-voice-hooks.git
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
- NOTE: You need to restart Claude Code each time you make changes.
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
- ## WIP voice responses
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: just run the MCP server
34
+ // Default behavior: ensure hooks are installed/updated, then run the MCP server
31
35
  console.log('🎤 MCP Voice Hooks - Starting server...');
32
- console.log('💡 Note: If hooks are not installed, run: npx mcp-voice-hooks install-hooks');
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
- // Create directories if they don't exist
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
- // Update settings
143
- settings.hooks = hookConfig;
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 = 3e3) {
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 httpServer = new HttpServer(utteranceQueue);
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("- HTTP server: http://localhost:3000");
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 = 3000) {\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 httpServer = new HttpServer(utteranceQueue);\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:3000');\n console.log('- MCP server: Ready for stdio connection');\n}\n\nmain().catch(console.error);"],"mappings":"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","names":[]}
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":"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","names":[]}
@@ -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 = 3e3;
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("http://localhost:3000/api/dequeue-utterances", {
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("http://localhost:3000/api/wait-for-utterances", {
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 })