mcp-voice-hooks 1.0.2 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/pre-speak-hook.sh +3 -0
- package/.claude/hooks/pre-tool-hook.sh +2 -29
- package/.claude/hooks/pre-wait-hook.sh +3 -0
- package/.claude/hooks/stop-hook.sh +2 -28
- package/README.md +43 -16
- package/bin/cli.js +138 -10
- 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/unified-server.js +208 -3
- package/dist/unified-server.js.map +1 -1
- package/package.json +5 -3
- package/public/app.js +0 -3
- package/.claude/hooks/post-tool-voice-hook.sh +0 -10
@@ -1,30 +1,3 @@
|
|
1
1
|
#!/bin/bash
|
2
|
-
|
3
|
-
|
4
|
-
# Forces Claude to use dequeue_utterances tool if there are pending utterances
|
5
|
-
|
6
|
-
# Check has-pending-utterances endpoint
|
7
|
-
response=$(curl -s http://localhost:3000/api/has-pending-utterances 2>/dev/null)
|
8
|
-
|
9
|
-
if [ $? -ne 0 ]; then
|
10
|
-
# Server not available, allow tool execution
|
11
|
-
echo '{"decision": "approve"}'
|
12
|
-
exit 0
|
13
|
-
fi
|
14
|
-
|
15
|
-
# Extract pending status
|
16
|
-
hasPending=$(echo "$response" | jq -r '.hasPending')
|
17
|
-
pendingCount=$(echo "$response" | jq -r '.pendingCount')
|
18
|
-
|
19
|
-
if [ "$hasPending" = "true" ]; then
|
20
|
-
# There are pending utterances, block tool execution
|
21
|
-
cat <<EOF
|
22
|
-
{
|
23
|
-
"decision": "block",
|
24
|
-
"reason": "There are $pendingCount pending voice utterances. Please use dequeue_utterances to process them first."
|
25
|
-
}
|
26
|
-
EOF
|
27
|
-
else
|
28
|
-
# No pending utterances, allow tool execution
|
29
|
-
echo '{"decision": "approve"}'
|
30
|
-
fi
|
2
|
+
PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
|
3
|
+
curl -s -X POST http://localhost:${PORT}/api/hooks/pre-tool || echo '{"decision": "approve"}'
|
@@ -1,29 +1,3 @@
|
|
1
1
|
#!/bin/bash
|
2
|
-
|
3
|
-
|
4
|
-
# Checks if there have been any utterances since the last timeout
|
5
|
-
|
6
|
-
# Check should-wait endpoint
|
7
|
-
response=$(curl -s http://localhost:3000/api/should-wait 2>/dev/null)
|
8
|
-
|
9
|
-
if [ $? -ne 0 ]; then
|
10
|
-
# Server not available, allow stop
|
11
|
-
echo '{"decision": "approve"}'
|
12
|
-
exit 0
|
13
|
-
fi
|
14
|
-
|
15
|
-
# Extract shouldWait boolean
|
16
|
-
shouldWait=$(echo "$response" | jq -r '.shouldWait')
|
17
|
-
|
18
|
-
if [ "$shouldWait" = "true" ]; then
|
19
|
-
# There have been utterances since last timeout, block and ask to wait
|
20
|
-
cat <<EOF
|
21
|
-
{
|
22
|
-
"decision": "block",
|
23
|
-
"reason": "Assistant tried to end its response. Stopping is not allowed without first checking for voice input. Assistant should now use wait_for_utterance to check for voice input"
|
24
|
-
}
|
25
|
-
EOF
|
26
|
-
else
|
27
|
-
# No utterances since last timeout, allow stop
|
28
|
-
echo '{"decision": "approve", "reason": "No utterances since last timeout"}'
|
29
|
-
fi
|
2
|
+
PORT="${MCP_VOICE_HOOKS_PORT:-5111}"
|
3
|
+
curl -s -X POST http://localhost:${PORT}/api/hooks/stop || echo '{"decision": "approve"}'
|
package/README.md
CHANGED
@@ -23,17 +23,7 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
23
23
|
|
24
24
|
## Installation in Your Own Project
|
25
25
|
|
26
|
-
1. **
|
27
|
-
|
28
|
-
```bash
|
29
|
-
npx mcp-voice-hooks install-hooks
|
30
|
-
```
|
31
|
-
|
32
|
-
This will:
|
33
|
-
- Install hook scripts to `~/.mcp-voice-hooks/hooks/`
|
34
|
-
- Configure your project's `.claude/settings.json`
|
35
|
-
|
36
|
-
2. **Add the MCP server**:
|
26
|
+
1. **Add the MCP server**:
|
37
27
|
|
38
28
|
Run the following command to automatically add the MCP server to your current project in `~/.claude.json`:
|
39
29
|
|
@@ -56,13 +46,17 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
56
46
|
}
|
57
47
|
```
|
58
48
|
|
59
|
-
|
49
|
+
2. **Start Claude Code**:
|
60
50
|
|
61
51
|
```bash
|
62
52
|
claude
|
63
53
|
```
|
64
54
|
|
65
|
-
|
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.
|
66
60
|
|
67
61
|
The default port is 5111. To use a different port, add to your project's `.claude/settings.json`:
|
68
62
|
|
@@ -74,13 +68,45 @@ mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
|
74
68
|
}
|
75
69
|
```
|
76
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
|
102
|
+
|
77
103
|
## Development Mode
|
78
104
|
|
79
105
|
If you're developing mcp-voice-hooks itself:
|
80
106
|
|
81
107
|
```bash
|
82
108
|
# 1. Clone the repository
|
83
|
-
git clone https://github.com/
|
109
|
+
git clone https://github.com/johnmatthewtennant/mcp-voice-hooks.git
|
84
110
|
cd mcp-voice-hooks
|
85
111
|
|
86
112
|
# 2. Install dependencies
|
@@ -99,8 +125,9 @@ claude
|
|
99
125
|
**Important**: When developing with `npm link`:
|
100
126
|
|
101
127
|
- Claude runs the compiled JavaScript from the `dist` folder, not your TypeScript source
|
102
|
-
- After making changes to
|
103
|
-
-
|
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
|
104
131
|
|
105
132
|
### Hot Reload
|
106
133
|
|
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
|
@@ -124,29 +163,68 @@ async function configureClaudeCodeSettings() {
|
|
124
163
|
"command": "sh ~/.mcp-voice-hooks/hooks/pre-tool-hook.sh"
|
125
164
|
}
|
126
165
|
]
|
127
|
-
}
|
128
|
-
|
129
|
-
|
166
|
+
},
|
167
|
+
{
|
168
|
+
"matcher": "^mcp__voice-hooks__speak$",
|
169
|
+
"hooks": [
|
170
|
+
{
|
171
|
+
"type": "command",
|
172
|
+
"command": "sh ~/.mcp-voice-hooks/hooks/pre-speak-hook.sh"
|
173
|
+
}
|
174
|
+
]
|
175
|
+
},
|
130
176
|
{
|
131
|
-
"matcher": "^mcp__voice-
|
177
|
+
"matcher": "^mcp__voice-hooks__wait_for_utterance$",
|
132
178
|
"hooks": [
|
133
179
|
{
|
134
180
|
"type": "command",
|
135
|
-
"command": "sh ~/.mcp-voice-hooks/hooks/
|
181
|
+
"command": "sh ~/.mcp-voice-hooks/hooks/pre-wait-hook.sh"
|
136
182
|
}
|
137
183
|
]
|
138
184
|
}
|
139
185
|
]
|
140
186
|
};
|
141
187
|
|
142
|
-
//
|
143
|
-
settings.hooks
|
188
|
+
// Replace voice hooks intelligently
|
189
|
+
const updatedHooks = replaceVoiceHooks(settings.hooks || {}, hookConfig);
|
190
|
+
|
191
|
+
// Check if hooks actually changed (ignoring order)
|
192
|
+
if (areHooksEqual(settings.hooks || {}, updatedHooks)) {
|
193
|
+
console.log('✅ Claude settings already up to date');
|
194
|
+
return;
|
195
|
+
}
|
196
|
+
|
197
|
+
// Update settings with new hooks
|
198
|
+
settings.hooks = updatedHooks;
|
144
199
|
|
145
200
|
// Write settings back
|
146
201
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
147
202
|
console.log('✅ Updated project Claude Code settings');
|
148
203
|
}
|
149
204
|
|
205
|
+
// Silent hook installation check - runs on every startup
|
206
|
+
async function ensureHooksInstalled() {
|
207
|
+
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
208
|
+
const hooksDir = path.join(userDir, 'hooks');
|
209
|
+
|
210
|
+
try {
|
211
|
+
console.log('🔄 Updating hooks to latest version...');
|
212
|
+
|
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();
|
220
|
+
await configureClaudeCodeSettings();
|
221
|
+
console.log('✅ Hooks and settings updated');
|
222
|
+
} catch (error) {
|
223
|
+
// Silently continue if hooks can't be updated
|
224
|
+
console.warn('⚠️ Could not auto-update hooks:', error.message);
|
225
|
+
}
|
226
|
+
}
|
227
|
+
|
150
228
|
// Run the MCP server
|
151
229
|
async function runMCPServer() {
|
152
230
|
const serverPath = path.join(__dirname, '..', 'dist', 'unified-server.js');
|
@@ -179,6 +257,56 @@ async function runMCPServer() {
|
|
179
257
|
});
|
180
258
|
}
|
181
259
|
|
260
|
+
// Uninstall MCP Voice Hooks
|
261
|
+
async function uninstall() {
|
262
|
+
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
263
|
+
const claudeSettingsPath = path.join(process.cwd(), '.claude', 'settings.json');
|
264
|
+
|
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
|
275
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
276
|
+
try {
|
277
|
+
console.log('⚙️ Removing voice hooks from Claude settings...');
|
278
|
+
|
279
|
+
const settingsContent = fs.readFileSync(claudeSettingsPath, 'utf8');
|
280
|
+
const settings = JSON.parse(settingsContent);
|
281
|
+
|
282
|
+
if (settings.hooks) {
|
283
|
+
// Remove voice hooks
|
284
|
+
const cleanedHooks = removeVoiceHooks(settings.hooks);
|
285
|
+
|
286
|
+
if (Object.keys(cleanedHooks).length === 0) {
|
287
|
+
// If no hooks remain, remove the hooks property entirely
|
288
|
+
delete settings.hooks;
|
289
|
+
} else {
|
290
|
+
settings.hooks = cleanedHooks;
|
291
|
+
}
|
292
|
+
|
293
|
+
// Write updated settings
|
294
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2));
|
295
|
+
console.log('✅ Removed voice hooks from Claude settings');
|
296
|
+
} else {
|
297
|
+
console.log('ℹ️ No hooks found in Claude settings');
|
298
|
+
}
|
299
|
+
} catch (error) {
|
300
|
+
console.log('⚠️ Could not update Claude settings:', error.message);
|
301
|
+
}
|
302
|
+
} else {
|
303
|
+
console.log('ℹ️ No Claude settings file found in current project');
|
304
|
+
}
|
305
|
+
|
306
|
+
console.log('\n✅ Uninstallation complete!');
|
307
|
+
console.log('👋 MCP Voice Hooks has been removed.');
|
308
|
+
}
|
309
|
+
|
182
310
|
// Run the main function
|
183
311
|
main().catch(error => {
|
184
312
|
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/unified-server.js
CHANGED
@@ -63,6 +63,8 @@ var UtteranceQueue = class {
|
|
63
63
|
var IS_MCP_MANAGED = process.argv.includes("--mcp-managed");
|
64
64
|
var queue = new UtteranceQueue();
|
65
65
|
var lastTimeoutTimestamp = null;
|
66
|
+
var lastToolUseTimestamp = null;
|
67
|
+
var lastSpeakTimestamp = null;
|
66
68
|
var app = express();
|
67
69
|
app.use(cors());
|
68
70
|
app.use(express.json());
|
@@ -196,6 +198,122 @@ app.get("/api/has-pending-utterances", (req, res) => {
|
|
196
198
|
pendingCount
|
197
199
|
});
|
198
200
|
});
|
201
|
+
app.post("/api/validate-action", (req, res) => {
|
202
|
+
const { action } = req.body;
|
203
|
+
const voiceResponsesEnabled = process.env.VOICE_RESPONSES_ENABLED === "true";
|
204
|
+
if (!action || !["tool-use", "stop"].includes(action)) {
|
205
|
+
res.status(400).json({ error: 'Invalid action. Must be "tool-use" or "stop"' });
|
206
|
+
return;
|
207
|
+
}
|
208
|
+
const pendingUtterances = queue.utterances.filter((u) => u.status === "pending");
|
209
|
+
if (pendingUtterances.length > 0) {
|
210
|
+
res.json({
|
211
|
+
allowed: false,
|
212
|
+
requiredAction: "dequeue_utterances",
|
213
|
+
reason: `${pendingUtterances.length} pending utterance(s) must be dequeued first. Please use dequeue_utterances to process them.`
|
214
|
+
});
|
215
|
+
return;
|
216
|
+
}
|
217
|
+
if (voiceResponsesEnabled) {
|
218
|
+
const deliveredUtterances = queue.utterances.filter((u) => u.status === "delivered");
|
219
|
+
if (deliveredUtterances.length > 0) {
|
220
|
+
res.json({
|
221
|
+
allowed: false,
|
222
|
+
requiredAction: "speak",
|
223
|
+
reason: `${deliveredUtterances.length} delivered utterance(s) require voice response. Please use the speak tool to respond before proceeding.`
|
224
|
+
});
|
225
|
+
return;
|
226
|
+
}
|
227
|
+
}
|
228
|
+
if (action === "stop") {
|
229
|
+
const shouldWait = !lastTimeoutTimestamp || queue.utterances.some((u) => u.timestamp > lastTimeoutTimestamp);
|
230
|
+
if (shouldWait) {
|
231
|
+
res.json({
|
232
|
+
allowed: false,
|
233
|
+
requiredAction: "wait_for_utterance",
|
234
|
+
reason: "Assistant tried to end its response. Stopping is not allowed without first checking for voice input. Assistant should now use wait_for_utterance to check for voice input"
|
235
|
+
});
|
236
|
+
return;
|
237
|
+
}
|
238
|
+
}
|
239
|
+
res.json({
|
240
|
+
allowed: true
|
241
|
+
});
|
242
|
+
});
|
243
|
+
function handleHookRequest(attemptedAction) {
|
244
|
+
const voiceResponsesEnabled = process.env.VOICE_RESPONSES_ENABLED === "true";
|
245
|
+
const pendingUtterances = queue.utterances.filter((u) => u.status === "pending");
|
246
|
+
if (pendingUtterances.length > 0) {
|
247
|
+
return {
|
248
|
+
decision: "block",
|
249
|
+
reason: `${pendingUtterances.length} pending utterance(s) must be dequeued first. Please use dequeue_utterances to process them.`
|
250
|
+
};
|
251
|
+
}
|
252
|
+
if (voiceResponsesEnabled) {
|
253
|
+
const deliveredUtterances = queue.utterances.filter((u) => u.status === "delivered");
|
254
|
+
if (deliveredUtterances.length > 0) {
|
255
|
+
if (attemptedAction === "speak") {
|
256
|
+
return { decision: "approve" };
|
257
|
+
}
|
258
|
+
return {
|
259
|
+
decision: "block",
|
260
|
+
reason: `${deliveredUtterances.length} delivered utterance(s) require voice response. Please use the speak tool to respond before proceeding.`
|
261
|
+
};
|
262
|
+
}
|
263
|
+
}
|
264
|
+
if (attemptedAction === "tool") {
|
265
|
+
lastToolUseTimestamp = /* @__PURE__ */ new Date();
|
266
|
+
return { decision: "approve" };
|
267
|
+
}
|
268
|
+
if (attemptedAction === "wait") {
|
269
|
+
if (voiceResponsesEnabled && lastToolUseTimestamp && (!lastSpeakTimestamp || lastSpeakTimestamp < lastToolUseTimestamp)) {
|
270
|
+
return {
|
271
|
+
decision: "block",
|
272
|
+
reason: "Assistant must speak after using tools. Please use the speak tool to respond before waiting for utterances."
|
273
|
+
};
|
274
|
+
}
|
275
|
+
return { decision: "approve" };
|
276
|
+
}
|
277
|
+
if (attemptedAction === "speak") {
|
278
|
+
return { decision: "approve" };
|
279
|
+
}
|
280
|
+
if (attemptedAction === "stop") {
|
281
|
+
if (voiceResponsesEnabled && lastToolUseTimestamp && (!lastSpeakTimestamp || lastSpeakTimestamp < lastToolUseTimestamp)) {
|
282
|
+
return {
|
283
|
+
decision: "block",
|
284
|
+
reason: "Assistant must speak after using tools. Please use the speak tool to respond before proceeding."
|
285
|
+
};
|
286
|
+
}
|
287
|
+
const shouldWait = !lastTimeoutTimestamp || queue.utterances.some((u) => u.timestamp > lastTimeoutTimestamp);
|
288
|
+
if (shouldWait) {
|
289
|
+
return {
|
290
|
+
decision: "block",
|
291
|
+
reason: "Assistant tried to end its response. Stopping is not allowed without first checking for voice input. Assistant should now use wait_for_utterance to check for voice input"
|
292
|
+
};
|
293
|
+
}
|
294
|
+
return {
|
295
|
+
decision: "approve",
|
296
|
+
reason: "No utterances since last timeout"
|
297
|
+
};
|
298
|
+
}
|
299
|
+
return { decision: "approve" };
|
300
|
+
}
|
301
|
+
app.post("/api/hooks/pre-tool", (_req, res) => {
|
302
|
+
const result = handleHookRequest("tool");
|
303
|
+
res.json(result);
|
304
|
+
});
|
305
|
+
app.post("/api/hooks/stop", (_req, res) => {
|
306
|
+
const result = handleHookRequest("stop");
|
307
|
+
res.json(result);
|
308
|
+
});
|
309
|
+
app.post("/api/hooks/pre-speak", (_req, res) => {
|
310
|
+
const result = handleHookRequest("speak");
|
311
|
+
res.json(result);
|
312
|
+
});
|
313
|
+
app.post("/api/hooks/pre-wait", (_req, res) => {
|
314
|
+
const result = handleHookRequest("wait");
|
315
|
+
res.json(result);
|
316
|
+
});
|
199
317
|
app.delete("/api/utterances", (req, res) => {
|
200
318
|
const clearedCount = queue.utterances.length;
|
201
319
|
queue.clear();
|
@@ -205,7 +323,35 @@ app.delete("/api/utterances", (req, res) => {
|
|
205
323
|
clearedCount
|
206
324
|
});
|
207
325
|
});
|
208
|
-
app.
|
326
|
+
app.post("/api/speak", async (req, res) => {
|
327
|
+
const { text } = req.body;
|
328
|
+
if (!text || !text.trim()) {
|
329
|
+
res.status(400).json({ error: "Text is required" });
|
330
|
+
return;
|
331
|
+
}
|
332
|
+
try {
|
333
|
+
await execAsync(`say -r 350 "${text.replace(/"/g, '\\"')}"`);
|
334
|
+
debugLog(`[Speak] Spoke text: "${text}"`);
|
335
|
+
const deliveredUtterances = queue.utterances.filter((u) => u.status === "delivered");
|
336
|
+
deliveredUtterances.forEach((u) => {
|
337
|
+
u.status = "responded";
|
338
|
+
debugLog(`[Queue] marked as responded: "${u.text}" [id: ${u.id}]`);
|
339
|
+
});
|
340
|
+
lastSpeakTimestamp = /* @__PURE__ */ new Date();
|
341
|
+
res.json({
|
342
|
+
success: true,
|
343
|
+
message: "Text spoken successfully",
|
344
|
+
respondedCount: deliveredUtterances.length
|
345
|
+
});
|
346
|
+
} catch (error) {
|
347
|
+
debugLog(`[Speak] Failed to speak text: ${error}`);
|
348
|
+
res.status(500).json({
|
349
|
+
error: "Failed to speak text",
|
350
|
+
details: error instanceof Error ? error.message : String(error)
|
351
|
+
});
|
352
|
+
}
|
353
|
+
});
|
354
|
+
app.get("/", (_req, res) => {
|
209
355
|
res.sendFile(path.join(__dirname, "..", "public", "index.html"));
|
210
356
|
});
|
211
357
|
var HTTP_PORT = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;
|
@@ -213,6 +359,10 @@ app.listen(HTTP_PORT, () => {
|
|
213
359
|
console.log(`[HTTP] Server listening on http://localhost:${HTTP_PORT}`);
|
214
360
|
console.log(`[Mode] Running in ${IS_MCP_MANAGED ? "MCP-managed" : "standalone"} mode`);
|
215
361
|
});
|
362
|
+
function getVoiceResponseReminder() {
|
363
|
+
const voiceResponsesEnabled = process.env.VOICE_RESPONSES_ENABLED === "true";
|
364
|
+
return voiceResponsesEnabled ? "\n\nThe user has enabled voice responses, so use the 'speak' tool to respond to the user's voice input before proceeding." : "";
|
365
|
+
}
|
216
366
|
if (IS_MCP_MANAGED) {
|
217
367
|
console.log("[MCP] Initializing MCP server...");
|
218
368
|
const mcpServer = new Server(
|
@@ -258,6 +408,20 @@ if (IS_MCP_MANAGED) {
|
|
258
408
|
}
|
259
409
|
}
|
260
410
|
}
|
411
|
+
},
|
412
|
+
{
|
413
|
+
name: "speak",
|
414
|
+
description: "Speak text using text-to-speech and mark delivered utterances as responded",
|
415
|
+
inputSchema: {
|
416
|
+
type: "object",
|
417
|
+
properties: {
|
418
|
+
text: {
|
419
|
+
type: "string",
|
420
|
+
description: "The text to speak"
|
421
|
+
}
|
422
|
+
},
|
423
|
+
required: ["text"]
|
424
|
+
}
|
261
425
|
}
|
262
426
|
]
|
263
427
|
};
|
@@ -289,7 +453,7 @@ if (IS_MCP_MANAGED) {
|
|
289
453
|
type: "text",
|
290
454
|
text: `Dequeued ${data.utterances.length} utterance(s):
|
291
455
|
|
292
|
-
${data.utterances.reverse().map((u) => `"${u.text}" [time: ${new Date(u.timestamp).toISOString()}]`).join("\n")}`
|
456
|
+
${data.utterances.reverse().map((u) => `"${u.text}" [time: ${new Date(u.timestamp).toISOString()}]`).join("\n")}${getVoiceResponseReminder()}`
|
293
457
|
}
|
294
458
|
]
|
295
459
|
};
|
@@ -315,7 +479,7 @@ ${data.utterances.reverse().map((u) => `"${u.text}" [time: ${new Date(u.timestam
|
|
315
479
|
type: "text",
|
316
480
|
text: `Found ${data.count} utterance(s):
|
317
481
|
|
318
|
-
${utteranceTexts}`
|
482
|
+
${utteranceTexts}${getVoiceResponseReminder()}`
|
319
483
|
}
|
320
484
|
]
|
321
485
|
};
|
@@ -330,6 +494,47 @@ ${utteranceTexts}`
|
|
330
494
|
};
|
331
495
|
}
|
332
496
|
}
|
497
|
+
if (name === "speak") {
|
498
|
+
const text = args?.text;
|
499
|
+
if (!text || !text.trim()) {
|
500
|
+
return {
|
501
|
+
content: [
|
502
|
+
{
|
503
|
+
type: "text",
|
504
|
+
text: "Error: Text is required for speak tool"
|
505
|
+
}
|
506
|
+
],
|
507
|
+
isError: true
|
508
|
+
};
|
509
|
+
}
|
510
|
+
const response = await fetch(`http://localhost:${HTTP_PORT}/api/speak`, {
|
511
|
+
method: "POST",
|
512
|
+
headers: { "Content-Type": "application/json" },
|
513
|
+
body: JSON.stringify({ text })
|
514
|
+
});
|
515
|
+
const data = await response.json();
|
516
|
+
if (response.ok) {
|
517
|
+
return {
|
518
|
+
content: [
|
519
|
+
{
|
520
|
+
type: "text",
|
521
|
+
text: `Spoke: "${text}"
|
522
|
+
${data.respondedCount > 0 ? `Marked ${data.respondedCount} utterance(s) as responded.` : "No delivered utterances to mark as responded."}`
|
523
|
+
}
|
524
|
+
]
|
525
|
+
};
|
526
|
+
} else {
|
527
|
+
return {
|
528
|
+
content: [
|
529
|
+
{
|
530
|
+
type: "text",
|
531
|
+
text: `Error speaking text: ${data.error || "Unknown error"}`
|
532
|
+
}
|
533
|
+
],
|
534
|
+
isError: true
|
535
|
+
};
|
536
|
+
}
|
537
|
+
}
|
333
538
|
throw new Error(`Unknown tool: ${name}`);
|
334
539
|
} catch (error) {
|
335
540
|
return {
|
@@ -1 +1 @@
|
|
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 = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;\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:${HTTP_PORT}/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:${HTTP_PORT}/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":[]}
|
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' | 'responded';\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;\nlet lastToolUseTimestamp: Date | null = null;\nlet lastSpeakTimestamp: 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// Unified action validation endpoint\napp.post('/api/validate-action', (req: Request, res: Response) => {\n const { action } = req.body;\n const voiceResponsesEnabled = process.env.VOICE_RESPONSES_ENABLED === 'true';\n\n if (!action || !['tool-use', 'stop'].includes(action)) {\n res.status(400).json({ error: 'Invalid action. Must be \"tool-use\" or \"stop\"' });\n return;\n }\n\n // Check for pending utterances (both actions)\n const pendingUtterances = queue.utterances.filter(u => u.status === 'pending');\n if (pendingUtterances.length > 0) {\n res.json({\n allowed: false,\n requiredAction: 'dequeue_utterances',\n reason: `${pendingUtterances.length} pending utterance(s) must be dequeued first. Please use dequeue_utterances to process them.`\n });\n return;\n }\n\n // Check for delivered but unresponded utterances (when voice enabled)\n if (voiceResponsesEnabled) {\n const deliveredUtterances = queue.utterances.filter(u => u.status === 'delivered');\n if (deliveredUtterances.length > 0) {\n res.json({\n allowed: false,\n requiredAction: 'speak',\n reason: `${deliveredUtterances.length} delivered utterance(s) require voice response. Please use the speak tool to respond before proceeding.`\n });\n return;\n }\n }\n\n // For stop action, check if we should wait\n if (action === 'stop') {\n const shouldWait = !lastTimeoutTimestamp ||\n queue.utterances.some(u => u.timestamp > lastTimeoutTimestamp!);\n\n if (shouldWait) {\n res.json({\n allowed: false,\n requiredAction: 'wait_for_utterance',\n reason: 'Assistant tried to end its response. Stopping is not allowed without first checking for voice input. Assistant should now use wait_for_utterance to check for voice input'\n });\n return;\n }\n }\n\n // All checks passed - action is allowed\n res.json({\n allowed: true\n });\n});\n\n// Unified hook handler\nfunction handleHookRequest(attemptedAction: 'tool' | 'speak' | 'wait' | 'stop'): { decision: 'approve' | 'block', reason?: string } {\n const voiceResponsesEnabled = process.env.VOICE_RESPONSES_ENABLED === 'true';\n \n // 1. Check for pending utterances\n const pendingUtterances = queue.utterances.filter(u => u.status === 'pending');\n if (pendingUtterances.length > 0) {\n // Allow dequeue to proceed (dequeue doesn't go through hooks)\n return {\n decision: 'block',\n reason: `${pendingUtterances.length} pending utterance(s) must be dequeued first. Please use dequeue_utterances to process them.`\n };\n }\n \n // 2. Check for delivered utterances (when voice enabled)\n if (voiceResponsesEnabled) {\n const deliveredUtterances = queue.utterances.filter(u => u.status === 'delivered');\n if (deliveredUtterances.length > 0) {\n // Only allow speak to proceed\n if (attemptedAction === 'speak') {\n return { decision: 'approve' };\n }\n return {\n decision: 'block',\n reason: `${deliveredUtterances.length} delivered utterance(s) require voice response. Please use the speak tool to respond before proceeding.`\n };\n }\n }\n \n // 3. Handle tool action\n if (attemptedAction === 'tool') {\n lastToolUseTimestamp = new Date();\n return { decision: 'approve' };\n }\n \n // 4. Handle wait for utterance\n if (attemptedAction === 'wait') {\n if (voiceResponsesEnabled && lastToolUseTimestamp && \n (!lastSpeakTimestamp || lastSpeakTimestamp < lastToolUseTimestamp)) {\n return {\n decision: 'block',\n reason: 'Assistant must speak after using tools. Please use the speak tool to respond before waiting for utterances.'\n };\n }\n return { decision: 'approve' };\n }\n \n // 5. Handle speak\n if (attemptedAction === 'speak') {\n return { decision: 'approve' };\n }\n \n // 6. Handle stop\n if (attemptedAction === 'stop') {\n // Check if must speak after tool use\n if (voiceResponsesEnabled && lastToolUseTimestamp && \n (!lastSpeakTimestamp || lastSpeakTimestamp < lastToolUseTimestamp)) {\n return {\n decision: 'block',\n reason: 'Assistant must speak after using tools. Please use the speak tool to respond before proceeding.'\n };\n }\n \n // Check if should wait for utterances\n const shouldWait = !lastTimeoutTimestamp ||\n queue.utterances.some(u => u.timestamp > lastTimeoutTimestamp!);\n \n if (shouldWait) {\n return {\n decision: 'block',\n reason: 'Assistant tried to end its response. Stopping is not allowed without first checking for voice input. Assistant should now use wait_for_utterance to check for voice input'\n };\n }\n \n return {\n decision: 'approve',\n reason: 'No utterances since last timeout'\n };\n }\n \n // Default to approve (shouldn't reach here)\n return { decision: 'approve' };\n}\n\n// Dedicated hook endpoints that return in Claude's expected format\napp.post('/api/hooks/pre-tool', (_req: Request, res: Response) => {\n const result = handleHookRequest('tool');\n res.json(result);\n});\n\napp.post('/api/hooks/stop', (_req: Request, res: Response) => {\n const result = handleHookRequest('stop');\n res.json(result);\n});\n\n// Pre-speak hook endpoint\napp.post('/api/hooks/pre-speak', (_req: Request, res: Response) => {\n const result = handleHookRequest('speak');\n res.json(result);\n});\n\n// Pre-wait hook endpoint\napp.post('/api/hooks/pre-wait', (_req: Request, res: Response) => {\n const result = handleHookRequest('wait');\n res.json(result);\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\n// API for text-to-speech\napp.post('/api/speak', async (req: Request, res: Response) => {\n const { text } = req.body;\n\n if (!text || !text.trim()) {\n res.status(400).json({ error: 'Text is required' });\n return;\n }\n\n try {\n // Execute text-to-speech using macOS say command\n await execAsync(`say -r 350 \"${text.replace(/\"/g, '\\\\\"')}\"`);\n debugLog(`[Speak] Spoke text: \"${text}\"`);\n\n // Mark all delivered utterances as responded\n const deliveredUtterances = queue.utterances.filter(u => u.status === 'delivered');\n deliveredUtterances.forEach(u => {\n u.status = 'responded';\n debugLog(`[Queue] marked as responded: \"${u.text}\"\t[id: ${u.id}]`);\n });\n\n // Track that speak was called\n lastSpeakTimestamp = new Date();\n\n res.json({\n success: true,\n message: 'Text spoken successfully',\n respondedCount: deliveredUtterances.length\n });\n } catch (error) {\n debugLog(`[Speak] Failed to speak text: ${error}`);\n res.status(500).json({\n error: 'Failed to speak text',\n details: error instanceof Error ? error.message : String(error)\n });\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 = process.env.MCP_VOICE_HOOKS_PORT ? parseInt(process.env.MCP_VOICE_HOOKS_PORT) : 5111;\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// Helper function to get voice response reminder\nfunction getVoiceResponseReminder(): string {\n const voiceResponsesEnabled = process.env.VOICE_RESPONSES_ENABLED === 'true';\n return voiceResponsesEnabled\n ? '\\n\\nThe user has enabled voice responses, so use the \\'speak\\' tool to respond to the user\\'s voice input before proceeding.'\n : '';\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 name: 'speak',\n description: 'Speak text using text-to-speech and mark delivered utterances as responded',\n inputSchema: {\n type: 'object',\n properties: {\n text: {\n type: 'string',\n description: 'The text to speak',\n },\n },\n required: ['text'],\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:${HTTP_PORT}/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 }${getVoiceResponseReminder()}`,\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:${HTTP_PORT}/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}${getVoiceResponseReminder()}`,\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 if (name === 'speak') {\n const text = args?.text as string;\n\n if (!text || !text.trim()) {\n return {\n content: [\n {\n type: 'text',\n text: 'Error: Text is required for speak tool',\n },\n ],\n isError: true,\n };\n }\n\n const response = await fetch(`http://localhost:${HTTP_PORT}/api/speak`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ text }),\n });\n\n const data = await response.json() as any;\n\n if (response.ok) {\n return {\n content: [\n {\n type: 'text',\n text: `Spoke: \"${text}\"\\n${data.respondedCount > 0 ? `Marked ${data.respondedCount} utterance(s) as responded.` : 'No delivered utterances to mark as responded.'}`,\n },\n ],\n };\n } else {\n return {\n content: [\n {\n type: 'text',\n text: `Error speaking text: ${data.error || 'Unknown error'}`,\n },\n ],\n isError: true,\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":[]}
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "mcp-voice-hooks",
|
3
|
-
"version": "1.0.
|
3
|
+
"version": "1.0.8",
|
4
4
|
"main": "dist/index.js",
|
5
5
|
"type": "module",
|
6
6
|
"bin": {
|
@@ -40,9 +40,9 @@
|
|
40
40
|
"url": "git+https://github.com/johnmatthewtennant/mcp-voice-hooks.git"
|
41
41
|
},
|
42
42
|
"bugs": {
|
43
|
-
"url": "https://github.com/
|
43
|
+
"url": "https://github.com/johnmatthewtennant/mcp-voice-hooks/issues"
|
44
44
|
},
|
45
|
-
"homepage": "https://github.com/
|
45
|
+
"homepage": "https://github.com/johnmatthewtennant/mcp-voice-hooks#readme",
|
46
46
|
"dependencies": {
|
47
47
|
"@modelcontextprotocol/sdk": "^1.13.3",
|
48
48
|
"cors": "^2.8.5",
|
@@ -53,10 +53,12 @@
|
|
53
53
|
"@types/express": "^5.0.3",
|
54
54
|
"@types/jest": "^30.0.0",
|
55
55
|
"@types/node": "^24.0.10",
|
56
|
+
"@types/supertest": "^6.0.3",
|
56
57
|
"husky": "^9.1.7",
|
57
58
|
"jest": "^29.7.0",
|
58
59
|
"knip": "^5.61.3",
|
59
60
|
"nodemon": "^3.1.10",
|
61
|
+
"supertest": "^7.1.1",
|
60
62
|
"ts-jest": "^29.4.0",
|
61
63
|
"ts-node": "^10.9.2",
|
62
64
|
"ts-prune": "^0.10.3",
|
package/public/app.js
CHANGED
@@ -1,10 +0,0 @@
|
|
1
|
-
#!/bin/bash
|
2
|
-
|
3
|
-
# Check if voice responses are enabled (defaults to false)
|
4
|
-
VOICE_RESPONSES_ENABLED="${VOICE_RESPONSES_ENABLED:-false}"
|
5
|
-
|
6
|
-
if [ "$VOICE_RESPONSES_ENABLED" == "true" ]; then
|
7
|
-
echo "Use the 'say' command to respond to the user's voice input before proceeding." >&2
|
8
|
-
echo "Example: bash -c 'say -r 300 \"I understand your request. I'll start working on...\"'" >&2
|
9
|
-
exit 2
|
10
|
-
fi
|