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.
@@ -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
@@ -0,0 +1,12 @@
1
+ {
2
+ "mcpServers": {
3
+ "voice-hooks": {
4
+ "type": "stdio",
5
+ "command": "npx",
6
+ "args": [
7
+ "mcp-voice-hooks"
8
+ ],
9
+ "env": {}
10
+ }
11
+ }
12
+ }
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
+ [![Voice Hooks Demo](https://img.youtube.com/vi/KpkxvJ65gbM/0.jpg)](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":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }
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":"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","names":[]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node