mcp-voice-hooks 1.0.0
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/hook-check.sh +15 -0
- package/.claude/hooks/post-tool-voice-hook.sh +10 -0
- package/.claude/hooks/pre-tool-hook.sh +30 -0
- package/.claude/hooks/stop-hook.sh +29 -0
- package/.mcp.json +12 -0
- package/README.md +147 -0
- package/bin/cli.js +186 -0
- package/dist/chunk-IYGM5COW.js +12 -0
- package/dist/chunk-IYGM5COW.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/unified-server.d.ts +1 -0
- package/dist/unified-server.js +352 -0
- package/dist/unified-server.js.map +1 -0
- package/mcp-voice-hooks-1.0.0.tgz +0 -0
- package/package.json +66 -0
- package/public/app.js +321 -0
- package/public/index.html +322 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
response=$(curl -s -X POST localhost:3000/api/dequeue-utterances 2>/dev/null)
|
3
|
+
if [[ "$response" == *'"utterances":['* && "$response" != *'"utterances":[]'* ]]; then
|
4
|
+
# Extract and format utterances as TSV
|
5
|
+
echo "=== Voice Input Received ===" >&2
|
6
|
+
echo "$response" | jq -r '.utterances[] | "\(.timestamp)\t\(.text)"' | while IFS=$'\t' read -r timestamp text; do
|
7
|
+
# Format timestamp to be more readable
|
8
|
+
formatted_time=$(echo "$timestamp" | sed 's/T/ /' | sed 's/\.[0-9]*Z$//')
|
9
|
+
printf "%s\t%s\n" "${formatted_time}" "${text}" >&2
|
10
|
+
done
|
11
|
+
echo "=== End of Voice Input ===" >&2
|
12
|
+
exit 2
|
13
|
+
else
|
14
|
+
exit 0
|
15
|
+
fi
|
@@ -0,0 +1,10 @@
|
|
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
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# Pre-Tool Hook - Checks for pending utterances before allowing tool execution
|
4
|
+
# Forces Claude to use dequeue_utterances tool if there are pending utterances
|
5
|
+
|
6
|
+
# 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
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
# Stop Hook - Intelligently decides whether to wait for voice input
|
4
|
+
# Checks if there have been any utterances since the last timeout
|
5
|
+
|
6
|
+
# 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
|
package/.mcp.json
ADDED
package/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# mcp-voice-hooks
|
2
|
+
|
3
|
+
Real-time voice interaction for Claude Code. Speak naturally while Claude works - interrupt, redirect, or provide continuous feedback without stopping.
|
4
|
+
|
5
|
+
## Demo
|
6
|
+
|
7
|
+
[](https://youtu.be/KpkxvJ65gbM)
|
8
|
+
|
9
|
+
## Overview
|
10
|
+
|
11
|
+
mcp-voice-hooks enables continuous voice conversations with AI assistants by:
|
12
|
+
|
13
|
+
- Capturing voice input in real-time through a web interface
|
14
|
+
- Queuing utterances for processing by Claude Code
|
15
|
+
- Using hooks to ensure Claude checks for voice input before tool use and before stopping
|
16
|
+
- Allowing natural interruptions like "No, stop that" or "Wait, try something else"
|
17
|
+
|
18
|
+
## Features
|
19
|
+
|
20
|
+
- 🎤 **Real-time Voice Capture**: Browser-based speech recognition with automatic segmentation
|
21
|
+
- 🔄 **Continuous Interaction**: Keep talking while Claude works - no need to stop between commands
|
22
|
+
- 🪝 **Smart Hook System**: Pre-tool and stop hooks ensure Claude always checks for your input
|
23
|
+
|
24
|
+
## Installation in Your Own Project
|
25
|
+
|
26
|
+
1. **Install the hooks** (first time only):
|
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** to your project's `.mcp.json`:
|
37
|
+
|
38
|
+
```json
|
39
|
+
{
|
40
|
+
"mcpServers": {
|
41
|
+
"voice-hooks": {
|
42
|
+
"type": "stdio",
|
43
|
+
"command": "npx",
|
44
|
+
"args": ["mcp-voice-hooks"],
|
45
|
+
"env": {}
|
46
|
+
}
|
47
|
+
}
|
48
|
+
}
|
49
|
+
```
|
50
|
+
|
51
|
+
3. **Start Claude Code**:
|
52
|
+
|
53
|
+
```bash
|
54
|
+
claude
|
55
|
+
```
|
56
|
+
|
57
|
+
4. **Open the voice interface** at <http://localhost:3000> and start speaking! Note: you need to send one text message to Claude to trigger the voice hooks.
|
58
|
+
|
59
|
+
## Development Mode
|
60
|
+
|
61
|
+
If you're developing mcp-voice-hooks itself:
|
62
|
+
|
63
|
+
```bash
|
64
|
+
# 1. Clone the repository
|
65
|
+
git clone https://github.com/yourusername/mcp-voice-hooks.git
|
66
|
+
cd mcp-voice-hooks
|
67
|
+
|
68
|
+
# 2. Install dependencies
|
69
|
+
npm install
|
70
|
+
|
71
|
+
# 3. Link the package locally
|
72
|
+
npm link
|
73
|
+
|
74
|
+
# 4. Install hooks (one time)
|
75
|
+
npx mcp-voice-hooks install-hooks
|
76
|
+
|
77
|
+
# 5. Start Claude Code
|
78
|
+
claude
|
79
|
+
```
|
80
|
+
|
81
|
+
NOTE: You need to restart Claude Code each time you make changes.
|
82
|
+
|
83
|
+
### Hot Reload
|
84
|
+
|
85
|
+
For hot reload during development, you can run the development server with
|
86
|
+
|
87
|
+
```bash
|
88
|
+
npm run dev-unified
|
89
|
+
```
|
90
|
+
|
91
|
+
and then configure claude to use the mcp proxy like so:
|
92
|
+
|
93
|
+
```json
|
94
|
+
{
|
95
|
+
"mcpServers": {
|
96
|
+
"voice-hooks": {
|
97
|
+
"type": "stdio",
|
98
|
+
"command": "npm",
|
99
|
+
"args": ["run", "mcp-proxy"],
|
100
|
+
"env": {}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
}
|
104
|
+
```
|
105
|
+
|
106
|
+
## WIP voice responses
|
107
|
+
|
108
|
+
Add the post tool hook to your claude settings:
|
109
|
+
|
110
|
+
```json
|
111
|
+
{
|
112
|
+
{
|
113
|
+
"hooks": {
|
114
|
+
"PostToolUse": [
|
115
|
+
{
|
116
|
+
"matcher": "^mcp__voice-hooks__",
|
117
|
+
"hooks": [
|
118
|
+
{
|
119
|
+
"type": "command",
|
120
|
+
"command": "./.claude/hooks/post-tool-voice-hook.sh"
|
121
|
+
}
|
122
|
+
]
|
123
|
+
}
|
124
|
+
]
|
125
|
+
},
|
126
|
+
"env": {
|
127
|
+
"VOICE_RESPONSES_ENABLED": "true"
|
128
|
+
}
|
129
|
+
}
|
130
|
+
}
|
131
|
+
```
|
132
|
+
|
133
|
+
### Configuration
|
134
|
+
|
135
|
+
Voice responses are disabled by default. To enable them:
|
136
|
+
|
137
|
+
Add to your Claude Code settings JSON:
|
138
|
+
|
139
|
+
```json
|
140
|
+
{
|
141
|
+
"env": {
|
142
|
+
"VOICE_RESPONSES_ENABLED": "true"
|
143
|
+
}
|
144
|
+
}
|
145
|
+
```
|
146
|
+
|
147
|
+
To disable voice responses, set the value to `false` or remove the setting entirely.
|
package/bin/cli.js
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
import fs from 'fs';
|
4
|
+
import path from 'path';
|
5
|
+
import os from 'os';
|
6
|
+
import { spawn } from 'child_process';
|
7
|
+
import { fileURLToPath } from 'url';
|
8
|
+
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
10
|
+
const __dirname = path.dirname(__filename);
|
11
|
+
|
12
|
+
// Main entry point for npx mcp-voice-hooks
|
13
|
+
async function main() {
|
14
|
+
const args = process.argv.slice(2);
|
15
|
+
const command = args[0];
|
16
|
+
|
17
|
+
try {
|
18
|
+
if (command === 'install-hooks') {
|
19
|
+
console.log('🔧 Installing MCP Voice Hooks...');
|
20
|
+
|
21
|
+
// Step 1: Ensure user directory exists and install/update hooks
|
22
|
+
await ensureUserDirectorySetup();
|
23
|
+
|
24
|
+
// Step 2: Configure Claude Code settings automatically
|
25
|
+
await configureClaudeCodeSettings();
|
26
|
+
|
27
|
+
console.log('\n✅ Installation complete!');
|
28
|
+
console.log('📝 To start the server, run: npx mcp-voice-hooks');
|
29
|
+
} else {
|
30
|
+
// Default behavior: just run the MCP server
|
31
|
+
console.log('🎤 MCP Voice Hooks - Starting server...');
|
32
|
+
console.log('💡 Note: If hooks are not installed, run: npx mcp-voice-hooks install-hooks');
|
33
|
+
console.log('');
|
34
|
+
await runMCPServer();
|
35
|
+
}
|
36
|
+
} catch (error) {
|
37
|
+
console.error('❌ Error:', error.message);
|
38
|
+
process.exit(1);
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
// Ensure ~/.mcp-voice-hooks/hooks/ directory exists and contains latest hook files
|
43
|
+
async function ensureUserDirectorySetup() {
|
44
|
+
const userDir = path.join(os.homedir(), '.mcp-voice-hooks');
|
45
|
+
const hooksDir = path.join(userDir, 'hooks');
|
46
|
+
|
47
|
+
console.log('📁 Setting up user directory:', userDir);
|
48
|
+
|
49
|
+
// Create directories if they don't exist
|
50
|
+
if (!fs.existsSync(userDir)) {
|
51
|
+
fs.mkdirSync(userDir, { recursive: true });
|
52
|
+
console.log('✅ Created user directory');
|
53
|
+
}
|
54
|
+
|
55
|
+
if (!fs.existsSync(hooksDir)) {
|
56
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
57
|
+
console.log('✅ Created hooks directory');
|
58
|
+
}
|
59
|
+
|
60
|
+
// Copy/update hook files from the package's .claude/hooks/ to user directory
|
61
|
+
const packageHooksDir = path.join(__dirname, '..', '.claude', 'hooks');
|
62
|
+
|
63
|
+
if (fs.existsSync(packageHooksDir)) {
|
64
|
+
const hookFiles = fs.readdirSync(packageHooksDir).filter(file => file.endsWith('.sh'));
|
65
|
+
|
66
|
+
for (const hookFile of hookFiles) {
|
67
|
+
const sourcePath = path.join(packageHooksDir, hookFile);
|
68
|
+
const destPath = path.join(hooksDir, hookFile);
|
69
|
+
|
70
|
+
// Copy hook file
|
71
|
+
fs.copyFileSync(sourcePath, destPath);
|
72
|
+
console.log(`✅ Updated hook: ${hookFile}`);
|
73
|
+
}
|
74
|
+
} else {
|
75
|
+
console.log('⚠️ Package hooks directory not found, skipping hook installation');
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
// Automatically configure Claude Code settings
|
80
|
+
async function configureClaudeCodeSettings() {
|
81
|
+
const claudeDir = path.join(process.cwd(), '.claude');
|
82
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
83
|
+
|
84
|
+
console.log('⚙️ Configuring project Claude Code settings...');
|
85
|
+
|
86
|
+
// Create .claude directory if it doesn't exist
|
87
|
+
if (!fs.existsSync(claudeDir)) {
|
88
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
89
|
+
console.log('✅ Created project .claude directory');
|
90
|
+
}
|
91
|
+
|
92
|
+
// Read existing settings or create new
|
93
|
+
let settings = {};
|
94
|
+
if (fs.existsSync(settingsPath)) {
|
95
|
+
try {
|
96
|
+
const settingsContent = fs.readFileSync(settingsPath, 'utf8');
|
97
|
+
settings = JSON.parse(settingsContent);
|
98
|
+
console.log('📖 Read existing settings');
|
99
|
+
} catch (error) {
|
100
|
+
console.log('⚠️ Error reading existing settings, creating new');
|
101
|
+
settings = {};
|
102
|
+
}
|
103
|
+
}
|
104
|
+
|
105
|
+
// Add hook configuration
|
106
|
+
const hookConfig = {
|
107
|
+
"Stop": [
|
108
|
+
{
|
109
|
+
"matcher": "",
|
110
|
+
"hooks": [
|
111
|
+
{
|
112
|
+
"type": "command",
|
113
|
+
"command": "sh ~/.mcp-voice-hooks/hooks/stop-hook.sh"
|
114
|
+
}
|
115
|
+
]
|
116
|
+
}
|
117
|
+
],
|
118
|
+
"PreToolUse": [
|
119
|
+
{
|
120
|
+
"matcher": "^(?!mcp__voice-hooks__).*",
|
121
|
+
"hooks": [
|
122
|
+
{
|
123
|
+
"type": "command",
|
124
|
+
"command": "sh ~/.mcp-voice-hooks/hooks/pre-tool-hook.sh"
|
125
|
+
}
|
126
|
+
]
|
127
|
+
}
|
128
|
+
],
|
129
|
+
"PostToolUse": [
|
130
|
+
{
|
131
|
+
"matcher": "^mcp__voice-hooks__",
|
132
|
+
"hooks": [
|
133
|
+
{
|
134
|
+
"type": "command",
|
135
|
+
"command": "sh ~/.mcp-voice-hooks/hooks/post-tool-voice-hook.sh"
|
136
|
+
}
|
137
|
+
]
|
138
|
+
}
|
139
|
+
]
|
140
|
+
};
|
141
|
+
|
142
|
+
// Update settings
|
143
|
+
settings.hooks = hookConfig;
|
144
|
+
|
145
|
+
// Write settings back
|
146
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
147
|
+
console.log('✅ Updated project Claude Code settings');
|
148
|
+
}
|
149
|
+
|
150
|
+
// Run the MCP server
|
151
|
+
async function runMCPServer() {
|
152
|
+
const serverPath = path.join(__dirname, '..', 'src', 'unified-server.ts');
|
153
|
+
|
154
|
+
// Use ts-node to run the TypeScript server
|
155
|
+
const child = spawn('npx', ['ts-node', '--esm', serverPath, '--mcp-managed'], {
|
156
|
+
stdio: 'inherit',
|
157
|
+
cwd: path.join(__dirname, '..')
|
158
|
+
});
|
159
|
+
|
160
|
+
child.on('error', (error) => {
|
161
|
+
console.error('❌ Failed to start MCP server:', error.message);
|
162
|
+
process.exit(1);
|
163
|
+
});
|
164
|
+
|
165
|
+
child.on('exit', (code) => {
|
166
|
+
console.log(`🔄 MCP server exited with code ${code}`);
|
167
|
+
process.exit(code);
|
168
|
+
});
|
169
|
+
|
170
|
+
// Handle graceful shutdown
|
171
|
+
process.on('SIGINT', () => {
|
172
|
+
console.log('\n🛑 Shutting down...');
|
173
|
+
child.kill('SIGINT');
|
174
|
+
});
|
175
|
+
|
176
|
+
process.on('SIGTERM', () => {
|
177
|
+
console.log('\n🛑 Shutting down...');
|
178
|
+
child.kill('SIGTERM');
|
179
|
+
});
|
180
|
+
}
|
181
|
+
|
182
|
+
// Run the main function
|
183
|
+
main().catch(error => {
|
184
|
+
console.error('❌ Unexpected error:', error);
|
185
|
+
process.exit(1);
|
186
|
+
});
|
@@ -0,0 +1,12 @@
|
|
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
|
@@ -0,0 +1 @@
|
|
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":[]}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
@@ -0,0 +1,125 @@
|
|
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
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"sources":["../src/utterance-queue.ts","../src/http-server.ts","../src/index.ts"],"sourcesContent":["import { Utterance, UtteranceQueue } from './types.js';\nimport { randomUUID } from 'crypto';\nimport { debugLog } from './debug.js';\n\nexport class InMemoryUtteranceQueue implements UtteranceQueue {\n public utterances: Utterance[] = [];\n\n add(text: string, timestamp?: Date): Utterance {\n const utterance: Utterance = {\n id: randomUUID(),\n text: text.trim(),\n timestamp: timestamp || new Date(),\n status: 'pending'\n };\n \n this.utterances.push(utterance);\n debugLog(`[Queue] queued:\t\"${utterance.text}\"\t[id: ${utterance.id}]`);\n return utterance;\n }\n\n getRecent(limit: number = 10): Utterance[] {\n return this.utterances\n .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())\n .slice(0, limit);\n }\n\n markDelivered(id: string): void {\n const utterance = this.utterances.find(u => u.id === id);\n if (utterance) {\n utterance.status = 'delivered';\n debugLog(`[Queue] delivered:\t\"${utterance.text}\"\t[id: ${id}]`);\n }\n }\n\n clear(): void {\n const count = this.utterances.length;\n this.utterances = [];\n debugLog(`[Queue] Cleared ${count} utterances`);\n }\n}","import express from 'express';\nimport cors from 'cors';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { InMemoryUtteranceQueue } from './utterance-queue.js';\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = path.dirname(__filename);\n\nexport class HttpServer {\n private app: express.Application;\n private utteranceQueue: InMemoryUtteranceQueue;\n private port: number;\n\n constructor(utteranceQueue: InMemoryUtteranceQueue, port: number = 3000) {\n this.utteranceQueue = utteranceQueue;\n this.port = port;\n this.app = express();\n this.setupMiddleware();\n this.setupRoutes();\n }\n\n private setupMiddleware() {\n this.app.use(cors());\n this.app.use(express.json());\n this.app.use(express.static(path.join(__dirname, '..', 'public')));\n }\n\n private setupRoutes() {\n // API Routes\n this.app.post('/api/potential-utterances', (req: express.Request, res: express.Response) => {\n const { text, timestamp } = req.body;\n \n if (!text || !text.trim()) {\n res.status(400).json({ error: 'Text is required' });\n return;\n }\n\n const parsedTimestamp = timestamp ? new Date(timestamp) : undefined;\n const utterance = this.utteranceQueue.add(text, parsedTimestamp);\n res.json({\n success: true,\n utterance: {\n id: utterance.id,\n text: utterance.text,\n timestamp: utterance.timestamp,\n status: utterance.status,\n },\n });\n });\n\n this.app.get('/api/utterances', (req: express.Request, res: express.Response) => {\n const limit = parseInt(req.query.limit as string) || 10;\n const utterances = this.utteranceQueue.getRecent(limit);\n \n res.json({\n utterances: utterances.map(u => ({\n id: u.id,\n text: u.text,\n timestamp: u.timestamp,\n status: u.status,\n })),\n });\n });\n\n this.app.get('/api/utterances/status', (req: express.Request, res: express.Response) => {\n const total = this.utteranceQueue.utterances.length;\n const pending = this.utteranceQueue.utterances.filter(u => u.status === 'pending').length;\n const delivered = this.utteranceQueue.utterances.filter(u => u.status === 'delivered').length;\n\n res.json({\n total,\n pending,\n delivered,\n });\n });\n\n // Serve the browser client\n this.app.get('/', (req: express.Request, res: express.Response) => {\n res.sendFile(path.join(__dirname, '..', 'public', 'index.html'));\n });\n }\n\n start(): Promise<void> {\n return new Promise((resolve) => {\n this.app.listen(this.port, () => {\n console.log(`HTTP Server running on http://localhost:${this.port}`);\n resolve();\n });\n });\n }\n}","import { InMemoryUtteranceQueue } from './utterance-queue.js';\nimport { HttpServer } from './http-server.js';\n\nasync function main() {\n // Shared utterance queue between HTTP and MCP servers\n const utteranceQueue = new InMemoryUtteranceQueue();\n \n // Start HTTP server for browser client\n const httpServer = new HttpServer(utteranceQueue);\n await httpServer.start();\n \n // Note: MCP server runs separately via `npm run mcp` command\n \n console.log('Voice Hooks servers ready!');\n console.log('- HTTP server: http://localhost:3000');\n console.log('- MCP server: Ready for stdio connection');\n}\n\nmain().catch(console.error);"],"mappings":";;;;;AACA,SAAS,kBAAkB;AAGpB,IAAM,yBAAN,MAAuD;AAAA,EACrD,aAA0B,CAAC;AAAA,EAElC,IAAI,MAAc,WAA6B;AAC7C,UAAM,YAAuB;AAAA,MAC3B,IAAI,WAAW;AAAA,MACf,MAAM,KAAK,KAAK;AAAA,MAChB,WAAW,aAAa,oBAAI,KAAK;AAAA,MACjC,QAAQ;AAAA,IACV;AAEA,SAAK,WAAW,KAAK,SAAS;AAC9B,aAAS,oBAAoB,UAAU,IAAI,UAAU,UAAU,EAAE,GAAG;AACpE,WAAO;AAAA,EACT;AAAA,EAEA,UAAU,QAAgB,IAAiB;AACzC,WAAO,KAAK,WACT,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,QAAQ,IAAI,EAAE,UAAU,QAAQ,CAAC,EAC5D,MAAM,GAAG,KAAK;AAAA,EACnB;AAAA,EAEA,cAAc,IAAkB;AAC9B,UAAM,YAAY,KAAK,WAAW,KAAK,OAAK,EAAE,OAAO,EAAE;AACvD,QAAI,WAAW;AACb,gBAAU,SAAS;AACnB,eAAS,uBAAuB,UAAU,IAAI,UAAU,EAAE,GAAG;AAAA,IAC/D;AAAA,EACF;AAAA,EAEA,QAAc;AACZ,UAAM,QAAQ,KAAK,WAAW;AAC9B,SAAK,aAAa,CAAC;AACnB,aAAS,mBAAmB,KAAK,aAAa;AAAA,EAChD;AACF;;;ACvCA,OAAO,aAAa;AACpB,OAAO,UAAU;AACjB,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAG9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,KAAK,QAAQ,UAAU;AAElC,IAAM,aAAN,MAAiB;AAAA,EACd;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,gBAAwC,OAAe,KAAM;AACvE,SAAK,iBAAiB;AACtB,SAAK,OAAO;AACZ,SAAK,MAAM,QAAQ;AACnB,SAAK,gBAAgB;AACrB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEQ,kBAAkB;AACxB,SAAK,IAAI,IAAI,KAAK,CAAC;AACnB,SAAK,IAAI,IAAI,QAAQ,KAAK,CAAC;AAC3B,SAAK,IAAI,IAAI,QAAQ,OAAO,KAAK,KAAK,WAAW,MAAM,QAAQ,CAAC,CAAC;AAAA,EACnE;AAAA,EAEQ,cAAc;AAEpB,SAAK,IAAI,KAAK,6BAA6B,CAAC,KAAsB,QAA0B;AAC1F,YAAM,EAAE,MAAM,UAAU,IAAI,IAAI;AAEhC,UAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,GAAG;AACzB,YAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,mBAAmB,CAAC;AAClD;AAAA,MACF;AAEA,YAAM,kBAAkB,YAAY,IAAI,KAAK,SAAS,IAAI;AAC1D,YAAM,YAAY,KAAK,eAAe,IAAI,MAAM,eAAe;AAC/D,UAAI,KAAK;AAAA,QACP,SAAS;AAAA,QACT,WAAW;AAAA,UACT,IAAI,UAAU;AAAA,UACd,MAAM,UAAU;AAAA,UAChB,WAAW,UAAU;AAAA,UACrB,QAAQ,UAAU;AAAA,QACpB;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAED,SAAK,IAAI,IAAI,mBAAmB,CAAC,KAAsB,QAA0B;AAC/E,YAAM,QAAQ,SAAS,IAAI,MAAM,KAAe,KAAK;AACrD,YAAM,aAAa,KAAK,eAAe,UAAU,KAAK;AAEtD,UAAI,KAAK;AAAA,QACP,YAAY,WAAW,IAAI,QAAM;AAAA,UAC/B,IAAI,EAAE;AAAA,UACN,MAAM,EAAE;AAAA,UACR,WAAW,EAAE;AAAA,UACb,QAAQ,EAAE;AAAA,QACZ,EAAE;AAAA,MACJ,CAAC;AAAA,IACH,CAAC;AAED,SAAK,IAAI,IAAI,0BAA0B,CAAC,KAAsB,QAA0B;AACtF,YAAM,QAAQ,KAAK,eAAe,WAAW;AAC7C,YAAM,UAAU,KAAK,eAAe,WAAW,OAAO,OAAK,EAAE,WAAW,SAAS,EAAE;AACnF,YAAM,YAAY,KAAK,eAAe,WAAW,OAAO,OAAK,EAAE,WAAW,WAAW,EAAE;AAEvF,UAAI,KAAK;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAGD,SAAK,IAAI,IAAI,KAAK,CAAC,KAAsB,QAA0B;AACjE,UAAI,SAAS,KAAK,KAAK,WAAW,MAAM,UAAU,YAAY,CAAC;AAAA,IACjE,CAAC;AAAA,EACH;AAAA,EAEA,QAAuB;AACrB,WAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,WAAK,IAAI,OAAO,KAAK,MAAM,MAAM;AAC/B,gBAAQ,IAAI,2CAA2C,KAAK,IAAI,EAAE;AAClE,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AACF;;;ACxFA,eAAe,OAAO;AAEpB,QAAM,iBAAiB,IAAI,uBAAuB;AAGlD,QAAM,aAAa,IAAI,WAAW,cAAc;AAChD,QAAM,WAAW,MAAM;AAIvB,UAAQ,IAAI,4BAA4B;AACxC,UAAQ,IAAI,sCAAsC;AAClD,UAAQ,IAAI,0CAA0C;AACxD;AAEA,KAAK,EAAE,MAAM,QAAQ,KAAK;","names":[]}
|
@@ -0,0 +1 @@
|
|
1
|
+
#!/usr/bin/env node
|