mcp-voice-hooks 1.0.12 → 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 CHANGED
@@ -21,8 +21,8 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
21
21
 
22
22
  ## Browser Compatibility
23
23
 
24
- - ✅ **Chrome**: Full support for speech recognition and text-to-speech
25
- - **Safari**: Full support for speech recognition and text-to-speech
24
+ - ✅ **Chrome**: Full support for speech recognition, browser text-to-speech, and system text-to-speech
25
+ - ⚠️ **Safari**: Full support for speech recognition, but only system text-to-speech is supported
26
26
  - ❌ **Edge**: Speech recognition not working on Apple Silicon (language-not-supported error)
27
27
 
28
28
  ## Installation in Your Own Project
@@ -66,23 +66,18 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
66
66
 
67
67
  There are two options for voice responses:
68
68
 
69
- 1. Browser Text-to-Speech (Cloud)
70
- 2. Browser Text-to-Speech (Local)
71
- 3. Mac System Voice
69
+ 1. Browser Text-to-Speech
70
+ 2. System Text-to-Speech
72
71
 
73
72
  ### Selecting and downloading high quality System Voices (Mac only)
74
73
 
75
- When "Mac System Voice" is selected, the system uses macOS's built-in `say` command.
74
+ Mac has built-in text to speech, but high quality voices are not available by default.
76
75
 
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.
76
+ You can download high quality voices from the system voice menu: `System Settings > Accessibility > Spoken Content > System Voice`
80
77
 
81
78
  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
79
 
83
- It may take a while to download.
84
-
85
- Once it's downloaded, you can select it in the system voice dropdown.
80
+ Once it's downloaded, you can select it in the system voice menu.
86
81
 
87
82
  Test it with the bash command:
88
83
 
@@ -90,7 +85,13 @@ Test it with the bash command:
90
85
  say "Hi, this is your mac system voice"
91
86
  ```
92
87
 
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.
88
+ For Siri voices you need to set your system voice and select "Mac System Voice" in the voice-hooks browser interface.
89
+
90
+ Other downloaded voices will show up in the voice dropdown in the voice-hooks browser interface.
91
+
92
+ ### Selecting and downloading high quality Browser Voices
93
+
94
+
94
95
 
95
96
  ## Manual Hook Installation
96
97
 
@@ -100,33 +101,30 @@ The hooks are automatically installed/updated when the MCP server starts. Howeve
100
101
  npx mcp-voice-hooks install-hooks
101
102
  ```
102
103
 
103
- This will:
104
-
105
- - Clean up any existing `~/.mcp-voice-hooks` directory contents
106
- - Install/update hook scripts to `~/.mcp-voice-hooks/hooks/`
107
- - Configure your project's `.claude/settings.json`
104
+ This will configure your project's `.claude/settings.json` with the necessary hook commands.
108
105
 
109
106
  ## Uninstallation
110
107
 
111
108
  To completely remove MCP Voice Hooks:
112
109
 
113
110
  ```bash
114
- # Remove hooks and settings
115
- npx mcp-voice-hooks uninstall
116
-
117
- # Also remove from Claude MCP servers
111
+ # Remove from Claude MCP servers
118
112
  claude mcp remove voice-hooks
119
113
  ```
120
114
 
115
+ ```bash
116
+ # Also remove hooks and settings
117
+ npx mcp-voice-hooks uninstall
118
+ ```
119
+
121
120
  This will:
122
121
 
123
- - Remove the `~/.mcp-voice-hooks` directory
124
122
  - Clean up voice hooks from your project's `.claude/settings.json`
125
123
  - Preserve any custom hooks you've added
126
124
 
127
125
  ## Known Limitations
128
126
 
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.
127
+ - **Intermittent Stop Hook Execution**: Claude Code's Stop hooks are not triggered if the agent stops immediately after a tool call. This results in the assistant occasionally stopping without checking for voice input. This will be fixed in 1.0.45. [github issue](https://github.com/anthropics/claude-code/issues/3113#issuecomment-3047324928)
130
128
 
131
129
  ## Development Mode
132
130
 
package/bin/cli.js CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
- import os from 'os';
6
5
  import { spawn } from 'child_process';
7
6
  import { fileURLToPath } from 'url';
8
- import { replaceVoiceHooks, areHooksEqual } from '../dist/hook-merger.js';
7
+ import { replaceVoiceHooks, areHooksEqual, removeVoiceHooks } from '../dist/hook-merger.js';
9
8
 
10
9
  const __filename = fileURLToPath(import.meta.url);
11
10
  const __dirname = path.dirname(__filename);
@@ -19,10 +18,7 @@ async function main() {
19
18
  if (command === 'install-hooks') {
20
19
  console.log('🔧 Installing MCP Voice Hooks...');
21
20
 
22
- // Step 1: Ensure user directory exists and install/update hooks
23
- await ensureUserDirectorySetup();
24
-
25
- // Step 2: Configure Claude Code settings automatically
21
+ // Configure Claude Code settings automatically
26
22
  await configureClaudeCodeSettings();
27
23
 
28
24
  console.log('\n✅ Installation complete!');
@@ -46,74 +42,6 @@ async function main() {
46
42
  }
47
43
  }
48
44
 
49
- // Ensure ~/.mcp-voice-hooks/hooks/ directory exists and contains latest hook files
50
- async function ensureUserDirectorySetup() {
51
- const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
52
- const hooksDir = path.join(userDir, 'hooks');
53
-
54
- console.log('📁 Setting up user directory:', userDir);
55
-
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
80
- if (!fs.existsSync(userDir)) {
81
- fs.mkdirSync(userDir, { recursive: true });
82
- console.log('✅ Created user directory');
83
- }
84
-
85
- if (!fs.existsSync(hooksDir)) {
86
- fs.mkdirSync(hooksDir, { recursive: true });
87
- console.log('✅ Created hooks directory');
88
- }
89
-
90
- // Copy/update hook files from the package's .claude/hooks/ to user directory
91
- const packageHooksDir = path.join(__dirname, '..', '.claude', 'hooks');
92
-
93
- if (fs.existsSync(packageHooksDir)) {
94
- const hookFiles = fs.readdirSync(packageHooksDir).filter(file => file.endsWith('.sh'));
95
-
96
- for (const hookFile of hookFiles) {
97
- const sourcePath = path.join(packageHooksDir, hookFile);
98
- const destPath = path.join(hooksDir, hookFile);
99
-
100
- // Copy hook file
101
- fs.copyFileSync(sourcePath, destPath);
102
- console.log(`✅ Updated hook: ${hookFile}`);
103
- }
104
- } else {
105
- console.log('⚠️ Package hooks directory not found, skipping hook installation');
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
- }
116
- }
117
45
 
118
46
  // Automatically configure Claude Code settings
119
47
  async function configureClaudeCodeSettings() {
@@ -141,7 +69,7 @@ async function configureClaudeCodeSettings() {
141
69
  }
142
70
  }
143
71
 
144
- // Add hook configuration
72
+ // Add hook configuration with inline commands
145
73
  const hookConfig = {
146
74
  "Stop": [
147
75
  {
@@ -149,7 +77,7 @@ async function configureClaudeCodeSettings() {
149
77
  "hooks": [
150
78
  {
151
79
  "type": "command",
152
- "command": "sh ~/.mcp-voice-hooks/hooks/stop-hook.sh"
80
+ "command": "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/stop\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks unavailable\"}'"
153
81
  }
154
82
  ]
155
83
  }
@@ -160,7 +88,7 @@ async function configureClaudeCodeSettings() {
160
88
  "hooks": [
161
89
  {
162
90
  "type": "command",
163
- "command": "sh ~/.mcp-voice-hooks/hooks/pre-tool-hook.sh"
91
+ "command": "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-tool\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks unavailable\"}'"
164
92
  }
165
93
  ]
166
94
  },
@@ -169,7 +97,7 @@ async function configureClaudeCodeSettings() {
169
97
  "hooks": [
170
98
  {
171
99
  "type": "command",
172
- "command": "sh ~/.mcp-voice-hooks/hooks/pre-speak-hook.sh"
100
+ "command": "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-speak\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks unavailable\"}'"
173
101
  }
174
102
  ]
175
103
  },
@@ -178,7 +106,7 @@ async function configureClaudeCodeSettings() {
178
106
  "hooks": [
179
107
  {
180
108
  "type": "command",
181
- "command": "sh ~/.mcp-voice-hooks/hooks/pre-wait-hook.sh"
109
+ "command": "curl -s -X POST \"http://localhost:${MCP_VOICE_HOOKS_PORT:-5111}/api/hooks/pre-wait\" || echo '{\"decision\": \"approve\", \"reason\": \"voice-hooks unavailable\"}'"
182
110
  }
183
111
  ]
184
112
  }
@@ -204,19 +132,10 @@ async function configureClaudeCodeSettings() {
204
132
 
205
133
  // Silent hook installation check - runs on every startup
206
134
  async function ensureHooksInstalled() {
207
- const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
208
- const hooksDir = path.join(userDir, 'hooks');
209
-
210
135
  try {
211
136
  console.log('🔄 Updating hooks to latest version...');
212
137
 
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 });
216
- }
217
-
218
- // Recreate with latest hooks
219
- await ensureUserDirectorySetup();
138
+ // Update hooks configuration in settings.json
220
139
  await configureClaudeCodeSettings();
221
140
  console.log('✅ Hooks and settings updated');
222
141
  } catch (error) {
@@ -259,19 +178,9 @@ async function runMCPServer() {
259
178
 
260
179
  // Uninstall MCP Voice Hooks
261
180
  async function uninstall() {
262
- const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
263
181
  const claudeSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
264
182
 
265
- // Step 1: Remove ~/.mcp-voice-hooks directory
266
- if (fs.existsSync(userDir)) {
267
- console.log('📁 Removing user directory:', userDir);
268
- fs.rmSync(userDir, { recursive: true, force: true });
269
- console.log('✅ Removed ~/.mcp-voice-hooks');
270
- } else {
271
- console.log('ℹ️ ~/.mcp-voice-hooks directory not found');
272
- }
273
-
274
- // Step 2: Remove voice hooks from Claude settings
183
+ // Remove voice hooks from Claude settings
275
184
  if (fs.existsSync(claudeSettingsPath)) {
276
185
  try {
277
186
  console.log('⚙️ Removing voice hooks from Claude settings...');
@@ -303,6 +212,7 @@ async function uninstall() {
303
212
  console.log('ℹ️ No Claude settings file found in current project');
304
213
  }
305
214
 
215
+
306
216
  console.log('\n✅ Uninstallation complete!');
307
217
  console.log('👋 MCP Voice Hooks has been removed.');
308
218
  }
@@ -10,7 +10,7 @@ interface HookSettings {
10
10
  [hookType: string]: HookConfig[];
11
11
  }
12
12
  /**
13
- * Removes any existing voice hooks that reference our directory
13
+ * Removes any existing voice hooks that use MCP_VOICE_HOOKS_PORT
14
14
  * @param hooks - The current hooks configuration
15
15
  * @returns The hooks with voice hooks removed
16
16
  */
@@ -1,7 +1,7 @@
1
1
  // src/hook-merger.ts
2
2
  function removeVoiceHooks(hooks = {}) {
3
3
  const cleaned = {};
4
- const voiceHookPattern = /\.mcp-voice-hooks/;
4
+ const voiceHookPattern = /MCP_VOICE_HOOKS_PORT/;
5
5
  for (const [hookType, hookArray] of Object.entries(hooks)) {
6
6
  cleaned[hookType] = hookArray.filter((hookConfig) => {
7
7
  return !hookConfig.hooks.some((hook) => voiceHookPattern.test(hook.command));
@@ -1 +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":[]}
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 use MCP_VOICE_HOOKS_PORT\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 // Pattern to match voice hooks by the unique environment variable\n const voiceHookPattern = /MCP_VOICE_HOOKS_PORT/;\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 match our pattern\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;AAE/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-voice-hooks",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,3 +0,0 @@
1
- #!/bin/bash
2
- PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
3
- curl -s -X POST http://localhost:${PORT}/api/hooks/pre-speak || echo '{"decision": "approve", "reason": "voice-hooks unavailable"}'
@@ -1,3 +0,0 @@
1
- #!/bin/bash
2
- PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
3
- curl -s -X POST http://localhost:${PORT}/api/hooks/pre-tool || echo '{"decision": "approve", "reason": "voice-hooks unavailable"}'
@@ -1,3 +0,0 @@
1
- #!/bin/bash
2
- PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
3
- curl -s -X POST http://localhost:${PORT}/api/hooks/pre-wait || echo '{"decision": "approve", "reason": "voice-hooks unavailable"}'
@@ -1,3 +0,0 @@
1
- #!/bin/bash
2
- PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
3
- curl -s -X POST http://localhost:${PORT}/api/hooks/stop || echo '{"decision": "approve", "reason": "voice-hooks unavailable"}'
package/CLAUDE.local.md DELETED
@@ -1,18 +0,0 @@
1
- # To publish to npm
2
-
3
- ```bash
4
- # 1. Build the project first
5
- npm run build
6
-
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
-
10
- # 3. Publish to npm (this creates the .tgz file automatically)
11
- npm publish --registry https://registry.npmjs.org/
12
-
13
- # Note: It can take 1-5 minutes for the package to be available globally
14
- # Check availability at: https://www.npmjs.com/package/mcp-voice-hooks
15
-
16
- # Optional: Create .tgz file without publishing
17
- npm pack
18
- ```
@@ -1,12 +0,0 @@
1
- // src/debug.ts
2
- var DEBUG = process.env.DEBUG === "true" || process.env.VOICE_HOOKS_DEBUG === "true";
3
- function debugLog(...args) {
4
- if (DEBUG) {
5
- console.log(...args);
6
- }
7
- }
8
-
9
- export {
10
- debugLog
11
- };
12
- //# sourceMappingURL=chunk-IYGM5COW.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/debug.ts"],"sourcesContent":["const DEBUG = process.env.DEBUG === 'true' || process.env.VOICE_HOOKS_DEBUG === 'true';\n\nexport function debugLog(...args: any[]): void {\n if (DEBUG) {\n console.log(...args);\n }\n}"],"mappings":";AAAA,IAAM,QAAQ,QAAQ,IAAI,UAAU,UAAU,QAAQ,IAAI,sBAAsB;AAEzE,SAAS,YAAY,MAAmB;AAC7C,MAAI,OAAO;AACT,YAAQ,IAAI,GAAG,IAAI;AAAA,EACrB;AACF;","names":[]}
@@ -1,2 +0,0 @@
1
-
2
- export { }
@@ -1,125 +0,0 @@
1
- import {
2
- debugLog
3
- } from "./chunk-IYGM5COW.js";
4
-
5
- // src/utterance-queue.ts
6
- import { randomUUID } from "crypto";
7
- var InMemoryUtteranceQueue = class {
8
- utterances = [];
9
- add(text, timestamp) {
10
- const utterance = {
11
- id: randomUUID(),
12
- text: text.trim(),
13
- timestamp: timestamp || /* @__PURE__ */ new Date(),
14
- status: "pending"
15
- };
16
- this.utterances.push(utterance);
17
- debugLog(`[Queue] queued: "${utterance.text}" [id: ${utterance.id}]`);
18
- return utterance;
19
- }
20
- getRecent(limit = 10) {
21
- return this.utterances.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, limit);
22
- }
23
- markDelivered(id) {
24
- const utterance = this.utterances.find((u) => u.id === id);
25
- if (utterance) {
26
- utterance.status = "delivered";
27
- debugLog(`[Queue] delivered: "${utterance.text}" [id: ${id}]`);
28
- }
29
- }
30
- clear() {
31
- const count = this.utterances.length;
32
- this.utterances = [];
33
- debugLog(`[Queue] Cleared ${count} utterances`);
34
- }
35
- };
36
-
37
- // src/http-server.ts
38
- import express from "express";
39
- import cors from "cors";
40
- import path from "path";
41
- import { fileURLToPath } from "url";
42
- var __filename = fileURLToPath(import.meta.url);
43
- var __dirname = path.dirname(__filename);
44
- var HttpServer = class {
45
- app;
46
- utteranceQueue;
47
- port;
48
- constructor(utteranceQueue, port = 3e3) {
49
- this.utteranceQueue = utteranceQueue;
50
- this.port = port;
51
- this.app = express();
52
- this.setupMiddleware();
53
- this.setupRoutes();
54
- }
55
- setupMiddleware() {
56
- this.app.use(cors());
57
- this.app.use(express.json());
58
- this.app.use(express.static(path.join(__dirname, "..", "public")));
59
- }
60
- setupRoutes() {
61
- this.app.post("/api/potential-utterances", (req, res) => {
62
- const { text, timestamp } = req.body;
63
- if (!text || !text.trim()) {
64
- res.status(400).json({ error: "Text is required" });
65
- return;
66
- }
67
- const parsedTimestamp = timestamp ? new Date(timestamp) : void 0;
68
- const utterance = this.utteranceQueue.add(text, parsedTimestamp);
69
- res.json({
70
- success: true,
71
- utterance: {
72
- id: utterance.id,
73
- text: utterance.text,
74
- timestamp: utterance.timestamp,
75
- status: utterance.status
76
- }
77
- });
78
- });
79
- this.app.get("/api/utterances", (req, res) => {
80
- const limit = parseInt(req.query.limit) || 10;
81
- const utterances = this.utteranceQueue.getRecent(limit);
82
- res.json({
83
- utterances: utterances.map((u) => ({
84
- id: u.id,
85
- text: u.text,
86
- timestamp: u.timestamp,
87
- status: u.status
88
- }))
89
- });
90
- });
91
- this.app.get("/api/utterances/status", (req, res) => {
92
- const total = this.utteranceQueue.utterances.length;
93
- const pending = this.utteranceQueue.utterances.filter((u) => u.status === "pending").length;
94
- const delivered = this.utteranceQueue.utterances.filter((u) => u.status === "delivered").length;
95
- res.json({
96
- total,
97
- pending,
98
- delivered
99
- });
100
- });
101
- this.app.get("/", (req, res) => {
102
- res.sendFile(path.join(__dirname, "..", "public", "index.html"));
103
- });
104
- }
105
- start() {
106
- return new Promise((resolve) => {
107
- this.app.listen(this.port, () => {
108
- console.log(`HTTP Server running on http://localhost:${this.port}`);
109
- resolve();
110
- });
111
- });
112
- }
113
- };
114
-
115
- // src/index.ts
116
- async function main() {
117
- const utteranceQueue = new InMemoryUtteranceQueue();
118
- const httpServer = new HttpServer(utteranceQueue);
119
- await httpServer.start();
120
- console.log("Voice Hooks servers ready!");
121
- console.log("- HTTP server: http://localhost:3000");
122
- console.log("- MCP server: Ready for stdio connection");
123
- }
124
- main().catch(console.error);
125
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
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 +0,0 @@
1
- #!/usr/bin/env node
@@ -1,352 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- debugLog
4
- } from "./chunk-IYGM5COW.js";
5
-
6
- // src/unified-server.ts
7
- import express from "express";
8
- import cors from "cors";
9
- import path from "path";
10
- import { fileURLToPath } from "url";
11
- import { randomUUID } from "crypto";
12
- import { exec } from "child_process";
13
- import { promisify } from "util";
14
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
- import {
17
- CallToolRequestSchema,
18
- ListToolsRequestSchema
19
- } from "@modelcontextprotocol/sdk/types.js";
20
- var __filename = fileURLToPath(import.meta.url);
21
- var __dirname = path.dirname(__filename);
22
- var DEFAULT_WAIT_TIMEOUT_SECONDS = 30;
23
- var MIN_WAIT_TIMEOUT_SECONDS = 30;
24
- var MAX_WAIT_TIMEOUT_SECONDS = 60;
25
- var execAsync = promisify(exec);
26
- async function playNotificationSound() {
27
- try {
28
- await execAsync("afplay /System/Library/Sounds/Funk.aiff");
29
- debugLog("[Sound] Played notification sound");
30
- } catch (error) {
31
- debugLog(`[Sound] Failed to play sound: ${error}`);
32
- }
33
- }
34
- var UtteranceQueue = class {
35
- utterances = [];
36
- add(text, timestamp) {
37
- const utterance = {
38
- id: randomUUID(),
39
- text: text.trim(),
40
- timestamp: timestamp || /* @__PURE__ */ new Date(),
41
- status: "pending"
42
- };
43
- this.utterances.push(utterance);
44
- debugLog(`[Queue] queued: "${utterance.text}" [id: ${utterance.id}]`);
45
- return utterance;
46
- }
47
- getRecent(limit = 10) {
48
- return this.utterances.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, limit);
49
- }
50
- markDelivered(id) {
51
- const utterance = this.utterances.find((u) => u.id === id);
52
- if (utterance) {
53
- utterance.status = "delivered";
54
- debugLog(`[Queue] delivered: "${utterance.text}" [id: ${id}]`);
55
- }
56
- }
57
- clear() {
58
- const count = this.utterances.length;
59
- this.utterances = [];
60
- debugLog(`[Queue] Cleared ${count} utterances`);
61
- }
62
- };
63
- var IS_MCP_MANAGED = process.argv.includes("--mcp-managed");
64
- var queue = new UtteranceQueue();
65
- var lastTimeoutTimestamp = null;
66
- var app = express();
67
- app.use(cors());
68
- app.use(express.json());
69
- app.use(express.static(path.join(__dirname, "..", "public")));
70
- app.post("/api/potential-utterances", (req, res) => {
71
- const { text, timestamp } = req.body;
72
- if (!text || !text.trim()) {
73
- res.status(400).json({ error: "Text is required" });
74
- return;
75
- }
76
- const parsedTimestamp = timestamp ? new Date(timestamp) : void 0;
77
- const utterance = queue.add(text, parsedTimestamp);
78
- res.json({
79
- success: true,
80
- utterance: {
81
- id: utterance.id,
82
- text: utterance.text,
83
- timestamp: utterance.timestamp,
84
- status: utterance.status
85
- }
86
- });
87
- });
88
- app.get("/api/utterances", (req, res) => {
89
- const limit = parseInt(req.query.limit) || 10;
90
- const utterances = queue.getRecent(limit);
91
- res.json({
92
- utterances: utterances.map((u) => ({
93
- id: u.id,
94
- text: u.text,
95
- timestamp: u.timestamp,
96
- status: u.status
97
- }))
98
- });
99
- });
100
- app.get("/api/utterances/status", (req, res) => {
101
- const total = queue.utterances.length;
102
- const pending = queue.utterances.filter((u) => u.status === "pending").length;
103
- const delivered = queue.utterances.filter((u) => u.status === "delivered").length;
104
- res.json({
105
- total,
106
- pending,
107
- delivered
108
- });
109
- });
110
- app.post("/api/dequeue-utterances", (req, res) => {
111
- const { limit = 10 } = req.body;
112
- const pendingUtterances = queue.utterances.filter((u) => u.status === "pending").sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()).slice(0, limit);
113
- pendingUtterances.forEach((u) => {
114
- queue.markDelivered(u.id);
115
- });
116
- res.json({
117
- success: true,
118
- utterances: pendingUtterances.map((u) => ({
119
- text: u.text,
120
- timestamp: u.timestamp
121
- }))
122
- });
123
- });
124
- app.post("/api/wait-for-utterances", async (req, res) => {
125
- const { seconds_to_wait = DEFAULT_WAIT_TIMEOUT_SECONDS } = req.body;
126
- const secondsToWait = Math.max(
127
- MIN_WAIT_TIMEOUT_SECONDS,
128
- Math.min(MAX_WAIT_TIMEOUT_SECONDS, seconds_to_wait)
129
- );
130
- const maxWaitMs = secondsToWait * 1e3;
131
- const startTime = Date.now();
132
- debugLog(`[Server] Starting wait_for_utterance (${secondsToWait}s)`);
133
- if (lastTimeoutTimestamp) {
134
- const hasNewUtterances = queue.utterances.some(
135
- (u) => u.timestamp > lastTimeoutTimestamp
136
- );
137
- if (!hasNewUtterances) {
138
- debugLog("[Server] No new utterances since last timeout, returning immediately");
139
- res.json({
140
- success: true,
141
- utterances: [],
142
- message: `No utterances found after waiting ${secondsToWait} seconds.`,
143
- waitTime: 0
144
- });
145
- return;
146
- }
147
- }
148
- let firstTime = true;
149
- while (Date.now() - startTime < maxWaitMs) {
150
- const pendingUtterances = queue.utterances.filter(
151
- (u) => u.status === "pending" && (!lastTimeoutTimestamp || u.timestamp > lastTimeoutTimestamp)
152
- );
153
- if (pendingUtterances.length > 0) {
154
- lastTimeoutTimestamp = null;
155
- const sortedUtterances = pendingUtterances.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
156
- sortedUtterances.forEach((u) => {
157
- queue.markDelivered(u.id);
158
- });
159
- res.json({
160
- success: true,
161
- utterances: sortedUtterances.map((u) => ({
162
- id: u.id,
163
- text: u.text,
164
- timestamp: u.timestamp,
165
- status: "delivered"
166
- // They are now delivered
167
- })),
168
- count: pendingUtterances.length,
169
- waitTime: Date.now() - startTime
170
- });
171
- return;
172
- }
173
- if (firstTime) {
174
- firstTime = false;
175
- await playNotificationSound();
176
- }
177
- await new Promise((resolve) => setTimeout(resolve, 100));
178
- }
179
- lastTimeoutTimestamp = /* @__PURE__ */ new Date();
180
- res.json({
181
- success: true,
182
- utterances: [],
183
- message: `No utterances found after waiting ${secondsToWait} seconds.`,
184
- waitTime: maxWaitMs
185
- });
186
- });
187
- app.get("/api/should-wait", (req, res) => {
188
- const shouldWait = !lastTimeoutTimestamp || queue.utterances.some((u) => u.timestamp > lastTimeoutTimestamp);
189
- res.json({ shouldWait });
190
- });
191
- app.get("/api/has-pending-utterances", (req, res) => {
192
- const pendingCount = queue.utterances.filter((u) => u.status === "pending").length;
193
- const hasPending = pendingCount > 0;
194
- res.json({
195
- hasPending,
196
- pendingCount
197
- });
198
- });
199
- app.delete("/api/utterances", (req, res) => {
200
- const clearedCount = queue.utterances.length;
201
- queue.clear();
202
- res.json({
203
- success: true,
204
- message: `Cleared ${clearedCount} utterances`,
205
- clearedCount
206
- });
207
- });
208
- app.get("/", (req, res) => {
209
- res.sendFile(path.join(__dirname, "..", "public", "index.html"));
210
- });
211
- var HTTP_PORT = 3e3;
212
- app.listen(HTTP_PORT, () => {
213
- console.log(`[HTTP] Server listening on http://localhost:${HTTP_PORT}`);
214
- console.log(`[Mode] Running in ${IS_MCP_MANAGED ? "MCP-managed" : "standalone"} mode`);
215
- });
216
- if (IS_MCP_MANAGED) {
217
- console.log("[MCP] Initializing MCP server...");
218
- const mcpServer = new Server(
219
- {
220
- name: "voice-hooks",
221
- version: "1.0.0"
222
- },
223
- {
224
- capabilities: {
225
- tools: {}
226
- }
227
- }
228
- );
229
- mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
230
- return {
231
- tools: [
232
- {
233
- name: "dequeue_utterances",
234
- description: "Dequeue pending utterances and mark them as delivered",
235
- inputSchema: {
236
- type: "object",
237
- properties: {
238
- limit: {
239
- type: "number",
240
- description: "Maximum number of utterances to dequeue (default: 10)",
241
- default: 10
242
- }
243
- }
244
- }
245
- },
246
- {
247
- name: "wait_for_utterance",
248
- description: "Wait for an utterance to be available or until timeout. Returns immediately if no utterances since last timeout.",
249
- inputSchema: {
250
- type: "object",
251
- properties: {
252
- seconds_to_wait: {
253
- type: "number",
254
- description: `Maximum seconds to wait for an utterance (default: ${DEFAULT_WAIT_TIMEOUT_SECONDS}, min: ${MIN_WAIT_TIMEOUT_SECONDS}, max: ${MAX_WAIT_TIMEOUT_SECONDS})`,
255
- default: DEFAULT_WAIT_TIMEOUT_SECONDS,
256
- minimum: MIN_WAIT_TIMEOUT_SECONDS,
257
- maximum: MAX_WAIT_TIMEOUT_SECONDS
258
- }
259
- }
260
- }
261
- }
262
- ]
263
- };
264
- });
265
- mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
266
- const { name, arguments: args } = request.params;
267
- try {
268
- if (name === "dequeue_utterances") {
269
- const limit = args?.limit ?? 10;
270
- const response = await fetch("http://localhost:3000/api/dequeue-utterances", {
271
- method: "POST",
272
- headers: { "Content-Type": "application/json" },
273
- body: JSON.stringify({ limit })
274
- });
275
- const data = await response.json();
276
- if (data.utterances.length === 0) {
277
- return {
278
- content: [
279
- {
280
- type: "text",
281
- text: "No recent utterances found."
282
- }
283
- ]
284
- };
285
- }
286
- return {
287
- content: [
288
- {
289
- type: "text",
290
- text: `Dequeued ${data.utterances.length} utterance(s):
291
-
292
- ${data.utterances.reverse().map((u) => `"${u.text}" [time: ${new Date(u.timestamp).toISOString()}]`).join("\n")}`
293
- }
294
- ]
295
- };
296
- }
297
- if (name === "wait_for_utterance") {
298
- const requestedSeconds = args?.seconds_to_wait ?? DEFAULT_WAIT_TIMEOUT_SECONDS;
299
- const secondsToWait = Math.max(
300
- MIN_WAIT_TIMEOUT_SECONDS,
301
- Math.min(MAX_WAIT_TIMEOUT_SECONDS, requestedSeconds)
302
- );
303
- debugLog(`[MCP] Calling wait_for_utterance with ${secondsToWait}s timeout`);
304
- const response = await fetch("http://localhost:3000/api/wait-for-utterances", {
305
- method: "POST",
306
- headers: { "Content-Type": "application/json" },
307
- body: JSON.stringify({ seconds_to_wait: secondsToWait })
308
- });
309
- const data = await response.json();
310
- if (data.utterances && data.utterances.length > 0) {
311
- const utteranceTexts = data.utterances.map((u) => `[${u.timestamp}] "${u.text}"`).join("\n");
312
- return {
313
- content: [
314
- {
315
- type: "text",
316
- text: `Found ${data.count} utterance(s):
317
-
318
- ${utteranceTexts}`
319
- }
320
- ]
321
- };
322
- } else {
323
- return {
324
- content: [
325
- {
326
- type: "text",
327
- text: data.message || `No utterances found after waiting ${secondsToWait} seconds.`
328
- }
329
- ]
330
- };
331
- }
332
- }
333
- throw new Error(`Unknown tool: ${name}`);
334
- } catch (error) {
335
- return {
336
- content: [
337
- {
338
- type: "text",
339
- text: `Error: ${error instanceof Error ? error.message : String(error)}`
340
- }
341
- ],
342
- isError: true
343
- };
344
- }
345
- });
346
- const transport = new StdioServerTransport();
347
- mcpServer.connect(transport);
348
- console.log("[MCP] Server connected via stdio");
349
- } else {
350
- console.log("[MCP] Skipping MCP server initialization (not in MCP-managed mode)");
351
- }
352
- //# sourceMappingURL=unified-server.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/unified-server.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport express from 'express';\nimport type { Request, Response } from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { randomUUID } from 'crypto';\nimport { exec } from 'child_process';\nimport { promisify } from 'util';\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { debugLog } from './debug.ts';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\n// Constants\nconst DEFAULT_WAIT_TIMEOUT_SECONDS = 30;\nconst MIN_WAIT_TIMEOUT_SECONDS = 30;\nconst MAX_WAIT_TIMEOUT_SECONDS = 60;\n\n// Promisified exec for async/await\nconst execAsync = promisify(exec);\n\n// Function to play a sound notification\nasync function playNotificationSound() {\n try {\n // Use macOS system sound\n await execAsync('afplay /System/Library/Sounds/Funk.aiff');\n debugLog('[Sound] Played notification sound');\n } catch (error) {\n debugLog(`[Sound] Failed to play sound: ${error}`);\n // Don't throw - sound is not critical\n }\n}\n\n// Shared utterance queue\ninterface Utterance {\n id: string;\n text: string;\n timestamp: Date;\n status: 'pending' | 'delivered';\n}\n\nclass UtteranceQueue {\n 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: \"${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: \"${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}\n\n// Determine if we're running in MCP-managed mode\nconst IS_MCP_MANAGED = process.argv.includes('--mcp-managed');\n\n// Global state\nconst queue = new UtteranceQueue();\nlet lastTimeoutTimestamp: Date | null = null;\n\n// HTTP Server Setup (always created)\nconst app = express();\napp.use(cors());\napp.use(express.json());\napp.use(express.static(path.join(__dirname, '..', 'public')));\n\n// API Routes\napp.post('/api/potential-utterances', (req: Request, res: 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 = queue.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\napp.get('/api/utterances', (req: Request, res: Response) => {\n const limit = parseInt(req.query.limit as string) || 10;\n const utterances = queue.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\napp.get('/api/utterances/status', (req: Request, res: Response) => {\n const total = queue.utterances.length;\n const pending = queue.utterances.filter(u => u.status === 'pending').length;\n const delivered = queue.utterances.filter(u => u.status === 'delivered').length;\n\n res.json({\n total,\n pending,\n delivered,\n });\n});\n\n// MCP server integration\napp.post('/api/dequeue-utterances', (req: Request, res: Response) => {\n const { limit = 10 } = req.body;\n const pendingUtterances = queue.utterances\n .filter(u => u.status === 'pending')\n .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n .slice(0, limit);\n\n // Mark as delivered\n pendingUtterances.forEach(u => {\n queue.markDelivered(u.id);\n });\n\n res.json({\n success: true,\n utterances: pendingUtterances.map(u => ({\n text: u.text,\n timestamp: u.timestamp,\n })),\n });\n});\n\n// Wait for utterance endpoint\napp.post('/api/wait-for-utterances', async (req: Request, res: Response) => {\n const { seconds_to_wait = DEFAULT_WAIT_TIMEOUT_SECONDS } = req.body;\n const secondsToWait = Math.max(\n MIN_WAIT_TIMEOUT_SECONDS,\n Math.min(MAX_WAIT_TIMEOUT_SECONDS, seconds_to_wait)\n );\n const maxWaitMs = secondsToWait * 1000;\n const startTime = Date.now();\n\n debugLog(`[Server] Starting wait_for_utterance (${secondsToWait}s)`);\n\n // Check if we should return immediately\n if (lastTimeoutTimestamp) {\n const hasNewUtterances = queue.utterances.some(\n u => u.timestamp > lastTimeoutTimestamp!\n );\n if (!hasNewUtterances) {\n debugLog('[Server] No new utterances since last timeout, returning immediately');\n res.json({\n success: true,\n utterances: [],\n message: `No utterances found after waiting ${secondsToWait} seconds.`,\n waitTime: 0,\n });\n return;\n }\n }\n\n let firstTime = true;\n\n // Poll for utterances\n while (Date.now() - startTime < maxWaitMs) {\n const pendingUtterances = queue.utterances.filter(\n u => u.status === 'pending' &&\n (!lastTimeoutTimestamp || u.timestamp > lastTimeoutTimestamp)\n );\n\n if (pendingUtterances.length > 0) {\n // Found utterances - clear lastTimeoutTimestamp\n lastTimeoutTimestamp = null;\n\n // Sort by timestamp (oldest first)\n const sortedUtterances = pendingUtterances\n .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());\n\n // Mark utterances as delivered\n sortedUtterances.forEach(u => {\n queue.markDelivered(u.id);\n });\n\n res.json({\n success: true,\n utterances: sortedUtterances.map(u => ({\n id: u.id,\n text: u.text,\n timestamp: u.timestamp,\n status: 'delivered', // They are now delivered\n })),\n count: pendingUtterances.length,\n waitTime: Date.now() - startTime,\n });\n return;\n }\n\n if (firstTime) {\n firstTime = false;\n // Play notification sound since we're about to start waiting\n await playNotificationSound();\n }\n\n // Wait 100ms before checking again\n await new Promise(resolve => setTimeout(resolve, 100));\n }\n\n // Timeout reached - no utterances found\n lastTimeoutTimestamp = new Date();\n\n res.json({\n success: true,\n utterances: [],\n message: `No utterances found after waiting ${secondsToWait} seconds.`,\n waitTime: maxWaitMs,\n });\n});\n\n// API for the stop hook to check if it should wait\napp.get('/api/should-wait', (req: Request, res: Response) => {\n const shouldWait = !lastTimeoutTimestamp ||\n queue.utterances.some(u => u.timestamp > lastTimeoutTimestamp!);\n\n res.json({ shouldWait });\n});\n\n// API for pre-tool hook to check for pending utterances\napp.get('/api/has-pending-utterances', (req: Request, res: Response) => {\n const pendingCount = queue.utterances.filter(u => u.status === 'pending').length;\n const hasPending = pendingCount > 0;\n\n res.json({\n hasPending,\n pendingCount\n });\n});\n\n// API to clear all utterances\napp.delete('/api/utterances', (req: Request, res: Response) => {\n const clearedCount = queue.utterances.length;\n queue.clear();\n\n res.json({\n success: true,\n message: `Cleared ${clearedCount} utterances`,\n clearedCount\n });\n});\n\napp.get('/', (req: Request, res: Response) => {\n res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));\n});\n\n// Start HTTP server\nconst HTTP_PORT = 3000;\napp.listen(HTTP_PORT, () => {\n console.log(`[HTTP] Server listening on http://localhost:${HTTP_PORT}`);\n console.log(`[Mode] Running in ${IS_MCP_MANAGED ? 'MCP-managed' : 'standalone'} mode`);\n});\n\n// MCP Server Setup (only if MCP-managed)\nif (IS_MCP_MANAGED) {\n console.log('[MCP] Initializing MCP server...');\n\n const mcpServer = new Server(\n {\n name: 'voice-hooks',\n version: '1.0.0',\n },\n {\n capabilities: {\n tools: {},\n },\n }\n );\n\n // Tool handlers\n mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {\n return {\n tools: [\n {\n name: 'dequeue_utterances',\n description: 'Dequeue pending utterances and mark them as delivered',\n inputSchema: {\n type: 'object',\n properties: {\n limit: {\n type: 'number',\n description: 'Maximum number of utterances to dequeue (default: 10)',\n default: 10,\n },\n },\n },\n },\n {\n name: 'wait_for_utterance',\n description: 'Wait for an utterance to be available or until timeout. Returns immediately if no utterances since last timeout.',\n inputSchema: {\n type: 'object',\n properties: {\n seconds_to_wait: {\n type: 'number',\n description: `Maximum seconds to wait for an utterance (default: ${DEFAULT_WAIT_TIMEOUT_SECONDS}, min: ${MIN_WAIT_TIMEOUT_SECONDS}, max: ${MAX_WAIT_TIMEOUT_SECONDS})`,\n default: DEFAULT_WAIT_TIMEOUT_SECONDS,\n minimum: MIN_WAIT_TIMEOUT_SECONDS,\n maximum: MAX_WAIT_TIMEOUT_SECONDS,\n },\n },\n },\n },\n ],\n };\n });\n\n mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n if (name === 'dequeue_utterances') {\n const limit = (args?.limit as number) ?? 10;\n const response = await fetch('http://localhost:3000/api/dequeue-utterances', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ limit }),\n });\n\n const data = await response.json() as any;\n\n if (data.utterances.length === 0) {\n return {\n content: [\n {\n type: 'text',\n text: 'No recent utterances found.',\n },\n ],\n };\n }\n\n return {\n content: [\n {\n type: 'text',\n text: `Dequeued ${data.utterances.length} utterance(s):\\n\\n${data.utterances.reverse().map((u: any) => `\"${u.text}\"\\t[time: ${new Date(u.timestamp).toISOString()}]`).join('\\n')\n }`,\n },\n ],\n };\n }\n\n if (name === 'wait_for_utterance') {\n const requestedSeconds = (args?.seconds_to_wait as number) ?? DEFAULT_WAIT_TIMEOUT_SECONDS;\n const secondsToWait = Math.max(\n MIN_WAIT_TIMEOUT_SECONDS,\n Math.min(MAX_WAIT_TIMEOUT_SECONDS, requestedSeconds)\n );\n debugLog(`[MCP] Calling wait_for_utterance with ${secondsToWait}s timeout`);\n\n const response = await fetch('http://localhost:3000/api/wait-for-utterances', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ seconds_to_wait: secondsToWait }),\n });\n\n const data = await response.json() as any;\n\n if (data.utterances && data.utterances.length > 0) {\n const utteranceTexts = data.utterances\n .map((u: any) => `[${u.timestamp}] \"${u.text}\"`)\n .join('\\n');\n\n return {\n content: [\n {\n type: 'text',\n text: `Found ${data.count} utterance(s):\\n\\n${utteranceTexts}`,\n },\n ],\n };\n } else {\n return {\n content: [\n {\n type: 'text',\n text: data.message || `No utterances found after waiting ${secondsToWait} seconds.`,\n },\n ],\n };\n }\n }\n\n throw new Error(`Unknown tool: ${name}`);\n } catch (error) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n isError: true,\n };\n }\n });\n\n // Connect via stdio\n const transport = new StdioServerTransport();\n mcpServer.connect(transport);\n console.log('[MCP] Server connected via stdio');\n} else {\n console.log('[MCP] Skipping MCP server initialization (not in MCP-managed mode)');\n}"],"mappings":"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","names":[]}